惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
D
Docker
GbyAI
GbyAI
B
Blog RSS Feed
博客园 - 司徒正美
博客园 - Franky
美团技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
C
Check Point Blog
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
AI
AI
O
OpenAI News
Attack and Defense Labs
Attack and Defense Labs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
S
Secure Thoughts
博客园 - 聂微东
L
LINUX DO - 最新话题
U
Unit 42
SecWiki News
SecWiki News
A
Arctic Wolf
Schneier on Security
Schneier on Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Visual Studio Blog
量子位
The Cloudflare Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
G
Google Developers Blog
T
Threat Research - Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
B
Blog
博客园 - 【当耐特】
C
CERT Recently Published Vulnerability Notes
Scott Helme
Scott Helme
Last Week in AI
Last Week in AI
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
A
About on SuperTechFans
博客园 - 三生石上(FineUI控件)
Latest news
Latest news

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
使用 Rust 实现高性能预写日志(Write Ahead Log)
她和她的猫 · 2024-06-02 · via 文章列表 - 她和她的猫

不知不觉已经三个多月没有更新博客了,这段时间也想写一点东西,但因为各种原因一直没有动笔。或许我的拖延症已经到达晚期了 ¯\(ツ)/¯。最近,我看到一句挺有意思的话:「战胜拖延症的方法:我就是要做一坨🐶💩出来」。话虽糙,但其本质是「完成比完美更重要」。如果一开始就抱着把事情搞砸的心态去做,说不定就成了呢?

当然,这段时间我也没有闲着,大部分时间都在看极客时间的《陈天 · Rust 编程第一课》专栏。在掌握了基础知识之后,我迫不及待地想要用 Rust 写些小工具练练手,由于没有特别好的想法,我决定用 Rust 实现一些「短小精悍」的开源项目,比如 rosedblabs/wal

「短小」指的是代码量方面,这个项目的代码量不多,核心逻辑大约只有几百行,这样一来实现起来不会太困难,比较容易落地。「精悍」则体现在功能方面,用几百行代码就实现了基于磁盘、支持并发读写的高性能预写日志库。所以选择它作为练手项目再合适不过了。

经过与 Rust 编译器的无数次斗争,我终于「磕磕绊绊」地完成了这个项目,并且通过了所有的单元测试。

为什么说是磕磕绊绊呢?因为在编码期间,多次由于代码无法通过编译,我一度想要暂时搁置这个项目。但每次总是在快要放弃的时候,又让我找到了解决方法,颇有一种「山重水复疑无路,柳暗花明又一村」的感觉。

我将项目代码放到了 GitHub:https://github.com/her-cat/wal-rs,如果你觉得不错,可以点个 star 哦~

接下来,我将通过 rosedblabs/wal 项目的 README.md 以及代码来介绍一下这个项目。

什么是 WAL?

WAL 是 Write Ahead Log 的简称,通常叫做预写日志,是一种用于防止内存崩溃、保证数据不丢失的手段。需要注意的是,WAL 是一种方法,而不是具体的实现形式,这意味着它可能会以不同的形式出现在各种项目中。

例如,在 Redis 中,有一种数据持久化方式叫做 AOF(Append Only File)。Redis 会将所有对数据库进行写入操作的命令以追加的方式写入到 AOF 文件中。当 Redis 重启时,它会读取 AOF 文件,通过重放所有写入命令来还原重启前的数据库状态,从而保证数据不会丢失。

RoseDB 是一个基于 Bitcask 的键值存储引擎,Bitcask 的数据文件也是一个仅附加的日志文件(Append Only File),因此,我们同样可以使用 WAL 作为 RoseDB 的底层存储,而 RoseDB 确实是这么做的。

关于 Bitcask,可以参考我之前翻译的文章:了解 Bitcask:基于日志结构的 KV 存储引擎

WAL 的整体设计

图片来源:rosedblabs/wal

从上图可以看出,在一个 WAL 实例中,会有多个用于存储数据的文件(Segment),每个文件包含若干个块(Block),每个块的大小固定为 32KB。每个块中又包含若干个大小可变的 Chunk,每个 Chunk 包含 7 个字节的头部和用户实际存储的数据。Chunk 分为 FULL、FIRST、MIDDLE、LAST 四种类型,这里借鉴了 LevelDB 和 RocksDB 的设计。

关于 Chunk 类型的分配,如果当前块有足够的剩余空间可以容纳写入的数据(即块的剩余空间大于或等于数据的大小),Segment 会直接写入一条包含该数据的 Chunk,Chunk 类型为 FULL,表示该 Chunk 包含的是一条完整的数据。如果当前块无法完全容纳写入的数据(即块的剩余空间小于数据的大小),Segment 会将该数据分成多个 Chunk 进行写入。

  • FIRST:表示该 Chunk 是数据的第一个部分。
  • MIDDLE:表示该 Chunk 是数据的中间部分。
  • LAST:表示该 Chunk 是数据的最后一个部分。

就像 Bitcask 那样,在一个 WAL 实例中,只会有一个活跃的 Segment 用于读写新数据,其它的 Segment 仅用于读取旧数据,接下来我们来看看 WAL 的写入和读取操作。

WAL 的写操作

对于写入操作,WAL 提供了 WAL::writeWAL::write_all 两个方法,前者用于写入单条数据,后者用于批量写入多条数据。

以下是 WAL::write 方法的主要流程:

  • 获取 WAL 实例的写锁,避免并发修改 WAL 实例。
  • 检查要写入的数据是否大于单个 Segment 的大小。
  • 检查当前活跃的 Segement 的剩余空间是否能够容纳要写入的数据。
    • 如果无法容纳,则调用 WAL::rotate_active_segment,将当前的活跃的 Segment 移动到旧的 Segment 中,并创建一个新的 Segment 作为活跃的 Segment。
  • 调用活跃的 Segment 的 Segment::write 写入数据,并得到该数据的 ChunkPosition
  • 检查是否需要同步数据,如果需要,则调用活跃的 Segment 的 Segment::sync
  • 最后,返回 ChunkPosition

Segment::write 中,首先会从 BUFFER_POOL 中获取一个可用的 Buffer,然后调用 Segment::write_to_buf,将数据以 Chunk 格式进行编码写入到 Buffer 中,最后调用 Segment::write_chunk_buf,将 Buffer 中的数据写入到 Segment 文件中。其中最核心的是 Segment::write_to_buf,它包含了数据填充、数据编码、更新块号及生成 ChunkPosition 等逻辑。

WAL::write_allWAL::write 的流程大致相同,只不过写入数据时调用的是 Segment::write_all。该方法与 Segment::write 唯一不同的是,该方法会循环调用 Segment::write_to_buf,将所有数据以 Chunk 格式进行编码写入到 Buffer 中。

WAL 的读操作

当数据被写入后,我们可以通过 ChunkPosition 调用 WAL::read 读取数据,或者调用 WAL::new_reader 生成 Reader 来遍历所有数据。

以下是 WAL::read 方法的主要流程:

由于我们的数据在写入时,可能被分成了多个 Chunk,所以在 Segment::read 中,需要循环读取所有数据块并汇总:

  • 调用 Segment::load_cache_block,尝试根据块号从缓存中读取相应块数据,如果不存在就从文件读取并缓存。
  • 根据偏移量从块数据中读取 Chunk 的头部和数据部分,并检查校验和是否一致。
  • 将数据部分追加到数据结果中。
  • 如果 Chunk 类型是 FULL 或者 LAST,表示已经读取完毕,需要跳出循环;
  • 否则,当前块号加一并重置当前偏移量,继续读取下一个 Chunk。
  • 最后,返回数据结果。

总结

至此,关于 WAL 项目的介绍已经结束。在文章开头我提到过,这个项目的代码量并不多,因此我非常推荐大家去看看源代码。无论你是想学习 Rust 语言还是 WAL 的设计,看完之后一定会有所收获。

当然,如果你还能找到一些 Bug,那就更好了。 :-P


2024.06.05 更新:

目前,该项目已贡献给 rosedblabs/wal-rs,并由 rosedblabs 社区继续维护。her-cat/wal-rs 将不再更新。