Rust 中實(shí)現(xiàn) API 健康檢查
當(dāng)我準(zhǔn)備將基于 Rust 的后端服務(wù)部署到 Kubernetes 集群時,我意識到我還沒有配置我的后端服務(wù)以供 kubelet[1] 探測以進(jìn)行活性[2]和就緒[3]檢查。我能夠通過添加一個/healthAPI 端點(diǎn)來滿足此要求,該端點(diǎn)根據(jù)你的服務(wù)的當(dāng)前狀態(tài)以Ok或ServiceUnavailableHTTP 狀態(tài)進(jìn)行響應(yīng)。
此/healthAPI 端點(diǎn)解決方案是 Health Check API 模式的實(shí)現(xiàn)[4],該模式用于檢查 API 服務(wù)的健康狀況。在像 Spring[5] 這樣的 Web 框架中,像 Spring Actuator[6] 這樣的嵌入式[7]解決方案可供你集成到 Spring 項目中。但是,在許多 Web 框架中,你必須自己構(gòu)建此 Health Check API 行為。
在這篇博文中,我們將使用 actix-web[8] Web 框架實(shí)現(xiàn)健康檢查 API 模式,該框架使用 sqlx[9] 連接到本地 PostgreSQL 數(shù)據(jù)庫實(shí)例。
01 先決條件
在開始之前,請確保你的機(jī)器上安裝了 Cargo[10] 和 Rust[11]。安裝這些工具的最簡單方法是使用 rustup[12]。
還要在你的機(jī)器上安裝 Docker[13],以便我們可以輕松創(chuàng)建并連接到 PostgreSQL 數(shù)據(jù)庫實(shí)例。
如果這是你第一次看到 Rust[14] 編程語言,我希望這篇博文能激勵你更深入地了解這門有趣的靜態(tài)類型語言和生態(tài)系統(tǒng)。
在 GitHub 上[15] 可以找到本文完整的源代碼。
02 創(chuàng)建一個新的 Actix-Web 項目
打開你最喜歡的 命令行終端[16] 并通過cargo new my-service --bin 創(chuàng)建一個 Cargo 項目。
--bin選項會告訴 Cargo 自動創(chuàng)建一個main.rs文件,讓 Cargo 知道這個項目不是一個庫,而是會生成一個可執(zhí)行文件。
接下來,我們能夠通過運(yùn)行以下命令來運(yùn)行項目:cargo run。運(yùn)行此命令后,應(yīng)該打印如下文本。
????Finished?dev?[unoptimized?+?debuginfo]?target(s)?in?0.00s
?????Running?`target/debug/health-endpoint`
Hello,?world!
是不是很容易?!
接下來,讓我們創(chuàng)建并運(yùn)行 PostgreSQL 實(shí)例。
03 運(yùn)行 PostgreSQL
在使用 Docker Compose 創(chuàng)建 PostgreSQL 實(shí)例之前,我們需要創(chuàng)建一個用于創(chuàng)建數(shù)據(jù)庫的初始 SQL 腳本。我們將以下內(nèi)容添加到在項目根目錄下的 db 目錄的 init.sql 文件中。
SELECT?'CREATE?DATABASE?member'
WHERE?NOT?EXISTS?(SELECT?FROM?pg_database?WHERE?datname?=?'member')\gexec
此腳本將檢查是否已存在名為“member”的數(shù)據(jù)庫,如果不存在,它將為我們創(chuàng)建數(shù)據(jù)庫。接下來,我們將以下 YAML 復(fù)制到docker-compose.yml文件中并運(yùn)行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:
在控制臺窗口打印出一些彩色文本 ?? 之后,表示已經(jīng)啟動了 PostgreSQL。
現(xiàn)在我們已經(jīng)確認(rèn)服務(wù)已經(jīng)運(yùn)行,并且我們有一個本地運(yùn)行的 PostgreSQL 實(shí)例,打開你最喜歡的文本編輯器[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 驅(qū)動程序來連接到我們的 PostgreSQL 數(shù)據(jù)庫。接下來,我們要確保包含 ?runtime-actix-native-tls 特性,以便 sqlx 可以支持actix-web使用 tokio[19] 運(yùn)行時的框架。最后,包含serde和serde_json序列化我們的 Health Check API 響應(yīng)主體,以供稍后在文章中使用。
注意:對于 Rust 的新手,你可能會想,“到底什么是 heck ?Actix 運(yùn)行時?我認(rèn)為 actix-web 只是 Rust 的一個 Web 框架。” 的確是,但不全是。由于 Rust 的設(shè)計沒有考慮任何特定的運(yùn)行時[20],因此你當(dāng)前所在的問題域需要一個特定的運(yùn)行時。有專門用于處理客戶端/服務(wù)器通信需求的運(yùn)行時,例如 Tokio[21],一種流行的事件驅(qū)動,非- 阻塞 I/O 運(yùn)行時。Actix[22] 是 actix-web 背后的底層運(yùn)行時,是一個構(gòu)建在 tokio 運(yùn)行時之上的基于 actor 的[23]消息傳遞框架。
所以,現(xiàn)在我們已經(jīng)添加了依賴項,繼續(xù)創(chuàng)建我們的actix-web服務(wù)。為此,我們用以下 Rust 代碼替換src/main.rs文件中的內(nèi)容:
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 上運(yùn)行的 HTTP 服務(wù)器和一個 /health 端點(diǎn),它始終返回 Ok 這個 HTTP 響應(yīng)狀態(tài)代碼。
回到終端,運(yùn)行cargo run 啟動服務(wù)。在新 tab 終端中,繼續(xù)運(yùn)行 curl -i localhost:8080/health 并查看你收到如下響應(yīng):
$?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!%
現(xiàn)在我們已經(jīng)啟動并運(yùn)行了基本的健康檢查 API 端點(diǎn),現(xiàn)在更改我們的健康 API 的行為,當(dāng)與 PostgreSQL 數(shù)據(jù)庫的連接處于活動狀態(tài)時讓其返回 OK 這個 HTTP 響應(yīng)狀態(tài)代碼。為此,我們需要先使用sqlx建立一個數(shù)據(jù)庫連接。
04 創(chuàng)建數(shù)據(jù)庫連接
在我們可以使用 sqlx 的 connect[24] 方法建立數(shù)據(jù)庫連接之前,我們需要創(chuàng)建一個數(shù)據(jù)庫連接字符串,格式類似 ,與我們本地的 PostgreSQL 設(shè)置相匹配。
此外,與其對我們的數(shù)據(jù)庫連接字符串進(jìn)行硬編碼,不如通過一個名為 ?DATABASE_URL 的環(huán)境變量對其進(jìn)行配置[25],DATABASE_URL 在每次cargo run調(diào)用之前添加該變量,如下所示:
DATABASE_URL=postgres://root:postgres@localhost:5432/member?sslmode=disable?cargo?run
有了 DATABASE_URL 環(huán)境變量,我們在main函數(shù)中添加一行來獲取我們新導(dǎo)出的環(huán)境變量。
#[actix_web::main]
async?fn?main()?->?std::io::Result<()>?{
????let?database_url?=?std::env::var("DATABASE_URL").expect("Should?set?'DATABASE_URL'");
????...
接下來,在 main 函數(shù)中編寫更多代碼來創(chuàng)建數(shù)據(jù)庫連接。
...
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");
...
在我們可以將數(shù)據(jù)庫連接傳遞給我們的健康端點(diǎn)處理程序之前,我們首先需要創(chuàng)建一個struct代表我們服務(wù)的共享可變狀態(tài)的 。Actix-web 使我們能夠在路由之間共享我們的數(shù)據(jù)庫連接,這樣我們就不會在每個請求上創(chuàng)建新的數(shù)據(jù)庫連接,這是一項高成本的操作,并且會真正降低我們的服務(wù)性能。
為了實(shí)現(xiàn)這一點(diǎn),我們需要創(chuàng)建一個 Rust struct(在我們的main函數(shù)上方),名為 AppState,有一個字段 db_conn,對數(shù)據(jù)庫連接的引用。
...
use?sqlx::{Pool,?Postgres,?postgres::PgPoolOptions};
...
struct?AppState?{
????db_conn:?Pool
}
現(xiàn)在,在我們的db_conn實(shí)例化之下,將創(chuàng)建一個包裝在web::Data包裝器中的 AppState 數(shù)據(jù)對象。該web::Data包裝在請求處理程序中訪問我們的 AppState。
...
let?app_state?=?web::Data::new(AppState?{
??db_conn:?db_conn
});
...
最后,我們設(shè)置 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變量不是在我們的閉包內(nèi)部創(chuàng)建的,并且 Rust 無法保證app_state在調(diào)用時不會被銷毀。有關(guān)更多信息,請查看 Rust Ownership[26] 和 Copy trait[27] 文檔。
到目前為止,我們的服務(wù)代碼應(yīng)該如下所示:
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
}
現(xiàn)在我們已經(jīng)將app_state對象,包含我們的數(shù)據(jù)庫連接傳遞到我們的App實(shí)例中,繼續(xù)更新我們的get_health_status函數(shù)以檢查我們的數(shù)據(jù)庫連接是否有效。
數(shù)據(jù)庫連接檢查
為了從我們的get_health_status函數(shù)中捕獲AppState數(shù)據(jù),我們需要添加一個Data參數(shù)到get_health_status函數(shù)中。
async?fn?get_health_status(data:?web::Data)?->?HttpResponse?{
????...
接下來,讓我們編寫一個輕量級的 PostgreSQL 查詢 SELECT 1 來檢查我們的數(shù)據(jù)庫連接。
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響應(yīng)以在我們的數(shù)據(jù)庫連接時返回一個Ok,當(dāng)它沒有連接時返回ServiceUnavailable。此外,為了調(diào)試的目的,我們有一個更有用的響應(yīng)主體,不是簡單的 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命令運(yùn)行我們的服務(wù):
DATABASE_URL=postgres://root:postgres@localhost:5432/member?sslmode=disable?cargo?run
打開另一個終端選項卡并運(yùn)行以下curl命令:
curl?-i?localhost:8080/health
應(yīng)該返回以下響應(yīng):
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 關(guān)閉我們的數(shù)據(jù)庫,那么兩秒鐘后,當(dāng)你再次調(diào)用以上 curl命令時,你會看到一個ServiceUnavailable的 HTTP 響應(yīng)。
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 結(jié)論
我希望這篇博文能成為實(shí)現(xiàn) Health Check API 模式的有用指南。你可以將更多信息應(yīng)用到您的/healthAPI 端點(diǎn),例如,在適用的情況下,當(dāng)前用戶的數(shù)量、緩存連接檢查等。需要任何信息來確保你的后端服務(wù)看起來“健康”。這因服務(wù)而異。
原文鏈接: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 模式的實(shí)現(xiàn): 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]運(yùn)行時: 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,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標(biāo)準(zhǔn)庫》等。
堅持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
