关于前端:性能优化如何更快地接收数据

4次阅读

共计 9021 个字符,预计需要花费 23 分钟才能阅读完成。

从网卡到应用程序,数据包会通过一系列组件,其中驱动做了什么?内核做了什么?为了优化,咱们又能做些什么?整个过程中波及到诸多轻微可调的软硬件参数,并且相互影响,不存在一劳永逸的“银弹”。本文中又拍云零碎开发高级工程师杨鹏将联合本人的的实践经验,介绍在深刻了解底层机制的根底上如何做出“场景化”的最优配置。

文章依据杨鹏在又拍云 Open Talk 技术沙龙北京站主题演讲《性能优化:更快地接收数据》整顿而成,现场视频及 PPT 可点击浏览原文查看。

大家好,我是又拍云开发工程师杨鹏,在又拍云工作已有四年工夫,期间始终从事 CDN 底层零碎开发的工作,负责调度、缓存、负载平衡等 CDN 的外围组件,很快乐来跟大家分享在网络数据处理方面的教训和感触。明天分享的主题是《如何更快地接收数据》,次要介绍减速网络数据处理的办法和实际。心愿能帮忙大家更好的理解如何在零碎的层面,尽量在应用程序无感的状况下做到极致的优化。言归正传,进入主题。

首先须要分明在尝试做任何优化的时候,想到的第一件事件应该是什么?集体感觉是掂量指标。做任何改变或优化之前,都要明确地晓得,是怎么的指标反映出了以后的问题。那么在做了相应的调整或改变之后,也能力通过指标去验证实际效果与作用。

针对要分享的主题,有一个围绕下面指标外围的根本准则。在网络层面做优化,归根结底只须要看一点,如果能够做到网络栈的每个档次,退出能监控到对应档次的丢包率,这样外围的指标,就能够明确地晓得问题出在哪一层。有了明确可监控的指标,之后做相应的调整与实际效果的验证也就很简略了。当然上述两点绝对有点虚,接下来就是比拟干的局部了。

如上图所示,当收到一个数据包,从进入网卡,始终达到应用层,总的数据流程有很多。在以后阶段,无需关注每个流程,注意其中几个外围的要害门路即可:

  • 第一个,数据包达到网卡;
  • 第二个,网卡在收到数据包时,它须要产生一个中断,通知 CPU 数据曾经到了;
  • 第三步,内核从这个时候开始进行接管,把数据从网卡中拿进去,交到前面内核的协定栈去解决。

以上是三个要害的门路。上图中左边的手绘图指的就是这三个步骤,并无意辨别了两个色彩。之所以这么辨别是因为 接下来会按这两局部进行分享,一是下层驱动局部,二是上层波及到内核的局部。 当然内核比拟多,通篇只波及到内核网络子系统,更具体来说是内核跟驱动交互局部的内容。

网卡驱动

网卡驱动的局部,网卡是硬件,驱动(driver)是软件,包含了网卡驱动局部的大部分。这部分可简略分四个点,顺次是初始化、启动、监控与调优驱动它的初始化流程。

网卡驱动 - 初始化

驱动初始化的过程和硬件相干,无需过分关注。但需注意一点就是注册 ethool 的一系列操作,这个工具能够对网卡做各种各样的操作,不止能够读取网卡的配置,还能够更改网卡的配置参数,是一个十分弱小的工具。

那它是如何管制网卡的呢?每个网卡的驱动在初始化时,通过接口,去注册反对 ethool 工具的一系列操作。ethool 是一套很通用的接口,比如说它反对 100 个性能,但每个型号的网卡,只能反对一个子集。所以具体反对哪些性能,会在这一步进行申明。

上图截取的局部,是在初始化时构造体的赋值。后面两个能够简略看一下,驱动在初始化的时候会通知内核,如果想要操作这块网卡对应的回调函数,其中最次要的是启动和敞开,有用 ifconfig 工具操作网卡的应该都很相熟,当用 ifconfig up/down 一张网卡的时候,调用的都是它初始化时指定的这几个函数。

网卡驱动 - 启动

驱动初始化过程之后就是启动(open)中的流程了,一共分为四步:调配 rx/tx 队列内存、

开启 NAPI、注册中断处理函数、开启中断。其中注册中断处理函数和开启中断是天经地义的,任何一个硬件接入到机器上都须要做这个操作。当前面收到一些事件时,它须要通过中断去告诉零碎,而后开启中断。

第二步的 NAPI 前面会具体阐明,这里先重点关注启动过程中对内存的调配。网卡在收到数据时,都必须把数据从链路层拷贝到机器的内存里,而这块内存就是网卡在启动时,通过接口向内核、向操作系统申请而来的。内存一旦申请下来,地址确定之后,后续网卡在收到数据的时候,就能够间接通过 DMA 的机制,间接把数据包传送到内存固定的地址中去,甚至不须要 CPU 的参加。

到队列内存的调配能够看下上图,很早之前的网卡都是单队列的机制,但古代的网卡大多都是多队列的。益处就是机器网卡的数据接管能够被负载平衡到多个 CPU 上,因而会提供多个队列,这里先有个概念前面会具体阐明。

上面来具体介绍启动过程中的第二步 NAPI,这是古代网络数据包解决框架中十分重要的一个扩大。之所以当初能反对 10G、20G、25G 等十分高速的网卡,NAPI 机制起到了十分大的作用。当然 NAPI 并不简单,其外围就两点:中断、轮循。一般来说,网卡在接收数据时必定是收一个包,产生一个中断,而后在中断处理函数的时候将包解决掉。处在收包、解决中断,下一个收包,再解决中断,这样的循环中。而 NAPI 机制劣势在于只须要一次中断,收到之后就能够通过轮循的形式,把队列内存中所有的数据都拿走,达到十分高效的状态。

网卡驱动 - 监控

接下来就是在驱动这层能够做的监控了,须要去关注其中一些数据的起源。


$ sudo ethtool -S eth0
NIC statistics:
     rx_packets: 597028087
     tx_packets: 5924278060
     rx_bytes: 112643393747
     tx_bytes: 990080156714
     rx_broadcast: 96
     tx_broadcast: 116
     rx_multicast:20294528
     .... 

首先十分重要的是 ethool 工具,它能够拿到网卡中统计的数据、接管的包数量、解决的流量等等惯例的信息,而咱们更多的是须要关注到异样信息。


$ cat /sys/class/net/eth0/statistics/rx_dropped
2

通过 sysfs 的接口,能够看到网卡的丢包数,这就是零碎出现异常的一个标记。

三个路径拿到的信息与后面差不多,只是格局有些乱,仅做理解即可。

上图是要分享的一个线上案例。过后业务上出现异常,通过排查最初是狐疑到网卡这层,为此须要做进一步的剖析。通过 ifconfig 工具能够很直观的查看到网卡的一些统计数据,图中能够看到网卡的 errors 数据指标十分高,显著呈现了问题。但更有意思的一点是,errors 左边最初的 frame 指标数值跟它完全相同。因为 errors 指标是网卡中很多谬误累加之后的指标,与它相邻的 dropped、overruns 这俩个指标都是零,也就是说在过后的状态下,网卡的谬误大部分来自 frame。

当然这只是刹时的状态,上图中上面局部是监控数据,能够显著看到稳定的变动,的确是某一台机器异样了。frame 谬误个别是在网卡收到数据包,进行 RCR 校验时失败导致的。当收到数据包,会对该包中的内容做校验,当发现跟曾经存下来的校验不匹配,阐明包是损坏的,因而会间接将其丢掉。

这个起因是比拟好剖析的,两点一线,机器的网卡通过网线接到上联交换机。当这里呈现问题,不是网线就是机器自身的网卡问题,或者是对端交换机的端口,也就是上联交换机端口呈现问题。当然按第一优先级去剖析,协调运维去更换了机器对应的网线,前面的指标状况也反映出了成果,指标间接突降直到齐全隐没,谬误也就不复存在了,对应下层的业务也很快复原了失常。

网卡驱动 - 调优

说完监控之后来看下最初的调优。在这个层面能调整的货色不多,次要是针对网卡多队列的调整,比拟直观。调整队列数目、大小,各队列间的权重,甚至是调整哈希的字段,都是能够的。

$ sudo ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:   0
TX:   0
Other:    0
Combined: 8
Current hardware settings:
RX:   0
TX:   0
Other:    0
Combined: 4

上图是针对多队列的调整。为了阐明方才的概念,举个例子,比方有个 web server 绑定到了 CPU2,而机器有多个 CPU,这个机器的网卡也是多队列的,其中某个队列会被 CPU2 解决。这个时候就会有一个问题,因为网卡有多个队列,所以 80 端口的流量只会被调配到其中一个队列下来。如果这个队列不是由 CPU2 解决的,就会波及到一些数据的腾挪。底层把数据接管上来后再交给应用层的时候,须要把这个数据挪动一下。如果原本在 CPU1 解决的,须要挪到 CPU2 去,这时会波及到 CPU cache 的生效,这对高速运转的 CPU 来说是代价很高的操作。

那么该怎么做呢?咱们能够通过后面提到的工具,特意把 80 端口 tcp 数据流量导向到对应 CPU2 解决的网卡队列。这么做的成果是数据包从达到网卡开始,到内核解决完再到送达应用层,都是同一个 CPU。这样最大的益处就是缓存,CPU 的 cache 始终是热的,如此整体下来,它的提早、成果也会十分好。当然这个例子并不理论,次要是为了阐明能做到的一个成果。

内核网络子系统

说完了整个网卡驱动局部,接下来是解说内核子系统局部,这块会分为软中断与网络子系统初始化两局部来分享。

软中断

上图的 NETDEV 是 linux 网络子系统每年都会开的一个分会,其中比拟有意思的点是每年大会举办的届数会以一个特殊字符来示意。图中是办到了 0X15 届,想必也都发现这是 16 进制的数字,0X15 刚好就是 21 年,也是比拟极客范。对网络子系统感兴趣的能够去关注一下。

言归正传,内核延时工作有多种机制,而软中断只是其中一种。上图是 linux 的根本构造,下层是用户态,两头是内核,上层是硬件,很形象的一个分层。用户态和内核态之间会有两种交互的形式:通过零碎调用,或者通过异样能够陷入到内核态外面。那底层的硬件跟内核又是怎么交互的呢?答案是中断,硬件跟内核交互的时候必须通过中断,解决任何事件都须要产生一个中断信号来告知 CPU 与内核。

不过这样的机制个别状况下兴许没有问题,然而对网络数据来说,一个数据报一个中断,这样会有很显著的两个问题。

问题一:中断在解决期间,会屏蔽之前的中断信号。当一个中断解决的工夫很长,在解决期间收到的中断信号都会丢掉。 如果解决一个包用了十秒,在这十秒期间又收到了五个数据包,但因为中断信号丢了,即使后面的解决完了,前面的数据包也不会再解决了。对应到 tcp 这边,如果客户端给服务端发了一个数据包,几秒后处理完了,但在解决期间客户端又发了后续的三个包,然而服务端前面并不知道,认为只收到了一个包,这时客户端又在期待服务端的回包,如此会导致两边都卡住了,也阐明了信号失落是一个极其重大的问题。

问题二:一个数据包触发一次中断解决的话,当有大量的数据包到来后,就会产生十分大量的中断。 如果达到了 10 万、50 万、甚至百万的 pps,那 CPU 就须要解决大量的网络中断,也就不必干其余事件了。

而针对以上两点问题的解决办法就是让中断解决尽可能的短。 具体来说,不能在中断处理函数,只能把它揪出来,交到软中断机制里。这样之后的理论后果是硬件的中断解决做的事件就很少了,将接收数据等一些必须的事件交到软中断去实现,这也是软中断存在的意义。

static struct smp_hotplug_thread softirq_threads = {
  .store              = &ksoftirqd,
  .thread_should_run  = ksoftirqd_should_run,
  .thread_fn          = run_ksoftirqd,
  .thread-comm        =“ksoftirqd/%u”,
};

static _init int spawn_ksoftirqd(void)
{regiter_cpu_notifier(&cpu_nfb);
  
  BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

  return 0;
}
early_initcall(spawn_ksoftirqd);

软中断机制是通过内核的线程来实现的。图中是对应的一个内核线程。服务器 CPU 都会有一个 ksoftirqd 这样的内核线程,多 CPU 的机器会绝对应的有多个线程。图中构造体最初一个成员 ksoftirqd/,如果有三个 CPU 对应就会有 /0/1/2 三个内核线程。

软中断机制的信息在 softirqs 上面能够看到。软中断并不多只有几种,其中须要关注的,跟网络相干的就是 NET-TX 和 NET-RX,网络数据收发的两种场景。

内核初始化

铺垫完软中断之后,上面来看内核初始化的流程。次要为两步:

  • 针对每个 CPU,创立一个数据结构,这下面挂了十分多的成员,与前面的解决密切相关;
  • 注册一个软中断处理函数,对应下面看到的 NET-TX 和 NET-RX 这两个软中断的处理函数。

上图是手绘的一个数据包的解决流程:

  • 第一步网卡收到了数据包;
  • 第二步把数据包通过 DMA 拷到了内存外面;
  • 第三步产生了一个中断通知 CPU 并开始解决中断。重点的中断解决可分为两步:一是将中断信号屏蔽了,二是唤醒 NAPI 机制。

static irqreturn_t igb_msix_ring(int irq, void *data)
{
  struct igb_q_vector *q_vector = data;
  
  /* Write the ITR value calculated from the previous interrupt. */
  igb_write_itr(q_vector);
  
  napi_schedule(&q_vector->napi);
  
  return IRO_HANDLED;
}

下面的代码是 igb 网卡驱动中断处理函数做的事件。如果省略掉开始的变量申明和前面的返回,这个中断处理函数只有两行代码,十分短。须要关注的是第二个,在硬件中断处理函数中,只用激活内部 NIPA 软中断解决机制,无需做其余任何事件。因而这个中断处理函数会返回的十分快。

NIPI 激活


/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi)
{list_add_tail(&napi->poll_list, &sd->poll_list);
  _raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

NIPI 的激活也很简略,次要为两步。内核网络系统在初始化的时每个 CPU 都会有一个构造体,它会把队列对应的信息插入到构造体的链表里。换句话说,每个网卡队列在收到数据的时候,须要把本人的队列信息通知对应的 CPU,将这两个信息绑定起来,保障某个 CPU 解决某个队列。

除此之外,还要与触发硬中断一样,须要触发软中断。下图将很多步骤放到了一块,后面讲过的就不再赘述了。图中要关注的是软中断是怎么触发的。与硬中断差不多,软中断也有中断的向量表。每个中断号,都会对应一个处理函数,当须要解决某个中断,只须要在对应的中断向量表里找就好了,跟硬中断的解决是截然不同的。

数据接管 - 监控

说完了运作机制,再来看看有哪些地方能够做监控。在 proc 上面有很多货色,能够看到中断的解决状况。第一列就是中断号,每个设施都有独立的中断号,这是写死的。对网络来说只须要关注网卡对应的中断号,图中是 65、66、67、68 等。当然看理论的数字并没有意义,而是须要看它的散布状况,中断是不是被不同 CPU 在解决,如果所有的中断都是被一个 CPU 解决,那么就须要做些调整,把它扩散开。

数据接管 - 调优

中断能够做的调整有两个:一是中断合并,二是中断亲和性。

自适应中断合并

  • rx-usecs: 数据帧达到后,提早多长时间产生中断信号,单位微秒
  • rx-frames: 触发中断前积攒数据帧的最大个数
  • rx-usecs-irq: 如果有中断解决正在执行,以后中断提早多久送达 CPU
  • rx-frames-irq: 如果有中断解决正在执行,最多积攒多少个数据帧

下面列的都是硬件网卡反对的性能。NAPI 实质上也是中断合并的机制,如果有很多包的到来,NAPI 就能够做到只产生一个中断,因而不须要硬件来帮忙做中断合并,实际效果是跟 NAPI 是雷同的,都是缩小了总的中断数量。

中断亲和性

$ sudo bash -c‘echo 1 > /proc/irq/8/smp_affinity’

这个与网卡多队列是密切相关的。如果网卡有多个队列,就能手动来明确指定由哪个 CPU 来解决,平衡的把数据处理的负载扩散到机器的可用 CPU 上。配置也比较简单,只需把数字写入到 /proc 对应的这个文件中就能够了。这是个位数组,转成二进制后就会有对应的 CPU 去解决。如果写个 1,可能就是 CPU0 来解决;如果写个 4,转化成二进制是 100,那么就会交给 CPU2 去解决。

另外有个小问题须要留神,很多发行版可能会自带一个 irqbalance 的守护过程(http://irqbalance.github.io/i…),会将手动中断平衡的设置给笼罩掉。这个程序做的外围事件就是把下面手动设置文件的操作放到程序里,有趣味能够去看下它的代码(https://github.com/Irqbalance…),也是把这个文件关上,写对应的数字进去就能够了。

内核 - 数据处理

最初是数据处理局部了。当数据达到网卡,进入队列内存后,就须要内核从队列内存中将数据拉进去。如果机器的 PPS 达到了十万甚至百万,而 CPU 只解决网络数据的话,那其余根本的业务逻辑也就不必干了,因而不能让数据包的解决独占整个 CPU,而外围点是怎么去做限度。

针对上述问题次要有两方面的限度:整体的限度和单次的限度

while (!list_empty(&sd->poll_list)){
  struct napi_struct *n;
  int work,weight;
  
  /* If softirq window is exhausted then punt.
   * Allow this to run for 2 jiffies since which will allow
   * an average latency of 1.5/HZ.
   */
   if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
   goto softnet_break;

整体限度很好了解,就是一个 CPU 对应一个队列。如果 CPU 的数量比队列数量少,那么一个 CPU 可能须要解决多个队列。

weight = n->weight;

work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) {work = n->poll(n,weight);
        trace_napi_poll(n);
}

WARN_ON_ONCE(work > weight);

budget -= work;

单次限度则是限度一个队列在一轮里解决包的数量。达到限度之后就停下来,期待下一轮的解决。

softnet_break:
  sd->time_squeeze++;
  _raise_softirq_irqoff(NET_RX_SOFTIRQ);
  goto out;

而停下来就是很要害的节点,侥幸的是有对应的指标记录,有 time-squeeze 这样中断的计数,拿到这个信息就能够判断出机器的网络解决是否有瓶颈,被迫中断的频率高下。

上图是监控 CPU 指标的数据,格局很简略,每行对应一个 CPU,数值之间用空格宰割,输入格局为 16 进制。那么每一列数值又代表什么呢?很可怜,这个没有文档,只能通过查看应用的内核版本,而后去看对应的代码。

seq_printf(seq,
     "%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n",
     sd->processed, sd->dropped, sd->time_squeeze, 0,
     0, 0, 0, 0, /* was fastroute */
     sd->cpu_collision, sd->received_rps, flow_limit_count);

上面阐明了文件中每个字段都是怎么来的,理论状况可能会有所不同,因为随着内核版本的迭代,字段的数量以及字段的程序都有可能发生变化,其中与网络数据处理被中断次数相干的就是 squeeze 字段:

  • sd->processed 解决的包数量(多网卡 bond 模式可能多于理论的收包数量)
  • sd->dropped 丢包数量,因为队列满了
  • sd->time_spueeze 软中断解决 net_rx_action 被迫打断的次数
  • sd->cpu_collision 发送数据时获取设施锁抵触,比方多个 CPU 同时发送数据
  • sd->received_rps 以后 CPU 被唤醒的次数(通过处理器间中断)
  • sd->flow_limit_count 触发 flow limit 的次数

下图是业务中遇到相干问题的案例,最初排查到 CPU 层面。图一是 TOP 命令的输入,显示了每个 CPU 的使用量,其中红框标出的 CPU4 的使用率存在着异样,尤其是倒数第二列的 SI 占用达到了 89%。SI 是 softirq 的缩写,示意 CPU 花在软中断解决上的工夫占比,而图中 CPU4 在工夫占比上显著过高。图二则是对应图一的输入后果,CPU4 对应的是第五行,其中第三列数值显著高于其余 CPU,表明它在解决网络数据的时被频繁的打断。

针对下面的问题推断 CPU4 存在肯定的性能消退,兴许是品质不过关或其余的起因。为了验证是否是性能消退,写了一个简略的 python 脚本,一个始终去累加的死循环。每次运行时,把这段脚本绑定到某个 CPU 上,而后察看不同 CPU 耗时的比照。最初比照后果也显示 CPU4 的耗时比其余的 CPU 高了几倍,也验证了之前的推断。之后协调运维更换了 CPU,动向指标也就恢复正常了。

总结

以上所有操作都只是在数据包从网卡到了内核层,还没到常见的协定,只是实现了万里长征第一步,前面还有一系列的步骤,例如数据包的压缩(GRO)、网卡多队列软件(RPS)还有 RFS 在负载平衡的根底上思考流的特色,就是 IP 端口四元组的特色,最初才是把数据递交到 IP 层,以及到相熟的 TCP 层。

总的来说,明天的分享都是围绕驱动来做的,我想强调的性能优化的外围点在于指标,不能测量也就很难去改善,要有指标的存在,这样所有的优化才有意义。

举荐浏览

MySQL 那些常见的谬误设计规范

全站 HTTPS 就肯定平安了吗?

正文完
 0