This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
<audio id="audio" title="11 | 技术架构:作为开发,你真的了解系统吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/45/50/452fcf2eaeb4c6229d325cfb6e9e2f50.mp3"></audio>
你好,我是王庆友。从今天开始,我们就进入了技术架构模块,所以,这一讲,我想先跟你聊聊技术架构要解决什么问题。
对于开发人员来说,我们每天都在用技术。但你要知道,我们写的代码,其实只是系统的一小部分,我们了解的技术,也只是系统用到的一小部分。要深入掌握技术架构,我们就需要了解整体的系统。
面对一个复杂的系统,我想你可能经常会有以下困扰:
1. 不清楚系统整体的处理过程,当系统出问题时,不知道如何有针对性地去排查问题。
1. 系统设计时,经常忽视非业务性功能的需求,也不清楚如何实现这些目标,经常是付出惨痛的教训后,才去亡羊补牢。
不知你是否还记得,在[第一讲“架构的本质”](https://time.geekbang.org/column/article/200825)中,我已经说过,技术架构是从物理层面定义系统,并保障系统的稳定运行。那么今天,我会先分析下系统在物理上由哪些部分组成,让你可以从全局的角度看一个系统;然后再和你一起讨论,技术架构会面临哪些软硬件的挑战,以及它都有哪些目标,让你能够深入地了解技术架构。
## 系统的物理模型
对于大部分开发人员来说,我们主要的工作是写业务相关的代码,保证业务逻辑正确、业务数据准确,然后,这些业务代码经过打包部署后,变成实际可运行的应用。但我们写的代码只是系统的冰山一角,为了保证应用能正常运行,我们需要从**端到端系统的角度**进行分析。
我们先看下一个系统的具体组成,这里我为你提供了一个简化的系统物理模型,你可以了解一个系统大致包含哪些部分。
<img src="https://static001.geekbang.org/resource/image/85/44/8567a6fb27cb0811643c9f922601a744.jpg" alt="">
从用户请求的处理过程来看,系统主要包括五大部分。
首先是**接入系统**它负责接收用户的请求然后把用户的请求分发到某个Web服务器进行处理接入系统主要包括DNS域名解析、负载均衡、Web服务器这些组件。
接下来Web服务器会把请求交给**应用系统**进行处理。一般来说我们是基于某个开发框架来开发应用的比如Java应用一般是基于Spring MVC框架。
这个时候开发框架首先会介入请求的处理比如对HTTP协议进行解析然后根据请求的URL和业务参数转给我们写的业务方法。接下来我们的应用代码会调用开发语言提供的库和各种第三方的库比如JDK和Log4j一起完成业务逻辑处理。在这里我们会把开发框架、应用代码还有这些库打包在一起组成一个应用系统作为独立的进程在Web服务器中进行部署和运行。
到这里,整个系统要做的事情就完了吗?
还没有呢,在我们的应用系统底下,还有**基础平台**它由好几个部分组成首先是各个语言的运行时比如说JVM然后是容器或虚拟机下面还有操作系统最底下就是硬件和网络。
**接入系统、应用系统、基础平台就构成一个最简单的系统。**
在大多数情况下应用系统还要借助大量外部的中间件来实现功能和落地数据比如数据库、缓存、消息队列以及RPC通讯框架等等。这里我统称它们为**核心组件**,它们也是系统不可缺少的一部分。
除此之外,还有大量周边的**支撑系统**在支持应用的正常运行,包括日志系统、配置系统,还有大量的运维系统,它们提供监控、安全、资源调度等功能,它们和核心组件的区别是,这些系统一般不参与实际的用户请求处理,但它们在背后默默保障系统的正常运行。
到这里,你可以发现,一个端到端的系统是非常复杂的,它包含了大量的软硬件。为了保障我们的应用代码能够正常运行,我们就需要保证这里的每个组件不出问题,否则一旦组件出问题,很可能就导致系统整体的不可用。
## 技术架构的挑战
应用代码怎么组织(比如模块划分和服务分层),那主要是业务架构的事,这部分在前面我们已经讨论过很多了;而**技术架构的职责,首先是负责系统所有组件的技术选型,然后确保这些组件可以正常运行。**
我们知道,系统是由硬件和软件组成的。接下来,我们就分别从软硬件的角度来看下,技术架构都会面临什么挑战,我们需要如何应对。
### 硬件的问题
硬件是一个系统最基础的部分,负责真正干活的,但它有两方面的问题。
**首先是硬件的处理能力有限。** 对于服务器来说它的CPU频率、内存容量、磁盘速度等等都是有限的。虽然说按照摩尔定律随着制造工艺的发展大概每隔18个月硬件的性能可以提升一倍但还是赶不上快速增长的系统处理能力的要求特别是目前许多互联网平台面向的都是海量的C端用户对系统处理能力的要求可以说是没有上限的。
从技术架构的角度,提升硬件的处理能力一般有两种方式。
- **Scale Up**
也就是垂直扩展,简单地说就是**通过升级硬件来提升处理能力**。CPU不够快升级内核数量内存不够多升级容量网络带宽不够升级带宽。所以说Scale Up实际上是提升硬件的质量。
- **Scale Out**
也就是水平扩展,**通过增加机器数量来提升处理能力**。一台机器不够就增加到2台、4台以及更多通过大量廉价设备的叠加增强系统整体的处理能力。所以说Scale Out是提升硬件的数量。
垂直扩展是最简单的方式,对系统来说,它看到的是一个性能更强的组件,技术架构上不需要任何改造。如果碰到性能有问题,垂直扩展是我们的首选,但它有物理上的瓶颈或成本的问题。受硬件的物理限制,机器的性能是有天花板的;或者有时候,硬件超出了主流的配置,它的成本会指数级增长,导致我们无法承受。
水平扩展通过硬件数量弥补性能问题,理论上可以应对所有服务器处理能力不足的情况,并实现系统处理能力和硬件成本保持一个线性增长的关系。
但水平扩展对于系统来说它看到的是多个组件比如说多台Web服务器。如何有效地管理大量的机器一方面使得性能上可以实现类似1+1=2的效果另一方面要让系统各个部分能够有效地衔接起来稳定地运行这不是一件容易的事情。我们需要通过很复杂的技术架构设计来保障比如说通过额外的负载均衡来支持多台Web服务器并行工作。
硬件的第二个问题是,**硬件不是100%的可靠,它本身也会出问题**。
比如说,服务器断电了,网络电缆被挖断了,甚至是各种自然灾害导致机房整体不可用。尤其是一个大型系统,服务器规模很大,网络很复杂,一旦某个节点出问题,整个系统都可能受影响,所以,机器数量变多,也放大了系统出故障的概率,导致系统整体的可用性变差。**我们在做技术架构设计时,就要充分考虑各种硬件故障的可能性,做好应对方案。**比如说针对自然灾害,系统做异地多机房部署。
### 软件的问题
接下来我们说下软件的问题,这里的软件,主要说的是各种中间件和系统级软件,它们配合我们的应用代码一起工作。
软件是硬件的延伸,它主要是解决硬件的各种问题,软件通过进一步封装,给系统带来了两大好处。
- **首先是弥补了硬件的缺陷。**比如Redis集群通过数据分片解决了单台服务器内存和带宽的瓶颈问题实现服务器处理能力的水平扩展通过数据多副本和故障节点转移解决了单台服务器故障导致的可用性问题。
- **其次,封装让我们可以更高效地访问系统资源。**比如说,数据库是对文件系统的加强,使数据的存取更高效;缓存是对数据库的加强,使热点数据的访问更高效。
**但软件在填硬件的各种坑的同时,也给系统挖了新的坑。**举个例子Redis集群的多节点它解决了单节点处理能力问题但同时也带来了新的问题比如节点内部的网络有问题即网络分区现象集群的可用性就有问题Redis数据的多副本它解决了单台服务器故障带来的可用性问题但同时也带来了数据的一致性问题。
我们知道分布式系统有个典型的CAP理论C代表系统内部的数据一致性A代码系统的可用性P代表节点之间的网络是否允许出问题我们在这三者里面只能选择两个。对于一个分布式系统来说网络出问题是比较常见的所以我们首先要选择P这意味着我们在剩下的C和A之间只能选择一个。
**CAP理论只是针对一个小的数据型的分布式系统如果放大到整个业务系统C和A的选择就更加复杂了。**
比如有时候我们直接对订单进行写库这是倾向于保证数据一致性C但如果数据库故障或者流量太大写入不成功导致当前的业务功能失败也就是系统的可用性A产生了问题。如果我们不直接落库先发订单数据到消息系统再由消费者接收消息进行落库这样即使单量很大或数据库有问题最终订单还是可以落地不影响当前的下单功能保证了系统的可用性但可能不同地方比如缓存和数据库的订单数据就有一致性的问题。
鱼和熊掌不能兼得系统无法同时满足CAP的要求我们就需要结合具体的业务场景识别最突出的挑战然后选择合适的组件并以合理的方式去使用它们最终保障系统的稳定运行不产生大的业务问题。
## 技术架构的目标
好,现在你已经了解了系统的复杂性和软硬件的问题,那技术架构就要选择和组合各种软硬件,再结合我们开发的应用代码,来解决系统非功能性需求。
**什么是系统非功能性需求呢?**这是相对于业务需求来说的,所谓的业务需求就是保证业务逻辑正确,数据准确。比如一个订单,我们要保证订单各项数据是准确的,订单优惠和金额计算逻辑是正确的。而一个订单页面打开需要多少时间,页面是不是每次都能打开,这些就和具体的业务逻辑没有关系,属于系统非功能性需求的范畴。产品经理在一般情况下,也不会明确提这些需求。非功能性需求,有时候我们也称之为系统级功能,和业务功能相区分。
**那对于一个系统来说,技术架构都要解决哪些非功能性需求呢?**
### 系统的高可用
可用性的衡量标准是系统正常工作的时间除以总体时间通常用几个9来表示比如3个9表示系统在99.9%的时间内可用4个9表示99.99%的时间内可用,这里的正常工作表示系统可以在相对合理的时间内返回预计的结果。
导致系统可用性出问题,一般是两种情况:
- 一种是软硬件本身有故障,比如机器断电,网络不通。这要求我们要么及时解决当前节点的故障问题,要么做故障转移,让备份系统快速顶上。
- 还有一种是高并发引起的系统处理能力的不足软硬件系统经常在处理能力不足时直接瘫痪掉比如CPU 100%的时候,整个系统完全不工作。这要求我们要么提升处理能力,比如采取水平扩展、缓存等措施;要么把流量控制在系统能处理的水平,比如采取限流、降级等措施。
### 系统的高性能
我们这里说的高性能并不是指系统的绝对性能要多高而是系统要提供合理的性能。比如说我们要保证前端页面可以在3s内打开这样用户体验比较好。
保证合理的性能分两种情况:
<li>
一种是常规的流量进来,但系统内部处理比较复杂,我们就需要运用技术手段进行优化。比如针对海量商品的检索,我们就需要构建复杂的搜索系统来支持。
</li>
<li>
第二种是高并发的流量进来,系统仍旧需要在合理的时间内提供响应,这就更强调我们做架构设计时,要保证系统的处理能力能够整体上做水平扩展,而不仅仅是对某个节点做绝对的性能优化,因为流量的提升是很难准确预计的。
</li>
### 系统的可伸缩和低成本
系统的业务量在不同的时间点,有高峰有低谷,比如餐饮行业有午高峰和晚高峰,还有电商的大促场景。我们的架构设计要保证系统在业务高峰时,要能快速地增加资源来提升系统处理能力;反之,当业务低谷时,可以快速地减少系统资源,保证系统的低成本。
高可用、高性能、可伸缩和低成本,这些技术架构的目标不是孤立的,相互之间有关联,比如说有大流量请求进来,如果系统有很好的伸缩能力,它就能通过水平扩展的方式,保证系统有高性能,同时也实现了系统的高可用。如果系统的处理能力无法快速提升,无法保证高性能,那我们还是可以通过限流、降级等措施,保证核心系统的高可用。
我在前面也提到,这些目标很多时候会冲突,或者只能部分实现,**我们在做技术架构设计时,不能不顾一切地要求达到所有目标,而是要根据业务特点,选择最关键的目标予以实现。**
比如说一个新闻阅读系统它和订单、钱没有关系即使短时间不可用对用户影响也不大。但在出现热点新闻时系统要能支持高并发的用户请求。因此这里的设计主要是考虑满足高性能而不用太过于追求4个9或5个9的可用性。
## 总结
系统比我们想象的要复杂得多,这里,我和你分享了系统的物理模型,相信你不再局限于我们自己写的代码,而是对系统的整体结构有了更清晰的认识。
你还记得吗?在前面介绍[业务架构](https://time.geekbang.org/column/article/204410)时,我和你分享的是**系统=模块+关系**,而在这里介绍技术架构时,我和你分享的是**系统的物理模型**。
因为**业务架构解决的是系统功能性问题**,我们更多的是**从人出发**,去更好地理解系统;而**技术架构解决的是系统非功能性问题**,我们在识别出业务上的性能、可用性等挑战后,更多的是**从软硬件节点的处理能力出发**,通过合理的技术选型和搭配,最终实现系统的高可用、高性能和可伸缩等目标。通过这一讲的介绍,相信你现在对技术架构的目标和常见的解决手段,已经有了更深入的理解。
当然,针对这些不同的目标,技术架构处理的原则和手段也是不一样的。后面的几讲中,我会针对每个目标,为你具体展开介绍。
**最后,给你留一道思考题:**技术架构除了我在课程中说的几个目标之外,还有哪些目标呢?
欢迎在留言区和我互动,我会第一时间给你反馈。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢阅读,我们下期再见。

View File

@@ -0,0 +1,170 @@
<audio id="audio" title="12 | 高可用架构:如何让你的系统不掉链子?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ae/26/ae9763d9d9ebf4af7cc3477c98ef0226.mp3"></audio>
你好,我是王庆友。今天我和你聊一聊,如何实现系统的高可用。
在实际工作中,我们平常更关注系统业务功能的实现,而对于系统是否会出故障,总觉得那是小概率事件,一开始不会考虑得太多。然而系统上线后,我们会发现系统其实很脆弱,每个地方都可能会出问题,处理线上事故的时间往往超过了开发功能的时间。
所以,对于系统的高可用,我想你经常会有这样的疑问:**系统的高可用真的很重要吗?如何实现系统的高可用,具体都有哪些手段呢?**
十年前我还在eBay那时候我们有几个数据来说明系统宕机对公司的影响我记得其中一个是系统每宕掉1秒公司将损失三千美金的收入现在的大型外卖平台也是如此如果就餐高峰期宕掉1小时平台至少损失几个亿的直接收入更加不用说对公司品牌的影响。
但是我们知道,系统中包含了大量的软硬件设备,要保证所有的节点都可用,不是一件容易的事。所以今天这一讲,我会从系统高可用的角度出发,和你介绍如何才能做到让系统不掉链子。
## 系统有哪些故障点?
那么一个系统,它在运行的过程中,都可能会出现哪些故障呢?我们来看一个简化的系统处理过程。
首先,客户端在远程发起请求,经过接入系统处理后,请求被转发给应用系统;应用系统调用服务完成具体的功能;在这个过程中,应用和服务还会访问各种资源,比如数据库和缓存。这里,我用红色部分,标识出了整个处理过程中可能出现的故障点,如下图所示:
<img src="https://static001.geekbang.org/resource/image/86/17/86d41e9801c949d04b42be6412bfe717.jpg" alt="">
这些故障点可以归纳为三类:
1. **资源不可用**,包括网络和服务器出故障,网络出故障表明节点连接不上,服务器出故障表明该节点本身不能正常工作。
1. **资源不足**,常规的流量进来,节点能正常工作,但在高并发的情况下,节点无法正常工作,对外表现为响应超时。
1. **节点的功能有问题**,这个主要体现在我们开发的代码上,比如它的内部业务逻辑有问题,或者是接口不兼容导致客户端调用出了问题;另外有些不够成熟的中间件,有时也会有功能性问题。
下面,我们就来看看如何才能应对这些问题,实现系统的高可用。
## 高可用策略和架构原则
系统可能出问题的地方有很多,解决的方式也不一样,在讨论具体的解决手段之前,我想先说下高可用的总体解决思路,这样你就能更好地理解具体的实现方式。
<img src="https://static001.geekbang.org/resource/image/82/09/825e9b25136970dfe41fa82317f0ca09.jpg" alt="">
要想让系统能够稳定可用,我们首先要考虑如何**避免问题的发生**。比如说我们可以通过UPSUninterruptible Power System不间断电源来避免服务器断电可以通过事先增加机器来解决硬件资源不足的问题。
然后,如果问题真的发生了,我们就要考虑怎么**转移故障**Failover。比如说我们可以通过冗余部署当一个节点发生故障时用其它正常的节点来代替问题节点。
如果故障无法以正面的方式解决,我们就要**努力降低故障带来的影响**。比如说流量太大,我们可以通过限流,来保证部分用户可以正常使用,或者通过业务降级的手段,关闭一些次要功能,保证核心功能仍旧可用。
最后是要**快速恢复系统**。我们要尽快找到问题的原因,然后修复故障节点,使系统恢复到正常状态。
这里我要强调的是,**处理线上事故的首要原则是先尽快恢复业务**,而不是先定位系统的问题,再通过解决问题来恢复系统。因为这样做往往比较耗时,这里给出的处理顺序也体现了这个原则。
那么结合前面介绍的系统故障点和高可用的解决思路,我们在做架构设计时,就可以从 **正面保障****减少损失** 两个角度来考虑具体的应对手段。下面,我就来和你分享一下高可用的设计原则。
<img src="https://static001.geekbang.org/resource/image/a1/c1/a1ccdeff03e66c86ca9e6dc891a0b7c1.jpg" alt="">
### 正面保障
**第一个设计原则是冗余无单点。**
首先,我们要保证系统的各个节点在部署时是冗余的,没有单点。比如在接入层中,我们可以实现负载均衡的双节点部署,这样在一个节点出现问题时,另一个节点可以快速接管,继续提供服务。
还有远程网络通信,它会涉及到很多节点,也很容易会出现问题,我们就可以提供多条通信线路,比如移动+电信线路,当一条线路出现问题时,系统就可以迅速切换到另一条线路。
甚至我们可以做到机房层面的冗余通过系统的异地多IDC部署解决自然灾害如地震、火灾导致的系统不可用问题。
**第二个设计原则是水平扩展。**
很多时候,系统的不可用都是因为流量引起的:在高并发的情况下,系统往往会整体瘫痪,完全不可用。
在前面的故障点介绍中,你可以看到,在应用层、服务层、资源层,它们的处理压力都是随着流量的增加而增加。[上一讲](https://time.geekbang.org/column/article/212066)中,我也提到过,由于硬件在物理上存在瓶颈,通过硬件升级(垂直扩展)一般不可行,我们需要通过增加机器数量,水平扩展这些节点的处理能力。
对于无状态的计算节点,比如应用层和服务层来说,水平扩展相对容易,我们直接增加机器就可以了;而对于有状态的节点,比如数据库,我们可以通过水平分库做水平扩展,不过这个需要应用一起配合,做比较大的改造。
### 减少损失
**第三个原则是柔性事务。**
我们知道系统的可用性经常会和数据的一致性相互矛盾。在CAP理论中系统的可用性、一致性和网络容错性三个最多只能保证两个在分布式系统的情况下我们只能在C和A中选一个。
在很多业务场景中,**系统的可用性比数据的实时一致性更重要**所以在实践中我们更多地使用BASE理论来指导系统设计。在这里我们努力实现系统的基本可用和数据的最终一致。
>
知识拓展关于BASE理论的详细信息你可以参考一下隔壁专栏《分布式协议与算法实战》的[这篇文章](https://time.geekbang.org/column/article/200717),这里就不详细展开了。
我们平时对单个数据库事务的ACID特性非常熟悉因为这里不存在P所以C和A都能得到很好地保证这是一种**刚性事务**。但在复杂的分布式场景下基于BASE理论我们通常只能实现部分的C软状态和最终一致和部分的A基本可用这是一种**柔性事务**。
柔性事务具体的实现方式有很多比如说通过异步消息在节点间同步数据。当然不同的方式对C和A的支持程度是不一样的我们在设计系统时要根据业务的特点来决定具体的方式。
**第四个原则是系统可降级。**
当系统问题无法在短时间内解决时,我们就要考虑尽快止损,为故障支付尽可能小的代价。具体的解决手段主要有以下这几种。
- **限流:**让部分用户流量进入系统处理,其它流量直接抛弃。
- **降级:**系统抛弃部分不重要的功能,比如不发送短信通知,以此确保核心功能不受影响。
- **熔断:**我们不去调用出问题的服务,让系统绕开故障点,就像电路的保险丝一样,自己熔断,切断通路,避免系统资源大量被占用。比如,用户下单时,如果积分服务出现问题,我们就先不送积分,后续再补偿。
- **功能禁用:**针对具体的功能,我们设置好功能开关,让代码根据开关设置,灵活决定是否执行这部分逻辑。比如商品搜索,在系统繁忙时,我们可以选择不进行复杂的深度搜索。
### 做好监控
**最后一个设计原则,是系统可监控。**
在实践中,系统的故障防不胜防,问题的定位和解决也非常的困难,所以,要想全面保障系统的可用性,最重要的手段就是监控。
当我们在做功能开发的时候,经常会强调功能的可测试性,我们通过测试来验证这个功能是否符合预期,而系统可监控,就像业务功能可测试一样重要。**通过监控,我们可以实时地了解系统的当前状态**,这样很多时候,业务还没出问题,我们就可以提前干预,避免事故;而当系统出现问题时,我们也可以借助监控信息,快速地定位和解决问题。
好,为了帮助你更好地理解,我对这些架构原则做个小结。
- 无单点和水平扩展是从正面的角度,直接保障系统的可用性。**无单点设计针对的是节点本身的故障,水平扩展针对的是节点处理能力的不足。**
- 柔性事务和可降级是通过提供有损服务的方式来保证系统的可用性。**柔性事务保证功能的基本可用和数据的最终一致,可降级通过损失非核心功能来保证核心功能的可用。**
- 最后,无论我们采取了多么强大的高可用措施,我们还是不能充分相信系统,还需要借助额外的监控来及时发现系统的问题并加以解决。**监控是我们的第二条保命措施。**
## 高可用手段
好了,通过前面的介绍,你应该已经了解了系统的故障点,以及高可用的设计原则。下面我们就一起来看下,在实践中都有哪些手段来保障系统的高可用。这里,我会按照系统的处理顺序来给你做详细介绍。
### 客户端-&gt;接入层
客户端到服务端通常是远程访问,所以我们首先要解决网络的可用性问题。
针对网络的高可用我们可以拉多条线路比如在企业私有的IDC机房和公有云之间同时拉移动和电信的线路让其中一条线路作为备份当主线路有问题时就切换到备份线路上。
在接入层也有很多成熟的HA方案比如说你可以选择Nginx、HAProxy、LVS等负载均衡软件它们都能很好地支持双节点+Keepalived部署。这样当一个节点出了问题另一个节点就可以自动顶上去而且两个节点对外是共享一个虚拟IP所以节点的切换对外部是透明的。
**这里,我们通过冗余和自动切换避免了单点的故障。**
### 接入层-&gt;Web应用
Web应用通常是无状态的我们可以部署多个实例很方便地通过水平扩展的方式提升系统的处理能力接入层的负载均衡设备可以通过各种算法进行多个Web实例的路由并且对它们进行健康检测如果某个实例有问题请求可以转发到另一个实例进行处理从而实现故障的自动转移。
通常情况下我们还可以在接入层做限流比如在Nginx中设置每秒多少个并发的限制超过这个并发数Nginx就直接返回错误。
**这里我们同时支持了Web节点的水平扩展、自动故障转移以及系统的可降级限流。**
### Web应用-&gt;内部服务
服务通常也是无状态的,我们也可以通过部署多个实例进行水平扩展。
有多种方式可以支持服务实例的发现和负载均衡比如说我们可以使用传统的代理服务器方式进行请求分发另外很多的微服务框架本身就支持服务的直接路由比如在Spring Cloud中我们就可以通过Eureka进行服务的自动注册和路由。
应用通常会访问多个服务,我们在这里可以做服务的隔离和熔断,避免服务之间相互影响。
比如在Spring Cloud的Hystrix组件开源熔断框架我们可以为不同服务配置不同的线程池实现资源隔离避免因为一个服务响应慢而占用所有的线程资源如果某个服务调用失败我们可以对它进行熔断操作避免无谓的超时等待影响调用方的整体性能。
在应用和服务的内部,针对具体的功能,我们还可以做一些**功能开关**。开关实际上是一个标志变量它的值可以是on/off 我们在代码中可以根据它的值来确定某一段逻辑是否要执行。开关的值可以在数据库或配置系统里定义这样我们就能够通过外部的开关值控制应用内部的行为这个在eBay有大量的落地。
**这里,我们同时支持了服务节点的水平扩展、自动故障转移以及系统的可降级(熔断和业务开关)。**
### 访问基础资源
常见的资源包括关系数据库、缓存和消息系统,我就以它们为例来介绍一下。
**关系数据库**属于有状态服务,它的水平扩展没有那么容易,但还是有很多手段能够保障数据库的可用性和处理能力。
首先,我们可以做数据库的主从部署,一方面通过读写分离,提升数据库**读**的性能减轻主库压力另一方面数据库有成熟的MHA方案支持主库故障时能够自动实现主从切换应用可以通过VIP访问数据库因此这个切换过程对应用也是透明的。
另外我们也可以通过物理的水平分库方式对数据进行分片这样就有多个主库支持写入。水平分库会涉及较多的应用改造后面会有一篇文章专门介绍1号店的订单水平分库项目到时我们再详细讨论。
再说下**缓存**。在数据读写比很高的情况下,我们可以利用缓存优化数据库的访问性能,包括进程内部缓存和分布式缓存,缓存是应对高并发的有效武器。
很多缓存方案比如Redis本身就支持集群方式它可以通过多节点支持处理能力的水平扩展通过数据的多副本来支持故障转移。
最后说下**消息系统**。消息系统有很多成熟的MQ组件比如说Kafka它可以通过多节点部署来支持处理能力的水平扩展也能通过数据的多分区实现故障的自动切换保证系统的可用性。
最后我想说的是,明天和意外你永远不知道哪个先到来,即使有了这些高可用措施,还是会有各种各样的意外等待着我们。所以,**系统的监控非常重要,只有准确地了解系统当前的状况,我们在面对问题时,才能快速响应,处理到点子上。**
## 总结
今天,我和你介绍了保障系统高可用都有哪些策略和设计原则,相信你现在对高可用的整体处理思路有了清楚的认识。
另外,我还针对典型的系统处理过程,和你介绍了各个环节都有哪些具体的高可用手段,希望你可以在工作中,结合系统的实际情况去落地它们。
接下来,我会通过几个实际的案例,来具体说明如何实现系统的高可用,你可以跟着课程继续学习,然后尝试着在实际的工作中去参考和灵活运用。
**最后,给你留一道思考题:**处理事故有三板斧的说法,你知道它们都是什么吗?你是怎么评价它们的呢?
欢迎在留言区和我互动,我会第一时间给你反馈。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢阅读,我们下期再见。

View File

@@ -0,0 +1,138 @@
<audio id="audio" title="13 | 高可用架构案例如何实现O2O平台日订单500万" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f9/d7/f9c0a0fdfd61407b9fb083e4ff90c0d7.mp3"></audio>
你好,我是王庆友。在上一讲中,我和你介绍了高可用系统的设计原则和常见手段。今天呢,我会通过一个实际的案例,告诉你具体如何落地一个高可用的架构,让你能够深入理解和运用这些高可用手段。
## 项目背景介绍
先说下项目的背景。这是一个小程序点餐平台,用户在小程序上点餐并支付完成后,订单会先落到订单库,然后进一步推送到门店的收银系统;收银系统接单后,推送给后厨系统进行生产;同时返回小程序取餐码,用户可以凭取餐码去门店取餐或收取外卖。
这个项目服务于一家大型的餐饮公司公司在全国有大量的门店他们准备搞一个长期的大型线上促销活动促销的力度很大用户可以在小程序上先领取优惠券然后凭券再支付1元就可以购买价值数十元的套餐。
结合以往的经验,以及这次的促销力度,我们预计在高峰时,前端小程序请求将会达到**每秒10万QPS**,并且预计**首日的订单数量会超过500万**。在这种高并发的情况下,我们为了保证用户的体验,**系统整体的可用性要达到99.99%**。
你可以先了解一下这个点餐平台的具体架构:
<img src="https://static001.geekbang.org/resource/image/f0/de/f0767d06f06e5166687d54b2c4b7a4de.jpg" alt="">
这里呢,我具体说下系统主要的调用过程,以便于你更好地理解它:
1. 小程序前端通过Nginx网关访问小程序服务端
1. 小程序服务端会调用一系列的基础服务完成相应的请求处理包括门店服务、会员服务、商品服务、订单服务、支付服务等每个服务都有自己独立的数据库和Redis缓存
1. 订单服务接收到新订单后先在本地数据库落地订单然后通过MQ同步订单给OMS履单中心
1. 门店的收银系统通过HTTP远程访问云端的OMS履单中心拉取新订单并返回取餐码给OMSOMS再调用小程序订单服务同步取餐码
1. 小程序前端刷新页面,访问服务端获得取餐码,然后用户可以根据取餐码到门店取餐或等待外卖。
## 高可用系统改造措施
我在前面也介绍了,这次活动的促销力度很大,高峰期流量将达到平时的数十倍,这就要求系统能够在高并发的场景下,保证高可用性。
所以基于访问量、日订单量和可用性的指标我们对原有系统进行了一系列改造最终顺利地实现了首日500万订单以及在大促期间系统4个9的可用性目标。这个500万的订单量也创造了中国单商户线上交易的历史记录。
在下面的系统架构图中我标出了具体的改造点主要有10处接下来我就给你分别具体介绍一下你可以通过这些具体的改造措施来真正理解高可用系统的设计手段。
<img src="https://static001.geekbang.org/resource/image/21/4c/218719777ff902f9b26cb88a6187ba4c.jpg" alt="">
### 前端接入改造
这里的前端有两个C端的小程序和B端的门店收银系统。前端部分主要是对三个点进行改造包括小程序端的CDN优化、Nginx负载均衡以及收银端的通信线路备份。
- **小程序端的CDN优化**
用户点餐前需要先浏览商品和菜单这个用户请求的频率很高数据流量大会对服务端造成很大的压力。所以针对这一点我们通过CDN供应商在全国各地构建了多个CDN中心储存静态的商品数据特别是图片这样小程序前端可以就近访问CDN流量无需通过小程序服务端缓解了服务端的压力。
- **Nginx负载均衡**
这个小程序点餐平台之前是直接利用云服务商提供的LB它只有简单的负载均衡能力。为了能应对这次的高并发流量现在我们独立搭建了数十台的Nginx集群集群除了负载均衡还提供限流支持如果QPS总数超过了10万前端的访问请求将会被丢弃掉。
另外Nginx在这里还有一个好处就是可以实时提供每个接口的访问频率和网络带宽占用情况能够起到很好的接入层监控功能。
>
**补充说明:**一台Nginx一般可以支持数万的并发本来这里无需这么多台Nginx这是因为云服务商对单个LB的接入有网络带宽的限制所以我们要通过提升Nginx的数量来保证接入有足够的带宽。
- **收银端的通信线路备份**
门店的收银系统会通过前置代理服务器来访问云端的OMS系统这个代理服务器部署在商户自己的IDC机房原来只通过电信线路和云端机房打通。在这次改造中我们**增加了移动线路**,这样当电信主线路出问题时,系统就可以快速地切换到移动线路。
### 应用和服务的水平扩展
首先针对小程序服务端的部署我们把实例数从十几台提升到了100台水平扩展它的处理能力。在上面的架构图中你可以看到小程序服务端依赖了7个基础服务每个基础服务也做了相应的水平扩展由于应用和基础服务都是无状态的因此我们很容易扩充。
这里的基础服务是Java开发的原来是用虚拟机方式部署的现在我们把基础服务全部迁移到了**容器环境**,这样在提升资源利用率的同时,也更好地支持了基础服务的弹性扩容。
### 订单水平分库
在大促情况下,下单高峰期,订单主库的**写访问**频率很高一个订单会对应6~7次的写操作包括了创建新订单和订单状态变更订单的**读操作**,我们之前通过一主多从部署和读写分离,已经得到了支持。
但负责写入的主库只有一个实例,所以这次我们通过**订单的水平分库**扩充了订单主库的实例数改造后我们有4个主库来负责订单数据写入。数据库的配置也从原来的8核16G提升到了16核32G这样我们通过硬件的垂直扩展进一步提升了数据库的处理能力。
这里的订单水平分库在实现上比较简单,我们是**通过订单ID取模进行分库基于进程内的Sharding-JDBC技术实现了数据库的自动路由**。后面的课程中,我会专门介绍电商平台的订单水平分库,它会更加复杂,到时你可以做个比较,如果有需要的话,也可以在实际项目参考落地。
### 异步化处理
你可以看到在前台订单中心和后台OMS之间我们需要同步订单数据所以这两者是紧密耦合的。不过这里我们通过**消息系统**对它们进行了解耦。 一方面前台下单要求比较快后台OMS的订单处理能力比较弱OMS库没有进行水平分库通过消息的异步化处理我们实现了对订单流量的削峰另一方面如果OMS有问题以异步的方式进行数据同步也不会影响前台用户下单。
还有在小程序服务端,在用户支付完成或者后台生成取餐码后,我们会以**微信消息**的方式通知用户,这个在代码中,也是通过异步方式实现的,如果微信消息发送不成功,用户还是可以在小程序上看到相关信息,不影响用户取餐。
### 主动通知,避免轮询
在原来的架构中前台小程序是通过轮询服务端的方式来获取取餐码同样商户的收银系统也是通过轮询OMS系统拉取新订单这样的收银系统有上万个每隔10s就会拉取一次。这种盲目轮询的方式不但效率低而且会对服务端造成很大的压力。
经过改造后,我们落地了**消息推送中心**收银系统通过Socket方式和推送中心保持长连接。当OMS系统接收到前台的新订单后会发送消息到消息推送中心然后收银系统就可以实时地获取新订单的消息再访问OMS系统拉取新订单。为了避免因消息推送中心出问题比如消息中心挂掉了导致收银系统拿不到新订单收银系统还保持对OMS系统的轮询但频率降低到了1分钟一次。
同理小程序前端会通过Web Socket方式和消息推送中心保持长连接。当OMS系统在接收到收银系统的取餐码后会发送消息到消息推送中心。这样小程序前端可以及时地获取取餐码信息。
### 缓存的使用
我们知道,缓存是提升性能十分有效的工具。这里的改造,就有两个地方使用了缓存。
- 当收银系统向OMS拉取新订单时OMS不是到数据库里查询新订单而是把新订单先保存在Redis队列里OMS通过直接查询Redis把新订单列表返回给收银系统。
- 在商品服务中菜单和商品数据也是放在了Redis中每天凌晨我们通过定时任务模仿前端小程序遍历访问每个商品数据实现对缓存的预刷新进一步保证缓存数据的一致性也避免了缓存数据的同时失效导致缓存雪崩。
### 一体化监控
在前面各个节点可用性优化的基础上我们也在系统的监控方面做了很多强化。除了常规的Zabbix做系统监控、CAT做应用监控、拉订单曲线做业务监控以外我们还对系统实现了一体化的监控。
在这里所有的节点都在一个页面里显示包括Web应用、Redis、MQ和数据库页面也会体现节点之间的上下游关系。**我们通过采集节点的状态数据,实时监测每个节点的健康程度,并且用红黄绿三种颜色,表示每个节点的健康状况。**这样,我们就可以非常直观地识别出,当前的哪些节点有问题。
监控的效果如下图所示,在下一讲中,我就会为你具体地介绍这个监控系统。
<img src="https://static001.geekbang.org/resource/image/ba/b0/ba4c6d94de2121ceef0e7cb579c2d8b0.jpg" alt="">
在实践中,这套监控系统也确实发挥了巨大的作用。很多时候,在系统问题还没有变得严重之前,我们就能够识别出来,并能进行主动干预。
比如说小程序服务端的部分节点有时候会假死这在Zabbix监控里往往看不出来但在我们的监控页面中这些节点就会飘红我们就可以通过重启节点来快速恢复。还有好几次系统有大面积的节点出问题了我们通过节点的上下游关系很容易地定位出了真正出现问题的地方避免所有人一窝蜂地扑上去排查问题。
除了这里我介绍的优化措施以外,我们也为系统可能出问题的地方做了各种预案。比如说,我们保留了部分虚拟机上部署的基础服务实例,这样如果容器出现了问题,基础服务可以快速切回到虚拟机上的实例。
## 系统改造小结
到这里为止,系统主要的优化措施就介绍完了,**但我们是如何知道要配置多少个节点,有没有达到预定的效果呢?**
对于这个问题,我们的做法是,**按照10万QPS和99.99%的可用指标要求,通过大量的压测来确定的。**
- 首先,我们对每个节点进行接口压测,做各种性能优化,确定好需要的机器数量;
- 然后我们利用JMeter模拟小程序前端发起混合场景的调用以此检验系统的抗压能力以及在压力下系统的可用性是否达到了预定的要求
- 最后我们在生产环境中根据压测环境按照服务器1:1的数量进行部署保证性能不打折最终这个小程序下单平台总的机器规模也达到了数百台的量级。
这里,我想结合着上一讲和你介绍的架构原则,来让你更深刻地理解这次系统可用性的改造过程。
从正面保障的角度来看,我们首先在各个环节都**避免了单点**,包括远程通信线路,这样能保证任意一个节点出了问题,都有其他实例可以顶上去;其次,我们通过节点的**垂直扩展和水平扩展**,大幅度提升了系统的处理能力,包括应用、服务和数据库的扩展;我们也有效地利用了**Redis缓存**,对高频的订单和菜单数据的读取进行了优化。
在**柔性处理**方面我们通过异步处理来优化系统的性能和避免大流量的直接冲击包括使用消息系统解耦前台下单系统和后台OMS系统以及通过及时的消息推送避免前端盲目轮询服务端。
同时,我们在**系统接入层**通过Nginx进行限流为系统的可用性进行兜底这样在流量超过预估时能够有效地避免后端系统被冲垮。
最后,我们通过**强有力的监控手段**,可以实时全面地了解系统运行状况,随时为异常情况做好准备。
## 总结
今天我与你分享了一个实际的O2O点餐平台在面对高并发流量时我们是如何对系统进行升级改造保证系统的高可用的。相信你在上一讲理论的基础上通过进一步结合实际的场景能够深入地理解如何运用各种高可用的手段了。
高可用的处理方式有很多,我这里给你介绍的也只是一部分,希望你能够在实践中,结合具体的业务场景,灵活地落地高可用的设计。
不过,无论我们采取多么周密的措施,总会有些地方我们没有考虑到,系统可能会出现各种各样的问题,这个时候对系统进行全面的监控就非常重要了。下一讲我会就如何做好系统的监控,和你做详细的介绍。
**最后,给你留一道思考题:**你当前的系统有单点吗?这个单点有没有出过问题呢?
欢迎你在留言区与大家分享你的问题和思考,我们一起讨论。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢阅读,我们下期再见。

View File

@@ -0,0 +1,144 @@
<audio id="audio" title="14 | 高可用架构案例(二):如何第一时间知道系统哪里有问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/96/d7/962dfeb3fc84272fbf0daa65bd00e5d7.mp3"></audio>
你好,我是王庆友。
在前面两讲中,我与你介绍了系统的高可用都有哪些设计原则和具体手段。其中我也特别提到,**要想保证系统的高可用,我们还需要对系统进行全面有效的监控**。
监控是系统的眼睛,无监控,不运维。今天我们就从监控的角度来聊聊如何保证系统的高可用。
在开发软件时,我们经常强调一个业务功能的可测性,甚至有一种说法是测试驱动开发。在开发之前,我们会先设计测试用例,再去考虑如何实现功能。同样,当我们对系统作了很多加固,也是希望能保证它的稳定可用。
**但我们怎么判断系统的各个节点当前是否正常呢?**这个就对应了节点的可监控性,如果你事先想好了系统应该如何监控,如何判断每个节点是否正常,那你就会更清楚应该采取什么样的措施。很多时候,我们可以从监控的角度来倒推系统的可用性设计。
## 监控的分类
在[第11讲](https://time.geekbang.org/column/article/212066)中我和你介绍了系统的组成它包括接入层、应用系统、中间件、基础设施这几个部分那我们的监控也是针对这些部分来实施的。一般来说监控可以分为5个层次如下图所示
<img src="https://static001.geekbang.org/resource/image/07/cf/079ff9bc96e0bb11d438b965ddd751cf.jpg" alt="">
从上到下,分别为用户体验监控、业务监控、应用监控、中间件监控、基础平台监控。
1. **用户体验监控:**指的是从前端用户的访问速度出发,来监测系统的可用性,包括页面能否打开、关键接口的响应时间等等,用户体验监控一般结合前端的埋点来实现。
1. **业务监控:**它是从业务结果的角度来看,比如说订单数、交易金额等等,业务监控也是最直观的,我们知道,如果业务数据没问题,系统整体也就没有问题。对于业务监控,我们一般是从数据库里定时拉取业务数据,然后以曲线的方式展示业务指标随着时间的变化过程。除了当前的曲线,一般还有同比和环比曲线。同比是和前一天的数据进行比较,环比是和一周前的数据进行比较,两方面结合起来,我们就能知道当前的业务指标有没有问题。
1. **应用监控:**指的是对自己开发的代码进行监控比如接口在一段时间内的调用次数、响应时间、出错次数等等。更深入一点的应用监控还包含了调用链监控我们知道一个外部请求的处理过程包含了很多环节比如说网关、应用、服务、缓存和数据库我们可以通过调用链监控把这些环节串起来当系统有问题时我们可以一步步地排查。有很多APM工具可以实现调用链监控如CAT、SkyWalking等等。
1. **中间件监控:**指的是对标准中间件进行监控它是第三方开发的代码比如数据库、缓存、Tomcat等等这些组件对应的是系统的PaaS层。这些中间件往往带有配套的监控系统比如RabbitMQ就有自带的监控后台。
1. **基础平台监控:**指的是对系统底层资源进行监控如操作系统、硬件设备等等这个层次的监控对应的是系统的IaaS层。Zabbix就是典型的基础设施监控工具它可以监控CPU、内存和磁盘的使用情况。
## 监控的痛点
我们知道,一个大型的互联网平台,背后对应的是大规模的分布式系统,有大量的软硬件节点一起协作,这里的任何节点都有可能出问题,所以我们需要通过监控,及时发现和解决问题,提升系统的可用性。
但想要实现高效的监控,这不是一件容易的事情。下面,我给你举一个线上事故处理的例子,你就能理解监控面临的挑战。
首先Monitor发现订单曲线突然跌停当前的订单数量变为0于是Monitor快速拉起电话会议或者在微信群里@所有人进行排查。这时候一大堆相关的或不相关的人都开始排查自己负责的那部分系统比如说运维在Zabbix里检查网络和机器开发在ELK系统Elasticsearch+Logstash+Kibana里检查错误日志DBA检查数据库。
过了一会儿负责App服务端的开发人员在ELK里发现有大量的调用下单服务超时于是他去询问下单服务的开发人员这是怎么回事。下单服务的开发人员就去检索错误日志结果发现调用会员服务有大量的超时情况然后他就去问会员服务的开发人员这是怎么回事。会员服务的开发人员通过错误日志发现会员数据库连接不上于是他把问题反映给DBA。DBA先拉上负责网络的同事一起看发现网络没啥问题然后他再去检查会员数据库本身这时他发现有慢查询把DB给挂住了。
这样通过一系列的接力式排查问题终于找到了最后DBA把慢查询杀掉所有人都去检查自己的系统发现没有新的错误情况系统恢复了正常。而这个时候距离问题的发生已经过去了很长时间在这个期间技术被老板催老板被商户催而商户也已经被用户投诉了N次。
以上的事故处理过程还算比较顺利的,毕竟我们通过顺藤摸瓜,最后找到并解决了问题。**更多的时候,我们面对事故,就像是热锅上的蚂蚁,众说纷纭,谁也不能肯定问题出在哪里。结果呢,我们病急乱投医,胡乱干预系统,不但没能解决问题,而且往往引发了二次事故。**
你可以发现在这个例子中虽然我们有应用日志监控有Zabbix系统监控有网络和数据库监控但对于一个大规模的分布式系统来说这种分散的监控方式在实践中有一系列的弊端。
- 首先,不同的节点,它的监控的方式是不一样的,相应地,监控的结果也在不同的系统里输出。
- 同时,系统不同部分的监控都是由不同的人负责的,比如说,运维负责的是基础平台监控,开发负责的是应用系统监控。而监控信息往往专门的人才能解读,比如应用监控,它需要对应的开发人员才能判断当前的接口访问是否有问题。
- 最后,系统作为一个整体,需要上下游各个环节的人一起协作,进行大量的沟通,才能最终找到问题。
你可以看到,这种监控方式是碎片化的,对于处理线上紧急事故,它无疑是低效的,这里有很多问题。
<img src="https://static001.geekbang.org/resource/image/87/d0/872d239bc37b3b28dca238fc3f6665d0.jpg" alt="">
1. **发现问题慢:**业务监控的曲线一般1分钟更新一次有时候因为正常的业务抖动Monitor还需要把这种情况排除掉。因此他会倾向于多观察几分钟这样就导致问题的确认有很大的滞后性。
1. **定位问题慢:**系统节点多,大量的人需要介入排查,而且由于节点依赖复杂,需要反复沟通才能把信息串起来,因此很多时候,这种排查方式是串行或者说无序的。一方面,无关的人会卷入进来,造成人员的浪费;另一方面排查效率低,定位问题的时间长。
1. **解决问题慢:**当定位到问题,对系统进行调整后,验证问题是否已经得到解决,也不是一件很直观的事情,需要各个研发到相应的监控系统里去进行观察,通过滞后的业务曲线观察业务是否恢复。
**那么,我们怎么解决监控面临的这些困境,以高效的方式解决线上事故,保障系统的高可用呢?**
## 解决思路
你可以看到,前面这种监控方式,它是碎片化和人工化的,它由不同的工具负责监控系统的不同部分,并且需要大量专业的人介入,并通过反复的沟通,才能把相关的信息拼接起来,最后定位到问题。
**那我们能不能把系统所有的监控信息自动关联起来,并且以一种直观的方式展示,让所有人一看就明白是哪里出了问题,以及出问题的原因是什么呢?**
从这个思路出发,对系统的监控,我们需要做到两点:
1. 系统能够自动地判断每个节点是否正常,并直观地给出结果,不需要经过专业人员的分析。
1. 系统能够自动把各个节点的监控信息有机地串起来,从整体的角度对系统进行监控,不需要很多人反复地进行沟通。
这里,我们可以借鉴一下道路交通监控的例子。
我们经常可以在市内的高架上看到交通拥堵示意图。在下面的这张交通信息图上,你可以看到,每条道路都通过上下左右不同的方位,有机地关联在一起,形成一个整体的交通网络;同时,在交通图上,通过红黄绿三种状态,实时地反映了每条道路的拥堵情况。这样,司机就可以非常直观地了解道路是否畅通,从而提前避开拥堵路段。
<img src="https://static001.geekbang.org/resource/image/fb/13/fba62d78cffa6e8ba1d1951e133edc13.jpg" alt="">
这里有几个关键词:实时、直观、整体。下面,我们就来对照下软件系统的监控,来看看要想实现类似的监控效果,我们应该怎么做。
首先要**实时**,我们需要第一时间知道系统当前是否有问题。
然后要**直观**,节点是否有问题,我们需要很直观地就能判断出来,就像交通图上的红黄绿颜色标识一样。我们知道,在发生紧急事故时,人脑很可能会处于错乱状态,这个时候,我们一定不能指望专业的头脑或者严密的分析来判断问题,这样不但慢,而且很容易出错。所以,系统哪些部分有问题,问题是否严重,以及出问题的大致原因是什么,这些信息,监控系统都必须能够直观地给出来。
最后是**整体**,我们需要针对系统做整体监控,就像交通图一样,它是针对周边整体的道路情况进行展示,我们也需要把系统的各个节点放在一起,清晰地给出节点依赖关系。系统真正出问题的地方往往只有一个,其他地方都是连带的,如果监控系统能够给出节点的上下游依赖关系,对于定位真正的问题源是非常有用的。
所以,对照道路交通监控的思路,我们可以采取这样的监控方式:
- 首先,系统中的每个节点对应交通图的一条道路;
- 然后,节点的健康状况对应道路的拥堵情况,节点同样也有红黄绿三种不同的颜色,来展示该节点是否正常;
- 最后,节点之间的调用关系对应道路的方位关系。
这样我们就能构建一个实时的、直观的、一体化的监控系统,类似交通图一样,可以一眼就看出系统的问题所在。
好,回到刚才事故处理的例子,如果我们的监控系统按照这种方式来设计,它的监控效果会是什么样的呢?
首先所有的节点,包括服务端应用、下单服务、会员服务还有其他服务,以及它们各自用到的缓存、消息队列和数据库,这些节点的健康状态我们在一个页面里就可以看到,包括它们的依赖关系。
如果会员数据库出了问题,我们根据依赖关系倒推,会员数据库-&gt;会员服务-&gt;下单服务-&gt;服务端应用这4个节点都会爆红而其他节点不受影响保持绿色。服务端应用和下单服务节点会有错误消息提示接口调用超时而会员服务和会员数据库节点的错误消息提示的是数据库连接超时。
这样其他绿色的节点我们就不用排查了然后我们观察爆红的节点通过上下游依赖关系就知道最终的问题很可能出在会员数据库上DBA重点检查会员数据库就可以了。当数据库问题解决以后我们可以看到所有爆红的节点马上变绿立即就能确认系统恢复了正常。
## 架构方案和效果
根据前面的思路,我们设计了监控系统的整体架构,如下图所示:
<img src="https://static001.geekbang.org/resource/image/a4/d2/a469d19331860e8d4084279c422753d2.jpg" alt="">
每个被监控的节点均有对应的Agent负责采集健康数据不同的节点类型数据采集的方式也不一样Web节点通过HTTP接口调用Redis通过JredisMQ也通过对应的API接口DB则采用JDBC。
Agent每隔3s采集节点数据然后上报数据给Monitor ServiceMonitor Service负责确定节点当前的状态并保存到数据库这样就完成了节点健康状态的检测最后前端Dashboard每隔3s拉取所有节点的状态以红黄绿三种颜色在同一页面展示同时还会显示具体的出错信息。
**那我们是根据什么规则来判断节点的健康状态呢?**
这里我以DB为例简单说明一下。Agent每隔3秒会去尝试连接数据库并进行简单的表读写操作如果连接和读写都能够成功那就说明该DB当前的运行是正常的相应的在Dashboard里面这个DB节点会显示为绿色。
Redis和MQ类似我们主要也是检测组件的可用性Web应用的健康规则会相对复杂一些我们会结合Web应用接口的功能和性能来做综合判断。这个监控系统的设计我还会在下一讲里具体介绍你到时候可以深入理解其中的细节。
我们最后来看下监控的效果。
下图是某个业务系统的实际监控效果图左边是系统的部署架构最上面是两个Web应用这两个应用分别有自己的Web服务器、MQ和Redis节点。
>
提示:这里,我对细节做了模糊化处理,不过没关系,我主要的目的是让你能了解监控的效果,尽管图片模糊,但它不会影响你理解内容。
<img src="https://static001.geekbang.org/resource/image/d6/41/d67b89c71a3888068ffc2559ef659141.jpg" alt="">
以左上角的应用为例它的Web应用部署在Docker里面所以这里只显示一个节点虚拟机部署可以看到每个实例的IP但Docker容器无法看到对外表现为一个地址对于Redis我们是购买公有云的服务所以也是一个实例但MQ是集群的方式它有三个实例。
然后这两个Web应用同时依赖后端的3个基础服务这3个服务是并列的关系每个服务又分别有自己的应用、MQ和Redis。所以你可以看到在这个监控页面里节点的部署情况和依赖关系都是一目了然的。
在这个例子中有一个节点显示为黄色黄色说明它有问题但并不严重。你可以在右边的异常消息列表里看到具体的原因在最近3s内这个Web应用的接口响应时间超过了正常值的5倍每条异常消息包括了出错的节点、具体出错的接口、该接口的正常响应时间以及当前的响应时间。这样你就可以很方便地把左边的出错节点和右边的异常消息对应起来知道哪些节点有错误还有出错的原因是什么。
另外如果你在左边的图里点击某个节点会弹出新页面显示该节点的历史出错信息并且新页面里有链接可以直接跳到Zabbix、CAT和ELK系统这样你可以在这些专门的系统里做进一步的排查。
所以说,这里的监控系统提供的是整体的监控信息,可以帮助你快速定位问题的根源,在很多情况下,我们通过这里给出的错误信息,就可以知道出错的原因。当然,如果碰到特别复杂的情况,你还是可以在这里快速关联到各个专业的监控系统去收集更深入的信息。
## 总结
今天,我与你介绍了一下监控的分类,你现在应该对监控有了比较深入的了解,知道一个完整的监控体系都包含了哪些内容。
此外,我也结合线上事故处理的例子,和你说明了碎片化的监控带来的一些问题,并给出了整体化的解决思路以及具体的落地方案。在实践中,这套监控系统也确实发挥了巨大的价值,让我们可以高效地应对线上事故,提升系统的可用性,希望你能够深入地领悟和掌握。
在下一讲中,我还会和你介绍这个方案的实现细节,这样,你也可以尝试着去落地类似的监控系统。
**最后,给你留一道思考题:** 你的公司都有哪些监控手段,当处理线上事故时,你遇到的最大的挑战是什么?
欢迎你在留言区与大家分享你的答案,如果你在学习和实践的过程中,有什么问题或者思考,也欢迎给我留言,我们一起讨论。感谢阅读,我们下期再见。

View File

@@ -0,0 +1,176 @@
<audio id="audio" title="15 | 高可用架构案例(三):如何打造一体化的监控系统?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/29/81/29812685b70f4fa0f2af764496c11a81.mp3"></audio>
你好,我是王庆友。
上一讲,我与你介绍了整体化监控系统的设计方案,今天我就带你深入它的内部设计,让你可以了解它具体是如何落地的。
这个监控系统主要分为4大部分节点信息采集、节点接入、数据上报和前端展示。下面我就来为你具体展开介绍。
## 节点信息采集
在上一讲中我提到过Agent负责采集节点的健康数据每隔3s主动访问一次然后Agent会根据这些数据结合相应的规则来判断节点的健康状态。最终的健康状态有三种分别是错误、警告和正常这三种状态也对应了Dashboard中节点的红黄绿三种颜色。
节点分为4类Web应用、Redis、MQ和数据库。下面我就来具体讲一下系统是如何对它们进行监控的。
- **对于Redis节点**Agent通过Jredis API尝试连接Redis实例并进行简单的读写。如果这个操作没有问题就表明Redis当前的健康状态是正常的否则就是错误的。
- **对于MQ节点**Agent是通过MQ API来检测MQ节点是否有活跃的消费者在连接同时检测队列积压的消息数量。如果没有活跃的消费者或者未消费的消息超过了预设的值就表明当前的MQ节点的健康状态是错误的否则它就是正常的。
- **对于数据库节点**Agent是通过JDBC去连接数据库并对表进行简单的读写。如果操作成功表明数据库的健康状态是正常的否则就是错误的。
**对于这三类节点,它们的健康状态只有正常和错误两种,没有警告状态。**如果节点有问题Agent会同时给出具体的出错信息比如节点连接错误、积压消息过多等等。
**对于Web应用来说**Agent采集的方式则稍微复杂一些它会同时采集应用的功能和性能数据具体包括最近3s的接口调用次数、接口平均响应时间、接口出错次数、节点的健康状态和错误消息。
这里我给你举一个Web节点请求和响应的例子来帮助你直观地了解Agent是如何采集数据的。
```
请求http://10.10.1.1/agent/check
返回信息:
&quot;status&quot;:“warning&quot;,
&quot;avg_time&quot;:“583.0&quot;,
&quot;call_count&quot;:&quot;10&quot;,
&quot;error_count&quot;:&quot;0&quot;,
&quot;error_info&quot;:&quot; orderListGet: current average time= 583.0, total average time =109.84, 调用次数= 10&quot;
```
Web节点会预先提供一个HTTP接口Agent通过调用这个接口返回当前Web实例最近3s的健康状态。
这里最主要的就是 **status字段**它表明了Web节点最近3s是否健康如果是“error”或者“warning”返回的结果还会包含 **error_info字段**,它负责给出具体的错误信息。
Agent在获取了这4类节点的健康状态后会调用Monitor Service进行数据上报如果节点有问题上报内容还包括具体的错误消息。
总体的架构如下图所示:
<img src="https://static001.geekbang.org/resource/image/23/92/23c7549bd553bab3b142aec6d5525792.jpg" alt="">
**你要注意的是**Agent本身是一个独立的应用它不需要和节点部署在一起如果节点数量少我们部署一个Agent实例就可以如果节点的数量比较多我们可以部署多个Agent实例比如给每类节点部署一个实例。总的要求就是让Agent能够在3s内完成所有节点的健康信息收集就可以了。
另外节点的连接信息事先是配置在数据库里的比如数据库节点的IP端口、账号和密码等等当Agent启动的时候它会通过Monitor Service获取节点配置信息Agent在运行过程中也会定期刷新这个配置。
## 接入监控系统
好,说完了节点信息的采集,下面我们来看下,这些节点要接入监控系统,都需要做些什么。
对于Redis、MQ、DB这三类节点接入监控系统只需要提供配置信息就可以了无需额外的开发。
而对于Web应用接入监控我们需要对应用代码做些改造
1. 针对每次接口调用,应用程序需要在接口代码中记录本次调用的耗时以及出错状况;
1. 应用程序需要汇总最近3秒的接口调用情况根据规则给出节点的健康状态
1. 应用程序提供一个对外的HTTP接口供Agent来获取上一步给出的健康状态。
为了方便Web应用的接入监控系统开发团队提供了SDK它内置了接口调用信息的统计和健康计算规则。应用程序借助SDK就可以给Agent提供最终的健康结果也就是说SDK帮助应用完成了最复杂的第二步工作。
所以,对应用来说,它接入监控系统是非常简单的。
首先在每个应用接口中调用SDK提供的 **logHeahthInfo方法**,这个方法的输入包括了接口名字、本次接口调用耗时和错误信息,这和我们平常接入日志系统是很类似的。
```
try{
result = service.invoke(request)
HealthUtil.logHealthInfo(&quot;xxx_method&quot;,
(System.currentTimeMillis() - start)null);
}catch (Exception e){
HealthUtil.logHealthInfo(&quot;xxx_method&quot;,
(System.currentTimeMillis() - start)
e.getMessage());}
```
然后应用提供一个额外的HTTP接口在接口中直接调用SDK内置的 **healthCheck**方法给Agent提供最终的健康信息。这些就是应用接入监控系统要做的全部事情。
```
@RequestMapping(value = &quot;/agent/check&quot;)
public String reportData(){
return HealthUtil.healthCheck();
}
```
我们可以看到,**SDK通过在接口方法中进行埋点可以收集每次接口的调用情况那它最终是怎么计算出当前节点的健康状况呢**
SDK的内部实际上是一个HashMap结构它的key就是Web应用的各个接口名字它的value是一个简单的对象包含这个接口最近3s总的调用数量、总的出错次数和总的耗时等。当每次Web应用有接口调用时我们在HashMap内部根据接口名字找到对应的value然后增加这三个数值就完成了接口调用数据的收集。
当Agent调用HTTP接口拉取节点健康数据时SDK会计算节点的健康状况具体规则如下
- 如果最近3s接口调用没有发生错误节点的健康结果就是正常如果出错次数在1到5之间健康结果就是警告如果大于5健康结果就是错误。
- 如果最近3s接口响应时间超过正常值的10倍健康结果就是错误如果在5倍到10倍之间健康结果就是警告否则结果就是正常。
这里有个问题,**接口调用响应时间的正常值是怎么来的呢?**这个值不是预先设置的我们知道如果预先设置的话这个数字很难确定。这里的正常值其实是SDK自动计算的SDK会记录应用从启动开始到目前为止接口的总耗时和总调用次数然后得出平均的响应时间作为接口调用的正常耗时总调用次数和总耗时也记录在HashMap的value里
你可以看到Web应用的健康状态判断是结合了应用的功能和性能的两者是“或”的逻辑关系只要某一项有问题健康结果就是有问题。比如说最近3s接口功能没出错但耗时是正常的10倍以上SDK就会认为节点的健康状态是错误的。
**值得注意的是**SDK会针对每个接口进行分别计算最后取最差接口的结果。比如说应用有10个接口如果其中8个接口是正常状态1个接口是警告状态1个接口是错误状态那么该应用的健康结果就是错误状态。
还有一点SDK在HashMap内部不会记录每个接口调用的详细日志而是只维护几个简单的总数值因此SDK对应用的内存和CPU影响都可以忽略不计。
## 前端信息展示
现在监控数据已经通过Agent和Monitor Service保存到数据库了前端的Dashboard通过调用Monitor Service接口就可以获取所有节点的最新健康状态Dashboard也是每3s刷新一次页面。接下来我们就要考虑**如何在Dashboard里展示节点健康状态这影响到我们能否直观地定位系统的问题。**
- 首先一个应用一般有多个实例比如Web应用很可能部署了多个实例
- 然后应用之间有上下游依赖关系比如Web应用依赖Redis和数据库。
我们在页面中,就需要把所有这些信息直观地体现出来,这对我们判断问题的源头很有帮助。
这里的页面显示有两种实现方式。
一种是页面定制的方式,我们把应用有哪些节点,以及应用的上下游依赖关系,在前端代码里固定死。但问题是,如果系统的部署有变动,页面就要重新调整。在我们的监控实践中,我们要监控很多套系统,这样我们就需要为每个系统定制页面,初始的工作量就很大,更加不用说后续的调整了。
所以,在实践中,我们采取了一种更加灵活的前端展现方式,能够通过一套前端代码,灵活地展示系统的节点以及依赖关系,效果上也非常直观。
它的具体实现方式是我们把页面的展示内容分为三个层次分组、应用和节点。一个页面代表一个系统它包含多个分组一个分组包含多个应用一个应用包含多个节点节点代表了一个具体的实例有独立IP
这里的分组实际上是对应用进行归类,比如说,共享服务是一个分组,它内部包含多个服务,这些服务是并列的关系。这样,我们通过分组在页面里的位置关系,来体现应用之间的上下游依赖关系。
如下图所示,红色圈里的是各个分组,蓝色圈里是各个应用。我们可以很清晰地看到,“应用层”分组里的会员应用,会调用“依赖服务”分组里的四个服务。
<img src="https://static001.geekbang.org/resource/image/66/4e/663f7ce00874ec02826e7f9d12c9844e.jpg" alt="">
这里你可以发现“应用层”分组里只有1个应用它采取了1行1列的布局而“依赖服务”分组里有四个服务它采用的是2行2列的布局。**那么这个布局是怎么实现的呢?**
首先布局是在后台定义的保存在数据库里。我们为每个系统预先设定好布局类似HTML里的Table布局语法行用TR表示列用TD表示。我们根据页面显示要求提前确定好分组和应用会占用多少行多少列。前端通过Monitor Service的接口获取页面的布局信息然后根据布局信息进行动态展示如果系统的部署有变化我们在管理后台调整布局就可以了非常灵活。
这样我们通过类似Table方式的布局前端通过一套代码就可以满足所有系统的节点展示需求并且能够比较好地体现应用之间的上下游依赖关系当系统有问题时我们就可以很直观地判断出问题的根源在哪里。
在前面,我说的是一个页面代表一个系统,其实我们也可以对所有系统的节点做一个整体的**大盘监控**,这样我们只需要看一个大盘页面,就可以监控所有的节点,如下图所示:
<img src="https://static001.geekbang.org/resource/image/54/01/5491914f7cf8f8a62d5087315a437701.jpg" alt="">
大盘监控具体的实现方式是这样的:
- 首先,前端页面读取所有节点的健康状态,按照节点分类展示有问题的节点,并标识出相应的颜色;
- 然后,节点的具体出错信息也可以在大盘中展示;
- 最后,我们根据每个系统内部节点的健康状况,按照一定的规则,算出各个系统的总体健康状态,在页面展示系统的健康状态。
比如说一个系统,如果它下面有一个节点是错误状态,对应的系统状态就是红色的;超过两个节点是警告状态,对应系统状态就是黄色的。如果我们点击相应的系统节点,就会跳转到具体系统的监控页面中,我们可以进一步了解该系统内部各个节点的详细状态信息。
通过这个大盘监控,我们就能在一个页面里,知道当前哪些节点有问题、哪些系统有问题、具体出错信息是什么,我们平常监控这一个页面就可以了。
## 库表设计
最后我简单介绍下监控系统的数据库表设计主要的表有3张
<img src="https://static001.geekbang.org/resource/image/ea/23/ea3537c06a2bc48f3ca9f95679da9023.jpg" alt="">
1. **系统信息表**用来定义监控体系里有哪些系统其中Layout布局定义了该系统前端的布局方式。
1. **节点信息表**用来定义节点的配置信息其中节点类型可选的值有Web应用、Redis、MQ、DB等等节点类型决定了节点健康信息的获取方式。其他字段用于Agent如何去连接节点还有邮箱和手机用于节点出错时相应的人可以接收报警信息。
1. **节点监控状态表**用来记录每个节点的最新健康状态用于Dashboard显示。
到这里为止,我给你介绍完了整个系统的核心设计。从监控的层次来看,这个监控系统可以分为大盘级别监控-&gt;系统级别监控-&gt;节点级别监控你甚至还可以快速关联到每个节点的专门监控系统比如Zabbix的硬件监控、CAT的应用监控、ELK的日志监控等等实现最粗粒度到最细粒度监控的一体化。
相比较各个专门的监控系统我们这里不求对各类节点的监控做得多深入而是大致上能反映节点的健康状况即可如果我们要对组件做更深入的监控组件的API也可以为我们提供非常详细的信息。我们更强调的是要把系统的所有节点串起来直观地反映它们的健康状况避免监控系统的碎片化和专业化。
总而言之这个监控系统就相当于是一个全身体检不同于对某个器官的深入检查它是把系统的各个部位都做了初步检查并且给出了一个很容易阅读的结果报告。这个系统实现起来很简单但非常实用我们相当于用20%的成本实现了80%的监控效果。
## 总结
今天,我与你分享了一体化监控系统具体的设计细节,相信你已经非常清楚了它的内部实现机制,如果有需要,你也可以在实践中尝试落地类似的监控系统。
这里,我讲得比较细,不仅仅是为了让你理解这个监控系统是怎么设计的,而是想和你分享做架构设计时,我们要做全面深入的考虑,要简化开发的对接工作,要简化用户的使用,这样的架构设计才能顺利落地,实现预期的价值。
比如在这里我们为Web应用提供了SDK这降低了开发者的接入成本我们通过页面的动态布局设计避免了前端开发工作的定制化我们通过大盘监控以及和现有监控系统进行打通进一步方便了用户的使用全面提升监控系统的价值。
**最后,给你留一道思考题:** 你觉得在做架构设计时,最大的挑战是什么?
欢迎你在留言区与大家分享你的答案,如果你在学习和实践的过程中,有什么问题或者思考,也欢迎给我留言,我们一起讨论。感谢阅读,我们下期再见。

View File

@@ -0,0 +1,137 @@
<audio id="audio" title="16 | 高性能和可伸缩架构:业务增长,能不能加台机器就搞定?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/39/3b/39bf384157fe8735071dba40db77573b.mp3"></audio>
你好,我是王庆友,今天我来和你聊一聊如何打造高性能和可伸缩的系统。
在课程的[第11讲](https://time.geekbang.org/column/article/212066),我和你介绍了,技术架构除了要保证系统的高可用,还要保证系统的高性能和可伸缩,并且能以低成本的方式落地。在实践中呢,高性能、可伸缩和低成本紧密相关,处理的手段也比较类似,这里我就放在一起来给你讲解。
在实际的工作当中,我们一般会比较关注业务功能的实现,而很少关注系统的性能,所以我们经常会面临以下这些挑战:
- **系统的TPS很低只要流量一大系统就挂加机器也没用**
- **机器的资源利用率很低,造成资源严重浪费。**
我曾经就统计过公司云服务器的资源利用率结果让我非常意外有相当比例的服务器它们的CPU和内存平均利用率长期不到1%但与此同时这些系统整体的TPS只有个位数。这里你可以发现资源利用率低和系统性能低的现象同时并存很显然系统没有充分利用硬件资源它的性能有很大的优化空间。
所以今天,我就先来给你介绍一下常用的性能数据,让你建立起对性能的基本概念;然后,我会和你具体讲解实现系统高性能和可伸缩的策略,让你能在实践中灵活运用。
## 常用的性能数据
对于服务器来说1ms的时间其实不算短它可以做很多事情我在这里列了几个基础的性能数据你可以把它们看做是系统性能的基线。
<img src="https://static001.geekbang.org/resource/image/7f/83/7fb77d02fc24085534617eab95e2e283.jpg" alt="">
你可以看到内存的数据读取是SSD磁盘的10倍SSD磁盘又是普通磁盘的10倍一个远程调用的网络耗时是机房内部调用的1000倍一个分布式缓存访问相对于数据库访问性能也有数十倍的提升。
了解了这些常用的性能数据你就能对性能建立一个直观的认识有些时候我们采取一些简单的手段就能提升系统的性能。比如说如果磁盘的IO访问是瓶颈我们只要用SSD磁盘来代替机械硬盘就能够大幅度地提升系统的性能。
## 高性能的策略和手段
那么对于一个实际的业务系统来说情况就会复杂很多。一个外部请求进来需要经过内部很多的软硬件节点处理用户请求的处理时间就等于所有节点的处理时间相加。只要某个节点性能有问题比如数据库或者某项资源不足比如网络带宽系统整体的TPS就上不去。这也是在实践中很多系统TPS只有个位数的原因。
不同类型的节点,提升性能的方法是不一样的,概括起来,总体上可以分为三类。
### 加快单个请求处理
这个其实很好理解。简单来说,就是当一个外部请求进来,我们要让系统在最短的时间内完成请求的处理,这样在单位时间内,系统就可以处理更多的请求。具体的做法主要有两种:
1. **优化处理路径上每个节点的处理速度。**比如说,我们可以在代码中使用更好的算法和数据结构,来降低算法的时间和空间复杂度;可以通过索引,来优化数据库查询;也可以在高读写比的场景下,通过缓存来代替数据库访问等等。
1. **并行处理单个请求。**我们把一个请求分解为多个子请求内部使用多个节点同时处理子请求然后对结果进行合并。典型的例子就是MapReduce思想这在大数据领域有很多实际的应用。
### 同时处理多个请求
当有多个外部请求进来时,系统同时使用多个节点来处理请求,每个节点分别来处理一个请求,从而提升系统单位时间内处理请求的数量。
比如说我们可以部署多个Web应用实例由负载均衡把外部请求转发到某个Web实例进行处理。这样如果我们有n个Web实例在单位时间里系统就可以处理n倍数量的请求。
除此之外,在同一个节点内部,我们还可以利用多进程、多线程技术,同时处理多个请求。
### 请求处理异步化
系统处理请求不一定要实时同步,请求流量的高峰期时间往往很短,所以有些时候,我们可以延长系统的处理时间,只要在一个相对合理的时间内,系统能够处理完请求就可以了,这是一种异步化的处理方式。
典型的例子呢,就是通过消息系统对流量进行削峰,系统先把请求存起来,然后再在后台慢慢处理。
我们在处理核心业务时,把相对不核心的逻辑做异步化处理,也是这个思路。比如说下单时,系统实时进行扣库存、生成订单等操作,而非核心的下单送积分、下单成功发消息等操作,我们就可以做异步处理,这样就能够提升下单接口的性能。
**那么,在实践中,我们应该使用哪种方式来保障系统的高性能呢?**答案是,我们需要根据实际情况,把三种手段结合起来。
首先,我们要加快单个请求的处理。单节点性能提升是系统整体处理能力提升的基础,这也是我们作为技术人员的基本功。**但这里的问题是,节点的性能提升是有瓶颈的**我们不能超越前面说的基础操作的性能。至于把请求分解为多个小请求进行并行处理这个在很多情况下并不可行我们知道MapReduce也有使用场景的限制。
对多个请求进行同时处理是应对海量请求的强有力手段,如果我们能够水平扩展每一个处理节点,这样在理论上,系统处理请求的能力无限的。**而这里的问题是**对于无状态的计算节点我们很容易扩展比如说Web应用和服务但对于有状态的存储节点比如说数据库要想水平扩展它的处理能力我们往往要对系统做很大的改造。
至于异步化处理,在某些场景下是很好的提升系统性能的方式,我们不用增加机器,系统就能够完成请求的处理。**但问题是**,同步调用变成异步的方式,往往会导致处理结果不能实时返回,有时候会影响到用户体验,而且对程序的改造也会比较大。
所以,我们在考虑系统高性能保障的时候,首先需要考虑提升单个请求的处理速度,然后再考虑多请求的并发处理,最后通过异步化,为系统争取更长的处理时间。
具体的处理手段,根据业务场景的不同,我们需要做综合考虑,在满足业务的基础上,争取对系统改造小,总体成本低。
## 可伸缩的策略和手段
我们经常说,业务是可运营的,而实际上,系统也是可运营的。我们可以动态地调整系统软硬件部署,在业务高峰期增加软硬件节点,在业务低谷期减少软硬件节点,这就是系统的可伸缩能力。
系统的可伸缩也有两种实现方式。
### 第一个是节点级别的可伸缩
**对于无状态的节点,我们直接增减节点就可以了**。比如说订单服务白天我们需要10台机器来提供服务到了半夜由于单量减少我们就可以停掉部分机器。
如果做得好,我们还可以实现**弹性伸缩**让系统根据硬件的负载情况来确定机器的数量。比如说当服务器的CPU或内存使用率在10%以下了,系统就自动减少服务实例的数量。
**而对于有状态的服务,我们需要能够支持状态数据的重新分布。**比如进行水平分库的时候要从4个库增加到8个库我们需要把原先4个库的数据按照新的分库规则重新分布到8个库中。如果这个调整对应用的影响小那系统的可伸缩性就高。
### 第二个是系统级别的可伸缩
我们知道,系统是一个整体,如果只是节点级别的伸缩,我们可能要对多个节点分别进行操作,而且不同节点的资源配置会相互影响,这样对各个节点的调整就非常复杂,影响了系统的可伸缩能力。**如果能实现系统端到端的伸缩,同时对多个节点进行伸缩处理,那系统的可伸缩能力就更高了。**
所以这里,我们可以把多个处理节点打包在一起,形成一个处理单元。
举个例子针对交易场景我们可以把商品浏览、加购物车、下单、支付这几个节点放一起形成一个逻辑上的单元在单元内部形成调用的闭环。具体使用的时候我们可以按照用户维度来划分交易单元。比如说让交易单元A处理用户ID对2取模为0的用户下单流程交易单元B处理用户ID对2取模为1的用户下单流程。
这样我们对一个整体的交易单元进行扩容或者缩容每增加一个交易单元就意味着同时增加商品浏览、加购物车、下单、支付4个节点这4个节点的处理能力是匹配的。你可以参考下面的这张交易单元化的示意图
<img src="https://static001.geekbang.org/resource/image/d6/54/d655e5fd2ff1c4c72dfae0779aa9ca54.jpg" alt="">
**通过单元化处理,我们把相关的节点绑定在一起,同进同退,更容易实现系统的可伸缩。**
而如果我们把单元扩大到系统的所有节点,这就是一个**虚拟机房**的概念。我们可以在一个物理机房部署多个虚拟机房,也可以在不同的物理机房部署多个虚拟机房,这样,部署系统就像部署一个应用一样,系统的可伸缩性自然就更好。
## 高性能和可伸缩架构原则
说完了高性能和可伸缩的策略,接下来,我再说下具体的架构设计原则,让你能够在实践中更好地落地。
- 可水平拆分和无状态
这意味着节点支持多实例部署,我们可以通过水平扩展,线性地提升节点的处理能力,保证良好的伸缩性以及低成本。
- 短事务和柔性事务
短事务意味着资源锁定的时间短,系统能够更好地支持并发处理;柔性事务意味着系统只需要保证状态的最终一致,这样我们就有更多的灵活手段来支持系统的高性能,比如说通过异步消息等等。
- 数据可缓存
缓存是系统性能优化的利器如果数据能够缓存我们就可以在内存里拿到数据而不是通过磁盘IO这样可以大大减少数据库的压力相对于数据库的成本缓存的成本显然也更低。
- 计算可并行
如果计算可并行我们就可以通过增加机器节点加快单次请求的速度提高性能。Hadoop对大数据的处理就是一个很好的例子。
- 可异步处理
异步处理给系统的处理增加了弹性空间,我们可以利用更多的处理时间,来降低系统对资源的实时需求,在保证系统处理能力的同时,降低系统的成本。
- 虚拟化和容器化
虚拟化和容器化是指对基础资源进行了抽象这意味着我们不需要再依赖具体的硬件对节点的移植和扩容也就更加方便。同时虚拟化和容器化对系统的资源切分得更细也就说明对资源的利用率更高系统的成本也就更低。举个例子我们可以为单个Docker容器分配0.1个CPU当容器的处理能力不足时我们可以给它分配更多的CPU或者增加Docker容器的数量从而实现系统的弹性扩容。
实现了系统的高性能和可伸缩,就表明我们已经最大限度地利用了机器资源,那么低成本就是自然的结果了。
## 总结
在课程的开始,我给出了一些基础的性能指标,在具体的业务场景中,你就可以参考这些指标,来评估你当前系统的性能。
另外,我还分别针对系统的高性能和可伸缩目标,介绍了它们的实现策略和设计原则,在工作中,你可以根据具体的业务,由易到难,采取合适的手段来实现这些目标。
在实践中呢,实现高可用和可伸缩的手段也是多种多样的,接下来的课程中,我还会通过实际的案例,来具体说明实现这些目标的有效手段,帮助你更好地落地。
**最后,给你留一道思考题**:在工作中,你都采取过哪些手段保证了系统的高性能和可伸缩呢?
欢迎在留言区和我互动,我会第一时间给你反馈。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢阅读,我们下期再见。

View File

@@ -0,0 +1,100 @@
<audio id="audio" title="17 | 高性能架构案例:如何设计一个秒杀系统?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d3/d4/d395a62a8d50780234331c6fe5877cd4.mp3"></audio>
你好,我是王庆友。
在上一讲中我和你详细介绍了打造一个高性能系统的应对策略和架构手段。那么今天我就以1号店的秒杀系统为例来具体说明如何实现一个高性能的系统。
## 背景和问题
先说下背景。在2014年的时候1号店作为网上超市类电商经常在线上举行各种大促活动。比如进口牛奶促销活动每次促销的牛奶有几十万盒促销价格非常优惠一般这样的促销活动会在某个整点的时间进行开卖如上午10点。对于这种质高价优并且是刚需的商品会有大量的用户来抢购俗话说“手快有手慢无”往往短短几分钟内所有牛奶就能售卖完毕。
这本质上是一种秒杀活动,但商品数量非常大,一瞬间会有大量的用户流量涌入,流量可以高达平时的几十倍。而且和少量商品的秒杀不同,这些都是有效流量,最终会生成订单。
而在正常情况下系统因为资源有限只能处理10%的流量无法处理剩下的90%流量,瞬间高并发的流量涌入,很大程度上会引起后台系统超时报错,导致用户下单不成功。这样一来,用户就会反复刷新页面,多次尝试下单,不但用户的体验不好,而且系统的压力会更大。
**最终的结果就是,系统往往由于过载,整体处理能力下降,甚至瘫痪,导致所有用户都无法购买。**就像下图表示的一样,在秒杀场景下,系统会面临这样的困境:
<img src="https://static001.geekbang.org/resource/image/e9/31/e933d73f5e41c6ef89080cf56be90031.jpg" alt="">
在这种情况下,对于用户来说,能不能买到商品,拼的是体力和人品,由于体验不好,用户会逐渐对活动失去兴趣;而对于系统来说,我们需要拼命地加机器来满足峰值流量。
每次1号店要进行大促的时候在活动开始前运营和技术人员会坐在一起大家一起来预估活动的峰值流量然后技术人员做评估系统的哪些节点需要加机器以及要加多少机器。但这样的做法其实存在几个问题
- 首先,我们对峰值流量的预估以及要加多少机器都是拍脑袋的,和实际出入往往很大,一旦估计少了,系统同样会面临过载的风险;
- 其次,为了短暂的几分钟促销,我们需要增加大量的机器,事先要做很多的运维准备工作,不但浪费资源,而且效率很低;
- 最为关键的是,有些处理节点,系统不是通过加机器就能扩展处理能力的,比如商品库存数据库,下单时,我们需要扣库存,而为了防止库存更新冲突,我们需要锁定库存记录,导致系统的并发处理能力有限,这个问题单靠加机器是解决不了的。
## 总体方案
对于这种高并发情况,看来让系统单纯地通过加机器去硬扛,是不可行的。**那么,我们有没有更好的办法,既保证用户体验,又保证系统能够轻松地应对流量挑战呢?**
我们先来深入分析下业务场景。这个秒杀活动的特点是在短期的1~2分钟内用户流量很大但只要促销的商品卖完流量马上恢复常态。所以对于前端短期内这么大的下单请求后端如果实时处理压力会非常大但如果把这个处理时间延长到10分钟后端是可以完成下单的。那对用户来说商品优惠的力度这么大他们关心的是能否买到所以会愿意多等一段时间而不是在页面上一次次点击下单每次系统都提示下单失败。
当然,如果我们把订单处理的时间延长了,只要我们在前台告诉用户,系统已经接受了他们的订单,并且不断同步用户订单处理的进度,用户体验的问题其实也不大。
基于这个分析,我们就可以利用**异步处理**的思路来应对秒杀活动。
我们先在前端接收用户所有的下单请求但不在后端实时生成订单而是放在队列里然后系统根据后端订单中心的实际处理能力从队列里获取订单请求再交给订单中心生成实际的订单。同时系统告诉用户当前的处理进度有多少订单排在TA的前面TA还要等多久。
这样对于用户来说,在前台下单一次就可以了,然后等系统慢慢处理,这也符合先到先得的原则,非常公平合理。对系统来说,只要根据大促的商品总量,一定程度上增强系统处理能力,保证下单请求从进来到最后处理完成,这个时间相对合理就可以了。
比如说有20万件的商品每人限购一件预计用户会在2分钟内完成下单但用户能够接受系统在20分钟内完成订单处理。这样系统只要保证每分钟能处理1万订单就行而如果不采取排队的方式系统就需要每分钟处理10万订单它的压力就会提升一个数量级。
基于排队的思路,系统总体架构设计如下图所示:
<img src="https://static001.geekbang.org/resource/image/6a/7f/6a809c9da997868271c2b493cb4f397f.jpg" alt="">
在这个架构中,我们**在前台和后台下单系统之间,新增了排队系统,它包括排队区和处理区两个部分。**系统整体的处理过程是这样的:
1. 用户在商品详情页提交订单后,这个订单会作为预订单进入排队区,同时排队系统会返回用户一个排队编号,这个编号用于跟踪后续的订单处理进度;
1. 用户被引导到一个等待页,这个页面会根据排队号,定时地查询排队系统,排队系统会返回预订单在队列中的位置信息,包括它前面还有多少未处理的预订单,以及后台系统大概还要多久会处理这个预订单,这样用户就不会焦虑;
1. 在排队系统的处理区,有很多消费者,它们依次从排队区的队列里获取预订单,然后调用后台下单系统生成实际的订单;
1. 随着预订单变成正式的订单,队列里的预订单会逐渐变少,如果当前的预订单已经从队列里被移除了,用户的等待页就会检测到这个情况,页面自动跳转到订单完成页,这就和常规的购物流程一样了,用户进行最后的支付,最终完成整个前台下单过程。
这里,你可以看到,**前台**的预订单有瞬时的大流量,但我们只是把它们放到队列里,这个处理起来很快,排队系统可以轻松应对;而**后台**生成实际的订单是匀速的,并且最大化地发挥了下单系统的处理能力。另一方面,对于用户体验来说,用户可以选择在等待页等候,实时获取订单处理进度的反馈,也可以选择离开,然后在用户中心的“待支付订单”里完成支付。通过这样的设计,排队系统既保证了系统处理订单的能力,也保证了用户良好的体验。
下面是一张用户等待页的效果图,你可以直观地了解秒杀系统的用户体验。
<img src="https://static001.geekbang.org/resource/image/c8/30/c8e4840b1e3064ecf84b6c77d38b9630.jpg" alt="">
现在,你已经了解了秒杀系统的总体设计。接下来,我深入介绍下这个排队系统的内部设计细节,帮助你更好地理解它。
## 内部设计
首先,**针对队列的技术选型**,排队系统使用的是**Redis**而不是MQ。因为相对于MQ来说Redis更轻量级性能更好它内置了队列数据结构除了和MQ一样支持消息的先进先出以外我们还可以获取队列的长度以及通过排队号获取消息在队列中的位置这样我们就可以给前端反馈预订单的处理进度。
对于秒杀场景来说,一个订单只能包含一个商品,这里我们**为每个秒杀商品提供一个单独的队列**这样就可以分散数据在Redis中的存取多个队列可以提供更好的性能。
**关于队列的调度问题**,也就是消费者优先从哪个队列里拿预订单,排队系统会结合下单时间和队列的长度来确定,以保证用户合理的时间体验。比如说,某个秒杀商品的队列很长,消费者会优先从这个队列拿预订单,从而避免用户等待太长的时间。
**关于队列长度**为了保证用户能够买到商品我们并不是把所有前台的下单请求都会放到队列里而是根据参与活动的秒杀商品数量按照1:1的比例设置队列初始长度这样就保证了进入队列的请求最终都能生成订单。
这个可用队列长度会随着预订单进入队列不断地减少当数值变为0时下单前台会拒绝接受新请求进入队列直接反馈用户下单失败。当然如果后台订单生成异常或用户取消订单后可用队列长度会增加前台会重新开放预订单进入队列。
## 更多优化:建立活动库存
除了秒杀流程,系统还有**常规的购物流程**,这两个购物方式都是从详情页开始,到订单完成页结束。不同的地方是,常规购物流程走的是购物车和结算页,系统是同步处理的,这样可以有更好的用户体验。
在这里,我们在系统设计上,可以很好地同时支持秒杀流程和常规购物流程。
如果运营人员在后台上架商品的时候,设置这是一个秒杀商品,那么从详情页开始,系统就会引导用户走秒杀流程,否则就走常规购物流程。特别是在早期秒杀系统刚落地的时候,如果发现秒杀流程有问题,我们还可以快速切回到常规的购物流程,实现了一定程度上的系统互备。
<img src="https://static001.geekbang.org/resource/image/74/75/74d1a5c896d5f21f42c202f4f3d3bc75.jpg" alt="">
此外,对于秒杀活动来说,参与活动的商品种类是有限的,但这些商品库存的扣减非常频繁,因此我们建立了**活动库存**的概念,把少量参与促销的商品种类单独放在一个库里,避免和大量常规的商品放在一起,这样也大幅度地提高了库存数据库的读写性能。
好了,通过这个秒杀系统的架构设计,你可以看到,我们巧妙地通过请求的异步化处理,对流量进行削峰,从而保证了系统的高性能。这里我们不需要增加太多的机器,在系统落地时,我们通过排队系统对前后台解耦,后台下单系统基本上也不需要修改,系统整体改造的工作量不大,整个落地过程也非常顺利。
不过值得注意的是,**这种方式比较适合瞬时有高并发流量的场景**,比如这里说的秒杀场景。如果订单高峰会持续一段较长的时间,而用户对订单处理又有比较高的时间要求,那就不适合采用这种异步削峰的方式。
举个例子,外卖订单的午高峰通常会持续两个小时,而用户普遍期待订单半小时能够送达。对于这种情况,我们就需要正面应对高峰流量,比如通过水平扩展各个节点,提升系统的处理能力。这也要求系统能够做到弹性伸缩,高效地支持资源的缩容或扩容,节省成本。
## 总结
今天我针对1号店的大促业务挑战与你分享了一个秒杀系统的具体设计对照我在上一讲中介绍的高性能应对策略秒杀系统主要使用了**异步化处理**的方式,这也符合实际的业务场景。
通过今天的分享,相信你对如何保障系统的高性能有了更深入的体会,如果你也有类似的瞬时高并发的场景,你也可以在实践中参考这里的做法。
**最后,给你留一道思考题:** 你的公司业务上有高并发的场景吗,系统是如何应对的呢?
欢迎给我留言,我会及时给你反馈。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢你的阅读,我们下期再见。

View File

@@ -0,0 +1,185 @@
<audio id="audio" title="18 | 可伸缩架构案例:数据太多,如何无限扩展你的数据库?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/62/80/62dee8d0c7359f3ce1d21c34c5184180.mp3"></audio>
你好,我是王庆友。在[第16讲](https://time.geekbang.org/column/article/217152)中我和你介绍了很多可伸缩的架构策略和原则。那么今天我会通过1号店订单水平分库的实际案例和你具体介绍如何实现系统的可伸缩。
## 问题和解决思路
2013年随着1号店业务的发展每日的订单量接近100万。这个时候订单库已有上亿条记录订单表有上百个字段这些数据存储在一个Oracle数据库里。当时我们已经实现了订单的服务化改造只有订单服务才能访问这个订单数据库但随着单量的增长以及在线促销的常态化单一数据库的存储容量和访问性能都已经不能满足业务需求了订单数据库已成为系统的瓶颈。所以对这个数据库的拆分势在必行。
数据库拆分一般有两种做法,一个是垂直分库,还有一个是水平分库。
- **垂直分库**
简单来说,垂直分库就是数据库里的表太多,我们把它们分散到多个数据库,一般是根据业务进行划分,把关系密切的表放在同一个数据库里,这个改造相对比较简单。
- **水平分库**
某些表太大,单个数据库存储不下,或者数据库的读写性能有压力。通过水平分库,我们把一张表拆成多张表,每张表存放部分记录,分别保存在不同的数据库里,水平分库需要对应用做比较大的改造。
<img src="https://static001.geekbang.org/resource/image/6a/81/6a414d387a08a6dc291c3a3a9e763c81.jpg" alt="">
当时1号店已经通过服务化实现了订单库的**垂直拆分**它的订单库主要包括订单基本信息表、订单商品明细表、订单扩展表。这里的问题不是表的数量太多而是单表的数据量太大读写性能差。所以1号店通过**水平分库**把这3张表的记录分到多个数据库当中从而分散了数据库的存储和性能压力。
水平分库后,应用通过订单服务来访问多个订单数据库,具体的方式如下图所示:
<img src="https://static001.geekbang.org/resource/image/7c/9f/7cf1df5c241cd515d5e89456d2a7f39f.jpg" alt="">
原来的一个Oracle库被现在的多个MySQL库给取代了每个MySQL数据库包括了1主1备2从都支持读写分离主备之间通过自带的同步机制来实现数据同步。所以你可以发现**这个项目实际包含了水平分库和去Oracle两大改造目标。**
## 分库策略
我们先来讨论一下水平分库的具体策略,包括要选择哪个分库维度,数据记录如何划分,以及要分为几个数据库。
### 分库维度怎么定?
首先,我们需要考虑根据哪个字段来作为分库的维度。
这个字段选择的标准是尽量避免应用代码和SQL性能受到影响。具体地说就是现有的SQL在分库后它的访问尽量落在单个数据库里否则原来的单库访问就变成了多库扫描不但SQL的性能会受到影响而且相应的代码也需要进行改造。
具体到订单数据库的拆分,你可能首先会想到按照**用户ID**来进行拆分。这个结论是没错,但我们最好还是要有量化的数据支持,不能拍脑袋。
这里最好的做法是先收集所有SQL挑选出WHERE语句中最常出现的过滤字段比如说这里有三个候选对象分别是用户ID、订单ID和商家ID每个字段在SQL中都会出现三种情况
1. 单ID过滤比如说“用户ID=?”;
1. 多ID过滤比如“用户ID IN(?,?,?)”;
1. 该ID不出现。
最后我们分别统计这三个字段的使用情况假设共有500个SQL访问订单库3个候选字段出现的情况如下
<img src="https://static001.geekbang.org/resource/image/26/9a/26480ace17c1629c24f5881f65f2fa9a.jpg" alt="">
从这张表来看结论非常明显我们应该选择用户ID来进行分库。
不过,等一等,这**只是静态分析**。我们知道每个SQL访问的频率是不一样的所以我们还要分析每个SQL的实际访问量。
在项目中我们分析了Top15执行次数最多的SQL 它们占总执行次数85%具有足够代表性按照执行的次数如果使用用户ID进行分库这些SQL 85%会落到单个数据库13%落到多个数据库只有2%需要遍历所有的数据库。所以说,**从SQL动态执行次数的角度来看**用户ID分库也明显优于使用其他两个ID进行分库。
这样通过前面的量化分析我们知道按照用户ID分库是最优的选择同时也大致知道了分库对现有系统会造成多大影响。比如在这个例子中85%的SQL会落到单个数据库那么这部分的数据访问相对于不分库来说执行性能会得到一定的优化这样也解决了我们之前对分库是否有效果的疑问坚定了分库的信心。
### 数据怎么分?
好,分库维度确定了以后,我们如何把记录分到各个库里呢?
一般有两种数据分法:
1. **根据ID范围进行分库**比如把用户ID为1 ~ 999的记录分到第一个库1000 ~ 1999的分到第二个库以此类推。
1. **根据ID取模进行分库**比如把用户ID mod 10余数为0的记录放到第一个库余数为1的放到第二个库以此类推。
这两种分法,各自存在优缺点,如下表所示:
<img src="https://static001.geekbang.org/resource/image/3b/81/3bf9fb5fb9e1569cf4478c493c02f081.jpg" alt="">
在实践中为了运维方便选择ID取模进行分库的做法比较多。同时为了数据迁移方便一般分库的数量是按照倍数增加的比如说一开始是4个库二次分裂为8个再分成16个。这样对于某个库的数据在分裂的时候一半数据会移到新库剩余的可以不用动。与此相反如果我们每次只增加一个库所有记录都要按照新的模数做调整。
在这个项目中,我们结合订单数据的实际情况,最后采用的是**取模**的方式来拆分记录。
**补充说明:**按照取模进行分库每个库记录数一般比较均匀但也有些数据库存在超级ID这些ID的记录远远超过其他ID。比如在广告场景下某个大广告主的广告数可能占很大比例。如果按照广告主ID取模进行分库某些库的记录数会特别多对于这些超级ID需要提供单独库来存储记录。
### 分几个库?
现在,我们确定了记录要怎么分,但具体要分成几个数据库呢?
分库数量,首先和**单库能处理的记录数**有关。一般来说MySQL单库超过了5000万条记录Oracle单库超过了1亿条记录DB的压力就很大当然这也和字段数量、字段长度和查询模式有关系
在满足前面记录数量限制的前提下如果分库的数量太少我们达不到分散存储和减轻DB性能压力的目的如果分库的数量太多好处是单库访问性能好但对于跨多个库的访问应用程序需要同时访问多个库如果我们并发地访问所有数据库就意味着要消耗更多的线程资源如果是串行的访问模式执行的时间会大大地增加。
另外,分库数量还直接影响了**硬件的投入**多一个库就意味着要多投入硬件设备。所以具体分多少个库需要做一个综合评估一般初次分库我建议你分成4~8个库。在项目中我们拆分为了6个数据库这样可以满足较长一段时间的订单业务需求。
## 分库带来的问题
不过水平分库解决了单个数据库容量和性能瓶颈的同时,也给我们带来了一系列新的问题,包括数据库路由、分页以及字段映射的问题。
### 分库路由
分库从某种意义上来说意味着DB Schema改变了必然会影响应用但这种改变和业务无关所以我们要尽量保证分库相关的逻辑都在数据访问层进行处理对上层的订单服务透明服务代码无需改造。
当然要完全做到这一点会很困难。那么具体哪些改动应该由DAL数据访问层负责哪些由订单服务负责这里我给你一些可行的建议
- 对于**单库访问**比如查询条件已经指定了用户ID那么该SQL只需访问特定库即可。此时应该由DAL层自动路由到特定库当库二次分裂时我们也只需要修改取模因子就可以了应用代码不会受到影响。
- 对于**简单的多库查询**DAL层负责汇总各个分库返回的记录此时它仍对上层应用透明。
- 对于**带聚合运算的多库查询**比如说带groupby、orderby、min、max、avg等关键字建议可以让DAL层汇总单个库返回的结果然后由上层应用做进一步的处理。这样做有两方面的原因一方面是因为让DAL层支持所有可能的聚合场景实现逻辑会很复杂另一方面从1号店的实践来看这样的聚合场景并不多在上层应用做针对性处理会更加灵活。
DAL层还可以进一步细分为**底层JDBC驱动层**和**偏上面的数据访问层**。如果我们基于JDBC层面实现分库路由系统开发难度大灵活性低目前也没有很好的成功案例。
在实践中,我们一般是基于持久层框架,把它进一步封装成**DDAL**Distributed Data Access Layer分布式数据访问层实现分库路由。1号店的DDAL就是基于iBatis进一步封装而来的。
### 分页处理
水平分库后,分页查询的问题比较突出,因为有些分页查询需要遍历所有库。
举个例子假设我们要按时间顺序展示某个商家的订单每页有100条记录由于是按商家查询我们需要遍历所有数据库。假设库数量是8我们来看下水平分库后的分页逻辑
- 如果是取第1页数据我们需要从每个库里按时间顺序取前100条记录8个库汇总后共有800条然后我们对这800条记录在应用里进行二次排序最后取前100条
- 如果取第10页数据则需要从每个库里取前1000100*10条记录汇总后共有8000条记录然后我们对这8000条记录进行二次排序后取第900到1000之间的记录。
你可以看到在分库情况下对于每个数据库我们要取更多的记录并且汇总后还要在应用里做二次排序越是靠后的分页系统要耗费更多的内存和执行时间。而在不分库的情况下无论取哪一页我们只要从单个DB里取100条记录即可也无需在应用内部做二次排序非常简单。
**那么,我们如何解决分库情况下的分页问题呢?**这需要具体情况具体分析:
- 如果是为前台应用提供分页我们可以限定用户只能看到前面n页这个限制在业务上也是合理的一般看后面的分页意义不大如果一定要看可以要求用户缩小范围重新查询
- 如果是后台批处理任务要求分批获取数据我们可以加大分页的大小比如设定每次获取5000条记录这样可以有效减少分页的访问次数
- 分库设计时,一般还有配套的大数据平台负责汇总所有分库的记录,所以有些分页查询,我们可以考虑走大数据平台。
### 分库字段映射
分库字段只有一个比如这里我们用的是用户ID如果给定用户ID这个查询会落到具体的某个库。但我们知道在订单服务里根据**订单ID**查询的场景也很多见不过由于订单ID不是分库字段如果不对它做特殊处理系统会盲目查询所有分库从而带来不必要的资源开销。
所以,这里我们**为订单ID和用户ID创建映射保存在Lookup表里**我们就可以根据订单ID找到相应的用户ID从而实现单库定位。
Lookup表的记录数和订单库记录总数相等但它只有2个字段所以存储和查询性能都不是问题这个表在单独的数据库里存放。在实际使用时我们可以通过**分布式缓存**来优化Lookup表的查询性能。此外对于新增的订单除了写订单表我们同时还要写Lookup表。
## 整体架构
通过以上分析最终的1号店订单水平分库的总体技术架构如下图所示
<img src="https://static001.geekbang.org/resource/image/3a/9c/3ae46ab0d2d5430f03b436c87247d59c.jpg" alt="">
- **上层应用**通过订单服务访问数据库;
- **分库代理**实现了分库相关的功能包括聚合运算、订单ID到用户ID的映射做到分库逻辑对订单服务透明
- **Lookup表**用于订单ID和用户ID的映射保证订单服务按订单ID访问时可以直接落到单个库Cache是Lookup表数据的缓存
- **DDAL**提供库的路由可以根据用户ID定位到某个库对于多库访问DDAL支持可选的多线程并发访问模式并支持简单的记录汇总
- **Lookup表初始化数据**来自于现有的分库数据,当新增订单记录时,由分库代理异步写入。
## 如何安全落地?
订单表是系统的核心业务表,它的水平拆分会影响到很多业务,订单服务本身的代码改造也很大,很容易导致依赖订单服务的应用出现问题。我们在上线时,必须谨慎考虑。
所以,为了保证订单水平分库的总体改造可以安全落地,整个方案的实施过程如下:
- 首先实现Oracle和MySQL两套库并行所有数据读写指向Oracle库我们通过数据同步程序把数据从Oracle拆分到多个MySQL库比如说3分钟增量同步一次。
- 其次我们选择几个对数据实时性要求不高的访问场景比如访问历史订单把订单服务转向访问MySQL数据库以检验整套方案的可行性。
- 最后经过大量测试如果性能和功能都没有问题我们再一次性把所有实时读写访问转向MySQL废弃Oracle。
这里,我们把上线分成了两个阶段:**第一阶段**把部分非实时的功能切换到MySQL这个阶段主要是为了**验证技术**它包括了分库代理、DDAL、Lookup表等基础设施的改造**第二阶段**,主要是**验证业务功能**我们把所有订单场景全面接入MySQL。1号店两个阶段的上线都是一次性成功的特别是第二阶段的上线100多个依赖订单服务的应用通过简单的重启就完成了系统的升级中间没有出现一例较大的问题。
## 项目总结
1号店在完成订单水平分库的同时也实现了去Oracle设备从小型机换成了X86服务器我们通过水平分库和去Oracle不但支持了订单量的未来增长并且总体成本也大幅下降。
不过由于去Oracle和订单分库一起实施带来了双重的性能影响我们花了很大精力做性能测试。为了模拟真实的线上场景我们通过**TCPCopy**把线上实际的查询流量引到测试环境先后经过13轮的性能测试最终6个MySQL库相对一个Oracle在当时的数据量下SQL执行时间基本持平。这样我们**在性能不降低的情况下,通过水平分库优化了架构,实现了订单处理能力的水平扩展。**
1号店最终是根据用户ID后三位取模进行分库初始分成了6个库理论上可以支持多达768个库。同时我们还改造了订单ID的生成规则使其包括用户ID后三位这样新订单ID本身就包含了库定位所需信息无需走Lookup映射机制。随着老订单归档到历史库在前面给出的架构中Lookup表相关的部分就可以逐渐废弃了。
如果要扩充数据库的数量从6个升到12个我们可以分三步走
1. 增加6个MySQL实例把现有6个库的数据同步到新的库比如说0号库同步到6号库1号库同步到7号库等等
1. 在配置文件里把分库的取模从6变成12
1. 通过数据库脚本每个库删掉一半数据比如对于0号库删掉用户ID%12=6的记录对于6号库删掉用户ID%12=0的记录。
你可以看到,通过这样的分库方式,整个数据库扩展是非常容易的,不涉及复杂的数据跨库迁移工作。
订单的水平分库是一项系统性工作,需要大胆设计,谨慎实施。**你需要把握住这几个要点:**
- 首先,你需要在分库策略的指导下,结合实际情况,在每个方面做出最合适的选择;
- 其次,对于特殊场景,如分页查询,你需要具体问题具体解决;
- 最后,你要总体规划,控制好落地步骤,包括对系统改造、性能测试、数据迁移、上线实施等各个环节做好衔接,保证业务不出问题。
## 总结
今天我和你分享了1号店订单水平分库的实际案例并给出了具体的做法和原因相信你已经掌握了如何通过对数据库的水平拆分来保证系统的高性能和可伸缩。
**水平分库是针对有状态的存储节点进行水平扩展**,相对于无状态的节点,系统改造的复杂性比较高,要考虑的点也比较多。通过今天的分享,希望你以后在设计一个复杂方案时,能够更全面地思考相关的细节,提升架构设计能力。
**最后,给你留一道思考题**:你公司的数据库有什么瓶颈吗,你计划对它做什么样的改造呢?
欢迎在留言区和我互动,我会第一时间给你反馈。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢阅读,我们下期再见。

View File

@@ -0,0 +1,112 @@
<audio id="audio" title="19 | 综合案例:电商平台技术架构是如何演变的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f8/35/f8fc29ff4a50970918b5dc849c7f7635.mp3"></audio>
你好,我是王庆友。
在前面的几讲中,我分别和你介绍了技术架构的高可用、高性能、可伸缩等目标,并通过实际的案例说明了如何实现这些目标。今天呢,我会通过一个综合案例,来说明电商平台的技术架构是如何演变的,让你可以全面地理解如何实现这些目标。
一个实际的电商系统很复杂,在案例介绍中,为了简化,我用比较有代表性的**交易系统**和**账户系统**来代表整体的电商系统,并具体分析这两个系统在电商平台发展过程中,它们都碰到了什么瓶颈,以及我们在技术架构上是如何解决的。
这一讲会包含很多架构图,每一张图都代表了不同时期的架构设计,为了方便你更好地理解它们,在每张架构图中,我都用红色方框圈出了当前架构存在的问题,用绿色实体部分代表了上一个架构所存在问题的解决办法,希望你听完今天的讲解,能够结合这些架构图,加深对技术架构的理解。
## 单体系统
<img src="https://static001.geekbang.org/resource/image/cf/cf/cf1a7a16f3a09eb85c7dd4c1c172c6cf.jpg" alt="">
第一代的电商系统是一个单体架构,所有的代码都打包在一个应用里,部署的时候会有多个实例,我们通过**负载均衡**,把用户请求分发到具体的实例中。这个时候,所有的数据表还在一个数据库里。
**这里的问题是**,单体应用的所有代码都放在一起,代码编译需要很长时间,应用启动也需要很长时间,并且代码相互依赖,开发效率低,并行开发困难。随着单体应用的体量越变越大,这些问题也越来越突出。
## SOA架构
<img src="https://static001.geekbang.org/resource/image/84/c9/84da834d04bd838a6c76b7a535340bc9.jpg" alt="">
**针对单体应用体量过大带来的问题**,我们**对系统进行拆分**,把整体系统分为多个子系统。比如在图中,我们把系统拆分为交易系统和账户系统,这两个系统(或者说服务)通过内部的负载均衡进行相互调用,这个时候,底层数据库还没有拆分,两个系统还是访问同一个数据库。
通过拆分系统整体就变成了SOA架构这样我们减少了代码冲突系统的开发也变得更加高效部署的时候我们也更容易针对各个系统的处理能力进行水平扩展。
**但这里的问题是**,内部服务通过中心化的负载均衡进行访问,中心化的负载均衡增加了服务的调用时间。此外,在电商场景下,内部的服务很多,服务调用的频率很高,每秒可能有上百万次,导致了负载均衡的连接能力不够。而且负载均衡是单点,如果它出了问题,很容易引发系统整体的可用性问题(即使负载均衡是多实例,当系统流量很大时,也会因为某台负载有问题,导致其他节点压力增大而引起雪崩效应)。
## 服务调用去中心化
<img src="https://static001.geekbang.org/resource/image/5c/a8/5cc8cddc0c25a847fce084ff890db1a8.jpg" alt="">
**针对内部服务路由中心化的问题**,我们去掉了内部的负载均衡,加入了**服务注册中心**比如ZooKeeper。
当服务实例启动或退出时,它们会自动在注册中心进行注册或销毁,服务的客户端和注册中心保持长连接,可以实时地获取可用的服务列表;然后在客户端,根据相应的算法选择服务实例,直接调用服务。每次调用无需经过注册中心,如果注册中心有问题,也只是新的服务实例无法注册,或者是已有的服务实例无法注销,这对客户端调用服务的影响是非常有限的。
当然,通过注册中心和更体系化的微服务框架,我们还可以实现完善的**服务治理**,包括服务隔离、熔断、降级,这些都是对原来简单的负载均衡方式的加强,能够进一步提升服务的可用性。
现在,我们解决了服务调用的问题,但随着业务量逐渐变大,数据表越来越多,数据量也越来越大,**单个数据库比如Oracle的性能和储存容量已经无法满足需求了**。这个时候,我们就需要对数据库进行改造,提升它的处理能力。
## 垂直分库
<img src="https://static001.geekbang.org/resource/image/84/18/8440763ab5e043a4099c1424ba595018.jpg" alt="">
**对于单个数据库性能和容量瓶颈**,解决的办法就是,我们对数据库进行**垂直拆分**,按照业务拆分为交易数据库和账户数据库,这样就可以满足它们各自的容量和性能需求,同时也避免了不同业务数据表之间的相互耦合。
你可以认为垂直分库是系统拆分的第二阶段,这样,通过第一阶段的应用代码拆分和这里的数据库表拆分,交易系统和账户系统就可以独立发展。
**不过,新的问题又来了**,垂直分库后,每个数据库都是单实例。随着业务的发展,和原来系统只有单个数据库类似,现在交易系统也只有一个数据库,它的性能和容量还是有问题,并且数据库单实例也带来了可用性的问题,如果数据库挂了,相应的系统也就不可用。
## 水平分库及高可用部署
<img src="https://static001.geekbang.org/resource/image/d6/70/d6b81b2d5317e872632e1d09ce2ac170.jpg" alt="">
**针对单个数据库的可用性问题**,我们可以采用 **MHA高可用**Master High Availability方式部署。比如数据库部署一主多从通过MHA机制我们可以实时检测主库的可用性如果主库有问题系统会自动Failover故障转移到最新的从库。另一方面我们还可以利用多个从库支持**读写分离**,减轻主库的访问压力。
**针对单个数据库的性能和容量问题**,首先我们可以**引入缓存**,在高读写比的场景下,让应用先访问缓存,大大减轻对底层数据库的压力。然后,我们可以对数据库按照某个维度(比如用户维度),进行**水平拆分**,把数据记录分布到多个实例中,最终分散主库的写压力以及数据存储的瓶颈(在[上一讲](https://time.geekbang.org/column/article/218385)中,我已经具体介绍过了,你可以点击链接去回顾内容)。
在实践中,我们还可以提供**多套水平分库**。比如说,针对交易数据,我们可以同时按照用户维度和商户维度进行水平分库,用户维度的库用于前台用户下单的场景,商户维度的库用于后台商家履单的场景。这里,只有用户维度的分库会支持**写**,我们通过数据同步机制,把用户维度分库的更新同步到商户维度的分库里。
**当系统体量发展到了一定程度,我们又碰到了新的问题**:单个机房的服务器不够用,无法在同一个机房找到更多的机器部署交易系统和账户系统。
## 多机房部署
<img src="https://static001.geekbang.org/resource/image/d6/cf/d618f33b2ac8799bf6bab3949ed344cf.jpg" alt="">
**对于单机房服务器不够的问题**,我们可以在新的机房部署交易系统和账户系统,为了落地方便,所有服务还是注册到旧机房的注册中心,数据还是存放在旧机房的交易数据库和账户数据库。 这样,我们通过在新机房部署应用,对应用节点进行水平扩展,从而解决了单机房机器不足的问题。
**但这里产生了跨机房访问的问题**:首先,我们只有一个服务注册中心,服务实例一部分部署在老机房,一部分部署在新机房,对于服务调用者来说,它会同时访问新旧机房的服务实例;其次,数据库部署在老机房,新机房的应用会访问旧机房的数据库。
这两种情况,都会产生大量的跨机房访问,我们知道,根据机房物理距离的不同,跨机房访问的网络延时在数十毫秒到数百毫秒之间,是机房内部通信耗时的上千倍,这会对**应用的性能**产生很大影响,而且跨机房的**网络可用性**也经常是一个问题。
## 服务调用本地化
<img src="https://static001.geekbang.org/resource/image/bf/07/bfbf070729b23e11308dc8ed19cbc607.jpg" alt="">
为了避免服务的跨机房访问,我们**在新机房也单独部署了服务注册中心**,让每个机房的服务注册到同机房的注册中心。这样,客户端的服务调用会路由到同机房的服务端,实现了服务调用的本地化,大大降低了跨机房通信带来的延时和不可用性问题。
这时,**随着业务越来越复杂,新的问题又来了**:交易系统会依赖很多周边服务。比如下单后,我们需要给用户送积分,交易系统会同步调用积分服务。但是同步调用积分服务,一方面会影响下单的性能,另一方面如果积分服务不可用,会导致核心的下单功能失败。
## 依赖分级管理
<img src="https://static001.geekbang.org/resource/image/f0/91/f010a6bb35627923566a0f3484447091.jpg" alt="">
**对于外部服务依赖的可用性问题**,我们的解决办法是,针对这些外部依赖进行**分级管理**,根据依赖功能的重要性不同,把它们分为强依赖和弱依赖。
- 对于强依赖,我们**实时同步调用**,比如在用户下单时调用库存服务,由于库存非常重要,必须实时扣减,如果调用库存服务失败,下单也失败。
- 对于大量的弱依赖,我们以**异步消息**的方式进行信息同步,比如对于积分服务,可以通过柔性事务来保证数据的最终一致性,这样大大提升了核心系统的性能和可用性。
**不过,这里存在的问题是**,新机房的交易系统和账户系统都在访问老机房的数据库,有跨机房数据库访问的性能问题,以及老机房整体故障带来的可用性问题。比如说,机房断电,通信光纤有问题或者发生自然灾害,导致老机房整体不可用,这就会导致所有系统都不可用。
## 多机房独立部署
<img src="https://static001.geekbang.org/resource/image/63/ca/6301edac111c2f9f4dbd5492cbf0aaca.jpg" alt="">
**针对机房整体不可用的问题**,解决方案是,我们**在多个机房做对等的部署**,这样每个机房的系统可以形成内部闭环,包括服务、注册中心和数据库,机房之间不产生直接的相互依赖,从而实现了机房级别的水平部署。
如果系统的单元化做得完善,我们还可以进一步支持**虚拟机房**的概念,一个物理机房可以部署多个虚拟机房,每个虚拟机房包含了一个完整的系统。通过多机房独立部署,我们极大地提升了系统的可用性、处理能力和可伸缩性,可以应对系统面临的各种异常情况。
另外,最近几年,容器化技术的发展很快,原来很多的电商平台都是基于**虚拟机**部署,现在也纷纷改造为用**Docker+K8s**的方式部署,这大大提升了资源的利用率、系统的弹性伸缩能力。在面临资源瓶颈时,你可以考虑用这种方式来优化系统的部署。
## 总结
今天,我基于一个简化的电商系统模型,与你分享了电商平台的技术架构发展过程,我们是如何通过一步步的架构升级,解决系统各个阶段出现的高可用、高性能和可伸缩问题的,相信你现在对技术架构如何应对各种系统性挑战,有了更深入的认识。
**值得注意的是**,系统的技术架构变化不一定要完全遵循这个过程,不同的业务、不同的发展阶段,对系统的要求都是不一样的,这里我给出的只是典型的问题和解决手段,希望你在工作中,能够具体情况具体分析,灵活地运用这些手段。
业务在不断发展,新的问题会不断出现,但技术也在不断地进步,解决的手段层出不穷,我们需要不断学习,找到新的手段来解决问题。
**最后,给你留一道思考题**:你的公司当前的系统架构处于哪个阶段,面临什么样的问题呢?
欢迎在留言区和我互动,我会第一时间给你反馈。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢阅读,我们下期再见。