首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >50G的数据,1TB的日志,凶手居然是...

50G的数据,1TB的日志,凶手居然是...

作者头像
NickYoung
发布2026-07-09 16:25:12
发布2026-07-09 16:25:12
20
举报

竟然是keepalive参数搞的鬼...

对于tcp keepalive参数,多年之前就遇到一个case,用户set tcp_keepalives_idle to 40000无法生效,但PG表明该参数为int类型,最大值为2147483647。用户吐槽:“弱智BUG,垃圾PG!!!” 老杨当时翻阅了TCP代码,梳理清楚了这里的逻辑。

最近又遇到类似问题,由于set tcp_keepalives_count失败。对应的失败调用记录太多,数据量仅仅几十GB,而server log达到数TB,几乎写满磁盘,给用户小帅造成了很大的困扰。所以,专门写一篇记录下。

1 问题现象

server log里有大量的setsockopt(TCP_KEEPXX) failed: Invalid argument异常打印:

代码语言:javascript
复制
2025-07-13 08:25:15.989 CST [2608148] LOG:  setsockopt(TCP_KEEPCNT) failed: Invalid argument
2025-07-13 08:25:41.874 CST [2608148] LOG:  setsockopt(TCP_KEEPCNT) failed: Invalid argument
2025-07-13 08:26:14.383 CST [2608148] LOG:  setsockopt(TCP_KEEPIDLE) failed: Invalid argument
2025-07-13 08:27:05.116 CST [2608148] LOG:  setsockopt(TCP_KEEPCNT) failed: Invalid argument
2025-07-13 08:27:06.901 CST [2608148] LOG:  setsockopt(TCP_KEEPIDLE) failed: Invalid argument
2025-07-13 08:27:08.041 CST [2608148] LOG:  setsockopt(TCP_KEEPCNT) failed: Invalid argument
2025-07-13 08:27:09.199 CST [2608148] LOG:  setsockopt(TCP_KEEPIDLE) failed: Invalid argument

日志类型为Log,从“setsockopt(TCP_KEEPXX)”老杨大致猜测这里是去设置tcp keepalive参数返回的信息。 在PG代码里检索后,印证了之前的想法。

为什么会有这个日志打印呢?

2 原理分析

PostgreSQL: 18beta1

Kernel:v6.16-rc5

在PG中,以设置tcp_keepalives_idle参数为例,完整代码请参考[1],这里摘录了部分代码,当调用 setsockopt返回值小于0,则打印日志“setsockopt(TCP_KEEPXX) failed”

代码语言:javascript
复制
int
pq_setkeepalivesidle(int idle, Port *port)
{
/* 若Unix域登录则不进行配置 */
if (port == NULL || port->laddr.addr.ss_family == AF_UNIX)
return STATUS_OK;
/* 省略 */
/* 如果设置为0,则读取操作系统默认值 */
if (idle == 0)
  idle = port->default_keepalives_idle;
/* 如果调用setsockopt失败,则返回"setsockopt(TCP_KEEPXX) failed" */
if (setsockopt(port->sock, IPPROTO_TCP, PG_TCP_KEEPALIVE_IDLE,
       (char *) &idle, sizeof(idle)) < 0)
 {
  ereport(LOG,
    (errmsg("%s(%s) failed: %m", "setsockopt", PG_TCP_KEEPALIVE_IDLE_STR)));
return STATUS_ERROR;
 }
/* 省略 */
}

所以这里要分析setsockopt函数,需要走读下操作系统内核代码。

setsockopt是glibc函数,通过sys call最终调用到内核态中由do_tcp_setsockopt函数[2]完成对应option设置。

代码语言:javascript
复制
/*
 * Socket option code for TCP.
 */
int do_tcp_setsockopt(struct sock *sk, int level, int optname,
        sockptr_t optval, unsigned int optlen)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
struct net *net = sock_net(sk);
int val;
int err = 0;
/* 省略 */
switch (optname) {
/* 省略 */
/* 当设置的option为KEEPINTVL时:*/
case TCP_KEEPINTVL:
return tcp_sock_set_keepintvl(sk, val);
/* 当设置的option为KEEPCNT时:*/
case TCP_KEEPCNT:
return tcp_sock_set_keepcnt(sk, val);
/* 省略 */
/* 当设置的option为KEEPIDLE时:*/
case TCP_KEEPIDLE:
  err = tcp_sock_set_keepidle_locked(sk, val);
break;
/* 省略 */
}

当option为KEEPIDLE时,调用tcp_sock_set_keepidle_locked[3]进行配置,当入参小于1< 或者入参大于MAX_TCP_KEEPIDLE时,返回-EINVAL

代码语言:javascript
复制
int tcp_sock_set_keepidle_locked(struct sock *sk, int val)
{
struct tcp_sock *tp = tcp_sk(sk);
/* 当入参小于1或者大于MAX_TCP_KEEPIDLE时返回-EINVAL */
if (val < 1 || val > MAX_TCP_KEEPIDLE)
return -EINVAL;

/* Paired with WRITE_ONCE() in keepalive_time_when() */
 WRITE_ONCE(tp->keepalive_time, val * HZ);
if (sock_flag(sk, SOCK_KEEPOPEN) &&
     !((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))) {
  u32 elapsed = keepalive_time_elapsed(tp);

if (tp->keepalive_time > elapsed)
   elapsed = tp->keepalive_time - elapsed;
else
   elapsed = 0;
  tcp_reset_keepalive_timer(sk, elapsed);
 }

return0;
}

MAX_TCP_KEEPIDLE[4]为32767

代码语言:javascript
复制
#define MAX_TCP_KEEPIDLE 32767
#define MAX_TCP_KEEPINTVL 32767
#define MAX_TCP_KEEPCNT  127

EINVAL是Linux/Unix 系统中表示"无效参数"的错误代码,值为22

代码语言:javascript
复制
#define EINVAL      22  /* Invalid argument */

因此,使用setsockopt进行KEEPIDLE设置时,入参小于1或者大于32767就会返回Invalid argument。

并且PG中已经对0值做了处理,当配置为0时,取操作系统默认值。

另外,为什么MAX_TCP_KEEPIDLE是32767?老杨不是操作系统专业从业者,自然不了解背后的逻辑。

不过大模型给出了答案:

  1. 历史原因和兼容性

早期的内核实现中,许多网络相关的参数使用的是 16 位整数(short 类型)来存储值,而 16 位整数的最大值是 32767(即 2^15 - 1)。虽然现代内核已经支持更大的范围,但某些参数的限制仍然保留了这种历史设计,以确保与旧版本的内核或应用程序的兼容性。 2. 实际需求的考虑 tcp_keepalive_time 的作用是设置 TCP 连接在空闲多长时间后开始发送 keepalive 探测包。对于大多数实际场景来说,超过 32767 秒(约 9.1 小时)的空闲时间已经足够长,几乎没有实际需求需要更大的值。因此,限制在 32767 秒可以满足绝大多数使用场景。

3 测试验证

实测tcp_keepalives_idle和tcp_keepalives_interval不能大于32767,tcp_keepalives_count不能大于127,若大于则打印 “LOG: setsockopt(TCP_KEEPXXX) failed: Invalid argument”。

代码语言:javascript
复制
[postgres@Nick ~]$ psql -h 127.0.0.1 -U testuser -d postgres
psql (18beta1)
Type "help" for help.

postgres=> set client_min_messages to log;
SET
postgres=> set tcp_keepalives_idle to 32768;
LOG:  setsockopt(TCP_KEEPIDLE) failed: Invalid argument
SET
postgres=> set tcp_keepalives_interval to 32768;
LOG:  setsockopt(TCP_KEEPINTVL) failed: Invalid argument
SET
postgres=> set tcp_keepalives_count to 128;
LOG:  setsockopt(TCP_KEEPCNT) failed: Invalid argument
SET
postgres=> set tcp_keepalives_idle to 32767;
SET
postgres=> set tcp_keepalives_interval to 32767;
SET
postgres=> set tcp_keepalives_count to 127;
SET
postgres=>

但是PG官方给定的这几个keepalive参数最大值为2147483647(int类型),这是为什么?

代码语言:javascript
复制
postgres=> select name,min_val,max_val from pg_settings where name like '%keepalives%';
          name           | min_val |  max_val   
-------------------------+---------+------------
 tcp_keepalives_count    | 0       | 2147483647
 tcp_keepalives_idle     | 0       | 2147483647
 tcp_keepalives_interval | 0       | 2147483647
(3 rows)

当客户端建连时配置的参数不合理,就有可能会导致日志占用过多空间,造成一些稳定性隐患。

是开发者不清楚TCP这里的底层逻辑?不得而知,代码和文档都没找到相关说明。

不过,要想修改范围也不难,以tcp_keepalives_count为例,最大值从INT_MAX替换为MAX_TCP_KEEPCNT即可。

代码语言:javascript
复制
{
                {"tcp_keepalives_count", PGC_USERSET, CONN_AUTH_TCP,
                        gettext_noop("Maximum number of TCP keepalive retransmits."),
                        gettext_noop("Number of consecutive keepalive retransmits that can be "
                                                 "lost before a connection is considered dead. "
                                                 "0 means use the system default."),
                },
                &tcp_keepalives_count,
                0, 0, MAX_TCP_KEEPCNT,
                NULL, assign_tcp_keepalives_count, show_tcp_keepalives_count
}

这样在设置时,超过TCP对应最大值就会报错,原生的逻辑是打印LOG,而不是ERROR,ERROR能更好的引起用户的注意,意识到给定的值是不合法的,这样就会去治理。 不像之前打印一堆“垃圾”日志,容易忽略,占用大量磁盘空间。

代码语言:javascript
复制
postgres=> select name,min_val,max_val from pg_settings where name like '%keepalives%';
          name           | min_val | max_val 
-------------------------+---------+---------
 tcp_keepalives_count    | 0       | 127
 tcp_keepalives_idle     | 0       | 32767
 tcp_keepalives_interval | 0       | 32767
(3 rows)

postgres=> set tcp_keepalives_count to 128;
ERROR:  128 is outside the valid range for parameter "tcp_keepalives_count" (0 .. 127)
postgres=> set tcp_keepalives_idle to 32768;
ERROR:  32768 s is outside the valid range for parameter "tcp_keepalives_idle" (0 s .. 32767 s)
postgres=>

4 小结

parameter

min_val

max_val

tcp_keepalives_count

0

127

tcp_keepalives_idle

0

32767

tcp_keepalives_interval

0

32767

PG客户端在配置keepalive参数时,一定要注意实际的生效范围,不然会打印大量setsockopt调用失败的记录到日志,浪费大量的存储空间。

Reference:

[1] https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/libpq/pqcomm.c#L1668

[2] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/tcp.c?h=v6.16-rc5#n3760

[3] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/tcp.c?h=v6.16-rc5#n3670

[4] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/net/tcp.h?h=v6.16-rc5#n170

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PostgreSQL运维之道 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 问题现象
  • 2 原理分析
  • 3 测试验证
  • 4 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档