《容器高手实战》笔记

156 阅读14分钟

背景

最近在接触《容器高手实战》这门课,作为有一定容器经验的工程师,从这个视角下,我会记录一些非基础的,但是不是需要回看一眼的课程笔记,用于分享。

课程相关源码及笔记我放在这个这个链接中, 欢迎 fork & comment~

《容器高手实战课程》 源码及笔记, 共分为以下几个模块,下面将逐一介绍,

  • Cpu
  • 内存
  • 文件系统
  • 一号进程
  • 容器网络
  • 容器 debug 工具
  • 容器安全

容器 load

Load Average 等于单位时间内正在运行的进程加上可运行队列的进程

第一,不论计算机 CPU 是空闲还是满负载,Load Average 都是 Linux 进程调度器中可运行队列(Running Queue)里的一段时间的平均进程数目。

第二,计算机上的 CPU 还有空闲的情况下,CPU Usage 可以直接反映到"load average"上,什么是 CPU 还有空闲呢?具体来说就是可运行队列中的进程数目小于 CPU 个数,这种情况下,单位时间进程 CPU Usage 相加的平均值应该就是"load average"的值。

第三,计算机上的 CPU 满负载的情况下,计算机上的 CPU 已经是满负载了,同时还有更多的进程在排队需要 CPU 资源。这时"load average"就不能和 CPU Usage 等同了。

比如对于单个 CPU 的系统,CPU Usage 最大只是有 100%,也就 1 个 CPU;而"load average"的值可以远远大于 1,因为"load average"看的是操作系统中可运行队列中进程的个数。

结论

第一种是 Linux 进程调度器中可运行队列(Running Queue)一段时间(1 分钟,5 分钟,15 分钟)的进程平均数。

第二种是 Linux 进程调度器中休眠队列(Sleeping Queue)里的一段时间的 TASK_UNINTERRUPTIBLE 状态下的进程平均数。

所以,最后的公式就是:Load Average= 可运行队列进程平均数 + 休眠队列中不可打断的进程平均数

load含义

load含义

容器 cpu

Top 命令中的各 Cpu 值是什么含义

top 命令中 cpu 含义

top 命令中 cpu 含义

top 命令中 cpu 含义2

top 命令中 cpu 含义2

CPU Cgroup

每个 Cgroups 子系统都是通过一个虚拟文件系统挂载点的方式,挂到一个缺省的目录下,CPU Cgroup 一般在 Linux 发行版里会放在 /sys/fs/cgroup/cpu 这个目录下。

在这个子系统的目录下,每个控制组(Control Group) 都是一个子目录,各个控制组之间的关系就是一个树状的层级关系(hierarchy)。

cpu cgoups

cpu cgoups

第一个参数是 cpu.cfs_period_us,它是 CFS 算法的一个调度周期,一般它的值是 100000,以 microseconds 为单位,也就 100ms。

第二个参数是 cpu.cfs_quota_us,它“表示 CFS 算法中,在一个调度周期里这个控制组被允许的运行时间,比如这个值为 50000 时,就是 50ms。

如果用这个值去除以调度周期(也就是 cpu.cfs_period_us),50ms/100ms = 0.5,这样这个控制组被允许使用的 CPU 最大配额就是 0.5 个 CPU。

第三个参数是 cpu.shares,这个值是 CPU Cgroup 对于控制组之间的 CPU 分配比例,它的缺省值是 1024。

总结

第一点,cpu.cfs_quota_us 和 cpu.cfs_period_us 这两个值决定了每个控制组中所有进程的可使用 CPU 资源的最大值。

第二点,cpu.shares 这个值决定了 CPU Cgroup 子系统下控制组可用 CPU 的相对比例,不过只有当系统上 CPU 完全被占满的时候,这个比例才会在各个控制组间起作用。

容器内存

memory 01

memory 01

所以在 Swap 空间打开的时候,问题也就来了,在内存紧张的时候,Linux 系统怎么决定是先释放 Page Cache,还是先把匿名内存释放并写入到 Swap 空间里呢?

我们一起来分析分析,都可能发生怎样的情况。最可能发生的是下面两种情况:

第一种情况是,如果系统先把 Page Cache 都释放了,那么一旦节点里有频繁的文件读写操作,系统的性能就会下降。

还有另一种情况,如果 Linux 系统先把匿名内存都释放并写入到 Swap,那么一旦这些被释放的匿名内存马上需要使用,又需要从 Swap 空间读回到内存中,这样又会让 Swap(其实也是磁盘)的读写频繁,导致系统性能下降。

结论

swappiness 的取值范围在 0 到 100,值为 100 的时候系统平等回收匿名内存和 Page Cache 内存;一般缺省值为 60,就是优先回收 Page Cache;即使 swappiness 为 0,也不能完全禁止 Swap 分区的使用,就是说在内存紧张的时候,也会使用 Swap 来回收匿名内存。

memory-swappiness

memory-swappiness

容器磁盘

Direct I/O 模式,用户进程如果要写磁盘文件,就会通过 Linux 内核的文件系统层 (filesystem) -> 块设备层 (block layer) -> 磁盘驱动 -> 磁盘硬件,这样一路下去写入磁盘。

而如果是 Buffered I/O 模式,那么用户进程只是把文件数据写到内存中(Page Cache)就返回了,而 Linux 内核自己有线程会把内存中的数据再写入到磁盘中。在 Linux 里,由于考虑到性能问题,绝大多数的应用都会使用 Buffered I/O 模式。

容器磁盘 I/O 模式

容器磁盘 I/O 模式

这是因为 Buffered I/O 会把数据先写入到内存 Page Cache 中,然后由内核线程把数据写入磁盘,而 Cgroup v1 blkio 的子系统独立于 memory 子系统,无法统计到由 Page Cache 刷入到磁盘的数据量。

I/O调度层

I/O调度层的功能是管理块设备的请求队列。即接收通用块层发出的I/O请求,缓存请求并试图合并相邻的请求。并根据设置好的调度算法,回调驱动层提供的请求处理函数,以处理具体的I/O请求。

如果简单地以内核产生请求的次序直接将请求发给块设备的话,那么块设备性能肯定让人难以接受,因为磁盘寻址是整个计算机中最慢的操作之一。为了优化寻址操作,内核不会一旦接收到I/O请求后,就按照请求的次序发起块I/O请求。为此Linux实现了几种I/O调度算法,算法基本思想就是通过合并和排序I/O请求队列中的请求,以此大大降低所需的磁盘寻道时间,从而提高整体I/O性能。

常见的I/O调度算法包括Noop调度算法(No Operation)、CFQ(完全公正排队I/O调度算法)、DeadLine(截止时间调度算法)、AS预测调度算法等。

  • Noop算法:最简单的I/O调度算法。该算法仅适当合并用户请求,并不排序请求。新的请求通常被插在调度队列的开头或末尾,下一个要处理的请求总是队列中的第一个请求。这种算法是为不需要寻道的块设备设计的,如SSD。因为其他三个算法的优化是基于缩短寻道时间的,而SSD硬盘没有所谓的寻道时间且I/O响应时间非常短。
  • CFQ算法:算法的主要目标是在触发I/O请求的所有进程中确保磁盘I/O带宽的公平分配。算法使用许多个排序队列,存放了不同进程发出的请求。通过散列将同一个进程发出的请求插入同一个队列中。采用轮询方式扫描队列,从第一个非空队列开始,依次调度不同队列中特定个数(公平)的请求,然后将这些请求移动到调度队列的末尾。
  • Deadline算法:算法引入了两个排队队列分别包含读请求和写请求,两个最后期限队列包含相同的读和写请求。本质就是一个超时定时器,当请求被传给电梯算法时开始计时。一旦最后期限队列中的超时时间已到,就想请求移至调度队列末尾。Deadline算法避免了电梯调度策略(为了减少寻道时间,会优先处理与上一个请求相近的请求)带来的对某个请求忽略很长一段时间的可能。
  • AS算法:AS算法本质上依据局部性原理,预测进程发出的读请求与刚被调度的请求在磁盘上可能是“近邻”。算法统计每个进程I/O操作信息,当刚刚调度了由某个进程的一个读请求之后,算法马上检查排序队列中的下一个请求是否来自同一个进程。如果是,立即调度下一个请求。否则,查看关于该进程的统计信息,如果确定进程p可能很快发出另一个读请求,那么就延迟一小段时间。

前文中计算出的IOPS是理论上的随机读写的最大IOPS,在随机读写中,每次I/O操作的寻址和旋转延时都不能忽略不计,有了这两个时间的存在也就限制了IOPS的大小。现在如果我们考虑在读取一个很大的存储连续分布在磁盘的文件,因为文件的存储的分布是连续的,磁头在完成一个读I/O操作之后,不需要重新寻址,也不需要旋转延时,在这种情况下我们能到一个很大的IOPS值。这时由于不再考虑寻址和旋转延时,则性能瓶颈仅是数据传输时延,假设数据传输时延为0.4ms,那么IOPS=1000 / 0.4 = 2500 IOPS。

在许多的开源框架如Kafka、HBase中,都通过追加写的方式来尽可能的将随机I/O转换为顺序I/O,以此来降低寻址时间和旋转延时,从而最大限度的提高IOPS。

块设备驱动层

驱动层中的驱动程序对应具体的物理块设备。它从上层中取出I/O请求,并根据该I/O请求中指定的信息,通过向具体块设备的设备控制器发送命令的方式,来操纵设备传输数据。这里不再赘述。

基于磁盘I/O特性设计的技巧

在上一节中我们了解了Linux系统中请求到达磁盘的一次完整过程,期间Linux通过Cache以及排序合并I/O请求来提高系统的性能。其本质就是由于磁盘随机读写慢、顺序读写快。本节针对常见开源系统阐述一些基于磁盘I/O特性的设计技巧。

采用追加写

在进行系统设计时,良好的读性能和写性能往往不可兼得。在许多常见的开源系统中都是优先在保证写性能的前提下来优化读性能。那么如何设计能让一个系统拥有良好的写性能呢?一个好的办法就是采用追加写,每次将数据添加到文件。由于完全是顺序的,所以可以具有非常好的写操作性能。但是这种方式也存在一些缺点:从文件中读一些数据时将会需要更多的时间:需要倒序扫描,直到找到所需要的内容。当然在一些简单的场景下也能够保证读操作的性能:

  • 数据是被整体访问,比如HDFS

    • HDFS建立在一次写多次读的模型之上。在HDFS中就是采用了追加写并且设计为高数据吞吐量;高吞吐量必然以高延迟为代价,所以HDFS并不适用于对数据访问要求低延迟的场景;由于采用是的追加写,也并不适用于任意修改文件的场景。HDFS设计为流式访问大文件,使用大数据块并且采用流式数据访问来保证数据被整体访问,同时最小化硬盘的寻址开销,只需要一次寻址即可,这时寻址时间相比于传输时延可忽略,从而也拥有良好的读性能。HDFS不适合存储小文件,原因之一是由于NameNode内存不足问题,还有就是因为访问大量小文件需要执行大量的寻址操作,并且需要不断的从一个datanode跳到另一个datanode,这样会大大降低数据访问性能。
  • 知道文件明确的偏移量,比如Kafka

    • 在Kafka中,采用消息追加的方式来写入每个消息,每个消息读写时都会利用Page Cache的预读和后写特性,同时partition中都使用顺序读写,以此来提高I/O性能。虽然Kafka能够根据偏移量查找到具体的某个消息,但是查找过程是顺序查找,因此如果数据很大的话,查找效率就很低。所以Kafka中采用了分段和索引的方式来解决查找效率问题。Kafka把一个patition大文件又分成了多个小文件段,每个小文件段以偏移量命名,通过多个小文件段,不仅可以使用二分搜索法很快定位消息,同时也容易定期清除或删除已经消费完的文件,减少磁盘占用。为了进一步提高查找效率,Kafka为每个分段后的数据建立了索引文件,并通过索引文件稀疏存储来降低元数据占用大小。一个段中数据对应结构如下图所示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/60005da2b25a4c8f8e58402955ce4844~tplv-k3u1fbpfcp-zoom-1.image

awps-assets.meituan.net/mit-x/blog-…

在面对更复杂的读场景(比如按key)时,如何来保证读操作的性能呢?简单的方式是像Kafka那样,将文件数据有序保存,使用二分查找来优化效率;或者通过建索引的方式来进行优化;也可以采用hash的方式将数据分割为不同的桶。以上的方法都能增加读操作的性能,但是由于在数据上强加了数据结构,又会降低写操作的性能。比如如果采用索引的方式来优化读操作,那么在更新索引时就需要更新B-tree中的特定部分,这时候的写操作就是随机写。那么有没有一种办法在保证写性能不损失的同时也提供较好的读性能呢?一个好的选择就是使用LSM-tree。LSM-tree与B-tree相比,LSM-tree牺牲了部分读操作,以此大幅提高写性能。

  • 日志结构的合并树LSM(The Log-Structured Merge-Tree)是HBase,LevelDB等NoSQL数据库的存储引擎。Log-Structured的思想是将整个磁盘看做一个日志,在日志中存放永久性数据及其索引,每次都添加到日志的末尾。并且通过将很多小文件的存取转换为连续的大批量传输,使得对于文件系统的大多数存取都是顺序的,从而提高磁盘I/O。LSM-tree就是这样一种采用追加写、数据有序以及将随机I/O转换为顺序I/O的延迟更新,批量写入硬盘的数据结构。LSM-tree将数据的修改增量先保存在内存中,达到指定的大小限制后再将这些修改操作批量写入磁盘。因此比较旧的文件不会被更新,重复的纪录只会通过创建新的纪录来覆盖,这也就产生了一些冗余的数据。所以系统会周期性的合并一些数据,移除重复的更新或者删除纪录,同时也会删除上述的冗余。在进行读操作时,如果内存中没有找到相应的key,那么就是倒序从一个个磁盘文件中查找。如果文件越来越多那么读性能就会越来越低,目前的解决方案是采用页缓存来减少查询次数,周期合并文件也有助于提高读性能。在文件越来越多时,可通过布隆过滤器来避免大量的读文件操作。LSM-tree牺牲了部分读性能,以此来换取写入的最大化性能,特别适用于读需求低,会产生大量插入操作的应用环境。