3.1 虚拟机运行时设计目标
在实现边缘计算平台基于Nomad的运行时时,最主要的工作是实现一个驱动,按照社区的模式,具体地讲是一个驱动插件。该运行时驱动插件在C/S架构中是一个服务器,实现社区公开的运行时驱动插件标准接口,并接受Nomad作为客户端进行RPC调用,松散耦合地处理相应请求。所以根据运行时驱动接口内容的定义,本文列出如下功能设计目标:
l 实现一个gRPC服务器,接受运行时驱动接口的请求。
l 获取插件信息。响应信息包含插件的元数据信息。
l 设置配置。当插件首次启动时为其设置配置,包含编码后的插件配置块和执行该运行时驱动插件的Nomad代理的配置。插件有足够的信息独立运行。
l 获取运行时驱动的配置模式(Schema)。配置在客户端配置的插件配置块中,该模式对应的配置是采用HCL(HashiCorp Configuration Language)定义的。
l 获取运行时驱动任务的配置模式。该模式对应的配置是采用HCL定义的。
l 获取能力(Capabilities),即运行时驱动实现的功能,例如:任务是否支持操作系统信号量,是否支持执行命令,运行时驱动支持的文件系统隔离类型等。
l 指纹识别(Fingerprint),即Nomad运行时驱动的健康检查机制。允许运行时驱动向Nomad客户端汇报其节点属性和运行状况,并定期发送直到上下文终止。
l 虚拟机任务生命周期管理,支持运行时驱动接口对虚拟机工作负载的管理,包括启动任务、恢复任务、等待任务、停止任务、销毁任务。
n 虚拟机网络,启动任务中运行时驱动需要负责创建和配置虚拟机网络并将该网络描述返回,包含IP地址,网络方案由预定义的作业配置文件决定。
n 虚拟机镜像,启动任务中由作业配置指定镜像路径,或者通过下载器拉取镜像。
n 任务状态存储,具体存储的细节留给运行时驱动实现。例如:将运行时驱动的任务详细信息存储在内存中,存储的状态可以用于Nomad客户端重启后恢复运行时驱动的任务。
l 任务日志管理,将虚拟机任务日志按照运行时驱动接口定义的格式存储到接口要求的路径。Nomad通过为标准输出和标准错误创建先入先出队列,支持跨平台写入这些路径,同时负责任务日志的滚动和处理。
l 任务监控数据,运行时驱动也负责具体的监控数据的采集,Nomad接收统计数据,而不用关心运行时驱动监控数据的采集方式。
l 任务事件通知。运行时驱动将事件发射出去,由Nomad客户端来进行事件轮询和侦听。
l 任务响应操作系统信号。支持将操作系统信号(SIGHUP,SIGKILL,SIGUSR1等)发送到任务。运行时驱动的一种能力。
l 在任务运行上下文中执行命令。运行时驱动的一种能力。但目前虚拟机运行时暂不支持。
3.2 Nomad运行时驱动插件运行机制分析
现在明确了管理虚拟机工作负载的需求和接口要求,在实现Libvirt虚拟机运行时之前,要理清Nomad运行时驱动插件运行机制和Libvirt的API模型,才能做好两者之间的衔接。上文相关技术综述中,本文简单介绍了Nomad社区v0.9引入的插件框架是本文这部分工作的基础。
如图3.1所示,Nomad CLI提交作业时,作业由任务组组成,任务组中的任务运行在同一设备节点上,每个任务需要具体指定运行时驱动运行工作负载。CLI提交的作业通过REST接口PUT到Nomad服务器的Endpoint,并将字节流解析封装成相应的请求。Nomad服务器负载管理作业队列、调度程序和服务器代理的通知总线。Nomad实现了有限状态机与Raft算法共用以提供强一致性。
图3.1 运行时驱动作业执行涉及的主要模块
在处理运行时驱动作业评估前,Nomad服务器代理会进行一系列操作,包括初始化默认值、添加隐式约束、校验作业等。作业会被单线程的调度工人(Worker)处理,每个服务器上可能运行多个调度工人——领导者或追随者(Follower),负责作业出队等待评估,调用调度器,提交计划等,围绕任务分配的生命周期,将调度器的业务逻辑与所需的组件设施连接起来,使其一起工作。作业评估分发器(JobEvalDispatcher)提交作业并为其创建评估,例如PeriodicDispatch用于跟踪和启动周期性作业。评估代理器(EvalBroker)是用于管理评估的代理,创建评估时,由于作业规范或节点的更改,会将其放入评估代理。代理根据优先级和调度器类型进行排序,允许先将最高优先级的工作从队列中取出,同时也允许子调度器只将它们知道如何处理的工作从队列中取出。评估代理器被设计为完全工作在内存中,由服务器领导者节点管理。它依赖于显式的Ack/Nack消息来保证至少提供一次交付语义。阻塞评估(BlockedEvals)用于跟踪在特定类型节点可用之前不应入队的评估,当一个评估通过调度器运行并产生失败的分配时,它将进入阻塞状态,只有当可以运行失败分配的节点的容量变为可用时,阻塞才被解除。计划器(Planner)用于管理分配计划,通过计划队列向当前服务器领导者提交任务分配的提交计划。监视器(Watcher)用于监视调度器创建的部署及其分配,并在分配健康状态转换时触发调度程序。Nomad提供了三种类型的调度器(Scheduler),为常驻服务提供了服务调度器,参考Borg系统,使用一个最合适的评分算法,对满足作业约束的大部分节点进行排名,并选择最佳节点来放置任务组,对较大的候选节点集进行排名会增加调度时间,但会对作业放置的最优性提供更大的保证;为短期性能波动不太敏感且驻留时间短的作业提供了批量调度器,它在找到满足作业约束的节点集之后,使用Berkeley提出的Sparrow[30]调度器中描述的功能来限制排名的节点数,对批处理工作负载进行延迟优化;还有一种系统调度器,用于注册应该在满足作业约束的所有客户端上运行的作业,对于部署和管理集群中每个节点上应该存在的任务非常有用。当客户端加入集群或转换到就绪状态时,系统调度器也会被调用。从Nomad v0.9开始,如果没有足够的容量放置系统作业,系统调度器将抢占在节点上运行的低优先级任务。除了上述三种内置的调度器,用户也可以根据需求定制调度器。
Nomad服务器在作业调度后将任务分配给具体满足约束的客户端设备节点,该节点必然支持任务所需的运行时驱动。在驱动插件机制中,Nomad客户端相当于运行时驱动插件的客户端,通过gRPC与运行时任务驱动插件通信。驱动插件服务器与客户端实现了必要的驱动插件接口,同时驱动插件客户端根据对接的运行时的特点,还可能实现其他接口。驱动插件客户端封装了很多API模型,在与驱动插件服务器的通信中,驱动客户端大多情况下都是在接收信息,驱动插件接口的功能具体实现还是在驱动插件服务器一方。
运行时任务驱动插件包含驱动插件服务器,执行具体的任务工作负载。驱动网络(DriverNetwork)是运行时任务驱动启动任务后返回的网络信息,对于相同的分配,驱动网络不能在调用之间发生改变。这部分创建运行时需要的具体网络方案,驱动插件客户端需要返回驱动网络信息,主要是任务运行的IP地址和端口用于服务注册和健康检查。任务句柄(TaskHandle)是运行时驱动和Nomad客户端共享的状态,它在启动任务后返回给驱动插件客户端,并用于在运行时驱动重新启动期间恢复任务。任务句柄的版本由运行时驱动设置,允许它处理从旧的驱动状态(DriverState)结构升级。事件通知器(Eventer)用于控制将任务事件广播给给所有消费者,它的生命周期与驱动的生命周期绑定在一起。执行器(Executor)用于运行时驱动启动和监控用户进程,进程之间需要进行资源隔离,并不强制要求资源限制,但对于Linux进程将进程放在Cgroups中可以对进程进行更精确的清理。Linux上默认的执行器实现设置了Cgroups,除了进程监控外提供了资源和文件系统隔离,统计内存和CPU相关资源数据指标。执行器根据运行时的特殊性,可以有一层垫片(Shim),例如接入runc/libcontainer运行时,实现的LibcontainerExecutor使用libcontainer shim接管,创建容器并对容器进行管理,在execve进入用户进程之前设置配置的隔离和限制。执行器同样也抽象了基于RPC解耦的C/S架构的插件机制,用于潜在的运行时驱动执行器扩展的需要,也是采取该方式兼容0.9版本之前的执行器的。
以上分三个部分简要分析了运行时驱动作业执行涉及的主要模块。Nomad的原则是简单、灵活,除了主要体现在支持微服务、批处理、容器化和非容器化等多种工作负载外,还有它特有的配置语言上。如图所示,以下是一段HCL配置样例。
图3.2 HCL配置样例
Nomad客户端驱动配置和作业配置等都采用HCL格式定义,这是HashiCorp开源产品统一的配置语言。HCL是专门针对服务器、DevOps工具等设计的一种人机友好的结构化配置语言,它是声明性基础设施即代码的,并提供配套命令行工具一起使用。HCL完全兼容JSON,是JSON的超集,弥补了JSON不支持注释、冗长等缺点。相对于YAML需要小心地使用空白和特殊字符,HCL也更友好。使用JSON格式作为HCL配置的输入是完全合法的,因而,即使以HCL作为专有的配置语言,以JSON作为互操作层也能与其他系统进行互操作。
以运行时驱动接口实现的方式接入,实现Libvirt虚拟机运行时,给运行时驱动很多自主权和独立性,包括虚拟机运行时驱动的配置模式定义、驱动作业的配置模式定义、配置解析、任务运行全生命周期的闭环,而Nomad代理的参与度被大大降低,仅设置部分必要信息和接收信息,在调度等关键生命周期节点上参与决策。