总结阅读DDIA和凤凰架构的感想和笔记。
Oh what a tangled web we weave,
When first we practice to deceive.
——Walter Scott
每当我们在日常讨论起软件系统的可靠性时,总是用模糊的定义或是一种高度理想化的期望来描述它:
在敏捷开发“轻文档,重沟通”的系统中,我们常常将系统的有效工作负载、在什么场景下应当有怎样的输出以用户故事的方式提出,往往规定的是正常流程,但对于错误、不规范的输入则较少以文档约束,其他开发模式中实际也很少用非常详实的文档约束什么是错误和不规范(或许有,但笔者未接触过)。
为此在工作中,我们建立了代码评审、结对编程和测试等流程,以隐含的、在开发者之间流传的编程规约,以及测试工程师以三方视角来检验可靠性,这被事实证明可以有效地保障系统的局部可靠性。但经验也是用试错换来的,每一条编程规约的背后,都是无数程序员血泪的教训。
只要是经验,在软件开发过程中,研发总会疏忽,代码总会有缺陷,我们必须正视人为错误的发生,逢山开路,遇水架桥。在开发测试层面,提升可靠性常用的手段有:
在整个软件生命周期中,我们提到了开发和测试,他们通过经验和规约在一定程度保障可靠性。当软件投产工作时,硬件损坏、接错网线、断电等也是不可忽略的问题。
除了人为导致的故障,硬件也有可能出现故障,服务器的各个元件在工作中收到物理环境的影响,会导致数据出现错误、损坏,硬件设计者们为此加入了纠错码来发现或纠正数据的正确性,但纠错码能够纠错的位数是有限的,纠错码甚至自身都有可能因为物理环境的影响而错误;硬件存在使用寿命,元件老化导致不正确的工作,出现错误的运算或是无法工作。这些都是小概率事件,为减少这些问题出现的概率,数据中心和机房每一条管理条例的背后都是无数运维血泪的教训。
我们常用失效率、平均失效间隔(MTBF)、平均无故障时间(MTTF)和平均修复时间(MTTR)来描述硬件的可靠性。
平均失效率:
λ
=
N
f
Δ
t
⋅
N
λ = \frac{N_f}{Δt \cdot N}
λ=Δt⋅NNf
Δ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=1∑NTi
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=1∑NRi
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(打不开?需要魔法),号称分布式系统八宗罪:
网络中的信息,从用户到数据中心,数据中心内部之间、数据中心之间,经过许多设备,这些跳转设备并非一成不变。回想以前学过的网络设备的工作机制、路由协议,上一节也说了硬件故障不可避免,人为错误或是硬件故障导致的网络分区可能发生。网络从设计上就天然带着不可靠性。即使是号称可靠的TCP协议,其可靠也只是对数据进行ACK确认,并不是确保数据一定到达、数据一定不出错、一定可以得到响应。
为了减少数据出错,我们在传输层协议之上又定义了各个应用层协议,在这些协议中加入校验纠错机制;为了尽可能保证数据到达和得到响应,我们采取超时重传机制。这一切只不过是减少错误的发生,而错误是必然发生的,只是概率大小问题。
考虑这些情况:
对于确认请求是否被接收,我们唯有采取超时机制,当在一定时间内没有收到响应,就认为请求或响应丢失了。如果请求是非幂等的,重新请求可能会导致数据的不一致性,构建可靠的系统,一般设计要求请求和处理请求是幂等的。
如果是因为目标节点繁忙未及时响应,那么超时重试会加剧目标节点的负载。对于这一点,可以考虑实现系统的横向扩展、负载均衡、服务降级和熔断机制等。对于故障节点,需要有故障检测的机制,将流量转发至其他对等节点,这一设计较复杂,暂且不表。
这里引出一个问题,超时配置多久合适?目前还没有一套理论可以给出最优值,在系统上一般被作为配置项交给用户和运维调整,更多的是使用经验值。
既然基础设施和软件系统总是不可靠的,那么现在一个个商业服务、流行开源框架宣称的高可靠又是如何做到的呢?那自然是避免错误和容忍错误。
为了避免系统故障,最简单的方法是冗余硬件设计,比如磁盘组RAID、UPS,采用带有ECC的企业级内存,从管理上遵循一条条机房管理条例,数据多副本等等。简单来说,就是加钱,加硬件的钱和为运维人员培训的钱。
硬件故障不可避免,而我们要避免的是硬件故障带来的系统失效,实现容错。相比如今硬件的发展和运维设施的完善程度,进一步提升硬件可靠性的成本十分高昂,加钱的效果也不好啦,这也就是常说的边际效应。
相比避免错误,容忍错误是另一种可行的方法,又或者说是转换思路,承认硬件和系统必然会故障,允许单机因为硬件和系统缺陷故障失效,通过在不可靠的机器和系统上建立可靠的应用。基于这一思路,我们看到了许多弹性的云服务、容器化的基础设施;带着天然不信任存储设备设计多副本、持续回读文件比较的存储系统(如HDFS, S3);微服务、Service Mesh架构理念的提出。
这里的容错设计是相当庞大且复杂的架构,后续再系统总结。
以上,我刻意地忽略了安全问题,如果有人或组织带着恶意访问我们的系统,或许一些简单的破坏就能导致整个系统崩溃。这些会在后续对认证授权小节进行系统总结。从硬件层面的破坏也存在,如RawHammer DRAM硬件设计上的物理漏洞,此前沸沸扬扬Intel CPU层面的Spectre、Meltdown漏洞。
在大型系统中,局部的错误是可能逐步累积的,最终演化成一场生产事故。从产品、开发、测试到运维都需要考虑如何保障可靠性。从产品设计上,需要明确业务场景,约束和规范错误的使用场景;从架构上,需要采取容错设计;在开发测试和运维上,需要规范流程。保障整个系统的可靠性是一场持续的战争,需要产品、研发、测试和运维的整体协作。幸运的是,前人已经为我们总结出许多编程规范、容错的架构设计等,我们还看到CI/CD的发展,以及DevOps理念的提出,从工具和方法论层面丰富了我们的武装。
我们在可靠性上撒了无数的谎,从硬件基础设施、软件基础设施(操作系统等对于软件系统来说的运行环境),到软件系统,再到运维维护,处处不可靠。我们总是信任基础设施的可靠性,毕竟开发一个功能时谁会去考虑内存工作故障和CPU运算错误呢?基于此,我们不得不在开发功能时”忽略“这些故障,但又不得不为整个应用系统设计容错,对外提供基于统计学意义上的SLA。
不得不感慨,what a tangled web we weave…