引言
最近使用Rust写了一个消费企业GitHub评论事件的微服务。本文记录自己选择Rust编写而不是Python的一些思考,如稍微花时间的点。
微服务要做的事情很简单:暴露一个接收post请求的http接口。每当收到payload的时候,如果是我感兴趣的消息,那么就提取相应的信息,发送给另外一个服务器。
我预先估计使用Rust会需要4天(每天有效工作时间4个小时,4*4=16小时),而如果使用Python会需要2天。
实际共花费了2天,大致时间分布如下,
- 1.5天编写完成Rust代码和单元测试
- 0.5天部署到kubenetes和GitHub完全打通。
完成的微服务的架构如下,
使用Rust需要额外思考的点
这是我用Rust编写的第2.5个项目,第一个是 https:// github.com/Celthi/symbo l-service
使用Rust写代码过程中,遇到了一些可能Rust独有的需要稍微花点时间的问题
全局可变变量
web模块收到消息后,要存放到消息队列里面。而web模块是一个接收特定参数的函数,那么它提取消息以后,怎么访问消息队列呢?
消息队列不能从参数传进来,那么只能使用全局变量了。
全局变量使用lazy_static这个库。但是现在要创建一个可变的channel,类似于下面的代码
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
我当时以为channel发送端和接收端必须是可变的,因为发送一个消息肯定要改变channel的队列。于是需要全局可共享多线程的可变变量。共享多线程因为web模块和处理模块不在同一个线程中。
共享可变,这时候需要Mutex,然后就花时间研究了一下怎么做,最后生成的代码如下,(可以用,但是有点丑
lazy_static! {
static ref CHANNEL: (
Arc<Mutex<Option<Sender<task::Task>>>>,
Arc<Mutex<Option<Receiver<toil_task::ToilTask>>>>
) = (Arc::new(Mutex::new(None)), Arc::new(Mutex::new(None)));
static ref CONFIG: config_env::ConfigEnv = (|| {
match config_env::ConfigEnv::new() {
Ok(c) => c,
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
}
})();
}
这里还把环境变量放到了全局的变量,方便各个地方使用。而且还用闭包来读取环境变量。为什么用闭包,因为我要处理环境变量不存在的情况,直接退出。
至于为什么使用Option?那是因为我找到不到在lazy_static如何创建channel。现在看来可以仿照闭包的方式来创建。
上面的解决方案,丑但是可以正常工作。我已经可以接受rc<Mutex<Option<Sendertask::Task>>>,毕竟用的地方少,可以稍微包装一下。
异步处理
从上面的架构图看,我们可以一条道走下去,同步的处理一个事件消息。但是我这里选择了创建两个异步事件循环来处理(event loop)
第一个event loop是web框架里面的事件循环。它负责获取http请求,放到消息队列中。
第二个event loop是处理消息的事件循坏。它负责从消息队列拿出消息,然后处理。
我采取了poem web框架。这是因为我在一个Rust的微信群,群里面有poem的作者和经常听到poem的讨论。
这里稍微花点时间的是第二个event loop,一开始我在poem的main函数里面开启一个线程,在线程里面使用loop来循环从消息队列获取消息。
/// !!!!错误代码
#[tokio::main]
async fn poem_main() -> Result<(), std::io::Error> {
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "poem=debug");
}
tracing_subscriber::fmt::init();
std::thread::spawn(|| {
loop {
tokio::spawn(async {
println!("");
});
}
});
let app = Route::new().at("/webhook", post(hello)).with(Tracing);
Server::new(TcpListener::bind("0.0.0.0:31440"))
.run(app)
.await
}
但是遇到了下面的报错
thread '<unnamed>' panicked at 'there is no reactor running, must be called from the context of a Tokio 1.x runtime', src/bin/ocr_agent.rs:185:8
于是Google了一番,发现(实际没发现)短时间内没找到有用的消息,于是,
把代码改成了如下,
#[tokio::main]
async fn web_main() -> Result<(), std::io::Error> {
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "poem=debug");
}
tracing_subscriber::fmt::init();
tokio::spawn(async {
loop {
tokio::spawn(async {
println!("");
});
}
});
let app = Route::new().at("/ocr_webhook", post(hello)).with(Tracing);
Server::new(TcpListener::bind("0.0.0.0:31430"))
.run(app)
.await
}
能启动了,但是短暂的测试后,发现这样的程序有一个很奇怪的bug:第一条消息永远不会被消费,而后面到来的消息正常。
经过短暂的搜索以后,看了网上一些人遇到的问题,但是他们跟我不一样。因为我的目标是两天把这个微服务写好,所以当时我就跳过这个问题了。截止此刻我还没没有确认原因。
于是我回到了第一个问题,通过阅读tokio的runtime那部分文档,我发现原来可以使用两个tokio runtime instance。这不正是我要的设计吗?
所以第二个事件循环,我直接创建了一个新的tokio runtime instance。所以正确且是我想要的代码如下,
fn main() {
ensure_config();
init_channels();
let mut v = vec![];
let j = thread::spawn(|| {
poem_main();
});
v.push(j);
let j = thread::spawn(|| {
message_main();
});
v.push(j);
for t in v {
t.join().unwrap();
}
}
这个问题解决了。
其他也遇到了一些问题,但是那些基本都是搜索一下,看一下示例代码就会了,这里就不赘述了。
从零开始的估计
如果从零估计使用Rust会花多少时间,那么下面是我的估计。注意:单位天是指4个小时以上的投入。
- 会使用Poem web框架 1天
- 会使用Tokio异步框架3天
- 会用lazy_static1天
- 日志x天
- 业务处理时间 n天
以上建立在会Rust的基础上。而学会Rust的投入因人而异,就不考虑了。因此,使用rust从零构建一个微服务需要的时间
5天+n天+x天。n天是业务处理需要的时间,因业务而异。x天是我不知道需要多少天
我写的微服务的业务是处理消息,发到另外一个服务器。简单得很,只需要1天。加上poem,tokio已经有点会了和我把所有的日志输出到stdout和stderr(因为kubenetes默认会保存10M的内容)所以总共用时2天。
对比Python
用Rust花费两天写了这个微服务,如果是使用Python,那么去掉Rust才有的问题,写这个微服务只需要1天。
但是我还是选择使用Rust来编写,原因是,
- 从真实使用的角度看,Rust写代码的体验感比Python差不了多少。除了要偶尔改改简单的编译错误,比如忘了加mut,使用了move的值,忘了加引用等等。
- Rust强制处理有错误的情况。有人会说,强制处理错误不是很烦吗,快速开发不应该先把功能做好吗?实际上,根据我多年的编程经验,前期多花点时间,好过后期排查故障。并不是说使用Rust就不用后期排查故障了。而是更好地排查故障。因为你已经处理了有错误的情况,那么有故障也大概率在你知道的地方。如果使用Python,那么常用的手段是在顶层把exception catch住,然后打印调用栈,接着像什么事情也没发生一样,继续运行。继续运行是为了不让服务中断,一两个请求失败了,没有关系。
- Python处理json很方便。因为github发的消息都是json格式,所以提取感兴趣的部分直接取就行了。但是Rust有serde_json,处理json也挺方便的,没有想象中那么麻烦。多出来的步骤是要定义结构体,调用解析函数,最后才可提取感兴趣的部分。
- Rust是强类型,但是类型推导比较强大,很多时候不需要写类型。但是IDE提示可以方便写代码。Python 3.10也有类型,IDE提示也还可以。
实践证明Rust写也挺方便的,现在已经在处理公司企业GitHub 7+代码库的事件。
微服务的资源占用情况为top -p