docker到kubernets概念详解

278 阅读5分钟
各个pod之间的通信:

Flannel是针对kubernetes设计的一个网络规划服务,

docker run -it bustbox /bin/sh

这个命令是Docker项目最重要的一个操作,即大名鼎鼎的docker run。

而-it参数告诉了Docker项目在启动容器后,需要给我们分配一个文本输入/输出环境,也就是TTY,跟容器的标准输入相关联,这样我们就可以和这个Docker容器进行交互了。而/bin/sh就是我们要在Docker容器里运行的程序。

所以,上面这条指令翻译成人类的语言就是:请帮我启动一个容器,在容器里执行/bin/sh,并且给我分配一个命令行终端跟这个容器交互。

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

每当我们在宿主机上运行了一个/bin/sh程序,操作系统都会给它分配一个进程编号,比如PID=100。这个编号是进程的唯一标识,就像员工的工牌一样。所以PID=100,可以粗略地理解为这个/bin/sh是我们公司里的第100号员工,而第1号员工就自然是比尔 · 盖茨这样统领全局的人物。

而现在,我们要通过Docker把这个/bin/sh程序运行在一个容器当中。这时候,Docker就会在这个第100号员工入职时给他施一个“障眼法”,让他永远看不到前面的其他99个员工,更看不到比尔 · 盖茨。这样,他就会错误地以为自己就是公司里的第1号员工。

而现在,我们要通过Docker把这个/bin/sh程序运行在一个容器当中。这时候,Docker就会在这个第100号员工入职时给他施一个“障眼法”,让他永远看不到前面的其他99个员工,更看不到比尔 · 盖茨。这样,他就会错误地以为自己就是公司里的第1号员工。

这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如PID=1。可实际上,他们在宿主机的操作系统里,还是原来的第100号进程。

这种技术,就是Linux里面的Namespace机制

NameSpace是Linux创建新进程的一个可选参数

int pid = clone(main_function, stack_size, SIGCHLD, NULL);

int pid = clone(main_function, stacj_size, CLONE_NEWPID | SIGCHLE, NULL)

参数中指定CLONE_NEWPID参数;新创建的进程将会“看到”一个全新的进程空间,在这个进程空间里,PID是1。但在宿主机真实的进程空间中。PID还是真实数值。

Docker容器,实际是在创建容器进程时,指定进程所需要启用的一组NameSpace参数。这样容器只能看到当前Namespace所限定的资源、文件、设备、状态或配置。而对于宿主器以及其他不相关的程序,完全看不到。

容器,是一种特殊进程。

Docker帮用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker为它们加上了各种各样的Namespace参数。

隔离与限制

进程都由宿主机操作系统统一管理。

Docker更多是旁路式辅助和管理工作。

虚拟机因为真实存在,必须运行Guest OS才能执行用户的应用进程,带来额外的资源消耗和占用。

因此敏捷、高性能是容器相对虚拟机最大优势;

但Linux Namespace的隔离机制不够彻底。使用多个操作系统内核,因此在windows宿主机运行Linux容器,或者在低版本Linux上运行高版本Linux容器都行不通。

且时间等共享,不符合预期。暴露的攻击面较大。

PID Namespce:

与其他进程依然是平等的竞争关系,使用的资源,可以随时被其它进程占用。

Linux Cgroups就是Linux内核中用来为进程设置资源限制的一个重要功能

作用:限制一个进程组能够使用的资源上限。

一个子系统目录加上一组资源限制文件的组合。

而对于Docker等Linux容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的PID填写到对应控制组的tasks文件中就可以了。

$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash

$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_period_us 
100000
$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_quota_us 
20000

因为容器是一个单进程模型,无法同时运行2个不同应用。

容器本身的审计,希望容器和应用能够同生命周期。

因此容器执行top指令,显示的信息是宿主机的COU和内存数据。

深入理解容器镜像

容器中的进程看到的文件系统是什么样子?

开启Mount Namespce,容器进程看到的文件系统和宿主机一样;

需要重新挂载mount 目录才可以生效

而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。

所以,一个最常见的rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如/bin,/etc,/proc等等:

对Docker项目来说,它最核心的原理实际上就是为待创建的用户进程:

  1. 启用Linux Namespace配置;
  2. 设置指定的Cgroups参数;
  3. 切换进程的根目录(Change Root)。

rootfs知识一个操作系统包含的文件、配置喝目录。不包含操作系统内核,两部分分开存放。

因此所有容器共享宿主机操作系统的内核。

由于rootfs的存在,容器才有了一个被反复宣传至今的重要特性:一致性。

由于rootfs里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。

事实上,对于大多数开发者而言,他们对应用依赖的理解,一直局限在编程语言层面。比如Golang的Godeps.json。但实际上,一个一直以来很容易被忽视的事实是,对一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。

有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。

这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。

不过,这时你可能已经发现了另一个非常棘手的问题:难道我每开发一个应用,或者升级一下现有的应用,都要重复制作一次rootfs吗?

Docker在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量rootfs。

rootfs。它只是一个操作系统的所有文件和目录,并不包含内核,最多也就几百兆。而相比之下,传统虚拟机的镜像大多是一个磁盘的“快照”,磁盘有多大,镜像就至少有多大

通过结合使用Mount Namespace和rootfs,容器就能够为进程构建出一个完善的文件系统隔离环境。当然,这个功能的实现还必须感谢chroot和pivot_root这两个系统调用切换进程根目录的能力。

而在rootfs的基础上,Docker公司创新性地提出了使用多个增量rootfs联合挂载一个完整rootfs的方案,这就是容器镜像中“层”的概念。

一旦这个镜像被发布,那么你在全世界的任何一个地方下载这个镜像,得到的内容都完全一致,可以完全复现这个镜像制作者当初的完整环境。这,就是容器技术“强一致性”的重要体现

容器镜像的发明,不仅打通了“开发-测试-部署”流程的每一个环节,更重要的是:

容器镜像将会成为未来软件的主流发布方式。

重新認識Docker容器
docker build -t helloword .

docker build会自动加载当前目录下的Dockerfile文件,然后按照顺序,执行文件中的原语。而这个过程,实际上可以等同于Docker使用基础镜像启动了一个容器,然后在容器中依次执行Dockerfile中的原语。

docker build 操作完成后,可以通过docker images命令查看结果

docker image ls
REPOSITORY            TAG                 IMAGE ID
helloworld         latest              653287cdf998

使用这个镜像,通过docker run命令启动容器

docker run -p 4000:80 helloworld

在这一句命令中,镜像名helloworld后面,我什么都不用写,因为在Dockerfile中已经指定了CMD。否则,我就得把进程的启动命令加在后面:

docker run -p 4000:8 helloworld python app.py

容器启动后,可以使用docker ps命令看到:

docker ps

通过-p 4000:80告诉了Docker,请把容器内的80端口映射在宿主机的4000端口上

这样做的目的是,只要访问宿主机的4000端口,我就可以看到容器里应用返回的结果

docker push

docker push geektime/helloworld:v1

这样,可以把这个镜像上传到Docker Hub上

此外,使用docker commit指令,把一个正在运行的容器,直接提交为一个镜像。

容器运行起来后,有做了一些操作,需要把操作结果保存到镜像中。

docker exec -it 4753c4db9e54 /bin/sh

docker commit 4753c4db9e54 geektime/helloworld:v2

docker exec是怎么进入容器中的?

Namespace信息在宿主机上是确实以文件的方式存在的

查看docker容器的进程号
docker inspect --format '{{ .State.pid }}' 4753c4db9e54

此时查看宿主机的proc文件,则是该进程的所有Namespace对应的文件

ls -l /proc/25686/ns

这也就意味着:一个进程,可以选择加入到某个进程已有的Namespace当中,从而达到“进入”这个进程所在容器的目的,这正是docker exec的实现原理。

kubernetes的本质

在Kubernetes项目中,kubelet主要负责同容器运行时(比如Docker项目)打交道。而这个交互所依赖的,是一个称作CRI(Container Runtime Interface)的远程调用接口,这个接口定义了容器运行时的各项核心操作,比如:启动一个容器需要的所有参数。

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

kubelet的另一个重要功能,则是调用网络插件和存储插件为容器配置网络和持久化存储。这两个插件与kubelet进行交互的接口,分别是CNI(Container Networking Interface)和CSI(Container Storage Interface)。

运行在大规模集群中的各种任务之间,实际上存在着各种各样的关系。这些关系的处理,才是作业编排和管理系统最困难的地方。

Kubernetes项目最主要的设计思想是,从更宏观的角度,以统一的方式来定义任务之间的各种关系,并且为将来支持更多种类的关系留有余地。

比如,Kubernetes项目对容器间的“访问”进行了分类,首先总结出了一类非常常见的“紧密交互”的关系,即:这些应用之间需要非常频繁的交互和访问;又或者,它们会直接通过本地文件进行信息交换。

在常规环境下,这些应用往往会被直接部署在同一台机器上,通过Localhost通信,通过本地磁盘目录交换文件。而在Kubernetes项目中,这些容器则会被划分为一个“Pod”,Pod里的容器共享同一个Network Namespace、同一组数据卷,从而达到高效率交换信息的目的。

而对于另外一种更为常见的需求,比如Web应用与数据库之间的访问关系,Kubernetes项目则提供了一种叫作“Service”的服务。像这样的两个应用,往往故意不部署在同一台机器上,这样即使Web应用所在的机器宕机了,数据库也完全不受影响。可是,我们知道,对于一个容器来说,它的IP地址等信息不是固定的,那么Web应用又怎么找到数据库容器的Pod呢?

所以,Kubernetes项目的做法是给Pod绑定一个Service服务,而Service服务声明的IP地址等信息是“终生不变”的。这个Service服务的主要作用,就是作为Pod的代理入口(Portal),从而代替Pod对外暴露一个固定的网络地址

这样,对于Web应用的Pod来说,最需要关心的就是数据库pod的Service信息。Service后端真正代理的pod的IP地址、端口等信息的自动更新、维护,则是kubernetes项目的职责

首先容器紧密协作,扩展出Pod;有了Pod后,希望一次启动多个应用的实例,这样需要Deployment这个Pod的多实例管理器;有了这样一组相同的Pod后,需要通过一个固定的IP和端口以负载均衡的方式访问,于是有了Service。

不同Pod之间不仅有“访问关系”,还要求在发起时加上授权信息。最典型的例子就是Web应用访问数据库需要用户名密码。

kebernetes项目提供了一个叫做Secret的对象,其实是一个保存在Etcd里的键值对数据。

把Credential信息以Secret的方式存在Etcd中,Kubernetes会在指定的pod启动时,自动把Secret里的数据以Volume的方式挂载到容器里。

除了应用与应用之间的关系外,应用运行的形态是影响“如何容器化这个应用”的第二个重要因素。

为此,Kubernetes定义了新的、基于Pod改进后的对象。比如Job,用来描述一次性运行的Pod(比如,大数据任务);再比如DaemonSet,用来描述每个宿主机上必须且只能运行一个副本的守护进程服务;又比如CronJob,则用于描述定时任务等等。

如此种种,正是Kubernetes项目定义容器间关系和形态的主要方法。

这种使用方法,就是所谓的“声明式API”。这种API对应的“编排对象”和“服务对象”,都是Kubernetes项目中的API对象(API Object)。

Kubernetes项目如何启动一个容器化任务呢?

比如,我现在已经制作好了一个Nginx容器镜像,希望让平台帮我启动这个镜像。并且,我要求平台帮我运行两个完全相同的Nginx副本,以负载均衡的方式共同对外提供服务。

编写YAML文件:nginx-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 2 // pod的副本数
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx // 主体部分
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

在上面这个YAML文件中,我们定义了一个Deployment对象

kubectl create -f nginx-deployment.yaml

而Kubernetes项目所擅长的,是按照用户的意愿和整个系统的规则,完全自动化地处理好容器之间的各种关系。这种功能,就是我们经常听到的一个概念:编排。

Kubernetes项目的本质,为用户提供一个具有普遍意义的容器编排工具。

一键部署利器之kubeadm

要真正发挥容器技术的实力,你就不能仅仅局限于对Linux容器本身的钻研和使用。

如何使用这些技术来“容器化”你的应用。

kubeadm

# 创建一个Master节点
$ kubeadm init

# 将一个Node节点加入到当前集群中
$ kubeadm join <Master节点的IP和端口>
我的第一个容器化应用

编写配置文件,YAML文件;

kubectl create -f 配置文件

pod是kebernetes世界里的应用,由多个容器组成

查看yaml运行起来的状态

kubectl get pods -l app=nginx

更新镜像

kebectl replace -f nginx-deployment.yaml

kubectl apply统一进行kebernets对象的创建和更新操作

kebectl apply -f 配置文件

可以通过kubectl get命令,查看两个pod被逐一更新的过程:

kubectl get pods

可以通过kebectl desctibe查看最新的pod

为什么我们需要pod

pod,是kubernetes项目中做小的API对象

容器的本质是进程;

容器想是系统里的exe安装包,kubernetes是操作系统。

进程组

首先,关于Pod最重要的一个事实是:它只是一个逻辑概念。

Pod,其实是一组共享了某些资源的容器

Pod里的所有容器,共享的是同一个Network Namespace,并且可以声明共享同一个Volume。

对于Pod里的容器A和容器B来说:

  • 它们可以直接使用localhost进行通信;
  • 它们看到的网络设备跟Infra容器看到的完全一样;
  • 一个Pod只有一个IP地址,也就是这个Pod的Network Namespace对应的IP地址;
  • 当然,其他的所有网络资源,都是一个Pod一份,并且被该Pod中的所有容器共享;
  • Pod的生命周期只跟Infra容器一致,而与容器A和B无关。

将来如果你要为Kubernetes开发一个网络插件时,应该重点考虑的是如何配置这个Pod的Network Namespace,而不是每一个用户容器如何使用你的网络配置,这是没有意义的。