今天我们来聊一聊Redis集群。先看看集群的特点,我对它的理解是要需要同时满足高可用性以及可扩展性,即任何时候对外的接口都要是基本可用的并具备一定的灾备能力,同时节点的数量能够根据业务量级的大小动态的伸缩。那么我们一般如何实现呢?

集群的实现方式

说到集群的实现,我会想到两种方式

  • 第一种是去中心化的集群
    • 整个集群是由一组水平的节点构建
    • 通过给各个节点分配不同的角色实现相互配合,并同步各自状态
    • 对外提供单调的接口
    • 整个集群依靠内部的同步机制来进行伸缩和容错
    • 实现复杂

这种集群,常见的有Zookeeper以及以MongoDB为代表的大部分NoSQL服务,包括Redis官方集群3.0 Cluster也是属于这种类型。

  • 第二种集群是基于Proxy的集群(反向代理)
    • 引入一个Proxy中间件来管理整个集群,托管后端节点
    • 通过Zookeeper这种第三方组件实现集群的数据和状态同步
    • Proxy本身能够水平扩展,并方便实现auto-balance
    • 实现简单
    • 需要保证Proxy本身的高可用性

这种方式有人会称之为伪集群,毕竟需要依靠第三方组件来实现集群化,但从整体架构上来看,这种实现方式确实可以满足集群的标准,即满足高可用性和可扩展性,它的优点是实现简单,并且通过Proxy去维护集群的状态要比去中心化的方式更加方便,这也是为什么我选择Codis而不是官方Cluster的原因。下面会详细介绍。

Redis集群——Codis2.0

那么具体到Redis的集群实现,目前最流行的应该是Twitter开源的Twemproxy,再就是近年官方推出的Redis 3.0 Cluster。今天我要介绍的是来自豌豆荚开源的Codis,它是一套基于Proxy模式的Redis集群服务,Codis目前的版本是2.0。

  • 相比Twemproxy,Codis能够提供动态的sharding,即无缝扩展redis的节点,省去了人工数据迁移的成本以及down掉服务带来的风险。另外,这二者都是基于Proxy模式的集群构建,性能上并不存在太大的差别。
  • 相比Redis Cluster,Codis的优势在于:首先它基于Proxy的代理模式能够无缝兼容所有的redis client;而Redis Cluster则需要使用配套的client sdk来替换我们之前的程序,因为这种模式下客户端需要维护集群相关的信息。其次,基于Proxy的集群,使得我们可以更加清晰的掌握整个集群的状态,因为Codis将所有的操作命令和集群拓扑结构都同步在Zookeeper中,而Zookeeper的集群也是我们常用并熟悉的,这也是我非常青睐Codis的一个原因。

Codis主要的关键技术我认为有三点:

  • Pre-sharding
    • Codis Proxy在Zookeeper维护了1024个slots,并建立了每个slot和后端redis group的路由表
    • 一个redis group包括一组master/slave的redis实例
    • 通过crc32(key)%1024计算出每个key对应的slot编号,然后查询路由表即可得到每个key对应的具体redis实例,从而打通数据代理
    • 这样redis实例可以从一开始的单台节点扩展到最大1024个节点,按照目前机器的配置基本可达到无限扩展
  • Zookeeper
    • Codis中所有的运维操作命令都通通过zookeeper同步到每一个Proxy中,包括slot的迁移、Group的变化等。此外,pre-sharding的路由信息也存放在zookeeper中。这些使得Codis Proxy能够水平扩展并协同工作
    • zookeeper还能够被用来做Proxy的服务发现和负载均衡。后面我们会再讲到
  • 动态迁移
    • 动态迁移是指不用down掉服务也能够将redis key平滑的迁移到另一个group上
    • 传统的redis迁移,我们可能会想到通过对先有Redis挂载slave,将存量数据热备到新增节点,然后改变slot和group的路由表,将一部分数据切到新机器上。然而这种方式很难保证节点切换中的数据一致性,如果要保证这一点只能做通过停服来做静态sharding,之前的Twemproxy就只能这么做
    • 实际上Codis实现的动态扩容是通过在官方的Redis Server中植入MIGRATE命令来实现的,并确保了该操作的原子性
    • 由于真正执行迁移的是通过额外的工具codis-config来实现,所以不用担心会影响Proxy正常处理请求的性能
    • 迁移slot的操作会通过zookeeper同步给Proxy用于快速感知,即如果在slot迁移中,对应的key发生了操作,Proxy会强制执行一次SLOTSMGRTTAGONE命令将这个key数据单独做一次迁移

以上的三条保证了Codis能够满足高可用性和可扩展性的标准。

 关于Codis的使用和性能测试,请转到他们的主页——https://github.com/wandoulabs/codis。本文主要从架构和源码上对Codis进行介绍。

Codis Proxy 2.0源码解析

下面我们一起解读一下Codis Proxy 2.0的源码。

以下内容推荐在电脑上阅读。

由于Codis是由Go语言编写的,这也是非常吸引我了解的一点,Go语言天生的高并发特性非常适合写这种高并发的接入层/中间件服务,codis代码正是运用了go routine的简洁高效,再配合channel做数据同步,sync做状态同步,整体代码还是比较简单明了的。

具体的Go语法可以参考https://golang.org,很值得去学习,特别是用惯了C/C++和python的同学,Go语言在开发和运行效率上的兼顾一定会让你觉得心旷神怡。

在了解Codis代码之前,我还是先解释一下go routine这个概念。我们了解以下几点:

  • routine可以解释为协程,类似于python中的greenlet(https://greenlet.readthedocs.org)
  • 协程可以看做是微小的线程,内存开销极小且由程序自己来进行调度,从而能最大化的利用CPU时间
  • 而标准的线程是由操作系统来统一调度,线程栈消耗一般在1-8M,这样在高并发的情况下二者的性能差异可想而知。

    go routine也是被先天植入go语言之中,因此用它来编写并发程序再适合不过了。

好了,下面我们来看Codis Proxy的代码。

Codis Proxy代码结构比较清晰,整个程序基本上就是在不同的go routine之间同步各种数据和状态,只要抓住几个关键的go routine流程,再结合Proxy的架构就能够很清晰的明白了。
下面我对Codis Proxy 2.0的程序架构做了模块化的展示。

  • 红色箭头代表集群和外部的连接
  • 黑色箭头属于内部连接
  • 蓝色模块代表go routine
  • 绿色模块代表程序中的模块和函数
  • 紫色模块代表监听的事件

结合以上的架构图,我们可以很清晰的知道Codis Proxy的工作流程。
下面对关键代码做进一步的讲解。

1、初始化Proxy Server对象

Codis Proxy在初始化时会构建一个Server的对象,并第一时间向zookeeper注册自己。

type Server struct  {
  conf *Config       //Proxy配置,包括proxy id、name、zk的地址、timeout参数、redis授权信息等
topo *Topology     //用于访问ZooKeeper的对象,顾名思义,能够从zk获取整个集群的拓扑结构
info models.ProxyInfo //封装Proxy的基本信息,包括id、addr等
groups map[int]int //存放slot和group的映射,index表示slot id,当slot对应group发生变化时,
proxy会根据此映射对slot做reset,即调用fillSlot
  lastActionSeq int //同步序列号,这个类似于版本号同步协议,用于同步zookeeper中的操作命令,比如slot迁移
evtbus chan interface{} //这个channel用于从zookeeper获取最新的操作指令
router *router.Router //路由对象,1、设置并维护slots的后端连接 2、dispatch客户端请求到后端redis
  listener net.Listener //tcp socket listener,用于监听并accept客户端的连接请求
kill chan interface{} //Proxy收到SIGTERM信号时会激活该channel,然后清理zk的状态并正常退出
wait sync.WaitGroup  //go routine的同步对象,用于主线程同步go routine的完成状态
stop sync.Once      // Proxy Close时一次性清理所有资源,包括client以及slot的后端连接
}

之后主线程通过go routine创建第一个协程G1,开始工作。
而主线程会调用wait.Wait(),等待G1的完成,只有在Proxy意外退出或是主动发送mark_offline时整个程序才会结束。G1在调用Serve方法之后,首先会check自己在zk的状态是否是online,然后才能开始工作。注意,在Codis2.0中,主线程会自动调用Codis-config来使自己上线,不再需要手动的去markonline。check成功之后,G1会向zookeeper注册actions节点的watch,这样就可以用来实时感知zookeeper中的操作命令了,包括slot迁移,group的变化等。之后G1会初始化各个slot的后端连接,紧接着再创建一个routineG2,用于handle客户端的连接,即承担接入redis客户端的工作。而G1自己会调用loopEvent,通过select监听zookeeper中的操作命令以及kill命令。

注意,Go中的select要比Unix的select调用强大很多,只是名字一样罢了,我想底层应该是采用epoll的实现方式

2、handleConns处理客户端连接

好了,现在G1和G2都进入了各自的Loop中高效的运转了。我们看一下G2的代码。

<br />func (s *Server) handleConns() {
     ch := make(chan net.Conn, 4096)
     defer close(ch)

go func() {
          for c := range ch {
               x := router.NewSessionSize(c, s.conf.passwd, s.conf.maxBufSize, s.conf.maxTimeout)
               go x.Serve(s.router, s.conf.maxPipeline)
          }
     }()

for {
          c, err := s.listener.Accept()
          if err != nil {
               return
          } else {
               ch <- c
          }
     }
}

这段代码用于处理客户端的接入请求,想起我们之前用C写的epoll单线程回调,这个看起来是不是很简洁呢^_^这就是go routine的魅力,可以抛弃繁琐的回调。

OK,下面我们继续进入G2这个协程,如代码所示。

  • 这里会实时的accept客户端的redis连接,并为每一个连接N单独创建一个协程G2N用于request/response(参考上面的架构图)。
  • G2N会运行在 loopReader中,实时的从socket读取client的请求,并按照RESP(Redis Protocol)的协议进行解码,接着调用 handleRequest进行请求分发。对于部分命令比如MSET/MGET,codis是做了特殊处理的,原因在于批量处理的key可能分布在不同的redis实例上,所以在codis这里需要将不同的key dispatch到不同的后端,得到响应之后再统一打包成Redis Array返回给客户端。
    此外,G2N会额外再创建一个routine G2NW,用于向client回写请求的数据结果,并运行在loopWriter中。参考G2N的主要逻辑代码如下。

<br />func (s *Session) Serve(d Dispatcher, maxPipeline int) {
     var errlist errors.ErrorList
     defer func() {
          if err := errlist.First(); err != nil {
               log.Infof("session [%p] closed: %s, error = %s", s, s, err)
          } else {
               log.Infof("session [%p] closed: %s, quit", s, s)
          }
     }()

tasks := make(chan *Request, maxPipeline)
     go func() {
          defer func() {
               s.Close()
               for _ = range tasks {
               }
          }()
          if err := s.loopWriter(tasks); err != nil {
               errlist.PushBack(err)
          }
     }()

defer close(tasks)
     if err := s.loopReader(tasks, d); err != nil {
          errlist.PushBack(err)
     }
}

其中loopWriter即为G2NW协程所运行的函数栈。

3、命令分发(反向代理/Dispatch)

下面说一下Dispatch

  • G2N在收到客户端请求的key时,会查看key相关的slot信息,通过查询路由表来获取对应后端redis实例的连接,后端连接的托管是放在backend.go这个模块中的。
  • router模块在初始化slots的时候,维护了一个backend的连接池,当有redis key的请求过来时,会将请求打包成Request对象然后再分发给该slot对应的后端连接,要注意的是归属同一个redis group的slot就会复用同一个后端连接。
  • router通过将Request对象dispatch到后端连接监听的同步队列channel中,以此来解决并发控制的问题。
  • 具体的每一个BackendConnection都会各自运行两个routine:
    • 一个执行loopWrite,不断的获取从router dispatch过来的Request对象,然后Encode成RESP格式发送给后端的redis实例
    • 另一个routine用于Decode从Redis实例返回的结果,并通过调用setResponse方法来告知前端的G2NW,这里setResponse代码如下:

<br />  func (bc *BackendConn) setResponse(r *Request, resp *redis.Resp, err error) error {
     r.Response.Resp, r.Response.Err = resp, err
     if err != nil && r.Failed != nil {
          r.Failed.Set(true)
     }
     if r.Wait != nil {
          r.Wait.Done()
     }
     if r.slot != nil {
          r.slot.Done()
     }
     return err
}

可以看到这里会调用wait.Done和slot.Done来通知前端的routine。这两个Done的区别在于,wait.Done用于同步请求的处理完毕状态,而slot.Done用于同步该slot的状态,因为当Codis在收到slot迁移指令时需要调用fillSlot对slot进行重置,而此操作需要等待对应slot上的所有代理请求处理完毕之后才能进行。

这里涉及到Go语言sync模块的内容,具体可以参考https://golang.org/pkg/sync/#WaitGroup

由于篇幅有限,整个Codis Proxy2.0的代码先介绍到这里,读者可以结合上面的架构图对代码做进一步的了解。我们不难发现,整个代码都是由go routine、channel、sync来构建,这也是go语言并发编程的核心概念。

Codis Proxy Auto-balance

因为Codis是基于Proxy模式构建的集群,这就要求我们必须保证Proxy组件的高可用性,换句话说,我们需要做好Proxy组件的auto-balance和服务发现。推荐一个解决方案如下:

  • 我们可以搭建N个Codis Proxy来分担负载,每个Proxy的id和addr不同即可。
  • Codis Proxy的服务发现,可以通过监听zookeeper来完成。
  • 对于pytho开发者,我实现了一个pycodis的组件,用于python连接codis proxy,大致原理是监听zookeeper中proxy节点的状态,如果某台proxy挂掉了,可以及时的调整连接池,保证client每次获取到的都是最新并可用的连接,不需要修改client配置和重启,同时也能够保证每台Proxy的负载均衡。这主要得益于Codis使用Zookeeper来进行状态同步,也就天生具备了服务发现的优势。

以上对了Redis集群——Codis2.0做了大致的介绍,也是我认为目前最可靠的redis集群方案之一,并且这种集群的实现架构也是值得我们其他系统借鉴的。当然这种Proxy的方式还是存在一些先天缺陷,比如很难支持事务和批量操作,但我想对于大部分应用场景来说它的支持已经足够了。

好了,期待Codis下一次的更新吧。另外,如果各位看官有更好的实践,欢迎赐教,期待和大家一起交流和探讨。

Redis集群最佳实践的更多相关文章

  1. windows搭建redis集群最佳实践

    一.redis的下载安装: (1)下载Redis-x64-3.2.100地址:https://github.com/MicrosoftArchive/redis/releases (2)安装后文件如下 ...

  2. redis集群搭建实践

    参考 第一个节点 第一个节点为本地的机器 IP:192.168.23.148 检查机器配置 $ uname -a Linux wangya-Lenovo-G480 4.8.0-52-generic # ...

  3. [转载] Redis集群搭建最佳实践

    转载自http://blog.csdn.net/sweetvvck/article/details/38315149?utm_source=tuicool 要搭建Redis集群,首先得考虑下面的几个问 ...

  4. Redis集群搭建最佳实践

    要搭建Redis集群.首先得考虑以下的几个问题; Redis集群搭建的目的是什么?或者说为什么要搭建Redis集群? Redis集群搭建的目的事实上也就是集群搭建的目的.全部的集群主要都是为了解决一个 ...

  5. 转 Redis集群技术及Codis实践

    转  Redis集群技术及Codis实践 转自 :http://blog.51cto.com/navyaijm/1637688 codis开源地址:https://github.com/CodisLa ...

  6. Redis集群研究和实践(基于redis 3.0.5)

    前言 redis 是我们目前大规模使用的缓存中间件,由于它强大高效而又便捷的功能,得到了广泛的使用.现在的2.x的稳定版本是2.8.19,也是我们项目中普遍用到的版本. redis在年初发布了3.0. ...

  7. redis集群与分片(2)-Redis Cluster集群的搭建与实践

    Redis Cluster集群 一.redis-cluster设计 Redis集群搭建的方式有多种,例如使用zookeeper等,但从redis 3.0之后版本支持redis-cluster集群,Re ...

  8. Redis集群环境搭建实践

    0 Redis集群简介 Redis集群(Redis Cluster)是Redis提供的分布式数据库方案,通过分片(sharding)来进行数据共享,并提供复制和故障转移功能.相比于主从复制.哨兵模式, ...

  9. Bash实践:抽样检测数据迁移至Redis集群后的数据一致性

    熟悉了一段时间的Bash编程,因此借此任务操作一把bash编程,主要涉及到Redis单节点与Redis集群的操作 1. 任务背景 近日有个任务需要将历史的Redis(主从节点)中的数据迁移至Redis ...

随机推荐

  1. linux 软连接和硬链接

    硬链接 ln sourceFile targetFile 硬链接只能给文件创建,不能为目录建立硬链接,硬链接不能跨分区创建,  硬链接会增加inode连接数, 硬链接的文件删除不影响其他文件 课外: ...

  2. Python全栈开发【基础三】

    Python全栈开发[基础三]  本节内容: 函数(全局与局部变量) 递归 内置函数 函数 一.定义和使用 函数最重要的是减少代码的重用性和增强代码可读性 def 函数名(参数): ... 函数体 . ...

  3. NodeJS 最快速搭建一个HttpServer

    最快速搭建一个HttpServer 在目录里放一个index.html cd D:\Web\InternalWeb start http-server -i -p 8081

  4. iOS SpriteKit 问题

    今天偶然发现 向SKShapeNode添加子 node时,子node参考的是 SKShapeNode的parent的坐标系,但是如果使用SKSpriteNode却是使用自己的坐标系,带研究.而且sha ...

  5. freemarker 中文乱码

    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker ...

  6. WPF 画心2.0版之元旦快乐

    2017年元旦已经到了,想做一个祝福语的窗口,就把上一篇画心的程序改了改,变成了如下界面. 说下改动的地方,首先窗口没有标题栏了. MainWindow.xaml AllowsTransparency ...

  7. splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目

    删除位于 index 2 的元素,并添加一个新元素来替代被删除的元素: <script type="text/javascript"> var arr = new Ar ...

  8. C语言-纸牌计算24点小游戏

    C语言实现纸牌计算24点小游戏 利用系统时间设定随机种子生成4个随机数,并对4个数字之间的运算次序以及运算符号进行枚举,从而计算判断是否能得出24,以达到程序目的.程序主要功能已完成,目前还有部分细节 ...

  9. Conditional project or library reference in Visual Studio

    Conditional project or library reference in Visual Studio In case you were wondering why you haven’t ...

  10. win10常用帮助

    添加自启动项: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp shell:startup win10找回图片查看器: Win ...