高併發服務器的設計--架構與瓶頸的設計

做架構設計,難免有時候被人問及系統的瓶頸在哪,那首先來了解下什麼是瓶頸?
打個形象的比方,人的嘴巴可以吞下一整個麪包,但是卻咽不下去,因爲食管不給力,它比較細,所以嘴巴能吞下的食物大小要受到食管的粗細限制。
城市內部每天會產生幾十萬件跨城快遞,可是跨城的交通不給力,只允許走小型卡車,一卡車一次就能裝幾千件,一天下來也不一定能投送的完。
人在一定時間內能嚥下多少食物,貨運公司在一天運送多少貨物,物理上叫做吞吐量,系統整體的吞吐量等於最小區域的吞吐量。
下面這張圖能夠反映:




土黃色管子的流量要受到紅色部分的制約。
服務器上也是這樣,好一點的設計框架結合物理高配可以處理高達幾十萬的併發,像土黃色的管子,可是偏偏有一些模塊像圖中紅色的管子那樣,一秒中只能同時處理幾百次,這樣就嚴重拖慢了服務器的性能,成了瓶頸。
現實開發中有時可能會要加上數據庫模塊,如mysql,雖然mysql號稱每秒處理幾十萬的查詢根本沒問題,但那只是運算能力。
服務器連mysql 是要通過tcp網絡的,有連接就需要時間,再加上數據量如果大點,自然就成了瓶頸。
相似的情況,一些特殊的業務,比如加解密服務,密鑰和隨機數的產生依賴加密機,中間件的性能就是我們圖中的紅管子。
有些開發還會涉及到跨網服務器查詢,比如騰訊電商會調用QQ服務器的登錄網關,跨網查詢的速度肯定沒有本地執行的快。
系統架構的設計是爭對業務的,業務裏如果存在這些紅管子,就必須要有相應的解決辦法。
不同人的處理方法不同,據我經驗,可以將瓶頸子分成兩類:
1.阻塞串行處理
2.異步並行處理

mysql,中間件的處理屬於第一類,異步網關查詢屬於第二類。
對於第一類,一種通用的解決方法是增加處理進程,其實是橫向擴容的思想,打個比方,一個進程的併發是600,10個進程就可以達到6000了,如何才能將請求均勻地分配到這10個進程是關鍵。
多個進程同時監聽一個端口,負載均衡的方法很多,這裏介紹nginx的做法,直接上代碼:

//接收握手後連接
void ngx_event_accept(ngx_event_t *ev)
{
...
ngx_accept_disabled = ngx_cycle->connection_n / 8
                              - ngx_cycle->free_connection_n;
    ...
}

//事件模型處理函數
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
...
if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

            if (ngx_accept_mutex_held) {
                flags |= NGX_POST_EVENTS;

            } else {
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    ...
}


//取得端口監聽的鎖
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
...
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }
    ...
}

//啓動端口監聽
static ngx_int_t ngx_enable_accept_events(ngx_cycle_t *cycle)
{
    ngx_uint_t         i;
    ngx_listening_t   *ls;
    ngx_connection_t  *c;

    ls = cycle->listening.elts;
    for (i = 0; i < cycle->listening.nelts; i++) {

        c = ls[i].connection;

        if (c->read->active) {
            continue;
        }

        if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {

            if (ngx_add_conn(c) == NGX_ERROR) {
                return NGX_ERROR;
            }

        } else {
            if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {
                return NGX_ERROR;
            }
        }
    }

    return NGX_OK;
}

從上面的代碼可以看出,nginx用一個全局變量ngx_accept_disabled 來控制單個進程的負載,當負載達到一定值的時候,不再接受新的負載。

對於第二類情況,解決的方法就像名字一樣,異步並行解決。
拿跨網查詢爲例:
創建一個查詢的請求,將請求放進事件模型中,等待服務端的返回,異步處理。
熟悉nginx的就知道nginx的upstream反向代理,這個解決方案跟反向代理很像,只不過在與上游服務器交互的前後分別還有其他的業務處理,而且可能還會有多次交互。
相應的流水圖是這樣的:

當客戶端請求量大時,事件模型的容量會成爲瓶頸,這樣仍然需要橫向擴容的方式來解決,增加處理進程。
這兩種情況的處理方法大致如此,有時候特殊問題特殊對待,比哪數據庫的瓶頸可以藉助緩存解決,有些高配服務器的內存128G,甚至幾臺高配服務器只爲一個業務,這樣的情況下,不喫點內存難免對不起老闆的money.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章