Node.js 后端 + 简易运维岗---面试全套指南(1)

事件 & 异步

一、Node 单线程 + 事件循环

1)Node 是单线程?但为什么能高并发?

要候选人答出这三点:

  1. JS 主线程只有 1 个(执行代码、处理回调)
  2. I/O 操作(网络、文件、数据库)不阻塞主线程,交给 libuv 线程池
  3. 结果回来后扔到事件队列,主线程空了再执行

一句话判断水平:

  • 垃圾:"Node 多线程,所以快"

  • 合格:"单线程 + 非阻塞 I/O + 事件循环"

  • 优秀:区分 JS 主线程libuv 工作线程

    2)事件循环 6 个阶段(顺序固定)

  • Node 事件循环是分阶段、按顺序跑的,每一轮循环叫一个 tick。

    顺序:

  • timers:setTimeout、setInterval

  • pending callbacks:上一轮没执行完的系统回调(如 TCP 错误)

  • idle/prepare:内部用

  • poll (最重要):

    • 获取新 I/O 事件
    • 阻塞等待直到有事件
  • check:setImmediate

  • close callbacks:close 事件(socket.on ('close'))

二、宏任务 / 微任务(Node 与浏览器不一样)

1)执行优先级(铁律)

每一个阶段执行前 / 后,都会清空微任务队列!

顺序:

  1. 同步代码
  2. 所有微任务(全部清空)
  3. 宏任务(取一个执行)
  4. 再清空微任务
  5. 进入下一个阶段

2)宏任务、微任务分别有哪些

微任务(Microtasks)

  • Promise.then/catch/finally
  • process.nextTick(比 Promise 还早!)
  • async/await 本质就是 Promise + 微任务

宏任务(Macrotasks)

  • setTimeout / setInterval
  • setImmediate
  • I/O(文件、网络、数据库)
  • DOM 事件(浏览器)
复制代码
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(()=>console.log('setTimeout'),0)

async1()

new Promise(resolve=>{
  console.log('Promise1')
  resolve()
}).then(()=>console.log('Promise2'))

console.log('script end')


正确顺序(Node & 浏览器一致):
script start
async1 start
async2
Promise1
script end
async1 end
Promise2
setTimeout
能答对 = 异步基础扎实。

三、回调 / Promise /async/await 实际项目怎么选型?

1)回调(callback)

  • 现在只用于老库、底层 API
  • 绝对禁止业务代码嵌套 → 回调地狱
  • 缺点:异常难捕获、嵌套深、控制流混乱

2)Promise

  • 解决回调地狱
  • 适合:并行、并发、多个异步组合
  • .then 链式清晰

3)async/await(现在标准写法)

  • 业务代码 99% 用这个
  • 同步写法,可读性最强
  • 可以 try/catch 捕获异常

面试官标准答案:

  • 业务逻辑:async/await 优先
  • 并发控制:Promise.all / race / allSettled
  • 老库兼容:包装成 Promise 再用 async/await

四、Promise.all/race /allSettled 场景 & 坑

1)Promise.all

  • 所有成功才成功,一个失败直接整体失败
  • 场景:
    • 同时请求多个接口,必须全部成功才继续
    • 批量查询、批量初始化
  • **大坑:**一个失败,全部失败,无法拿到成功部分结果。

all 中一个失败,其他请求还在跑吗?→ 还在跑,只是不会返回结果

all 并发量太大,把服务打挂→ 必须做限流、分批

2)Promise.race

  • 谁快返回谁,不管成功失败
  • 场景:
  • 请求超时控制
复制代码
  Promise.race([
    fetch(url),
    new Promise((_,reject)=>setTimeout(()=>reject('timeout'),5000))
  ])

3)Promise.allSettled(ES2020)

  • 全部执行完,不管成功失败
  • 返回数组:{ status: 'fulfilled/rejected', value/reason }
  • 场景:
    • 批量上报、批量任务,不关心个别失败

五、你面试时可以直接问的 5 个灵魂问题

  1. Node 事件循环 6 个阶段顺序?
  2. 微任务和宏任务执行顺序?process.nextTick 优先级?
  3. setTimeout 0 和 setImmediate 谁先?为什么不确定?
  4. 业务里你用 callback / Promise /async/await 怎么选?
  5. Promise.all、race、allSettled 区别与实际场景?

内存 & 性能 & 安全

一、内存 & 性能

1)常见内存泄漏场景 + 怎么排查

(1)常见泄漏点(候选人必须能说出)

  1. 全局变量滥用

    • 意外挂载到 global / windowthis 指向错乱
    • 引用一直存在,GC 无法回收
  2. 闭包持有意外引用

    • 闭包引用了大对象、request、response、数据库连接
    • 函数退出了,但引用还在
  3. 定时器 / 异步任务不清理

    • setIntervalsetTimeoutclear
    • 里面持有大引用,整个作用域无法回收
  4. 事件监听不销毁

    • EventEmittersocketstream 持续 onoff
    • 每次请求加监听,越积越多
  5. 缓存不设上限 / 不淘汰

    • 简单对象缓存无限增长
    • 未用 LRU、TTL
  6. 数据库连接 / 文件句柄未释放

    • 每次请求新建连接,不释放
    • 造成内存 + 句柄双泄漏

(2)怎么排查(面试官必问)

标准流程:

  1. 复现:压测 / 流量上涨 → 内存持续涨不回落
  2. 抓堆快照
    • --expose-gc
    • chrome://inspect 调试
    • 取 2~3 次堆快照对比
  3. 看几点
    • 对象数量持续增长
    • 哪个构造器 / 闭包持有引用
    • GC 无法回收的路径
  4. 工具
  • Chrome DevTools Memory

  • clinic.js

  • memwatch-next

  • process.memoryUsage()

合格回答:知道抓快照、看引用链、定位闭包 / 全局 / 定时器。

2)Stream 是什么?大文件为什么必须用流?

(1)Stream 本质

  • 把数据切成一小块一小块(chunk) 流动处理
  • 不一次性加载到内存
  • 四种流:
    • Readable 可读
    • Writable 可写
    • Duplex 双工
    • Transform 转换(压缩、加密)

(2)为什么大文件要用 Stream?

  • 不用流:fs.readFile把整个文件丢进内存
    • 文件 1GB → 直接爆内存、OOM
  • 用流:边读边处理边发,内存只占一个 chunk 大小
  • 适用:
    • 大文件上传 / 下载
    • 日志切割
    • 视频 / 音频处理
    • 接口大数据导出

(3)经典坑

  • 背压问题(backpressure) 读得快、写得慢 → 内存积压解决:pipe() 自动处理背压,不要自己手动 on('data')

3)Node 常见安全漏洞 & 防御

(1)XSS

  • 场景:用户输入直接渲染到页面
  • 防御:
    • 对输出转义(&<>"')
    • 使用模板引擎自带转义(ejs/nunjucks 等)
    • 接口返回 JSON 不直接拼接 HTML

(2)SQL 注入

  • 场景:直接拼接 SQL
  • 防御:
    • 禁止字符串拼接 SQL
    • 使用 参数化查询 / 预处理语句
    • ORM(Sequelize、Mongoose、Prisma)

(3)参数篡改 & 越权

  • 场景:前端传 id=1,后端直接用,导致查别人数据
  • 防御:
    • 所有接口必须鉴权
    • 数据权限:只能操作 userId = 当前登录ID
    • 关键参数(userId、role、admin)不从前端传,从 Token / 会话里取

(4)接口鉴权绕过

  • 防御:
    • JWT 校验过期、签名
    • 中间件统一鉴权,不要每个接口手写
    • 敏感接口(删除、修改)加二次验证

(5)其他

  • 目录遍历:../ 穿透 → 规范化路径、限制访问目录
  • 拒绝服务:大 body 攻击 → 限制 body 大小
  • 敏感信息:不要把栈错误返回前端

二、cluster 多进程 & PM2 优化 CPU

1)为什么要用多进程?

  • Node JS 主线程单线程,只能跑满 1 核
  • 服务器多核 CPU 浪费
  • 用 cluster 启动多实例 = 利用全部核心

2)cluster 原理

  • master 主进程:管理、负载均衡
  • worker 子进程:真正执行业务
  • 端口可以共享(内核转发)

3)PM2 怎么配置

标准用法:

bash

运行

复制代码
pm2 start app.js -i max
  • -i max:自动使用 CPU 全部核心
  • -i 2:指定 2 个进程

4)优化要点

  1. 进程数 = CPU 核心数(不要超,否则上下文切换损耗)
  2. 不要在 master 做业务,只做调度
  3. 进程间不要共享内存,用 Redis 共享状态
  4. PM2 开启:
    • 日志切割
    • 内存超限自动重启
    • 开机自启

5)误区

  • 开过多进程 = 更慢
  • 单进程里开多线程意义不大(CPU 密集才需要)

你作为面试官可以直接问的 8 个问题(非常准)

  • 你在项目中遇到过内存泄漏吗?哪些场景最容易漏?
  • 内存泄漏排查步骤是什么?用什么工具?
  • 大文件上传为什么不能用 readFile?Stream 好在哪?
  • 什么是背压?怎么解决?
  • SQL 注入怎么防?你写 SQL 用拼接还是参数化?
  • 接口越权怎么发生?怎么避免?
  • 为什么 Node 要开多进程?单进程为什么用不满多核?
  • PM2 启动参数 -i max 是什么意思?

三、网络 & 接口(高频必问)

1)HTTP 1.1 / HTTP 2 区别 + Keep‑Alive

Keep-Alive 作用(HTTP 1.1 默认开启)

  • 复用 TCP 连接,不用每次请求重新握手
  • 减少 TCP 三次握手、四次挥手开销
  • 降低延迟,提升并发性能

面试官追问: 没有 Keep‑Alive 会怎样?→ 每个请求都新建连接,大量 TIME_WAIT,服务器压力大,速度慢。


HTTP 1.1 缺点

  • 串行请求(队头阻塞)
  • 同一域名最多 6~8 个连接
  • 纯文本,体积大
  • 无优先级

HTTP 2 优点(必须记住 4 点)

  1. 多路复用:一个连接并行多个请求 - 响应,解决队头阻塞
  2. 二进制分帧:不再是文本协议,解析更快
  3. 头部压缩 HPACK:减少体积
  4. 流优先级:重要资源优先加载

**一句话区分:**1.1:多个 TCP 连接,串行排队2:一个 TCP 连接,并行乱序传输


2)跨域 CORS 底层原理 + Node 配置

什么是跨域?

协议、域名、端口任意一个不同,浏览器就拦截。

CORS 底层(面试官最爱问)

  1. 浏览器先判断是否跨域
  2. 简单请求 :直接发,带 Origin
  3. 非简单请求 (PUT/DELETE/Content-Type: application/json、自定义头):→ 先发 OPTIONS 预检请求→ 服务器返回允许的源、头、方法→ 浏览器通过才发真实请求

服务器关键响应头

  • Access-Control-Allow-Origin: * 或指定域名

  • Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS

  • Access-Control-Allow-Headers: Content-Type,Authorization

  • Access-Control-Allow-Credentials: true(允许带 cookie)

    Node 端怎么配(Koa/Express 通用思路)

    1)手写中间件(最能体现水平)

    js

    复制代码
    app.use(async (ctx, next) => {
      ctx.set('Access-Control-Allow-Origin', ctx.header.origin || '*')
      ctx.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
      ctx.set('Access-Control-Allow-Headers', 'Content-Type,Authorization')
      ctx.set('Access-Control-Allow-Credentials', 'true')
      if (ctx.method === 'OPTIONS') {
        ctx.status = 204
        return
      }
      await next()
    })
    2)用库

    Express:corsKoa:@koa/cors

    高频坑

  • 带 Cookie 时,Allow-Origin 不能为 *,必须具体域名

  • 没处理 OPTIONS,预检直接报错 404

3)JWT vs Session 鉴权区别 + 过期刷新

1)Session(基于 Cookie)

  • 存在 服务器内存 / Redis
  • 客户端只存 sessionId 在 Cookie
  • 优点:安全、可主动踢人、修改密码立即失效
  • 缺点:服务器有状态、分布式需要共享存储(Redis)

2)JWT(JSON Web Token)

  • 信息存在 客户端
  • 服务器只用 验签,不用存状态
  • 结构:Header.Payload.Signature
  • 优点:无状态、易扩展、多域名 / 微服务友好
  • 缺点:
    • 无法主动作废(除非放黑名单)
    • 不能存敏感信息(base64 可解码)

什么时候用 JWT?

  • 前后端分离
  • 微服务、多域名
  • APP、小程序、第三方授权
  • 不想依赖 Redis 共享 Session

3)JWT 过期 & 刷新方案(标准做法)

  • Access Token:短有效期(15min~2h),每次请求带
  • Refresh Token:长有效期(7d~30d),只用于刷新

流程:

  1. 登录 → 返回 accessToken + refreshToken
  2. accessToken 过期 → 前端 401 拦截
  3. 请求 /refresh 接口,用 refreshToken 换新 accessToken
  4. refreshToken 过期 → 重新登录

安全要点

  • 密钥足够复杂
  • Payload 不要放密码、敏感信息
  • 开启 exp 过期时间
  • 支持刷新但不永久有效
  • 可在 Redis 存黑名单实现注销

面试官可直接问的 8 道题

  • Keep‑Alive 解决什么问题?
  • HTTP 1.1 队头阻塞是什么?HTTP2 怎么解决?
  • 什么是预检 OPTIONS?什么时候触发?
  • 带 Cookie 跨域要配置哪几个头?
  • Session 和 JWT 本质区别是什么?
  • JWT 存在客户端,怎么保证不被篡改?
  • JWT 过期了怎么办?刷新机制怎么设计?
  • JWT 怎么实现用户退出登录 / 强制下线?
相关推荐
Olivia_0_0_2 小时前
【面试题】C++面试题整理——具身智能 / 自动驾驶 / 嵌入式 / 后台开发通用
c++·面试
冬瓜神君2 小时前
Token 预估这件小事:使用HuggingFace Tokenizers精准预估上下文Tokens
node.js·huggingface·tiktoken·tokens预估
虹科网络安全2 小时前
艾体宝洞察|NPM供应链攻击:复杂的多链加密货币攻擊渗透流行软件包
前端·npm·node.js
zjeweler10 小时前
“网安+护网”终极300多问题面试笔记-3共3-综合题型(最多)
笔记·网络安全·面试·职场和发展·护网行动
Hacker_Nightrain11 小时前
详解Selenium 和Playwright两大框架的不同之处
自动化测试·软件测试·selenium·测试工具·职场和发展
鹿角片ljp11 小时前
最长回文子串(LeetCode 5)详解
算法·leetcode·职场和发展
逻辑驱动的ken12 小时前
Java高频面试题:03
java·开发语言·面试·求职招聘·春招
ayqy贾杰15 小时前
Claude Code 重构,并行化或终结 IDE 时代
前端·javascript·面试
知识浅谈16 小时前
OpenClaw保姆级安装教程:基于ubuntu系统
linux·ubuntu·node.js