| RFC-0079:检测 debuglog 数据丢失 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 一种用于检测调试日志数据丢失的修订机制。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2021-02-17 |
| 审核日期(年-月-日) | 2021-03-25 |
摘要
本文档提议更新用于从内核的 debuglog 对象检测丢弃消息的机制。
背景和动机
内核调试日志子系统是一种简单的日志记录工具,可让用户模式程序读取和写入日志消息。从逻辑上讲,此系统提供了一个 FIFO 日志缓冲区,可供多个写入者或读取者写入或读取。
调试日志可能会丢失。假设读者可以跟上写入速率,那么所有读者将看到所有写入者写入的所有消息,并且消息的顺序与写入顺序一致。不过,如果读取器速度较慢,无法跟上,则会错过消息。
用户模式程序可以通过 zx_debuglog_write 将消息写入调试日志,内核可以通过 printf 将消息写入调试日志。可以通过 zx_debuglog_read 读取消息。此外,内核还具有一个专门的线程(称为 debuglog_dumper),用于从调试日志读取消息并将其写入调试串行端口。
debuglog 缓冲区具有固定的容量。当达到该容量时,系统可能会舍弃最近写入时间最久的消息,以便为新消息腾出空间。任何尚未“跟上”的读者都永远不会看到丢弃的消息。
知道日志是否完整有助于推断是否存在某些事件,因此检测丢弃的日志消息是日志系统的一项重要功能。Debuglog 读取器需要能够检测到何时丢弃了日志消息。
目前,debuglog 提供了一种机制,供读取者检测何时丢弃了多少字节的日志数据,即 zx_log_record_t 的 uint32_t rolled_out 字段。
使用 zx_debuglog_read 读取 zx_log_record_t 后,rolled_out 字段将包含自相应读取器上次成功读取以来,从调试日志中丢弃的日志消息的字节数。该值包括丢弃的日志标头的字节数和丢弃的日志正文的字节数。
rolled_out 机制的实现方式是让每个读取器维护一个指向 debuglog 缓冲区的指针,该指针指向尚未读取的下一条消息。调试日志会维护一个写入指针,该指针指向下一个消息将写入的位置。如果读取器发现写入指针已超过其读取指针,则表示它错过了一条或多条消息。通过减去指针值,读取器可以确定它错过了多少字节的日志数据(包括标头)。
rolled_out 机制目前尚未使用。
提案
此 RFC 提议...
将面向字节的数据丢失检测替换为面向记录的检测。
为了更贴近 debuglog 读取器(尤其是 debuglog_dumper)的预期,现有的面向字节的
rolled_out机制将被替换为可用于检测数据丢失(序列中的间隙)的每条记录序列号。debuglog_dumper 必须将读取的每条消息写入调试串行端口。由于串行端口可能不是特别快,debuglog_dumper 通常无法跟上调试日志,从而导致消息丢失。发生这种情况时,我们希望在串行端口上打印一条消息,指明发生了数据丢失以及丢失了多少条消息,类似于 Linux 的
printk输出。使用 64 位值消除了未检测到数据丢失的可能性。
由于
rolled_out字段的大小为 32 位,并且统计的是字节数而非记录数,因此如果在两次zx_debuglog_read调用之间写入 4 GB 的日志数据,则可能会导致值溢出,从而导致数据丢失而未被检测到。这种情况在实践中不太可能发生,但最好能完全消除这种可能性。如果我们用 32 位每记录序列字段替换 32 位字节序列字段,则创建溢出所需的数据量将增加到大约 128GB。通过使用 64 位序列字段,即使在非常高的日志记录速率下,我们也可以完全忽略溢出的可能性。启用未来的实现优化。
未来可能会进行一些优化,这些优化需要允许多个 debuglog 读取器“共享”单个
zx_log_record_t。如果没有缓慢的读取器,所有 debuglog 读取器都应看到完全相同的日志数据(按相同的顺序)。除了
rolled_out之外,zx_log_record_t的所有字段在记录写入调试日志时都是固定的。rolled_out的不同之处在于,它是根据每个读取器在从调试日志中读取记录时计算的。如果我们有其他方法来检测数据丢失,而无需使用可能因读取器而异的字段,我们就可以启用一些潜在的未来优化,从而在所有读取器之间共享单个记录。
设计
debuglog 在将每条记录写入 debuglog 时,会为其分配一个从 0 开始的 64 位序列号。
每个记录的序列号都比前一个记录的序列号大 1。移除 zx_log_record_t 的 rolled_out 字段,并将其替换为记录的序列号 uint64_t sequence。
zx_debuglog_read 的调用者随后可以检测序列中的缺口,并计算丢弃了多少消息。
实现
zx_debuglog_read 和 zx_log_record_t 不在树外使用。虽然不需要完整的 Fuchsia 大规模变更 (LSC) 流程,但会提交一个 FYI LSC bug,并且实现将分阶段完成。
rolled_out 字段未被使用,但包含它的结构体 zx_log_record_t 在 fuchsia.git 中的多个位置被使用。需要注意不要破坏现有代码。zx_log_record_t 不在树外使用。
zx_debuglog_read 的系统调用定义和文档实际上并未指定它返回 zx_log_record_t。而是指定 void* 和 size_t,调用者必须知道如何将 zx_log_record_t 投放到结果之上或“叠加”到结果之上。强制转换为 zx_log_record_t* 容易出错,因此 void* 系统调用参数将更改为 zx_log_record_t*,并且调用方将更新。
目前,没有与 zx_log_record_t 等效的 Rust 类型,并且 Rust 调用者使用硬编码的偏移量来访问字段,因此更改其字段的大小或偏移量可能会悄无声息地破坏这些调用者。在实现过程中,我们将创建 zx_log_record_t 的 Rust 等效项,并更新 Rust 调用方以使用该等效项。
相关步骤如下:
向调试日志消息 (
dlog_header_t) 的私有内部表示形式添加 64 位序列号。将 debuglog 使用者更改为使用
zx_log_record_t(或等效语言),而不是硬编码的字段偏移量。具体而言,请创建等效的 Rust 类型并更新 Rust 代码以使用该类型。将
zx_debuglog_read的void*参数更改为zx_log_record_t*。将
zx_log_record_t的rolled_out字段更改为零填充的unused字段。将
zx_log_record_t的unused字段替换为uint64_t sequence,并确保不会创建隐式结构体填充。对所有zx_log_record_t等效类型执行相同操作,无论语言是什么。更新了
zx_debuglog_read文档,以说明调用方如何使用新的序列字段来检测数据丢失。
步骤 1、2 和 3 将分别获得自己的 CL。步骤 4、5 和 6 将在单个 CL 中完成。
性能
管理序列计数器的运行时费用
调试日志操作是在持有锁的情况下执行的,因此我们可以使用常规的 uint64_t 值来生成序列。预计不会对性能产生可衡量的影响。
每个记录序列值的大小影响
zx_log_record_t 和 dlog_header_t(内核的私有记录实现)的大小都会增加。zx_log_record_t 的 32 位 rolled_out 字段将被替换为 64 位记录序列字段,从而净增 4 个字节。
dlog_header_t 大小变化更有趣,因为它是日志记录存储在 FIFO 中的原生形式。dlog_header_t 的大小为 32 字节,没有 rolled_out 字段,因此其净增益为完整的 8 字节。debuglog 对象中的 FIFO 空间有限,因此将每个日志记录增加 8 个字节会减少可存储在 FIFO 中的记录数量上限,还会减少消息大小上限(从 224 字节减少到 216 字节)。
FIFO 可以存储 128 KB 的标头和消息。对消息进行抽样后发现,平均大小约为 100 字节。假设平均大小为 16 字节,标头为 32 字节,则 FIFO 可存储约 971 条消息。如果使用每条记录的序列号,则该数字会减少到大约 917。
安全注意事项
该提案不会改变系统的安全性。debuglog 读取器是特权组件。如果没有数据丢失,debuglog 读取器已经可以完美准确地合成日志记录序列。
隐私注意事项
无隐私保护方面的影响。
测试
内核内单元测试将验证底层 debuglog 实现,而 debuglog 核心测试将验证 syscall 层的可观测行为。
文档
zx_log_record_t 的文档将会更新。
缺点、替代方案和未知因素
不进行任何操作
由于 rolled_out 尚未投入使用,因此目前只需投入相对较少的工程工作量即可实现该提案。一旦下游代码使用 rolled_out,实现此提案或类似提案的成本就会更高。
通过将 UINT32_MAX 的语义记录为“UINT32_MAX 或更多”,可以一定程度上缓解 32 位环绕问题。或者,我们可以将 rolled_out 的类型更改为 uint64_t。
记录序列和字节序列
如果空间足够,我们可以在每个 zx_log_record_t 中同时放置记录序列值和字节序列值。然后,debuglog 读取器可以测量记录丢失数或字节丢失数。不过,这会进一步增加 dlog_header_t 的大小,而且我们不清楚是否有丢弃字节的使用情形。
在先技术和参考资料
Linux 的 printk 会报告丢弃/抑制的消息数量,而不是丢弃/抑制的日志数据字节数。