k8s源码
# API SERVER
APIServer提供了 k8s各类资源对象的CURD/watch、认证授权、准入控制等众多核心功能,在k8s中定位类似于大脑和心脏,它的功能包括:
- 提供了集群管理的REST API接口(包括资源CURD、认证授权、数据校验以及集群状态变更);
- 是所有模块的数据交互和通信的枢纽,各模块的运作都依赖于APIServer
- 提供丰富多样的集群安全管控机制
- 直连后端存储(Etcd),是唯一与存储后端直接通信的模块
如图所示,这是创建一个资源(Pod)实例过程中,控制层面所经过的调用过程
因此,APIServer无疑是各模块中 最复杂、定位最核心、涉及面最广、代码量最大 的模块。
APIServer的工作主要围绕着对各类资源对象的管控,因此,在开始阅读APIServer的源码之前,有必要笼统地列举一下它在运行中所用到的核心数据结构等基础性信息
# 基础结构信息
APIServer的工作主要围绕着对各类资源对象的管控,因此,在开始阅读APIServer的源码之前,有必要笼统地列举一下它在运行中所用到的核心数据结构等基础性信息
Group/Version/Kind/Resource
在K8s的设计中,resource是其最基础、最重要的概念,也是最小的管理单位,所有的管理对象都承载在一个个的resource实例上,为了实现这些resource的复杂管理逻辑,又进一步地将他们分组化、版本化,依照逻辑层次,形成了Group、Version、Kind、Resource核心数据结构:
- Group:资源组,也称APIGroup,常见的有core、apps、extensions等
- Version:资源版本,也称APIVersion,常见的有v1、v1beta1 (Resource可能属于拥有多个Version,这些version也会有优先级之分,例如deployment即属于apps/v1,又属于extensions/v1beta1,在不同k8s版本中,version的优先级可能会变化)
- Kind:资源种类,描述资源的类别,例如pod类别、svc类别等
- Resource:资源实例对象,也称为APIResource
- SubResource:子资源,部分资源实例会 有子资源,例如Deployment资源会拥有Status子资源
- CRD: Custom Resource Definitions,用户自定义资源类型
锚定形式
概念层面,在K8s中,常见的资源路径锚定形式为:///,例如deployment对应的路径是:apps/v1/deployments/status
官方通常通过缩写词GVR(GroupVersionKind)来描述一个资源的明确锚定位置(类似于绝对路径?),同理,GVK(GroupVersionKind)锚定资源的明确所属类型,在项目代码中也经常用到,例如
资源结构体
而落实到代码中,每一种资源的结构体定义文件都位于其Group下的的types.go文件中,例如,Deployment资源的结构体定义在这里pkg/apis/apps/types.go:268
资源操作方法
概念层面,每种resource都有对应的管理操作方法,目前支持的有这几种
- get
- list
- create
- update
- patch
- delete
- deletecolletction
- watch
使用[]string结构来描述资源所对应的操作,而[]string终归只是描述,需要与实际的存储资源CRUD操作关联,因此,不难猜测,每种string描述的方法会map到具体的方法上去,结构类似于: map[string]Function
内部和外部Version
在k8s的设计中,资源版本分外部版本(external)和内部版本(internal)之分,外部版本(例如v1/v1beta1/v1beta2)提供给外部使用,而对应的内部版本仅在APIServer内部使用
区分内外版本的作用:
- 提供不同版本之间的转换功能,例如从v1beta1-->v1的过程实际是v1beta1--> internal -->v1,转换函数会注册到scheme表中
- 减少复杂度,方便版本维护,避免维护多个版本的对应代码,实际APIServer端处理的都是转换后的内部版本
- 不同外部版本资源之间的字段/功能可能存在些许差异,而内部版本包含所有版本的字段/功能,这为它作为外部资源版本之间转换的桥梁提供了基础。
Schema注册表
每一种Resource都有对应的Kind,为了更便于分类管理这些资源,APIServer设计了一种名为scheme的结构体,类似于注册表,运行时数据存放内存中,提供给各种资源进行注册,scheme有如下作用:
- 提供资源的版本转换功能
- 提供资源的序列化/反序列化功能
Scheme支持注册两种类型的资源:
- UnversionedType 无版本资源。这个在现版本的k8s中使用非常少,可以忽略
- VersionedType 几乎所有的资源都是携带版本的,是常用的类型
注册方法
scheme表提供两个注册方法:AddKnownTypes
| AddKnownTypeWithName
,使用reflect反射的方式获取type obj的gvk然后进行注册
序列化和反序列化
APIServer对资源的描述支持yaml和json格式,分别对应不同的Serializer,Serializer内置有bool类型的yaml字段,来辨别是否是yaml Serializer。
序列化代码位于:vendor/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go:223
可以得知,默认以json格式响应,而对于yaml格式,先将其转换为json格式,再转换回yaml格式响应
反序列化代码:vendor/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go:86
go-restful
k8s选用的Restful框架是go-restful,简单说明一下go-restful的结构,辅助后面对于APIServer工作流程的理解。
go-restful层级结构概念自顶上下依次有:
- Container: 一个Container就是一个独立的http server,可拥有独立的地址端口组合(类似nginx的server层级)
- WebService: 大粒度的分类,某一类别的服务可归属到同一个WebService中,其下包含多个Route
- Route: 每个Route对应具体的uri路径,将该路径路由到对应的handler函数上
# 预启动和启动流程
资源注册
scheme是一种内存型的注册表,提供给各类gvk进行注册。在APIServer http服务启动前的第一步,就是将所支持的gvk注册到scheme中,后面的步骤会依赖scheme注册表信息。
值得注意的是,并没有函数方法来显示地注册scheme,而是通过go语言的包导入init机制来初始化注册的
认证配置
APIServer支持如下的认证策略:
- X509 Client Certs
- Static Token File
- Bootstrap Tokens
- Service Account Tokens
- OpenID Connect Tokens(OIDC)
- Webhook Token Authentication
- Authenticating Proxy
# 认证机制
所有 Kubernetes 集群都有两类用户:由 Kubernetes 管理的服务账号和普通用户。
其中服务账号(ServiceAccount)是提供给集群中的程序使用,以Secret资源保存凭据,挂载到pod中,从而允许集群内的服务调用k8s API。
而普通用户,尚不支持使用API创建,一般由证书创建,Kubernetes 使用证书中的 'subject' 的通用名称(Common Name)字段(例如,"/CN=bob")来 确定用户名。
Kubernetes 使用身份认证插件利用客户端证书、持有者令牌(Bearer Token)、身份认证代理(Proxy) 或者 HTTP 基本认证机制来认证 API 请求的身份。HTTP 请求发给 API 服务器时, 插件会将以下属性关联到请求本身:
- 用户名:用来辩识最终用户的字符串。常见的值可以是
kube-admin
或jane@example.com
。 - 用户 ID:用来辩识最终用户的字符串,旨在比用户名有更好的一致性和唯一性。
- 用户组:取值为一组字符串,其中各个字符串用来标明用户是某个命名的用户逻辑集合的成员。 常见的值可能是
system:masters
或者devops-team
等。 - 附加字段:一组额外的键-值映射,键是字符串,值是一组字符串;用来保存一些鉴权组件可能 觉得有用的额外信息。
与其它身份认证协议(LDAP、SAML、Kerberos、X509 的替代模式等等)都可以通过 使用一个身份认证代理 (opens new window)或 身份认证 Webhoook (opens new window)来实现。
认证流程
RequestHeader认证
是一种代理认证方式,需要再apiserver启动时以参数形式配置,来看看官方的介绍:
API 服务器可以配置成从请求的头部字段值(如 X-Remote-User
)中辩识用户。 这一设计是用来与某身份认证代理一起使用 API 服务器,代理负责设置请求的头部字段值。
--requestheader-username-headers
必需字段,大小写不敏感。用来设置要获得用户身份所要检查的头部字段名称列表(有序)。第一个包含数值的字段会被用来提取用户名。--requestheader-group-headers
可选字段,在 Kubernetes 1.6 版本以后支持,大小写不敏感。 建议设置为 "X-Remote-Group"。用来指定一组头部字段名称列表,以供检查用户所属的组名称。 所找到的全部头部字段的取值都会被用作用户组名。--requestheader-extra-headers-prefix
可选字段,在 Kubernetes 1.6 版本以后支持,大小写不敏感。 建议设置为 "X-Remote-Extra-"。用来设置一个头部字段的前缀字符串,API 服务器会基于所给 前缀来查找与用户有关的一些额外信息。这些额外信息通常用于所配置的鉴权插件。 API 服务器会将与所给前缀匹配的头部字段过滤出来,去掉其前缀部分,将剩余部分 转换为小写字符串并在必要时执行百分号解码 (opens new window) 后,构造新的附加信息字段键名。原来的头部字段值直接作为附加信息字段的值。
BasicAuth认证
BasicAuth是一种简单的基础http认证,用户名、密码写入http请求头中,用base64编码,防君子不防小人,安全性较低,因此很少使用。快速略过
启动apiserver时,使用--basic-auth-file参数指定csv文件,csv里面以逗号切割,存放用户名、密码、uid
X509 CA认证
又称TLS双向认证,APIServer启动时使用--client-ca-file指定客户端的证书文件,用作请求的认证
BearerToken认证
这种认证方式是专为k8s节点准备的,避免每个节点都要手动配置TLS证书,在apiserver启动时指定--enable-bootstrap-token-auth
参数来启用这种认证方式
bearertoken的认证方式是在请求头里放入bearer令牌,令牌的格式为 [a-z0-9]{6}.[a-z0-9]{16}
。第一个部分是令牌的 ID;第二个部分 是令牌的 Secret。对应http请求头的格式是:
Authorization: Bearer xxxxxx.xxxxxxxxxxxxxxxx
ServiceAccount 认证
启用方式为APIServer命令使用--service-account-key-file
参数指定一个为token签名的PEM秘钥文件。
SA认证是jwt形式的认证,使用方式与bearer token类似,也是放在请求头里,内容为Base64编码,header格式为:
Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo0NTk0LCJ1c2VybmFtZSI6InlpbndlbnFpbiIsImV4cCI6MTU3MDY3NDEyNywiZW1haWwiOiIifQ.djC2w5l3IiXYv7slZtGzlMzLc3_oPuR1M0dM9FwoaUU
token哪里来呢?答案就是ServiceAccount。
SA是一种面向集群内部应用需要调用APIServer的场景所设计的认证方式。在创建ServiceAccount资源时,可以显示地设置标签将ServiceAccount绑定给某Deploy/sts/pod,也可以在Deploy/sts/pod的声明文件里显示指定ServiceAccount。ServiceAccount会自动创建Secret资源,token秘钥存放其中。
在相应的容器层面,token信息会被挂载进容器中,包含3个文件:
- namespace文件:指明命名空间
- ca.crt文件:APIServer的公钥证书,容器用来校验APIServer
- token文件: 存放在Secret里的JWT token
WebhookToken认证
Webhook 身份认证是一种用来验证持有者令牌的回调机制。
--authentication-token-webhook-config-file
指向一个配置文件,其中描述 如何访问远程的 Webhook 服务。--authentication-token-webhook-cache-ttl
用来设定身份认证决定的缓存时间。 默认时长为 2 分钟。
当客户端尝试在 API 服务器上使用持有者令牌完成身份认证( 如前 (opens new window)所述)时, 身份认证 Webhook 会用 POST 请求发送一个 JSON 序列化的对象到远程服务。 该对象是 authentication.k8s.io/v1beta1
组的 TokenReview
对象, 其中包含持有者令牌。 Kubernetes 不会强制请求提供此 HTTP 头部。
要注意的是,Webhook API 对象和其他 Kubernetes API 对象一样,也要受到同一 版本兼容规则 (opens new window)约束。 实现者要了解对 Beta 阶段对象的兼容性承诺,并检查请求的 apiVersion
字段, 以确保数据结构能够正常反序列化解析。此外,API 服务器必须启用 authentication.k8s.io/v1beta1
API 扩展组 (--runtime-config=authentication.k8s.io/v1beta1=true
)。
Anonymous认证
启用匿名请求支持之后,如果请求没有被已配置的其他身份认证方法拒绝,则被视作 匿名请求(Anonymous Requests)。这类请求获得用户名 system:anonymous
和 对应的用户组 system:unauthenticated
。
例如,在一个配置了令牌身份认证且启用了匿名访问的服务器上,如果请求提供了非法的 持有者令牌,则会返回 401 Unauthorized
错误。 如果请求没有提供持有者令牌,则被视为匿名请求。
在 1.5.1-1.5.x 版本中,匿名访问默认情况下是被禁用的,可以通过为 API 服务器设定 --anonymous-auth=true
来启用。
在 1.6 及之后版本中,如果所使用的鉴权模式不是 AlwaysAllow
,则匿名访问默认是被启用的。 从 1.6 版本开始,ABAC 和 RBAC 鉴权模块要求对 system:anonymous
用户或者 system:unauthenticated
用户组执行显式的权限判定,所以之前的为 *
用户或 *
用户组赋予访问权限的策略规则都不再包含匿名用户。
# 鉴权
请求在通过认证之后,请求将进入鉴权环节
审查请求属性
Kubernetes 仅审查以下 API 请求属性:
- 用户 - 身份验证期间提供的
user
字符串。 - 组 - 经过身份验证的用户所属的组名列表。
- 额外信息 - 由身份验证层提供的任意字符串键到字符串值的映射。
- API - 指示请求是否针对 API 资源。
- 请求路径 - 各种非资源端点的路径,如
/api
或/healthz
。 - API 请求动词 - API 动词
get
、list
、create
、update
、patch
、watch
、proxy
、redirect
、delete
和deletecollection
用于资源请求。 要确定资源 API 端点的请求动词,请参阅 确定请求动词 (opens new window)。 - HTTP 请求动词 - HTTP 动词
get
、post
、put
和delete
用于非资源请求。 - Resource - 正在访问的资源的 ID 或名称(仅限资源请求)- 对于使用
get
、update
、patch
和delete
动词的资源请求,你必须提供资源名称。 - 子资源 - 正在访问的子资源(仅限资源请求)。
- 名字空间 - 正在访问的对象的名称空间(仅适用于名字空间资源请求)。
- API 组 - 正在访问的 API 组 (opens new window) (仅限资源请求)。空字符串表示核心 API 组 (opens new window)
鉴权策略
目前支持6种鉴权策略,每种鉴权策略对应一个鉴权器,使用的鉴权策略需要在APIServer启动时以参数--authorization-mode
的形式指定,多种策略同时指定时使用','号连接:
策略分类有:
--authorization-mode=ABAC
基于属性的访问控制(ABAC)模式允许你 使用本地文件配置策略。--authorization-mode=RBAC
基于角色的访问控制(RBAC)模式允许你使用 Kubernetes API 创建和存储策略。--authorization-mode=Webhook
WebHook 是一种 HTTP 回调模式,允许你使用远程 REST 端点管理鉴权。--authorization-mode=Node
节点鉴权是一种特殊用途的鉴权模式,专门对 kubelet 发出的 API 请求执行鉴权。--authorization-mode=AlwaysDeny
该标志阻止所有请求。仅将此标志用于测试。--authorization-mode=AlwaysAllow
此标志允许所有请求。仅在你不需要 API 请求 的鉴权时才使用此标志。
与上一篇的认证模块不同的是,当配置多个鉴权模块时,鉴权模块按顺序检查,靠前的模块具有更高的优先级来允许或拒绝请求。
鉴权结果
# Kubelet
Kubelet作为k8s核心组件中的daemon端运行在集群中的每一个节点上,承接着控制平面的指令向数据平面传达。不像scheduler、controller组件只负责相对单一的功能,kubelet除了管理自身的运行时外,还需要和宿主系统(linux)、CRI、CNI、CSI等外部组件对接,无疑是一个复杂度很高的组件
Kubelet的主要功能
- pod启停
- 容器网络管理
- Volume管理
- 探针检查
- 容器监控
本篇开始进入数据交互平面的daemon组件kubelet部分,看看kubelet是如何在控制平面和数据平面中以承上启下的模式工作的
# kube-proxy
https://www.zhihu.com/topic/21216319/top-answers
# 公共库
# client-go
client-go 是 kubernetes 中比较重要的一个组件,从我上一篇文章中梳理的图中可以看出来,apiserver 是一个核心,其它组件都要和这个核心模块交互,所以 client-go 的出现就是为了统一封装对 apiserver 的交互访问。
client-go 这种设计思路还是不错的,当然是适合 kubernetes 这样的项目,几乎所有的模块都在围绕 apiserver,那么和 apiserver 的交互就显的尤为重要,那么这部分代码的抽象封装也就顺理成章了。这种解偶方式也是挺特别的,在看了书,走读了这部分的源码之后也才发现,同样的 client 在使用方式,使用对象不一样,就需要不一样的封装方式
目录名 | 用途 |
---|---|
discovery | 这个是 discovery client 的代码,是对 rest 客户端的进一步封装,用于发现 apiserver 所支持的能力和信息 |
dynamic | 这个是 dynamic client 的代码,是对 rest 客户端的进一步封装,动态客户端,面向处理 CRD |
examples | 这里面有一些例子,比如对 deployment 创建、修改,如何选主,workqueue 如何使用等等 |
informers | 这就是 client-go 中非常有名的 informer 机制的核心代码 |
kubernetes | clientset 的代码,也是对 rest 客户端的进一步封装,提供复杂的资源访问和管理能力 |
listers | 为每个 k8s 资源提供 lister 功能,提供了只读缓存功能 |
metadata | |
pkg | 主要是一些功能函数,比如版本函数 |
rest | 这是最基础的 client,其它的 client 都是基于此派生的 |
scale | scale client 的代码 |
tools | 工具函数库,主要是和 k8s 相关的工具函数 |
util | 通用的一些工具函数 |
transport | 提供安全 tcp 链接 |
核心数据结构
type RESTClient struct {
// 这个初始化的 apiserver 的地址,下面我也贴了一个 kubeconfig 文件的内容,这个地址就是 cluster 的 server。
base *url.URL
// 这个是 apiVersion
versionedAPIPath string
// 对客户端编解码的设置
content ClientContentConfig
// creates BackoffManager that is passed to requests.
createBackoffMgr func() BackoffManager
// 限流控制,是针对这个客户端的所有请求的。这个也是非常好的一个设计,一般 sdk 的设计很少考虑这个,大多数只考虑功能
rateLimiter flowcontrol.RateLimiter
// warningHandler is shared among all requests created by this client.
// If not set, defaultWarningHandler is used.
warningHandler WarningHandler
// http 请求客户端
Client *http.Client
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# informer
Informer (就是 SharedInformer)是 client-go 的重要组成部分,在了解 client-go 之前,了解一下 Informer 的实现是很有必要的
主要使用到 Informer 和 workqueue 两个核心组件。Controller 可以有一个或多个 informer 来跟踪某一个 resource。Informter 跟 API server 保持通讯获取资源的最新状态并更新到本地的 cache 中,一旦跟踪的资源有变化,informer 就会调用 callback。把关心的变更的 Object 放到 workqueue 里面。然后 woker 执行真正的业务逻辑,计算和比较 workerqueue 里 items 的当前状态和期望状态的差别,然后通过 client-go 向 API server 发送请求,直到驱动这个集群向用户要求的状态演化
# wait
代码路径: vendor/k8s.io/apimachinery/pkg/util/wait/wait.go
wait库内的各种function,大体来说都是以轮询的形式,根据时间间隔、条件判断,来确定工具执行函数是否应被继续执行。按代码中呈现,按触发形式再细化一下,各function则可以分为这几类
条件类型 | 说明 |
---|---|
Until类 | 用得最多的类型,一般以一条chan struct{} 或context Done接收done信号作为终止轮询的依据 |
Backoff类 | 每间隔一定的时长执行一次回溯函数,一般情况下,间隔时长随着回溯次数递增而倍数级延长,但间隔时长也会有上限值 |
poll类 | 两条channel,一条用作传递单次执行信号用来轮询,一条用作传递done信号 |
Untile类型有两个具体实现,分别是Until和UntilWithContext
func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
JitterUntil(f, period, 0.0, true, stopCh)
}
2
3
JitterUntil函数可谓是把条件考虑得很细致,参数上有执行周期、抖动因子、窗口期(是否包含函数执行时间),另外在stopCh信号处理上也做到了预防超期执行,JitterUntil函数已经足以应对各类以时间间隔维度的轮询场景了
UntilWithContext
func UntilWithContext(ctx context.Context, f func(context.Context), period time.Duration) {
JitterUntilWithContext(ctx, f, period, 0.0, true)
}
2
3
Backoff类
https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/pkg/pkg-01-wait-%E5%AE%9A%E6%97%B6(%E6%9D%A1%E4%BB%B6)%E8%BD%AE%E8%AF%A2%E5%BA%93.md
# 资源
源码剖析:https://wqyin.cn/gitbooks/kubeSourceCodeNote/apiServer/Kubernetes%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-APIServer-P1-%E5%9F%BA%E7%A1%80%E7%BB%93%E6%9E%84%E4%BF%A1%E6%81%AF.html
源码剖析:https://helight.cn/blog/2020/kube-controller-manager-code-1/
源码剖析:https://github.com/cloudnativeto/sig-kubernetes/blob/master/docs/event/code-club.md
https://sulao.cn/post/828.html