zoukankan      html  css  js  c++  java
  • 开源|如何开发一个高性能的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模型,上述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,同时附上地址:

    https://github.com/netease-im...

    如果你有什么好的想法或者提案,或者有什么问题,欢迎提交issue与我们交流!

    关于作者

    曹佳俊。网易智慧企业资深服务端开发工程师。中科院研究生毕业后加入网易,一直在网易云信负责IM服务器相关的开发工作。

    作者:网易云信
    链接:https://segmentfault.com/a/1190000023210717
    来源:SegmentFault 思否
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    POJ 2175 Evacuation Plan 费用流 负圈定理
    POJ 2983 Is the Information Reliable? 差分约束
    codeforces 420B Online Meeting
    POJ 3181 Dollar Dayz DP
    POJ Ant Counting DP
    POJ 1742 Coins DP 01背包
    中国儒学史
    产品思维30讲
    Java多线程编程核心技术
    编写高质量代码:改善Java程序的151个建议
  • 原文地址:https://www.cnblogs.com/wangyiyunxin/p/13295071.html
Copyright © 2011-2022 走看看