1. 别被报错文案骗了:"An IO error occurred while sending to the backend"的真实含义
先简单交代一下场景。这几天在帮一个团队排查线上 PostgreSQL 间歇性报错,应用日志里反复出现一行异常:
org.postgresql.util.PSQLException: An IO error occurred while sending to the backend.异常栈里还带了一行 Caused by,通常是:
Caused by: java.io.EOFException Caused by: java.net.SocketException: Connection reset Caused by: java.net.SocketTimeoutException: Read timed out很多同学第一次看到这个报错会下意识以为“数据库挂了”“网络断了”,然后开始疯狂重启 PostgreSQL、重启应用服务器,结果问题依旧。等到想深入看的时候,又因为异常信息太笼统,根本不知道从哪下手。
这个报错文案直译是“在向后端发送数据时发生了一个 IO 错误”,但实际上它涵盖的场景比你想象的要广得多。它不只是“发送数据失败”,还包含“发送过程中连接被对端重置”“发送后等待响应时超时”“连接实际上已经被回收但应用还在用”等各种情况。换句话说,这个异常是 PostgreSQL JDBC 驱动抛出的一个统一出口,而不是某种特定故障的专属报错。
所以,第一步要做的事情是:先看懂驱动在什么情况下会走到这个异常分支。这决定了整个排查方向对不对。如果方向错了,后边再怎么折腾也是白费力气。
2. 从驱动源码角度拆解:JDBC 在哪些情况下会抛出这条异常
2.1 发送阶段的异常:真的写在 socket 上失败
PostgreSQL JDBC 驱动在向数据库发送查询、绑定参数、执行语句时,底层走的是 Socket 输出流。具体代码在org.postgresql.core.v3.QueryExecutorImpl里,sendQuery方法会把查询打包成消息,然后调用pgStream.flush()或pgStream.write()把数据写到网络层。
如果这一步出了问题,比如:
- Socket 连接已经被对端关闭(数据库重启、防火墙踢掉连接、网络设备回收了空闲连接)
- TCP 层发生 RST(Connection reset)
- 内核发送缓冲区满且对端一直不读,导致写入超时
驱动就会抛出PSQLException,文案统一是An IO error occurred while sending to the backend。如果你用的是旧版驱动,可能还会附加一个Caused by: java.net.SocketException: Broken pipe。
这里的关键点是:“sending” 这个词并不一定代表是发送那一刻才断的,它也可能是在发送之前连接就已经死了,只是你发送时才暴露出来。
2.2 读取响应阶段:等待数据库回复时连接出了问题
另一种更常见的情况是:查询已经成功发给数据库,数据库也执行完了,但在驱动等待读取响应数据的过程中,Socket 连接被中断了。
比如:
- PostgreSQL 后端进程在返回数据前崩溃,导致连接被系统回收
- 数据库设置了
statement_timeout,查询执行超时被取消,但连接层面没有正常返回错误消息,而是直接断掉 - 应用和数据库之间有负载均衡器、云数据库代理,这些中间组件因为空闲超时、内存压力等原因主动断开了连接
- 网络本身不稳定,丢包严重导致 TCP 重传最终失败
这种情况下,驱动抛出的同样是An IO error occurred while sending to the backend。我第一次踩坑的时候就很困惑:明明错误说的是 “sending”,但我正在执行的是一个很简单的 SELECT,怎么可能发送失败?后来看了驱动源码才发现,读取阶段出问题也会抛同样的文案,只是触发的代码路径不同。
2.3 空闲连接回收后继续使用:最常见但最隐蔽的场景
这是我在实际业务里遇到最多的情况,也是这个报错最容易反复出现的原因。
应用启动后,连接池创建了一批到 PostgreSQL 的连接。这些连接在池子里待着,可能几分钟甚至几小时都没有任何查询。结果这段时间里:
- PostgreSQL 侧因为
idle_in_transaction_session_timeout或tcp_keepalives_idle等参数设置,主动断开了空闲连接 - 云数据库(比如 AWS RDS、阿里云 RDS)的数据库代理、负载均衡器在连接空闲一段时间后主动断开
- 公司内部防火墙对长空闲的 TCP 连接进行清理
然后某个请求从连接池里捞出了一条已经被对端断开的连接,驱动在真正发数据之前先做一次 socket 写入,此时内核才发现“这条 TCP 连接其实早就死了”,于是返回Broken pipe或Connection reset,最终变成了这个异常。
这种情况往往有个特点:报错是间歇性的,而且经常在流量低峰期后的第一个请求出现。我见过很多团队半夜收到告警,早上到公司一看应用日志,全是这个异常,但数据库 CPU、连接数、慢查询全都正常。
2.4 连接池配置引发的“连接”问题
还有一种情况,数据库本身没问题,应用代码也没问题,问题出在连接池配置上。
以 HikariCP 为例,它默认的maxLifetime是 30 分钟,默认的connectionTimeout是 30 秒。如果你把maxLifetime设置得比数据库或中间件的空闲超时时间还长,就会出现连接池以为自己手里的连接是健康的,但数据库那边早就把连接断掉的情况。
再者,如果连接池的validationTimeout配置不当,或者没有配置连接有效性检测(connectionTestQuery或依赖 JDBC4 的isValid()),池子就不会在把连接交给业务线程前检查连接是否可用,这也会加大拿到死连接的概率。
所以,在排查问题时,先看一眼你的连接池配置,可能比直接看数据库还快。
提示:PostgreSQL JDBC 驱动从 9.4 开始支持在获取连接时进行 Socket 超时设置,合理配置
socketTimeout和connectTimeout也能减少这类异常带来的影响,但这个我们后面会详细讲。
3. 一套能落地的排查路径:我每次遇到这个报错都按这个顺序查
3.1 先分清故障范围:是单点还是大面积
遇到报错后,第一件事不是改代码,而是回答三个问题:
- 这个异常是发生在同一个应用实例上,还是所有应用实例都有?
- 是某一个数据库节点的问题,还是所有数据库节点都异常?
- 是持续报错,还是间歇性飘几个?
如果是单实例单连接报错,大概率是连接被回收、网络瞬间抖动这类局部问题;如果所有实例同时报错,那基本可以确定是数据库节点故障、网络分区或者数据库配置变更导致连接全部失效。
有一个很典型的场景我遇到过多次:某团队做 PostgreSQL 大版本升级,用pg_ctl promote切换了主备节点,但应用端的连接池没有感知到连接失效,还是拿着旧连接发请求,结果批量抛IO error。这时候数据库侧看日志一切正常,因为新的主库已经在正常服务了,但应用侧所有旧连接的写操作都失败。
所以,如果你恰好做了数据库重启、主备切换、参数 reload、网络变更等操作,先把时间线和报错出现的时间对上,很可能问题不用深入排查就已经定位了。
3.2 看 PostgreSQL 服务端日志,确认是谁先断开的
排查这种连接异常,最有效的手段之一就是看 PostgreSQL 的服务端日志。
默认情况下,PostgreSQL 的日志会记录连接终止信息。如果你的log_connections和log_disconnections参数是开启的(很多云数据库默认开启),那你能在日志里看到类似这样的记录:
LOG: disconnection: session time: 0:00:12.345 user=appuser database=mydb host=10.0.0.5这条日志意味着是 PostgreSQL 主动记录了一次正常断开。如果报错出现的瞬间,数据库日志里有对应的 disconnect 记录,那连接大概率是数据库侧(或数据库所在网络路径上)先断的。
如果数据库日志里没有相关记录,那说明可能是网络设备、防火墙、云平台 LB 之类的中间层断的,也可能是应用所在服务器的内核断的。这时候可以再用 tcpdump 抓包来确认断开方向。
我自己的经验是:排查时间线超过 30 分钟还没定位的话,就直接上抓包,别靠猜。
3.3 用 tcpdump 抓包:看 TCP 层的 RST/FIN 是谁发的
抓包是定断开方向最直接的手段。在应用服务器上执行:
tcpdump -i eth0 -nn port 5432 -w /tmp/pg_io_error.pcap在数据库服务器上也可以同时抓一份。抓到报错复现后,重点看几个标志:
- 连接断开那一刻,是数据库端发了
FIN还是RST - RST 包的源 IP 是谁,目的地是谁
- 报错之前是否有大量 TCP 重传(retransmission)
- 连接 idle 了多长时间后断开的
如果是数据库主动发 FIN,说明数据库(或数据库所在宿主机)正常关闭了连接。比如idle_in_session_timeout到期就会这样。
如果是 RST,说明某一边在处理一个不属于当前连接状态的数据包时,直接重置了连接。常见原因包括:
- 应用在连接已关闭后继续写数据
- 防火墙对异常状态的连接发了 RST
- 数据库后端进程崩溃,内核清理 socket
抓包数据配合 PostgreSQL 日志,基本能把问题定位到三层:应用层、数据库层、网络层。
3.4 检查 PostgreSQL 关键超时参数,别让数据库替你“关连接”
PostgreSQL 有几个参数会直接导致空闲连接被断开,如果应用侧连接池不知道这件事,就会出现文章开头那个异常。
| 参数名 | 默认值 | 作用 |
|---|---|---|
idle_in_transaction_session_timeout | 0(不限制) | 空闲事务超过该时间后被强制断开 |
statement_timeout | 0(不限制) | 单条语句执行超过该时间后被取消 |
tcp_keepalives_idle | 系统默认(通常 7200s) | TCP keepalive 探测包发送间隔 |
tcp_keepalives_interval | 系统默认(通常 75s) | keepalive 重试间隔 |
tcp_keepalives_count | 系统默认(通常 9次) | keepalive 失败多少次后断开 |
其中idle_in_transaction_session_timeout是一个很容易被忽略的坑。很多团队代码里写着BEGIN之后开了事务,但代码里某个分支忘了COMMIT或ROLLBACK,事务一直挂着。数据库为了不让你把事务挂一辈子,设置了该参数(比如 60 秒),到点直接断连接。应用下次从连接池拿连接时,自然就报IO error。
检查方法:
SHOW idle_in_transaction_session_timeout; SHOW statement_timeout;这两个参数如果被设置了非零值,一定要确保应用连接池的连接生命周期、应用事务执行时长在预期范围内,否则你就是在和数据库的自我保护机制对着干。
3.5 检查 TCP keepalive 和防火墙,这是“空闲连接被回收”的重灾区
PostgreSQL 默认的 TCP keepalive 参数继承自操作系统默认值。Linux 上通常要等 2 小时才开始发送 keepalive 探测包,如果探测失败要等 75 秒重试,重试 9 次才放弃,一个死连接最长可能存活 2 小时 11 分 15 秒。
也就是说,如果一个连接在空闲状态下被防火墙静默丢弃(无 RST 无 FIN,直接丢包),应用侧至少要等 2 小时 11 分钟后才会发现这个连接已经死了。连接池在这段时间里可能早就把这根连接分发了 N 次,每次都要等 socket 超时,表现就是间歇性的An IO error occurred while sending to the backend。
Linux 下查看默认值:
sysctl net.ipv4.tcp_keepalive_time sysctl net.ipv4.tcp_keepalive_intvl sysctl net.ipv4.tcp_keepalive_probes如果系统默认是 7200 秒,而你的防火墙空闲超时是 300 秒,那基本必踩坑。建议把 PostgreSQL 的 keepalive 参数调小,让连接在防火墙超时前就能保持活跃:
ALTER SYSTEM SET tcp_keepalives_idle = 60; ALTER SYSTEM SET tcp_keepalives_interval = 10; ALTER SYSTEM SET tcp_keepalives_count = 6;注意,这些参数是 PostgreSQL 侧在新建连接时设置的,对已有连接不生效,需要重启应用连接池,让连接重新建立才能生效。
4. 连接池配置的常见病根:为什么这个问题总在“低峰期”之后爆发
4.1 HikariCP 配置里最容易出问题的几个参数
国内用 Spring Boot 的团队,连接池大部分是 HikariCP。默认配置看起来“开箱就能用”,但很多默认参数在真实生产环境里并不合适。
我遇到过最典型的配置问题,是maxLifetime和数据库/网络中间件的空闲回收时间没有对齐。
比如:
- 云数据库代理空闲超时:300 秒
- 防火墙无状态连接超时:900 秒
- HikariCP
maxLifetime:1800000 毫秒(30 分钟)
这个配置下,数据库代理把空闲了 300 秒的连接断开,但连接池里的连接以为还能用到 30 分钟。下一次请求发出去,直接打到已关闭的连接上,报错。
正确的做法是:maxLifetime要小于数据库侧和网络侧的最小空闲超时时间。比如中间件 300 秒断空闲连接,那maxLifetime应该设置在 240000 毫秒(4 分钟)甚至更短,确保连接池在数据库断开前主动重建连接。
另一个常被忽略的参数是connectionTimeout。它控制的是“从池子拿连接时,等多久拿不到就放弃”,而不是“连接建立后多久超时”。很多人把connectionTimeout设得很长,结果数据库一抖,所有请求都堆积在等待连接上,数据库连接数被打满,应用侧就开始大面积报错。
4.2 一个很值钱的配置:connectionTestQuery 还是 JDBC4 isValid
HikariCP 默认使用 JDBC4 的Connection.isValid()来检测连接是否可用。PostgreSQL JDBC 驱动实现了这个方法,它内部会执行一个/* ping */ SELECT 1到数据库。
但这里有个细节:isValid()在每次从连接池借出连接时都会执行,如果数据库压力很大,这个 ping 本身也会变成一种负担。另外,如果你的驱动版本太老,isValid()的实现在某些异常场景下会误判连接可用性。
也可以用connectionTestQuery显式指定一个更轻量的 SQL:
spring: datasource: hikari: connection-test-query: SELECT 1不过说实话,现代数据库和驱动版本性能都足够好,默认的isValid()直接用的场景更多。这里提出来是为了让你知道有这个可选项,免得被各种博客带着走偏。
4.3 连接池参数的一个推荐起点
以下是我在多个生产项目里验证过的配置起点,视业务情况调整:
spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 600000 max-lifetime: 1800000 connection-timeout: 30000 validation-timeout: 5000跑一段时间后,重点观察监控里的“借出连接耗时”“等待连接数”“活跃连接数”。如果频繁出现拿连接超时,就把maximum-pool-size调大;如果连接池一直空闲但数据库连接数很高,就把minimum-idle调低。
注意:
idle-timeout一定要小于max-lifetime,不然可能出现连接池里连接已经空闲超时,但池子又从“非空闲”列表里把它捞出来用的情况,这是 HikariCP 文档里明确说明的注意事项。
5. 驱动参数配置:socketTimeout、connectTimeout 该怎么设
5.1 这两个参数的作用边界
PostgreSQL JDBC 驱动有几个连接超时参数,经常有人搞混:
connectTimeout:TCP 建立连接的超时时间,单位秒。默认 10 秒。这个只影响建连阶段。socketTimeout:Socket 读数据的超时时间,单位秒。默认 0,表示永不超时。一旦设置了非零值,驱动在等待数据库响应时会强制在指定时间内放弃。
很多人为了“防止慢查询把线程卡死”,给socketTimeout设置了很小的值,比如 5 秒或 10 秒。结果遇到稍微复杂一点的报表查询,数据库执行要 3 秒,返回数据要 2 秒,网络再抖动一下,驱动直接抛SocketTimeoutException,最终表现为:
Caused by: java.net.SocketTimeoutException: Read timed out你会看到异常文案还是An IO error occurred while sending to the backend,但实际原因是你自己把超时时间设得太短了。
5.2 推荐的配置策略
我的建议是:
connectTimeout保持默认或设为 5~10 秒即可,太长会拖慢故障感知,太短在 DNS 解析慢的网络里会误报socketTimeout的取值要围绕业务里的“最长查询耗时”来定。如果业务里最慢的查询是 30 秒,那socketTimeout至少要设 45 秒,留 50% 以上的冗余- 如果业务里没有任何长时间运行的查询,可以设成 60 秒,避免单个线程被无限期卡住
- 如果业务里有 ETL 任务、大量数据导出,建议不要全局设置
socketTimeout,而是在执行这些特殊 SQL 时通过Statement.setQueryTimeout()单独控制
JDBC URL 示例:
jdbc:postgresql://10.0.0.10:5432/appdb?connectTimeout=5&socketTimeout=60顺带一提,Statement层的setQueryTimeout()和驱动层的socketTimeout是两个独立的机制。前者是驱动层面计时,到了时间主动取消;后者是 Socket 层面的读超时。两者不要混为一谈。
5.3 一个很容易坑到人的组合:statement_timeout + socketTimeout
如果 PostgreSQL 服务端设置了statement_timeout,应用侧又设置了socketTimeout,两者之间会发生什么?
比如:
statement_timeout = 30ssocketTimeout = 10s
你执行一条可能要跑 20 秒的查询。数据库那边还没到 30 秒,查询还没被取消;但应用侧 Socket 层在 10 秒没读到数据就抛Read timed out了。这时候数据库其实还在继续执行这条查询,应用已经放弃了。
更麻烦的是,这条查询的 PostgreSQL 后端进程会继续占用 CPU、内存,直到statement_timeout到了才被取消。如果并发多,数据库直接拖垮。
所以,socketTimeout一定要大于服务端最慢查询可能执行的时间。否则你就是在人为制造“死连接”。
6. 深入了解 PostgreSQL 服务端行为:从“等闲断连”到“挽留连接”
6.1 idle 连接被后端主动清理的几种场景
前面已经提到了idle_in_transaction_session_timeout。但还有几个场景也会让 PostgreSQL 主动断连:
- 数据库在
pg_ctl reload或重启时,现有连接会被断开(重启必然断,reload 保留连接但某些参数变更会影响连接行为) - 主备切换后,旧主库上的连接全部失效
- 超级用户执行
pg_terminate_backend(pid)定向杀会话 - 发生了
Out of Memory或Too many open files,后端进程崩溃
如果数据库日志里能看到:
LOG: terminating inactive session DETAIL: idle_in_transaction_session_timeout那就说明连接是被参数主动清理的,这类问题通过调整应用事务和参数就能解决。
6.2 为什么说“等闲断连”是数据库自我保护,但有时会误伤
idle_in_transaction_session_timeout的初衷是防止事务开启后一直不提交,导致xmin老化、VACUUM 无法清理死元组、表膨胀。这些是 PostgreSQL 运维里非常严重的问题。
但在实际业务里,连接池连接空闲在池中时,通常不会处于事务中。如果连接池拿到一条连接后自己开启了事务,然后又因为代码 bug 没提交,这条连接就会一直处于 idle-in-transaction 状态,直到被数据库断开。
这种情况下,应用侧拿连接时可能不知道这条连接已经“被断过”,发 SQL 就报IO error。而根因其实是代码里事务管理出了问题,不是连接池也不是数据库的问题。
排查方法很简单:在数据库侧执行:
SELECT pid, state, query, xact_start, now() - xact_start AS xact_age FROM pg_stat_activity WHERE state = 'idle in transaction' AND xact_start IS NOT NULL;如果频繁出现idle in transaction的连接,那就顺着这个 pid 查应用堆栈,看是哪段代码开事务忘了关。
6.3 后端连接的内存与文件描述符压力
另外一个常被忽略的方面:每一条 PostgreSQL 后端连接本身就要占用内存和文件描述符。如果应用连接池没有正确回收连接,数据库侧会积累大量 idle 连接,每个连接至少占几 MB 内存,连接数多了以后,系统文件描述符会被占满,新的连接直接进不来,已有连接也可能在后端进程崩溃时被断开。
查看当前连接数:
SELECT count(*) FROM pg_stat_activity; SELECT max_conn FROM pg_settings WHERE name = 'max_connections';如果连接数接近max_connections,那问题已经不是“个别连接报 IO error”了,而是“整个数据库无法接受任何新连接”。这种情况下,应用侧的表现往往是大量IO error和connection refused混杂出现。
7. 实践案例:我遇到的一次典型“低峰期第一个请求报错”完整排查过程
这里分享一个完整的案例,希望能让大家把前面所有知识点串起来。
背景:某电商后台,Java 8 + Spring Boot 2.x + HikariCP + PostgreSQL 12(云数据库 RDS)。现象:每天早上 8 点前后,应用日志里出现几十条An IO error occurred while sending to the backend,持续时间约几分钟,之后自动恢复。白天高峰时段几乎不报错。
第一反应:难道是早上流量起来了,连接池不够用?但看了监控,数据库连接数不到 20,CPU 使用率不到 10%,慢查询也没有。
接着看报错时间,每次都集中在早上 8:00~8:05。查看数据库日志,发现这期间有大量LOG: disconnection记录,且断开时间集中在 8:00 左右。进一步看,这些连接大多是凌晨 2 点~ 6 点建立后一直 idle 的。
于是查数据库参数:
SHOW idle_in_transaction_session_timeout;结果是 0(不限制)。那就不是事务超时。
再查网络层:云数据库控制台显示“连接空闲超时”设置为 300 秒。这意味着所有空闲超过 5 分钟的连接都会被云平台的代理断开。凌晨低峰期,连接基本都处于空闲状态,到 8 点前就被代理全部清理掉了。8 点一到,应用收到第一批请求,连接池拿出来的全是已经被断开的死连接,于是批量报错。
解决方案:
- 将 HikariCP 的
maxLifetime从 30 分钟改为 240000 毫秒(4 分钟),确保连接在代理断开之前被池子主动重建 - 将
minimum-idle调低,避免凌晨空闲连接过多,减少无效占用的同时降低被断开后重连的频率 - 应用侧加一个定时任务,每 3 分钟执行一次
SELECT 1预热连接,让连接永远不会空闲超过 3 分钟
改完后跑了整整一周,这个报错一次都没再出现。
这个案例告诉我们一个很简单的道理:你的连接池、数据库、中间件,对“连接空闲多久算失效”的理解必须一致。有一方不统一,就会出现“你觉得连接活着,它觉得连接死了”的错位。
8. 进阶排查手段:当常规手段无效时,还可以从这几条路继续深入
8.1 用 pg_stat_activity 抓报错瞬间的会话状态
有些问题不是稳定的,你很难在几十分钟内等到报错复现。这时候可以采用“带病观察”的方式:在应用日志里找到报错时间点,去数据库侧查执行记录。
SELECT pid, state, query, query_start, xact_start, wait_event_type, wait_event, backend_start FROM pg_stat_activity WHERE backend_start < now() - interval '10 minutes' ORDER BY query_start DESC NULLS LAST;重点看报错时间里,是否有连接处于active状态但query字段为空、或wait_event异常的情况。如果某些连接长时间没有 query 但也没有 idle 标记,也有可能是连接状态错乱。
8.2 让应用记录连接的唯一标识,从应用日志到数据库日志串起全链路
把两边的日志时间对齐,往往能快速锁定是哪条连接出了问题。
PostgreSQL 的日志可以通过log_line_prefix加上连接信息:
%p = process id %c = session id %u = user name %d = database name %a = application name建议配置:
log_line_prefix = '%m [%p] %q%u@%d %a '这样数据库每次记录 disconnect 或 error 时,就能看到 session、用户、数据库、应用名。应用侧如果在日志里打上连接 ID,再把连接池的connection-init-sql里设置application_name:
SET application_name = 'my-service-01'这样两边就能用 application_name 对上,快速确定是哪条连接、哪个实例出了问题。
8.3 检查驱动版本,老版本驱动的确有过类似问题的修复
PostgreSQL JDBC 驱动更新频率不低,很多个版本就是专门修这种连接层面的 bug。比如:
- 42.2.x 系列修过
isValid()不准确、socket 超时处理异常等问题 - 42.3.x 系列修过多个与 TLS/SSL 握手相关的 IO 异常
- 42.5.x 系列对连接状态的标记逻辑做了调整
如果你项目里的驱动版本停留在一两年以前,而你们恰好用了云数据库、启用了 SSL、或者连接池经常空闲,那先升级驱动版本再观察,可能是性价比最高的修复方案。
检查自己项目的驱动版本:
- Maven 项目:查看
pom.xml里org.postgresql:postgresql的版本 - Gradle 项目:查看
build.gradle里的依赖声明
升级的时候注意,PostgreSQL JDBC 驱动要求的 Java 版本不同:
- 42.2.x 支持 Java 7+
- 42.3.x 支持 Java 8+
- 42.6.x 支持 Java 8+ 且完善了不少连接回收场景的处理
如果你的应用还在 Java 7 上跑,可能需要考虑先升 Java 再升驱动,否则只能停在老版本。
8.4 PgBouncer 等连接池中间件的额外排查点
如果你用了 PgBouncer,那问题又多了一层。
PgBouncer 的server_idle_timeout参数会控制后端连接的存活时间。如果这个值太小,PgBouncer 会频繁断开与 PostgreSQL 的连接,而应用侧通过 PgBouncer 拿到的连接是“客户端连接”,它不一定会感知到后端连接已经重建,于是就可能出现发送时 IO 错误。
检查 PgBouncer 配置:
[databases] appdb = host=127.0.0.1 port=5432 dbname=appdb [pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 server_idle_timeout = 300 max_client_conn = 1000这时候要同时调整:
- PgBouncer 的
server_idle_timeout小于 PostgreSQL 侧的tcp_keepalives_idle - 应用连接池的
maxLifetime也小于这个值
三层之间的超时时间必须形成一个递减链,否则哪层先断,哪层就要承担报错的“风口”。
9. 最终处理建议与预防措施:真正把问题按死
把前边的排查路径走完,针对不同根因,最后给出一份可以直接抄的应对清单。
连接池配置调整(最优先)
- 确认
maxLifetime小于数据库侧和中间件侧的最小空闲超时 - 确认
idle-timeout小于max-lifetime - 确认
validation-timeout介于 1~5 秒 - 确认
connectionTimeout不是无限大,一般 30 秒内 - 确认连接池监控开启,能查到借出连接的平均耗时和等待数
PostgreSQL 参数调整(需结合运维规范)
- 保持
idle_in_transaction_session_timeout为一个合理的非零值,建议 30~60 秒,前提是应用事务都足够短 - 设置
tcp_keepalives_idle为 30~60 秒,让 PostgreSQL 更早感知到死连接 - 设置
tcp_keepalives_interval为 5~10 秒 - 设置
tcp_keepalives_count为 3~6 次
应用代码层面
- 确保所有 SQL 操作都有合理的超时控制,不能用无限制的查询把连接占住
- 大数据量查询尽量单独走只读数据源,和在线业务数据源隔离
- 定时任务中避免使用连接池里长时间持有的连接,需要时即取即用即还
驱动层面
- 升级到当前最新的稳定版 JDBC 驱动
- 根据业务最大查询耗时设置
socketTimeout,不要拍脑袋设个 5 秒 - 判断慢查询用数据库侧
statement_timeout控制,同时确保应用侧socketTimeout大于这个值
监控与告警
- 建议把
An IO error occurred while sending to the backend纳入监控关键字,但要区分频率 - 偶发几条(每分钟小于个位数)可能是网络抖动,不必过于紧张
- 如果分钟级数量超过几十条甚至上百条,就要触发告警让人介入
最后说一个我自己的体会:这个报错之所以能把很多人绕晕,是因为它太“笼统”了。它像是一个综合门诊,背后可能是心脏病、胃病、甚至是心理问题,但挂号的地方只有一个。真正的解法其实是平时把连接池、超时参数、keepalive 这些基本功做扎实,大部分时候问题根本不会冒出来。
如果你已经被这个问题折腾了一两天,不妨把上面这些点逐项过一遍,尤其是连接池的maxLifetime和数据库侧的 keepalive 参数,大概率能帮你找到答案。这个内容后续还可以继续扩展的方向是:如何基于pg_stat_activity和应用程序日志做全链路的连接生命周期分析,那套体系搭出来之后,应对这类问题会从容很多。