写点什么

课程实录 | Ingress Controller 与 Master 的通讯机制

  • 2025-01-22
    北京
  • 本文字数:6544 字

    阅读完需:约 21 分钟

课程实录 | Ingress Controller 与 Master 的通讯机制

原文作者:陶辉

原文链接:课程实录 | Ingress Controller 与 Master 的通讯机制

内容来源:NGINX 中文官网


NGINX 唯一中文官方社区 ,尽在 nginx.org.cn


编者按——本文为系列课程《K8S Ingress Controller 技术细节探讨》的第二节《Ingress Controller 与 Master 的通讯机制》的课程实录,点击观看课程回放


在本节课程中,陶辉老师介绍了 Ingress Controller 与 Master 节点的通讯机制和流程,并列举了不同应用场景下的注意事项。


课程内容包括 Ingress Controller 的通讯原理,Kubernetes 通讯协议以及 K8s 官方 Ingress Controller 的源代码架构。

课程背景简介



在上一节课中,我们详细阐述了 Ingress Controller 的工作机制。本节课将专门探讨 Ingress Controller 如何与 Master 节点进行通信。尽管这个知识点相对较小,但具有非常重要的实际意义,包含以下四点:


1. 了解 Ingress Controller 与 Master 的通信机制有助于我们更好地解决可能遇到的问题和故障。在分布式网络中,负载均衡是一项至关重要的功能。无论您是在使用 API Gateway、CDN,还是其他各种负载均衡算法,都离不开负载均衡。当 Ingress Controller 形成一个集群,需要与管理节点进行通讯,我们需要一个可视化的、能够自动调度的管理节点来通讯。


2. 在开发 Kubernetes 的插件时,也是需要与 API Server 和 K8s 管理节点进行通讯。了解这些通信细节对于成功开发此类插件非常有帮助。


3. 想要阅读 Ingress Controller 源代码,需要先掌握通讯机制。因为通讯机制占据了将近一半的代码量。


4. 通讯机制是通用的功能。比如,一个集群必须有一个管理节点进行管理。如何确保管理节点可以正常地与每个 agent 进行交互和通讯,是有难度的。试想一下,如果你来设计一个集群,并需要一个中心管理节点,你应如何制定通信协议?你可能会面临三个问题:


第一,安全。众所周知,安全一旦出问题,就是大问题。比如,四年前,MongoDB 监听某个特定地址,是一个默认在公网云上的地址,没有配置秘钥,虽然有很好的易用性,但这种情况属于在公网上裸奔,因此被很多黑客机构敲诈。当时有将近 4 万个 MongoDB 被黑客搬走了数据,留下了名为 warning 的数据库。所以我们要引起重视,避免这样的风险。


第二,多语言。K8s 是一个底层的基础设施,Java、Python、go 等各种语言的开发者,都会基于 K8s 进行开发,因此,Ingress Controller 与 Master 通讯时,要支持各种各样的语言,让开发者方便易用,同时还要考虑低成本,以及能生成文档等功能。


第三,性能。比如,选择使用哪种协议,哪种编码方式解压速度快,如何缓存在本地并维持一致,消息变更时如何尽快通知等等,都是开发者需要考虑的问题。

Ingress Controller 原理


Ingress Controller,其本质是运行在 Pod 中的负载均衡,而多个 Pod 共同构成了 NGINX 负载均衡集群。这些集群各自拥有虚拟 IP,如何将这些虚拟 IP 对外网提供服务呢?答案是采用裸机方式,不依赖亚马逊、阿里云、腾讯云等基础设施,而是通过映射端口的方式为公网提供服务。


正如上图中所示,亚马逊提供了负载均衡,并拥有自己的公网 IP,负载均衡与 API Server 之间也需要互相通讯。因为 NGINX 负载均衡,即 Ingress Controller,是动态变化的,可能会挂掉一台,但另外的 Pod 会起立,或者可能会增加更多的 Pod。


亚马逊的负载均衡与 API Server 进行通信,运行在 master 节点。从用户流量角度来看,Ingress Controller 接收外网或类似 Load Balancer 的流量,并将其往下分发。本篇的关注点是上图的左下角,讲解 API Server 如何与 Ingress Controller 进程进行通讯。

Kubernetes 通讯协议



通讯协议有很多细节,也有很多要求。我们先来看 API Server 的作用:


在 Kubernetes 的 master 上,主要部署了 3 个节点,分别是 scheduler、API Server、Controller Manager。他们与 Etcd 数据库是通过 gRPC 协议进行通讯。HTTP/PB 是 Ingress Controller 程序与 API Server 进行通讯时所采用的协议。


HTTP 对于今天的我们具有很多学习意义:


第一,HTTP 采用 Rest 架构,Rest 架构要求在 URL 中明确表示一个对象,并且 method 必须明确表示一个动作。HTTP 消息的核心在于是传输自描述的协议,包括 head、body 和响应码等。这些描述类的 Rest 架构必须符合 Rest 规范。它的定义的规范值得我们研究学习。


第二,在 body 方面,编码支持三种方式:yaml、json 和 Protobuf。Ingress Controller 和 API Server 之间的 body 默认采用的是 Protobuf,即 PB。Etcd 是数据库,Controller Manager 负责维护预定义的语言,scheduler 负责编排工作,如何维护和重启。Pod 是在 Ingress Controller 进程直接与 API Server 进行交互。


以上这些内容还是很复杂的。那么,如何做到既要符合 Rest 架构,又要支持三种 body 的消息编码方式,还要通过封装缓存来提升性能?就是通过 OpenAPI 来实现,即 Swagger,通过可视化的方式,自动生成 HTTP client 和 Server 的语言。那么,如何管理好缓存等功能呢?K8s 为我们提供了 client-go 的数据库来管理,下面为大家详细讲解。

REST API



Rest API 的 method 一定是明确的,分别是 get,post,put,delete。Get 是获取,put 要求是幂等的,post 要求通常是不幂等的。



我们继续探讨 Rest API 的要求。URL 必须是一个对象,Ingress Controller 监控五种资源,分别是 Ingress,Endpoint,Service,Secret,ConfigMap。


Ingress 决定了核心规则,通过域名 host 头部到 server-name,这是 NGINX 的配置。还有一个映射,从 URL 到 location 的映射。Ingress 与 API Server 进行通讯时,Rest put API 的 URL 应该如何编写呢?Kubernetes 定义了一个规范,它由五部分组成:apis、group、version、resource 以及 subresource。


这些是 Kubernetes 中的概念,比如 networking 和 core 是 group (资源组)。v1beta1、v1alpha 和 v1 是 version (资源版本),通常需要相互兼容。然而,v1beta1 版本优于 v1alpha 版本,因为 v1alpha 版本可能会被淘汰,即不再支持也不兼容了,v1beta1 也不是一个正式版本。


接下来是我们的资源,IngressClass 和 Ingress,用两个不同的 Ingress Controller 将它们区分开。它们需要建立对应关系,以便 Ingress Controller 监控这些规则。


一旦发现这两个东西发生了变化,就修改 nginx.conf,然后我们通过 /apis/<group>/<version>/<resource>/<subresource> 这个 URL 进行监控和通讯,是如何定义的?通过 Endpoint、service、secret 配置 TLS 秘钥等。


Service 是将许多 Pod 绑定到一个 service,并为 Ingress 提供负载均衡后端的标识。Nginx.conf 支持很多特殊的配置,特别是增加第三方模块时,可能又会增加某个配置,这时,我们会把这类未抽象到 IngressClass 中的信息放到 ConfigMap 中。Endpoint 则是每个 Pod 的 IP 地址。



在假定 HTTP 协议的 Restful API 已实现的基础上,仍有以下几点需要关注:


首先,我们应使用 TLS 协议进行通信。考虑到 API 与管理节点以及被管理集群之间的连接可能经过不安全的网络,因此不能简单地认为网络是可信任的。在这种情况下,使用 TLS 认证,这是八种认证之一,是默认使用的认证方式。



Body 支持三种协议。首先,HTTP2 是默认使用的协议。相较于 HTTP,HTTP2 具备两个显著优点。第一个优点是多路复用。在同一个 TCP 连接上,可以同时运行多个 stream,每个 stream 独立承载一个消息和 Rest API。


这种方式减少了 TCP 握手次数和慢启动现象,甚至减少了 TLS 认证的次数。第二个优点是消息推送机制,例如当 Etcd 中的数据发生变化时,可以通过 API Server 即时推送给 Ingress Controller。这种推送是毫秒级别的,确保了消息的即时性。



如果要实现 API Server,至少完成两项任务:第一项是认证,第二项是授权。我们需要验证您是否有权限访问该资源以及确定您的身份,并确定您可以执行哪些操作。如果您选择使用 ClientCA 认证,则不能使用 HTTP 协议,而应使用 HTTPs 和 TLS 协议。

Protobuf 和 Swagger 的应用


Protobuf 是 body 编码支持的格式之一。其他两种格式 yaml 和 json 其实是基于字符串的分隔符,效率相对较低。在 Ingress Controller 的 go 语言程序与 API Server 之间进行通信时,为了提高效率,通常采用 Protobuf。


Protobuf 使用通用格式,例如一个 64 位整数、字符串等。同时,它也自定义了一套语言,大家可以看作成一种伪代码。这种伪代码通过程序生成多种语言的编解码函数,包括 C++、Java、Python 等。使用 Protobuf 避免了用户编写序列化函数的繁琐过程,具有高性能的优点。



接下来,我们介绍 Protobuf 的一个具体示例。


如上图所示,这里有 3 个字段,分别是字符串(string name)、32 位的整数(unit32 id)、枚举(Sex Type sex)。这 3 个字段如果采用 json 编码,{"name":"John","id": 1234,"sex":"FEMALE"},需要将近 40 个字节,而采用  Protobuf 编码只需 11 个字节。因此可以看出,Protobuf 的优点是压缩率特别高。


给大家详细介绍下 Protobuf 是如何编码的。首选,采用 json 编码时,name、id、sex 分别是 6、4、4 个字节表示,而采用 Protobuf 编码,这三个都只需要 1 个字节表示,即使用标号的形式,分别写成 1,2,3。


然后,如何判断一个数字是数值还是字符串,在 json 编码时,通常需要采用两个引号,也就是 2 个字节来判断,而采用 Protobuf 编码时,使用二进制位,只需要 3/8 个字节。


再看下 id,首先判断 id 是可变长整数(Varint)还是定长整数(64-bit)。如果是可变长整数,采用 Protobuf 编码时,对小的数值编码比较短,相反,对大的数值编码会比较长。


最后,看下枚举,采用 Protobuf 编码,只需要通过写 0 或 1 来表示。


第二个优点是解码速度特别快。还是以上述字段为例,json 解码时,需要还原成一棵树,大概是树状的格式,解码速度相对比较慢。Protobuf 解码时,是线性的向后解码,解码速度快。



有了 Protobuf 和 Rest API 后,我们面临多种语言的需求,需要支持 Java、go、C++、Python 等。为了满足这些不同语言的需求,我们需要为每种语言生成独立的 API。虽然我们可以选择使用 Protobuf,但我们同时需要生成文档,这对于可读性是个挑战。在这种情况下,可以使用 Swagger 来解决。


Protobuf 只可以生成序列化和反序列化函数。对比之下,Swagger 还可以生成 HTTP Client 和 HTTP Server 的源代码,生成文档,同时还可以自动化完成认证、验证、Rest 参数等。


写过 Rest API 的人可能知道,如果我们需要验证例如 POST 这样的 API,验证它的表单是一个复杂的过程。有了 Swagger 后,整个过程就会变得非常方便。除了序列化,Swagger 在通讯、文档的验证等方面都会起到很大帮助。



Swagger 有三个工具,分别是 editor,UI,codegen。使用 editor 可以在可视化界面中编写 OpenAPI,使用 Swagger UI 生成文档,自动浏览每个 API 的使用文档,然后通过 Swagger.codegen 生成代码。

Client-go



在实际操作中,我们需要去解决通知性能和缓存等问题。这时我们应当封装一个 client-go,即用于从各种语言访问 API Server 的库,这个库能为我们处理许多任务。如果您自己要开发一个 K8s 的 plugin,必须对 Kubernetes 集群进行管理。


通常来说,我们无法使用 OpenAPI 裸写 RESTful 的接口。一种更可取的方法是借助预先提供的库 client-go。该库的源代码地址是 HTTPs://github.com/kubernetes/client-go。


Client-go 提供了多种访问方式,其中底层为 RESTClient,通过 HTTP 接口直接发送消息。在大多数默认情况下,我们都使用 Clientset。DynamicClient 和 DiscoveryClient 的使用都比较少。Ingress Controller 也在使用 Clientset。



当 Clientset 与 API Server 进行通讯时,需要做 ClientCA 鉴权通过 TLS 证书进行身份验证。在部署 Kubernetes 时,会生成一个根证书,Client 获取由该根证书颁发的 CA 证书后,就可以进行访问了。Client-go 通过读取 $home/.kube/config 配置文件帮我们读取这些证书和信息。



接下来介绍 client-go 的 2 个主要功能。


第一,每一种资源都有其独立的协程,这些协程都通过 reflector 进行管理。比如,现在我们收到 ingress 新加了一条,或者 Endpoint 新起了一个 Pod,这时就会 add 一个 object 到 Fifo 队列中。从队列中取出后,再放入 informer 中。Informer 的主要作用是用来进行通知。


第二,它的缓存,称为 store 或 indexer,缓存与 Etcd 中的数据完全一致。将数据缓存到这里以后,每次接收到数据修改请求时,只需修改缓存中的内容即可。这大大减少了 Ingress Controller 需要进行的大量查询,提高性能。


Client-go 通讯方式是这样的:在处理时,Ingress Controller 的代码会通过 store 接口获取所需的对象。看到该对象后,我们就可以从中提取数据。

Goroutine



在介绍源代码之前,我们先来了解一下 Goroutine。Go 语言使用协程,每个协程叫做 Goroutine。一般来说,在 go 语言中我们会写一个 go,后面接一个例如 func 的函数,这个函数就会进入协程中运行。每个线程可以运行多个协程。线程由内核进行调度,而协程是由 go 语言的调度器自行管理。这样调度的好处是,一个协程只需要 8kb 来进行基本的并发。


Ingress Controller 在处理任务时,倾向于使用协程,每个任务单独开启一个协程进行处理,每个协程只干一件事。与此不同,当我们编写线程代码时,通常会选择将多个任务集成到一个线程中执行。这是两者在处理任务方面的一个显著差异。



接下来介绍第二个差异。两个 go 协程之间是如何进行通讯的?很简单。首先,创建一个名为 message 的 channel。使用 go 语言中的 make 函数就可以生成一个 channel。然后,如何向 channel 发送消息?


比如,我们可以发送一个名为 ping 的消息,消息可以是任何结构体或对象。这里我们以发送字符串为例,使用箭头符号表示将消息发送到 channel 中。最后,我们使用另一个协程从 channel 中接收消息,获取到被发送的消息。这就是协程间进行通讯的基本流程。


在这里我们回顾下 client-go 的原理,但凡我们要与 API Server 通讯,拉微服务集群中的各种信息,一定要用 client-go。因为 client-go 可以把鉴权、认证等处理好,还有另一个好处是 informer。Informer 只要实现了 Sync 函数,一旦有新消息通知,Sync 函数就能在协程中自动执行。


在执行的过程中,如果需要拉取数据,就可以通过 key,到 indexer,即本地的内存 local store 里面拉取数据。Indexer 具体是如何实现的,这是一个 map,这里不做深入讨论。

官方 Ingress Controller 架构

现在我们来看源代码。Ingress Controller 源代码的架构如上图所示。第一个协程是 store,store 封装了 informer。任何的 API Server 都可以通过 control manager 向其发送消息。当检测到某个 Pod 挂掉时,store 将首先通过 API Server 接收到相关消息。


收到消息后,store 将通过一个名为 updateChannel 的环形通道(ring channel)与一个名为 NginxController 的协程进行通信。Ring channel 是一个环状通道,其默认分配为 1024,可以存放 1024 个消息。


当发现是 store 协程发送的 informer 消息时,NginxController 将通过 SyncQueue 的先入先出队列 (Fifo) 进行处理。为什么要有队列呢?因为如果有些消息延迟太久,有些不重要的消息可能会丢,也就是不处理了。如果出现重大的网络问题,可能会有大量东西下线。SyncQueue 最大的用处是可以丢掉不重要的消息,及时处理重要的消息。


SyncQueue 还有一个同名的协程,是实际处理逻辑的,即如何生成 nginx.config。生成 nginx.config 时,我们要去执行 reload。然后,在 K8s 社区中,通过 Lua 监听特定端口更改共享内存,修改 Endpoint upstream 后面的信息。下面我们将简单介绍它的流程。



当 Ingress Controller 启动时,它将监听 10254 的 HTTP 端口,主要是为了与 kubelet 通讯。每一个 Node 上面会运行一个 kubelet,负责管理网络、存储以及容器。


Kube API Server,使用 client-go 主要是为了 store 封装的,以保证 indexer 和 informer,然后通过 Sync 函数,一种可能是根据模版生成 NGINX.config,另一种可能是通过 Lua 语言 10246 端口更新共享内存。



NGINX 唯一中文官方社区 ,尽在 nginx.org.cn

更多 NGINX 相关的技术干货、互动问答、系列课程、活动资源: 开源社区官网 | 微信公众号

发布于: 刚刚阅读数: 2
用户头像

NGINX 唯一中文官方社区 2022-07-04 加入

NGINX 是全世界最流行的 Web 服务器,也可用于反向代理、负载均衡、API 网关等场景的开源软件,为全世界最繁忙的网站和应用提供支持。 微信:#NGINX开源社区

评论

发布
暂无评论
课程实录 | Ingress Controller 与 Master 的通讯机制_Kubernetes_NGINX开源社区_InfoQ写作社区