一个正在运行的Linux容器,可以被一分为二地看待:
1.一组联合挂载在/var/lib/docker/aufs/mnt上的rootfs,这一部分我们称为容器镜像(Container Image)是容器的静态视图。
2.一个由Namespace+Cgroups构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime)是容器的动态视图
作为开发者,我们并不关心容器运行时的差异,因为,在整个"开发-测试-发布"的流程中,真正承载着容器信息进行传递的,是容器镜像,而不是容器运行时。
作为一个云服务商或者基础设施提供商,我只要能够将用户提交的Docker镜像以容器的方式运行起来,便可以将整个容器技术栈上的价值,汇聚在自己的节点上。
另外,从这个节点向Docker镜像制作者和使用者方向回溯,整个路径上的各个服务节点: 如CI/CD, 监控,安全,网络,存储,都可以有服务商发挥和盈利的地方。(通过容器镜像,和潜在用户,直接关联)
而容器编排技术,就是当前容器领域最核心的部分。当今的主流就是Google和Redhat公司所主导的Kubernetes项目。
Kubernetes项目的理论基础来自于Google的Borg系统。
Kubernete项目2014年立项,2015年Borg才对外公开发表
相比于Spanner,BigTable等相对上层的项目,Borg要承担的责任,是居于整个基础设施技术栈的最底层
图片来源http://malteschwarzkopf.de/research/assets/google-stack.pdf
这个图来自于Google Omega论文作者的博士论文,描绘了当时Google已经公开发表的整个基础设施栈,
Kubernetes项目的架构,跟它的原型项目Borg非常类似,都由Master和Node两种节点组成,而且这两种角色分别对应着控制节点和计算节点。
控制节点,(Master节点) 有三个紧密协作的独立组件组合而成,分别是负责API服务的kube-apiserver,负责调度的kube-scheduler, 负责容器编排的kube-controller-manager。整个集群的持久化数据,则有kube-apiserver处理后保存在Etcd中。
计算节点(Node节点),最核心的部分是一个叫做kubelet的组件。
在Kubernetes项目中,kubelet主要负责同容器运行时,(比如Docker项目)打交道,
这个交互所依赖的是一个称作CRI的远程调用接口(Container Runtime Interface)的远程调用接口,这个接口定义了容器运行时的各项核心操作。
这也是Kubernetes并不关心你部署的是什么容器运行时,使用的什么技术实现,只要你的这个容器运行时能够运行标准的容器镜像,它就可以通过实现CRI接入到Kubernetes项目当中。
具体的容器运行时,一般通过OCI这个容器运行时规范同底层的Linux操作系统进行交互,即把CRI请求翻译成对Linux操作系统的调用(操作Linux Namespace和Cgroups等 )
此外Kubernetes还通过gRPC协议同一个协议叫做Device Plugin的插件进行交互。这个插件,是Kubernetes项目用来管理GPU等宿主机物理设备的主要组件,也是基于Kubernetes项目进行机器学习训练,高性能作业支持等工作必须关注的功能。
而kubelet的另一个重要功能,则是调用网路插件和存储插件为容器配置网络和持久化存储。这两个插件,分别是CNI(Container Networking Interface)和CSI(Container Storage Interface)
尽管像Docker这样的容器镜像在Borg中是不存在的,自然Borg也不需要考虑同Docker交互,容器镜像进行管理,也不需要支持CRI,CNI,CSI等多容器技术。
但对于Kubernetes项目来说,Borg的指导意义在于master节点 : 即如何编排,管理,调度用户提交的作业。
Borg项目完全可以把Docker镜像看做是一种新的应用打包方式。这样,Borg团队过去在大规模作业管理与编排的经验就可以直接套用了。
在Borg项目中,运行在大规模集群中的各种任务之间,实际上存在着各种各样的关系。这些关系的处理,才是作业编排和管理最困难的地方。
举例来说,一个Web应用和数据库之间的访问关系,一个负载均衡器和它的后端服务之间的代理关系,一个门户应用与授权组件之间的调用关系。
在Compose项目中,可以为两个容器定义一个link,而Docker项目会负责维护这个link关系。具体做法是Docker会在Web容器中,将DB容器的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
Kubernetes项目最主要的设计思想是,从更宏观的角度,以统一的方式来定义任务之间的各种关系,并且为将来支持各种种类的关系留有余地。
Kubernetes对容器间的访问进行了分类:
第一种是 紧密交互关系:应用之间需要非常频繁的交互和访问,或者双方需要通过本地文件进行信息交互。
常规情况下,这些应用会直接部署在同一台机器上,通过localhost通信,通过本地磁盘交换文件。而在Kubernetes项目中,这些容器会被划分到一个Pod,Pod里的容器共享一个Network Namespace,同一个数据卷,从而达到高效率交换信息的目的。
Pod是Kubernetes项目中最基础的一个对象,源自于Google Borg论文中一个名叫ALLOC的设计
另一种需求,如Web应用与数据库之间的访问关系,Kubernetes项目则提供一种叫做“Service”的服务.
一般像这样两个应用,往往不部署在同一台机器上,这样即使Web应用宕机,数据库也完全不受影响。
Kubernetes的做法就是给Pod绑定一个Service服务,而service服务声明的IP地址信息是不变的。这个service服务的主要作用,就是作为Pod的代理入口(Portal),从而代替Pod对外暴露一个固定的网络地址。
如果两个不同pod之间不仅有访问关系,还要求在发起时加上授权信息。最典型的例子就是Web应用对数据库访问时需要Credential(用户名和密码)信息。Kubernetes项目提供一种叫做Secret的对象,以键值对的形式保存在Etcd里
除了应用与应用之间的关系外,应用运行的形态也是影响“如何容器化这个应用”的重要因素。
为此,Kubernetes定义了新的,基于Pod改进后的对象。比如job,用来描述一次性运行的Pod(比如,大数据任务);再比如DaemonSet,用来描述每个宿主机上必须只能运行一个副本的守护进程服务,有比如CronJob,用于描述定时任务等等。
Kubernetes并没有像其他项目那样,为每一个管理功能创建一个指令,然后在项目中实现其中的逻辑。
而是:
- 首先,通过编排对象,比如Pod, Job, CronJob等, 来描述你试图管理的应用;
- 然后,在为它定义一些服务对象,比如Service,Secret,Horizontal Pod AutoScaler(自动水平扩展器)等。来负责具体的平台级业务
这种使用方法,就是所谓的“声明式API”。这种API对于的编排对象和服务对象,都是Kubernetes项目中的API对象(API Object)。
例如声明两台Nginx服务器,编写如下代码在YAML文件
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
在上面的YAML文件中,我们定义了一个Deployment对象,主体是(spec.template包含内容)是一个使用Nginx镜像的Pod,而这个Pod的副本数是2.
然后执行
kubectl create -f nginx-deployment.yaml
这样两个完全相同的Nginx容器就被启动了