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
  • 写工具,分析问题
  • 第一个BUG: 发生频率-三亿分之一
  • 第二个BUG: 发生频率-十亿分之一到百亿分之一
  • 第一个问题:并没有出现超时、校验码错误、长度校验错误、写重试和写错误的情况下,单单出现了和节点错误计数。(十亿分之一)
  • 第二个问题:更诡异的数据被复写(百亿分之一)
  • 顺带一点点小优化
  • 新的稳定性测试

Was this helpful?

  1. 2017

libatbus的几个藏得很深的bug

PreviousGCC 7和LLVM+Clang+libc++abi 4.0的构建脚本Next用cmake交叉编译到iOS和Android

Last updated 6 years ago

Was this helpful?

在写这篇文章前,我突然想到以前流行了一段时间的服务器面试题:当一个BUG只有几百万分之一的概率会出现,怎么办?这个问题在这个BUG里只是毛毛雨而已,因为这次的BUG的出现概率是夸张的三亿分之一。

我在好几年前自己开发了一个用于服务器进程间通信的框架,,以前用在了客户端的模块隔离的通信心痛中。并且现在开始正式用于服务器。但是前两天看了一下服务器的日志,发现了比较奇怪的-202错误。

这个问题本来是已知的,在当时通信系统设计并且做测试的时候就发现了这个问题,详见。后来经过几次设计细节的调整,具体的实现可这篇文章有一些变化,但是主要原理是一样的。

因为在benchmark时BUG的发生概率在大约三亿分之一,并且在单进程多线程模式下没有发现过这个错误。并且后来在增加了自动重试之后很长时间都没出现过问题,所以当时认为可能是跨进程原子操作的时候可能内存屏障有些问题导致的。也没太在意,毕竟服务器本身也有一层超时保护,而且还得保持在高压力上才会发生,三亿分之一的发送错误实在微不足道。

但是我们服务器程序开发阶段,压力不可能高,又出现这个问题,说明我之前的猜测是不对的(果然猜测是大忌啊)。虽然发生频率很低,但是通信服务是一个非常基础的东西,这就需要关注下保证更高的可靠性了。

写工具,分析问题

为了更深入地了解到底是什么情况,因为发生概率十分地低,所以很难抓到现场,第一步只能是写个小工具导出共享内存通道的统计数据,然后发现有一些奇怪的计数。

summary:
channel node size: 128
channel node count: 123331
channel using memory size: 16777112
channel available node number: 123330

configure:
send timeout(ms): 4
protect memory size(Bytes): 123264
protect node number: 963
write retry times: 4

read&write:
first waiting time: 0
read index: 51896
write index: 51896
operation sequence: 1064070

stat:
bad block count: 22
bad node count: 3110
timeout block count: 18

read&write:
first waiting time: 0
read index: 51896
write index: 51896
operation sequence: 1064070

在几个月的运行中,这种低负载的情况下,竟然出现了22次错误数据和18次timeout。这显然不太可能于是需要进一步地完善工具提供更多的信息。收集错误的数据。

第一个BUG: 发生频率-三亿分之一

第一个BUG,其实是review了一遍代码发现的。还是比较2B的BUG。

原来的逻辑里有一段功能是为了防止某些发送端进程在写出数据时崩溃,为了在这种情况下接收端需要能够自动修复并跳过错误数据。但是接收端并不知道写出端是崩溃了还是正在写出,所以有一个遇到未完成写出的节点时的超时判定,在超时后认为出现错误而强制跳过。原来有BUG的代码如下:

// 容错处理 -- 未写入完成
if (!check_flag(node_head->flag, MF_WRITEN)) {
  uint64_t cnow = (uint64_t)clock() * (CLOCKS_PER_SEC / 1000); // 转换到毫秒

  // 初次读取
  if (!channel->first_failed_writing_time) {
    channel->first_failed_writing_time = cnow;
    ret = ret ? ret : EN_ATBUS_ERR_NO_DATA;
    break;
  }

  uint64_t cd = cnow > channel->first_failed_writing_time ? cnow - channel->first_failed_writing_time
    : channel->first_failed_writing_time - cnow;
  // 写入超时
  if (channel->first_failed_writing_time && cd > channel->block_timeout_count) {
    read_begin_cur = mem_next_index(channel, read_begin_cur, 1);
    ++channel->block_bad_count;
    ++channel->node_bad_count;
    ++channel->block_timeout_count;

    channel->first_failed_writing_time = 0;
    continue;
  }

  // 未到超时时间
  ret = ret ? ret : EN_ATBUS_ERR_NO_DATA;
  break;
}

这里面认为first_failed_writing_time为0则是第一次出错,然后下一次recv的时候如果未超时则认为是写出端数据没写完而返回无数据,但是超时后返回错误并且跳过错误数据块。

这里的问题就出在时间计算上,那里不应该是乘,而应该是除。在linux里,CLOCKS_PER_SEC / 1000的值是1000,那么cnow变成了1000倍的clock()。然后还有个问题是超时时间不是channel->block_timeout_count,而且应该是,channel->conf.conf_send_timeout_ms。

所以在这两个BUG的夹击下,只要是连续第二次进入判定超时阶段,十有八九会被判定为超时。之所以发生概率三亿分之一,是以为其他情况下在第二次执行尝试读取数据时,写出端都把数据写完了,那么MF_WRITEN就会被标记。所以这里必须是两次连续读期间写出端还没写完才行,所以发生频率才很低。

所以修复完以后,代码如下:

// 容错处理 -- 未写入完成
if (!check_flag(node_head->flag, MF_WRITEN)) {
  uint64_t cnow = (uint64_t)(clock() / (CLOCKS_PER_SEC / 1000)); // 转换到毫秒

  // 初次读取
  if (!channel->first_failed_writing_time) {
    channel->first_failed_writing_time = cnow;
    ret = ret ? ret : EN_ATBUS_ERR_NO_DATA;
    break;
  }

  uint64_t cd = cnow > channel->first_failed_writing_time ? cnow - channel->first_failed_writing_time
    : channel->first_failed_writing_time - cnow;
  // 写入超时
  if (channel->first_failed_writing_time && cd > channel->conf.conf_send_timeout_ms) {
    read_begin_cur = mem_next_index(channel, read_begin_cur, 1);
    ++channel->block_bad_count;
    ++channel->node_bad_count;
    ++channel->block_timeout_count;

    channel->first_failed_writing_time = 0;
    continue;
  }

  // 未到超时时间
  ret = ret ? ret : EN_ATBUS_ERR_NO_DATA;
  break;
}

经过上面的修复,继续跑debug版本的benchmark,连续2小时都没再出现任何错误,但是切到RelWithDebInfo(-O2 -g -ggdb)后大约平均十亿分之一的概率出现了错误记录。

第二个BUG: 发生频率-十亿分之一到百亿分之一

这次已经没有这种很傻的BUG了,并且错误出现率进一步降低。这时候发现之前的错误信息转储不太能展示错误到底出现在哪个流程里。于是乎review完写出和接收端代码没有发现问题后我就给通道数据导出增加了更多的信息。这个信息后来证明很有价值。当时查出问题所在的日志我没保存,但是在我区分了各个状态的错误码后我发现了两个问题。

第一个问题:并没有出现超时、校验码错误、长度校验错误、写重试和写错误的情况下,单单出现了和节点错误计数。(十亿分之一)

这个而只有在一个流程下没有其他计数只有节点错误计数。那就是如下位置:

read_end_cur = read_begin_cur; // read_begin_cur是通过channel->atomic_read_cur.load()获取的

if (read_begin_cur == write_cur) {
  ret = ret ? ret : EN_ATBUS_ERR_NO_DATA;
  break;
}

volatile mem_node_head *node_head = mem_get_node_head(channel, read_begin_cur, NULL, NULL);
// 容错处理 -- 不是起始节点
if (!check_flag(node_head->flag, MF_START_NODE)) {
  read_begin_cur = mem_next_index(channel, read_begin_cur, 1);
  node_head->flag = 0;

  ++channel->read_bad_node_count;
  continue;
}

这里本意是其他位置发现了错误,跳过起始节点,然后在主循环里计数并跳过所有的错误数据节点。但是莫名奇妙在没有检测到错误起始节点的情况下直接进了这个流程,就比较诡异了。

后来我就再分析了发送端代码,结果发现了问题所在。

// 查找可用数据节点阶段
while (true) {
  read_cur = channel->atomic_read_cur.load();
  // std::atomic_thread_fence(std::memory_order_seq_cst);

  // 要留下一个node做tail, 所以多减1
  size_t available_node = mem_get_available_node_count(channel, read_cur, write_cur);
  if (node_count > available_node) {
    return EN_ATBUS_ERR_BUFF_LIMIT;
  }

  // 新的尾部node游标
  new_write_cur = mem_next_index(channel, write_cur, node_count);

  // CAS
  bool f = channel->atomic_write_cur.compare_exchange_weak(write_cur, new_write_cur);

  if (f) break;

  // 发现冲突原子操作失败则重试
}

// 数据缓冲区操作 - 初始化
void *buffer_start = NULL;
size_t buffer_len = 0;
mem_block_head *block_head = mem_get_block_head(channel, write_cur, &buffer_start, &buffer_len);
memset(block_head, 0x00, sizeof(mem_block_head));

// 数据缓冲区操作 - 要写入的节点
{
  block_head->buffer_size = 0;

  volatile mem_node_head *first_node_head = mem_get_node_head(channel, write_cur, NULL, NULL);
  first_node_head->flag = set_flag(0, MF_START_NODE);
  first_node_head->operation_seq = opr_seq;

  for (size_t i = mem_next_index(channel, write_cur, 1); i != new_write_cur; i = mem_next_index(channel, i, 1)) {
    volatile mem_node_head *this_node_head = mem_get_node_head(channel, i, NULL, NULL);
    assert((char *)this_node_head < (char *)channel + channel->area_data_offset);

    // 写数据node出现冲突
    // 写超时会导致this_node_head还是之前版本的数据,并不会被清空。所以不再恢复 operation_seq
    // if (this_node_head->operation_seq) {
    //     return EN_ATBUS_ERR_NODE_BAD_BLOCK_WSEQ_ID;
    // }

    this_node_head->flag = set_flag(0, MF_WRITEN);
    this_node_head->operation_seq = opr_seq;
  }
}
block_head->buffer_size = len;

上面的代码先通过原子CAS操作获取到可用的内存块,然后对节点头设置标记位,并且在第一个节点设为MF_START_NODE。问题就在这里,CAS操作是先进行的,然后第一个节点设为MF_START_NODE在后。虽然这两个操作的距离很近,可能只涉及几十条指令。但是这段时间内如果进程被切出,然后读进程读到了CAS之后的写游标,又发现第一个节点不是MF_START_NODE的时候,就被判为错误节点了,于是乎才丢失了数据。这个问题处理起来比较麻烦,因为不能破坏对真正错误数据的容错处理,还要能正确处理这种情况。

最终我分析了一下流程,在读取端获取了新的写游标位置后,可能写出端处于移动了atomic_write_cur,但是还没有写出MF_START_NODE的情况。所以情况列举如下:

  1. MF_START_NODE | MF_WRITEN: 数据块已写完

  2. MF_WRITEN: 节点容错

  3. MF_START_NODE: 是起始节点但是数据未写完(也可能是发送端在写出过程中崩溃)

  4. 空: 移动游标后尚未设置MF_START_NODE,这个出现概率非常低,但是也会出现。(也可能是发送端在写出过程中崩溃)

由于MF_START_NODE和空都是无法判定是没写完还是写出端崩溃的,所以都要走超时检测逻辑。

但是如果被判定超时并且写出端只写出了部分节点的的MF_WRITEN这时候剩下的节点的flag都会是空。

如果这些都通过超时机制判定,则最多可能等待消息长度*超时判定时长/节点长度,默认设置是最少2秒钟。

所以这里需要特别处理下,当进入超时流程后,所有非MF_START_NODE并且operation_seq相等的节点也应该视为错误。

注意上面这个流程只能在超时流程中进行,因为其他错误流程可能第一个数据块错误,但是紧接着的第二个数据块处于正在写出的状态而没有设置

MF_START_NODE和operation_seq。我们的operation_seq取值范围是uint32,所以max(uint32)节点长度(默认是500GB)以内的通道里operation_seq*不会重复

我们的数据通道不可能使用这么大的内存,所以加上operation_seq后能尽可能地消除空数据快的超时影响。

第二个问题:更诡异的数据被复写(百亿分之一)

重点日志如下:

shm_recv error, ret code: -202. start: 101398, end: 101403
Summary:
    channel node size: 128
    channel node count: 123331
    channel using memory size: 16777112
    channel available node number: 12578
Configure:
    send timeout(ms): 4
    protect memory size(Bytes): 16384
    protect node number: 128
    write retry times: 4
IO:
    first waiting time: 0
    read index: 101403
    write index: 88696
    operation sequence: 4023560795
Statistics:
    write - check sequence failed: 0
    write - retry times: 0
    read - bad node: 16
    read - bad block: 0
    read - write timeout: 0
    read - check block size failed: 0
    read - check node count failed: 1
    read - check hash failed: 0
Debug:
    last action - channel: 0x7f9291997000
    last action - begin node index: 101398
    last action - end node index: 101403
......
Node index:     101398 => seq=4023547849, is start node=No, is written=No, data(Hex): 3807000000000000fdeb7cfc0000000000670100b0b0b0b0
Node index:     101399 => seq=4023547849, is start node=No, is written=No, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101400 => seq=4023547849, is start node=No, is written=No, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101401 => seq=4023547849, is start node=No, is written=No, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101402 => seq=4023547849, is start node=No, is written=No, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101403 => seq=4023533496, is start node=Yes, Data Length=616, Hash=872284744, is written=Yes, data(Hex): 68020000000000004802fe3300000000fe66010033333333
Node index:     101404 => seq=4023533496, is start node=No, is written=Yes, data(Hex): 333333333333333333333333333333333333333333333333
Node index:     101405 => seq=4023533496, is start node=No, is written=Yes, data(Hex): 333333333333333333333333333333333333333333333333
Node index:     101406 => seq=4023533496, is start node=No, is written=Yes, data(Hex): 333333333333333333333333333333333333333333333333
Node index:     101407 => seq=4023533496, is start node=No, is written=Yes, data(Hex): 333333333333333333333333333333333333333333333333
Node index:     101408 => seq=4023547849, is start node=No, is written=Yes, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101409 => seq=4023547849, is start node=No, is written=Yes, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101410 => seq=4023547849, is start node=No, is written=Yes, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101411 => seq=4023547849, is start node=No, is written=Yes, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101412 => seq=4023547849, is start node=No, is written=Yes, data(Hex): b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0
Node index:     101413 => seq=4023547850, is start node=Yes, Data Length=1864, Hash=938159398, is written=Yes, data(Hex): 4807000000000000262deb3700000000ff66010027272727

节点101398已被跳过,所以MF_START_NODE标记被清理了,但是在出错时这里是其实节点。这个数据环境是三进程写,一进程读,通道长度16MB,超时判定4ms。

出错的结果是有一块数据被复写而丢失了。

错误数据在 Node index: 101398 处。中间被插入了Node index: 101403。

插入偏移量等于Node index: 101403的长度。怀疑是compare_exchange_weak在成功后内存未接受内存屏障进cache,并且返回失败,在下一次操作直接成功。

另外发现Node index: 101403的操作序号远低于Node index: 101398。开始分析认为可能是进程fe660100被切出,超时时间后才切回。导致前一次写数据覆写了后面的。但是这种情况应该先会被判定为缓冲区不足,而不是直接覆写。如果是接收端进入了超时检测分支,那么会有超时计数。但是包括后面几小时后的数据,超时计数都一直是0。所以排除了这种情况。

顺带一点点小优化

这次BUG也查了挺久,加上移入了一些其他依赖库的一些小细节功能,所以随手加上了一些小优化。

  1. cmake的环境配置不再使用加编译宏的形式,而是configure(detail/libatbus_config.h.in)文件。这样如果移植给其他模块使用可以直接用生成好的头文件,保持配置一致。

  2. 很多地方都是在冲突时才有分支流程,这些地方加了一些likely和unlikely,已辅助编译器进行编译优化。

  3. 完善导出共享内存通道数据的小工具,原来输出的可读性稍微差一点。

  4. 因为是内存操作,增加了一些volatile。但是测试的时候未见性能下降,所以为保证正确,就留着了。这样能保证更快速的内存同步。

  5. 压力测试工具实现了更严格的数据校验,保证数据完全可预测然后精确校验数据。

新的稳定性测试

在上面的修复完成后我又进行了一整天的benchmark,环境仍然是三进程写,一进程读,通道长度16MB,超时判定4ms。最后的log如下:

[ RUNNING  ] NO.1450 m
[ RUNNING  ] recv(93055489089 times, 88395 GB) recv err 0 times, data valid failed 0 times 

Summary:
    channel node size: 128
    channel node count: 123331
    channel using memory size: 16777112
    channel available node number: 1775

Configure:
    send timeout(ms): 4
    protect memory size(Bytes): 16384 
    protect node number: 128
    write retry times: 4

IO:
    first waiting time: 0
    read index: 117979
    write index: 116075
    operation sequence: 2877148690

Statistics:
    write - check sequence failed: 0
    write - retry times: 0
    read - bad node: 0
    read - bad block: 0
    read - write timeout: 0
    read - check block size failed: 0
    read - check node count failed: 0
    read - check hash failed: 0

Debug:
    last action - channel: 0x7f5688d7b000
    last action - begin node index: 118300
    last action - end node index: 118301

这里的性能指标低于文档里说示的结果,是因为这并不是单独对吞吐量和QPS的压力测试,发包的数据量比较大,范围是8B-2KB平均是1KB,所以QPS只有一百万多一点点,吞吐量也只是1GB/s多一点。但是实际性能并没有下降,因为改完之前这个数据包范围的数据也差不多是这样。当然以后有空还是可以重跑下压力测试。

最重要的是,可以看到一整天的满负载执行后,并没有出现一次错误。我们这次的BUG修复就算圆满达成了。

由于 里建议除非涉及数据表示多重含义或者附带对齐或数据切割,否则建议使用compare_exchange_strong。所以改成了compare_exchange_strong。可能是平台里原子操作的缓存逻辑问题所致。这篇文档里也说了compare_exchange_weak的情况下某些成功的行为会类似失败所以保险起见换成了compare_exchange_strong。而且后来压力测试也没发现性能有下降,所以就保持这样了。

libatbus
关于BUS通信系统的一些思考(二)
libatbus
http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange