Kubernetes 实战

1. Kubernetes 系统的需求

实现硬件资源的管理和应用执行环境管理二者的分离,即开发人员和运维人员不再需要有交集,而只需专注自己的那一部分工作;

介绍容器技术

Linux 从内核层面实现的隔离技术,包括进程命名空间和 cgroup 资源隔离两种机制;

优点:同样实现隔离功能,容器技术相对重量级的 VM 虚拟机机制,更加轻量化,相同的硬件资源,可以更大效率的利用;

缺点:由于不同容器共用主机的内核,因此当容器环境对内核有特定要求时,会降低容器的可移植性;

Kubernetes 介绍

Kubernetes 对硬件资源进行了抽象,部署应用程序时,不用再关心需要使用哪些硬件资源;所有资源都被抽象成单个大节点;不管集群中包含多少节点,集群规模都不会造成差异性,额外的集群节点只是代表一些额外的可用来部署应用的资源;

在开发者眼中,Kubernetes 可以被视为关于集群的一个操作系统,因此只需专注实现应用本身,而无须关心应用与基础设施如何集成;

Kubernetes 集群结构

主节点
  • scheduler:负责调度,为应用分配节点;
  • controler manager;负责管理集群;
  • etcd:负责存储,持久化存储集群的配置信息;
  • API 服务器:负责各个组件之间的通讯;
工作节点
  • 容器运行时:即 Docker 或 rtk等;
  • Kubelet:负责与 API 服务器通信,并管理当前节点内的容器;
  • Kube-proxy:负责网络流量的负载均衡;

在 Kubernetes 中运行应用

应用转描述
  1. 将应用打包成镜像;
  2. 将镜像推送到仓库;
  3. 将应用的描述发布到 Kubernetes API 服务器;
描述转容器

调度器根据描述文件中每组所需的计算资源,以及每个节点当前未分配的资源,调度指定的组到可用的工作节点上;

节点收到调度器指派的组后,从仓库拉取镜像并运行容器;

保持容器运行

对运行中的容器和工作节点进行监控,如果容器退出则重新创建;若工作节点宕机,则分配相应组到新的工作节点;

扩展副本数量

副本数量可以手工进行增加或减少,也可以交给 Kubernetes 自行调整为最佳副本数;

命中移动目标

由于容器是动态调度的,这意味着它们会移动;因此 Kubernetes 通过提供服务的静态 IP 或 DNS 服务查找 IP 两种方式,来对外提供稳定的服务;

使用 Kubernetes 的好处

在任何部署了 Kubernetes 的机器上,系统管理员不再需要安装任何东西来部署和运行应用程序;而开发人员也将不需要系统管理员的任何帮助,即可以立即运行应用程序;

  • 简化应用程序部署:所有的工作节点被抽象成一个部署平台;对于异构节点,只需在描述中增加对应用程序所需资源的选择条件即可;
  • 更好的利用硬件:当硬件资源很多时,人工找到最佳组合的难度会变得很大;
  • 健康检查和自修复:通过自动监控,当出现故障时,可以将应用程序迁移到备用资源上;运维人员无需立即做出反应,可以等到上班时间再排查故障即可;
  • 自动扩容:自动根据应用程序的荷载,放大或缩小集群的规模;
  • 简化开发人员的部署:无须系统管理员的帮助即可实现部署;同时方便 BUG 排查,在部署出错时可以停止更新自动回滚;

2. 开始使用 Kubernetes 和 Docker

创建、运行和推送镜像(略)

配置 Kubernetes 集群

有多种方法可以安装 Kubernetes 集群,包括:

  1. 本地的开发机器;
  2. 自己组织的机器;
  3. 虚拟机提供商的机器;
  4. 托管的集群;

由于集群的配置工作比较复杂,因此使用较多的是第1和第4种,即本地和托管两种;另外两种需要使用 kubeadm 或 kops 工具来实现;

在 Kubernetes 上运行应用

最简单的方式是使用 run 命令,但常规的方式是使用 YAML 或 JSON 描述文件;

pod 很像一个独立的逻辑机器,拥有自己的 IP、主机名、进程等;因此,pod 内的容器总是运行在同一个工作节点上;

每个 pod 都有自己的 IP,但这个 IP 是集群内使用的,不能被外部访问,需要通过创建服务来公开它;

loadBalancer 类型的服务,会创建一个可以公开访问的公网 IP,因此它需要使用托管的集群才能实现这点,本地运行的 minikube 做不到;

服务与 pod 的关系

之所以需要服务这个抽象层,其原因在于 pod 的生命周期是短暂的,它有可能因为各种意外的场景消失了,而重新创建的 pod 会有不一样的 IP 地址;因此,需要有一个能够提供静态 IP 访问地址的服务层,并由这个服务层将访问请求路由到当前正常工作的 pod 中;

3. pod:运行于 Kubernetes 中的容器

pod 是 kubernetes 中最核心的概念,而其他组件仅仅是管理或暴露它,或者被它所使用;

介绍 pod

每个容器只运行一个单独的进程是一种好的 docker 实践(除非是该进程自行产生的子进程);

为什么多容器协作优于单容器多进程的协作?

  • 多进程之间需要解释依赖冲突的问题;
  • 当某个进程崩溃需要重启时,多进程场景增加了复杂度;

为何需要 pod

Kubernetes 通过配置 docker 让一个 pod 内的所有容器共享相同的 Linux 命令空间,而不是每个容器都有自己的一组命名空间;这种做法可以让容器之间很方便的实现资源共享,包括 IP 地址、端口空间、主机名、IPC命名空间等;但不共享文件系统,而是通过 docker 的 volume 机制来实现数据的共享;

一个 pod 中的所有容器具有相同的 loopback 网络接口,因此容器之间可以通过 localhost 与同一个 pod 中的其他容器进行通信;

集群中的所有 pod 都在同一个网络地址空间中,这意味着每个 pod 都可以使用其他 pod 的 IP 地址,与该 pod 直接进行通信,而无须 NAT 网络地址转换;

总结:pod 就像一台逻辑主机,其行为和物理主机或虚拟主机非常相似,区别在于运行于 pod 当中的每个进程被封装在一个容器之中;

通过 pod 合理管理容器

将多层应用分散到多个 pod 中是一种更好的实践,这样可以更充分的利用集群中的节点的计算资源,因为单个 pod 只会被安装在一个工作节点上;并且这样也方便更细粒度的对应用进行扩容;

何时在一个 pod 中使用多个容器?

仅当其他容器是做为主容器的辅助身份出现时,例如提供日志转换器和收集器、数据处理器、通信适配器等;

做决定前待思考的问题
  • 它们需要一起运行,还是可以在不同的主机上运行?
  • 它们代表的是一个整体,还是相互独立的组件?
  • 它们必须一起进行扩缩容还是可以分别进行?

以 YAML 或 JSON 描述文件创建 pod

YAML 的基本结构组成

  • 版本
  • 类型
  • 元信息
  • 规格

在 pod 定义中指定端口仅起到展示性的作用,以便让看到这个文件的人知道当前 pod 有哪些端口可以被访问,即使不写,也仍然可以访问;另外一个好处是可以给该端口指定名称,这样使用起来将更加方便;

常用命令

kubectl get pod -o yaml

查看容器的描述,支持 yaml 和 json 两种格式

kubectl create -f

按 yaml 文件创建相应的资源;

好奇 create 和 apply 有什么区别?答:使用 apply 创建的资源,后果可以再次使用 apply 来检查声明文件是否存在更新,如果有更新,会自动删除旧资源,并创建新资源;

kubectl logs

查看 pod 的日志;

当日志文件达到 10MB 大小时,日志会自动轮替;

kubectl logs -c

获取 pod 中某个容器的日志;

默认情况下,日志的生命周期和 pod 绑定,即 pod 删除后,日志也消失了;如果想保留日志,则需要另外建立一个中心化的日志系统来存储日志;

向 pod 发送请求

kubectl port-forwad kubia-manual 8888:8080

在不使用 service 的情况下,port-forwad 可将本地端口转发到 pod 中的某个端口

使用标签组织 pod

标签不仅可以用来组织 pod,也可以用来组织其他的 kubernetes 资源;

创建资源时,可以附加标签;创建之后,仍然可以添加标签或修改标签;

通过标签,可以非常方便的对资源进行分类管理;也可以实现批量化操作;

添加或修改标签

kubectl label po =

新增标签

kubectl label po = –overwrite

通过 –overwrite 选项更改旧标签

通过标签选择器列出 pod 子集

标签选择器的选择条件

  • 包含(或不包含)特定键;
  • 包含特定的键值对;
  • 包含特定键,但值不同;

标签选择器支持多个条件,此时需要满足全部条件才算匹配成功;

使用标签和选择器来约束 pod 调度

当节点是同质的时候,无须显式的声明 pod 应该被调度的位置;但当节点是异质的时候,如果应用程序对硬件有要求,则需要使用需求描述,来告知 kubernetes 对调度的要求(但仍然不是显式指定节点,而是由 kubernetes 自行安排),例如设置标签做为过滤的条件 label gpu=true;

1
2
3
4
5
6
7
8
9
10
11
# 示例
apiVersion: v1
kind: Pod
metadata:
name: kubia-gpu
spec:
nodeSelector:
gpu: "true"
containers:
- image: luksa/kubia
name: kubia

虽然也可以将 pod 调度到某个确定的节点(通过节点唯一标签实现,即 kubernetes.io/hostname),但是这样风险很大,因为有可能该节点刚好处于不可用状态,这样会导致部署不成功;所以,最好的方式是使用标签选择器;

注解 pod

注解也是一个类似标签的键值对的形式,但是它不能用于选择器,它的用途在于给对象添加更多说明性的信息,方便其他人了解对象的一些重要信息;

除了手动添加注解后,Kubernetes 本身也会根据需要,自动给对象添加一些注解;

注解信息存储于对象 metadata 下的 annotations 字段中;

添加和修改注解
kubectl annotate pod kubia-manual mycompany.com/somea nnotation=”foo bar”

使用“mycompany.com/someannotation”这种格式的目的在于尽量减少冲突,避免不小心覆盖的可能性

使用命名空间对资源进行分组

通过标签来分组,存在的问题是不同标签之间的对象可能会有重叠,如果想实现不重叠,则可以通过命名空间来进行分组;这样可以解决资源名称冲突、不同用户误删除其他用户资源的问题;同时还可以限制某些用户仅可访问某些资源、限制单个用户可用的计算资源数量等;

命名空间是比资源更高一个层级的抽象,所以对象都默认属于某个命名空间中;如果没有特意指明哪个命名空间,一般是在 default 命名空间中操作对象;命名空间相当于给资源名称提供了一个作用域;

当需要操作某个特定命名空间中的对象时,需要在命令中指定相应的命名空间名称;

创建一个命名空间

有两种创建方法:

  • 直接通过 create 命令创建,示例: kubectl create namespace
  • 通过 YAML 描述文件创建;

领悟:在 kubernetes 中,所有东西其实都是对象,都可以使用 YAML 文件来描述对象的一些属性特征,然后通过 create 命令创建该对象;

管理命名空间中的资源

当命名空间创建好了以后,如果要将某个对象放入该空间,也有两种方法:

  • 在 create 命令中通过 -n 选项指定空间名,示例:kubectl create -f -n
  • 在 YAML 文件中的 metadata 下的字段 namespace 指定所属的命名空间

在对命名空间中的对象进行增删改查操作时,需要指定相应的命名空间名称,否则将默认操作当前上下文命名空间中的资源(默认是 default ,但可以通过 kubectl config 对当前上下文进行修改);

注:命名空间仅仅是一种逻辑上的资源分组,它并不提供资源之间的物理隔离,因此不同命名空间的对象之间,如果知道对方的 IP 地址,仍然是可以相互通信的;

停止和移除 pod

按名称删除 pod

kubectl delete po

使用标签选择器删除 pod

kubectl delete po -l =

通过删除整个命名空间来删除 pod

kubectl delete ns

该命名将删除整个命名空间,以及里面的 pod

删除命名空间中的 pod,但保留命名空间

kubectl delete po –all

–all 选项确实会删除当前运行中的所有 pod,但问题是如果控制器没有停止运行的话,它将根据描述文件的描述,重新创建 pod 出来;

删除命名空间中的(几乎)所有资源

kubectl delete all –all

4. 副本机制和其他控制器:部署托管的 pod

pod 是最小的单元,它需要被部署到节点上,而这意味着当节点失败时,pod 也将被删除;因此需要引入一种机制,当发现节点失败时,会在新节点上面部署 pod,这样才可以确保 pod 随时健康运行;一般通过 ReplicationController 或 Deployment 来实现这一点;

保持 pod 健康

应用存在于容器之中,如果是容器挂了,K8s 会重启容器,但如果是应用程序挂了而容器还正常运行时,就需要引入一种监控机制,来重启应用程序了;

介绍存活探针

存活探针:liveness probe,用来探测应用程序是否在正常运行中,如果探测失败,就会重启容器;

另外还有一种就绪探针,readiness probe,它适用于不同的场景;

三种类型的探针:
  • HTTP GET 探针:向应用发送 GET 请求,像是否收到正确的响应码;
  • TCP 套接字探针:与容器中的指定端口建立连接,像是否能够连接成功;
  • Exec 探针:在容器内运行指定的命令,看退出状态码是否为 0(表示正常),非零表示失败;

创建基于 HTTP 的存活探针

使用存活探针

logs 命令是查看当前 pod 的日志,如果加上 –previous 选项,则可以查看之前 pod 的日志

当探针检查到容器不健康后,K8s 会删除旧的容器,创建新容器,而不是重启原来的容器;

配置存活探针的附加属性

设置首次探测等待时间

如果不设置初始等待时间,则将在启动时马上探测容器,这样通常会导致失败;

创建有效的存活探针

存活探针应该检查什么

存活探针的作用在于确保应用程序健康工作,因此可以在应用程序中增加一个 API,当正常工作时,访问该 API 可以运行相应的代码,检查各项组件正常工作,之后返回一个正确的代号;

保持探针轻量

探针本身是会消耗计算资源的,而且由于它的运行频率也比较高,因此非常有必要保证它是轻量的,一般可以使用 HTTP GET 探针;

无须在探针中实现重试循环

虽然探针的失败阈值是可以配置的,但是貌似没有必要;

了解 ReplicationController

ReplicationController 副本管理器;pod 运行在节点中,只有当 pod 被 ReplicationController 管理时,pod 才会在节点故障消失后马上被重建;

ReplicationController 通过标签选择器来判断符合条件的 pod 数量是否与预期相符;

ReplicationController 的操作

ReplicationController 有三个组件,分别是标签选择器、副本数量、pod 模板;当更改副本数量时,会影响现有的 pod;当更改标签选择器和模板时,会使现在的 pod 脱离监控;ReplicationController 将不再关注这些 pod;

创建一个 ReplicationController

如果不指定选择器,则 K8S 会以模板里面的标签自动作为选择器的内容,这样更安全,避免因为不小心写错选择器,导致无休止的一直创建 pod;

使用 ReplicationController

查看 rc 的状态

将 pod 移入或移出 ReplicationController 的作用域

ReplicationController 与 pod 之间其实没有任何的绑定管理,它们纯粹是通过标签选择器联系在一起的,因此只需要改变 rc 的标签选择器,或者改变 pod 的标签,它们就会建立或者断开联系;

如果改动了 pod 的标签,它与原来的 rc 失去联系,rc 会发现少了一个家伙,之后 rc 会重新创建一个 pod;

如果改动了 rc 的标签选择器,将导致现有的 pod 全部脱离联系,并且会生成三个新的 pod;

修改 pod 模板

rc 的 pod 模板也可以被修改,但是修改之后并不会影响当前正在运行的 pod,而只会影响后续新创建出来的 pod;这样方法可以用来升级 pod,但它不是升级 pod 的最好方法;

edit 命令会使用默认的编辑器来操作 yaml 文件,可以通过设置 KUBE_EDITOR 环境变量来改变默认编辑器

水平缩放 pod

有两种方法可以实现水平缩放,一种是使用 kubectl scale 命令,一种是直接编辑修改 yaml 文件;

删除一个 ReplicationCotroller

删除 ReplicationCotroller 时,默认会删除由其监管的 pod,但如果加上 cascade 选项后,就可以仅删除 rc 本身,而不删除 pod;

使用 ReplicaSet 而不是 ReplicationController

ReplicaSet 是新一代的 ReplicationController,用来取代 ReplicationController;

比较 ReplicationController 和 ReplicaSet

它们二者基本上完全相同,区别在于 ReplicaSet 里面标签选择器的表达能力更强;例如可以支持:包含、不包含、等于等多种条件表达式;

定义 ReplicaSet

创建和检查 ReplicaSet

使用 ReplicaSet 的更富表达力的标签选择器

ReplicaSet 的更富表达力的标签选择器主要由它的 matchExpressions 属性来体现,它由三部分组成,分别是键名、条件运算符、键值(可以是列表);

条件运算符包括:

  • In:标签值包含在列表中
  • NotIn:标签值不在列表中
  • Exists:存在指定的标签(值无所谓);
  • DoesNotExist:不存在指定的标签(值无所谓);

使用 DaemonSet 在每个节点上运行一个 pod

由 ReplicaSet 管理的 pod 是随机分布在节点上面的,有可能每个节点刚好一个 pod,也有可能不那么平均,有些多点,有些少点;如果想让每个节点刚好运行一个 pod,则需要用到 DaemonSet 来搞定;

一般来说,有这种特殊部署要求的 pod 主要是用来运行一些系统服务进程的;

使用 DaemonSet 在每个节点上运行一个 pod

DaemonSet 根据选择器选择出匹配的节点后,就会在每个节点上运行一个 pod;

  • 如果节点挂了,则它不会有动作;
  • 但如果添加一个新节点到集群中,则它会马上给这个新节点创建一个 pod;
  • 如果节点上面的 pod 挂了,则它会重新在该节点上面创建一个 pod;

使用 DaemonSet 只在特定的节点上运行 pod

DaemonSet 通过 pod 模板中的 nodeSelector 来选择匹配的节点;

由于 DaemonSet 是使用标签选择器来匹配节点,因此让节点的标签被修改后不再匹配时,DaemonSet 会帮忙将该节点上面已经创建的 pod 删除掉;

运行执行单个任务的 pod

ReplicationController、ReplicaSet、DaemonSet 创建出来的 pod 都是持续运行的,当需要创建一些只运行一次就退出的 pod 时,这个时候 Job 出场才能搞定了;

介绍 Job 资源

Job 很适合去干一些临时任务,尤其是这些临时任务需要在每个节点上面跑一次,而且每次跑的时间比较长,有可能中途出现意外,需要重新再跑的时候;这时用 Job 的优势就体现出来了,因为它可以通过选择器批量在多个节点上面跑任务,然后会持续监控任务顺利完成才罢休,不然会自动重新运行意外退出的任务,直到它成功为止,这样是可以让人很省心的;

定义 Job 资源

Job 的 restartPolicy 只能是 onFailure 或者 Never,不能是通常默认的 Always;

在 Job 运行一个 pod

Job 管理的 pod 在运行完成后,会变成“已完成”的状态,但不会被删除,因为这样可以查阅日志,如果删除了就没有办法看到运行的日志了;

在 Job 中运行多个 pod 实例

Job 可以运行一次创建一个 pod,也可以运行多次,创建多个 pod 实例,这些实例可以并行运行,也可以串行;

限制 Job pod 完成任务的时间

有些 pod 有可能运行很久才能结束,但有时候万一卡住了则将永不结束;因此,可以通过设置运行时间的上限来解决这个问题;当超时后,就会 pod 终止,并将 Job 标记为失败;

通过设置 activeDeadlineSeconds 属性来实现;

安排 Job 定期运行或在将来运行一次

如果有些任务需要定期重复执行,如果在某个特定的时间点执行,则此时通过通过创建 CronJob 来实现;

创建一个 CronJob

了解计划任务的运行方式

设置 pod 的最迟开始时间,如果超过了指定的时间还没有开始运行,则 Job 会被标记为失败;

问题一:如果 CronJob 同时创建了两个任务怎么办?

答:执行的任务需要是幂等的,即多次运行仍然会得到相同的结果;

问题二:如果 CronJob 遗漏没有创建任务怎么办?

答:当下一个任务开始时,如果发现上一个任务错过了,则应该先完成前面一个任务的工作;

5. 服务:让客户端发现 pod 并与之通信

由于 pod 的生命周期是短暂的,因此它的 IP 地址是动态变化的,所以需要有一种机制,能够稳定的连接到提供服务的 pod,这种机制就是服务;服务需要做两个事情,当 pod 就绪后,能够将请求路由给 pod 进行响应;当 pod 变动后,能够发现新 pod 的通信地址;

猜测它的实现机制是让 pod 被创建并进入就绪状态后,就向相关控制器进行报告,相当于在控制器那里做一个登记备案,之后控制器就可以将外部请求路由给它了;

介绍服务

概念:服务很像一个有固定 IP 地址的负载均衡器,既能够被内部的 Pod 稳定的访问,也能够被外部稳定的访问,同时能够将外部请求路由给当前正在工作的 Pod;

创建服务

与其他资源类似,服务同样是通过标签选择器,来判断当前服务应该路由匹配到哪些 Pod;

通过 kubectl expose 创建服务

kubectl expose deployment hello-world –type=LoadBalancer –name=my-service

通过 YAML 文件创建服务
kubectl create kubia-svc.yaml

kubia-svc.yaml

检测新的服务

kubectl get svc

默认情况下,服务的作用范围在集群内部,让 pod 之间可以通讯;

从内部集群测试服务

此处的双横杠是命令的间隔,以便匹配给 kubectl 的参数和远程要执行的命令 curl

配置服务上的会话亲和性

由于负载均衡的存在,一般来说每次服务调用都会随机分配给不同的 pod 进行响应,但是可以通过设置 sessionAffinity 属性来指定倾向性的 pod IP;它会将来源某个特定 ClientIP 的请求都转发到某个特定的 Pod 上面;

同一个服务暴露多个端口

使用命名的端口

使用命名端口的好处是万一端口号改了,也不需要改动调用的地方;

服务发现

当服务创建好了后,Kubernetes 会将服务的地址存起来,这样当后续有创建新的 Pod 时,它就会把服务的地址写入新 Pod 的环境变量中,这样新 Pod 就可以通过环境变量来访问服务了;

但是如果 Pod 早于服务之前创建的话,就没有办法使用写入环境变量的方式了;

通过环境变量发现服务

通过 DNS 发现服务

当 Kubernetes 启动的时候,它其实会创建一个 Kube-dns 的 Pod,这个 Pod 的功能就是用来做 DNS 的工作的;所有服务都会在那里备案(即添加一个条目),以便其他 Pod 可以通过全限定域名(FQDN)查询到服务;

Kubernetes 通过修改 Pod 中的 /etc/resolv.conf 文件, 强制 Pod 访问其创建的 内部 DNS 服务器(即名为 Kube-dns 的 Pod) ;但是 Pod 可以通过修改 spec 中的 dnsPolicy 属性来绕过它;

通过 FQDN 连接服务

  • backend-database 表示服务的名称
  • default 表示命名空间
  • svc.cluster.local 表示本地集群

虽然服务可以通过名称进行访问,但访问者仍然需要知道服务的端口号,除非服务使用了标准端口号;

如果访问者的 Pod 与提供服务的 Pod 在同一个命名空间和集群,则只需要服务名称就够了;

连接集群外部的服务

介绍服务 endpoint

直觉上服务和 pod 是直接连接的,但实际上之间隔着 endpoint 资源,服务直接对话的是 endpoint,之后才是 Pod;

手动配置服务的 endpoint

服务是通过标签选择器来创建相应数量的 endpoint 资源的,因此,如果服务没有写标签选择器,则 Kubernets 就不会为服务创建 endpoint,但是我们可以通过手动创建的方式,为服务创建相应的 endpoint;服务和 endpoint 需要使用相应的名称,才能建立关联;

此时通过创建外部 IP 地址的 endpoint,就可以实现对外部服务的访问;

为外部服务创建别名

由于 ExternalName 已经提供了外部域名和端口,因此实际内部 Pod 在获得这些信息后,并不需要再走内部的 DNS 服务代理,而是可以直接访问公网的 DNS 服务器,完成对外部服务的访问;

因此 Kubernetes 都不需要为 ExternalName 类型的服务分配内部 IP 地址了;

将服务暴露给外部客户端

暴露服务给外部有三种方法:

  • NodePort
  • LoadBalance
  • Ingress

使用 NodePort 类型的服务

NodePort 的机制是在所有的节点上预留一个相同的端口,当外部访问该端口时,就将请求转发到内部提供服务的资源(其实它也是一个 Pod);这意味着不仅可以通过 ClusterIP 访问服务,也可以通过任意节点的公网 IP 访问服务;

虽然 NodePort 服务的好处是访问任意节点的 IP 和相应端口即可以访问服务,但其实这种方式并不好,因为万一节点刚好宕机了,则访问将被拒绝;

通过负载均衡器将服务暴露出来

负载均衡器需要集群托管供应商支持才行;如果支持的话,当配置服务的类型为 LoadBalancer 时,Kubernetes 就会调用供应商提供的接口,创建一个负载均衡器服务;

负载均衡器的本质仍然是一个 NodePort 服务,唯一的区别是它由云基础架构的供应商支持并单独部署出来,如果打开防火墙的话,仍然可以像 NodePort 服务那样通过节点的公网 IP 来访问服务;

由于负载均衡器由云基础架构供应商单独提供,这意味着它是在集群外部、独立的;因此它需要将请求先路由到某个 node,再由该 node 将请求转给服务,之后服务再去寻找对应的 pod;

了解外部连接的特性

了解并防止不必要的网络跳数

正常来说,当外部请求到达节点时,节点会将连接请求转发到内部服务,然后由内部服务转发给任一 Pod,而这个 Pod 有可能在另外一个节点上面,导致出现不必要的跳转,因为本来在当前节点就有 Pod 可以提供服务了;

为了避免这个问题,可以通过设置 externalTrafficPolicy:local 来阻止额外的跳转;但是如果设置了这个属性为 local,则如果当前节点没有可用的 Pod 时,连接不会被转发,而是会被挂起,这就糟糕了;此时需要负载均衡器将连接转到至少有一个可用 pod 的节点上;

另外这个属性还有一个缺点是它会导致负载均衡器的效率变低,因为负载均衡本来是以 Pod 为单位进行均衡的,但是启用这个属性后,就变成以 Node 为单位了;

记住客户端 IP 是不记录的

如果外部请求是先到节点,再到服务,则会存在一个问题,即请求中的数据包的源地址将会被节点做 SNAT 转换,这会导致最终提供服务的 Pod 无法看到请求的源地址;如果请求是先到服务,则不存在以上问题;

貌似使用负载均衡器将不可避免会遇到上述的问题?

通过 Ingress 暴露服务

LoadBalance 类型的服务的成本是很高的,因为每个服务都需要有自己的公网 IP;Ingress 即是为了解决这个问题而出现的;

由于 Ingress 是在 HTTP 层工作,因此它还可以提供 cookie 亲和性的功能;

不是每一种 Kubernetes 实现都默认开启支持 Ingress 的,需要提前确认一下功能开启可用;

创建 Ingress 资源

使用描述文件创建:

通过 Ingress 访问服务

它的工作原理跟 Nginx 几乎是一模一样的,唯一的区别是不需要在 Nginx 配置文件中说明如何转发请求了,而是在 Ingress 的描述文件中说明;

通过相同的 Ingress 暴露多个服务

方式一

方式二

配置 Ingress 处理 TLS 传输

在 Kubernetes 中创建 secrets 资源,然后在 Ingress 中引用它,就可以实现与客户端的加密传输了;

当增加证书选项后,如果 Ingress 资源已经创建,此时不需要删除重建,只需要再次运行 kubectl apply 命令,即可更新资源;

问:如何给证书添加 secret 以便 ingress 可以引用?

pod 就绪后发出信号

pod 的就绪一般需要一点时间,如果 pod 启动后,立刻将请求接入进来,则第一个响应可能花费的时间比较久,因此需要有个机制能够声明自己是否进入就绪状态;

介绍就绪探针

每个容器就绪的状态各有不同,因此就绪探针需要开发人员针对每个容器单独设置;

就绪探针的三种类型
  • Exec 探针:执行某个进程,状态由进程的退出状态码来确定;
  • HTTP GET 探针:发送 HTTP GET 请求,就绪状态由响应码确定;
  • TCP socket 探针:创建一个 TCP 连接,创建成功表示就绪
了解就绪探针的操作

一般会设置一段等待的时间,之后再开启就绪探针的探测;如果容器未通过就绪状态的检查,容器不会被终止或者重新启动,但是存活探针就会;这是二者的主要区别;

向 pod 添加就绪探针

可以 ReplicationController 描述文件中的模板添加关于探针的描述,示例如下:

了解就绪探针的实际作用

  • 务必定义就绪探针:因为 Pod 的就绪是需要时间的,如果一创建就接入请求,会导致客户端收到错误的响应;
  • 不要将停止 Pod 的操作逻辑放在就绪探针中,这超出了就绪探针的使用范围;

使用 headless 服务来发现独立的 pod

在一些特殊的情况下,客户端可能连接到每个 Pod,而不是只连接到其中一个 Pod;此时客户端需要能够获取到所有 Pod 的 IP 地址列表,然后向它们发起请求;此时可以通过向 Kubernetes 中的 DNS 发起服务查询请求,正常情况下,这个请求返回的是服务 的 IP,但是如果配置服务的时候,其 ClusterIP 字段设置为 None,此该查询请求会获得所有的 Pod 的 IP;

创建 Headless 服务

将服务的 ClusterIP 字段设置为 None 会使该服务变成一个 headless 服务;

通过 DNS 发现 pod

Kubernetes 没有自带 nslookup 功能,但查询 DNS 需要使用这个功能,因此,可以通过创建一个带此功能的临时 pod 来实现查询(只需选择一个包含该功能的镜像就可以创建相应的 pod 了,使用 kubectl run 命令来创建,而不是使用描述文件);

发现所有的 pod(包括未就绪的)

headless 类型的服务,可以查询到所有 pod,但默认只限为已经准备就绪的,如果想让它返回的结果包含未就绪的,需要在服务的 metadata 中添加一个字段进行描述,示例如下:

貌似这是一个老方案了,最新的版本中据说要使用 publishNotReadyAddress 字段来实现相同的功能;

排除服务故障

有时候服务不能正常工作,此时需要进行调试,以排除故障,找出原因;调试的如下:

  • 确保是从集群内部发起的服务连接请求,而不是从集群外部;
  • 不要通过 ping 来尝试连接集群内的服务,因为服务的 IP 是虚拟的;
  • 如果有定义了就绪探针,确保它已经返回成功,因为未就绪的 pod 不会成为服务的组成部分;
  • 可通过 kubectl get endpoints 来确认某个容器是否已经是服务的一部分了;
  • 当尝试通过 FQDN 来访问服务时,可以试一下能够使用服务的集群 IP 来访问;
  • 检查连接是否访问的是服务的公开端口,而不是其映射的目标端口;
  • 尝试直接连接 pod IP,以确认 Pod 已经在正常工作;
  • 如果 Pod IP 不可访问,需要检查一下 Pod 中的应用是否绑定并暴露相应的端口;

6. 卷:将磁盘挂载到容器

存储卷的级别低于 pod,它被定义为 pod 的一部分;因此,它不能被单独创建或者删除;当 pod 被销毁时,存储卷也会被销毁;(好奇如何存储全局数据?)

介绍卷

卷的应用示例

发现跟之前了解的 docker 存储卷的用法并没有区别,卷需要在 pod 文件中定义,而且,还需要在 containers 部分将它们进行挂载;

存储卷的生命周期跟 pod 绑定,但是据说即使在 pod 和存储卷被销毁后,里面的内容仍然存在(好奇如何实现);

可用的卷类型

  • emptyDir:用于存储临时数据的简单空目录;
  • hostPath:用于将目录从工作节点的文件系统挂载到 pod 中;
  • gitRepo:用于检出 Git 仓库的内容来初始化的卷;p
  • nfs:挂载到 pod 中的 NFS 共享卷;
  • 云存储:用于挂载云供应商提供的特定存储类型,例如 Google 的 gcePersistentDisk,亚马逊的 awsElastic BlockStore;微软的 azureDisk等;
  • 网络存储:用于挂载其他类型的网络存储,例如 cinder, cephfs, iscsi, flocker, glusterfs 等等;
  • 资源卷:用于将 Kubernetes 中的元数据资源公开给 pod 使用的特殊类型存储卷,例如 configMap, secret, downwardAPI 等;
  • persistentVolumeClaim:使用预置或者动态配置的持久存储类型;

通过卷在容器之间共享数据

使用 emptyDir 卷

在 pod 的描述文件中使用存储卷

另外,可通过 medium:Memory 将存储卷的介质限定为内存;

使用 Git 仓库作为存储卷

gitRepo 本质上也是一个 emptyDir 存储卷,差别在于初始化的时候,会检出代码进行数据填充;但是如果 Git 仓库中的代码出现更新时,存储卷并不会跟着更新,此时如果删除旧 pod,重新创建新 pod 时就会拉取最新的代码;

保持代码和仓库同步的办法,可以在 pod 中增加一个 git sync 镜像(这类型的镜像有很多),存储卷同时也挂载到基于该镜像所创建的容器(这类容器称为 sidecar 容器)中,然后配置 Github 的 Webhook 进行访问即可;

gitRepo 卷有一个缺点,它不能拉取私有的仓库;如果需要拉取私有仓库,则只能使用 sidecar 容器了;

访问工作节点文件系统上的文件

由于 pod 跟 Node 是解耦的,因此 pod 理论上不应该使用 node 文件系统中的数据,但存在一些例外情况;当 pod 需要根据 node 的配置文件,对 node 做一些管理工作时,就需要去读取 node 上的文件(这种类型的 pod 一般由 DaemonSet 来管理);

hostPath 卷

hostPath 卷指向节点上的某个特定文件或者目录;同一个节点上的多个 pod,如果都有挂载相同路径的 hostPath 卷,则会实现文件的共享;

hostPath 卷可以实现一定程度的持久性,即当一个 pod 被删除后,后续在同一个节点上建立的 pod 仍然可以使用上一个 pod 的遗留数据;但是这些数据无法在不同节点之间同步,所以它并不是一个适用于放置数据库文件的方案;

使用 hostPath 卷的 pod

貌似 hostPath 卷挺适合用来访问节点上的日志文件或者 CA 证书;

使用持久化存储

当数据需要在不同节点的 pod 之间共享时,此时需要使用某种类型的网络存储,pod 通过访问网络存储(NAS)进行数据的读取和写入;

使用 GCE 持久磁盘作为 pod 存储卷

步骤

  • 先创建 GCE 持久磁盘:将持久磁盘创建在相同区域的 Kubernetes 集群中;
  • 创建一个使用持久磁盘卷的 pod;

通过底层持久化存储使用其他类型的卷

方法大同小异,都是先准备好持久性的存储资源,然后在 pod 描述文件中进行配置以连接它们进行使用;

但是这种方法有很大的缺点,即开发人员需要了解这些持久性存储资源,并且描述文件和它们强耦合,如果换了一个集群环境,描述文件将不再可用,这不是一种最佳实践,有待改进;

从底层存储技术解耦 pod

介绍持久卷和持久卷声明

持久卷 persistent volume(PV)是一种资源,就像 service/pod 一样,它由集群的硬件管理员通过声明来创建;之后开发人员通过持久卷(使用)声明 persistent volume claim (PVC) 来绑定它,然后再通过 pod 声明来来引用相应的持久卷声明;

在同一个时间点,持久卷只能被声明并创建一次,即在它没有被删除前,不能在集群中声明相同名称的另外一个持久卷,除非先把原来旧的删掉;

创建持久卷

集群硬件管理员通过声明挂载网络存储来生成持久卷

注:持久卷是全局资源,即它不属于任何单独的命名空间,就像节点一样;但是持久卷的使用声明是归属于特定命名空间的;

通过创建持久卷声明来获取持久卷

开发人员在 pod 中引用持久卷之前,需要先创建持久卷声明,绑定某个持久卷,之后才能在 pod 中进行引用该持久卷声明;

在 pod 中使用持久卷声明

在创建了持久卷声明后,接下来可以在 pod 声明中引用该持久卷声明;

了解使用持久卷和持久卷声明的好处

通过增加了两层抽象,让开发人员和硬件管理员之间的工作实现了解耦,并增加了代码的可移植性,无须更改代码即可在不同的集群之间进行部署;

硬件管理员负责写创建声明创建持久卷,开发人员负责写使用声明绑定和引用持久卷;

持久卷有多种读写模式,例如 RWO, ROX, RWX,它们限定的单位是工作节点 node,而不是 pod

回收持久卷

当删除了持久卷声明后,如果之前绑定的持久卷的 reclaim policy 为 retain,则此时该持久卷仍然处于不可用的状态,因为里面存放着上一个 pod 的数据,为了确保数据安全,此时需要手工回收持久卷(即删除并重新创建持久卷资源);

reclaim polic 还有另外两个选项:

  • recycle:删除卷中的内容,并可被绑定到新的声明;
  • delete:删除底层存储;

并不是每一种云存储都同时全部三个选项的,不同的云存储的支持情况不同;
持久卷的回收策略,在持久卷创建之后,仍然是可以变更的;

持久卷的动态卷配置

集群管理员除了通过手工的方式来创建一个特定技术或平台的存储卷以外,还可以使用动态配置来自动化执行这个任务;

Kubernetes 内置了主流云服务提供商的 provisioner 脚本,通过调用脚本,可以实现自动化的资源申请;

动态配置的工作原理是集群管理员声明一个或多个的存储类 storageClass,然后开发人员在引用的时候,在声明中指定需要使用的类即可;

通过 StorageClass 资源定义可用存储类型

在 provisioner 属性中指定了使用哪个云服务供应商的脚本创建存储资源;

请求持久卷声明中的存储类

在集群管理员创建了 storageClass 资源后,接下开发人员就可以在 PVC 中进行引用;

StorageClass 是通过名称进行引用的,这意味着 PVC 的描述文件是可以在不同的集群中移植的;

不指定存储类的动态配置

Kubernetes 自带一个默认的存储类,当开发人员在 PVC 中没有显示指定要引用的存储类时,将会默认使用自带的存储类;因此,如果想要让 Kubernetes 将 PVC 绑定到预先创建的 PV 时,需要将 storangeClasName 设置为空字符串,不然它会调用默认的云服务资源置备脚本自动创建新的存储卷;

因此,设置持久化存储的最简单办法 是创建 PVC 资源就好,至于 PV 此时可以由默认的置备脚本自行创建;

7 ConfigMap 和 Secret: 配置应用程序

配置容器内应用程序

常见的传递配置参数的做法:

  • 传递命令行参数:参数少的时候;
  • 引用配置文件:参数多的时候,运行容器前将配置文件挂载到卷中;
  • 设置环境变量

敏感配置数据应区别对待,在 Kubernetes 中一般使用 configMap 保存非敏感配置项,用 secret 保存敏感配置项;

向容器传递命令行参数

在 Docker 中定义命令与参数

  • ENTRYPOINT 负责定义启动时要调用的命令;
  • CMD 负责定义传递给 ENTRYPOINT 的参数;
  • RUN 附加参数(会覆盖 CMD 的参数设置,如有);

虽然也可以使用 CMD 将要执行的命令传递给容器,而不使用 ENTRYPOINT,但这样不太好,因为设置了 ENTRYPOINT 后,即使没有 CMD 选项,容器也依然能够正常运行;因此,CMD 最好只用来传递参数即可;

指令可以有两种格式,分别是:

  • shell 格式:例如 node app.js,该格式将使得 node 进程在 shell 运行;
  • exec 格式:例如 [“node”, “app.js”],该格式将直接运行 node 进程,不在 shell 中运行;

在 Kubernetes 中覆盖命令和参数

镜像中的 ENTRYPOINT 和 CMD 都可以被运行时的命令行参数 command 和 args 覆盖;

为容器设置环境变量

在容器定义中指定环境变量

在环境变量值中引用其他环境变量

了解硬编码环境变量的不足之处

环境变量如果硬编码在 pod 和容器定义中,意味着需要区别生产容器和非生产容器,这将增加很多管理负担;如果能够将配置参数从 pod 定义中解耦脱离出来的话,将使得 pod 本币的定义更加纯粹;

利用 ConfigMap 解耦配置

ConfigMap 介绍

为了解决前面遇到的配置项耦合问题,Kubernetes 提供了 ConfigMap 资源来单独管理配置项;它本质上只是简单的键值对映射,值可以是字面量,也可以是文件;

ConfigMap 是一种资源,它并不是直接传递给容器,而是通过卷或者环境变量的形式,传递到容器中;因此, 容器中的应用仍然像传统方式一样读取环境变量或者文件来做出不同的行为,这样可以让应用保持对 Kubernetes 的无感知(最佳实践,有利于移植);

创建 ConfigMap

有四种方法创建 ConfigMap:

  • 可以直接在命令行中写字面量;
  • 通过描述文件来创建 .yaml
  • 通过导入文件来创建 –from-file
  • 通过导入文件夹来创建

给容器传递 ConfigMap条目作为环境变量

如果某个容器所引用的 ConfigMap 资源不存在时,该容器将无法正常创建,会处于挂起状态,需要一直等到 ConfigMap 可用以后,容器才会被创建;除非将 ConfigMap 的引用备注为 optional,则此时虽然没有 ConfigMap,容器也会正常启动;

使用 ConfigMap 的好处在于将所有的配置参数作为全局资源进行管理,而不是分散在各个单独的资源描述文件中;

一次性传递 ConfigMap 的所有条目作为环境变量

若 ConfigMap 中存在不合格的键名,在创建的时候将被忽略;

传递 ConfigMap 条目作为命令行参数

ConfigMap 并不能直接传递命令行参数,但是可以曲线救国,即通过设置环境变量,然后在命令行参数中引用环境变量就可以了;

使用 ConfigMap 卷将条目暴露为文件

存储卷有一种特殊的类型是 ConfigMap 卷,在创建了以文件作为条目的 ConfigMap 后,在声明存储卷时,可以引用该 ConfigMap,这样 ConfigMap 中的文件条目将被存储到卷中,然后我们可以在 Pod 的描述中引用该存储卷即可;

在描述文件中引用

另外还可以只暴露部分条目到卷中

默认情况下,挂载卷到容器中的某个文件夹时,该文件夹中原本的内容将全部被隐藏覆盖;但是可以通过 subpath 字段来避免覆盖原来的文件;此时 mountPath 的值是一个文件名,而不是文件夹,subPath 则是卷中的一个条目,而不是整个卷;

当设置 ConfigMap 作为存储卷的内容来源时,还可以同时设置这些内容的读写权限

更新应用配置且不重启应用程序

使用环境变量或者命令行参数给容器传递配置信息的缺点当配置信息出现变更时,无法动态将变更后的数据传递给容器;但是如果使用 configMap 卷就可以,不过此时还是需要容器内的应用有监控文件变化并自动重新加载才行;

对于挂载到容器中的卷,如果卷中的文件发生了变化,它在容器中的内容也是实时变化的,但是容器中的应用程序并不一定会监控变化并重新加载;但是如果有重新加载的话,则变化将实时的体现出来;

卷中文件的更新并不是逐个文件进行的,Kubernetes 实际是先将卷的所有文件都复制到容器中的一个新文件夹,然后再更改链接指向这个新建的文件夹;这样就可以避免仅更新部分文件,还没有完成所有文件更新的情况下,容器中的应用程序已经开始加载文件了;

这意味着挂载的更新是以文件夹为单位的,因此,如果挂载的是单个文件,而该文件不会被更新;

虽然对于单个 pod 内部的容器,文件的更新是一次性完整的,但是对于不同 pod 引用相同的 configMap 的情况,这些 pod 之间并不是同步的,它们的更新有先有后;

仅在容器中的应用可以监控并主动重新加载更新后的文件时,挂载可以动态变化配置文件的 ConfigMap 才比较有意义;因为不然即使 ConfigMap 中的文件变化了,应用程序也不需要跟着变化;

使用 Secret 给容器传递敏感数据

介绍 Secret

Secret 被设计用来存储敏感信息,它的用法跟 ConfigMap 类似,区别在于它在写入节点时,不会被物理存储,只是仅存储在内存中,这样当 pod 删除时,也不会在物理介质中留下痕迹;

默认令牌 Secret 介绍

为了让 pod 从内部可以访问 Kubernetes API,每个 pod 初始化创建时,都会写入一个默认的 secret 资源,它包含用来访问 API 的三个条目,分别是 ca.cert, token, namespace 等;

创建 Secret

对比 ConfigMap 与 Secret

ConfigMap 中的条目以纯文件存储,但是 Secret 的条目会被以 base64 编码后存储;这样导致读取的时候,需要进行解码;不过正因为使用了 base64 编码,这意味着 secret 可以支持二进制格式的条目内容;

Secret 的大小有上限,最多只能是 1MB;

对于非二进制的数据,如果不想使用默认的 base64 编码,则可以在 secret 的描述文件中使用 stringData 属性来存放;但是在的展示时候看不出来,它仍然会以 base64 编码的形式展示在 data 字段中;

当 secret 卷被挂载到容器中后,条目的值会预先解码,并以原本的形式写入对应的文件,这样容器的应用程序在访问该值时,无须再做进一步的转换;

在 pod 中使用 Secret

将敏感数据暴露为环境变量的做法其实是有安全隐患的:

  • 有些应用程序在启动或报错时,会打印环境变量到日志中;
  • 应用程序在创建子程序时,会复制当前进程的环境变量;子进程可以访问到这些敏感信息;

当访问私有镜像仓库时,需要访问凭证进行登录,此时可以将访问凭证存放在 secret 中,然后在相关字段引用该 secret 即可;不过此时对凭证的引用是写在 pod 的定义文件中的,如果凭证被很多 pod 共用,则这显然不是一个好的作法,另外有一个 ServiceAcount 可以用来实现复用;

8. 从应用访问 pod 元数据以及其他资源

通过 Downward API 传递元数据

对于可以提前预知的信息,那么可以通过 ConfigMap 写入容器的环境变量,以便容器进行访问;但是对于容器生成之后才知道的信息,例如 pod 名称、IP 等,则这个方法就行不通了;此时可以使用 Downward API,它通过创建 DownwardAPI 卷,将 pod 的元数据作为环境变量或文件注入或挂载到容器中;

了解可用的元数据

  • pod 的名称
  • pod 的 IP
  • pod 所在的命名空间
  • pod 运行节点的名称
  • pod 运行所归属的服务账户的名称
  • 每个容器请求的 CPU 和内存的使用量
  • 每个容器可以使用的 CPU 和内存的限制
  • pod 的标签
  • pod 的注解

服务账户是指 pod 访问 API 服务器时用来进行身份验证的账户;

通过环境变量暴露元数据


通过 downwardAPI 卷来传递元数据


卷中包含的文件由 items 属性来定义;

元数据被存储到了文件中,这些文件的访问权限可以由 downwardAPI 卷的 defaultMode 属性来设置;

相对于环境变量的方式,使用卷的好处是当 pod 创建后,如果某些元数据出现变更,例如标签或注解,则卷中文件的数据会实时更新,而环境变量就做不到这一点了;

由于卷是 pod 级别的资源,因此相对环境变量,它还有另外一个好处是可以让同一个 pod 上的多个容器共享彼此的元数据值;

使用 downwardAPI 来获取元数据的好处是简单方便,缺点是它只能获取部分数据(例如仅限于单个 pod),并不能获取所有数据,如果想要获取更多数据,就需要使用 Kubernetes API 的方式;

与 Kubernetes API 服务器交互

探究 Kubernetes REST API

运行 kubectl 时,本质上是通过 HTTP 来调用 Kubernetes 的 REST API 接口 url;因此,沿用相同的思路,我们也可以从容器内部调用这些 API 来实现与 Kubernetes 服务器的交互;

以下两个命令的效果相同

从 pod 内部与 API 服务器进行交互

从 pod 内部与 API 服务器进行交互需要确认三件事情:

  • 找到 Kubernetes 服务器的 IP 地址和端口;
  • 对服务器进行验证,确保是与真正的服务器交互,而不是冒充者;
  • 通过服务器对客户端的验证,确保客户商具备相应的操作权限;

pod 创建过程中自动注入的 secret 含有用来和 Kubernetes 进行通信的证书;并且还含有令牌 token,用来实现已授权的操作;同时还有一个 namespace 文件包含当前 pod 所在的命名空间名称;

通过 ambassador 容器简化与 API 服务器的交互

ambassador 容器和应用程序的容器运行在同一个 pod 中,它的作用类似于一个中间代理;应用程序通过 HTTP 发送请求给它,再由它使用 HTTPS 和 API 服务器交互;ambassador 容器本质上是在其中运行了 kubectl proxy,就这么简单;

同一个 pod 中的多个容器使用相同的本地回环地址,因此可以通过 localhost 来访问其他容器中暴露的服务端口;

使用客户端库与 API 服务器交互

除了使用原始的 HTTPS 请求外,还可以使用第三方库来实现交互,不同的语言都有相应的实现,可以在应用程序代码中引入这些库,来实现与 API 服务器的交互;

另外 Kubernetes 还自带了一个 swagger API 框架可以用来生成客户端库和文档;同时还提供了 swagger UI 界面可用来查看和访问 API;但它默认没有开启,需要在启动时通过选项设置为开启,之后就可以通过浏览器进行访问了;

9. Deployment:声明式地升级应用

更新运行在 pod 内的应用程序

删除旧版本 pod,创建新版本 pod

更新 ReplicationController 中的模板信息(例如镜像版本)后,RC 控制器将会发现当前没有 pod 与模板相匹配,因此它会把旧版本的 pod 删除掉,之后创建新版本的 pod;

这种升级的方式非常简单易懂,但是它的缺点是在删除和新建之间,会出现短暂的服务不可用状态;

先创建新 pod 再删除旧版本 pod

由于 pod 一般使用 service 对外暴露服务,因此可以先等所有的新版本 pod 都创建好了后,再修改 service 的标签选择器,让其绑定到新的 pod 上面即可;

使用 ReplicationController 实现自动的滚动升级

kubectl rolling-update 命令可以用来执行滚动升级的操作;它会创建一个新的 replicationController ,然后由它来创建新版本的 pod;

kubectl 执行滚动升级的过程中,除了创建新 RC 外,它还会给旧的 RC 和旧的 pod 添加标签(不会改动旧标签,以免影响原来的服务稳定性),通过新增的标签来区分新旧 pod,然后通过逐渐递减旧 RC 的副本数和递增新 RC 的副本数,来实现滚动升级的过程;

kubectl rolling-update 并不是一种理想的滚动升级方式,原因如下:

  • 它在更新过程中会去修改旧的资源;
  • 它通过 kubectl 客户端发起更新的请求,在这一过程中有可能出现网络异常和中断,将导致整个更新过程失败;

使用 Deployment 声明式的升级应用

滚动升级过程不可避免涉及到了两个 replicaSet,一个用来管理旧 pod,一个负责新 pod;因此,通过在 relicaSet 之上引入新的 Deployment 资源,就可以实现两个 replicaSet 的协调工作,让开发人员将预期结果写在 deployment 的描述文件中,之后实现的复杂性被隐藏;

创建 deployment

deployment 并不直接创建 pod,它仍然通过 replicaSet 来管理和创建 pod;一个 deployment 可以对应多个 replicaSet,它通过给这些 replicaSet 加上模板的哈希值进行区分,同时也可以确保相同的模板会创建出相同的 pod;

升级 deployment

deployment 的升级是非常简单的,它非常类似于 pod 的扩容或缩容,只需要更改模板中的镜像 tag,Kubernetes 就会自动进行收敛,达成预期的状态;

deployment 的升级支持多种策略,默认使用 rollingUpdate 滚动升级,此外还支持 recreate 的一次性升级(即删除所有旧的,再创建所有新的,服务会短暂中断);

deployment 比 kubectl rolling-update 更好的原因在于升级过程是由上kubernetes 的控制器来完成,而不是客户端,这样就可以避免可能出现的网络中断问题;

deployment 升级成功后,并不会删除旧的 replicaSet,因为它可以用来实现快速回滚;

回滚 deployment

在升级的过程中,如果发现错误,此时可以使用 kubectl rollout undo 命令来实现回滚;

由于 deployment 保留着每一次升级时旧版本的 replicaSet,因此它也可以实现回滚到指定版本的 replicaSet

控制滚动升级速率

在更新策略中,有两个属性会影响升级速度

  • maxSurge:表示允许超出预期副本数的 pod 数量或比例;
  • maxUnavailable:表示允许少于预期副本数的 pod 数量或比例;

暂停和恢复滚动升级

阻止出错版本的滚动升级

deployment 有一个 minReadySeconds 属性,它表示 pod 需要就绪一定的时间后,才能继续余下的升级工作,这样的好处是在发现 pod 有错误时,能否阻止错误进一步蔓延扩大到所有的 pod;一般来说它需要配合就绪探针使用;

kubectl apply 可以用来更新当前已经创建的资源;如果资源不存在,则它会创建;

deployment 有一个 progressDeadlineSeconds 属性,可以用来设置升级的最长时间,如果超过了这个时间,则意味着升级失败,升级操作将会被自动取消;

10. StatefulSet:部署有状态的多副本应用

创建有状态 pod

使用 replicaSet 创建的多个 pod,它们可以很容易的实现同一个持久卷的共享,但是如果想让每个 pod 拥有自己的持久卷,则无法实现;

有一种解决办法是让每个 replicaSet 只创建一个 pod,多个 pod 将产生个多个的 replicaSet,这样就可以实现每个 pod 有自己的独立存储;

另外,为了实现让每个 pod 都可以访问其他 pod,还需要为每个 pod 创建单独的 service,避免因为 pod 被删除后,重新创建的 pod 使用新的 IP 和名称,导致无法访问;

了解 StatefulSet

对比 StatefulSet 和 ReplicaSet

由ReplicaSet 创建的 pod,其名称是随机的,每次新建的 pod 的标识都跟之前的不同;它适用于完全无状态的应用,每个应用之间都可以相互替换而不会有影响;

由 StatefulSet 创建的 pod 将拥有唯一的标识和状态,名称是有规律和固定的(按顺序索引编号);如果某个 pod 挂掉了,StatefulSet 将再创建一个有相同标识的 pod;

提供稳定的网络标识

每个 pod 的名称由 StatefulSet 的名称加上索引号来组成;如果某个 pod 挂了,新建的 pod 将仍然使用和之前一样的名称;

当 pod 有了固定的名称后,意味着可以创建基于该名称的服务,然后其他 pod 可以通过它来实现稳定的访问;这么做还可以顺带有一个效果,即通过检查服务的列表后,就可以发现有多少个 StatefulSet 的 pod;

StatefulSet 在缩容的时候,假设需要删除多个 pod,它每次只会操作一个,以便确保被删除的 pod 的数据有机会复制保存起来;因此,如果有某个 pod 处于不健康的状态,则此时不允许进行缩容操作,因为它可能会导致数据出现丢失;

为每个有状态实例提供稳定的专属存储

就像 StatefulSet 的 pod 与服务一一对应一样,如果需要为pod 提供持久存储,则在模板中同时写出持久卷声明,之后在创建 pod 之前,就会先创建出与 pod 一一对应的持久卷声明;而每个持久卷声明又将会与某个持久卷一一对应;

当 pod 被缩容删除后,它原先绑定的持久卷声明并不会被自动删除,而是会持续保留着,因为里面可能存储着有状态的数据;直到被手工删除为止;

StatefulSet 的保障

由于 StatefulSet 中的每个 pod 都有唯一标识和存储,因此这意味着 K8s 不应该创建出两个相同的 pod,或者会发生冲突;

使用 StatefulSet

创建应用和容器镜像

通过StatefulSet 部署应用

一般需要创建三个对象,包括:持久卷(用于存储数据)、Service(用来外部访问)、StatefulSet本身;以下以谷歌的 Kubernetes 集群做为示例。

第1步:先创建磁盘

第2步:创建三个持久卷

第3步:创建 Headless Service

好奇:为什么使用 headless service 可以让 pod 之间彼此发现,而普通的 service 就做不到这点了吗?
答:因为普通的 service 会对接请求,然后将请求随机转发至某个 pod,这样会导致 pod 之间不能实现与特定 pod 的通讯,因为普通 service 的转发是随机的;而 headless service 不再直接对接请求,而是让请求直接对接 pod,因此,它可以实现 pod 之间的直接访问;不过,为此付出的代价是,headless service 虽然也叫 service,但实际上并不能仅通过 service 来访问 pod,而是需要 . 这样来访问;

第4步:创建 StatefulSet


由于 statefulset 的 pod 是有状态的,因此在启动 statefulset 时,它们并不是同时启动的,而是按顺序启动,以免引起竞态条件;

使用你的 pod

删除 statefulset 中的某个 pod 后,它会被重新创建,但不一定是调度到原来的节点上,有可能会被安排到新的节点上,不过问题不大,因为这个新建的 pod 会使用旧的名称,并且关联原有的旧的持久卷(如有);

虽然 statefulset 在创建过程中,需要有一个 headless service;但是在 pod 都创建完毕后,也可以额外定义一个 service 来指向这些 pod;

在 StatefulSet 中发现伙伴节点

headless service 之所以可以让 pod 之间彼此发现和通信,其原理在于它使用了 DNS 域名系统中的 SRV 记录,它会将请求转发到提供特定服务的那台服务器上面;

问:什么是 SRV?
答:DNS 系统中保存着很多域名解析的记录,当收到一个解析请求时,DNS 根据这些记录为请求找到相应的目标 IP 地址;DNS 保存的记录有很多种类型,它们分别适用于不同的解析场景,例如 A记录(指向一个 IPv4地址)、MX记录(指向电子邮件服务器的地址)、CNAME记录(用于将当前域名映射到另外一个域名),以及 SRV记录(指向提供特定服务的服务器的地址)等等;(怎么感觉它跟子域名很像?)

通过 DNS 实现伙伴间彼此发现

不同语言的代码都有关于如何做 SRV DNS 查询的实现,只要调用相应的方法,以服务域名作为参数,即可以查询该域名项下的所有的 SRV 记录,从而获得了各个 pod 的访问地址,实现 pod 之间的彼此发现;

更新 Statefulset

通过命令 kubectl edit statefulset 可以调用默认的编辑器打开某个资源相应的声明文件,在对其更改并进行保存后,就可以实现对资源的更新;

此处 statefulset 有一个行为和 deployment 不太一样,即当对镜像的版本进行更新后,并不会影响原来已经在运行的容器,只会影响后续新建的容器;如果想让镜像马上得到使用的话,需要搬运删除原来的副本,然后 statefulset 就会根据新的模板创建新的容器;这一点跟 ReplicaSet 一致;

尝试集群数据存储

对于 Statefulset 里面的 pod 来说,每个 pod 有自己的独立存储,因此数据事实上是分散在不同的 pod 之间的;通过在应用中调取 SRV 记录,实现对其它的 pod 的访问,可以收集散落在各个 pod 中的数据,统一返回给客户端,这样可以解决数据分散存储的问题,实现访问上的统一;

这种方式的缺点是代码写起来很麻烦,或许可以通过封装一个公用的函数来实现;

了解 StatuefulSet 如何处理节点失效

由于 statefulset 中的 pod 是唯一的,这意味着如果调度器在不能明确某个 pod 是否已经失效时,不能随意去创建新的 pod,不然将有可能跟原来的 pod 产生冲突;

模拟一个节点的网络断开

断开的命令: sudo ifconfig eth0 down,这个命令运行后,将导致原本进行中的 SSH 连接断开;

当断开网络连接时,pod 实际上是有在运行的,只是不再与调度器通信;调度器在失去该 pod 的通信后,一开始会将它标记为 unknown 状态,并在超过一定的时间后(可配置),会将 pod 从集群中删除掉;

手动删除 pod

通过情况下,当通过命令调用 API 服务器来执行某个动作时(例如删除 pod ),API 服务器只是先发了一个删除指令给 kubelet,实际上是由 kubelet 来执行删除动作;在 kubelet 删除成功后,它会发通知给 API 服务器; API 服务器在收到通知后,更新自己的状态记录;

如果不想等待 kubelet 的通知,则可以在删除指令中加上 –force 和 –grace-period 两个参数,直接强制更新状态(一般情况下,最好不要使用这种方法,因为它有可能导致冲突);

11. 了解 Kubernetes 机理

了解架构

Kubernetes 组件的分布式特性

总共有三种类型的组件,分别是主节点组件、工作节点组件,以及一些提供额外功能的附加组件;所有的组件之间都是通过 API 服务器进行通信;

工作节点上的组件是一个整体,它们需要被安排在同一个节点上才能协同工作,但是主节点上的组件则没有这个要求,它们可以是分布式部署在不同的节点上的,甚至还可以有多个实例(以此来保证高可用性);不过多出的实例只是作为备用,在某个的时间点,有且只有一个组件在真正的工作;

除了 Kubelet 组件外,其他组件都是做为 pod 来运行的,只有 Kubelet 需要做为常规的系统应用直接部署在节点上,因为总是需要有一个人来完成自举的动作,将其他组件作为 pod 部署在节点上;

Flannel pod 据说是用来为 pod 提供重叠网络,啥是重叠网络?

Kubernetes 如何使用 etcd

问:什么是 etcd?

答:原来它是一个数据库应用,类似 redis,提供 key-value 形式的存储,支持分布式部署,以提供高可用性和更好的性能;它使用乐观并发控制(也叫乐观锁)功能,即为数据提供版本号,在客户端尝试对数据进行修改时,需要提供之前客户端读取的版本号,如果与当前数据库中保存的版本号一致,则允许修改;如果版本号不一致,则拒绝修改请求,并要求客户端重新读取一下最新的数据后,再根据情况重新提交修改请求;etcd 的键名支持斜杠,因此导致键名看起来很像目录名,感觉像是有层级存在一样;键的值是以 JSON 形式存储的;

让所有组件通过 API 服务器来对接 etcd 有两个好处:

  • 只有 API 服务器本身实现了并发控制机制(乐观锁)即可,无须担心直接对接的场景下,有些组件没有遵循乐观锁机制;
  • API 服务器可以增加一层权限控制,确保授权的客户端才能够对数据发起修改;

当存在多个 etcd 实例时,etcd 集群使用 RAFT 算法来保证节点之间数据的一致性;该算法要求集群过半数的节点参与,才能进入下一个状态;这样可以避免某几个实例失联后带来的影响;因此 etcd 的实例数据必须为单数,这样才有可能过半数,避免出现平局的情况;

API 服务器做了什么

API 服务器以 REST API 的形式,提供了对集群状态进行增删改查 CRUD 的接口;

当客户端(例如 kubectl)向 API 服务器发起请求后,API 服务器在收到请求后,会先根据事先配置好的插件,对请求进行预处理,包括:验证身份(认证类插件)、授权核实(授权类插件)、准入控制(准入类插件);

请求只有通过了以上所有这些插件的处理后,API 服务器才会验证存储到 etcd 的对象,然后返回响应给客户端;

API 服务器如何通知客户端资源变更

API 服务器除了做前面提到的那些工作外,其他就没有做其他的;唯一的事项是当资源发生变更时,给之前监听的客户端发送通知;关于资源的创建和维护工作,实际上是由其他组件完成的(例如调度器和 kubelet);

了解调度器

表面上看调度器做的工作很简单,当它监听到 API 服务器关于新建资源的通知后,它就为该资源指定一个节点,然后通知 API 服务器修改资源的定义,加上节点信息;之后 API 服务器会将该信息做为新通知发出来,此时处于监听状态的 kubelet 就会受到通知,然后在其节点上新建相应的资源;建好之后,再发通知给 API 服务器更新资源的状态;

虽然调度器的工作看上去很简单,但其实最难的部分在于如何最高效的调度资源,以便充分利用硬件资源,提高效率;此便会涉及到设计一套高效的调度算法;

调度算法分为两个步骤,第一步是先找出所有可用的节点;第二步是对可用节点进行排序,选择优先级分数最高的节点;

查找节点的工作涉及一系列应满足条件的判断;选择最佳节点则因情况而异,即不同情况下,有不同的优先级标准,例如是高可用性优先,还是成本优先等;

集群中允许有多个调度器,其中有一个会被当作默认调度器;当 pod 没有指定由哪个调度器进行调度时,则由默认的调度器进行调度;不同的调度器可以有不同的调度算法,以实现不同的优先级目标;

了解控制器

不管是 API 服务器,或者是调度器,它们都只负责定义状态,而控制器的工作就在于让集群的状态向定义的状态收敛;控制器有很多个,每种资源都有一种相应的控制器;

当监听到 API 服务器关于资源状态的通知后,控制器就会去做实际的资源管理动作(例如新建、修改和删除等,注意:此处仅仅是操作资源,而不是容器),调整资源的最终状态与定义的状态相符,然后将新的资源状态反馈给 API 服务器;之后 API 服务器发布通知,最后由 Kubelet 完成容器级别的操作;

Kubelet 做了什么

动作一:通知 API 服务器创建一个 Node 资源,以注册其所在的节点;

动作二:持续监听 API 服务器的通知,如果有新消息,就通知容器运行时(例如 Docker),对节点上的容器进行操作;

动作三:当容器启动后,持续监控容器的运行状态、资源消耗、触发事件等;

有意思的是,Kubelet 不但可以从 API 服务器接收消息来创建和管理 pod,也可以从本地的文件目录中导入 pod 定义,来创建和管理 pod,即它是可以脱离 API 服务器独立运行的;

Kubernetes Service Proxy 的作用

工作节点上除了运行 Kubelet 外,还会运行一个 kube-proxy,它用来确保客户端可以通过 Kubernetes API 连接到节点上的服务;

kube-proxy 的名称中之所以带有 proxy 字样,是因为在早期的设计中,它确实扮演着 proxy 的功能,请求会被 iptables 转到它这里,并由它再转发给后端的 pod;但后来这个设计做了改进;kube-proxy 只负责更新 iptables 里面的规则就好,实际请求可以由 iptables 直接转发给 pod,不再经过 kube-proxy,这样可以很好的提高性能;

介绍 Kubernetes 插件

除了核心组件外,还有一些插件用来提供额外的功能,例如 DNS 服务器、仪表板、Ingress 控制器等;

DNS 插件可以为集群内的所有 pod 提供 DNS 服务,这样 pod 之间就可以使用服务名进行彼此的访问,而无须事先知道对方的 IP 地址是多少,甚至是无头服务的 pod 也可以;

Ingress 控制器实现的功能和 DNS 插件差不多,只是实现方式不同,它通过运行一个 nginx 服务器来实现创建和维护规则;相同的部分在于二者都是通过订阅监控 API 服务器的通知来实现更新;

控制器如何协作

了解涉及哪些组件

当创建一个 deployment 资源时,将会涉及以下这些组件的相互协作

事件链

观察集群事件

通过 kubectl get events –watch 命令可以动态的观察集群中发生的事件;

有意思的是,当主节点的组件或者工作节点的 kubelet 执行动作后,需要发送事件给 API 服务器时,它们是通过创建事件资源来实现的,而不是直接调用 API 服务器的接口发送相应的请求(有点意思,为什么要这么做呢?虽然增加了一层抽象后提高了健壮性,不过貌似动作成本也不小);

了解运行中的 pod 是什么

当 kubelet 创建一个 pod 时,它并不仅仅只运行资源定义文件中声明的容器,它还会在 pod 上面运行一个基础容器,它用来保存命名空间,实现一个 pod 上的所有容器共享同一个网络和 Linux 命名空间;这个基础容器的生命周期和 pod 绑定在一起,当 pod 增加运行其他容器时,会从这个基础容器中获得需要的命名空间数据;

跨 pod 网络

网络应该是什么样的

对于同一个 pod 内部的容器,它们之间实现相互访问是非常简单的,因为它们共享一个网络,因此使用本地网络 localhost 就可以实现相互访问了;但如果想要实现跨 pod 的容器之间的相互访问,就需要一套每个 pod 共用的网络机制,这样才能够让每个 pod 的 IP 地址在这个网络中保持唯一性,让其他 pod 可以使用 IP 地址就可以实现连接,而无需使用 NAT 进行网络地址的转换;

Kubernetes 本身只要求通信需要使用非 NAT 网络,但并没有规定这样的一个网络在技术上如何实现,而是交由插件来处理,这意味着可以根据需要,使用不同的网络插件来达到相同的目的;

深入了解网络工作原理

同节点 上的 pod 通信

假设 pod 是一台虚拟机的话,那么运行 pod 所在的节点有点像是一台物理机;虚拟机内部的容器之间由于共享一个网络,相互通信是很容易的;而对于节点所在这台物理机上面的不同 pod,它们本质上只是基于 Linux 命名空间的虚拟化技术下的一个分组,而节点 host 本身也是一个分组(即另一个命名空间);分组和分组之间,共享节点上的同一个网络,但是它们的物理网卡接口却只有一个;为了解决这个问题,引入了一个叫做 veth(virtual ethernet)的虚拟网卡,并创建一个 veth 对,其中一个放在虚拟机的命名空间中,一个在物理机的命名空间中,二者之间形成一个管道,可以相互传输数据;同时将物理机的 veth 连接到物理机的网络上,这样就间接可以实现不同分组之间的相互通信了;

不同节点上的 pod 通信

对于不同节点之间的通信,由于涉及不同的网卡,开始需要引入交换机或者路由器,此时可以有多种实现方式,例如:

  • underlay:即传统的网络基础结构,每个节点有一个自己的独立物理 IP 地址,因此所有其他节点都可以访问;
  • overlay:在 underlay 的基础上,增加一层逻辑网络(虚拟的),这样就可以脱离 IP 地址的限制,拥有自己独立的 IP 地址空间;
  • 三层路由:节点之间共用一台交换机或者路由器进行连接,由路由器实现转发;此方案比较适合中小型局域网中;如果需要应对复杂的场景,则使用 SDN (软件定义网络)的 overlay 更合适;

引入容器网络接口

为了实现容器连接到网络,以便和其他容器互相通信,有一系列的工作需要做,因此 Kubernetes 采用 Container Network Interface 接口来标准化这项工作;CNI 有很多插件实现,包括:Calino、Flannel、Romana 和 WaveNet 等;

服务是如何实现的

引入 kube-proxy

每个节点上都会运行一个 kube-proxy,和 Service 相关的所有事情,实际上都是由 kube-proxy 进行处理的;虽然 service 对外提供了一个稳定的 IP 地址和端口号,但其实它们都是虚拟的,并不能真正的 ping 通;

kube-proxy 在早期版本的时候,确实有发挥代理的作用,对请求进行转发;但现在新的版本中,请求的转发工作是由 iptables 来处理的,kube-proxy 只需负责维护 iptables 的工作了;

kube-proxy 如何使用 iptables

当创建了一个 service 资源时,API 服务器会给所有的节点发通知,kube-proxy 在收到通知后,就会更新自己负责的 iptables 规则,在上面建立一个映射,将服务的 IP 地址和端口映射到能够真正提供服务的 pod 的 IP 地址和端口;之后如果 iptables 发挥有数据包的目标地址是 service 的地址,它就会按映射表将其替换为实际的 pod 地址,将数据包重定向到 提供服务的 pod;

除了要监控 API 服务器关于 service 变更的通知外,kube-proxy 还需要监控 API 服务器关于 Endpoint 变更的通知;因为 Endpoint 对象中保存着关于提供某个 service 服务的 pod 信息(IP 地址和端口号);

运行高可用集群

使用 Kubernetes 来部署应用的最核心目的,就是减少运维的工作,让应用能够以最简单的方式可靠的运行,因此 Kubernetes 还需要提供一系列的组件来监控各类资源的状态,确保它们在发生故障后,能够被及时处理;

让应用变得高可用

方案一:运行多个实例来减少宕机的可能性

该方案需要应用本身支持水平扩展;如果不支持,仍然可以使用 Deployment,只需将副本数设置为 1;这样当实例发生故障时,Deployment 会创建一个新的 pod 实例来替换它;当然,由于创建 pod 的过程需要一点时间,因此不可避免会出现一小段的宕机时间;

方案二:对无法实现水平扩展的应用使用领导选举机制

提前创建多个实例,但在某个时刻就有一个在工作,其中实例处于备用状态;当工作中的实例发生故障时,就在备用的实例中选举一个实例成为工作实例;

实例的选举工作可以在不改变原应用代码的情况下实现,即通过创建一个 sidecar 容器来完成领导选举的工作,点击这里查看更多实现代码

让 Kubernetes 主节点变得高可用

实现办法:增加多个主节点的实例

  • etcd 本身就已经是多实例的分布式设计,多个实例之间会自动同步;
  • API 服务器是无状态的,本身不存储任何数据,因此多少个都没有问题;
  • 管理器和调度器需要实施领导推选机制,某个时候有且只一个处于工作的状态,其他实例作为备用;(它的推举机制特别简单,类似乐观锁的机制,当某个实例能够将自己的名字写入指定对象的属性中时,谁就成为领导,剩下的成为备用;领导者默认每2秒钟需要做一次更新资源的动作;其他实例则监控领导者是否定时更新,如果它们发现领导者超过时间没有更新,大家就重新开始竞争将自己的名字写入指定对象的属性;

12. Kubernetes API 服务器的安全防护

了解认证机制

当请求到达 API 服务器后,API 服务器需要验证该请求是否合法,因此将首先由认证类的插件提取请求中的用户身份,当获得用户的身份信息后,API 服务器就会停止调用剩下的其他插件,直接进入授权插件处理的阶段;常用的认证插件包括:

  • 客户端证书
  • HTTP 头部中的认证 token
  • 基础的 HTTP 认证

用户和组

API 服务器允许被两种类型的用户访问:

  • 一种是机器用户,例如 pod 或者运行在 pod 中的应用;
  • 一种是真人用户,例如开发人员或者运维人员通过 kubectl 客户端发起的请求;

每个用户都属于一个或者多个组,而每个组背后将关联不同的权限;认证插件在认证用户身份后,会返回该用户所属的组名;

ServiceAccount 介绍

pod 与 API 服务器进行通信时,使用 ServiceAccount 机制来证明自己的身份,它会在请求中附带发送 token;token 的内容是在创建容器时,提前挂载到容器中的某个文件里面;

ServiceAccount 本身也是一个资源,跟 pod、secret、configMap 等资源的性质是一样的,因此它们只会作用于某个单独的命名空间,而不是全局有效的;

在一个命名空间中,可以有多个 ServiceAccount 资源;一个 ServiceAccount 资源可以被多个 pod 关联;但一个 pod 不能关联多个 ServiceAccount;

在 pod 的声明文件中,如果不显式的指定 pod 所关联的 ServiceAccount,则 pod 将被关联到其所在的命名空间中的默认的 ServiceAccount;当然,也可以显式的指定要关联的其他 ServiceAccount 名称;

当 pod 关联 ServiceAccount 后,它所能访问的资源,将由 ServiceAccount 来决定了;

创建 ServiceAccount

默认的 ServiceAccount 的权限还是很大的,如果让所有的 pod 都使用默认的 ServiceAccount,显然这种做法并不够安全;每个 pod 所能操作的资源应当在不影响其正常工作的范围内,尽可能的小;

通过 kubectl create serviceaccount 命令就可以快速创建一个 ServiceAccount,但是在创建 ServiceAccount 之前,需要创建一个 token(用 Secret 资源来实现),因为创建 ServiceAccount 时,需要引用一个已经提前创建好的 token;

理论上 pod 允许挂载任何的 secret 到其容器中,但是这样有风险,会导致某些 secret 被暴露了;此时可以通过在 ServiceAccount 指定 pod 允许挂载的 secret 列表,来限制 pod 的挂载范围;

ServiceAccount 还有一个设置镜像拉取密钥的属性,这个属性不是用来限制可挂载的密钥范围的,而是用来实现挂载镜像拉取密钥的自动化;所有关联该 ServiceAccount 的 pod,都会自动被挂载该镜像拉取密钥,从而能够从私有仓库拉取需要的镜像;

将 ServiceAccount 分配给 pod

在 pod 的定义文件中的 spec.serviceAccountName 字段,即可以用来显式的指定 pod 所要关联的 ServiceAccount;该字段的值在 pod 创建后就不能修改了,需要在创建时提前设置好;

ServiceAccount 本身并不包含任何的权限功能(除了控制可挂载密钥的范围外),因此如果没有特别的进行设置的话,所有新创建 ServiceAccount 都默认具有全部的资源操作权限;因此它需要配合 RBAC 授权插件一起使用,才能起到控制权限的效果;

通过基于角色的权限控制加强集群安全

在早期的 Kubernetes 版本中,由于安全控制做得不够完善,只要在某个 pod 中查找到其所用的 token,就可以实现和 API 服务器的通信,对集群中的资源做任何想做的操作;在 1.8 版本之后,RBAC 插件升级为全局可用并默认开启,它会阻止未授权的用户查看和修改集群的状态;

介绍 RBAC 授权插件

背景:API 服务器对外暴露的是 REST 接口,因此用户是通过发送 HTTP 请求调用相应的接口来实现某个操作的;请求由动作+资源名称来组成;基于该背景,RBAC 的控制机制就是检查该请求的动作和资源是否都属于允许操作的范围;

RBAC 是基于用户所属的角色来检查用户的授权情况的;一个用户可能对应多个角色,只要某个角色拥有某种资源的某个操作权限,则请求就会得到通过;

介绍 RBAC 资源

RBAC 授权规则通过四种资源来实现配置;这四种资源可分为两个组:

  • 角色组:Role、ClusterRole,它们指定了在资源上面可以执行哪些动词;二者的差别在于前者面向命名空间内的资源,后者面向集群级别的资源;
  • 角色绑定组:RoleBinding、ClusterRoleBinding,它们将上述角色绑定到特定的用户、组或者 ServiceAccount 上面;

角色组决定了用户可以做哪些操作,角色绑定组决定了谁可以做这些操作;

虽然 RoleBinding 在命名空间下起作用,不能跨命名空间,但是这并不影响它们引用集群级别的角色,因此集群角色并不属于任何的命名空间;

在启用了 RBAC 插件后,pod 默认绑定的 serviceAccount 并不具备查询或修改集群资源的权限,这样可以最大程度的保证集群的安全性;

使用 Role 和 RoleBinding

定义 role 资源的示例

每个资源都属于某个 API 资源组,在声明文件中定义资源的时候,字段 apiVersion 即是指定资源所属的 API 资源组;

复数的资源名称表示可以访问所有的同类型资源,但是也可以通过增加资源名称进一步缩小访问范围;

Role 是归属于命名空间的资源,因为不同的命名空间可以拥有相同的 Role 名称,但里面的内容可能不同

在 Kubernetes 中需要通过创建 RoleBinding 资源来实现角色与相关主体(如用户、ServiceAccount、组等)的绑定(这个理念很有意思,有点面向对象的意思,即想要实现的动作,通过创建对象来实现);

在 GKE 中创建角色之前,需要让当前的客户端账号获取集群管理员的角色,即需要为当前账号创建一个 clusterRoleBinding 资源,来进行集群管理员角色的绑定,示例如下:

RoleBinding 只能将单个角色绑定到一个或多个主体上,这些主体可以归属于不同的命名空间;但是不能反过来,即将多个角色绑定到一个或多个主体上;

问:这貌似意味着如果主体需要绑定多个角色,要创建多个 RoleBinding 资源?

使用 ClusterRole 和 ClusterRoleBinding

普通的角色只能访问到自己所处命名空间中的资源;ClusterRole 则可以访问集群级别的资源,或者所有命名空间中的资源(这样可以避免在多个命名空间中定义相同的角色,只需定义一个 ClusterRole 的角色,就可以多次使用了,即被不同命名空间中的 RoleBinding 进行绑定);至于是哪一种,它是通过绑定过程来实现的;当使用 ClusterRoleBinding 进行绑定的时候,被绑定的主体就可以访问所有命名空间中的资源;当使用 RoleBinding 进行绑定的时候,被绑定的主体则只能访问其所在的命名空间中的资源;

了解默认的 ClusterRole 和 ClusterRoleBinding

Kubernetes 启动时,即已经内置好了一些常用的 ClusterRole,其中最常用的四个分别是:

  • edit:对命名空间中的资源的修改权(除不允许修改 Role 和 RoleBinding);
  • view:对命名空间中的资源的读取权(除不能读取 Role、RoleBinding 和 Secret);
  • admin:对命名空间中的资源的完全控制权(除 ResourceQuota 资源外);
  • cluster-admin:对整个集群的完全控制权

其中一些 ClusterRole 和相同名称的 ClusterRoleBinding 主要是用来给各种控制组件分配权限的;

理性的授予权限

为了安全起见,默认的 ServiceAccount 几乎没有什么权限,连查看集群状态的权限都没有,几乎等于未经认证的用户;但这显然无法应对工作中的需要,好的做法不是给默认 ServiceAccount 添加各种权限,因为它会导致这些权限扩散到那些同样使用默认 ServiceAccount 的 pod 上;而是应该单独给每个需要权限的 pod 创建单独的 ServiceAccount、Role 和 RoleBinding,并在 Role 里面设置所需要的最小权限;

13. 保障集群内节点的网络安全

在 pod 中使用宿主节点的 Linux 命名空间

背景:宿主节点有自己的默认命名空间,而在节点上运行的每个 pod 也有各自的命名空间;通过命名空间实现了彼此的隔离;这些命名空间包括独立的 PID 命名空间、IPC 命名空间、网络命名空间等;

问:貌似即使是同一 pod 中的容器也有各自的命名空间,那么它们如何实现与 pod 的对应?猜测有可能需要在某个地方进行映射登记;

在 pod 中使用宿主节点的网络命名空间

缘起:某些 pod 需要运行在宿主节点的默认网络命名空间中,例如执行系统级功能的 pod(像那些运行 Kubernetes 的控制组件的 pod),这样它们才能够查看和操作节点上的资源和设备;

解决办法:在 pod 的 spec 字段中,启用 hostNetwork: true 选项;

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: pod-with-host-network
spec:
hostNetwork: true
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]

绑定宿主节点上的端口而不使用宿主节点的网络命名空间

实现办法:在 spec.containers.ports 下,有一个 hostPort 属性,可以用来设置 pod 端口和节点端口的映射绑定;

NodePort 类型的 service 也可以用来做相同的绑定,但是区别在于,它是通过修改 iptables 来实现的映射,并且它会作用在所有节点上的 iptables,不管该节点是否有运行 pod;同时,iptables 的路由是随机的,即当前节点 iptable 有可能将请求转发到节点上,以实现负载均衡;

当使用宿主节点的端口时,将带来一个副作用,因为某个编号的端口在节点上有且仅有一个,这意味着该节点最多运行一个存在这种绑定的 pod;如果所需 pod 副本数大于节点数,将导致部分 pod 一直处于 pending,无法创建成功;

hostPort 最初是设计用来给节点上 daemonSet 类型的 pod 暴露端口用的,它恰恰好也兼顾保证了一个 pod 只会被安排在节点一次;

不过据说现在已经有其他更好的实现方法了;是什么呢?

使用宿主节点的 PID 与 IPC 命名空间

跟通过 hostNetwork 属性开启与节点相同网络空间的方法一样,也存在 hostPID 和 hostIPC 选项,可以让容器使用节点上默认的进程命名空间和进程间通信空间;开启后,将可以在容器内看到节点上进行的进程,并可以使用内部进程通信机制与它们进行通信;

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: pod-with-host-pid-and-ipc
spec:
hostPID: true
hostIPC: true
containers:
- name: main
image: alpine
command: ["/bin/bash", "999999"]

配置节点的安全上下文

容器中的进程常常以 root 身份运行应用,当容器和节点共享命名空间时,意味着有可能存在安全隐患,因此有必要进一步对 pod 对宿主节点的访问权限,进行更细粒度的设置;此时可以通过一个叫做安全上下文(security-context)的选项来实现配置;

使用指定用户运行容器

阻止容器以 root 用户运行

容器以 root 用户运行存在一定的安全隐患,例如当节点上有目录被挂载到容器中时,将使得攻击者有机会访问和修改该目录中的内容;

使用特权模式运行pod

有时候根据业务场景需要,不可避免需要让 pod 访问节点上的资源,例如硬件设备、内核功能等;此时需要增加 pod 的权限,让其拥有访问的特权;

为容器单独添加内核功能

通过 privileged 开启特权模式并不是好的做法,因为它意味着赋予容器完全的权限,但实际上并不需要那么多,因此需要进一步做更细粒度的配置,仅赋予所需要的个别权限即可;

在容器中禁用内核功能

阻止对容器根文件系统的写入

好的实践:将根文件系统的阻止写入设置为 true,然后为需要写入的数据,例如日志文件、磁盘缓存等单独挂载存储卷;

上述的各个上下文选项是在容器中设置的,但也可以设置在 pod 项下,这样会对 pod 中的所有容器都产生作用,而不局限于单个容器;

容器使用不同用户运行时共享存储卷

通过存储卷,可以让两个不同的容器共享数据,例如一个负责写入,一个负责读取;但是这样做的前提是两个容器都以 root 用户来运行;如果不是的话则会出现权限问题,导致共享不成功;

有两个属性可以用来解决这个问题

  • fsGroup:用来设置 pod 中所有容器在存储卷中创建的文件的所属组别
  • supplementalGroups:用来给容器中的用户添加新组别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: v1
kind: Pod
metadata:
name: pod-with-shared-volume-fsgroup
spec:
securityContext:
fsGroup: 555 # 写入存储卷的文件的组别
supplementalGroups: [666, 777] # 用户的其他组别
containers:
- name: first
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 1111
volumeMounts:
- name: shared-volume
mountPath: /volume
readOnly: false
- name: second
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 2222
volumeMounts:
- name: shared-volume
mountPath: /volume
readOnly: false
volumes:
- name: shared-volume
emptyDir:

限制 pod 使用安全相关的特性

集群中有两种角色,一个是集群的管理员(创建集群资源的人),一个是开发人员(使用集群资源的人);为了避免开发人员滥用某些功能,例如开启容器的 privilege 权限,导致埋下安全隐患,集群管理员可以通过添加全局设置,来限制部分功能的使用;

PodSecurityPolicy 资源介绍

PodSecurityPolicy 是一个全局资源,即不属于任何的命名空间,用来限制 Pod 可以开启的安全特性;它需要集群开启 PodSecurityPolicy 插件(负责准入控制)后才能使用;当 API 服务器收到创建 Pod 的请求后,它会调用插件,检查该 Pod 的安全特征是否符合 PodSecurityPolicy 里面规定的要求;如果符合,则开始创建;如果不符合,则拒绝请求;

PodSecurityPolicy 能够控制的事情,差不多全部就是上一节提到的那些安全上下文选项,额外还有一项是可以控制 Pod 可以使用的存储卷类型;

了解 runAsUser、fsGroup 和 supplementalGroup 策略

对 Pod 可用的用户 ID、用户组 ID 进行限制的示例

PodSecurityPolicy 仅会在创建 Pod 时起作用,如果在创建 PodSecurityPolicy 资源之前, Pod 已经创建了,则已经创建的 Pod 不会受到 PodSecurityPolicy 的影响;

如果创建 Pod 时没有声明用户 ID,则 Pod 创建后,PodSecurityPolicy 会将容器中的用户 ID 强制修改为策略所允许的 ID,即使容器镜像有定义自己的 ID 也一样会被覆盖;

配置允许、默认添加、禁止使用的内核功能

通过以下三个字段实现控制:

  • allowedCapabilities
  • defaultAddCapabilities
  • requiredDropCapabilities

对于出现在 defaultAddCapabilities 的功能,将会被自动添加到容器中;如果不希望某个容器拥有该功能,则可以在该 Pod 的声明文件中显式的禁用该功能;

限制 pod 可以使用的存储卷类型

如果集群中存在多个 PodSecurityPolicy ,则容器可以使用的存储卷类型是所有 PodSecurityPolicy 中罗列出的类型的合集;

对不同的用户与组分配不同的 PodSecurityPolicy

虽然 PodSecurityPolicy 是集群组别的资源,不归属任何的命名空间,但是它并不会默认对所有命名空间中创建的 pod 生效;它需要被 ClusterRole 引用,然后经由 ClusterRoleBinding 绑定到指定的用户或组之后,才会生效;因此,本质上来说,策略并不针对命名空间,而是针对用户或组的;

隔离 pod 的网络

除了上一节提到的可以对 Pod 的安全选项进行配置外,还可以配置 Pod 的网络访问规则,允许或限制入网和出网通信,实现一定程度的网络隔离;默认情况下 Pod 是可以被任意来源的请求进行访问的;

在一个命名空间中启用网络隔离

如果想把某个命名空间中的所有 Pod 隔离起来,可以创建一个没有写 ingress 入网规则的 NetworkPolicy,同时标签选择器放空,这样它会匹配命名空间中的所有 pod

NetworkPolicy 资源能够生效的前提,是需要集群中的 CNI 插件支持这种资源;不然创建了资源,也不能发挥作用;

允许同一命名空间中的部分 pod 访问一个服务端 pod

如果不加限制,同个命名空间中的 pod 之间,是可以自由的相互访问的,为了提高安全性,可以设置让某个 pod 只允许被指定pod 访问,而不能被未指定的 pod 访问;做法就是创建一个 NetworkPolicy,作用于该 pod,然后在入网规则中写上允许访问的 pod 的标签选择器和可访问的端口号,这些就只有标签选择器匹配的那些 pod, 才具有访问权限;

即使 pod 之间是通过 service 相互访问,以上规则仍然会生效;因为 service 的本质仍然是要回到 iptables 去实现的;

在不同命名空间之间进行网络隔离

实现方法很简单,在入网规则中,有一个 namespaceSelector 的属性,可以用来写命名空间的选择器;

使用 CIDR 隔离网络

前面提到的入网规则是通过标签选择器来实现的,另外还可以通过 IP 段来限制,即只允许某个 IP 段范围内的请求,实现办法是通过 ipBlock.cidr 属性来实现

限制 pod 的对外访问流量

通过对出网规则 egress 规则进入设置即可实现 pod 的对外访问

14. 计算资源管理

为 pod 中的容器申请资源

创建包含资源 requests 的pod

资源 requests 如何影响调度

requests 用来指定容器所需要资源的最小值,而不是上限值;但它会影响调度器的调度,但调度器发现某个节点的资源已经不满足 requests 要求的最小值时,就不会将 pod 调度到该节点上;

调度器有两种优先级调度函数,一种是优先调度到最有空闲的节点,另一种是优先调度到最满负荷的节点;前者可以让节点的资源使用平均化;后者则可以尽量少的节点运行尽可能多的 pod;

当节点上的可用资源不足时,pod 将无法正常进入运行状态,而会一直处于 pending 状态,直到有 pod 被删除后资源被释放出来;

CPU requests 如何影响 CPU 时间分配

CPU requests 不仅会影响调度器的调度工作,还会影响到节点上可用资源在多个 pod 之间的分配工作;调度器会根据申请的资源数量的比例,来分配余下的可用资源给相应的 pod;但如果剩余可用资源刚好没有其他 pod 占用时,调度器会将所有的剩余资源临时全部分配某个繁忙的容器;当其他容器开始要用时,再退还;

定义和申请自定义资源

CPU 和内存是常规的可用资源,Kubernetes 还支持一些自定义资源,例如 GPU;在使用这类自定义资源时,需要先将自定义资源加入节点 API 对象的 capacity属性中,以便 Kubernetes 可以知道该资源的存在;之后,就可以像常规资源一样去引用它了;

限制容器的可用资源

设置容器可使用资源量的硬限制

CPU 是一种可压缩资源,即对进程做出使用限制,并不会影响进程的正常运行,只是会让它的性能下降,计算时间变长而已;而内存是一种不可压缩资源,当为某个进程分配了一块内存后,如果进程没有释放该内存,将导致该块内存一直被占用,即使内存存在空闲,其他进程也没有机会使用;因此,对 pod 的可用资源数量做出最大限制是有必要的,这样可以防止出现恶意 pod 导致整个节点不可用;

当设置了 limits 值后,如果没有设置 requests 值,而默认使用 limits 值做为 requests 值;

调度器不会将 pod 调度到剩余资源不足的节点上,但是会调度到 limits 超过 100% 的节点上,limits 存在超卖现象;limits 并不作为节点调度的控制因素;但是当节点节点上的一个或多个容器使用的资源使用超过 limits 总量时,将导致个别容器被干掉;

超过 limits

当某个容器申请超过 limits 限制的内存资源时,如果 pod 的重启策略设置为 Always 或者 OnFailure时,容器将会被干掉(OOMKilled, out of memory killed);此时 pod 会呈现 CrashLoopBackOff 状态(即不断重启,每次增加一部的间隔时间,最大间隔规定为 5 分钟);

容器中的应用如何看待 limits

当在容器中运行 top 命令来查看内存使用情况时,显示的结果是节点的内存使用情况,而不是真实的容器中的进程所使用的内存情况;不仅内存有这个情况,CPU 的使用也是这个情况;

因此,如果需要在代码中查询可用资源数量时,应避免使用常规的 linux
命令来查看,而应该通过 downward API 来查看实际配置的 limits 值,然后再采取相应的操作;另外也可以通过 cgroup 系统来获取配置的 CPU 限制(如下面的两个文件);

1
2
/sys/fs/cgroup/cpu/cpu.cfs_quota_us
/sys/fs/cgroup/cpu/cpu.cfs_period_us

了解 pod QoS 等级

由于 limits 会被超卖导致某些容器在内存资源不足时被杀死,因此需要制定一个优先级的规则,来决定谁应该优先被杀死;

优先级从低到高分别是:

  • BestEffort:低
  • Burstable:中
  • Guaranteed:高

定义 pod 的 QoS 等级

QoS 等级来源于容器的 requests 和 limits 字段的配置,并没有一个单独的字段可以进行定义;

BestEffort 等级

容器没有设置 requests 和 limits 值的 pod 都属于这个等级;

  • 优点:内存资源充足的情况下,可使用的内存无上限;
  • 缺点:没有任何的资源保证;资源不足时,则啥也分不到;需要释放资源时,首批被杀死;
Guaranteed 等级

所有容器 requests 和 limits 值相等的 pod 属于这个等级;

  • 优点:可保证所请求的资源能够全额分配;
  • 缺点:除了已分配的外,无法使用更多的资源;
Burstable 资源

不属于前面两个等级的 pod,属于这个级别;

对于多容器的 pod,只有当所有的容器都属于 BestEffort 或者 Guranteed 等级时,pod 才是相应的等级,不然全部归属于 Burstable 等级;

内存不足时哪个进程会被杀死

被杀掉的顺序跟 QoS 等级对应,BestEffort 最先被杀掉,最后是 Guaranteed;只有在系统进程需要内存时,Guranteed 进程才可能被杀掉;

对于两个等级相同的 pod,当内存不足时,那个实际使用内存量占申请量更高的 pod 将会被杀掉,即优先杀掉大骗子,留下小骗子;

为命名空间中的 pod 设置默认的 requests 和 limits

LimitRange 资源简介

LimitRange 资源有点像是一个模板,当没有显式的为 pod 设置资源使用声明时,默认使用模板提供的值;并且如果 pod 申请的值超过了模板允许的上限,pod 将不会被允许创建,直接出现报错;

LimitRange 只作用于单独的 pod,所以它不会对所有 pod 要使用的资源总量起作用;

LimitRange 资源的创建

示例的写法将不同类型对象的资源使用限制写在了同一个 LimitRange对象中,但是也可以拆分写在多个对象中,每个控制一种类型;

LimitRange 只适用于在其后创建的资源,如果某个资源在 LimitRange 创建之前已经存在,则不会受到限制;

强制进行限制

当对可用资源进行了限制后,此时如果创建超过限制的对象,Kubernetes 将直接给出报错信息;

应用资源 requests 和 limits 的默认值

LimitRange 的作用域是以命名空间为单位的,即只对当前命名内的对象有效,而对其他命名空间的对象无效;

限制命名空间中的可用资源总量

LimitRange 只能限制单个 pod 的资源使用,没有对可使用的资源总量做出限制,因此如果恶意创建大量的 pod,将会导致整个集群的资源全部被占用掉;

ResourceQuota 资源介绍

ResourceQuota 可以对两个事情做出限制

  • 一个是所有 pod 可以使用的资源总量,当监控限制量此,如果此时新增一个 pod 导致超出限额,则该 pod 不会创建成功;
  • 另一个是可创建的对象数量;
创建 ResouceQuota 对象示例

在创建 ResouceQuota 之前,必须先创建 LimitRange,这样 ResouceQuota 才能创建成功,不然会报错;因为如果没有 LimitRange,则 BestEffort 等级的 pod 可使用的资源是没有上限的;

为持久化存储指定配额

限制可创建对象的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: ResourceQuota
metadata:
name: objects
spec:
hard:
pods: 10
replicationcontrollers: 5
secrets: 10
configmaps: 10
persistentvolumeclaims: 4
services: 5
services.loadbalancers: 1
services.nodeports: 2
ss.storageclass.storage.k8s.io/persistentvolumeclaims: 2

为特定的 pod 状态或者 QoS 等级指定配额

配额可以指定作用范围,总共有四种作用范围,只有当对象满足作用范围的条件时,配额限制才会生效;

  • BestEffort:BestEffort 类型的对象
  • NotBestEffort:非 BestEffort 类型的对象
  • Terminating:Terminating 类型的对象(已进入 Failed 但未真正停止的状态)
  • NotTerminating:非Terminating 类型的对象

BestEffort 只允许限制 pod 的个数,而其他三种还可以限制 CPU 和内存;

监控 pod 的资源使用量

资源使用配额如果写得太高,则会导致资源浪费,如果定得太低,则会导致应用经常被杀死,服务不稳定,因此需要找到一个最佳平衡点;平衡点的寻找办法即是通过监控应用的资源使用情况来进行决策;

收集、获取实际资源使用情况

在每个节点上,Kubelet 自带有一个插件,可以用来收集节点上的资源消耗情况;而 Kubernetes 则可以通过附加组件 Heapster 来进行统计,得到监控信息;

Heapster 已经停用,现在改成了 metrics-server, 本地集群的启用方法 minikube addons enable metrics-server,启用后,需要等待好几分钟才能收集到数据并准备好

显示节点的 CPU 和内存使用量

显示 pod 的 CPU 和内存使用量

如果要查看容器的资源使用情况,则需要加上 –container 选项;

保存并分析历史资源的使用统计信息

top 命令只显示当前的资源使用情况,而不是历史的统计;即使是 cAdvisor 和Heapster 也只保留了很短的一段时间内的数据;如果想要获得比较长的一段时间的统计数据,需要引入数据库对数据进行保存和可视化,常用的工具为 InfiuxDB 和 Grafana;

InfiuxDB 和 Grafana 都是以 pod 的形式运行的,因此只要下载相应的声明文件,即可快速部署;如果是 Minikube 则更加简单,只需要启用相应的插件就可以了;

找到 grafana 的地址

15. 自动横向伸缩 pod 与集群节点

在 pod 开始运行起来之后,如果发现请求量逐渐增加,通过手工更改 deployment、replicaSet 等资源的副本数量,可以实现 pod 数量的增加;但是这需要提前知道流量何时会增加,可是有时候并没有办法提前知道,因此需要引入一套监控的机制,当监控的指标发生变化时,让集群根据提前设置好的规则,自动增加 pod 的副本数量或者是节点数量;

pod 横向自动伸缩

HPA 插件,horizontalPodAutoscaler,是一个专门用来监控 pod 的运行状态指标的插件,当规则条件满足时,它就会自动调整 pod 的副本数量;

了解自动伸缩过程

分为三个步骤来实现

获取状态指标

HPA 并不用自己去采集指标数据,因为有其他插件已经做了这个工作(即工作节点上的 cAdvisor 和主节点上的 Heapster),它只需要跟 Heapster 拿数据就可以了;

计算所需 pod 副本数

一般根据 CPU 使用率和 QPS 每秒访问数量来计算

调整 replica 属性

HPA 并不是直接调用 API 服务器的接口对相关资源(如 Deployment、ReplicaSet等)的副本数进行修改,而是通过联系这些资源的子资源对象来修改;这样做的好处是任何资源如果在实现上有任何变更,HPA 这边不会受到任何影响,不需要做任何的修改,它们之间通过子资源实现了隔离;同时不同的资源之间也不会相互影响,因为它们只要管好自己的子资源就可以了;

整个自动伸缩的过程

基于 CPU 使用率进行自动伸缩

在使用 CPU 使用率指标监控 pod 的使用情况时,HPA 插件实际上是根据 pod 定义中提到的 CPU 资源请求来计算的,即根据 pod 运行过程中使用的 CPU 和原请求的 CPU 之间的比例,来判断 pod 是否在超负荷运转;

HPA 对象有两种创建方法,一种是通过 YAML 声明文件,一种是通过 kubectl autoscale 命令(表面上看它操作的对象是 deployment,但在操作的过程中,它会自动创建一个 HPA 对象)

当 pod 的 CPU 使用率超过目标值时,HPA 会对其进行扩容;反之变然,即当运行中的 pod 的 CPU 使用率远低于目标值时,HPA 也会做缩容的动作;

HPA 在扩容的时候,虽然会根据目标值进行计算,得到达成目标值的最少 pod 数量;但是它并不一定能够一步达到将 pod 调整到该数量,尤其是当这个数量比较大的时候;在单次扩容操作中,如果当前副本数小于等于2,则最多只能扩容到4个副本;如果当前副本数大于2,则最多只能扩容一倍;

另外触发扩容或者缩容也有时间间隔的限制;只有距离上一次扩缩容超过3分钟时,才会触发扩容;超过5分钟时,才会触发缩容;

当需要对 HPA 中设定的目标值进行修改时,有两种操作办法,一种是使用 kubectl edit 命令;另一种是先删除原先的 HPA 资源,然后再重新一个;

基于内存使用进行自动伸缩

使用方法跟 CPU 一样,没有区别,此处略;

基于其他自定义度量进行自动伸缩

想要使用其他自定义度量进行自动伸缩,需要有一个前提,即度量涉及的指标数据有被收集;度量有有如下类型:

resources 度量类型

例如 CPU,内存等;

pod 度量类型

例如 QPS(每秒查询次数)

Object 度量类型

这种类型极大的扩展了 HPA 的使用场景,它让 HPA 可以根据集群中的其他资源对象的属性来计算是否需要扩缩容

确定哪些度量适合用于自动伸缩

如果增加副本数之后,并不能使度量的目标值线性的降低,而很可能让度量指标并不适宜;因为该指标的变化,跟 pod 扩缩容可能并不存在实际上的关系;

缩容到零个副本

目前暂时还不允许缩容到零个副本,但据说未来会实现这个功能;

pod 的纵向自动伸缩

目前 Kubernetes 官方还没有实现这个功能,但是 google GKE 却有这个功能,它会统计 pod 的资源使用情况,然后自动调整 pod 定义信息中的 resource require 和 limit,以最大化的利用硬件资源;

集群节点的横向伸缩

当现有的节点不再满足需求,需要添加更多节点时,有一个 ClusterAutoscaler 插件可以用来完成这个任务;

当要添加新节点时,还会遇到该新节点应该是什么样的规格,因此集群需要提前配置好可用的节点规格;这样 ClusterAutoscaler 插件会检索这些可用规格,从中找到一个能够满足 pod 要求的规格,然后创建该节点;

当有多个规格都能够满足 pod 需求时,此时插件就需要从中找一个最合适的;

当插件发现节点上所有 pod 的 CPU 和内存使用率都低于 50% 时,它会开始考虑归还该节点;但是前提上节点上运行的 pod 是否可以被调度到其他节点上,如果可以就归还;如果不可以,就不归还;

  • kubectl cordon 命令会将节点标记为不可调度(即不会再往该节点添加新 pod),但已在节点上运行的 pod 不受影响,仍然正常运行;
  • kubectl drain 命令除了将节点标记为不可调度外,还会将节点上已在运行的 pod 疏散到其他节点上;

启用 Cluster Autoscaler

如果启用 Cluster Autoscaler 跟集群部署哪家云供应商有关系,因为不同的云供应商有不同的作法,以下是 GKE 的示例:

1
gcloud container clusters update kubia --enable-autoscaling --min-nodes=3 --max-nodes=5

限制集群缩容时对服务的干扰

当发生缩容时,节点会被回收,因此运行在 pod 上的节点将变得不可用;但是有可能业务场景对可用的 pod 数量有最低要求,例如 mongo 至少需要有3个实例组成 replica set;因此,为了避免这种状况发生,可以通过创建 podDisruptionBudget 资源来限制集群缩容时对服务带来的干扰

1
kubectl create pdb kubia-pdb --selector=app=kubia --min-available=3

podDisruptionBudget 资源的属性很简单,只由标签选择器和最小可用数 min-available 和 max-unavailable 两个属性组成;

16. 高级调度

使用污点和容忍度阻 pod 调度到特定节点

它的工作原理是给节点添加污点,然后只有那些在定义中规定该种污点可容忍的 pod, 才会被调度到该节点上(有污点相当于默认不分配,让普通 pod 远离该节点);

想要实现节点和 pod 之间的关系安排,不外乎有两种做法,一种是不主动对节点做标记,而是在 pod 中做标记,定义应使用哪些节点;另一种是反过来,不主动在 pod 中进行定义,而是先对节点做标记,然后只用那些在定义中明确表示可接受节点上的相关标记的 pod,才会被调度到该节点;

介绍污点和容忍度

污点和容忍度的做法默认会用在主节点,这样确保只有标记了可容忍该污点的那些系统级 pod,才会被安排在主节点上;

此处 effect 字段的值是 NoSchedule,它表示不能容忍这种污点的 pod 不要调度到当前节点来

污点的效果:

  • NoSchedule:表示如果不能容忍,则不调度 pod 到节点上;
  • PreferedNoSchedue:表示如果不能容忍,则尽量不调度到该节点上,除非没有其他节点可以调度;
  • NoExecute:前两个 schedule 只会在创建 pod 时影响 pod 的调度;execute 则会在 pod 运行过程中影响调度;当某个节点在 pod 运行期间突然改变状态,导致 pod 不能容忍时,就会重新调度该 pod 到其他节点;

在节点上添加自定义污点

给节点 node1.k8s 添加自定义污点 node-type=production,这样如果不属于生产环境的 pod,就不调度到该节点上,即该节点属于生产环境 pod 的专用;

1
kubectl taint node node1.k8s node-type=production:NoSchedule

虽然这种做法可以保证非生产环境 pod 不会被调度到该节点上,但却无法保证生产 pod 被调度到非生产环境的节点上;为了让生产环境和非生产环境的 pod 隔离开,还需要额外给非生产环境的节点添加污点;

在 pod 上添加污点容忍度

污点操作符除了 Equal 外,还有 Exist;

污点容忍度的时间限制

在某些情况下,节点可能会失效,此时集群管理组件会给节点添加污点 unready 或 unreachable,那么如果 pod 对这两种污点没有容忍度的话,就会被调度到其他节点上;但是有时节点在一定的时间后,会恢复正常,此时 pod 并不需要被重新调度,只需要等待一段时间即可;至于想要等待多久,可以通过在 pod 容忍度的设置中,添加容忍时间;

使用节点亲缘性将 pod 调度到特定节点上

关于如何调度 pod 到指定节点,早期 Kubernetes 的实现是使用 nodeSelector 的机制;但后来发现它并不能满足所有类型的业务需求,因此在新的版本中引入了亲缘性规则,后续预计将逐步替代旧的 nodeSelector 机制;

问:节点亲缘性貌似并不是强制的,而是一种倾向偏好性,即当所有节点都无法满足 pod 的亲缘性需求时,调度组件就会将 pod 调度到任意节点上?

答:后来发现它的规则要复杂得多,虽然默认状态下是非强制性的,但也可以通过规则定义成强制性的;

使用节点亲缘性的前提是节点需要设置有一些标签,这样 pod 才能根据这些标签判断节点是否有亲缘性;如果没有标签,那就没有办法了;

在 GKE 上面创建集群时,需要设置集群的名称,同时选择地理区域和该区域内部的可用分区,现在才发现,原来 GKE 是通过给节点添加亲缘性标签来实现的;而实质上所有的节点都是在一个大集群内,我们创建的小集群只是逻辑上的;

指定强制性亲缘性规则

强制指定 pod 只能被分配到配备有 GPU 的节点上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: kubia-gpu
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoreDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- "true"

requiredDuringSchedulingIgnoreDuringExecution 表示本规则只适用于新创建的 pod, 不影响已经在运行中的 pod;

调度 pod 时优先考虑某些节点

前面提到亲缘性的规则要生效,节点本身必须被提前打上标签;有趣的是,还可以在这些标签中指定节点是独占的还是共享的;不同的标签还可以设置权重系数,用来计算优先级;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: pref
spec:
template:
...
spec:
affinity:
nodeAffinity:
# 此处使用了 preferred,而不是 required,表示是非强制性的,只是优先考虑
preferredDuringSchedulingIgnoreDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: availability-zone
operator: In
values:
- zone1
- weight: 20
preference:
matchExpressions:
- key: share-type
operator: In
values:
- dedicated

使用 pod 亲缘性与非亲缘性对 pod 进行协同部署

亲缘性的规则除了可以用来指定 pod 应该部署到哪些节点上,还可以用来设置哪些 pod 应该尽量被部署在相近的位置,就像亲人住在一起一样;

前者通过 nodeAffinity 字段来实现,后者通过 podAffinity 字段来实现;

使用 pod 间亲缘性将多个 pod 部署在同一个节点上

此处使用了 labelSelector.matchLabels 字段来设置标签选择器,另外还可以使用表达能力更强的 matchExpressions 字段;

假设 A pod 要追随 B pod 的部署位置,那么只需在 A pod 上面定义亲缘性规则即可,并不需要在 B pod 上面定义;但是,当 B pod 因为某些原因被删除而重新创建时,调度器仍然会根据 A pod 的规则,将 B pod 部署在 A pod 所处的节点上(这样做才能维护 A pod 亲缘性规则的一致性,不然如果 B pod 部署到节点上,规则就被违反了)

实现原理:当存在多个可用节点时,调度器本质上是通过给不同节点的优先级打分以选择最合适的节点;

将 pod 部署在同一机柜、可用性区域或者地理地域

将所有的亲缘 pod 都部署在同一节点并不一定是最好的选择,因为当节点发生故障时,会导致服务不可用;从健壮性的角度,部署在相同地理区域的不同节点上,也是一种好的方案;此点可以通过 topologyKey 字段来实现;它可以有多种值,表示不同的规则

  • failure-domain.beta.kubernetes.io/zone 指定将 pod 部署在相同的可用区中;
  • failure-domain.beta.kubernetes.io/region 指定将 pod 部署在相同的地理区域中;

topologyKey 字段有好几个值,看上去好像很复杂很神奇的样子,但其实它的实现原理特别简单,就是在节点上添加标签键值对,然后在 pod 定义中的 topologyKey 添加相应的键名,这样调度器会优先选择匹配的节点来部署相应的 pod,其作用很像是标签选择器;

表达 pod 亲缘性优先级取代强制性要求

使用 required 类型的亲缘性规则意味着调度是强制性的,但如果不需要强制,只是优先考虑,则可以使用 prefered 开头的规则,并为之写上权重系数即可;

利用 pod 的非亲缘性分开调度 pod

有时候我们想将一些 pod 部署在一起,有时候则相反,想让某些 pod 具有互斥性,即不要安排在一起,这时可以使用非亲缘性(感觉用互斥性更直观)规则来实现这个效果,即 nodeAntiAffinity 或者 podAntiAffinity 字段;

使用强制的互斥性规则来部署 pod 时,如果节点的数量不够,将使用一部分 pod 处于 pending 状态,无法调度成功;如果对互斥程度要求没有那么高,则可以考虑使用 prefered 规则来实现;

17. 开发应用的最佳实践

集中一切资源

在 Kubernetes 中,所有的一切都是资源,但是它们有些是由开发人员创建并维护的,有些则是由集群人员创建和维护;二者有所分工,互不耦合;

Pod 通常会用到两种类型的 secret 数据,一种是用来拉取镜像用的,一种是在 Pod 中运行的进程所用的;secret 一般并不作为声明文件的组成部分,而应该是由运维人员进行配置,并分配给 serviceAccount,然后 serviceAccount 再分配给各个 pod;

初始化环境变量一般使用 configMap 卷;这样对于开发人员来说,引用的卷是固定的,但是卷的内容是可以根据环境变化的;

集群管理员会创建一些 LimitRange 或 ResourceQuota 对象,由开发人员在声明文件中引用;这些对象可以控制 pod 可以使用的硬件资源;

了解 pod 的生命周期

将应用交给 Kubernetes 来运行的注意事项:

  • Pod 中的应用随时可能被杀死,并由新 Pod 来替代;
  • 写入磁盘的数据可能会消失;
  • 使用存储卷来跨容器持久化数据;
  • 如果 Pod 是正常的,但 Pod 内的容器持续崩溃,Pod 并不会被销毁重建;
  • Pod 的启动是没有顺序的;
  • 可以在 Pod 中创建 init 容器来控制主容器的启动顺序;
  • Pod 中的容器支持启动前 post-start 和启动后 pre-stop 的钩子;

问:貌似可以通过启动后钩子来控制容器的启动顺序?

答:后来发现更好的做法是让容器自己能够应对无顺序的情况,即在其他容器没有就绪前不会出错,而是会进行一定时间的等待;

以固定的顺序启动 pod

通过在 pod 中创建 init 类型的容器,在它里面写一段脚本来监测其他容器或服务是否已经就绪,如果就绪,就开始启动主容器;init 容器写在声明文件的 spec 属性下面,如下图所示;

虽然有机制来控制应用的启动顺序,但更好的实践作法是放应用本身可以应对其所依赖的服务未准备好时的情况;例如对于应用所连接的数据库服务,当连接不上时,就先暂停,然后每隔一段时间后进行重试;

问:应用有可能在中途出现断开依赖服务的情况,不知此时是否可以通过 readiness 探针来告知 Kubernetes 当前应用进入了未准备好的状态?如果 readiness 探针是一次性的话,那或许这个工作可以交给 liveness 探针来完成;

答:后果发现 readiness 的探针不是一次性的,它会在容器运行过程中仍然保持工作;

增加生命周期钩子

启动后钩子执行成功,容器才会启动,不然会呈现等待的状态,但它和容器中的主进程是同时开始执行的;

钩子的输出信息如果是输出到标准输出的,将会导致查看不到,这样会不方便高度,因此,如果可以的话,最好还是输出信息到日志文件中更好;

停止前钩子没有成功也不影响容器被终止;

了解 pod 的关闭

kubelet 关闭 pod 时涉及如下顺序的动作:

  • 执行容器停止前钩子(如有);
  • 向容器的主进程发送 SIGTERM 信号(因为容器本质上只是操作系统中的一个进程,所以关闭容器跟关闭进程本质上是一样的);
  • 给容器一定的时间(宽限期),让其优雅的关闭;
  • 如果容器动作超时,则使用 SIGKILL 信号进行强制关闭;

终止宽限期的时间是可以配置的,默认是 30 秒;

由于 kubelet 是将关闭信号发给容器,而不是发给容器中的应用,因此应用有可能并没有收到这个信号;此时应用可以通过停止前钩子来让自己得到通知;

此处存在一个悖论,即 pod 是运行在节点上的,因此不管在 pod 中设计了何种优雅的关闭机制,它都无法保证和控制它所在的节点突然出现的崩溃;此时会导致它的任何优雅关闭流程被强行终止;针对这个悖论的解决办法是另辟蹊径,即通过长期或定期运行一个 job,检查有没有出现一些孤立的资源(说明其所有节点可能已经崩溃了),如果有的话,就把它们安置到妥善的地方去;

确保所有的客户端请求都得到了妥善处理

在 pod 启动时避免客户端连接断开

解决方案:在 pod 声明文件中添加一个就绪指针,探测 pod 就绪成功后,再对外提供服务;

在 pod 关闭时避免客户端连接断开

API 服务器在收到停止并删除某个 pod 的请求后,会同时做两件事情,一件是通知 endpoint 管理器更新转发规则,一件是通知 kubelet 删除 pod;前一个动作需要较长的执行时间(因为需要多个 endpoint 的 iptables 转发规则),后一个动作所需要的执行时间比较短,因此,后者大概率会以更快的速度完成;这会产生一个问题,即 pod 已经停止工作了,但是可能仍有请求被转发了进来,导致这些请求无法被正确处理;

以上问题并没有百分百的解决办法,唯一的办法是延长 pod 关闭时的等待时间,多几秒钟即可,例如 5-10 秒;这可以通过添加一个停止前的钩子,让容器睡眠一段时间来解决;

让应用在 Kubernetes 中方便运行和管理

构建可管理的容器镜像

冲突点:生产环境使用的镜像应该尽可能的小,这样可以缩短节点上镜像的下载时间,让 pod 更快的进入准备就绪的状态;而开发环境使用的镜像应该大一些,尽量包括一些方便在开发过程中进行调试的工具,例如 ping、curl、dig 等;

合理地给镜像打标签

避免使用 latest 作为标签,因为无法通过这个标签知道当前 pod 运行的是那个版本的镜像,表面上它们的标签都一样,但实际上有一些可能是使用新镜像,一些使用的是旧镜像;

资源使用多维度而不是单维度的标签

资源常见的标签维度:

  • 资源所属的应用名称;
  • 应用层级,例如前端、后端等;
  • 运行环境,例如开发、测试、生产等;
  • 版本号;
  • 发布类型,例如稳定版、beta 版等;
  • 分片,如果存在分片的话;
  • 租户,如果存在租户的话(貌似还可以使用命名空间);

通过注解描述每个资源

注解可以给资源增加一些额外的信息,方便其他人更好的管理;一般有两个常用的注解,一个是关于资源负责人信息,一个是关于资源的描述;

其他类型的注解:

  • pod 所依赖的服务:用来展示 pod 之间的依赖关系;
  • 构建和版本信息;
  • 第三方工具或图形界面可能要用到的元信息;

给进程终止提供更多的信息

当容器挂掉后,需要调查挂掉的原因;为了让这个事情更便利,Kubernetes 提供了一个终止专用的日志文件 /etc/termination-log;当进程发生失败时,可以将消息写入这个文件,这样在调查原因时,可以通过 describe 查看到这个日志文件里面的内容;

我们并不知道容器中的应用何时会因意外终止退出,因此貌似可以通过捕捉错误,并将错误写入集中式的日志,以便后续进行错误的定位和排查;

处理应用日志

在开发环境中,容器中的应用正常应将日志输出到标准输出,这样可以使用 logs 命令方便的进行查看;但如果是写到文件中,则需要使用 exec cat 命令进行查看;

在生产环境中,则使用一个集中式的日志管理器(不然崩溃的 pod 被删除后,pod 上面的日志也会跟着消失),它一般会部署在某个 pod 上,统一接收所有的日志消息;一个常见的解决方案是使用 EFK 栈,它们是三个工具,一个负责收集(FluentID)、一个存储(ElasticSearch)、一个展示(Kibana);

开发和测试的最佳实践

开发过程中在 Kubernetes 之外运行应用

Kubernetes 要求将应用打包成镜像之后才能执行它,但这样显然不利于提高开发效率;如果应用的运行需要用到 Kubernetes 的某些功能,则可以模拟出来;API 服务器对于集群内和集群外的请求都是透明的,对它们一视同仁;

貌似唯一需要模拟的是 configMap 和 secret,其他的部分好像跟运行 docker-compose 没有特别大的差别;

在开发过程中使用 Minikube

可以使用 Minikube 来模拟集群,同时通过 minikube mount 的功能,将本地文件夹挂载到 minikube 虚拟机中,然后再通过 hostPath 载挂载到容器中,这样就可以让本地文件的更改,实时的传递到容器中了;

在 shell 中设置 DOCKER_HOST 变量,让 dockers daemon 指向 minikube 虚拟机中的 docker daemon 后,则可以通过本地的 docker 命令来实现对虚拟内的 docker操作

将本地的镜像推送到 minikube 虚拟机中

发布版本和自动部署资源清单

声明文件可以使用版本系统进行单独管理,每次提交更改后,就可以使用 apply 命令来更新当前的资源了;

甚至连手工的 apply 动作也可以进行自动后,可以采用第三方工具例如 kube-applier,它的作用有点像 github 的 webhook,当检测到有新提交的声明文件版本后,就会自动更新资源;

使用 Ksonnet 作为编写 YAML/JSON 声明文件的额外选择

Ksonnet 可以将声明文件模块化(使用 JSON 格式),然后通过组合共用的模块来减少编写重复的代码;

编写完成后,调用命令行进行转换

利用持续集成和持续交付

可以参考 Fabric8 项目 http://fabric8.io

18. Kubernetes 应用扩展

定义自定义对象

自定义对象可以让集群的使用者站在更宏观的角度来使用集群,由更抽象的高级对象来实现业务需求,而不再直接与 Deployment、Secret、Service、Pod 等基础对象打交道;整个集群的管理和使用变得更加傻瓜化了;

CustomResourceDefinitions 介绍

简称 CRD,自定义资源应该至少由两部分构成,一个是该自定义资源对象的定义,另一个是资源的管理组件,这样当用户创建某个资源实例时,集群才能够调用该资源的管理组件,去做余下的工作(创建各种基础资源对象);

定义好了后,就可以通过声明文件或者命令创建该种类型的资源了;

截止到这里,由于还没有创建控制器,因此实际上这些对象暂时还起不到任何实际的业务作用;

使用自定义控制器自动定制资源

控制器应该至少做两个动作,一个是监控 API 服务器发出的事件通知,另一个是向 API 服务器提交请求,创建相应的资源;

当控制器启动后,它实际上并不是直接向 API 服务器发请求,而是通过当前 pod 中的 sidecar 容器 kubectl-proxy 来发送请求的,sidecar 充当了一个代理的作用;

控制器的本质其实很简单,它其实就是持续监听相关资源的事件,然后将原来手工操作的动作,转换成代码来实现;这些动作包括创建资源、删除资源、更新资源、查看资源等;

由于控制器本质上就是一个帮忙自动化干活的 pod,因此在部署到生产环境时,一般可以将它部署为 Deployment 资源,这样如果出现故障,可以自动恢复;

当实现了控制器的这些自动化的操作后,意味着可以将它们封装起来,对外隐藏复杂性,对于最终用户来说,他只要提供一个源代码的仓库链接,就可以快速的将网站部署起来,完全不需要了解关于 kubernetes 的任何知识;这样就可以构建 PaaS 服务了;

验证自定义对象

如果让用户直接提交 YAML 文件来创建自定义的资源对象,会存在一个问题,即用户可能会提交无效的字段;因此集群在收到用户的资源创建请求时,有必要对其进行验证,确保 YAML 合法有效时,才进行创建;由于此时创建事件还没有发生,因此控制器无法完成验证的工作;因此需要由 API 服务器来完成(通过启用 CustomResourceValidation 特性来实现,在 1.8 以上版本中才有)

为自定义对象提供自定义 API 服务器

除了使用 CustomResourceValidation 来验证请求的合法性外,还有另外一种更激进的办法,即自定义一个 API 服务器;当有了这个自定义的 API 服务器后,甚至连原本的 CRD 对象都不需要了,可以直接写到自定义的 API 服务器中;此时多个 API 服务器形成了一种聚合,对客户端来说是无感知的,客户端的请求将被分发到不同的 API 服务器进行处理;

原来以为多 API 服务器的实现将是一个很复杂的功能,后果发现原来 Kubernetes 有内置了一个 APIService 的资源(本质上就是将该某些自定义资源对象的请求转发到提供该 APIService 的 pod,并不复杂,转发规则为 API 组名+版本号),只要创建该类型的资源,就可以实现多个 API 服务器;主 API 服务器会根据请求的属性,将其转发到这些多出来的 API,由它们做进一步的处理(但是仍然不可避免需要提供此 APIService 的 pod 写上一段自动化的代码,即将原来 CRD 控制器的代码移到这里来了);

除了可以自定义 API 服务器,还可以实现自定义 CLI 客户端,实现 Kubectl 不方便完成的更多自定义功能;

使用 Kubernetes 服务目录扩展 Kubernetes

服务目录是指列出所有可用服务的目录,然后用户根据需要选择对应的服务即可,而不需要自己去创建各种基础资源(如 Deployment、Service 等),简化用户对 Kubernetes 的使用门槛;

服务目录介绍

服务目录听上去很像是另外一种资源的抽象和封装,用户可以调用查看当前可用的服务目录,然后选择并创建某个服务;之后该服务会自动去创建各种基础资源,如 Deployment、Pod 等;

后来发现,它最大的作用并止于此,而是 Kuberbetes 可以跟集群进行协作;即有些云服务供应商,或者是内部的不同部门的团队,它们可以创建自己的 Kubernetes 集群,然后对外提供一些特定服务;其他集群的用户只要通过服务目录这个功能,来调用它们提供的服务即可,而无须在自己的集群上创建资源;

服务目录有四种内置的资源类型,分别为:

  • ClusterServiceBroker
  • ClusterServiceClass
  • ServiceInstance
  • ServiceBinding

运作流程

  • 集群管理员为服务代理创建一个 ClusterServiceBroker 资源,对应一个外部的服务代理商;
  • 集群通过该 Broker 资源,从服务代理商处获得它可以提供的服务列表,并为每种服务创建一个 ClusterServiceClass 资源;
  • 当集群内的用户想要使用某种服务时,只须创建一个 ServiceInstance 实例,并创建一个 ServiceBinding 绑定该 instance;之后集群内的 pod 就可以访问该外部服务了;

服务目录 API 服务器与控制器管理器介绍

服务目录跟集群一样,也有自己的 API 服务器、控制器管理器、etcd 数据库等三大件;通过这些组件,可以为外部其他集群的用户提供一些抽象后的高层级服务功能;

以上示意图是站在服务目录提供商集群的视角,实际的用户处于 External system;

Service Broker 和 OpenServiceBroker API

当创建好 ClusterServiceBroker 后,集群就会根据资源中的 URL,去代理处请求得到相应的服务列表,之后自动创建相应的 SerivceClass 与之对应;

提供服务与使用服务

当需要使用某个外部服务时,只须创建相应的 ServiceInstance 实例,并创建 ServiceBinding 与该实例进行绑定即可;

解除绑定与取消配置

当不再需要服务时,通过删除服务实例和服务绑定即可取消;

1
2
kubectl delete servicebinding <my-postgres-db-binding-name>
kubectl delete serviceinstance <my-postgres-db-name>

基于 Kubernetes 搭建的平台

由于 Kubernetes 方便拓展的特征,很多原本也研发 PaaS 平台的公司,也重新改写它们的产品,变成基于 Kubernetes 进行拓展;

红帽 OpenShift 容器平台

Kubernetes 中的很多资源还是非常底层的,OpenShift 对它们进行了封装,提供了更多的抽象资源,并提供参数化的模板,让开发者的工作变得更加简单起来,完全无须了解 Kubernetes 的知识,也能够轻松使用完成部署和维护的工作;

Deis Workfiow 与 Helm

另外一个有名的 PaaS 产品是 Deis 的 Workfiow(已被微软收购),该团队还开发了一个 Helm 工具,用来简化部署的过程;目前 Helm 已成为社区中的部署标准工具;

仅有镜像是不足以创建应用的,还需要配合声明文件;但对于很多常见的应用来说,例如数据库应用,编写这它们的声明文件就变成了一件重复造轮子的工作,为了避免这个问题,发明了 Helm 这个工具,它将应用和声明文件绑在一起,称为包,然后再结合用户的自定义配置文件,即可以形成应用的发行版本;就像很多人会共享镜像文件一样,也有很多人会共享做好的 Helm 包,当我们需要用到某个通包的软件时,应该先找一下有没有将其做成了 Helm 包,如果有的话,直接拿过来用就可以了;

示例如下:

OpenShift 本质上是一个基于 Kubernetes 开发的平台,它有自己优化后的 API 服务器和管理组件,因此,它并不能与用户的现在集群进行整合;而 Deis Workfiow 则可以部署到任何现有的 Kubernetes 集群中,因此 Workfiow 看起来更像是 Kubernetes 的一个插件,让集群的使用更加方便简单;

Helm 是 Kubernetes 的一个包管理器,类似于 Ubuntu 里面的 apt,或者 CentOS 里面的 yum;它由两部分组成,一部分是客户端,用于接收和发送用户指令;另一部分则运行在 Kubernetes 集群中(以 pod 的形式存在,通过在集群中安装 Tiller 组件来实现),用来接收客户端发出的指令,并在集群中执行相应的动作;

Helm 仓库地址:https://github.com/kubernetes/charts

使用 Helm 的流程:

  • 在仓库中找到合适的图表,git clone 到本地
  • 通过本地的 Helm 客户端,发送图表到集群中;
  • 搞掂!

19. 经验积累

K8s 的本质

感觉 Kubernetes 的本质就像一个部署的管理器,它可以将 YAML 所描述的抽象的资源,部署到集群中的机器上面去;这些抽象的资源包括应用、服务、任务、存储、管理器等;所有这些抽象的资源,都需要将它们镜像化和容器化;从而便资源的部署工作简化成创建和运行容器而已;

GKE 工作方式

对于 GKE,它自带一个客户端 gcloud 可用来实现集群层面的操作,包括创建、更新、删除集群等场景,增加和减少节点数量等;而集群内部的资源操作,则由 kubectl 处理;

管理集群

  • 创建一个 config 文件;

访问 pod 的几种方法

  • 在集群中创建一个 pod,在里面使用 curl 或者端口转发;

  • 通过 API 服务器作为代理

  • 运行命令: kubectl proxy,之后就可以通过代理 URL 来访问 pod 了;

  • 直接访问的 URL::/api/v1/namespaces/default/pods/kubia-0/proxy/
    通过代理访问的 URL: localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/


Kubernetes 实战
https://ccw1078.github.io/2020/08/12/Kubernetes 实战/
作者
ccw
发布于
2020年8月12日
许可协议