分片、网关与执行(s200/shard、s200/gateway、s200/executor、s200/unit-metrics、s200/shard-dev)
路由策略、分片声明、生成的网关配置、受监督的分片执行与每单元可观测性。
分片(s200/shard)
路由策略与分片声明皆为普通数据 —— 网关生成器、开发调度器与执行监督器消费同一 ShardSpec 形状,这正是各自输出保持一致的原因。
import { policy, shardSpecs, shardApp, matchShard } from 's200/shard';
policy(app, 'GET', '/admin/:path*', { timeoutMs: 30_000, streaming: true });
const specs = shardSpecs(app, [
{ id: 'api', prefix: '/api', policy: { timeoutMs: 5_000 } },
{ id: 'admin', prefix: '/admin', entry: './shards/admin.ts' },
{ id: 'main', prefix: '/' },
]);
const adminApp = shardApp(app, { id: 'admin', prefix: '/admin' });
const serving = matchShard(specs, '/api/users'); // 最长前缀分发| 函数 | 含义 |
|---|---|
policy(app, method, pattern, p) | 为所有注册于 method + pattern 的路由标注 RoutePolicy;无匹配为 no-op,二次调用替换首次 |
getPolicy(route) | 路由附带的策略(policy 设置过时) |
shardSpecs(app, groups) | 把 ShardGroup 声明对真实路由表解析 → readonly ShardSpec[];spec 保持声明序、路由保持注册序(生成确定性)。重复分组 id 抛错 |
shardApp(app, group) | 把一个分组的路由切出为新 App(纯数据变换 —— 源应用绝不修改) |
matchShard(specs, pathname) | spec 上的最长前缀分发 —— 网关配置、开发调度器与任何自定义路由器共享的接缝;返回胜出 spec 或 undefined |
RoutePolicy:{ timeoutMs?(响应截止提示), memoryMb?(执行器资源限制), maxConcurrency?(调度上限 —— 分组合并策略取最大值), streaming?(生成的代理配置关闭缓冲), shard?(显式分片 id 覆盖 —— 无论前缀声明该路由) }。
ShardGroup:{ id(DNS 安全标签 —— 变成 upstream/worker 名), prefix('/' 根;'/' 声明兜底分组), policy?(分组级基础策略), entry?(thread/process 监督的执行器模块路径/URL) }。
ShardSpec:{ id, prefix, routes: readonly RouteTableEntry[](s200/route-table 的条目形状), policy(分组 + 路由策略合并,始终盖 shard: id) }。
成员规则:显式 policy.shard: id 声明优先(被他处声明的路由留在后面);否则按前缀子树。父分片可携带嵌套分片也服务的路由 —— 最长前缀分发保持其流量分离。
网关(s200/gateway)
由 ShardSpec[] 生成网关配置 —— 三个文本视图,全部字面前缀语义(^~、PathPrefix、path-pattern),全部带 # generated by s200/gateway 前缀。策略映射:timeoutMs → proxy_read_timeout、streaming → proxy_buffering off;分片无该策略字段则省略指令。前缀非绝对路径的 spec 抛错并点名每个违规者。
| 函数 | 选项 | 输出 |
|---|---|---|
nginxConf(specs, opts?) | NginxConfOptions:socketDir?(默认 /run/s200,unix socket ${id}.sock)或 portBase?(分片 i → 127.0.0.1:(portBase + i)) | nginx upstream 块 + location ^~ <prefix> server 块 |
gatewayRoutes(specs, opts?) | GatewayRoutesOptions:namespace?、gatewayName?(默认 s200)、hostname? | Kubernetes HTTPRoute 清单(Gateway API),YAML |
albRules(specs, opts?) | AlbRulesOptions:listenerArn?、targetGroupArns: Record<shardId, arn>(每个分片都需要 —— 无处转发的规则在生成时而非应用时失败) | AWS ALB 监听规则,JSON |
执行器(s200/executor)
受监督的分片执行:inline 留在进程内,thread/process 在隔离递增的子域中运行 entry,external 标记本监督器既不启动也不拥有的单元。
import { runShards } from 's200/executor';
const supervisor = runShards([
{ spec: apiSpec, executor: { kind: 'thread', entry: './shards/api.ts' } },
{ spec: mainSpec, executor: { kind: 'inline', app } },
]);
await supervisor.start();
console.log(supervisor.units());
await supervisor.stop();runShards(plans) → Supervisor:
| 成员 | 含义 |
|---|---|
start() | 启动每个自有单元:process 单元立即 fork;thread 单元只预绑定端口(暂停的监听器)—— 工作者线程在首个连接到达时才惰性生成,因此空闲分片零线程成本。inline 单元是调用方的 |
stop() | 在 STOP_GRACE_MS 内排空每个自有单元,强杀无视者 |
units() | 快照 UnitStatus[]:{ id, state, port?, restarts, lastError? } |
ShardPlan:{ spec, executor: Executor, port? } —— 显式端口覆盖;省略时 thread/process 单元从 31000+ 计数器取。Executor:{ kind: 'inline'; app } \| { kind: 'thread'; entry } \| { kind: 'process'; entry } \| { kind: 'external'; address }。UnitState:'spawning' | 'ready' | 'draining' | 'stopped' | 'failed' | 'external'。意外子进程退出以指数退避重启(100ms 翻倍、上限 5s);稳定期重置。
线程交接是双模式的:在能转移服务器句柄的运行时(node ≥ 26)上,预绑定的监听器与其停泊的套接字经一次 postMessage 移入工作者(零代理);在更早的运行时上,工作者改为服务私有端口,监督器把接受的套接字逐字节管道到该端口 —— 同样的惰性生成、同样的首请求保证,监督器始终不解析 HTTP。工作者在采纳其套接字之前死亡时,停泊的套接字被销毁,客户端快速失败而非挂死。
另导出:installShardResolution()、appFromEntry(entry)、createShardServer(app)、startShardWorker(...)、runThreadShard(...) —— runShards 驱动的子域机制(thread/process 单元把本模块作为其运行时重新导入)—— 以及 WorkerLaunch({ mode?: 'lazy' | 'eager'; port? }),监督器→工作者的启动消息。
单元指标(s200/unit-metrics)
单元的单点读数。所有计数自上次 createUnitMetrics 调用或 reset() 累计;eventLoopUtilization 是上一快照与本次之间的 0..1 忙碌比,因此按自身节奏轮询的监督器得到恰好该区间的利用率。
import { createUnitMetrics, unitMetricsEndpoint } from 's200/unit-metrics';
const m = createUnitMetrics({ maxConcurrency: 64 });
use(app, m.middleware); // 尽早注册 —— 在路由之前
get(app, '/metrics', unitMetricsEndpoint(m)); // JSON(默认)
get(app, '/metrics', unitMetricsEndpoint(m, { format: 'prometheus' })); // Prometheus 文本 0.0.4createUnitMetrics(options?) → { middleware, snapshot(): UnitMetrics, reset(): void }。尽早注册 middleware,使每个请求 —— 已处理的、404、错误的 —— 在进入与退出时都被计数;出错的请求仍计为已完成。UnitMetricsOptions:{ maxConcurrency?(*分发器*宣告的并发 —— 塑造 queueDepth), warmAfter?(默认 1) }。
UnitMetrics:{ inFlight, maxInFlight(单元实际吸收的峰值), requests(完成数而非到达数), queueDepth(max(0, inFlight - maxConcurrency),未宣告上限时恒 0), eventLoopUtilization, rss, heapUsed, warm(粘性:requests达到warmAfter后为 true,直到reset()), startedAt }。
unitMetricsEndpoint(m)(或 unitMetricsEndpoint(m, { format })):把 snapshot() 服务出去的 handler —— 默认为 JSON,或以 format: 'prometheus' 服务 Prometheus 文本曝光格式。在 m.middleware 之前或之后挂载都正确(未包装时报告总量;包装时诚实显示自身为当前在途的那一个请求)。prometheus 形态以 Content-Type: text/plain; version=0.0.4; charset=utf-8 服务 —— 把 Prometheus 抓取器指向同一条 /metrics 路由即可。
prometheusText(snapshot):该形态背后的纯字符串化函数 —— 把快照渲染为 Prometheus 文本曝光格式 0.0.4:# HELP/# TYPE 行加每个字段一个 gauge,无标签,顺序确定(UnitMetrics 声明顺序)。指标名为 s200_unit_*,camelCase 字段转 snake_case(inFlight → s200_unit_in_flight);字节量带 _bytes 基本单位后缀(rss → s200_unit_rss_bytes、heapUsed → s200_unit_heap_used_bytes);startedAt 重标度为 unix 秒(s200_unit_started_at_seconds);warm 映射为 0/1。该格式没有 NaN/Infinity 字面量 —— 非有限读数退化为 0,不会毒化整个抓取。PROMETHEUS_CONTENT_TYPE 是导出的 content-type 常量。
开发调度器(s200/shard-dev)
分片的本地开发:createDispatcher 把每个 ShardSpec 与支撑它的应用配对(通常用 shardApp 从单体切出),返回做最长前缀分发的普通 (request) => Promise<Response> —— 与 matchShard 和生成的 nginx 配置共享的同一切缝。fallback 是无分片前缀认领的路径名的应用(生产中由网关的兜底 upstream 扮演)。
import { createDispatcher, dispatchPlan } from 's200/shard-dev';
const dispatch = createDispatcher([
{ spec: apiSpec, app: apiApp },
{ spec: adminSpec, app: adminApp },
], fallbackApp);
const res = await dispatch(new Request('http://localhost/api/users'));spec 快照在创建时取一次,镜像生成的 nginx 配置是固定制品:分片应用上重新注册路由仍然有效(应用是活的),但增加分片意味着构建新的调度器。dispatchPlan(specs) 是决策表的纯数据视图 —— 分片按最长前缀优先排序,供测试与文档(把它渲染在生成的 nginx 映射旁边,两者必须讲同一个故事)。