Docker 和 K8S

Docker

Docker 是什么?

Docker 是基于容器技术实现的,容器技术最开始是基于 Linux Container(简称 LXC)技术实现的,通过内核提供的 NamespaceCgroups 机制,实现了对应用程序的隔离以及物理资源的分配

Docker 在容器基础上发展出了一个完善的生态系统,它将容器视为一种打包格式,将应用程序所需的一切,比如依赖库、运行时环境等都集合在了在一起,使得一次构建,到处运行

它将开发与运维很好的融合在一起。开发人员可以很轻松的构建、打包、推送和运行应用程序。而且还允许我们将容器视为部署单元,以模块化的方式发布,降低了系统的运维管理难度。

Docker 基本概念

  • 镜像。

    镜像就像是包含了操作系统的一张光碟,它是一个模板文件,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。

    另外,镜像是一个分层的文件系统,通过一层层的组合,使得我们可以复用这些不同粒度的镜像文件。

  • 容器。

    容器是镜像的运行实例,我们可以把镜像看成是一个个的构建块,容器根据这些构建块搭建起了一个隔离的,拥有整个包的应用程序。每一个容器都是一个标准化单元,确保了在不同机子上也能拥有一致的行为。

  • 数据卷。

    Docker 对数据持久化的解决方案,数据不会随着容器结束而丢失,通过将宿主机的某一文件目录挂载到容器里来实现。在 Docker 里提供了三种方法来实现目录的挂载:

    • volumes:属于 Docker 管理的目录,无需自己创建目录。
    • bind mounts:自己指定的某个目录,需注意不同操作系统的文件路径格式。
    • tmpfs:仅存储在主机系统的内存中,不会持久保存在磁盘上。容器可以使用它来共享简单状态或非敏感的信息。
  • 网络

    • bridge 模式。

      Docker 在主机上会创建一个 docker0 的网桥,每当有容器要创建时,便会为容器分配一个独立的网卡,然后桥接到虚拟网桥上。这其实是一对虚拟网卡,一端放在容器里,另一端放在 docker0 网桥里。当一端有数据达到时,就会把数据包转发到另一端上,这就实现了网络通信。

    • host 模式

      在建一个容器时,一般会为容器分配一个独立的 Network Namespace 以进行网络隔离。如果我们使用了 Host 模式,则不再分配 Network Namespace,而是和宿主机共用一个 Network Namespace。此时容器将不再拥有自己的虚拟网卡、IP 和端口,而是和宿主机共用一个 IP 和端口。

    • none 模式

      使用 none 模式的容器拥有属于自己的 Network Namespace,但不做任何网络配置。它和宿主机以及其他容器是不互通的。如果需要和外部通信,则需要自定义网络驱动程序,自己添加网卡、配置 IP 等。

Docker 的底层技术是什么

Docker 在 Linux 的底层技术有:NameSpaces (资源隔离)Cgroups (资源限制)UnionFS (联合文件系统)。其中:

  • NameSpaces(资源隔离):将系统的全局资源通过抽象划分,使得在同一 NameSpaces 中的进程看起来拥有自己的全局资源。主要有 Mount namespaces(文件系统挂载)、Network namespaces(网络)、User namespaces(用户)等的资源隔离。
  • Cgroups(资源限制):对系统资源的限制,比如 CPU、内存等。
  • UnionFS(联合文件系统):一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。便于镜像的分层继承。

Docker 和虚拟机的区别

容器技术和虚拟机都提供了环境隔离的功能。不同的是。容器是运行在操作系统上的一个进程,它和其他应用程序是共享内核的,由操作系统提供虚拟化隔离功能;

而虚拟机则是完完全全起了个操作系统,将环境隔离的更加彻底。

centos 镜像几个G,但是 docker centos 镜像才几百兆,这是为什么?

一个完整的 Linux 操作系统包含 Linux 内核和 rootfs 根文件系统,即我们熟悉的 /dev、/proc/、/bin 等目录。我们平时看到的 CentOS 除了 rootfs,还会选装很多软件,服务,图形桌面等,所以 CentOS 镜像有好几个 G 也不足为奇。

而对于容器镜像而言,所有容器都是共享宿主机的 Linux 内核的,而对于 docker 镜像而言,docker 镜像只需要提供一个很小的 rootfs 即可,只需要包含最基本的命令,工具,程序库即可,所以 docker 镜像才会这么小。

镜像分层结构以及为什么要使用镜像分层结构

新的镜像其实是从 base 镜像一层一层叠加生成的,每安装一个软件,dockerfile 中使用 RUM 命令,就会在现有镜像的基础上增加一层,这样一层一层的叠加最后构成完整镜像。所以我们 docker pull 拉取镜像的时候发现是一层一层拉取得。

分层的好处:共享资源,比如有多个镜像都从相同的 base 镜像构建而来,那么 docker host 只需要在磁盘上保存一份 base 镜像,同时,内存中也只需要加载一份 base 镜像,就可以为所有容器服务了。

而且镜像的每一层都可以被共享。

Docker 常用命令有哪些?

  • 容器生命周期管理: run、start/stop/restart、kill、rm、pause/unpause
  • 容器操作:ps、inspect、top
  • 镜像仓库:login、pull、push、search
  • 本地镜像管理:images、rmi、tag、build、history

例如,当我们需要运行一个容器时,则可以执行:docker run nginx:test

  • attach 是直接进入容器启动命令的终端,不会启动新的进程;
  • exec 则是在容器里面打开新的终端,会启动新的进程;一般建议使用 exec 进入容器。

K8S

k8s 是什么,特点

k8s 是一个容器管理平台,负责容器的编排、管理、调度,支持故障转移/重启、自动扩缩容、服务发现/负载均衡、配置管理等功能,使得应用服务能从打包到部署再到监控能有一条完整的自动化流程。

k8s 相关概念

  • Container。
    即容器,通过镜像包含软件的运行环境,加上 namespace 的隔离功能,使得容器可以很方便的在任何地方运行。

  • Pod。
    k8s 使用 Pod 来管理容器,一个 Pod 可以包含一个或多个容器。它是一组紧密关联的容器集合,共享 PID、IPC、Network 和 UTS namespace,是 Kubernetes 调度的基本单位。

    Pod 内的多个容器共享网络和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。

  • Namespace。
    Namespace 是对一组资源和对象的抽象集合,比如可以用来将系统内部的对象划分为不同的项目组或用户组。常见的 pods, services, replication controllers 和 deployments 等都是属于某一个 namespace 的(默认是 default),而 node, persistentVolumes 等则不属于任何 namespace。

  • Service。
    Service 是应用服务的抽象,通过 labels 为应用提供负载均衡和服务发现。匹配 labels 的 Pod IP 和端口列表组成 endpoints,由 kube-proxy 负责将服务 IP 负载均衡到这些 endpoints 上。
    每个 Service 都会自动分配一个 cluster IP(仅在集群内部可访问的虚拟地址)和 DNS 名,其他容器可以通过该地址或 DNS 来访问服务,而不需要了解后端容器的运行。

  • Label。
    Label 是识别 Kubernetes 对象的标签,以 key/value 的方式附加到对象上。Label 不提供唯一性,并且实际上经常是很多对象(如 Pods)都使用相同的 label 来标志具体的应用。

  • Node。
    Node 是 Pod 真正运行的主机,可以是物理机,也可以是虚拟机。为了管理 Pod,每个 Node 节点上至少要运行 container runtime(比如 docker 或者 rkt)、kubelet 和 kube-proxy 服务。

K8S 架构图及交互流程

k8s 主要有 Master 节点和工作节点组成。主节点主要对集群做出全局决策(比如调度),以及检测和响应集群事件(例如资源不足,自动扩缩容);从节点负责维护运行的 Pod 并进行通信的网络代理。

Mater 节点主要有以下组件:

  • kube-apiserver:负责对外暴露 Kubernetes API。
  • etcd:作为保存 Kubernetes 所有集群数据的后台数据库。
  • kube-scheduler:在适当的时候进行调度决策,让 Pod 在合适的节点上创建运行。
  • kube-controller-manager:负责监控调整调整集群的状态,比如故障检测、自动扩展、滚动更新等

Node 节点有以下组件:

  • kubelet:主要负责执行、监控由调度器分配的 Pod,相当于是 Master 在每个 Node 节点上的代理。保证 Pod 的运行状态与目标状态一致。
  • kube-proxy:k8s 在每个节点上的网络代理,负责为 Service 提供集群内部的服务发现和负载均衡。

kubelet 功能和作用

  1. 节点管理。kubelet 启动时会向 api-server 进行注册,然后会定时的向 api-server 汇报本节点信息状态,资源使用状态等,这样 master 就能够知道 node 节点的资源剩余,节点是否失联等等相关的信息了。

    master 知道了整个集群所有节点的资源情况,这对于 Pod 的调度和正常运行至关重要。

  2. Pod 管理。kubelet 负责维护 node 节点上 Pod 的生命周期,当 kubelet 监听到 master 的下发到自己节点的任务时,比如要创建、更新、删除一个 Pod,kubelet 就会通过 CRI(容器运行时接口)插件来调用不同的容器运行时来创建、更新、删除容器;

    常见的容器运行时有 docker、containerd、rkt 等等这些容器运行时,我们最熟悉的就是 docker 了,但在新版本的 k8s 已经弃用 docker 了,k8s1.24 版本中已经使用 containerd 作为容器运行时了。

  3. 容器健康检查。

    Pod 中可以定义启动探针、存活探针、就绪探针等3种,我们最常用的就是存活探针、就绪探针,kubelet 会定期调用容器中的探针来检测容器是否存活,是否就绪,如果是存活探针,则会根据探测结果对检查失败的容器进行相应的重启策略;

  4. Metrics server 资源监控。在 node 节点上部署 Metrics Server 用于监控 node 节点、Pod 的 CPU、内存、文件系统、网络使用等资源使用情况,而 kubelet 则通过 Metrics Server 获取所在节点及容器的上的数据。

k8s 的健康检查机制是什么?★★★

k8s 主要通过提供下面两种探针(probe)来进行 Pod 的健康检测:

  • livenessProbe(存活探针):用来确定什么时候要重启容器,例如通过一个 HTTP GET 请求来判断容器是否健康存活。
  • readinessProbe(就绪探针):有时候,应用程序会暂时性的不能提供通信服务。(例如启动加载大文件)。在这种情况下,既不想杀死应用程序,也不想给它发送请求。Kubernetes 提供了就绪探测器来发现并缓解这些情况,设置后,流量将不会打到 Service 上。

Pod 的存活探针有哪几种?★★★

kubernetes 可以通过存活探针检查容器是否还在运行,可以为 Pod 中的每个容器单独定义存活探针,kubernetes 将定期执行探针,如果探测失败,将杀死容器,并根据 restartPolicy 策略来决定是否重启容器,

kubernetes 提供了 3 种探测容器的存活探针,如下:

  • httpGet:通过容器的 IP、端口、路径发送 http 请求,返回 200-400 范围内的状态码表示成功。
  • exec:在容器内执行 shell 命令,根据命令退出状态码是否为 0 进行判断,0 表示健康,非 0 表示不健康。
  • TCPSocket:与容器的端口建立 TCP Socket 链接。,能建立则说明探测成功,不能建立则说明探测失败。

存活探针的附加属性参数有以下几个:

  • initialDelaySeconds:表示在容器启动后延时多久秒才开始探测;
  • periodSeconds:表示执行探测的频率,即间隔多少秒探测一次,默认间隔周期是10秒,最小1秒;
  • timeoutSeconds:表示探测超时时间,默认1秒,最小1秒,表示容器必须在超时时间范围内做出响应,否则视为本次探测失败;
  • successThreshold:表示最少连续探测成功多少次才被认定为成功,默认是1,对于liveness必须是1,最小值是1;
  • failureThreshold:表示连续探测失败多少次才被认定为失败,默认是3,连续3次失败,k8s 将根据pod重启策略对容器做出决定;

注意:定义存活探针时,一定要设置 initialDelaySeconds 属性,该属性为初始延时,如果不设置,默认容器启动时探针就开始探测了,这样可能会存在应用程序还未启动就绪,就会导致探针检测失败,k8s 就会根据 pod 重启策略杀掉容器然后再重新创建容器的莫名其妙的问题。

在生产环境中,一定要定义一个存活探针。

Pod 的就绪探针有哪几种?

我们知道,当一个 Pod 启动后,就会立即加入 service 的 endpoint ip 列表中,并开始接收到客户端的链接请求,假若此时 Pod 中的容器的业务进程还没有初始化完毕,那么这些客户端链接请求就会失败,为了解决这个问题,kubernetes 提供了就绪探针来解决这个问题的。

在 Pod 中的容器定义一个就绪探针,就绪探针周期性检查容器,如果就绪探针检查失败了,说明该 Pod 还未准备就绪,不能接受客户端链接,则该 Pod 将从 endpoint 列表中移除,被剔除了 service 就不会把请求分发给该 Pod,然后就绪探针继续检查,如果随后容器就绪,则再重新把 Pod 加回 endpoint 列表。

k8s 提供了 3 种就绪探针,如下:

  • exec:在容器中执行命令并检查命令退出的状态码,如果状态码为 0,则说明容器已经准备就绪;
  • httpGet:向容器发送 http get 请求,通过响应的 http 状态码判断容器是否准备就绪;
  • tcpSocket:打开一个 tcp 连接到容器的指定端口,如果连接已建立,则认为容器已经准备就绪。

就绪探针的附加属性参数有以下几个:

  • initialDelaySeconds:延时秒数,即容器启动多少秒后才开始探测,不写默认容器启动就探测;
  • periodSeconds :执行探测的频率(秒),默认为10秒,最低值为1;
  • timeoutSeconds :超时时间,表示探测时在超时时间内必须得到响应,负责视为本次探测失败,默认为1秒,最小值为1;
  • failureThreshold :连续探测失败的次数,视为本次探测失败,默认为3次,最小值为1次;
  • successThreshold :连续探测成功的次数,视为本次探测成功,默认为1次,最小值为1次;

就绪探针与存活探针区别是什么?

两者作用不一样

  • 存活探针是将检查失败的容器杀死,创建新的启动容器来保持 Pod 正常工作;
  • 就绪探针是,当就绪探针检查失败,并不重启容器,而是将 Pod 移出 endpoint,就绪探针确保了 service 中的 Pod 都是可用的,确保客户端只与正常的 Pod 交互并且客户端永远不会知道系统存在问题。

Pod

Pod 的原理是什么?★★★

在微服务的概念里,一般的,一个容器会被设计为运行一个进程,除非进程本身产生子进程,这样,由于不能将多个进程聚集在同一个单独的容器中,所以需要一种更高级的结构将容器绑定在一起,并将它们作为一个单元进行管理,这就是 k8s 中 Pod 的背后原理。

Pod 特点

  1. 每个 Pod 就像一个独立的逻辑机器,k8s 会为每个 Pod 分配一个集群内部唯一的 IP 地址,所以每个 Pod 都拥有自己的 IP 地址、主机名、进程等;

  2. 一个 Pod 可以包含 1 个或多个容器,1 个容器一般被设计成只运行 1 个进程,1 个 Pod 只可能运行在单个节点上,即不可能 1 个 Pod 跨节点运行,Pod 的生命周期是短暂,也就是说 Pod 可能随时被消亡(如节点异常,Pod 异常等情况);

  3. 每一个 Pod 都有一个特殊的被称为”根容器”的 pause 容器,也称 info 容器,pause 容器对应的镜像属于 k8s 平台的一部分,除了 pause 容器,每个 Pod 还包含一个或多个跑业务相关组件的应用容器;

  4. 一个 Pod 中的容器共享 network 命名空间;

  5. 一个 Pod 里的多个容器共享 Pod IP,这就意味着 1 个 Pod 里面的多个容器的进程所占用的端口不能相同,否则在这个 Pod 里面就会产生端口冲突;既然每个 Pod 都有自己的 IP 和端口空间,那么对不同的两个 Pod 来说就不可能存在端口冲突;

  6. 应该将应用程序组织到多个 Pod 中,而每个 Pod 只包含紧密相关的组件或进程;

  7. Pod 是 k8s 中扩容、缩容的基本单位,也就是说 k8s 中扩容缩容是针对 Pod 而言而非容器。

pause 容器作用

每个 Pod 里运行着一个特殊的被称之为 pause 的容器,也称根容器,而其他容器则称为业务容器;

创建 pause 容器主要是为了为业务容器提供 Linux 命名空间,共享基础:包括 pid、icp、net 等,以及启动 init 进程,并收割僵尸进程;

这些业务容器共享 pause 容器的网络命名空间和 volume 挂载卷,当 Pod 被创建时,Pod 首先会创建 pause 容器,从而把其他业务容器加入 pause 容器,从而让所有业务容器都在同一个命名空间中,这样可以就可以实现网络共享。

Pod 还可以共享存储,在 Pod 级别引入数据卷 volume,业务容器都可以挂载这个数据卷从而实现持久化存储。

Pod 的重启策略

可以通过 restartPolicy 字段配置 Pod 重启容器的策略

  • Always: 当容器终止退出后,总是重启容器,默认策略就是 Always
  • OnFailure: 当容器异常退出,退出状态码非 0 时,才重启容器。
  • Never: 当容器终止退出,不管退出状态码是什么,从不重启容器。

Pod 的状态有哪些

  • Pending:Pod 正在等待 kube-scheduler 选择合适的节点创建。
  • Running:Pod 已正常创建,并且至少有一个容器正在运行。
  • Succeeded:所有容器已成功启动运行。
  • Failed:Pod 的容器非正常退出。
  • Unknown:无法获取 Pod 状态,可能节点间通信出现问题。

Pod 创建过程

情况一、如果面试官问的是使用 kubectl run 命令创建的 Pod,可以这样说:

注意:kubectl run 在旧版本中创建的是 deployment,但在新的版本中创建的是 Pod,则其创建过程不涉及 deployment

如果是单独的创建一个 Pod,则其创建过程是这样的:

  1. 首先,用户通过 kubectl 或其他 api 客户端工具提交需要创建的 Pod 信息给 apiserver;

  2. apiserver 验证客户端的用户权限信息,验证通过开始处理创建请求生成 Pod 对象信息,并将信息存入 etcd,然后返回确认信息给客户端;

  3. apiserver 开始反馈 etcd 中 Pod 对象的变化,其他组件使用 watch 机制跟踪 apiserver 上的变动;

  4. scheduler 发现有新的 Pod 对象要创建,开始调用内部算法机制为 Pod 分配最佳的主机,并将结果信息更新至 apiserver;

  5. node 节点上的 kubelet 通过 watch 机制跟踪 apiserver 发现有 Pod 调度到本节点,尝试调用 docker 启动容器,并将结果反馈 apiserver;

  6. apiserver 将收到的 Pod 状态信息存入 etcd 中。

至此,整个 Pod 创建完毕。

情况二、如果面试官说的是使用 deployment 来创建 Pod,则可以这样回答:

  1. 首先,用户使用 kubectl create 命令或者 kubectl apply 命令提交了要创建一个 deployment 资源请求;

  2. api-server 收到创建资源的请求后,会对客户端操作进行身份认证,在客户端的 ~/.kube 文件夹下,已经设置好了相关的用户认证信息,这样 api-server 会知道我是哪个用户,并对此用户进行鉴权,当 api-server 确定客户端的请求合法后,就会接受本次操作,并把相关的信息保存到 etcd 中,然后返回确认信息给客户端。

  3. apiserver 开始反馈 etcd 中过程创建的对象的变化,其他组件使用 watch 机制跟踪 apiserver 上的变动。

  4. controller-manager 组件会监听 api-server 的信息,controller-manager 是有多个类型的,比如 Deployment Controller, 它的作用就是负责监听 Deployment,此时 Deployment Controller 发现有新的 deployment 要创建,那么它就会去创建一个ReplicaSet,一个 ReplicaSet 的产生,又被另一个叫做 ReplicaSet Controller 监听到了,紧接着它就会去分析 ReplicaSet 的语义,它了解到是要依照 ReplicaSet 的 template 去创建 Pod, 它一看这个 Pod 并不存在,那么就新建此 Pod,当 Pod 刚被创建时,它的 nodeName 属性值为空,代表着此 Pod 未被调度。

  5. 调度器 Scheduler 组件开始介入工作,Scheduler 也是通过 watch 机制跟踪 apiserver 上的变动,发现有未调度的 Pod,则根据内部算法、节点资源情况,Pod 定义的亲和性反亲和性等等,调度器会综合的选出一批候选节点,在候选节点中选择一个最优的节点,然后将 Pod 绑定该该节点,将信息反馈给api-server。

  6. kubelet 组件布署于 Node 之上,它也是通过 watch 机制跟踪 apiserver 上的变动,监听到有一个 Pod 应该要被调度到自身所在 Node 上来,kubelet 首先判断本地是否在此 Pod,如果不存在,则会进入创建 Pod 流程,创建 Pod 有分为几种情况,第一种是容器不需要挂载外部存储,则相当于直接 docker run 把容器启动,但不会直接挂载 docker 网络,而是通过 CNI 调用网络插件配置容器网络,如果需要挂载外部存储,则还要调用 CSI 来挂载存储。kubelet 创建完 Pod,将信息反馈给 api-server,api-servier将 Pod 信息写入 etcd。

  7. Pod 建立成功后,ReplicaSet Controller 会对其持续进行关注,如果 Pod 因意外或被我们手动退出,ReplicaSet Controller 会知道,并创建新的 Pod,以保持 replicas 数量期望值。

Pod 的终止过程

  1. 用户向 apiserver 发送删除 pod 对象的命令;
  2. apiserver 中的 Pod 对象信息会随着时间的推移而更新,在宽限期内(默认30s),pod 被视为 dead;
  3. 将 pod 标记为 terminating 状态;
  4. kubectl 在监控到 Pod 对象为 terminating 状态了就会启动 Pod 关闭过程;
  5. endpoint 控制器监控到 Pod 对象的关闭行为时将其从所有匹配到此 endpoint 的 server 资源 endpoint 列表中删除;
  6. 如果当前 Pod 对象定义了 preStop 钩子处理器,则在其被标记为 terminating 后会意同步的方式启动执行;
  7. pod 对象中的容器进程收到停止信息;
  8. 宽限期结束后,若 pod 中还存在运行的进程,那么 Pod 对象会收到立即终止的信息;
  9. kubelet 请求 apiserve r将此 pod 资源的宽限期设置为 0 从而完成删除操作,此时 pod 对用户已不可见。

镜像的下载策略有哪些?

主要分为三种:

  • Always:总是从指定的仓库中获取镜像。
  • Never:使用本地镜像,不从仓库中下载。
  • IfNotPresent:当本地镜像不存在时,才从仓库拉取。

当镜像标签是 latest 时,默认下载策略是 Always。当镜像标签是自定义时,使用 IfNotPresent。

service 是什么?

Pod 每次重启或者重新部署,其 IP 地址都会产生变化,这使得 Pod 间通信和 Pod 与外部通信变得困难,这时候,就需要 Service 为 Pod 提供一个固定的入口。

Service 的 Endpoint 列表通常绑定了一组相同配置的 Pod,通过负载均衡的方式把外界请求分配到多个 Pod 上

怎么服务注册?

Pod 启动后会加载当前环境所有 Service 信息,以便不同 Pod 根据 Service 名进行通信。

持久化方式

  1. EmptyDir(空目录):没有指定要挂载宿主机上的某个目录,直接由 Pod 内保部映射到宿主机上。类似于 docker 中的 manager volume。

  2. Hostpath:将宿主机上已存在的目录或文件挂载到容器内部。类似于 docker 中的 bind mount 挂载方式。

  3. PersistentVolume(简称 PV):
    基于 NFS 服务的 PV,也可以基于 GFS 的 PV。它的作用是统一数据持久化目录,方便管理。

常用命令

  • kubectl create:创建资源(kubectl create -f docker-registry.yaml)
  • kubectl run:快速创建容器(kubectl run nginx --image=nginx)
  • kubectl get:获取资源列表(kubectl get pods)
  • kubectl describe:查看资源详细信息(kubectl describe pods/nginx)
  • kubectl exec:在容器内执行命令(kubectl exec podName -c containerName -i -t -- bash -il)
  • kubectl logs:查看 Pod 日志(kubectl logs nginx)
  • kubectl delete:删除一个资源(kubectl delete pods --all)

Reference


Docker 和 K8S
https://flepeng.github.io/interview-42-云原生-Docker-和-K8S/
作者
Lepeng
发布于
2020年8月8日
许可协议