Kubernetes

Kubernetes

Kubernetes 发音 /Koo-ber-nay-tace/ 或者 /Koo-ber-netties/

解决的问题

微服务部署和配置困难。

  • 简化应用程序的部署。开发者无须知道背后有多少台机器需要部署,也无需知道自己的 APP 运行在哪几台机器上。
  • 对于资源的更为高效的利用。K8S 可以在任意时刻将 APP 迁移到其他 worker 节点上,以便更好的利用资源。
  • 健康检查。node 挂掉后,自动将 APP 调度到其他节点上。
  • 自动伸缩。K8S 可以自己关注资源的利用率,动态调整 APP 的实例数量。

概念解释

VM 和容器

APP 运行在 VM 中

APP 运行在容器中

Pod

一个 Pod 是一组互相协作的容器构成的,一般情况下,一个 Pod 仅包含一个容器:

容器设计初衷就是为了一个容器仅运行一个进程的理念去运行程序,如果运行多个,那么它们的状态、生命周期、日志等都难以管理。所以,我们需要站在更高层级来去管理、组织、单元化分布在多个容器中的多个进程,这就是引入 Pod 的意义。Pod 来去给一组容器中的各个进程提供一种假象,让这些进程认为各自都运行在一个容器里似的,同时在需要隔离的时候,也能提供隔离的能力。

运行在相同 Pod 中的各个容器,它们的 IP 和端口资源是共享的,所以不要让它们引起端口冲突。Pod 是轻量级的,应该将一个应用程序分为多个 Pod,而不是在一个 Pod 中塞入所有东西。仅当这些进程是互相协作、必须同时运行、必须同时扩展、对外是一个整体服务等这种情况下,可以考虑将多个容器放在一个 Pod 中进行管理。

使用 cubectl create 来创建一个 Pod:

$ kubectl create -f kubia-manual.yaml

创建好之后,可以通过命令 kubectl get po kubia-manual -o yaml 来查看此 Pod 的完整 Yaml 配置。可以使用命令 kebectl get pods 可以查看当前的 Pod 列表,以及它们的状态。

通过 Docker 查看应用日志:

$ docker logs <容器 ID>

通过 K8S 查看 Pod 日志:

$ kubectl logs kubia-manual
$ kubectl logs kubia-manual -c <具体的容器名>

如果不想通过 Service 的方式访问 Pod,而是要简单调试、或其他原因想访问 Pod,那么 K8S 提供了端口转发功能,如下流量将本地的 8888 端口流量转到 Pod 的 8080 端口:

$ kubectl port-forward kubia-manual 8888:8080

为了更方便的管理、组织 Pod,还可以给 Pod 打标签。可以在创建 Pod 的时候打标签:

metadata:
  labels:
    key1=value1
    key2=value2

这样可以给 kubectl get 命令使用 --show-labels 来显示所有 Pod 的标签。


可以给 Pod 标注元信息,放置更多的描述信息:

metadata:
  annotations:
    kubernetes.io/created-by:
      {"kind":"SerializedReference", "apiVersion":"v1","reference":{"kind":"ReplicationController", "namespace":"default", ...

一个 Pod 可以拥有多个标签,所以选中此标签和其他标签的时候,里面选中的资源可能有重叠。而 K8S 也可以通过命名空间的方式来组织 Pod,来提供一种管理不同 Pod 的作用域,可以将这些 Pod 划分为不重叠的、独立的组,例如生产、开发、QA环境组等。

通过命令 kubectl get ns 查看所有命名空间,kubectl 命令默认情况下展示的都是 default 命名空间的资源,通过如下命令展示其他命名空间的资源:

$ kubectl get po --namespace kube-system

创建 Pod 的时候指定命名空间

kind: Namespace

删除 Pod 的命令 kubectl delete po kubia-gpu 会发送 SIGTERM 信号给所有的进程,然后等待最多 30 秒,否则直接发送 SIGKILL 信号。

Docker 运行 K8S

需要将它们打包制作成镜像才可以运行在 K8S 中的应用。<tag> 是可选的,如果不提供,默认应该是 latest,其表示的含义是版本号

$ docker run <image>:<tag>

K8S 集群

下面是 K8S 集群的概览图:

K8S 运行 APP

开发者告诉 K8S 的 master 节点,哪些 APP 必须部署在一起,每一个 APP 需要部署几个实例,K8S 就会自动按照要求将这些 APP 部署到 worker 节点上。

K8S 由 master 和 worker 节点构成,其中 K8S 管理控制台位于 master 节点上,可以通过此平台管理整个 K8S 系统;而 worker 节点用于实际运行 APP。

master 节点包含

  • K8S API Server
  • Scheduler: 调度 APP,分配 worker 节点等
  • Control Manager: 跟踪 worker 节点状态,处理节点失败等
  • etcd: 存储集群的配置信息

worker 节点包含

  • Docker: 或者其他容器
  • Kubelet: 调用 API Server,控制 Container
  • K8S Service Proxy: 流量的负载均衡

在 K8S 中运行程序,必须首先将其制作成容器的镜像,然后推送到镜像仓库,然后告诉 K8S 此 APP 的描述文件。

控制器

HTTP 探针:

spec:
  containers:
  - image: luksa/kubia-unhealthy
    name: kubia
    livenessProbe:
      httpGet: # HTTP GET 探针
        path: /
        port: 8080
      initialDelaySeconds: 15 # 15 秒之后发送第一个探针

ReplicationController 保证 Pod 总是处于运行状态,如果某个 Pod 因为某些原因宕掉了,那么 ReplicationController 将会创建一个新的 Pod 以替代旧的 Pod。

使用命令 kubectl create -f kubia-rc.yaml 创建一个 ReplicationController :

apiVersion: v1
kind: ReplicationController # 指定类型
metadata:
  name: kubia
spec:
  replicas: 3 # 指定需要 3 个 Pod
  selector:
    app: kubia # Pod Selector
template: # template 下面的这些是创建一个新的 Pod 所需要的模板
  metadata:
    labels:
      app: kubia
  spec:
    containers:
    - name: kubia
      image: luksa/kubia
      ports:
      - containerPort: 8080

ReplicationController 可以随时通过命令 kubectl edit rc kubia 修改 Pod 模板:


ReplicaSet 是用来替代 ReplicationController 的,它有更多的 Pod Selector。创建一个 ReplicaSet :

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

它有更为丰富的 label 选择器:

selector:
  matchExpressions:
    - key: app # Pod 必须有一个 key 是 app 的 label
      operator: In
      values:
        - kubia # label 的 value 必须位于 kubia 里面

DaemonSets 确保一个 Worker Node 只运行一个 Pod 副本,假设你运行的是一个日志采集器程序,那么可能会使用 DaemonSets

Service:与 Pod 沟通

K8S Service 提供了对于一组提供相同服务的 Pod 的一个固定入口。

通过 kubia-svc.yaml 文件创建 Service,port: 80 表示 Service 监听在 80 端口,targetPort: 8080 表示 Service 将要用某种路由算法转发到多个 Pod 的 8080 端口,app: kubia 表示所有具有 app = kubia 标签的 Pod 都属于这个 Service:

spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia

通过 kubectl get svc 命令查看默认命名空间的所有的 Service 列表,EXTERNAL-IP 表示外网 IP:

NAME       CLUSTER-IP   EXTERNAL-IP    PORT(S)        AGE
kubernetes 10.3.240.1   <none>         443/TCP        35m
kubia-http 10.3.246.185 104.155.74.57  8080:31348/TCP 1m

会话 Session 附着,某一个 Client 的请求总是转发到某一个 Pod:

spec:
  sessionAffinity: ClientIP

可以通过 ENV 环境变量,查看 Service 的地址:

$ kubectl exec kubia-3inly env
KUBIA_SERVICE_HOST=10.111.249.153
KUBIA_SERVICE_PORT=80

也可以通过 FQDN 查看 Service 地址,其中 backend-database 表示 Service 名称,default 表示命名空间,svc.cluster.local 是一个可配置的集群 Domain 的前缀:

backend-database.default.svc.cluster.local

验证是否可以联通:

$ kubectl exec -it kubia-3inly bash

[email protected]:/# curl http://kubia.default.svc.cluster.local
You’ve hit kubia-5asi2

[email protected]:/# curl http://kubia.default
You’ve hit kubia-3inly

[email protected]:/# curl http://kubia
You’ve hit kubia-8awf3

查看一个 Service 背后可以访问哪些 Pod :

$ kubectl describe svc kubia
Selector: app=kubia
Endpoints: 10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080

刚才讨论的只是能让 K8S Cluster 内的各个 Pod 访问 Service ,有三种方式可以让外部的 Client (外网) 访问 Service:

  1. 指定 Service 的类型为 NodePort
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30123
  selector:
    app: kubia

  1. 指定 Service 的类型为 LoadBalancer,这是 NodePort 的扩展类型
metadata:
  name: kubia-loadbalancer
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia

获取 LoadBalancer 的 IP:

$ kubectl get svc kubia-loadbalancer
NAME               CLUSTER-IP     EXTERNAL-IP    PORT(S)      AGE
kubia-loadbalancer 10.111.241.153 130.211.53.173 80:32143/TCP 1m

  1. 创建 Ingress 资源

一个 Ingress,就可以管控多个 Service。如果使用 LoadBalancer 的话,那么每个 Service 必须配备一个 IP :

创建 Ingress :

apiVersion: extensions/v1beta1
  kind: Ingress
metadata:
  name: kubia
spec:
  rules:
  - host: kubia.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: kubia-nodeport
          servicePort: 80

获取 Ingress IP:

$ kubectl get ingresses

配置 DNS:

$ vi /etc/hosts
192.168.99.100 kubia.example.com

使用 Ingress 访问 Pod:

ConfigMaps 和秘钥

ConfigMap: 传递参数给 K8S 的 Pod

在 Yaml 中通过 env 参数定义环境变量,这个环境变量是定义在 Container 级别,而非 Pod 级别:

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

使用 ConfigMap 避免硬编码环境变量。

从命令行创建 ConfigMap

$ kubectl create configmap myconfigmap --from-literal=foo=bar --from-literal=bar=baz --from-literal=one=two
$ kubectl get configmap myconfigmap -o yaml

文件或者目录创建 ConfigMap

# key 就是文件名,value 就是文件内容
$ kubectl create configmap my-config --from-file=config-file.conf
# 目录中的每个文件名都作为 key
$ kubectl create configmap my-config --from-file=/path/to/dir

使用命令 kubectl get configmap fortune-config -o yaml 查看 ConfigMap 定义:

apiVersion: v1
data:
  sleep-interval: "25" # 只有一个配置项
kind: ConfigMap
metadata:
  creationTimestamp: 2016-08-11T20:31:08Z
  name: fortune-config # ConfigMap 的名字,通过这个名字来引用这个 Config
  namespace: default
  resourceVersion: "910025"
  selfLink: /api/v1/namespaces/default/configmaps/fortune-config
  uid: 88c4167e-6002-11e6-a50d-42010af00237

Pod 通过环境变量或者 configMap volume 来使用 ConfigMap :

ConfigMap 传递给容器,作为环境变量存在,如果 Pod 引用了不存在的 ConfigMap 那么 Pod 将拒绝启动,当然也可以将 ConfigMap 作为可选的 configMapKeyRef.optional: true

apiVersion: v1
kind: Pod
metadata:
  name: fortune-env-from-configmap
spec:
  containers:
  - image: luksa/fortune:env
    env:
    - name: INTERVAL # 给容器定义了一个环境变量
      valueFrom: # 从 ConfigMap 引用这个值
        configMapKeyRef:
          name: fortune-config # ConfigMap 的名字
          key: sleep-interval # ConfigMap 引用的 key 

ConfigMap 作为命令行参数

apiVersion: v1
kind: Pod
metadata:
  name: fortune-args-from-configmap
spec:
  containers:
  - image: luksa/fortune:args
    env:
    - name: INTERVAL
      valueFrom:
        configMapKeyRef:
          name: fortune-config
          key: sleep-interval
    args: ["$(INTERVAL)"] # 命令行参数引用环境变量参数

使用 configMap volume 来将 ConfigMap 作为文件:

apiVersion: v1
kind: Pod
metadata:
  name: fortune-configmap-volume
spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: config
      mountPath: /etc/nginx/conf.d # 将 volume ConfigMap 挂载到这个位置
      readOnly: true
  volumes:
  - name: config # volume 引用 ConfigMap
    configMap:
      name: fortune-config

查看挂载的目录下是否有文件:

$ kubectl exec fortune-configmap-volume -c web-server ls /etc/nginx/conf.d

每一个 Pod 都有一个自动附带的 secret volume:

$ kubectl get secrets
$ kubectl describe secrets
ca.crt: 1139 bytes
namespace: 7 bytes
token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

创建 Secret:

$ openssl genrsa -out https.key 2048
$ openssl req -new -x509 -key https.key -out https.cert -days 3650 -subj/CN=www.kubia-example.com
$ echo bar > foo
$ kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert --from-file=foo

Secret 的每一项以 Base64 展示,ConfigMap 都是明文展示。如下是 Secret 的 Yaml 文件展示:

$ kubectl get secret fortune-https -o yaml
apiVersion: v1
data:
  foo: YmFyCg==
  https.cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCekNDQ...
  https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcE...
kind: Secret

Volume

更新

滚动更新:

滚动更新策略:

spec:
  strategy:
    rollingUpdate:
      maxSurge: 1 # 默认值是 25%
      maxUnavailable: 0 # 默认值也是 25%
    type: RollingUpdate

当前线上存活 Pod 是 3 个,maxSurge = 1 表示部署的时候,最多存在的 Pod 个数是 3 + 1 = 4 个,maxUnavailable = 0 表示部署的时候,至少 3 - 0 = 3 个 Pod 必须处于存活状态:

maxUnavailable = 1 的时候,那么至少 3 - 1 = 2 个 Pod 必须处于存活状态:

控制更新,用一小部分流量验证新的版本:

$ kubectl set image deployment kubia nodejs=luksa/kubia:v4
# 暂停更新
$ kubectl rollout pause deployment kubia
# 恢复更新
$ kubectl rollout resume deployment kubia

minReadySeconds 一个新的 Pod 起来之后,多久才可以对外宣传自己准备好接受请求了,时间不到,更新不会继续:

spec:
  minReadySeconds: 10
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate

readiness probe 如果失败的话,那么流量不会转发到这台 Pod 上:

K8S 机制

K8S 架构

(1)使用 etcd 存储资源

Pod、ReplicationControllers、Services、Secrets 等的 manifests 都存储在 etcd 中,唯一与 etcd 有关联的是 API Server,其他所有组件的读或者写都得通过 API Server 才能访问 etcd,在 Kubernetes 中,它所有的数据都存储在 /registry 路径下:

$ etcdctl ls /registry

(2)API Server

API Server 收到请求后是如何执行的:

API Server 如何通知 Client 资源发生了变动:

(3)kubelet

  • 通过 API Server 创建 Node 资源
  • 通过 API Server 监控哪些 Pods 调度给了 Node,然后告诉 Docker 从镜像启动 Container
  • 持续不断汇报状态、事件、资源消耗给 API Server

(4)kube-proxy

本节部分内容来自阿里云社区《深入浅出K8S》

在 K8S 集群中,服务的实现,实际上是为每一个集群节点上,部署了一个反向代理 Sidecar。而所有对集群服务的访问,都会被节点上的反向代理转换成对服务后端容器组的访问。

K8S 集群的服务,本质上是负载均衡,即反向代理;在实际实现中,这个反向代理,并不是部署在集群某一个节点上,而是作为集群节点的边车,部署在每个节点上的。

K8S 集群节点实现服务反向代理的方法,目前主要有三种,即 userspace、iptables 以及 ipvs。

userspace 模式:作为早期实现,proxy 运行了一个进程,负责配置 iptables 路由规则,以便将连接重定向到 proxy server:

现在更好的实现模式是 iptables 模式:直接将 packets 重定向到随机选择的后端的 Pod 上,而无须额外的将它们再转发到 proxy server 上。userspace 模式,packets 必须经过 proxy server,也就是必须到 user space,而 iptables 模式则直接在 kernel space 区。两者有比较显著的性能差异

另外一点,userspace 模式执行的是 round-robin 的方式选择 Pod,而 iptables 模式是随机选择 Pod

K8S 网络

K8S Inter-pod 网络通过 Container Network Interface (CNI)插件建立。

Pod A 与 Pod B 之间的 IP 没有经过 NAT 转换:

一个 Node 里边的 Pod A 与 Pod B 连接在同一个网桥上:

不同 Node 里边的 Pod A 与 Pod B 需要以某种方式才能通信:

K8S 调度器

默认调度算法:

  • 过滤出符合要求的能够被调度的节点。
  • 对这些节点进行排序,选择最佳的一个节点,如果多个节点具有相同的分数,那么会采用 round-robin 算法来选取一个节点。

K8S 水平伸缩

每一个 node 上的 Kubelet 里面都有一个 cAdvisor ,负责收集各种指标,然后通过位于集群级别的 Heapster 进行指标聚合。

根据一个或者多个指标计算 APP 所需要的 Pods 数量:

最后 Autoscaler Controller 更新副本数量:

K8S 高可用

高可用需要运行多个 master 节点:

K8S 资源控制

Hard 约束的 Pod :

apiVersion: v1
kind: Pod
metadata:
  name: requests-pod
spec:
  containers:
  - image: busybox
    command: ["dd", "if=/dev/zero", "of=/dev/null"]
    name: main
    resources:
      requests:
        cpu: 200m # CPU 200 millicores,1/5 单核 CPU 的时间
        memory: 10Mi # 申请的内存大小: 10MB
apiVersion: v1
kind: Pod
metadata:
  name: limited-pod
spec:
  containers:
  - image: busybox
    command: ["dd", "if=/dev/zero", "of=/dev/null"]
    name: main
    resources:
      limits:
        cpu: 1 # 最多允许容器使用 1 个 CPU
        memory: 20Mi # 最多允许使用 20 MB

Kublet 包含了一个 Agent 称作 cAdvisor,来收集 Node 上的多个 Container 的资源消耗情况。将整个集群的整个静态数据收集起来需要运行一个额外的组件:Heapster。Heapster 作为一个正常的 Pod 而存在。

最佳实践

(1)妥善处理所有客户请求:

应用程序正在启动的时候,确保没有请求进来:

  • 指定 readiness probe。如果不指定的话,Pod 就会被认为已经准备好接受请求了。

应用程序正在关闭的时候,妥善处理当前现存的客户请求:

pre-stop hook 防止 broken 链接:

lifecycle:
  preStop:
    exec:
      command:
      - sh
      - -c
      - "sleep 5"

参考