更多奇技淫巧欢迎订阅博客:https://fuckcloudnative.io

前言

排查问题时研究了一下 Cilium health probe 相关的代码,本文略作整理,仅供参考。代码基于 1.8.4

流程图:

调用栈:

runDaemon                                       // daemon/cmd/daemon_main.go|-initHealth                                  // daemon/cmd/health.go|  |-health.Launch                             // cilium-health/launch/launcher.go|  |  |-ch.server = server.NewServer()|  |  |             |-newServer|  |  |                |-api := restapi.NewCiliumHealthAPI|  |  |                |-srv := healthApi.NewServer(api)|  |  |-ch.client = client.NewDefaultClient()|  |  |-go runServer()                         // cilium-health/launch/launcher.go|  |      |-go ch.server.Serve()|  |      |     |-for { go tcpServer.Serve() }|  |      |     |-go runActiveServices()|  |      |           |-s.FetchStatusResponse|  |      |           |   |-getAllNodes|  |      |           |   | |-s.Daemon.GetClusterNodes|  |      |           |   |    |-"GET /cluster/nodes" to cilium-agent API|  |      |           |   |-prober.Run|  |      |           |   |   |-p.Pinger.Run|  |      |           |   |   |-p.runHTTPProbe|  |      |           |   |        |-for node in nodes:|  |      |           |   |            for ip in [nodeIP, healthIP]:|  |      |           |   |              for port in ports:|  |      |           |   |                httpProbe(node, ip, port) // port=4240|  |      |           |   |-updateCluster(prober.Results)|  |      |           |-s.getNodes|  |      |           |-prober.OnIdle = func() {|  |      |           |   updateCluster(prober.Results)|  |      |           |   nodesAdded, nodesRemoved := getNodes()|  |      |           |     |-s.Daemon.GetClusterNodes|  |      |           |        |-"GET /cluster/nodes" to cilium-agent API|  |      |           |   prober.setNodes(nodesAdded, nodesRemoved)|  |      |           | }|  |      |           |-prober.RunLoop|  |      |           |-s.Server.Serve()  // api/v1/health/server/server.go|  |      |              |-s.Listen()     // listen at unix://xxx/health.sock|  |      |              |-if unix sock:|  |      |                  |-domainSocket.Serve(l)|  |      |-for {|  |          ch.client.Restapi.GetHealthz()|  |          ch.setStatus(status)|  |        }|  ||  |-pidfile.Remove("/var/run/cilium/state/health-endpoint.pid")|  |-UpdateController("cilium-health-ep", func() {|      DoFunc: func() {|          LaunchAsEndpoint|          RunInterval: 60 * time.Second,|      }|    })||-startAgentHealthHTTPService                 // daemon/cmd/agenthealth.go|- mux.Handle("/healthz")

1. 设计

Full-mesh 健康探测

在 Cilium 的设计中,每个 node 都可以主动探测(probe)其他 node 的健康状态, 这样它们就能拿到第一手的全局健康状态信息(global health status of all nodes)。

默认情况下,任何两个 node 之间都会互相 probe,因此最终形成一张 full-mesh probe 网络,如下图所示:

Full-mesh health probe among Cilium nodes

Probe 行为由 cilium-agent 的两个开关控制,默认都是开的,

  1. enable-health-checking:probe 其他 node的健康状态。

  2. enable-endpoint-health-checking:probe 其他 node 上的 cilium-health-ep 的健康状态。稍后会介绍 cilium-health-ep 是什么。

四种 probe 类型

从网络层级的角度,probe 分两个维度:

  1. 三层(L3)探测:ping(ICMP)

  2. 七层(L7)探测:GET API。

再结合以上两个开关,总共就有四种 probe

  1. enable-health-checking=true

    1. ICMP probe (L3):ping <NodeIP>

    2. HTTP probe (L7):GET http://<NodeIP>:4240/hello

  2. enable-endpoint-health-checking=true

    1. ICMP probe (L3):ping <HealthIP>

    2. HTTP probe (L7):GET http://<HealthIP>:4240/hello

Probe results

Probe 结果会缓存到 cilium-agent 中,可以通过下面命令查看(# 开头的注释是后加的):

(node1) $ cilium-health status
Probe time:   2020-12-29T15:17:02Z
Nodes:cluster1/node1 (localhost):Host connectivity to 10.5.6.60:        # <-- NodeIPICMP to stack:   OK, RTT=9.557967msHTTP to agent:   OK, RTT=405.072µsEndpoint connectivity to 10.6.2.213:   # <-- HealthIPICMP to stack:   OK, RTT=9.951333msHTTP to agent:   OK, RTT=468.645µscluster1/node2:...cluster2/node100:Host connectivity to 10.6.6.100:        # <-- NodeIPICMP to stack:   OK, RTT=10.164048msHTTP to agent:   OK, RTT=694.196µsEndpoint connectivity to 10.22.1.3:     # <-- HealthIPICMP to stack:   OK, RTT=11.282117msHTTP to agent:   OK, RTT=765.092µs

如果启用了 clustermesh[1],那 cilium-agent 也会对其他集群的 node 进行探测,所以我们看到上面的输出中有其他集群的 node 信息。

cilium-health-ep: cilium-health endpoint

简单来说,cilium-agent 会为每个 Pod 创建一个它所谓的 Endpoint 对象。而在这里, cilium-health-ep 是个特殊的 endpoint

(node) $ cilium endpoint list
ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value])  IPv4         STATUS
...
2399       Disabled           Disabled          4          reserved:health              10.6.2.213   ready
...

它并不是一个 Pod,但可以看到,它有自己独立的

  1. Endpoint ID:随机分配,每台节点内唯一。

  2. Identity:reserved identity,固定值 4,也就是说每台节点上的 cilium-health identity 都是 4

  3. IP address:cilium-agent 随机分配。

  4. Veth pair:lxc_health@<peer>

也可以用下面的方式查看 cilium-health 使用的 IP 地址:

$ cilium status --all-addresses | grep health10.6.2.213 (health)

小结

由以上内容可知,Cilium health probe 的整体设计还是非常简单直接的,并没有很高深的东西。

但到了实现层面,就要复杂很多了。

2. 实现

完整的流程图和调用关系见本文开篇。接下来分步介绍这个过程。

初始化流程

cilium-agent(daemon)初始化代码开始。

大致步骤:

  1. 调用 initHealth() 完成 prober 的初始化工作,大部分工作都在这里面完成

    1. 初始化 prober,顺序对其他所有 node 执行 probe

    2. 创建 cilium-health-ep,这一步不依赖上面 probe 的结果,二者是独立进行的。

  2. 注册 cilium-agent /healthz API 并开始提供服务。

  • 这个 API 用于检测 cilium-agent 是否正常

  • cilium status --brief 返回的就是这个 API 的结果

但这里要注意,以上两个步骤是异步进行的,initHealth() 中会创建很多 goroutine 异步执行。也就是说,cilium-agent 的 /healthz 很快会进入 ready 状态,而并不会等待 initHealth() 对所有 node 执行完 health probe,因为后者可能需要几秒钟、几分钟,甚至几个小时。

大部分工作都在 initHealth() 中完成,接下来看这里的实现。

initHealth() -> Launch() -> runServer() -> server.Serve()

initHealth() 做的事情:

  1. 调用 health.Launch(),后者

    1. 初始化 ch.server

    2. 初始化 ch.client

    3. go ch.runServer()接下来的大部分逻辑,都在这里面

  2. 清理之前的 cilium-health pid 文件(/var/run/cilium/state/health-endpoint.pid

  3. 创建一个名为 cilium-health-ep 的 controller(定时任务),这里面会

    1. 创建 cilium-health endpoint,

    2. 定期将该 cilium-health endpoint 状态同步到 K8s。

接下来看 go ch.runServer()

runServer()

代码见 cilium-health/launch/launcher.go

逻辑:

  1. 等待 cilium-agent 启动成功GET /healthz 返回成功),然后转步骤 2

  2. 删除之前的 /var/run/cilium/health.sock 文件。本地执行 cilium-health 命令时会用到这个 socket 文件

  3. go ch.server.Serve()创建一个 goroutine,在里面启动 cilium-health API server,主逻辑在这里面,包括:

    1. 创建 TCP servers

    2. 运行 runActiveServices(),这里面会创建 prober 和 unix servers,其中 unix server 在 Listen() 时会创建新的 health.sock 文件

  4. 等待,直到新的 health.sock 文件 ready,然后给其设置合适的文件权限

  5. statusProbeInterval 的间隔,定时向 cilium-agent 发起 GET /healthz,并将结果保存

ch.server.Serve() 实现:

// pkg/health/server/server.go// Serve spins up the following goroutines:
// * TCP API Server: Responders to the health API "/hello" message, one per path
// * Prober: Periodically run pings across the cluster, update server's connectivity status cache.
// * Unix API Server: Handle all health API requests over a unix socket.
func (s *Server) Serve() (err error) {for i := range s.tcpServers {srv := s.tcpServers[i]go func() {errors <- srv.Serve()}()}go func() {errors <- s.runActiveServices()}()err = <-errors // Block for the first error, then return.return err
}

这里面最重要的是 runActiveServices()

runActiveServices()

主要步骤:

  1. 执行 FetchStatusResponse(),这会用一个三层 for 循环对所有 node 顺序进行 probe;

  2. 设置 prober OnIdle() handler,然后启动 prober.RunLoop(),定期更新 node 集 合;

  3. 执行 s.Server.Seve(),开始接收 Unix、HTTP、HTTPS 请求。

注意其中的第一步,

  1. 对所有 node 的 probe 操作是顺序进行的。

  2. 每次 probe 如果不通,需要过 30s 超时退出。

因此,如果有大量 node 不通,这里就会花费大量时间,导致后面的 UNIX server 迟 迟无法启动,具体表现就是宿主机执行 cilium-health 命令报以下错误:

$ cilium-health status
Error: Cannot get status: Get "http://%2Fvar%2Frun%2Fcilium%2Fhealth.sock/v1beta/status": dial unix /var/run/cilium/health.sock: connect: no such file or directory

因为这个文件是在第三步 s.Server.Serve() -> Listen() 里面才创建的。

代码

// pkg/health/server/server.go// Run services that are actively probing other hosts and endpoints over ICMP and HTTP,
// and hosting the health admin API on a local Unix socket.
func (s *Server) runActiveServices() error {s.FetchStatusResponse()nodesAdded, _, _ := s.getNodes()prober := newProber(s, nodesAdded)prober.OnIdle = func() {// Fetch results and update set of nodes to probe every ProbeIntervals.updateCluster(prober.getResults())if nodesAdded, nodesRemoved, err := s.getNodes(); err != nil {log.WithError(err).Error("unable to get cluster nodes")} else {prober.setNodes(nodesAdded, nodesRemoved)}}prober.RunLoop()return s.Server.Serve()
}

最后一行 s.Server.Serve() 调用到下面这里:

// api/v1/server/server.go// Serve the api
func (s *Server) Serve() (err error) {if !s.hasListenerss.Listen() // net.Listen(s.SocketPath) -> create sock fileif s.handler == nil // set default handler, if none is sets.SetHandler(s.api.Serve(nil))if s.hasScheme(schemeUnix) { // "Serving cilium at unix://%s", s.SocketPathgo func(l net.Listener) {domainSocket.Serve(l)}(s.domainSocketL)}if s.hasScheme(schemeHTTP) {...}if s.hasScheme(schemeHTTPS) {...}return nil
}

创建 cilium-health-ep

如上图所示,创建 cilium-health-ep 过程和 2.2~2.4 是异步的(从 go runServer() 开始就是异步了),因此不用等待后者的完成。

cilium-health-ep 也是一个 Endpoint,因此会经历:

  1. 分配 IP

  2. 创建 netns

  3. 创建 veth pair(lxc_health

  4. 创建 Endpoint

  5. 分配 Identity:注意 Cilium 里面都是先创建 Endpoint,再为 Endpoint 分配 Identity

  6. Regenerate BPF

等等过程,代码见 cilium-health/launch/endpoint.go。

3. CLI cheat sheet

一些相关的命令。

Check cilium agent status

  • cilium status --brief: brief output, which internally performs "GET /healthz"

  • cilium status: normal output

  • cilium status --verbose: verbose output

Check connectivity results

$ cilium-health status
...$ cilium status --verbose
...
Cluster health:                          265/268 reachable   (2020-12-31T09:00:03Z)Name                         IP                Node          Endpointscluster1/node1 (localhost)   10.5.6.60         reachable     reachablecluster2/node2               10.6.9.132        reachable     reachable...

Check health info in CT/NAT tables

ICMP records in Conntrack (CT) table and NAT table:

$ cilium bpf ct list global | grep ICMP |head -n4
ICMP IN 10.4.9.12:0 -> 10.5.8.4:518 expires=1987899 RxPackets=1 RxBytes=50 RxFlagsSeen=0x00 LastRxReport=1987839 TxPackets=1 TxBytes=50 TxFlagsSeen=0x00 LastTxReport=1987839 Flags=0x0000 [ ] RevNAT=0 SourceSecurityID=2 IfIndex=0
ICMP OUT 10.5.2.101:47153 -> 10.4.9.11:0 expires=1987951 RxPackets=0 RxBytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxPackets=1 TxBytes=50 TxFlagsSeen=0x00 LastTxReport=1987891 Flags=0x0000 [ ] RevNAT=0 SourceSecurityID=0 IfIndex=0$ cilium bpf nat list | grep ICMP |head -n4
ICMP OUT 10.5.2.101:65204 -> 10.4.6.9:0 XLATE_SRC 10.5.2.101:65204 Created=1987884sec HostLocal=1
ICMP IN 10.4.4.11:0 -> 10.5.2.101:39843 XLATE_DST 10.5.2.101:39843 Created=1987884sec HostLocal=1

参考资料

[1]

启用了 clustermesh: http://arthurchiao.art/blog/cilium-clustermesh/

你可能还喜欢

点击下方图片即可阅读

容器网络一直在颤抖,罪魁祸首竟然是 ipvs 定时器

云原生是一种信仰 ????

码关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

点击 "阅读原文" 获取更好的阅读体验!

❤️给个「在看」,是对我最大的支持❤️

Cilium 源码解析:Node 之间的健康探测(health probe)机制相关推荐

  1. Spring源码解析:自定义标签的解析过程

    2019独角兽企业重金招聘Python工程师标准>>> spring version : 4.3.x Spring 中的标签分为默认标签和自定义标签两类,上一篇我们探究了默认标签的解 ...

  2. JAVA并发容器-ConcurrentHashMap 1.7和1.8 源码解析

    HashMap是一个线程不安全的类,在并发情况下会产生很多问题,详情可以参考HashMap 源码解析:HashTable是线程安全的类,但是它使用的是synchronized来保证线程安全,线程竞争激 ...

  3. ConcurrentHashMap 1.7和1.8 源码解析

    HashMap是一个线程不安全的类,在并发情况下会产生很多问题,详情可以参考HashMap 源码解析:HashTable是线程安全的类,但是它使用的是synchronized来保证线程安全,线程竞争激 ...

  4. (Nacos源码解析五)Nacos服务事件变动源码解析

    Nacos源码解析系列目录 Nacos 源码编译运行 (Nacos源码解析一)Nacos 注册实例源码解析 (Nacos源码解析二)Nacos 服务发现源码解析 (Nacos源码解析三)Nacos 心 ...

  5. Aspects源码解析

    Aspects使用方法 Aspects源码只有两个文件:Aspects.h和Aspects.m文件.使用的方式就是对NSObject添加了一个Category,其中有两个方法分别为类和对象添加切面bl ...

  6. 【dubbo源码解析】--- dubbo中Invoker嵌套调用底层原理

    本文对应源码地址:https://github.com/nieandsun/dubbo-study 文章目录 1 dubbo中Invoker的重要性 2 dubbo RPC链条中代理对象的底层逻辑 2 ...

  7. Cilium创建pod network源码解析

    01 Overview 我们生产K8s使用容器网络插件 Cilium 来创建 Pod network,下发 eBPF 程序实现 service 负载均衡来替换 kube-proxy,并且使用 BGP ...

  8. btcd源码解析——peer节点之间的区块数据同步 (3) —— 非headersFirstMode模式

    文章目录 1. 写在前面 2. 非headersFirstMode模式下的数据同步过程 2.1 peer A 发送"获取区块哈希"的请求 2.2 peer B 响应"获取 ...

  9. JUC.Condition学习笔记[附详细源码解析]

    JUC.Condition学习笔记[附详细源码解析] 目录 Condition的概念 大体实现流程 I.初始化状态 II.await()操作 III.signal()操作 3个主要方法 Conditi ...

最新文章

  1. 基于人工智能方法的手写数字图像识别_【工程分析】基于ResNet的手写数字识别...
  2. 汇编语言--转移指令
  3. Linux常用的基本命令head、tail、tar、grep、date、cal(二)
  4. 网站如何从http升级成https
  5. 我犯的错误--关于数据库类型不对
  6. SAP C4C计价(Pricing)中折扣(Discount)的使用
  7. linux中设备文件的主要内容包括什么,LINUX期末考试复习题.doc
  8. [USACO4.1]麦香牛块Beef McNuggets By cellur925
  9. Python采集3000条北京二手房数据,看我都分析出了啥?
  10. java基础知识简化
  11. python输入一个整数、输出该整数的所有素数因子_【401】Python 求合数的所有质数因子...
  12. SqlServer修改密码后登陆不上
  13. oracle11g服务项及其启动顺序
  14. 小众绿软|媒体:myPlayer 2.1
  15. 小白量化彩票实战(5)彩票号码快速生成组合及利用数据库生成彩票号码组合
  16. ACM学习历程—HDU 5025 Saving Tang Monk(广州赛区网赛)(bfs)
  17. 【路径规划】A*三维全局路径规划(附Python实现源码)
  18. 档案重要吗有什么作用(转载记录避免以后麻烦)
  19. 「我们只投这两种AI公司」, 三位局内人首次公开AI投资的技术与产业标准
  20. 全概率公式与贝叶斯公式-机器学习

热门文章

  1. 深入解析UUID及其应用
  2. Zebra斑马LI3678一维超耐用型无线扫描器
  3. Git创建远程分支并提交代码到远程分支
  4. BZOJ4770 图样
  5. TTS行业调研20221201
  6. java实现beanstalkd,springboot2.x监听beanstalk实现异步任务
  7. 10-QNX与Android双系统通讯之FDBUS(1)
  8. for循环、find、findIndex
  9. DNS收集分析DMitry
  10. 问题分析报告--在压力场景下OS在某种硬件环境下的性能可能会下降90%的问题