从零到一的系统工具开发复盘:需求、设计、实现、发布四个阶段

📅 2026/7/25 6:06:58
从零到一的系统工具开发复盘:需求、设计、实现、发布四个阶段
从零到一的系统工具开发复盘需求、设计、实现、发布四个阶段一、需求阶段从我想要到别人也要最开始的想法很简单我是一个后端开发每天要 grep 十几 GB 的日志文件查错误信息。传统grep是单线程 IO 单线程匹配读 10GB 文件需要 90 秒。我想要的只是一个快 10 倍的 grep。但我没有马上开始写代码。我先做了两件事确认这不是一个已被解决的需求调研了ripgrep、ag、ugrep发现它们都非常优秀但在大文件流式搜索和结构化日志过滤上都不太理想。和 3 个同事聊了他们的痛点发现不只是我一个人受罪——运维同事每天要跨 10 台机器搜日志测试团队在 CI 日志里找失败原因也要花大量时间。于是需求变成了二、设计阶段架构决策的三个关键时刻决策 1内存映射还是流式读取大文件读取有两种方案mmap内存映射让操作系统把文件映射到虚拟内存按需加载页。随机访问快但 10GB 文件可能触发 OOM。流式读取读一段处理一段内存可控但需要自己管理缓冲区。我选择了一个折中方案——分段 mmap// // 分段内存映射兼顾速度与内存安全 // use memmap2::Mmap; use std::fs::File; /// 大文件分段读取器 /// 将文件分成多个固定大小的段用 mmap 逐段处理 pub struct ChunkedFileReader { file: File, file_size: u64, /// 每段的大小默认 64MB chunk_size: u64, /// 段间重叠大小防止关键词被截断在边界 overlap: usize, } impl ChunkedFileReader { pub fn new(path: str, chunk_size_mb: u64) - std::io::ResultSelf { let file File::open(path)?; let file_size file.metadata()?.len(); Ok(Self { file, file_size, chunk_size: chunk_size_mb * 1024 * 1024, overlap: 4096, // 4KB 重叠区确保不会截断行 }) } /// 迭代器每次返回一个可独立处理的数据段 pub fn chunks(self) - impl IteratorItem std::io::ResultChunk _ { let mut offset 0u64; let chunk_size self.chunk_size; let file_size self.file_size; std::iter::from_fn(move || { if offset file_size { return None; } // 计算实际要读的段大小含重叠区 let read_size chunk_size.min(file_size - offset) as usize 4096; // 创建该段的 mmap 视图 let mmap unsafe { Mmap::map(File::open().unwrap()) // 简化示例 }; offset chunk_size; Some(Ok(Chunk { offset, data: vec![] })) }) } } /// 文件的一个数据段 pub struct Chunk { pub offset: u64, pub data: Vecu8, }决策 2用线程池还是 Tokio并行处理有两个方案rayon线程池简单粗暴适合纯 CPU 任务。Tokio适合 IO 密集型任务。这个工具的核心瓶颈在 IO读文件和 CPU正则匹配所以我用了混合方案IO 层用 Tokio处理多文件并发读取、SSH 连接。计算层用 rayon正则匹配、JSON 解析。三、实现阶段最磨人的两周难点 1正则匹配的边界处理分段读取后一个关键词可能刚好跨越两个段的边界。处理方案段之间留 4KB 的重叠区重叠区的结果去重。/// 正则匹配器处理段边界去重 pub struct DedupMatcher { /// 已匹配行的哈希集合用于去重 seen: HashSetu64, /// 上一段的最后几行用于检测边界匹配 tail_buffer: VecString, } impl DedupMatcher { /// 对一段数据进行匹配自动处理边界去重 pub fn match_chunk(mut self, chunk: [u8], regex: Regex) - VecMatch { let text String::from_utf8_lossy(chunk); let lines: Vecstr text.lines().collect(); // 把上一段的尾部和当前段的头部拼接检查边界处的完整行 let boundary format!({}{}, self.tail_buffer.join(\n), lines.first().unwrap_or()); let mut results vec![]; for line in lines { let hash calculate_hash(line); if regex.is_match(line) !self.seen.contains(hash) { self.seen.insert(hash); results.push(Match { /* ... */ }); } } // 保存当前段的尾部供下一段使用 self.tail_buffer lines.iter().rev().take(3).map(|s| s.to_string()).collect(); results } }难点 2压缩文件的流式处理.gz文件不能直接 mmap需要用flate2流式解压。但解压是 CPU 密集型操作要和正则匹配做流水线化// // 流水线解压 → 正则匹配 → 输出 // use std::io::Read; use flate2::read::GzDecoder; pub fn search_gzip(path: str, pattern: str) - anyhow::ResultVecString { let file File::open(path)?; // GzDecoder 包装了文件读取器自动流式解压 let decoder GzDecoder::new(file); let reader std::io::BufReader::new(decoder); let regex Regex::new(pattern)?; let mut results vec![]; for line in reader.lines() { let line line?; if regex.is_match(line) { results.push(line); } } Ok(results) }四、发布阶段从能跑到有人用工具开发完成后我做的不是扔到 GitHub 上等 star而是写了一个 60 秒的演示 GIF展示 10GB 文件搜索从 90 秒降到 3.2 秒。写了一份 benchmark README和 ripgrep、ag 做了系统对比公平起见只比较纯文本搜索场景。在公司内部先推广让运维团队的 3 个同事试用了一周根据反馈改了 6 个 issues。打包发布用 GitHub Actions 自动编译 Linux/macOS/Windows 的二进制包。# # GitHub Actions 多平台编译配置 # name: Release on: push: tags: [v*] jobs: build: strategy: matrix: target: - x86_64-unknown-linux-gnu - x86_64-apple-darwin - aarch64-apple-darwin - x86_64-pc-windows-msvc runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - run: rustup target add ${{ matrix.target }} - run: cargo build --release --target ${{ matrix.target }} - uses: softprops/action-gh-releasev1 with: files: target/${{ matrix.target }}/release/logsearch*第一次发版就被坑了忘了加--target构建macOS 用户下载后发现跑不了——因为我用 M1 Pro 构建的二进制是 arm64Intel Mac 用户根本打不开。后来老老实实配了三个 target 的 matrix build再也没收到打不开的 issue。cross 编译是工具发布的第一道坎别跳过。五、总结从零到一开发一个系统工具的四个阶段我学到的最重要的东西需求阶段先问这个轮子真的没人造过吗我的工具虽然和 ripgrep 有重叠但在大文件场景有差异化优势。架构决策要做实验不要凭感觉。我测试了 mmap vs 流式、rayon vs Tokio 之后才做的选择。边界处理才是实现的难点。核心逻辑可能 100 行搞定但段边界去重、压缩文件处理、错误恢复等占了 70% 的代码量。发布不等于完成。工具的价值在于有人用、有人反馈、持续迭代。v0.1.0 发了之后公司内部已经有 30 人在用它。v0.2.0 正在开发中计划加入正则表达式的可视化调试功能。