个人心得,仅供参考
1. 保持规范整洁的目录结构,用不同的目录存储不同的模块。比如ugc目录存储ugc模块,activity目录存储活动平台模块。模块内部进一步细分share、接口、cgi、server等子目录。避免将不同模块的东西放到一起而显得凌乱。
2. 与时俱进,文件编码尽量用utf8。
3. 尽量避免使用全局using namespace hydra,会导致符号暴露到全局空间,加大符号冲突的可能性。
4. 设计接口的时候,接口是接口,实现是实现。实现跟接口可以都放在头文件里面,但是尽量不要没有接口声明直接上实现,这样不便于使用者查找接口。
5. 规范使用错误码,避免滥用-1,-2,-3之类的错误码,除非是内部的小接口。对外返回的接口最好是有一个单独的错误码定义文件,并且区分逻辑错误与系统错误,逻辑错误可以用正数表示,系统错误可以用负数表示。
6. 设计接口的时候多考虑是否方便单元测试,比如可以返回一个字符串,而不是直接将字符串打印到终端。
7. 私有接口用private标识,避免对使用者造成困扰。
8. 类型不同,逻辑相同的代码考虑用模板函数封装。
9. 变量/函数命名保持一致,尽量避免一会儿用uin、一会用qq。同时避免驼峰跟下划线一起用,比如不要用Hydra_GetFriendList,可以用HydraGetFriendList。
10. 代码块可以用宏包装,但是常量定义尽量用const,因为宏不受名字空间约束!!!但是const常量可以。
11. 配置文件管理类尽量用单体实现,而不是让使用者声明一个对象,然后用这个对象的接口。
12. 代码里面用空格代替制表符,保持在不同编辑器环境下格式依旧正确。
13. 全局函数做成static被类包裹,不如使用名字空间来的自然。
14. 判断stl类容器是否为空,用empty()方法即可,用size() ==0判断显得麻烦而且山寨。
15. 判断bool值boolval是否为真,用if (boolval) 即可,用if(boolval == true)判断显得麻烦而且山寨。
16. 判断整数intval是否为0,尽量用if (intval != 0), 用if (intval)也没有错,但是不标准,毕竟括号里面需要的是一个bool值。
17. 按场景合理选择数据结构和算法,比如需要快速检索一个key对应的value,就应该用map/hash_map,而不应该用vector之类的线性结构。
18. 多线程下应该尽量减少资源竞争,可以考虑线程私有。不能私有的尽量不用锁,必须用锁的选择合适的锁,比如读多写少的场景更适合用读写锁而不是互斥锁。而且锁的粒度不宜过大,只锁公共资源,否则程序性能会很低。
19. 避免每次请求都加载文件,应该做好缓存。
20. 设计数据结构的时候,字段尽量手工进行字节对齐,尽量避免使用pack属性。
21. 设计协议头的时候,公共字段放到协议头里面,显得清晰,而且不用每个body都去定义这个字段。
22. 在tcp连接数允许的情况下,尽量使用长连接。并做好连接数限制。
23. 局部函数尽量加static标识,可以加快寻址,同时减少跟其他符号冲突的概率。
24. 大对象参数传递尽量用引用或者指针,避免使用值拷贝。
25. 可以只运算一次的,坚决不运算两次。比方说多维数组寻址的时候,避免arr[i][j], 可以先保存ptr = &a[i], 然后ptr[j],因为a[i]在寻址的时候涉及到乘法运算,而乘法运算比加法运算慢很多。
26. 避免频繁的new、delete,考虑new一次反复使用或者类似于内存池的做法(平台mpns大量使用了内存池)。
27. 调用频繁,逻辑简单的代码尽量封装成内联函数或者是宏。频繁的压栈、退栈也是比较消耗cpu的。
28. 尽量少用memset,尤其当buffer很大的时候,很多时候只需要将最后的字符赋值为0 即可,或者用单独的length字段来标识边界。
29. stl尽量避免多次查找,比方说,想判断uin(作为key)是否在一个map里面,如果不在就插入一条记录,如果在就不插入。可以
if (find() == end()) Insert(uin, value), 也可以直接insert(uin, value), 前者需要两次查找,后者只需要一次。
30. 能用位运算的尽量用位运算。比方说可以用intval >> 1代替intval / 2. 但是要注意intval为负数会引发符号位扩展,所以为负数的时候尽量不用移位操作。
31. 慎用string、vector等的resize操作,resize会自动填充空缺,当resize参数很大的时候,可能会比较慢。
32. 充分利用cpu的亲和性和数据局部性原理,提升cache命中率。
33. 当需要new大量对象的时候,尽量避免循环new,可以先new一个buffer,然后使用placement new来挨个调用构造函数。
34. 减少系统调用,getpid()之类的可以只调用一次. 在单次操作中,time(NULL)一般只需要调用一次即可。
35. 多线程申请内存可以考虑使用tcmalloc代替new/malloc以提升效率。
36. 尽量减少网络交互次数,可以用批量,而不是多个单key交互。
37. 接口要保持详细准确的注释, 及时更新注释。而且参数设计要尽量简单,不用的参数不要暴露。
38. 接口最好按照pack,route/client/接口分层设计。以适应异步,同步不同场景的需求。
39. 接口出参放在前面,入参放到后面,这样可以保证所有出参挨到一起,所有入参挨到一起。否则前面是入参,中间是出参,最后还有默认参数值的入参,出参被夹在中间,稍显凌乱。
40. 接口批量接口做好内部切片,可以避免使用者切片,方便使用。
41. 有时候查找map、set的中的key是否存在,可以用count(key) != 0来判断,比用find(key) != end()略显简洁。
42. 80%的需求可以提供默认函数,并将默认函数做成弱符号,这个弱符号可以被20%的人进行重写覆盖。比方说hydra_frame里面弱符号版本的main函数。
43.尽量使一些东西自动化,比如cgi框架用getenv(“SCRIPT_NAME”)自动获取cgi名字进行日志初始化。cgi接口内部可以自动获取登录态的cookie(uin,skey)等而不用显式传入。hydra_cgi_handler内部可以做到自动探测当前是否新的请求,从而决定是否重新解析参数,使用方不用显式的做任何清理操作。
44. 对外提供接口对应 的makefile宏定义,方便使用,不需要使用方在makefile里面书写- I include_path, -Llib_path -lxyz
45. 接口在升级的时候应该做到向后(老接口)兼容,无法兼容的时候增加一个新的接口,以保证老的服务可用。
46. 全局构造/析构函数可以加 __attribute__((constructor)),__attribute__((destructor))修饰,这样的函数不用显式调用,比较方便,还可以设置优先级。
47. 接口做好异常分支、边界检测,做到任何输入都无法引发崩溃,做成打不死的小强。
48. 接口尽量考虑线程安全,使得能够使用在多线程环境下。
49. 避免使用strcpy,gets之类的高危函数,使用有长度限制的替代接口,比如:strncpy,fgets
50. 必须在底层做好单元测试,墙不会坍塌的前提是砖不是用豆腐做的。
51. 尽量使用智能清理策略来对资源进行释放,比如c++的shared_ptr,C++的析构函数,gcc对C扩展的__attribute__((cleanup (cleanup_function)))属性等。普通清理容易造成在异常分支的遗漏,从而造成资源泄漏。
52. 避免操作时序造成的问题。比方说在多线程环境下清理跟fd相关的全局数据结构,一定要先清理跟fd对应的数据结构,最后才close(fd)。不然很可能出现close(fd)后,其他线程又将fd打开,而此时fd对应的数据结构还未来得及清理,最终结果难以预料,很可能导致crash。
53. 声明变量的时候记得初始化,不然很容易引起难以排除的错误。
54. 注意资源的及时释放。比方说95%的请求只需要1k的内存buffer,5%的请求需要1M的内存buffer,程序在第一次请求的时候分配一块内存,然后每次请求对这块内存进行复用,当内存不足的时候对内存进行扩张。这样一段时间之后,大量进程都会持有一个1M的buffer,实际上大部分时间只需要用到里面的1K。当系统内存不富裕的情况下,很容易造成其他进程分配不到内存的情况。所以要对进程内分配的大内存适时的进行释放,避免每个进程都占用大量内存尸位素餐。
55. 设计协议的时候,必须考虑协议的扩展性,要适当预留一些字段。要不然很容易导致程序难以维护,举个例子: 之前在设计老的留言系统的时候,一味的节省存储,没有预留任何的字段,结果当新需求需要新的字段的时候,只能在已有字段里面抠出一些bit位来进行存储,勉强支持了需求,结果代码十分恶心,难以维护。
56. 充分利用gcc编译器的错误检查,比如设计日志函数的时候加__attribute__((format(printf, x, y)))属性对格式参数进行检查,将错误扼杀在编译阶段,不至于引起运行时的coredump。号称我们互联网最大的某中间件日志函数居然没有作这种检查,导致我在上面载过两次跟头。另外,在编译期尽量消除所有的警告,可以给gcc加-Werror选项,强制将警告转换成错误。
57. 接口对象(比如transaction对象)尽量做好内部清理,做到使用全局变量可以像使用临时变量一样安全,不用担心资源泄漏,也不用担心内部状态混乱。
58. if 中的常量写在前面,可以避免==误写成=造成的悲剧。比如if (3 = intval) 会引发编译错误,而if (intval = 3)不会。
59. 如果使用第三方接口,又不确定它是否抛异常的时候,尽量进行try catch。并将exception信息打印出来,可以避免程序crash。
60. 不使用的头文件最好不要去包含它,容易给自己挖坑,包含的头文件越多,符号冲突的概率也越高,编译预处理的时间也更长。
61. 在设计接口的时候,不需要对外暴露的头文件include在cpp里面,对外暴露的符号越少,符号冲突的概率也就越低。
62. 设计存储服务的时候引入sequence机制,以保证数据安全。
63. 涉及到网络交互的整数在打包的时候记得先hton*,解包的时候再ntoh*,以做到在不同平台下的兼容。
64. server进程快慢分离 ,重要跟次要分离,避免相互影响。
65. 吞吐量大的服务考虑用异步逻辑实现,以避免阻塞。
66. 接口超时控制在毫秒,避免秒级的超时,尽量避免使用alarm等过时接口作超时管理。
67. 配置信息可以定时读取,从而避免更新配置文件的时候重启进程对服务造成中断。
68. 可以在发布的库里面加上自己的特定标识,比如libhydra_log.a里面有hydra的版本号信息(HVx.y.z),链接过libhydra_log.a的ELF文件(cgi、server等)通过strings ELF文件 | grep HV即可获取到版本号信息。当ELF文件运行故障的时候,可以通过此种方式先获取到版本号,再从对应版本的代码找原因。
69. 尽量避免依赖gcc优化,手工优化更好。因为gcc优化后很多符号都找不到,不利于调试。非要优化的话,尽量用低级别的优化,比如-O,-O2。
70. 设计server的时候,可以通过prctl(PR_SET_NAME, name)给进程/线程命名,方便调试跟踪。
71. 打印日志的时候,必须要有关键字段,比如uin,appid,耗时,ip,port,errno等,保证根据日志可以找到错误的原因或者是线索。
72. 异常操作要有流水,比方说农场里面卖种子,绝大部分人可能一次性只会卖掉3000金币的种子,当有操作的金币数超过3000的时候,最好是记录一条流水,方便外挂打击、异常操作统计分析等。
73. 最好在关键函数的入口设置一个TRACE_LOG,当给hydra_log配置上某个用户的uin后,就可以跟踪这个用户的操作流,方便排查问题。
74. 注意容灾建设,避免在代码写死ip,尽量使用L5,cmlb等具有负载均衡、容灾能力的接口来作路由。
75. 服务要有完善的监控(模调、特性统计等)与告警提醒,以便及时发现服务存在的问题。并提供完善的工具和管理端用来辅助功能测试、问题定位。