Kubernetes

k8s informer 介绍

informer 提供一个保持更新的 k8s 资源的本地缓存。

informer #

深入了解 Kubernetes CNI 网络插件 Flannel

关于 #

flannel 是由 CoreOS 开发的一个简单易用的容器网络插件

网络是 k8s 中至关重要的一部分, 这里以简单的 flannel 为例做深入分析

工作原理 #

以下介绍在 chart 方式部署的 flannel

flanneld 进程以 daemonset/kube-flannel-ds 方式运行在所有 node 上, 负责从提前配置好的网络池中分配子网租约 (subnet lease) 给 node.

flanneld 使用 k8s api 或者 etcd 存储网络配置、分配的子网和任何补充数据(如 node 的 public ip), 在 k8s 中使用一般不会单独提供 etcd 去存储这些数据.

  • 网络配置存储在 configmap 中, kube-flannel ns 下的 cm/kube-flannel-cfg 中
  • 分配的子网存储在 PodCIDR 中

几个名词解释:

  1. subnet: 对应 node.spec.podCIDR
  2. backend: 负责 node 之间 pod 通讯的后端。

flanneld 进程通过监听 node 资源来生成 subnet event, 然后在对应 backendhandleSubnetEvents 方法中处理逻辑,对于 vxlan backend 主要是按顺序设置 arp, fdbroute 来实现pod跨节点通讯。

...

Kube Scheduler

RKE2 自定义调度器配置

  1. 创建调度器配置文件

NodeResourcesFit 是一个调度插件, 检查节点是否拥有 Pod 请求的所有资源, 得分可以使用以下三种策略之一: LeastAllocated (默认)、MostAllocatedRequestedToCapacityRatio

实现了多个扩展点: preFilterfilterpreScorescore

我这里自定义使用 MostAllocated 策略, 优选分配比率较高的节点

# /etc/rancher/rke2/kube-scheduler-config.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /var/lib/rancher/rke2/server/cred/scheduler.kubeconfig
profiles:
  - schedulerName: default-scheduler
    pluginConfig:
      - name: NodeResourcesFit
        args:
          scoringStrategy:
            type: MostAllocated
            resources:
              - name: cpu
                weight: 1
              - name: memory
                weight: 1
  1. 修改 rke2 配置文件

修改 /etc/rancher/rke2/config.yaml

kube-scheduler-arg:
+ - config=/etc/rancher/rke2/kube-scheduler-config.yaml
  1. 重启 rke2-server

会重新生成 kube-scheduler 的 static pod manifest 文件 /var/lib/rancher/rke2/agent/pod-manifests/kube-scheduler.yaml

会挂载 /etc/rancher/rke2/kube-scheduler-config.yaml 文件到 pod 中

CRI 工作原理

关于 CRI #

CRI 全称为 Container Runtime Interface (容器运行时接口), 是 kubelet 与 容器运行时进行通讯的主要协议。

是 k8s 根据 OCI runtime-spec

kubelet-cri

cri-api 主要定义了六个接口:

staging/src/k8s.io/cri-api/
├── pkg
│   ├── apis
│   │   ├── runtime
│   │   │   └── v1
│   │   │       ├── api.pb.go    {RuntimeServiceClient RuntimeServiceServer ImageServiceClient ImageServiceServer}
│   │   │       ├── api.proto
│   │   │       └── constants.go
│   │   ├── services.go          {RuntimeService ImageManagerService}

CNI 工作原理

关于 CNI #

CNI 全称 Container Network Interface, 容器网络接口, cni 插件是可执行文件, 一般位于 /opt/cni/bin/ 目录

在 k8s 中, kubelet 调用 cri 创建 sandbox 时(RunPodSandbox)会先去创建 network namespace, 然后创建 pause 和 其他容器并将容器加入到同一个 network namespace 中

cni spec 文档: https://www.cni.dev/docs/spec/

有如下环境变量参数:

  • CNI_COMMAND: 对应操作 ADD, DEL, CHECK, or VERSION.
  • CNI_CONTAINERID: 容器 id
  • CNI_NETNS: 如 /var/run/netns/[nsname]
  • CNI_IFNAME: 要在容器中创建的接口名称, 一般容器中都是 eth0
  • CNI_ARGS: 额外的 kv 参数, 如 FOO=BAR;ABC=123
  • CNI_PATH: 搜索 cni plugin 可执行文件的目录

插件分析 #

bridge #

主要是 cmdAddcmdDel 两个函数, 对应 CNI spec 中的 ADDDEL 两个主要操作

...

CSI 工作原理

关于 CSI #

CSI 全称为 Container Storage Interface, 容器存储接口

要实现一个第三方的 csi driver 需要实现下面的 gRPC service csi spec

// 如果 NodeServer 和 ControllerServer 对应服务运行在不同 pod 中, 那么两个服务都要实现 IdentityServer
type IdentityServer interface {
    // 用来获取插件名称
    GetPluginInfo(context.Context, *GetPluginInfoRequest) (*GetPluginInfoResponse, error)
    GetPluginCapabilities(context.Context, *GetPluginCapabilitiesRequest) (*GetPluginCapabilitiesResponse, error)
    Probe(context.Context, *ProbeRequest) (*ProbeResponse, error)
    mustEmbedUnimplementedIdentityServer()
}

type ControllerServer interface {
    // 创建 volume, 如 ceph 创建一个 rbd 或者 hostpath 创建一个目录
    CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
    // 删除 volume, 如 ceph 删除一个 rbd 或者 hostpath 删除一个目录
    DeleteVolume(context.Context, *DeleteVolumeRequest) (*DeleteVolumeResponse, error)
    // 将 volume attach 到 node 上, 如 rbd 通过 rbd map 命令 attach, 成功后 node 上会多出一个 rbdx 的 block 设备
    ControllerPublishVolume(context.Context, *ControllerPublishVolumeRequest) (*ControllerPublishVolumeResponse, error)
    // 将 volume 从 node 上 detach, 如 rbd 通过 rbd unmap 命令 detach
    ControllerUnpublishVolume(context.Context, *ControllerUnpublishVolumeRequest) (*ControllerUnpublishVolumeResponse, error)
    ValidateVolumeCapabilities(context.Context, *ValidateVolumeCapabilitiesRequest) (*ValidateVolumeCapabilitiesResponse, error)
    // 列出所有 volume
    ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
    GetCapacity(context.Context, *GetCapacityRequest) (*GetCapacityResponse, error)
    ControllerGetCapabilities(context.Context, *ControllerGetCapabilitiesRequest) (*ControllerGetCapabilitiesResponse, error)
    CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error)
    DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*DeleteSnapshotResponse, error)
    ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error)
    ControllerExpandVolume(context.Context, *ControllerExpandVolumeRequest) (*ControllerExpandVolumeResponse, error)
    ControllerGetVolume(context.Context, *ControllerGetVolumeRequest) (*ControllerGetVolumeResponse, error)
    ControllerModifyVolume(context.Context, *ControllerModifyVolumeRequest) (*ControllerModifyVolumeResponse, error)
    mustEmbedUnimplementedControllerServer()
}

// 这些会被 kubelet 调用
type NodeServer interface {
    // format (如果没format), mount 到 node 的 global directory
    NodeStageVolume(context.Context, *NodeStageVolumeRequest) (*NodeStageVolumeResponse, error)
    // umount
    NodeUnstageVolume(context.Context, *NodeUnstageVolumeRequest) (*NodeUnstageVolumeResponse, error)
    // mount --bind 到 pod directory
    NodePublishVolume(context.Context, *NodePublishVolumeRequest) (*NodePublishVolumeResponse, error)
    // umount --bind
    NodeUnpublishVolume(context.Context, *NodeUnpublishVolumeRequest) (*NodeUnpublishVolumeResponse, error)
    NodeGetVolumeStats(context.Context, *NodeGetVolumeStatsRequest) (*NodeGetVolumeStatsResponse, error)
    NodeExpandVolume(context.Context, *NodeExpandVolumeRequest) (*NodeExpandVolumeResponse, error)
    NodeGetCapabilities(context.Context, *NodeGetCapabilitiesRequest) (*NodeGetCapabilitiesResponse, error)
    NodeGetInfo(context.Context, *NodeGetInfoRequest) (*NodeGetInfoResponse, error)
    mustEmbedUnimplementedNodeServer()
}

关于 Sidecar Containers #

Sidecar Containers 是一系列标准容器,用于简化 CSI 插件的开发和部署

...

构建多平台容器镜像

构建多平台容器镜像

Docker buildx 插件/子命令 #

buildx 是 Docker 的一个 CLI 插件,用于扩展来自于 Moby BuildKit 项目的构建功能。

注意:buildx 需要 Docker 19.03 或更高版本。

BuildKit #

BuildKit是一个build引擎,它接收一个配置文件(Dockerfile),并转化成一个制品(容器镜像或其他制品)。相较与传统的build具有多阶段并发构建、更好的layer缓存支持等优点,Dockerfile中的RUN指令会被runc执行。

Docker Engine 从 23.0.0 版本开始默认在Linux上使用Buildx和BuildKit为builder。

Builder: a BuildKit daemon #

一个 builder 是一个 BuildKit 守护进程,BuildKit是build引擎,它解决Dockerfile中的构建步骤,以生成容器镜像或其他制品。

Build drivers #

Build 驱动有多种,例如 dockerdocker-containerkubernetesremote 等。

  • docker 使用捆绑在Docker守护进程中的BuildKit库。默认的Builder使用的该驱动。
  • docker-container 使用Docker创建一个专用的BuildKit容器。
  • kubernetes 在Kubernetes集群中创建BuildKit pods。
  • remote 直接连接到手动管理的BuildKit守护进程。
Build Drivers Comparison
Featuredockerdocker-containerkubernetesremote
Automatically load image
Cache export✓*
Tarball output
Multi-arch images
BuildKit configurationManaged externally
* The docker driver doesn't support all cache export options

默认的 Builder 实例 #

docker engine 会自动创建一个默认的 builder 实例,例如 default。默认的驱动是 docker,不支持多平台构建。

...

k8s namespaces

一个 namespace 将全局系统资源封装在一个抽象中,使得 namespace 内的进程看起来像是拥有该全局资源的独立实例。 对全局资源的更改只对属于同一 namespace 的其他进程可见。

Linux 上可用的 namespace 有:

namespace 类型隔离内容
cgroupCgroup 根目录
ipcSystem V IPC, POSIX 消息队列
network网络设备、协议栈、端口等
mount挂载点
pid进程 ID
time启动时间和单调时钟
user用户和组 ID
uts主机名和 NIS 域名

k8s cgroups

目前主要用 cgroup v2, 下面记录 k8s 如何通过 cgroup v2 管理 cpu 和 memory 资源

k8s 使用 cgroup 对容器进行资源管理 #

  1. kubelet 启动时会创建不同 QOS 级别的 root cgroup
  • Guaranteed: /sys/fs/cgroup/kubepods.slice/
  • Burstable: /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice
  • BestEffort: /sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice
  1. kubelet 通过 CRI 调用 container runtime 创建 sandbox 和 container

这里有个注意点:根据 pod QOS 类型不同, cgroup 创建的目录也不一样, 但是 container runtime 是不知道 kubelet 定义的 QOS 级别的, 所以 kubelet 通过 CRI 调用 container runtime 时会携带 CgroupParent 来指定对应 QOS 对应的 cgroup parent.

最终 container runtime 调用 runc 创建 sandbox 和 container, 并且会设置对应的 cgroup 和设置 cpu.maxmemory.max, 将 sandbox 和 container 启动进程的 PID 添加进 cgroup.procs

...

RKE2 安装 k8s 集群

根据创建 bridge 网络创建虚拟机时使用 cloudinit 初始化创建虚拟机, 并配置静态ip如下

主机名配置ip (域名)系统盘 / 数据盘
k8s-node018核16G192.168.1.218 (lb.k8s.lan)50GB / 100GB*1
k8s-node028核16G192.168.1.21950GB / 100GB*1
k8s-node038核16G192.168.1.22050GB / 100GB*1

安装 RKE2 #

安装第一个 server 节点 #

在 k8s-node01 节点执行

# 初始化 rke2 配置文件
mkdir -p /etc/rancher/rke2
cat <<EOF > /etc/rancher/rke2/config.yaml
tls-san:
  - lb.k8s.lan
write-kubeconfig-mode: "0600"
disable-cloud-controller: true
# cni 单独部署, 如无特殊需求, 这里也可以直接指定 flannel 或 calico
cni: none
debug: true
# 指定 kube-scheduler 自定义参数, 会自动覆盖到 /var/lib/rancher/rke2/agent/pod-manifests/kube-scheduler.yaml
kube-scheduler-arg:
  - v=4
  - bind-address=0.0.0.0
kube-controller-manager-arg:
  - bind-address=0.0.0.0
etcd-expose-metrics: true
EOF

curl -sfL https://rancher-mirror.rancher.cn/rke2/install.sh | INSTALL_RKE2_MIRROR=cn sh -
systemctl enable rke2-server.service
systemctl start rke2-server.service

配置介绍 #

tls-san #

tls-san 在 server 的 TLS 证书中增加了多个地址作为 Subject Alternative Name, 这样就可以通过 lb.k8s.lan 和 各个 server 节点 ip 访问 apiserver 服务.

...