可扩展微服务架构:使用Rust和RabbitMQ构建应用
1. 可扩展微服务架构概述
在处理I/O密集型任务时,可以使用负载均衡器将负载导向特定服务。不过,微服务应具备可替换性,且不应保留持久状态,而是从数据库加载状态。这样能借助Kubernetes等工具实现应用的自动扩展。
此外,可按逻辑域将大型任务拆分为多个小型独立的微服务。例如,在在线商店中,可以创建一个单独的微服务来处理账户,另一个微服务用于渲染和显示购物车,还可以添加一个处理支付的微服务,它们通过消息代理传递消息进行交互。
2. 构建可扩展应用
接下来,我们将构建一个能通过运行部分组件的额外实例实现扩展的应用,具体是一个将图像中的二维码解码为文本字符串的应用。此应用包含两个服务:一个用于处理传入请求和解码任务,另一个作为工作者,接收任务并将图像解码为字符串,然后将结果返回给服务器。为实现服务间的交互,我们将使用RabbitMQ;对于服务器和工作者的实现,将采用Actix框架。
3. 启动RabbitMQ实例
在开始编码前,需使用Docker启动一个RabbitMQ实例。可从DockerHub获取官方镜像,使用以下命令启动:
docker run -it --rm --name test-rabbit -p 5672:5672 rabbitmq:3
此命令启动了一个名为
test-rabbit
的容器,并将主机的5672端口映射到容器的5672端口。RabbitMQ镜像还暴露了4369、5671和25672端口,若要使用消息代理的高级功能,需打开这些端口。
若要启动一个可从其他容器访问的RabbitMQ实例,可在
run
命令中设置
--hostname
参数,并在其他容器中使用该名称连接到RabbitMQ实例。
当消息代理实例启动后,可使用以下命令获取相关统计信息:
docker exec -it test-rabbit rabbitmqctl
该命令会打印可用命令,可添加具体命令,如:
docker exec -it test-rabbit rabbitmqctl trace_on
此命令将激活对推送到队列的所有消息的跟踪,可使用以下命令查看:
docker exec -it test-rabbit rabbitmqctl list_exchanges
该命令会输出如下内容:
Listing exchanges for vhost / ...
amq.headers headers
amq.direct direct
amq.topic topic
amq.rabbitmq.trace topic
direct
amq.fanout fanout
amq.match headers
4. 项目依赖
创建一个名为
rabbit-actix
的新库 crate,在其中添加两个二进制文件。以下是
Cargo.toml
文件的内容:
[package]
name = "rabbit-actix"
version = "0.1.0"
edition = "2018"
[dependencies]
actix = "0.7"
actix-web = "0.7"
askama = "0.7"
chrono = "0.4"
env_logger = "0.6"
image = "0.21"
indexmap = "1.0"
failure = "0.1"
futures = "0.1"
log = "0.4"
queens-rock = "0.1"
rmp-serde = "0.13"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
tokio = "0.1"
uuid = "0.7"
[dependencies.lapin]
version = "0.15"
package = "lapin-futures"
[[bin]]
name = "rabbit-actix-server"
path = "src/server.rs"
test = false
[[bin]]
name = "rabbit-actix-worker"
path = "src/worker.rs"
test = false
[build-dependencies]
askama = "0.7"
同时,在
build.rs
脚本中添加以下代码:
fn main() {
askama::rerun_if_templates_changed();
}
5. 抽象队列交互参与者
添加
src/queue_actor.rs
文件,创建一个使用抽象处理程序处理传入消息并能向队列发送新消息的参与者。该参与者还需在RabbitMQ中创建所有必要的队列,并订阅相应队列中的新事件。
以下是创建参与者所需的依赖:
use super::{ensure_queue, spawn_client};
use actix::fut::wrap_future;
use actix::{Actor, Addr, AsyncContext, Context, Handler, Message,
StreamHandler, SystemRunner};
use failure::{format_err, Error};
use futures::Future;
use lapin::channel::{BasicConsumeOptions, BasicProperties,
BasicPublishOptions, Channel};
use lapin::error::Error as LapinError;
use lapin::message::Delivery;
use lapin::types::{FieldTable, ShortString};
use log::{debug, warn};
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use uuid::Uuid;
pub type TaskId = ShortString;
6. 抽象消息处理程序
在
queue_actor.rs
文件中创建
QueueHandler
结构体:
pub trait QueueHandler: 'static {
type Incoming: for<'de> Deserialize<'de>;
type Outgoing: Serialize;
fn incoming(&self) -> &str;
fn outgoing(&self) -> &str;
fn handle(
&self,
id: &TaskId,
incoming: Self::Incoming,
) -> Result<Option<Self::Outgoing>, Error>;
}
QueueHandler
是一个具有两个关联类型和三个方法的trait。它要求实现该trait的类型具有静态生命周期,因为该trait的实例将作为具有静态生命周期的参与者的字段使用。
7. 参与者实现
添加一个名为
QueueActor
的新结构体,并添加一个实现
QueueHandler
trait的类型参数:
pub struct QueueActor<T: QueueHandler> {
channel: Channel<TcpStream>,
handler: T,
}
impl<T: QueueHandler> Actor for QueueActor<T> {
type Context = Context<Self>;
fn started(&mut self, _: &mut Self::Context) {}
}
impl<T: QueueHandler> QueueActor<T> {
pub fn new(handler: T, mut sys: &mut SystemRunner) ->
Result<Addr<Self>, Error> {
let channel = spawn_client(&mut sys)?;
let chan = channel.clone();
let fut = ensure_queue(&chan, handler.outgoing());
sys.block_on(fut)?;
let fut = ensure_queue(&chan, handler.incoming()).and_then(move
|queue| {
let opts = BasicConsumeOptions {
..Default::default()
};
let table = FieldTable::new();
let name = format!("{}-consumer", queue.name());
chan.basic_consume(&queue, &name, opts, table)
});
let stream = sys.block_on(fut)?;
let addr = QueueActor::create(move |ctx| {
ctx.add_stream(stream);
Self { channel, handler }
});
Ok(addr)
}
}
8. 处理传入流
为
QueueActor
类型实现
StreamHandler
,以处理从队列返回的
Delivery
对象:
impl<T: QueueHandler> StreamHandler<Delivery, LapinError> for QueueActor<T>
{
fn handle(&mut self, item: Delivery, ctx: &mut Context<Self>) {
debug!("Message received!");
let fut = self
.channel
.basic_ack(item.delivery_tag, false)
.map_err(drop);
ctx.spawn(wrap_future(fut));
match self.process_message(item, ctx) {
Ok(pair) => {
if let Some((corr_id, data)) = pair {
self.send_message(corr_id, data, ctx);
}
}
Err(err) => {
warn!("Message processing error: {}", err);
}
}
}
}
9. 发送新消息
添加
SendMessage
结构体,并为其实现
Message
trait:
pub struct SendMessage<T>(pub T);
impl<T> Message for SendMessage<T> {
type Result = TaskId;
}
impl<T: QueueHandler> Handler<SendMessage<T::Outgoing>> for QueueActor<T> {
type Result = TaskId;
fn handle(&mut self, msg: SendMessage<T::Outgoing>, ctx: &mut
Self::Context) -> Self::Result {
let corr_id = Uuid::new_v4().to_simple().to_string();
self.send_message(corr_id.clone(), msg.0, ctx);
corr_id
}
}
10. 实用方法实现
实现
process_message
和
send_message
方法:
impl<T: QueueHandler> QueueActor<T> {
fn process_message(
&self,
item: Delivery,
_: &mut Context<Self>,
) -> Result<Option<(ShortString, T::Outgoing)>, Error> {
let corr_id = item
.properties
.correlation_id()
.to_owned()
.ok_or_else(|| format_err!("Message has no address for the
response"))?;
let incoming = serde_json::from_slice(&item.data)?;
let outgoing = self.handler.handle(&corr_id, incoming)?;
if let Some(outgoing) = outgoing {
Ok(Some((corr_id, outgoing)))
} else {
Ok(None)
}
}
fn send_message(&self, corr_id: ShortString, outgoing: T::Outgoing,
ctx: &mut Context<Self>) {
let data = serde_json::to_vec(&outgoing);
match data {
Ok(data) => {
let opts = BasicPublishOptions::default();
let props =
BasicProperties::default().with_correlation_id(corr_id);
debug!("Sending to: {}", self.handler.outgoing());
let fut = self
.channel
.basic_publish("", self.handler.outgoing(), data,
opts, props)
.map(drop)
.map_err(drop);
ctx.spawn(wrap_future(fut));
}
Err(err) => {
warn!("Can't encode an outgoing message: {}", err);
}
}
}
}
11. 总结
通过以上步骤,我们完成了一个使用Rust和RabbitMQ构建的可扩展微服务应用的基础架构。该应用通过消息代理实现服务间的交互,具备可扩展性和灵活性。以下是整个流程的mermaid流程图:
graph LR
A[启动RabbitMQ实例] --> B[创建项目及依赖]
B --> C[定义抽象队列交互参与者]
C --> D[实现抽象消息处理程序]
D --> E[实现参与者及相关方法]
E --> F[处理传入流]
F --> G[发送新消息]
G --> H[实现实用方法]
同时,我们还可以用表格总结项目的主要依赖:
| 依赖名称 | 版本 | 用途 |
| ---- | ---- | ---- |
| actix | 0.7 | 实现服务器和工作者 |
| actix-web | 0.7 | 构建Web应用 |
| askama | 0.7 | 渲染HTML页面 |
| chrono | 0.4 | 处理时间相关操作 |
| env_logger | 0.6 | 日志记录 |
| image | 0.21 | 处理图像格式 |
| indexmap | 1.0 | 获取有序哈希映射 |
| failure | 0.1 | 错误处理 |
| futures | 0.1 | 异步编程 |
| log | 0.4 | 日志记录 |
| queens-rock | 0.1 | 二维码解码 |
| rmp-serde | 0.13 | 序列化和反序列化 |
| serde | 1.0 | 序列化和反序列化 |
| serde_derive | 1.0 | 序列化和反序列化派生宏 |
| serde_json | 1.0 | JSON序列化和反序列化 |
| tokio | 0.1 | 异步运行时 |
| uuid | 0.7 | 生成唯一ID |
| lapin | 0.15 | 与RabbitMQ交互 |
可扩展微服务架构:使用Rust和RabbitMQ构建应用
12. 代码解释与关键概念
以上代码实现了一个基于Rust和RabbitMQ的可扩展微服务架构,下面对关键部分进行详细解释:
-
QueueHandler特性(trait) : -
该特性定义了处理队列消息的接口。
Incoming和Outgoing关联类型分别表示传入和传出消息的类型。 -
incoming和outgoing方法分别返回用于接收和发送消息的队列名称。 -
handle方法处理传入消息,并返回一个可选的传出消息。 -
QueueActor结构体 : -
该结构体封装了与RabbitMQ的连接通道
channel和消息处理程序handler。 -
new方法用于创建QueueActor实例,它会确保所需的队列存在,并开始监听传入消息。 -
StreamHandler实现用于处理从队列接收到的消息。收到消息后,会发送确认消息,并调用process_message方法处理消息。 -
process_message方法解析消息,调用QueueHandler的handle方法处理消息,并根据处理结果决定是否发送响应消息。 -
send_message方法将消息发送到指定的队列,并设置关联的correlation_id。
13. 扩展与优化建议
在实际应用中,可以根据需求对上述架构进行扩展和优化:
- 错误处理 :当前的错误处理较为简单,可以添加更详细的错误日志和重试机制,以提高系统的健壮性。
- 性能优化 :可以使用连接池来管理与RabbitMQ的连接,减少连接开销。还可以考虑使用异步I/O和并发处理来提高消息处理的性能。
- 安全性 :在生产环境中,需要对RabbitMQ进行安全配置,如设置用户名和密码、使用SSL/TLS加密连接等。
14. 示例使用场景
该架构适用于各种需要异步消息处理和可扩展性的场景,例如:
- 图像处理 :如本文示例中的二维码解码任务,可以将图像解码任务分发到多个工作者节点,提高处理速度。
- 数据处理 :可以将大量数据的处理任务拆分为多个小任务,通过消息队列分发给不同的工作者进行处理。
- 实时通知 :可以使用消息队列实现实时通知功能,将通知消息发送到订阅的客户端。
15. 总结与展望
通过使用Rust和RabbitMQ,我们构建了一个可扩展的微服务架构,实现了服务间的异步消息通信。该架构具有以下优点:
- 可扩展性 :可以通过添加更多的工作者节点来处理更多的任务。
- 灵活性 :可以根据需求轻松调整消息处理逻辑。
- 高性能 :Rust的高性能和异步编程模型确保了系统的高效运行。
未来,可以进一步探索如何将该架构与其他技术(如容器化、自动化部署等)结合,以实现更强大、更高效的微服务系统。
以下是一个简单的mermaid流程图,展示消息处理的主要流程:
graph LR
A[消息进入队列] --> B[QueueActor接收消息]
B --> C[发送确认消息]
C --> D[调用process_message处理消息]
D --> E{是否有响应消息}
E -- 是 --> F[调用send_message发送响应消息]
E -- 否 --> G[结束处理]
同时,为了更清晰地展示代码结构和依赖关系,我们可以用表格总结主要的代码文件和其功能:
| 文件名称 | 功能 |
| ---- | ---- |
|
src/server.rs
| 服务器实现 |
|
src/worker.rs
| 工作者实现 |
|
src/queue_actor.rs
| 抽象队列交互参与者实现 |
|
build.rs
| 构建脚本,处理模板更新 |
|
Cargo.toml
| 项目依赖配置 |
通过以上的架构设计和实现,我们可以构建出一个高效、可扩展的微服务系统,满足不同场景下的业务需求。
超级会员免费看
1922

被折叠的 条评论
为什么被折叠?



