Slack架构解耦消息落盘与高频互动 规避热点行锁瓶颈

[Chinese] Case Study: Design Slack — Scott Shi

Scott Shi

分布式即时通讯系统必须将静态消息存储与高频交互状态彻底解耦,在核心数据表原地更新字段或强求分布式强一致性必然导致行锁竞争雪崩与系统性能断崖式下跌。

AI-assisted summary of [Chinese] Case Study: Design Slack — original source. Check the original for full context.

Source published:

Key ideas

  • 存储架构核心解耦:账户与群组元数据由分片 SQL 承载,海量消息存储依托支持线性水平扩展的 DynamoDB,高频 Emoji 反应则独立至 Redis 内存计数器,严禁在核心消息记录上频繁原地更新引发行锁竞争。 (25:00)
  • 关系与未读游标设计:好友表采用以 user_id_1 为分片键的双向冗余存储避免广播扫描,单聊与群聊均通过独立的 last_view_date 游标比对识别未读,规避在大群消息体上打全员阅读标记的失控复杂度。 (55:00)
  • 多媒体与状态机异步流转:消息状态机严格区隔 Received 与 Sent,大文件通过客户端直传 S3 预签名 URL 结合异步 Worker 池处理,完成转码、缩略图提取与安全扫描后再推向 CDN 分发。 (35:00)
  • 一致性与分布式时序妥协:依赖 APNs/FCM 等第三方通道的通知推送应放弃损耗极大的两阶段提交(2PC),以 At-Least-Once 加幂等重试为主;而在海量分布式节点下,除依托硬件原子钟外绝对消息顺序无法 100% 得到保证。 (45:00)

Detailed notes

## Background 该视频由 Scott Shi 于 2020 年 11 月 1 日发布在 YouTube 平台上。视频简介指出,这是其首期专注于即时通讯软件 Slack 的系统设计案例分析,并附带了对应的 Google Docs 设计方案讲义。

## TL;DR 本期内容围绕设计大型企业即时通讯系统 Slack 展开,系统性剖析了从业务需求梳理、容量估算到微服务拆分的完整链路。讲者重点讲解了核心消息流与富媒体后处理管道的设计,并在存储选型上深入对比了分片关系型数据库(SQL)、DynamoDB 与 Redis 的适用边界,规避诸如高频就地更新与热点行锁等架构陷阱。此外,讲者还透彻推导了未读消息游标、群发扩散(Fan-out)、分布式消息乱序以及基于 WebSocket 的长连接扩展方案,并结合工业界实战数据剖析了系统达成无限水平扩展(Infinite Scale)的真实工程权衡。

## Atmosphere & style 整场讲解洋溢着浓厚的大厂资深技术评审氛围,兼具工业界老兵的务实硬核与循循善诱的导师风格。讲者毫不避讳地拆解业内浮夸的“基准测试神话”,频繁结合在 Uber 等一线大厂踩过的真实工程暗礁与生产监控指标,让复杂的架构推导变得生动而具象。现场互动直接且充满交锋感,从驳回图数据库执念到直言分布式时钟乱序的不可避免,给观众带来一种如同坐在硅谷顶级架构委员会会议室、直面严苛而深刻的技术复盘体验。

## Bulletin - [25:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=1500s) 存储架构核心解耦:账户与群组元数据由分片 SQL 承载,海量消息存储依托支持线性水平扩展的 DynamoDB,高频 Emoji 反应则独立至 Redis 内存计数器,严禁在核心消息记录上频繁原地更新引发行锁竞争。 - [55:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=3300s) 关系与未读游标设计:好友表采用以 user_id_1 为分片键的双向冗余存储避免广播扫描,单聊与群聊均通过独立的 last_view_date 游标比对识别未读,规避在大群消息体上打全员阅读标记的失控复杂度。 - [35:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2100s) 多媒体与状态机异步流转:消息状态机严格区隔 Received 与 Sent,大文件通过客户端直传 S3 预签名 URL 结合异步 Worker 池处理,完成转码、缩略图提取与安全扫描后再推向 CDN 分发。 - [45:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2700s) 一致性与分布式时序妥协:依赖 APNs/FCM 等第三方通道的通知推送应放弃损耗极大的两阶段提交(2PC),以 At-Least-Once 加幂等重试为主;而在海量分布式节点下,除依托硬件原子钟外绝对消息顺序无法 100% 得到保证。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=0s) 系统设计如同“堆积木”,API 与数据模型决定积木形状,存储选型决定积木抗压材质;开局应预留时间搞清业务场景与扩展边界,避免因缺乏业务直觉引发系统全盘推倒重来。 - [03:45](https://www.youtube.com/watch?v=-UXWETzHG38&t=225s) 架构评审中最致命的是底层硬伤(如存在漏洞的并发控制机制),在方案设计初期暴露并拦截理论级硬伤,能规避上线后长达数年的高昂架构纠偏代价。 - [11:15](https://www.youtube.com/watch?v=-UXWETzHG38&t=675s) 容量估算需辩证看待基准测试与实际业务差异,单机数百万 QPS 极限通常基于纯内存只读,真实线上混合读写单机达到 10 万 QPS 即属于稳健表现。 - [20:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=1200s) 基础关系型数据(账户、好友、群组)适合选用 SQL 数据库并辅以缓存层;聊天消息核心持久化选用 DynamoDB,借助原生水平扩展和 DAX 缓存应对高吞吐与读时间局部性。 - [25:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=1500s) 系统设计必须规避在核心实体(如 Message、User 表)上高频就地更新动态数据(如 Emoji 反应、GPS 定位)的红线,需通过独立服务与 Redis 内存表解耦并发锁竞争。 - [30:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=1800s) API 契约采用 gRPC/Protobuf 替代 Thrift,网关统一为消息注入生命周期元数据与状态机(Received、Sent、Read、Deleted),清晰隔离外部业务接口与内部管道接口。 - [35:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2100s) 多媒体后处理采用客户端 S3 预签名直传加队列 Worker 异步处理管道,由 Received 状态吸收安全审查与转码延迟,避免大文件上传挤占核心网关带宽。 - [45:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2700s) 跨第三方通道的通知推送若强行依赖两阶段提交(2PC)实现 Exactly-Once 会导致性能降低一个数量级,工业界更倾向于采用 At-Least-Once 结合业务端幂等与后台补偿。 - [55:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=3300s) 好友关系表采用双向冗余存储(A-B 与 B-A 两条记录),以 user_id_1 作为单向哈希分片键,从而将全局广播扫描转为单分片精准读取,并利用 last_view_date 游标判定未读。 - [01:00:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=3600s) 群组成员关系表(group_membership)进行行级规范化拆分,各成员拥有独立的 last_view_date 字段,用户切入群聊仅触发自身记录的点查更新,不会产生全群争抢单行互斥锁的问题。 - [01:07:33](https://www.youtube.com/watch?v=-UXWETzHG38&t=4053s) DynamoDB 消息表以容器 ID(群 ID 或双人 ID 排序组合)作为 Partition Key、时间戳作为 Sort Key,满足物理有序的高效范围检索与分页,但直接通过 Message ID 单点点查需依赖有最终一致性延迟的 GSI。 - [01:15:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=4500s) 客户端时间戳易被篡改,而分布式服务端时钟因网络抖动和时钟漂移无法避免微小错序;除 Google Spanner 等依赖硬件原子钟的系统外,绝对消息时序在分布式网络中无法 100% 保证。 - [01:18:40](https://www.youtube.com/watch?v=-UXWETzHG38&t=4720s) 群发消息向成员 Channel Table 写入扩散(Fan-out)在分片 Redis 的支撑下能在毫秒级内完成,但严禁在 User 记录中将群列表作为数组字段频繁更新,否则核心用户易成为热点行引发性能雪崩。 - [01:25:28](https://www.youtube.com/watch?v=-UXWETzHG38&t=5128s) 用户在线状态感知应依托网关集群的 WebSocket 双向长连接与定期心跳机制,将高频活跃状态抽离至独立的活跃用户表或内存服务,通过无状态水平扩容分摊连接压力。 - [01:34:45](https://www.youtube.com/watch?v=-UXWETzHG38&t=5685s) 原生 SQL 数据库缺乏多节点分片感知能力,必须依托应用层与 ZooKeeper 等分布式协调组件维护分片映射与路由查找(如 Uber Schemaless 架构),才能实现关系型存储的高可用水平扩展。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=0s) 系统设计的业务直觉与“堆积木”哲学

设计系统绝不能只关注眼前死板的技术实现,在工业界实际工作中,最核心的能力之一是预判“近期需求”(immediate near future features requirement)。如果技术人员缺乏 business sense,跟偏重业务的 Product Manager 沟通时就会频遭挑战。例如 PM 提出在用户档案里加一个出生日期(Date of Birth)并支持按生日搜索,工程师如果回答“这得全盘重构系统架构(totally restructure)”,就会给人留下极不靠谱的职业印象。因此在面试或实战开局时,建议至少花十分钟搞清楚业务场景(Business Use Case)与常识边界,第二个十分钟梳理系统约束条件(Constraints),随后再将整体架构、API 与数据模型/数据库选型结合起来推导。

```mermaid flowchart TD accTitle: System Design Time Allocation Flow accDescr: Recommended interview roadmap showing time spent on use cases, constraints, and architecture. subgraph Prep ["前期明确 (20 Mins)"] A["Business Use Case & Common Sense<br/>(前 10 分钟:搞清业务背景与近期扩展)"] --> B["Constraints & Scale Estimation<br/>(后 10 分钟:量化约束与计算)"] end subgraph Arch ["核心落地"] B --> C["High-Level Architecture<br/>(划分子服务/积木形状)"] C --> D["Database Choice & Data Models<br/>(积木材质:MySQL vs Cassandra)"] D --> E["Deep Dive & Bottlenecks<br/>(容量瓶颈/并发控制)"] end ```

分布式系统设计本质上就像“堆积木”。面试官如果提出要求搭建一座埃菲尔铁塔,并要求塔顶能承受一公斤重的旗帜或糖果: - **积木的形状**:对应 API 设计和数据结构,必须拼装出契合埃菲尔铁塔外观的轮廓。 - **积木的材质**:对应存储选型与底层基础设施。如果要承载 1000 万用户同时在线并发,你就必须深刻理解 MySQL 与 Cassandra 等不同积木的物理特性与抗压强度差异。

[03:45](https://www.youtube.com/watch?v=-UXWETzHG38&t=225s) 评审中的两种分歧与“围棋之神”理论

在架构评审或 Hacker News 的技术讨论中,常见的质疑(disagree)通常分为两类: 1. **根本性不可行(Fundamentally not working)**:系统在理论或逻辑底层就存在硬伤。讲者分享了自己的一段真实经历:曾花了一个月时间做方案准备,信心满满地向杰出工程师(Distinguished Engineer)汇报,结果被当场指出并发控制中采用的乐观锁(optimistic locking)机制存在根本性漏洞,完全无法正常 work,方案被直接打回重做。虽然这种场面非常尴尬(embarrassing),但能在架构评审阶段从理论层面提前拦截错误是极其幸运的("You're blessed, you're fortunate to find it from system design level")。代码级 Bug 可以短时间修复,但架构底层的重大缺陷一旦上线,纠偏往往要以年为单位计算。 2. **缺乏最优解的质疑(Not the best solution)**:评审者认为这未必是最佳方案,但自身也拿不出更好的解法。因为现实中的系统设计充斥着各种约束与假设,很多时候只有全知全能的“神”(Oracle)才知道绝对最优解。如同 Google 研发的 AlphaGo 横扫职业棋坛,围棋解说常感慨或许存在技艺更高的“围棋之神”(OracleGo),但人类眼下造不出比 AlphaGo 更强的棋手。只要某种方案是当前边界下最强的 working solution,就是极佳的设计。现实中许多通用工具(如使用起来较为死板的 DynamoDB)并非完全契合特定业务,除非是底层 Infra 团队有资源去彻底重复发明轮子(reinvent the tool),否则大多数业务团队必须在现有工具上做出妥协与权衡。

[08:50](https://www.youtube.com/watch?v=-UXWETzHG38&t=530s) Slack 的功能降级、MVP 拆分与非功能性指标

面对复杂的聊天系统设计,必须将庞大的系统拆解为最小可行产品(MVP)功能与加分项(Bonus Features): - **核心功能范围**:点对点私聊(Direct Message)、群聊(Group Message)、链接预览(Link Preview)、多媒体消息、未读高亮与通知。 - **前提假设(Assumptions)**:假定认证与授权服务(AuthN/AuthZ)已就绪,所有传入请求均携带合法的 User Token 来验证身份;同时主动将客户端(Mobile App / Web Client)的展示层细节剔除在后端架构讨论之外。 - **非功能性目标**: - **高可用性(High Availability)**:服务时刻保持在线可用。 - **高可靠性(High Reliability)**:消息绝不能丢失。 - **一致性(Consistency)**:保证各客户端看到的聊天流顺序与内容一致,不能出现 A 与 B 看到的聊天记录严重脱节。 - **实时性(Real-time)**:消息必须低延迟、相对及时地推送到端。

[11:15](https://www.youtube.com/watch?v=-UXWETzHG38&t=675s) 容量估算:从常规分片到 Expensify 的单机“泥石流”方案

根据规模进行粗略量化估算: - 设日活用户(DAU)为 1000 万,每人每天发送 100 条消息; - 峰值流量按平均流量的 5 倍计算,系统需承受的写入吞吐量约为 50,000 QPS(message/sec); - 若单条消息估算为 100 字节,网络写入吞吐约为 100 MB/s;每日消息存储增量约为 10 TB。假设群组上限为 1000 人。 - 存储保留策略:若设置 TTL 则可滚动淘汰,若类似 Discord 承诺永久保留,则需引入冷热分层优化(用户几乎不会翻阅两年前的历史消息)。

常规经验法则(Rule of thumb)中,当 QPS 超过 1,000 时,单机即存在单点故障(Single Point of Failure),至少需要双机冗余;当吞吐量达到 1,000 至 10,000 QPS 时,通常必须进行分区(Partition)和分库分表。

然而讲者补充了一起极具启发性的“反模式/泥石流”案例:Expensify CEO 曾撰文分享其极端的单机纵向扩展(Scale-up)实践。他们依靠一台怪兽级单机运行 SQLite 处理了高达 4,000,000 QPS(全部为读请求): - **硬件配置**:192 个物理 CPU 核心、1 TB 内存、3 TB 高性能 SSD,在 AWS 上的等效硬件租用成本约为每年 24 万美元。 - **架构极简奇招**:放弃内网 DNS 解析服务,将各服务主机的映射硬编码在 `/etc/hosts` 中,配置文件转为 JSON,通过脚本利用 `scp` 批量同步到目标主机。

[15:10](https://www.youtube.com/watch?v=-UXWETzHG38&t=910s) 资源约束瓶颈与数据库边界

在容量规划中,瓶颈通常由两类维度决定: 1. **QPS 瓶颈 vs 数据包大小瓶颈**:某些数据分析类任务每三小时仅处理一个请求,但单个请求体高达 2 GB;而即时通信则是小报文、高 QPS 模式。 2. **底层硬件与树高开销**:单台 MySQL 实例承载的建议数据量通常在 500 GB 上下(留出磁盘余量给系统和其他进程)。若单表 B+ 树层级达到 3 层,对应索引及数据容量通常在数十吉字节以内;一旦攀升到 4 层,性能和磁盘 I/O 代价便会大幅上升。 3. **真实资源瓶颈排序**:网络带宽往往不是即时通讯服务最致命的硬约束,核心瓶颈通常落在数据库的磁盘 I/O 读写延迟。监控指标中,CPU 利用率、内存占用、磁盘 I/O 以及网络带宽建议长期压制在 40% 到 50% 以下,留足安全水位以应对机房级容灾故障转移(Data Center Failover)。

[17:40](https://www.youtube.com/watch?v=-UXWETzHG38&t=1060s) Slack 微服务核心架构拆解

系统将业务拆分为专注单一职责的高内聚微服务矩阵:

```mermaid flowchart TD accTitle: Slack Core Microservices Architecture accDescr: Microservices breakdown showing account, user, group, media, channel, message, and search services. subgraph ClientLayer ["客户端接入"] Client["User Client"] end

subgraph GatewayServices ["路由与状态服务"] ChannelSvc["Channel Service<br/>(Master 长连接与请求调度)"] NotifySvc["Notification Service<br/>(推送唤醒服务)"] end

subgraph BusinessServices ["实体与业务微服务"] AccountSvc["Account Service<br/>(用户账户管理)"] UserSvc["User Service<br/>(好友关系/用户元数据)"] GroupSvc["Group Service<br/>(群组与成员关系)"] MediaSvc["Media Service<br/>(富媒体与链接元数据预览)"] end

subgraph StorageLayer ["存储与索引微服务"] MsgStorage["Message Storage Service<br/>(消息持久化服务)"] MsgSearch["Message Search Service<br/>(历史消息搜索与索引)"] end

Client --> ChannelSvc ChannelSvc --> AccountSvc ChannelSvc --> UserSvc ChannelSvc --> GroupSvc ChannelSvc --> MediaSvc ChannelSvc --> MsgStorage ChannelSvc --> NotifySvc MsgStorage -. 异步索引.-> MsgSearch ```

- **Account Service**:管理账户注册、登录凭证及账户状态。 - **User Service**:负责用户好友关系维护(在 Slack 场景中组织内部成员默认互通,但在微信等场景中需独立验证好友拓扑)。 - **Group Service**:管理群组(Channels)元信息及群成员的加入、退出与变更。 - **Media Service**:解析处理富媒体资源(图片、语音、文档)以及 URL 链接预览信息的异步爬取抓取。 - **Message Storage Service**:专注于纯聊天文本流的高性能持久化入库,与传输调度解耦。 - **Notification Service**:负责离线用户的消息唤醒通知及移动端推送下发。 - **Channel Service**:主服务(Master Service),充当长连接通道与核心请求的分发协调中枢。 - **Message Search Service**:处理用户对海量历史消息的全文检索与索引构建。微服务粒度拆细后外表看似复杂,但内部各子模块的职责链路异常清晰。

[20:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=1200s) 基础服务架构与存储选型:SQL、Kafka与DynamoDB

在基础架构的设计上,Account Service(账户服务)、Friendship Service(好友关系服务)以及 Group Service(群组服务)具有高度结构化与强关联性的特征,最适合采用传统的关系型数据库(SQL)来存储。这些服务主要承载账户元数据、好友关系链以及群组成员归属等信息。由于系统在读取时呈现出鲜明的访问偏好——越新的数据查询越频繁,因此在 SQL 上层架设 Redis 或 Memcached 缓存层是标准的优化手段。在系统设计面试中,缓存往往可以作为后续的性能优化点提出,先给出一个可工作的方案,再引入缓存将查询耗时压低 10 到 100 倍。对于包含图片、视频的富媒体服务(Media Service),文件实体应直接上传至 Amazon S3,并在前端配置 CDN(内容分发网络)以加速分发。

消息通知服务(Notification Service)既可以封装成基础存储的 API,也可以构建为基于消息队列的异步投递管道。比如 Uber 就构建了专用的 Kafka Notification Service,服务只需向特定的 Kafka Topic 发送消息,由管道保证交付。针对通知系统是否要做到“恰好一次”(Exactly-Once)交付,这里存在巨大的工程权衡:由于下发通知往往严重依赖 Apple APNs 或 Google Android FCM 等第三方通道,若要实现严格的 Exactly-Once,必须引入两阶段提交(2PC),不仅实现极其复杂,而且性能损耗极高;对于通知场景来说,偶尔出现重复通知是完全可以接受的,因此执着于 Exactly-Once 属于典型的过度设计。

负责核心消息落盘的 Message Storage Service 则推荐选用 Amazon DynamoDB。相较于需要耗费大量运维心力进行分库分表的 SQL 方案,DynamoDB 天然支持水平扩展,在工业界轻松支撑成百上千个节点。虽然 DynamoDB 与开源的 Cassandra 同源,都采用了 Hinted Handoff 等机制,但两者在冲突解决与索引支持上存在差异:Cassandra 默认采用最后写入胜出(Last-Write-Wins),而 DynamoDB 允许配置更为灵活的冲突解决逻辑,且对二级索引(Secondary Index)的支持明显优于 Cassandra。此外,消息读取具有极强的时间局部性(近期消息高频阅读),配合 DynamoDB 专属的内存缓存层 DAX(或自建 Redis)能大幅降低存储成本与延迟。Twitter 曾发表过多篇关于缓存架构的技术博客,展示了通过对消息流引入精细化缓存,成功抵消了全平台 90% 以上的数据访问流量。

- **关系型数据(Account / Group / Friend)**:采用 SQL 存储元数据,辅以 Redis/Memcached 提升热点数据命中率。 - **通知投递权衡**:第三方推送通道无法天然保证原子性,放弃高代价的 2PC 恰好一次语义,容忍小概率重复通知(At-Least-Once)。 - **DynamoDB 核心优势**:开箱即用的近乎无限水平扩展能力、完善的托管运维、原生的 DAX 缓存加速以及更可靠的二级索引支持。

---

[25:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=1500s) 搜索、图数据库之辩与 Emoji 服务的架构避坑

消息写入完成后,需通过异步管道投递给 Message Search Service 进行索引。搜索底座依托于基于 Lucene 的 Elasticsearch,其底层依靠倒排索引(Inverted Index)运转。例如存在两条消息“System Design is fine”与“I like chocolate”,当检索词“I”进入引擎时,倒排索引能瞬间定位到该词在 Message 1 与 Message 2 中的对应偏移量(Offset),从而迅速完成跨会话的全文检索。

在讨论消息附加表情(Emoji)时,架构中单独拆分出了一个 Emoji Service,并将计数与状态存放于 Redis。有现场学员提出疑问:为什么不直接把 Emoji 作为字段存在消息记录里?这里引出了系统设计面试中的一个“致命红线”(Big Red Flag)——在同一个数据记录上进行高频重复更新(Repeatedly update the same record)。如果在大型组织或大群里,数千人同时对一条爆炸性消息点赞或添加 Emoji,瞬间就会在单条消息记录上产生高达 1000 QPS 的行级锁竞争与写入轰炸;这与在设计 Uber 时直接将司机实时的 GPS 坐标高频更新到静态的 User 数据表是同一种典型的设计失误。因此,必须将低频更新的消息主体与高频变动的 Emoji 交互彻底解耦。Redis 虽然存在断电丢数据的微小风险,但对于表情点赞这类非核心资产数据而言,性能远比极端持久性重要。

面对学员关于“好友关系是否必须采用图数据库(Graph Database)”的提问,回答中明确否定了这一执念。虽然图数据库擅长深度嵌套查询(例如 Friends of Friends 深度关系挖掘),但工业界海量社交场景完全可以通过分片关系型数据库(Sharded MySQL)配合规范化数据模型来高效支撑。Facebook 的经典架构论文《TAO》就是利用分片 MySQL 承载全网社交图谱的典范。在面试中如果不熟悉复杂图数据库的边界,引用经典论文并阐述为何使用规范化的 Sharded SQL 往往是更为稳妥、不易踩雷的成熟选择。

```mermaid flowchart TD accTitle: Message and Interaction Decoupling Pipeline accDescr: Demonstrates decoupling high-frequency emoji reactions and search indexing from base message storage.

subgraph Ingestion ["消息写入与持久化"] Client["客户端 Client"] MsgSvc["Message Service"] Dynamo["Message Storage (DynamoDB)"] end

subgraph AsyncPipeline ["异步索引与检索"] ESWorker["Search Index Worker"] ES[("Elasticsearch (Lucene)")] end

subgraph HighFreq ["高频互动独立服务"] EmojiSvc["Emoji Service"] RedisCache[("Redis (In-memory Counters)")] end

Client -->|"发送消息"| MsgSvc MsgSvc -->|"落盘消息主体 (低频写)"| Dynamo MsgSvc -.->|"异步事件投递"| ESWorker ESWorker -->|"构建倒排索引"| ES

Client -->|"高频点赞/Emoji (1000+ QPS)"| EmojiSvc EmojiSvc -->|"原子递增 / 避免锁冲突"| RedisCache ```

- **系统设计红线**:严禁在核心静态实体表(如 Message、User)上高频就地更新动态数据(如 Emoji 反应、实时 GPS 定位)。 - **Emoji 容错定位**:放置于 Redis 内存存储,即便节点故障引发瞬时计数轻微抖动,亦不会危及核心聊天流。 - **图数据选型辨析**:普通的点对点聊天关系网不需要图数据库,分片 MySQL 配合内存缓存不仅性能可靠,且整体复杂度与维护成本显著低于图引擎。

---

[30:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=1800s) 核心客户端 API 与消息状态机设计

进入 API 设计环节,首要任务是将客户端核心界面的信息流映射到接口契约中。当用户打开 Slack 客户端时,界面必须呈现出几类关键数据:用户最近访问的频道(Last-Viewed Channel)、可见频道列表(Visible Channels,涵盖常驻频道与最近的私信 Direct Messages)、各频道的未读消息标记(Unread Indicators)以及提及当前用户的 @ 统计数值。

针对初始化首屏拉取,核心接口设计为 `getChannels(userId)`。它负责聚合下发当前会话树的所有关键上下文,包括用户最后停留的 Channel ID、拥有访问权限的群组列表、未读计数以及最新的消息预览(Last Message Preview)。在序列化协议的选择上,虽然 RESTful 架构通用直观,但在强实时与跨平台的工业级通信中,采用 gRPC 配套 Protobuf(Protocol Buffers)相比 Apache Thrift 展现出明显的优势;Thrift 的生态与 IDL 结构较为繁重冗杂,而 Protobuf 文档完备、解析性能高,在现代服务治理中更为流行。

而在消息发送路径上,基础接口设计为 `sendMessage`,负载包含 `sender_id`、`receiver_id`、`message_type`(标识私信或群聊)以及多媒体引用的 `media_id`。进入作为用户网关的 Channel Service 后,服务端会主动为数据包注入生命周期元数据:创建时间、更新时间、过期时间,以及驱动整个消息生命周期的状态机(State)。消息状态被划分为四大阶段: 1. **Received**:服务端网关已成功接收该消息。 2. **Sent**:服务端已完成前置校验与处理,并将消息正式投递给接收端。 3. **Read / Viewed**:接收端用户已打开客户端并阅读该内容(类似 WhatsApp 的双蓝勾逻辑)。 4. **Deleted**:消息已被撤回或物理标记删除。

- **`getChannels(userId)` 响应要素**:`last_viewed_channel`、`visible_channels`、`unread_counts`、`mention_counts`、`last_message_preview`。 - **数据传输协议**:优先推荐 gRPC + Protobuf,编码紧凑、类型强校验,相较于 Thrift 具有更好的开发体验与文档支持。 - **服务端元数据增强**:由 Channel Service 统一补齐时戳与状态机变更,客户端仅负责提交纯净的业务载荷。

---

[35:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2100s) 多媒体后处理管道与内部/外部 API 分离

很多面试者容易忽视状态机中为什么一定要区分 `Received` 与 `Sent` 状态。对于纯文本消息而言,服务端收到后基本能瞬时完成校验并下发,90% 以上的消息只经历一次直接流转;但对于携带复杂媒体或富文本的内容,服务端必须执行耗时的异步后处理(Post-Processing)。例如发送包含 URL 的消息时,服务端需爬取外部站点生成卡片预览(URL Preview);若包含图片或音视频,服务端则必须生成缩略图、压缩码率甚至通过安全与风控团队(Safety Team)的审核系统过滤有害内容。因此,系统必须先将状态标记为 `Received` 确认收单,待后置任务完成后方能转为 `Sent` 交付给对端。

在文件上传架构中,若让大文件直接穿透业务网关将严重挤占核心带宽。标准做法是引入基于预签名 URL 的分块上传机制:客户端首先向多媒体服务申请 Amazon S3 的上传授权,直接将数据流推送到 S3 存储桶;S3 内部完成跨可用区(AZ)持久化复制后,触发事件通知背后的任务队列(Message Queue)与 Worker 线程池(Worker Pool)启动后处理流程。当所有切片转码、缩略图提取与风控扫描全部完成,后处理工作流才会发布“Message Ready”事件通知 Channel Service 推进消息状态,最终由 CDN 承接后续所有的拉流分发。

最后,API 体系必须严格划分为外部公开接口(Public API)与内部微服务接口(Internal API)。面向移动端与 Web 客户端的 Public API 侧重业务表达(如单聊/群聊发起、基础群组元数据 CRUD、群成员进出);而 Internal API 则专注于后端管道间的长生命周期协作(例如内部的 `processMedia` 任务分发与进度回调)。保持内外部 API 的解耦与边界清晰,是保障大型分布式聊天平台可靠演进的核心基石。

- **多媒体处理异步解耦**:客户端直传 S3 $\rightarrow$ 跨区复制校验 $\rightarrow$ 消息队列驱动 Worker 线程池(生成 Preview/缩略图/安全审核) $\rightarrow$ 标记 Ready 并推向 CDN。 - **状态分离必要性**:引入 `Received` 到 `Sent` 的异步中间态,吸收后处理及安全合规扫描所带来的耗时毛刺。 - **API 治理分层**:对外暴露精简的用户态端点(CRUD、频道监听),对内隔离重型的系统管道端点(`processMedia`)。

[40:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2400s) CDN 的妙用与 Notification 服务的消息节流设计

日常互联网体验中,CDN(内容分发网络)是用户接触最多、也是性能优化的基石。无论是刷 YouTube 还是看 Bilibili,视频流通常都会被切碎成极其微小的切片(例如 1 秒到十几秒的视频片段),再推送到 CDN 边缘节点进行高效分发。甚至连经典的 Craigslist 网站,其底层也是极度依赖 CDN 实现的极简高吞吐架构:当用户发布一条新帖子时,系统仅需将其塞入队列;后台 Worker 每小时拉取所有新帖并与旧数据合并,重新渲染生成纯静态的 HTML 网页,分发部署到全球 CDN 节点。用户访问时直接拉取静态网页,这种方式虽然非常“非主流”,但极其稳定且迅速——“它 work 就 work”。

回到聊天系统,Notification Service(通知服务)也是一种核心的用户服务。当系统要给离线或后台用户发送通知时,通知载荷(Payload)通常只需要精简元数据: - 目标用户的 Receiver ID - 发送方的 Sender ID - 消息本身的 Message ID - 消息预览(Message Preview)

很多场景下根本无需将完整消息(Full Message)全部下发,仅传预览即可。而在安全与高可用层面,必须严格考虑频率限制(Rate Limiting)和 DDoS 防护。例如限制单条消息的大小不超过特定阈值(如不能超过 2 KB),同时严格限制发送频率(比如单个用户每秒最多发送 10 条消息、每小时最多发送 300 条消息)。如果对接口不加限流,系统极易被恶意脚本通过高并发消息击垮。因此,Rate Limiter 在工业界通常会设置多层级阈值,本身也是一个非常独立的系统设计考题。在具体通知的流转上,很多大型企业(例如 Uber)会选择采用 Kafka 作为缓冲队列,通过 Message Queue 实现异步解耦与削峰填谷。

```mermaid flowchart LR accTitle: Notification Push Pipeline accDescr: Pipeline showing notification delivery from message queue to mobile devices via third-party providers with DB tracking.

subgraph Ingestion ["消息接入"] A["Message Trigger"] --> B["Kafka / Message Queue"] end

subgraph Workers ["异步处理池"] B --> C["Worker Pool Handler"] end

subgraph Providers ["下发通道"] C --> D["Twilio / SMS"] C --> E["APNs (iOS)"] C --> F["FCM (Android)"] end

subgraph StateManagement ["状态回执"] D & E & F --> G["Notification State DB\n(At-Least-Once / Sweeper)"] end ```

---

[45:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2700s) 通知下发的工作流、At-Least-Once 保证与两阶段提交的代价

在回答现场关于 Notification 下发及重试机制的端到端(End-to-End)工作流时,讨论重点聚焦在 Worker Pool(工作者池)模型与异步处理(Asynchronous Processing)。当业务服务发起一条通知请求时,系统并不会同步阻塞等待,而是将其作为一个异步任务投递进 Message Queue: 1. **Worker 拾取任务**:Worker Pool 中的闲置 Worker 节点拉取消息。 2. **多端下发路由**:Worker 判断用户当前状态。若存在活跃的 WebSocket 长连接,可尝试直接推送;若无长连接或针对移动端,则调用第三方通道,如 Twilio(短信)、Apple APNs(iOS 推送)或 Google Firebase Cloud Messaging(FCM,Android 推送)。 3. **回执与重试状态持久化**:如果系统需要达到 **At-Least-Once(至少一次)** 的投递保证,必须引入状态存储。可以在消息状态中增加 `notification_sent` 标志位。只有当第三方 API 显式返回成功回执且本地数据库成功标记为 `true` 时,才算正式完成;若第三方调用超时或网络无响应,状态保持未完成,后续会有后台巡检程序(Sweeper Process)扫表进行重试补偿。

现场有人提问是否可以通过消息系统的 ACK(应答)机制或者事务机制来实现 **Exactly-Once(恰好一次)**。讨论明确指出:在跨越第三方外部服务(Third-Party API)的场景下,想要达成纯粹的 Exactly-Once 极其困难且昂贵,这往往需要强依赖 XA 规范或两阶段提交(Two-Phase Commit, 2PC)。两阶段提交涉及协调者(Coordinator)向所有参与者节点发起两轮网络通信与资源锁定,性能损耗极为巨大,“会把你的 performance 降低至少一个数量级,慢了十倍”。在绝大多数没有硬性强一致性约束的业务中,盲目上 Exactly-Once 和 2PC 是典型的过度设计;更优雅、高效的做法是保持系统操作的**幂等性(Idempotent)**,利用幂等去重配合 At-Least-Once 重试。

---

[50:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=3000s) 关系型数据库的分库分表、无限扩容与非关系型存储分工

在数据模型(Data Model)的设计选型上,Account Service(账户服务)、Friendship Service(好友关系)和 Group Service(群组服务)更适合采用传统的 SQL 关系型数据库。虽然关系型模型受限于单机性能,但只要规划好分片(Sharding),SQL 完全可以做到“Infinite Scale”(近乎无限的水平扩展): - **垂直分片(Vertical Sharding)**:将业务按领域彻底拆分,Account Service、Friendship Service 和 Group Service 分别独享各自独立的 SQL 数据库集群,杜绝跨库事务。 - **水平分片(Horizontal Sharding)**:对于关系量级较大的表,在系统上线之初就预先规划分片方案。推荐一种最通用、易落地的“Schemeless Sharding”策略:基于未来 5 到 10 年的数据增量预估,固定切分成固定的份数(例如固定的 4096 个物理或逻辑 Shard)。路由时直接将主键 ID 计算 Hash,取模定位到对应的分片(`Hash(ID) % 4096`)。

针对海量多媒体文件(图片、视频、语音片段),毫无疑问全量托管在类似 Amazon S3 的对象存储中;而全系统中吞吐量和容量需求最大、增长最爆炸的聊天消息本身(Message),则选择使用 DynamoDB 等具备高扩展能力的 NoSQL 数据库进行承载。

在服务调用链路的设计中,服务之间的单向依赖(One-Way Dependency)非常关键。比如消息通过 Channel Service 写入后,绝不能让下游的 Account Service 逆向反查回调 Channel Service,避免造成微服务架构中的循环依赖(Circular Dependency)。发送通知完毕后,状态回写直接作用于 Message Storage Service,保证数据流向清晰。

---

[55:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=3300s) 好友表双向冗余存储与未读消息定位机制

在讨论 User Table 时,通常用户基础信息的更新频率极低,但在类似 Uber 寻找可用司机、或聊天应用展示全量在线状态时,不应在主表上频繁原地更新行数据。推荐将高频波动的状态拆分出单独的 `active_user_table` 以避免行锁竞争。

对于好友关系表(Friendship Table),存在一个经典设计难题:A 和 B 是好友,意味着 B 和 A 互为好友。关系本质上是对等的双向图,但在分库分表环境下无法同时根据两个 User ID 进行哈希路由。如果一张表只存一条单向记录(如 `user_id_1 < user_id_2`),当需要查询某个用户的所有好友列表时,系统就必须对所有 4096 个分库分表执行广播式全表扫描(Fan-out Query),系统开销不可承受。

因此,架构中采取了**双向冗余存储(Duplicate Rows)**的设计: 1. **Sharding 路由友好**:当 A 与 B 建立关系,表内插入两条记录:一条以 A 作为 `user_id_1`,另一条以 B 作为 `user_id_1`。整个 Friendship 表统一只以 `user_id_1` 作为分片键(Sharding Key)。这样查询某位用户的所有好友时,仅需定位到该用户所在的单点分片即可单次读取完毕。 2. **独立的未读游标(Last View Date)**:好友表中包含一个核心时间戳字段 `last_view_date`。这个字段决定了未读消息红点与历史断点的判定:

| 字段名 | 类型 | 说明 | |:--- |:--- |:--- | | `user_id_1` | Long (PK / Shard Key) | 当前用户 ID,数据水平分片的唯一路由依据 | | `user_id_2` | Long | 好友用户 ID | | `created_at` | Timestamp | 好友关系建立时间 | | `last_view_date` | Timestamp | 当前用户(`user_id_1`)最后一次查看与该好友聊天的时间点 |

```mermaid flowchart TD accTitle: Friendship Table Row Duplication accDescr: Diagram showing how A and B friendship is stored in two separate rows with independent view dates.

subgraph Storage ["Friendship 冗余双写"] R1["Row 1: user_id_1 = A, user_id_2 = B\nlast_view_date = 10:00 (A 查看的时间)"] R2["Row 2: user_id_1 = B, user_id_2 = A\nlast_view_date = 10:05 (B 查看的时间)"] end

Q1["查询 A 的好友列表"] -->|命中 Shard(A)| R1 Q2["查询 B 的好友列表"] -->|命中 Shard(B)| R2 ```

未读消息绝对不能把“谁读了、谁没读”的状态记录直接打在 Message 实体上。如果是千人甚至五千人的大群,一条消息需要维护数千个用户的阅读状态位,且群成员随时动态进出,存储和并发写回的复杂度将彻底失控。利用 `last_view_date`,任何大于该时间戳的消息均为未读,A 看对话和 B 看对话的游标彼此独立、互不干扰。

---

[01:00:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=3600s) 群组成员未读状态(Group Last View)与高并发读写性能考量

面对现场提问“Slack 是否有像微信一样的 1 对 1 好友体系”,讨论指出:虽然 Slack 核心组织形式是 Channel 与 Direct Message,但底层机制与微信、Telegram、Facebook Messenger 别无二致。所谓“好友表”,其底层元数据就是 Direct Message 的两端映射与游标记录,属于同一类抽象。

在 Group 维度,群消息未读状态的处理机制与好友表完全相同。通过一张规范化(Normalized)的 `group_membership` 关系表记录每个用户在特定群的上下文: - 联合主键包含 `group_id` 和 `user_id`。 - 包含用户加入时间 `join_date`、用户在群内的权限角色 `role`。 - 最核心的仍是 `last_view_date` 字段。

当用户打开特定群组(如 Random Group)时,客户端读取该用户在该群的历史 `last_view_date`,拉取该时间点以后的所有消息渲染为未读高亮;随后将该字段刷新为当前最新时间戳。

针对“每次点击频道更新 `last_view_date` 是否会导致数据库 CPU 瞬间打满”的顾虑,现场给出了原理解释:因为数据结构已经彻底行级规范化(Normalized),一个拥有 5000 人的群组在数据表中映射为 5000 条独立的 Membership 行记录。每个用户只有在主动点击或切入该频道查看时,才会发起一条基于自身 `(group_id, user_id)` 的轻量点查与单行 Update,绝非 5000 人去并发争抢同一行的行级互斥锁。因此,这种更新模式对数据库的并发和负载压力非常均匀,完全无需为了防抖而强行将更新逻辑从表中剥离。

[01:05:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=3900s) Redis Channel Table 与读写流转

在整个架构设计中,Channel Table 承担了聚合展示的核心职责。它的主要作用是将与当前用户相关的频道和会话状态高效可视化出来。用户登录或者打开客户端时,界面上会呈现所有的对话列表,比如 Safety 频道、Random 频道,或者 David、Uncle 发来的私聊;这非常类似于微信的消息列表(Friend List),一眼看去就能知道 Safety 组有 10 条未读、最新的一条消息预览(Message Preview)是什么、时间戳是多少。为了极致的读取性能,Channel Table 是完全反范式化(Denormalized)后存放在 Redis 缓存中的。即便 Redis 节点发生宕机(Crash),由于其本质是派生数据,系统随时可以通过底层的 Group Membership Table、Friends Table、Messages Table 和 Message Storage Table 重新构建(Reconstruct)出来。如果某条私聊由于过期机制失效了,在反范式化聚合时也可以直接将其过滤,无需渲染在界面上。

对于写消息的完整生命周期,讲师以“David 给我发了一条新消息”为例梳理了数据流: 1. 请求首先打到 Channel Service; 2. Channel Service 转发给 Message Storage Service,优先将消息写入持久化数据库(DynamoDB); 3. 只有当 DynamoDB 确认落盘成功、保证消息绝不会丢失之后,系统才会向发送方 David 确认消息发送成功; 4. 随后系统再去异步或同步更新接收方在 Redis 里的 Channel Table,将未读数(Unread Message Count)递增、更新最新消息预览内容。

针对有学员提问“如果在某个频道停留很久,最后浏览时间(Last View Date)会不会不准确”,讲师指出可以通过双重上报来解决:客户端在进入(打开)频道时更新一次 Last View Date,退出离开时再更新一次即可。关于“Last View Date 能否只保存在客户端”的疑问,讲师明确指出这在多端设备(Multi-platform)场景下是行不通的:用户如果在电脑端看过了最新消息,换到手机端打开时,由于客户端本地数据不互通,未读状态便无法同步,因此必须由服务端统一持久化。

```mermaid flowchart LR accTitle: Message Write and Channel Update Flow accDescr: Message flows from sender through Channel Service to DynamoDB, returns ACK, then updates Redis Channel Table.

subgraph ClientStage["客户端请求"] C1["发送方 (David)"] end

subgraph ServiceStage["后端服务与存储"] CS["Channel Service"] MSS["Message Storage Service"] DDB[("DynamoDB\n(Message Storage Table)")] RC[("Redis\n(Channel Table)")] end

C1 -->|"1. Send Message"| CS CS -->|"2. Save Message"| MSS MSS -->|"3. Insert Record"| DDB DDB -.->|"4. 落盘确认 (ACK)"| MSS MSS -.->|"5. 消息已发送"| C1 MSS -->|"6. 更新未读数与预览"| RC ```

---

[01:07:33](https://www.youtube.com/watch?v=-UXWETzHG38&t=4053s) DynamoDB 的 Partition Key 与 Sort Key 设计考量

对于消息的持久化底座 Message Storage Table,系统选用 DynamoDB,这要求设计者必须深刻理解其分区键(Partition Key)与排序键(Sort Key)的底层运作机制。DynamoDB 的 Partition Key 决定了数据散列分布在后端的哪一个物理物理分片上,且同一个分片通常会进行多副本复制(通常默认是 3 副本,也可按需自定义)。如果两条记录的 Partition Key 不同,大概率不会落在同一个物理分区。而 Sort Key 与 Partition Key 组合构成联合主键(Primary Key),具备唯一性约束。更重要的是,在同一个 Partition Key 下,底层数据会按照 Sort Key 严格物理排好序(Sorted),支持高效的范围扫描(Range Search)与二分检索(Binary Search),避免全表低效扫描(Scan)。

结合具体的业务查询模式(Request Pattern),系统中最核心的高频读取行为只有两类: 1. 读取某个群聊(Group)里的历史消息列表; 2. 读取两个人之间的私聊(Direct Message)历史消息。

基于这个读模式,Partition Key 可以被设计为容器 ID(Container ID): - **群聊消息**:Partition Key 直接设为群标识,例如 `group + groupID`,查询时指定 `Partition Key = groupID`,一次就能拉出群内全部消息; - **私聊消息**:可以将两个通信双方的 User ID 进行升序排序后拼接(例如 `min(userID1, userID2) + "_" + max(userID1, userID2)`)作为 Partition Key; - **排序与分页**:两者的 Sort Key 均统一采用消息的时间戳(Timestamp)。因为同一会话内的消息按时间自然排列,客户端拉取历史记录时做分页(Pagination,如第一页先读前 100 条)就会极其简单高效。

这种设计也存在一个明显且略微反直觉的缺陷:无法直接根据 Message ID 主键单点查询一条消息(`Get Message by Message ID`)。不过在实际聊天业务中,通过单一 Message ID 独立查询单条消息的频次极低;如果真有此需求,通常需要借助全局二级索引(Global Secondary Index, GSI)。然而 GSI 大多是异步构建的,往往存在一秒左右的最终一致性延迟(Not immediately available),刚插入的消息无法即刻根据 ID 检索出来。

针对消息过期(Message Expiration)功能,讲师提出了两种实践思路: - **数据库原生机制**:直接利用 DynamoDB 提供的 TTL(Time to Live)功能,给记录附带过期时间戳,到期由存储引擎自动清除。 - **服务端读取时修复(Read Repair)**:客户端请求批量拉取消息列表时,服务层在内存中过滤掉已过期的消息,不返回给用户,同时反手向 Redis 发起更新将其剔除。

---

[01:15:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=4500s) 全文搜索、时间戳一致性与消息乱序痛点

对于群聊消息检索(Message Search),很多场景需要支持按关键词(例如搜“Happy Hour”)查找对应的 Message ID。这类需求并不适合让 DynamoDB 强行抗压,而是需要同步导入到外部的全文检索引擎(如 Elasticsearch)中。无论是 DynamoDB 的 GSI 还是 Elasticsearch 的倒排索引,其写入构建都具备非即时性的延迟特征(通常需要 1 到 2 秒),刚发出的内容无法在全局搜索中秒级可见,在工程实践中属于普遍接受的折中。

紧接着,讨论切入了分布式即时通讯领域极其经典的痛点:**消息乱序问题**(即用户 A 看到回复消息 Message 2 竟排在了被回复的 Message 1 之前)。 - 讲师严正指出,直接采用客户端本地时间戳(Client Timestamp)是一个“极其危险的 Red Flag”,因为客户端时间极其容易被恶意用户篡改(比如故意伪造两天后或十五天前的时间戳),因此时间戳必须且只能在服务端生成。 - 但如果采用分布式微服务节点生成时间戳,当两个用户发送时间极其贴近且请求落到了不同的服务端 Node 上时,由于时钟漂移和网络抖动,Message 2 先于 Message 1 盖上时间戳是完全可能发生的。

解决或缓解乱序的常见手段包括: - **按会话分片(Sharding by Session)**:在网关请求层或者数据库层,强制将同属两人(或同一个群)的所有读写请求固定路由到同一个微服务实例或同一个数据库分片(Shard)。甚至直接依赖单机 MySQL 插入行时由同一物理机时钟自动生成的时间戳,这样冲突概率会骤降。 - **时钟回拨与单调性现实**:即使限定在单节点时钟上,机器本地时钟仍有可能发生跳跃(Jump Back)。讲师直言,除非底层使用像 Google Spanner 这样依赖原子钟与 GPS 硬件级别 TrueTime 的强一致性服务,否则在海量分布式数据面前,“消息绝对时序是永远无法百分之百被保证的(Message order is never guaranteed)”,乱序在概率上必然存在。

---

[01:18:40](https://www.youtube.com/watch?v=-UXWETzHG38&t=4720s) 群发扩散(Fan-out)、分片路由与热点行陷阱

当群内有人发送一条消息时,系统需要把所有成员在 Redis 中的 Channel Table 逐一更新(更新未读数与最新消息摘要)。假设按照原先的假设,一个群有 1,000 个成员,每次发言都需要更新 1,000 条 Redis 记录。这是否会拖垮系统?讲师分析道:单台 Redis 节点每秒就能轻松扛住 10 万次以上的内存操作;结合 Redis 分片集群,1,000 个成员可能被均匀哈希分流到了 3 个物理 Redis 实例上,每台机器仅仅需要承载约 300 次内存写入,总耗时不过 0.233 秒左右,速度极快。

这类似于 Twitter 早期的推文 Fan-out 写扩散模型:超级网红(如拥有几千万甚至数亿粉丝的公众人物)发一条动态,所有粉丝的 Timeline 都会被批量插入一条记录。早期微博常开玩笑说“经不起两个明星同时出轨”,正是因为瞬间并发引爆的写扩散(Fan-out)导致写入指标全线爆表。

在数据建模中,是否可以将私聊也视作只有两人的特殊群聊(Single/Double-member group message)?讲师肯定了这种统一抽象的可行性,但也指出了底层的**二维查找路由两难(Two-dimensional Lookup)**: - 如果数据库按 Group ID 做了分片(Shard),查找一个群里有哪些用户非常直接(直奔目标分片);但若想反查“某一个 User 到底加入了哪些群”,客户端将无法精准定位物理分片,只能暴力广播扫描全部 1,000 多个物理分片,效率极其低下。 - 解决该问题必须付出代价:要么进行双写,即按 User ID 和 Group ID 分别维护两张独立分片维度的映射表;要么自建全局二级索引(Global Secondary Index),先查出该用户所在的物理分片集合,再做局部精准查询。

针对“Channel Table 究竟该存全量用户还是仅存当前在线用户”的权衡: - **方案 A(仅存在线用户)**:Redis 存储体量较小,但离线用户一旦重新登录,必须从底层数据库通过群组表、好友表和消息表昂贵地重新聚合重建数据,冷启动极慢; - **方案 B(存储全量用户)**:采用分片 Redis 缓存全体用户,数据量显著增加,且需要后台定期运行清理脚本(Sleeper)扫除过期消息,但用户无论何时登录都能实现毫秒级的秒开体验。

此外,针对学员提出“能不能直接把加入的 Group ID 作为标签数组(Tag)塞在 User 记录中”的设想,讲师直接给出了否定意见,指出这是严重的架构暗礁:一旦用户是业务高频核心人员(如公司 CEO 每天被无数人拉进各种临时临时群、移出群聊),系统就必须频繁并发原地更新(Update)该用户的同一行记录(Row),这会瞬间造成严重的数据库单行锁竞争与热点行(Hot Spot)阻塞,引发全盘性能雪崩。

```mermaid flowchart TD accTitle: User to Group Routing Dilemma accDescr: Shows the trade-off of sharding by Group ID versus User ID and how Global Secondary Index bridges them.

subgraph DirectLookup["按 Group ID 分片"] G1["Group ID: 888"] -->|"精准定位"| S1["分片 Shard 1"] S1 -->|"包含成员"| UList["[User A, User B, User C]"] end

subgraph ReverseLookup["反查 User 在哪些 Group"] UA["User A 请求其加入的群"] UA -.->|"若无全局索引: 必须广播全部分片"| S1 UA -.->|"广播扫描"| S2["分片 Shard 2"] UA -.->|"广播扫描"| S3["分片 Shard N (1000+ 个 Shards)"] end

subgraph Solution["优化解法"] GSI["自建全局二级索引 (GSI)\n或双写按 UserID 分片表"] UA ==>|"先查索引获知所属 Shard"| GSI GSI ==>|"直接提取"| S1 end ```

---

[01:25:28](https://www.youtube.com/watch?v=-UXWETzHG38&t=5128s) 在线状态更新与 WebSocket 心跳设计

在问答环节,有学员针对用户的实时在线状态(Online Status)提出质疑:好友谁在不在线的状态变化频率极高,把这种超高频状态丢在常规业务数据表里处理显然并不合理,请求量(QPS)会不会彻底打崩系统?

讲师对此进行了详细拆解: 首先,需要用具体的业务量化(Quantify)来理性评估。假设系统拥有 1,000 万用户(10 Million Users),如果大部分用户处于活跃在线状态,且状态心跳策略设为每 10 分钟或 1 小时上报一次,结合底层用户表已做好的哈希分库分表(例如拆分成数十个 Shard),分摊到每个具体分片上的单行更新 QPS 实际上极为有限,并不会构成毁灭性的热点红线。这与 Uber 的司乘架构存在本质不同:Uber 用户绝大多数时间处于离线状态,且车辆位置与在线切换属于高随机、高密度的地理跳变。

其次,对于即时通信中的在线感知,业内通行的标准方案是依靠 **WebSocket 长连接与心跳机制(Heartbeat)**: - 当客户端 App 启动上线时,会与网关接入层建立持久的 WebSocket 双向连接; - 客户端与服务端通过轻量级的定期心跳包维持存活感知,一旦连接断开或心跳超时,连接即失效; - 当有他人给该用户发消息时,服务端便可通过现存的 WebSocket 连接主动向该用户即时下发通知推送(Push Notification)。

讲师总结道,如果基于这种长连接模型,将用户在线状态抽离为一个独立的活跃用户表(Active User Table)或内存状态服务是非常合理的设计。而且 WebSocket 接入层可以通过无状态扩展(Scale Out)持续水平扩容机器节点来分散并发,完全不用担心单机 QPS 过载的问题。

- **核心要点补充**: - **索引类型划分**:标准 SQL 不原生支持跨 Shard 的全局索引,因此 Global Secondary Index 必须在应用服务层自行维护映射;而 Local Secondary Index 则是各个独立单机分片(Shard)内部的原生 MySQL 局部 B+ 树索引。 - **时钟回滚陷阱**:分布式时钟不存在绝对基准,除使用 Google Spanner 等强依赖硬件原子钟的系统外,普通服务器无法绝对避免乱序。 - **Redis 吞吐优势**:单节点每秒可达 10 万+ QPS,使得面向群组成员的小规模写扩散在分片后能控制在毫秒级内完成。

[01:30:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=5400s) WebSocket 集群管理与系统无限水平扩展(Infinite Scale)

在消息推送的连接管理上,系统会部署专门的服务器来维护客户端的 WebSocket 长连接。面对海量在线用户,连接层通常采用集群化水平切分:例如拥有一百万用户时,可以拆分出 100 台网关服务器,每台分别承载和维持数万个连接。系统能够根据 User ID 迅速定位到具体是哪一台服务器在管理该用户的 WebSocket 连接,进而精准推送通知。讲师指出,这套即时通信架构背后的权衡与思考非常复杂,他在开课当天下午花费了两个多小时才将整体设计方案梳理成型;但只要把这套架构彻底吃透,无论是 Facebook Messenger 还是微信(WeChat),其核心设计思想和底层脉络都是相通的。

针对系统的高性能与实时性,讲师解释道,虽然文档中没有逐一单列 Availability 与 Reliability,但整个链路中大量引入了 Cache,其端到端(End-to-End)延迟极低。在底层的存储选型上,普通的 MySQL 读写 P50 延迟大约在 10 毫秒(ms)左右,若走 Redis 缓存甚至不到 1 毫秒。对于业界毁誉参半的 DynamoDB,讲师客观评价其延迟虽然相对偏高一些,但核心优势在于具备真正的“无限扩展”(infinite scale)能力。原生支持无缝扩展至数千个节点,学术界多篇关于 Cassandra 与 Dynamo 架构的论文也早已证明,它们的节点数量增长与集群吞吐性能呈现出完美的线性伸缩关系,是横向扩展性最为优异的分布式数据库代表。

不仅 NoSQL 如此,整套架构的各个层级都具备横向无限扩展的潜力。存储非结构化媒体文件的 Amazon S3 天然支持持续追加推送资源;关系型数据库(SQL)可以通过垂直分片与水平分片(Vertical & Horizontal Sharding)突破单机瓶颈;即便是 Redis,由于普通用户界面之间天然相互隔离、彼此不会查看到对方的视图,也可以完全按照用户维度进行平滑的分片扩展。

- **连接层分流估算**:通过网关集群将海量长连接打散,依据 User ID 的映射关系将 Notification 路由到指定的 WebSocket Server。 - **存储性能分级**:Redis 命中时延迟低于 1ms,标准 MySQL 实例 P50 延迟约 10ms,DynamoDB 兼顾超高吞吐与容灾。 - **线性扩展数据库**:DynamoDB 与 Cassandra 在多篇系统研究论文中被证实性能随节点增加呈线性水平伸缩(Linear Horizontal Scaling)。 - **业务天然隔离**:即时通讯中用户的聊天视窗与消息列表互不相干,为基于 User ID 的全局分片提供了天然边界。

---

[01:32:15](https://www.youtube.com/watch?v=-UXWETzHG38&t=5535s) 工程师的数据敏感度:从单机400万QPS神话到Kafka延时实战

谈及面试中如何准确预估容量(Capacity)、延迟(Latency)与最大并发(Concurrency),讲师强调这依赖于平时的工业界积累与批判性阅读。以网络流传的“单机支持 400 万 QPS”技术文章为例,讲师提醒大家必须穿透数字看本质:这类极限基准测试通常剥离了所有真实业务负担,仅仅是纯内存读取;一旦面对真实线上环境里有读有写、包含完整编解码与落盘逻辑的 Normal Request,单机绝对不可能跑到 400 万 QPS,单机读写吞吐能达到 10 万 QPS 左右就已经是非常扎实稳定的表现。

这种“心中有数”的工程素养往往源自日常对生产监控系统的观察。讲师分享了自己在 Uber 内部观察各项指标的经历,建议工程师平时没事多打开 Grafana 等监控仪表盘,多看一眼底层基础设施(如 Kafka 集群)的运行水位,将关键 SLA 指标烂熟于心。当在系统设计中提出引入 Kafka 实现实时通信时,如果被追问技术选型的依据,就能直接拿出一手数据作答:在单数据中心(Single Data Center)环境下,Kafka 的 P50 延迟仅在 5 毫秒左右,完全满足实时性需求;而对于跨数据中心汇总集群(Cross Data Center Aggregate Kafka Cluster),P50 延迟通常也只在 100 毫秒上下,尽管他曾亲身体验过出现数分钟延迟的极端异常,但这属于 P999 的罕见长尾表现。

讲师直言,无论是在系统设计面试中,还是在日常工作中去“以 Data 服人”说服产品经理(PM)以及资深/主任工程师(Staff/Principal Engineer),这种基于真实数据指标的深度理解都是最强有力的论据。如果业务部署在公有云且由于成本考虑觉得自建或托管 Kafka 太贵,也可以灵活替代为 AWS 原生的 Kinesis,或者结合场景退而求其次选择 SNS 与 SQS 等消息传递组件。

- **基准测试辨析**:纯只读测试极限可达数百万 QPS,但混合读写真实业务单机能支撑 10 万 QPS 属于正常业界标准。 - **单机房 Kafka 延迟指标**:Single Data Center 场景下 Kafka P50 延迟约 5ms,保障低延迟消息吞吐。 - **跨机房 Kafka 延迟表现**:Cross Data Center 聚合集群正常 P50 约 100ms 左右,在网络抖动极端情况下可能退化至 P999 的分钟级延迟。 - **云厂商替代选项**:针对 AWS 托管 Kafka 成本高昂的问题,可考虑 AWS Kinesis,或利用 AWS SNS / SQS 组合方案。

---

[01:34:45](https://www.youtube.com/watch?v=-UXWETzHG38&t=5685s) 关系型数据库的分片本质与 Uber Schemaless 架构

针对学员提出的疑问——“使用 SQL 数据库会默认支持数据分片(Data Partition)吗?分片不应该是 NoSQL 独有的优势(Advantage)吗?”,讲师做出了透彻的底层原理解析。从 SQL 数据库引擎本身的视角来看,它压根不知道什么是 Data Partition。如果部署了多个 MySQL 实例,承担 Partition 1 的 MySQL Server 完全不知道世界上还有 Partition 2、Partition 3 和 Partition 4 的存在。所谓关系型数据库的分片扩展,百分之百需要基础架构工程师(Infrastructure Engineer)在应用层和代理层自己编码搭建实现。

要让单机引擎感知不到分片的 MySQL 实现集群化数据分区,系统必须构建两个核心组件: 1. **成员状态与元数据管理(Membership Management)**:记录哪个分片(Partition)映射在物理拓扑中的哪一台 MySQL 节点实例上。这个元数据注册中心一般通过成熟的分布式协调服务 ZooKeeper 来搭建。 2. **路由查找服务(Lookup Service)**:客户端或代理层在执行 SQL 查询前,必须先请求 Lookup Service,向背后的 ZooKeeper 检索当前的 Partition-Node 映射关系,随后将真正的读写流量精准路由到目标 MySQL 节点。

讲师特别向希望搞懂关系型数据库分片实战的工程师推荐了 Uber 官方技术博客在 2016 年发表的经典三部曲——《Uber Schemaless》。Uber 当时正是为了解决打车海量数据的持久化与高可用,在底层裸 MySQL 之上自研了这套无模式(Schemaless)、支持自动分片与容灾的分布式数据存储系统,该系列工程博客是理解数据库分片架构不可多得的必读经典。最后,讲师表示本堂 Slack 系统设计课的内容已经完整覆盖,正式停止了视频录制。

```mermaid flowchart TD accTitle: Sharded MySQL Architecture with ZooKeeper accDescr: 客户端通过查找服务与ZooKeeper获取分片元数据并路由到独立的MySQL实例

Client["客户端应用 (Client)"]

subgraph CoordLayer ["路由协调层 (Coordination Layer)"] Lookup["路由查找服务 (Lookup Service)"] ZK["分布式协调集群 (ZooKeeper)<br/>• 维护分片元数据 (Metadata)<br/>• 节点成员管理 (Membership)"] end

subgraph StorageLayer ["MySQL 分片存储层 (Storage Layer)"] M1["MySQL 节点 1<br/>[仅自知 Partition 1]"] M2["MySQL 节点 2<br/>[仅自知 Partition 2]"] M3["MySQL 节点 3<br/>[仅自知 Partition 3]"] end

Client -->|"1. 查询数据所在节点"| Lookup Lookup <-->|"2. 获取分片与节点映射"| ZK Client -->|"3. 直连目标节点读写数据"| M1 Client -.->|"路由请求"| M2 Client -.->|"路由请求"| M3 ```

- **SQL 节点的孤岛特性**:原生 MySQL 实例无法感知其他 Partition 节点的存在,数据分片完全依赖外部基础设施进行协调与路由。 - **ZooKeeper 的核心职责**:在自定义分片架构中充当 Single Source of Truth,全权托管 Partition 到具体物理机器 Node 的映射元数据与状态心跳。 - **推荐参考资料**:Uber Engineering 博客于 2016 年发布的 Uber Schemaless 设计系列文章(共三篇),深入剖析了基于 MySQL 构建高可用分片数据存储的真实落地实践。

## Notable quotes > [00:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=0s) 这得全盘重构系统架构(totally restructure) — Scott Shi

> [03:45](https://www.youtube.com/watch?v=-UXWETzHG38&t=225s) You're blessed, you're fortunate to find it from system design level — Scott Shi

> [25:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=1500s) 在同一个数据记录上进行高频重复更新(Repeatedly update the same record) — Scott Shi

> [40:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2400s) 它 work 就 work — Scott Shi

> [45:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=2700s) 会把你的 performance 降低至少一个数量级,慢了十倍 — Scott Shi

> [01:15:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=4500s) 消息绝对时序是永远无法百分之百被保证的(Message order is never guaranteed) — Scott Shi

> [01:18:40](https://www.youtube.com/watch?v=-UXWETzHG38&t=4720s) 经不起两个明星同时出轨 — Scott Shi

> [01:30:00](https://www.youtube.com/watch?v=-UXWETzHG38&t=5400s) 无论是 Facebook Messenger 还是微信(WeChat),其核心设计思想和底层脉络都是相通的 — Scott Shi

> [01:32:15](https://www.youtube.com/watch?v=-UXWETzHG38&t=5535s) 以 Data 服人 — Scott Shi

> [01:34:45](https://www.youtube.com/watch?v=-UXWETzHG38&t=5685s) 从 SQL 数据库引擎本身的视角来看,它压根不知道什么是 Data Partition — Scott Shi

## Mentioned Slack, Scott Shi, MySQL, Cassandra, Amazon DynamoDB, DAX, Amazon S3, Expensify, Discord, Apache Kafka, Elasticsearch, Lucene, Redis, Memcached, Apple APNs, Google FCM, Twilio, gRPC, Protocol Buffers, Apache Thrift, Facebook TAO, Craigslist, Uber, Uber Schemaless, Twitter, Google Spanner, 微信 (WeChat), Facebook Messenger, Telegram, Apache ZooKeeper, Grafana, AWS Kinesis, AWS SNS, AWS SQS

## Video description This is the first system design session focus on design slack. doc: https://docs.google.com/document/d/1h3xoOvewFLf0KJGCNRhFV9HiK1W_ehHc6wG4bjUPTTc/edit

[Source](https://www.youtube.com/watch?v=-UXWETzHG38)

中文简报

Slack架构解耦消息落盘与高频互动 规避热点行锁瓶颈 分布式即时通讯系统必须将静态消息存储与高频交互状态彻底解耦,在核心数据表原地更新字段或强求分布式强一致性必然导致行锁竞争雪崩与系统性能断崖式下跌。 本期内容围绕设计大型企业即时通讯系统 Slack 展开,系统性剖析了从业务需求梳理、容量估算到微服务拆分的完整链路。讲者重点讲解了核心消息流与富媒体后处理管道的设计,并在存储选型上深入对比了分片关系型数据库(SQL)、DynamoDB 与 Redis 的适用边界,规避诸如高频就地更新与热点行锁等架构陷阱。此外,讲者还透彻推导了未读消息游标、群发扩散(Fan-out)、分布式消息乱序以及基于 WebSocket 的长连接扩展方案,并结合工业界实战数据剖析了系统达成无限水平扩展(Infinite Scale)的真实工程权衡。 存储架构核心解耦:账户与群组元数据由分片 SQL 承载,海量消息存储依托支持线性水平扩展的 DynamoDB,高频 Emoji 反应则独立至 Redis 内存计数器,严禁在核心消息记录上频繁原地更新引发行锁竞争。 (25:00) 关系与未读游标设计:好友表采用以 user_id_1 为分片键的双向冗余存储避免广播扫描,单聊与群聊均通过独立的 last_view_date 游标比对识别未读,规避在大群消息体上打全员阅读标记的失控复杂度。 (55:00) 多媒体与状态机异步流转:消息状态机严格区隔 Received 与 Sent,大文件通过客户端直传 S3 预签名 URL 结合异步 Worker 池处理,完成转码、缩略图提取与安全扫描后再推向 CDN 分发。 (35:00) 一致性与分布式时序妥协:依赖 APNs/FCM 等第三方通道的通知推送应放弃损耗极大的两阶段提交(2PC),以 At-Least-Once 加幂等重试为主;而在海量分布式节点下,除依托硬件原子钟外绝对消息顺序无法 100% 得到保证。 (45:00)

Reading summary

讲师 Scott Shi 指出,即时通讯系统设计的核心在于结合业务直觉将庞大架构拆解为高内聚积木,通过动静解耦、读写隔离与精细分片来规避锁竞争与热点瓶颈。 系统设计哲学与评审边界 (0:00) 架构设计需优先明确业务场景与约束边界,避免因缺乏业务直觉而在面对微小需求变更时陷入推倒重来的误区。讲师将分布式系统比作“堆积木”,API 和数据结构决定积木形状,存储选型决定承载强度。在方案评审中,根本性逻辑缺陷(如不当使用乐观锁)会带来以年为单位的纠偏代价;而缺乏理论最优解的方案只要在当前现实约束下可行且稳定,就是合格的工程妥协。 动静分离与避免就地高频更新 (25:00) 针对聊天系统的热点竞争,设计红线是严禁在静态主表上高频原地更新动态数据。例如海量用户点赞或发送表情时,单条消息可能面临数千 QPS 的行锁轰炸,必须将 Emoji 计数与状态剥离至独立的 Emoji Service 并由 Redis 承接。同理,用户在线状态不应频繁改写基础信息表,而应抽离出独立的活跃用户表或通过网关层的 WebSocket 心跳感知维持。 存储分工与未读状态的游标建模 (55:00) 关系型数据(账户、群元数据)适合使用关系型数据库,海量多媒体文件直传 S3 并经 CDN 分发,而吞吐增长最高的核心聊天流则依赖 DynamoDB 的水平扩展能力。未读消息状态绝不能直接维护在 Message 实体上,否则多人群聊的并发写回将彻底失控。系统通过在好友关系表和群成员关系表中维护各自独立的 last_view_date 时间戳游标,仅在用户主动切入频道时发起轻量单行点查与更新,以规范化行记录抹平并发锁争抢。 通知下发与工程交付语义的权衡 (45:00) 在消息通知流转中,盲目追求 Exactly-Once 交付是典型的过度设计。由于推送往往依赖 APNs 或 FCM 等外部第三方通道,实现强一致需引入性能损耗巨大、通信延时翻倍的两阶段提交(2PC)。工程上更推荐基于消息队列实现 At-Least-Once 异步投递,结合巡检补偿与消费端幂等去重来高效达成业务可靠性。 分布式时序现实与数据库分片机制 (75:00) 在海量分布式环境下,客户端时间戳极易被篡改,必须由服务端生成;但因时钟漂移与网络抖动,除非引入类似 Google Spanner 依托硬件原子钟的方案,普通分布式系统的消息绝对时序永远无法百分之百被保证。此外,原生 MySQL 无法感知集群分片,横向无限扩展必须依赖应用层或代理层自研路由服务,并通过 ZooKeeper 维护分片与物理节点的拓扑映射。 该架构通过清晰解耦为高并发即时通信提供了通用范式,但跨数据中心网络抖动下的长尾延迟以及完全规避消息乱序等极端问题,依然受限于物理硬件与分布式时钟的客观规律。

Source quotes

[0:00] Scott Shi: 这得全盘重构系统架构(totally restructure) [3:45] Scott Shi: You're blessed, you're fortunate to find it from system design level [25:00] Scott Shi: 在同一个数据记录上进行高频重复更新(Repeatedly update the same record) [40:00] Scott Shi: 它 work 就 work [45:00] Scott Shi: 会把你的 performance 降低至少一个数量级,慢了十倍 [75:00] Scott Shi: 消息绝对时序是永远无法百分之百被保证的(Message order is never guaranteed) [78:40] Scott Shi: 经不起两个明星同时出轨 [90:00] Scott Shi: 无论是 Facebook Messenger 还是微信(WeChat),其核心设计思想和底层脉络都是相通的 [92:15] Scott Shi: 以 Data 服人 [94:45] Scott Shi: 从 SQL 数据库引擎本身的视角来看,它压根不知道什么是 Data Partition

Original transcript

[0:00] 好,然后我先录上音,我把这个folder再发一下,然后大家有空可以看一下然后In case,你没有直接link,然后我们这次呢,我这样propost了一个solution,然后最下面有个Q&A如果你有什么问题的话,不是很urgent的问题,比如说你觉得我这个design不work,你要觉得design不work,你就直接提出来,或者你觉得不是urgent的question,你就直接提到Q&A,然后我们work through这个design以后呢,会go through Q&A谢谢大家好然后我接着谈,就是说很多时候就是如果没有额外的要求,特别面试的时候尤其如此,就是怎么样怎样怎样来。然后但是在比如说工程中,比如在工作中的System Design的话,你需要做一些额外的事情,比如说你要分析可能的很快的,就是immediate near future feature,Near future features requirement,这个是非常重要的,因为如果你比如说你设计一个系统,很多时候跟你讨论的人不是technical oriented,比如说product manager,当你说让你加个额外feature,比如说在user profile里面加一个birth of date,然后能search based on birth of date,然后你说我们必须totally restructure,或者totally那这样的话就会给人一种很不靠谱的印象,所以说你就是当你design的时候,你很快要fool-seen,就是很多near future 的requirements,所以说在system design 里面,就是 business sense 非常重要,你一定要理解就是你的 business sense 所以包括在interview的时候,你可能需要花10分钟时间去搞清楚你的business sales case和business sense然后呢,就是第二个十分钟可能要花在这个constance上,然后就是architecture,api,database choice,data model,很多时候你architecture和database choice可以放一起,就是当你画完那个graph的时候,你就知道就是用什么database了。所以你这个后面的时间其实还是要稍微长一点,但是我还是强烈建议大家花点十分钟时间,去focus on the use case和 common sense,还有 business use case,这是非常重要的,就是所有的design都是business oriented,所以business sense非常重要非常重要啊,然后这个后面的Design呢,其实有一系列就是有迹可循,我喜欢把这个design变成堆积木,就比如说面试官让你堆一个埃菲尔铁塔,然后面试官要求埃菲尔铁塔上面能放一个1公斤的旗帜或1公斤的糖果,是吧?那么你就得选择,首先你得看这个埃菲尔铁塔形状是什么样子,是吧?你得选择积木的形状符合埃菲尔铁塔然后这是相当于选database,选API design,这个形状向FH大。然后同时有一些subtile要求,比如说这个user要求10个million user同时上来,那你就必须对你的积木的材质有所了解。那你就必须对你这个比如说你这个database我到底选用mySQL呢还是选用Cassandra呢,我们必须对这个积木的材质有所了解,所以说这个system,distribute system design和那个传统的engineering design和对积木其实没有本质的区别,就是你知道你的tool是什么,然后你把它tool combine起来,然后然后堆层一个能解决问题的积木能堆层一个符合甲方要求的积木符合面试官要求的积木然后其次的话在System Design中你会发现你看到System Design work不work,比如说,Present your system design,用你的common sense,用你的back knowledge,想它work,是难度是远远低于你自己设计一个的,知道吧?所以面试关键System Design中,其实工作会远远比被面试的人要容易,因为你只要看这个人设计的容易,就是work不work就行了,满不满足,说要求checklist就可以了,但是你要设计一个就很难,所以说这个就是Hacker News,我喜欢读Hacker News,Hacker News上有一种成熟的观点就是挺多人观点,包括我有时候在跟组里面讨论的时候说,System Design有两种disagree一种 disagree是我认为它是不正常的工作。 基本上,它不正常在做的。这种是一种design,这种一般你就得搞清楚为什么fundamentally don't work,有可能是提问题的人有问题,提的问题有问题,有可能是你的设计有问题,这是非常常见的。我有这种经验,就是我设计的一个系统,然后fundamentally didn't work,然后被打回去从设计的,这样是很不错的,因为你从series的角度, [5:00] 就说你的System don't work,有点 embarrassing,但是确实有这种情况,就是我花了一个月时间准备,然后去给那个Distinguished Engineer President去了,然后说这个不 work,然后想了想确实不 work,有个optimistic locking的一个,就是concurrency control的一个设计,确实不 work, embarrassing,但是你花一周以后再解决这个问题的时候,你会设计一个从Series去work的系统,因为很多时候我觉得,就是比如说,你发现BIG的话Coding 的 bug 你都是能解决的,但是System Design 的 bug 你除非 architecture 那可能花以年来计算了。所以这个如果能从 theory 的角度就发现不 work 的话,那需要就是,you're blessed, you're fortunate to find it from system design level。然后另一种disgrade,我们说过两种disgrade,一种disgrade是 fundamentally not working,另一种 discrete是I don't know what's the best solution, but I don't think your system design is actually a best solution.就是他不知道你这个design solution是不是best,但是他觉得你的design solution不是best,如果他能提出Best Solution,那当然就好了,但是很有可能很多时候提不出Best Solution,为什么提出Best Solution呢?因为你这个System Design是有很多Construents,有很多假设的Assumption的,可能只有Oracle,可能只有Oracle知道Better Solution或Best Solution,就比如说你下那个,围棋像alphaGo就是出来砍瓜切菜,就是Google研发的alphaGo砍瓜切菜。但是我看了好多职业围棋的评论说,他们假设有一种叫围棋之神的话,就是Oracle,OracleGo,那觉得alphaGo是不到围棋之神的水平的,但是他没有办法去beatalphaGo。你知道吧,就是没有办法去beat AlphaGo,但是他们觉得AlphaGo没到围棋之神的水平。那他就是最强的但是没有办法,但是他没有就是最强的,但是他们不是最优的。对吧这是那种,如果是这种的话,像AlphaGo别人觉得别人不赞同他是最强的但是别人早不见比他强的,那就是一个最好的design,至少现在是最好的design。但是还有另一种情况,不是best design原因,是best design一般是要求你下面积木非常customized,但是我们看到已有的比如说database啊,已有的tools啊,其实它很多都是非常僵硬的,就是比如说denomodb就用起来非常僵硬很多时候。这是有多种原因的,所以你不是bestdesign很有可能是因为已有的tools不是能 support 你变成 best design 是吧?你可能会有个 better solution 如果你 reinvented tool的话 你可能会有个 better solution 但你不可能花时间去 reinvented tool 除非你是做 infra 是吧?做 infra 你可能就是根据你的 case 来 fundamentally reinvented your tool但是很多时候你没有这个时间,所以你必须将就一下,所以说这种的话你就可能不是best solution,但 is a working solution.所以说,包括不管是工作的时候还是系统面试的时候,要分清两种一种是这个design fundamentally not working,那你就该discuss,该改改,然后另一种是It's not the best solution,但是我可能没有想到better solution,或者是我觉得有可能有better solution,但我就是总是没有想到,那其实这个是design to work,你到时候跟面试官讨论的时候也可以这么说,就是我觉得,I don't think my design is fundamentally incorrect,but I think there might be a better solution in this one,but I couldn't find one.你就说可能会有个围棋之神下的比α够好,但是短时间内早不见是这样子的所以说System Design只要有找一个working solution那就很不错了好,有什么问题吗?大家有什么问题吗?我觉得有什么想法,有什么问题?好,那我继续了。我们上周讨论了一下Design Slack,满足以下几个functionality。通过有Direct Message,我们当时谈的这个Requirement就是Direct Message可能有 group message 或 individual message 然后您可以有 link preview然后你有 multimedia message,然后 message notification, highlight on-rate message首先就面对这个,如果从这个Design的这个System Design Interview的Trix来讲的话,就是你觉得首先这个问题就太大了。很多时候我会拿这个问题问三年工作以上的同学的,就是你遇见这个以后你会发现这个问题太大了,说它会reduce it to a smaller group,就是把它分成一个MVP feature和bonus feature,这是第一点。所以我们到时候可以说我们MVP feature是以下几个,然后bonus feature是剩下的。然后其次呢,你要分析出这个有什么assignment,是在这个design里面,因为所有design问题都非常非常非常笼统 [10:00] 你要导清楚一些 assumption 比如说你可以说这个里面我们就assume有 authentication authorization done 了 你这个shoulder request都有个user token就是Verify A是 A, B是 B了然后你也可以信我们说这个Mobile方面或web client方面就是不在这个design讨论中,我们这次所有的design discussion都没有mobile和web client,因为我对这块不熟。如果有同学改进去organize办法,我也会挺感兴趣的,但是我对这块不熟,所以我可能就bypass所有的mobile和web client的design。它们有不同的 product designSystem Design好,然后另一点的话,在这些都搞定了以后,你要明确你这个Design是为什么目的服务的,我觉得上次那个孙长同学说的很好,说这个Slack首先是要high available就是什么时候在在线,然后 high reliable 就是你Message不应该lost 是吧 然后Consistent 你不能就是Consistent的话 最后就是你不能出现这个inconsistent message 是吧,就是A和B看的不一样,这样会出现很多问题,然后最后一点是real time,就是message应该deliver相对比较及时一点,然后我们上一次讨论的时候呢是,讨论了一个就是把它量化,这块前面都是定向那个问题,然后讨论一下这个Use Case,然后把这个问题量化,量化比如说我们可能有Daily Active User可能是10 million Daily Active User,然后我们这时候就做了一些简单计算假设一个user每天发100个信息的话然后高峰时段是平均时段的5倍的话,那我们平均下来的message per second是5万个message per second,这个系统需要处理5万个message per second,然后这个throughput是假设一个message 100byte,100byte可能稍微有点小,但是假设的话就是差不多throughput 100MB/sStorage 需要10个TB的Data然后我们这会还调了一些其他的条件,比如说一个group里面有多少个member,我们说可能1000个member,这个都是条件,你要跟你的面试官问这个问主要focus上两个方面,一个是QPS比如说这块QPS就是message角度是5万个message每秒,然后从data的角度是100个message每秒,然后另一个角度就是total data size,然后你这个storage可能就是10个T,这块应该是assume你有time to leave的,有的message app是没有storage limitation的,它是guarantee forever store store message的,那个的话可能就得,那个的话可能就比如说discord,那种你就可能得计算10年以后的storage有多少或者是五年之内的Storage growth多少,因为你可以通过其他方式,虽然你是stall all message,但没有人会看两年前的message,一般来说。所以可能有通过其他的优化的方式。然后我看到一个常见的问题就是有同学问说,怎么样来判断这个数值大小,该什么时候做什么了?这个其实这个数值是一直在变化的,我觉得常见的 rule of sum就是,如果你有超过1000个message, QPS 每秒的话,那你多半是要多于两台机器的,首先你不能只有一台机器,因为只有一台机器有Single Pied Failure,所以你一定要至少两台。但是如果你是要多于1000的话,1000到10000之间的话,你多半是要把它在就是further partition,但是这个只是一个大概的数字,就是比如说1万是基本上一定要做partition的,就需要就是多台机器来处理的但是这个数字其实一直在变的,我这块列了一个Further Reading,这个是Expensify的CEO写的,非常有趣,他是用一个非常powerful的single,Sequal Lite,一个非常powerful的single signal light,然后可以处理4个million的QPS,它是用192个核,然后这个storage是3个T的SSD,然后一个T的memory,它是可以处理4个million的QPS,但是这个所有的QPS都是read的,它有write的话,可能会更复杂一点,所以你就可以看到,你如果在AWS上做一个相似的机器,我查了一下,大概一年花24万美元,大概一年花24万美元就可能比这个稍微差一点因为它这个机器实在有点过于powerful了这是18年1月分析的,这个机器过有点powerful了,可能需要24万多美元。但是它可以达到4个million的QPS,就是一台机器,Skill up可以到4个million,所以感觉很有趣。这篇文章我也很适合读啊,我觉得它design非常泥石流,就是不用DNS,然后把说的那个server lookup放到那个exactual host里面,然后换成一个JSON,然后copy它那个configuration,就是SCP copy。 [15:00] For example, besidehub DNS, domain name server loophub service. I think the design is very nice, but work is very nice, I think it's pretty interesting. So you can see, you can probably see that if you can scale up, 4 million will be able to reach out, but for Ray, 1 million will be able to reach out. So this is a very expensive skill up. And you can probably say that if you're a tablet, you can use 1000 to 1 million. And this one million is only read, right? You can use it for a little bit. Um, So, usually, if you have 1000 of the KPS, you need to do a partition. And the other thing, You have to look at data. There are QPSs very low. For example, I do data. It's only three hours, but every request is 2 gigabytes. So you can read a minute to 10,000,000 requests. And then you can read a byte of data size. So you can read a little bit more. But your data size is a big. So you can see if data size is QPS, or bottleneck, or QPS. to bootle neck, you know, you're gonna be able to do it. And storage size, For a MySQL server, it's about 500 gigabytes. 500pkg, it may be a little more small. Like the skill app server, storage is 3TB. It has to be divided for the system and other things. You can't use it all, because you have other needs. So you have a device, I think it's more reasonable. I will talk about B-tray, B-tray is 3 or 4. If you have B-tray level is 3, it's about 50 gigabytes. Estimated data size. If you have B-tray level is 4, it's about 50 gigabytes. Constraints should consider every device network bandwidth. This is what we should consider. But this is not the main limitation. There are some people who are dependent on that. A lot of these are not the limitations. Why is it not the limitation? Basically, your limitation is usually the database read and write Because you have to be asked for disk, so the response time is the most slow. Network bandwidth, no, you're not sure what's going on. You know, you're not sure what's going on. uh If you think about the constraints, it's CPU utilization rate, memory utilization rate, disk utilization rate, network bandwidth, from the computer perspective. And if it's over 50% then it's usually less than 50% or 40% because you may have data center fill over. If you have 50% of the time, you need to horizontally scale, you need to open up a different device. How? I can see there are some people who are using this. You can sit here, it's fine. Let's talk about architecture. This is the design of the architecture. It's much more complicated than the first topic. It's also more complicated than the other topic. If you have any questions, you can ask. We break up from micro services, break up from many. For example, you have account services. It's manager user account information. You have a user service, maybe manage user, friendship information. You may not have Slack, because you have Slack, you can talk with any other organization. But you can use WeChat in the chat. or you can also use verify friends You can call a group service, manage group membership. Media Service is a network of multimedia and linked information. Message Storage Service is a network of messages. This is not responsible for the information. Notification Service is a service for notification users. And Channel Service is a service for Master Service, so you can use the request from user to channel service. This is my design. If you have a design, work, then it will be fine. And the last one is Message Search Service. This is the message searching. So in this design, we all break into small parts. So it looks more complicated. There are many micro services. But if you want to see all the functions, it will not be very complicated. And in architecture, we will talk about the database. In this case [20:00] 我们先讨论简单的Account Service, Friendship Service和 Group Service呢,这个可以用SQL来存储它的Data,因为它就是非常relational database,比如说这个Account的一些metadata information,然后谁和谁是friend,谁在哪一个group里面,这个可以用SQL来存。然后因为它这个Lookup有非常多的,显著的最新的Data Lookup更frequent的所以你可以在上面加一层Redis和Mem Cache 做Caching所有的Caching都是优化 你可以最后再提啊你可以先设计一个working solution 然后最后加Caching一般加了Caching以后你的Lookup Time会缩小至少一个数量级是10倍到20倍甚至100倍也可能所以这是开现的主要目的然后Medium Service的话比如说你upload一个message或upload一个video的话说你要uploadAmazon S3然后你可以在上面加一个CDN, Content Delivery Network这个是为了更快的deliver data的谢谢谢谢大家这个是为了更快deliver data的请这位同学mute一下如果你有问题的话请问然后 Notification Service的话,你可以通过传统的Storage去做,你可以把它变成一个API。然后靠 external notification 或者你可以做成 Kafka,Uber 是有个 Kafka notification service,你只要往这个 Kafka message queue 里面卡夫卡这个message topic 里面发message,然后他就会guarantee delivery,然后这个notification这块exactly wants是非常复杂的,因为很多时候你要设计靠一个 third party API,比如说一般 notification 的话,比如说 Apple 它的 Apple notification API,然后然后Android有它自己的API,然后如果你要Exactly wants的话,如果你要用Exactly Wants的话,你需要做Two-Face Commit,非常复杂,not worth it,除非你有这个需求Exactly Wants,所以一般不做Exactly Wants,你如果出现了Duplation Notification, that's fineMeshed Story Service的话Message Story Service的话,这块的话就主要存储Message的,我是用Amazon的DenimalDB存储的,这个DenimalDB的话有多方面原因,一方面原因是DenimalDB呢,就是它Originally support horizontal scale,它基本上可以 almost can be infinite scale,但DenimalDB在工业界中已经常见,就是scale to a thousand nodes,然后就是它skewability相对来说,你比sql来说,sql其实你也可以infinite scale,但是你就是不需要花那么多额外的精力去管理它,然后其次的话DenimalDB的API呢对这个message storage有额外的好处,我们会稍后讨论。你有它那个Zaml的定位,Zaml的那个cacher叫DAXDenemo是closed source,是AWS版的Cassandra,Cassandra是open source版的Denemo,但是中间有一些不一样,就是它有一些implementation details不一样,比如说它那个Hinterly handoff是一样的,但是它conflict resolution,Cassandra是用last right wing的,DenemoDB是可以支持其他的不同的conflict resolution logic,但是很多地方是相似的,然后其次的话DenemoDB其实没有开源,你只知道它大概的设计,它有一个Denemo paper,我会把它打开,是就很简单一读啊,你知道它的API,但是还有很多地方它其实细节是不知道的。然后其次的话,最后一点的话是,DenableDB的Secondary Support比较好,Cassandra的Secondary Support不是太好Secondary index support不是太好。然后就是你可以再,然后我们接着谈,这个message呢,读的时候你是很有跟时间性相关,你很常见的是读最近的message。所以你可能要在message上面加个cashCashLayer,然后Dynamo有自己的CashLayer叫DAX,然后如果没有的话你可以自己加一个Redis也可以的,这个也可以加个CashLayer。我们说加CashLayer有多种原因,一种原因就是它不skill。那你要加CacheLayer,一种原因是Reduce Cost,比如说他这个Access有fundamentally有迹可循,比如说他就是经常是读最新的Data,比如说最新的数据,那你可以加个Cache,然后可以减少Cost,或者是他那个反应时间太长,你可以加Cache加Cost。Twitter这个Caching做特别好,Twitter有很多Cache文章,写的非常非常好Twitter有很多blog讲他用caching的经验他们很多时候加了caching以后能减少90%的traffic就是看Twitter的文章Twitter对caching那块有很多优化因为很多时候你发一个message Twitter以后你其实很少去modify他们发Twitter是常见的一个操作包括你modify其实也可以cache是吧modify总体来说这个Twitter是它开始里面做的比较好,你如果读开始有兴趣的话,你可以读一些推特写的文章,写的还是很不错的。 [25:00] 然后这个你Message写完以后,你可能需要Asynchronize用Message Search Service去index一下,然后这个主要是为了search,比如说我要search一些Message,然后它背后呢是用Elastic Search这个Engine来power的,也是用Losing,估计能力 然后这个 elastic search 用 inverted index比如说我说一个我打了一个message说System Design is fine然后就是article a 然后又打了一个message说啊I like chocolate,就是message 2,然后它那个inverted index呢就是说当我search i的时候I就在Message1和Message2同时出现了,然后在Message1的Offset是什么,在Message2的Offset是什么,它就是做这个Inverted Index。原理比较简单,不是很复杂,你可以自己search一下。然后这块的话,我又加了一个额外的EmojiService,就是你可以在Message后面加Emoji,我觉得还是挺Pock的一个feature。这个你可以放到Redis里面,因为这个Redis有一个fundamental issue是Redis有个fundamental issue是什么呢?它就是不reliable,在data可能会loss,但是emoji的话,loss就loss了,无所谓。为什么emoji services separate from message storage system?好问题。我觉得在system design中有很多我们之前说过的big red flag大约大约其中最大的一个Red Flag是你repeatly update the same record如果你把这个emoji放到这个message里面,那你其实在repeatly update这个message。那这样的话就是一个非常大的Big Red Flag这个人的design功力很有问题,你不停的再update一个record。还有另一种常见的就是system design中很大的一个red flag就是比如说你在这个Uber,Design Uber里面,你会不停的说,我想看一下这个Current User的GPS Center,在哪个location。然后如果你把这个GPS的location存到user table里面然后你就不停的update这个user帖帮你的这个role,那也是非常大的big red flag。所以这个emoji service separate from message service,因为message很少去update,但是你如果把emoji和message从story的角度连在一起的话,那你就不停的update同一个message,就是big red flag。好然后我们看到还有同学说这个Friends service use graph database,不用 graph database,因为这个graph database是擅长叫search app of friends of friends,我建议你,这块有很多衍生阅读啊嗯这块有两个衍生阅读我觉得你可以对Graph Database的理解比较深,一个是Design Data Intensity Application里面在Data Model那块关于Graph Database的一种常见的那个Model描述和他常见的use case描述你会看到他最擅长的是search friends of friends或者是search 就是那个nested search这种你可以用MySQL来实践,你可以用SQL实践,MySQL支持不太好,Postgres有一些非常powerful的feature可以实践,Postgres有一些非常powerful的feature可以实践Graph database。所以很多时候,你包括Facebook是典型的Graph的一种case,他也不用Graph database,他用的是sharded MySQL,他也用的sharded MySQL,像Uber一样用的scheme类似用sharded MySQL,因为所有的Graph那个Graph的Application 即是你是GraphLookup GraphApplication你可以用graph database 但是很多时候你可以用这个normalized database也可以实现并且没有本质上的问题,没有说这个就是非常大的performance degradation并且这个更 reliable 一点对,这个emoji是给message.3,counter。所以你首先要了解这个graph database,然后知道它为什么用graph database,然后如果你没有时间去了解这个graph database,你可以这么说,这句话是绝对不会有问题的,就是说我读了这个taught paper,就是Facebooktaught paper,说我觉得所有的graph的问题可以用normalized的messicle来解决,没有fundamentally red flag,这是非常好的一个回答。然后这个Channel Service需要一个Redis去Quickly Initialize OneWap,我们会讲一下这个Channel Service的一会儿会讲一些细节,然后包括为什么讲用Redis来这个catch 就是可以quickly lookup好,我们现在结合这个,有什么question吗?在这个在这个architecture这块有什么question吗我有question 就是和那个channel里chat聊的那个question一样,emojiupdate为什么是update,非常会频繁呢?emoji在我们看来不是emoji的每个emoji icon是不变的,你只要把它放成一个text,类似于text in band到message里面就行。哦,这个是like的意思是吗? [30:00] 对啊,这就是Emoji啊你看到如果这样的话,如果update到同一个message里面的话,像我们大的组的话,几千个人同时在update,可能有1000QPS在一个data上不停的update。我们是以为是在那个message里面本身inband的emoji。不是的,那个其实就是message,比如说在这块发一个emoji,这和message没有本质区别。这就和Message没有本质区别它就不是emoji,这个就不是message emoji service,这就是message,说白了是一个你看的是emoji,但实际上它就是一个sum up这种你想像这就是一个tax?嗯对对Emoji本身存在哪里,Emoji这块提了Redis 0,我们之后会提这个就是它那个Storage 和怎么样存然后我们之后会贴,然后就是DevDev一下谢谢大家对就是每个message被点赞的意思好,我估计没有问题了,我们来看一眼这个API。这个API呢,我自己有时候也容易写漏,所以我还仔细研究了一下这块。比如说当你打开这个界面的时候,就是我这个界面,打开这个界面的时候你实际上是有个Most Recently Channel就是你的Most Recently Channel 或者Last View的Channel然后你有丽丝的 channels就是List of channels然后有一些是 direct message,有一些是 group message,然后如果你有Unread group message的话,你还会就是highlight一下然后有人mention你的话你还会有个number是吧然后这是你的这个 service 所以你这块 API 呢你可能最常见的一个API就是getChannels,然后给个UserID然后你需要start last view channel你这个就是你return的这个data model呢你要有last view channel然后呢你需要install visible channels比如说哪些channel是visible的 然后包括这个就包括什么呢 recent direct message membership group你需要收 unread message对吧,你需要说onread message,我这块没有,我没有列的很细啊,这块应该的话,你比如说你可能会选择一个不管是 REST API 我建议大家面试用 Swift 或Swift 或者是 其实Swift 没有那个GRPC 好 可能用GRPC 这种比较好一点如果你习惯用 REST API 用 REST API 也可以 但是你这块应该把那个Data列的细一点,就是你比如说,input payload是什么,output payload是什么,我这块就省略了,我觉得这个不是难点所在啊Protobuf比Swift要好一点,Swift的英诡过于复杂,然后我现在看大家工业界的普遍观点是觉得Protobuf是比较好的,Swift问题相对比较多一点然后并且Protobuf我感觉好像Document相对比Srift好一点有各种各样原因,它说白了是个encoding,但是总体来说,感觉ProBuff的项目可能比Swift更 popular一点。但是这个popularity也是风水淋流转的然后你进化的话还要show onread message 和 last message preview这是你这个就是一开始就是你打开那个页面的一个API,GateChannel,给一个UserIDreturn data,我们稍后会看到这个在database里面怎么存,然后你可能有一个message,比如send message,比如说我可以有个人自己说,send message,是吧,比如说hello什么的。然后这个里面有什么呢?比如说有Sender ID, Receiver ID, Message Type, Message Type可能是Direct Message还是Goof Message然后还有media ID,media的话比如说你可以就是连一个videoAudio或者是URL然后在Internal里 你的Channel Service 我们讨论过Channel Service是User Face-in Service这是Channel Service,你的Channel Service会把它append很多额外的MetaData,比如说什么时候create的什么时候updated 什么时候expired然后State是什么?State可能是received的,比如说这个Message的server端received掉了,这个Message的server端send给recipient了,这个Message的server端的recipient,就是你的responder read了然后这个MessageDeleted然后这个有的是有requirement的,比如说你用whatsapp的话他会明确的告诉你这个message什么时候send了,然后这个message什么时候view的了,就是额外的feature为什么要分成receive和send呢?因为你这个有的multimedia language,multimedia message你需要额外的做 [35:00] Close as a. You may need to add some additional details, like you need to fix it, add a thumbnail, add a photo of the photo. For example, it may be a URL, you need to crawl, preview the URL. So you have to do an additional solution. So in this situation, you will need to receive and then send You need to receive and then you can send it. So if we look at this message, this message is a good feeling. If the user is just signed, the user is just signed. If it's just a single time, it has had a 4-day update. The first one is received and sent. If you need to fix it, you need to receive. Then send it to the user and then send it to send. And then again called viewed and then again deleted If you don't have the features, like what is the view, what is the sound? You can receive it, not okay. Receive it is not okay. Like, if you send it, you will send it. If you send it, you will send it. If you send it, you will send it. If you send it, you will send it. If you send it, you will send it. or the same thing, the server is not in the same way. Just to show me server done, no received. So the same thing is very small for the user to update the same message. There are 90% cases of the cases that only one update. Then you can update message and update message and delete message. Then you can upload files, for example, I want to send a file to a URL, for example, I want to send a URL to a URL. I was writing it UIL and image are not exactly the same. image is a Just need to put it I put it in a state, but I don't need to use it. But when I found it, it was a preview. Upload file, you can upload an image, an audio, or a video. upload a video And these are all post-processing. The most known upload image is used to be a message queue. It's using message queue or worker pool. When you upload it, you first request a request for Amazon S3. I'll just request a call And then you put the file Take a look. and then turn to M1c and then turn it off after it will be replicated I'll just replicate it, if not replicate it, it will be lost. Replicated it, and then post processing. When it's done well, you can say message is ready, and send it to the recipient. And you may need some metadata, upload file. You can do some metadata. You don't need to worry about it. Media service is a very complicated service. Safety team has been spent a long time. It's been a engineer. So it's pretty long. Media services are not easy to do, but rather complicated. And you may need a loopup entity. For example, I need a loopup entity. I need a user. I need a user. And you can quickly return. This is a try to create a new tool. You can use a join group. Just like you said, you can change your group membership. You can get direct message, for example, I'm getting direct message, I'm going to click on my channel. Then I'll get out of my direct messages. You can get group messages. I open a group message. I just opened a group message. I just opened a group message. And then there are some of the most famous crude for group information and crude for User information, I don't want to read this. This is a non-design area. It's too much. It's time to be able to grow. Group information, crude is create, read, update, delete. And in the design API, you should be careful about that, like this service, it is a public API and internal API. For example, the public API is what we said about mobile support. Internal API, for example, you have Process Media, internal API, when you upload it after When you upload file is completed, you may have a process media process. Then you can do it in the same way. And this is the reliability and post processing and general preview. General Proviews has a very interesting reading. The dating app is a evaluation of the data. And the generated preview is a bug. It's interesting. You can use the "distribute to CDN" to do it. You may be able to distribute to CDNs. This is to make multi-media in CDN. [40:00] CDN其实是我觉得用户日常生活中使用最多的,比如说你去看YouTube,看B站,很多这个Data全部都是chop层非常小的一部分,比如说是好十几秒或一秒然后把这个放到CDN上去传的然后我看了没有人问怎么样design cracklist 啊 其实cracklist 是纯粹用cdn 实践的就是当你post一个新的cracklist post的时候,其实cracklist就有一个worker就把它放在一个那个queue里面然后他每小时呢,把所有的新的post结合之前的postgenerated成一个static的一个HTML就是一个静态的网页,然后把它distribute到CDN上,然后别人当你访问cracklist的时候,他就把静态的网页给你。cracklist就是用通过CDN时间的,所以可以很快。也是一个非常女士流的时间方式,但是他work就work。非常有趣,然后 Notification Service 也是另一种User Service 你可能需要Notify User 比如说你需要Notify User 这个有一个message,需要 notify user,是哪一个被notify的user,然后你可能需要有个sender ID,是吧?就是谁sender给你的然后需要Message ID,然后Message Preview,很多时候你的那个就是 Notification 用户,你不需要把Full Message Pass 过去,你就只要一个Message Preview 就可以了。然后我们这块这个Stretch Hold 没有谈很多,就是Security 角度的,比如说这个Message不能超过两个Key 的一个Corrector 啊这种包括你这个user是send user,就是你不能一个user一秒钟发1000个message,然后DDOS这个server,都是有这种假设的如果面试官问到的话你可以提一下,但是你就假设这个你要根据你的那个用户是吧,说我这个用户,因为我要去那个throttle这个用户,或者rate limiting这个用户,他一秒钟只能发10个message,如果他一秒钟发10个message以上的话,我就应该throttle,然后你每小时可能还要throttle,比如说这个user每小时只能发300个message,一般这种搜索是非常非常多的,因为你如果被人知道这个搜索怎么搜索的话,你总有办法来DDoS你。所以它这个搜索会很多层限制在上面。所以这个design rate limiter也是一个比较有趣的问题,单独的一个问题然后这块呢就是notify user呢可以用kafka,就是像uber的话就用kafka,这个相当于request through message queue了你用卡夫卡就是你这块send notification就是Insert一个Message到Kafka里面,然后Kafka会有个Handler,然后自动帮你去Send Notification,然后有Retri嗯现在的变化是很难的,可能是两个 phase commit好,我们这时候就要看到table层面了。我在table 层面之前想问一下大家对这个API 这块有什么问题吗?能不能 end to end 讲一下 notification 的 retry 是怎么样的一个 workflow 呢?因为 notification 你 send out 出去以后,收到的肯定会收到一个 received thing,我 notification send successfully,然后它存到哪里,然后再怎么样 retry,对这个我们可以用卡夫卡卡夫卡汉诺了或者是这种UserDB,我就说用Message Queue和UserDB吧,因为我对Message Queue更熟一点,我自己个人是对Message Queue更熟一点。假设你用Message Queue的话,你实际上是,当你说我要Send一个就是,呃,呃,呃,比如说Sending notification message,你通过靠Message Queue,你是Asynchronized的交给另一个worker,就是从worker pool里面那个worker来处理的。如果大家对worker pool不熟悉的话,我建议补一下那个就是有一个worker pool model,或者Asynchronized ProcessingWorker pool和Asynchronized Processing很多时候相互等价的,就是很多时候说的两个意思很接近。这个您可以读一下,我看有没有情况,或者是你就是我也可以花点时间解释,看大家都有没有这个Worker pool或者Asynchronized Processing这块有没有问题。然后你删了一个Message, NotificationMessage,到这个WorkerPool里面,会有一个WorkerPickup, and then it will be sent to the third-party API, for example,你可以去Tradio删了Message, you can see if there is a WebSocket, if you have active WebSocket, you can just就是 notify mobile或者是您可以request API 卡到 Apple 的 notification 或者是Android Notification我看到有同学人说Android是Firebase的Clock Messaging Notification然后它会给你个Receipt表示自己个成功了然后他会去 [45:00] 当你接到这个resheet的时候你可以mark这个 database 说这个 complete, work complete然后把它mark成complete,如果你要guaranteeat least once的话,很有可能你需要一个database来存储,就是说我这个message,比如说我们在这个design里面,我们在刚才这个Design里面,这块可以加一个这个State这里面可以有个message notification center notification sent可以加到这个state里面对吧那他如果require notification然后我们就去靠 Trilio, Trilio,或者是那个 third party API return success 了我们再把这个 Notification Sign 填成 Yes如果这个比如说这个 third party API没有响应,它可能 sent 了,也可能没有 sent 了。那我们都不填yes 那我们可能会有一个sweeper 就是有一个background process 重新再处理一下这个message 重新再处理下这个notification上只有当你这个Sign Notification写成true的时候,当且紧张,只有一种情况就是你靠ThirtyParty API成功了,然后同时你写入Database成功了,这个Notification才是Sign。所以这是at least once的这个 notification send回答你的问题了吗嗯,差不多。还有还有其他问题问大家How about use egg?亨瑞你能聊一下这个AC吗?我对这话不熟悉我不知道这个ack指的是哪一种ack我记得以前好像是做那个Storm的时候,他好像说说,好像卡普卡里面也有,你可以那个open它那个那个aclogment的选项,它就会就是那个好像跟那个exactly once相关,但是那个东西它会让那个系统会比较慢一点。但是跟你刚才讲的一个就是用那个third party service 好像不大一样,我不知道有什么不同,我其实也不在弄。我对这块也不懂,你只要懂一个就可以了,我觉得没必要都懂,只要懂一个就可以了,因为毕竟有不同的处理方式然后我再次强调一下Exactions是非常非常复杂的一件事情,它是一定要一定要request特别是这种这个2-phase,这种3-party API的话,它是要require2-phase commits的。2-phase commits它效率非常非常低就就是一两个数量级就会把你的performance降低至少一个数量级啊,很多时候就降低至少一个数量级,就慢了10倍。所以如果你没有这个需求的话,不应该做 exactly ones我记得常见的一个就是这个Exactly Once,包括这个Two-Phase Committer有个常见的Protocol叫XA ProtocolOpenAC,对,XT你要是感兴趣的话,你可以读一下Extended Architecture去做这个其实挺早的,1991年的。很多 service 会实践这个 API,它有不同的 language API,这样的话它可以帮助你实践这个帮助你实践这个two-phase locking和这个two-phase commit和two-phase locking不要可以帮助你实践这个two-phase commit和exact events 我把它做到这块做为衍生阅读好了Cafka是类似的,Cafka,sorry加这个作为演示音读好了Kafka是类似的,您可能你可能到时候如果它repeated fill 可能是要放了一个什么地方 为了blocking notification 不管是Message Queue Kafka的话 很多时候可以把两个问题相互等价转换我个人对Message Queue少有数点,我可能更多用Message Queue来解释。如果对Cafka2也熟的话,也可以用Cafka来解释一遍这个问题。好我们接着看,然后就看到这个Data Model这块了。我们之前讨论过,如果我们看一下我们的Architecture的话,谢谢大家就是 user service, account service,User Service, Account Service, Friend Service, Group Service它是通过一个嗯SEQL来实践的,因为它更像relational database,这个general question的话,大家可以google一下,就是我解释的可能没有google好,就是你google一下,然后search一下。我不讲two-phase commit了 [50:00] 这个相对来说很复杂,Two-Face Commit很复杂,这个是前人研究的非常复杂的一个地方。如果感兴趣的话,你就直接搜Two-Face Commit,看一遍就行了。总体来说,好说白了是这样子,就是你Two-Face Committed有一个Coordinator,然后你有一大堆Group然后你当你当且紧张,所有的GrooveMember都同意说,哎,我Ready to Commit了。然后Coordinator说那我们现在commit然后再send给group member说这个全部commit了如果有一个人about的话,那么这个commit就about所以这个对,这2-phase commit 呢,很多时候在payment system, 其实也不用规定,完全用2-phase commit。它也可以通过其他的方式来解决,因为你只要你只要 item potent 的话 你就不需要 two-fifths commit 的就是你重复一个request 没有set effect的话就不用two-phase commit的MessageState通过ChannelService来update,会不会形成Service间的CircularDependency?Message是中国Channel的update 会为行用ServiceChannel应该不会,你这块其实是不会call back to channel service,你只有channel service call 这些其他的service这个direction很多时候是万位的就是你不会有个 account service 在 call back to channel service就是我们不会有一个request from account service trigger backdoc channel service这个很多时候是万味的嗯你的 notification 应该靠的不是 channel 你这个 notification update 的时候很有可能update 是 message story service当你notify完的时候你去updateMessageStoryServiceOK就是我们就不讨论这个太过于细节了就是你总有办法能解决的这个太过于细节这个问题这个问题还是挺大的考虑太过于细节的东西了我们接着讨论刚才这个Data Model的问题好我们在这讨论 database 这个里面 account service, friendship service, group service, relationship 都比较relational的一些关系,所以就放到信号里面。相对来说,它的 data 不多, account user 的话,我们像之前讨论过,10个 million,不是很多,然后Fanshift的话,一个人有100个Fund的话,大概也就是个10 billion也不是很多Group membership也不是一个非常夸张的东西它这里面data量最大的可能是message这个message很大所以你可能放到DynamoDB里面其次的话,即使它group量很大,你这个Sycle是可以通过horizontal sharding or vertical sharding来解决的,vertical sharding就是说你account service可以放到自己的sqlshard里面FriendService可以放到自己的设计圈里面,然后GroupService可以放自己设计圈里面,然后Horizontal Sharding就是说你可以把这个,比如说你把这个Friendship service呢brick 成 1000 个 shard,然后你上来就分成 1000 个 shard,然后这样的话也是可以基本上可以 infinite skill,所以sql 也是可以 infinite skill,sql 是可以 infinite skill,Depends on 还是有跟着不同的需求来说,很多时候他们很多时候说Sycle不skill是假设你不做sharing的话那是不skill的但是你可以infinite skill就是你做sharing的话可以infinite skillsharding 有些额外的technique 我觉得有三四种就是怎么样做sharding我推荐大家如果是没有时间理解的话,你就可以这么说,就用Schemeless的sharding,Schemeless就是把所有的那个SQLSharz分成4096分你就直接说根据你的那个估计未来10年data的估计直接分成多少分?分成固定的X分然后你把那个 id hash 以后,根据最后几位磨这个X 然后知道它在哪个shard这是最简单的shard 计方式 有其他shard 计方式我们可能会有空的话再聊好 所以相对比较relational的意味就是放在SQL里面然后你Multimedia肯定放到AMLON S3里面然后Message放DenomDB里面因为Message有一些额外的featureDenomDB比较能满足比如说这个user table里面我就不work 塑造这个user table 相对比较简单注意到这个user table 你create了以后其实很少去update它的你不会反复的update这个user你可能只有一个status是可能一天update几次但是总体来说你不会是反复的update这个table,很多时候很有可能,如果你有说,我要有一个方便的方式找见所有的online user,你可能需要有个active user table,当你这个user online的时候,你去update active user table,就是很多时候比如说你Uber,你靠一个Uber的话,我想知道哪些driver在online的,我们有几百万个driver, [55:00] 是吧我没有我们有几亿的rider,那怎么办呢?你应该有个active user table来找一些active user那这种情况下你反复update同一个roll的话你就应该把那个这个column从这个table里面取出来,然后形成一个自己的table可能会有个active user table,但是因为我们这个相对来说frequency不是太高,我们就可以放在一个user table里面。然后find table,计划稍微有点复杂,Find的table相对于复杂为什么相对有点复杂呢?就是你仔细想一下这个Find呢可能是说我A和B是犯的 B和A也是犯的是吧这个关系是相互的那你这个user怎么存呢?如果你只有一个的话也可以,就是说你user只存一个的话也可以,但是你share的时候会有问题,因为你没办法来说根据两个key来刷的因为你lookup的时候很有可能说,我lookup根据userid1来lookup,或者lookup根据userid2来lookup。你可能听过这样的lookup,所以说这个france相对比较复杂,我这块采取的处理方式是,比如说我和David的connect,然后然后我把它Duplicate成两个Role,就相当于是我和David Crank,David作为User1一个Role,我作为User1一个Role然后我这块同时呢把lastViewData分成两个这个主要的原因是为了Infinite Skills做的,就是为了sharding处理的,这样的话我可以说sharding可以是UserID1这样的话我就是比如说我想找所有的我和其他人发的message,对吧?所有属于我的message,不是,我想找所有属于我的friends,是吧?我想找所有我的friends,那么我就可以只根据UserID1来找就可以了我不用根据UZID2来找我可以只根据User ID 1来找,因为所有的Friend一定会User ID 1存一遍我对于任何一个Friendship来说,一定会User ID一存一遍,User ID二存一遍。但是我share的时候我只能根据一个column来share的,所以我可能说我就根据useride来刷的话,因为我duplicate,所以我只要根据useride来刷就可以了其次这个last view date这个很重要这个是什么呢,这个是用来处理这个highlight unread message 的highlight unread message 的这个data,比如说,我给你举个例子,我这块有一个onread message那我不知道我上次read在哪 是吧我往前走的时候他很快他会告诉我说你上次read在哪 是吧那这个data是不能存在message上的,为什么不能存在message上?如果你是点对点的 directmessage还可以相对来说还ok是吧点对点的嘛但是如果你是group message的话这个message里面要存一千多个人读了没有读的状态,这是非常糟糕的。所以呢,我这个设计呢,说,我这个unread message是存在last view date 上的。就说白了,比如说我send David 的一个message,我这个message有没有读,是depends上我上次什么时候看我和David之间的message的。我后来last view date 的timestamp。然后如果这个所有的新的message所有的message在这个timestamp之后,那我是没有读过的如果所有的message在timestamp之前,那我是读过的那这样的话,这个last view data其实是两种,一个是David看我们之间对话是一种一个是我看我和David之间对话是两种,是个双向的,所以说我把它分成两个了。所以这个这个Fineship Duplicate的原因是两个原因一个是为了sharing方便一点,share方便一点,你不想就是你不想读一个人的Friendship的时候,你就把这个request去所有的Sharer,去4000个Sharer一次读一遍,这个效率很低。然后第二个原因是我读我和David的Message和David读他和我的Message的last view date呢这两个data是不一样的,有各自的意义,所以我这块把它分成两个roll的话,也可以把这两个意义明确一点。这样比较好。有不同的设计也是可以满足的。但是我觉得这块我的focus,包括我出这个体面试题的考量点就是,你这个unread message的tag是不应该放到message上的。因为这个Message上的group 比如说我上了一个group的话 如果你这个Message,这个unreadMessage这个 status 放到这个 message 上的话你这个 message 要 keep 比如说一个 group 有1000 个人比如说我在这个group有5000个人那这个message他要单独处理这5000个人哪些人读了哪些人没有读这是非常累的其次,membership还是在动态变换的,复杂度就更高了,这是boowork的。谢谢大家slack有没有EV1好友这个概念我问问这个问题是什么宇航你建议解释一下这个问题吗 [60:00] 就是说能评价吧就是想问一下这slack里面就是有家好友这个概念吗还是说就是slack是没有好友的概念但是比如说你微信是有好友的概念吗这个微信不是和slack没有本质的区别吗哦,ok,所以说是扩展到其他的那个我们这把focus说这个API的focus是focus on Slack 但是你看这个这个可以用在微信可以用在telegram可以用在Facebook message可以用在WChat去吧都是相似的东西没有本质的不同,所以这是相似的,所以我说这话这个好友的概念可能是很多时候比如说WeChat啊或者什么其他的好友的概念其次的话,您可以用Find来存DirectMessage,DirectMessage有些Metadata,A和B发,B和A发,A看了A和B之间对话哪了,B看了A和B之间哪了,这个有一些metadata也是可以存在这个里面好OK检视group 里面 last meal message 怎么实践呢简单假设我们就举这个例子来说,我们就讲的group了,group membership是类似的,和上面这个非常相似,这两个是非常相似的。谢谢大家比如说我加入这个 Random Group,这叫 Random Group,所以我在这个 Random Group 里面 insert 了一个新的 row,那就是Random group 我在这个里面 我什么时候加入 我刚刚加入的 我的肉是什么 我是个user那我last view of date 就是我刚刚看过的这个那比如说divid 我们在这块加一个新的肉David刚加入这个RandomViewGroup那这块就有一个David是吧然后它JoinDate是什么然后它的肉是什么,然后它lastviewdate是什么,它lastviewdate上一秒是吧,last second那我的last view data和David last view data不一样那我在这个里面比如说这个group里面我读到所有的message因为我有个last field date我当打开这个的时候,我就refresh了last field data,但是我可以用旧的last field data来determine哪些message我读过,哪些message我没有读过。当我打开这个channel的时候,我之前有个last view date我读一下我自己的我读一下我自己的table说Scout 在这个Random这个Group 里面,LastViewData 是这个时间段,那所有信息这个时间段都是UnreadMessage但我打开这个channel以后,我把我的last view data refresh一下,refresh成刚刚我再打开的时候,新的message又不一样了那我的last field data和那个David last field data不一样,因为David还有自己的membership role它自己的Membership Row呢是它有自己的last view date解释到这个问题吗?好这个问题呢,这个问题呢和这个find table是完全,这个问题其实和这个last field data,这个group last field data和find last field data是相似的相似的,但我们这个group里面没有duplicate存,是因为这个,我觉得是为了减少一些lookup,因为你想这个事儿的话,你想一下这个user事的话,如果是这么存的话我要查一个group membership的话我只有两种方式,我去所有的group membership table 里面去找,那我能找到所有的user,或者说我可能单独存一个额外的table,专门存这个group user relationship 可能能存。Last View Date 什么时候更新? 当我Click的时候更新,比如说我这时候我click的时候那我last view of data 更新了我再click random on last view 再更新了等了吗?谢谢大家谢谢大家对于每一个登录的Client需要maintain一个long-league的collection你可以用Hardbit或者是那个WebSocket sorry好像这块逻辑呢好像Mobile 端好像会 maintain 一个 Web Socket, Web Socket 也方便 server 端进行 notificationLastView可以timestamp频繁更新CPU单独拿出来不会单独拿出来,因为你其实更新不会太频繁,你思考一下,你注意一下,每个user是有自己的那个last update,last view data的,比如说这个group是random group,每个user有自己的那个这个什么的,我在这个Random Group 更新我这个Last Mill Date 我refresh一下 我更新一次你知道吗这个更新不会太频繁的我不是说一个group只有一个roll那所有优在更新这一个肉那是有问题的但我现在是Normalize了,我是User加上GroupID加上UserID这个group有5000个user,其实是5000个肉,我只更新我自己的肉,所以这个更新不会太频繁。谢谢大家每个group上有多少条未读信息什么时候更新的这就是我们这个channel service了channel table很好,我们就接着读这个Channel这一把 [65:00] 相对比较复杂一点我看这是一个Channel Table 这个Channel Table 呢这个Channel Table是Virulize一些额外的事情,比如说我这个Channel Table就显示什么呢?显示我所有的Channel,比如说我在SafetyChannel,RandomChannel,David发了我一个东西,Uncle发了我一个东西对吧那我打开的时候或者您可以想象成微协的一个friendlist然后你会看到说哪一个饭子散了一个什么东西了然后这个Channel Table是放在Redis上的因为这个data如果丢失的话你可以通过假设这个data丢失的话这个redis crash 掉的话您可以通过 Group Membership和Funtable去reconstruct你可以通过这两个东西去reconstruct,还有Messages和MessageStoryTable,你可以去reconstruct但是你放在redis上呢你就可以快速的得到这个比如说我一打开这个我很快就得到这些所有的信息了可以快速的得到这个结果那这个Channel Table是denormalized的,是纯粹是为了一个API Request,就是能看到你,比如说你可以把它想象成一个微信是吧你读你自己的时候读你自己的微信谁发给你message然后你就会看到我在Safety 组有10个Message 未读最新的一个Message Preview是什么样子的Timestamp是什么样子的David发给我有什么messageHongray的蜡蜡蜡Uncle发给我什么时候message然后可能Uncle这个expired了如果他expired的话可能需要delete掉然后就把它不vigualize出来这个messageUnreadMessage什么时候读比如说我send一个message比如说我们就拿Davidsend给我message来说Davidsend了我一个新message对吧我们看David send me a new message, the request is to channel service, see architecture这个Request is essentially channel service,然后这个request去MessageStoryService把它作为Message存下来就是说这个Message先存到这个DenomDB里面当Message存好以后然后他在update Redis这种情况下就当你message已经存好了这个message不会丢了你不会丢了你就可以显示给David说David这个message已经send了已经send了然后这时候你再把它updateonread message变成1last message reveal变成了什么如果你在这个channel端很长时间, last view of date就不对啊,你可以离开那个channel的时候,然后再把last view of date更新啊,你可以在打开的时候更新一遍,离开的时候再更新一遍。你去更新两遍就可以了LastViewData可以只在可以指在client端做吗last build data 在client专做也行但如果你换一个client呢比如说你在手机端看的时候和你在比如说我在这看我偶尔还用手机端看那你这样的话你不是就两个之间不沟通了吗你在这块看的时候我知道有哪些message你这个multiplatform就不work了谢谢大家我估计这个Channel Table会有挺多问题的大家计划有问题吗谢谢大家 谢谢大家好 那我们接着看Message Story Table呢,这个是用DynamoDB的,DynamoDB是支持一个PartitionKey和SouthKey,这个Message expiration是在Message story 特别表里面的这个partition 支持一个partition key和soft key这块需要对denomoDB有一些额外的了解FenableDB呢它是有一个partition key和sodic key,然后partition key是决定这个data是在哪几个partition上在哪个partition上,然后它同一个partition是会replicateN点,这个N是可以定义的,一般是三变,但是你可以customize它,定义它。比如说message1,它partition可能是2,message1假设partition是1,Message 2,它partition是2,就在不同的partition上,但每个Message同时又replicate了三遍然后这个partitionK如果你partitionK不一样的话你多半不在一个partition上你就说说partition key要非常小心的定义然后sout key是什么呢?sout key叫partition key是primary key然后这个是要 Unique 的所以这个Sought Key呢,就是常见的Sought Key呢就是,比如说常见的,我给你举个例子,比如说你要读你的Shorting Card你是Amazon user,你住你的shopping card那你就可以读你的partition case user id那你读你的UZID的时候,你SoutK就是你的ShoppingCart里面Shopping cart里面你添加了item的顺序是吧那你这样读的话我就可以读出我这个user 我都要有shipping card 都有哪些item 就可以读一下partition cache就读出来了然后你也可以根据它那个partition key + third key来读出来SoughtKey好处是它在Denamo里面 [70:00] 上的好的所以您可以有比如说Binary Search啊或者是你Rent Search是比较方便的如果你没有Sought好的话,您可能需要Scan一个整个就是整个一个table才能找件所以有时候效率可能会比较低那你Sought好的话你就可以就是非常快的不用 Binary Search Transition, Index 好,你直接去就早见了不需要BinarySearcher你就index好就直接找见了所以这个SoughtK有自己额外的好处所以我们看一下我们这个request pattern我们之前看到过这个request pattern常见的是我和一个friend的message和我看一个grouply message对吧这是常见两种pattern我看一个group里面的message 就是我读list的message和我看我和一个贩子建了Message 这是常见两段read pattern如果他是这么锐的的话我们可以PartitionK,DynamalDB PartitionK变成什么呢?叫你 container ID比如说如果在group里面你可以叫group+groupid,那你就是当我读这个grouping message 的时候我就可以说我get这个partition key equal to group ID 是吧那你就读出来一个group里面所有的message然后你这个SoutKit可以变timestamp然后你可以做pagination 是吧 比如说我先读前100个messages是吧然后再读下一百个message这是getGroupMessage 如果是directMessage呢你可以把userId1和userId2 sort一下就是小的永远在前那么你用这个sought好的这个作为partition key那你这样的话也可以非常快的说我读一下我和David之间的direct message 我可以很快的读出来因为 container id 就是两个user id concordinated所以你可以很快地出出来因为它timestamp效的好的你可以非常容易的做pagination这块有不同的设计,这块的Design设计有不同的设计很多就是design不一样的还是挺复杂的这块我觉得最常见的一个design就是这个问题是什么?这块这个design有个问题是什么呢?你这个message ID你其实是没法直接找见的你这个Message ID很有可能需要做Global Index你很可能要做 Global Index, which is Secondary Key Index你是没法直接找件的,因为你这个你其实你思考一下这个里面没有一个直接的request是get message by message id有点反直觉,但是我想一下好像没有一个直接的用的case是get message by message 不是一个很常见的一个request所以你这个global secondary index 应该是work的嗯不是一个 common pattern 不是一个就是不是一个 use case and frequent pattern你更多的是get一个两个人对话之间的direct message和get一个group message,所以它这个partition可以使用Group ID或者是两个UserID concordinated起来更容易实践就是做这个然后Global Secondary Index有什么问题呢?Global Secondary Index的主要问题是它这个index可能不会immediately available比如说你刚insert了一个message你read message by ID 可能不会 immediately available,很多时候这个 global secondary index都是 asynchronous 做的,它可能一秒延迟,一秒后才能读它。所以它这个可能会有其他问题我不会太多cover global secondary index 就额外的考量那我们回答这个问题这个 message expiration 怎么实践呢你可以在这实践在这一块加一个 message expiration timestamp然后呢因为你是用DenimalDB的,DenimalDB的API是time to leave的,你可以直接说这个message time to leave是什么时候他过了这段时间以后他就直接expire了这是用database就是从database层面支持还有一种什么办法支持呢你可以从client层面支持比如说像这种情况下是你在这个channels里面是存了这些的然后你读了一大堆message,但你看到这个message已经expired的时候,你就不返回给client端了你在server端就filter掉了同时你也把Redis updated把它给删掉你懂吗?就是我现在这个Redis server在 under我这个User ID 下有一大堆message,有list of messageA sad home message嗯我读定message,我看哪些message已经expire了把它filter掉我在update Redis,我在返回给user同时,我会update Redis但我就不嫌谁不显示给user这个expired message了这是可以在client端实践所以这个你有多种实验方式你可以在database端用database已有的feature实验你可以在client端实验你这种情况看也可以叫 RAID repair将进管RagerPot更多时候指的是那个Dynamo [75:00] 没有不同的那个不同的那个nodes 它让data不一样然后再把这个data convert 成一样的我觉得这个叫radery part本身也没有什么本质的问题好How about message search would group message be used?对这个可能会用所以很有可能是你这个Message这个DataStructure很有可能是你给一个termHappy hour比如search happy hour然后它这个happy hour指向message ID的这个Message Search很有可能可以用到的你这个Message ID如果用在Global Secondary Index的话它主要的问题就是它的index时间有延迟Message search不会一个很严重的问题,因为Message search你要放到那个你要放到Elasticsearch 上 Elasticsearch Index 的时间和这个Denimal Global Secondary Index的时间机会是相似的这个都需要一些时间都需要一两秒特别是那个 Elasticsearch index时间可能会更长一点,我之前起过一个SID project用过ElexicSert,它那个Index时间就是很多时候read or write 其实会有问题就是你刚写了读你的你根据你写的东西index search 这些东西会有问题的就不会immediately available 我觉得这个大家应该有影响 比如说你在一个系统里面刚加了一个record 然后你搜你自己的刚加的record 你搜不到 在search里面搜不到 因为它这个elastic search需要一些延迟的好我一套一套读啊这里的timetable是哪个timestamp试试的好哪个node支持的那你应该是在哪一个channel service就是哪一个node哪一个channel service的node服务给你就在哪个node生存User A看到User A的Message 1回复Message 2会不会出现Message 2在Message 1之前有可能嗯这是有可能的 你这个timestamp 我觉得 big red flag 是你用clienttimestamp 这个是不可以的 因为你clienttimestamp 你可以trick it你要是melicious user的话我可以set一个timestamp在两天后那我会两天后再看到这个message吗或者你算了一个timestime在15天前那你这个是这个是一个red flag所以这个timestamp一定是在server.node生存的所以你如果有两个server.node如果你sign的时间很接近的话你很有可能会看到这个Message 1应该在Message 2之前发的但是显示的时候是Message2在Message1之前这个其实我觉得你如果就是用过一些chatting app的话你有发现这个问题这个还是挺常见的一个问题我觉得有一种方式能解决就是看你怎么事的看你怎么事的比如说你可以在request level share的是吧或者是你在DB level 12的你在DP Level 上的也可以,比如说我和送样发短信比如说送样后发短信我们俩每次在database里面都去同一个事的那就不会有问题这个timestamp也可以在那个database层面生存MySQL 都支持那个timestamp生存的就是你insert这个Role的时候它自动给你生成timestamp也可以在database层面生存这也是可以的可能在database层面生存出现的conflict可能性更小一点因为你可以通过sharding时间嘛比如说我和书雅说的对话都在一个shard里面如果是用database的生存的话那就是同一个shard里面用同一个clock那生存的话相对来说不太会有问题但是我们之前讨论过即使是同一个node的clock它有可能jumpback对吧他有可能jumpback虽然这个概率很低那也还是会出现这个问题的所以这个message order是永远不可能guarantee的这是很难guarantee的除非你用那个spanner这种这种Service 负责你的Message order 这个是给足够大的数据一定会出现message order, auto order的好,羊羊说,每次有人发一个message,所有的membersChannel推保都会被更新一下吗我们来看一看你在group里面发一个message的时候比如说你发一个message的时候你要经过哪些东西首先你要去把它message insert到Insert the message story table你这个里面把Message insert进去当你 insert 成功以后你去把所有的 membership 的 user 比如说这个 safety group 你把所有所有那个Safety Group的User全部更新一下更新怎么了更新unread message和更新last message preview是的社会更新一下但是呢这个很快,为什么很快呢?因为Redis能 handle 1秒钟可以很轻松的 handle 10.5 次 found request很快 [80:00] 就是假设你这个group 里面像我们之前assumption 是1000个user1000个user的话并且你Redis也可以sharelet,你Redis本身可以sharelet,你1000个user,你需要更新1000个user的Redis record你可能share到三个redis server里面你每个service只要更新300次然后那你其实就是0.233秒就供应完了,其实很快的非常快乐是的 如果我们 你如果看过那个推特的那个Design 的话你会知道Twitter的Design,包括现在也是,他可能后来做了一些额外的优化比如说创这种有30个M或者是300个million的follower当川普发一条文件我的时候他所有的follower都会被insert一遍同一条微博在早些年后面他好像说做了一些优化, Twitter好像做了一些优化,但是你就是比如说你作为一个网络大看,你作为一个popular的那个influencer,你发了一条推特你所有的 follower 都会更新一点所以说他们说那个开玩笑微博说那个不能支持两个明星同事出轨吗就是可能就是这么来的因为你两个明星同时出轨的时候发了一条message然后这个message呢你所有的follower全部更新一遍,然后你瞬间这个read file就报表了,这个就报表了。这块有问题,Commitual Direct Message as a SingleDoubleMember group message.所以我们不需要不同的 video message 这是可以做到的这是可以做到的这是可以做到的,包括其实这里面有很多,比如说这属于这个Random里面属于有Name的Group,你要是随便拉两个人这属于Unname的Group,对吧,这属于TemporaryGroup,你TemporaryCreative的一个Group,是吧,这个Random Group有区别,就是可能Temporary Group没有Group Name什么的但是你direct message也可以认为是就是直接是一个group只有一个人,这都是可以做到的,这是一种optimization但是 Direct Message 如果和 Group 分起来的话, 有一个问题是你这个怎么事儿那你这话可能得对这个group id 也进行一事儿的了你可能得全两遍我来解释一下为什么存量变你这块group ID和user ID,你必须share其中一个但是你这个group membership可能从user来找,比如说我作为一个user,我在哪个group里面也可能从group来找我这个group里有哪些user这个是二位的lookup 的假设我们上的是 group id假设我们share的group id,那你lookup的时候,根据你的group id,你就知道去哪个share去找了。你比如说你group id,你去share的1找就行了,注意我们可能有1000多个share的是吧?很常见可能有1000多个事的你根据你的group id就知道去哪个事就走了但如果你找这个user在哪个group呢那你就没法去具定的一个groupshare找了你可能要把所有的groupshare全部找一遍你可能知道这个user在哪个group对吧所以这就是问题所在所以这种情况下的话你可能这两个得存两遍或者是你这个UserID做一个Global你自己做一个Global Secondary Index,如果用Syncl的话自己做Global Secondary Index就是你就根据UZID你就知道在哪几个share之间了Channel Table里面存全变量的user还是只是online的user这个不是个Redis吗?您可以Redis 的话,我觉得这个是 trade-off了,您可以只存 online user,但是有一个新 user online 的时候,你需要生存一个 generate 一个新的这个。所以这个table是可以通过Message Storage Tablegroups table和friends table generate出来的那你这种情况下你当你新打开一个app的时候你就很慢或者您可以把这个redis shard 成,就是redis也可以shard 了吗?shart成支持所有的Redis,然后你所有的user都存一个这个也可以这个是一个 trade-off好处就是如果支撑 Active Online User 的话你的这个data量比较小但是你新user login的时候时间比较长如果你存所有的user的话,你data存的量比较大,然后可能你需要不停的有slipper,比如说把这expired message clean up 掉但是你user login 的时候特别快能不能把 group ID 当成一个 tag user tag这个是个问题,那你group id 如果当成一个 user tag 你是存在user table 里吗? 那如果我不停的 frequently加入一个group或者是离开group的话那我是不是得不停的update同一个table了 [85:00] 也可以但是还是有问题就是如果你是一个比如说相对来我的message比较少了,比如说我们CEO Daryl的话经常有人拼他那如果你被别人聘的话被人拉到一个组里面就相当于temporary这样的一个group,那你是不是也会出现我们之前说的ref flag是不停的update同一个roll,就是daro这个userroll不停的在被update说他加入这个group了然后他要离开那个group了,他要加入这个group了,这个是不是一个red flag会不会把这个变成一个hot spot然后会不会使这个database变得很慢我觉得把group IP变成一个tag不是一个很好的一个事件谢谢大家谢谢大家一般的群组合。Secondary index有两种一zone secondary index是 global secondary index这个SQL是没有Global Cycle,因为Sycle不知道shard,shard是我们额外在SQL的表度实践的所以这个Global Secondary Index要你单独去实践这个什么叫Global Secondary Index呢给一个column给一个value他告诉你在哪个shard里面有这个值比如说比如说如果我们这个shard是这个group id 的话,您可以,如果你是lookup by group id,你可以直接找,但你如果是UserID的,找UserID的话,你可能去Global Secondary Index,然后先说在哪个share里面有scot的这个group membership然后再去那个share里面再去找这个你需要自己实践 global secondary index如果是 local secondary index的话那就很容易了因为你这个Local secondary index 说完了就是 MySQL的index 就是你在这个share的里面index好了 你就只有MySQL的index就可以了这是有不同的,你如果这个不太熟的话,你可以search一下什么是global secondary index,local secondary index,然后如果这块就是,我觉得这个相对来说中间还是有很多context,大家不懂的话就是可以反复的查一下这个信息,然后查一下这个keyword,然后查一下viki啊,或者查什么的,很多解释解释的很好,我觉得可能解释比我好多了。谢谢大家没有看到有Q&A还有什么问题吗谢谢大家好我想我想问一下那个status更新比如说谁是谁在线这个功能的workflow能能讲一遍吗安推安的就我觉得这个更新的frequency是很高的然后放在那个table里面会比较不合理我这边 again 说一下我看到有人问就是说这个QPS会不会太高放这个table里面是不合理如果不合理的话,单独立到一个active user table,这个跟我们之后可能会讲Design uber,这个和Uber design是类似的,大部分user都是not online的话可以就是放了一个active user table如果它这个 你分析 你可以就是quantify的计算下这个QPS比如说你要是10个million的user的话那你实际上我一天大部分时间可能就是在线是吧?一个user可能一小时更新一下status,你算一下这个QPS然后这个QPS注意到你这个是要翻就是翻奥子的因为你这个user table 本身就是二个了你比如说放了十个事在里面那你放到每个GPS合不合理好其实对于每一个user来说,你如果一小时update一次的话,你放了一个肉里无所谓去吧你一小时或十几分钟update一次你放了一个肉里无所备那你就是说你额外的对于这个User Table 额外的QPS放到user这合不合理我觉得还是相对来说不是一个red flagUber 那个 case 可能是 red flag 因为Uber 大部分的用者都是 offline而且你online是非常random的行为就是那个online的detect你是怎么detect的,是用heartbeat还是用就是它自己send,主动push给suffice一个notice, saying I'm offline, I'm online, that's it.我知道了,你这个online detect,其实是那个,你可以用那个web socket,就是首先我对这话不是太熟,但是我看到的一些已有的实践是用web socket的就是说当我online的时候谢谢大家我建立了一个socket然后呢这个websocket没有X bar 你可以用heartbeat来不停的detect没有X bar别人发过message的时候我可以pushNotification,就是server可以push notification给我这个web socket,然后notify我。如果从这个角度来讲的话,我觉得status是可以放到一个单独的table。OKQPS 会不会太大?你QPS 分两种吗?一种是机器上的QPS,一个是每个人的,不会太大,因为你这个web socket的话,你这个server端可以不停的skill吗? [90:00] 你就是有专门有server管这个web socket这块的,你可以比如说你现在有一个million的user,你可以分成100个server,然后每个server maintain 10万个web socket,是吧?然后你根据user id 能知道哪个server管你的web socket,然后来发你的notification,是吧?这个是可以解决的。好 我觉得还是大家消化一下如果没有问题的话大家消化一下这个有很多内容这个其实想想还是挺复杂的还是挺复杂的这些思考还是挺复杂的我也是今天下午花了两个多小时写出来我觉得还是挺复杂的有很多内容可以思考一下然后有很多trade off可以想一下我觉得想通这个问题的话所有的Facebook message然后WeChat这些都是相似的然后你也仔细思考一下我觉得因为我这个design的时候这个问题我前后想过几次吧我很多东西是没有直接写明的比如说availability, reliability我这没有直接写明的没有直接明确的说过几次的然后像这个high performance real time 应该assume 因为这个中间加了很多cash 是吧这个design里面加了很多cash,这个时间是非常快的你像你用sicle的话,sicle mysicle的话,average p50可能就是10毫秒10ms,你用redis更快了可能不到1毫秒是吧不到1毫秒,那你整个end to end flow 其实非常快的非常快的,DenimalDB它其实批评挺多的,DenimalDB相对来说,它那个lendness也比较高一点但是它更scale,DenimalDB是infinite scale,如果你从skill的角度来讲的话,其实这个design是infinite scaleDenimalDB是可以scale到几千个nodes上,就是它本身就是native的支持的,以及prove了这个,它并且如果你看那个很多research paperresource paper 做那个resource的话它skill它那个nodes数量是和它performance是线性的你不会说就是突然就不horizontal skill了它这个Cassandra和这个Late Dynamo skill level是线性的skill非常好是最好的database skill从skill的角度来讲是最好的database了你说吗然后这个amazon S3这个也是可以无线skill你可以不停地往push里面push那个resource嘛然后这个sicle的话你可以vertical sharding和horizontal sharding,你也可以infinite skill,所有这些都可以infinite skill你像这种情况下Redis这个你也可以shard是吧,因为你这个user,你不会你作为一个user,我作为一个scout user,我不会看到David的界面,所以我这个也是可以infinite skill怎么样得到这个tool capacity, latency, maximum concurrency读很多东西比如说我读了这篇文章才知道现在有人能非常疯狂的能支持一个单机能支持4个million 2ps了你读它的时候你要思考一下如果我是个normal request的话那4个million肯定是达不到的我觉得如果作为一个读和写的normal request的话比如说一个正常的有读有写normal request可能要出一个40可能不是400万,可能是个10万左右的QPS,应该是没问题的就是他出一个40万,因为他这块只有读所以你需要日常经验积累然后如果大家有经验的话,比如在Uber的时候有很多dashboard可以读比如说我没事的时看一眼就是看一眼卡夫卡你希望对自己的service这些数字了记于心比如说你design的时候说我要design real time service我要进行卡夫卡别人问为什么要进行卡夫卡那你就可以说如果在卡夫卡如果是single data center的话Lentency就是5毫秒左右是吧P50你看一下dashboard P50就5毫秒左右你如果是单data center就没问题但是我们不是有个cross data center那个Aggregate Kafka cluster其实它那个Lentency我记得也是个P50也就是一百来毫秒还是多少的不是太大但我个人体验过就是那个Cross Data CenterAggregate Kafka cluster有过几分钟的延迟但是可能就是P999那种了所以这个你要读知道吗很多时候你这个Persivate and Product Manager或者Persivate Science Engineer的时候你对这个理解很深的话你就说就抛出一些Data以Data服人如果Kafka在AWS很贵可以用Kinesis, AWS有自己的Kafka, AWS有一个SNS和SQS它好像还出了一个,Kinesis好像是新的,它有个SNS和SQS,我不是太熟 [95:00] 我没有做过这个方面的Sed Project我只是读过一些,对我没有读过这个这个每个人经验时间是有限的嘛只知道有一些那个对用SQL的话会默认他支持Data Partition吗?Data Partition不是NoSQL的Vantage吗?你Data Partition从SQL的角度来讲他不知道什么是Data PartitionData Partition是要你Engineer自己做的Infrastructure Engineer自己做的说白了Data Partition来说对于SQL来说每个partition对于假设我是个MySQL Server我是partition1我不知道有partition2,3,4的存在这个你是不知道的所以你做Data Partition的时候实际上你要做两个东西一个是你要把MySQL就是Native就是你要有个Membership Management这个Membership Management就是哪一个partition在哪一个MySQL哪一个partitionMySQL Server Partition在哪一个Node上这一般是用什么稿呢一般这个是用Rulekeeper稿Rulekeeper一般是做这个Membership Management还有这个Partition这种MetaData这种东西所以当你sharedMetSQL的时候很多时候你要实践一个MetSQL就是你用Rulekembership management还有partition这种metadata这种东西所以当你扫着mySQL的时候很多时候你要实践一个mySQL就是你用rutekeeper来就是加一个mySQL你就可以实践了对于任何一个mySQL来说他是不知道partition的他是不知道有多少partition的但是rutekeeper是知道哪个partition哪个data在哪个partition上哪个partition在哪个node上然后还有包括什么对在哪个node上然后你同时要有个lookup servicelookup service 你可以有多种时间方式但是很多时候你lookup service 要去那个rootkeeper来找见这个membership如果你这个对data partition没有读过的话我非常推荐你读一下uber的schemelessuberschemeless在16年发了三篇paper 写的很不错不是paper 这是uberengineeringblock写的很不错 其实写的挺不错的还是 skimless engineering blocks所以还不错好 我觉得差不多到这里了我现在 stop recording