Kubernetes 的使用

前面我们已经学习了 Docker 镜像的搭建过程,另外还学习了 Docker Compose 工具的用法。我们可以使用 Docker Compose 非常方便地启动 Docker 容器运行爬虫,然而这个过程距离真正的大规模运维还是不够。

还是前几节的儿个同题:

  • 如何快速部署几十、上百、上千个爬虫程序并协同爬取?口如何实现爬虫的批量更新?

  • 如何实时查看爬虫的运行状态和日志?

其实利用 Kubernetes,我们同样可以非常方便地解决上文提到的各个问题。

本节中,我们会首先了解 Kubernetes 的基本概念和原理、核心的组件和 Kubernetes 的基本使用方式,以便为后文实现 Kubernetes 部署和管理 Scrapy 爬虫程序打下基础。

准备工作

在本节开始之前,请确保已经对 Docker 等容器技术有了一定的了解,如果你没有相关的经验请先学习一下 Docker 和容器技术的相关知识。

另外,还需要在本机安装 Docker 并在本地启用 Kubernetes 服务,具体的操作可以参考 https://setup.scrape.center/kubernetes

配置好 Kubermetes 之后,我们就可以使用 kubectl 命令来操作一个 Kubernetes 集群了。

Kubernetes 简介

Kubernetes,简称 K8s(K 和 s 中间含有 8 个字母),它是用于编排容器化应用程序的云原生系统。Kubernetes 诞生自 Google,现在已经由 CNCF(云原生计算基金会)维护更新。Kubernetes 是目前最受欢迎的集群管理方案之一,可以非常容易地实现容器的管理和编排。

刚刚我们提到,Kubernetes 是一个容器编排系统。对于“编排”二字,我们可能不太容易理解其中的含义。为了对它有更好的理解,我们先回过头来看看容器的定义以及容器解决了什么问题,不能解决什么问题,然后了解下 Kubernetes 能够弥补容器哪些缺失的内容。

好,首先来看看容器。最常见的容器技术就是 Docker 了,它提供了比传统虚拟化技术更轻量级的机制来创建隔离的应用程序的运行环境。比如对于某个应用程序,我们使用容器运行时,不必担心它与宿主机之间产生资源冲突,不必担心多个容器之间产生资源冲突。同时借助于容器技术,我们还能更好地保证开发环境和生产环境的运行一致性。另外,由于每个容器都是独立的,因此可以将多个容器运行在同一台宿主机上,以提高宿主机的资源利用率,从而进一步降低成本。总之,容器带来的好处有很多,可以为我们带来极大的便利。

不过单依靠容器技术并不能解决所有问题,也可以说容器技术也引入了新的问题,比如说:

  • 如果容器突然运行异常了,怎么办?

  • 如果容器所在的宿主机突然运行异常了,怎么办?

  • 如果有多个容器,它们之间怎么有效地传输数据?

  • 如果单个容器达到了瓶颈,如何平稳有效地进行扩容?

  • 如果生产环境是由多台主机组成的,我们怎样更好地决定使用哪台主机来运行哪个容器?

以上列举了一些单依靠容器技术或者单纯依靠 Docker 不能解决的问题,而 Kubernetes 作为容器编排平台,提供了一个可弹性运行的分布式系统框架,各个容器可以运行在 Kubernetes 平台上,容器的管理、调度、部署、扩容等等各个操作都可以经由 Kubernetes 来有效实现。比如说,Kubernetes 可以管理单个容器的生命周期,并且可以根据需要来扩展和释放资源。如果某个容器意外关闭,Kubernetes 可以根据对应的策略重启该容器,以保证服务正常运行。再比如说,Kubernetes 是一个分布式平台,当容器所在的主机突然发生异常,Kubernetes可以将异常主机上运行的容器转移到其他正常的主机上运行。另外,Kubernetes还可以根据容器运行所需要占用的资源自动选择合适的主机来运行。总之,Kubernetes对容器的调度和管理提供了非常强大的支持,可以帮我们解决上述的诸多问题。

Kubernetes 关键概念

下面我们来介绍 Kubernetes 中的关键概念,包括 Node、Namespace、Pod、Deployment、Service、Ingress 等,了解了这些,有助于我们更加得心应手地实现 Kubermetes 的管理和操作。

Node

Node,即节点,在Kubernetes中,节点就意味着容器运行的宿主机。因为Kubernetes是一个集群所以我们可以把节点看作组成集群的一台台主机。

既然是集群,那么多个Node相互协作一定是一个需要解决的问题,到底应该听谁的呢?所以 Node又分了MasterNode和WorkerNode,其中MasterNode可以认为是集群的管理节点,负责管理整个集群,并提供集群的数据访问入口,在它之上运行着一些核心组件,如APIServer负责接收API指令,ControllerManager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等。

Namespace

Namespace,即命名空间,对一组资源和对象的抽象集合。可以认为Namespace是Kubernetes集群中的虚拟化集群。在一个Kubernetes集群中,可以拥有多个命名空间,它们在逻辑上彼此隔离。

Pod

Pod,它运行在Node上,是Kubernetes的最小调度单位,也是Kubernetes针对容器编排作出的设计方案。

这时候大家可能有个疑问,为什么最小的调度单位不是容器,而是又另外设计了一个Pod的概念呢?因为容器单独运行,这确实是没有问题的,但有时候几个容器是需要协同运行的,它们需要共享同样的资源、同样的网络,比如说这里运行了一个MySQL容器,但这个容器在启动时需要进行一些初始化的配置,比较好的设计就是单独有一个Sidecar容器为这个MySOL主容器进行初始化操作,所以这个MySQL容器就需要配有一个Sidecar容器,它们还需要共享相同的网络和资源。所以在容器的基础上,Kubernetes进一步抽象了一层,叫作Pod。Pod里面是可以运行多个容器的,同一个Pod里面的容器可以共享资源、网络、存储系统。

通常情况下,我们不会单独显式地创建Pod对象,而是会借助于Deployment等对象来创建。

Deployment

Deployment,即部署,利用它我们可以定义Pod的配置,如副本、镜像、运行所需要的资源等。

Deployment在Pod和ReplicaSet之上,提供了一个声明式定义方法,比如说我们声明一个 Deployment并指定Pod副本数量为2,应用该Deployment之后,Kubernetes便会为我们创建两个Pod。因此,我们只需要在Deployment中描述想要的目标状态是什么。Kubernetes有一个Deployment Controller,它会帮我们将Pod和ReplicaSet的实际状态改变到我们想要的目标状态。

Service

设想这么一个场景,假如一个服务,我们在部署的时候声明了副本数量为2,即创建两个Pod,每个Pod都有自已在Kubernetes中的IP地址并在对应的端口上启动了服务,但这一组Pod的服务怎么统一暴露给Kubernetes之外来访问呢?这就引入了Service的概念。

Service是将运行在一组Pod上的应用程序公开为网络服务的抽象机制。Service相当于一个负载均衡器,通过一些定义可以找到关联的一组Pod,当请求到来时,它可以将流量转发到对应的任一Pod 上进行处理。所以对外来说,客户端不需要关心怎么调用具体哪个Pod的服务,Service相当于在Pod 之上的一个负载均衡器,对应的请求会由Service转发给Pod。

Ingress

Ingress用于对外暴露服务,该资源对象定义了不同主机名(域名)及URL和对应后端Service的绑定,根据不同的路径路由HTTP和HTTPS流量。比如通过IngresS,我们可以配置哪个域名对应的流量转发到哪个Service上,还可以配置一些HTTPS证书相关的内容。

以上我们就简单介绍了Kubernetes里面的部分基本组件,这些全新的概念其实还是比较难理解的,接下来我们就通过一个实战例子来加深理解。另外,也强烈推荐查看Kubermetes的官方文档了解更加详细的内容:https:/kubernetes.io/docs/concepts/。

Kubernetes 案例上手

下面我们来用一个实际案例了解Kubernetes的部署过程。这里我们会介绍如何创建Namespace 如何使用YAML来定义Deployment和Service,以及怎样访问Service,通过这些操作,我们可以先对 Kubernetes的操作有个简单的认识。

接下来的演示是基于Docker自带的Kubernetes集群实现的,安装好Docker之后,我们在Docker 的设置面板中只需要勾选EnableKubernetes即可在本地开启一个Kubernetes服务,如图17-12所示。

图17-12Docker的设置面板

配置完成之后,可以发现左下角的Kubernetes是绿色的running状态,这就说明配置成功了。

kubectl是用来操作Kubernetes的命令行工具,可以参考https://kubernetes.io/zh/docs/tasks/tools/ install-kubect/来安装。安装完成之后,请将KubernetesContext切换为本地Kubernetes。

比如,我这边DockerDesktop创建的Kubernetes的Context名称叫作docker-desktop。如果你也使用同样的方法创建集群,名字默认也是一样的,此时可以运行如下命令来使用该 Context:

kubectl config use-context docker-desktop

运行之后,会有如下提示:

Switched to context "docker-desktop".

如果你使用的是其他 Kubernetes 集群,可以自行更改 Context 名称。

这时候我们可以使用 kubectl 命令来查看当前本地 Kubernetes 集群的运行状态。首先看下节点的信息,命令如下:

kubectl get nodes

类似的输出如下:

NAME STATUS ROLES AGE VERSION
docker-desktop Ready master 1d v1.16.6-beta.0

这里列出来了节点的相关信息:NAME 代表名称;STATUS 代表当前节点的状态,如果其值是 Ready 的话,代表节点状态正常;ROLES 代表角色,这里因为只有一个节点,所以它的 ROLES 就是 master;AGE 代表节点自创建以来到现在的时间;VERSION 代表当前 Kubernetes 的版本号。

接着,我们再来看看 Namespace,命令如下:

kubectl get namespaces

输出类似如下:

NAME STATUS AGE
default Active 1d
kube-node-lease Active 1d
kube-public Active 1d
kube-system Active 1d

这里列出了当前所有的 Namespace,它们都是 Kubernetes 预置的 Namespace。我们可以自行创建一个 Namespace,将资源部署到新的 Namespace 下,比如创建一个叫做 service 的 Namespace,命令如下:

kubectl create namespace service

运行结果如下:

namespace/service created

如果看到如上提示,就说明 Kubernetes 已经创建好了。

这时候我们来创建一个示例 Docker 镜像。首先,新建一个文件夹并将其当作工作目录,在该工作目录下创建一个 app 文件夹,其内创建一个 main.py 文件,目录结构如下:

└── app
    └── main.py

main.py 文件的内容如下:

from fastapi import FastAPI

app = FastAPI()

@app.get('/')
def index():
    return 'Hello World'

这是 FastAPI 编写的一个服务,通过代码可以看出,这里定义了一个路由,访问根路径就可以返回 Hello World。

接着,我们在 app 同级目录下创建一个 Dockerfile 文件,目录树结构如下:

├── Dockerfile
└── app
    └── main.py

Dockerfile 的内容如下:

FROM python:3.7
RUN pip install fastapi uvicorn
EXPOSE 80
COPY ./app /app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]

接下来,我们可以在 Dockerfile 所在文件夹下运行命令构建一个 Docker 镜像,如:

docker build -t testserver .

这样一个镜像就构建好了。我们来运行一下试试看:

docker run -p 8888:80 testserver

这里我们运行了当前的镜像,启动了一个容器,容器本身是在 80 端口上运行的。由于我们设置了端口映射,将宿主机的 8888 端口转发到容器的 80 端口,因此我们在浏览器中打开 http://localhost:8888/,就可以看到如下图 17-13 所示的结果。

图 17-13 运行结果

这说明本镜像史没有任何问题的。

接下来,我把镜像推送到 Docker Hub。先修改镜像名称,然后推送即可:

docker tag testserver germey/testserver
docker push germey/testserver

这里请自行修改 Docker Hub 的用户名,将 germey 替换为你自己的用户名。

如果出现类似下面的结果,就说明推送成功了:

The push refers to repository [docker.io/germey/testserver]
0c80be9761b3: Layer already exists
a4b0f6a9292c: Layer already exists
...
aif2f42922b1: Layer already exists
4762552ad7d8: Layer already exists
latest: digest: sha256:b92c66daf4627eb069dc3343e9a9c3d24d6b122db47e847c4deb52dfa5b2f2e0 size: 2636

当然,你也可以选择不推送自己的镜像,直接使用我的镜像 germey/testserver 也是没问题的。

好,接下来让我们创建一个 YAML 文件,叫作 deployment.yaml,其内容如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: testserver
  namespace: service
  labels:
    app: testserver
spec:
    replicas: 3
    selector:
      matchLabels:
        app: testserver
    template:
      metadata:
        labels:
          app: testserver
      spec:
        containers:
        - name: testserver
          image: germey/testserver
          ports:
          - containerPort: 80

这里我们定义了一个 Deployment 对象,一些配置项如下。

  • kind: 其值就是 Deployment,代表我们声明的是 Deployment 对象。

  • metadata: 定义了 Deployment 的基本信息。

    • name: Deployment 的名称,我们可以任取,这里也取名为 testserver

    • namespace: 命名空间,这里就使用刚才我们所创建的 service 这个命名空间。

    • labels: 声明了一些标签,是一些键值对的形式,可以任意取值,它旨在用于指定用户有意义且相关的对象的标识属性。

  • spec: 声明该 Deployment 对象对应的 Pod 的基本信息。

    • replicas: 这里指定为 3,这就声明了需要创建三个 Pod,即创建三个 Pod 副本。

    • selector: 声明了该 Deployment 如何查找要管理的 Pod,这里通过 matchLabels 指定了一个键值对,这样符合该键值对的 Pod 就归属该 Deployment 管理。另外,还有一些更复杂的匹配,如使用 matchExpressions 匹配某个表达式规则。

    • template: 声明了 Pod 里面运行的容器的配置,其中 metadata 里面声明了 Pod 的 labels,这和上述 selectormatchLabels 匹配即可。containers 字段指定运行容器的配置,其中包括容器名称、使用的镜像、容器运行端口等。

通过如上配置,我们就完成了 Deployment 的声明。现在我们来执行一下部署,此时可以运行如下命令:

kubectl apply -f deployment.yaml

这里 apply 命令就会代表 kubectl 应用该选项,-f 代表文件选项,后面要跟一个文件路径,即 deployment.yaml

运行结果类似如下:

deployment.apps/testserver created

如果出现这样的提示,就说明该部署已经生效了。

接着我们可以用如下命令来看看 Pod 的运行状态:

kubectl get pod -n service

这里注意我们需要使用 -n 指定 Namespace,运行结果类似如下:

NAME                          READY   STATUS    RESTARTS   AGE
testserver-685978f9f9-lj4v6   1/1     Running   0          1m
testserver-685978f9f9-q6v5k   1/1     Running   0          1m
testserver-685978f9f9-tspzz   1/1     Running   0          1m

可以看到,这里创建了 3 个 Pod(就是刚才 replicas 参数所指定的 3),而且都是 Running 状态。

好,现在在 Pod 已经创建好了,接下来我们需要将服务通过 Service 暴露出来。

接下来,我们声明一个 Service 对象,再创建一个 service.yaml 文件,内容如下:

apiVersion: v1
kind: Service
metadata:
  name: testserver
  namespace: service
spec:
  type: NodePort
  selector:
    app: testserver
  ports:
  - protocol: TCP
    port: 8888
    targetPort: 80

这里我们定义了一个 Service 对象,部分配置项如下。

  • kind: 其值就是 Service,代表我们声明的是 Service 对象。

  • metadata: 定义了 Service 的基本信息。

  • name: Service 的名称,我们可以任取,这里也取名为 testserver。它和其他对象重名,这是不冲突的,只要在一个命名空间下没有其他相同名称的 Service 即可。

  • namespace: 表示命名空间,这里就使用刚才我们所创建的 service 这个命名空间。

  • spec: 声明该 Service 对象对应的 Pod 的基本信息。

  • selector: 声明该 Service 如何查找要关联的 Pod,这里通过 selector 指定了一个键值对,这样所有带有 apptestserver 标签的 Pod 都会被关联到这个 Service 上。

  • ports: 声明该 Service 和 Pod 的通信协议,这里指定为 TCP。同时,port 指明了 Service 的运行端口,这里声明为 8888,但是 targetPort 指的是 Pod 容器的运行端口。由于在 Deployment 中容器是运行在 80 端口的,所以 targetPort 指定为 80。

现在我们再部署这个 Service 对象,其命令如下:

kubectl apply -f service.yaml

运行结果如下:

service/testserver created

这就表明 Service 已经创建成功了。

接下来,我们其实是仍然不能访问这个 Service 的。要访问的话,可以通过端口转发的方式将服务端口映射到宿主机,或者修改 Service 相关的配置,把 Service 的类型修改为 NodePort 或者将 Service 进一步通过 Ingress 暴露出来。这里我们直接采取端口转发的方式将 Kubernetes 中的 Service 转发到本机的某个端口上,命令如下:

kubectl port-forward service/testserver 9999:8888 -n service

这里我们将宿主机的 9999 端口转发到 Service 的 8888 端口,这样我们在本地访问 9999 端口就相当于访问 Kubernetes 的 Service 的 8888 端口了。

运行结果类似如下:

Forwarding from 127.0.0.1:9999 -> 80
Forwarding from [::1]:9999 -> 80

这里输出的其实是 9999 映射到 80,因为这里显示的是容器的端口,容器的运行端口是 80。

这时候在浏览器中打开 http://localhost:9999/ ,即可看到刚才部署的服务,如图17-14所示。

图17-14 运行结果

总结

到此,我们通过声明 Deployment 和 Service 实现了一个服务的部署,同时体会了 Kubernetes 的部署流程。

在后面,我们会介绍利用 Kubernetes 进行 Scrapy 分布式爬虫部署的方案。