容器共享底层操作系统(OS)的组件,所以OS为容器分配的资源(如CPU、磁盘、网络带宽和内存)提供资源隔离至关重要。目前,Linux中的cgroups通过在内核中分配、计量和实施资源使用来实现资源隔离。
1 问题
容器在发送或接收网络流量时可以利用比各自的cgroup分配的更多的CPU,从而有效地打破隔离: 处理中断所花费的时间通常不会被计算到容器发送或接收通信流上。
1.1 软中断的处理开销
-
软件中断在硬件中断处理结束时或内核重新启用softirq处理时被进行检查。软件中断在进程上下文中运行。也就是说,无论哪个不幸的进程正在运行,都必须使用它的预定时间来服务softirq。在这里,当一个容器必须使用自己的CPU时间来处理另一个容器的流量时,隔离就会打破。
-
内核试图通过限制softirq处理程序运行一段固定的时间或预算的数据包数量来最小化进程上下文中的softirq处理。当超出预算时,softirq停止执行,并计划运行ksoftirqd。Ksoftirqd是一个内核线程(它不在进程上下文中运行),为剩余的softirqs服务。每个处理器有一个ksoftirqd线程。因为ksoftirqd是一个内核线程,所以它处理数据包的时间不会计入任何容器。这通过限制调度其他容器的可用时间或允许用尽其组配额的容器获得更多处理资源来打破隔离。
1.2 收发消息情况分析
-
数据包从其套接字穿过内核到达网卡,是在进程上下文中执行的,因此发送数据包所花费的时间被计入正确的容器中(和上文冲突),但有两种情况,隔离会被Break:
-
当网卡完成数据包传输时,它会安排一个中断来释放数据包资源。这项工作是在softirq context完成的,因此可能会向没有发送流量的容器收费
-
数据包在进程上下文中从套接字被带到缓冲区。然而,在缓冲数据包时,内核系统调用退出。然后,软件中断负责将数据包从缓冲区中取出,并将其移至网络堆栈中。
-
-
接收数据包比发送数据包会产生更高的softirq开销。数据包从驱动程序的环形缓冲区一直移动到softirq上下文中的应用程序套接字。这种遍历可能需要与多个协议处理器(例如,IP、TCP)、NFVs、网卡卸载(例如GRO )接口,或者甚至发送新的数据包(例如,TCP ack或ICMP消息)。总之,整个接收链是在softirq context中执行的,因此可能有相当长的时间没有计入正确的容器。
研究人员已经开发了隔离CPU和网络带宽的方案``todo`。但隔离基于网络的处理的研究非常有限
2 贡献
Iron实现了一组细内核检测工具,集成了Linux cgroup调度器,在维持保持内核中中断处理的效率和响应性,获得细粒度的处理数据包的成本,来为容器的流量收费,并实现了一种基于硬件的丢包机制,将数据包丢弃到已耗尽其分配的容器中。
2.1 现有方案的不足
Iron’s contributions are put in context.
-
Resource containers:通过一个特殊的活形成一个抽象,捕获、计费系统资源;当一个进程被调度时,一个接收系统调用延迟调用内核中的协议处理,因此处理数据包所花费的时间被正确地计入一个进程。但是很低效,因为要给每个接受进程分配一个per-socket线程。
所以Iron遵循softirq handler集中处理所有进程中断的原则,旨在减小带来的额外开销(尤其是上述的多线程)和复杂性
-
Redesigning the OS:一些系统库将网络协议处理转移到用户态,包处理直接从网卡copy到application,这样就可以直接计费了;但这同样会带来额外开销,并且会让用户使用门槛变高,管理难度加大;
2.2 实验:网络拥塞带来的影响
引入惩罚因子这个概念,通过一些数据证明现象的存在
3 设计
基本思路:计算在softirq context中处理数据包所花费的时间。在获得包成本后,Iron与Linux调度程序集成,为softirq处理对容器进行计费。当容器的运行时间耗尽时,Iron通过限制容器并通过基于硬件的方法丢弃传入的数据包来实施强化隔离。
3.1 计费方案
如何计费以及将数据包分配给容器,并描述了用于计费的状态
3.1.1 接收端
接收数据的时候,堆栈中低层的会嵌套调用堆栈高层的函数,可以通过从堆栈中低层的接收函数结束时间中减去函数开始时间来获得处理数据包所花费的时间。
Iron使用 netif_receive_skb获取每个包成本,它是第一个在驱动程序之外处理单个数据包的功能,与传输协议无关。获取时间差并不是准确的,因为内核是可抢占的,调用树中的函数可以随时中断。
为了确保只捕获处理数据包所花费的时间,Iron依赖于调度程序数据。调度器保存线程已经运行的累计执行时间(cumtime),以及线程最后被换入的时间(swaptime),结合本地时钟(now),开始和结束时间可以计算为:time = cumtime+(now-swaptime)。此外,进入处理硬件中断(do_IRQ)的函数、处理softirqs和执行skb垃圾收集都有开销,这些开销被集中在一起,并以加权的方式分配:
在Linux中,由softirq处理程序(do softirq)处理六种类型的soft_IRQ:HI、TX、RX、TIMER、SCSI和TASKLET。对于每个中断,我们获得总的do_IRQ成本,表示为;以及处理每个特定softirq的成本(表示为等)。注意软件中断是在硬件中断结束时处理的,所以 ,与处理中断相关的开销定义为:,该中断内recv overhead的公平份额为 。最后,在给定do softirq调用中处理的数据包之间平均分配,以获得添加到每个数据包的固定费用。
3.1.2 发送端
与接收器类似,使用net_tx_action操作获得发送批处理的开始和结束时间,然后平均分配到每个包。而固定开销也会平均分配。开销是按每个内核计算的。
3.1.3 映射到容器
Iron必须识别一个包所属的容器。在sender,当一个skb在qdisc层排队时,它与它的cgroup相关联。在接收器上,Iron维护一个IP地址hashtable,在将数据包复制到套接字时填充该hashtable。
此外,每个进程都维护一个在softirq context中处理的数据包的列表及其各自的开销(每个内核)。每个进程的结构最终被合并成一个全局的per-cgroup的结构。Iron在调度器获得全局锁时合并状态(减少了新建锁的开销),每个cgroup结构维护一个变量(),该变量指示一个cgroup是否应该被记入网络处理。
3.2 隔离方案
通过将计费数据与Linux的CFS(完全公平调度器)中的CPU分配 集成,并在容器受到限制时丢弃数据包来实现的。
3.2.1 集成scheduler
scheduler 通过一个混合方案来实现Cgroup的CPU分配,该方案同时保持local(即每个核)和globle状态:容器被允许在一段内运行给定的。scheduler通过在细粒度级别更新local state和在粗粒度级别更新globle state来最小化锁定开销。
-
在globle level,在一个CPU周期开始时被设置为。scheduler从中减去一个,并将其分配给一个核。逐渐递减,直到=0或周期结束。无论如何,在一个周期结束时,会被重新填充到配额。
-
在local level,从全局中提取的分配给一个变量。scheduler会随着cgroup占用CPU而减少,当为零时,scheduler会试图从全局poo中获取一个,如果不够了,就等下一个周期
3.2.1.1 globle level填充run_time
Iron:周期结束时的全局补充算法
1: if gained > 0 then
2: runtime ← runtime + gained
3: gained ← 0
4: end if
5: if cgroup_idled() and runtime > 0 then
6: runtime ← 0
7: end if
8: runtime ← quota + runtime
9: set_timer(now + period)</pre>
-
1-4行:在Iron中,一个全局变量跟踪一个容器应该拿回的时间(也就是它用来处理了另一个容器的softirqs);
-
5-7行:如果容器受到其需求的限制没有使用其先前的分配,也就是处于idle状态(空闲状态),那么需要额外补给的份额置零(保留CFS策略,该策略不允许累积未使用的周期用于后续周期。)
-
8-9行:分配的来填充并更新时间
3.2.1.2 local level填充rt_remain
Iron:re_remain<0时触发的向globle runtime请求算法
1: amount ← 0
2: min_amount ← slice - rt_remain
3: if cpuusage > 0 then
4: if cpuusage > gained then
5: runtime ← runtime - (cpuusage - gained)
6: gained ← 0
7: else
8: gained ← gained - cpuusage
9: end if
10: else
11: gained ← gained + abs(cpuusage)
12: end if
13: cpuusage ← 0
14: if runtime = 0 and gained > 0 then
15: refill ← min(min_amount, gained)
16: runtime ← refill
17: gained ← gained - refill
18: end if
19: if runtime > 0 then
20: amount ← min(runtime, min_amount)
21: runtime ← runtime - amount
22: end if
23: rt_remain ← rt_remain + amount</pre>
引入变量是为了维护local level计费:
-
2-9:>0表示容器需要为未计算的网络周期付费
-
10-12:<0表示容器可以拿回被别的容器占用的资源
-
14-18:globle runtime耗尽但是有gained可以填充runtime
-
19-22:正常的run_time分配
3.2.2 丢包减少影响
丢弃receiver端多余的数据包更为重要,因为受限制的reciver可能会继续接收流量,从而破坏隔离。
现代系统用NAPI管理网络中断。收到新数据包后,NAPI会禁用硬件中断,并通知OS用轮询方法来检索数据包。同时,网卡将额外接收的包简单地放入环形缓冲区。当内核轮询网卡时,它会从环形缓冲区中删除尽可能多的数据包。当移除的数据包数量少于预算时,NAPI轮询退出,中断驱动接收恢复。
基于硬件的丢包的工作原理如下
-
假设网卡每个容器有一个队列。Iron使用从队列到其容器(即cgroup)的映射来扩展NAPI队列结构。
-
当调度程序限制一个容器时,它会修改任务组中的一个布尔值。
-
不同于原生NAPI,Iron不会从容器被限制的队列中轮询数据包。
-
从内核的角度来看,队列是从轮询列表中剥离出来的,这样它就不会不断地被重新填充。
-
从网卡的角度来看,内核不会轮询队列中的数据包,因此它会保持轮询模式,并保持硬件中断禁用。
-
-
如果收到新的数据包,只需将它们插入环形缓冲区。这种技术有效地减轻了接收开销,因为内核不会被中断或者被要求代表节流容器做任何工作。当调度程序释放一个容器时,它重置它的布尔值,并调度一个softirq来处理可能在排队的数据包。
此外还有一个轻微的优化,Iron还可以在容器被限制之前丢弃数据包。也就是说,如果一个容器正在接收大量流量,并且该容器在其配额的%以内,则可以丢包。这允许容器使用一些剩余的运行时间来阻止大量的传入数据包。
Iron可以为每个内核分配固定数量的队列,然后将有问题的容器动态映射到自己的队列中。而对于没有过重流量负担的容器可以通过使用一种通过实现__netif_receive_skb
来基于软件的丢包。这种动态分配方案从SENIC中得到启发,SENIC使用类似的方法来调整基于网卡的速率限制器。或者,可以根据预先购买的带宽分配将容器映射到队列。
网友评论