1. Bugs
1. The serial connection is broke.
2. The destination session can be wrong in Google cloud dialog if nothing is selected.
2. Other changes
1. Do not reset the session group collapse status.
为项目定义环境变量和别名,是个很好的实践:提高效率,屏蔽复杂的命令,降低策划使用自己的一套服务器环境的门槛。
我们的项目环境变量和别名存在名为alias_cmd的文件中,置于项目目录下,操作项目前需要source或“. ”导入变量或别名定义。目前的命令主要有下面几类。
别名文件需要source或“. ”导入变量或别名定义,直接“./”执行会报错,防止被错误执行。
1 | SELF_NAME="${BASH_ARGV[0]}" |
1 | USER=`whoami` |
1 | alias cdr="cd $PROJ_BASE" |
1 | alias startgs="echo -ne \"\\e]2;game_server\\a\";cd $PROJ_BASE/server/gameserver/UnitTest;./gameservergame_app -c gameserver.lua" |
1 | alias profgs="cd $PROJ_BASE/server/gameserver/UnitTest;env CPUPROFILE=./gameservergame_app.prof ./gameservergame_app -c gameserver.lua" |
1 | alias gdbgs="cd $PROJ_BASE/server/gameserver/UnitTest;gdb --args ./gameservergame_app -c gameserver.lua" |
1 | alias agdbgs="export a=\`psmy gameservergame_ap[p] |awk '{print \$2;}' \`; gdb -p \$a;" |
别名文件跟随分支,不用分支的别名文件直接改,交由版本工具管理,不要弄多份,不然会混淆。
一个ssh终端对应一个项目分支,不然也容易混淆(可以通过命令改终端名字)。
经历的项目里,见过有用autoconf的,但是私有的server,部署环境是确定的,大多都是直接用Makefile。
为什么说是强迫症的呢?主要是最初有这么几个想法:
这个Makefile框架后来用于游戏项目,另外加了
对项目文件的管理,设想是一个顶层目录一个库(如前文的线程组一个目录,网络库一个目录,逻辑层的FSM一个目录…),先编译成静态库或者动态库(动态库问题比较多,我们最后都用静态库了);每个库带一个UnitTest目录,一边写一边测,最后这个UnitTest就被当成执行文件了。
先看一个目录的例子。
1 | ######################################### |
1 | CC=g++ |
OK,设计思想就这样,定义变量,其他交给makefile.compile.rules,如下。
1 | ######################################### |
1 | ######################################### |
1 | ######################################### |
和前面类似
1 | ######################################### |
我们会在.d文件里临时存放.cpp所依赖的头文件
1 | test: CFLAGS+=$(DEBUG_FLAGS) |
不同的target应用不同的变量,执行pre/post相关操作。debug版本是make test,release版本是make rtest。
1 | $(DEBUG_TEST_TARGET): $(MOD_OUTDIR) $(AUTOTARGET) $(DEBUG_TARGET) testdepend $(TEST_OUT_SUBDIR) $(DEBUG_TEST_OBJECTS) $(TEST_OUTDIR) $(TEST_OUT_FILE) $(SYMBOL_OUTDIR) deploy_common |
1 | $(MOD_OUTDIR): |
1 | -include $(DEPENDFILES) $(TESTDEPENDFILES) |
.o文件依赖.d.tmp和源文件,%d.tmp依赖与源文件,调用gcc生成源文件的依赖头文件,然后将这些文件include进来。这样,依赖的头文件或源文件有改动,都会再重新编译。
1 | ######################################### |
1 | ######################################### |
无侵入设计,把任意类变成singleton:
1 | template<typename DataType, int instanceId = 0> |
因为是模板,static变量的定义可放头文件,c++编译器对每个实体类的static变量仅定义一次。
如果需要多个实例,可以为instanceId赋不同的值(如下),否则忽略。
1 | class A{}; |
后来碰到的问题,析构顺序需要控制,当时是通过调整链接顺序解决。现在想想,更好的是at_exit,main前先全声明一遍,逆序析构。
设计框架(背景见链接)的时候,经常会碰到生产者消费者模型,当时就想整型以机器字长对齐,其赋值和读取都是原子的。举个例子
1 | size_t i = 0; |
在机器字长对齐的情况下,任何时候,a要么是0,要么是0xFFFF,不会出现0xFF00。(如果是pack(1),上面就不成立了)
因此就有个很天然的想法,如果生产者和消费者各一个,分属不同线程,生产者只修改写偏移量,消费者只改读偏移量,两个偏移量设为volatile保证其只能从内存读取(volatile不能保证线程安全,这里的安全是通过内存总线保证偏移量的完整性实现的),是不是能实现一个FIFO的循环队列,不加锁的情况下也能保证队列内数据的完整性。
设想一下,如果生产者写偏移量不更新,消费者首先判断当前无数据可读;
生产者将数据写入,然后才更新写偏移量,整个过程只有写偏移量修改修改后,消费者才能开始读。
以上写/读流程都工作得很好,直到写偏移量>buffer大小
因为写之前要判断剩下多少空间可写
1 | size_t usedSize = writeIndexM - readIndexM; |
如果将writeIndexM限制于[0, sizeM),因为readIndexM只能由消费者改,那么下次写,这条公式就不对了。
怎么处理呢?与其做复杂的判断,不如不限偏移量的范围,就让writeIndexM和readIndexM只增不减,即使是溢出,因为sizeM远小于量偏移量的取值范围,上面的公式还是成立的。
这样子,带来另外一个问题,如何将writeIndexM映射回buffer的偏移量呢?很简单,对sizeM取余就好。这样对sizeM会有限制,比如32bit,因为有溢出的问题,推导要求 (0xFFFFFFFF + 1) % sizeM = 0 % sizeM = 0,即
也就是说sizeM必须是2的倍数
至此,这个循环的buffer就成型了,一个线程读一个线程写,不用加锁,互不干扰。如果多个生产者,需要在生产者侧加锁。消费者类似。
源代码,32位及以上适用:
当时查过,size_t的赋值在机器字长对齐的情况下,部分平台可能会由多条指令实现的,这种平台(网上也没说是什么平台,估计比较小众)不适用。根据笔者的测试,x86/x64是ok的,arm的默认thumb指令集也ok。请测试使用。
框架背景见上篇。
网络框架的设计参考了python的异步框架twisted。
笔者对同步和异步性能的认识源于还在爱立信的两个项目,第一个项目是同步的,2-3k的tps,性能压不上去,客户端您随便加压力,我服务端就不理你,就这种;第二个项目是异步的,1w的tps,实验室环境在瑞典,一直没机会玩玩。但笔者因为是研发中心支持,在一线人员配合下能直接登录上生成环境,最惨烈的一次是阿三的服务器,就看着内存一直在涨,5分钟一挂…您没听错,就是5分钟一挂,提单的严重级别是中级,要放国内早爆了…除了升级扩容别无他法。当时就感叹,异步没有压不爆的机器。
鉴于此,笔者当时看了挺多异步的设计,twisted是印象最深的一个。接着用python写一个自己的python测试框架pythontest, 但一直都没派上用场。framework-nd的网络模型其实就是pythontest的c++版。但笔者的python仅限于东拼西凑,答不出所以然。
写网络框架的人,应该都看过《unix网络编程》,也知道平台差异性。笔者选用libevent1(2的设计比较复杂,没用)去封装平台差异,socket的读写按边缘触发处理。
恩, 好像完了?大的部分好像真的完了,但是细节都在代码里。例如做了流控,即接收缓存在高水位,则不再从socket读取,滑动窗口协议会导致客户端不再发送,知道接收队列被处理至正常水平,防止服务器被输入压垮了。
另外,这玩意还是很考验多线程调试技巧的。
EchoServer是这么启的,telnetServer是管理端口。
1 | Processor::BoostProcessor processor("NetProcessor", 4); |
putty-nd同步session数据到Google Drive,需要启一HttpServer配合Google账号的验证,对这套框架略有修改。加力之后代码段增加得不多,还是很小巧的。
笔者出道于爱立信,电信行业,回想这么多年技术或管理上所用的技巧,思想多源于斯,深有感触。在爱立信还是有些狗屎运的,曾于项目青黄不接时独立维护一个AAAServer,熟悉了内存分配器,把该server的内存按得纹丝不动;然后又在一个全球化的网关产品中做PLM,能纵览其异步io+任务式线程组+javascript的设计。但项目中接触到的框架都是现成的,临渊羡鱼不如退而结网,自己也在开始将认识写成代码,即framework-nd。只是没有需求,怎知其可堪一用?
机会还是来了,即我的第二份工作(加薪30%,如果各种福利一起算,实际只加了几百,如果不是因为这是个机会,估计留不下来,从大平台跳出来,请慎重),当时是要做信令和HTTP流量分析平台,一组人负责解析信令,我一人负责HTTP流量解析(当时https用得不多,不然就歇菜了),信令组需要记录的关键字段发给我,我将其和同时段相应的HTTP的请求/响应的关键字段汇总成一条话单,写文件,待后续分析。然后这个框架就用上了,解决了无数的问题后,在惠州移动上线了,日处理数据约1.5TB/日(我们用TAP设备抓了一天的包,1.5TB,内部拿这1.5T文件测试,千兆网卡带宽压到80%~90%之间,cpu和内存的相关数据时间太久就不记得)。第二次上线信令组也用了这套线程框架。
后来这个框架经过适当的改造,应用于我的工具putty-nd中(zmodem和同步Google Drive的本地服务器)。
其实大家都知道,要使系统CPU达到最大的利用率,全局活跃线程数应等于逻辑CPU个数,这样CPU没有切换的开销,负载达到CPU个数,刚好吃饱。
这是条基本规则,现实中会有些变化。
结合之前的经验,做了下面的设计。
在调试中发现session的任务可能会在错误的线程处理,因此加入了一些防错的校验。
在后来的调优中,发现如果各个线程共享系统的定时器队列,输入在100Mbit/s时达到处理的瓶颈。因为会话需要同步各个输入,用到了大量的定时器,因此改在线程上用最小堆实现了会话的定时器管理。因为同一会话的处理都在同一线程,避免了定时器任务的加锁,顺利跨过这个坎,使输入达到网卡带宽的80%~90%时,仍能稳定处理。
实践发现的一些冗余的设计:对队列设计了池子,但对tcmalloc这种现代分配器,对比发现完全看不出有任何优化。也就是说,无初始化负担仅是内存池子的设计,可以完全交给内存分配器。
有设计的蓝图,实现就不难了,链接。
主要有两个类,Boost的版本,实现还有Pthread的版本,不知道丢哪去了,简单改几个东西, 不麻烦。
对外提供的接口有三类。
1 | BoostProcessor(const unsigned theThreadCount); |
构造函数除了数量,还有一个带名字的。方便知道是哪组线程发生了堆积。其实建议用pthread_setname_np为每个线程都设个名字,这样用Top命令就能看到各个线程的cpu负载情况。
1 | void start(); |
1 | struct event* addLocalTimer( |
Timer放在线程上的原因说过了。
看了一下,最新代码的任务还是带了池子的版本,用了大量的模板,导致执行文件过大,性能上没提升,这是一个不太成功的实践。直接用boost::function简化就行了。
1 | void BoostWorker::run() |
bufferJobQueueM是读写分离的一个数据结构,因为读取只有一个线程,所以没有锁(改天开篇讲)。
定时器队列用的是libevent的最小堆,小巧,提供了删除的接口。
处理完任务后,如果还有任务要处理,则继续处理,如果还有定时时间,time_wait 500微秒(0.5ms,定时器精度就是这个了),否则直接wait。只要有wait,CPU就被释放了,要么timeout切回来,要么新任务有signal唤醒。有人说线程的切换至少要耗时10ms,笔者有印象这应该是出自某本经典的书,说内核的时间片算法最小单位大约10ms。后来再查了一下,这时间片算法单位应该是和频率有关,10ms应该是书作者写书时的数据,算算下应该是上世纪的事情了。
其他就不贴了。代码都在github上。
在游戏行业的时候,因为帧循环是单线程处理的,曾有想法引入线程组,按场景id分线程去处理。当时最大的问题是32bit程序内存受限,n个线程n个lua vm,配置表得n份,太占内存。因为够用,程序人少事多,晃着晃着就过去了。
以下脚本均为bash脚本,在git bash下执行。gdb调试android的动态库,本质和gdb远程调试Linux程序一样:远端启动gdbserver attach上要调试的应用,同时开放监听端口,然后gdb远程连上去调试。所以其涉及远端(Android)和本机两个系统。
1 | export PACKAGE_NAME=com.test.test |
就这几个,因人而异,自己改,都很简单,不多做解释了,下面的命令会用到。
我用的是模拟器,规避权限问题,所以用的是x86版本的。
用Debug版本的动态库构建App,安装App,App本身是不是debug版本无所谓(App的这个debug版本是给java调试用的,如果是在没有root的真机上,需要开App的Debug,用run-as切到app对应userid,才能attach)。其实用Release版本的动态库也可以调,只是所有调试信息都没了,只能用public的符号或绝对地址下断点,想看参数得直接用栈指针,像下面这样。Ok,这么说,读者应该知道怎么灵活选择了,你想调的那个库是Debug版就行了。
1 | b __android_log_print |
1 | # 把端口映射打开,adb负责把本机的5039端口,映射到远端Android的5039端口 |
1 | function kill_gdbserver(){ |
1 | function start_app(){ |
1 | function map_app(){ |
1 | function attach_app(){ |
建议开三个终端(git bash),全都定义好变量和封装的函数。
一个跑
1 | start_app |
一个接着跑
1 | attach_app |
最后一个跑
1 | map_app #这条命令比较慢,仅第一次启动的时候跑就好了 |
然后,读者就可以放飞自我了。
关于为什么需要做x64升级的原因参见上篇。
这里分享的是升级之后发生的一次诡异的内存泄漏。
最初始的怀疑对象是新引入的腾讯的behaviac组件,因为刷怪之后内存涨得特别明显。经过简单地查找,发现behaviac组件确实有自己的内存分配器。进一步分析发现,behaviac组件只是有一层内存池的管理,它还是用系统的malloc等接口申请内存。也就是说,我们项目里behaviac组件也是通过tcmalloc分配内存,不是这个组件的问题。
所以,只剩一条路,gdb下函数断点。内存申请的点多,下个函数断点打日志。当时的gdb命令没记下来,信手写一下吧,大概长得这个样子。
1 | b malloc |
断了几个分配的接口,发现打印的分配值和实际增长值都对不上。最后,负责副本的骏图同学(当年招到的一位特别靠谱的同事,服务器出身,新项目负责客户端渲染,水面/人物材质做得很漂亮。低调,不玩朋友圈不发博,所以没链接,面试不招是损失)把这个函数锁定在了mmap。多采样几次,看看共同点,下面是从聊天记录里挖出来的两个堆栈,前一张来自骏图,后一张我的。
堆栈大致分三个部分,由下到上分别是
Debug版本的也得解决啊,节操问题。直觉上怀疑是tcmalloc 64bit下暂存的池子太大了(上图frame 16),调试一看,按字节算的,撑死10MB
在往上看frame 15,源码如下,MallocBlockQueueEntry传了16个指针数组给libunwind填空,和libunwind的交互不需要再分配什么内存,tcmalloc也不需要调libunwind接口释放什么东西, 所以就libunwind自己申请的得自己管理,这样子的话,就是libunwind漏了。
仔细看堆栈,libunwind部分也不完全一样,一阵小郁闷,libunwind咱不熟。不过这库挺老了,用的人多,应该会有人报bug,遂Google “libunwind memory leak”。果不其然,真找着了,链接。
我们用的版本是libunwind-0.99-beta,tcmalloc推荐版,老了,和链接里的Patch代码长得不一样,照着描,core了几次之后最终版diff如下,中间过程也不表了。好吧,老实说不记得了,其实我现在看回改过的代码也挺懵B的,算了,不是研究这个的。。。好吧,我还是记得只是一些先后顺序的出了问题,但是读者不会满意的:(。
1 | --- |
总结一下,这问题刚开始认为是刷怪的问题,其实是刷怪了之后,内存分配释放比较频繁,把问题暴露了出来,让副本的同学背了会儿锅。
然后,这个逻辑是debug版才有,怪不得x86和x64做机器人压测比较的时候,也没发现。
是的,2018年以前,我们的服务器是32位程序,超过4GB内存会挂。
这其中很大一部分原因是luajit 2.1版本前内存受限(原因见链接),一个vm只支持不超过2GB的内存(看下面的图里的编译选项,MAP_32BIT 只能1G,开了LJ_ALLOC_MMAP_PROBE能到2G, GC64往下看。。。 oh,no,写文档前一直以为MAP_32BIT是2G,没实践过,前面的链接也没仔细看),32位程序反而没这个限制。
另外一个原因是,够用。当然,发生内存泄漏的时候,总会心慌慌。
2018年我们开启了新项目,luajit 2.1分支从提交上看趋于稳定,是时候趁此机会往前走一步了。
直接用git上最新代码,不过得选对分支,默认master是2.0(我就这样掉了一下坑),看来一下今年以来的改动基本都是修修小bug,影响不大,应该是稳定了。
在Makefile里打开GC64选项,为了方便gdb,把调试符号给加上。
1 | # Uncomment the next line to generate debug information: |
编译,测试,不行,调。关键字mmap,找到这段代码。
宏隔开,三段,用“#error msg”编译,用的是LJ_ALLOC_MMAP_PROBE这段。把下面的宏定义取消,重新编译,就ok了。
(补充一下,仔细看上图,GC64可用多少内存?128TB,够用了,笔者在电信行业见过最大的core dump是46GB,漏着跑了一夜,太大了,mdb都没法加载,之后再也没用过这么大内存的机器了,哈哈哈)
到这里,就差不多了,我们新项目用着没问题。
luajit 2.1中lua和luac两个命令合一,都叫luajit,把之前的lua和luac改成软链。
Makefile(也是我弄的,设计挺轻巧的,改天开篇讲,7年前初始版)相应修改:
1 | - $(PROJBASE)/lib/bin/luac -o $(DEST_OUTDIR)/{}.lc {}.lua |
其他改asset版本号自己find + xargs + sed改一下,debug不用设,opt level默认3,lua内断点依然生效,没特别关注,项目跑了几个月,这块没出什么问题。倒是tcmalloc + libunwind出了些问题,下篇讲。
tag:
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true