浅谈一下kubernetes

在谈kubernetes之前,先看看一切的基础:容器。

容器

容器是什么?谷歌IBMAWS华为微软对容器都有各自的解释。

容器可以分上下两个维度来定义:

  • 从底下看,容器是一种内核轻量级操作系统层虚拟化技术。用于在单个操作系统实例中运行多个隔离的用户空间。
  • 从上面看,容器是在应用程序层面上的一个抽象,它将应用程序及其依赖项打包在一起,以便可以在不同的计算环境中快速而一致地运行。

vs虚拟机

虽然都是虚拟化技术,但它和虚拟机是完全不同的。从Docker官网借一张对比图。

  • 容器是应用程序层的抽象,容器之间共享操作系统内核,每个容器作为隔离的进程在用户空间中运行。容器占用的空间比虚拟机少,可以处理更多的应用程序。
  • 虚拟机是物理硬件的抽象,将一台服务器变成多台服务器。每个虚拟机都包括完整的操作系统和应用程序,需要更多资源。

运行效率,是容器相比虚拟机最大的优势。

本质

虚拟机使用的是Hypervisor技术,那容器是怎么实现如此高效轻便的隔离特性呢?

其奥秘就在Linux操作系统内核中,为资源隔离提供了三种技术:namespacecgroupschroot。虽然这三种技术的初衷并不是为了容器,但是它们结合在一起却发生了奇妙的反应。

  • namespace
    可以创建出独立的文件系统、主机名、进程号,还有网络等资源空间。
  • cgroup
    用来实现对进程的CPU、内存等资源的优先级和配额限制。
  • chroot
    可以更改进程的根目录,也就是限制进程访问原有的文件系统。

综合这三种技术,具有完善的隔离特性的容器就此出现了。早期的容器有Solaris ContainersOpenVzLinux Container(LXC),但直到Docker的出现,才让容器技术真正大众化起来。可以看一下10年前Solomon HykesPyCon 2013大会上的Lightning talks:The future of Linux Containers,首次向全世界展示了Docker技术。顺便,Aqua Blog上有一篇容器发展简史值得一看。

容器编排(Container Orchestration)

Docker出现后,容器被越来越多人使用。但到生产环境应用部署的时候,却显得步履维艰。因为容器只是针对单个进程的隔离和封装,而实际的应用场景却是要求许多的应用进程互相协同工作,其中的各种关系和需求非常复杂,在容器这个技术层次很难掌控。

于是在Docker周边涌现出的数不胜数的扩展、增强产品中,出现了一个叫Fig的小项目。Fig为Docker引入了容器编排的概念,使用YAML来定义容器的启动参数、先后顺序和依赖关系,让用户不再有Docker冗长命令行的烦恼,并第一次见识到了声明式的威力。Docker公司也很快意识到了Fig这个小工具的价值,于是就在2014年7月把它买了下来,集成进Docker内部,改名成了docker compose

云原生时代

容器技术开启了云原生的大潮,面对服务器集群,只能编排单机的docker compose已经力不从心。于是在2014年,Docker公司推出了Docker Swarm(现已更名为Docker “Classic” Swarm)来支持集群。

然后在2015年,谷歌将换代的内部集群应用管理系统Borg用Go语言改写并开源,命名为Kubernetes。因为Kubernetes背后有着谷歌十多年生产环境经验的支持,理论水平也非常高,一经推出就引起了轰动。

同年,谷歌联合Linux基金会成立了CNCF(Cloud Native Computing Foundation,云原生基金会)把Kubernetes捐献出来作为种子项目。有了谷歌和Linux的保驾护航,再加上宽容开放的社区,作为CNCF的“头把交椅”,Kubernetes仅用了两年就打败了Apache MesosDocker Swarm,成为了云原生时代容器编排的唯一霸主和事实标准。

Kubernetes

从某种角度上说,k8s(k8s代表k和s中间有8个字母,类似用i18n表示internationalization)可以说是一个集群级别的操作系统,主要功能就是资源管理和作业调度。

操作系统的一个重要功能就是抽象,从繁琐的底层事务中抽象出一些简洁的概念,然后基于这些概念去管理系统资源。

Kubernetes也是这样,它的管理目标是大规模的集群和应用,必须要能够把系统抽象到足够高的层次,分解出一些松耦合的对象,才能简化系统模型,减轻用户的心智负担。

集群架构

Kubernetes采用了现今流行的控制面/数据面(Control Plane/Data Plane)架构,集群里的计算机被称为节点(Node),少量的节点用作控制面来执行集群的管理维护工作,其他的大部分节点都被划归数据面,用来跑业务应用。

节点内部也具有复杂的结构,可分为核心的组件和锦上添花的插件

Master节点

Master里有4个组件,分别是apiserver、etcd、scheduler、controller-manager。

  • API Server
    apiserver是Master节点,同时也是整个k8s系统的唯一入口,它对外公开了一系列的RESTful API,并且加上了验证、授权等功能,所有其他组件只能和它直接通信,可以说是k8s里的联络员。
  • Scheduler
  • Controller Manager
  • etcd

Node节点

  • kubelet
    定期向apiserver上报节点状态,apiserver再存到etcd里。
  • kube-proxy
    实现了TCP/UDP反向代理,让容器对外提供稳定的服务。
  • Container Runtime
    任何支持CRI(Container Runtime Interface)的容器运行时,比如containerdCRI-O

插件

从可用到好用的实现路径之一就是插件。k8s设计非常灵活,使用k8s资源对象(如DaemonSet、Deployment等)来实现插件。

常用的有DashboardDNS

YAML

声明式与命令式

  • 命令式
    交互性强,注重顺序和过程。但必须“告诉”计算机每步该做什么,所有的步骤都列清楚,这样程序才能够一步步走下去,最后完成任务,显得计算机有点“笨”。

  • 声明式
    不关心具体的过程,更注重结果。我们不需要“教”计算机该怎么做,只要告诉它一个目标状态,它自己就会想办法去完成任务,相比起来自动化、智能化程度更高。

来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ngx-dep
name: ngx-dep

spec:
replicas: 2
selector:
matchLabels:
app: ngx-dep

template:
metadata:
labels:
app: ngx-dep
spec:
containers:
- image: nginx:alpine
name: nginx

注意里面的replicas: 2,这一句表示期望的副本数量。也就是说,要在集群中运行多少个实例。

我们只要声明期望的状态,Deployment对象就可以扮演运维监控人员的角色,自动地在集群里调整Pod的数量,这就是声明式的魅力。

为了更好的实现声明式,k8s使用了YAML,它是JSON的一个超集。

API对象

YAML语言只相当于“语法”,要与Kubernetes对话,我们还必须有足够的“词汇”来表示“语义”。

作为一个集群操作系统,Kubernetes 归纳总结了 Google 多年的经验,在理论层面抽象出了很多个概念,用来描述系统的管理运维工作,这些概念就叫做“API 对象”。说到这个名字,你也许会联想到上次课里讲到的 Kubernetes 组件 apiserver。没错,它正是来源于此。

因为 apiserver 是 Kubernetes 系统的唯一入口,外部用户和内部组件都必须和它通信,而它采用了 HTTP 协议的 URL 资源理念,API 风格也用 RESTful 的 GET/POST/DELETE 等等,所以,这些概念很自然地就被称为是“API 对象”了。

运行机制

Kubernetes的Master/Node架构是它具有自动化运维能力的关键,再用另一张参考架构图来简略说明一下它的运行机制。

工作负荷(Workload)

工作负荷就是在k8s中运行的应用。而pods是k8s管理应用的最小单位,所有的工作负荷都是从pod扩展出来的。

Pod

一个容器中只运行一个进程或应用。Pod可以解决联合运行的需求,但作为一个整体它又足够轻量。类似Docker中的compose project。

一个简单的pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spec:
containers:
- image: busybox:latest
name: busy
imagePullPolicy: IfNotPresent
env:
- name: os
value: "ubuntu"
- name: debug
value: "on"
command:
- /bin/echo
args:
- "$(os), $(debug)"

问题

如果你试一下,就会发现上面这个pod运行后的状态是CrashLoopBackOff。其实在docker里也是一样的,因为/bin/echo不是一个守护进程,它执行完就退出了,而不管是docker还是k8s,默认运行的都是在线服务,而不是一次性任务。

我们当然可以使用tail -f /dev/nullexec /bin/bash -c "trap : TERM INT; sleep infinity & wait"来保持进程不退出,但这样会占用资源,而且不够优雅。

Job/CronJob

为了解决离线任务的需求,k8s引入了job和cronjob。Job是一次性任务,而CronJob是定时任务。

job

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: batch/v1
kind: Job
metadata:
name: echo-job

spec:
template:
spec:
restartPolicy: OnFailure
containers:
- image: busybox
name: echo-job
imagePullPolicy: IfNotPresent
command: ["/bin/echo"]
args: ["hello", "world"]

从这里可以感受到api对象的魅力。

cronjob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: batch/v1
kind: CronJob
metadata:
name: echo-cj

spec:
schedule: '*/1 * * * *'
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- image: busybox
name: echo-cj
imagePullPolicy: IfNotPresent
command: ["/bin/echo"]
args: ["hello", "world"]

Deployment

Daemonset

StatefulSet

服务与网络

Service

Ingress

配置与存储

ConfigMap/Secret

PersistentVolume

资源限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
name: ngx-pod-resources

spec:
containers:
- image: nginx:alpine
name: ngx

resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: 20m
memory: 200Mi