今天我们来聊一聊Redis集群。先看看集群的特点,我对它的理解是要需要同时满足高可用性以及可扩展性,即任何时候对外的接口都要是基本可用的并具备一定的灾备能力,同时节点的数量能够根据业务量级的大小动态的伸缩。那么我们一般如何实现呢?
说到集群的实现,我会想到两种方式
这种集群,常见的有Zookeeper以及以MongoDB为代表的大部分NoSQL服务,包括Redis官方集群3.0 Cluster也是属于这种类型。
这种方式有人会称之为伪集群,毕竟需要依靠第三方组件来实现集群化,但从整体架构上来看,这种实现方式确实可以满足集群的标准,即满足高可用性和可扩展性,它的优点是实现简单,并且通过Proxy去维护集群的状态要比去中心化的方式更加方便,这也是为什么我选择Codis而不是官方Cluster的原因。下面会详细介绍。
那么具体到Redis的集群实现,目前最流行的应该是Twitter开源的Twemproxy,再就是近年官方推出的Redis 3.0 Cluster。今天我要介绍的是来自豌豆荚开源的Codis,它是一套基于Proxy模式的Redis集群服务,Codis目前的版本是2.0。
Codis主要的关键技术我认为有三点:
以上的三条保证了Codis能够满足高可用性和可扩展性的标准。
关于Codis的使用和性能测试,请转到他们的主页——https://github.com/wandoulabs/codis。本文主要从架构和源码上对Codis进行介绍。
下面我们一起解读一下Codis Proxy 2.0的源码。
以下内容推荐在电脑上阅读。
由于Codis是由Go语言编写的,这也是非常吸引我了解的一点,Go语言天生的高并发特性非常适合写这种高并发的接入层/中间件服务,codis代码正是运用了go routine的简洁高效,再配合channel做数据同步,sync做状态同步,整体代码还是比较简单明了的。
具体的Go语法可以参考https://golang.org,很值得去学习,特别是用惯了C/C++和python的同学,Go语言在开发和运行效率上的兼顾一定会让你觉得心旷神怡。
在了解Codis代码之前,我还是先解释一下go routine这个概念。我们了解以下几点:
go routine也是被先天植入go语言之中,因此用它来编写并发程序再适合不过了。
好了,下面我们来看Codis Proxy的代码。
Codis Proxy代码结构比较清晰,整个程序基本上就是在不同的go routine之间同步各种数据和状态,只要抓住几个关键的go routine流程,再结合Proxy的架构就能够很清晰的明白了。
下面我对Codis Proxy 2.0的程序架构做了模块化的展示。
结合以上的架构图,我们可以很清晰的知道Codis Proxy的工作流程。
下面对关键代码做进一步的讲解。
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的实现方式
好了,现在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这个协程,如代码所示。
<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协程所运行的函数栈。
下面说一下Dispatch。
<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模式构建的集群,这就要求我们必须保证Proxy组件的高可用性,换句话说,我们需要做好Proxy组件的auto-balance和服务发现。推荐一个解决方案如下:
以上对了Redis集群——Codis2.0做了大致的介绍,也是我认为目前最可靠的redis集群方案之一,并且这种集群的实现架构也是值得我们其他系统借鉴的。当然这种Proxy的方式还是存在一些先天缺陷,比如很难支持事务和批量操作,但我想对于大部分应用场景来说它的支持已经足够了。
好了,期待Codis下一次的更新吧。另外,如果各位看官有更好的实践,欢迎赐教,期待和大家一起交流和探讨。
原文:http://www.cnblogs.com/HANYI7399/p/4939336.html