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 旗下的 minitraceminstant 水了一些 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,帮助他将自己的文章补充的更完整,形成了一个正向循环。

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

今天你外卷了吗?