就绪端点失败时 up 仍可为一,直到独立的指标端点也失败。

Docker 健康状态与 Prometheus up 回答不同问题

Docker 健康状态与 Prometheus 的 up 序列来自不同检查。Compose 健康检查在应用容器内运行命令;Prometheus 则独立抓取配置的指标端点,并记录抓取是否成功。两项观察可以合理地不一致,因为应用可能已经无法处理有用工作,却仍能提供指标,也可能业务正常而指标端点损坏。理解区别,才能避免用一个绿灯替代全部服务状态。 坏掉的样子 把 up=1 解释成应用已经就绪,可能掩盖故障。它说明目标指标抓取成功,不说明登录、数据库写入或其他业务操作成功。对于这些问题,需要合适的行为指标或外部探测。自定义就绪 gauge 可以有用,但具体含义必须由应用契约定义,不能仅凭名字叫 ready,就认为它覆盖了所有必需依赖。 反向误解是认为 Docker 健康状态会自动导出到 Prometheus。Compose 中定义 healthcheck,不会自动创建 Prometheus 指标,也不会配置抓取目标。需要 exporter 或应用埋点明确暴露所需观察。即使完成导出,采集路径和新鲜度也与引擎健康状态不同,需要知道更新延迟和失败方式,才能解释不一致。 两种观察都不会自动定义重启或路由动作。Docker 的 restart: always 处理容器终止;容器仍在运行时,不会仅因 unhealthy 就重启。监控可以根据证据告警,明确配置的控制器可以采取动作,但这些连接不会因为两个工具恰好位于同一个 Compose 项目里就凭空出现。观察、告警和修复需要分别验证。 快速验证 使用宿主 Python、Docker Compose 和两个固定镜像运行脚本。实测为 Compose 5.1.1、Docker 29.3.1、Python 3.11.16,以及 Linux arm64 上的 Prometheus 3.5.1。它构建临时 Prometheus 配置镜像,创建唯一项目,只把 Prometheus API 发布到宿主回环地址,结束时清理自身项目、数据卷和标签: ...

二月 21, 2028 · 2 分钟 · XB Field Notes
探测可以改变健康状态,却不会自动重启应用进程。

Docker 健康检查报告状态,恢复动作需要明确的消费者

Docker HEALTHCHECK 在容器内运行配置的命令,并把结果记录为健康状态。它提供了主进程是否运行之外的另一项观察,但含义取决于命令检查什么:进程存在、HTTP 响应,还是应用特定的就绪条件。Docker 不会因为出现 HEALTHCHECK 指令,就自动推导出一份通用业务就绪契约,检查者必须明确这个信号能证明哪些行为。 坏掉的样子 总是返回零的命令,即使应用无法提供有用工作,也会产生看起来正常的健康报告。shell 命令序列还可能因为最后一条命令成功,而掩盖前面的失败。反过来,如果探测依赖一个非必需外部服务,就可能把仍可工作的应用标成不健康,并促使消费该状态的系统执行不必要恢复。探测含义必须符合实际消费者需求。 独立 Docker 的重启策略响应容器终止,不是只要健康状态变成 unhealthy 就触发。加入 restart=always,不会让 Docker 因探测失败而重启仍然存活的应用。同样,健康检查也不会自动从所有代理的路由表中移除容器。必须有控制器或代理明确读取该状态,并实现预期动作,不能把观察配置当成恢复配置。 探测时序也容易被误解。重试阈值统计连续失败,启动宽限则改变早期哪些失败会被计数。启动期的一次成功可以结束初始宽限行为。检测耗时取决于命令持续时间、间隔、超时和配置,不能简单承诺一次请求失败就立刻停止流量。解释延迟时,应沿实际探测时间线检查,而不是只看一个 retries 数字。 快速验证 先取得固定基础镜像,再用宿主 Python 和 Docker 运行脚本。实测为 Docker 29.3.1、Linux arm64,镜像内 Python 3.11.16。它只创建一个临时镜像和容器,关闭容器外部网络,使用自身回环 HTTP 服务。被改变的状态仅是实验内部一个无害就绪文件,结束后清理自己的容器和标签: from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,time,uuid BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' def docker(*args,check=True): result=subprocess.run(['docker',*args],text=True,capture_output=True,timeout=120) if check:assert result.returncode==0,result.stderr return result def inspect(container):return json.loads(docker('inspect',container).stdout)[0] def wait_for(container,wanted): deadline=time.monotonic()+25 while time.monotonic()<deadline: state=inspect(container) if state['State']['Health']['Status']==wanted:return state time.sleep(.1) raise AssertionError('health did not reach '+wanted) name='xbcatm-health-'+uuid.uuid4().hex[:10];tag=name+':probe' try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory) (root/'server.py').write_text('from http.server import BaseHTTPRequestHandler,HTTPServer\nfrom pathlib import Path\nclass Handler(BaseHTTPRequestHandler):\n def do_GET(self):\n code=200 if Path("/ready").exists() else 503\n self.send_response(code);self.end_headers();self.wfile.write(str(code).encode())\n def log_message(self,*args):pass\nHTTPServer(("127.0.0.1",8080),Handler).serve_forever()\n') probe='import urllib.request;urllib.request.urlopen("http://127.0.0.1:8080/health",timeout=.5).read()' (root/'Dockerfile').write_text(f'FROM {BASE}\nCOPY server.py /server.py\nHEALTHCHECK --interval=1s --timeout=1s --retries=2 --start-period=5s CMD ["python", "-c", {json.dumps(probe)}]\nCMD ["python", "/server.py"]\n') docker('build','--network','none','-t',tag,str(root)) began=time.monotonic() docker('run','-d','--name',name,'--network','none','--restart','always',tag) initial=inspect(name);assert initial['State']['Health']['Status']=='starting' first=wait_for(name,'unhealthy');assert first['State']['Running'] and first['RestartCount']==0 assert first['State']['StartedAt']==initial['State']['StartedAt'] assert first['State']['Health']['FailingStreak']>=2 assert any(log['ExitCode']!=0 and '503' in log['Output'] for log in first['State']['Health']['Log']) time.sleep(max(0,12-(time.monotonic()-began))) stable=inspect(name);assert stable['State']['Running'] and stable['RestartCount']==0 assert stable['State']['StartedAt']==initial['State']['StartedAt'] and stable['State']['Health']['Status']=='unhealthy' docker('exec',name,'python','-c','from pathlib import Path;Path("/ready").touch()') healthy=wait_for(name,'healthy');assert healthy['RestartCount']==0 and healthy['State']['Health']['FailingStreak']==0 docker('exec',name,'python','-c','from pathlib import Path;Path("/ready").unlink()') unhealthy=wait_for(name,'unhealthy');assert unhealthy['RestartCount']==0 and unhealthy['State']['Running'] assert unhealthy['State']['StartedAt']==initial['State']['StartedAt'] print('starting -> unhealthy -> healthy -> unhealthy follows the real HTTP probe; failure logs contain HTTP 503') print('restart=always does not restart a still-running process solely because its health is unhealthy') finally: docker('rm','-f',name,check=False);docker('image','rm',tag,check=False) /ready 不存在时,服务返回 HTTP 503。Dockerfile 健康检查使用 Python HTTP 客户端,遇到该状态会抛出错误并以失败状态结束。配置为一秒间隔、一秒超时、两次重试、五秒启动期。容器采用 restart=always 启动,初始检查记录健康状态为 starting,随后观察实际探测结果。 ...

七月 5, 2027 · 2 分钟 · XB Field Notes