Go 自帶的 http/server.go 的鏈接解析 與 如何結合 master-worker 併發模式,提升單機併發能力

做者:林冠宏 / 指尖下的幽靈git

掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8github

博客:http://www.cnblogs.com/linguanh/golang

GitHub : https://github.com/af913337456/api

騰訊雲專欄: https://cloud.tencent.com/developer/user/1148436/activities服務器


關於 server.go 源碼的解析能夠去搜下,已經有不少且還不錯的文章。併發

正文:

從咱們啓動http.ListenAndServe(port,router)開始,server.go 內部最終在一個for 循環中的 accept 方法中不停地等待客戶端的鏈接到來。源碼分析

每接收到一個accept 就啓動一個 gorutine 去處理當前ip的鏈接。也就是源碼裏的go c.serve(ctx)。這一個步驟在 c.serve(ctx) 它並非簡單的形式:post

請求-->處理請求-->返回結果-->斷開這個鏈接-->結束當前的 gorutine優化

根據個人調試結果源碼分析顯示,正確的形式是下面這樣的:

  1. 爲每個鏈接的用戶啓動了一個長鏈接,serve 方法內部有個超時的設置是c.rwc.SetReadDeadline(time.Time{}),這樣子的狀況,若是內部不出錯,當前的鏈接斷開的條件是客戶端本身斷開,或nat超時。spa

  2. 這個鏈接創建後,以ip爲單位,當前的客戶端,此時它的全部http請求,例如getpost,它們都會在這個啓動的gorutine 內進行分發被處理

  3. 也就是說,同一個ip,多個不一樣的請求,這裏不會觸發另外一個 accept,不會再去啓動一個go c.serve(ctx)

上述咱們得出結論:

  1. 若是有 100萬accept,就證實有100萬個鏈接,100萬ip與當前server鏈接。便是咱們說的百萬鏈接

  2. 百萬鏈接 不是百萬請求

  3. 每個鏈接,它能夠進行多個http請求,它的請求都在當前啓動這個鏈接的gorutine裏面進行。

  4. c.serve(...) 源碼中的for 死循環就是負責讀取每一個請求再分發

for {
    w, err := c.readRequest(ctx) // 讀取一個 http 請求
    //...
    ServeHTTP(...)
}
複製代碼
  1. 咱們的100萬 鏈接裏面,有可能併發更多的請求,例如幾百萬請求,一個客戶端快速調用多個請求api

圖解總結

結合 master-worker 併發模式

根據咱們上面的分析,每個新鏈接到來,go 就會啓動一個 gorutine,在源碼裏面也沒有看到有一個量級的限制,也就是達到多少鏈接就再也不接收。咱們也知道,服務器是有處理瓶頸的。

因此,在這裏插播一個優化點,就是在server.go 內部作一個鏈接數目的限制。

master-worker 模式自己是啓動多個worker 線程,去併發讀取有界隊列裏面的任務,並執行。

我自身已經實現了一個go版本master-worker,作過下面的嘗試:

  1. go c.serve(ctx) 處作修改,以下。
if srv.masterWorkerModel {
	// lgh --- way to execute
    PoolMaster.AddJob(
    	masterworker.Job{
    		Tag:" http server ",
    		Handler: func() {
    			c.serve(ctx)
    			fmt.Println("finish job") // 這一句在當前 ip 斷開鏈接後纔會輸出
    		},
    	})
}else{
    go c.serve(ctx)
}

func (m Master) AddJob(job Job)  {
    fmt.Println("add a job ")
    m.JobQueue <- job // jobQueue 是具有緩衝的
}
複製代碼
// worker
func (w Worker) startWork(master *Master)  {
    go func() {
        for {
        	select {
            	case job := <-master.JobQueue:
            		job.doJob(master)
        	}
        }
    }()
}
複製代碼
// job
func (j Job) doJob(master *Master) {
    go func() {
    	fmt.Println(j.Tag+" --- doing job...")
    	j.Handler()
    }()
}
複製代碼

不難理解它的模式。

如今咱們使用生產者--消費者模式進行假設,鏈接的產生生產者<-master.JobQueue消費者,由於每一次消費就是啓動一個處理的gorutine

由於咱們在accept 一個請求到<-master.JobQueue,管道輸出一個的這個過程當中,能夠說是沒有耗時操做的,這個job,它很快就被輸出了管道。也就是說,消費很快,那麼實際的生產環境中,咱們的worker工做協程啓動5~10個就有餘了。

考慮若是出現了消費跟不上的狀況,那麼多出來的job將會被緩衝到channel裏面。這種狀況可能出現的情景是:

短期十萬+級別鏈接的創建,就會致使worker讀取不過來。不過,即便發生了,也是很快就取完的。由於間中的耗時幾乎能夠忽略不計!

也就說,短期大量鏈接的創建,它的瓶頸在隊列的緩衝數。可是即便瓶頸發生了,它又能很快被分發處理掉。因此說:

  • 個人這個第一點的嘗試的意義事實上沒有多大的。只不過是換了一種方式去分發go c.serve(ctx)

  1. 這個是第二種結合方式,把master-worker放置到ServeHTTP的分發階段。例以下面代碼,是常見的http handler寫法,咱們就能夠嵌套進去。
func (x XHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)  {
    //...
    if x.MasterWorker {
    	poolMaster.AddJob(master_worker.Job{
    		Tag:"normal",
    		XContext:xc,
    		Handler: func(context model.XContext) {
    			x.HandleFunc(w,r)
    		},
    	})
    	return
    }
    x.HandleFunc(w,r)
    //...
}
複製代碼

這樣的話,咱們就能控制全部鏈接的併發請求最大數。超出的將會進行排隊,等待被執行,而不會由於短期 http 請求數目不受控暴增 而致使服務器掛掉。

此外上述第二種還存在一個:讀,過早關閉問題,這個留給讀者嘗試解決。

相關文章
相關標籤/搜索