docker & kubernetes 面试(某互联网公司)

两年容器云工作经验,牛刀小试了几家公司,将面试问到的问题记录下来,鞭策自己不断学习。

1、docker 后端存储驱动 devicemapper、overlay 几种的区别?

这道题是考察 docker 后端存储知识。

刚开始拿到这道题我有点蒙,因为我只知道目前我们用的是vg-pool devicemapper 来存储镜像和容器,后来面试官问我镜像分层的技术知道吗?我说知道,就是联合文件系统,多层文件系统联合组成一个统一的文件系统视角,当需要修改文件时采用写时复制(CopyW)的技术从上往下查找,找到之后复制到可写的容器层,进行修改并保存至容器层,说完之后面试官再问我,那每次修改文件都需要从上往下查找,层数又那么多,性能是否比较差,现在才反应回来,原先面试官想考察我aufs、overlay 或者是 devicemapper 等几种存储驱动的区别。

AUFS


AUFS (Another UnionFS)是一种 Union FS,是文件级的存储驱动,AUFS 简单理解就是将多层的文件系统联合挂载成统一的文件系统,这种文件系统可以一层一层地叠加修改文件,只有最上层是可写层,底下所有层都是只读层,对应到 Docker,最上层就是 container 层,底层就是 image 层,结构如下图所示:


aufs 结构

Overlay


Overlay 也是一种 Union FS,和 AUFS 多层相比,Overlay 只有两层:一个 upper 文件系统和一个 lower 文件系统,分别代表 Docker 的容器层(upper)和镜像层(lower)。当需要修改一个文件时,使用 CopyW 将文件从只读的 lower 层复制到可写层 upper,结果也保存在 upper 层,结构如下图所示:


overlay 结构

Devicemapper


Device mapper,提供的是一种从逻辑设备到物理设备的映射框架机制,前面讲的 AUFS 和 OverlayFS 都是文件级存储,而 Device mapper 是块级存储,所有的操作都是直接对块进行操作,而不是文件。Device mapper 驱动会先在块设备上创建一个资源池,然后在资源池上创建一个带有文件系统的基本设备,所有镜像都是这个基本设备的快照,而容器则是镜像的快照。所以在容器里看到文件系统是资源池上基本设备的文件系统的快照。当要写入一个新文件时,在容器的镜像内为其分配新的块并写入数据,这个叫用时分配。当要修改已有文件时,再使用CoW为容器快照分配块空间,将要修改的数据复制到在容器快照中新的块里再进行修改。Devicemapper 驱动默认会创建一个100G 的文件包含镜像和容器。每一个容器被限制在 10G 大小的卷内,可以自己配置调整。结构如下图所示:


devicemapper 结构

这里只介绍三种常见的 docker 存储驱动,更多详细的内容可以参考这两博客: 

Docker 五种存储驱动

深入了解 Docker 存储驱动

2、k8s 创建一个pod的详细流程,涉及的组件怎么通信的?

这道题考察的是 k8s 内部组件通信。

k8s 创建一个 Pod 的详细流程如下: 


(1) 客户端提交创建请求,可以通过 api-server 提供的 restful 接口,或者是通过 kubectl 命令行工具,支持的数据类型包括 JSON 和 YAML。

(2) api-server 处理用户请求,将 pod 信息存储至 etcd 中。

(3) kube-scheduler 通过 api-server 提供的接口监控到未绑定的 pod,尝试为 pod 分配 node 节点,主要分为两个阶段,预选阶段和优选阶段,其中预选阶段是遍历所有的 node 节点,根据策略筛选出候选节点,而优选阶段是在第一步的基础上,为每一个候选节点进行打分,分数最高者胜出。

(4) 选择分数最高的节点,进行 pod binding 操作,并将结果存储至 etcd 中。

(5) 随后目标节点的 kubelet 进程通过 api-server 提供的接口监测到 kube-scheduler 产生的 pod 绑定事件,然后从 etcd 获取 pod 清单,下载镜像并启动容器。


整个事件流可以参考下图:


pod 创建事件流

3、k8s 架构体系了解吗?简单描述下

这道题主要考察 k8s 体系,涉及的范围其实太广泛,可以从本身 k8s 组件、存储、网络、监控等方面阐述,当时我主要将 k8s 的每个组件功能都大概说了一下。

Master节点

Master节点主要有四个组件,分别是:api-server、controller-manager、kube-scheduler 和 etcd。

api-server


kube-apiserver 作为 k8s 集群的核心,负责整个集群功能模块的交互和通信,集群内的各个功能模块如 kubelet、controller、scheduler 等都通过 api-server 提供的接口将信息存入到 etcd 中,当需要这些信息时,又通过 api-server 提供的 restful 接口,如get、watch 接口来获取,从而实现整个 k8s 集群功能模块的数据交互。


controller-manager


controller-manager 作为 k8s 集群的管理控制中心,负责集群内 Node、Namespace、Service、Token、Replication 等资源对象的管理,使集群内的资源对象维持在预期的工作状态。

每一个 controller 通过 api-server 提供的 restful 接口实时监控集群内每个资源对象的状态,当发生故障,导致资源对象的工作状态发生变化,就进行干预,尝试将资源对象从当前状态恢复为预期的工作状态,常见的 controller 有 Namespace Controller、Node Controller、Service Controller、ServiceAccount Controller、Token Controller、ResourceQuote Controller、Replication Controller等。


kube-scheduler


kube-scheduler 简单理解为通过特定的调度算法和策略为待调度的 Pod 列表中的每个 Pod 选择一个最合适的节点进行调度,调度主要分为两个阶段,预选阶段和优选阶段,其中预选阶段是遍历所有的 node 节点,根据策略和限制筛选出候选节点,优选阶段是在第一步的基础上,通过相应的策略为每一个候选节点进行打分,分数最高者胜出,随后目标节点的 kubelet 进程通过 api-server 提供的接口监控到 kube-scheduler 产生的 pod 绑定事件,从 etcd 中获取 Pod 的清单,然后下载镜像,启动容器。

预选阶段的策略有:

(1) MatchNodeSelector:判断节点的 label 是否满足 Pod 的 nodeSelector 属性值。

(2) PodFitResource:判断节点的资源是否满足 Pod 的需求,批判的标准是:当前节点已运行的所有 Pod 的 request值 + 待调度的 Pod 的 request 值是否超过节点的资源容量。

(3) PodFitHostName:判断节点的主机名称是否满足 Pod 的 nodeName 属性值。

(4) PodFitHostPort:判断 Pod 的端口所映射的节点端口是否被节点其他 Pod 所占用。

(5) CheckNodeMemoryPressure:判断 Pod 是否可以调度到内存有压力的节点,这取决于 Pod 的 Qos 配置,如果是 BestEffort(尽量满足,优先级最低),则不允许调度。

(6) CheckNodeDiskPressure:如果当前节点磁盘有压力,则不允许调度。

优选阶段的策略有:

(1) SelectorSpreadPriority:尽量减少节点上同属一个 SVC/RC/RS 的 Pod 副本数,为了更好的实现容灾,对于同属一个 SVC/RC/RS 的 Pod 实例,应尽量调度到不同的 node 节点。

(2) LeastRequestPriority:优先调度到请求资源较少的节点,节点的优先级由节点的空闲资源与节点总容量的比值决定的,即(节点总容量 - 已经运行的 Pod 所需资源)/ 节点总容量,CPU 和 Memory 具有相同的权重,最终的值由这两部分组成。

(3) BalancedResourceAllocation:该策略不能单独使用,必须和 LeaseRequestPriority 策略一起结合使用,尽量调度到 CPU 和 Memory 使用均衡的节点上。


ETCD


强一致性的键值对存储,k8s 集群中的所有资源对象都存储在 etcd 中。


Node节点

node节点主要有三个组件:分别是 kubelet、kube-proxy 和 容器运行时 docker 或者 rkt。

kubelet


在 k8s 集群中,每个 node 节点都会运行一个 kubelet 进程,该进程用来处理 Master 节点下达到该节点的任务,同时,通过 api-server 提供的接口定期向 Master 节点报告自身的资源使用情况,并通过 cadvisor 组件监控节点和容器的使用情况。


kube-proxy


kube-proxy 就是一个智能的软件负载均衡器,将 service 的请求转发到后端具体的 Pod 实例上,并提供负载均衡和会话保持机制,目前有三种工作模式,分别是:用户模式(userspace)、iptables 模式和 IPVS 模式。


容器运行时——docker


负责管理 node 节点上的所有容器和容器 IP 的分配。


4、单元测试有考虑直接在 k8s 集群上做吗?

这道题问的有点蒙,目前我们的工程是直接在 jenkins 编译打包时强制要求跑单元测试,面试官的意思是说,能否单元测试不在 jenkins 公共环境上执行,而直接在 k8s 集群上运行,利用 k8s 有多租户和 Namespace 隔离的机制,确保每个工程执行单元测试能做到隔离和并行。


5、Google 号称可以支撑5000个节点,在调度这方面是有做了哪些优化呢

这道题我回答的也不好,我只从调度分为两个阶段,每个阶段有多种调度算法和策略适用于不同场景,另外也支撑自定义调度算法,我就大概说了这几点,面试官听完之后也没有继续追问下去,面试官说算了,有点深。。。

6、flannel 和 ovs 网络的区别

这道题主要考察 k8s 集群中跨节点容器间通信的 sdn 网络组件:flannel 和 openvswitch,主要有以下两方面的区别:

(1)配置是否自动化


ovs 作为开源的交换机软件,相对比较成熟和稳定,支持各种网络隧道和协议,经历了大型项目 OpenStack 的考验,而 flannel 除了支持建立覆盖网络来实现 Pod 到 Pod 之间的无缝通信之外,还跟 docker、k8s 的架构体系紧密结合,flannel 能感知 k8s 中的 service 对象,然后动态维护自己的路由表,并通过 etcd 来协助 docker 对整个 k8s 集群的 docker0 网段进行规范,而 ovs ,这些操作则需要手动完成,假如集群中有 N 个节点,则需要建立 N(N-1)/2 个 Vxlan 或者 gre 连接,这取决于集群的规模,如果集群的规模很大,则必须通过自动化脚本来初始化,避免出错。


(2)是否支持隔离


flannel 虽然很方便实现 Pod 到 Pod 之间的通信,但不能实现多租户隔离,也不能很好地限制 Pod 的网络流量,而 ovs 网络有两种模式:单租户模式和多租户模式,单租户模式直接使用 openvswitch + vxlan 将 k8s 的 pod 网络组成一个大二层,所有的 pod 可以互相通信访问,多租户模式以 Namespace 为维度分配虚拟网络,从而形成一个网络独立用户,一个 Namespace 中的 pod 无法访问其他 Namespace 中的 pod 和 svc 对象。

7、k8s 中服务级别,怎样设置服务的级别才是最高的

这道题主要考察 k8s Qos 类别。

在 k8s 中,Qos 主要有三种类别,分别是 BestEffort、Burstable 和 Guaranteed,三种类别区别如下:

BestEffort


什么都不设置(CPU or Memory),佛系申请资源。


Burstable


Pod 中的容器至少一个设置了CPU 或者 Memory 的请求


Guaranteed


Pod 中的所有容器必须设置 CPU 和 Memory,并且 request 和 limit 值相等。


详情可以参考这篇博客:K8s Qos

8、容器隔离不彻底,Memory 和 CPU 隔离不彻底,怎么处理解决这个问题?

由于 /proc 文件系统是以只读的方式挂载到容器内部,所以在容器内看到的都是宿主机的信息,包括 CPU 和 Memory,docker 是以 cgroups 来进行资源限制的,而 jdk1.9 以下版本目前无法自动识别容器的资源配额,1.9以上版本会自动识别和正常读取 cgroups 中为容器限制的资源大小。

Memory 隔离不彻底


Docker 通过 cgroups 完成对内存的限制,而 /proc 文件目录是以只读的形式挂载到容器中,由于默认情况下,Java 压根就看不到 cgroups 限制的内容的大小,而默认使用 /proc/meminfo 中的信息作为内存信息进行启动,默认情况下,JVM 初始堆大小为内存总量的 1/4,这种情况会导致,如果容器分配的内存小于 JVM 的内存, JVM 进程会被 linux killer 杀死。

那么目前有几种解决方式:

(1)升级 JDK 版本到1.9以上,让 JVM 能自动识别 cgroups 对容器的资源限制,从而自动调整 JVM 的参数并启动 JVM 进程。

(2)对于较低版本的JDK,一定要设置 JVM 初始堆大小,并且JVM 的最大堆内存不能超过容器的最大内存值,正常理论值应该是:容器 limit-memory = JVM 最大堆内存 + 750MB。

(3)使用 lxcfs ,这是一种用户态文件系统,用来支持LXC 容器,lxcfs 通过用户态文件系统,在容器中提供下列 procfs 的文件,启动时,把宿主机对应的目录 /var/lib/lxcfu/proc/meminfo 文件挂载到 Docker 容器的 /proc/meminfo 位置后,容器中进程(JVM)读取相应文件内容时,lxcfs 的 fuse 将会从容器对应的 cgroups 中读取正确的内存限制,从而获得正确的资源约束设定。


CPU 隔离不彻底


JVM GC (垃圾回收)对于 java 程序执行性能有一定的影响,默认的 JVM 使用如下公式: ParallelGCThreads = ( ncpu <= 8 ) ? ncpu:3 + (ncpu * 5)/ 8 来计算并行 GC 的线程数,但是在容器里面,ncpu 获取的就是所在宿主机的 cpu 个数,这会导致 JVM 启动过多的 GC 线程,直接的结果就是 GC 的性能下降,java 服务的感受就是:延时增加, TPS 吞度量下降,针对这种问题,也有以下几种解决方案:

(1)显示传递 JVM 启动参数:“-XX: ParallelGCThreads" 告诉 JVM 应该启动多少个并行 GC 线程,缺点是需要业务感知,而且需要为不同配置的容器传递不同的 JVM 参数。

(2)在容器内使用 Hack 过的 glibc ,使 JVM 通过 sysconf 系统调用能正确获取容器内 CPU 资源核数,优点是业务无感知,并且能自动适配不同配置的容器,缺点是有一定的维护成本。


9、kubelet 监控 Node 节点资源使用是通过什么组件来实现的?

这道题主要考察 cAdvisor 组件

开源软件 cAdvisor 是用于监控容器运行状态的利器之一,在 Kubernetes 系统中,cAdvisor 已被默认集成到 kubelet 组件内,当 kubelet 服务启动时,它会自动启动 cAdvisor 服务,然后 cAdvisor 会实时采集所在节点的性能指标及在节点上运行的容器的性能指标。kubelet 的启动参数 --cadvisor-port 可自定义 cAdvisor 对外提供服务的端口号,默认是 4194。.


10、docker runc 漏洞是怎么修复的?

这道题是考察前阵子出现的 docker runc 漏洞获得 Docker k8s 主机的 root 权限问题。

漏洞详情


Docker、containerd或者其他基于runc的容器运行时存在安全漏洞,攻击者可以通过特定的容器镜像或者exec操作可以获取到所在宿主机的runc执行时的文件句柄,并修改掉runc的二进制文件,从而获取到所在宿主机的root执行权限。


关于这个漏洞如何发现,并且如何修复,可以参考如下以下两个博客:

runC容器逃逸漏洞分析(CVE-2019-5736)

runC漏洞

11、集群扩广遇到的挑战是什么

这道题主要考察在扩广 k8s 集群实现微服务容器化部署实际落地过程中遇到的挑战和踩过的坑有哪些,话题有点广,可以说的点其实挺多的,我主要从以下几个方面来阐述的。

部署的规范流程


虽然说容器和虚拟机部署本质上没有多大区别,但还是有些许不同的。容器的可执行文件是一个镜像,而虚拟机的可执行文件往往是一个二进制文件如 jar 包或者是 war包,另外,由于容器隔离的不是特别彻底,在上文也有所阐述,针对这种情况,如何更准确获取 cgroups 给容器限定的 Memory 和 CPU 值,这给平台开发者带来相应的挑战。此外,在容器化部署时,作为用户而言,需要遵循相应的使用规范和流程,如每个 Pod 都必须设置资源限额和健康检测探针,在设置资源限额时,又不能盲目设置,需要依赖监控组件或者是开发者本身对自身应用的认知,进行相关经验值的设置。


多集群调度


对于如何管理多个 k8s 集群,如何进行跨集群调度、应用部署和资源对象管理,这对于平台本身,都是一个很大的挑战。


调度均衡问题


随着集群规模的扩大以及微服务部署的数量增加,同个计算节点,可以会运行很多 Pod,这个时候就会出现资源争用的问题,k8s 本身调度层面有两个阶段,分别是预选阶段和优选阶段,每个阶段都有对应的调度策略和算法,关于如何均衡节点之后的调度,这需要在平台层面上对调度算法有所研究,并进行适当的调整。


推荐阅读更多精彩内容