Flynn,一个小而美的两层架构
如果需要管理或者构建一个完整的服务栈,容器扮演的仅仅是一个基本工作单元的角色。在服务栈的最下层,需要有一种资源抽象来为工作单元展示一个统一的资源视图。这样容器便不必关心服务器集群资源情况和网络拓扑,即从容器视角看到的仅仅是“一台”服务器而已。
然而,还应该能够根据用户提交的容器描述文件来进行应用容器的编排和调度,为用户创建出符合预期描述的一个或多个容器,交给调度引擎放置到一台或多台物理服务器上运行。这个过程正是Fleet所擅长的。
早在Docker得到普遍认同之前,就有一小撮敏感的极客们意识到了这一点,他们给出的解决方案被称为Flynn,一个具有Layer 0和Layer 1两层架构的类PaaS项目。之所以称Flynn为类PaaS,是因为Flynn面向的不仅仅是用户应用,而是任何需要发布的服务。例如,应用(更确切地说是Web应用)是一个长运行任务(Long Running Task, LRT),与之相对应的是一次性任务(One Off Task),两者的主要区别在于生命周期。经典PaaS主要是面向LRT的,多数只支持HTTP协议的Web应用。对于Flynn来说,理论上任何可以实现进程管理的任务都可以运行在Flynn上,它可以是一个Web应用,也可以是自定义的一次性计算任务,在Flynn里被统称为待发布服务。所以,Flynn严格意义上是一套面向“服务发布”的框架。
第0层:容器云的基础设施
所谓第0层,其实是担当之前所提的Fleet的角色。简单地说,它能够对宿主机集群实现一个统一的抽象,将容器化的任务进程合理调度并运行于集群上,然后对这些任务进行容器层面的生命周期管理。这里可以比Fleet更进一步,将这一层负责的工作总结为以下4点。
❏ 分布式配置和协调:毋庸置疑,这个一定是Zookeeper或etcd的工作了。Flynn选择了etcd,但并没有直接依赖它,也就是说,可以方便地通过实现Flynn定义的抽象接
口将分布式协同组件更换成其他的方案。关于这里涉及的一致性算法等相关知识,建议大家复习第4章etcd的相关内容。
❏ 任务调度:Flynn团队曾重点关注了Mesos和Omega这两个调度方案,最后选择了更简单更易掌控的Omega[插图]。在此基础上,Flynn原生提供了两种调度器,一种负责调度长运行任务(Service Scheduler),另一种负责一次性任务的调度(Ephemeral Scheduler)。
❏ 服务发现:引入etcd后,服务发现就水到渠成了。在Flynn中,服务发现的主要任务是watch被监控节点(包括服务实例和宿主机节点)的上线和下线事件,从而在callback回调里完成每个事件对应的处理逻辑(如更新负载均衡的server列表)。跟分布式协调组件一样,Flynn同样有一个discoverd来封装etcd,对外提供统一的服务发现接口,鉴于该设计,Zookeeper、mDNS也可以用于Flynn的服务发现后端。
❏ 宿主机抽象:所谓宿主机抽象,是指上层系统(Layer 1)以何种方式与宿主机交互。宿主机抽象可以屏蔽不同宿主机系统和硬件带来的不一致。一般来讲,抽象实现的方式是在宿主机上运行一个agent进程来响应上层的RPC请求,向上层的调度组件报告这台宿主机的资源情况,以及向服务发现组件注册宿主机的存活状态等。Flynn的做法与之类似,不过Flynn还将这个agent进程作为自身服务框架托管下的一组“服务”进行管理,从而避免了从外部引入一套守护进程带来的烦琐。
Layer 0所做的工作与Fleet核心功能非常一致,它们都提供了底层服务器资源的统一抽象和对容器的发现和调度功能,成为了统一管理成千上万个容器组成的容器云的基石。
第1层:容器云的功能框架
Flynn构建在Layer 0之上的一套组件统称为Layer 1,它能够基于Layer 0提供的资源,抽象实现容器云所需的上层功能。这些上层功能可以总结为以下4点。❏ API控制器:同经典PaaS一样,Flynn也运行着一个API后端以响应用户的HTTP管理请求。这个组件同Cloud Foundry的cloud_controller基本一致,所以这个API控制器也需要维护Flynn的应用逻辑模型,包括以下3个方面。
- 代码制品(artifact):一般来说,就是一个含有可执行代码的Docker镜像的URI。
- 发布包(release):即代码制品加上配置信息,由这些配置信息来指定同时启动几份代码制品,以及启动所需的二进制文件名称、参数、环境变量、端口配置和所需资源。
- 应用(App):即运行起来的发布包实例,显然它是一个容器或者多个容器的集合,并且包含了发布包指定的所有配置信息。API控制器的调度组件通过服务发现来感知正在运行的容器数目的变化,从而决定是否要执行重调度。重调度的操作请求会交给Layer 0完成。值得一提的是,对Layer 0而言,API控制器以及Layer1本身都是应用,它可以采用和用户应用一样的逻辑来管理和扩展这些组件。
❏ Git接收器:Flynn使用Git来发布用户代码,Git接收器作为一个git remote配置在用户方,所以用户push的代码会直接交给这个接收器来制作代码制品和发布包。从另一个角度来讲,这相当于Flynn集成了代码版本管理等Git的功能,完成从源代码管理到发布之间的衔接工作。
❏ Buildpacks:曾提到,Heroku Buildpack是第二代PaaS(PaaS分类)的一个创举,用户只需要上传可执行文件包(如WAR包),Buildpack就能够将这些文件按照一定的格式组织成可以运行的实体(如Tomcat+WAR组成的压缩包)。通过定义不同的Buildpack, PaaS就能实现支持不同的编程语言、运行时环境、Web容器的组合。这个优点Flynn也没有放过:对于大部分编程语言而言,当用户使用Git上传源码之后,Git接收器正是通过运行Buildpack来制作名称为slug的可运行实体。所谓发布包也只是一个slug的引用而已。
❏ 路由组件:前面介绍过,容器服务栈要想正常工作,一个为集群服务的负载均衡路由组件是必不可少的。Flynn的路由组件支持HTTP和TCP协议,它能够支持大部分用户服务的访问需求。Flynn的管理类请求也是由路由组件来转交给API控制器的。通过与服务发现组件etcd协作,路由组件可以及时地更新被代理服务的IP和端口。不过,与Cloud Foundry的Gorouter组件相比,Flynn路由组件目前存在的问题是不支持session sticky,即当应用在session保存了数据时,不能保证下一次应用访问的请求依然命中上一次访问过的实例,因此session中的数据就失去了意义。当然,许多经典PaaS平台都会建议用户使用无状态应用,不过现实场景往往无法在理想状态中运作。
Flynn体系架构与实现原理
本节对Flynn体系结构下Layer 0一层就不做过多介绍了,而是直接从Layer 1入手剖析Flynn的工作机制。前文已提到过,Layer 1的主要任务是填补用户代码到Docker镜像之间的空白。那么,用户代码是怎么上传到Flynn并执行运作的呢?主要有两种方式。
第一种方式,用户通过Git指令直接提交代码,这时Flynn需要做的工作至少包括以下几点:❏ 接收用户上传的代码;
- 如果需要的话,按照一定的标准编译代码,组织代码目录;
- 按照一定的标准将编译后的可执行文件保存到预设的目录中;
- 如果需要的话,按照一定的标准将可执行文件目录和Web服务器目录组装起来,生成启停脚本和必要的配置信息;
- 将上述包含了可执行文件、Web服务器、控制脚本和配置文件的目录打成一个包保存起来;在需要运行代码时,只需在一个指定的base容器中解压上述包,然后执行启动脚本即可。
第二种方式,Flynn直接接受一个用户上传的“万事俱备,只欠东风”的Docker镜像,这一点非常容易做到。
1.从源码到可运行实体这里的源码既指用户源代码,也指用户镜像。
下面将分别解读两种方式从源码到可运行实体的过程。
● 第一种方式:从Git直接push代码给Flynn并运行起来为了实现“从代码到镜像”的飞跃,用户代码必须通过一系列“标准化”流程,才能实现自动化托管。所谓的标准化流程就是为用户指定上传的代码如何与Web服务器协作,代码如何配置、日志如何保存、代码如何编译、启动停止命令和参数如何指定等步骤。在这里,Flynn通过Buildpack对支持的编程语言提供标准化编译和打包流程。
下面将遵照官方示例向Flynn上传应用,验证上面的思路。
(1) 创建或者克隆一个git项目。
git clone https://github.com/flynn/nodejs-flynn-example.git
(2) 在这个项目目录下,使用flynn命令创建一个应用。
$ cd nodejs-flynn-example
$ flynn create example
Created example
看到这里应该就能明白了,当需要发布代码到Flynn时,只需要把代码git push到flynn这个远端。工作在demo.localflynn.com:2222上的Flynn组件(Git Reciever)就能拦截下这个请求,然后通过Git的hook机制完成后续的打包、发布操作。其实,这只是Git一个非常基本的功能。这也解释了为什么所有新创建的项目,都要添加如上所示的flynn remote地址。为了使这个应用能够访问,还需要为它添加HTTP协议的route,
即路由组件提供的访问代理服务,如下所示:
$ flynn route
ROUTE SERVICE ID
http:example.demo.localflynn.com example http/1ba949d1654e711d03b5f1e471426512
(3) 向该flynn remote push代码,就会触发Flynn一系列“从代码到镜像”的编译、打包、发布流程,过程如下所示。
$ git push flynn master
...
————-> Building example...
————-> Node.js app detected
...
————-> Creating release...
=====> Application deployed
=====> Added default web=1 formation
To ssh://git@demo.localflynn.com:2222/example.git
* [new branch] master -> master
发布成功之后的应用就可以正常访问了,示例如下:
$ curl http://example.demo.localflynn.com
Hello from Flynn on port 55006 from container d55c7a2d5ef542c186e0feac5b94a0b0
仅仅需要三步,Flynn就能够将用户刚刚写完的代码变成可运行的容器镜像,这个流程是完全与Git流程结合在一起的,这也意味着还可以通过为Git库添加service hook等方法,将自己的第三方服务(如CI系统)也集成进来。那么,这一系列打包发布流程的工作原理又是怎样的呢?首先是打包系统,图展示了这部分流程的工作机制。
第二种方法:直接上传用户Docker镜像并运行起来
当需要运行某个应用时,Flynn会从Blobstore中下载对应的Slug来运行。读者理解了这个工作流程,就不难想到,Flynn如果想要用户上传一个Docker镜像来运行,就要想办法把镜像做成一个伪Slug。下面来看Flynn是如何做到这一点的。
回顾Flynn借助Buildpack做的三步工作,对于Docker镜像来说,detect和compile两步是不需要的,所以Flynn处理Docker镜像的过程直接来到了release这一步骤。以上传一个简单的Redis镜像为例来进一步说明。首先,创建对应的服务:
首先,创建对应的服务:
$ flynn create --remote "" redis
然后,为该服务直接添加一个release,而非触发Flynn Receiver的流程:
$ flynn -a redis release add -f config.json
"https://registry.hub.docker.com? name=redis&id=868be653dea3ff6082b043c0f34b95bb180cc82ab14a18d9d6b8e27b7929762c"
此处的config.json是一个负责将Docker镜像描述为被Flynn接受的服务的配置文件,它的内容如下:
{
"processes": {
"server": {
"cmd": ["redis-server"],
"data": true,
"ports": [{
"port": 6379,
"proto": "tcp",
"service": {
"name": "redis",
"create": true,
"check": {
"type": "tcp"
}
}
}]
}
}
}
可以看到,Flynn直接运行Docker镜像的过程非常简洁,不过很快将会发现这套机制的背后其实也存在着一些不尽如人意的地方。
2.从可运行实体到应用实例
前文提到,当用户的应用已经被上传并在Flynn中完成了打包工作之后,生成的Slug就是一个按照Flynn规定的组织方式,将可执行文件、Web服务器等应用运行所需的各种制品组织在一起的压缩包。因此,要运行这个Slug,必然需要一个能够知晓Slug文件结构和各项调用命令的组件slugrunner,它的主要工作流程如图所示。
与其说slugrunner是一个组件,不如说它是一段shell,它用来完成如下3项核心工作:
- 创建工作目录,解压Slug;
- 加载Slug目录中的profile文件,用来初始化应用运行所需的各项环境变量;
- 根据procfile中的内容,为这个Slug生成一句启动命令。这个过程的最终产出就是启动命令command,在slugrunner的最后一步执行下面这条命令就可以将Slug运行起来:
chown -R nobody:nogroup .exec setuidgid nobody bash -c "${command}"
不妨设想一下,Flynn既然已经持有了应用的可运行单元,它只需要向某个工作节点上的Docker daemon发送请求,拉来一个base Docker镜像运行起来,然后执行上述slugrunner过程,不就完成应用或服务的启动了吗?遗憾的是,事实并非如此。Flynn有点出人意料,它并不同Docker daemon发生交互,甚至它的工作节点都不需要Docker daemon。因为Flynn依靠libvirt-lxc启动容器,加载Docker基础镜像,并在容器里执行slugrunner过程。也就是说,Flynn中运行的容器事实上是LXC容器而非Docker容器,亦即Docker daemon本身的很多特性在Flynn中是不被支持的。
为什么Flynn要这么做?虽然官方没有作出正式解释,但根据GitHub上的部分issue,能够推断出Flynn早期确实是直接同运行Flynn节点上的Docker daemon交互的,但是出于一些技术原因(比如Docker的某些设计和issue与Flynn的设计出现了冲突), Flynn开始寻求基于第三方的库来加载并运行Docker镜像,在选型过程中,libvirt-lxc最终被采纳。这样做最大的好处是让Flynn终于脱离了Docker限制,使得它可以自由地定义容器行为和运行配置,但同时依旧能支持从Docker registry里加载Docker镜像作为容器的rootfs。
在介绍了Flynn的应用或服务打包策略和slugrunner组件之后,剩下的服务启动工作就顺理成章了,下面就用图7-3来解释Flynn启动容器并把用户上传的应用/服务运行起来的过程。
① 制作完Slug之后,Flynn通过调度器选择一个合适的Flynn Host(需要Layer 0参与)来运行服务实例。
② Flynn Host调用libvirt-lxc来启动一个LXC容器。
③ 这个容器使用的Docker镜像是Flynn提供的一个base镜像,主要的改动在于镜像中包含了slugrunner脚本和所需的环境依赖。
④ 在LXC容器中,Flynn从指定位置下载对应的Slug;如果用户上传的是Docker镜像,这个Slug就是镜像文件本身加上配置和启动信息。
⑤ 容器里运行slugrunner生成启动命令并启动这个服务。
⑥ Flynn Router为这个服务实例分配一条路由规则(服务名称+域名),这个规则默认是HTTP的,可以指定为TCP。当然在路由节点上Flynn会帮助用户配置好对应的代理。
至此,服务就可以被外界访问了。服务发布宣告完成,Layer 1的主要工作也就结束了。
3.不要停止思考
当服务发布完成后,作为一个类PaaS项目,Flynn还需要实现这个服务或应用的整个生命周期管理,包括应用启动和停止、状态监控和Scaling。下面将逐一进行简单介绍。
第一,整个服务的生命周期管理的实现很简单,只需要针对slugrunner生成的启动命令和运行起来的PID进行操作即可,这里不再过多介绍,有兴趣的读者可以自行研究buildpack的工作原理。
第二,服务和应用的状态监控。Flynn Host上的Container Manager进程负责实施健康检查,并检测本身运行着的容器数目,检测结果会更新Flynn数据库中的Formations表。另一端的Flynn Controller保持监听该表数据变化,一旦发现desired实例数和actual实例数不一致,Controller就会根据差异值重新在某台Flynn Host下载并运行对应的Slug(或者删除多余的实例)。
第三,服务的水平扩展。如果需要增加实例,Flynn由用户指定某个Flynn Host来启动新的实例容器。如果用户不指定Host,那么Flynn Scheduler会选择一个当前运行中的实例数目最小的Host来运行。如果用户需要减少实例,Flynn直接选择这个服务或应用的最新实例,然后把它们kill掉(虽然是软kill,但是还是略显粗暴)。截止到本书完成,Flynn为每个实例容器设置的资源是固定的一个CPU和1G内存,所以选择Host的策略也很简单。
Flynn为每个实例容器设置的资源是固定的一个CPU和1G内存,所以选择Host的策略也很简单。
至此,已经可以清楚地看到,Flynn的两层架构有着非常强的普适性,甚至可以仅采用Flynn Layer 0,而重新构建符合差异化需求的Layer 1来实现一整套DIY的PaaS。传统PaaS真正的挑战大部分集中在Layer 0上,尤其是资源的隔离、划分和调度。但是,如果有一种非常简便有效的对进程进行资源抽象和统一管理的工具(如Docker和其他正在崛起的容器技术),打造自己的PaaS(DIY Layer 1)就会变得简单很多,这正是容器对PaaS影响最深远之处。可以预见,基于Docker或者其他容器技术作为底层支撑的各种PaaS平台很快会如雨后春笋般在国内外涌现出来。在全面了解Flynn的架构和原理之后,下面对其进行深入的总结。
在全面了解Flynn的架构和原理之后,下面对其进行深入的总结。
(1) Flynn的Layer 0确实很完善,即使一个计算节点宕机系统也能够重新调度正在运行的用户服务以保证服务不中断。
(2) Flynn的两层架构很简洁,这一点比大部分经典PaaS都要强。
(3) Flynn在应用打包这条路径上跟经典PaaS相差不大,它甚至沿用了Heroku的buildpack体系作为发布标准,但Flynn既可以发布“十二要素”应用,也可以发布有状态的服务,这使它与经典PaaS平台在对应用架构限制上有所不同。
(4) Flynn支持用户上传Docker镜像并运行起来,这个过程的本质是将Docker镜像当作一个另一种形态的Slug。
(5) Flynn不使用Docker daemon作为容器运行依赖,因此大部分Docker style的trick(如link)在Flynn上是不支持的。
(6) Flynn Host统一使用zfs来支持volume挂载在容器的/data目录下,volume同LXC容器之间没有sticky关系。这虽然与Docker的volume做法在原理上如出一辙,但并不代表Flynn支持“Docker style”的volume配置。
(7) Flynn的Router目前不支持session sticky,即对于有多个实例的服务/应用,Flynn不保证用户的会话能够一直保持在同一个实例上。
(8) 目前Flynn只内置了一个PostgreSQL数据库通过环境变量同应用绑定并访问。
(9) Flynn有完善支持HTTP和TCP协议的Router机制,对服务的端口不做数量限定,通过环境变量传递端口参数。这一点也比大部分经典PaaS强。
可以看出,作为后起之秀,Flynn“待发布的都是服务”的思路是正确的,它并不强制区分所谓应用和服务的差别(如Web App和它的数据库)。但另一方面,Flynn对于Docker容器的支持只是镜像层面的,这与Cloud Foundry这类经典PaaS不谋而合。这使得Flynn可以自由地定义容器的行为和运行配置,但也因此放弃了整个快速进步Docker技术栈,对于Flynn来言令人扼腕。同样作为后起之秀,还有一家公司在实现上已经采用了Docker技术栈,但相比Flynn,