标签:style http color os io 使用 ar for 文件
由于水平有限,以下仅仅是个人的一些心得,希望对新人有一点参考作用。另外由于时间关系,写得有点杂,有些点可能并不是跟服务器编程强相关的。
性能相关
1. 应用各种pool。
a) Mempool
比如为了提高内存分配效率,可以使用Mem pool。当对应的场景简单时,可以自己定制私有的内存池管理。当内存池设计相对复杂的时候,可以考虑直接使用jemalloc、tcmalloc。
b) Socket pool
比如dns解析一般是基于udp协议,为了提高性能,避免反复创建、销毁socket带来的开销,可以创建一个udp socket pool,而且预先将这些fd的EPOLLIN事件注册到epoll里面。这样有dns请求的时候,直接从udp socket pool取一个fd进行send,然后等待epoll的input事件,recv完成后将这个fd归还到udp socket pool。可以参考http://code.oa.com/v2/weima/detail/7373,里面有dns ipv4地址解析的完整的代码。
2. 网络请求,在允许的前提下尽量用批量操作,可以大幅提高整体传输性能
3. 网络send/recv buffer避免多余的memset,memset只会浪费cpu运算,其实只需要管理好相应的length字段就好了。之前看到过一段代码,开了一个非常大的buffer,不管三七二十一直接memset为0,仅memset就花费了几百毫秒。
4. 避免重复运算。看到过很多这样写的:for (int I = 0; I < strlen(str); ++i) {… }。其实完全可以先计算strlen(str)并存到一个变量,然后再用这个变量,可以减少不必要的性能损耗。
5. 能用栈的尽量不用new、delete。提升性能的同时,还可以降低编码复杂度,因为不用显式地delete,最重要的是可以降低内存泄漏的风险,真是一举三得。gcc支持变成数组,加之现在机器的默认栈都是好几M,很多场景下可以直接这样声明数组:int arr[num]。看到过在同一个函数里面反复new、delete buffer的蛋疼写法,即便这里不能用栈,起码可以new一个buffer反复用。
6. 避免烂用值拷贝和引用。 拷贝大对象是非常耗时的,所以应该尽量避免。但是滥用引用一样不好,比如传递一个int参数,用值拷贝比引用要好,引用本质上是一个指针,寻址运算有一定的开销。不过滥用引用比滥用值拷贝的害处小很多。
7. epollout事件使用是个技术活,使用不好很容易导致cpu彪得很高。个人感觉一般有两种做法:
a) 只在异步connect时监听一次,连接成功后即可把out事件去掉。因为send操作只是将数据填充到socket send buffer,大部分情况下send操作可以立马成功。即便不成功,也可以把未发送完成的数据放到一个队列里面,等下一轮epoll事件处理完成后再把数据拿出来发送,很可能发送成功(因为上次的数据很可能被完全收走了或者收走了一部分),没有发送(完成)的数据则继续等待下一轮epoll事件处理完后再处理。
b) 跟上面类似,异步connect时监听一次,连接成功后把out事件去掉。有发送任务时先试着发送一次,很可能就全部发送完成了。不能发送完成则将数据先放入一个队列里面,同时添加out事件到epoll里面,等到out事件到来时发送余下的数据。
8. 在封装日志接口时,最好将日志接口封装成一个宏,一个好处是可以自动的嵌入file、line、function等信息。另外一个好处是,可以先检查当前这个调用的log level有没有被enable,如果没有enable则不会产生任何调用;否则,debug log即使没有被打开,还是会进行函数的参数压栈等操作,而且有些参数本身又涉及到复杂的运算,这个时候会无谓的牺牲大量cpu时间。
9. 很多时候需要用到(hash) map, 在key是一个复杂对象时性能是比较低的。在场景允许的情况下,可以先将对象进行一次hash预处理,后序的比较操作直接用这个hash值作为key可以大幅提升性能。可以尝试使用XXH64这个接口,性能好而且冲突率极低。
10. 空间换时间。有时候可能需要比较多的整数转字符串操作,可以利用字典的方式做预处理,后序的转换直接查字典就行了。字典大小有限,遇到比较大的整数时,先将整数除以某个值(比如100000)然后分治查字典,最后拼接。
11. 减少临界资源的数量, 能线程私有的尽量私有化。避免非临界资源放到锁里面,只会延迟锁的持有时间,增加锁冲突的概率。如果邻临界资源只涉及轻量的cpu运算,尽量用原子操作、自旋锁、顺序锁、cas等。自旋锁推荐使用intel 的tbb spin lock。当线程数较多时比pthread_spin_lock高效,而且tbb spin lock有读写锁,而pthread版本则没有。
12. 场景允许的前提下尽量用长连接,有时候可以大幅提高性能。
13. Stl使用时尽量先reserve,可以大幅降低内存分配和数据拷贝的次数。
14. 减少系统调用,比如使用accept4接收一个连接,用sendfile传输文件等。搞不懂为啥accept有对应的accept4,而socket没有对应的socket4?如果系统再提供一个接口sockctrl(fd, sndsize, rcvsize, sndtimeout, rcvtimeout)就更完美了,调用一次干四件事情。适当设置send recv缓冲区大小,可以减少send、recv的次数
15. 当在传送大量数据的时候,为了提高TCP发送效率,可以设置TCP_CORK
16. 对小包的实时性要求比较高的时候,应该设置TCP_NODELAY 以关闭Nagle算法。
17. 线程在必要的时候绑定到cpu,可以避免上下文切换和减少cache miss。
18. worker数目自动适配。外网的服务配置可能不尽相同,服务器需要根据cpu数目自动分配对应数量的线程。
陷阱相关
1. 在循环里面用迭代器进行删除的时候要特别注意,关联容器(map、set)应该用erase(it++), 顺序容器(vector、list)应该用it = erase(it)。C++11里面可以统一为it = erase(it)
2. 慎用tcp快速回收,很容易引起tcp reset。调整net.ipv4.tcp_fin_timeout参数代替快速回收更加靠谱。
3. 系统调用select是个坑,当fd超过1024后就会出问题。尤其当子进程隐式地继承了父进程的fd后更容易莫名的出问题,尽量用poll、epoll代替。
4. 封装日志函数时,要加__attribute__((__format__(__printf__, x, y)))属性,以便在编译的时候做参数类型匹配检查,不然遇到类似LOG(“%s”, 123)调用可以编译过,但是运行就coredump。
5. 坑爹的时序问题。在多线程环境要尤其注意close(fd)的时序问题,如果有一个全局数组,用fd去索引的话,close(fd)操作必须要放到最后。先调用close(fd),然后再做array[fd]对应的清理会有问题,因为本线程关闭的这个fd很可能被快速分配给了其他线程,多线程同时操作array[fd]这个对象可能造成问题。
6. 过大的mtu值可能导致数据包无法穿过路由器,不要为了提高性能随意调整。
7. 异步连接必须加超时管理。之前遇到过不给异步连接加超时管理,而某些事件永远不通知epoll,导致不断地创建socket,无形中造成socket句柄泄漏。
tcp连接失败原因分析:
1. 网络不通。看看iptables防火墙规则,确认是否请求被drop。
2. 网络波动。用ping看看是否大量丢包导致。
3. client端分配不到“端口”。
a) 如果日志显示can not assigne requested address,可以cat /proc/sys/net/ipv4/ip_local_port_range确认一下,必要时调大区间值。
b) 调小tcp_fin_timeout(推荐)或者开启快速回收(不推荐)
4. server繁忙、处理能力太弱,导致不能及时地accept客户端的连接
5. 确认是否server端的队列(内核参数)配置的过小
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
cat /proc/sys/net/core/somaxconn
如果确定是这个问题,调大这2个值。
调试相关
1. 用Strace跟踪系统调用,strace –etrace进行选择性地监控系统调用。
2. 加上必要的debug log。平时关闭,出问题时打开方便跟踪问题
3. 可以通过prctl(PR_SET_NAME, name)给线程命名,方便调试跟踪。用ps -eLo nlwp,vsz,sz,stat,wchan,%cpu,%mem,ppid,pid,tid,comm=THREADS,lstart,cmd可以查看各种详尽信息,包括线程名字。
推送服务相关
1. 推送服务器一般需要维持大量的长连接,内存往往成为瓶颈,所以需要调整内核参数(下面是几个基本的)。
a) 调大文件句柄: fs.file-max
b) 调大连接队列:net.ipv4.tcp_max_syn_backlog、net.core.somaxconn
c) 调小默认的接收、发送缓冲大小net.core.rmem_default、net.core.wmem_default,接收缓冲最好不低于1k,否则容易出问题。
2. 使用新的内核
新版内核做了一些优化,将per socket cache变成per task cache,可以大幅降低空闲内存的占用,从而让创建大量socket成为可能。
3. SO_REUSEPORT
如果要充分利用多cpu、多队列网卡的优势,一个接入线程可能是不够的。但是开多个端口明显是不利于使用的,所以新版的内核(貌似是3.9开始的)支持端口重用(SO_REUSEPORT)选项。使得不同的进程可以监听同一个端口,不同的进程可以均匀地accept而不至于引起惊群效应和accept不均匀的问题。大概的原理是:通过对新建连接的(sip, sport, dip, dport)四元组做hash,通过hash值对应到多进程中的一个监听socket,从而实现连接在多个进程上面的的均匀分配。
安全相关
1. defer accept。推迟accept,当接收到第一个数据之后,三次握手方能完成。同时也可以提高性能
2. 使用防火墙,挡住不需要开放的端口或者屏蔽某些黑名单ip、端口
3. 避免系统单点。尽量让服务器无状态,有状态的话可能需要提供主备的模式。或者主主的模式,平时2个都是主,各自处理不同的任务单元,互通心跳,如果一台发现另外一台宕机,则实时接管另外一台的任务,不过需要做好容量预留。
4. 主次逻辑分离、读写分离。心跳逻辑可以简单的用udp实现,跟业务逻辑的tcp连接无关。
监控告警
1. 关键地方一定要加详细的日志、流水,不然遇到问题很可能无从下手
2. 对访问量、失败率、延时分布、错误码等要做上报。如果统计项非常多,用特性统计性能更高(写共享内存)。
3. 在网管、模调系统对特性统计、模调进行告警配置,当发现失败率高过某个阀值,或者请求量陡增、陡降的时候进行告警。
一点编码小技巧
1. C++有析构函数,可以方便的清理资源。C没有析构函数,但是gcc对C做了扩展,借助cleanup属性可以完成资源的自动清理,举例如下:
void Free(char** ptr)
{
If (*ptr != 0)
free(*ptr);
}
void func()
{
char* array __attribute__((cleanup(Free))) = (char*)malloc(1024);
/* 函数退出的时候array被自动释放 */
}
2. 如果要在进入main之前自动完成某些初始化,可以将初始化操作放到__attribute__((constructor)) void Init() { .. }函数里面。
3. typeof, 用gcc的typeof关键字简化编码:
std::map<int, std::map<int, int>> dict;
迭代遍历的时候可以用typeof(dict.end()) it = dict.begin()代替 std::map<int, std::map<int, int>>::iterator it = dict.begin();
其他
1. 协议必须可扩展, 可以根据业务场景选择json、protobuf等协议。
2. 超时管理:
复杂场景下,可能需要用堆、红黑树、timerfd等机制实现超时管理。
但是大部分场景下可以用更简单的方式,这些简单方式往往也更高效。比如把连接信息放到一个map里面,只要定期(比如一秒一次)扫描这个map就行了,注意这里一定要控制好频率,否则频繁的超时检测会耗费大量的cpu。一般我的习惯是,一轮epoll事件后取一次tsc(cpu寄存器的计数),然后减去tsc_prev,判断这个差值有没有达到阀值(比如1秒),达到阀值才进行超时检查。因为读取tsc非常的快,而且最频繁一秒才检查一次超时,所以这里的超时检测效率还是比较高的。
3. worker间用无锁队列通信。 生产者将消息压入队列,消费者处理完一轮epoll事件后peek一下队列数据,如果队列有数据则将数据全部取出,该怎么处理就怎么处理; 如果队列为空则啥都不做,继续epol_wait或者干线程循环里面的其他事情。
标签:style http color os io 使用 ar for 文件
原文地址:http://blog.csdn.net/nullzhou/article/details/39090441