集群搭建

Kubernetes本质

"容器",实际上是一个由Linux Namespace、Linux Cgroups和rootfs三种技术构建出来的进程的隔离环境。

一个正在运行的容器可以被“一分为二”看待:

  1. 一组联合挂载在/var/lib/docker/aufs/mnt上的rootfs,这部分称为“容器镜像”,是容器的静态视图

  2. 一个由Namespace+Cgroups构成的隔离环境,这部分称为“容器运行时”,是容器的动态视图

作为一个开发者,我们并不关心容器运行时的差异,因为,在整个“开发->测试->发布”的流程中,真正承载着容器信息进行传递的,是容器镜像,而不是运行时。

这也正是在Docker项目成功后,迅速走向“容器编排”这个“上层建筑”的主要原因。

  1. 作为一家云服务商或者基础设施提供商,只要能够将用户提交的Docker镜像以容器的方式运行起来,就能够成为容器生态圈上的一个承载点,从而将整个容器技术栈上的价值,沉淀在这个节点上

  2. 从这个承载点向Docker镜像制作者和使用者方向回溯,整条路径上的各个服务节点,比如CI/CD、监控、安全、网络、存储等,都有可以发挥和盈利的余地

这个逻辑正是所有云计算提供商如此热衷容器技术的重要原因:通过容器镜像,它们可以和潜在用户(开发者)直接关联起来。

  • 从单一容器到容器集群,容器技术实现了从“容器”到“容器云”的飞跃,标志着它真正得到了市场和生态的认可

  • 容器从开发者手中一个小工具,成为了云计算领域的主角,能够定义容器组织和管理规范的“容器编排”技术,则坐上了容器技术的“头把交椅”

容器编排工具:

  1. Compose+Swarm (Docker)

  2. Kubernetes (Google + RedHat)

谷歌公开发表的基础设施体系The Google Stack

Kubernetes架构

Kubernetes要解决的问题是编排?调度?容器云?集群管理?

  • 这个问题目前没有固定的答案,不同阶段,Kubernetes重点解决的问题不同

  • 但是对于用户来说,希望Kubernetes帮助我们把容器镜像在一个给定的集群上运行起来(希望Kubernetes提供路由网关、水平扩展、监控、备份、灾难恢复等)

以上功能,Docker的(Compose+Swarm)或者传统的PaaS就能做到,因此Kubernetes的核心定位不止于此,全局架构如下:

Kubernetes由Master和Node两种节点组成,分别对应控制节点和计算节点。

控制节点(Master)

出发点:如何编排、管理、调度用户提交的作业。由三个密切协作的独立组件组合而成:

  1. 负责API服务的Kube-apiserver:用来处理 API 操作的,Kubernetes 中所有的组件都会和 API Server 进行连接,组件与组件之间一般不进行独立的连接,都依赖于 API Server 进行消息的传送。

  2. 负责调度的kube-scheduler:是调度器,完成调度的操作,比如,把一个用户提交的 Container,依据它对 CPU、对 memory 请求大小,找一台合适的节点,进行放置。

  3. 负责容器编排的kube-controller-manager:是控制器,它用来完成对集群状态的一些管理。比如,自动对容器进行修复、自动进行水平扩张。

  4. 负责整个集群的持久化数据etcd:是一个分布式的存储系统,API Server 中所需要的这些原信息都被放置在 etcd 中,etcd 本身是一个高可用系统,通过 etcd 保证整个 Kubernetes 的 Master 组件的高可用性。

API Server在部署结构上是一个可以水平扩展的一个部署组件;Controller和Scheduler是可以进行热备的一个部署组件,只有一个 active。

计算节点(Node)

  • kubelet,与容器运行时(比如Docker项目)交互,这个交互所依赖的是CRI(Container Runtime Interface)的远程调用接口。这个接口定义了容器运行时各项核心操作,比如:启动一个容器需要的所有参数。具体的容器运行时,比如Docker一般通过OCI规范与底层的Linux操作系统进行交互。也就是将CRI请求翻译成对Linux操作系统的调用(操作Linux Namespace和Cgroups等)。

  • kubelet通过gRPC协议与Device Plugin插件交互。这个插件是Kubernetes用来管理GPU等宿主机物理设备的主要组件,这个插件是基于Kubernetes项目进行机器学习训练,高性能作业支持等工作必须关注的功能。

  • kubelet调用网络插件和存储插件为容器配置网络和持久化存储。这两个插件与kubelet交互的接口:CNI(Container Networking Interface)和CSI(Container Storage Interface)

kubernetes项目并不关心部署的是什么容器运行时,使用的什么技术实现,只要容器运行时能够运行标准的容器,就可以通过实现CRI接入到Kubernetes中。Kubernetes项目着重要解决的问题是:运行在大规模集群中的各种任务之间,实际上存在着各种各样的关系,这些关系的处理,才是作业编排和管理系统最困难的地方。

如何处理这些关系?利用Docker Swarm和Compose来处理一些简单依赖关系

比如,在Compose项目中,可以为两个容器定义一个“link”,Docker项目负责维护这个“link”关系,具体的做法,将两个容器相互访问所需要的IP地址,端口号等信息以环境变量的形式注入,供应用进程使用:

DB_NAME=/web/db
DB_PORT=tcp://172.17.0.5:5432
DB_PORT_5432_TCP=tcp://172.17.0.5:5432
DB_PORT_5432_TCP_PROTO=tcp
DB_PORT_5432_TCP_PORT=5432
DB_PORT_5432_TCP_ADDR=172.17.0.5

当容器发生变化时(如镜像更新或者被迁移到其他宿主机),这些环境变量的值会由Docker项目自动更新。简单的依赖关系,使用以上方法没有问题,但是如果要将所有的依赖关系都处理好,link这种简单的方式就不行了。所以,Kubernetes项目最主要的设计思想是:从更宏观的角度,以统一的方式来定义任务之间的各种关系,并且为将来支持更多种类的关系保留余地

启动一个Pod为例,看Kubernetes架构中这些组件是如何进行交互的,如下图所示:

  1. 通过UI或者CLI提交一个Pod给Kubernetes进行部署,这个Pod请求首先会通过UI或者CLI提交给Kubernetes API Server

  2. API Server把这个信息写入分布式存储系统etcd中

  3. Scheduler通过API Server的Watch(Notification)机制,得到有一个Pod需要调度的信息

  4. Scheduler根据Pod需要的资源(CPU/Mem等)进行一次调度决策,完成决策后向API Server报告该Pod被调度的节点信息

  5. API Server接收到报告信息后,把结果写入到etcd中,然后通过相应节点进行Pod真正的执行启动

  6. 相应节点的Kubelet得到这个通知,调用Container runtime来真正启动配置这个容器和需要的运行环境,调用Storage Plugin和Network Plugin配置存储和网络

Kubernetes对容器常见的“访问”进行了分类

  • 常见的“紧密交互”关系:应用之间需要非常频繁的交互和访问或者通过本地文件进行信息交换。常规环境下,这些应用会被部署在同一台服务器,通过localhost通信,通过本地磁盘交换文件。在Kubernetes中,这些容器会被划分为一个Pod,Pod中的容器共享同一个Network Namespace同一组数据卷,从而达到高效交换信息的目的。

  • 常规需求,如web服务和数据库之间的访问关系;kubernetes提供了一种叫“Service”的服务。像这样的两个应用,往往故意部署在不同的机器上,从而提高容灾能力。但是对于一个容器来说IP地址是不固定的,那么Web怎么找到数据库容器对应的Pod呢?kubernetes通过给Pod绑定Service,而Service声明的IP地址始终不变。 这个Service主要作用是作为Pod的代理入口,从而代替Pod对外暴露一个固定的网络地址。

这样,Web应用只需要关心数据库Pod的Service的信息,Servie后端真正代理的Pod的IP地址、端口等信息的自动更新、维护,则是kuKubernetes项目的职责。

围绕Pod为核心,构建出Kubernetes项目的核心功能“全景图”:

  • 不同Pod之间不仅有访问关系,还要求发起时加上授权信息。那么如何实现?使用Secret对象,它其实是一个保存在Etcd里的键值对数据。把授权信息以Secret的方式存在Etcd里,Kubernetes会在指定的Pod启动时,自动把Secret里的数据以Volume的方式挂载到容器里。这样就可以使用授权信息进行容器之间的访问。

容器的运行形态

Kubernetes将容器运行形态抽象为Pod,并基于Pod对象抽象出:

  • Job:用来描述一次性运行的Pod(比如,大数据任务)

  • CronJob:用于描述定时任务

  • DaemonSet:用来描述每个宿主机上必须且只能运行一个副本的守护进程服务

Kubernetes推崇的做法:通过一个“编排对象”,如Pod、Job等,来描述你试图管理的应用。再定义一些“服务对象”,如Service,Secret,Horizontal Pod Autoscaler等,这些对象会负责具体的平台级功能。这就是所谓的声明式API,这些API对应的“编排对象”和“服务对象”,都是kubernetes项目中的API对象。

  • 过去很多集群管理项目(Yarn、Mesos、Swarm)所擅长的是把一个容器,按照某种规则,放置在某个最佳节点上运行起来,这种功能称为“调度”。

  • Kubernetes擅长的是按照用户意愿和整个系统的规则,完全自动化地处理好容器之间的各种关系,这种功能称为“编排”。

Kubernetes不仅提供了一个编排工具,更重要的是提供了一套基于容器构建分布式系统的基础依赖:Linux容器相关的技术可以帮助我们快速定位问题,并解决问题,要真正发挥容器技术的实力的关键在于如何使用这些技术“容器化”应用。单单通过Docker把一个应用的镜像跑起来,并没有什么用。关键是处理好容器之间的编排关系。比如:

  • 主从容器如何区分?

  • 容器之间的自动发现和通信如何完成?

  • 容器的持久化数据如何保持?

部署Kubernetes

主流云厂商使用SaltStackAnsible等运维工具自动化地执行安装脚本和配置文件。但是,这些工具的学习成本比kubernetes项目还高。社区开发了一个独立部署的工具:kubeadm,执行以下两条命令就可以部署一个集群:

# 创建一个 Master 节点
kubeadm init

# 将一个 Node 节点加入到当前集群中
kubeadm join <Master节点的IP和端口>

Kubeadm原理

  1. 传统部署方式:在部署Kubernetes时,它的每一个组件都是一个需要被执行的、单独的二进制文件。使用SaltStack这样的运维工具或者社区维护的脚本,就需要把这些二进制文件传输到指定的节点上,然后编写控制脚本来启停这些组件

  2. 容器化部署方式:给每个组件做一个容器镜像,然后在每台宿主机上运行docker run命令来启动这些组件容器(存在一个问题,如何容器化kubelet?)

Kubelet是Kubernetes项目用来操作Docker等容器运行时的核心组件,除了和容器运行时打交道之外,kubelet在配置容器网络管理容器数据卷都需要直接操作宿主机。如果kubelet本身就运行在一个容器中,那么直接操作宿主机就会变得很麻烦

  • 对于配置网络:kubelet容器可以通过不开启Network Namespace(即Docker的host network模式)的方式,直接共享宿主机的网络栈

  • 对于操作文件系统:让kubelet隔着容器的Mount Namespace和文件系统,操作宿主机的文件系统,就有点难了

举个例子:用户想要使用NFS做容器的持久化数据卷,那么kubelet就需要在容器进行绑定挂载前,在宿主机的指定目录上,先挂载NFS的远程目录。那么问题来了,由于现在kubelet是运行在容器里的,这就意味着它要做的这个mount -F nfs命令,被隔离在了一个单独的Mount Namespace中,即kubelet做的挂载操作,不能被“传播”到宿主机上。

妥协的方案就是,kubelet直接运行在宿主机上,然后使用容器部署其他的kubernetes组件。

kubeadm步骤

使用kubeadm的第一步,在机器上手动安装 kubeadmkubeletkubectl 这3个二进制文件。kubeadm已经为各个发行版的Linux准备好了安装包,所以只需要执行如下命令:

apt-get install kubeadm   #Debain or Ubuntu
yum install kubeadm       #Redhat or Centos

kubeadm  init   #部署Master 节点

第一步:Preflight Checks

在kubernetes项目中,主机名以及一切存储在Etcd中的API对象,都必须使用标准的DNS命名RFC1123

确定服务器是否可以用来部署kubernetes,主要包括:

  1. Linux内核的版本必须是否是3.10以上

  2. Linux Cgroup模块是否可用

  3. 服务器的hostname是否标准

  4. 安装的kubeadm和kubelet的版本是否匹配

  5. 服务器上是否已经安装了Kubernetes的二进制文件

  6. Kubernetes的工作端口10250/10251/10252端口是不是已经被占用

  7. ip、mount等Linux指令是否存在

  8. Docker是否已经安装

  9. 。。。。。。

第二步:生成证书

当通过了Preflight Checks后,kubeadm会生成Kubernetes对外提供服务所需的各种证书和对应的目录。Kubernetes对外提供服务时,除非专门开启“不安全模式”,否则都需要通过HTTPS才能访问kube-apiserver,这就需要为Kubernetes集群配置好证书文件。证书存放在Master节点的/etc/kubernetes/pki目录下,其中最主要的证书是ca.crt和对应的私钥ca.key

用户使用kubectl获取容器日志等streaming操作时,需要通过kube-apiserver向kubelet发起请求,这个链接也必须是安全的。kubeadm为上述操作生成的是apiserver-kubelet-client.crt文件,对应的私钥是apiserver-kubelet-client.key

其他的如Aggregate APIServer等特性,也需要生成专门的证书,同时也可以选择不让kubeadm生成证书,而是拷贝现成的证书到指定的目录中/etc/kubernetes/pki/ca.{crt,key}。那么,此时kubeadm会跳过生成证书的步骤。

第三步:生成conf文件

证书生成后,kubeadm接下来会为其他组件生成访问kube-apiserver所需的配置文件。配置文件的路径是:/etc/kubernetes/xxx.conf

ls /etc/kubernetes/
admin.conf controller-manager.conf kubelet.conf scheduler.conf

这些文件里记录的是当前这个Master节点的 服务器地址监听端口证书目录 等信息。这样,对应的客户端(如scheduler、kubelet等),可以直接加载相应的文件,使用里面的信息与kube-apiserver建立安全连接。

第四步:生成YAML文件

kubeadm为Master组件(kube-apiserver、kube-controller-manager、kube-scheduler)生成YAML文件,它们都以Pod的方式部署起来。

在kubernetes中,有一种特殊的容器启动方法叫 “Static Pod” 。它允许你把要部署的Pod的YAML文件放在一个指定的目录里。这样,当这台服务器上的kubelet启动时,它会自动检查这个目录,加载所有Pod的YAML文件,然后在这台服务器上启动它们。

在kubeadm中,Master组件的YAML文件会被生成在/etc/kubernetes/manifests路径下。如果需要修改已有集群的kubernetes组件的配置,需要修改对应的YAML文件。同样通过 Static Pod 的方式启动Etcd,所以,Master组件的Pod文如下:

ls /etc/kubernetes/manifests/
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml

一旦这些文件出现在被kubelet监视的/etc/kubernetes/manifests目录下,kubelet就会自动创建这些YAML文件中定义的Pod(即Master组件的容器)。

  1. Master组件的容器启动后,kubeadm会通过检查localhost:6443/healthz这个Master组件的健康检查URL,等待Master组件完全运行起来

  2. kubeadm为集群生成bootstrap token,持有这个token的任何一个kubelet和kubeadm节点,都可以通过kubeadm join加入到这个集群中(token的值和使用方法会在kubeadm init结束后打印出来)

  3. token生成后,kubeadm会将ca.crt等Master节点的重要信息,通过ConfigMap的方式保存在Etcd中,供后续部署Node节点使用(这个ConfigMap的名字 cluster-info)

  4. kubernetes默认kube-proxy和DNS这两个插件是必须安装的,提供集群的服务发现DNS功能,这两个插件也是两个容器镜像,创建两个Pod即可

第五步:kubeadm join

使用kubeadm init生成的bootstrap token在安装了kubeadm和kubelet的服务器上执行kubeadm join,bootstrap token的作用:

  1. 一台服务器想要成为kubernetes集群中的节点,就必须在集群的kube-apiserver上注册

  2. 想要与apiserver通信,这台服务器必须获取相应的证书文件(CA文件)

  3. 为了一键安装,就不能手动去拷贝证书文件

  4. kubeadm至少要发起一次“不安全模式”的访问到kube-apiserver,从而拿到保存在ConfigMap中的cluster-info(这里保存了APIServer的授权信息)

bootstrap token扮演的就是这个过程中的安全验证的角色,有了cluster-info里的kube-apiserver的地址、端口、证书,kubelet就可以“安全模式”连接到apiserver上

配置kubeadm参数

可以通过 --config 参数指定启动时读取的配置文件:

kubeadm init --config kubeadm.yaml

这样可以给kubeadm提供一个YAML文件,例如:

apiVersion: kubeadm.k8s.io/v1alpha2
kind: MasterConfiguration
kubernetesVersion: v1.11.0
api:
    advertiseAddress: 192.168.0.102
    bindPort: 6443
    ...
etcd:
    local:
        dataDir: /var/lib/etcd
        image: ""
imageRepository: k8s.gcr.io
kubeProxy:
    config:
        bindAddress: 0.0.0.0
        ...
kubeletConfiguration:
    baseConfig:
        address: 0.0.0.0
        ...
networking:
    dnsDomain: cluster.local
    podSubnet: ""
    serviceSubnet: 10.96.0.0/12
nodeRegistration:
    criSocket: /var/run/dockershim.sock
    ...

通过指定这样一个配置文件,可以方便地在文件里填写各种自定义的部署参数。

比如,要自动化kube-apiserver的参数,添加如下信息:

...
apiServerExtraArgs:
    advertise-address: 192.168.0.103
    anonymous-auth: false
    enable-admission-plugins: AlwaysPullImages,DefaultStorageClass
    audit-log-path: /home/johndoe/audit.log

然后,kubeadm就会使用上面的信息替换/etc/kubernetes/manifests/kube-apiserver.yaml里的command字段里的参数。

更具体的:

  1. 修改kubelet的配置

  2. 修改kube-proxy的配置

  3. 修改kubernetes使用的基础镜像的URL(默认的k8s.gcr.io/xxx镜像URL在国内不能访问)

  4. 指定自己的证书文件

  5. 指定特殊的容器运行时

kubeadm的源代码在kubernetes/cmd/kubeadm目录下,其中app/phases文件夹下的代码就是上述的步骤。

生产环境部署

部署规模化的生产环境,推荐使用:

制作证书的方法有:

  1. CFSSL

  2. OpenSSL

  3. easyrsa

  4. GnuGPG

  5. keybase

实际动手部署一个集群

google镜像下载地址

google-image

安装前检查

  • Linux操作系统:Centos 7

  • 2GB以上内存、2核以上CPU

  • 集群之间网络互通

  • 每个节点需要有唯一的hostname、MAC地址和produc_uuid

  • 打开特定的端口

  • 禁用swap(必须禁用swap才能使kubelet正常工作

# 获取网络接口的MAC地址
ip link
ifconfig -a

# 查看product_uuid
sudo cat /sys/class/dmi/id/product_uuid

某些虚拟机可能有相同的值,但是硬件设备具有唯一的值,Kubernetes使用这些值来唯一标识集群中的节点,如果这些值对于每个节点都不是唯一的,会导致安装失败。

如果有多个网络适配器,并且在默认路由上无法访问的Kubernetes组件,建议添加IP路由,以便通过适当的适配器访问Kubernetes群集。

Master

协议

方向

端口范围

目的

使用者

TCP

入站

6443*

Kubernetes API server

All

TCP

入站

2379-2380

etcd server client API

kube-apiserver, etcd

TCP

入站

10250

Kubelet API

Self, Control plane

TCP

入站

10251

kube-scheduler

Self

TCP

入站

10252

kube-controller-manager

Self

*标记的端口可以自定义为其他端口。etcd也可以使用集群外的集群或自定义的其他端口。

Worker

协议

方向

端口范围

目的

使用者

TCP

入站

10250

Kubelet API

Self, Control plane

TCP

入站

30000-32767

NodePort Services**

All

**标记的端口是对外提供服务是的Service的默认端口范围。Pod的网络插件也需要使用特定的端口。

安装运行时环境

kubeadm将尝试通过扫描已知的域套接字列表来自动检测Linux节点上的容器运行时,可以在下表中找到所使用的可检测运行时和套接字路径。

Runtime

Domain Socket

Docker

/var/run/docker.sock

containerd

/run/containerd/containerd.sock

CRI-O

/var/run/crio/crio.sock

如果同时检测到Dockercontainerd,则Docker优先。这是必需的,因为Docker 18.09附带了containerd,两者都是可检测的,如果检测到任何其他两个或更多运行时,kubeadm将退出并显示相应的错误消息。如果选择的容器运行时是Docker,则通过kubelet内置的dockershim CRI实现使用它。

root用户或者在增加命令前缀sudo

Docker

# Install Docker CE
## Set up the repository
### Install required packages.
yum install yum-utils device-mapper-persistent-data lvm2

### Add Docker repository.
yum-config-manager \
  --add-repo \
  https://download.docker.com/linux/centos/docker-ce.repo

## Install Docker CE.
yum update && yum install docker-ce-18.06.2.ce

## Create /etc/docker directory.
mkdir /etc/docker

# Setup daemon.
cat > /etc/docker/daemon.json <<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"], #这个参数修改默认的Cgroups管理器
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ]
}
EOF

mkdir -p /etc/systemd/system/docker.service.d

# Restart Docker
systemctl daemon-reload
systemctl restart docker

更多安装细节参考

Kubeadm、Kubelet、kubectl

每个节点都需要安装:

  • kubeadm:用于安装集群

  • kubelet:在群集中的所有计算机上运行的组件,并执行诸如启动Pod和容器之类的操作

  • kubectl:与集群通信的命令行工具

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
exclude=kube*
EOF

# Set SELinux in permissive mode (effectively disabling it)
# 关闭 SeLinux 使得容器能够访问宿主机文件系统(例如容器网络)
setenforce 0
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

systemctl enable --now kubelet

# 由于iptables被绕过而导致流量路由不正确的问题
# 确保在`sysctl`中将`net.bridge.bridge-nf-call-iptables`设置为1
cat <<EOF >  /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system

# 确保`br_netfilter`模块已经加载
lsmod | grep br_netfilter   # 查看模块是否加载
modprobe br_netfilter       # 加载模块

完成以上设置后,kubelet进入一个crashloop中(每隔几秒重新启动一次)等待kubeadm告诉它该怎么做。

配置主节点的kubelet的Cgroup驱动

当使用Docker作为容器运行时,kubeadm会自动检测cgroup驱动,并且在运行时期间将其设置在/var/lib/kubelet/kubeadm-flags.env文件中。

如果使用其他CRI,需要修改/etc/default/kubelet文件中cgroup-driver的值,例如:KUBELET_EXTRA_ARGS=--cgroup-driver=<value>

kubeadm init和kubeadm join将使用此文件为kubelet提供额外的用户定义参数。

请注意,如果CRI的cgroup驱动程序不是cgroupfs,才需要进行修改,因为这已经是kubelet中的默认值。

修改完成之后,需要重启kubelet:

systemctl daemon-reload
systemctl restart kubelet

部署高可用集群

为kube-apiserver部署负载均衡器

  1. 创建一个名称可解析为DNS的kube-apiserver负载均衡器

    • 负载均衡器必须能够通过apiserver端口与所有控制平面节点通信,还必须允许其监听端口上的传入流量

    • HAProxy可以作为一个负载均衡器

    • 确保负载均衡器的地址始终与kubeadm的Control Plane Endpoint的地址匹配

  2. 添加第一个控制平面节点到负载均衡器中,并测试通信

     nc -v LOAD_BALANCER_IP PORT

    由于apiserver尚未运行,因此预计会出现连接拒绝错误。但是,超时意味着负载均衡器无法与控制平面节点通信。如果发生超时,请重新配置负载平衡器以与控制平面节点通信。

  3. 将剩余的控制平面节点添加到负载均衡器目标组

安装keepalived & HAProxy

通过keepalived + haproxy实现的,其中:

  • keepalived是提供一个VIP,通过VIP关联所有的Master节点

  • 然后haproxy提供端口转发功能

由于VIP在Master的机器上,默认配置API Server的端口是6443,所以需要将另外一个端口关联到这个VIP上,一般用8443。如下图所示:

  1. 在Master手工安装keepalived, haproxy

     yum install keepalived
     yum install haproxy
  2. 修改HAProxy的配置文件

    配置文件是:haproxy.cfg,默认路径是/etc/haproxy/haproxy.cfg,同时需要手动创建/run/haproxy目录,否则haproxy会启动失败。

    注意:

    • bind绑定的就是VIP对外的端口号,这里是8443

    • balance指定的负载均衡方式是roundrobin方式

    • server指定的就是实际的Master节点地址以及真正工作的端口号,这里是6443,有多少台Master就写多少条记录

      # haproxy.cfg sample
      global
        log /dev/log    local0
        log /dev/log    local1 notice
        chroot /var/lib/haproxy
        stats socket /var/run/haproxy-admin.sock mode 660 level admin
        stats timeout 30s
        user haproxy
        group haproxy
        daemon
        nbproc 1
      
      defaults
            log     global
            timeout connect 5000
            timeout client  50000
            timeout server  50000
      
      listen  admin_stats
        bind 0.0.0.0:10080
        mode http
        log 127.0.0.1 local0 err
        stats refresh 30s
        stats uri /status
        stats realm welcome login\ Haproxy
        stats auth admin:123456
        stats hide-version
        stats admin if TRUE
      
      listen kube-master
            bind 0.0.0.0:8443
            mode tcp
            option tcplog
            balance roundrobin
            server tuo-1 172.18.52.34:6443  check inter 10000 fall 2 rise 2 weight 1
            server tuo-2 172.18.52.33:6443  check inter 10000 fall 2 rise 2 weight 1
            server tuo-3 172.18.52.32:6443  check inter 10000 fall 2 rise 2 weight 1
  3. 修改keepalived的配置文件

    修改keepalived的配置文件,配置正确的VIP,keepalived的配置文件keepalived.conf的默认路径是/etc/keepalived/keepalived.conf

    注意:

    • priority决定Master的主次,数字越小优先级越高

    • virtual_router_id决定当前VIP的路由号,实际上VIP提供了一个虚拟的路由功能,该VIP在同一个子网内必须是唯一

    • virtual_ipaddress提供的就是VIP的地址,该地址在子网内必须是空闲未必分配的

      # keepalived.cfg sample(Master)
      
      global_defs {
        router_id K8s_Master
      }
      
      vrrp_script check_haproxy {
        script "killall -0 haproxy"
        interval 3
        weight -2
        fall 10
        rise 2
      }
      
      vrrp_instance VI-kube-master {
        state MASTER
        interface eno16777728
        priority 150
        virtual_router_id 51
        advert_int 3
        authentication {
            auth_type PASS
            auth_pass transwarp
        }
      
        virtual_ipaddress {
            172.18.52.33
        }
      
        track_script {
            check_haproxy
        }
      }
      
      # keepalived.cfg sample(Backup)
      
      global_defs {
        router_id K8s_Backup_1
      }
      
      vrrp_script check_haproxy {
        script "killall -0 haproxy"
        interval 3
        weight -2
        fall 10
        rise 2
      }
      
      vrrp_instance VI-kube-master {
        state BACKUP
        interface eno16777728
        priority 140
        virtual_router_id 51
        advert_int 3
        authentication {
            auth_type PASS
            auth_pass transwarp
        }
      
        virtual_ipaddress {
            172.18.52.33
        }
      
        track_script {
            check_haproxy
        }
      }
  4. 优先启动主Master的keepalived和haproxy

     systemctl enable keepalived
     systemctl start keepalived
     systemctl enable haproxy
     systemctl start haproxy
  5. 检查keepalived是否启动成功

ip a s
# 查看是否有VIP地址分配
# 如果看到VIP地址已经成功分配在eth0网卡上,说明keepalived启动成功
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 00:50:56:a9:d5:be brd ff:ff:ff:ff:ff:ff
    inet 10.86.13.32/23 brd 10.86.13.255 scope global eth0
       valid_lft forever preferred_lft forever
    **inet 10.86.13.36/32 scope global eth0**
       valid_lft forever preferred_lft forever
    inet6 fe80::250:56ff:fea9:d5be/64 scope link
       valid_lft forever preferred_lft forever

# 更保险的方法是查看keepalived的状态和HAProxy的状态
systemctl status keepalived -l
● keepalived.service - LVS and VRRP High Availability Monitor
   Loaded: loaded (/usr/lib/systemd/system/keepalived.service; enabled; vendor preset: disabled)
   Active: active (running) since Thu 2018-02-01 10:24:51 CST; 1 months 16 days ago
 Main PID: 13448 (keepalived)
   Memory: 6.0M
   CGroup: /system.slice/keepalived.service
           ├─13448 /usr/sbin/keepalived -D
           ├─13449 /usr/sbin/keepalived -D
           └─13450 /usr/sbin/keepalived -D

Mar 20 04:51:15 kube32 Keepalived_vrrp[13450]: VRRP_Instance(VI-kube-master) Dropping received VRRP packet...
**Mar 20 04:51:18 kube32 Keepalived_vrrp[13450]: (VI-kube-master): ip address associated with VRID 51 not present in MASTER advert : 10.86.13.36
Mar 20 04:51:18 kube32 Keepalived_vrrp[13450]: bogus VRRP packet received on eth0 !!!**

systemctl status haproxy -l
● haproxy.service - HAProxy Load Balancer
   Loaded: loaded (/usr/lib/systemd/system/haproxy.service; enabled; vendor preset: disabled)
   Active: active (running) since Thu 2018-02-01 10:33:22 CST; 1 months 16 days ago
 Main PID: 15116 (haproxy-systemd)
   Memory: 3.2M
   CGroup: /system.slice/haproxy.service
           ├─15116 /usr/sbin/haproxy-systemd-wrapper -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid
           ├─15117 /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -Ds
           └─15118 /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -Ds

# 查看kubernetes集群信息
kubectl version
**Client Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.1", \
GitCommit:"3a1c9449a956b6026f075fa3134ff92f7d55f812", GitTreeState:"clean", \
BuildDate:"2018-01-03T22:31:01Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"linux/amd64"}

Server Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.1", \
GitCommit:"3a1c9449a956b6026f075fa3134ff92f7d55f812", GitTreeState:"clean", \
BuildDate:"2018-01-03T22:18:41Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"linux/amd64"}**

此时,说明keepalived和haproxy都是成功,可以依次将其他Master节点的keepalived和haproxy启动。

此时,通过ip a s命令去查看其中一台非主Master时看不到VIP,因为VIP永远只在主Master节点上,只有当主Master节点挂掉后,才会切换到其他Master节点上。

主Master获取VIP是需要时间的,如果多个Master同时启动,会导致冲突。最稳妥的方式是先启动一台主Master,等VIP确定后再启动其他Master。

部署堆叠集群

部署第一个控制平面节点

  1. 在第一个控制平面节点上创建kubeadm-config.yaml文件:

     apiVersion: kubeadm.k8s.io/v1beta1
     kind: ClusterConfiguration
     kubernetesVersion: stable   # 应该设置为使用的版本,例如stable
     controlPlaneEndpoint: "LOAD_BALANCER_DNS:LOAD_BALANCER_PORT"       # 应匹配负载均衡器的地址(或DNS)和端口
     ClusterConfiguration:
       networking:
         podSubnet: 192.168.0.0/16

    建议kubeadm,kubelet,kubectl和Kubernetes的版本匹配

    注意,一些CNI网络插件,需要CIDR,如192.168.0.0/16,但是有些不需要。在ClusterConfiguration配置项的networking对象中设置podSubnet:192.168.0.0/16字段来为Pod设置CIDR。

  2. 初始化控制平台

     sudo kubeadm init --config=kubeadm-config.yaml --experimental-upload-certs
    
     # --experimental-upload-certs参数用于将需要在所有控制平面节点之间共享的证书上传到集群中
     # 删除这个参数,实现手动证书复制分发
    
     # 命令执行完成后,会看到如下信息
     ...
     You can now join any number of control-plane node by running the following command on each as a root:\
    
       kubeadm join 192.168.0.200:6443 --token 9vr73a.a8uxyaju799qwdjv --discovery-token-ca-cert-hash \
       sha256:7c2e69131a36ae2a042a339b33381c6d0d43887e2de83720eff5359e26aec866 \
       --experimental-control-plane --certificate-key f8902e114ef118304e561c3ecd4d0b543adc226b7a07f675f56564185ffe0c07
    
     Please note that the certificate-key gives access to cluster sensitive data, keep it secret!\
    
     As a safeguard, uploaded-certs will be deleted in 2 hours; If necessary, you can use kubeadm init phase upload-certs to reload certs afterward.
    
     Then you can join any number of worker nodes by running the following on each as root:\
    
       kubeadm join 192.168.0.200:6443 --token 9vr73a.a8uxyaju799qwdjv \
       --discovery-token-ca-cert-hash sha256:7c2e69131a36ae2a042a339b33381c6d0d43887e2de83720eff5359e26aec866
    
     # 将输出信息保存到文本中,添加控制平面节点和工作节点到集群时,需要使用
     # 在kubeadm init的时候使用了`experimental-upload-certs`参数后,主控制平面的证书被加密并上传到kubeadm-certs Secret中
    
     # 要重新上传证书并生成新的解密密钥,请在已加入群集的控制平面节点上使用以下命令:
     sudo kubeadm init phase upload-certs --experimental-upload-certs

    注意,kubeadm-certsSecret和解密秘钥的有效时间是两个小时

  3. 部署CNI插件

必须安装Pod网络插件,以便Pod可以相互通信,且每个集群只能安装一个Pod网络。

必须在任何应用程序之前部署网络。此外,CoreDNS将不会在安装网络之前启动。 kubeadm仅支持基于容器网络接口(CNI)的网络(并且不支持kubenet)

注意,Pod网络不能与任何主机网络网络重叠。如果网络插件的首选Pod网络与某些主机网络之间发生冲突,应该考虑一个合适的CIDR替换,并在kubeadm init期间使用--pod-network-cidr并在网络插件的YAML中替换它。

  • 安装Flannel时需要在kubeadm init中添加参数--pod-network-cidr=10.244.0.0/16

  • 设置/proc/sys/net/bridge-nf-call-iptables的值为1,将桥接的IPv4流量传递给iptables的链

  • 保防火墙规则允许参与覆盖网络的所有主机的UDP端口8285和8472流量

一旦Pod网络安装完成,CoreDNS就能正常运行,然后就可以开始添加其他节点。

添加其他控制平面节点

警告:只有在第一个节点完成初始化后,才能按顺序添加新的控制平面节点。

对于每一个控制平面节点,执行如下操作:

  1. 执行先前由第一个节点上的kubeadm init输出提供给您的join命令

sudo kubeadm join 192.168.0.200:6443 --token 9vr73a.a8uxyaju799qwdjv \
--discovery-token-ca-cert-hash sha256:7c2e69131a36ae2a042a339b33381c6d0d43887e2de83720eff5359e26aec866 \
--experimental-control-plane --certificate-key f8902e114ef118304e561c3ecd4d0b543adc226b7a07f675f56564185ffe0c07

# `--experimental-control-plane`参数是告诉kubeadm join创建一个新的控制平面节点
# ` --certificate-key`参数将导致控制平面证书从集群中下`kubeadm-certs`Secret中下载下来,并使用对应的秘钥进行解密

其余控制平面节点添加完成后,开始添加worker节点。

添加worker节点

可以使用先前存储的命令将工作节点连接到集群,作为kubeadm init命令的输出:

sudo kubeadm join 192.168.0.200:6443 --token 9vr73a.a8uxyaju799qwdjv \
--discovery-token-ca-cert-hash sha256:7c2e69131a36ae2a042a339b33381c6d0d43887e2de83720eff5359e26aec866

容器化应用

使用Kubernetes的必备技能:编写配置文件。这些配置文件可以是 YAML 或者 JSON 格式的,一般都是用YAML格式。Kubernetes不推荐直接使用命令行的方式运行容器,而是使用YAML文件的方式,即:把容器的定义、参数、配置都记录在一个YAML文件中,然后使用如下命令kubectl create -f <xxx.yaml>这么做的最大好处,有一个文件能记录Kubernetes到底运行了什么

YAML示例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

这个 YAML 文件,对应到 Kubernetes 中,就是一个 API Object(API 对象)。为这个对象的各个字段填好值并提交给Kubernetes之后,Kubernetes就会负责创建出这些对象所定义的容器或者其他类型的API 资源。可以看到,这个 YAML 文件中的 Kind 字段,指定了这个API对象的类型(Type),是一个Deployment

所谓 Deployment,是一个定义多副本应用(即多个副本 Pod)的对象,此外,Deployment 还负责在 Pod 定义发生变化时,对每个副本进行滚动更新(Rolling Update)。

这个 YAML 文件中:

  • 定义的 Pod 副本个数 (spec.replicas) 是:2

  • 定义了一个 Pod 模版(spec.template),这个模版描述了要创建的 Pod 的细节

  • 这个 Pod 里只有一个容器,这个容器的镜像(spec.containers.image)是nginx:1.7.9,这个容器监听端口(containerPort)是 80

API对象

Pod就是Kubernetes世界里的“应用”,而一个应用,可以由多个容器组成

像这样使用一种 API 对象(Deployment)管理另一种 API 对象(Pod)的方法,在 Kubernetes 中,叫作“控制器”模式(controller pattern)。

在上面的例子中,Deployment扮演的正是 Pod 的控制器的角色。

Metadata

这样的每一个 API 对象都有一个叫作 Metadata 的字段,这个字段就是 API 对象的“标识”,即元数据,它也是从 Kubernetes 里找到这个对象的主要依据,这其中最主要使用到的字段是Labels

Labels 就是一组 key-value 格式的标签。而像 Deployment 这样的控制器对象,就可以通过这个 Labels 字段从 Kubernetes 中过滤出它所关心的被控制对象。

比如,在上面这个 YAML 文件中,Deployment 会把所有正在运行的、携带“app:nginx”标签的Pod识别为被管理的对象,并确保这些 Pod 的总数严格等于两个。

Label Selector

过滤规则的定义,是在 Deployment 的“spec.selector.matchLabels”字段,一般称之为:Label Selector。

Annotations

在 Metadata中,还有一个与Labels格式、层级完全相同的字段叫Annotations,它专门用来携带 key-value 格式的内部信息。

内部信息,指的是对这些信息感兴趣的是Kubernetes组件本身而不是用户。所以大多数Annotations,都是在 Kubernetes 运行过程中,被自动加在这个 API 对象上。

一个 Kubernetes 的 API 对象的定义,大多可以分为 MetadataSpec 两个部分:

  • 前者存放的是这个对象的元数据,对所有 API 对象来说,这一部分的字段和格式基本上是一样的

  • 后者存放的是属于这个对象独有的定义,用来描述它所要表达的功能

运行API对象

# 创建API对象
kubectl create -f nginx-deployment.yaml

# 查看API对象,-l参数获取所有匹配标签的Pod
kubectl get pods -l app=nginx

# 查看一个 API 对象的细节
kubectl describe pod nginx

kubectl get 指令的作用,就是从 Kubernetes 里面获取(GET)指定的 API 对象。需要注意的是,在命令行中,所有 key-value格式的参数,都使用“=”而非“:”表示。

kubectl describe 命令返回的结果中,可以清楚地看到这个 Pod 的详细信息,比如它的 IP 地址等等。其中,有一个部分值得特别关注,就是Events(事件)。

在 Kubernetes 执行的过程中,对 API 对象的所有重要操作,都会被记录在这个对象的 Events里,并且显示在 kubectl describe 指令返回的结果中。这个部分正是我们将来进行 Debug 的重要依据。如果有异常发生,要第一时间查看这些 Events,往往可以看到非常详细的错误信息。

上述deployment中的Pod运行的是1.7.9的nginx容器,如何升级成1.8?只要修改刚才的YAML文件即可:

...
    spec:
      containers:
      - name: nginx
        image: nginx:1.8 # 这里从 1.7.9 修改为 1.8
        ports:
        - containerPort: 80

这样对YAML配置文件的本地修改就完成了,通过如下命令更新到kubernetes集群中:

kubectl replace -f nginx-deployment.yaml

# 1
kubectl create -f   file.yaml

# 2
kubectl replace -f  file.yaml

# 3
kubectl apply -f    file.yaml

# 上述的命令1和2可以用3替换掉,这也是kubernetes“声明式API”推荐的做法
  • 通过容器镜像,保证了应用本身在开发和部署环境里的一致性(当应用发生变化时,开发和运维可以依靠容器进行同步)

  • 通过YAML配置文件,保证了应用“部署参数”在开发和部署环境中的一致性(当应用部署参数发生变化时,开发和运维可以依靠YAML配置文件进行沟通)

挂载volume

在 Kubernetes 中,Volume 是属于 Pod 对象的一部分。所以,我们就需要修改这个 YAML 文件里的 template.spec 字段,如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.8
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: nginx-vol
      volumes:
      - name: nginx-vol
        emptyDir: {}

在 Deployment 的 Pod 模板部分添加了一个 volumes 字段,定义了这个 Pod 声明的所有 Volume。它的名字叫作 nginx-vol,类型是 emptyDir。

emptyDir类型,其实就等同于Docker的隐式Volume参数,即:不显式声明宿主机目录的Volume。所以,Kubernetes也会在宿主机上创建一个临时目录,这个目录将来就会被绑定挂载到容器所声明的 Volume 目录上。

Kubernetes 的 emptyDir类型,只是把Kubernetes创建的临时目录作为Volume的宿主机目录交给Docker,因为 Kubernetes 不想依赖 Docker 创建的 _data 目录。

而 Pod 中的容器,使用的是 volumeMounts 字段来声明自己要挂载哪个 Volume,并通过mountPath 字段来定义容器内的 Volume 目录,比如:/usr/share/nginx/html

当然,Kubernetes 也提供了显式的 Volume 定义,它叫做 hostPath。比如下面的这个 YAML 文件:

 ...
    volumes:
    - name: nginx-vol
      hostPath:
        path: /var/data

这样volume挂载的宿主机目录,就变成了/var/data

# 使用如下命令,进入到Pod中,即容器的Namespace中
kubectl exec -it nginx-deployment-5c678cfb6d-lg9lw -- /bin/bash
ls /usr/share/nginx/html

# 从Kubernetes集群中删除部署的Deployment的命令
kubectl delete -f nginx-deployment.yaml

最后更新于

这有帮助吗?