Iteration 8 的主要工作仍然是围绕着 OpenDAL 展开,有三件事情比较有意思。

首先是多读了部分数据导致读取性能下降的 BUG: Improvement: Avoid reading unnecessary data。

Databend 在 benchmark 的过程中发现 opendal 会读取比预想的要更多的数据:

let r = o.reader();
let buf = vec[0;4*1024*1024];
r.read_exact(buf).await;

理论上应该只从 S3 获取 4MB 的数据,但实际上会下载将近 4.5MB 的数据,其中多余的部分数据会在这个请求销毁的时候被一同丢弃掉。Databend 平均每次请求的大小大约为 256KB,opendal 多读取的数据经常会超出一倍左右。

背后的原因是 opendal 并不知道用户会如何使用这个 reader ,所以在实现的时候总是使用当前 reader 的 size 来发送请求,这就使得 reader 会从服务器端获取比用户预期更多的数据。

let op = OpRead {
  path: self.path.to_string(),
  offset: Some(self.current_offset()),
  size: self.current_size(),
};

为了解决这个问题,OpenDAL 引入了 limited_reader proposal,通过更明确的语义避免用户错误地多读数据。

其次是新增加的 ObserveReader 抽象,databend 需要统计每次 read 读取的 size 和花费的时间。size 比较好做,简单的 callback 就能做好,但是花费的时间就稍微麻烦一点,需要能够允许用户在每次读取的前后都能正确的统计时间,还需要能够排除 Poll::Pending 上的开销。

ObserveReader 通过 ReadEvent 来实现这个功能:

/// ReadEvent will emitted by `ObserveReader`.
#[derive(Copy, Clone, Debug)]
pub enum ReadEvent {
    /// `poll_read` has been called.
    Started,
    /// `poll_read` returns `Pending`, we are back to the runtime.
    Pending,
    /// `poll_read` returns `Ready(Ok(n))`, we have read `n` bytes of data.
    Read(usize),
    /// `poll_read` returns `Ready(Err(e))`, we will have an `ErrorKind` here.
    Error(std::io::ErrorKind),
}

impl<R, F> futures::AsyncRead for ObserveReader<R, F>
where
    R: AsyncRead + Send + Unpin,
    F: FnMut(ReadEvent),
{
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut [u8],
    ) -> Poll<std::io::Result<usize>> {
        (self.f)(ReadEvent::Started);

        match Pin::new(&mut self.r).poll_read(cx, buf) {
            Poll::Ready(Ok(n)) => {
                (self.f)(ReadEvent::Read(n));
                Poll::Ready(Ok(n))
            }
            Poll::Ready(Err(e)) => {
                (self.f)(ReadEvent::Error(e.kind()));
                Poll::Ready(Err(e))
            }
            Poll::Pending => {
                (self.f)(ReadEvent::Pending);
                Poll::Pending
            }
        }
    }
}

每当 ObserveReader 出现状态切换的时候,就会回调一下用户传入的 callback 函数。在 PR dal_context: Use ObserveReader to calculate metrics 中,我使用 ObserveReader 为 databend 增加了时间统计的支持。

最后是 S3 匿名访问的问题。由于 aws-sdk 不支持匿名访问的功能,所以被迫自己搞了一些 Hack,通过修改 AWS SDK 的 Middleware,实现了在没有读取到密钥时直接发送未签名的请求。在这个功能的加持下,databend 能够直接从一个公开的 S3 Bucket 中直接加载数据,非常适合用来做 demo。

这个周期推动 OpenDAL 进行了一轮迭代,上线了 main 分支的文档网站 https://opendal.databend.rs/opendal/,方便用户查看还没有正式 release 的 API。花了不少时间补全了所有公开 API 的文档和样例,现在访问 docs.rs 终于不是光秃秃的一片了。

现在 OpenDAL 对外的接口基本上稳定,接下来计划增加服务器端加密的支持,然后把能力扩展到更多的服务,只支持 S3 怎么能叫做 OpenDAL 呢!此外还有个重点话题是可观察性,通过为 OpenDAL 增加完善的 logging,tracing,metrics 支持,用户能够知道 OpenDAL 的内部状态,从而做出更好的决策。

除了 Databend 社区之外,这个周期还给 tikv 旗下的 minitrace 和 minstant 水了一些 PR。

minitrace 是一个超快的 tracing 库,从 benchmark 的结果看能比 tokio-tracing 快十倍,在收集的 span 特别多的时候差距能拉大到 100 倍以上。我帮助 minitrace 修复了 contributing guide 中的 dead link,增加了简单的开发入门指导,此外还在 PR deps: Reduce version requirements 中统一了依赖的版本规则,将 v0.x.y 统一成了 v0.x,放松了一些对版本的要求。

minstant 是 minitrace 的依赖,是 std::time::Instant 的高性能替代,在支持的平台上会使用 CPU 中的 TSC,比 std 中的实现快一倍。我在 PR ci: Say goodbye to travis 中删掉了已经不再工作的 travis CI 的配置(时代的眼泪)。


接下来聊聊外卷的话题。

今年的 1/7 我上线了 Xuanwo's Note,然后从 1/18 开始我以差不多每个工作日一篇的速度分享今天学到的东西,内容囊括了方方面面:从 Rust 相关到 Linux 的小技巧。

最开始的想法是通过这种方式逼迫自己每天去学习,去分享一些新的东西,最大化的利用已经开源出来的 Xuanwo's Note。但是我很快发现我收获的价值要比分享出去的更多:各位推友从各自不同的角度向我回馈了完全超出我知识边界的内容。推友们有些指出了分享内容的不足,有些在内容的基础上进行了进一步的完善。

典型的例子是 2022-04: Iteration 5 汇报 中提到的

最让我开心的是某天关于 Futures 的分享还启发了参考资料中的作者 @JmPotat0,帮助他将自己的文章补充的更完整,形成了一个正向循环。

我越来越相信这种外卷的方式跟内卷是有本质区别的:内卷产生的是无谓的内耗,而外卷却可以创造更多的价值。每个人有自己各自不同的知识来源和工作背景,对同一个技术话题也有自己完全不同的角度。如果大家能够互相学习,共同进步,相信会是一件非常有意思的事情。从我的角度看,这是开源精神在抽象意义上的延伸:自己的知识开源出去,让所有人都可以获取,可以参与改进,反过来改进自己的知识结构与体系,形成一个正向循环。

今天你外卷了吗?