作者:xixie,腾讯 IEG 后台开发工程师

这篇文章,你要翻很久,建议收藏。

Kubernetes,简称 K8s,是用 8 代替 8 个字符“ubernete”而成的缩写。是一个开源的,用于管理云平台中多个主机上的容器化的应用。k8s 作为学习云原生的入门技术,熟练运用 k8s 就相当于打开了云原生的大门。本文通过笔者阅读书籍整理完成,希望能帮助想学习云原生、以及正在学习云原生的童鞋快速掌握核心要点。学习 k8s 和大家学习 linux 差不多,看似复杂,但掌握了日常熟悉的指令和运行机理就能愉快的使用了,本文的重点和难点是服务、kubernetes 机理部分。

要点

Kubemetes 采用的是指令式模型, 你不必判断出部署的资源的当前状态, 然后向它们发送命令来将资源状态切换到你期望的那样。你需要做的就是告诉 Kuberetes 你希望的状态, 然后 Kubemetes 会采取相关的必要措施来将集群的状态 切换到你期望的样子。

1:资源:定义了一份资源,意味着将创建一个对象(如 Pod) 或 新加了某种规则(类似于打补丁,NetworkPolicy);

2:每个种类的 资源都对应一个 控制器,负责资源的管理;

3:pod 可以看成远行单个应用的 虚拟机,但可能被频繁地自动迁移而无需人工介入;

集群管理和部署的最小单位。

  • 无状态服务:新的 IP 名和主机地址

  • 有状态服务: StatefulSet, 一致的主机名 和 持久化状态

pod 中应用写入磁盘的数据随时 会丢失 【包括运行时, 容器重启,会在新的写入层写入】

记住,pod 是随时可能会被重启

4:容器重启原因:

  • 进程崩溃了

  • 存活探针返回失败

  • 节点内存耗尽,进程被 OOM

需要 Pod 级别的存储卷, 和 Pod 同生命周期, 防止容器重启丢失数据, 例如挂载 emptyDir 卷,看容器启动日志。

容器重启以指数时间避退,直到满 5 分钟。特别注意,容器重启并不影响 Pod 数量变化【理论上所有 Pod 都是一样,即使换新的 Pod,容器还是会重启】。

5:控制 Pod 的启动顺序;

  • Init 容器:Pod 中可以有任意多个 initContainers,初始化容器用来控制 Pod 与 Pod 之间 启动 的先后顺序;

  • 就绪探针: 一个应用依赖于另一个应用,若依赖无法工作,则 阻止 当前应用成为服务 端点群内的 pod 访问。

    • Deployment 滚动升级中会应用 就绪探针, 避免错误版本的出现。

6:localhost 一般指代节点,而非 pod;

7:当 Pod 关闭的时候,可能工作节点上 kube-proxy 还没来得及修改 Iptables,还是会将通信转移到 关闭的 Pod 上去;

推荐是 Pod 等一段时间再关闭【等多长时间是个问题, 和 TCP 断开连接等 2MSL 再关闭差不多】,直到 kube-proxy 已修改 路由表。

8:服务目录可以在 kubernetes 中轻松配置和暴露服务;

9:Kubernetes 可以通过单个 JSON 或 YAML 清单部署 一 组资源;

10:Endpoint, 有站点的意思(URL), REST endpoint, 就是一个 http 请求而已。

Endpoint 资源指 Service 下覆盖的 pod 的 ip:端口列表。

整理

字段

1:在定义 manifest 时, 常用的一些字段罗列如下:

备注:常用字段,非全部。

在线文档

k8s-字段
标注和必知

1:常见的注解整列

在线文档

k8s-标注和必知
命名空间和资源

1: k8s 中整理的命名空间和常用资源如下:

在线文档

k8s-命名空间和资源
常用指令

1:梳理常用指令

在线文档

kubernetes 指令汇总

环境

集群安装

1:单节点集群,minikube

2: 多节点集群,虚拟机 + kubeadm

k8s 介绍

微服务

1:微服务:大量的单体应用 被拆成独立的、小的 组件

2:配置、管理 需要自动化;

3:监控应用,变成了监控 kubernets

传统的应用 由 kubernets 自己去监控

4:拆成微服务的好处:

  • 1: 改动单个服务的 API 成本更小;

  • 2:服务之间可通过 HTTP(同步协议)、AMQP(异步协议)通信;

  • 3:新服务可用不同语言开发;

虚拟机和容器

1:虚拟机多出来的三个部分:

  • 虚拟化 CPU;

  • 用户操作系统;

  • 管理程序:透传虚拟机上应用的 操作指令 到 宿主机上的 物理 CPU 来执行;

2:容器的隔离机制

  • Linux 命名空间, 每个进程只能看到自己的系统视图(文件、进程、网络接口、主机名等)

    • Mount: 挂载卷,存储;

    • PID:proceess ID , 进程树;

    • Network:网络接口;

    • Inter-process communication: IPC, 进程间通信;

    • UTS:本地主机名;

    • User ID: 用户。

  • Linux 控制组 (cgroups), 限制进程能使用的资源量(CPU、内存、网络带宽等)

3:容器限制了 只能使用 母机的 Linux 内核;

  • X86 上编译的应用 容器化后,不能运行在 ARM 架构的母机上;

Docker

1:Docker:打包、分发、运行应用程序的 ==平台==。

运行容器镜像的 软件,类似于 VMware

简化了 Linux 命名空间隔离 和 cgroups 之内的 系统管理;

  • 镜像:经过 Docker 打包的 环境(包含应用程序的依赖,配置文件,运行 app)

  • 镜像仓库:云端存储;

  • 容器:基于 Docker 创建的运行时环境,是一个 运行在 Docker 主机上的进程, 和其他进程隔离且能使用的资源受限;

2:类似的容器运行时,还有 rock-it;

k8s 组成

1:一个 k8s 分成两类:

  • master node (主节点):主节点上的组件可以组成一个集群,负责集群的控制和调度

  • work node (工作节点):工作节点一般是多个,实际部署应用的 节点

2:组件

  • 1:调度器:选择资源足够的 node 分配 pod

    节点不够,Cluster Autoscaller 横向扩容节点控制 pod 选在哪个节点上【亲缘性 affinity】pod 要和 哪个 pod 在一块。

  • 2:控制器:跟踪节点状态,复制 pod,持续跟踪 pod, 处理节点失败, 大部分资源都会对应一个 控制器

例如 Endpoint Controller 通知工作节点上的 kube-proxy 修改 iptables 。

本质是一个 死循环,监听 API 服务器的状态;

  • 3:etcd 分布式数据存储,API 服务器将集群配置存入。比如每次提交一个 yaml 文件,校验通过后会存入;

  • 4:kubelete: 接收 API 服务器通知 和 Docker 交互,控制容器的启动 上报 node 的资源总量

  • 5:Docker 容器运行时, 拉取镜像、启动容器

  • 6:pod:

1:独立的 IP 和 端口空间 ;

pod.hostNetwork: true, 使用宿主节点的网络接口和端口;containers.ports.hostPort: 仅把容器端口绑定在节点上。

2:独立的进程树,自己的 PID 命名空间

  • pod.hostPID: true, 使用宿主节点的 进程空间

  • pod.hostIPC: true, 使用宿主节点的 IPC 命名空间

3:自己的 IPC 命名空间,同 pod 可以通过进程间通信 IPC

4:同 pod 两个容器,可以共享存储卷

k8s总体架构图-第 2 页

3: k8s 功能

1:自动处理失败 容器:重启, 可自动选择 新的 工作节点。【自修复】

2:自动调整 副本数:根据 CPU 负载、内存消耗、每秒查询 应用程序的其它指标 等;【==自动扩缩容==】

3:容器 移除或移动, 对外暴露的 静态 IP 不变(环境变量 or client 通过 DNS 查询 IP) 【==可实现复杂的 集群 leader 选举==】

4:可以限制某些容器镜像 运行 在指定硬件的机器 【在 HDDS 系列的机器上选择一个】上:

- HDDS;
- SSD;

Docker 和 k8s

使用 Docker
安装 Docker

1:安装:略

2:docker run <image> :现在本地查找镜像, 若无, 前往 http://docker.io 中 pull 镜像。

其它可用镜像:http://hub.docker.com

# 运行一个镜像,并传入参数
docker run busybox echo "Hello World"
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
b71f96345d44: Pull complete
Digest: sha256:930490f97e5b921535c153e0e7110d251134cc4b72bbb8133c6a5065cc68580d
Status: Downloaded newer image for busybox:latest
Hello World

3:==docker 查看镜像本地是否已存在 --下载镜像-- 创建容器 -- 运行 echo 命令--- 进程终止 --- 容器停止运行;==

创建应用

1:创建一个简单的 node.js 应用, 输出主机名:

const http = require('http');
const os = require('os');

console.log("Kubia server starting...");

var handler = function(request, response) {
  console.log("Received request from " + request.connection.remoteAddress);
  response.writeHead(200);
  response.end("You've hit " + os.hostname() + "\n");
};

var www = http.createServer(handler);
www.listen(8080);

2:构建镜像,需要一个 Dockerfile 文件

和 app.js 同一目录

# From 定义了基础镜像,可以是 Ubuntu 等系统,但应尽量遵从精简
FROM node:7
# 本地文件添加到 镜像的根目录
ADD app.js /app.js
# 镜像被运行时 需要执行的命令
ENTRYPOINT ["node", "app.js"]
构建镜像

1:Dockerfile + app.js 就能创建镜像包:

# 基于当前目录 创建 kubia 镜像
$ docker build -t kubia .
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM node:7
7: Pulling from library/node
ad74af05f5a2: Pull complete
2b032b8bbe8b: Pull complete
a9a5b35f6ead: Pull complete
3245b5a1c52c: Pull complete
afa075743392: Pull complete
9fb9f21641cd: Pull complete
3f40ad2666bc: Pull complete
49c0ed396b49: Pull complete
Digest: sha256:af5c2c6ac8bc3fa372ac031ef60c45a285eeba7bce9ee9ed66dad3a01e29ab8d
Status: Downloaded newer image for node:7
 ---> d9aed20b68a4
Step 2/3 : ADD app.js /app.js
 ---> 43461e2e8cef
Step 3/3 : ENTRYPOINT ["node", "app.js"]
 ---> Running in 56bc0e5982ce
Removing intermediate container 56bc0e5982ce
 ---> 0d2c12c8cc80
Successfully built 0d2c12c8cc80
Successfully tagged kubia:latest

2: 镜像构建过程

  • Docker 客户端:在宿主机上的;

  • Docker 守护进程:运行在一个虚拟机内;【可以在远端机器】

镜像分层

1:Dockerfile 中的的每一行 都会 形成一个 镜像层

最后一层 是 kubia:latest 镜像

2:镜像分层的好处:节省下载,

3:查看本地新镜像 docker image

运行镜像

1:端口映射后,可用 http://localhost:8080 访问;

                 # 容器名称    # 本机8080 映射到容器的 8080 端口      # 镜像文件
docker run --name kubia-container -p 8080:8080                  -d kubia
4099df6236c5d4905a268b213ab986949f6522122454de41f56293ce3508e958 # 容器 ID


# test,主机名就是 分配的 容器 ID
$ curl localhost:8080
You've hit 4099df6236c5

2: 查看运行中的容器:

$ docker ps
CONTAINER ID   IMAGE                                 COMMAND                  CREATED          STATUS          PORTS                                                                                                                                  NAMES
4099df6236c5   kubia                                 "node app.js"            40 seconds ago   Up 38 seconds   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp                                                                                              kubia-container
2744c31527b9   gcr.io/k8s-minikube/kicbase:v0.0.22   "/usr/local/bin/entr…"   3 days ago       Up 3 days       127.0.0.1:49172->22/tcp, 127.0.0.1:49171->2376/tcp, 127.0.0.1:49170->5000/tcp, 127.0.0.1:49169->8443/tcp, 127.0.0.1:49168->32443/tcp   minikube

# 查看容器 详细信息
docker inspect kubia-container
查看容器内部

1:一个容器可以运行多个进程;

2:Node.js 镜像中 包含了 bash shell , 可在容器 内运行 shell :

# kubia-container 容器内 运行 bash 进程
docker exec -it kubia-container bash

# 退出
exit
  • -i,确保标准输入流保待开放。需要在 shell 中输入命令。

  • -t, 分配一个伪终端(TTY)。

3: 查看容器内的进程;

$ docker exec -it kubia-container bash
root@4099df6236c5:/# ps axuf
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          12  0.6  0.0  20244  2984 pts/0    Ss   21:44   0:00 bash
root          18  0.0  0.0  17496  2020 pts/0    R+   21:44   0:00  \_ ps axuf
root           1  0.0  0.6 614432 26320 ?        Ssl  21:41   0:00 node app.js

只能在 docker 的守护进程内查看

4:在母机上查看进程

user00@ubuntu:~$ ps axuf | grep  app.js
app.js
root     3224898  0.0  0.6 614432 26320 ?        Ssl  14:41   0:00  \_ node app.js

容器和主机上的进程 ID 是独立、不同的;

5:容器 是独立的:

  • PID Linux 命名空间

  • 文件系统 都是独立的

  • 进程

  • 用户

  • 主机名

  • 网络接口

停止和删除容器

1:停止容器,会停止容器内的主进程。

容器本身依然存在,通过 docker ps -a

docker stop kubia-container

# 打印所有容器, 包括 运行中的和已停止的
$ docker ps -a
CONTAINER ID  IMAGE  COMMAND         CREATED                STATUS    PORTS      NAMES
4099df6236c5  kubia  "node app.js"   6 minutes ago    Exited (137) 4 seconds ago

# 真正的 删除容器
docker rm kubia-container
向仓库推送镜像

1:仓库 http://hub.docker.com 镜像中心;

2:按照仓库要求,创建额外的 tag

     # 个人 ID
docker tag kubia luksa/kubia

# 查看镜像
docker images | head

# 自己的 ID 登录
docker login

# 推送镜像
docker push luksa/kubia

3: 可在任意机器上运行 云端 docker 镜像:

docker run -p 8080:8080 -d luksa/kubia
使用 kubernetes 集群
安装集群的方式

1:安装集群通常有以下四种方式:

  • 本地 单点;

  • Google Kubernetes Engine(GKE) 上托管的集群;

  • kubeadm 工具安装;

  • 亚马逊的 AWS 上安装 kubernetes

Minikue 启动 k8s 集群

1:安装略

2:启动 Minikube 虚拟机

minikube start

3: 安装 k8s 客户端(kubectl)

GKE 创建三节点集群

1:创建 3 个工作节点的示例:

gcloud container clusters create kubia --num-node 3 --machine-type f1-micro

2:本地发起请求 到 k8s 的 master 节点;master 节点负责 调度 pod, 以下创建了 3 个工作节点;

部署 Node.js 应用
kubectl run kubia --image=luksa/kubia --port=8080 --generator=run/v1
pod

1: 多个容器运行在一起:

一个 pod 可以包含任意数量的容器;

2:pod 拥有单独的 私有 IP、主机名、单独的 Linux 命名空间;

==Pod 是扩缩容的基本单位;==

3:k8s 中 运行 容器镜像需要经历两个步骤:

  • 1:推送 docker 镜像 到云端 【不同工作节点上的 Docker 能访问到 该镜像】;

  • 2:运行 kubectl ,==创建一个 ReplicationController 对象 【运行指定数量的 pod 副本】;==

    • 调度器 创建 pod,并 选择一个 工作节点,分配给 pod;

创建外部访问的服务

1:常规服务:ClusterIP 服务, 比如 pod, 只能从 集群内部访问;

2:创建 LoadBalancer 类型的服务,负载均衡,对外提供 公共 IP 访问 pod;

内部删减 pod, 移动 pod,外部 IP 不变, 一个外部 IP 可对应 多个 pod。

kubectl expose rc kubia --type=LoadBalancer --name kubia-http

kubectl get svc # 查看服务是否分配 EXTERNAL-IP(外部IP)

minikube service kubia-http 可查看 IP:端口

系统逻辑部分

1:服务、pod、对象:

水平伸缩 pod 节点
kubectl get replicationcontrollers  # 获取 ReplicationControllers 状态

告诉期望数量即可:

kubectl scale rc kubia --replicas=3

多次访问,服务作为负载均衡,会随机选择一个 pod:

和上面对比,这里有三个 pod 实例。

pod 运行在哪个节点上

1: pod 的 IP 和运行的工作节点

image-20210617220307330
$ kubectl  describe  pod kubia-jppwd
Name:         kubia-jppwd
Namespace:    default
Priority:     0
Node:         minikube/192.168.49.2   # 具体的节点
Dashboard

1:若是 GKE 集群, 可通过 kubectl cluster-info | grep dashboard 获取 dashboard 的 URL。

2:终端输入 minikube dashboard 会自动打开浏览器:

pod

Pod

1: 容器被设计为每个容器只运行一个进程, 保证轻量和一定的隔离性;

2:有些容器又有一些紧密的联系,例如常说的 side-car 容器,负责网络代理,日志采集的容器, 这些容器最好放一起。这就出现了 上层的 pod 。

pod 是 k8s 中引入的概念,docker 是可以直接运行容器的。

3:k8s 通过配置 Docker 让一个 pod 内的所有容器 共享 相同的 Linux 命名空间 【有些容器放到一个 pod 的好处】:

  • 相同的 network 和 UTS 命名空间;

  • 共享相同的主机名和网络接口;pod 中的端口,不能绑定多次;

    • 两个 pod 之间可以实现 两个 IP 相互访问

不管两个 pod 是否在同一节点, 可以想 无 NAT 的平坦网络之间通信(类似局域网 LAN)

  • 相同的 IPC 命名空间下运行;能通过 IPC 进行通信;

  • 共享相同的 PID 命名空间

注意:文件系统来自容器镜像,默认容器的文件系统彼此隔离。

4:pod 是逻辑主机, 其行为与非容器世界中的物理主机或虚拟机非常相似。此外, 运行在同一个 pod 中的进程与运行在同一物理机或虚拟机上的进程相似, 只是每个进程都封装在一个容器之中。

5: pod 可以当做独立的机器,非常轻量, 可同时有大量的 pod;

6: pod 是扩缩容的基本单位;

7: pod 的定义包含三个部分:

  • metadata 包括名称、命名空间、标签和关于该容器的其他信息 。

  • spec 包含 pod 内容的实际说明 , 例如 pod 的容器、卷和其他数据 。

  • status 包含运行中的 pod 的当前信息,例如 pod 所处的条件 、 每个容器的描述和状态,以及内部 IP 和其他基本信息 。

一个简单 pod 包含的三部分:

apiVersion: v1  # 分组和版本
kind: Pod       # 资源类型
metadata:
  name: kubia-manual  # Pod 名称
spec:
  containers:
  - image: luksa/kubia  # 容器使用的镜像
    name: kubia
    ports:
    - containerPort: 8080 # 应用监听的端口
      protocol: TCP

8:可使用 kubectl explain 查看指定资源信息

kubectl explain pods

9:创建资源:

# 所有定义的 资源 manifest 都通过该指令来创建,非常重要
kubectl create -f kubia-manual.yaml

10:查看日志:

kubectl logs <pod-name>

11:若不想通过 Service 与 pod 通信,可通过端口转发:

# 将 8888 端口 转发到 该 pod的 8080 端口
kubectl port-forward <pod-name> 8888:8080

curl localhost:8888

端口转发是 kubectl 内部实现的。

标签

1:可使用 标签组织管理 pod

标签也能组织其他 k8s 资源

2:例如定义两组标签,可不同维度管理 pod

  • label_key = app: 按应用分类

  • label_key = rel: 按版本分类

3:pod 中使用 labels 定义标签:

kind: Pod
metadata:
  name: kubia-manual-v2
  labels:
    # 定义的两组标签
    creation_method: manual
    env: prod

4:可通过 -l 指定标签选择列出 pod 子集:

$  kubectl get po -l  creation_method=manual
NAME              READY   STATUS    RESTARTS   AGE
kubia-manual-v2   1/1     Running   0          31s

使用多个标签, 中间用 逗号分隔即可。

pod 调度到特定节点

1:默认下,pod 调度到哪个 节点是不确定的,这由调度器决定。

2:有些情况,需要将 pod 调度到特定的节点上(比如偏计算的 pod 需要调度到 gpu 集群上)

3:给节点打标签

kubectl label node <node-name> <label-key>=<label_value>

4:通过 nodeSelector 调度到含有 gpu=true 标签的节点上。

apiVersion: v1
kind: Pod
metadata:
  name: kubia-gpu
spec:
  # 调度到含有 gpu=true 的节点上
  nodeSelector:
    gpu: "true"
  containers:
  - image: luksa/kubia
    name: kubia

注意:含有该标签的节点可能有多个,届时将选择其中一个。

候选节点最好是一个集合,避免单个节点故障会造成服务不可用。

注解

1:注解(Annotation)和 标签类似,也是键值对。

有些注解是 k8s 自动添加的。

注解不能超过 256K

命名空间

1:命名空间(namespace, 简称 ns)可对对象分组。

资源名称只需要在命名空间内保证唯一即可, 跨命名空间可以重。

2:列出某个命名空间下的 pod

kubectl get po --namespace kube-system

3:命名空间,可控制用户在该命名空间的访问权限, 限制单个用户可用的 资源数量;

4:创建命名空间:

apiVersion: v1
kind: Namespace
metadata:
  name: custom-namespace

5:尽管命名空间对 对象进行了分组, 但 ==并不提供实质上的隔离== ,例如不同命名空间的 pod 能否通信取决于网络策略。

6:删除 pod 有多种方式:

kubectl delete po <pod-name>  # 按名称删除
kubectl delete po --all  # 删除当前命名空间的所有 pod, 若 ReplicationController 未删除,将重新创建 pods
kubectl delete po -l <label-key>=<label-val>  # 按标签删除

kubectl delete ns <ns-anme>  # 删除整个命名空间

kubectl delete all --all     # 删除当前命名空间内的所有资源,包括托管的 ReplicationController, Service,  但 Secret 会保留

托管集群

保持进程健康

1:进程异常的几种情形:

  • 主进程 崩溃->kubelet 将 重启 容器;

  • 内存泄漏, JVM 会一直运行,但会抛出 OutofMemoryErrors, 让程序 向 k8s 发出信号 触发 重启;

  • 外部检查:应用死循环 or 死锁

存活探针

1:定期检查容器

2:三种探测机制:

  • HTTP Get 向容器发送请求;

  • TCP 套接字,与容器建立 TCP 连接;

  • Exec 探针,在容器内执行任意指令,查看退出状态码;

3:HTTP 探针,定期发送 http Get 请求;

/heath HTTP 站点不需要认证,否则会一直认为失败,容器 无限重启;

apiVersion: v1
kind: Pod
metadata:
  name: kubia-liveness
spec:
  containers:
  # 镜像内有坏掉的应用
  - image: luksa/kubia-unhealthy
    name: kubia
    # 存活探针
    livenessProbe:
      httpGet:
        path: /
        port: 8080

4:返回的状态码 137 和 143:

$ kubectl describe  pod kubia-liveness
 State:          Running
      Started:      Thu, 17 Jun 2021 11:04:53 -0700
    Last State:     Terminated
      Reason:       Error
      Exit Code:    137   # 有错误码返回

 Warning  Unhealthy  10s    kubelet            Liveness probe failed: HTTP probe failed with statuscode: 500

5:探针的附加信息:

查看状态时,可看到 存活探针信息:

 Liveness:       http-get http://:8080/ delay=0s timeout=1s period=10s #success=1 #failure=3
  • delay=0s: 容器启动后立即检测;

  • timeout=1s:限制容器在 1s 内响应,否则失败;

  • period=10s:每隔 10s 探测一次;

  • failure=3:连续三次失败后,重启容器;

6:具有初始延迟的 存活探针:【程序还未 启动稳定】

# 第一次探针前,等15s,防止容器没准备好
      initialDelaySeconds: 15

7:若无探针, k8s 认为进程还在运行,容器就是健康的。

8:探针的注意事项:

  • 1:探针应该轻量,不能占用太多 cpu 【应 计入 容器的 CPU 配额】, 一般 1s 内执行完;

  • 2:java 程序应该用 http get 探针,而非启动全新 JVM 获取存活信息的 Exec 探针(太耗时)

  • 3:无需设置 探针的失败重试次数, k8s 为了确认一次探测的失败,==默认就会尝试若干次==;

9:重启容器由 kubelet 执行;主服务器上的 k8s Control Plane 组件不会参与;

  • ==若整个节点崩溃, 则无法重启== 【kubelet 依赖于节点】;

  • 若要保证节点挂了,pod 能重启,应该使用 RC 或 RS;

ReplicationController

1: 用于管理 pod 的多个副本;

2:会自动调整 pod 数量为 指定数量:

多余副本在以下几种情况下会出现:

  • 有人会手动创建相同类型的 pod。

  • 有人更改现有的 pod 的 ” 类型” 。

  • 有人减少了所需的 pod 的数量, 等等。

3:ReplicationController 的功能:

  • 确保一 个 pod (或多个 pod 副本)持续运行, 方法是在现有 pod 丢失时启动一 个新 pod。

  • 集群节点发生故障时, 它将为故障节 点 上运 行的所有 pod (即受 ReplicationController 控制的节点上的那些 pod) 创建替代副本。

  • 它能轻松实现 pod 的水平伸缩:手动和自动都可以

4: 根据 pod 是否匹配 标签选择器 来调整:

5: 更改标签选择器 和 pod 模板,对当前的 pod 没有影响;

  • 也不关心 容器镜像、 环境变量和 其它;

  • 只影响 创建新的 pod (新的 曲奇 切模 cookie cutter )

修改 pod 模板:

更改副本个数,就能实现动态扩缩容:

kubectl scale rc kubia  --replicas=3 # 调整副本数为3

6: 创建对象:

上传到 API 服务器, 会创建 kubia 的 ReplicationController 【简称 RC】。

  • 模板中的 pod 标签 必须 与 RC 一致,否则会无休止创建容器(达不到期望数量的 pod)

  • API 服务会校验 RC 的定义,不会接受错误配置;

  • 可以不指定 RC 的选择器,会自动根据 pod 模板中的标签自动设置;

apiVersion: v1
kind: ReplicationController
metadata:
  name: kubia
spec:
  replicas: 3  # pod 副本数量

  # 标签选择器,选择 标签 app = kubia的pod进行管理
  selector:
    app: kubia

  # pod 模板
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia
        ports:
        - containerPort: 8080

7:删除 pod 标签(或者 移入另一个 RC 的掌控), 脱离 RC 掌控,RC 会自动起一个 新的 pod;

  • 原 pod 可用于调试,完成后,手动删除 该 pod 即可;

  • 实际 pod 数量, RC 通过就绪探针;

  • 删除 pod, 允许 客户端监听到 API 服务器的通知,通过检查实际的 pod 数量 采取适当的措施;

kubectl delete pod <pod-name>

添加 pod 另外的标签, RC 并不 care;

8:通过 pod 的 metadata.ownerReferences 可以知道 该 pod 属于哪个 RC;

9:节点故障:例如网络断开;

  • RC 一段时间后检测到 pod 关闭(旧节点变为 unknown), 会启动新的 pod 代替原来的 pod;

  • 当旧节点恢复时, 节点状态变为 ready, unknown pod 会被删除;

10:更改 RC 的 标签选择器:

  • 原有 pod 都会脱离管控;

  • RC 会创建(若无)新的指定数量、指定标签 的 pod

==RC 的标签选择器可以修改,但其他的 控制器对象 不能。==

11:删除 RC,默认会删除 RC 管理的 pod

可以使用 选项 --cascade=false 保留 pod.

kubectl delete rc <rc-name> --cascade=false
使用 ReplicaSet 替换 ReplicationController

1:ReplicationController 最初是 用于赋值和异常时重新调度节点 的唯一 组件。

2:一般不会直接创建 ReplicaSet , 而是 创建 更高层级的 Deployment 资源时(第 9 章) 自动创建他们。

3:ReplicaSet 功能和 ReplicationController 一样, pod 选择器的 表达能力更强:

  • ReplicationController 只允许 k和v 同时 匹配的标签;

  • ReplicationController 只能匹配单个 kv;

  • ReplicaSet 基于 标签名 k 匹配;

4:若已经有了 3 个 pod,不会创建任何新的 pod,会将 旧 pod 纳入自己的管辖范围;

基础使用 和 RC 一样简单。

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: kubia
spec:
  replicas: 3
  selector:
    matchLabels:
      app: kubia
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia

5:matchExpressions 更强大的选择器;

selector:
    # 标签 需要包含的 key 和 val
    matchExpressions:
      - key: app
        operator: In
        values:
         - kubia

6:四个有效的运算符 operator

  • In : Label 的值 必须与其中 一个指定的 values 匹配。

  • Notln : Label 的值与任何指定的 values 不匹配。

  • Exists : pod 必须包含一个指定名称的标签(值不重要)。使用此运算符时,不应指定 values 字段。

  • DoesNotExist : pod 不得包含有指定名称的标签。values 属性不得指定 。

7: matchLabelsmatchExpressions 可以同时指定,条件是的关系。

8:删除 ReplicaSet 也会删除 pod:

kubectl delete rs <rs-name>
DaemonSet: 在每个节点上运行一个 pod

1:需在每个节点上运行日志收集 or 监控:如 k8s 的kube-proxy 进程

2:通过 系统初始化脚本 or systemd 守护进程启动;

3:无期望副本数概念,在 节点选择器下, 运行一个 pod;

  • 和节点绑定在一起:节点下线,并不会再创建 新 pod;

  • ==新节点加入 【添加 节点 label 后】, 匹配节点选择器, 自动创建一个 新的 pod ;==

  • 无意中删除了 该 pod, 会自动创建一个 pod;

4:从 DaemonSet 的 pod 模板 创建 pod

5:通过 节点选择器 nodeSelector 选中 部分节点创建 pod;

apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
  name: ssd-monitor
spec:
  selector:
    matchLabels:
      app: ssd-monitor
  template:
    metadata:
      labels:
        app: ssd-monitor
    spec:
      # 节点选择器,选择 标签为 disk=ssd 的节点
      nodeSelector:
        disk: ssd
      containers:
      - name: main
        image: luksa/ssd-monitor

6: 节点可以设置为 不可调度 【通过调度器控制】, 防止 pod 部署到 该节点;

但 DaemonSet 管理的 pod 作为==系统服务,完全绕过调度器,== 即使 节点是 不可调度的,仍然可以运行系统服务;

7:从节点删除 节点标签, DaemonSet 管理的 Pod 也会被删除:

kubectl label node <node-name> disk=hdd --overwrite
Job:运行单个任务的 pod

1:Job: 一旦任务完成,不重启 容器;

两个地方会重启:

  • 1:job 异常;

  • 2:pod 在执行任务时,被从节点逐出;

2:会重启的资源

job 只有在执行失败的时候才会被重启;

被托管的 ReplicaSet 会重启, Job 若未完成,也会重启。

image-20210617182356951

3:job 资源:

restartPolicy 配置为 Onfailure or Never:完成后 不需要 一直重启

apiVersion: batch/v1
kind: Job
metadata:
  name: batch-job
spec:
  template:
    metadata:
      labels:
        # Job 未指定 pod 选择器,默认根据 pod 模板中的标签创建
        app: batch-job
    spec:
      # job 不能使用默认的 Always 作为重启策略
      restartPolicy: OnFailure
      containers:
      - name: main
        image: luksa/batch-job

4:job 中串行运行多个 pod:

kind: Job
metadata:
  name: multi-completion-batch-job
spec:
  completions: 5 # 顺序执行 5 次

5:job 中并行运行多个 pod:

spec:
  completions: 5  # 需要完成 5 次
  parallelism: 2  # 最多同时 2 个并行

6:Job 在运行时,可调整 Job 数量:

kubectl scale job <job-name> --replicas 3

7:限制 Job 完成时间和失败重试次数:

  • activeDeadlineSeconds: 限制 Job 运行的时间

  • spec.backoffLimit: 默认 6,Job 失败前 可重试的次数

CronJob: 定期执行

1:时间格式:cron

2: 每隔 15 分钟运行一次:

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: batch-job-every-fifteen-minutes
spec:
  # 每15分钟运行一次
  schedule: "0,15,30,45 * * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app: periodic-batch-job
        spec:
          restartPolicy: OnFailure
          containers:
          - name: main
            image: luksa/batch-job

3:时间格式:

  • 分钟

  • 小时

  • 每月中的第几天

  • 星期几

4:注意事项

  • CronJob 根据 jobTemplate 创建 job 对象;

  • startingDeadlineSeconds 超时未执行 视为 Failed;

  • Job 能被重复执行,可能会被创建多个;

  • Job 应该是串行的, 中间不能有 遗漏的任务;

服务

1:服务可以说是 k8s 中最复杂的一环。

k8s 集群和普通集群不同的是,

  • pod 是临时的,随时会被创建和关闭(动态扩缩容)

  • pod 重启,,会分配新的 IP 地址;

2:Service 资源:外部访问后端 pod, 可提供一个统一可供外部访问的 IP 地址和端口

这里更多的是针对无状态服务,所有 pod 都是对等的, API 服务器只需 随机分配一个 pod

3:应用服务的两种情形:

  • 外部集群 可通过 服务 连接 pod

  • 内部 pod 之间也可通过服务连接

image-20210617141428940
连接集群内部的 Service

4:服可通过标签选择器,选择 需要 连接的 pod

控制 Service 服务的范围。

image-20210617141644761

5:服务可通过 kubectl expose 创建,也可通过 yaml 创建。

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  ports:
  # 服务对外的端口
  - port: 80
    # 服务转发到容器的端口
    targetPort: 8080

  # 连接pod 集合是带有 app: kubia 标签的 pods
  selector:
    app: kubia

创建 svc 后,会分配一个 集群 IP,==该 IP 对外不可用==,仅限于 集群内的 pod 访问 【pod 和 pod 之间也可通过 服务连接】。

image-20210621205022413

集群内部测试服务,有三种方法向 Service 发送请求:

  • 创建一个 pod 应用,并向 集群 IP 发送 requests;

  • ssh 登录到节点上,使用 curl

  • 登录到其中的一个 pod 运行 curl 指令

6:可用 kubectl exec 在远程容器里执行:

user00@ubuntu:~$ kubectl get po
NAME          READY   STATUS    RESTARTS   AGE
kubia-jppwd   1/1     Running   0          5m13s
kubia-sxkrr   1/1     Running   0          5m13s
kubia-xvkpg   1/1     Running   0          5m13s

# --  表示 Kubectl 执行命令的结束
# -s 告诉 kubectl 需要连接不同的 API服务器,而非默认的
user00@ubuntu:~$ kubectl exec kubia-jppwd -- curl -s  http://10.105.237.81

curl 的过程如下, Service 选择 pod 是随机选择一个。

image-20210617144543726

7:Service 可以通过设置 sessionAffinity: ClientIP 来让同一个客户端的请求每次指向同一个 pod.

默认值是 None

8:Service 可同时暴露多个端口, 例如 http 请求时, 80 端口映射到 8080, https 请求时, 443 端口映射到 8443

9:在 Service 的生命周期内, 服务 ip 不变。

  • 当 pod 创建时,k8s 会初始环境变量指向现在的 集群 IP

user00@ubuntu:~$ kubectl exec kubia-jppwd  env | grep -in  service
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
3:KUBERNETES_SERVICE_HOST=10.96.0.1
4:KUBERNETES_SERVICE_PORT_HTTPS=443

# 集群IP 和 端口
7:KUBIA_SERVICE_HOST=10.105.237.81
8:KUBIA_SERVICE_PORT=80
12:KUBERNETES_SERVICE_PORT=443

对应了两个服务:

$ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   2d21h
kubia        ClusterIP   10.105.237.81   <none>        80/TCP    50m

10:每个 pod 默认使用的 dns:

$kubectl exec kubia-jppwd  -- cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local localdomain
options ndots:5

pod 是否适用 dns, 由 dnsPolicy 属性决定。

系统命名空间,通常有个 pod 运行 DNS 服务;

$ kubectl get po -n kube-system
NAME                               READY   STATUS    RESTARTS   AGE
coredns-74ff55c5b-pvqxv            1/1     Running   0          2d21h

11:在 pod 中,可使用全限定域名(FQDN) 来访问 Service。

格式如下:

kubia.default.svc.cluster.local

# kubia: Service 名称
# default: namespace
# svc.cluster.local: 所有集群本地服务中使用的可配置集群域后缀

若在同命名空间下, svc.cluster.localdefault 可省略。

user00@ubuntu:~$  kubectl exec -it  kubia-jppwd  bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@kubia-jppwd:/# curl http://kubia.default.svc.cluster.local
You've hit kubia-sxkrr
root@kubia-jppwd:/# curl http://kubia.default
You've hit kubia-xvkpg
root@kubia-jppwd:/# curl http://kubia
You've hit kubia-xvkpg

12:集群 IP 是一个 虚拟的 IP, 只有配合服务端口才有意义。

root@kubia-jppwd:/# ping kubia
PING kubia.default.svc.cluster.local (10.105.237.81): 56 data bytes
^C--- kubia.default.svc.cluster.local ping statistics ---
51 packets transmitted, 0 packets received, 100% packet loss
连接集群外部的 Service

1:让 pod 连接到集群外部。

2:服务并不是和 pod 直接相连,中间有 Endpoint 资源

user00@ubuntu:~$ kubectl get svc kubia
Selector:          app=kubia # 创建 endpoint 资源,选择的 pod 标签

TargetPort:        8080/TCP
Endpoints:         172.17.0.3:8080,172.17.0.4:8080,172.17.0.5:8080

Service 通过选择器构建 IP 和 端口 列表,然后存储在 endpoint 资源中

连接时,随机选择其中一个

3:当 Service 无节点选择器时,不会自动创建 Endpoint 资源。

apiVersion: v1
kind: Service
metadata:
  # Service 名称,后面会用
  name: external-service
spec:
  ports:
  - port: 80

  # 无 选择器

手动指定 Endpoint 注意需要**==和 Service 同名称==**。

apiVersion: v1
kind: Endpoints
metadata:
  # 和 Service 同名称
  name: external-service
subsets:
  # Service 重定向的 IP 地址
  - addresses:
    - ip: 11.11.11.11
    - ip: 22.22.22.22
    ports:
    - port: 80

通过 endpoint 列表, 可连向其它地址。

image-20210617154439324

4:也可创建具有别名的外部服务:

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  # 别名
  type: ExternalName
  externalName: api.somecompany.com
  ports:
  - port: 80

创建该服务后,内部 pod 可通过 external-service.default.svc.cluster.local 访问外部域名

将服务暴露给外部客户端

1:pod 向外部公开的服务,如 web 服务。

2:有以下几种方式,使外部可访问服务:

  • 服务类型为 NodePort: 每个节点上开放一个端口,访问内部服务 (可被 Service 访问)。

  • 服务类型 为 LoadBalance:NodePort 的一种扩展, 通过负载均衡器访问, 将流量重定向到所有节点的 NodePort;

  • 创建 Ingress 资源:通过一个 IP 地址公开多个 服务 (运行在 HTTP 层)

NodePort

1:创建一个 NodePort 类型的 Service。

apiVersion: v1
kind: Service
metadata:
  name: kubia-nodeport
spec:
  # 服务类型为 NodePort
  type: NodePort
  ports:
   # 集群IP的端口
  - port: 80
    # pod 开放的端口
    targetPort: 8080

    # 不指定端口,将随机选择一个端口
    nodePort: 30123
  selector:
    app: kubia

2:现在可通过以下几种方式访问服务:

user00@ubuntu:~$ kubectl get svc
NAME               TYPE           CLUSTER-IP
kubia-nodeport     NodePort       10.104.119.122   <none>          80:30123/TCP   5m21s
  • 1: <集群 IP>:80

  • 2: 多节点集群

    • 节点 1IP:30123

    • 节点 2IP:30123

注意:即使连到节点 1 的端口, 也可能分配到 节点 2 上执行。

设置 externalTrafficPolicy:local 属性,可将 流量路由到当前节点所在的 pod . 若当前节点 pod 不存在,连接挂起。

externalTrafficPolicy:local 不利于负载均衡, 当节点上的 pod 分散不均时。

有个好处是,转发到本地 pod,不用进行 SNAT(源网路地址转换),这会将 改变 源 IP 记录。

image-20210617170835886

开放 NodePort 端口。

image-20210617162126942

3:获取 节点 IP 【特意观察了下,节点 IP 和 机器 IP 不是一个,但在同一网关下】

user00@ubuntu:~$ kubectl get nodes -o json | grep address
                "addresses": [
                        "address": "192.168.49.2",

可在本地机器上,向 节点IP:nodePort 发送请求:

$ curl http://192.168.49.2:30123
You've hit kubia-jppwd

minikue 可通过网页访问 minikube service <service-name>

image-20210617164756909
LoadBalancer

负载均衡器在 NodePort 外面包了一层。

1:负载均衡器放在节点前面,将请求传播到 健康的节点。

负载均衡器拥有 自己独一无二的 可公开访问的IP 地址, 并将连接重定向到服务。

apiVersion: v1
kind: Service
metadata:
  name: kubia-loadbalancer
spec:
  # 负载均衡器类型
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia

若没有指定特定端口,负载均衡器会随机选择一个端口。

$ kubectl get svc | grep load
              # <EXTERNAL IP>
kubia-loadbalancer   LoadBalancer   10.96.24.178     <pending>       80:30600/TCP   11m

2:创建服务后,要等待很长时间, 才能将 外部 IP 地址写入对象。

可通过外部 ip 直接访问:

curl http://<EXTERNAL IP>

3:可以通过 浏览器访问,但每次访问都是一个 pod, 即使 Session Affinity 设为 None

因为浏览器采用 keep-alive 连接,而 curl 每次开新连接。

4:请求连的时候, 会连接到负载均衡器的 80 端口, 并路由到某个节点上分配的 NodePort 上,随后转发到 一个 pod 实例上。

本质还是打开了 NodePort,仍能继续使用 节点IP:隐式 NodePort 端口访问:

$ curl http://192.168.49.2:30600
You've hit kubia-sxkrr
image-20210617170233160
Ingress

1:Ingress 也可对外暴露服务,准入流量控制。

2:Ingress 工作在 HTTP 层,通过一个 主机名 + 路径 就能转到不同的服务

而 每个 LoadBalancer 服务需要自己的负载均衡器和独有的 公有 IP 地址。

工作在更高层次的协议层,可以提供更丰富的服务。

ingress 可以绑定 ==多主机、同主机多路径。==

image-20210617171436236

3:启用 Ingress 资源需要 Ingress 控制器。

通过 minikube addons list 确认。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kubia
spec:
  rules:
  - host: kubia.example.com
    http:
      paths:
      - path: /
        backend:
          # 将 kubia.example.com/ 请求转发到 nodeport 服务
          serviceName: kubia-nodeport
          servicePort: 80

4:kubia.example.com 访问服务的前提是 域名能正确解析为 ingress 的 IP。

$ kubectl get ingress
NAME    CLASS    HOSTS               ADDRESS        PORTS   AGE
kubia   <none>   kubia.example.com   192.168.49.2   80      70s

然后再 /etc/hosts 加入映射:

root@ubuntu:~# cat /etc/hosts
127.0.0.1       localhost
127.0.1.1       ubuntu

192.168.49.2 kubia.example.com

通过地址访问:

# 必须 配置 hosts,直接通过 ingress IP 访问是不行的,无法知道访问的是哪个服务
$ curl http://kubia.example.com
You've hit kubia-xvkpg

5:ingress 访问流程如下:

  • 通过 DNS 查找 http://kubia.example.com 对应的 ingress IP

  • 向 Ingress 控制器发送 请求,并在头部中包含 需要访问的 服务 kubia.example.com

  • 在 Endpoints 中查看该服务对应的 pod 的 IP 表,选择其中一个 pod 进行处理;

image-20210617172908817

6:若要采用 https 访问,需要 配置 tls.secrete

针对每个主机域名 配置一个。

name: kubia
spec:
  tls:
  - hosts:
    - kubia.example.com
    secretName: tls-secret
就绪探针

1:确认服务启动后,对外提供服务。

headless 服务

有时候在集群内部/或者集群外部 需要知道 其它节点的 IP 列表,创建一个 headless 服务包裹一层, 去查询该服务的 DNS,会打印 该服务下的(标签选择器选中的)所有 pod。

1:clusterIP:None, DNS 服务器返回的是 pod 的 IP,而非集群 IP

2:创建 headless Service:

该 headless 后端,包含标签选择器选择的所有 pod

apiVersion: v1
kind: Service
metadata:
  name: kubia-headless
spec:
  # headless 服务
  clusterIP: None
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia

headless 服务无集群 IP

image-20210617174322816

运行临时 pod

kubectl run dnsutils --image=tutum/dnsutils   --command -- sleep infinitypod/dnsutils created

在该临时 pod 查看 headless 服务标签选择器选择的 pod

注意,只会展示 就绪的 pod ip

$ kubectl exec dnsutils nslookup kubia-headless
Name:   kubia-headless.default.svc.cluster.local
Address: 172.17.0.5
Name:   kubia-headless.default.svc.cluster.local
Address: 172.17.0.4
Name:   kubia-headless.default.svc.cluster.local
Address: 172.17.0.3

普通 service 返回的是集群 IP:

$ kubectl exec dnsutils nslookup kubia

Name:   kubia.default.svc.cluster.local
Address: 10.105.237.81

1:卷的作用是将 磁盘挂载到容器。

这个 和 linux 将指定目录挂载到盘 很类似。

2:每个 pod 都有独立的文件系统,文件系统来自于 容器镜像。

默认, 容器重启后并不能识别 之前容器写入文件系统的内容。

这是因为 新的容器拥有 新的 写入层。

3:pod 中的所有容器都能使用卷,但是需要提前挂载。

4:emptyDir 卷是挂载一个空的目录。

  • 卷的装载在容器启动之前执行;

  • emptyDir 卷 的生命周期 和 pod 相同;

5:可用的卷类型:

  • emptyDir —— 用于存储临时数据的简单空目录。

  • hostPath —— 用于将目录从工作节点的文件系统挂载到 pod 中。

  • gitRepo —— 通过检出 Git 仓库的内容来初始化的卷。

  • nfs —— 挂载到 pod 中的 NFS 共享卷。

  • gcePersistentDisk (Google 高效能型存储磁盘卷)、 awsElastic BlockStore (AmazonWeb 服务弹性块存储卷)、 azureDisk (Microsoft Azure 磁盘卷)一一用于挂载云服务商提供的特定存储类型。

  • cinder 、 cephf 、 iscsi 、 flocker 、 glusterfs 、 quobyte 、 rbd 、 flexVolume 、 vsphere-Volume 、 photonPersistentDis k、 scaleIO 用于挂载其他类型的网络存储。

  • configMap 、 secret 、 downwardAPI 一一用于将 Kubemetes 部分资源和集群信息公开给 pod 的特殊类型的卷 。

  • persistentVolumeClaim 一一一种使用预置或者动态配置的持久存储类型(我们将在本章 的最后一节对此展开讨论) 。

单个容器可同时使用不同类型的多个卷

emptyDir

1:emptyDir: 在 pod 中的多个容器间共享存储:

apiVersion: v1
kind: Pod
metadata:
  name: fortune
spec:
  containers:
  - image: luksa/fortune
    name: html-generator
    volumeMounts:
    - name: html
      # 挂载的目录
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      # 只读
      readOnly: true
    ports:
    # Nginx 监听80端口
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html # 卷名称
    emptyDir: {}

可在内存上创建 emptyDir

volumes:
  - name: html # 卷名称
    # emptyDir: {}
    emptyDir:
      medium: Memory
gitRepo

1:gitRepo 卷:gitRepo 卷基本上也是 一 个 emptyDir 卷,它通过克隆 Git 仓库并在 pod 启动时(但在创建容器之前 ) 检出特定版本来填充数据

创建 pod 时,会 checkout 指定版本。

apiVersion: v1
kind: Pod
metadata:
  name: gitrepo-volume-pod
spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    # 创建一个 gitRepo 卷
    gitRepo:
      repository: https://github.com/luksa/kubia-website-example.git
      revision: master
      # checkout 到当前目录, 可通过路径 /usr/share/nginx/html 访问
      directory: .

8:可创建一个 sidecar 容器,实时同步 git, 如 Docker Hub 上的 gitsync

hostPath

1:hostPath 卷,指向节点文件系统上特定的文件或目录。

注意是一些 系统级别的 pod (通常由 DaemonSet 管理)需要访问。

2:多个 pod hostPath 卷中使用相同的路径,可看到相同的文件。

image-20210617125426036

3:hostPath 是第一种持久性存储的卷。【pod 删除后依然还在】

emptyDir 和 gitRepod 随 pod 删除而删除。

4:若将数据存储到 节点上,则 pod 不能随机调度,需要调度到指定节点才行。

特别是 pod 重启时。

nfs

1:nfs 服务器可共享路径

apiVersion: v1
kind: Pod
metadata:
  name: mongodb-nfs
spec:
  volumes:
  - name: mongodb-data
    nfs:
      # 指定 nfs 服务
      server: 1.2.3.4
      # 共享路径
      path: /some/path
  containers:
  - image: mongo
    name: mongodb
    volumeMounts:
    - name: mongodb-data
      mountPath: /data/db
    ports:
    - containerPort: 27017
      protocol: TCP
PV 和 PVC

1:将卷这种持久性信息 和 pod 解耦,可避免处理基础设施细节。

2:PersistentVlume (持久卷, 简称 PV) ,由集群管理员设置的 底层存储。

3:PersistentVlumeClaim (持久卷声明,简称 PVC),用户声明需要申请的(存储容量和访问模式)

API 服务器 负责找到满足要求的 持久卷并绑定到 持久卷声明。

持久卷声明可当做 pod 中的一个卷来使用;

image-20210617130652486

4:创建 PV

apiVersion: v1

# 创建持久卷
kind: PersistentVolume
metadata:
  name: mongodb-pv
spec:
  # 持久卷大小
  capacity:
    storage: 1Gi
  # 读写模式
  accessModes:
    # 单个客户端挂载是 读写模式
    - ReadWriteOnce
    # 多个客户端挂载是 只读模式
    - ReadOnlyMany

  # 设置策略, PVC 释放后,PB将保留
  persistentVolumeReclaimPolicy: Retain

  # 在Google的 gce 磁盘上分配
  gcePersistentDisk:
    pdName: mongodb
    fsType: ext4

persistentVolumeReclaimPolicy 可指定回收策略:

  • Retain: 删除 PVC 后, PV 保留;

  • Recycle:删除内容,可再次被 PVC 绑定

  • Deleet: 删除底层存储

5: 特别注意的是:

PV:属于集群层面的资源;

PVC 和 Pod: 属于命名空间内的资源;

PVC 声明:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc
spec:
  resources:
    # 申请 容量
    requests:
      storage: 1Gi
  # 单个客户端,支持读写
  accessModes:
  - ReadWriteOnce
  # 动态配置
  storageClassName: ""

k8s 根据声明的访问权限、容量大小, 寻找满足要求的 PV

PV 和 PVC 的创建相对独立。

6:使用 PVC,只需要在 volumes 中引用即可。

apiVersion: v1
kind: Pod
metadata:
  name: mongodb
spec:
  containers:
  - image: mongo
    name: mongodb
    volumeMounts:
    - name: mongodb-data
      mountPath: /data/db
    ports:
    - containerPort: 27017
      protocol: TCP
  volumes:
  - name: mongodb-data
  # 引用 PVC
    persistentVolumeClaim:
      claimName: mongodb-pvc

7: PV 和 PVC 的解耦,存储这块,方便管理。


StorageClass

1: 创建存储类 StorageClass,可动态创建 PV。

方便集群管理员管理。

2:不指定 StorgeClass ,会使用集群默认的存储类分配。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc2
spec:
  resources:
    requests:
      storage: 100Mi
  accessModes:
    - ReadWriteOnce

  # 未指定 StorageClass,则使用默认的 StorageClass 分配

3:手动配置 的 PV 和 StorageClass 可同时存在,若不想用 StorageClass 分配时,可将 StorageClass:"" 配置为空,则将使用 预先配置的 PV 持久卷。

4:创建一个 StorageClass 对象:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast

# 创建 PV的预制程序
provisioner: k8s.io/minikube-hostpath
# 参数
parameters:
  type: pd-ssd

在 PVC 中使用 StorageClass:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc
spec:
  # 引用 StorageClass 即可
  storageClassName: fast
  resources:
    requests:
      storage: 100Mi
  accessModes:
    - ReadWriteOnce

5:整体关系如图所示:

image-20210617140107905

ConfigMap 和 Secret

ConfigMap

1:给应用传递参数,有以下几种方式:

  • 命令行参数;

docker run <image> <arguments>

在容器中启动有两种方式:

-- shell 形式一如 ENTRYPOINT node app.js。

-- exec 形式一如 ENTRYPOINT ["node", "app. js"]

前者是通过 shell 启动的, 后者是 进程直接运行。

例如 在 Dockerfile 镜像中传入:

FROM ubuntu:latest

RUN apt-get update ; apt-get -y install fortune
ADD fortuneloop.sh /bin/fortuneloop.sh

ENTRYPOINT ["/bin/fortuneloop.sh"]
# 参数列表
CMD ["10"]

在 pod 中定义容器时,可覆盖 ENTRYPOINT 和 CMD,只需要设置 command 和 args

kind:pod
spec:
 containers:
 - image: some/image
   command: ["/bin/command"]
   args: ["arg1", "arg2", "arg3"]

   # 多个参数,可用下面格式
   args:
   - foo
   - bar

   # 数值需要用引号
   - "15"
  • 环境变量;

在容器中设置环境变量如下, 可以在 shell 中直接引用该变量 $INTERVAL

kind: Pod
metadata:
  name: fortune-env
spec:
  containers:
  - image: luksa/fortune:env
    env:
    - name: INTERVAL
      value: "30"

    - name: SECOND_VAR
      # 引用第一个参数 30test
      value: "${INTERVAL}test"
  • 通过特殊类型的卷挂载到容器

卷挂载如下:

apiVersion: v1
kind: Pod
metadata:
  name: fortune-env
spec:
  containers:
  - image: luksa/fortune:env
    env:
    - name: INTERVAL
      value: "30"

    - name: SECOND_VAR
      # 引用第一个参数 30test
      value: "${INTERVAL}test"
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    emptyDir: {}
  • ConfigMap 是 k8s 中存储配置数据的资源。

  • 证书和私钥相关的配置数据,使用 Secret 资源存储。

2:ConfigMap 主要是将配置 从 pod 中解耦出来, 这样生产环境和非生产环境下的 pod 定义文件可以保持不变。

多种环境下,只要保证 ConfigMap 不同即可。

kubectl create configmap <configmap-name> --from-literal=foo=bar  # kv 结构,key 是 foo,value 是bar

kubectl create configmap <configmap-name> --from-file=customkey=config.conf  # 从文件读取 或者 文件夹读取

如:

image-20210617103038594

对应关系如下:

image-20210617103121948

3:将 configMap 条目作为环境变量:

kind: Pod
metadata:
  name: fortune-env-from-configmap
spec:
  containers:
  - image: luksa/fortune:env
    env:
    - name: INTERVAL # 设置环境变量
      valueFrom:
        configMapKeyRef: # 来自于 ConfigMap
          # 引用 config-map 的名称
          name: fortune-config
          # config-map 下的 key
          key: sleep-interval
    args: ["${INTERVAL}"]  # 作为命令行参数

引用不存在的 configMap, 容器会启动失败,从而一直重启。

4:envFrom 字段可暴露所有来自 ConfigMap 的变量,并可在变量名引入后加入 前缀。

5:可将 configMap 中的条目挂载到指定目录下 【每个 key 作为 文件存在】

kind: Pod
metadata:
  name: fortune-configmap-volume
spec:
  containers:
  - image: luksa/fortune:env
    env:
    - name: INTERVAL
      valueFrom:
        configMapKeyRef:
          name: fortune-config
          key: sleep-interval
    name: html-generator
    volumeMounts:
    - name: html
    # 将卷中的条目,挂载至该目录下,条目的 名称就是该目录下的文件名
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    - name: config
      mountPath: /etc/nginx/conf.d
      readOnly: true
    - name: config
      mountPath: /tmp/whole-fortune-config-volume
      readOnly: true
    ports:
      - containerPort: 80
        name: http
        protocol: TCP
  volumes:
  - name: html
    emptyDir: {}
  - name: config
    #  configMap 卷
    configMap:
      name: fortune-config

6:注意:configMap 挂载到已存在的文件夹,会隐藏所有已有的条目。

可以选择挂载部分卷,避免隐藏整个文件夹。

spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      # 只挂载 configmap 中的指定条目
      subPath: myconfig.conf

configMap 卷中的文件权限默认设置为 644 (-rw-r-r--),

可通过 defaultMode 修改

7:ConfigMap 通过暴露卷,可以达到配置热更的效果,无需新建 pod 或 重启容器。

Secret

1:和 configMap 一样, secret 也是 key-val 存储。

2:使用 和 ConfigMap 相同:

  • 将 Secret 条目作为环境变量传递给容器

  • 将 Secret 条目暴露为卷中的文件

Secret 只会存储在节点的内存中, 永不写入物理存储,

3:使用场景:

  • 采用 ConfgMap 存储非敏感的文本配置数据。

  • 采用 Secret 存储天生敏感的数据, 通过键来引用。如果一 个配置文件同时包 含敏感与非敏感数据, 该文件应该被存储在 Secret 中。

4:Secretes 一般包含三种文件:

  • ca.crt

  • namespace

  • token

user00@ubuntu:~$ kubectl describe secrets
Name:         default-token-c2v26
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: default
              kubernetes.io/service-account.uid: 1fe33279-b738-4a3b-a012-5c5a709cdcb8

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1111 bytes
namespace:  7 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6IjVVWWRld2FCX0tLbGJFZFdBd3JBcncxRmxQRnBGMTFaRDZfM2FJWTVra0kifQ.

Secret 条目的内容会以 Base64 编码

5:挂载 Secrete 卷:

kind: Pod
spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: certs

      # 挂载证书的路径
      mountPath: /etc/nginx/certs/
      readOnly: true
    ports:
    - containerPort: 80
    - containerPort: 443
  volumes:

  # 引用 secret 卷
  - name: certs
    secret:
      secretName: fortune-https

6: k8s 允许通过环境变量暴露 Secret,但是不安全。

env:
 -name: FOO_SECRET
 valueFrom:
  secretKeyRef:
      # secret 资源名称
   name: fortune-https
   # 引用的条目
   key:foo

7:从 Docker Hub 网站拉取私有 Docker 镜像仓库时,需用 Secret 鉴权

$ kubectl create secret docker-registry mydockerhubsecret \
--docker-username=myusername --docker-password=mypassword \
--docker-email=my.email@provider.com

8:Pod 中使用 Secret。

kind: Pod
metadata:
  name: private-pod
spec:
  # 引用  Secret
  imagePullSecrets:
  - name: mydockerhubsecret
  containers:
  - image: username/private:tag
    name: main

若运行大量 pod,可通过将 Secret 添加到 ServiceAccout, 所有 pod 只要使用了 sa,都能自动添加上镜像拉取的 Secret.

从应用访问 pod 元数据及其它资源

Downward API

1:传递不能提前知道的数据,如 pod 的 IP、主机名

或者在别处预定义的数据,如 pod 标签和注解。

2:Downward API 不像 REST Endpoint 那样需要通过访问的方式获取数据;

通过将 pod 中取得的数据作为环境变量 或 文件的值(Downward API 卷)对外暴露。

注意:标签和注解只能通过卷暴露

3:Downward API 可以给容器传递的 元数据有:

  • pod 的名称

  • pod 的 IP

  • pod 所在的命名空间

  • pod 运行节点的名称

  • pod 运行所归属的服务账户的名称

  • 每个容器请求的 CPU 和内存的使用量

  • 每个容器可以使用的 CPU 和内存的限制

  • pod 的标签

  • pod 的注解

4:使用元数据作为环境变量:

apiVersion: v1
kind: Pod
metadata:
  name: downward
spec:
  containers:
  - name: main
    image: busybox
    command: ["sleep", "9999999"]
    resources:
      requests:
        cpu: 15m
        memory: 100Ki
      limits:
        cpu: 100m
        memory: 4Mi

    # 设置环境变量
    env:
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name  # 应用 pod 中的元数据名称字段
    - name: POD_NAMESPACE
      valueFrom:
        fieldRef:
          fieldPath: metadata.namespace
    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP
    - name: NODE_NAME
      valueFrom:
        fieldRef:
          fieldPath: spec.nodeName
    - name: SERVICE_ACCOUNT
      valueFrom:
        fieldRef:
          fieldPath: spec.serviceAccountName
    - name: CONTAINER_CPU_REQUEST_MILLICORES
      valueFrom:
        # 注意: CPU 和 内存的引用使用 resourceFieldRef, 而非 fieldRef
        resourceFieldRef:
          resource: requests.cpu
          # 基数单位是 1 毫核,1/1000 核
          divisor: 1m
    - name: CONTAINER_MEMORY_LIMIT_KIBIBYTES
      valueFrom:
        resourceFieldRef:
          resource: limits.memory
          divisor: 1Ki

对应关系如下:

image-20210616211558165

5:挂载 downwardAPI 卷暴露 【推荐】

apiVersion: v1
kind: Pod
metadata:
  name: downward

  # 将会在 Downward  卷暴露的元数据
  labels:
    foo: bar
  annotations:
    key1: value1
    key2: |
      multi
      line
      value
spec:
  containers:
  - name: main
    image: busybox
    command: ["sleep", "9999999"]
    resources:
      requests:
        cpu: 15m
        memory: 100Ki
      limits:
        cpu: 100m
        memory: 4Mi
    volumeMounts:
    - name: downward
      # downwardAPI 挂载的目录
      mountPath: /etc/downward
  volumes:
  - name: downward
    downwardAPI:
      items:
      # pod 的名称会写入文件 /etc/downward/podName
      - path: "podName"
        fieldRef:
          fieldPath: metadata.name

      # 写入文件 /etc/downward/podNamespace
      - path: "podNamespace"
        fieldRef:
          fieldPath: metadata.namespace

      # 写入文件 /etc/downward/labels
      - path: "labels"
        fieldRef:
          fieldPath: metadata.labels

      # 写入文件 /etc/downward/annotations
      - path: "annotations"
        fieldRef:
          fieldPath: metadata.annotations

      # 写入文件 /etc/downward/containerCpuRequestMilliCores
      - path: "containerCpuRequestMilliCores"
        resourceFieldRef:
          # 资源相关的必须指定容器名称,因为卷 是 pod 级别的
          containerName: main
          resource: requests.cpu
          divisor: 1m

      # 写入文件 /etc/downward/containerMemoryLimitBytes
      - path: "containerMemoryLimitBytes"
        resourceFieldRef:
          containerName: main
          resource: limits.memory
          divisor: 1

文件列表的对应关系:

image-20210616212130889

6:推荐用卷的方式暴露,在运行时修改 注解 或 标签, k8s 会更新相关的文件;

且卷能在 同 pod 的多容器间传递;

但环境变量一旦设置,修改后无法暴露新值。

7:downward API 只能对 pod 内的容器暴露数据,有一定的局限性;

与 k8s API 服务器交互

1:k8s API 可获取其它 pod 或集群中其它资源信息;

2:可以通过 kube proxy 启动一个代理服务接收本地的 http 连接并转发至 API 服务器。

同时处理身份认证, 所以不需要每次请求都上传认证凭证。它也可以确保我们直接与真实的 API 服务器交互 , 而不是一个中间入(通过每次验证服务器证书的方式)

kube proxy 会捎带 API 服务器的 URL 、认证凭证等。

代理服务器 在本地的 8001 端口接收请求。

$ curl localhost:8001
{
"paths": [
"/api",
"/api/vl",

3:可以通过 URL 查看对应资源的 API, 例如 Job 资源的 API,分组在 /apis/batch

例如:

image-20210616213620233

4:从 pod 内部访问 API 服务器,需要带上 凭证和授权:

image-20210616213916792

后续可通过 ServiceAccount 和 RBAC 解决账户和授权的问题。

5:Pod 与 API 服务器的交互如下:

  • 应用应该验证 API 服务器的证书是否是证书机构所签发, 这个证书是在 ca.crt 文件中。

  • 应用应该将它在 token 文件中持有的凭证通过 Authorization 标头来获得 API 服务器的授权。

  • 当对 pod 所 在 命 名空间的 API 对 象进行 CRUD 操作时, 应 该使 用 namespace 文件来传递命名空间信息到 API 服务器

CRUD 代表创建、 读取、 修改和删除操作, 与之对应的 HTTP 方法分别是 POST、 GET、 PATCH/PUT 以及 DELETE。

可在 pod 中运行一个 sidecar 容器(代理服务器);

  • 主容器 通过 端口访问 代理 容器 【同一网络命名空间】

  • 代理服务器 运行 kubectl-proxy 命令,实现和 API 服务器通信;

image-20210616214346169

代理容器如下,

apiVersion: v1
kind: Pod
metadata:
  name: curl-with-ambassador
spec:
  containers:
  - name: main
    image: curlimages/curl:7.77.0
    command: ["sleep", "9999999"]
  - name: ambassador
    # 代理容器,在该容器中, 可 通过 curl localhost:8001  访问 API 服务器
    image: luksa/kubectl-proxy:1.6.2

由于主容器和 sidecar 容器共享网络命名空间, 直接可以访问 代理服务器的 8001 端口。

image-20210616214821893

6:也可使用标准的 客户端库与 API 服务器交互。

Deployment

1:Deployment 是基于 ReplicaSet 资源, 声明式的升级应用 【滚动更新】

image-20210616202200622

Deployment 在上层 控制期望的状态,更新时,会创建 新的 ReplicaSet 资源

2:通常有两种更新方式:

  • 直接删除所有现有的 pod, 然后创建新的 pod。

    • 停机更新,短时间不可用

使用 ReplicationController 管理,修改 replicas 副本数即可。

image-20210616201348186
  • 也可以先创建新的 pod, 并等待它们成功运行之后, 再删除旧的 pod。可以先创建所有新的 pod, 然后一 次性删除所有旧的 pod, 或者按顺序创建新的 pod, 然后逐渐删除旧的 pod。

    • 需要支持两个版本同时对外提供服务

第一种方法:先创建新的,可用后,一次性删除旧的 【蓝绿部署】:

image-20210616201709906

第二种方法:手动执行滚动升级 【旧的副本数在减少,新的副本数在增加。】:

image-20210616201824859

3:创建一个 Deployment 示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kubia
spec:
  # 目标副本数
  replicas: 3

  # 新的 pod 模板
  template:
    metadata:
      name: kubia
      labels:
        app: kubia
    spec:
      containers:
      - image: luksa/kubia:v1
        name: nodejs

  # 需要更新的 pod
  selector:
    matchLabels:
      app: kubia

4:Deployment 提供两种升级策略:

  • 1:滚动更新, RollingUpdate;

升级过程中 速度可控:minReadySeconds 可控制滚动更新的速度

minReadySeconds 要求 pod 至少运行多久才算可用。在 新 pod 可用之前, maxUnavailable 会卡主更新。

若在 minReadySeconds 时间内,就绪探针失败, 新版本滚动会停止。

pod 未就绪时,新的请求也不会 分发到上面。

image-20210616204948017

触发更新如下, 直接设置新的镜像版本即可:

kubectl set image deployment/replicationcontroller/replicaset <资源名> <container-name>=<ID>/<image>:<tag>: 修改 资源下容器里的 镜像,会修改 pod 模板; 并 触发 Deployment 的 滚动升级
  • 2:一次性删除所有旧的,然后创建新的, Recreate

5:Deployment 升级完后,旧的 ReplicaSet 还会保留,方便出错之后 回滚。

image-20210616203617762

可以回滚到指定的版本,保留的历史版本数 通过 revisionHistoryLimit 属性控制:

image-20210616203753923

6:控制滚动 更新速度的两个参数:

  • maxSurge: 某一时刻,最多运行的 pod 数量(包括 新版本下的 pod 和旧版本下的 pod 数总和), 控制 每次新版本 增加的 pod 数

  • maxUnavaliable: 滚动期间,最大不可用 pod 数量,控制必须得保留一定量 旧版本的 pod 树;

image-20210616204222968

假设 集群目标数量是 3, maxSurge 允许最多 pod 数量达到 4, 同时 maxUnavailable = 0(任意时刻, 必须至少有 3 个可用)

3 个副本数下的更新过程:

image-20210616204359032

7:升级过程中,可以暂停滚动升级(发布金丝雀版本进行测试),但在哪个时刻暂停无法控制。

功能验证后,可恢复滚动升级。

8:可以通过 progressDeadlineSeconds 属性指定滚动升级必须在 多长时间 完成,否则视为失败。

部署有状态多副本

1:有状态服务通常需要考虑:

  • 有状态服务需要有独立的存储:

  • 不变的主机名和 ip 地址

StatefulSet

1:StatefulSet 可以保证重启一个 pod 实例,拥有和之前一样的 名称 和 主机名。

2:StatefulSet 可以保证每个 pod 都有稳定的名字和状态;

  • RS 分配的主机名都是随机的(默认平等)

  • StatefulSet 分配主机名是按顺序递增的

3:pod 重启后,并不保证在原来的节点上:

但 主机名和 ip 保持不变

4:StatefulSet 缩容时,会优先删除 高索引主机名的 实例 (如下,第一次缩容,Pod A-2 最先被删除。)

5:为每个 pod 声明单独的 PVC,提供独立的存储

缩容时,只会删除 pod, PVC 默认不会删除(除非手动),当下次重新扩容时, 会绑定到之前的 PVC

可以保证扩容出来的 pod 还是写相同的文件。

6:StatefulSet 实例,创建三个对象

  • 创建 PV

kind: List
apiVersion: v1
items:
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-a
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    gcePersistentDisk:
      pdName: pv-a
      fsType: ext4
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-b
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    gcePersistentDisk:
      pdName: pv-b
      fsType: ext4
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-c
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    gcePersistentDisk:
      pdName: pv-c
      fsType: ext4

  • 创建一个 控制 Service

创建一个 headless Service, 可以让 pod 之间彼此发现。

通过这个 Service, 每个 pod 都有 独立的 DNS 记录, 可以通过主机名方便的找到它。

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  # StatefulSet 的控制 Service 必须是 headless 模式
  clusterIP: None

  # 所有 带有 app: kubia 标签的pod 都属于这个 service
  selector:
    app: kubia
  ports:
  - name: http
    port: 80

可以通过在 pod 中触发一次 SRV DNS 查询,获取其它 pod 列表

有时不需要或不想要负载均衡,以及单独的 Service IP。遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP)的值为 "None" 来创建 Headless Service。

你可以使用无头 Service 与其他服务发现机制进行接口,而不必与 Kubernetes 的实现捆绑在一起。

对这无头 Service 并 不会分配 Cluster IP,kube-proxy 不会处理它们, 而且平台也不会为它们进行负载均衡和路由。DNS 如何实现自动配置,依赖于 Service 是否定义了选择算符。

带选择算符的服务

对定义了选择算符的无头服务,Endpoint 控制器在 API 中创建了 Endpoints 记录, 并且修改 DNS 配置返回 A 记录(IP 地址),通过这个地址直接到达 Service 的后端 Pod 上。

无选择算符的服务

对没有定义选择算符的无头服务,Endpoint 控制器不会创建 Endpoints 记录。然而 DNS 系统会查找和配置,无论是:

  • 对于 ExternalName 类型的服务,查找其 CNAME 记录

  • 对所有其他类型的服务,查找与 Service 名称相同的任何 Endpoints 的记录

  • StatefulSet 本身

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kubia
spec:
  serviceName: kubia

  # 创建2个副本,pod 上带有 标签 app: kubia
  replicas: 2
  selector:
    matchLabels:
      app: kubia # has to match .spec.template.metadata.labels
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia-pet
        ports:
        - name: http
          containerPort: 8080
        volumeMounts:
        - name: data
          mountPath: /var/data

  # 创建 pvc 模板, 生成的pvc  data-<主机名>
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      resources:
        requests:
          storage: 1Mi
      accessModes:
      - ReadWriteOnce

7:StatefulSet 对集群同时启动两个 pod 非常敏感,可能存在竞争。

依次启动 是比较安全可靠的:所以后面的 pod 会等待前面的 pod 成为就绪状态后 创建。

8:StatefulSet 修改模板文件后,不会自动触发运行的 pod 更新。

和 ReplicaSet 一样,重启更新。

9:当 pod 突然失效时(NotReady), StatefulSet 不会立刻驱逐. 需要等足够多的时间,或者显示的删除 该 pod,才能触发 重新调度。

简言之, StatefulSet 会避免同时运行两个一样的 pod.

kubernetes 机理

架构

1:k8s 集群分两部分:

  • master node (The k8s Control Plane, 控制面板):存储和管理集群的状态

    • etcd 分布式持久化存储

    • API 服务器

    • 调度器

    • 控制器管理器

  • work node

    • Kubelet

    • Kubelet 服务代理( kube-proxy)

    • 容器运行时(Docker、rkt 或者其他)

  • 附加组件

    • KubemetesDNS 服务器:通过 IP 对外暴露 HTTP 服务;

    • 仪表板

    • Ingress 控制器:对客户端 ip 保存,后续多次连接路由到 同一个 pod

    • Heapster(容器集群监控)

    • 容器网络接口插件

整体组件如下:

kubectl get componentstatus :显示控制面板各个组件的状态

2:系统组件只能通过 API 服务器进行通信, 相互之间不同通信。

3:etcd 和 API 服务器可以有多个实例 同时并行工作。【分布式集群】

为了保证集群一致性,采用 RAFT 算法。

但 调度器 和 控制器 在某一个时刻,只能有一个 实例起作用,其它实例处于 待命状态。

4:kubelet 是唯一一直作为常规系统运行的组件。

5: API 服务器, 可查询、修改集群状态的 CRUD (Create、Read、Update、Delete),并最终存入 etcd .

对请求的 Rest 进行校验。

  • Authentication plugin: 认证插件,获取用户、用户组 等信息;

  • Authorization plugin:授权插件,是否有权限对指定资源进行 指定的操作;

  • Admission Control plugin: 准入插件控制,例如 资源限制 ResourceQuota 等

准入控制插件包括:• AlwaysPullImages:重写 pod 的 imagePullPolicy 为 Always, 强制每次部署 pod 时拉取镜像。• ServiceAccount:未明确定义服务账户的使用默认账户。• NamespaceLifecycle:防止在命名空间中创建正在被删除的 pod, 或在不存在的命名空间中创建 pod。• ResourceQuota:保证特定命名空间中的 pod 只能使用该命名空间分配数量的资源, 如 CPU 和内存。

控制器可通过定期的去拉取 API 服务器信息,监听资源的变化。

6:调度器:为 pod 选择合适的节点

  • 最简单的是随机选择一个;

  • 以更优的方式选择一个, 对所有节点按优先级排序,找出最优节点(若分数一致,则循环分配)

筛选哪些节点可用:

  • 节点是否能满足 pod 对硬件资源的请求。

  • 节点是否耗尽资源(是否报告过内存/硬盘压力参数) ?

  • pod 是否要求被调度到指定节点(通过名字), 是否是当前节点?

  • 节点是否有和 pod 规格定义里的节点选择器一致的标签(如果定义了的话) ?

  • 如果 pod 要求绑定指定的主机端口(第 13 章中讨论)那么这个节点上的这个端口是否已经 被占用?

  • 如果 pod 要求有特定类型的卷, 该节点是否能为此 pod 加载此卷, 或者说该节点上是否已经有 pod 在使用该卷了?

  • pod 是否能够容忍节点的污点,涉及污点和容忍度。

  • pod 是否定义了节点、pod 的亲缘性以及非亲缘性规则?如果是, 那么调度节点给该 pod 是否会违反规则?

集群中可运行 多个调度器 而非单个, 设置 schedulerName 来调度特定的 pod

7: 控制器:控制集群服务器的状态 朝 API 服务器定义的期望状态 收敛。

控制器包括

  • Replication 管理器 (ReplicationController 资源的管理器):监听 pod 的数量

  • ReplicaSet、 DaemonSet 以及 Job 控制器:部署和维护 pod

  • Deployment 控制器:控制滚动更新,每个版本,都会创建一个 ReplicaSet

  • StatefulSet 控制器: 有状态服务的管理,挂载到相同的 PV,相同的 IP 和主机名

  • Node 控制器:管理 Node 资源,监控 每个 Node 的健康状态;

  • Serice 控制器:网络管理相关组件,创建或删除 LoadBalancer 类型服务;

  • Endpoints 控制器:从 Service 的 pod 选择器中选出指定的 pod,并将 IP 和端口更新到 Endpoint 资源中;

  • Namespace 控制器:创建 或 删除 Namespace 对象;

  • PersistentVolume 控制器:创建一个 PVC 后,由 该控制器找到一个 合适的 PV 绑定。【存储量大于 PVC 声明的 最小 PV】

  • 其他

控制器就是活跃的 Kuberetes 组件, 去做具体工作部署资源。

控制器通过监听 API 资源,作出相应的调整,如 更新、删除已有对象。

控制器之间不会直接通信, 每个控制器都会连接到 API 服务器。

8:kubelet: 监控 API 服务器 是否在当前节点 新分配了 pod,告知 容器运行时(如 Docker) 运行容器。

9:kube-proxy: 通过 修改 iptables ,将请求重定向到 服务器。

userspace 代理模式如下, 对每个进来的连接,代理到一个 pod

现在是 默认的 iptables 模式。

10:控制器之间是相互协作的,通过监听 API 服务器来判断 是否要创建 / 删除 资源。

如下 是创建一个 Deployment 资源的事件链:

11:在每个 pod 中会有一个 基础容器(处于 pending 状态),用于保存 Linux 命名空间,当容器被重启时,需要保持和之前的命名空间一样, 这个 pod 就发挥了作用。

网络通信

1:同节点的 pod 之间通信。

基础容器启动前,会为容器创建一个 虚拟 Ethernet 接口对 (veth pair):

  • 一端在 node 节点的命名空间中:vethXXXX

  • 一端在容器网络命名空间中:eth0

只要连接到 同一 网桥,相互之间就能通信。

image-20210616165429183

2:不同节点的 pod 之间通信,两个节点之间需要连接网桥

连接网桥的方式有:

  • overlay

  • underlay 网络

  • 常规的三层路由

跨节点网桥必须使用 非重叠地址段, 保证不同的 pod 有不同的 IP

以下节点需要配置路由 或者 连接到相同网关【中间无路由】,否则会因为 pod IP 是局域私有的,会丢包。

使用 SDN (软件定义网络) 技术,可以忽略底层网络拓扑,所有节点就像连接到同一个网关;

pod 发出的报文会被封装,到达其它节点,会被解封装;

服务

1:和 Service 相关的操作 都是由 每个 节点上的 kube-proxy 进行处理的。

2:每个 service 都有一个稳定的 IP 地址 和端口 对;【针对多端口 Service 有多个 IP:端口对】

单独的服务 IP 无任何意义,不能 ping

3:创建一个 Service,会发生以下事件链:

  • 当创建一个服务时, 虚拟 IP 地址会分配给它

  • API 服务器 会通知所有 节点上的 kube-proxy, 有个新的服务创建了, 修改 iptables, 让服务在字节所在的节点可寻址;【修改目的地址 ,重定向到其中的一个 pod】

  • kube - proxy 还会监听 Endpoint 对象的更改

    • Endpoint 保存了所有 pod 的 ip:端口 对 【每次创建 或 删除 pod,都会影响 Endpoint】

下图中 pod A 请求 pod B 时, 会随机选择一个 pod(假设 pod B2 选中), 根据 iptables 规则,目标地址被修改为 pod B2 的 ip:端口

高可用集群

1:应用高可用:分布式集群;

2:主节点高可用:部署成集群

  • API server 通过负载均衡器选择,同时并行工作;

  • 控制器和调度器:领导者选举一个运行;

image-20210616172020019

kubernetes API 服务器的安全防护

认证机制

1:pod 与 API 服务器进行通信时,会经过 API 服务器的 认证插件

该插件会根据 证书+token 或 HTTP 用户验证 提取 客户端的 用户名、用户 ID 和 组信息。

2:连接 API 服务器有两种客户端:

  • 真实的用户;

  • 运行在 pod 中的应用;

3:系统内置的组 有特殊含义

  • system:unauthenticated 组用于所有认证插件都不会认证客户端身份的 请求。

  • system:authenticated 组会自动分配给一个成功通过认证的用户。

  • system:serviceaccounts 组包含所有在系统中的 ServiceAccount 。

  • system:serviceaccounts:<namespace>组包含了所有在特定命名空间中的 ServiceAccount。

ServiceAccount

1:ServiceAccount 也是一种资源(简称 sa), 会为每个命名空间自动创建一个 默认的 ServiceAccount

主要用于 客户端身份认证,省去 手动传 token

2:pod 只能使用 同一命名空间的 ServiceAccount

  • 可单独使用一个 ServiceAccount

  • 也可和同命名空间的 其它 pod 共用 ServiceAccount

3:可在 pod 中显示指定使用的 ServiceAccount, 否则使用该命名 空间下 默认的 sa.

添加注解 kubernetes.io/enforce-mountable-secrets= "true", 可强制 pod 只允许挂载 ServiceAccount 中的秘钥

4:ServiceAccount 的镜像拉取秘钥, 用于拉取容器镜像的凭证。

注意:所有使用 该 ServiceAccount 的 pod 都会拥有这个秘钥,而不必每个单独添加。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-service-account
imagePullSecrets:
  # 所有使用 该 ServiceAccount的 pod 都会拥有这个秘钥
- name: my-dockerhub-secret

5:在 pod 中使用 serviceAccount

apiVersion: v1
kind: Pod
metadata:
  name: curl-custom-sa
spec:
  # 若不显示声明,则使用 默认的 serviceAccount
  serviceAccountName: foo
  containers:
  - name: main
    image: tutum/curl
    command: ["sleep", "9999999"]
  - name: ambassador
    image: luksa/kubectl-proxy:1.6.2

ServiceAccount 账户 产生的 Token 在 /var/run/secrets/kubernetes.io/serviceaccount/ 目录。

未使用 RBAC 授权插件, ==默认的 serviceaccount 和 显示创建的 serviceaccount 都允许 执行任何操作。==

RBAC

1:RBAC:基于 角色的权限访问控制插件;

开启 RBAC 后,未经授权的 serviceAccount 或 默认的 serviceAccount 禁止查看集群状态。

2:RBAC 用来设置一个用户、ServiceAccount 或者一组用户,控制该角色在特定资源上 能否执行动作的权限,比如以下动作:

use 动词用于 PodSecurityPolicy 资源。

3:RBAC 授权规则是通过四种资源来进行配置的, 它们可以分为两个组

  • Role( 角色)和 ClusterRole (集群角色), 它们指定了在资源上可以执行哪些动词。

    • 命名空间 范围内的资源

  • RoleBinding (角色绑定) 和 ClusterRoleBinding (集群角色绑定), 它们将上述角色绑定到特定的用户、 组或 ServiceAccounts 上。

    • 集群级别 的资源

角色定义了可以做什么操作,而绑定定义了谁可以做这些操作

需要注意的是,RoleBinding 也可以引用 不在命名空间中的集群角色。

Role 和 RoleBinding 都在命名空间中, ClusterRole 和 ClusterRoleBinding 不在命名空间中。

4:启用 RBAC,一旦开启,禁止未授权的 serviceAccount 查看/修改 资源。

kubectl delete clusterrolebinding permissive-binding: 重新启用 RBAC

5:Role: 哪些操作可以在 哪些资源上执行。【授权某些资源的 操作权限。】

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  # Role 所在的命名空间,若没指定,默认是当前命名空间
  namespace: foo
  name: service-reader
rules:
# service 是核心 apiGroup 资源,无 apiGroup 就是 ""
- apiGroups: [""]
  # 允许执行的 操作
  verbs: ["get", "list"]

  # 该规则和服务相关 (复数)
  resources: ["services"]

在本例中,你允许访 问所有服 务资原,但是也可以通过额外的 resourceNames 字段指定服务实例的名称来限制对服务实例的访问。

Role 中的声明,表明 能 get 和 list 中的 service 资源。【默认是禁止的】

image-20210616143601754

6:rolebinding, 将 Role 中允许的权限 绑定到指定的账户上。

kubectl create rolebinding <rolebinding-name> --role=<role-name> serviceaccount=foo:deault -n foo: 创建 rolebinding 资源,绑定到 foo 命名空间中, default  的 ServiceAccount 账号上

--user :绑定到用户

--group: 绑定 Role 到组

RoleBinding 也能绑定到其它 命名空间 的 serviceAccount。总之,绑定到哪个账户,就赋予哪个账户权限。

如下,RoleBinding 绑定到 bar 命名空间的 sa 账户时,也能查看 services。

7:ClusterRole 和 ClusterRoleBinding 属于集群级别的资源管理

以下两种情况,需要使用 集群级别的授权:

  • 1:当允许跨命名空间访问资源时, 每次扩展,都需要为新的命名空间 添加 Role 和 RoleBinding 【命名空间与命名空间之间会相互绑定】

  • 2:有些资源不在命名空间中: Node、PersistentVolume、Namespace 等

    • 非资源的 URL 路径 (/healthz)

默认的 ClusterRole 都以 system: 为前缀

注意:sa 账户 可以通过 RoleBinding 绑定 到 ClusterRole 上,但是无法 访问集群级别的资源

只能访问指定命名空间中的资源。

只有 通过 ClusterRoleBinding 才能访问集群级别的资源。

非资源型的 URL 在 ClusterRole 中 使用 的是 URL 路径 而非资源。

通过 ClusterRoleBinding 绑定后,也能访问。

以下是一个 ClusterRole 示例。

8:使用 ClusterRole 授权访问指定命名空间中的资源。

  • ClusterRoleBinding --- 绑定 --- ClusterRole: 可以发查看所有命名空间、集群中的资源;

  • RoleBinding --- 绑定 --- ClusterRole: 只能查看 绑定的 RoleBinding 命名空间中的资源

9:Role 和 Binding 的组合,特别 注意 倒数第二条

节点和网络安全

1:pod 可以访问 宿主 node 的资源。

使用节点的 Linux 命名空间
使用节点的网络命名空间和端口

1:默认 每个 pod 拥有自己的 IP 和 端口空间。

设置 pod.spec.hostNetwork:true , pod 使用节点的网络接口,而无自己的 IP 地址。

端口也会绑定到主节点

2:如下, pod B 和 节点公用 ip: 端口空间,

3:一般 master 节点上 部署的 node 经常会开启 pod.spec.hostNetwork:true

仅绑定节点的端口

1:仅仅绑定节点的端口,而让 pod 继续有自己的网络命名空间。

2:设置 pod.spec.hostPort:true

使用 hostPort 和 NodePort 有两点不同:

  • 到达节点端口的连接

    • hostPort: 会直接转发 到 pod 对应的端口上;

    • NodePort: 随机选择一个 pod

  • 作用范围

    • hostPort: 仅有 运行了 hostPort 配置的 pod 才会绑定对应的端口,未运行则不绑定(Node 3)

    • NodePort: 集群中的所有节点 都会绑定。

3:若采用 hostPort 公用主机端口,则在一个节点上,一个端口只允许绑定一次。

若在调度的时候,多个 pod 需要绑定到同一端口, 则控制器会分散到不同的 节点。

若无足够多的节点,pod 会保持 pending 状态

4:同样的,设置 hostPID: 可共用 Node 的 进程树空间。

hostIPC: 可通过 IPC 进行进程间通信

配置节点安全上下文

1: 配置节点的安全上下文,可通过 securityContext 设置。

配置安全上下文可以允许你做很多事 :

  • 指定容器中运行进程的用户(用户 ID )。

  • 阻止容器使用 root 用户运行(容器的默认运行用户通常在其镜像中指定,所以可能需要阻止容器 以 root 用户运行〉。

  • 使用特权模式运行容器,使其对宿主节点的内核具有完全的访问权限 。

  • 与以上相反,通过添加或禁用内核功能,配置细粒度的内核访问权限。

    • 修改系统时间

  • 设置 SELinux (Security Enhaced Linux , 安全增强型 Linux )边项,加强对容器的限制。

  • 阻止进程写入容器的根文件系统

  • 同 pod 多容器下,多用户共享存储卷。

    • fsGroup 属性, 在创建文件时起作用

    • supplementalGroups 属性定义了某个用户所关联的额外的用户组。

PodSecurityPolicy

1: PodSecurityPolicy 是集群级别的资源, 限制用户 在 pod 中能否使用安全相关的特性。

2:PodSecurityPolicy 可以做的事项:

  • 是否允许 pod 使用宿主节点的 PID、 IPC、 网络命名空间

  • pod 允许绑定的宿主节点端口

  • 容器运行时允许使用的用户 ID

  • 是否允许拥有特权模式容器的 pod

  • 允许添加哪些内核功能, 默认添加哪些内核功能, 总是禁用哪些内核功能

  • 允许容器使用哪些 SELinux 选项

  • 容器是否允许使用可写的根文件系统

  • 允许容器在哪些文件系统组下运行

  • 允许 pod 使用哪些类型的存储卷

NetworkPolicy

1: NetworkPolicy 用于限制 pod 与 pod 之间的通信。网络 隔离 组件。

  • ingress: 允许访问这些 pod 的源地址;【入向规则,能被 哪些 pod/命名空间下的 pod/IP 段 访问】

  • egress: 这些 pod 可以访问的 目标地址;【出向规则, 该 pod 只能与 哪些 pod 通信

2:可选定 pod 的范围:

  • 标签选择器 选出的 pod;

  • 一个 namespace 中的所有 pod;

  • 无类别域间路由 (Classes Inter-Domain Routing, CIDR) 指定的 IP 段;

3:由于 NetworkPolicy 是网络隔离组件, 该命名空间下,pod 无法访问。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  # 空的标签选择器 匹配 命名空间内的 所有pod
  podSelector:

4:即使其他 pod 通过 service 访问,依然会被 NetworkPolicy 隔离。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-netpolicy
spec:
  podSelector:
    # 标签为 app=database 的 pod 设置了访问权限
    matchLabels:
      app: database
  ingress:
  - from:
    # 只对 app=webserver 的 pod 开放了 5432 端口
    - podSelector:
        matchLabels:
          app: webserver
    ports:
    - port: 5432

5:在不同的命名空间之间隔离。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: shoppingcart-netpolicy
spec:
  podSelector:
    # 限定 pod 的范围
    matchLabels:
      app: shopping-cart
  ingress:
  - from:
    # 只对以下命名空间 开放了 80 端口
    - namespaceSelector:
        matchLabels:
          tenant: manning
    ports:
    - port: 80

6: CIDR 表示法:设置一个 IP 段

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ipblock-netpolicy
spec:
  podSelector:
    matchLabels:
      app: shopping-cart
  ingress:
  - from:
    # 限定 只有  192.168.1.1 ~ 192.168.1.255 的 pod 可以访问
    - ipBlock:
        cidr: 192.168.1.0/24

7:出向规则 egress: 指定 pod 只能对外 访问 哪些 pod。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: egress-net-policy
spec:
  podSelector:
    matchLabels:
      app: webserver
  egress:
  - to:
    # webserver 只能访问 带有 app: database 标签的 pod
    - podSelector:
        matchLabels:
          app: database
    ports:
    - port: 5432

资源管理

1:配置 pod 资源的 预期使用量和最大使用量,可保证 pod 公平使用 集群内的资源。

容器申请资源

1:requests 中可申请 CPU 和内存使用量。

  • 若不申请 CPU,极端情形,会被挂起

containers:
  - image: busybox
    command: ["dd", "if=/dev/zero", "of=/dev/null"]
    name: main
    resources:
      requests:
        # 200 毫核,单核的 1/5,1200m,则是 1.2 个核【多核CPU】
        cpu: 200m
        memory: 10Mi

top 中 CPU 的使用量率 占所有核的百分比

2:调度器在调度 pod 时,判断 pod 是否能调度到该节点的依据是根据 资源的申请量之和,而非资源的实际使用量。

3:调度器利用 pod requests 为其选择最佳节点 有两种策略:

  • LeastRequestedPriority: pod 调度到 资源使用量少的节点上

  • MostRequestedPriority: pod 调度到资源使用量高的节点上 【按节点付费时,可选择】

4:当没有合适的 节点分配 给待调度的 pod 时, pod 状态会一直卡在 Pending 状态。

但此时并未放弃,一旦有 pod 删除,调度器将收到通知, 有可能重新 将 pod 部署在上面。

5:CPU requests 会影响时间片的分配。

假设一个节点上运行两个 pod, 一个 pod 请求 200 毫核 CPU,另一个是 1000 毫核 CPU,则时间片将按照 1:5 分配。

  • 若一个 pod 空闲,另一个 pod 跑满,则另一个 pod 将占用 全部 CPU;

  • 当空闲的 pod 重新运转, 另一个 pod 的 CPU 会立刻压缩到之前的比例 【动态伸缩,提高 CPU 使用率】

6:允许自定义资源,例如 GPU。

限制容器资源

1:限制容器可以消耗资源的最大量;

特别是内存,不可压缩资源,会影响后来 pod 的内存分配。

2:未设置 requests, 则将指定与资源 limits 相同的值.

containers:
  - image: busybox
    command: ["dd", "if=/dev/zero", "of=/dev/null"]
    name: main
    resources:
      limits:
        cpu: 1
        memory: 20Mi

3: 节点中 所有 pod 的 limits 总量允许超过 节点总量 100%

4: 若内存超过物理上限,容器会被 OOM killed

若 Pod 的重启策略 restartPolicy 设置为 Always 或 OnFailuer, 容器会立刻重启。

若 pod 连续重启 CrashLoopBackOff, 下次重启时间呈 指数级避退: 10、20、40、80、160 秒,最终收敛到 300s。一旦达到 300s 间隔,后续将以 5 分钟为间隔 无限重启。

5:注意:在容器内看的内存(free -g)始终是节点的内存,而非容器的内存

无论有没有配置 CPU limits , 容器内也会看到节点所有的 CPU。将 CPU 限额配置为 1,并不会神奇地只为容器暴露一个核。CPU limits 做的只是限制容器使用的 CPU 时间 。

因此如果一个拥有 i 核 CPU 限额的容器运行在 64 核 CPU 上,只能获得 1/64 的全部 CPU 时间 。而且即使限额设置为 1 核, 容器进程也不会只运行在一个核上,不同时刻,代码还是会在多个核上执行 。

一些程序通过查询系统 CPU 核数来决定启动工作线程的数量。同样在开发环境的笔记本电脑上运行良好,但是部署在拥有更多数量 CPU 的节点上,程序将快速启动大量线程,所有线程都会争夺(可能极其)有限的 CPU 时间。同时每个线程通常都需要额外的内存资源,导致应用的内存用量急剧增加 。

pod QoS 等级

1:当资源 limits 超出节点上限时, 可指定哪些 pod 从优先级更高,当资源不足时,首先 杀掉的是 低优先级的 pod.

可给 pod 分配三种 QoS 等级:

  • BestEffort (优先级最低)

    • 没有设置任何 requests 和 limits 的 pod;

    • 无任何资源保证;

    • 最坏情况下,分不到 CPU 时间片,同时为其它 pod 释放内存时,第一批被杀死;

    • 由于无 limits,内存充足时,可使用 任意多的 内存;

  • Burstable

    • 容器的 requests 和 limits 不等

    • 只定义了 requests 的 pod

    • 部分容器 requests 和 limits 相等,部分不等

Burstable 获得它们所申请的等额资源,并可以使用额外的资源(不超过 imits ) 。

  • Guaranteed (优先级最高)

    • requests 和 limits 相等的 pod

    • CPU 和 内存都要设置 requests 和 limits

    • 每个容器都要设置 资源量 【注意是每个都要设置】

    • 每个容器的每种资源的 requests 和 limits 必须相等

这些 pod 的 容器可以使用 它所申请的等额资源,但是无法消耗更多的资源(因为它们的 limits 和 requests 相等) 。

若没显示设置 requests, 则默认和 Limits 相同。所以设置了 limits 的 pod,QoS 就是 Guaranteed

三种等级的分类:

单容器 pod 的 QoS 等级

对千多容器 pod, 如果所有的容器的 QoS 等级相同, 那么这个等级就是 pod 的 QoS 等级。如果至少有一个容器的 QoS 等级与其他不同,无论这个容器是什么等级,这个 pod 的 QoS 等级都是 Burstable 等级。

2:内存不足时, BestEffort 等级的 pod 首先被杀掉。

其次是 Burstable

最后 是 Guaranteed,只有系统进程需要内存时, 才会被杀掉。

对于 QoS 等级相同的 pod,最先被杀掉的是 实际内存占内存申请量比例更高的 pod.

如下图, pod B 虽然 requests 比 pod C 少,但使用 率高达 90%, 所以先杀掉的是 pod B.

LimitRange

1:LimitRange 给命名空间中的 pod 设置默认的 requests 和 limits

LimitRange 资源中的 limit 应用于同一 个命名空间中每个独立的 pod、 容器,或者其他类型的对象。

当为显示指定 资源 requests 时,设置默认值。

它并不会限制这个命名空间中所有 pod 可用资源的总量, 总量是通过 ResourceQuota 对象指定的,

2:LimitRange 资源被 LimitRanger 准入插件控制。

LimitRange 示例如下:

apiVersion: v1
kind: LimitRange
metadata:
  name: example
spec:
  limits:
  # 整个 pod 的资源限制
  - type: Pod
    min:
      cpu: 50m
      memory: 5Mi
    max:
      cpu: 1
      memory: 1Gi

  # 单个容器的资源限制
  - type: Container
    defaultRequest:
      cpu: 100m
      memory: 10Mi

    # limits 默认值
    default:
      cpu: 200m
      memory: 100Mi
    min:
      cpu: 50m
      memory: 5Mi
    max:
      cpu: 1
      memory: 1Gi
    # 每种资源 requests / limits 的最大比值
    maxLimitRequestRatio:
      cpu: 4
      memory: 10

  # PVC 限制
  - type: PersistentVolumeClaim
    min:
      storage: 1Gi
    max:
      storage: 10Gi

3:LimitRange 中配置的 limits只能应用于单独的 pod 或容器 。用户仍然可以创建大量的 pod 吃掉集群所有可用资源。

ResourceQuota

1: 限制命名空间中的 可用资源总量。

LimitRange 是针对单个的实体

ResourceQuota 是针对命名空间下的总量

2:对象个数配额目前可以为以下对象配置 :• pod • ReplicationController • Secret • ConfigMap • Persistent Volume Claim • Service (通 用) , 以 及两种特定类型的 Service,比如 LoadBalancer Service (services . loadbalancers )和 NodePort Service ( services.nodeports)

3: 可通过 以下四种类型 控制 Quota 作用的范围

  • BestEffort : Quota 是否应用于 BestEffort QoS 等级的 pod

注意:BestEffort 只能限制 pod 的个数。不能限制 CPU/内存的 requests 和 limits

下面的都能限制:

  • NotBestEffort :Quota 是否应用于 Burstable 和 Guaranteed QoS 等级的 pod

  • Termination:设置了 activeDeadlineSeconds 的 pod (pod 被标记为 Failed 到 真正停止前还能运行的事件)

  • NotTerminating:未 设置 activeDeadlineSeconds 的 pod

apiVersion: v1
kind: ResourceQuota
metadata:
  name: besteffort-notterminating-pods
spec:
  # 这个 Quota 值作用于 拥有 BestEffort QoS, 以及未设置 最大可存活时间的 pod
  scopes:
  - BestEffort
  - NotTerminating

  # 这种的pod 只允许存在 4 个
  hard:
    pods: 4
监控 pod 资源使用量

1:集群中,每个 Node 的 Kubelet 中 cAdvisor 代理负责收集 本节点数据;

最终汇总到 Heapster 上(也运行在一个 pod)

2: 存储历史监控数据,可用 InfluxDB

可视化套件使用 Grafana

自动横向伸缩

1:根据 CPU 使用率或其它度量指标,自动横向扩缩容。

  • pod 的横向扩缩容;

  • node 的横向扩缩容;

pod 的横向伸缩

1:横向 pod 自动伸缩是指由控制器管理的 pod 副本数量 的自动伸缩。它由 Horizontal 控制器执行, 我们通过创建 一个HorizontalpodAutoscaler (HPA)资源来启用和配置 Horizontal 控制器。

该控制器周期性检查 pod 度量, 计算满足 HPA 资源所配置的目标数值所需的副本数量, 进而调整目标资源(如 Deployment、ReplicaSet、 ReplicationController、 StateflSet 等)的 replicas 字段。

AutoScale 能避免 抖动情况下的 自动扩缩容。

2: Pod 的度量数据 通过 kubelet 上的 cAdvisor agent 采集,并汇总到 Heapster.

流程如下:

3:HPA 获得 度量值后(可以有多个度量),取每个度量下 计算的副本数 最大值作为 最终 pod 数量

度量的目标值 是一个平均值。

例如,同时按照 CPU 使用率和 QPS 指标计算。

计算公式:
目标 Replicas = (所有Pod 度量值总和) / 目标度量值 # 向上取整

4: HPA 控制器 通过 Scale 子资源 修改 部署(Deployment、ReplicaSet、ReplicationController、StatefulSet) 的 replicas 字段,由部署相关的 控制器 实现 pod 的增减。

5: 拿 CPU 使用率举例, 已经使用了 60% 的 CPU 是相对于 pod 请求量的 CPU 【并非节点 CPU 的 60% 或者 资源上限 Quota 的 60%】

pod 请求量的基准可在 pod 模板的 requests 或者 LimitRange 间接设置

  • 容器的 CPU 使用率是它实际的 CPU 使用除以它的 CPU 请求 。

  • 请求量是 ==最低标准, 相当于有下限,没上限。==

例如:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: kubia
spec:
  replicas: 3
  template:
    metadata:
      name: kubia
      labels:
        app: kubia
    spec:
      containers:
      - image: luksa/kubia:v1
        name: nodejs
        resources:
          requests:
            # 100 毫核,1/10 的CPU
            cpu: 100m

6: 创建一个 HPA 对象,并指向 该 Deployment。

kubactl autoscale deployment <deployment-name> --cpu-percent=30 -min=1 --max=5: 对指定 deployment 自动伸缩pod,使CPU使用量达到 30%, 最少1个 pod,最多5个

使用 yaml 文件定义:

注意:HPA 的目标是 Deployment, 在副本数量 replicas 更新后, Deployment 会给每个应用版本 创建一个 新的 ReplicaSet.

7:基于内存的自动伸缩比基于 CPU 的 更复杂, 因为无法强制 Pod 释放内存,除非杀死并重启应用。

k8s 1.8 开始支持基于内存的自动伸缩

8:HPA 不支持 minReplicas:0, 即不允许缩容到 0 个副本, 总得保持一个 空载(idling)。

扩缩容速度

1:HPA 单次扩容操作,至多使副本数翻倍, 如果 副本数只有 1 或 2,则最多扩容到 4 个副本。

两次扩容之间也有时间限制,只有 当 3 分钟 内没有任何伸缩操作,才会继续触发扩容。

缩容频率更低,需要 5 分钟。

Resource 度量类型

1:基于 Pod 的度量类型,例如 pod 的 QPS, 运行在 Pod 中消息队列的消息数量。

注意,此时会 从 所有 Pod 中取度量值后,看平均值 与 目标 度量对比。

2:基于 Object 的度量类型, 间接基于另一个集群对象,例如 Ingress 来伸缩 pod

image-20210615171232588

注意, HPA 只会从这个 特定的 对象中 获取单个的度量数据。【并非 基于 Pod 的从所有 对象中获取】

3:一个好的度量类型,应该是 增加副本数时,可以使度量平均值 下降。例如 CPU、QPS

内存占用 并非好的度量类型。

自动配置资源请求

1:如果新创建的 pod 的容器没有明确设置 CPU 与内存请求, 该特性即会代为设置。

这一特性由一 个叫作lnitialResources的准入控制(AdmissionControl)插件提供 。当 一个没有资源请求的 pod 被 创建时, 该插件 会根据 pod 容器的历史资源使用数据(随容器镜像、tag 而变)来设置资源请求。

如果一个容器总是内存不足, 下次创建一 个包含该容器镜像的 pod 的时候, 它的内存 资源请求就会被自动调高了

集群节点的横向伸缩

1:集群节点的横向伸缩, 解决所有节点都满了,放不下 Pod。

2:当在云服务厂商上运行集群时, Cluster Autoscaler 负责请求/释放 节点。

  • 节点资源不足, 申请节点 ;

    • 会先检查新节点有没有可能容纳这个 pod,若无法容纳,则不用启动该 node

    • 当有不同规格的节点类型时,会挑选一个最合适的节点(最差是随机选择一个)

  • 节点长时间使用率底下,下线节点;

下图显示,当没有节点分配 pod 时,Cluster Autoscaler 会申请一个新的节点。

3:归还节点:Cluster Autoscale 通过监控所有节点请求的 CPU 与 内存实现。

若某个 node 上,所有 pod 请求的 CPU、内存均达不到 50%, 该节点认定为不再需要。

以下几种情形,节点不会被归还:【如果回收,会导致服务中断】

  • 1:有系统 pod 在运行(DameonSet 部署的服务);

  • 2:非托管 pod

  • 3:本地存储的 pod

只有当节点上运行的所有 pod 能被重新调度到其它节点时, Cluster Autoscale 才会触发缩容。

缩容时,该节点 首先会被标记为不可调度 【拒绝 pod 重新调度回来】,然后 该节点上的 pod 会疏散到其它节点。

4:节点下线时,可以通过 podDisruptionBudget 资源的指定 下线等操作时需要保待的最少 pod 数量【逐步迁移】,避免服务受影响。

# 标签为 app=kubia 的 pod,至少要保证3个在运行。
kubectl create pd kubia-pdb --selector=app=kubia --min-available=3

高级调度

污点和容忍度

1:Pod 可通过 nodeSelector 和 节点亲缘性 选择在 哪些节点上部署;

2:从 node 侧,也能 限制 哪些 pod 可以部署在上面;

污点(Taints): 节点添加污点,拒绝 pod 在该节点上部署;

污点在 master-node 上用的比较多,可以限制普通 pod 部署,只有系统 pod 才能部署在上面。

除非 pod 能容忍(Toleration) 这个污点, 否则不能部署在该 node.

某些硬件只能运行特殊的 pod, 可采用这种形式

3: 通过 kubeadm 部署的集群上的 主节点, 查看其 污点:

污点的格式

<key>=<value>:<effect>

# 若value 为空
<key>:<effect>

每个污点的效果(effect) 包含三种:

  • NoSchedule 表示如果 pod 没有容忍这些污点, pod 则不能被调度到包含 这些污点的节点上。

  • PreferNoSchedule 是 NoSchedule 的一个宽松的版本, 表示尽量阻止 pod 被调度到这个节点上, 但是如果没有其他节点可以调度, pod 依然会被调度到这个节点上。

  • NoExecute 不同于 NoSchedule 以及 PreferNoSchedule, 后两者只在调度期间起作用, 而 NoExecute 也会影响正在节点上运行着的 pod。如果在一个节点上添加了 NoExecute 污点, 那些在该节点上运行着的 pod, 如果没有容忍这个 NoExecute 污点, 将会从这个节点去除。

4:查看已有的污点:

如下,

  • System pod 可以同时部署在 主节点和普通节点;

  • 但未添加容忍度(默认) 的 pod 只能部署在 普通节点;

5:节点上添加自定义污点:

kubectl taint node node1.k8s node-type=production:NoSchedule: 在节点上添加 key = node-type, value = production 的污点

6:给 pod 添加容忍度示例:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: prod
spec:
  replicas: 5
  template:
    metadata:
      labels:
        app: prod
    spec:
      containers:
      - args:
        - sleep
        - "99999"
        image: busybox
        name: main
      tolerations:
        # 和 paint 配对使用
      - key: node-type
        # 使用 Equal 或 Exists
        operator: Equal
        value: production
        effect: NoSchedule

7: 配置 node 不可用后,pod 最长等待时间:

可用于网络抖动的情形,若 超时后,pod 将被调度到其它 node.

节点亲缘性

1:亲缘性(node affinity): 允许你通知 Kubemetes 将 pod 只调度到某个几点子集上面。

通过给 节点 打标签,然后 在 pod 中使用 pod.spec.affinity.nodeaffinity 强制选择(require) 或者 推荐选择(prefer) 节点。

一个典型的 gke 标准节点,常常会打以下三种标签:

  • failure-domain.beta.kubernetes.io/region 表示该节点所在的地 理地域 。

  • failure-domain.beta.kubernetes.io/zone 表示该节点所在的可用 性区域( availability zone ) 。

  • kubernetes.io/hostname 很显然是该节点的主机名 。

已知的,租户常常会选择 机型、区、地理位置来确保服务的可用和高可用,通过打标签,相当于划分了 机群。

2:节点亲缘性 比 nodeSelector 表达能力更强

  • requiredDuringScheduling...: 调度的时候,强制要求。若找不到合适的 node, 则 pod 无法部署

  • preferredDuringScheduling...: 调度的时候,推荐

    • 通常和 权重 weight 搭配使用,用于控制优先级;

  • ...IgnoredDuringExecution: 忽略已经在 node 上运行的 pod,否则会把不符合条件的 pod 踢出

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 5
  template:
    metadata:
      labels:
        app: frontend
    spec:
      affinity:
        podAffinity:
          # 强制要求,忽略已经执行的pod
          requiredDuringSchedulingIgnoredDuringExecution:
          # 选择节点的范围
          - topologyKey: rack
            labelSelector:
              matchLabels:
                app: backend
      containers:
      - name: main
        image: busybox
        args:
        - sleep
        - "99999"

3:节点亲缘性为 requried 的 ,只会在符合条件的 node 中部署:

4:亲缘性为 preferred , 会优先选择 都符合的,其次是 一个符合的(按 weight 排列),最后是都不符合的 【一般会有一个 pod 部署在上面】

因为调度器还会使用其他优先级函数:Selector SpreadPriority 函数. 确保了属于同一个 ReplicaSet 或者 Serice 的 pod, 将分散部署在不同节点上,以避免单个节点失效导致这个服务也宅机。

spec:
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          # 配置第一个权重
          - weight: 80
            preference:
              matchExpressions:
              - key: availability-zone
                operator: In
                values:
                - zone1
          # 配置第2个的权重
          - weight: 20
            preference:
              matchExpressions:
              - key: share-type
                operator: In
                values:
                - dedicated
pod 间亲缘性

1: pod 间亲缘性 可以控制 多个 pod 部署在同一节点、同一机架、同一数据中心。

通常是 联系比较紧密的 pod 需要部署在一块,降低延时等。

kind: Deployment
# ....
spec:
  replicas: 5
  template:
    #....
    spec:
      affinity:
        podAffinity:
          # 强制限定
          requiredDuringSchedulingIgnoredDuringExecution:
          # 节点选择的范围, 含有 label-key=kubernetes.io/hostname 的节点集合
          - topologyKey: kubernetes.io/hostname

           # 依赖的 pod 的标签
            labelSelector:
              matchLabels:
                app: backend

2:若被依赖的 pod 被删除了, 重新调度时, 还是会调用到原来的节点(调度器根据被依赖度,有一套反向打分机制)

pod 间非亲缘性

1:有些 pod 部署在一起会影响彼此的性能,需要分开部署

spec:
      affinity:
        # 非亲缘性
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          # 在指定的节点集中,决定 pod 不能被调度的范围
          - topologyKey: kubernetes.io/hostname
            # pod 标签
            labelSelector:
              matchLabels:
                app: frontend

preferredDuringSchedulingIgnoredDuringExecution: 可指定软性要求

开发应用

1:典型应用中使用的 k8s 组件如下:

  • Manifest

    • 引用两种 Secret

    • 拉取私有镜像

    • pod 中的运行进程 访问其他 pod 或 k8s API 服务器

    • CronJobs

    • DaemonSet: 集群管理员 在每个 pod 上运行的系统服务

    • HorizontalpodAutoscaler: 水平 pod 扩容器

    • Deployment、StatefulSet 对象

    • Jobs

    • pod 模板

    • 每个容器包含 存活探针 和 就绪探针

  • 集群外访问服务

    • LoadBalancer

    • NodePort

    • Ingress 资源

  • 存储

    • pod 中以 configmap 卷挂载

    • ConfigMap: 初始化环境变量

    • emptyDir 卷

    • gitRepo 卷

    • PVC 卷

    • StorageClass 资源

  • 资源管理

    • LimitRange

    • ResourceQuota

  • 运行时创建的对象

    • 端点控制器(Endpoint controller) 创建的 Endpoint 对象;

    • Deployment Controller 创建的 ReplicaSet 对象

    • ReplicaSet 创建的 pod 对象

pod 生命周期

1:一个 pod 可以比作只运行单个应用的虚拟机。

pod 随时会重启,主机名和 ip 会变化(除非使用 StatefulSet)

容器重启,会有新的写入层,磁盘数据会丢失。应该使用 pod 级的存储卷,和 pod 同生命周期

2:容器重启, 并不会影响 ReplicaSet 重新调度 pod, RS 只关注 pod 的数量是否符合预期。

3:以固定顺序启动 pod

  • 一个 pod 可有任意数量的 initContainer 容器

  • init 容器结束后,才会启动主容器

  • 有依赖启动顺序的应用,最好是添加 Readliness 探针,特别是 Deployment,避免在滚动升级时,出现错误版本

4:生命周期的钩子

  • Post-start: 启动后钩子,容器启动后, 和主进程并行执行;【钩子运行失败或返回非零错误码,会杀死主容器】

  • Pre-Stop: 停止前钩子, 容器停止前执行;执行后给容器发送 SIGTERM 【不管执行成功与否,都会使容器终止】

容器崩溃不会执行

5:Pod 的关闭

Pod 的关闭是通过 API 服务器 删除 pod 对象来触发的。

6: Kubelet 关闭 pod 时,会给每个容器一定的时间期限进行优雅的终止,这个时间叫做 终止宽限期(Termination Grace Period)。

pod 的 spec.terminationGracePeriod 参数,默认 30

  • \1. 执行停止前钩子(如果配置了的话), 然后等待它执行完毕

  • \2. 向容器的主进程发送 SIGTERM 信号

  • \3. 等待容器优雅地关闭或者等待终止宽限期超时

  • \4. 如果容器主进程没有优雅地关闭, 使用 SIGKILL 信号强制终止进程

7:若是有状态的 Pod 关闭,关闭前,需要将数据迁移到 其它 存活的 pod。

不建议在收到关闭信号的时候,触发数据迁移:

  • 容器终止不一定代表整个 Pod 终止了 (会有其它容器)

  • 无法保证 迁移流程在进程被杀死前执行完毕;(宽限期不够 或 关闭过程中 pod 发生故障)

若 pod 重启是不会触发迁移流程的。

推荐做法:

  • 应用终止前, 创建 Job 资源,运行一个单独的 pod 迁移数据;【前提是创建 Job 不会出故障】

  • 创建一个 CronJob, 周期性的 检测是否有孤立数据

  • 若使用 StatefulSet, 缩容,会使 PVC 处于孤立状态,数据搁浅。【若扩容永远不发生的时候】, 可在终止前拉起一个数据迁移的 pod

妥善处理 client 请求

1:pod 需要设置就绪探针, 确保服务可用,才将 pod 加入可对外服务的集合中。

若不设置就绪探针,则默认 pod 启动后,服务立马可用。

2:k8s API 在收到 关闭 Pod 时,要做两件事:

  • 修改 etcd 的状态,

  • 删除通知给观察者 Kubelet 和 端点控制器(Endpoint Controller)

如下图所示,这里的问题是 容器停止的事件 和 kube-proxy 修改 iptables 的前后是不确定的。

image-20210615141054178

删除 pod 的时序如下:


分两种情形:

  • 1:容器先停止, iptables 后被修改,这时候 API 服务器会接着分配请求 【不合理】

  • 2:iptables 先修改,容器后停止 【合理】

对于第一种情形,一般是 将容器 延后停止,尽量满足第二种情形。停止时间需要取一个合理的值, 若时间太长,会导致容器无法正常关闭,太短可能无法处理 request。


应用在 k8s 中合理管理

1:打包镜像时:包含最小工具集即可,避免更新版本时间过长。

2:合理给镜像打标签

  • 若一直是 latest 镜像,则 imagePullPolicy 需要设置为 Always,会有两个问题

    • 1:无法回退到旧镜像;

    • 2:每次创建新 pod,都要去检查 镜像是否被修改了;

3:使用多维度标签

标签可以包含如下的内容 ·资源所属的应用(或者微服务) 的名称 ·应用层级(前端、后端,等等) ·运行环境(开发、测试、预发布、生产,等等) · 版本号 ·发布类型(稳定版 、 金丝雀、蓝绿开发中的绿色或者蓝色,等等) ·租户 (如果你在每个租户中运行不同的 pod 而不是使用命名空间) .分片(带分片的系统)

分组管理资源

4:添加注解

  • 包含作者;

  • 应用必须的依赖;

5:更完善的进程终止信息

  • 将终止消息 写入 /dev/termination-log 【可通过 terminationMessagePath 修改 】 , 当 kubectl describe pod 可看到终止日志

    • 若未设置,容器终止的最后几行日志会当做终止消息

  • 将日志打印标准中断, kubectl logs 可见

  • 或者写到 pod 中 【挂载 pod 级别的卷】, 通过 kubectl cp 可拷贝出来;

6:集中式日志记录, 由 ElasticSearch、Logstash 和 Kibana 组成的 ELK 栈。【可通过 Helm 部署】

  • FluentD 通过 DaemonSet 部署 Pod 收集日志

  • Kibana 可视化 ElasticSearch 的 web 工具, 也是单独作为 Pod 运行


k8s 应用扩展

自定义 API 对象

1:开发者需要向 k8s API 提交 CustomResourceDefinitions (CRD) 对象,即可提交 Json 或 YAML 清单的方式创建新的资源类型。

2:例如创建一个 静态网站,自动拉取 Git, 创建 pod,并通过 Service 对外公开。


3:先创建 CRD 对象,让 k8s API 服务器识别该类型。

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  # 资源名称
  name: websites.extensions.example.com
spec:
  # Website 这种资源属于哪种命名空间
  scope: Namespaced

  # 定义API 集群和所属版本
  group: extensions.example.com
  version: v1
  names:
    # 缩短资源的名称
    kind: Website
    singular: website
    #  最后 url 的链接
    #  http://localhost:8001/apis/extensions.example.com/v1/websites?watch=true
    plural: websites

4:提交自定义资源请求:

kind: Website
metadata:
  name: kubia
spec:
  gitRepo: https://github.com/luksa/kubia-website-example.git

5: create 上述两种资源后,可用 kubectl get 查看资源实例

kubectl get websites

6: 自定义控制器,通过 HTTP 监听 API 服务器 Add/Delete 接口,创建 Deployment 资源和 Service 资源。

7: 控制器部署成一个 pod

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: website-controller
spec:
  replicas: 1
  template:
    metadata:
      name: website-controller
      labels:
        app: website-controller
    spec:
      serviceAccountName: website-controller
      containers:
      - name: main
        image: luksa/website-controller
      - name: proxy
        image: luksa/kubectl-proxy:1.6.2

自定义控制器 先和 proxy 通信,然后由 proxy 连接 k8s API.


运行这个控制器 pod,和 API 服务器进行通信, 需要创建 特殊的 ServiceAccount,

若启用了角色访问控制(RBAC), 则需要 clusterrolebinding 到 clusterrole=cluster-admin

控制器运行的核心逻辑如下:


func main() {
 log.Println("website-controller started.")
 for {
  resp, err := http.Get("http://localhost:8001/apis/extensions.example.com/v1/websites?watch=true")
  if err != nil {
   panic(err)
  }
  defer resp.Body.Close()

  decoder := json.NewDecoder(resp.Body)
  for {
   var event v1.WebsiteWatchEvent
   if err := decoder.Decode(&event); err == io.EOF {
    break
   } else if err != nil {
    log.Fatal(err)
   }

   log.Printf("Received watch event: %s: %s: %s\n", event.Type, event.Object.Metadata.Name, event.Object.Spec.GitRepo)

   if event.Type == "ADDED" {
    createWebsite(event.Object)
   } else if event.Type == "DELETED" {
    deleteWebsite(event.Object)
   }
  }
 }

}

func createWebsite(website v1.Website) {
 createResource(website, "api/v1", "services", "service-template.json")
 createResource(website, "apis/extensions/v1beta1", "deployments", "deployment-template.json")
}

func deleteWebsite(website v1.Website) {
 deleteResource(website, "api/v1", "services", getName(website));
 deleteResource(website, "apis/extensions/v1beta1", "deployments", getName(website));
}

func createResource(webserver v1.Website, apiGroup string, kind string, filename string) {
 log.Printf("Creating %s with name %s in namespace %s", kind, getName(webserver), webserver.Metadata.Namespace)
 templateBytes, err := ioutil.ReadFile(filename)
 if err != nil {
  log.Fatal(err)
 }
 template := strings.Replace(string(templateBytes), "[NAME]", getName(webserver), -1)
 template = strings.Replace(template, "[GIT-REPO]", webserver.Spec.GitRepo, -1)

 resp, err := http.Post(fmt.Sprintf("http://localhost:8001/%s/namespaces/%s/%s/", apiGroup, webserver.Metadata.Namespace, kind), "application/json", strings.NewReader(template))
 if err != nil {
  log.Fatal(err)
 }
 log.Println("response Status:", resp.Status)
}

func deleteResource(webserver v1.Website, apiGroup string, kind string, name string) {
 log.Printf("Deleting %s with name %s in namespace %s", kind, name, webserver.Metadata.Namespace)
 req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://localhost:8001/%s/namespaces/%s/%s/%s", apiGroup, webserver.Metadata.Namespace, kind, name), nil)
 if err != nil {
  log.Fatal(err)
  return
 }
 resp, err := http.DefaultClient.Do(req)
 if err != nil {
  log.Fatal(err)
  return
 }
 log.Println("response Status:", resp.Status)

}

func getName(website v1.Website) string {
 return website.Metadata.Name + "-website";
}

8:通过 模板 创建所需要的 部署:

请求创建的 deployment-template.json pod 模板如下:

  • 创建 nginx 容器,提供服务

  • 创建 git-sync 拉取仓库, 并通过 emptyDir 进行容器间共享

{
    "apiVersion": "extensions/v1beta1",
    "kind": "Deployment",
    "metadata": {
        "name": "[NAME]",
        "labels": {
            "webserver": "[NAME]"
        }
    },
    "spec": {
        "replicas": 1,
        "template": {
            "metadata": {
                "name": "[NAME]",
                "labels": {
                    "webserver": "[NAME]"
                }
            },
            "spec": {
                "containers": [
                    {
                        "image": "nginx:alpine",
                        "name": "main",
                        "volumeMounts": [
                            {
                                "name": "html",
                                "mountPath": "/usr/share/nginx/html",
                                "readOnly": true
                            }
                        ],
                        "ports": [
                            {
                                "containerPort": 80,
                                "protocol": "TCP"
                            }
                        ]
                    },
                    {
                        "image": "openweb/git-sync",
                        "name": "git-sync",
                        "env": [
                            {
                                "name": "GIT_SYNC_REPO",
                                "value": "[GIT-REPO]"
                            },
                            {
                                "name": "GIT_SYNC_DEST",
                                "value": "/gitrepo"
                            },
                            {
                                "name": "GIT_SYNC_BRANCH",
                                "value": "master"
                            },
                            {
                                "name": "GIT_SYNC_REV",
                                "value": "FETCH_HEAD"
                            },
                            {
                                "name": "GIT_SYNC_WAIT",
                                "value": "10"
                            }
                        ],
                        "volumeMounts": [
                            {
                                "name": "html",
                                "mountPath": "/gitrepo"
                            }
                        ]
                    }
                ],
                "volumes": [
                    {
                        "name": "html",
                        "emptyDir": {
                            "medium": ""
                        }
                    }
                ]
            }
        }
    }
}

service 模板 service-template.json 如下

{
    "apiVersion": "v1",
    "kind": "Service",
    "metadata": {
        "labels": {
            "webserver": "[NAME]"
        },
        "name": "[NAME]"
    },
    "spec": {
        "type": "NodePort",
        "ports": [
            {
                "port": 80,
                "protocol": "TCP",
                "targetPort": 80
            }
        ],
        "selector": {
            "webserver": "[NAME]"
        }
    }
}
自定义 API 服务器

1:k8s 1.7 后, 可自定义 API 服务器 集成到 主 API 服务器上

使用 k8s 服务目录扩展

1:服务目录可以快速创建服务实例,而无需处理一个个的 Pod、service、configMap 和其它资源。

2:服务目录使用四种通用 API 资源:

  • 一个 ClusterServiceBroker, 描述一个可以提供服务的(外部)系统

    • 集群管理员 为每个 服务代理创建 Broker 资源

  • 一个 ClusterServiceClass, 描述一个可供应的服务类型

    • k8s 从服务代理获取到的 服务列表

  • 一个 Servicelnstance, 已配置服务的一个实例

    • 用户调配服务时,创建实例

  • 一个 ServiceBinding, 表示 一 组客户端(pod) 和 Servicelnstance 之间的绑定。

    • 绑定到具体的 pod (instanceRef 引入具体的实例),并注入一个 Secret

3:服务目录是作为一种外部系统提供服务, 向 k8s 集群中注册代理, 暴露服务。

PaaS

1: platform-as-a-Service, 平台即服务

2:红帽(Red Hat) 的 OpenShift

提供多用户环境

  • OpenShift PaaS 服务

    • Minishift,与 Minikube 等效

    • https://manage.openshift.com

3:

  • Deis Workflow 【微软】

    • https://deis.com/workflow

  • Helm :部署现有应用的标准方式, 包管理器,类似于 yum,apt,homebrew

    • 寻找现有的图表: https://github.com/kubernetes/charts

    • helm install --name my-database stable/mysql: 自动部署所需的 Deployment、Service、Secret 和 PersistentVolumeClaim

    • OpenVPN: 最有用的图表,通过 VPN 和访问服务来输入 pod 网络,类似本地计算机是集群中的一个容器, 对开发应用程序并在本地运行时很有用

    • helm CLI 客户端

    • Tiller

致谢

感谢读者阅读,欢迎指正错误,谢谢。

参考及扩展

  • Luksa M. Kubernetes in action[M]. Shelter Island: Manning Publications, 2018.

  • https://skyao.io/

  • https://jimmysong.io/

  • 搜索镜像:https://hub.docker.com/

  • 社区兴趣小组:https://github.com/kubernetes/community/blob/master/sig-list.md

  • Swagger 创建 访问 API 服务器的客户端 : https://swagger.io/

  • 控制器相关 源代码:https://github.com/kubernetes/kubernetes/tree/master/pkg/controller

  • k8s 中领导者选举的例子:https://github.com/kubernetes-retired/contrib/blob/master/election/

  • k8s 集群管理员指南:http://kubernetes.io/docs/admin

  • minikube 文档:https://minikube.sigs.k8s.io/docs/commands/ip/

  • minikube mount 将本地文件系统挂载到 Minikube Vm 中, 然后通过 一个 hostPath 卷 挂载到容器

    • https://github.com/kuberetes/minikube/tree/master/docs

  • kube-applier 工具:可以定时从 版本控制中检出资源,提交更改

  • Ksonnet + jsonnet: 自定义高级片段,快速转换成完整的 Deployment 配置文件

    • https://github.com/ksonnet/ksonnet-lib

  • Fabric8 持续集成方案: http://fabric8.io

    • Google Cloud 在线实验室:https://github.com/GoogleCloudPlatform/continuous-deployment-on-kubenetes

  • k8s 最新的 代码仓库: http://github.com/kubernetes

  • OpenShift PaaS 服务

    • Minishift,与 Minikube 等效

    • https://manage.openshift.com

  • Deis Workflow 【微软】

    • https://deis.com/workflow

  • Helm :部署现有应用的标准方式, 包管理器,类似于 yum,apt,homebrew

    • 寻找现有的图表: https://github.com/kubernetes/charts

    • helm install --name my-database stable/mysql: 自动部署所需的 Deployment、Service、Secret 和 PersistentVolumeClaim

    • OpenVPN: 最有用的图表,通过 VPN 和访问服务来输入 pod 网络,类似本地计算机是集群中的一个容器, 对开发应用程序并在本地运行时很有用

    • helm CLI 客户端

    • Tiller

  • kubernetes 中文文档:https://kubernetes.io/zh/docs/home/

视频号最新视频

同学,你打拳这么厉害,是拳击教练么?

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐