@absolutejs/路由器
Bun PaaS网关的多租户连接路由原始. 在前面坐着
n后端进程(每个进程a) @absolutejs/runtime
托管实例 @absolutejs/sync 引擎
(a) 根据要求决定:
- 哪个Shard拥有这个租户(一致的散货,粘着)
- 房客是不是超额了?
- 房客是否超额了?
- 被选中的硬体健康吗?
核心仍然是运输独立的。 对于邦 /bun 子路径提供
hTTP 代理服务器和双向 WebSocket 桥,同时持有
路由器为真正的请求/口袋寿命获取手柄.
import { createBunGateway } from '@absolutejs/router/bun';
const gateway = createBunGateway({
router,
resolve: (request) => {
const hit = domainMap.resolve(request.headers.get('host') ?? '');
return hit
? { route: new URL(request.url).pathname, tenantId: hit.tenantId }
: null;
}
});
Bun.serve({ port: 3001, ...gateway });
专用的运行时间 : tenants: [tenantId]; 共享硬度
主机省略 tenants无合格专用/共享硬盘的请求
无法关闭 no-tenant-shards。 。 。 .
import { createRouter } from '@absolutejs/router';
const router = createRouter({
shards: [
{ id: 'engine-1', url: 'ws://10.0.0.11:3000' },
{ id: 'engine-2', url: 'ws://10.0.0.12:3000' }
],
hashStrategy: 'jump',
perTenantConnectionCap: 100,
perTenantRateLimit: { tokens: 100, refillPerSecond: 10 }
});
// In your WS upgrade handler:
const decision = router.route({ tenantId, channelId });
if (decision.decision !== 'allow') {
return new Response(decision.decision, { status: 429 });
}
const handle = router.acquire(tenantId);
ws.data = {
...ws.data,
release: handle.release,
upstream: decision.shard!.url
};
// ...proxy WS frames to decision.shard.url; call handle.release() on close.
地表 (0.1.0) (中文(简体) )
| API 密码 | 目的 |
|---|---|
createRouter(options) | 工厂。 回返a Router。 。 。 . |
router.route({ tenantId, channelId?, route?, region? }) | 回返 { shard, decision, emptiedBucket? }决定是: allow 编号 : rate-limited 编号 : capped 编号 : no-shards 编号 : no-tenant-shards 编号 : no-region-shards 编号 : denied。 。 。 . |
router.acquire(tenantId) | 增加活动连接计数器; 返回 { active, release }。 。 。 . release 是无能的. |
router.markHealthy(id) 编号 : router.markUnhealthy(id) | 呼叫者驱动的健康状况。 身体不适的硬块被跳过. |
router.drainShard(id) | 拒绝新路线;现有途径不受影响。 操作者意图状态不同于不健康状态. markHealthy 取消这个. |
router.isHealthy(id) 编号 : router.isDraining(id) | 入所检所. |
router.addShard(shard) 编号 : router.removeShard(id) | 运行时间硬化 成员变化. |
router.shards() | 入想坚固相. |
router.snapshot() 编号 : router.restore(snap, options?) | 相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相相接相接相接相相相相接相相接相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相. resetConnections 在边缘重新启动后清除已死连接时保留速率限制符号. |
createBunGateway(options) (单位:千美元)/bun(中文(简体) ) | Bun HTTP/WebSocket 代理适配器,具有录用寿命,转发头,协议桥接,以及决策状态映射等功能. |
router.dispose() | 停止接受路径; 所有后续 route() 返回时 no-shards。 。 。 . |
散列策略+负载偏差
jump(默认)——Lamping & Veach 2014. O(log n) 无内存, 当尾部被添加出硬块时, 精确的 1/ N 键会移动 。 忽略weight和load(其设计属性为无条件粘性).rendezvous人权观察的散列。 支持单硬weight不同引擎大小; 又支持load: (shardId) => number悬钩跑时热点避去——effectiveWeight = weight / load. O(N) 一次检查.- 自定义: 通过
(key, shards) => index。 。 。 .
散列策略仅适用于 'sticky' 平衡——见下文.
平衡战略(0.6.0)
一个散列策略回答“哪个WHONS这把钥匙很硬?” 这是一个正确的问题,当一个硬块持有钥匙的状态,而一个错误的问题,当硬块是一个无国籍应用的可互换的复制品——散去将租户的所有交通都钉在一个单一的复制品上,无论登记了多少. balance 选择问题 :
'sticky'(默认)——连续散列通过hashStrategy同样的钥匙,同样的硬。 对于 会话关联性,通过每个客户channelId:每个客户端会粘到一个复制品上,而不同的客户端会分散出.'round-robin'——相继呼吁合格硬币采用相同的关键循环. 光标是每个路由键, 所以一个忙的租户不能扭曲另一个的传播.'least-connections'– 最活跃的连接获胜的最少,来自被硬标记的acquire()打电话 铁丝网打破了圆形相机,所以一个闲置的复制品田地仍然会扩散.
设置路由器全宽默认并覆盖每个呼叫 :
const router = createRouter({ balance: 'round-robin', shards });
router.route({ tenantId }); // round-robin across replicas
router.route({ tenantId, balance: 'sticky', channelId: sessionId }); // affinity
这三个机构都同样尊重健康、排水、租户许可名单和区域过滤. 'round-robin' 财务报告和审定财务报表 'least-connections' 忽略 hashStrategy 并且不承诺相互呼唤的稳定.
硬接通会计(0.6.0)
acquire(tenantId, shardId?) 把连接路由的断接接上 标记获取维持一个活的硬性活性计数——什么 'least-connections' 阅读,什么 metrics().shardActiveConnections 报告与累计报告相邻 shardLoadDistribution。 。 。 . createBunGateway 自动标记。 Omitting the id 保留了0. 6.0前的行为:租户上限仍然被强制,连接只是隐形的per-hard.
排水模式
drainShard(id) 排除了新的路由线上的硬块 而没有标记它坏了。 在计划中的硬性关闭之前使用这个——租户在排水的硬性重排到他们NEXT路线上的健康的非干燥的硬体上,但飞行中的要求并没有被撕毁. 呼唤者等取出"硬"来静一静(例如通过跑步时间的统计),然后 removeShard()。 。 。 . markHealthy() 取消排水管,以防行动改变主意.
连接封顶
perTenantConnectionCap 是租户可以保持的最大连接,
通过 acquire() / 返回时 release()实现时, route()
返回时 capped - 你的网关应该拒绝升级 429 编号 :
503默认 Infinity (无盖.
费率限额——租户+每道
perTenantRateLimit 这是每个租户的象征性桶: tokens 有桶容量和起步平衡; refillPerSecond 不断重新填充到容量。 每个成功 route() 花了一分钱 Bucket在检查时间上是懒散地计算出来的——对于闲置的租户来说,没有计时器。 默认 { tokens: Infinity, refillPerSecond: 0 } (无所限.
perRouteRateLimits: Record<string, RateLimit> 一 二一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一. route({ route: 'expensive' }) 检查两者; 如果两者均为空, 调用返回 rate-limited 与 emptiedBucket 报告哪一个。 用于"100个便宜电话/分钟,5个贵电话/分钟"的外形,一个全租户封顶不会表达政策. 失败的路由桶不会消耗租户桶 ——除非两者都通过,否则均不扣除任何令牌.
允许钩 (米集成)
allow: (tenantId) => boolean 是一个呼叫器供应的门。 正在返回 false 制造 route() 返回时 { decision: 'denied' } 在任何水桶被触动之前 想要的配对是 @absolutejs/metering是因为 meter.allow ——直接通过:
const meter = createMeter({ ... });
const router = createRouter({
shards,
allow: meter.allow, // refuse routes for over-quota tenants
load: (id) => runtimeRoster.load(id), // and bias toward less-loaded shards
});
卫生
路由器不会探测后端本身——保持其包/elysia-无代表 I/O. 给自己的健康检查回路打电话 markHealthy 编号 : markUnhealthy。现场健康检查适配器是后期0.0.x子路径的候选.
抓图+还原
const json = JSON.stringify(router.snapshot());
await persistToDisk('/var/lib/router/state.json', json);
// On edge restart:
const restored = createRouter({ ... same config ... });
restored.restore(JSON.parse(await readFromDisk('/var/lib/router/state.json')));
捕获速率限制符号计数,每行道桶状态,硬质健康+排水状态,每租户活性连接计数. 没有这个,一个边缘就让每个租户手接一桶新鲜的活桶——即时限速通过,供任何观看部署时间的人使用.
区域认知出行路线(4.0)
createRouter 与一个地区相接而来; createRegionDirectory 决定哪个
租户住的地方 粘性、确定性的任务——加权
默认在区域编号上汇合,所以每个复制品都计算相同
没有协调的回答——加上基于耐久性的可选呼叫钩
控制飞机上飞机的决定的放置和明确取消.
import { createRegionDirectory, createRouter } from '@absolutejs/router';
const directory = createRegionDirectory({
regions: [
{ id: 'us-east', weight: 2 }, // twice the tenants of eu-west
{ id: 'eu-west' }
],
// Optional: latency-based placement. Return undefined to fall back to
// the default weighted-rendezvous strategy.
assign: (tenantId) => edgeProbe.closestRegion(tenantId)
});
const router = createRouter({
shards: [
{ id: 'us-1', url: 'ws://10.0.0.11:3000', region: 'us-east' },
{ id: 'us-2', url: 'ws://10.0.0.12:3000', region: 'us-east' },
{ id: 'eu-1', url: 'ws://10.1.0.11:3000', region: 'eu-west' }
]
});
// The wire-up: the directory picks the region, the router picks the shard.
const decision = router.route({
tenantId,
region: directory.regionFor(tenantId)
});
| API 密码 | 目的 |
|---|---|
createRegionDirectory({ regions, assign?, clock?, tracerProvider? }) | 工厂。 至少需要一个区域. |
directory.regionFor(tenantId) | 粘接任务,在一线创建. 如果区域被取消,则重新分配懒散. |
directory.assignRegion(tenantId, regionId) | 明确重置(机上控制飞机). 抛出未知区域. |
directory.release(tenantId) | 忘记任务; 下一个 regionFor 调任. |
directory.addRegion(region) 编号 : directory.removeRegion(id) | 运行时区成员. 迁出重新分配其租户懒散地. |
directory.regions() | 请检查date=中的日期值 (帮助) 区域列表. |
directory.snapshot() 编号 : directory.restore(snap) | 任务(+ 覆盖旗帜) 幸存控制- 飞机重启 . |
directory.metrics() | { assignments, byRegion, overrides }。 。 。 . |
沙发没有 region 他们是区域不可知的,他们仍然是候选人
任何要求的区域(背对接:维持现有的单一区域部署)
工作未触及。 A 类 route({ region }) 里面没有候选人
区域返回 decision: 'no-region-shards' ——可与
全组 no-shards 内 metrics().rejectsByDecision因此,"地区枯竭"
和"分组空"分别发出警报.
自定义域图( 0. 4. 0)
租户的交通是 app.acme.com 不是作为租户
编号. createDomainMap 是边缘网关中的第一个查询:主机名
租户,无依赖和O(1)-ish(精确主机地图,按
通配符的后缀).
import { createDomainMap } from '@absolutejs/router';
const domainMap = createDomainMap({
// Resolver of last resort — the platform's own subdomain scheme.
fallback: (host) =>
host.endsWith('.cloud.absolutejs.com')
? host.slice(0, -'.cloud.absolutejs.com'.length)
: undefined
});
domainMap.add('app.acme.com', 'acme'); // exact custom domain
domainMap.add('*.acme.com', 'acme'); // wildcard — exactly one label deep
// Edge gateway: host header → tenant → region → shard.
const hit = domainMap.resolve(request.headers.get('host') ?? '');
if (!hit) return new Response('unknown domain', { status: 404 });
const decision = router.route({
tenantId: hit.tenantId,
region: directory.regionFor(hit.tenantId)
});
| API 密码 | 目的 |
|---|---|
createDomainMap({ fallback?, clock?, tracerProvider? }) | 工厂. |
map.add(hostname, tenantId) | 小写 准确的主机和 *.example.com 通配符(一个标签深度——TLS-wildcard-certificate survival). 抛出其他 * 安置人员. |
map.remove(hostname) | 删除条目(准确或 *. 表单). |
map.resolve(hostname) | { tenantId, matched: 'exact' | 'wildcard' | 'fallback' } | null. 剥取端口,小写. 准确击败通卡击败倒回. |
map.list() | 每个条目; 其中的通配符 *. 形式. |
map.snapshot() 编号 : map.restore(snap) | 条目幸存边缘重启 . |
map.metrics() | { entries, resolves, hits, misses, lastResolveMs }。爬升误入率通常意味着离机后DNS对接网关. |
建筑作用
@absolutejs/sync——每个后端硬盘运行的引擎.@absolutejs/runtime——过程集合出每个后端的硬卵.@absolutejs/metering- 计票;meter.allow(tenant)读取.@absolutejs/router—— 说 这个图书馆在交通到达困难之前的边缘决定.meter.allow()可以连接到网关旁边router.route()拒绝过量租户 不支付上游跳台.
V.0.0.1不包括的内容
- 实际WS代理执行. 呼叫线
Bun.serve(或任何HTTP/WS层)改为router.route()和前进的字节本身. - Elysia适配器(后期为0.0.x中的子路径).
- 分出路由器状态跨越多边复制(v0.2+).
- 后端健康检查探测环.
- TLS / HTTP3 终止.
许可证
BSL 1.1为主机多租户连接路由/WebSocket边缘网关类别(Cloudflare Workers WebSockets,Cloudflare Smart Placet,Vercel边缘路由器,Liveblocks的WebSocket粉丝出道,PartyKit,Ably,Pusher,Soketi)设置了命名的外置. 见 自由主义. 更改日期: 4 从首次发布起; 更改许可证: Apache 2.0.
Bun 网关前的 TLS 终止
当信任的入侵终止 HTTPS 并连接到 HTTP 的网关时,
设定 publicProtocol: 'https' 内 createBunGateway。网关将发布广告
用于上游HTTP和WebSocket服务器的公共方案
X-Forwarded-Proto,因此与浏览器的出处进行比较.
默认仍然是收到的请求的计划。 客户端提供转发
信头从不选择公开协议;从信任的部署设置配置它.
当上游失败时
上游无法达到的请求(重发、重发或无法处理)
在任何答复之前)再次被发送到另一个硬体,而其方法是:
安全重复: 默认情况下获取、 头和选项 。 重试是普通的
route() 与失败的硬体在 excludeShards,所以它是平衡的,盖
并且象第一次一样,税率是有限的。 POST从未发送过两次. 给它
retry: { attempts, methods },或将其关闭 retry: false。 。 。 .
onUpstreamFailure 要求每个上游没有请求,所以
来电者可以马上标出 坚硬的不健康 而不是等待下一次
健康检查. upstreamHeadersTimeoutMs 限定上游需要多长时间
以发送其响应头(身体随需要而流出).
一个没有时间的要求回答了504 并且从未被重审过,这么慢的工作
从不跑两次.
upstreamLiveness 告诉一个死了的宿主 从慢。 连接到主机
消失不会被拒绝,所以一个请求会被吊死。 之后 afterMs 没有
响应信头, 网关尝试连接上游的 TCP : 一个缓慢的应用程序
仍然接受一个并留待回答,失败的连接会停止
请求为无法到达,所以 onUpstreamFailure 财务报告和审定财务报表 retry 在秒内应用.