redis cluster规范的官方文档,yinmingjun翻译




Redis cluster规范


Redis Cluster目标


Redis Cluster是Redis的一个分布式的实现,有下面这些目标,按设计上的重要程度列出:


  • 高性能和线性的扩展性,可以支持到1000个节点。
  • 在Redis的数据模型层面,没有必须的因为值大小和语义而要做的操作合并。
  • 写安全:系统尝试保留所有的来自客户端连接的到节点主体的写。然而,还是会存在写丢失的可能性。
  • 可用性:Redis Cluster能在主体的master节点群的可以连接到的情况下,并且每个连接不上的master节点都至少有一个可以连接的slave时,保障分区的可用性。


本文描述的是Github Redis的unstable代码分支。Redis Cluster目前进入了beta阶段,每个月都会有新的release,可以在Redis的web站点的download page找到。


 


实现的子集


Redis Cluster实现了所有的非分布式版本的Redis单key的命令。那些执行复杂的多key操作的命令,如Set类型的并集或子集的实现是假设所有的key都从属于相同的节点。


Redis Cluster的实现有一个hash tags的概念,可以强制那些key是从属于相同的节点。然而,在手工的reshardings的过程中,多key的操作会变得一段时间不可用,而单key操作总是可用。


Redis Cluster不支持多databases。只是选择database 0,并且SELECT命令是不可用的。


 


客户端和服务器在Redis cluster协议中的角色


在Redis cluster中,节点用于存储数据,并保存cluster的状态,包括映射key到正确的节点。Cluster的节点也能自动发现其他的节点,探测不能工作的节点,并在必要的情况下执行slave节点代master节点的选举。


未来执行所有cluster节点的会话,所有节点使用一个TCP总线和一个二进制的协议互联(cluster bus)。每个节点和每个cluster的其它的节点使用cluster bus连接。节点之间使用gossip协议传播cluster的信息,用于发现新节点,发送ping包来确保其它的节点都能正确的工作,并发送必要的cluster消息来传递针对特定条件的信号。cluster bus还用于传递跨越cluster的Pub/Sub消息。


由于cluster的节点不能作为客户端请求的代理,客户会根据节点返回的错误信息-MOVED和-ASK来重定向请求。客户端在理论上是可以想cluster中的所有的节点发送请求,在必要的时候来处理重定向,因此客户端不被要求了解cluster的状态。然而,客户端可以了解和缓存key和节点直接的映射关系,用一种明智的方式来提高性能。


 


写安全


Redis Cluster在节点之间使用异步的复制,因此总是有这样的可能会在写的时候丢失数据。然而,这些情况与一个客户端连接到master的主体,而另外一个客户端和master的一小部分相连的情况是非常不同的。


Redis Cluster会最大程度上保留客户端在master的主体中写入的数据,有2个例外:


1)一个写入也许到达一个master,但当这个master可以应答这个client的时候,写入也许还没有经过主从之间的异步复制传递到slaves。如果master在写到slaves之前死掉,这个写入会因为这个master因一个足够长的期间不能联系上而导致它的一个slaves被升级,从而这个写入会永久的丢失。


2)另外一个理论上存在的写丢失的错误的模式有:


  • 因为分区导致的一个master联系不上。
  • 被它的一个slaves发起故障恢复。
  • 过了一些时间,它有能联系上了。
  • 一个客户端使用未更新的路由表在这个master被cluster配置成一个slave(新master的)之前写入到这个节点。


实际上这是非常不可能发生的,因为节点如果足够长的时间不能联系上主体中的其它的master,就会触发故障恢复,也就不再接收写入,并且当分区被修复之后,在一小段时间之内还是会拒绝写入,使其它的节点能感知配置的变更。


当存在一个master的小分区连着一个或多个客户端的情况下,Redis Cluster会丢失不确定数量的对分区的写入,因为所有对master的写入都会因为主体的故障恢复而有潜在的丢失的可能性。


特别,对于一个即将执行故障恢复的master,它一定是不能没master的主体至少NODE_TIMEOUT的时长后没联系上,因此如果在这个时间内分区被修复,不会有写入丢失。


当一个分区丢失超过NODE_TIMEOUT时长,小分区的一侧会在超过NODE_TIMEOUT时长之后开始拒绝写入,所以在小分区形成到小分区变得不再可用之间存在一个最大化的窗口期,因为在这个时间之后不会再接收写入。


 


可用性


Redis Cluster在小分区一边变得不再可用。在主分区一侧假设存在每个不再可用的master都存在一个slave,cluster会在NODE_TIMEOUT时长加上几秒slave选举和对master的错误恢复的时间之后变得可用。


这表示Redis Cluster在设计上可以对抗几个节点失效的故障,但不是应用需要的面向大的网络分裂事件的可用性的完备的方案。


在这个例子中,一个cluster由N个master节点组成,每个节点都有一个slave,cluster的主体一侧将在一个单独的节点出现分区后继续可用,并在2个节点分离开之后,继续1-(1/(N*2-1))的概率保持可用性(在第一个节点失效之后,还剩总计N*2-1个节点,而单个master不包括复制失败的概率是1/(N*2-1))。


例如,5个节点的cluster每个节点有一个slave,是1/(5*2-1) = 0.1111,在两个节点从主体分离,cluster将不再可用,这个概率大概是11%。


感谢Redis Cluster的一个replicas migration的特性,由于支持副本迁移到独立的master的特征,提高了Cluster在真实世界的可用性(master不再有副本)。


 


性能


在Redis Cluster中,节点不会将指定的key转发到指定的节点,但会通过提供key的分区协助客户端重定向到正确的节点。


最终,客户端会获取并更新cluster中那些节点对应那些key的表达,因此在经过一些正常的活动后,客户端可以直接发送命令到正确的节点。


因为异步复制的使用,节点不会等待其它的节点的写操作的反馈(可以选择的同步复制正在开发中,未来可能会添加到release之中)。


另外,因为存在不支持多key命令的命令子集的限制,数据在除了resharding之外是不会在节点之间复制的。


所以,一般的操作就像单redis实例一样来处理。这表示一个有N个master节点的Redis Cluster可以具有单个Redis实例N倍的性能,作为设计上承诺的线性的扩展能力。同时,查询通常是单个的往返,由于clients通常保留到节点的持久连接,因此延时也会像单Redis实例一样。


对于高性能和高可扩展性,和相对弱一些(非CAP),但是合理的一致性和可用性的方式,是Redis Cluster的设计的主要目标。


 


为什么要避免合并操作


Redis Cluster设计上避免相同key-value对在多个节点之间的版本冲突,由于Redis的数据模型并不是总是可取的:Redis中的值通常非常大,一般是有数百万成员的列表或排序集合。或数据类型的语义复杂。传输或合并这些值会成为一个大的瓶颈,也许是应该在客户端一侧提供一个非通用的解决方案。


 


key的分布模式


key的空间被分成16384个槽,有效的cluster的节点的上限是16384个节点(然而建议的最大的节点数是~1000个节点)。


所有的master节点会处理16384个hash槽中一定比例的槽。当cluster处于稳定的状态,这表示cluster中没有重新配置的操作在处理(那样hash槽会从一个节点移动到另外一个节点)一个hash槽会明确的被一个节点来处理(但是处理的节点可以有一个或多个slave节点在出现网络分裂或故障的时候来替换它)。


用于映射key到hash槽的基本的算法是下面(读下一个小节来看hash tag的例外情况):


HASH_SLOT = CRC16(key) mod 16384


CRC16是指下面:


  • Name: XMODEM (also known as ZMODEM or CRC-16/ACORN)
  • Width: 16 bit
  • Poly: 1021 (That is actually x^16 + x^12 + x^5 + 1)
  • Initialization: 0000
  • Reflect Input byte: False
  • Reflect Output CRC: False
  • Xor constant to output CRC: 0000
  • Output for "123456789": 31C3


CRC16的16位输出的14位被使用(这就是为在什么在上面的公式中对16384取模的原因)。


在我们的测试中,CRC16在分发不同类型的可以到最终的16384个hash槽的过程中表现的非常优异。


注意:使用的是一个CRC16算法的参考实现,在附录A中能看到。


 


key的hash tag


在hash槽的计算上这里存在一个例外,就是实现hash tags。hash tags是一种保障两个key可以存在于相同的hash槽的方式。这用于在Redis Cluster中实现多key操作。


为了实现hash tags,hash slot的计算过程有所不同。基本上,如果一个key包含"{...}"的形式,那么只有{和}之间的字符串用于计算hash来获取一个hash槽。然而,由于存在这种可能性会出现多个{或},在算法明确了下面的规则:


  • 如果key包含一个{字符。
  • 如果在{的右边有一个}字符。
  • 存在一个或多个字符在首次出现的{和首次出现的}之间。


那么使用其来替代这个key来做hash,只有首次出现的{和其右侧首次出现的}之间的内容用来做hash。


例如:


  • 有两个key:{user1000}.following和{user1000}.followers将具有相同的hash槽,因为它们都使用user1000子字符串来计算hash槽的hash值。
  • key:foo{}{bar}的整个key会被用来计算hash值,因为它首次出现的{和其右侧首次出现的}之间没有字符。
  • key:foo{{bar}}zap的子字符串{bar会被用来计算hash值,因为它是首次出现的{和其右侧首次出现的}之间的字符。
  • key:foo{bar}{zap}的子字符串bar会被用来计算hash值,因为算法会在第一次的有效或无效({和}之间没有子字符串)的{和}的匹配之后停止。
  • 如果key以{}开头无论后面是什么,算法保证整个key会作为一个整体来算hash。这在使用二进制数据做key的时候会用到。


增加了这个hash tags的例外,下面是使用Ruby和C语言给出的HASH_SLOT函数的实现。


Ruby代码:


def HASH_SLOT(key)

    s = key.index "{"

    if s

        e = key.index "}",s+1

        if e && e != s+1

            key = key[s+1..e-1]

        end

    end

    crc16(key) % 16384

end

C代码:

unsigned int HASH_SLOT(char *key, int keylen) {

    int s, e; /* start-end indexes of { and } */

 

    /* Search the first occurrence of '{'. */

    for (s = 0; s < keylen; s++)

        if (key[s] == '{') break;

 

    /* No '{' ? Hash the whole key. This is the base case. */

    if (s == keylen) return crc16(key,keylen) & 16383;

 

    /* '{' found? Check if we have the corresponding '}'. */

    for (e = s+1; e < keylen; e++)

        if (key[e] == '}') break;

 

    /* No '}' or nothing between {} ? Hash the whole key. */

    if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;

 

    /* If we are here there is both a { and a } on its right. Hash

     * what is in the middle between { and }. */

    return crc16(key+s+1,e-s-1) & 16383;

}

Cluster节点的属性


cluster中的每个节点都有一个唯一的名字。节点的名字是一个160 bit的随机数,在这个节点首次启动的时候获取(一般使用/dev/urandom)。


节点会保存它的ID到节点的配置文件,并会永远的使用这个ID,直到这个节点的配置文件被管理员删除。


节点的ID用来在整个cluster中识别节点。因此有可能一个指定的节点在不变更其节点ID的情况下,变更其IP/端口。cluster也可以感知一个节点的IP/端口的变化,并通过cluster bus上运行的gossip协议重新配置和广播这些信息。


每个节点都有一些其它节点应该了解的关联信息:


  • 节点的IP地址和TCP端口。
  • 一个标志集。
  • 节点的hash槽的集合。
  • 最近一次使用cluster bus发送ping包的时间。
  • 最近一次我们收到pong包的回复时间。
  • 这个节点我们标记为失效的时间。
  • 这个节点的slave的数量。
  • 如果这个节点是slave,它的master节点的ID(如果这个节点是master,这个值是0000000...)


一些信息可以使用CLUSTER NODES命令发送到一个cluster的任意的节点来获取群集的信息,无论是master节点还是slave节点。


下面是一个3节点的小cluster的发送CLUSTER NODES到一个master节点的输出的例子。


$ redis-cli cluster nodes

d1861060fe6a534d42d8a19aeb36600e18785e04 :0 myself - 0 1318428930 connected 0-1364

3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 connected 1365-2729

d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 connected 2730-4095


在上面的列表中的不同的信息域有:节点ID、地址:端口、标志位、最近ping发送时间、最近pong响应时间、连接状态、槽数量。


 


Cluster拓扑结构


Redis cluster是一个完整的网格,每个节点都与每一个其它的节点通过TCP连接。


在一个N节点的cluster中,每个节点都有N-1个对外的TCP连接和N-1个向内的连接。


这些TCP连接一直保持活跃,不需要人工干预。


 


节点握手


节点总是从cluster bus的端口接收连接,甚至响应所有收到的ping,即便这些ping的节点可能不是信任的节点。然而,如果这个节点不是这个cluster的一部分,那么它发送所有包都会被丢弃。


有两种方式,可以让一个节点会接受其它的节点成为这个cluster的一部分:


  • 如果一个节点使用MEET消息来展示自己。一个meet消息和PING消息极为相似,但是会迫使接收者接收这个节点成为cluster的一部分。系统管理员执行下面的命令,会让节点发送MEET消息到其它节点:


CLUSTER MEET ip port


  • 如果一个节点已经通过gossip信任其它的节点,也会注册其它的节点成为cluster的一部分。比如,A知道B,B知道C,最终B会发送C的gossip消息到A。如果A收到,A会注册C成为网格的一个部分,会尝试和C连接。


这表示,只要我们加入图中的任何一个节点,它们会最终自动的全部联通。这意味着cluster基本上可以自动的发现其它节点,但只受限于管理员强制形成的信任关系。


这种机制使cluster更健壮,但要预防不同的Redis的cluster会意外的由于IP地址或其它的网络相关的事件导致混合。


 


MOVED重定向


一个Redis的客户端可以向cluster中的任意节点发送请求,包括slave节点。节点会分析请求,如果请求是能处理的(这表示,请求中只有一个key涉及)它会看key属于那个节点的hash槽。如果hash槽是当前节点负责,这个请求会被直接处理,否则,这个节点会检测它的内部的hash槽到节点的映射关系,并恢复给客户端一个MOVED错误。


一个MOVED错误看起来像下面这样:


GET x


-MOVED 3999 127.0.0.1:6381


这个错误包含key的hash槽(3999)和对应这个hash槽的实例的ip:port。客户端需要根据指定的IP地址和端口重新的发送请求。注意,即使客户端在重新发送请求之前等待了很久,同时cluster变更了配置,如果现在3999这个hash槽在另外一个节点上,那么目标节点会再次回复一个MOVED错误。


因此,从这个角度来看cluster的节点是使用ID来标识,并试图简化面向客户端的接口,仅仅发布一个hash槽到Redis节点对应的ip:port对的映射。


客户端不被要求,但应该记住3999hash槽是由127.0.0.1:6381处理。这样,一旦一个新命令需要被执行,它可以计算目标key的hash槽,并选择正确的节点。


主要,如果cluster是稳定的,最终所有客户端会获取一个hash槽 -> 节点的映射,这样客户端可以之间访问正确的节点不需要重定向或代理或其它的单点容错的实体。


一个客户端还应该能处理后面描述的-ASK重定向。


 


Cluster在线重配置


Redis cluster支持在cluster运行的期间增加或减少节点。实际上,增加或减少一个节点被抽象成相同的操作,就是,把hash槽从一个节点移动到另一个节点。


  • 增加一个新节点到cluster,cluster增加了一个空节点,会从已有的节点中移动一些hash槽到新节点。
  • 从cluster删除一个节点,会把这个节点的hash槽转移到其它存在的节点上。


所以,实现的核心是围绕移动hash槽的能力。实际上,从事实的角度看一个hash槽就是一个key的集合,因此Redis cluster 的resharding过程实际上做的事情就是把key从一个实例转移到另外一个实例。


如果要理解这是怎么工作的,需要展示在一个Redis cluster节点中用于操控hash槽传输表的CLUSTER子命令。


有下面这些子命令:


  • CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
  • CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
  • CLUSTER SETSLOT slot NODE node
  • CLUSTER SETSLOT slot MIGRATING node
  • CLUSTER SETSLOT slot IMPORTING node


前面两个命令,ADDSLOTS和DELSLOTS,只是简单的将槽分给一个redis节点(或从节点中去掉)。在hash槽被分配之后,他们会通过gossip协议传播到整个cluster。ADDSLOTS命令通常用于一个新的cluster建立之后,快速的从头开始分配hash槽。


SETSLOT子命令用于将一个hash槽分给一个指定的正在使用的节点。此外,这个hash移动的hash槽会被设置成2个特殊的状态MIGRATING和IMPORTING:


  • 当一个槽被设置成MIGRATING,这个节点将在请求的key存在的情况下,接受所有这个hash槽的请求;如果key不存在,请求会会使用-ASK重定向代迁移的目标节点。
  • 当一个槽被设置为IMPORTING,这个节点会接受所有的这个hash槽的请求,但只会处理前面加有ASKING的请求。如果客户端没有在请求前加ASKING命令,请求会通过-MOVED被重定向到真实的拥有者。


开始,这也许感到奇怪,但我们会让这些更清晰。假设我们有两个Redis节点,A和B。我们希望将hash槽8从A移动到B,我们执行了下面的命令:


  • 我们向B发送:CLUSTER SETSLOT 8 IMPORTING A
  • 我们向A发送:CLUSTER SETSLOT 8 MIGRATING B


所有的其它的节点会在接收到hash槽8的请求的时候继续告知客户端这个hash槽在节点A上,因此发生的故事是:


  • 所有的存在key的请求都是由A再处理。
  • 所有的不在A上的key都由B来处理。


这种方式下,我们不会在A上继续创建新key。同时,一个叫redis-trib特殊的客户端,是一个Redis cluster的配置工具,会确保将A上存在的key移动到B上。这是使用下面的命令来完成的:


CLUSTER GETKEYSINSLOT slot count


上面的命令会返回指定的hash槽中key的数量。对于每一个key,redis-trib向节点发送一个MIGRATE命令,这会将指定的key从A自动的迁移到B(两个实例都会在加锁,确保不会出现同步问题)。这是MIGRATE如何工作的:


MIGRATE target_host target_port key target_database id timeout


MIGRATE会连接目标节点进程,并发送这个key的序列化的版本,当收到OK之后会从存储中删除旧的key。从这点来看,在任意的时间点,一个key要么在A,要么在B。


在Redis cluster中没有必要指定0以外的database,但是MIGRATE命令能在与Redis cluster无关的作业中使用,是一个通用的命令。


移动像list那样的复杂的key,MIGRATE被优化成尽可能的快,但是在有应用程序正在使用、有延迟的约束的时候,重新配置存在大量key的redis cluster,这是不明智的。


 


ASK重定向


在前面的小节我们简要的讨论了ASK重定向,为什么我们不是简单的使用MOVED重定向?因为MOVED表示这个hash槽是永久性的被另一个节点所拥有,下一个请求应该直接到那个节点,ASK表示只有ask的下个请求到指定的节点。


需要这个是因为下个关于hash槽8的请求,可能这个key还在A,所以我们总是希望客户端如果必要先试A然后B。由于这只会出现在16384个hash槽中的一个,因此cluster的hit性能的负担是能接受的。


然而,我需要强制客户端的行为,确保客户端只是在试过A之后才尝试B,如果客户端在请求的前面发送ASKING,那么B只会接收针对标记为IMPORTING的槽的请求。


基本上ASKING命令设置了一个一次性的标志,强制一个节点服务于一个IMPORTING的槽。


因此,下面是从客户端的角度来看ASK重定向的全部语义:


  • 如果收到ASK重定向,仅这个请求被指向指定的节点。
  • 使用ASKING命令开始这个请求。
  • 不更新客户端的hash槽8的映射表。


一旦hash槽8的迁移结束,A会发送一个MOVED消息,客户端会永久性的将hash槽8指向新的ip:port对。注意,如果一个错误的客户端使用之前的节点映射,这也不会有问题,因为他不会发送对着B在请求前发送ASKING命令,而B会使用MOVED重定向错误将客户端重定向到A。


译注:


如果状态改变在A返回-ASK之后,那么B状态已经是OK,ASKING命令引导的查询命令不会找到hash槽,根据映射发送一个MOVED重定向,目标是B。


如果状态改变之后,访问A会发现hash槽已经指向B,A会返回一个MOVED重定向,目标是B。


多key操作


使用hash tag的客户端可以自由的使用多key操作。比如,下面的命令是有效的:


MSET {user:1000}.name Angela {user:1000}.surname White


然而多key操作会因为hash槽的resharding变得不可用,因为一个节点的hash槽会被移动到其它节点上(因为一个手工的resharding)。


更进一步,甚至在resharding之中,多key操作的目标key要么全在同一个节点上(要么是源,要么是目标节点)还是可以使用的。


对于key不存在,或在resharding期间,在源和目标节点中分裂,会产生一个-TRYAGAIN错误,客户端会在一些时间后重试,或返回这个错误。


 


容错性


节点的心跳和gossip消息


cluster中的节点交换ping / pong包。


一般,一个节点每秒会随机的ping几个节点,因此无论cluster有多少个节点,整体的发送ping包(和接收pong包)的是一个常量。


然而,每个节点需要确保向没有发过ping或接受过pong的节点在NODE_TIMEOUT的一半时间内ping其它所有的节点。在NODE_TIMEOUT时间过去之前,节点还会尝试再连接节点,确保节点不是连接不上,只是当前TCP连接的问题。


如果NODE_TIMEOUT设置是一个小值,而节点是数量(N)非常大,交互的消息总量大于O(N),由于每个节点会ping每个其它的在NODE_TIMEOUT一半的时间内没有更新信息的节点。


例如,一个100个节点的cluster有一个60秒的节点超时,每个节点会在30秒内发送99个ping,大概每秒3.3个ping,整个cluster有100个节点,每秒会有330个ping。


Redis Cluster使用gossip交互信息的方式来显著的减少交换信息的总量。例如,我们我们也许在NODE_TIMEOUT一半的时间内ping那些已经报告为处于“可能失效”状态(参考后面)的节点,只使用最高效的方式在每秒的时间内ping有限的几个其它的可以工作的节点。然而,在真实的世界中,用一个非常小的NODE_TIMEOUT可靠的验证一个大的cluster会在将来作为一个真实的配置来部署和测试。


Ping和Pong包内容


Ping和Pong包包含一个对所有的包都通用的头(例如,请求投票的包),和一个对于Ping和Pong包特有的Gossip区。


通用的头有下面的信息:


  • 节点ID,是160 bit伪随机字符串,在节点初次创建的时候分配,并在作为一个Redis Cluster中的同一个节点在其生命周期中一直保持不变。
  • currentEpoch和configEpoch域,是用于为了使用Redis Cluster的分布式算法(下个小节会详细描述)。如果一个节点是slave,那么configEpoch是它了解的最新的master的configEpoch。
  • 节点的标志位,指出节点是slave、master、和其它的single-bit的节点信息。
  • 一个节点对应的hash槽的bitmap,如果节点是slave,是它的master对应的hash槽的bitmap。
  • 端口:发送者的TCP基础端口(就是Redis用于接收客户端命令的端口,加10000就是cluster port )。
  • 状态:从发送者的角度看到的cluster的状态(下线还是OK)。
  • 如果这时一个slave,它的master节点的ID。


Ping和pong包包含一个gossip区。这个区域一般给接收者一个发送者的节点认为cluster中的其它节点状态的视角。gossip区域一般只包含有限的从发送者节点了解的节点中,随机选择的几个节点。


对于gossip区域中的每个节点,会提供下面几个信息域:


  • 节点ID。
  • 节点的IP和端口。
  • 节点的标志。


gossip区域允许通过发送者的视角来了解其它节点的状态信息。这有助于探测cluster中的失效节点,和发现其它节点。


失效探测


Redis Cluster的失效探测,用来检测一个master或slave节点已经无法被主体节点联系上,并作为这个事件的结果,要么提升一个slave成为master的角色,如果这个做不到,将cluster设置为一个错误的状态,并停止从客户端接收请求。


每个节点都会携带与其关联的节点的列表。有两个和失效检测相关的标识,是PFAIL和FAIL。PFAIL表示可能失效,是非正式的错误状态。FAIL表示这个节点正在失效,并且这个已经经过一个固定的时间内被主体的master节点确认。


PFAIL标识:


如果一个节点在超过NODE_TIMEOUT时长还联系不上另外一个节点,会给联系不上的节点打上PFAIL标志。master和slave节点都能给其它节点打PFAIL标志,无论它是什么类型。


一个Redis Cluster的节点的不可达的概念是我们有一个活跃的ping(我们发送一个ping,并且获得了回复)会延迟NODE_TIMEOUT时长,因此如果希望这个机制能工作,NODE_TIMEOUT必须长于网络的往返时间。为了在正常的操作过程中增加可靠性,节点会在过了NODE_TIMEOUT一半的时长后再次连接没回复的ping。这个机制确保偶然中断的活跃连接不会导致节点之间的失效报告。


FAIL标志:


单独的PFAIL标志只是每个节点在本地对其它节点的信息,仅仅是这些还不能用于触发一个slave的提升。因此一个被认为是PFAIL的节点还需要被提示到FAIL。


就像在节点的心跳的章节概述的那样,每个节点发送gossip消息到其它节点,会包含一些它自己知道的节点。所以,最终每个节点会从其它的节点收到一个节点信息的集合。按这种方式,每个节点都有通知其它节点关于失效检测的机制。


这个机制用于逐步升级一个PFAIL条件成为一个FAIL条件。当下面这些条件都具备:


  • 一些节点,我们称为A,有一个B节点被标记为PFAIL。
  • 节点A通过gossip区收集关于节点B的信息,了解cluster中主体的master对B的状态描述。
  • master的主体在NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT时间内发送PFAIL或PFAIL条件。


如果上面的条件都是真,节点A会:


  • 标记这个节点为FAIL。
  • 发送FAIL消息到所有能到达的节点。


FAIL消息会强制每个接收到消息的节点标记这个节点为FAIL状态。


注意,FAIL标志基本上是单向的,这表示一个节点可以从PFAIL变成FAIL,但是FAIL标志只有两种可能会被清除:


  • 节点能联系上,并且是一个slave。这种情况下FAIL标志由于slave不需要故障恢复而被清除。
  • 节点能联系上,并且是一个没有任何hash槽的master。这种情况下FAIL标志会因为master没有hash槽而被清除。没有hash槽的master不被看作是cluster的一个部分,它是正在等待被配置加入cluster的master。
  • 这个节点已经能联系上,是一个master,但是在很长的时间(NODE_TIMEOUT的N倍)过去后,没有任何slave被提升。


在PFAIL -> FAIL的转换中使用了一种形式的协议,协议的使用弱方式:


1)节点收集一段时间内的节点信息,即使master节点的主体需要“同意”,实际上这只是我们在不同的时间点收集的状态,并且我们不能担保这个状态是稳定的。


2)当每个节点检测到FAIL条件,将会通过FAIL消息迫使其它的节点接受这个条件,这里没有这个消息能到达所有节点的保证。比如,一个节点也许检测到一个FAIL条件,但是由于因为网络分区不能传达到任何其它的节点。


然而Redis Cluster的失效检测有存活的需求:最终所有的节点应该认同一个节点的状态,即便有节点被分区,一旦分区融合。有两种情况可以导致脑裂,要么节点的小部分认为这个节点是处于FAIL状态,要么是节点的小部分认为节点不是FAIL状态。在上面两种情况最终cluster会有一个对这个节点的单一的状态:


Case 1:如果一个真实的master的主体标记一个节点为FAIL,作为连锁反应每个节点最终都会标记这个master为FAIL。


Case 2:如果只有一个小部分的master标记一个节点为FAIL,那么slave的提升将不会发生(因为使用了一个更形式化的算法来确保每个节点都最终会知道这个提升),根据上面的FAIL状态清除的规则,每个节点都会清除这个FAIL状态(如果过了NODE_TIMEOUT时长的倍之后没有提升slave)。


基本上,FAIL标志仅用于触发来运行slave提升的算法的安全部分。在理论上一个slave也许是独立的和在其master不可达之后开始一个提升,并等master群体在这个master在主体能连接到的情况下拒绝提供许可。然而,PFAIL -> FAIL状态的迁移,弱许可和FAIL消息迫使状态在最短的时间散布到cluster所有可达到的节点,等等,增加的复杂性,尤其实践上的优势。因为这个机制,通常一个cluster会在错误的情况下停止接受写入,这是使用Redis Cluster的那些应用渴望的特性。并且,不需要尝试选举,slave节点如果因为本地的原因连接不了master(指master可以其它master的主体能连接上)的问题,被避免了。


Cluster的代


Redis Cluster使用了一个和Raft算法的"term"类似的概念。在In Redis Cluster这个术语是代(epoch),被用于提供一个递增的版本给事件,这样当多个节点多提供相互冲突的信息,可以让节点明白那个的状态信息是更新的。


currentEpoch是一个64 bit的无符号整数。


在Redis Cluster中每个节点创建时,包括master和slave节点,currentEpoch被设置为0。


每次ping或接收到其它节点的pong,如果发送者的代(是cluster bus的消息头)比本地节点的代大,currentEpoch会被更新为发送者的代。


因为这个语义,最终一个cluster的所有节点都会认同最大的代。


当有状态发生变化,并且一个节点在寻求许可来执行什么操作,这个信息就有用处了。


当前,这只是在slave提升的时候出现,像下个小节描述的那样。基本上,代是cluster的逻辑时钟,决定一个信息会覆盖更小代的相同信息。


 


Config的代


每个master总是会在ping和pong包中的它负责的hash槽的bitmap中告知它的configEpoch。


configEpoch在节点刚创建的时候被设置为0。


在slave的选举时,一个新的configEpoch被创建。 slave会尝试替代失效的master,并增加他们的代,并获得master主体的授权。当一个slave被授权,一个新的唯一的configEpoch产生了,slave会使用新的configEpoch成为master。


像下一个小节解释的,configEpoch帮助解决不同节点的提交的不同的配置(一个因为网络分区或节点失效导致的情况)。


slave节点也会在ping或pong包中通告configEpoch信息,但是slave的域中对应的是它master的最近一次通过包交换得到的configEpoch。这允许其它实例可以检测一个slave是否拿着一个需要更新的旧配置(master节点将不会允许拿着旧配置的slave投票)。


每次来自其它已知的节点的configEpoch变更,都会被永久的记录到nodes.conf文件中。


当前,当一个节点被重新启动,它的currentEpoch都会被设置的比已知的节点的configEpoch要大。这对于崩溃-恢复的系统模型来说不安全,系统将会被修改在持久化的配置文件中也保存currentEpoch。


Slave的选举和提升


slave的选举和提升由slave节点来处理,并由master节点来帮助投票提升slave。当一个master,从至少它的一个预先配置好的将会成为master的slave的角度来看,处于一个FAIL状态,slave的选举会发生。


为了一个slave将自己提升成为master,它要求开始一个选举,并赢得它。如果一个master处于FAIL状态,这个master的所有的slave都可以开始一个选举,然而,只有一个slave可以赢得选举,并将自身提升成为一个master。


当下面的条件都具备,一个slave开始选举:


  • slave的master处于FAIL状态。
  • master包含非0个hash槽。
  • slave到master的复制连接被断开的时间没有超过一个给出的总时间,是为了确保提升的slave有一个合理的数据新鲜度。


为了被选举,第一步就是增加它自身的currentEpoch计数器,并请求master实例投票。


投票是slave通过广播一个FAILOVER_AUTH_REQUEST的包到cluster的每个master节点。当它会等待回复最多等待NODE_TIMEOUT的两倍时长,但至少会等待2秒钟。


一旦一个master给一个指定的slave投票,会回复给FAILOVER_AUTH_ACK一个正数,它不能在NODE_TIMEOUT * 2时长内再给相同的master节点投票。在这个期间,它不能对相同的master的其它授权申请提供回复。这不必保证是安全的,但是对多个slave同时选举的场景(甚至有不同的configEpoch)很有效。


在投票请求发送时,一个slave会丢弃所有的代小于currentEpoch的AUTH_ACK回复,这样确保统计结果不会包含以前的选举的投票。


一旦slave从master的主体收到ACKs,它就赢得了选举。否则,如果主体在的NODE_TIMEOUT两倍时长内没到达,选举会被终止,将会在NODE_TIMEOUT * 4的时长后再次发起。


一个slave不能在master一进入FAIL就发起选举,有一个小的延时,是这么计算的:


DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +


        SLAVE_RANK * 1000 milliseconds.


这个固有的延时用来确保FAIL状态在cluster中散布,否则slave会因为master还不知道FAIL状态,拒绝同意投票。


这个随机的延时用于在slave开始选举阶段的去同步化,使选举在不同的时刻开始。


SLAVE_RANK是根据slave的复制流的总量,建立的它到master的排列的序号。slave在一个master失效的时候交换消息,以建立一个排列:包含最多的update的复制的是rank 0,第二多的是rank 1,等等。按这种方式,包含最多更新的slave会更早的开始选举。


一旦一个slave赢得选举,它开始通过作为master的ping和pong的包来通告自己,提供一个自己服务的hash槽的集合,携带选举开始时的configEpoch作为currentEpoch。


为了加速其它节点的重新配置,一个pong包被广播到这个cluster的所有节点(然而,当前到达不了的节点最终会收到一个ping或pong包,将会被重新配置的)。


其它的节点会探测是否存在新master服务于相同的hash槽,并携带更大的configEpoch值,将会根据它升级配置。老master的slave,和重新加入cluster的故障恢复的master节点,将不仅会升级配置,还会配置自身从新master复制数据。


 


master响应slave的投票请求


在前面的小节讨论了slave如何尝试发起选举,这个小节解释从master的角度来看,收到一个slave发出的投票请求之后发生了什么。


master从slave节点接收FAILOVER_AUTH_REQUEST形式的投票请求。


一个投票在下面的条件都具备的时候被许可:


  • 1) 一个master在一个时间点只给一个指定的代(epoch)投票,并拒绝给更老的代投票:每个master都有一个lastVoteEpoch信息域,并拒绝投票给auth请求包中的currentEpoch不大于的lastVoteEpoch请求。当一个master回复正数给一个投票请求,lastVoteEpoch就会跟着更新。
  • 2) 一个master只有在slave的master的标志是FAIL的时候才会投票给slave。
  • 3) 携带currentEpoch的auth请求,如果currentEpoch小于master的currentEpoch,请求会被忽略。因此,master总是回复具有相同currentEpoch的auth请求。如果相同的slave再次请求投票,会增加currentEpoch,这保证老的来自master的延迟的回复不会被新的投票接收。


没有使用这个规则导致的问题的例子:


master的currentEpoch是5,lastVoteEpoch是1(这在几次失败的选举后会出现)


ü Slave的currentEpoch是3。


ü Slave尝试发起选举,使用的是代4 (3+1),master回复ok,并返回currentEpoch  的值5,然而这个回复被延迟了。


ü Slave再次尝试发起选举,使用的代是5 (4+1),这时延迟的回复到达slave,并带有currentEpoch 5,这被认为是有效的回复接受了。


·        4) master在经过的NODE_TIMEOUT * 2时间内不会给相同的master的slave投票,因为这个master的投票已经给一个slave了。这不是严格的要求,因为不可能有两个slave在相同的代中赢得选举,但是在实践中它确保一个slave被选举后,它有足够的时间通知其它slave避免其它的slave尝试赢取一个新的选举。


·        5) master将不会以任何方式来选择最佳的slave,仅简单的判断slave的master是否是FAIL状态,并据此决定本期不投票,还是给一个正值的投票来发出许可。然而,最佳的slave通常是早于其它的slave开始并赢取选举。


·        6) 如果一个master拒绝投票给slave,不会有负值的回复,请求是简单的被忽略。


·        7) master不会投票许可,如果一个slave发送的configEpoch小于master表中对应这个hash槽的任何slave的configEpoch的值。记住slave发送的它的master的configEpoch,并且hash槽的bitmap由它的master负责。这表示一个请求投票的slave最起码需要有一个配置,对应它希望做故障恢复的hash槽,要比许可投票的那个master上的配置相同或更新。


slave选举时的同步


这个小节阐述代的概念是如何使一个slave完成耐分区的提升。


  • 一个master永久的不可达。这个master有3个slave A, B, C。
  • Slave A 赢得了选举,并被提升为master。
  • 一个分区导致A不能被cluster的主体看到。
  • Slave B赢得选举,并被提升为master。
  • 一个分区导致B又不能被cluster的主体看到。
  • 以前的分区消失了,A又能够使用了。


在这个点上,B已经下线,A又能够使用,将会和C竞争尝试对B的故障恢复发起选举。


这两个最终都会声称对相同的hash槽完成slave的提升,然而它们发布的configEpoch将不会是一样的,C的代会更大,因此其它的节点会升级他们的配置到C。


A本身会检测到C的ping,会发现C服务的hash槽和自己的相同,并有更大的代,将会配置成为C的一个slave。


hash槽的信息的广播的规则


Redis Cluster的一个重要部分就是散布关于cluster节点对应那些hash槽的信息的机制。这对一个新cluster的启动过,或对一个失效的master升级一个slave之后的配置的更新,都至关重要。


不断交换的ping和pong包都包含一个头,被发送者用于通告它对应的hash槽的信息。这是散布变化的主要的机制,存在cluster的管理员手工重新配置的例外情况(比如,通过redis-trib做的resharding在master之间调整hash槽)。


当一个新的Redis Cluster的节点被创建,它的本地的hash槽的表格,是通过一个给出的节点ID来映射的,被初始化,它的任何一个hash槽都被设置为空,这表示hash槽是空的。


一个节点更新它的hash槽的表格遵守的第一个规则是:


规则1:如果一个hash槽没有使用,并且一个已知的节点声明了它,我将会修改hash槽的表格来将hash槽和一个节点关联。


由于这个规则,当一个新的cluster被创建,只需要手工的指定(使用CLUSTER命令,一般通过redis-trib命令行工具)每个master节点负责的hash槽,这个信息会快速的在cluster中散布。


然而这个规则对因为master失效出现的一个slave提升为master导致的配置升级来说还是不够。新master的实例会通告这些以前被以前的老的master服务的hash槽,但这些hash槽从其它的节点来看没有被回收,根据第一个规则配置将不会升级。


因为这个原因,有了第二个规则,用于将一个已经指派给之前的一个节点的hash槽重新绑定到一个新的声明拥有它的新节点。最规则如下:


规则2:如果一个hash槽已经被指派,并且一个已知的节点使用一个比当前hash槽的拥有者的更大的configEpoch通告它,我会重新绑定hash槽到新的节点。


因为第二个规则,cluster中所有的节点会认可那些通过中带有最大的configEpoch的节点作为一个hash槽的拥有者。


 


UPDATE消息


散布hash槽配置的分布式的系统只使用通常的的节点之间交换的ping和pong消息。


它还要求有一个对一个指定的hash槽的无论是slave或master的节点有更新后的配置,因为节点通过ping和pong包的头发送他们自身的配置。


然而,有些时候一个节点,是服务于给定的hash槽的唯一的节点,在一次网络分区之后重新被发现。


例如:一个指定的hash槽由A和B节点来服务,在一些点上发生故障,因此B被提升为新的master。后来,B也失效了,cluster没有办法来恢复,因为没有更多的hash槽的复制了。


然而,A也许一段时间以后恢复了,并带着认为自己是可以写入的master的老的配置重新加入cluster。这里没有复制品可以更新它的配置。这是UPDATE消息的全局性:当一个节点检测到另外一个正在通告自身的节点正在使用一个旧的配置,它会向这个节点发送一个UPDATE消息,带有服务这些hash槽的新的节点的ID和hash槽的集合(bitmap)。


注意:当通过ping / pong和UPDATE更新配置共享相同大代码路径,这两种更新配置的方式在功能上存在重叠。然而,这两种机制都很有用,因为ping / pong是能散播一个新节点的hash槽的路由信息,而UPDATE消息仅在检测到一个旧的配置的时候才发送,仅恢复和修正错误的配置。


副本迁移


Redis Cluster实现了一个概念叫副本迁移,是为了提高系统的可用性。想法是一个cluser包含一个master-slave的配置,如果master和slave的关系固定,对于独立的多个节点的失效来说,只是拥有有限的可用性。


例如在一个cluster中,每个master有一个单独的slave,这个cluster可以在master或slave其中一个失效的情况下一直工作,但是如果都失效的将不能工作。然而,有一类失效,是由硬件或软件导致的独立节点的失效,会随时时间的增加而累积,例如:


  • Master A 有一个slave A1。
  • Master A 失效,A1被提升为新的master。
  • 三小时以后A1以独立的方式失效(和A的失效没有关系)。因为A也DOWN掉了,所以没有其它的slave可以提升,cluster就不能继续做正常的操作。


如果master和slave的关系是固定的,为了让cluster可以对抗上面的场景,唯一的方式就是为每个master增加slave,然而要求运行更多的redis实例会增加额外的支出,更多的钱,等等。


一个可选方式是在cluster中创建一个不对称,并让cluster在运行期间自动的调整。例如,cluster也许有A, B, C.三个master。A和B有一个单独的slave A1和B1。然而C是不同的,有两个slave:C1和C2。


副本复制是指在一个master没有可恢复性(没有能工作的slave)时自动的重新配置一个slave复制这个master。有了副本复制,上面描述的场景会变成下面这样:


  • Master A 失效,A1被提升。
  • C2 作为A1的slave复制,此外没有任何的slave支持。
  • 3小时以后A1也失效。
  • C2 被提升作为替代A1的新的master。
  • 这个cluster可以继续操作。


副本迁移算法


副本迁移算法不使用任何形式的协议,因为在Redis Cluster中的slave布局不是cluster配置的一个部分,cluster配置要求一致性,并使用配置代作为配置的版本。它使用算法来避免一个没有后备的master大规模的迁移slave。


这时算法如何工作的。开始我们需要在这里定义什么是一个好的slave:


一个好的slave是从一个指定的节点来看不处于FAIL状态的slave。


触发算法执行是每个slave探测一个master是否拥有至少一个好的slave。然而在所有检测这个条件的slave中,只有一个子集应该行动。这个子集实际上通常是一个单独的slave,除非不同的slave在一个指定的时刻有一个略微不同的其它失效节点的版本。


这个行动slave是在master拥有的最大数量的连接上的slave中,不是FAIL状态的,并且有最小的节点ID的slave。


因此,例如有10个master,每个都有一个slave,2个master每个有5个slave,slave尝试迁移,从2个master的5个slave中,有最小的节点ID的那个。给予没有使用任何协议,在cluster的配置不稳定的时候也能工作,在多个slave带着最小的节点ID(但是很难触发竞争条件)作为非失效的slave的竞争条件出现的时候,如果这个发生,结果就是多个slave迁移到相同的master,这也是无害的。如果这个竞争条件导致让出master没有slave,当cluster文档之后,算法会再次执行,会迁移slave回到最初的master。


最终每个master都会至少有一个slave作为后备,然而通常的行为是一个单独的slave从一个有很多个slave的master到一个孤立的master的迁移。


这个算法由一个用户配置参数控制,是cluster-migration-barrier,这是预留让一个slave迁移的master的好的slave的数量。因此,举个例子,如果设置这个参数为2,slave将会迁移保障一个master保留2个可以工作的slave。


configEpoch冲突解决算法


当新的configEpoch值在slave故障回复期间的提升时创建,他们被担保是唯一的。


然而在手工resharding时,当一个hash槽从A节点迁移到B节点,resharding程序会强制B升级它的配置到一个新的能在cluster中找到的最大的代加1(除非这个节点已经是有最大的配置代),不会请求其它节点的同意。这在一个新的hash槽的配置取代旧配置的时候需要。


这个过程在系统管理员做手工resharding的时候会发生,当然也可能resharding之后hash槽被关闭,但是在新configEpoch被散布到cluster之前,失效发生了。一个slave会开始一个故障回复,并获得授权。


这个场景会导致两个节点有相同的configEpoch。还有其它场景两个节点有相同的configEpoch:


  • 新cluster创建:所有的节点开始于相同的configEpoch 0。
  • 软件的BUG。
  • 手工改变配置文件,文件系统损坏。


当服务不同的hash槽的master有相同的configEpoch是没问题的,我们更感兴趣于确保slave在一个master的故障恢复时有一个不同的和唯一的配置代。


然而手工干预或多次的reshardings也许会用不同的方式改变cluster的配置。Redis Cluster的主要的活性在于hash槽的配置总是收敛,这样我会希望在任何情况下所有的master节点都有一个不同的configEpoch。


为了强化这点,一个解决冲突的算法用于处理两个节点最终有相同的configEpoch。


  • 如果一个master节点检测到另外一个master节点的自身通告,有着一样的configEpoch。
  • 并且,如果这个节点与另外一个声称有相同configEpoch的节点相比,有更小的节点ID。
  • 然后它将自己的currentEpoch增加1,然后使用它作为最新的configEpoch。


如果有任何节点的集合有相同的configEpoch,除了有最大节点ID外的所有节点都会往前移,担保每个节点无论发生什么都会选择一个唯一的configEpoch。这个机制也保证在一个新cluster创建以后每个节点都开始于一个不同的configEpoch。


 


Publish/Subscribe


在一个Redis Cluster的客户端可以订阅任何节点,也可以发布到任何节点。cluster会保证发布的消息的转播。


当前的实现会简单的广播所有的发布的消息到所有的节点,但同时会使用bloom过滤器或其它算法来做优化。


Appendix A: CRC16 reference implementationin ANSI C

/*

 * Copyright2001-2010 Georges Menie (www.menie.org)

 * Copyright 2010Salvatore Sanfilippo (adapted to Redis coding style)

 * All rightsreserved.

 * Redistributionand use in source and binary forms, with or without

 * modification,are permitted provided that the following conditions are met:

 *

 *    * Redistributions of source code mustretain the above copyright

 *      notice, this list of conditions and thefollowing disclaimer.

 *    * Redistributions in binary form mustreproduce the above copyright

 *      notice, this list of conditions and thefollowing disclaimer in the

 *      documentation and/or other materialsprovided with the distribution.

 *    * Neither the name of the University ofCalifornia, Berkeley nor the

 *      names of its contributors may be used toendorse or promote products

 *      derived from this software withoutspecific prior written permission.

 *

 * THIS SOFTWARE ISPROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY

 * EXPRESS ORIMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED

 * WARRANTIES OFMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

 * DISCLAIMED. INNO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY

 * DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

 * (INCLUDING, BUTNOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

 * LOSS OF USE,DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND

 * ON ANY THEORY OFLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

 * (INCLUDINGNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

 * SOFTWARE, EVENIF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 */

 

/* CRC16 implementation according to CCITT standards.

 *

 * Note by@antirez: this is actually the XMODEM CRC 16 algorithm, using the

 * followingparameters:

 *

 * Name                      : "XMODEM",also known as "ZMODEM", "CRC-16/ACORN"

 * Width                     : 16 bit

 * Poly                      : 1021 (That is actuallyx^16 + x^12 + x^5 + 1)

 *Initialization            : 0000

 * Reflect Inputbyte        : False

 * Reflect OutputCRC        : False

 * Xor constant tooutput CRC : 0000

 * Output for"123456789"    : 31C3

 */

 

static const uint16_t crc16tab[256]= {

   0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,

   0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,

   0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,

   0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,

   0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,

   0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,

   0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,

   0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,

   0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,

   0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,

   0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,

   0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,

   0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,

   0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,

   0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,

   0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,

   0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,

   0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,

   0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,

   0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,

   0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,

   0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,

   0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,

   0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,

   0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,

   0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,

   0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,

   0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,

   0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,

   0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,

   0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,

   0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0

};

 

uint16_t crc16(const char *buf, int len) {

   int counter;

   uint16_t crc =0;

   for (counter =0; counter < len; counter++)

           crc =(crc<<8) ^ crc16tab[((crc>>8) ^ *buf++)&0x00FF];

   return crc;

}