容器实战过程中的部分问题总结
一、k8s 在落地过程中,可能碰到的问题
- 行为模式不同,比如虚拟机中的 CPU 监控程序,到容器后不再适用;
- 隔离程度不同,CPU、memory、IO 的精确隔离,要保证每个容器的限额和合理分配性能;
- 性能敏感应用的处理,容器的引入带来新的开销,需要对容器网络,CGroup 优化,保证性能控制差在 2% 以内。对高内存的使用,还要考虑 Page Cache、Swap 和 HugePage 等。
二、所有容器问题的本质核心
- 落地到 Linux 操作系统,容器进程被 OOM Kill 是 Linux 的内存保护机制;OverlayFS 也是内核维护的文件系统;
- Linux 操作系统本质上也是进程管理、内存管理、文件系统、网络协议栈和安全管理,都是容器问题的本质;
- 容器自己的特性:Namespace 和 CGroups,它们是容器的基石。
三、程序代码发生非预期的结果,如何处理
- 逐步缩减可能的问题点,用简单的代码复现问题;
- 想办法把黑盒变白盒,尤其是对开源软件,要深究源码。
四、Linux 系统启动流程
- 通电 -> 执行 BIOS/boot-loader-> 加载 Linux 内核 -> 内核初始化 -> 运行第一个用户态进程 init 进程
五、容器的 2 个核心概念
容器共享宿主机的 Linux 内核,但拥有 2 个独立使用的概念特性。对 Docker 来说,它是由宿主机上运行的 Containerd 进程、shim 进程和容器进程共同构成。
- Namespace:相互隔离,创造了容器独立的运行环境。比如,运行在容器里的进程 pid(Host Pid)、网络配置(Network)和根文件系统(Mount)各不相同。它的目的是隔离在同一个宿主机上运行的进程,让容器间不能访问彼此的资源。即,第一可以运行多个用户容器、充分利用系统资源;第二保证不同用户之间的安全性。
- Cgroups:使用树结构管理,指定容器能够使用的资源大小。比如,CPU、memory、pids 和 cpuset 子系统。对每个启动的容器,会在 /sys/fs/cgroup 的不同子系统下设置值。主要有 v1 和 v2 版本,前者导致相同进程对资源协调困难,后者解决了问题。但前者仍然是主流。
六、为什么 SIGTERM 和 SIGKILL 无法在容器中终止进程
- init 进程:默认 1 号进程,Linux 系统用来创建和管理所有进程的进程;
- 信号:是外部发送给进程的通知。有 3 种处理选择:忽略、捕获、缺省。SIGKILL 和 SIGSTOP 不能捕获,只能缺省;
- 内核中有 3 个子条件判断是否将信号发送给进程。容器中 SIGKILL 和 SIGSTOP 是不工作的,内核阻止了 1 号进程对它的响应。如果 1 号进程捕获了 SIGTERM 或其他信号,就能响应。否则不响应。
七、如何保证容器在运行过程中不出现僵尸进程
- 僵尸进程:Linux 进程状态在 top 命令的 status 栏,处于 EXIT_ZOMBIE 状态的进程,只占用 pid 资源;
- 容器限制最大进程数,向 Croup 的 pids.max 写入值;
- 僵尸进程的根本原因是父进程 fork 完就不管了,一定要调用 wait/waitpid 或注册 SIGCHLD 信号收尾僵尸进程。
八、限制容器对 cpu 的使用
- CPU 使用时间包括用户态和内核态 2 部分构成,只要不是系统调用,都属于用户态时间。如果是系统调用,更详细些还分为调用前的准备时间、调用等待时间、中断响应时间等;
- CPU CGroup 子系统挂载点在 /sys/fs/cgroup/cpu 目录下,每个控制组都是一个子目录,各个控制组间的关系就是树状层级关系;
- 对 CPU 普通调度中的 CFS 完全公平调度来说,cfs_period_us 是指调度周期 100ms,cfs_quota_us 是指在一个调度中期内,该控制组被允许运行的时间 50ms,则该控制组配额是 0.5 个 CPU;shares 是控制组间 CPU 的分配基准 1024,通过数值相对比例计算 CPU 比例;
- CPU CGroup 子系统使用 cfs_quota_us、cfs_period_us 控制进程对 CPU 的最大使用,shares 控制容器间的最大使用比例。
九、正确拿到容器内的 cpu/内存 开销
- top 和 free 工具是读取 /proc 下的 stat 文件来获取 CPU 开销,但它是宿主机全局的状态文件,容器内无法通过该文件获取 cpu/mem 使用率;
- 容器获取 CPU 使用率需要通过 CPU CGroup 中每个控制组自己的统计文件 cpuacct.stat 中获取并计算。
- 容器内获取内存分配/使用情况也是要通过 container 自己的 CGroup 文件获取,如: 使用率:cat /sys/fs/cgroup/memory/memory.usage_in_bytes,注意这里已经使用的内存 usage_in_bytes 是包含 cache 的,详细的情况可以从/sys/fs/cgroup/memory/memory.stat 中获取 查看内存限额:cat /sys/fs/cgroup/memory/memory.limit_in_bytes
十、CPU 利用率低,Load Average 高的原因
- Load Average 计算的是进程调度器中可运行队列里一段时间内的平均进程数,Linux 还包括休眠队列里 TASK_UNINTERRUPTIBLE 状态的平均进程数;
- D 进程产生的根本原因是磁盘 IO 或信号量争用,所以看上去 CPU 不高但负载很高。所以,还需要检测宿主机节点上 D 状态的数目。比如,磁盘硬件问题出现 D 数据增加,需要更换磁盘。
十一、容器进程被 OOM Kill
- malloc 申请的是虚拟内存,系统只给了一个地址范围,由于并没有写入数据,所以并没有真正分配物理内存;
- 通过 /var/log/message 查看内核日志,OOM 由 2 个原因决定:进程已经使用的物理内存页数、每个进程自己的 OOM 校准值。即,系统总的可用页数 * OOM 校准值 + 进程使用的物理页数,值越大,OOM Kill 概率越大;
- 限制 CGroups Memory:limit_in_bytes(可使用最大内存),oom_control(是否 Kill),usage_in_bytes(实际使用的内存综合)
十二、容器内存使用,总在临界点
- RSS 是进程实际占有的物理内存,包括代码段、堆、栈等;Page Cache 是进程在读写磁盘后作为 Cache 保留在内存中,提高读写性能;
- 当内存紧张或 Memory CGroup 的内存达到上限时,即根据空闲物理内存是否低于某个阈值,会回收 Page Cache,所以只需要关注 RSS 即可。
十三、容器是否可以使用 Swap 空间
- Swap 空间是一块磁盘空间,当内存写满时,会将内存中不常用的数据暂时写到 Swap 空间,使得内存释放出来满足新的内存申请需求;
- /proc/sys/vm/swappiness 可以决定系统将会多频繁的使用 Swap 空间,即当内存紧张时,优先释放 Page Cache 还是将匿名内存写入 Swap 空间;它像一个衡量 Swap 交换的权重,即 100 平等,60 优先 Page Cache,0 内存极其紧张时才使用 Swap(宿主机)或禁止 Swap(Memory CGroup);
- 因为 Swap 会使得 Memory CGroup 限制无效,所以容器一般会关闭 Swap。
十四、容器文件系统 OverylayFS
- 为了减少磁盘上冗余的镜像数据和冗余数据在网络上的传输,使用 UnionFS。它将处于不同分区的多个目录一起挂载到同一个目录下;
- OverlayFS 有 3 层目录:lower 处于最底层,文件只读不能被修改。允许多个 lower;upper 处于中间层,可读写,读写后的内容在该层反应;merged 处于最上层,是用户实际操作的目录层,是 upper 和 lower 的合并结果。
十五、容器写数据占满宿主机硬盘
- 对宿主机而言,upper 也是一个真实存在的目录。如果容器往这个目录疯狂写数据,是会占满宿主机硬盘的。所以,一般大文件写会挂载 volumn。但对于 upper 可能存在的问题,可以在 upper 目录上,使用 XFS 文件系统的 Quota 特性,限制用户、用户组对文件系统的写入总量;
十六、多容器写同一块磁盘时,会互相干扰
- CGroup V1 的 blkio 可以限制容器中进程读写 IOPS 和吞吐量,但只对 Direct IO 有效,对 Buffered IO 无效。因为后者先将数据写入 Page Cache,再由 Page Cache 刷磁盘,memory 子系统无法被 blkio 子系统统计;
- CGroup V2 已经解决该问题,但需要时间去切换。
十七、容器内写文件耗时波动比较大
- Buffered IO 先将数据写入 Page Cache,此时需要不断的释放原有的页面,开销最大。如果容器限制内存后 Page Cache 数量少,需要不断申请释放;
- 在对容器的 Memory CGroup 限制内存大小时,除了考虑进程实际使用的内存量,还要考虑进程 IO 的量,为 Buffered IO 合理预留足够的内存。
十八、为什么修改 /proc/sys/net 下的参数,对容器不生效
- Network Namespace 的网络参数既不是完全从宿主机继承,也不是完全重新初始化。比如,tcp_congestion_control 是继承的,但 tcp_keepalive 被重新初始化;
- 由于 runC 的安全考虑,容器中的 /proc/sys 是只读的,不能直接修改。但它也在容器刚刚启动,但进程未启动时,预留了 sysctl 参数来修改。
十九、如何为容器配置网络接口
- 容器网络主要涉及 2 个方面:首先可以配置一对 veth 虚拟网络设备,让数据包从容器的 Network Namespace 发送到宿主机的 Network Namespace;接着配置 bridge+nat/forward 让数据从宿主机的 eth0 发送出去;
- 排查容器网络问题:首先明确容器和宿主机的网络配置,在主要设备上 tcpdump 找到具体哪一步停止转发,然后结合内核网络配置参数、路由表信息,防火墙规则一般都能定位问题。
二十、容器网络开销比较大
- veth 对确实会带来比较大的开销,导致高的网络延时。除此之外,可以选择 macvlan 和 ipvlan 的方式配置容器网络;
- macvlan 和 ipvlan 都可以在一个物理网卡上配置多个虚拟网卡和独立的虚拟 IP,它们都可以属于不同的 Namespace;但前者的每个虚拟 IP 都有自己独立的 mac 地址,而后者的虚拟 IP 和物理网卡共用一个 mac 地址。所以,可以将虚拟 IP 都分配给容器网卡,当容器向外发送数据时,会直接发送到宿主机的物理网卡,优化耗时。
二十一、容器平台乱序包导致重传较多
- 网络包被发到了不同的 CPU,使得系统有更强的网络包处理能力,但会导致乱序;
- 开启 RPS,保证同一个数据流由一个特定的 CPU 处理,减少乱序几率,但会带来额外开销。
二十二、容器中 root 用户也不能运行 iptables
- Linux 有 capabilities 的概念,宿主机 root 拥有完全的 capabilities,但容器 root 只有特定的 15 个 capabilities;
- iptables 需要 CAP_NET_ADMIN 的 capabilities,需要为容器 root 单独开启;
- 因为没有 User Namespace,虽然容器的 root 只有部分 capabilities,但它和宿主机 root 用户的 uid 是完全相同的,一旦有漏洞容器的 root 用户就可以操作宿主机;
- 非 root 启动容器进程的方法主要有 2 个:将容器 root 用户映射成宿主机的普通用户;使用 rootless container 启动和管理容器。
参考资料: 【极客时间】容器实战高手课 - 程远,原文链接:https://zhuanlan.zhihu.com/p/346381224