前面两篇文章每一篇都花了我五十个小时以上,写的我是欲仙欲死,本文我们来务点虚,上上价值。
我们将从微服务架构讲起,一步一步追根溯源,找寻“分布式数据库”在另一个维度的投影,探寻基建、应用、服务、组织之间的联系,通过观察自然规律和人类社会,引出本文的中心思想,并对“找出单点,进行拆分”做出最后的升华。
最后,我们将得到一个可行的 100 万 API QPS、500 万数据库 QPS 的系统设计方案,并顺便简单地讨论一下“高可用”背后的哲学悖论。
前面第七篇文章我们说过:
如果两个系统的数据库不在一起,那他们就不是一个系统,就像拼多多有 7.5 亿月活用户,淘宝有 8.5 亿,你不能说“拼宝宝”电商系统有 16 亿月活用户一样。
如果两个 API 的数据落到同一张表上,那他们两个就属于同一个系统。
那,在一个电商系统内,用户 API 和商品 API 似乎没有多对多交集?是的,这就是微服务架构的拆分逻辑。
我们都知道,微服务的拆分方式,反映的是其背后技术团队的组织方式。(你不知道也没关系,你现在知道了( ̄▽ ̄)" )
那,技术团队的组织方式是什么决定的呢?
是由系统内各部分天然的内聚性决定的:用户相关的业务和商品相关的业务都有很强的内聚性,他们之间不会主动发生关联,但他们会分别和订单发生关联。
我们用商品下单处理流程来推演一个电商订单的生命周期内对用户、商品、订单三个部分数据库的读写情况。
我们可以看出,大部分情况下,每一步都只有一个主要的微服务被需要,其它微服务都处于辅助地位:只读,且大部分都是单点读取。这就为我们降低了数据库单点的负载——只要把这三个微服务部署到三个独立的数据库上,就可以通过 API 调用的形式降低单个数据库的极限 QPS。
既然我们不能把拼多多和淘宝的系统称作一个系统,那么,在拼多多和淘宝系统内,肯定还可以基于类似的逻辑继续拆分:
把几乎不相互写入的数据拆到两个数据库上,这种组织形态在人类社会随处可见:两个国家的人分别在自己国家申请护照,他们有时也可以到对方国家内的本国领事馆申领本国护照;两个村的人各自在本村的井里打水,有时也可以不怕麻烦地去隔壁村的水井里打水;你每天早上都用滴滴打车上班,万一滴滴打不到车你还可以用高德来补救...
微服务的拆分思想相信大家都理解了,下面我们来解决现实问题。
如果你真的成为了“设计百万 QPS 系统”的架构师,相信我,你第一个想到的,一定是“削峰”。
顾名思义,将突发的流量高峰削平。大促的时候,系统顶不住,其实就是那么一会儿顶不住,只要初期的高热度下去了,系统的整体负载会迅速下降到没那么危险的水平。所以,对付突发流量,削峰是第一要务。
请原谅我用饮鸩止渴这个词形容 95% 的 web 系统的真正性能顶梁柱,实际上缓存的贡献远不止于此。
但是,使用缓存确实是在饮鸩止渴:缓存在带来性能的同时,大幅削弱了数据库提供的“单点性”,为系统失效埋下了一堆地雷。
缓存的毒性无法消除,一旦系统的某些部分失效,这杯毒酒就会发作,但是会不会把自己毒死还要看架构水平和基建能力:你的机房别随便断电,你的服务器别随便宕机,你在喝下缓存毒酒之后,就能活的够长。
我们上一篇文章讨论的 OceanBase 就采用了一种终极缓存方案:
12306 每天夜里都要维护一个小时,我有理由怀疑,它在将 redo log 落盘¹ :-D
缓存可以削读的峰,队列就是拿来削写的峰的。
队列的思想其实早就贯穿在人类社会的每一个角落了:超市结账需要排队,做核酸需要排队,火车站打车需要排队,网上抢购商品也需要排队。排队的本质是将一拥而上购买变成了“异步”购买:你想买东西?先排队,过段时间轮到你了,看看商品有没有卖完,没卖完你就能买到。
排队的思维特别好理解,而现实中防止超售功能也确实是基于排队功能做的。传统数据库的事务隔离属于强迫用户等待,而现在大多使用队列系统来处理排队,排队这件事情才真正异步起来了。
电商下单在普通压力下,一个队列就能解决问题,但是当你面临每秒几十万单的时候,如何让这些订单真正地下单成功,才是最需要解决的问题,这个数字就是“系统容量”。队列是无法提升系统的绝对容量的,那该怎么办呢?
继续找东西和信息之神交换:过去的时间换现在的时间。
在大促之前,先把订单生成好,然后用户下单时直接写入用户信息:不需要执行“检查库存”这个单点操作了,负载低了很多。
骚,实在是骚。
现实世界中,一个一百万 QPS 的电商系统,真正需要触达到数据的 QPS 其实是没有 500 万那么多的,在削峰的操作下,200 万 QPS 的 PolarDB 集群在 Redis 集群的配合下,是可以顶住 100 万 API QPS 的。史上流量最大的那一年双 11,每秒订单创建最大值仅为 58.3 万笔,这已经是这个地球上的最高记录了。
但是,现实世界中,一切都要讲 ROI(收益成本之比),搞一个顶配的 PolarDB 集群确实可以顶住巅峰时期一百万 API QPS,但是你老板看着账单肯定会肉痛,那,该如何省钱呢?
答:站在地球表面
。
北京位于美丽的华北平原北端,生活着两千多万人,在巅峰的 2020 年双 11,天猫平台北京地区销售额为 216 亿,全国总额为 4982 亿,占比为 4.33%,略高于北京占全国 3.44% 的 GDP 比例。那我们就可以计算得出,北京的两千多万人,给天猫贡献了583000 * 0.0433 = 25243.9
笔/秒的并发。
虽然全国订单数看起来十分惊人,但是北京这一个地方的压力却只有 2.5 万单每秒,这个哪怕不用奇技淫巧硬抗,十万数据库 QPS 只用主从架构可能都能抗住。但是,系统能基于地理位置划分吗?系统不是必须全国一盘棋吗?不是的,可以划分。
下面我们讨论一下怎么划分。
为什么非要全国的用户访问同一个数据库呢?我们可以利用微服务思想对业务系统和数据进行拆分:北京的用户和上海的用户,理论上讲可以只访问“本地天猫”。
接下来我们分析一下,在一个标准的电商业务中,哪些地方会让一个北京的用户和一个上海的用户发生联系。
实际上,地理上被隔开的两个人,在系统内还真没什么机会需要相互查询对方的数据,这就是我们能基于地理位置对应用和数据库进行分区的逻辑原因。下面我们一一拆除上面的单点:
基于地理位置对应用和数据库进行划分,产生出两个“本地天猫”后,就需要我们老朋友 DNS 出来表演了。
域名当初可能是为了方便记忆而发明的,但是域名背后的 DNS 服务却几乎是最重要的互联网高并发基础设施:不同地区的人,对同一个域名进行访问,可以获得两个公网 ip,这样“本地天猫”就实现了。
DNS 几乎完全放弃了一致性,但却实现了极高的可用性和分区容错性。其实,gossip 协议也是这个思想:让消息像病毒一样传播,能够实现最终一致性就行了,要啥自行车。
异曲同工的 Kong 集群思想也让我震惊:所有节点每 5 秒从数据库读取最新的配置文件,然后,这些节点就成了一个行为完全一致的集群啦。“想那么多干什么,短时间内多个节点的行为不一致,就让他们不一致好了,5 秒之后不就一致了。”
在我们熟悉的存储器山中,这是一个大家都理解的基本特性,而这个特性引申到分布式系统中,就是:一定不能让应用和数据库分离。
和 InnoDB 一样,很多时候其实是“局部性”这个我们宇宙的基本属性在帮助我们提升系统的性能,让应用和数据库分布在同一个地域,也是在利用局部性获得性能增益。
所以,让应用去隔壁区域的数据库读数据是要极力避免的——我们应该用 API 网关直接把请求发给隔壁区域的应用服务器,这显然是在今天这个异地网络传输速度接近光速的时代最佳的选择。
Clickhouse 在亿级数据量面前丝毫不怵:MySQL、MongoDB、Hadoop,谁也没有老子快。为什么 Clickhouse 这么快呢?
首先,它将数据以列为单位组织起来,压缩后存入磁盘上一个又一个的 block,这些 block 就像 InnoDB 的 16KB 页一样,只是它更大(64KB~1MB)。这样,当我们 select 某个 column 的时候,Clickhouse 就能顺序读出磁盘上这个 column 下面所有行的数据。
除了列存储之外,每个 block 内,Clickhouse 还用“稀疏索引”的方式,将每一列的数据划分为了多个 granularity(颗粒度),然后给每个 granularity 分配一个 CPU 核心进行并行计算,并且它还利用 SSE4.2 指令集,利用 CPU 的 SIMD(Single Instruction Multiple Data) 指令,在 CPU 寄存器层面进行并行操作。
这是 Clickhouse 整个架构中我最喜欢的部分。我们通过上一篇文章可以看出,所有的分布式数据库,其本质都是在搞“内存缓存的数据同步”,Clickhouse 直接掀桌子:老子不要内存缓存了。由于所有数据都在磁盘上,而节点的 CPU 又直接和磁盘数据打交道,所以 Clickhouse 实现了真正的并行:增加 CPU 核心数就能提升系统容量,无论在不在同一台机器上都行,反正 CPU 相互之前完全不需要通信。这样,Clickhouse 通过堆核心数就能够实现系统容量的“近线性扩展”。
我们可以学习这种思想,打造一个可以线性扩展的系统架构:只要不同地区的本地系统之间完全没有“数据实时同步”需求,那其实他们就是两个系统,就可以实现性能的线性扩展。
我们说过,关系型数据库的关系,指的就是两行数据之间的关系。现实世界中,位于异地甚至是异国的两个人之间,几乎是不会发生实时相互数据读取的。
站在地球表面来思考,你会发现人类社会和自然规律都是契合高并发“找出单点,进行拆分”哲学原理的:每一个人类居所,本质上都是散落在整个地球上的一个又一个点。因为这些点的存在,我们发明了国省市县乡村逐级政府,同级政府之间几乎没有相互通信。
将一个大系统拆成不需要实时相互通信的多个小系统,可以获得线性的性能提升。
当你的系统顶不住的时候,按照这个原理来拆就行了,绝对顶得住。别说区区一百万 QPS 了,服务全人类也做得到。
价值上完了,我们最后再讨论五毛钱的高可用。
我相信,很多人都像我一样做过思想实验,希望设计一个“完全高可用”的系统,但是最终可能都败下阵来,为什么?因为高可用和其它常见的分布式系统需求是互斥的。
数据重要如银行,也只是要求在天灾面前要尽量不丢数据、少丢数据,凭什么你就要求自己的系统永远可用呢?其实,想从架构层面实现高可用是非常困难的,终极高可用就是将数据完整地复制到世界各地的所有节点上,并用超长的时间来达到完全一致,这是什么,这是区块链呀。
高可用和性能、一致性都是冲突的,只能采用策略尽量压制问题。
这个词在技术圈的流行应该有微博一半功劳,压力一大就熔断:主动停止不重要的服务,断尾自救,争取让核心业务不挂。
限制一部分地区、一部分用户的访问,以保护整个集群不崩,一般用于限制单个用户对系统造成的压力过大,对面很可能是机器人。
Facebook 的用户不可谓不多,对高可用的投入不可谓不足,为什么还是会整个公司完全宕机 7 小时呢?
事故的起因是一个错误的命令意外断开了 Facebook 的 DNS 服务,结果问题大了:
结合阿里云香港一个数据中心因为空调故障导致整个数据中心宕机超过 24 小时³,认命吧,商业机构做不了真正的高可用的:资源使用率就是钱呐。
出自:https://github.com/johnlui/PPHC
本文由 mdnice 多平台发布