最近接到博客园的反馈,SLB 7层负载均衡的实例会不定期出现流量突跌的情况,突跌持续10s左右;同时,SLB自身监控也观察到了相同的现象;
针对该问题,我们进行了持续追查,最终定位到是nginx配置的原因;在此,分享一下分析排查过程,希望对大家使用nginx有所帮助;
问题描述
两个时间点吻合,初步推断是突增流量导致nginx异常,从而导致流量下跌;
分析过程
从上述现象,怀疑是网络问题,但从协议栈/网卡/交换机多个层面排查,没有发现网络异常;
抓包发现即使是本机发起的请求也会出现 syn 包丢弃重传,从而基本可以确定不是网络的问题,而是我们ngnix有问题。
查看linux协议栈源码,引起 syn 包被丢弃的原因可能有以下两点:
1. Accept backlog(接收队列)满了
2.内存分配不出来了
内核代码如下:
inttcp_v4_conn_request(structsock*sk,structsk_buff*skb)
{
 ...
 if(sk_acceptq_is_full(sk)&&inet_csk_reqsk_queue_young(sk)>1){
NET_INC_STATS_BH(sock_net(sk),LINUX_MIB_LISTENOVERFLOWS);
gotodrop;
}
 req=inet_reqsk_alloc(&tcp_request_sock_ops);
if(!req)
gotodrop;
 ...
}
机器内存是够用的,只能是 accept backlog 满掉了,
但是问题在于我们是给每一个 virtual ip 配置一个单独的 server { listen vip; } 的, 怎么会在 backlog 满的时候影响到其他业务的 virtual ip 呢?
我们再回到我们的 Nginx 的配置文件来:
http{
server{
listen 1.1.1.1:80;
location /{
return200"10.232.6.3:80";
}
}
 server{
listen 1.1.1.2:80;
location /{
return200"10.232.6.3:80";
}
}
 server{
listen 1.1.1.3:80;
location /{
return200"10.232.6.3:80";
}
}
     ... server{
listen 80;
location /{
return200"0.0.0.0:80";
}
}
}
深入了解 Nginx 的同学看到这里或许也就了然了。
但是我们拥有非常多的 virtual ip server 在配置文件中,一开始也并没有注意到最后一条 listen 80 的配置(该配置用于 nginx健康检查 和 状态统计)。
原因定位:Nginx 处理 bind listen 的时候会对监听的所有 ip:port 做一次规整合并,也就是由于最后一条 listen 80 导致 Nginx 在 listen 的时候只 bind 了一个 0.0.0.0:80 端口, 之后请求进入 Nginx 的时候会通过 ip 再来查找其对应的 virtual server。这也就导致了我们前面看到的结果,当有瞬时的大流量进来时引起 accept backlog 被占满,从而也影响了其他 virtual ip 的服务。
我们也可以在 Nginx 源码里看到这点:
void
ngx_http_init_connection(ngx_connection_t*c)
{
 ...
 port=c->listening->servers;
 if(port->naddrs>1){
 /*
         * there are several addresses on this port and one of them
         * is an "*:port" wildcard so getsockname() in ngx_http_server_addr()
         * is required to determine a server address
         */
 if(ngx_connection_local_sockaddr(c,NULL,0)!=NGX_OK){
ngx_http_close_connection(c);
return;
}
 switch(c->local_sockaddr->sa_family){
 #if(NGX_HAVE_INET6)
caseAF_INET6:
sin6=(structsockaddr_in6*)c->local_sockaddr;
 addr6=port->addrs;
 /* the last address is "*" */
 for(i=0;i<port->naddrs-1;i++){
if(ngx_memcmp(&addr6[i].addr6,&sin6->sin6_addr,16)==0){
break;
}
}
 hc->addr_conf=&addr6[i].conf;
 break;
#endif
 default:/* AF_INET */
sin=(structsockaddr_in*)c->local_sockaddr;
 addr=port->addrs;
 /* the last address is "*" */
 for(i=0;i<port->naddrs-1;i++){
if(addr[i].addr==sin->sin_addr.s_addr){
break;
}
}
 hc->addr_conf=&addr[i].conf;
 break;
}
 }else{
 switch(c->local_sockaddr->sa_family){
 #if(NGX_HAVE_INET6)
caseAF_INET6:
addr6=port->addrs;
hc->addr_conf=&addr6[0].conf;
break;
#endif
 default:/* AF_INET */
addr=port->addrs;
hc->addr_conf=&addr[0].conf;
break;
}
}
 /* the default server configuration for the address:port */
hc->conf_ctx=hc->addr_conf->default_server->ctx;
 ...
}
这里是在建立连接结构体时去查找所属 server,可以清晰的看到针对一个 listening 会有多个 server,也就是说这些 server 公用了一个 listen socket,以及 backlog。
问题解决
原因定位了,解决可以有多种方法;
我们采取的措施是把 listen 80 这条配置加上本地内网IP  listen 172.168.1.1:80,这样Nginx 会对每个 virtual ip 进行一次 bind 和 listen,从而做到了实例间的隔离,各个 virtual ip 之间不会互相影响;
也就不再出现当有一个 virtual ip 瞬时流量过大时导致整个服务看起来像是 hung 住的问题。