天天看点

Redis Cluster 主要特点和设计原理

作者:后端开发进阶

Redis cluster目标

Redis Cluster 是分布式Redis, 按重要性排序, 需要实现以下几个目标:

  • 高性能、可线性扩展至1000个节点。不使用代理, 没有异步复制, 不需要任何值的合并操作
  • 比较高的写安全度: 系统尝试(以最大努力的方式)保留来自与大多数主节点连接的客户端的所有写入。通常有一些小窗口,确认的写入可能会丢失,只有小部分分区会丢失已确认写入的窗口。
  • 可用性: Redis集群能够在大多数主节点可访问的分区中生存,并且对于每个不再可访问的主节点,至少有一个可访问的从节点。此外,使用副本迁移时, 当某一个主没有任何从复制时, 可以从另外一个有多个从的主那里获取到新的从

Redis Cluster 是3.0版本以后支持的。

Redis Cluster是单机功能的子集

Cluster实现了所有单机Redis的命令。对于一些复杂的多key批量操作比如集合的并集和交集也是支持的, 只是需要这些keys能hash到一个槽上(slot)。

Redis Cluster实现了一个称为哈希标签的概念,可以使用它来强制将某些keys存储在同一哈希槽中。但是,在手动重分片期间,多键操作可能在一段时间内不可用,而单键操作始终可用。

Redis Cluster 不支持单机redis的那种多database的功能。集群模式只有database 0 因此 切换database的SELECT 命令是不支持的。

Redis Cluster协议中客户端和服务端角色

节点负责保存数据,并获取集群的状态,包括将keys映射到正确的节点。集群节点还能够自动发现其他节点,检测非工作节点,并在需要时将从属节点提升为主节点,以便在发生故障时继续运行。

为了执行任务,所有集群节点都使用TCP总线和称为Redis集群总线的二进制协议连接。每个节点都使用集群总线连接到集群中的每个其他节点。节点使用gossip协议来传播关于集群的信息,以便发现新节点,发送ping包以确保所有其他节点都正常工作,并发送集群消息以通知特定条件。集群总线还用于在集群中传播发布/订阅消息,并在用户请求时协调手动故障切换(手动故障切换不是由Redis集群故障检测器发起,而是由系统管理员直接发起)。

由于群集节点无法代理请求,因此可能会使用重定向错误-MOVED和-ASK将客户端重定向到其他节点。理论上,客户机可以自由地向集群中的所有节点发送请求,如果需要,可以重定向,因此客户机不需要保持集群的状态。然而,能够缓存键和节点之间映射的客户端可以以合理的方式提高性能。

写安全

Redis集群在节点之间使用异步复制,最后一次故障转移赢得了隐式合并功能。这意味着最后选择的主数据集最终将替换所有其他副本。分区期间可能丢失写操作的时间窗口总是存在的。然而,在连接到大多数主机的客户端和连接到少数主机的客户端的情况下,这些窗口非常不同。

Redis集群更努力地保留由连接到大多数主机的客户端执行的写入,而不是在少数侧执行的写入。以下是导致在故障期间大多数分区中收到的确认写入丢失的场景示例:

1. 写入可能到达主节点,但虽然主节点可能能够回复客户端,但写入可能不会通过主节点和从节点之间使用的异步复制传播到从节点。如果主在写入未到达从节点的情况下死亡,则如果主无法在足够长的时间内可访问,那么其一个从节点被提升为主,则写入将永远丢失。在主节点完全、突然故障的情况下,通常很难观察到这一点,因为主节点尝试在几乎同时回复客户端(确认写入)和从节点(传播写入)。然而,这是一种真实的故障模式。

2. 另外一种理论上存在的写入失败可能性如下:

  • 主因为分区不可达
  • 这时候被一个从故障转移了
  • 一段时间后主又恢复了
  • 一个客户端写入请求会带上过期的路由信息请求到老的主, 然后新的主没来得及通知自己已经变成了主, 然后新的主会丢失部分数据

第二种故障模式不太可能发生,因为在足够长的时间内无法与大多数其他主机通信以进行故障切换的主节点将不再接受写入,并且当分区固定时,写入仍会被拒绝一小段时间,以允许其他节点通知配置更改。此故障模式还要求客户端的路由表尚未更新。

针对分区少数侧的写入具有较大的丢失窗口。例如,Redis集群在有少数主机和至少一个或多个客户端的分区上丢失了大量写入,因为如果主机在多数端发生故障,发送到主机的所有写入都可能丢失。

具体而言,对于要进行故障转移的主机,大多数主机必须至少在NODE_TIMEOUT内无法访问它,因此,如果在此时间之前分区已固定,则不会丢失任何写入。当分区持续超过NODE_TIMEOUT时,在少数端执行的所有写入可能会丢失。然而,Redis集群的少数方将在NODE_TIMEOUT时间未与多数方接触的情况下立即开始拒绝写入,因此有一个最大窗口,在此之后,少数方不再可用。因此,在此时间之后,不会接受或丢失写入。

可用性

Redis集群在分区的少数端不可用。在分区的多数侧,假设每个不可访问的主至少有大多数主和一个从,集群在NODE_TIMEOUT时间加上从节点选举和故障切换到其主所需的几秒钟后再次可用(故障切换通常在1或2秒钟内执行)。

这意味着Redis Cluster被设计为能够在集群中的几个节点发生故障时存活,但对于在发生大网络分裂时需要可用性的应用程序来说,它不是一个合适的解决方案。

在由N个主节点组成的集群的示例中,其中每个节点具有单个从节点,并且当两个节点被分开时,将以1-(1/(N*2-1))的概率保持可用(在第一个节点发生故障之后,我们总共剩下N*2-1个节点,并且没有从的单主节点发生故障的概率为1/(N*2-2))。

例如,在具有5个节点和每个节点一个从节点的集群中,存在1/(5*2-1)=11.11%的概率,即在将两个节点从多数节点分离后,集群将不再可用。

由于Redis集群的一项称为副本迁移的功能,在许多现实场景中,由于副本迁移到孤立主机(主机不再具有副本),集群可用性得到了提高。因此,在每个成功的故障事件中,集群可以重新配置从属布局,以便更好地抵御下一个故障。

性能

在Redis集群中,节点不会将命令代理给负责给定key的节点,而是将客户端重定向到服务于给定key空间部分的正确节点。

最终,客户端获得集群的最新表示,以及哪个节点服务于哪个key子集,因此在正常操作期间,客户端直接连接正确的节点,以便发送给定的命令。

由于使用异步复制,节点不会等待其他节点的写入确认(如果没有使用wait命令明确请求)。

此外,由于多key命令仅限于相近key,因此除非重新分区,否则数据不会在节点之间移动。

正常操作的处理方式与单个Redis实例完全相同。这意味着,在具有N个主节点的Redis集群中,因为是线性扩容的设计,您可以预期得到单个Redis实例乘以N的性能。同时,查询通常在一次往返中执行,因为客户端通常保留与节点的持久连接,因此延迟数据也与单个独立Redis节点的情况相同。

非常高的性能和可扩展性,同时保持较弱但合理的数据安全和可用性是Redis集群的主要目标。

为什么合并操作是被禁止的

Redis Cluster 的设计避免了在多个节点中同一键值对的版本冲突,Redis数据模型决定了,合并操作并不总是理想的。Redis中的值通常非常大,常见的情况是,列表或排序集包含数百万个元素。数据类型在语义上也是复杂的。传输和合并这些类型的值可能是一个主要瓶颈,可能需要应用程序端非核心逻辑升级、使用额外内存存储元数据等等。

这里没有严格的技术限制。CRDT或同步复制状态机可以模拟类似于Redis的复杂数据类型。然而,这些系统的实际运行时行为与Redis集群不同。Redis集群的设计是为了涵盖非集群Redis版本的确切用例。

思考题

  • Redis Cluster满足了CAP理论的哪几条?
  • Redis的无Proxy的架构有什么优缺点 ?

附录: CRC16 C代码实现

/*
 * Copyright 2001-2010 Georges Menie (www.menie.org)
 * Copyright 2010 Salvatore Sanfilippo (adapted to Redis coding style)
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the University of California, Berkeley nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF 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
 * following parameters:
 *
 * Name                       : "XMODEM", also known as "ZMODEM", "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
 */

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;
}           

原文链接:https://zhuanlan.zhihu.com/p/577422724

继续阅读