接前一篇文章
TCP的backlog导致的HBase超时问题
https://yq.aliyun.com/articles/117801?spm=5176.8091938.0.0.kypXIC
问题场景
如上图所示,用户业务服务器(ApplicationServer)上面发起HTTP GET/PUT请求,经过SLB到达后端服务器(HBase-Rest-Server), 一般请求链路耗时大概100ms左右,但是会有一定的概率出现耗时很长(超过3s)。
业务方提出问题:
1.为什么slb到ecs连接多
2.为什么过slb后耗时多了2s多
排查分析过程
- 首先查询slb http层访问日志, 发现有很多超过3秒的访问请求,包括put 和get方法, 其记录表明slb 访问业务ECS得到处理结果时间较长, 可以判断出耗时较长的原因可能是在后端网络或ECS服务处理上.因为业务ECS HBase并没有记录每条访问请求的处理时间,所以不能排除服务本身处理的耗时.
另外分析日志还存在有与业务ECS 5秒不能建立连接成功请求, 这一般可能是后端服务不稳定或服务器没做优化会出现此情况 - 网络同事查看业务ECS 发现有TCP: time wait bucket table overflow内核日志提示, 证明业务ECS没有做TIME_WAIT内核参数的优化;
- 网络同事抓包后发现有一些syn报文重传情况,从网络同事处拿到报文后分别用(tcp.analysis.retransmission), (tcp.flags.syn==1)&&(tcp.analysis.retransmission)过滤发现5分钟内有3329条syn报文的重传,且重传时间都是3秒.
备注:
linux 2.6.32 内核 TCP_TIMEOUT_INIT 3秒是syn报文第一次重传默认规定时间
linux 3.10内核 TCP_TIMEOUT_INIT 1秒是syn报文第一次重传默认规定时间.
到此问题比较明确是TCP建立连接失败, 不断重传SYN报文引起延时, 为了证明让在业务ECS上用nstat以下命令查看一下建立连接失败的统计和建立连接队列是否存在溢出.
果然发现执行结果TcpAttemptFails 偶尔非0, TcpExtListenOverflows有10多的值出现.
关于连接数量的分析:
查看用户slb实例前端流量与连接状态图,可知每秒平均259条新建连接
业务服务器两台, 从上边nstat输出观察可知每台每秒约100-130条TcpPassiveOpens连接,两台的和值与前端连接数量符合.
问题解决建议
给出业务ECS端建立连接失败优化调整建议:
net.ipv4.tcp_max_syn_backlog = 16384 原默认值1024
net.core.somaxconn = 65535 原默认值128
服务器程序中listen backlog = 8191
tw参数:
net.ipv4.tcp_tw_reuse改成1
net.ipv4.tcp_max_tw_buckets增加一倍,改为10000
tcp_fin_timeout 调低吧 15秒
但调整后还是发现有耗时3秒的情况, 随后调查HBase服务发现jetty默认backlog 50, 没有做修改, 修改后耗时较长现象消除.
下边属于上述参数内核代码部分分析
建立连接半连接与全连接队列
内核参数somaxconn,sysctl_max_syn_backlog 和listen backlog参数之间的关系分析
要搞清楚内核中与建立连接有关tcp_max_syn_backlog,somaxconn两个参数的作用,需要细化说明tcp建立连接阶段两个队列的含义:
linux内核中用struct request_sock_queue -> struct listen_sock->struct request_sock结构存储当前正在请求建立连接的sock,称作半连接状态(用syn_backlog表示)。request_sock有个成员变量指针指向对应的struct sock。struct request_sock_queue中rskq_accept_head和rskq_accept_tail分别指向已经建立完连接的request_sock,称作全连接状态(用backlog表示),这些sock都是完成了三次握手等待程序调用accept接受连接。半连接队列在内核中的具体的变量是:
inet_csk(sk)->icsk_accept_queue-> listen_opt
使用了以下两个变量维护半连接队列长度:
已使用队列长度 inet_csk(sk)->icsk_accept_queue->qlen
最大队列长度限制 icsk_accept_queue->listen_opt->max_qlen_log (即长度限制2^ max_qlen_log)
全连接队列在内核中的表示:
request_sock_queue结构中使用rskq_accept_head和rskq_accept_tail维护了全连接队列。inet_csk(sk)->icsk_accept_queue->rskq_accept_head
inet_csk(sk)->icsk_accept_queue->rskq_accept_tail
使用了以下两个变量维护全连接队列长度
unsigned short sk_ack_backlog; //(全连接队列)可接受连接队列,使用过程中队列的计数
unsigned short sk_max_ack_backlog; //(全连接队列)最大的可接受连接队列,默认值是listen时设置的backlog参数
服务器端程序通过listen函数设置监听端口 backlog参数,内核理论上将允许该端口最大同时接收2*backlog参数个并发连接“请求”(不含已被应用程序接管的连接)——分别存放在 syn_backlog 和 backlog 队列——每个队列的最大长度为backlog值(为什么是最大还取决于内核参数稍后解释)。syn_backlog 队列存储 SYN_ACK 状态的连接,backlog 则存储 ESTABLISHED 状态但尚未被应用程序接管的连接。accept系统调用时将从全连接backlog队列的rskq_accept_head取出head节点req请求,并从此队列中移除,具体的实现可以参考内核中的tcp_check_req()函数,此函数调用完成后也将握手完成后的连接放入了ehash散列表,就与连接阶段的两个队列没有关系了。
内核参数somaxconn,sysctl_max_syn_backlog 和listen backlog参数之间的关系分析
这部分的分析有兴趣的可以查看内核中SYSCALL_DEFINE2(listen, int, fd, int, backlog)和reqsk_queue_alloc()函数的实现。
listen()参数backlog,内核参数somaxconn, 内核参数tcp_max_syn_backlog中最小值加1后,向上扩展为2整数次幂后 做为半连接队列的长度。代码的实现如下:
if ((unsigned)backlog > somaxconn)//listen调用的backlog大于somaxconn则取两者之小值
backlog = somaxconn;
nr_table_entries = min_t(u32, backlog, sysctl_max_syn_backlog);//取与max_syn_backlog之最小值
nr_table_entries = max_t(u32, nr_table_entries, 8);// nr_table_entries小于8时取为8
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1); //roundup_pow_of_two - round the given value up to nearest power of two 作用是:计算出最接近2的n次方并且大于size的值即向上扩展为2整数次幂
最终nr_table_entries的值就是半连接队列的长度。
listen()参数backlog,内核参数somaxconn取二者的最小值做为全连接队列的长度。
if ((unsigned)backlog > somaxconn)//listen调用的backlog大于somaxconn则取两者之小值backlog = somaxconn;
sk->sk_max_ack_backlog = backlog;