
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 层发生 RSTConnection 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 userappuser databasemydb host10.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还是RSTRST 包的源 IP 是谁,目的地是谁报错之前是否有大量 TCP 重传(retransmission)连接 idle 了多长时间后断开的如果是数据库主动发 FIN,说明数据库(或数据库所在宿主机)正常关闭了连接。比如idle_in_session_timeout到期就会这样。如果是 RST,说明某一边在处理一个不属于当前连接状态的数据包时,直接重置了连接。常见原因包括:应用在连接已关闭后继续写数据防火墙对异常状态的连接发了 RST数据库后端进程崩溃,内核清理 socket抓包数据配合 PostgreSQL 日志,基本能把问题定位到三层:应用层、数据库层、网络层。3.4 检查 PostgreSQL 关键超时参数,别让数据库替你“关连接”PostgreSQL 有几个参数会直接导致空闲连接被断开,如果应用侧连接池不知道这件事,就会出现文章开头那个异常。参数名默认值作用idle_in_transaction_session_timeout0(不限制)空闲事务超过该时间后被强制断开statement_timeout0(不限制)单条语句执行超过该时间后被取消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 秒HikariCPmaxLifetime:1800000 毫秒(30 分钟)这个配置下,数据库代理把空闲了 300 秒的连接断开,但连接池里的连接以为还能用到 30 分钟。下一次请求发出去,直接打到已关闭的连接上,报错。正确的做法是:maxLifetime要小于数据库侧和网络侧的最小空闲超时时间。比如中间件 300 秒断空闲连接,那maxLifetime应该设置在 240000 毫秒(4 分钟)甚至更短,确保连接池在数据库断开前主动重建连接。另一个常被忽略的参数是connectionTimeout。它控制的是“从池子拿连接时,等多久拿不到就放弃”,而不是“连接建立后多久超时”。很多人把connectionTimeout设得很长,结果数据库一抖,所有请求都堆积在等待连接上,数据库连接数被打满,应用侧就开始大面积报错。4.2 一个很值钱的配置:connectionTestQuery 还是 JDBC4 isValidHikariCP 默认使用 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?connectTimeout5socketTimeout60顺带一提,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 742.3.x 支持 Java 842.6.x 支持 Java 8 且完善了不少连接回收场景的处理如果你的应用还在 Java 7 上跑,可能需要考虑先升 Java 再升驱动,否则只能停在老版本。8.4 PgBouncer 等连接池中间件的额外排查点如果你用了 PgBouncer,那问题又多了一层。PgBouncer 的server_idle_timeout参数会控制后端连接的存活时间。如果这个值太小,PgBouncer 会频繁断开与 PostgreSQL 的连接,而应用侧通过 PgBouncer 拿到的连接是“客户端连接”,它不一定会感知到后端连接已经重建,于是就可能出现发送时 IO 错误。检查 PgBouncer 配置:[databases] appdb host127.0.0.1 port5432 dbnameappdb [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和应用程序日志做全链路的连接生命周期分析,那套体系搭出来之后,应对这类问题会从容很多。