先定位层次,再改配置
服务端问题的排查顺序,比具体命令更重要。同一个「接口 500」,可能来自编译产物、数据库连接、SQL、事务、序列化或容器网络。先确认失败发生在哪一层,再决定看什么日志。
本书建议的顺序是:
编译/构建 → 进程启动 → 数据库连接 → 迁移 → SQL/事务 → HTTP 处理 → 容器/部署
每一层都有明确的失败信号:
| 层次 | 典型信号 | 先看什么 |
|---|---|---|
| 构建 | mvn package 失败 | Maven 的 [ERROR] 首行与模块名 |
| 启动 | 进程退出、端口未监听 | 启动日志最后 30 行、BindException |
| 数据库连接 | 启动慢、健康检查 DOWN | JDBC URL、容器状态、pg_isready |
| 迁移 | 启动即失败 | Flyway 校验和、schema history 表 |
| SQL/事务 | 接口 500、超时、死锁 | 数据库日志、pg_locks、SQL 参数 |
| HTTP | 4xx/5xx、格式不对 | 状态码、Content-Type、响应体 |
| 容器 | 镜像启动即退出 | docker logs、端口映射、环境变量 |
下面按这个顺序列出真实报错与处理方式。所有输出都在本机 macOS + JDK 25.0.5 + PostgreSQL 17.11 上实际产生;你的行号、端口和对象名可能不同。
构建阶段
编译目标版本不支持
下面是一次用 compiler 插件 3.13.0 复现的原始输出(本书工程锁定 3.16.0,报错文案一致):
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile
(default-compile) on project libraryd-domain: Fatal error compiling:
error: release version 99 not supported -> [Help 1]
触发条件:maven.compiler.release 写了一个当前 JDK 不支持的版本,或者 Maven 实际使用的 JDK 低于目标版本。
排查:
mvn -version
java -version
mvn -q help:evaluate -Dexpression=maven.compiler.release -DforceStdout
mvn -version 里的 Java version 必须是 25.x。如果它显示 21 或 17,先修 JAVA_HOME,不要改 <release> 去迁就错误的 JDK。
依赖找不到
[ERROR] Failed to execute goal on project libraryd-web: Could not resolve dependencies
for project com.plume:libraryd-web:jar:1.0.0-SNAPSHOT
[ERROR] Could not find artifact com.example:does-not-exist:jar:99.99.99 in central
(https://repo.maven.apache.org/maven2)
触发条件:坐标拼错、版本不存在、私服缺少该构件,或离线模式下列表不全。
排查顺序:
- 在 Maven Central 搜索页确认
groupId:artifactId:version三者都存在。 mvn -q dependency:tree -Dincludes=com.example:does-not-exist看是谁引入的。- 检查
settings.xml的镜像与私服配置。 - 删除本地缓存中对应目录再重试(只删该构件,不要清空整个
.m2):
rm -rf ~/.m2/repository/com/example/does-not-exist
依赖收敛失败
Enforcer 的 dependencyConvergence 报错通常长这样:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce
(enforce-toolchain) on project libraryd-parent: Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence failed with message:
Dependency convergence error for org.slf4j:slf4j-api:2.0.20 paths to dependency are:
...
处理方式不是关掉规则,而是找到分叉点:
mvn -q dependency:tree -Dverbose -Dincludes=org.slf4j:slf4j-api
然后在父 POM 的 dependencyManagement 里显式锁定版本,或在引入方排除旧版本。关闭规则只会把问题推迟到运行时。
可执行 jar 没有主清单
no main manifest attribute, in libraryd-app-1.0.0-SNAPSHOT.jar
触发条件:直接运行 mvn package 产出的普通 jar,而不是 Shade 重新打包后的 fat jar;或者 Shade 的 ManifestResourceTransformer 没配置 mainClass。
排查:
unzip -p libraryd-app/target/libraryd-app-1.0.0-SNAPSHOT.jar META-INF/MANIFEST.MF
确认有 Main-Class: com.plume.library.AppMain,并且 jar 里包含依赖类。如果只在 target/ 下看到 original-*.jar,说明 Shade 没有执行。
Shade 后签名校验失败
Exception in thread "main" java.lang.SecurityException: Invalid signature file digest
for Manifest main attributes
触发条件:某个依赖 jar 带签名文件(META-INF/*.SF、*.DSA、*.RSA),Shade 合并后签名与内容不匹配。
修复:在 Shade 的 filter 里排除签名文件:
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
找不到 SPI 实现
现象:启动时报 No suitable driver、Unable to locate appropriate Flyway configuration extension,或 Jackson 的多态类型找不到子类。
原因通常是 Shade 覆盖了 META-INF/services/*,只保留了最后一个依赖的文件。
修复:加入 ServicesResourceTransformer:
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
数据库启动与连接
Docker 端口已被占用
docker: Error response from daemon: failed to set up container networking:
driver failed programming external connectivity on endpoint libraryd-pg
(760e86b24ff0...): Bind for 127.0.0.1:5432 failed: port is already allocated
排查本机监听:
lsof -nP -iTCP:5432 -sTCP:LISTEN
处理方式二选一:停掉占用端口的进程,或换一个宿主端口:
docker run -d --rm --name libraryd-pg \
-e POSTGRES_DB=libraryd -e POSTGRES_USER=libraryd -e POSTGRES_PASSWORD=libraryd \
-p 127.0.0.1:55432:5432 postgres:17-alpine
换端口后,JDBC URL 和 LIBRARYD_DB_URL 也要同步改。容器内的 5432 不变,变的只是宿主端口。
密码错误
psql: error: connection to server at "127.0.0.1", port 5432 failed:
FATAL: password authentication failed for user "libraryd"
排查:
- 容器启动时的
POSTGRES_PASSWORD是否与连接配置一致。 - 是否连到了另一个同名容器或本机已有的 PostgreSQL。
- 环境变量是否被 shell 中的旧值覆盖:
env | rg '^LIBRARYD_DB_'
POSTGRES_PASSWORD 只在容器首次初始化数据目录时生效。改了环境变量但复用旧数据卷,密码不会变。
数据库不存在
psql: error: connection to server at "127.0.0.1", port 5432 failed:
FATAL: database "libraryd" does not exist
确认容器内的库列表:
docker exec -it libraryd-pg psql -U libraryd -d postgres -c '\l'
常见原因是启动容器时漏了 POSTGRES_DB=libraryd,默认库名变成了用户名。补建数据库:
docker exec -it libraryd-pg createdb -U libraryd libraryd
连接被拒绝
Caused by: java.net.ConnectException: Connection refused (connect failed)
这表示 TCP 层没有服务在监听,不是密码或库名问题。检查:
docker ps --filter name=libraryd-pg
docker exec libraryd-pg pg_isready -U libraryd -d libraryd
容器刚启动时 PostgreSQL 需要几百毫秒到几秒初始化。应用启动脚本应等待 pg_isready,而不是 docker run 后立刻连接。
迁移
校验和不匹配
org.flywaydb.core.api.exception.FlywayValidateException: Validate failed:
Migrations have failed validation
Migration checksum mismatch for migration version 1
-> Applied to database : -1891601629
-> Resolved locally : 186179074
Either revert the changes to the migration, or run repair to update the schema history.
触发条件:已经执行过的 V1__init.sql 被修改。Flyway 用校验和确认「历史迁移没有被改过」。
正确做法:
- 开发阶段、数据库可丢弃:重建数据库,重新迁移。
- 数据库已有数据:不要改历史迁移,新增
V3__...sql表达变更。 - 确实需要修正历史记录(例如本地实验库):
flyway repair只更新 schema history 的校验和,不会改表结构,生产环境慎用。
查看已执行记录:
select installed_rank, version, description, checksum, success
from flyway_schema_history
order by installed_rank;
表已存在
ERROR: relation "books" already exists
触发条件:手工建过表,或迁移脚本被重复执行,但 schema history 里没有对应记录。
处理:先确认表结构是否与迁移一致。如果一致,可以 baseline 当前版本;如果不一致,删除表后让 Flyway 重新执行,或写一个新的迁移修正差异。不要直接删 flyway_schema_history 了事,那会让 Flyway 以为什么都没执行过。
连接池
连接超时
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available,
request timed out after 1001ms (total=0, active=0, idle=0, waiting=0)
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:714)
Caused by: java.net.ConnectException: Connection refused (connect failed)
这段输出说明两件事:池里一个连接都没建起来(total=0),底层是连接被拒绝。先修数据库可达性,再谈池大小。
如果 total 接近 maximumPoolSize、active 长期占满,才是池太小或连接泄漏:
HikariPool-1 - Connection is not available, request timed out after 30000ms
(total=10, active=10, idle=0, waiting=7)
排查顺序:
- 确认每个
getConnection()都在 try-with-resources 里关闭。 - 打开泄漏检测:
config.setLeakDetectionThreshold(10_000);
日志会出现持有连接超过阈值的堆栈,指向具体代码行。
3. 检查长事务:一个事务里做 HTTP 调用、文件读写或大批量计算,会长时间占用连接。
4. 最后才考虑调大 maximumPoolSize。数据库连接是有限资源,盲目调大只会把压力转移到数据库。
连接池指标
HikariCP 通过 Micrometer 暴露 hikaricp.connections.active、hikaricp.connections.idle、hikaricp.connections.pending 等指标。排障时看 active 与 pending 的比值,比看单次日志更快。
SQL 与事务
重复借阅:部分唯一索引生效
ERROR: duplicate key value violates unique constraint "ux_loans_active_isbn"
DETAIL: Key (isbn)=(B1) already exists.
这是预期行为。ux_loans_active_isbn 是 where returned_on is null 的部分唯一索引,用来保证一本书同时只有一条活动借阅。
应用层应当在捕获 SQLException、SQLState 为 23505 时,转换成 BOOK_ALREADY_BORROWED(HTTP 409),而不是把数据库错误直接暴露给调用方。
外键 violation
ERROR: insert or update on table "loans" violates foreign key constraint "loans_isbn_fkey"
DETAIL: Key (isbn)=(B404) is not present in table "books".
说明借阅记录引用了不存在的图书。应用层应先查图书是否存在,返回 BOOK_NOT_FOUND;数据库约束是最后一道防线,不是主要校验手段。
序列化失败
ERROR: could not serialize access due to concurrent update
触发条件:REPEATABLE READ 或 SERIALIZABLE 隔离级别下,两个事务同时修改同一行,后提交者失败。
这不是 bug,而是隔离级别保证一致性的方式。应用层需要识别 SQLState 40001,回滚并重试整个事务。重试次数要有上限,并且事务内不能有不可重复的副作用(发邮件、调用外部支付)。
死锁
ERROR: deadlock detected
DETAIL: Process 79 waits for ShareLock on transaction 742; blocked by process 77.
Process 77 waits for ShareLock on transaction 743; blocked by process 79.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,1) in relation "loans"
触发条件:两个事务以相反顺序锁定同一组行。PostgreSQL 会自动检测并回滚其中一个。
处理:
- 统一加锁顺序。借阅和归还都按
isbn排序后再更新,能从根上减少死锁。 - 缩短事务,避免在事务中做网络调用。
- 捕获 SQLState
40P01,回滚后重试。 - 查看当前锁等待:
select pid, state, wait_event_type, wait_event, query
from pg_stat_activity
where datname = 'libraryd' and state <> 'idle';
HTTP 服务
端口被占用
io.javalin.util.JavalinBindException: Port already in use.
Make sure no other process is using port 8080 and try again.
at io.javalin.jetty.JettyServer.start(JettyServer.kt:103)
Caused by: java.io.IOException: Failed to bind to 0.0.0.0/0.0.0.0:8080
Caused by: java.net.BindException: Address already in use
排查:
lsof -nP -iTCP:8080 -sTCP:LISTEN
如果是上一个 libraryd 没停干净,先结束进程;如果是其他服务,改 LIBRARYD_HTTP_PORT,或者停掉冲突服务。不要用随机端口掩盖问题——端口是部署契约的一部分。
缺少 Content-Type:返回 400 而不是 415
请求缺少 Content-Type: application/json 时,Javalin 的 bodyAsClass 无法确定解析方式。libraryd 的 parseBody 显式检查,返回 400:
{
"type": "https://plumphp.com/problems/invalid-command",
"title": "Bad Request",
"status": 400,
"detail": "Content-Type must be application/json",
"code": "E_INVALID_COMMAND",
"requestId": "5f1c2a0e-...",
"errors": {}
}
这里没有用 415,是为了让所有「请求本身不合法」的错误共享同一套 Problem Details 结构和客户端处理分支。如果团队更希望遵循语义,把 INVALID_COMMAND 的状态码改成 415 即可,但要在接口契约里统一,不能一部分接口 400、一部分 415。
排查时先看 curl -i 的请求头,不要只看响应体。很多「参数没收到」的问题,实际是请求头或 JSON 字段名不对。
Jackson 反序列化失败
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException:
Unrecognized field "publishedYear" (class com.plume.library.web.dto.CreateBookRequest),
not marked as ignorable (4 known properties: "isbn", "title", "author", "year"])
at [Source: (String)"{...}"; line: 1, column: 42]
这说明请求字段与 DTO 不一致。本书统一用 year,对应数据库列 published_year。要么改请求,要么在 DTO 上显式兼容旧字段——不要全局打开 FAIL_ON_UNKNOWN_PROPERTIES=false 来掩盖拼写错误。
日期序列化失败
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 date/time type `java.time.LocalDate` not supported by default:
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
修复:注册 JavaTimeModule,并关闭时间戳输出:
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
只加依赖不注册模块,报错不会消失。
容器与发布
容器启动即退出
docker ps -a --filter name=libraryd
docker logs --tail 50 libraryd
常见原因:
- 缺少环境变量:
LIBRARYD_DB_URL指向localhost,但容器里的localhost是容器自己,应改为宿主机名或 compose 服务名。 - 迁移失败:Flyway 校验和或权限问题,进程在启动阶段退出。
- 端口写错:应用监听 8080,
-p映射的是 8080,但配置被覆盖成别的端口。
容器连不上宿主机数据库
容器内的 localhost 不等于宿主机。macOS 与 Linux 的处理不同:
- Docker Desktop(macOS):用
host.docker.internal。 - Linux:用
--add-host=host.docker.internal:host-gateway,或直接使用容器网络中的服务名。
更稳妥的做法是把应用和数据库放进同一个 compose 网络,用服务名互相访问。
镜像架构不匹配
WARNING: The requested image's platform (linux/amd64) does not match the detected
host platform (linux/arm64/v8)
Apple Silicon 上运行 amd64 镜像会走模拟层,性能明显下降。构建时指定目标架构:
docker build --platform linux/arm64 -t libraryd:dev .
CI 与生产环境统一架构,避免「本地能跑、线上很慢」。
一页诊断命令
# 版本与工具链
java -version && mvn -version && docker --version
# Maven 依赖
mvn -q dependency:tree -Dincludes=<groupId>:<artifactId>
mvn -q help:effective-pom -Doutput=/tmp/effective-pom.xml
# 容器与数据库
docker ps --filter name=libraryd
docker logs --tail 50 libraryd-pg
docker exec libraryd-pg pg_isready -U libraryd -d libraryd
docker exec -it libraryd-pg psql -U libraryd -d libraryd -c '\dt'
# 端口
lsof -nP -iTCP:8080 -sTCP:LISTEN
lsof -nP -iTCP:5432 -sTCP:LISTEN
# HTTP
curl -i http://localhost:8080/health
curl -i -X POST http://localhost:8080/api/books \
-H 'Content-Type: application/json' \
-d '{"isbn":"978-7-115-54736-1","title":"Java 编程实战","author":"Leeting Yan","year":2026}'
# 数据库活动与锁
psql -U libraryd -d libraryd -c \
"select pid, state, wait_event_type, wait_event, query from pg_stat_activity where datname='libraryd';"
把这些命令按「构建 → 容器 → 端口 → HTTP → 数据库」的顺序跑一遍,大多数问题能在五分钟内定位到某一层。
小结
- 先确认失败发生在哪一层,再决定看什么;不要从最熟悉的配置开始改。
- 构建阶段最常见的是 JDK 版本、依赖坐标和 Shade 资源合并。
- 数据库阶段先确认容器可达,再谈密码、库名和迁移。
- Flyway 校验和错误不要靠改历史迁移解决,新增迁移或 repair。
- Hikari 超时先看
total/active/pending,再判断是池小、泄漏还是数据库慢。 - 数据库的唯一约束、外键、序列化失败和死锁都是信号,应用层要把它们翻译成领域错误。
- HTTP 4xx 先看请求头和字段名,5xx 先看服务端日志和异常栈。
- 容器问题优先看
docker logs、环境变量和网络地址,不要先怀疑镜像。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。