评论系统,或者称为跟帖、留言板,是所有门户网站的核心标准服务组件之一。与论坛、博客等其他互联网UGC系统相比,评论系统虽然从产品功能角度衡量相对简单,但因为需要能够在突发热点新闻事件时,在没有任何预警和准备的前提下支撑住短短几分钟内上百倍甚至更高的访问量暴涨,而评论系统既无法像静态新闻内容业务那样通过CDN和反向代理等中间缓存手段化解冲击,也不可能在平时储备大量冗余设备应对突发新闻,所以如何在有限的设备资源条件下提升系统的抗压性和伸缩性,也是对一个貌似简单的UGC系统的不小考验。
新闻评论系统的起源新浪网很早就在新闻中提供了评论功能,最开始是使用Perl语言开发的简单脚本,目前能找到的最早具备评论功能的新闻是年4月7日的,经过多次系统升级,年前的评论地址已经失效了,但数据仍保存在数据库中。直到今天,评论仍是国内所有新闻网站的标配功能。
评论系统3.0年左右,我接手负责评论系统,系统版本为3.0。当时的评论系统运行在单机环境,一台x86版本Solaris系统的Dell服务器提供了全部服务,包括MySQL和Apache,以及所有前后台CGI程序,使用C++开发。
图13.0系统流程和架构
3.0系统的缓存模块设计得比较巧妙,以显示页面为单位缓存数据,因为评论页面依照提交时间降序排列,每新增一条评论,所有帖子都需要向下移动一位,所以缓存格式设计为每两页数据一个文件,前后相邻的两个文件有一页数据重复,最新的缓存文件通常情况下不满两页数据。
图2页面缓存算法示意图
图2是假设评论总数95条,每页显示20条时的页面缓存结构,此时用户看到的第一页数据读取自“缓存页4”的95~76,第二页数据读取自“缓存页3”的75~56,以此类推。
这样发帖动作对应的缓存更新可简化为一次文件追加写操作,效率最高。而且可保证任意评论总量和显示顺序下的翻页动作,都可在一个缓存文件中读到所需的全部数据,而不需要跨页读取再合并。缺点是更新评论状态时(如删除),需要清空自被删除帖子开始的所有后续缓存文件。缓存模块采取主动+被动更新模式,发帖为主动,每次发帖后触发一次页面缓存追加写操作。更新评论状态为被动,所涉及缓存页面文件会被清空,直到下一次用户读取页面缓存时再连接数据库完成查询,然后更新页面缓存,以备下次读取。这个针对发帖优化的页面缓存算法继续沿用到了后续版本的评论系统中。
此时的评论系统就已具备了将同一专题事件下所有新闻评论汇总显示的能力,在很长一段时间内这都是新浪评论系统的独有功能。
虽然3.0系统基本满足了当时的产品需求,但毕竟是单机系统,热点新闻时瞬间涌来的大量发帖和读取操作,经常会压垮这台当时已属高配的4U服务器,频繁显示资源耗尽的错误页面。我接手后的首要任务就是尽量在最短时间内最大限度降低系统的宕机频率,通过观察分析确定主要性能瓶颈在数据库层面。
3.0系统中,每个新闻频道的全部评论数据都保存在一张MyISAM表中,部分频道的数据量已经超过百万,在当时已属海量规模,而且只有一个数据库实例,读写竞争非常严重。一旦有评论状态更新,就会导致很多缓存页面失效,瞬间引发大量数据库查询,进一步加剧了读写竞争。当所有CGI进程都阻塞在数据库环节无法退出时,殃及Apache,进而导致系统Load值急剧上升无法响应任何操作,只有重启才能恢复。
解决方案是增加了一台FreeBSD系统的低配服务器用于数据库分流,当时MySQL的版本是3.23,Replication主从同步还未发布,采取的办法是每天给数据表减肥,把超过一周的评论数据搬到2号服务器上,保证主服务器的评论表数据量维持在合理范围,在这样的临时方案下,3.0系统又撑了几个月。
现在看来,在相当简陋的系统架构下,新浪评论系统3.0与中国互联网产业的门户时代一起经历了南海撞机、劫机、非典、孙志刚等新闻事件。
评论系统4.0启动年左右,运行了近三年的3.0系统已无法支撑新浪新闻流量的持续上涨,技术部门启动了4.0计划,核心需求就是三个字:不宕机。
因为当时我还负责了新浪聊天系统的工作,不得不分身应对新旧系统的开发维护和其他项目任务,所以在现有评论系统线上服务不能中断的前提下,制定了数据库结构不变,历史数据全部保留,双系统逐步无缝切换,升级期间新旧系统并存的大方针。
第一阶段:文件系统代替数据库,基于ICE的分布式系统既然3.0系统数据库结构不可变,除了把数据库升级到MySQL4.0启用Repliaction分解读写压力以外,最开始的设计重点是如何把数据库与用户行为隔离开。
解决方案是在MySQL数据库和页面缓存模块之间,新建一个带索引的数据文件层,每条新闻的所有评论都单独保存在一个索引文件和一个数据文件中,期望通过把对数据库单一表文件的读写操作,分解为文件系统上互不干涉可并发执行的读写操作,来提高系统并发处理能力。在新的索引数据模块中,查询评论总数、追加评论、更新评论状态都是针对性优化过的高效率操作。从这时起,MySQL数据库就降到了只提供归档备份和内部管理查询的角色,不再直接承载任何用户更新和查询请求了。
同时引入了数据库更新队列来缓解数据库并发写操作的压力,因为当时消息队列中间件远不如现在百花齐放,自行实现了一个简单的文件方式消息队列模块,逐步应用到4.0系统各个模块间异步通信场合中。
图34.0系统流程
选用了ICE作为RPC组件,用于所有的模块间调用和网络通信,这大概是刚设计4.0系统时唯一没做错的选择,在整个4.0系统项目生命周期,ICE的稳定性和性能表现从未成为过问题。
图44.0索引缓存结构
4.0系统开发语言仍为C++,因为同时选用了MySQL4.0、ICE、Linux系统和新文件系统等多项应用经验不足的新技术,也为后来的系统表现动荡埋下了伏笔(新浪到年左右才逐步从FreeBSD和Solaris迁移到了CentOS系统)。
图54.0系统架构
此时的4.0评论系统已从双机互备扩容到五机集群,进入小范围试用阶段,虽然扛过了刘翔第一次夺金时创纪录的发帖高峰,但倒在了年亚洲杯中国队1:3败于日本队的那个夜晚。
当时系统在进入宕机之前的最高发帖速度大约是每分钟千帖量级,在十年前还算得上是业界同类系统的峰值,最终确认问题出在文件系统的I/O负载上。
设计索引缓存模块时的设想过于理想化,虽然把单一数据表的读写操作分解到了文件系统的多个文件上,但不可避免地带来了对机械磁盘的大量随机读写操作,在CentOS默认的Ext3文件系统上,每条新闻对应两个文件的设计(年新浪新闻总量为千万左右),虽然已采取了×的两层目录HASH来预防单目录下文件过多隐患,但刚上线时还表现良好的系统,稍过几个月后就把文件系统彻底拖垮了。
既然Ext3无法应对大数量文件的频繁随机读写,当时我们还可以选择使用B*树数据结构专为海量文件优化的ReiserFS文件系统,在与系统部同事配合反复对比测试,解决了ReiserFS与特定LinuxKernel版本搭配时的kswapd进程大量消耗CPU资源的问题后,终于选定了可以正常工作的Kernel和ReiserFS对应版本,当然这也埋下了ReiserFS作者杀妻入狱后新装的CentOS服务器找不到可用的ReiserFS安装包这个大隐患。
第二阶段:全系统异步化,索引分页算法优化直到这个阶段,新浪评论系统的前端页面仍是传统的Apache+CGI模式,随着剩余频道的逐步切换,新浪评论系统升级为静态HTML页面使用XMLHTTP组件异步加载XML数据的AJAX模式,当时跨域限制更少的JSON还未流行。升级为当时刚刚开始流行的AJAX模式并不是盲目追新,而是为了实现一个非常重要的目标:缓存被动更新的异步化。
随着消息队列的普遍应用,4.0系统中所有的数据库写操作和缓存主动更新(即后台程序逻辑触发的更新)都异步化了,当时已在实践中证明,系统访问量大幅波动时,模块间异步化通信是解决系统伸缩性和保证系统响应性的唯一途径。但在CGI页面模式下,由用户动作触发的缓存被动更新,只能阻塞在等待状态,直到查询数据和更新缓存完成后才能返回,会导致前端服务器ApacheCGI进程的堆积。
使用AJAX模式异步加载数据,可在几乎不影响用户体验的前提下完成等待和循环重试动作,接收缓存更新请求的支持优先级的消息队列还可合并对同一页面的重复请求,也隔离了用户行为对前端服务器的直接冲击,极大提高了前端服务器的伸缩性和适应能力,甚至连低硬件配置的客户端电脑在AJAX模式加载数据时都明显更顺畅了。前端页面静态化还可将全部数据组装和渲染逻辑,包括分页计算都转移到了客户端浏览器上,充分借用用户端资源,唯一的缺点是对SEO不友好。
通过以上各项措施,此时的4.0系统抗冲击能力已有明显改善,但是接下来出现了新的问题。在3.0系统时代,上万条评论的新闻已属少见,随着业务的增长,类似年超女专题或者体育频道NBA专题这样千万评论数级别的巨无霸留言板开始出现。
为了提高分页操作时定位和读取索引的效率,4.0系统的算法是先通过mmap操作把一个评论的索引文件加载到内存,然后按照评论状态(通过或者删除)和评论时间进行快速排序,筛选出通过状态的帖子并按时间降序排列,这样读取任意一页的索引数据,都是内存中一次常量时间成本的偏移量定位和读取操作。几百条或者几千条评论时,上述方案运作得很好,但在千万留言数量的索引文件上进行全量排序,占用大量内存和CPU资源,严重影响系统性能。我们曾尝试改用BerkeleyDB的Btree模式来存储评论索引,但性能不升反降。
为避免大数据量排序操作的成本,只能改为简单遍历方式,从头开始依次读取,直到获取所需的数据。虽可通过从索引文件的两端分别作为起点,来提升较新和较早页面的定位效率,但遍历读取本身就是一个随着请求页数增大越来越慢的线性算法,并且随着4.0系统滑动翻页功能的上线,原本用户无法轻易访问到的中间页面数据也开始被频繁请求,因此最终改为了两端精确分页,中间模糊分页的方式。模糊分页就是根据评论帖子的通过比例,假设可显示帖子均匀分布,一步跳到估算的索引偏移位置。毕竟在数十万甚至上百万页的评论里,精确计算分页偏移量没有太大实际意义。
图6异步缓存更新流程
年非常受