高并发系统:设计原理与实践

978-7-115-66205-7
作者: 唐扬
译者:
编辑: 贾静
分类: 其他

图书目录:

详情

  本书主要探讨高并发场景下系统设计的原理和实践案例,帮助读者系统、快速地理解高并发系统的设计原理与相关实践,以及掌握解决高并发场景下可能遇到的各种问题的方法。   本书共6章。第1章介绍高并发系统的发展历史、设计难点和基本设计原则,以及度量指标;第2~4章介绍有助于提升高并发系统可用性的3种方法—系统容错、冗余和分片;第5章从提升高并发系统性能的角度讲解并发与异步的原理和实践技巧;第6章从系统运维和团队流程管理两个角度讲解如何提升团队对高并发系统的把控性,进而提升系统的可用性。   本书既适合软件开发人员阅读,也适合对高并发系统设计感兴趣的科研人员和计算机相关专业的学生阅读,还可用作中小型企业的内训教材。

图书摘要

版权信息

书名:高并发系统:设计原理与实践

ISBN:978-7-115-66205-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

版  权

著    唐 扬

责任编辑 贾 静

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内 容 提 要

本书主要探讨高并发场景下系统设计的原理和实践案例,帮助读者系统、快速地理解高并发系统的设计原理与相关实践,以及掌握解决高并发场景下可能遇到的各种问题的方法。

本书共6章。第1章介绍高并发系统的发展历史、设计难点和基本设计原则,以及度量指标;第2~4章介绍有助于提升高并发系统可用性的3种方法——系统容错、冗余和分片;第5章从提升高并发系统性能的角度讲解并发与异步的原理和实践技巧;第6章从系统运维和团队流程管理两个角度讲解如何提升团队对高并发系统的把控性,进而提升系统的可用性。

本书既适合软件开发人员阅读,也适合对高并发系统设计感兴趣的科研人员和计算机相关专业的学生阅读,还可用作中小型企业的内训教材。

前  言

随着互联网的迅猛发展,全球网民数量以及网民在网络上消耗的时间都明显增长,随之而来的就是互联网系统的并发量明显提升。以下是几个我们曾参与其中的案例:

QuestMobile的数据显示,2023年“双11”期间,购物类App的日均活跃用户数约为7.0亿;

2024年春运期间,火车票官方售票网站12306的单日最高售票量创下春运历史纪录,达到2090.1万张,单日最高访问量达838.8亿次;

在2015年除夕第一次出现抢红包的互动形式时,微信“摇一摇”的互动峰值达到了每分钟8.1亿次。

然而,并不是只有开发、运维这些大型应用的开发和运维人员才会面对高并发的问题。无论是电子商务、社交网络、在线娱乐还是金融服务等方面的系统,当面对运营活动时,都可能需要处理大量的并发请求,需要系统开发和运维人员确保系统在高负载下仍然能保持稳定、高效运行。

前瞻产业研究院在2024年发布的一份报告中指出,在全球从事软件行业的公司中,99%以上是中小微企业。对于这些企业运维的系统来说,其日均活跃用户数不会超过百万,请求量也不高,因此其开发人员缺少高并发系统设计的经验。

而在当今竞争激烈的技术行业,高并发系统设计成了许多岗位的必备技能之一。无论是互联网公司、金融机构还是其他企业,都对具备高并发系统设计技能的候选人持有很高的需求。这是因为高并发系统设计不仅代表着对系统架构和性能优化的深入理解,更意味着候选人具备解决复杂技术难题的能力。因此,对于求职者而言,精通高并发系统设计成了一张极具吸引力的名片,是打开许多公司大门的“钥匙”之一。因此,缺乏高并发系统设计经验的开发人员,亟需一本系统、全面地讲解高并发系统设计的图书,帮助他们更深入地理解和应用高并发系统设计技术。

本书的内容覆盖高并发系统设计和运维,旨在帮助读者系统、快速地掌握设计和搭建高并发系统的原理,学习互联网“大厂”积累的与高并发系统相关的实践经验。

高并发系统的设计是本书的重点内容,本书先明确设计难点是系统的高可用和访问的高性能,然后展开讲解4个基本设计原则(面向失败编程、可扩展、缓存和并发)及其实现方式。做好运维,是保证高并发系统的高可用和高性能的重要手段,本书将从系统运维和团队流程管理两个角度展开。

本书共6章,内容组织如下。

第1章,从高并发系统的发展历史讲起,重点介绍高并发系统的设计难点、基本设计原则和度量指标。需要注意,高并发系统的4个基本设计原则对应了众多方式,如系统容错、冗余、分片等,这些内容将在后续章节展开。通常情况下,在高并发系统的设计中,系统的高可用可以通过面向失败编程、可扩展和缓存这3个基本设计原则的实现来保证,访问的高性能可以通过并发这一基本设计原则的实现来保证。如图0-1中两条虚线所示。

第2章,讲解系统容错的6种实现方式,包括重试、熔断、降级、超时、限流和隔离。系统容错用于解决当系统出现局部问题时如何确保整体系统的高可用的问题。

第3章,讲解冗余的3种实现方式,包括存储资源冗余、计算资源冗余和机房冗余。其中,存储资源冗余包括存储冗余、缓存冗余、CDN冗余;计算资源冗余的主要实现方式是服务器冗余;而机房冗余则将存储资源和计算资源作为实现冗余的方法,从而提升系统跨机房、跨地域的可用性。

第4章,介绍分片,包括数据库分片和缓存分片这两种实现方式,以及分片后引入的新问题及对应的解决方案。

第5章,介绍并发与异步,这是提高高并发系统访问性能的常用手段。本章先介绍并发与异步的区别,然后介绍并发编程的实现方式、安全性与性能,最后介绍使用消息队列实现异步编程时可能遇到的问题及相应的解决方案。

第6章,从系统运维和团队流程管理两个角度介绍高并发系统的运维。从系统运维角度,本章将介绍如何通过全链路监控、报警系统、全链路压测和故障演练发现系统问题、性能瓶颈和系统可用性方面的薄弱点;从团队流程管理角度,本章将从变更流程、SOP文档、故障复盘机制和日常系统梳理方面介绍如何在团队内构建稳定性保障流程,从而提升高并发系统的可用性。

本书的内容组织结构如图0-1所示。

图0-1 本书的内容组织结构

本书案例的代码主要使用的是Java语言,在讲解熔断开源实现时用到了Go语言。为方便阅读,本书将在正文和代码注释中对代码的主要逻辑、重点和难点进行讲解,因此不要求读者同时具备Java和Go的语言基础。

最后,我要向许多人表达最诚挚的感谢和深深的敬意,本书的完成离不开他们的支持、帮助和鼓励。首先,我要感谢我的家人,他们在我最需要支持和理解的时候给予了我最大程度的关爱和包容,让我有了充分的精力和动力投入写作;其次,我要衷心感谢我的领导,他们在我职业生涯的各个阶段都给予了我无私的指导、支持和鼓励;同时,我还要感谢所有参与本书撰写和编辑的同事们,他们辛勤工作、通力合作,为本书的顺利完成提供了坚实的保障;最后,我要向所有阅读本书的读者表示诚挚的感谢,是你们的支持和关注,让我有了写作的动力和信心。我希望本书能够给读者带来有益的指导,成为读者学习和工作中的良师益友。

唐扬

2024年9月

资源与支持

资源获取

本书提供如下资源:

本书思维导图;

异步社区7天VIP会员。

要获得以上资源,您可以扫描下方二维码,根据指引领取。

提交勘误

作者和编辑已尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区(https://www.epubit.com),按书名搜索,进入本书页面,点击“发表勘误”,输入勘误信息,点击“提交勘误”按钮即可(见下页图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

与我们联系

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给本书的责任编辑(jiajing@ptpress.com.cn)。

如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

关于异步社区和异步图书

“异步社区”(www.epubit.com)是由人民邮电出版社创办的IT专业图书社区,于2015年8月上线运营,致力于优质内容的出版和分享,为读者提供高品质的学习内容,为作译者提供专业的出版服务,实现作译者与读者的在线交流互动,以及传统出版与数字出版的融合发展。

“异步图书”是异步社区策划的精品IT图书的品牌,依托人民邮电出版社在计算机图书领域30余年的发展与积淀。异步图书面向IT行业以及各行业使用相关技术的用户。

第1章 高并发系统概述

本章内容是高并发系统概述,主要包括高并发系统的发展历史、一个特点和两个设计难点、4个基本设计原则和度量指标。

1.1 高并发系统的发展历史

Web 1.0时代的互联网处于早期发展阶段,在这个时代,互联网的主要功能是提供静态的内容展示,用户主要是被动地消费内容,而不会参与内容的创造和分享,因此这个时代的互联网也被称为“只读网络”,其标志性的应用就是三大门户网站——新浪、搜狐和网易。

在Web 1.0时代里并没有产生所谓高并发的概念,原因就在于虽然这三大门户网站的日均页面浏览量(page view,PV)已经超过千万,但是因为其“只读”的特性,网站的维护者只需要使用内容分发网络(content delivery network,CDN)和静态文件缓存组件就能够应对用户的大量请求。

随着Meta(曾用名Facebook)、微博等社交媒体的风靡,互联网也正式进入了Web 2.0时代。在这个时代,高并发逐渐成了行业内的核心话题,导致这种情况出现的原因主要有以下两个。

无论是社交媒体,还是电商应用,抑或是直播短视频业务,都不再是网站让用户看什么用户就看什么,而是用户会更加深入地参与到网站的核心流程中。随之而来的问题是,网站的维护者只通过CDN和静态文件缓存组件不能应对来自用户的请求,需要考虑使用分布式缓存、限流、降级、熔断、隔离等技术,系统的复杂度也大大地提高了。

移动应用的兴起和移动网络技术的快速发展,标志着互联网正式进入移动互联网的时代。在这个时代,用户使用互联网的门槛大大降低了。随之而来的问题是,全球网民数量暴涨,给系统带来了大量的并发流量。这是“高并发”被广泛讨论的更主要的原因。

1.2 高并发系统的设计难点

在Web 2.0时代,涌现出了许多拥有海量用户的应用,它们所承受的并发流量是Web 1.0时代的网站无法想象的。如此大的并发流量,给系统带来的技术挑战是前所未有的。对承受如此大并发流量的高并发系统来说,系统设计的难点主要有两个:系统的高可用和访问的高性能。

1.2.1 系统的高可用

在维基百科中,高可用(high availability,HA)的定义是系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。系统无中断地运行其功能的时间越长,代表系统的高可用越好。在很多开源组件中,都有保证高可用的方案。

例如,Redis的官方文档中专门有一部分用来介绍Redis的高可用方案——哨兵(sentinel),该方案的示意如图1-1所示。哨兵节点是一组独立部署的进程,每个进程会定期向Redis主节点和从节点发送命令,探测它们是否存活。如果主节点没有正确地响应命令,那么哨兵节点就会触发Redis集群的主从切换,重新为Redis集群指定一个能够正常运行的从节点作为主节点。

图1-1 哨兵高可用方案的示意

再如,在Hadoop 1.0中,用来存储集群元信息的NameNode只有一个,没有提供高可用方案。NameNode相当于整个Hadoop集群的心脏,一旦发生故障对整体系统的影响非常大,因此Hadoop 2.0提供了NameNode的高可用方案,即系统中除了存在一个正常运行的NameNode(active NameNode),还存在一个备用NameNode(standby NameNode)。正常运行的NameNode和备用NameNode之间可以通过共享存储文件来共享数据。当正常运行的NameNode发生故障时,备用NameNode会自动代替故障的NameNode,成为新的正常运行的NameNode提供给集群使用。

对于任何系统来说,保证系统的高可用是一名开发人员的首要任务,而在现实的系统运维过程中,影响系统可用性的原因有很多。下面就介绍几个重要的原因。

影响系统可用性的一个原因是系统可能随时发生灾难性的“黑天鹅”事件,这对整体系统可用性的影响是巨大的。庞大的系统中存在海量的服务和资源,这些服务和资源之间的关系错综复杂。即使开发和运维人员在保障系统高可用方面采取了多种措施,也很难做到尽善尽美。一个小的疏漏可能就会导致系统的全盘崩溃。

例如,2022年12月18日,阿里云香港Region的可用区C出现了一次近14个小时的故障[],故障的原因简单来说就是机房里的制冷系统出现了问题,机房里4套主备制冷系统共用了一套水路循环系统,导致制冷系统无法做到主备切换,必须花3个多小时等待制冷设备供应商来现场处理;供应商来了之后,又花了2个多小时对制冷系统进行了补水排气,结果在尝试重启制冷设备的时候,发现制冷系统存在一个群控的逻辑,导致无法重启单台制冷设备。于是,供应商又花了3个多小时来解除这个群控逻辑,并且对全部4台制冷设备进行了重启。在故障发生了近11个小时之后,机房的温度终于趋于稳定了。

[1] 此案例于2024年9月20日引自阿里云发布的“关于阿里云香港Region可用区C服务中断事件的说明”。

在整个处理过程中还发生了一个插曲,就是在整体机房温度没有恢复正常的时候,机房的一个包间竟然触发了强制消防喷淋,所以在机房温度恢复正常之后,阿里云没有对这个包间的服务器直接上电,担心服务器进水导致数据丢失,因此阿里云又不得不对触发强制消防喷淋的包间内的服务器做了数据完整性校验,这个过程又延续了3个多小时。

总结这个案例可以得出,无论是设计系统还是设计系统故障时的应急响应都需要注意以下3个关键点。

不能忽视系统中的单点问题。例如,制冷系统存在单点问题,虽然有主备制冷系统,但是它们共用一套水路循环系统,一旦水路循环系统发生故障,主备制冷系统都不能正常工作。

做好预案,且要经过完善的故障演练。预案不足或预案没有经过完善的故障演练,可能导致切换备用制冷系统不成功;又因存在群控逻辑而无法重启单台制冷设备,需要由供应商现场修改配置来解除群控逻辑,导致故障的解决时间延长了几个小时。对于一些互联网系统,尤其是“头部”互联网公司的核心系统,系统的高可用至关重要,即使出现一个小时的故障都可能带来不可估量的损失。

高可用策略不能只是理论上的高可用,需要能在实践中真正发挥作用。2021年,我维护的一个系统就出现了“只是理论上的高可用”的问题。当时,我维护的系统已经在云上做了多可用区部署,保证发生故障时可以把系统流量从一个可用区切换到另一个可用区,但是因为部署流量切换系统、监控系统、报警系统等多个运维系统的可用区恰好发生了故障,导致高可用策略失效。

当然,“黑天鹅”事件发生的概率非常小,可一旦发生,对于系统来说就是致命的灾难。然而无独有偶,在2023年,国内因为机房制冷系统故障导致的系统长时间故障又接连发生了2起。此类故障不仅会给公司带来巨大的经济损失,还会严重影响用户体验和伤害公司信誉等。因此,排除故障隐患、演练故障预案,是降低“黑天鹅”事件影响的必做之事。

影响系统可用性的另一个原因是突发流量。突发流量的典型载体是以微博为代表的社交媒体,它们经常会因突发流量而出现不可用的问题。大型的社交媒体往往拥有海量的用户,极大地加快了信息流动的速度。用户可以非常便捷地在社交媒体上获取信息,一些热点事件也更容易在社交媒体上发酵,短时间内带来大量的用户访问请求,给系统带来极大的冲击,影响系统的可用性。

影响系统可用性的更重要原因是系统变更。《SRE:Google运维解密》一书中提到:SRE的经验告诉我们,大概70%的生产事故由某种部署的变更而触发。这里提到的变更不仅仅是代码的变更,也包括数据的变更和配置的变更等。因此,开发团队需要关注变更的流程,如规定合适的变更时间、变更前准备必要的检查清单等,这部分内容将在6.5.1节详细介绍。

1.2.2 访问的高性能

系统性能是影响用户体验的直接因素,谷歌针对网页的用户体验提出了一个指标——与下一次绘制的交互(interaction to next paint,INP),它会在网页的生命周期中记录用户与网页所有交互的延迟时间(包含输入延迟、处理延迟和展示延迟),以度量网页对用户交互的总体响应速度。低INP意味着网页能够可靠地响应用户交互。INP分为以下3个等级。

INP小于等于200 ms:表明网页的响应速度良好,用户基本感知不到网页的刷新和渲染。

INP大于200 ms但是小于等于500 ms:表明网页响应速度有一定的优化空间。

INP大于500 ms:表明网页响应性差,用户体验也比较差,需要进行网页加载优化。

优化INP的方式是多种多样的,如减少请求服务端次数、优化接口响应时间、延迟加载、使用页面缓存等。在服务端优化INP是非常直接的方式之一,而优化接口响应时间一般有以下两个思路:

提高系统的并行能力,让系统在单位时间内能够处理更多请求;

缩短单次请求的响应时间。

下面分别介绍这两个思路。

1.提高系统的并行能力

阿姆达尔定律(Amdahl’s law)由吉恩·阿姆达尔(Gene Amdahl)在1967年提出,是描述计算机系统设计的重要定量原理之一。在高并发系统设计中,阿姆达尔定律描述的是在固定负载下,并发进程数与响应时间之间的关系。

在固定负载下,并行计算加速比(即进行并行化处理后,系统处理任务的效率提升情况)可以定义为RR越大表示并行化处理后效率提升越明显,即系统响应时间越短;R越小表示并行化处理后效率提升越不明显,即系统响应时间越长。R可以用式(1.1)表示:

(1.1)

其中,表示任务中串行进程的计算量,表示任务中并行进程的计算量,n表示并行化处理的节点个数。从式(1.1)可以推导出式(1.2):

(1.2)

其中,n表示并行化处理的节点个数,p表示并行任务数占总体任务数的比例。

p=1时,即任务全部是并行任务时,R=n;当p=0时,即任务全部是串行任务时,R=1,也就是完全无加速;当n趋近于无穷大时,也就是拥有无限个并行化处理节点时,R=1/(1−p),此时Rp正相关,即并行任务数占总体任务数的比例越高,R越大。

2.缩短单次请求的响应时间

想要缩短单次请求的响应时间,首先要判断系统是CPU密集型还是I/O密集型的,因为优化不同类型的系统性能的方式不尽相同。

CPU密集型系统需要处理大量的CPU运算,因此选用更高效的算法或者减少CPU运算次数就是这类系统性能的优化方式。例如,如果系统的主要任务是计算哈希值,那么这时选用更高效的哈希算法就可以大大提升系统的性能。发现CPU过载问题的主要方式,是通过一些CPU剖析工具(如针对Linux系统的perf、eBPF、gperftools等)来找到消耗CPU时间最多的方法或者模块。

I/O密集型系统的主要特点是系统中请求的大部分时间都用在等待I/O读写操作完成。当前大多数互联网系统都属于I/O密集型系统。如果观察这类系统的运行指标,就会发现系统中计算资源的CPU使用率的峰值基本上在40%~50%,这就是因为系统中请求的大部分时间都用在等待I/O读写操作完成了,如等待数据库、缓存中的数据返回,或者等待依赖的其他服务的请求返回,CPU反倒处于相对空闲的状态。这类系统的优化方式多是减少I/O读写操作的次数和耗时,如优化数据库的慢查询,使用速度更快、性能更好的缓存来减少对数据库的请求,使用连接池来减少连接的频繁创建,等等。

1.3 高并发系统的基本设计原则

“高并发、高性能、高可用”通常被称为高并发系统的“三高”,高并发是高并发系统的特点,而高性能和高可用是高并发系统的设计难点。但是不同的高并发系统面对的业务场景不同,设计方案也会有比较大的不同。那么,在设计高并发系统的时候是否有一些通用的原则呢?当然是有的。下面将介绍高并发系统的4个基本设计原则:

面向失败编程;

可扩展;

缓存;

并发。

这4个基本设计原则,为设计高并发系统的设计提供了一个通用框架,但具体的实施细节需要根据实际的业务需求、技术栈和系统特点来定制,并需要考虑其他因素(如系统的安全性、可维护性和成本效益等)。

下面分别介绍这4个基本设计原则。

1.3.1 面向失败编程

在一个使用大量服务器横向扩展的系统中,因某一台或某几台服务器故障而影响系统的高可用就变成了常态。假设一个业务使用了一万台服务器,即使每天每台服务器的故障概率只有万分之一,那么集群中每天依然会有服务器发生故障。

为了保障系统的高可用,就必须在系统设计、开发和部署的全过程中,时刻考虑在服务异常、服务器故障的场景下,如何确保故障不影响整体系统的可用性。

面向失败编程是软件设计和开发的一种理念,其核心思想在于假设系统的组件、服务或依赖的服务和组件一定会发生故障,并在设计和开发中考虑这些故障,以确保系统在发生故障后能够保持稳定运行或快速恢复。

面向失败编程的实现方式有很多,在系统设计时可以考虑使用多种系统容错手段来避免局部的故障影响整体系统的可用性,而在系统开发时可以考虑使用防御性编程。

1.系统容错

系统容错指的是系统在发生局部错误或者故障的时候,仍能够继续提供服务而不中断,其关键在于自动触发容错机制,尽量避免人为参与。系统容错的常用手段有6个:重试、熔断、降级、超时、限流和隔离。

下面先看两个系统容错手段的实现案例。

案例1:在社交媒体(如微博)中,用户发布内容时,如果遇到写入数据库失败的情况,可能会导致内容丢失。然而,如果在写入数据库失败时,能够将该内容作为一条消息写入消息队列,并配置一个消息队列的处理程序来消费这些异常消息,然后重新将其写入数据库,那么在数据库短暂故障期间,系统就能够很好地容错。这是降级的一个实现案例。

案例2:在系统架构设计中,网关通常充当系统的入口,负责接收和转发外部流量。所有进入系统的流量都必须由网关进行转发,因此,网关的可用性对整个系统至关重要。如果一些非核心接口的响应速度较慢,导致网关的线程资源被阻塞,那么网关转发流量的效率就会降低,进而影响整个系统的可用性。为了解决这个问题,可以考虑将响应速度较慢的非核心接口隔离到一个独立的“慢”网关上,这样就能够防止这些接口影响整个系统的可用性。这是隔离的一个实现案例。

系统容错是面向失败编程的重要实现方式,也是本书的重点内容,将在第2章详细介绍。

2.防御性编程

防御性编程是一种比较常见的编程方法。顾名思义,防御性编程指的是在编程过程中对于可预见的错误提前采取防御性措施,以避免系统崩溃的方法。

我曾处理过一次系统故障,这个故障的根源是系统运行过程中一个异常被吞没,使得某个比较耗时的初始化方法没有被正常初始化,而系统每次请求都要调用这个初始化方法,使得系统整体的响应时间异常延长,最终导致了系统崩溃。

常见的防御性编程有以下3种。

输入验证:对系统接收到的所有输入数据进行有效性验证,确保输入数据符合预期的格式、范围和类型,防止用户恶意输入造成安全漏洞和错误。

错误处理:合理地处理各种可能的错误场景,如类型转换错误、依赖系统故障、网络异常等。及时捕获和记录异常信息,采取适当的措施进行错误恢复或提示用户进行操作。

降级开关:任何的线上变更都需要增加降级开关,以保证一旦出现问题便可通过操作降级开关实现快速回滚。

1.3.2 可扩展

互联网系统的流量通常是具有潮汐特点的,如常见的白天流量低、晚上流量高,工作日流量低、非工作日流量高。但仍然会有一些不符合潮汐特点的特殊情况,可能是突发热点事件带来的突增流量,也可能是运营活动带来的超预期流量等,而应对这些预期外的突发流量是每一个系统的开发和运维人员要面临的挑战。

应对突发流量的方案通常是通过扩容来提升系统的可扩展性,而扩容又分为纵向扩容(scale-up)和横向扩容(scale-out)。

纵向扩容,指的是通过提升单个硬件节点的性能来提升系统的并发处理能力。这种方案的效果受到摩尔定律的影响。摩尔定律由英特尔公司的创始人之一Gordon Moore在1965年4月提出,用来描述信息技术的一种发展趋势,即在价格不变的情况下,集成电路上晶体管的数量在18个月左右就要翻一番,性能也增加一倍。

根据摩尔定律,系统的硬件性能应该会随着时间的推移而稳步提升。然而,在实际应用中,诸多因素(如制造工艺的瓶颈、物理限制、制造成本等)会影响硬件性能的提升,使其不能完全遵守摩尔定律。近年来,硬件性能的提升遇到了瓶颈,因此对摩尔定律的质疑声此起彼伏。随着互联网系统的发展和应用场景的复杂化,纵向扩容单位成本带来的性能提升效果明显下降。

因此,需要通过横向扩容的方式来提升系统的可扩展性。横向扩容,指的是通过使用大量的小规格服务器组成分布式集群来共同应对高并发大流量的冲击。使用横向扩容时需要考虑以下4个方面。

数据分片:将系统中的数据按照某种规则分割成多份,并存储在不同的节点上。数据分片可以降低单个节点的负载,提高系统的并发处理能力。数据分片的关键在于选择合适的分割规则,确保数据均匀地存储在各个节点上,同时保证分割后的数据仍然可以被有效地查询和操作。

负载均衡:将流量有效地分发到多个节点。负载均衡器可以根据一定的策略将流量分发到不同的节点,以避免单个节点负载过高。

无状态化:尽量减少节点之间的状态依赖,使得任意节点都可以处理任意请求,从而实现系统的横向扩展。无状态化的关键在于将状态从节点中分离出来,采用外部化存储或共享存储来管理状态数据,以确保节点的无状态性和易于横向扩展。

数据一致性:在分布式环境下,保证数据一致性是一个重要挑战。不同的业务需求可能对一致性有不同的要求,需要根据业务的具体情况并权衡数据的一致性与系统的性能之间的关系,选择合适的数据一致性方案,并采取相应的数据同步与冲突解决策略。

横向扩容固然可以突破硬件性能的瓶颈,但是需要更复杂的分布式架构设计和管理,有一定的开发和维护成本。

1.3.3 缓存

缓存通过牺牲空间来换取时间,以提升高并发系统的性能。不过,缓存并不是Web 2.0时代的产物,而是在计算机技术发展早期,为了解决CPU指令的执行速度和内存访问速度的差距问题而产生的。

正如之前提到的,摩尔定律指出:集成电路上可以容纳的晶体管数量大约每经过18个月便会增加一倍。换言之,处理器的性能大约每18个月会翻一倍,而CPU的性能也遵循着这一定律按指数级提升。

然而,在指令执行的过程中,除了需要CPU的参与,还需要从内存中查找并传输数据,而内存访问速度远远小于CPU指令的执行速度。因此,为了缩小CPU指令的执行速度和内存访问速度之间的差距,缓存被引入并发展起来,它可以加速数据的访问和处理过程。

目前CPU的时钟周期通常在纳秒级别,而CPU访问内存的时间约为100 ns,这意味着访问内存的时间大约是CPU时钟周期的100倍。这就导致CPU在执行指令时大部分时间都在等待从内存中获取数据,造成了CPU的空闲。

为了解决这个问题,CPU内部增加了多级缓存,数据从内存中取回之后会暂时存储在CPU的一级缓存(L1 cache)和二级缓存(L2 cache)中。这样,当CPU需要访问数据时,它可以首先尝试在缓存中查找,而不是直接从内存中读取。由于缓存的访问时间较短,一级缓存的访问时间约为0.5 ns,二级缓存的访问时间约为7 ns,这与CPU的时钟周期比较相近,因此CPU可以更充分地利用计算资源,提升指令执行的性能。

这就是使用缓存提升系统性能的典型案例,也是缓存最初的应用场景之一。实际上,缓存被提出时的定义就是:访问速度比一般随机存储器(random access memory,RAM)的访问速度更快的随机存储器。然而,随着IT行业的发展,缓存的外延也被大大扩展了,凡是处于访问速度相差较大的硬件之间,用于协调两者之间访问速度差异的组件都可以被称为缓存。

超文本传送协议(hypertext transfer protocol,HTTP)的缓存机制,是在互联网系统中经常出现且比较成熟的机制,是缓存的典型实现。下面以HTTP的缓存机制为例,讲解缓存这个基本设计原则。如果没有HTTP的缓存机制,那么客户端每次请求都需要服务端(运行在服务器上)重新生成相同的页面数据,这会导致以下3个问题。

提高服务器负载:服务器需要处理大量重复的请求,重复执行包括生成页面数据、数据库查询等操作,这会提高服务器的负载,降低系统的响应能力。

增加请求的处理时间:每次收到重复请求,服务器都需要从头开始生成页面数据,这会增加请求的处理时间,导致客户端等待时间变长。

消耗网络带宽资源:每次收到重复请求,服务器都需要返回相同的页面数据,这会消耗大量的网络带宽资源,尤其是对于大型网站或高流量网站来说,这个问题的影响更为显著。

因此,通过使用HTTP的缓存机制,服务器可以在首次收到请求时将页面数据缓存到客户端上,在后续发送相同请求时客户端直接从缓存中获取数据,这样就可以避免重复生成页面数据的开销了。

HTTP的缓存机制主要是由“Expires”和“Cache-Control”这两个HTTP响应首部来实现的。

“Expires”设置了缓存资源的过期时间,它是一个绝对时间,过了这个时间,缓存就失效了,必须重新从服务端获取最新的资源。例如,“Expires: Wed, 21 Feb 2024 07:28:00 GMT”表示缓存资源的过期时间是格林尼治标准时2024年2月21日7点28分。

“Expires”是在HTTP/1.0引入的,其在HTTP/1.1被“Cache-Control:max-age”代替。需要注意“Cache-Control:max-age”使用的过期时间是相对时间,例如“Cache-Control:max-age:2000”代表的是缓存资源2 s以后过期。

“Cache-Control”通过指定指令来实现HTTP的缓存机制,它有3个比较重要的缓存响应指令:

“Cache-Control:no-store”表示缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存,直接向服务端请求获取资源;

“Cache-Control:no-cache”表示无论缓存资源是否过期,在发布缓存副本之前,强制要求缓存把请求提交给原始服务器验证有效性;

“Cache-Control:must-revalidate”表示一旦缓存资源过期,在成功向原始服务器提交请求进行验证之前,缓存不能用该资源响应后续请求。

为了进一步降低服务端的负载和带宽消耗,HTTP还制定了缓存资源在服务端的两种验证机制——Last-Modified机制和ETag机制。

Last-Modified机制通过HTTP响应首部“Last-Modified”和HTTP请求首部“If-Modified-Since”实现。

缓存资源在返回HTTP客户端的时候会带着一个HTTP响应首部“Last-Modified”,表示此资源在服务端的最近一次修改时间,这个时间会和资源一起被HTTP客户端缓存起来。例如“Last-Modified: Wed, 8 May 2024 15:31:30 GMT”表示此资源在服务端的最近一次修改时间是格林尼治标准时2024年5月8日15点31分30秒。

然后,HTTP客户端再请求同一个资源的时候,会在“If-Modified-Since”请求首部中带上这个最近一次修改时间。服务端解析这个最近一次修改时间,判断资源在这个时间之后是否有修改,如果没有修改则直接返回304状态码,否则返回资源并在响应首部里更新资源的最近一次修改时间,这样HTTP客户端也可以同步更新最近一次修改时间。

不过这种机制有一个问题,如果资源被修改后又被恢复,就会造成最近一次修改时间变了但是资源并没有修改的情况。因此,HTTP提供了另一种服务端验证机制——ETag机制。

在缓存资源返回的时候,服务端会给缓存资源计算一个唯一的ID,然后把ID放在“ETag”这个HTTP响应首部里,客户端缓存就可以把这个ID和资源一并缓存起来。下次再请求同样的资源时,HTTP客户端会在HTTP请求首部“If-None-Match”里带上这个ID,服务端解析出这个ID后会把服务端的资源按照同样的计算方式算出ID,并对两个ID做比较,如果相同就直接返回304,否则返回服务端更新了的资源和新的ID给客户端。ETag机制虽然可以保证很强的缓存一致性,但是由于每次请求都需要服务端重新计算资源的ID,因此会有一些性能的损耗。

除了HTTP的缓存机制,在高并发系统的设计和开发过程中,缓存机制还有多种实现,本书将会从缓存冗余和缓存分片两个方面展开。这些内容将分布在第3章和第4章中,尤其集中在3.3节和4.2节。

1.3.4 并发

并发和并行是两种任务处理方式,二者虽然都可以提升系统处理任务的效率,也都是常见的优化系统性能的手段,但含义上却有很大区别。

并行指的是多个任务在微观上同时处理。例如,在具有多核处理器的计算机上,不同的CPU核心可以同时执行不同的任务。而并发则是指多个任务在宏观上看似同时处理,实际上是通过共享一个计算单元、快速交替执行来实现的。

例如,用户在个人计算机上一边听歌一边使用文档编辑程序写年终总结,音乐播放和文档编辑就是两个任务。如果这台个人计算机的CPU核心数恰好是两个,那么就可以使用一个CPU核心来处理音乐播放任务,使用另一个CPU核心来处理文档编辑任务,这就是并行。但如果这台个人计算机的CPU核心数是一个,那么就只能规定这个CPU核心50%的时间处理音乐播放任务,另外50%的时间处理文档编辑任务,两个任务交替占用CPU核心的时间片来运行,这就是并发。

由于系统的CPU核心数不能无限多,在高并发系统设计中使用并发的场景更多。系统实现并发的方式有很多,常见的有多进程、多线程和多协程这3种方式。

多进程指的是在操作系统中同时运行多个独立的进程,并发地执行多个任务。这种方式的优点在于每个进程都有独立的内存空间,不同进程之间相互独立,一个进程的崩溃不会影响到其他进程。它的缺点也很明显,进程的创建和销毁的开销较大,并且多个进程之间需要额外的机制进行通信,实现复杂且开销较大。

多线程指的是在同一个进程内同时执行多个线程,线程共享相同的内存空间和系统资源。线程是操作系统资源调度的基本单元,它运行在系统态,多个线程之间通过抢占CPU的时间片来实现并发。由于创建线程的过程比较耗时和耗费系统资源,因此开发人员通常使用池化技术来缓存未使用的线程方便后续使用。多个线程发生切换时,CPU需要缓存当前执行线程的执行现场,这样当这个线程重新被CPU调度回来时可以继续执行,这个过程叫作线程的上下文切换。上下文切换的过程虽然很快,但是需要涉及一个从用户态到内核态再到用户态的变化过程,因此频繁的上下文切换对于系统来说是一个比较沉重的负担。

多协程则通过在同一个线程内切换执行多个协程来实现并发,多协程是比多线程更轻量级的一种实现并发的方式。协程完全运行在用户态,由少量的几个线程来做调度,因此上下文切换的开销很小。而且协程相比线程,占用的空间很小,基本上在千字节级别,因此在系统运行阶段调度大量的协程来并发地执行任务,可以大大提升系统的性能。

无论是多进程、多线程还是多协程,本质上都是通过技术手段让系统可以同时执行多个相关性较低的任务。从这方面来讲,异步也能起到同样的作用,而消息队列是异步的常见实现方式。

消息队列的典型使用场景就是“秒杀”的场景,当购买商品的大量请求到达后端服务器的时候,这些请求会先被写入消息队列中,后续的消息队列处理程序再逐一执行扣减库存和发货等流程。在系统开发过程中,使用消息队列可以提升系统的响应能力,降低系统的耦合度。5.4节将详细介绍与消息队列相关的内容。

1.4 高并发系统的度量指标

著名的管理学大师彼得·德鲁克(Peter Drucker)曾经说过:“You can’t manage what you can’t measure.”运维人员在维护系统的时候,也需要对可用性和性能分别进行度量,才能衡量系统当前的可用性和性能,为后续的系统优化提供数据支撑。

1.4.1 可用性的度量

对于可用性的度量,人们经常会提到一个名词——服务等级协定(service level agreement,SLA)。

SLA指的是服务提供者与服务使用者就服务类型、服务质量和客户付款等达成的一个共识。与SLA相关的另外两个名词是——服务等级指示器(service level indicator,SLI)和服务等级目标(service level objective,SLO)。

SLI用来衡量服务水平。可以用作SLI的指标非常多,如可用性、吞吐量、平均响应时间和数据完整性等,选择的指标通常由服务提供者和服务使用者协商决定。

SLO是根据选定的SLI设置的具体目标,用来衡量服务期望状态和目标范围。假如选择平均响应时间作为SLI,那么可以设置SLO为平均响应时间小于10 ms;而假如选择数据完整性作为SLI,那么可以设置SLO为数据完整性达到99.999%及以上。

SLA则在SLO的基础上对服务水平提出进一步的要求,可以理解为SLA= SLO+违约责任。也就是说,SLA是在没有达到SLO的情况下,服务提供者需要向服务使用者进行补偿的协定。如果服务提供者是一个数据存储服务提供商,它给服务使用者承诺的SLO为存储数据完整性达到99.99999%,那么SLA可以为存储数据完整性未达到99.99999%时服务提供商需要向服务使用者进行补偿的协定。

可以看出,SLA的使用范围更广,不仅可以度量可用性,还可以从性能、质量和安全性等多个维度对高并发系统进行度量。现在回到可用性的度量,业界通用的度量方法是使用故障的平均维修时间(mean time to repair,MTTR)和平均故障间隔时间(mean time between failures,MTBF)进行度量,可以表示为式(1.3):

(1.3)

式(1.3)计算得到的可用性是一个小于1的数字,通常以百分数来表示,如90%、99%、99.9%,这就是我们经常提到的“一个9”“两个9”“三个9”。可用性和年允许故障时间、日允许故障时间之间的对应关系,如表1-1所示。

表1-1 可用性和年允许故障时间、日允许故障时间之间的对应关系

可用性

年允许故障时间

日允许故障时间

90%

36.5 d

2.4 h

99%

3.65 d

14.4 min

99.9%

8.76 h

1.44 min

99.99%

52.56 min

8.64 s

99.999%

约5 min

0.864 s

99.9999%

约32 s

86.4 ms

可以看出,90%和99%可用性的年允许故障时间都大于1 d,这在实践中比较容易达到。例如,开发团队和运维团队对系统的可用性做日常巡检,一旦发现问题立刻跟进处理,并且没有发生重大的“黑天鹅”事件,基本上就可以达到这一目标。而对于99.99%可用性,年允许故障时间为52.56 min、日允许故障时间为8.64 s,这对系统的开发流程、开发质量和基础设施建设提出了很高要求。

想象一下值班人员收到故障报警后处理故障的过程:

值班人员先打开个人计算机上的监控页面和日志系统,这时2~3 min的时间已经过去了;

值班人员需要花费10~15 min从监控页面和日志系统中定位故障服务和故障根源;

值班人员通过限流、分流、熔断、降级等手段,快速完成故障修复,这可能还需要5 min。

因此,完成一次故障处理可能需要20 min左右,这意味着一年内只要发生两次这样的故障,可用性就无法达到99.99%。

需要说明的是,上述故障处理过程有以下前提条件:

故障发生在工作日,换句话说,值班人员能够立即投入故障排查工作;

从监控页面和日志系统中能够迅速发现故障根源,而不需要逐行查看代码来排查故障原因;

有快速修复故障的手段,而不需要修改和发布代码;

……

以上任何一个条件没有满足,都可能延长故障处理的时间。

如果想要将系统的可用性提升至99.999%甚至99.9999%,也就是将年允许故障时间控制在分钟级别甚至秒级别,很难只依靠人工处理来排查和处理故障。如果单纯依靠人工处理来排查和处理故障,可能在尚未查明故障原因甚至开始排查故障原因之前,故障的持续时间已经超过5 min了。

目前已知的可用性能够达到99.999%的系统,基本上都通过自动化的故障转移来处理故障。尽管99.999%的可用性看起来很理想,但需要投入的开发成本是巨大的。因此综合考虑投入产出比和系统对可用性的要求,绝大部分系统的可用性还是会设定为99.99%。

高并发系统通常是服务于用户的,对可用性要求比较高,对于超过1 d的年允许故障时间明显是不能接受的,因此很少有高并发系统会把可用性设定为90%或者99%。主流的可用性是99.9%和99.99%,业务核心链路上的系统一般会将可用性设定为99.99%,而非业务核心链路上的系统一般会将可用性设定为99.9%。

1.4.2 性能的度量

对于性能的度量,非常直接的指标就是请求的响应时间,但是响应时间是一个瞬时指标,每一次请求都会有一个响应时间,那么当一段时间内存在多次请求时,如何度量这段时间内系统的性能呢?

计算这段时间的平均响应时间是一个解决方案,但使用平均响应时间作为度量指标,无法反映这段时间内的异常响应时间。例如,在1 s内系统的请求量是100次,其中99次请求的响应时间是1 ms,另一次请求的响应时间是1000 ms,那么在这1 s内的平均响应时间约等于11 ms,看起来请求的响应时间尚可,但是无法体现响应时间过长的、异常的请求。而如果使用最长响应时间作为度量指标,固然可以体现这次异常的请求响应,但是这次异常的请求会对整个系统的响应时间造成比较大的影响。

因此,在实际工作中通常会使用响应时间的分位值来度量系统的性能。响应时间的分位值是指在一组响应时间数据中,某个特定百分比的数据所对应的响应时间。75分位值和90分位值是度量系统性能的两个常用指标,如图1-2所示。

图1-2 响应时间的分位值示意

以90分位值为例,假如在一段时间内有100次请求,那么把这100次请求的响应时间按照从小到大的顺序排列,排在第90位的请求的响应时间就是90分位值。

1.5 小结

本章介绍了高并发系统的发展历史、设计难点、基本设计原则和度量指标。面对高并发系统,保证系统的高可用和访问的高性能是两个主要的设计难点,其中系统的可用性可以通过计算MTTR和MTBF来度量,系统的性能可以通过计算请求响应时间的分位值来度量。

在系统设计、开发和运维阶段,要保证系统易于横向扩容、实现系统容错,可以遵循面向失败编程、可扩展、缓存和并发这4个基本设计原则,进而保证系统的高可用和访问的高性能。

相关图书

Joy RL:强化学习实践教程
Joy RL:强化学习实践教程
DeepSeek极速上手 :高效做事不内耗
DeepSeek极速上手 :高效做事不内耗
计算机组成原理(基于x86-64架构)
计算机组成原理(基于x86-64架构)
秒懂AI辅助论文写作
秒懂AI辅助论文写作
AI商业进化论:“人工智能+”赋能新质生产力发展
AI商业进化论:“人工智能+”赋能新质生产力发展
DeepSeek快速上手
DeepSeek快速上手

相关文章

相关课程