阅读更多
1 云原生概述
1.1 云原生技术发展简史
首先从第一个问题进行分享,那就是“为什么要开设云原生技术公开课?”云原生、CNCF
都是目前非常热门的关键词,但是这些技术并不是非常新鲜的内容。
- 2004年—2007年,
Google
已在内部大规模地使用像Cgroups
这样的容器技术 - 2008年,
Google
将Cgroups
合并进入了Linux内核主干 - 2013年,
Docker
项目正式发布 - 2014年,
Kubernetes
项目也正式发布。这样的原因也非常容易理解,因为有了容器和Docker
之后,就需要有一种方式去帮助大家方便、快速、优雅地管理这些容器,这就是Kubernetes
项目的初衷。在Google
和Redhat
发布了Kubernetes
之后,这个项目的发展速度非常之快 - 2015年,由
Google
、Redhat
以及微软等大型云计算厂商以及一些开源公司共同牵头成立了CNCF
云原生基金会。CNCF
成立之初,就有22个创始会员,而且Kubernetes
也成为了CNCF
托管的第一个开源项目。在这之后,CNCF
的发展速度非常迅猛 - 2017年,
CNCF
达到170个成员和14个基金项目 - 2018年,
CNCF
成立三周年有了195个成员,19个基金会项目和11个孵化项目,如此之快的发展速度在整个云计算领域都是非常罕见的
1.2 我们正处于时代的关键节点
2019年正是云原生时代的关键节点,为什么这么说?我们这里就为大家简单梳理一下
从2013年Docker
项目发布开始说起,Docker
项目的发布使得全操作系统语义的沙盒技术唾手可得,使得用户能够更好地、更完整地打包自己的应用,使得开发者可以轻而易举的获得了一个应用的最小可运行单位,而不需要依赖任何PaaS
能力。这对经典PaaS产业其实是一个“降维打击”
2014年的时候,Kubernetes
项目发布,其意义在于Google
将内部的Borg/Omega
系统思想借助开源社区实现了“重生”,并且提出了“容器设计模式”的思想。而Google
之所以选择间接开源Kubernetes
而不是直接开源Borg
项目,其实背后的原因也比较容易理解:Borg/Omega
这样的系统太复杂了,是没办法提供给Google
之外的人使用,但是Borg/Omega
这样的设计思想却可以借助Kubernetes
让大家接触到,这也是开源Kubernetes
的重要背景
这样到了2015年-2016年,就到了容器编排“三国争霸”的时代,当时Docker
、Swarm
、Mesos
、Kubernetes
都在容器编排领域展开角逐,他们竞争的原因其实也比较容易理解, 那就是Docker
或者容器本身的价值虽然大,但是如果想要让其产生商业价值或者说对云的价值,那么就一定需要在编排上面占据一个有利的位置
Swarm
和Mesos
的特点,那就是各自只在生态和技术方面比较强,其中,Swarm
更偏向于生态,而Mesos
技术更强一些。相比之下,Kubernetes
则兼具了两者优势,最终在 2017年“三国争霸”的局面中得以胜出,成为了当时直到现在的容器编排标准。这一过程的代表性事件就是Docker
公司宣布在核心产品中内置了Kubernetes
服务,并且Swarm
项目逐渐停止维护
到了2018年的时候,云原生技术理念开始逐渐萌芽,这是因为此时Kubernetes
以及容器都成为了云厂商的既定标准,以“云”为核心的软件研发思想逐步形成
而到了2019年,情况似乎又将发生一些变化
1.3 2019年——云原生技术普及元年
为什么说2019年很可能是一个关键节点呢?我们认为2019年是云原生技术的普及元年
首先大家可以看到,在2019年,阿里巴巴宣布要全面上云,而且“上云就要上云原生”。我们还可以看到,以“云”为核心的软件研发思想,正逐步成为所有开发者的默认选项。像Kubernetes
等云原生技术正在成为技术人员的必修课,大量的工作岗位正在涌现出来
这种背景下,“会Kubernetes
”已经远远不够了,“懂Kubernetes
”、“会云原生架构”的重要性正日益凸显出来。从2019年开始,云原生技术将会大规模普及,这也是为什么大家都要在这个时间点上学习和投资云原生技术的重要原因
1.4 云原生的技术范畴
云原生的技术范畴包括了以下几个方面:
- 第一部分是云应用定义与开发流程。这包括应用定义与镜像制作、配置
CI/CD
、消息和Streaming
以及数据库等 - 第二部分是云应用的编排与管理流程。这也是
Kubernetes
比较关注的一部分,包括了应用编排与调度、服务发现治理、远程调用、API 网关以及Service Mesh
- 第三部分是监控与可观测性。这部分所强调的是云上应用如何进行监控、日志收集、Tracing以及在云上如何实现破坏性测试,也就是混沌工程的概念
- 第四部分就是云原生的底层技术,比如容器运行时、云原生存储技术、云原生网络技术等
- 第五部分是云原生工具集,在前面的这些核心技术点之上,还有很多配套的生态或者周边的工具需要使用,比如流程自动化与配置管理、容器镜像仓库、云原生安全技术以及云端密码管理等
- 最后则是
Serverless
。Serverless
是一种PaaS
的特殊形态,它定义了一种更为“极端抽象”的应用编写方式,包含了FaaS
和BaaS
这样的概念。而无论是FaaS
还是BaaS
,其最为典型的特点就是按实际使用计费(Pay as you go),因此Serverless
计费也是重要的知识和概念
1.5 云原生思想的两个理论
在了解完云原生的技术范畴之后你就会发现,其所包含的技术内容还是很多的,但是这些内容的技术本质却是类似的。云原生技术的本质是两个理论基础。
- 第一个理论基础是:不可变基础设施。这一点目前是通过容器镜像来实现的,其含义就是应用的基础设施应该是不可变的,是一个自包含、自描述可以完全在不同环境中迁移的东西
- 第二个理论基础就是:云应用编排理论。当前的实现方式就是
Google
所提出来的“容器设计模式”,这也是本系列课程中的Kubernetes
部分所需主要讲解的内容
1.6 基础设施向云演进的过程
首先为大家介绍一下“不可变基础设施”的概念。其实,应用所依赖的基础设施也在经历一个向云演进的过程,举例而言,对于传统的应用基础设施而言,其实往往是可变的。
大家可能经常会干这样一件事情,比如需要发布或者更新一个软件,那么流程大致是这样的,先通过SSH
连到服务器,然后手动升级或者降级软件包,逐个调整服务器上的配置文件,并且将新代码直接都部署到现有服务器上。因此,这套基础设施会不断地被调整和修改
但是在云上,对“云”友好的应用基础设施是不可变的
这种场景下的上述更新过程会这么做:一旦应用部署完成之后,那么这套应用基础设施就不会再修改了。如果需要更新,那么需要现更改公共镜像来构建新服务直接替换旧服务。而我们之所以能够实现直接替换,就是因为容器提供了自包含的环境(包含应用运行所需的所有依赖)。所以对于应用而言,完全不需要关心容器发生了什么变化,只需要把容器镜像本身修改掉就可以了。因此,对于云友好的基础设施是随时可以替换和更换的,这就是因为容器具有敏捷和一致性的能力,也就是云时代的应用基础设施
所以,总结而言,云时代的基础设施就像是可以替代的“牲口”,可以随时替换;而传统的基础设施则是独一无二的“宠物”,需要细心呵护,这就体现出了云时代不可变基础设施的优点
1.7 基础设施向云演进的意义
所以,像这样的基础设施向“不可变”演进的过程,为我们提供了两个非常重要的优点
- 基础设施的一致性和可靠性。同样一个镜像,无论是在美国打开,在中国打开,还是在印度打开都是一样的。并且其中的
OS
环境对于应用而言都是一致的。而对于应用而言,它就不需要关心容器跑在哪里,这就是基础设施一致性非常重要的一个特征 - 这样的镜像本身就是自包含的,其包含了应用运行所需要的所有依赖,因此也可以漂移到云上的任何一个位置
此外,云原生的基础设施还提供了简单、可预测的部署和运维能力。由于现在有了镜像,应用还是自描述的,通过镜像运行起来的整个容器其实可以像Kubernetes
的Operator
技术一样将其做成自运维的,所以整个应用本身都是自包含的行为,使得其能够迁移到云上任何一个位置。这也使得整个流程的自动化变得非常容易
应用本身也可以更好地扩容,从1个实例变成100个实例,进而变成1万个实例,这个过程对于容器化后的应用没有任何特殊的。最后,我们这时也能够通过不可变的基础设施来地快速周围的管控系统和支撑组件。因为,这些组件本身也是容器化的,是符合不可变基础设施这样一套理论的组件
以上就是不可变基础设施为用户带来的最大的优点
1.8 云原生关键技术点
当我们回过头来看云原生关键技术点或者说它所依赖的技术理论的时候,可以看到主要有这样的四个方向:
- 如何构建自包含、可定制的应用镜像
- 能不能实现应用快速部署与隔离能力
- 应用基础设施创建和销毁的自动化管理
- 可复制的管控系统和支撑组件
2 容器基本概念
2.1 容器与镜像
什么是容器?
在介绍容器的具体概念之前,先简单回顾一下操作系统是如何管理进程的
首先,当我们登录到操作系统之后,可以通过ps
等操作看到各式各样的进程,这些进程包括系统自带的服务和用户的应用进程。那么,这些进程都有什么样的特点?
- 这些进程可以相互看到、相互通信
- 它们使用的是同一个文件系统,可以对同一个文件进行读写操作
- 这些进程会使用相同的系统资源
这样的三个特点会带来什么问题呢?
- 因为这些进程能够相互看到并且进行通信,高级权限的进程可以攻击其他进程
- 因为它们使用的是同一个文件系统,因此会带来两个问题
- 这些进程可以对于已有的数据进行增删改查,具有高级权限的进程可能会将其他进程的数据删除掉,破坏掉其他进程的正常运行
- 此外,进程与进程之间的依赖可能会存在冲突,如此一来就会给运维带来很大的压力
- 因为这些进程使用的是同一个宿主机的资源,应用之间可能会存在资源抢占的问题,当一个应用需要消耗大量
CPU
和内存资源的时候,就可能会破坏其他应用的运行,导致其他应用无法正常地提供服务
针对上述的三个问题,如何为进程提供一个独立的运行环境呢?
针对不同进程使用同一个文件系统所造成的问题而言,Linux
和Unix
操作系统可以通过chroot
系统调用将子目录变成根目录,达到视图级别的隔离;进程在chroot
的帮助下可以具有独立的文件系统,对于这样的文件系统进行增删改查不会影响到其他进程
因为进程之间相互可见并且可以相互通信,使用Namespace
技术来实现进程在资源的视图上进行隔离。在chroot
和Namespace
的帮助下,进程就能够运行在一个独立的环境下了
但在独立的环境下,进程所使用的还是同一个操作系统的资源,一些进程可能会侵蚀掉整个系统的资源。为了减少进程彼此之间的影响,可以通过Cgroup
来限制其资源使用率,设置其能够使用的CPU
以及内存量
那么,应该如何定义这样的进程集合呢?其实,容器就是一个视图隔离、资源可限制、独立文件系统的进程集合
- 所谓“视图隔离”就是能够看到部分进程以及具有独立的主机名等
- 控制资源使用率则是可以对于内存大小以及
CPU
使用个数等进行限制 - 容器就是一个进程集合,它将系统的其他资源隔离开来,具有自己独立的资源视图
- 容器具有一个独立的文件系统,因为使用的是系统的资源,所以在独立的文件系统内不需要具备内核相关的代码或者工具,我们只需要提供容器所需的二进制文件、配置文件以及依赖即可。只要容器运行时所需的文件集合都能够具备,那么这个容器就能够运行起来
什么是镜像?综上所述,我们将这些容器运行时所需要的所有的文件集合称之为容器镜像
那么,一般都是通过什么样的方式来构建镜像的呢?通常情况下,我们会采用Dockerfile
来构建镜像,这是因为Dockerfile
提供了非常便利的语法糖,能够帮助我们很好地描述构建的每个步骤。当然,每个构建步骤都会对已有的文件系统进行操作,这样就会带来文件系统内容的变化,我们将这些变化称之为changeset
。当我们把构建步骤所产生的变化依次作用到一个空文件夹上,就能够得到一个完整的镜像
changeset
的分层以及复用特点能够带来几点优势:
- 能够提高分发效率,简单试想一下,对于大的镜像而言,如果将其拆分成各个小块就能够提高镜像的分发效率,这是因为镜像拆分之后就可以并行下载这些数据
- 因为这些数据是相互共享的,也就意味着当本地存储上包含了一些数据的时候,只需要下载本地没有的数据即可,举个简单的例子就是
golang
镜像是基于alpine
镜像进行构建的,当本地已经具有了alpine
镜像之后,在下载golang
镜像的时候只需要下载本地alpine
镜像中没有的部分即可 - 因为镜像数据是共享的,因此可以节约大量的磁盘空间,简单设想一下,当本地存储具有了
alpine
镜像和golang
镜像,在没有复用的能力之前,alpine
镜像具有5M
大小,golang
镜像有300M
大小,因此就会占用305M
空间;而当具有了复用能力之后,只需要300M
空间即可
如何构建镜像?
FROM
行表示以下的构建步骤基于什么镜像进行构建,正如前面所提到的,镜像是可以复用的WORKDIR
行表示会把接下来的构建步骤都在哪一个相应的具体目录下进行,其起到的作用类似于Shell
里面的cd
COPY
行表示的是可以将宿主机上的文件拷贝到容器镜像内RUN
行表示在具体的文件系统内执行相应的动作。当我们运行完毕之后就可以得到一个应用了CMD
行表示使用镜像时的默认程序名字
当有了Dockerfile
之后,就可以通过docker build
命令构建出所需要的应用。构建出的结果存储在本地,一般情况下,镜像构建会在打包机或者其他的隔离环境下完成
那么,这些镜像如何运行在生产环境或者测试环境上呢?这时候就需要一个中转站或者中心存储,我们称之为docker registry
,也就是镜像仓库,其负责存储所有产生的镜像数据。我们只需要通过docker push
就能够将本地镜像推动到镜像仓库中,这样一来,就能够在生产环境上或者测试环境上将相应的数据下载下来并运行了
如何运行容器?运行一个容器一般情况下分为三步:
- 从镜像仓库中将相应的镜像下载下来
- 当镜像下载完成之后就可以通过
docker images
来查看本地镜像,这里会给出一个完整的列表,我们可以在列表中选中想要的镜像 - 当选中镜像之后,就可以通过
docker run
来运行这个镜像得到想要的容器,当然可以通过多次运行得到多个容器。一个镜像就相当于是一个模板,一个容器就像是一个具体的运行实例,因此镜像就具有了一次构建、到处运行的特点
小结
简单回顾一下,容器就是和系统其它部分隔离开来的进程集合,这里的其他部分包括进程、网络资源以及文件系统等。而镜像就是容器所需要的所有文件集合,其具备一次构建、到处运行的特点
2.2 容器的生命周期
容器运行时的生命周期
容器是一组具有隔离特性的进程集合,在使用docker run
的时候会选择一个镜像来提供独立的文件系统并指定相应的运行程序。这里指定的运行程序称之为initial
进程,这个initial
进程启动的时候,容器也会随之启动,当initial
进程退出的时候,容器也会随之退出。
因此,可以认为容器的生命周期和initial
进程的生命周期是一致的。当然,因为容器内不只有这样的一个initial
进程,initial
进程本身也可以产生其他的子进程或者通过docker exec
产生出来的运维操作,也属于initial
进程管理的范围内。当initial
进程退出的时候,所有的子进程也会随之退出,这样也是为了防止资源的泄漏
但是这样的做法也会存在一些问题,首先应用里面的程序往往是有状态的,其可能会产生一些重要的数据,当一个容器退出被删除之后,数据也就会丢失了,这对于应用方而言是不能接受的,所以需要将容器所产生出来的重要数据持久化下来。容器能够直接将数据持久化到指定的目录上,这个目录就称之为数据卷
数据卷有一些特点,其中非常明显的就是数据卷的生命周期是独立于容器的生命周期的,也就是说容器的创建、运行、停止、删除等操作都和数据卷没有任何关系,因为它是一个特殊的目录,是用于帮助容器进行持久化的。简单而言,我们会将数据卷挂载到容器内,这样一来容器就能够将数据写入到相应的目录里面了,而且容器的退出并不会导致数据的丢失
通常情况下,数据卷管理主要有两种方式:
- 通过
bind
的方式,直接将宿主机的目录直接挂载到容器内。这种方式比较简单,但是会带来运维成本,因为其依赖于宿主机的目录,需要对于所有的宿主机进行统一管理 - 将目录管理交给运行引擎
2.3 容器项目架构
moby
容器引擎架构
moby
是目前最流行的容器管理引擎,moby daemon
会对上提供有关于容器、镜像、网络以及 Volume
的管理。moby daemon
所依赖的最重要的组件就是containerd
,containerd
是一个容器运行时管理引擎,其独立于moby daemon
,可以对上提供容器、镜像的相关管理
containerd
底层有containerd shim
模块,其类似于一个守护进程,这样设计的原因有几点:
- 首先,
containerd
需要管理容器生命周期,而容器可能是由不同的容器运行时所创建出来的,因此需要提供一个灵活的插件化管理。而shim
就是针对于不同的容器运行时所开发的,这样就能够从containerd
中脱离出来,通过插件的形式进行管理 - 其次,因为
shim
插件化的实现,使其能够被containerd
动态接管。如果不具备这样的能力,当moby daemon
或者containerd daemon
意外退出的时候,容器就没人管理了,那么它也会随之消失、退出,这样就会影响到应用的运行 - 最后,因为随时可能会对
moby
或者containerd
进行升级,如果不提供shim
机制,那么就无法做到原地升级,也无法做到不影响业务的升级,因此containerd shim
非常重要,它实现了动态接管的能力
2.4 容器 VS VM
容器和VM
之间的差异
VM
利用Hypervisor
虚拟化技术来模拟CPU
、内存等硬件资源,这样就可以在宿主机上建立一个 Guest OS
,这是常说的安装一个虚拟机
每一个Guest OS
都有一个独立的内核,比如Ubuntu
、CentOS
甚至是Windows
等,在这样的 Guest OS
之下,每个应用都是相互独立的,VM
可以提供一个更好的隔离效果。但这样的隔离效果需要付出一定的代价,因为需要把一部分的计算资源交给虚拟化,这样就很难充分利用现有的计算资源,并且每个Guest OS
都需要占用大量的磁盘空间,比如Windows
操作系统的安装需要10~30G
的磁盘空间,Ubuntu
也需要 5~6G,同时这样的方式启动很慢。正是因为虚拟机技术的缺点,催生出了容器技术
容器是针对于进程而言的,因此无需Guest OS
,只需要一个独立的文件系统提供其所需要文件集合即可。所有的文件隔离都是进程级别的,因此启动时间快于VM
,并且所需的磁盘空间也小于VM
。当然了,进程级别的隔离并没有想象中的那么好,隔离效果相比VM
要差很多。
总体而言,容器和VM
相比,各有优劣,因此容器技术也在向着强隔离方向发展
2.5 总结
- 容器是一个进程集合,具有自己独特的视图视角
- 镜像是容器所需要的所有文件集合,其具备一次构建、到处运行的特点
- 容器的生命周期和
initial
进程的生命周期是一样的 - 容器和
VM
相比,各有优劣,容器技术在向着强隔离方向发展
3 Kubernetes核心概念
3.1 什么是Kubernetes
Kubernetes
,从官方网站上可以看到,它是一个工业级的容器编排平台。Kubernetes
这个单词是希腊语,它的中文翻译是“舵手”或者“飞行员”。在一些常见的资料中也会看到“ks”这个词,也就是“k8s”,它是通过将8个字母“ubernete”替换为“8”而导致的一个缩写
Kubernetes
为什么要用“舵手”来命名呢?大家可以看一下这张图:
这是一艘载着一堆集装箱的轮船,轮船在大海上运着集装箱奔波,把集装箱送到它们该去的地方。我们之前其实介绍过一个概念叫做container
,container
这个英文单词也有另外的一个意思就是“集装箱”。Kubernetes
也就借着这个寓意,希望成为运送集装箱的一个轮船,来帮助我们管理这些集装箱,也就是管理这些容器
这个就是为什么会选用Kubernetes
这个词来代表这个项目的原因。更具体一点地来说:Kubernetes
是一个自动化的容器编排平台,它负责应用的部署、应用的弹性以及应用的管理,这些都是基于容器的
3.2 Kubernetes的几个核心的功能
- 服务的发现与负载的均衡
- 容器的自动装箱,我们也会把它叫做
scheduling
,就是“调度”,把一个容器放到一个集群的某一个机器上,Kubernetes
会帮助我们去做存储的编排,让存储的声明周期与容器的生命周期能有一个连接 Kubernetes
会帮助我们去做自动化的容器的恢复。在一个集群中,经常会出现宿主机的问题或者说是OS
的问题,导致容器本身的不可用,Kubernetes
会自动地对这些不可用的容器进行恢复Kubernetes
会帮助我们去做应用的自动发布与应用的回滚,以及与应用相关的配置密文的管理- 对于
job
类型任务,Kubernetes
可以去做批量的执行 - 为了让这个集群、这个应用更富有弹性,
Kubernetes
也支持水平的伸缩
下面,我们希望以三个例子跟大家更切实地介绍一下Kubernetes
的能力
3.2.1 调度
Kubernetes
可以把用户提交的容器放到Kubernetes
管理的集群的某一台节点上去。Kubernetes
的调度器是执行这项能力的组件,它会观察正在被调度的这个容器的大小、规格
比如说它所需要的CPU
以及它所需要的memory
,然后在集群中找一台相对比较空闲的机器来进行一次placement
,也就是一次放置的操作。在这个例子中,它可能会把红颜色的这个容器放置到第二个空闲的机器上,来完成一次调度的工作
3.2.2 自动修复
Kubernetes
有一个节点健康检查的功能,它会监测这个集群中所有的宿主机,当宿主机本身出现故障,或者软件出现故障的时候,这个节点健康检查会自动对它进行发现
下面Kubernetes
会把运行在这些失败节点上的容器进行自动迁移,迁移到一个正在健康运行的宿主机上,来完成集群内容器的一个自动恢复
3.2.3 水平伸缩
Kubernetes
有业务负载检查的能力,它会监测业务上所承担的负载,如果这个业务本身的CPU
利用率过高,或者响应时间过长,它可以对这个业务进行一次扩容
比如说在下面的例子中,黄颜色的过度忙碌,Kubernetes
就可以把黄颜色负载从一份变为三份。接下来,它就可以通过负载均衡把原来打到第一个黄颜色上的负载平均分到三个黄颜色的负载上去,以此来提高响应的时间
3.3 Kubernetes的架构
Kubernetes
架构是一个比较典型的二层架构
和server-client
架构。Master
作为中央的管控节点,会去与Node
进行一个连接
所有UI
的、clients
、这些user
侧的组件,只会和Master
进行连接,把希望的状态或者想执行的命令下发给Master
,Master
会把这些命令或者状态下发给相应的节点,进行最终的执行
Kubernetes
的Master
包含四个主要的组件:API Server
、Controller
、Scheduler
以及etcd
。如下图所示:
API Server
:顾名思义是用来处理API
操作的,Kubernetes
中所有的组件都会和API Server
进行连接,组件与组件之间一般不进行独立的连接,都依赖于API Server
进行消息的传送Controller
:是控制器,它用来完成对集群状态的一些管理。比如刚刚我们提到的两个例子之中,第一个自动对容器进行修复、第二个自动进行水平扩张,都是由Kubernetes
中的Controller
来进行完成的Scheduler
:是调度器,“调度器”顾名思义就是完成调度的操作,就是我们刚才介绍的第一个例子中,把一个用户提交的Container
,依据它对CPU
、对memory
请求大小,找一台合适的节点,进行放置etcd
:是一个分布式的一个存储系统,API Server
中所需要的这些原信息都被放置在etcd
中,etcd
本身是一个高可用系统,通过etcd
保证整个Kubernetes
的Master
组件的高可用性
我们刚刚提到的API Server
,它本身在部署结构上是一个可以水平扩展的一个部署组件;Controller
是一个可以进行热备的一个部署组件,它只有一个active
,它的调度器也是相应的,虽然只有一个active
,但是可以进行热备
Kubernetes
的架构Node
Kubernetes
的Node
是真正运行业务负载的,每个业务负载会以Pod
的形式运行。等一下我会介绍一下Pod
的概念。一个Pod
中运行的一个或者多个容器,真正去运行这些Pod
的组件的是叫做kubelet
,也就是Node
上最为关键的组件,它通过API Server
接收到所需要Pod
运行的状态,然后提交到我们下面画的这个Container Runtime
组件中
在OS
上去创建容器所需要运行的环境,最终把容器或者Pod
运行起来,也需要对存储跟网络进行管理。Kubernetes
并不会直接进行网络存储的操作,他们会靠Storage Plugin
或者是网络的 Plugin
来进行操作。用户自己或者云厂商都会去写相应的Storage Plugin
或者Network Plugin
,去完成存储操作或网络操作
在Kubernetes
自己的环境中,也会有Kubernetes
的Network
,它是为了提供Service network
来进行搭网组网的。(等一下我们也会去介绍“service”这个概念。)真正完成service
组网的组件的是Kube-proxy
,它是利用了iptable
的能力来进行组建Kubernetes
的Network
,就是cluster network
,以上就是Node
上面的四个组件
Kubernetes
的Node
并不会直接和user
进行interaction
,它的interaction
只会通过Master
。而User
是通过Master
向节点下发这些信息的。Kubernetes
每个Node
上,都会运行我们刚才提到的这几个组件
下面我们以一个例子再去看一下Kubernetes
架构中的这些组件,是如何互相进行interaction
的
用户可以通过UI
或者CLI
提交一个Pod
给Kubernetes
进行部署,这个Pod
请求首先会通过CLI
或者UI
提交给Kubernetes API Server
,下一步API Server
会把这个信息写入到它的存储系统etcd
,之后Scheduler
会通过API Server
的watch
或者叫做notification
机制得到这个信息:有一个Pod
需要被调度
这个时候Scheduler
会根据它的内存状态进行一次调度决策,在完成这次调度之后,它会向API Server report
说:“OK!这个 Pod 需要被调度到某一个节点上。”
这个时候API Server
接收到这次操作之后,会把这次的结果再次写到etcd
中,然后API Server
会通知相应的节点进行这次Pod
真正的执行启动。相应节点的kubelet
会得到这个通知,kubelet
就会去调Container runtime
来真正去启动配置这个容器和这个容器的运行环境,去调度Storage Plugin
来去配置存储,network Plugin
去配置网络
这个例子我们可以看到:这些组件之间是如何相互沟通相互通信,协调来完成一次Pod
的调度执行操作的
3.4 Kubernetes的核心概念与它的API
3.4.1 核心概念
3.4.1.1 第一个概念:Pod
Pod
是Kubernetes
的一个最小调度以及资源单元。用户可以通过Kubernetes
的Pod API
生产一个Pod
,让Kubernetes
对这个Pod
进行调度,也就是把它放在某一个Kubernetes
管理的节点上运行起来。一个Pod
简单来说是对一组容器的抽象,它里面会包含一个或多个容器
比如像下面的这幅图里面,它包含了两个容器,每个容器可以指定它所需要资源大小。比如说,1
个核1
个G
,或者说0.5
个核,0.5
个G
当然在这个Pod
中也可以包含一些其他所需要的资源:比如说我们所看到的Volume
卷这个存储资源;比如说我们需要100
个GB
的存储或者20GB
的另外一个存储
在Pod
里面,我们也可以去定义容器所需要运行的方式。比如说运行容器的Command
,以及运行容器的环境变量等等。Pod
这个抽象也给这些容器提供了一个共享的运行环境,它们会共享同一个网络环境,这些容器可以用localhost
来进行直接的连接。而Pod
与Pod
之间,是互相有isolation
隔离的
3.4.1.2 第二个概念:Volume
Volume
就是卷的概念,它是用来管理Kubernetes
存储的,是用来声明在Pod
中的容器可以访问文件目录的,一个卷可以被挂载在Pod
中一个或者多个容器的指定路径下面
而Volume
本身是一个抽象的概念,一个Volume
可以去支持多种的后端的存储。比如说Kubernetes
的Volume
就支持了很多存储插件,它可以支持本地的存储,可以支持分布式的存储,比如说像ceph
,GlusterFS
;它也可以支持云存储,比如说阿里云上的云盘、AWS
上的云盘、Google
上的云盘等等
3.4.1.3 第三个概念:Deployment
Deployment
是在Pod
这个抽象上更为上层的一个抽象,它可以定义一组Pod
的副本数目、以及这个Pod
的版本。一般大家用Deployment
这个抽象来做应用的真正的管理,而Pod
是组成Deployment
最小的单元
Kubernetes
是通过Controller
,也就是我们刚才提到的控制器去维护Deployment
中Pod
的数目,它也会去帮助Deployment
自动恢复失败的Pod
比如说我可以定义一个Deployment
,这个Deployment
里面需要两个Pod
,当一个Pod
失败的时候,控制器就会监测到,它重新把Deployment
中的Pod
数目从一个恢复到两个,通过再去新生成一个Pod
。通过控制器,我们也会帮助完成发布的策略。比如说进行滚动升级,进行重新生成的升级,或者进行版本的回滚
3.4.1.4 第四个概念:Service
Service
提供了一个或者多个Pod
实例的稳定访问地址
比如在上面的例子中,我们看到:一个Deployment
可能有两个甚至更多个完全相同的Pod
。对于一个外部的用户来讲,访问哪个Pod
其实都是一样的,所以它希望做一次负载均衡,在做负载均衡的同时,我只想访问某一个固定的VIP
,也就是Virtual IP
地址,而不希望得知每一个具体的Pod
的IP
地址。
我们刚才提到,这个Pod
本身可能terminal go
(终止),如果一个Pod
失败了,可能会换成另外一个新的
对一个外部用户来讲,提供了多个具体的Pod
地址,这个用户要不停地去更新Pod
地址,当这个Pod
再失败重启之后,我们希望有一个抽象,把所有Pod
的访问能力抽象成一个第三方的一个IP
地址,实现这个的Kubernetes
的抽象就叫Service
实现Service
有多种方式,Kubernetes
支持Cluster IP
,上面我们讲过的Kuber-proxy
的组网,它也支持nodePort
、LoadBalancer
等其他的一些访问的能力
3.4.1.5 第五个概念:Namespace
Namespace
是用来做一个集群内部的逻辑隔离的,它包括鉴权、资源管理等。Kubernetes
的每个资源,比如刚才讲的Pod
、Deployment
、Service
都属于一个Namespace
,同一个Namespace
中的资源需要命名的唯一性,不同的Namespace
中的资源可以重名
Namespace
一个用例,比如像在阿里巴巴,我们内部会有很多个business units
,在每一个 business units
之间,希望有一个视图上的隔离,并且在鉴权上也不一样,在cuda
上面也不一样,我们就会用Namespace
来去给每一个BU
提供一个他所看到的这么一个看到的隔离的机制
3.4.2 Kubernetes的API
下面我们介绍一下Kubernetes
的API
的基础知识。从high-level
上看,Kubernetes API
是由HTTP+JSON
组成的:用户访问的方式是HTTP
,访问的API
中content
的内容是JSON
格式的
Kubernetes
的kubectl
也就是command tool
,Kubernetes UI
,或者有时候用curl
,直接与Kubernetes
进行沟通,都是使用HTTP + JSON
这种形式
下面有个例子:比如说,对于这个Pod
类型的资源,它的HTTP
访问的路径,就是API
,然后是apiVesion: V1
, 之后是相应的Namespaces
,以及Pods
资源,最终是Podname
,也就是Pod
的名字。
如果我们去提交一个Pod
,或者get
一个Pod
的时候,它的content
内容都是用JSON
或者是YAML
表达的。上图中有个yaml
的例子,在这个yaml file
中,对Pod
资源的描述也分为几个部分。
第一个部分,一般来讲会是API
的version
。比如在这个例子中是V1
,它也会描述我在操作哪个资源;比如说我的kind
如果是pod
,在Metadata
中,就写上这个Pod
的名字;比如说nginx
,我们也会给它打一些label
,我们等下会讲到label
的概念。在Metadata
中,有时候也会去写annotation
,也就是对资源的额外的一些用户层次的描述
比较重要的一个部分叫做Spec
,Spec
也就是我们希望Pod
达到的一个预期的状态。比如说它内部需要有哪些container
被运行;比如说这里面有一个nginx
的container
,它的image
是什么?它暴露的port
是什么?
当我们从Kubernetes API
中去获取这个资源的时候,一般来讲在Spec
下面会有一个项目叫status
,它表达了这个资源当前的状态;比如说一个Pod
的状态可能是正在被调度、或者是已经 running
、或者是已经被terminates
,就是被执行完毕了
刚刚在API
之中,我们讲了一个比较有意思的metadata
叫做“label”,这个label
可以是一组KeyValuePair
比如下图的第一个Pod
中,label
就可能是一个color
等于red
,即它的颜色是红颜色。当然你也可以加其他label
,比如说size: big
就是大小,定义为大的,它可以是一组label
这些label
是可以被selector
,也就是选择器所查询的。这个能力实际上跟我们的sql
类型的select
语句是非常相似的,比如下图中的三个Pod
资源中,我们就可以进行select
。name color
等于red
,就是它的颜色是红色的,我们也可以看到,只有两个被选中了,因为只有他们的label
是红色的,另外一个label
中写的color
等于yellow
,也就是它的颜色是黄色,是不会被选中的
通过label
,Kubernetes
的API
层就可以对这些资源进行一个筛选,那这些筛选也是 Kubernetes
对资源的集合所表达默认的一种方式。
例如说,我们刚刚介绍的Deployment
,它可能是代表一组的Pod
,它是一组Pod
的抽象,一组Pod
就是通过label selector
来表达的。当然我们刚才讲到说service
对应的一组Pod
,就是一个service
要对应一个或者多个的Pod
,来对它们进行统一的访问,这个描述也是通过label selector
来进行select
选取的一组Pod
所以可以看到label
是一个非常核心的Kubernetes API
的概念,我们在接下来的课程中也会着重地去讲解和介绍label
这个概念,以及如何更好地去使用它
3.5 本节总结
Kubernetes
是一个自动化的容器编排平台,它负责应用的部署、应用的弹性以及应用的管理,这些都是基于容器的Kubernetes
架构是一个比较典型的二层架构和server-client
架构;
4 理解Pod和容器设计模式
4.1 为什么需要Pod
4.1.1 容器的基本概念
现在来看第一个问题:为什么需要Pod
?我们知道Pod
是Kubernetes
项目里面一个非常重要的概念,也是非常重要的一个原子调度单位,但是为什么我们会需要这样一个概念呢?我们在使用容器Docker
的时候,也没有这个说法。其实如果要理解Pod
,我们首先要理解容器,所以首先来回顾一下容器的概念:
容器的本质实际上是一个进程,是一个视图被隔离,资源受限的进程
容器里面PID=1
的进程就是应用本身,这意味着管理虚拟机等于管理基础设施,因为我们是在管理机器,但管理容器却等于直接管理应用本身。这也是之前说过的不可变基础设施的一个最佳体现,这个时候,你的应用就等于你的基础设施,它一定是不可变的
在以上面的例子为前提的情况下,Kubernetes
又是什么呢?我们知道,很多人都说Kubernetes
是云时代的操作系统,这个非常有意思,因为如果以此类推,容器镜像就是这个操作系统的软件安装包,它们之间是这样的一个类比关系
4.1.2 真实操作系统里的例子
如果说Kubernetes
就是操作系统的话,那么我们不妨看一下真实的操作系统的例子
例子里面有一个程序叫做Helloworld
,这个Helloworld
程序实际上是由一组进程组成的,需要注意一下,这里说的进程实际上等同于Linux
中的线程
因为Linux
中的线程是轻量级进程,所以如果从Linux
系统中去查看Helloworld
中的pstree
,将会看到这个Helloworld
实际上是由四个线程组成的,分别是{api、main、log、compute}
。也就是说,四个这样的线程共同协作,共享Helloworld
程序的资源,组成了Helloworld
程序的真实工作情况
这是操作系统里面进程组或者线程组中一个非常真实的例子,以上就是进程组的一个概念
那么大家不妨思考一下,在真实的操作系统里面,一个程序往往是根据进程组来进行管理的。Kubernetes
把它类比为一个操作系统,比如说Linux
。针对于容器我们前面提到可以类比为进程,就是前面的Linux
线程。那么Pod
又是什么呢?实际上Pod
就是我们刚刚提到的进程组,也就是Linux
里的线程组
4.1.3 进程组概念
说到进程组,首先建议大家至少有个概念上的理解,然后我们再详细的解释一下
还是前面那个例子:Helloworld
程序由四个进程组成,这些进程之间会共享一些资源和文件。那么现在有一个问题:假如说现在把Helloworld
程序用容器跑起来,你会怎么去做?
当然,最自然的一个解法就是,我现在就启动一个Docker
容器,里面运行四个进程。可是这样会有一个问题,这种情况下容器里面PID=1
的进程该是谁? 比如说,它应该是我的main
进程,那么问题来了,“谁”又负责去管理剩余的3
个进程呢?
这个核心问题在于,容器的设计本身是一种“单进程”模型,不是说容器里只能起一个进程,由于容器的应用等于进程,所以只能去管理PID=1
的这个进程,其他再起来的进程其实是一个托管状态。 所以说服务应用进程本身就具有“进程管理”的能力
比如说Helloworld
的程序有system
的能力,或者直接把容器里PID=1
的进程直接改成systemd
,否则这个应用,或者是容器是没有办法去管理很多个进程的。因为PID=1
进程是应用本身,如果现在把这个PID=1
的进程给kill
了,或者它自己运行过程中死掉了,那么剩下三个进程的资源就没有人回收了,这个是非常非常严重的一个问题
而反过来真的把这个应用本身改成了systemd
,或者在容器里面运行了一个systemd
,将会导致另外一个问题:使得管理容器,不再是管理应用本身了,而等于是管理systemd
,这里的问题就非常明显了。比如说我这个容器里面run
的程序或者进程是systemd
,那么接下来,这个应用是不是退出了?是不是fail
了?是不是出现异常失败了?实际上是没办法直接知道的,因为容器管理的是systemd
。这就是为什么在容器里面运行一个复杂程序往往比较困难的一个原因
这里再帮大家梳理一下:由于容器实际上是一个“单进程”模型,所以如果你在容器里启动多个进程,只有一个可以作为PID=1
的进程,而这时候,如果这个PID=1
的进程挂了,或者说失败退出了,那么其他三个进程就会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资源,这是一个非常不好的情况
注意:Linux
容器的“单进程”模型,指的是容器的生命周期等同于PID=1
的进程(容器应用进程)的生命周期,而不是说容器里不能创建多进程。当然,一般情况下,容器应用进程并不具备进程管理能力,所以你通过exec
或者ssh
在容器里创建的其他进程,一旦异常退出(比如ssh
终止)是很容易变成孤儿进程的
反过来,其实可以在容器里面run
一个systemd
,用它来管理其他所有的进程。这样会产生第二个问题:实际上没办法直接管理我的应用了,因为我的应用被systemd
给接管了,那么这个时候应用状态的生命周期就不等于容器生命周期。这个管理模型实际上是非常非常复杂的。
4.1.4 Pod = “进程组”
在kubernetes
里面,Pod
实际上正是kubernetes
项目为你抽象出来的一个可以类比为进程组的概念
前面提到的,由四个进程共同组成的一个应用Helloworld
,在Kubernetes
里面,实际上会被定义为一个拥有四个容器的Pod
,这个概念大家一定要非常仔细的理解
就是说现在有四个职责不同、相互协作的进程,需要放在容器里去运行,在Kubernetes
里面并不会把它们放到一个容器里,因为这里会遇到两个问题。那么在Kubernetes
里会怎么去做呢?它会把四个独立的进程分别用四个独立的容器启动起来,然后把它们定义在一个Pod
里面
所以当Kubernetes
把Helloworld
给拉起来的时候,你实际上会看到四个容器,它们共享了某些资源,这些资源都属于Pod
,所以我们说Pod
在Kubernetes
里面只有一个逻辑单位,没有一个真实的东西对应说这个就是Pod
,不会有的。真正起来在物理上存在的东西,就是四个容器。这四个容器,或者说是多个容器的组合就叫做Pod
。并且还有一个概念一定要非常明确,Pod
是Kubernetes
分配资源的一个单位,因为里面的容器要共享某些资源,所以Pod
也是Kubernetes
的原子调度单位
上面提到的Pod
设计,也不是Kubernetes
项目自己想出来的, 而是早在Google
研发Borg
的时候,就已经发现了这样一个问题。这个在Borg paper
里面有非常非常明确的描述。简单来说Google
工程师发现在Borg
下面部署应用时,很多场景下都存在着类似于“进程与进程组”的关系。更具体的是,这些应用之前往往有着密切的协作关系,使得它们必须部署在同一台机器上并且共享某些信息
以上就是进程组的概念,也是 Pod 的用法
4.1.5 为什么Pod必须是原子调度单位?
可能到这里大家会有一些问题:虽然了解这个东西是一个进程组,但是为什么要把Pod
本身作为一个概念抽象出来呢?或者说能不能通过调度把Pod
这个事情给解决掉呢?为什么Pod
必须是Kubernetes
里面的原子调度单位?
下面我们通过一个例子来解释。
假如现在有两个容器,它们是紧密协作的,所以它们应该被部署在一个Pod
里面。具体来说,第一个容器叫做App
,就是业务容器,它会写日志文件;第二个容器叫做 LogCollector
,它会把刚刚App
容器写的日志文件转发到后端的ElasticSearch
中
两个容器的资源需求是这样的:App
容器需要1G
内存,LogCollector
需要0.5G
内存,而当前集群环境的可用内存是这样一个情况:Node_A:1.25G
内存,Node_B:2G
内存。
假如说现在没有Pod
概念,就只有两个容器,这两个容器要紧密协作、运行在一台机器上。可是,如果调度器先把App
调度到了Node_A
上面,接下来会怎么样呢?这时你会发现:LogCollector
实际上是没办法调度到Node_A
上的,因为资源不够。其实此时整个应用本身就已经出问题了,调度已经失败了,必须去重新调度
以上就是一个非常典型的成组调度失败的例子。英文叫做:Task co-scheduling
问题,这个问题不是说不能解,在很多项目里面,这样的问题都有解法
比如说在Mesos
里面,它会做一个事情,叫做资源囤积(resource hoarding):即当所有设置了Affinity
约束的任务都达到时,才开始统一调度,这是一个非常典型的成组调度的解法
所以上面提到的“App”和“LogCollector”这两个容器,在Mesos
里面,他们不会说立刻调度,而是等两个容器都提交完成,才开始统一调度。这样也会带来新的问题,首先调度效率会损失,因为需要等待。由于需要等还会有外一个情况会出现,就是产生死锁,就是互相等待的一个情况。这些机制在Mesos
里都是需要解决的,也带来了额外的复杂度
另一种解法是Google
的解法。它在Omega
系统(就是Borg
下一代)里面,做了一个非常复杂且非常厉害的解法,叫做乐观调度。比如说:不管这些冲突的异常情况,先调度,同时设置一个非常精妙的回滚机制,这样经过冲突后,通过回滚来解决问题。这个方式相对来说要更加优雅,也更加高效,但是它的实现机制是非常复杂的。这个有很多人也能理解,就是悲观锁的设置一定比乐观锁要简单
而像这样的一个Task co-scheduling
问题,在Kubernetes
里,就直接通过Pod
这样一个概念去解决了。因为在Kubernetes
里,这样的一个App
容器和LogCollector
容器一定是属于一个Pod
的,它们在调度时必然是以一个Pod
为单位进行调度,所以这个问题是根本不存在的
4.1.6 再次理解Pod
在讲了前面这些知识点之后,我们来再次理解一下Pod
,首先Pod
里面的容器是“超亲密关系”
这里有个“超”字需要大家理解,正常来说,有一种关系叫做亲密关系,这个亲密关系是一定可以通过调度来解决的
比如说现在有两个Pod
,它们需要运行在同一台宿主机上,那这样就属于亲密关系,调度器一定是可以帮助去做的。但是对于超亲密关系来说,有一个问题,即它必须通过Pod
来解决。因为如果超亲密关系赋予不了,那么整个Pod
或者说是整个应用都无法启动
什么叫做超亲密关系呢?大概分为以下几类:
- 比如说两个进程之间会发生文件交换,前面提到的例子就是这样,一个写日志,一个读日志
- 两个进程之间需要通过
localhost
或者说是本地的Socket
去进行通信,这种本地通信也是超亲密关系 - 这两个容器或者是微服务之间,需要发生非常频繁的
RPC
调用,出于性能的考虑,也希望它们是超亲密关系 - 两个容器或者是应用,它们需要共享某些
Linux Namespace
。最简单常见的一个例子,就是我有一个容器需要加入另一个容器的Network Namespace
。这样我就能看到另一个容器的网络设备,和它的网络信息
像以上几种关系都属于超亲密关系,它们都是在Kubernetes
中会通过Pod
的概念去解决的
现在我们理解了Pod
这样的概念设计,理解了为什么需要Pod
。它解决了两个问题:
- 我们怎么去描述超亲密关系
- 我们怎么去对超亲密关系的容器或者说是业务去做统一调度,这是
Pod
最主要的一个诉求
4.2 Pod的实现机制
4.2.1 Pod要解决的问题
像Pod
这样一个东西,本身是一个逻辑概念。那在机器上,它究竟是怎么实现的呢?这就是我们要解释的第二个问题
既然说Pod
要解决这个问题,核心就在于如何让一个Pod
里的多个容器之间最高效的共享某些资源和数据
因为容器之间原本是被Linux Namespace
和cgroups
隔开的,所以现在实际要解决的是怎么去打破这个隔离,然后共享某些事情和某些信息。这就是Pod
的设计要解决的核心问题所在
所以说具体的解法分为两个部分:网络和存储
共享网络
第一个问题是Pod
里的多个容器怎么去共享网络?下面是个例子:
比如说现在有一个Pod
,其中包含了一个容器A
和一个容器B
,它们两个就要共享Network Namespace
。在Kubernetes
里的解法是这样的:它会在每个Pod
里,额外起一个Infra container
小容器来共享整个Pod
的Network Namespace
Infra container
是一个非常小的镜像,大概100~200KB
左右,是一个汇编语言写的、永远处于“暂停”状态的容器。由于有了这样一个Infra container
之后,其他所有容器都会通过Join Namespace
的方式加入到Infra container
的Network Namespace
中
所以说一个Pod
里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于Pod
第一次创建的这个Infra container
。这就是Pod
解决网络共享的一个解法
在Pod
里面,一定有一个IP
地址,是这个Pod
的Network Namespace
对应的地址,也是这个Infra container
的IP
地址。所以大家看到的都是一份,而其他所有网络资源,都是一个Pod
一份,并且被Pod
中的所有容器共享。这就是Pod
的网络实现方式
由于需要有一个相当于说中间的容器存在,所以整个Pod
里面,必然是Infra container
第一个启动。并且整个Pod
的生命周期是等同于Infra container
的生命周期的,与容器A
和B
是无关的。这也是为什么在Kubernetes
里面,它是允许去单独更新Pod
里的某一个镜像的,即:做这个操作,整个Pod
不会重建,也不会重启,这是非常重要的一个设计
共享存储
第二问题:Pod
怎么去共享存储?Pod
共享存储就相对比较简单
比如说现在有两个容器,一个是Nginx
,另外一个是非常普通的容器,在Nginx
里放一些文件,让我能通过Nginx
访问到。所以它需要去share
这个目录。我share
文件或者是share
目录在Pod
里面是非常简单的,实际上就是把volume
变成了Pod level
。然后所有容器,就是所有同属于一个Pod
的容器,他们共享所有的volume
比如说上图的例子,这个volume
叫做shared-data
,它是属于Pod level
的,所以在每一个容器里可以直接声明:要挂载shared-data
这个volume
,只要你声明了你挂载这个volume
,你在容器里去看这个目录,实际上大家看到的就是同一份。这个就是Kubernetes
通过Pod
来给容器共享存储的一个做法
所以在之前的例子中,应用容器App
写了日志,只要这个日志是写在一个volume
中,只要声明挂载了同样的volume
,这个volume
就可以立刻被另外一个LogCollector
容器给看到。以上就是Pod
实现存储的方式。
4.3 详解容器设计模式
现在我们知道了为什么需要Pod
,也了解了Pod
这个东西到底是怎么实现的。最后,以此为基础,详细介绍一下Kubernetes
非常提倡的一个概念,叫做容器设计模式。
举例
接下来将会用一个例子来给大家进行讲解
比如我现在有一个非常常见的一个诉求:我现在要发布一个应用,这个应用是JAVA
写的,有一个WAR
包需要把它放到Tomcat
的web APP
目录下面,这样就可以把它启动起来了。可是像这样一个WAR
包或Tomcat
这样一个容器的话,怎么去做,怎么去发布?这里面有几种做法
- 第一种方式:可以把
WAR
包和Tomcat
打包放进一个镜像里面。但是这样带来一个问题,就是现在这个镜像实际上揉进了两个东西。那么接下来,无论是我要更新WAR
包还是说我要更新Tomcat
,都要重新做一个新的镜像,这是比较麻烦的 - 第二种方式:就是镜像里面只打包
Tomcat
。它就是一个Tomcat
,但是需要使用数据卷的方式,比如说hostPath
,从宿主机上把WAR
包挂载进我们Tomcat
容器中,挂到我的web APP
目录下面,这样把这个容器启用起来之后,里面就能用了
但是这时会发现一个问题:这种做法一定需要维护一套分布式存储系统。因为这个容器可能第一次启动是在宿主机A
上面,第二次重新启动就可能跑到B
上去了,容器它是一个可迁移的东西,它的状态是不保持的。所以必须维护一套分布式存储系统,使容器不管是在A
还是在B
上,都可以找到这个WAR
包,找到这个数据
注意,即使有了分布式存储系统做Volume
,你还需要负责维护Volume
里的WAR
包。比如:你需要单独写一套Kubernetes Volume
插件,用来在每次Pod
启动之前,把应用启动所需的WAR
包下载到这个Volume
里,然后才能被应用挂载使用到
这样操作带来的复杂程度还是比较高的,且这个容器本身必须依赖于一套持久化的存储插件(用来管理Volume
里的WAR
包内容)
InitContainer
所以大家有没有考虑过,像这样的组合方式,有没有更加通用的方法?哪怕在本地Kubernetes
上,没有分布式存储的情况下也能用、能玩、能发布
实际上方法是有的,在Kubernetes
里面,像这样的组合方式,叫做Init Container
还是同样一个例子:在上图的yaml
里,首先定义一个Init Container
,它只做一件事情,就是把WAR
包从镜像里拷贝到一个Volume
里面,它做完这个操作就退出了,所以Init Container
会比用户容器先启动,并且严格按照定义顺序来依次执行
然后,这个关键在于刚刚拷贝到的这样一个目的目录APP
目录,实际上是一个Volume
。而我们前面提到,一个Pod
里面的多个容器,它们是可以共享Volume
的,所以现在这个Tomcat
容器,只是打包了一个Tomcat
镜像。但在启动的时候,要声明使用APP
目录作为我的Volume
,并且要把它们挂载在Web APP
目录下面
而这个时候,由于前面已经运行过了一个Init Container
,已经执行完拷贝操作了,所以这个Volume
里面已经存在了应用的WAR
包:就是sample.war
,绝对已经存在这个Volume
里面了。等到第二步执行启动这个Tomcat
容器的时候,去挂这个Volume
,一定能在里面找到前面拷贝来的sample.war
所以可以这样去描述:这个Pod
就是一个自包含的,可以把这一个Pod
在全世界任何一个Kubernetes
上面都顺利启用起来。不用担心没有分布式存储、Volume
不是持久化的,它一定是可以公布的
所以这是一个通过组合两个不同角色的容器,并且按照这样一些像Init Container
这样一种编排方式,统一的去打包这样一个应用,把它用Pod
来去做的非常典型的一个例子。像这样的一个概念,在Kubernetes
里面就是一个非常经典的容器设计模式,叫做:“Sidecar”。
4.3.1 容器设计模式:Sidecar
什么是Sidecar
?就是说其实在Pod
里面,可以定义一些专门的容器,来执行主业务容器所需要的一些辅助工作,比如我们前面举的例子,其实就干了一个事儿,这个Init Container
,它就是一个Sidecar
,它只负责把镜像里的WAR
包拷贝到共享目录里面,以便被Tomcat
能够用起来
其它有哪些操作呢?比如说:
- 原本需要在容器里面执行
SSH
需要干的一些事情,可以写脚本、一些前置的条件,其实都可以通过像Init Container
或者另外像Sidecar
的方式去解决 - 当然还有一个典型例子就是我的日志收集,日志收集本身是一个进程,是一个小容器,那么就可以把它打包进
Pod
里面去做这个收集工作 - 还有一个非常重要的东西就是
Debug
应用,实际上现在Debug
整个应用都可以在应用Pod
里面再次定义一个额外的小的Container
,它可以去exec
应用pod
的namespace
- 查看其他容器的工作状态,这也是它可以做的事情。不再需要去
SSH
登陆到容器里去看,只要把监控组件装到额外的小容器里面就可以了,然后把它作为一个Sidecar
启动起来,跟主业务容器进行协作,所以同样业务监控也都可以通过Sidecar
方式来去做
这种做法一个非常明显的优势就是在于其实将辅助功能从我的业务容器解耦了,所以我就能够独立发布Sidecar
容器,并且更重要的是这个能力是可以重用的,即同样的一个监控Sidecar
或者日志Sidecar
,可以被全公司的人共用的。这就是设计模式的一个威力
Sidecar:应用与日志收集
接下来,我们再详细细化一下Sidecar
这样一个模式,它还有一些其他的场景
比如说前面提到的应用日志收集,业务容器将日志写在一个Volume
里面,而由于Volume
在Pod
里面是被共享的,所以日志容器 —— 即Sidecar
容器一定可以通过共享该Volume
,直接把日志文件读出来,然后存到远程存储里面,或者转发到另外一个例子。现在业界常用的Fluentd
日志进程或日志组件,基本上都是这样的工作方式
Sidecar:代理容器
Sidecar
的第二个用法,可以称作为代理容器Proxy
。什么叫做代理容器呢?
假如现在有个Pod
需要访问一个外部系统,或者一些外部服务,但是这些外部系统是一个集群,那么这个时候如何通过一个统一的、简单的方式,用一个IP
地址,就把这些集群都访问到?有一种方法就是:修改代码。因为代码里记录了这些集群的地址;另外还有一种解耦的方法,即通过Sidecar
代理容器
简单说,单独写一个这么小的Proxy
,用来处理对接外部的服务集群,它对外暴露出来只有一个IP
地址就可以了。所以接下来,业务容器主要访问Proxy
,然后由Proxy
去连接这些服务集群,这里的关键在于Pod
里面多个容器是通过localhost
直接通信的,因为它们同属于一个network Namespace
,网络视图都一样,所以它们俩通信localhost
,并没有性能损耗
所以说代理容器除了做了解耦之外,并不会降低性能,更重要的是,像这样一个代理容器的代码就又可以被全公司重用了
Sidecar:适配器容器
Sidecar
的第三个设计模式 —— 适配器容器Adapter
,什么叫Adapter
呢?
现在业务暴露出来的API
,比如说有个API
的一个格式是A
,但是现在有一个外部系统要去访问我的业务容器,它只知道的一种格式是API B
,所以要做一个工作,就是把业务容器怎么想办法改掉,要去改业务代码。但实际上,你可以通过一个Adapter
帮你来做这层转换
现在有个例子:现在业务容器暴露出来的监控接口是/metrics
,访问这个这个容器的metrics
的这个URL
就可以拿到了。可是现在,这个监控系统升级了,它访问的URL
是/health
,我只认得暴露出health
健康检查的URL
,才能去做监控,metrics
不认识。那这个怎么办?那就需要改代码了,但可以不去改代码,而是额外写一个Adapter
,用来把所有对health
的这个请求转发给metrics
就可以了,所以这个Adapter
对外暴露的是health
这样一个监控的URL
,这就可以了,你的业务就又可以工作了
这样的关键还在于Pod
之中的容器是通过localhost
直接通信的,所以没有性能损耗,并且这样一个Adapter
容器可以被全公司重用起来,这些都是设计模式给我们带来的好处
4.4 本节总结
Pod
是Kubernetes
项目里实现“容器设计模式”的核心机制
“容器设计模式”是Google Borg
的大规模容器集群管理最佳实践之一,也是Kubernetes
进行复杂应用编排的基础依赖之一
所有“设计模式”的本质都是:解耦和重用
Pod
与容器设计模式是Kubernetes
体系里面最重要的一个基础知识点,希望读者能够仔细揣摩和掌握。在这里,我建议你去重新审视一下之前自己公司或者团队里使用Pod
方式,是不是或多或少采用了所谓“富容器”这种设计呢?这种设计,只是一种过渡形态,会培养出很多非常不好的运维习惯。我强烈建议你逐渐采用容器设计模式的思想对富容器进行解耦,将它们拆分成多个容器组成一个Pod
。这也正是当前阿里巴巴“全面上云”战役中正在全力推进的一项重要的工作内容
5 Kubernetes 网络模型进阶
5.1 Service
- 一组Pod组成一组功能后端
- 定义一个稳定的虚IP作为访问前端,一般还附赠一个DNS域名,Client无需感知Pod的细节
- kube-proxy是实现核心,隐藏了大量复杂性,通过apiserver监控Pod/Service的变化,反馈到LB配置中
- LB的实现机制与目标解耦,可以是个用户态进程,也可以是一堆精心设计的Rules(iptables/ipvs)
如何实现一个LVS版Service
1 | # 第一步,绑定VIP到本地(欺骗内核) |