点击上方小坤探游架构笔记可以订阅哦
今天我同样结合《设计数据密集系统》来聊分布式系统另外一个话题,即分布式系统的不可靠时钟问题.
同步网络与异步网络时钟对比
在前面我们讲述了分布式系统网络不可靠的原因是由于我们数据中心服务通信采用的是异步网络实现, 同样地我们来看为什么同步与异步网络在时钟上也存在差异呢?对此我们可以先看下面同步与异步网络在时钟控制上的差异:
通过上述我们可以清晰地知道:
对此我们对同步网络以及异步网络的时钟精度做一个对比总结如下:
单调时钟以及日时钟的不可靠性
通过时钟可以来反映我们系统想要表达的时间问题, 一般在我们应用程序会拆分两类层次的含义:
持续时间: 描述的是一个事件发生的开始到结束整个过程经历的时间
比如一个请求的p99耗时是多少, 这个就是我们需要依赖时钟去测量应用程序请求开始到结束的持续时间.
时间点: 指事件发生的时候对应的那一时刻
比如我们的应用程序发生NullPointerException异常的时候日志系统对应的时间戳,这个就是时间点.
在现代计算机系统中时钟又拆分为单调时钟以及日时钟.即:
日时钟: 它根据某种日历返回当前的日期和时间, 也称为我们墙上的时钟时间, 比如Java的System.currentTimeMillis(), 用于表示时间点. 日时钟通常与网络时间协议(NTP)同步,这意味着一台机器上的时间戳(理想情况下)与另一台机器上的时间戳含义相同.然而由于存在硬件时钟与NTP协议的不稳定,不适合用于测量持续时间.
通过以上描述, 我们可以依赖日时钟来描述我们事件发生的时间点.但是不适合测量持续时间,为什么? 我们先来看下单调时钟:
单调时钟: 主要用于测量持续时间, 比如超时时间或者服务响应时间, 比如Java的System.nanoTime()就是单调时钟.需要注意一点就是单调时钟仅在单机单进程下计算对应的差异才有意义, 否则没有实际比较性, 因为该时钟可能是计算机启动以来的纳秒数,或者是类似的任意数值开始统计. 由于单调时钟不需要与NTP同步, 但如果NTP发现本地石英晶体振荡器比NTP走得快或者慢, 就会调整单调时钟向前走的频率, 即时钟微调.
我们可以看到单调时钟始终是被保证向前移动,因此在分布式系统中单进程内计算持续时间可以采用单调时钟而非日时钟.这是因为日时钟存在同步以及准确性问题如下:
因此在分布式系统中我们设计应用程序也需要充分考虑到日时钟存在不可靠的问题.
分布式系统依赖时钟带来的问题
依赖时间戳进行事件排序导致乱序
如果两个客户端向一个分布式数据库写入数据,那么谁先到达? 如果以最后写入为准机制(LWW)的话会发生什么问题呢? 如下所示(来自《设计数据密集系统》):
可以看到上述的时钟在不同节点的差异, 我们可以看下上述的逻辑处理过程:
在上述的例子中由于Node3节点与Node1节点存在时钟相差的间隔,导致Node1先写入的时间戳要大于Node3节点写入的时间戳, 从而导致Node2节点接收到其他节点的复制命令时候, 由于并发写冲突的存在且采用LWW机制最终导致`set x = 2`的命令被丢弃导致数据丢失.
那么有什么解决方案吗? 这里我们会用到一个概念称为逻辑时钟, 即基于全局递增计数器而非振荡的石英晶体, 同时在分布式数据库环境中, 要保证是全局性递增,逻辑时钟不测量具体日期时间或者秒数, 仅测量事件的相对顺序, 即事件发生在另一事件之前还是之后, 这样对于LWW机制解决并发写冲突是一种更为安全的选择.
而上述的单调时钟或者是日时钟我们称为物理时钟.但是逻辑时钟依赖因果关系实现并需要具备持久化等机制防止丢失,在实现上更为复杂.
STW引发数据写入安全问题
假如现在有一个数据库,并且每个数据库分区仅有一个主节点能够接受写入操作,同时数据库的主节点与其他副本节点通过租约实现共识,也就是Master节点持有一份从其他节点获取带有过期时间的租约, 并在在租约的有效期内是能够处理外部的写入请求并将命令复制到其他节点实现同步直到租约过期, 如下:
每个Node节点对应的伪代码如下:
上述代码存在什么问题? 主要有两个方面:
因此在分布式系统中我们必须要假定其中一个节点在执行过程中任何时刻都可能存在被暂停一段时间, 甚至是在函数执行的中间也不例外, 只有这样的假定基础上去设计我们构建的分布式系统才能确保我们数据的可靠性以及完整性.
全局快照的同步时钟
在上述我们提到使用全局单调递增的计算器来保证我们执行事务前后的顺序性,也就是说我们的数据库是分布在多台机器上甚至是多数据中心分布, 那么我们要实现一个全局的、单调递增的计数器就会变得很困难, 因为多数据中心存在跨区问题, 需要进行协调. 一般地我们要么会增加同步互斥锁机制, 类似数据库的悲观锁仅允许一个操作后再进行下一个操作, 但是这样会严重影响性能.
为了提升性能并兼顾对外部提供读取操作, 在我们数据库层面中会引入一个快照隔离机制, 对于那些支持小型、快速的读写事务又能支持大型、长时间运行的只读事务的数据库而言是一项非常有用的功能, 但是也存在同样的问题, 数据分布在多台机器怎么办呢? 快照隔离和我们处理事件的顺序性有共同之处: 事务B如果能够读取事务A的数据, 那么事务B的ID必须要高于事务A, 否则快照就是不一致的.
目前在业界中, 谷歌云Spanner就是基于日时钟作为事务ID实现多数据中心的快照隔离, 为什么它能够利用日时钟实现呢? 它主要采用TrueTime API明确报告本地时钟的置信区间(所谓的时钟置信区间, 我们可以理解为它是一个日时钟的时间段而不是一个时间点,即[最早时间,最晚时间]), 并基于以下观察结果:
我们对比下逻辑时钟实现以及TrueTime的不同维度:
分布式系统问题小结
最后我对前面阐述的分布式系统问题做一个总结如下, 即我们在搭建一个分布式系统时需要建立对应的系统模型, 思考在面临组件/服务故障、网络延迟、时钟依赖以及节点暂停等情况下要如何进行设计与取舍来保证分布式系统的数据可靠性.
你好,我是疾风先生, 主要从事互联网搜广推行业, 技术栈为java/go/python, 记录并分享个人对技术的理解与思考, 欢迎关注我的公众号, 致力于做一个有深度,有广度,有故事的工程师,欢迎成长的路上有你陪伴,关注后回复greek可添加私人微信,欢迎技术互动和交流,谢谢!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有