读《深入理解 Java 虚拟机》:从背 JVM 知识点到建立问题证据链

本文以《深入理解 Java 虚拟机》学习笔记为阅读入口,结合当前 JDK 25 诊断与 GC 文档重写。这里不再按章节列知识点,而是讨论怎样把 JVM 原理变成生产排查能力。 学习 JVM 很容易掉进两个极端:一种只背“堆、栈、方法区”,另一种一遇到问题就复制一串 -XX 参数。前者解释不了线上现象,后者可能暂时改变现象,却没有证明原因。 这本书真正值得反复阅读的地方,是它建立了从语言代码到虚拟机实现、再到操作系统资源的映射。把这套映射用于工作时,核心不是记住多少名词,而是形成下面这条链路: 用户症状 → 系统指标 → JVM 运行数据 → 可复查证据 → 代码原因 → 对照验证 内存区域图只是起点,进程预算才是现场 -Xmx4g 不代表 Java 进程最多只用 4 GiB。容器或宿主机看到的是整个进程,它还包含: 区域 常见内容 典型风险 Java Heap 普通对象和数组 泄漏、缓存无界、分配过快 Metaspace 类元数据 动态生成类、ClassLoader 无法回收 Thread Stack 平台线程栈帧 线程数过多、单栈配置过大 Direct/Native Memory DirectByteBuffer、JNI、GC 结构等 堆很健康但 RSS 持续增长 Code Cache JIT 编译后的机器码 编译受限、性能退化 因此容器内存规划不能只把 limit 设置成 Xmx。至少要给 metaspace、直接内存、线程栈、JIT、GC 和本地库留下余量,并用实际工作负载观察 RSS 峰值。 ...

2026年7月15日 · 2 分钟

读《凤凰架构》:架构的价值不是先进,而是匹配当前复杂度

本文以《凤凰架构》阅读笔记为阅读入口,结合《凤凰架构》当前公开在线文档重新组织。它不是章节摘要,而是我认为最值得带回日常项目的几条判断标准。 很多架构讨论很像技术产品展览:Kubernetes、Service Mesh、事件驱动、DDD、Serverless 都被摆在台面上,却没有先回答业务规模、团队结构和失败成本。 读《凤凰架构》后,我最强烈的感受是:架构的价值不在于“用了更先进的东西”,而在于让团队能够承受当前复杂度,并为下一次变化留下空间。 架构不是技术栈,而是一组约束下的选择 同一个方案放进不同团队,结论可能完全相反。 一个五人团队维护六七个强耦合服务,通常没有获得独立交付的收益,却要承担网络故障、接口兼容、分布式追踪、多仓库发布和数据一致性的成本。反过来,一个数百人共同修改同一单体、任何发布都要全员协调的系统,也可能已经超过单体边界能够承受的组织复杂度。 所以架构评审不应先问“要不要微服务”,而应先写清楚: 一个需求通常会修改几个模块、由几个团队协作; 模块能否独立测试、发布和回滚; 数据所有权是否清晰; 故障需要隔离到什么范围; 团队是否具备监控、发布和故障处理能力; 引入新边界后,沟通成本究竟下降还是上升。 工具只是答案的一部分,组织和业务约束才是问题本身。 微服务边界的核心是独立交付 微服务不是把 Controller 按名词拆成多个仓库,也不是一个数据库表对应一个服务。一个有意义的服务边界至少应具备三种性质: 内聚:一组高频共同变化的业务规则留在一起; 自治:服务可以独立开发、验证、发布和恢复; 可理解:团队能够说清它拥有的数据、能力和对外契约。 如果一次业务变更总要同时修改五个服务、开五个 PR、等待固定顺序发布,那么只是把单体内部调用变成了远程调用。代码目录看起来更“分布式”,实际交付仍然是一个整体。 拆分前可以先做一次变更耦合分析:统计最近三个月的需求和故障,看看哪些模块总是一起改、哪些团队总要一起发布。真实变更历史通常比一张理想领域图更能暴露边界。 基础设施下沉,但业务语义不能一起下沉 把通用能力交给数据库、容器平台或网关,能减少应用中的重复样板。例如: 唯一约束交给数据库,避免并发下的 check-then-insert; 服务发现、健康检查和滚动发布交给编排平台; TLS 终止、基础鉴权和流量限制放到统一入口; 日志、指标和追踪通过平台约定采集。 不过“基础设施下沉”不能理解为把所有正确性都交给平台。库存不能为负、订单只能支付一次、退款必须经过审批,这些是业务不变量,仍应在领域代码、事务和数据约束中明确表达。 判断一项能力该放在哪里,可以问三个问题: 它是否与具体业务语义无关; 平台实现是否比每个应用重复实现更可靠; 下沉后是否仍能被测试、观察和局部覆盖。 如果答案不清楚,先保留在应用边界内,等语义稳定后再平台化。过早下沉会把一次局部需求变成全平台升级。 演进不是持续加层,而是允许替换 系统会随着需求、人员和规模变化而老化,这并不证明最初设计失败。一个简单单体如果帮助产品验证了市场、让团队建立了领域认识,它已经完成了当前阶段的使命。 真正危险的是假设现有结构必须永远保留,于是每次变化只能继续加适配层、同步任务和例外配置。久而久之,所有人都知道结构不合理,却没人敢动。 演进式架构需要给每个阶段性方案写出退出条件: 当前方案解决什么问题: 可以承受的规模与团队边界: 已知限制: 需要持续观察的指标: 触发重新设计的条件: 数据和调用方怎样迁移: 失败时怎样回退: 例如“当前先使用单库”不是欠债声明。只要同时明确容量假设、热点指标、备份恢复和未来分区键候选,它就是一个有边界的工程决策。 大规模重写不是演进的默认方式 接受系统可替换,不等于轻易宣布全面重写。大爆炸式重写经常同时失去三样东西:旧系统里没写下来的业务规则、真实流量验证,以及随时回滚的能力。 更稳妥的迁移通常包含: 先建立可观测基线,知道旧系统当前表现; 在旧边界外建立新接口或防腐层; 选择一条低风险、可独立验证的路径切流; 新旧实现并行对比结果; 小比例流量开始,保留快速回退; 数据迁移有校验、补偿和最终完成条件; 确认没有调用方后再删除旧路径。 演进的本质是控制变化风险,而不是让新旧两套系统永久共存。每一层兼容代码都应有负责人和删除日期。 复杂度像预算,不是越少越好 微服务、消息队列、缓存和容器平台都会引入复杂度,但它们也可能解决真实问题。关键不是追求“零复杂度”,而是判断新增复杂度是否买到了可量化收益。 可以把每次架构升级写成一份复杂度预算: 新增机制 希望购买的收益 新的失败模式 谁负责 消息队列 削峰、异步解耦 重复、乱序、积压 业务团队 + 平台 服务拆分 独立发布、故障隔离 网络、契约、分布式一致性 服务所有者 Kubernetes 标准化调度与发布 集群、网络、权限复杂度 平台团队 如果收益只是“看起来更现代”,而新的失败模式没人负责,这笔预算就不应批准。 ...

2026年7月15日 · 1 分钟

设计 Loki 日志系统:先控制标签基数,再选择部署模式

本文参考《告别笨重的 ELK,试试用 Loki 处理日志吧》的实践方向,但按当前 Loki 文档重写。需要特别更新两点:Promtail 已在 Loki 3.7.3 中移除,采集端应迁移到 Grafana Alloy;Simple Scalable Deployment 正在弃用,新系统不应再把它作为长期架构。 Loki 的优势不是“比 Elasticsearch 小”,而是采用了不同的索引策略:索引日志流的 labels,而不是为每个日志字段建立全文索引。 这让它在容器日志场景里更节省索引成本,也意味着标签设计一旦失控,系统同样会变慢、变贵。 先理解 stream 一组完全相同 labels 的日志构成一个 stream: {service_name="checkout", deployment_environment="prod", cluster="hk-1"} 只要任意 label 值变化,就会创建新的 stream。Loki 查询时先通过 labels 找到候选 stream,再扫描其中的日志内容。 因此 labels 适合描述来源和稳定分组: service_name; deployment_environment; cluster、region; namespace; 有界的业务域或日志级别。 不适合做 labels 的字段包括: request_id、trace_id; user_id、订单号; IP 地址; 时间戳; 完整 URL; Pod UID、不断变化的实例 id。 这些值基数高且生命周期短,会制造大量小 stream 和小 chunk,增加索引、对象存储请求和查询开销。 高基数字段放进日志正文或 structured metadata 推荐应用输出结构化 JSON: { "timestamp": "2026-07-15T09:00:00Z", "level": "ERROR", "service": "checkout", "message": "payment provider timeout", "trace_id": "4c9c...", "order_id": "o_123", "duration_ms": 2018 } service、环境和集群可以在采集阶段提升为 labels;trace_id、order_id 和耗时保留在正文或 structured metadata 中,查询时再解析: ...

2026年7月15日 · 2 分钟

让 Coding Agent 维护存量仓库:任务边界、验证与状态机

本文参考《用 AI 复活停更开源项目是一个务实的选择》中的 Knife4j Next 维护经验,抽象成一套与具体模型、IDE 和开源项目无关的仓库维护方法。 Coding Agent 最容易展示的是“根据一句话生成很多代码”,但真实维护的难点通常不在代码量,而在它是否理解同一个问题、有没有越界、怎样证明改动有效,以及下次换会话后状态是否还在。 与其问 Agent 能不能接管仓库,不如先建立一条更小的闭环:一个边界明确的问题,由一个 Agent 完成最小改动,并留下可复查证据。 先选适合的仓库和任务 适合交给 Agent 推进的任务通常具有: 真实用户或维护者提出的明确问题; 能稳定复现的失败; 清晰的模块边界; 可以运行的测试、构建或静态检查; 改错后能够快速回滚; 完成条件可以写成列表。 例如: 修复一个有复现步骤的 bug; 补上缺失的边界测试; 更新与代码不一致的文档; 将手工发布检查脚本化; 迁移一个有官方升级指南的依赖; 清理一段确定没有调用方的代码。 不适合直接自治推进的任务包括:产品要不要转型、架构要不要重写、体验怎样才“更现代”、权限核心链路的大改,以及没有测试和回滚手段的数据迁移。 Agent 可以帮助调研开放问题,但在约束收敛前不应直接改仓库。 把维护者脑中的规则写进仓库 每次对话都重新解释规则,既浪费上下文,也容易漏项。至少应有下面这些持久文件: 文件 内容 README.md 项目用途、快速开始、主要入口 CONTRIBUTING.md 分支、提交、测试和 PR 约定 AGENTS.md 或同类说明 Agent 的范围、禁止项和验证命令 docs/architecture.md 模块职责、依赖方向、重要不变量 docs/releasing.md 版本、构件、发布和回滚步骤 scripts/test-*.sh 可重复执行的验证入口 规则应尽量可操作。例如“保持高质量”无法验证;下面这些可以: 不修改 legacy-v2/,除非 Issue 明确要求; bug 修复必须先添加失败测试; 数据库 schema 修改需要兼容上一版本应用; 前端变更运行 npm test 和视觉回归; 未经人工批准不得推送 tag、发布包或修改生产配置。 这些规则同样帮助新加入的人类维护者,并不是只为 AI 准备。 ...

2026年7月15日 · 2 分钟

远程调用 Docker API:先把它当作宿主机 root 权限

本文从《安全地远程调用 Docker API》重新整理。最大的调整是删除“先开放无鉴权端口快速体验”的路径:Docker API 是宿主机控制面,即使只是短暂暴露,风险也远高于普通业务接口。 能够访问 Docker daemon 的调用方通常可以: 启动特权容器; 挂载宿主机根目录; 读取其他容器环境变量和挂载卷; 注入网络、执行命令和导出镜像; 取得宿主机级控制能力。 因此 Docker socket 权限应近似看作 root 权限。把 tcp://0.0.0.0:2375 暴露到网络,不是“缺少一个登录页”,而是把服务器控制面交给能连接该端口的人。 先判断是否真的需要远程 Docker API 不同需求对应不同方案: 需求 更合适的入口 管理员偶尔执行 docker 命令 SSH Docker context CI 发布固定服务 部署平台、受限 job 或 GitOps 应用只需重启一个服务 自建窄接口的本地 agent 跨主机编排容器 Kubernetes、Swarm 或其他编排 API 完整远程 Engine API 仅在确有需要时使用 mTLS + 网络隔离 如果业务只需要“重启 order-service”,就不要给它创建、挂载和执行任意容器的权限。建立一个只接受服务白名单和固定动作的 broker,通常比直接暴露 Engine API 更容易审计。 首选:通过 SSH 使用 Docker context Docker CLI 可以通过 SSH 把请求转发到远端 Unix socket,不需要额外开放 Docker TCP 端口: ...

2026年7月15日 · 2 分钟

小团队 Kubernetes CI/CD:从一次提交到可回滚发布

本文参考《使用 k8s 搭建一个小巧但健全的 CI/CD 系统》的完整链路,但不沿用旧的云厂商命令和组件版本。重点改为:一条小团队真正能维护、能验证、能回滚的发布路径需要哪些不变量。 “用了 Kubernetes”不等于拥有 CI/CD。真正的流水线必须能回答:这次发布对应哪次提交、构建产物是否唯一、谁有生产权限、部署有没有完成、失败后回到哪里。 先定义最小完成条件 一次发布至少满足: 同一提交可以重复构建; 测试失败不会生成可发布版本; 镜像使用不可变标识; 生产部署有明确权限边界; 流水线等待 rollout 完成,而不是执行 kubectl apply 后就报成功; 可以找到上一稳定版本并回滚; 数据库变更与应用回滚兼容; 发布记录能关联 commit、镜像 digest、操作者和环境。 缺少任何一项,自动化可能只是“更快地执行一组命令”。 把 CI 和 CD 分开 CI 负责证明产物可以交付 典型步骤: 检出固定 commit; 安装锁定版本的依赖; 运行静态检查、单元测试和必要的集成测试; 构建镜像; 扫描依赖与镜像; 推送到镜像仓库; 输出镜像 digest 和构建元数据。 CD 负责把已证明的产物放进环境 CD 不应重新编译。测试环境和生产环境应提升同一个镜像 digest,只改变配置和部署策略。否则“测试通过的产物”和“生产运行的产物”可能不是同一个东西。 镜像不要只用 latest 至少为镜像保存 commit SHA: IMAGE="registry.example.com/order-service:${GITHUB_SHA}" docker build --pull -t "$IMAGE" . docker push "$IMAGE" 更严格的部署使用 registry 返回的 digest: registry.example.com/order-service@sha256:... tag 可能被移动,digest 指向具体内容。即使保留 main、stable 等便捷 tag,生产记录也应保存解析后的 digest。 ...

2026年7月14日 · 2 分钟

数据密集型系统设计:从需求到存储、复制和处理的决策框架

本文以《数据密集型应用系统设计》阅读笔记为起点,按“做架构决策时先问什么”的顺序重新组织。DDIA 第二版已于 2026 年出版,因此这里不复述第一版章节,而是保留跨版本仍然有效的权衡框架。 数据系统选型最容易犯的错误,是先从产品名字开始:“要不要上 Kafka”“该用 MySQL 还是 Elasticsearch”“要不要分库分表”。工具没有脱离约束的优劣,应该先描述工作负载和失败后果,再选择机制。 第一步:把需求写成可判断的约束 至少回答下面这些问题: 维度 需要量化的内容 数据规模 当前量、年增长、单条大小、保留时间 访问模式 点查、范围查、全文检索、聚合、图遍历 写入模式 单条、批量、追加、更新、删除比例 时效性 同步返回、秒级可见、小时级完成 正确性 能否接受旧读、重复、乱序、短暂缺失 可用性 故障时拒绝、降级还是继续写入 恢复 RPO、RTO、能否从源数据重建 合规 审计、删除、地域、加密和访问边界 团队能力 谁运维、怎样升级、是否能演练恢复 “数据量很大”“必须强一致”“实时”都不是可执行需求。把它们换成数字、窗口和可接受后果,很多争论会自动消失。 第二步:区分事实源与派生视图 一个系统里不必只有一种存储,但必须知道谁是 system of record。 例如商品系统可以这样划分: 关系数据库保存商品和库存事实; Elasticsearch 提供全文检索视图; Redis 保存可丢失、可重建的热点缓存; 数仓保存分析视图; 对象存储保存图片和不可变文件。 派生数据的关键不是“永远不丢”,而是: 能否从事实源重建; 重建需要多久; 增量更新丢失时怎样发现; 新旧模型如何并行验证; 删除和权限变更如何传播。 如果一个搜索索引实际上已经成为唯一数据来源,它就不能继续按“缓存坏了重建”来运维。 第三步:根据访问路径选择数据布局 OLTP 与 OLAP 不要互相勉强 事务系统通常围绕少量记录的低延迟读写设计;分析系统更关心扫描大量列、聚合和压缩。把复杂分析直接压在主业务库上,可能让两种负载互相争抢缓存、I/O 和锁。 应该先问:查询要访问哪些行和列,怎样排序,是否需要最新值,再决定索引、行存或列存,而不是因为“数据多”就自动引入一整套大数据平台。 索引是在写入和读取之间做交换 每个索引都会占用空间并增加写放大。评审索引时记录: 它服务哪些真实查询; 查询选择性和排序需求; 写入额外成本; 是否能被另一个复合索引覆盖; 过期后谁负责删除。 没有查询所有者的索引,通常会逐渐变成没人敢删的成本。 第四步:把模式演化当成双向兼容问题 滚动发布意味着新旧代码会同时存在。数据编码至少要考虑: ...

2026年7月14日 · 1 分钟

小团队 SRE 最小闭环:从 SLI、SLO 到错误预算

本文以《SRE Google 运维解密》阅读笔记为选题起点,结合 Google 当前公开的 SRE Book 重写。原文覆盖监控、容量、过载和重试;这里进一步把这些手段放进一条可执行的可靠性闭环。 SRE 不是“再装一套 Prometheus 和 Grafana”,也不是把所有基础设施指标都设成告警。真正的起点是一个产品问题:用户依赖的服务行为是什么,我们准备把它做到多可靠? 只有先回答这个问题,监控、扩容、限流和故障处理才有共同目标。 SLI、SLO 和 SLA 分别是什么 概念 要回答的问题 示例 SLI 实际服务表现怎样 成功请求比例、延迟达标比例 SLO 我们希望达到什么目标 30 天内 99.9% 请求成功 SLA 未达到承诺会有什么后果 退款、赔偿或合同责任 SLO 是内部可靠性目标,不一定写进合同。它的价值是让产品、研发和运维可以围绕同一条数据做决策,而不是一方要求“绝不能出故障”,另一方只说“CPU 看起来正常”。 从用户旅程反推 SLI 不要先翻指标列表。先列出最关键的用户旅程,例如: 用户能够登录; 用户能够创建订单; 支付结果最终能回写订单; 商家能在承诺时间内看到结算数据。 然后为每条旅程定义“好事件”和“总事件”: 可用性 SLI = 好事件数量 / 符合条件的总事件数量 例如订单创建接口可以定义为: 好事件:非调用方错误,并在 800 ms 内返回成功的请求 总事件:进入服务且通过基础参数校验的创建订单请求 定义中必须说清楚: 在客户端、网关还是服务端测量; 哪些状态码属于服务失败; 主动取消和无效参数是否进入分母; 延迟从哪里开始、到哪里结束; 统计窗口和地区范围是什么。 如果只测服务端 HTTP 200,页面 JavaScript 崩溃、网关超时或错误业务结果都可能被漏掉。SLI 应尽量贴近用户真正感知的结果。 延迟不要只看平均值 平均延迟很容易掩盖长尾。100 个请求中 99 个耗时 20 ms,一个耗时 10 秒,平均值仍可能看起来不算离谱,但那个用户已经失败。 ...

2026年7月14日 · 1 分钟