Compare commits

..

21 Commits

Author SHA1 Message Date
yinwenqin fa44e77f47 pkg-01-wait-定时(条件)轮询库
5 years ago
yinwenqin 7093310392 pkg-01-wait-定时(条件)轮询库
5 years ago
yinwenqin fdc96bb8ca update cdn url
5 years ago
yinwenqin 86d0448e22 update cdn url
5 years ago
yinwenqin f32a2b21b0 readme modify
5 years ago
yinwenqin 44dbb0594f kubelet p1
5 years ago
yinwenqin 28ab4a9408 readme
6 years ago
yinwenqin b95769ba1d pic domain change
6 years ago
yinwenqin e2f071ccbf 忽略.DS_Store文件
6 years ago
yinwenqin 925499f54e 忽略.DS_Store文件
6 years ago
yinwenqin 6e572c7abd update
6 years ago
yinwenqin cc49274fb2 apiserver p4
6 years ago
yinwenqin b3bae73cbd apiserver p4
6 years ago
yinwenqin fb6821dcd9 APIServer p3
6 years ago
yinwenqin b563cc97d8 readme update
6 years ago
yinwenqin d1a55bdae3 p2 modify
6 years ago
yinwenqin 6869d75f71 apiserver p2
6 years ago
yinwenqin 63472f121c apiserver p2
6 years ago
yinwenqin 243d121beb apiserver p1
6 years ago
yinwenqin aeb43da63a apiserver readme
6 years ago
yinwenqin 11517f3bad
Merge pull request #1 from yinwenqin/add-license-1
6 years ago

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored

@ -0,0 +1 @@
.DS_Store

@ -2,14 +2,12 @@
在熟悉kubernetes及常用组件、插件的管理使用后,总还觉得差了些什么,不够通透,是时候来读一读源码了,结合代码与实际使用场景来互相印证,有助于对kubernetes的理解更为透彻。这里将会分多篇介绍kubernetes各核心组件的工作模式、调度管理算法等。 在熟悉kubernetes及常用组件、插件的管理使用后,总还觉得差了些什么,不够通透,是时候来读一读源码了,结合代码与实际使用场景来互相印证,有助于对kubernetes的理解更为透彻。这里将会分多篇介绍kubernetes各核心组件的工作模式、调度管理算法等。
## 版本
Kubernetes v1.14.3 ,最新部署的一套环境是此版本,代码版本保持一致,方便后续测试调试
## 核心组件 ## 核心组件
- [Scheduler](https://github.com/yinwenqin/kubeSourceCodeNote/tree/master/scheduler) - [Scheduler](https://github.com/yinwenqin/kubeSourceCodeNote/tree/master/scheduler)
- [Controller](https://github.com/yinwenqin/kubeSourceCodeNote/tree/master/controller) - [Controller](https://github.com/yinwenqin/kubeSourceCodeNote/tree/master/controller)
- 待补充 - [APIServer](https://github.com/yinwenqin/kubeSourceCodeNote/tree/master/apiServer)
- [Kubelet](https://github.com/yinwenqin/kubeSourceCodeNote/tree/master/kubelet)
- -
## 环境准备 ## 环境准备
@ -31,14 +29,14 @@ cd kubernetes
git checkout v1.14.3 # 切换到tag为v1.14.3的版本 git checkout v1.14.3 # 切换到tag为v1.14.3的版本
``` ```
文章全部基于v1.14版本,后续版本主要代码大同小异亦可参考,文中若有误,还请指出,非常感谢~
源码拉取完成后,用自己的IDE打开,准备工作完毕. 源码拉取完成后,用自己的IDE打开,准备工作完毕.
## Tips ## Tips
Kubernetes这一整个项目颇为庞大,一般情况下,如果熟悉kubernetes的应用,结合应用来理解源码的设计理念会容易许多,因此,对其应用不熟悉的朋友,不建议直接阅读源码。另外,目前处于边阅读代码边输出总结的阶段,如文中有误,请予以指正,非常感谢! Kubernetes这一整个项目颇为庞大,一般情况下,如果熟悉kubernetes的应用,结合应用来理解源码的设计理念会容易许多,因此,对其应用不熟悉的朋友,不建议直接阅读源码。另外,目前处于边阅读代码边输出总结的阶段,如文中有误,请予以指正,非常感谢!
在此立一个flag,源码干货总结笔记,至少做到周不断更!
## 参考 ## 参考

@ -0,0 +1,490 @@
## 前言
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中,常见的资源路径锚定形式为:<Group>/<Version>/<Resource>/<SubResource>,例如deployment对应的路径是:apps/v1/deployments/status
官方通常通过缩写词**GVR**(GroupVersionKind)来描述一个资源的明确锚定位置(类似于绝对路径?),同理,**GVK**(GroupVersionKind)锚定资源的明确所属类型,在项目代码中也经常用到,例如:
`vendor/k8s.io/apimachinery/pkg/runtime/schema/group_version.go:96`
```go
// GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
// to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling
type GroupVersionResource struct {
Group string
Version string
Resource string
}
```
### 资源结构体
而落实到代码中,每一种资源的结构体定义文件都位于其Group下的的types.go文件中,例如,Deployment资源的结构体定义在这里:
`pkg/apis/apps/types.go:268`
```go
type Deployment struct {
metav1.TypeMeta
// +optional
metav1.ObjectMeta
// Specification of the desired behavior of the Deployment.
// +optional
Spec DeploymentSpec
// Most recently observed status of the Deployment.
// +optional
Status DeploymentStatus
}
```
`metav1.TypeMeta`
```go
type TypeMeta struct {
// Kind is a string value representing the REST resource this object represents.
// Servers may infer this from the endpoint the client submits requests to.
// Cannot be updated.
// In CamelCase.
// More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds
// +optional
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
// APIVersion defines the versioned schema of this representation of an object.
// Servers should convert recognized schemas to the latest internal value, and
// may reject unrecognized values.
// More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources
// +optional
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
}
```
`metav1.ObjectMeta`
```go
type ObjectMeta struct {
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`
GenerateName string `json:"generateName,omitempty" protobuf:"bytes,2,opt,name=generateName"`
Namespace string `json:"namespace,omitempty" protobuf:"bytes,3,opt,name=namespace"`
SelfLink string `json:"selfLink,omitempty" protobuf:"bytes,4,opt,name=selfLink"`
UID types.UID `json:"uid,omitempty" protobuf:"bytes,5,opt,name=uid,casttype=k8s.io/kubernetes/pkg/types.UID"`
ResourceVersion string `json:"resourceVersion,omitempty" protobuf:"bytes,6,opt,name=resourceVersion"`
Generation int64 `json:"generation,omitempty" protobuf:"varint,7,opt,name=generation"`
CreationTimestamp Time `json:"creationTimestamp,omitempty" protobuf:"bytes,8,opt,name=creationTimestamp"`
DeletionTimestamp *Time `json:"deletionTimestamp,omitempty" protobuf:"bytes,9,opt,name=deletionTimestamp"`
DeletionGracePeriodSeconds *int64 `json:"deletionGracePeriodSeconds,omitempty" protobuf:"varint,10,opt,name=deletionGracePeriodSeconds"`
Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"`
Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"`
OwnerReferences []OwnerReference `json:"ownerReferences,omitempty" patchStrategy:"merge" patchMergeKey:"uid" protobuf:"bytes,13,rep,name=ownerReferences"`
Initializers *Initializers `json:"initializers,omitempty" protobuf:"bytes,16,opt,name=initializers"`
Finalizers []string `json:"finalizers,omitempty" patchStrategy:"merge" protobuf:"bytes,14,rep,name=finalizers"`
ClusterName string `json:"clusterName,omitempty" protobuf:"bytes,15,opt,name=clusterName"`
ManagedFields []ManagedFieldsEntry `json:"managedFields,omitempty" protobuf:"bytes,17,rep,name=managedFields"`
}
```
### 资源操作方法
概念层面,每种resource都有对应的管理操作方法,目前支持的有这几种:
- get
- list
- create
- update
- patch
- delete
- deletecolletction
- watch
分别归属于 增、删、改、查四类。
落实到代码里,资源对应的操作方法,都在metav1.Verbs结构体中归纳:
`vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go:970`
```go
// APIResource specifies the name of a resource and whether it is namespaced.
type APIResource struct {
...
// verbs is a list of supported kube verbs (this includes get, list, watch, create,
// update, patch, delete, deletecollection, and proxy)
Verbs Verbs `json:"verbs" protobuf:"bytes,4,opt,name=verbs"`
...
}
type Verbs []string
func (vs Verbs) String() string {
return fmt.Sprintf("%v", []string(vs))
}
```
使用[]string结构来描述资源所对应的操作,而[]string终归只是描述,需要与实际的存储资源CRUD操作关联,因此,不难猜测,每种string描述的方法会map到具体的方法上去,结构类似于: map[string]Function,这里不继续展开,后面的章节中会提及。
### 内部和外部Version
在k8s的设计中,资源版本分外部版本(external)和内部版本(internal)之分,外部版本(例如v1/v1beta1/v1beta2)提供给外部使用,而对应的内部版本仅在APIServer内部使用。
区分内外版本的作用:
- 提供不同版本之间的转换功能,例如从v1beta1-->v1的过程实际是v1beta1--> internal -->v1,转换函数会注册到scheme表中
- 减少复杂度,方便版本维护,避免维护多个版本的对应代码,实际APIServer端处理的都是转换后的内部版本
- 不同外部版本资源之间的字段/功能可能存在些许差异,而内部版本包含所有版本的字段/功能,这为它作为外部资源版本之间转换的桥梁提供了基础。
内部版本和外部版本对于资源结构体定义显著的区别是,内部版本是不带json和proto标签的,因为其不需要结构化提供给外部。以Deployment资源为例
内部版本的代码路径为:`pkg/apis/apps/types.go:268`
```go
type Deployment struct {
metav1.TypeMeta
// +optional
metav1.ObjectMeta
// Specification of the desired behavior of the Deployment.
// +optional
Spec DeploymentSpec
// Most recently observed status of the Deployment.
// +optional
Status DeploymentStatus
}
```
外部版本代码路径:`vendor/k8s.io/api/apps/v1/types.go:254`
```go
type Deployment struct {
metav1.TypeMeta `json:",inline"`
// Standard object metadata.
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Specification of the desired behavior of the Deployment.
// +optional
Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
// Most recently observed status of the Deployment.
// +optional
Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}
```
以向APIServer发起创建一个资源实例为例,在解码时,APIServer首先从 HTTP 路径中获取对应的外部version,然后使用 scheme 以外部version创建一个空对象,然后将该对象转换成内部版本对应的对象结构进行持久存储(写入etcd).
而在查询请求中,资源对象会被从内部版本转换为路径对应的外部版本,响应给请求端。
关于内外部版本的说明,这篇文章讲得很详细,可参考:
https://www.kubernetes.org.cn/6839.html
## Scheme注册表
每一种Resource都有对应的Kind,为了更便于分类管理这些资源,APIServer设计了一种名为scheme的结构体,类似于注册表,运行时数据存放内存中,提供给各种资源进行注册,scheme有如下作用:
- 提供资源的版本转换功能
- 提供资源的序列化/反序列化功能
Scheme支持注册两种类型的资源:
- UnversionedType 无版本资源。这个在现版本的k8s中使用非常少,可以忽略
- VersionedType 几乎所有的资源都是携带版本的,是常用的类型
### scheme结构体
`vendor/k8s.io/apimachinery/pkg/runtime/scheme.go:47`
```go
type Scheme struct {
// versionMap allows one to figure out the go type of an object with
// the given version and name.
gvkToType map[schema.GroupVersionKind]reflect.Type
// typeToGroupVersion allows one to find metadata for a given go object.
// The reflect.Type we index by should *not* be a pointer.
typeToGVK map[reflect.Type][]schema.GroupVersionKind
// unversionedTypes are transformed without conversion in ConvertToVersion.
unversionedTypes map[reflect.Type]schema.GroupVersionKind
// unversionedKinds are the names of kinds that can be created in the context of any group
// or version
// TODO: resolve the status of unversioned types.
unversionedKinds map[string]reflect.Type
// Map from version and resource to the corresponding func to convert
// resource field labels in that version to internal version.
fieldLabelConversionFuncs map[schema.GroupVersionKind]FieldLabelConversionFunc
// defaulterFuncs is an array of interfaces to be called with an object to provide defaulting
// the provided object must be a pointer.
defaulterFuncs map[reflect.Type]func(interface{})
// converter stores all registered conversion functions. It also has
// default converting behavior.
converter *conversion.Converter
// versionPriority is a map of groups to ordered lists of versions for those groups indicating the
// default priorities of these versions as registered in the scheme
versionPriority map[string][]string
// observedVersions keeps track of the order we've seen versions during type registration
observedVersions []schema.GroupVersion
// schemeName is the name of this scheme. If you don't specify a name, the stack of the NewScheme caller will be used.
// This is useful for error reporting to indicate the origin of the scheme.
schemeName string
}
```
重点关注字段:
- gvkToType :用map结构存储gvk和Type的映射关系,一个gvk只会具体对应一个Type
- typeToGVK:用map结构存储type和gvk的映射关系,不同的是,一个type可能会对应多个gvk
- converter:map结构,存放资源版本转换的方法
### 注册方法
scheme表提供两个注册方法:`AddKnownTypes` | `AddKnownTypeWithName` ,使用reflect反射的方式获取type obj的gvk然后进行注册。
`vendor/k8s.io/apimachinery/pkg/runtime/scheme.go:172`
```go
func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) {
s.addObservedVersion(gv)
for _, obj := range types {
t := reflect.TypeOf(obj)
if t.Kind() != reflect.Ptr {
panic("All types must be pointers to structs.")
}
t = t.Elem()
s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)
}
}
```
`vendor/k8s.io/apimachinery/pkg/runtime/scheme.go:188`
```go
func (s *Scheme) AddKnownTypeWithName(gvk schema.GroupVersionKind, obj Object) {
s.addObservedVersion(gvk.GroupVersion())
t := reflect.TypeOf(obj)
if len(gvk.Version) == 0 {
panic(fmt.Sprintf("version is required on all types: %s %v", gvk, t))
}
if t.Kind() != reflect.Ptr {
panic("All types must be pointers to structs.")
}
t = t.Elem()
if t.Kind() != reflect.Struct {
panic("All types must be pointers to structs.")
}
if oldT, found := s.gvkToType[gvk]; found && oldT != t {
panic(fmt.Sprintf("Double registration of different types for %v: old=%v.%v, new=%v.%v in scheme %q", gvk, oldT.PkgPath(), oldT.Name(), t.PkgPath(), t.Name(), s.schemeName))
}
s.gvkToType[gvk] = t
for _, existingGvk := range s.typeToGVK[t] {
if existingGvk == gvk {
return
}
}
s.typeToGVK[t] = append(s.typeToGVK[t], gvk)
}
```
## runtime.Object
Runtime库被称为运行时,几乎是所有程序的核心库,在k8s中也不例外,k8s的运行时实现在`vendor/k8s.io/apimachinery/pkg/runtime/`路径下。
其中,runtime.Object是K8s中所有资源类型结构的基石,作为interface被封装,所有的资源对象均以runtime.Object为基础结构实现了相应的interface方法,runtime.Object是一种通用的struct结构体,资源对象可以与runtime.Object通用对象互相转换。runtime.Object结构如下:
`vendor/k8s.io/apimachinery/pkg/runtime/interfaces.go:231`
```go
// Object interface must be supported by all API types registered with Scheme. Since objects in a scheme are
// expected to be serialized to the wire, the interface an Object must provide to the Scheme allows
// serializers to set the kind, version, and group the object is represented as. An Object may choose
// to return a no-op ObjectKindAccessor in cases where it is not expected to be serialized.
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
```
`vendor/k8s.io/apimachinery/pkg/runtime/schema/interfaces.go:22`
```go
// All objects that are serialized from a Scheme encode their type information. This interface is used
// by serialization to set type information from the Scheme onto the serialized version of an object.
// For objects that cannot be serialized or have unique requirements, this interface may be a no-op.
type ObjectKind interface {
// SetGroupVersionKind sets or clears the intended serialized kind of an object. Passing kind nil
// should clear the current setting.
SetGroupVersionKind(kind GroupVersionKind)
// GroupVersionKind returns the stored group, version, and kind of an object, or nil if the object does
// not expose or provide these fields.
GroupVersionKind() GroupVersionKind
}
```
- schema.ObjectKind提供两个方法,分别用于设置和获取对象的gvk
- DeepCopyObject()则用于深复制对象,这个方法会被经常用到,尤其是在写操作的场景中,避免直接操作对象本身。
## 序列化与反序列化
APIServer对资源的描述支持yaml和json格式,分别对应不同的Serializer,Serializer内置有bool类型的yaml字段,来辨别是否是yaml Serializer。
`vendor/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go:60`
```go
type Serializer struct {
meta MetaFactory
creater runtime.ObjectCreater
typer runtime.ObjectTyper
yaml bool
pretty bool
}
```
序列化代码位于:
`vendor/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go:223`
```go
// Encode serializes the provided object to the given writer.
func (s *Serializer) Encode(obj runtime.Object, w io.Writer) error {
if s.yaml {
json, err := caseSensitiveJsonIterator.Marshal(obj)
if err != nil {
return err
}
data, err := yaml.JSONToYAML(json)
if err != nil {
return err
}
_, err = w.Write(data)
return err
}
if s.pretty {
data, err := caseSensitiveJsonIterator.MarshalIndent(obj, "", " ")
if err != nil {
return err
}
_, err = w.Write(data)
return err
}
encoder := json.NewEncoder(w)
return encoder.Encode(obj)
}
```
可以得知,默认以json格式响应,而对于yaml格式,先将其转换为json格式,再转换回yaml格式响应
反序列化:
`vendor/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go:86`
```go
func (customNumberDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
switch iter.WhatIsNext() {
case jsoniter.NumberValue:
var number jsoniter.Number
iter.ReadVal(&number)
i64, err := strconv.ParseInt(string(number), 10, 64)
if err == nil {
*(*interface{})(ptr) = i64
return
}
f64, err := strconv.ParseFloat(string(number), 64)
if err == nil {
*(*interface{})(ptr) = f64
return
}
iter.ReportError("DecodeNumber", err.Error())
default:
*(*interface{})(ptr) = iter.Read()
}
}
```
## go-restful
k8s选用的Restful框架是go-restful,简单说明一下go-restful的结构,辅助后面对于APIServer工作流程的理解。
go-restful层级结构概念自顶上下依次有:
- Container: 一个Container就是一个独立的http server,可拥有独立的地址端口组合(类似nginx的server层级)
- WebService: 大粒度的分类,某一类别的服务可归属到同一个WebService中,其下包含多个Route
- Route: 每个Route对应具体的uri路径,将该路径路由到对应的handler函数上

@ -0,0 +1,791 @@
## 前言
在上一篇[**APIServer-P2-启动流程**](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-APIServer-P2-%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.md)中,把APIServer预启动、加载配置、启动 的流程大致过了一遍,其中也有提到,APIServer的认证机制分为多种,以组合的形式遍历认证,任一机制认证成功,则判定请求认证成功,顺利进入下一个授权判定的环节。那么在本篇,就来详细看看这些认证机制。
## Kubernetes用户
所有 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 的替代模式等等)都可以通过 使用一个[身份认证代理](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#authenticating-proxy)或 [身份认证 Webhoook](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#webhook-token-authentication)来实现。
> 以上摘自官方文档
## 认证机制
### 流程图
首先为了加深印象,再贴一下上一章列出的认证流程图:
![](https://mycloudn.wqyin.cn/20210203112246.png)
> 注:认证器的执行顺序为随机的,图中流向不代表固定顺序
当客户端发送请求到达APIServer端,请求首先进入认证环节,对应处理认证的是Authentication Handler方法,
Authentication Handler方法中,遍历每一个已启用的认证器,仅需某一个认证器返回true,则认证成功结束遍历,若所有认证器全部返回false,则认证失败。
代码路径:
`staging/src/k8s.io/apiserver/pkg/server/config.go:550`
--> `vendor/k8s.io/apiserver/pkg/endpoints/filters/authentication.go:53`
```go
func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences) http.Handler {
// 关闭认证则直接进入下一个环节
if auth == nil {
klog.Warningf("Authentication is disabled")
return handler
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if len(apiAuds) > 0 {
req = req.WithContext(authenticator.WithAudiences(req.Context(), apiAuds))
}
// 这里进行认证
resp, ok, err := auth.AuthenticateRequest(req)
if err != nil || !ok {
if err != nil {
klog.Errorf("Unable to authenticate the request due to an error: %v", err)
}
failed.ServeHTTP(w, req)
return
}
// TODO(mikedanese): verify the response audience matches one of apiAuds if
// non-empty
// authorization header is not required anymore in case of a successful authentication.
req.Header.Del("Authorization")
// 请求上下文携带上认证后的用户的信息
req = req.WithContext(genericapirequest.WithUser(req.Context(), resp.User))
// 计数器
authenticatedUserCounter.WithLabelValues(compressUsername(resp.User.GetName())).Inc()
// 处理请求
handler.ServeHTTP(w, req)
})
}
```
`auth.AuthenticateRequest`是一个接口方法,看看它的引用:
`vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go:35`
![](https://mycloudn.wqyin.cn/20210202115500.png)
引用中的union.go中,聚合了所有的Authenticator链,进去看看
`staging/src/k8s.io/apiserver/pkg/authentication/request/union/union.go:53`
```go
// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects.
func (authHandler *unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
var errlist []error
for _, currAuthRequestHandler := range authHandler.Handlers {
resp, ok, err := currAuthRequestHandler.AuthenticateRequest(req)
if err != nil {
if authHandler.FailOnError {
return resp, ok, err
}
errlist = append(errlist, err)
continue
}
if ok {
return resp, ok, err
}
}
return nil, false, utilerrors.NewAggregate(errlist)
}
```
apiserver的各种request相关定义代码大多集中在这个目录里:
<img src="https://mycloudn.wqyin.cn/20210202145632.png" style="zoom:70%;" />
那么依次逐个来看看每一种Authenticator对应的AuthenticateRequest方法。
### 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 服务器会将与所给前缀匹配的头部字段过滤出来,去掉其前缀部分,将剩余部分 转换为小写字符串并在必要时执行[百分号解码](https://tools.ietf.org/html/rfc3986#section-2.1) 后,构造新的附加信息字段键名。原来的头部字段值直接作为附加信息字段的值。
例如,使用下面的配置:
```
--requestheader-username-headers=X-Remote-User
--requestheader-group-headers=X-Remote-Group
--requestheader-extra-headers-prefix=X-Remote-Extra-
```
针对所收到的如下请求:
```http
GET / HTTP/1.1
X-Remote-User: fido
X-Remote-Group: dogs
X-Remote-Group: dachshunds
X-Remote-Extra-Acme.com%2Fproject: some-project
X-Remote-Extra-Scopes: openid
X-Remote-Extra-Scopes: profile
```
会生成下面的用户信息:
```yaml
name: fido
groups:
- dogs
- dachshunds
extra:
acme.com/project:
- some-project
scopes:
- openid
- profile
```
为了防范头部信息侦听,在请求中的头部字段被检视之前, 身份认证代理需要向 API 服务器提供一份合法的客户端证书, 供后者使用所给的 CA 来执行验证。 警告:*不要* 在不同的上下文中复用 CA 证书,除非你清楚这样做的风险是什么以及 应如何保护 CA 用法的机制。
- `--requestheader-client-ca-file` 必需字段,给出 PEM 编码的证书包。 在检查请求的头部字段以提取用户名信息之前,必须提供一个合法的客户端证书, 且该证书要能够被所给文件中的机构所验证。
- `--requestheader-allowed-names` 可选字段,用来给出一组公共名称(CN)。 如果此标志被设置,则在检视请求中的头部以提取用户信息之前,必须提供 包含此列表中所给的 CN 名的、合法的客户端证书。
> 以上摘自官方文档[身份认证代理](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#authenticating-proxy)
简而言之,就是在apiserver之前有一个代理服务器,通过代理服务器的名义,可以将相关的认证信息在请求头透传给apiserver,以通过apiserver的认证。代理服务的相关信息需要预先配置。
使用kubeadm默认部署的集群,就启用了requestheader的配置:
![](https://mycloudn.wqyin.cn/20210202163330.png)
来看看AuthenticateRequest方法的代码:
`vendor/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader.go:108`
```go
func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
// 取到用户名
name := headerValue(req.Header, a.nameHeaders)
if len(name) == 0 {
return nil, false, nil
}
// 取到用户group
groups := allHeaderValues(req.Header, a.groupHeaders)
// 取到其他信息
extra := newExtra(req.Header, a.extraHeaderPrefixes)
// clear headers used for authentication
// 请求的header信息去除
for _, headerName := range a.nameHeaders {
req.Header.Del(headerName)
}
for _, headerName := range a.groupHeaders {
req.Header.Del(headerName)
}
for k := range extra {
for _, prefix := range a.extraHeaderPrefixes {
req.Header.Del(prefix + k)
}
}
// 返回认证成功信息
return &authenticator.Response{
User: &user.DefaultInfo{
Name: name,
Groups: groups,
Extra: extra,
},
}, true, nil
}
```
参考代码片中注释
### BasicAuth认证
BasicAuth是一种简单的基础http认证,用户名、密码写入http请求头中,用base64编码,防君子不防小人,安全性较低,因此很少使用。快速略过
启动apiserver时,使用--basic-auth-file参数指定csv文件,csv里面以逗号切割,存放用户名、密码、uid。
看看AuthenticateRequest方法:
`vendor/k8s.io/apiserver/plugin/pkg/authenticator/request/basicauth/basicauth.go:38`
```go
// AuthenticateRequest authenticates the request using the "Authorization: Basic" header in the request
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
username, password, found := req.BasicAuth()
if !found {
return nil, false, nil
}
// 简单的账号密码比对,没有什么值得深入的
resp, ok, err := a.auth.AuthenticatePassword(req.Context(), username, password)
// If the password authenticator didn't error, provide a default error
if !ok && err == nil {
err = errInvalidAuth
}
return resp, ok, err
}
```
### x509 CA认证
又称TLS双向认证,APIServer启动时使用--client-ca-file指定客户端的证书文件,用作请求的认证。
`vendor/k8s.io/apiserver/pkg/authentication/request/x509/x509.go:88`
```go
// AuthenticateRequest authenticates the request using presented client certificates
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {
return nil, false, nil
}
// Use intermediates, if provided
optsCopy := a.opts
if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 {
optsCopy.Intermediates = x509.NewCertPool()
for _, intermediate := range req.TLS.PeerCertificates[1:] {
optsCopy.Intermediates.AddCert(intermediate)
}
}
remaining := req.TLS.PeerCertificates[0].NotAfter.Sub(time.Now())
clientCertificateExpirationHistogram.Observe(remaining.Seconds())
// 校验客户端request携带的证书,如果是ca签名的证书,且在有效期内,则视为有效,返回对应的证书chain
chains, err := req.TLS.PeerCertificates[0].Verify(optsCopy)
if err != nil {
return nil, false, err
}
var errlist []error
for _, chain := range chains {
// 这里再展开一下
user, ok, err := a.user.User(chain)
if err != nil {
errlist = append(errlist, err)
continue
}
if ok {
return user, ok, err
}
}
return nil, false, utilerrors.NewAggregate(errlist)
}
```
--> `vendor/k8s.io/apiserver/pkg/authentication/request/x509/x509.go:72`
--> `vendor/k8s.io/apiserver/pkg/authentication/request/x509/x509.go:185`
```go
// CommonNameUserConversion builds user info from a certificate chain using the subject's CommonName
var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (*authenticator.Response, bool, error) {
if len(chain[0].Subject.CommonName) == 0 {
return nil, false, nil
}
// 取得证书chain中的CommonName/Organization作为用户和组
return &authenticator.Response{
User: &user.DefaultInfo{
Name: chain[0].Subject.CommonName,
Groups: chain[0].Subject.Organization,
},
}, true, nil
})
```
### BearerToken认证
这种认证方式是专为k8s节点准备的,避免每个节点都要手动配置TLS证书,在apiserver启动时指定`--enable-bootstrap-token-auth`参数来启用这种认证方式,详细说明见官方文档:
> [启动引导令牌](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#bootstrap-tokens)
bearertoken的认证方式是在请求头里放入bearer令牌,令牌的格式为 `[a-z0-9]{6}.[a-z0-9]{16}`。第一个部分是令牌的 ID;第二个部分 是令牌的 Secret。对应http请求头的格式是:
`Authorization: Bearer xxxxxx.xxxxxxxxxxxxxxxx`
代码路径:
`vendor/k8s.io/apiserver/pkg/authentication/request/bearertoken/bearertoken.go:37`
```go
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
// Authorization header
auth := strings.TrimSpace(req.Header.Get("Authorization"))
if auth == "" {
return nil, false, nil
}
// 切割分开,第二部分就是token
parts := strings.Split(auth, " ")
if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
return nil, false, nil
}
token := parts[1]
// Empty bearer tokens aren't valid
if len(token) == 0 {
return nil, false, nil
}
// 校验token
resp, ok, err := a.auth.AuthenticateToken(req.Context(), token)
// if we authenticated successfully, go ahead and remove the bearer token so that no one
// is ever tempted to use it inside of the API server
if ok {
req.Header.Del("Authorization")
}
// If the token authenticator didn't error, provide a default error
if !ok && err == nil {
err = invalidToken
}
return resp, ok, err
}
```
`AuthenticateToken`是一个接口方法:
`vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go:28`
```go
type Token interface {
AuthenticateToken(ctx context.Context, token string) (*Response, bool, error)
}
// TokenFunc is a function that implements the Token interface.
type TokenFunc func(ctx context.Context, token string) (*Response, bool, error)
// AuthenticateToken implements authenticator.Token.
func (f TokenFunc) AuthenticateToken(ctx context.Context, token string) (*Response, bool, error) {
return f(ctx, token)
}
```
接口的实现方法在:
`plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go:94`
```go
func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
// 把token用'.'切割分为id和密文两段
tokenID, tokenSecret, err := parseToken(token)
if err != nil {
// Token isn't of the correct form, ignore it.
return nil, false, nil
}
// 从APIServer取到对应的secret资源对象,跟这个token进行信息比对
secretName := bootstrapapi.BootstrapTokenSecretPrefix + tokenID
secret, err := t.lister.Get(secretName)
if err != nil {
if errors.IsNotFound(err) {
klog.V(3).Infof("No secret of name %s to match bootstrap bearer token", secretName)
return nil, false, nil
}
return nil, false, err
}
if secret.DeletionTimestamp != nil {
tokenErrorf(secret, "is deleted and awaiting removal")
return nil, false, nil
}
if string(secret.Type) != string(bootstrapapi.SecretTypeBootstrapToken) || secret.Data == nil {
tokenErrorf(secret, "has invalid type, expected %s.", bootstrapapi.SecretTypeBootstrapToken)
return nil, false, nil
}
ts := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey)
if subtle.ConstantTimeCompare([]byte(ts), []byte(tokenSecret)) != 1 {
tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenSecretKey, tokenSecret)
return nil, false, nil
}
id := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey)
if id != tokenID {
tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenIDKey, tokenID)
return nil, false, nil
}
if isSecretExpired(secret) {
// logging done in isSecretExpired method.
return nil, false, nil
}
if getSecretString(secret, bootstrapapi.BootstrapTokenUsageAuthentication) != "true" {
tokenErrorf(secret, "not marked %s=true.", bootstrapapi.BootstrapTokenUsageAuthentication)
return nil, false, nil
}
groups, err := getGroups(secret)
if err != nil {
tokenErrorf(secret, "has invalid value for key %s: %v.", bootstrapapi.BootstrapTokenExtraGroupsKey, err)
return nil, false, nil
}
return &authenticator.Response{
User: &user.DefaultInfo{
Name: bootstrapapi.BootstrapUserPrefix + string(id),
Groups: groups,
},
}, true, nil
}
```
### ServiceAccount(SA)认证
启用方式为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
在集群上找一个例子,以kube-proxy举例:
![](https://mycloudn.wqyin.cn/20210203103806.png)
看看代码层面:
`pkg/serviceaccount/jwt.go:145`
```go
func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData string) (*authenticator.Response, bool, error) {
if !j.hasCorrectIssuer(tokenData) {
return nil, false, nil
}
// 拿到token
tok, err := jwt.ParseSigned(tokenData)
if err != nil {
return nil, false, nil
}
public := &jwt.Claims{}
private := j.validator.NewPrivateClaims()
var (
found bool
errlist []error
)
for _, key := range j.keys {
if err := tok.Claims(key, public, private); err != nil {
errlist = append(errlist, err)
continue
}
found = true
break
}
if !found {
return nil, false, utilerrors.NewAggregate(errlist)
}
tokenAudiences := authenticator.Audiences(public.Audience)
if len(tokenAudiences) == 0 {
// only apiserver audiences are allowed for legacy tokens
tokenAudiences = j.implicitAuds
}
requestedAudiences, ok := authenticator.AudiencesFrom(ctx)
if !ok {
// default to apiserver audiences
requestedAudiences = j.implicitAuds
}
auds := authenticator.Audiences(tokenAudiences).Intersect(requestedAudiences)
if len(auds) == 0 && len(j.implicitAuds) != 0 {
return nil, false, fmt.Errorf("token audiences %q is invalid for the target audiences %q", tokenAudiences, requestedAudiences)
}
// If we get here, we have a token with a recognized signature and
// issuer string.
// 校验token并拿到SA信息
sa, err := j.validator.Validate(tokenData, public, private)
if err != nil {
return nil, false, err
}
return &authenticator.Response{
User: sa.UserInfo(),
Audiences: auds,
}, true, nil
}
```
### WebhookToken认证
Webhook 身份认证是一种用来验证持有者令牌的回调机制。
- `--authentication-token-webhook-config-file` 指向一个配置文件,其中描述 如何访问远程的 Webhook 服务。
- `--authentication-token-webhook-cache-ttl` 用来设定身份认证决定的缓存时间。 默认时长为 2 分钟。
配置文件使用 [kubeconfig](https://kubernetes.io/zh/docs/concepts/configuration/organize-cluster-access-kubeconfig/) 文件的格式。文件中,`clusters` 指代远程服务,`users` 指代远程 API 服务 Webhook。下面是一个例子:
```yaml
# Kubernetes API 版本
apiVersion: v1
# API 对象类别
kind: Config
# clusters 指代远程服务
clusters:
- name: name-of-remote-authn-service
cluster:
certificate-authority: /path/to/ca.pem # 用来验证远程服务的 CA
server: https://authn.example.com/authenticate # 要查询的远程服务 URL。必须使用 'https'。
# users 指代 API 服务的 Webhook 配置
users:
- name: name-of-api-server
user:
client-certificate: /path/to/cert.pem # Webhook 插件要使用的证书
client-key: /path/to/key.pem # 与证书匹配的密钥
# kubeconfig 文件需要一个上下文(Context),此上下文用于本 API 服务器
current-context: webhook
contexts:
- context:
cluster: name-of-remote-authn-service
user: name-of-api-sever
name: webhook
```
当客户端尝试在 API 服务器上使用持有者令牌完成身份认证( 如[前](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#putting-a-bearer-token-in-a-request)所述)时, 身份认证 Webhook 会用 POST 请求发送一个 JSON 序列化的对象到远程服务。 该对象是 `authentication.k8s.io/v1beta1` 组的 `TokenReview` 对象, 其中包含持有者令牌。 Kubernetes 不会强制请求提供此 HTTP 头部。
要注意的是,Webhook API 对象和其他 Kubernetes API 对象一样,也要受到同一 [版本兼容规则](https://kubernetes.io/zh/docs/concepts/overview/kubernetes-api/)约束。 实现者要了解对 Beta 阶段对象的兼容性承诺,并检查请求的 `apiVersion` 字段, 以确保数据结构能够正常反序列化解析。此外,API 服务器必须启用 `authentication.k8s.io/v1beta1` API 扩展组 (`--runtime-config=authentication.k8s.io/v1beta1=true`)。
POST 请求的 Body 部分将是如下格式:
```json
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"spec": {
"token": "<持有者令牌>"
}
}
```
远程服务应该会填充请求的 `status` 字段,以标明登录操作是否成功。 响应的 Body 中的 `spec` 字段会被忽略,因此可以省略。 如果持有者令牌验证成功,应该返回如下所示的响应:
```json
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"status": {
"authenticated": true,
"user": {
"username": "janedoe@example.com",
"uid": "42",
"groups": [
"developers",
"qa"
],
"extra": {
"extrafield1": [
"extravalue1",
"extravalue2"
]
}
}
}
}
```
而不成功的请求会返回:
```json
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"status": {
"authenticated": false
}
}
```
HTTP 状态码可用来提供进一步的错误语境信息。
> 以上摘自官方文档[Webhook 令牌身份认证](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#webhook-token-authentication)
代码:
`vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go:77`
```go
// AuthenticateToken implements the authenticator.Token interface.
func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
// We take implicit audiences of the API server at WebhookTokenAuthenticator
// construction time. The outline of how we validate audience here is:
//
// * if the ctx is not audience limited, don't do any audience validation.
// * if ctx is audience-limited, add the audiences to the tokenreview spec
// * if the tokenreview returns with audiences in the status that intersect
// with the audiences in the ctx, copy into the response and return success
// * if the tokenreview returns without an audience in the status, ensure
// the ctx audiences intersect with the implicit audiences, and set the
// intersection in the response.
// * otherwise return unauthenticated.
wantAuds, checkAuds := authenticator.AudiencesFrom(ctx)
r := &authentication.TokenReview{
Spec: authentication.TokenReviewSpec{
Token: token,
Audiences: wantAuds,
},
}
var (
result *authentication.TokenReview
err error
auds authenticator.Audiences
)
webhook.WithExponentialBackoff(w.initialBackoff, func() error {
// webhook插件向远端认证服务器发起post请求
result, err = w.tokenReview.Create(r)
return err
})
if err != nil {
// An error here indicates bad configuration or an outage. Log for debugging.
klog.Errorf("Failed to make webhook authenticator request: %v", err)
return nil, false, err
}
if checkAuds {
gotAuds := w.implicitAuds
if len(result.Status.Audiences) > 0 {
gotAuds = result.Status.Audiences
}
auds = wantAuds.Intersect(gotAuds)
if len(auds) == 0 {
return nil, false, nil
}
}
r.Status = result.Status
if !r.Status.Authenticated {
var err error
if len(r.Status.Error) != 0 {
err = errors.New(r.Status.Error)
}
return nil, false, err
}
var extra map[string][]string
if r.Status.User.Extra != nil {
extra = map[string][]string{}
for k, v := range r.Status.User.Extra {
extra[k] = v
}
}
// 根据远端认证服务器返回的status字段,填充请求的相关用户信息(name/uid/group等)
return &authenticator.Response{
User: &user.DefaultInfo{
Name: r.Status.User.Username,
UID: r.Status.User.UID,
Groups: r.Status.User.Groups,
Extra: extra,
},
Audiences: auds,
}, true, nil
}
```
### 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` 用户组执行显式的权限判定,所以之前的为 `*` 用户或 `*` 用户组赋予访问权限的策略规则都不再包含匿名用户。
> 以上摘自官方文档[匿名请求](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#anonymous-requests)
代码:
`vendor/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous.go:32`
```go
func NewAuthenticator() authenticator.Request {
return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
auds, _ := authenticator.AudiencesFrom(req.Context())
return &authenticator.Response{
User: &user.DefaultInfo{
Name: anonymousUser,
Groups: []string{unauthenticatedGroup},
},
Audiences: auds,
}, true, nil
})
}
```
## 总结
认证机制有许多种,每种都有不同的适用场景,一般情况下只需要了解认证流程和其中常见的几种认证器即可,不必过分深挖。认证流程通过之后,下一篇进入授权环节

@ -0,0 +1,655 @@
## 前言
在上一篇[**APIServer-P3-APIServer的认证机制**](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-APIServer-P3-APIServer%E7%9A%84%E8%AE%A4%E8%AF%81%E6%9C%BA%E5%88%B6.md)中,讲述了请求进入后的认证过程,在通过认证之后,请求将进入鉴权环节,本篇就此展开。
## 审查请求属性
Kubernetes 仅审查以下 API 请求属性:
- **用户** - 身份验证期间提供的 `user` 字符串。
- **组** - 经过身份验证的用户所属的组名列表。
- **额外信息** - 由身份验证层提供的任意字符串键到字符串值的映射。
- **API** - 指示请求是否针对 API 资源。
- **请求路径** - 各种非资源端点的路径,如 `/api` 或 `/healthz`。
- **API 请求动词** - API 动词 `get`、`list`、`create`、`update`、`patch`、`watch`、 `proxy`、`redirect`、`delete` 和 `deletecollection` 用于资源请求。 要确定资源 API 端点的请求动词,请参阅 [确定请求动词](https://kubernetes.io/zh/docs/reference/access-authn-authz/authorization/#determine-the-request-verb)。
- **HTTP 请求动词** - HTTP 动词 `get`、`post`、`put` 和 `delete` 用于非资源请求。
- **Resource** - 正在访问的资源的 ID 或名称(仅限资源请求)- 对于使用 `get`、`update`、`patch` 和 `delete` 动词的资源请求,你必须提供资源名称。
- **子资源** - 正在访问的子资源(仅限资源请求)。
- **名字空间** - 正在访问的对象的名称空间(仅适用于名字空间资源请求)。
- **API 组** - 正在访问的 [API 组](https://kubernetes.io/zh/docs/concepts/overview/kubernetes-api/#api-groups) (仅限资源请求)。空字符串表示[核心 API 组](https://kubernetes.io/zh/docs/reference/using-api/#api-groups)
## 鉴权的描述
### 鉴权策略分类
目前支持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 请求 的鉴权时才使用此标志。
与上一篇的认证模块不同的是,当配置多个鉴权模块时,鉴权模块按**顺序**检查,靠前的模块具有更高的优先级来允许或拒绝请求。
来看看现有的kubeadm部署集群启用的鉴权策略:
![](https://mycloudn.wqyin.cn/20210203155452.png)
可以看到,默认启用了Node授权和RBAC授权模块。
### 鉴权结果
对于每一个请求的鉴权结果,有专门为其设计的健全结果描述结构体,如下:
`vendor/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go:148`
```go
type Decision int
const (
// 拒绝
DecisionDeny Decision = iota
// 允许,则鉴权流程视为成功,请求顺利进入
DecisionAllow
// 无操作,进入下一个鉴权模块,相当于pass
DecisionNoOpinion
)
```
### 鉴权接口方法
`vendor/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go:69`
```go
// Authorizer makes an authorization decision based on information gained by making
// zero or more calls to methods of the Attributes interface. It returns nil when an action is
// authorized, otherwise it returns an error.
type Authorizer interface {
Authorize(a Attributes) (authorized Decision, reason string, err error)
}
```
所有的鉴权模块(鉴权器)都要实现这个`Authorize`方法,返回鉴权结果。
### 规则解析器
规则解析器可以根据认证之后所得到的用户信息,获取该用户对应的资源对象的操作权限。
```go
// RuleResolver provides a mechanism for resolving the list of rules that apply to a given user within a namespace.
type RuleResolver interface {
// RulesFor get the list of cluster wide rules, the list of rules in the specific namespace, incomplete status and errors.
RulesFor(user user.Info, namespace string) ([]ResourceRuleInfo, []NonResourceRuleInfo, bool, error)
}
```
以这里的返回值类型`ResourceRuleInfo`为例,默认的`DefaultResourceRuleInfo`结构体是这样的:
`vendor/k8s.io/apiserver/pkg/authorization/authorizer/rule.go:31`
```go
// DefaultResourceRuleInfo holds information that describes a rule for the resource
type DefaultResourceRuleInfo struct {
Verbs []string
APIGroups []string
Resources []string
ResourceNames []string
}
```
例如对pod资源的任意操作权限的描述可以描述为:
```go
DefaultResourceRuleInfo{
Verbs []string{"*"}
APIGroups []string{"*"}
Resources []string{"pod"}
}
```
这个DefaultResourceRuleInfo对象描述的规则是,允许对所有api group 的pod资源进行的所有类型的操作,包括{"get", "list", "update", "patch","create", "delete", "watch", "deletecollection"}操作。
### 鉴权流程图
![](https://mycloudn.wqyin.cn/20210203180748.png)
## 鉴权器
`AlwaysAllow`和`AlwaysDeny`这两种鉴权器很少使用,就不看了,直接略过.
### ABAC鉴权器
#### 简介
基于属性的访问控制(Attribute-based access control - ABAC)定义了访问控制范例,其中通过使用将属性组合在一起的策略来向用户授予访问权限。
启用ABAC鉴权器需要额外增加一个`--authorization-policy-file=SOME_FILENAME`参数,指定一个json格式的文件预设鉴权策略,是一种静态的权限配置方式。json格式样例如下:
```json
// 授予pod资源的任意操作权限给用户podManager
{
"apiVersion":"abac.authorization.kubernetes.io/v1beta1",
"kind":"Policy",
"spec":{
"user":"podManager",
"namespace":"*",
"resource":"pods",
"readonly":true
}
}
```
#### 代码实现
`pkg/auth/authorizer/abac/abac.go:224`
```go
// Authorizer implements authorizer.Authorize
func (pl policyList) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
for _, p := range pl {
if matches(*p, a) {
return authorizer.DecisionAllow, "", nil
}
}
return authorizer.DecisionNoOpinion, "No policy matched.", nil
}
```
--> `pkg/auth/authorizer/abac/abac.go:117`
```go
func matches(p abac.Policy, a authorizer.Attributes) bool {
if subjectMatches(p, a.GetUser()) {
// 操作类型与规则匹配
if verbMatches(p, a) {
// Resource and non-resource requests are mutually exclusive, at most one will match a policy
// 资源类型与规则匹配(包含namespace/APIGroup/Resource)
if resourceMatches(p, a) {
return true
}
// 针对非资源对象的操作匹配(请求路径匹配)
if nonResourceMatches(p, a) {
return true
}
}
}
return false
}
```
### RBAC鉴权器
#### 简介
基于角色(Role)的访问控制(RBAC)是一种基于组织中用户的角色来调节控制对 计算机或网络资源的访问的方法。
RBAC 鉴权机制使用 `rbac.authorization.k8s.io` [API 组](https://kubernetes.io/zh/docs/concepts/overview/kubernetes-api/#api-groups) 来驱动鉴权决定,允许你通过 Kubernetes API 动态配置策略。
通过创建`Role` 或` ClusterRole`来描述具体的资源授权策略,再通过创建RoleBinding/ClusterRoleBinding将策略绑定到用户/群组/服务上。
RBAC模式的详细描述和使用样例请参考我之前的文章:
[k8s(十四)、RBAC权限控制](https://wqyin.cn/2018/12/06/k8s(%E5%8D%81%E5%9B%9B)%E3%80%81RBAC%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6.html)
#### 代码实现
`plugin/pkg/auth/authorizer/rbac/rbac.go:74`
```go
func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (authorizer.Decision, string, error) {
ruleCheckingVisitor := &authorizingVisitor{requestAttributes: requestAttributes}
// 规则解析器解析请求的属性,返回鉴权结果,判断匹配用的是ruleCheckingVisitor.visit方法
r.authorizationRuleResolver.VisitRulesFor(requestAttributes.GetUser(), requestAttributes.GetNamespace(), ruleCheckingVisitor.visit)
if ruleCheckingVisitor.allowed {
return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil
}
...
reason := ""
if len(ruleCheckingVisitor.errors) > 0 {
reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors))
}
return authorizer.DecisionNoOpinion, reason, nil
}
```
对比规则和请求属性,返回true or false的visit方法:
`plugin/pkg/auth/authorizer/rbac/rbac.go:62`
```go
func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
if rule != nil && RuleAllows(v.requestAttributes, rule) {
// 请求与规则匹配,则鉴权成功
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
// 返回false是为了提前break鉴权过程
return false
}
if err != nil {
v.errors = append(v.errors, err)
}
return true
}
```
获取规则并调用visit方法的是`VisitRulesFor`接口方法,找一下`VisitRulesFor`方法:
`pkg/registry/rbac/validation/rule.go:178`
```go
func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) {
// 先拿到所有的ClusterRoleBinding对象,ClusterRoleBinding资源是cluster级别的,不区分命名空间
if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil {
if !visitor(nil, nil, err) {
return
}
} else {
sourceDescriber := &clusterRoleBindingDescriber{}
for _, clusterRoleBinding := range clusterRoleBindings {
// clusterRoleBinding.Subjects指定的绑定的用户对象,对比请求的所属用户,不匹配则continue
subjectIndex, applies := appliesTo(user, clusterRoleBinding.Subjects, "")
if !applies {
continue
}
rules, err := r.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
if err != nil {
if !visitor(nil, nil, err) {
return
}
continue
}
sourceDescriber.binding = clusterRoleBinding
sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
for i := range rules {
// visit方法返回false是代表鉴权成功了,提前break鉴权过程
if !visitor(sourceDescriber, &rules[i], nil) {
return
}
}
}
}
// 如果指定了namespace,再取命名空间级别的roleBinding资源对象,重复一次上面的过程
if len(namespace) > 0 {
if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil {
if !visitor(nil, nil, err) {
return
}
} else {
sourceDescriber := &roleBindingDescriber{}
for _, roleBinding := range roleBindings {
subjectIndex, applies := appliesTo(user, roleBinding.Subjects, namespace)
if !applies {
continue
}
rules, err := r.GetRoleReferenceRules(roleBinding.RoleRef, namespace)
if err != nil {
if !visitor(nil, nil, err) {
return
}
continue
}
sourceDescriber.binding = roleBinding
sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
return
}
}
}
}
}
}
```
#### 过程总结
参考代码片中的注释,rbac鉴权过程如下:
- 1.取到所有的clusterRoleBinding/roleBindings资源对象,遍历它们对比请求用户
- 2.对比roleBindings/clusterRoleBinding指向的用户(主体)与请求用户,相同则选中,不相同continue
- 3.对比规则与请求属性,符合则提前结束鉴权
### Node鉴权器
上面有提过,node鉴权器是专为kubelet组件设计的,按照kubeadm集群的默认配置,它是排序在第一位的鉴权器,为什么把它放在后面再讲呢,因为node鉴权器本质上也是利用了rbac鉴权器,是通过为`system:node`这个内置用户授权来实现的,来看一下。
#### 默认Node规则生成
`plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go:97`
```go
func NodeRules() []rbacv1.PolicyRule {
nodePolicyRules := []rbacv1.PolicyRule{
// Needed to check API access. These creates are non-mutating
rbacv1helpers.NewRule("create").Groups(authenticationGroup).Resources("tokenreviews").RuleOrDie(),
rbacv1helpers.NewRule("create").Groups(authorizationGroup).Resources("subjectaccessreviews", "localsubjectaccessreviews").RuleOrDie(),
// Needed to build serviceLister, to populate env vars for services
rbacv1helpers.NewRule(Read...).Groups(legacyGroup).Resources("services").RuleOrDie(),
// Nodes can register Node API objects and report status.
// Use the NodeRestriction admission plugin to limit a node to creating/updating its own API object.
rbacv1helpers.NewRule("create", "get", "list", "watch").Groups(legacyGroup).Resources("nodes").RuleOrDie(),
rbacv1helpers.NewRule("update", "patch").Groups(legacyGroup).Resources("nodes/status").RuleOrDie(),
rbacv1helpers.NewRule("update", "patch").Groups(legacyGroup).Resources("nodes").RuleOrDie(),
// TODO: restrict to the bound node as creator in the NodeRestrictions admission plugin
rbacv1helpers.NewRule("create", "update", "patch").Groups(legacyGroup).Resources("events").RuleOrDie(),
// TODO: restrict to pods scheduled on the bound node once field selectors are supported by list/watch authorization
rbacv1helpers.NewRule(Read...).Groups(legacyGroup).Resources("pods").RuleOrDie(),
// Needed for the node to create/delete mirror pods.
// Use the NodeRestriction admission plugin to limit a node to creating/deleting mirror pods bound to itself.
rbacv1helpers.NewRule("create", "delete").Groups(legacyGroup).Resources("pods").RuleOrDie(),
// Needed for the node to report status of pods it is running.
// Use the NodeRestriction admission plugin to limit a node to updating status of pods bound to itself.
rbacv1helpers.NewRule("update", "patch").Groups(legacyGroup).Resources("pods/status").RuleOrDie(),
// Needed for the node to create pod evictions.
// Use the NodeRestriction admission plugin to limit a node to creating evictions for pods bound to itself.
rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("pods/eviction").RuleOrDie(),
// Needed for imagepullsecrets, rbd/ceph and secret volumes, and secrets in envs
// Needed for configmap volume and envs
// Use the Node authorization mode to limit a node to get secrets/configmaps referenced by pods bound to itself.
rbacv1helpers.NewRule("get", "list", "watch").Groups(legacyGroup).Resources("secrets", "configmaps").RuleOrDie(),
// Needed for persistent volumes
// Use the Node authorization mode to limit a node to get pv/pvc objects referenced by pods bound to itself.
rbacv1helpers.NewRule("get").Groups(legacyGroup).Resources("persistentvolumeclaims", "persistentvolumes").RuleOrDie(),
// TODO: add to the Node authorizer and restrict to endpoints referenced by pods or PVs bound to the node
// Needed for glusterfs volumes
rbacv1helpers.NewRule("get").Groups(legacyGroup).Resources("endpoints").RuleOrDie(),
// Used to create a certificatesigningrequest for a node-specific client certificate, and watch
// for it to be signed. This allows the kubelet to rotate it's own certificate.
rbacv1helpers.NewRule("create", "get", "list", "watch").Groups(certificatesGroup).Resources("certificatesigningrequests").RuleOrDie(),
}
if utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
// Use the Node authorization mode to limit a node to update status of pvc objects referenced by pods bound to itself.
// Use the NodeRestriction admission plugin to limit a node to just update the status stanza.
pvcStatusPolicyRule := rbacv1helpers.NewRule("get", "update", "patch").Groups(legacyGroup).Resources("persistentvolumeclaims/status").RuleOrDie()
nodePolicyRules = append(nodePolicyRules, pvcStatusPolicyRule)
}
if utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
// Use the Node authorization to limit a node to create tokens for service accounts running on that node
// Use the NodeRestriction admission plugin to limit a node to create tokens bound to pods on that node
tokenRequestRule := rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("serviceaccounts/token").RuleOrDie()
nodePolicyRules = append(nodePolicyRules, tokenRequestRule)
}
// CSI
if utilfeature.DefaultFeatureGate.Enabled(features.CSIPersistentVolume) {
volAttachRule := rbacv1helpers.NewRule("get").Groups(storageGroup).Resources("volumeattachments").RuleOrDie()
nodePolicyRules = append(nodePolicyRules, volAttachRule)
if utilfeature.DefaultFeatureGate.Enabled(features.CSIDriverRegistry) {
csiDriverRule := rbacv1helpers.NewRule("get", "watch", "list").Groups("storage.k8s.io").Resources("csidrivers").RuleOrDie()
nodePolicyRules = append(nodePolicyRules, csiDriverRule)
}
}
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletPluginsWatcher) &&
utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) {
csiNodeInfoRule := rbacv1helpers.NewRule("get", "create", "update", "patch", "delete").Groups("storage.k8s.io").Resources("csinodes").RuleOrDie()
nodePolicyRules = append(nodePolicyRules, csiNodeInfoRule)
}
// Node leases
if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get", "create", "update", "patch", "delete").Groups("coordination.k8s.io").Resources("leases").RuleOrDie())
}
// RuntimeClass
if utilfeature.DefaultFeatureGate.Enabled(features.RuntimeClass) {
nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get", "list", "watch").Groups("node.k8s.io").Resources("runtimeclasses").RuleOrDie())
}
return nodePolicyRules
}
```
这里初始化了kubelet工作所需要的资源的权限,如(node/pod/cm/secret/pvc等)
#### Authorize代码实现
`plugin/pkg/auth/authorizer/node/node_authorizer.go:80`
```go
func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Decision, string, error) {
// 判断是不是node发起的请求(所属的group是不是system:node)
nodeName, isNode := r.identifier.NodeIdentity(attrs.GetUser())
if !isNode {
// reject requests from non-nodes
return authorizer.DecisionNoOpinion, "", nil
}
if len(nodeName) == 0 {
// reject requests from unidentifiable nodes
klog.V(2).Infof("NODE DENY: unknown node for user %q", attrs.GetUser().GetName())
return authorizer.DecisionNoOpinion, fmt.Sprintf("unknown node for user %q", attrs.GetUser().GetName()), nil
}
// subdivide access to specific resources
if attrs.IsResourceRequest() {
// 根据请求属性(路径)获取资源类型,不同类型资源不同的方式处理
requestResource := schema.GroupResource{Group: attrs.GetAPIGroup(), Resource: attrs.GetResource()}
switch requestResource {
case secretResource:
return r.authorizeReadNamespacedObject(nodeName, secretVertexType, attrs)
case configMapResource:
return r.authorizeReadNamespacedObject(nodeName, configMapVertexType, attrs)
case pvcResource:
if r.features.Enabled(features.ExpandPersistentVolumes) {
if attrs.GetSubresource() == "status" {
return r.authorizeStatusUpdate(nodeName, pvcVertexType, attrs)
}
}
return r.authorizeGet(nodeName, pvcVertexType, attrs)
case pvResource:
return r.authorizeGet(nodeName, pvVertexType, attrs)
case vaResource:
if r.features.Enabled(features.CSIPersistentVolume) {
return r.authorizeGet(nodeName, vaVertexType, attrs)
}
return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.CSIPersistentVolume), nil
case svcAcctResource:
if r.features.Enabled(features.TokenRequest) {
return r.authorizeCreateToken(nodeName, serviceAccountVertexType, attrs)
}
return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.TokenRequest), nil
case leaseResource:
if r.features.Enabled(features.NodeLease) {
return r.authorizeLease(nodeName, attrs)
}
return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.NodeLease), nil
case csiNodeResource:
if r.features.Enabled(features.KubeletPluginsWatcher) && r.features.Enabled(features.CSINodeInfo) {
return r.authorizeCSINode(nodeName, attrs)
}
return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gates %s and %s", features.KubeletPluginsWatcher, features.CSINodeInfo), nil
}
}
// Access to other resources is not subdivided, so just evaluate against the statically defined node rules
if rbac.RulesAllow(attrs, r.nodeRules...) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionNoOpinion, "", nil
}
```
### WebHook鉴权器
#### 简介
与上一篇中的WebHook认证器类似,WebHook鉴权器也是依赖于集群外部的鉴权服务器,将鉴权请求POST发送给外部的鉴权服务器。
`Webhook` 模式需要一个 HTTP 配置文件,通过 `--authorization-webhook-config-file=SOME_FILENAME` 的参数声明。
配置文件的格式使用 [kubeconfig](https://kubernetes.io/zh/docs/tasks/access-application-cluster/configure-access-multiple-clusters/)。在文件中,"users" 代表着 API 服务器的 webhook,而 "cluster" 代表着远程服务。
使用 HTTPS 客户端认证的配置例子:
```yaml
# Kubernetes API 版本
apiVersion: v1
# API 对象种类
kind: Config
# clusters 代表远程服务。
clusters:
- name: name-of-remote-authz-service
cluster:
# 对远程服务进行身份认证的 CA。
certificate-authority: /path/to/ca.pem
# 远程服务的查询 URL。必须使用 'https'。
server: https://authz.example.com/authorize
# users 代表 API 服务器的 webhook 配置
users:
- name: name-of-api-server
user:
client-certificate: /path/to/cert.pem # webhook plugin 使用 cert
client-key: /path/to/key.pem # cert 所对应的 key
# kubeconfig 文件必须有 context。需要提供一个给 API 服务器。
current-context: webhook
contexts:
- context:
cluster: name-of-remote-authz-service
user: name-of-api-server
name: webhook
```
> 摘自官方文档[Webhook 模式](https://kubernetes.io/zh/docs/reference/access-authn-authz/webhook/)
#### 代码实现
`vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go:152`
```go
func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
r := &authorization.SubjectAccessReview{}
if user := attr.GetUser(); user != nil {
r.Spec = authorization.SubjectAccessReviewSpec{
User: user.GetName(),
UID: user.GetUID(),
Groups: user.GetGroups(),
Extra: convertToSARExtra(user.GetExtra()),
}
}
if attr.IsResourceRequest() {
r.Spec.ResourceAttributes = &authorization.ResourceAttributes{
Namespace: attr.GetNamespace(),
Verb: attr.GetVerb(),
Group: attr.GetAPIGroup(),
Version: attr.GetAPIVersion(),
Resource: attr.GetResource(),
Subresource: attr.GetSubresource(),
Name: attr.GetName(),
}
} else {
r.Spec.NonResourceAttributes = &authorization.NonResourceAttributes{
Path: attr.GetPath(),
Verb: attr.GetVerb(),
}
}
// 将请求的主体/资源/操作等字段放在一个json里
key, err := json.Marshal(r.Spec)
if err != nil {
return w.decisionOnError, "", err
}
// 从本地的缓存里取,有则不发起远端post请求了
if entry, ok := w.responseCache.Get(string(key)); ok {
r.Status = entry.(authorization.SubjectAccessReviewStatus)
} else {
var (
result *authorization.SubjectAccessReview
err error
)
webhook.WithExponentialBackoff(w.initialBackoff, func() error {
// 缓存里没有,则发起post请求给远端鉴权服务器
result, err = w.subjectAccessReview.Create(r)
return err
})
if err != nil {
// An error here indicates bad configuration or an outage. Log for debugging.
klog.Errorf("Failed to make webhook authorizer request: %v", err)
return w.decisionOnError, "", err
}
r.Status = result.Status
// 长度不超过10000则缓存结果
if shouldCache(attr) {
if r.Status.Allowed {
w.responseCache.Add(string(key), r.Status, w.authorizedTTL)
} else {
w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL)
}
}
}
switch {
// 根据远端鉴权服务器的响应状态,返回鉴权结果
case r.Status.Denied && r.Status.Allowed:
return authorizer.DecisionDeny, r.Status.Reason, fmt.Errorf("webhook subject access review returned both allow and deny response")
case r.Status.Denied:
return authorizer.DecisionDeny, r.Status.Reason, nil
case r.Status.Allowed:
return authorizer.DecisionAllow, r.Status.Reason, nil
default:
return authorizer.DecisionNoOpinion, r.Status.Reason, nil
}
}
```
## 总结
鉴权的流程与认证的流程大体类似,但也有所不同,例如认证器链的执行顺序是无序的,而鉴权器链的执行顺序是有序的(按参数指定的顺序)。另外鉴权器的数量没有认证器那么多,因此相对容易理解一些。

@ -1,2 +1,42 @@
# apiServer ## APIServer源码分段阅读导航
待补充---
- [开胃菜-基础结构信息](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P1-基础结构信息.md)
- [APIServer的启动流程](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P2-启动流程.md)
- [APIServer的认证机制](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P3-APIServer的认证机制.md)
- [APIServer的鉴权机制](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P4-APIServer的鉴权机制.md)
## 概述
APIServer提供了 k8s各类资源对象的CURD/watch、认证授权、准入控制等众多核心功能,在k8s中定位类似于大脑和心脏,它的功能包括:
- 提供了集群管理的REST API接口(包括资源CURD、认证授权、数据校验以及集群状态变更);
- 是所有模块的数据交互和通信的枢纽,各模块的运作都依赖于APIServer
- 提供丰富多样的集群安全管控机制
- 直连后端存储(Etcd),是唯一与存储后端直接通信的模块
如图所示,这是创建一个资源(Pod)实例过程中,控制层面所经过的调用过程:
![](https://mycloudn.wqyin.cn/20201126165154.png)
在这个过程中,控制层面的每个组件、每一步骤,都需要与APIServer交互。
因此,APIServer无疑是各模块中 **最复杂、定位最核心、涉及面最广、代码量最大 **的模块。
**To be honest,看这个模块真的是压力山大,不求深解,不钻牛角尖,目标是摸清大体结构~**

@ -34,7 +34,7 @@ watch依然比较麻烦,毕竟list获取的数据以及后续watch到的数据
#### 工作流程图 #### 工作流程图
![](http://mycloudn.kokoerp.com/20191218161137.png) ![](http://mycloudn.wqyin.cn/20191218161137.png)
#### 流程图组件解释 #### 流程图组件解释

@ -6,11 +6,11 @@
Controller部分的第一篇文章中,我们从cobra启动命令入口开始,进入到了多实例leader选举部分的代码,对leader选举流程做了详细地分析: Controller部分的第一篇文章中,我们从cobra启动命令入口开始,进入到了多实例leader选举部分的代码,对leader选举流程做了详细地分析:
[Controller-P1-多实例leader选举](https://blog.upweto.top/gitbooks/kubeSourceCodeNote/controller/Kubernetes源码学习-Controller-P1-多实例leader选举.html) [Controller-P1-多实例leader选举](https://wqyin.cn/gitbooks/kubeSourceCodeNote/controller/Kubernetes源码学习-Controller-P1-多实例leader选举.html)
接着在第二篇中,文字和图解简单描述了controller是如何结合client-go模块中的informer工作的,为本篇及后面的几篇作铺垫: 接着在第二篇中,文字和图解简单描述了controller是如何结合client-go模块中的informer工作的,为本篇及后面的几篇作铺垫:
[Controller-P2-Controller与informer](https://blog.upweto.top/gitbooks/kubeSourceCodeNote/controller/Kubernetes源码学习-Controller-P2-Controller与informer.html) [Controller-P2-Controller与informer](https://wqyin.cn/gitbooks/kubeSourceCodeNote/controller/Kubernetes源码学习-Controller-P2-Controller与informer.html)
那么本篇,就接着第一篇往下,继续看代码。 那么本篇,就接着第一篇往下,继续看代码。
@ -38,7 +38,7 @@ if err := StartControllers(controllerContext, saTokenControllerInitFunc, NewCont
==> `cmd/kube-controller-manager/app/controllermanager.go:343` ==> `cmd/kube-controller-manager/app/controllermanager.go:343`
<img src="http://mycloudn.kokoerp.com/20200127122642.png" style="zoom:80%;" /> <img src="http://mycloudn.wqyin.cn/20200127122642.png" style="zoom:80%;" />
可以看到,controller会对不同的资源,分别初始化相应的controller,包含我们常见的deployment、statefulset、endpoint、pvc等等资源,controller种类有多达30余个。因此,在controller整个章节中,不会对它们逐一分析,只会抽取几个常见有代表性地进行深入,本篇就来看看deployment controller吧。 可以看到,controller会对不同的资源,分别初始化相应的controller,包含我们常见的deployment、statefulset、endpoint、pvc等等资源,controller种类有多达30余个。因此,在controller整个章节中,不会对它们逐一分析,只会抽取几个常见有代表性地进行深入,本篇就来看看deployment controller吧。
@ -745,7 +745,7 @@ func (dc *DeploymentController) syncRolloutStatus(allRSs []*apps.ReplicaSet, new
DeploymentCondition在这里面反复出现,便于理解,参照一个正常状态的deployment condition样例: DeploymentCondition在这里面反复出现,便于理解,参照一个正常状态的deployment condition样例:
<img src="http://mycloudn.kokoerp.com/20200130125357.png" style="zoom:50%;" /> <img src="http://mycloudn.wqyin.cn/20200130125357.png" style="zoom:50%;" />
#### 总结 #### 总结

@ -49,7 +49,7 @@ for {
kube-controller是一个控制组件,根据我们的使用经验,有多种经常使用的资源,都不是实际地直接进行任务计算的资源类型,而在申明之后由k8s自动发现并保证以达成申明语义状态的逻辑资源,例如deployment、statefulSet、pvc、endpoint等,这些资源都分别由对应的controller子组件,那么这样的controller子组件有多少呢?如下图: kube-controller是一个控制组件,根据我们的使用经验,有多种经常使用的资源,都不是实际地直接进行任务计算的资源类型,而在申明之后由k8s自动发现并保证以达成申明语义状态的逻辑资源,例如deployment、statefulSet、pvc、endpoint等,这些资源都分别由对应的controller子组件,那么这样的controller子组件有多少呢?如下图:
![](http://mycloudn.kokoerp.com/20191206111312.jpg) ![](http://mycloudn.wqyin.cn/20191206111312.jpg)
可见controller的组件数量是非常之多的,因此在本部分中计划只抽选其中的deploymentController和statefulSetController这两种常见的对pod管理类型资源对应的controller来进行源码分析。 可见controller的组件数量是非常之多的,因此在本部分中计划只抽选其中的deploymentController和statefulSetController这两种常见的对pod管理类型资源对应的controller来进行源码分析。
@ -59,11 +59,11 @@ kube-controller是一个控制组件,根据我们的使用经验,有多种
`kubernetes/src/k8s.io/kubernetes/cmd/kube-controller-manager/controller-manager.go` `kubernetes/src/k8s.io/kubernetes/cmd/kube-controller-manager/controller-manager.go`
![](http://mycloudn.kokoerp.com/20191206153026.jpg) ![](http://mycloudn.wqyin.cn/20191206153026.jpg)
**功能模块** **功能模块**
`kubernetes/src/k8s.io/kubernetes/pkg/controller` `kubernetes/src/k8s.io/kubernetes/pkg/controller`
![](http://mycloudn.kokoerp.com/20191206154051.jpg) ![](http://mycloudn.wqyin.cn/20191206154051.jpg)

@ -0,0 +1,816 @@
## 前言
在大致分析过k8s的Scheduler、Controller、APIServer三个控制平面组件后,本篇开始进入数据交互平面的daemon组件kubelet部分,看看kubelet是如何在控制平面和数据平面中以承上启下的模式工作的。
## 启动流程
启动入口照旧,位于项目的cmd路径下,使用cobra做cmd封装:
`cmd/kubelet/kubelet.go:39`
```go
func main() {
rand.Seed(time.Now().UnixNano())
command := app.NewKubeletCommand(server.SetupSignalHandler())
logs.InitLogs()
defer logs.FlushLogs()
if err := command.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
```
`cmd/kubelet/app/server.go:112`
`NewKubeletFlags`和`NewKubeletConfiguration`方法会初始化kubelet的很多默认flag和参数,来分别看下:
`cmd/kubelet/app/options/options.go:214`
```go
func NewKubeletFlags() *KubeletFlags {
remoteRuntimeEndpoint := ""
if runtime.GOOS == "linux" {
remoteRuntimeEndpoint = "unix:///var/run/dockershim.sock"
} else if runtime.GOOS == "windows" {
remoteRuntimeEndpoint = "npipe:////./pipe/dockershim"
}
return &KubeletFlags{
EnableServer: true,
// 容器运行时这个参数需要留意下
ContainerRuntimeOptions: *NewContainerRuntimeOptions(),
CertDirectory: "/var/lib/kubelet/pki",
RootDirectory: defaultRootDir,
MasterServiceNamespace: metav1.NamespaceDefault,
MaxContainerCount: -1,
MaxPerPodContainerCount: 1,
MinimumGCAge: metav1.Duration{Duration: 0},
NonMasqueradeCIDR: "10.0.0.0/8",
RegisterSchedulable: true,
ExperimentalKernelMemcgNotification: false,
RemoteRuntimeEndpoint: remoteRuntimeEndpoint,
NodeLabels: make(map[string]string),
VolumePluginDir: "/usr/libexec/kubernetes/kubelet-plugins/volume/exec/",
RegisterNode: true,
SeccompProfileRoot: filepath.Join(defaultRootDir, "seccomp"),
HostNetworkSources: []string{kubetypes.AllSource},
HostPIDSources: []string{kubetypes.AllSource},
HostIPCSources: []string{kubetypes.AllSource},
// TODO(#58010:v1.13.0): Remove --allow-privileged, it is deprecated
AllowPrivileged: true,
// prior to the introduction of this flag, there was a hardcoded cap of 50 images
NodeStatusMaxImages: 50,
}
}
```
ContainerRuntimeOptions有必要看看:
`cmd/kubelet/app/options/container_runtime.go:41`
```go
func NewContainerRuntimeOptions() *config.ContainerRuntimeOptions {
dockerEndpoint := ""
if runtime.GOOS != "windows" {
// 默认的容器驱动是docker
dockerEndpoint = "unix:///var/run/docker.sock"
}
return &config.ContainerRuntimeOptions{
ContainerRuntime: kubetypes.DockerContainerRuntime,
RedirectContainerStreaming: false,
DockerEndpoint: dockerEndpoint,
// dockershim路径,dockershim是容器运行中的实际载体,每个docker容器都会产生一个shim进程
DockershimRootDirectory: "/var/lib/dockershim",
// pause容器
PodSandboxImage: defaultPodSandboxImage,
ImagePullProgressDeadline: metav1.Duration{Duration: 1 * time.Minute},
ExperimentalDockershim: false,
// 这个目录下都是网络相关功能工具的执行文件
CNIBinDir: "/opt/cni/bin",
// 这里是cni的配置文件,如pod网段、网关、bridge等,一般由cni动态生成
CNIConfDir: "/etc/cni/net.d",
}
}
```
`cmd/kubelet/app/options/options.go:293`
--> `cmd/kubelet/app/options/options.go:311`
```go
// `NewKubeletConfiguration`方法则会默认设置一些参数
func applyLegacyDefaults(kc *kubeletconfig.KubeletConfiguration) {
// --anonymous-auth
kc.Authentication.Anonymous.Enabled = true
// --authentication-token-webhook
kc.Authentication.Webhook.Enabled = false
// --authorization-mode
// apiserver认证篇提到的针对node设计的AlwaysAllow认证模式
kc.Authorization.Mode = kubeletconfig.KubeletAuthorizationModeAlwaysAllow
// 10255采集信息的接口,如prometheus采集cadvisor的metrics
kc.ReadOnlyPort = ports.KubeletReadOnlyPort
}
```
再来看看Run方法里面做了哪些操作:
--> `cmd/kubelet/app/server.go:148`
```go
Run: func(cmd *cobra.Command, args []string) {
...
// 上面百来行代码都是默认的init flag和config相关处理,例如featureGates等,略过
// 加载kubelet配置文件,展开进去看可以看到即是--config参数对应指定的文件,一般kubeadm部署时使用的是/var/lib/kubelet/config.yaml
if configFile := kubeletFlags.KubeletConfigFile; len(configFile) > 0 {
kubeletConfig, err = loadConfigFile(configFile)
if err != nil {
klog.Fatal(err)
}
...
}
// 实例化KubeletServer
kubeletServer := &options.KubeletServer{
KubeletFlags: *kubeletFlags,
KubeletConfiguration: *kubeletConfig,
}
// 构建一些kubelet的依赖插件,例如nsenter,连接dockershim的client端
kubeletDeps, err := UnsecuredDependencies(kubeletServer)
if err != nil {
klog.Fatal(err)
}
// add the kubelet config controller to kubeletDeps
kubeletDeps.KubeletConfigController = kubeletConfigController
// start the experimental docker shim, if enabled
if kubeletServer.KubeletFlags.ExperimentalDockershim {
if err := RunDockershim(&kubeletServer.KubeletFlags, kubeletConfig, stopCh); err != nil {
klog.Fatal(err)
}
return
}
// 启动kubelet
klog.V(5).Infof("KubeletConfiguration: %#v", kubeletServer.KubeletConfiguration)
if err := Run(kubeletServer, kubeletDeps, stopCh); err != nil {
klog.Fatal(err)
}
},
}
// 下面是一些cmd help信息,省略
...
return cmd
```
--> `cmd/kubelet/app/server.go:416`
--> `cmd/kubelet/app/server.go:479` 这个函数代码段很长,两百多行,挑主要片段
```go
func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan struct{}) (err error) {
...
// 独立模式指的是不与外部(如apiserver)交互的模式,一般在调试中使用,所以独立模式不需要起client
standaloneMode := true
if len(s.KubeConfig) > 0 {
standaloneMode = false
}
if kubeDeps == nil {
kubeDeps, err = UnsecuredDependencies(s)
if err != nil {
return err
}
}
// 取得注册node名
hostName, err := nodeutil.GetHostname(s.HostnameOverride)
if err != nil {
return err
}
nodeName, err := getNodeName(kubeDeps.Cloud, hostName)
if err != nil {
return err
}
switch {
// 独立模式,则所有client设为nil
case standaloneMode:
kubeDeps.KubeClient = nil
kubeDeps.EventClient = nil
kubeDeps.HeartbeatClient = nil
klog.Warningf("standalone mode, no API client")
// 正常模式,则初始化client,包括kubeClient/eventClient/heartBeatClient
case kubeDeps.KubeClient == nil, kubeDeps.EventClient == nil, kubeDeps.HeartbeatClient == nil:
// client的配置,主要是连接apiserver的cert相关的配置,cert文件默认放在/var/lib/kubelet/pki下,如果开启了循环续期证书,则相应的异步进程会从cert manager循环检测和更新证书。其他的配置诸如超时时间,长连接时间等。closeAllConns接收的是一个方法,用来断开连接。
clientConfig, closeAllConns, err := buildKubeletClientConfig(s, nodeName)
if err != nil {
return err
}
if closeAllConns == nil {
return errors.New("closeAllConns must be a valid function other than nil")
}
kubeDeps.OnHeartbeatFailure = closeAllConns
// 构建一个client-go里的clientset实例,访问各个GV和GVR对象使用
kubeDeps.KubeClient, err = clientset.NewForConfig(clientConfig)
if err != nil {
return fmt.Errorf("failed to initialize kubelet client: %v", err)
}
// event事件使用独立的client,与上面的访问GVR使用的client区分开
eventClientConfig := *clientConfig
eventClientConfig.QPS = float32(s.EventRecordQPS)
eventClientConfig.Burst = int(s.EventBurst)
kubeDeps.EventClient, err = v1core.NewForConfig(&eventClientConfig)
if err != nil {
return fmt.Errorf("failed to initialize kubelet event client: %v", err)
}
// 再开启一个心跳检测的client
heartbeatClientConfig := *clientConfig
heartbeatClientConfig.Timeout = s.KubeletConfiguration.NodeStatusUpdateFrequency.Duration
// 如果开启了NodeLease(node定期向apiserver汇报运行状态),那么心跳间隔最大不超过NodeLease duration
if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
leaseTimeout := time.Duration(s.KubeletConfiguration.NodeLeaseDurationSeconds) * time.Second
if heartbeatClientConfig.Timeout > leaseTimeout {
heartbeatClientConfig.Timeout = leaseTimeout
}
}
// 心跳1次/s
heartbeatClientConfig.QPS = float32(-1)
kubeDeps.HeartbeatClient, err = clientset.NewForConfig(&heartbeatClientConfig)
if err != nil {
return fmt.Errorf("failed to initialize kubelet heartbeat client: %v", err)
}
}
// 向apiserver发起认证建立会话
if kubeDeps.Auth == nil {
auth, err := BuildAuth(nodeName, kubeDeps.KubeClient, s.KubeletConfiguration)
if err != nil {
return err
}
kubeDeps.Auth = auth
}
// 填充cadvisor接口
if kubeDeps.CAdvisorInterface == nil {
imageFsInfoProvider := cadvisor.NewImageFsInfoProvider(s.ContainerRuntime, s.RemoteRuntimeEndpoint)
kubeDeps.CAdvisorInterface, err = cadvisor.New(imageFsInfoProvider, s.RootDirectory, cadvisor.UsingLegacyCadvisorStats(s.ContainerRuntime, s.RemoteRuntimeEndpoint))
if err != nil {
return err
}
}
// Setup event recorder if required.
makeEventRecorder(kubeDeps, nodeName)
if kubeDeps.ContainerManager == nil {
if s.CgroupsPerQOS && s.CgroupRoot == "" {
klog.Info("--cgroups-per-qos enabled, but --cgroup-root was not specified. defaulting to /")
s.CgroupRoot = "/"
}
// /var/lib/kubelt/config.yaml里可以指定,为系统和kube组件指定不同的cgroup,为它们预留资源
// kubeReserved即为kube组件指定cgroup预留的资源
kubeReserved, err := parseResourceList(s.KubeReserved)
if err != nil {
return err
}
// kubeReserved即为宿主机系统进程指定cgroup预留的资源
systemReserved, err := parseResourceList(s.SystemReserved)
if err != nil {
return err
}
// 硬驱逐容器的资源阈值
var hardEvictionThresholds []evictionapi.Threshold
// If the user requested to ignore eviction thresholds, then do not set valid values for hardEvictionThresholds here.
if !s.ExperimentalNodeAllocatableIgnoreEvictionThreshold {
hardEvictionThresholds, err = eviction.ParseThresholdConfig([]string{}, s.EvictionHard, nil, nil, nil)
if err != nil {
return err
}
}
experimentalQOSReserved, err := cm.ParseQOSReserved(s.QOSReserved)
if err != nil {
return err
}
devicePluginEnabled := utilfeature.DefaultFeatureGate.Enabled(features.DevicePlugins)
// 上面的参数汇集起来,初始化容器管理器
kubeDeps.ContainerManager, err = cm.NewContainerManager(
kubeDeps.Mounter,
kubeDeps.CAdvisorInterface,
cm.NodeConfig{
RuntimeCgroupsName: s.RuntimeCgroups,
SystemCgroupsName: s.SystemCgroups,
KubeletCgroupsName: s.KubeletCgroups,
ContainerRuntime: s.ContainerRuntime,
CgroupsPerQOS: s.CgroupsPerQOS,
CgroupRoot: s.CgroupRoot,
CgroupDriver: s.CgroupDriver,
KubeletRootDir: s.RootDirectory,
ProtectKernelDefaults: s.ProtectKernelDefaults,
NodeAllocatableConfig: cm.NodeAllocatableConfig{
KubeReservedCgroupName: s.KubeReservedCgroup,
SystemReservedCgroupName: s.SystemReservedCgroup,
EnforceNodeAllocatable: sets.NewString(s.EnforceNodeAllocatable...),
KubeReserved: kubeReserved,
SystemReserved: systemReserved,
HardEvictionThresholds: hardEvictionThresholds,
},
QOSReserved: *experimentalQOSReserved,
ExperimentalCPUManagerPolicy: s.CPUManagerPolicy,
ExperimentalCPUManagerReconcilePeriod: s.CPUManagerReconcilePeriod.Duration,
ExperimentalPodPidsLimit: s.PodPidsLimit,
EnforceCPULimits: s.CPUCFSQuota,
CPUCFSQuotaPeriod: s.CPUCFSQuotaPeriod.Duration,
},
s.FailSwapOn,
devicePluginEnabled,
kubeDeps.Recorder)
if err != nil {
return err
}
}
if err := checkPermissions(); err != nil {
klog.Error(err)
}
utilruntime.ReallyCrash = s.ReallyCrashForTesting
rand.Seed(time.Now().UnixNano())
// oom判定器给当前进程设置oom分数,容器内存资源管控的手段就是使用的oom,这里待会儿拎出来单独分析
oomAdjuster := kubeDeps.OOMAdjuster
if err := oomAdjuster.ApplyOOMScoreAdj(0, int(s.OOMScoreAdj)); err != nil {
klog.Warning(err)
}
// RunKubelet接往下文
if err := RunKubelet(s, kubeDeps, s.RunOnce); err != nil {
return err
}
// 起一个健康检查的http服务
if s.HealthzPort > 0 {
healthz.DefaultHealthz()
go wait.Until(func() {
err := http.ListenAndServe(net.JoinHostPort(s.HealthzBindAddress, strconv.Itoa(int(s.HealthzPort))), nil)
if err != nil {
klog.Errorf("Starting health server failed: %v", err)
}
}, 5*time.Second, wait.NeverStop)
}
if s.RunOnce {
return nil
}
// If systemd is used, notify it that we have started
go daemon.SdNotify(false, "READY=1")
select {
case <-done:
break
case <-stopCh:
break
}
return nil
}
```
### OOMAdjuster
--> `pkg/util/oom/oom.go:22`上面提到的oom判定器,这里分析一下,这个结构体有三个方法:
```go
// 这里目前用的还是结构体,看todo描述是后面要改成interface
// TODO: make this an interface, and inject a mock ioutil struct for testing.
type OOMAdjuster struct {
pidLister func(cgroupName string) ([]int, error)
ApplyOOMScoreAdj func(pid int, oomScoreAdj int) error
ApplyOOMScoreAdjContainer func(cgroupName string, oomScoreAdj, maxTries int) error
}
```
--> `pkg/util/oom/oom_linux.go:35`实现方法
```go
func NewOOMAdjuster() *OOMAdjuster {
oomAdjuster := &OOMAdjuster{
pidLister: getPids,
ApplyOOMScoreAdj: applyOOMScoreAdj,
}
oomAdjuster.ApplyOOMScoreAdjContainer = oomAdjuster.applyOOMScoreAdjContainer
return oomAdjuster
}
// 获取cgroup下所有进程的pid
func getPids(cgroupName string) ([]int, error) {
return cmutil.GetPids(filepath.Join("/", cgroupName))
}
// 修改oom分数,在linux下即是修改/proc/<pid>/oom_score_adj对应的值,当内存紧张时由linux系统的oom机制去杀掉oom score最高的进程,默认情况下是使用内存越多的进程oom score越高越容易被kill,applyOOMScoreAdj函数就是用来修改oom score的。
// Writes 'value' to /proc/<pid>/oom_score_adj. PID = 0 means self
// Returns os.ErrNotExist if the `pid` does not exist.
func applyOOMScoreAdj(pid int, oomScoreAdj int) error {
if pid < 0 {
return fmt.Errorf("invalid PID %d specified for oom_score_adj", pid)
}
var pidStr string
if pid == 0 {
pidStr = "self"
} else {
pidStr = strconv.Itoa(pid)
}
maxTries := 2
oomScoreAdjPath := path.Join("/proc", pidStr, "oom_score_adj")
value := strconv.Itoa(oomScoreAdj)
klog.V(4).Infof("attempting to set %q to %q", oomScoreAdjPath, value)
var err error
for i := 0; i < maxTries; i++ {
err = ioutil.WriteFile(oomScoreAdjPath, []byte(value), 0700)
if err != nil {
if os.IsNotExist(err) {
klog.V(2).Infof("%q does not exist", oomScoreAdjPath)
return os.ErrNotExist
}
klog.V(3).Info(err)
time.Sleep(100 * time.Millisecond)
continue
}
return nil
}
if err != nil {
klog.V(2).Infof("failed to set %q to %q: %v", oomScoreAdjPath, value, err)
}
return err
}
// 修改整个容器的oom评分,即修改某个cgroup下所有进程的评分,getPids取得所有pid遍历执行applyOOMScoreAdj
// Writes 'value' to /proc/<pid>/oom_score_adj for all processes in cgroup cgroupName.
// Keeps trying to write until the process list of the cgroup stabilizes, or until maxTries tries.
func (oomAdjuster *OOMAdjuster) applyOOMScoreAdjContainer(cgroupName string, oomScoreAdj, maxTries int) error {
adjustedProcessSet := make(map[int]bool)
for i := 0; i < maxTries; i++ {
continueAdjusting := false
pidList, err := oomAdjuster.pidLister(cgroupName)
if err != nil {
if os.IsNotExist(err) {
// Nothing to do since the container doesn't exist anymore.
return os.ErrNotExist
}
continueAdjusting = true
klog.V(10).Infof("Error getting process list for cgroup %s: %+v", cgroupName, err)
} else if len(pidList) == 0 {
klog.V(10).Infof("Pid list is empty")
continueAdjusting = true
} else {
for _, pid := range pidList {
if !adjustedProcessSet[pid] {
klog.V(10).Infof("pid %d needs to be set", pid)
if err = oomAdjuster.ApplyOOMScoreAdj(pid, oomScoreAdj); err == nil {
adjustedProcessSet[pid] = true
} else if err == os.ErrNotExist {
continue
} else {
klog.V(10).Infof("cannot adjust oom score for pid %d - %v", pid, err)
continueAdjusting = true
}
// Processes can come and go while we try to apply oom score adjust value. So ignore errors here.
}
}
}
if !continueAdjusting {
return nil
}
// There's a slight race. A process might have forked just before we write its OOM score adjust.
// The fork might copy the parent process's old OOM score, then this function might execute and
// update the parent's OOM score, but the forked process id might not be reflected in cgroup.procs
// for a short amount of time. So this function might return without changing the forked process's
// OOM score. Very unlikely race, so ignoring this for now.
}
return fmt.Errorf("exceeded maxTries, some processes might not have desired OOM score")
}
```
### RunKubelet
--> `cmd/kubelet/app/server.go:955` 回到主线
```go
func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencies, runOnce bool) error {
...
// 这里的几个source都是"*",意为接收api/file/http来源的pod更新
hostNetworkSources, err := kubetypes.GetValidatedSources(kubeServer.HostNetworkSources)
if err != nil {
return err
}
hostPIDSources, err := kubetypes.GetValidatedSources(kubeServer.HostPIDSources)
if err != nil {
return err
}
hostIPCSources, err := kubetypes.GetValidatedSources(kubeServer.HostIPCSources)
if err != nil {
return err
}
privilegedSources := capabilities.PrivilegedSources{
HostNetworkSources: hostNetworkSources,
HostPIDSources: hostPIDSources,
HostIPCSources: hostIPCSources,
}
capabilities.Setup(kubeServer.AllowPrivileged, privilegedSources, 0)
credentialprovider.SetPreferredDockercfgPath(kubeServer.RootDirectory)
klog.V(2).Infof("Using root directory: %v", kubeServer.RootDirectory)
if kubeDeps.OSInterface == nil {
kubeDeps.OSInterface = kubecontainer.RealOS{}
}
// kubelet初始化,这个函数比较复杂,下面拎出来分析
k, err := createAndInitKubelet(&kubeServer.KubeletConfiguration,
kubeDeps,
&kubeServer.ContainerRuntimeOptions,
kubeServer.ContainerRuntime,
kubeServer.RuntimeCgroups,
kubeServer.HostnameOverride,
kubeServer.NodeIP,
kubeServer.ProviderID,
kubeServer.CloudProvider,
kubeServer.CertDirectory,
kubeServer.RootDirectory,
kubeServer.RegisterNode,
kubeServer.RegisterWithTaints,
kubeServer.AllowedUnsafeSysctls,
kubeServer.RemoteRuntimeEndpoint,
kubeServer.RemoteImageEndpoint,
kubeServer.ExperimentalMounterPath,
kubeServer.ExperimentalKernelMemcgNotification,
kubeServer.ExperimentalCheckNodeCapabilitiesBeforeMount,
kubeServer.ExperimentalNodeAllocatableIgnoreEvictionThreshold,
kubeServer.MinimumGCAge,
kubeServer.MaxPerPodContainerCount,
kubeServer.MaxContainerCount,
kubeServer.MasterServiceNamespace,
kubeServer.RegisterSchedulable,
kubeServer.NonMasqueradeCIDR,
kubeServer.KeepTerminatedPodVolumes,
kubeServer.NodeLabels,
kubeServer.SeccompProfileRoot,
kubeServer.BootstrapCheckpointPath,
kubeServer.NodeStatusMaxImages)
if err != nil {
return fmt.Errorf("failed to create kubelet: %v", err)
}
// NewMainKubelet should have set up a pod source config if one didn't exist
// when the builder was run. This is just a precaution.
if kubeDeps.PodConfig == nil {
return fmt.Errorf("failed to create kubelet, pod source config was nil")
}
podCfg := kubeDeps.PodConfig
rlimit.RlimitNumFiles(uint64(kubeServer.MaxOpenFiles))
// 只运行一次处理完pod就退出
if runOnce {
if _, err := k.RunOnce(podCfg.Updates()); err != nil {
return fmt.Errorf("runonce failed: %v", err)
}
klog.Info("Started kubelet as runonce")
} else {
// 正常运行
startKubelet(k, podCfg, &kubeServer.KubeletConfiguration, kubeDeps, kubeServer.EnableServer)
klog.Info("Started kubelet")
}
return nil
}
```
### createAndInitKubelet
--> `cmd/kubelet/app/server.go:1078`,这里主要走到NewMainKubelet函数:
--> `pkg/kubelet/kubelet.go:326` God!这个函数简直了,500+行代码...挑重要的说一下吧
```go
func NewMainKubelet(...) (...) {
...
// 加载service informer
serviceIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
if kubeDeps.KubeClient != nil {
serviceLW := cache.NewListWatchFromClient(kubeDeps.KubeClient.CoreV1().RESTClient(), "services", metav1.NamespaceAll, fields.Everything())
r := cache.NewReflector(serviceLW, &v1.Service{}, serviceIndexer, 0)
go r.Run(wait.NeverStop)
}
serviceLister := corelisters.NewServiceLister(serviceIndexer)
// 加载node informer
nodeIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
if kubeDeps.KubeClient != nil {
fieldSelector := fields.Set{api.ObjectNameField: string(nodeName)}.AsSelector()
nodeLW := cache.NewListWatchFromClient(kubeDeps.KubeClient.CoreV1().RESTClient(), "nodes", metav1.NamespaceAll, fieldSelector)
r := cache.NewReflector(nodeLW, &v1.Node{}, nodeIndexer, 0)
go r.Run(wait.NeverStop)
}
nodeInfo := &predicates.CachedNodeInfo{NodeLister: corelisters.NewNodeLister(nodeIndexer)}
...
// secretManager和configMapManager初始化,因为这两者被使用都是需要往容器内挂载目录的,需要kubelet来参与
var secretManager secret.Manager
var configMapManager configmap.Manager
switch kubeCfg.ConfigMapAndSecretChangeDetectionStrategy {
case kubeletconfiginternal.WatchChangeDetectionStrategy:
secretManager = secret.NewWatchingSecretManager(kubeDeps.KubeClient)
configMapManager = configmap.NewWatchingConfigMapManager(kubeDeps.KubeClient)
case kubeletconfiginternal.TTLCacheChangeDetectionStrategy:
secretManager = secret.NewCachingSecretManager(
kubeDeps.KubeClient, manager.GetObjectTTLFromNodeFunc(klet.GetNode))
configMapManager = configmap.NewCachingConfigMapManager(
kubeDeps.KubeClient, manager.GetObjectTTLFromNodeFunc(klet.GetNode))
case kubeletconfiginternal.GetChangeDetectionStrategy:
secretManager = secret.NewSimpleSecretManager(kubeDeps.KubeClient)
configMapManager = configmap.NewSimpleConfigMapManager(kubeDeps.KubeClient)
default:
return nil, fmt.Errorf("unknown configmap and secret manager mode: %v", kubeCfg.ConfigMapAndSecretChangeDetectionStrategy)
}
klet.secretManager = secretManager
klet.configMapManager = configMapManager
// 初始化存活探针管理器
klet.livenessManager = proberesults.NewManager()
//专为dockershim开辟的网络插件集
pluginSettings := dockershim.NetworkPluginSettings{
HairpinMode: kubeletconfiginternal.HairpinMode(kubeCfg.HairpinMode),
NonMasqueradeCIDR: nonMasqueradeCIDR,
PluginName: crOptions.NetworkPluginName,
PluginConfDir: crOptions.CNIConfDir, // 默认在/etc/cni/net.d/下存放cni配置文件
PluginBinDirString: crOptions.CNIBinDir, // 默认在/opt/cni/bin/下存放cni二进制文件,如bridge/tuning/vlan/dhcp/macvlan等等
MTU: int(crOptions.NetworkPluginMTU), // 网卡mtu
}
// kubelet相关运行时初始化
runtime, err := kuberuntime.NewKubeGenericRuntimeManager(
kubecontainer.FilterEventRecorder(kubeDeps.Recorder),
klet.livenessManager,
seccompProfileRoot,
containerRefManager,
machineInfo,
klet,
kubeDeps.OSInterface,
klet,
httpClient,
imageBackOff,
kubeCfg.SerializeImagePulls,
float32(kubeCfg.RegistryPullQPS),
int(kubeCfg.RegistryBurst),
kubeCfg.CPUCFSQuota,
kubeCfg.CPUCFSQuotaPeriod,
runtimeService,
imageService,
kubeDeps.ContainerManager.InternalContainerLifecycle(),
legacyLogProvider,
klet.runtimeClassManager,
)
if err != nil {
return nil, err
}
klet.containerRuntime = runtime
klet.streamingRuntime = runtime
klet.runner = runtime
runtimeCache, err := kubecontainer.NewRuntimeCache(klet.containerRuntime)
if err != nil {
return nil, err
}
klet.runtimeCache = runtimeCache
// pleg初始化(Pod Lifecycle Event Generator)
klet.pleg = pleg.NewGenericPLEG(klet.containerRuntime, plegChannelCapacity, plegRelistPeriod, klet.podCache, clock.RealClock{})
// pod workQueue初始化
klet.workQueue = queue.NewBasicWorkQueue(klet.clock)
// pod worker初始化,worker从workQueue中取队首,根据指令对pod进行相应的直接操作,另外还有更新pod cache的操作
klet.podWorkers = newPodWorkers(klet.syncPod, kubeDeps.Recorder, klet.workQueue, klet.resyncInterval, backOffPeriod, klet.podCache)
klet.backOff = flowcontrol.NewBackOff(backOffPeriod, MaxContainerBackOff)
klet.podKillingCh = make(chan *kubecontainer.PodPair, podKillingChannelCapacity)
// 初始化驱逐管理器
evictionManager, evictionAdmitHandler := eviction.NewManager(klet.resourceAnalyzer, evictionConfig, killPodNow(klet.podWorkers, kubeDeps.Recorder), klet.podManager.GetMirrorPodByPod, klet.imageManager, klet.containerGC, kubeDeps.Recorder, nodeRef, klet.clock)
klet.evictionManager = evictionManager
klet.admitHandlers.AddPodAdmitHandler(evictionAdmitHandler)
...
}
```
再次回到主线,进入最后的k.Run()函数循环逻辑:
--> `cmd/kubelet/app/server.go:1058`
```go
func startKubelet(k kubelet.Bootstrap, podCfg *config.PodConfig, kubeCfg *kubeletconfiginternal.KubeletConfiguration, kubeDeps *kubelet.Dependencies, enableServer bool) {
// start the kubelet
// 循环执行kubelet的工作逻辑k.Run()方法
go wait.Until(func() {
k.Run(podCfg.Updates())
}, 0, wait.NeverStop)
// start the kubelet server
// 提供诸如/metrics /health等api
if enableServer {
go k.ListenAndServe(net.ParseIP(kubeCfg.Address), uint(kubeCfg.Port), kubeDeps.TLSOptions, kubeDeps.Auth, kubeCfg.EnableDebuggingHandlers, kubeCfg.EnableContentionProfiling)
}
// 配置查询api
if kubeCfg.ReadOnlyPort > 0 {
go k.ListenAndServeReadOnly(net.ParseIP(kubeCfg.Address), uint(kubeCfg.ReadOnlyPort))
}
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletPodResources) {
go k.ListenAndServePodResources()
}
}
```
wait.Until()循环执行函数前面的文章中已经分析过多次了,不再过多赘述,这里传参是period是0,说明是无间隔死循环调用k.Run()方法,体现在实际环境中kubelet运行时的表现就是:无论运行中遇到什么报错,kubelet都会持续工作。
来分析一下k.Run(podCfg.Updates())传的实参是什么:
--> `pkg/kubelet/config/config.go:105`
```
func (c *PodConfig) Updates() <-chan kubetypes.PodUpdate {
return c.updates
}
```
接着看`c.updates` --> `pkg/kubelet/config/config.go:58`
```go
type PodConfig struct {
pods *podStorage
mux *config.Mux
// the channel of denormalized changes passed to listeners
updates chan kubetypes.PodUpdate
// contains the list of all configured sources
sourcesLock sync.Mutex
sources sets.String
checkpointManager checkpointmanager.CheckpointManager
}
```
--> `pkg/kubelet/types/pod_update.go:80`
```go
type PodUpdate struct {
Pods []*v1.Pod
Op PodOperation
Source string
}
```
猜测是将pod的写(删查改)请求转换成结构体,放入chan中,然后由k.Run()方法来处理这些写请求,k.Run()的实现在这里``pkg/kubelet/kubelet.go:1382`,留作下回分析,本篇启动流程篇到此结束。
## 小结
kubelet的源码果真是相当的复杂,一个函数动辄数百行,也难怪,毕竟作为daemon端执行数据平面工作的它要承担着很多职责,先到这吧

@ -0,0 +1,22 @@
## Controller源码分段阅读导航
- [启动流程](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/kubelet/Kubernetes源码学习-Kubelet-P1-启动流程篇.md)
- (待补充)
## 概述
Kubelet作为k8s核心组件中的daemon端运行在集群中的每一个节点上,承接着控制平面的指令向数据平面传达。不像scheduler、controller组件只负责相对单一的功能,kubelet除了管理自身的运行时外,还需要和宿主系统(linux)、CRI、CNI、CSI等外部组件对接,无疑是一个复杂度很高的组件。
## kubelet的主要功能
- pod启停
- 容器网络管理
- Volume管理
- 探针检查
- 容器监控

3
package-lock.json generated

@ -1,3 +0,0 @@
{
"lockfileVersion": 1
}

@ -0,0 +1,17 @@
## 盘点k8s pkg中有趣的公共库
- [wait-定时(条件)轮询库](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/pkg/pkg-01-wait-定时\(条件\)轮询库.md)
- client-go库(待补充)
## 概述
在kubernetes整个项目中,除了几个主要组件外,在pkg/下有一些公共库也封装得非常精彩且有趣,上至client-go这类复杂度高的库,小至wait这类瑞士军刀般精巧的库。本系列将扒几个这样"有意思"的库来瞧瞧

@ -0,0 +1,332 @@
---
title: "Kubernetes源码-pkg-01-wait-条件定时器库"
date: 2021/11/21 22:59:53
tags:
- Kubernetes
- Golang
- 读源码
---
# Kubernetes源码-pkg-01-wait-定时(条件)轮询库
## 前言
在前面的主要组件分析过程中,有数次提及到的wait库让我记忆犹新,这是一个被高频引用的库,各个主要组件如scheduler、controller、kubelet等都常常使用wait库中的function轮询间隔(或条件)触发执行动作。整个wait库只有一个代码文件,代码行数不过400余行,本篇就来完整地分析一下这个库。
代码路径: `vendor/k8s.io/apimachinery/pkg/util/wait/wait.go`
## 分类
wait库内的各种function,大体来说都是以轮询的形式,根据时间间隔、条件判断,来确定工具执行函数是否应被继续执行。按代码中呈现,按触发形式再细化一下,各function则可以分为这几类:
| 条件类型 | 说明 |
| --------- | ------------------------------------------------------------ |
| Until类 | 用得最多的类型,一般以一条chan struct{} 或context Done接收done信号作为终止轮询的依据 |
| Backoff类 | 每间隔一定的时长执行一次回溯函数,一般情况下,间隔时长随着回溯次数递增而倍数级延长,但间隔时长也会有上限值 |
| poll类 | 两条channel,一条用作传递单次执行信号用来轮询,一条用作传递done信号 |
### Until类
Untile类型有两个具体实现,分别是Until和UntilWithContext
#### Until函数
`vendor/k8s.io/apimachinery/pkg/util/wait/wait.go:87`
```go
func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
JitterUntil(f, period, 0.0, true, stopCh)
}
```
调用的是JitterUntil函数
```go
func JitterUntil(f func(), period time.Duration, jitterFactor float64, sliding bool, stopCh <-chan struct{}) {
var t *time.Timer
var sawTimeout bool
for {
// f()执行前先判断一次stopCh是否有信号,执行完之后也还要执行一次,说明见下方
select {
case <-stopCh:
return
default:
}
jitteredPeriod := period
if jitterFactor > 0.0 {
// 如果加入抖动因子随机数,则要把间隔周期延长至原周期的随机倍数
jitteredPeriod = Jitter(period, jitterFactor)
}
// sliding = false,则把f()的运行时间计入间隔周期内
if !sliding {
t = resetOrReuseTimer(t, jitteredPeriod, sawTimeout)
}
func() {
defer runtime.HandleCrash()
f()
}()
// sliding = true,则f()执行完成后继续等待间隔周期再进入下一次循环
if sliding {
t = resetOrReuseTimer(t, jitteredPeriod, sawTimeout)
}
// select下的各case分支权重相等随机分配,因此为了避免在stopCh有停止信号时却select到了t.C信号分支上,从而导致进入下一个逻辑循环超额执行了一次f()函数的情况,在代码段开头就要判断一次stopCh是否有信号
select {
case <-stopCh:
return
case <-t.C:
sawTimeout = true
}
}
}
```
```go
func Jitter(duration time.Duration, maxFactor float64) time.Duration {
if maxFactor <= 0.0 {
maxFactor = 1.0
}
// 在抖动因子范围内选择随机数,放大间隔周期的倍数
wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration))
return wait
}
```
JitterUntil函数可谓是把条件考虑得很细致,参数上有执行周期、抖动因子、窗口期(是否包含函数执行时间),另外在stopCh信号处理上也做到了预防超期执行,JitterUntil函数已经足以应对各类以时间间隔维度的轮询场景了。
#### UntilWithContext
`vendor/k8s.io/apimachinery/pkg/util/wait/wait.go:96`
```go
func UntilWithContext(ctx context.Context, f func(context.Context), period time.Duration) {
JitterUntilWithContext(ctx, f, period, 0.0, true)
}
```
```go
func JitterUntilWithContext(ctx context.Context, f func(context.Context), period time.Duration, jitterFactor float64, sliding bool) {
JitterUntil(func() { f(ctx) }, period, jitterFactor, sliding, ctx.Done())
}
```
可以看到,最后也是调用的JitterUntil,唯一的不同只是参数stopCh换成了包了一层的context.Done()
### Backoff类
#### Backoff结构体
Backoff类函数都有一个类型为Backoff结构体的形参,先来看看这个结构体的定义
`vendor/k8s.io/apimachinery/pkg/util/wait/wait.go:207`
```go
type Backoff struct {
// 初始设定的间隔
Duration time.Duration
// 间隔时间的倍数
Factor float64
// 抖动因子,抖动因子是在最后计算的
Jitter float64
// duration最大步进次数,下面还有个专门的Step()方法,依据此字段动态计算出每次轮询的duration
Steps int
// 最大duration上限(计算抖动因子之前)
Cap time.Duration
}
```
```go
func (b *Backoff) Step() time.Duration {
if b.Steps < 1 {
// 当Steps == 0时,duration不再变化
if b.Jitter > 0 {
return Jitter(b.Duration, b.Jitter)
}
return b.Duration
}
// Steps每循环一次会递减
b.Steps--
duration := b.Duration
// calculate the next step
if b.Factor != 0 {
// duration每次轮询随着倍数因子倍数级递增
b.Duration = time.Duration(float64(b.Duration) * b.Factor)
if b.Cap > 0 && b.Duration > b.Cap {
// 但duration最大也不会超过Cap
b.Duration = b.Cap
b.Steps = 0
}
}
// 在计算的最后一步使用抖动因子放大duration,再返回最终的duration
if b.Jitter > 0 {
duration = Jitter(duration, b.Jitter)
}
return duration
}
```
ok,下面来看看具体的Backoff类实现函数。
#### ExponentialBackoff函数
```go
func ExponentialBackoff(backoff Backoff, condition ConditionFunc) error {
for backoff.Steps > 0 {
if ok, err := condition(); err != nil || ok {
return err
}
if backoff.Steps == 1 {
break
}
time.Sleep(backoff.Step())
}
return ErrWaitTimeout
}
```
ExponentialBackoff函数的工作逻辑是:
- 在最外层限定了backoff函数的最多重复执行次数,即等于Steps字段值
- 过程中condition()条件函数执行异常或正常则直接返回,否则按最大限定次数,每次轮询等待时间参照Step()方法返回值进行
- 到达上限次数后若条件函数仍未返回结果,则返回超时错误
显然,此函数适用于失败重试的场景,常用k8s的同学一定不会陌生,我们常遇到pod多次失败重试的状态,如:CrashLoopBackOff/ImagePullBackOff状态,重试过程正是使用此函数封装的
### poll类
#### WaitFunc结构体
```go
type WaitFunc func(done <-chan struct{}) <-chan struct{}
```
这里定义的type WaitFunc下面都会用到,接收的参数chan用作done信号传递,返回的chan用作执行信号传递
#### Poll
`vendor/k8s.io/apimachinery/pkg/util/wait/wait.go:286`
```go
func Poll(interval, timeout time.Duration, condition ConditionFunc) error {
return pollInternal(poller(interval, timeout), condition)
}
```
调用的pollInternal函数,看命名就知道是间隔执行的意思:
```go
func pollInternal(wait WaitFunc, condition ConditionFunc) error {
done := make(chan struct{})
defer close(done)
return WaitFor(wait, condition, done)
}
```
-->
```go
func WaitFor(wait WaitFunc, fn ConditionFunc, done <-chan struct{}) error {
stopCh := make(chan struct{})
defer close(stopCh)
// type WaitFunc函数返回的c是传递执行信号的chan
c := wait(stopCh)
for {
select {
// c每取出一次数据,则执行一次条件函数fn()
case _, open := <-c:
ok, err := fn()
if err != nil {
return err
}
// fn()条件满足,则轮询结束返回nil
if ok {
return nil
}
if !open {
return ErrWaitTimeout
}
case <-done:
return ErrWaitTimeout
}
}
}
```
WaitFor函数可实现按条件结果来决定对执行函数的执行次数、结束时机的控制,实现更高的可控性,但与之对应的是,执行信号、结束信号的发送全部需要在type waitFunc内部实现。即上面的Poll()函数中的`poller(interval, timeout)`是`waitFunc`的实现,来看看:
```go
func poller(interval, timeout time.Duration) WaitFunc {
return WaitFunc(func(done <-chan struct{}) <-chan struct{} {
// 执行信号chan
ch := make(chan struct{})
go func() {
defer close(ch)
// new一个ticker
tick := time.NewTicker(interval)
defer tick.Stop()
// 默认无超时时间设定
var after <-chan time.Time
if timeout != 0 {
// 当超时时间>0时,设定超时判定计时器
timer := time.NewTimer(timeout)
after = timer.C
defer timer.Stop()
}
for {
select {
case <-tick.C:
// 达到interval tick时间后往ch插入一个执行信号
select {
case ch <- struct{}{}:
default:
}
case <-after:
return
case <-done:
return
}
}
}()
return ch
})
}
```
从pollInternal的实现上来看,看起来与Until差别不大,但在WaitFunc的实现层面,除了像poller()函数这样以定时器为条件外,也可以取其他更加灵活的条件作为判定往执行chan发送信号的依据。

BIN
scheduler/.DS_Store vendored

Binary file not shown.

@ -56,8 +56,8 @@ matebook-x-pro:local ywq$ go run main.go
# 因需要多次测试,这里所有的测试步骤就把build的步骤跳过,直接使用go run main.go进行测试 # 因需要多次测试,这里所有的测试步骤就把build的步骤跳过,直接使用go run main.go进行测试
``` ```
**我们打开IDE来查看一下testapp的代码结构:** **我们打开IDE来查看一下testapp的代码结构:**
![image](http://mycloudn.kokoerp.com/cobra1.jpg) ![image](http://mycloudn.wqyin.cn/cobra1.jpg)
![image](http://mycloudn.kokoerp.com/cobra2.jpg) ![image](http://mycloudn.wqyin.cn/cobra2.jpg)
``` ```
# 现在还未创建子命令,那么来创建几个试试: # 现在还未创建子命令,那么来创建几个试试:
@ -90,7 +90,7 @@ add called
``` ```
**来看看新增的子命令是怎么运行的呢?** **来看看新增的子命令是怎么运行的呢?**
![image](http://mycloudn.kokoerp.com/cobra3.jpg) ![image](http://mycloudn.wqyin.cn/cobra3.jpg)
截图圈中部分可以看出,子命令是在init()函数里为root级添加了一个子命令,先不去管底层实现,接着往下. 截图圈中部分可以看出,子命令是在init()函数里为root级添加了一个子命令,先不去管底层实现,接着往下.
**测试cobra的强大简洁的flag处理** **测试cobra的强大简洁的flag处理**
@ -101,7 +101,7 @@ deleteCmd.PersistentFlags().StringVar(&obj,"object", "", "A function to delete a
``` ```
在`Run:func()`匿名函数中添加一行输出: 在`Run:func()`匿名函数中添加一行输出:
`fmt.Println("delete obj:",cmd.Flag("object").Value)` `fmt.Println("delete obj:",cmd.Flag("object").Value)`
![image](http://mycloudn.kokoerp.com/cobra4.jpg) ![image](http://mycloudn.wqyin.cn/cobra4.jpg)
运行结果: 运行结果:
@ -112,7 +112,7 @@ delete obj: obj1
``` ```
如果觉得`--`flag符号太麻烦,cobra同样支持短符号`-`flag缩写: 如果觉得`--`flag符号太麻烦,cobra同样支持短符号`-`flag缩写:
![image](http://mycloudn.kokoerp.com/cobra5.jpg) ![image](http://mycloudn.wqyin.cn/cobra5.jpg)
运行结果: 运行结果:
@ -132,7 +132,7 @@ add.go delete.go get.go pods.go root.go
``` ```
可以发现,cmd/目录下多了一个pods.go文件,我们来看看它是怎么关联上delete父级命令的,同时为它添加一行输出: 可以发现,cmd/目录下多了一个pods.go文件,我们来看看它是怎么关联上delete父级命令的,同时为它添加一行输出:
![image](http://mycloudn.kokoerp.com/cobra6.jpg) ![image](http://mycloudn.wqyin.cn/cobra6.jpg)
执行命令: 执行命令:
``` ```
@ -146,13 +146,13 @@ delete pods: pod1
## 入口 ## 入口
通过对上方cobra的基本了解,我们不难知道,`cmd/kube-scheduler/scheduler.go`内的main()方法内部实际调用的是`cobra.Command.Run`内的匿名函数,我们可以进入`NewSchedulerCommand()`内部确认: 通过对上方cobra的基本了解,我们不难知道,`cmd/kube-scheduler/scheduler.go`内的main()方法内部实际调用的是`cobra.Command.Run`内的匿名函数,我们可以进入`NewSchedulerCommand()`内部确认:
![image](http://mycloudn.kokoerp.com/main1.jpg) ![image](http://mycloudn.wqyin.cn/main1.jpg)
可以看到,调用了`Run`内部`runCommand`方法,再来看看Run方法内部需要重点关注的几个点: 可以看到,调用了`Run`内部`runCommand`方法,再来看看Run方法内部需要重点关注的几个点:
![image](http://mycloudn.kokoerp.com/runCommand.jpg) ![image](http://mycloudn.wqyin.cn/runCommand.jpg)
其中,上方是对命令行的参数、选项校验的步骤,跳过,重点关注两个变量:`cc和stopCh`,这两个变量会作为最后调用`Run()`方法的参数,其中`stopCh`作用是作为主程序退出的信号通知其他各协程进行相关的退出操作的,另外一个cc变量非常重要,可以点击`c.Complete()`方法,查看该方法的详情: 其中,上方是对命令行的参数、选项校验的步骤,跳过,重点关注两个变量:`cc和stopCh`,这两个变量会作为最后调用`Run()`方法的参数,其中`stopCh`作用是作为主程序退出的信号通知其他各协程进行相关的退出操作的,另外一个cc变量非常重要,可以点击`c.Complete()`方法,查看该方法的详情:
![image](http://mycloudn.kokoerp.com/runCommand.jpg) ![image](http://mycloudn.wqyin.cn/runCommand.jpg)
`Complete()`方法本质上返回的是一个Config结构体,该结构体内部的元素非常丰富,篇幅有限就不一一点开截图了,大家可以自行深入查看这些元素的作用,这里简单概括一下其中几个: `Complete()`方法本质上返回的是一个Config结构体,该结构体内部的元素非常丰富,篇幅有限就不一一点开截图了,大家可以自行深入查看这些元素的作用,这里简单概括一下其中几个:
``` ```
@ -175,14 +175,14 @@ Recorder record.EventRecorder
Broadcaster record.EventBroadcaster Broadcaster record.EventBroadcaster
``` ```
这里层级非常深,不便展示,Config这一个结构体非常重要,可以认真读一读代码。回到`cmd/kube-scheduler/app/server.go`.`runCommand`这里来,接着往下,进入其最后return调用的`Run()`函数中,函数中的前部分都是启动scheduler相关的组件,如event broadcaster、informers、healthz server、metric server等,重点看图中红框圈出的`sched.Run()`,这才是scheduler主程序的调用运行函数: 这里层级非常深,不便展示,Config这一个结构体非常重要,可以认真读一读代码。回到`cmd/kube-scheduler/app/server.go`.`runCommand`这里来,接着往下,进入其最后return调用的`Run()`函数中,函数中的前部分都是启动scheduler相关的组件,如event broadcaster、informers、healthz server、metric server等,重点看图中红框圈出的`sched.Run()`,这才是scheduler主程序的调用运行函数:
![image](http://mycloudn.kokoerp.com/Run.jpg) ![image](http://mycloudn.wqyin.cn/Run.jpg)
进入`sched.Run()`: 进入`sched.Run()`:
![image](http://mycloudn.kokoerp.com/scheRun.jpg) ![image](http://mycloudn.wqyin.cn/scheRun.jpg)
`wait.Until`这个调用的逻辑是,直到收到stop信号才终止,在此之前循环运行`sched.scheduleOne`。代码走到这里,终于找到启动入口最内部的主体啦: `wait.Until`这个调用的逻辑是,直到收到stop信号才终止,在此之前循环运行`sched.scheduleOne`。代码走到这里,终于找到启动入口最内部的主体啦:
![](http://mycloudn.kokoerp.com/image-20190827163439895.png) ![](http://mycloudn.wqyin.cn/image-20190827163439895.png)
`sched.scheduleOne`这个函数有代码点长,整体的功能可以概括为: `sched.scheduleOne`这个函数有代码点长,整体的功能可以概括为:

@ -12,15 +12,15 @@
回顾上一篇篇末,我们找到了调度框架的实际调度工作逻辑的入口位置,`pkg/scheduler/scheduler.go:435`, `scheduleOne()`函数内部,定位在`pkg/scheduler/scheduler.go:457`位置,是通过这个`sched.schedule(pod)`方法来获取与pod匹配的node的,我们直接跳转2次,来到了这里`pkg/scheduler/core/generic_scheduler.go:107` 回顾上一篇篇末,我们找到了调度框架的实际调度工作逻辑的入口位置,`pkg/scheduler/scheduler.go:435`, `scheduleOne()`函数内部,定位在`pkg/scheduler/scheduler.go:457`位置,是通过这个`sched.schedule(pod)`方法来获取与pod匹配的node的,我们直接跳转2次,来到了这里`pkg/scheduler/core/generic_scheduler.go:107`
![](http://mycloudn.kokoerp.com/schedule.jpg) ![](http://mycloudn.wqyin.cn/schedule.jpg)
![](http://mycloudn.kokoerp.com/AlgSchedule.jpg) ![](http://mycloudn.wqyin.cn/AlgSchedule.jpg)
![](http://mycloudn.kokoerp.com/scheduleStruct.jpg) ![](http://mycloudn.wqyin.cn/scheduleStruct.jpg)
通过注释可以知道,ScheduleAlgorithm interface中的Schedule方法就是用来为pod筛选node的,但这是个接口方法,并不是实际调用的,我们稍微往下,在`pkg/scheduler/core/generic_scheduler.go:162`这个位置,就可以找到实际调用的Schedule方法: 通过注释可以知道,ScheduleAlgorithm interface中的Schedule方法就是用来为pod筛选node的,但这是个接口方法,并不是实际调用的,我们稍微往下,在`pkg/scheduler/core/generic_scheduler.go:162`这个位置,就可以找到实际调用的Schedule方法:
![](http://mycloudn.kokoerp.com/genericSchedule.jpg) ![](http://mycloudn.wqyin.cn/genericSchedule.jpg)
这个函数里面有4个重要的步骤: 这个函数里面有4个重要的步骤:
@ -46,41 +46,41 @@ g.selectHost(priorityList)
先来逆向回溯代码结构,找到哪里创建了scheduler,调度器的默认初始化配置,默认的调度算法来源等等框架相关的东西。`Schedule()`方法属于`genericScheduler`结构体,先查看`genericScheduler`结构体,再选中结构体名称,crtl + b组合键查看它在哪些地方被引用,找出创建结构体的位置: 先来逆向回溯代码结构,找到哪里创建了scheduler,调度器的默认初始化配置,默认的调度算法来源等等框架相关的东西。`Schedule()`方法属于`genericScheduler`结构体,先查看`genericScheduler`结构体,再选中结构体名称,crtl + b组合键查看它在哪些地方被引用,找出创建结构体的位置:
![](http://mycloudn.kokoerp.com/createGenSche.jpg) ![](http://mycloudn.wqyin.cn/createGenSche.jpg)
通过缩略代码框,排除test相关的测试文件,很容易找出创建结构体的地方位于`pkg/scheduler/core/generic_scheduler.go:1189`,点击图中红框圈中位置,跳转过去,果然找到了`NewGenericScheduler()`方法,这个方法是用来创建一个`genericScheduler`对象的,那么我们再次crtl + b组合键查看`NewGenericScheduler`再什么地方被调用: 通过缩略代码框,排除test相关的测试文件,很容易找出创建结构体的地方位于`pkg/scheduler/core/generic_scheduler.go:1189`,点击图中红框圈中位置,跳转过去,果然找到了`NewGenericScheduler()`方法,这个方法是用来创建一个`genericScheduler`对象的,那么我们再次crtl + b组合键查看`NewGenericScheduler`再什么地方被调用:
![](http://mycloudn.kokoerp.com/newGenericScheduler.jpg) ![](http://mycloudn.wqyin.cn/newGenericScheduler.jpg)
找出了在`pkg/scheduler/factory/factory.go:441`这个位置上找到了调用入口,这里位于`CreateFromKeys()`方法中,继续crtl + b查看它的引用,跳转到`pkg/scheduler/factory/factory.go:336`这个位置: 找出了在`pkg/scheduler/factory/factory.go:441`这个位置上找到了调用入口,这里位于`CreateFromKeys()`方法中,继续crtl + b查看它的引用,跳转到`pkg/scheduler/factory/factory.go:336`这个位置:
![](http://mycloudn.kokoerp.com/newGenericScheduler.jpg) ![](http://mycloudn.wqyin.cn/newGenericScheduler.jpg)
![](http://mycloudn.kokoerp.com/createFromProvider.jpg) ![](http://mycloudn.wqyin.cn/createFromProvider.jpg)
![](http://mycloudn.kokoerp.com/getAlgorithmProvider.jpg) ![](http://mycloudn.wqyin.cn/getAlgorithmProvider.jpg)
这里找到了`algorithmProviderMap`这个变量,顾名思义,这个变量里面包含的应该就是调度算法的来源,点击进去查看,跳转到了`pkg/scheduler/factory/plugins.go:86`这个位置,组合键查看引用,一眼就可以看出哪个引用为这个map添加了元素: 这里找到了`algorithmProviderMap`这个变量,顾名思义,这个变量里面包含的应该就是调度算法的来源,点击进去查看,跳转到了`pkg/scheduler/factory/plugins.go:86`这个位置,组合键查看引用,一眼就可以看出哪个引用为这个map添加了元素:
![](http://mycloudn.kokoerp.com/addMapEle.jpg) ![](http://mycloudn.wqyin.cn/addMapEle.jpg)
跳转过去,来到了`pkg/scheduler/factory/plugins.go:391`这个位置,这个函数的作用是为scheduler的配置指定调度算法,即`FitPredicate、Priority`这两个算法需要用到的metric或者方法,再次请出组合键,查找哪个地方调用了这个方法: 跳转过去,来到了`pkg/scheduler/factory/plugins.go:391`这个位置,这个函数的作用是为scheduler的配置指定调度算法,即`FitPredicate、Priority`这两个算法需要用到的metric或者方法,再次请出组合键,查找哪个地方调用了这个方法:
![](http://mycloudn.kokoerp.com/registerAlgorithmProvider.jpg) ![](http://mycloudn.wqyin.cn/registerAlgorithmProvider.jpg)
来到了`pkg/scheduler/algorithmprovider/defaults/defaults.go:99`,继续组合键向上查找引用,这次引用只有一个,没有弹窗直接跳转过去了`pkg/scheduler/algorithmprovider/defaults/defaults.go:36`: 来到了`pkg/scheduler/algorithmprovider/defaults/defaults.go:99`,继续组合键向上查找引用,这次引用只有一个,没有弹窗直接跳转过去了`pkg/scheduler/algorithmprovider/defaults/defaults.go:36`:
![](http://mycloudn.kokoerp.com/registerAlgorithmProvider1.jpg) ![](http://mycloudn.wqyin.cn/registerAlgorithmProvider1.jpg)
![](http://mycloudn.kokoerp.com/init.jpg) ![](http://mycloudn.wqyin.cn/init.jpg)
我们来看看`defaultPredicates(), defaultPriorities()`这两个函数具体的内容: 我们来看看`defaultPredicates(), defaultPriorities()`这两个函数具体的内容:
![](http://mycloudn.kokoerp.com/default.jpg) ![](http://mycloudn.wqyin.cn/default.jpg)
我们随便点击进去一个`predicates`选项查看其内容: 我们随便点击进去一个`predicates`选项查看其内容:
![](http://mycloudn.kokoerp.com/memPressure.jpg) ![](http://mycloudn.wqyin.cn/memPressure.jpg)
`CheckNodeMemoryPressure`这个词相应熟悉kubernetes 应用的朋友一定不会陌生,例如在node内存压力大无法调度的pod时,`kubectl describe pod xxx`就会在状态信息里面看到这个关键词。 `CheckNodeMemoryPressure`这个词相应熟悉kubernetes 应用的朋友一定不会陌生,例如在node内存压力大无法调度的pod时,`kubectl describe pod xxx`就会在状态信息里面看到这个关键词。
@ -103,7 +103,7 @@ func RegisterAlgorithmProvider(name string, predicateKeys, priorityKeys sets.Str
可以看到,这个方法为DefaultProvider绑定了配置:筛选算法和优先级排序算法的key集合,这些key只是字符串,那么是怎么具体落实到计算的方法过程上去的呢?让我们看看`pkg/scheduler/algorithmprovider/defaults/`目录下的`register_predicates.go,register_priorities.go`这两个文件: 可以看到,这个方法为DefaultProvider绑定了配置:筛选算法和优先级排序算法的key集合,这些key只是字符串,那么是怎么具体落实到计算的方法过程上去的呢?让我们看看`pkg/scheduler/algorithmprovider/defaults/`目录下的`register_predicates.go,register_priorities.go`这两个文件:
![](http://mycloudn.kokoerp.com/preinit.jpg) ![](http://mycloudn.wqyin.cn/preinit.jpg)
它们同样也在init()函数中初始化时使用`factory.RegisterFitPredicate()`方法做了一些注册操作,这个方法的两个参数,前一个是筛选/计算优先级 的关键key名,后一个是具体计算的功能实现方法,点击`factory.RegisterFitPredicate()`方法,深入一级,查看内部代码, 它们同样也在init()函数中初始化时使用`factory.RegisterFitPredicate()`方法做了一些注册操作,这个方法的两个参数,前一个是筛选/计算优先级 的关键key名,后一个是具体计算的功能实现方法,点击`factory.RegisterFitPredicate()`方法,深入一级,查看内部代码,
@ -177,15 +177,15 @@ func (s *Scheme) AddTypeDefaultingFunc(srcType Object, fn func(interface{})) {
我们选中然后ctrl+b,查找AddTypeDefaultingFunc()的引用,弹窗中你可以看到有非常非常多的对象都引用了该方法,这些不同类型的对象相信无一例外都是通过Default()方法来生成默认配置的,我们找到其中的包含scheduler的方法: 我们选中然后ctrl+b,查找AddTypeDefaultingFunc()的引用,弹窗中你可以看到有非常非常多的对象都引用了该方法,这些不同类型的对象相信无一例外都是通过Default()方法来生成默认配置的,我们找到其中的包含scheduler的方法:
![](http://mycloudn.kokoerp.com/addDefaultFunc.jpg) ![](http://mycloudn.wqyin.cn/addDefaultFunc.jpg)
跳转进去,来到了这个位置`pkg/scheduler/apis/config/v1alpha1/zz_generated.defaults.go:31`(原谅我的灵魂笔法): 跳转进去,来到了这个位置`pkg/scheduler/apis/config/v1alpha1/zz_generated.defaults.go:31`(原谅我的灵魂笔法):
![](http://mycloudn.kokoerp.com/registerDefaults.jpg) ![](http://mycloudn.wqyin.cn/registerDefaults.jpg)
进入`SetDefaults_KubeSchedulerConfiguration()`,来到`pkg/scheduler/apis/config/v1alpha1/defaults.go:42`: 进入`SetDefaults_KubeSchedulerConfiguration()`,来到`pkg/scheduler/apis/config/v1alpha1/defaults.go:42`:
![](http://mycloudn.kokoerp.com/SetDefaults_KubeSchedulerConfiguration.jpg) ![](http://mycloudn.wqyin.cn/SetDefaults_KubeSchedulerConfiguration.jpg)
看到了`DefaultProvider`吗?是不是觉得瞬间豁然开朗,原来是在这里调用指定了scheduler配置的`AlgorithmSource.Provider`。 看到了`DefaultProvider`吗?是不是觉得瞬间豁然开朗,原来是在这里调用指定了scheduler配置的`AlgorithmSource.Provider`。
@ -276,7 +276,7 @@ func podFitsOnNode(
最后,对`pkg/scheduler`路径下的各子目录的功能来一个图文总结吧: 最后,对`pkg/scheduler`路径下的各子目录的功能来一个图文总结吧:
![](http://mycloudn.kokoerp.com/dir.jpg) ![](http://mycloudn.wqyin.cn/dir.jpg)

@ -145,7 +145,7 @@ ParallelizeUntil()的这种实现方式,可以很好地将并发实现和具
`pkg/scheduler/core/generic_scheduler.go:460 --> pkg/scheduler/internal/cache/node_tree.go:161` `pkg/scheduler/core/generic_scheduler.go:460 --> pkg/scheduler/internal/cache/node_tree.go:161`
![](http://mycloudn.kokoerp.com/zone.jpg) ![](http://mycloudn.wqyin.cn/zone.jpg)
可以看到,这里有一个zone的逻辑层级,这个层级仿佛没有见过,google了一番才了解了这个颇为冷门的功能:这是一个轻量级的支持集群联邦特性的实现,单个cluster可以属于多个zone,但这个功能目前只有GCE和AWS支持,且绝大多数的使用场景也用不到,可以说是颇为冷门。默认情况下,cluster只属于一个zone,可以理解为cluster和zone是同层级,因此后面见到有关zone相关的层级,我们直接越过它。有兴趣的朋友可以了解一下zone的概念: 可以看到,这里有一个zone的逻辑层级,这个层级仿佛没有见过,google了一番才了解了这个颇为冷门的功能:这是一个轻量级的支持集群联邦特性的实现,单个cluster可以属于多个zone,但这个功能目前只有GCE和AWS支持,且绝大多数的使用场景也用不到,可以说是颇为冷门。默认情况下,cluster只属于一个zone,可以理解为cluster和zone是同层级,因此后面见到有关zone相关的层级,我们直接越过它。有兴趣的朋友可以了解一下zone的概念:
@ -231,7 +231,7 @@ func podFitsOnNode(
有了以上理解,我们接着看代码,图中已注释: 有了以上理解,我们接着看代码,图中已注释:
![](http://mycloudn.kokoerp.com/podFitsOnNode.jpg) ![](http://mycloudn.wqyin.cn/podFitsOnNode.jpg)
图中`pkg/scheduler/core/generic_scheduler.go:608`位置正式开始了逐个计算筛选算法,那么筛选方法、筛选方法顺序在哪里呢?在上一篇[P2-框架篇]([https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/scheduler/P2-%E8%B0%83%E5%BA%A6%E5%99%A8%E6%A1%86%E6%9E%B6.md](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/scheduler/P2-调度器框架.md))中已经有讲过,默认调度算法都在`pkg/scheduler/algorithm/`路径下,我们接着往下看. 图中`pkg/scheduler/core/generic_scheduler.go:608`位置正式开始了逐个计算筛选算法,那么筛选方法、筛选方法顺序在哪里呢?在上一篇[P2-框架篇]([https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/scheduler/P2-%E8%B0%83%E5%BA%A6%E5%99%A8%E6%A1%86%E6%9E%B6.md](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/scheduler/P2-调度器框架.md))中已经有讲过,默认调度算法都在`pkg/scheduler/algorithm/`路径下,我们接着往下看.
@ -260,7 +260,7 @@ var (
[链接](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/scheduling/predicates-ordering.md) [链接](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/scheduling/predicates-ordering.md)
![](http://mycloudn.kokoerp.com/predicates.jpg) ![](http://mycloudn.wqyin.cn/predicates.jpg)
#### 筛选key #### 筛选key

@ -18,7 +18,7 @@ Tips: 本篇篇幅较长,因调度优选算法较为复杂,但请耐心结
同上一篇,回到`pkg/scheduler/core/generic_scheduler.go`中的`Schedule()`函数,`pkg/scheduler/core/generic_scheduler.go:184`: 同上一篇,回到`pkg/scheduler/core/generic_scheduler.go`中的`Schedule()`函数,`pkg/scheduler/core/generic_scheduler.go:184`:
![](http://mycloudn.kokoerp.com/20190822165920.png) ![](http://mycloudn.wqyin.cn/20190822165920.png)
截图中有几处标注,metric相关的几行,是收集metric信息,用以提供给prometheus使用的,kubernetes的几个核心组件都有这个功能,以后如果读prometheus的源码,这个单独拎出来再讲。直接进入优先级函数`PrioritizeNodes()`内部`pkg/scheduler/core/generic_scheduler.go:215` 截图中有几处标注,metric相关的几行,是收集metric信息,用以提供给prometheus使用的,kubernetes的几个核心组件都有这个功能,以后如果读prometheus的源码,这个单独拎出来再讲。直接进入优先级函数`PrioritizeNodes()`内部`pkg/scheduler/core/generic_scheduler.go:215`
@ -201,7 +201,7 @@ func defaultPriorities() sets.String {
在注册的每一个计算维度,都有专属的维度描述关键字,即factory方法的第一个参数(str类型)。不难发现,这里的每一个关键字,`pkg/scheduler/algorithm/priorities`目录内都有与其对应的文件,图中圈出了几个例子(灵魂画笔请原谅): 在注册的每一个计算维度,都有专属的维度描述关键字,即factory方法的第一个参数(str类型)。不难发现,这里的每一个关键字,`pkg/scheduler/algorithm/priorities`目录内都有与其对应的文件,图中圈出了几个例子(灵魂画笔请原谅):
![](http://mycloudn.kokoerp.com/image-20190821171031395.png) ![](http://mycloudn.wqyin.cn/image-20190821171031395.png)
显而易见,维度计算的内容就在这些文件中,可以自行通过编辑器的跳转功能逐级查看进行验证. 显而易见,维度计算的内容就在这些文件中,可以自行通过编辑器的跳转功能逐级查看进行验证.
@ -239,7 +239,7 @@ func CalculateNodePreferAvoidPodsPriorityMap(pod *v1.Pod, meta interface{}, node
这也印证了前面说的当前仅剩pod亲和性这一个维度在使用传统的Function,虽然已经被DEPRECATED掉了,传统的Function是直接计算出结果,Map-Reduce是将这个过程解耦拆成了两个步骤,且我们可以看到所有的factory函数,很多形参`reduceFunction`接收到的实参实际是是`nil`: 这也印证了前面说的当前仅剩pod亲和性这一个维度在使用传统的Function,虽然已经被DEPRECATED掉了,传统的Function是直接计算出结果,Map-Reduce是将这个过程解耦拆成了两个步骤,且我们可以看到所有的factory函数,很多形参`reduceFunction`接收到的实参实际是是`nil`:
![](http://mycloudn.kokoerp.com/image-20190822111624614.png) ![](http://mycloudn.wqyin.cn/image-20190822111624614.png)
这就说明这些维度的计算工作在map函数里面已经执行完成了,不需要再执行reduce函数了。因此,传统的Function的计算过程同样值得参考,那么首先就来看看`InterPodAffinityPriority`维度是怎么计算的吧! 这就说明这些维度的计算工作在map函数里面已经执行完成了,不需要再执行reduce函数了。因此,传统的Function的计算过程同样值得参考,那么首先就来看看`InterPodAffinityPriority`维度是怎么计算的吧!

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save