当前位置: 首页 > 工具软件 > Weave > 使用案例 >

分布式之基石: 可靠性——What a tangled web we weave

袁骏祥
2023-12-01

总结阅读DDIA和凤凰架构的感想和笔记。

可靠性

Oh what a tangled web we weave,
When first we practice to deceive.
——Walter Scott

每当我们在日常讨论起软件系统的可靠性时,总是用模糊的定义或是一种高度理想化的期望来描述它:

  • 在给定规模的输入,软件正常工作且在预期时间内输出。
  • 在错误或不规范使用的场景下,软件仍能按用户期望输出。
  • 有效限制恶意访问。

开发和测试保障可靠性

在敏捷开发“轻文档,重沟通”的系统中,我们常常将系统的有效工作负载、在什么场景下应当有怎样的输出以用户故事的方式提出,往往规定的是正常流程,但对于错误、不规范的输入则较少以文档约束,其他开发模式中实际也很少用非常详实的文档约束什么是错误和不规范(或许有,但笔者未接触过)。

为此在工作中,我们建立了代码评审、结对编程和测试等流程,以隐含的、在开发者之间流传的编程规约,以及测试工程师以三方视角来检验可靠性,这被事实证明可以有效地保障系统的局部可靠性。但经验也是用试错换来的,每一条编程规约的背后,都是无数程序员血泪的教训。

只要是经验,在软件开发过程中,研发总会疏忽,代码总会有缺陷,我们必须正视人为错误的发生,逢山开路,遇水架桥。在开发测试层面,提升可靠性常用的手段有:

  • 文档评审:产品经理在完成需求文档PRD的编写后,和研发以及测试工程师讲解需求背景、业务目标和业务效果。研发工程师在完成技术方案后,和其他研发以及测试工程师采取合适机制评审,探讨技术细节。
  • 选择合适的开发模型,瀑布式开发、敏捷开发、XP等,他们各自有各自的方法论,比如敏捷开发可以考虑采取测试驱动开发(TDD),XP中的结对编程。
  • 单元测试、代码评审等,避免编程错误和常见的开发经验错误,保障代码质量最基本的手段。
  • 联调测试:重点测试各模块各组件的接口,确认接口正确对接,业务功能符合设计。
  • 系统测试:完成整体端到端的业务逻辑的测试,确保在给定规模下,系统能够正常工作,且非功能指标达到要求。
  • 验收测试:由产品、客户等确认系统的完备性,以及符合其业务预期。
  • 回归测试:在持续交付上,系统的演进需要通过回归测试确保变更外的功能也能够正确工作。
  • 自动化测试、非功能测试等等常见测试手段与流程。

硬件的可靠性

在整个软件生命周期中,我们提到了开发和测试,他们通过经验和规约在一定程度保障可靠性。当软件投产工作时,硬件损坏、接错网线、断电等也是不可忽略的问题。

除了人为导致的故障,硬件也有可能出现故障,服务器的各个元件在工作中收到物理环境的影响,会导致数据出现错误、损坏,硬件设计者们为此加入了纠错码来发现或纠正数据的正确性,但纠错码能够纠错的位数是有限的,纠错码甚至自身都有可能因为物理环境的影响而错误;硬件存在使用寿命,元件老化导致不正确的工作,出现错误的运算或是无法工作。这些都是小概率事件,为减少这些问题出现的概率,数据中心和机房每一条管理条例的背后都是无数运维血泪的教训。

我们常用失效率、平均失效间隔(MTBF)、平均无故障时间(MTTF)和平均修复时间(MTTR)来描述硬件的可靠性。

平均失效率:
λ = N f Δ t ⋅ N λ = \frac{N_f}{Δt \cdot N} λ=ΔtNNf
Δt: 试验的时间区间;
N: 试验样本总数;
Nf: 试验区间内失效的样本总数。
λ越小越好。

MTTF:
Q = M T T F = 1 N ∑ i = 1 N T i Q = MTTF = \frac{1}{N} \sum_{i=1}^N T_i Q=MTTF=N1i=1NTi
N: 试验样本总数。
Ti: 同等试验条件下,测得的所有寿命数据T1,T2,…,Tn
MTTF越大,意味着系统可靠性越高。

MTTR:
M c t ‾ = M T T R = 1 N ∑ i = 1 N R i \overline{M_{ct}} = MTTR = \frac{1}{N} \sum_{i=1}^NR_i Mct=MTTR=N1i=1NRi
N: 试验样本总数。
Ri: 记录得到第i个样本的一次从故障到恢复的维修时间。
MTTR越小,意味着修复时间就越短。

MTBF从定义上是相邻两次故障之间的平均工作时间,包含了MTTR和MTTF,因此:
M T B F = M T T R + M T T F MTBF = MTTR + MTTF MTBF=MTTR+MTTF
实际生产,MTTR相比MTTF通常可以忽略不计,因此MTBF就约等于MTTF,越大可靠性也就越高。

根据以上指标,选择更加可靠的硬件设备和基础设施能够减少错误的发生。

运维的可靠性

经统计,运维错误带来的故障占总体故障的大头,而运维错误中占大头的是配置错误,人总是不可靠的,除去上一小节的常用手段、方法论,对于运维层面的错误的常规解决方案很少:

  • 明确清晰的监控指标以及告警:非功能指标以及错误率,当非功能指标持续低于阈值、错误率高于阈值,向研发和运维发起告警,并
  • 蓝绿发布、灰度发布、滚动发布、金丝雀发布等,根据软件系统特性选择合适的部署手段,确保错误配置可快速回滚或及时得到控制。
  • 合适的管理机制以及充分培训,不仅仅是开发测试阶段的流程管理、运维中的流程管理以及培训至关重要…

只有这些吗?近些年随着CI/CD理念和相关工具链的不断完善,DevOps的思想也被提出,目前仍在探索实践中。

网络的不可靠性

网络是可靠的吗?这里不得不提大名鼎鼎的Fallacies of distributed computing(打不开?需要魔法),号称分布式系统八宗罪:

  • The network is reliable,网络是可靠的。
  • Latency is zero,网络不存在无延时。
  • Bandwidth is infinite,带宽是无限的。
  • The network is secure,网络是安全的。
  • Topology doesn’t change,网络拓扑是不变的。
  • There is one administrator,总存在一个管理员。
  • Transport cost is zero,不存在传输成本。
  • The network is homogeneous,网络是同质化的。

网络中的信息,从用户到数据中心,数据中心内部之间、数据中心之间,经过许多设备,这些跳转设备并非一成不变。回想以前学过的网络设备的工作机制、路由协议,上一节也说了硬件故障不可避免,人为错误或是硬件故障导致的网络分区可能发生。网络从设计上就天然带着不可靠性。即使是号称可靠的TCP协议,其可靠也只是对数据进行ACK确认,并不是确保数据一定到达、数据一定不出错、一定可以得到响应。

为了减少数据出错,我们在传输层协议之上又定义了各个应用层协议,在这些协议中加入校验纠错机制;为了尽可能保证数据到达和得到响应,我们采取超时重传机制。这一切只不过是减少错误的发生,而错误是必然发生的,只是概率大小问题。

考虑这些情况:

  • 请求和响应丢失,在不可靠的网络中,请求在经历一些节点后丢失了。
  • 请求和响应在所经历网络的各个节点的硬件层、操作系统层或应用层连接上排队。
  • 被请求节点宕机、下线。
  • 采用一些语言编写的程序,可能出现因GC带来的暂停,又或者是处理繁忙,线程未及时被调度唤起执行,此时请求和响应会延迟或可能永远不会发出(OOM导致进程崩溃)。

对于确认请求是否被接收,我们唯有采取超时机制,当在一定时间内没有收到响应,就认为请求或响应丢失了。如果请求是非幂等的,重新请求可能会导致数据的不一致性,构建可靠的系统,一般设计要求请求和处理请求是幂等的。

如果是因为目标节点繁忙未及时响应,那么超时重试会加剧目标节点的负载。对于这一点,可以考虑实现系统的横向扩展、负载均衡、服务降级和熔断机制等。对于故障节点,需要有故障检测的机制,将流量转发至其他对等节点,这一设计较复杂,暂且不表。

这里引出一个问题,超时配置多久合适?目前还没有一套理论可以给出最优值,在系统上一般被作为配置项交给用户和运维调整,更多的是使用经验值。

容错

既然基础设施和软件系统总是不可靠的,那么现在一个个商业服务、流行开源框架宣称的高可靠又是如何做到的呢?那自然是避免错误和容忍错误。

为了避免系统故障,最简单的方法是冗余硬件设计,比如磁盘组RAID、UPS,采用带有ECC的企业级内存,从管理上遵循一条条机房管理条例,数据多副本等等。简单来说,就是加钱,加硬件的钱和为运维人员培训的钱。

硬件故障不可避免,而我们要避免的是硬件故障带来的系统失效,实现容错。相比如今硬件的发展和运维设施的完善程度,进一步提升硬件可靠性的成本十分高昂,加钱的效果也不好啦,这也就是常说的边际效应。

相比避免错误,容忍错误是另一种可行的方法,又或者说是转换思路,承认硬件和系统必然会故障,允许单机因为硬件和系统缺陷故障失效,通过在不可靠的机器和系统上建立可靠的应用。基于这一思路,我们看到了许多弹性的云服务、容器化的基础设施;带着天然不信任存储设备设计多副本、持续回读文件比较的存储系统(如HDFS, S3);微服务、Service Mesh架构理念的提出。

这里的容错设计是相当庞大且复杂的架构,后续再系统总结。

小结

以上,我刻意地忽略了安全问题,如果有人或组织带着恶意访问我们的系统,或许一些简单的破坏就能导致整个系统崩溃。这些会在后续对认证授权小节进行系统总结。从硬件层面的破坏也存在,如RawHammer DRAM硬件设计上的物理漏洞,此前沸沸扬扬Intel CPU层面的Spectre、Meltdown漏洞。

在大型系统中,局部的错误是可能逐步累积的,最终演化成一场生产事故。从产品、开发、测试到运维都需要考虑如何保障可靠性。从产品设计上,需要明确业务场景,约束和规范错误的使用场景;从架构上,需要采取容错设计;在开发测试和运维上,需要规范流程。保障整个系统的可靠性是一场持续的战争,需要产品、研发、测试和运维的整体协作。幸运的是,前人已经为我们总结出许多编程规范、容错的架构设计等,我们还看到CI/CD的发展,以及DevOps理念的提出,从工具和方法论层面丰富了我们的武装。

我们在可靠性上撒了无数的谎,从硬件基础设施、软件基础设施(操作系统等对于软件系统来说的运行环境),到软件系统,再到运维维护,处处不可靠。我们总是信任基础设施的可靠性,毕竟开发一个功能时谁会去考虑内存工作故障和CPU运算错误呢?基于此,我们不得不在开发功能时”忽略“这些故障,但又不得不为整个应用系统设计容错,对外提供基于统计学意义上的SLA。
不得不感慨,what a tangled web we weave…

 类似资料: