高可用

介绍可用性的概念,以及Pigsty在高可用上的实践

可用性的概念

人们对于一个东西是否可靠,都有一个直观的想法。人们对可靠软件的典型期望包括:

  • 应用程序表现出用户所期望的功能。
  • 允许用户犯错,允许用户以出乎意料的方式使用软件。
  • 在预期的负载和数据量下,性能满足要求。
  • 系统能防止未经授权的访问和滥用。

如果所有这些在一起意味着“正确工作”,那么可以把可靠性粗略理解为“即使出现问题,也能继续正确工作”。

​ 造成错误的原因叫做故障(fault),能预料并应对故障的系统特性可称为容错(fault-tolerant)韧性(resilient)。“容错”一词可能会产生误导,因为它暗示着系统可以容忍所有可能的错误,但在实际中这是不可能的。比方说,如果整个地球(及其上的所有服务器)都被黑洞吞噬了,想要容忍这种错误,需要把网络托管到太空中——这种预算能不能批准就祝你好运了。所以在讨论容错时,只有谈论特定类型的错误才有意义。

​ 注意故障(fault)不同于失效(failure)【2】。故障通常定义为系统的一部分状态偏离其标准,而失效则是系统作为一个整体停止向用户提供服务。故障的概率不可能降到零,因此最好设计容错机制以防因故障而导致失效。本书中我们将介绍几种用不可靠的部件构建可靠系统的技术。

​ 反直觉的是,在这类容错系统中,通过故意触发来提高故障率是有意义的,例如:在没有警告的情况下随机地杀死单个进程。许多高危漏洞实际上是由糟糕的错误处理导致的【3】,因此我们可以通过故意引发故障来确保容错机制不断运行并接受考验,从而提高故障自然发生时系统能正确处理的信心。Netflix公司的Chaos Monkey【4】就是这种方法的一个例子。

​ 尽管比起阻止错误(prevent error),我们通常更倾向于容忍错误。但也有预防胜于治疗的情况(比如不存在治疗方法时)。安全问题就属于这种情况。例如,如果攻击者破坏了系统,并获取了敏感数据,这种事是撤销不了的。但本书主要讨论的是可以恢复的故障种类,正如下面几节所述。

硬件故障

​ 当想到系统失效的原因时,硬件故障(hardware faults)总会第一个进入脑海。硬盘崩溃、内存出错、机房断电、有人拔错网线……任何与大型数据中心打过交道的人都会告诉你:一旦你拥有很多机器,这些事情会发生!

​ 据报道称,硬盘的 平均无故障时间(MTTF mean time to failure) 约为10到50年【5】【6】。因此从数学期望上讲,在拥有10000个磁盘的存储集群上,平均每天会有1个磁盘出故障。

​ 为了减少系统的故障率,第一反应通常都是增加单个硬件的冗余度,例如:磁盘可以组建RAID,服务器可能有双路电源和热插拔CPU,数据中心可能有电池和柴油发电机作为后备电源,某个组件挂掉时冗余组件可以立刻接管。这种方法虽然不能完全防止由硬件问题导致的系统失效,但它简单易懂,通常也足以让机器不间断运行很多年。

​ 直到最近,硬件冗余对于大多数应用来说已经足够了,它使单台机器完全失效变得相当罕见。只要你能快速地把备份恢复到新机器上,故障停机时间对大多数应用而言都算不上灾难性的。只有少量高可用性至关重要的应用才会要求有多套硬件冗余。

​ 但是随着数据量和应用计算需求的增加,越来越多的应用开始大量使用机器,这会相应地增加硬件故障率。此外在一些云平台(如亚马逊网络服务(AWS, Amazon Web Services))中,虚拟机实例不可用却没有任何警告也是很常见的【7】,因为云平台的设计就是优先考虑灵活性(flexibility)弹性(elasticity)[^i],而不是单机可靠性。

​ 如果在硬件冗余的基础上进一步引入软件容错机制,那么系统在容忍整个(单台)机器故障的道路上就更进一步了。这样的系统也有运维上的便利,例如:如果需要重启机器(例如应用操作系统安全补丁),单服务器系统就需要计划停机。而允许机器失效的系统则可以一次修复一个节点,无需整个系统停机。

软件错误

​ 我们通常认为硬件故障是随机的、相互独立的:一台机器的磁盘失效并不意味着另一台机器的磁盘也会失效。大量硬件组件不可能同时发生故障,除非它们存在比较弱的相关性(同样的原因导致关联性错误,例如服务器机架的温度)。

​ 另一类错误是内部的系统性错误(systematic error)【7】。这类错误难以预料,而且因为是跨节点相关的,所以比起不相关的硬件故障往往可能造成更多的系统失效【5】。例子包括:

  • 接受特定的错误输入,便导致所有应用服务器实例崩溃的BUG。例如2012年6月30日的闰秒,由于Linux内核中的一个错误,许多应用同时挂掉了。
  • 失控进程会用尽一些共享资源,包括CPU时间、内存、磁盘空间或网络带宽。
  • 系统依赖的服务变慢,没有响应,或者开始返回错误的响应。
  • 级联故障,一个组件中的小故障触发另一个组件中的故障,进而触发更多的故障【10】。

导致这类软件故障的BUG通常会潜伏很长时间,直到被异常情况触发为止。这种情况意味着软件对其环境做出了某种假设——虽然这种假设通常来说是正确的,但由于某种原因最后不再成立了【11】。

​ 虽然软件中的系统性故障没有速效药,但我们还是有很多小办法,例如:仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现差异(discrepancy) 时报警【12】。

人为错误

​ 设计并构建了软件系统的工程师是人类,维持系统运行的运维也是人类。即使他们怀有最大的善意,人类也是不可靠的。举个例子,一项关于大型互联网服务的研究发现,运维配置错误是导致服务中断的首要原因,而硬件故障(服务器或网络)仅导致了10-25%的服务中断【13】。

​ 尽管人类不可靠,但怎么做才能让系统变得可靠?最好的系统会组合使用以下几种办法:

  • 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。
  • 将人们最容易犯错的地方与可能导致失效的地方解耦(decouple)。特别是提供一个功能齐全的非生产环境沙箱(sandbox),使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验。
  • 在各个层次进行彻底的测试【3】,从单元测试、全系统集成测试到手动测试。自动化测试易于理解,已经被广泛使用,特别适合用来覆盖正常情况中少见的边缘场景(corner case)
  • 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。 例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)。
  • 配置详细和明确的监控,比如性能指标和错误率。 在其他工程学科中这指的是遥测(telemetry)。 (一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的。)监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。
  • 良好的管理实践与充分的培训——一个复杂而重要的方面,但超出了本书的范围。

可靠性有多重要?

​ 可靠性不仅仅是针对核电站和空中交通管制软件而言,我们也期望更多平凡的应用能可靠地运行。商务应用中的错误会导致生产力损失(也许数据报告不完整还会有法律风险),而电商网站的中断则可能会导致收入和声誉的巨大损失。

​ 即使在“非关键”应用中,我们也对用户负有责任。试想一位家长把所有的照片和孩子的视频储存在你的照片应用里【15】。如果数据库突然损坏,他们会感觉如何?他们可能会知道如何从备份恢复吗?

​ 在某些情况下,我们可能会选择牺牲可靠性来降低开发成本(例如为未经证实的市场开发产品原型)或运营成本(例如利润率极低的服务),但我们偷工减料时,应该清楚意识到自己在做什么。

一、意义

  1. 显著提高系统整体可用性,提高RTO与RPO水平。
  2. 极大提高运维灵活性与可演化性,可以通过主动切换进行滚动升级,灰度停机维护。
  3. 极大提高系统可维护性,自动维护域名,服务,角色,机器,监控等系统间的一致性。显著减少运维工作量,降低管理成本

二、目标

当我们在说高可用时,究竟在说什么?Several nines ?

说到底,对于传统单领导者数据库来说,核心问题是就是故障切换,是领导权力交接的问题。

目标层次

  • L0,手工操作,完全通过DBA人工介入,手工操作完成故障切换(十几分钟到小时级)
  • L1,辅助操作,有一系列手工脚本,完成选主,拓扑切换,流量切换等操作(几分钟)
  • L2,半自动化,自动检测,人工决策,自动操作。(1分钟)
  • L3,全自动化:自动检测,自动决策,自动操作。(10s)

关键指标

  • 允许进行日常Failover与Switchover操作,不允许出现脑裂。
  • 无需客户端介入,提供代理切换机制,基于流复制,不依赖特殊硬件。
  • 域名解析,VIP流量切换,服务发现,监控适配都需要与自动故障切换对接,做到自动化。
  • 支持PG 10~12版本与CentOS 7,不会给云原生改造埋坑。

交付方式

  • 沙盒模型,展示期待的部署架构与状态
  • 调整方案,说明如何将现有环境调整至理想状态。

三、效果

场景演示

集群状况介绍

  • 主库URL:postgres://dbuser_test:dbuser_test@testdb:5555/testdb
  • 从库URL:postgres://dbuser_test:dbuser_test@testdb:5556/testdb

HA的两个核心场景:

  • Switchover演示
  • Failover演示

故障切换的四个核心问题:

  • 故障检测(Lease, TTL,Patroni向DCS获取Leader Key)
  • Fencing(Patroni demote,kill PG进程,或通过Watchdog直接重启)
  • 拓扑调整(通过DCS选主,其他从库从DCS获取新主库信息,修改自身复制源并重启生效)
  • 流量切换(监听选主事件,通知网络层修改解析)

Patroni原理:故障检测

  • 基于DCS判定
  • 心跳包保活
  • Leader Key Lease
  • 秦失其鹿,天下共逐之。

Patroni原理:Fencing

  • 一山不容二虎,成王败寇,血腥的权力交接。

Patroni原理:选主

  • The king is dead, long live the king
  • 先入关者王

流量切换原理

  • 回调事件,或监听DCS变化。

搭建环境

https://github.com/Vonng/pigsty

五、细节,问题,与风险

场景演示

  • Switchover
  • Standby Down
    • Patroni Down
    • Postgres Down
    • Accidentally Promote
  • Primary Down
  • Failover
  • DCS Down
    • DCS Service Down
    • DCS Primary Client Down
    • DCS Standby Client Down
  • Fencing And corner cases
  • Standby Cluster
  • Sync Standby
  • Takeover existing cluster

问题探讨

关键问题:DCS的SLA如何保障?

==在自动切换模式下,如果DCS挂了,当前主库会在retry_timeout 后Demote成从库,导致所有集群不可写==。

作为分布式共识数据库,Consul/Etcd是相当稳健的,但仍必须确保DCS的SLA高于DB的SLA。

解决方法:配置一个足够大的retry_timeout,并通过几种以下方式从管理上解决此问题。

  1. SLA确保DCS一年的不可用时间短于该时长
  2. 运维人员能确保在retry_timeout之内解决DCS Service Down的问题。
  3. DBA能确保在retry_timeout之内将关闭集群的自动切换功能(打开维护模式)。

可以优化的点? 添加绕开DCS的P2P检测,如果主库意识到自己所处的分区仍为Major分区,不触发操作。 具体来说,当节点与DCS失联,节点应当首先执行自己的P2P检测协议,判断是不是DCS本身的故障,如果是DCS本身的故障,则应当拒绝角色切换,或者直接进入维护模式,保持现状。

关键问题:HA策略,RPO优先或RTO优先?

可用性与一致性谁优先?例如,普通库RTO优先,金融支付类RPO优先。

普通库允许紧急故障切换时丢失极少量数据(阈值可配置,例如最近1M写入)

与钱相关的库不允许丢数据,相应地在故障切换时需要更多更审慎的检查或人工介入。

关键问题:Fencing机制,是否允许关机?

在正常情况下,Patroni会在发生Leader Change时先执行Primary Fencing,通过杀掉PG进程的方式进行。

但在某些极端情况下,比如vm暂停,软件Bug,或者极高负载,有可能没法成功完成这一点。那么就需要通过重启机器的方式一了百了。是否可以接受?在极端环境下会有怎样的表现?

关键操作:选主之后

选主之后要记得存盘。手工做一次Checkpoint确保万无一失。

关键问题:流量切换怎样做,2层,4层,7层

  • 2层:VIP漂移
  • 4层:Haproxy分发
  • 7层:DNS域名解析

关键问题:一主一从的特殊场景

  • 2层:VIP漂移
  • 4层:Haproxy分发
  • 7层:DNS域名解析

切换流程细节

主动切换流程

假设集群包括一台主库P,n台从库S,所有从库直接挂载在主库上。

  • 检测:主动切换不需要检测故障
  • 选主:人工从集群中选择复制延迟最低的从库,将其作为候选主库(C)andidate。
  • 拓扑调整
    • 修改主库P配置,使得C成为同步从库,使切换RTO = 0。
    • 重定向其他从库,将其primary_conninfo指向C,作为级连从库,滚动重启生效。
  • 流量切换:需要快速自动化执行以下步骤
    • Fencing P,停止当前主库P,视流量来源决定手段狠辣程度
      • PAUSE Pgbouncer连接池
      • 修改P的HBA文件并Reload
      • 停止Postgres服务。
      • 确认无法写入
    • Promote C:提升候选主库C为新主库
      • 移除standby.signal 或 recovery.conf。执行promote
      • 如果Promote失败,重启P完成回滚。
      • 如果Promote成功,执行以下任务:
      • 自动生成候选主库C的新角色域名:.primary.
      • 调整集群主库域名/VIP解析:primary. ,指向C
      • 调整集群从库域名/VIP解析:standby.,摘除C(一主一从除外)
      • 根据新的角色域名重置监控(修改Consul Node名称并重启)
    • Rewind P:(可选)将旧主库Rewind后作为新从库
      • 运行pg_rewind,如果成功则继续,如果失败则直接重做从库。
      • 修改recovery.conf(12-)|postgresql.auto.conf(12),将其primary_conninfo指向C
      • 自动生成P的新角色域名:< max(standby_sequence) + 1>.standby.
      • 集群从库域名/VIP解析变更:standby.,向S中添加P,承接读流量
      • 根据角色域名重置监控

自动切换流程

自动切换的核心区别在于主库不可用。如果主库可用,那么完全同主动切换一样即可。 自动切换相比之下要多了两个问题,即检测与选主的问题,同时拓扑调整也因为主库不可用而有所区别。

  • 检测 (网络不可达,端口拒绝连接,进程消失,无法写入,多个从库上的WAL Receiver断开)
    • 实现:检测可以使用主动/定时脚本,也可以直接访问pg_exporter,或者由Agent定期向DCS汇报。
    • 触发:主动式检测触发,或监听DCS事件。触发结果可以是调用中控机上的HA脚本进行集中式调整,也可以由Agent进行本机操作。
  • 选主
    • Fencing P:同手动切换,因为自动切换中主库不可用,无法修改同步提交配置,因此存在RPO > 0 的可能性。
    • 遍历所有可达从库,找出LSN最大者,选定为C,最小化RPO。
  • 流量切换:需要快速自动化执行以下步骤
    • Promote C:提升候选主库C为新主库
      • 移除standby.signal 或 recovery.conf。执行promote
      • 自动生成候选主库C的新角色域名:.primary.
      • 调整集群主库域名/VIP解析:primary. ,指向C
      • 调整集群从库域名/VIP解析:standby.,摘除C(一主一从除外)
      • 根据新的角色域名重置监控(修改Consul Node名称并重启)
  • 拓扑调整
    • 重定向其他从库,将其primary_conninfo指向C,作为级连从库,滚动重启生效,并追赶新主库C。
    • 如果使用一主一从,之前C仍然承接读流量,则拓扑调整完成后将C摘除。
  • 修复旧主库P(如果是一主一从配置且读写负载单台C撑不住,则需要立刻进行,否则这一步不紧急)
    • 修复有以下两种方式:Rewind,Remake
    • Rewind P:(可选)将旧主库Rewind后作为新从库(如果只有一主一从则是必选)
      • 运行pg_rewind,如果成功则继续,如果失败则直接重做从库。
      • 修改recovery.conf(12-)|postgresql.auto.conf(12),将其primary_conninfo指向C
      • 自动生成P的新角色域名:< max(standby_sequence) + 1>.standby.
      • 集群从库域名/VIP解析变更:standby.,向S中添加P,承接读流量
      • 根据角色域名重置监控
    • Remake P:
      • 以新角色域名< max(standby_sequence) + 1>.standby.向集群添加新从库。
最后修改 2021-01-21: update (73df78a)