作者:杨奕 华为云技术规划专家 | 殷森道 华为云高级软件工程师 | 张豪鹏 华为云高级软件工程师
摘要
随着2022年来eBPF的技术大火,该技术以其非侵入的优点在可观测领域开始大放异彩。我们基于eBPF技术也做了许多实践,总的来看,eBPF在网络运维的四层网络客观性方面具备得天独厚的优势,然而在七层(应用流)监控领域仍有力不从心的“死角”,比如对Java应用的观测。我们基于这个场景,结合gala-gopher和Sermant两大利器,通过eBPF对四层的观测,以及Sermant对Java应用七层的观测能力,互相补充网络、应用两个层面的运维能力,真正做到了应用侧/网络侧问题的快速界定。本文亦给出了一个实践案例来呈现效果。
一、 背景
随着2022年来eBPF的技术大火,该技术以其非侵入的优点在可观测领域开始大放异彩。eBPF是一个能够在内核中运行沙箱程序的技术,提供安全的注入代码的机制,通过注入eBPF代码可以安全、高性能地访问整个系统的运行状态。其在OS内核中运行有一个验证器(eBPF Verifier),对注入内核的eBPF代码进行严格把关,保证不侵害内核安全。并且提供了JIT即时编译器,对注入内核的eBPF代码进行即时编译运行。对用户态提供eBPF MAP机制做数据交互。同时,eBPF在用户空间提供了丰富的SDK,支持C/C++、Golang、Rust等编程语言的接口,使不同领域的用户都能使用这门技术,在网络、安全、可观测性和追踪领域都有所建树。eBPF技术全景图如下图[1]所示。
图1-1 eBPF技术架构及其生态
eBPF由于其可通过内核埋入观测点的技术原理,在面向网络流的应用监控方面有着以下天然优势:
1) 面向4层的网络监控,其本身具备对内核“编程”的能力,天然可在内核协议栈中添加观测点;
2) 对于一些7层的网络协议,可简单通过捕获原始报文,在监控程序处进行协议解码,还原业务信息,并进行metrics统计。
但是针对一些比较复杂的7层协议,用eBPF做监控就比较繁琐,甚至力不从心。设想以下几个典型场景:
1) 应用流之间的加密访问场景,比如Java应用默认使用JDK中的JSSE,OS上的OpenSSL,GoSSL,RustSSL等;
2) 复杂RPC协议,如gRPC, Dubbo 3.0等场景下容易导致采集的数据(非业务面数据)丢失。
为了解决以上问题,业界的通用做法是采用eBPF + uprobe方案,去截取带有业务语义的应用层数据。例如,Deepflow、Pixie针对gRPC协议数据采集的做法。
但是基于uprobe的做法同样缺点很明显。主要体现在:
1) 实现比较复杂,要基于三方库协议一个个适配,还要找准uprobe的观测点;
2) 版本兼容难度高,业务层如果有函数改动,则需要重新适配;
3) 如果以上两点开发者还算都能克服,第三点则算是硬伤了,就是针对国内政企行业面向业务开发使用最主流的编程语言——Java,暂时没有比较好的解决方案。
所谓白猫、黑猫,抓到老鼠就是好猫。对于面向应用流的可观测技术,只要能做到非侵入就是好技术。在Java领域,很自然地,有一个技术能补充Java应用场景的可观测性能力,那就是基于字节码增强技术的JavaAgent方案。
这个方案其实在Java领域已经相对比较成熟了,其核心就是利用JavaAgent通过运行时的字节码增强,在Java函数埋入可观测点。在业界也有很多优秀的开源实践,如Skywalking, OpenTelemetry等。
图1-2 JavaAgent技术生态
与eBPF相比,JavaAgent对Java的L7业务层的观测具备更加完备的能力的和优势,同时亦能收到更好的监控效果。但是针对在内核层才能观测到的4层网络指标,JavaAgent则没有太好的解决方案。然而,一个完整应用流的监控产品,不仅需要L7指标,同时也需要L4指标。这样当业务出现问题时,才能第一时间定界到底是应用侧的问题 (L7异常,L4正常), 还是网络侧的问题 (L7, L4均异常)。
为此,针对应用流的可观测场景,能否将JavaAgent和eBPF二者相结合,就成了解决问题的关键。以下则是我们解决这个问题做的一次实践——结合Sermant + gala-gopher的数据采集方案。
二、 Sermant + gala-gopher的数据采集方案
2.1 gala-gopher
gala-gopher是一款C/S架构、基于AI的操作系统亚健康诊断工具。其基于eBPF + Java Agent无侵入观测技术,并以AI技术辅助,实现亚健康故障(比如性能抖动、错误率提升、系统卡顿等问题现象)分钟级诊断,简化IT基础设施的运维过程。架构图如下图[2]所示:
图2-1 gala技术框架
gala的整个架构由gala-gopher、gala-ops两个软件组成,gala-ops安装在管理节点内, gala-gopher安装在生产节点内。
gala-gopher是gala项目内负责数据采集的组件,其为gala项目提供Metrics、Event、Perf等数据,便于gala项目完成系统拓扑的绘制和故障根因的定位。其是一款结合eBPF、Java Agent等非侵入可观测技术的观测平台,探针是gala-gopher用于观测和采集数据的主要工具,通过探针式架构gala-gopher可以轻松实现增加、减少探针。gala-gopher应用drill-down观测,将Linux操作系统层、基础设施层、容器、应用、业务等多层次采集到的数据结合起来,避免形成数据孤岛,从而达到更好的可观测效果。其覆盖了业界主流的开源应用协议的观测,使用非侵入式、零修改的能力使得用户能够安全、便捷地使用,并且基于插件化的架构,也能让用户可以定制化自己的可观测探针,如下图所示:
图2-2 gala-gopher的能力框架
图2-3 gala-gopher能力架构及支持场景
gala-gopher主要包括探针框架和探针程序两部分,探针有Native Probe、Extend Probe两类。Native Probe只能使用C语言实现,Extend Probe不限定编程语言。
图2-4 gala-gopher技术框架
如图所示[3],gala-gopher框架进程负责探针的生命周期管理(探针启停、保活等),收集并处理探针采集到的数据,并提供kafka、prometheus等输出方式。
框架进程中,API模块提供了用户动态配置接口,用户可以自定义开启的探针及每个探针的监控范围;probe-mng模块负责探针的声明周期管理和调度;ipc模块负责与每个探针交互,动态下发监控配置等;ingress负责接收探针,并缓存到imdb模块中;imdb模块负责数据缓存处理;egress负责将结构化数据通过kafka、prometheus等接口形式输出。
而探针分为native probes和extend probes,native probe内嵌于gala-gopher框架进程中,非独立进程运行,且不支持扩展;extend probes则是扩展探针,其以独立的子进程形式运行,在gala-gopher框架下可以自定义扩展。
整体的运行流程为:用户通过访问gala-gopher框架进程提供的动态配置API,向gopher下发探针配置及启动命令;框架进程的probe-mng模块处理配置信息,并通过IPC机制向探针进程下发配置参数等信息,探针根据这些信息动态调整采集行为和监控范围等,最后将采集到的数据通过上报PIPE文件的方式传递给框架进程的ingress。ingress将数据结构化后缓存到imdb中,最终由egress统一输出。
2.1.1 gala-gopher探针扩展机制开发框架
1) 在gopher探针框架中新增扩展的探针类型、配置api、探针子功能(采集子项)
2) 定义meta文件,用于探针框架与探针之间的数据上报的协议约束
3) 编写扩展探针程序
a) 与探针框架建立IPC通道,获取IPC消息,并使能配置
b) 完成自身探针的采集功能
c) 通过PIPE将采集到的数据上送给探针框架
4) 如果探针涉及编译,需要提供build.sh脚本负责本探针的编译(python探针无需提供)
2.2 Sermant
Sermant是基于Java字节码增强技术的无代理的服务网格技术。其利用JavaAgent为宿主应用程序提供增强的服务治理功能,以解决大规模微服务场景中的服务治理问题。Sermant架构如下所示:
图2-5 Sermant架构
Sermant中JavaAgent主要包含两层功能,框架核心层和插件服务层。框架核心层提供Sermant的基本框架功能,以简化插件开发,包括心跳、数据传输、动态配置等。插件服务层为宿主应用提供实际的治理服务。
2.2.1 Sermant的动态插件机制
Sermant的动态插件机制是指Sermant可以在宿主服务运行过程中进行插件的安装和卸载。Sermant支持通过指令的方式进行插件的动态安装和卸载,如下图所示:
图2-6 Sermant动态插件机制
Sermant的动态插件机制主要基于Java Agent的agentmain模式实现,agentmain是Java Agent的入口方法,它可以在Java应用程序启动后动态地加载并运行Java Agent。和premain模式不同,agentmain模式不能通过添加启动参数的方式来连接agent和主程序,需要使用VirtualMachine的attach机制来将agent加载到主程序中, 使用者可以通过Attach参数下发指令来控制Sermant的安装和卸载生命周期。如下所示:
图2-7 Sermant动态插件机制实现方式
2.3 Sermant和gala-gopher的联合方案
Sermant和gala-gopher的联合方案主要是通过gala-gopher的探针扩展机制,将Sermant作为gala-gopher的探针的一部分来进行指标采集和上报,架构图如下所示:
图2-8 gala-gopher和Sermant联合方案的技术架构
从架构图可以看出,Sermant通过JavaAgent技术采集主程序的指标信息,并通过Java Agent技术来解决针对复杂协议eBPF无法进行协议解码、还原业务信息并进行metrics统计的问题。
在扩展探针中,Sermant Probe会接受到gala-gopher下发的配置,Sermant Probe接收到gala-gopher下发的配置之后,会通过IPC进程间通信机制和Sermant的动态安装机制将Sermant安装到Java主程序上,Sermant安装到Java主程序上之后会自动开始采集Java主程序的指标信息。当Sermant Probe接收到Sermant采集的指标信息之后,会根据Meta文件将采集的指标信息解析并通过上报PIPE文件的方式传递给框架进程的ingress。ingress将数据结构化后缓存到imdb中,最终由egress统一输出。
三、联合方案使用案例
以上方案为了引入Sermant对Java应用的Dubbo协议调用性能指标采集的能力,在gala-gopher的基础上扩展了Serment Probe探针。本案例中,分别部署了Dubbo协议的客户端和服务端,作为受观测对象,以此来验证本方案的效果。
3.1 操作指南
3.1.1 编译docker镜像
1) 取包
图3-1 Sermant取包位置
gopher编译出包或release取包指南:https://gitee.com/openeuler/gala-gopher/tree/dev/#rpm%E6%96%B9%E5%BC%8F%E9%83%A8%E7%BD%B2
2) 构建Docker容器镜像,并手动分发到所有K8S负载节点上。
a) 以上步骤得到Sermant软件包及gala-gopher的RPM包,将其放在同一个目录下。
b) 解压Sermant压缩包,将其中的agent整个目录放入当前目录的sermant/目录下。
c) 从gala-gopher源码中取出entrypoint.sh脚本,放到当前目录下,内容如下:
[root@master gopher-sermant-docker]# ll
总用量 952
-rw-r--r--. 1 root root 1121 12月 25 19:54 Dockerfile
-rw-r--r--. 1 root root 2188 12月 25 19:54 entrypoint.sh
-rw-r--r--. 1 root root 961605 12月 25 19:54 gala-gopher-1.0.2-3.x86_64.rpm
drwxr-xr-x. 3 root root 4096 12月 25 19:54 sermant
d) 编写Dockerfile,将上述两个软件打包到同一个Docker镜像中。
# 基础镜像使用OpenEuler-22.03-LTS-SP1
FROM hub.oepkgs.net/openeuler/openeuler_x86_64:22.03-lts-sp1
MAINTAINER GALA
# 指定容器工作目录
WORKDIR /gala-gopher
COPY gala-gopher-1.0.2-3.x86_64.rpm .
# 将当前宿主机目录(gopher的rpm包和Sermant软件包所在目录)下的所有内容都添加到容器的/gala-gopher/目录下
ADD . /gala-gopher
# 赋予entrypoint.sh脚本执行权限
RUN chmod +x /entrypoint.sh
# 在容器中安装gopher
RUN yum localinstall -y gala-gopher-1.0.2-3.x86_64.rpm
# 将Sermant拷贝到容器的/opt/目录下
COPY sermant/ /opt/sermant
# 启动容器进程入口,启动容器
ENTRYPOINT [ "/entrypoint.sh" ]
CMD [ "/usr/bin/gala-gopher" ]
e) 构建Docker镜像,并打上tag为gala-gopher:1.0.2。
1. $ docker build -f Dockerfile -t gala-gopher:1.0.2 .
f) Docker镜像导出到指定目录(本例导出到/tmp/目录下),image_id为上一步构建好的docker镜像的id。
$ docker save {image_id}> /tmp/gala-gopher.docker.tar
g) gDocker镜像导入所有K8S负载节点(Slave),并打上tag,image_id为镜像id。
$ docker load -i /tmp/gala-gopher.docker.tar
$ docker tag {image_id} gala-gopher:1.0.2
3) 运行gopher + Sermant容器并打开四七层探针。
# 使用docker run命令启动容器,以host模式运行,并开启特权模式
$ docker run -d --name gala-gopher --privileged --pid=host --network=host -v /:/host -v /etc/localtime:/etc/localtime:ro -v /sys:/sys -v /usr/lib/debug:/usr/lib/debug -v /var/lib/docker:/var/lib/docker -e GOPHER_HOST_PATH=/host gala-gopher:1.0.2
# 在本机节点上访问gopher的动态配置接口,以开启相关探针(baseinfo、tcpprobe、endpoint、l7probe、Sermant_probe)
$ curl -X PUT http://localhost:9999/baseinfo --data-urlencode json='{"cmd":{"probe":["cpu","mem","nic","disk","net","fs","proc","host"]},"snoopers":{"proc_name":[{"comm":"java","cmdline":"dubbo"}]},"state":"running","params":{"report_period":60}}'
$ curl -X PUT http://localhost:9999/tcp --data-urlencode json='{"cmd":{"probe":["tcp_abnormal","tcp_rtt","tcp_windows","tcp_rate","tcp_rtt","tcp_sockbuf","tcp_stats"]},"snoopers":{"proc_name":[{"comm":"java","cmdline":"dubbo"}]},"state":"running","params":{"report_period":60,"l7_protocol":["http"]}}'
$ curl -X PUT http://localhost:9999/socket --data-urlencode json='{"cmd":{"probe":["tcp_socket"]},"snoopers":{"proc_name":[{"comm":"java","cmdline":"dubbo"}]},"state":"running","params":{"report_period":60,"l7_protocol":["http"]}}'
$ curl -X PUT http://localhost:9999/sermant --data-urlencode json='{"cmd":{"probe":["l7_bytes_metrics","l7_rpc_metrics","l7_rpc_trace"]},"snoopers":{"proc_name":[{"name":"sermantTest","comm":"java","cmdline":"dubbo"}]},"state":"running","params":{"report_period":60,"l7_protocol":["http"]}}'
4) 在Prometheus上配置定期拉取数据的任务。
$ vim /etc/prometheus/prometheus.yml
# 在scrape_configs下添加如下内容,8888端口为gopher的数据端口,可从该端口获取指标数据,{K8S-slave-1_ip}为slave-1节点的ip,slave-2同理
- job_name: "k8s-Slave1-{K8S-slave-1_ip}"
static_configs:
- targets: ["{K8S-slave-1_ip}:8888"]
- job_name: "k8s-Slave2-{K8S-slave-2_ip}"
static_configs:
- targets: ["{K8S-slave-1_ip}:8888"]
5) 在Grafana界面上配置好各个指标的panel以呈现。
3.2 验证实验
为了验证以上效果,本案例设计了Dubbo应用客户端到服务端的调用链,将其部署在K8S上,并开启一个定期打流的任务,定时给consumer-dubbo服务打流,触发调用链。本案例实验的调用链应用以及gala-gopher、Sermant的部署形态如下图所示:
图3-2 实验案例部署视图
在consumer-dubbo、provider-dubbo以及gala-gopher都已正常运行时,访问gala-gopher的动态配置接口下发观测配置,即告知gopher要开启Sermant probe来观测dubbo应用指标,此时可以观察到在gopher的docker容器内开启了Sermant probe探针子进程。随即Sermant probe会将sermant的Java Agent通过动态attach的方式注入到指定的dubbo应用进程中,从而Sermant的java-agent不停地采集应用调用产生的指标数据,并即时地上报给Sermant Probe。Sermant Probe再转而上报给gala-gopher主进程进行“临时归档”。
本案例采用prometheus+grafana的方式进行呈现,在prometheus配置了从每个gala-gopher实例处定时拉取指标数据的策略,并在Grafana界面上配置了Dubbo协议指标相关的panel进行呈现。
3.3 实验效果
实验效果如下图3-3 ~ 图3-6所示,在Grafana界面上呈现了当前实践案例中Dubbo客户端、服务端的时延、吞吐量、错误率等性能指标的变化曲线。
图3-3 Dubbo客户端/服务端请求/响应吞吐量、错误率变化曲线
图3-4 Dubbo客户端/服务端请求/响应吞吐量、错误率变化曲线
图3-5 TCP层观测指标变化曲线(时延、丢包、OOM、未处理字节数)
图3-6 TCP层观测指标变化曲线(重传、丢包、收发(错误)字节数)
3.4 实验总结
我们通过在gala-gopher的基础上扩展了Sermant_probe的能力,通过Sermant探针采集dubbo应用的性能指标,并反馈给gala-gopher进行统一上报。在prometheus+grafana的可视化框架下呈现dubbo协议调用以及网络tcp层面相关的指标变化,可以让用户在界面上直观地观测到四层(网络视角)/七层(应用视角)两个层面的性能指标变化,从而提升运维效率。
参考资料:
[1] eBPF官方文档: https://ebpf.io/what-is-ebpf/
[2] gala项目社区官方文档:https://gitee.com/openeuler/gala-docs/tree/master
[3] gopher探针开发指南:https://gitee.com/openeuler/gala-gopher/blob/dev/doc/how_to_add_probe.md#%E5%BC%80%E5%8F%91%E6%8E%A2%E9%92%88%E5%8A%9F%E8%83%BD
Sermant作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。
- Sermant 官网:https://sermant.io
- GitHub 仓库地址:https://github.com/huaweicloud/Sermant
- 扫码加入 Sermant 社区交流群