什么是声明式 API

k8s 在创建时,可以使用多种命令比如:

  1. 方法一:通过 create 命令创建
  2. $ kubectl create -f nginx.yaml
  3. 方法二:通过 apply 命令创建
  4. $ kubectl apply -f nginx.yaml
两个方法的在首次创建时没有区别,但是在更新信息时出现了不同,create 只能用于创建,无法做修改操作,一般情况下重新修改 yaml 文件后,使用如下命令替换:
$ kubectl replace -f nginx.yaml

而 apply 不同,可以继续使用 apply 操作修改后的 yaml 文件。 kubectl apply 本质其实时执行了一个对原有 API 对象的 PATCH 操作,一次能处理多个写操作,并且具备 merge 能力。说到这里,可以把 apply 理解成一次 git 的 merge 的过程,而 k8s 通过控制器调协达到新的 yaml 文件定义的预期状态,而不需要关心原始文件是什么。最终文件是什么,就实现达到 yaml 预期状态。
所以这就是声明式 API 的基本能力。

Istio

是基于 Kubernetes 项目的微服务治理框架,它在运行的每一个应用 Pod 里部署 Envoy 容器。因为 Pod 里所有容器都共享一个 Network Namespace,Envoy 容器通过修改 Pod 的 iptables 规则,实现 Pod 的整体流量控制。
这些各个场景下 Istio 也是通过控制 Envoy 来控制环境内的流量,这个过程对用户和应用是“无感”的。Istio 通过 Kubernetes 的一个重要功能,即 Dynamic Admission Control。在 K8S 项目中,当一个新建一个 Pod 时,会有一些初始化的操作,比如自动为所有 Pod 加上某些标签(Labels),就是通过 Admission Control 的功能。如果想要修改一些配置,就需要重新编译并重启 APIServer。这样很麻烦,所以 K8S 提供了一种“热插拔”式的 Admission 机制,就是 Dynamic Admission Control,也叫 Initializer。
Initializer 实际要做的就是在用户 Pod 提交给 K8S 之后,在它对应的 API 对象里自动加上 Envoy 容器的配置,使得原有配置多了预先准备好的 Envoy 配置。

# Envoy 配置以 ConfigMap 的方式保存在 Kubernetes 中。
apiVersion: v1
kind: ConfigMap
metadata:
  name: envoy-initializer
data:
  config: |
    containers:
      - name: envoy
        image: lyft/envoy:845747db88f102c0fd262ab234308e9e22f693a1
        command: ["/usr/local/bin/envoy"]
        args:
          - "--concurrency 4"
          - "--config-path /etc/envoy/envoy.json"
          - "--mode serve"
        ports:
          - containerPort: 80
            protocol: TCP
        resources:
          limits:
            cpu: "1000m"
            memory: "512Mi"
          requests:
            cpu: "100m"
            memory: "64Mi"
        volumeMounts:
          - name: envoy-conf
            mountPath: /etc/envoy
    volumes:
      - name: envoy-conf
        configMap:
          name: envoy

K8S 允许通过配置,指定对什么样的资源进行 Initilize 操作,比如下面的 yaml,表示对所有Pod 操作,且名字叫 envoy.initializer。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
  name: envoy-config
initializers:
  // 这个名字必须至少包括两个 "."
  - name: envoy.initializer.kubernetes.io
    rules:
      - apiGroups:
          - "" // 前面说过, ""就是core API Group的意思
        apiVersions:
          - v1
        resources:
          - pods

在提交之后,可以在 Pod 中看到 metadata 中 pengding 字段下有 envoy.initializer 名字,表示当前 Pod 执行了 initialize。执行完成后一定要把字段清除。

apiVersion: v1
kind: Pod
metadata:
  initializers:
    pending:
      - name: envoy.initializer.kubernetes.io
  name: myapp-pod
  labels:
    app: myapp
...

另外一种方法是通过在 Annotation 字段中声明

apiVersion: v1
kind: Pod
metadata
  annotations:
    "initializer.kubernetes.io/envoy": "true"
    ...

API 对象的奥秘

API 结构

一个 API 对象在 Etcd 里的完整资源路径,是由:Group(API 组)、Version(API 版本)和 Resource(API 资源类型)三个部分组成。下图是 API 结构图:

  • pod,Node 等核心 API 不需要 Group,直接在 /api 下面
  • 非核心 API 在 /apis 下面,Job 和 CronJob 在 batch(离线业务)下

image.png

API 注册过程

在匹配到正确 API 版本之后,k8s 就知道需要创建的资源对象,并开始走创建过程:

  1. 提交 YAML 文件到 APIServer,APIServer 进行过滤,并完成一些前置工作,比如授权、超时处理、审计等;
  2. 请求进入 MUX 和 Routes 流程;
  3. 根据资源对象的定义,使用用户提交的 YAML 文件里的字段,创建一个 CronJob 对象。APIServer 进行一个 Convert 工作,把用户提交的 YAML 文件转换成一个叫 Super Version 对象;
  4. APIServer 先后进行 Admission() 和 Validation() 操作。Validation 负责验证这个对象里的各个字段是否合法。
  5. APIServer 把验证过的 API 对象转换成用户最初提交的版本,进行序列化操作,并调用 Etcd 的 API 保存。

image.png

CRD

CRD 全称 Custom Resource Definition,顾名思义,它指的就是,允许用户在 K8S 中添加一个跟 Pod、Node 类似的,新的 API 资源类型,即:自定义 API 资源。
CRD 定义过程:

  1. 定义 CRD YAML 文件

    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
    name: networks.samplecrd.k8s.io
    spec:
    group: samplecrd.k8s.io # API 信息
    version: v1 # API 信息
    names:
     kind: Network  # 资源类型叫 Network
     plural: networks # 复数是 networks。
    scope: Namespaced # 定义这个 Network 是一个属于 Namespace 的对象
    
  2. 在 GOPATH 下创建项目

    $ tree $GOPATH/src/github.com/<your-name>/k8s-controller-custom-resource
    .
    ├── controller.go
    ├── crd
    │   └── network.yaml
    ├── example
    │   └── example-network.yaml
    ├── main.go
    └── pkg
     └── apis
         └── samplecrd
             ├── register.go # 存放全局变量
             └── v1
                 ├── doc.go # 文档,其他全局的代码生成控制左右,所以也叫 Global Tags
                 ├── types.go # 定义 YAML 可以引用的参数
                 └── register.go # 用于客户端识别资源类型的定义。
    

    register.go 文件内容: ```go package samplecrd

const ( GroupName = “samplecrd.k8s.io” Version = “v1” )

doc.go 文件内容:
```go
// +k8s:deepcopy-gen=package # 请为整个 v1 包里的所有类型定义自动生成 DeepCopy 方法;

// +groupName=samplecrd.k8s.io # 定义了这个包对应的 API 组的名字。
package v1

types.go 文件内容:

package v1
...
// +genclient # 请为下面这个 API 资源类型生成对应的 Client 代码
// +genclient:noStatus # 这个 API 资源类型定义里没有 Status 字段,如果不写这个字段,生成的 Client 就会自动带上 UpdateStatus 方法; 如果 struct 下定义了 Status 字段,也不需要这个字段了
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
# 请在生成 DeepCopy 的时候,实现 k8s 提供的 runtime.object 接口

// Network describes a Network resource
type Network struct {
 // TypeMeta is the metadata for the resource, like kind and apiversion
 metav1.TypeMeta `json:",inline"` # API 元数据
 // ObjectMeta contains the metadata for the particular object, including
 // things like...
 //  - name
 //  - namespace
 //  - self link
 //  - labels
 //  - ... etc ...
 metav1.ObjectMeta `json:"metadata,omitempty"` # 对象元数据

 Spec networkspec `json:"spec"` 
}
// networkspec is the spec for a Network resource
type networkspec struct {
 Cidr    string `json:"cidr"`
 Gateway string `json:"gateway"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// NetworkList is a list of Network resources
type NetworkList struct {
 metav1.TypeMeta `json:",inline"`
 metav1.ListMeta `json:"metadata"`

 Items []Network `json:"items"`
}
v1 下的 register.go 文件内容:
package v1
...
// addKnownTypes adds our types to the API scheme by registering
// Network and NetworkList
func addKnownTypes(scheme *runtime.Scheme) error {
 scheme.AddKnownTypes(
  SchemeGroupVersion,
  &Network{},
  &NetworkList{},
 )

 // register the type in the scheme
 metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
 return nil
}

定义完成后,可以使用 k8s 的代码生成器 k8s.io/code-generator。
操作参考链接 https://time.geekbang.org/column/article/41876?utm_source=u_nav_web&utm_medium=u_nav_web&utm_term=u_nav_web

自定义控制器

基于声明式 API 的业务功能实现,往往需要通过控制器模式来“监视” API 对象的变化,然后以此决定实际要执行的具体工作。
变细自定义控制器代码的过程包括:编写 main 函数、编写自定义控制器的定义、编写控制器里的业务控制逻辑三个部分。
main 函数的作用是定义并初始化一个自定义控制器,然后启动它,分三个步骤:

  1. main 函数更具提供的 Master 配置(APIServer 的地址端口和 Kubeconfig 的路径),创建 k8s 的 client 和 Network 对象的 client(基于前面创建 network CRD 来开始);

如果没提供 Master,则会使用 InClusterConfig 的方式来创建 Client;

  1. main 函数为 Network 对象创建一个叫作 InformerFactory 的工厂,并使用它生成一个 Network 对象的 Informer,传递给控制器;
  2. main 启动 Informer ,然后执行 controller.Run

详细过程分析

image.png

  1. 控制器从 Kubernetes 的 APISever 里获取 Network 对象,依靠 Informer(通知器)完成对象信息的监听;
  2. Informer 要和 APIServer 通信需要传递一个 networkClient,而 networkClient 是由 Reflector 维护。 Reflector 通过 ListAndWatch 的方法,来“获取”并“监听”这些 Network 对象实例的变化;Reflector 收到“事件通知”,会被放进 Delta FIFO Queue (增量先进先出队列)中;
  3. Informer 不断从 Delta FIFO Queue 里读取增量,每拿到一个增量,Informer 判断增量的事件类型,如果是 ADD 则通过 Indexer 这个库,保存到本地对象的缓存(Store);如果是 Delete 则删除;
  4. Informer 根据事件的类型,触发事先注册好的 ResourceEventHandler;
  5. Informer 的队列会被 WorkQueue 同步给 Control loop

ListAndWatch 方法的含义:通过 APIServer 的 LIST API “获取”所有最新版本的 API 对象;然后,通过 WATCH API 来 “监听”所有这些 API 对象的变化;
在 Informer 监听过程中, 每经过 resyncPeriod 指定的时间,Informer 维护的本地缓存会被强制更新一次。当 resync 动作触发后,informer 会先比较 Resourceversion ,如果没有变化则不对更新事件做进一步处理。

control loop

基本逻辑:

  • 等待 Informer 完成一次本地缓存的数据同步操作;
  • 直接通过 goroutine 启动一个或者多个“无限循环”的任务;

详细过程:

  • 从 WokreQueue 中获取一个 Key,出队动作;
  • syncHandler 方法使用这个 Key,尝试从 Informer 维护的缓存中拿到它所对应的 Network 对象;两个场景需要处理:
    • 获取失败,返回 IsNotFound 错误;意味着这个Key 是通过前面的“删除”事件添加进工作队列的,我就需要调用 Neutron API ,从真实集群中删除;
    • 如果能够获取到,则执行控制器比对“期望状态”和“实际状态”;实际状态也从 Neutron 获取实际集群。如果在这个过程中状态存在差异,就完成一次调协(Reconcile)的过程。