Book Review of Kubernates in Action

这几天看了一下 Kubernates in action 这本书,看的是中文版本。把自己觉得有用的记录一下。

怎么决定一个 pod 里面包含多少容器?基本上更倾向于分开。

  1. 它们必须要一起运行还是可以在不同主机运行?
  2. 他们代表的是一个整体还是相互独立的组件?
  3. 他们必须一起进行扩缩容还是可以分别进行?

pod 定义中的端口是展示性的,有没有设置不影响是否可以被连接,明确定义的 pod 端口可以方便查看,另外为端口命名还可以方便引用。

可以使用 kubectl explain pod.spec 查看具体字段的定义

可以使用 kubectl port-forward kubi-manual 8888:8080 把本地 8888 的请求转发到对应的 kubi-manual pod 里面。

使用标签组织 pod 可以方便的管理。

  1. app:指定 pod 属于哪个应用,组件,或者微服务。
  2. rel:指定 pod 中运行的程序是版本是 stable,beta,canary。

可以在 pod.yaml 文件中指定 label,也可以使用 kubectl label po kubia-manual rel=stable 增加标签。更改现有标签,需要使用 –overwrite 参数。

TODO:命令行指定标签之后,如果再次 apply 那个 yaml 会出现什么情况?

可以使用 kubectl label node worker1 gpu=true 给 node 打标签。

可以使用 nodeSelector 指定调度到符合条件的 node 上面。node 有一个 kubernates.io/hostname 标签是 node 的 hostname。

在使用多个 ns 的前提下,我们可以将包含大量组件的复杂系统拆分为更小的不同组,这些不同组也可以用于在多租户环境中分配资源,将资源分配为生产、开发和 QA 环境,或者以其他任何你需要的方式分配资源。资源名称只需要在命名空间内唯一即可,因此两个不同的命名空间可以包含同名的资源。

命名空间还可以用于允许某些用户访问某些特定资源,甚至限制单个用户可用的计算资源数量。

alias kcd=`kubectl config set-context $(kubectl cofig current-context) –namespace`

然后使用 kcd some-namespace 来切换。

命名空间之间是否网络隔离取决于 kubernates 使用的网络解决方案。

pod 的两个 probe 探针很重要

  1. readiness: 就绪探针,用来表示 pod 已经可以接受请求了。
  2. liveness: 存活探针,用来表示 pod 是不是还在正常工作。有几个属性需要注意:delay 容器启动之后等多久开始监测,timeout 监测超时,period 周期。

一个 pod 重新部署之后,可以通过 kubectl logs mypod –previous 看前一个 pod 的日志。

对于 liveness probe,一定要检查程序内部,而没有任何外部因素的影响。例如,当服务器无法连接到后端数据库的时候,前端 web 服务器的存活探针不应该返回失败。如果问题的底层原因在数据库中,重启 web 服务器容器不会解决任何问题,由于重启之后探针会继续失败,web 容器将被反复重启。同时保持探针轻量,无需在探针里面重试。

一个 RC(replicationController) 有三个主要部分

  1. label selector:选择 pod
  2. replica count:有几个副本
  3. pod template:pod 定义

如果某个 pod 发生了故障,可以将它从 rc 的管理范围里面移除(例如通过修改 label,增加一个 enable=true 选项),让控制器替换为新的 pod,这个旧的 pod 就任你处置了,用完后删除就可以了。

TODO:测试一下通过修改 label 从 rc,rs,deploy 里面移除 pod。

KUBE_EDITOR 环境变量用来指定执行 kubectl edit 命令的时候使用的编辑器,如果没设置,会使用 EDITOR 环境变量。

使用 kubectl delete rc 删除 rc 的时候,可以使用 –cascade=false 来保留 pod 继续运行。

节点可以被设置为不可以调度,防止 pod 被部署到节点。但是 DeamonSet 甚至会将 pod 部署到这些节点,因为无法调度的属性只会被调度器使用,DeaemonSet 管理的 pod 则完全绕过调度器。这是符合预期的,因为 DaemonSet 的目的是运行系统服务,即使在不可调度的节点上,系统服务通常也是需要运行的。

job 的 restartPolicy 只能是 OnFailure 或者 Never,不能是 Always。

可以通过 completions 和 parallelism 指定需要运行的总数和同时运行的数量。

service 允许通过 sessionAffinity 来指定会话亲和性。可选值有 None 和 ClientIP。因为 service 不是工作在 http 层面,所以不能基于 cookie 来做。

前面提到的 pod 模版里面给端口命名的格式在 service 里面有用了,可以通过端口名称引用定义的端口,这样好处是即使更换端口号也无需更改服务 spec。

当前端 pod 需要访问后端数据库服务 pod 时,可以通过名为 backend-database 的 service 将后端 pod 暴露出来

  1. 前端 pod 可以通过环境变量去获取服务的 IP 地址和端口。
  2. 通过 FQDN 连接,backend-database.default.svc.cluster.local 。

也可以使用 static service 包装外部资源(比如和其他公司合作,对方提供的服务)

  1. 只有ip:自定义 endpoint 指向外部 ip。
  2. 域名:通过 service.spec.externalName 新建一个 cname。

这样例如以后有需要的时候,可以把外部服务迁移到内部,而内部代码不用做任何改变。(似乎有点蛋疼。。。可能只有当服务够多的时候有意义吧,例如如果有 10 个服务使用到了这个 service 。。)

一般情况下,node 上面会通过 iptables 把对 service 的请求随机转发到 pod 上面。把 externalTrafficPolicy 字段设置为 Local 可以避免多余的转发,只会到本地的 pod。这样会带来一些问题,没有 pod 的机器上面将不能访问通,负载将可能不再均衡,例如一个机器上面有多个 pod 的时候。

TODO: 对于 headless 服务,没有 clusterIP,可以通过域名访问,那么对于有 clusterIP 的,是不是也可以通过域名访问?

需要测试网络的时候,可以使用 tutum/dnsutils 容器,里面包括了 nslookup 和 dig。

设置 emptyDir 的属性 midium: Memory 可以建立内存文件系统。可以使用 gitRepo 建立 gitrepo 类型的 vol,会自动获取代码,私有服务需要配置对应的凭证(imagePullSecrets)。

可以使用 sidecar 容器配合主容器做一些事情,例如自动同步更新 git 代码,建立访问 API 的代理等。

TODO: 可以使用 awsElasticBlockStore 创建 aws 的磁盘挂载,需要测试一下例如新建删除是如何管理的,通过什么来识别的。

PV 持久卷可以设置 accessModes 例如 ReadWriteOnce, readOnlyMany 等,可以设置 persistentVolumeReclaimPolicy 为 Retain 保留数据。

PV 需要提前声明,才能被 PVC 使用。在云服务里面,可以事先定义 StorageClass 来提供给 PVC 使用,定义好 provisioner 提供商即可。

Dockerfile 里面 ENTRYPOINT 的两种形式,区别在于 pid 1 的进程是什么,1 是 /bin/sh。

  1. shell 形式:ENTRYPOINT node app.js
  2. exec 形式:ENTRYPOINT ["node", "app.js"]

Dockerfile 里面的配置和 kubernates 里面的对应:

  1. ENTRYPOINT: command 可执行文件
  2. CMD: args 传递的参数,参数里面字符串不用引号,数值需要引号。

ConfigMap 数据可以通过环境变量或者卷文件的形式传递给容器。

  1. –from-file=bar=foobar.conf:bar 的值为文件内容
  2. –from-file=foo.json:相当于 –from-file=foo.json=foo.json
  3. –from-file-config-opts/:config-opts 目录里面的每个文件都会用文件名和文件内容创建键值对。
  4. –from-literal=some=thing:创建 some=thing

把 ConfigMap 引入环境变量:

  1. 通过 spec.containers.env.valueFrom.configMapKeyRef.{name,key} 引用名为 name 的 configmap 里面的 key。设置 configMapKeyRef.optional: true 可以设置为可选。
  2. 通过 spec.containers.envFrom.prefix: pre_ 设置引入所有 pre_ 开头的变量。
  3. 如果 ConfigMap 里面有键名格式不正确,创建环境变量的时候会被忽略而不会报错。例如 CONFIG_FOO-BAR 这样的。

可以使用 volumeMounts.subPath 只挂载部分卷而不是全部的,例如只挂载里面某个文件,某个子目录。

  1. 这样有一个问题,据说是这么挂载的时候,更新 ConfigMap 不会更新文件。TODO: 检查是不是这样的。

通过 defaultMode 可以改变挂载属性。

ConfigMap 更新之后,卷会自动更新,但是卷对应的文件更新可能会花一些时间(例如数分钟)。

  1. 文件更新之后需要你的程序重新读入才能真正产生影响。
  2. 如果不支持自动读入,那可能会导致新建的 pod 用的是新的 ConfigMap,旧的依然用的是旧的。
  3. 并且自动更新在各个 pod 出现的时间也有区别,可能会有先后。

Secret 和 ConfigMap 类似,也可以使用环境变量或者卷的形式传递给 pod。Secret 只会存在于内存中。

  1. 采用 ConfigMap 存储非敏感的文本配置数据。
  2. 采用 Secret 存储天生敏感的数据,如果配置文件同时存在敏感和不敏感的,那应该用 Secret。

可以使用 Downward API 获取 pod 的元信息。

  1. pod 名称
  2. pod ip
  3. pod 所在的 ns
  4. pod 运行的 node 名称
  5. pod 运行的所属账户的名称
  6. 每个容器请求的 CPU 和内存的使用量
  7. 每个容器可以使用的 CPU 和内存的限制
  8. pod 的标签
  9. pod 的注解

可以通过 spec.containters.env.valueFrom.fieldRef.fieldPath: metadata.name 引用 metadata 的数据。也可以使用 Downward API 卷获取这些数据。

还可以通过和 API 服务交互获取数据,token 卷会自动 mount 到 pod 里面,也可以通过一个 kubectl proxy sidecar 容器来转发。

Docker image 的 tag 是版本号,需要能保证某个 tag 固定指向某个 image 版本,最好不要覆盖已经发布的 tag 对应的 image,否则容易出现不一致的情况。对于 latest(或者不指定) tag,imagePullPolicy 默认是 Always,如果指定来其他 tag,默认策略是 IfNotPresent。

可以使用 kubectl rolling-update kubia-v1 kubia-v2 –image-luksa/kubia:v2 来升级 RC replicationcontroller。执行的时候,会创建一个 kubia-v2 的 rc,然后通过给 rc 和 pod 增加 label 并通过修改 replicas 数量逐渐用新的代替旧的。执行升级过程中,如果 kubectl 失去网络,可能会导致 rc 和 pod 处于中间状态。

使用 Deployment 的时候,实际的 pod 是由 Deployment 和 ReplicaSet 共同管理的。

Deployment 升级的时候,只需要修改 deploy 的定义即可。升级有两种策略

  1. Recreate:旧的全部删除之后才开始创建新的。
  2. RollingUpdate:渐进式替代,升级过程中会有新旧版本共存状态。

使用 spec.minReadySeconds 指定新 pod 最小存活时间。

使用 kubectl set image deployment kubia nodejs=luksa/kubia:v2 修改为新版本的 image 进行升级。

TODO: 可以通过 kubectl 命令直接操作修改,也可以通过 yaml 方式修改,那么如何保证双方状态一致?要不下次执行 yaml 的时候可能会把一些 kubectl 的操作回滚。

更改 ConfigMap 资源不会触发升级操作,如果需要通过修改配置触发更新,那可以新建一个新的 ConfigMap,然后修改 pod 模版使用这个新的。

使用 kubectl rollout undo deployment kubia 可以回滚到上一个版本。

undo 命令也可以在滚动升级过程中执行,并直接停止滚动升级。升级过程中创建的新的 pod 会被删除并被老版本替代。

使用 kubectl rollout history deployment kubia 可以查看旧版本。使用 –to-revision=1 可以回滚到特定版本。创建 deploy 时使用 –record 记录 CHANGE-CAUSE。

不应该手动删除 ReplicaSet,如果这么做可能会丢失 Deploy 的历史版本记录而导致无法回滚。

revisionHistoryLimit 属性可以限制历史版本数量。

使用 kubectl rollout status 可以查看升级过程。

使用 maxSurge 和 maxUnavailable 控制升级的速度。

使用 kubectl rollout pause deployment kubia 可以暂停升级,这个时候可以做金丝雀测试。使用 kubectl rollout resume deploy kubia 恢复。

默认情况下,如果 10 分钟内不能完成升级会被视为失败。可以设置 spec.progressDeadlineSeconds 来设置这个时间。

StatefulSet 最初被叫做 PetSet,因为 pet 是有名字的。。。。无状态的类似牛,都没名字。。

StatefulSet 做缩容一次只会操作一个节点,在有实例不健康的情况下是不允许做缩容操作的。

Kubernates 必须保证两个拥有相同标记和绑定相同持久卷声明的有状态的 pod 实例不会同时。一个 StatefulSet 必须保证有状态的 pod 实例的 at-most-one 语义。也就是说一个 StatefulSet 必须在准确确认一个 pod 不在运行后,才会去创建它的替换 pod。

yaml 文件里面可以使用 — 来区分多个资源,也可以使用 kind: List 创建多个资源。

StatefulSet 里面,每个节点挂载的数据卷有两个方式实现:

  1. 使用 volumeClaimTemplates 挂载不同的卷。
  2. 使用 PVC 挂载相同的卷,但是在卷里面使用不同的目录区分各节点的数据。

节点失败的时候,普通 pod 会被如何处理:

  1. 节点会被标记为 NotReady。上面运行的 pod 状态变成 Unknown。
  2. pod Unknown 一段时间之后,kubernates 标记这些 pod 为删除,同时安排其他节点新建对应的 pod。
  3. 节点重新加入后会知道需要删除上面的 pod,执行删除。

对于 StatefulSet:

  1. pod 会被标记为 Unknown。
  2. 执行手动强制删除 kubectl delete po kubia-0 –force –grace-period 0
  3. kubernates 会调度其他节点新建 pod。

可以使用 kubectl get pods –watch 观察 pod 事件。使用 kubectl get events –watch 观察控制器发出的事件。

kubernates 调度器的作用是为 pod 找到可用节点,然后选择最优节点。可以通过 spec.schedulerName 来指定调度器。

跨 pod 的网络是通过 Container Network Interface(CNI) 插件建立的。跨整个集群的 pod 的 IP 地址必须是唯一的,所以跨节点的网桥必须使用非重叠的地址段,防止不同的 pod 拿到同一个 IP。例如不同节点分别使用 10.1.1.0/24 和 10.1.2.0/24 。

让你的应用变得高可用:

  1. 运行多实例来减少宕机可能性。
  2. 对不能水平扩展的应用使用领导选举机制。可以通过 sidecar 容器做选举的逻辑,选举完毕之后通知主容器结果即可。这样的 sidecar 可以复用。

kube-schedular 容器的选举结果可以观察 holderIdentity 字段,还可以看看 acquireTime 和 renewTime。

serviceaccount 的缩写是 sa。每个 pod 都与一个 sa 相关联。pod 只能使用同一个命名空间的 ServiceAccount。

pod 的 manifest 文件里面,可以指定账户名称。不指定会使用这个命名空间里面默认的。

不需要读取任何集群元数据的 pod 应该运行在一个受限制的账户下。

将 spec.hostNetwork 设置为 true 可以使用宿主节点的网络命名空间。

不要混淆使用 hostPort 的 pod 和通过 NodePort 服务暴露的 pod。

  1. NodePort 服务会把到达宿主机的请求随机转发到 service 里面的 pod 。
  2. hostPort 只会在运行了这个 pod 的节点绑定这个端口,NodePort 会在集群所有节点上面绑定这个端口。

hostPort 最初是用于暴露 DeamonSet 部署在每个节点的系统服务的,也用于保证一个 pod 的两个副本不会被调度到同一个节点。

pod spec 里面的 hostPID 和 hostIPC 可以让容器使用宿主节点的 PID 和 IPC 命名空间,允许容器看到宿主的全部进程并与他们进行 IPC 通信。

securityContext 的一些设置:

  1. runAsUser 指定容器运行的用户。runAsAny 允许任何用户和组运行。
  2. runAsNonRoot 可以阻止容器使用 root 运行。
  3. privileged 可以允许 pod 在特权模式下运行。
  4. 通过 capabilities 可以允许或者禁止容器进行特定的系统调用。
  5. 通过 fsGroup 和 supplementalGroups 可以设置挂载卷的一些权限。

通过 PodSecurityPolicy 可以设置默认的安全配置。通过 NetworkPolicy 可以设置 pod 间网络规则。

调度器在调度时并不关注各类资源在当前时刻的实际使用量,而只是关心节点上部署的所有 pod 的资源申请量之和。调度算法必须要保证这些 pod 需要这些用量的时候可以提供。

内存不足时哪个进程会被杀死?BestEffort 等级的 pod 会首先被杀掉,其次是 Burstable 的 pod,最后是 Guaranteed 的 pod。

可以通过调整 rc, rs, deploy 等可伸缩资源的 replicas 字段来手动实现 pod 中应用的横向扩容。

集群必须运行了 Heapster 才能实现自动伸缩。

自动伸缩大致逻辑是,设置目标用量,例如 cpu 使用率,qps 之类,然后由 Autoscaler 根据目前的 pod 数量和各自的运行情况,计算达成目标的 pod 数量,然后调整可伸缩资源来做扩缩容。

使用 kubectl get hpa 显示 HPA 资源。

如果增加副本数量不能导致被观测度量的平均值线性(或者接近线性)下降,那么 autoscaler 就不能正常工作。

Cluster Autoscaler 负责在节点资源不足的时候,自动增加节点。它也会在节点长时间使用率比较低的情况下下线节点。

  1. 只有当 Cluster Autoscaler 知道节点上面运行的 pod 能够重新调度到其他节点的时候节点才会被归还。

节点也可以被手动标记为不可调度,并排空节点

  1. kubectl cordon <node> 标记节点为不可调度(但不会对其上 pod 做任何事)
  2. kubectl drain <node> 标记节点为不可调度,随后疏散其上所有 pod

主节点有一个污点,污点包含一个 key,value,以及一个 effect,格式是 <key>=<value>:<effect>。主节点包含一个 node-role.kubernates.io/master:NoSchedule 的污点(value为空)。除非有 pod 指定可以容忍这个污点,否则 pod 不会调度到这个节点。

pod 的 Tolerations 字段会说明可以容忍的污点,例如 node-role.kubernates.io/master=:NoSchedule 。注意污点和容忍度这里的区别,差了一个 = 。

使用 kubectl taint node node1.k8s node-type=production:NoSchedule 增加污点.

pod 定义里面增加对应的 tolerations 才能把 pod 部署上去。

tolerations:

  • key: node-type operator: Euqal value: production effect: NoSchedule

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

通过 spec.affinity.nodeAffinity 可以实现比 nodeSelector 复杂的调度规则。还有 spec.affinity.podAffinity 和 podAntiAffinity 。

应用必须预料到会被杀死或者重新调度

  1. 预料到本地 IP 和主机名会变化。
  2. 预料到写入磁盘的数据会消失。使用存储卷来跨容器持久化数据。

rs 本身不关心 pod 是否处于死亡状态,只关心 pod 的数量是否匹配期望的数量。crash 的时候也不会重新调度 pod,因为通常调度到其他 node 也是这么个情况,一般认为这些 node 都是一样的。

可以给 pod 增加 pre-stop 和 post-start hook。

  1. post-start hook 是和主进程并行执行的。在钩子执行完毕之前,容器会一直停留在 Waiting 状态,其原因是 ContainerCreating 。因此 pod 的状态是 Pending 而不是 Running。如果钩子失败或者返回了非 0 的状态码,主容器会被杀死。
  2. 钩子程序失败的话,不好 debug,容器重启的话日志就没有了,不过可以通过写入到一个 emptyDir 的卷里面,让钩子程序向这个存储写入内容来解决。
  3. pre-stop 钩子是在容器被终止之前执行的。并且会在执行完钩子程序之后才向容器进程发送 SIGTERM 信号。
  4. pre-stop 钩子无论执行成功失败都不会阻止容器被停止。

将重要的关闭流程替换为专注关闭流程的 pod。

当且仅当你的应用准备好处理进来的请求的时候,才去让就绪探针返回成功。

给所有资源都打上标签,而不仅仅是 pod。标签可以包含如下的内容:

  • 资源所属的应用(或者微服务)的名称
  • 应用层级(前端,后端,等等)
  • 运行环境(开发,测试,预发布,生产等等)
  • 版本号
  • 发布类型(稳定版,金丝雀,蓝绿开发中的绿色或者蓝色等等)
  • 租户(如果你在每个租户中运行不同的 pod 而不是使用命名空间)
  • 分片(带分片的系统)

资源应该至少包括一个描述资源的注解和一个描述资源负责人的注解。在微服务框架中,pod 应该包含一个注解来描述该 pod 依赖的其他服务的名称。

可以指定 spec.containers.terminationMessagePath 路径,将来 pod 有问题会读取这个文件里面的内容显示在 describe 结果里面。

通过自定义 CustomResourceDefinitions CRD 对象,可以做到类似 deploy 那样,自动帮你建立好 rs 和 pod,并且还可以避免重复的写冗长的 pod 定义之类。

实现思路是,需要配合建立一个自定义控制器,监听 API 上面的 CRD 对象的事件,例如有新建的时候,像 API 提交对应的 deploy pod 等新建请求。删除 CRD 的时候,删除相关联的资源。

其他资源:

  1. https://github.com/box/kube-applier 可以做到自动检出 yaml 执行 apply。
  2. https://ksonnet.io/docs/ 可以方便的复用 yaml 文件的配置,让你随意组合他们。
  3. https://fabric8.io/ 也是一个自动部署的工具。
  4. https://helm.sh/ 是一个 kubernates 包管理器,可以类似装包一样部署 pod。其实就是他们事先写好了一堆的 pod 定义。有需要自己写的时候可以先来这里看看。