Worker池:大载荷编解码卸载,主进程不被阻塞
Created By RV, and licensed with Creative Commons "CC BY-NC-ND 4.0"
目的与场景
NodeJS,适合IO密集的应用,在IO的空隙进行适量的CPU处理,整体RPS可以逆天。
如果您的应用,是CPU计算密集的,NodeJS“单进程、无线程”的编程模型,则会构成一个无解的叹息之墙。
RPC中,调用参数、调用结果的序列化及反序列化,是CPU计算密集的。
这意味着,大数据、深度多层、复杂的对象序列化,会卡住NodeJS的事件循环,RPS、P50/P90延迟等性能指标,会惨不忍睹。
解决方法,有两个:
启用NodeJS Cluster,这是运维层的解决办法。
启用Worker,这是开发层的解决办法。
当负载大于一定阈值后,将序列化/反序列化任务,零拷贝(Zero-Copy)卸载到Worker中,才可以维持主进程的高RPS、低延迟、稳定延迟特性。
在这一章,讲解如何在Kree4X中开启Worker池,将服务调用的编解码任务,卸载到独立的Worker进程,避免主进程事件循环阻塞,导致RPS、P50、P90等指标大幅劣化。
一. 概念
1. 编解码:主进程与Worker进程之分
默认情况下,载荷编解码在主进程中执行,和其他调用处理共享主进程事件循环。
负载过小,转移到Worker进程,“转移”本身的开销会大于“负载编解码”的开销,得不偿失。
2. Worker池:WorkerPool
开启Worker模式后,Kree4X会启动一个Worker池,包含一个或多个独立的Worker进程。
大载荷的编解码,卸载到Worker进程执行,主进程只负责帧的组装与分发,不再被大载荷编解码阻塞。
3. 卸载阈值:threshold
创建Kree4X节点时,可通过worker.threshold参数指定卸载阈值,不设定时,默认为32KB。
主进程与Worker进程的数据交换,使用的是IPC协议。
进程间IPC通信也是有开销的,大载荷使用Worker处理,才能值回IPC本身的开销。
4. Worker数量:workerCount
Worker池中的Worker进程数量,默认为1。
创建Kree4X节点时,可通过worker.workerCount参数设定。
5. Worker能大幅提升整体RPS?
不要抱有任何不切实际的幻想。
NodeJS应用,要大幅提升整体RPS,要靠NodeJS Cluster。否则一个CPU内核干活,其他几十个袖手旁观,这是极其荒谬的。
启用Worker的主要目的,是保住主进程的Event Loop不要卡死,保证绝大多数的小负载请求的P50、P90延迟。
大负载,需要大量的CPU计算,不论放在哪里,这个“计算”都是逃避不掉的。
划个圈,把所有的怪物都扔到里面让它们自萌,这是Worker方案唯一的显著效果。
6. 单实例基准 vs 生产集群
在我14款的古董MBP上,未启用Worker,跑了一个简单的Kree4X单实例基准测试,仅供参考:RPC RPS 破10K了
生产环境,启用Worker的同时,请采用集群,原生NodeJS Cluster或者PM2 Cluster,都可。
二. 示例代码
在下边的示例中,我们将:
- 创建一个开启了Worker模式的节点,监听tcp端口
- 创建另一个普通节点,连接并调用
- 分别用小载荷与大载荷调用,验证调用正常完成
import Kree4n from '@kree4js/kree4n'
// callee:创建节点时,通过options.worker开启Worker模式
// - workerCount: 2 —— 启动2个Worker进程
// - threshold: 64KB —— 载荷超过64KB,编解码卸载到Worker进程
const callee = Kree4n.create('node-a', '', {
worker: {
workerCount: 2,
threshold: 64 * 1024
}
})
callee.listen('tcp://127.0.0.1:8130')
// caller:普通节点,不开启Worker模式
const caller = Kree4n.create('node-b')
caller.attach('tcp://127.0.0.1:8130')
await callee.start()
await caller.start()
// 数据暂存服务:echo() 原样返回收到的载荷
const store = {
echo (data) {
return data
}
}
callee.register('store', store)
const storeStub = caller.service('store')
// 小载荷调用:低于阈值,主进程直接处理
const small = await storeStub.echo('hello')
// 大载荷调用:128KB载荷,超过64KB阈值,编解码卸载到Worker进程
const payload = 'x'.repeat(128 * 1024)
const result = await storeStub.echo(payload)
三. 须强调的细节
1. 小载荷不经Worker
载荷不超过阈值时,主进程直接编解码,不经过Worker进程,避免IPC开销。
2. 生命周期随Transport
Worker池的生命周期由Transport管理。
服务节点启动时,启动Worker池,节点停止时,停止Worker池,所有Worker进程被终止销毁。
3. 请求超时保护
Worker池的编解码请求有超时保护,超时标记为失败,避免Worker异常导致的无限挂起。
4. Worker进程是长期复用的进程
Kree4X的Worker池,只负责编解码,不承载业务逻辑。
Worker进程在节点生命周期内长期复用,不会为每次调用创建销毁进程。
四. 涉及到的API
1. 创建节点时开启Worker模式
/**
* 创建Kree4X节点。
*
* @param {string} name - 节点名称。
* @param {string} description - 节点描述。
* @param {Object} [options] - 节点选项。
* @param {Object} [options.worker] - Worker模式配置。
* @param {number} [options.worker.workerCount] - Worker数量,默认1。
* @param {number} [options.worker.threshold] - 卸载阈值(字节),默认32 * 1024。
* @param {string} [options.worker.runtimeScript] - Worker入口脚本路径(默认自动注入,一般无需设置)。
* @returns {KreeX} 配置好的KreeX节点。
*/
create(name, description, options?): KreeX
2. 节点创建后开启Worker模式
/**
* 开启Worker模式。
*
* Worker池由Transport管理生命周期:Transport.start()时启动,Transport.stop()时停止。
*
* @param {Object} config - Worker配置。
* @param {number} [config.workerCount] - Worker数量,默认1。
* @param {number} [config.threshold] - 卸载阈值(字节),默认32 * 1024。
* @param {string} [config.runtimeScript] - Worker入口脚本路径(默认自动注入,一般无需设置)。
* @returns {this} 当前节点,支持链式。
*/
enableWorker(config): this
五. 可运行代码
完整示例代码,参见:13-worker-pool.mjs