首页 文章 精选 留言 我的

精选列表

搜索[AI医疗],共10000篇文章
优秀的个人博客,低调大师

AI时代的Docker教程36招-第14招-配置、Secret和Healthcheck到底应该放哪里?

# Docker进化36记-第14记-配置、Secret和Healthcheck到底应该放哪里? > 配置进镜像、密钥进环境变量、探针只探端口,是同一场事故的三个阶段。 ```text [读者] 正在把数字人平台交付到客户机房的平台工程师 [痛点] 密钥写死在编排文件里,容器 Running 但业务不可用 [现在读] 交付在即,配置面与探活必须一次说清 [读完] 分得清五条配置通道,写得出分级健康检查 ``` ```text [旧方案] environment 硬编码 + HEALTHCHECK 只探端口 | v [新需求] 同一套编排要交付多个客户机房,地址与密钥各不相同 | v [冲突] 环境变量会进元数据,端口通不等于服务就绪 | v [后果] 密钥随镜像流出,启动顺序靠 sleep,故障被判定为健康 ``` 我是老李,企业数字人平台这个项目已经写到第 13 记:网关、用户、ASR、TTS、LLM Gateway、PostgreSQL、Redis 都能在本地用一份 compose.yaml 拉起来,端口也通了,语音链路从上传音频到回一段合成音,跑得挺顺。 顺是顺,但那份 compose.yaml 里躺着三十多个 environment,包括 `POSTGRES_PASSWORD` 和 `ASR_API_KEY`,全都是明文。 真正的变化来自交付。平台要装进两个客户的机房:客户 A 的 PostgreSQL 不在容器里,在他们自建的数据库集群上;客户 B 不用我们的 TTS,走的是外采的语音云,每家的 API Key 都不一样。同一套镜像,要跑出两种配置。 第一次交付我就栽了:客户运维改一个数据库地址,得从我们这里要一份改了 ENV 的新镜像;我发过去的 compose.yaml 截图里,密码就那么明晃晃地摆在第二屏。 于是问题来了:配置写进镜像,为什么迟早会出事? 数据库地址、模型地址、API Key、运行参数,这四类东西到底该分别放在哪里? 还有更麻烦的:容器状态写着 Up,健康状态写着 healthy,可业务请求照样 503——我们到底该相信谁? # 01、故事 还是那套企业数字人平台。第 13 记结束时,它的 compose.yaml 大概长这样:postgres 存会话和知识库元数据,redis 做缓存,asr 和 tts 各自是一个 Python 服务,llm-gateway 负责把请求转发给模型,gateway 对外统一暴露 /v1 接口。 任务是:三周内把这一整坨交付到两个客户机房,并且允许客户自己替换数据库地址、模型地址和上游 API Key。 当时的方案是「一份 compose.yaml 打天下」:所有配置项都写在 environment 里,靠一份 .env 提供值,`.env.example` 提交到仓库当作模板。本地开发没问题,`docker compose up -d` 一转,全绿。 变的不是代码,是边界。本地开发时,数据库地址永远是 `postgres:5432`,密钥永远是测试密钥,改不改都无所谓。一旦交付,配置就成了客户资产:地址是客户的,密钥是客户的,我们连看都不该看到。 冲突立刻出现:配置被写进了镜像和编排文件,而这两样东西正是我们交付出去的产物。产物里带着客户的密钥,还要求客户为了改一个地址去等我们重新构建——这已经不是技术问题,是交付模式问题。 更隐蔽的第二个冲突在探活上。为了让 gateway 别在 postgres 没起来时启动失败,我们在 compose.yaml 里加了 depends_on,又让 gateway 的启动脚本先 sleep 10 秒。本地机器快,能过;客户机房的磁盘慢一倍,就不一定了。 那段时间我常问自己:我们交付的到底是「一套能跑的镜像」,还是「一套能被客户接管的系统」? 如果是后者,那配置该住在哪里? Running、healthy、可用,这三个词在我们嘴里是不是一直混着用? > 镜像非配置 > 交付即边界 > 一次改地址 > 三周变三天 # 02、问题 旧方案在本地是成立的,在交付场景下失效得很彻底。 第一层失效是镜像与配置绑死。数据库地址、模型服务地址、上游 API Key 全在 environment 里,改任何一项都要重新走一遍构建、推送、拉取。密钥轮换本来是客户安全团队的例行操作,到我们这里变成了发版。 第二层失效是密钥的暴露面。容器元数据里存着 environment 的完整副本,任何能访问 Docker API 的进程都能读到;`.env` 一旦被复制进构建上下文,还有可能被 COPY 进镜像层,之后 `docker history` 就能翻出来。 第三层失效是探活语义混乱。我们给 asr 配的 HEALTHCHECK 是 `curl -f http://localhost:9001/`,只要 HTTP 服务起来了就返回 200,可模型权重还在加载,真正的转写请求会返回 503。编排层看到 healthy 就认为一切正常,把流量切了进去。 业务影响很直接:每次交付要人工改配置、人工核对密钥、人工确认服务真的起来了,一次交付大概要折腾大半天;密钥轮换要走发版流程,客户安全团队不认。 技术表现上,可以归纳成三个可复现的现象: - `docker inspect` 能看到明文密钥; - `docker compose ps` 显示所有服务 Up,但 gateway 日志里全是 `connection refused`; - `docker inspect --format '{{.State.Health.Status}}'` 返回 healthy,业务接口返回 503。 完成标准因此可以写得很具体:一套编排文件,不改镜像就能适配两个客户机房;密钥不出现在 `docker inspect`、`docker compose config` 和镜像历史里;编排层能区分「进程活着」和「服务能干活」,并且不需要任何 sleep。 > 元数据会说话 > 端口通不算数 > 标准要可验 > 失败要可复 # 03、原理 要讲清楚这件事,得先把「配置」这个词拆开。它至少包含四种东西:地址(数据库、模型、上游服务)、凭证(密码、API Key、Token)、运行参数(并发数、超时、日志级别),以及结构性配置(路由表、模型清单)。 这四类东西的敏感度、变更频率、变更主体都不一样,把它们塞进同一个通道,是问题的起点。 第一个原理是构建期与运行期的分界。Dockerfile 里的 ENV、ARG、COPY 都发生在构建期,产物是镜像;镜像一旦被推送,它携带的任何东西都不可撤销。运行期的 configuration 发生在容器创建时,产物是容器,容器可以删了重建,镜像不用动。配置应该尽量往右挪,能留在运行期就不要进构建期。 第二个原理是环境变量只是元数据。很多人把「用环境变量传密钥」当成安全实践,其实它只是把密钥从文件挪到了进程环境里。容器的环境变量会被写进容器配置,`docker inspect` 可读,`/proc/1/environ` 在容器内可读,任何被挂载了 docker.sock 的容器也能读到。它能防的是「密钥进镜像」,防不了「密钥进元数据」。 反直觉判断:环境变量不是更安全的密钥通道,它只是更隐蔽的密钥通道。真正的变化发生在它被挪出元数据、变成文件挂载之后。 第三个原理是 secret 的本质是「挂载而非注入」。Compose 的 secrets 在非 Swarm 模式下不是加密存储,它做的事情是把一份宿主机上的文件以挂载的方式提供给容器,默认落在 `/run/secrets/`。相比环境变量,它少了元数据副本;相比 COPY,它不进镜像层。它的边界也很清楚:它防的是泄漏面扩大,不防宿主机 root,也不防容器内进程把文件内容读出来打印到日志。 第四个原理是 Dockerfile 的 HEALTHCHECK 实际上是一次进程内探测。命令在容器内执行,退出码 0 表示 healthy,1 表示 unhealthy,2 是保留值。它支持的选项包括 `--interval`、`--timeout`、`--start-period`、`--retries`,官方文档给出的默认值分别是 30s、30s、0s、3 次;较新的 Docker Engine 还提供了 `--start-interval`,用于在 start-period 内以更高频率探测,具体版本下限需按当前官方文档核验。整份 Dockerfile 里只有最后一条 HEALTHCHECK 生效。 关键点在这里:Docker 只提供两个状态,healthy 和 unhealthy,没有第三档。所谓「三层健康检查」是应用自己提供的三组端点,Docker 只是其中一层的消费者。这一点想清楚了,后面的分级探活才不会写成玄学。 反直觉判断:探针探得越深,系统可用性反而可能越低。探针一旦把下游依赖也纳入判定,任何一个下游抖动都会把本来能提供降级服务的实例判成 unhealthy,然后被编排层摘掉——健康检查的严格程度,本质上是在「漏判」和「误杀」之间做取舍。 > 构建期落定 > 运行期可变 > 变量非保险 > 挂载才收窄 # 04、架构 ```text [输入] 客户机房的数据库地址 / 模型地址 / API Key / 运行参数 | v [模块] .env 插值 → env_file → environment → secrets / configs → build --secret | v [数据/状态] Compose 渲染结果 / 进程环境变量 / /run/secrets/* / 镜像层 | v [处理] compose 渲染 → 容器创建 → 应用读取 → HEALTHCHECK 探测 → depends_on 判定 | v [输出] 可替换的运行时配置 + 不落元数据的密钥 + 分级健康状态 ``` 这张图里有五条通道,从「最该用」到「最不该用」排下来,各自的定位完全不同。 `.env` 是给 Compose 自己做变量插值用的,它不直接进容器,但它的值会被渲染进最终的配置里。`env_file` 是给单个服务注入容器环境变量的,文件里的每一行都会成为容器进程的环境变量。`environment` 是直接写在编排文件里的键值对,优先级高于 `env_file`。 `secrets` 和 `configs` 是挂载通道:前者挂载敏感文件,后者挂载非敏感文件,默认路径同样是 `/run/secrets/` 或容器内指定的 target。`build --secret` 是构建期通道,只在某一条 RUN 指令执行期间存在。 边界很清楚:`.env` 和 `env_file` 适合非敏感的地址与运行参数;`environment` 适合需要覆盖的值和指向 secret 文件路径的变量;`secrets` 适合密码和 Key;`configs` 适合路由表、模型清单这类需要版本管理的文件;`build --secret` 只适合「构建时需要、运行时不需要」的东西,比如私有源的凭证。 收益是可替换性和泄漏面收窄:同一份镜像可以在不同机房跑出不同配置,密钥不再出现在容器元数据里。 代价是复杂度上升:配置来源从一处变成五处,必须有一套明确的约定,否则新人根本不知道该去哪改。而且 secrets 是文件不是环境变量,应用代码要改成「读文件」,这是一个真实的改造成本。 适用条件是:只要这套编排需要交付给别人、或者需要承载敏感凭证,就应该按这套通道划分;如果只是本地跑个 Demo,全写在 environment 里没有任何问题。 > 五条通道走 > 各管各的事 > 越权即隐患 > 分层才可控 # 05、实战一次 先把环境钉住。我用的是 Docker Engine 26 以上版本和 Compose v2,命令行为如下。 ```bash $ docker version --format '{{.Server.Version}}' 示例输出,以你的环境为准 $ docker compose version 示例输出,以你的环境为准 ``` 先建目录约定。 `compose/.env.example`: ```dotenv # 非敏感:可提交进版本库 COMPOSE_PROJECT_NAME=dhr DHR_ENV=dev # 服务地址 POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DB=digital_human POSTGRES_USER=dhr ASR_BASE_URL=http://asr:9001 TTS_BASE_URL=http://tts:9002 LLM_BASE_URL=http://llm-gateway:9100 GATEWAY_PORT=8080 # 运行参数 ASR_MODEL=paraformer-zh ASR_MAX_CONCURRENCY=4 LOG_LEVEL=info # 敏感项只保留文件路径约定,真实值走 secrets 目录 POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password ASR_API_KEY_FILE=/run/secrets/asr_api_key ``` `compose/secrets/README.md`: ````markdown # secrets 目录约定 真实密钥文件不进入版本库,本目录只提交本文件与 .gitkeep。 | 文件名 | 用途 | 容器内挂载点 | 消费方 | | --- | --- | --- | --- | | postgres_password | PostgreSQL 密码 | /run/secrets/postgres_password | postgres、gateway | | asr_api_key | ASR 鉴权 Key | /run/secrets/asr_api_key | asr、gateway | | tts_api_key | TTS 鉴权 Key | /run/secrets/tts_api_key | tts、gateway | | pip_index_token | 私有源 Token,仅构建期 | /run/secrets/pip_index_token | 镜像构建 | 本地初始化: ```bash mkdir -p compose/secrets printf '%s' "$(openssl rand -hex 16)" > compose/secrets/postgres_password printf '%s' "sk-local-asr-dev" > compose/secrets/asr_api_key printf '%s' "sk-local-tts-dev" > compose/secrets/tts_api_key chmod 600 compose/secrets/* ``` 轮换流程:覆盖文件内容,然后执行 `docker compose up -d --force-recreate `,不需要重新构建镜像。 ```` V1 的 `compose/compose.yaml`: ```yaml name: dhr services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 asr: image: dhr/asr:0.1 environment: ASR_API_KEY: ${ASR_API_KEY} ASR_MODEL: ${ASR_MODEL} ASR_MAX_CONCURRENCY: ${ASR_MAX_CONCURRENCY} healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9001/"] interval: 30s timeout: 5s retries: 3 gateway: image: dhr/gateway:0.1 environment: DHR_ENV: ${DHR_ENV} POSTGRES_HOST: ${POSTGRES_HOST} POSTGRES_PORT: ${POSTGRES_PORT} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ASR_BASE_URL: ${ASR_BASE_URL} ASR_API_KEY: ${ASR_API_KEY} LOG_LEVEL: ${LOG_LEVEL} depends_on: - postgres - asr ports: - "${GATEWAY_PORT}:8080" volumes: pgdata: ``` 启动: ```bash cd compose cp .env.example .env printf '\nPOSTGRES_PASSWORD=devpass\nASR_API_KEY=sk-local-asr-dev\n' >> .env docker compose up -d docker compose ps ``` `docker compose ps` 的示例输出: ```bash NAME IMAGE STATUS PORTS dhr-asr-1 dhr/asr:0.1 Up 25 seconds (healthy) 9001/tcp dhr-gateway-1 dhr/gateway:0.1 Up 24 seconds 0.0.0.0:8080->8080/tcp dhr-postgres-1 postgres:16-alpine Up 26 seconds (healthy) 5432/tcp ``` 验证一次正常请求: ```bash curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/v1/health ``` 到这里 V1 是能跑的。但它同时留下了三个后续问题:密钥能不能被读出来、gateway 会不会抢跑、asr 的 healthy 到底代表什么。下一章一个一个验。 > 先让它跑通 > 再让它露馅 > 三个问题留 > 下一章来验 # 06、排查 ## 诊断链 A:gateway 抢跑 现象:`docker compose up -d` 之后立刻 `docker compose logs gateway`,报 `failed to connect to postgres:5432: connection refused`,但 `docker compose ps` 显示 postgres 是 `Up`。 怀疑:网络别名解析错,gateway 找不到 postgres 主机名。 检查: ```bash docker compose exec gateway getent hosts postgres docker compose exec postgres ss -lntp | grep 5432 ``` 证据:`getent hosts postgres` 正常返回容器 IP;`ss -lntp` 此时没有任何监听 5432 的记录。再看 `docker compose logs postgres`,能看到初始化脚本还在跑。 根因:PostgreSQL 官方镜像在首次启动时,会先用一个只监听 unix socket 的临时服务器执行初始化脚本,完成后才真正以 `listen_addresses` 启动正式实例。这段时间容器状态是 Running,但 TCP 5432 根本没开。`depends_on` 的默认条件是 `service_started`,它只保证容器已经启动,不保证端口可接受连接。 修复:把依赖条件改成 `service_healthy`,并把 postgres 的 HEALTHCHECK 配上——pg_isready 走连接探测,要等正式实例起来才会返回 0。 **错误尝试:** 在 gateway 的启动命令前加 `sleep 15`。这个做法错在两点:一是 15 秒是拍脑袋的,客户机房的磁盘慢一倍就失效;二是它把真实的依赖关系藏了起来,之后的日志里再也看不到「我在等数据库」这个事实,只会看到「我晚了 15 秒」。 ## 诊断链 B:密钥藏不住 现象:客户运维随手执行 `docker inspect`,在输出里看到了 ASR 的 Key。 怀疑:是不是只有 gateway 泄漏? 检查: ```bash docker inspect --format '{{json .Config.Env}}' dhr-gateway-1 | tr ',' '\n' | grep -i key docker compose config | grep -i -A1 'ASR_API_KEY' ``` 证据:两处都能看到明文。`docker inspect` 读到的是容器元数据,`docker compose config` 读到的是插值渲染后的最终配置。 根因:environment 是容器元数据的一部分,写在 compose.yaml 里的值会随容器创建一起落在 Docker 守护进程的状态里;而 `.env` 里的值会被 Compose 插值进渲染结果,凡是能执行 `docker compose config` 的人都能看到。 修复:把真正敏感的项从 environment 移出去,改成 secrets 文件挂载,environment 里只保留指路用的 `*_FILE` 路径。 **错误尝试:** 把 Key 从 `.env` 挪进 Dockerfile,用 `ARG ASR_API_KEY` 接收再 `ENV ASR_API_KEY=$ASR_API_KEY`。这个做法比 environment 更糟:ARG 会出现在构建历史和构建缓存里,ENV 会直接固化进镜像层。密钥一旦进了层,推送到 Registry 就等于公开,且无法通过重新构建同一 tag 来真正收回。 ## 诊断链 C:healthy 不代表能用 现象:`docker inspect --format '{{.State.Health.Status}}' dhr-asr-1` 返回 `healthy`,但业务接口返回 503。 怀疑:模型文件没挂进去。 检查: ```bash docker compose exec asr curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9001/ docker compose exec asr curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9001/healthz/ready docker compose logs asr | tail -n 20 ``` 证据:根路径返回 200,`/healthz/ready` 返回 503,日志里能看到模型权重仍在加载。 根因:HEALTHCHECK 探的是最浅的一层——HTTP 服务的进程活着。它证明了「容器活着」,但没有证明「服务就绪」。而编排层只认这一个信号,于是把没准备好的实例当成了可用实例。 修复:应用侧提供分级端点,HEALTHCHECK 指向 readiness 那一层,并且用 `--start-period` 给模型加载留出窗口。 **错误尝试:** 把 `--retries` 调到 30,靠次数硬熬。这个做法错在没有区分「启动慢」和「依赖挂了」:启动慢应该用 `--start-period` 表达,它期间的失败不计入 retries;而把 retries 拉高,会让真正的故障被延迟 30 个周期才暴露,故障发现时间被无限拉长。 > 现象先看清 > 证据再说话 > 猜秒最误事 > 探浅最坑人 # 07、优化 三条排查链分别指向三个根因:依赖条件太弱、密钥走错了通道、探针层次太浅。V2 就针对这三点改。 第一个修改:把敏感项从 environment 迁到 secrets。 ```yaml services: postgres: environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password secrets: - postgres_password gateway: env_file: - ./.env environment: DHR_ENV: ${DHR_ENV:-prod} POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password ASR_API_KEY_FILE: /run/secrets/asr_api_key TTS_API_KEY_FILE: /run/secrets/tts_api_key secrets: - postgres_password - asr_api_key - tts_api_key secrets: postgres_password: file: ./secrets/postgres_password asr_api_key: file: ./secrets/asr_api_key tts_api_key: file: ./secrets/tts_api_key ``` 原因:`_FILE` 后缀是 PostgreSQL 等官方镜像的既定约定,镜像自己会去读这个路径下的文件内容并当作密码,不需要我们改启动逻辑。应用侧没有这个约定,需要自己实现「读文件优先、读环境变量兜底」的配置加载顺序。 新行为:密钥不再出现在 `docker inspect` 的 Env 里,也不再出现在 `docker compose config` 的渲染结果里;Compose 在非 Swarm 模式下把它作为文件挂载到容器的 `/run/secrets/` 下(Windows 容器的挂载路径不同,需按当前官方文档核验)。 验证: ```bash docker compose up -d --force-recreate gateway postgres docker inspect --format '{{json .Config.Env}}' dhr-gateway-1 | tr ',' '\n' | grep -i 'key\|password' || echo 'no plaintext secret in env' docker compose exec gateway ls -l /run/secrets ``` 第二个修改:把 depends_on 改成带条件的等待。 ```yaml depends_on: postgres: condition: service_healthy asr: condition: service_healthy tts: condition: service_healthy ``` 原因:`service_started` 只保证容器创建,`service_healthy` 会等 HEALTHCHECK 通过。这样启动顺序由证据决定,而不是由猜测的秒数决定。 新行为:gateway 在依赖服务 healthy 之前不会创建,日志里不再出现 `connection refused`。 验证: ```bash docker compose up -d --wait docker compose ps --format 'table {{.Name}}\t{{.Status}}' ``` 第三个修改:分级健康检查。应用侧暴露三个端点,语义各自独立。 ```text /healthz/live → 进程与事件循环存活,不查任何下游 /healthz/ready → 依赖就绪(模型已加载、连接池已建、必需的下游可达) /healthz/business → 关键业务链路可用(一次轻量的端到端探测) ``` 上面这段是端点语义说明,不是字符图。Docker 的 HEALTHCHECK 指向 readiness 那一层,Dockerfile 里写成: ```dockerfile HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=3 \ CMD curl -fsS http://127.0.0.1:9001/healthz/ready || exit 1 ``` 原因:模型加载是几十秒量级的事情,这个时间属于「启动慢」,应该用 `--start-period` 表达,而不是靠 retries 硬熬。start-period 期间的探测失败不计入 retries,第一次成功之后才开始正常计数。 新行为:asr 在模型加载完成前状态是 `starting`,加载完成后才变成 `healthy`,gateway 也才会被创建。 验证: ```bash docker compose up -d docker inspect --format '{{.State.Health.Status}}' dhr-asr-1 ``` 第四个修改:把构建期密钥从 Dockerfile 里剥离。ASR 镜像要从私有源装依赖,凭证不能落进层里。 ```dockerfile # syntax=docker/dockerfile:1 FROM python:3.12-slim AS builder WORKDIR /app RUN --mount=type=secret,id=pip_index_token \ --mount=type=cache,target=/root/.cache/pip \ PIP_INDEX_URL="$(cat /run/secrets/pip_index_token)" \ pip install --no-cache-dir -r requirements.txt ``` 构建时通过 `--secret` 传入: ```bash docker build \ --secret id=pip_index_token,src=./secrets/pip_index_token \ -t dhr/asr:0.2 . ``` 原因:`--mount=type=secret` 把文件挂在一个临时的挂载点上,只在对应的 RUN 指令执行期间可见,不写进镜像层,也不进入构建缓存。 新行为:`docker history dhr/asr:0.2` 里看不到任何凭证相关内容,镜像可以放心推到私有 Registry。 注意:这里的 secret 文件仍然要留在构建机上,本地开发时它和 Compose 用的是同一份 `secrets/` 目录,所以 `.gitignore` 必须覆盖它。 > 根因定改动 > 改动必验证 > 起点保启动 > 挂载守密钥 # 08、演进 ```text [同一份镜像 + 同一份编排] | +--[V1] environment 硬编码 + depends_on 无条件 + 探针只探端口 | 正确性:本地可用 | 代价:密钥进元数据、启动靠 sleep、healthy 可能是假信号 | +--[V2] env_file 承载非敏感 + secrets 挂载敏感 + service_healthy + 分级探针 | 正确性:不改镜像即可适配多机房,密钥不进元数据 | 代价:配置来源从 1 处变 5 处,应用要改成读文件 | [Trade-off] 得到:可交付性、可轮换性、可判定的启动顺序 失去:单点看配置的直观性,以及「一个文件看懂全部」的便利 边界:secrets 只挪动泄漏面,不提供加密存储,也不防宿主机 root ``` 正确性上,V1 在单机房、单副本、人工盯着的情况下是对的;V2 把「配置正确」从人的记忆变成了机器的约束。 稳定性上,V1 的启动顺序依赖 sleep,是概率性的;V2 依赖 HEALTHCHECK 的实际结果,是确定性的。代价是启动变慢——V2 必须等依赖真正就绪,冷启动时间比 V1 长。 复杂度上,V1 是 1 处配置来源,V2 是 5 处。这个复杂度不是白来的,它换来了敏感与非敏感的分层;但如果没有配套的文档约定,它也会变成新的坑——所以 `compose/secrets/README.md` 不是可选项。 成本上,V2 的改动量主要在应用侧:所有读环境变量的地方都要加上「读文件优先」的分支。这是实打实的代码改造,不是改几行 YAML 就能完事的。 适用范围上,V2 适用于任何需要交付、需要轮换密钥、需要多环境的场景。如果只是本地跑个一次性验证,V1 反而更省事。 遗留问题上,V2 还没解决三件事:一是 secrets 文件的轮换需要重建容器才生效,运行时热更新没做;二是 `/healthz/business` 这类深度探针还没接进编排,业务可用性目前只能靠外部监控;三是多个服务共享同一份 `env_file` 时,最容易出现「这个变量到底是谁在用」的困惑,还没有静态检查工具来兜底。 > 同一份镜像 > 两套运行时 > 得到可交付 > 失去一眼懂 # 09、洞见 ## 9.1 配置的第一属性是「谁改」,不是「是什么」 反直觉判断:决定一个配置该放哪条通道的,不是它的内容类型,而是「谁能改它」。数据库地址是客户的,就该走 `.env` 或 `env_file`;密码是客户安全团队的,就该走 secrets;模型清单是我们自己的,就该走 `configs` 进版本库。 一旦用「谁改」来分类,那三十多个 environment 会瞬间分成三堆,每堆对应一条通道,不需要争论。 ## 9.2 环境变量不是安全通道,只是隐蔽通道 反直觉判断:把密钥从 Dockerfile 挪到 environment,看起来是安全改进,实际上只是把它从「镜像层」挪到了「容器元数据」。两者的区别只是读取门槛高低,不是有没有泄漏。 真正的安全改进只有一个:让密钥从元数据里消失,变成一次性挂载的文件。secrets 做的就是这个。 ## 9.3 探针的严格程度是一个可用性预算 诊断链 C 里,我们差点用 `--retries 30` 去解决一个本该用 `--start-period` 解决的问题。这两个参数解决的是完全不同的事:start-period 处理「启动慢」,retries 处理「抖动容错」。 反直觉判断:把探针做深、做严,并不总是提升可靠性。一个把下游依赖也纳入判定的 readiness 探针,会在下游抖动时把所有实例同时判死,把一个局部故障放大成全站故障。探针应该探到「这个实例能不能提供它的核心能力」为止,再深就是替编排层做决定了。 ## 9.4 交付物的边界决定配置的边界 这次踩坑的真正起点,是我们把「能跑起来的编排」当成了交付物。一旦交付物变成「客户能接管的系统」,配置就不再是我们的实现细节,而是客户的运维界面。 界面要有契约:哪些字段客户可以改、改了要做什么、密钥放在哪、怎么轮换。把这些写进 `compose/secrets/README.md`,比写进任何一篇内部文档都有效。 > 谁改定通道 > 探深有代价 > 边界即契约 > 交付即界面 # 10、系统落地 原来我们有什么:一份能跑的 compose.yaml,包含 gateway、postgres、asr、tts、llm-gateway、redis,靠 environment 注入全部配置,靠 depends_on 保证顺序,探针只探端口。 这一篇新增了两样东西。第一样是配置通道的分层约定:`compose/.env.example` 明确了哪些是可提交的非敏感项,`compose/secrets/README.md` 明确了敏感文件的命名、挂载点和消费方,`env_file` 与 `environment` 的职责边界第一次写清楚。第二样是分级健康检查:live / ready / business 三层端点的语义,以及 HEALTHCHECK 该指向哪一层。 现在能做的是:同一份镜像,不改一行代码,靠替换 `.env` 和 `secrets/` 目录,就能适配两个客户机房;密钥不出现在 `docker inspect` 和 `docker compose config` 里;启动顺序由 `service_healthy` 决定,不需要任何 sleep;构建期的私有源凭证不再进镜像层。 还缺什么:一是没有做配置变更的静态校验,比如「某条 secrets 定义了但没有任何服务引用」这类问题现在只能靠人看;二是没有把 `docker compose config` 的渲染结果纳入 CI,密钥泄漏到渲染输出这种情况目前只能在交付前人工 grep;三是业务级探针 `/healthz/business` 还停留在应用内部,没有接到编排层。 下一步的演进方向很自然:这套配置面和探活面一旦稳定,就应该固化进镜像构建和发布流程——让镜像在构建时就把「哪些东西不能被写死」变成流水线上的检查项,而不是靠交付前的人工核对。这也是这个平台从「能跑」走向「可交付」的必经一步。 > 原来能跑通 > 现在可交付 > 还缺自动化 > 下一步固化 # 11、小结 ```text Q1 → 配置进镜像,问题不在改起来麻烦,而在于镜像一旦分发就不可撤销 Q2 → 非敏感走 env_file / environment,敏感走 secrets 文件挂载,构建期凭证走 build secret Q3 → Running 只是进程在跑,healthy 只是探针通过了,业务可用要由分层端点自己证明 状态 → 数字人平台已具备一份镜像、多套配置的交付能力,以及可判定的启动顺序 ``` > 配置不落盘 > 密钥不入模 > 就绪自己证 > 状态可判定 # 12、作业 ## 12.1 理解题:为什么 `environment` 优先级高于 `env_file`,但在生产里反而不该依赖这个优先级? 参考答案:优先级的存在意味着两处都能定义同一个变量,出问题时需要同时看两个来源才能定位。生产配置应该让每一个变量的来源唯一:能确定的值写进 `env_file`,需要运行时覆盖的(比如 CI 里临时指定的)才用 `environment` 覆盖。把优先级当常规手段,会制造「改了没生效」这类排查成本极高的故障。 ## 12.2 实战题:给 tts 服务加一个 secrets 挂载,并保证本地开发时即使没有密钥文件也能启动。 参考答案:在 `secrets` 顶层定义 `tts_api_key: file: ./secrets/tts_api_key`,在 tts 服务下引用它,并在 environment 里传 `TTS_API_KEY_FILE: /run/secrets/tts_api_key`。应用侧实现「文件存在且非空则读文件,否则读 `TTS_API_KEY` 环境变量,两者都没有就降级为本地免鉴权模式」。注意 Compose 的 `file` 类型 secrets 在源文件不存在时会导致 `up` 失败,所以开发环境要么保证文件存在,要么用 `docker compose -f compose.yaml -f compose.dev.yaml` 做覆盖。 ## 12.3 排障题:`docker compose up -d` 之后,某个服务一直停在 `health: starting`,从不变成 `healthy`。你怎么定位? 参考答案:先看四件事。一看 `docker inspect --format '{{json .State.Health}}' ` 拿最近几次探测的输出和退出码;二看 `--start-period` 是否足够覆盖冷启动时间(模型加载、数据库迁移);三看探针命令在容器内能否手工执行成功,如果不能,通常是镜像里没有 curl 或探针指向了错误的地址端口;四看 `depends_on: service_healthy` 是否形成了环,Compose 会拒绝创建这种依赖。退出码 0 是 healthy,1 是 unhealthy,2 是保留值——如果探针脚本里用了 `exit 2`,状态会一直是 starting 而不是 unhealthy。 ## 12.4 架构判断题:有同事主张「所有配置统一走 secrets,反正 secrets 更安全」。这个方案可行吗?代价是什么? 参考答案:技术上可行,工程上不建议。secrets 的通道语义是「敏感文件挂载」,用它承载日志级别、并发数这类非敏感参数,会带来两个代价:一是丧失了配置的可读性和可 diff 性,因为值变成了不可提交的文件内容;二是丧失了插值能力,secrets 的值不参与 Compose 的变量替换,很多需要拼地址的场景会写不下去。通道的选择依据是「谁改、敏不敏感、要不要参与插值」,不是「哪个更安全」。 > 四题四把尺 > 量通道选择 > 量探针深度 > 量边界所在 # 13、思考 这篇的核心冲突其实只有一句话:配置和密钥属于运行时,我们却一直把它们当成构建时的问题在处理。 由此可以得到一个可复用的判断:凡是「交付之后会变」的东西,都不该进镜像;凡是「不该被别人看到」的东西,都不该进元数据;凡是「会变慢」的东西,都不该用固定秒数去等。三条规则分别对应配置通道、secret 通道和健康检查,它们不是三个独立的最佳实践,而是同一条原则在三个面上的投影。 还有一个更值得记住的判断:健康检查不是给自己看的,是给编排层看的。它是一次契约——你告诉编排层「什么时候可以把请求交给我」。这个契约写得浅,编排层就会把一个还在加载模型的实例当成可用;写得深,编排层就会因为下游抖动而误杀一个本可以降级服务的实例。探针的深度,就是你对「可用」这件事的定义深度。 > 配置归运行 > 密钥离元数据 > 就绪分三层 > 探针即契约

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册