使用Go語言每分鐘處理一百萬個請求
該文是Malwarebytes首席架構師介紹其希望如何使用Go語言實現每分鐘處理100萬個請求。
其主要職責是加強系統基礎架構,以支援每天數百萬人使用,其本人已經在反病毒和反惡意軟體領域工作12年,他深深知道這些系統的複雜原因最終是由於每天處理大量資料。
其過去九年後端經歷大部分使用Ruby On Rails,雖然他相信RoR是一個令人驚奇的環境,但是當你以Ruby方式開始思考和設計系統一段時間以後,在你需要考慮多執行緒 並行和快速執行以及小記憶體消耗的情況下,你會忘記原來系統架構的所謂有效與簡單。多年來,他也是C/C++, Delphi 和C# ,他開始認識到使用正確工具做事能減少不必要的複雜性。
作為一個首席架構師,他並不執著於語言和框架,而這兩點總是網上爭論的焦點,他相信效率 產品性和程式碼維護性大部分依賴於你的架構解決方案有多簡單。
起初他們試圖並行化這種接受請求然後轉發上傳的處理工作,將這些處理過程放入一個簡單的goroutine(Go語言並行協程)
在負載穩定情況下,這種方案能夠大部分工作得很好,但是在大規模訪問下,卻工作得不怎麼樣。當每分鐘達到100萬POST請求時,這段程式碼崩潰了。
他們引入了buffered channel緩衝通道,將一些工作放入佇列。
在佇列另外一端,也就是出列工作中再處理上傳到S3的工作,如下:
但是帶來問題是這個佇列迅速達到其上限,堵塞住了請求處理器,這樣就不能向佇列中繼續放入。這可以透過加入一個倒數計數來避免,但是系統的延遲會恆速上升(系統變慢)。
導致這個原因是因為上傳到S3這個工作非常耗時,因為是透過網路連線,因此引入Java和C概念中的執行緒池來處理上傳工作,這樣使用Channel實現Queue+Worker的概念.
在佇列放入的一端程式碼改為如下:
透過StartDispatcher建立worker池,然後開始監聽JobQueue中的job:
最後終於達到了每分鐘處理100萬個請求,更多測試結果見原文:
其主要職責是加強系統基礎架構,以支援每天數百萬人使用,其本人已經在反病毒和反惡意軟體領域工作12年,他深深知道這些系統的複雜原因最終是由於每天處理大量資料。
其過去九年後端經歷大部分使用Ruby On Rails,雖然他相信RoR是一個令人驚奇的環境,但是當你以Ruby方式開始思考和設計系統一段時間以後,在你需要考慮多執行緒 並行和快速執行以及小記憶體消耗的情況下,你會忘記原來系統架構的所謂有效與簡單。多年來,他也是C/C++, Delphi 和C# ,他開始認識到使用正確工具做事能減少不必要的複雜性。
作為一個首席架構師,他並不執著於語言和框架,而這兩點總是網上爭論的焦點,他相信效率 產品性和程式碼維護性大部分依賴於你的架構解決方案有多簡單。
問題
目標是能夠處理來自數百萬端點的POST請求,Web處理器會接受到一個JSON文件,其中包含一個許多資料集合,這些需要寫入到Amazon S3,然後Map-reduce系統稍後可以操作分析這些資料。
傳統地闖進一個worker-tier工作層架構,如:
1.Sidekiq
2.Resque
3.DelayedJob
4. Elasticbeanstalk Worker Tier
5,RabbitMQ
然後設定兩個不同叢集,一個是用於Web前端,另外一個是用於worker,這樣我們可以擴充套件很多後端伺服器數量以應付大量增長的請求。
當他們發現這是高流量系統時,意識到應該使用Go語言完成,之前其本人已經使用Go語言兩年,開發過一些系統,但是沒有人確證Go語言能夠應付如此大的負載。
他們定義了資料結構,這是透過POST提交獲得的請求,然後透過一個方法上傳到S3。
type PayloadCollection struct { WindowsVersion string `json:"version"` Token string `json:"token"` Payloads []Payload `json:"data"` } type Payload struct { // [redacted] } func (p *Payload) UploadToS3() error { // the storageFolder method ensures that there are no name collision in // case we get same timestamp in the key name storage_path := fmt.Sprintf("%v/%v", p.storageFolder, time.Now().UnixNano()) bucket := S3Bucket b := new(bytes.Buffer) encodeErr := json.NewEncoder(b).Encode(payload) if encodeErr != nil { return encodeErr } // Everything we post to the S3 bucket should be marked 'private' var acl = s3.Private var contentType = "application/octet-stream" return bucket.PutReader(storage_path, b, int64(b.Len()), contentType, acl, s3.Options{}) } <p class="indent"> |
起初他們試圖並行化這種接受請求然後轉發上傳的處理工作,將這些處理過程放入一個簡單的goroutine(Go語言並行協程)
func payloadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) return } // Read the body into a string for json decoding var content = &PayloadCollection{} err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(&content) if err != nil { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusBadRequest) return } // Go through each payload and queue items individually to be posted to S3 for _, payload := range content.Payloads { go payload.UploadToS3() // <----- DON'T DO THIS } w.WriteHeader(http.StatusOK) } <p class="indent"> |
在負載穩定情況下,這種方案能夠大部分工作得很好,但是在大規模訪問下,卻工作得不怎麼樣。當每分鐘達到100萬POST請求時,這段程式碼崩潰了。
他們引入了buffered channel緩衝通道,將一些工作放入佇列。
var Queue chan Payload func init() { Queue = make(chan Payload, MAX_QUEUE) } func payloadHandler(w http.ResponseWriter, r *http.Request) { ... // Go through each payload and queue items individually to be posted to S3 for _, payload := range content.Payloads { Queue <- payload } ... } <p class="indent"> |
在佇列另外一端,也就是出列工作中再處理上傳到S3的工作,如下:
func StartProcessor() { for { select { case job := <-Queue: job.payload.UploadToS3() // <-- STILL NOT GOOD } } } <p class="indent"> |
但是帶來問題是這個佇列迅速達到其上限,堵塞住了請求處理器,這樣就不能向佇列中繼續放入。這可以透過加入一個倒數計數來避免,但是系統的延遲會恆速上升(系統變慢)。
導致這個原因是因為上傳到S3這個工作非常耗時,因為是透過網路連線,因此引入Java和C概念中的執行緒池來處理上傳工作,這樣使用Channel實現Queue+Worker的概念.
var ( MaxWorker = os.Getenv("MAX_WORKERS") MaxQueue = os.Getenv("MAX_QUEUE") ) // Job represents the job to be run type Job struct { Payload Payload } // A buffered channel that we can send work requests on. var JobQueue chan Job // A pool of workers that are instantianted to perform the work var WorkerPool chan chan Job // Worker represents the worker that executes the job type Worker struct { ID int JobChannel chan Job WorkerPool chan chan Job QuitChan chan bool } func NewWorker(id int, workerPool chan chan Job) Worker { worker := Worker{ ID: id, Work: make(chan Job), WorkerPool: workerPool, QuitChan: make(chan bool)} return worker } // Start method starts the run loop for the worker, listening for a quit channel in // case we need to stop it func (w Worker) Start() { go func() { for { // register the current worker into the worker queue. w.WorkerPool <- w.JobChannel select { case job := <-w.JobChannel: // we have received a work request. if err := job.Payload.UploadToS3(); err != nil { log.Errorf("Error uploading to S3: %s", err.Error()) } case <-w.QuitChan: // we have received a signal to stop return } } }() } // Stop signals the worker to stop listening for work requests. func (w Worker) Stop() { go func() { w.QuitChan <- true }() } <p class="indent"> |
在佇列放入的一端程式碼改為如下:
func payloadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) return } // Read the body into a string for json decoding var content = &PayloadCollection{} err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(&content) if err != nil { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusBadRequest) return } // Go through each payload and queue items individually to be posted to S3 for _, payload := range content.Payloads { // let's create a job with the payload work := Job{Payload: payload} // Push the work onto the queue. JobQueue <- work } w.WriteHeader(http.StatusOK) } <p class="indent"> |
透過StartDispatcher建立worker池,然後開始監聽JobQueue中的job:
func StartDispatcher(maxWorkers int) { WorkerPool = make(chan chan Job, maxWorkers) // starting n number of workers for i := 0; i < maxWorkers; i++ { worker := NewWorker(i+1, WorkerPool) worker.Start() } go func() { for { select { case job := <-JobQueue: // a job request has been received go func(jobChannel chan Job) { // try to obtain a worker that is available. // this will block until a worker is idle worker := <-WorkerPool // dispatch the job to the worker, dequeuing from // the jobChannel worker <- jobChannel }(job) } } }() } <p class="indent"> |
最後終於達到了每分鐘處理100萬個請求,更多測試結果見原文:
相關文章
- 如何用Golang處理每分鐘100萬個請求Golang
- 用Golang處理每分鐘100萬份請求Golang
- Go Web如何處理Web請求?GoWeb
- Go 語言異常處理Go
- Go語言HTTP請求流式寫入bodyGoHTTP
- Go 語言處理 yaml 檔案GoYAML
- nginx 如何處理一個請求Nginx
- go語言請求http介面示例 並解析jsonGoHTTPJSON
- Go 語言操作 MySQL 之 預處理GoMySql
- Go語言基礎-錯誤處理Go
- Go語言錯誤處理機制Go
- 使用cors完成跨域請求處理CORS跨域
- go語言處理TCP拆包/粘包GoTCP
- Node.js如何處理多個請求?Node.js
- 請求資料處理
- springmvc處理ajax請求SpringMVC
- [ gev ] Go 語言優雅處理 TCP “粘包”GoTCP
- Go語言處理—Day11—反射機制Go反射
- Spring MVC 處理一個請求的流程分析SpringMVC
- SpringBoot使用Axios傳送請求,引數處理Spring BootiOS
- 在 .NET 中使用 Flurl 高效處理Http請求HTTP
- DeferredResult——非同步請求處理非同步
- Go語言的 序列處理 和 並行處理 有什麼區別 ?Go並行
- 15分鐘學會Go語言Go
- go語言使用Go
- Angular 記錄 - Rxjs 完整處理一個 Http 請求AngularJSHTTP
- 600 字每分鐘
- 如何使用策略模式處理多種型別請求模式型別
- 使用go語言對csv檔案進行解析處理,匯入匯出。Go
- yai 請求預處理指令碼AI指令碼
- Netty(二):如何處理io請求?Netty
- Laravel請求處理管道理解Laravel
- 處理 HTTP 請求的註解HTTP
- springmvc原始碼 ---DispatcherServlet 處理請求SpringMVC原始碼Servlet
- 全球每分鐘約扔掉300萬個口罩
- [Go語言寫介面]一、使用xcgui完成go語言第一個軟體介面GoGUI
- 清華尹成帶你實戰GO案例(48)Go 請求處理頻率控制Go
- 從錯誤處理看 Rust 的語言和 Go 語言的設計RustGo
- 處理請求(AFURLRequestSerialization)和響應(AFURLResponseSerialization)