大家好,我是老登, 负责python和rust开发,很高兴大家能够通过本篇文章来认识大家

鼠鼠我啊, 最近前段时间正好有这方面的需求,于是把我这段经历来分享给大家, 如何一步步的去提升性能,在获取行情这块尽可能的快, 也是尽快的去清洗数据给到策略, 尽量的抢先别人一步, 老话有一句讲得好, 早起的鸟儿有虫吃

本文假定有rust的基础经验和基本的交易所知识, 所以本文并不是0基础

开发环境
macos 15.2 (无法在windows环境下运行)
rust 1.83.0
一个海外服务器, 最好是东京服务器, 一般交易所的数据中心大多在东京

前言

主要的交易所, 选择某安

副交易所, 选择比特get

需要用到的crates

[dependencies]
futures-util = "0.3.31"
mimalloc = "0.1.43"
rustls = { version = "0.23.20", features = ["ring"] }
sonic-rs = "0.3.17"
tokio = { version = "1.42.0", features = ["rt", "rt-multi-thread", "macros", "time"] }
tokio-tungstenite = { version = "0.26.1", features = ["rustls-tls-native-roots"] }

[profile.release]
opt-level = 3  # 最大优化级别
lto = true     # 启用 Link Time Optimization (LTO), 将各个模块在链接时进行优化,有助于消除冗余并提升性能
codegen-units = 1  # 使用单个代码生成单元,提升优化效果
debug = false  # 关闭调试信息
panic = 'abort'  # 使用最小化 panic 处理,以提高性能
incremental = false  # 禁用增量编译(只适用于开发阶段)
overflow-checks = false  # 禁用溢出检查,以提高性能

首先我们新增以下代码

use mimalloc::MiMalloc;

#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;


#[tokio::main]
async fn main() {
    rustls::crypto::ring::default_provider()
        .install_default()
        .expect("Failed to install rustls crypto provider");
}

将tls库切换至mimalloc

MiMalloc通常适用于高性能计算、低延迟、高并发或需要精细内存控制的应用
我们替换了Rust自带的内存分配器, 改用微软现代的内存分配器, 可以带来以下的优势
● 减少内存碎片
● 性能优化
● 减少底层锁竞争,提高并发性能
● 跨平台
● 低延迟
同时我们将TLS库由openssl切换至rustls, 这样会带来以下优势
● 纯Rust实现, 具有较低的延迟
● 简单,现代, 安全, 减少了内存漏洞
● 去掉了老旧的加密算法
● 跨平台
● 无外部依赖,不依赖于系统提供的openssl库

天下武功,为快不破

方案一

采用标准的Rust套件
首先, 访问某安行情,根据响应进行建模, 新增以下的代码

#[derive(Default)]
struct Ticker {
    /// 主所(买1, 卖1)
    left_exchange: (f64, f64),
    /// 副所(买1, 卖1)
    right_exchange: (f64, f64),
}

type Tickers = Arc<Mutex<Ticker>>;

该结构体不带String类型, 所以隐含Copy trait, 我们可以直接将数据扔到多线程当中, 不会涉及到堆分配

增加连接某安的逻辑

use futures_util::{SinkExt, StreamExt};
use mimalloc::MiMalloc;
use sonic_rs::{JsonValueTrait, Value};
use std::sync::{Arc, Mutex};
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;


#[tokio::main]
async fn main() {
    // .....
  
    let ticker = Arc::new(Tickers::default());
    let ticker_clone_1 = Arc::clone(&ticker);

    // 某安采集线程
    tokio::spawn(async move {
        loop {
            // 连接某安
            let (ws_stream, _) = connect_async("wss://fstream.binance.com/ws/btcusdt@bookTicker")
                .await
                .unwrap();

            // 切分流
            let (mut write, mut read) = ws_stream.split();

            loop {
                // 处理某安传来的消息
                match read.next().await.unwrap().unwrap() {
                    Message::Text(data) => {
                        // 将数据转换成document对象
                        // 解析到sonic_rs::Value后,底层是一个 Key-Value pair 的数组,而不会建立 HashMap 或 BTreeMap, 因此没有建表开销。
                        let root: Value =
                            unsafe { sonic_rs::from_slice_unchecked(data.as_bytes()).unwrap() };

                        let mut temp_ticker = ticker.lock().unwrap();

                        // 保存主所的买一卖一
                        temp_ticker.left_exchange = (
                            root.get("b").as_str().unwrap().parse().unwrap(),
                            root.get("a").as_str().unwrap().parse().unwrap(),
                        );
                    }
                    Message::Ping(_) => {
                        // 收到ping, 回复pong
                        write.send(Message::Pong(Default::default())).await.unwrap();
                    }
                    _ => break,
                }
            }
        }
    });

    // 执行策略的线程
    let task = tokio::task::spawn_blocking(move || {
        loop {
            // 获取最新行情
            // 通过Drop trait来达到快速释放锁
            let temp_ticker = {
                let temp_ticker = ticker_clone_1.lock().unwrap();
                // 因为f64是Rust的基本类型,所以实现了Copy trait, 这里会执行按位复制,不会涉及堆分配
                (temp_ticker.left_exchange, temp_ticker.right_exchange)
            };

            // .......这里模拟执行策略
            println!("{:?}", temp_ticker);

            // 大概每10ms轮训一次新行情
            std::thread::sleep(std::time::Duration::from_millis(10));
        }
    });

    task.await.unwrap();
}

发起ws连接, 并处理接受到的数据, 通过利用sonic_rs的simd指令特性, 来通过unsafe方法跳过检验json完整性,生成document对象, 然后提取相应的买1和卖1数据, 这样的话,通过unsafe可以节省至少一倍左右的时间

执行策略的线程获取到最新的行情, 拿到最新的行情去执行策略, 这里的休眠时间我选择10ms, 这里的时间取决于推送最快的交易所, 某安的数据每10ms左右推送一次数据, 所以这里假设我们是最优的等待时间, 休眠时间大了, 我们容易拿不到最优行情, 时间低了, 我们反而浪费cpu做了大量无用功, 这里的值要取一个度,也算做一个简单的因子

RUSTFLAGS="-C target-cpu=native" cargo build --release

我们通过执行程序, 可以源源不断的看到, 我们已经拿左边主所的某安行情了

接下来, 让我们继续完成比特get副所的行情采集
新增以下代码

use futures_util::{SinkExt, StreamExt};
use mimalloc::MiMalloc;
use sonic_rs::{pointer, JsonValueTrait, Value};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::{Message, Utf8Bytes};


#[tokio::main]
async fn main() {
  // ....

  let ticker_clone_2 = Arc::clone(&ticker);

  // 比特get采集线程
  tokio::spawn(async move {
        loop {
            let (ws_stream, _) = connect_async("wss://ws.比特get.com/v2/ws/public")
                .await
                .unwrap();

            let (mut write, mut read) = ws_stream.split();

            // 订阅btc频道
            let data = sonic_rs::json!({
                "op": "subscribe",
                "args": [
                    {
                        "instType": "USDT-FUTURES",
                        "channel": "books1",
                        "instId": "BTCUSDT"
                    }
                ]
            });
            write
                .send(Message::Text(Utf8Bytes::from(data.to_string())))
                .await
                .unwrap();

            // 定时ping
            tokio::spawn(async move {
                loop {
                    tokio::time::sleep(Duration::from_secs(30)).await;

                    write
                        .send(Message::Text(Utf8Bytes::from("ping")))
                        .await
                        .unwrap();
                }
            });

            // 跳过 订阅成功回调
            read.next().await.unwrap().unwrap();

            loop {
                // 处理某安传来的消息
                while let Ok(data) = read.next().await.unwrap() {
                    // 跳过pong心跳
                    if data.len() == 4 {
                        continue;
                    }

                    if let Message::Text(data) = data {
                        // 这里我展示了另外一种提取数据的手段
                        // 优点是零拷贝
                        let bid: f64 = unsafe {
                            sonic_rs::get_from_slice_unchecked(
                                data.as_bytes(),
                                pointer!["data", 0, "bids", 0, 0],
                            )
                            .unwrap()
                        }
                        .as_str()
                        .unwrap()
                        .parse()
                        .unwrap();

                        let ask: f64 = unsafe {
                            sonic_rs::get_from_slice_unchecked(
                                data.as_bytes(),
                                pointer!["data", 0, "asks", 0, 0],
                            )
                            .unwrap()
                        }
                        .as_str()
                        .unwrap()
                        .parse()
                        .unwrap();

                        let mut temp_ticker = ticker_clone_2.lock().unwrap();

                        // 保存主所的买一卖一
                        temp_ticker.right_exchange = (bid, ask);

                        continue;
                    }

                    // 占位符, 不会走到这里
                    todo!()
                }
            }
        }
  });
  
  // ....
}

这段代码中, 我采用了另外一种提取数据的手段, 还是通过unsafe的方式, 零拷贝, 不会额外分配内存或复制数据, 这使得非常快速

这里如果不是为了可读性, 其实可以把ask和bid的中间变量去掉,直接放在元组中, 省去定义中间变量的步骤

这个交易所的心跳返回的是"pong"字符串, 我没有直接和"pong"字符串直接进行对比, 因为在底层还是要一个个字符进行遍历的, Bytes数据结构是一个高效的字节缓冲区, 内部有记录指向自身视图的长度字段, 可以通过O(1)的方式快速获得长度直接进行判断

RUSTFLAGS="-C target-cpu=native" cargo build --release

接下来我们继续运行程序, 稍等一会, 我们两个交易所的数据源源不断的推送过来

程序现在是没有问题了, 但是我们现在想想, 我们采用的是共享内存的方式去存储数据, 但是rust保证线程安全和所有权机制, 必须对数据加以限制
Mutex提供一种动态的锁机制, 确保同时只有一个线程可以访问数据,其他线程会阻塞程序,我们现在这个程序是无所谓的, 但是假设我要单机采集多个交易所多个交易对, 并且推送频率都很快, 这个锁会不会限制我们的运行时性能呢?我们深深的思考一下, 锁保证了我们程序的稳定性,但是同时也锁住了我们的性能
这个方案本质是属于, 主动拉取,定时拉取最新数据, 实在是不太完美

长风破浪会有时,直挂云帆济沧海

有没有更加完美的解决方案呢?

有....

方案二

有一句老话是, 退一步海阔天空

我们不在需要主动出击, 被动等待交易所推送行情, 行情一推送, 就执行回调函数一次, 程序就去做一次计算, 不再需要定时轮询行情

同时我们也用第三方库把Mutex互斥锁干掉,整个程序无锁并发

借助Rust的闭包功能,我们来实现这一点

创建一个新的项目, 并把原来的部分代码拿过来

[dependencies]
// ....
crossbeam-utils = "0.8.21"

我们先定义一个模拟策略执行的闭包

use crossbeam_utils::atomic::AtomicCell;
use futures_util::{SinkExt, StreamExt};
use mimalloc::MiMalloc;
use sonic_rs::{pointer, JsonValueTrait, Value};
use std::sync::Arc;
use std::time::Duration;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::{Message, Utf8Bytes};


#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;


#[derive(Default, Debug)]
struct Ticker {
    /// 主所(买1, 卖1)
    left_exchange: (AtomicCell<f64>, AtomicCell<f64>),
    /// 副所(买1, 卖1)
    right_exchange: (AtomicCell<f64>, AtomicCell<f64>),
}

#[tokio::main]
async fn main() {
    // ...

    let ticker = Arc::new(Ticker::default());
    let ticker_clone_1 = Arc::clone(&ticker);
    let ticker_clone_2 = Arc::clone(&ticker);

    // 策略回调
    let strategy_callback = Arc::new(move || {
        // 读取行情
        let bid1 = ticker.left_exchange.0.load();
        let ask1 = ticker.left_exchange.1.load();

        let bid2 = ticker.right_exchange.0.load();
        let ask2 = ticker.right_exchange.1.load();

        // ... 模拟执行策略
        println!("{:?}", (bid1, ask1, bid2, ask2));
    });

    let strategy_callback_clone = Arc::clone(&strategy_callback);

    // ...
}
AtomicCell相比普通的Mutex的优势

● 使用原子操作来实现线程安全, 避免上下文阻塞

● 由于不需要锁的释放, 通常比Mutex快, 尤其是读写频繁的场景

● 简单易用

● 无死锁风险,内部不使用锁机制

● 较小的内存开销

● 跨线程共享, 可以直接在多线程中共享而无需包装

我们设计的数据结构很简单, 不涉及复杂类型, 从实现来看, 就已经满足Copy trait, 符合AtomicCell设计原则, 也符合高性能, 低延迟读写的场景

我们简单修改下两个交易所的行情数据写入

#[tokio::main]
async fn main() {
    // ...

    // 某安采集线程
    tokio::spawn(async move {
        loop {
            // 连接某安
            let (ws_stream, _) = connect_async("wss://fstream.binance.com/ws/btcusdt@bookTicker")
                .await
                .unwrap();

            // 切分流
            let (mut write, mut read) = ws_stream.split();

            loop {
                // 处理某安传来的消息
                match read.next().await.unwrap().unwrap() {
                    Message::Text(data) => {
                        let root: Value =
                            unsafe { sonic_rs::from_slice_unchecked(data.as_bytes()).unwrap() };

                        // 无锁写入
                        ticker_clone_1
                            .left_exchange
                            .0
                            .store(root.get("b").as_str().unwrap().parse().unwrap());
                        ticker_clone_1
                            .left_exchange
                            .1
                            .store(root.get("a").as_str().unwrap().parse().unwrap());

                        // 执行策略回调
                        strategy_callback()
                    }
                    Message::Ping(_) => {
                        // 收到ping, 回复pong
                        write.send(Message::Pong(Default::default())).await.unwrap();
                    }
                    _ => break,
                }
            }
        }
    });

    // 比特get采集线程
    let task = tokio::spawn(async move {
        loop {
            // 连接某安
            let (ws_stream, _) = connect_async("wss://ws.比特get.com/v2/ws/public")
                .await
                .unwrap();

            // 切分流
            let (mut write, mut read) = ws_stream.split();

            // 订阅btc频道
            let data = sonic_rs::json!({
                "op": "subscribe",
                "args": [
                    {
                        "instType": "USDT-FUTURES",
                        "channel": "books1",
                        "instId": "BTCUSDT"
                    }
                ]
            });
            write
                .send(Message::Text(Utf8Bytes::from(data.to_string())))
                .await
                .unwrap();

            // 定时ping
            tokio::spawn(async move {
                loop {
                    tokio::time::sleep(Duration::from_secs(30)).await;

                    write
                        .send(Message::Text(Utf8Bytes::from("ping")))
                        .await
                        .unwrap();
                }
            });

            // 跳过 订阅成功回调
            read.next().await.unwrap().unwrap();

            loop {
                // 处理某安传来的消息
                while let Ok(data) = read.next().await.unwrap() {
                    // 跳过pong心跳
                    if data.len() == 4 {
                        continue;
                    }

                    if let Message::Text(data) = data {
                        // 这里我展示了另外一种提取数据的手段
                        // 优点是零拷贝
                        let bid: f64 = unsafe {
                            sonic_rs::get_from_slice_unchecked(
                                data.as_bytes(),
                                pointer!["data", 0, "bids", 0, 0],
                            )
                            .unwrap()
                        }
                        .as_str()
                        .unwrap()
                        .parse()
                        .unwrap();

                        let ask: f64 = unsafe {
                            sonic_rs::get_from_slice_unchecked(
                                data.as_bytes(),
                                pointer!["data", 0, "asks", 0, 0],
                            )
                            .unwrap()
                        }
                        .as_str()
                        .unwrap()
                        .parse()
                        .unwrap();

                        // 无锁写入
                        ticker_clone_2.right_exchange.0.store(bid);
                        ticker_clone_2.right_exchange.1.store(ask);

                        // 执行策略回调
                        strategy_callback_clone();

                        continue;
                    }

                    // 占位符, 不会走到这里
                    todo!()
                }
            }
        }
    });

    task.await.unwrap();
}
RUSTFLAGS="-C target-cpu=native" cargo build --release

执行程序, 我们可以拿到最终数据了

通过两个简单的案例, 我们使用了常规的Mutex和无锁设计模式来进行实战, 向你们展示了如何通过非常规手段去获取数据, 以及如何快速, 高效的清洗数据给到策略, 之后我们巧用闭包和第三方数据结构解决了我们第一个案例所遇到的问题

好了, 这次我分享的内容就这么多, 还有一些东西, 我会在下次文章继续进行分享, 感谢您的收看, 我们后会有期!

大纲

☑ Mutex设计模式

☑ 无锁设计模式

☑ 使用闭包

☑ 使用sonic_rs快速获取值

☐ 内存对齐, 避免伪共享

☐ 分布式采集

☐ CPU亲和绑定线程

本文仓库地址: https://gitee.com/kxx0710/rust_ticker
文献

● 了解什么是mimalloc?

● 了解什么是sonic_rs?

Rust的闭包官方概念

AtomicCell


云梦量化科技
1 声望0 粉丝