《Linux 性能优化实战》第五周--网络性能篇

个人感悟

本周的学习首先了解了Linux网络的工作原理,OSI七层网络模型TCP/IP模型以及网络包的收发流程。应用程序通过Socket接口发送数据包时先要在网络协议栈从上到下逐层处理最终到网卡上发送,接收也要经过网络协议栈从下到上逐层解析,最后送到应用程序。以及网络传输相关性能指标和响应的查看工具。

并且学习了经典的C10K问题,以及延伸的C1000K和C10M问题。这个印象中研究生毕业面试B家的时候被问到过。C10K问题的根源一方面在于系统有限的资源,另一方面,同步阻塞I/O模型以及轮询的套接字接口限制了网络事件的处理效率。目前高性能网络方法都基于epoll。从10K到100K,增加物理资源就能解决,但是到1000K时就需要多方面的优化工作,从硬件中断处理和网络功能卸载、到网络协议栈的文件描述符数量、连接状态跟踪、缓存队列等内核的优化,再到应用程序的工作模型优化,都需要考虑。

再进一步实现10M,就需要用XDP的方式,在内核协议栈之前处理网络包;或者用DPDK直接跳过网络协议栈在用户空间通过轮询的方式直接处理网络包。其中DPDK时目前最主流的高性能网络解决方案,但是需要能支持DPDK的网卡配合使用。

新工具GET

查看网络配置
ifconfig/ip
套接字信息/协议栈统计信息
netstat/ss
网络吞吐量和PPS
sar
带宽
ethtool
连通性和延时
ping
应用层性能
wrk/jmeter
传输层性能
iperf
转发性能
pktgen

捞评论GET

1、客户端的网络环境复杂,出现网络抖动如何分析解决?

在第一个网络出入口记录每次收发消息的内容和具体时间戳(精确到ms),遇到玩家反馈时根据id及发生的大致时间在日志中查找响应记录,看是服务器响应慢还是客户端到服务器的线路慢。可以考虑更多的接入点、专线、CDN等优化公网的链路延迟问题。


接下来是本周读书笔记


Lesson 33/34 关于网络,你必须知道这些

网络模型

开放式系统互联通信参考模型(Open System Interconnection Reference Model),简称OSI网络模型

为了解决网络互联中异构设备的兼容性,并解耦复杂的网络包处理流程,OSI模型把网络互联的框架分为应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层等七层。

  • 应用层,负责为应用程序提供统一的接口
  • 表示层,负责把数据转换成兼容接收系统的格式
  • 会话层,负责维护计算机之间的通信连接
  • 传输层,负责为数据加上传输表头,形成数据包
  • 网络层,负责数据的路由和转发
  • 数据链路层,负责MAC寻址,错误侦测和改错
  • 物理层,负责在物理网络中传输数据帧

在Linux中我们实际上使用的是一个更实用的四层模型,即TCP/IP网络模型。

TCP/IP模型把网络互联的框架分为应用层、传输层、网络层、网络接口层等四层。

  • 应用层,负责向用户提供一组应用程序,如HTTP,FTP,DNS等
  • 传输层,负责端到端的通信,如TCP,UDP
  • 网络层,负责网络包的封装、寻址和路由,如IP,ICMP
  • 网络接口层,负责网络包在物理网络中的传输,比如MAC寻址、错误侦测以及通过网卡传输网络帧等

Linux网络栈

  • 传输层在应用数据前面增加了TCP头
  • 网络层在TCP数据包前增加了IP头
  • 网络接口层在IP数据包前后分别增加了帧头和帧尾

网络接口配置的最大传输单元MTU规定了最大的IP包大小,以太网中MTU默认时1500

Linux网络收发流程

网络包的接收流程

  • 当一个网络帧到达网卡后,网卡通过DMA方式,把网络包放到收包队列中;然后通过硬中断告诉中断处理程序已经接收到了网络包。
  • 网卡中断处理程序为网络帧分配内核数据结构sk_buff,并将其拷贝到sk_buff缓冲区中;再通过软中断通知内核接收到了新的网络帧。
  • 内核协议栈从缓冲区中取出网络帧,并通过网络协议,从上到下处理这个网络帧
    • 链路层检查报文合法性,找出上层协议的类型(IPv4 or IPv6),再去掉帧头和帧尾,交给网络层;
    • 网络层取出IP头,判断网络包的下一步走向,比如是交给上层处理还是转发。当网络层确认这个包发送本机后,取出上层协议的类型(TCP or UDP),去掉IP头再交给传输层处理。
    • 传输层取出TCP/UDP头之后根据<源IP,源端口,目的IP,目的端口>四元组作为标识,找出对应的Socket,并把数据拷贝到Socket接收缓存中
  • 应用程序使用Socket接口读取到新收到的数据

网络包的发送流程

  • 应用程序通过调用Socket API发送网络包
  • 由于这是系统调用,会陷入内核态的套接字层中。套接字层把数据包放到Socket发送缓冲区中
  • 网络协议栈从Socket发送缓冲区中取出数据包,再按照TCP/IP栈,从上到下逐层处理
  • 分片后的网络包再发送到网络接口层,进行物理地址寻址,找到下一跳的MAC地址,然后添加帧头和帧尾,放到发包队列中。这一切完成后会有软中断通知驱动程序
  • 驱动程序通过DMA从发包队列中读出网络帧,并通过物理网卡发送出去

性能指标

  • 带宽, 表示链路的最大传输速率,单位b/s
  • 吞吐量,表示单位时间内成功传输的数据量,单位b/s或者B/s 吞吐量/带宽=网络使用率
  • 延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。
    • 建立连接需要的时间, TCP握手延时
    • 一个数据包往返所需的时间,RTT
  • PPS,Packet Per Second,表示以网络包为单位的传输速率。通常用来评估网络的转发能力

此外,网络的可用性、并发连接数、丢包率、重传率也是常用的性能指标。

网络配置

ifconfig eth0
ip -s addr show dev eth0
  • 网络接口的状态标志, ifconfig输出中的RUNNING,ip输出中的LOWER_UP,都表示物理网络是联通的
  • MTU大小
  • 网络接口的IP地址、子网、以及MAC地址
  • 网络收发的字节数、包数、错误数以及丢包情况,特别是TX和RX部分的errors、dropped、overruns、carrier以及collisions等指标不为0时,通常表示出现了网络I/O问题
    • errors表示发生错误的数据包数,比如校验错误、帧同步错误等
    • dropped表示丢弃的数据包数,即数据包已经收到了Ring Buffer,但是因为内存不足等原因丢包
    • overruns表示超限数据包数,即网络I/O速度过快,导致RingBuffer中的数据包来不及处理导致的丢包
    • carrier表示发生carrier错误的数据包数,比如双工模式不匹配、物理电缆出问题
    • collisions表示碰撞数据包数

套接字信息

netstat -nlp | head -n 3
ss -ltnp | head -n 3

netstat和ss用来查看套接字、网络栈、网络接口以及路由表的信息。其中Recv-Q和Send-Q信息需要特别关注,如果不是0的话说明有网络包的堆积发生。

当Socket处于Established时,Recv-Q表示套接字缓冲中还没有被应用取走的字节数,Send-Q表示还没有被远端主机确认的字节数。
当Socket处于Listening时,Recv-Q表示全连接队列的长度,Send-Q表示全连接队列的最大长度。

协议栈统计信息

netstat -s 
ss -s

吞吐量和PPS

sar -n DEV 1 

带宽可以用ethtool来查询

ethtool eth0 | grep Speed

连通性和延时

ping -c3 XXX.XXX.XXX.XXX

Lesson 35 基础篇:C10K和C1000K回顾

C10K问题最早由Dan Kegel在1999年提出,那是服务器还是32位系统,运行Linux2.2版本,只配置的很少的内存(2G)和千兆网卡。怎样在这样的系统中支持并发1万的请求?

从资源上说,2G内存和千兆网卡服务器,同时处理1w请求,只要每个请求处理占用不超200KB内存和100Kbit的网络带宽就可以。所以物理资源充足,接下来时软件的问题。

如果每个请求分配一个进程/线程,1w个请求会涉及1w个进程/线程的调度、上下文切换乃至它们占用的内存都会成为瓶颈。

  • 怎样在一个线程内处理多个请求?非阻塞I/O或者异步I/O?
  • 怎么更节省资源地处理用户请求?用最少的线程来服务这些请求?

I/O模型优化

I/O事件的通知方式:

  • 水平触发:只要文件描述符可以非阻塞的执行I/O,就会触发通知
  • 边缘出发:只有在文件描述符发生改变时(I/O请求到达时),才发送一次通知。

I/O多路复用的方法:

  • 使用非阻塞I/O和水平触发通知,比如使用select和poll
    • select和 poll从文件描述符列表中,找出哪些可以执行IO,然后进行真正的网络I/O读写。由于I/O是非阻塞的,一个线程中就可以同时监控一批套接字的文件描述符,达到了单线程处理多请求的目的。
    • 优点:对程序友好,API简单。
    • 缺点:需要对文件描述符列表轮询,请求多时较为耗时,且select和poll还有一些限制。以及应用程序每次调用select和poll时还需要把文件描述符的集合从用户空间传入内核空间,由内核修改后再传回用户空间。增加了处理成本。
  • 使用非阻塞I/O和边缘触发通知,如epoll
    • epoll使用红黑树在内核中管理文件描述符的集合,使用事件驱动的机制,只关注有I/O事件发生的文件描述符,不需要轮询整个集合
    • epoll在Linux2.6之后提供,由于边缘触发只在文件描述符可读或可写事件发生时才通知,应用程序需要尽可能多地执行I/O并要处理更多的异常事件
  • 使用异步I/O
    • 异步I/O也是在Linux2.6后提供,和直观逻辑不太一样,使用时要小心设计,难度较高

工作模型优化

  • 主进程+多个worker子进程
    • 主进程执行bind()+listen()后创建多个子进程
    • 每个子进程中都通过accept()和epoll_wait()来处理相同的套接字
    • Nginx就是采取这种模式,主进程用来初始化套接字并管理子进程的生命周期,worker进程用来负责实际的请求处理
    • accept和epoll_wait调用存在一个惊群问题,当网络I/O事件发生时多个进程被同时唤醒,但实际上只有一个进程来响应事件,其他被唤醒的进程都会重新休眠。
      • accept惊群问题在Linux2.6中解决了
      • epoll_wait到Linux4.5才通过EPOLLEXCLUSIVE解决
      • nginx通过在worker进程中增加一个全局锁来解决,worker进程首先要竞争到锁,然后才加入到epoll中,确保只有一个worker子进程被唤醒
  • 监听相同端口的多进程模型
    • 所有进程都监听相同的接口,并且开启SO_REUSEPORT选项,由内核将请求负载均衡到这些监听进程中
    • 不会存在惊群问题,Nginx1.9.1中支持该模式,SO_REUSEPORT选项在Linux3.9以上版本才有

C1000K

基于I/O多路复用和请求处理的优化,C10K问题很容易解决,那么C1000K呢?

100万个请求需要大量的系统资源

  • 假设一个请求16KB,需要15GB内存
  • 带宽上来看,假设只有20%的活跃连接,即使每个连接只需要1KB/s的吞吐量,总共也需要1.6Gb/s的吞吐量。因此还需要配置万兆网卡,或者基于多网卡bonding承载更大的吞吐量。

C1000K的解决方法,本质上还是构建在epoll的非阻塞I/O模型上,只不过除了I/O模型外还需要从应用程序到Linux内核,再到CPU、内存和网络各个层次的深度优化,特别是需要借助硬件来卸载哪些通过软件处理的大量功能。

C10M

同时处理1000w条请求呢?在C1000K时各种软件硬件的优化可能已经做到极致了,此时无论怎么优化应用程序和内核中各种网络参数,想实现1000万请求的并发都是及其困难的。

究其根本,还是Linux内核协议栈做了太多太多繁重的工作,从网卡中断带来的硬中断处理程序开始到软中断中的各层网络协议处理,最后再到应用程序,这个路径太长导致网络包的处理优化到一定程度后就无法再进一步。

要解决这个问题,就要跳过内核协议栈的冗长路径,把网络包直接发送到要处理的应用程序那里。

  • DPDK,用户态网络的标准,跳过内核协议栈直接由用户进程通过轮询方式处理网络接收。 还通过大页、CPU绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
  • XDP,Linux内核提供的一种高性能网络数据路径。它允许网络包在进入内核协议栈之前就进行处理,也可以带来更高的性能。XDP底层也是基于Linux内核的eBPF机制实现的。

Lesson 36 套路篇:怎么评估系统的网络性能?

各协议层的性能测试

转发性能

网络接口和网络层,主要负责网络包的封装、寻址、路由以及发送和接收。在这两个网络协议中,每秒可处理的网络包数PPS就是最重要的性能指标。特别是64B小包的处理能力,值得我们特别关注。如何来测试网络包的处理能力呢?

Linux内核自带的高性能网络测试工具pktgen,但是并不能直接找到pktgen命令,需要加载pktgen内核模块后,再通过/proc文件系统来交互

modprobe pktgen
ps -ef | grep pktgen | grep -v grep
ls /proc/net/pktgen/

TCP/UDP性能

iperf
netperf

HTTP性能

在应用层,有的应用程序会直接基于TCP或UDP构建服务,也有大量的应用基于应用层的协议来构建服务。HTTP就是一个最常用的应用层协议,要测试HTTP性能可以通过ab、webbench等。

ab
webbench

应用负载性能

wrk
TCPCopy
Jmeter

Lesson 36 套路篇:怎么评估系统的网络性能

上节课学习了C10M的解决方案,不过在大多数场景下,我们并不需要单机并发1000万请求。通过调整系统架构,把请求分发到多台服务器中并行处理,才是更简单、扩展性更好的方案。

这就需要我们评估系统的网络性能,以便考察系统的处理能力,并为容量规划提供基准数据。

性能指标回顾

带宽、吞吐量、延时、PPS,这四个性能指标中带宽跟物理网卡配置直接关联;Linux服务器的网络吞吐量一般会比带宽小,交换机等专门的网络设备吞吐量一般接近带宽;PPS以网络包为单位的网络传输速率,通常用在需要大量转发的场景中;对于TCP或者Web服务来说通常会用并发连接数和每秒请求数QPS等指标。

网络基准测试

在测试之前需要弄清楚需要测试的应用程序基于协议栈的哪一层?

  • 基于HTTP和HTTPS的Web应用程序,属于应用层,需要测试HTTP/HTTPS的性能;
  • 大多数游戏服务器,为了支持更大的在线人数。通常会基于TCP/UDP与客户端交互,需要测试TCP/UDP性能
  • 还有一些场景将Linux作为一个软交换机或者路由器来使用,此时要更关注网络包的处理能力。即PPS,关注网络层的转发能力。

各协议层性能测试

转发性能

网络接口层和网络层,主要负责网络包的封装、寻址、路由以及发送和接收。这里最重要的性能指标就是PPS每秒可处理的网络包数。

可以用hping3或者pktgen来测试网络包处理能力。其中pktgen作为一个Linux内核自带的高性能网络测试工具,需要加载pktgen内核模块后再通过/proc文件系统交互。

modprobe pktgen
ls /proc/net/pktgen

在测试时,需要先给每个内核线程kpktgend_X以及测试网卡,配置pktgen选项。再通过pgctrl启动测试。

假设发包及其使用网卡eth0,目标机器的IP为192.168.0.30, MAC地址为11:11:11:11:11:11

#define function for test options
function pgset() {
    local result
    echo $1 > $PGDEV
    result=`cat $PGDEV | fgrep "Result: OK:"`

    if [ "$result" = "" ]; then 
        cat $PGDEV | fgrep Result:
    fi
} 

#bind eth0 for thread 0
PGDEV=/proc/net/pktgen/eht0
pgset "count 1000000" #total packages
pgset "delay 5000"
pgset "clone_skb 0"
pgset "pkt_size 64"
pgset "dst 192.168.0.30"
pgset "dst_mac 11.11.11.11.11.11"

#Start test
PGDEV=/proc/net/pktgen/pgctrl
pgset "start"

#Check result
cat /proc/net/pktgen/eth0

TCP/UDP性能

iperf
netperf

HTTP性能

ab -c 1000 -n 10000 http://www.baidu.com
webbench

应用负载性能

wrk TCPCopy Jmeter LoadRunner
工作中用过Jmeter进行测试