首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >基础设施及系统层网络调优思路

基础设施及系统层网络调优思路

作者头像
才浅Coding攻略
发布2022-12-12 16:59:10
发布2022-12-12 16:59:10
58600
代码可运行
举报
文章被收录于专栏:才浅coding攻略才浅coding攻略
运行总次数:0
代码可运行

“文章整理了基础设施和网络两大块调优思路。基础设施从提升单机进程的性能入手,包括高效地使用主机的CPU、内存、磁盘等硬件,通过并发编程提升吞吐量;系统层网络优化传输层网络从降低请求的时延、提升总体吞吐量两个方向尝试优化。让我们开始吧!”

基础设施优化

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次上下文切换。(用户态到内核态间切换)下图为零拷贝示意图:

零拷贝技术:它是操作系统提供的新函数,同时接收文件描述符和 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的)。

零拷贝根据应用场景有不同实现方式:

1、sendfile:直接从磁盘读到内核发送到网卡,不需经过用户态到内核态的拷贝转换。

2、mmap:虚拟内存映射到用户空间,需要保证内存与磁盘的数据一致性。

————————————————————————————————————

5、实现高并发服务的思路

1、使用多线程,为每一个请求分配一个线程来执行。弊端:单个线程消耗内存过多,没有足够的内存创建几万线程实现并发;线程切换导致上下文切换,耗费CPU资源。

2、异步编程,实现用户态的请求切换。异步化依赖 IO 多路复用机制的同时,还需要把阻塞方法改为非阻塞方法(IO多路复用+回调函数)。处理基于 TCP 的应用层协议时,一个请求的处理代码必须被拆分到多个回调函数中,由异步框架在相应的事件生成时调用它们。弊端:代码书写难度大,易出错。

3、使用协程,协程可看作用户态的线程。与异步框架不同点在于,协程把异步化中的两段函数封装成一个阻塞的协程函数。在该函数执行时,由协程框架完成协程之间的切换,协程是无感知的。弊端:由于一个线程可以包含多个协程,如果协程触发了线程的切换就会导致该线程上的所有协程都阻塞,所以需要使用生态完善的协程,如GO语言天然支持协程。python 3内置库Asyncio支持原生协程,但生态不够完善,基于aiohttp可实现一些小的服务。

6、根据业务场景选择合适的锁

常见的各种锁是有层级的,最底层的两种锁就是互斥锁和自旋锁,其他锁都是基于它们实现的。

互斥锁:当 A 线程取到锁后,互斥锁将被 A 线程独自占有,当 A 没有释放这把锁时,其他线程的取锁代码都会被阻塞。(阻塞是由操作系统内核提供的信号量实现的)

互斥锁的线程切换由内核完成,简化了业务代码使用锁的难度。但是为每个请求单独开一个线程是不现实的,线程主动进入休眠是高并发服务无法容忍的行为,这让其他异步请求都无法执行。如果你能确定被锁住的代码执行时间很短,就应该用自旋锁取代互斥锁。当取不到锁时,互斥锁用“线程切换”来面对,自旋锁则用“忙等待”来面对。

自旋锁:自旋锁比互斥锁快得多,它通过 CPU 提供的 CAS 函数在用户态代码中完成加锁与解锁操作。自旋锁开销少,适合资源占用时间短的情况。

代码语言:javascript
代码运行次数:0
运行
复制
# 生产级的自旋锁实现
while (true) {
  //因为判断lock变量的值比CAS操作更快,所以先判断lock再调用CAS效率更高
  if (lock == 0 &&  CAS(lock, 0, pid) == 1) return;
  
  if (CPU_count > 1 ) { //如果是多核CPU,“忙等待”才有意义
      for (n = 1; n < 2048; n <<= 1) {//pause的时间,应当越来越长
        for (i = 0; i < n; i++) pause();//CPU专为自旋锁设计了pause指令
        if (lock == 0 && CAS(lock, 0, pid)) return;//pause后再尝试获取锁
      }
  }
  sched_yield();//单核CPU,或者长时间不能获取到锁,应主动休眠,让出CPU
}

读写锁:如果你能够明确区分出读和写两种场景,而且属于读多写少的情况,可以选择读写锁。它既可以使用互斥锁实现,也可以使用自旋锁实现,我们应根据场景来选择合适的实现。

乐观锁:无论互斥锁、自旋锁还是读写锁,都属于悲观锁。即访问共享资源前,先加上锁。当并发访问共享资源,冲突概率非常低的时候,可以选择乐观锁进行无锁编程。

不管使用哪种锁,锁范围内的代码都应尽量的少,执行速度要快。

系统网络优化

1、一对多通讯提升局域网传输效率

广播:对局域网内所有主机发送消息

组播:对部分主机发送消息

相比于TCP协议需要建立连接,UDP协议无需建立连接,所以我们常用 UDP 协议发送广播。

广播性能高的原因:1、交换机直接转发给接收方,要比从发送方到接收方的传输路径更短。2、原本需要发送方复制多份报文再逐一发送至各个接受者的工作,被交换机完成了,这既分担了发送方的负载,也充分使用了整个网络的带宽。

组播——更精准的定向广播:网络 API 中的 setsockopt 函数可以通过 IGMP 协议,向虚拟组中添加或者删除 IP 地址。当路由器支持 IGMP 协议时,组播就可以跨越多个网络实现更广泛的一对多通讯。

广播和组播能够充分地使用全网带宽,在更关注及时性、对丢包不敏感的流媒体直播中更有应用前景。

2、C10M实现之事件驱动

早些年提到的C10K,是指服务器同时处理1万个TCP连接,近来我们更希望单台服务器的并发能力可以达到 C10M,也就是同时可以处理 1 千万个 TCP 连接。

什么是事件:从网络中接收到一个报文,就可能产生一个事件,进而触发回调函数执行。由于常见的HTTP协议是基于TCP实现的,在TCP报文中有两种事件类型:读事件和写事件。

TCP 连接建立时,会在客户端产生写事件,在服务器端产生读事件。连接关闭时,则会在被动关闭端产生读事件。在连接上收发消息时,也会产生事件,其中发送消息前的写事件与内核分配的缓冲区有关。

获取事件:多路复用epoll 函数使得进程可以高效地收集到事件。

  1. 把需要监控的 socket 传给内核(epoll_ctl 函数),它仅在连接建立等有限的时机调用;
  2. 收集事件(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四次挥手的性能

为什么建立连接是三次握手,而关闭连接需要四次挥手呢?

  • TCP 不允许连接处于半打开状态时就单向传输数据,所以在三次握手建立连接时,服务器会把 ACK 和 SYN 放在一起发给客户端,其中,ACK 用来打开客户端的发送通道,SYN 用来打开服务器的发送通道。
  • 但是当连接处于半关闭状态时,TCP 是允许单向传输数据的。主动方关闭连接时,被动方仍然可以在不调用 close 函数的状态下,长时间发送数据。四次挥手除了彼此确认双向通道关闭,还为服务端状态变成关闭提供异步等待时间。

close 和 shutdown 函数都可以关闭连接,但close 函数会让连接变为孤儿连接,shutdown 函数则允许在半关闭的连接上长时间传输数据。

孤儿连接:主动方close 调用后,哪怕对方(被动方)在半关闭状态下发送的数据到达主动方,进程也无法接收。用 netstat -p 命令,进程名为空。

主动方的优化:

  • 应对丢包:调低 tcp_orphan_retries 控制重发 FIN 报文次数。
代码语言:javascript
代码运行次数:0
运行
复制
net.ipv4.tcp_orphan_retries = 0
  • 如果遇到恶意攻击,FIN 报文无法发出去:调整 tcp_max_orphans(孤儿连接的最大数量) 参数。如果孤儿连接数量大于它,新增的孤儿连接将不再走四次挥手,而是直接发送 RST 复位报文强制关闭。
代码语言:javascript
代码运行次数:0
运行
复制
net.ipv4.tcp_max_orphans = 16384
  • 当接收到 FIN 报文,并返回 ACK 后,主动方的连接进入 TIME_WAIT 状态。为了防止 TIME_WAIT 状态占用太多的资源,tcp_max_tw_buckets 定义了最大数量,超过时连接也会直接释放。
  • 当 TIME_WAIT 状态过多时,还可以通过设置 tcp_tw_reuse 和 tcp_timestamps 为 1 ,将 TIME_WAIT 状态的端口复用于作为客户端的新连接。

被动方优化:

  • 出现大量 CLOSE_WAIT 状态的连接时,应当从应用程序中找问题。
  • 当被动方发送 FIN 报文后,连接就进入 LAST_ACK 状态,在未等来 ACK 时,会在 tcp_orphan_retries 参数的控制下重发 FIN 报文。

5、修改TCP缓冲区兼顾并发数量和传输速度

  • 发送缓冲区自动调整(自动开启):
代码语言:javascript
代码运行次数:0
运行
复制
net.ipv4.tcp_wmem = 4096(动态范围下限)   16384(初始默认值)   4194304(动态范围上限)

一旦发送出的数据被确认,而且没有新的数据要发送,就可以把发送缓冲区的内存释放掉

  • 接收缓冲区自动调整(通过设置net.ipv4.tcp_moderate_rcvbuf = 1开启):
代码语言:javascript
代码运行次数:0
运行
复制
net.ipv4.tcp_moderate_rcvbuf = 1
net.ipv4.tcp_rmem = 4096        87380      6291456

可以依据空闲系统内存的数量来调节接收窗口。如果系统的空闲内存很多,就可以把缓冲区增大一些,这样传给对方的接收窗口也会变大,因而对方的发送速度就会通过增加飞行报文来提升。反之,内存紧张时就会缩小缓冲区,这虽然会减慢速度,但可以保证更多的并发连接正常工作。

  • 接收缓冲区判断内存空闲的方式:
代码语言:javascript
代码运行次数:0
运行
复制
net.ipv4.tcp_mem = 88560     118080    177120

当 TCP 内存小于第 1 个值时,不需要进行自动调节; 在第 1 和第 2 个值之间时,内核开始调节接收缓冲区的大小; 大于第 3 个值时,内核不再为 TCP 分配新内存,此时新连接是无法建立的。

  • 带宽时延积的衡量方式:对网络时延多次取样计算平均值,再乘以带宽。

6、调整TCP拥塞控制性能

ss 命令查看当前拥塞窗口:

代码语言:javascript
代码运行次数:0
运行
复制
# ss -nli|fgrep cwnd
         cubic rto:1000 mss:536 cwnd:10 segs_in:10621866 lastsnd:1716864402 lastrcv:1716864402 lastack:1716864402

ip route change 命令修改初始拥塞窗口:

代码语言:javascript
代码运行次数:0
运行
复制
# ip route | while read r; do
           ip route change $r initcwnd 10;
       done

tcp_available_congestion_control 配置查看内核支持的算法列表:

代码语言:javascript
代码运行次数:0
运行
复制
net.ipv4.tcp_available_congestion_control = cubic reno

tcp_congestion_control 配置选择一个具体的拥塞控制算法:

代码语言:javascript
代码运行次数:0
运行
复制
net.ipv4.tcp_congestion_control = cubic

慢启动、拥塞避免、快速重传、快速恢复,共同构成了拥塞控制算法:

慢启动:拥塞窗口一开始是一个很小的值,然后每 RTT 时间翻倍 拥塞避免:当拥塞窗口达到拥塞阈值(ssthresh)时,拥塞窗口从指数增长变为线性增长。 快速重传:发送端接收到 3 个重复 ACK 时立即进行重传。 快速恢复:当收到三次重复 ACK 时,进入快速恢复阶段,此时拥塞阈值降为之前的一半,然后进入线性增长阶段。

Linux 高版本支持 BBR 算法,可以通过 tcp_congestion_control 配置更改拥塞控制算法。

拥塞控制是控制网络流量的算法,主机间会互相影响,在生产环境更改之前必须经过完善的测试。

本文参考:陶辉《系统性能调优必知必会》

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 才浅coding攻略 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • “文章整理了基础设施和网络两大块调优思路。基础设施从提升单机进程的性能入手,包括高效地使用主机的CPU、内存、磁盘等硬件,通过并发编程提升吞吐量;系统层网络优化传输层网络从降低请求的时延、提升总体吞吐量两个方向尝试优化。让我们开始吧!”
  • 基础设施优化
    • 1、提升 CPU 缓存的命中率
    • 2、提升内存分配效率
    • 3、使用哈希表管理亿级数据
    • 4、磁盘优化技术
    • 5、实现高并发服务的思路
    • 6、根据业务场景选择合适的锁
  • 系统网络优化
    • 1、一对多通讯提升局域网传输效率
    • 3、提升TCP三次握手性能
    • 4、提升TCP四次挥手的性能
    • 5、修改TCP缓冲区兼顾并发数量和传输速度
    • 6、调整TCP拥塞控制性能
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档