关于我们

更多关于云原生的案例和知识,可关注同名【腾讯云原生】公众号~

福利:

①公众号后台回复【手册】,可获得《腾讯云原生路线图手册》&《腾讯云原生最佳实践》~

②公众号后台回复【系列】,可获得《15个系列100+篇超实用云原生原创干货合集》,包含Kubernetes 降本增效、K8s 性能优化实践、最佳实践等系列。

③公众号后台回复【白皮书】,可获得《腾讯云容器安全白皮书》&《降本之源-云原生成本管理白皮书v1.0》

④公众号后台回复【光速入门】,可获得腾讯云专家5万字精华教程,光速入门Prometheus和Grafana。

作者

王成,腾讯云研发工程师,Kubernetes member,从事数据库产品容器化、资源管控等工作,关注 Kubernetes、Go、云原生领域。

目录

1.概述

2.从 HTTP 说起

2.1 Content-Length

2.2 Chunked Transfer Encoding

2.3 HTTP/2

3.APIServer 启动

4.ETCD 资源封装

5.客户端 Watch 实现

6.服务端 Watch 实现

7.小结

概述

进入 K8s 的世界,会发现几乎所有对象都被抽象为了资源(Resource),包括 K8s Core Resources(Pod, Service, Namespace 等)、CRD、APIService 扩展的资源类型。同时 K8s 底层将这些资源统一抽象为了 RESTful 的存储(Storage),一方面服务端按目录形式(/registry/xxx) 存放在 ETCD 中,另一方面也为客户端提供了 RESTful API 接口,便于对资源的操作(get/post/put/patch/delete 等)。

K8s Watch API 就是为资源提供的一种持续监听其变化的机制,当资源有任何变化的时候,都可以实时、顺序、可靠的传递给客户端,使得用户可以针对目标资源进行灵活应用与操作。

那 K8s Watch 机制是怎么实现的呢?底层具体依赖了哪些技术?

本文将从 HTTP 协议、APIServer 启动、ETCD Watch 封装、服务端 Watch 实现、客户端 Watch 实现等方面,对 K8s Watch 实现机制进行了解析。

流程概览如下:

本文及后续相关文章都基于 K8s v1.23

从 HTTP 说起

Content-Length

如下图所示,HTTP 发送请求 Request 或服务端 Response,会在 HTTP header 中携带 Content-Length,以表明此次传输的总数据长度。如果 Content-Length 长度与实际传输长度不一致,则会发生异常(大于实际值会超时, 小于实际值会截断并可能导致后续的数据解析混乱)。

curl baidu.com -v

> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: baidu.com
> Accept: */* < HTTP/1.1 200 OK
< Date: Thu, 17 Mar 2022 04:15:25 GMT
< Server: Apache
< Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
< ETag: "51-47cf7e6ee8400"
< Accept-Ranges: bytes
< Content-Length: 81
< Cache-Control: max-age=86400
< Expires: Fri, 18 Mar 2022 04:15:25 GMT
< Connection: Keep-Alive
< Content-Type: text/html <html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>

如果服务端提前不知道要传输数据的总长度,怎么办?

Chunked Transfer Encoding

HTTP 从 1.1 开始增加了分块传输编码(Chunked Transfer Encoding),将数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。数据块长度以十六进制的形式表示,后面紧跟着 \r\n,之后是分块数据本身,后面也是 \r\n,终止块则是一个长度为 0 的分块。

> GET /test HTTP/1.1
> Host: baidu.com
> Accept-Encoding: gzip < HTTP/1.1 200 OK
< Server: Apache
< Date: Sun, 03 May 2015 17:25:23 GMT
< Content-Type: text/html
< Transfer-Encoding: chunked
< Connection: keep-alive
< Content-Encoding: gzip 4\r\n (bytes to send)
Wiki\r\n (data)
6\r\n (bytes to send)
pedia \r\n (data)
E\r\n (bytes to send)
in \r\n
\r\n
chunks.\r\n (data)
0\r\n (final byte - 0)
\r\n (end message)

为了实现以流(Streaming)的方式 Watch 服务端资源变更,HTTP1.1 Server 端会在 Header 里告诉 Client 要变更 Transfer-Encoding 为 chunked,之后进行分块传输,直到 Server 端发送了大小为 0 的数据。

HTTP/2

HTTP/2 并没有使用 Chunked Transfer Encoding 进行流式传输,而是引入了以 Frame(帧) 为单位来进行传输,其数据完全改变了原来的编解码方式,整个方式类似很多 RPC协议。Frame 由二进制编码,帧头固定位置的字节描述 Body 长度,就可以读取 Body 体,直到 Flags 遇到 END_STREAM。这种方式天然支持服务端在 Stream 上发送数据,不需要通知客户端做什么改变。

+-----------------------------------------------+
| Body Length (24) | ----Frame Header
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------+
|R| Stream Identifier (31) |
+=+=================================================+
| Frame Payload (0...) ... ----Frame Data
+---------------------------------------------------+

K8s 为了充分利用 HTTP/2 在 Server-Push、Multiplexing 上的高性能 Stream 特性,在实现 RESTful Watch 时,提供了 HTTP1.1/HTTP2 的协议协商(ALPN, Application-Layer Protocol Negotiation) 机制,在服务端优先选中 HTTP2,协商过程如下:

curl  https://{kube-apiserver}/api/v1/watch/namespaces/default/pods/mysql-0 -v

* ALPN, offering h2
* ALPN, offering http/1.1
* SSL verify...
* ALPN, server accepted to use h2
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7f2b921a6a90)
> GET /api/v1/watch/namespaces/default/pods/mysql-0 HTTP/2
> Host: 9.165.12.1
> user-agent: curl/7.79.1
> accept: */*
> authorization: Bearer xxx
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)! < HTTP/2 200
< cache-control: no-cache, private
< content-type: application/json
< date: Thu, 17 Mar 2022 04:46:36 GMT {"type":"ADDED","object":{"kind":"Pod","apiVersion":"v1","metadata":xxx}}

APIServer 启动

APIServer 启动采用 Cobra 命令行,解析相关 flags 参数,经过 Complete(填充默认值)->Validate(校验) 逻辑后,通过 Run 启动服务。启动入口如下:

// kubernetes/cmd/kube-apiserver/app/server.go
// NewAPIServerCommand creates a *cobra.Command object with default parameters
func NewAPIServerCommand() *cobra.Command {
s := options.NewServerRunOptions()
cmd := &amp;cobra.Command{
Use: "kube-apiserver",
...
RunE: func(cmd *cobra.Command, args []string) error {
...
// set default options
completedOptions, err := Complete(s)
if err != nil {
return err
} // validate options
if errs := completedOptions.Validate(); len(errs) != 0 {
return utilerrors.NewAggregate(errs)
} return Run(completedOptions, genericapiserver.SetupSignalHandler())
},
}
... return cmd
}

在 Run 函数中,按序分别初始化 APIServer 链(APIExtensionsServer、KubeAPIServer、AggregatorServer),分别服务于 CRD(用户自定义资源)、K8s API(内置资源)、API Service(API 扩展资源) 对应的资源请求。相关代码如下:

// kubernetes/cmd/kube-apiserver/app/server.go
// 创建 APIServer 链(APIExtensionsServer、KubeAPIServer、AggregatorServer),分别服务 CRD、K8s API、API Service
func CreateServerChain(completedOptions completedServerRunOptions, stopCh <-chan struct{}) (*aggregatorapiserver.APIAggregator, error) {
// 创建 APIServer 通用配置
kubeAPIServerConfig, serviceResolver, pluginInitializer, err := CreateKubeAPIServerConfig(completedOptions)
if err != nil {
return nil, err
}
... // 第一:创建 APIExtensionsServer
apiExtensionsServer, err := createAPIExtensionsServer(apiExtensionsConfig, genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
if err != nil {
return nil, err
} // 第二:创建 KubeAPIServer
kubeAPIServer, err := CreateKubeAPIServer(kubeAPIServerConfig, apiExtensionsServer.GenericAPIServer)
if err != nil {
return nil, err
}
... // 第三:创建 AggregatorServer
aggregatorServer, err := createAggregatorServer(aggregatorConfig, kubeAPIServer.GenericAPIServer, apiExtensionsServer.Informers)
if err != nil {
// we don't need special handling for innerStopCh because the aggregator server doesn't create any go routines
return nil, err
} return aggregatorServer, nil
}

之后,经过非阻塞(NonBlockingRun) 方式启动 SecureServingInfo.Serve,并配置 HTTP2(默认开启) 相关传输选项,最后启动 Serve 监听客户端请求。

K8s APIServer 为了安全考虑,只支持客户端 HTTPS 请求,不支持 HTTP。

ETCD 资源封装

ETCD 实现 Watch 机制,经历了从 ETCD2 到 ETCD3 实现方式的转变。ETCD2 通过长轮询 Long-Polling 的方式监听资源事件的变更;ETCD3 则通过基于 HTTP2 的 gRPC 实现 Watch stream,性能得到了很大的提升。

Polling(轮询):由于 http1.x 没有服务端 push 的机制,为了 Watch 服务端的数据变化,最简单的办法当然是客户端去 pull:客户端每隔定长时间去服务端拉数据同步,无论服务端有没有数据变化。但是必然存在通知不及时和大量无效的轮询的问题。

Long-Polling(长轮询):就是在这个 Polling 的基础上的优化,当客户端发起 Long-Polling 时,如果服务端没有相关数据,会 hold 住请求,直到服务端有数据要发或者超时才会返回。

在上一步配置 APIServerConfig 时,封装了底层存储用的 ETCD。以 kubeAPIServerConfig 为例,说明 K8s 内置资源是如何封装 ETCD 底层存储的。

首先,通过 buildGenericConfig 实例化 RESTOptionsGetter,用于封装 RESTStorage。之后通过 InstallLegacyAPI -> NewLegacyRESTStorage 实例化 K8s 内置资源的 RESTStorage,包括 podStorage、nsStorage、pvStorage、serviceStorage 等,用于 APIServer 在处理客户端资源请求时,调用的后端资源存储。

InstallLegacyAPI 源码如下:

// kubernetes/pkg/controlplane/instance.go
// 注册 K8s 的内置资源,并封装到对应的 RESTStorage(如 podStorage/pvStorage)
func (m *Instance) InstallLegacyAPI(c *completedConfig, restOptionsGetter generic.RESTOptionsGetter) error {
...
legacyRESTStorage, apiGroupInfo, err := legacyRESTStorageProvider.NewLegacyRESTStorage(c.ExtraConfig.APIResourceConfigSource, restOptionsGetter)
if err != nil {
return fmt.Errorf("error building core storage: %v", err)
}
if len(apiGroupInfo.VersionedResourcesStorageMap) == 0 { // if all core storage is disabled, return.
return nil
} controllerName := "bootstrap-controller"
coreClient := corev1client.NewForConfigOrDie(c.GenericConfig.LoopbackClientConfig)
bootstrapController, err := c.NewBootstrapController(legacyRESTStorage, coreClient, coreClient, coreClient, coreClient.RESTClient())
if err != nil {
return fmt.Errorf("error creating bootstrap controller: %v", err)
}
m.GenericAPIServer.AddPostStartHookOrDie(controllerName, bootstrapController.PostStartHook)
m.GenericAPIServer.AddPreShutdownHookOrDie(controllerName, bootstrapController.PreShutdownHook) ...
return nil
}

在实例化 ETCD 底层存储中,通过开关 EnableWatchCache 来控制是否启用 Watch 缓存。如果启用了,则会先走 StorageWithCacher 逻辑,然后才走 UndecoratedStorage 真正调用底层 ETCD3 存储。

K8s 当前只支持 ETCD3,不再支持 ETCD2。K8s 充分信任 ETCD3 的 Watch 机制,保证资源状态与 ETCD 底层存储的一致性。

整个调用过程如下:

K8s 各类资源(CRD/Core/Aggregator) 都统一以 RESTful 风格暴露 HTTP 请求接口,并支持多种类型的编解码格式,如 json/yaml/protobuf。

客户端 Watch 实现

经过上面的步骤,APIServer 服务端已准备好 K8s 各类资源的 RESTStorage(底层封装了 ETCD3),此时客户端可通过 RESTful HTTP 接口向 APIServer 发出资源请求,包括 GET/POST/PATCH/WATCH/DELETE 等操作。

客户端 Watch 包括:

(1). kubectl get xxx -w,获取某类资源、并持续监听资源变化;

(2). client-go 中 Reflector ListAndWatch APIServer 各类资源;

我们以 kubectl get pod -w 为例,说明客户端是如何实现资源的 Watch 操作。

首先,kubectl 也是通过 Cobra 命令行解析参数(--watch,或 --watch-only),然后调用 Run 调用 cli-runtime 包下面的 Watch 接口,之后通过 RESTClient.Watch 向 APIServer 发起 Watch 请求,获得一个流式 watch.Interface,然后不断从其中 ResultChan 获取 watch.Event。之后,根据客户端发送的编解码类型(json/yaml/protobuf),从 stream 中按帧(Frame) 读取并解码(Decode) 数据,输出显示到命令行终端。

客户端通过 RESTClient 发起 Watch 请求,代码如下:

// kubernetes/staging/src/k8s.io/cli-runtime/pkg/resource/helper.go
func (m *Helper) Watch(namespace, apiVersion string, options *metav1.ListOptions) (watch.Interface, error) {
options.Watch = true
return m.RESTClient.Get().
NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource).
VersionedParams(options, metav1.ParameterCodec).
Watch(context.TODO())
}

客户端 Watch 实现过程小结如下:

服务端 Watch 实现

服务端 APIServer 启动后,一直在持续监听着各类资源的变更事件。在接收到某类资源的 Watch 请求后,调用 RESTStorage 的 Watch 接口,通过开关 EnableWatchCache 来控制是否启用 Watch 缓存,最终通过 etcd3.Watch 封装实现了 ETCD 底层的 Event 变更事件。

RESTStorage 就是在 APIServer 启动时候,提前注册、封装的 ETCD 资源存储。

etcd3.watcher 通过两个 channel(incomingEventChan、resultChan,默认容量都为 100) 实现 ETCD 底层事件到 watch.Event 的转换,然后通过 serveWatch 流式监听返回的 watch.Interface,不断从 resultChan 中取出变更事件。之后,根据客户端发送的编解码类型(json/yaml/protobuf),编码(Encode) 数据,按帧(Frame) 组装后发送到 stream 中给客户端。

服务端通过 serveWatch 流式监听返回的 watch.Interface,代码如下:

// kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go
func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
... if opts.Watch || forceWatch {
...
watcher, err := rw.Watch(ctx, &amp;opts)
if err != nil {
scope.err(err, w, req)
return
}
requestInfo, _ := request.RequestInfoFrom(ctx)
metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() {
serveWatch(watcher, scope, outputMediaType, req, w, timeout)
})
return
}
...
}
}

K8s 在 v1.11 之后将 WATCH/WATCHLIST 类型的 action.Verb 废弃了,统一都交由 LIST -> restfulListResource 处理。

服务端 Watch 实现过程小结如下:

APIServer 除了支持 HTTP2,也支持 WebSocket 通信。当客户端请求包含了 Upgrade: websocket,Connection: Upgrade 时,则服务端会通过 WebSocket 与客户端进行数据传输。

值得注意的是,底层 ETCD 事件通过 transform 函数转换为 watch.Event,包括以下几种类型(Type):

小结

本文通过分析 K8s 中 APIServer 启动、ETCD watch 封装、服务端 Watch 实现、客户端 Watch 实现等核心流程,对 K8s Watch 实现机制进行了解析。通过源码、图文方式说明了相关流程逻辑,以期更好的理解 K8s Watch 实现细节。

K8s 底层完全信任 ETCD(ListAndWatch),将各类资源统一抽象为了 RESTful 的存储(Storage),通过 Watch 机制获取各类资源的变更事件,然后通过 Informer 机制分发给下游监听的 ResourceEventHandler,最终由 Controller 实现资源的业务逻辑处理。随着 ETCD3 在 HTTP/2 基础上不断优化完善,K8s 将提供更高效、更稳定的编排能力。

参考资料

  1. HTTP/2 Wikipedia
  2. Chunked Transfer Encoding
  3. Kubernetes 源码
  4. ETCD watch-api
  5. K8s API Concepts
  6. Server Push 与 Client Poll

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!

K8s 如何提供更高效稳定的编排能力?K8s Watch 实现机制浅析的更多相关文章

  1. 3D建模服务提供更高效、专业的能力,“筑”力开发者

    3D建模服务(3D Modeling Kit)是HMS Core在图形图像领域又一技术开放.3D建模产品的定位就是要做快速.简洁.低成本的3D制作能力,并陆续开放给有3D模型.动画游戏制作等能力诉求的 ...

  2. 3D建模服务提供更高效、专业的3D制作能力,“筑”力开发者

    3D建模服务(3D Modeling Kit)是HMS Core在图形图像领域又一技术开放.3D建模产品的定位就是要做快速.简洁.低成本的3D制作能力,并陆续开放给有3D模型.动画游戏制作等能力诉求的 ...

  3. 微服务平台(Micro Service Platform : MSP)旨在提供一个集开发、测试、运维于一体的开发者专属平台,让开发者能快速构建或使用微服务,让开发更简单,让运维更高效。

    微服务平台(Micro Service Platform : MSP)旨在提供一个集开发.测试.运维于一体的开发者专属平台,让开发者能快速构建或使用微服务,让开发更简单,让运维更高效. MSP采用业界 ...

  4. 更强、更稳、更高效:解读 etcd 技术升级的三驾马车

    点击下载<不一样的 双11 技术:阿里巴巴经济体云原生实践> 本文节选自<不一样的 双11 技术:阿里巴巴经济体云原生实践>一书,点击上方图片即可下载! 作者 | 陈星宇(宇慕 ...

  5. 本塔科技借力VR Engine,提供更优质的3D VR内容

    本文分享于华为开发者论坛<本塔科技借力HMS Core VR Engine,提供更优质的3D VR视频内容> 本塔科技致力于为影视行业提供优质的3D VR视频内容,追求稳定.观看感舒适.良 ...

  6. Karmada v1.3:更优雅 更精准 更高效

    摘要:最新发布的1.3版本中,Karmada重新设计了应用跨集群故障迁移功能,实现了基于污点的故障驱逐机制,并提供平滑的故障迁移过程,可以有效保障服务迁移过程的连续性(不断服). 本文分享自华为云社区 ...

  7. Azure IoT带来更高效的新能源生产和会看人脸色的无人超市

    全球分析机构都认为物联网将在未来几年呈现爆发式增长,到2020年,各种传感器.新型物联网设备,再加上传统PC.智能手机.平板电脑.网络电视,以及各类可穿戴智能设备,将交织成一个由300亿到500亿台设 ...

  8. 如何更高效地定制你的bootstrap

    bootstrap已经作为前端开发必不可少的框架之一,应用bootstrap使得我们对布局.样式的设定变得非常简单.但bootstrap提供的默认样式往往不能满足我们的需求,从而定制化bootstra ...

  9. 更高效地提高redis client多线程操作的并发吞吐设计

    Redis是一个非常高效的基于内存的NOSQL数据库,它提供非常高效的数据读写效能.在实际应用中往往是带宽和CLIENT库读写损耗过高导致无法更好地发挥出Redis更出色的能力.下面结合一些redis ...

随机推荐

  1. JSBridge通信原理, 有哪几种实现的方式?

    JsBridge给JavaScript提供了调用Native功能,Native也能够操控JavaScript.这样前端部分就可以方便使用地理位置.摄像头以及登录支付等Native能力啦.JSBridg ...

  2. python实现AES加密、解密

    AES加密方式有五种:ECB, CBC, CTR, CFB, OFB 从安全性角度推荐CBC加密方法,本文介绍了CBC,ECB两种加密方法的python实现 python 在 Windows下使用AE ...

  3. Java 中是如何支持正则表达式操作的?

    Java 中的 String 类提供了支持正则表达式操作的方法,包括:matches(). replaceAll().replaceFirst().split().此外,Java 中可以用 Patte ...

  4. springboot-数据库访问之mybatis

    选中mysqldirver,数据库驱动器 选中jdbc ,帮我们配置数据源,连接数据库 选中mybatis,持久层 <!-- 这个不是springboot官方出来的,是mybatis为了设备sp ...

  5. java-集合-realdo-集合一对多

    school类: package setdone; import java.util.List; public class School { private String schoolname; pr ...

  6. STM32 之 HAL库(固件库) _

    1 STM32的三种开发方式 通常新手在入门STM32的时候,首先都要先选择一种要用的开发方式,不同的开发方式会导致你编程的架构是完全不一样的.一般大多数都会选用标准库和HAL库,而极少部分人会通过直 ...

  7. (stm32f103学习总结)—待机唤醒实验

    一.STM32待机模式介绍 1.1 STM32低功耗模式介绍 很多单片机具有低功耗模式,比如MSP430.STM8L等,我们的STM32 也不例外.默认情况下,系统复位或上电复位后,微控制器进入运行模 ...

  8. html5与css交互 API 《一》classList

    用过jquery的朋友都知道,jquery提供的方法中(3个)可以很方便的为指定的节点添加.删除类选择器,即addClass.removeClass.toggleClass.具体的用法我这里就不谈了, ...

  9. Canvas 与 SVG

    什么是SVG? 引用w3c的一段话就是: SVG 指可伸缩矢量图形 (Scalable Vector Graphics) SVG 用来定义用于网络的基于矢量的图形 SVG 使用 XML 格式定义图形 ...

  10. Vue.js 开发实践:实现精巧的无限加载与分页功能

    本篇文章是一篇Vue.js的教程,目标在于用一种常见的业务场景--分页/无限加载,帮助读者更好的理解Vue.js中的一些设计思想.与许多Todo List类的入门教程相比,更全面的展示使用Vue.js ...