本文简单介绍一种分布式授时机制,用纯软件思路实现了一套去中心化,多点授时的机制。cockroachDB就用这套机制来保证事务的ACID特性。下面我用一个简单的FAQ带大家快速熟悉HLC:
FAQ
Q:为什么要用混合逻辑时钟?
A:分布式系统保证事件顺序。
Q:为什么不用物理时钟?
A:物理时钟要么误差大的感人,要么贵的感人,硬件搞不定就需要软件来搞定。
Q:混合是谁和谁混合?
A:物理时钟和逻辑时钟混合。物理时钟误差大,逻辑时钟来辅助。减少分布式系统的不确定性。
Q:逻辑时钟遵循什么逻辑?
A:(核心思想)每当节点收到外部事件时,它会根据收到的时间戳调整自己的物理时钟。因为发来消息的时间肯定小于自己收到消息的时间,如果误差导致此时本地物理时钟小于该消息的时间,那么就不看物理时钟,而是用消息时钟更新本地时钟!
Q:看起来这么麻烦,为什么不用中心授时?
A:因为分布式的终极目标就是去中心化啊,如果每个节点以来一个中心,那么伸缩性和灵活性就会下降很多,还会增加很多网络通信成本。
Q:说了那么多,HLC有什么坏处吗?
A:当然是有的,既然没有一个中心作为绝对公平的裁判,所以总有节点之间误差大到逻辑时钟都解决不了的时候,物理时钟失效之后,数据一致性被破坏的风险显著增加。
例子
pt是本地时钟,L是高位时钟也就是做物理时钟,C是低位时钟也就是逻辑时钟。如果节点生成一个新的事件,它会用物理时钟与逻辑时钟的组合来表示该事件的时间。
有一个消息A→B→C→D→B,每次收发消息都会会修改每个节点的HLC,B2时刻B发现本地物理时间落后于另一个节点的物理时间,就用那个物理时间10作为高位,同时B2作为接收方,A1作为发送方,那么B2时间戳肯定要比A1大。
如果B2没有低位逻辑时钟,而是直接把高位物理时钟改成11,那肯定不合适,因为这是物理时钟,随便+1(单位都不能统一)那是脱离物理世界会出事的,所以为了表明A1和B2的顺序,又添加了一个低位逻辑位,表明A1和B2逻辑上的先后关系。
所以每个节点的时间顺序就一目了然了,可以看到除了A节点,BCD的本地物理时钟都失效了,被逻辑时钟替换了,而之后这些节点的时间戳增长完全脱离了本地物理时钟
code
代码比较复杂,需要考虑本地事件更新时间和接受其他节点消息更新时间两种情况,只需记住如果事件时间戳太大导致本地物理时间失效,则不管物理位置用+1逻辑位。如果事件没有影响本地物理时间,则HLC还是按照物理时间增加,逻辑时间就置0
#include <iostream>
#include <algorithm>
class HybridLogicalClock {
public:
int localTime; // 本地物理时钟(物理时间戳)
int localCounter; // 本地逻辑时钟(计数器)
// 构造函数:初始化时钟值为0
HybridLogicalClock() : localTime(0), localCounter(0) {}
// 处理本地事件
void handleLocalEvent() {
// 记录本地事件发生前的物理时钟值
int previousLocalTime = localTime;
// 更新物理时钟,确保本地时钟不回退
localTime = std::max(localTime, getCurrentTime());
// 根据物理时钟是否更新,调整逻辑时钟
if (localTime == previousLocalTime) {
localCounter++; // 物理时钟没有回退,逻辑时钟递增
} else {
localCounter = 0; // 物理时钟发生了回退,重置逻辑时钟
}
// 输出事件的时间戳(物理时钟和逻辑时钟)
std::cout << "Local event timestamp: (" << localTime << ", " << localCounter << ")\n";
}
// 处理接收到的外部消息
void handleReceivedEvent(int receivedTime, int receivedCounter) {
// 记录接收前的物理时钟值
int previousLocalTime = localTime;
// 更新物理时钟,确保不回退
localTime = std::max({localTime, receivedTime, getCurrentTime()});
// 根据物理时钟的变化来调整逻辑时钟
if (localTime == previousLocalTime && receivedTime == previousLocalTime) {
localCounter = std::max(localCounter, receivedCounter) + 1; // 不同事件顺序,但物理时钟一致
} else if (localTime == previousLocalTime) {
localCounter++; // 本地时钟没有变化,递增逻辑时钟
} else if (localTime == receivedTime) {
localCounter = receivedCounter + 1; // 本地时钟更新,接受的消息时间戳更新
} else {
localCounter = 0; // 时钟回退,重置逻辑时钟
}
// 输出接收到的消息的时间戳(物理时钟和逻辑时钟)
std::cout << "Received event timestamp: (" << localTime << ", " << localCounter << ")\n";
}
private:
// 获取当前本地时间戳(模拟物理时钟)
int getCurrentTime() {
return localTime + 1; // 每次调用返回本地时间的递增值
}
};
int main() {
HybridLogicalClock clock;
// 处理本地事件
clock.handleLocalEvent(); // 输出:Local event timestamp: (1, 1)
// 处理接收到的事件(例如,来自其他节点的消息)
clock.handleReceivedEvent(3, 5); // 输出:Received event timestamp: (3, 6)
// 再次处理本地事件
clock.handleLocalEvent(); // 输出:Local event timestamp: (4, 1)
// 再次接收消息,模拟时钟的回退情况
clock.handleReceivedEvent(2, 3); // 输出:Received event timestamp: (3, 4)
return 0;
}
最后打个广告,了解开源时序数据库TDengine 更多内容欢迎访问:TDengine