OWenT's blog
  • Introduction
  • About Me
  • 2020
    • 近期对libatapp的一些优化调整(增加服务发现和连接管理,支持yaml等)
    • xresloader转表工具链增加了一些新功能(map,oneof支持,输出矩阵,基于模板引擎的加载代码生成等)
    • 在游戏服务器中使用分布式事务
    • libcopp接入C++20 Coroutine和一些过渡期的设计
    • libatbus 的大幅优化
    • nftables初体验
    • 容器配置开发环境小计
  • 2019
    • PALM Tree - 适合多核并发架构的B+树 - 论文阅读小记
    • 跨平台协程库 - libcopp 简介
    • C++20 Coroutine 性能测试 (附带和libcopp/libco/libgo/goroutine/linux ucontext对比)
    • 尝鲜Github Action
    • 一些xresloader(转表工具)的改进
    • protobuf、flatbuffer、msgpack 针对小数据包的简单对比
    • 协程框架(libcopp) 小幅优化
    • Excel转表工具(xresloader) 增加protobuf插件功能和集成 UnrealEngine 支持
    • Anna(支持任意扩展和超高性能的KV数据库系统)阅读笔记
    • C++20 Coroutine
    • libcopp merge boost.context 1.69.0
    • Google去中心化分布式系统论文三件套(Percolator、Spanner、F1)读后感
    • Rust玩具-企业微信机器人通用服务
  • 2018
    • 使用ELK辅助监控开发测试环境服务质量和问题定位
    • Webpack+vue+boostrap+ejs构建Web版GM工具
    • 2018年的新通用伪随机数算法(xoshiro / xoroshiro)的C++(head only)实现
    • Rust的第二次接触-写个小服务器程序
    • 理解和适配AEAD加密套件
    • atsf4g-co的进化:协程框架v2、对象路由系统和一些其他细节优化
    • 协程框架(libcopp)v2优化、自适应栈池和同类库的Benchmark对比
    • 可执行文件压缩
    • 初识Rust
    • 使用restructedtext编写xresloader文档
    • atframework的etcd模块化重构
    • C++的backtrace
  • 2017
    • ECDH椭圆双曲线(比DH快10倍的密钥交换)算法简介和封装
    • protobuf-net的动态Message实现
    • pbc的proto3接入
    • atgateway内置协议流程优化-加密、算法协商和ECDH
    • 整理一波软件源镜像同步工具+DevOps工具
    • Blog切换到Hugo
    • libcopp v2的第一波优化完成
    • libcopp(v2) vs goroutine性能测试
    • libcopp的线程安全、栈池和merge boost.context 1.64.0
    • GCC 7和LLVM+Clang+libc++abi 4.0的构建脚本
    • libatbus的几个藏得很深的bug
    • 用cmake交叉编译到iOS和Android
    • 开源项目得一些小维护
    • atapp的c binding和c#适配
    • 对象路由系统设计
    • 2016年总结
    • 近期的一个协程流程BUG
  • 2016
    • 重写了llvm+clang+libc++和libc++abi的构建脚本
    • atsf4g完整游戏工程示例
    • atframework基本框架已经完成
    • 游戏服务器的不停服更新
    • 对atbus的小数据包的优化
    • Android和IOS的TLS问题
    • pbc的一个陈年老BUG
    • boost.context-1.61版本的设计模型变化
    • 接入letsencrypt+全面启用HTTP/2
    • 理解Raft算法
    • libatbus基本功能及单元测试终于写完啦
    • 博客文章和文档迁移到gitbook
  • 2015
    • 博客文章和文档迁移到gitbook
    • 给客户端写得LRU缓存
    • 近期活动比较零散
    • 关于BUS通信系统的一些思考(三)
    • 针对Java JIT的优化(转表工具:xresloader)
    • libcopp更新 (merge boost 1.59 context)
    • 小记最近踩得两个C++坑
    • Redis全异步(HA)Driver设计稿
    • Vim常用命令
    • 关于firewalld和systemd的一些命令速记
    • Jenkins(hudson)插件记录
    • 我们的Lua类绑定机制
    • LLVM+Clang+Libcxx+Libcxxabi(3.6)工具链编译(完成自举编译)
    • 回顾2014
    • Android NDK undefined reference to ___tls_get_addr 错误
    • gitlab腾讯企业邮箱配置
  • 2014
    • 回顾2013
    • C++11动态模板参数和type_traits
    • C++又一坑:动态链接库中的全局变量
    • tolua++内存释放坑
    • [转]类似github的框架
    • Lua性能分析
    • 集成Qt Webkit 到cocos2d-x
    • Gitlab环境搭建小计
    • 近期研究VPN的一些记录(OpenVPN,pptp,l2tp)
    • LLVM + Clang + Libcxx + Libcxxabi 工具链编译
    • 关于BUS通信系统的一些思考(二)
    • 关于BUS通信系统的一些思考(一)
    • [libiniloader] Project
    • 记录一些在线编辑器
    • [WP Code Highlight.js] Project
    • 再议 C++ 11 Lambda表达式
    • 基于Chrome插件的开发工具链
    • [ACM] HDU 1006 解题报告
    • Linux 编译安装 GCC 4.9
    • 又碰到了这个解谜游戏,顺带记下地址
    • 简单C++单元测试框架(支持一键切到GTest或Boost.Test)
    • 捣鼓一个协程库
  • 2013
    • std和boost的function与bind实现剖析
    • 不知道是哪一年的腾讯马拉松题目 照片评级 解题报告
    • Lua 挺好用的样子
    • VC和GCC成员函数指针实现的研究(三)
    • VC和GCC成员函数指针实现的研究(二)
    • VC和GCC内成员函数指针实现的研究(一)
    • 一个C++关于成员变量偏移地址的小Trick
    • ptmalloc,tcmalloc和jemalloc内存分配策略研究
    • POJ 2192 Zipper HDU 2059 龟兔赛跑
    • 从Javascript到Typescript到Node.js
    • 网络编程小结
    • 试试Boost.Asio
    • Lnmp yum 安装脚本 (for CentOS)
    • ARM 交叉编译环境搭建
    • Linux 编译安装 GCC 4.8
    • [记录]虚拟硬盘的压缩|磁盘写零
  • 2012
    • Boost.Spirit 初体验
    • “C++的90个坑”-阅读笔记
    • AC自动机
    • C++ 标准过渡期
    • 程序员修炼之道 -- 阅读笔记
    • [转载]狼与哈士奇
    • C++ 新特性学习(八) — 原子操作和多线程库[多工内存模型]
    • C++ 新特性学习(七) — 右值引用
    • 理解Protobuf的数据编码规则
    • 忆往昔ECUST的ACM时代
    • Linux编译安装GCC 4.7
    • JSON显示库 -- showJson (Javascript)
    • C++ 新特性学习(六) — 新的字符串编码和伪随机数
    • C++ 新特性学习(五) — 引用包装、元编程的类型属性和计算函数对象返回类型
    • C++ 新特性学习(四) — Bind和Function
  • 2011
    • C++ 新特性学习(三) — Regex库
    • C++ 新特性学习(二) -- Array、Tuple和Hash库
    • C++ 新特性学习(一) -- 概述+智能指针(smart_ptr)
    • Linux 和 Windows PowerShell 常用工具/命令 记录
    • 非常帅气的Linq to sql
    • 2011 Google Code Jam 小记
    • C++总是很神奇
    • 大学生创新项目[国家级]经费使用记录
    • 常用官方文档整理
    • 我们学校的IPV6很不错嘛
  • 2010
    • 线段树相关问题 (引用 PKU POJ题目) 整理
    • 2010 ACM 赛前笔记
    • POJ PKU 2596 Dice Stacking 解题报告
    • POJ PKU 3631 Cuckoo Hashing 解题报告
    • POJ PKU 1065 Wooden Sticks 3636 Nested Dolls 解题报告
    • HDU 3336 Count the string 解题报告
    • Hash模板 个人模板
    • ZOJ 3309 Search New Posts 解题报告
    • POJ PKU Let's Go to the Movies 解题报告
    • 注册表常用键值意义
    • PKU POJ 1724 ROADS 解题报告
    • 《神奇古今秘方集锦》&《民间秘术大全》
    • PKU POJ 1720 SQUARES 解题报告
    • POJ PKU 2155 Matrix 解题报告
    • PKU POJ 1141 Brackets Sequence 解题报告
    • PKU POJ 2728 Desert King 解题报告
    • PKU POJ 2976 Dropping tests 解题报告
    • PKU POJ 3757 Simple Distributed storage system 解题报告
    • GCD Determinant 解题报告
    • Southeastern European 2008 Sky Code 解题报告
    • HDU HDOJ 3400 Line belt 解题报告
    • 线性筛法求质数(素数)表 及其原理
    • HDU HDOJ 3398 String 解题报告
    • 树状数组模块(个人模板)
    • 浙江理工 省赛总结 team62 By OWenT of Coeus
    • POJ PKU 3659 Cell Phone Network 解题报告
    • USACO 2008 March Gold Cow Jogging 解题报告
    • C#格式化输出(记录)
    • 参加有道难题笔记
    • POJ PKU 2446 Chessboard 解题报告
    • POJ PKU 1986 Distance Queries 解题报告
    • 计算几何算法概览[转载]
    • 关于差分约束(转载)
    • POJ PKU 2826 An Easy Problem?! 解题报告
    • 数论模板(个人模板)
    • 简易四则运算(ACM个人模板)
    • Catalan 数
    • The 35th ACM/ICPC Asia Regional Tianjin Site —— Online Contest 1009 Convex 解题报告
    • JQuery扩展插件--提示信息
    • ACM 计算几何 个人模板
    • 解析网站字符串型参数 Javascript QueryString 操作 TQueryString类
    • POJ PKU 1474 Video Surveillance 解题报告
  • 2009
    • 模式匹配(kmp)个人模板
    • 并查集 模板
    • POJ 3267 The Cow Lexicon 解题报告
    • C/C++语言常用排序算法
    • POJ 2606 Rabbit hunt 2780 Linearity 1118 Lining Up 解题报告
    • 打造最快的Hash表(转) [以暴雪的游戏的Hash为例]
    • ECUST 09年 校赛个人赛第六,七场总结
    • ECUST 09年 校赛个人赛第三场部分解题报告(A,D,F,I)
    • 牛顿迭代解方程 ax^3+bX^2+cx+d=0
    • 09年8月9日 ECUST ACM 练习赛总结
    • 连接最多点直线 (OWenT 个人模板)
    • 点到直线距离 和 线段间最短距离 (OWenT 模板)
    • ECUST 09年 校赛个人训练赛第五场总结
    • ECUST 09年 校赛个人赛第八场(最后一场)总结
    • 09年8月14日 ECUST ACM 练习赛总结
    • 矩阵相关 (增强中)
    • Prime最小生成树(个人模板)
    • 最长单调子序列 复杂度nlog(n)
    • POJ PKU 2549 Sumsets 解题报告
    • POJ PKU 3277 City Horizon 解题报告
    • 我的ACM生涯
    • POJ PKU 2528 Mayor's posters 解题报告
    • POJ PKU 2378 Tree Cutting 解题报告
    • POJ PKU 1990 MooFest 解题报告
Powered by GitBook
On this page
  • 前言
  • Cluster
  • Sentinel
  • 要解决的问题
  • Redis-Cluster适配设计
  • Redis-Cluster环境条件
  • redis-rb-cluster 分析
  • 设计要点
  • 定时器
  • 连接和重连等待
  • 设计总结
  • Sentinel适配设计
  • 设计思路
  • 更进一步
  • 设计总结
  • 集群健康报告
  • 写在最后

Was this helpful?

  1. 2015

Redis全异步(HA)Driver设计稿

Previous小记最近踩得两个C++坑NextVim常用命令

Last updated 6 years ago

Was this helpful?

前言

现在Redis的集群功能已经Release。但是并没有一个官方直接提供的高可用性的API可以使用。有的只有解决方案,Sentinel和Cluster。所以有必要自己设计一套高可用的Driver层以供业务使用。

Cluster

Redis 3.X已经release,这个版本提供了一个重要的功能,那就是集群(Cluster)。但是虽然redis的集群功能已经提供,但是目前还没有一个非常成熟的操作Redis集群的连接driver。而在我们游戏项目中,会需要一个稳健的driver来屏蔽底层细节,并且必须要使用全异步接口。

在开始前,我在某个论坛里看到 对redis的cluster支持得不错,就先大概看了一下 的实现,发现了几个问题。

  1. 第一次连接集群节点采用的是同步操作,不过只有第一次是这样,问题也不大。

  2. 执行redisAsyncConnect函数以后并没有关心是否连接成功,也就是说,即便连接失败了,连接仍然保存到了连接池中。

  3. 没有关心断线的情况,也就是说,如果连接断开了,既不会重连,也不可能发送成功。

  4. 没有支持slaver,也没有关心断线问题,如果master挂了,就没有然后了。

  5. retry机制有问题,我看到的逻辑大概是这样:如果发现CLUSTERDOWN消息,则重试,并且是立即重试。并且重试的时候仍然用了原来的连接(还是没支持slaver导致的)。一般情况下,集群出现故障的时候必然会有一定的恢复时间,并且按照redis的规则,会把slaver节点提升会master,立即用原来的master连接重试毫无意义。

  6. 每次检测到MOVE或者ASK消息都创建了一个新redis context,其实可以重用已有的。

  7. 它里面使用了std::map来保存slot到redis context的映射关系,key是一个slot区间。搜索算法是找到第一个最小区间满足要求的slot。再加上上一条问题,导致redis如果在做扩容,原先已经失效的索引仍然存在,并且之后对这个key一定每次都会收到MOVED消息,然后每次都重新创建redis context和新连接,并覆盖原先的连接,并且覆盖先没有释放前一个redis context,造成严重的资源泄露。

总而言之,实现得很渣渣,所以还是需要自己维护一个连接redis cluster的工具。

Sentinel

有一些服务,其实不需要Cluster那么复杂的操作。比如积分排行榜,直接主+备就够了。而要做到这方面数据安全,还是需要Sentinel来监控。

要解决的问题

  1. 支持自动断线重连;

  2. Redis Cluster不支持多个DB分区(一定要用DB0)并且最好对于不同类型业务可以部署在不同集群里,以便减少不同模块之间的影响,所以也需要提供多集群功能(类似SQL中的多个数据库);

  3. 要支持对Cluster的监控和统计;

  4. 要支持多个通道(Channel)的设计(类似Redis的多个DB库)要实现不同通道之间完全隔离。

Redis-Cluster适配设计

Redis-Cluster环境条件

  1. 因为Slot只有16384(16K)个,即便把所有的Slot按Index放在数组里缓存也不会消耗太大的内存,并且这样查找是O(1)的

  2. 要内部完成 指令跳转和重试 - 需要自动处理MOVED命令、ASK命令和TRYAGAIN错误

    MOVED 命令出现在扩容和故障迁移时。这时候要永久更换Slot对应的连接。 ASK 表示临时跳转。仅仅这一次消息使用新的连接。比如正在扩容的过程中(某个slot由A转向了B),数据可能还没全部转移完,那么访问A节点的这个slot时,可能找不到数据,这时候ASK跳转可以把目标指向当前有数据的节点(B)。 ASK跳转还有一个特别的步骤是客户端先要发送一个ASKING命令,然后再重发这次的命令,不然处于导入转态的槽会被拒绝访问 在重新分片过程中的多个键值操作核能导致TRYAGAIN错误,这时候需要尝试重发命令

  1. 它实现了按需连接,就是说当第一次连接某个server时,仅仅会拉取Slot和服务器的关系列表,并没有真正建立连接,而是等第一次需要这个连接的时候才建立连接。

  2. 限制了最大重定向次数,防止重定向死循环

  3. 按需连接的时候,如果出现超时、连接被拒绝、连接失败的错误0.1秒后重试

  4. 如果按slot查找连接没找到,则会返回一个随机的连接,然后根据ASK或者MOVED跳转来处理

  5. ASK和MOVED跳转都会启动拉取所有Slot信息的行为,来更新Slot缓存

  6. 拥有最大连接数限制,如果新建连接的时候超出最大连接数,随机关闭一个连接

执行Redis指令流程:

设计要点

  • 首先是整个操作过程可以直接使用hiredis,使用起来比较简单,全程单线程,避免不必要的加解锁和逻辑复杂度;

  • 然后,要增加集群Server的概念,以便用一套接口操纵多个集群。这样的话,所有的数据结构中不能出现单例;

  • 直接利用hiredis的adapter来做事件绑定,方便工具迁移;

  • 同样,是用主循环就需要设定最大循环次数,并且失败次数过高时休眠一段时间,用以避免逻辑死循环;

  • 使用按需建立连接,全局只保存Slot-服务器地址缓存和服务器地址-连接池缓存;

  • hiredis里大量使用了malloc,所以还是必须上jemalloc或者tcmalloc才比较靠谱;

最后有一个要特别注意的是丢包和超时。

  1. 丢包问题:虽然说TCP连接能保证数据包的顺序和并且自带网络包重发,但是在连接断开的时候仍然会出现丢包的情况。

  2. 超时问题:hiredis的异步API里没有超时的判定,但是因为TCP包底层的重传机制,超时只有一种可能,那就是连接断开。然后要么是上面提到的情况,没有发送成功,要么是回包丢失。

无论上诉哪种情况,都会导致连接异常。根据对hiredis源代码的分析,(除了subscribe和unsubscribe命令外)这时候hiredis一定会回调所有没有完成的callback,然后响应disconnect事件,并且这时候redisReply *reply=NULL。

subscribe和unsubscribe命令外,订阅命令的回调是个字典

subscribe命令的回调会在每次收到消息的时候都调用

上层应用逻辑需要自己有一个超时机制和对超时后又收到回包的容错机制。

定时器

由于异步API不允许sleep操作,所以所有延迟操作都应该在定时器回调中执行。然而为了保证像hiredis一样支持多种binding机制,只能由使用方来创建和设置定时器回调,并在回调中调用提供的proc方法。

为了简化设计,我们定义以下规则:

  1. 所有定时器的间隔一致(定时器队列可以直接链表实现);

  2. 每个Channel有自己的定时器,并且定时器接口调用至少一次以后才会开启带定时器的功能(例如:sleep);

  3. 假设系统中的Channel数量不会很多,这样定时器就不会很多,性能开销比较小。

连接和重连等待

异步操作的另一个问题是连接和重连的时候的等待问题,因为在连接完成期间,可能会收到新的命令请求。hiredis的做法是每次来了一个请求以后就放到缓冲区里,并且在Context可写时立即写出。

我们这里可以直接利用它的这个机制。但是在重新拉取并建立Slot缓存的时候,没有Redis连接可以用于保存,命令,所以可以在Channel里使用一个链表保存更新完Slot缓存后的执行命令集。

然后额外需要做的就是支持断线后的重连功能了。

设计总结

简单地说,就是需要在hiredis上包一层,来完成对Cluster中的内部操作。实现的过程中会导致多一次malloc和多一次sds复制操作。流程图如下:

Sentinel适配设计

设计思路

Sentinel比较简单,大体上和Cluster一致,有几个不一样的地方如下:

  1. 第一次连接的是Sentinel节点而不是数据节点;

  2. 连接的Channel要附带,并且要通过SENTINEL sentinels 拉取并连接Sentinel;

  3. 连接完毕后需要先通过SENTINEL master 拉取master数据;

  4. 发送失败的重试流程是重新走SENTINEL master 拉取master;

  5. SENTINEL master 失败和Sentinel连接出现问题需要先执行2,再重试;

  6. 由于Sentinel拉取master地址之前,还不能建立到master的连接,所以Channel里要保存需要发送的命令Sds队列,并等待连接成功后发送。如果Sential连接失败或者拉取不到服务器地址,要执行回调并出错。

更进一步

  1. 更好的实现方式是可以订阅Sentinel,从而更快地响应故障转移;

  2. 读操作在配置允许的情况下可以走slave,以减小master压力;

  3. Sentinel的连接可以共享。(这里貌似会涉及到单例管理器)。

不过这些功能可选,以后可以有时间再加。

设计总结

流程图如下:

集群健康报告

对于Cluster而言,使用CLUSTER * 命令就可以完成这些功能,并且总是随机取发送目标。

对于Sentinel而言,Sentinel提供了简单地方式获取master的状态。要获取更详细的信息,可能得用比较麻烦的方法,一次访问多个master和slaves(这里没仔细研究,以后再看)

另外Driver层可以提供一些事件给上层用于统计重连、断线等情况。

写在最后

整体上最重要的思路就是用主循环来简化逻辑复杂度。而主循环增加的那部分CPU消耗几乎可以忽略,不过这种异步传参方式对应着大量的malloc操作,以后看需要可以优化成c++ allocator的机制,这样就能支持自定义内存池。

流程图中建立连接后的命令发送流程比较特别,因为hiredis的异步发送接口是向缓冲区中添加数据,并且等fd可写后才实际执行,所以可以不等connect完成就直接调用发送接口。

另外Cluster更新服务器连接池的方式比较讨巧,既能避免频繁更新地址池,又可以及时更新Slot缓存,需要注意一下。

尽量减少类似MSET的多key指令使用(包括涉及多个key的脚本),因为可能不同的Key会分布在不同的Slot上。在使用Cluster时,涉及多个key的指令,这些key必须拥有相同的hash值或hash tag,详见

基本操作参考这是redis作者写得ruby版本支持redis的cluster的driver

分析

redis作者建议对cluster的支持仿照 进行,那么我们来对这个库的实现流程做一个简单的分析。

首先, 和我们上面想得一样,是缓存了16384个槽。

写得确实比较漂亮,简单清晰。连我这种完全不懂ruby的人都能看懂。但是他的实现是全同步的操作。我们这里要求全异步操作时就会更加麻烦一点。

其次所有操作都要转为异步模式,因为不能预估操作的流程,所以还必须增加一层调用包装,用来包裹指令数据,这点和的Command一样;

使用redisFormatSdsCommandArgv和redisAsyncFormattedCommand来保存命令和执行命令(和一致)执行的命令保存为Sds后放到Command的数据包装里;

为保证简单,我们的driver也可以使用主循环的模式(和 一样)。因为出现异常的情况会是少数,而正常的情况下,主循环只会执行一个循环;

第一次连上以后应该像发送一次拉取所有Slot信息的操作;

某些命令和一样,随机选取发送目标。

以上思路我会先在 中进行实现和测试,然后用于生产环境。

Written with .

cpp-hiredis-cluster
cpp-hiredis-cluster
http://redis.io/topics/cluster-tutorial#migrating-to-redis-cluster
redis-rb-cluster
redis-rb-cluster
redis-rb-cluster
redis-rb-cluster
redis-rb-cluster
cpp-hiredis-cluster
cpp-hiredis-cluster
redis-rb-cluster
redis-rb-cluster
redis-rb-cluster
https://github.com/owt5008137/hiredis-happ
StackEdit
redis-rb-cluster.png
redis-ha-cluster.png
redis-ha-sentinel.png