RFC-0079:检测调试日志数据丢失

RFC-0079:检测调试日志数据丢失
状态已接受
领域
  • 内核
  • 诊断
说明

修订了用于检测调试日志数据丢失的机制。

问题
Gerrit 更改
  • 487876
作者
审核人
提交日期(年-月-日)2021-02-17
审核日期(年-月-日)2021-03-25

总结

本文档提出了对用于检测内核调试日志对象中丢弃的消息的机制的更新。

背景和动力

内核调试日志子系统是一个简单的日志记录工具,可让用户模式程序读取和写入日志消息。从逻辑上来说,该系统提供单个 FIFO 日志缓冲区,该缓冲区可以被多个写入器或读取器写入或读取。

调试日志可能是有损的。假设读取器可以跟上写入速率,那么所有读取器都将按写入顺序看到所有写入者写入的所有消息。但是,如果读取器运行缓慢且跟不上,它将丢失消息。

消息可以由用户模式程序通过 zx_debuglog_write 写入调试日志,或者由内核通过 printf 写入。消息可以通过 zx_debuglog_read 读取。此外,内核还有一个称为 debuglog_dumper 的专用线程,用于从调试日志读取消息并将其写入调试串行端口。

调试日志缓冲区具有固定容量。达到该容量上限时,最晚写入的消息可能会被丢弃,为新消息腾出空间。所有尚未“了解”的读者将永远不会看到丢失的消息。

知道日志是完整的后,就可以推断缺少某些事件的原因,因此检测日志消息是日志系统的一项重要功能。Debuglog 读取器需要能够检测日志消息何时被丢弃。

目前,debuglog 提供了一种机制,供读取器检测日志数据何时被丢弃以及丢弃了多少字节,即 zx_log_record_tuint32_t rolled_out 字段。

使用 zx_debuglog_read 读取 zx_log_record_t 后,rolled_out 字段将包含自该读取器上次成功读取以来从调试日志中丢弃的日志消息字节数。该值包括被丢弃的日志标头的字节以及被丢弃的日志正文的字节。

rolled_out 机制的实现方法是让每个读取器在调试日志缓冲区中保留一个指针,该指针指向尚未读取的下一条消息。调试日志会维护一个写入指针,该指针指向将写入下一条消息的位置。如果读取器注意到写入指针已通过其读取指针,就会知道自己错过了一条或多条消息。通过减去指针值,读取器可以确定丢失的日志数据(包括标头)字节数。

rolled_out 机制目前尚未使用。

提案

此 RFC 建议...

  1. 将面向字节的数据丢失检测替换为面向记录的检测。

    为了更加贴近调试日志读取器(尤其是 debuglog_dumper)的预期,现有的面向字节的 rolled_out 机制将被替换为每条记录的序列号,该序列号可用于检测数据丢失(序列中的缺口)。

    debuglog_dumper 必须将它读取的每条消息写入调试串行端口。由于串行端口可能速度不是特别快,debuglog_dumper 通常无法跟上调试日志的速度,从而导致消息丢失。发生这种情况时,我们希望向串行端口输出一条消息,指明已发生数据丢失以及丢失了多少消息,类似于 Linux 的 printk 输出。

  2. 使用 64 位值消除了未检测到数据丢失的可能性。

    由于 rolled_out 字段的大小为 32 位,并且统计字节数而不是记录,因此如果在两次 zx_debuglog_read 调用之间写入 4GB 日志数据,则该值可能会溢出,从而导致未检测到的数据丢失。这在实践中不太可能发生,但最好完全消除这种可能性。如果要将 32 位字节序列字段替换为每条记录 32 位的序列字段,则创建溢出所需的数据量会增至约 128 GB。通过使用 64 位序列字段,即使在记录速率非常高时,我们也可以完全忽略溢出的可能性。

  3. 实现未来的实现优化。

    未来可能会发生一些优化,将依赖于允许多个调试日志读取器“共享”单个 zx_log_record_t

    如果没有慢速读取器,所有 debuglog 读取器都能以相同的顺序看到完全相同的日志数据。除 rolled_out 外,zx_log_record_t 的所有字段在记录写入调试日志时都是固定的。rolled_out 的不同之处在于,它是在从调试日志中读取记录时针对每个读取器计算的。如果我们有其他方法来检测数据丢失,而不使用可能因读取器而异的字段,就可以在未来实现一些潜在的优化,即在所有读取器之间共享一条记录。

设计

调试日志会在写入调试日志的每条记录中分配一个从 0 开始的 64 位序列号。

每条记录的序列号将正好比前一条记录大 1。移除 zx_log_record_trolled_out 字段,并将其替换为记录的序列号 uint64_t sequence

然后,zx_debuglog_read 的调用方就可以检测序列中的空缺并计算丢弃的消息数量。

实现

不在树之外使用 zx_debuglog_readzx_log_record_t。虽然不需要完整的 Fuchsia Large Scale Changes (LSC) 流程,但我们会提交供参考的 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 调用方以使用它。

相关步骤如下:

  1. 将 64 位序列号添加到调试日志消息的不公开内部表示形式 (dlog_header_t)。

  2. 更改调试日志使用方以使用 zx_log_record_t(或等效语言),而不是硬编码字段偏移量。具体而言,您需要创建一个 Rust 等效类型,并更新 Rust 代码来使用该类型。

  3. zx_debuglog_readvoid* 参数更改为 zx_log_record_t*

  4. zx_log_record_trolled_out 字段更改为一个填充零的 unused 字段。

  5. zx_log_record_tunused 字段替换为 uint64_t sequence,并确保不会创建隐式结构体填充。不管使用哪种语言,对所有 zx_log_record_t 等效类型都执行相同的操作。

  6. 更新了 zx_debuglog_read 文档,以说明调用方可以如何使用新的序列字段来检测数据丢失。

第 1 步、第 2 步和第 3 步将分别获得自己的 CL。第 4 步、第 5 步和第 6 步将发生在单个 CL 中。

性能

管理序列计数器的运行时费用

Debuglog 操作在持有锁时执行,因此我们可以使用常规的 uint64_t 值生成序列。预计不会产生可衡量的性能影响。

每个记录序列值的大小影响

内核的专用记录实现 zx_log_record_tdlog_header_t 的大小都会增加。zx_log_record_t 的 32 位 rolled_out 字段将替换为 64 位记录序列字段,净增益为 4 个字节。

dlog_header_t 大小的变化更有趣,因为这是日志记录存储在 FIFO 中的原生形式。dlog_header_t 的大小为 32 个字节,并且没有 rolled_out 字段,因此其净增益将是完整的 8 个字节。FIFO 空间在调试日志对象中是有限的,因此将每条日志记录增加 8 个字节会减少可存储在 FIFO 中的记录数上限,还可以减少消息大小上限(从 224 个字节减少到 216 个字节)。

该 FIFO 可存储 128KB 的标头和消息。消息采样表明平均大小约为 100 字节。假设该平均大小,具有 32 字节标头,FIFO 可以存储大约 971 条消息。根据每条记录的序列号,该序列号会减少到约 917。

安全注意事项

此方案不会改变系统的安全性。Debuglog 读取器是特权组件。如果没有数据丢失,调试日志读取器可能已经以完美的准确性合成日志记录序列。

隐私注意事项

不会对隐私保护产生影响。

测试

内核中单元测试将验证底层调试日志实现,调试日志核心测试将验证在系统调用层可观察的行为。

文档

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 会报告丢弃/抑制的消息数,而不是丢弃/抑制的日志数据的字节数。