O-RAN 定义的 RIC 模型并不能很好支持对实时性有很高要求的用例,本文定义了一套基于 eBPF 的内联执行架构,从而可以将 RIC 的支持扩展到实时场景。原文: Taking 5G RAN Analytics and Control to a New Level
摘要
Open RAN 为 5G 无线接入网(RAN)引入了模块化和解耦的设计范式,并承诺通过 O-RAN 定义的 RAN 智能控制器(RIC, RAN Intelligent Controller)实现完全的可编程性。然而,由于延迟和安全方面的挑战,RIC 提供的遥测和控制主要局限于较高层面以及较长的时间尺度(>10𝑚𝑠),同时还依赖于难以改变的预定义服务模型。为了解决这些问题,我们提出了一个完全可编程的监测控制系统--Janus,考虑到 RAN 的特殊性,该系统专注于灵活性、高效率和安全性。Janus 建立在 eBPF 基础上,允许第三方以可证明的安全方式在 RAN 的功能组件中直接加载小程序(codelet)。我们用一种新的字节码修补算法来扩展 eBPF,该算法可强制保证小程序运行时的阈值,并以安全的方式收集用户定义的遥测数据。我们建立了 3 个不同类别的应用(共 17 个应用,大多数在现有的 RIC 上无法实现),并在不影响 RAN 性能的情况下部署在 100MHz 的 4x4 MIMO 5G 小区上,从而可以证明 Janus 的灵活和高效。
1. 简介
5G 无线接入网(RAN)的一个关键转变是迁移到 Open RAN 架构,即 RAN 功能的虚拟化(vRAN)和解耦。这种方法允许供应商以更快的速度为不同组件提出独特的解决方案,从而促进创新。此外,无线智能控制器(RIC)[27, 31]允许第三方通过 O-RAN[19]标准化开放接口建立数据驱动的、与供应商无关的监测和控制应用[9, 20],从而更好的优化网络。
尽管愿景引人注目,但相关创新很大程度上仍处于非常早期的阶段,主要有两个原因。首先,RAN 以很高频率产生大量数据,为 RIC 应用捕获、传输和处理数据会给计算和网络带来巨大压力。为了克服这个问题,由 3GPP[13,14]标准化的传统方案是定义一小套关键性能指标(KPI)集合,每几秒钟或几分钟收集一次。O-RAN RIC 通过提供一套新的标准化聚合 KPI 和数据源[25],扩展了这一方案。每个 KPI 都通过服务模型(一种静态 API 形式,作为嵌入 vRAN 功能的 RIC 代理的一部分[24])定义,并规定了哪些数据可以被收集、以何种颗粒度收集。然而,这种方法发展缓慢,而且不能灵活扩展,任何不适合现有服务模型的用例,都需要指定定义了不同 KPI 集的新服务模型,并与选定的 RIC 和 RAN 供应商合作,增加对这种服务模型的支持,然后经历漫长的标准化过程,说服所有 O-RAN 供应商支持并实施这种模型。
其次,许多关键的 RAN 操作,如用户无线资源调度和功率控制必须在期限内完成,通常是几十𝜇s 到几 ms 不等。为了满足最后期限,任何相关控制逻辑和推理必须在 vRAN 功能内部运行,而不是在 RIC 上,RIC 的设计是为了处理时间尺度>10ms[78]的应用。现有 RIC 方法通过指定针对特定用例的服务模型来处理这个问题,每个模型都有一套支持策略(例如,从𝑁个可用无线资源调度算法中选择一种)。然而,与数据收集的情况类似,由于无法灵活引入新的控制和推理算法,因此很难扩展。此外,许多 vRAN 操作具有实时性要去,意味着为了支持新服务模型而增加的任何新功能必须在 vRAN 处理期限内完成,超出期限可能会导致性能下降[45],甚至使 vRAN 崩溃(正如第 7.2 节中所示),这使得 RAN 供应商不愿意增加新的功能和服务模型。
为了解决这些问题,我们提出了 Janus,一个提供动态监测和控制 vRAN 功能的系统。Janus 扩展了 RIC,允许运营商和受信任的第三方编写自己的遥测、控制和推理代码(我们称之为小程序 codelet),这些代码可以在运行时部署在不同的 vRAN 组件上,不需要供应商的任何帮助,也不会干扰 vRAN 的运行。小程序通常在 vRAN 关键路径上内联执行,直接访问所有重要的原始内部数据结构,收集任意统计数据,并做出实时推理和控制决策。
虽然 Janus 通过将 vRAN 实现和数据收集、控制操作解耦,大大增强了 RIC 的能力,但也带来了一系列挑战。第一个挑战与灵活性有关,目前还不清楚构建有用的应用需要哪些以及多少 RAN 监控数据,以及哪些控制功能应该暴露给小程序开发者。我们通过识别标准 vRAN 架构中的关键位置和接口(称之为钩子 hook)来解决这个问题,接口可以从若干收集点(collection point)收集丰富多样的数据,从而实现广泛的实时控制应用。我们还建立了一个工具链,允许开发者定义任意的数据输出结构,将收集的数据发送给 RIC。最后,我们确定了一小部分精心选择的控制功能(我们称之为执行器功能 actuator functions),可以由加载在控制钩子中的小程序调用,实时改变 RAN 的行为(如改变功率、资源块分配等)。通过第 4 节中展示的用例,可以证明 O-RAN 服务模型可以作为 Janus 小程序,实现快速有效的控制和推理操作(例如,无线资源分配和干扰检测),而这在 O-RAN RIC 中是不可能的。
第二个挑战是执行的安全性。虽然小程序是由受信任的各方提供的,但仍然可能出现无效内存访问或过长执行时间,从而导致错误和低效,造成数据损坏,超过实时完成时间,最终导致 vRAN 功能崩溃。我们通过提供基于 eBPF[3, 92]的沙盒执行环境来解决这一挑战,eBPF 解决了 Linux 内核[2]中的类似问题。小程序由 C 语言编写,被编译成 eBPF 字节码,字节码在 vRAN 控制和数据路径中通过内联的 eBPF 虚拟环境运行,可以直接访问选定的内部 RAN 数据结构和控制功能。在加载小程序之前,eBPF 执行环境会对字节码[48, 92]进行静态检查,只允许运行内存访问安全的小程序。
我们进一步扩展该模型,使其符合 vRAN 要求,通过一种新颖的 eBPF 字节码修补机制,我们为小程序的执行时延加入了硬性的𝜇s 级控制,该机制允许强制中断运行超过一定时间阈值的小程序。此外,我们还扩展了小程序的静态验证过程,以覆盖新引入的灵活的输出数据结构。我们还做了一些优化,确保在 fastpath(Janus 小程序运行的地方)支持当前还不知道的设计,使 Janus 对 vRAN 的性能影响最小。最后,我们将 Janus 与 CapGemini 公司[35]的商业 5G vRAN 协议栈(基于英特尔 FlexRAN 参考设计[57])和 OpenAirInterface(OAI)[26]的开源 4G/5G 协议栈集成。
综上所述,我们作了如下工作:
- 提出了第一个安全、可编程框架,以最小的计算开销为 vRAN 动态引入灵活的监测和控制能力(第 3 节),并通过在其上开发新的遥测、控制和推理应用来说明其功能(共 17 个应用)(第 4 节)。
- 提出并构建强制限制小程序运行时间和数据安全收集的机制,以确保 vRAN 满足安全和延迟要求(第 5 节)。
- 实现并优化了 Janus(第 6 节),并进行了全面评估(第 7 节)。
我们希望将来 Janus 能与 O-RAN RIC 集成,以进一步加强 Open RAN 架构,实现可观察、可编程和自动化的最终目标。
2. 背景及动机
2.1. vRAN 架构
如图 1 所示,5G RAN 由若干层组成(如 PHY、MAC、RLC),每一层负责一组不同的控制和/或数据平面操作。例如,PHY 负责信号处理,MAC 负责用户设备(UE, UserEquipment)之间的无线资源实时调度。这些层分布在三个网络功能中,称为无线电单元(RU, Radio Unit)、分布式单元(DU, DistributedUnit)和集中式单元(CU, Centralized Unit),CU 又被进一步分解为控制面和用户面组件(CU-CP 和 CU-UP)。RU 通常基于 ASIC 或 FPGA,而 CU 和 DU 是虚拟化的(即 vCU 和 vDU),运行在带有通用处理器和加速器的商品硬件上[56, 90]。不同组件和层有不同的时延要求(参见[32]),并以不同速率产生事件和数据,如图 1 所示。
图 1: vRAN 体系架构和流程概要以及吞吐量要求。
vRAN 组件之间的通信是通过 O-RAN[19]和 Small Cell Forum[41]等标准化组织定义的开放接口实现的,而可编程性是通过近实时 RIC[47]来实现。网络运营商在 RIC 上安装应用程序(O-RAN 术语中的 xApps),以收集数据并进行推理、闭环网络优化或以近实时(>10ms)的反应速度报告问题。vRAN 组件的数据收集和控制是通过供应商嵌入 vRAN 功能的服务模型来实现的,这些模型定义了每个 xApp 的数据报告类型和频率以及 RIC 可以执行的控制策略列表。
2.2. vRAN 可编程性的限制
RIC 用例最初的重点是网络自我优化、异常检测和粗粒度无线资源分配[60, 75, 78, 85]。在这样的用例中,重要的网络事件和控制决策以较低频率发生(每秒 10 次到 100 次)。xApp 可以收集所需的遥测数据,执行推理并通过预先确定的控制策略集调整 vRAN 功能。不幸的是,该方案有以下严重限制:
数据量限制。 许多应用,如定位[62]、信道估算[66, 71]、干扰检测[63]和波束成形[72]都需要来自 PHY 的上行 IQ 样本。将所有 IQ 样本传送到 RIC 是不可行的(对于 100MHz 的 4×4 MIMO 来说,每个小区需要超过 5Gbps 的数据)。目前 RIC 通过在每个 xApp 服务模型中指定所需数据类型和频率(例如[37,38])来克服这一问题。数据格式及其所需的预处理(例如,子采样和平均数)取决于服务模型,vRAN 供应商必须实现和支持每个专有服务模型,因此对互操作性构成了严重限制。
实时性限制。 一些 vRAN 控制回路(如 UE 无线资源分配和功率控制)有严格的时间限制(几十𝜇s 到几 ms)。不幸的是,目前的 RIC 设计无法满足这种时间限制,预期时延>10ms[78]。与遥测的情况一样,xApps 通过使用服务模型提供的一组策略来克服这个问题,这些策略可以在 vRAN 功能内运行。然而,随着可用策略数量的增加,这种方法并不能扩展。例如,已经为网络切片提出了几种控制算法(例如,[42, 49, 52, 61, 74, 87]),每一种都是为特定情况量身定做的。将这种算法作为服务模型的一部分来实施将会非常困难,因为不得不要求所有 RAN 供应商都采用这些算法。
2.3. vRAN 可编程性的需求
基于上述的局限性,我们认为需要新的解决方案来释放 RIC 的真正能力,该解决方案应满足以下要求:
(1)提供灵活的遥测机制,受信任的开发者可以访问原始 vRAN 数据,并根据应用要求和基础设施限制选择输出数据的类型、频率和粒度。
(2)实现任意控制和推理逻辑的能力,并且可以在 RAN 内部实时运行。
(3)安全的执行环境,保证任何(受信任的)代码在 vRAN 功能内运行时不会因为执行无效的内存访问而使 vRAN 崩溃,或超出实时处理的最后期限。
3. Janus 概述
为了克服上述限制,Janus 引入了一个内联代码执行框架,允许在 vRAN 的沙盒环境中动态加载自定义遥测或控制/推理代码。
3.1. 内联代码执行框架
图 2: Janus 架构概要
图 2 显示了 Janus 的高层架构,下面介绍其主要组件。
Janus 设备(Janus device)。 Janus 设备是任何允许执行自定义代码的 vRAN 组件(即 vCU 或 vDU 进程)。Janus 在用户空间的 eBPF VM 实例内执行自定义代码[59]。我们在 vRAN 中选定的地方引入 Janus 调用点或钩子(hook) ,这些地方可以调用自定义 eBPF 代码,与 vRAN 代码内联,使 eBPF 代码可以只读访问选定的 vRAN 内部上下文和辅助函数,包括各种 3GPP 定义的数据结构(见表 1 和第 3.2 节)。自定义代码可以在运行时从 Janus 设备上动态加载和卸载,而不会影响设备性能。我们选择 eBPF 作为沙箱技术,该技术可以支持内联、快速执行,并支持用高级语言(C)编写小程序,而且提供静态代码验证。其他方法,如 Sandbox2[50]和 SAPI[51],需要在单独进程中运行自定义代码,有较高的 IPC 时延。WebAssembly[53]也是内联的,但缺乏静态验证,会导致异常内存问题[64]。
表 1: 商业级 vCU/vDU 网络功能和 OpenAirInterface 中引入的 Janus 监控钩子。
Janus 小程序(Janus codelets)。 Janus 小程序是可以在运行时动态部署在 Janus 设备上的自定义代码。开发人员用 C 语言编写小程序,使用 eBPF 编译器将其编译成字节码。与任何 eBPF 程序类似,Janus 小程序必须通过静态验证(例如,必须引入内存检查,只能有有界循环)。任何可能不安全的操作(例如,访问允许范围之外的内存),只能通过一组白名单辅助函数(helper) 来执行。小程序在两次调用之间不保留任何状态,所有状态必须存储在一个叫做 map 的外部位置。小程序通过一个特殊的 map 将遥测数据发送到设备上运行的 Janus 输出线程,后者将其转发给 Janus 控制器。小程序集(codeletset) 是一组小程序的集合,在 Janus 设备的多个钩子上运行,并通过共享 map 以非常低的延迟进行协同。如果需要,跨设备的小程序可以通过控制器协调。
Janus 控制器(Janus controller)和 SDK。 Janus 控制器负责控制 Janus 设备和小程序集,在 O-RAN 背景下,可以作为 RIC xApp 来实现。开发人员将小程序集上传到控制器,并为一个或多个 Janus 设备提供加载/卸载指令。在控制器允许加载小程序集之前,验证每个小程序集的安全性并确保其能终止。为此,我们增强了开源 PREVAIL eBPF 验证器[48],对 Janus 辅助函数和输出模式进行验证。该控制器进一步用额外的控制代码对经过验证的字节码进行处理,如果运行时间超过了某个阈值,就会强制终止(见第 5.1 节)。打过补丁的小程序被 JIT 编译并通过网络推送到 Janus 设备上,同时还有基于 protobuf 灵活定义数据输出格式所需的元数据文件(见第 5.2 节)。控制器提供数据收集器,收集并反序列化从 Janus 小程序发送的数据。Janus 还提供了 SDK,允许开发者在开发周期早期对 Janus 小程序进行本地测试,该 SDK 包括编译器、验证器和调试器,以及 Janus 设备支持的所有辅助函数和 map 定义。
3.2. vRAN RIC 新功能
我们通过一个简单而现实的例子(示例 1)来介绍 Janus 实现的新的监测和控制能力。这个例子是为 OpenAirInterface[26]的 vDU 开发的 Janus 小程序,被 FAPI 接口钩子所调用([88],图 1)。FAPI 消息是 C 结构,有发给 UE 的关于无线资源的调度信息。在这个小程序中,每隔 1000 个事件,就会有一个维护获取的 FAPI 消息数量的计数器被发送到数据采集器。虽然很简单,但这个小程序提供了重要特征,证明 Janus 比传统 RIC 的设计更强大。
1 struct janus_load_map_def SEC (" maps ") countermap = { 2 . type = JANUS_MAP_TYPE_ARRAY , 3 . key_size = sizeof ( uint32_t ) , 4 . value_size = sizeof ( uint32_t ) , 5 . max_entries = 1 , 6 }; 7 8 struct janus_load_map_def SEC (" maps ") outmap = { 9 . type = JANUS_MAP_TYPE_RINGBUF ,10 . max_entries = 1024 ,11 . proto_msg_name = " output_msg ",12 . proto_name = " output_msg ",13 . proto_hash = PROTO_OUTPUT_MSG_HASH ,14 };1516 SEC (" janus_ran_fapi ")17 uint64_t bpf_prog ( void * state ) {18 void * c ;19 uint32_t index = 0 , counter ;20 nfapi_dl_config_request_pdu_t *p , * pend ;21 output_msg s ;2223 struct janus_ran_fapi_ctx * ctx = state ;24 p = ( nfapi_dl_config_request_pdu_t *) ctx - > data ;25 pend = ( nfapi_dl_config_request_pdu_t *) ctx - > data_end ;2627 if ( p + 1 > pend ) return 1;2829 if (p - > ndlsch_pdu > 0) {30 c = janus_map_lookup_elem (& countermap , & index ) ;31 if (! c ) return 1;32 counter = (*( int *) c + p - > ndlsch_pdu ) ;33 if ( counter == 1000) {34 s . counter = counter ;35 janus_ringbuf_output (& outmap , &s , sizeof ( s ) ) ;36 counter = 0;37 }38 }39 return 0;40 }
复制代码
示例 1: Janus 小程序示例
安全访问丰富的 vRAN 数据。 示例 1 第 17 行的状态参数是传递给 Janus 钩子的上下文,包含指向 vRAN FAPI 结构的指针41, 81。该结构含有特定下行链路时隙的调度分配信息,每个用户信息有多达 20 多个字段,包括调制和编码方案、传输块大小、分配的资源块、MIMO 等。验证器确保对内部 vRAN 上下文信息的只读访问。由于 vRAN 的模块化设计,不同标准(3GPP,Small cell 论坛)规定了少量类似接口,这些接口在 vRAN 组件中携带所有相关状态。通过在这些接口上添加钩子,可以让 Janus 开发者访问大量 vRAN 遥测数据。我们已经确定并实现了这些钩子(表 1),在第 4 节中将演示如何利用这些数据实现多个 RIC 应用,而不需要修改 vRAN 的代码。请注意,不同 vRAN 供应商对接口的实现可能有所不同(例如,不同的 C 结构内存布局)。由于 Janus 的灵活性,我们能够根据观察到的差异进行调整,通过少量代码修改来保证相同的小程序功能。
状态维护。 Janus 小程序依靠被称为 map 的共享内存区域来存储状态,并与其他小程序交换状态(参见第 4 节和附录 A.1)。Janus 提供了各种存储数据的 map 类型,包括数组、hashmap 和布隆过滤器。在这个例子中,我们使用单元素数组来维护 FAPI 数据包计数器(第 1-6 行)。每次调用时,通过辅助函数从内存中恢复计数器引用(第 30 行),根据新收到的数据包数量递增(第 32 行),同时进行各种安全检查以实现静态验证(例如第 27 行和第 31 行)。
灵活的输出格式。 Janus 小程序可以通过一个特殊类型的环形缓冲区 map(第 8-14 行),使用灵活的输出格式向数据采集器发送任意遥测数据,该 map 被关联到一个由小程序开发人员定义的特定 protobuf 格式(见第 5.2 节)。这个例子使用了一个名为output_msg
的自定义 protobuf 格式(第 21 行),包含一个计数器字段(第 34 行),通过辅助函数(第 35 行)将数据导出到数据收集器。这种灵活性使我们能够基于 Janus vRAN 钩子实现 O-RAN RIC 规范中目前可用的数据模型,而无需修改 vRAN 中的任何一行代码。
安全、丰富的自定义控制操作。 Janus 小程序不能直接修改 vRAN 状态,相反,对 vRAN 行为的修改是通过由 vRAN 供应商提供的执行器辅助功能(actuator helper function) 完成的,执行器负责应用小程序决定的变更。鉴于 vRAN 功能中只有少量实时控制回路,只修改符合标准的 3GPP 参数(存在于所有 RAN 供应商的实现中),我们认为使用执行器辅助功能是合理的方式。在此基础上,我们确定并实现了一些控制钩子(列在表 2 中),这些钩子控制了大多数关键控制回路,可用于新的控制应用。我们使用切片间无线资源调度钩子作为示例,实现 OAI 中的 3 种网络切片算法(见第 4 节),通过使用allocate_slice_rbs()
执行器辅助函数修改分配给每个切片的资源块数量。
表 2: 在商用级 vDU 网络功能和 OpenAirInterface 中引入 Janus 控制钩子。
4. Janus 创新用例
我们在这节介绍几个有代表性的遥测、推理和控制应用示例,说明 Janus 的价值(关于评估见第 7 节)。
表 3: Janus 的监控(M)、控制(C)、推理(I)用例,LOC 为代码行数。
灵活监控。 基于 Janus 实现的小程序可以帮助我们提取 O-RAN KPM 模型[21]中指定的 KPI(表 3 1-8 行),以及原始调度数据(9-10 行),而无需改变 vRAN 功能中的任何一行代码。这表明 Janus 有能力动态建立新的或者改变现有 O-RAN 服务模型[31, 47],无需经历漫长的标准化过程。例如,我们能够收集下行链路总物理资源块(PRB, Physical Resource Block)使用量 KPI[13],方法是利用表 1 的 Janus FAPI 钩子,并获取示例 1 中提到的nfapi_dl_config_request_pdu_t
结构,其中包含每个用户调度决策分配的 PRB 数量。该结构被保存在 Janus map 中,取 0.5ms 内的平均值,并发送给 Janus 数据采集器。
低成本实时推理。 为了证明 Janus 如何克服第 2.2 节所述的数据量限制,我们开发了一个小程序集,通过将正在运行的 5G RU 转化为频谱传感器来检测外部无线干扰。小程序集由两个通过 map 进行协调的小程序组成,第一个检测没有 5G 传输时的空闲时段(挂在表 1 的 FAPI 钩子上),第二个在空闲时段对干扰进行采样(挂在表 1 的 IQ 采样钩子上)。Janus 的灵活性使我们能够根据需要指定参数调整干扰检测器的保真度和开销,比如基于哪些天线端口和 symbol 收集 IQ 样本,以什么样的频率(例如每个接收槽或每 10ms)和粒度(例如原始 IQ 样本与每个资源块平均能量比值)收集。正如第 7.2 节所示,在 vRAN 中内联执行这一推理,而不是将原始 IQ 样本导出到 RIC,能够将遥测带宽减少 40 倍。类似方法可以用来实现其他需要不同格式的无线电信道遥测数据的推理用例(例如,无线定位[62,86]和信道估算[37,38])。
此外,许多 RAN 控制回路需要实时参数预测,预测的及时性对网络性能有直接影响[28, 29, 40, 46, 66, 83, 89, 98, 100]。由于 O-RAN RIC 的延迟,xApp 只能提供 10 几毫秒前的预测,不支持实时推理[23, 77]。通过 Janus 的小程序,可以在预测延迟低于 10 毫秒的情况下在 vRAN 功能内进行推理,我们通过构建表 3 第 12-14 行所列的推理模型来验证这一功能。第一个是用于预测用户信号质量的 ARIMA 时间序列模型,采用的方法类似于[33]。第二个是用于预测信号处理任务运行时间的量化决策树,采用的是[45]的方法。更复杂的模型,如[98]中的随机森林,在 Janus C 代码中更难实现,会导致大量字节码指令(> 100K),使验证过程变得缓慢(> 20 分钟)。为了克服这一问题,我们以 map(JANUS_MAP_TYPE_ML_MODEL
)的形式增强了对随机森林的支持,预先训练好的序列化随机森林模型可以被传递给 Janus,并在小程序加载过程中链接到这个 map,Janus 解析序列化的模型并验证,然后在内存中重建这一模型,小程序可以通过辅助函数janus_model_predict()
访问该模型进行推理。这种方法类似于 Tensorflow 等框架为微控制器提供的序列化功能[91],并可以扩展到其他常用的 ML 模型(例如 LSTM)。关于这个过程的更多细节,请见附录 A.4。
实时控制。 许多企业应用需要网络切片以保证更严格的服务 QoS[39, 44]。现有 O-RAN 服务模型允许预先定义一组切片调度策略[34, 60, 85],以 10 秒粒度控制调度。使用 Janus,我们实现了任意调度策略的实时分片,粒度为 0.5-10ms,这在今天的 RIC 模型中是不可能的。依靠表 2 中 Janus 的片间无线资源调度钩子,由 MAC 调度器在每个调度周期开始时调用,该钩子接收基站的调度状态作为上下文(设备数量、所属切片、缓冲区大小、信号质量等)。通过这个钩子,我们用 Janus 小程序实现了三个网络切片调度器,如表 3 所列(第 15-17 行),这三个调度器都使用表 2 的allocate_slice_rbs()
执行器将调度决策应用于基站。
5. 系统设计挑战
5.1. 运行时控制
现有 eBPF 验证器可以确认内存安全和终止条件,如果小程序不能有效终止(例如由于无限循环),将被拒绝执行。然而,正如第 2.3 节所提到的,当前无法对小程序在最差情况下的执行时间给予足够严格的保证。
5.1.1. 运行时间估算的挑战
估算最差情况下的执行时间的简单方法是分析小程序可以执行的最大 eBPF 指令数,这一信息可以通过对小程序最长路径的静态分析推断出来,并同时考虑有界循环的情况。然而,将指令数转化为预期运行时间是非常困难的,因为这取决于许多因素,包括 CPU 时钟、内存和缓存、eBPF 指令向 JIT 代码的转化等[36, 94]。另一个挑战是辅助函数,其执行时间在不同函数和不同参数值之间可能有很大差异。
为了说明这些挑战,请看示例 2 和 3 中的小程序。两者都执行了一个 1000 次的迭代循环,其中第一个循环调用了一个辅助函数。验证器显示,与示例 2 相比,示例 3 的小程序需要多执行 64 条指令。然而,对于参考硬件 Xeon Platinum 8168 CPU @ 2.7GHz,我们看到示例 2 小程序运行开销更大(运行时间为 4.3𝜇s,示例 3 为 2.4𝜇s)。这是因为与小程序的乘法和加法指令相比,辅助函数产生了更大的开销,表明每个小程序的最大指令数并不能很好的表示最大运行时间。
1 for (int k = 0; k < 1000; k ++) {2 index = 0;3 c = janus_map_lookup_elem (& counter , & index ) ;4 s . counter = k + 10;5 }
复制代码
示例 2: 带辅助函数的循环(平均运行时间 4.3𝜇s)
1 for ( volatile int k = 0; k < 1000; k ++) {2 counter += i ;3 counter2 = counter * i ;4 s . counter += counter2 ;5 i ++;6 }
复制代码
示例 3: 没有辅助函数的循环(平均运行时间 2.4𝜇s)
5.1.2. 通过字节码动态增强运行时
图 3: 代码修补简化流程。
为了应对这些挑战,Janus 在 eBPF 字节码中注入指令,在运行时测量小程序的执行时间,并在超过阈值时进行抢占。如图 3 所示,一个辅助函数(mark_init_time()
)被添加到小程序头部,将当前时间(基于 Inte rdtsc
指令)存储在线程局部变量中。补丁程序在选定位置引入检查点,调用辅助函数(runtime_limit_exceeded()
,图 3 第 18 行),检查自mark_init_time()
以来小程序的运行时间,并将其与阈值进行比较。如果超过了阈值,小程序将被迫退出并返回错误(图 3 第 19-21 行)。运行时阈值是在加载小程序集时为每个小程序指定的(见附录 A.1 和示例 6)。最后,由于注入了指令,补丁程序需要更新所有跳转偏移量。这种方法使我们能够验证修补后的字节码的安全性,确保补丁程序所做的任何修改都不会影响小程序集的安全性。
时间检查是作为辅助函数实现的,调用了 Intel rdtsc
指令,而这在 eBPF 指令集中没有对应的指令。辅助函数的调用使 eBPF 寄存器 r0-r5 失效,这些寄存器可能保存了小程序正常执行流程的状态。为了确保可验证性,Janus 存储并重新加载这些寄存器的值(图 3 第 17 和 22 行),这要求小程序在其内存栈中至少有 48 字节的存储空间(eBPF 函数有 512 字节的栈)。我们认为这是一个合理的要求,小程序总是可以使用 map 来存储更多状态。
注入点。 打补丁的关键问题是在哪里注入检查点。我们想限制两个连续检查点之间的最大指令数𝑁,以减少运行时抖动的影响(如示例 2-3 所示)。然而,每个检查点都会产生开销(调用辅助函数runtime_limit_exceeded()
,保存和恢复寄存器,等等)。所有这些加起来,对于参考硬件 Xeon Platinum 8168 CPU @ 2.7GHz 来说,每个检查点开销超过 24ns。为了保持低开销,Janus 使用算法 1 分散检查点。Janus 一开始在调用被供应商标记为长效辅助函数之后添加检查点,因为如果这些函数被经常调用,可能会不安全(第 2 行)。接下来使用验证器的静态分析来穷举(从最短到最长)从小程序的第一条指令到最后一条指令的所有简单路径,以及所有循环。对于每个路径,Janus 每隔𝑁条指令添加一个检查点(第 11-14 行)。该算法考虑到了在遍历其他路径时已经添加的检查点。如果发现一个检查点,则以现有检查点为起点,对重置指令计数(第 8-9 行)。即使距离小于𝑁,每个周期也至少增加一个检查点(第 17-19 行),从而保证每隔𝑁条指令总有一个检查点。
算法 1: 检查点注入决策
数据: 𝑁 > 0,小程序指令列表𝐹,其中调用了长期辅助函数,从第一条指令到最后一条指令的所有简单小程序路径的有序列表𝑃和循环(长度增加)结果: 检查点指令位置列表𝐶 1 𝐶 ← { }; 2 foreach instruction 𝑓 in 𝐹 do 𝐶 ← 𝐶 + 𝑓; 3 foreach 𝑝 in 𝑃 do 4 ins ← 0; 5 fins ← first instruction of 𝑝; 6 foreach instruction 𝑖 in 𝑝 do 7 ins ← ins + 1; 8 if 𝑖 has already checkpoint then 9 ins ← 0;10 else11 if ins = 𝑁 then12 𝐶 ← 𝐶 + 𝑖;13 ins ← 0;14 end15 end16 end17 if 𝑝 is cycle and no checkpoint was added then18 𝐶 ← 𝐶 + fins;19 end20 end
复制代码
为了进行更精细的控制,Janus 允许供应商使用宏(RUNTIME_LIMIT_EXCEEDED
)在长效辅助函数中设置检查点,执行与图 3 补丁类似的操作。例如,在第 4 节中讨论的随机森林模型的情况下,我们在模型的每个估算器(树)的推断后添加这样的检查。
即使是几个检查点,开销也会变得很大(例如在有紧密循环的小程序中)。为了进一步减少开销,补丁代码以抽样的频率进行检查(在𝑀个检查点中抽一个)。补丁程序在 eBPF 堆栈中增加了一个 32 位计数器,只有当这个计数器达到某个值时才进行检查(图 3 的第 15 行,右图),否则执行流程将跳回原始字节码指令,从而保证每𝑀×𝑁条指令至少进行一次检查。
抢占式控制循环。 每个控制钩子必须提供默认控制决策,如果加载的控制小程序被抢占而没有做出决策,被抢占的小程序会返回CONTROL_FAILED
错误码,并执行 RAN 供应商提供的默认动作(如示例 4 所示)。例如,表 2 的切片间无线资源调度钩子的供应商默认动作可能是在切片间平均分配无线资源块。同样,对于链路适配钩子,可以设置一个强大的调制和编码方案,无论接入 UE 的信号条件如何,都能提供低错误率。最后,Janus 提供了一个辅助函数check_preemption()
,允许小程序检查在上一次运行中是否被抢占,如果小程序保存了错误状态,可以进行重新设置。
1 decision = hook_custom_janus_control_operation ( ctx ) ;2 if ( decision != CONTROL_SUCCESS )3 decision = call_default_control_operation ( ctx ) ;
复制代码
示例 4: Janus 自定义控制钩子伪代码
5.2. 灵活可验证的输出格式
Janus 可以定义任意输出格式,在运行时与小程序一起加载,通过网络将输出数据传输到集中式控制器(更多细节见附录 A.2)。Janus 基于 protobufs 实现数据序列化。但是支持任意格式不利于安全性。具体来说,Janus 必须处理两个挑战。
1 message Example {2 repeated int32 element = 1;3 }4 Example.element max_count:16
复制代码
示例 5: 带有可变大小字段(最多 16 个元素)的输出格式定义示例。
首先是确保小程序不能生成任意大的可序列化消息,因为这可能不利于内存安全。Protobuf 消息由开发者定义,可以包含可变大小的字段(如重复字段或字符串)。Janus 验证器在编译时不知道消息的实际大小,因此不能静态验证。为了克服这个问题,Janus 要求在消息规范中对所有具有可变大小字段的 protobuf 定义设定一个上限,如示例 5 所示。Janus 根据消息的最大大小为小程序 C 代码分配内存,并向验证器报告(在示例中,16 × sizeof(int32) + sizeof(int16) = 66B
),代价是内存消耗略有增加(在 vRAN 系统中不是瓶颈)。
其次是确保格式不正确的消息不会导致内存问题。考虑这种情况: 程序员分配了一个 30B 的内存块,作为Example
的一个实例,设置 element 数量为 16,并调用 protobuf 编码器。由于内存块对于 16 个 element 来说太小了,编码器将试图从分配的内存块之外的地方进行编码,这可能会导致 segfault。为了确保内存访问安全,我们修改了验证器,以断言传递给 protobuf 编码器的内存总是等于可能的最大大小(在本例中为 66B)。像上面这样的问题仍然会造成逻辑错误,会发送乱码,但不违反安全性。