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
  • 目录
  • BUS系统的设计思路
  • 结构设计
  • 通信模式
  • 其他杂项
  • 最后

Was this helpful?

  1. 2014

关于BUS通信系统的一些思考(二)

接上文

目录

BUS系统的设计思路

虽然我很不愿意再设计一套BUS系统,但是现有的一些确实都没有特别符合我的口味的。所以还是尝试设计一个出来。

结构设计

简单来说,我希望BUS系统可以简单、高效、稳定。

节点标识

首先,在节点标识方面,类似ZeroMQ的用字符串来标识端点的做法我认为是不必要的。这点上可以参照前面两种的设计,但某些情况下32位作类型分割的可能会不够,所以可以使用64位标识。其实还有一个重要的原因是64位数字可以整个存放在CPU寄存器里,可以通过一个汇编指令进行比较操作,无论性能还是可以表示的节点个数都很足够。

节点间关系

第二点就是节点间关系,我觉得可以设计成树形结构,而不是像上面一样的代理节点+数据节点的结构。再考虑BUS通信一般会碰到的几种消息流转方式。

第一中情况是两个节点直连。那么节点里要记录直连的节点表。

节点A->节点B:
节点B->节点A:

第二种是需要通过公共父节点转发。

节点A->公共父节点:找不到直连信息,发给父节点
公共父节点->节点B:转发消息

这种情况又有分支,一种是接下来父节点通知两个子节点直接建立连接通道,另一种是不通知,每次都由父节点转发。如果子节点要建立直连通道则如下图所示:

节点A->公共父节点:找不到直连信息,发给父节点
公共父节点->节点B:转发消息
公共父节点->节点A:通知节点A直连到节点B
节点A->节点B:有直连信息,直接发送

按照之前真实系统的设计,节点分为三层的情况下(第一层是集群管理节点;第二层为物理机代理节点;第三层是业务进程节点)。第一层节点再转发的时候会通知第二层节点之间直连,第二层也会通知第三层节点之间互联,所以这里建议默认开启通知子节点互相直连的通知。

这里的第二个问题在于需要转发时的消息路由。在没有目标节点信息的情况下,当节点需要发送消息,是直接扔给父节点呢还是直接返回错误呢?这两种方式都各有利弊。一,没有目标节点就发给父节点,这种方式很简单,但是发送节点自身不能立刻感知是否发送成功,如果不成功需要等父节点通知,这一定是一个异步的过程;二,没有目标节点就直接返回错误,这种方式就会导致父节点在收到节点变更的通知后要把整个路由表下发,并且这期间必然存在时延。

前一种需要错误回执协议(假设数据不会丢失,那么正确转发的情况下不需要回包),而且这种错误回执是可选的。而后一种需要路由表同步协议,并且节点内至少要对节点ID做索引。

所以按照这样的设计思路,节点注册到父节点时要报告给父节点自己是否需要全局路由表,并且当父节点没有全局路由表时,子节点也不能有(这里不符合条件时最好注册出错)。另外父节点需要维护一个有全局路由表的子节点列表。

在线上实际运行的环境里还碰到一个问题,就是当有大量需要全局路由表的子节点注册时,只需要把最终结果广播一下就好了。不需要每次注册上来子节点都广播通知,这样会浪费大量通信流量。这里可以拿一个定时器保护一下。

当然还有第三种方式,就是没有公共父节点的情况。

节点A->A的父节点:找不到直连信息,发给父节点
A的父节点-->B的父节点:任何方式都可能
B的父节点->节点B:直接转给节点B

这种情况可以简单暴力了,每次有消息都转给父节点就好了。

通信模式

接下来时通信模式。每个节点都有一个唯一ID,但是通信模式可以有很多。其中最主流的就是网络Socket。BUS系统应该设计成容易拓展或者拆除某些通信模式,所以通信模式必须抽象出来。

另外一点,就是希望BUS系统里,节点的接收端只有一个。这样可以做到地址收敛,简化通信模型。

通信连接的建立

当有多种连接方式的时候,问题之一就是两个节点间应该以什么连接方式互联。

很多BUS系统的通道是读写分离和控制通道和数据通道分离的,这么设计的好处是简单、高效。带来的坏处就是,通道缓冲区浪费和可能某一个通道堵塞但是另一个通道正常。比如,控制通道堵塞,但是数据通道正常,这时候父节点可能回认为子节点下线;又或是控制通道正常,但是数据通道堵塞,这时候父节点回认为子节点正常,但这时候数据是发送不成功的。所以理想情况下通道还是应该只有一个,这样也就意味着要有控制协议和包头。

再拿之前的共享内存的例子来说,进程节点只有共享内存通道一种,但是代理节点有socket和共享内存通道两种。节点再注册时,怎么连接到父节点的通信通道和自身的接收通道是子节点决定的。如果我们把通道按优先级分化,假设网络Socket的优先级是3,Unix Socket的优先级是2,共享内存的优先级是1。那么子节点到父节点的通道优先级优先级高的一方连接低一方或者相等的一方一定是通的(当然是不出现故障的情况下)。

双方都是高优先级时:

节点B->父节点:共享内存,优先级1(通路)
父节点->节点A:共享内存,优先级1(通路)
节点B->节点A:共享内存,优先级1(通路)

一方是高优先级时:

节点B->父节点:共享内存,优先级1(通路)
父节点->节点A:网络Socket,优先级3(通路)
节点B->节点A:网络Socket,优先级3(通路)
节点B->父节点:网络Socket,优先级3(通路)
父节点->节点A:共享内存,优先级1(通路)
节点B->节点A:网络Socket,优先级3(如果节点A有网络Socket接收通道,通路)
节点B-->节点A:共享内存,优先级1(不一定通)

双方是低优先级时:

节点B->父节点:网络Socket,优先级3(通路)
父节点->节点A:网络Socket,优先级3(通路)
节点B->节点A:网络Socket,优先级3(通路)
节点B-->节点A:共享内存,优先级1(如果节点A有共享内存通道,不一定通)

所以子节点上报时还要通知兄弟节点接入的允许的通道类型。同样,如果没有允许的连接通道,则父节点也不需要再发兄弟节点直连通知。

网络Socket

网络Socket就很简单,因为本来就是面向连接的。加一下断线重联什么的就好了。然后linux下加个epoll、windows下加个iocp、其他什么系统价格kqueue什么的就好了。

Unix Socket

这玩意纯属IPC用得,操作和网络Socket一样,不能跨机器通信。

共享内存

使用共享内存最大的困难就是前面的收敛接收端点的需求。对于socket而言,因为一定是面向连接的一对一的所以比较好说。而共享内存要收敛接收端就必须实现至少多写单读的共享内存通道。

单读单写的共享内存通道

前面提及的BUS系统有些在内存和共享内存通道上是单读单写的模式。这种方式比较简单,对于一个用于循环队列的内存块。

                         ▼
|||||||||||###############||||||||||||||||||
          △

单读单写需要一个读游标一个写游标。写入操作时,先写入数据。

                         ▼
|||||||||||###############WWWWWWWWWW||||||||
          △

再移动写游标位置。当然写入前要先判断是否有足够的写入空间,并且不会抵达读游标的位置。

                                   ▼
|||||||||||#########################||||||||
          △

而读操作的时候,首先要判断读游标没有到写游标的位置,这样表示有数据。然后读入时,也是先读数据。

                                   ▼
|||||||||||RRRRRR###################||||||||
          △

再移动游标位置。

                                   ▼
|||||||||||||||||###################||||||||
                △

这种情况,在共享内存里操作甚至不需要原子操作,只要注意去掉寄存器缓存(C/C++里加volatile关键字)就可以了。不需要原子操作的原因在于,每个节点只会操作一个游标,并且一个节点只操作空白区域,另一个节点只操作数据区域。

单读多写的共享内存通道

单读多写的通道有助于把收消息节点收敛到一处。而单读多写意味着最大的难点在于单处读取和多处写入不冲突,并且当有节点出错的时候能保证赃数据被跳过。

为了解决上诉问题,可以把内存分为若干个内存块,然后每个内存块有一个信息头,记录了这个块内的标记位(对齐到4字节,便于编译优化)(包含写完标记位、是否起始node标记位和是否有后续节点的标记位)。

另外首节点还需要附加CRC校验码(使用自定义memcpy函数,copy的同时做校验并清0)、数据总长度和第一次尝试读取的时间。

最后整个内存块前端有一个整体head,记录了每个数据node的大小,数据node的个数,原子操作的读游标,写游标,统计信息和一些配置,比如读取时间容忍值(据Google一个文档说内存访问大约每毫秒可以到4MB,所以1毫秒的容忍值已经绰绰有余)。最后对齐到4KB(默认一个分页)用于以后拓展。

对于冲突问题 1. 读-读冲突:只考虑单点读,没有这个问题。 2. 读-写冲突:head有写完毕标记位,当写数据块准备完毕时才开始读。 3. 写-写冲突:写游标是原子操作,每个节点写缓冲区独立。 4. 写进程崩溃:会产生赃数据块,即写完标记永远是未写完。这时候可以利用上上面提到的第一次读取时间。如果是0,则取当前时间赋值,否则如果超出容忍值,就视为赃数据块。取时间可以使用clock函数(Linux下实测每次执行消耗约160ns),也可以用汇编直接提取CPU时钟。一般情况下系统应该在数百次读取无数据后休眠至少一个时间片的时间(Linux下一般最少有4ms),这时候写进程还没写完基本可以认为是出现赃数据。 5. 读进程崩溃:移动读游标是最后的操作,下次启动时可以继续,不会丢失数据。

2014/11/07 实际实现过程中发现共享通道时的读-写冲突和写-写冲突是不能完全避免的,另外多进程结构下的原子操作也很难保证强一致。所以在代码中增加了校验和自动重试。最终实现的代码中多进程发消息时,消息丢失率在三亿分之一左右。我觉得属于可接受范围,以后有时间可以抠细节去优化调整它。

多读多写的共享内存通道

这个可以作为以后拓展项。不是基本功能可以暂不提供。

共享内存消息通知

以上通道完成以后,共享内存消息还只能通过轮询得知是否有数据。对于每个线程只有一个节点的情况还比较好说,但是节点个数多了以后就有必要仿照epoll提供通知机制。常见的解决方案有: 1. fifo 2. eventfd/signalfd 3. socket 各有利弊吧。无论使用哪种,都需要注意的是通知的性能会远低于数据收发,所以每次通知需要尝试读完通道里的消息;另外要注意重复通知的问题。

消息通知也不是核心功能,早期也可以不提供。

其他杂项

序列化、反序列化和包头

为了减少内存消耗,需要在包头里对表示整形的数据做一些压缩处理。

比较上面几种压缩方式,Protobuf和Cap's Proto都支持不限长度的int,但是由于系统里的Cap's Proto的方案,只要不是0,就至少需要两字节。只有当数字大于7个字节时,protobuf的长度才会比Cap's Proto长;再考虑到bus通信中的消息一般不会很大,一个字节的protobuf的表示范围是[0, 128),涵盖了大部分消息包长度,两个字节是[0, 16K)基本涵盖所有消息包长度。所以这里推崇使用Protobuf的方案。

在包头方面,第一层包头一般在共享内存或者socket表示长度上,共享内存的数据都在同一台机器上,进程间系统架构一致,所以为了简单、高效,共享内存包头直接上裸内存数据即可。如果在socket上通信则可以选择用前面提到的varint。

最后

这些想法最终我会尝试一个实现放在github上,实现过程中可能会碰到一些问题会导致这些想法的细微变更。届时会同步更新到blog里。并且在单元测试和压力测试通过后会把地址更新到这里。

PreviousLLVM + Clang + Libcxx + Libcxxabi 工具链编译Next关于BUS通信系统的一些思考(一)

Last updated 6 years ago

Was this helpful?

另一个问题是节点注册消息的时延问题,如果两个相同ID的节点出现在两个不同的地方同时注册,那么当他们传递到第一个有全局路由表的节点时可能都是合法的。要解决这个问题方式之一是选举出一个总裁决节点,但是决策这个总裁决节点也存在时延,而如果要引入算法又过于复杂了。而且这个问题很容易在配置管理层解决,所以这里倾向于参考的做法,给每一个节点设置子节点范围,注册时节点ID和其子节点可变范围都必须在范围内。比如,节点上报ID为0x1234,子节点可变范围是24位,则0x000000 1234 000001到0x000000 1234 FFFFFF 都是可用的子节点ID(其中0x0000001234000000是这个节点的ID)。

比较成熟的解决方案有很多,比如的(varint)[]。每个字节的第一个bit用于表示后一个字节是不是这个varint,其他7个bit表示数据。

还有的。简化为int型压缩可以总结为:第一个字节作为bitmap表示后面8个非0字节的位置,然后后面依次跟非0字节的内容;如果第一个字节是0x00表示数字0;第一个字节是0xFF则第10个字节再按上面的流程走一次。

另外还有的。第一个字节描述类型,后面跟数据。

第二层包头用于区分控制指令和消息转发。这部分建议用来打解包。当然也可以选择或者或者。推荐Flatbuffers的原因是简单高效。但是要注意一点就是Flatbuffers依赖比较高版本的编译器,而使用Cap's Proto必须保证通行的机器之间的架构一致(这一点再服务端比较容易达成)。

第三部分一般来说这里直接就是数据区了。但是也可以增加一些拓展功能包头,比如可以拿或者做数据压缩,再或者拿、或者来做加解密。有点像的。

GitHub地址:

Written with .

Paxos
子网掩码
Protobuf
Cap's Proto
压缩方案
MessagePack
int压缩方案
Flatbuffers
Protobuf
Cap's Proto
MessagePack
zlib
Snappy
openssl
boringssl
Crypto++
IPv6
扩展包头
https://github.com/atframework/libatbus
StackEdit