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,随后观察实际探测结果。
...