开源|如何开发一个高性能的redis cluster proxy?

背景

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,咱们还能够作不少额外的事情:
  • 好比能够在proxy层作分片逻辑,这样当单集群的redis cluster不知足需求(内存/QPS)时,就能够经过proxy层实现透明的同时访问多个redis cluster集群。
  • 再好比能够在proxy层作双写逻辑,这样在迁移或者拆分缓存类型的redis时,就不须要使用redis-migrate-tool之类的工具进行全量迁移,而只须要按需双写,便可完成迁移。
  • 此外由于proxy实现了redis协议,所以能够在proxy层利用其它存储介质实现redis相关命令,从而能够模拟成redis对外服务。一个典型的场景就是冷热分离存储。

功能

介于上述各类缘由和需求,咱们基于netty开发了camellia-redis-proxy这样一个中间件,支持以下特性:
  • 支持设置密码
  • 支持代理到普通redis,也支持代理到redis cluster
  • 支持配置自定义的分片逻辑(能够代理到多个redis/redis-cluster集群)
  • 支持配置自定义的双写逻辑(服务器会识别命令的读写属性,配置双写以后写命令会同时发往多个后端)
  • 支持外部插件,从而能够复用协议解析模块(当前提供了camellia-redis-proxy-hbase插件,实现了zset命令的冷热分离存储)
  • 支持在线变动配置(需引入camellia-dashboard)
  • 支持多个业务逻辑共享一套proxy集群,如:A业务配置转发规则1,B业务配置转发规则2(须要在创建redis链接时经过client命令设置业务类型)
  • 对外提供了一个spring-boot-starter,3行代码便可快速搭建一个proxy集群

如何提高性能

客户端向camellia-redis-proxy发起一条请求,到收到请求回包的过程当中,依次经历了以下过程
  • 上行协议解析(IO读写)
  • 协议转发规则匹配(内存计算)
  • 请求转发(IO读写)
  • 后端redis回包解包(IO读写)
  • 后端redis回包下发到客户端(IO读写)
能够看到做为一个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也充分利用了这样的特性,主要包括两方面:
  • 上行协议解析时尽量的一次性解析多个命令,从而进行规则转发时能够批量进行
  • 日后端redis节点进行转发时尽量的批量提交,这里除了对来自同一个客户端链接的命令进行聚合,还能够对来自不一样客户端链接,但转发目标redis相同时,也能够进行命令聚合
固然,全部这些批量和聚合的操做都须要保证请求和响应的一一对应。

TCP分包和大包处理

不论是上行协议解析,仍是来自后端redis的回包,特别是大包的场景,在碰到TCP分包时,利用合适的checkpoint的机制能够有效减小重复解包的次数,提高性能。

异常处理和异常日志合并

若是没有有效的处理各类异常,在异常发生时也会致使服务器性能迅速降低。想象一个场景,咱们配置了90%的流量转发给A集群,10%的流量转发到B集群,若是B集群发生了宕机,咱们指望的是来自客户端的90%的请求正常执行,10%的请求失败,可是实际上却可能远远超过10%的请求都失败了,缘由是多方面的:
  • 后端操做系统层面的忽然宕机proxy层可能没法当即感知(没有收到TCP fin包),致使大量请求在等待回包,虽然proxy层没有阻塞,可是客户端表现为请求超时
  • proxy在尝试转发请求到B集群时,针对B集群的从新链接请求可能拖慢整个流程
  • 宕机致使的大量异常日志可能会引发服务器性能降低(这是一个容易忽视的地方)
  • pipeline提交上来的请求,99个请求指向A集群,1个请求指向B集群,可是因为B集群的不可用,致使指向B集群的请求迟迟不回包或者异常响应过慢,客户端的最终表现是100个请求所有失败了
camellia-redis-proxy在处理上述问题时,采起了以下策略:
  • 设置对异常后端节点的快速失败降级策略,避免拖慢整个服务
  • 异常日志统一管理,合并输出,在不丢失异常信息的状况下,减小异常日志对服务器性能的影响
  • 增长对后端redis的定时探活探测,避免宕机没法当即感知致使业务长时间异常

部署架构

proxy做为无状态的服务,能够作到水平扩展,为了服务的高可用,也至少要部署两个以上的proxy节点,对于客户端来讲,想要像使用单节点redis同样访问proxy,能够在proxy层以前设置一个LVS代理服务,此时,部署架构图以下:
固然,还有另一个方案,能够将proxy节点注册到zk/Eureka/Consul等注册中心,客户端经过拉取和监听proxy的列表,而后再向访问单节点redis同样访问每一个proxy便可。以Jedis为例,仅需将JedisPool替换为封装了注册发现逻辑的RedisProxyJedisPool,便可像访问普通redis同样使用proxy了,此时,部署架构图以下:

应用场景

  • 须要从redis迁移到redis-cluster,可是客户端代码不方便修改
  • 客户端直连redis-cluster,致使cluster服务器链接过多,致使服务器性能降低
  • 单个redis/redis-cluster集群容量/QPS不知足业务需求,使用camellia-redis-proxy的分片功能
  • 缓存类redis/redis-cluster集群拆分迁移,使用camellia-redis-proxy的双写功能
  • 使用双写功能进行redis/redis-cluster的灾备
  • 混合使用分片和双写功能的一些业务场景
  • 基于camellia-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,地址以下: github.com/netease-im/…
若是你有什么好的想法或者提案,或者有什么问题,欢迎提交issue与咱们交流!

关于做者
曹佳俊。网易智慧企业资深服务端开发工程师。中科院研究生毕业后加入网易,一直在网易云信负责IM服务器相关的开发工做。
相关文章
相关标签/搜索