之前写过一篇文章, 介绍"可靠通信三原则". 对于一个分布式数据库, 如果想实现 100% 高可用(也即客户端的请求永远不会返回失败), 同样可以用可靠通信三原则中的重试理论和去重理论来解决. 但在实践上, 需要在成功率, 耗时(速度和性能)各方面进行取舍. 本文分享实际经验, 介绍什么样的选择是普适的, 各位可以参考.
客户端访问数据库服务器, 发起大量的请求, 绝对不可能做到每一个请求都是成功的. 因为网络原因, 请求可能失败. 因为服务器内部处理冲突, 或者分布式节点间协调冲突, 都可能导致请求失败.
所谓容错处理, 就是在遇到错误的时候进行重试. 因为错误必然发生, 只有重试才能消除错误的影响, 就好像 IP 层必然会丢包, 但 TCP 协议通过重传达到某种程度的可靠传输.
某些实现了 Basic Paxos + 日志复制状态机模型的系统, 因为所谓的"Leaderless", 会产生大量冲突. 即使是使用 Raft, 在某些情况下意外发生选举, 也会导致请求冲突.
面对冲突(失败)到底应该由谁来重试呢? 这涉及到工程实践上模块职责划分的问题, 模块职责的划分, 往往比代码实现更重要. 一般来说, 发生重试的位置越底层, 性能会越好; 发生重试的位置越上层, 判断是否应该重试的依据就能更全面.
我们简单把数据库系统(生态)划分为几个大的模块, 从底层(左)到上层(右)是:
replication->server->clientSDK->user
最常见的做法是让 user 自己重试, 例如常见的 Redis SDK, 如果某台 server 宕机导致请求失败, 那么要求用户换一个 IP, 重新创建连接, 再次重复请求.
某些系统会封装专属的 client SDK, 例如, 把官方的 Redis SDK 做一下简单封装, 拦截每一个请求的结果, 如果发现错误, SDK 内部就自动重试. 这样做, user 就不需要有重试逻辑, 代码可以简化. 是这样的, 多个协作的模块, 如果某个模块揽了一些职责, 那它的上层模块就能省些工夫.
如果 user 既不想重试, client SDK 也不想重试, 那怎么办呢? 能不能把职责全推给 server 呢? 绝对不可能, 参见这篇文章的总结. 那么, 为什么 SDK 重试之后, user 就不需要重试呢? 因为 SDK 和 user 是在同一个运行空间内, 它们是一个整体, 两者之间没有可靠传输问题.
那么, 既然 client SDK 必须有重试逻辑, server 是否就不需要有重试逻辑了呢? 理论上可以, 但实践上, server 自身依然要降低自己的故障率, 降低故障率的必要手段就是重试. 例如, server 请求 paxos 模块同步一条操作日志, 但因为非预期的 multi-master 出现, 导致和其它节点争抢同一个位置失败, 这时, server 如果直接报错给 client, 那么, server 的故障数量就加一. 但是, server 可以重试, 再次调用 paxos 模块, 去争抢下一个位置, 直到成功. 这样, client 就会很少见到 server 报错.
但是, 无论是 server 还是 client 都不可能无限次重试, 因为每一次重试都会消耗时间, 最极端的情况可能要重试几个小时直到永远, 这当然不行, 所以, 需要引入超时机制, 重试一定次数之后即使还是失败, 也必须报错给上层.
重试会增加总的耗时, 这样, 给上层带来的不好效果就是, 上层觉得下层速度慢, 性能差. 所以, 必须有系统思维, 做出判断, 做综合的取舍. 从经验上看, 无论 server 还是 client SDK, 都必须分析细化, 尽可能重试, 以提高成功率. 大部分情况下, 开发者往往过多地放弃重试, 而较少地进行重试, 毕竟, 多一种重试场景, 就多写一段代码, 人总是会想偷懒的.
要设计一个高可靠的系统, 可靠传输三原则是非常有用的基础理论, 但不是银弹. 本质上, 软件开发就是大量的分析细化体力活, 以及对系统复杂度的把控.
重试带来的额外问题就是去重, 这也是可靠传输三原则里的第二项原则. 你可能听过"幂等性"这个词汇, 和去重是一回事. 如果一个操作是非幂等的, 那么, 就不能重试.
但是, 实践上, 我们可以把幂等性的职责向上推, 尽可能推给上层. 毕竟, 至少对于 user 来说, 100% 的成功率, 优先级比对幂等性的疑虑要高得多. 用户同意下层不考虑幂等性, 而大胆地去重试, 但是, 对下层偶然的失败会非常敏感. 简单说就是:别管什么幂等性, 在超时时间限制以内, 大胆重试!
转载请注明:IT运维空间 » 运维技术 » 分布式数据库系统的容错处理 – 100% 成功率, 超时和性能
发表评论