个人开发经验总结

个人心得,仅供参考


一.提升规范性

1. 保持规范整洁的目录结构,用不同的目录存储不同的模块。比如ugc目录存储ugc模块,activity目录存储活动平台模块。模块内部进一步细分share、接口、cgiserver等子目录。避免将不同模块的东西放到一起而显得凌乱。

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. 判断boolboolval是否为真,用if (boolval) 即可,用ifboolval == 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. 避免频繁的newdelete,考虑new一次反复使用或者类似于内存池的做法(平台mpns大量使用了内存池)。

27. 调用频繁,逻辑简单的代码尽量封装成内联函数或者是宏。频繁的压栈、退栈也是比较消耗cpu的。

28. 尽量少用memset,尤其当buffer很大的时候,很多时候只需要将最后的字符赋值为即可,或者用单独的length字段来标识边界。

29. stl尽量避免多次查找,比方说,想判断uin(作为key)是否在一个map里面,如果不在就插入一条记录,如果在就不插入。可以

if (find() == end()) Insert(uin, value), 也可以直接insert(uin, value), 前者需要两次查找,后者只需要一次。

30. 能用位运算的尽量用位运算。比方说可以用intval >> 1代替intval / 2. 但是要注意intval为负数会引发符号位扩展,所以为负数的时候尽量不用移位操作。

31. 慎用stringvector等的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. 有时候查找mapset的中的key是否存在,可以用count(key) != 0来判断,比用find(key) = end()略显简洁。

42. 80%的需求可以提供默认函数,并将默认函数做成弱符号,这个弱符号可以被20%的人进行重写覆盖。比方说hydra_frame里面弱符号版本的main函数。

43.尽量使一些东西自动化,比如cgi框架用getenvSCRIPT_NAME)自动获取cgi名字进行日志初始化。cgi接口内部可以自动获取登录态的cookieuinskey)等而不用显式传入。hydra_cgi_handler内部可以做到自动探测当前是否新的请求,从而决定是否重新解析参数,使用方不用显式的做任何清理操作。

44. 对外提供接口对应 的makefile宏定义,方便使用,不需要使用方在makefile里面书写- I include_path, -Llib_path -lxyz

45. 接口在升级的时候应该做到向后(老接口)兼容,无法兼容的时候增加一个新的接口,以保证老的服务可用。

46. 全局构造/析构函数可以加 __attribute__((constructor))__attribute__((destructor))修饰,这样的函数不用显式调用,比较方便,还可以设置优先级。


四.提升安全性

47. 接口做好异常分支、边界检测,做到任何输入都无法引发崩溃,做成打不死的小强。

48. 接口尽量考虑线程安全,使得能够使用在多线程环境下。

49. 避免使用strcpygets之类的高危函数,使用有长度限制的替代接口,比如:strncpyfgets

50. 必须在底层做好单元测试,墙不会坍塌的前提是砖不是用豆腐做的。

51. 尽量使用智能清理策略来对资源进行释放,比如c++shared_ptrC++的析构函数,gccC扩展的__attribute__((cleanup (cleanup_function)))属性等。普通清理容易造成在异常分支的遗漏,从而造成资源泄漏。

52. 避免操作时序造成的问题。比方说在多线程环境下清理跟fd相关的全局数据结构,一定要先清理跟fd对应的数据结构,最后才close(fd)。不然很可能出现closefd)后,其他线程又将fd打开,而此时fd对应的数据结构还未来得及清理,最终结果难以预料,很可能导致crash

53. 声明变量的时候记得初始化,不然很容易引起难以排除的错误。

54. 注意资源的及时释放。比方说95%的请求只需要1k的内存buffer5%的请求需要1M的内存buffer,程序在第一次请求的时候分配一块内存,然后每次请求对这块内存进行复用,当内存不足的时候对内存进行扩张。这样一段时间之后,大量进程都会持有一个1Mbuffer,实际上大部分时间只需要用到里面的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. 在设计接口的时候,不需要对外暴露的头文件includecpp里面,对外暴露的符号越少,符号冲突的概率也就越低。

62. 设计存储服务的时候引入sequence机制,以保证数据安全。

63. 涉及到网络交互的整数在打包的时候记得先hton*,解包的时候再ntoh*,以做到在不同平台下的兼容。


五.提升可用性

64. server进程快慢分离 ,重要跟次要分离,避免相互影响。

65. 吞吐量大的服务考虑用异步逻辑实现,以避免阻塞。

66. 接口超时控制在毫秒,避免秒级的超时,尽量避免使用alarm等过时接口作超时管理。

67. 配置信息可以定时读取,从而避免更新配置文件的时候重启进程对服务造成中断。


六.提升调式运营能力

68. 可以在发布的库里面加上自己的特定标识,比如libhydra_log.a里面有hydra的版本号信息(HVx.y.z),链接过libhydra_log.aELF文件(cgiserver等)通过strings ELF文件 | grep HV即可获取到版本号信息。当ELF文件运行故障的时候,可以通过此种方式先获取到版本号,再从对应版本的代码找原因。

69. 尽量避免依赖gcc优化,手工优化更好。因为gcc优化后很多符号都找不到,不利于调试。非要优化的话,尽量用低级别的优化,比如-O-O2

70. 设计server的时候,可以通过prctl(PR_SET_NAME, name)给进程/线程命名,方便调试跟踪。

71. 打印日志的时候,必须要有关键字段,比如uinappid,耗时,ipporterrno等,保证根据日志可以找到错误的原因或者是线索。

72. 异常操作要有流水,比方说农场里面卖种子,绝大部分人可能一次性只会卖掉3000金币的种子,当有操作的金币数超过3000的时候,最好是记录一条流水,方便外挂打击、异常操作统计分析等。

73. 最好在关键函数的入口设置一个TRACE_LOG,当给hydra_log配置上某个用户的uin后,就可以跟踪这个用户的操作流,方便排查问题。

74. 注意容灾建设,避免在代码写死ip,尽量使用L5cmlb等具有负载均衡、容灾能力的接口来作路由。

75. 服务要有完善的监控(模调、特性统计等)与告警提醒,以便及时发现服务存在的问题。并提供完善的工具和管理端用来辅助功能测试、问题定位。

你可能感兴趣的:(个人开发经验总结)