健康检查
服务活着不等于服务可用——探针说了算
这是什么
服务启动了,端口监听了——但数据库连不上、Redis 挂了、磁盘满了,没人知道,直到用户报错。
健康检查就是给每个关键依赖插一根探针。IHealthCheckService 一声令下,所有探针并发执行,几秒内告诉你整个系统是绿是黄还是红。
你的服务 ─── 探针 ─── 数据库 ✅ Healthy
─── 探针 ─── Redis ⚠️ Degraded(响应超过 1 秒)
─── 探针 ─── 磁盘 ❌ Unhealthy(不足 100MB)
聚合结果 → Degraded不用等到用户发现。容器平台(Kubernetes、Nomad)定期调 checkHealth(),不健康就自动切流量——这是基础设施层的最后一道防线。
快速开始
引入 soulsoft_extensions_healthchecks 和 soulsoft_extensions_injection 后,3 步搞定:
import soulsoft_extensions_injection.*
import soulsoft_extensions_healthchecks.*
main(): Int64 {
// 1. 注册:启动健康检查 + 添加探针
let services = ServiceCollection()
services.addHealthChecks()
.addCheck("database") {
HealthCheckResult.healthy(description: "连接正常")
}
let provider = services.build()
// 2. 拿到服务
let svc = provider.getOrThrow<IHealthCheckService>()
// 3. 执行——所有探针并发跑,返回聚合报告
let report = svc.checkHealth()
println("状态:${report.status},耗时:${report.totalDuration.toMilliseconds()}ms")
return 0
}运行输出:
状态:Healthy,耗时:1ms整个流程只有三个角色:
| 步骤 | 做什么 | 用的类型 |
|---|---|---|
| 注册 | 启动健康检查,链式添加探针 | addHealthChecks() → addCheck(...) |
| 获取 | 从容器解析服务 | IHealthCheckService |
| 执行 | 并发执行探针,返回报告 | checkHealth() → HealthReport |
下面逐一展开。
核心设计
健康检查系统的设计思路:你只定义"查什么",框架接管"怎么查"。
ServiceCollection
│
▼
addHealthChecks()
├── 注册 HealthCheckServiceOptions(IOptions 模式)
├── 注册 IHealthCheckService → HealthCheckService(Singleton)
└── 返回 HealthChecksBuilder
│
├── .addCheck("db", { ... }) ← Lambda 探针
├── .addCheck("redis", { ... })
└── .addCheck<DiskHealthCheck>("disk") ← 类型探针
执行时:
IHealthCheckService.checkHealth()
│
├── 从 Options 读取所有注册的探针
├── spawn 并发执行每个探针(各自独立的 DI Scope)
├── timeout 超时自动返回 Unhealthy
└── 聚合所有结果 → HealthReport- 注册期:探针挂到
HealthChecksBuilder,存入HealthCheckServiceOptions,什么都不执行 - 执行期:
checkHealth()一声令下,所有探针spawn并发跑,各自有超时兜底 - 聚合期:取所有探针中最差的状态作为最终结果——只要有一个
Unhealthy,整体就是Unhealthy - 隔离:每个探针运行在独立的 DI Scope 中——一个探针打开数据库连接,另一个探针不会共享它
注册健康检查
addHealthChecks() 返回 HealthChecksBuilder,支持两种注册方式:
方式一:Lambda 注册(简单探针)
一行逻辑的探针,直接传闭包:
let services = ServiceCollection()
services.addHealthChecks()
.addCheck("ping") {
HealthCheckResult.healthy(description: "ping 正常")
}闭包签名是 () -> HealthCheckResult,框架内部用 DelegateHealthCheck 包装成 IHealthCheck。
带标签和超时的完整写法:
services.addHealthChecks()
.addCheck("redis",
timeout: Duration.second * 3,
tags: ["storage", "critical"]) {
// 探针逻辑:连接 Redis 并 PING
HealthCheckResult.healthy(description: "PONG")
}方式二:IHealthCheck 类型注册(复杂探针)
探针逻辑复杂、需要从 DI 容器注入依赖时,推荐实现 IHealthCheck 接口:
import soulsoft_extensions_options.*
class RedisOptions {
public var connectionString: String = "localhost:6379"
}
class RedisHealthCheck <: IHealthCheck {
private let options: RedisOptions
public init(options: IOptions<RedisOptions>) {
this.options = options.value
}
public func check(context: HealthCheckContext): HealthCheckResult {
// 实际项目中:连接 Redis 并 PING
HealthCheckResult.healthy(description: "Redis PONG")
}
}
// 注册选项 → 注册探针,框架通过 DI 容器自动创建实例
services.configure<RedisOptions> { options =>
options.connectionString = "redis-cluster:6379"
}
services.addHealthChecks()
.addCheck<RedisHealthCheck>("redis", tags: ["storage"])ActivatorUtilities.createInstance<T>(sp) 从 DI 容器解析构造函数参数,自动创建 RedisHealthCheck 实例。
注册方式速查
| 场景 | 用哪种 | 写法 |
|---|---|---|
| 简单检查 | Lambda | .addCheck("name") { HealthCheckResult.healthy() } |
| 需要 DI 注入 | IHealthCheck 类型 | .addCheck<MyHealthCheck>("name") |
| 带标签 | 两种都支持 | addCheck("name", tags: [...]) { ... } |
| 带超时 | 两种都支持 | addCheck("name", timeout: Duration.second * 5, tags: [...]) { ... } |
健康状态
HealthStatus 是三级枚举,数值越小状态越差:
| 状态 | 数值 | 含义 | 什么时候用 |
|---|---|---|---|
Unhealthy | 0 | 挂了 | 数据库连不上、Redis 超时、磁盘满了 |
Degraded | 1 | 能用但不太行 | 响应时间超过阈值、主节点挂了切到备节点 |
Healthy | 2 | 一切正常 | 所有依赖正常响应 |
聚合规则:取所有探针中最差的。一个 Unhealthy 就拉红全局。
检查结果
HealthCheckResult 通过静态工厂方法创建,不需要 new:
// 健康——一切正常
HealthCheckResult.healthy(description: "连接正常")
// 降级——能用但有隐患
HealthCheckResult.degraded(
description: "响应时间 3 秒,超过阈值 1 秒"
)
// 不健康——挂了,附带异常
HealthCheckResult.unhealthy(
description: "无法连接到数据库",
exception: Some(ex)
)每个工厂方法都能附带 data: HashMap<String, Object>,往报告里塞自定义结构化数据:
let data = HashMap<String, Object>()
data["free_mb"] = 86
data["threshold_mb"] = 100
let result = HealthCheckResult.unhealthy(
description: "磁盘不足",
data: data
)工厂方法速查
| 需求 | 方法 |
|---|---|
| 健康 | HealthCheckResult.healthy(description: "ok") |
| 健康+数据 | HealthCheckResult.healthy(description: "ok", data: map) |
| 降级 | HealthCheckResult.degraded(description: "slow") |
| 降级+异常 | HealthCheckResult.degraded(description: "slow", exception: Some(ex)) |
| 降级+数据 | HealthCheckResult.degraded(description: "slow", data: map) |
| 不健康 | HealthCheckResult.unhealthy(description: "down") |
| 不健康+异常 | HealthCheckResult.unhealthy(description: "down", exception: Some(ex)) |
| 不健康+数据 | HealthCheckResult.unhealthy(description: "down", data: map) |
标签与过滤
探针多起来后,不同场景需要查不同的子集。"启动就绪"只关心关键依赖,"详细诊断"连缓存和队列一起查。标签就是用来区分这些场景的。
注册时打标签:
let services = ServiceCollection()
services.addHealthChecks()
.addCheck("database", tags: ["critical", "storage"]) {
HealthCheckResult.healthy(description: "连接正常")
}
.addCheck("redis", tags: ["storage"]) {
HealthCheckResult.healthy(description: "PONG")
}
.addCheck("disk", tags: ["critical"]) {
HealthCheckResult.healthy(description: "磁盘充足")
}执行时按标签过滤:
let provider = services.build()
let svc = provider.getOrThrow<IHealthCheckService>()
// 只检查"critical"标签的探针——启动就绪检查
let readyReport = svc.checkHealth { reg =>
reg.tags.contains("critical")
}
// 只执行 database 和 disk
// 检查全部
let fullReport = svc.checkHealth()
// 执行 database、redis、diskcheckHealth() 无参版本执行全部;checkHealth(predicate) 按条件筛选:
| 场景 | 写法 |
|---|---|
| 全部执行 | svc.checkHealth() |
| 按标签过滤 | svc.checkHealth { reg => reg.tags.contains("critical") } |
| 按名称过滤 | svc.checkHealth { reg => reg.name == "database" } |
超时控制
每个探针可以设独立超时。超时后框架调用 Future.cancel() 发送取消信号,探针主动检查 Thread.currentThread.hasPendingCancellation 并退出:
services.addHealthChecks()
.addCheck("database",
timeout: Duration.second * 5,
tags: ["critical"]) {
var connected = false
var retries = 0
while (!connected && retries < 3) {
// 检查是否已被取消——不检查就一直跑,设超时也没用
if (Thread.currentThread.hasPendingCancellation) {
return HealthCheckResult.unhealthy(description: "健康检查超时被取消")
}
// 尝试连接数据库...
retries++
}
if (connected) {
HealthCheckResult.healthy(description: "连接正常")
} else {
HealthCheckResult.unhealthy(description: "重试${retries}次后仍无法连接")
}
}不设超时时 timeout 为 Duration.Zero,表示永不超时。
两个要点:
- 取消是协作式的——框架通过
Future.cancel()发信号,线程不会强杀。探针内部必须在循环、阻塞操作前检查hasPendingCancellation - 生产环境每个探针都设超时——避免一个慢查询把整个健康检查卡死
读取报告
checkHealth() 返回 HealthReport,包含聚合状态和每个探针的详细结果:
let provider = services.build()
let svc = provider.getOrThrow<IHealthCheckService>()
let report = svc.checkHealth()
// 聚合状态——取最差的那个
println("整体:${report.status}")
// 总耗时——所有探针耗时之和
println("总耗时:${report.totalDuration.toMilliseconds()}ms")
// 逐个探针的详情
for ((name, entry) in report.entries) {
println("${name}:${entry.status}(${entry.duration.toMilliseconds()}ms)")
match (entry.description) {
case Some(let desc): println(" 描述:${desc}")
case None: {}
}
match (entry.exception) {
case Some(let ex): println(" 异常:${ex.message}")
case None: {}
}
}HealthReportEntry 包含六个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
status | HealthStatus | 该探针的健康状态 |
duration | Duration | 该探针的执行耗时 |
description | ?String | 可选的描述信息 |
exception | ?Exception | 可选的异常对象 |
tags | Array<String> | 该探针的标签集合 |
data | ReadOnlyMap<String, Object> | 探针附带的结构化数据 |
最佳实践
三步走:实现 IHealthCheck → 扩展 HealthChecksBuilder → 一行链式注册。
import soulsoft_extensions_healthchecks.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_options.*
import std.net.*
import std.time.*
// 1. 定义选项类——探针配置收敛到一个强类型里
class TcpProbeOptions {
public var host: String = ""
public var port: UInt16 = 0
}
// 2. 实现 IHealthCheck——通过 IOptionsMonitor 按名称获取配置,避免多次注册互相覆盖
class TcpHealthCheck <: IHealthCheck {
private let name: String
private let monitor: IOptionsMonitor<TcpProbeOptions>
public init(name: String, monitor: IOptionsMonitor<TcpProbeOptions>) {
this.name = name
this.monitor = monitor
}
public func check(context: HealthCheckContext): HealthCheckResult {
// 按探针名称取出对应配置,不同探针的配置互不干扰
let options = monitor.get(name)
try (socket = TcpSocket(options.host, options.port)) {
if (Thread.currentThread.hasPendingCancellation) {
return HealthCheckResult.unhealthy(description: "探测被取消")
}
socket.connect()
HealthCheckResult.healthy(description: "${options.host}:${options.port} 可达")
} catch (ex: Exception) {
HealthCheckResult.unhealthy(
description: "无法连接 ${options.host}:${options.port}",
exception: Some(ex))
}
}
}
// 3. 扩展 HealthChecksBuilder——
// 用命名选项(`configure<T>(name)`)注册配置,每个探针的名称作为选项名,
// 避免多次 addTcpCheck 共用一个 TcpProbeOptions 类型导致配置互相覆盖。
extend HealthChecksBuilder {
public func addTcpCheck(name: String,
timeout!: Duration = Duration.second * 5,
tags!: Array<String> = [],
configureOptions: (TcpProbeOptions) -> Unit): HealthChecksBuilder {
// 以探针名称作为选项名——不同探针的 TcpProbeOptions 实例相互隔离
// 用选项模式,将来可以无缝切换到配置文件绑定,
_services.configure<TcpProbeOptions>(name, configureOptions)
this.addCheck(HealthCheckRegistration(name, timeout, tags) { sp =>
TcpHealthCheck(name, sp.getOrThrow<IOptionsMonitor<TcpProbeOptions>>())
})
}
}
// 4. 使用——和其他 addCheck 一样链式调用
let services = ServiceCollection()
services.addHealthChecks()
.addTcpCheck("postgresql",
timeout: Duration.second * 5,
tags: ["critical"]) { options =>
options.host = "127.0.0.1"
options.port = 5432
}
.addTcpCheck("redis",
timeout: Duration.second * 3,
tags: ["storage"]) { options =>
options.host = "localhost"
options.port = 6379
}
let provider = services.build()
let svc = provider.getOrThrow<IHealthCheckService>()
// 启动就绪检查
let startup = svc.checkHealth { reg =>
reg.tags.contains("critical")
}
if (startup.status == HealthStatus.Unhealthy) {
println("关键依赖不健康,拒绝启动")
return 1
}
// 全量检查
println("==== 健康检查 ====")
let full = svc.checkHealth()
for ((name, entry) in full.entries) {
match (entry.description) {
case Some(let desc): println("${name}:${entry.status} — ${desc}")
case None: println("${name}:${entry.status}")
}
}分层检查策略:
| 阶段 | 检查范围 | 目的 |
|---|---|---|
| 启动就绪 | critical 标签 | 关键依赖不通过 → 拒绝启动 |
| 存活探测 | critical 标签 | 定期轻量检查,决定是否切流量 |
| 详细诊断 | 全部探针 | 运维排查,看每个依赖的详细状态 |
addTcpCheck 这个名字可以换成任何语义——addRedisCheck、addDatabaseCheck、addKafkaCheck。模式不变:实现 IHealthCheck + extend HealthChecksBuilder,调用方永远是一行链式注册。
常见问题
健康检查和直接 try-catch 有什么区别?
try-catch 只能抓到当前这次调用失败。健康检查的探针是主动探测——每隔一段时间确认下游还活着。更重要的是,容器平台(Kubernetes、Nomad)依赖健康检查接口来决定是否把流量切走。try-catch 没法替代这个。
为什么每个探针跑在独立的 Scope 里?
防止一个探针污染另一个。比如 DatabaseHealthCheck 打开了一个连接,RedisHealthCheck 不应该共享这个连接——各自在独立 Scope 中创建、执行、释放。
超时后探针会怎样?
超时的探针对应的 spawn 协程会被 cancel(),立即终止执行,返回 HealthStatus.Unhealthy。不会无限等待拖垮整体检查。
多个探针的执行顺序?
并发执行,不分先后。框架用 spawn 把所有探针同时发出去,Future.get() 收结果。慢的探针不阻塞快的。
聚合状态是 Healthy,但某个探针是 Degraded?
不会。聚合取最差状态——只要有一个 Degraded,整体就是 Degraded。只有一个 Unhealthy,整体就是 Unhealthy。只有全部 Healthy 才返回 Healthy。
怎么扩展新的探针类型?
两种方式:
- 简单:
addCheck("name") { ... }写闭包 - 复杂:实现
IHealthCheck接口,addCheck<MyCheck>("name")注册
推荐把可复用的探针(如 RedisHealthCheck、DatabaseHealthCheck)封装成单独的类型,方便多个项目共享。