MST

星途 面试题库

面试题:Node.js Express中间件在高并发场景下的优化与设计

在一个高并发的Express应用中,中间件需要处理大量的请求。假设存在一个中间件用于记录请求日志,随着并发量的增加,文件I/O操作可能会成为性能瓶颈。请设计一种优化方案,既能保证准确记录日志,又能最大程度减少对系统性能的影响。同时,阐述如何在中间件设计中考虑资源竞争和线程安全问题,以确保应用在高并发场景下的稳定性和可靠性。
42.3万 热度难度
前端开发Node.js

知识考点

AI 面试

面试题答案

一键面试

优化方案

  1. 使用日志队列
    • 引入一个队列(如async - queue),当请求到达记录日志的中间件时,将日志信息放入队列,而不是直接进行文件I/O操作。这样可以避免大量请求同时竞争文件I/O资源。
    • 示例代码:
    const asyncQueue = require('async - queue');
    const logQueue = asyncQueue((logEntry, callback) => {
        // 这里进行实际的文件写入操作
        fs.appendFile('log.txt', logEntry + '\n', (err) => {
            if (err) {
                console.error('Error writing to log file:', err);
            }
            callback();
        });
    }, 1); // 并发数设为1,确保顺序写入
    
    app.use((req, res, next) => {
        const logEntry = `${new Date().toISOString()} ${req.method} ${req.url}`;
        logQueue.push(logEntry);
        next();
    });
    
  2. 异步写入
    • 使用Node.js的异步文件I/O操作(如fs.appendFile的异步版本),这样在进行文件写入时不会阻塞事件循环,从而不影响其他请求的处理。
  3. 日志缓存
    • 可以设置一个缓存(如内存缓存),当缓存达到一定大小(如100条日志)或者达到一定时间间隔(如1秒),再批量将缓存中的日志写入文件。这减少了文件I/O的频率。
    • 示例代码:
    let logCache = [];
    const cacheInterval = 1000; // 1秒
    const cacheSize = 100;
    
    setInterval(() => {
        if (logCache.length > 0) {
            const logString = logCache.join('\n');
            fs.appendFile('log.txt', logString + '\n', (err) => {
                if (err) {
                    console.error('Error writing cached logs to file:', err);
                }
            });
            logCache = [];
        }
    }, cacheInterval);
    
    app.use((req, res, next) => {
        const logEntry = `${new Date().toISOString()} ${req.method} ${req.url}`;
        logCache.push(logEntry);
        if (logCache.length >= cacheSize) {
            const logString = logCache.join('\n');
            fs.appendFile('log.txt', logString + '\n', (err) => {
                if (err) {
                    console.error('Error writing cached logs to file:', err);
                }
            });
            logCache = [];
        }
        next();
    });
    

资源竞争和线程安全问题考虑

  1. 队列和并发控制
    • 如上述使用队列时,将队列的并发数设置为1(asyncQueue((logEntry, callback) => { /*... */ }, 1)),确保日志是顺序写入文件的,避免多个写入操作同时进行导致数据错乱。
  2. 锁机制(在需要时)
    • 如果在缓存写入等操作中有共享资源(如共享的缓存数组),可以使用简单的锁机制。例如,设置一个标志位表示是否正在进行写入操作,在写入前检查并设置标志位,写入完成后清除标志位。
    let isWriting = false;
    const writeCacheToFile = () => {
        if (isWriting) {
            return;
        }
        isWriting = true;
        const logString = logCache.join('\n');
        fs.appendFile('log.txt', logString + '\n', (err) => {
            if (err) {
                console.error('Error writing cached logs to file:', err);
            }
            isWriting = false;
            logCache = [];
        });
    };
    
  3. 使用线程安全的库
    • 选择线程安全的日志库(如winston等),这些库在设计上已经考虑了多线程或高并发场景下的安全问题,能有效避免资源竞争。例如,winston支持多种日志传输(包括文件传输),并且在内部处理了并发写入等问题。
    const winston = require('winston');
    
    const logger = winston.createLogger({
        level: 'info',
        format: winston.format.json(),
        transports: [
            new winston.transport.Console(),
            new winston.transport.File({ filename: 'log.txt' })
        ]
    });
    
    app.use((req, res, next) => {
        logger.info(`${new Date().toISOString()} ${req.method} ${req.url}`);
        next();
    });