在2016杭州云栖大会的“开源数据库之Redis专场”上,来自唯品会的高级数据工程师申政带来了题为《Redis在唯品会的应用实践》的精彩分享。分享中,他主要介绍Redis集群架构演进、Redis使用经验以及唯品会对Redis二次开发实践积累三部分,干货满满,精彩不容错过。
以下内容根据演讲PPT及现场分享整理。
Redis集群架构演进
目前在唯品会内对Redis的使用属于重量级别,目前在唯品会内大概有8000个Redis实例、1000台物理机、500个应用。
上图是唯品会对Redis使用的演进历史图。在2014年7月左右,采用的是客户端分片(Client Sharding)的架构;2014年7月到2015年12月期间,主要采用的是Twemproxy集群模式;从2015年6月至今,唯品会内主要采用Redis Cluster框架。
下面简单介绍上述三种框架的优缺点。
Client Sharding架构
Client Shareding的架构如上图所示,从图上可以看出该架构十分简单稳定,但它需要业务方面自定义Key分布,进而增加了业务开发难度和工作量;其次该架构的在线扩容能力非常弱,并且不能在数据库层面提供Failover能力。
Twemproxy架构
唯品会内对Twemproxy的使用超过2000个实例、800台物理机、150个应用,其框架如上图所示,相比于Client Shareding的架构复杂了很多,通过在Twempoxy层上增加了LVS,既增加了Twempoxy的高可用,又可以在LVS下任意增加或删除Twempoxy节点,很好的支持了增量业务。
Twemproxy负责数据分片,下面可以挂载多个Redis节点,此外架构中通过采用Redis官方提供的HA保证主从之间的高可用。Twemproxy具有很多优点:
- 支持数据自分片和一致性Hash
- 同时支持Redis和MC协议
- 支持pipeline、mget、mset等多Key操作
- Twemproxy自身扩容简单,可以做到用户无感知,降低了业务开发复杂度
事有正反面,在使用Twemproxy过程中也发现其存在很多不足:首先该架构十分复杂,导致机器成本高;其次,Redis和MC扩容比较难;此外,该架构分为三层,每一个环节都可能成为集群的性能瓶颈;客户端的请求需要经过三层的请求链路,导致请求相应时间长;最后尤其值得注意的一点是:它的运维成本很高。
Redis Cluster
Redis Cluster框架如上图所示,从图中可以看出:它是一个无中心框架,集群中每个节点完全对等,任一节点都与其他节点保持一个长连接,通过高速协议维护整个集群的状态信息;并且该架构还提供了自动Failover和在线扩缩容能力;同时采用单层框架,相应时间短;此外,由于该架构中客户端与Redis直接相连,相比于Twemproxy架构,节省了一半的机器开销。
但该架构对客户端依赖非常重,需要智能客户端支持;对Mget、Mset、Pipeline支持不友好。
一些经验
下面分享一下在使用Redis过程中踩过的坑,供大家借鉴。
在使用Twemproxy时,常发现线上同目录实例的内存特别大,达到几G、几十G,甚至比Redis占据的内存还要大。这是因为在业务开发时,一次使用Pipeline过大,命令数量特别多,这是由于Twemproxy是一次性接受完客户端发来的所有数据再转发导致;同时Twemproxy申请的内存申请后不释放,进而导致了内存只增不减。
因此,建议在使用Twemproxy时,一次Pipeline包含的命令量适中,最好不要超过1000个。
按照Twemproxy执行Hash规则,当后端server出现问题时,理论上只会影响当前Server内的Key。但我们遇到的问题是:当其中的一个Server出现问题时,其他Server返回请求都出现问题。这是由Twemproxy代码结构所决定的,它对每一个客户端的链接都有一个队列,当队列头部的请求恰好是出现问题Server的请求(不能响应到Twemproxy),则该请求会将队列中后续的其他Server请求全部卡死,此时这些请求都缓存在Twemproxy,不能相应到客户端,进而导致Twemproxy的内存飙升。
还有一点需要注意:Twemproxy中Timeout参数,默认是永远等待,即便Server没用相应,但其仍认为该Server是正常的,一直等待下去。因此在使用Twemproxy时,务必要注意Timeout参数的设置。
Twemproxy中设计了剔除策略。有时Twemproxy下面挂载的Redis/MC并没有挂掉,网络也没出现问题,但Twemproxy会剔除某些Server。这是由Twemprox自身的设计缺陷导致,在Twemproxy中存在server_connections和server_failure_limit两个参数,前者表示Twemproxy与后端Server建立长连接的数量;后者是Twemproxy与后端server出现问题的连接数,如果连接连续出现问题,则触发剔除策略。
在实际使用时,建议设置server_connections数目小于server_failure_limit,以规避剔除现象的发生。
在使用Twemproxy时,曾出现过这种情况:业务监控客户端连接只有几十个,而Twemproxy端连接却有几千个、甚至上万个。
这是由LVS机制导致的:LVS内有一个专门维护客户端向后端发起连接数量的表,其内置了exprie机制,当连接在15min没有活跃的请求时,会将该连接从表内剔除,而不通知双方。此时,Twemproxy认为该连接是正常的,而客户端发现连接不通时,会新建连接继续发送请求,长此以往,两者的连接数差距越来越大;并且Twemproxy的文件描述符会消耗很快,直到不能再正常建立连接。
为了解决该问题,在Twemproxy中增加了TCPKeepalive功能,完美地规避了该情况的发生。
如果在使用Twemproxy时,发现线上Twemproxy内存监控曲线如上图所示,并且在业务中还用到了Mset命令,基本上可以确定是由Mset引起的内存泄露。
在Mset命令进行拆分时,由于特殊情景导致内存不被释放,进而导致Twemproxy内存越来越大。应对该问题,可以采用重启Twemproxy进行解决。
在使用Redis Cluster的过程中,唯品会积累了一些实践技巧,先总结如下:
- Cluster-require-full-coverage建议为no(官方默认为yes)
- Cluster-node-timeout建议适当增大
- jedisCluster注意捕获异常MaxRedirectionsException(该异常表示命令执行不成功)
- 不建议使用mget、mset等multi-key命令
- 尽量使用官方redis-trib脚本管理集群,不要轻易使用Cluster命令
二次开发
在使用Redis过程中,为了更加满足业务需求,唯品会对其进行了二次开发。
对于Twemproxy,为了保障其高可用性,增加了replace_server命令,当Redis Master挂掉之后,Sentinel调用脚本通过replace_server命令把Twemproxy中的Master替换成Slave。
目前唯品会线上最大的Twemproxy集群用到的实例超过100个,如果想更改Twemproxy配置时,工作量是比较大的。因此,唯品会在Twemproxy集群中集成zookeeper,Twemproxy集群的配置信息统一由Zookeeper管理、分发,用户无感知。
唯品会对MC也进行可二次开发,由于MC中不存在主从概念,但MV集群中其中一个节点挂掉之后会对后端的数据库造成很大的压力。为了解决该问题,在Twemproxy层面增加了复制池(Replication pool)功能,在Master pool和Slave pool之间建立主从关系,客户端同时写入两个pool内;读取时,首先读取Master pool中内容,当Master pool中无数据时,并不会回传到数据库,而是从Slave pool中获取数据,有效减轻由于MC挂掉带给后端数据库的压力。
借鉴Redis,在Twemproxy中增加了Slowlog功能,停留时间;此外,还增加简单统计信息功能,将一天分为几个时间段,每个时间段记录Slowlog次数,便于定位Slowlog和解决Slowlog。
增加多线程功能是对Twemproxy一次大的改动。Twemproxy提供的QPS在几万左右,由于线上每个物理机上只能部署一个Twemproxy,导致了物理机极大地浪费。借鉴MC多线程模型,建立了master线程+N个worker线程多线程模型:master线程负责监听端口,接受新的客户端连接;worker线程负责处理客户端请求。Twemproxy多线程开6个线程时,QPS可以达到35万/s,同时也节约了千万服务器成本。
很多用户在使用Redis Cluster是都会遇到这样问题:flushall删不掉数据;明明Master是存活的,网络也正常,但Cluster却执行了failover。这是因为Redis是单线程的,在执行时间复杂度为O(n)命令时,线程被阻塞,无法响应其他节点的ping命令,造成假死现象,进而导致主从切换。
针对该问题的处理方式是:增加了额外进程监听额外端口(extra-port),当超过cluster-node-timeout/2s时间,ping请求没有收到回复,主动向extra-port发送ping请求。
最初上线Redis cluster时,很多客户端并不是友善支持,因此增加了Redis Cluster客户端功能,在Hiredis上进行功能改造使其支持集群模式,用于:
- 解析并更新路由表
- 处理moved/ask错误
- 内含Max redirect机制
- 支持mget、mset、pipeline、异步API
为了应对Redis集群之间的数据迁移,唯品会开发了Redis Migrate迁移工具,它是基于Redis复制功能的,在迁移过程中,老集群正常对外提供服务;并且支持多线程异构迁移,支持Twemproxy、Redis Cluster、RDB文件和AOF文件恢复;同时迁移过程也可以实时查看;此外,该迁移工具还具有比较完善的数据抽样校验机制,保障迁移数据的一致性。
Redis Migrate具体迁移步骤如上所示,这其中关键点是左侧的配置文件,这里不不再一一叙述。
Reids多线程是目前唯品会正在改造,借鉴于上文提到的Twemproxy多线程改造,将Redis改造成Master+N个worker多线程模型:客户端有worker线程管理;多线程中不可能避免有锁的存在,Redis中存在DB级别读写锁,通过单独后台线程处理过期key和dict维护。
目前Redis多线程实现支持五大数据结构(String/list/hash/set/zset);支持100个Redis命令(包括管理命令)。
Redis多线程中,为了降低DB锁竞争,引入了逻辑DB。一个逻辑DB包含N个真实物理DB,用户使用的是逻辑DB。一个逻辑DB的所有Key分散在各个物理DB上吗,每个物理DB拥有一把读写锁——pthread_rwlock_t。
上图是Redis处理客户端请求的命令过程,这里不再一一叙述。每个Worker线程都会进行上图六个步骤,DB锁的竞争主要是在第四步和第五步上,其他步骤Worker之间完全并行。
Redis多线程-性能测试
上图是用于Redis多线程性能测试的测试环境配置。
测试用例图上图所示,从图上可以看出SET的效率为34万左右;GET效率为40万左右。
热Key问题是使用Redis中不可避免的问题。在Redis多线程中,遇到热Key首先将其拆分,将其分散到不同的Redis实例中。上图是多线程中针对热Key的性能测试:只写一个key时,四个线程的QPS可以达到30万/s;只读一个Key时,四个线程的QPS可以达到43万/s,一定程度上有效改善了热Key问题。
附录:
文章中提到的唯品会对Redis二次开发的源代码开源地址:http://github.com/vipshop。