前端初学 Rust 实战:实现自动化部署项目的微服务

契机

笔者是名前端开发,有些个人的业余全栈项目,前几年在部署这些项目的方案选择上,并没有将云主机作为首要选择,而是选择了彼时热度很高的 Serverless。

Serverless 的愿景很美好,它允许开发人员不需要关注服务器的管理和维护,只关注于应用程序的开发即可;除此之外,还有快速部署、按量付费等特点。

Next.js 框架的缔造者 Vercel 提供的也是 Serverless 服务,你只需要授权 Github 账号,选中项目 git 仓库,然后就可以自动拉取代码、安装依赖、构建项目、部署应用。

然而,在体验了包括 Vercel 在内的多家云服务商提供的 Serverless 服务之后,我发现了这种架构存在着诸多无法忽视的缺点,其中最致命的一点是就是冷启动! 它非常非常影响用户体验!我多次试图让自己接受它,但都失败了 🥹。

所以兜兜转转,我还是决定放弃 Serverless,回到云主机的怀抱。

在这个时间点,我已经通过工作触到了许多实践中的概念,CI/CD 便是其中之一。决定把项目部署在云主机上后,就着手写一个微服务实现 CI/CD。最初使用了 Node.js 实现,当它运行起来的时候,看着内存占用我陷入了沉默:那么简单的微服务,也要吃我 ~100MB 的内存 🥲。这使服务器只有 2G 内存的我感到如芒刺背。

实现内存占用更少的自动化部署微服务一直是我心中的结。但 Node.js 实在让人束手无策,于是就搁置了许久,直到我快忘了它......

时间来到 2023 年,🦀 Rust 在前端生态掀起了狂风暴雨,Deno、SWC、Rspack、Turbopack......一个接一个来势汹汹。对于入门 Rust 我徘徊纠结了相当长一段时间。学它干嘛?工作中用得到吗?那么难学,值的投入精力去学它么?直到我想到了那个自动化部署的微服务需要优化,直到我用 Rust 写了个 Web Server Demo,直到我看到了它的内存占用,我便确定需要 Rust 作为我的第二语言,当我在 JavaScript 的世界中无可奈何时,还能有它。

实现一个 GET / 返回 "Hello World" 的 Web Server :

👆Node.js(Express) / Rust(Axum)👇

主流程一览

下面通过一张流程图介绍我们的微服务实现自动化部署项目的整个过程。

微服务的调用,借助了 Github 提供的 Webhook 的能力,当每次代码 push 到代码仓库时,Github 便会发送一个请求,请求地址便是我们的微服务的地址,例如 https://example.com/webhook/project-a,然后服务器上就可以启动项目的自动化部署。

截图便是 Github 上设置项目的 Webhooks 的页面。「Payload URL」 处填入的便是我们的微服务地址。在 「Which events would you like to trigger this webhook?」 区域可以指定哪种事件会触发 Webhook,这里我们选择第一项,即每次有代码 push 进来时都会触发 Webhook。

本文以 Github 举例说明,其他代码平台如 Gitlab、Bitbucket 也都是有相关功能的,具体配置查看各个平台的文档即可。

实现微服务

本文不会介绍每一行代码的实现,不会介绍 Rust 语法的细枝末节,只介绍大致实现,核心代码给出具体说明,文末会给出完整代码的 Github 仓库。

项目初始化

类似于 Node.js 中的 npm,Rust 中用 cargo 来管理项目的工具。

bash 复制代码
# 新建 auto-deploy 项目
cargo new auto-deploy
cd auto-deploy

# 安装依赖
cargo add tokio
cargo add axum

这里安装了两个依赖:

  • tokio - Rust 只提供了异步编程所需的基本特性,例如 async/await 关键字,但运行这些有异步特性的代码需要一个异步运行时;目前为止,官方标准库中并没有异步运行时,而 tokio 便是一款由社区实现的、最为流行的异步运行时。
  • axum - Rust 中流行的 Web 框架之一,专注于提供高性能和易用性;底层基于 tokio 也使它具备高效的异步处理能力。

接着在 Cargo.toml 文件中,启用 tokio 依赖的所有特性:

toml 复制代码
[dependencies]
tokio = { version = "1.36.0", features = ["full"]}

下面就可以开始编码了。

异步运行时

src/main.rs 文件是我们项目的入口文件,对于其中的 fn main() {} 函数需要改写成下面的写法让我们 Web 服务支持异步运行。

rust 复制代码
// src/main.rs

#[test::main]
async fn main() {
		// 异步代码
}

#[test::main] 是 Tokio 提供的一个将主函数转换支持成异步运行时环境的宏(macro),类似于下面的代码:

rust 复制代码
fn main() {
    tokio::runtime::Runtime::new().unwrap().block_on(async {
        // 异步代码
    });
}

创建 Web Server:

rust 复制代码
#[tokio::main]
async fn main() {
    let tcp_listen = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    let handler = || async { "Hello World 👋" };
    let app = axum::Router::new().route("/", axum::routing::get(handler));
    axum::serve(tcp_listen, app).await.unwrap() // 注意这里没有分号,代表它是个 return
}

在浏览器中访问 http://127.0.0.1:3000 便能看到返回的 "Hello World 👋"

添加路由

本文将会以存在 ProjectA 和 ProjectB 两个需要自动化部署的项目为例子。

参照 route("/", axum::routing::get(handler)) ,添加两个处理 POST 请求的路由:

rust 复制代码
let app = axum::Router::new()
    .route("/", axum::routing::get(handler))
    .route("/webhook/project-a", axum::routing::post(deploy_project_a))
    .route("/webhook/project-b", axum::routing::post(deploy_project_b));

deploy_project_adeploy_project_bhandler 一样,都是实际的处理请求的异步函数。

正如流程图中介绍的,微服务在执行自动化部署项目时,主要的工作就是执行项目的部署脚本、收集日志、邮件推送结果。这里唯一的变量就是不同项目的部署脚本是不同的,并且在实践中,这个脚本通常是和项目放在一起的。

基于此,我们做一些代码封装。

rust 复制代码
// src/main.rs
use axum::{
    response::IntoResponse,
    routing::{get, post},
};

#[tokio::main]
async fn main() {
		// ...
    let app = axum::Router::new()
        .route("/", get(handler))
        .route("/webhook/project-a", post(|| async { handle_webhook("project-a").await }))
        .route("/webhook/project-b", post(|| async { handle_webhook("project-b").await }));
		// ...
}

async fn handle_webhook(project_name: &str) -> impl axum::response::IntoResponse {
    let script_path = if let Some(val) = get_script_path(project_name) {
        val
    } else {
        return (axum::http::StatusCode::BAD_REQUEST, "无法获取部署脚本").into_response();
    };
    let project_name = project_name.to_string();
    tokio::spawn(async move {
        // ...
        deploy(&project_name, &script_path).await;
    });
    (axum::http::StatusCode::OK, "开始部署项目...").into_response()
}

// 获取项目的部署脚本路径
fn get_script_path(project_name: &str) -> Option<String> {
    let cwd = std::env::current_dir().unwrap().display().to_string();
    match project_name {
        // 注意这里有 2 种格式的脚本,下面会介绍到
	     // 这里的路径只是示例,真正的路径根据需要部署的项目指定
        "project-a" => Some(cwd + "/scripts/deploy.sh"),
        "project-b" => Some(cwd + "/scripts/deploy.zx.mjs"),
        _ => None,
    }
}

async fn deploy(project_name: &str, script_path: &str) {
    // 执行部署脚本
}

函数 handle_webhook() 说明:

  1. 根据入参 project_name 匹配脚本路径,如果匹配不到就返回 400 Bad Request 的响应
  2. 使用 tokio::spawn() 创建一个新的异步任务,该任务的执行不会阻塞当前函数
  3. 函数 handle_webhook() 返回 200 OK 响应给 Github Server
  4. 等待 tokio 任务调度,再执行 2 中新创建的异步任务

之所以这么设计,是因为 Github Webhook 要求请求必须在 10秒内作出响应 ,而部署流程一系列步骤执行下来肯定会超时的,所以需要尽可能快的对请求作出响应,因此执行脚本放在 tokio::spawn() 新开辟的异步任务中。这个限制也决定了我们不能通过请求的响应给出脚本执行的结果,所以本文将会实现通过 Rust 发送 Email 通知项目构建结果。

shell / zx 脚本

在函数 get_script_path() 中看到有两种格式的脚本:

  • .sh 常用的 shell 脚本
  • .zx.mjs 使用 npm 库 zx 解释执行的脚本,你可以利用现有的 JavaScript 知识编写脚本

作为一名前端开发,在实际使用了 zx 一段时间后发现它确实特别好用 👍,所以对 zx 脚本的支持也作为这个微服务的一个功能,当然,使用它之前请确保 npm install -g zx

执行脚本

deploy() 函数是整个微服务的核心所在,它主要做了:执行构建脚本、记录构建日志、保存日志、消息推送构建结果。这里是该函数完整的代码实现:

rust 复制代码
// 每次记录日志时,都带上当前时间
fn write_log(owner: &mut String, content: &str) {
    let time = chrono::Local::now().format("%H:%M:%S").to_string();
    owner.push_str(format!("⏰ {}: {}\n", time, content).as_str())
}
    
async fn deploy(project_name: &str, script_path: &str) {
    let executer = if script_path.ends_with(".zx.mjs") {
        "zx"
    } else {
        "sh"
    };
    let output = std::process::Command::new(executer)
        .arg(script_path)
        .output()
        .expect("❌ 脚本执行失败");
    let mut email_payload = mail::EmailPayload {
        subject: Some("".into()),
        content: "".into(),
    };
    let deploy_success = output.status.success();
    let mut log = String::new();
    // 将脚本的输出写入变量 log
    if deploy_success {
        write_log(&mut log, &String::from_utf8_lossy(&output.stdout));
        email_payload.subject = Some(format!("✅ 应用[{}]部署成功", project_name));
    } else {
        write_log(&mut log, &String::from_utf8_lossy(&output.stderr));
        email_payload.subject = Some(format!("❌ 应用[{}]部署失败", project_name));
    };
    // 这里演示一下 format!() 中的命名占位符和"三元运算"
    write_log(
        &mut log,
        &format!(
            "构建结果: {res}",
            res = if deploy_success { "成功" } else { "失败" },
        ),
    );
    // 邮件内容接收 HTML 排版会更好
    email_payload.content = log.replace("\n", "<br>");
    // 发送邮件
    if let Err(e) = mail::send_email_to_myself(email_payload).await {
        write_log(&mut log, &format!("邮件发送失败: {}", e.to_string()));
    } else {
        write_log(&mut log, "邮件已成功发送");
    }
    // 将日志输出到本地文件
    let mut file = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open("./log.txt")
        .unwrap();
    file.write_all(format!("\n{}\n", log).as_bytes())
        .expect("本地文件写入失败");
}

邮件推送结果

代码中的 mail 是一个自行封装好的 mod,内容主要是借助 lettre crate 发送邮件,同时封装了一些类似邮件账户、邮件格式的配置细节,具体代码可以去文末的代码仓库中查看。

大家如果也有在服务器上发送 Email 的需求,建议优先使用 Gmail 或 Outlook,它们的文档、示例代码、问题讨论都要多一些。万一遇到了什么问题,都比较容易从中找到解决方案。

我选择的是使用 Outlook 账户发送的邮件,也分享一个我遇到的坑:如果你肯定账号密码都正确,但连接邮件服务器时还是提示认证失败,这时可以去查看一下账户的「最近活动」,如果它检测到了异常,就会拦截你的登录,在这个页面信任一下就可以: 查看最近活动

笔者的微服务真实推送的 Email

总结

要实现一款自动化部署项目的微服务,首先需要在代码平台中配置 Webhook,接着在我们的微服务中,根据请求内容,决定部署哪个项目。在执行部署脚本时,记录日志是很必要的,如果部署失败,有了日志可以帮我们快速定位问题。同时在构建结束之后,将结果通知到位可以让我们对自动化部署程序更放心------毕竟谁也不想因为部署失败而导致了服务故障但却没能及时得知。

再来看看实际运行起来后的内存占用,3.1MB! 这是写 Node.js 的人敢想的?🌝

写 Rust 的感受

  • 写 JavaScript 的我:写代码速度飞起,一边写一边 console,很多代码中的问题可能需要项目跑起来后才能发现。
  • 写 Rust 的我:永远在和 VS Code 的报红做斗争,但是只要代码能编译通过,项目跑起来后的问题是很少的,Rust 确实能给人带来安全感。再去看一下跑起来的应用的个位数的内存占用!莫大的满足感!😍
  • Option<T>Result<T, E> 让我格外关注程序中可能为空、可能会错误的值,当然也不能忽视掉它们,因为这会导致程序编译不通过,这让我们编写出来的程序潜在问题更少。
  • Rust 是个系统编程语言,相比 JavaScript 会有很多更底层的技术需要了解学习,这和前端开发不是相互独立的,融会贯通是必然的结果。
  • 很多文章会说 Rust 难学,我想说和 JavaScript / Python 比起来确实难,但和 C 比的话要简单多了;而且它诞生的晚,也意味着现代化,里面的很多语法、API 写起来是很舒服的。
  • Rust 中的函数式编程、 async/await 、闭包很多概念都让前端开发很熟悉。
  • 如果是写业务、追求速度,最好的选择还是 JavaScript。

相关链接

相关推荐
Martin -Tang6 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发6 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端