云原生网络性能优化:service mesh 篇

 

凌云时刻 · 技术

导读:sockmap 允许将 TCP 连接之间的数据转发过程卸载到内核中,减少了上下文切换以及用户态和内核态之间的数据拷贝操作,极大的优化了 TCP 连接之间 socket 数据转发的性能。

作者|寒蝉

来源|云巅论剑

背景

众所周知,Service Mesh 的总体架构如下图所示,主要由控制平面(control plane)和数据平面(data plane)两部分组成。

图 1 Service Mesh 总体架构

在数据平面中,如上图红框所示,两个服务 Pod(这里我们沿用 Kubernetes Pod 的叫法,后续一律简称为 Pod)间的数据交换就是 Service Mesh 中最基本的通信场景。如果我们将其放大来看,其典型架构如下图所示:

图 2 Service Mesh 基本通信流程

客户端 Pod 和服务端 Pod(客户端和服务端是相对的叫法,这里为了方便描述)都含有一个 sidecar 用来做代理。其中 Node X 和 Node Y 可以是同一个节点。一次 request/responce 的基本流程如下:

1. 客户端 Pod 内所有对外部发出的流量①都会被同 Pod 下的 sidecar 拦截并代理发送流量②到服务端,在这里是即 Server Pod。

2. Server Pod 中的 sidecar 会将所有入站流量拦截,并代理发送流量③到最终的 Server Container。

3. Server Pod 中由 Server Container 接收到来自客户端的请求后发送响应数据到客户端,即 Server Container 发出的响应流量④会被同 Pod 下的 sidecar 拦截并代理发送流量⑤到 Client Pod。

4. Client Pod 中的 Sidecar 会将入站流量拦截并代理发送流量⑥到 Client Container。

熟悉代理服务的同学都知道,在传统实现中,代理需要从一个 socket 中将客户端的报文读出来(每一次读取意味着从内核态到用户态的一次数据拷贝),然后通过另一个 socket 将数据发送给目标设备(每一次发送意味着从用户态到内核态的一次数据拷贝)。这个过程涉及到两次系统调用(sys_read/sys_write)和两次用户态/内核态之间的数据拷贝。很显然,这个代理转发行为会非常消耗性能。

所以在上述 Service Mesh 中两个 Pod 间的数据交换服务中,由于 sidecar 代理的存在会导致 Service Mesh 中数据交换存在很明显的性能瓶颈。

代理服务性能瓶颈已有解决方案

如上所述,加速 socket 之间的数据转发过程是解决代理服务性能瓶颈的一个重点。

在 sockmap 提出之前,对加速 socket 之间数据转发过程已经有很多解决方案被提出,但是这些方案都或多或少存在一些的致命的缺陷。

 sendfile

sendfile 用于加速两个文件描述符之间的数据传输,避免了用户态/内核态之间的数据拷贝操作,但是该系统调用不支持 socket 到 socket 的数据转发。

 splice

splice 实现了在文件描述符之间直接进行数据拷贝操作,避免了用户态/内核态之间的数据拷贝操作,因此该系统调用可以用来做 socket 之间的数据转发加速。但是使用 splice 依然需要唤醒用户态程序,每转发一份数据依然需要在唤醒的进程上下文中进行两次 splice 系统调用。

 io_submit

io_submit 是为了异步提交 IO 任务而设计的接口。对于 socket 之间转发数据这个场景,尽管 io_submit 无法避免用户态/内核态之间的数据拷贝,但是通过该接口进行 socket 数据的批处理操作可以减少上下文切换操作以及减少系统调用。

sockmap 的提出

在上述背景下,Linux 内核在 4.14 版本引入了一个基于 eBPF 的新特性 sockmap,该特性允许将 TCP 连接之间的数据转发过程卸载到内核中,从而绕过复杂的 Linux 网络协议栈直接在内核完成 socket 之间的数据转发操作,减少了上下文切换以及用户态和内核态之间的数据拷贝操作,极大的优化了 TCP 连接之间 socket 数据转发的性能。

本质上 sockmap 只是一种 BPF map 类型,该类型 map 的值代表一个指向数据结构 struct sock 的引用。然后我们就可以使用 BPF 程序来通过该类型的 map 完成 socket 间的数据流的重定向,使得数据流无需经过复杂的内核网络协议栈,也不需要进行用户态/内核态的数据拷贝,并且也不需要多余的系统调用。

下面我们回想下传统实现方式下 socket 间数据的转发过程。

用户态进程先从 socket 中读取数据(内核中的 sk_data_ready 回调函数是内核协议栈和进程上下文的之间的数据读取通道接口,当用户态进程有数据可以从 socket 读取时,sk_data_ready 唤醒用户态进程并把数据从内核协议栈转交给了持有 socket 的用户态进程,这意味着一次读取操作会有一次上下文切换和一次内核态到用户态的数据拷贝过程),然后用户态进程将刚才读取到的数据再写入到另一个 socket 里(内核中的 sk_write_space 回调函数时进程上下文和内核协议栈之间的数据写入通道接口,当用户态进程向 socket 写入数据时,sk_write_space 唤醒内核态进程并把数据从用户态进程转交到内核协议栈,这意味着一次读取操作会有一次上下文切换和一次用户态到内核态的数据拷贝过程)。

// net/core/sock.c


void sock_def_readable(struct sock *sk)
{
    struct socket_wq *wq;


    rcu_read_lock();
    wq = rcu_dereference(sk->sk_wq);
    if (skwq_has_sleeper(wq))
        // 同步唤醒进程,监听可读的事件
        wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN | EPOLLPRI |
                        EPOLLRDNORM | EPOLLRDBAND);
    sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
    rcu_read_unlock();
}


static void sock_def_write_space(struct sock *sk)
{
    struct socket_wq *wq;


    rcu_read_lock();


    if ((refcount_read(&sk->sk_wmem_alloc) << 1) <= READ_ONCE(sk->sk_sndbuf)) {
        wq = rcu_dereference(sk->sk_wq);
        if (skwq_has_sleeper(wq))
            // 同步唤醒进程,监听可写的事件
            wake_up_interruptible_sync_poll(&wq->wait, EPOLLOUT |
                        EPOLLWRNORM | EPOLLWRBAND);


        /* Should agree with poll, otherwise some programs break */
        if (sock_writeable(sk))
            sk_wake_async(sk, SOCK_WAKE_SPACE, POLL_OUT);
    }


    rcu_read_unlock();
}


void sock_init_data(struct socket *sock, struct sock *sk)
{
    ...
    sk->sk_data_ready   =   sock_def_readable;
    sk->sk_write_space  =   sock_def_write_space;
    ...
}

现在来看 sockmap 的处理过程。

sockmap 直接替换了 sk_data_ready 和 sk_write_space 回调函数的实现,通过一种叫 Stream Parser 的机制将从内核协议栈收取到的数据包的控制权转移到eBPF处理程序,eBPF 处理程序通过 bpf_sk_redirect_map 将收到的 socket 数据包重定向到指定的 socket 中最后经过内核协议栈将数据包发送出去。前面我们说了 sockmap 本质上是一个 BPF 的 map,bpf_sk_redirect_map 做的事很简单,就是去这个 map 里查询我们要用来发送数据的 socket 对应的 struct sock* 数据结构存不存在,如果找到了,那么就把数据重定向给该 socket。这就完成了 socket 间的数据转发。甚至你用来接受数据的 socket 和用来发送数据的 socket 可以是同一个 socket,也就是说你可以从一个 socket 接受数据然后通过 sockmap 再把收取到的数据通过同一个 socket 再发送出去。

// net/core/sk_msg.c


void sk_psock_start_strp(struct sock *sk, struct sk_psock *psock)
{
    struct sk_psock_parser *parser = &psock->parser;


    if (parser->enabled)
        return;


    parser->saved_data_ready = sk->sk_data_ready;
    // 替换回调函数
    sk->sk_data_ready = sk_psock_strp_data_ready;
    sk->sk_write_space = sk_psock_write_space;
    parser->enabled = true;
}

可以看到,使用 sockmap 来进行 socket 间的数据转发不会存在用户态/内核态之间的数据拷贝的开销,也不会存在唤醒用户态进程进行上下文切换的开销。相比与上述的所有方案都更轻便、更完美并且提供更好的转发性能。

使用 sockmap 加速 Service Mesh

尽管 Cilium 在其 1.4 版本已经开始使用 sockmap 这一特性来加速同 Node 下 Pod 和 L7 Proxy 之间的通信,以及同 Node 下普通 Pod 到普通 Pod 间的通信,但是根据我们对 Cilium(v1.7 版本)的测试来看,该功能无法正常工作且存在以下问题:

1. 缺少对 Service Mesh 场景下 Pod 内部的普通容器和 sidecar 之间通信加速的支持;

2. Cilium 用来控制 sockmap 加速的基于 label 的 Policy 对 Service Mesh 场景下的 sidecar 拦截规则不友好;

因此我们团队基于 Cilium 已有的 sockmap 实现开发并支持了 Service Mesh 场景下的 Pod 内部容器和 sidecar 的通信(也就是加速了上图 2 中的①③④⑥流量),同时在控制面上支持通过 Network Policy 控制 Service Mesh 的 sockmap 加速。

性能评估

我们在阿里云上购买了 2 台型号为 ecs.ebmg5s.24xlarge 的神龙服务器,内核版本号为 5.4.10,开启 numa, 其他(如网卡/磁盘等)均为默认配置。

 场景一:本机直接加速

在同一台服务器内,使用 netperf 和 iperf 工具部署服务端和客户端,直接在本机内测试使用 sockmap 和不使用 sockmap 两种情况下的带宽和时延,其中 netperf 工具用于测试时延,iperf 用于测试带宽。

考虑到 NUMA 亲和性的问题,分设两组测试:

  • 服务端和客户端同 NUMA node


bandwidth(Gbits/s)mean latency(us)P99 latency(us)
without sockmap42.3
12.34
14
with sockmap65.28.28
9
比例54.1%32.9%
36%
  • 服务端和客户端不同 NUMA node


bandwidth(Gbits/s)mean latency(us)P99 latency(us)
without sockmap42.3
12.34
14
with sockmap65.28.28
9
比例54.1%32.9%
36%

从以上数据可以看出,在本机直接加速的场景下:

1. sockmap 对带宽有很大提升,客户端和服务端在同一 NUMA 节点上,带宽提升大约在 50%~60% 之间。即使是跨 NUMA 节点的情况下,也有约 33% 的提升;

2. sockmap 显著降低时延,降低大约 30%~40% 之间;

3. NUMA 亲和性对带宽和时延影响很大,跨 NUMA 的情况下,性能数据都明显比同 NUMA 的情况低。

 场景二:service mesh 下 fortio 测试

本测试使用 fortio 工具在 QPS 固定为 2000 以及 server playload 为 16k 的前提下,测试不同并发度在以下三种数据路径的 RT 结果。

  • 直连(baseline):位于 A 服务器的客户端直接访问位于 B 服务器的服务端

  • envoy 代理:分别在客户端和服务端所在机器上部署一个 envoy 代理,劫持来自 80 端口的流量

  • envoy + sockmap:在上述 envoy 代理的情况下,在两台机器上都使用 sockmap 加速 envoy 代理到客户端/服务端的流量

从以上数据可以看出:

1. server mesh 中添加代理(比如 envoy)会成倍提高 RT 值,并且随着并发度的增长进行指数级的增长;

2. sockmap 对由于代理而带来的 RT 值增长有一定的改善, 提升比例大概在 10~15% 之间。

参考

《Istio Handbook——Istio 服务网格进阶实战》

https://www.servicemesher.com/istio-handbook

END

往期精彩文章回顾

我在阿里巴巴做 Serverless 云研发平台

Code Review 效率低?来试试智能语法服务

7 年零故障支撑双 11,消息中间件 RocketMQ 如何做到?

5分钟完成业务实时监控系统搭建,是一种什么样的体验?

企业 IT 治理沙龙·北京站:业务优先?治理优先?

轻松玩转全链路监控

连续 3 年支撑双 11,阿里云神龙如何扛住全球流量洪峰?

Cloud Native Infrastructures Meetup 精彩回顾

趣头条 Spark Remote Shuffle Service 最佳实践

ECS 云助手,实现云上运维自动化


长按扫描二维码关注凌云时刻

每日收获前沿技术与科技洞见

相关推荐