第 11 章 / 共 51 章
卷二 · 配置与服务装配

多服务与拓扑:一个进程,一整张服务地图

services[] 不必只有一个。真实部署常常是"HTTP 站群 + WebSocket 推送 + TCP 网关 + UDP 心跳"的混布——它们共享一个引擎、一套脚本能力与一次优雅停机。

本章导读

站位:从"配好一个服务"到"理解整张拓扑"。本章还引入运行期拓扑查询 API——它是第 40 章管理面插件的原料。

11.1 引入

多服务部署的朴素做法是"一类服务一个进程",代价是进程 sprawl、端口规划混乱、日志分散。XServer 的拓扑模型是进程内多 server:一个 xs.json 描述全部端点,一个进程承载全部服务——共享引擎线程池、共享脚本运行时能力、统一优雅停机。生产环境 home 仓库就是这个形态:一个 XServer 进程,14 个 vhost、http 与静态站混布。

11.2 一个进程,多种协议

混合拓扑示例
{
  "engine": { "workers": 8 },
  "services": [
    { "class": "http", "name": "web",  "port": 80,  "tls": true, "port_tls": 443,
      "host_default": { "name": "app", "path": "wwwroot", "devlang": "c", "devfile": "main.c" } },
    { "class": "ws",   "name": "push", "port": 8081,
      "host_default": { "name": "app", "devlang": "c", "devfile": "push_main.c" } },
    { "class": "tcp",  "name": "gw",   "port": 9000, "tls": true, "port_tls": 9443,
      "devlang": "c", "devfile": "gw_main.c" },
    { "class": "udp",  "name": "beat", "port": 9010,
      "devlang": "c", "devfile": "beat_main.c" }
  ]
}

刚才发生了什么:四个服务四份脚本,装配时全部候选先编译成功才进入 bind——任何一个编译失败,整个进程拒绝启动(与热重载的"先编译后换代"同一条纪律)。每个服务独立换代(reload 以 server 为发布单元),互不影响。

11.3 engine.workers

  • 进程级唯一旋钮:0–1024,0 = xrt 默认(demo 实测本机 12)
  • 所有 server 的回调共享这一个线程池——HTTP 请求、WS 消息、TCP 数据、UDP 报文都在 workers 上执行
  • 热重载不可变:变更 workers 的 reload 意图被拒绝(线程数是进程级事实)
  • 容量估算:回调是短平快的事件处理,workers ≈ CPU 核数是合理起点;有长阻塞任务应放独立线程(第 41 章)而非加大 workers

11.4 运行期拓扑查询

跨服务协作:从 ws 服务找到 http 服务
/* 在 push 服务的脚本里枚举全部服务 */
static bool PrintTopo(XS_ServerInfo* pServer, void* pUserData)
{
    printf("- %s (%s) :%u%s\n", pServer->Name, pServer->Class,
        pServer->Port, pServer->TLS ? " +tls" : "");
    return true;
}
xsEnumServers(PrintTopo, NULL);

/* 按名取一个 retained lease(必须 Release)*/
XS_ServerInfo* pWeb = xsServerFind("web");
if ( pWeb != NULL ) {
    XS_HostInfo* pHost = xsHostFind(pWeb, "app");
    /* ... 跨服务读取拓扑信息 ... */
    xsServerRelease(pWeb);
}

拓扑读侧由 RWLock 保护(读多写少),Accept 路径每次进读侧——查询本身是低开销的。枚举参数回调内借用、lease 纪律见 参考页。

11.5 契约

  • 装配线:多服务全部候选编译成功才 bind;任一失败整体拒绝启动。
  • 换代线:reload 以单个 server 为单元;xsReloadAllSubmit 是整批事务(全成或全回滚)。
  • 查询线:xsServerFind 返回 retained lease;枚举参数回调内借用,保存前 Retain。

常见错误

试图热改 workers
症状
reload 意图 FAILED,Message 指出 workers 不可变。
原因
线程池是进程级资源,运行期重建会改变所有回调的并发语义——被明确拒绝而非冒险执行。
跨服务持有裸指针跨代
症状
重载 web 服务后,push 服务里缓存的 XS_ServerInfo* 变成野指针。
原因
枚举回调里的指针只借用一轮;跨代/跨服务保存必须 Retain 成 lease。

编程练习

基础四协议混布

把 11.2 的拓扑跑起来(脚本可各自空导出),对照启动日志画出拓扑表。

验收:日志含四个服务的 server/bound 行,端口与配置一致。
进阶拓扑导出路由

在 web 服务加 /topo 路由,用 xsEnumServers 输出 JSON 拓扑。

验收:输出含全部服务的 name/class/port/tls 字段。
挑战单服务故障实验

故意让 gw 服务的脚本编译失败再启动,观察整体行为;然后修复 gw、故意改坏 web 的脚本再启动。

验收:能说出"任一候选失败"时的启动日志形态,并解释为什么不允许"三个好的先跑"。

小结

要点结论
拓扑一进程多 server 混布五类协议,共享引擎与停机
装配全部候选先编译成功才 bind,任一失败整体拒绝
workers进程级、回调共享、热重载不可变;≈CPU 核数起步
查询xsServerFind(lease)/ xsEnumServers(借用)
xrt 延伸阅读