ebpf linux 内核 linux内核开发
一、内内核Cilium 原理解析:网络数据包在内核中的内内核流转过程
Cilium,作为一款云原生网络插件,内内核凭借eBPF技术,内内核革新了kube-proxy的内内核功能。它在数据包处理中的内内核核心运作机制,如同一场精密的内内核交响乐,从网络请求的内内核源头直至响应的终点,每一个环节都彰显了其卓越性能。内内核让我们深入解析这个过程,内内核跟随数据包在Linux内核中的内内核奇妙旅程。
首先,内内核从用户发起请求的内内核那一刻起,经过DNS解析,内内核数据包通过应用层(如浏览器)传递到操作系统。内内核在这个阶段,内核扮演着关键角色,通过TCP/IP模型,对数据包进行TCP/UDP传输处理。接着,IP封装和MAC处理确保数据包准确无误地到达目标网络接口。Cilium凭借eBPF的强大支持,将自身嵌入这个流程,实现了高效的数据包处理。
Linux网络协议栈是Cilium施展魔法的舞台,包括Socket层、传输层、网络层和接口层,底层则是网卡驱动与硬件设备的交互。Cilium巧妙地利用了BPF(Berkeley Packet Filter),在数据包的每个处理阶段,无论是L1-L2的物理-数据链路层,还是在XDP(eXtensible Data Path)的底层网络处理,都能发挥关键作用。
具体到接收网络包的流程,NAPI(Network Adaptation Interface)机制结合中断和轮询,使得网卡驱动在DMA区域轮询时,能高效地处理RX队列,避免过多的CPU开销。XDP则提供了三种模式,Native、Offloaded和Generic,其中Native和Offloaded在性能上更优,是生产环境中的首选。XDP不仅能进行基本的数据包检查,还能执行负载均衡策略,通过早期丢包来防范DDoS攻击。
Cilium的eBPF Datapath在数据包从硬件到内核,再到用户空间的流转过程中,通过clean_rx()创建skb,处理统计信息和校验和,同时GRO(Generic Receive Offloading)功能则合并小包,减少CPU消耗。然后,receive_skb()处理XDP后的数据,进一步进行L2到L3的通用处理,以及对Tap设备的二层协议支持。
在用户空间,L4层的处理更为细致,如UDP接收,经过ip_rcv()、ip_routing()等步骤,最终到达socket接收队列。这个过程中的每一步,都伴随着对socket操作的抽象,以及与进程的紧密协作。Cilium巧妙地整合了epoll实例、UDP socket监听和eBPF负载均衡,为用户提供强大而灵活的网络控制。
理解Cilium的数据包收发和转发路径,对优化其性能至关重要。深入研究Nathan Sweet的DigitalOcean演讲[1]、Cilium eBPF发送路径详解[2]、Linux发包过程[3]、GRO详解[4],以及Linux内核官方文档[5]和接收过程图解[7],将帮助我们更好地掌握Cilium背后的精髓。
总之,Cilium凭借eBPF的力量,编织了一张高效的网络数据包处理网,将复杂的内核操作转化为简洁而强大的功能。掌握这一原理,无疑为网络运维和优化提供了强大的工具。现在,让我们一起探索Cilium如何在内核的深处实现网络流量的无缝转换和处理吧!
二、内核4.14.117什么意思
中国武术博大精深,其中太极作为不以拙力胜人的功夫备受推崇。同样如果从攻击的角度窥视漏洞领域,也不难看出攻防之间的博弈不乏“太极”的身影,轻巧稳定易利用的漏洞与工具往往更吸引黑客,今天笔者要着墨分析的就是这样一个擅长“四两拨千斤”的0day漏洞。
0day漏洞的攻击威力想必大家都听说过,内核0day更因为其影响范围广,修复周期长而备受攻击者的青睐。近期,国外安全研究者Vitaly Nikolenko在twitter[1]上公布了一个Ubuntu 16.04的内核0day利用代码[2],攻击者可以无门槛的直接利用该代码拿到Ubuntu的最高权限(root);虽然只影响特定版本,但鉴于Ubuntu在全球拥有大量用户,尤其是公有云用户,所以该漏洞对企业和个人用户还是有不小的风险。
笔者对该漏洞进行了技术分析,不管从漏洞原因还是利用技术看,都相当有代表性,是Data-Oriented Attacks在linux内核上的一个典型应用。仅利用传入的精心构造的数据即可控制程序流程,达到攻击目的,完全绕过现有的一些内存防护措施,有着“四两拨千斤”的效果。
0×02漏洞原因
这个漏洞存在于Linux内核的eBPF模块,我们先来简单了解下eBPF。
eBPF(extended Berkeley Packet Filter)是内核源自于BPF的一套包过滤机制,严格来说,eBPF的功能已经不仅仅局限于网络包过滤,利用它可以实现kernel tracing,tracfic control,应用性能监控等强大功能。为了实现如此强大的功能,eBPF提供了一套类RISC指令集,并实现了该指令集的虚拟机,使用者通过内核API向eBPF提交指令代码来完成特定的功能。
看到这里,有经验的安全研究者可能会想到,能向内核提交可控的指令代码去执行,很可能会带来安全问题。事实也确实如此,历史上BPF存在大量漏洞 [3]。关于eBPF的更多细节,可以参考这里[4][5]。
eBPF在设计时当然也考虑了安全问题,它在内核中实现了一套verifier机制,过滤不合规的eBPF代码。然而这次的漏洞就出在eBPF的verifier机制。
从最初Vitaly Nikolenko公布的补丁截图,我们初步判断该漏洞很有可能和CVE-2017-16995是同一个漏洞洞[6],但随后有2个疑问:
1.CVE-2017-16995在去年12月份,内核4.9和4.14及后续版本已经修复,为何Ubuntu使用的4.4版本没有修复?2.CVE-2017-16995是Google Project Zero团队的Jann Horn发现的eBPF漏洞,存在于内核4.9和4.14版本[7],作者在漏洞报告中对漏洞原因只有简短的描述,跟本次的漏洞是否完全相同?
注:笔者所有的代码分析及调试均基于Ubuntu 14.04,内核版本为4.4.0-31-generic#50~14.04.1-Ubuntu[8]。
先来回答第二个问题,中间的调试分析过程在此不表。
参考以下代码,eBPF的verifer代码(kernel/bpf/verifier.c)中会对ALU指令进行检查(check_alu_op),该段代码最后一个else分支检查的指令是:
1.BPF_ALU64|BPF_MOV|BPF_K,把64位立即数赋值给目的寄存器;2.BPF_ALU|BPF_MOV|BPF_K,把32位立即数赋值给目的寄存器;
但这里并没有对2条指令进行区分,直接把用户指令中的立即数insn->imm赋值给了目的寄存器,insn->imm和目的寄存器的类型是integer,这个操作会有什么影响呢?
参考以下代码,上面2条ALU指令分别对应ALU_MOV_K和ALU64_MOV_K,可以看出verifier和eBPF运行时代码对于2条指令的语义解释并不一样,DST是64bit寄存器,因此ALU_MOV_K得到的是一个32bit unsigned integer,而ALU64_MOV_K会对imm进行sign extension,得到一个signed 64bit integer。至此,我们大概知道漏洞的原因,这个逻辑与CVE-2017-16995基本一致,虽然代码细节上有些不同(内核4.9和4.14对verifier进行了较大调整)。但这里的语义不一致又会造成什么影响?
我们再来看下vefier中以下代码(check_cond_jmp_op),这段代码是对BPF_JMP|BPF_JNE|BPF_IMM指令进行检查,这条指令的语义是:如果目的寄存器立即数==指令的立即数(insn->imm),程序继续执行,否则执行pc+off处的指令;注意判断立即数相等的条件,因为前面ALU指令对32bit和64bit integer不加区分,不论imm是否有符号,在这里都是相等的。再看下eBPF运行时对BPF_JMP|BPF_JNE|BPF_IMM指令的解释(bpf_prog_run),显然当imm为有符合和无符号时,因为sign extension,DST!=IMM结果是不一样的。注意这是条跳转指令,这里的语义不一致后果就比较直观了,相当于我们可以通过ALU指令的立即数,控制跳转指令的逻辑。这个想象空间就比较大了,也是后面漏洞利用的基础,比如可以控制eBPF程序完全绕过verifier机制的检查,直接在运行时执行恶意代码。
值得一提的是,虽然这个漏洞的原因和CVE-2017-16995基本一样,但但控制跳转指令的思路和CVE-2017-16995中Jann Horn给的POC思路并不一样。感兴趣的读者可以分析下,CVE-2017-16995中POC,因为ALU sign extension的缺陷,导致eBPF中对指针的操作会计算不正确,从而绕过verifier的指针检查,最终读写任意kernel内存。但这种利用方法,在4.4的内核中是行不通的,因为4.4内核的eBPF不允许对指针类型进行ALU运算。
到这里,我们回过头来看下第一个问题,既然漏洞原因一致,为什么Ubuntu 4.4的内核没有修复该漏洞呢?和Linux kernel的开发模式有关。
Linux kernel分mainline,stable,longterm 3种版本[9],一般安全问题都会在mainline中修复,但对于longterm,仅会选择重要的安全补丁进行backport,因此可能会出现,对某个漏洞不重视或判断有误,导致该漏洞仍然存在于longterm版本中,比如本次的4.4 longterm,最初Jann Horn并没有在报告中提到影响4.9以下的版本。
关于Linux kernel对longterm版本的维护,争论由来已久[10],社区主流意见是建议用户使用最新版本。但各个发行版(比如Ubuntu)出于稳定性及开发成本考虑,一般选择longterm版本作为base,自行维护一套kernel。
对于嵌入式系统,这个问题更严重,大量厂商代码导致内核升级的风险及成本都远高于backport安全补丁,因此大部分嵌入式系统至今也都在使用比较老的longterm版本。比如Google Android在去年Pixel/Pixel XL 2发布时,内核版本才从3.18升级到4.4,原因也许是3.18已经进入EOL了(End of Life),也就是社区要宣布3.18进入死亡期了,后续不会在backport安全补丁到3.18,而最新的mainline版本已经到了4.16。笔者去年也在Android kernel中发现了一个未修复的历史漏洞(已报告给google并修复),但upstream在2年前就修复了。
而Vitaly Nikolenko可能是基于CVE-2017-16995的报告,在4.4版本中发现存在类似漏洞,并找到了一个种更通用的利用方法(控制跳转指令)。
0×03漏洞利用
根据上一节对漏洞原因的分析,我们利用漏洞绕过eBPF verifier机制后,就可以执行任意eBPF支持的指令,当然最直接的就是读写任意内存。漏洞利用步骤如下:
1.构造eBPF指令,利用ALU指令缺陷,绕过eBPF verifier机制;
2.构造eBPF指令,读取内核栈基址;
3.根据泄漏的SP地址,继续构造eBPF指令,读取task_struct地址,进而得到task_struct->cred地址;
4.构造eBPF指令,覆写cred->uid, cred->gid为0,完成提权。
漏洞利用的核心,在于精心构造的恶意eBPF指令,这段指令在Vitaly Nikolenko的exp中是16机制字符串(char*__prog),并不直观,笔者为了方便,写了个小工具,把这些指令还原成比较友好的形式,当然也可以利用eBPF的调试机制,在内核log中打印出eBPF指令的可读形式。我们来看下这段eBPF程序,共41条指令(笔者写的小工具的输出):
parsing eBPF prog, size 328, len 41 ins 0: code(b4) alu|=| imm, dst_reg 9, src_reg 0, off 0, imm ffffffff ins 1: code(55) jmp|!=| imm, dst_reg 9, src_reg 0, off 2, imm ffffffff ins 2: code(b7) alu64|=| imm, dst_reg 0, src_reg 0, off 0, imm 0 ins 3: code(95) jmp| exit| imm, dst_reg 0, src_reg 0, off 0, imm 0 ins 4: code(18) ld| BPF_IMM| u64, dst_reg 9, src_reg 1, off 0, imm 3 ins 5: code(00) ld| BPF_IMM| u32, dst_reg 0, src_reg 0, off 0, imm 0 ins 6: code(bf) alu64|=| src_reg, dst_reg 1, src_reg 9, off 0, imm 0 ins 7: code(bf) alu64|=| src_reg, dst_reg 2, src_reg a, off 0, imm 0 ins 8: code(07) alu64|+=| imm, dst_reg 2, src_reg 0, off 0, imm fffffffc ins 9: code(62) st| BPF_MEM| u32, dst_reg a, src_reg 0, off fffffffc, imm 0 ins 10: code(85) jmp| call| imm, dst_reg 0, src_reg 0, off 0, imm 1 ins 11: code(55) jmp|!=| imm, dst_reg 0, src_reg 0, off 1, imm 0 ins 12: code(95) jmp| exit| imm, dst_reg 0, src_reg 0, off 0, imm 0 ins 13: code(79) ldx| BPF_MEM| u64, dst_reg 6, src_reg 0, off 0, imm 0 ins 14: code(bf) alu64|=| src_reg, dst_reg 1, src_reg 9, off 0, imm 0 ins 15: code(bf) alu64|=| src_reg, dst_reg 2, src_reg a, off 0, imm 0 ins 16: code(07) alu64|+=| imm, dst_reg 2, src_reg 0, off 0, imm fffffffc ins 17: code(62) st| BPF_MEM| u32, dst_reg a, src_reg 0, off fffffffc, imm 1 ins 18: code(85) jmp| call| imm, dst_reg 0, src_reg 0, off 0, imm 1 ins 19: code(55) jmp|!=| imm, dst_reg 0, src_reg 0, off 1, imm 0 ins 20: code(95) jmp| exit| imm, dst_reg 0, src_reg 0, off 0, imm 0 ins 21: code(79) ldx| BPF_MEM| u64, dst_reg 7, src_reg 0, off 0, imm 0 ins 22: code(bf) alu64|=| src_reg, dst_reg 1, src_reg 9, off 0, imm 0 ins 23: code(bf) alu64|=| src_reg, dst_reg 2, src_reg a, off 0, imm 0 ins 24: code(07) alu64|+=| imm, dst_reg 2, src_reg 0, off 0, imm fffffffc ins 25: code(62) st| BPF_MEM| u32, dst_reg a, src_reg 0, off fffffffc, imm 2 ins 26: code(85) jmp| call| imm, dst_reg 0, src_reg 0, off 0, imm 1 ins 27: code(55) jmp|!=| imm, dst_reg 0, src_reg 0, off 1, imm 0 ins 28: code(95) jmp| exit| imm, dst_reg 0, src_reg 0, off 0, imm 0 ins 29: code(79) ldx| BPF_MEM| u64, dst_reg 8, src_reg 0, off 0, imm 0 ins 30: code(bf) alu64|=| src_reg, dst_reg 2, src_reg 0, off 0, imm 0 ins 31: code(b7) alu64|=| imm, dst_reg 0, src_reg 0, off 0, imm 0 ins 32: code(55) jmp|!=| imm, dst_reg 6, src_reg 0, off 3, imm 0 ins 33: code(79) ldx| BPF_MEM| u64, dst_reg 3, src_reg 7, off 0, imm 0 ins 34: code(7b) stx| BPF_MEM| u64, dst_reg 2, src_reg 3, off 0, imm 0 ins 35: code(95) jmp| exit| imm, dst_reg 0, src_reg 0, off 0, imm 0 ins 36: code(55) jmp|!=| imm, dst_reg 6, src_reg 0, off 2, imm 1 ins 37: code(7b) stx| BPF_MEM| u64, dst_reg 2, src_reg a, off 0, imm 0 ins 38: code(95) jmp| exit| imm, dst_reg 0, src_reg 0, off 0, imm 0 ins 39: code(7b) stx| BPF_MEM| u64, dst_reg 7, src_reg 8, off 0, imm 0 ins 40: code(95) jmp| exit| imm, dst_reg 0, src_reg 0, off 0, imm 0 parsed 41 ins, total 41
稍微解释下,ins 0和 ins 1一起完成了绕过eBPF verifier机制。ins 0指令后,regs[9]= 0xffffffff,但在verifier中,regs[9].imm=-1,当执行ins 1时,jmp指令判断regs[9]== 0xffffffff,注意regs[9]是64bit integer,因为sign extension,regs[9]== 0xffffffff结果为false,eBPF跳过2(off)条指令,继续往下执行;而在verifier中,jmp指令的regs[9].imm== insn->imm结果为true,程序走另一个分支,会执行ins 3 jmp|exit指令,导致verifier认为程序已结束,不会去检查其余的dead code。
这样因为eBPF的检测逻辑和运行时逻辑不一致,我们就绕过了verifier。后续的指令就是配合用户态exp完成对kernel内存的读写。
这里还需要知道下eBPF的map机制,eBPF为了用户态更高效的与内核态交互,设计了一套map机制,用户态程序和eBPF程序都可以对map区域的内存进行读写,交换数据。利用代码中,就是利用map机制,完成用户态程序与eBPF程序的交互。
ins4-ins5: regs[9]= struct bpf_map*map,得到用户态程序申请的map的地址,注意这2条指令,笔者的静态解析并不准确,获取map指针的指令,在eBPF verifier中,会对指令内容进行修改,替换map指针的值。
ins6-ins12:调用bpf_map_lookup_elem(map,&key),返回值为regs[0]=&map->value[0]
ins13: regs[6]=*regs[0], regs[6]得到map中key=0的value值
ins14-ins20:继续调用bpf_map_lookup_elem(map,&key),regs[0]=&map->value[1]
ins21: regs[7]=*regs[0],regs[7]得到map中key=1的value值
ins22-ins28:继续调用bpf_map_lookup_elem(map,&key),regs[0]=&map->value[2]
ins29: regs[8]=*regs[0],regs[8]得到map中key=2的value值
ins30: regs[2]= regs[0]
ins32: if(regs[6]!= 0) jmp ins32+ 3,根据用户态传入的key值不同,做不同的操作
ins33: regs[3]=*regs[7],读取regs[7]中地址的内容,用户态的read原语,就在这里完成,regs[7]中的地址为用户态传入的任意内核地址
ins34:*regs[2]= regs[3],把上调指令读取的值返回给用户态
ins36: if(regs[6]!= 1) jmp ins36+ 2
ins37:*regs[2]= regs[FP],读取eBPF的运行时栈指针,返回给用户态,注意这个eBPF的栈指针实际上指向bpf_prog_run函数中的一个局部uint64数组,在内核栈上,从这个值可以得到内核栈的基址,这段指令对应用户态的get_fp
ins39:*regs[7]= regs[8],向regs[7]中的地址写入regs[8],对应用户态的write原语,regs[7]中的地址为用户态传入的任意内核地址
理解了这段eBPF程序,再看用户态exp就很容易理解了。需要注意的是,eBPF指令中的3个关键点:泄漏FP,读任意kernel地址,写任意kernel地址,在verifier中都是有检查的,但因为开始的2条指令完全绕过了verifier,导致后续的指令长驱直入。
笔者在Ubuntu 14.04上提权成功:这种攻击方式和传统的内存破坏型漏洞不同,不需要做复杂的内存布局,只需要修改用户态传入的数据,就可以达到控制程序指令流的目的,利用的是原有程序的正常功能,会完全绕过现有的各种内存防御机制(SMEP/SMAP等),有一种四两拨千斤的效果。这也是这两年流行的Data-Oriented Attacks,在linux kernel中似乎并不多见。
0×04漏洞影响范围&修复
因为linux kernel的内核版本众多,对于安全漏洞的影响范围往往并不容易确认,最准确的方式是搞清楚漏洞根因后,从代码层面判断,但这也带来了高成本的问题,快速应急时,我们往往需要尽快确认漏洞影响范围。从前面的漏洞原理来看,笔者大致给一个全面的linux kernel受影响版本:
3.18-4.4所有版本(包括longterm 3.18,4.1,4.4);
<3.18,因内核eBPF还未引入verifier机制,不受影响。
对于大量用户使用的各个发行版,还需要具体确认,因为该漏洞的触发,还需要2个条件
1.Kernel编译选项CONFIG_BPF_SYSCALL打开,启用了bpf syscall;
2./proc/sys/kernel/unprivileged_bpf_disabled设置为0,允许非特权用户调用bpf syscall
而Ubuntu正好满足以上3个条件。关于修复,upstream kernel在3月22日发布的4.4.123版已经修复该漏洞[11][12], Ubuntu官方4月5日也正式发布了安全公告和修复版本[13][14],没有修复的同学可以尽快升级了。
但现在距漏洞Exp公开已经过去20多天了,在漏洞应急时,我们显然等不了这么久,回过头看看当初的临时修复方案:
1.设置/proc/sys/kernel/unprivileged_bpf_disabled为1,也是最简单有效的方式,虽然漏洞仍然存在,但会让exp失效;
2.使用Ubuntu的预发布源,更新Ubuntu 4.4的内核版本,因为是非正式版,其稳定性无法确认。
Vitaly Nikolenko在twitter上公布的Ubuntu预发布源:all 4.4 ubuntu aws instances are vulnerable: echo“deb restricted main multiverse universe”>/etc/apt/sources.list&& apt update&& apt install linux-image-4.4.0-117-generic
Ubuntu的非正式内核版本,做了哪些修复,我们可以看下补丁的关键内容(注意这是Ubuntu的kernel版本,非upstream):
git diff Ubuntu-lts-4.4.0-116.140_14.04.1 Ubuntu-lts-4.4.0-117.141_14.04.1ALU指令区分了32bit和64bit立即数,同时regs[].imm改为了64bit integer
我们再看下upstream kernel 4.4.123的修复,相比之下,要简洁的多,仅有3行代码改动[12]:
当处理32bit ALU指令时,如果imm为负数,直接忽略,认为是UNKNOWN_VALUE,这样也就避免了前面提到的verifer和运行时语义不一致的问题。
我们回顾以下整个漏洞分析过程,有几点值得注意和思考:
1.eBPF作为内核提供的一种强大机制,因为其复杂的过滤机制,稍有不慎,将会引入致命的安全问题,笔者推测后续eBPF可能还会有类似安全漏洞。2.受限于linux kernel的开发模式及众多版本,安全漏洞的确认和修复可能存在被忽视的情况,出现N day变0 day的场景。
3.Vitaly Nikolenko公布漏洞exp后,有网友就提出了批评,在厂商发布正式补丁前,不应该公布细节。我们暂且不讨论Vitaly Nikolenko的动机,作为一名安全从业者,负责任的披露漏洞是基本守则。
4.笔者所在公司使用的OS是经过专门的团队量身定制,进行了不少的安全加固和冗余组件裁剪,故不受到此次漏洞影响。可见维护一个安全可靠的OS不失为保障大型企业的安全方案之一。
感谢阅读,行文匆忙,如有不正之处,敬请指出。
0×06参考文档
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
[11]
[12]
[13]
[14]
*本文来自腾讯安全应急响应中心(TSRC)投稿,作者:Tencent Blade Team:Cradmin,转载须注明原文链接及出处。
三、ebfp什么意思
关于EBFP什么意思如下:
一、EBFP是什么
EBPF是一个能够在内核运行沙箱程序的技术,提供了一种在内核事件和用户程序事件发生时安全注入代码的机制,使得非内核开发人员也可以对内核进行控制。
随着内核的发展,eBPF逐步从最初的数据包过滤扩展到了网络、内核、安全、跟踪等,而且它的功能特性还在快速发展中,早期的BPF被称为经典BPF,简称cBPF,正是这种功能扩展,使得现在的BPF被称为扩展BPF,简称eBPF。
二、EBPF为什么会出现?
EBPF的出现本质上是为了解决内核迭代速度慢和系统需求快速变化的矛盾,在eBPF领域常用的一个例子是eBPF相对于Linux Kernel类似于Javascript相对于HTML,突出的是可编程性。
一般来说可编程性的支持通常会带来一些新的问题,比如内核模块其实也是为了解决这个问题,但是他没有提供很好的边界,导致内核模块会影响内核本身的稳定性,在不同的内核版本需要做适配等。eBPF采用以下策略,使得其成为一种安全。
三、EBFP怎么使用?
1、使用C语言开发一个eBPF程序;即插桩点触发事件时要调用的eBPF沙箱程序,该程序会在内核态运行。
2、借助LLVM把eBPF程序编译成BPF字节码;eBPF程序编译成BPF字节码,用于后续在eBPF虚拟机内验证并运行。
3、通过bpf系统调用,把BPF字节码提交给内核;在用户态通过bpf系统,将BPF字节码加载到内核。
4、内核验证并运行BPF字节码,并把相应的状态保存到BPF映射中;内核验证BPF字节码安全,并且确保对应事件发生时调用正确的eBPF程序,如果有状态需要保存,则写入对应BPF映射中,比如监控数据就可以写到BPF映射中。
5、用户程序通过BPF映射查询BPF字节码的运行状态。用户态通过查询BPF映射的内容,获取字节码运行的状态,比如获取抓取到的监控数据。
参考资料:微服务监控