背景
redis cluster简介
Redis cluster是redis官方提供集群方案,设计上采用非中心化的架构,节点之间通过gossip协议交换互相的状态,redis cluster使用数据分片的方式来构建集群,集群内置了16384个哈希槽,每个key都属于这16384这个哈希槽中的一个,通过crc16算法计算哈希值,再取余可得每个key归属的哈希槽;redis cluster支持动态加入新节点,动态迁移slot,自动的故障转移等。
Redis cluster的架构要求客户端需要直接与redis集群中的每个节点建立连接,并且当出现新增节点加入、节点宕机failover、slot迁移等事件时,客户端需要能够通过redis cluster协议去更新本地的slot映射表,并且能处理ASK/MOVE语义,因此,我们一般称实现了redis cluster协议的客户端为smart redis client。
Redis cluster最多可以构建超过100个主节点的集群(超过之后gossip协议开销过大,且可能引起集群不稳定),按照单节点10G容量(单实例内存过大可能导致性能下降),单集群最多可以支撑1T左右的容量。
问题
redis cluster有很多优点(比如可以构建大容量集群,性能好,扩缩容灵活),但是当一些项目工程期望从redis迁移到redis cluster时,客户端却面临着大量的改造工作,与此同时带来的是需要大量的测试工作以及引入的新风险,这对于一些稳定运行的线上工程代价无疑是巨大的。
需求
为了更方便的将业务迁移到redis cluster,最期望的是客户端SDK的API完全兼容redis/redis-cluster,spring提供的RedisTemplate是一个很好实现,但是对于没有使用SpringRedisTemplate的项目,很多客户端实现的redis和redis-cluster访问API是不一致的(比如Java中流行的Jedis),这无形中提高了迁移工作的工作量和复杂性,此时redis cluster proxy是不错的选择,有了proxy,就可以像操作单实例redis一样操作redis cluster,客户端程序就不需要做任何的修改。
当然,增加一层proxy,必然会导致性能有一定程度的下降,但是proxy作为无状态的服务,理论上可以水平扩展,并且由于proxy层的存在减少了后端redis server的连接数,在某些极端场景下甚至能提高redis集群整体的吞吐量。此外,基于proxy,我们还可以做很多额外的事情:
功能
介于上述各种原因和需求,我们基于netty开发了camellia-redis-proxy这样一个中间件,支持如下特性:
如何提升性能
客户端向camellia-redis-proxy发起一条请求,到收到请求回包的过程中,依次经历了如下过程
可以看到作为一个proxy,大量的工作是在进行网络IO的操作,为了提升proxy的性能,做了以下工作:
多线程
我们知道redis本身是单线程的,但是作为一个proxy,完全可以使用多线程来充分利用多核CPU的性能,但是过多的线程引起不必要的上下文切换又会引起性能的下降。camellia-redis-proxy使用了netty的多线程reactor模型来确保服务器的处理性能,默认会开启cpu核心数的work线程。 此外,如果服务器支持网卡多队列,开启它,能避免CPU不同核心之间的load不均衡;如果不支持,那么将业务进程绑核到非CPU0的其他核心,从而让CPU0专心处理网卡中断而不被业务进程过多的影响。
异步非阻塞
异步非阻塞的IO模型一般情况下性能都是优于同步阻塞的IO模型,对于proxy场景尤其如此,上述5个过程中,除了协议转发规则匹配这样的内存计算,整个转发流程都是异步非阻塞的,确保不会因为个别流程的故障影响整个服务。
流水线
我们知道redis协议支持流水线(pipeline),pipeline的使用,可以有效减少网络开销。camellia-redis-proxy也充分利用了这样的特性,主要包括两方面:
当然,所有这些批量和聚合的操作都需要保证请求和响应的一一对应。
TCP分包和大包处理
不管是上行协议解析,还是来自后端redis的回包,特别是大包的场景,在碰到TCP分包时,利用合适的checkpoint的机制可以有效减少重复解包的次数,提升性能。
异常处理和异常日志合并
如果没有有效的处理各种异常,在异常发生时也会导致服务器性能迅速下降。想象一个场景,我们配置了90%的流量转发给A集群,10%的流量转发到B集群,如果B集群发生了宕机,我们期望的是来自客户端的90%的请求正常执行,10%的请求失败,但是实际上却可能远远超过10%的请求都失败了,原因是多方面的:
camellia-redis-proxy在处理上述问题时,采取了如下策略:
部署架构
proxy作为无状态的服务,可以做到水平扩展,为了服务的高可用,也至少要部署两个以上的proxy节点,对于客户端来说,想要像使用单节点redis一样访问proxy,可以在proxy层之前设置一个LVS代理服务,此时,部署架构图如下:
当然,还有另外一个方案,可以将proxy节点注册到zk/Eureka/Consul等注册中心,客户端通过拉取和监听proxy的列表,然后再向访问单节点redis一样访问每个proxy即可。以Jedis为例,仅需将JedisPool替换为封装了注册发现逻辑的RedisProxyJedisPool,即可像访问普通redis一样使用proxy了,此时,部署架构图如下:
应用场景
结语
Redis cluster作为官方推荐的集群方案,越来越多的项目已经或正在迁移到redis cluster,camellia-redis-proxy正是在这样的背景下诞生的;特别的,如果你是一个Java开发者,camellia还提供了CamelliaRedisTemplate这样的方案,CamelliaRedisTemplate拥有和普通Jedis一致的API,提供了mget/mset/pipeline等原生JedisCluster不支持的特性,且提供了和camellia-redis-proxy功能一致的分片/双写等特性。
为了回馈社区,camellia已经正式开源了,想详细了解camellia项目的请移步github,地址如下:https://github.com/netease-im/camellia
如果你有什么好的想法或者提案,或者有什么问题,欢迎提交issue与我们交流!
关于作者
曹佳俊。网易智慧企业资深服务端开发工程师。中科院研究生毕业后加入网易,一直在网易云信负责IM服务器相关的开发工作。
更多技术干货,欢迎关注vx公众号“网易智慧企业技术+”。听网易CTO讲述前沿观察,看最有价值技术干货,学网易最新实践经验。网易智慧企业技术+,陪你从思考者成长为技术专家。