填写您的日志

2021-03-20 09:54:22

九个月前,我们开始了持久性(上磁盘)表示的格式迁移,例如BackTrace服务器中的符号值堆栈等变量长度字符串。戴上一致的架空字节填充(COB),一个自同步代码的变体,用于元数据(也是可变长度)。这选择让我们改善我们的软件对当地文件中的数据托密的恢复力,然后并行数据保湿,如此启动时间为10倍......没有从旧到当前磁盘数据格式的任何淬火。

在这篇文章中,我将解释为什么我认为,对于二进制日志的代表性offirst度假村(编辑,恢复,重播,可以由程序消耗的Orthing),应该由此迁移并通过先前的经验来支持COBS风格的编码。我还将描述我们为服务器软件实施的特定算法(可用的MIT许可证下提供)。

此编码为框架提供低空间开销,用于框架,快速编码和更快地解码,恢复数据损坏以及随机访问的受限形式。也许为自己的数据使用它是有意义的!

代码是自同步,当它始终可以明确地检测有效的码字(记录)在符号流(字节)流中开始。这是stronger属性比霍夫曼代码等前缀代码,只有在有效的代码字结束时才会被禁止。例如,UTF-8Encoding是自同步的,因为初始字节和延续字节在其高位中不同。这就是为什么在拖尾UTF-8流时可以解码为字节代码点。

UTF-8代码专为小型整数(Unicode Code Points)设计,并且可以增加二进制数据的大小。其他编码是任意字节的MoreAPPR;例如,一致的开销字节填充(COBS),一个字节流的自同步代码,提供一个字节的最差谱率加上0.4%的空间爆炸。

自同步对于二进制日志很重要,因为它以简单且强大的方式(相对于运行时和空间开销)framerecords以简单且强大的方式......我们想要简单的androbustness,因为在出​​现问题时,日志最有用。

当然,存储层应该检测和纠正错误,但有时会通过,特别是对于永不动手,没有人完全控制部署。发生这种情况时,优雅的部分失败是优选的,例如,丢失文件中的所有本信息,因为其页面之一去了天空中的伟大比特。

一个简单的解决方案是通过多个文件orblobs传播数据。但是,在检查中保留Datafraging和File Metadata开销之间存在权衡,并最大限度地减少轻微损坏的括号半径。我们的服务器必须能够逃避孤立的节点,因此我们不能依赖设计选项可用的ToReplated Systems ......加上错误往往会在副本中相关,因此有些可以在深度防御,甚至被批判的存储。

当每个记录都以自同步的Codelike CobsBefore转换为磁盘,我们可以解码WERETECTLY受损坏影响的所有记录,完全类似地解码最有效的UTF-8字节的流。任何形式的腐败只会Mantus丢失字节损坏的记录,最多,两个Recordsthat立即在损坏的字节范围之前或遵循损坏的字节范围。 ThisGuerAntee封闭覆盖数据(例如,当网络交换机翻转时,或使用零填充页面默默地错误地默认读取Syscall),以及在LogFiles中间删除的字节或垃圾。

编码不存储冗余信息:复制或替代是存储层的责任。它相反,验收始终最大限度地减少腐败的影响,只有丢失录音题与腐败相邻或直接遭受损坏。

编码用于日志记录的COBS实现了通过使用保留字节(例如,0)和重新编码的记录来实现的,以避免分隔符字节。因此,读取器可以假设潜在的记录在日志文件的第一个和LastBytes上启动和结束,否则查找分隔字节以确定所有潜在记录的位置。这些记录可能无效:通过损坏可以介绍或删除ASeparator字节,并且正确框架的记录可能会损坏。发生了Whenthat,读者可以简单地扫描下一个分隔符字节和特性以验证新的潜在记录。解码器的状态重置每个分隔符字节,因此,只要Decoder查找有效分隔字节,就会“忘记”任何损坏。

在写入方面,编码逻辑简单(C代码几十二线),并使用可预测的空间量,从适用于微控制器的算法预期的空间。

实际编写的数据也很容易:在POSIX文件系统上,我们可以确保每个记录都被分隔(例如,与分隔符字节的前缀),并发出常规O_APPEND WRITE(2)。传输器写入甚至可以在不复制INUSEREPACE的情况下插入分隔符。实际上,我们的代码可能不那么稳定,它运行的系统和硬件运行,所以我们确保自己一旦将其发出到内核,并让Fsyncshappen在计时器上。

当写出错误时,我们可以盲目地(可能一次或两次)Tryagain:编码与输出文件的状态无关。当Swrite被切割时,我们仍然可以发出相同的1个写入呼叫,而不尝试“修复”短写:编码和理论侧逻辑已经防止这种腐败。

如果多个线程或进程写入相同的日志文件,那么?当我们使用O_Append打开时,操作系统可以处理其余部分。这不会消失,但至少我们没有在附加到附加到同一文件所需的内容中添加瓶颈内部空间。缓冲也是琐碎的:编码与目标文件的状态无关,所以我们可以始终连接缓冲records和单个Syscall将结果写入。

这种简单性也与IO_URIP等高吞吐量I / O基元效果很好,并且支持附加的BLOB存储:独立工作人员可以同时向上队列盲目的附加请求Andretry On Failure。没有应用程序级相互排除或回滚。

我们的日志编码将从错误的字节中恢复,只要读者念珠花纹和拒绝整体记录无效记录;处理逻辑也可以处理重复的有效记录。这些是可靠的日志消费者的表阶旗。

在我们的可变长度元数据用例中,每个记录都介绍了脱节的呼叫堆栈,我们重新创建内存数据结构ByReplaying的元数据记录日志,一个用于每个单级调用堆栈。水合作阶段处理无效的记录(不重新创建)任何具有损坏元数据的呼叫堆栈,而只有那些调用堆栈。这绝对是对Pruvious的情况的改进,其中大小标题中的损坏会阻止解码文件的其余部分,从而让我们忘记损坏后存储在文件偏移中的所有呼叫堆栈。

当然,应该避免丢失数据,所以我们要定期小心fsync并推荐合理的存储配置。但是,人们只能使数据丢失不太可能,而不是不可能的(如果只有胖指法),特别是在成本是一个因素时。通过COBSEncoding,我们可以优雅地恢复,并自动从Anyunfortunate数据损坏事件中恢复。

处理常规Cadence的日志尾部通常很有用。例如,我曾经维护了一个定期淘汰小时以更新近似视图的系统。人们可以支持使用Length页脚的使用情况。 COBS框架让我们改为从任意字节位置扫描有效记录,并正常读取剩下的数据。

当日志足够大时,我们希望并行处理它们。 TheStandard解决方案是分离日志流,不幸的是耦合并行和存储策略,并增加了对其的复杂性。

COBS构筑让我们独立于作家的平行读者。缺点是,读侧代码和I / O模式现在是MoreComplex,但是,所有其他东西都是平等的,这是一个折衷的人,特别是我们的服务器在独立使用中运行,并将其数据存储在文件中,其中读取是细长的延迟相对较低的。

并行COB读卡器为独立工人任意(例如,封闭尺寸块)分区数据文件。一名工人将在其分配的块中启动的第一个有效记录,并处理截至其块中的记录。在启动Bytemeans上过滤,即工作人员可能会读取其块的逻辑末尾,何时完全解码块中的最后一个记录:这就是何时默认将工作者分配给每个记录,包括Recordsthat跨块边界。

随机访问甚至让我们在原始未弯曲的日志上实现一系列二进制或插值研究,我们知道记录是(k-)sortedon搜索键!这使我们可以让我们访问几码堆栈的元数据,而无需解析整个日志。

当代文件系统,如XFS(甚至ext4)支持大型SparseFiles。例如,稀疏文件可以达到\(2 ^ {63} - 1 \)字节onxfs,只有最小的元数据 - 仅限于最小的占用空间:在我们发出实际写入时,QuiceStems文件的磁盘数据仅分配。如今,我们可以在事实之后缩小文件,并将非零数据的范围转换为zerous填充的“孔”中,以释放存储,而不会使用文件偏移(甚至原子折叠旧数据)。

文件系统只能以粗略粒度执行这些操作,但这不是我们读者的问题:他们必须仅记得TOSKIP稀疏孔,并且解码环路自然地处理任何留下的ANARBACHEARFALL部分记录。

柴郡和面包师的原始字节填充架构小型机器和慢速运输(业余无线电话线)。这就是为什么它界限为254个字节的作家和9位为读者的9位,并尝试长截线空间开销,超出其最坏情况的0.4%。

该算法也是合理的。编码器缓冲数据,直到IteAccuters为保留的0字节(分隔符字节),或者有254字节的缓冲数据。每当编码器停止缓冲时,它就会屏蔽其内容由其第一个字节描述的块。如果作者停止缓冲,因为它找到了一个保留的字节,在写入和清除缓冲之前,可以将一个字节与buffer_size + 1进行一字音。否则,它输出255(超过缓冲区大小),然后是缓冲区的内容。

在解码器侧,我们知道每个BlockDescesces的第一个字节它的大小和解码值(255表示254字节的LittlealData,任何其他值都是文字字节Tocopy的数量,其次是保留的0字节)。我们表示一个隐式分隔符的记录结束:当我们耗尽数据来解码时,Weshould刚刚解码了不是数据的额外分隔符字节。

划定“0”字节在Afile的开头和结尾处是可选的,并且每个Blen大小前缀是一个字节,其中值为\([1,255] \)。 [1,254] \中的值\(\ mathtt {blen} \)表示块\(\ mathtt {blen} - 1 \)文字字节,后跟一个umlicic0字节。如果我们改用\(\ mathtt {blen} = 255 \),则Wehave of \(254 \)字节,没有任何隐式字节。 ReadersOnly需要记住剩下多少个字节,直到Churrent块的末尾(计数器的八位),以及在解码下一个块(一个二进制标志)之前是否应该插入隐式0字节。

我们为我们在后退时写的软件有不同的目标。对于我们的记录用例,浏览完全构造的记录,我们希望每条记录发出单词Syscall,定期FSYNC。 2缓冲被烘焙,所以没有点钟确保我们可以为小写缓冲区工作。我们也不关心太空飞行员(最坏情况绑定已经很好),以及Wedo关于编码和解码速度。

它使用双字节“保留序列”,仔细选择在我们的数据中呈现出来

...但每个后续块的极限都大大,65008字节,用于渐近空间开销0.0031%。

该混合方案改善了编码和解码速度比较了与COOBS相比,甚至略微改善了渐近空间开销。在低端,最坏情况的开销只会比传统的COB略差略差:我们需要三个附加字节,包括Framing Stepator,记录252字节或更少的记录,有五个字节的记录为253-64260字节。

在过去,我已经看到了“Word”填充模式,通过缩放COB循环来减少COBS编解码器的运行时间开销,以一次在两个或四个字节上工作。然而,字节搜索是琐碎的向上的矢量,并且无法保证将其对齐到字边界(例如,POSIX允许任意数量的字节短写入)。

我们的混合词填充在任意字节偏移中查找保留的双字节分隔符。我们必须一次仍然必须一个概念性地处理,但与单个字节的一对BytesInstead界定使得可以更轻松地制作分隔符,不太可能出现在我们的数据中。

柴郡和面包师做相反,并使用频繁的字节(0)在常用案例中味道在空间开销。我们关心许多Moreabout编码和解码速度,所以一个不太可能的分隔符对我们来说是莫雷斯语。我们选择了0xFE 0xFD,因为不管Endianness的小整数(无符号,两个补充,varint,varint,solour double float)序列都令人厌恶,也不是有效的UTF-8字符串。

任何带有0xFE 0xFD(254 253)的正整数在其字节中必须高跟位于\(2 ^ {16} \)或更多。如果整数代替负少indian两者的补充,则0xFE 0xFd等于-514作为allittle-endian int16_t,而-259在大endian中(不如伟大,但没有,但没有)。当然,序列可以出现在两个相邻的UINT8_TS中,但否则,对于0xFE或0xFD,才能出现大32位或64位整数的最重要的字节(与0xFF不同,这可能是符号扩展,例如-1) 。

任何(u)leb varint thincludes 0xfe 0xfd必须跨越至少3个字节(即,15位),因为这两个字节都具有最高有效位设置为1.even的负面排卵必须至少为否定为\( - 2 ^ {14} = -16384 \)。

对于浮点类型,我们可以观察到Thesignificand中的0xFE 0xFd将代表很少或大型人中的可怕分数,因此只能遇到IEEE-754表示的IEEE-754(大约\(\ PM 2 ^ {15})。如果我们在标志和指数字段中出现0xFd或0xFe,则会出现0xFD或0xFE,我们发现非常积极或非常负指数(指数是缺陷的,而不是补充)。一个半详尽的搜索证明,其中包含陈列的最小整数单曲浮点数是32511.0,在小endian中,130554.0;在整数的双浮子中,我们发现122852.0和126928.0 .0。

最后,序列不是有效的UTF-8,因为0xFE和0xFdhave它们的顶部位设置(表示多字节代码点),但是仍然存在像延续字节:两种情况下的两个最高有效位是0b11,而UTF -8延续必须有0b10。

一致的开销字节填充重写保留0个字节,远离记录的开头的字节数,直到thext 0,并将该计数存储在块大小标题中,然后在保留的字节中,然后重置计数器,然后执行stamething记录的剩余记录。一个完整的记录存储了编码块的序列,其中没有一个包括储留字节0。每个块头部跨越一个字节,并且必须自己是0,因此字节计数在254处升高,并递增(例如,标题值为1表示0的计数;当标题中的电流等于最大值时,解码器知道编码器在不找到0的情况下停止短路。

使用我们的双字节保留序列,我们可以编码RADIX 253(0xFD)中的每个拦截的大小;给定每个块的双字节标题,Sizescan高达\(253 ^ 2 - 1 = 64008 \)。这是一个合理的颗粒状,有意造粒备忘录。此基数转换替换了OFF-ONE WEIRICNITYIN COB:原始算法的那部分仅将值\([0,254] \)编码为一个字节,同时避免保留的字节0。

对于小型记录有点荒谬,对于小型记录(OuStend大约为30-50字节)。因此,我们特别编码了第一块,在sizePrefix中用单个字节\([0,252] \)。由于保留的序列0xFE 0xFD不太可能出现Inour数据,因此短记录的编码通常会逐渐消失以添加UINT8_T长度前缀。

第一个变形是在\([0,252] \)中,并告诉我们初始块中的遵循多少个文字字节。如果初始\(\ mathtt {blen} = 252 \),则立即关闭文字字节后跟下一个块的odecoded内容。否则,我们必须首先将隐式0xFE0xFD序列...添加到每个记录结束时的人为保留序列。

在Little-EndianRadix-253中,每个后续块都有一个双字节大小前缀。换句话说,| Blen_1 | Blen_2 |代表Theblock size \(\ mathtt {blen} \ sb {1} + 253 \ cdot \ mathtt {blen} \ sb {2} \),其中\(\ mathtt {blen} _ {{1,2}} \ [0,252] \)。同样,如果BlockSize是最大可编码大小,\(253 ^ 2 - 1 = 64008 \),Wehave文字数据后跟下一个块;否则,我们将将0xFE 0xFD序列章程致力于输出对下一个块的输出。

对于第一个块,请在前252bytes中查找保留序列。如果我们发现它,它会发出其位置(必须小于251),然后所有数据字节,但不包括保留序列,然后在保留序列后输入定期编码。如果序列不在第一个块中,则发出252,随后是252字节的数据,并在这些字节之后输入定期编码。

对于常规(除了第一个)块,查找预留序列Inthe接下来的64008字节。如果我们发现它,将序列的字节偏移(必须小于64008),然后在Little-Endian RADIX 253中发出,然后是THEDATA,但不包括保留序列,并跳过编码数据其余部分的序列。如果我们没有找到保留序列,则在RADIX 253(0xFC 0xFC)中发出64008,请复制下一个64008bytes的数据,并在不跳过任何内容的情况下对数据进行编码。

请记住,我们在终端上概念填充数据。这意味着我们将始终观察到我们在块边界处完全消耗了该数据。当我们对阻止人为保留序列的块进行编码时,我们停止(和框架与保留序列以限定记录边界)。

写入短记录时,我们已经注意到编码步骤等效应添加一个字节大小前缀。 实际上,我们可以在适当的位置管理和将所有尺寸的所有记录解码到位,并且只需要幻灯片才能幻灯片:只要块短于最大长度(252字节 第一个块,64008用于后续的块),这是因为我们在解码数据中的保留序列。 发生这种情况时,Wecan在编码时用大小标头替换保留序列,并在解码时撤消替换。 我们的代码不实现这些优化,因为编码anddecoding填充字节不是我们使用案例的瓶颈,但它可以知道我们无处可行的是绩效天花板。 填料方案仅提供弹性框架。 那么,但不是eno ......