跳到主要内容

认识

一、认识


二、问题


2.1 Node.js 服务是如何保证稳定性的?

一、Node 部署: 基于 Docker 容器化与 Kubernetes 编排, 将单机器 PM2 管理的 Node.js 服务迁移至多机器 Kubernetes 集群。PM2 是一个流行的进程管理工具,专门用于管理 Node.js 应用程序。虽然 PM2 对于 Node.js 应用的部署与管理非常有效,但相比于 KubernetesPM2 更适用于单机环境或者较小的服务集群。以下是二者的主要区别和优势对比:

  • PM2PM2 主要用于单节点的进程管理,虽然支持多进程模式(通过 pm2 scale 命令),但其本质上是管理本地机器上的 Node.js 进程,不能自动扩展到多个机器,也无法进行跨节点的服务管理。

  • KubernetesKubernetes 是一个分布式系统平台,能够管理跨多个节点的服务部署、扩展和管理。通过 PodsDeploymentsReplicaSets,可以轻松实现自动扩容、滚动更新、负载均衡等功能,适用于大规模分布式应用。

  • PM2, PM2 的核心工作机制是基于 进程管理守护进程 的实现,主要包括以下几个方面:

    1. 守护进程(Daemon Process: PM2 通过启动一个独立的 守护进程(Daemon 来管理所有的 应用进程。守护进程常驻后台,负责 监控重启管理 所有子进程。当执行 pm2 start app.js 命令时,PM2 会首先检查守护进程是否存在,如果不存在,则启动守护进程。PM2 CLI 将用户的启动指令通过 IPC 通信 发送给 守护进程。守护进程收到指令后,通过 child_process.fork 启动一个新的子进程运行 app.js守护进程用户应用进程 分离,保证应用进程异常退出后,守护进程可以检测到并自动重启。守护进程 会保存应用进程的状态到文件中(~/.pm2/dump.pm2),即使服务器重启也能恢复进程状态。

    2. Cluster 模式(多进程模式): 适合多核 CPU 的场景。PM2 支持 Cluster 模式,利用 Node.js 提供的 cluster 模块实现多进程并发。PM2 会启动一个 Master 进程,再 fork 出多个 Worker 子进程(基于 IPC 机制与 Master 通信),每个子进程都会运行同一个服务脚本(app.js), 所有子进程共享同一个端口。主进程通过 Round-Robin(轮询)调度算法实现将请求分发到不同的子进程中,实现负载均衡。利用多核 CPU,提高服务的性能和并发能力。通过 IPC 机制MasterWorker 之间传递消息。

    3. 自动故障恢复与重启机制: PM2 内置故障检测机制,当应用进程崩溃或退出时,守护进程会自动重启它。确保服务持续运行,减少人工干预。可以通过 --max-memory-restart 限制进程内存使用,超过阈值后自动重启,防止内存泄漏。具体实现为: 守护进程监听子进程的退出事件,

  1. PM2 自动捕获应用的标准输出(stdout)和错误输出(stderr),并将日志保存在文件中。PM2 提供 pm2 monit 命令监控 CPU、内存等资源使用情况。与外部监控工具集成(如 PM2 PlusPrometheus),实现自定义监控和告警。
  • Kubernetes 集群

    1. 基于 Deployment 来快速部署多台机器上的 Node.js 服务 Pod, 通过配置 replicas 使用多副本部署,保证服务不会因单个 Pod 故障而中断。配置 LivenessProbe 自动检测不健康的容器并重启, 使服务具有故障恢复的功能。配置 ReadinessProbe 确保服务准备好接收流量, 做好负载准备。设置 resources 限制资源使用,防止资源耗尽。基于 Pod Anti-AffinitynodeAffinity 将副本分布在不同节点,避免单点故障,做到服务容灾和高可用。

    2. 基于 Service 使用 NodePortLoadBalancer 将服务暴露给外部,并结合 ReadinessProbe 确保 Service 仅将流量分发给健康的 Pod, 保证服务稳定和服务高可用。

    3. Ingress: 基于 NGINX Ingress Controller 部署服务,通过路径和域名规则进行流量分发

    4. Horizontal Pod Autoscaler (HPA): 通过 HPA 基于 CPU 和内存使用率动态扩展 Pod,并结合 Prometheus Adapter 支持获取 Prometheus 自定义指标, HPA 使用这些自定义指标来自动扩展 Pod,提升资源利用率。对于突发流量,可与 Cluster Autoscaler 根据 Pod 扩容自动增加或减少节点,联动扩展节点资源以解决节点资源不足的问题。

二、Node.js 监控告警: Prometheus 会周期性地拉取并高效存储收集的数据、指标。结合 Grafana 进行数据可视化, 帮助开发团队实时监控应用性能、健康状况以及资源使用情况。同时,利用 Alertmanager 配置告警策略,确保在关键指标异常时能及时通知相关人员进行处理

  • 性能指标

    • 事件循环延迟 (Event Loop Delay):监控 Node.js 事件循环的延迟,评估系统性能。事件循环延迟是监控 Node.js 性能的一个关键指标,直接反映了系统是否出现性能瓶颈。使用 perf_hooks.monitorEventLoopDelay 来获取事件循环延迟的详细统计(需要 Node.js 10.10 以上版本)。如果版本不支持,可以使用 process.hrtime() 来手动计算事件循环延迟。

    • 垃圾回收 (GC):监控垃圾回收的行为和对应用性能的影响。使用 perf_hooks.PerformanceObserver 监控垃圾回收的持续时间、频率和类型。监控 GC 可帮助我们理解内存清理和优化的需要。

    • 内存使用情况:包括堆内存和非堆内存的使用,帮助监控内存泄漏和资源消耗。使用 process.memoryUsage() 来获取内存使用情况(rss, heapUsed, heapTotal, external)。使用 v8.getHeapStatistics() 获取 V8 堆的信息。

    • CPU 使用率:监控 CPU 的整体使用情况,识别是否存在 CPU 密集型操作。使用 os.cpus()process.cpuUsage() 监控 CPU 使用率。可以计算每个核的使用情况,获取 CPU 使用时间,帮助识别 CPU 密集型任务。

    • CPU 负载:通过 CPU 负载来判断系统资源是否充足,避免过载。通过 Node.js 中的 os.loadavg() 方法获取系统的负载平均值(即 1 分钟、5 分钟和 15 分钟的负载平均值)

  • 服务状态指标

    • 平均响应时间 (ART):监控服务的响应速度,及时发现性能瓶颈。使用中间件来测量每个请求的响应时间, 帮助定位慢请求

    • 每秒事务数 (TPS):衡量系统的处理能力,反映应用的吞吐量。使用中间件来监控请求数,计算每秒事务数(TPS)。

    • 每秒请求数 (QPS):用于监控服务的请求量,及时发现流量异常。使用中间件来监控请求数,计算每秒请求数(QPS)。

    • 真实请求数 (Real QPS):仅统计成功的请求数,用于评估服务的实际处理能力。使用中间件来监控请求数,计算每秒真实请求数(QPS)。

  • 系统资源监控

    • 句柄数 (Handlers):监控系统打开的文件句柄和网络连接数,确保资源不会被耗尽。通常指的是进程中打开的各种资源,如文件描述符、TCP/UDP 套接字、进程的 I/O 等等。每个句柄都占用一些内存,并且可以影响系统的性能。process._getActiveHandles() 返回当前 Node.js 进程的所有活动句柄(如 TCPUDP、定时器、请求等)

三、内存泄漏检测:

  • 测试环境: 通过 node --inspect 开启 Node 调试模式, 打开 Chrome 浏览器,在地址栏输入 chrome://inspect, 打开专用 DevTools 窗口, 根据可疑时间点, 录制内存快照(Heap Snapshot)。 通过 Summary 或者 Comparison 来定位可疑对象。在 Comparison 视图中选择两个堆快照,并在它们之间进行比较。您可以查看哪些对象在两个堆快照之间新增,哪些对象在两个堆快照之间减少,以及哪些对象的大小发生了变化。Comparison 视图还允许查看对象之间的关系,以及对象的详细信息,如类型、大小和引用计数。通过这些信息,可以了解哪些对象是导致内存泄漏的原因。在 Comparison 视图中查找可疑对象,用于比较当前快照和之前快照的内存变化。主要关注 Comparison.Delta 对象数量的净增量和 Comparison.Size Delta 对象分配的内存变化量。然后通过 Comparison.Retainers 引用链 显示某个对象被哪些对象引用,为什么无法被回收。

  • 生产环境: heapdump 模块可以在运行时生成堆快照。堆快照可导入到 Chrome DevTools 进行分析,找到泄漏原因。监控内存使用情况,自动生成 heap snapshot,并将快照上传到远程服务器。它还包含了手动生成 heap snapshot 和触发垃圾回收(GC)的功能,帮助开发者定位和排查内存泄漏问题。

    1. 应用启动时, 生成启动的 Heapdump 快照, 并开始定时监控内存使用情况

    2. 针对可疑路由添加 memoryUsageMiddlewaretrackRequestCountMiddleware 中间件。memoryUsageMiddleware:定期检查内存使用情况并触发生成 heap snapshot, 比如内存每达到 100M 生成一次 heap snapshottrackRequestCountMiddleware:根据请求计数触发 heap snapshot, 比如每 请求 100 次生成一次 heap snapshot

    3. 生成的 heap snapshot 会保存在本地目录 heapdump, 并通过 scp 上传到指定的远程服务器目录。生成 heap snapshot 会主动调用 gc。通过 v8 global.gc() 允许你主动触发垃圾回收,让 V8 进行内存清理。这对于 诊断内存泄漏生成更准确的堆快照 非常有用。在某些场景下,通过手动触发 GC,可以释放不再使用的内存,确保堆快照中只保留真实的内存占用情况, 如果手动触发 GC 后,内存仍然无法释放,说明存在未被回收的对象,即可能存在内存泄漏。

    4. 提供了 /heap-snapshot 接口,开发者可以手动触发 heap snapshot 的生成。提供了 /trigger-gc 开发者可以手动触发垃圾回收。

  • 但是在生产环境中, Heapdump 生成快照 会消耗大量的内存和 CPU,从而会影响服务的正常运行。有时候, Node 服务会中断,根据当时服务器内存大小这个时间会在 2 ~ 30min 左右。确保服务高可用性和生成 Heapdump 快照的核心策略是将快照生成的资源消耗隔离。

    1. 可以通过扩展 Pod 副本数,通过负载均衡消除单个 Pod 性能下降的影响。: 通过 ServiceNode.js 服务暴露给外部访问,并结合 Deployment ReadinessProbe 确保 Service 仅将流量分发给健康的 Pod, 保证服务稳定和服务高可用。配置Deployment replicas, 配置 Node.js Pod 多个副本, 在生成 Heapdump 之前,通过 Kubernetes 修改 PodreadinessProbe,让流量从负载均衡中移除, 将当前 Pod 的流量隔离,防止因性能下降影响请求响应。这样即使某个 Pod 性能受损,服务流量会自动分配到其他健康的 Pod,保证服务的高可用性。

    2. 可以使用 child_process 独立进程分离快照生成逻辑,避免主服务受影响。: 将生成 Heapdump 的任务交给一个独立的进程,避免主服务受到影响。快照生成由独立的脚本或服务运行。主服务通过异步调用启动快照生成服务。

    3. 可以使用 Job 或 分离快照生成逻辑,避免主服务受影响。: 通过 KubernetesJobCronJob 创建临时 Pod 专门用于生成 Heapdump,而不是在主服务 Pod 中生成。Heapdump 的生成在独立的 Pod 中进行,完全不影响主服务。

2.2 Node.js 做过哪些工作?做过哪些性能优化?

Node BFF 工作、以及性能优化如下:

一、HTML 模版分发: 输入 umu.cn 经过缓存检测、DNS 解析TCP 连接HTTP 请求 最终会走到 Node BFF, 首先进行登录鉴权,检测当前登录状态、当前用户角色。检测通过, 我们会在 HTML 模版中注入用户、页面基础数据, 返回给客户端。将最终的模版通过 Redis 进行缓存, 并基于模版类型和使用频率设置 TTL 过期时间, 添加随机抖动防止缓存雪崩。那有时可能会遇到 RedisMongoDB 数据不一致的问题, 我们采用了 旁路缓存模式 来解决, 获取数据时, 首先检查 Redis 缓存, 如果命中直接返回, 如果未命中, 查询数据库, 并写入缓存, 设置合理 TTL 加随机抖动防止缓存雪崩。更新数据时, 先更新数据库, 再删除缓存(这里不更新缓存, 直接删除)。我们的 HTML 模板缓存减少了平均响应时间 65%

二、微前端子应用静态资源分发: QianKunMicroApp 的入口都会进入 Node BFF 层, 我们在 Node BFF 层请求资源, 对资源进行处理, 比如说扫描公共依赖、压缩资源、注入数据后, 返回给客户端。

三、接口二次封装、合并: 对于一些基础服务接口, 通过 Node BFF 进行合并, 减少 HTTP 请求。或者对服务端请求进行二次封装, 提前处理。

四、提供截图服务: 我们对于截图有两种方案, 一种是基于 DomToImage 前端实现截图, 一种是在 Node BFF 通过 Puppeteer 给定宽高或者元素 id 进行截图。但是, 在生产环境中, Puppeteer 作为截图服务面临的主要挑战是内存占用高且易泄漏, 每个Chrome实例可能占用数百 MB内存, 大量并发请求可能导致服务宕机。我做了如下控制: 1. 维护单一全局 Puppeteer 浏览器实例, 这样可以避免多实例资源浪费; 2. 然后预创建固定数量页面实例, 尽可能实现复用这些页面实例, 而不是频繁创建/销毁, 再根据使用率指标动态调整这些预创建页面实例的大小, 繁忙时扩容, 空闲时收缩, 每次使用后清理页面状态防止内存累积; 3. 检测当前内存使用情况, 系统内存使用率 >80% 时暂停处理, 连续3次超限自动重启浏览器。进行并发控制, 将每个截图请求加入优先级队列, 限制同时执行的截图任务数量; 4. 设置任务超时机制, 防止长时间运行任务占用资源, 批处理相似请求, 多个相同 URL 请求共享一次页面加载, 实现结果缓存, 短时间内相同参数请求直接返回缓存结果。补充、扩展: 1. 如何查看内存使用情况: 使用 process.memoryUsage() 来获取内存使用情况(rss, heapUsed, heapTotal, external); 使用 v8.getHeapStatistics() 获取 V8 堆的信息。2. Puppeteer 底层原理: PuppeteerGoogle Chrome 团队开发的 Node.js 库, 本质上是一个浏览器自动化工具。核心优势在于它提供了一个端到端测试自动化、网页抓取和截图的高级 API, 同时保持对底层CDP协议的完全访问能力, 使开发者能够精确控制浏览器行为, 实现各种复杂的自动化任务。

五、AI 服务: 基于 Redis Sub 订阅 AI 团队生成结果, AI 生成结果后, 通过 Pub 发布消息。并对生成结果进行 Redis 缓存, 并设置过期时间, 后面加一个随机时间, 防止缓存雪崩。另外, 我们对单个用户进行限流, 防止暴力请求。每个 用户 + IP 对应一个 Redis 键, 在成功放行请求时, 将该键设置为存在, TTL300 秒。其他限流的方案有: 令牌桶限流、固定窗口限流、滑动窗口限流。如果 AI 预计的生成时间超过 3 分钟, 我们会通过 web-push 发送浏览器通知。

六、平台用户数据管理, 针对平台用户数据, 比如说用户信息、用户收藏、用户最近使用等进行 CRUD 管理。并对首页多个模块, 基于 aggregate$lookup 管道、$facet 子管道高效聚合多个模块数据。并优化聚合管道, 我们可以在阶段顺序上, $match 应尽早执行,减少后续阶段的数据量, $group 尽量靠后,避免不必要的计算, $project 一般在管道的最后,控制返回的字段; 合并可合并的阶段; 另外, 我们可以增加索引, 确保 $match$lookup 关联字段有索引,否则会导致全表扫描; 我们可以使用 $facet 来实现并行统计。同样, 将聚合结果缓存到 Redis, 并建立合适的 TTL 以及抖动。

七、基于 promClient 来收集 内存使用情况CPU 使用率CPU 负载垃圾回收 (GC)事件循环延迟 (Event Loop Delay) 以及 每秒请求数 (QPS)平均响应时间 (ART) 等。Prometheus 会定期调用 promClient 提供的 ``HTTP** 服务, 来收集、存储数据。通过 **Grafana` 数据可视化, 查看采集的数据。

八、基于 heapdump 生成内存快照, heapdump 模块可以在运行时生成堆快照。堆快照可导入到 Chrome DevTools 进行分析,找到泄漏原因。监控内存使用情况,自动生成 heap snapshot,并将快照上传到远程服务器。它还包含了手动生成 heap snapshot 和触发垃圾回收(GC)的功能,帮助开发者定位和排查内存泄漏问题。我们提供了 /heap-snapshot 接口, 开发者可以手动触发 heap snapshot 的生成。提供了 /trigger-gc 开发者可以手动触发垃圾回收。

九、将 PM2 部署迁移升级为 Kubernetes 部署, 将单机迁移升级为集群, 并通过 DevelopmentServiceHPA 来实现动态扩容缩容、自动对 Pod 进行健康检测, 如果内存、CPU 超过一定阈值, 自动重启 Pod