跳至主要内容

分片、网关与执行(s200/shard、s200/gateway、s200/executor、s200/unit-metrics、s200/shard-dev) ​

路由策略、分片声明、生成的网关配置、受监督的分片执行与每单元可观测性。

分片(s200/shard) ​

路由策略与分片声明皆为普通数据 —— 网关生成器、开发调度器与执行监督器消费同一 ShardSpec 形状,这正是各自输出保持一致的原因。

ts
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 标记本监督器既不启动也不拥有的单元。

ts
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 忙碌比,因此按自身节奏轮询的监督器得到恰好该区间的利用率。

ts
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.4

createUnitMetrics(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 扮演)。

ts
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 映射旁边,两者必须讲同一个故事)。

基于 MIT 许可证发布。