1. 程式人生 > >記一次nginx負載均衡轉發錯誤 “no live upstreams while connecting to upstream ”

記一次nginx負載均衡轉發錯誤 “no live upstreams while connecting to upstream ”

先描述一下環境,前段的負載均衡轉發給nginx,nginx再轉發給後端的應用伺服器。

nginx配置檔案如下:

upstream ads {

        server ap1:8888 max_fails=1 fail_timeout=60s;

        server ap2:8888 max_fails=1 fail_timeout=60s;

}

出現的現象是:

日誌裡面每隔一兩分鐘就會記錄一條類似 *379803415 no live upstreams while connecting to upstream  的日誌,

此外,還有大量的“upstream prematurely closed connection while reading response header from upstream”的日誌。

 

我們先看“no live upstreams”的問題。

看字面意思是nginx發現沒有存活的後端了,但是很奇怪的事情是,這段時間一直訪問都正常,並且用wireshark看到的也是有進來的,也有返回的。

 

現在只能從nginx原始碼的角度來看了。

 

因為是upstream有關的報錯,所以在ngx_http_upstream.c中查詢“no live upstreams”的關鍵字,可以找到如下程式碼(其實,你會發現,如果在nginx全域性程式碼中找的話,也只有這個檔案裡面有這個關鍵字): 

 

技術分享

 

在這裡可以看出,當rc等於NGX_BUSY的時候,就會記錄“no live upstreams”的錯誤。

往上看1328行,可以發現rc的值又是ngx_event_connect_peer這個函式返回的。

 

ngx_event_connect_peer是在event/ngx_event_connect.c中實現的。這個函式中,只有這個地方會返回NGX_BUSY,其他地方都是NGX_OK或者NGX_ERROR或者NGX_AGAIN之類的。

 

 rc = pc->get(pc, pc->data);

    if (rc != NGX_OK) {

        return rc;

    }

 

這裡的pc是指向ngx_peer_connection_t結構體的指標, get是個ngx_event_get_peer_pt的函式指標,具體指向哪裡,一時無從得知。接著翻看ngx_http_upstream.c

在ngx_http_upstream_init_main_conf中看到了,如下程式碼:

 

 

    uscfp = umcf->upstreams.elts;

 

    for (i = 0; i < umcf->upstreams.nelts; i++) {

 

        init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream:

                                            ngx_http_upstream_init_round_robin;

 

        if (init(cf, uscfp[i]) != NGX_OK) {

            return NGX_CONF_ERROR;

        }

    }

 

這裡可以看到,預設的配置為輪詢(事實上負載均衡的各個模組組成了一個連結串列,每次從連結串列到頭開始往後處理,從上面到配置檔案可以看出,nginx不會在輪詢前呼叫其他的模組),並且用ngx_http_upstream_init_round_robin初始化每個upstream。

再看ngx_http_upstream_init_round_robin函式,裡面有如下行:

r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer;

這裡把get指標指向了ngx_http_upstream_get_round_robin_peer

 

在ngx_http_upstream_get_round_robin_peer中,可以看到:

 

    if (peers->single) {

        peer = &peers->peer[0];

 

        if (peer->down) {

            goto failed;

        }

 

    } else {

 

        /* there are several peers */

 

        peer = ngx_http_upstream_get_peer(rrp);

 

        if (peer == NULL) {

            goto failed;

        }

再看看failed的部分:

 

failed:

 

    if (peers->next) {

 

        /* ngx_unlock_mutex(peers->mutex); */

 

        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pc->log, 0, "backup servers");

 

        rrp->peers = peers->next;

 

        n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1))

                / (8 * sizeof(uintptr_t));

 

        for (i = 0; i < n; i++) {

             rrp->tried[i] = 0;

        }

 

        rc = ngx_http_upstream_get_round_robin_peer(pc, rrp);

 

        if (rc != NGX_BUSY) {

            return rc;

        }

 

        /* ngx_lock_mutex(peers->mutex); */

    }

 

    /* all peers failed, mark them as live for quick recovery */

 

    for (i = 0; i < peers->number; i++) {

        peers->peer[i].fails = 0;

    }

 

    /* ngx_unlock_mutex(peers->mutex); */

 

    pc->name = peers->name;

 

    return NGX_BUSY;

 

這裡就真相大白了,如果連線失敗了,就去嘗試連下一個,如果所有的都失敗了,就會進行quick recovery  把每個peer的失敗次數都重置為0,然後再返回一個NGX_BUSY,然後nginx就會列印一條no live upstreams ,最後又回到原始狀態,接著進行轉發了。

 

這就解釋了no live upstreams之後還能正常訪問。

 

重新看配置檔案,如果其中一臺有一次失敗,nginx就會認為它已經死掉,然後就會把以後的流量全都打到另一臺上面,當另外一臺也有一次失敗的時候,就認為兩個都死掉了,然後quick recovery,然後列印一條日誌。

 

這樣帶來的另一個問題是,如果幾臺同時認定一臺後端已經死掉的時候,會造成流量的不均衡,看zabbix監控的截圖也能看出來:

 

技術分享

技術分享

 

初步的解決方法:

upstream ads {

        server ap1:8888 max_fails=1 fail_timeout=60s;

        server ap2:8888 max_fails=1 fail_timeout=60s;

}

改成


upstream ads {

        server ap1:8888 max_fails=5 fail_timeout=60s;

        server ap2:8888 max_fails=5  fail_timeout=60s;

}
 

把max_fails從1改成5,效果很明顯,“no live upstreams”出現的概率變少了很多,但卻沒有完全消失。

另外,日誌裡面還會有大量的“upstream prematurely closed connection while reading response header from upstream”。

這次從原始碼上看,在執行ngx_http_upstream_process_header這個函式的時候,會報這個錯,但具體是網路原因還是其他原因不是很明顯,下面就tcpdump抓一下包。

 

技術分享

其中54是nginx前端的負載均衡的地址,171是nginx地址,32是ap1的地址,另外ap2的地址是201

如截圖所示:

請求由負載均衡發到nginx上,nginx先是迴應ack給負載均衡,然後跟ap1進行三次握手,隨後傳送了一個長度為614的資料包給ap1.然而卻收到了一個ack和fin+ack,從Ack=615可以看出,這兩個包都是針對長度為614的資料包的迴應,後端app直接就把連線給關閉掉了!

 

再然後,nginx迴應給後端的app一個ack和fin+ack,從Ack=2可以看出這是對fin+ack的迴應。

再然後,nginx就向ap2發出了一個syn包,並且也收到了第一臺返回的ack。

第二張圖:

技術分享

 

如圖,可以看出,nginx跟ap2三次握手後,也傳送了一個請求的資料包,同樣被直接關閉連線了。

隨後,nginx就把502返回給了負載均衡。

 

這裡的抓包又一次從側面支援了上面程式碼的分析。