如何基于eBPF实现跨语言、无侵入的流量录制?

news2024/11/15 4:19:40

测试是产品发布上线的一个重要环节,但随着业务规模和复杂度不断提高,每次上线需要回归的功能越来越多,给测试工作带来了巨大的压力。在这样的大背景下,越来越多的团队开始使用流量回放对服务进行回归测试。

在建设流量回放能力之前,我们必须将线上服务的流量录制下来。通常要结合对流量特征的要求、实现成本、对业务的侵入性等方面综合考虑,选择不同的实现方式。

65e14fce145f07c3c8cf283e67c09149.jpeg

对于 Java 和 PHP 语言,目前业界已经有比较成熟的解决方案 jvm-sandbox-repeater、rdebug,基本可以做到低成本、无侵入式的流量录制;但 Go 语言由于缺少像 jvm 或 libc 等可利用的中间层,现有的方案 sharingan 需要修改官方 Go 源码并且侵入业务代码,稳定性风险较大;并且随着官方 Go 版本升级,需要持续维护迭代,使用和维护成本较高。

鉴于滴滴多语言的技术栈,我们经过调研发现可以通过 eBPF 实现一种跨语言无侵入的流量录制方案,大幅降低流量录制的使用和维护成本。        

流量录制原理

录制内容

流量回放时需要对下游依赖服务进行 mock,因此录制的一条完整流量中不仅需要包含入口调用的请求/响应,还需要包含处理这次请求时所调用依赖服务的请求/响应。

5d02c205072f4e1e180fffc09b163837.jpeg

实现思路

在介绍流量录制方案之前,我们先来看一个请求的处理过程(简化后):

a823246c46434390182904d7b1dc3aa8.jpeg

观察上述流程我们发现目标服务处理一个请求的大致流程如下:

  • 首先,调用 accept 获得一个调用方的连接;

  • 第二步,在这个连接上通过调用 recv 读取请求数据,解析请求;

  • 第三步,目标服务开始执行业务逻辑,过程中可能需要调用一个或多个依赖服务,对于每一次依赖服务调用,目标服务需要通过 connect 与依赖服务建立连接,然后在这个连接上通过 send 发送请求数据,通过 recv 接收依赖服务响应;

  • 最后,目标服务通过 send 给调用方返回响应数据。

为了实现流量录制,我们需要把图中所有的请求和响应数据保存下来。传统的流量录制方法需要跟踪服务框架、RPC框架、依赖服务sdk等所有涉及发送/接收数据的方法,将数据收集并保存下来。由于框架和sdk多种多样,需要大量的代码改造和开发工作,成本难以控制。

这里我们考虑更通用的方式:跟踪 socket 相关操作,例如 accept、connect、send、recv 等。通过这种方式我们可以不用关心业务中使用的应用层协议、框架、sdk等,实现更通用的流量录制方法。

但是,由于实现录制的位置更底层,能够获取的上下文信息更少,只有每个 socket 发送和接收的数据是不够的。我们需要借助其他信息对原始数据进行串联,从而组装完整的一条流量。

区分不同的请求

线上服务处理的请求大多是并发的,同时会有多个请求交织在一起,我们录制到原始数据是分散的,如何把同一个请求的数据合并,把不同请求的数据区分开呢?通过分析实际的请求处理过程,我们不难发现:

1、通常情况下,每个请求是在单独的线程中进行处理的。   

f33d542b7394861751afd4d647ef41df.jpeg

2、为了提高处理速度,可能创建子线程并发调用依赖服务。

d3d3557bf64b4b97a6b248cf8664226c.jpeg

实际上,子线程也可能再创建子线程,形成下图所示的线程关系:

40838f2070359f24b3102226d6f9cfb2.png

对于这种涉及子线程的场景,我们只要把子线程的数据合并到请求处理线程即可。每个请求都会对应一个请求处理线程和一系列的子线程,最终我们可以根据线程 ID 来区分出不同请求。

区分数据类型

在每一条流量中包含了两类数据:入口调用的请求和响应,下游依赖调用的请求和响应。我们需要在流量录制时进行区分。通过观察请求处理流程,我们不难发现其中的规律:

1、入口调用的请求和响应是在 accept 获得的 socket 上接收和发送的,recv 的数据是 request,send 的数据是 response。

2、下游依赖调用的请求和响应是在 connect 获得的 socket 上接收和发送的,send 的数据是 request,recv 的数据是 response;不同的 socket 对应不同的下游调用。

因此,我们可以根据 socket 类型和标识区分出不同的数据类型和不同的下游依赖调用。 

流量录制实现

考虑到目前大部分服务已经上云,因此方案需要支持容器化部署。eBPF 程序运行在内核中,而同一宿主机上的所有容器共享同一个内核,因此 eBPF 程序只需要加载一次即可录制到所有进程的数据。整体方案如下:              

fe49d9e56ccfcbbd43d7e5d5568df865.png

  • 录制agent:与目标进程部署在相同容器中,根据进程名找到要录制的目标进程 pid,(1) 控制录制 server 开启/关闭录制;(7) 从录制 server 接收原始数据,解析成完整流量,(8) 保存到日志文件中。

  • 录制server:部署在宿主机上,负责 (2, 3) 加载/挂载 eBPF程序、(6) 从 eBPF Map 中读取原始数据。

  • eBPF 程序:负责在目标进程 (4) 发送和接收数据时,(5) 从挂载的函数中读取原始数据并写入 eBPF Map 中。

选择插桩点

根据前面的讨论,我们需要跟踪的 socket 操作包括:

  • accept 和 connect 用于区分 socket 类型。

  • send 和 recv 用于捕获发送和接收的数据。

  • close 用于识别调用的结束。

对于 Go 语言,还需要获取执行上述 socket 操作的 goroutine id 和跟踪 goroutine 的父子关系。

在开发 eBPF 程序之前,需要选择合适的 eBPF 程序挂载位置,不同的 eBPF 程序类型,能够获取到的上下文不同,可调用的 bpf-helper 函数也不同。我们需要录制的数据只有 TCP 和 UDP 两种协议,因此可以通过 kprobe 挂载到内核的以下函数:

  • inet_accept

  • inet_stream_connect

  • inet_sendmsg

  • inet_recvmsg

  • inet_release

为了跟踪 goroutine 之间的关系,我们可以通过 uprobe 挂载到 Go 运行时的 runtime.newproc1 函数,从 callergp 和 newg 中获取对应的 goroutine 信息。

开发eBPF程序

流量录制虽然涉及了多个内核函数,但流程基本是一样的,下面以录制 socket 发送数据为例进行详细介绍。

函数签名:

int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)

参数说明:

  • sock  socket 指针

  • msg 要发送的数据

  • size 要发送数据的长度

返回值:

  • 成功时返回发送的数据长度,失败时返回错误码。

由于实际发送的数据长度是在函数返回时才能获取到的,因此我们需要开发两个程序,分别完成以下工作:

  • 在函数入口处记录函数参数和上下文

  • 在函数返回时记录实际发送的数据内容

函数入口 eBPF 程序:

SEC("kprobe/inet_sendmsg")
int BPF_KPROBE(inet_sendmsg_entry, struct socket *sock, struct msghdr *msg)
{
    struct probe_ctx pctx = {
        .bpf_ctx = ctx,
        .version = EVENT_VERSION,
        .source = EVENT_SOURCE_SOCKET,
        .type = EVENT_SOCK_SENDMSG,
        .sr.sock = sock,
    };
    int err;




    // 过滤掉不需要录制的进程
    if (pid_filter(&pctx)) {
        return 0;
    }




    // 读取 socket 类型信息
    err = read_socket_info(&pctx, &pctx.sr.sockinfo, sock);
    if (err) {
        tm_err2(&pctx, ERROR_READ_SOCKET_INFO, __LINE__, err);
        return 0;
    }




    // 记录 msg 中的数据信息
    err = bpf_probe_read(&pctx.sr.iter, sizeof(pctx.sr.iter), &msg->msg_iter);
    if (err) {
        tm_err2(&pctx, ERROR_BPF_PROBE_READ, __LINE__, err);
        return 0;
    }




    // 将相关上下文信息保存到 map 中
    pctx.id = bpf_ktime_get_ns();
    err = save_context(pctx.pid, &pctx);
    if (err) {
        tm_err2(&pctx, ERROR_SAVE_CONTEXT, __LINE__, err);
    }
    return 0;
}

函数返回 eBPF 程序:

SEC("kretprobe/inet_sendmsg")
int BPF_KRETPROBE(inet_sendmsg_exit, int retval)
{
    struct probe_ctx pctx = {
        .bpf_ctx = ctx,
        .version = EVENT_VERSION,
        .source = EVENT_SOURCE_SOCKET,
        .type = EVENT_SOCK_SENDMSG,
    };
    struct sock_send_recv_event event = {};
    int err;




    // 过滤掉不需要录制的进程
    if (pid_filter(&pctx)) {
        return 0;
    }




    // 如果发送失败, 跳过录制数据
    if (retval <= 0) {
        goto out;
    }




    // 从 map 中读取提前保存的上下文信息
    err = read_context(pctx.pid, &pctx);
    if (err) {
        tm_err2(&pctx, ERROR_READ_CONTEXT, __LINE__, err);
        goto out;
    }




    // 构造 sendmsg 报文
    event.version = pctx.version;
    event.source = pctx.source;
    event.type = pctx.type;
    event.tgid = pctx.tgid;
    event.pid = pctx.pid;
    event.id = pctx.id;
    event.sock = (u64)pctx.sr.s;
    event.sock_family = pctx.sr.sockinfo.sock_family;
    event.sock_type = pctx.sr.sockinfo.sock_type;




    // 从 msg 中读取数据填充到 event 报文, 并通过 map 传递到用户空间
    sock_data_output(&pctx, &event, &pctx.sr.iter);




out:
    // 清理上下文信息
    err = delete_context(pctx.pid);
    if (err) {
        tm_err2(&pctx, ERROR_DELETE_CONTEXT, __LINE__, err);
    }
    return 0;
}

获取goid

对于 Go 语言,我们需要根据发送和接收数据时 goroutine id 进行数据串联,如何在 eBPF 程序中获取呢?通过分析 go 源码,我们发现 goroutine id 是保存在 struct g 中的,并且可以通过 getg() 来获取当前 g 的指针。

getg 函数:

// getg returns the pointer to the current g.
// The compiler rewrites calls to this function into instructions
// that fetch the g directly (from TLS or from the dedicated register).
func getg() *g

根据函数注释,当前 g 的指针是放在线程本地存储(TLS)中的,调用 getg() 的代码由编译器进行重写。为了找到 getg() 的实现方式,我们看到 runtime.newg 函数中调用了 getg,对它进行反汇编,发现 g 的指针保存在 fs 寄存器 -8 的内存地址上:

671f66efe5ac93dc7e91884cc765a42d.png

接下来,我们找到 struct g 中的 goid 字段(位于 runtime/runtime2.go):

type g struct {
    .... 此处省略大量字段
    goid         int64
    .... 此处省略大量字段
}

拿到 g 的指针后,只要加上 goid 字段的偏移量即可获取到 goid。同时,考虑到不同的 go 版本之间,goid 偏移量可能不同,最终在 eBPF 程序中我们可以这样获取当前 goid:

static __always_inline
u64 get_goid()
{
      struct task_struct *task = (struct task_struct *)bpf_get_current_task();
      unsigned long fsbase = 0;
      void *g = NULL;
      u64 goid = 0;
      bpf_probe_read(&fsbase, sizeof(fsbase), &task->thread.fsbase);
      bpf_probe_read(&g, sizeof(g), (void*)fsbase-8);
      bpf_probe_read(&goid, sizeof(goid), (void*)g+GOID_OFFSET);
      return goid;
}

遇到的问题

eBPF 程序虽然可以使用 C 语言开发,但是与普通 C 语言开发过程有较大的差别,增加了很多限制。

以下为开发时遇到的比较关键的问题和解决思路:

  • 不允许使用全局变量、常量字符串或数组,可以保存到 map 中。

  • 不支持函数调用,可以通过 inline 内联解决。

  • 栈空间不能超过512字节,必要时可通过 array 类型的 map 做缓冲区。

  • 不能直接访问用户态和内核态内存,要通过 bpf-helper 的相关函数。

  • 单个程序指令条数不能超过 1000000,尽量保持 eBPF 程序逻辑简单,复杂的处理放在用户态程序完成。

  • 循环必须有明确的次数上限,不能只靠运行时判断。

  • 结构体成员要内存对齐,否则可能导致部分内存未初始化,引发 verifier 报错。

  • 代码经过编译器优化后 verifier 可能误报内存访问越界问题,可以在代码中增加 if 判断帮助 verifer 识别,必要时可通过内联汇编的方式解决。

  • ....

随着 clang 和内核对 ebpf 支持的逐渐完善,很多问题也在逐步得到解决,后续的开发体验也会变得更顺畅。

安全机制

为了保障流量数据的安全性,降低数据脱敏对线上机器的性能影响,我们选择在流量采集阶段进行加密:          

6675a03cc0aac4d310212fa092060368.jpeg

总结

本文介绍了 eBPF 在流量录制方向的应用,希望可以帮助大家降低流量录制的实现和接入成本,快速建设流量回放能力。由于篇幅原因,流量录制的很多细节不能展开分享,后续计划将该方案开源,欢迎大家持续关注滴滴开源项目。更多关于 eBPF 的应用场景,感兴趣的同学也可以进一步阅读《eBPF内核技术在滴滴云原生的落地实践》进行了解。

限于作者技术水平,文中难免有所错漏,大家可以在评论区留言指正,期待后续更多的交流和讨论。

 END 

作者及部门介绍 

本篇文章作者王超锋,来自滴滴网约车出行技术团队,出行技术作为网约车业务研发团队,通过建设终端用户体验平台、C端用户产品生态、B端运力供给生态、出行安全生态、服务治理生态、核心保障体系,打造安全可靠、高效便捷、用户可信赖的出行平台。

招聘信息

团队后端、测试需求招聘中,欢迎有兴趣的小伙伴加入,可以扫描下方二维码简历直投,期待你的加入!

研发工程师

岗位描述:

1. 负责相关业务系统后台研发工作,包括业务的架构设计、开发,控制复杂度,提升系统性能和研发效率;

2. 有业务 sense,通过不断的技术研究和创新,与产品、运营一起快速迭代提升业务的核心数据。

251066c45fcd0baf36f051708961bf7e.png

测试开发工程师

岗位描述: 

1. 构建适用于网约车业务的质量保障体系,制定并推进相关质量技术方案落地,持续保障业务质量;

2. 深入了解业务,与业务中各角色建立沟通,总结业务问题与痛点,全方位为业务创造价值,工作不设固定边界;

3. 通过应用相关质量基础设施,提升业务代码质量和交付效率;

4. 沉淀高效测试解决方案,并能提供通用化方案,支持在其他业务线落地应用;

5. 解决业务质量保障中的难点问题、复杂技术难题;

6. 质量技术领域前瞻性探索。

1b35e80d52dd3cd0ad79fb26da54648f.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/834219.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一台电脑给另外一台电脑共享网络

这里写自定义目录标题 有网的电脑上操作一根网线连接两台电脑没网的电脑上 有网的电脑上操作 右键->属性->共享 如同选择以太网&#xff0c;勾选。确认。 一根网线连接两台电脑 没网的电脑上 没网的电脑为mips&麒麟V10 新增个网络配置ww&#xff0c;设置如下。 …

产品设计中的小体验:带来大问题解决之道

在激烈的市场竞争中&#xff0c;产品的体验设计已成为区分优劣的重要标志。用户不仅仅关注产品的核心功能&#xff0c;更重视产品在使用过程中的舒适度、易用性和情感体验。产品设计中的细节体验&#xff0c;看似微不足道&#xff0c;却往往能带来意想不到的效果。这是因为&…

线上服务挂了 3 分钟

在一个风和日丽的下午&#xff0c;刚打算饮茶&#xff0c;线上就开始报警了&#xff0c;一看情况网关报 500 了。。 网关&#xff08;用的是Spring Cloud Gateway&#xff09;挂了可还行&#xff0c;这可是对外的们&#xff0c;门没了岂不是所有请求都进不来了&#xff01; 说…

Linux 系统编程 开篇/ 文件的打开/创建

从本节开始学习关于Linux系统编程的知识&#xff01; 学习Linux的系统编程有非常多的知识点&#xff0c;在应用层面&#xff0c;很重要的一点就是学习如何“用代码操作文件来实现文件创建&#xff0c;打开&#xff0c;编辑等自动化执行” 那如何自动化实现对文件的创建&#…

成本控制策略:加强企业安全

我们生活在一个不确定的时代。大多数经济学家预测&#xff0c;今年全球经济将继续放缓&#xff0c;亚太地区当然也不会逆势而上。 在供应链问题、大规模裁员、高通胀和高利率之间&#xff0c;我们毫不奇怪地看到大多数公司和行业采取谨慎态度&#xff0c;战略、增长计划和预算…

使用docker安装wordpress详细教程及出现数据库无法连接问题解决方法

1.获取wordpress镜像 docker pull wordpress 2.创建wordpress 的容器 a.创建wordpress的文件镜像卷文件夹 mkdir wordpress b.创建wordpress镜像 docker run --name wp -p8080:80 -v /home/wordpress/:/var/www/html -d wordpress c.查看容器运行情况 3.在本地或者其他服务器创…

AutoDL从0到1搭建stable-diffusion-webui

前言 AI绘画当前非常的火爆&#xff0c;随着Stable diffusion&#xff0c;Midjourney的出现将AI绘画推到顶端&#xff0c;各大行业均受其影响&#xff0c;离我们最近的AI绘画当属Stable diffusion&#xff0c;可本地化部署&#xff0c;只需电脑配备显卡即可完成AI绘画工作&…

Go语言并发编程(千锋教育)

Go语言并发编程&#xff08;千锋教育&#xff09; 视频地址&#xff1a;https://www.bilibili.com/video/BV1t541147Bc?p14 作者B站&#xff1a;https://space.bilibili.com/353694001 源代码&#xff1a;https://github.com/rubyhan1314/go_goroutine 1、基本概念 1.1、…

宋老板教我做人--背后少说别人

宋老板教我做人——背后少说别人 2000年&#xff5e;2004年间发生的事 让我很难忘&#xff0c;让我长记性 趣讲大白话&#xff1a;是不是传说中的&#xff0c;发自内心的善良&#xff1f; 【趣讲信息科技246期】 **************************** 真实故事1&#xff1a; 2000年5月…

Embedding入门介绍以及为什么Embedding在大语言模型中很重要

Embeddings技术简介及其历史概要 在机器学习和自然语言处理中&#xff0c;embedding是指将高维度的数据&#xff08;例如文字、图片、音频&#xff09;映射到低维度空间的过程。embedding向量通常是一个由实数构成的向量&#xff0c;它将输入的数据表示成一个连续的数值空间中…

【python】绘图代码模板

【python】绘图代码模板 pandas.DataFrame.plot( )画图函数Seaborn绘图 -数据可视化必备导入数据集可视化统计关系使用Seaborn绘制散点图抖动图箱线图小提琴图Pointplot群图 可视化数据集的分布绘制单变量分布柱状图直方图 绘制双变量分布Hex图KDE 图可视化数据集中的成对关系 …

【数据结构与算法】线索化二叉树

线索化二叉树 n 个节点的二叉链表中含有 n 1 【公式 2n - (n - 1) n 1】个空指针域。利用二叉链表中的空指针域&#xff0c;存放指向该节点在某种遍历次序下的前驱和后继节点的指针&#xff08;这种附加的指针称为“线索”&#xff09;。这种加上了线索的二叉链表称为线索链…

Anteater/食蚁兽 V1.0.0 (帮助开发者快速找到项目中敏感信息)

Github>https://github.com/MartinxMax/Anteater 首页 Anteater/食蚁兽 V1.0.0 帮助开发者快速找到项目中存在敏感信息的文件,并且以时间戳为文件名保存日志 Anteater/食蚁兽 使用方法 #python3 anteater.py -h ps:当前目录下存在Windows_install.bat,Linux_install.sh请…

爆肝整理,Postman接口测试-参数关联实战(详细步骤)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 接口测试什么时候…

忘掉MacType吧,TtfAutoHint手工删除ttc、ttf字体的hinting,微软雅黑字体更显平滑

Windows的ClearType渲染字体方式&#xff0c;结合臭名昭著的hinting技术使微软雅黑字体备受争议&#xff0c;正所谓&#xff1a;成也hinting&#xff0c;败也hinting。 首先什么是hinting&#xff1f; Hinting 这个词一直都没有中文名称&#xff0c;我用粤语将它音译为“牵挺”…

javascript 7种继承-- class继承(7)

文章目录 概要继承的进化史class继承1. 类声明与严格模式2. 类的实现3. 类的静态方法4. get,set 存取器5. 类中的公有继承以及私有继承6. 使用 super 调用超类7. Mix-ins / 混入 源码&#xff1a; 类的继承效果图小结 概要 这阵子在整理JS的7种继承方式&#xff0c;发现很多文…

【2023 华数杯全国大学生数学建模竞赛】 B题 不透明制品最优配色方案设计 详细建模方案解析及参考文献

【2023 华数杯全国大学生数学建模竞赛】 B题 不透明制品最优配色方案设计 详细建模方案解析及参考文献 1 题目 B 题 不透明制品最优配色方案设计 日常生活中五彩缤纷的不透明有色制品是由着色剂染色而成。因此&#xff0c;不透明制品的配色对其外观美观度和市场竞争力起着重要…

GD32F103VE定时器0

本测试程序&#xff0c;配置GD32F103VE定时器0每500ms中断一次&#xff0c;中断时&#xff0c;开关LED灯。 只讲定时器&#xff0c;多了&#xff0c;有点乱。有的人喜欢汇总&#xff0c;Timer的功能太多&#xff0c;放在一起&#xff0c;会搞混&#xff0c;不好移植。即使放一…

【雕爷学编程】MicroPython动手做(31)——物联网之Easy IoT 2

1、物联网的诞生 美国计算机巨头微软(Microsoft)创办人、世界首富比尔盖茨&#xff0c;在1995年出版的《未来之路》一书中&#xff0c;提及“物物互联”。1998年麻省理工学院提出&#xff0c;当时被称作EPC系统的物联网构想。2005年11月&#xff0c;国际电信联盟发布《ITU互联网…

哪些情况下需要使用爬虫IP

不知道小伙伴们有没有遇到过这种场景&#xff1a;上网闲逛&#xff0c;看一些搞笑的视频或者想下载一些酷炫的文件&#xff0c;正点击呢&#xff0c;结果却发现被网站限制了&#xff0c;无法访问或者下载&#xff1f; 别急&#xff0c;今天我来告诉大家&#xff0c;如何借助使…