新浪微博架构演进:高并发微服务与Feed流设计深度解析
做后端这么久我一直觉得新浪微博架构是最值得反复研究的系统之一。它不是一个教科书式的整洁架构而是一部在高并发、高热点、海量数据下被逼出来的演化史。从早期一台数据库撑起千万用户的单体应用到后来颇具代表性的微服务、推拉结合Feed流、多机房容灾体系这里面每一个决策背后都有明确的业务压力和真实代价。今天想从架构演进、微服务拆分、热点流量应对、Feed流设计、多机房容灾这几个角度把新浪微博架构的关键脉络完整梳理一遍。无论你是刚入行的后端工程师还是正在设计高并发系统的架构师这些经验都值得参考。1. 从单体到分布式新浪微博架构演进的主线1.1 早期形态一台数据库撑住千万用户很多人在看微博架构时会忽略它最早期其实非常朴素。我接触过的老资料显示微博起步阶段接近传统论坛用户、关系、微博、评论全部放在MySQL里业务逻辑集中在一个Web应用中。早期用户量可控时这套单库单点的方案确实最简单开发效率最高什么问题都通过加机器、加缓存来解决。但微博这类产品有一个极其特殊的属性内容分发天然不均匀。一个普通用户发一条微博可能只有几十个人看而一个明星发一条微博可以在几秒内带来几十万次读取请求。当这种热点流量出现时数据库连接池会瞬间被打满紧接着整个应用不可用。我第一次看到这类事故复盘时最大的感受是不是数据库不够强而是业务模型决定了流量洪峰无法用买更好的硬件解决。1.2 第一次转折读写分离与服务化面对读压力第一步动作通常是MySQL读写分离。主库负责写入从库负责读应用层根据业务场景选择数据源。这个方案能顶住常规读流量但解决不了热点。因为当一条微博成为热点所有从库都会收到对同一条记录的读请求任何一个从库都扛不住这么密集的重复读。于是自然引出缓存层。新浪微博很早就大规模引入Memcached和Redis把用户信息、关系链、微博内容这些基础数据缓存到内存。但这里同样藏着一个关键选择缓存粒度是什么直接缓存整个页面会让缓存命中率低且更新困难微博的做法是缓存原子数据比如一条微博的正文、一个用户的昵称、一段关注关系然后由应用层组合拼装。这个思想后来被很多团队沿用——缓存永远缓存组件不要缓存页面。1.3 服务化拆分的逻辑当业务模块越来越多团队规模变大单应用部署的痛点就出来了任何一行代码的改动都可能影响整个系统某个模块的流量暴涨会导致其他模块跟着遭殃数据库表互相join一旦数据量大到超出单机容量连查询计划都会变得不可控。微博的第一次服务化拆分实际上是把用户关系微博内容评论计数拆成独立系统。拆分依据很简单找变化率不同、扩展性要求不同的业务域。用户资料读写比例相对稳定关系链数据量大且是图结构Feed流读多写多且带时间线语义评论写多读也多是典型的热点对象。每个业务域独立演进、独立扩容这比在应用层做大一统要清晰得多。2. 微服务拆分微博的领域边界与中间件选型2.1 核心领域的数据特征如果给微博的核心服务画一张简化地图大概有这么几个域业务域核心实体数据特征典型存储方案用户账号用户资料、关注关系读多写少要求低延迟Redis缓存 MySQL分库关系服务关注、粉丝、好友高扇出图结构Redis Set 图存储Feed服务微博内容、时间线海量写入读写不均分布式KV MySQL分库评论服务评论、转发、点赞计数热点集中写多读多消息队列 MySQL计数服务关注数、粉丝数、转发评论数高并发原子自增Redis 异步持久化这张表不是说我见过微博的真实表结构而是这类业务域划分在大型社交平台里具有相当的代表性。每个域的服务独立部署内部有各自的缓存策略和存储方案对外提供RPC接口。2.2 服务通信与注册发现为什么不用HTTP服务化之后紧接着要解决的是服务间怎么通信。微博内部服务之间主要使用Thrift这类二进制RPC框架而不是普遍理解的HTTP接口。原因无非两点。第一是性能。HTTP报文头开销大JSON序列化慢而Thrift使用二进制协议、长连接复用在网络开销上优势明显。微博服务之间的调用量以千亿次计哪怕每次通信节省0.1毫秒总量也很可观。第二是服务治理能力。搭建微服务不只是把接口拆出去还需要超时控制、熔断、限流、负载均衡、链路追踪。主流的RPC框架把这些能力做成标准组件比用HTTP 手写客户端更可靠。服务启动时注册到ZooKeeper消费方通过注册中心感知服务地址变化避免把服务地址写死在配置里。不过HTTP并没有被淘汰。客户端App访问网关时基本还是HTTP因为公网环境需要穿透各种网络设备HTTP更通用而网关内部再通过RPC转发到具体服务。这种对外HTTP、对内RPC的架构是当前大型互联网公司的标准姿势。2.3 Redis、Kafka、MySQL的真实分工很多刚接触高并发架构的同学容易陷入一个误区把所有数据都塞进Redis把Redis当成万能数据库。微博这种体量下每种存储都有自己的边界。Redis解决的是热数据的快速读取和时间线的顺序访问。微博的Feed流天然适合用Redis的List或Sorted Set来存储时间线ID列表关注关系可以用Set来表示。但Redis是内存存储价格昂贵且数据不能无限增长所以它只服务最近的、最热的数据全部数据最终归宿还是MySQL或其他分布式存储。Kafka解决的是异步削峰和数据管道两个问题。当明星发一条微博导致海量评论进入系统时直接同步写数据库必然拖垮数据库。Kafka先把请求暂存起来消费者按照自己能承受的速度慢慢写入数据。这样用户在界面上看到评论发出去了哪怕底层写库有几十毫秒的延迟体验也完全不受影响。MySQL在这套体系里承担的是最终一致性的角色订单类、评论类、关系类的持久化主存储通过分库分表扩展容量。微博的体量一定不会把MySQL当作全量热存储而是让MySQL成为一个可靠落点配合缓存和消息队列共同工作。3. 热点突发流量缓存穿透、消息队列与降级预案3.1 热点Key带来的击穿问题微博业务里最典型的技术难点就是热点数据。试想一个场景某位明星发出一条微博一秒内几百万人刷到这条内容此时如果缓存中这个微博Key恰好过期那么所有请求都会穿透到数据库。数据库瞬间被打爆这个现象叫缓存击穿。解决热点Key击穿有三条常见路线。第一热点数据逻辑上永不过期设置Key时不设物理过期时间而是把过期时间写入Value后台异步更新缓存这样缓存永远存在只是内容会延迟刷新。第二互斥锁重建缓存当缓存未命中时只允许一个线程去数据库加载其他线程等待或返回旧值。第三多副本缓存将同一个热点Key复制到多台缓存节点以避免某台缓存节点故障导致流量集中。我在复盘微博几次大事件时发现单纯用其中任何一种方案都不够通常会叠加使用。比如热点微博本身不设置过期时间但用逻辑过期控制新评论的可见性对于不同规模的大V缓存副本数量也不同。这种资源向热点倾斜的思路非常值得借鉴。3.2 评论与转发的异步写路径写操作在微博的架构里同样是一场攻防战。一条微博发出去之后转发、评论、点赞会像滚雪球一样增长。如果每次转发都同步更新博主和转发者的计数数据库的写压力会成倍叠加。标准的削峰做法是引入消息队列。客户端发出转发请求后网关把请求写入Kafka然后立刻返回转发成功。后台消费者从Kafka拉取数据执行真正的写入逻辑插入转发关系表、更新微博计数、同步粉丝时间线。这个过程天然解耦了响应速度和落库速度。这里有一个细节值得注意消息队列不是只做缓冲它还要承担最终一致性的协调器角色。比如一个转发动作涉及更新Feed、更新计数、更新粉丝动态通过Kafka的Topic分区可以把这些操作串行化从而保证一个用户一次转发产生的多个数据变更不会出现语义错乱。3.3 缓存一致性最终的取舍缓存加上异步后缓存与数据库的一致性就成了绕不开的问题。微博这类读多写少、对一致性要求没那么苛刻的场景最常用的是Cache Aside模式读缓存未命中则读数据库并回填更新时先更新数据库再删除缓存。但并发场景下先删缓存再更新数据库和先更新数据库再删缓存都有各自的坑。实践中常用的一种补偿是延时双删更新完数据库后删除缓存等待几百毫秒再删除一次。为什么还要再删一次因为在你第一次删除缓存之后可能恰好有一个并发请求把旧数据重新写入缓存延迟删除可以把这个残留的旧值清掉。更彻底的做法是订阅MySQL的binlog将变更事件发送到消息队列消费者拿到事件后再删除对应缓存。这种方式把缓存更新变成异步事件不会影响主链路性能也比较好控制顺序。微博在评论计数这类强一致要求场景下甚至不直接读缓存而是通过RPC获取一个近似值。这就是大型系统里常见的一致性分级策略——不同业务允许不同级别的一致性。4. Feed流架构推拉结合模型的精髓4.1 为什么全推或全拉都不现实微博最核心的是Feed流也就是用户打开App看到按时间排序的微博列表。设计Feed流的两个极端方案是Fanout-on-Write全推和Fanout-on-Read全拉。全推的意思是博主发微博时系统把这条微博写入所有粉丝的收件箱。这个方案的好处是读取时直接查询自己的收件箱延迟低逻辑简单。坏处也很明显一个大V有几千万粉丝推送一次要写几千万条数据这不仅是存储成本问题更是严重的写放大。全拉的意思是博主发微博时只写入博主自己的发件箱粉丝读取时再查询自己关注的所有人把他们的微博合并起来。这个方案写成本低但读成本高。普通用户关注几百人每次刷新都要查几百个列表再合并延迟会高到不可接受。所以微博采取的是推拉结合核心判据是粉丝数量级。4.2 普通用户与大V的差异化处理推拉结合的模型可以简化成两套流程。对于普通用户粉丝数很少发微博使用推送模型。发微博时直接将微博ID写入其所有粉丝的Timeline列表。因为普通用户粉丝少推送成本低粉丝读取时间线时几乎不需要额外合并。对于大V用户粉丝数很多发微博使用拉取模型。大V发微博时只把微博写入自己的发件箱不推送。粉丝刷新时先读自己的Timeline已经合入了普通用户的最新微博再读所有关注大V的最近微博在内存里做一次归并排序返回前N条。这套方案的关键在于中间那条同时向推拉模型切换的阈值。阈值以下的用户减少读取时的合并成本阈值以上的用户减少写入时的扇出压力。微博这类平台在选择这个阈值时会参考平均粉丝数、服务器成本、用户体验目标并没有绝对正确的标准只有最贴合自身业务曲线的参数。4.3 时间线存储与分页的工程细节Feed流的底层存储很长一段时间里都离不开Redis。每个用户的Timeline对应Redis中的一个List或Sorted Set里面存的是微博ID。发微博时LPSH或ZADD一条ID刷新时LRANGE或ZREVRANGE取若干条。用Redis存时间线的另一个原因是天然支持分页而且可以直接返回ID列表然后通过Feed服务批量查询微博完整内容。这样做的好处是Feed内容本身可能被多次复用比如首页信息流、个人主页、搜索页都会用到同一条微博通过ID引用可以避免每条信息流存一份完整内容极大节省内存。当然Redis不可能存储所有用户从注册到现在的全部时间线数据。随着用户时间线越来越长通常的策略是只保留最近N条或最近N天更早的数据走冷存储。当用户向上翻页翻到时间线边缘系统会从冷存储集群中补数据。这里又会涉及一次热数据未命中、冷数据读取的路径设计一旦处理不好翻页就会出现转圈。4.4 大V发件的时效性与合并优化大V场景下粉丝读取时需要去每个关注大V的发件箱拉取最近微博如果一个人关注了10个大V一次刷新要发起10次RPC。这个数量级可以接受但微博的用户普遍关注较多账号如果不做优化读放大同样可观。常见的优化是在本地加一层大V微博聚合缓存每隔几十秒拉取所有关注的大V新微博ID合并写入一个临时列表用户刷新时只读这个聚合列表。相当于把实时拉取变成准实时拉取用户几乎感知不到差别但服务端压力会大幅下降。这个思路本质上是对读放大的又一次折中以大V微博的平均时效性换取读取性能。微博是否会为超头部大V单独建设定时同步任务我无法确认但这种按账号重要程度分级的思路绝对是工程上的常规做法。5. 多机房容灾微博的高可用体系5.1 从同城双活到异地多活微博的系统体量决定了它不可能只部署在一个机房。早期是同城双机房目的主要是容灾一个机房出故障另一个机房能接管流量。同城双活相对简单因为机房之间的网络延迟很低数据同步压力小。但同城双活解决不了区域级故障所以演进方向是异地多活。异地多活的核心思路不是把整个系统复制一份到异地而是将用户分片。按用户ID划分属于A机房的用户其读写都在A机房完成B机房负责另一部分用户。A机房故障时通过路由规则的变更把A机房的用户临时切到B机房。这个设计看起来清晰实际最难的是数据同步。一个用户的数据可能分散在MySQL、Redis、Kafka中机房之间需要异步同步所有数据变更。同步有延迟切换后用户可能读到旧数据但我们接受短暂不一致只要最终能追上。5.2 分片、路由与数据同步多机房架构下的数据分片通常和用户维度绑定。用户表、关注关系、Feed元数据都可以按用户ID做哈希分片。每个分片有一个主机房负责读写其他机房保留备份或冷数据。这里容易踩的坑是跨机房调用。如果用户主机房在北京但TA访问了上海的接入点请求会被上海网关转发到北京机房处理。这个转发路径上班能行但网络RT会明显增长。因此大型系统通常会做就近接入 数据中心路由尽量让用户流量接入离TA最近且能处理其数据的机房。MySQL跨机房同步常见方案是主从复制或基于binlog的消息管道。Redis跨机房同步则更复杂常用手段是自研同步组件或者通过中间件进行增量复制。跨机房数据同步的延迟哪怕只有几十毫秒在写路径上也会造成用户体验下降所以设计原则是让写操作永远路由到主机房从机房只承接查不到强一致的读请求。5.3 故障切换与降级方案多机房架构不是部署完就高枕无忧。真正考验架构的是故障发生时的应对预案。流量调度层面DNS和HTTPDNS配合负载均衡在机房故障时可以把域名流量切走。但是光靠DNS不够TTL缓存会导致切换有延迟。所以在服务端还要有RPC路由级别的故障剔除服务消费者每秒探测数据中心的健康状态一旦检测到故障立刻调整路由权重。降级方案的核心是区分核心链路和非核心链路。微博的核心链路是刷Feed和发微博其它像热搜榜单、商品广告、视频推荐都可以降级。大流量冲击时系统会强制关闭非核心功能把资源预留给核心链路。这就是平时所说的保命设计。熔断和限流是最后一道防线。当依赖的下游服务超时率飙升熔断器会直接在调用端返回错误避免请求继续堆积限流则针对单一用户或单一接口比如同一用户在几秒内的刷新次数超过阈值就拒绝部分请求。在微博的大事件期间这些保护策略几乎是默认开启的。6. 资深架构师的几条实在经验6.1 架构是被流量逼出来的别为了架构而架构回顾新浪微博架构的演进没有哪个时期的设计是拍脑袋想出来的。单体应用扛不住了才做拆分缓存扛不住了才引入消息队列单机房不够用了才做多活。对中小团队来说最大的参考价值不是照搬它的微服务清单而是理解每个阶段的问题和约束。在用户量没有到那个量级时套用大规模架构只会增加成本和复杂度。6.2 缓存失效是万恶之源我自己的经验是高并发系统90%的事故都和缓存有关。缓存穿透、缓存击穿、缓存雪崩是三个被反复提起又反复踩的坑。穿透是指查一个根本不存在的数据缓存里没有、数据库里也没有请求每次都打到数据库。处理方式很简单对空值也做缓存短期缓存一个空结果。击穿上文提过可以用逻辑过期和互斥锁。雪崩则是大量Key在同一时间过期导致数据库压力瞬间上升解决办法是给过期时间加一个随机偏移量。这三板斧不是理论是真能救命的。6.3 可观测性必须前置只要有几十个微服务没有全链路监控就像进了没有照明的地下室事故排查全靠猜。微博这种体量下必须有统一的TraceId贯穿请求从网关到RPC到缓存再到数据库完整记录一次请求经过的所有节点。当面试题里常考的分布式追踪在这个场景下根本不是加分项而是保命项。商业产品或开源工具如SkyWalking、Zipkin都可以用关键是接入要早数据要全。6.4 没有压测和预案就没有高可用每次有大型活动或热点事件之前架构团队最忙的一段时间一定是在做容量评估和压测。压测不能只测单接口要模拟真实用户行为比如加载首页、下拉刷新、查看评论、发微博、转发。只测单接口会造成压测全绿一上线就挂的错觉因为真实流量是复杂的混合场景且读与写会互相影响。同时要把故障场景做成预案并且定期演练。很多团队做预案只是写文档真正出了事故才发现文档里的命令已经过时了。像机房断电、数据库主从切换、缓存集群故障、Kafka积压这些场景都应该在测试环境演练过甚至要在生产环境做灰度故障演练。微博架构走到今天正是无数这类经验堆出来的结果。