引子
在某种程度上来说,软件的复杂性是应对无处不在的错误所带来的。要想在不可靠的硬件、软件和网络的基础上构建可靠的系统,容错是必不可少的。
哪里会出错
要做到更好的容错、健壮和可靠,首先需要全面的梳理可能导致错误的源头和可能性。
要分析错误源头,则要首先分析应用及流程锁依赖的要素和环节。针对每一个要素和环节,推敲会出错的地方;要了解可预料到的错误,可以看看 Java 库或框架里的各种 Exception 。
机器节点
- 磁盘故障、内存耗尽、CPU 100% 占用、掉电;
网络
- DNS 故障、机架故障、路由器故障、设备故障、电缆故障;
- 连接中断、请求排队(延迟)、网络丢包、网络重传、网络拥塞、网络分区。
时间
- 很多监控统计依赖于时钟;
- 数据最终一致性的操作依赖于到达先后顺序;
- 同一台机器的时钟晶振可能受温度影响而波动;
- 不同机器上的时钟是不一致的,通过 NTP 协议同步;
- NTP 协议是经过网络的,这意味着网络的不稳定会影响时钟的同步;
- “跳秒”现象:1 分钟有 59s 或 61s ;
- 任务耗时过长,对外部来说就是无响应。
资源
- 资源不存在,比如文件不存在;
- 资源暂时不可用,比如端口已占用;
- 没有可用资源,比如连接池满;
- 资源路径已经被移动;
- 资源访问时的同步死锁。
数据
- 不符合预期格式的数据;
- 脏数据引起解析错误;
- 不一致的数据引起后继行为错误;
- 大对象数据引起 FullGC 导致响应不稳定;
- 错误配置;
- 非法请求获得正常资源;
- 恶意代码。
计算
- 溢出,不符合运算法则;
- 除零,无值可表示;
- 有限精度,浮点计算错误;
- 逻辑错误,比如越界、不正确的算法。
设计
- 设计不足或不合理,容易令人疏忽而导致误操作;
- 危险操作无确认、无提示,容易造成损失;
- 少数服从多数原则,达不到多数;
- Leader 的消息无法被其它节点接收,被其它节点判定为下线。
流程
- 中途取消操作;
- 逆向操作。
负荷
- 大流量超出应用承受负荷。
安全
- 非授权访问;
- 数据泄露;
- 数据被篡改;
- 访问拒绝。
并发
- 数据覆写:访问一个共享资源时,进程 A 获取锁,然后进入了 stop-the-world GC pause ; 进程 B 发现锁已过期,然后申请获得锁,进行数据写操作,接着释放锁;进程 A 结束 GC,进行数据写操作。 进程 A 将 进程 B 的写数据覆盖了。
拜占庭错误
- 当分布式系统里的节点要达成共识时,少数节点故意发送错误消息迷惑其它节点,以造成整体错误决策。比如航天领域防电子辐射干扰、多参与者协作和决策。
健壮性
健壮性是极为重要的程序质量属性。分为代码健壮性和业务健壮性。健壮性体现在代码和业务上的错误和异常处理上,避免整体失败、数据泄露、不一致、资损等故障。要做出健壮性好的设计和程序,就要预先思考清楚:流程中有哪些可能的错误和异常,每一种对应的处理措施是什么 ? 这样,才能让逻辑思维更加缜密,也是锻炼逻辑思维的一种有效之法。
- 代码健壮性体现在避免局部失败导致整体失败。常见考虑:参数校验以拦截不合法请求、越界异常捕获、JSON 脏数据异常捕获、类型转换异常捕获、底层异常捕获(连接异常、DB 异常、网络超时异常等)。
- 业务健壮性体现在业务的闭合环。在整个业务过程中会发生什么异常,导致什么问题(体验或资损问题),如何处理。比如同城异常检测要考虑商家同城呼叫失败后又快递发货的情形。
容错机制
思路与方法
- 设定系统假定,检测系统假定是否成立,然后在系统假定上构建系统;
- 聚焦高频错误:磁盘故障 > 服务器单机故障 > DNS 故障 > 机架故障 > 路由器重启;
- 错误提示规范:定义规范一致的错误码和错误消息;
- 快速失败并记录日志:适用于“请求检测,请求中含有错误或非法数据”的场景;
- 忽略失败并记录日志:适用于“不影响整体输出且不造成负面影响的极次要地方有点小问题”的场景;
- 预防策略:避免容易导致错误的做法;
- 冗余策略:冗余、替换、路由,见高可用部分;
- 重试策略:幂等;完全重试;补偿重试;
- 回滚策略:中途取消,重续执行很容易导致脏数据,考虑回滚操作;
- 故障恢复:监控、检测错误和故障、自动恢复;
- 乐观锁:递增的 fencing token ,防止过期写操作覆盖已经完成的写操作;
系统假定
-
同步模型假定:任何网络延迟、进程暂停、时钟错误都不可能超出某个上限值。即:有限的网络延迟;有限的进程暂停;有限的时钟错误。
-
部分同步模型假定:在同步模型假定的基础上,允许极少数的无法预测的超上限的网络延迟、进程暂停、时钟错误。
-
异步模型假定:对时序不做假定,难以预料事件何时发生和动作何时执行。
-
节点崩溃假定:节点突然失去响应,再也无法正常运行;
-
节点崩溃-恢复假定:节点可能在任何时候失去响应,在一段时间之后自动恢复并正常运行;易失性存储(比如内存)中的数据丢失,而持久性存储(比如磁盘)中的数据完好;
-
拜占庭假定:部分节点通过虚假消息欺骗其它节点,从而诱导作出错误的整体决策。
最常见的系统假定:部分同步模型假定 + 节点崩溃-恢复假定。
算法的正确性
- 正确性假定:算法满足某些指定性质。
- 达成预期结果、安全、活性。
重试
- 使操作满足幂等性质;
- 可以使用失败队列来记录失败的操作及失败信息、失败现场;
- 完全重试策略:整个操作从头开始执行,适合多读少写的长事务;
- 补偿重试策略:从失败现场的地方重续执行,适合多写且回滚代价昂贵的长事务;‘
- 完全重试策略,可指定重试次数;
- 可采用定时任务重试。
- 幂等:唯一索引、Token 机制(防页面重复提交)、分布式锁、select+insert、状态机幂等、查询/删除天然幂等。
可靠性实现
TCP可靠传输
- 网络的不可靠性: IP 数据包的丢失、重复、失序;
- TCP 连接可靠性: 三次握手、四次挥手;
- TCP 可靠机制:全双工连接、字节流、数据包有序编号、数据校验和、确认、定时器与超时重传、重复丢弃、流量控制(滑动窗口机制)、拥塞控制;
- TCP 重传机制: 超时重传和快速重传。超时重传依赖于超时时间的设定,快速重传依赖确认包的接收。可以说,选择不同的依赖因子决定了不同的解决途径,也就有不同的优点和缺点。
超时重传机制
TCP 发送数据包后,会启动相应的定时器,如果超时 RTO 还没有收到数据确认包,就会重新发送该数据包。
超时 RTO 的选择非常重要。若 RTO > RTT, 则可能网络利用率低,若 RTO < RTT 则可能拥塞。RTO 的选取基于 RTT 的统计及 BEB (二进制指数退避算法),重传次数及重传最大超时时间(RFC1122)。
RTO 估计: EWMA RTT 权重估计法(RTO = min(ubound, max(lbound,(SRTT)β)), RTO = (1-2)*SRTT, SRTT ← α(SRTT) + (1 − α) RTTs, α 取 80%-90%, β 取 1.3-2.0),标准估计法(查阅相关文献)。由于 RTO 通常至少是 RTT 的 2 倍,因此超时重传机制容易导致网络利用率不高。
快速重传机制
TCP 收到失序数据包后会立即发送确认包,意在告诉发送方迅速发送需要的数据包以填补“包漏洞”(可能存在包丢失)。如果有 dupthresh 个包重新确认了,则很可能存在包丢失,将启动快速重传机制,而不是依赖超时。也就是说,依赖确认包的快速发送和重复确认包来决定是否重传。
SACK (选择性确认)用来解决数据包重复和失序的问题,总是发送最近收到确认的序列号的范围值。发送者可以推理出最需要发送的未确认的包,并优先发送这些数据包。可以使用位图来检测没有收到的确认序号。快速重传更高效,不过会产生误重传问题。
重传的性能优化
- TCP 可以记录每一次的连接和传输,作为性能调优的依据(srtt 和 rttvar),可以做成自适应的;
- TCP 重传时,可以将多个数据包重组起来一起发送(总大小不能超过 MSS 和 MTU),以提升网络传输效率,也可以减轻重传数据包的二义性。
TIME_WAIT
TCP 连接关闭的过程中,客户端在发送服务端 FIN 包的 ACK 之后,进入 TIME_WAIT 状态,需要等待 2MSL 才能关闭。这是为了确保 ACK 可靠到达服务端。
参考资料
- 《 Designing Data-Intensive Applications》 : 第 8 章
- 《TCP/IP ILLustrated Volume 1: The Protocols》:第 12-17 章