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
  • 前言
  • 基本流程和通用分布式事务服务器
  • 容灾和负载均衡
  • 简单对比
  • 后续的优化

Was this helpful?

  1. 2020

在游戏服务器中使用分布式事务

Previousxresloader转表工具链增加了一些新功能(map,oneof支持,输出矩阵,基于模板引擎的加载代码生成等)Nextlibcopp接入C++20 Coroutine和一些过渡期的设计

Last updated 4 years ago

Was this helpful?

前言

游戏业务通常有个特点是模块相关性非常高,模块之间的联动也非常密集且复杂。要保持各个相关模块的数据一致性,同时又兼顾效率和,没有一个通用的方法。通常的做法是走有损服务(也叫柔性服务)和自动修复的方式。比如支付服务一般的做法是在2PC的基础上增加redo log,对于发放和订单确认这两方,如果失败了会尝试几次补发。又或者好友系统或者公会,因为涉及多个对象的数据相互索引,一些做法是玩家在线的时候定期去检查数据是否正确,如果不正确走修复流程。

我去年的时候看了下Google的一些分布式系统的分布式事务的设计的论文(),感觉上也挺适合游戏业务中某些系统的使用场景。所以在我们现在的项目中就做了一些尝试,对一些系统引入了分布式事务的设计,主要目的还是为了解决数据一致性,并且提供可大规模平行扩容的能力。

基本流程和通用分布式事务服务器

其实分布式事务的本质是把事务的成功与否收敛到单点上,也就是需要唯一的 协调者 。然后执行方要支持TCC (Try-Commit/Cancel)操作。简单得描述流程如下:

  1. 创建事务,分配事务ID(两次数据库操作,一次分配ID,一次创建)

  2. 所有参与者预提交阶段(Try,1次RPC)

  3. 事务提交(Commit/Cancel,1次RPC+1次数据库操作)

  4. 通知所有参与者提交成功/失败(2次RPC)

  5. 所有参与者确认提交成功后删除事务(数据库操作)

  6. 如果没有参与者成功,执行者直接通知事务 协调者 清理事务(1次RPC+1次数据库操作)

的论文里有伪代码,写得比较清楚易懂。它的 Try 阶段叫 Prewrite 。

为了尽可能地抽象和简化分布式事务的接入,我们把分布式事务的流程划分成了上面也提到过的三个角色: 参与者 、 执行者 、 协调者 。

  • 执行者 是事务的发起方,也是执行事务的推动方,负责创建事务、通知 参与者 执行状态等;

  • 参与者 即是不同事务事件处理的执行方,一个事务可能涉及多种不同类型的参与者和事务事件操作,参与者自己要实现事务的retry和超时retry流程;

  • 协调者 即是如上面提到的,收敛事务的最终状态。以 协调者 对事务的操作结果为准,来断定事务是否成功执行。同时 协调者 也负责事务的数据清理。

在我们的系统中,提供了 执行者 流程模板。执行阶段,我们会使用一个全局唯一ID分配器来分配事务ID,并创建事务数据,这个流程和执行过程中的检查、超时、通知流程都是统一的。同时我们提供了统一的 协调者 服务,用于管理事务数据。这样,业务模块要接入分布式事务,仅仅需要实现 参与者 流程即可,相对来说,这个流程实现就比较简单了。

比较完整的流程时序图如下:

在我们目前实现的事务 参与者 实现中,均采用给事务数据操作单元分配一个递增的事件ID( event_id )来实现,这个ID仅仅和某个力度很小的逻辑模块数据单元有关(比如每个玩家的好友数据块上都有自己的事件ID分配器,多个玩家之间的分配器之间不会相互影响;同一个玩家的好友和公会数据之间也不会互相影响)。在业务逻辑中,我们会处理事务关联关系,并在逻辑模块数据单元里记录下最后执行的事务的事件ID。因为无论什么数据,到最基础的逻辑也都是增删改查,只是不同类型数据的关联关系和逻辑条件不一样而已。对于删除的数据,我们会保留一段时间,这个时间至少要大于事务的超时时间+N次自动retry的时间+时间误差。通过这种方式,我们就可以仅根据事件ID的大小来决定事务的流程是需要执行还是忽略。

我们还增加了数据块级别的事件ID,这个数据块级别的事件ID和逻辑模块数据单元里记录的事件ID的关系有点像数据库的行级锁和数据列的关系。这有什么作用呢?比如现如今的业务有要求必须提供删除账号的功能,那么怎么判定删除账号和新增或者变更数据的事务同时进行,那么我们怎么判定新增或者变更数据的操作是否要被忽略呢?这时候我们就可以给删除事务的事件ID赋值到数据块级别的事件ID记录。如果后续的事务操作大于这个数据块级别的事件ID则是需要执行的,否则是可以直接忽略的。

即便实现了上述的分布式事务的系统,我们还是少不了传统的 业务补偿 逻辑,只是可以简化一些。这是因为在游戏服务器业务中,一般不会有特别高的一致性要求,比如上面这些业务服务器大多数是没有主备的。那么我们还是要有一定的策略来处理执行者认为事务成功,但是执行节点宕机导致的数据一致性问题。这个 业务补偿 逻辑就和传统的方案很像了,只是可以比较简单一些,因为这个阶段只是需要保证数据能正常运行,并不特别依赖它作逻辑一致性保证,可以弱化修复流程。

容灾和负载均衡

  1. Not Found : 视为失败,放弃执行

  2. Commited : 事务成功,提交执行

  3. Abort : 事务终止,放弃执行

这里特别解释一下 Not Found 的原因。正常情况下在所有的 参与者 收到通知或失败消息后,会通知 协调者 它已完成对事务的处理。而 协调者 在收到所有参与者的完成通知后,会对事务做清理,即从数据库里移除。而 执行者 创建完事务后要通知所有 参与者 进行 Try 阶段,只要 参与者 进行了 Try 阶段,那么 参与者 一定会通知到 协调者 最后清理数据。但是如果 执行者 第一次通知 参与者 进行 Try 阶段失败了,或者不知道是否失败,那么 执行者 无法知道 参与者 是否真的执行了这个请求,如果 参与者 没有执行到 Try 阶段,那这个事务就只要会有一个 参与者 没有完成,如果不处理就会导致事务数据泄露。所以如果 执行者 通知所有 参与者 进行 Try 阶段有任意的失败的话, 执行者 会直接执行删除事务的操作。

对于玩家对象我们有一些特殊对待,因为它由网关层决定在哪个gamesvr节点上拉起。如果在执行事务的过程中玩家对象被需要在其他节点上拉起,则需要引入一些列的锁、超时管理等机制,因为事务执行的RPC次数比较多,如果事务执行延迟比较大,也会增加登入时的稳定性。所以为了避免这些麻烦,我们目前的玩家对象(gamesvr上)不作为事务的 参与者 ,只能作为 执行者 。目前我们的服务器实现中,对好友服务和公会服务实施了分布式事务支持。

我们目前实现的服务结构和关系如下:

简单对比

特性

我们的分布式事务服务

Google Percolator

Google Spanner

事件ID/事件版本号

池化分配,按服务对象key分布式事件版本号,设计QPS>1亿/s

全服单点timestamp oracle服务,QPS大约200万/s

TrueTime时间戳服务

协调者

专用 协调者 服务器

固定算法,指定一个参与事务执行者做协调者

Paxos算法选举Leader做 协调者

脏数据/垃圾清理

参与者 服务器定期清理,下一次GET/SET时清理

下一次GET/SET时恢复/清理,定期扫描

参与者 超时清理。切换 协调者 leader时,新leader负责恢复/清理未完成事务

底层存储

延迟

每个事务4次DB请求,1次内部RPC,每个参与者3次内部RPC

索引服务,延迟不敏感

26-200ms

QPS

依赖服务器间通信和底层存储

对比BigTable,读事务*0.94,写事务BigTable*0.23

-

冲突策略

根据业务实现,目前业务实现是无锁

悲观锁,Get前清理失效事务

一致性保证

目前业务实现是每个Key独立的递增事件ID,协调者CAS

锁记录操作的节点,Chubby锁服务记录节点存活

依赖TrueTime时间戳,服务器间低延迟和安全时间 \( T_{safe} \)

其他特点

适用于多参与者对等接入,适用于多种业务类型,但接入较复杂

索引服务,自动化依赖分析和触发依赖事务

分布式关系型数据库

后续的优化

最后,欢迎有兴趣的小伙伴们一起交流,如果文中有什么偏差的地方欢迎指正。

实现分布式事务需要所有参与者实现事务操作的 ( f(f(x))=f(x)f(f(x)) = f(x)f(f(x))=f(x) ) 。说人话就是执行一次和执行N次,的效果相同。在事务系统中还要更进一步,要至少保证最终一致性。为了达到这个目标,一般不同的系统有不同的取舍和方案,这在我的另一篇文章中 有一些简单的总结。

在传统存储系统中,这个 的事务操作比较容易抽象统一,因为存储系统的操作无非是增删改查,其中删比较特殊复杂一些( 的论文有提及)。而在 系统中,主要是Google的索引系统,它的模块间关系更加简单一些,只是需要触发被依赖的模块更新而已。

而在游戏项目中,这个关系就比较复杂,因为涉及多方和多种不同的逻辑。比如移除好友时可能也要清理多个账户上好友间的其他交互数据(邀请、通知、分享等),玩家被批准加入公会后可能要保证这个玩家加入其他公会的申请失效,而此时玩家可能并不在线,者就涉及不同的业务模块(也就是 参与者 )需要根据自身的业务特性来实现这个 ,实现的内容和策略都大相径庭。

由于我们用 分布式池化ID分配器 来分配事务,所以我们对事务的负载均衡就可以使用事务ID作为一致性Hash的来分发到 协调者 服务器上。而 协调者 服务是一个带缓存的无状态服务,逻辑也很简单(创建保存、读数据、标记已完成的参与者、如果所有参与者都处理完毕了就清理数据这几种)。所以它的每次操作都是用CAS对内存数据操作,如果CAS返回数据版本过老,则是缓存过期,会重新拉取数据然后尝试retry(类似 策略,这里 协调者 仅仅是记录 参与者 的完成情况,不影响事务的结果,所以不存在 策略通常会有的饥饿的问题)。这里的一致性保证依赖了所使用的内存数据库的一致性保证。 执行者 是发起事务的一方,直接使用在业务设计的本身负载均衡策略即可。

前面提到,新的业务模块想要接入这个分布式事务系统的话只需要实现 参与者 逻辑即可,实际上 参与者 的实现还是有一定要求的。一是上面已经提到过的通过 事件ID( event_id ) 来实现的 保证;二是在对 协调者 的通知失败或者不确定成功还是失败时要有retry机制;三是需要在超时的时候去拉取事务状态,然后拉事务状态会有以下这三种结果。

我们简单的和 和 的分布式事务做一个对比如下:

任意支持CAS的内存Key-Value数据库(比如 )

只读无锁,读写悲观锁,(F1引入了乐观锁)

我们的无锁是业务特性+简化设计导决定的。如果实现得更严谨一些,还是应该走乐观锁的流程然后走 策略解决冲突。举个例子,比如我们的公会功能有多个管理员,一个管理员同意某个人加入公会的同时另一个管理员拒绝了,这种情况下严格的做法是按先后顺序(即按事件ID( event_id ) ),先锁加锁然后后请求的失败。而我们这里会直接返回 其他管理员正在操作中,请稍后... ,然后在 Try 阶段会直接失败掉。这样能省去定时器和稍后重试和解死锁的逻辑,能够极大地降低复杂度。但是这显然是和业务相关的,并不是所有的业务形式都可以这么做,具体还是得根据业务需求来抉择。

分布式事务的细节优化点其实非常多,在我们目前的系统中主要还有两个方向的优化。第一是上面提到的更严谨的事务加锁流程。第二是我们目前的事务完成完全是 参与者 驱动的,那么如果 参与者 所在机器如果宕机了,并且没有再上线的话(比如特别是玩家流失以后),那么 协调者 就会一直收不到 参与者 的完成通知,就会遗留一些事务数据一直处于没有清理的状态。 目前我的想法是后续加一个定时任务,定期扫描出超时很久并且未完成的事务,然后通知 参与者 拉起对应数据块继续把事务执行完,可能会和 里的定期扫描比较像。因为事务表数据库只会保存正在进行未完成的事务数据,所以即便全表扫描数据量也不会很大,而且如果数据库支持快照的话可以从快照里读,也不会影响正在运行的业务。

《Google去中心化分布式系统论文三件套(Percolator、Spanner、F1)读后感》
Percolator
幂等性
《Anna(支持任意扩展和超高性能的KV数据库系统)阅读笔记 - 主流分布式KVS的比较》
幂等性
GFS
Percolator
幂等性
Wait-die
Wait-die
幂等性
Percolator
Spanner
Wound-wait
Percolator
redis
BigTable
BigTable
Wound-wait
2005-transaction-process.png
2005-transaction-roles.png