做者:稻农
阿里云智能事业群高级技术专家
参与主导容器运行时及网络创新工做;目前的工做侧重于基于进程虚拟化的研究及加强(网络及热迁移方面),在阿里推行微安全容器及热迁移等,力图在保持容器简单高资源利用率前提下,提供高安全及热迁移等加强功能数据库
你们好,个人花名是稻农,首先我简单介绍一下我在这个领域的工做。在阿里,咱们如今主要的侧重点是作大规模的运维和新的容器运行时。目前,你们可能已经对 Kubernetes 进行了普遍地使用,但多数尚未达到必定规模,有不少痛点以及内部的问题尚未获得充分暴露。安全
容器迁移背景及现状
目前,大多数容器的使用还在百台到千台的规模。我先简单介绍一下阿里目前内部容器服务。阿里的淘系应用如天猫、淘宝,目前已经所有实现了容器化,在集团的场景下面是没有虚拟机的。阿里用了大概三年到四年的时间,作到了 100% 容器化。网络
你们都知道使用容器有不少好处,好比它在资源耗费方面有很大优点。对于“双十一”你们应该有很明显地感觉,相比以前,如今的“双十一”会“顺滑”不少,这样地转变也有容器化的功劳。架构
若是你有存量的业务,那你必定会面临从虚拟机或物理机迁移到容器的过程。绝大多数开发人员其实认为这是一个负担,由于他们的应用已经跑起来了,就不太但愿由于基础设施地改变,去作更多工做去进行适配。因此出现了一些咱们叫“富容器”或者“丰富复杂应用容器”的特殊容器。app
简单说,所谓富容器,就是咱们回在容器内放置一些管理组件。阿里内部组件叫 star agent,它会提供登录服务,提供各类各样的包管理,命令行的执行,诸如此类的事情。在真正运维和使用的过程当中,整个容器与虚拟机的差异不大。运维
固然这个东西在业界是存在争议的,好比咱们是否是应该先作微服务化,把全部服务都变成单1、不可改变的镜像再run 起来,仍是咱们为了迁就一些技术债务引入富容器这种技术,这个地方是存在争议的。可是能够告诉你们的是,若是你要彻底按照理想化的微服务去执行,基本上不少大的应用(像淘系这些很是复杂的应用,须要改造一下可能要几个月)可能在第一步就被卡死了。由于咱们有富容器,因此这个应用是有状态的,并非随便说我砍掉他,而后异地重启就能够了,这就是双刃剑的另外一面,上线改造容易,运维变得复杂了。微服务
咱们有富容器的一些传统应用,很难对他们进行微服务无状态的改造,因此咱们看到有不少场景,好比说容器出现故障时,开发或者运维的同窗很是但愿故障以后的新容器长得跟原来容器如出一辙,好比 ip、名字等任何东西都不变,很是符合他们的理想。 工具
有时候,咱们面对一些大规模的容器迁移,好比说在地方开一个很大的机房,咱们就会把杭州或者是上海的容器所有迁走。在过程当中很是麻烦的是有一些容器是有状态的,你迁的时候你还不敢动它,由于万一砍掉,可能红包就发不了了……阿里云
大的有状态的应用会占住物理机,形成没有办法去迁移。以上都是容器可携带状态迁移成为规模化运维的典型场景。spa
容器可携带状态迁移成为规模化运维难点在于 K8s 或者说整个容器与虚拟机的运维。Docker 公司曾给出说法,虚拟机像宠物同样,须要受到很精心地呵护才能永远活得很好,只要很差就须要去修它,这个就是宠物式的管理。K8s 认为容器应该是牛群式的放养,死了就直接重启而不须要对每一头牛作特别好地呵护,由于成本很高。
在 K8s 里面,咱们常常看到的就是扩缩容,针对他的假设都是里面的应用是无状态。而后在执行层面,你们如今用的通常都是普通容器,或者说是标准容器引擎,就是 runC,虽然 runC
里面有个 checkpoint 和 restore 的机制,你们用起来就会发现基本上是不可用,坑很是多。
容器可携带状态迁移成为规模化运维有两个难点,刚好是咱们要解决的两个问题:
首先是管理面,K8s 上支撑 pod 的迁移与伸缩不同,咱们所认为迁移就是这个容器要原封不动地在异地再重生;另外,咱们认为冷迁移就是业务时间中断比较长,中断时间短的就是热迁移。那这个长短的分界岭在哪呢?每一个云厂商会有一些不一样的见解。咱们认为大概到毫秒级如下,一百毫秒或者十个毫秒这样的级别,能够认为它是热迁移。其实任何迁移基本上都会有业务中断的时间,任何一种机制去实现都不可能实现零时间切换。咱们看一下 K8s 系统对整个容器迁移,2015 年开始,咱们就讨论过 pod 的迁移要不要放到K8s里面去,你们能够去翻 K8s 社区 issue,但一直没有下文。
其次是在执行层面,runC 做为容器运行时主流,虽有 CRIU 的项目辅助,仍然没法提供完善可靠的迁移机制。
管理面支撑 Pod 迁移
接下来,咱们看看 K8s 为何不可以作迁移?当前 K8s 系统的 Pod 迁移仍为空白。其中存在如下问题:
由于每一个 pod 有独立的标识,还有名字、ID 等。这个东西是要保证惟一性的,否则 K8s 本身也管理不了这些东西。假设有两个同窗,他们的学号、名字长相彻底同样,校长是要糊涂的。K8s 主推了对业务的伸缩,就是靠无状态伸缩,他不会把某一个容器从这迁到那去。
另外,K8s 骨干系统不支持 Pod
标识及 IP 冲突。咱们认为 API server、schedule 这种必不可少的部分是骨干系统。几个骨干系统是不支持任何标识冲突的。若是有两个 pod,他们 ID 同样, API server
就会糊涂,逻辑就会出问题。
K8s 是一套容器的管理系统,阿里周边对网盘 ID、对 ip,各类各样的资源,都有本身的管理系统。这些管理系统在 K8s 的世界里面,表现为不一样的资源 controler,由于这些资源都是钱买来的,要跟底层帐务系统联动,未来你们都会遇到这些问题,好比说这个部门是否有预算等。在阿里的 K8s 周边,咱们已经开发了大量的这种 controler 了,他们都是按照这个标识,咱们叫 SN 来管理应用的。数据库里面记录,每一个容器都有一个的标识。
伸缩跟迁移是冲突。由于迁移的时候你可能须要砍掉旧容器。砍掉容器以后,伸缩控制若是正在生效(RC),它就会自动起一个新容器,而不是把这个容器迁移过去,因此这个地方咱们对 RC 这些控制器都要作必定程度地改造。
其余问题,好比说不少远程盘不支持多 Mount。由于咱们在作迁移的时候,这个盘必定要作到至少有两个 Mount。就是个人旧容器跟新容器,可以同时把 PV mount 上去,不少远程盘仍是不支持的。
剩下还有一些传统底层支撑系统,好比 IP 管理系统不容许出现地址冲突。好比咱们在分 ip 的时候分两个同样的是不可想象的,这个是咱们最简单的迁移过程。迁移过程是这样的,咱们想最小地去改造 K8s,当你的系统真正上了管理系统,复杂了之后,你们都会对 K8s 有一些适应性地改造。
这些改造最好的表现形式可能就是 controler 了,你不要去对骨干系统作改动,改动以后就很难再回到主线来了。在迁移过程当中,咱们仍是没有办法必定要对骨干系统作一些改造,那么咱们想尽可能减小它的改造量。
第一步,咱们会生成一个从资源上来说跟原来的 pod 或者容器彻底同样的一个 pod,它须要几核几 U,它须要一个什么远程盘,它须要一个什么的多少个 ip,多 ip 的话还要考虑多少个 ip,多少个直通网卡,或者是非直通的网卡,资源彻底同样,这个是彻底标准的。咱们建立一个资源,一个新 pod,这就像一个占位符同样。假设我这台物理机要坏了,那么我在一个打标以后,我在一个新的好的物理集群上,生产一个这样的 pod,让我拿到了资源。
第二个过程就是说咱们两边的 agent,好比说是 runC 或者是阿里作的 pouch-container 也好,咱们这种 OCI 的 Agent 之间会有一个协商的过程,它的协商过程就是会把旧的 pod 的状态同步过去,刚才咱们新生成的 pod ,实际它是占位符。
咱们会把新的镜像动态地插入 pod 里,API 对 CRI 的接口是支持的,当前咱们没有办法在一个已经产生的 pod 里面去插入新的 container。但实际上 OCI 接口自己是支持的,能够在一个 sandbox 里面去删掉已有的 container 和增长新的 container,不须要作什么新的工做,只要打通管理层的事情就能够了。
另外,唤醒记忆的过程其实就是两边状态同步,状态同步完毕,咱们会作一个切流,切流就是把旧的容器再也不让新的需求过来,一旦咱们监控到一个静默期,它没有新的需求过来,咱们会把旧的 pod 停掉。其实暂时不停掉也不要紧,由于反正没有客户来找他进行服务了,已经被隔离到整个系统外面去了,删除资源是危险操做,通常会放置个一两天,以备万一要回滚。
最后一个过程新开发工做量比较大,咱们要把前面那个占位做用的 pod 标识改掉,ip 与旧的设置成同样,而后一切须要同步的东西都在这一步完成。完成以后就上去通知 API
server 说迁移过程完成,最后完成整个过程。因此你们会看到,其实第一步基本上标准的 K8s 就支持。
第二步咱们是 K8s 不感知的,就是咱们在两个宿主机上作两个 agent 作状态同步。对 K8S 的改造也比较小。那么最后一个会比较多,API server 确定是要改。RC 控制器可能改,若是你有 CI 的这种就是 IPM 的管理,IPM 的管理,这个地方要改。
接下来,我从 OCI 的运行这个地方来来讨论这个过程,由于实际上是有两层面,一个是咱们篮框这里是一个 pod,从它的状态 Dump 落盘到远端把它恢复,整个同步过程当中,咱们会插入对 K8s 系统的调用,涉及对容器管理系统的改造。
看外面这两个白框,上面这个咱们叫预处理过程,其实就是前面讲的,咱们要去建立新的 pod、占位符,而后在那边把资源申请到最后一个后期建议。咱们刚才说的最后一步,咱们叫标识的重构重建跟旧的 pod 彻底同样的,你们在咱们开发过程当中会遇到各类各样的冲突,好比说 API server 会说,你有两个标识同样的,这个代码就要特殊处理。APM 有时候会跳出来讲你有 ip 冲突,这样也要特殊处理,至少有几个骨干系统确定是要作的。
这部分由于涉及到 K8s 骨干的改造, patch 咱们尚未提上来。接下来还要跟社区讨论他们要不要 follow 咱们的作法,由于如今 K8s 的容器就是无状态的观点还比较占上风。
刚才咱们讲到管理面咱们认为是事务处理的,路上会有不少障碍,可是这些障碍都是能够搬掉的,就是说无非是这个东西不容许冲突,我改一改让他容许冲突,或者容许短期的一个并存,那个东西不容许我再改一改。比较硬核的部分是底层引擎去支撑热迁移,尤为是热迁移,冷迁移其实问题不大,冷迁移就是说我只是恢复那些外部可见的状态(不迁移内存页表等内部数据),若是对个人业务恢复时间没有什么要求的话,就比较容易作。
RunC 引擎的可迁移性
接下来,咱们讲 RunC,RunC 应·该是你们用的最多的,它就是标准的 container 去进行的迁移改造。若是你们去看过 checkpoint 开放的这部分代码,能够发现 RunC 依靠的机制就是一个叫 CRIU 的东西。他在优点技术已经出现比较长的时间了,他的整个的想法就是,用户态把一个进程或者一个进程数彻底落盘在把它存到磁盘上,而后在异地从磁盘把进程恢复,包括它的 PC 指针,它的栈,它的各类各样的资源,通过一段时间地摸索,基本上能够认为内存状态是没有问题的,就是页表,页表是能够作到精确恢复的。
无论你这边涉及多少物理页是脏的,仍是干净的,这个都是百分之百能够还原出来的。进程执行的上下文,好比各类寄存器,调用 stack 等,这些都没问题。跟纯进程执行态相关的问题都已经彻底解决了,这个不用担忧。而后你们比较担忧的就是一个网络状态,好比说你们都知道 TCP 是带状态的,它是已链接?等待链接仍是断开?其实这个网络 Socket 迁移的工做也基本完成了。
我能够简单讲一下,它的实现方法是这样,就是说它在 Linux set 里面加了一个修复模式,修复模式一旦启动,就再也不向外发送真正的数据包,而是只进行状态及内部 buffer 的同步,好比你下达的这种 close,不会向外发包,只是体现为对状态信息的导出。
好比说你要进入修复模式,那在原端就要关闭 Socket,它并不会真正的去发 close 的 TCP 包,它只是把信息 Dump 出来,在新的目的地端去 connect。它也不会真正去包,最后的结果就是除了 mac 地址不同,TCP 里面的状态也恢复到远端了,里面的内存状态都转过去了,通过实际验证其实也是比较可靠。还有打开的文件句柄恢复,你打开的文件,你如今文件好比说读写指针到了 0xFF,文件的 off
set 恢复都是没有问题的。
其实咱们在冷热迁移中最担忧的就是耗时问题,我一个容器究竟花多长时间?一个 pod 多长时间能够迁移到新目的地的宿主机上去,耗时的就是内存。也就是说像不少 Java 应用,假设你的内存用得越多,你的迁移时间就是准备时间就越长。能够看到咱们刚才实际上是有一个协商过程。在协商过程当中旧的 pod 还在继续提供服务,可是它会不停的把它的状态 Sync 到远端去。这个时间其实并非业务中断的时间,若是耗时特别长,也会由于业务时在不停转的,若是你的内存老是在不停地作大量改动,你的准备时间和最后的完成时间就会很是长,有可能会超时。
咱们去评估一个业务能不能作热迁移,或者这个的时候或者一些状况,它所使用的内存大小是一个比较大的考量。
而后剩下就是咱们踩到的坑。如今还有不少东西它支持的不大好。这个地方你们能够理解一个进程,一个进程其实就是一个本身页表,有本身的堆栈,一个可执行的活体。那么它支持很差的部分都是外部的,若是它依赖一些主机设备,就很难把一个设备迁移走。
而后剩下就是文件锁,若是这个文件是的多个进程共用就会加锁。由于这个锁的状态还涉及到别的进程,因此你只迁移这一个进程的时候会出问题。这个地方逻辑上会有问题,其实你们能够笼统地这样去判断,若是我依赖的东西是跟别用户、进程有一些共享,甚至这个东西就是内核的一个什么设备,这种就比较难迁移走。因此简单来讲就是自包含程度越高越容易迁移。
这个是一个比较详细的一个图,跟我刚才讲的过程实际上是差很少,咱们仍是会在原端发起热迁移的请求,请求以后,会发起两端两边 Agent 的sick,而后最后中间会切流。
等到 Sync 状态完成,咱们会通知 K8s 说我这边能够了,那么 K8s 会完成一个,从旧的容器,就是把流从旧的 pod 切到新 pod 来,而后最后把全部的标识,这个是由底向上的,个人标识跟个人 SN 或者说个人 ip 都改造完了,最后通知一下 K8s 就结束了。
新运行时带来的机会
最后,我想分享的是新的运行时。咱们在社区里面会看到容器,如今来讲在私有云上,它的主要的形态还时 RunC,就是普通的 Linux 标准容器。那么咱们在公有云上为了能混布,或者说跟内外客户/在线离线都在一块儿,他对安全的要求比较高,咱们通常会选虚拟机类型的容器引擎,好比像 kata 这样的东西。
早先来讲你的选择就只有两种,要么你讲效率,不讲安全,就是纯容器;要么你讲安全,不讲效率,就跑一个虚拟机式的容器。自从从去年开始,谷歌、亚马逊等头部玩家开始作一些新的事情,叫作进程及虚拟化。就像咱们在中学物理讲过,好比说光的波粒二象性,它在一些维度上看起来是波,一些维度上是粒子。其实这个与程级虚拟化是很相像的,也就是说从资源管理这个角度看,它是一个普通进程。 可是在内部为了增强隔离性,会作一个本身的内核,咱们认为从这个角度看,它是一个虚机;可是从外部资源角度来看,由于这个内核是隐形生效的(并无动用其余虚拟机工具去启动容器),也不会去实现完整的设备级模拟,管理系统感受它就是一个普通进程。
这是一种新的潮流,也就是说咱们判断这个东西(固然咱们还须要在里面作不少工做),像这种比较颠覆性的事情,有可能会未来成为容器运行时的一个有益补充或者是主流。简单的说,这种新的运行时会有本身的私有内核,并且这个内核通常如今都不会再用 C 语言去再写一遍,由于底层语言比较繁琐,也很容易出错。
用过 C语言的人都知道带指针管理很危险,Linux 社区 bug 比比皆是,现代的作法都会用 Go 语言或其余一些高级语言重写,有本身的垃圾回收的机制,指针就不要本身去管理了。通常不会提供很丰富的虚拟设备管理。由于这部分对一个应用来讲是冗余的,普通应用它跑起来,其实不多去关心我要用什么设备须要什么特殊 proc 配置,简单的说就是把虚拟机的冗余部分所有砍掉,只留下我跟普通应用 Linux 的 APP 跑起来。
这个是咱们对运行史的一个简单的比较,是从自包含的角度来说,由于包含的程度越高,它的热迁移越容易实现,或通常来讲安全性也越高。
亚马逊如今在作 Firecracker,它也是用现代语言重写了内核。微软的云也在作一个事情。你们的思路是比较一致的。由于硅谷技术交流是很频繁的,他们的技术人员之间都是比较知根知底的,Google 作了 gVisor。华为用的是 Kata。
你们可能据说过谷歌的 gVisor,gVisor 是这样一个机制,就是说我会在一个 APP,就是普通的未经任何修改的 ,跑在容器里的 Linux 应用,那么咱们怎么去让他用咱们的内核而不用 Linux 内核?核心的事情,就是要捕获他的系统调用,或者说劫持均可以。
系统调用的劫持有软硬两种方法,软件来讲,咱们在 Linux 内核里面利用 pTrace 的机制,强迫就设完以后你设置的进程的全部系统调用,他不会让内核去,而是先到你进程来。这个叫作软件实现。
其次咱们叫硬件实现的,就是说咱们会强迫这个 APP 跑在虚拟机的状态。咱们知道在虚拟机里面,虚拟机会有本身中断向量表,他是经过这种方式来获取执行时的。而后咱们的 Guest kernel 是这样的,咱们会看到如今的相似内核是无比庞大的,应该截止到如今有 2000 万行代码,这里面绝大部分其实跟容器运行时没有太大关系。
因此像 Google 和亚马逊包括咱们如今想法就是,我只须要把 Syscall 服务做好,也就是说 APP 它看到的无非就是这 300 多个 Syscall。这 300 多个系统调用你可以服务好,就是无论你的 Syscall 服务用 Go 写,仍是用 Python 写的(不讲究效率的话)你均可以认为你有本身的内核,而后跟主机的内核是隔离的,由于我没有让 APP 直接接触主机内核的东西。
为了安全,咱们也不容许用户直接去操做主机文件。 你们看到,RunC 上面像这样的你去操做的文件,事实上在主机上或者在宿主机上都是有一个表明,无论你 Overlay 出来,仍是快照 DeviceMapper,你能够在磁盘上还能找到这个真实的存储。
其实这个是一个很大的威胁,就是说用户能够直接去操做文件系统。他们操做文件系统以后,其实咱们能够相信文件系统是有不少 bug 的。它代码量那么大,老是有可能突破的。因此咱们加了防御层叫 Gofer。Gofer 是一个文件的代理进程,只有就是用户所发出的全部 file 的 read 和 right 都会被咱们截获,截获完会通过 Gofer 的一个审查。若是你确实有权限去碰这个文件,他才会去给你这个操做,这个是大概的一个架构。
而后简单讲一下 gVisor 里面是怎么跑的,APP 在 RunC 里面,它直接 call 到的就是主机的内核。就是这条红线,Call 这个内核,他该得到哪些 syscall 就会得到 syscall,若是假设内核是有什么故障或者 bug,这个时候它就能够突破一些限制,应该是上星期吧, RunC 就报了一个很大的逃逸漏洞,须要整改!
在 Gvisor 的里面,他的执行方是这样的,个人 APP 第一步会被 PTrace 或者个人 KVM-Guest 内核捕获,捕获以后在咱们叫 Sentry,为何这个红线画的划到 kernel 上面,由于捕获的过程,要么是通过 KVM-Guest-ring0 的,要么是通过PTrace系统调用,因此我认为仍是内核要帮忙。而后sEntry拿到这个系统调用以后,他会去作力所能及的事情,好比说你要去读一些PROC文件,你要去申请文件句柄,本地就能够完成服务返回,这个事很是高效的。
而后有一些事情,好比说你要去,假设说你要去读写主机上的一个网卡这样的事情,sEentry 本身确实作不了,他就会把这个需求转发到主机内核上去,等到获得服务以后再原路返回。文件操做就是这样的,若是你它读写任何的主机文件,都会去Call 到 Gofer的进程(审查请求),而后代理访问服务,去读写真正的文件把结果返回。这个是你们能够看到,APP 就是被关在两重牢笼里面就叫 Guest Kernel(sentry),一个是 Host Kernel,由于他自己又是一个进程,因此从安全性上来说,由于 APP 和 sEntry 不共页表,其实能够说比虚拟机还要安全。
由于虚拟机里面 Guest Kernel 就是跟 APP 共页表,Guest Kernel 躲在这个列表的上端。而在 gVisor 里面 Guest Kernel 跟 APP 是彻底不一样的两套页表,诸如此类有不少方面,你们会发现 gVisor 比虚拟机更加的安全。
固然咱们作了这么多隔离,也会有反作用,就是运行效率会有问题,尤为是网络,包括阿里、谷歌咱们都会持续改进,还有不少同事在里面作不少工做,会把虚拟机已有的一些经验用到 Go 的内核上去,咱们的理想是虚拟损耗到 5% 如下。
最后这个议题就是说咱们如今有不少新的运行时,你们在选型的时候,其实除了 RunC 除了 Kata 等等。
将来,你们能够去比较各类运行时。当咱们选型一个容器的引擎,会去综合地看它的运行效率,它的安全性,尤为是代码复杂度,代码越多,基本上你能够认为这个东西出 bug 的概率就越高,代码越少其实越好,大概是这样的考量。
咱们跟业界还有一个合做,还再有一个咱们还在想作对容器运行时作一个平,最后综合打一个分。完成后会开源给你们使用。怎么去评价一个 runtime 是好的?高效的,它的安全性到多少分?就跟汽车的这种评分同样的。我今天介绍大概就是这些。