C/C++Linux服务器开发/后台架构师知识体系
Netmap 是一个高性能收发原始数据包的框架,由 Luigi Rizzo 等人开发完成,其包含了内核
模块以及用户态库函数。其目标是,不修改现有操作系统软件以及不需要特殊硬件支持,实
现用户态和网卡之间数据包的高性能传递。其原理图如下,数据包不经过操作系统内核进行
处理,用户空间程序收发数据包时,直接与网卡进行通信。
代码位置:https://github.com/luigirizzo/netmap
在 Netmap 框架下,内核拥有数据包池,发送环接收环上的数据包不需要动态申请,有数据到达网卡时,当有数据到达后,直接从数据包池中取出一个数据包,然后将数据放入此数据包中,再将数据包的描述符放入接收环中。内核中的数据包池,通过 mmap 技术映射到用户空间。用户态程序最终通过 netmap_if 获取接收发送环 netmap_ring,进行数据包的获取发送。
(1) 性能高 :数据包不走传统协议栈,不需要层层解析,用户态直接与网卡的接受环和发送环交互。性能高的具体原因有一下三个:
(2) 稳定性高 :有关网卡寄存器数据的维护都是在内核模块进行,用户不会直接操作寄存器。所以在用户态操作时,不会导致操作系统崩溃
(3) 亲和性 :可采用了 CPU 亲和性,实现 CPU 和网卡绑定,提高性能。
(4) 易用性好 :API 操作简单,用户态只需要调用 ioctl 函数即可完成数据包收发工作
与硬件解耦 :不依赖硬件,只需要对网卡驱动程序稍微做点修改就可以使用此框架
(几十行行),传统网卡驱动将数据包传递给操作系统内核中协议栈,而修改后的数据包直接放入 Netmap_ring 供用户使用。
这两个宏定义是对编译器做优化的,并不会对变量做什么改变。后面看到这两个宏的调用自动忽略就好了。
#ifndef likely
#define likely(x) #define unlikely(x)
builtin_expect(!!(x), 1)
builtin_expect(!!(x), 0)
#endif /* likely and unlikely */
1. _NETMAP_OFFSET
#define _NETMAP_OFFSET(type, ptr, offset) \
((type)(void *)((char *)(ptr) + (offset)))
解释:该宏定义的作用是将 ptr 指针(强转成 char *类型)向右偏移 offset 个字节,再将其转化为指定的类型 type。
2. NETMAP_IF
#define NETMAP_IF(_base, _ofs) NETMAP_OFFSET(struct netmap_if *,
_base, ofs)
解释:该宏定义将_base 指针向右偏移_ofs 个字节后,强转为 netmap_if *类型返回。在 nemap
中通过此宏得到 d->nifp 的地址。
3. NETMAP_TXRING
#define NETMAP_TXRING(nifp, index) _NETMAP_OFFSET(struct netmap_ring
*, \
nifp, (nifp)->ring_ofs[index] )
解释:1.通过该宏定义,可以找到 nifp 的第 index 个发送环的地址(index 是从 0 开始的),
ring_ofs[index]为偏移量,由内核生成。
2. 其中,我们注意到 struct netmap_if{}最后面只定义了 const ssize_t ring_ofs[0],实际上其它的 netmap 环的偏移量都写在了该结构体后面的内存地址里面,直接访问就可以了。
4. NETMAP_RXRING
#define NETMAP_RXRING(nifp, index) _NETMAP_OFFSET(struct netmap_ring
*, \
nifp, (nifp)->ring_ofs[index + (nifp)->ni_tx_rings + 1] )
解释:通过该宏定义,可以找到 nifp 的第 index 个接收环的地址,其中(nifp)->ring_ofs[]里面的下标为 index+(nifp)->ni_tx_rings+1,正好与发送环的偏移量区间隔开 1 个。(我想这应该是作者特意设计的)
5. NETMAP_BUF
#define NETMAP_BUF(ring, index) \
((char *)(ring) + (ring)->buf_ofs + ((index)*(ring)->nr_buf_size))
解释:1.通过该宏定义,可以找到 ring 这个环的第 index 个 buffer 的地址(buffer 里面存的就是我们接收/将发送的完整数据包),每个 buffer 占的长度是 2048 字节(在(ring)->nr_buf_size 也给出了)。
2. 其中(ring) ->buf_ofs 是固定的偏移量,不同的环这个值不相同,但所有的(char
*)(ring)+(ring)->buf_ofs 会指向同一个地址,也就是存放 buffer 的连续内存的开始地址
(d->buf_start 会指向该地址)。
6. NETMAP_BUF_IDX
#define NETMAP_BUF_IDX(ring, buf) \
( ((char *)(buf) - ((char *)(ring) + (ring)->buf_ofs) ) / \
(ring)->nr_buf_size )
解释:在讲 NETMAP_BUF 的时候我们说(char *)(ring) + (ring)->buf_ofs)总会指向存放 buffer 的起始位置( 无论是哪一个环), 在这段内存中将第一个 buffer 下标标记为 0 的话,
NETMAP_BUF_IDX 计算的恰好是指针 buf 所指 buffer 的下标。上面几个宏一时没弄懂也没关系,下面调用的时候还会提的。
nm_mmap()源码:
static int nm_mmap(struct nm_desc *d, const struct nm_desc *parent)
{
//XXX TODO: check if mmap is already done
if (IS_NETMAP_DESC(parent) && parent->mem && parent->req.nr_arg2
== d->req.nr_arg2)
{
/* do not mmap, inherit from parent */
D("do not mmap, inherit from parent");
d->memsize = parent->memsize;
d->mem = parent->mem;
} else
{
/* XXX TODO: 检查如果想申请的内存太大 (or there is overflow)
*/
d->memsize = d->req.nr_memsize; /* 将需要申请的内存大小赋值给 d->memsize */
d->mem = mmap(0, d->memsize, PROT_WRITE | PROT_READ, MAP_SHARED, d->fd, 0); /* 申请共享内存 */
if (d->mem == MAP_FAILED)
{
goto fail;
}
d->done_mmap = 1;
}
{
struct netmap_if *nifp = NETMAP_IF(d->mem, d->req.nr_offset);
/*通过 d->req.nr_offset 这个偏移量的到 nifp 的地址,NETMAP_IF 前面说过
*/
int i;
/*
*for(i=0; i<=2; i++)
* printf("ring_ofs[%d]:0x%x\n",i,nifp->ring_ofs[i]); // 这里是我自己加的,为了手动计算收/发环的偏移量
*/
struct netmap_ring *r = NETMAP_RXRING(nifp,); //对 nifp,找接收包的环 r,因为 index 为 0,所以省略了
*(struct netmap_if **) (uintptr_t) &(d->nifp) = nifp; // 对d->nifp 赋值,虽然 d->nifp 使用 const 定义的,但对其取地址再强值类型转换后,依然可以对其指向的空间进行操作
*(struct netmap_ring **) (uintptr_t) &d->some_ring = r; //同理,对 d->some_ring 进行赋值,此处指向了第一个接受(rx)环。
//printf("buf_ofs:0x%x\n", (u_int)r->buf_ofs);
*(void **) (uintptr_t) &d->buf_start = NETMAP_BUF(r, 0);//计算第一个 buffer 的地址,并存入 d->buf_start 指针中
*(void **) (uintptr_t) &d->buf_end = (char *) d->mem + d->memsize; //计算共享区间的最后一个地址,赋值给 d->buf_end
}
return 0;
fail: return EINVAL;
}
其中:
nm_nextpkt()源代码:
static u_char *nm_nextpkt(struct nm_desc *d, struct nm_pkthdr *hdr)
{
int ri = d->cur_rx_ring; //当前的接收环的编号
do
{
/* compute current ring to use */
struct netmap_ring *ring = NETMAP_RXRING(d->nifp, ri); // 得到当前 rx 环的地址
if (!nm_ring_empty(ring)) //判断环里是否有新到的包
{
u_int i = ring->cur; //当前该访问哪个槽(buffer)了
u_int idx = ring->slot[i].buf_idx; //得到第 i 个 buffer 的下标
//printf("%d\n", idx);
u_char *buf = (u_char *) NETMAP_BUF(ring, idx); //得到存有到来数据包的地址
// builtin_prefetch(buf);
hdr->ts = ring->ts;
hdr->len = hdr->caplen = ring->slot[i].len;
ring->cur = nm_ring_next(ring, i); //ring->cur 向后移动一位
/* we could postpone advancing head if we want
* to hold the buffer. This can be supported in
* the future.
*/
ring->head = ring->cur;
d->cur_rx_ring = ri; //将当前环(d->cur_rx_ring)指向第 ri 个(因为可能有多个环)。
return buf; //将数据包地址返回
}
ri++;
if (ri > d->last_rx_ring) //如果 ri 超过了 rx 环的数量,则再从第一个 rx 环开始检测是否有包到来。
ri = d->first_rx_ring;
} while (ri != d->cur_rx_ring);
return NULL; /* 什么也没发现 */
}
源代码:
static int nm_inject(struct nm_desc *d, const void *buf, size_t size)
{
u_int c, n = d->last_tx_ring - d->first_tx_ring + 1;
for (c = 0; c < n; c++)
{
/* 计算当前的环去使用(compute current ring to use) */
struct netmap_ring *ring;
uint32_t i, idx;
uint32_t ri = d->cur_tx_ring + c; //该访问第几个 tx 环了
if (ri > d->last_tx_ring) //当超过访问的 tx 环的下标范围时, 从头开始访问
ri = d->first_tx_ring;
ring = NETMAP_TXRING(d->nifp, ri); //得到当前 tx 环的地址
if (nm_ring_empty(ring)) //如果当前 tx 环是满的(ring->cur=ring->tail 表示没地方存数据包了),就跳过
{
continue;
}
i = ring->cur; //当前要往哪个槽(槽指向 buffer)中写入数据
idx = ring->slot[i].buf_idx; //得到这个槽相对于 buffer 起始地址(d->buf_start)的下标编号
ring->slot[i].len = size; //size 为待发送数据包的长度
nm_pkt_copy(buf, NETMAP_BUF(ring, idx), size); //将 buf 里存的数据包拷贝给 ring 这个环的第 i 个槽
d->cur_tx_ring = ri;
ring->head = ring->cur = nm_ring_next(ring, i); //将 head 和
cur 指向下一个槽
return size;
}
return 0; /* 失败 */
}
源代码:
static int nm_close(struct nm_desc *d)
{
/*
* ugly trick to avoid unused warnings
*/
static void * xxzt[] attribute ((unused)) =
{ (void *) nm_open, (void *) nm_inject, (void *) nm_dispatch, (void *) nm_nextpkt };
if (d == NULL || d->self != d)
return EINVAL;
if (d->done_mmap && d->mem)
munmap(d->mem, d->memsize); //释放申请的共享内存
if (d->fd != -1)
{
close(d->fd); //关闭文件描述符
}
bzero(d, sizeof(*d)); //将 d 指向的空间全部置 0
free(d); //释放指针 d 指向的空间
return 0;
}
VMWare 编译与调试
添加绑定的网卡的 IP 地址,十六进制 IP 地址,网卡相应的 MAC 地址。代码地址
https://github.com/wangbojing/NtyTcp.git
环境编译,下面以 ubuntu server 版本为例。先安装 netmap
Ubuntu 14.04
https://github.com/wangbojing/netmap.git
Ubuntu 16.04
https://github.com/luigirizzo/netmap.git
# ./configure # make
# make install
截至目前,40gpbs、32-cores、256G RAM 的 X86 服务器在 Newegg 网站上的报价是几千美元。实际上以这样的硬件配置来看,它完全可以处理 1000 万个以上的并发连接,如果它们不能,那是因为你选择了错误的软件,而不是底层硬件的问题。
可以预见在接下来的 10 年里,因为 IPv6 协议下每个服务器的潜在连接数都是数以百万级的,单机服务器处理数百万的并发连接(甚至千万)并非不可能,但我们需要重新审视目前主流 OS 针对网络编程这一块的具体技术实现。
很多人会想当然的认为,要实现 C10M(即单机千万)并发连接和处理能力,是不可能的。不过事实并非如此,现在系统已经在用你可能不熟悉甚至激进的方式支持千万级别的并发连接。
要知道它是如何做到的,我们首先要了解 Errata Security 的 CEO Robert Graham,以及他在Shmoocon 2013 大会上的“天方夜谈”视频记录: C10M Defending The Internet At Scale(此为 Yutube 视频,你懂的)。
Robert 用一种我以前从未听说的方式来很巧妙地解释了这个问题。他首先介绍了一点有关
Unix 的历史,Unix 的设计初衷并不是一般的服务器操作系统,而是电话网络的控制系统。由于是实际传送数据的电话网络,所以在控制层和数据层之间有明确的界限。问题是我们现在根本不应该使用 Unix 服务器作为数据层的一部分。正如设计只运行一个应用程序的服务器内核,肯定和设计多用户的服务器内核是不同的。
Robert Graham 的结论是:OS 的内核不是解决 C10M 问题的办法,恰恰相反 OS 的内核正是导致 C10M 问题的关键所在。
这也就意味着:
不要让 OS 内核执行所有繁重的任务:将数据包处理、内存管理、处理器调度等任务从内核转移到应用程序高效地完成,让诸如 Linux 这样的 OS 只处理控制层,数据层完全交给应用程序来处理。
最终就是要设计这样一个系统,该系统可以处理千万级别的并发连接,它在 200 个时钟周期
内处理数据包,在 14 万个时钟周期内处理应用程序逻辑。由于一次主存储器访问就要花费
300 个时钟周期,所以这是最大限度的减少代码和缓存丢失的关键。
面向数据层的系统可以每秒处理 1 千万个数据包,面向控制层的系统,每秒只能处理 1 百万个数据包。这似乎很极端,请记住一句老话:可扩展性是专业化的,为了做好一些事情,你不能把性能问题外包给操作系统来解决,你必须自己做。
10 年前,开发人员处理 C10K 可扩展性问题时,尽量避免服务器处理超过 1 万个的并发连接。通过改进操作系统内核以及用事件驱动服务器(典型技术实现如:Nginx 和Node)代替线程服务器(典型代表:Apache),使得这个问题已经被解决。人们用十年的时间从 Apache 转移到可扩展服务器,在近几年,可扩展服务器的采用率增长得更快了。 以传统网络编程模型作为代表的 Apache 为例,我们来看看它在 C10K 问题上的局限表现在哪些方面,并针对性的讨论对应的解决方法。Apache 的问题在于服务器的性能会随着连接数的增多而变差,实际上性能和可扩展性并不是一回事。当人们谈论规模时,他们往往是在谈论性能,但是规模和性能是不同的,比如 Apache。持续几秒的短期连接:比如快速事务,如果每秒处理 1000 个事务,只能有约 1000 个并发连接到服务器。如果事务延长到 10 秒,要维持每秒 1000 个事务则必须打开 1 万个并发连接。这种情况下:尽管你不顾DoS 攻击,Apache 也会性能陡降,同时大量的下载操作也会使 Apache 崩溃。
如果每秒处理的连接从 5 千增加到 1 万,你会怎么做?比方说,你升级硬件并且提高
处理器速度到原来的 2 倍。到底发生了什么?你得到两倍的性能,但你没有得到两倍的处
理规模。每秒处理的连接可能只达到了 6000。你继续提高速度,情况也没有改善。甚至 16
倍的性能时,仍然不能处理 1 万个并发连接。所以说性能和可扩展性是不一样的。
问题在于 Apache 会创建一个 CGI 进程,然后关闭,这个步骤并没有扩展。为什么呢? 内核使用的 O(N^2)算法使服务器无法处理 1 万个并发连接。
OS 内核中的两个基本问题:
通过上述针对 Apache 所表现出的问题,实际上彻底解决并发性能问题的解决方法的根本就是改进 OS 内核使其在常数时间内查找,使线程切换时间与线程数量无关,使用一个新的可扩展 epoll()/IOCompletionPort 常数时间去做 socket 查询。
因为线程调度并没有得到扩展,所以服务器大规模对 socket 使用 epoll 方法,这样就导致需要使用异步编程模式,而这些编程模式正是 Nginx 和 Node 类型服务器具有的。所以当从 Apache 迁移到 Nginx 和 Node 类型服务器时,即使在一个配置较低的服务器上增加连接数,性能也不会突降。所以在处理 C10K 连接时,一台笔记本电脑的速度甚至超过了 16 核的服务器。这也是前一个 10 年解决 C10K 问题的普遍方法。
实现 10M(即 1 千万)的并发连接挑战意味着什么:
1. 理由概述
硬件不是 10M 问题的性能瓶颈所在处,真正的问题出在软件上,尤其是*nux 操作系统。理由如下面这几点:
首先:最初的设计是让 Unix 成为一个电话网络的控制系统,而不是成为一个服务器操作系统。对于控制系统而言,针对的主要目标是用户和任务,而并没有针对作为协助功能的数据处理做特别设计,也就是既没有所谓的快速路径、慢速路径,也没有各种数据服务处理的优先级差别。
其次:传统的 CPU,因为只有一个核,操作系统代码以多线程或多任务的形式来提升整体性能。而现在,4 核、8 核、32 核、64 核和 100 核,都已经是真实存在的 CPU 芯片,如何提高多核的性能可扩展性,是一个必须面对的问题。比如让同一任务分割在多个核心上执行,以避免 CPU 的空闲浪费,当然,这里面要解决的技术点有任务分割、任务同步和异步等。
再次:核心缓存大小与内存速度是一个关键问题。现在,内存已经变得非常的便宜,随便一台普通的笔记本电脑,内存至少也就是 4G 以上,高端服务器的内存上 24G 那是相当的平常。但是,内存的访问速度仍然很慢,CPU 访问一次内存需要约 60~100 纳秒,相比很久以前的内存访问速度,这基本没有增长多少。对于在一个带有 1GHZ 主频 CPU 的电脑硬件里,如果要实现 10M 性能,那么平均每一个包只有 100 纳秒,如果存在两次 CPU 访问内存, 那么 10M 性能就达不到了。核心缓存,也就是 CPU L1/L2/LL Cache,虽然访问速度会快些, 但大小仍然不够,我之前接触到的高端至强,LLC 容量大小貌似也就是 12M。
看一下这些高性能框架的共同特点:
2. 关于 Intel 的 DPDK 框架/ Netmap 开源框架
随着网络技术的不断创新和市场的发展,越来越多的网络设备基础架构开始向基于通用处理器平台的架构方向融合,期望用更低的成本和更短的产品开发周期来提供多样的网络单元和丰富的功能,如应用处理、控制处理、包处理、信号处理等。为了适应这一新的产业趋势,
Intel 推出了基于 Intel x86 架构 DPDK (Data Plane Development Kit,数据平面开发套件) 实现了高效灵活的包处理解决方案。经过近 6 年的发展,DPDK 已经发展成支持多种高性能网卡和多通用处理器平台的开源软件工具包。
综上所述,解决 C10M 问题的关键主要是从下面几个方面入手:
网卡问题
网卡问题:通过内核工作效率不高
解决方案:使用自己的驱动程序并管理它们,使适配器远离操作系统。
CPU 问题
CPU 问题:使用传统的内核方法来协调你的应用程序是行不通的。
解决方案:Linux 管理前两个 CPU,你的应用程序管理其余的 CPU,中断只发生在你允许的
CPU 上。
内存问题
内存问题:内存需要特别关注,以求高效。
解决方案:在系统启动时就分配大部分内存给你管理的大内存页。
以 Linux 为例,解决的思咯就是将控制层交给 Linux,应用程序管理数据。应用程序与内核之间没有交互、没有线程调度、没有系统调用、没有中断,什么都没有。 然而,你有的是在 Linux 上运行的代码,你可以正常调试,这不是某种怪异的硬件系统,需要特定的工程师。你需要定制的硬件在数据层提升性能,但是必须是在你熟悉的编程和开发环境上进行。
C/C++Linux服务器开发/后台架构师视频学习推荐:https://ke.qq.com/course/417774?flowToken=1031343
Linux服务器开发/架构师面试题、学习资料、教学视频和学习路线图(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享有需要的可以自行添加学习交流群960994558