Docker 健康检查:它能发现故障,但不会替你恢复服务

本文参考《Docker 健康检查的目的、影响和处理》的选题,并根据当前 Docker 官方文档重写。其中一个关键修正是:普通 Docker Engine 将容器标记为 unhealthy 后,不会自动把它移出网络,也不会因此触发 restart policy。 容器的主进程还活着,不代表服务还能工作。线程死锁、连接池耗尽、事件循环卡死,都可能让 PID 1 继续运行,却无法处理请求。 Docker HEALTHCHECK 正是为这种差异准备的:它给正常运行状态之外再加一个 starting、healthy 或 unhealthy 的健康状态。不过它首先是检测和信号机制,不是完整的故障恢复系统。 running、healthy 和 restarted 是三件事 需要先把三个机制分开: 机制 判断什么 默认会做什么 容器状态 PID 1 是否仍在运行 进程退出后容器停止 健康检查 自定义命令是否连续成功 更新 health status,发出 health_status event restart policy 容器是否停止或异常退出 按策略重新启动已停止的容器 restart: always 或 restart: unless-stopped 关注的是“容器停止”,不是“健康状态变为 unhealthy”。如果进程卡死但没有退出,restart policy 不会被触发。 同样,Docker Engine 的普通网络不会因为某个容器 unhealthy 就自动修改 DNS 解析或隔离流量。是否摘除实例、重建任务,要看上层编排器或负载均衡器是否消费这个健康信号。 一个好的健康检查应该检查什么 健康命令应回答一个窄问题:这个容器此刻是否具备继续承担其核心职责的最低条件? 推荐特征: 检查本机回环地址,不绕公网域名和外部负载均衡; 快速、确定、资源消耗小; 有严格 timeout; 允许启动预热期; 连续失败多次才转为 unhealthy,避免单次抖动; 输出简短但可诊断,不包含密钥和个人信息。 不要把所有下游依赖都塞进一个探针。数据库短暂抖动时,如果几十个应用容器同时变 unhealthy 并被重启,可能把局部故障放大成重启风暴。应用应区分:自身已经不可恢复,还是某个下游暂时不可用但可以退避重试。 ...

2026年7月13日 · 2 分钟

密码不是加密保存:现代 Web 应用的密码存储基线

本文参考了《如何在数据库中安全地存储用户密码》的选题和层次,但按当前 OWASP 与 Spring Security 文档重新校准了算法选择、参数和迁移方式。 密码存储的目标不是“数据库里看不到明文”这么简单,而是:即使攻击者拿到了完整用户表和备份,也要让每一次离线猜测都足够昂贵。 因此密码通常不应被“加密后保存”。加密是可逆的,只要密钥泄露就能批量还原;密码验证并不需要取回原文,适合的是带随机盐、可调成本的单向密码哈希。 先排除四种错误方案 明文 数据库、备份、日志或只读账号任意一处泄露,所有密码都会立即暴露。这不是“内网系统可以接受”的折中,而是无法补救的设计缺陷。 可逆加密 如果应用能解密,攻击者在拿到密钥、环境变量或应用执行权限后也能解密。除非系统必须把这个密码转交给一个只接受原始密码的旧系统,否则应重新设计认证方式,而不是保存可逆密文。 MD5、SHA-1、SHA-256 这些通用摘要算法追求速度。文件校验时这是优点,密码存储时却意味着 GPU 可以更快尝试候选密码。即使加盐,快速哈希也只是阻止预计算表,不能显著提高单次猜测成本。 自己拼接多轮哈希 “SHA-256 三次 + 自定义 salt + 一段固定字符串”仍然缺少成熟方案的参数编码、内存成本、审查和升级机制。密码学中,难以读懂不等于难以破解。 当前算法怎么选 OWASP 当前的优先建议是: 优先使用 Argon2id,最低基线为 19 MiB 内存、2 次迭代、并行度 1; 无法使用 Argon2id 时选择 scrypt; bcrypt 更适合需要兼容的存量系统,work factor 至少为 10,并注意其 72 字节输入限制; 有 FIPS-140 要求时使用 PBKDF2-HMAC-SHA-256,并按当前指南设置迭代次数。 这些是起点,不是永远不变的常量。部署前应在生产同规格硬件上压测登录延迟和并发容量,再选择攻击成本与服务容量之间的平衡点。 盐、pepper 和工作因子分别解决什么 每个密码一个随机盐 盐让相同密码产生不同结果,也让攻击者无法用一份预计算结果同时匹配所有用户。成熟库通常会自动生成盐,并把盐、算法版本和成本参数一起编码进最终字符串;应用不需要再建一个“salt 字段”手工拼接。 盐不需要保密。它的价值来自唯一性,而不是秘密性。 工作因子 工作因子控制一次验证需要多少 CPU 和内存。参数应该能随硬件发展逐步提高,所以数据库中必须保留算法和参数信息,不能只存一段无法辨认版本的摘要。 pepper pepper 是数据库之外的额外秘密,可以保存在密钥管理服务或 HSM 中,用于纵深防御。它不是 salt 的替代品,而且轮换困难:如果旧 pepper 泄露,通常无法在不知道用户原始密码的情况下直接重算,只能要求用户重置或在后续登录时迁移。 ...

2026年7月13日 · 2 分钟

ThreadLocal 泄漏与上下文污染:线程池里的正确用法

本文以《ThreadLocal 内存泄漏问题》为选题起点,结合 Oracle 当前的 ThreadLocal 与 ScopedValue 文档重新梳理。原文的核心提醒仍然成立,但生产问题往往不只表现为 OOM,更常见的是线程复用导致的上下文污染。 ThreadLocal 不是一块独立于线程的全局缓存。更准确的理解是:每个线程内部都有一份以 ThreadLocal 对象为键的值表。调用 set() 时,值被放进当前线程;调用 get() 时,也只从当前线程读取。 这个模型非常适合保存一次同步调用链中的 request id、租户、鉴权上下文等信息。问题也恰好来自这里:只要线程还活着,线程里的值就可能继续活着。在线程池中,一个工作线程会连续处理许多互不相关的任务,忘记清理就会把上一次任务的状态带给下一次任务。 两类问题不要混为一谈 1. 内存滞留 JDK 的 ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,但 value 仍是强引用。某个 ThreadLocal 在其他地方不可达后,key 可以被 GC 清掉,value 却可能继续挂在线程的 map 中,直到后续操作触发槽位清理或线程结束。 因此,“key 是弱引用”只能降低一部分风险,不能代替 remove()。线程池里的核心线程可能与进程存活时间接近,value 如果持有大对象、类加载器或完整请求对象,滞留时间也会非常长。 把 ThreadLocal 声明成 static final 同样不是清理方案。它能避免反复创建 key,却会让这个 key 始终可达;如果没有 remove(),对应 value 仍会跟着线程长期存在。 2. 跨请求上下文污染 比 OOM 更隐蔽的故障是串数据。假设一个线程先处理租户 A 的请求,随后被线程池复用来处理租户 B: private static final ThreadLocal<String> TENANT = new ThreadLocal<>(); public void handle(Request request) { TENANT.set(request.tenantId()); service.execute(); // 忘记清理 } 只要后续某条异常路径没有重新赋值,就可能读到 A 的租户信息。这类问题不会立刻打满内存,却可能造成日志串号、错误路由,严重时甚至形成越权。 ...

2026年7月13日 · 2 分钟

把 Hugo 博客扩展成个人私密图床与内容网关

我一直希望这个博客除了放公开文章,也能顺手承担一些个人内容的展示需求,有些图片、PDF、笔记或作品集并不适合挂到首页、菜单、归档和 RSS 里,但又确实有临时分享的需要,这时我只要把指定 URL 发给对方,对方带上密码参数后就能看到内容。 单靠纯静态博客做不到真正的访问控制,因为文件一旦被 Hugo 构建到了 public/ 或放进 static/,前端的密码判断就只是在页面上多加了一层遮挡,知道真实路径的人仍有办法绕过去;考虑到这一点,我保留了 Hugo 原有的公开站点,再给它接上一个服务端私密内容网关,把需要保护的文件放到公开构建目录之外。 目标 我给这套方案定下的范围不算复杂,主要包含下面这些能力: URL 会校验 query 参数中的密码,例如 /p/trip-2026-cover?password=xxx 密码配置保存在本地文件,不需要依赖 Vercel Storage、KV、数据库或 Dashboard 环境变量 PNG、JPG、WebP、GIF、AVIF、PDF、Markdown、HTML、TXT 等常见内容都能处理 私密内容不会进入博客首页、菜单、归档、RSS 或 sitemap,访问者只能拿指定 URL 打开 同一份内容可以配置多个密码,临时分享和后续撤销都会方便一些 加上 download=1 后,可以在浏览器预览与文件下载之间切换 相册模式能让一个密码打开一组图片和 PDF,也可以生成 expires + sig 形式的过期签名链接 访问时没有密码会看到提示页,输入完成后再跳到带参数的 URL 所有私密响应都会带上 X-Robots-Tag: noindex, nofollow 与 Cache-Control: private, no-store 批量导入脚本会生成别名、密码哈希和配置模板;本机装有 ImageMagick 时,还能一并生成缩略图、水印图以及去 EXIF 版本 对外只暴露 /p/trip-2026-cover 这类 alias,真实文件名不会跟着出现在 URL 里 架构选择 实际落地时,我增加了一个 Node.js Vercel Function,入口关系如下: /p/:id -> /api/private-content?path=:id 原先的图片入口没有直接删掉,仍然可以兼容使用: ...

2026年7月9日 · 2 分钟

Java 后端校招准备的最小可行清单

这份清单写给正在准备 Java 后端校招(实习 / 提前批 / 秋招)的同学,正常安排时间的话,3–6 个月内可以执行完。 它并不是把 “Java 八股文目录” 又抄一遍,而是我复盘后留下的一份 执行计划,哪些事情需要做、哪些可以暂时放下,以及做到什么程度才算够,文中都会讲清楚。 写在最前 准备校招时,真正让很多同学难受的往往不是 “没东西学”,恰恰是能学的内容太多了: 八股、项目、算法和行为面试会 一起压过来,时间很快就被切碎 看见别人都在刷剑指 Offer、补 JVM 视频,自己却 判断不出最缺的是哪一块 真进了面试,又可能被一个 没准备过的问题 卡住,随后开始怀疑前面学的内容是不是全都白费了 手里只有 3–6 个月时,可以把任务收拢到 5 个核心模块 和 1 个心态模块,先保证主要部分都有覆盖,不需要逼着自己做到面面俱到。 一、基础知识:覆盖面 vs 深度 1.1 必须掌握的基础 Java 后端面试的问题会有变化,不过下面这些基础模块 被问到的概率一直很高: 模块 必须掌握 可选深入 Java 基础 集合、泛型、异常、IO、反射、注解 字节码、Unsafe、ClassLoader JVM 内存区域、GC 算法、垃圾收集器、类加载 JIT、逃逸分析、ZGC / Shenandoah 并发 synchronized、Lock、AQS、volatile、线程池 CAS 内存语义、JMM happens-before Spring IoC、AOP、Bean 生命周期、事务、@Transactional Spring Boot 自动配置、starter 原理 MySQL 索引、事务、隔离级别、慢查询、explain InnoDB MVCC、redo / undo / binlog Redis 数据结构、过期策略、持久化、热 key / 大 key 集群分片、主从复制、cluster slot 计算机基础 TCP / HTTP、操作系统进程线程、IO 模型 epoll、零拷贝、NUMA 判断标准:不看背诵材料,也能用 自己的话 说清楚 “为什么会这样设计”,同时还能联系到一个实际工程场景。 ...

2026年5月8日 · 3 分钟

Java 后端开发者理解 Vue:从 Controller 思维到响应式 UI

Java 后端开发者理解 Vue:从 Controller 思维到响应式 UI 如果你已经写过 Java,也熟悉 Spring、Controller、Service、DTO、Repository、拦截器、配置文件和请求响应链路,再去看 Vue 代码时,一般不会到“完全看不懂”的程度,变量、函数、条件判断和数组遍历都不陌生,看到 <div>、<span>、class、@click、:disabled,多少也能猜到它们会在页面上产生什么效果。 真正容易让人卡住的并不是语法,而是我们会很自然地拿后端那套心智模型去套前端,很多似乎合理的类比,也正是在这里开始偏掉了。 比如,有人会把 .vue 文件里的 <template> 看成 Spring XML、JSP、Thymeleaf 一类配置或模板,把 props 当作 DTO 字段,还会觉得 emit() 就是在直接调用父组件方法,v-model 则是子组件越过边界修改了父组件变量;页面一有变化,又下意识认为它“重新请求了一次接口并刷新了页面”,这些直觉在入门时能帮上一点忙,但继续理解 Vue 的核心机制时,就会带来不少误会。 下面不会从头罗列一遍 Vue API,而是从 Java/Spring 后端开发者容易困惑的地方切入,把 Vue 页面为何会自动更新、ref() 与 .value 的关系、v-model 双向绑定、:xxx 和 @xxx 的含义,以及 props 向下 emit 向上的通信方式慢慢串起来;Element Plus、插槽、scoped CSS、:deep() 这些经常在项目里碰到的内容,也会放到具体场景中解释。 1. 一个需要换掉的旧直觉:Vue 页面不是一次请求生成一次响应 写后端时,我们最熟悉的处理链路通常是: HTTP 请求 -> Controller -> Service -> Repository / Mapper -> 返回 JSON 或 HTML -> 本次请求结束 这套模型围绕“一次请求、一次处理、一次响应”展开,请求结束以后,本轮执行基本也就结束了。 ...

2026年5月5日 · 11 分钟

Java 性能问题定位:从系统指标到应用排查

Java 性能问题定位:从系统指标到应用排查 这篇笔记参考了阿里云相关博客中的排查思路,原文写得很细,我在此基础上按自己的使用习惯重新做了整理。 线上 Java 应用一旦出现响应变慢、资源飙升或进程异常,排查范围很容易越铺越大,下面会从业务代码、CPU、线程、内存、GC、磁盘 I/O、网络 I/O 和常用命令几个角度,把常见的定位方法串起来。 实际的性能问题很少只对应一个异常指标,更多时候是系统层、组件层与应用层互相影响后的结果;排查时可以从最贴近业务的应用层入手,拿到初步方向后,再回到系统指标中交叉验证。 性能优化工具图谱 ├── 系统层 │ ├── CPU │ │ ├── 🚩 CPU利用率(top/vmstat/sar/dstat) │ │ ├── 🚩 CPU平均负载(top/uptime) │ │ └── 上下文切换次数(pidstat/vmstat/dstat) │ ├── 内存 │ │ ├── 全局内存使用 │ │ │ ├── 🚩 已用/剩余/可用内存(free/vmstat/sar) │ │ │ └── 缓冲区/缓存(pcstat/cachestat/cachetop) │ │ └── 进程内存使用 │ │ ├── 🚩 虚拟内存/常驻内存/共享内存(top/ps/pidstat) │ │ ├── 🚩 SWAP 内存使用/换入换出速度(top/free/vmstat/sar) │ │ ├── 缺页异常(ps/pidstat) │ │ └── 内存分布(pmap/jmap) │ ├── 磁盘 │ │ ├── 空间容量(df/du) │ │ ├── 🚩 吞吐量/磁盘 I/O 使用率(iostat/dstat/sar) │ │ └── 缓冲区/缓存(pcstat/cachestat/cachetop) │ └── 网络 │ ├── 🚩 吞吐量(sar) │ ├── 网络延迟(ping) │ ├── 🚩 网络连接数/错误数(netstat/ss/sar) │ └── 网络抓包(tcpdump/wireshark) ├── 组件层 │ ├── 数据库 │ │ ├── SQL 调优 │ │ ├── 🚩 索引调优 │ │ └── 连接池配置 │ ├── 网络 IO │ │ ├── I/O 调度模型 │ │ ├── 序列化框架 │ │ └── 线程调度模型 │ ├── Web 容器 │ │ └── 线程池配置 │ └── 缓存/MQ…… └── 应用层 ├── 线程 │ ├── 死锁检查(jstack/arthas) │ ├── 🚩 线程状态分布(jstack/arthas) │ ├── 锁竞争分布(jstack/arthas) │ ├── 🚩 代码执行热点(jprofiler/zprofiler) │ ├── 🚩 占用 CPU 较重的线程(top + pidstat + jstack) │ └── 代码追踪(btrace/housemd/greys/arthas) ├── 内存 │ ├── 内存分配 │ │ ├── 常驻内存/虚拟内存(top) │ │ ├── 对象分配热点(jprofiler/zprofiler) │ │ ├── 🚩 堆内对象分布(jmap/zprofiler/MAT) │ │ ├── 类加载相关(jstat/greys/arthas) │ │ ├── 🚩 内存泄漏(gperf/MAT/zprofiler) │ │ └── 堆外内存(jmap + MAT + NMT + gdb + perf) │ └── 垃圾回收 │ ├── GC 线程使用(jinfo) │ ├── 对象晋升年龄(gclog) │ ├── 🚩 GC 的频率和时间(jstat/gclog) │ ├── 垃圾回收器类型/JVM 参数(jinfo/jcmd) │ └── 🚩 堆大小设置及分区大小(jinfo/jstat) ├── 网络 │ ├── 带宽使用 │ ├── 流量异动 │ └── 网络分区 └── ★ 业务(日志、监控…) ├── 🚩 代码逻辑 ├── 远程调用 └── 架构设计 CPU 与线程 观察 CPU 时,最常看的指标是下面三项,常用工具包括 top、ps、uptime、vmstat 和 pidstat。 ...

2026年5月3日 · 8 分钟

大语言模型的可观测性应该有哪些

LLM 与 AI Agent 可观测性 传统微服务常看请求量、错误率和耗时,到了 AI 应用里,也可以先用 Token、Error、Duration 建立一组基础指标;不过这三项只能说明服务有没有运行、花了多少钱、速度是否正常,还不能回答模型为什么给出了这个结果,更无法判断一次 HTTP 200 背后的答案是不是正确。 对普通 LLM 调用来说,一次请求已经可能包括 prompt 拼装、模型推理、内容清洗和结果返回;换成 LLM Agent / AI Agent 后,链路还会加入意图识别、RAG 检索、工具选择、参数构造、多次 LLM 调用与重试,因此真正需要建设的不是“多打一批日志”,而是让整条执行轨迹能够被还原、评分和归因。 1. 为什么传统 APM 还不够 传统 APM 擅长回答“哪里慢了”“哪里报错了”,但 AI Agent 会出现一些没有异常堆栈的失败,例如模型很自信地答错、选中了相似但不适用的工具、参数类型正确却语义错误,或者在几个工具之间来回循环,最终造成 token 和账单持续增长;这些现象在 HTTP 状态码上可能全部是成功,用户拿到的结果却不可用。 因此,AI Agent 可观测性至少要能回答下面几类问题: 一次任务内部执行过哪些步骤,每一步花了多少时间和 Token RAG 召回了哪些文档,排序和 score 是否合理,最终答案有没有使用这些证据 Agent 选择了哪个工具,tool args 是否正确,调用顺序与依赖关系有没有被满足 最终输出是否准确、相关、完整,格式和安全要求有没有达到 失败发生在 Prompt、retrieval、Tool、LLM 还是 orchestration,能否复现到具体 span 单次请求、用户、组织和模型分别产生了多少成本,异常调用是否触发了熔断或 fallback AgentTrace 将结构化轨迹分为 operational、cognitive、contextual 三类 surface,并把可观测性用于安全、可复现性、问责与实时监控;这类设计比单纯保存最终 input/output 更接近 Agent 的实际运行方式。 ...

2026年4月25日 · 5 分钟