分布式系统演进简述

“网络与分布式计算”课程布置了一个作业,要求写一篇题目为“构建分布式系统技术发展历史”的小论文。我个人以为“历史”二字应该是要我们去了解每项技术诞生的时间点,然而踏破铁鞋无处可觅。最后写成的文章主题,我个人认为比起“历史”来说,“演进过程”可能更为恰当。

事实上我们可能也更为注重演进过程,因为分布式系统的演进过程,就是一代代工程师们对抗随时代和应用自身发展而逐渐升高的访问量的一部血泪史。这个过程是每一个健康发展的网站都要经历的。

这次作业也给了我一个机会,去大致了解网站在发展过程中为了提升性能、简化部署、弹性扩展而做出的种种措施,受益匪浅。因此将文章放到博客上。

单体应用架构

在应用诞生初始,用户量、数据量规模都比较小。因此应用程序、数据库、文件等所有的资源都部署在同一台服务器上即可。这样的架构既简单实用、便于维护,成本又低,成为了这个时代的主流架构方式。在 2003 年淘宝最初上线的时候,就是以一套当时十分风靡的 LAMP 架构(Linux+Apache+MySQL+PHP)实现并部署的。

单体应用架构的特点就是功能和代码集中、一个发布包部署后运行在一个进程内的应用程序。

垂直应用架构

随着应用的发展,用户访问量逐渐增加,单台服务器性能及存储空间不足,因此服务器的压力在访问高峰期会上升到比较高,响应时间逐渐变长。这时,工程师们考虑增加几台服务器,将应用程序、数据库、文件分别部署在独立的资源上。一个大的单体应用被拆分成若干个小的单体应用,这就是垂直应用架构。这种拆分方法内含分治的思想,即将一个大的问题按一定业务规则分成若干个小的问题,逐个解决。

通过采用垂直应用架构,应用服务和数据服务得以分离,并发处理能力和数据存储空间得到了很大改善。

使用缓存改善性能

将大的单体应用拆分后,对业务有所了解和调查的工程师们发现,系统访问的特点遵循二八定律,即 80% 的业务访问集中在 20% 的数据上。那么,使用缓存技术将这 20% 访问较集中的数据缓存下来,就有助于减少数据库的访问次数,降低数据库的访问压力。同时,引入缓存也有助于降低存储成本,因为缓存+数据库服务器可以承担原本需要多台数据库服务器才能承担的请求量,因此节省了机器成本。

缓存分为本地缓存(Local Cache)和远程分布式缓存(Remote Distributed Cache)。本地缓存访问速度更快,但内存资源以及承载能力有限,存在与应用程序争用内存的情况。因此,分布式缓存在如今的大型网站中得到广泛的使用。

使用应用服务器集群 & 负载均衡

虽然做了这么多工作,但是请求量还是水涨船高。当请求量达到需要排队等待的规模时,即使单台应用服务器性能强大,处理请求速度很快,响应速度也依然会变慢。这时,工程师们就要考虑使用多台服务器所组成的集群,通过向集群中追加资源,提升系统的并发处理能力,解决单台服务器处理能力和存储空间上限的问题。

在数据中心内部,外部请求首先被定向到负载均衡器,以主机当前的负载作为函数来在主机之间负载均衡。其作为中继站向外部提供服务,使得服务器的负载压力不再成为整个系统的瓶颈。负载均衡器同时也进行外部 IP 地址和内部适当主机 IP 地址的互相转换,防止客户直接接触主机,从而具有隐藏网络内部结构和防止客户直接与主机交互等安全性益处。

负载均衡的策略包括:

  • 轮询:每一次来自网络的请求轮流分配给内部中的服务器,从 1 至 N 然后重新开始。此种均衡算法适合于服务器组中的所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。实现简单,但存在服务器的处理能力不同的情况;

  • 权重:根据服务器的不同处理能力,给每个服务器分配不同的权值,使其能够接受相应权值数的服务请求。此种均衡算法能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。考虑了服务器处理能力的不足;

  • 随机均衡:把来自网络的请求随机分配给内部中的多个服务器。

反向代理也是负载均衡的一种方法。反向代理指隐藏了真实的服务端,帮用户把请求转发到真实的服务器那里去。Nginx 就是性能非常好的反向代理服务器,可以用来做负载均衡。它会将请求在读取完整之前缓冲,这样交给后端的就是一个完整的 HTTP 请求,而不是断断续续的传递(互联网上连接速度一般比较慢),以此减少网络 IO 次数,从而提高后端的效率。

数据库读写分离

在一系列架构的增强后,用户量增加所带来的系统瓶颈可能转移到数据库上。每次读和写的操作都要经过同一个数据库服务器,在大量用户访问的情况下,资源竞争激烈。数据库的并发量有限,在保证数据库事务的 ACID 特性(原子性、一致性、隔离性和持久性)时难免发生阻塞。

为了保证数据库的高可用性,工程师们将数据库进行读写分离,写操作全部引入主库,读操作引入从库。这种做法极大程度地缓解了数据库锁之间的竞争,提高了并发吞吐量和负载能力。当然,读写分离架构适用的应用中,由于读库需要不断做批量的更新操作,读操作不应要求数据强一致,即对数据的实时性要求并没有那么高。

CDN 加速和反向代理

鉴于网站的访问速度基于页面包含的内容传输到用户电脑的速度,如果用户到服务器的链路之间有一段比较缓慢的话,整体访问速度会受到很大的影响。因此,让用户从就近的服务器获取网页内容是一个理想的选择。

对于业务加载需要的一些不经常改变的静态数据(一般是 css 和 js 文件),可以考虑将其缓存到 CDN 服务器上,而不是每次都到应用服务器进行获取。CDN(内容分发网络)是运营商提供的服务,服务器部署在机房,可以根据用户访问的链路直接选择最近的 CDN 服务器,将已缓存的静态数据返回,以加快静态数据加载速度。这样可以减少服务器的压力,并解决不同地区访问速度差距较大的问题。

分布式数据库

就在工程师们忙碌的时候,数据量仍然随着业务的发展在悄无声息地增长。数据库读写分离最终也将无法满足业务需求,因此需要将数据库再次拆分。比较常用的数据库拆分手段是业务垂直分库,因为不同业务中表和表之间的数据大多没有关联,因此可以将不同的业务数据库部署在不同的物理服务器上。

如果单表数据规模非常庞大,还需要使用分布式数据库来支撑,即对这些表进行水平拆分,将同一个表中的数据拆分到两个甚至多个数据库中。

使用 NoSQL 和搜索引擎

随着业务越来越复杂,对数据存储和检索的需求也越来越复杂,系统需要采用一些非关系型数据库如 NoSQL 和分数据库查询技术如搜索引擎。应用服务器通过统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。

业务拆分

为了应对日益复杂的业务场景,通常使用分而治之的手段将整个系统业务分成不同的产品线,应用之间通过超链接建立关系,也可以通过消息队列进行数据分发,当然更多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。

业务拆分包括纵向拆分和横向拆分:

  • 纵向拆分:将一个大应用拆分为多个小应用,如果新业务较为独立,那么就直接将其设计部署为一个独立的 Web 应用系统。纵向拆分相对较为简单,通过梳理业务,将较少相关的业务剥离即可。

  • 横向拆分:将复用的业务拆分出来,独立部署为分布式服务,新增业务只需要调用这些分布式服务。横向拆分需要识别可复用的业务,设计服务接口,规范服务依赖关系。

其他问题:异步、冗余、自动化

在漫长的演变过程中,除开以上,分布式系统的构建还有一些值得考虑的问题:

  • 异步:将一个复杂的业务操作转换为多个阶段操作,每个阶段的操作通过消息队列进行异步处理。可以理解为生产者-消费者模式,好处是加快系统响应速度和消除并发高峰。Node.js 正是因为其为异步而生,才逐渐成为服务端开发的选择之一。

  • 冗余:为抵抗一些不可控因素,如自然灾害(火灾、地震)等因素导致系统不可用,应考虑异地建立容灾中心,实时备份数据。

  • 自动化:自动化包括监控、告警、失效转移恢复、降级等功能,这些功能让系统在无人值守的情况下仍可以正常运行。

结语

每个小节看似容易理解,真正想要实现起来都是学问,更别谈考虑每个细节,选择最好的方式。之前看《淘宝技术这十年》,里面谈到淘宝作为中国互联网前几个称得上是超大规模的互联网系统,其发展历程中遇到了很多前人根本没有遇到过的问题。淘宝的一代代工程师们或主动或被动的开始进行一次次技术变革,这才造就了如今双十一每秒巨额的并发量。也因此,大麦、12306 等短时间要接受大量访问的网站时常瘫痪,也情有可原。架构是一门学问,想真正将每个环节都研究透彻,不是简简单单看几篇文章,做一个总结就够了的,需要和业务紧密结合。

参考资料