Kubernetes 简介

引言

月初公司内云计算团队组织了一次 Workshop,介绍了一下如何使用他们的 Kubernetes 产品,并带着我们实践了一下。感觉 Kubernetes 相较于我们现在使用的部署模式,方便了很多,虽然我们现在的系统使用场景并不适合使用 Kubernetes,但是说不定以后就能用到它了,所以我就深入的研究了一下 Kubernetes 的使用和实现机理,希望能给大家带来帮助。本文作为 Kubernetes 系列文章的开篇,着重介绍一下 Kubernetes 是什么以及简单的使用方式,更多关于 Kubernetes 的介绍均收录于<Kubernetes系列文章>中。

介绍

背景

在过去,多数的应用都是大型单体应用,以单个进程或几个进程的方式,运行于几台服务器之上。这些应用的发布周期长,而且迭代也不频繁。每个发布周期结束前,开发者会把应用程序打包后交付给运维团队,运维人员再处理部署、监控事件,并且在硬件发生故障时手动迁移应用。
single-2-microservice
今天,大型单体应用正被逐渐分解成小的、可独立运行的组件,我们称之为微服务。微服务彼此之间解耦,所以它们可以被独立开发、部署、升级、伸缩。这使得我们可以对每一个微服务实现快速迭代,并且迭代的速度可以和市场需求变化的速度保持一致。

但是,随着部署组件的增多和数据中心的增长,配置、管理并保持系统的正常运行变得越来越困难。如果我们想要获得足够高的资源利用率并降低硬件成本,把组件部署在什么地方变得越来越难以决策。手动做所有的事情,显然不太可行。我们需要一些自动化的措施,包括自动调度、配置、监管和故障处理。这正是 Kubernetes 的用武之地。

Kubernetes 使开发者可以自主部署应用,并且控制部署的频率,完全脱离运维团队的帮助。Kubernetes 同时能让运维团队监控整个系统,并且在硬件故障时重新调度应用。系统管理员的工作重心,从监管应用转移到了监管 Kubernetes,以及剩余的系统资源,因为 Kubernetes 会帮助监管所有的应用。

正如己经提到的,一个微服务架构中的组件不仅被独立部署,也被独立开发。因为它们的独立性,出现不同的团队开发不同的组件是很正常的事实,每个团队都有可能使用不同的库并在需求升级时替换它们。如图所示,因为组件之间依赖的差异性,应用程序需要同一个库的不同版本是不可避免的。
lib-different
部署动态链接的应用需要不同版本的共享库,或者需要其他特殊环境,在生产服务器部署并管理这种应用很快会成为运维团队的噩梦。需要在同一个主机上部署的组件数量越大,满足这些组件的所有需求就越难。为了减少这种问题,最理想的做法是让应用在开发和生产阶段可以运行在完全一样的环境下,它们有完全一样的操作系统、库、系统配置、网络环境和其他所有的条件。你也不想让这个环境随着时间推移而改变。如果可能,你想要确保在一台服务器上部署新的应用时,不会影响到机器上已有的应用。

在最近几年中,我们看到了应用在开发流程和生产运维流程中的变化。在过去,开发团队的任务是创建应用并交付给运维团队,然后运维团队部署应用并使它运行。但是现在,公司都意识到,让同一个团队参与应用的开发、部署、运维的整个生命周期更好。这意味着开发者、QA 和运维团队彼此之间的合作需要贯穿整个流程。这种实践被称为 DevOps。

正如你所看到的,Kubernetes 能让我们实现所有这些想法。通过对实际硬件做抽象,然后将自身暴露成一个平台,用于部署和运行应用程序。它允许开发者自己配置和部署应用程序,而不需要系统管理员的任何帮助,让系统管理员聚焦于保持底层基础设施运转正常的同时,不需要关注实际运行在平台上的应用程序。

容器

Kubernetes 使用 Linux 容器技术来提供应用的隔离,所以在钻研 Kubernetes 之前,需要通过熟悉容器的基本知识来更加深入地理解 Kubernetes, 包括认识到存在的容器技术分支,诸如 Docker 或者 rkt。

在前面我们看到在同一台机器上运行的不同组件需要不同的、可能存在冲突的依赖库版本,或者是其他的不同环境需求。

当一个应用程序仅由较少数量的大组件构成时,完全可以接受给每个组件分配专用的虚拟机,以及通过给每个组件提供自己的操作系统实例来隔离它们的环境。但是当这些组件开始变小且数量开始增长时,如果你不想浪费硬件资源,又想持续压低硬件成本,那就不能给每个组件配置一个虚拟机了。但是这还不仅仅是浪费硬件资源,因为每个虚拟机都需要被单独配置和管理,所以增加虚拟机的数量也就导致了人力资源的浪费,因为这增加了系统管理员的工作负担。

开发者不是使用虚拟机来隔离每个微服务环境,而是正在转向 Linux 容器技术。容器允许你在同一台机器上运行多个服务,不仅提供不同的环境给每个服务,而且将它们互相隔离。容器类似虚拟机,但开销小很多。它允许在相同的硬件上运行更多数量的组件。主要是因为每个虚拟机需要运行自己的一组系统进程,这就产生了除组件进程消耗以外的额外计算资源损耗。从另一方面说,一个容器仅仅是运行在宿主机上被隔离的单个进程,仅消耗应用容器消耗的资源,不会有其他进程的开销。
vm-container
当你在一台主机上运行三个虚拟机的时候,你拥有了三个完全分离的操作系统,它们运行并共享一台裸机。在那些虚拟机之下是宿主机的操作系统与一个管理程序,它将物理硬件资源分成较小部分的虚拟硬件资源,从而被每个虚拟机里的操作系统使用。运行在那些虚拟机里的应用程序会执行虚拟机操作系统的系统调用,然后虚拟机内核会通过管理程序在宿主机上的物理 CPU 来执行 x86 指令。
vm-arch
多个容器则会完全执行运行在宿主机上的同一个内核的系统调用,此内核是唯一一个在宿主操作系统上执行 x86 指令的内核。 CPU 也不需要做任何对虚拟机能做那样的虚拟化。
container-arch
虚拟机的主要好处是它们提供完全隔离的环境,因为每个虚拟机运行在它自己的 Linux 内核上,而容器都是调用同一个内核,这自然会有安全隐患。但是,为了在同一台机器上运行大量被隔离的进程,容器因它的低消耗而成为一个更好的选择。记住,每个虚拟机运行它自己的一组系统服务,而容器则不会,因为它们都运行在同一个操作系统上。那也就意味着运行一个容器不用像虚拟机那样要开机,它的进程可以很快被启动。

那么,容器技术是如何在同一个操作系统上隔离各个进程的呢。有两个机制可用:第一个是 Linux 命名空间,它使每个进程只看到它自己的系统视图(文件、进程、网络接口、主机名等);第二个是 Linux 控制组(cgroups), 它限制了进程能使用的资源量(CPU、 内存、 网络带宽等)。

Kubernetes

Kubernetes 是一个软件系统,它允许你在其上很容易地部署和管理容器化的应用。它依赖于 Linux 容器的特性来运行异构应用,而无须知道这些应用的内部详情,也不需要手动将这些应用部署到每台机器。Kubernetes 使你在数以千计的电脑节点上运行软件时就像所有这些节点是单个大节点一样。它将底层基础设施抽象,这样做同时简化了应用的开发、部署,以及对开发和运维团队的管理。

下图展示了一幅最简单的 Kubernetes 系统图。整个系统由 一个主节点和若干个工作节点组成。 开发者把一个应用列表提交到主节点,Kubernetes 会将它们部署到集群的工作节点。组件被部署在哪个节点对于开发者和系统管理员来说都不用关心。
sample-k8s-arch
开发者能指定一些应用必须一起运行,Kubernetes 将会在一个工作节点上部署它们。其他的将被分散部署到集群中,但是不管部署在哪儿,它们都能以相同的方式互相通信。

Kubernetes 可以被当作集群的一个操作系统来看待。它降低了开发者不得不在他们的应用里实现一些和基础设施相关服务的心智负担。他们现在依赖于 Kubernetes 来提供这些服务,包括服务发现、扩容、负载均衡、自恢复,甚至领导者的选举。Kubernetes 还能在任何时间迁移应用并通过混合和匹配应用来获得比手动调度高很多的资源利用率。

我们已经知道了 Kubernetes 的工作方式,现在让我们近距离看一下 Kubernetes 集群的组成。在硬件层面,Kubernetes 集群由两种节点组成:

  • 主节点:它承载着 Kubernetes 控制和管理整个集群系统的控制面板
  • 工作节点:它们运行用户实际部署的应用

k8s-nodes
控制面板用于控制集群并使它工作。它包含多个组件,组件可以运行在单个主节点上或者通过副本分别部署在多个主节点以确保高可用性。这些组件是:

  • Kubernetes API 服务器:你和其他控制面板组件都要和它通信
  • Scheduler:它调度你的应用(为应用的每个可部署组件分配一个工作节点〕
  • Controller Manager:它执行集群级别的功能,如复制组件、持续跟踪工作节点、处理节点失败等
  • etcd:一个可靠的分布式数据存储,它能持久化存储集群配置

控制面板的组件持有并控制集群状态,但是它们不运行你的应用程序。这是由工作节点完成的。工作节点是运行容器化应用的机器。运行、监控和管理应用服务的任务是由以下组件完成的:

  • 容器:Docker、rtk 或其他的容器
  • Kubelet:它与 API 服务器通信,并管理它所在节点的容器
  • Kubernetes Service Proxy (kube-proxy),它负责组件之间的负载均衡网络流量

为了在 Kubernetes 中运行应用,首先需要将应用打包进一个或多个容器镜像,再将那些镜像推送到镜像仓库,然后将应用的描述发布到 Kubernetes API 服务器。

该描述包括诸如容器镜像或者包含应用程序组件的容器镜像、这些组件如何相互关联,以及哪些组件需要同时运行在同一个节点上和哪些组件不需要同时运行等信息。此外,该描述还包括哪些组件为内部或外部客户提供服务且应该通过单个 IP 地址暴露,并使其他组件可以发现。

当 API 服务器处理应用的描述时,调度器调度指定组(Pod)的容器到可用的工作节点上,调度是基于每组(Pod)所需的计算资源,以及调度时每个节点未分配的资源。然后,那些节点上的 Kubelet 指示容器运行时(例如 Docker)拉取所需的镜像并运行容器。这里提到的”组”概念,在 Kubernetes 中被称为 Pod,同一个 Pod 可以包含多个镜像,但是同一个 Pod 中的所有进程就如同运行在同一个物理机一样相互可以看见,它们之间未做隔离。

一旦应用程序运行起来,Kubernetes 就会不断地确认应用程序的部署状态始终与你提供的描述相匹配。例如,如果你指出你需要运行五个 web 服务器实例,那么 Kubernetes 总是保持正好运行五个实例。如果实例之一停止了正常工作,比如当进程崩溃或停止响应时,Kubernetes 将自动重启它。
k8s-work
当应用程序运行时,可以决定要增加或减少副本量,而 Kubernetes 将分别增加附加的或停止多余的副本。甚至可以把决定最佳副本数目的工作交给 Kubernetes。它可以根据实时指标(如 CPU 负载、内存消耗、每秒查询或应用程序公开的任何其他指标)自动调整副本数。

我们已经说过,Kubernetes 中的容器可能是动态的,它随时可能从一台机器上被销毁,然后在另一台机器上重新启动。那么当容器在集群内频繁调度时,它们该如何正确使用这个容器?当这些容器被复制并分布在整个集群中时,客户端如何连接到提供服务的容器呢?

为了让客户能够轻松地找到提供特定服务的容器,可以告诉 Kubernetes 哪些容 器提供相同的服务,而 Kubernetes 将通过一个静态 IP 地址暴露所有容器,并将该地址暴露给集群中运行的所有应用程序。您可以通过环境变量或者 DNS 服务来共享服务 IP。kube-proxy 将确保到服务能连接到对应的容器上。

Kubernetes 的优点:

  • 简化应用程序部署
  • 更好地利用硬件
  • 健康检查和自修复
  • 自动扩容

试玩环节

在深入学习 Kubernetes 的概念之前,先来看看如何创建一个简单的应用,把它打包成容器镜像并在远端的 Kubernetes 集群中运行。这会对整个 Kubernetes 体系有较好的了解,并且会让接下来几个章节对 Kubernetes 基本概念的学习变得简单。

Docker

正如在之前章节所介绍的,在 Kubernetes 中运行应用需要打包好的容器镜像。本节将会对 Docker 的使用做简单的介绍。接下来将会介绍:

  1. 安装 Docker 并运行第一个 “Hello world” 容器
  2. 创建一个简单的 Node.js 应用并部署在 Docker 中
  3. 把应用打包成可以独立运行的容器镜像
  4. 基于镜像运行容器
  5. 把镜像推送到 DockerHub,这样任何人在任何地方都可以使用

首先,我们需要在 Linux 主机上安装 Docker,我使用了公司内部的云平台,申请了一台虚拟机,因为之后打算用它来做 Kubernetes 集群的节点(Kubernetes 的 master 节点需要至少 2 核 cpu),所以我选用的虚拟机配置如下:

  • CPU:4 核
  • 内存:4G
  • 硬盘:60 GB SSD
  • 系统:CentOS 7.6

第一步,安装并启动 Docker,并运行一个 busybox 的 “Hello world” 容器:

1
2
3
4
5
6
7
8
# 使用 yum 安装 docker
sudo yum install docker-1.13.1
# 启动 docker
sudo systemctl start docker
# 修改本地 docker 监听的文件的权限
sudo chmod 777 /var/run/docker.sock
# 运行 busybox 容器
docker run busybox echo "Hello world"

busybox 是一个单一可执行文件,包含多种标准 UNIX 命令行工具,如: echo、ls、 gzip 等。在这一步中,我们仅通过一条命令,就下载运行了一个完整的”应用”,而不用做其他事情。其背后的原理是:首先,Docker会检查 busybox:latest 镜像是否己经存在于本机。如果没有,Docker 会从 http://docker.io 的 Docker 镜像中心拉取镜像。镜像下载到本机之后,Docker 基于这个镜像创建一个容器并在容器中运行命令。echo 命令打印文字到标准输出流,然后进程终止,容器停止运行。
run-docker-busybox
运行其他容器镜像和上述过程是一样的,您只需要执行docker run <image>即可。Docker 使用 tag 来表示同一个镜像的不同版本,默认情况下会使用最新的镜像,当您想要执行指定版本的镜像时,可以执行docker run <image>:<tag>

接下来,创建一个简单的 Node.js 应用并打包部署在 Docker 中:

该应用会将自己的主机名作为 HTTP 请求的响应内容。

1
2
3
4
5
6
7
8
9
10
11
// app.js
const http = require('http');
const os = require ('os');
console.log("Kubi a server starting ... ");
var handler = function(request, response) {
console.log("Recei ved request from " + request.connection.remoteAddress);
response.writeHead(200);
response.end("You've hit " + os.hostname() + "\n");
};
var www = http.createServer(handler);
www.listen(8080);

为了将上述的 node 应用打包成镜像,我们还需要在 app.js 的相同路径下创建一个 Dockerfile 文件,它包含了构建镜像的指令:

1
2
3
FROM node:7
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]

From 行定义了镜像的起始内容(构建所基于的基础镜像)。是 node 镜像的 tag 7 版本。第二行中把 app.js 文件从本地文件夹添加到镜像的根目录,保持 app.js 这个文件名。最后一行定义了当镜像被运行时需要被执行的命令,这个例子中,命令是 node app.js

构建好 app.js 和 Dockerfile 后,只需要执行docker build -t kubia .,Docker 就会自动帮我们把镜像创建出来。
docker-build
镜像不是一个大的二进制块,而是由多层组成的,不同镜像可能会共享分层,这会让存储和传输变得更加高效。你或许会认为每个 Dockerfile 只创建一个新层,但是并不是这样的。构建镜像时,Dockerfile 中每一条单独的指令都会创建一个新层。下图就展示了 Docker 的分层思想,其中也展示了一个名 other:last 的镜像,它就和我们正在构建的镜像共享了部分分层。
docker-build-level
构建完成后,新的镜像会存储在本地,我们可以使用docker images查看刚才构建好的镜像, 可以通过如下命令运行刚才构建出来的镜像:

1
2
3
4
5
6
7
docker run --name kubia-container -p 8080:8080 -d kubia
# 返回 container id
curl http://localhost:8080
# 查看运行中的容器
docker ps
# 查看更详细的容器描述
docker inspect kubia-container

这条命令告知 Docker 基于 kubia 镜像创建一个叫 kubia-container 的新容器。这个容器与命令行分离(-d 标志),这意味着在后台运行。本机上的 8080 端口会被映射到容器内的 8080 端口(-p 8080:8080),所以可以通过 http://localhost:8080 访问这个应用。通过执行docker ps我们可以发现,容器内的主机名实际上就是容器 ID。

我们可以通过如下命令进入到容器内部,该命令中 -i 确保准输入流保持开放。需要在 shell 中输入命令。-t, 分配一个伪终端(TTY)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 进入容器内部
docker exec -it kubia-container bash
# 查看容器内进程
ps -aux
# 查看容器内文件系统
ls
# 退出容器 shell
exit
# 查看宿主机是否存在容器内的进程
ps -aux|grep app.js
# 停止容器
docker stop kubia-container
# 删除容器
docker rm kubia-container

通过对比容器内的进程和宿主机进程,你就可以证明运行在容器中的进程是运行在主机操作系统上的。如果你足够敏锐,会发现进程的 ID 在容器中与主机上不同。容器使用独立的 PID Linux 命名空间并且有着独立的系列号,完全独立于宿主机进程树。正如拥有独立的进程树一样,每个容器也拥有独立的文件系统。在容器内列出根目录的内容,只会展示容器内的文件,包括镜像内的所有文件,再加上容器运行时创建的任何文件。

最后,我们试一试将镜像推送到镜像仓库中:

1
2
3
4
5
6
# 定义镜像的新 tag,<docker hub id>/kubia
docker tag kubia beikejiedeliulangmao/kubia
# 登录 docker hub io,并输入账号密码
docker login
# 推送本地镜像到 docker 镜像仓库中
docker push beikejiedeliulangmao/kubia

如果您和我一样上述操作都执行成功的话,应该就能在 docker hub 中看到刚才提交的镜像了,最后我们确认一下直接从仓库下载并运行镜像的流程是否通畅:

1
2
3
4
5
6
# 先删除本地的镜像,确保后面的指令从镜像仓库下载镜像,dcb98c6ae282 是我这里的镜像 id
docker image rm dcb98c6ae282 -f
# 运行 docker 仓库中的镜像
docker run -p 8080:8080 -d beikejiedeliulangmao/kubia
# 测试
curl http://localhost:8080

很棒,一切都运转正常,以后应用无论在哪运行,都能确保在完全一致的环境中。我们不用再关心主机是否安装了应用下层依赖的库(Node.js),因为当应用启动后,只会使用镜像内部的 Node。上述的流程,就会是日后使用 Kubernetes 发布应用的例行工作,开发完成-> 构建新的镜像 -> 提交到镜像仓库 -> 使用 Kubernetes 发布最新镜像中的应用。

Kubernetes 集群

现在,应用被打包在一个容器镜像中,并通过 Docker Hub 给 Kubernetes 使用。但是,我们还没有自己的 Kubernetes 集群,所以,在进行 Kubernetes 发布应用的试玩环节前,我们先构建一个自己的 Kubernetes 集群。为了让本文更具实际指导意义,我们将构建一个多节点的 Kubernetes 集群,而不是使用第三方云平台的工具或者 Minikube 这样的单机构建工具。

我们的目标集群规模如下:

  • Master: 3 台
  • Etcd:3 台
  • Worker: 3 台

为了达到本节的试玩目标,我又申请了 8 台虚拟机,它们的配置都和 Docker 试玩时使用的虚拟机配置相同。因为我们的云服务并不提供服务器镜像功能,所以准备好机器后,我分别在每台机器上执行了如下准备命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 切换到 root 身份
sudo -i
# 临时性地禁用 SELinux,因为开着它会出现一些权限问题,解决起来很麻烦,所以指导手册上都建议关闭
setenforce 0
# 修改系统配置文件永久禁用 SELinux, 修改 SeLinux 配置为 SELINUX=permissive, 因为我们的虚拟机默认都是关闭该组件,所以我这里跳过该过程
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
# 为了避免遇到防火墙相关问题,禁用防火墙
systemctl disable firewalld && systemctl stop firewalld
# 在 yum 仓库中添加 Kubernetes
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF
# 安装各种组件:容器运行时,Kubernetes 节点代理,集群管理工具,交互式命令行工具,网络接口插件
yum install -y docker-1.13.1 kubelet-1.16.0 kubeadm-1.16.0 kubectl-1.16.0 kubernetes-cni-0.7.5
# 开启 docker
systemctl enable docker && systemctl start docker
# 开启节点代理
systemctl enable kubelet && systemctl start kubelet
# 为了确保 iptable 的使用设置相关参数
cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system
# 禁用交换分区
swapoff -a && sed -i '/swap/s/^/#/' /etc/fstab

因为默认 Kubernetes 集群的 etcd 是和控制节点运行在一起的,这样当任意一台控制节点宕机时,也就意味着一个 etcd 节点也会宕机,为了让整个集群更加高可用,所以本例中,我们配置并搭设独立的 etcd 集群,这两种方案的对比可以参考官方文档。不过好在 kubeadm 中包含了搭建 etcd 集群的基本工具,我们可以快速的构建起独立的 etcd 集群。

搭建 etcd 集群

为了搭建独立的 etcd 集群,我们需要重新配置 kubelet 的参数:

1
2
3
4
5
6
7
8
9
10
11
mkdir /etc/systemd/system/kubelet.service.d/
cat << EOF > /etc/systemd/system/kubelet.service.d/20-etcd-service-manager.conf
[Service]
ExecStart=
# Replace "systemd" with the cgroup driver of your container runtime. The default value in the kubelet is "cgroupfs".
ExecStart=/usr/bin/kubelet --address=127.0.0.1 --pod-manifest-path=/etc/kubernetes/manifests --cgroup-driver=systemd
Restart=always
EOF

systemctl daemon-reload
systemctl restart kubelet

然后,我们使用如下脚本给 kubeadm 创建配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 直接在命令行执行即可不需要放入脚本文件执行
# Update HOST0, HOST1, and HOST2 with the IPs or resolvable names of your hosts
export HOST0=10.x.x.x
export HOST1=10.x.x.x
export HOST2=10.x.x.x

# Create temp directories to store files that will end up on other hosts.
mkdir -p /tmp/${HOST0}/ /tmp/${HOST1}/ /tmp/${HOST2}/

ETCDHOSTS=(${HOST0} ${HOST1} ${HOST2})
NAMES=("infra0" "infra1" "infra2")

for i in "${!ETCDHOSTS[@]}"; do
HOST=${ETCDHOSTS[$i]}
NAME=${NAMES[$i]}
cat << EOF > /tmp/${HOST}/kubeadmcfg.yaml
apiVersion: "kubeadm.k8s.io/v1beta2"
kind: ClusterConfiguration
etcd:
local:
serverCertSANs:
- "${HOST}"
peerCertSANs:
- "${HOST}"
extraArgs:
initial-cluster: ${NAMES[0]}=https://${ETCDHOSTS[0]}:2380,${NAMES[1]}=https://${ETCDHOSTS[1]}:2380,${NAMES[2]}=https://${ETCDHOSTS[2]}:2380
initial-cluster-state: new
name: ${NAME}
listen-peer-urls: https://${HOST}:2380
listen-client-urls: https://${HOST}:2379
advertise-client-urls: https://${HOST}:2379
initial-advertise-peer-urls: https://${HOST}:2380
EOF
done

如果您没有现成的身份认证秘钥对,就需要像我一样自己给 etcd 集群创建秘钥,我们可以在任意 etcd 节点上通过 kubeadm 创建,命令为kubeadm init phase certs etcd-ca,它会自动创建如下两个秘钥对:

  • /etc/kubernetes/pki/etcd/ca.crt
  • /etc/kubernetes/pki/etcd/ca.key

接下来,我们需要将该秘钥拷贝到其他 etcd 节点上去,因为内部服务器不能直接使用 scp 拷贝文件,所以我手动将生成的秘钥对文件分别复制到了另外两台机器的相同路径。

准备就绪后,我们要在这三台机器上分别启动 etcd。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 机器 1
kubeadm init phase certs etcd-server --config=/tmp/${HOST0}/kubeadmcfg.yaml
kubeadm init phase certs etcd-peer --config=/tmp/${HOST0}/kubeadmcfg.yaml
kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST0}/kubeadmcfg.yaml
kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST0}/kubeadmcfg.yaml
# 机器 2
kubeadm init phase certs etcd-server --config=/tmp/${HOST1}/kubeadmcfg.yaml
kubeadm init phase certs etcd-peer --config=/tmp/${HOST1}/kubeadmcfg.yaml
kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST1}/kubeadmcfg.yaml
kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST1}/kubeadmcfg.yaml
# 机器 3
kubeadm init phase certs etcd-server --config=/tmp/${HOST2}/kubeadmcfg.yaml
kubeadm init phase certs etcd-peer --config=/tmp/${HOST2}/kubeadmcfg.yaml
kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST2}/kubeadmcfg.yaml
kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST2}/kubeadmcfg.yaml

当上述操作都顺利执行完毕后,确认一下各个 etcd 节点上的文件是否都型如下图,其中 HOST0 在每个机器上应该对应为该机器的 IP 地址:
etcd-files
最后,在每台机器上生成静态 pod 清单:

1
2
3
4
5
6
# 机器 1,HOST0 是机器 1 的 ip 地址,下同
kubeadm init phase etcd local --config=/tmp/${HOST0}/kubeadmcfg.yaml
#机器 2
kubeadm init phase etcd local --config=/tmp/${HOST1}/kubeadmcfg.yaml
# 机器 3
kubeadm init phase etcd local --config=/tmp/${HOST2}/kubeadmcfg.yaml

这里大家可能会不太懂这个静态 pod 清单的意义。实际上,当 Kubernetes 的 kubelet 组件启动后,会监视本机的所有静态 pod 清单,并自动启动这些静态 pod 对应的容器,换言之,这些 etcd 节点上的 etcd 进程实际上是由 Kubernetes 托管的,它会保证每个节点上的 etcd 正常的在容器中运行,当前节点的 etcd 意外关闭时,kubelet 会自动恢复它。你可以大胆的通过 docker stop来关闭任意节点上的 etcd 容器,当你再执行 docker ps时,你就会发现当前节点的 etcd 实例已经被自动重启了。

此外,我们还可以通过 etcdctl 来测试 etcd 的可用性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 下面的 HOST0 需要修改为准备执行下列脚本的机器 IP
docker run --rm -it \
--net host \
-v /etc/kubernetes:/etc/kubernetes quay.io/coreos/etcd:v3.2.24 etcdctl \
--cert-file /etc/kubernetes/pki/etcd/peer.crt \
--key-file /etc/kubernetes/pki/etcd/peer.key \
--ca-file /etc/kubernetes/pki/etcd/ca.crt \
--endpoints https://${HOST0}:2379 cluster-health
# 当集群启动后,您可以通过如下命令确认 Kubernetes 都在 etcd 中存储了什么内容
docker run --rm -it -e "ETCDCTL_API=3" \
--net host \
-v /etc/kubernetes:/etc/kubernetes quay.io/coreos/etcd:v3.2.24 etcdctl \
--cert=/etc/kubernetes/pki/etcd/peer.crt \
--key=/etc/kubernetes/pki/etcd/peer.key \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--endpoints=[https://${HOST0}:2379] get / --prefix --keys-only

如果上述过程都正常完成的话,就说明 etcd 集群已经搭建好了,接下来我们搭建 Kubernetes 集群。

搭建控制节点

在这一步,我们得先把 etcd 服务的证书和 etcd 接口通讯的证书和秘钥拷贝到其中一个控制节点上,拷贝到一台就够了,其他控制节点在加入集群时会自动获取。要拷贝的文件列表如下:

  • etcd 服务证书:/etc/kubernetes/pki/etcd/ca.crt
  • etcd 接口证书:/etc/kubernetes/pki/apiserver-etcd-client.crt
  • etcd 接口秘钥:/etc/kubernetes/pki/apiserver-etcd-client.key

证书和秘钥准备完毕后,我们需要创建如下配置文件 kubeadm-config.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 配置文件中 LOAD_BALANCER_DNS 和 LOAD_BALANCER_PORT 需要修改为您为控制节点申请的 DNS(dns 包含所有控制节点的 ip)和各个控制节点 api server 使用的端口(默认:6443),ETCD_X_IP 改为 etcd 节点的 ip
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
kubernetesVersion: stable
controlPlaneEndpoint: "LOAD_BALANCER_DNS:LOAD_BALANCER_PORT"
etcd:
external:
endpoints:
- https://ETCD_0_IP:2379
- https://ETCD_1_IP:2379
- https://ETCD_2_IP:2379
caFile: /etc/kubernetes/pki/etcd/ca.crt
certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt
keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key
# 填写 `podSubnet` 和 `serviceSubnet` 时一定要注意避开节点的 host ip 域(我这是 10.0.0.0/8,如果不避开该域会导致无法登陆主机) 和 docker 的 ip 域(172.17.0.1/16),同时它们之间也要互不冲突
networking:
serviceSubnet: "YOUR_SERVICE_SUBNET"
podSubnet: "YOUR_POD_SUBNET"

修改好配置文件之后,我们通过 kubeadm 的初始化命令kubeadm init --config kubeadm-config.yaml --upload-certs启动首个控制节点。在该命令的返回日志中,会包含命令行工具的验证文件,其他控制节点加入集群的命令,以及工作节点加入集群的命令,您需要将这些命令保存起来方便以后使用,它们的格式大致如下:

1
2
3
4
5
6
7
8
9
10
11
# 命令行工具验证文件配置
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
# 控制节点接入命令,需要在其他控制节点执行
kubeadm join <LOAD_BALANCER_DNS>:<LOAD_BALANCER_PORT> --token 0ur873.o4zl4fldg7ewrce0 \
--discovery-token-ca-cert-hash sha256:7bc51f3ec0f250f3e1e0d812fe61fe9a0a3d94abd66a4971f780ca2a6cbf88bb \
--control-plane --certificate-key 28806632940988ed0ed67c68c763485c9a5bb4df43e7860849702419cec71c48
# 工作节点接入命令,需要在所有工作节点执行
kubeadm join <LOAD_BALANCER_DNS>:<LOAD_BALANCER_PORT> --token 0ur873.o4zl4fldg7ewrce0 \
--discovery-token-ca-cert-hash sha256:7bc51f3ec0f250f3e1e0d812fe61fe9a0a3d94abd66a4971f780ca2a6cbf88bb

配置好命令行工具的验证文件后,还需要在首个控制节点应用网络插件,您可以通过如下命令做到:

1
2
3
4
5
6
7
8
# 先下载网络插件(flannel)的配置文件
wget https://raw.githubusercontent.com/coreos/flannel/b30e6895ef6429d47eaee1a72047ca349b3b5de3/Documentation/kube-flannel.yml
# 修改插件配置文件,加入 cniVersion 的描述,如果不加的话会导致网络插件启动报错
sed -i '/"name": "cbr0"/s/$/\n "cniVersion":"0.3.1",/' kube-flannel.yml
# 除了 cniVersion 之外还要将插件配置文件中 net-conf.json 的 Network 改成您之前设置的 podSubnet(别忘了用\转义'/'),否则在 pod 内部无法访问其他 pod
sed -i 's/"Network": "10.244.0.0\/16"/"Network": "<YOUR_POD_SUBNET>"/g' kube-flannel.yml
# 应用插件配置
kubectl create -f kube-flannel.yml

配置好网络插件后,你就可以在其他节点上按照前面保存的接入命令分别进行节点接入,最后您可以将控制节点的 /etc/kubernetes/admin.conf 拷贝到本地的 $HOME/.kube/config,这样您就可以在本地操作整个集群了。配置好 Kubernetes 命令行环境后,可以通过下列命令确认整个集群的运转情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看集群中的节点以及状态,正常情况下应该看到各个节点的状态都是 Ready
kubectl get nodes -o wide
# 查看集群中运行的 Pod,正常情况下每个节点都有kube-flannel,kube-proxy,此外控制节点额外有kube-scheduler,kube-controller,kube-apiserver,最后还有2个 coredns
kubectl get pods -A -o wide
# 其中,kube-flannel 这个网络插件负责不同主机 pod 之间的通讯,您可以看到虚拟网卡 flannel.1 的 ip 段和之前配置的 pod-subnet 相同,如果您在 worker 节点执行 ifconfig 则还会看到一个 cni0 网卡,它负责同一个节点内 pod 之间的网络通讯,该网卡的 ip 域实际上就是 host 该节点上分配到的 pod 网段
ifconfig
# 如果最终的效果和本文的描述有出入,可以通过下述命令进行排查
journalctl -xu kubelet.service -f
# 因为我的 log 里会报一个 'Failed to get system container stats for "/system.slice/docker.service": failed to get cgroup stats for "/system.slice/docker.service"' 的错误,所以还执行了下列命令
sed -i '/KUBELET_EXTRA_ARGS=/s/$/--runtime-cgroups=\/systemd\/system.slice --kubelet-cgroups=\/systemd\/system.slice/' /etc/sysconfig/kubelet
# 上述命令在 /etc/sysconfig/kubelet 末尾增加了一句--runtime-cgroups=/systemd/system.slice --kubelet-cgroups=/systemd/system.slice
# 最后重启 kubelet 就能解决上述报错
systemctl restart kubelet

部署应用

配置好 Kubernetes 集群后,我们就通过之前 Docker 试玩环节使用的 Node Demo kubia 来体验一下如何在 Kubernetes 中部署应用。这里我们先通过 ReplicationController 接口来启动该应用,首先我们要创建一个配置文件 rc.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: ReplicationController
metadata:
name: kubia
spec:
replicas: 3 # 同时运行 3 个服务
selector:
app: kubia
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- name: kubia
image: beikejiedeliulangmao/kubia # 镜像
ports:
- containerPort: 8080

准备好配置文件之后,我们通过命令行工具来注册该 ReplicationController:

1
kubectl apply -f rc.yaml

如果上述命令正常执行,您将像我一样创建了一个名为 kubia 的 ReplicationController,它会帮助我们维持整个集群恰好有 3 个 kubia 服务(之前的 Node 服务),您可以通过 kubectl get pod -o wide 查看 ReplicationController 为我们创建的 Pod,您可以删除任意一个 Pod 来模拟服务中崩溃,当你再次查看运行中的 pod 时,你会发现 ReplicationController 帮我们又重建了一个新的 Pod。

一个 Pod 是一组紧密相关的容器,它们总是一起运行在同一个工作节点上,以及同一个 Linux 命名空间中。每个 pod 就像一个独立的逻辑机器,拥有自己的 IP、主机名、进程等,运行一个独立的应用程序。应用程序可以是单个进程,运行在单个容器中,也可以是一个主应用进程或者其他支持进程,每个进程都在自己的容器中运行。一个 pod 的所有容器都运行在同一个逻辑机器上,而其他 pod 中的容器,即使运行在同一个工作节点上,也会出现在不同的节点上。
pod
当运行 kubectl 命令时,它通过向 Kubernetes API 服务器发送一个 REST HTTP 请求来创建一个 ReplicationController,ReplicationController 创建成功后,会根据配置文件中指定的 Pod 个数,在集群中创建一个相应数量 Pod 对象。然后,调度器将其调度到一个工作节点上。随后,该工作节点上的 Kubelet 看到 pod 被调度到自己身上,就告知 Docker 从镜像中心中拉取指定的镜像,因为本地没有该镜像。下载镜像后,Docker 创建并运行容器。
kubectl-run
那么,既然服务已经运行起来了,我们怎么访问该服务呢?我们提到过每个 Pod 都有自己的 IP,但是 Pod 可能随时都会崩溃,为了让集群内部的其他服务能够稳定的访问前面创建的 kubia,我们还需要创建一个服务对象,来暴露刚才的 kubia Pods,这里我们创建一个 ClusterIP 类型的 service,它会在之前配置的 serviceSubnet 网络中暴露服务。

1
2
3
4
# 创建 service 对象
kubectl expose rc kubia --type=NodePort --name kubia
# 查看 service 对象
kubectl get service

通过上述命令,我们就会看到 kubernetes 为我们创建的 service ip(又名 CLUSTER-IP),您可以在集群中的任意机器中通过 curl <YOUR_SERVICE_IP>:8080 访问刚才创建的服务,kubernetes 会自动帮我们做负载均衡,将请求转发到不同的 Pod 中。

您可能已经注意到了,这时候的 service ip 仍然是只能集群内访问的虚拟 ip,它只是能保证无论 pod 何时崩溃,我们都能通过恒定不变的 service ip 访问其他可用的 kubia 服务。那么如何让集群之外的机器也能访问 kubia 服务呢?有一种方法是创建 LoadBalance 类型的 service,这需要云服务提供商开放一定的 LoadBalance 服务,让 Kubernetes 集群可以自主创建和修改 VIP。不过我所使用的云服务并不提供该功能,所以我手动创建了 VIP,并将 VIP 的 target 指向了所有的节点(master 和 worker),因为前面创建 service 时,使用的 service 类型是 NodePort,Kubernetes 会在所有节点上找一个相同的端口暴露服务,我在手动创建 VIP 时就指定了该端口。

到此为止,我们就构建出了一个稳定可靠的服务集群,并且外部网络就能访问我们的服务。现在让我们来创造更多魔法。

使用 Kubernetes 的一个主要好处是可以简单地扩展部署。让我们看看扩容 pod 有多容易。

1
2
# 将 kubia 服务扩容为 6 个 pod
kubectl scale rc kubia --replicas=6

现在已经告诉 Kubernetes 需要确保 pod 始终有六个实例在运行。注意,你没有告诉 Kubernetes 需要采取什么行动,也没有告诉 Kubernetes 增加三个 pod, 只设置新的期望的实例数量并让 Kubernetes 决定需要采取哪些操作来实现期望的状态。

这是 Kubernetes 最基本的原则之一。不是告诉 Kubernetes 应该执行什么操作,而是声明性地改变系统的期望状态,并让 Kubernetes 检查当前的状态是否与期望的状态一致,如果不一致的话,它会自动的进行处理。在整个 Kubernetes 世界中都是这样的。

我们可以查看一下现在的 kubia 服务是否如我们所愿,变成了六个 pod。

1
2
3
4
# 确认 ReplicationController 的状态
kubectl get rc
# 查看 pod 您可以看到每个 pod 都运行在哪个工作节点上
kubectl get pod -o wide

在试玩环节的最后,让我们看看探索 Kubernetes 集群的另一种方式。到目前为止,我们只使用了 kubectl 命令行工具。实际上 Kubernetes 也提供了一个图形化的 web 用户界面,接下来我们就使用一下它,首先你可以通过如下命令启动 dashboard 功能。

1
2
3
4
# 启动 dashboard 服务
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml
# 打开 dashboard 服务代理,这样我们就可以从本地访问 dashboard 服务
kubectl proxy

成功执行上述命令后,您就可以打开 本地 Dashboard 页面
dashboard-login
看来是要登录,那么怎么获取登录的信息呢?您可以通过如下命令创建账号并获取token:

1
2
3
4
5
6
7
8
# 创建账号
kubectl create serviceaccount cluster-admin-dashboard-sa
# 赋予权限
kubectl create clusterrolebinding cluster-admin-dashboard-sa \
--clusterrole=cluster-admin \
--serviceaccount=default:cluster-admin-dashboard-sa
# 获取 token
kubectl describe secret $(kubectl get secret | grep cluster-admin-dashboard-sa | awk '{print $1}')

将上述命令获取的 token 输入到登录页面,就能访问 dashboard 的主页面啦!
dashboard-index

参考内容

[1] kubernetes GitHub 仓库
[2] Kubernetes 官方主页
[3] Kubernetes 官方 Demo
[4] 《Kubernetes in Action》
[5] 理解Kubernetes网络之Flannel网络
[6] Kubernetes Handbook
[7] iptables概念介绍及相关操作
[8] iptables超全详解
[9] 理解Docker容器网络之Linux Network Namespace
[10] A Guide to the Kubernetes Networking Model
[11] Kubernetes with Flannel — Understanding the Networking
[12] 四层、七层负载均衡的区别

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/container/kubernetes/introduction/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。