openJiuwen Agent Runtime Session 包架构解读:双队列调度、Session 亲和与弹性伸缩的工程实践

  • 人工智能
  • AI Agent
  • 后端
  • LLMOps

【免费下载链接】agent-runtime

openJiuwen Agent Runtime 是 openJiuwen 体系中的智能体运行与部署管理平台,致力于将 Agent 从“开发态”稳定带到“生产态”,让发布、运行、管理更加标准与可控。

项目地址: https://gitcode.com/openJiuwen/agent-runtime
点击查看 免费下载

本文以 session 软件设计说明 为骨架,结合 openjiuwen_runtime.management.session 的真实源码与系统测试,完整解析 openJiuwen Agent Runtime 如何在「入口请求」与「多实例、多 Session、可伸缩的后端工作负载(K8s Pod)」之间搭建编排层。读完本文,你将掌握双队列系统优先调度、两级并发控制、Session 亲和路由、min_idle 预热与 service_ttl 缩容、WebSocket 多路复用下行、以及主备切换时的 Pod 批量清理等整套生产级设计,并能对照源码定位每一步实现。


1. 定位与目标:编排层要解决的五个问题

Session 包位于「入口请求 → 多实例、多 Session、可伸缩的后端工作负载(如 K8s Pod)」之间,核心提供一层编排能力,设计文档归纳为五个要点:

能力说明
双队列调度用户业务请求与系统内部事件分队列,系统侧优先,保证缩容等运维动作不被用户流量饿死
单实例 = 部署单元 + 下行通道一个服务实例对应一个部署单元(如一个 Pod)+ 一条下行通道,典型形态是 WebSocket 上按 request_id 多路复用
两级并发控制服务级(实例内总并行度)与 Session 内(同一会话内并行度)相互独立,通过信号量 acquire 排队配合
Session 亲和同一 session_id 路由到同一 service_id,在 TTL 内可滑动续期;可选策略(如 chat_id + bot_id 生成稳定 session 键)
弹性伸缩min_idle 预热、max_services 上限、空闲 service_ttl 后缩容;部署抽象支持 K8s / Docker / 无部署调试用 NoOp

这些能力全部落在 management/openjiuwen_runtime/management/session 这一个包内,对外通过 __init__.py 导出 Access、ServiceManager、ServiceHandler、WSServiceMessageChannel、PerChatBotStrategy、AccessConfig、SessionConfig 等公共符号。

2. 整体架构:五层职责与消息流向

设计文档给出如下架构图(mermaid):

mermaid

各层职责可以浓缩为一张表:

层职责
Access将 IRequest 经策略变成 ISessionRequest,封 SessionRequestWrapper,只负责入队与消费 response_queue 的异步迭代
ServiceManager双队列、路由、多实例池(in_use / idle)、bootstrap min_idle、autoscale、session TTL 与 service idle 回收
ServiceHandler单实例:服务级信号量、SessionRouter(子 SessionHandler)、deploy / delete、经 invoke_channel 调通道 send
SessionHandler同 session_id 的会话内信号量、调用父级 invoke_channel
IServiceMessageChannel上行业务、下行多路分片、完成时 on_request_complete 归还服务级并发
IDeployController创建/删除后端资源,返回 PodDeployInfo 等,供通道 on_pod_ready 建链

注意一个关键设计:Access 只入队、不路由。handle_message 是异步队列写入,真正路由发生在 _message_loop 独立 task 中,避免单条长请求阻塞下一条入队(多 session 可并行进入不同实例)。这一"入队与消费解耦"的原则在 access.py 与 service_manager.py 中均有体现:send_message 末尾 await self._service_manager.handle_message(wrapper) 立即返回,而 _message_loop 为每条用户消息 asyncio.create_task(self._handle_user_request(item))。

3. 核心类型与「接口 I/O」

3.1 请求链上的三种类型

从业务入口到编排层,请求经过三层包装:

类型说明主要输入 / 输出
IRequest业务入口行协议(request_id / chat_id / bot_id / user_id / session_id),定义在 interfaces.py入:由调用方提供实现;Access 在缺省 request_id 时包一层 _AutoIdRequest 并写回 wire_dict(若存在)
ISessionRequest策略产出的会话化请求,含 session_id、session_concurrency、session_ttl、raw(原 IRequest),定义见 interfaces.py出:ISessionStrategy.handle_session(IRequest) -> ISessionRequest;标准实现是 session_request.py 中的 SessionRequest
SessionRequestWrapper一次用户调用对应一个 wrapper:session_request + response_queue + cancel Future,定义见 interfaces.py入:Access 创建;ServiceManager 入队;通道下行 put 到 response_queue;Access 以 is_completed 判终态

其中 _AutoIdRequest 是个重要细节(access.py):当用户 IRequest.request_id 为空时,Access 自动生成 UUID 并覆盖 request_id(同时若 wire_dict 存在则 inplace 注入),因为 request_id 非空是 WSS 多路复用硬条件,下游通道会拒收空 request_id。真实代码路径见 send_message 中 rid = uuid.uuid4().hex 的分支。

3.2 控制面接口一览

设计文档摘录了六组核心接口,其完整抽象定义在 interfaces.py:

接口方法含义
IAccessinit(...) / send_message(IRequest) -> AsyncIterator / shutdown() / update_config() / cleanup_all_pods(namespace, ...)初始化并 start 服务管理;流式产出来自下游的解析后结果;运行时热更新配置;主备切换时按 K8s label 清理 Pod
IServiceManagerinit / start / stop / handle_message / enqueue_system / mark_deprecated / try_cleanup_if_idle对 wrapper 入用户队列;系统事件(如缩容)入系统队列;热更新后支持旧实例老化回收
IServiceHandlerhandle_message / deploy / delete / remove_session / 并发与 session 只读属性单实例生命周期与消息处理
IServiceMessageChannelsend(service_id, wrapper, *, response_parser, on_request_complete)上行一帧 + 下行在独立接收循环中 dispatch + 完成时 await on_request_complete(rid)
IResponseParserrequest_id / is_completed / response 作用于 dict 分片多路流式与终态判断
IDeployControllerdeploy() -> info / delete() / resource_id与具体运行时解耦,协议见 runtime.py

值得注意的实现约定:IServiceMessageChannel 是 @runtime_checkable 的 Protocol,设计文档与源码注释(interfaces.py)均强调:用结构子类型实现该 Protocol,不要让具体子类继承 Protocol,以免与类型检查器对 Protocol 子类化的限制冲突。WSServiceMessageChannel 正是这一约定的落地范例。

回调约定:on_request_complete(Optional[str]) 为可 await 的异步回调(别名 OnRequestCompleteCallback),必须在单条用户请求在通道侧视为结束后调用,以释放 ServiceHandler 的服务级信号量。在 service_handler.py 中,invoke_channel 构造 _complete 闭包:先 self._inflight += 1 登记在途,回调时减一并在 inflight == 0 时通过 _idle_pool_hook 通知 Manager 检查到期 session 与 service TTL。

3.3 数据模型:AccessConfig 与 SessionConfig

两个关键配置类定义在 models.py,设计文档列举的字段与实际默认值如下:

AccessConfig(入口与 ServiceManager 共享运行配置)

字段默认值语义
user_queue_size / system_queue_size1000 / 100双队列容量
image"app:latest"业务镜像
service_concurrency200单实例总并发(服务级信号量上限)
min_idle_services1池中至少保持的空闲实例数;仅由 autoscale 补位,不会因 service_ttl 从 idle 删 Pod;设为 0 表示不维护热备
max_services10实例池上限,达到后新 session 将 _fail(100001)
target_port / invoke_path / ws_use_tls8000 / "" / False与 pod_ip 拼 WS URI;空路径仅 ws://host:port;True 为 wss://
service_ttl300① in_use 实例在所有 session 归还 + 无 inflight 后等待本秒数再转入 _idle;② 当 len(idle) > min_idle_services 时多余 idle 立即被回收(从 in_use 转入的由于已等过一次不再叠二次等待)。底数 min 台不受影响;0 表示①阶段无等待;负值未定义勿用
message_timeout / max_retries600 / 3响应等待超时与重试次数
autoscale_interval0.2autoscale 巡检周期(秒)
db_handlerNone预留存储入口,当前核心路径可不落库

SessionConfig(单 session 维度,随策略写入 ISessionRequest)

  • concurrency:默认 1,同一 session 内最大并行处理中的请求数(会话级限流);
  • ttl:默认 300 秒,session 亲和保活窗口:请求结束起计时,期间收到该 session 的新消息会取消计时;超时则移除 session handler、归还其在 service_handler 上占用的并发位。设为 0 表示请求一结束、且该 session 无 inflight 时立刻释放,不保留亲和。

3.4 内部事件与错误码

  • 内部事件:ServiceReclaimEvent(service_id: str)(internal_events.py)由 Timer 与 service_idle_ttl 触发,经 系统队列 优先消费,执行缩容 delete。ServiceManager 中 _message_loop 对 isinstance(item, ServiceReclaimEvent) 分支直接调用 _on_service_reclaim(service_manager.py)。
  • 错误码:ERROR_CODE_MAPPING(exception.py)与 ServiceManager._fail 配合,向 response_queue 写入 error_code + message + completed 的 legacy chunk 形状。常见:100001(服务并发度超过上限,资源满)、100002(服务启动失败/路由处理异常)、-1(内部错误)。

_fail 的实现细节(service_manager.py)非常讲究:错误响应必须符合上游 wire_codec.parse_agent_server_wire_chunk 的 legacy chunk 形状(request_id + channel_id + is_complete + payload,且无 ok 字段),否则上游会抛 ValueError: unrecognized wire shape,前端收不到错误信息。

4. 模块与文件功能一览

文件功能
access.pyAccess:策略生成 ISessionRequest、自动补 request_id(多路复用必须)、handle_message 入队、从 response_queue 流式 yield;init 时拉 ServiceManager.start();cleanup_all_pods 直接调 K8sServiceHandler 按 label 清理 Pod(主备切换用)
service_manager.py双队列消费循环、用户消息独立 task 路由、_pick_or_create 亲和/选实例/新 deploy、_bootstrap_min_idle、autoscale、session 与 service 空闲计时器、_fail 写错误、get_stats 统计、Pod 监控/Watch
service_handler.pyServiceHandler:deploy→on_pod_ready、invoke_channel→channel.send、dispatch_inbound_chunk 按 request_id 写回、delete 先关通道再删资源
session_handler.pySessionHandler:会话内 BoundedSemaphore + invoke_channel
dual_queue.py系统优先的 get():先 drain 系统队列,再与阻塞用户队列用 asyncio.wait(FIRST_COMPLETED)
router.pyServiceRouter:session_id -> service_id;SessionRouter:request_id -> session_id(在 ServiceHandler 上用于下行匹配)
ws_client_channel.pyWSServiceMessageChannel:serialize_request_payload / wire_dict 上行业务、_ensure_connected、on_pod_ready 中预建链、send 内等待 is_completed 与 cancel、close
k8s_service_handler.pyK8sServiceHandler 创建/等待 Pod Ready、cleanup_all_agentserver_pods 按 label 批量删 Pod;K8sDeployController 适配 IDeployController、PodDeployInfo
docker_service_handler.pyDocker 侧部署信息与实现
runtime.pyIDeployController 协议、NoOpDeployController 不调真实部署(调试用)
strategies/_base.py + per_chat_bot.pyBaseSessionStrategy 提供 configure 与 handle_session 骨架;PerChatBotStrategy 以 f"{chat_id}::{bot_id}" 为 session 键
session_request.pySessionRequest 实现 ISessionRequest(优先级为 MessagePriority.MEDIUM)
timer.py抽象 ITimer 的 asyncio 调度实现,供 ServiceManager arm session/service 空闲计时
interfaces.py各类 Protocol/ABC/别名如 OnRequestCompleteCallback、IServiceMessageChannel
init.py对外的公开导出

5. 全链路业务流程

5.1 启动(含 min_idle 预热)

启动序列与文档一致,源码对应关系如下:

  1. 构造 ServiceManager(注入 IServiceInstanceFactory、PriorityDualAsyncQueues、Timer 等),见 service_manager.py;
  2. Access.init → ServiceManager.init(response_parser) → ServiceManager.start(),见 access.py;
  3. start() 内部(service_manager.py):
    • 启动 _message_loop(从双队列取项)、_autoscale_loop;
    • 若 _pod_monitor_enabled and deploy_mode == "k8s",额外启动 _pod_monitor_loop 与 _pod_watch_loop;
    • await _bootstrap_min_idle():在 lock 内循环 min_idle 与 max_services,对每个缺口调用 _new_deployed()。
  4. _new_deployed()(service_manager.py):factory.new_service(response_parser) 得到 ServiceHandler,await h.deploy()(K8s 等创建 Pod/容器并等 Ready;WSS 在 on_pod_ready 里预建 WebSocket),成功后放入 idle 池。

_bootstrap_min_idle 还支持按模板分组预热(service_templates 列表):每个 template_id 可独立配置 min_idle_services、max_services、service_concurrency 等,池结构按 template_id 分组为 {template_id: {service_id: handler}}(None 为默认组)。预拉热入 idle 的实例不启动「in_use→idle / 删 Pod」的 service_ttl 计时——它们只由 autoscale 维护数量。

5.2 单次用户请求

设计文档给出的时序图(mermaid):

mermaid

对照源码的关键点:

  • 入队与消费解耦:handle_message 只是把 RawMessage(MessageType.USER_REQUEST, msg, priority=sreq.priority) 写入用户队列(service_manager.py);真正的路由在 _handle_user_request(service_manager.py)中执行:先 await self._timer.cancel_timer(f"sess:{session_id}") 取消待生效的 session TTL(满足"TTL 内有新消息则删除计时器"规则),再在 lock 内完成亲和查找/额度预留/路由绑定。
  • 下行写回:WSServiceMessageChannel._recv_loop 收到 JSON 后调用 h.dispatch_inbound_chunk(data, p)(ws_client_channel.py),ServiceHandler.dispatch_inbound_chunk 按 request_id 找到 _by_request 中等待的 wrapper,await w.response_queue.put(data)(service_handler.py)。
  • 超时兜底:Access 消费循环用 asyncio.wait_for(response_queue.get(), timeout=message_timeout),超时结束迭代并视为挂起(access.py)。

5.3 亲和路由的四步决策(_pick_or_create)

_pick_or_create(service_manager.py)按优先级决策:

  1. 亲和命中:ServiceRouter 已有 session_id -> service_id 且实例仍在池 → 复用;若在 idle 则唤醒到 in_use。
  2. 容量选择:否则在相同 template_id 组的 in_use / idle 中找 available_concurrency >= need 的实例(need = max(1, int(sreq.session_concurrency)))。
  3. 新部署:否则若该组未达 max_services,await _new_deployed() 并入 in_use。
  4. 拒绝:达上限仍无位 → _fail(100001)。

另外有两处防御性设计值得注意:

  • 预检拦截:若 need > service_concurrency(单 session 声明的并发超过单实例总并发),直接拒绝扩容,避免"新 Pod 一创建即成孤儿"的无效扩容;
  • 孤儿实例兜底:try_reserve_session_quota 预留失败时,锁外推动该实例走 in_use→idle 回收流程(_arm_in_use_to_idle_pool),防止 _pick_or_create 刚放入 _in_use 的实例因无 session/inflight 而永不触发 idle 钩子,导致 Pod 永久驻留。

5.4 两级并发控制

  • 服务级:ServiceHandler 维护 _session_reserved: Dict[str, int](session → 预留额度),available_concurrency = total - sum(reserved)(service_handler.py)。新 session 首次占用时通过 try_reserve_session_quota 预留 session_concurrency 额度;同 session 的后续消息不再增加预留,仅受 SessionHandler 内并发限制。单条请求 = 1 点消息级并发(_inflight 计数,用于 TTL/idle 钩子,与服务额度无关)。
  • Session 级:SessionHandler 持有一个 asyncio.BoundedSemaphore(max_parallel)(session_handler.py),由 SessionConfig→策略→ISessionRequest.session_concurrency 提供上限。handle_message 先 await self._sem.acquire(),finally 中释放,并维护 _active_rids 与 request_id -> session_id 映射(SessionRouter)。

两级信号量配合方式:服务级额度在路由时预留(持锁、幂等),消息级在 invoke_channel 中计数(service_handler.py);通道完成回调 on_request_complete 释放 _inflight,与"服务额度占用"解耦。

5.5 Session TTL 与实例空闲回收

  • 若 session_ttl > 0:成功处理完一条用户请求后 _arm_session_timer 对 sess:{session_id} arm(service_manager.py);到期 _on_session_expired 移除 session 并清 ServiceRouter 映射,若该 session 仍有 inflight 则进入 _pending_expired_sessions 延期清理队列,由 _complete 钩子触发 _flush_pending_expired_for_service 兜底(service_manager.py)。
  • 若无其它 session 且实例在 in_use,_arm_in_use_to_idle_pool 对 to_idle:svc:{service_id} arm service_ttl(service_manager.py)。
  • service_idle_ttl 到期 → ServiceReclaimEvent 入系统队列,_on_service_reclaim 确认无活跃 session / inflight 且 idle 数量超过 min 后 h.delete()(先关 WSS 再删 Pod 等),见 service_manager.py。

5.6 部署与 WebSocket 预建链

  • ServiceHandler.deploy()(service_handler.py):pod_info = await deploy_controller.deploy(),若非空且通道有 on_pod_ready,await on_pod_ready(service_id, pod_info)。
  • WSServiceMessageChannel.on_pod_ready(ws_client_channel.py):用 PodDeployInfo.pod_ip(或 Docker 的 host)与部署声明的 port 拼 ws:///wss:// URL——端口选择优先级为 pod_info.containers[target_container].port → pod_info.port → 构造时 target_port;随后 await _ensure_connected() 完成与业务进程握手,避免「K8s Ready 但首包发不出去」的纯懒连问题。
  • 上行体:serialize_request_payload(ws_client_channel.py)对 IRequest/wire_dict 序列化:优先 wire_dict,其次 dataclass asdict、model_dump(),最后退回字段组装;request_id 非空是 WSS 多路复用硬条件,Access 缺失时自动生成并补 wire_dict。
  • 链路鉴权扩展:通道支持 additional_headers(静态 dict 或无参回调,用于令牌逐次刷新)与 verify_peer(消费首帧 connection.ack 反向核验对端,失败即断开)。

5.7 缩容与优雅停止

  • 空闲回收见 5.5;ServiceManager.stop()(service_manager.py)标记队列关闭、取消 _message_loop / _autoscale_loop / Pod 监控任务与用户路由子 task、timer.stop_all(),遍历 in_use/idle/deploying 全部实例 delete,最后清空 ServiceRouter 亲和表;只有全部成功才标记 _stop_completed = True,否则保持可重试。
  • Access.shutdown() 幂等封装(access.py),重复调用直接忽略。

6. 主备切换与 Pod 批量清理

Gateway 部署为 K8s 主备模式(LeaderElection),主备切换时需清理旧 PRIMARY 遗留的 AgentServer Pod。设计文档给出的调用链:

app_gateway._session_on_role_change(PRIMARY)
  → RuntimeManagementAgentClient.cleanup_all_pods()
    → Access.cleanup_all_pods(namespace, kubeconfig, label_selector)
      → K8sServiceHandler.cleanup_all_agentserver_pods()

标签设计:

Pod 类型标签说明
AgentServerjiuwenclaw-component=agentserver由 RuntimeManagementAgentClient 通过 extra_labels 传入 K8sServiceHandler,在 Pod 创建时打上
Gatewayjiuwenclaw-component=gateway在 K8s YAML 中配置,不在代码中管理

对应常量见 k8s_service_handler.py:POD_LABEL_KEY = "jiuwenclaw-component"、POD_LABEL_VALUE = "agentserver"、POD_LABEL_SELECTOR = "jiuwenclaw-component=agentserver"。

流程:

  1. STANDBY 升为 PRIMARY 时,LeaderElection 回调触发 cleanup_all_pods;
  2. cleanup_all_pods(access.py)通过 K8s label selector 批量删除所有 jiuwenclaw-component=agentserver Pod(cleanup_all_agentserver_pods,k8s_service_handler.py,不依赖内存池状态——STANDBY 的池为空);
  3. 清理完成后,ServiceManager 的 autoscale 机制自动重建 min_idle 数量的新 Pod。

为什么不在 ServiceManager 内实现:cleanup_all_pods 不操作 ServiceManager 的内存状态(STANDBY 的 in_use/idle 池为空),只做 K8s API 调用,因此放在 Access 层直接调 K8sServiceHandler 的静态方法。

此外,K8s 部署还内置了 Pod 自愈监控:ServiceManager 在 k8s 模式下启动 _pod_monitor_loop(轮询)与 _pod_watch_loop(长连接 Watch,timeout_seconds=300),检测 FAILED_POD_STATUSES(Terminating、Failed、CrashLoopBackOff、ImagePullBackOff 等)或从 K8s list 消失的 Pod,通过 _cleanup_dead_pods 同步清理服务池、session 路由与计时器(service_manager.py)。

7. 设计取舍与扩展点

点说明
策略新增 ISessionStrategy 可换「按 user 维 session」「仅 request_id 维」等。现有 PerChatBotStrategy 以 f"{chat_id}::{bot_id}" 为 session 键(per_chat_bot.py),BaseSessionStrategy 只需子类实现 _build_key
通道实现 IServiceMessageChannel:除 send 外可实现 bind_handler / on_pod_ready / close 以配合 ServiceHandler。MockMessageChannel(测试用)与 WSServiceMessageChannel(生产用)是两种参考实现
部署实现 IDeployController 即可接 VM、Nomad 等。现有 NoOpDeployController(调试)、K8sDeployController(生产)、docker_service_handler.py(Docker)
存储AccessConfig.db_handler 预留,当前核心路径可不落库
热更新Access.update_config()(access.py)在锁外并行创建并启动新 ServiceManager,持锁原子切换引用,再异步 mark_deprecated() + try_cleanup_if_idle() 优雅回收旧实例;无在途任务即可清理,否则 _periodic_cleanup_deprecated_sm 每 30 秒重试(最多 20 次)。旧实例标记 deprecated 后自然回收,新请求路由到新一代实例

8. 与测试 / 可执行样例的对应关系

  • 系统级 Mock:tests/system_tests/management_session/test_session_sdk.py 用 NoOpDeployController + MockMessageChannel 覆盖了:session 亲和复用(test_session_affinity_same_service,断言同一 chat+bot 两条消息打到同一 service_id)、min_idle 预热建实例、无可用实例返回 100001、不同 session 在容量足够时共享单实例、deploy/delete 生命周期等。
  • 真 K8s + WSS:tests/system_tests/management_session/main_k8s_access.py 用 K8sServiceHandler + WSServiceMessageChannel 跑通全链路,业务 JSON 可经 WireIRequest 等 IRequest 实现上送;其 E2aEnvelopResponseParser 展示了 jiuwenclaw 网关归一化下行帧的解析方式(request_id 优先取 request_id,否则 response_id/id;终态判定 is_final / provenance.details.is_complete / 历史兼容字段)。可执行命令示例:
# 在 management 包根目录、已 uv sync 安装依赖后
uv run python tests/system_tests/management_session/main_k8s_access.py \
  --image your-registry/svc:tag \
  --namespace default \
  --name-prefix jiuwenclaw \
  --container-port 18092 \
  --ws-tls 0 \
  --min-idle 1 --max-services 2 --service-concurrency 10 \
  --message-json '{"text":"hi","request_id":"r1","chat_id":"c1","bot_id":"b1"}'

文档版本与代码包同步于 management/openjiuwen_runtime/management/session/;若行为与实现不一致,以源码为准。设计文档全文见 DESIGN.md。

  • 人工智能
  • AI Agent
  • 后端
  • LLMOps

【免费下载链接】agent-runtime

openJiuwen Agent Runtime 是 openJiuwen 体系中的智能体运行与部署管理平台,致力于将 Agent 从“开发态”稳定带到“生产态”,让发布、运行、管理更加标准与可控。

项目地址: https://gitcode.com/openJiuwen/agent-runtime
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值