五⼤实例详解,携程Redis跨机房双向同步实践
前⾔
在⼀⽂曾和⼤家分享过携程在 Redis 双向同步⽅⾯的⼼得,简单介绍了实现⼀个 Redis 双向同步系统中可能⾯临的问题,以及其中⼀种问题(分布式⼀致性)的部分处理⽅案 -- CRDT(Conflict-free ReplicatedData Types)。本⽂将进⼀步阐述在具体设计和落地过程中的⼀些细节, 希望对⼤家能够有所帮助。包括:
Cycle Break -- 如何打破盗梦空间的⽆限循环
Last Write Wins & Vector Clock -- 冲突的解决既简单⼜复杂
Tomstone -- 忆往昔才能看今朝
GC -- CRDT 取经之路的通天河
Expire -- ⼀致 or 不⼀致, 这是个问题
相信通过对这些问题的描述和解答, ⼤家对于如何实现⼀个双向同步的 Redis 会有⼀幅清晰的构图。
⼀、Cycle Break -- 如何打破盗梦空间的⽆限循环
1.1 复制回环
以下图为例,假设 A <-> B <-> C 三个 Redis 建⽴起了双向复制关系。现在客户端先向其中⼀个 Redis(假设 A)发送了命令,SET KEY VAL(将 key 的值,设置或更新为 val),那么⼤概率会发⽣以下这样的步骤:
1)A 将 SET KEY VAL 同步⾄ B 和 C
redis五种数据结构
2)B 和 C 接收到操作后,⼜再次同步给其他两个 Redis
3)如此循环往复 ...
综上所述,复制回环所带来的问题结合普通的数据结构,会带来以下问题:
⽹络风暴
数据不⼀致
1.2 如何解决
如何解决这个问题呢,⽆⾮以下⼏种⽅式:
1)在数据上做处理,使数据携带⼀定的信息,服务端通过对数据所携带信息的甄别,过滤掉冗余消息。
2)在内容分发上做处理,服务端能够识别不同的链接类型,从⽽做到有的放⽮,在同步数据之初便加以控制;
针对 Redis 这种场景,我们选择了第⼆种处理⽅案,既在复制数据的时候,根据数据来源的类型,来决策是否同步给其他 Redis。
为了⽅便⼤家理解, 这⾥简单介绍⼀下 Redis 的内部实现:Redis 对于每⼀个TCP链接,都会抽象成为⼀个叫 client 的对象,见下图。⽽其中有⼀个 flag 表⽰了这个链接(client)对应的类型,这就很好地契合了上⽂中列举出的第⼆条选项。
所以,我们最终的处理⽅案是:Redis对数据源进⾏甄别,只有属于来⾃客户端的操作,才会被选择性
地同步给 Peer Master。然⽽,对于传统的 Master-Slave 架构来讲,还是会把所有对数据库有变更的操作,都同步给 Slave。
⼆、Last Write Wins & Vector Clock -- 冲突的解决既简单⼜复杂
这⾥以⼀对简单的 K/V 为例,介绍下是如何处理冲突的。
2.1 冲突是如何产⽣的
下⾯⼀幅图很好地诠释了,为什么会有冲突以及冲突的后果。
假设我们在同⼀时刻,分别在两个互相同步的 Redis 上更新了⼀个 Key,左边的试图将 Key 设置为 CAT,⽽后边的客户端试图将 Key 设置为 DOG。
那么总共会有以下 4 种结果,前两种虽然不尽如⼈意,但⾄少保证了数据的⼀致性。⽽后⾯两种则是⼤家不希望看到的,因为数据不⼀致对业务造成不可忽略的风险。
2.2 LWW -- Last Write Wins
其实解决这个问题也很简单,就是“最后写⼊为准”的原则。以下图为例,假设两个 Redis 分别收到了对于同⼀个 Key 的设值需求,那么我们就可以简单地根据这个原则来判定,最终的结果是最后⼀次的写⼊为准。
看到这⾥,⼤家也许会发现,原来冲突处理如此简单,那我也可以⼤展⾝⼿了。当然,⼤部分系统的实现,做到这⼀层,已经解决了分布式⼀致性的问题。但是,是不是这样就皆⼤欢喜了呢?
答案当然是否定的,继续往下看你就会发现,这⼩⼩的 K/V ⼀致性,只是分布式系统中的冰⼭⼀⾓。冰⼭的下⾯有着千奇百怪的洪⽔猛兽,⼀个没处理到,都会带来⽆可估量的业务损失。
2.3 时钟 -- 分布式系统永远的痛
相信部分同学在上学阶段或是⼯作以后,拜读过分布式系统的经典书⽬ --Distributed System Concept and Design (如下图)。这本书在开篇就对分布式系统有了⼀个经典的定义:
Concurrency
No Global Clock
Independent Failures
下⾯这个问题就是由时钟问题引起的。⼤家知道,不同的互联⽹组件之间是靠着 NPT-Server 这⼀⼯具来达到时间的⼀致性的,但是不同的⽹络区域之间的 NTP-Server 却并不⼀定是同步的。即使同步,时钟的准确性往往取决于⽹络的稳定性(这⼀点与⽹络延迟⽆关,也就是说即时延迟是中美之间⼤概 200~300ms,如果是稳定的延迟,那么 NTP-Server 的同步也基本稳定)。
如下图所⽰,在下⾯的 Redis(我们称之为 Redis-B)的⽹络域的物理时钟(Wall Clock),⽐上⾯的 Redis(我们称之为 Redis-A)的⽹络域的时钟慢,在 Redis-A 上⼀个很早的操作发⽣之后,Redis-B ⽅才收到关于同样Key的操作。从逻辑上讲我们更希望 Redis-B 的操作作为最终结果,然⽽由于时钟的快慢,如果使⽤ Last Write Wins 的策略,反⽽是早些时候在 Redis-A 上⾯的操作占了上风,最终值为VAL1。
2.4 Vector Clock
那么时钟快慢带来的问题,是否⽆可避免?其实未必。
以上⾯的问题为例,是不需要冲突处理的,只是单从 Wall Clock,我们⽆法判定逻辑操作的时间。所以引⼊了⼀个叫 Vector Clock 的逻辑时钟,来表⽰⼀个操作的发⽣时刻。
以下图为例,全局有两个点,我们通过两个向量来表⽰发⽣过的逻辑操作。
这⾥不展开讲了,具体 Vector Clock 是如何定义的,有专门的论⽂论述。⽽ AWS 闻名遐迩的 DynamoDB,更是通过对 Vector Clock 的理论改进,到了更加适合⾃⼰的⼀种叫 Version Clock 的理论依据,感兴趣的同学可以 Google 。
三、Tomstone -- 忆往昔才能看今朝
3.1 Delete
前⾯讲了数据回环复制的处理、数据⼀致性的处理,这样⼀个简单的分布式K/V 数据库就诞⽣了,但是删除操作依然会成为系统的“阿喀琉斯之踵”。
请看下图,假设我们已经存在了⼀个Key,在同⼀时刻 Redis-A(依照上⽂惯例,我们称上⾯的 Redis 为 Redis-A)收到了更新请求,设置 Key 为 VAL,⽽ Redis-B 却收到了 Delete 的命令,两个 Redis 互相同步之后,发现数据不⼀致了。
问题的根源在哪⾥呢?在于 Delete 操作,将 Redis-B 上的值删除了,当 SET KEY=VAL 的更新操作到达之时,便没有了可以⽐较的对象。
3.2 Tomstone
这个问题该如何处理?既然是没有对象可⽐,我们创造⼀个对象不就可以了吗?于是诞⽣了 Tomstone —— 被删除对象的栖⾝地。对象的删除,我们只做逻辑删除,并不会将对象真正地从内存中抹去,⽽是放置在⼀个叫做 Tomstone 的地⽅,让其他后续的命令,能够和之前的命令有⼀个对⽐。数据的存留与否也就有了判定的依据。
四、GC -- CRDT 取经之路的通天河
GC -- Garbage Collection,很多语⾔都有这个特性,像 Java,Go。⽆独有偶,我们这⾥所说的 GC,原则和这些语⾔⽆异,都是为了处理⼀类不再使⽤,但是⼜占有资源(通常是内存资源)的⼀些数据的回收。
4.1 GC 的痛点
上⼀⼩节,我们简单介绍了 Tomstone 的概念,GC 也是由于 Tomstone 的引⼊⽽带来的在实践中不得不⾯对的问题,如下图所⽰:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。