你好,我是李智慧。
百科知识应用网站是互联网应用中一个重要的类别。很多人上网是为了获取知识,而互联网上的信息良莠并存,相对说来,百科知识应用网站能为普通人提供较为可信的信息。因此,百科知识网站虽然功能单一、设计简单,但是依然撑起了互联网的一片天空:维基百科是全球访问量TOP10的网站,百度百科是百度的核心产品之一。
我们准备开发一个供全球用户使用的百科知识应用系统,系统名称为“Wepedia”。
Wepedia的功能比较简单,只有编辑词条和搜索查看词条这两个核心功能。但是Wepedia的设计目标是支撑每日10亿次以上的访问压力。因此设计目标主要是简单、高效地支持高并发访问,以及面对全球用户时保证$\small 7\times24$小时高可用。
概要设计
Wepedia的整体架构,也就是简化的部署模型如图。

在梳理Wepedia整体逻辑之前,先说明下架构图中核心组件的作用。

用户在Web端查看一个百科词条的时候,首先通过GeoDNS进行域名解析,得到离用户最近的数据中心所属的CDN服务器的IP地址。用户浏览器根据这个IP地址访问CDN服务器,如果CDN服务器上缓存有用户访问的词条内容,就直接返回给用户;如果没有,CDN会访问和自己在同一个区域的Wepedia的数据中心服务器。
准确地说,CDN访问的是Wepedia数据中心负载均衡服务器LVS的IP地址。请求到达LVS后,LVS会将该请求分发到某个Nginx服务器上。Nginx收到请求后,也查找自己服务器上是否有对应的词条内容,如果没有,就将请求发送给第二级LVS负载均衡服务器。
接着,第二级LVS将请求分发给某个Apache服务器,Apache会调用PHP程序处理该请求。PHP程序访问Redis服务器集群,确认是否有该词条的对象。如果有,就将该对象封装成HTML响应内容,返回给用户;如果没有,就访问MySQL数据库来查找该词条的数据内容。PHP程序一方面会将MySQL返回的数据构造成对象,然后封装成HTML返回用户,一方面会将该对象缓存到Redis。
如果用户的HTTP请求是一个图片,那么Nginx则会访问lighttpd服务器,获取图片内容。
因为Nginx缓存着词条内容,那么当词条编辑者修改了词条内容时,Nginx缓存的词条内容就会成为脏数据。解决这个问题通常有两种方案,一种是设置失效时间,到了失效时间,缓存内容自动失效,Nginx重新从Apache获取最新的内容。但是这种方案并不适合Wepedia的场景,因为词条内容不会经常被编辑,频繁失效没有意义,只是增加了系统负载压力;而且,在失效时间到期前,依然有脏数据的问题。
Wepedia为了解决Nginx缓存失效的问题,采用了另一种解决方案:失效通知。词条编辑者修改词条后,Invalidation notification模块就会通知所有Nginx服务器,该词条内容失效,进而从缓存中删除它。这样,当用户访问的时候,就不会得到脏数据了。
多数据中心架构
Wepedia在全球部署多个数据中心,可以就近为用户提供服务。因为即使是最快的光纤网络,从地球一端访问另一端的数据中心,在通信链路上的延迟就需要近150ms。
$\small (地球周长4万KM\div2)\div光速30万KM/s\times请求响应2次通信\approx133ms$
150ms是一个人类能够明显感知的卡顿时间。再加上服务器的处理时间,用户的响应等待时间可能会超过1秒钟,而页面加载时间超过1秒钟,用户就会明显不耐烦。多数据中心架构可以通过GeoDNS为用户选择最近的数据中心服务器,减少网络通信延迟,提升用户体验。
另一方面,多数据中心还具有容灾备份功能,如果因为天灾或者人祸导致某个数据中心机房不可用,那么用户还可以访问其他数据中心,保证Wepedia是可用的。
但是多数据中心需要解决数据一致性的问题:如果词条编辑者修改词条内容,只记录在距离自己最近的数据中心,那么这份数据就会和其他数据中心的不一致。所以,Wepedia需要在多个数据中心之间进行数据同步,用户不管访问哪个数据中心,看到的词条内容都应该是一样的。
Wepedia的多数据中心架构如图。

Wepedia的多数据中心架构为一主多从架构,即一个主数据中心,多个从数据中心。如果用户请求是Get请求(读请求),那么请求就会在该数据中心处理。如果请求是Post请求(写请求),那么请求到达Nginx的时候,Nginx会判断自己是否为主数据中心,如果是,就直接在该数据中心处理请求;如果不是,Nginx会将该Post请求转发给主数据中心。
通过这种方式,主数据中心根据Post请求更新数据库后,再通过Canal组件将更新同步给其他所有从数据中心的MySQL,从而使所有数据中心的数据保持一致。同样,lighttpd中的图片数据也进行同步,开发lighttpd插件,将收到的图片,发送给所有从数据中心。
数据中心之间采用类似ZooKeeper的选主策略进行通信,如果主数据中心不可用,其他数据中心会重新选举一个主数据中心。而如果某个从数据中心失火了,用户请求域名解析到其他数据中心即可。
这种多数据中心架构虽然使词条编辑操作的时间变长,但是由于Wepedia的绝大多数请求都是Get请求(Get与Post请求比超过1000:1),因此对系统的整体影响并不很大。同时用一种简单、廉价的方式实现多数据中心的数据一致性,开发和运维成本都比较低。
详细设计
作为一个百科服务类网站,Wepedia 主要面临的挑战是:应对来自全球各地的巨量并发的词条查询请求。因此详细设计重点关注Wepedia的性能优化。
前端性能优化
前端是指应用服务器(也就是 PHP 服务器)之前的部分,包括 DNS 服务、 CDN 服务、反向代理服务、静态资源服务等。对 Wepedia 而言,80% 以上的用户请求可以通过前端服务返回,请求根本不会到达应用服务器,这也就使得网站最复杂、最有挑战的PHP应用服务端和存储端压力骤减。
Wepedia 前端架构的核心是反向代理服务器 Nginx 集群,大约需要部署数十台服务器。请求通过 LVS 负载均衡地分发到每台 Nginx 服务器,热点词条被缓存在这里,大量请求可直接返回响应,减轻应用负载压力。而Nginx 缓存 不能命中的请求,会再通过 LVS 发送到 Apache 应用服务器集群。
在反向代理 Nginx 之前,是 CDN 服务,它对于 Wepedia 性能优化功不可没。因为用户查询的词条大部分集中在比重很小的热点词条上,这些词条内容页面缓存在 CDN 服务器上,而 CDN 服务器又部署在离用户浏览器最近的地方,用户请求直接从 CDN 返回,响应速度非常快,这些请求甚至根本不会到达 Wepedia 数据中心的 Nginx 服务器,服务器压力减小,节省的资源可以更快地处理其他未被 CDN 缓存的请求。
Wepedia CDN 缓存的几条准则:
- 内容页面不包含动态信息,以免页面内容缓存很快失效或者包含过时信息。
- 每个内容页面有唯一的 REST 风格的 URL,以便 CDN 快速查找并避免重复缓存。
- 在 HTML 响应头写入缓存控制信息,通过应用控制内容是否缓存及缓存有效期等。
服务端性能优化
服务端主要是 PHP 服务器,这里是业务逻辑的核心部分,运行的模块都比较复杂笨重,需要消耗较多的资源,Wepedia 需要将最好的服务器部署在这里(和数据库配置一样的服务器),从硬件上改善性能。
除了硬件改善,Wepedia 还需要使用其他开源组件对应用层进行优化:
- 使用 APC,这是一个 PHP 字节码缓存模块,可以加速代码执行,减少资源消耗。
- 使用 Tex 进行文本格式化,特别是将科学公式内容转换成图片格式。
- 替换 PHP 的字符串查找函数 strtr(),使用更优化的算法重构。
存储端性能优化
包括缓存、存储、数据库等被应用服务器依赖的服务都可以归类为存储端服务。存储端服务通常是一些有状态的服务,即需要进行数据存储。这些服务大多建立在网络通信和磁盘操作基础上,是性能的瓶颈,也是性能优化的关键环节。
存储端优化最主要的手段是使用缓存,将热点数据缓存在分布式缓存系统的内存中,加速应用服务器的数据读操作速度,减轻存储和数据库服务器的负载。
Wepedia 的缓存使用策略如下:
- 热点特别集中的数据直接缓存到应用服务器的本地内存中,因为要占用应用服务器的内存且每台服务器都需要重复缓存这些数据,因此这些数据量很小,但是读取频率极高。
- 缓存数据的内容尽量是应用服务器可以直接使用的格式,比如 HTML 格式,以减少应用服务器从缓存中获取数据后解析构造数据的代价。
- 使用缓存服务器存储 session 对象。
作为存储核心数据资产的 MySQL 数据库,需要做如下优化:
- 使用较大的服务器内存。在 Wepedia 应用场景中,增加内存比增加其他资源更能改善 MySQL 性能。
- 使用 RAID5 磁盘阵列以加速磁盘访问。
- 使用MySQL 主主复制及主从复制,保证数据库写入高可用,并将读负载分散在多台服务器。
小结
高可用架构中的各种策略,基本上都是针对一个数据中心内的系统架构、针对服务器级别的软硬件故障而进行设计的。但如果整个数据中心都不可用,比如数据中心所在城市遭遇了地震,机房遭遇了火灾或者停电,不管我们架构的设计多么的高可用,应用依然是不可用的。
为了解决这个问题,同时也为了提高系统的处理能力、改善用户体验,很多大型互联网应用都采用了异地多活的多机房架构策略,也就是说将数据中心分布在多个不同地点的机房里,这些机房都可以对外提供服务。用户可以连接任何一个机房进行访问,这样每个机房都可以提供完整的系统服务,即使某一个机房不可使用,系统也不会宕机,依然保持可用。
思考题
词条编辑者修改词条的时候,可能会同时修改(新增)词条文本和图片。而数据从主数据中心同步到多个从数据中心的时候,数据库同步可能和图片同步时间不一致,导致用户查看词条的时候,图片无法加载或者图片和文本内容不一致。
如何解决这个问题?
附1:阿里巴巴在十几年前,也遇到数据和图片同步不一致的问题,后来解决这个问题的开发工程师晋升为阿里集团副总裁,欢迎有志于成为副总裁的同学思考下这个问题。
附2:阿里当年遇到并解决这个问题的系统:https://github.com/alibaba/otter
附3:阿里当年解决这个问题的工程师访谈:https://www.infoq.cn/article/pl-alibaba
欢迎在评论区分享你的思考,我们共同进步。
精选留言
2022-04-02 08:15:17
2022-08-24 15:05:33
2022-05-04 01:34:37
1、每次上传图片时,都生成一个唯一文件UUID,词条中仅记录图片的唯一UUID。
2、在服务器返回词条时,先通过文件UUID判断是否完成同步,如果完成同步拼接本地服务地址,如果没有完成同步拼接主站点地址。
3、服务器还可以直接缓存网页,对于已经同步全部图片的,设置较长超时时间。对于没有完成全部图片同步的,设置较短超时时间。
4、刷新页面缓存时,判断MD5码,如果页面变动,主动通知CDN等,原页面失效;或直接推送新版本。
5、缓存页面时,可以前1%热点话题放内存,前5%话题放到SSD
6、 Tex公式转图片,可以用类似逻辑
2024-10-01 23:51:58
2023-06-11 12:48:35
对于刚刚上传的图片, 算一个 Hash 值并一起同步到从数据中心的MySQL (其表中要先定义对应的 Hash 值这一列). 后续的读请求来到从数据中心的时候, 先尝试检查 MySQL 中的数据有没有这个 Hash 值, 如果有, 则尝试计算从数据中心中的对应图片的 Hash 值, 看两个 Hash 值是否相等.
如果相等, 说明图片的同步完成了. 这个时候可以从 MySQL 中删除这个 Hash 值了, 以后的读请求再来到从数据中心的时候, 由于没有这个 Hash 值, 则知道图片和数据的同步性检验已经通过了, 则跳过Hash比对阶段, 正常返回图片和数据.
如果两个 Hash 值不相等, 则让从数据中心的 nginx 转发这个 GET 请求给主数据中心, 将返回的页面再转交给客户端.
2023-05-24 08:58:26
一个在从数据中心的写请求后立即请求读。这时候主数据中心的数据还未同步到从中心,读请求读到的数据不是最新的。这种读不一致的问题不需要解决吗
2022-05-24 22:37:15
2022-05-04 17:36:00
在多个数据中心之间,保持数据库的双A同步时,otter的单项回环补救机制,确实可以保证双中心数据最终是一致的。
但在一些极端情况下,业务上要如何处理的呢。比如:
货物A,双地库存一致都为200,同一时刻杭州卖了150件,US库也卖了150件,扣库存都可以成功,但最终超卖了。这种情况,业务上要如何处理的呢。
感谢!
2022-05-01 11:58:54
2022-04-21 23:21:11
2022-04-21 08:41:43
2022-04-05 17:40:38
2022-04-02 08:13:16
2022-04-02 08:11:58
2022-03-30 20:58:53
词条保存时,图片 URI 可以使用类似 “/词条ID/图片文件名”,图片文件名由图片内容的 hash 码组成,这样当词条数据同步到不同的数据中心,即使图片资源文件还没有同步,也不会影响用户访问。因为加载词条时访问的是 CDN ,如果 CDN 没有图片资源文件会回源。(回源需要设置到不同的数据中心)
2022-03-27 03:14:16
2022-03-25 23:45:51
2022-03-21 20:41:37
2022-03-21 18:52:36
2022-03-21 18:45:38
保障数据一致性的解决方案。
所有的节点之间有一个选主的流程。非主节点只允许读操作。并且,每一条数据都会有一个版本号,主节点修改或者创建数据时,会同步通知非主节点,当所有的非主节点都告知主节点数据更新成功时,才会返回这条最新的数据。
但是,因为其数据强一致性的保障,牺牲了很多性能。这种方式是否合理也是一个问题。