news 2026/9/26 5:50:07

PostgreSQL发送IO错误排查:sending to backend解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL发送IO错误排查:sending to backend解析

用PostgreSQL做开发或者维护的人,多半在日志里撞见过“An IO error occurred while sending to the backend”。我第一次和它打交道,是在维护一个Java批量同步任务的时候:任务跑到一半,日志里突然冒出一行PSQLException,整个批处理直接中断。当时第一反应是去翻数据库日志,结果服务端干干净净,连个warning都没有。后来从应用配置、连接池、网络设备一层层查下去,才把真正原因找出来。这篇就把这个异常在什么场景下出现、底层是怎么回事、怎么一步步定位、最后怎么处理,完整拆开讲一遍。不管你是写业务的开发,还是管库的DBA,只要你的应用走TCP长连接连PostgreSQL,都可能用得上。

1. 这个报错到底在说什么

1.1 报错发生的典型场景

我梳理了一下自己遇到、以及帮别人排查过的案例,这个报错出现频率最高的场景有三个,基本覆盖了九成以上的线上问题。

第一是应用侧的连接池长期空闲后,第一次发起查询。比如晚上系统没人用,第二天早上第一个请求打过来,直接报错。我前同事负责的一个系统就是这样,每天早上一上班,第一批请求里总有几条报这个错。业务方第一个电话打给DBA,DBA查了一圈说数据库没毛病,最后问题落到了连接池的maxLifetime上。这种情况其实不是数据库挂了,而是连接中间某个环节已经被断掉,客户端自己却不知道。

第二是长时间执行或大数据量交互的任务。批量导数据、报表聚合、大事务提交,这类跑很久的操作在中途报“sending to the backend”失败,往往意味着服务端或网络侧中途把连接关了。我之前跑一个千万级数据的报表导出,跑了大概40分钟,眼看快结束了,客户端突然报这个错误。后来定位是数据库在做主从切换,旧连接被服务端重置,应用没有及时感知。

第三是在云数据库或远程访问场景下,公网、NAT网关这类链路经常出现。连接保持一段时间后,中间的网关设备会静默回收空闲连接,客户端完全无感知,等下一次发数据才发现写不进去。云厂商的SLB、NAT网关对空闲TCP连接的老化时间各不相同,短的可能只有一两分钟,长的有十分钟,如果应用和数据库之间隔了这类设备,这个坑几乎必踩。

和MySQL对比一下会更直观:MySQL的JDBC驱动遇到类似问题,常见报错是“Communications link failure”或“The last packet successfully received from the server was … milliseconds ago”,而PostgreSQL的JDBC驱动则直接告诉你是“sending to the backend”阶段出了问题。两者底层原因有交集,但PG的连接模型和参数体系不太一样,排查路径也略有区别。

1.2 从协议和连接机制看根因

要真正理解这个报错,得先明白PostgreSQL的通信模型。PG使用的是基于TCP的明文协议,客户端与服务端之间是一个持久化的TCP连接。正常情况下,应用每次查询都复用这个连接,而不是重新建连接。连接一旦建立,两边都不会主动发额外的心跳包,除非你显式开启了TCP keepalive。

所谓的“an IO error occurred while sending to the backend”,拆开看就是:客户端正在向服务端写数据,但在socket写入时发生了IO错误。底层的Java异常通常还能看到“Broken pipe”或者“Connection reset by peer”之类的caused by。Broken pipe的意思是,客户端往一个已经被对端关闭的socket里写数据;Connection reset则是对端直接回了RST包。

这里有个很多新手容易忽略的细节:socket写入失败不一定发生在“发出去”的那一瞬间。TCP是有发送缓冲区的,客户端调用write的时候,数据可能先进了本地内核缓冲区,内核后续尝试发送时才发现连接已经断了,然后才给应用返回错误。所以这个报错出现的时间点,跟你上一次成功通信之间可能已经隔了很久。这也解释了为什么很多报错出现在“空闲后第一次使用”而不是“正在频繁读写时”。

从协议上还有一个视角:如果在事务中服务端主动断连,客户端通常要等到下一次与服务器交互(发送查询或提交事务)时才会感知到错误。如果是网络设备静默丢包,客户端甚至要等TCP超时重传失败以后才报错。connectTimeout、socketTimeout这些参数决定的就是客户端在什么时间点放弃等待,这个后面会单独讲。

2. 最常踩的四个坑

2.1 连接池里的“僵尸连接”

这是出现频率最高的一类。应用普遍会引入连接池(HikariCP、Druid、dbcp2),连接池为了性能会缓存一批空闲连接。但连接池只知道它把连接给了应用、应用又还回来了,它并不知道数据库那边是不是还认这条连接。

数据库、操作系统、网络设备并不会无限期维护一条空闲连接。比如Linux系统默认的TCP keepalive时间通常要7200秒(2小时)才首次探活;一些云厂商的SLB/NAT网关对空闲连接的回收时间可能只有300秒左右。如果连接池里的连接空闲超过这个时间,中间设备可能直接丢弃这条连接,而数据库和客户端两边都没有立刻收到通知。连接池继续把这根“已经死了”的连接返给应用,应用一写数据就报IO error。

解决思路也很明确:让连接池在把连接交给应用之前先确认连接可用,同时让连接池里的连接生命周期短于外部设备的回收周期。具体参数我放到第3节讲,这里只强调一个理念——连接池不是万能的,它默认不检测“远端是否还活着”,是需要你显式配置的。很多人以为开启了连接池就一劳永逸,实际上连接池只负责复用连接,不负责连接的健康管理。

2.2 数据库参数提前断开了会话

除了外部设备断开连接,PostgreSQL本身也可能主动切断连接。两个最容易被踩中的参数是statement_timeout和idle_in_transaction_session_timeout。

statement_timeout设置的是单条SQL的最长执行时间,超过就取消SQL。这个报错通常伴随“canceling statement due to statement timeout”,应用会看到SQL exception,但不会直接看到IO error。这个参数往往不是莫名其妙的,更多是业务SQL写得太烂,或者没有走索引导致的全表扫描,把数据库卡死了。

更隐蔽的是idle_in_transaction_session_timeout。它的意思是:如果客户端打开了一个事务,但又不在执行任何SQL,处于“事务中空闲”状态,超过设定秒数之后服务端直接断开连接。很多应用在连接池里开启了自动提交,但也有的框架(比如某些ORM)会手动begin事务,然后因为业务逻辑慢或者等待外部接口,事务一直挂着。等它终于想提交时,发现连接早被服务端关了。

这种断开是服务端主动发起的,客户端拿到的是“connection already closed”或者“An IO error”。数据库日志里也会记录“terminating connection due to idle-in-transaction timeout”。如果你在数据库里查不到这类日志,只看到IO error,那基本可以排除这个参数。顺带说一句,很多团队为了防僵尸事务把这个参数设置得很激进,比如30秒或60秒,结果业务稍微慢一点就中招,设置前得想清楚业务特征。

2.3 中间网络设备悄悄回收了连接

云环境里最常见的坑。数据库可能放在云RDS,应用在自己的服务器上,两边要走公网或经过SLB/NAT网关。这些网络中间设备出于资源考虑,通常会对空闲的TCP连接做老化回收。有的设备回收时会给对端发送FIN或RST,但也有的设备就是静默丢包,什么都不发。静默丢包最难受,因为TCP两端都还认为连接活着,实际上链路早就断了。

我们团队踩过的例子:一个定时任务每5分钟跑一次,白天一直正常,但每天早上第一次跑的时候必现这个IO error。分析下来,夜间没有任务,连接空闲了几个小时,云网关那边的空闲连接老化时间早就到了。任务一启动,连接池取出空闲连接,往服务端发SQL,socket写失败,任务就崩了。

判断这类问题有一个技巧:如果你把应用和数据库放在同一内网、同一个VPC,绕开公网和负载均衡,同一套代码就不再报错,那基本可以肯定是中间链路的问题。另外,抓包时如果发现只有最后一个数据包之后就没了动静,既没有FIN也没有RST,等到超时才报错,也是网络设备静默丢弃的典型特征。我遇到过最夸张的一个案例,问题出在客户公司自己的防火墙策略上,安全团队为了防扫描设置了空闲连接回收,结果业务侧遭殃还不自知。

2.4 数据库进程异常退出

连接是服务端和客户端共同维护的,如果服务端进程异常退出,所有连接也会全部断开。这类原因在日志里会有明显特征,比如“server process (pid 12345) was terminated by signal 9: Killed”或“terminating connection because of crash of another server process”。最常见的诱因是内存不足(OOM Killer把postgres进程杀了)或者数据库未正常关闭直接重启,极端情况下也可能和checkpointer等后台进程异常有关系。

这个问题和业务代码基本无关,但应用侧的表现同样是IO error,而且常常是大量连接同时报错,不像前几种情况往往是零散几条。排查时不要只盯应用,记得看看数据库服务器上的dmesg,有没有大量Out of memory的记录,以及数据库日志最后几行的状态。如果确认是OOM,那不是改连接池能解决的,得去查内存分配、shared_buffers设置、系统可用内存,甚至要考虑是不是其他进程把内存吃光了。

3. 实操排查路径全记录

3.1 先查数据库侧的证据

遇到这类报错,第一步不是去改代码,而是先确认数据库当时在干什么。PostgreSQL提供了pg_stat_activity视图,可以查看当前所有连接的状态。如果你能把报错时间点对应上,就能看到报错前这个连接是什么状态。

SELECT datname, usename, application_name, client_addr, state, backend_start, state_change, xact_start, query FROM pg_stat_activity WHERE datname = 'your_database';

state字段是关键。如果是idle,说明这个连接当时没有在跑查询,只是静静地挂在池子里,IO error大概率是连接复用时的失效问题。如果是active,说明服务端正在执行某条SQL,那要去看查询本身有没有问题、有没有长时间执行触发超时。xact_start字段也值得看,如果它显示一个很早的时间,说明事务挂了很久,就要怀疑idle_in_transaction_session_timeout这类参数。

数据库日志也很重要。检查log_connections和log_disconnections这两个参数有没有开启,开了的话,服务端每次收到和断开连接都会写日志。报错时间点如果只有connection received,却没有对应的disconnection日志,说明服务端进程认为连接还活着,但客户端已经写不进去了。这个时候问题多半在网络链路或客户端侧,而不是数据库。

另外顺带确认一下相关参数当前的值。很多生产库的statement_timeout和idle_in_transaction_session_timeout可能是被之前的人调过的,直接SHOW出来看又快又准:

SHOW statement_timeout; SHOW idle_in_transaction_session_timeout; SHOW tcp_keepalives_idle; SHOW tcp_keepalives_interval; SHOW tcp_keepalives_count;

tcp_keepalives三个参数默认都是0,意思是沿用操作系统默认值。如果数据库部署在云上,或者连接要穿越多层网络,强烈建议把这几个参数显式调小,下面会给具体值。这一套查下来,至少能排除掉一半的原因。

3.2 用抓包定位“谁先断开”

如果数据库日志里看不到端倪,下一步就是用tcpdump抓包,搞清楚连接到底是谁断开的。抓包位置最好同时覆盖客户端和服务端两端,如果条件不允许,至少抓数据库这一侧的包。

tcpdump -i eth0 host <数据库IP> and port 5432 -w pg_cli.pcap

抓一段时间后,用Wireshark打开,过滤出对应的TCP流,重点看挥手阶段。关键判断规则如下:

  • 如果看到服务端先发FIN,客户端随后响应FIN/ACK,说明是服务端主动关闭。此时去查服务端日志和参数。
  • 如果看到RST包,说明对端认为连接异常,直接重置。常见于服务端进程崩溃或中间防火墙主动拒绝。
  • 如果TCP流里最后一个包之后什么也没有,直到客户端重传超时(能看到TCP Retransmission),说明是中间链路静默丢包。这时要重点检查NAT、SLB、防火墙的老化配置。

我见过很多人一上来就怀疑数据库,其实抓包一次就能把锅甩给正确的方向。花10分钟抓包,比盲目的在代码里加日志强得多。抓包的时候别忘了一点:确认应用当时真的在用这个连接,别抓到的是其他客户端的无关流量,那会干扰判断。

3.3 连接池参数核对清单

如果确认是连接空闲失效或复用失效,那就轮到连接池背锅了。这里针对最常用的三个池子各说一套推荐配置。

HikariCP是目前Spring Boot的默认连接池,配置参数如下:

maximumPoolSize: 20 minimumIdle: 5 maxLifetime: 1500000 idleTimeout: 600000 keepaliveTime: 60000 connectionTestQuery: SELECT 1

注意几个关键点。maxLifetime一定要小于数据库和网络链路可能断开连接的时间。如果外部设备回收时间是300秒,那maxLifetime最多设240秒左右,留出余量。keepaliveTime是HikariCP 4.x新增的参数,目的是让连接池里的空闲连接每隔一段时间就发一个探测请求,防止中间设备回收。connectionTestQuery在某些JDBC驱动版本里可以省略,因为可以用JDBC4的Connection.isValid()方法做探测,但显式写出来更稳妥。

Druid是阿里开源的连接池,比较典型的配置:

minIdle: 5 maxActive: 20 initialSize: 5 testWhileIdle: true testOnBorrow: true timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 keepAlive: true validationQuery: SELECT 1

testOnBorrow每次从池里取连接都会执行一次探测,性能有损耗,但查错方便。网上很多配置只开testWhileIdle,实际效果是空闲连接定期检查;如果你需要更严格地把关,testOnBorrow可以临时开起来做排查,确认问题后可以关掉。Druid的keepAlive参数是告诉连接池保持最小空闲连接,同时主动检测无效连接并移除,对这类问题很有用,不要漏配。

Python用户如果用psycopg2/SQLAlchemy,其实也有对应的开关。SQLAlchemy的create_engine里有一个叫pool_pre_ping的参数,它会在每次从连接池取连接前用SELECT 1检测连接有效性。很多人以为这是给数据库连不上时用的,其实它最初的定位就是解决这类空闲连接失效问题:

engine = create_engine( "postgresql+psycopg2://user:pass@host:5432/dbname", pool_pre_ping=True, )

如果直接用psycopg2,也可以在连接参数里开启libpq的keepalive能力:

conn = psycopg2.connect( host="dbhost", port=5432, dbname="mydb", keepalives=1, keepalives_idle=60, keepalives_interval=10, keepalives_count=6, )

3.4 参数调整与代码改造示例

确认原因并针对连接池做了配置后,我还有一套配套的调整方案,按优先级从高到低排列。

第一,JDBC URL里显式开启TCP keepalive,并设置socketTimeout。对于PostgreSQL JDBC驱动,URL可以这样写:

jdbc:postgresql://dbhost:5432/mydb?tcpKeepAlive=true&socketTimeout=600&connectTimeout=10

tcpKeepAlive对应到操作系统层面的TCP keepalive,让内核定时探测连接是否真的活着。socketTimeout是socket读超时,默认是0表示无限等待。请注意,socketTimeout不是连接超时,而是建立连接之后的每次读操作超时。对于批量大查询,建议给一个合理较大的值,比如600秒,避免长查询因为读超时被中断,同时又不至于让应用无限等下去。

第二,在数据库侧调整tcp keepalive参数。如果你有权限改数据库配置,建议至少在集群级别设置:

ALTER SYSTEM SET tcp_keepalives_idle = 60; ALTER SYSTEM SET tcp_keepalives_interval = 10; ALTER SYSTEM SET tcp_keepalives_count = 6;

上面的含义是:连接空闲60秒后开始发keepalive探测包,每10秒发一次,连续6次没响应就判定连接失效。这样一个连接最长120秒左右就能感知到对端死掉,比系统默认的2小时快得多。改完要reload或重启生效,可以用SHOW确认。注意这个参数是让连接更快发现异常,并不会主动断开健康的连接,所以调小不会带来副作用。

第三,Linux系统层面也可以修改全局的TCP keepalive参数。操作数据库所在机器(或客户端所在机器)时,在/etc/sysctl.conf中设置:

net.ipv4.tcp_keepalive_time = 60 net.ipv4.tcp_keepalive_intvl = 10 net.ipv4.tcp_keepalive_probes = 6

然后执行sysctl -p生效。注意这个改动影响机器上所有TCP连接,建议在测试环境验证之后再上生产。如果团队规范不允许动系统参数,那就在PostgreSQL里设置tcp_keepalives_*,效果是等价的。

第四,应用层给IO错误加一次重试。毕竟无论怎么调,偶发的网络抖动还是存在,业务代码里对这类可重试错误做兜底很重要。比如Spring里可以用@Retryable对DataAccessException做一次重试,Python的SQLAlchemy里可以通过自定义重试装饰器实现。需要注意:重试只适合那些幂等的操作,写操作能不能安全重试要自己评估。另外重试次数别太多,一两次就够,加个短暂的退避时间,避免雪崩。

4. 常见问题速查与避坑经验

4.1 典型问题对照表

我把排查过程中最常见的五种情况整理成一张表,方便直接对照。这张表里的前两行覆盖了八成的线上场景,剩下三行属于冷门但致命的情况。

现象可能原因优先检查项处理手段
每天第一次请求报IO error连接池空闲连接被网络设备回收连接池maxLifetime、网络设备老化时间调短连接池生命周期,开启keepalive
批量任务跑到一半报错statement_timeout或网络中断pg_stat_activity、服务端日志调大statement_timeout,或拆分任务
空闲一段时间后,事务提交时报错idle_in_transaction_session_timeout数据库日志、参数调大该参数,或缩短事务空闲时间
偶发IO error,频率不固定网络抖动或中间防火墙RSTtcpdump抓包应用层重试,网络链路整改
数据库服务器重启后应用全挂连接已被服务端重置数据库日志、dmesg连接池自动重连机制,启动检查连接

4.2 三个最容易踩的坑

第一个坑是乱调socketTimeout。有人为了“让应用不超时”,把socketTimeout设成0或者极大值。这样做表面上一时不出错,但连接如果在中间被静默断掉,应用会一直挂起等待,直到系统内核TCP超时(可能几分钟甚至十几分钟)才返回错误。反而是设置一个合理的读超时,让应用快速失败再重试,体验更好。

第二个坑是只改应用不改数据库。如果你把HikariCP的maxLifetime调成30分钟,但数据库侧还是有idle_in_transaction_session_timeout在5分钟就断开空闲事务,那连接照样会挂掉。这种问题要靠两端协作解决:应用管连接池,数据库管会话参数,系统管TCP参数,三边都对了才能稳定。我见过一个项目,运维把数据库参数调得很激进,应用怎么改都无效,最后两边对线才对齐。

第三个坑是测试环境一切正常、生产才出问题。这通常不是代码的锅,而是环境差异。测试环境没有公网NAT、没有防火墙老化,生产环境全有。我在帮人排查时一定会问一句:同一个应用连内网数据库报不报错?如果内网正常、公网必现,优先查网络设备。这种环境差异性问题,代码改不出结果,得靠基础设施配合。

4.3 我的最终建议

如果你正在被这个报错折磨,我建议按这个顺序来:

先把pg_stat_activity和数据库日志拉出来,确认报错时数据库侧有没有异常;再开抓包确认断链方向;然后按第3节的清单检查连接池参数和tcp keepalive配置;最后在应用层为这类错误加一次安全重试。大多数情况下,做完前三步问题就已经解决了,第四步是为了兜底偶发情况,属于保险措施。

我个人实际操作中的体会是,这个报错本身不可怕,可怕的是把它当成偶发现象忽略掉。很多线上事故其实在早期日志里就有蛛丝马迹,只是没有追下去。现在每当我接到这类问题,都会先问一句“你数据库日志和抓包看了吗”,而不是急着改代码。TCP这条链路上任何一环出了问题,最终都会以IO error的形式暴露到应用层,所以排查时一定要有链路思维。如果你按这篇文章的步骤走一遍还没定位到问题,大概率是抓包时长不够或者环境里有其他应用在共用连接,把包抓全、时间拉长,答案总会浮出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 5:49:57

MySQL事务底层原理:从日志到MVCC的完整拆解

写了好几年业务代码&#xff0c;真正让我对 MySQL 事务原理产生敬畏心的&#xff0c;是一次线上库存超卖的排查。那个下午代码里明明加了事务注解&#xff0c;数据却还是错了&#xff0c;我把日志翻了个底朝天&#xff0c;最后发现是事务隔离级别和锁机制在背后搞鬼。自那以后我…

作者头像 李华
网站建设 2026/9/26 5:49:26

知识图谱入门:从本体建模到Cypher实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 5:49:18

LeetCode移除元素题解:双指针与原地修改的两种高效解法

1. 题目理解与核心考点1.1 题目原文与要点拆解"移除元素"是LeetCode上的第27题。原题描述很简单&#xff1a;给你一个数组 nums 和一个值 val&#xff0c;你需要原地移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。不要使用额外的数组空间&#…

作者头像 李华
网站建设 2026/9/26 5:49:16

RAG知识库全链路实战:从文档解析到混合检索的工程指南

RAG 这个词这两年出现的频率太高了&#xff0c;高到很多人一上来就问“用哪个向量数据库”&#xff0c;却很少有人先把整条链路想清楚。我前后搭过七八套知识库系统&#xff0c;从最早的纯关键词检索&#xff0c;到后来的向量召回&#xff0c;再到现在的混合检索加重排&#xf…

作者头像 李华
网站建设 2026/9/26 5:49:01

BugKu——game1

一、题目2、方法访问服务器&#xff0c;是一个游戏。F12&#xff0c;发现里面有个js文件。这段代码是一个经过混淆的 JavaScript 脚本&#xff0c;核心功能是&#xff1a;实现 Base64 的编码&#xff08;encode&#xff09;和解码&#xff08;decode&#xff09;&#xff0c;并…

作者头像 李华
网站建设 2026/9/26 5:48:34

DeepSeek V4.1 Flash (Batch) 批量推理性能与质量深度评测

在处理大规模数据或需要自动化生成大量内容的场景中&#xff0c;单个请求逐个处理的方式往往显得力不从心。无论是电商平台的商品描述生成、金融领域的日报汇总&#xff0c;还是教育行业的试题批量制作&#xff0c;传统模式下的等待时间和资源消耗都成为了制约效率的瓶颈。许多…

作者头像 李华