1. Bugs
1. making the Default Settings's protocol/hostname/portnum unsavable.
2. disable the flag TOOLTYPE_HOST_ARG_CAN_BE_SESSION in cmdline to avoid the missed-saving of the tmp/session.
L is the lua_State.
1 | # released under GPLv3 license(http://www.gnu.org/licenses/gpl-3.0.txt). |
You are charged for the scripts which works for the older/luajit version.
为项目定义环境变量和别名,是个很好的实践:提高效率,屏蔽复杂的命令,降低策划使用自己的一套服务器环境的门槛。
我们的项目环境变量和别名存在名为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 #这条命令比较慢,仅第一次启动的时候跑就好了 |
然后,读者就可以放飞自我了。
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