认识
一、认识
二、问题
2.1 Node.js 服务是如何保证稳定性的?
一、Node
部署: 基于 Docker
容器化与 Kubernetes
编排, 将单机器 PM2
管理的 Node.js
服务迁移至多机器 Kubernetes
集群。PM2
是一个流行的进程管理工具,专门用于管理 Node.js
应用程序。虽然 PM2
对于 Node.js
应用的部署与管理非常有效,但相比于 Kubernetes
,PM2
更适用于单机环境或者较小的服务集群。以下是二者的主要区别和优势对比:
-
PM2
:PM2
主要用于单节点的进程管理,虽然支持多进程模式(通过pm2 scale
命令),但其本质上是管理本地机器上的Node.js
进程,不能自动扩展到多个机器,也无法进行跨节点的服务管理。 -
Kubernetes
:Kubernetes
是一个分布式系统平台,能够管理跨多个节点的服务部署、扩展和管理。通过Pods
、Deployments
和ReplicaSets
,可以轻松实现自动扩容、滚动更新、负载均衡等功能,适用于大规模分布式应用。 -
PM2
,PM2
的核心工作机制是基于 进程管理 和 守护进程 的实现,主要包括以下几个方面:-
守护进程(
Daemon Process
):PM2
通过启动一个独立的 守护进程(Daemon
) 来管理所有的 应用进程。守护进程常驻后台,负责 监控、重启 和 管理 所有子进程。当执行pm2 start app.js
命令时,PM2
会首先检查守护进程是否存在,如果不存在,则启动守护进程。PM2 CLI
将用户的启动指令通过IPC
通信 发送给 守护进程。守护进程收到指令后,通过child_process.fork
启动一个新的子进程运行app.js
。守护进程 与 用户应用进程 分离,保证应用进程异常退出后,守护进程可以检测到并自动重启。守护进程 会保存应用进程的状态到文件中(~/.pm2/dump.pm2
),即使服务器重启也能恢复进程状态。 -
Cluster
模式(多进程模式): 适合多核CPU
的场景。PM2
支持Cluster
模式,利用Node.js
提供的cluster
模块实现多进程并发。PM2
会启动一个Master
进程,再fork
出多个Worker
子进程(基于IPC
机制与Master
通信),每个子进程都会运行同一个服务脚本(app.js
), 所有子进程共享同一个端口。主进程通过Round-Robin
(轮询)调度算法实现将请求分发到不同的子进程中,实现负载均衡。利用多核CPU
,提高服务的性能和并发能力。通过IPC
机制 在Master
和Worker
之间传递消息。 -
自动故障恢复与重启机制:
PM2
内置故障检测机制,当应用进程崩溃或退出时,守护进程会自动重启它。确保服务持续运行,减少人工干预。可以通过--max-memory-restart
限制进程内存使用,超过阈值后自动重启,防止内存泄漏。具体实现为: 守护进程监听子进程的退出事件,
-
PM2
自动捕获应用的标准输出(stdout
)和错误输出(stderr
),并将日志保存在文件中。PM2
提供pm2 monit
命令监控CPU
、内存等资源使用情况。与外部监控工具集成(如PM2 Plus
和Prometheus
),实现自定义监控和告警。
-
Kubernetes
集群-
基于
Deployment
来快速部署多台机器上的Node.js 服务 Pod
, 通过配置replicas
使用多副本部署,保证服务不会因单个Pod
故障而中断。配置LivenessProbe
自动检测不健康的容器并重启, 使服务具有故障恢复的功能。配置ReadinessProbe
确保服务准备好接收流量, 做好负载准备。设置resources
限制资源使用,防止资源耗尽。基于Pod Anti-Affinity
和nodeAffinity
将副本分布在不同节点,避免单点故障,做到服务容灾和高可用。 -
基于
Service
使用NodePort
或LoadBalancer
将服务暴露给外部,并结合ReadinessProbe
确保Service
仅将流量分发给健康的Pod
, 保证服务稳定和服务高可用。 -
Ingress
: 基于NGINX Ingress Controller
部署服务,通过路径和域名规则进行流量分发 -
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
进程的所有活动句柄(如TCP
、UDP
、定时器、请求等)
- 句柄数 (
三、内存泄漏检测:
-
测试环境: 通过
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
)的功能,帮助开发者定位和排查内存泄漏问题。-
应用启动时, 生成启动的
Heapdump
快照, 并开始定时监控内存使用情况 -
针对可疑路由添加
memoryUsageMiddleware
、trackRequestCountMiddleware
中间件。memoryUsageMiddleware
:定期检查内存使用情况并触发生成heap snapshot
, 比如内存每达到100M
生成一次heap snapshot
。trackRequestCountMiddleware
:根据请求计数触发heap snapshot
, 比如每 请求100
次生成一次heap snapshot
。 -
生成的
heap snapshot
会保存在本地目录heapdump
, 并通过scp
上传到指定的远程服务器目录。生成heap snapshot
会主动调用gc
。通过v8 global.gc()
允许你主动触发垃圾回收,让V8
进行内存清理。这对于 诊断内存泄漏 和 生成更准确的堆快照 非常有用。在某些场景下,通过手动触发GC
,可以释放不再使用的内存,确保堆快照中只保留真实的内存占用情况, 如果手动触发GC
后,内存仍然无法释放,说明存在未被回收的对象,即可能存在内存泄漏。 -
提供了
/heap-snapshot
接口,开发者可以手动触发heap snapshot
的生成。提供了/trigger-gc
开发者可以手动触发垃圾回收。
-
-
但是在生产环境中,
Heapdump
生成快照 会消耗大量的内存和CPU
,从而会影响服务的正常运行。有时候,Node
服务会中断,根据当时服务器内存大小这个时间会在2 ~ 30min
左右。确保服务高可用性和生成Heapdump
快照的核心策略是将快照生成的资源消耗隔离。-
可以通过扩展
Pod
副本数,通过负载均衡消除单个Pod
性能下降的影响。: 通过Service
将Node.js
服务暴露给外部访问,并结合Deployment ReadinessProbe
确保Service
仅将流量分发给健康的Pod
, 保证服务稳定和服务高可用。配置Deployment replicas
, 配置Node.js Pod
多个副本, 在生成Heapdump
之前,通过Kubernetes
修改Pod
的readinessProbe
,让流量从负载均衡中移除, 将当前Pod
的流量隔离,防止因性能下降影响请求响应。这样即使某个Pod
性能受损,服务流量会自动分配到其他健康的Pod
,保证服务的高可用性。 -
可以使用
child_process
独立进程分离快照生成逻辑,避免主服务受影响。: 将生成Heapdump
的任务交给一个独立的进程,避免主服务受到影响。快照生成由独立的脚本或服务运行。主服务通过异步调用启动快照生成服务。 -
可以使用
Job
或 分离快照生成逻辑,避免主服务受影响。: 通过Kubernetes
的Job
或CronJob
创建临时Pod
专门用于生成Heapdump
,而不是在主服务Pod
中生成。Heapdump
的生成在独立的Pod
中进行,完全不影响主服务。
-
2.2 Node.js 做过哪些工作?做过哪些性能优化?
Node BFF
工作、以及性能优化如下:
一、HTML
模版分发: 输入 umu.cn
经过缓存检测、DNS
解析、TCP
连接、HTTP
请求 最终会走到 Node BFF
层, 首先进行登录鉴权,检测当前登录状态、当前用户角色。检测通过, 我们会在 HTML
模版中注入用户、页面基础数据, 返回给客户端。将最终的模版通过 Redis
进行缓存, 并基于模版类型和使用频率设置 TTL
过期时间, 添加随机抖动防止缓存雪崩。那有时可能会遇到 Redis
与 MongoDB
数据不一致的问题, 我们采用了 旁路缓存模式 来解决, 获取数据时, 首先检查 Redis
缓存, 如果命中直接返回, 如果未命中, 查询数据库, 并写入缓存, 设置合理 TTL
加随机抖动防止缓存雪崩。更新数据时, 先更新数据库, 再删除缓存(这里不更新缓存, 直接删除)。我们的 HTML
模板缓存减少了平均响应时间 65%
二、微前端子应用静态资源分发: QianKun
、MicroApp
的入口都会进入 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
底层原理: Puppeteer
是 Google Chrome
团队开发的 Node.js
库, 本质上是一个浏览器自动化工具。核心优势在于它提供了一个端到端测试自动化、网页抓取和截图的高级 API
, 同时保持对底层CDP
协议的完全访问能力, 使开发者能够精确控制浏览器行为, 实现各种复杂的自动化任务。
五、AI
服务: 基于 Redis Sub
订阅 AI
团队生成结果, AI
生成结果后, 通过 Pub
发布消息。并对生成结果进行 Redis
缓存, 并设置过期时间, 后面加一个随机时间, 防止缓存雪崩。另外, 我们对单个用户进行限流, 防止暴力请求。每个 用户 + IP
对应一个 Redis
键, 在成功放行请求时, 将该键设置为存在, TTL
为 300
秒。其他限流的方案有: 令牌桶限流、固定窗口限流、滑动窗口限流。如果 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
部署, 将单机迁移升级为集群, 并通过 Development
、Service
、HPA
来实现动态扩容缩容、自动对 Pod
进行健康检测, 如果内存、CPU
超过一定阈值, 自动重启 Pod
。