在容器里开发 — 热重载、逐行调试、日志与分布式追踪¶
这一章解决一个具体的怨念: "容器很好用,但每次改代码都要重建镜像、重跑容器,开发变慢了。" 作者把这个摩擦拆成三级台阶,逐级拆掉:改动进得了容器(绑定挂载)→ 应用自动重启、 调试器进得去(热重载 + attach)→ 生产环境看不见的时候还能排查(日志 + 追踪)。 主线例子只有一个:一个 Node.js 小应用的
/colors端点,从"改完不生效"一路进化到 "在 Jaeger 里看到它跨服务的调用链"。
1. 顶层全景:三级台阶¶
第 ① 级 改动进容器 -v $(pwd):/app 把源码挂进去(第 05 章的 bind mount)
第 ② 级 应用自动重启 nodemon / devtools / --reload / watch(语言各一把工具)
调试器 attach --inspect=0.0.0.0 + 映射调试端口 + VS Code launch.json
第 ③ 级 生产可排查 结构化日志(级别过滤、写 STDOUT)
分布式追踪(OpenTelemetry 埋点 → Jaeger 看调用链)
图说:①②管开发期,③管生产期。作者在每一级都先演示"不行"再给"行",本章照这个顺序走。
2. 核心原理:第一级——改动进容器,为什么不生效¶
书里的实验很诚实。容器以挂载方式跑 Node.js 应用,改代码保存,容器里的文件确实变了
(用 docker container exec my-sample-app cat index.js 验证过,新代码在),
但应用行为没变——因为 Node.js 把代码读进内存后不会重读,必须重启进程才生效1。
手动重启容器能解决问题,但"改一次重启一次"的摩擦还在2。
这一级留下的教训:挂载解决的是"文件同步",不解决"进程感知"。下一级才是进程的事。
3. 核心原理:第二级——四种语言的热重载¶
每种语言都有自己的"文件一变就重启进程"的工具,书里逐个演示(都遵循同一模式: 开发专用 Dockerfile + 挂载源码 + 覆盖启动命令):
| 语言 | 工具 | 容器里的启动命令 | 备注 |
|---|---|---|---|
| Node.js | nodemon | nodemon --inspect=0.0.0.0 index.js |
解析 package.json 自动推断启动命令;改文件即重启,终端打 [nodemon] restarting due to changes...3 |
| Java/Spring Boot | Spring Boot DevTools | 依赖加进 pom.xml 即可 | 改代码后自动重新编译并重启(注意是 rebuild,不只是 reload)4 |
| Python/Flask | debug=True 或 nodemon | flask run --reload;或 nodemon --exec python main.py |
debug 模式自带 reloader;作者提醒 Flask 要监听 0.0.0.0 才能被容器外访问5 |
| .NET/C# | dotnet watch | dotnet watch run(容器里配 --urls http://0.0.0.0:5000) |
监听变化自动重启;--urls 必须显式绑 0.0.0.0,默认 localhost 容器外不可达6 |
配套模式值得单独说:开发专用 Dockerfile(书里的叫法 Dockerfile.dev)——
在基础镜像上多装一个热重载工具、把启动命令换成它;构建时用 -f Dockerfile.dev 指定
非默认文件名,生产 Dockerfile 保持干净7。
主线例子的开发循环(至此达成):
改 index.js → 存盘 → nodemon 重启容器内应用 → curl localhost:3000/colors 立刻看到新结果
全程:不重建镜像、不手动重启容器[^8]
图说:这就是作者说的"和本地开发一样顺,但环境仍是容器"。
调试器进容器:--inspect + attach¶
逐行调试这一节,作者开头先立了免责声明:这应当是最后手段——理想情况下单测/集成测 已经兜住,实在不行可以在宿主机跑代码用 IDE 调试;真到在容器里逐行调试,说明前面的 防线都没挡住9。做法(Node.js 版):
① 容器带调试参数跑,并映射调试端口:
$ docker container run --rm -it --init --name node-demo \
-p 3000:3000 -p 9229:9229 \
-v $(pwd):/app \
node-demo-dev node --inspect=0.0.0.0 index.js
↑ 9229 是 Node.js 调试协议端口;--inspect=0.0.0.0 让调试器监听容器所有网卡
(书特意提醒:这个端口只在调试会话期间开!)[^10]
② VS Code 加 .vscode/launch.json,type 为 node、request 为 attach
③ 代码里点行号设断点 → RUN AND DEBUG 视图选 Attach → 绿按钮
④ curl 打一下端点 → 执行停在断点,可看变量、调用栈;容器终端显示 debugger is attached[^11]
一个真实的坑:配了 nodemon 后,应用一重启调试器就断开。解法是 launch.json 里加
"restart": true——VS Code 会在应用重启后自动重新 attach,改代码→热重载→断点
重新命中,闭环完成12。
.NET 版更省事:VS Code 命令面板执行 Docker: Add Docker Files to Workspace, 选平台(.NET ASP.NET Core)、操作系统(Linux)、端口,它自动产出多阶段 Dockerfile、 .dockerignore 和一套 launch.json/tasks.json——绿按钮一点,自动构建镜像、跑容器、 配好调试,浏览器自动打开并停在断点13。
4. 核心原理:第三级——生产环境的眼睛¶
生产环境不能 attach、不能随便重启,书里给了两件替代武器。
4.1 日志:级别与去向¶
日志分级(书里给了完整六级表,是行业通用口径)14:
| 级别 | 含义 |
|---|---|
| TRACE | 最细颗粒,抓应用行为的每个细节 |
| DEBUG | 较细的诊断信息,定位潜在问题 |
| INFO | 正常行为/里程碑(启动、关闭) |
| WARNING | 可能有问题或不寻常的情况 |
| ERROR | 严重问题,重要任务失败 |
| FATAL | 灾难性失败,建议立即关闭应用 |
去向:sink(日志槽)。 日志库可以把输出导向多个 sink;容器里的铁律再次出现——
导向控制台/STDOUT,Docker 用 docker container logs 收,Logstash/Fluentd/Loki
等采集器再从那里刮走15。
Python 演示了级别过滤的机制:logger 设了 WARNING 阈值,代码里一条 INFO、
一条 WARNING,输出里只有 WARNING 那条——"级别 ≥ 阈值的才输出"16。
.NET 对应 Microsoft.Extensions.Logging 包17。
4.2 分布式追踪:日志给不了的那张图¶
为什么日志不够: 一个请求穿过多个服务时,每处日志是碎片;要知道"这个请求端到端 走了多久、卡在哪个服务",需要追踪(trace)——把一次请求在系统中的全程,连带 每个环节耗时,串成一条18。
OpenTelemetry + Jaeger 的架构(书里给的概念图)19:
应用进程(埋点) 收集与展示
┌─────────────────┐ OTLP ┌──────────────────────────┐
│ OpenTelemetry SDK │ ──────▶ │ Jaeger all-in-one(容器) │
│ 自动/自定义 span │ 4317 │ 16686 = Web UI │
└─────────────────┘ │ 4317 = 接收端 │
└──────────────────────────┘
docker run -d --name jaeger -e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 -p 4317:4317 jaegertracing/all-in-one:latest
图说:应用里的 OTel SDK 把 span 发到 Jaeger;16686 是给人看的 UI,4317 是给机器走的端口。
主线例子收尾在两个服务上。Java 示例有 service1 和 service2:
curl localhost:8080/test → service1 内部再调 service2 的 /nested →
Jaeger 里能看到一条 trace 里嵌着两次调用,各自耗时多少,清清楚楚20。
.NET 版演示了自定义 span:给 /hello/{name} 端点手工开一个叫 FormatString 的
activity,还带一个 parameter.name=Gabriel 的标签——trace 详情里能看到这个
自定义环节嵌在默认端点活动之下21。
书的最后顺手给了一个 docker-compose.yml 把 Jaeger + 两个服务一把起,
用环境变量 OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:14250 把各服务的
追踪指向 Jaeger 容器——这是第 11 章 Compose 的预告22。
5. 作者的判断与证据¶
- "逐行调试是最后手段":这是明确的立场,依据是测试驱动开发的前提(单测/集成测 已保证大部分正确性)和"宿主机调试更简单"的替代路径9。
- "--inspect 端口只在调试时开":安全提醒,与第 13 章的最小暴露原则一致10。
- 热重载四连是工具教学,非观点;但"开发 Dockerfile 与生产 Dockerfile 分离" (Dockerfile.dev)是一个可带走的架构判断7。
- OpenTelemetry 章的定位诚实:明说"不讲 Jaeger 的全部细节,只给概念层 overview"19。
判断(我们的,不是书里的): 本章真正的主角不是某个工具,而是"摩擦预算"这个概念—— 容器化每引入一处摩擦(重建、重启、进不去),开发流程就会悄悄绕开容器, "在我机器上能跑"就会回来。作者逐个消灭摩擦,本质是在保卫"开发与生产同环境" 这条容器最大的卖点。 如果错,会错在: 对于不依赖热重载的语言/场景(比如纯函数式批处理), "改代码→重建镜像"的摩擦本来就小,本章的整套手艺权重应相应降低; 以及 AI 编程工具普及后,"本地直接跑"的吸引力回升,容器开发流是否仍是默认答案,值得持续观察。
6. 边界与局限¶
- 四种语言的热重载是"每个一段"的速写,Java/Python/.NET 部分大半是装环境 (JDK 21、Python 3.13、.NET 9)的逐步截图,机制本身一页就够——照做时以各工具 当前文档为准。
- Jaeger 只到"看到 trace"为止:采样策略、生产级 Jaeger 架构(agent/collector 分离)、trace 与 metrics/logs 的联动,都没展开。
- macOS 上端口 5000 常被系统服务占用(AirPlay),书里的对策是换 5001—— 这类平台怪癖 scattered 在各节,需要自己留意5。
--init参数(容器里跑 nodemon 时回收僵尸进程)书里反复使用但没解释, 第一次见会困惑(补充,不在书里:--init 在容器里起一个微型 init 进程,负责收养孤儿进程)。
7. 可带走的¶
- 挂载让文件同步,热重载让进程感知——两级缺一不可1。
- Dockerfile.dev 模式:开发镜像多装热重载工具,
-f Dockerfile.dev构建,生产镜像保持干净7。 - 四语言热重载:nodemon、Spring DevTools、Flask
--reload、dotnet watch3456。 - 服务要绑 0.0.0.0,绑 localhost 容器外永远不可达(.NET 要
--urls,Flask 要host="0.0.0.0")56。 - Node 调试 =
--inspect=0.0.0.0+ 映射 9229 + VS Code attach;应用会重启就加"restart": true1012。 - .NET 偷懒神器:Docker: Add Docker Files to Workspace,一条命令配好整套调试13。
- 日志六级:TRACE/DEBUG/INFO/WARNING/ERROR/FATAL;容器里一律写 STDOUT1415。
- 级别阈值过滤:logger 设 WARNING,INFO 就不出声——配置一次,全局生效16。
- 追踪 = 请求的端到端视图:OpenTelemetry 埋点 → OTLP 4317 → Jaeger 16686; 跨服务调用链和分段耗时一眼可见1920。
8. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 改动进容器但不生效 | Auto-restarting code upon changes | text/72-fm-auto-restarting-code-upon-changes.txt:12(搜「cat index.js」) · text/72-fm-auto-restarting-code-upon-changes.txt:26(搜「propagated into the container」) |
| 手动重启的残留摩擦 | Auto-restarting code upon changes | text/72-fm-auto-restarting-code-upon-changes.txt:38(搜「manually restart」) |
| nodemon 行为 | Auto-restarting code upon changes | text/72-fm-auto-restarting-code-upon-changes.txt:63(搜「nodemon」) · text/72-fm-auto-restarting-code-upon-changes.txt:89(搜「restarting due to changes」) |
| Dockerfile.dev 模式 | Auto-restarting code upon changes | text/72-fm-auto-restarting-code-upon-changes.txt:111(搜「does not contain nodemon」) · text/72-fm-auto-restarting-code-upon-changes.txt:124(搜「-f Dockerfile.dev」) |
| 挂载+热重载的开发循环 | Auto-restarting code upon changes | text/72-fm-auto-restarting-code-upon-changes.txt:132(搜「--init」) · text/72-fm-auto-restarting-code-upon-changes.txt:138(搜「automatically restarted」) |
| Spring DevTools | Auto-restarting code upon changes | text/72-fm-auto-restarting-code-upon-changes.txt:270(搜「dev tools」) · text/72-fm-auto-restarting-code-upon-changes.txt:280(搜「automatically rebuilds」) |
| Flask debug/reload、0.0.0.0 | Auto-restarting code upon changes | text/72-fm-auto-restarting-code-upon-changes.txt:371(搜「host=」) · text/72-fm-auto-restarting-code-upon-changes.txt:394(搜「debug=True」) · text/72-fm-auto-restarting-code-upon-changes.txt:403(搜「nodemon --exec」) |
| dotnet watch、--urls | Auto-restarting code upon changes | text/72-fm-auto-restarting-code-upon-changes.txt:584(搜「watch command」) · text/72-fm-auto-restarting-code-upon-changes.txt:587(搜「dotnet watch run」) · text/72-fm-auto-restarting-code-upon-changes.txt:611(搜「--urls」) |
| 调试免责声明 | Line-by-line code debugging inside a container | text/73-fm-line-by-line-code-debugging-inside-a-container.txt:4(搜「last resort」) |
| --inspect、9229、只在调试开 | Line-by-line code debugging inside a container | text/73-fm-line-by-line-code-debugging-inside-a-container.txt:38(搜「9229」) · text/73-fm-line-by-line-code-debugging-inside-a-container.txt:45(搜「only during a debugging session」) |
| attach 与断点命中 | Line-by-line code debugging inside a container | text/73-fm-line-by-line-code-debugging-inside-a-container.txt:77(搜「green start button」) · text/73-fm-line-by-line-code-debugging-inside-a-container.txt:85(搜「stops at the breakpoint」) |
| restart:true 重连 | Line-by-line code debugging inside a container | text/73-fm-line-by-line-code-debugging-inside-a-container.txt:119(搜「loses its connection」) · text/73-fm-line-by-line-code-debugging-inside-a-container.txt:128(搜「restart」) |
| .NET Add Docker Files | Line-by-line code debugging inside a container | text/73-fm-line-by-line-code-debugging-inside-a-container.txt:178(搜「Add Docker Files」) · text/73-fm-line-by-line-code-debugging-inside-a-container.txt:208(搜「build the Docker images」) |
| 日志六级表 | Instrumenting your code to produce meaningful logging information | text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:24(搜「severity levels」) · text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:30(搜「TRACE」) |
| sink 与 STDOUT 铁律 | Instrumenting your code to produce meaningful logging information | text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:48(搜「log sinks」) · text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:50(搜「console or STDOUT」) |
| Python 级别过滤演示 | Instrumenting your code to produce meaningful logging information | text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:70(搜「WARNING」) · text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:115(搜「only the WARNING」) |
| .NET 日志包 | Instrumenting your code to produce meaningful logging information | text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:141(搜「dotnet add package」) |
| 为什么需要追踪 | Using OpenTelemetry and Jaeger to monitor and troubleshoot | text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:11(搜「trace a single request」) · text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:14(搜「time spent on each」) |
| OTel+Jaeger 概念与 all-in-one | Using OpenTelemetry and Jaeger to monitor and troubleshoot | text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:19(搜「OpenTelemetry」) · text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:62(搜「jaegertracing/all-in-one」) |
| 自定义 span 与嵌套 | Using OpenTelemetry and Jaeger to monitor and troubleshoot | text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:89(搜「activity called FormatString」) · text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:113(搜「nested」) |
| 跨服务 trace(service1→service2) | Using OpenTelemetry and Jaeger to monitor and troubleshoot | text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:175(搜「distributed call」) · text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:188(搜「originated」) |
| compose 起整套追踪 | Using OpenTelemetry and Jaeger to monitor and troubleshoot | text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:214(搜「docker-compose.yml」) · text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:232(搜「OTEL_EXPORTER」) |
| 章小结 | Summary | text/76-fm-summary.txt:3(搜「bind-mounting」) |
-
出处:「Auto-restarting code upon changes」第 5–35 段(
text/72-fm-auto-restarting-code-upon-changes.txt:12,搜「cat index.js」;:26,搜「propagated into the container」;:28,搜「has to be restarted」)。 ↩↩ -
出处:「Auto-restarting code upon changes」第 36–43 段(
text/72-fm-auto-restarting-code-upon-changes.txt:38,搜「manually restart」;:40,搜「automate」)。 ↩ -
出处:「Auto-restarting code upon changes」第 57–99 段(
text/72-fm-auto-restarting-code-upon-changes.txt:63,搜「most popular tool is nodemon」;:68,搜「npm install -g nodemon」;:89,搜「restarting due to changes」)。 ↩↩ -
出处:「Auto-restarting code upon changes」第 148–321 段(
text/72-fm-auto-restarting-code-upon-changes.txt:270,搜「dev tools」;:280,搜「automatically rebuilds」;:299,搜「-v $(pwd)/.:/app」)。 ↩↩ -
出处:「Auto-restarting code upon changes」第 325–488 段(
text/72-fm-auto-restarting-code-upon-changes.txt:371,搜「host="0.0.0.0"」;:394,搜「debug=True」;:403,搜「nodemon --exec」)。 ↩↩↩↩ -
出处:「Auto-restarting code upon changes」第 492–663 段(
text/72-fm-auto-restarting-code-upon-changes.txt:587,搜「dotnet watch run」;:611,搜「--urls」;:612,搜「0.0.0.0」)。 ↩↩↩ -
出处:「Auto-restarting code upon changes」第 100–147 段(
text/72-fm-auto-restarting-code-upon-changes.txt:111,搜「does not contain nodemon」;:113,搜「Dockerfile.dev」;:124,搜「-f Dockerfile.dev」;:127,搜「non-standard name」)。 ↩↩↩ -
出处:「Auto-restarting code upon changes」第 130–147 段(
text/72-fm-auto-restarting-code-upon-changes.txt:138,搜「automatically restarted」;:139,搜「reduction in friction」)。 ↩ -
出处:「Line-by-line code debugging inside a container」第 1–13 段(
text/73-fm-line-by-line-code-debugging-inside-a-container.txt:4,搜「last resort」;:8,搜「running your code directly on your host」)。 ↩↩ -
出处:「Line-by-line code debugging inside a container」第 32–49 段(
text/73-fm-line-by-line-code-debugging-inside-a-container.txt:38,搜「9229」;:40,搜「--inspect=0.0.0.0」;:43,搜「only during a debugging session」)。 ↩↩ -
出处:「Line-by-line code debugging inside a container」第 77–99 段(
text/73-fm-line-by-line-code-debugging-inside-a-container.txt:83,搜「localhost:3000/colors」;:85,搜「stops at the breakpoint」;:91,搜「CALL STACK」)。 ↩ -
出处:「Line-by-line code debugging inside a container」第 119–144 段(
text/73-fm-line-by-line-code-debugging-inside-a-container.txt:119,搜「loses its connection」;:128,搜「restart」)。 ↩↩ -
出处:「Line-by-line code debugging inside a container」第 158–214 段(
text/73-fm-line-by-line-code-debugging-inside-a-container.txt:178,搜「Add Docker Files」;:186,搜「multistage」;:208,搜「build the Docker images」)。 ↩↩ -
出处:「Instrumenting your code to produce meaningful logging information」第 24–46 段(
text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:24,搜「severity levels」;:30,搜「TRACE」;:43,搜「FATAL」)。 ↩↩ -
出处:「Instrumenting your code to produce meaningful logging information」第 48–52 段(
text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:48,搜「log sinks」;:50,搜「console or STDOUT」)。 ↩↩ -
出处:「Instrumenting your code to produce meaningful logging information」第 66–118 段(
text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:70,搜「WARNING」;:115,搜「only the WARNING message」)。 ↩↩ -
出处:「Instrumenting your code to produce meaningful logging information」第 127–150 段(
text/74-fm-instrumenting-your-code-to-produce-meaningful-lo.txt:141,搜「dotnet add package」;:141,搜「Microsoft.Extensions.Logging」)。 ↩ -
出处:「Using OpenTelemetry and Jaeger to monitor and troubleshoot」第 5–27 段(
text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:11,搜「trace a single request」;:14,搜「time spent on each」)。 ↩ -
出处:「Using OpenTelemetry and Jaeger to monitor and troubleshoot」第 16–27 段与第 53–62 段(
text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:19,搜「OpenTelemetry」;:26,搜「high-level」;:62,搜「jaegertracing/all-in-one」)。 ↩↩↩ -
出处:「Using OpenTelemetry and Jaeger to monitor and troubleshoot」第 152–191 段(
text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:175,搜「distributed call」;:178,搜「localhost:8080/test」;:188,搜「originated in the /test endpoint」)。 ↩↩ -
出处:「Using OpenTelemetry and Jaeger to monitor and troubleshoot」第 80–117 段(
text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:89,搜「activity called FormatString」;:113,搜「nested」;:113,搜「parameter.name」)。 ↩ -
出处:「Using OpenTelemetry and Jaeger to monitor and troubleshoot」第 209–240 段(
text/75-fm-using-opentelemetry-and-jaeger-to-monitor-and-tr.txt:214,搜「docker-compose.yml」;:232,搜「OTEL_EXPORTER_JAEGER_ENDPOINT」)。 ↩