在 tRPC-Python 中,任务插件(Task)是一组专门用于后台批处理计算的子进程。处理网络请求的 Worker 进程可以将耗时阻塞或适合凑批(batching)执行的请求(如模型推理、CPU 密集型计算)发送给任务子进程,任务子进程按批调用用户定义的处理函数后再将结果异步返回给 Worker。
使用任务插件前,需要注意它与常规插件的几点区别:
- 不继承
BasePlugin:任务插件没有init()/start()生命周期方法,而是通过register_task函数登记(内部固定使用PluginType.TASK,但PluginSpec.cls为None,因此调用get_plugin(PluginType.TASK, name)不会返回可调用对象)。 - 无
plugins.task配置:任务是否启用完全取决于模块导入时是否执行了register_task。定义任务的模块必须在服务入口调用serve()之前被import。 - 与其他进程/定时器的区分:定时任务请使用
TimerTask;multi_process+separate模式下的 RPC 子进程属于网络工作进程;本文介绍的register_task专用于批处理计算子进程或者其他计算密集型的任务。
整体数据流转如下:
flowchart LR
worker[Worker 处理协程] --> send["send_to_task"]
send --> queue[任务进程请求队列]
queue --> consumer[TaskConsumer 消费者线程]
consumer --> handler["handler(batch)"]
handler --> rspQueue[响应队列]
rspQueue --> future[Worker 端 Future]
注册任务需使用公开的数据类 trpc_sdk.abc.TaskInfo 与注册函数 trpc_sdk.plugin.register_task:
from trpc_sdk.abc import TaskInfo
from trpc_sdk.plugin import register_task
def handler(reqs):
return [req.upper() for req in reqs]
def initializer():
return None
register_task(
"normal_task",
TaskInfo(
name="normal_task",
handler=handler,
initializer=initializer,
batch_size=5,
),
)
register_task会自动将task_info.name覆盖为第一个参数传入的名称,建议两处保持一致。
TaskConsumer 对批处理函数 handler 有以下要求:
- 输入与输出等长且一一对应:入参
reqs是一个list,返回值也必须是一个长度相同的list,且下标与入参严格对应。如果返回值不是list、长度不一致,或者handler执行过程中抛出异常,该批次内的所有调用方都会收到TaskHandleError。 - 支持同步函数或异步协程函数:消费者线程会通过
inspect.iscoroutinefunction自动判断并选择同步或异步执行入口,且线程内部自带独立的事件循环(请勿在handler中引用 Worker 进程的事件循环)。 - 直接接收业务对象:批次列表中的每个元素就是调用
send_to_task时传入的req对象。框架会自动通过任务 codec 完成跨进程序列化与反序列化,业务函数无需手动处理字节流。
handler(必填):批处理执行函数。initializer(可选):在每个任务子进程fork启动后、开始从管道接收请求前调用一次(详见TaskProcess._prepare_to_serve)。非常适合在此处加载深度学习模型或初始化进程内资源。注意它运行在任务子进程而非 Worker 进程中;若抛出异常,该任务子进程将无法启动。batch_size:当前任务的单批次最大请求数。若设为<= 0,则回退使用全局服务配置server.task_batch_size。task_num:启动的任务子进程数量,默认为1。当启动多个子进程时,实际进程名形如normal_task:0、normal_task:1,但业务调用send_to_task时仍统一填写注册名"normal_task",由框架负责选择连接。pause_heartbeat:默认为False。设为True时,build_task_infos会在fork前暂停名字服务心跳,待子进程initializer执行完毕后再恢复心跳。仅当需要等待任务子进程初始化完成后才对外暴露服务可发现状态时才需开启;常规场景保持默认False即可。
只有 Worker 侧的进程可以向 Task 发送任务,Task 进程之间不能互相调用。
当服务启动时,框架按以下步骤拉起并连接任务子进程:
- 登记任务:导入服务入口模块时,
register_task将任务信息写入注册表(可通过get_registered_tasks()查看)。 - 构建进程拓扑:调用
serve()后,在fork前进入_spawn_children(),由build_task_infos()根据每个任务的task_num生成进程记录,并按需执行pause_registry_heartbeat()。 - 创建进程间管道:
TaskIpcMesh在fork前预先创建好 Worker 进程到各个 Task 进程的socketpair通信管道。 - 启动子进程与初始化插件:
fork出TaskProcess子进程后,子进程先执行task_init(queue_size)初始化底层队列,再调用init_task_plugins()(仅初始化标记为PluginInitPos.TASK且非USER_DEFINED的插件)。 - 运行消费者:子进程关闭无关的文件描述符,执行
initializer(),随后根据server.task_consumer_num启动对应数量的TaskConsumer线程,并在主事件循环上运行consume_response回传结果。同时,Worker 进程在自己的服务事件循环上调用register_task_conn注册任务连接。
在 Worker 的 RPC 处理函数中,直接调用并 await send_to_task(ctx, task_name, req) 即可将请求发往任务子进程:
from trpc_sdk.task import send_to_task
async def SayHello(self, ctx, request):
rsp = await send_to_task(ctx, "normal_task", request.message)
return rsp- 凑批机制:
TaskConsumer每次从队列中最多取出batch_size条请求组装成一个列表传给handler(batch_size是上限,若当前只有 1 条请求也会单独成批处理)。当队列为空时,消费者线程会休眠task_wait_interval毫秒后再检查。 - 常见异常说明:
RuntimeError:当前线程未绑定任务连接表。send_to_task必须在 Worker 的服务事件循环(service loop)上调用;当配置了handle_thread_num > 0时,独立的 I/O 线程上没有任务管道,不能直接调用。TaskHandleError:指定的task_name未注册(解析到的连接为None),或者任务子进程中的handler执行报错 / 返回列表长度不匹配。
任务队列与消费者线程的全局参数配置在 server 节点下(而非 plugins 下),默认值定义在 trpc_sdk/config/_types.py 与 _constants.py 中:
server:
task_batch_size: 1
task_wait_interval: 5
task_queue_size: 1000
task_consumer_num: 1各字段说明:
task_batch_size:全局默认批次上限,默认为1。仅当某个任务的TaskInfo.batch_size <= 0时生效;若注册任务时显式指定了正数(如上面示例中的batch_size=5),则以TaskInfo.batch_size为准。task_wait_interval:任务队列为空时消费者线程的休眠等待时间,默认为5(单位:毫秒)。task_queue_size:每个任务子进程的请求队列容量,默认为1000。task_consumer_num:每个任务子进程内启动的TaskConsumer消费者线程数,默认为1。