技术干货 | cilium 原理之sock_connect

news/2024/7/6 5:34:14 标签: 服务器, 数据库, ebpf, 云计算

1.背景

在集群网络使用cilium之后,最明显的情况就是:服务暴露vip+port,在集群内怎么测试都正常,但集群外访问可能是有问题的。而这就在于cilium所使用的ebpf科技。

2.引子:curl请求的路程

相对底层一点的语言,比如c语言,在创建一个tcp连接时,主要分两步(其它语言可能会更简单):

    int socket_desc;
    struct sockaddr_in server;
	
	//Create socket
	socket_desc = socket(AF_INET , SOCK_STREAM , 0);

	server.sin_addr.s_addr = inet_addr("1.1.1.1");
	server.sin_family = AF_INET;
	server.sin_port = htons( 80 );

	//Connect to remote server
	if (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0)

一个连接的创建,主要分两个步骤:

  1. 创建socket对象

  2. 发起connect连接

而实际上,在内核层,它经历的步骤会非常多。可以通过perf工具来查看:

perf trace  -e 'net:*' -e 'sock:*' -e 'syscalls:*'  curl 1.1.1.1 -s >& /dev/stdout 

上面的输出很多,而syscalls:sys_enter_socket前面的很长一段,是curl程序打开本身加载动态链接库需要的系统调用。

而本次需要关心的是以下这部分(截取的部分内容):

   108.294 curl/15819 syscalls:sys_enter_socket(family: INET, type: STREAM)
   108.351 curl/15819 syscalls:sys_exit_socket(__syscall_nr: 41, ret: AX25)
   108.939 curl/15819 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 16)
   108.991 curl/15819 sock:inet_sock_set_state(skaddr: 0xffff902527424c80, oldstate: 7, newstate: 2, dport: 80, family: 2, protocol: 6, saddr: 0x7f176658943b, daddr: 0x7f176658943f, saddr_v6: 0x7f1766589443, daddr_v6: 0x7f1766589453)
   109.090 curl/15819 net:net_dev_queue(skbaddr: 0xffff9024f0a2d4e8, len: 74, name: "enp1s0")
   109.140 curl/15819 net:net_dev_start_xmit(name: "enp1s0", skbaddr: 0xffff9024f0a2d4e8, protocol: 2048, ip_summed: 3, len: 74, network_offset: 14, transport_offset_vali
d: 1, transport_offset: 34, gso_segs: 1, gso_type: 1)

从上面可以看出,在定义socket后,接着就是connect连接,而在sock:inet_sock_set_state这一步,有输出地址相关信息,但输出的是内存地址,无法直接查看。能通过bcc工具集中的tcplife来查看。

# 一个终端中运行:
tcplife -D 12345
# 另一个终端中运行:
curl 1.1.1.1:12345

虽然访问的是不存在的地址,但内核也会基于默认路由,走默认网关,将报文发送到enp1s0网卡上。而在sock:inet_sock_set_state可以抓取到源地址与目的地址信息。 既然我们能在sock:inet_sock_set_state点挂载程序,抓取报文信息,那我们是否可以在挂载点,修改socket的目的地址与目的端口信息?

        

答案是肯定的。但cilium是在cgroup/connect4进行修改的(和上面从perf查出来的不同,但可以通过bcc的工具来验证。cgroup是高版本内核才有的特殊,具体可参考链接,里面有标识内核版本的特性。

那么,这是如何查到的呢?

[root@c7-1 ~]# bpftool prog |grep sock
1653: type 18  name sock6_connect  tag d526fd1cb49a372e  gpl
1657: cgroup_sock  name sock6_post_bind  tag e46a7916c9c72e67  gpl
1661: type 18  name sock6_sendmsg  tag 19094f9c26d4dddf  gpl
1665: type 18  name sock6_recvmsg  tag 282bf4c10eff7f73  gpl
1669: type 18  name sock4_connect  tag 57eae2cf019378cc  gpl
1673: cgroup_sock  name sock4_post_bind  tag ddd7183184f2e6e9  gpl
1677: type 18  name sock4_sendmsg  tag 570ef9d580ce0589  gpl
1681: type 18  name sock4_recvmsg  tag 0bdebe7409ceb49f  gpl
[root@c7-1 ~]# bpftool prog |grep connect
1653: type 18  name sock6_connect  tag d526fd1cb49a372e  gpl
1669: type 18  name sock4_connect  tag 57eae2cf019378cc  gpl

在有运行cilium的机器上,使用bpftool工具查询挂载的程序,发现与socket相关的就是这些。

再到cilium的源代码中,查看对应的代码段定义:

github.com/cilium/cilium/bpf$ grep -i "__section(" *.c
bpf_host.c:__section("from-netdev")
bpf_host.c:__section("from-host")
bpf_host.c:__section("to-netdev")
bpf_host.c:__section("to-host")
bpf_lxc.c:__section("from-container")
bpf_lxc.c:__section("mydebug1")
bpf_lxc.c:__section("mydebug2")
bpf_lxc.c:__section("to-container")
bpf_network.c:__section("from-network")
bpf_overlay.c:__section("from-overlay")
bpf_overlay.c:__section("to-overlay")
bpf_sock.c:__section("cgroup/connect4")
bpf_sock.c:__section("cgroup/post_bind4")
bpf_sock.c:__section("cgroup/bind4")
bpf_sock.c:__section("cgroup/sendmsg4")
bpf_sock.c:__section("cgroup/recvmsg4")
bpf_sock.c:__section("cgroup/getpeername4")
bpf_sock.c:__section("cgroup/post_bind6")
bpf_sock.c:__section("cgroup/bind6")
bpf_sock.c:__section("cgroup/connect6")
bpf_sock.c:__section("cgroup/sendmsg6")
bpf_sock.c:__section("cgroup/recvmsg6")
bpf_sock.c:__section("cgroup/getpeername6")
bpf_xdp.c:__section("from-netdev")

由此,cilium使用的科技就很明显了。

3. 手写ebpf

1. ebpf程序实现

在看cilium源码实现之前,先手写一个最简单的修改目的地址与端口的程序。因为cilium本身框架很复杂,代码也有相关,所以先以最简单的(写死的)程序入手。代码可以参考cilium源码。

#include <bpf/ctx/unspec.h>
#include <bpf/api.h>

#define SKIP_POLICY_MAP 1
#define SKIP_CALLS_MAP  1

#define SYS_REJECT      0
#define SYS_PROCEED     1

# define printk(fmt, ...)                                       \
                ({                                              \
                        const char ____fmt[] = fmt;             \
                        trace_printk(____fmt, sizeof(____fmt),  \
                                     ##__VA_ARGS__);            \
                })

__section("cgroup/connect4")
int sock4_connect(struct bpf_sock_addr *ctx )
{
        if (ctx->user_ip4 != 0x04030201) {  // des ip is 1.2.3.4
            return SYS_PROCEED;
        }
        printk("aa %x ", ctx->user_ip4);
        ctx->user_ip4=0x19280a0a;  // set to 10.10.40.25
        printk("set ok %x,%x", ctx->user_ip4, ctx->user_port);
        return SYS_PROCEED;
}

BPF_LICENSE("Dual BSD/GPL");

程序说明:

  1. 判断目标ip是1.2.3.4才处理(对应16进制顺序相反,是因为系统为小端模式)。

  2. 输出目的ip,方便debug。

  3. 修改目的ip为指定的ip。

  4. 输出设置的结果。

入参bpf_sock_addr,可从cilium的源码中找到相关定义。

mysock.c

​​​​​​​

/* User bpf_sock_addr struct to access socket fields and sockaddr struct passed
 * by user and intended to be used by socket (e.g. to bind to, depends on
 * attach type).
 */
struct bpf_sock_addr {
	__u32 user_family;	/* Allows 4-byte read, but no write. */
	__u32 user_ip4;		/* Allows 1,2,4-byte read and 4-byte write.
				 * Stored in network byte order.
				 */
	__u32 user_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.
				 * Stored in network byte order.
				 */
	__u32 user_port;	/* Allows 1,2,4-byte read and 4-byte write.
				 * Stored in network byte order
				 */
	__u32 family;		/* Allows 4-byte read, but no write */
	__u32 type;		/* Allows 4-byte read, but no write */
	__u32 protocol;		/* Allows 4-byte read, but no write */
	__u32 msg_src_ip4;	/* Allows 1,2,4-byte read and 4-byte write.
				 * Stored in network byte order.
				 */
	__u32 msg_src_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.
				 * Stored in network byte order.
				 */
	__bpf_md_ptr(struct bpf_sock *, sk);
};

2. 程序加载

基于k8s部署cilium后,cilium会在容器中初始化好环境,我们可以直接使用,省去编译环境、cgroupv2配置的麻烦。

将上面的文件,复制到cilium的容器中(本样例中使用的cilium版本为1.12.7)。

​​​​​​​

file=./mysock.c

clang -O2 -target bpf -std=gnu89 -nostdinc -emit-llvm -g -Wall -Wextra -Werror -Wshadow -Wno-address-of-packed-member -Wno-unknown-warning-option -Wno-gnu-variable-sized-type-not-at-end -Wdeclaration-after-statement -Wimplicit-int-conversion -Wenum-conversion -I. -I/run/cilium/state/globals -I/var/lib/cilium/bpf -I/var/lib/cilium/bpf/include -D__NR_CPUS__=8 -DENABLE_ARP_RESPONDER=1 -DCALLS_MAP=cilium_calls_lb -c $file -o - | llc -march=bpf -mcpu=v2 -mattr=dwarfris -filetype=obj -o mysock.o

bpftool cgroup detach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest
rm -f /sys/fs/bpf/tc/globals/mytest

tc exec bpf pin /sys/fs/bpf/tc/globals/mytest obj mysock.o type sockaddr attach_type connect4 sec cgroup/connect4
bpftool cgroup attach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest

3.测试

开启四个终端,分别执行如下命令(直接在主机上执行):

​​​​​​​​​​​​​​

# command 1
cat /sys/kernel/debug/tracing/trace_pipe
# command 2
tcpconnect -P 80
# command 3
tcplife -D 80
# command 4
curl 1.2.3.4:80

因为我们会变更目的ip,所以就基于端口来抓包。

  1. 用tcplife抓包,抓的是上面perf的sock:inet_sock_set_state时的状态。

  2. 用tcpconnect抓的是connect() syscall时的状态。

4. 自己搭建ebpf环境

1. 挂载cgroup2

mkdir -p /run/cilium/cgroupv2
mount -t cgroup2  none /run/cilium/cgroupv2/

2. 加载ebp程序

因为centos8自带的tc与bpftool版本有点低,所以使用cilium中已经适配好的版本。

​​​​​​​​​​​​​​

docker run -it --name=mytest --network=host --privileged -v $PWD:/hosts/ -v /sys/fs/bpf:/sys/fs/bpf -v /run/cilium/cgroupv2/:/run/cilium/cgroupv2 cilium:v1.12.7 bash

cd /hosts/
# 可以直接用之前编译好的文件
tc exec bpf pin /sys/fs/bpf/tc/globals/mytest obj mysock.o type sockaddr attach_type connect4 sec cgroup/connect4
bpftool cgroup attach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest

很香!你会发现,功能已经实现了。

5. cilium逻辑讲解

  • 框架已定型,通过在ebpf中,获取目的ip与目的端口,然后基于映射规则将目的ip与端口进行修改,从而实现vip到目的地址的转换。

  • 由于它是在connect阶段做的转换,类似在调用connect函数时注册一个回调函数,和dnat是不同的,所以不需要在回包时转换还原。

cilium service list

这个命令可以查看cilium基于service配置的映射规则,ebpf程序再从这个规则中找到合适的bacend,并修改目的地址,然后完成转换。

6.展望

1. 这个功能可以做什么?

服务暴露关心的主要是两点:1. vip的高可用。2.负载均衡。而这两点,通过本文所介绍的方式都是可以实现的。

1.vip的高可用

  • vip的高可用,其本质就是在服务异常时,可以切换到服务b(这里暂不考虑有状态服务分主备的情况)。

  • 当我们在客户端运行ebpf程序时,就不需要这个vip了。在应用时可以配置一个虚拟的地址,比如1.2.3.4,由ebpf程序来决定转换到哪个实际的后端服务。而且当服务a异常后,可以变更映射规则,切换到服务b。这一切对应用都是透明的。

2.负载均衡

  • 既然可以将目的地址映射到服务a,那么基于服务a,b,c之间做负载均衡也是可行的。包括设置权重、熔断等。

  • 如istio,需要在客户端注入sidecar,运行envoy程序,其实也是相类似的逻辑,只不过它是通过代理实现。除了解析目的地址外,它还支持解析数据包,比如解析http协议,在异常时自动重试,实现服务切换应用无感知。相对于应用来说,只是卡顿了一下。

  • ebpf程序能够满足大部分场景,而且很高效。


作者:

沃趣科技产品研发部


http://www.niftyadmin.cn/n/4928123.html

相关文章

动态规划解0-1背包问题(超详细理解)

前言&#xff1a; 好久没写0-1背包问题了&#xff0c;都有些不记得了&#xff0c;写这篇文章给自己以后做简单参考&#xff0c;如果能同时帮到读者&#xff0c;不胜荣幸。 正文 0-1背包问题是这样的一个问题&#xff0c;假设有一个背包&#xff0c;其容量为 capacity 。在地…

有答案:10个网络工程师面试常见问题

目录 1、交换机转发数据包的原理&#xff1f; 2. 数据包如果经过二层交换机转发后&#xff0c;那这个数据包的源MAC会变化吗&#xff1f;如果经过三层交换机理由转发&#xff0c;那源MAC会变成什么呢&#xff1f; 3. 如何查看PC的ARP表&#xff0c;如何清除ARP表&#xff0c…

vue代码中访问一个链接拉取图片

当在Vue代码中访问链接拉取图片速度较慢时&#xff0c;你可以考虑以下方法来优化图片加载速度&#xff1a; 1. **异步加载图片&#xff1a;** 在Vue中&#xff0c;你可以使用<img>标签来加载图片&#xff0c;确保将src属性设置为图片链接。此外&#xff0c;你可以使用v-…

【位操作符的几种题型】

位操作符的几种题型 目录 题型一&#xff1a;寻找“单身狗”。 题型二&#xff1a;计算一个数在二进制中1的个数 题型三&#xff1a;不允许创建临时变量&#xff0c;交换两个整数的内容 题型一&#xff1a;寻找“单身狗”。 1.1题目解析 在一个整型数组中&#xff0c;只有…

易服客工作室:如何创建有用的内容日历

利用技巧和工具优化您的内容营销效率和效果。创建一个内容日历&#xff0c;您的整个团队都会从中受益&#xff01; 欢迎来到熙熙攘攘、瞬息万变的内容营销世界&#xff0c;在这里&#xff0c;截止日期到来的速度比喝咖啡的猎豹还要快。 现在&#xff0c;想象一下在没有地图、…

MyBatis操作数据库常见用法总结2

文章目录 1.动态SQL使用什么是动态sql为什么用动态sql标签拼接标签拼接标签拼接标签拼接标签拼接 补充1&#xff1a;resultType和resultMap补充2&#xff1a;后端开发中单元测试工具使用&#xff08;Junit框架&#xff09; 1.动态SQL使用 以insert标签为例 什么是动态sql 是…

ELF program/section segment解析

ELF program/section segment解析 1 elf program segment1.1 elf program header1.2 ELF32和ELF64示例1.2.1 ELF32 program segment1.2.2 ELF64 program segment 1.3 elf program segment数据流向图 2 elf section2.1 eld section header2.2 ELF32和ELF64示例2.2.1 ELF32 secti…

01-向量究竟是什么?

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan 向量究竟是什么 引入一些数作为坐标是一种鲁莽的行为 ——赫尔曼外尔 The introduction of numbers as coordinates is an act of violence - Hermann Weyl 向量的定义 向量&#xff0…