系统设计与技术架构全景 —— 从客户端到数据中心的完整拆解:前端、设计系统、网关、微服务、时间线排序、存储、流计算、边缘网络与安全;七大专题深挖:搜索、通知、DM 消息、媒体管线、ML 平台、实验与变现;五大扩展专题:内容治理、多区域容灾、计数系统、国际化与开放平台;并附 19 个可复现的实战教程(T1–T19),每篇按「原理 → 代码 → 要点 → 动手练习」组织。读多写少、缓存优先、优雅降级,是贯穿全文的三条主线。
文档信息
| 项目 | 内容 |
|---|---|
| 文档版本 | v15.0 |
| 维护团队 | Core Infra |
| 章节 / 教程 | 27 章 + 19 教程 |
| 密级 | INTERNAL |
| 上次评审 | 2026-07-28 |
卷首指标(原页为数字滚动动画,此处静态化)
- 日活跃用户(可货币化 DAU):2.52 亿
- 峰值推文 / 秒:9400
- 在线微服务:1200+
- 边缘 POP 城市:40+
- 近 12 个月可用性:99.96%
01 · 总体架构
SYSTEM MAP
五层模型 · 读写分离 · 多区域
X 是典型的读多写少社交系统,读写比约 100:1。整体采用五层模型:请求自客户端经边缘 POP 进入,由统一网关完成鉴权与路由后进入领域服务层;数据按访问模式拆分至事务库、KV、缓存与对象存储;ML 平台横向贯穿,为召回与排序提供特征和推理能力。
分层系统图
- L1 · 客户端:iOS · Swift · Android · Kotlin · Web SPA · React · 第三方 API
- L2 · 边缘层:Anycast POP · WAF · Bot 评分 · TLS 1.3 · HTTP/3 · 边缘缓存 · 速率限制
- L3 · 接入层:GraphQL 网关 · WebSocket 通道 · REST v2 · OAuth2 · JWT
- L4 · 领域服务:推文 tweetypedia · 时间线 mixer · 社交图谱 · 推荐 cr-mixer · 通知 · DM · 媒体 · 搜索
- L5 · 数据层:MySQL 分片 · Manhattan KV · Pelikan 缓存 · Blobstore · Kafka · Elasticsearch
- ◈ ML 平台横向贯穿 L3–L5:特征仓库 · GPU 训练集群 · 在线推理(<10ms / 样本)
五条设计原则
- CACHE-FIRST · 缓存优先:读路径默认命中缓存;一切可重建的数据皆可丢弃。
- EVENTUAL · 最终一致:观众视角容忍秒级延迟;作者视角保证 read-your-writes。
- HYBRID FANOUT · 混合扇出:普通用户写扩散、超大 V 读扩散,兼顾延迟与成本。
- GRACEFUL · 优雅降级:推荐失效回退关注流;依赖故障自动熔断与限流。
- MOBILE-FIRST · 移动优先:面向弱网与高丢包网络设计,预取与请求合并为默认能力。
关键负载画像
| 接口 / 场景 | 峰值吞吐 | P95 目标 | 备注 |
|---|---|---|---|
| 时间线拉取 | 420K QPS | 250ms | 缓存命中率 >95% |
| 发布推文 | 9.4K /s | 150ms | 触发扇出与索引双写 |
| 媒体上传 | 2.3K /s | 400ms | 异步转码矩阵 |
| 搜索查询 | 38K QPS | 300ms | 查询理解 + 检索分离 |
| DM 消息 | 96K /s | 200ms | 端到端加密路径 |
| 通知扇出 | 1.2M 事件/s | — | Kafka 削峰 |
02 · Web 前端架构
WEB CLIENT
React · GraphQL · PWA
Web 端为React 单页应用:首屏由服务端预渲染 + 客户端水合,后续路由完全客户端渲染。数据层统一走GraphQL 持久化查询,只传查询哈希与变量,显著压缩弱网载荷;Service Worker 负责离线壳、静态资源缓存与 Web Push。状态管理采用轻量自研 store,配合乐观更新保证交互零等待。
TTI < 3s · 首屏 JS ≤ 180KB gz · i18n 40+ 语言 · A/B 实验 SDK · WebSocket 增量时间线
模块划分
| 模块 | 技术 | 职责 |
|---|---|---|
| 渲染框架 | React 18+ Concurrent | 流式渲染、可中断更新 |
| 数据层 | GraphQL / Relay | 持久化查询、字段订阅、缓存归一化 |
| 状态管理 | 自研 store(不可变快照) | 乐观更新、失败回滚 |
| 构建 | Vite + SWC | 路由级代码分割、Tree-shaking |
| 实时通道 | WebSocket | 时间线增量、通知、DM 打字态 |
| 离线 / 推送 | Service Worker + Web Push | 离线壳、后台同步、通知唤起 |
GRAPHQL · 首页时间线持久化查询
query HomeTimeline($cursor: String) {
home { timeline(count: 20, cursor: $cursor) {
entries { id sortIndex content { ... on Tweet {
id text lang createdAt favoriteCount retweetCount
author { id name verified } media { url type }
} } }
nextCursor
} }
}
# 持久化查询 sha256=3f2a…c9 —— 服务端按哈希命中,仅传输变量
乐观更新:点赞 / 转推先写入本地 store 即时反馈,服务端失败则回滚并提示;弱网下写操作进入离线队列,恢复后按序重放(幂等键去重)。对应教程见T6 GraphQL 网关;长列表渲染优化见T14 虚拟长列表。
03 · 移动客户端
iOS · ANDROID
Swift · Kotlin · 模块化
双端均为模块化原生工程:feed / compose / media / dm 独立编译、独立发布。iOS 全面 Swift 并渐进迁移 SwiftUI;Android 以 Kotlin 为主,Compose 已覆盖约 60% 界面。共享层包括 Protobuf 协议模型、实验 SDK、图片管线(降采样 + 渐进式解码)与崩溃防护框架。发布采用每周版本火车 + 配置中心热修。
双端质量指标
| 指标 | iOS | Android | 口径 |
|---|---|---|---|
| 冷启动 P75 | 1.6s | 2.1s | 中端机型实测 |
| 崩溃率 | 0.03% | 0.05% | 会话级 |
| 下载包体积 | 112MB | 78MB | 商店分发尺寸 |
| 帧率基准 | 60fps | 60fps | 高端机型 120fps |
| 启动内存 | 320MB | 380MB | P75 |
弱网策略
请求合并 Batching · 图片自适应降质 · 首屏 20 条预取 · 2G 纯文本降级 · 失败指数退避重试
04 · 设计系统
DESIGN SYSTEM
Design Token · 暗黑模式 · 无障碍
X 的界面语言以「内容即界面」为核心:极简黑白基调、单一品牌蓝点缀、信息密度高而秩序清晰。所有视觉决策沉淀为Design Token,由设计工具同步到三端(iOS / Android / Web),任何组件不允许硬编码色值与间距。
色彩 Token(亮色主题)
| Token | 色值 | 用途 |
|---|---|---|
| accent | #1d9bf0 | 主行动按钮、链接、选中态(X Blue) |
| accentHover | #1a8cd8 | 悬停 / 按压 |
| textPrimary | #0f1419 | 正文与标题 |
| textSecondary | #536471 | 次要文字、时间戳 |
| bgSecondary | #f7f9f9 | 次级背景、悬停行 |
| divider | #eff3f4 | 分割线、边框 |
| like | #f91880 | 点赞(品红) |
| retweet | #00ba7c | 转推 / 成功(绿) |
| danger | #f4212e | 删除、错误、录制中 |
| warning | #ffd400 | 警示、订阅高亮 |
暗黑模式(双档)
| 主题 | 背景 | 主文本 | 说明 |
|---|---|---|---|
| Dim 暗灰 | #15202b | #f7f9f9 | 默认深色,低对比刺激 |
| Lights out 纯黑 | #000000 | #e7e9ea | OLED 省电,分割线 #2f3336 |
字体 / 间距 / 圆角
- 品牌字体 Chirp(自研,覆盖多语言字重 12 档),Web 端按语言回退系统字体栈;正文基准
15px / 1.5,字号阶梯 13 / 15 / 17 / 20 / 24 / 31。 - 4pt 间距网格:所有留白取自 0 · 4 · 8 · 12 · 16 · 24 · 32 · 48,禁止奇数值。
- 圆角语义:头像全圆、主按钮胶囊(9999px)、卡片 16px、输入框 8px。
- 图标:24px 网格、2px 描边、选中态填充实心;全局统一一套线性图标库。
DESIGN TOKEN · JSON 同步源(节选)
{
"color": {
"accent": "#1d9bf0", "accentHover": "#1a8cd8",
"textPrimary": "#0f1419", "textSecondary": "#536471",
"bgPrimary": "#ffffff", "bgSecondary": "#f7f9f9",
"divider": "#eff3f4", "like": "#f91880",
"retweet": "#00ba7c", "danger": "#f4212e"
},
"space": [0, 4, 8, 12, 16, 24, 32, 48],
"radius": { "full": "9999px", "card": "16px", "input": "8px" },
"font": { "brand": "Chirp", "body": "15px/1.5", "scale": [13, 15, 17, 20, 24, 31] },
"motion": { "duration": "200ms", "easing": "cubic-bezier(.2,.8,.4,1)" }
}
信息架构(IA)
| 页面 | 移动端入口 | Web 端入口 | 核心能力 |
|---|---|---|---|
| 首页 Home | 底部 Tab 1 | 左侧导航 1 | 关注流 / 推荐流切换,无限滚动 |
| 探索 Explore | 底部 Tab 2 | 左侧导航 2 | 趋势、话题、搜索一体 |
| 通知 Notifications | 底部 Tab 3 | 左侧导航 3 | 全部 / 提及 / 已验证 过滤 |
| 私信 Messages | 底部 Tab 4 | 左侧导航 4 | DM 会话列表,E2E 加密入口 |
| AI 助手 Grok | 底部 Tab 5 | 左侧导航 5 | 对话、图像理解、实时资讯 |
| 个人主页 Profile | 头像 / 侧滑 | 左侧导航底部 | 推文 / 回复 / 媒体 / 点赞 四页签 |
动效与无障碍
- 动效:统一 200ms + ease-out 曲线;列表进入、点赞爆炸粒子、底部弹层三套标准动效;骨架屏代替转圈。
- 对比度:正文对背景 ≥ 4.5:1(WCAG AA),大字号放宽至 3:1。
- 触控目标:最小 44×44pt;图标按钮扩大命中热区。
- 读屏:图片支持 ALT 文本(AI 自动建议);全部控件带语义标签;支持系统「减弱动态效果」。
组件清单(节选):Primary / Secondary / Negative 按钮、Tab 页签、Toast、底部弹层 BottomSheet、模态 Modal、骨架屏 Skeleton、无限滚动 Sentinel —— 每个组件在三端同名、同 Token、同交互规范。
05 · API 与统一网关
GATEWAY & GRAPHQL
GraphQL · 幂等 · 限流
所有流量在边缘完成 TLS 终止后进入统一 GraphQL 网关,聚合约 30 个领域服务。字段级鉴权:viewer context(登录态、屏蔽关系、敏感内容偏好)注入每个字段解析器;持久化查询白名单杜绝任意查询注入。写接口全部幂等——客户端生成 Idempotency-Key,服务端 24h 去重窗口。限流采用三级令牌桶:IP → 用户 → 设备,配额通过响应头透出(实现见教程T4)。
| 协议 | 用途 | 说明 |
|---|---|---|
| GraphQL | 主站 / App 全量读路径 | 持久化查询 + 字段级权限 |
| REST v2 | 开放平台、合规接口 | 保持向后兼容与分页游标 |
| WebSocket | 时间线增量 / 通知 / DM | 按频道订阅,断线自动补帧 |
幂等写入 MUTATION
mutation PostTweet($idk: ID!) {
createTweet(input: {
idempotencyKey: $idk, # 客户端生成,服务端 24h 去重
tweetText: "hello, world",
replySettings: "everyone",
media: { ids: ["195391…", "195392…"] }
}) {
tweet { id }
... on RateLimited { retryAfter }
}
}
限流响应示例
HTTP/1.1 429 Too Many Requests
x-rate-limit-limit: 300
x-rate-limit-remaining: 0
x-rate-limit-reset: 1770480600
retry-after: 42
content-type: application/problem+json
{ "title": "RateLimited", "detail": "timeline.read" }
06 · 微服务拓扑
SERVICES
~1,200 服务 · 服务网格 · Snowflake
经 2023 年大规模整合后,线上约1,200 个服务:Go 主导无状态边缘与工具服务,JVM(Scala/Java)承载核心领域逻辑,Rust 用于高性能基础设施组件。服务间走自研 RPC + mTLS 服务网格,注册中心基于一致性哈希;无状态优先,一切会话态外置到 Pelikan 缓存层。
核心服务清单
| 服务 | 职责 | 关键特性 |
|---|---|---|
tweetypedia | 推文读写核心 | 一致性裁决、双写索引与缓存 |
timeline-mixer | 时间线组装混排 | 关注流与推荐流统一管线 |
fanout-service | 写扩散投递 | 为每个粉丝写入收件箱 |
socialgraph | 关注 / 粉丝图谱 | 边存储 + 实时计数 |
visibility | 可见性裁决 | 屏蔽、限流、法律移除统一出口 |
cr-mixer | 推荐候选混合 | 多路召回融合,已开源 |
reco-platform | 特征与模型推理 | GPU 推理,P99 < 10ms |
notify-service | 通知聚合 | 去重、合并、优先级降级(详见17) |
dm-platform | 私信与群聊 | MLS 端到端加密(详见18) |
media-service | 上传 / 转码 | 图片即时裁剪、视频多码率(详见19) |
search-service | 查询理解与检索 | 热词缓存、30 天滚动索引(详见16) |
Snowflake 全局 ID · 64 bit(实现见教程T2)
1
41
10
12
符号
毫秒时间戳 · 纪元 2010-11-04
机器 ID · 1024 节点
序列 · 4096/ms
单节点理论产能 4096 × 1000 ≈ 410 万 ID/秒;ID 单调递增且可反解生成时间,天然适合作为游标与分片键。部署流程:K8s + canary 1%→5%→25%→100%,指标门禁异常自动回滚。
07 · 时间线与排序引擎
TIMELINE & RANKING
重召回 · 神经排序 · 混合扇出
关注流采用混合扇出:普通用户发推后由 fanout-service 写扩散到每位粉丝的缓存收件箱;粉丝量超阈值的账号改为读扩散,拉取时实时合并。推荐流走「重召回 → 过滤 → 精排 → 混排」四级管线,端到端 P95 控制在 500ms 内。想亲手实现?见教程T1与T5。
一次首页请求的旅程(延迟预算)
- 8ms · 接入鉴权
- 22ms · 邮箱拉取
- 120ms · 6 路并行 · 多路召回
- 60ms · 可见性过滤
- 180ms · 48M 参数 · 神经精排
- 40ms · 启发式混排
- 60ms · 数据富化
- P95 ≤ 500ms · 响应返回
- ~2,000 条 · 6 路并行 · 召回候选
- ~500 条 · 屏蔽 / 去重 / 法律 · 可见性过滤
- 48M 参数 · 多任务打分 · 神经精排
- 40 条 · 作者多样性启发式 · 首屏混排
- 2006:Ruby on Rails 单体上线,五个月完成 MVP。
- 2008 – 2012:去 Rails 化:JVM + SOA,告别「Fail Whale」时代。
- 2010:Snowflake 全局 ID 与大规模缓存体系上线。
- 2013:自研 KV 存储 Manhattan 成为低延迟数据主干。
- 2017:Web 全面 React + GraphQL 重写,首屏提速 3 倍。
- 2019:推荐算法透明化,cr-mixer 等核心组件开源。
- 2023:成本重构:服务与服务器大规模整合,迁移多云,关闭冗余机房。
- 2024:长文 / 长视频 / 创作者分成上线,媒体管线重构。
- 2025:支付牌照落地、X Money 试点,AI 深度融入时间线与搜索。
- 2026:多云双活与「Everything App」:通讯 · 支付 · 内容 · AI 一体化。
- Kafka · 事件接入
- spam 拦截 · 风控过滤
- P0–P3 · 优先级打分
- 10min 合并 · 聚合窗口
- 文案 + 头像堆叠 · 渲染
- 推送/应用内/邮件 · 通道路由
- 失败降级重投 · 回执闭环
- 签名直传 S3 · 分片断点上传
- 格式/病毒/违规哈希 · 校验与扫描
- 主色 + 去 EXIF · 缩略图提取
- 6 档码率 + AVIF · 转码矩阵
- 按粉丝地域推 POP · 预热分发
- 邮箱封顶:ltrim 保证单用户收件箱有界,控制内存与扇出成本。
- 游标 ≠ 页码:以 ID 定位,插入新推文不会造成翻页重复。
- hydrate 批量化:一次 mget 拉齐作者与计数,避免逐条查询。
- 容量:单节点 4096 × 1000 ≈ 410 万 ID/秒,远超单机写入能力。
- 时钟回拨:小回拨可等待追平,大回拨必须拒绝服务或切换机器号,绝不可静默生成。
- 机器号分配:生产中由配置中心 / K8s StatefulSet 序号下发,避免冲突。
- 分布式:多实例场景把计数放进 Redis,用 Lua 脚本保证「读-改-写」原子性。
- 分级:IP 防爬虫、用户保公平、设备防多开,三层独立又叠加。
- 客户端义务:收到 429 必须按 retry-after 指数退避,禁止硬重试。
- 负反馈权重最高:一次「不感兴趣」要抵消多次点赞,避免标题党绑架时间线。
- 粗排-精排分离:候选多时用轻量模型粗筛,控制 GPU 成本。
- 离线评估:用历史互动回放,算 precision@40 / NDCG,再上 A/B(见21 实验平台)。
- 游标分页:nextCursor 用最后一条推文 ID,天然抗并发插入。
- 字段级权限:viewer context 注入 resolver,屏蔽关系在字段解析时裁决。
- 深度限制:对查询做最大深度 / 复杂度分析,防止恶意嵌套。
- 分位数优先:histogram 而不是 avg,P99 才代表真实痛感。
- 采样策略:正常流量 1% 采样 trace,错误与慢请求 100% 全采。
- 错误预算:30 天窗口内 (1 − SLO) × 总分钟数,烧完即冻结发布。
- 《Designing Data-Intensive Applications》— 分布式系统取舍的最佳入门框架。
- Snowflake 官方博客 — 分布式 ID 的原始设计动机。
- cr-mixer 开源仓库 — 真实的多路召回工程实现。
- Zipkin 论文 — 分布式链路追踪的起源(诞生于 Twitter)。
- Manhattan, our real-time, multi-tenant distributed database — 自研 KV 的工程细节。
- RFC 9420(MLS)— 群聊端到端加密协议规范。
- 中文分词:无空格语言需分词器(ngram 简单、词典法准确),直接影响召回。
- 交集优化:从最短 posting list 起步,逐个过滤,避免大表全扫。
- 时效衰减:freshBoost 用指数衰减,新闻意图下加大权重。
- 去重:用集合存 actorId,同一人重复点赞只计一次。
- 幂等 flush:定时任务可能重放,flush 前先原子取走集合。
- 最后一道闸:免打扰时段与按类型开关在投递前再做一次过滤。
- 前向安全:任何一把密钥泄露,历史消息仍安全——这是 E2E 与 TLS 的本质区别。
- 服务器零知识:只转发密文与元数据,无法解密内容。
- 中间人防护:双方比对安全码(密钥指纹)确认身份。
- 不要按请求分桶:否则用户体验闪烁,指标也失去意义。
- AA 空跑校验:实验未开启时两组指标应无显著差异,否则分桶有偏。
- 护栏单侧报警:崩溃率、举报率只要显著变差立即停,不与收益对冲。
- 动态高度:先用估计值占位,渲染后用 ResizeObserver 实测回填修正。
- 缓冲防白屏:快速滚动时预留窗口外的若干条,牺牲少量节点换流畅。
- 资源回收:滑出窗口的图片释放解码内存;key 稳定避免整段重建。
- 降级阶梯预先设计:推荐 → 关注 → 热门缓存 → 骨架页,逐级兜底。
- 舱壁隔离:每个下游独立连接池,防止一个慢服务耗尽全部资源。
- 分布式状态:多实例各自熔断可接受短暂不一致,必要时广播状态。
- 随机杀 pod / 缓存分区 · 每月
- 区域 DNS 切换演练 · 每季
- 整云撤离演习 · 每年
- 假设文档 + 结论复盘 · 每次演练
- 伪本地化:文本膨胀 40% + 加重音字符,在 CI 阶段提前暴露截断问题。
- 时区:存储永远 UTC,展示按观众 locale 转换——绝不在库里存本地时间。
- 字体:Chirp 按文字系统回退系统字体;阿拉伯语需整形引擎(shaping)。
- 合并写的代价:进程崩溃丢失最多 1s 窗口——计数可容忍,资金绝不可容忍。
- 展示压缩:
Intl.NumberFormat(locale, {notation:'compact'})输出 1.2万 / 1.2M。 - 对账兜底:夜间任务重放事件流重算,纠正漂移。
- 高估特性:CMS 只保证「不低于真值」,上榜前必须精确复核,防止误报。
- 反操纵:剔除机器人账号贡献、打压单一来源突增(刷榜检测)。
- 人工闸门:敏感词进入趋势前经过审核队列,避免热点被恶意绑架。
- 开关 × 实验:开关 + 指标采集即成 A/B(见T13),一套分桶两处复用。
- 客户端开关:配置中心推送热更新,冷启动拉取 + 本地缓存兜底。
- 组合管理:多开关并存的测试矩阵要显式维护,别让它隐式爆炸。
- 爆炸半径:从 1% 灰度时段开始,任何异常一键中止,绝不在高峰期首演。
- 假设先行:每次演练必须先写下「我们认为会发生什么」,结束后对账。
- Runbook 闭环:演练暴露的每个问题都产出 Runbook,挂进告警附件(见T8)。
召回源(六路并行)
| 召回路 | 信号 | 说明 |
|---|---|---|
| 关注收件箱 | 写扩散缓存 | 基础盘,保证关注内容不缺席 |
| SimClusters | 兴趣社区相似度 | 约 2 万个稀疏社区向量 |
| TwHIN | 异构图嵌入 | 用户–推文–实体联合图 |
| RealGraph | 互动亲密度 | 回复 / 点赞 / 点击加权 |
| Trends | 地域热点 | 突发话题快速注入 |
| 冷启动兜底 | 热门 + 编辑精选 | 新用户与低信号场景 |
排序漏斗
排序模型为多任务结构,示例目标权重:点赞 1.0 / 回复 1.3 / 转推 1.2 / 关注 3.0 / 负反馈 −8.0。线上以「重度用户活跃天数」为北极星指标做 A/B 决策;混排层强制同作者连续不超过 2 条、按节奏穿插视频。
08 · 存储体系
DATA & STORAGE
MySQL · Manhattan · Pelikan
数据层按访问模式拆分:事务归 MySQL、低延迟 KV 归 Manhattan、热点归 Pelikan、二进制归 Blobstore。核心原则是「读路径必须命中缓存」,缓存整体命中率长期维持在 97% 以上。缓存层的工程细节(穿透 / 击穿 / 雪崩)见教程T3。
存储矩阵
| 系统 | 类型 | 承载数据 | 关键特性 |
|---|---|---|---|
MySQL | 分库分表 RDBMS | 推文主数据、用户资料 | user_id 哈希分片,3 副本半同步 |
Manhattan | 自研分布式 KV | 收件箱、计数、会话 | SSD + RAM 分层,P99 < 5ms |
Pelikan | 统一缓存层 | 热点推文、图谱边、会话 | Twemcache 演进,命中率 > 97% |
Blobstore | 对象存储 | 图片 / 视频 / 语音 | 纠删码 EC,跨区域复制 |
Kafka | 事件主干 | 行为流、审计日志 | 3 副本,按用户哈希分区 |
Elasticsearch | 检索索引 | 推文全文、用户搜索 | 30 天热索引 + 冷归档 |
一致性矩阵
| 操作 | 一致性级别 | 实现 |
|---|---|---|
| 发推后作者刷新 | 强一致(read-your-writes) | 写后读主库 / 旁路标记 |
| 他人时间线可见 | 最终一致 ≤ 3s | 异步扇出 + 缓存失效广播 |
| 点赞 / 转推计数 | 最终一致(±1s) | 增量合并写,Kafka 聚合 |
| DM 会话 | 因果一致 | 会话级序列号 + 确认回执 |
备份与容灾:逻辑备份 + binlog 增量;跨区域异步复制 RPO < 30s;每季度切换演练,RTO < 15min。
09 · 实时流式管道
STREAMING PIPELINE
Kafka · Flink · 事件驱动
事件驱动是 X 的神经系统:所有行为(发推、互动、曝光、关注)以事件形式进入Kafka 主干,由 Flink 集群完成实时计数、趋势窗口与风控特征计算,同时在 5 分钟内把「曝光 ↔ 互动」按 request_id join 成训练样本入湖,供排序模型持续迭代。动手搭一条消费管道见教程T7。
核心 Topic
| Topic | 峰值吞吐 | 分区策略 | 下游消费方 |
|---|---|---|---|
tweet.create | 9.4K /s | 512 · user_id 哈希 | 扇出、索引、风控 |
engagement.events | 900K /s | 1024 | 计数、样本拼接 |
impression.stream | 3.5M /s | 2048 | 样本拼接、指标 |
social.follow | 12K /s | 128 | 图谱、推荐 |
abuse.signals | 260K /s | 512 | 实时风控模型 |
可靠性设计
at-least-once + 幂等 sink · Flink checkpoint · Schema Registry 契约 · 反压与死信队列 · 消费滞后 SLO < 2s
互动事件样例
{
"event": "tweet.engagement",
"type": "favorite",
"tweet_id": 1953921047311282176,
"user_id": 88214526,
"request_id": "r-7f3c…", // 与曝光事件 join 成训练样本
"ts_ms": 1770480600123,
"surface": "home_timeline",
"experiment": ["ranker_v42", "ui_density_b"]
}
10 · 基础设施与交付
INFRASTRUCTURE
多云 · Kubernetes · IaC
基础设施已从自有机房迁移为多云架构:AWS 为主、GCP 为辅,媒体负载保留部分自持节点。Kubernetes 联邦按节点池(计算 / 内存 / GPU)调度,GPU 集群同时服务推荐推理与 AI 训练。全量环境 Terraform 化,漂移巡检每日执行。
| 环境 | 区域 | 用途 |
|---|---|---|
| prod-us | us-east / us-west | 主生产,全量服务 |
| prod-eu | eu-west | 数据驻留合规 + 就近服务 |
| prod-ap | ap-northeast | 亚太只读副本 + 边缘回源 |
| staging | us-east | 预发,镜像生产拓扑 |
| edge | 40+ POP | 缓存 / 安全 / 媒体就近分发 |
交付流水线
Mono-repo + 增量构建 → 单元测试与契约测试 → 镜像构建与 SBOM 生成 →canary 1%→5%→25%→100%,每级由指标门禁自动裁决,异常 5 分钟内自动回滚。容量规划由历史负载预测模型驱动,批处理与训练任务使用 Spot 实例压低成本。
成本治理:2023 年整合关闭大量冗余服务与机房,服务器规模显著收缩;此后持续做 right-sizing 与存储分层(热 / 温 / 冷)。下游故障时的自保机制见教程T15 熔断与降级。
11 · 边缘网络与媒体
EDGE & MEDIA
Anycast · HTTP/3 · AVIF
全球40+ Anycast POP承担 TLS 终止、WAF、Bot 评分与缓存。媒体是不可变内容,天然适合边缘缓存——命中率长期 >96%;动态 API 请求走优化专线回源。图片 URL 参数化,边缘按需即时裁剪转码;视频上传后进入转码矩阵(HLS/DASH,6 档码率),分片预热到各 POP;直播走 LL-HLS,端到端约 3s。媒体在源站侧的完整加工流程见19 媒体处理管线。
URL 即转码指令
原图 https://pbs.twimg.com/media/GBxK2…?format=jpg&name=4096x4096 3.8 MB
边缘 https://pbs.twimg.com/media/GBxK2…?format=avif&name=900x900 38 KB
视频 https://video.twimg.com/ext_tw_video/1953…/pu/vid/avc1/1280x720/seg-3.m4s (HLS 分片)
媒体缓存命中 > 96% · HTTP/3 · QUIC · 签名 URL 防盗链 · Origin Shield 二级回源 · 边缘 JS 挑战 · Bot 评分
12 · 可观测性
OBSERVABILITY
Metrics · Tracing · SLO
三支柱体系:Metrics(自研时序库,约 1,200 万时序,10s 粒度)、Tracing(全链路 1% 采样,错误与慢请求 100% 全采)、Logs(结构化 JSON,热存 7 天、冷归档 90 天)。告警按 P1/P2/P3 分级:P1 立即呼叫值班,P2 出工单,P3 仅观察。落地代码见教程T8。
SLO 与错误预算(30 天窗口)
| 服务 | SLO | 错误预算 | 超限动作 |
|---|---|---|---|
| 统一网关 | 99.95% | 21.6 min | 冻结非必要发布 |
| 时间线 | 99.90% | 43 min | 降级为关注流兜底 |
| 媒体分发 | 99.95% | 21.6 min | 切换备用回源链路 |
| DM | 99.90% | 43 min | 只读模式预案 |
P1 MTTA < 5min · P1 MTTR < 30min · 无责复盘周会 · Runbook 随告警附送 · RUM 10% 采样
13 · 安全与合规
SECURITY & COMPLIANCE
2FA · E2E · 风控
安全设计覆盖身份、传输、内容治理与数据合规四层。DM 采用MLS 协议端到端加密,密钥定期轮换,服务端零知识(协议细节见18);内容治理坚持「言论自由,触达受限」——高风险内容不删除但降权分发。
威胁与对策矩阵
| 威胁 | 对策 |
|---|---|
| 账号盗用 | 2FA(TOTP / WebAuthn 通行密钥)+ 异地登录挑战 |
| 接口滥用 / 爬虫 | 设备指纹 + 行为评分 + 梯度限流(429 响应头透出配额) |
| 垃圾与操纵 | 实时风控模型 + 可见性分级降级 |
| DM 窃听 | MLS 端到端加密 · 服务端零知识 |
| 数据泄露 | PII 字段级加密 + KMS 密钥托管 + 最小权限 |
| 合规要求 | GDPR / CCPA 数据导出与删除,SLA 30 天;审计日志不可篡改 |
| 供应链攻击 | SBOM + SLSA L3 构建溯源 + 依赖准入 |
Bug Bounty:关键漏洞设高额赏金,7×24 应急响应;白帽提交平均首次响应 < 24h。加密实践见教程T12。
14 · 性能预算
PERFORMANCE BUDGET
P75/P95 · CI 门禁 · RUM
所有体验指标以分位数而非均值治理,预算写入 CI 门禁:Lighthouse CI + bundle diff 超 5% 即阻断合并;线上 RUM 按地域 / 网络制式分位看板持续跟踪,性能 OKR 与团队发布节奏挂钩。
预算总表
| 指标 | 预算 | 口径 |
|---|---|---|
| FCP | ≤ 1.5s | P75 · RUM |
| LCP | ≤ 2.0s | P75 · RUM |
| TTI | ≤ 3.0s | 中端机(Moto G 档位) |
| 时间线 API | P95 ≤ 250ms | 网关出口 |
| 首屏端到端 | P99 ≤ 800ms | 冷缓存最坏路径 |
| 首屏 JS | ≤ 180KB gz | 路由首包 |
| 图片 TTFB | P75 ≤ 80ms | 边缘命中 |
| 崩溃率 | ≤ 0.05% | 双端会话级 |
15 · 架构演进路线
ROADMAP
2006 → 2026
架构的终点不是某个技术栈,而是让数亿人同时说话时,系统依然稳定的那种韧性。
16 · 搜索系统深挖
SEARCH
查询理解 · 滚动索引 · LTR
搜索意图分为三类:用户、话题、实时新闻。写路径消费tweet.create事件,富化实体信息(用户、媒体、话题标签)后构建 30 天滚动热索引,更早内容转入冷归档;读路径经过查询理解、混合检索、多因子排序三级,热词与结果页均有二级缓存(命中率约 78%)。键入联想(typeahead)走独立集群,P95 < 80ms。
搜索请求四级管线
| 阶段 | 组件 | 延迟预算 |
|---|---|---|
| 查询理解 | 分词 / 拼写纠错 / 意图分类 / 改写 | 15ms |
| 混合检索 | ES 热索引(BM25)+ 向量 ANN | 120ms |
| 排序 | LTR 模型:相关性 + 互动 + 时效 + 个性化 | 80ms |
| 渲染 | 高亮、媒体富化、作者批量回填 | 40ms |
| 合计 | — | P95 ≤ 300ms |
查询改写示例
输入: 怎么学习分布式
改写: +分布式 +系统 +入门 -广告 -营销号
意图: topic
扩展: 同义词[分布式 → 分库分表, 多机] 语言: zh
时效性在新闻意图下是一等公民:1 小时内的内容时效因子权重 ×3;用户意图则直接命中资料卡 + 近期推文。动手实现见教程T10 手写倒排索引。
17 · 通知系统设计
NOTIFICATIONS
聚合窗口 · 优先级 · 多通道
通知系统完全事件驱动:任何互动(点赞 / 关注 / 提及)成为事件,经「风控过滤 → 优先级打分 → 聚合窗口 → 渲染 → 通道路由」管线投递。核心设计是聚合与频控——「10 人赞了你的帖子」只产生一条通知;提及与回复永不聚合(高价值),点赞按 10 分钟窗口合并;全局频率上限与免打扰时段防止通知风暴。
通知类型策略表
| 类型 | 触发 | 聚合策略 | 优先级 | 通道 |
|---|---|---|---|---|
| 提及 / 回复 | 有人 @ 或回复我 | 不聚合 | P0 | 推送 + 应用内 |
| 引用 | 引用我的帖子 | 不聚合 | P0 | 推送 + 应用内 |
| 转推 | 转推我的帖子 | 10 分钟窗口合并 | P1 | 推送 + 应用内 |
| 点赞 | 赞了我的帖子 | 10 分钟窗口合并 | P2 | 应用内角标 |
| 新关注 | 有人关注我 | 每小时聚合 | P2 | 推送 |
| 系统推荐 | 运营 / 算法 | 每日摘要 | P3 | 仅邮件 |
回执形成闭环:推送失败降级为应用内角标;角标 24h 未读可再降级为邮件摘要;用户可按类型配置开关——通知设置页本质是一台路由规则编辑器。聚合的实现简化版见教程T11。
18 · DM 消息与端到端加密
DM & MLS
MLS · 密钥树 · 零知识
会话式模型:会话列表 + 消息时间线两级结构。在线投递走 WebSocket 实时通道,离线转 APNs/FCM 推送;已读回执与「正在输入…」走短暂通道(不落库)。加密采用MLS(Messaging Layer Security)协议:每个会话维护一棵棘轮密钥树,成员变更时只更新受影响路径的密钥,同时满足前向安全与后向安全;服务器只存密文,零知识。
能力矩阵
| 能力 | 状态 | 说明 |
|---|---|---|
| 文本 / 表情 | ✔ | E2E 加密 |
| 图片 / GIF | ✔ | 媒体密钥单独加密 |
| 群聊 | ✔ | MLS 密钥树,成员变更路径更新 |
| 多端同步 | ✔ | 每设备独立密钥对 |
| 阅后即焚 | ✔ | 客户端定时销毁 |
| 服务端内容审查 | ✘ | 无法解密,仅客户端举报通道 |
加密消息信封
{
"conv_id": "c-9f2e…",
"seq": 48213, // 会话级单调序号,用于排序与去重
"sender_device": "d-77ab…",
"key_epoch": 12, // 密钥树代次,接收方据此取解密钥匙
"ciphertext": "base64…", // AES-GCM 加密的正文
"mac": "…" // 完整性校验
}
E2E 的代价:服务端搜索、审查与多端漫游都变得更昂贵。X 的选择是「内容加密 + 元数据(会话关系、时间戳)可见」——投递优化与合规调查依赖元数据。简化实践见教程T12。
19 · 媒体处理管线
MEDIA PIPELINE
转码矩阵 · 预热 · 直播
一张图 / 一段视频从上传到被看见要经过五道工序:分片断点上传 → 校验与扫描 → 缩略图提取 → 转码矩阵 → 预热分发。视频在转码矩阵中产出 6 档码率的 HLS/DASH,客户端按带宽自动选档(ABR);GIF 统一转成 MP4 循环视频节省约 60% 带宽;图片在源站只保留 4096 原图与少数标准尺寸,其余规格由边缘按指令即时处理(见11 边缘与媒体)。
视频码率阶梯
| 分辨率 | 编码 | 码率 | 场景 |
|---|---|---|---|
| 1080p | H.264 / HEVC | 6.0 Mbps | Wi-Fi 高清 |
| 720p | H.264 | 3.0 Mbps | 默认档 |
| 480p | H.264 | 1.2 Mbps | 4G |
| 288p | H.264 | 0.5 Mbps | 弱网兜底 |
| 音轨 | AAC | 64 kbps | 独立分片 |
LL-HLS 直播延迟 ~3s · WebRTC 连麦 · AVIF 体积 −35% · 预热命中率 92%
20 · 机器学习平台与 AI
ML PLATFORM & GROK
特征仓库 · 训练 · 推理
平台分三层:特征仓库(在线 / 离线共用一套定义,杜绝训练-推理不一致)、训练(样本拼接复用09 流式管道,GPU 集群日增量 + 周全量训练)、在线推理(模型注册表 → canary → 全量,P99 < 10ms)。Grok 大模型运行在独立推理集群上,承担帖子撰写建议、搜索答案生成、图像理解与审核辅助。
模型矩阵
| 模型 / 场景 | 输入 | 延迟 SLA | 更新频率 |
|---|---|---|---|
| 时间线精排 | 用户 × 推文特征(48M 参数) | P99 10ms | 每日 |
| 粗排双塔 | embedding 点积 | P99 3ms | 每日 |
| 风控评分 | 实时事件流 | P99 5ms | 每小时 |
| 图像理解 | 视觉编码器 | P99 400ms | 每周 |
| Grok 对话 | LLM 流式生成 | TTFT < 1.5s | 每周 |
铁律:在线服务与离线训练必须命中同一份特征快照(point-in-time correctness),否则离线指标漂亮、上线即翻车——「训练-推理偏差」是推荐系统最常见的深坑。
21 · 实验平台
EXPERIMENTATION
正交分层 · holdout · CUPED
一切排序与 UI 变更必须经过 A/B。分桶用user_id + 实验 id的稳定哈希,保证同一用户全程看到同一版本;实验按正交层组织——排序层与 UI 层互不干扰;保留 5% 长期 holdout 组衡量长期效应。指标计算依赖曝光 × 互动的样本拼接(见09),并用 CUPED 方差缩减压缩实验周期。
稳定分桶(简化版,完整实现见 T13)
const bucket = (userId, expId) => fnv1a(`${userId}:${expId}`) % 100;
const variant = b => b < 50 ? 'control' : b < 95 ? 'treatment' : null; // 5% holdout
决策流程
| 阶段 | 动作 | 产出 |
|---|---|---|
| 假设 | 明确改动与预期影响 | 实验文档 |
| 功效计算 | 估算最小可检测效应与样本量 | 预计时长 |
| 放量 | 1% → 10% → 50% 阶梯 | 护栏指标监控 |
| 决策 | 显著性 + 北极星 + 护栏三查 | 全量 / 回滚 |
护栏指标(崩溃率、举报率、取关率)单侧报警——只要显著变差立即停实验,不看收益;北极星是「重度用户活跃天数」而非短期点击。分桶与指标计算见教程T13。
22 · 变现与支付
MONETIZATION & X MONEY
订阅 · 分成 · 双式记账
四条业务线:Premium 订阅(分层会员)、创作者广告分成(按回复区产生的可验证曝光结算,需过滤机器人)、广告系统(实时竞价 + 受众定向 + 频次控制)、X Money(P2P 转账与汇款,多地区牌照)。技术重点:计费幂等、账本与对账、反作弊、地区税务合规。
| 业务 | 收益模型 | 技术要点 |
|---|---|---|
| Premium | 月费 / 年费订阅 | 订阅生命周期、Apple/Google/Stripe 多渠道、区域定价 |
| 创作者分成 | 广告收益分成 | 可验证曝光计数、反作弊过滤、T+30 结算 |
| 广告 | CPM / CPC 竞价 | 竞价 P99 50ms、定向隐私合规、频次上限 |
| X Money | 转账 / 汇兑手续费 | KYC/AML、双式记账、日终对账 |
支付是强一致领域——与时间线的最终一致世界观截然不同:资金要求同步双式记账、事件 at-least-once + 对账任务兜底。同一个平台并存两种一致性策略,这是「架构服从业务」最直观的体现(对照08 一致性矩阵)。
T1 · 教程:迷你时间线:扇出与收件箱
FANOUT & INBOX
写扩散 · 读扩散 · 游标分页
难度 ★★☆ · 耗时 3–4h · 栈 Node.js + Redis
原理:写扩散(fan-out on write)在发推时把推文 ID 推入每位粉丝的收件箱列表,读路径只需取自己的收件箱,复杂度 O(1);粉丝量巨大的账号改为读扩散,拉取时实时合并,避免一次写入百万级列表。分页使用 Snowflake ID 作游标,天然按时间倒序且无偏移漂移。
NODE.JS · 发布 = 写扩散
const BIG_V = 10_000; // 大 V 阈值(粉丝数)
async function postTweet(userId, text) {
const tweet = { id: snowflake(), text, author: userId, ts: Date.now() };
await kv.set(`tweet:${tweet.id}`, tweet); // ① 主数据落盘
await redis.lpush(`user:${userId}:tweets`, tweet.id); // ② 个人主页列表
const followers = await graph.followers(userId); // ③ 粉丝列表
if (followers.length > BIG_V) {
await redis.sadd('bigv:active', userId); // ④ 大 V:读扩散登记
} else {
await Promise.all(followers.map(f =>
redis.lpush(`inbox:${f}`, tweet.id)
.then(() => redis.ltrim(`inbox:${f}`, 0, 799)) // ⑤ 邮箱封顶 800 条
));
}
bus.publish('tweet.create', tweet); // ⑥ 事件总线通知下游
return tweet;
}
NODE.JS · 读取 = 组装时间线
async function homeTimeline(userId, cursor = 0, count = 20) {
let ids = await redis.lrange(`inbox:${userId}`, cursor, cursor + count - 1);
const bigvs = await redis.smembers('bigv:active'); // 大 V 读扩散补偿
ids = ids.concat(await recentTweetsOf(bigvs, count));
ids = [...new Set(ids)].sort((a, b) => b - a); // Snowflake 即时间序
const page = ids.slice(0, count);
return { entries: await hydrate(page), nextCursor: cursor + page.length };
}
async function hydrate(ids) { // 批量回填,杜绝 N+1
const tweets = await kv.mget(ids.map(i => `tweet:${i}`));
const authors = await kv.mget([...new Set(tweets.map(t => t.author))]
.map(u => `user:${u}`));
return tweets.map(t => ({ ...t, author: authors[t.author] }));
}
动手练习:① 把同步扇出改为消费 Kafka 的异步扇出(见T7),观察发推延迟变化;② 在 sort 之后插入一个简单 ranker(点赞数加权),体会排序对停留的影响;③ 压测 1 万粉丝用户发推,统计扇出耗时分布。
T2 · 教程:Snowflake 分布式 ID
DISTRIBUTED ID
64bit · 位运算 · 时钟回拨
难度 ★☆☆ · 耗时 1–2h · 栈 Node.js(BigInt)
原理:64 位 = 1 符号 + 41 位毫秒时间戳(可用 69 年)+ 10 位机器号(1024 节点)+ 12 位序列号(每毫秒 4096 个)。无需数据库自增即可全局唯一、趋势递增,且可从 ID 反解生成时间。
NODE.JS · 生成与反解
const EPOCH = 1288834974657n; // 2010-11-04,自定义纪元
let seq = 0n, lastTs = -1n;
function snowflake(machineId) { // machineId ∈ [0, 1023]
let ts = BigInt(Date.now());
if (ts === lastTs) {
seq = (seq + 1n) & 4095n; // 同毫秒内序列号自增取模
if (seq === 0n) { // 序列耗尽 → 自旋等下一毫秒
while (ts <= lastTs) ts = BigInt(Date.now());
}
} else {
seq = 0n;
}
if (ts < lastTs) throw new Error('clock moved backwards'); // 时钟回拨保护
lastTs = ts;
return ((ts - EPOCH) << 22n) | (BigInt(machineId) << 12n) | seq;
}
const parseTime = id => new Date(Number(id >> 22n) + Number(EPOCH));
console.log(parseTime(1953921047311282176n)); // 从任意推文 ID 反解时间
动手练习:① 把 10 位机器号拆成 5 位机房 + 5 位 worker;② 写基准测试统计每秒生成量;③ 对比 UUID v7 的长度、索引友好度与隐私泄露风险。
T3 · 教程:多级缓存与三大故障
CACHING PATTERNS
Cache-Aside · 击穿 · 雪崩
难度 ★★☆ · 耗时 2–3h · 栈 Node.js + Redis
原理:X 的读路径 97% 命中缓存。标准模式是 Cache-Aside:读未命中时回源并回填;写时「先更新库,再删缓存」。三个经典故障——穿透(查不存在的 key)、击穿(热点 key 过期瞬间)、雪崩(大批 key 同时过期)——分别用空值哨兵、singleflight 合并请求、TTL 抖动化解。
NODE.JS · CACHE-ASIDE + 防护三件套
const NULL = Symbol('NULL_SENTINEL');
const inflight = new Map();
async function getTweet(id) {
const key = `tweet:${id}`;
let v = cache.get(key);
if (v === undefined) { // 未命中
v = await singleflight(key, async () => { // ② 击穿:合并并发回源
return (await db.getTweet(id)) ?? NULL; // ① 穿透:空值哨兵
});
cache.set(key, v, 300_000 + Math.random() * 60_000); // ③ 雪崩:TTL 抖动
}
return v === NULL ? null : v;
}
function singleflight(key, fn) { // 同 key 只放一个请求回源
if (!inflight.has(key)) {
inflight.set(key, fn().finally(() => inflight.delete(key)));
}
return inflight.get(key);
}
async function updateTweet(id, patch) {
await db.updateTweet(id, patch); // 先写库
cache.del(`tweet:${id}`); // 再删缓存(可加延迟双删防脏读)
}
| 故障 | 现象 | 对策 |
|---|---|---|
| 穿透 | 大量查询不存在的 ID,直达数据库 | 空值哨兵 + 布隆过滤器 |
| 击穿 | 热点 key 过期瞬间并发打爆 DB | singleflight / 互斥重建 / 逻辑过期 |
| 雪崩 | 大批 key 同时失效 | TTL 加随机抖动 + 多级缓存兜底 |
动手练习:① 给 cache 加 LRU 淘汰并统计命中率曲线;② 模拟热点 key 过期,对比有无 singleflight 时 DB 的 QPS 峰值;③ 比较「删缓存」与「写缓存」两种更新策略的脏读窗口。
T4 · 教程:令牌桶限流
RATE LIMITING
Token Bucket · 分级 Key · 429
难度 ★☆☆ · 耗时 1–2h · 栈 Node.js(+Redis 进阶)
原理:令牌以固定速率注入桶中,请求到来时取走令牌,桶空即拒。相比漏桶,令牌桶允许一定程度的突发,更贴近真实用户体验。网关按 IP → 用户 → 设备多级 Key 叠加限流,并把配额写进响应头。
NODE.JS · 令牌桶与网关接入
class TokenBucket {
constructor(rate, capacity) { // rate: 每秒补充数
this.rate = rate; this.cap = capacity;
this.tokens = capacity; this.ts = Date.now();
}
take(n = 1) {
const now = Date.now();
this.tokens = Math.min(this.cap, this.tokens + (now - this.ts) / 1000 * this.rate);
this.ts = now;
if (this.tokens >= n) { this.tokens -= n; return true; }
return false;
}
}
const buckets = new Map();
const bucket = (key, rate, cap) =>
buckets.get(key) ?? buckets.set(key, new TokenBucket(rate, cap)).get(key);
app.use('/timeline', (req, res, next) => {
const b = bucket(`u:${req.userId}`, 300, 300); // 用户级
const ok = b.take() && bucket(`ip:${req.ip}`, 100, 200).take(); // 叠加 IP 级
res.set('x-rate-limit-remaining', Math.floor(b.tokens));
if (!ok) { res.set('retry-after', 42); return res.status(429).end(); }
next();
});
动手练习:① 用 Redis + Lua 重写 take(),压测分布式一致性;② 实现滑动窗口日志算法并对比两者对突发流量的表现;③ 给客户端加退避重试,画出 QPS 恢复曲线。
T5 · 教程:推荐召回与排序
RECALL & RANKING
多路召回 · 多目标打分 · 重排
难度 ★★★ · 耗时 4–6h · 栈 Node.js / Python
原理:推荐 = 漏斗。先用多路召回从亿级内容池捞出约 1,000 条候选,再用模型对候选打分排序,最后用启发式规则重排保证多样性。本节用一个「兴趣向量 + 余弦相似度」的简化模型复现这条管线。
NODE.JS · 召回 → 打分 → 重排
// ① 用户兴趣向量 = 历史互动推文的主题向量加权平均(简化版 SimClusters)
function userVector(uid) {
const hist = engagementHistory(uid); // 点赞/转推记录
return normalize(sum(hist.map(t => weight(t) * t.topicVec)));
}
const cos = (a, b) => dot(a, b) / (norm(a) * norm(b));
// ② 多路召回(并行执行,互相兜底)
async function recall(uid) {
const [follow, interest, hot] = await Promise.all([
inbox(uid, 400), // 关注路
topKByScore(candidatePool(), t => cos(userVector(uid), t.topicVec), 400), // 兴趣路
trending(regionOf(uid), 200), // 热门路
]);
return dedupe([...follow, ...interest, ...hot]); // ~1000 候选
}
// ③ 打分:多目标加权(简化版 48M 参数模型)
const W = { like: 1.0, reply: 1.3, retweet: 1.2, follow: 3.0, negative: -8.0 };
const score = (u, t) => Object.entries(W)
.reduce((s, [k, w]) => s + w * predict(u, t, k), 0);
// ④ 重排启发式:同作者最多连续 2 条,每 4 条穿插一个视频
function rerank(list) {
const out = []; let streak = {};
for (const t of list.sort((a, b) => score(u, b) - score(u, a))) {
if ((streak[t.author] ?? 0) >= 2) continue;
streak[t.author] = (streak[t.author] ?? 0) + 1;
out.push(t);
}
return interleaveMedia(out);
}
动手练习:① 把余弦相似度换成真实 ANN 索引(HNSW);② 加入「作者多样性」约束前后对比点击分布;③ 设计一个 1% 流量的 A/B 实验方案,明确北极星指标与护栏指标。
T6 · 教程:GraphQL 网关与 DataLoader
GRAPHQL GATEWAY
Schema · N+1 · 持久化查询
难度 ★★☆ · 耗时 3–4h · 栈 Node.js + graphql
原理:GraphQL 让客户端按需取字段,一次请求聚合多个领域。网关层必须解决两件事:N+1 查询(用 DataLoader 把同一帧内的请求合并成批量查询)与查询滥用(持久化查询白名单,只接受注册过的查询哈希)。
SCHEMA · 最小时间线模型
type Tweet { id: ID! text: String! author: User! stats: Stats! }
type User { id: ID! name: String! verified: Boolean! }
type Stats { like: Int! retweet: Int! reply: Int! }
type Timeline { entries: [Tweet!]! nextCursor: String }
type Query { homeTimeline(count: Int = 20, cursor: String): Timeline! }
NODE.JS · DATALOADER 消除 N+1
import DataLoader from 'dataloader';
// 每请求一个 loader:同一事件循环内的 load() 自动合并成一次批量查询
const userLoader = new DataLoader(async ids => {
const users = await db.users.findByIds(ids); // 单条 IN 查询
return ids.map(id => users.find(u => u.id === id)); // 顺序必须对齐
});
const resolvers = {
Tweet: {
author: t => userLoader.load(t.authorId), // 20 条推文 → 1 次 DB 查询
stats: t => countLoader.load(t.id),
},
Query: {
homeTimeline: async (_, { count, cursor }, ctx) => {
const ids = await timelineSvc.get(ctx.viewer.id, cursor, count);
return { entries: await hydrate(ids), nextCursor: ids.at(-1) ?? null };
},
},
};
// 持久化查询:只放行白名单哈希,拒绝任意 query 字符串
app.post('/graphql', (req, res) => {
const doc = registry.get(req.body.sha256);
if (!doc) return res.status(400).json({ error: 'unknown persisted query' });
return execute({ schema, document: doc, variableValues: req.body.variables });
});
动手练习:① 给 homeTimeline 加一个 subscription,新推文实时推送;② 实现一个权限中间件,未登录用户隐藏 stats.reply;③ 对比 DataLoader 开 / 关时的 SQL 条数与总耗时。
T7 · 教程:Kafka 事件管道
EVENT PIPELINE
分区 · 幂等消费 · 死信
难度 ★★☆ · 耗时 2–3h · 栈 Node.js + KafkaJS
原理:生产者按用户 ID 做分区键,保证同一用户的事件有序;消费者采用 at-least-once + 幂等 sink:处理成功后才提交位点,重放时靠去重表跳过已处理事件;反复失败的消息进死信队列人工介入。
NODE.JS · 生产与消费
// 生产者:key = 用户 ID → 同用户事件落入同一分区,保持顺序
await producer.send({
topic: 'tweet.create',
messages: [{ key: String(tweet.authorId), value: JSON.stringify(tweet) }],
});
// 消费者:at-least-once + 幂等 sink
for await (const batch of consumer.subscribe('tweet.create')) {
for (const msg of batch) {
const evt = JSON.parse(msg.value);
if (await dedupe.seen(evt.id)) continue; // ① 去重(Redis SETNX 24h)
try {
await Promise.all([ // ② 多个下游并行消费
fanout(evt), // 扇出服务
searchIndex(evt), // 搜索索引
riskScan(evt), // 风控扫描
]);
await dedupe.mark(evt.id);
} catch (e) {
if (msg.retries >= 3) await dlq.send(evt); // ③ 三次失败进死信
else throw e; // 否则重试
}
}
await consumer.commit(batch); // ④ 全部成功才提交位点
}
| 语义 | 含义 | 适用场景 |
|---|---|---|
| at-most-once | 可能丢,不重复 | 采样指标、日志 |
| at-least-once | 不丢,可能重复 | 主流场景 + 幂等 sink(X 默认) |
| exactly-once | 不丢不重 | 计费、事务型流处理(成本高) |
动手练习:① 用 Redis Streams 搭一个迷你版,理解 offset / 消费组概念;② 制造下游故障,观察死信队列与滞后(lag)变化;③ 给消费加背压:处理不过来时主动暂停拉取。
T8 · 教程:可观测性:RED 指标与链路
RED + TRACING
Rate · Errors · Duration
难度 ★☆☆ · 耗时 2h · 栈 Node.js + Prometheus
原理:每个服务先回答三个问题——每秒多少请求(Rate)、多少失败(Errors)、耗时分布(Duration)。用一个中间件就能采集 RED 三件套;再用贯穿全链路的 trace_id 把跨服务调用串成一条链路。
NODE.JS · RED 中间件 + TRACE_ID
app.use((req, res, next) => {
const t0 = process.hrtime.bigint();
req.traceId = req.headers['x-trace-id'] ?? crypto.randomUUID(); // 入口生成/继承
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
const labels = { route: req.path, status: res.statusCode };
metrics.counter('api_requests_total').inc(labels); // Rate + Errors
metrics.histogram('api_latency_ms').observe(labels, ms); // Duration 分布
});
next();
});
// 调用下游时透传 trace_id,即可拼出全链路
await fetch('http://timeline-svc/get', {
headers: { 'x-trace-id': req.traceId },
});
// 告警示例(PromQL):5xx 比例超 SLO 即触发
// sum(rate(api_requests_total{status=~"5.."}[5m]))
// / sum(rate(api_requests_total[5m])) > 0.0005
动手练习:① 在 Grafana 画一张 P50/P95/P99 三曲线面板;② 给慢请求(>800ms)自动打日志并附 trace_id;③ 用 k6 制造流量尖峰,验证告警在 1 分钟内触发。
T9 · 教程:学习路线图与书单
LEARNING ROADMAP
入门 · 进阶 · 专家
把本文档当成一张地图:先跑通最小系统,再逐层替换成真实组件。三个阶段,每阶段都有明确产出物,避免「只看不练」。
| 阶段 | 周期 | 目标 | 行动 | 产出物 |
|---|---|---|---|---|
| 入门 | 1–2 月 | 跑通最小闭环 | HTTP / SQL / 缓存基础;完成 T2、T4 | 可发推、可刷新的 CRUD 小站 |
| 进阶 | 3–6 月 | 逼近真实架构 | 完成 T1、T3、T6、T7;引入 Kafka 与 GraphQL | 带扇出、缓存、限流的迷你 Twitter + 压测报告 |
| 专家 | 6–12 月 | 理解规模化取舍 | 完成 T5、T8 与全部进阶 / 扩展教程(T10–T19);读论文;做分片与多区域复制 | 系统设计博客 / 开源贡献 / 故障演练记录 |
延伸阅读
建议节奏:每周至少 2 个晚上写代码 + 1 篇复盘笔记;每完成一个教程,回头对照本文档 01–27 章,标注「我现在理解了哪条设计决策、为什么」。
T10 · 进阶:手写倒排索引与搜索
INVERTED INDEX
分词 · posting list · BM25
难度 ★★☆ · 耗时 3h · 栈 Node.js
原理:搜索的核心是倒排索引——把词元映射到包含它的文档 posting list。建索引时对每条推文分词入表,查询时取各词 posting list 求交集,再用 BM25 打分、叠加时效与互动权重。生产环境由 ES 完成这一切,但手写一遍才能理解分词器、posting list、打分三要素(对应16 搜索系统)。
NODE.JS · 建索引与查询
const index = new Map(); // 词元 → posting list
const docs = new Map(); // id → 文档
function addDoc(doc) {
docs.set(doc.id, doc);
for (const [pos, w] of tokenize(doc.text).entries()) {
if (!index.has(w)) index.set(w, []);
index.get(w).push({ id: doc.id, pos }); // 记录位置,支持短语查询
}
}
function search(q, k = 20) {
const lists = tokenize(q).map(w => index.get(w) ?? []);
const ids = intersect(lists.map(l => new Set(l.map(x => x.id)))); // 从最短表开始交
return [...ids]
.map(id => ({ id, score: bm25(docs.get(id), q) * freshBoost(docs.get(id)) }))
.sort((a, b) => b.score - a.score)
.slice(0, k);
}
动手练习:① 支持 @用户 与 #话题 前缀语法;② 加「你是不是想找」纠错(编辑距离 ≤ 1);③ 对比 ngram 与分词法的命中率与索引体积。
T11 · 进阶:通知聚合与优先级
NOTIFICATION FANOUT
聚合窗口 · 去重 · 延迟投递
难度 ★☆☆ · 耗时 2h · 栈 Node.js + Redis
原理:高频低价值通知(点赞、关注)必须聚合,否则用户会关掉通知。通用做法是聚合窗口:窗口期内同一目标的事件累计在 Redis 集合里,窗口到期统一 flush,渲染成「某某和其他 9 人喜欢了你的帖子」;高价值通知(提及 / 回复)跳过窗口直接投递(对应17 通知系统)。
NODE.JS · 窗口聚合
const WINDOW = 10 * 60_000; // 10 分钟聚合窗口
async function onEvent(evt) {
if (evt.type === 'mention' || evt.type === 'reply') return deliver(evt); // P0 直发
const key = `agg:${evt.targetId}:${evt.type}`;
const isNew = await redis.sadd(key, evt.actorId); // 参与者集合,天然去重
await redis.pexpire(key, WINDOW);
if (isNew === 1) scheduler.push({ key, at: Date.now() + WINDOW }); // 首事件定时
}
async function flush({ key }) {
const actors = await redis.smembers(key);
if (!actors.length) return;
await redis.del(key);
deliver({ // 渲染聚合文案
text: actors.length === 1
? `${name(actors[0])} 赞了你的帖子`
: `${name(actors[0])} 和其他 ${actors.length - 1} 人赞了你的帖子`,
icons: avatars(actors.slice(0, 3)), // 头像堆叠最多 3 个
});
}
动手练习:① 加优先级队列,让 mention 抢占聚合任务;② 模拟一次 1 万点赞风暴,验证通知条数从 1 万压缩到 1;③ 实现「静默时段」并测试跨时区用户。
T12 · 进阶:端到端加密简化实现
E2E ENCRYPTION
ECDH · 棘轮 · 前向安全
难度 ★★★ · 耗时 4h · 栈 Node.js + tweetnacl
原理:E2E 的核心是两件事——密钥协商(ECDH/X3DH 让两端算出共享密钥,服务器不参与)与密钥棘轮(每轮通信都推进密钥,即使某把密钥泄露也不影响之后的消息,即前向安全)。MLS 在群聊中用密钥树把成员变更的轮换成本从 O(N) 降到 O(log N)(对应18 DM 消息)。
NODE.JS · 会话骨架(简化)
// ① 握手:ECDH 派生会话根密钥(生产需 X3DH + 预密钥包)
const root = HKDF(ECDH(alice.priv, bob.pub), salt);
// ② 棘轮:每次收发都推进根密钥 → 前向安全
function ratchet(state, theirNewPub) {
const dh = ECDH(state.ephPriv, theirNewPub);
state.root = HKDF(state.root, dh);
state.ephPriv = keygen(); // 自己的临时密钥也换新
return {
sendKey: HKDF(state.root, 'send'), // 收发密钥分离
recvKey: HKDF(state.root, 'recv'),
};
}
// ③ 消息:AES-GCM 认证加密 = 保密 + 完整性
const enc = (key, msg) => AES_GCM_seal(key, nonce(), msg);
动手练习:① 用 tweetnacl 的 box/open 跑通真实加解密;② 模拟中间人替换公钥,理解签名的作用;③ 阅读 RFC 9420 中群聊密钥树章节,画出成员退出时的路径更新。
T13 · 进阶:A/B 分桶与指标计算
EXPERIMENTATION
稳定哈希 · 正交层 · holdout
难度 ★☆☆ · 耗时 1–2h · 栈 Node.js
原理:分桶只有一个要求——稳定:同一用户每次请求、每台设备都必须落在同一组,所以不能用随机数,而要把 user_id 与实验 id 一起哈希取桶;多实验用正交层组织,层 id 参与哈希,保证实验组合均匀分布;再预留 holdout 组做长期观察(对应21 实验平台)。
NODE.JS · 稳定分桶
function fnv1a(str) { // 快速非加密哈希
let h = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 0x01000193) >>> 0;
}
return h;
}
function variant(userId, exp) { // exp: { layer, id, control: 50, treat: 45 }
const b = fnv1a(`${exp.layer}:${exp.id}:${userId}`) % 100;
if (b < exp.control) return 'control';
if (b < exp.control + exp.treat) return 'treatment';
return null; // holdout,不参与实验
}
// 指标:互动率提升 + 置信区间(简化)
const lift = (t, c) => (t.mean - c.mean) / c.mean;
动手练习:① 用 10 万模拟用户验证分桶均匀性(卡方检验);② 加层正交校验,统计层间组合分布;③ 用 bootstrap 给点击率差值算 95% 置信区间。
T14 · 进阶:虚拟长列表:时间线渲染
VIRTUAL LIST
Windowing · 缓冲 · 回收
难度 ★★☆ · 耗时 2–3h · 栈 React
原理:时间线可能有上千条,但一屏只看得见 5–10 条。全量渲染会让 DOM 爆炸、滚动掉帧。虚拟列表只渲染「可视区 ± 缓冲」的窗口:容器用总高度撑开保证滚动条真实,条目绝对定位按序摆放,滚动时重算窗口起止下标——DOM 节点数从 N 降到 ~20。
REACT · 定高虚拟列表
function VirtualTimeline({ items, itemH = 140, viewH = 800 }) {
const [scrollTop, setScrollTop] = useState(0);
const BUFFER = 5; // 上下各缓冲 5 条
const start = Math.max(0, Math.floor(scrollTop / itemH) - BUFFER);
const end = Math.min(items.length, start + Math.ceil(viewH / itemH) + BUFFER * 2);
return (
<div className="viewport" style={{ height: viewH, overflow: 'auto' }}
onScroll={e => setScrollTop(e.currentTarget.scrollTop)}>
<div style={{ height: items.length * itemH, position: 'relative' }}>
{items.slice(start, end).map((it, i) => (
<TweetCard key={it.id} data={it}
style={{ position: 'absolute', top: (start + i) * itemH, width: '100%' }} />
))}
</div>
</div>
);
}
动手练习:① 对比虚拟化前后的 DOM 节点数与滚动帧率;② 实现动态高度版(测量回写 + 位置缓存);③ 结合 IntersectionObserver 做提前 3 屏的下一页预取。
T15 · 进阶:熔断器与服务降级
CIRCUIT BREAKER
三态机 · 快速失败 · 降级阶梯
难度 ★★☆ · 耗时 2h · 栈 Node.js
原理:下游故障不该拖垮上游。熔断器三态:CLOSED正常放行;连续失败达到阈值切换OPEN,后续请求快速失败并走降级;冷却期后进入HALF_OPEN试探放行一次,成功则关闭、失败重开。时间线场景下推荐服务熔断即降级为关注流——用户几乎无感(对应设计原则「优雅降级」)。
NODE.JS · 三态熔断器
class CircuitBreaker {
constructor(fn, { threshold = 5, cooldown = 10_000 } = {}) {
this.fn = fn; this.threshold = threshold; this.cooldown = cooldown;
this.fails = 0; this.state = 'CLOSED'; this.openAt = 0;
}
async call(fallback) {
if (this.state === 'OPEN') {
if (Date.now() - this.openAt > this.cooldown) this.state = 'HALF_OPEN';
else return fallback?.(); // 开路期间:快速失败 + 降级
}
try {
const r = await this.fn();
if (this.state === 'HALF_OPEN') { this.fails = 0; this.state = 'CLOSED'; }
return r;
} catch (e) {
if (this.state === 'HALF_OPEN' || ++this.fails >= this.threshold) {
this.state = 'OPEN'; this.openAt = Date.now();
}
return fallback?.();
}
}
}
// 时间线场景:推荐熔断 → 降级关注流
const recTimeline = new CircuitBreaker(() => svc.recommended(uid));
const timeline = await recTimeline.call(() => svc.followingOnly(uid));
动手练习:① 注入故障,画出三态切换时间线;② 为时间线设计完整四级降级预案并演练;③ 结合T1验证「关注流兜底」在推荐全挂时真的可用。
23 · 反滥用与内容治理
TRUST & SAFETY
可见性分级 · 风险评分 · 申诉
治理理念是「言论自由,而非触达自由」:高风险内容不一刀切删除,而是按可见性分级限制触达。防线共四层:边缘 Bot 评分在入口拦截机器流量;abuse.signals实时流(见09)喂给风险评分模型;visibility 服务统一执行分级裁决(与13威胁矩阵呼应);人工复核 + 申诉通道完成最后纠偏。媒体完整性依赖哈希指纹库(CSAM / 恐袭内容),上传即比对拦截已知违规。
可见性分级表
| 层级 | 动作 | 典型触发 | 用户感知 |
|---|---|---|---|
| T0 正常 | — | 低风险 | 无 |
| T1 降权 | 减少推荐分发 | 风险评分灰区 | 无感知,可申诉查状态 |
| T2 回复警示 | 回复需点开、默认折叠 | 中度攻击性 | 回复前弹提示 |
| T3 内容警示 | 敏感媒体覆盖层 | 媒体分类器 | 点开才可见 |
| T4 功能限制 | 限发帖 / 限关注 | 高置信违规 | 站内信告知理由 |
| T5 移除与封禁 | 内容删除、账号停用 | 法律要求 / 严重违规 | 正式通知 |
所有分级都可申诉,申诉队列 SLO 48h;规则条款公开、执行记录可审计;透明度报告按季度披露政府数据请求与内容处置量。模型误伤的兜底永远是人工通道——治理系统的最后一道防线是人。
24 · 多区域容灾与双活
MULTI-REGION DR
RPO/RTO · 切换演练 · 混沌工程
演进路径:单数据中心 → 三地备份 → 多云双活。写路径仍以主区域为主——资金、ID 生成等强一致域永远保持单写者原则;读路径与可重建数据全球分布;欧盟数据驻留合规要求独立存储集群。RPO/RTO 按领域分级承诺,每季度真实切换演练,混沌工程常态化(见教程T19)。
分领域 RPO / RTO 承诺
| 领域 | RPO | RTO | 恢复策略 |
|---|---|---|---|
| 核心写入(发推) | ≈0(半同步) | < 15min | 区域级主从切换 |
| 时间线读 | 缓存可重建 | < 5min | 降级关注流(见T15) |
| 媒体 Blob | 跨区异步复制 | < 30s | 多云回源自动切换 |
| 搜索索引 | 可重建 | < 2h | Kafka 重放重建 |
| DM 密文 | 3 副本 | < 15min | 密钥服务优先恢复 |
演练日历
双活 ≠ 双写:只有读域与可重建数据做多区域 active,强一致域保持单写者 + 同步复制。把「所有东西都双活」是多云架构最常见的误解,也是事故的主要来源。
25 · 计数系统深挖
COUNTING AT SCALE
合并写 · 近似算法 · 展示分层
点赞、转推、浏览量是写入频率最高的场景(合计 >900K/s),直接更新数据库会立即形成热点。方案是三件事:事件流合并写——1 秒窗口微批求和后批量增量,源头是09的 engagement.events;展示分层——全局计数接受 ±1s 最终一致,个人「我是否点过赞」走强一致 read-your-writes(对照08一致性矩阵);近似算法——浏览量去重统计用 HyperLogLog。展示层统一压缩大数字(1.2万 / 1.2M)。
四类计数的路径与承诺
| 计数 | 写入路径 | 一致性 | 展示策略 |
|---|---|---|---|
| 点赞 / 转推数 | Kafka 合并写(1s 微批) | ±1s 最终一致 | 压缩显示 1.2万 |
| 浏览量 | HyperLogLog 近似去重 | 误差 ±5% | 超过阈值才显示 |
| 粉丝数 | 缓存 + 异步校正 | 自己强一致、他人最终 | 主页实时、列表缓存 |
| 我是否点赞 | 本地状态 + DB 双写 | 强一致 | 乐观渲染即时反馈 |
工程实现见教程T16 分布式计数器。计数校正:夜间对账任务以事件流重放为准重算全量计数,纠正合并写丢失或重放重复造成的漂移——「最终一致」的「最终」靠对账保证,而不是靠祈祷。
26 · 国际化与本地化
I18N & L10N
RTL · ICU · 加权字数
支持40+ 语言,含右起书写文字(阿拉伯语、希伯来语)。四大挑战:布局镜像——设计系统(见04)整体支持 RTL,图标选择性翻转;复数与格式——ICU MessageFormat + CLDR 日期数字格式;字数加权——280 上限按权重计:拉丁字符 1、CJK 计 2;翻译——帖子由 LLM 机器翻译 + 社区校对。本地化流水线:字符串提取 → 翻译平台 → 伪本地化测试 → 发布。
加权字数与 ICU 复数
// 加权文本长度:CJK 计 2(X 280 上限的真实规则)
const weight = c => /[\u2E80-\u9FFF\uFF66-\uFFEF]/.test(c) ? 2 : 1;
const weightedLen = s => [...s].reduce((n, c) => n + weight(c), 0);
// ICU MessageFormat 处理复数(每种语言的复数规则都不同)
"{count, plural, =0 {还没有赞} other {# 个赞}}"
RTL 不只是镜像翻转:数字方向、时间线排序、动画方向、滑动返回手势都要按语言翻转——设计 Token 层提供 dir 全局开关,业务代码禁止硬编码 left/right。
27 · 开放平台与开发者 API
PLATFORM API
分级订阅 · OAuth · Webhook
对外接口以REST v2为主,按分级订阅:Free / Basic / Pro / Enterprise,各级有不同的读写配额与端点范围;OAuth 2.0 PKCE 用于第三方登录与应用授权管理;Webhook(Account Activity API)把事件推给开发者服务器,HMAC 签名防伪造;合规数据接口面向学术机构。外部接口复用与内部相同的网关(05)限流(T4)与幂等机制——对外 API 本质是内部网关能力的出口。
API 分级
| 层级 | 读取配额 | 写入配额 | 典型场景 |
|---|---|---|---|
| Free | 抽样端点 | 500 帖 / 月 | 学习试用 |
| Basic | 1 万读 / 月 | 3 千帖 / 月 | 小型机器人 |
| Pro | 100 万读 / 月 | 30 万帖 / 月 | 商业产品 |
| Enterprise | 定制 · Firehose | 定制 | 研究 / 舆情监测 |
废弃策略:接口退役提前 6 个月公告 + sunset HTTP 头 + changelog 双周更新;限流头完整透出配额让开发者自适应(429 示例见05)。Webhook 重试采用指数退避,签名校验失败的推送直接丢弃并告警。
T16 · 扩展:分布式计数器
DISTRIBUTED COUNTERS
合并写 · HLL · 读写分离
难度 ★★☆ · 耗时 2h · 栈 Node.js + Redis
原理:高频计数直接写库会形成热点。两个关键手段:①合并写——1 秒窗口内的事件先求和,一次 INCRBY 顶上千次单写;②读写分层——全局计数最终一致即可,个人状态(我是否点赞)必须强一致。浏览量这类「去重统计」用 HyperLogLog 近似,误差不到 1%(对应25 计数系统)。
NODE.JS · 合并写 + 近似去重
// ① 合并写:1s 窗口微批,千次事件变成一次 INCRBY
const buf = new Map();
function onLike(tweetId) { buf.set(tweetId, (buf.get(tweetId) ?? 0) + 1); }
setInterval(async () => {
for (const [id, n] of buf) await redis.incrby(`cnt:${id}`, n);
buf.clear();
}, 1000);
// ② 读:全局计数与个人状态分离
const likeCount = await redis.get(`cnt:${id}`); // 最终一致 ±1s
const iLiked = await redis.sismember(`liked:${uid}`, id); // 强一致
// ③ 浏览量去重:HyperLogLog 近似基数(标准误差 ≈ 0.81%)
await redis.pfadd(`views:${id}`, uid);
const views = await redis.pfcount(`views:${id}`);
动手练习:① 模拟 1 万 QPS 点赞,对比合并写与单写的 Redis 压力;② 对比 HLL、位图、精确集合三者的内存占用与误差;③ 写一个「计数校正」对账 job 并故意制造丢失验证它能修复。
T17 · 扩展:趋势热点:Count-Min Sketch
TRENDING / HEAVY HITTERS
滑动窗口 · Top-K · 反操纵
难度 ★★★ · 耗时 3h · 栈 Node.js
原理:趋势要在 3.5M/s 的事件流里找出滑动窗口内最热的词。全量排序内存会爆炸,工业界用 Heavy Hitters 近似算法:Count-Min Sketch用多行哈希表估计频率(只会高估不会低估),候选再用精确计数复核;配合增速与质量分产出趋势榜,并按地域 / 语言分片独立计算(对应09流式管道)。
NODE.JS · CMS + 滑动窗口
class CountMinSketch {
constructor(w = 2048, d = 4) {
this.t = Array.from({ length: d }, () => new Int32Array(w));
this.w = w;
}
hash(s, i) { return fnv1a(i + s) % this.w; }
add(key, n = 1) { this.t.forEach((row, i) => row[this.hash(key, i)] += n); }
est(key) { // 取多行最小值压低高估
return Math.min(...this.t.map((row, i) => row[this.hash(key, i)]));
}
}
// 双桶滑动窗口:两个 30min 桶轮换,过期桶清零后复用
// 每 5min 对候选重打分:score = z(频率)*0.6 + z(增速)*0.3 − 垃圾风险
动手练习:① 用 Zipf 分布数据验证估计误差随宽度的变化;② 实现双桶轮换的 1 小时滑动窗口;③ 加地域分片,再做跨片聚合出全球榜。
T18 · 扩展:特性开关与 Kill Switch
FEATURE FLAGS
发布解耦 · 灰度 · 生命周期
难度 ★☆☆ · 耗时 2h · 栈 Node.js
原理:特性开关把「部署」和「发布」解耦——代码可以合并上线但默认关闭,再按百分比 / 用户标签 / 地域灰度打开;事故时 Kill Switch 秒级全关,无需回滚构建。开关有生命周期:全量后 2 周内必须清理,否则开关债务会让组合状态爆炸(配合10 交付流水线的 canary 机制)。
NODE.JS · 开关求值与 Kill Switch
const flags = { darkTimeline: { pct: 20, tags: ['beta'], kill: false } };
function isEnabled(flag, user) {
const f = flags[flag];
if (!f || f.kill) return false; // Kill Switch:一键全关
if (user.tags.some(t => f.tags.includes(t))) return true; // 白名单优先
return bucket(user.id, flag) < f.pct; // 百分比用稳定分桶(见 T13)
}
// 用法:代码分支收敛在开关处,而非发布分支
render(isEnabled('darkTimeline', user) ? <NewTimeline/> : <ClassicTimeline/>);
动手练习:① 加配置中心推送实现秒级热更新;② 写生命周期 lint:全量超 14 天未清理自动告警;③ 设计一条「开关 → 实验 → 全量 → 清理」的完整流水线。
T19 · 扩展:混沌工程演练
CHAOS ENGINEERING
故障注入 · 爆炸半径 · 假设验证
难度 ★★★ · 耗时 4h · 栈 Node.js + K8s
原理:可靠性不是设计出来的,是演练出来的。混沌工程 = 在生产环境主动注入受控故障,验证系统假设:杀死单 pod、切断缓存、切换区域 DNS。核心循环是「假设 → 最小爆炸半径 → 观察 → 复盘」;先在 staging 跑,再到生产低峰窗口,全程可一键中止(配合24 多区域容灾的演练日历)。
NODE.JS · 简易故障注入框架
const chaos = {
enabled: env === 'chaos',
async maybe(name, fault, normal) {
if (this.enabled && injectRate(name) > Math.random()) {
log(`💥 inject: ${name}`);
return fault(); // 模拟延迟 / 异常 / 断连
}
return normal(); // 正常路径
},
};
// 演练假设:缓存全挂时 SLO 不破(用 T8 的指标做裁判)
const v = await chaos.maybe('cache-down',
() => { throw new Error('cache unreachable'); },
() => redis.get(key));
expect(p99('api_latency_ms')).toBeLessThan(800); // 假设成立?
动手练习:① 设计三个假设:单实例死亡 / Redis 断连 / 下游延迟 +500ms,逐一注入验证;② 画出注入前后的 P99 对比图;③ 把演练流程写成季度 gameday 剧本,包含角色分工与中止条件。
X.COM ARCHITECTURE SPEC · v15.0 · 27 章 + 19 教程 · 2026-08-09 本文为架构研究示例文档,非 X Corp 官方资料;数据为公开信息与行业惯例的整理演示 ↑ 返回顶部 · 打印 / 导出 PDF