Eru in 2020

我做私有云已经做腻了,那就做个公有云玩玩吧

我在去年的时候写下了 Eru in 2019 ,当时我没去想 Eru 管理着数千服务器数万容器/虚拟机混排在一个集群中会怎样。它是有这个能力,但之前并未有这样的规模,尤其是理论管理机器规模超越 5K 台是得有公司支持的。随着我司市值超越百度,Eru 的规模也越来越大,驱动的项目也越来越多。正所谓用户越多锅越大,整个 2020 上半年我都在思考它的定位和未来的发展。毕竟从一开始只有我和 tonicbupt 2个人在国企芒果 TV 做着这个没人懂的项目到今天已经 5 年了,再只把它当做个人项目或者说「反正要换 k8s 」这样的心态是有问题的。2018 错过了上 k8s 车的机会,未来短期甚至中期内再上车就很难了,换引擎折腾一段时间这个成本我是不会接受的。

毕竟,革命尚未成功呐同志们。

状态

经过一年的 CRUD,目前 Eru 的代码已经从 10K 行膨胀到了 18K 行,当然横向对比来说依然称得上是「最简单」的资源编排和调度系统。随着公司快速发展的这 2 年期间,我记忆里面关于 Eru 的事故并且产生了影响的只有 2 个,要让一个旁路系统能给主业务造成影响实在是太难了:

  1. 通过 stdout/stderr 收集日志导致进程阻塞。
  2. physical server down 导致状态检查机制失效。

对于 1 来说,container 时代大多数的日志都是走 stdout/stderr 输出的,对于 Eru 而言就是其 Agent 负责日志转发,基于 attach 接口。这个接口有且只有 1MB Cache,并且是同步的,因此在极限情况下会阻塞容器进程。以前我们在 ENJOY 也遇到过,当时是强力的 flex 折腾了一套基于 syslog + kafka 的 flow 撑住了异常情况下巨量日志的输出,然而 Shopee 并没这套机制。为此我们设计了一个 buffered pipe 来解决这个问题,允许在巨量日志的情况下,agent 自身可以通过丢日志来防止阻塞进程。

当然你说能不能用 logs 接口而非 attach 接口了,当然是可以的。但 logs 接口需要 journal/json-file 的 log driver,前者依然会有阻塞问题,后者有磁盘管理问题,并且我们需要给每条日志打上一定的元数据标签。在巨量日志的前提下,丢日志比起阻塞业务不可用的损失小多了。

对于 2 来说,这算是我个人偷懒,毕竟有着 flex 的前提下报警是早于 agent 猝死的,因此我并未去实现基于 ttl 的探活机制。在这次事故后,我改写了整个 health check 的流程,基于 etcd 的 ttl 实现了被动报活。

这里大量的使用了 etcd 的 txn 机制,有一说一 etcd 这个 txn 不能在 get 的同时 refresh ttl 个人是觉得非常非常……蠢的。毕竟 grant 一个 lease 的成本比复用一个 lease 高多了,而且没法实现原子操作。以至于上了这个机制后我们需要定期的 defrag 和 compaction etcd 来防止 no space 错误(因为 revision 增长太快),还需要给 ttl 留一定时间的冗余防止误报。

资源和引擎

随着 Eru 的混合编排日渐成熟,我们目前在同一平台上同时驱动着 Container(基于 Docker),VirtualMachine(基于 libvirt),Systemd(真的就是 Systemd)。那么对于资源维度的需求也更多了,目前 Eru 支持 5 种资源计算:

  1. CPU
  2. CPU Cores(NUMA Supported)
  3. Memory
  4. Volume Size
  5. Disk

网络我一直期望于上层 TOR 或者 Spine 来做,但我们的 VM 引擎在这一年中有很大的发展,基于 VM + BGP 我们实现了自动 ECMP 的「容器」可以运行任意的 LB 功能的组件来满足 General LB 的需求,这也是我认为公有云上最麻烦的一项技术,既要保证隔离性,又要带宽融合,还要 HA/LB 缺一不可。因此对网络的编排很快也会纳入到 Eru 统一的资源计算维度里来。至于动态的添加资源配额和解除资源配额,继承资源配额等 Sugar 就不说了,看着没卵用,真用起来比如以 Systemd 运行无资源限制的系统组件简化 ansible 逻辑什么的还是蛮香的。

因为 Eru 一直是悲观锁的资源计算方式,对资源的读写在某些情况下会导致最终资源状态不一致从而影响到下一次的分配。虽然我之前花了比较多的精力来解决这个问题,但随着时间的增长,资源维度的增多以及更加灵活的 context 控制,被动的维持资源一致的成本也越来越高。比如创建目标容器/虚拟机/进程的时候都有很多步,每一步都有可能失败,那么怎么去正确的回收资源就成老大难的问题了。因此在 Grey 的这个 PR 里面引入了类似于 etcd 的 txn 机制,从根本上解决了这个问题。

当然就目前的资源计算和 realloc 的实现来说,还有一定的抽象空间,这一部分彻底释放出来之后 Eru 对资源扩展的支持就会像目前 Engine 一样简单和方便。

在对引擎支持方面,抽象完 BuildImage 接口后事实上做到了 Eru 引擎层完全等价,目前我们所有的操作都可以通过 Eru 来实现,不需要像去年一样给 VM 打镜像还需要绕开 Eru 等。我们目前既可以不同机器不同引擎,也可以一台机器多套引擎,通过虚拟的 Pod(Group)概念编组,跨组编排调度。

可靠性

就本质上来说 Eru 依然是旁路系统,这一点不会因为它的代码量从 10K 到 18K 就发生变化,因此它依然不能像 K8s 那样有「自动拉起挂掉的容器」「自动保持某类容器数量」这样的能力,但可以随意撸棒,反正撸不秃,不过在今年我们依旧搞了一些幺蛾子来提升其可靠性。

首先是彻底去掉 libgit2 的依赖,转为 go-git。libgit2 的作风实在是充满槽点,莫名其妙从 0.28 跳到了 1.0,每个版本都是最好最靠谱的版本。然后就是 git2go 莫名其妙的版本管理,经常性换个镜像就编译不能,只能手工的编译这些 C level 的组件。go-git 就没这些问题了,功能不全都是小事,反正能用到的就那么多,还可以跨平台编译,因此在发布上我们也开始提供 Darwin 的 Core 二进制发布。

另外还有 etcd 的依赖问题,这也是个很恼人的事情,花了不少时间从 github 的 coreos 下 etcd 依赖切换到了 go.etcd.io,也算是跟上官方发布节奏了,不然真的是 incompatible 版本用一辈子,一更新就各种报错。就私人来说,我对 etcd 这个项目的工程质量表示很不满意,至少作为阿里最年轻的 P9 的作品不应该这样粗糙。作为库,它没有真正意义上的「发布」,你要吃很多的屎踩很多的坑才能把它用得像个库的样子,比如依赖问题比如 embed etcd 垃圾文件问题。作为服务,我们不但发现了一个按年算横跨几个版本的 Bug( 是的,从 issue 到最后解决方案,官方没任何反馈 ),也在 txn 的使用上尝试了各种姿势,受到了一万点暴击。所以我从不迷信什么 CNCF/Apache 这样的光环,前人挖坑,后人踩屎,都是码农谁不知道谁啊。

最后就是 Core 本身的 HA 支持,Core 确实是 stateless 的组件,但这需要 Client 来判活并作出不同的 failover 行为,或者传统的加一层反向代理。我们在最新的版本里面引入了 gRPC 自身的 HA/LB 机制,来使得 Client 端能更智能的找到「活着」的 Core 进行请求,并且这一机制的加入能使得 Core 能够真正的 self-deploy。

未来

就像开篇所说,我做私有云已经做吐了,公有云技术的出发点和难点都和私有云不太一样。我们现在基本是按照公有云的路子在做私有云,多租户强隔离是一开始就考虑的要素之一。我们也在关注 KataContainer 但觉得短期内并不能在功能上完全覆盖 VM 的场景。就 Eru 这块来说,磁盘作为资源维度可以很方便的接入我们计划的分布式块存储,在我们自己的 RDS 项目上实现某种意义上的数据和计算分离,在 VM 上实现完全体的 EC2( 除了绑独立存储我们已经实现了几乎所有 EC2 的能力,动态扩容缩容,动态 attach 磁盘等 )。对于 NUMA 的支持则使得我们在使用如 Huawei kunlun 这样 NUMA 架构的机器时最大化其性能能力,降低上面每个服务的单位成本。

我们在今年会彻底解决 Docker 或者说容器这块「先天」的 IP 动态性问题( 是的这个 PR 也是我们提出来的 )。目前 calico 官方已经停更了 libnetwork-plugin这个插件,而这个项目最后的版本是我写的,因此我 port 了一份放在了这里。基于这个插件我们会尝试自定义 IPAM 来满足 IP 分配/释放能力,确保同一非 Remove 行为的容器保证永远都得到一样的 IP。另外就是通过 Barrel 项目绕开 Docker daemon 对于 Remove/Stop 行为等效判断机制,从上层控制 IP 生命周期。当然这 2 个都只是用来解决基于 Docker 怎么办这个问题,而 Docker 式微之后有很多需求没实现在我看来还是挺……糟心的:

  1. 缺乏外部 Volume 大小限制( XFS 可行,Extfs4 需要改点东西,最新版本没看也不知道怎么样了)。
  2. 缺乏外部 Volume 清理机制,对于有状态的服务来说还是蛮有用的。
  3. 对于无限制资源配额支持不完善,比如你可以绑定 0,11 号核给某个容器,但你没法去掉这个容器的配额,只能通过把 0-31(假设 32 个核)都给它绑上去才行,但本身 CGroup 是支持的,Containerd 也是支持的。
  4. 区分不了 Stop/Remove,每次只要容器停止都会去调用一次 CNM 接口释放网卡。
  5. 教练我想用 CNI 的插件,他们的大腿更粗。
  6. 一如既往的健壮性问题,包括不仅限于循环依赖,IO 操作阻塞等。

因此在「神秘人」的帮助下我们会基于 Containerd 来实现这个「Docker」引擎,用于替换目前的 Docker daemon。同时对于缺乏的 PaaS 能力这个问题,神秘人会发布他实现的 Pipe Workflow 项目,类似于 K8s 的 Argo,我个人还是蛮期待的。另外就是很多朋友问我 VM 的引擎实现,由于目前我司开源政策尚不明确,行政阻力还是蛮大的,如果我有时间的话我自己会实现一个 Clean room 的版本。

最后老实说我也惊讶于公司几十亿美刀到超越百度也就是半年的事,虽然口里说着不想做 Eru 了唉早点换 K8s 让我退休吧但既然有这么个机会可以搞它一票大的,那自然还是想搏一搏单车变摩托的,万一以后虾皮也走上了阿里老路呢对吧。身体嘛,总是诚实的,况且就我们这种更偏向大一统的用况来说,成本上还真不好说谁高谁低。所以咯,如果有兴趣搞个「公有云」历史进程的朋友,赶紧来简历吧。