专栏名称: 程序人生
十年漫漫程序人生,打过各种杂,也做过让我骄傲的软件;管理过数十人的团队,还带领一班兄弟姐妹创过业,目前在硅谷一家创业公司担任 VP。关注程序人生,了解程序猿,学做程序猿,做好程序猿,让我们的程序人生精彩满满。
目录
相关文章推荐
极客之家  ·  22k star,微软硬核开源,让 ... ·  昨天  
稀土掘金技术社区  ·  掘金 AI 编程社区- 人人都是 AI 编程家竞赛 ·  4 天前  
稀土掘金技术社区  ·  URL地址末尾加不加”/“有什么区别 ·  3 天前  
伯乐在线  ·  为什么 DeepSeek ... ·  昨天  
伯乐在线  ·  为什么 DeepSeek ... ·  昨天  
51好读  ›  专栏  ›  程序人生

从微秒到纳秒:关于性能的奇妙旅程

程序人生  · 公众号  · 程序员  · 2021-03-08 08:04

正文

请到「今天看啥」查看全文


有了这两个基本的设计, append(event) 操作就显而易见:

  1. 找到日志中 pos 指向的位置,写入 varint 长度和数据本身。写完后更新 pos。

  1. 找到索引中 total 指向的位置,记录写入的是哪个日志文件,以及写之前的 pos。写完后更新 total。

get(idx) 操作也很直观:

  1. 找到 idx 对应的 IndexEntry( INDEX_HEADER + idx * ENTRY_SIZE ),取出 file 和 pos

  1. 找到对应的日志文件从 pos 里用 varint decode 出长度,然后读出整个 event

iterator 实现类似,这里就不赘述。

由于文件 mmap 后,拿到的就是一个容量很大的 &[u8] ,所以各种操作爽到飞起,全程零拷贝,比如需要修改某个 IndexEntry ,可以直接 data.align_to_mut:: () ,然后就往里灌数据。效率高到不能更高了。

然而,这里有一个待解决的问题:什么时候把 mmap 内存中修改的数据刷到磁盘上?间隔太短会影响性能,间隔太长万一进程崩溃,丢失的数据就太多(linux 4 以后支持 MAP_SYNC,理论上可以在崩溃后依然把内存内容写入文件)。不管怎么说,我们需要一定的刷新策略。我实现了一个简单的刷新策略:在配置文件中配置 flush size,比如 1024,那么每写入 1024 项,就会刷新一下索引文件和当前日志文件。

这会带来一个新的问题:每次刷新会急剧拉低整体的性能。

这是因为,对 mmap 的无脑刷新会导致 mmap 的所有内存页面被刷新到硬盘。我本以为 mmap 会更聪明一些,看看哪些页面是 dirty 再真正去刷新,但实际不是。由于我们写入的文件都是 append only 的,除了文件头,刷新过的部分就不会修改,因此就无需重新刷新。所以,我们只需要额外记录一下上次刷新到哪个位置,这样,刷新的时候只需要刷文件头(因为一直在修改),以及上次刷新的位置到当前写入的位置即可。做了这个调整后,写入性能急剧提升。我使用 criterion.rs [5] 做了这么几组 benchmark(所有刷新策略都是写入 1024 个事件就刷新一下):

  • 在 index 中查找 1024 次数据:和优化无关,所以没有变化,1024 次查询 6.6us 左右(单次查询 6ns 左右)

  • 在 index 中写入 1024 个 Index Entry:时间从 2.6ms 直线下降到 200us(单次写入小于 200ns)

  • 在 log 中写入 122 字节大小的事件:时间从 10.5us 直线下降到 350ns

  • 在 log 中写入 16373 字节大小的事件:时间从 107us 直线下降到 19us所有 benchmark 均使用 criterion.rs 完成(warm 3s,benchmark 5s)。

随后,我又做了一些其它的优化和功能上的增减(比如代码上对 Writer / Reader 做了分离,日志支持了 varint,以及把索引和日志结合起来提供完整方案的 logger),并没有明显变化:

在真实的使用场景中,添加一条事件,会写入日志和索引,所以 benchmark 把索引和日志结合起来提供完整方案的 logger 更有意义。根据上图,Logger 一次 append 1k 左右的数据,大概需要 1.59us,这意味着单核每秒钟可以写入 630k 条日志,或者 630MBps(我的 mbp 2019 SSD 写入速度在 2.6GBps 左右),而 append 16k 左右的数据,需要 18.3us,大概每秒 55k 条日志,或者约 900MBps。

这个设计存在一个架构上的问题 — 索引文件会随着日志数量的增加而不断增大。当 repo 中有 1M 条事件时,索引占用的内存将达到 8M,加上每个 repo 当前正在写入的日志(假设每 4M 我们 rotate 一次日志文件,内存中每个 repo 就会有 4M 日志 mmap 的内存),这样一共是 12M。乍一看这似乎没什么,占用内存很小啊,但如果同一台机器上要处理 1000 个活跃 repo 的数据(每个 repo 平均有 1M 条事件),内存的消耗就在 12G。如何在保持相同的读写性能的情况下,降低内存的消耗呢?







请到「今天看啥」查看全文