关于 sharded-counters
面向高并发计数设计的技能,指导何时将单个逻辑计数拆分为多个写入分片,并权衡写入吞吐、读取成本与一致性。还涵盖近似去重计数、时间窗口计数及聚合缓存。
主要功能
- 01判断是否需要拆分计数器
- 02将写入分散到随机分片
- 03用 HyperLogLog 估算去重数量
- 04按时间桶统计滚动窗口
- 05缓存分片聚合结果
适用场景
- 01统计热门内容的点赞或浏览量
- 02汇总实时事件的访问次数
- 03计算时间窗口内的请求速率
- 04大规模估算独立访客数
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ proyecto26/ system-design-skills --skill sharded-counters安装后在 Claude Code 中输入/sharded-counters确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/deep-dive.mdreferences/providers/aws.mdazure.mdgcp.mdgeneric.md详细介绍
原文在翻译后有更新,译文可能不是最新。
分片计数器
统计增长速度远超单行、单个键或单个分区串行写入能力的内容,例如点赞、浏览量、投票、速率统计、库存扣减。陷阱是热点计数器:每个写入者都争用同一条记录,因此延迟不断上升、吞吐量停滞不前,无论机器有多强大都无济于事。设计不当会让简单的 +1 成为整个功能的瓶颈。
适用场景
对单个逻辑计数的并发增量超过单行或单个键的承载能力,例如热门帖子的点赞数、直播活动的浏览量计数器、全局速率统计。其表现是某条记录上出现写入争用(锁等待、CAS 重试、分区热点),而存储的其他部分都处于空闲状态。选择这种方案意味着问题出在写入侧,而且不要求总数精确到毫秒。
不适用场景
写入频率较低(单个原子 INCR 每秒可处理数千次——没人频繁操作的计数器不必分片;YAGNI)。必须在任何时刻都保持事务级精确且读后写一致的计数(银行余额、售罄时的座位库存)——这需要事务性扣减,参见 consistency-coordination,而不是扇出式计数。需要精确统计不同项(独立访客),并且还需要成员列表——这需要在存储中使用集合,而不是计数器。如果读取远多于写入,且写入成本很低,你需要的是缓存聚合值,而不是分片。
先明确
- 最热单个计数的写入速率——单个逻辑计数器每秒的峰值增量,而非总量(→
back-of-the-envelope)。 - 精确还是近似——总数相差几个是否可以接受,各分片最长可以不一致多久(最终一致)?这会影响分片数量和读取路径。
- 统计出现次数还是不同项——累计总数还是唯一计数(点赞数还是独立观众数)决定使用普通分片还是 HyperLogLog。
- 读取频率和新鲜度——总数多久读取一次,返回的数字最多可以过时多久(不到一秒?几分钟?)。
- 按时间窗口统计还是累计总数——“过去一小时的浏览量”需要分桶键和过期机制;累计总数则不需要。
方案
- 单个原子计数器——使用一行/一个键,通过原子
INCR/UPDATE +1更新。当最热计数的峰值写入速率远低于单个节点的串行写入吞吐量时使用。默认选择;不要过早认为它无法满足需求。 - 写入分片(条带化)计数器——将一个逻辑计数拆分为 N 个物理分片(
counter:{id}:shard:{0..N-1});每次写入时递增一个随机或哈希选出的分片,读取时汇总全部 N 个分片。当单键争用成为瓶颈,且总数可以最终一致时使用。 - 近似不同项计数(HyperLogLog)——固定大小的概率性数据结构(约 12 KB),用于统计唯一项,误差约为 2%。当规模较大且不需要精确成员信息时,用于统计唯一项(独立访客、不同搜索词)。
- 按时间窗口统计(分桶)计数器——按时间桶设置计数器键(
views:{id}:2026-06-01T14),递增当前时间桶,读取时汇总近期时间桶,并让旧时间桶过期。用于统计“过去 N 分钟/小时”这类速率指标。 - 读取时聚合并缓存总数——定期汇总分片(或进行汇总计算),并返回缓存的数字。当读取量远超写入量且总数稍有过时也可以接受时使用(可与
caching搭配)。
权衡
| 选项 | 解决的问题 | 带来的问题 | 何时更改 |
|---|---|---|---|
| 单个原子计数器 | 最简单;精确;写后读取很简单 | 一个热点记录会限制写入吞吐量;流量激增时会发生争用 | 单个计数的增量超过一个节点的承载能力 → 将写入分片 |
| 写入分片计数器 | 将写入负载分散到 N 个分片;消除热点 | 读取需要查询 N 次并求和;总数最终一致;必须预先选择 N | 对 N 个分片求和的读取成本变得难以承受 → 缓存聚合结果/汇总 |
| HyperLogLog | 以固定的极小内存,在极大规模下统计唯一项 | 约 2% 的误差;无法列出成员或进行精确计数 | 需要精确统计唯一项或获取成员集合 → 使用存储集合 |
| 时间窗口分桶 | 低成本地统计滚动计数/速率;旧数据会自动过期 | 键更多;需要谨慎处理窗口边界;跨桶读取需要对多个键求和 | 需要精确的终身总数 → 单独维护一个终身计数器 |
| 读取时聚合 + 缓存 | 以较低成本读取写入频繁的计数 | 提供的总数会比写入滞后一个刷新间隔 | 读取必须反映最新写入 → 实时读取分片(承担 N 次求和的成本) |
压力下的行为
计数器看似很小,在故障期间却可能造成远超预期的影响。
- 热点分片倾斜: 如果根据
hash(userId)而非随机方式选择分片, 一个爆红的用户或糟糕的哈希函数仍可能导致写入集中到一个分片。缓解措施: 每次写入都随机选择分片;根据峰值争用而非平均水平确定 N。 - 流量激增时的读取放大: 当某个计数爆红时,总数读取会在读取扇出过程中多次触发 N 分片求和,可能使存储过载。
缓解措施: 缓存聚合结果并按间隔刷新,而非每次读取都刷新
(→
caching)。 - 增量丢失: 即发即弃的增量(或缓冲/回写路径在刷新前崩溃)会导致计数无声地偏低。缓解措施: 使用存储的原子增量操作,明确接受最终一致性窗口;如果之后需要精确值,则从事实来源进行校准。
- 窗口边界惊群: 按时间分桶的计数器会在整点时同时切换到新键——冷桶同时出现,又涌入大量读取。缓解措施: 预先创建桶,并为汇总操作加入随机抖动。
- 批量过期: 分桶计数器同时过期可能导致存储负载激增。 缓解措施: 错开 TTL。
监控: 每个分片的写入分布(倾斜程度)、每秒增量与节点上限的对比、N 次求和读取路径的延迟、近似算法的误差预算(HLL),以及与任何事实来源对比得出的计数偏差。
如何应用
- 明确输入 — 单个最热点计数的每秒峰值增量、精确与近似的容忍度、事件计数与去重计数、读取速率,以及新鲜度预算(参见先明确这些内容)。如果没有数字表明某个计数过热,就继续使用单个原子计数器(YAGNI)。
- 根据权衡表选择 — 单个节点能承载时用单个原子计数器;单键争用成为瓶颈时用写入分片;统计唯一项时用 HyperLogLog;统计滚动窗口时用时间分桶。也可组合使用(例如:分片 + 缓存聚合结果)。
- 设置关键参数 — 根据峰值争用选择 N(分片数)、分片选择规则(随机,而非按用户哈希)、读取聚合方式、窗口的分桶粒度 + TTL,以及缓存刷新间隔。
- 对方案进行压力测试 — 按照压力下的行为逐项检查:分片倾斜、读取放大、增量丢失和窗口边界是否都已采取符合实际流量特征的缓解措施。
- 用数字确定规模 — N ≥ 每秒峰值增量 ÷ 单分片写入上限;确认 N 次求和的读取成本和任何 HLL 误差都符合预算
(→
back-of-the-envelope)。 - 选择提供方 — 默认使用通用方案;只有在用户指定云服务时才查看提供方文件(参见选择提供方)。
注意事项
应做
- 从单个原子计数器开始;只有在某个数字表明单个计数器已成为瓶颈时,才进行分片。
- 随机选择写入分片,这样无论操作主体是谁,负载都能均匀分散。
- 如果读取占主导,就缓存汇总值并定期刷新。
- 对大规模去重计数使用 HyperLogLog,并明确说明约 2% 的误差是已知代价。
- 为时间桶键设置错开的 TTL,并预先创建下一个时间桶。
- 明确说明最终一致性的时间窗口——分片是以牺牲即时精确性换取吞吐量。
不应做
- 不要对单个
INCR已能处理的计数器进行分片(过早分片只会增加读取成本,没有收益)。 - 不要按用户/实体 ID 对分片进行哈希——热点主体会让负载再次集中。
- 不要在每次读取爆红计数时都对 N 个分片求和——应缓存汇总值。
- 不要在计数必须通过事务保证精确的场景(金额、售罄库存)使用分片/最终一致计数器 →
consistency-coordination。 - 如果还需要成员列表或精确计数,就不要使用 HyperLogLog。
关键数字
决定性指标是最热门单个计数器的峰值每秒增量与节点串行写入上限的比值——该比值决定 N。对于数十亿个去重项,HyperLogLog 草图约为 12 KB,标准误差约为 2%,且与基数无关——这正是它在大规模场景下胜过存储集合的原因。读取分片总数需要进行 N 次查找,因此 N 是以读取成本换取写入余量。如需 QPS、每节点写入上限和存储容量估算,请勿在此重复——请参阅 back-of-the-envelope。
接口示意
分片计数器是一种键契约,而非单个值:
- 写入:
INCR counter:{id}:shard:{rand(0..N-1)}(原子操作,写入一个分片)。 - 读取:
sum(GET counter:{id}:shard:{0..N-1})——或读取每隔 T 秒刷新的缓存汇总值count:{id}。 - 去重计数:
PFADD uniq:{id} {member},然后执行PFCOUNT uniq:{id}(HLL 草图)。 - 窗口计数:
INCR views:{id}:{bucket}并设置 TTL;读取近期时间桶的总和。
预先确定 N、分片选择规则、聚合/刷新策略和时间窗口粒度——它们都是契约的一部分,而非实现细节。
选择提供商
默认采用上述通用方案。如果用户指定了云服务,请阅读 references/providers/<provider>.md,了解托管服务映射、配额/限制以及提供商特有的权衡。如果该提供商没有对应文件,则采用通用方案。
图示
若要可视化扇出写入路径(写入方 → 随机分片)和读取时聚合求和(读取 → N 个分片 → 缓存总数),请使用插件内的 architecture-diagram 技能——分片使用存储区的颜色,读取扇出使用虚线求和箭头,缓存汇总值使用缓存区的颜色。
相关构建模块
data-storage— 依赖它来确定分片的物理存放位置;分片/分区理论和键设计归其负责。caching— 与它配合使用,以提供缓存汇总值,避免每次读取爆红计数时都重新对 N 个分片求和。consistency-coordination— 依赖它来决定采用精确计数还是最终一致计数;事务/原子语义和法定人数归其负责。back-of-the-envelope— 为它提供依据:该模块给出支持分片并确定 N 的单计数器写入速率和每节点上限。system-design— 由编排器中的概念负责:推理循环、权衡方法和十种故障模式。
参考资料
references/deep-dive.md— 分片数量计算、随机与哈希选择、HyperLogLog 机制和误差、时间桶布局、汇总/聚合模式以及对账。设计计数器细节时请阅读。references/providers/{generic,aws,azure,gcp}.md— 各环境的服务映射、原子性/争用限制和注意事项。