Kubernetes in Action—开始使用 k8s 和 Docker

从创建一个简单应用,到打包镜像并在远端 Kubernetes 集群运行,对 Kubernetes 体系进一步加深理解。

创建、运行及共享容器镜像

安装 Docker 并运行 Hello World 容器

根据操作系统不同,按照 https://docs.docker.com/install/ 安装 Docker。

运行 Hello World 容器

busybox 是一个单一可执行文件,包含多种标准 UNIX 命令行工具,如:echo、ls、gzip 等。

  • 拉取 busybox 镜像

    docker pull busybox
    
  • 使用 docker run 命令并指定运行的镜像

    docker run busybox echo "Hello World"
    

背后的原理

下图展示了上述运行 Hello World 容器时发生的事情,首先,Docker 从 http://docker.io 的 Docker 镜像中心拉取镜像,镜像下载到本机之后, Docker 基于该镜像创建一个容器并在容器中运行命令。echo 打印文字到标准输出,之后进程终止,容器停止运行。

image-20191215214303725

运行其他镜像

http://hub.docker.com 或其他公开镜像中心下载可用镜像后,一般都可以像运行 busybox 镜像一样在 Docker 中运行镜像:

docker run <image>

容器镜像的版本管理

Docker 支持同一镜像的多个版本,以支持软件包的更新,每个版本都必须有唯一的 tag 名,当 docker run 引用的镜像没有显示指定 tag 时,则 Docker 默认指定 tag 为 latest,通过以下命令指定特定版本的镜像:

docker run <image>:<tag>

创建一个简单的 Nodejs 应用

构建一个简单的 Nodejs Web 应用,并将其打包到容器镜像中,该应用接受 Http 请求并相应应用运行的主机名(应用看到的主机名是容器内自身的主机名而非宿主机的主机名),对后续应用部署在 Kubernetes 上进行伸缩时(水平伸缩,复制多个节点),会发现 Http 请求切换到了不同的实例上,以下为该 Nodejs Web 应用的代码:

const http = require( 'http' );
const os = require( 'os' );

console.log ( "Kubia server starting..." ) ;

const handler = function (request, response) {
  console.log ( "Received request from " + request.connection.remoteAddress);
  response.writeHead(200);
  response.end( "You've hit " + os.hostname() + "\n" );
}

const www = http.createServer(handler);
www.listen(8080);

浏览器访问 http://localhost:8080 ,服务器以状态码和“You‘ve hit ”响应请求。

##为镜像创建 Dockerfile

为了把应用打包为镜像,首先需要创建 Dockerfile 文件,其包含了一系列打包镜像时会执行的指令。

Dockerfile 和 app.js 在同一目录:

FROM node:12
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]

##构建容器镜像

运行下面 Docker 命令构建镜像:

docker build -t kubia .

下图展示了基于 Dockerfile 构建新的容器镜像的过程:

image-20191216123551792

镜像是如何构建的

构建过程是将整个目录上传到 Docker 守护进程并在那里运行的。由于 Docker 客户端和 Docker 守护进程不要求在同一台机器上,因此构建目录要避免包含任何不需要的文件,这会减慢构建的速度。

构建过程中, Docker 首次会从 Docker hub 中拉取镜像(node:12),之后使用 node:12 的镜像则使用存储在本机上的 node:12 镜像作为基础镜像。

镜像分层

镜像是由多层组成的,不同镜像可能会共享分层,这让存储和传输更加高效,当创建多个基于相同基础镜像的镜像时,所有组成基础镜像的分层只会被存储一次,Docker 只会下载未被存储的层。

当构建镜像时, Dockerfile 中每一条单独指令都会创建新层,如上述拉取 node:12 镜像后, Docker 会在其上创建新层并添加 app.js,之后创建一层来指定镜像被运行时所执行的命令,最后一层被标记为 kubia:latest

image-20191216124458909

查看本地存储的镜像

docker images
# docker image ls

比较使用 Dockerfile 和手动构建镜像

Dockerfile 构建镜像是使用 Docker 最常用的方式。

也可以通过运行已有镜像容器手动构建镜像,首先进入容器,运行命令,退出容器,再将最终状态作为新镜像。但 Dockerfile 构建镜像是自动化且可重复的,随时可以通过修改 Dockerfile 重新构建镜像,因此日常还是推荐使用 Dockerfile 构建镜像。

运行容器镜像

通过以下命令运行镜像:

docker run --name kubia-container -p 8080:8080 -d kubia

该命令告知 Docker 基于 kubia 镜像创建名为 kubia-container 的新容器,-d 表示后台运行,-p 映射本机的 8080 到容器内的 8080 端口,因此同样可以通过 http://localhost:8080 访问该应用。

列出所有运行中的容器:

docker ps -a

获取更多容器信息:

docker inspect kubia-container

探索运行容器内部

在已有容器内部运行 Shell

node:12 镜像包含了 bash shell,因此可以运行下面命令在容器内执行命令:

docker exec -it kubia-container bash

Bash 进程会和主容器进程拥有相同的命名空间,得益于此可以从内部探索容器,-it 选项的意思是:

  • -i,确保标准输入流保持开放,用于在 shell 中输入命令
  • -t,分配伪终端(TTY)

从内部探索容器

root@df8caf4d598e:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  1.4 562364 29152 ?        Ssl  04:52   0:00 node app.js
root        25  2.0  0.1  18184  3176 pts/0    Ss   05:01   0:00 bash
root        31  0.0  0.1  36632  2908 pts/0    R+   05:01   0:00 ps aux

从容器内部查看到的 3 个进程,不能查看到宿主机上的其他进程。

容器内的进程运行在主机操作系统上

在 Linux 上,打开另一终端运行 ps aux|grep app.js, 可以发现运行在容器中的进程是运行在宿主机操作系统上的,但其进程 ID 在容器和主机上是不同的,因此也说明了容器使用独立的 PID Linux 命名空间且有着独立的系列号,完全独立于进程树。

注意:如果使用 Mac 系统,需要登录到 Docker 守护进程运行的 VM 查看守护进程。

容器的文件系统也是独立的

每个容器也拥有独立的文件系统,在容器内列出根目录内容,只会展示容器内文件,包括镜像内的所有文件及容器创建时的任何文件:

root@df8caf4d598e:/# ls /
app.js	bin  boot  dev	etc  home  lib	lib64  media  mnt  opt	proc  root  run  sbin  srv  sys  tmp  usr  var

停止和删除容器

通过以下命令停止 kubia-container 容器:

docker stop kubia-container

删除容器:

docker rm kubia-container
# 删除所有容器
docker rm -f $(docker ps -aq)

这将彻底删除容器,包含所有容器中的文件且无法再次启动。

向镜像仓库推送镜像

为了能在任何机器上使用镜像,可以把镜像推送到外部镜像仓库(如公开的 Docker hub 或者私有镜像仓库)。

使用附加标签标注镜像:

docker tag kubia april5/kubia
docker images | head
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
april5/kubia        latest              b3291932ea07        40 minutes ago      908MB
kubia               latest              b3291932ea07        40 minutes ago      908MB

april5/kubia 和 kubia 指向同一个镜像 ID, 因此 docker tag 只是给相同镜像创建一个额外标签。

向 Docker Hub 推送镜像

首先运行 docker login 登录 Docker hub,在向 docker hub 推送 yourid/kubia 镜像:

docker push april5/kubia

推送完成后,在其他机子上运行以下命令即可启动和之前一样的 Http Server:

docker run -p 8080:8080 -d april5/kubia

这带来的好处是应用即使运行在不同的机器上,但却运行在完全一致的环境中,只要应用在 Linux 机器上能正常运行,那么它也会在所有的 Linux 机器上正常运行,而无须担心其他机子是否安装了 nodejs。

配置 Kubernetes 集群

安装 Kubernetes 集群的方法在 http://kubernetes.io 的文档中有详细描述。

用 Minikube 运行一个本地单节点 Kubernetes 集群

使用 Minikube 是运行 Kubernetes 集群最简单、最便捷的途径,其是一个构建单节点集群的工具,对于测试 Kubernetes 和本地开发应用十分有用。

安装 Minikube

访问 Minikube 代码仓库(https://github.com/kubernetes/minikube)按照说明进行安装。

如 Macos 和 Linux 系统下可以使用以下命令进行安装,其区别只在于架构是 darwin 还是 linux:

curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-$(darwin/linux)-amd64
chmod +x minikube
sudo mv minikube /usr/local/bin

使用 Minikube 启动一个 Kubernetes 集群

通过以下命令启动 Kubernetes 集群,如果配置代理,可以通过添加多一张网卡连接网络并作为固定的代理 ip 地址,注意这里代理不能使用回环 ip:

minikube start
# 使用 proxy, 假设 proxy 地址为 http://192.168.3.199:8001
https_proxy=http://192.168.3.199:8001 minikube start --docker-env HTTP_PROXY=http://192.168.3.199:8001 --docker-env HTTPS_PROXY=http://192.168.3.199:8001 PROXY=192.168.99.0/24

集群启动需要花费一定时间,耐心等待。

通过运行 shell minikube ssh 登录到 Minikube VM 内部探索。

安装 Kubernetes 客户端(kubectl)

与 Kubernetes 集群交互的客户端工具为 kubectl CLI 客户端,在 Macos 或者 Linux 系统可以使用如下命令进行安装,其区别只在于架构是 darwin 还是 linux:

curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/$(darwin/linux)/amd64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/

为 kubectl 配置别名和命令行补齐

创建别名:

alias k=kubectl

为 kubectl 配置 tab 补全:

# bash 
echo "source <(kubectl completion bash)" >> ~/.bashrc

# zsh
echo "source <(kubectl completion zsh)" >> ~/.zshrc

在 Kubernetes 上运行第一个应用

通常在 Kubernetes 上运行应用程序需要准备一个 JSON 或者 YAML,包含想要部署所有组件的描述配置文件,下面先从简单使用单行命令运行应用。

部署 Nodejs 应用

使用 kubectl run 命令部署应用,该命令可以创建所有必要的组件而无需 JSON 或 YAML 文件,而不需要深入了解每个组件对象的结构:

kubectl run kubia --image=april5/kubia --port=8080 --generator=run-pod/v1

—image=april5/kubia 指定要运行的容器镜像,—port=8080 告诉 Kubernetes 应用正在监听 8080 端口,—generator 让 Kubernetes 创建一个 ReplicationController,通常不会使用它。

介绍 pod

Kubernetes 以 pod 为单位,一个 pod 是一组紧密相关的容器,他们一起运行在同一个工作节点上以及同一个 Linux 命名空间中,每个 pod 拥有自己的 IP、主机名、进程等,运行一个独立的应用程序该应用程序可以是单进程运行在单个容器中,也可以是一个主应用进程或者其他支持进程,每个进程都在自身容器中运行。下图展示了 pod、容器与工作节点之间的关系:

image-20191217231123681

列出 pod

kubectl get pods

查看 pod 的更多信息:

kubectl describe pod

幕后发生的事情

下图展示了 Kubernetes 运行 kubia 容器镜像时发生的事情:

image-20191217231631932

  • 构建镜像并推送到镜像仓库
  • kubectl 命令向 Kubernetes API 服务发送 REST HTTP 请求在集群创建一个 ReplicationController 对象
  • ReplicationController 创建一个新 pod,调度器将其调度到一个工作节点上(pod 将立即运行)
  • Kubelet 看到 pod 被调度到节点上,告知 Docker 拉取指定镜像

访问 Web 应用

每个 pod 都有独立的 IP 地址,但是该地址属于集群内部,在集群外部无法访问,要从外部访问该 pod,需要通过服务对象公开它,需要创建一个特殊的 LoadBalancer(相应地还有 ClusterIp 服务,只能从集群内部访问) 类型的服务。顾名思义,LoadBalancer 类型服务即外部的负载均衡服务

创建一个服务对象

kubectl expose rc kubia --type=LoadBalancer --name kubia-http

列出服务:

kubectl get service
kubectl get svc

注意:由于 Minikube 不支持 LoadBalancer 类型的服务,因此服务不会有外部 IP,但可以通过 shell minikube service kubia-http 命令访问到服务的 IP 和端口。

使用外部 IP 访问服务

curl $(EXTERNAL-IP):$(PORT)

可以发现,应用将 pod 作为其主机名,这也应证了对于应用程序来说,其就像运行在一台专用机器上,没有其他进程一同运行。

系统的逻辑部分

ReplicationController、pod 和 服务是如何组合在一起的

我们并不直接创建 pod,而是通过运行 kubectl run 创建了一个 ReplicationController,其用于创建 pod 实例。而为了使 pod 能从集群外部访问,Kubernetes 将该 ReplicationController 管理的 pod 由一个服务对外暴露。

image-20191217235846337

pod 和 它的容器

通常 pod 可以包含任意数量的容器,容器内部运行 Nodejs 进程,该进程绑定到 8080 端口,等待 HTTP 请求,每个 pod 有独立的私有 IP 和主机名。

ReplicationController 的角色

ReplicationController 确保始终存在一个运行的 pod 实例。

通常,ReplicationController 用于复制 pod(即创建 pod 的多个副本)并让它们运行。

为什么需要服务

pod 可能会在任何时候消失,或者发生故障,或者被删除等,此时其将会被 ReplicationController 替换为新的 pod,而新 pod 与被替换的 pod 具有不同的 IP 地址,因此就需要服务来解决 IP 地址不断变化的问题。

服务被创建后会得到一个静态 IP,且在服务的生命周期中 IP 不会发生改变。客户端通过该 IP 连接到服务,到达服务的 IP 和端口的请求江北转到到属于该服务的一个容器的 IP 和端口,服务会确保其中一个 pod 接收到连接。

水平伸缩应用

查看 ReplicationController:

kubectl get replicationcontrollers
kubectl get rc

DESIRED 列显示希望 ReplicationController 保持的 pod 副本数,而 CURRENT 列显示当前运行的 pod 数。

增加期望副本数

kubectl scale rc kubia --replicas=3
kubectl get pods

此时 ReplicationController 将保证有 3 个实例在运行。当应用本身支持水平伸缩时,Kubernetes 只是让应用的扩容和缩容变得简单,而不会让应用变得可扩展。

当切换服务时请求切换到所有三个 pod 上

当再次从集群外部访问服务时,请求被随机切换到不同的 pod 上(通过从请求获取的主机名获得该请求落到哪个 pod 上),服务作为负载均衡挡在 pod 前面。

可视化系统新状态

image-20191218001652869

查看应用运行在哪个节点上

在 Kubernetes 世界中,pod 运行在哪个节点并不重要,容器运行的所有应用都具有相同的操作系统。

列出 pod 时显示 pod IP 和 pod 的节点

kubectl get pods -o wide

查看 pod 的其他细节

kubectl describe pod kubia-snxzv

介绍 Kubernetes dashboard

Kubernetes 提供了 web dashboard,通过以下方式可以访问到集群的 dashboard:

  • GKE:从 shell kubectl cluster-info | grep dashboard 命令获取 dashboard URL

  • Minikube: 运行 shell minikube dashboard 打开 dashboard