0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

踩坑rust的partial copy导致metrics丢失

jf_wN0SrCdH 来源: RisingWave中文开源社区 2024-01-03 10:02 次阅读

作者:温一鸣RisingWaveLabs 内核开发工程师

在 RisingWave 的存储代码中,我们使用 RAII [1] 的思想来对 LSM iterator 的 metrics 进行监控,从而避免在代码中忘记上报 metrics 而导致 metrics 丢失。在实现中,我们设计了一个 rust 的 struct MonitoredStateStoreIterStats去收集 LSM iterator 的 metrics,去统计 iterator 中 key 的数量和长度,并为这个 struct 实现了 Drop,在这个 struct 被释放的时候把在本地收集的 metrics 上报 prometheus。通过这种方式,我们不需要在每次 iterator 使用完后都手动上报 metrics,从而避免了由于代码的疏忽导致忘记上报 metrics。

以下是一段简化过的代码。我们通过try_stream[2] 这个宏来封装一个 iterator 的 stream 来收集这个 stream 的 metrics。在返回的 stream 被释放时,stats 随着 stream 被释放,并调用其 drop 方法来上报收集到的 metrics。

pubstructMonitoredStateStoreIter{
inner:S,
stats:MonitoredStateStoreIterStats,
}

structMonitoredStateStoreIterStats{
total_items:usize,
total_size:usize,
storage_metrics:Arc,
}

implMonitoredStateStoreIter{
#[try_stream(ok=StateStoreIterItem,error=StorageError)]
asyncfninto_stream_inner(mutself){
letinner=self.inner;
futures::pin_mut!(inner);
whileletSome((key,value))=inner
.try_next()
.await
.inspect_err(|e|error!("Failedinnext:{:?}",e))?
{
self.stats.total_items+=1;
self.stats.total_size+=key.encoded_len()+value.len();
yield(key,value);
}
}
}

implDropforMonitoredStateStoreIterStats{
fndrop(&mutself){
self.storage_metrics
.iter_item
.observe(self.total_itemsasf64);
self.storage_metrics
.iter_size
.observe(self.total_sizeasf64);
}
}

然而,在使用过程中,我们遇到了上报的 metrics 全部为 0 的问题。

1最小复现

由于使用了try_stream宏来生成 stream,因此我们怀疑在try_stream生成的代码中有 bug 导致 metrics 丢失。于是我们用 cargo-expand [3] 来将查看宏生成的代码。展开后的代码如下:

fninto_stream_inner(
mutself,
)->implStream
>{
::from_generator(staticmove|
mut__task_context:::ResumeTy,
|->::Result<(), StorageError>{
let():()={
letinner=self.inner;
letmutinner=inner;
#[allow(unused_mut)]
letmutinner=unsafe{
::new_unchecked(
&mutinner,
)
};
whileletSome((key,value))
={
letmut__pinned=inner.try_next();
loop{
iflet::Ready(
result,
)=unsafe{
poll(Pin::as_mut(&mut__pinned),get_context(__task_context))
}{
breakresult;
}
__task_context=(yield::Pending);
}
}?
{
self.stats.total_items+=1;
self.stats.total_size+=key.encoded_len()+value.len();
__task_context=(yield::Ready((
key,
value,
)));
}
};
#[allow(unreachable_code)]
{
return::Ok(());
loop{
__task_context=(yield::Pending);
}
}
})
}

可以看到,try_stream宏生成的代码中,包含了一个 rust generator 的闭包。闭包中收集和上报 metrics 的逻辑与原代码基本相同,按照我们对 rust 的理解,仍然不应该会出现 metrics 丢失的问题。因此我们怀疑是 rust 编译器中与 generator 相关的逻辑存在问题。在 rust playground 上,我们尝试了以下代码来对问题进行复现。

structStat{
count:usize,
vec:Vec,
}

implDropforStat{
fndrop(&mutself){
println!("count:{}",self.count);
}
}

fnmain(){

letmutstat=Stat{
count:0,
vec:Vec::new(),
};

letmutf=move||{
stat.count+=1;
1
};

println!("num:{}",f());
}

执行以后输出如下。

num:1
count:0

按照预期,输出的 num 和 count 应该都为1,因为在调用闭包 f 时stat.count += 1被调用了,但是以上代码中遇到了和最开始同样的问题。因此以上代码可以作为我们问题的一个最小复现。

2问题分析

对以上代码进行分析的话,我们看到闭包 f 的代码中使用了 move,因此在闭包中使用过的对象的 ownership 应该都会转移到闭包中。而 struct Stats实现了Drop,因此Stats是不可以 partial move 的,其必须作为一个整体被 move 进入闭包。而在闭包中执行了stats.count += 1,因此 stats 中的 count 应该被置为1。但是从程序的输出可以看到在 stats 被 drop 时,stats 的 count 是 0。

我们尝试将闭包 f 改为如下代码,来显式的将 stats 的 ownership 给 move 进闭包里。

letmutf=move||{
letmutstat=stat;
stat.count+=1;
1
};

输出恢复正常。

num:1
count:1

我们再次尝试在闭包 f 中使用 stat 中的另一个字段 vec:

letmutf=move||{
let_=stat.vec.len();
stat.count+=1;
1
};

输出同样恢复正常。

num:1
count:1

可以看到,我们显式地将 stat 整个 move 进闭包,或者在闭包中使用类型为 vec 的字段,都会使得 stat 的ownership 被 move 进闭包。

于是我们推测,尽管 stat 实现了自己的 drop 导致不能被 partial move,但是如果我们在 move 的闭包中只使用了 stat 中实现了 Copy 类型的字段,则这个字段的值会被 Copy 到闭包中,而闭包中对这个字段的修改只会作用于被 Copy 后的值,而原字段并不会改变。

3验证猜想

我们可以通过将以上代码编译成二进制代码后,对其汇编代码进行分析,从而验证我们的猜想。然而,编译后的汇编代码会过于复杂且晦涩难懂,同时编译器对其进行的一些优化也会改变原有的逻辑导致汇编代码难以理解。因此我们打算通过分析在编译过程中产生的 MIR 中间代码来对问题进行分析。在 rust playground 上可以十分方便地生成 MIR 代码。

首先我们对存在问题的最小复现代码生成 MIR,生成后与闭包相关的 MIR 如下。可以看到这个闭包确实只包含了一个类型为 usize 的字段,这个字段的值取的是 stat 中的 count 值。

bb1:{
_1=Stat{count:const0_usize,vec:move_2};
_3={closure@src/main.rs:19:17:19:24}{stat:(_1.0:usize)};
}

而我们对后续测试中有正常输出的代码生成 MIR,生成后与闭包相关的 MIR 如下。可以看到这个闭包将整个 stat 的 ownership 给 move 了进去。

bb1:{
_1=Stat{count:const0_usize,vec:move_2};
_3={closure@src/main.rs:19:17:19:24}{stat:move_1};
}

于是,我们的猜想得到了验证,在我们出现问题的代码中,闭包确实没有捕获 stat 的 ownership。

4后续与总结

我们向 rust 社区反映了这个问题,得到的反馈是,这个是 rust 2021 后实现的一个 feature [4]。在 rust 2021 中,一个使用了 move 的闭包在捕获一个 struct 的时候,会尽可能少地去捕获 struct 中的字段。

如果一个 struct 没有实现 Drop,这意味着他里面的字段可以被分开 move,而闭包只会捕获闭包中用到的字段。

如果某个被闭包使用的字段实现了 Copy,那他闭包并不会捕获这个字段的 ownership,而是将这个字段 copy 一份放在闭包中。

如果一个 struct 实现了 Drop,那他里面的字段只能作为一个整体被捕获。但如果闭包只使用了这个闭包中实现了 Copy 的字段,那这个闭包不会捕获这个 struct,而是将使用到的字段 copy 一份。

我们的代码中,正是因为这个行为,导致我们的代码产生了歧义,而出现了 metrics 的丢失。

针对这个问题,我们认为有两个地方有提升的空间。

首先,try_stream这个宏的封装存在一定的问题。在使用宏来声明代码中,其暴露出来的使用方法是通过调用一个方法来生成 stream,而在调用方法时,如果参数是通过 move ownership 的形式传入的,同时在生成 stream 的代码中我们使用了这个参数,那我们应该认为这个 stream 包含了这个参数的 ownership。然而,由于这个宏在实现的时候使用了闭包,导致这个 stream 并没有包含这个参数的 ownership,从而导致问题。这个是宏封装逻辑时的问题。

其次,rust 在语言设计上,由于引入了这个闭包捕获 ownership 的特殊逻辑,导致会写出有歧义的代码。例如,在上述代码中,很难想象stat.count += 1并没有去修改 stat 中的 count 值。我们也向 rust 社区反映了这个问题 [5]。

最后,回到我们最开始的问题中。要想解决 metrics 丢失的问题,在我们的代码中,我们只需要做以下修改就能让代码正常运行[6]。

#[try_stream(ok=StateStoreIterItem,error=StorageError)]
asyncfninto_stream_inner(mutself){
letinner=self.inner;
...
self.stats.total_items+=1;
self.stats.total_size+=key.encoded_len()+value.len();
}

修改为

#[try_stream(ok=StateStoreIterItem,error=StorageError)]
asyncfninto_stream_inner(self){
letinner=self.inner;
letmutstats=self.stats;
...
stats.total_items+=1;
stats.total_size+=key.encoded_len()+value.len();
}

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内核
    +关注

    关注

    3

    文章

    1309

    浏览量

    39862
  • 代码
    +关注

    关注

    30

    文章

    4556

    浏览量

    66814
  • Rust
    +关注

    关注

    1

    文章

    223

    浏览量

    6387

原文标题:踩坑 rust 的 partial copy 导致 metrics 丢失

文章出处:【微信号:Rust语言中文社区,微信公众号:Rust语言中文社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    使用STM32采集电池电压过的那些

    本文来解析一个盆友在使用STM32采集电池电压过的。以STM32F4 的ADC属于逐次逼近SAR 型ADC为例进行分析,参考STM32F405xxDatasheet,对于如何编写ADC程序就不做描述了。
    发表于 03-01 07:39

    【STM32+机智云】机智云手机APP点灯实验记录 精选资料分享

    【STM32+机智云】机智云手机APP点灯实验记录一、实验背景因为项目开发需要用到云平台,所以开始学习机智云平台,听说机智云比较容易入门,还有手机APP。因此开始了之旅,一切的
    发表于 08-04 08:30

    开发STM32 USB HID过的

    记录一下 开发STM32 USB HID过的一、前言二、代码配置一、前言MCU: STM32F103C8T6CubeMX: STM32CubeMX 5.3.0二、代码配置引脚配置时钟树配置我
    发表于 08-24 07:15

    使用树莓派搭建stm32开发环境过的以及碰到的问题

    使用树莓派搭建stm32开发环境了很多,下面主要是记录一下过的,以及碰到的问题。##开发方式的选择1.使用Eclipse+GDB+OpenOCD+STlink这种方式我发现ec
    发表于 08-24 07:47

    有没有关于STM32入门经验分享

    有没有关于STM32入门经验分享
    发表于 10-13 06:52

    NodeMCU开发板经历分享

    写在前面今天入手了一个NodeMCU的板子,准备学习一下物联网相关的知识。不过由于博主学艺不精,在第一步烧写固件上就了,所以就想着把自己的经历写出来分享给大家,希望能有一些帮助
    发表于 11-01 07:55

    Linux学习过程过的与如何解决

    Linux记录记录Linux学习过程过的与如何解决1解决方法:F10进入BIOS使能
    发表于 11-04 08:44

    移植debian系统过的

    基本的linux系统,板子的交叉编译器是arm-linux-gnueabihf-gcc,这给我带来了不少的麻烦,以至于想重新移植一下debian系统。ok,转入正题,说说这两天我吧。首先...
    发表于 12-14 08:42

    STM32编程常有哪些?

    STM32编程常有哪些?
    发表于 12-17 06:15

    使用MDK5时出现过的一些error过的分享

    使用MDK5时出现过的一些error过的分享
    发表于 12-17 07:49

    记录写SAM4S的bootloader所

    记录写SAM4S的bootloader所
    发表于 01-24 07:16

    总结一下GD32F13x移植过的

    奇奇怪怪的问题,下面总结一下过的。第一次移植GD时,没有完全移植,只是部分外设移植,导致配置混乱。STM和GD在寄存器命名上有区别,部分寄存器GD专用,导致配置困难,所以,最后进行
    发表于 02-11 07:54

    关于RK1808板子调试过程过的记录

    关于RK1808板子调试过程过的记录
    发表于 02-16 06:38

    STM32H7+UCOSIII+LWIP记录相关资料推荐

    STM32H7+UCOSIII+LWIP记录主要功能:单片机作TCP服务器实现PC端多客户端连接单片机,并发传输数据。点1、优先级问题:一个客户端连接就创建一个线程,优先级由高到低递减,即先
    发表于 02-18 06:30

    STM32G070CB cubemx串口调试过哪些

    使用G070CB时写的中断程序是怎样的?STM32G070CB cubemx串口调试过哪些呢?
    发表于 02-18 06:08