Gitpod另辟蹊径打造全新开发环境 Kubernetes是一条 死胡同 ! 6年挣扎后 (gitpod要验证电话号码怎么办)

Gitpod另辟蹊径打造全新开发环境 Kubernetes是一条 死胡同 ! 6年挣扎后 (gitpod要验证电话号码怎么办)

近年来,Kubernetes 似乎已经成为建立远程标准化及自动化开发环境的最佳选项,我们也不例外。之前六年时间,我们大规模建立起广受欢迎的云开发环境平台,其中的 150 万用户定期运行着成千上万个开发环境。但在实践过程中,我们逐渐意识到 Kubernetes 并不是建立开发环境的正确选择。

我们曾在 Kubernetes 上开展过建立开发环境的诸多实验,过程当中不乏各种失败和挫折。多年来,我们尝试过许多涉及 SSD、PVC、EBPF、SecComp Notify、TC 和 io_uring、shiftfs,FUSE 和 idmapped mounts 的想法,范围也从 Microvms、Kubevirt 再到 Vcluster。

所谓追求最佳基础设施,就是希望在安全性、性能和互操作性之间找到平衡点。使其能够在稳定运行的同时,建立起一套能够适应系统扩展挑战的解决方案,既能够在处理代码执行时保持安全、又能以良好的稳定性支撑起开发者的使用需求。

接下来的内容主要探讨的,并不在于 Kubernetes 到底适不适合用于支撑生产级工作负载。相反,我们的重点在于怎样在 Kubernetes 上建立起全面且良好的开发者使用体验。

换言之,我们想要讨论的,是如何在云端构建起理想的开发环境。

开发环境为什么值得单独拿来一说?

在深入讨论之前,我们先聊聊与生产工作负载相比,开发环境到底有哪些关键的特别之处:

这些特征共同成就了开发环境的特殊性,而且与典型的应用工作负载不同,这将显著影响我们在开发过程中做出的每一个基础设施决策。

当今系统:Kubernetes 占据绝对主导

当我们启动 Gitpod 时,Kubernetes 似乎就是最理想的基础设施选项。它对可伸缩性、容器编排以及丰富生态系统的承诺,与我们对于云开发环境的愿景完全一致。然而,随着我们业务规模的扩展以及用户群体的增长,我们在安全性和状态管理方面开始遇到一些挑战,并很快意识到 Kubernetes 在这些方面存在局限。从根本上讲,Kubernetes 是为了运行具有良好控制的应用类工作负载所设计,却并不太适应以不守规矩为常态的开发环境。

对 Kubernetes 进行大规模管理是件复杂的工作。尽管 GKE 和 EKS 之类的托管服务有助于缓解某些痛点,但它们自己也同样会带来一系列约束和局限性。我们发现,许多希望操作 CDE 的团队低估了 Kubernetes 的复杂性,这也为我们长久以来自主管理的 Gitpod 产品带来了沉重的支持负担。

陷入困境的资源管理

我们面临的核心挑战之一就是资源管理,特别是在不同环境下如何分配 CPU 与内存资源。乍看之下,在节点上运行多个环境,似乎意味着可能在各节点之间共享资源(例如 CPU、内存、IO 和网络带宽)。但在实践层面,这会产生严重的相邻影响,进而损害用户的开发体验。

CPU 挑战

CPU 时间似乎是在不同环境间最易于共享的资源类型。大多数时间下,开发环境并不需要占用太多 CPU 资源,但在需要时又要求迅速提供。而一旦用户的语言服务器开始滞后或者终端发生卡顿时,开发体验将立即受到影响。开发环境这种高度强调峰值状态的 CPU 需求(大段闲置期加上频繁出现的偶发资源需求)导致我们很难对其趋势做出预测。

至于解决方案,我们尝试了基于各种 CFS(完全公平调度程序)的方案,并使用 Daemonset 建立了自定义控制器。但其中最大的挑战,在于我们无法预测何时需要 CPU 带宽,而只能发现何时需要(通过观察 CGROUPCPU_STATS 的 nr_throttled)。

即使是使用静态 CPU 资源限制,也同样无法彻底解决挑战。因为与应用级工作负载不同,开发环境需要在同一容器中运行多个进程。这些进程会相互竞争 CPU 带宽,因此导致 VS Code 服务器无法获取 CPU 时间而断开连接等问题。

我们也尝试过调整各个进程的优先级来解决这个问题,例如上调 BASH 或者 VScode-server 的优先级。然而,这些进程优先级适用于整个进程组(取决于内核的 autogroup scheduling 调度配置),因此 VS Code 终端内启动的编译器也会大量吞噬计算资源。总而言之,要想使用进程优先级来对抗终端卡顿,至少得精心设计出符合开发实践要求的控制循环才有可能起效。

我们还引入了基于 cgroupv1 构建的自定义 CFS 和基于进程优先级的控制循环,并在 Kubernetes 1.24 平台上迎来了 cgroupsv2。由 Kubernetes 1.26 版本引入的动态资源分配,意味着我们不再需要部署守护程序并直接修改 cgroup,但这似乎需要以控制循环速度和有效性为代价。总之,这些方案都依赖于对 CFS 限制还有正确取值的秒级重新调整,对应的工作量可见一斑。

内存管理

内存管理也有属于自己的一系列挑战。为每个环境分配固定数量的内存,意味着在最大占用之下,分配过程虽然简单但成效有限。在云端,内存则属于成本高昂的资源类型,每位租户都希望超订、俗称“多吃多占”。

在 Kubernetes 1.22 中 swap-space 出现之前,我们几乎不可能对内存进行超订,因为回忆的回收将不可避免地消除进程。随着 swap-space 的引入,超订内存的需求开始逐渐消失,托管开发环境中的交换实践真正成为首选方案。

存储性能优化

存储性能对于启动性能和开发环境的使用体验同样非常重要。我们发现,IOPS 和延迟效应对于开发者体验的影响尤其明显。其中 I/O 带宽直接影响工作区的启动性能,在创建/还原备份或者提取成规模的工作区内容时体现得最为直接。

我们尝试了各种设置,希望能在速度、可靠性、成本以及性能之间找到适当的平衡。

事实证明,对本地磁盘的备份和恢复是现基成本高昂的操作。我们使用 Daemonset 实施了一套解决方案,该方案面向 S3 上传和下载的均是未经压缩的 TAR 归档文件。这种方法需要认真平衡 I/O、网络带宽和 CPU 资源:例如,压缩档案会在节点上消耗掉大量可用 CPU,而未压缩备份产生的额外流量则会严重占用网络传输带宽(除非认真控制同时启动/停止的工作区数量)。

节点上的 I/O 带宽跨工作区共享。我们发现,除非对各个工作区上的可用 I/O 带宽做出限制,否则其他工作区很可能由于得不到 I/O 带宽而停止运行。内容备份/还原造成的此类问题最为严重。为此我们实施了基于 cgroup 的 IO 限制器,用于为各个环境施加固定的 I/O 带宽限制以解决这方面问题。

自动规模伸缩与启动时间优化

我们的另一个主要目标,就是不惜一切代价尽可能缩短启动时间。不可预测的等待时间会大大影响生产效率和用户满意度。但是,这个目标往往跟我们希望密集加载工作区,从而实现设备利用率最大化的愿望相互抵触。

我们刚开始认为,在同一节点上运行多个工作区将有助于优化启动时间,毕竟这样可以实现缓存资源共享。但实际情况与我们的预期完全相反。现实情况是,Kubernetes 为了应对所有将要执行的操作而令启动时间有了硬性下限,就是为了给负载转移到位留下充足的时间。

所以除了将工作区保持在热待机状态(但这非常昂贵)之外,我们还得找到其他方法来优化启动速度。

提前扩展:我们的方案演进之路

为了最大程度缩短启动时间,我们探索了各种规模扩展方法:

峰值负载的等比例自动伸缩

为了更有效地处理峰值负载,我们建立起一套等比例自动伸缩系统。该方案能够控制伸缩速率,依靠的就是初始开发环境的速率函数。它能够使用暂停镜像启动空 pod 来发挥作用,使得我们能够快速纠结容量以满足峰值需求。

镜像拉取优化:一场旷日持久的拉锯战

启动时间优化当中的另一个关键方向,就是改善镜像拉取时间。工作区容器镜像(包含开发者所使用的全部工具)最多可能达到 10 GB 的未压缩体量。因此在为各个工作区下载和提取相应数据时,自然会严重占用节点上的资源。为此,我们探索了多种加快镜像拉取速度的策略:

可以看到,并不存在一种能够适应所有镜像缓存需求的解决方案,我们只能在复杂性、成本和限制(可以使用的镜像)之间做出权衡。最终在实践中,我们发现保证工作区镜像的同质性才是优化启动时间最为直接的方法。

网络复杂性

Kubernetes 中的网络也有自己的一系列挑战,具体包括:

最初,我们使用 Kuberntes 服务控制了对单个环境端口(例如在工作区中运行的 IDE 或者服务)的访问,并且连带将访问量转发至服务的 Ingress 代理,再使用 DNS 解析该服务。但由于服务数量过于庞大,这种方式很快就出现了可靠性问题。名称解析经常失败,稍有不慎(例如设置 enableServiceLinks:false)甚至可能导致整个工作区宕机。

安全与隔离:在灵活性与安全保护间求取平衡

我们在基于 Kubernetes 的基础设施当中面临的最大挑战之一,就是如何提供一套安全的环境,同时为用户提供高效开发所必需的灵活性。用户希望能够随时安装其他工具(例如使用 APT-GET 进行安装)、运行 Docker,甚至在开发环境录中进一步设置 Kubernetes 集群。事实证明,在这些要求与强有力的安全保障之间求取平衡,又成了令人头痛的新难题。

最简单粗暴的方法:root 访问

最简单粗暴的解决方案,就是允许用户 root 访问其容器。但是,这种方法很快就暴露出了其缺陷:

很明显,我们还得想一种更完备的解决办法。

用户名称空间:更细致的解决方案

为了应对这些挑战,我们开始转向用户名称空间。这是一项 Linux 内核功能,允许对容器中的用户和组 ID 映射进行细粒度控制。这种方法让我们能够在容器中为用户提供“类 root”的权限,但又不致损害主机系统的安全性。

尽管 Kubernetes 在 1.25 版本中引入了对用户名称空间的支持,但我们早在 1.22 版本起就开始实现了自己的解决方案。我们的实施使用到好几款复杂的组件:

为了实现此类功能,我们最终在 Kubernetes 容器内建立了另一个网络名称空间。该容器首先使用 Slirp4netns 与外部连接,之后再使用 VETH Pair 以及自定义及 Nftables 规则。

但这套安全模型的实施,也给我们带来了以下几项新的挑战:

微虚拟机实验

在应对 Kubernetes 挑战的过程中,我们开始以中立的态度探索微虚拟机技术的实践意义,包括 Firecracker、Cloud Hypervisor 以及 QEMU 等等。这种探索来自对于改善资源隔离、与其他工作负载(例如 Kubernetes)的安全兼容等追求,同时希望能够维持住容器化技术的固有优势。

微虚拟机的承诺

微虚拟机提出的承诺可以说相当诱人,也与我们在云开发环境中的目标保持一致:

微虚拟机面临的挑战

但是,我们对微虚拟机的实验也发现了不少重大难题:

微虚拟机实验带来的教训

尽管微虚拟机最终未能成为我们主基础设施的实际解决方案,但本轮实验仍然让我们积累下了宝贵的经验和教训:

事实证明,Kubernetes 在充当开发环境平台方面表现堪忧

正如我们在文章起首所提到,对于开发环境,我们需要一套在设计阶段就充分考虑到其独特状态的托管系统。我们需要为开发人员提供必要的权限,同时又保证具有理想的安全边界。总而言之,我们需要尽一切努力让操作趋近于底层,同时又不致危害安全性。

就目前来看,Kubernetes 确实能够满足上述需求,但却需要付出相当大的代价或者说成本。我们也用自己的血泪教训,真正认识到应用级负载一系统级负载之间的区别。

别误会, Kubernetes 仍然是个令人难以置信的伟大项目,它拥有敬业且热情的社区支持,并且衍生出一个极其丰富的生态系统 。如果大家正在运行应用级工作负载,那么 Kubernetes 绝对是个理想的选择。但是对于系统级工作负载,比如说开发环境,Kubernetes 在安全性和运营开销方面都面临着巨大挑战。配合微虚拟机和明确的资源规划肯定有助于解决问题,但也使得成本在决策流程中占据了过高的比重。

因此在经过多年的逆向工程和运用 Kubernetes 构建开发环境的实践探索之后,我们决定退后一步,从头开始规划更适应未来开发架构的平台样式。2024 年 1 月我们开始着手,2024 年 10 月正式发布,这就是 Gitpod Flex。

它的诞生,离不开我们以超大规模体量在六年多时间内安全运行开发环境的实践经验,以及过程当中积累下的无数心得和洞见。

面向未来的开发环境

在 Gitpod Flex 当中,我们延续了 Kubernetes 的很多基础性优势,例如对控制理论的自由应用以及声明性 API,同时简化了架构并改善了其安全基础。

我们使用受 Kubernetes 启发而来的控制平面对开发环境进行编排。我们还引入了一系列针对开发环境的必要抽象层,抛弃了大量并不需要的基础设施复杂性因素,同时将零信任安全性放在设计路线的第一位。

这套全新架构使得我们能够无缝集成开发容器。我们还解锁了在桌面端运行开发环境的能力。由于不再需要承担 Kubernetes 平台的体量,Gitpod Flex 能够在三分钟以内在任意数量的区域实现自部署,同时在合规性层面实现细粒度控制,还可根据不同组织的业务模型随意调整灵活性边界和领域设置。

在为标准化、自动化且安全的开发环境建立平台时,选择合适的系统无法能帮助我们改善开发者体验、减轻运营负担并改善成本投入。Gitpod Flex 的意义并不在于替代 Kubernetes,而在于为大家提供更多选择,在更广阔的空间之内尽可能改善开发团队的工作体验。

作者简介:

声明:本文来自用户分享和网络收集,仅供学习与参考,测试请备份。