OpenHarmony Hilog 架构趣读
最近看到一篇讨论 OpenHarmony Hilog 日志子系统的设计的论文,遂进行了一番阅读。该论文发表在软件学报上。
摘要
- 分析当今主流日志系统的技术架构和优缺点;
- 基于
OpenHarmony
操作系统的异构设备互联特性,设计HiLog
日志系统模型规范; - 设计并实现第 1 个面向
OpenHarmony
的日志系统HiLog
, 并贡献到OpenHarmony
主线; - 对
HiLog
日志系统的关键指标进行测试和对比试验;
实现的 HiLog
具有以下特征:
- 基础性能:日志写入阶段吞吐量分别为 1 500 KB/s 和 700 KB/s,吞吐量相对 Android Log 提升 114%;
- 日志持久化:压缩率 3.5%,丢包率 0.6%;
- 数据安全、流量控制等等新型实用能力;
背景概述
地位:在计算机系统中,日志作为一种基于时间序列的数据,记录了在操作系统中发生的事件或其他软件运行的事件。
作用:
- 实用价值:系统开发和运维人员需要通过日志对程序中存在的问题进行定位和分析,提高工作效率;
- 商业价值:日志记录了大量用户行为习惯信息,这些信息通过大数据分析可用于了解用户需求,作为改进产品或孵化新的商业项目的依据;
目前产业界的日志系统:Android Log、FTrace、NanoLog、Log4j2 等等;
OpenHarmony
日志系统需要具备的功能:生成、过滤、记录和消息分析的能力。
- 多进程日志读写:
OpenHarmony
是支持多进程并发的操作系统,其日志系统需要具备从多进程收集日志的能力; - 实时日志读写:作为操作系统的高效调试辅助工具,日志系统需要具备事件发生-日志输出的实时响应能力;
- 多内核适配:
OpenHarmony
是多内核的操作系统,其日志系统需要具备多种内核适配能力;
经过分析,目前产业界的日志系统均不适合 OpenHarmony
:
Log4j2
单进程日志架构;- 借助 CAS 实现缓冲区加解锁,降低日志读写接口的延时;
- 缓存行填充解决伪共享问题,隔离更新操作,进一步提升运行效率;
- CAS 方法的 CPU 开销较大(也就是适用于单进程的日志,不适用多进程并发的状况),且存在日志缓冲区修改的 ABA 问题 (ABA problem);
NanoLog
虽然日志写入效率很高(大量数据操作以二进制形式完成),但是:- 其读取需要复杂的后处理机制(反序列化、格式化、排序等);
- 采用空间换时间的策略,内存消耗大;
因此,不能满足 OS 调试所需实时读日志需求;
FTrace
日志系统仅适用于内核日志读写,使用就和 Linux 强耦合,不适用于多内核的OpenHarmony
;- 每个 CPU 上维护一个缓冲区,因此读写时延低(与前述日志系统“为每个操作系统维护唯一缓冲区”的设计理念不同);
- Page 结构组织数据,单 Page 上记录一个时间戳,Page 内日志记录相对时间差,节约记录时间所需的存储空间;
Android Log(5.0 后)满足内核解耦、多进程、实时读写的需求,如下图:
- 日志写:IPC 采用原生 Socket +
/proc/kmsg
内核日志; - 日志缓存:
logd
用户态 list buffer; - 日志读:
logcat
使用 Socket 读;
但是有 4 个关键问题:
- 吞吐量不足:负载超过吞吐量将会导致严重的日志丢包问题;
- 缺乏资源分配机制:没有对日志资源的使用进行合理分配或约束,写日志进程间可能出现资源竞争;
- 缺乏数据安全能力:未提供相应的敏感数据保护功能, 任何读权限日志用户均可阅读全部日志信息;
- 面向轻量设备的兼容性差:没有特别面向资源受限设备进行兼容设计。Android Log 在用户态维护独立缓冲区(list buffer),保存包括 Linux 内核日志在内的所有日志数据, 因此需要消耗大量的内存资源;
- 日志写:IPC 采用原生 Socket +
Fuchsia OS 中的日志系统:Socket 通信(类似 Android Log),不过使用链表作为缓冲区(有效利用碎片化的内存空间)。但是存在内存拷贝次数多、用户态内存频繁分配释放的问题;
上面的案例中,只有 Android Log 最接近要求。但它的问题也亟需优化。
HiLog 日志系统模型规范
基于以上分析,本文将要设计面向 OpenHarmony 操作系统的高性能日志系统 HiLog。首先,为了明确日志系统的研发目标与技术特点,文章为 HiLog 设计了相应的模型规范。
虽然 HiLog 与 Android 日志系统有相同的基础架构,但提出了更多的场景要求,以及原则:
性能原则:应当针对高吞吐量需求进行设计,从软件层面解决吞吐量瓶颈问题(以及引发的丢包问题);
资源分配原则:从操作系统层面看,日志系统作为操作系统的信息记录者,不应抢占过多的系统资源。应当在设计时考虑资源分配问题;
- 一方面在操作系统层面合理分配日志系统和其他程序占用的资源;
- 另一方面是在日志系统层面合理分配各个程序占用的日志资源;
设备兼容性原则:OpenHarmony 操作系统即是一种面向全设备的操作系统,因此需要考虑资源受限的轻量化设备, 如蓝牙耳机、键盘、智能音箱、传感器等。
需要注意:减小 CPU、内存、存储空间占用。
数据安全原则:常见的隐私保护方法有匿名化、同态加密、差分隐私等等,由于它们需要基于静态的、结构相同的数据集合计算数据之间的相关性,因此难以适用:
- 基于时间序列意味着日志数据是随时间快速更新的, 每次更新都需要重新计算数据之间的关系, 计算开销是昂贵的;
- 长文本缺乏字段概念, 日志语句的长短、句式各不相同 (结构不同), 难以基于规则分辨需要保护的内容;
因此 HiLog 应当具备一定的日志数据安全能力, 但是同时需要保证轻量化。
HiLog 系统设计实现
1. 日志类型
OpenHarmony 操作系统由下至上分为内核层、系统层和应用层:
- 内核层(对应内核开发者):可由面向标准系统的 Linux 内核或面向轻量系统的 LiteOS 内核构成;
- 系统层(对应系统开发者):主要由 OpenHarmony 操作系统的各个子系统构成;
- 应用层(对应应用开发者):由运行在 OpenHarmony 上的系统应用以及第三方应用构成;
不同开发者关心的信息是不同的. 因此为了方便开发者区分不同层级产生的日志, HiLog 将日志分为内核日志、系统日志和应用日志 3 类, 并实现日志的分类管理。
2. 日志级别
为了方便开发者和运维人员快速分辨系统状态的严重程度, 日志应当基于记录事件的重要程度划分级别。
标准需要:
- 日志的级别数目不应过多或过少, 防止检索困难或分类标准不明;
- 每个日志级别应当有清晰的使用标准, 开发者在开发时不可混用;
- 写入时, 每条日志都应当分配到一个日志级别;
- 在输出时, 每个级别的日志都需要采用不同的字体或者颜色来区分;
因此 HiLog 分为:
3. 日志数据结构
按位紧密存储(packed),节省空间可以减少 IPC 开销和存储开销, 提高日志系统的日志吞吐量。
4. 日志功能
日志写入:包括日志生成、日志排序、日志暂存。
- 在开发时 HiLog 的使用者通过引入 libhilog 的头文件, 使用 libhilog 提供的写日志接口编写程序, 在程序运行时 libhilog 即可生成日志;
- libhilog 在生成日志过程中还提供数据保护、进程流控等辅助能力;
- 在标准 HiLog 中, hilogd 收集来自各个 libhilog 的日志信息, 按时间进行排序, 并进入缓冲区暂存. 在轻量 HiLog 中, 日志缓冲区是 LiteOS 的
kernel_log_buffer
, 相应的日志排序和暂存能力由kernel_log_buffer
实现;
日志输出:包括日志打印、日志持久化。
- 读取暂存的日志写入到标准输出 (
stdout
), 并且支持通过辅助信息等特征进行筛选; - 将暂存的日志写入文件, 并进一步地提供压缩功能;
可以通过 hilogtool 命令行工具执行日志打印、持久化等输出工作;
- 读取暂存的日志写入到标准输出 (
日志系统控制:包括数据安全、进程流控、业务流控、缓冲区以及持久化的配置;
- 例如, 当操作系统内存空间紧张, 可以缩小日志缓冲区空间, 为其他程序让出更多内存;又例如, 当操作系统 CPU 负载较高, 可以降低流量控制阈值, 减少 HiLog 日志处理消耗的 CPU 资源;
5. 架构 & 模块设计
- 标准 Hilog(L2-L5):维护守护进程 hilogd 实现高性能的日志缓冲区管理;
- 轻量 Hilog(L1):直接将日志写入内核的缓冲区中;
其中:
libhilog
:提供头文件 & 动态链接库。一方面提供静态写日志接口, 另一方面负责运行时日志生成。附加:- 写日志接口的敏感数据标识, 实现数据安全;
- 基于进程的日志流控机制, 实现对所有进程日志写入资源的合理分配;
轻量 Hilog:添加敏感数据标识和流控后, 将日志直接写入内核缓冲区;
标准 Hilog:直接发送至
hilogd
模块;hilogtool
:提供读日志能力。一方面提供与操作系统 Shell 交互的能力, 另一方面负责执行读日志任务。- 开发者通过 Shell 命令控制日志打印或日志持久化任务;
- 根据平台种类,从不同位置读取日志;
hilogd
:面向 L2–L5 平台设计的高性能日志缓冲区 (hilog_buffer
) 及其管理模块;- 与系统的其他模块是解耦的(IPC 交互);
- 提供日志监听、排序和存储的功能, 其运行时具备系统守护进程的特性;
图中“内核缓冲区”含义不同:
- 轻量 Hilog:指 LiteOS 内核的内核日志缓冲区, 负责暂存全量的日志信息;
- 标准 Hilog:指 Linux 的内核日志缓冲区, hilogd 将会读取其中的日志信息到
hilog_buffer
, 保证hilog_buffer
中拥有系统的全量日志信息;
6. HiLog 日志系统 IPC
socket_input
服务端:采用 I/O Multiplxing,而不是多线程策略。因为下面的原因导致单独线程处理会浪费资源:- 日志的写入存在并发特征;
- 每个进程在每个时间段产生的日志数量是不定的, 且每一条日志的长度 (字节数) 也是不等的, 因此日志的写入数据量存在时间分布不均匀特征;
socket_input
客户端:采用非阻塞 IO 模型 (non-blocking input/output),异步传输;由于服务端 I/O Multiplxing 不能保证及时处理,因此同步方式会使进程阻塞。socket_output
客户端/服务端均采用阻塞 IO (blocking input/output) 模型构建;- 读日志事件的数据量较大且需要确保数据到达的先后顺序;
- 从需求分析不会同时存在太多读日志进程, 因此阻塞 IO 不会给系统带来过大的负担;
因此:
hilogtool
, 维护各自的socket_output
客户端向hilogd
发送读日志请求;hilogd
对于每一个客户端创建一个线程操作socket_output
服务端;
而对于轻量 HiLog 而言,IPC 机制直接采用 ioctl
;
6. HiLog 日志数据安全
为了平衡安全性和性能开销, 在设计 HiLog 的数据安全能力时重点考虑了变量的安全问题。仅基于静态的源码分析难以捕捉是否有敏感数据会被日志系统记录, 因此变量是敏感数据泄露的重要风险因素。
开发者指定变量的敏感标识,HiLog 通过识别这些标识来提供数据安全能力。
敏感数据标识分为 2 种, 分别是公开标识 {public}
和隐藏标识 {private}
,例如 "%{public}s"
,"%{private}s"
;
若修饰 {private}
, 在开启数据安全能力的情况下, libhilog 会以字符串 "<private>"
替换原参数后, 再将对应日志发送到 hilog_buffer
;
7. HiLog 日志流量控制
作为实现系统资源合理分配的手段, HiLog 提供日志流量控制 (简称流控) 机制, 避免部分进程日志流量过大造成的系统负载过高和日志丢包问题。
流量控制原理:
- 设置流量阈值 $q$,每个时间片段 Δt 内统计日志流量, 当某个时间片段内的日志流量超出阈值 q 时, 按照默认配额或者进程白名单设置的配额进行控制, 对超出配额的日志进行抛弃;
- 这种控制方法会在 进程端(
libhilog
)和业务端(hilogd
)同时开展,可以平衡 IPC 资源的使用并降低性能开销; - 除了从进程端进行流控,HiLog 可以跨进程针对同一业务流控。OH 使用
Domain
(领域标识)将进程归类,具备相同 Domain 的进程被归纳为同一业务,然后在业务粒度上进行流控。
注:轻量 HiLog 不存在 hilogd
,不存在业务流控,只有进程流控。实际上本身轻量级设备无法运行大量进程,因此这么做本身没有问题。
8. HiLog 日志缓冲区管理
对于标准 HiLog 而言,使用如图双循环链表作为缓冲区 hilog_buffer
的数据结构:
- 高效利用碎片化的内存空间;
- 有效降低日志的排序、插入、读取等操作时指针需要跳转的链表节点数目;
其特性如下:
时间戳有序(为了给开发和维护人员提供符合逻辑的信息):
hilog_buffer
中的数据按照日志数据的时间戳排序。这个特性需要编码时保证,因为 OH 是分时操作系统,随机的延时会导致日志产生时间顺序与到达缓冲区的时间顺序不一致,进而增大日志理解难度。因此提供两种排序方案:- 先排序:日志在写入日志缓冲区时进行排序;
- 后排序:从缓冲区读取日志进行输出时排序;
由于 HiLog 单生产者、多消费者模型,因此采用 “先排序” 的方案。
原因如下:
- “先排序”方案执行一次排序即可解决顺序问题。并且由于缓冲区内日志有序, 排序是较为简单的, 只需将新到日志的时间戳依次与缓冲区日志的时间戳进行比较 (由新到旧) 然后插入即可;
- “后排序”需要每个消费者都进行排序, 由于缓冲区内日志无序, 消费者需要遍历一次链表才能排序一条日志, 效率较低;
读写指针:
hilog_buffer
包含 3 类读写指针成员:- 写指针 $w$:指向当前最新日志结点;
- 公共读指针 $r$:指向当前最旧的日志节点;
- 读者指针 $r_i$:位于 $w$ 和 $r$ 间若干份,以供不同的日志读需求;
单生产者、多消费者模型:
- 在
hilogd
中只运行一个生产者线程。插入日志时扫描日志应该处于的时间区间并移动 $w$ 插入(可能在 $w$ 当前指向结点前或后);如果新插入的日志记录在 $r$ 的前面,则将 $r$ 移动到该结点。最终 $w$ 需要复位至时间最新的结点; - 每个消费者独占 $r_i$ 指针,起始时将读指针 $r_i$ 指向公共读指针 $r$ 所指节点, 接下来操作 $r_i$ 依次读取后续节点;
- 在
hilog_buffer
线程同步:仅处理生产者和消费者间的并发同步问题就行(读者间不存在同步问题)。- 对 “消费者跳转读指针” 和 “生产者调整链表结构” 这两类过程加锁即可;
考虑两种锁:线程互斥锁(
mutex
)和 CAS(Compare And Swap);mutex: CPU 占用低、不存在 ABA Problem;缺点在于存在 domain switch 开销;
CAS: 无需 domain switch;单需要轮询锁状态, CPU 消耗大, 并且存在 ABA 风险;
综合考虑:
- 资源消耗:在单生产者、多消费者的场景下,多个 client 轮询 CAS 可能对移动终端设备不友好;
- 临界区执行时间较长:使用 mutex 带来的 domain switch 开销不显著。
缓冲区容量可定制:缓冲区容量上限可配置。当 hilog_buffer 容量已达上限时, 插入数据会导致时间戳最早的一部分数据被删除, 以存储最新的数据;总体过程:
- 将公用读指针 $r$ 指向剩余数据(不包括将要被删除的结点)中最旧的节点;
- 遍历检查读者列表中每个读者的读指针是否指向将被删除的节点,如果是,则移至 $r$;
- 最终释放要被删除的结点;
9. HiLog 日志系统持久化 & 压缩
日志持久化:
使用一般的日志轮替机制。指定日志文件的总数和每个日志文件的大小,日志系统的内存部分大小与一个日志文件大小匹配。
当正在写入的文件超过规定阈值后创建新日志文件,当日志文件数量超过规定阈值时删除最旧的日志文件;
日志压缩:
提供两种不同的日志压缩方法, 一种是日志流压缩(从日志缓冲区读取日志, 接下来将日志作为比特流输入压缩算法接口。使用 zlib 流压缩算法库),另一种是日志文件压缩(主要面向小流量的写日志场景,先创建临时日志文件,后续压缩并删除)。
当日志流量较小时, 如果采用流压缩方法, 压缩算法的输出数据量达到单文件大小阈值需要很长的时间, 期间一旦出现系统崩溃或断电问题, 就会导致内存中的日志数据丢失。
HiLog 日志系统实验分析
基本性能、流控性能、持久化性能、设备兼容性分析、数据安全能力分析。
总结
完成了前述的几项基本要求、遵循了前述的设计原则;
同时也反映出 HiLog
的一些问题与改进空间:
业界对于日志系统的数据安全的研究较少, HiLog 的轻量化数据安全能力是对于日志数据安全问题的初步探索;
OpenHarmony 作为分布式操作系统, 原生支持分布式能力。
然而目前 HiLog 尚不具备从多设备统一收集日志并进行管理的能力. 这种缺陷对于分布式能力的开发和调试造成了一定的不便, 具备优化的空间;
构造分布式日志系统有两个重要的问题需要解决, 其一是设备间高速、高稳定的连接问题, 其二是多设备的时钟同步问题。
对于第 1 个问题, 可以等待 OpenHarmony 的软总线 (SoftBus) 技术成熟后,利用 SoftBus 作为稳定高速的日志传输的通道;
对于第 2 个问题, 可以考虑基于精确时间协议 (precision time protocol, PTP) 实现无线局域网内的多设备时钟同步;