大型系统的演进(下)
回顾一下上篇的系统架构:
以一个购物网站来说,这样的架构已经撑得起一定流量了。但仍有几个常见的议题,想要在本篇进一步讨论。分别是:静态资源的扩展性、异步任务的处理、全文检索的性能,以及非常重要的——安全性!
静态资源的扩展性
静态资源指的是像图片、影片这类型的文件。它们通常变动的频率相对较低、需要更多传输流量,以及多位使用者会下载同一份文件。
举例来说,有位商家上架了一个热门商品,并在商品介绍中加入了一堆宣传照片。假设这个商品有上万名使用者浏览,若每个人都来服务器下载这些照片。可以想像,对系统将是一个非常可观的负担!
还记得先前介绍数据库的扩展性时,我们引入了 缓存来降低重复查询数据库的成本。这边也是类似的做法,只要有使用者请求过某张图片,我们就加到缓存,让其它使用者可以重复利用而不用再从服务器下载。
静态资源的缓存,已经有许多供应商提供现成的服务,称为内容分发网络(CDN)。收费上通常都是用多少付多少,并且保证了非常高的可用性!
内容分发网络(Content Delivery Network, CDN)
内容分发网络(Content Delivery Network,简称CDN)是由大量分布在全球范围内的“缓存节点”构建而成的一种分布式网络系统。根据用户请求的发起地,该系统会选择地理位置最近的缓存服务器来向用户提供所需的内容。若请求的资源尚未存储在缓存中,则CDN会连接到原始服务器获取该资源,并将其保存至CDN自身的缓存中,以便后续请求快速响应。
将静态内容分离,并使用CDN来负责传输后,我们的架构如下:
使用者首先通过我们的服务器,上传资源到特定的储存空间(常常也会采用云端服务,例如 AWS S3 )。之后当有人需要下载这个资源,一律通过CDN而不是我们的服务器。在接到下载请求时,若有缓存过这个资源就直接返回对应资源,否则CDN便会从我们的储存空间下载给使用者并且缓存。
异步任务
何谓同步(Synchronous)、异步(Asynchronous)任务?简单来说,同步任务就是任务开始后我们 “要等” ,直到任务完成后才能离开。而 异步任务 则是我们 “不用等” ,期间我们可以先去做别的事,等任务完成后再进行后续处理。
比如餐厅取餐的例子中,同步方式就像是没有点餐台的餐厅,顾客需直接和厨师交流点餐,并在料理制作期间一直等待。这种情况下,耗时长的菜品会导致客人等待时间过长,高峰期时甚至可能导致无法正常点餐。
而异步模式就如同设有专门点餐台的餐厅,顾客提交订单后即可离开,订单会在点餐台排队,厨师按顺序准备菜品,完成后通知顾客领取。
可以想像,比较耗时的订单就很适合用异步的方式处理。即使在点餐的高峰期时刻,也不会发生有客人没办法点餐的情况。厨房还可以偷偷安排更多的厨师维持来出餐速度而不会影响到客人的用餐体验。
这些角色在系统设计中的名称是:
- 订单信息被称为消息(Message),描述工作内容;
- 提交订单的顾客相当于生产者(Producer),负责创建消息;
- 点餐台,即消息排队的地方,称为消息队列(Message Queue),采用先进先出(FIFO)的数据结构;
- 厨师则代表消费者(Consumer),负责处理队列中的消息。
消息队列(Message Queue)
一种异步的架构,生产者根据任务内容生成消息,再放到消息队列中排队,由消费者处理队列中的消息。
在实际应用中,生产者通常是应用程序服务器,它们会将复杂的或者计算密集型的任务“委托”给专门的工作者(worker)服务器处理。这些工作者服务器作为消费者的角色,其主要职责就是从消息队列中取出并执行那些待处理的任务。
采用这种架构的好处有:
使用消息队列实现异步处理的优点主要包括以下几点:
- 提高系统响应速度:
- 异步处理可以将耗时的操作(如数据库写入、文件操作、网络请求等)放入消息队列,使得主线程或服务端能够快速响应用户的请求,从而提高系统的响应时间和用户体验。
- 消息队列作为中间件,能够在生产者(发送消息的系统)和消费者(接收并处理消息的系统)之间建立一层抽象。这样,生产者不需要关心消息如何被消费,消费者也不需要知道消息来自何处,增强了系统的模块化和可扩展性。
- 在高并发场景下,通过将大量请求转化为消息队列中的任务,能够避免直接冲击后端服务,起到缓冲作用,防止系统因瞬时峰值而崩溃。同时,多个消费者可以从队列中拉取任务并行处理,实现负载均衡。
- 由于异步处理可以并行执行任务,不依赖于串行调用链路,因此能显着提高整个系统的处理能力,即增加系统的吞吐量。
引入消息队列,用异步方式处理耗时的请求后,我们的系统架构如下:
既然有这么多好处,干脆都用异步的方式处理所有请求?不尽然,还记得在系统设计中,一切的选择都是取舍!现在来看看可能的缺点或限制:
- 系统复杂度提高原本我们以同步方式处理请求,整个过程相对单纯。但现在被拆分成三个角色,就必须考虑到每个环节都有可能发生失败。若不只一个消息队列,甚至彼此关联。那么复杂度会提升的更快,增加监控、追踪的难度!
- 消息可能不会照发送顺序完成假设消息队列中,依序有A和B两个请求,分别由两个不同的消费者处理,结果有可能B的处理速度比较快,反而比A先做完。若对处理顺序有要求(例如B会依赖A的结果),就需要特别留意。
- 同样的消息被重复处理通常消息队列会让 处理失败的消息重新入列 ,来保证每条消息都有确实完成。试想一个例子,有条消息是要扣款,当某个消费者收到消息且执行扣款后,却在确认消息前挂掉。队列以为消息没有完成就将它重新入列,导致又被发送到另一个消费者而再度扣款。像这样不允许消息被重复处理的场合,也需要额外设计。
现今主流的消息队列技术(RabbitMQ,Kafka)各有不同的优缺点。根据选择的技术和实作方式,以上情况可能很简单,也可能很复杂。重要的是, 永远都要设想到最坏的情况 ,依据我们的需求适当地应对。
全文检索的性能
全文检索(Full-text search) ,简单来说就是针对整个数据库中的文字,找出符合特定字词的内容。例如使用者在购物网站的搜索列中,用关键字寻找商品。这对一般的数据库而言是很耗时的工作,而搜索引擎就是专门用来处理这样的需求。
搜索引擎(Search Engine)
可视为一种特殊的数据库,用非常“特殊”的方式来储存数据。为了加速全文检索的性能,大多采用倒排索引(Inverted index)来保存数据。
什么是 倒排索引 呢?假设有三篇文章,下图左边为一般关联式数据库的保存方式。若要搜索所有包含“apple”关键字的文章,恐怕只能扫完整张表格才能得到结果(虽然这个例子中只有三篇文章,但若有上万篇呢?)。
而右边就是倒排索引的保存方式,所有文章 被打散成一个个词 ,像字典一样排序好,并纪录在哪里出现过。由于每个词就是索引,一样是搜索“apple”,可以很快速地知道在doc 1和doc 2有出现过。
除了核心技术的倒排索引外,各家搜索引擎会再加上种种优化搜索的功能。例如支持单复数、同义词的近似搜索,或是将多个关键字的顺序也列入考虑等等。
加入搜索引擎处理全文检索后,我们的系统架构如下:
安全性(Security)
最后,随着系统规模扩张,安全性也是非常重要的课题!由于这是非常专业的领域,我们会着重在大方向的观念,以及实务上常见的做法。
首先要思考的,就是系统中的组件,例如网站服务器、数据库等等,哪些要“ 暴露在外 ”,也就是让使用者可以接触到的?
这类型的组件数量应该 越少越好 ,理论上除了少数的进入点外,系统的其它部分对使用者来说都要是封闭的,而这些进入点需要加上额外的验证机制。我们希望系统像是一座堡垒,而不是自由穿梭的园游会!
要如何做到呢?最直接的方式就是将所有组件都放到一个 私人网络中。只有同一个网络中的组件可以互相连接,就像在宿舍用内网玩对战游戏那样。此外,我们还要帮每个组件设定好 防火墙 ,让它们只接受特定的连接方式(例如指定来源),来进一步提升安全性。
至于哪些组件适合当作进入点开放给外部使用者?负载均衡器就是一个非常好的选择。我们让它(而且只有它)拥有对外公开的IP地址,使用者只能通过负载均衡器进入到我们的系统。
若整个系统是放在云端,这个封闭网络就称为虚拟私人云端(Virtual Private Cloud, VPC)。现在我们的架构如下:
使用者可以访问 DNS 询问 IP 地址、从 CDN 下载静态资源,以及发送请求到负载均衡器。除了这三者外的所有组件,对使用者来说都是封闭的。
将系统封闭起来后,目前最需要保护的路径,就剩下使用者到负载均衡器这一段。一般都会采用HTTPS(相对于HTTP更安全)的方式来保护传输过程。
为什么不让所有的连接都通过 HTTPS 就好了呢?因为通过 HTTPS 传输的运算成本比较高(需要加解密),所以只在 VPC 外采用,一旦通过了负载均衡器的验证,进到内部后就可以简单使用 HTTP 来提升传输效率。
总结
在上篇中,我们从一个简单的架构开始,针对系统的扩展性和可用性,依序介绍了垂直扩展、服务分离、水平扩展(状态抽离),以及数据库的复写、读写分离、故障转移与缓存。
在本篇中,我们把静态资源交由CDN处理、让比较耗时的请求,通过消息队列用异步的方式来完成,以及引入搜索引擎加速全文检索的速度。最后,我们将架构中的大部分组件封闭在VPC中,只开放少数组件作为使用者的进入点,并采用HTTPS来保护传输过程。
本文以一个广度优先的方式,为系统设计做一个比较基础的介绍。旨在提供一个全面概览,为深入理解各个子系统打下基础。
需要特别强调的是:
- 扩展数据库的复杂性和成本远高于应用程序服务器,应当始终视访问数据库为昂贵操作,尽量避免在数据库端处理复杂的业务逻辑和频繁查询,可通过缓存减轻数据库负担;
- 在设计系统时,考虑到未来的可扩展性(如服务状态是否易于剥离)是必要的,但系统架构的演进是一个迭代过程,不必一开始就设计过于庞大复杂的架构。实际上,大多数系统的实际用户规模往往达不到预期设想,过度设计不仅会增加复杂度和开发维护成本,还会影响初期产品快速推向市场并获得反馈的重要性;
- 在系统设计中,每一个选择都是权衡的结果。虽然文中提到了许多组件和技术,但并非适用于所有系统,何时导入何种技术也并无定论。关键在于明确当前系统瓶颈所在,了解有哪些解决方案可供选择,以及考虑引入新技术的成本和风险。