Golang 後臺守護程式佇列處理方式小結
在寫一個 Golang Server
的時候,比如 http 介面,最簡單的就是使用 net/http 包,每個請求就會起一個 goroutine 來進行操作。很方便,但是,當併發量大的時候,就會起了成千上萬的 goroutine,當 goroutine 的量達到一個很大的數量,服務效能也就出現了瓶頸。
我們可以手動構建簡單的 goroutine 池,再借助 channel 做佇列,後臺起守護 goroutine,來處理 channel 中的資料。模型大概是這樣的:
var myChan = make(chan int, 1000) // 全域性channel佇列
for i := 0; i < poolSize; i++{
go initWorker()
}
func ProcessWorker() {
for {
select {
case value := <-myChan:
fmt.Println(value)
// ... do something with value
case done := <-done: // 關閉機制
break
default: // 這裡不用超時處理,就是希望goroutine一直在後臺執行
}
}
}
func indexHandler() {
//...
myChan <- value
}
這樣我們就建立了 poolSize 數量的 goroutine,在不斷的從 myChan 中獲取資料,Golang 的排程會幫我們做這一切。
很容易就可以發現,這裡的一個瓶頸就是 myChan 的 size 大小。當 myChan 很快就滿了,後續的請求就會阻塞了。在我的專案的嘗試中,我將 size 設為 10000
, 進行 ab 測試,發現測試結果比用普通的多 goroutine 處理方式要好。我加到了 10w,效能反而下降了。所以,myChan Size 的代銷不是越多越好,而是根據你實際情況來測試出一個合適的值。
在我的另一個專案中,是對檔案的格式轉換,對核心程式碼的執行,希望不要支援高併發,在高併發下核心的命令程式碼會執行報錯。所以,我只建立了一個守護 goroutine,並且 make myChan 的 Size 為 200.這樣,用 ab 測試時,能支援 200 併發,並且幾乎沒有執行失敗的報錯。
我似乎喜歡上了使用上述的方式來處理請求,Golang select 和 channel 的設計讓我可以很愉悅的使用上面的方式,並且達到預期的效果。
我並沒有發現程式執行的異常,直到今天,我忽然發現服務在沒有請求的時候,CPU 佔用率達到了 100%(4 核心的機器)。我十分詫異,第一時間想到了可能是 ProcessWorker
的問題。
這裡涉及到了 select 排程的方法:
當某個 case 的 channel 資料可以取到了就執行它;
當多個 case 同時取到資料了,會隨機執行一個;
當沒有 case 取到資料,都阻塞時,會執行 default。
當服務沒有接收請求的時候,ProcessWorker
方法中會執行的應該是 default,這個沒錯。而在外層我用了 for 的死迴圈,以保證 goroutine 一直執行。這個問題就來了,ProcessWorker
在沒有請求的時候會一直執行 default,而不會阻塞在 case。如果你在 default 列印日誌
func ProcessWorker() {
for {
select {
case value := <-myChan:
fmt.Println(value)
// ... do something with value
case done := <-done: // 關閉機制
break
default: // 這裡不用超時處理,就是希望goroutine一直在後臺執行
log.Println("default")
}
}
}
你會發現後臺在不斷的輸出日誌。
一個簡單的程式碼例子
// example.go
package main
import (
"fmt"
"log"
"net/http"
)
var c = make(chan int, 100)
func main() {
go worker()
addr := ":9090"
http.HandleFunc("/", index)
log.Fatal(http.ListenAndServe(addr, nil))
}
func worker() {
for {
select {
case d := <-c:
fmt.Println(d)
default:
}
}
}
func index(w http.ResponseWriter, r *http.Request) {
c <- 1
w.Write([]byte("OK"))
}
當你 go run example.go
的時候,開啟 htop 或 top,檢視這個服務,在我的 Ubuntu 14.04 環境下 (Mac 下也是),CPU 佔用到了 100%(用了一個核心執行緒).
當把 default 註釋了變成
func worker() {
for {
select {
case d := <-c:
fmt.Println(d)
// default:
}
}
}
CPU 佔用率就恢復了正常。這時候,worker()
是阻塞在了 case d:= <-c:
上,這種阻塞並不會佔用 CPU 的排程處理,CPU 會閒置或去處理別的任務。直到 channel 中有資料了,會喚醒該 channel 的資料結構中對應的 goroutine,設定為 runnable 的狀態,該 goroutine 的排程才會繼續進行。
解決方法: 將default:
刪除即可
default 並非是罪惡之人
只是在上述的使用情況下,default 變成了不必要的了。 當你的使用方式並非如此時,比如下面的方法:
func TryRun() {
select {
case a := <-c:
//...
// case t := time.After(10 * time.Second):
// return
default:
return
}
}
外層沒有 for 死迴圈,當 <-c
阻塞時,default 會裡執行 return,而起到立即釋放 goroutine 的效果,或者你可以加一定的超時機制。要不然,在大量使用 goroutine 的時候,極有可能造成 goroutine 洩露或僵死。
Golang 設計了便捷的 goroutine 的建立方式: go 一下
,方便的進行併發處理。並且設計了使用select
方法來進行排程處理。讓併發程式設計變得簡單。
不過,我們還是需要對其原理和底層結構有所掌握,這樣才能寫出合適的程式碼。
關於 channel 的參考:
https://about.sourcegraph.com/go/understanding-channels-kavya-joshi/<span class="embed-responsive embed-responsive-16by9"><iframe class="embed-responsive-item" src="//www.youtube.com/embed/KBZlN0izeiY" allowfullscreen></iframe></span>
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- laravel佇列之Supervisor守護程式Laravel佇列
- laravel佇列之Supervisor守護程式(centos篇)Laravel佇列CentOS
- Golang 程式守護 SupervisorGolang
- Linux 下後臺執行和按照守護程式方式後臺執行的坑Linux
- 寶塔中使用Supervisor給laravel佇列新增程式守護Laravel佇列
- python併發程式設計之程式1(守護程式,程式鎖,程式佇列)Python程式設計佇列
- 維護你的請求佇列,處理token異常佇列
- 檔案處理平臺後端Golang外包專案後端Golang
- 處理線上RabbitMQ佇列阻塞MQ佇列
- 後臺處理
- 守護程式
- 常見佇列等待事件處理思路佇列事件
- mount程式在systemctl守護的情況下,mount dir程式被oom後重新啟動失敗的處理方法OOM
- Node 程式守護
- 程式守護 supervisor
- Linux 守護程式Linux
- Kafka聯結器深度解讀之錯誤處理和死信佇列Kafka佇列
- 【資料結構】佇列(順序佇列、鏈佇列)的JAVA程式碼實現資料結構佇列Java
- Hadoop小檔案的處理方式Hadoop
- 守護程式那些事
- php訂單延時處理-延時佇列PHP佇列
- disruptor佇列SleepingWaitStrategy與YieldingWaitStrategy處理的區別佇列AI
- 故障處理】佇列等待之enq: US - contention案例佇列ENQ
- 【故障處理】佇列等待之enq: US - contention案例佇列ENQ
- 小程式程式碼打包處理
- 當 Vue 處理陣列與處理純物件的方式一樣Vue陣列物件
- Python編寫守護程式程式Python
- Spring Cloud Stream消費失敗後的處理策略(三):使用DLQ佇列(RabbitMQ)SpringCloud佇列MQ
- PHP 編寫守護程式PHP
- PHP 實現守護程式PHP
- Linux守護程式及SystemdLinux
- [Golang]呼叫外部shell程式處理檔案Golang
- 實用!Swoole + Redis 佇列 訂單處理系統Redis佇列
- golang處理signalGolang
- netcore下RabbitMQ佇列、死信佇列、延時佇列及小應用NetCoreMQ佇列
- Golang 實現 RabbitMQ 的死信佇列GolangMQ佇列
- Java版-資料結構-佇列(陣列佇列)Java資料結構佇列陣列
- SAP CRM點了附件的超連結後報錯的處理方式