伸缩与容量
服务通过增加节点来伸缩:节点在彼此之间划分它的分区,每个节点运行它拥有的分区的命令和订阅,而底下的数据库承载所有节点的追加。本页说明要确定什么的大小、节点为了降级而不是崩溃所施加的限制,以及下一个瓶颈通常在哪里。分区与归属见部署模型。
节点与分区
每个上下文的日志有固定数量的分区 wow.log.partitions,默认 64 个。每个分区同一时刻只有一个属主,它运行该分区内聚合的每条命令,以及该分区上的每个订阅。所以:
- 节点越多,吞吐越高,只要数据库跟得上:N 个节点时,每个节点拥有大约 64 / N 个分区。
- 每个分区至多一个节点在工作。 超出分区数的节点不拥有任何分区,只转发命令。64 个分区适合大多数服务;可能需要比这更多节点的服务,选择一个更大的 2 的幂,最多 4,096。
- 分区数是固定的,日志有了条目之后就不能改,所以要在首次部署之前选定。
节点加入或离开时,大约 N 分之一的分区会移动,它们的订阅在新属主上从检查点继续。宕掉的节点会一直持有它的租约,直到租约过期,即 wow.cluster.lease-ttl(10 秒):它的分区要等这么久才有新属主。分区没有可达属主的聚合,其命令在收到它的节点上运行;聚合的版本保证结果正确,属主的订阅一读到这次提交,属主就丢弃该聚合的缓存状态,下一条命令按日志决定而不会冲突(wow.state_cache.invalidations)。
命令发往属主
命令可以到达任何节点;节点经内部端口把它转发给分区的属主,所以每个聚合的命令在一个节点上、基于它缓存的状态逐条运行。转发的代价是集群内的一跳。负载均衡器无法按聚合路由,也不需要这样做。
节点施加的限制
节点限制它接受的量,并让调用方稍后再来,而不是无休止地排队:
| 限制 | 默认值 | 超出时 |
|---|---|---|
wow.executor.max-in-flight:节点上的在途命令 | 4,096 | 命令以 wow.command.busy 拒绝(429,带 Retry-After) |
wow.executor.lane-capacity:等待同一个聚合的命令 | 1,024 | 同上,针对这个聚合 |
| 存储的并发查询 | 与追加共用的连接池的四分之一(至少 2),自适应,最多升到一半;查询专用连接池则为全部;连接池不报告大小时为 16 | 查询以 wow.query.busy 拒绝(429) |
wow.query.dataset.max-concurrent:同时运行的数据集查询 | 8,固定上限,不自适应 | 查询在一个短队列中等待,然后以 wow.query.busy 拒绝 |
带 Retry-After 的拒绝是背压,不是错误:客户端稍后重试,遇到它的 Saga 会重试该条目并放慢它的分区,同一条目被拒绝 20 次(wow.subscription.backpressure.max-attempts)后才停放该聚合(失败与重试)。
查询永远不会让追加饿死。 每个存储的快照查询和 events 查询都在它们自己的上限下运行,它的日志的重放也是:状态读取的重放,以及订阅重建状态时的重放。除非 wow.query.max-concurrent 把它固定下来,这个上限会随存储的写入压力自适应:追加变慢或等待连接时减半,恢复后再增长回来。为查询使用单独的连接池或只读副本,可以完全消除这种争用(PostgreSQL)。
订阅
分区的订阅在它的属主上运行,每个订阅至多领先它的检查点 wow.subscription.max-in-flight(1,024)个条目。落后的订阅不会让节点停止服务:它表现为降级的健康状况和不断增长的 wow.subscription.backlog。原因通常是它的处理函数的延迟:缓慢的外部调用,或者写入读模型存储时缺少索引。更多节点能分散它的分区;更快的处理函数帮助更大。
订阅从节点在提交时保留的状态(wow.shared-states)中读取每次提交之后的状态。落后得超出这些状态所及的订阅,从聚合的快照重建状态,或者重放它的日志。同一个聚合的重建在节点的各个订阅之间一次只运行一个:其他订阅等第一个完成,再从它重建的状态继续。重建在日志所在存储的查询上限下运行;超出上限时,订阅把它当作背压,稍后重试该条目。
积压之后(migrate-data 导入的历史,或者长时间停机),节点同时至多追赶 wow.subscription.max-catch-ups(2)个分区。落后于日志头部的分区停在它的检查点上等待轮到它,所以节点在此期间接收的命令仍保有它们到存储的连接。在迁移演练中(50,000 个订单,PostgreSQL 上的 64 个分区,追赶期间每秒稳定 50 个命令):
max-catch-ups | 失败的命令(共 6,000 个) | 命令最差一秒的 p99 | 追赶用时 |
|---|---|---|---|
| 2,默认值 | 无 | 2.0 到 3.7 秒 | 35 到 64 秒 |
| 4 | 无 | 4.5 到 10.2 秒 | 38 到 73 秒 |
| 8 | 四次中有一次失败 48 个 | 4.2 到 12 秒 | 41 到 93 秒 |
| 0,不限制 | 772 到 2,495 个 | 21 到 30 秒 | 58 到 96 秒 |
同时追赶更多分区并没有更快追上,因为它们共用同一个存储:存储应答越慢,用时反而越长。它们会抬高期间到达的命令的延迟,直到等待提交的命令超时。连接有富余、追赶期间又没有流量的存储适合更大的值;0 取消这个上限。
要关注什么
| 负载增长时 | 关注 | 然后 |
|---|---|---|
| 命令延迟上升 | wow.command.duration、存储的追加延迟 | 存储跟得上就增加节点;否则扩容存储 |
| 命令被拒绝 | error.type=wow.command.busy 的 wow.command.refusals | 增加节点,或者找出缓慢的存储或聚合 |
| 某个聚合很热 | 它的车道上的拒绝 | 把它的工作拆分到多个聚合:一个聚合的命令按设计就是串行的 |
| 订阅落后 | wow.subscription.backlog、wow.subscription.lag | 加快处理函数,增加节点 |
| 查询被拒绝 | wow.query.limit.current、wow.query.duration | 查询连接池或只读副本、更窄的查询、索引 |