Rust 中實現 API 健康檢查

當我準備將基於 Rust 的後端服務部署到 Kubernetes 集羣時,我意識到我還沒有配置我的後端服務以供 kubelet[1] 探測以進行活性 [2] 和就緒 [3] 檢查。我能夠通過添加一個/healthAPI 端點來滿足此要求,該端點根據你的服務的當前狀態以OkServiceUnavailableHTTP 狀態進行響應。

/healthAPI 端點解決方案是 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] _運行時_的框架。最後,包含serdeserde_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] 方法建立數據庫連接之前,我們需要創建一個數據庫連接字符串,格式類似 <database-type>://<user>:<password>@<db-host>:<db-port>/<db-name>,與我們本地的 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<Postgres>
}

現在,在我們的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<Postgres>
}

#[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<AppState>參數到get_health_status函數中。

async fn get_health_status(data: web::Data<AppState>) -> HttpResponse {
    ...

接下來,讓我們編寫一個輕量級的 PostgreSQL 查詢 SELECT 1 來檢查我們的數據庫連接。

async fn get_health_status(data: web::Data<AppState>) -> 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 模式的有用指南。你可以將更多信息應用到您的/healthAPI 端點,例如,在適用的情況下,當前用戶的數量、緩存連接檢查等。需要任何信息來確保你的後端服務看起來 “健康”。這因服務而異。

原文鏈接:https://dev.to/tjmaynes/implementing-the-health-check-api-pattern-with-rust-29ll

參考資料

[1]

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/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wQ6JeHgy0nvhwHyfnLgRQw