优化网站技术,深圳网站设计网站建设哪个好,丽水百度seo,网站做支付宝接口吗近期我们容器 SRE 团队基于 eBPF 技术建设网络连接异常感知能力#xff0c;灰度上线过程中发现了生产环境 10 以上的应用配置错误、程序 Bug 等问题。在和应用负责同学同步风险过程中#xff0c;大家都挺好奇我们如何实现在对应用无侵入的情况下发现服务连接异常的。本篇文档…近期我们容器 SRE 团队基于 eBPF 技术建设网络连接异常感知能力灰度上线过程中发现了生产环境 10 以上的应用配置错误、程序 Bug 等问题。在和应用负责同学同步风险过程中大家都挺好奇我们如何实现在对应用无侵入的情况下发现服务连接异常的。本篇文档将尝试从技术角度和大家聊聊我们是如何通过内核超能力 eBPF 为以摄像头式的实现应用连接异常感知和检测。
背景
在云上生产环境中相信大家一定遇到过应用偶发访问中间件或下游服务异常的场景这些异常究竟是云厂商虚拟网络环境的抖动还是应用自身连接设置不当亦或网络策略变更所导致我们总是不能快速便捷地做出初步判断此类问题的排查多半需要具有丰富网络经验的 SRE 同学在主机上使用 tcpdump 抓包分析但与此同时网络异常问题多半具有偶发性我们不仅需要在捉摸不定的场景下捕获到异常信息同样也需要能够基于应用信息进行历史异常数据追溯偶发性和可追溯性叠加这无疑进一步增加了此类问题排查难度。
业内有句万金油的大法一切问题都皆可归结于网络偶发抖动导致至于到底是否是网络抖动导致的云商一般也会是惜字如金“指标正常没有异常”这类问题往往最后被时间所淹没。
那么是否可参考城市道路上架设摄像头的模式即可实现网络异常实时查看又可进行历史异常追溯呢经过一番调研我们注意结合 Linux 内核观察和业内流行的 eBPF 技术可实现网络质量摄像头的能力本文尝试以网络连接异常场景建设网络摄像头能力进行展开关键技术给与介绍和实践验证。
方案调研
经过业内相关技术调研我们发现 Linux 内核的跟踪机制配合 eBPF 技术可实现灵活的网络事件采集和过滤能力并且可实现多个网络事件的联合分析和汇聚这个章节我将分别给与介绍。为简单起见Linux 内核跟踪机制我仅以跟踪点机制进行介绍实际场景中内核中还有动态跟踪机制 kprobe 更加灵活但跨内核移植性略差。
可观测基石 Linux 内核网络跟踪点
内核跟踪点Tracepoint 是内核开发人员提前在代码中预定义的探测点开发者在这些跟踪点上注入自己定义的处理函数以便在事件发生时进行数据采集。内核跟踪点的优势在于是内核开发者预先定义好的位置和语义明确使用时不需要修改内核代码。通过Linux 内核中的 tracefs 文件机制使用者可以方便地启用或禁用特定的跟踪点按需采集自己感兴趣的数据。Linux 内核跟踪点可在目录 /sys/kernel/debug/tracing/events/中按照类别查看 网络跟踪点介绍
网络相关的跟踪点主要位于在 /sys/kernel/debug/tracing/events/目录下的 net /sock/ tcp 等子目录其中 tcp 目录下跟踪主要和 TCP 网络协议相关sock 目录下为网络资源对象 socket 相关的跟踪点。如果本地安装了性能分析工具 perf 则可以使用 perf list tcp:* sock:inet* 命令查看当前内核相关的 跟踪点(此处为 5.10 内核版本不同内核版本跟踪点数量略有不同 sock:inet_sock_set_state [Tracepoint event]tcp:tcp_retransmit_skb [Tracepoint event]tcp:tcp_retransmit_synack [Tracepoint event]tcp:tcp_rcv_space_adjust [Tracepoint event]tcp:tcp_probe [Tracepoint event]tcp:tcp_send_reset [Tracepoint event]tcp:tcp_receive_reset [Tracepoint event]tcp:tcp_destroy_sock [Tracepoint event]
这里我们对常用的网络相关跟踪点功能给与简单介绍结合这些跟踪点可实现不同场景下的网络质量监控和诊断 网络跟踪点实践
Linux 内核中可基于 tracefs 机制直接控制跟踪点的启用和停止方便我们可以快速进行测试和数据搜集这里以 sock/inet_sock_set_state 跟踪点观察 Socket 对象状态变化为例演示
echo 1 /sys/kernel/debug/tracing/events/sock/inet_sock_set_state/enablecat /sys/kernel/debug/tracing
etcd-3825 [006] ..s2. 2198.949362: inet_sock_set_state: familyAF_INET protocolIPPROTO_TCP sport46142 dport32766 saddr127.0.0.1 daddr127.0.0.1 saddrv6::ffff:127.0.0.1 daddrv6::ffff:127.0.0.1 oldstateTCP_ESTABLISHED newstateTCP_CLOSE_WAIT
....echo 0 /sys/kernel/debug/tracing/events/sock/inet_sock_set_state/enable
上述样例输出中我们可以看到进程 etcd 进程中的某个连接状态从 TCP_ESTABLISHED 到 TCP_CLOSE_WAIT 的变化。该跟踪点打印数据的格式可在文件/sys/kernel/debug/tracing/events/sock/inet_sock_set_state/format 中查看。
内核可编程Linux 内核超能力 eBPF
尽管我们在上述场景进行了验证可以获取到文本数据但是仍然存在较多难以解决的问题比如我们需要基于特定条件过滤和多个跟踪点场景进行关联分析基于 tracefs 的方式可能会面临采集信息低效而且无法实现多更重点场景数据关联合并聚合分析场景。此时我们就需要赋予内核编程能力的超能力 eBPF 技术来进行高效和灵活的采集和聚合分析能力。
eBPF 超能力
eBPF 是一项革命性的技术起源于 Linux 内核它可以在特权上下文中如操作系统内核运行沙盒程序其可用于安全有效地扩展内核的功能而无需通过更改内核源代码或加载内核模块的方式来实现。
eBPF 自 2014 年引入到内核以后经历了快速的发展和完善当前已经成为顶级的内核模块。应用场景也从最初的网络数据包过滤扩展到内核可观测、安全、网络和调度等多个领域。eBPF 可实现动态地编程内核以实现高效的网络、可观测性、追踪和安全性。eBPF 相对于内核犹如 JavaScript 与 HTML 本身eBPF 技术赋予了内核的可编程能力带来了众多的创新可能。
就 eBPF 技术而言其具有以下特性 高性能eBPF 通过 JIT 编译以及运行在内核空间中可以大幅提高内核中数据处理能力 安全eBPF 程序经过严格的验证不会导致内核崩溃并且只能由特权用户进行修改 灵活功能和用例的修改、增加即时加载到内核并生效而不需要重启或者打补丁。 eBPF 技术原理
eBPF 技术原理简单而言就是在内核中基于事件运行用户自定义的程序并且能通过提供的 map/perf 等机制提供用户空间与内核空间的双向数据交换。工作原理的核心流程如下图所示 编译 用户可基于场景编写特定用途的 eBPF 程序借助于 LLVMclang套件将其编译为通用的 eBPF 字节码有点类似 Java 字节码 加载 通过系统调用将编译后的字节码通过系统调用 bpf() 加载到内核对应的事件 Hook 点 验证内核会针对加载的 eBPF 字节码进行各种安全检查确保程序运行时的安全性避免对内核造成灾难验证通过后将 eBPF 字节码通过 JIT 优化为二进制指令默认启用并与特定的事件相关联 触发运行在内核对应的事件触发时就会运行对应的 eBPF 程序基于上下文数据进行处理或将处理后的数据发送至用户空间的程序进行进一步处理分析。
如果你想进一步了解 eBPF 技术推荐进一步阅读BPF 技术概览文章。
使用 eBPF 注意事项
用于 eBPF 是在内核事件发生时触发的运行的程序其需要保障安全和高效 不能访问内核任务函数不同的场景下只能访问特定场景下的上下文信息比如网络类型的 eBPF 程序只能访问到网络相关上下文信息 eBPF 程序运行的栈只有 512 个字节能够运行的指令数量也最大只能为 100 万条指令。
上述限制都是为了保障在内核事件触发后不会影响内核安全和内核运行效率。在介绍完内核网络跟踪点和 eBPF 基础技术后我们进入到整体方案设计阶段。
架构设计和实现
方案架构设计
在整体架构上我们可将程序部署在 K8s 容器集群或单机 ECS 上。当 K8s 调度到单机或部署到单机上需要运行用户空间程序 net-collector 和内核事件处理程序 bpf_collector核心流程如下 用户空间程序 net-collector 用于加载 BPF 程序至内核中 当内核中有网络连接异常信息时内核中的 bpf_collector 程序就会被触发 bpf_collector 程序将采集到数据发送到用户空间程序 net-collector 用户空间程序 net-collector 在进行数据过滤和聚合后将数据写入到本地日志记录文件中 单机部署的日志采集 agent 将数据采集到日志平台 最后我们可通过可视化界面完成日志平台保存数据的消费处理和架构化展示以应用维度提供异常展示。 部署在单机 eBPF 程序就像核心路口的摄像头一样可时时刻刻按照约定规则采集信息我们既可以实时查看采集的信息还可以通过日志平台进行回溯查看。
方案实现
以网络连接超时为例我们尝试采集环境中所有新建连接的网络重传事件这里有两个关键点 连接初始阶段这时 TCP 状态位于 SYN_SENT 状态 在连接状态出现了数据包的重传基于上述网络跟踪点章节介绍我们适合选取跟踪点 tcp:tcp_retransmit_skb 附着编写的 eBPF 程序即可。 单机采集端的实现我们需要编写两部分程序 用户空间程序 net-colloctor用于将 eBPF 程序加载到内核并接受在内核事件触发 eBPF 程序传递的数据进行采集、聚合和分析并写入到日志文件记录中 内核中运行的 eBPF 程序 bpf-collector内核程序附着与 tcp:tcp_retransmit_skb 跟踪点在网络连接重传事件发生时候采集网络上下文信息并将其发送至用户空间程序 net-colloctor 进行分析。
内核空间 eBPF 程序
内核中的 eBPF 程序给与简单说明核心包括 3 个步骤
SEC(tracepoint/tcp/tcp_retransmit_skb) // 与跟踪点对应的声明
int handle_tcp_retransmit_skb(struct trace_event_raw_tcp_event_sk_skb *ctx)
{struct event event {};const struct sock *sk ctx-skaddr;// 步骤 ①char state 0;state BPF_CORE_READ(sk, __sk_common.skc_state); if (state ! TCP_SYN_SENT){return 0;}// 步骤 ②event.family BPF_CORE_READ(sk, __sk_common.skc_family); event.ts_us bpf_ktime_get_ns() / 1000;event.type RESTRANSMIT;event.sport BPF_CORE_READ(ctx, sport);event.dport BPF_CORE_READ(ctx, dport);switch (event.family){case AF_INET:bpf_probe_read_kernel(event.saddr, sizeof(ctx-saddr), BPF_CORE_READ(ctx, saddr));bpf_probe_read_kernel(event.daddr, sizeof(ctx-daddr), BPF_CORE_READ(ctx, daddr));break;case AF_INET6:bpf_probe_read_kernel(event.saddr, sizeof(ctx-saddr_v6), BPF_CORE_READ(ctx, saddr_v6));bpf_probe_read_kernel(event.daddr, sizeof(ctx-daddr_v6), BPF_CORE_READ(ctx, daddr_v6));break;default:}// 步骤 ③bpf_perf_event_output(ctx, dw_net_col_events, BPF_F_CURRENT_CPU, event, sizeof(event));return 0;
}
上述代码中的核心步骤介绍如下
步骤 1获取到触发重传函数上下文中的 TCP Sock 对象的状态信息这里我们仅采集 TCP_SYN_SENT 状态的否则就直接返回
步骤 2基于内核跟踪点的 Famliy 信息来区分是 IPv4 或 IPv6获取到必要的信息并填充到 event 对象event 对象是用户空间程序与内核 BPF 程序公用的数据结构
步骤 3将内核中封装好的数据发送到用户空间。
这里需要注意的是不同内核版本的跟踪点结构体可能会发生变化。为了同时适配多个内核版本我们使用可以跨多个内核版本移植方案 CO-RE。CO-RE 全称为 Compile Once – Run Everywhere中文翻译为一次编译、到处运行当前社区已经提供在低版本支持 CO-RE 的实现我们在 4.19 内核中由于没有默认支持 CO-RE这需要进行一些特定的处理步骤。关于 CO-RE 详细介绍可参考这篇文档。
用户空间程序
用户空间的程序主要用于加载 BPF 程序到内核并读取 BPF 程序运行过程中上报的数据。程序使用 go 开发基于库 cilium/ebpf。简化代码如下所示
func main() {flag.Parse()var btfSpec *btf.Specvar err error// 步骤 ①// Load pre-compiled programs and maps into the kernel.var opts ebpf.CollectionOptionsopts.Programs.KernelTypes btfSpecobjs : bpfObjects{}if err : loadBpfObjects(objs, opts); err ! nil {log.Fatalf(loading objects: %v, err)}defer objs.Close()// 步骤 ②tpRestranSkb, err : link.Tracepoint(tcp, tcp_retransmit_skb, objs.HandleTcpRetransmitSkb, nil)if err ! nil {log.Fatalf(opening tracepoint tcp/tcp_retransmit_skb %s, err)}defer tpRestranSkb.Close()// 步骤 ③rd, err : perf.NewReader(objs.DwNetColEvents, os.Getpagesize()*10)if err ! nil {log.Fatalf(creating perf event reader: %s, err)}defer rd.Close()// 步骤 ④var event bpfEventfor {record, err : rd.Read()if err : binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, event); err ! nil {continue}eventHandler.Handle(event)}
}
上述代码已经经过简化上述核心步骤介绍如下
步骤 1加载内核运行的 eBPF 程序到内存对象中完成内存初始化并加载到内核
步骤 2将加载到内核的 eBPF 程序与我们定义的跟踪点关联此处关联后当事件触发时就可运行 eBPF 程序
步骤 3打开与 eBPF 程序约定的通信 perf 通道后续可以持续读取数据
步骤 4循环读取 perf 通道中的数据然后调用 eventHandler.Handle(event) 函数进行后续的各种分析和处理这里主要是数据过滤、聚合分析和缓存等各种我们特定的业务处理。
容器集群部署
在单机上运行 BPF 程序需要特权模式 privileged: true 和挂载宿主机相关目录 /lib/modules和/sys/kernel/debug。同时考虑在版本升级和变更的灰度我们还需要将部署策略 updateStrategy 设置为type: OnDelete。 部署文件主要设置内容如下所示 template:spec:containers:- name: mainimage: repoin.shizhuang-inc.net/component/net_conn_collector:0.1imagePullPolicy: IfNotPresentsecurityContext:privileged: truecapabilities:add:- SYS_ADMINvolumeMounts:- name: modules-hostmountPath: /lib/modulesreadOnly: false- name: debug-fs-hostmountPath: /sys/kernel/debugreadOnly: falsevolumes:- name: modules-hosthostPath:path: /lib/modules- name: debug-fs-hosthostPath:path: /sys/kernel/debugupdateStrategy:type: OnDelete
效果展示
当在部分 K8s 集群灰度上线后我们可直观针对主机上网络连接异常情况基于主机和上下游分析。基于原始数据我们与 CMDB 数据整合打通通过图形化的方式展示异常链路可快速定位到应用上下游大幅提升了问题定位效率。