CH1 背景回顾:云原生大事记

初出茅庐

2013年的后端技术领域,最如日中天的技术当属于 Paas (Platform-as-a-service, 平台即服务) 项目。以Cloud Foundry 为代表的 Paas 项目备受欢迎的一个主要原因,在于它提供了一种“应用托管”的能力。

当时部署应用的主流做法,是租一批虚拟机,用脚本或者手动的方式在这些机器上部署应用。 但是在部署过程中经常会遇到云端虚拟机和本地环境不一致的问题,所以当时的云计算服务比的就是谁能更好地模拟本地服务器环境,这个问题的最佳解决方案就是 Paas 开源项目。例如,虚拟机创建好之后,开发者只需要一条命令就能把本地应用部署到云上:

cf push <myApplication>

其中最核心的组件,就是一套应用的打包和分发机制。用户把应用的可执行文件和启动脚本打进一个压缩包内,上传到云上Cloud Foundry 的存储中。接着 Cloud Foundry 会通过调度器选择一个可以运行这个应用的虚拟机,然后通知这个机器上的 Agent 下载应用压缩包并启动。

由于需要在一个虚拟机上启动来自多个不同用户的应用,Cloud Foundry 会调用操作系统的 Cgroups 和 Namespace 机制为每一个应用单独创建一个称为“沙盒”的隔离环境,然后在“沙盒”中启动这些应用进程,从而把多个用户的应用互不干涉地在虚拟机里批量地、自动地运行起来。这个隔离环境,实际上就是所谓的“容器”。

然而,就在此时,dotCloud 公司决定将自己的容器项目 Docker 开源,在短短几个月内就迅速崛起,取代了 Cloud Foundry 等一众 Paas 社区。Docker 实际上只是一个同样使用 Cgroups 和 Namespace 实现的“沙盒“,与前面提到的 Paas 没有太大区别,除了一小部分不同的功能—— Docker 镜像。

前面讲到,Paas 之所以能够帮助用户大规模地部署应用到集群中,是因为它提供了一个应用打包的功能,但这也成为了用户不断诟病的一个功能。一方面,对于不同的语言、不同的框架,以及应用更新的不同版本,用户都需要分别单独维护一个打好的包,十分混乱。另一方面,由于本地与服务器环境的差异,将应用部署到云上时,常常要进行许多配置的修改工作,才能正常运行起来。

而 Docker 镜像的创新,从根本上解决了这个问题。所谓Docker镜像,其实也是一个压缩包。但是这个压缩包里的内容,比 PaaS 的应用可执行文件 + 启停脚本的组合要丰富得多。实际上,大多数 Docker 镜像是直接由一个完整操作系统的所有文件和目录构成的,所以这个压缩包里的内容跟你本地开发和测试环境用的操作系统完全一样。

这样,只要拥有了 Docker 镜像,然后用某种技术创建一个“沙盒”,将镜像解压到“沙盒”中,就可以运行你的应用了。更重要的是,这个压缩包包含了完整的操作系统文件和目录,也就是包含了这个应用运行所需要的所有依赖,所以你可以先用这个压缩包在本地进行开发和测试,完成之后再上传到云端运行。在整个过程中,无论是本地还是云端,获得的都是一致的运行环境。

所以,你只需要提供下载好的操作系统文件与目录,然后使用它制作—个压缩包即可:

docker build <myImage>

一旦镜像制作完成,也只需要一行命令就能启动它:

docker run <myImage>

崭露头角

前面讲到,Docker 镜像这个功能的创新,解决了 Paas 中一个棘手的问题:打包应用。一时之间,“容器化”取代“ PaaS 化”成为了基础设施领域最炙手可热的关键词,一个以“容器”为中心的全新的云计算市场正呼之欲出。

此时,Docker 公司又做出了一个重大的决定:发布 Swarm 项目。

虽然 Docker 解决了应用打包的问题,但是归根结底,如何将应用部署到平台上才是最重要的问题,而 Swarm 项目则是为了这个目的。

群雄并起

虽然 Docker 项日备受追捧,但用户最终要部署的还是他们的网站、服务、数据库,甚至是云计算业务。这就意味着,只有那些能够为用户提供平台层能力的工具才会真正成为开发者关心和愿意付费的产品。而 Docker 项目这样一个只能用来创建和启停容器的小工具,最终只能充当这些平台项目的“幂后英雄”。

Swarm 项目作为一个整体对外提供集群管理功能,其最大亮点是它完全使用 Docker 项目原本的容器管理 API 来完成集群管理,比如:

单机 Docker 项目:

docker run <myContainer>

多机 Docker 项目:

docker run -H <Swarm集群地址> <myContainer>

所以,在部署了 Swam 的多机环境中,用户只需要使用原先的 Docker 指令创建—个容器,Swarm 就会拦截这个请求并处理,然后通过具体的调度算法找到一个合适的 Docker Daemon 运行起来。

后来,Docker 又收购了Fig 项目,并改名为 Compose,从而实现了“容器编排”的功能。假如现在用户需要部署的是应用容器A,数据库容器B,负载均衡容器C,那么 Docker Compose 就允许用户把 A、B、C 这 3 个容器定义在一个配置文件中,并且可以指定它们之间的关联关系,比如容器 A 需要访问数据库容器 B。然后使用一行命令即可启动这三个容器:

docker compose up

尘埃落定

就在 Docker 项目如日中天,并对其他公司造成了极大的挑战之时,基础设施领域的翘楚谷歌公司突然发力,正式宣告了 Kubernetes 项目的诞生,再一次改变了整个容器市场的格局。

CH2 容器技术基础

从进程开始说起

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。

容器技术的核心功能,就是通过约束和修改进程的动态表现,为其创造一个“边界”。对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段而 Namespace 技术是用来修改进程视图的主要方法。

下面通过实践说明 Cgroups 和 Namespace 这两个概念:

首先创建一个容器:

$ docker run -it busybox /bin/sh
/ #

然后在容器中执行 ps 指令:

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    7 root      0:00 ps

最开始执行的 /bin/sh 是这个容器内部的第 1 号进程,并且共有两个进程在运行。这就意味着前面执行的 /bin/sh 以及刚刚执行的 ps,已经被 Docker 隔离在了一个跟宿主机完全不同的世界当中。

正常情况下,在宿主机上运行了一个 /bin/sh 程序时,操作系统会给它分配一个 PID,比如 PID=100。现在,通过 Docker 在一个容器中运行 /bin/sh 时,会通过 Linux 中的 Namespace 机制,对被隔离应用的进程空间做手脚,使得这些进程只能“看到”重新计算过的 PID,比如上面的 PID=1,实际上,在宿主机的操作系统中,还是 PID=100 的进程。

除了 PID Namespace,Linux 还提供了 Mount、UTS、 IPC、NetWork 和 User 这些 Namespace,用来对各种进程上下文进行处理。

隔离与限制

虽然容器内的第 1 号进程只能“看到”容器里的情况,但是在宿主机上,它作为第 100 号进程与其他所有进程之间依然是平等的竞争关系。这就意味看虽然第100号进程表面上被隔离了起来,但是它所能够使用到的资源(比如CPU、内存),可以随时被宿主机上的其他进程(或者其他容器)占用。当然,这个第 100 号进程自己也可能用光所有资源。这些情况显然都不是—个“沙盒”应该表现出来的合理行为。

Linux Cgroups(Linux control groups)就是 Linux 内核中用来为进程设置资源限制的一个重要功能。Linux Cgroups 最主要的作用就是限制一个进程组能够使用的资源上限,包括CPU、内存、磁盘、网络带宽等等。此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进进程挂起和恢复等操作。

在 Linux 中,Cgroups 以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径:

$ mount -t cgroup2    
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
$ ls /sys/fs/cgroup
cgroup.controllers      cpu.stat             memory.pressure
cgroup.max.depth        dev-hugepages.mount  memory.reclaim
cgroup.max.descendants  dev-mqueue.mount     memory.stat
cgroup.pressure         init.scope           misc.capacity
cgroup.procs            io.cost.model        sys-fs-fuse-connections.mount
cgroup.stat             io.cost.qos          sys-kernel-config.mount
cgroup.subtree_control  io.pressure          sys-kernel-debug.mount
cgroup.threads          io.prio.class        sys-kernel-tracing.mount
cpu.pressure            io.stat              system.slice
cpuset.cpus.effective   irq.pressure         user.slice
cpuset.mems.effective   memory.numa_stat

如何使用 Cgroup 的配置呢?首先需要创建一个“控制组”:

$ mkdir container
$ ls container     
cgroup.controllers      hugetlb.1GB.current       memory.high
cgroup.events           hugetlb.1GB.events        memory.low
cgroup.freeze           hugetlb.1GB.events.local  memory.max
cgroup.kill             hugetlb.1GB.max           memory.min
cgroup.max.depth        hugetlb.1GB.numa_stat     memory.numa_stat
cgroup.max.descendants  hugetlb.1GB.rsvd.current  memory.oom.group
cgroup.pressure         hugetlb.1GB.rsvd.max      memory.peak
cgroup.procs            hugetlb.2MB.current       memory.pressure
cgroup.stat             hugetlb.2MB.events        memory.reclaim
cgroup.subtree_control  hugetlb.2MB.events.local  memory.stat
cgroup.threads          hugetlb.2MB.max           memory.swap.current
cgroup.type             hugetlb.2MB.numa_stat     memory.swap.events
cpu.idle                hugetlb.2MB.rsvd.current  memory.swap.high
cpu.max                 hugetlb.2MB.rsvd.max      memory.swap.max
cpu.max.burst           io.bfq.weight             memory.zswap.current
cpu.pressure            io.latency                memory.zswap.max
cpuset.cpus             io.low                    misc.current
cpuset.cpus.effective   io.max                    misc.events
cpuset.cpus.partition   io.pressure               misc.max
cpuset.mems             io.prio.class             pids.current
cpuset.mems.effective   io.stat                   pids.events
cpu.stat                io.weight                 pids.max
cpu.uclamp.max          irq.pressure              pids.peak
cpu.uclamp.min          memory.current            rdma.current
cpu.weight              memory.events             rdma.max
cpu.weight.nice         memory.events.local

现在以修改 CPU 配额为例:

$ echo 20000 100000 > /sys/fs/cgroup/container/cpu.max
$ cat while.sh 
while :
do
    :
done
$ sh ./while.sh &
[1] 67372
$ top 
top - 15:10:29 up 20:17,  2 users,  load average: 1.62, 1.11, 1.32
任务: 402 total,   2 running, 400 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.2 us,  0.2 sy,  6.3 ni, 92.7 id,  0.1 wa,  0.4 hi,  0.1 si,  0.0 st 
MiB Mem :  15721.4 total,   4781.3 free,   6629.8 used,   6106.2 buff/cache     
MiB Swap:  16384.0 total,  14614.0 free,   1770.0 used.   9091.6 avail Mem 

 进程号 USER      PR  NI    VIRT    RES    SHR    %CPU  %MEM     TIME+ COMMAND  
  67372 kkkstra   25   5   10612   2956   2624 R 100.0   0.0   0:36.24 sh       
...
$ echo 67372 > /sys/fs/cgroup/container/cgroup.procs
$ top
top - 15:11:56 up 20:19,  2 users,  load average: 1.20, 1.09, 1.29
任务: 402 total,   2 running, 400 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.9 us,  0.5 sy,  1.5 ni, 96.7 id,  0.0 wa,  0.3 hi,  0.1 si,  0.0 st 
MiB Mem :  15721.4 total,   4704.9 free,   6664.1 used,   6143.7 buff/cache     
MiB Swap:  16384.0 total,  14616.0 free,   1768.0 used.   9057.3 avail Mem 

 进程号 USER      PR  NI    VIRT    RES    SHR    %CPU  %MEM     TIME+ COMMAND  
  67372 kkkstra   25   5   10612   2956   2624 R  20.0   0.0   1:56.83 sh       
...

CH3 Kubernetes设计与架构

CH4 Kubernetes集群搭建与配置

1. Kubeadm

工作原理

直接在宿主机上运行 kubelet,然后使用容器部署其他 Kubernetes 组件。

kube init 的工作流程

  1. Preflight Checks,在执行 kubeadm init 指令后 kubeadm 首先要做一系列检查工作,以确定这台机器可以用来部署 Kubemetes。
  2. 生成 Kubernetes 对外提供服务所需的各种证书和对应目录。kubeadm为 Kubernetes 项目生成的证书文件都放在 Master 节点的 /etc/kubernetes/pki 目录。
  3. 为其他组件生成访问 kube-apiserver 所需的配置文件,这些文件的路径是 /etc/kubernetes/xxx.conf
  4. 为 Master 组件生成 Pod 配置文件,包括 kube-apiserver, kube-controller-manager 和 kube-scheduler,它们被生成在 /etc/kubernetes/manifests
  5. 生成一个 etcd 的 Pod YAML 文件,用来通过同样的 Static Pod 的方式启动 etcd。
  6. 为集群生成一个 bootsrap token,用于后续节点加入集群。
  7. ca.crt 等 Master 节点的重要信息通过 ConfigMap 的方式保存在 etcd 当中。
  8. 安装默认插件,kube-proxy 和 DNS,用于提供整个集群的服务发现和 DNS 功能。

kube join 的工作流程

kubeadm 生成 bootstrap token 之后,就可以在任意一台安装了 kubelet 和 kubeadm 的机器上执行 kubeadm join 了。

配置 kubeadm 的部署参数

使用命令:

kubeadm init --config kubeadm.yaml

2 搭建完整 Kubernetes集群

1. 安装 kubeadm 和 Docker

添加 kubeadm 的源,然后直接安装即可,同时 kubelet、kubectl 和 kubernetes-cni 都会自动安装。

2. 部署 Kubernetes 的 Master 节点

部署 Master 节点只需要一行命令:

kubeadm init --config kubeadm.yaml

完成后,kubeadm 会生成一行指令:

kubeadm join 10.168.0.2:6443 --token 00wbx.uvnaa2ewjflwu1ry
--discovery-token-ca-cert-hash
sha256: 00eh62己2己6020f94132e3fe1abh721349bbcd3e9h94d己9654cfe15f2985ebd711

查看节点状态:

kubectl get nodes

查看详细信息:

kubectl describe node master

3. 部署网络插件

在一切皆容器的理念下,部署网络插件只需要一行命令:

kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"

部署完成后,可以通过 kubectl get 检查 Pod 的状态:

kubectl get pods -n kube-system

4. 部署 Kubernetes 的Worker节点

  1. 安装 kubeadm 和 Docker
  2. 执行部署 Master 时生成的 kubeadm join 指令

5. 通过 Taint/Toleration 调整 Master 执行 Pod 的策略

一旦某个节点被加上了一个 Taint,即”染上污点“,那么所有 Pod 都不能在该节点上运行,除非有个别 Pod 声明了 Toleration,它才可以在该节点上运行。

为节点加上污点的命令:

kubectl taint nodes node1 foo=bar:NoSchedule

NoSchedule 意味着不影响节点上已经在运行的 Pod,只会在调度新 Pod 时起作用。

声明 Toleration 只需要在 Pod 的 .yaml 文件的 spec 部分加入 tolerations 字段即可:

apiVersion: v1
kind: Pod
...
spec:
    tolerations:
    - key: "foo"
      operator: "Equal"
      value: "bar"
      effect: "NoSchedule"

该 Pod 能“容忍”所有键值对为 foo=barTaint

6. 部署 Dashboard 可视化插件

kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml

7. 部署容器存储插件

3. 第一个 Kubernetes 应用

Kubernetes 跟 Docker 等很多项目最大的不同是它不推荐你使用命令行的方式直接运行容器,而是希望你用 YAML 文件的方
式,即把容器的定义、参数、配置统统记录在一个 YAML 文件中,然后用这样一句指令把它运行起来:

kubectl create -f <配置文件>

一个配置文件的例子如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  namespace: hlwds
spec:
  minReadySeconds: 5
  selector:
    matchLabels:
      app: hlwds
  template:
    metadata:
      labels:
        app: hlwds
    spec:
      containers:
      - env:
        - name: TRAEFIK_HTTP_ROUTERS_QIMING_RULE
          value: Host(`hlwds.hustonline.net`)
        image: registry-vpc.cn-shenzhen.aliyuncs.com/bingyan_studio/qiming:7dfc04fe
        livenessProbe:
          initialDelaySeconds: 10
          tcpSocket:
            port: 80
        name: app
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
        resources:
          limits:
            cpu: "2"
            memory: 500Mi
          requests:
            cpu: 1m
            memory: 10Mi
        volumeMounts:
        - mountPath: /app/db
          name: hlwds-storage
      volumes:
      - name: hlwds-storage
        persistentVolumeClaim:
          claimName: hlwds-storage

其中 kind 指定了这个 API 对象的类型,是一个 Deployment

spec.template 中定义了一个 Pod 模板,描述了我想要的 Pod 的细节。

此外,每个 API 对象都有一个叫做 Metadata 的字段,这个字段就是 API 对象的“标识”,是从 Kubernetes 中找到这个对象的主要依据,最常用到的是 Labels

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

kubectl get pods -l app=nginx

还可以用 kubectl describe 查看一个 API 对象的具体细节:

kubectl describe pod xxxxxx

其中一个重要的信息是 Events,对 API 对象的所有重要操作都会被记录在里面。

如果要升级服务,只需要修改 .yaml 文件,然后执行命令:

kubectl apply -f xxx.yaml

还可以在 Deployment 中声明 Volume:

...
spec:
  template:
    ...
    volumes:
    - name: nginx-vol
      emptyDir: {}

最后,还可以使用 kubectl exec 进入 Pod 中:

kubectl exec -it nginx-deploymnt-xxxxx -- /bin/bash

若要在集群中删除 Deployment,则执行:

kubectl delede -f nginx-deployment.yaml