随着技术的发展以及应用对时延、带宽、安全的追求,一个明显的技术趋势是越来越多的应用组件将会被部署到企业所管理的网络边缘。本系列是开源电子书Edge Cloud Operations: A Systems Approach的中文版,详细介绍了基于开源组件构建的边缘云的架构、功能及具体实现。
第5章 运行时控制
运行时控制子系统提供API,各主体(如终端用户、企业系统管理员和云运维人员)可以通过API对正在运行的系统进行更改,为一个或多个运行时参数指定新值。
下面以Aether 5G连接服务为例,假设企业系统管理员想更改一组移动设备的服务质量(Quality-of-Service)。Aether定义了设备组(Device Group) 抽象,以便相关设备可以一起配置。然后,管理员可以修改最大上行带宽(Maximum Uplink Bandwidth) 或最大下行带宽(Maximum Downlink Bandwidth) ,甚至可以为该组选择不同的流量类(Traffic Class) 。类似的,可以设想一下运营商想为现有设备的流量类(Traffic Classes) 中添加一个新的关键任务(Mission-Critical) 选项。先不考虑这些操作的API调用的确切语法,运行时控制子系统需要:
- 验证要执行操作的主体。
- 确定该主体是否有执行操作的足够权限。
- 将新参数设置推送到一个或多个后端组件中。
- 记录指定的参数设置,以便保留新值。
在这个例子中,设备组(Device Group) 和流量类(Traffic Class) 是被操作的抽象对象,虽然运行时控制子系统必须理解这些对象,但对它们的改变可能涉及调用多个子系统的低级控制操作,如SD-RAN(负责RAN中的QoS)、SD-Fabric(负责交换网络的QoS)、SD-Core UP(负责移动核心网用户平面的QoS)和SD-Core CP(负责移动核心网控制平面的QoS)。
简而言之,运行时控制在后端组件集合之上定义了抽象层,将它们有效的转变成外部可见(和可控)的云服务。有时,单一后端组件实现了全部服务,在这种情况下,运行时控制可能只是增加了一个AAA层而已。但是,对于由分散组件组成的云来说,运行时控制是我们定义API的地方,可以在逻辑上将这些组件整合为一套统一的、连贯的抽象服务。这也是为底层子系统"提高抽象水平"和隐藏实现细节的机会。
请注意,由于其作用是基于一组后端组件提供端到端服务,本章介绍的运行时控制机制类似于服务编排器(Service Orchestrator) ,将电信网络中的VNF集合连在一起。这里可以使用任一术语,但我们选择"运行时控制(Runtime Control)"来强调问题的时间性,特别是与生命周期管理的关系。另外,"编排"也是一个有内涵的术语,在不同背景下有不同的含义。在云计算环境中,意味着装配虚拟资源,而在电信环境中,意味着装配虚拟功能。正如在复杂系统中经常出现的情况那样(特别是在促进竞争的商业模式时),你在堆栈中走得越高,对术语的共识就越少。
无论如何称呼这种机制,定义一组抽象和相应的API都是具有挑战性的工作。合适的工具能够帮助我们专注于任务的创造性部分,但却不能消除它。挑战部分在于判断哪些内容应该对用户可见,应该隐藏哪些实现细节,其中部分挑战在于如何处理、合并冲突的概念和术语。我们将在5.3节看到一个完整示例,但为了说明这一困难,请考虑Aether是如何在其5G连接服务中提及用户的。如果直接从电信行业借用术语,那指的是使用移动设备的人(subscriber),同时意味着向该设备提供服务的帐户和设置集合。事实上,用户(subscriber)是SD-Core实现的核心对象。但Aether是为支持企业部署5G而设计的,为此,将用户定义为具有某种规定权限级别的访问API或GUI门户的主体。该用户和核心网定义的用户(subscriber)之间不一定存在一对一的关系,更重要的是,不是所有设备都有用户,像物联网设备的情况一样,通常不与某个人相关联。
5.1 设计概述
在高层次上,运行时控制的目的是提供一个各方可以用来配置和控制云服务的API。为此,运行时控制必须:
- 支持跨多个后端子系统的端到端抽象。
- 将控制和配置状态与抽象相关联。
- 支持配置状态的版本控制,可以根据需要回滚更改,并且可以检索以前配置的审计历史记录。
- 在如何实现这个抽象层方面采用高性能(performance) 、高可用性(high availability) 、可靠性(reliability) 和安全性(security) 的最佳实践。
- 支持基于角色的访问控制(Role-Based Access Control, RBAC) ,以便不同主体对底层抽象对象具有不同的可见性和控制。
- 具有可扩展性,能够随着时间的推移合并新的服务或者升级现有服务的新的抽象。
核心要求是运行时控制能够代表一组抽象对象,也就是说,实现一个数据模型。虽然用于表示数据模型的规范语言有几种可行的选择,但对于运行时控制,我们使用YANG。这有三个原因,首先,YANG是一种丰富的数据建模语言,支持对存储在模型中的数据进行强有力的验证,并且能够定义对象之间的关系。其次,对数据的存储方式没有要求(即不直接与SQL/RDBMS或NoSQL范式挂钩),从而为我们提供了广泛的工程选项。最后,YANG被广泛用于这一目的,意味着有大量强大的基于YANG的工具集合作为我们构建的基础。
延伸阅读:
YANG - A Data Modeling Language for the Network Configuration Protocol. RFC 6020. October 2010.
基于这一背景,图22显示了Aether运行时控制的内部结构,其核心是x-config(维护一组YANG模型的微服务)[1]。x-config使用Atomix(一个键值存储微服务),使配置状态持久化。因为x-config最初被设计为管理设备的配置状态,使用gNMI作为其南向接口,将配置变化传达给设备(或在我们的例子中,软件服务)。必须为任何不支持gNMI的服务/设备编写适配器,这些适配器在图22中被显示为运行时控制的一部分,但将每个适配器视为后端组件的一部分也同样正确,它负责使该组件做好管理准备。最后,运行时控制还包括工作流引擎,负责在数据模型上执行多步骤操作。例如,当一个模型的改变触发了另一个模型上的某些操作时,就会发生这种情况。下一节将详细介绍这些组件。
[1] x-config是一个通用的、模型无关的工具。在AMP中,它管理云服务的YANG模型,但SD-Fabric也基于它来管理网络交换机的YANG模型,SD-RAN也用它来管理RAN组件的YANG模型。这意味着在给定的Aether边缘集群中运行着x-config微服务的多个实例。
Web框架
运行时控制在云运维中扮演的角色类似于Web框架在Web服务运维中扮演的角色。如果一开始就假设某些类别的用户将通过GUI与系统交互(在我们的例子中边缘云就是这一系统),那么你要么用PHP这样的语言编写GUI(就像早期的web开发者所做的那样),要么利用Django或Ruby on Rails这样的框架。这些框架提供了一种方法来定义一组用户友好的抽象(称为模型Models),通过这种方法来在GUI中可视化这些抽象(称为视图Views),以及基于用户输入对后端系统进行更改(称为控制器Controllers)。模型-视图-控制器(MVP, Model-View-Controller)是一种被广泛理解的设计范式,这并非偶然现象。
本章介绍的运行时控制子系统采用了类似的方法,但我们不是用Python(如Django)或Ruby(如Ruby on Rails)来定义模型,而是用一种声明性语言(YANG)来定义模型,而这种语言又被用来生成可编程API。这个API可以从(1)GUI中调用,GUI本身通常是使用另一个框架,如AngularJS;(2)CLI;或(3)某个闭环控制程序。还有其他不同之处,例如,适配器(控制器的一种)使用gNMI作为控制后端组件的标准接口,持久化状态存储在键值存储中,而不是SQL数据库中,但最大的区别是使用声明性语言而不是命令性语言来定义模型。
运行时控制API是从YANG数据模型自动生成的,如图22所示,支持两类门户以及一组闭环控制应用程序,还支持CLI(没有显示)。这个API可以为所有需要读取或写入Aether控制信息的相关方提供单一入口,从而通过中介访问控制和管理平台(Control and Management Platform)的其他子系统(不仅仅是图22中所示的子系统)。
这种情况如图23所示,其关键启示是:(1)我们希望对所有操作进行RBAC和审计;(2)我们希望有权威的配置状态单一来源;(3)我们希望授予每一个负责人对管理功能的有限(细粒度)访问权,而不是假设只有一个特权类别的操作者。当然,底层子系统的私有API仍然存在,操作人员可以直接使用。这在诊断问题时可能特别有用,但由于上述三个原因,有强力理由支持使用运行时控制API来代理所有控制活动。
这个讨论与第4章文末的"GitOps呢?"相关。我们在本章最后将再次讨论同样的问题,但我们现在有了运行时控制选项,在键值存储中维护了系统权威配置和控制状态,为此做好了准备。这就提出了如何与实现生命周期管理的存储库"共享配置状态所有权"的问题。
一种选择是根据具体情况来决定,运行时控制维护某些参数的权威状态,配置存储库维护其他参数的权威状态。我们只需要弄清楚哪个是哪个,这样每个后端组件就知道需要响应哪个"配置路径"。然后,对于任何我们希望运行时控制来代理访问的重新维护的状态(例如,为更广泛的主体集提供细粒度访问),我们需要小心任何对重新维护的状态的后门(直接)更改的后果,例如,通过只在运行时控制的键值存储中存储该状态的一个缓存副本(作为一种优化)。
图23另一个值得注意的方面是,虽然运行时控制代理了所有与控制有关的活动,但它不在所控制的子系统的"数据路径"中。例如,监控和遥测子系统返回的监测数据不经过运行时控制,被直接传递给运行在API之上的仪表盘和应用程序,运行时控制只参与授权对这些数据的访问。另外,运行时控制子系统和监控子系统有自己独立的数据存储,运行时控制使用Atomix键值存储,监控使用时间序列数据库(在第6章有更详细的讨论)。
总之,对统一的运行时控制API的价值最好的说明是能够实现闭环控制应用程序(和其他仪表板): "读取"监控子系统收集的数据,对该数据进行某种分析,也许做出某种决定采取纠正措施,然后"写入"新的控制指令,x-config将其传递给SD-RAN、SD-Core和SD-Fabric的其中一个或某几个,有时甚至传递给生命周期管理子系统(我们将在第5.3节中看到后者的例子)。图24介绍了这种闭环情况,通过显示j将监控子系统和运行时控制子系统放在同一层级(而不是在它下面)而提供了不同视角,尽管两种视角都是有效的。
5.2 实现细节
本节介绍运行时控制子系统中的每个组件,重点介绍每个组件在云管理中扮演的角色。
5.2.1 模型与状态(Models & State)
x-config是运行时控制的核心,其主要工作是对配置数据进行存储和版本控制。配置通过北向gNMI接口推送到x-config,存储在持久化键值存储中,并使用南向gNMI接口推送到后端子系统。
一组基于YANG的模型定义了配置状态的模式。这些模型被加载到x-config中,并共同定义了运行时控制负责的所有配置和控制状态的数据模型。作为示例,Aether的数据模型(schema)在第5.3节中概述,但另一个例子是用于管理网络设备的OpenConfig模型集。
延伸阅读:
OpenConfig: Vendor-neutral, model-driven network management.
这一机制有四个重要方面:
- 持久化存储(Persistent Store): Atomix是云原生键值存储,用于在x-config中持久化数据。Atomix支持分布式映射抽象,实现了Raft共识算法,以实现容错和可伸缩性能。x-config使用NoSQL数据库通用的简单GET/PUT接口向Atomix写入和读取数据。
- 加载模型(Loading Models): 使用模型插件(Model Plugins) 加载模型。x-config通过gRPC API与模型插件通信,在运行时加载模型。模型插件是预编译的,因此在运行时不需要编译。x-config和插件之间的接口消除了动态加载兼容性问题。
- 版本控制和迁移(Versioning and Migration): 所有加载到x-config中的模型都有版本控制,更新模型的过程触发持久状化态从一个版本到另一个版本的迁移。迁移机制支持多版本同时操作。
- 同步(Synchronization): 预计x-config控制的后端组件将周期失败并重启。由于x-config是这些组件的运行时真实来源,负责确保在重启时与最新状态重新同步。x-config的模型包括反映这些组件的操作状态的变量,因此能够检测重启(并触发同步)。
有两点需要进一步阐述。首先,因为Atomix只要在多个物理服务器上运行就具有容错性,可以建立在不可靠的本地(每台服务器)存储之上,因此没有理由使用高可用的云存储。另一方面,为谨慎起见,运行时控制子系统维护的所有状态都要定期备份,以防因灾难性故障而需要从头开始重启。这些检查点,加上存储在Git仓库中的所有配置即代码文件,共同定义了(重新)实例化云部署所需的全部权威状态。
第二,模型定义集就像任何其他的配置即代码的片段,被签入代码库中并进行版本管理,就像第4.5节中介绍的那样。此外,指定如何部署运行时控制子系统的HelmChart确定了要加载的模型版本,类似于Helm Chart确定要部署的每个微服务(Docker镜像)版本的方式。因为运行时控制API是由模型集自动生成的,这意味着Helm Chart有效指定了运行时控制API的版本,我们将在下一小节看到。所有这些都是说,云北向接口的版本控制,作为一个聚合的整体,其管理方式与对云的内部实现作出贡献的每个功能构件的版本控制完全相同。
5.2.2 运行时控制API
API提供了位于x-config和更高层门户和应用程序之间的接口包装器(interface wrapper) ,北向提供了RESTful API,南向提供了gNMI和x-config通信。运行时控制API有三个主要目的:
- 与gNMI(只支持GET和SET操作)不同,GUI开发需要RESTful API(支持GET、PUT、POST、PATCH和DELETE操作)。
- API层是实现早期参数验证和安全检查的机会,使得我们能够在离用户更近的地方捕捉错误,并生成比gNMI更有意义的错误消息。
- API层定义了"门控",可用于审计谁在什么时候执行了什么操作的历史记录(利用下面介绍的身份管理机制)。
从加载到x-config的模型集自动生成REST API是可行的,尽管也可以为了方便而用额外的"手工制作的"调用来增强这组模型(注意这可能意味着API不再是RESTful的)。将模型规范作为单一真实来源,并从规范中导出其他工件,如API,这个想法很吸引人,这么做能够提高开发者生产力,并减少各层之间的不一致。例如,如果开发者希望在某个模型中添加一个字段,如果没有自动生成,下面的内容必须全部更新:
- 模型
- API规范
- 通过对模型进行操作来为API服务的Stub
- 客户端库或开发人员工具包
- 可视化模型的GUI视图
Aether的解决方案是使用名为oapi-codegen
的工具将YANG声明转换为OpenAPI3规范,然后使用oapi-codegen
自动生成实现API的stub。
延伸阅读:
OpenAPI 3.0: API Development for Everyone.
自动生成API并非没有缺陷。模型和API迅速发展成1:1的对应关系,意味着建模中的任何更改都会立即在API中可见。因此,如果要保持向后兼容性,必须小心处理建模更改。因为单个API不能轻易满足两组模型的需求,迁移也更加困难。
另一种方法是引入第二个面向外部的API,并在自动生成的内部API和外部API之间建立一个小转换层(shim)。shim层将起到减震器的作用,减轻内部API可能发生的频繁变更。当然,需要假设面向外部的API是相对稳定的,如果模型更改的首要原因是服务定义还不成熟,那么这就有问题了。如果模型由于控制的后端系统的改变而改变,那么通常情况下模型可以被区分为"低级"或"高级",只有后者通过API直接对客户可见。在语义版本控制术语中,对低级模型的更改可以成为向后兼容的PATCH。
5.2.3 身份管理
运行时控制利用外部身份数据库(LDAP服务器)存储用户数据,例如能够登录的用户的帐户名和密码,LDAP服务器还具有将用户与组关联的功能。例如,将管理员添加到AetherAdmin
组将是在运行时控制中授予个人管理权限的一种明显的方法。
外部认证服务Keycloak用作数据库(如LDAP)的前端,对用户进行身份验证,处理接受密码、验证密码的机制,并安全返回用户所属的组。
延伸阅读:
Keycloak: Open Source Identity and Access Management.
然后,使用组标识符授予对运行时控制子系统内资源的访问权,这将指向相关的问题,即确定哪类用户被允许创建/读/写/删除各种对象集合。与身份管理一样,定义这样的RBAC策略是很容易理解的,并得到开源工具的支持。对于Aether,Open Policy Agent (OPA)扮演了这个角色。
延伸阅读:
Policy-based control for cloud native environments.
5.2.4 适配器
并非每个服务以及运行时控制下面的子系统都支持gNMI,在不支持gNMI的情况下,需要编写适配器来翻译gNMI和服务本地API之间的交互。以Aether为例,一个gNMI->REST适配器在运行时控制的南向gNMI调用和SD-Core子系统的REST北向接口之间进行转换。适配器不一定只是一个语法翻译器,还可能包括自己的语义层。这支持存储在x-config中的模型与南向设备/服务使用的接口之间的逻辑解耦,允许南向设备/服务和运行时控制独立发展,还允许南向设备/服务的替换不影响北向接口。
适配器不一定只支持单一服务。适配器采取的是一种跨越多个服务的抽象手段,并将其应用于每个服务中。Aether中的一个例子是用户平面功能(SD-Core用户平面中的主要数据包转发模块)和SD-Core,它们共同负责确定服务质量,其中适配器将一套单一的模型应用于两个服务。需要注意处理部分失败的情况,即一个服务接受变化,但另一个不接受。在这种情况下,适配器会继续尝试失败的后端服务,直到它成功。
5.2.5 工作流引擎
图22中x-config左边的工作流引擎是实现多步工作流的地方。例如,定义新的5G连接或将设备与现有连接关联是一个多步骤的过程,需要使用多个模型并影响多个后端子系统。根据我们的经验,甚至可能有复杂的状态机来实现这些步骤。
有一些众所周知的开源工作流引擎(例如,Airflow),但我们的经验是,它们与Aether等系统典型的工作流类型不匹配。因此,当前采用了特别的实现,使用命令式代码监视目标模型集,并在它们发生更改时采取适当的行动。为工作流定义更严格的方法是正在进行中的课题。
5.2.6 安全通信
gNMI自然适合使用双向TLS进行身份验证,这是使用gNMI的组件之间安全通信的推荐方式。例如,x-config与其适配器之间的通信使用gNMI,因此使用双向TLS。在组件之间分发证书是运行时控制范围之外的问题,我们假定另一个工具将负责分发、撤销和更新证书。
对于使用REST的组件,HTTPS用于保护连接,可以使用HTTPS协议中的机制进行身份验证(基本身份验证、令牌等)。当使用这些REST API时,Oauth2和OpenID Connect被用作授权提供者。
5.3 连接服务建模
本节概述了Aether连接服务的数据模型,以说明运行时控制扮演的角色。这些模型是在YANG中指定的(我们为其中一个模型提供了具体示例),但由于运行时控制API是由这些规范生成的,它同样有效的考虑了支持REST的GET, POST, PUT, PATCH和DELETE操作的一组web资源(对象):
- GET: 获取对象。
- POST: 创建对象。
- PUT, PATCH: 更改对象。
- DELETE: 删除对象。
每个对象都是YANG定义的某个模型的实例,每个对象都包含用于标识对象的id字段。这些标识符是特定于模型的,例如,站点有site-id,而企业有enterprise-id。模型通常是嵌套的,例如,站点是企业的成员。对象还可以包含对其他对象的引用,这样的引用是基于对象的唯一id实现的。在数据库设置中,通常称为外键(foreign keys)。
除了id字段之外,其他几个字段对于所有模型也是通用的。这些包括:
- description: 人类可读的描述,用于存储关于对象的附加上下文。
- display-name: 用于在GUI中显示人类可读的名称。
因为这些字段对所有模型都是通用的,所以将在接下来的模型介绍中省略。在下面的例子中,我们用大写来表示模型(例如,Enterprise),用小写表示模型中的字段(例如,enterprise)。
5.3.1 企业(Enterprises)
Aether部署在企业中,因此定义了一组具有代表性的组织抽象。其中包括Enterprise,它形成了特定于客户的层次结构的根。Enterprise模型是许多其他对象的父对象,并允许将这些对象限定在特定的Enterprise范围内,以实现所有权和基于角色的访问控制目的。Enterprise模型包含以下字段:
- connectivity-service: 为该企业实现连接性的后端子系统列表,对应SD-Core、SD-Fabric和SD-RAN的API端点。
Enterprises进一步分为Sites。站点是Enterprise的接入点,可以是物理的,也可以是逻辑的(例如,一个地理位置可以包含多个逻辑站点)。Site模型包含以下字段:
- imsi-definition: 如何为本站点建立IMSI的描述,包含以下子字段:
- mcc: Mobile country code。
- mnc: Mobile network code。
- enterprise: 数字表示的enterprise id。
- format: 允许上述三个字段嵌入到IMSI中的掩码。例如,CCCNNNEEESSSSSS将使用3位MCC、3位MNC、3位ENT和6位subscriber构建IMSI。
- small-cell: 5G无线基站或接入点或无线电的列表,每个small cell有以下内容:
- small-cell-id: small cell的标识符,与其他id字段的用途相同。
- address: small cell的主机名。
- tac: Type Allocation Code。
- enable: 如果设置为true,启用small cell,否则为禁用。
imsi-definition是特定于移动蜂窝网络的,对应于刻入每一张SIM卡的唯一标识符。
5.3.2 切片(Slices)
Aether将5G连接建模为一个Slice,表示一个隔离的通信通道(和相关的QoS参数),该通道将一组设备(建模为一个Device-Group)连接到一组应用程序(每个应用程序建模为一个Application)。每个slice都嵌套在某个site中(该site又嵌套在某个enterprise中),例如,企业可能配置一个切片承载IoT流量,另一个切片承载视频流量。Slice模型有以下字段:
- device-group: 可以加入此Slice的Device-Group对象列表。列表中的每个条目都包含对Device-Group的引用以及enable字段,该字段可能用于临时删除对组的访问。
- app-list: 这个Slice允许或拒绝的Application对象列表。列表中的每个条目都包含对Application的引用以及allow字段,该字段可以设置true或者false以允许或拒绝应用程序接入。
- template: 对用来初始化这个Slice的Template的引用。
- upf: 索引用于处理该Slice报文的UPF(User Plane Function),允许多个Slices共享一个UPF。
- sst, sd: 3GPP定义的切片标识符,由运维团队分配。
- mbr.uplink, mbr.downlink, mbr.uplink-burst-size, mbr.downlink-burst-size: 该切片所有设备的最大比特率和峰值大小。
使用选定的模板(template) 初始化与速率相关的参数,如下所述。还要注意,这个例子说明了如何使用建模来强制定义不变量,在这种情况下,UPF和Device-Group的Site必须匹配Slice的Site。也就是说,连接到切片的物理设备和实现切片的核心段的UPF必须被限制在一个物理位置内。
Slice的一端是Device-Group,标识一组允许使用切片连接到各种应用程序的设备。Device-Group模型包含以下字段:
- devices: 设备列表,每个设备都有enable字段,可以用来启用或禁用设备。
- ip-domain: 引用IP-Domain对象,描述该组内终端的IP和DNS设置。
- mbr.uplink, mbr.downlink: 设备组的每设备最大比特率。
- traffic-class: 该组中设备使用的流量类。
Slice的另一端是Application对象列表,指定程序设备通信的端点。Application模型包含以下字段:
- address: 终端的DNS名称或IP地址。
- endpoint: 端点列表,每个都有以下字段:
- name: 端点名称,用作key。
- port-start: 起始端口号。
- port-end: 结束端口号。
- protocol: 端点的协议(TCP|UDP)。
- mbr.uplink, mbr.downlink: 应用程序端点的每设备最大比特率。
- traffic-class: 与此应用程序通信的设备的流量类。
熟悉3GPP的人都知道Aether的Slice抽象类似于规范中的网络切片概念。Slice模型定义包括3GPP指定的标识符(例如,sst和sd)的组合,以及底层实现的细节(例如,upf表示核心网用户平面的UPF实现)。虽然还不是生产系统的一部分,但有一个版本的Slice也包括了与RAN切片相关的字段,其中的运行时控制子系统负责将RAN、Core和Fabric之间的端到端连接无缝整合在一起。
平台服务API
我们使用Connectivity-as-a-Service作为运行时控制扮演的角色的一个示例,但也可以使用相同的机制为其他平台服务定义API。例如,由于Aether中的SD-Fabric是用可编程交换硬件实现的,转发平面是用带内网络遥测(INT)仪表化的,北向API支持在运行时对每个流进行细粒度数据收集,这使得在Aether之上编写闭环控制应用程序成为可能。
本着类似的精神,本节中给出的与QoS相关的控制示例可以通过附加对象进行扩展,这些对象提供了对SD-RAN实现的各种无线电相关参数的可见性和控制机会。这样做将是向平台API迈出的一步,该API有助于实现一类新的行业自动化边缘云应用程序。
一般来说,IaaS和PaaS产品需要支持面向应用程序和用户的API,这些API超出了底层软件组件(即微服务)所使用的DevOps级配置文件。创建这些接口是定义有意义的抽象层的练习,如果使用声明性工具,就变成了定义高级数据模型的练习。运行时控制是管理子系统,负责为这样的抽象层指定和实现API。
5.3.3 模板和流量类
与每个Slice相关联的是QoS相关配置文件,该配置文件管理如何处理该Slice携带的流量。从Template模型开始,定义了有效的(可接受的)连接设置。Aether运维团队负责定义这些(他们提供的特性必须得到后端子系统的支持),企业可以选择想要应用于创建的连接性服务的任何实例的模板(例如,通过下拉菜单)。也就是说,模板被用来初始化Slice对象。Template模型有以下字段:
- sst, sd: 切片标识符,由3GPP指定。
- mbr.uplink, mbr.downlink: 最大上下行带宽。
- mbr.uplink-burst-size, mbr.downlink-burst-size: 最大峰值大小。
- traffic-class: 链接到描述流量类的Traffic-Class对象。
注意,Device-Group和Application模型也包含类似字段。其理念是,将QoS参数作为一个整体(基于所选template)建立起来,然后连接到该切片的各个设备和应用程序可以在逐个实例的基础上分配自己的、限制性更强的QoS参数。
如前一节所述,Aether将抽象Slice对象从端到端切片的后端段的实现细节中分离出来。这种解耦的一个原因是,支持选择一个全新的SD-Core副本,而不是与另一个Slice共享同一个UPF。这样做是为了确保隔离,并说明了运行时控制和生命周期管理子系统之间可能的接触点: 运行时控制通过适配器,与生命周期管理一起启动必要的Kubernetes容器集,以实现隔离的切片。
Traffic-Class模型指定了流量的分类,包括以下字段:
- arp: 分配和保留优先级(Allocation and retention priority)。
- qci: QoS类标识符(QoS class identifier)。
- pelr: 丢包率(Packet error loss rate)。
- pdb: 包时延预算(Packet delay budget)。
完整起见,下面显示了Template模型对应的YANG。为了简单起见,示例省略了一些介绍性的样板文件。该示例突出了模型声明的嵌套性质,包括container
字段和leaf
字段。
module onf-template {
...
description
"The aether vcs-template holds common parameters used
by a virtual connectivity service. Templates are used to
populate a VCS.";
typedef template-id {
type yg:yang-identifier {
length 1..32;
}
}
container template {
description "The top level container";
list template {
key "id";
description
"List of vcs templates";
leaf id {
type template-id;
description "ID for this vcs template.";
}
leaf display-name {
type string {
length 1..80;
}
description "display name to use in GUI or CLI";
}
leaf sst {
type at:sst;
description "Slice/Service type";
}
leaf sd {
type at:sd;
description "Slice differentiator";
}
container device {
description "Per-device QOS Settings";
container mbr {
description "Maximum bitrate";
leaf uplink {
type at:bitrate;
units bps;
description "Per-device mbr uplink data rate in mbps";
}
leaf downlink {
type at:bitrate;
units bps;
description "Per-device mbr downlink data rate in mbps";
}
}
}
container slice {
description "Per-Slice QOS Settings";
container mbr {
description "Maximum bitrate";
leaf uplink {
type at:bitrate;
units bps;
description "Per-Slice mbr uplink data rate in mbps";
}
leaf downlink {
type at:bitrate;
units bps;
description "Per-Slice mbr downlink data rate in mbps";
}
}
}
leaf traffic-class {
type leafref {
path "/tc:traffic-class/tc:traffic-class/tc:id";
}
description
"Link to traffic class";
}
leaf description {
type at:description;
description "description of this vcs template";
}
}
}
}
5.3.4 其他模型
上述介绍参考了其他模型,我们在此不做全面描述。其中包括IP-Domain,指定IP和DNS设置,以及UPF,指定用户平面功能(SD-Core的数据平面组件),代表连接服务的特定实例转发数据包。UPF模型是必要的,因为一个Aether部署可以运行许多UPF实例。有两种不同的实现方式(一种是作为服务器上的微服务运行,另一种是作为加载到交换网络中的P4程序运行),而且在任何时候都可以实例化多个基于微服务的UPF,每个都隔离不同的流量。
延伸阅读:
L. Peterson, et al. Software-Defined Networks: A Systems Approach. November 2021.
5.4 重温GitOps
正如我们在第四章末尾所做的那样,重温如何区分配置状态和控制状态的问题是有意义的,生命周期管理(和它的配置存储库)负责前者,而运行时控制(和它的键值存储)负责后者。现在我们已经更详细的了解了运行时控制子系统,很明显,关键因素是访问和更改该状态是否需要通过编程接口(加上访问控制机制)。
云运营商和DevOps团队完全有能力将配置变化签入配置存储库中,这可能会使人们倾向于将所有在配置文件中指定的状态视为生命周期管理的配置状态。增强的配置机制的可用性,如Kubernetes Operators,使这种诱惑力更大。但是,任何可能被运维人员以外的人触及的状态,包括企业IT管理员和运行时控制应用程序,都需要通过定义明确的API来访问。给予企业设置隔离和QoS参数的能力是Aether的一个说明性例子。从一组模型中自动生成API是实现这种控制接口的一个有吸引力的方法,不考虑其他原因的话,至少它迫使接口定义与底层实现解耦(用适配器来弥补这个差距)。
在后一点上,我们很容易想象一种运行时控制操作的实现,包括将配置变更签入配置存储库中并触发重新部署。认为这样的方法是优雅的还是笨拙的这是一个品味问题,但如何做出这样的工程决策,很大程度上取决于后端组件的实现方式。例如,如果配置变更需要重新启动容器,那么可能没有什么选择。但理想情况是,微服务实现有自己明确的管理接口,可以被初始化时的操作者(启动初始化组件)或控制时的适配器(在运行时改变组件)调用。
对于与资源相关的操作上,如响应用户请求创建Slice或激活边缘服务而启动额外的容器,类似的实施策略是可行的。Kubernetes API可以从Helm(在启动时初始化微服务)或从运行时控制适配器(在运行时添加资源)调用。剩下的挑战是决定哪个子系统维护该状态的权威副本,并确保该决定作为系统的不变性而被强制执行[2]。这样的决策通常是根据情况而定的,但我们的经验是,使用运行时控制作为唯一真实来源是一个可靠的方法。
[2] 还可以维护状态的两个权威副本,并实现保持同步的机制。这种策略的困难在于如何避免绕过同步机制的后门访问。
当然,硬币有两面。提供配置参数的运行时控制也很有诱惑力,在一天结束的时候,只有云运维人员需要能够更改这些参数。配置RBAC(例如,添加组并定义允许给定组访问哪些对象)是一个示例。除非有令人信服的理由向最终用户开放这样的配置决策,否则在配置存储库中维护RBAC相关配置状态(即OPA规范文件),在生命周期管理的权限下,是完全有意义的。
这些示例说明了运行时控制接口的核心价值主张,即扩展操作,让终端用户和闭环控制程序直接控制系统,而不需要运维团队充当中介。
用户体验方面的考虑
运行时控制涉及到云运维的一个重要方面,但往往没有得到充分重视: 考虑用户体验(UX)。如果你唯一关心的用户是云及其服务的开发人员和运营商,我们可以假设他们愿意编辑少量YAML文件来执行更改请求,那么也许我们可以就此停止。但是,如果我们希望最终用户有一定的能力来操纵我们正在构建的系统,还需要通过一组用户可以访问的仪表盘和按钮来"探测"我们已经实现的底层变量。
用户体验设计是一门成熟的学科。它在一定程度上是关于设计具有直观工作流程的GUI,但GUI依赖于程序化的界面,定义这个界面是我们在本书中关注的管理和控制平台与我们想要支持的用户之间的接触点。这在很大程度上是一个定义抽象的练习,这使我们回到了想要表达的核心观点:是底层实现的现实和目标用户的心理模型塑造了这些抽象。正如任何读过用户手册的人所理解的那样,只考虑其中一个而不考虑另一个,将是一种灾难。
你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
微信公众号:DeepNoMind