Docker如何做资源隔离,还是不做?怎样做才能最大化利用服务器资源,避免浪费?
我们看看Netflix是如何做到的。
我们都曾有过吵闹的邻居。无论是在咖啡馆,还是穿过公寓的墙壁,它总是具有破坏性。事实证明,在共享空间中保持良好的礼仪不仅对人很重要,对Docker容器也很重要。
当你在云中运行时,你的containers在一个共享空间中;特别是它们共享主机实例的CPU内存层次结构。
由于微处理器速度如此之快,计算机体系结构设计已经发展到在计算单元和主内存之间添加不同级别的缓存,以隐藏将bits带到CPU的延迟。然而,这里的关键观点是,这些Cache在cpu之间部分共享,这意味着不可能对共同承载的containers进行完美的性能隔离。如果在containers旁边的核心上运行的containers突然决定从RAM中获取大量数据,这将不可避免地导致更多的缓存丢失(从而导致潜在的性能下降)。
传统上,减轻性能隔离问题一直是操作系统任务调度程序的职责。在Linux中,当前的主流解决方案是CFS(Completely Fair Scheduler)( https://en.wikipedia.org/wiki/Completely_Fair_Scheduler)。它的目标是以"公平"的方式将正在运行的进程分配给CPU的时间片。
CFS得到了广泛的应用,因此经过了良好的测试,世界各地的Linux机器运行起来都具有合理的性能。那么,为什么要搅乱它呢?事实证明,对于Netflix的大多数用例来说,它的性能远远不是最优的。Titus(https://netflix.github.io/titus/)是Netflix的集装箱平台。每个月,我们在Titus上的数千台机器上运行数百万个容器,为数百个内部应用程序和客户提供服务。这些应用程序的范围从支持面向客户的视频流服务的关键低延迟服务,到用于编码或机器学习的批处理作业。维护这些不同应用程序之间的性能隔离对于确保内部和外部客户的良好体验至关重要。
通过将一些CPU隔离责任从操作系统转移到包含组合优化和机器学习的数据驱动解决方案,我们能够显著提高这些容器的可预测性和性能。
CFS非常频繁地(每隔几微秒)应用一组启发式操作,这些启发式操作封装了围绕CPU硬件使用的最佳实践的一般概念。
相反,如果我们减少干预的频率(每隔几秒钟),但在分配计算资源的过程方面做出更好的数据驱动决策,以最小化配置噪音,结果会怎样?
减轻CFS性能问题的一种传统方法是让应用程序所有者通过使用核心固定或nice值手动协作。然而,通过基于实际使用信息检测搭配机会,我们可以自动做出更好的全局决策。例如,如果我们预测容器A将很快变得非常CPU密集型,那么也许我们应该在一个不同的NUMA(https://en.wikipedia.org/wiki/Non-uniform_memory_access)套接字上运行它,而容器B对延迟非常敏感。这避免了过多的抖动缓存为B和平衡的压力,对L3(https://en.wikipedia.org/wiki/Memory_hierarchy)缓存的机器。
OS任务调度程序所做的实际上是解决一个资源分配问题:我有X个线程要运行,但只有Y个cpu可用,我如何将线程分配给cpu,以产生并发的假象?
作为一个演示示例,让我们考虑一个包含16(https://en.wikipedia.org/wiki/Hyper-threading)个超线程的玩具实例。它有8个物理超线程核心,分裂在2个NUMA套接字上。每个超线程与其邻居共享其L1和L2缓存,并与套接字上的其他7个超线程共享其L3缓存:
如果我们想在4个线程上运行容器A,在这个实例上在2个线程上运行容器B,我们可以看看"坏"和"好"的布局决策是什么样子的:
第一个位置在直觉上是不好的,因为我们可能通过L1/L2缓存在前2个核心上创建A和B之间的并置噪声(collocation noise),而通过L3缓存在套接字上创建套接字,同时保留整个套接字为空。第二个位置看起来更好,因为每个CPU都有自己的L1/L2缓存,我们更好地利用了两个可用的L3缓存。
资源分配问题可以通过数学的一个分支组合优化有效地解决,例如用于航空公司调度或物流问题。
我们将问题表示为一个混合整数程序(MIP)。给定一组K个容器,每个容器在拥有d个线程的实例上请求特定数量的cpu,目标是找到一个大小为M (d, K)的二进制赋值矩阵,以便每个容器获得它请求的cpu数量。损失函数和约束包含了表示先验的良好配置决策的各种术语,例如:
· 避免将容器分散到多个NUMA套接字(避免潜在的缓慢的跨套接字内存访问或页面迁移)
· 除非需要,否则不要使用超线程(以减少L1/L2抖动)
· 尝试平衡L3缓存上的压力(基于对容器硬件使用的潜在测量)
· 不要在不同的位置决策之间做太多的调整
考虑到系统的低延迟和低计算需求(我们当然不希望花费太多的CPU周期来弄清楚容器应该如何使用CPU周期!),我们实际上能在实践中实现这一点吗?
我们决定通过Linux cgroups(http://man7.org/linux/man-pages/man7/cgroups.7.html)实现该策略,因为CFS完全支持它们,方法是根据容器到超线程的所需映射修改每个容器的cpuset cgroup。通过这种方式,用户空间进程定义了一个"围栏",CFS在其中对每个容器进行操作。实际上,我们消除了CFS启发式算法对性能隔离的影响,同时保留了它的核心调度功能。
这个用户空间进程是一个名为Titus - isolation的Titus(https://github.com/Netflix-Skunkworks/titus-isolate)子系统,其工作原理如下。在每个实例上,我们定义三个触发布局优化的事件:
· add: Titus调度程序为这个实例分配了一个新的容器,需要运行它
· remove:一个正在运行的容器刚刚完成
· rebalance:容器中的CPU使用量可能发生了变化,因此我们应该重新评估我们的位置决策
当最近没有其他事件触发位置决策时,我们会定期对重新平衡事件进行排队。
每次触发一个放置事件时,Titus -隔离都会查询一个远程优化服务(https://en.wikipedia.org/wiki/Turtles_all_the_way_down),从而解决容器到线程的放置问题。
然后,该服务查询一个本地GBRT(https://en.wikipedia.org/wiki/Gradient_boosting)模型(每隔几小时对从整个Titus平台收集的数据进行重新培训),该模型预测未来10分钟内每个容器的P95 CPU使用情况(条件分位数回归)。该模型既包含上下文特性(与容器关联的元数据:谁启动了它、图像、内存和网络配置、应用程序名称……),也包含从主机定期从内核CPU会计控制器(https://www.kernel.org/doc/Documentation/cgroup-v1/cpuacct.txt)收集的容器的历史CPU使用量的最后一个小时中提取的时间序列特性。
然后,这些预测被输入到一个MIP中,该MIP将被动态求解。我们使用cvxpy(https://www.cvxpy.org/)作为一个很好的通用符号前端来表示问题,然后可以将其输入各种开源或专有的MIP解决程序后端。由于MIPs是np困难的,因此需要采取一些谨慎的措施。我们对求解程序施加了一个艰难的时间预算,以将分支和削减策略驱动到低延迟状态,并在MIP缺口周围设置护栏,以控制找到的解决方案的总体质量。
然后,服务将位置决策返回给主机,主机通过修改容器的cpuset来执行该决策。
例如,在任何时候,一个包含64个逻辑CPU的r4.16xlarge可能看起来是这样的(颜色比例表示CPU使用量):
该系统的第一个版本带来了令人惊讶的好结果。我们平均将批处理作业的总体运行时减少了几个百分点,同时最重要的是减少了作业运行时差异(隔离的一个合理代理),如下所示。在这里,我们看到一个实际的批处理作业运行时分布,有和没有改进的隔离:
请注意,我们主要是如何使长时间运行的异常值问题消失的。不幸的吵闹邻居的右尾巴现在不见了。
在服务业方面,增长更为显著。一个专门为Netflix流媒体服务的Titus中间件服务在高峰流量时减少了13%的容量(减少了1000多个容器),以满足相同负载所需的P99延迟SLA!我们还注意到,由于内核在缓存失效逻辑上花费的时间要少得多,因此计算机上的CPU使用率也有了大幅下降。我们的容器现在更容易预测,速度更快,机器的使用也更少了!鱼与熊掌不可兼得的情况并不常见。
我们对该领域迄今取得的进展感到兴奋。我们正从多个方面着手扩展本文提出的解决方案。
我们希望扩展系统以支持CPU超订阅。我们的大多数用户都不知道如何正确设置应用程序所需的cpu数量。事实上,这个数字在容器的生命周期内是变化的。由于我们已经预测了容器未来的CPU使用情况,所以我们希望自动检测和回收未使用的资源。例如,如果我们能够沿着下图的各个轴检测用户的灵敏度阈值,则可以决定将特定容器自动分配给未充分利用cpu的共享cgroup,从而更好地提高总体隔离和机器利用率。
我们还希望利用内核PMC事件()更直接地优化缓存噪声。一种可能的方法是使用Amazon最近引入的基于Intel的bare metal实例(https://phoenixnap.com/blog/what-is-bare-metal-hypervisor),该实例允许对性能分析工具进行深度访问。然后,我们可以将这些信息直接输入到优化引擎中,从而转向一种更有监督的学习方法。这需要一个合适的连续随机化的位置收集无偏反设事实,所以我们可以建立某种干扰模型("什么将容器的性能在下一分钟,如果我把它的一个线程在同一核心容器B,知道还有C运行在同一个插座吗?")。