深入理解Linux网络如何从网卡到达用户进程的?

张燕飞,腾讯搜狗十年资深员工,畅销书《深入了解Linux网络》作者,运营表演领域知名技术公众号《开发内功》 “

在张燕飞老师的《深入理解Linux网络》一书中,很多与内核网络模块相关的问题都进行了深入的讨论。讨论了网络数据包如何从网卡到达用户进程,谈到同步阻塞和多路复用epoll,并详细讨论了数据是如何从进程中发送出去的,以及一系列深入的网络工作原理。

这本书在首发当天就成为了京东科技销售日的冠军。它在发布仅三周后就已经开始了第三次印刷。可以说是很受欢迎了。如果你能读到这本书,你就会像跑丁一样。从今天开始,我们将不再看到整个 Linux(整头牛),而是内核的内部模块(肌肉纹理)。 .

对网络有深入了解后,有哪些优化可用于性能?以下是开发或运维中性能优化的一些建议。这些建议都是从书中摘录的。但需要注意的是,每种性能优化方法都有适合与不适合的应用场景。您应该根据您当前的项目状态灵活选择使用或不使用它。

建议一:尽量减少不必要的网络IO

我要给出的第一条建议是在不必要的时候尽可能多地使用网络 IO。

是的,互联网在现代互联网世界中发挥着重要作用。用户通过网络请求在线服务,服务器通过网络读取数据库中的数据,通过网络构建一个极其强大的分布式系统。网络很好,可以降低模块开发的难度linux服务器ip映射到外网,也可以用来搭建更强大的系统。但这不是你滥用它的理由!

原因是即使原生网络 IO 开销仍然很大。首先,要发送一个网络数据包,必须先从用户态切换到内核态,这需要系统调用的开销。进入内核后,要经过一个冗长的协议栈,会占用大量的CPU周期,最后进入环回设备的“驱动程序”。在接收端,软中断占用大量的CPU周期,必须由接收协议栈处理,最后唤醒或通知用户进程处理。当服务器完成处理后,它必须再次发送结果。您必须再次执行此操作,最后您的流程才能收到结果。你说麻烦不是麻烦。另一个问题是多个进程协同完成一个作业,必然会引入更多的进程上下文切换开销。从开发的角度来看,这些开销其实是没有用的。

我们上面也分析的只是本地网络IO。如果是跨机,肯定有两个网卡的DMA拷贝过程,以及两端之间的网络RTT耗时延迟。所以,网络虽好,但不能随意滥用!

建议2:尽可能整合网络请求

如果可能的话,将多个网络请求合二为一,既节省了两端的CPU开销,又减少了多个RTT带来的时间消耗。

举个实际例子可能会更好理解。如果有redis,里面存储了每个App的信息(应用名、包名、版本、截图等)。现在需要根据用户已安装的应用列表查询数据库中哪些应用比用户版本更新,如果有的话提醒用户更新。

那最好不要这样写代码:

<?php 
for(安装列表 as 包名){
  redis->get(包名)
  ...
}

上面的代码在功能上没问题,问题是性能。根据我们的统计,现代用户平均安装的应用程序数量在 60 个左右。这段代码运行时,每次用户发出请求,你的服务器需要用 redis 发出 60 个网络请求。花费的总时间至少为 60 个 RTT。更好的方法是使用redis中提供的批量获取命令,如hmget、pipeline等,一次网络IO后获取所有想要的数据,如图。

图片[1]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

建议3:调用者和被调用机器尽量靠近部署

在上一章中,我们看到在握手没问题的情况下,TCP握手的时间基本上取决于两台机器之间的RTT时间。虽然我们无法完全消除这种耗时,但我们可以通过将客户端和服务器放置得足够近来减少 RTT。尽量解决本地机房内各机房的数据请求,减少跨区域网络传输。

比如你的服务部署在北京机房,那么你调用的mysql和redis最好位于北京机房。尽量不要跨越千里跑到广东机房索取数据。就算有专线,耗时也会大大增加!机房服务器之间的RTT延迟只有零点几毫秒,同一区域的不同机房之间的RTT延迟大约是1毫秒多一点。但是如果从北京到广东,延迟大概是30-40毫秒,高出几十倍!

建议四:内网调用不要使用外部域名

假设你负责的服务需要调用一个兄弟部门的搜索接口,假设接口为:“http://www.sogou.com/wq?key=Development Internal Skills Cultivation”。

既然是兄弟部门,很有可能这个接口和你的服务部署在同一个机房​​。即使不部署在机房,一般也可以通过专线到达。所以不要直接请求,而应该在公司使用该服务对应的内网域名。在我们公司内部,每个外网服务都会配置一个对应的内网域名,相信你们公司也有。

为什么要这样做,原因如下

1)外网接口慢。本来内网可能通过交换机就可以到达兄弟部门的机器。如果非要绕外网转回来,时间肯定会很慢。

2)高带宽成本。在互联网业务中,除了机器之外,另一个很大的成本是IDC机房的进出带宽成本。无论两台机器在内网如何通信,都不涉及带宽计算。但是你上网一回来就没事,每次进出都要交带宽费,你说不亏! !

3)NAT 单点瓶颈。一般的服务器是没有外网IP的,所以如果想从外网请求资源,就必须经过NAT服务器。然而,在公司机房的数千台服务器中,可能只有少数几台可以承担 NAT 的角色。它很容易成为瓶颈。我们的业务遇到过几次NAT失败导致外网请求失败的案例。如果NAT机器宕机了,你的服务也有可能宕机,故障率大大增加。

建议5:调整网卡RingBuffer大小

在Linux的整个网络栈中,RingBuffer扮演着一个任务发送和接收中继站的角色。对于接收进程,网卡负责将接收到的数据帧写入RingBuffer,由ksoftirqd内核线程负责取出处理。只要ksoftirqd线程工作的足够快,RingBuffer作为中转站是没有问题的。

但是让我们想象一下,如果在某个时刻,数据包太多,而 ksoftirqd 处理不了,会发生什么?这时候RingBuffer可能瞬间就被填满了,后面来的分组网卡不做任何处理就被丢弃了!

RingBuffer的“中转仓库”的大小可以通过ethtool增加。 .

# ethtool -G eth1 rx 4096 tx 4096

这样网卡会被分配一个更大的“中转站”,可以解决偶尔的瞬时丢包。但是,这种方法有一个小的副作用,就是排队的数据包过多会增加处理网络数据包的延迟。所以最好让内核更快地处理网络数据包,而不是让网络数据包愚蠢地在 RingBuffer 中排队。我们稍后会介绍 RSS,它允许更多的核参与网络数据包的接收。

建议 6:减少内存副本

如果要将文件发送到另一台机器,基本方法是调用read读取文件,然后调用send发送数据。这样,需要在内核态内存和用户态内存之间频繁地复制数据,如图9.6.

图片[2]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

目前减少内存拷贝的方式主要有两种,分别是使用mmap和sendfile系统调用。如果使用 mmap 系统调用,则映射地址空间的内存在用户态和内核态都可以使用。如果你发送的数据是mmap映射的数据,内核可以直接从地址空间读取,省去了一个从内核态到用户态的拷贝过程。

图片[3]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

但是,在mmap发送文件的方式上,系统调用的开销并没有减少,而且发生了内核态和用户态的两次上下文切换。如果你只是想发送一个文件而不关心它的内容,你可以调用另一个更极端的系统调用——sendfile。在这个系统调用中,文件的读取和发送完全结合在一起,再次节省了系统调用的开销。结合大多数网卡支持的“Scatter-gather”DMA功能。可以直接从PageCache缓冲区DMA复制到网卡。这样可以节省大部分 CPU 复制操作。

图片[4]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

建议 7:使用 eBPF 绕过栈的原生 IO

如果您的业务涉及大量原生网络 IO,请考虑此优化。与本地网络IO和跨机IO相比,确实节省了一些驱动开销。发送数据不需要进入RingBuffer的驱动队列,直接将skb传给接收协议栈(通过软中断)。但是在内核的其他组件中,并没有什么遗漏,系统调用、协议栈(传输层、网络层等)、设备子系统都被一遍遍的走了一遍。甚至连“驱动”程序都没有了(虽然这个驱动只是环回设备的纯软件虚拟东西)。

如果你想使用原生网络 IO,但又不想频繁地绕过协议栈。然后你可以试试 eBPF。使用 eBPF 的 sockmap 和 sk 重定向可以绕过 TCP/IP 协议栈,直接发送到接收方的套接字。业内已经有公司这样做了。

图片[5]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

建议8:尽量少用recvfrom等进程阻塞方法

当使用 recvfrom 阻塞方法在套接字上接收数据时。每次进程在套接字上等待数据时,都必须将其从 CPU 中取出。然后切换到另一个进程。当数据准备好后,休眠的进程会再次被唤醒。总共有两个进程上下文切换开销。如果我们需要在我们的服务器上处理大量的用户请求,我们需要有很多进程并且不断切换。缺点如下:

您可能认为这种网络 IO 模型很少见。但实际上,这种方式在很多传统的客户端SDK中,如mysql、redis、kafka等,依然在使用。

建议 9:使用成熟的网络库

使用epoll高效管理大量socket。在服务器端。我们有各种成熟的网络库可供使用。这些网络库都对 epoll 使用了不同级别的封装。

首先,大家第一个参考的就是Redis。在旧版本的 Redis 中,单个进程可以高效地使用 epoll 来支持每秒数万 QPS 的高性能。如果你的服务是单进程的,可以参考网络IO部分的Redis源码。

如果是多线程的话linux服务器ip映射到外网,线程之间的分工方式有很多种。那么哪个线程负责等待读IO事件,哪个线程负责处理用户请求,哪个线程负责写返回给用户。根据分工的不同,衍生出单Reactor、多Reactor、Proactor等多种模式。不用头疼,只要明白这些原理,就可以选择性能不错的网络库。比如PHP中的Swoole,Golang中的net package,Java中的netty,C++中的Sogou Workflow都封装的很好。

建议 10:使用新的 Kernel-ByPass 技术

如果你的服务对网络的要求真的很高,而且各种优化手段都用过,那么优化的绝招就是——Kernel-ByPass技术。

内核在接收网络数据包时要经过很长的发送和接收路径。这期间涉及到很多内核组件之间的协调,协议栈的处理,内核态和用户态的复制和切换。 Kernel-ByPass等技术方案绕过内核协议栈,实现用户态网络数据包的收发。这样既避免了内核协议栈的复杂处理,又减少了内核态和用户态之间频繁的拷贝和切换,性能将得到最大化的发挥!

目前我知道的解决方案包括SOLARFLARE的软硬件解决方案,DPDK等。有兴趣的可以了解一下!

图片[6]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

建议 11:配置适当的端口范围

客户端调用connect系统调用发起连接时,需要先选择一个可用的端口。当内核选择一个端口时,它会从可用端口范围内的一个随机位置开始遍历。如果没有足够的端口,内核可能需要循环多次才能选择一个可用的端口。这也会导致更多的 CPU 周期花费在内部哈希表查找和可能的自旋锁等待上。所以,不要等到端口耗尽报错才增加端口范围,从一开始就应该保持一个相对足够的值。

如果端口大小仍然不够,可以考虑启用端口复用和回收。这样,端口在连接断开时不需要等待2MSL,可以快速恢复。在启用该参数之前,请确保已启用 tcp_timestamps。

# vi /etc/sysctl.conf
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tw_recycle = 1
# sysctl -p

建议 12:小心连接队列溢出

服务器使用两个连接队列来响应来自客户端的握手请求。这两个队列的长度是在服务器监听的时候确定的。如果发生溢出,很可能会丢包。所以如果你的业务使用的是短连接,流量比较大,就要学会观察两个队列是否溢出。因为一旦出现连接队列导致的握手问题,TCP连接时间就超过了秒。

对于半加入队列,有一个简单的解决方案。即只要保证内核参数tcp_syncookies为1,就保证不会因为半连接队列满而丢包。

对于全连接队列,可以用 netstat -s 观察。 netstat -s 可以查看当前系统连接队列满造成的丢包统计。但是这个数字记录的是丢包的总数,所以需要使用watch命令来动态监控。

# watch 'netstat -s | grep overflowed' 
160 times the listen queue of a socket overflowed //全连接队列满导致的丢包

如果在你的监控过程中输出的数量发生变化,说明当前服务器出现了满连接队列满导致的丢包。您需要增加完全连接队列的长度。全连接队列是应用调用listen时传入的backlog和内核参数net.core.somaxconn中较小的一个。如果需要增加,可能需要更改这两个参数。

如果你手头没有服务器的权限,只是发现你的客户端连接到某台服务器需要很长时间,你想看看是不是因为服务器的问题握手队列。还有一种间接的方式,可以使用tcpdump抓包,看看是否有SYN TCP Retransmission。如果偶尔出现TCP重传,说明对应的服务器连接队列可能有问题。

建议 13:减少握手重试次数

在6.5中我们看到如果握手异常,客户端或服务器会启动超时重传机制。这个超时重试间隔加倍,1秒、3秒、7秒、15秒、31秒、63秒……。对于我们提供用户直接访问的接口,第一次重试需要1秒以上,严重影响了用户体验。如果第三次重试,很有可能是某个链接报错,返回504。因此,在这种应用场景下,维持这么多超时没有任何意义。最好将它们设置得更小并尽快放弃。客户端的syn重传次数由tcp_syn_retries控制,服务器半连接队列的超时次数由tcp_synack_retries控制。将它们都调整到您想要的值。

建议14:如果请求频繁,请放弃短连接,使用长连接

如果你的服务器频繁请求一个服务器,比如redis缓存。比建议 1 稍好一点的方法是使用持久连接。这些好处包括

1)节省握手开销。短连接中的每个请求都需要在服务和缓存之间进行一次握手,因此用户每次都必须等待额外的握手时间开销。

2) 规避了队列满的问题。前面我们看到当全连接或半连接队列溢出时,服务器直接丢包。客户端不知道,所以它只是等待 3 秒再重试。请注意,tcp 本身并不是专门为 Internet 服务设计的。这 3 秒的超时对互联网用户的体验来说是致命的。

3)端口数量不是问题。在终端连接中,释放连接时,客户端使用的端口需要进入TIME_WAIT状态,等待2个MSL时间释放。因此,如果连接频繁,端口数量很容易不足。对于长连接,几十或几百个端口就足够了。

建议15:TIME_WAIT的优化

如果使用短连接,许多在线服务将有很多 TIME_WAIT。

首先我想说的是,当你看到 20,000 或 30,000 TIME_WAITs 时,没有必要恐慌。从内存的角度来看,一个TIME_WAIT状态的连接只有0.5KB的内存。从端口占用情况来看,确实是消耗了一个端口。但如果下次连接到不同的服务器,该端口仍然可以使用。只有当所有 TIME_WAIT 都聚集在与服务器的连接上时才会出现问题。

如何解决?其实方法很多。第一种方法是启用端口重用和回收,如上所述。第二种解决方法是限制TIME_WAIT状态下的最大连接数。

# vi /etc/sysctl.conf
net.ipv4.tcp_max_tw_buckets = 32768
# sysctl -p

如果你更彻底,你可以简单地使用长连接而不是频繁的短连接。连接频率大大降低后,自然就没有TIME_WAIT的问题了。

结束

现代互联网服务基本都是海量请求,这种情况下难免会遇到各种网络问题。

8月2日,张艳飞先生将与新书《理解Linux网络》的策展人姚新军携手,共同探讨这些问题背后最深层次的原理。同时,我也会和大家聊一聊热门新书《理解Linux网络》诞生背后鲜为人知的故事。感兴趣的朋友赶紧扫描下方海报二维码预约观看直播吧!

图片[7]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

同时,张燕飞先生还将出席8月19-20日在深圳举行的K+全球软件研发产业创新峰会。扫描下方海报二维码或点击“阅读原文”了解峰会详情!

图片[8]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

点击这里↓↓↓记得关注星星哦~


图片[9]-深入理解Linux网络如何从网卡到达用户进程的?-唐朝资源网

© 版权声明
THE END
喜欢就支持一下吧
点赞258赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容