閱讀274 返回首頁    go 阿裏雲 go 技術社區[雲棲]


SLB 7層負載均衡“HUNG”問題追查

作者:吳佳明

 

最近接到博客園的反饋,SLB 7層負載均衡的實例會不定期出現流量突跌的情況,突跌持續10s左右;同時,SLB自身監控也觀察到了相同的現象;針對該問題,我們進行了持續追查,最終定位到是nginx配置的原因;在此,分享一下分析排查過程,希望對大家使用nginx有所幫助。

 

問題描述

  1. SLB 7層負載均衡(nginx)流量會出現不定期的突跌,每次突跌持續10s左右;同時,每次突跌必然發生在 12點 或者  0點;
  2. 查看SLB實例流量圖,發現 部分實例 在12點 和 0點 流量突增幾十倍;

兩個時間點吻合,初步推斷是突增流量導致nginx異常,從而導致流量下跌。

 

分析過程

  1. 觀察每台nginx流量,發現當前運行負載比較低,遠遠小於閾值;CPU/MEM/NET各項指標都不高;
  2. 通過抓包發現大量的 syn 包被丟棄重傳;

從上述現象,懷疑是網絡問題,但從協議棧/網卡/交換機多個層麵排查,沒有發現網絡異常;

  1. 在 Nginx 的機器上 curl 服務的統計接口時也出現了請求被 hang 住的情況;- 突破點

抓包發現即使是本機發起的請求也會出現 syn 包丟棄重傳,從而基本可以確定不是網絡的問題,而是我們ngnix有問題。

查看linux協議棧源碼,引起 syn 包被丟棄的原因可能有以下兩點:

1. Accept backlog (接收隊列)滿了
2. 內存分配不出來了

內核代碼如下:

int tcp_v4_conn_request(struct sock *sk, struct sk_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);
        goto drop;
    }
 
    req = inet_reqsk_alloc(&tcp_request_sock_ops);
    if (!req)
        goto drop;
 
...
}

機器內存是夠用的,隻能是 accept backlog 滿掉了,

但是問題在於我們是給每一個 virtual ip 配置一個單獨的 server { listen vip; } 的, 怎麼會在 backlog 滿的時候影響到其他業務的 virtual ip 呢?

我們再回到我們的 Nginx 的配置文件來:

http {
    server {
        listen 1.1.1.1:80;
        location / {
            return 200 "1.1.1.1:80";
        }
    }
 
    server {
        listen 1.1.1.2:80;
        location / {
            return 200 "1.1.1.2:80";
        }
    }
 
    server {
        listen 1.1.1.3:80;
        location / {
            return 200 "1.1.1.3:80";
        }
    }
 
    ...
 
    server {
        listen 80;
        location / {
            return 200 "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)
        case AF_INET6:
            sin6 = (struct sockaddr_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 = (struct sockaddr_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)
        case AF_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 住的問題。

最後更新:2017-04-03 05:39:36

  上一篇:go morphologyEx,dilate兩種膨脹操作對比
  下一篇:go while(~scanf(&quot;%d%d&quot;,&amp;n,&amp;m)) {...}