Rust 中实现 API 健康检查
当我准备将基于 Rust 的后端服务部署到 Kubernetes 集群时,我意识到我还没有配置我的后端服务以供 kubelet[1] 探测以进行活性[2]和就绪[3]检查。我能够通过添加一个/health
API 端点来满足此要求,该端点根据你的服务的当前状态以Ok
或ServiceUnavailable
HTTP 状态进行响应。
此/health
API 端点解决方案是 Health Check API 模式的实现[4],该模式用于检查 API 服务的健康状况。在像 Spring[5] 这样的 Web 框架中,像 Spring Actuator[6] 这样的嵌入式[7]解决方案可供你集成到 Spring 项目中。但是,在许多 Web 框架中,你必须自己构建此 Health Check API 行为。
在这篇博文中,我们将使用 actix-web[8] Web 框架实现健康检查 API 模式,该框架使用 sqlx[9] 连接到本地 PostgreSQL 数据库实例。
01 先决条件
在开始之前,请确保你的机器上安装了 Cargo[10] 和 Rust[11]。安装这些工具的最简单方法是使用 rustup[12]。
还要在你的机器上安装 Docker[13],以便我们可以轻松创建并连接到 PostgreSQL 数据库实例。
如果这是你第一次看到 Rust[14] 编程语言,我希望这篇博文能激励你更深入地了解这门有趣的静态类型语言和生态系统。
在 GitHub 上[15] 可以找到本文完整的源代码。
02 创建一个新的 Actix-Web 项目
打开你最喜欢的 命令行终端[16] 并通过cargo new my-service --bin
创建一个 Cargo 项目。
--bin
选项会告诉 Cargo 自动创建一个main.rs
文件,让 Cargo 知道这个项目不是一个库,而是会生成一个可执行文件。
接下来,我们能够通过运行以下命令来运行项目:cargo run
。运行此命令后,应该打印如下文本。
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/health-endpoint`
Hello, world!
是不是很容易?!
接下来,让我们创建并运行 PostgreSQL 实例。
03 运行 PostgreSQL
在使用 Docker Compose 创建 PostgreSQL 实例之前,我们需要创建一个用于创建数据库的初始 SQL 脚本。我们将以下内容添加到在项目根目录下的 db
目录的 init.sql
文件中。
SELECT 'CREATE DATABASE member'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'member')\gexec
此脚本将检查是否已存在名为“member”的数据库,如果不存在,它将为我们创建数据库。接下来,我们将以下 YAML 复制到docker-compose.yml
文件中并运行docker compose up
.
version: '3.1'
services:
my-service-db:
image: "postgres:11.5-alpine"
restart: always
volumes:
- my-service-volume:/var/lib/postgresql/data/
- ./db:/docker-entrypoint-initdb.d/
networks:
- my-service-network
ports:
- "5432:5432"
environment:
POSTGRES_HOST: localhost
POSTGRES_DB: my-service
POSTGRES_USER: root
POSTGRES_PASSWORD: postgres
volumes:
my-service-volume:
networks:
my-service-network:
在控制台窗口打印出一些彩色文本 🌈 之后,表示已经启动了 PostgreSQL。
现在我们已经确认服务已经运行,并且我们有一个本地运行的 PostgreSQL 实例,打开你最喜欢的文本编辑器[17]或IDE[18],并将我们的项目依赖项添加到我们的Cargo.toml
文件中。
[dependencies]
actix-web = "4.0.0-beta.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.5.7", features = [ "runtime-actix-native-tls", "postgres" ] }
对于sqlx
,我们希望确保在编译期间包含 “postgres” 功能,因此我们有 PostgreSQL 驱动程序来连接到我们的 PostgreSQL 数据库。接下来,我们要确保包含 runtime-actix-native-tls
特性,以便 sqlx 可以支持actix-web
使用 tokio[19] 运行时的框架。最后,包含serde
和serde_json
序列化我们的 Health Check API 响应主体,以供稍后在文章中使用。
注意:对于 Rust 的新手,你可能会想,“到底什么是 heck ?Actix 运行时?我认为 actix-web 只是 Rust 的一个 Web 框架。” 的确是,但不全是。由于 Rust 的设计没有考虑任何特定的运行时[20],因此你当前所在的问题域需要一个特定的运行时。有专门用于处理客户端/服务器通信需求的运行时,例如 Tokio[21],一种流行的事件驱动,非- 阻塞 I/O 运行时。Actix[22] 是 actix-web 背后的底层运行时,是一个构建在 tokio 运行时之上的基于 actor 的[23]消息传递框架。
所以,现在我们已经添加了依赖项,继续创建我们的actix-web
服务。为此,我们用以下 Rust 代码替换src/main.rs
文件中的内容:
use actix_web::{web, App, HttpServer, HttpResponse};
async fn get_health_status() -> HttpResponse {
HttpResponse::Ok()
.content_type("application/json")
.body("Healthy!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/health", web::get().to(get_health_status))
// ^ Our new health route points to the get_health_status handler
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
上面的代码为我们提供了一个在端口 8080
上运行的 HTTP 服务器和一个 /health
端点,它始终返回 Ok
这个 HTTP 响应状态代码。
回到终端,运行cargo run
启动服务。在新 tab 终端中,继续运行 curl -i localhost:8080/health
并查看你收到如下响应:
$ curl -i localhost:8080/health
HTTP/1.1 200 OK
content-length: 8
content-type: application/json
date: Wed, 22 Sep 2021 17:16:47 GMT
Healthy!%
现在我们已经启动并运行了基本的健康检查 API 端点,现在更改我们的健康 API 的行为,当与 PostgreSQL
数据库的连接处于活动状态时让其返回 OK 这个 HTTP 响应状态代码。为此,我们需要先使用sqlx
建立一个数据库连接。
04 创建数据库连接
在我们可以使用 sqlx 的 connect[24] 方法建立数据库连接之前,我们需要创建一个数据库连接字符串,格式类似
,与我们本地的 PostgreSQL 设置相匹配。
此外,与其对我们的数据库连接字符串进行硬编码,不如通过一个名为 DATABASE_URL
的环境变量对其进行配置[25],DATABASE_URL
在每次cargo run
调用之前添加该变量,如下所示:
DATABASE_URL=postgres://root:postgres@localhost:5432/member?sslmode=disable cargo run
有了 DATABASE_URL
环境变量,我们在main
函数中添加一行来获取我们新导出的环境变量。
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let database_url = std::env::var("DATABASE_URL").expect("Should set 'DATABASE_URL'");
...
接下来,在 main
函数中编写更多代码来创建数据库连接。
...
let db_conn = PgPoolOptions::new()
.max_connections(5)
.connect_timeout(Duration::from_secs(2))
.connect(database_url.as_str()) // <- Use the str version of database_url variable.
.await
.expect("Should have created a database connection");
...
在我们可以将数据库连接传递给我们的健康端点处理程序之前,我们首先需要创建一个struct
代表我们服务的共享可变状态的 。Actix-web 使我们能够在路由之间共享我们的数据库连接,这样我们就不会在每个请求上创建新的数据库连接,这是一项高成本的操作,并且会真正降低我们的服务性能。
为了实现这一点,我们需要创建一个 Rust struct
(在我们的main
函数上方),名为 AppState
,有一个字段 db_conn
,对数据库连接的引用。
...
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
...
struct AppState {
db_conn: Pool
}
现在,在我们的db_conn
实例化之下,将创建一个包装在web::Data
包装器中的 AppState 数据对象。该web::Data
包装在请求处理程序中访问我们的 AppState。
...
let app_state = web::Data::new(AppState {
db_conn: db_conn
});
...
最后,我们设置 App 的app_data
为我们的克隆app_state
的变量,并使用move
语句更新我们的HttpServer::new
闭包。
...
let app_state = web::Data::new(AppState {
db_conn: db_conn
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone()) // <- cloned app_state variable
.route("/health", web::get().to(get_health_status))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
如果我们不克隆app_state
变量,Rust 会提出我们的app_state
变量不是在我们的闭包内部创建的,并且 Rust 无法保证app_state
在调用时不会被销毁。有关更多信息,请查看 Rust Ownership[26] 和 Copy trait[27] 文档。
到目前为止,我们的服务代码应该如下所示:
use actix_web::{web, App, HttpServer, HttpResponse};
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
async fn get_health_status() -> HttpResponse {
HttpResponse::Ok()
.content_type("application/json")
.body("Healthy!")
}
struct AppState {
db_conn: Pool
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let database_url = std::env::var("DATABASE_URL").expect("Should set 'DATABASE_URL'");
let db_conn = PgPoolOptions::new()
.max_connections(5)
.connect_timeout(Duration::from_secs(2))
.connect(database_url.as_str())
.await
.expect("Should have created a database connection");
let app_state = web::Data::new(AppState {
db_conn: db_conn
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.route("/health", web::get().to(get_health_status))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
现在我们已经将app_state
对象,包含我们的数据库连接传递到我们的App
实例中,继续更新我们的get_health_status
函数以检查我们的数据库连接是否有效。
数据库连接检查
为了从我们的get_health_status
函数中捕获AppState
数据,我们需要添加一个Data
参数到get_health_status
函数中。
async fn get_health_status(data: web::Data) -> HttpResponse {
...
接下来,让我们编写一个轻量级的 PostgreSQL 查询 SELECT 1
来检查我们的数据库连接。
async fn get_health_status(data: web::Data) -> HttpResponse {
let is_database_connected = sqlx::query("SELECT 1")
.fetch_one(&data.db_conn)
.await
.is_ok();
...
然后,我们更新HttpResponse
响应以在我们的数据库连接时返回一个Ok
,当它没有连接时返回ServiceUnavailable
。此外,为了调试的目的,我们有一个更有用的响应主体,不是简单的 healthy
或者not healthy
,使用 serde_json
序列化 Ruststruct
,描述为什么我们的健康检查是成功还是失败。
...
if is_database_connected {
HttpResponse::Ok()
.content_type("application/json")
.body(serde_json::json!({ "database_connected": is_database_connected }).to_string())
} else {
HttpResponse::ServiceUnavailable()
.content_type("application/json")
.body(serde_json::json!({ "database_connected": is_database_connected }).to_string())
}
}
最后,我们使用以下cargo run
命令运行我们的服务:
DATABASE_URL=postgres://root:postgres@localhost:5432/member?sslmode=disable cargo run
打开另一个终端选项卡并运行以下curl
命令:
curl -i localhost:8080/health
应该返回以下响应:
HTTP/1.1 200 OK
content-length: 27
content-type: application/json
date: Tue, 12 Oct 2021 15:56:00 GMT
{"database_connected":true}%
如果我们通过docker compose stop
关闭我们的数据库,那么两秒钟后,当你再次调用以上 curl
命令时,你会看到一个ServiceUnavailable
的 HTTP 响应。
HTTP/1.1 503 Service Unavailable
content-length: 28
content-type: application/json
date: Tue, 12 Oct 2021 16:07:03 GMT
{"database_connected":false}%
05 结论
我希望这篇博文能成为实现 Health Check API 模式的有用指南。你可以将更多信息应用到您的/health
API 端点,例如,在适用的情况下,当前用户的数量、缓存连接检查等。需要任何信息来确保你的后端服务看起来“健康”。这因服务而异。
原文链接:https://dev.to/tjmaynes/implementing-the-health-check-api-pattern-with-rust-29ll
参考资料
kubelet: https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/
[2]活性: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-startup-probes
[3]就绪: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes
[4]Health Check API 模式的实现: https://microservices.io/patterns/observability/health-check-api.html
[5]Spring: https://spring.io/
[6]Spring Actuator: https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html
[7]嵌入式: https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html
[8]actix-web: https://actix.rs/
[9]sqlx: https://github.com/launchbadge/sqlx
[10]Cargo: https://doc.rust-lang.org/cargo/getting-started/installation.html
[11]Rust: https://www.rust-lang.org/
[12]rustup: https://rustup.rs/
[13]Docker: https://docs.docker.com/get-docker/
[14]Rust: https://rust-lang.org/
[15]GitHub 上: https://github.com/tjmaynes/health-check-rust
[16]命令行终端: https://github.com/alacritty/alacritty
[17]文本编辑器: https://code.visualstudio.com/
[18]IDE: https://www.jetbrains.com/idea/
[19]tokio: https://tokio.rs/
[20]运行时: https://en.wikipedia.org/wiki/Runtime_system
[21]Tokio: https://docs.rs/tokio/1.12.0/tokio/
[22]Actix: https://docs.rs/actix/
[23]基于 actor 的: https://en.wikipedia.org/wiki/Actor_model
[24]connect: https://docs.rs/sqlx/0.5.7/sqlx/postgres/struct.PgConnection.html#method.connect
[25]配置: https://12factor.net/config
[26]Rust Ownership: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
[27]Copy trait: https://hashrust.com/blog/moves-copies-and-clones-in-rust/
我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。
坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio