第5章第2节:保安和VIP通道

1 阅读12分钟

问题已足够清晰。我们的图书馆只有一个通道,海量的读者洪流在仅有通道的中排起了长队,导致作家们无法及时完成新书的注册登记工作。解决方案是新开设一个专用通道。这样我们的作家们就有了专属的VIP的通道,前来借阅的芸芸大众也会有更宽敞的读者大厅。

在数据库技术架构中,这种策略被称作数据同步

深入技术细节:数据库同步机制

正如字面含义,同步就是为同一个数据库创建并维护数据副本的过程。相比之前我们只有总包总揽的唯一数据库,现在我们有了一组各司其职的数据库集群。我们采用的正是最常见的一种同步形式:主-从同步

让我们暂时抛开图书馆的类比,设想一个网红酒吧的场景。

数据库主库:VIP厅

主库就类似酒吧里只服务尊贵VIP的专属区域。所有数据的正确性以主库为准。

  • 主库会处理所有写入操作(INSERT,UPDATE以及DELETE)。酒吧状态的任何改变,比如新抵达了一位VIP顾客,在场的客人点了一杯酒,或者有客人离店了都需要通过主库来完成。就好像VIP厅门口站着一位很严格很干练的保安,它会确保所有状态改变都是符合规则且被如实准确地记录在案。
  • 对Dukaan来说,主库就是卖家的访问入口。当店家更新商品价格,增加某种新商品,或者删除某样下架的商品,这些请求都会直接发送到数据库主库。这些操作都十分重要,所以会优先通过没那么拥挤的专享主库来完成。

只读从库(Slave):酒吧大厅

只读从库就如同酒吧里服务普通顾客的大厅。从库是VIP厅(主库)所有改变的完美即时复制,允许所有人随时访问。

  • 从库只允许读操作(SELECT)。成千上万的顾客可以同时出现在宽敞的大厅里,享受音乐,欣赏周围的一切。他们可以看到VIP厅的活动,但是无法做出任何“写入”操作。
  • 从库的任务就是承载巨大的读取流量。由于有了从库帮助承担那些只是“随便看看”的读请求,主库的资源就被解放出来得以去完成重要的写入操作。如果流量实在太大, 甚至我们可以同时拥有多个只读从库,就如同多个大厅。

我们需要的架构升级就是如上所述的职责分离。这样可以让我们单独地拓展数据库读取和数据库写入操作。

深入技术细节:实施数据库同步

理论听上去非常不错,具体应该如何操作呢?酒吧大厅里的人们该如何即时得知VIP区域发生的事情呢?

PostgreSQL的流式同步机制

PostgreSQL内置了超级好用的流式同步功能。

  1. WAL(预写日志):我们的主库(VIP厅)有一位勤勉的保安,它在一本特殊的日志中事无巨细地记录下了所有发生的变化。来了一位新顾客?记下来。价格变化了?记下来。这样一本日志被称作预写日志(WAL)。本质是按时间排序且实时变化的数据库完整修改记录。
  2. 数据流:我们建立一个只读从库把它配置成连接到我们的主库。从库的第一条指令便是“订阅WAL”。然后主库开始像打开的水龙头一样哗哗地把预写日志里的每一条记录通过安全的专属网络连接实时复制到从库。
  3. 从库更新:制度从库收到主库发来的数据修改之后,会严格按照原有顺序一条不差地复制到自己的数据副本上。

以上流程的结果便是从库近乎完美地镜像了主库。就好像VIP厅的活动通过视频流转播到酒吧大厅巨大的屏幕上,让所有人都能实时看到。

更新Django应用以便使用只读从库

设置好数据库同步只完成了任务的一半。我们Django应用代码还不了解发生了什么。它现在仍然只能够与一个数据库通信。我们需要让它变得更聪明,就像酒吧保安那样决定谁能进VIP厅以及谁只能去大厅。

我们的应用代码需要若干主要的改动:

  1. 配置多个数据库连接:我们首先需要在Django的配置里从一个数据库连接增加到两个数据库连接:默认的(default)连接至主库,只读连接(read_replica)至新创建的制度从库。
  2. 创建一个数据库路由:接下来我们需要定制一个“数据库路由”。这是Django中一段特殊的代码,它负责拦截每一个数据库请求然后决定把该请求送往哪一个数据库。逻辑很简单但至关重要:
# 我们数据库路由的简化版本
class PrimaryReplicaRouter:
   def db_for_read(self, model, **hints):
       # 所有只读操作都送到从库
       return 'read_replica'
   
   def db_for_write(self, model, **hints):
       # 所有写入操作都送到主库
       return 'default'

有了数据库路由,现在我们的应用代码变得更聪明。每次当用户加载店铺页面(触发一系列SELECT数据查询),路由代码会把这些流量都发送到强大的只读从库。当卖家点击按钮保存新商品时(触发INSERT或者UPDATE写入操作),路由代码会把该写入操作发送到受到严密访问控制的主库。

这些改变实施之后的效果立竿见影且显著。商铺页面瞬间能完成加载。店家们反馈保存信息修改也很快。我们主库的CPU和输入输出负载恢复了正常。我们成功地拓展了数据库。

不可能三角:CAP理论

数据库同步实施之后,效果就如同魔法。主库负责写,从库负责读,整个酒吧的人流瞬间变得通畅无比。卖家再也不必因为突然一波顾客流量暴涨而拖慢修改商品名录而头疼。顾客们也可以随意浏览店铺页面,而无需因为页面无法加载反复刷新。看上去我们找到了完美的系统。

但分布式系统的完美从不是免费的。 计算机行业里有一个很经典的理论,之前曾被我忽视,但现在成了我每天都必须面对的:CAP原理。

CAP表示一致性(Consistency)可用性(Availability) 以及 分区容忍(Partition olerance)。CAP原理意为,对于分布式系统,上述3项性质只能3选2,不可能3项同时满足。

  • 一致性意味着酒吧所有人都能同时看到同一个状态。比如VIP厅更改了背景音乐歌单,那大厅的人们也能立刻听到相同的新歌曲。
  • 可用性意味着酒吧永不打烊。无论发生什么,任何顾客任何时候来到酒吧都能得到应有的服务,即所有请求都可以得到某种响应。
  • 分区容忍意味着即使走廊被阻塞,酒吧也可以继续营业。又或者连接VIP厅与大厅之间的音响设备有一些小故障,但是不影响酒吧接着奏乐接着舞。

关键在于:现实世界中分区必然存在。有时候是网络中断,网络包丢失,光缆损坏等等。所以在分区存在的情况下,现实的分布式系统必须要在一致性和可用性中2选1。

当我们引入只读从库时,虽然我们可能尚未意识到,但实际我们已经做出了选择。酒吧大厅(从库)在即使与VIP厅(主库)不同步的情况下仍然会继续保持营业,为众多顾客提供服务。结果就是大厅显示的可能是过时的信息。

具体例子

比如一个卖家在VIP厅(主库)修改某款裙子的价格:从1000卢比降到800卢比。主库马上记录下这条修改。

  • 如果下次请求直抵主库,顾客将看到修改后的数据:800卢比。
  • 如果请求在数据同步复制之前就到达从库,顾客看到的仍是旧的价格:1000卢比。

这两个价格可能都算是“合理”的,取决于所处是VIP厅还是大厅。但是从卖家角度看,这显然有问题。价格刚才已经被修改了,为啥店铺仍然显示旧的价格?

CAP为什么重要

CAP不只是书本上遥远的理论,它是添加从库,分发数据或者跨区域同步时必然需要处理的隐型不可能三角。当我们开始拥抱数据同步,我们就一定会碰到某些读取会与之前的写入没有及时同步。这不是代码错误(bug)。CAP理论提醒我们分布式系统里没有银弹,为了某些目的却总得选择一款毒酒喝下去。

一致性的阴影

如果你认可CAP理论,下一个问题来了:如果不能同时满足CAP的3项,那我们所需的一致性到底是什么?实际答案可能不止一种。分布式系统的一致性要求各有不同,每一款分布式软件也都依赖各自设计哲学侧重做出了不同的选择。

以下是常见的3种类型:

  • 强一致性 这是人们直观上最容易理解的情况。如果卖家把价格改成了800卢比,那么此后任何读取,无论请求发送至哪个服务器(主库或者从库),都必须返回相同的800卢比。 在酒吧的类比中,VIP厅的DJ一旦修改背景音乐曲目,大厅里的人们也会立刻毫无意外地听到相同的新歌曲。 强一致性让人感觉一切有条不紊,但代价是可用性。如果VIP厅和大厅之间的联络即使短暂被阻塞一小会,整个酒吧 也会宁愿暂停营业,也不想让人们听到“错误”的歌曲。

  • 最终一致性 这种情况就是只读从库发挥的场景。VIP厅的变化会尽快传播到大厅,但也许会稍有延迟。如果倒霉的话,可能会先仍听到一段旧歌曲的旋律,最终才能听到新歌的响起。 从用户角度看,这也许令人困扰:刚明明已经保存了新数据修改,但是店铺页面仍然显示的旧数据。虽然一段时间之后,最终所有数据同步都会完成,所有显示都会一致,但是这个“一段时间”可能是1秒,也可能是5秒,无法准确得知。

  • 因果一致性 这是一种试图维护因果顺序的中间方案。比如某个名叫Priya的卖家给她家卖的项链降价了然后查看自家店铺页面,因果一致性可以保证她自己是能够马上看到降价之后的价格,但是其它用户却不一定能马上看到。 在酒吧的类比中,如果DJ修改了播放曲目,VIP厅里见证这个改动的人们都能立马听到新曲目,但是大厅里的人们却不一定能即刻同步听到。 虽然因果一致性不保证完美地全局同步,但它保证“我修改了某项数据,我立马能看到修改后的结果。”

选择适合的方案

不同的系统做出的选择各有差别。银行系统需要强一致性,比如顾客肯定不愿意看到某个分行显示余额有10000卢比,但是另一家分行显示余额只有一半。社交网络应用可能更倾向最终一致性,比如给你点赞的计数延时了几秒钟,大家都能接受。因果一致性在面向终端用户的应用中越来越流行,因为它考虑到了用户个人即时的期待从而做出了一定平衡。

对于Dukaan,我们通过只读从库建立了一个实现最终一致性的系统。这就是卖家Priya有时候看到旧数据的原因。这并不是bug,而是教科书上关于最终一致性的经典场景。

但如同每一个改进,新架构引入了一个新的不易察觉的潜在危险副作用。

新问题:同步延时

主库到从库的同步数据流虽然很快耗时很短,但的确不是“瞬间”。一般总会有毫秒级别的延时。在高负载情况下,延时甚至可能飙升到1秒或者2秒。这个延时被称作同步延时

这意味着大厅的人们接受到的VIP厅状态变化比实际发生总是晚了一点点(1秒之内)。

这会导致一系列潜在的令人困惑的问题。比如当店家把某款商品价格从100卢比降到90卢比(送往主库的写操作)之后马上刷新店铺页面(发送到读库),结果看到仍是修改前的100卢比,因为此时数据同步尚未完成。这种情况怎么办?

这就是最终一致性导致的令人困惑的危险局面。