一个 /api/health,就可以让开发和运维做好朋友

一、我以前根本不知道这东西是给谁写的

刚做开发那会儿,看别人项目里有个接口叫 /api/health,点进去就返回个 ok

我当时心想:这玩意儿有啥用?又不查数据库,又不处理业务,就干巴巴返回个200。谁写的,闲的吧。

直到有一次,服务挂了。

不是完全挂,是半死不活那种——进程还在,但请求全超时。负载均衡不知道,还在哗哗地导流量过来。用户那边白屏,运维冲过来问:“你这服务有没有健康检查端点?”

我说没有。

他叹了口气。那个叹气声我到现在还记得。不是骂人,是那种“又来了”的疲惫。

后来他跟我解释了半天,我才明白这东西到底是给谁用的。负载均衡靠它决定要不要导流量,K8s靠它决定要不要重启Pod,注册中心靠它决定要不要摘掉节点。没有这个端点,服务挂了,全世界都不知道,还一个劲往你身上发请求。

说白了,/api/health 就是服务对外说的唯一一句话:“我还活着,可以来找我。”

开发花三分钟写好,运维靠它睡个好觉。


二、这东西到底怎么写,运维最想要什么

1. 最简版:返回200

csharp

[HttpGet("/api/health")]
public IActionResult Health()
{
    return Ok("healthy");
}

能跑。进程活着就返回200。但这个版本的毛病也很明显:进程活着不代表业务活着。数据库连接池满了、Redis挂了、消息队列堆积了,它全不知道,照常返回200。运维最怕的就是这种——监控全绿,业务全挂。

这个版本只适合服务刚启动还没接依赖的阶段,或者给负载均衡做个最基本的存活探测。


2. 进阶版:逐个检查依赖

csharp

[HttpGet("/api/health")]
public async Task<IActionResult> Health()
{
    var dbOk = await CheckDbAsync();
    var redisOk = await CheckRedisAsync();
    var mqOk = await CheckRabbitMQAsync();

    var healthy = dbOk && redisOk && mqOk;

    return healthy ? Ok(new { status = "healthy" })
                   : StatusCode(503, new { status = "unhealthy", db = dbOk, redis = redisOk, mq = mqOk });
}

能定位到具体是哪个组件挂了。但问题来了:同步检查,串行执行,一个慢全慢。

比如Redis超时3秒,整个 /api/health 就卡3秒。负载均衡器那边超时2秒就直接判定你挂了,把节点摘掉。这就是典型的“健康检查把自己搞死”。


3. 正确姿势:并行检查 + 超时控制

csharp

[HttpGet("/api/health")]
public async Task<IActionResult> Health()
{
    var checks = new Dictionary<string, Task<bool>>();

    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));

    checks["db"] = CheckDbAsync(cts.Token);
    checks["redis"] = CheckRedisAsync(cts.Token);
    checks["mq"] = CheckRabbitMQAsync(cts.Token);

    var results = new Dictionary<string, string>();
    foreach (var (name, task) in checks)
    {
        try
        {
            var ok = await task;
            results[name] = ok ? "healthy" : "unhealthy";
        }
        catch (OperationCanceledException)
        {
            results[name] = "timeout";
        }
        catch
        {
            results[name] = "unhealthy";
        }
    }

    var healthy = results.All(r => r.Value == "healthy");

    return healthy ? Ok(new { status = "healthy", checks = results })
                   : StatusCode(503, new { status = "unhealthy", checks = results });
}

每个检查独立跑,互不拖累。全局2秒超时,不会无限等。单个依赖超时只影响自己,不影响其他组件的判断结果。


4. .NET 原生方案:HealthChecks 中间件

上面是自己手写的版本。.NET有现成的轮子,直接用 HealthChecks 中间件更省事:

csharp

builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, timeout: TimeSpan.FromSeconds(2), name: "db")
    .AddRedis(redisConnection, timeout: TimeSpan.FromSeconds(1), name: "redis")
    .AddRabbitMQ(timeout: TimeSpan.FromSeconds(2), name: "mq")
    .AddUrlGroup(new Uri("https://api.downstream.com/health"), timeout: TimeSpan.FromSeconds(2), name: "downstream");

映射到端点时,可以自定义返回格式,顺便带上每个依赖的耗时和异常信息:

csharp

app.MapHealthChecks("/api/health", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        var result = new
        {
            status = report.Status.ToString(),
            totalDuration = report.TotalDuration.TotalMilliseconds + "ms",
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                duration = e.Value.Duration.TotalMilliseconds + "ms",
                error = e.Value.Exception?.Message
            })
        };

        context.Response.ContentType = "application/json";
        context.Response.StatusCode = report.Status == HealthStatus.Healthy ? 200 : 503;
        await context.Response.WriteAsJsonAsync(result);
    }
});

返回示例:

json

{
    "status": "Unhealthy",
    "totalDuration": "2134ms",
    "checks": [
        { "name": "db", "status": "Healthy", "duration": "15ms", "error": null },
        { "name": "redis", "status": "Unhealthy", "duration": "2012ms", "error": "Timeout" },
        { "name": "mq", "status": "Healthy", "duration": "8ms", "error": null },
        { "name": "downstream", "status": "Healthy", "duration": "96ms", "error": null }
    ]
}

运维拿到这个,一眼就知道:Redis超时了2秒,其他正常。不用翻日志、不用查监控大盘,一个端点就是一份诊断报告。


5. K8s 场景:liveness 和 readiness 要区分

健康检查写到一定程度,K8s 的两种探针就该分开了:

  • liveness probe:服务还活着吗?挂了就重启Pod。应该检查最轻量的东西,比如进程是否响应。别查数据库,别查Redis。 否则数据库一挂,所有Pod轮番重启,雪崩。

  • readiness probe:服务能接请求了吗?不能就先别导流量。这个可以查依赖,比如数据库连不上就返回未就绪,K8s暂时不给这个Pod导流量,但不会重启它。

大多数团队直接用一个 /api/health 同时当 liveness 和 readiness,小项目没问题。但上了规模,建议分开:

csharp

// liveness:只要进程活着就行,什么都不查
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false
});

// readiness:检查所有依赖,用来决定要不要导流量
app.MapHealthChecks("/health/ready", new HealthCheckOptions { ... });

6. 一个容易被忽略的坑:健康检查不能暴露到公网

/api/health 如果暴露在公网,外部随便谁都能调一下看你的依赖信息——数据库地址、Redis IP、下游API全在返回体里。所以必须:

  • 防火墙/IP白名单只允许内网或监控节点访问

  • 或者区分公网版和内网版:

csharp

// 公网版:只告诉活着没
app.MapHealthChecks("/api/health/public", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
        await context.Response.WriteAsync(report.Status == HealthStatus.Healthy ? "ok" : "fail")
});

// 内网版:带完整诊断信息
app.MapHealthChecks("/api/health/internal", new HealthCheckOptions
{
    ResponseWriter = WriteDetailedReport
});

7. 性能边界:健康检查本身不能拖垮服务

负载均衡可能每秒都在探测 /api/health。如果每次都真的去连数据库、真的连Redis,开销不是零。几种优化:

  • 缓存结果:检查一次,缓存5-10秒,下次探测直接返回缓存

  • 后台轮询:健康检查在后台定时跑,/api/health 只返回上一次结果,请求本身不触发任何连接

  • 熔断逻辑:如果依赖已经挂了,没必要每次都去连,直接返回失败

一个 /api/health 写成这样,开发多花了半小时,运维少熬了半个夜。朋友就是这么当的。


三、加上了,朋友就有了

后来只要是我负责的服务,我都加上 /api/health。小项目没什么依赖,就写个最简版,返回200就行。复杂点的,把数据库、Redis、下游API都检查上,一个端点就是一份诊断报告。跟SSL证书监控、数据库端口监控一起,全接到云哨兵上。

配完之后,事情就变简单了。每周上云哨兵看一眼周报,三个任务整整齐齐列在那里,不用挨个服务器登进去查,不用凭感觉猜“应该还活着吧”。扫一眼,心里有数,该干嘛干嘛。

一个小项目的周报截图:

三个任务,SSL证书正常,数据库端口全绿,/api/health 有一次告警。就这一次,在凌晨,几分钟后自己恢复了。偶发性的小波动,不用急着半夜爬起来查。等数据积累多了,看看有没有规律,再统一排查。

做好 /api/health,然后花几分钟在云哨兵配置一下,告警就有了,汇报也有了。朋友也就有了。


写在最后

/api/health 不是什么新技术,花几分钟就能加。

但它有意思的地方在于,一个端点,两种思维。

开发看它,就是一个接口,返回200完事。运维看它,是整个服务唯一能主动报平安的窗口。开发多检查一个依赖,运维就少排查一个问题。开发统一了标准,运维就有了全局视野。

以前我写代码只管功能跑不跑得通,后来才意识到,服务上线之后还有另一个角色在盯着它——那个人半夜被叫起来的时候,第一个想找的就是 /api/health

一个端点,一份周报,开发和运维之间那堵墙,其实没那么厚。

如果你手里管着一堆服务但不知道它们到底稳不稳,先把 /api/health 加上。写代码的人、看服务的人、中间件、云哨兵,人也好物也罢,都有一个共同的锚点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值