做者:林冠宏 / 指尖下的幽靈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
優化
調試結果
與源碼分析
顯示,正確的形式是下面這樣的:爲每個鏈接的用戶啓動了一個長鏈接,serve
方法內部有個超時的設置是c.rwc.SetReadDeadline(time.Time{})
,這樣子的狀況,若是內部不出錯,當前的鏈接斷開的條件是客戶端
本身斷開,或nat
超時。spa
這個鏈接創建後,以ip
爲單位,當前的客戶端
,此時它的全部http請求
,例如get
,post
,它們都會在這個啓動的gorutine
內進行分發
與被處理
。
也就是說,同一個ip
,多個不一樣的
請求,這裏不會觸發另外一個 accept
,不會再去啓動一個go c.serve(ctx)
若是有 100萬
個 accept
,就證實有100萬
個鏈接,100萬
個ip
與當前server
鏈接。便是咱們說的百萬鏈接
百萬鏈接
不是百萬請求
每個鏈接,它能夠進行多個http請求
,它的請求都在當前啓動這個鏈接的gorutine
裏面進行。
c.serve(...)
源碼中的for 死循環
就是負責讀取每一個請求再分發
for {
w, err := c.readRequest(ctx) // 讀取一個 http 請求
//...
ServeHTTP(...)
}
複製代碼
100萬
鏈接裏面,有可能併發更多的請求,例如幾百萬請求,一個客戶端
快速調用多個請求api
根據咱們上面的分析,每個新鏈接到來,go 就會啓動一個 gorutine
,在源碼裏面也沒有看到有一個量級的限制,也就是達到多少鏈接就再也不接收。咱們也知道,服務器是有處理瓶頸的。
因此,在這裏插播一個優化點
,就是在server.go
內部作一個鏈接數目的限制。
master-worker
模式自己是啓動多個worker
線程,去併發讀取
有界隊列裏面的任務,並執行。
我自身已經實現了一個go版本
的master-worker
,作過下面的嘗試:
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)
。
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 請求數目不受控暴增
而致使服務器
掛掉。
此外上述第二種
還存在一個:讀,過早關閉問題
,這個留給讀者嘗試解決。