前言

Kubernetes (音 kubə'netis) 这个名字来自希腊语,意思是「舵手」或「领航员」,它是一个起源于 Google 的开源项目,允许自动化部署、管理和扩容容器化应用,它现在已成为容器编排的事实标准。

其实 Kubernetes 的简称 k8s 存在感更强,这个简称缘由是单词中间刚好是 8 个字母,这是一种数字缩写 (Numeronym) 方式。类似的如internationalization(国际化) 叫做i18naccessibility叫做a11y等。以下如无特别原因均使用它的简称 k8s。

接下来几篇文章将分享我学习和使用它的一些经验,今天是第一篇,先了解 k8s 的架构、核心概念和基本用法。

在 macOS 上安装 Kubernetes

首先需要安装 Kubernetes。有 3 个方法:

Docker Desktop for Mac

在 macOS 上最简单的方法就是安装「Docker Desktop for Mac」,并开启 Kubernetes 支持,可以通过延伸阅读链接 1 的地址下载它,开启 Kubernetes 支持的方法非常简单,需要 2 步:

  1. 使用代理。众所周知,安装过程会请求一些会超时的网站页面,需要设置代理。右键点 Docker 图标选: Preferences -> Proxies,选择手动指定,输入你的代理地址,然后点 Apply && Restart 重启 Docker
  2. 开启 Kubernetes 支持。重启后,右键点 Docker 图标选: Preferences -> Kubernetes Tab,给 Enable Kubernetes 等项打勾再点 Apply,再点弹出窗口的「Install」项等待完成完成即可

P.S 安装过程中如果有任何问题可以使用延伸阅读链接 2 的检查日志方法去做故障排错,根据日志输出再搜索解决方案。

通过 k8s-docker-desktop-for-mac

可以用我厂平台同学搞的 k8s-docker-desktop-for-mac 项目完成安装~

命令行安装

如果你没有好用的代理,也不想尝试👆这个项目,兜底方案是手动安装。第一步先安装 Kubernetes 的命令行客户端 kubectl,再安装一个可以在本地跑起来的 Kubernetes 环境 Minikube、以及给 Minikube 使用的虚拟化引擎 HyperKit:

❯ brew cask install minikube
❯ brew install docker-machine-driver-hyperkit
❯ sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit # 文件路径可以不同,注意终端输出
❯ sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit
❯ minikube start --vm-driver hyperkit --image-mirror-country cn # 使用国内镜像

Minikube 默认的虚拟化引擎是 VirtualBox,Hyperkit 是一个更快更轻量的替代。Minikube 自带了 Docker 引擎,所以我们需要重新配置客户端,让 docker 命令行与 Minikube 中的 Docker 进程通讯:

eval $(minikube docker-env)

现在 k8s 就安装完成啦。我们进入正题~

Kubernetes 架构

典型 Kubernetes 集群包含一个 Master 和多个 Node,简单架构如下图所示 (未来 3 张图来源于延伸阅读链接 3):

Master 节点

Master 是集群的控制节点,它负责整个集群的管理和控制 (调度)。这个节点上运行着多个组件,核心的如下:

  • API 服务器 (kube-apiserver)。API Server 对外暴露了 k8s API,提供 HTTP REST 服务。提供了认证、授权、访问控制、API 注册和发现等机制
  • etcd。是一个高可用的键值对存储系统,被用作 k8s 的后端存储,所有集群配置数据都存储在里面,用于服务发现和集群管理,可以说是存了整个集群的状态
  • 控制管理器 (kube-controller-manager)。Controller 负责维护集群的状态,比如故障检测、自动扩展、滚动更新等
  • 调度器 (kube-scheduler)。Scheduler 负责资源的调度,按照预定的调度策略将 Pod 调度到相应的机器上

看一下 Mater 节点的组件架构效果图:

Pod 是什么很快就说到了哈~

Node

Node 是集群的工作节点,它提供 CPU、内存和存储等资源。它上面运行哪些容器应用由 Master 节点分配。这个节点上也运行着多个组件,核心的如下:

  • 节点代理 (kubelet)。它和 Master 节点协作,实现 Pod 的创建、启动、监控、重启、销毁等集群管理工作
  • 转发代理 (kube-proxy)。维护主机上的网络规则并执行连接转发,实现服务的负载均衡和反向代理
  • Docker。负责用于运行容器 (或者说 Pod)
  • fluentd。fluentd 是一个守护进程,它有助于提供集群层面日志 集群层面的日志。

看一下 Node 节点的组件架构效果图:

这个图里面还标了一些插件,如 CoreDNS、Dashboard 以及没有替到的 Ingress Controller 等,之后还会再说,内容太多,我们先了解基础部分。

通过下面的命令可以看到集群节点情况:

❯ kubectl get nodes
NAME             STATUS   ROLES    AGE    VERSION
docker-desktop   Ready    master   2m    v1.14.6

当然,我们这个是单节点集群环境,只有一个 Master 节点,但肩负了 Node 节点的作用。

Pod

Pod 是 k8s 里面基本的部署调度单元,每个 Pod 可以由一个或多个容器组成,容器之间共享存储、网络等资源。Pod 这个词一眼看去是不是很懵?其实看一下它中文翻译「豆荚」的图就好理解了:

豌豆被豌豆荚「包」了起来,这就是一个 Pod。所以可以把单个 Pod 理解成是运行独立应用的「逻辑主机」—— 其中运行着一个或者多个紧密耦合的应用容器。

在一开始学习基础概念阶段我会用一些非常简单的例子帮助大家消化 k8s 的知识。首先创建一个单独的目录来存放示例所需的文件,实现一个用 Nginx 访问静态文件的服务:

❯ mkdir k8s-demo
❯ cd k8s-demo
❯ echo '<h1>Hello Docker!</h1>' > index.html

接着创建 Dockerfile 文件并构建镜像:

❯ cat Dockerfile
FROM nginx:1.17.4-alpine
COPY index.html /usr/share/nginx/html
❯ docker build -t k8s-demo:0.1 .
...  # 省略输出
Successfully built 1cdb5b879af0
Successfully tagged k8s-demo:0.1

这样就在本地构建了一个叫做k8s-demo、标签 (版本) 为 0.1 的镜像。接着把它提交给 k8s 并部署,不过要注意,在 k8s 里面最小的部署单元不是容器而是 Pod,所以需要「转化」成 Pod 对象,再交由 k8s 创建。怎么做呢?

# 1. 创建一个定义文件,比如这里叫做pod.yaml,当然用JSON格式也可以,不过我觉得YAML的表达能力和可读性更强
❯ cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: k8s-demo
  labels:
    app: k8s
spec:
  containers:
    - name: k8s-demo
      image: k8s-demo:0.1
      ports:
        - containerPort: 80
# 2. 使用「kubectl create -f YAML_FILE」创建Pod
❯ kubectl create -f pod.yaml
pod/k8s-demo created
❯ kubectl get pods  # 查看Pod状态
NAME       READY   STATUS    RESTARTS   AGE
k8s-demo   1/1     Running   0          21s
❯ kubectl describe pods | grep Labels  # 查看Pod标签
Labels:             app=k8s

到这里,这个叫做 k8s-demo 的 Pod 就创建成功了,而且已经是运行状态了。在继续之前,非常有必要先说一下 k8s 的对象以及对象管理

对象和对象管理

Pod 是一种 k8s 中对象 (Object) 中的一种,k8s 用各种对象来表示对应实体在集群中的状态。上面说的 Pod 就是一种对象,之后我们还会介绍非常多的实体,如 ReplicaSet、Deployment、Job、Service、CronJob、ConfigMap、Volume...

这些对象有 3 个显著的特点:

  • 被持久化的存储进 etcd
  • 通过对象配置的方式管理
  • 一旦创建对象,k8s 将持续工作以确保对象按照其配置期望的那样存在

仔细的回味下 pod.yaml 这个配置文件的键值内容:

  • apiVersion。API 版本号,这里的值是 v1,但是这个版本号需要根据我们安装的 k8s 版本和资源对象类型变化,不是固定的
  • kind。描述对象的类型,本例中它就是一个「Pod」,不同的对象类型肯定不一样 (如 Deployment、Service、Job、Ingress 等)
  • metadata。定义元数据,用于唯一识别对象的数据。常用的配置项如 name (名字)、namespace (命令空间)、labels (标签) 等
  • spec。规格声明,描述了某一个实体的期望状态,常用项如 containers、storage、volumes、template 等。在这里就是说用的容器名字为 k8s-demo,使用刚才构建的 k8s-demo:0.1 作为镜像,容器可被访问的端口是 80

前三个键 (apiVersion、kind 和 metadata) 是所有对象都有的,而所有表示物理资源的对象,其状态可能与用户期望的意图不同时才可能有 spec 键。

P.S 上述配置项相关理解基于延伸阅读链接 5 里面的「API 约定文档」而来,建议阅读。

经过这段时间学习 k8s,非常喜欢它的最核心的对象配置方法:「命令式对象配置」和「声明式对象配置」

命令式对象配置

前面的kubectl create -f pod.yaml就是命令式:kubectl命令指定操作 (创建,替换等),需要接受至少一个文件名称。指定的文件必须包含对象的完整定义 (以 YAML 或 JSON 格式):

kubectl create -f nginx.yaml  # 创建对象定义配置文件
kubectl delete -f nginx.yaml -f redis.yaml  # 删除2个配置文件中定义的对象
kubectl replace -f nginx.yaml  # 通过覆写实时配置更新配置文件中定义的对象

要不然是基于 YAML 文件执行添加 / 删除操作,否则就是直接修改 YAML 文件内容实现对 k8s 中对象的修改。

它和传统的、把全部配置都作为参数写在一个命令里面的方式相比,非常节省命令长度,一切相关的配置都在 YAML 文件中表达,另外这样写的好处还有:

  • 方便把对象配置存储在源码控制系统 (如 Git) 中,且方便对比修改
  • 表达能力极强,这样支持的配置项更丰富灵活,且能使用模板

声明式对象配置

通过声明式对象配置的用法,可以实现目录级别的对象管理,能自动检测实现对每个对象进行创建、更新和删除等操作:

kubectl apply -f configs/

延伸阅读链接 7 中对于这部分概念讲的非常好,强烈建议阅读

Service (服务)

前面已经了解了 Pod,但是要注意 Pod 是不能从外部直接访问的 (除非用 kubectl port-forward 等方案)。要把服务暴露出来给用户访问,需要创建一个服务 (Service)。Service 的作用主要是做反向代理 (Reverse Proxy) 和负载均衡 (LB),负责把请求分发给后面的 Pod。

首先要创建一个 Service 定义文件 svc.yaml:

❯ cat svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: k8s-demo-svc
spec:
  selector:
    app: k8s
  type: NodePort
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

这个配置文件描述了如下内容:

  • 资源类型 kind 是 Service
  • 服务名字 k8s-demo-svc
  • spec.selector 指定请求会被发送给集群里的哪些 Pod,一般是用标签选择,app:k8s 在上面的 pod.yaml 里面指定了,所以这个服务会把流量发送给 k8s-demo 这个 Pod
  • spec.type 指定服务暴露方式,这里是 NodePort,Master 会从由启动参数配置的范围 (默认是 30000-32767) 中分配一个端口,然后每一个 Node 都会将这个端口代理到你的 Service。。其他还有 LoadBalancer、Ingress 等。
  • spec.ports 的设置表示访问节点的 9376 端口,会被转发到 Pod 的 80 端口

创建并查看服务:

❯ kubectl create -f svc.yaml  # 创建服务
service/k8s-demo-svc created
❯ kubectl get svc k8s-demo-svc
NAME           TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
k8s-demo-svc   NodePort   10.100.68.48   <none>        80:31404/TCP   6s

PORT 项下面有个80:31404,就是说访问本机 (127.0.0.1) 的 31404 端口 (端口号是 k8s 分配的,你在使用时很可能不是这个端口号) 会被转发到这个服务的 80 端口,所以现在在浏览器输入http://127.0.0.1:31404/就可以看到「Hello Docker!」了。

ReplicaSet (副本)

假如现在有一个 Pod 正在提供线上的服务,设想出现如下问题时应该怎么办:

  • 某次广告投放或者运营活动引来大量用户,网站访问量突然暴增
  • 运行当前 Pod 的节点发生故障了,Pod 不能正常提供服务了

针对第一点,可以提前和运维打好招呼,按照预估提前多启动 N 个 Pod,结束后删掉,缺点是不能自动扩展造成资源浪费,给平台带来开发和运维成本;针对第二点,提前充分准备并演练好切换预案,运维 24 小时待命并及时关注上线服务报警,出现问题时快速切换节点。

这样的解决方案其实不算解决方案,因为的弊端很明显:纯靠人工!!!所以我们需要 k8s 这样的工具帮助我们实现 Pod 的自动扩展和「故障转移」。

在 k8s 资源对象中的 ReplicaSet (以下简称 RS,其实还有另外一种资源对象 Replication Controller,简称 RC,它已经被 ReplicaSet 取代) 就是做这件事的,它保证在任意时间运行 Pod 的副本数量,能够保证 Pod 总是可用的。也就是说如果实际的 Pod 数量比指定的多就结束掉多余的,反之亦然。如果 Pod 失败、被删除或者挂掉后 ReplicaSet 会重新创建 Pod,所以即使只有一个 Pod,也应该使用 RS 来管理我们的 Pod。

另外 RS 还能实现非常有用的「滚动升级」,实现了零停机的前提下部署新版本,马上会详细说到。

Deployment

实际上很少直接用 ReplicaSet 这个对象,一般用 Deployment 这个更加高层的资源对象代替。为什么呢?

RS 主要功能是确保 Pod 数量、健康度和滚动升级等,但 Deployment 除具备 RS 全部功能之外还有如下功能:

  • 事件和状态查看。可以查看 Deployment 的升级详细进度和状态
  • 回滚。当升级 Pod 的时候如果出现问题,可以使用回滚操作回滚到之前的任一版本
  • 版本记录。每一次对 Deployment 的操作,都能够保存下来,这也是保证可以回滚到任一版本的基础
  • 暂停和启动。对于每一次升级都能够随时暂停和启动

可以这样理解 Deployment、ReplicaSet 和 Pod 的关系:

一个 Deployment 拥有多个 Replica Set,而一个 Replica Set 拥有一个或多个 Pod

通过一个小例子完整的感受一下,先创建 deployment.yaml:

❯ cat deployment.yaml
apiVersion: apps/v1beta1 # for versions before 1.6.0 use extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-demo-deployment
spec:
  replicas: 10
  template:
    metadata:
      labels:
        app: k8s
    spec:
      containers:
      - name: k8s-demo-pod
        image: k8s-demo:0.1
        ports:
        - containerPort: 80
❯ kubectl create -f deployment.yaml
deployment.apps/k8s-demo-deployment created

这次 YAML 文件和前面的 Pod 的有个和不同的地方:

  • apiVersion 不同,且 k8s 版本不同值也不同
  • kind 是 Deployment
  • spec.replicas 指定了这个 deployment 要有 10 个 Pod

注意:创建 Deployment 的命令还是一样的kubectl create -f XXX.yaml,一切都在配置文件中指定。

看一下现在的副本集情况 (有 10 个 Pod 在运行):

❯ kubectl get rs  # READY=10表示都可用
NAME                             DESIRED   CURRENT   READY   AGE
k8s-demo-deployment-7f6d84f56b   10        10        10      12s

❯ kubectl get deployments  # 其实还可以直接看Deployment,AVAILABLE=10
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
k8s-demo-deployment   10/10   10           10          20s

❯ kubectl get pods |grep k8s-demo-deployment | wc -l  # 可以看到Deployment维护了10个Pod
10

接着修改一下 k8s-demo 镜像:

echo '<h1>Hello Kubernetes!</h1>' > index.html

❯ docker build -t k8s-demo:0.2 .
...
Successfully built 068ab5dbcf44
Successfully tagged k8s-demo:0.2

接着要更新 deployment.yaml:

❯ cat deployment.yaml
apiVersion: apps/v1beta1 # for versions before 1.6.0 use extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-demo-deployment
spec:
  replicas: 10
  minReadySeconds: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels:
        app: k8s-demo
    spec:
      containers:
      - name: k8s-demo-pod
        image: k8s-demo:0.2
        ports:
        - containerPort: 80

和之前的 deployment.yaml 相比,改动如下:

  • 增加了 minReadySeconds,指在更新了一个 Pod 后,需要在它进入正常状态后 3 秒再更新下一个 Pod
  • 增加 strategy,升级策略是滚动升级 (RollingUpdate),maxUnavailable: 1 指同时处于不可用状态的 Pod 不能超过一个,maxSurge: 1 指多余的 Pod 不能超过一个
  • 修改了 spec.containers.image (的标签),因为要升级镜像了

这样 k8s 就会逐个替换 Service 后面的 Pod 实现更新:

❯ kubectl apply -f deployment.yaml --record

这次多了一个--record参数,这会让 k8s 把这行命令记到发布历史中备查。执行完上述命令可以马上执行如下命令显示发布的实时状态 (晚了就看不到了):

❯ kubectl rollout status deployment k8s-demo-deployment
...  # 省略
Waiting for deployment "k8s-demo-deployment" rollout to finish: 8 out of 10 new replicas have been updated...
Waiting for rollout to finish: 1 old replicas are pending termination...
...  # 省略
deployment "k8s-demo-deployment" successfully rolled out

更新过程会先添加新的副本集,再删除旧的,所以过程中如果看kubectl get pods |grep k8s-demo-deployment | wc -l值会大于 10。

还可以查看发布历史 (正是由于 --record):

❯ kubectl rollout history deployment k8s-demo-deployment
deployment.extensions/k8s-demo-deployment
REVISION  CHANGE-CAUSE
1         <none>
2         kubectl apply --filename=deployment.yaml --record=true

现在你刷新页面 (http://127.0.0.1:31404/)时可以看到文本已经成了「Hello Kubernetes!」

假设新版发布后发现有严重的 bug,需要马上回滚到上个版本,可以用下面命令完成:

❯ kubectl rollout undo deployment k8s-demo-deployment --to-revision=1
deployment.extensions/k8s-demo-deployment rolled back

其中--to-revision参数指定要回滚到那个位置,版本号 1 可以在上面kubectl rollout history的输出列表中找到。在回滚结束之后,刷新浏览器可以看到内容又改回了「Hello Docker!」,这种滚动升级是不是非常好用?

Deployment 如其名,更适合生产环境的部署。

P.S. 可以感受到前面用到的 Service、Deployment 和 Pod 的关系是解耦的: Service 用于暴露给外部访问、Deployment 用户部署方式、Pod 包装容器,让它们关联起来的唯一纽带是配置文件中的app: k8s,对 Deployment 的修改会影响到 Service 和 Pod。

后记

k8s 中要说的内容实在太多了,所以我会分成 2 篇来说。

项目源码

本文提到的全部源码可以在 mp 找到。

延伸阅读

  1. https://www.docker.com/products/docker-desktop
  2. https://docs.docker.com/docker-for-mac/troubleshoot/#check-the-logs
  3. https://thenewstack.io/kubernetes-an-overview/
  4. https://1byte.io/developer-guide-to-docker-and-kubernetes/
  5. https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
  6. https://www.qikqiak.com/k8s-book/docs/14.Kubernetes%E5%88%9D%E4%BD%93%E9%AA%8C.html
  7. http://www.k8smeetup.com/article/VyaHa$XRm