一、基础设施优化
1、提升 CPU 缓存的命中率
CPU 缓存分为数据缓存与指令缓存。
- 按顺序访问数据(操作连续内存):利用数据缓存,提高读数据缓存的命中率。
- 有规律的条件分支(如数据集先排序再处理):利用指令缓存,提高读指令缓存的命中率。
- 数据按缓存行大小填充/对齐(通常为64字节):防止伪共享,提高并发处理能力和缓存命中率。
- 对于多核CPU,如果缓存命中率很高,可以考虑进行CPU绑定。
伪共享:假设cache line是64字节,我们在一个64字节的并且和cache line 对齐后的内存中放入两个4字节的整数A和B,然后线程a和b分别访问A和B,在内存层面的语义是这两个线程分别独享一块内存区域,操作时互不干扰,但是在缓存cache line层面他们是共享一个cache line的,是一个"原子的数",这就是伪共享。
缓存层面的伪共享的一致性由硬件保证,对程序员透明,也就是对这个"原子数"的操作不用显式加锁,但是伪共享会降低程序效率。
2、提升内存分配效率
进程申请内存的速度会受到内存池的影响,隐藏的内存池如下:
其中不同的 C 库内存池,都有它们最适合的应用场景,例如 :TCMalloc 对多线程下的小内存分配特别友好,每个线程独立分配内存,无须加锁,所以速度更快;而Ptmalloc2 则对各类尺寸的内存申请都有稳定的表现,更加通用。
在栈中分配内存比内存池中堆分配内存要快很多。
3、使用哈希表管理亿级数据
作为索引,哈希表是第一选择。由于哈希表基于数组实现,数组可根据下标随机访问任意元素,即下标乘以元素大小,再加上数组的首地址,就可以获得目标访问地址,直接获取数据。因此查找时间复杂度为O(1),不会随着业务规模增长而变化。
位图,哈希表的变种,限制每个哈希桶只有 1 个比特位,消耗的空间少。常用于解决缓存穿透的问题,以及快速判断对象是否存在。(布隆过滤器也可判断状态)
由于哈希表不支持范围查询与遍历,如果业务需要,可以考虑B树。
- 如何降低 / 解决哈希冲突?
1、由于生产环境需要考虑容灾,如将哈希表原地序列化为文件,保证新进程快速恢复哈希表。相较于拉链法,开放寻址法更擅长序列化数据。
2、注重内存的节约使用。数亿条数据会放大节约下的点滴内存,再把它们用于提升哈希数组的大小,就可以通过降低装载因子来减少哈希冲突。
3、优化哈希函数。(集群的负载均衡正是用到了这个思想)
4、磁盘优化技术
磁盘是主机中最慢的硬件之一,常常是性能瓶颈,所以优化它能获得立竿见影的效果。
- 零拷贝提升文件传输性能:
为什么读取磁盘文件时,一定要做上下文切换呢?
服务器提供文件传输功能,需要将磁盘上的文件读取出来,通过网络协议发送到客户端,然而读取磁盘或者操作网卡都由操作系统内核完成,内核权限最高。对于执行的read和write系统调用,一定会发生2次上下文切换。(用户态到内核态间切换)
![](https://i-blog.csdnimg.cn/blog_migrate/68379a2f19f12dd5db6205cacde6a929.jpeg)
零拷贝技术:它是操作系统提供的新函数,同时接收文件描述符和 TCP socket 作为输入参数,这样执行时就可以完全在内核态完成内存拷贝,既减少了内存拷贝次数,也降低了上下文切换次数。
零拷贝技术基于 PageCache(磁盘高速缓存)。
1、PageCache 缓存了最近访问过的数据,提升了访问缓存数据的性能。
2、PageCache协助 IO 调度算法实现了 IO 合并与预读(这也是顺序读比随机读性能好的原因),进一步提升了零拷贝的性能。
异步 IO + 直接 IO:高并发场景处理大文件时,应当使用异步 IO+直接 IO 来替换零拷贝技术。(可以设定一个文件大小阈值)
知识点补充:
常见网络IO模型:
- 同步阻塞IO(BIO)
- 同步非阻塞IO(NIO)
- IO多路复用
- 异步非阻塞(AIO)
其中前三种为同步模型,AIO为异步;BIO是最简单、最常见的IO模型(Linux下默认所有socket为blocking的);IO多路复用可参考文章什么是IO多路复用?用来解决什么问题?如何实现?
零拷贝根据应用场景有不同实现方式:
1、sendfile:直接从磁盘读到内核发送到网卡,不需经过用户态到内核态的拷贝转换。
2、mmap:虚拟内存映射到用户空间,需要保证内存与磁盘的数据一致性。
5、实现高并发服务的思路
1、使用多线程,为每一个请求分配一个线程来执行。弊端:单个线程消耗内存过多,没有足够的内存创建几万线程实现并发;线程切换导致上下文切换,耗费CPU资源。
2、异步编程,实现用户态的请求切换。异步化依赖 IO 多路复用机制的同时,还需要把阻塞方法改为非阻塞方法(IO多路复用+回调函数)。处理基于 TCP 的应用层协议时,一个请求的处理代码必须被拆分到多个回调函数中,由异步框架在相应的事件生成时调用它们。弊端:代码书写难度大,易出错。
3、使用协程,协程可看作用户态的线程。与异步框架不同点在于,协程把异步化中的两段函数封装成一个阻塞的协程函数。在该函数执行时,由协程框架完成协程之间的切换,协程是无感知的。弊端:由于一个线程可以包含多个协程,如果协程触发了线程的切换就会导致该线程上的所有协程都阻塞,所以需要使用生态完善的协程,如GO语言天然支持协程。python 3内置库Asyncio支持原生协程,但生态不够完善,基于aiohttp可实现一些小的服务。
二、系统网络优化
1、一对多通讯提升局域网传输效率
广播:对局域网内所有主机发送消息
组播:对部分主机发送消息
相比于TCP协议需要建立连接,UDP协议无需建立连接,所以我们常用 UDP 协议发送广播。
广播性能高的原因:1、交换机直接转发给接收方,要比从发送方到接收方的传输路径更短。2、原本需要发送方复制多份报文再逐一发送至各个接受者的工作,被交换机完成了,这既分担了发送方的负载,也充分使用了整个网络的带宽。
组播——更精准的定向广播:网络 API 中的 setsockopt 函数可以通过 IGMP 协议,向虚拟组中添加或者删除 IP 地址。当路由器支持 IGMP 协议时,组播就可以跨越多个网络实现更广泛的一对多通讯。
广播和组播能够充分地使用全网带宽,在更关注及时性、对丢包不敏感的流媒体直播中更有应用前景。
2、C10M实现之事件驱动
早些年提到的C10K,是指服务器同时处理1万个TCP连接,近来我们更希望单台服务器的并发能力可以达到 C10M,也就是同时可以处理 1 千万个 TCP 连接。
什么是事件:从网络中接收到一个报文,就可能产生一个事件,进而触发回调函数执行。由于常见的HTTP协议是基于TCP实现的,在TCP报文中有两种事件类型:读事件和写事件。
TCP 连接建立时,会在客户端产生写事件,在服务器端产生读事件。连接关闭时,则会在被动关闭端产生读事件。在连接上收发消息时,也会产生事件,其中发送消息前的写事件与内核分配的缓冲区有关。
获取事件:多路复用epoll 函数使得进程可以高效地收集到事件。
- 把需要监控的 socket 传给内核(epoll_ctl 函数),它仅在连接建立等有限的时机调用;
- 收集事件(epoll_wait 函数)便不用传递 socket 了,这样就把 socket 的重复传递改为了一次传递,降低了性能损耗。
一个事件的时间不易过长,对于处理事件代码分为以下三类:
- 对于计算任务,可以将请求放在独立线程中完成或者把请求拆分成多段,放慢该请求处理时间保证其他请求及时处理。
- 读写磁盘,需要将大文件的读取拆分成多份,每份仅有几十KB,降低单次操作的耗时。
- 通过网络访问上游服务,必须将阻塞socket改造成非阻塞 socket,用事件驱动方式处理请求。比如 Memcached 的官方 SDK 是用阻塞 socket 实现的,Nginx正确的访问方式,是使用第三方提供的 ngx_http_memcached_module 模块,它用非阻塞 socket 重新封装了 SDK。
3、提升TCP三次握手性能
TCP 是一个可以双向传输的全双工协议,所以需要经过三次握手才能建立连接。三次握手在一个 HTTP 请求中的平均时间占比在 10% 以上,在网络状况不佳、高并发或者遭遇 SYN 泛洪攻击等场景中,如果不能正确地调整三次握手中的参数,就会对性能有很大的影响。
客户端优化:
- 当客户端通过发送SYN发起握手时,可以通过tcp_syn_retries控制重发次数;
- 当服务器的 SYN 半连接队列溢出后,SYN 报文会丢失从而导致连接建立失败。我们可以通过 netstat -s 给出的统计结果判断队列长度是否合适,进而通过 tcp_max_syn_backlog 参数调整队列的长度;
- 服务器回复 SYN+ACK 报文的重试次数由 tcp_synack_retries 参数控制,网络稳定时可以调小它。
- 为了应对 SYN 泛洪攻击,应将 tcp_syncookies 参数设置为 1,它仅在 SYN 队列满后开启 syncookie 功能,保证连接成功建立。
服务端优化:
- 服务器收到客户端返回的 ACK 后,会把连接移入 accept 队列,等待进程调用 accept 函数取出连接。
- 如果 accept 队列溢出,默认系统会丢弃 ACK,也可以通过 tcp_abort_on_overflow 参数用 RST 通知客户端连接建立失败。
- 如果 netstat 统计信息显示,大量的 ACK 被丢弃后,可以通过 listen 函数的 backlog 参数和 somaxconn 系统参数提高队列上限。
4、提升TCP四次挥手的性能
四次挥手只涉及两种报文:FIN 和 ACK。
为什么建立连接是三次握手,而关闭连接需要四次挥手?
建立连接三次握手为了保证client到server和server到client双向通道都可达;结束连接四次挥手,除了彼此确认双向通道关闭,还为服务端状态变成关闭提供异步等待时间。
三、应用层编解码优化