工程化与基础设施
本文合并自 4 篇笔记,点击目录跳转。
目录
- 可观测性:SkyWalking 链路追踪 / Prometheus + Grafana 指标监控 / 日志体系
- 容器化部署:Docker Compose 编排
- 认证鉴权与网关架构:JWT / 统一鉴权 / CI-CD 基础概念
- Nginx 基础配置:从静态托管到反向代理
可观测性:SkyWalking 链路追踪 / Prometheus + Grafana 指标监控 / 日志体系
一句话速览:可观测性的三大支柱各回答一个问题——Logging 回答”发生了什么”,Metrics 回答”系统整体是否健康”,Tracing 回答”这一次请求卡在哪一环”。三者互补不可替代;同时要记住:可观测性工具本身也是跑在你环境里的软件,有兼容性和资源成本,不是配完就万事大吉。
涉及原始记录:
07-面试问题/开发问题/01-依赖与配置问题.mdP0-02;07-面试问题/验证问题/03-基础设施与中间件问题.mdP2-01最近修订:2026-08-07
目录
- 技术点 1:SkyWalking Agent —— 无侵入式链路追踪的原理与代价
- 技术点 2:可观测性三大支柱与 Prometheus 指标监控
- 技术点 3:日志体系 —— 结构化输出与 traceId 透传
- 术语表
技术点 1:SkyWalking Agent —— 无侵入式链路追踪的原理与代价
难度:⭐⭐ | 掌握要求:Java Agent 字节码增强原理、JDK 强封装与
--add-opens
核心概念
SkyWalking Agent 的”无侵入”是通过 Java Agent 机制(-javaagent 参数)实现的:JVM 启动时先加载 Agent,Agent 用字节码增强(instrumentation)的方式在运行时修改目标类的字节码,在方法调用前后插入埋点逻辑(记录 traceId、耗时等),业务代码完全不需要手写任何埋点代码。这个机制天然要求 Agent 能够用反射去访问、修改目标类的内部结构,包括很多 JDK 内部类。
原理深挖:一条 Trace 是怎么串起来的
链路追踪的数据模型很简单:Trace(一次完整请求链路)由若干 Span(一个环节,如一次 HTTP 调用、一次 DB 查询)组成,Span 之间用 parent-spanId 连成树。难点在跨进程传播:Gateway 收到请求生成 traceId 后,必须在调下游服务时把它带过去——SkyWalking Agent 会自动在 HTTP 请求头里注入 sw8 头(携带 traceId、父 spanId 等),下游服务的 Agent 解析这个头把新 Span 挂到同一棵树上。跨进程上下文传播全靠这个头,这和 Feign 拦截器透传 xid(Seata)、透传用户 Header(鉴权)是完全相同的设计思想——链路上下文必须显式在线程间/进程间搬运,不会自动跟随。
sequenceDiagram
participant C as 客户端
participant G as Gateway
participant L as lease服务
participant H as house服务
participant S as SkyWalking OAP
C->>G: 请求(无 trace 上下文)
Note over G: Agent 创建 Trace
traceId=T1, span0
G->>L: Feign/HTTP 调用
Header 注入 sw8(T1, span0)
Note over L: Agent 创建子 Span
(T1, span1, parent=span0)
L->>H: 调用 house
sw8(T1, span1)
Note over H: Agent 创建子 Span
(T1, span2, parent=span1)
G-->>S: 各 Agent 异步上报 Span 数据
L-->>S: OAP 按 traceId 归并
H-->>S: 还原完整调用链
我们踩过的坑
P0-02:JDK 21 的强封装机制拦住了 Agent 的反射操作
JDK 9 引入的模块系统(Jigsaw)默认对 JDK 内部包做了”强封装”(Strong Encapsulation),未经显式声明,外部代码不能通过反射访问这些内部类的非 public 成员。SkyWalking 9.4.0 版本的部分插件实现是在 JDK 更早期版本的开放策略下写的,没有预料到新版本 JDK 的限制会更严格,遇到 JDK 21 时触发 InaccessibleObjectException,或者导致 Agent 初始化阶段卡顿。
解决方式是在 JVM 启动参数里显式声明”开放”这些内部模块,允许反射访问:
1 | JAVA_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED \ |
--add-opens <module>/<package>=<目标模块> 的含义是”把这个包对指定的目标模块开放,允许深度反射访问”,ALL-UNNAMED 表示对所有未命名模块(也就是传统的 classpath 应用)开放。
必须记住的结论
- “无侵入式”监控工具的代价是它依赖大量反射/字节码增强能力,JDK 版本升级带来的模块化限制会直接影响它的可用性。引入这类 Agent 工具、同时使用较新的 JDK 版本时,要有意识地检查该工具官方是否明确支持这个 JDK 版本,版本适配问题(尤其是新旧 JDK 的兼容性)几乎是这类工具的常见坑。
--add-opens是绕过 JDK 强封装限制的标准手段,遇到第三方库反射访问 JDK 内部类报InaccessibleObjectException时,思路都是类似的:找到具体报错涉及的模块和包名,加对应的--add-opens参数,而不是降级 JDK 版本(降级往往有更多连锁影响)。- 日志噪音问题(P2-01):Agent 默认日志级别是 INFO,会输出大量插件加载相关的日志,跟业务日志混在一起影响阅读。这是一个小但常见的运维细节:接入任何 Agent/中间件类工具后,第一件事应该顺手检查它的默认日志级别和输出路径,通过配置把它和业务日志分开(比如这里配置 Agent 单独写到
skywalking-api.log),避免污染主日志。 - 链路追踪跨进程传播的机制(
sw8头)和 Feign 拦截器透传是同一思想:上下文不会自动跟随请求,必须显式注入和解析。
一个更宏观的认识:可观测性工具本身也是有”使用成本”的软件
这条记录虽然只对应两个具体问题,但值得提炼一个更普遍的认知:监控/追踪类工具虽然定位是”辅助排障”,但它本身也是运行在同一个 JVM/环境里的软件,会有自己的兼容性问题、资源开销、配置复杂度。接入可观测性工具不是”配完就一定好用”,同样需要像对待业务依赖一样去验证它在当前技术栈组合(这里是 JDK 21)下能不能正常工作。这也是为什么 Sentinel Dashboard 在 JDK 21 下有已知 bug(见 01-微服务治理.md)——中间件生态对最新 JDK 版本的适配,往往会落后于 JDK 本身的发布节奏,引入新 JDK 大版本时要对整条工具链做一次兼容性摸底,而不是只关心业务框架本身。
技术点 2:可观测性三大支柱与 Prometheus 指标监控
难度:⭐⭐ | 掌握要求:三支柱分工、Prometheus 拉模型、四种指标类型
三大支柱的分工
| 支柱 | 回答的问题 | 本项目实现 | 典型使用时机 |
|---|---|---|---|
| Logging(日志) | 发生了什么(离散事件的细节) | Logback + 文件输出 | 已知大概位置,要看具体参数/异常栈 |
| Metrics(指标) | 系统整体是否健康(聚合数值趋势) | Micrometer + Prometheus + Grafana | 巡检、告警、容量评估、”是不是变慢了” |
| Tracing(链路) | 这一次请求卡在哪一环(调用链拓扑+耗时) | SkyWalking | 微服务间互相调用,定位慢/错发生在哪个服务 |
三者的排查顺序通常是:Metrics 发现异常(QPS 暴跌/错误率上升)→ Tracing 定位到具体服务和环节 → Logging 在该服务里看细节。只依赖日志排障,在微服务架构下等于在十几个服务的日志文件里大海捞针。
Prometheus 的工作模型
Prometheus 与”应用主动上报”的监控方案不同,它是拉(Pull)模型:Prometheus Server 按配置周期(本项目 scrape_interval: 15s)主动抓取每个目标的 /actuator/prometheus 端点。本项目 deploy/docker/prometheus/prometheus.yml 为每个微服务配了一个 job,指标暴露靠的是 Spring Boot Actuator + Micrometer(micrometer-registry-prometheus 依赖),业务代码零侵入。
flowchart LR
subgraph 微服务
S1[livin-gateway
/actuator/prometheus]
S2[livin-house
/actuator/prometheus]
S3[其他服务...]
end
P[Prometheus Server
每15s拉取] --> S1 & S2 & S3
P --> TS[(内置时序数据库
按指标名+标签存储)]
TS --> G[Grafana
PromQL 查询出图]
TS --> A[Alertmanager
告警规则触发通知]
四种指标类型是必背概念:
| 类型 | 语义 | 典型例子 |
|---|---|---|
| Counter | 只增不减的计数器 | 请求总数、异常总数(配 rate() 算 QPS) |
| Gauge | 可增可减的瞬时值 | 当前线程数、内存占用、连接池活跃连接 |
| Histogram | 分桶统计分布 | 请求耗时分布(算 P95/P99 延迟) |
| Summary | 客户端侧算分位数 | 类似 Histogram,但分位数在客户端算,无法跨实例聚合 |
两个实践要点:① “平均值延迟”具有欺骗性——1 个 10 秒的请求和 99 个 10 毫秒的请求平均下来很正常,看 P95/P99 才能发现长尾;② Prometheus 的标签(label,如本项目给每个 job 打的 application 标签)是做维度下钻的关键——“全系统错误率”按 application 一切开,立刻知道是哪个服务的问题。
Grafana 的角色
Grafana 本身不存数据,是可视化 + 告警入口:配置 Prometheus 为数据源,用 PromQL 写查询出图。本项目的 dashboard 配置在 deploy/docker/grafana/dashboards/。学会读三个黄金指标图就能覆盖 80% 的巡检场景:流量(QPS)、错误率、延迟(P95)——这就是 Google SRE 的”四个黄金信号”(再加一个饱和度 Saturation)。
技术点 3:日志体系 —— 结构化输出与 traceId 透传
难度:⭐⭐ | 掌握要求:日志级别语义、MDC 原理、异步日志
核心概念
日志是三大支柱里最”古老”的,但微服务下有两个必须主动解决的问题:
① 日志级别语义要统一:ERROR 表示”需要人工介入的异常”、WARN 表示”异常但系统已自愈”(如重试成功)、INFO 是关键业务节点、DEBUG 只在排查时打开。最常见的问题是 ERROR 滥用(业务校验失败也打 ERROR),导致真正的故障淹没在噪音里——ERROR 日志应该是”每条都值得看一眼”的,否则告警无法基于日志搭建。
② 跨线程/跨服务的日志串联:一个请求在 Gateway → lease → house 三个服务各打了若干行日志,没有关联标识就无法拼出完整链路。解法是把 traceId 打进每条日志:
- MDC(Mapped Diagnostic Context):SLF4J 提供的基于 ThreadLocal 的日志上下文。在请求入口(过滤器/拦截器)把 traceId 塞进
MDC.put("traceId", ...),日志 Pattern 里加%X{traceId},这个线程后续打的每一行日志都会自动带上它。请求结束必须MDC.clear()——MDC 底层是 ThreadLocal,线程池复用线程时不清会串数据(ThreadLocal 的坑详见10-JVM与并发编程.md)。 - 异步场景要显式传递:MDC 绑在当前线程上,一旦逻辑切到线程池/MQ 消费者/CompletableFuture,traceId 就断了,需要用装饰的线程池(如 TtlExecutors)或在异步入口重新 set。接入了 SkyWalking 的话,其 Agent 的 logback 插件可以自动把 traceId 注入 MDC,免去手写。
- 异步追加器:高吞吐服务用 Logback 的
AsyncAppender把”日志 IO”挪到后台线程,避免业务线程阻塞在磁盘写;代价是 JVM 崩溃时队列里未来得及落盘的日志会丢——核心审计类日志不适合走异步。
与三支柱的呼应
日志带上 traceId 之后,三大支柱就真正打通了:Grafana 看板发现错误率突增 → SkyWalking 按时间找到失败 Trace 拿到 traceId → 用 traceId 去日志里精确捞出这次请求在所有服务里的每一行日志。这套”指标 → 链路 → 日志”的下钻路径,是微服务排障的标准动作。
术语表
| 术语 | 含义 |
|---|---|
| Java Agent / instrumentation | JVM 启动期加载的字节码增强机制,无侵入埋点的实现基础 |
| Trace / Span | 一次完整请求链路 / 链路中的一个环节(一次调用、一次查询) |
| sw8 头 | SkyWalking 跨进程传播链路上下文的 HTTP 请求头 |
| 三大支柱 | Logging(事件细节)/ Metrics(聚合趋势)/ Tracing(调用链) |
| 拉模型(Pull) | Prometheus 主动周期抓取目标指标端点的采集方式 |
| Micrometer / Actuator | Spring Boot 的指标门面与端点暴露组件,/actuator/prometheus 由此而来 |
| Counter / Gauge / Histogram / Summary | Prometheus 四种指标类型:计数/瞬时值/分桶分布/客户端分位数 |
| P95 / P99 | 95%/99% 分位延迟,发现长尾慢请求的关键指标 |
| 四个黄金信号 | 延迟、流量、错误率、饱和度(Google SRE 提出的监控核心维度) |
| MDC | SLF4J 基于 ThreadLocal 的日志上下文,用于给日志自动附带 traceId |
| AsyncAppender | Logback 异步日志追加器,日志 IO 后台化,崩溃时可能丢尾部日志 |
容器化部署:Docker Compose 编排
一句话速览:Docker 的心智模型只有一句话——镜像分层 + 容器可写层 + 数据卷,三层各有什么生命周期想清楚了,”插件丢失””数据丢失””重建后配置还原”这类问题全部可以提前预判;编排层面则要记住:
depends_on管顺序不管可用,up -d成功不代表健康。涉及原始记录:
07-面试问题/验证问题/03-基础设施与中间件问题.mdP2-02、P1-03、P1-05;07-面试问题/开发问题/02-框架与中间件集成问题.mdP1-05(ES 插件持久化);07-面试问题/开发问题/01-依赖与配置问题.mdP1-06、P1-07、P1-08(多服务脚本管理、Nacos 内存调优、健康检查超时)最近修订:2026-08-07
目录
- 技术点 1:数据卷(Volume)—— 决定什么东西能在容器重建后存活
- 技术点 2:容器编排的依赖顺序(
depends_on+ healthcheck) - 技术点 3:资源过载是环境问题,不要误判成代码 bug
- 技术点 4:多服务脚本管理——服务清单是唯一事实来源,且要随规模持续演进
- 技术点 5:镜像分层、网络与资源限制 —— 必补的地基
- 术语表
技术点 1:数据卷(Volume)—— 决定什么东西能在容器重建后存活
难度:⭐⭐ | 掌握要求:可写层 vs 数据卷生命周期、restart vs recreate 的区别
核心概念
理解 Docker 容器的存储模型,关键是分清”容器的可写层”和”数据卷”两种东西的生命周期:
- 容器可写层:容器运行时在镜像基础上叠加的一层可写文件系统,容器删除(不是 stop,是 rm)或者被”重建”(
--force-recreate、重新up一个同名但配置变了的服务)时,这一层连同里面的所有修改会被丢弃,回到镜像原始状态。 - 数据卷(named volume / bind mount):显式挂载的存储,生命周期独立于容器实例,容器怎么重建,只要 volume 还在,数据就还在。
任何不是通过 volumes 显式挂载出来的目录,写进去的东西都只活在容器可写层里,本质上是”临时的”,即使这个容器可能会正常运行很长时间不重启——它随时可能因为某次部署变更、镜像升级而被重建,届时所有没有落到数据卷里的修改都会消失。
我们踩过的坑
开发问题 P1-05:ES 插件目录没挂载数据卷,容器重建后插件丢失
docker exec livin-es elasticsearch-plugin install ... 这条命令是在容器内部执行的,装好的 IK 插件文件写在了容器的可写层(/usr/share/elasticsearch/plugins 这个路径当时没有被 volumes 挂载出来)。容器只是简单 restart 时插件还在,但只要触发了”重建”(比如改了 compose 配置后 up,或者显式 --force-recreate),插件就消失了,依赖这个插件的自定义分词器索引也会变成不可用状态(red)。
根治方案是把插件目录也纳入数据卷管理:
1 | volumes: |
迁移已经装好的插件到新卷(避免重装)的操作顺序值得记住,这是一个通用的”给已运行容器补挂载卷”的操作模式:
1 | # 1. 从旧容器里把数据先备份到宿主机 |
验证问题 P1-03:容器没起来但 compose 命令没报错
docker compose up -d 是后台执行的,如果某个容器因为内存不足或启动超时失败,命令本身可能不会有明显报错,容易让人误以为所有服务都正常启动了。必须显式检查容器的实际状态,不能只看 docker compose up 命令有没有报错:
1 | docker ps -a --filter name=elastic # 确认容器状态,而不是假设它成功了 |
如果服务有健康检查配置,更可靠的做法是轮询等待健康检查通过,而不是固定 sleep 一个时间就假设已经就绪:
1 | for i in (curl -sf -o /dev/null -w "%{http_code}" http://localhost:9200 2>/dev/null || echo "000") |
必须记住的结论
- 任何通过
docker exec进入容器内部做的修改(装软件、改配置文件、写文件),只要对应路径没有挂载数据卷,这个修改就是”一次性”的,下次容器重建就会消失。养成习惯:手动进容器改了什么东西之后,先问自己”这个目录持久化了吗”,没有的话要么补上 volume 配置,要么这次修改注定要在下次重建时重做一遍。 docker compose up -d命令执行成功,不代表容器真的健康运行,尤其是后台模式下,启动失败可能被悄悄吞掉。判断服务是否就绪,永远以docker ps查看容器实际状态、或者主动探测健康检查端点为准,不能只看命令的退出码。
技术点 2:容器编排的依赖顺序(depends_on + healthcheck)
难度:⭐⭐ | 掌握要求:启动顺序 vs 就绪的区别、service_healthy 条件、脚本层等待逻辑
核心概念
多容器应用之间往往有依赖关系(业务服务要等 MySQL 就绪才能连接),但 Docker Compose 默认的 depends_on 只保证启动顺序,不保证”依赖的服务已经可以正常提供服务”——一个 MySQL 容器”启动了”(进程起来了)和”已经初始化完成、可以接受连接了”是两个不同的时间点,中间可能差好几秒到几十秒。
要让 Compose 真正等到依赖服务”可用”再启动下一个,需要给依赖的服务配置 healthcheck,并且在 depends_on 里声明 condition: service_healthy:
1 | mysql: |
我们踩过的坑
验证问题 P2-02:中间件未就绪时业务服务提前启动失败
这条记录本身在文档里更多是作为”最佳实践”被提前预防,而不是一次线上事故复盘,但值得强调的一点是:Docker Compose 层面的 depends_on: service_healthy 只能约束 Compose 管理的容器之间的启动顺序,管不到”Compose 外部、由脚本单独启动的 Java 微服务”。本项目的微服务不是用 Compose 管理的,是用 start-all.sh 脚本单独拉起的 Java 进程,所以真正的等待逻辑要写在这个脚本里,而不能指望 Compose 的健康检查机制覆盖到微服务层面。
这一点在实际操作中体现为一个反复出现的踩坑记录(验证问题 P1-05):start-all.sh 本身不会启动 Docker 中间件容器,只负责检测状态和直接拉起 Java 微服务。如果没有先手动确认中间件(尤其是 Nacos)健康,就直接跑这个脚本,Java 微服务会因为连不上 Nacos/MySQL 而集体启动失败。而且 Nacos 的健康检查通过(HTTP 端口响应)和它的 gRPC 端口(微服务注册发现实际用的协议)真正就绪之间,还有大约 50 秒的滞后——这是本项目实践中总结出的一个具体的经验数值,不是 Nacos 官方文档写的标准值,仅供参考。
必须记住的结论
depends_on默认只管启动顺序(谁先谁后),不管”是否真的可用”;要精确控制”等到真正可用再继续”,必须配healthcheck+condition: service_healthy。- 这套依赖等待机制只在 Compose 管理的服务范围内有效。如果应用服务是脚本单独管理、不在同一个 compose 文件里,等待逻辑需要在脚本层面自己实现(轮询健康检查端点),不能假设 Compose 会帮忙处理。
- 中间件的”健康检查通过”和”所有子功能都真正可用”之间可能存在滞后(比如这里 Nacos 的 HTTP 健康检查和 gRPC 注册端口就绪不是同一时刻),涉及这类中间件时,如果启动脚本里只等健康检查通过就立刻启动依赖它的服务,遇到偶发的连接失败要考虑加一点缓冲等待时间,而不是第一反应怀疑代码逻辑有问题。
技术点 3:资源过载是环境问题,不要误判成代码 bug
难度:⭐⭐ | 掌握要求:排查顺序——先看资源再看代码、docker stats 用法
核心概念
本地开发环境用容器同时跑十几个中间件加十几个 Java 微服务,是对宿主机资源的极限压榨。当 CPU/内存严重超载时,会引发一系列表现上像是代码 bug、实际是资源不足导致的间接故障:容器健康检查超时被判定 unhealthy、服务间 RPC 调用超时、消息消费延迟等等。
我们踩过的坑
验证问题 P1-05:资源过载导致的连锁反应,一度被误以为是业务代码问题
阶段7验证合同链路时,同时运行 19+ 容器和 9~10 个 Java 微服务,出现了两个看起来很像代码 bug 的现象:支付回调后租约状态没有正常流转(消费者没有消费日志),以及某个接口偶发返回网关兜底的”系统内部错误”。
排查过程本身是一个值得记住的方法论:没有一头扎进业务代码找 bug,而是先用 docker stats 看了一眼资源占用,发现多个容器 CPU 占用破百,据此判断更可能是资源过载导致 Nacos/RocketMQ 探测超时,进而引发 Feign 调用超时、消息消费延迟。验证方式是完整走一遍 stop-all.sh all(docker compose down,比单独 docker kill 更彻底)重启整个环境,再重新走一遍完整链路——两个问题都没有再复现,从而确认是资源问题而不是代码逻辑问题。
由此也沉淀出一条实际的操作经验:测试非全链路功能时,没必要每次都启动全部中间件,只启动当前验证场景真正需要的核心组件(比如验证合同/租约/支付链路,只需要 MySQL/Redis/Nacos/RocketMQ/MinIO/Seata,完全不需要 ES/Kibana/各种 Dashboard/SkyWalking/Prometheus/Grafana),可以显著降低资源压力。
必须记住的结论
- 排查”偶发的、没有明确异常信息、看起来毫无规律的故障”时,先看资源使用情况(CPU/内存/磁盘 IO),而不是立刻扎进业务代码逻辑。资源过载引发的连锁故障往往没有清晰的因果链,代码层面怎么看都找不出毛病,因为问题根本不在代码里。
- 遇到”这个问题上次出现过,这次重启环境后又没了”的情况,第一反应不是”问题玄学消失了”,而是要考虑是不是资源过载/环境不稳定导致的偶发现象,
stop-all.sh all完整重启环境是一个简单但有效的排查/规避手段。 - 本地开发环境资源有限时,按需启动中间件(而不是无脑全量启动)是一个能减少很多”莫名其妙”问题的实用习惯。
技术点 4:多服务脚本管理——服务清单是唯一事实来源,且要随规模持续演进
难度:⭐⭐ | 掌握要求:清单漂移问题、JVM 参数与规模的关系、僵尸容器与 init
核心概念
本项目的微服务不是用 Docker Compose 编排的,而是用 start-all.sh/stop-all.sh 两个 Shell 脚本,靠一个写死的 SERVICES=(...) 数组 + PID 文件来管理生命周期。这种”轻量脚本 + PID 文件”的管理方式简单直接,但有一个天然的脆弱点:它没有像 Kubernetes/Compose 那样从声明式配置(如 pom.xml 的 module 列表、Gateway 路由表)自动推导出服务清单,SERVICES 数组是一份完全独立维护的”第二份配置”。任何”新增一个微服务模块”的操作,本质上都需要同时改好几个地方保持一致:根 pom.xml 的 <module>、Gateway 的路由配置、以及这里的 SERVICES 数组——它们之间没有任何强制校验,改漏一个不会有任何编译或运行时报错。
此外,这类脚本里常见的两个”魔法数字”——JVM 内存参数(如 JVM_XMX=256m)、健康检查探测超时(如 curl --max-time 3)——都是在项目早期、服务规模较小时按当时的实际情况设定的,它们的合理性是和”当时的服务数量、当时的宿主机负载”绑定的,不会随着服务规模增长自动调整,也很容易被遗忘在角落里,直到规模涨到某个临界点集中爆发问题。
我们踩过的坑
开发问题 P1-06:新增服务模块忘记同步登记到启动脚本,一键启动静默漏起
新增 livin-admin、livin-file 两个模块时,pom.xml、application.yml、Gateway 路由都配置齐全,jar 也编译成功,唯独忘了把它们加进 start-all.sh/stop-all.sh 的 SERVICES 数组。后果是这两个服务在”一键启动全部”时被完全跳过,而且脚本不会有任何报错或 [FAIL] 提示,因为 for 循环压根不会遍历到它们——这是最隐蔽的一类问题:所有静态检查(编译、配置文件语法)都通过,只有在”实际有没有对应进程在跑”这个运行时事实层面才能发现异常。排查时还顺带发现 livin-file 和 livin-notify 的端口配置意外撞车(都是 8090),如果两个服务真的同时被启动,会有一个绑定端口失败退出。
开发问题 P1-07:Nacos 容器 JVM 堆内存配置没有随服务数增长而调整,触发 OOM
docker-compose-infra.yml 里 Nacos 的 JVM_XMX=256m 是服务只有 6~7 个时定的,随着微服务数量涨到 13 个,注册表项、长轮询连接、心跳线程都线性增长,256MB 堆内存长时间运行后必然 OOM——容器整体的内存上限(7.8G)完全够用,问题出在 JVM 参数本身太保守,物理资源和 JVM 实际能用的资源是两回事,扩容宿主机内存对这类问题毫无帮助,只能针对性调大 -Xmx。更麻烦的是,OOM 后容器一度进入”僵尸态”,普通 docker stop/kill 都无法结束,需要给容器补充 init: true 让 Docker 注入专职的 PID 1 进程去正确回收异常退出的子进程。
开发问题 P1-08:健康检查探测超时是按早期负载设定的,服务规模涨上来后频繁误判
start-all.sh 里 curl --max-time 3 探测各服务健康检查,在服务数量少、宿主机负载低的阶段完全够用。规模涨到 13 个微服务 + 9 个中间件容器同时运行后,宿主机负载偶尔冲到 Load Average 15+,明明所有服务都在正常返回 HTTP 200,仅仅因为 3 秒内来不及响应,就被脚本整批误判成 [WAITING],看起来像是”全部服务都启动失败了”,实际上只是探测方法本身对高负载场景没有留出容错空间。
必须记住的结论
- 多服务管理脚本里任何形式的”服务清单”(数组、配置列表)都要当成和
pom.xml模块声明、网关路由表同等重要的配置,新增服务的检查清单里必须包含”是否同步更新了启动/停止脚本”这一项,最好和其他改动放在同一次提交里,减少遗漏窗口。 - 判断”服务是否启动成功”要以运行时事实(进程是否存活、健康检查是否通过)为准,不能只看编译是否成功、配置文件是否完整——配置齐全但没人启动进程,是一种和”启动失败”完全不同、但表现上同样让人困惑的状态。
- JVM 内存参数、超时阈值这类和”当前系统规模”强相关的配置,不是设一次就一劳永逸的,服务数量、并发量、数据量出现数量级变化时,应该主动回头检查一遍这些历史遗留的参数是否还合理,而不是等到 OOM/大面积误判发生了才回头查。
- 容器 OOM 后可能进入无法被普通信号结束的僵尸态,给长期运行、内存敏感的容器(尤其是 JVM 类应用)默认加上
init: true,是一个能减少”容器卡死必须docker rm -f兜底”情况的低成本加固手段。
技术点 5:镜像分层、网络与资源限制 —— 必补的地基
难度:⭐⭐ | 掌握要求:镜像分层与缓存、多阶段构建、bridge 网络、内存限制行为
镜像分层:为什么 Dockerfile 指令顺序很重要
Docker 镜像是只读层的堆叠:Dockerfile 里每条指令(FROM/RUN/COPY…)生成一个新层,层有缓存——某一层变了,它之后的所有层缓存全部失效。由此得到两条 Dockerfile 最佳实践:
- 变化频率低的内容放前面:基础依赖安装(
apt-get install)放前面,频繁变化的代码拷贝(COPY . /app)放最后,最大化利用构建缓存。 - 多阶段构建(multi-stage build)控制镜像体积:第一阶段用 JDK/Maven 完整镜像编译打包,第二阶段只
COPY产物 jar 到一个精简 JRE 镜像运行——最终镜像不带编译工具链,体积从 GB 级降到几百 MB,攻击面也更小。
1 | # 阶段1:构建 |
容器网络
- bridge(默认):每个容器分配虚拟网卡和私有 IP,同一 bridge 网络内的容器可以用容器名互相解析(Compose 自动把服务名注册成 DNS)——这就是 compose 文件里
mysql:3306、nacos:8848能直接写服务名的原因。 - 端口映射
-p 8080:8080只影响”宿主机访问容器”,不影响容器之间互访——容器间通信走内部网络,不需要映射。 - 容器访问宿主机:用
host.docker.internal(本项目prometheus.yml抓微服务指标就是这么配的),不能写localhost——容器里的 localhost 是容器自己。这是容器化新手的高频坑。
资源限制与 JVM 的特殊性
mem_limit/cpus可以限制容器资源,超限的行为是内存超限直接 OOM Kill(不是变慢,是杀掉)、CPU 超限只是限流变慢。- JVM 与容器限制的版本陷阱:老版本 JDK(8u131 之前)读的是宿主机内存而不是容器 limit,容器限 512M 但 JVM 按宿主机 64G 算堆,必然 OOM Kill。JDK 8u191+/10+ 默认支持容器感知(
UseContainerSupport),JDK 21 无此问题——但JVM_XMX这类显式参数会覆盖容器感知,本项目 Nacos OOM 案例的本质就是”显式参数太小”而不是”容器内存不够”。 - 配套认知:容器里 JVM 的 OOM 分两层——Java 堆 OOM(
OutOfMemoryError)和容器级 OOM Kill(进程直接被 SIGKILL,日志里什么 Java 异常都没有),后者排查要看docker inspect的OOMKilled字段和dmesg。OOM 排查完整方法详见10-JVM与并发编程.md。
从 Compose 到 K8s 的演进认知
Compose 解决的是”单机多容器编排”,K8s 解决的是”多机集群调度 + 自愈 + 弹性伸缩”。概念上有一组对应关系,提前建立映射学 K8s 会快很多:Compose service → K8s Deployment/Service、depends_on+healthcheck → 探针(liveness/readiness probe)、数据卷 → PV/PVC、服务名 DNS → Service 发现。本项目 deploy/k8s/ 目录已开始积累相关配置,作为后续演进方向了解即可。
术语表
| 术语 | 含义 |
|---|---|
| 容器可写层 | 容器运行时叠加在镜像上的可写文件层,容器删除/重建即丢失 |
| 数据卷(Volume) | 生命周期独立于容器的持久化存储,named volume 由 Docker 管理 |
| restart vs recreate | 重启保留可写层 / 重建从镜像重新创建、可写层重置 |
| healthcheck + service_healthy | 让 Compose 等到依赖”真正可用”再启动后续服务的机制 |
| 镜像分层 / 构建缓存 | 每条 Dockerfile 指令生成一个只读层;某层变化则其后缓存全失效 |
| 多阶段构建 | 编译环境与运行环境分离的 Dockerfile 写法,控制产物镜像体积 |
| bridge 网络 | 默认容器网络模式,同网络内容器名即 DNS 名 |
| host.docker.internal | 容器内访问宿主机服务的特殊 DNS 名 |
| OOM Kill | 容器内存超限被内核直接杀掉,区别于 JVM 堆 OOM |
init: true |
给容器注入专职 PID 1 进程,正确回收僵尸子进程 |
| liveness / readiness probe | K8s 探针:判断容器要不要重启 / 要不要接流量 |
认证鉴权与网关架构:JWT / 统一鉴权 / CI-CD 基础概念
一句话速览:本项目的鉴权架构是”网关一处验签、下游全部信任 Header”——这个模式用”业务服务零鉴权代码”换来了”直连业务端口这条路在语义上不成立”的硬约束;JWT 本身用签名换无状态,代价是无法主动失效,踢人/续期都要额外的机制来补。
涉及原始记录:
07-面试问题/验证问题/01-业务逻辑问题.mdP0-01;07-面试问题/验证问题/02-数据与环境问题.mdP1-02、P1-03;
补充内容:CI/CD 基础概念(阶段12引入,本项目未做 CD)最近修订:2026-08-07
目录
- 技术点 1:网关统一鉴权 + Header 透传模式
- 技术点 2:JWT(JSON Web Token)基础
- 技术点 3:JWT 的续期与主动失效 —— 无状态的两块补丁
- 技术点 4:CI/CD 基础概念(了解即可,本项目未落地 CD)
- 术语表
技术点 1:网关统一鉴权 + Header 透传模式
难度:⭐⭐ | 掌握要求:能画出透传链路、说清直连端口为什么必然拿不到用户
核心概念
本项目的鉴权架构是一种常见的微服务模式:只在网关这一层做 JWT 验签,验签通过后把解析出的用户信息通过请求 Header 透传给下游服务,下游业务服务本身不再重复验签,只信任 Header 里的内容。
1 | 客户端请求(带 JWT token) |
这个模式的核心假设是:业务服务的入口只能是网关,一旦这个假设被打破(比如测试时直连业务端口),下游的 UserContextHolder 就会拿到空值——因为业务服务的拦截器压根不会去解析请求里的 JWT token,它只认 Header。
我们踩过的坑
这个模式带来的”副作用”在本项目里以两种不同的表现形式反复出现,值得放在一起理解成同一个问题:
验证问题 P0-01:直连业务服务导致 tenant_id 为空,进而影响 Seata 回滚
验证脚本为了图方便,直接调用了 lease 服务的内部接口(绕过 Gateway),UserContextHolder.getUserId() 返回 null,导致创建的租约记录 tenant_id 字段是空的,后续再牵连到 Seata 回滚异常(详见 02-分布式事务与消息队列.md)。表面上看是一个 Seata 问题,根因其实是验证方式违反了架构假设。
数据与环境问题 P1-02:直连业务端口返回”请先登录”
同样是直连业务服务的表现,这次更直白:接口直接返回 10003”请先登录”的业务错误码,即使请求明明带了有效的 token。原因跟上面完全一致——业务服务的拦截器根本不解析 Header 之外的任何东西:
1 | public class UserContextInterceptor implements HandlerInterceptor { |
解决方式是建立一条硬性验证规范:所有需要鉴权的验证请求,必须统一通过 Gateway 端口(8080)访问 /api/** 路径,不允许为了”方便调试”直连业务服务端口。
必须记住的结论
- “网关统一鉴权、业务服务信任 Header”这个架构模式的代价是:业务服务自己完全丧失了独立验证请求合法性的能力。这不是缺陷,是这个架构模式本身的设计取舍(换来的是业务服务不用各自实现一遍 JWT 验签逻辑),但意味着这个架构下”直连业务服务”这条路径在语义上是不完整的、不能代表真实的请求处理链路,任何验证、调试都必须走网关。
- 排查”带了 token 但还是提示未登录/用户信息为空”类问题时,第一步永远是确认请求到底有没有经过网关,而不是去查 JWT 本身的签名、有效期这些。
- 这类架构模式的选择也解释了为什么 WebSocket(IM 模块)不能照搬同样的方式——WebSocket 是直连独立端口、不经过 Gateway 的,所以它必须自己具备完整的 JWT 验签能力,这是两种架构模式的分界点(详见
05-网络通信Netty.md的鉴权部分)。 - 安全视角的提醒(本项目单机部署暂未暴露,但生产必须考虑):”信任 Header”的前提是外部流量无法绕过网关直达业务服务。生产环境必须在网络层(安全组/内网隔离/服务端口不对外暴露)保证这一点,否则任何人都可以自己伪造一个
X-User-Id头直连业务服务冒充任意用户。
技术点 2:JWT(JSON Web Token)基础
难度:⭐⭐ | 掌握要求:三段结构、HS256 vs RS256、无状态的优缺点
核心概念
JWT 是一种自包含的身份令牌,由三部分组成(header.payload.signature),服务端不需要为每个登录用户保存 session 状态——校验的时候只需要验证签名是否合法(用密钥/公钥验签),再解析出 payload 里携带的用户信息(userId、role、过期时间等)即可,这也是为什么 JWT 特别适合分布式/微服务架构:任何一个持有验签密钥的服务,都能独立完成校验,不需要跟一个中心化的 session 存储通信。
原理深挖:三段结构各是什么
1 | eyJhbGciOiJSUzI1NiJ9 . eyJ1c2VySWQiOjEsImV4cCI6MTc1NDU2fQ . X9v...签名 |
两个最容易被误解的点:
- Base64URL 是编码不是加密:payload 部分任何人都能解码出明文(在线工具一秒解),所以 JWT 里绝对不能放密码、身份证号等敏感信息。JWT 保证的是”不可篡改”(改了签名就对不上),不是”不可见”。
- 签名验签的过程:签发方对
header + "." + payload用私钥(RS256)或共享密钥(HS256)算出签名;验签方用公钥/同一密钥重新算一遍对比。任何对 payload 的篡改都会导致签名不匹配。
对称 HS256 vs 非对称 RS256
| HS256(对称) | RS256(非对称,本项目选择) | |
|---|---|---|
| 密钥 | 同一个密钥既签发又验签 | 私钥签发、公钥验签 |
| 微服务适配性 | 差:每个要验签的服务都得持有”能伪造 token 的密钥”,一个服务泄漏全盘皆输 | 好:验签服务只持有公钥,公钥泄漏也无法伪造 token |
| 性能 | 快 | 慢一些(RSA 运算贵),可忽略 |
本项目用的是非对称签名(RSA):Auth 服务持有私钥,签发 token 时用私钥签名;其他所有需要验签的地方(Gateway、以及独立于网关之外的 IM 服务)只需要持有公钥即可完成验签,不需要也不应该持有私钥。这个设计的好处是即使某个下游服务的公钥配置泄漏,也不可能被用来伪造 token(伪造需要私钥)。
我们踩过的坑
参见 05-网络通信Netty.md 里 IM 服务忘记配置 JWT 公钥导致鉴权必然失败的案例——这个坑本质上属于”JWT 验签所需的密钥材料没有正确分发到需要它的服务”这一类问题。
必须记住的结论
- 区分清楚”谁需要私钥、谁只需要公钥”:只有签发 token 的服务(本项目是 auth 服务)需要私钥,所有其他做验签的地方只需要公钥。新增一个需要独立验签能力的服务时,只需要同步公钥配置,不要图省事把私钥也一起同步过去(会扩大私钥泄漏的风险面)。
- JWT 本身不提供”主动失效”能力(一旦签发,在过期之前始终有效),如果业务上需要强制下线/踢人,需要额外的机制(比如本项目 IM 模块的”同用户多端登录踢旧连接”就是应用层维护会话表实现的,不是依赖 JWT 本身)。
- payload 是明文编码不是加密,敏感信息绝不进 JWT;签名保证的是”不可篡改”,不是”不可见”。
技术点 3:JWT 的续期与主动失效 —— 无状态的两块补丁
难度:⭐⭐ | 掌握要求:refresh token 双 token 机制、黑名单/版本号两种失效方案
JWT 无状态设计带来两个天然的”功能缺口”,是面试必考的延伸问题,本项目虽未实现但方案要能讲清:
缺口一:token 过期了怎么办——双 Token 续期
access token 有效期设短了用户体验差(频繁重新登录),设长了泄漏风险大。标准解法是双 Token:
sequenceDiagram
participant C as 客户端
participant A as Auth服务
participant R as 业务链路
C->>A: 登录
A-->>C: accessToken(2小时) + refreshToken(7天, 存服务端/Redis)
C->>R: 用 accessToken 调业务接口
Note over C,R: 2小时后 accessToken 过期
C->>A: 用 refreshToken 换新 accessToken
A-->>C: 新 accessToken(无感续期,不用重新登录)
要点:accessToken 无状态验签(快),refreshToken 有状态存服务端(可控制)——吊销 refreshToken 就能阻止续期,等现有 accessToken 自然过期即完成下线。这是”无状态性能”与”可控制性”的折中。
缺口二:怎么让没过期的 token 立刻失效——两种方案
- 黑名单方案:把被吊销的 token(或其 jti 唯一标识)存 Redis,TTL 设为 token 剩余有效期;验签时先查黑名单。缺点:每次验签多一次 Redis 查询,部分牺牲了无状态性。
- 用户版本号方案(更常用):用户表维护一个
tokenVersion字段,签发时写进 payload;踢人/改密时把tokenVersion +1,验签时对比 token 里的版本号和当前版本号,不一致即拒绝。好处是一次操作吊销该用户所有已签发 token,且版本号可以缓存,开销比黑名单小。
本项目 IM 模块的”多端登录踢旧连接”走的是另一条路——不吊销 JWT,而是在应用层会话表里让新连接顶掉旧 Channel(详见 05-网络通信Netty.md),这说明”登录态控制”和”JWT 失效”是两个可以分开解决的问题。
技术点 4:CI/CD 基础概念(了解即可,本项目未落地 CD)
难度:⭐ | 掌握要求:CI 与 CD 的区别、本项目”只做 CI”的判断逻辑
这部分是纯概念性学习内容,本项目已经搭建了 CI(见
.github/workflows/ci.yml),但结合项目实际情况(本地 2核2G 服务器资源无法承载 11 微服务+8 中间件的完整技术栈)判断不做自动化部署,只需要理解概念、知道行业里通常怎么做即可。
核心概念
- CI(持续集成,Continuous Integration):代码提交后自动触发一套流水线,做编译、跑测试、代码质量扫描,尽早发现”这次改动把项目弄坏了”,而不是等到人工发现或者部署时才暴露。
- CD(持续交付/持续部署,Continuous Delivery / Deployment):CI 通过之后,自动把构建产物发布出去。”持续交付”是打包好、但上线需要人工点一下确认;”持续部署”是全自动,测试通过就直接上线,中间没有人工审批环节。这两者的区别就在于”最后一步要不要人工确认”。
- 常见工具:GitHub Actions、GitLab CI、Jenkins。核心工作模式都是相同的——监听代码仓库事件(push/PR),按照一份 YAML/Groovy 定义的流水线脚本,在一个临时的、全新的 runner 环境里依次执行各个步骤。
本项目的技术选型判断(为什么只做 CI 不做 CD)
这是一次真实的、基于资源约束做的架构判断,值得记住判断过程而不只是结论:
- 项目由 11 个自研微服务 + 8+ 个中间件(Nacos/RocketMQ/Seata/ES/Redis/MySQL/MongoDB/MinIO)组成,粗略估算内存需求:光中间件就需要 3~4G,11 个 Java 微服务哪怕每个只给 256M 堆内存也要 2.7G+,合计轻松超过 8G。
- 现有可用于部署的服务器只有 2核2G,连单独跑好 ES 一个组件都紧张,完全不可能承载完整技术栈——不是”能不能优化”的问题,是内存总量的硬约束。
- GitHub Actions 的 runner 是每次全新创建的临时容器,即使强行想在流水线里做自动部署,也没有一个现实存在的、能承载这套技术栈的目标环境可以部署过去。
- 结论:CI(编译/打包门禁)依然有实际价值,能防止把多模块聚合顺序、依赖声明这类低级错误合入主分支;但 CD 环节因为没有可行的部署目标,本阶段不实施,属于”评估后主动不做”,而不是”不会做”。
必须记住的结论
- CI 和 CD 是两个独立的、可以分开决策是否要做的环节,不是绑定的一个整体。”做了 CI”不代表一定要”做 CD”,两者的价值和成本是分开评估的。
- 判断要不要上某个工程化设施(这里是 CD),核心问题永远是”这个东西的价值能不能落地”,而不是”这个技术流不流行”。没有可部署的目标环境时,强行搭建自动部署流水线只是空转,没有实际产出。
- 如果未来有了更充足的服务器资源,把 CD 补上的思路应该是:先从”只部署 1~2 个核心服务做演示”这种小范围场景切入验证流程通不通,而不是一上来就追求”全部 11 个服务自动部署”,这跟本项目其他很多问题的教训是一致的——先保证核心链路正确可信,再谈规模化。
术语表
| 术语 | 含义 |
|---|---|
| Header 透传模式 | 网关验签后把用户信息写入请求头,下游服务只读头不验签的架构模式 |
| Base64URL | JWT 三段用的编码方式——是编码不是加密,payload 可被任何人解码 |
| HS256 / RS256 | 对称签名(共享密钥)/ 非对称签名(私钥签、公钥验)算法 |
| accessToken / refreshToken | 双 Token 机制:短效业务令牌 + 长效可吊销的续期令牌 |
| 黑名单 / tokenVersion | 两种主动失效方案:吊销列表逐个封禁 / 用户版本号一次全废 |
| 白名单 | 网关鉴权中显式放行、不做登录校验的路径清单 |
| CI / CD | 持续集成(自动编译测试门禁)/ 持续交付与持续部署(自动发布) |
| runner | CI 工具中执行流水线的临时干净环境 |
Nginx 基础配置:从静态托管到反向代理
一句话速览:Nginx 配置的心智模型只有三层——http 块管全局、server 块管一个站点、location 块管一个 URL 路径。三者嵌套,从外到内逐层缩小范围。90% 的 Nginx 配置问题都能用三句话定位:哪个 server 块匹配了请求?哪个 location 匹配了路径?匹配到的指令做了什么?
目录
- 技术点 1:配置文件的层级结构——http → server → location
- 技术点 2:server 块——虚拟主机,一个 Nginx 托管多个站点
- 技术点 3:location 匹配规则——Nginx 最容易踩坑的地方
- 技术点 4:反向代理——Nginx 最常见的用途
- 技术点 5:静态文件托管与 try_files
- 技术点 6:负载均衡与 upstream
- 技术点 7:HTTPS 配置与证书
- 技术点 8:常用运维命令
技术点 1:配置文件的层级结构——http → server → location
Nginx 配置文件(通常在 /etc/nginx/nginx.conf)是嵌套的块结构:
1 | # 全局配置——工作进程数、日志路径等 |
记忆方式:俄罗斯套娃。http 是最外层(全局 HTTP 设置),server 是中间层(一个站点),location 是最内层(一条 URL 路径的规则)。配置项可以在外层定义、内层继承,内层也可以覆盖外层。
1Panel / 宝塔等面板的做法:通常主配置文件 nginx.conf 里 include /etc/nginx/conf.d/*.conf,每个站点一个 .conf 文件(如 example.com.conf),里面就是一个 server 块。这样加站点只需加文件、不用改主配置。
技术点 2:server 块——虚拟主机,一个 Nginx 托管多个站点
一个 Nginx 可以托管多个站点,靠的是 server_name 区分:
1 | # 站点 A:博客 |
关键概念:
listen:监听端口,80 是 HTTP、443 是 HTTPSserver_name:匹配请求的 Host 头。Nginx 收到请求后,按server_name找对应的 server 块default_server:没有匹配时的兜底,一个端口只能有一个default_server- 多个域名指向同一个站点:
server_name a.com b.com c.com;
技术点 3:location 匹配规则——Nginx 最容易踩坑的地方
location 决定了”这个 URL 路径由什么规则处理”,匹配规则有优先级:
1 | location = /exact { |
优先级从高到低:
=精确匹配(最高)^~前缀匹配(匹配后不再检查正则)~/~*正则匹配(按出现顺序,先匹配到的生效)- 普通前缀匹配(最低,最长前缀优先)
实际例子:
1 | server { |
SPA 单页应用的 try_files:try_files $uri uri,再找目录 $uri/,都找不到就返回 index.html(让前端路由处理)。Vue/React 项目部署必配。
技术点 4:反向代理——Nginx 最常见的用途
反向代理就是:用户请求 Nginx → Nginx 转发给后端服务 → 后端返回 → Nginx 转给用户。
1 | location /api { |
为什么要传这些 header:后端服务收到的请求是 Nginx 发的,不传的话后端以为请求来自 127.0.0.1,拿不到真实用户 IP 和协议。
带路径前缀的代理:
1 | # 访问 https://example.com/app/xxx → 转发到 http://localhost:3000/xxx(去掉 /app 前缀) |
末尾 / 的区别是最大的坑:
proxy_pass http://backend/;(带/)→/app/foo变成/fooproxy_pass http://backend;(不带/)→/app/foo保持/app/foo
WebSocket 代理(如果后端用了 WS):
1 | location /ws { |
技术点 5:静态文件托管与 try_files
最简单的用途——把 HTML/CSS/JS 文件托管出去:
1 | server { |
root vs alias:
root /var/www:location 路径拼在 root 后面。location /img+root /var/www→ 找/var/www/img/xxxalias /var/www/images:location 路径被替换。location /img+alias /var/www/images→ 找/var/www/images/xxx
记忆方式:root 是”路径拼接”,alias 是”路径替换”。alias 更直观但要注意末尾 / 要对齐。
技术点 6:负载均衡与 upstream
后端有多个实例时,Nginx 可以分发请求:
1 | # 定义后端服务器组 |
负载均衡策略:
- 默认:轮询(round-robin),均匀分配
weight=N:按权重分配ip_hash:同一 IP 固定走同一后端(解决 session 问题)least_conn:分配给当前连接数最少的后端
1 | upstream backend { |
技术点 7:HTTPS 配置与证书
1 | server { |
证书来源:
- Let’s Encrypt(免费,用 certbot 自动申请和续期)
- 阿里云/腾讯云免费 DV 证书
- 付费证书(OV/EV)
HTTP 跳 HTTPS 的坑:后端服务如果需要知道用户用的是 HTTPS,必须传 X-Forwarded-Proto $scheme,否则后端以为请求是 HTTP 的,生成 HTTP 的重定向链接造成无限重定向。
技术点 8:常用运维命令
1 | # 测试配置语法是否正确 |
配置变更流程:改配置 → nginx -t 测试 → nginx -s reload 重载。永远先测试再重载,配置语法错误会导致 reload 失败但旧配置仍在跑(不中断服务)。
术语表
| 术语 | 含义 |
|---|---|
| http 块 | Nginx 配置最外层,管全局 HTTP 设置 |
| server 块 | 虚拟主机,一个 server 对应一个站点 |
| location 块 | URL 路径匹配规则,一个 location 对应一组 URL |
| root | 文件根目录,location 路径拼在后面 |
| alias | 路径别名,location 路径被替换 |
| try_files | 按顺序尝试文件/目录,找不到用最后一个兜底 |
| proxy_pass | 反向代理,把请求转发给后端 |
| upstream | 后端服务器组,配合 proxy_pass 做负载均衡 |
$uri |
当前请求的 URI(不含参数) |
$host |
请求的 Host 头 |
$remote_addr |
客户端 IP |
default_server |
没有匹配时的兜底 server |
499 |
客户端主动断开(常见于前端超时取消请求) |
502 |
后端服务没起来或端口不对 |
504 |
后端响应超时(Gateway Timeout) |




