Xline 异步运行时IO问题分析

news2024/11/15 11:16:55

Table of Contents

1. Xline运行时性能问题

2. 异步运行时和阻塞操作

3. Runtime调度问题

4. 性能测试

    4.1 测试结果分析

5. 如何正确实现?

6. 何时能够在Runtime上阻塞

7. 总结

在异步运行时上进行编程经常是很困难的,在本篇文章中,我们主要会通过Xline开发中的几个例子,讨论Rust的异步运行时中有关于IO的问题,以及在代码实现中如何正确使用Tokio runtime以实现最佳性能。我会使用Tokio runtime作为本文的示例。本文所有示例都运行在:

AMD EPYC 7543 32-Core ProcessorSamsung 980 pro NVME SSDUbuntu 22.04.3 LTS, GNU/Linux 6.5.0-21-genericrustc 1.79.0 (129f3b996 2024-06-10)tokio 1.38.0

01Xline运行时性能问题

在Xline的性能测试中我遇到了使用异步运行时的一些麻烦,我发现在WAL(Write-Ahead-Log)的实现中,使用异步的文件读写会产生相当高的延迟,由于WAL的写入存在于关键路径上,这是不可以接受的,那么如何进行优化呢?

02异步运行时和阻塞操作

我们首先简单回顾一下Rust中异步运行时的机制。我们知道Rust的async机制是通过协作式调度来实现并发的,Runtime维护了一个线程池(在Tokio中称为worker threads),通常等于主机上CPU的线程数量,对于线程池中的每个线程,runtime会分配一个executor。Runtime通过scheduler将任务(task)分发到每个executor上。

在Rust async的代码中,我们通过在 .await 点处进行切换不同的任务,当调用 .await 时,我们会将控制权交还给scheduler,以便能够运行其他的任务,这样就实现了各个任务之间的协作模式。下面就是一个异步的模式:

#[tokio::main(flavor = "current_thread")]async fn main() {    let task = tokio::spawn(async {        println!("Hello");    });    tokio::time::sleep(Duration::from_secs(1)).await;    println!("World!");    task.await.unwrap();}

我们设置tokio使用 current_thread ,因此现在tokio runtime中只有一个线程。在以上的代码中存在两个task,一个是 main 本身的task,另外一个是通过 tokio::spawn 生成的另一个task,我们尝试在 main 中调用异步的 tokio::time::sleep ,那么我们就会通过这个 .await 挂起当前的task,然后切换到我们spawn的task。这样就不会有时间浪费在等待文件读取上。同时我们也可以给出一个阻塞操作的示例:

```rustuse tokio::time::Duration;
#[tokio::main(flavor = "current_thread")]async fn main() {    let task = tokio::spawn(async {        println!("World!");    });    std::thread::sleep(Duration::from_secs(1));    println!("Hello");    task.await.unwrap();}```

std::thread::sleep 就是一个阻塞操作,和 tokio::time::sleep 不同的是,它会直接进行系统调用,将整个线程置入休眠状态,因此,当我们执行sleep时,它同时也阻止了scheduler切换到其他的task,我们spawn的task无法取得任何进展,因此造成CPU资源的浪费。

从理论上来说,只要我们不使用阻塞的操作,Rust的异步运行时在逻辑上就不会浪费CPU时间,提供了一层零成本的抽象。但是,在实际使用中,用户仍然需要考虑如何才能最高效地使用runtime。

03Runtime调度问题

回到我们之前的WAL的性能问题,我们首先来阅读下面简化过的代码:

#[tonic::async_trait]impl Protocol for Server {    async fn propose(        &self,        request: Request<ProposeRequest>,    ) -> Result<Response<ProposeResponse>, Status> {        let req = request.into_inner();        let (tx, rx) = oneshot::channel();        self.send_to_persistent((req, tx));        Ok(Response::new(rx.recv().await))    }}
impl Server {    async fn persistent_task(&self) {        loop {            let (reqs, txs) = self.recv_batch().await {                self.log_file.write_all(reqs.encode()).await;                self.log_file.fdatasync().await;                for tx in txs {                    tx.send(ProposeResponse {});                }            }        }    }}

这段代码中Xline中的gRPC server会接受Client的Propose请求,对于每个ProposeRequest,Xline必须将其写入WAL文件,并且同步到存储设备上。为了降低写入的延迟,我们在持久化中使用Dispatch的模式,每个请求都会通过channel发送到 persistent_task 。而在 persistent_task 中每次会获取一个batch的请求并写入文件。

可以注意到的是,对于 log_file 的写入和同步,都是使用的Tokio的异步fs实现的,使得在写入的过程中我们可以继续处理其他client传递的command。表面看上去似乎没有发现什么问题,但是在性能测试时,我们发现在高负载下 write_all 与 fdatasync 都经历了相当高的延迟,单次完整的写入甚至需要数毫秒才能完成。

这个例子实际上就是使用异步运行时进行文件操作的一个反模式。当Xline server经历大量负载时,runtime上此时会堆积非常多的任务,当我们的 persistent_task 进行异步写入的过程中yeild到runtime后,它重新被schedule的时间就会显著延长。这时即使内核中写入操作已经完成, persistent_task 仍然无法取得进展。在高负载下,runtime可能优先处理gRPC的请求,而server最先收到的请求反而无法优先完成处理,这个例子实际上体现了我们对请求处理的优先级,和Runtime调度的顺序之间产生了冲突,因此导致性能的下降。

04性能测试

我们可以通过编写一个简单的性能测试以佐证上述的说法,这个测试用于显示在高负载runtime中使用异步IO的问题。简要来说,在这个测试中我们会首先需要在runtime上生成一些CPU任务增加负载,而在此期间进行文件IO的性能测试。

完整的测试的代码:

https://github.com/bsbds/bench_tokio_fs

测试结果:

https://bsbds.github.io/bench_tokio_fs/report/

测试中使用的名称:

fswrite: 在Tokio runtime中进行文件写入,使用同步文件IO

fs_write_async: 在Tokio runtime中进行文件写入,使用异步的文件IO

fs_write_thread: 使用单独的线程进行同步文件IO

noload: Tokio runtime上此时没有工作负载

stress: Tokio runtime上此时存在大量工作负载

测试结果分析

以下是测试结果的提琴图。

从图中可以看出,当我们在Tokio runtime上使用文件IO,不论是同步还是异步,都会出现不同程度的延迟上升。特别是对于纯粹异步的文件IO,我们可以观察到它的平均延迟出现了大幅度的上升,并且写入延迟的方差也相对增加,这进一步增加了写入延迟的不确定性。符合我们的预期。

05

如何正确实现?

导致我们问题的核心是因为Tokio不支持tasks之间的优先级,这使得更重要的task无法优先得到执行,由于各个task的执行时间是不确定的,在高负载的情况下我们无法准确预测每个任务被重新调度的时间。

如果要在Tokio上实现task的优先级,我们可能就需要使用优先队列来维护各个task之间的优先级,由于Tokio可能需要同时处理数十万甚至上百万的任务,大多数任务本身可能耗时较短,使用优先队列会带来显著的开销。另外一个问题是如果我们需要维护全局任务的优先级,那么各个worker thread之间就会产生较强的竞争,和Tokio work stealing的机制背道而驰,因此是不可行的。因此,如果需要实现调度优先级的特性,前提是用户所需的任务数量较少并且不会有大量短任务,并且优先级只能够在worker threads的本地工作队列中实现以避免竞争。

由于在现有runtime中难以实现开销较低的优先级机制,在使用Tokio runtime时,如果系统存在优先级较高的任务,正确的方式是需要让这些任务运行在单独的线程池中,避免被其他任务饿死,例如我们可以同时运行多个Tokio runtime,或者手动构建自己的线程池。

在Tokio runtime之上编写代码,明确任务的优先级是非常重要的。在我们的用例中, Tonic gRPC server本身会占用大量CPU时间,因此在高负载下很有可能饿死同一runtime下的其他任务,因此需要考虑将CPU-bound的任务和IO-bound的任务分成两个线程池。persistent_task 是位于关键路径上的一个IO任务,它的优先级应当是最高的。所以一个更合适的方式是让 persistent_task 与Tonic所在的runtime隔离开来,对其生成一个独立的系统线程,并使用同步的文件操作,这样就能够获得稳定的延迟。

06

何时能够在Runtime上阻塞

在上面的性能测试的例子中,我们可以发现,即使在Runtime上直接进行同步的文件IO(fs_write)性能也相对较好。可能有人会告诉你,永远不要在runtime上阻塞!例如在Future trait中有一段对Future特性的描述:

"An implementation of poll should strive to return quickly, and should not block."

(https://doc.rust-lang.org/stable/std/future/trait.Future.html#runtime-characteristics)

然而这并非在任何时候都成立,盲目地实现理论上非阻塞的IO反而会对系统性能带来负面影响。

例如,在Xline中,我们使用RocksDB作为底层KV存储引擎。由于rocksdb crate是对C++库的一层包装,本身并不支持异步的接口,很多人会担心使用同步接口会导致阻塞runtime,他们可能会这样使用RocksDB:

{    tokio::task::spawn_blocking(move || {        db.put("foo", "bar").unwrap();    })    .await;}

tokio::task::spawn_blocking 的作用是将阻塞任务生成到runtime的blocking threads上,blocking threads是独立的线程池,因此不会阻塞executor所在的线程池。

对于RocksDB不熟悉的人可能会认为 db.put 涉及到文件操作,所以是阻塞的,这并不完全正确。我们可以构建简单的benchmark来说明这一点。

下面的benchmark向db写入一个由100个key组成的batch,key size = 256bytes, value size = 1024bytes。

完整的测试代码:

https://github.com/bsbds/bench_rocksdb

测试结果:

https://bsbds.github.io/bench_rocksdb/report/

fn rocksdb_benchmark(c: &mut Criterion) {    let mut group = c.benchmark_group("rocksdb_benchmarks");    let rt = build_runtime();    let db = Arc::new(DB::open_default(BENCH_PATH).unwrap());
    group.bench_function("rocksdb_write_sync", |b| {        b.to_async(&rt).iter(|| async {            db_write(&db);        })    });    group.bench_function("rocksdb_write_async", |b| {        b.to_async(&rt).iter(|| {            let db_c = Arc::clone(&db);            tokio::task::spawn_blocking(move || db_write(&db_c))        })    });    group.finish();
    clean_up();}

在 rocksdb_write_sync 中,直接同步地写入db,而在 rocksdb_write_async 中,使用 spawn_blocking 将同步操作转换为异步写入db。结果如下:

可见我们的异步版本比同步版本慢了46%。这个例子中我们使用RocksDB的同步API实际上是更优的,而如果使用我们转化后的异步版本反而会造成较大的开销。

一个原因是当我们使用 spawn_blocking 运行一个任务时,必定会产生从tokio的worker thread到blocking thread的切换,涉及到两个线程间的数据同步,并且会使得CPU缓存失效,因此这个操作实际上相当昂贵。

使用同步版本的另外一个理由是我们的写操作仅仅花费了数百微秒,这实际上并不能算是一个阻塞操作,RocksDB本身会首先将操作写入到MemTable中,然后通过后台任务异步落盘,因此耗时很短。对于这类时间本身很短的任务如果中途yield到runtime会产生两个问题:

  • 由于时间太短,runtime即使在此期间切换到其他任务也无法有效节省CPU时间

  • Runtime高负载情况下会导致延迟增加,并且也可能涉及到任务被发送到其他worker线程的情况产生同步成本。

然而,熟悉RocksDB的读者可能会疑惑,RocksDB存在write stalls的现象,并不能保证每次写入都保持稳定的延迟,那么这样使用同步版本会产生问题吗?一般来说,偶尔阻塞Runtime实际上不会对整体性能产生明显影响,例如Tokio实现了work stealing机制,如果一个worker thread被阻塞了,其他的worker threads能够从它的工作队列中偷取任务进行执行,这一定程度上缓解阻塞情况,这对于拥有较多核心的CPU产生的影响是较小的, 而 async-std 中会对阻塞时间较长的任务生成一个新线程,这同样缓解了runtime上偶尔阻塞的问题。

综上所属,调用RocksDB的同步API只是“可能阻塞”,这个可能的概率取决于实际的工作负载,在决定是否需要在Runtime中调用同步的阻塞时,请首先评估系统中阻塞的概率。例如在Xline中,我们主要的目标是运行在拥有现代SSD的机器上,并且Xline的设计吞吐量相对于RocksDB最大吞吐量较低,性能瓶颈并不在RocksDB本身的吞吐上,因此RocksDB几乎不会出现write stalls的现象,此时我们在runtime中同步地调用API就能够实现最佳性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1933880.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

万界星空科技电线电缆MES系统实现线缆全流程追溯

MES系统通过高度集成的数据平台&#xff0c;对电线电缆的生产全过程进行实时监控与记录&#xff0c;从原材料入库开始&#xff0c;到生产过程中的各个关键控制点&#xff0c;再到成品出库&#xff0c;每一步操作都被详细记录并可追溯。这种全流程追溯能力主要体现在以下几个方面…

React学习笔记02-----React基本使用

一、React简介 想实现页面的局部刷新&#xff0c;而不是整个网页的刷新。AJAXDOM可以实现局部刷新 1.特点 &#xff08;1&#xff09;虚拟DOM 开发者通过React来操作原生DOM&#xff0c;从而构建页面。 React通过虚拟DOM来实现&#xff0c;可以解决DOM的兼容性问题&#x…

Android10.0 锁屏分析-KeyguardPatternView图案锁分析

首先一起看看下面这张图&#xff1a; 通过前面锁屏加载流程可以知道在KeyguardSecurityContainer中使用getSecurityView()根据不同的securityMode inflate出来&#xff0c;并添加到界面上的。 我们知道&#xff0c;Pattern锁所使用的layout是 R.layout.keyguard_pattern_view&a…

【ESP32】打造全网最强esp-idf基础教程——18.ESP32连接MQTT Broker

ESP32连接MQTT Broker 一、MQTT Broker 在开始ESP32编程之前&#xff0c;我们要先来看看公共主流的MQTT服务器可供使用&#xff0c;所谓的公共MQTT服务器就是一些网站给我们提供了在线的MQTT Broker&#xff0c;我可以直接利用其进行 MQTT 学习、测试甚至是小规模使用&…

表格竖向展示

最近在做手机端web页面&#xff0c;页面中需要有个表格来显示数据&#xff0c;但是由于数据太多页面太窄&#xff0c;table展示横向滑动的话感觉很丑。所以让表格竖向显示了 具体页面如下: 实现代码&#xff1a;当然代码里面绑定的数据啊什么的你都可以修改为自己的内容&#…

【软件建模与设计】-05-软件建模和设计方法概览

目录 1、COMET基于用例的软件生命周期 1.1、需求建模 1.2、分析建模 1.3、设计建模 1.4、增量软件构建 1.5、增量软件集成 1.6、系统测试 2、COMET与其他软件过程比较 2.1、与RUP对比 2.2、与螺旋模型对比 3、需求、分析和设计建模 3.1、需求建模活动 3.2、分析建…

机器学习入门【经典的CIFAR10分类】

模型 神经网络采用下图 我使用之后发现迭代多了之后一直最高是正确率65%左右&#xff0c;然后我自己添加了一些Relu激活函数和正则化&#xff0c;现在正确率可以有80%左右。 模型代码 import torch from torch import nnclass YmModel(nn.Module):def __init__(self):super(…

【香橙派】Orange pi AIpro开发板评测,与树莓派的横向对比以及实机性能测试

一、前言 在人工智能领域飞速发展的时代&#xff0c;国产厂商们也是紧随时代的步伐&#xff0c;迅龙公司联合华为推出了一款全新的开发板 Orange pi AIpro 作为一款建设人工智能新生态的开发板&#xff0c;它可广泛适用于AI边缘计算、深度视觉学习及视频流AI分析、视频图像分析…

ssh远程登录另一台linux电脑

大部分的博客内容所说的安装好ssh服务后&#xff0c;terminal输入 ssh -p port_number clientnameserver_ip 之后输入密码等等就可以登上别人的电脑 但是这是有一个前提的&#xff0c;就是这两台电脑要在同一个局域网下面。 如果很远呢&#xff1f; 远到不在同一个网下面怎么办…

【智能算法应用】粒子群算法求解带出入点车间布局设计问题

目录 1.算法原理2.数学模型3.结果展示4.参考文献5.代码获取 1.算法原理 【智能算法】粒子群算法&#xff08;PSO&#xff09;原理及实现 设施布局问题&#xff08;Facility Layout Problem, FLP&#xff09;&#xff0c;主要目的是在给定的区域内有效地放置不同设备或部件&am…

大模型学习笔记十一:视觉大模型

一、判别式模型和生成式模型 1&#xff09;判别式模型Discriminative ①给某一个样本&#xff0c;判断属于某个类别的概率&#xff0c;擅长分类任务&#xff0c;计算量少。&#xff08;学习策略函数Y f(X)或者条件概率P(YIX)&#xff09; ②不能反映训练数据本身的特性 ③学习…

JavaScript学习笔记(九)

56、JavaScript 类 56.1 JavaScript 类的语法 请使用关键字 class 创建一个类。 请始终添加一个名为 constructor() 的方法。 JavaScript 类不是对象。 它是 JavaScript 对象的模板。 语法&#xff1a; class ClassName {constructor() { ... } }示例&#xff1a;例子创…

【无人值守】对数据中心电力分配系统发展的影响

数据中心在现代信息发展中承载着巨量数据的计算、存储、挖掘、分析和应用等多个方面的功能&#xff0c;是国计民生各行业的多样化的信息化的资产。对稳定的运行与安全运维是基本需求也是重要的保障。 数据中心属于高能耗产业&#xff0c;对用电负荷大且要求极度稳定。除了对电力…

一文-深入了解Ansible常见模块、安装和部署

1 Ansible 介绍 Ansible是一个配置管理系统configuration management system, python 语言是运维人员必须会的语言, ansible 是一个基于python 开发的&#xff08;集合了众多运维工具 puppet、cfengine、chef、func、fabric的优点&#xff09;自动化运维工具, 其功能实现基于ss…

HarmonyOS介绍

一、什么是HarmonyOS HarmonyOS是新一代的智能终端操作系统&#xff0c;为不同设备的智能化、互联与协同提供了统一的语言&#xff0c;为用户带来简捷、流畅、连续、安全可靠的全场景交互体验。 二、HarmonyOS的核心理念 1、一次开发 多端部署 指的是一个工程&#xf…

题解|2023暑期杭电多校05

【原文链接】 &#xff08;补发&#xff09;题解|2023暑期杭电多校05 1001.Typhoon 计算几何 题目大意 依次给定 n n n 个坐标 P P P &#xff0c;预测的台风路线为按顺序两两连接给定坐标所得的折线 现在有 m m m 个庇护所的坐标 S S S &#xff0c;求每个庇护所到台风…

基于AT89C51单片机的多功能自行车测速计程器(含文档、源码与proteus仿真,以及系统详细介绍)

本篇文章论述的是基于AT89C51单片机的多功能自行车测速计程器的详情介绍&#xff0c;如果对您有帮助的话&#xff0c;还请关注一下哦&#xff0c;如果有资源方面的需要可以联系我。 目录 选题背景 原理图 PCB图 仿真图 代码 系统论文 资源下载 选题背景 美丽的夜晚&…

c++树(一)定义,遍历

目录 树的定义 树的基本术语 树的初始起点&#xff1a;我们定义为根 树的层次&#xff1a; 树的定义&#xff1a; 树的性质 性质1&#xff1a; 性质2&#xff1a; 树形结构存储的两种思路 树的遍历模板 树上信息统计方式1-自顶向下统计 树上信息统计方式2-自底向上统…

【漏洞复现】泛微E-Cology WorkflowServiceXml SQL注入漏洞

0x01 产品简介 泛微e-cology是一款由泛微网络科技开发的协同管理平台&#xff0c;支持人力资源、财务、行政等多功能管理和移动办公。 0x02 漏洞概述 泛微OAE-Cology 接口/services/WorkflowServiceXml 存在SQL注入漏洞&#xff0c;可获取数据库权限&#xff0c;导致数据泄露…

Purple Pi OH在Android11下测试WiFi和LAN的TCP和UDP传输速率

本文适用于在Purple Pi OH在Andriod11下如何测试WiFi和LAN的TCP和UDP传输速率。触觉智能的Purple Pi OH鸿蒙开源主板&#xff0c;是华为Laval官方社区主荐的一款鸿蒙开发主板。 该主板主要针对学生党&#xff0c;极客&#xff0c;工程师&#xff0c;极大降低了开源鸿蒙开发者的…