网易伏羲私有云基于eBPF的云原生网络可观测性探索与实践
近年来,由于eBPF在Linux内核级别灵活的可编程性、安全性等优势,在云原生网络、安全和可观测性等方面应用广泛。eBPF可以在不侵入任何业务代码的基础上实现云原生应用的可观测性。但是eBPF对Linux内核版本是有一定要求的(4.14以上),伏羲私有云部分生产集群的内核版本比较低,升级内核会影响大量线上应用,成本太高。
而kindling正好在基于eBPF实现云原生可观测性能力的基础上,借助内核模块技术在低版本Linux内核上实现了等价于eBPF的相关能力。本文会介绍一些伏羲私有云基于eBPF和kindling在云原生可观测性领域的一些探索。
背景
伏羲私有云的监控系统是基于主流的Prometheus+Grafana实现的,能较好地满足大部分日常的监控需求,但是随着业务的发展,内部多个RPC等自研网络通信协议被广泛应用,对监控系统提出了更高的要求。目前主要面临的挑战有:
• 伏羲私有云内部有多个自研的网络通信协议被各项业务广泛使用,如何低成本、灵活地获取使用业务自研协议相关的监控指标?
• 业务请求链路上内部服务众多,如何在不侵入业务代码的情况下,为没有经过kong代理对外暴露的内部服务提供延时和TPS等监控指标,以便更好地了解服务的运行状态和资源需求。基于TPS还可以为服务实现自动化的扩缩容,提升集群的资源利用率,实现降本增效。
eBPF与kindling
eBPF简介
eBPF为Extended Berkeley Packet Filter的缩写,字面意思为扩展的伯克利数据包过滤器,经过扩展,eBPF的功能远比包过滤器强大得多。eBPF是一个框架,允许用户在操作系统的内核中加载和运行自定义程序,这意味着它可以扩展甚至修改内核的行为。eBPF程序加载和运行架构图如下图所示:
eBPF相关技术原理不是本文重点,不再赘述。近年来,eBPF之所在云原生领域发展迅速,主要得益于几个优势:
• eBPF有一套完整的验证机制来保障eBPF程序的安全性,只有当eBPF程序代码通过eBPF验证器(verifier)的安全性分析后,才能被允许加载到Linux内核中运行,且eBPF验证器能确保eBPF程序不能访问任意内存空间,只能访问特定套接字缓冲区(socket buffer)中的数据;
• eBPF程序可以动态加载到内核中,在无需重启Linux系统的情况下,实现对内核自定义功能的增减,且性能开销较小;
• eBPF程序是事件驱动的,功能强大,将原本单一的数据包过滤事件扩展到了内核态函数、用户态函数、跟踪点、性能事件、安全控制等领域,不同的eBPF程序可以由不同类型的事件触发运行。
• Kubernetes的可观测性和安全工具大多都采用了sidecar模式,但是当pod数量很多时,对应的sidecar容器也会很多,会占用大量资源,而eBPF直接在内核级别就能实现可观测性,无需运行sidecar容器。
kindling简介
kindling是一款基于eBPF的云原生可观测性开源工具,旨在帮助用户更好、更快地定界(triage)云原生系统故障。通过kindling,用户可以快速定界问题类型,比如是应用代码问题还是基础设施问题。如果是代码问题,可以借助APM(Application Performance Monitoring应用性能监控)监控进一步排查问题;如果是基础设施问题,那么通过分析来自内核的相关监控指标,定位故障点。
kindling架构如下图所示:
kindling整体架构包含三个部分:用户态Go程序、用户态C/C++程序和内核态drivers程序。用户态Go程序满足的是上层可观测需求的开发,其他两个部分实现是内核需求的开发。
伏羲私有云之所以选择kindling进行基于eBPF的云原生可观测性探索,是经过大量调研后综合考虑的结果,主要因为kindling有以下几个优势:
• kindling兼容不支持eBPF的低版本Linux内核(4.14以下),通过内核模块的形式在低版本Linux内核实现了等价于eBPF的云原生可观测能力;
• kindling-collector部分模块集成了Opentelemetry的SDK,这样kindling的指标在输出时有较高的灵活性,可以输出到opentelemetry collector、Prometheus等多种可观测性平台;
• kindling自研了一个grafana插件,提供了方便的云原生监控的可视化功能;
• kindling部署维护成本较低,以daemonset的形式运行在Kubernetes集群中,且性能开销较低。
探索与实践
伏羲私有云基于eBPF和kindling在云原生可观测性领域探索和实践中取得了一些阶段性工作成果,并回馈开源社区做了一些贡献。
自动化适配任意Linux内核版本
kindling采用的内核模块或eBPF形式的探针工作在内核态,和内核版本强绑定,为了程序的准确性,目前是完全匹配内核版本号,使用时需要为每一个内核版本单独编译探针。编译依赖Linux内核头文件,但我们的集群众多,不同时期的集群内核版本有所差异,即使在同一个集群内,也有不同批次的机器加入集群的情况,同时共存了多个内核版本。构建kindling镜像时就会面临以下两个问题:
1. 构建成本高,需要手动在不同内核版本的节点上执行构建镜像的操作,另外出于安全考虑,线上集群原则上是不能开放过多的操作权限的,下载头文件 -> 编译探针 -> 构建镜像的操作流程需要优化;
2. 部分内核版本的头文件已被Debian软件源移除,无法直接下载。
对此,我们实现了一套自动化适配任意Linux内核版本的kindling镜像构建流程,大幅提升了基于kindling进行二次开发的工作效率,整体流程如下图所示:
其核心在于:
1. 将构建镜像流程容器化,在docker容器中执行下载头文件 -> 编译探针 -> 构建镜像的操作;
2. 通过snapshot.debian.org时光机自动匹配下载已被Debian官方源移除的内核头文件。
伏羲私有云自研RPC协议解析方案设计与实现
伏羲私有云分析得出kindling现有能力存在一定缺陷,针对自研RPC协议的特性提出了自研的解决方案,基于kindling源码二次开发实现并验证了方案的正确性,在对业务代码零侵入性的基础上,实现了自研协议的可观测性能力,并回馈社区,参与到kindling协议解析核心流程改进方案的设计和验证工作中。
kindling缺陷分析
kindling目前支持为使用HTTP、DNS、MySQL等通用协议的服务提供请求级的RED指标(TPS,错误率、延时)。伏羲私有云内部很多服务使用自研协议进行通信,需要基于kindling进行二次开发,解析自研协议,来获取这些服务的RED指标。
在开发测试过程中,我们发现,由于自研协议通信场景相较于HTTP更为复杂,当测试用例中存在以下三种情况之一时,kindling提供的tps指标不准确:
1. 客户端异步的向服务端发送多次请求;
2. 服务端在响应某些请求前,会先请求客户端;
3. 服务端主动向客户端发送消息,而且不要求得到响应。
通信场景建模
根据自研协议的特性,我们将通信场景进行建模,以下三种通信场景是kindling已有的协议解析流程不能支持的。
• 双工通信:客户端和服务端都可以向对方发起请求;
• 流式通信:客户端和服务端建立的是TCP长连接,在TCP长连接上并发发生多次网络调用,客户端在未收到前一次系统调用的响应之前就发送下一次网络调用的请求,同时允许服务端乱序响应,如下图所示,服务端首先响应response2,后响应response1;
• 单向请求:服务端会主动向客户端发送消息,而且不要求得到响应。
自研协议解析
自研协议简介
我们选取其中一个RPC自研协议(以下简称gateway协议,已脱敏)进行介绍,它的协议格式和字段含义如下:
gateway协议用在客户端和网关的通信中,网关代理了客户端和服务端的通信。客户端请求网关时,service_id字段标识真正要请求的服务,sub_service_id是二级服务标识,标识service_id服务的某个接口,网关根据这两个字段把请求转发给对应服务的对应接口。body_size字段标识body字段的长度,body字段存放请求和响应内容。响应报文body字段前4个字节必须是int32类型的字节码。session_id为会话标识,访问无状态服务时可为空字符串,访问有状态服务时如果为空,代表开启一个新的会话,由gateway根据负载均衡策略分配session_id,交由响应带回,下次请求若要保持会话则需携带。
解析流程设计与实现
解析流程中,我们专门为gateway协议开启一条旁路,优先按照gateway协议格式解析输入的数据包。内存中维护gatewayMap哈希结构,记录所有按照gateway协议格式的tcp连接,对于系统捕获的每一个kindlingEvent,获取唯一标识TCP连接的PID+FD信息。若gatewayMap记录该TCP连接采用gateway协议,则数据包送入gateway专属解析流程处理;若requestMonitor记录该TCP连接非gateway协议,数据包送入已有解析流程处理。对于新建立的TCP连接,两个哈希表中均无记录,则按照gateway请求报文格式解析数据包,判断是否属gateway协议,对应更新哈希表。
gateway协议专属解析流程进行处理时,判断数据包是请求报文或响应报文,分别按照请求/响应报文的格式解析,若解析失败,则直接丢弃数据包;若因数据包太短无法完成解析,则停止处理,等待合并下一数据包内容,重新解析。成功解析出gateway请求报文时,先将gateway请求详情缓存到requestMap中,然后,数据包截掉当前请求报文,剩余部分继续进入请求报文的解析流程。成功解析出gateway响应报文时,从requestMap匹配对应请求详情,匹配失败则丢弃响应,匹配成功即完整构成一次网络调用详情,生成DataGroup。
gateway协议专属解析流程中涉及包太短等待合并、包太长需要分隔。为了便于进行两种操作,用ByteStream存储TCP链接连续出现的数据包。它是类似队列的数据结构,入队KindingEvent,出队字节数组。
基于TPS的内部服务自动扩缩容,实现降本增效
伏羲私有云上除了通过kong代理对外暴露的服务外,还有很多应用是作为服务调用链的中间服务存在的,并没有通过kong代理对外暴露,因此现有的监控系统得不到基于kong的服务TPS监控指标。
如果需要对这些应用进行TPS监控,就需要对业务代码进行侵入性改造,而基于eBPF和kindling则可以在完全不侵入业务代码的情况下,通过基础指标过滤、转换得到应用的TPS指标,伏羲私有云用户可以根据业务特点在页面上配置基于TPS的自动扩缩容策略,有助于提升集群的资源利用率,实现降本增效。
参与开源社区
伏羲私有云在eBPF和kindling的云原生可观测性的探索和实践中,给社区提了多个issue和PR,在开源技术生态建设中与社区保持沟通与合作,下文介绍其中两个案例。
部署更新kindling流程优化
在基于kindling进行二次开发的过程中,我们会在集群中频繁批量更新部署kindling daemonset服务,却发现kubernetes API Server所在节点的监控数据会伴随着出现异常,在kindling批量删除重建的时间点,正好对应了节点上ETCD磁盘IO负载和CPU负载飙升的情况。
通过阅读源码,我们定位到kindling程序刚启动时会从API Server同步集群全部的Pod、Service、ReplicateSet、Node等资源信息,并且确认调用API Server接口时带有参数"re source_version=0"(会默认优先读缓存数据),通过查看集群日志,我们进一步发现是因为开发集群API Server的内存配额较小,直接OOM重启了,这一过程中kindling还在大量并发向API Server发送查询请求,连锁引发了ETCD的负载飙升。
经过和社区沟通确认,我们做了如下操作优化kindling的部署更新流程:
• 修改了kindling源码,使其同步集群的deployment作为各指标的workload_name标签,而不是replicateSet,从而大幅减少同步的数据量;
• 滚动更新,为限制更新kindling时对API Server的并发请求量,我们让kindling分批次滚动更新,分批的间隔时间作为可配置项;
• 渐次部署,为限制首次部署kindling时对API Server的并发请求量,我们限制kindling只部署在有特定标签的节点上,并通过脚本控制节点标签的增删操作,从而控制kindling整体的部署进度。
提供容器中编译topo plugin的方法
kindling提供了一个Grafana插件(topo plugin)用来绘制网络调用拓扑图,该插件尚未通过Grafana官方的认证,无法从应用商店直接安装使用。kindling只提供了一个预置了topo plugin插件的Grafana镜像供用户试用,缺少该插件的可执行文件。为此,提供了在容器中编译topo plugin的方法,免于在宿主机配置node.js环境(#332)。
总结与展望
通过一段时间的探索和实践,伏羲私有云在基于eBPF技术生态的云原生可观测领域取得了一些阶段性成果,与开源社区保持着良好的沟通与合作,也回馈社区参与到eBPF开源技术生态的建设中。
近年来,eBPF技术发展十分迅速,相关的云原生可观测性领域的技术生态也非常活跃,但是目前成熟稳定的基础设施还相对较少,将eBPF技术生态在生产环境落地的门槛还是很高,一些比较热门的开源项目,比如cilium等,在我们的实际工作中经过测试和试用,发现与我们的预期还有一定差距,我们会对基于eBPF的云原生可观测领域保持关注。
作者介绍
石钟浩,网易伏羲高级平台开发工程师。
张龙,网易伏羲平台开发实习生。