在上一篇文章中我们通过主从复制可以人为的搭建集群,也可以通过 Sentinel 管理多台 Redis 服务器,自动维护一个集群,都可以解决单机情况下 master 出现单点故障问题,实现服务的高可用。但是都属于在 X 轴方向上的水平扩展,每台机器都需要存储全量数据,所以没有解决单台机器容量有限的问题
我们可以把数据根据业务逻辑进行划分,每台 Redis 服务只存储一部分内容,并对外提供一部分服务,这样单台 Redis 服务器就不需要存储全量数据。
既然有多个 Redis 实例对外提供服务,那么数据是怎么分区映射存储到不同的 Redis 实例?请求是怎么负载到不同的服务进程?
数据分治可以在 Redis 服务下手,也可以在客户端下手,我们先来考虑一下在 client 端如何进行数据分治
首先如果数据可以根据逻辑拆分,可以让不同逻辑的数据存储到不同的 Redis 服务实例
弊端:很多场景下,数据并不能简单的根据逻辑进行拆分,所以这种方案对数据特征有很大的要求,限制性较大
如果数据不可以简单的根据逻辑拆分,我们可以使用各种映射算法,让数据按照一定的规则映射、保存在不同的 Redis 服务实例上。映射算法中我们最常用的就是 hash 算法
首先可以使用 hash 算法 + 取模,将数据模上 Redis 服务实例个数,让数据映射、存放到不同的服务实例
弊端:hash 算法有一个天生的缺陷——算法取模需要根据机器数进行取模,但是增加机器就会导致 hash 算法的取模混乱,无法在正确的 Redis 实例中取到数据。
此时需要进行全局洗牌,全部的数据都需要 rehash,数据需要重新计算和迁移,在目前数据量这么大的情况下,会造成整个系统响应速度变慢,甚至服务不可用,所以 hash 算法会限制整个系统的扩展性
既然 hash 算法需要根据实际的物理机器取模,会限制系统的扩展性,那么我们想办法让它不进行取模不就行了?这样的算法有很多,主要思想就是无论给出什么数据,算法都能把它映射成等宽的字符串(或者数字),与数据一一对应
一致性哈希算法没有取模的过程,因为取模会受限于模数,模数限制了扩展性。一致性哈希算法要求数据(key)和设备(node)都是要参与 hash 运算的,一般把它抽象成一个环形,称之为哈希环。
Redis 进程实例会映射在哈希环上的某一个点,数据到达的时候也会映射到哈希环上的一个点,然后就会找到最近的、代表 Redis 实例的一个点进行存储
当添加新的 Redis 服务实例的时候,也会映射在哈希环上一个点,代表此物理进程实例,此时映射关系如下:
优点:新增节点不会造成全局洗牌,数据不需要全部 rehash,确实可以分担其他节点压力
弊端:一小部分数据不能命中,相当于缓存失效,查询击穿到数据库
解决:每次查的时候,不再是查最近的一个node,而是查最近的两个node
特征:这种方案更倾向于作为缓存使用,而不是数据库用!!!
以上的这几种方案都是发生在客户端,客户端里面需要实现逻辑代码实现数据分治。我们来想一下客户端处理会产生什么问题?
以上拓扑关系图中只有一个 client 进行数据请求,实际情况中不可能只有一个 client 连接,并发场景下,会有大量的 client 与 redis 实例进行连接,当 server 端需要处理大量的 socket 连接,压力就会很大
既然由 server 端直接处理连接时的压力过大,此时就可以使用代理处理所有的连接请求,减少 server 端的压力
代理层能够保证 server 端压力不要过大,此时需要关注的点就是代理层如何保证高性能,以及保证可用性,那我们就可以引入负载均衡来保证高性能,并使用主备关系来保证可用性
总结:以上讲了这么多数据分治方案,并没有很明显的优劣之分,每种方案有其独特的使用场景,应该根据具体的场景来选择合适的方案。
重点理解适用于缓存的一致性哈希算法和处理并发大量连接的代理概念
以上的所有方案,redis 都适合当作缓存来用,而不能当作数据库使用,为了将 redis 用作分布式数据库,就需要理解预分区的概念
通过预分区保证,添加新节点的时候并不需要全部数据 rehash,也不会造成部分数据丢失,通过数据迁移保证数据一致性,保证数据不丢失
在上面我们讲为了减少 redis 的 server 服务处理大量的 client 并发请求,所以可以引入 proxy 代理,将请求进行处理,并分发给特定的 redis 实例
其实 redis 自己内部的实现,要比这个方案更加优秀,甚至直接省略了代理层,使用预分区功能实现请求的处理
Redis 将数据进行预分区,每个 redis 实例存储指定槽位的数据,实现数据分治Redis 通过预分区保证添加节点时,会进行数据迁移,数据不会发生错乱当有 client 连接请求到达时,也不需要代理来减轻 server 端连接压力,client 可以任意选择一个 redis 实例连接,由于每个实例中都知道其它所有实例存储数据的情况,所以最多只需要 3 步就可以找到数据Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽
我们演示的时候只说了 10 个槽位,其实 Redis 有 16384 个槽位,意味着你系统可以扩展至 16384 个 Redis 服务实例
官网分区:http://redis.cn/topics/partitioning.html
分区是将你的数据分发到不同redis实例上的一个过程,每个redis实例只是你所有key的一个子集。文档第一部分将介绍分区概念,第二部分介绍分区的另外一种可选方案
Redis分区主要有两个目的:
分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。分区可以在程序的不同层次实现。
客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。无论是把Redis当做持久化的数据存储还是当作一个缓存,从分区的角度来看是没有区别的。当把Redis当做一个持久化的存储(服务)时,一个key必须严格地每次被映射到同一个Redis实例。当把Redis当做一个缓存(服务)时,即使Redis的其中一个节点不可用而把请求转给另外一个Redis实例,也不对我们的系统产生什么影响,我们可用任意的规则更改映射,进而提高系统的高可用(即系统的响应能力)。
一致性哈希能够实现当一个key的首选的节点不可用时切换至其他节点。同样地,如果你增加了一个新节点,立刻就会有新的key被分配至这个新节点。
重要结论如下:
如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样 - Redis 集群已经可用 2015.4.1.从上面获知,除非我们把Redis当做缓存使用,否则(在生产环境动态)增加和删除节点将非常麻烦,但是使用固定的keys-instances则比较简单。
一般情况下随着时间的推移,数据存储需求总会发生变化。今天可能10个Redis节点就够了,但是明天可能就需要增加到50个节点。
既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。
一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。
这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。
截止到目前,我们从理论上讨论了Redis分区,但是实际上是怎样的呢?你应该采用哪种实现方案呢?
官方 Redis 集群教程:http://redis.cn/topics/cluster-tutorial.html
Redis集群是自动分片和高可用的首选方案。新的集群方案2015年4月1日就已经可用。2015.4.1 Google论文. 你可以从这里Cluster tutorial了解更多。
当Redis集群可用,并且有兼容Redis 集群客户端可用于你的编程语言,Redis 集群将成为Redis分区的实际标准.
Redis集群是 query routing 和 client side partitioning的一种混合实现。
官方github:https://github.com/twitter/twemproxy
Twemproxy是Twitter维护的(缓存)代理系统,代理Memcached的ASCII协议和Redis协议。它是单线程程序,使用c语言编写,运行起来非常快。它是采用Apache 2.0 license的开源软件。
Twemproxy支持自动分区,如果其代理的其中一个Redis节点不可用时,会自动将该节点排除(这将改变原来的keys-instances的映射关系,所以你应该仅在把Redis当缓存时使用Twemproxy)。
Twemproxy本身不存在单点问题,因为你可以启动多个Twemproxy实例,然后让你的客户端去连接任意一个Twemproxy实例。
Twemproxy是Redis客户端和服务器端的一个中间层,由它来处理分区功能应该不算复杂,并且应该算比较可靠的。
更多关于Twemproxy in this antirez blog post.
官方github:https://github.com/predis/predis
相对于Twemproxy,另一种可选的分区方案是在客户端实现一致性哈希或者其他类似算法。有很多客户端已经支持一致性哈希,如 Redis-rb 和 Predis.
请检查 Redis客户端完整列表 以确认在你想要使用的编程语言,有成熟的一致性哈希客户端实现。
关联文章:
Redis入门–万字长文详解epoll
Redis——详解五种数据结构
Redis——Redis的进阶使用(管道/发布订阅/事务/布隆过滤器)
Redis——Redis用作缓存(内存回收/穿透/击穿/雪崩)
Redis——Redis用作数据库(持久化/RDB/AOF)
Redis——Redis集群理论
Redis——集群高可用(脑裂/主从复值/哨兵Sentinel)