這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
首先給出 原文連結.
筆者覺得此文寫的通俗易懂,言簡意賅,於是打算翻譯出來供go伺服器新手學習參考,好了廢話不說,開始本文
在Malwarebytes的工作讓我經曆的驚人的成長,一年前,在我加入這家公司之前,我在矽谷的主要工作內容就是定製解決方案,以應對快速增長的公司每天數百萬的使用頻率。我已經在不同的公司從事反病毒和反惡意程式碼軟體行業工作了12年,我知道這些系統的複雜程度取主要決於我們每天要處理多大量資料。
有趣的是,在過去的9年當中,我從事的所有網站開發都是在Ruby on Rails的方案下實施的。不要誤會我的意思,我非常熱愛 Ruby on Rails,我認為這是一個令人驚奇的開發方案。但是在一段時間以後,當你開始用Ruby on Rails的思路去思考和設計系統,你就會忘記那些可以利用的多線程、並行,快速執行和小的記憶體開銷的解決方案,而這些將會使你開發的軟體高效和簡潔。多年來,我一直是一個C / C++、Delphi和C #開發人員,我開始意識到,當你使用適合的工具時,事情會是多麼的簡潔。
作為一個架構師,我不是很在意那些網站之間的關於語言和架構之間孰優孰劣的紛爭。我相信,效率,生產力和代碼的可維護性主要依賴於如何簡單的構建你的解決方案。
一個難題
當我們在進行一個匿名的測試和分析系統時,我們的目標是能夠處理來自數百萬端點的大量POST請求。web處理常式將接收一個包含眾多資料集合的JSON文檔,它們將被寫入Amazon S3資料庫,以供我們的大資料系統進行後續操作
面對這樣的需求,通常我們會考慮諸如:
Sidekiq
Resque
DelayedJob
Elasticbeanstalk Worker Tier
RabbitMQ
等等架構和方案...
並且,我們會設定2個不同的叢集,一個用於Web前端,另一個用於後台服務,這樣我們就可以通過增加減少後台伺服器的數量來控制我們能夠處理的請求數。但是,從一開始我們的團隊就決定採用Go語言作為開發方案,因為經過討論,我們發現這將會是一個非常龐大的系統。我們已經從事Go語言開發兩年,在工作中設計架構了一些系統,但是還沒有任何一個系統有如此龐大的資料量。
我們首先建立了一些結構體來定義POST的request中內容,以及一個上傳到S3庫的方法。
p1.png
小試牛刀
最初我們採取了一個非常簡單的POST請求處理實現方案,嘗試簡單並發goroutine去處理任務
p2.png
對於中等的負載量,這個方案可以滿足大多數人的需求。但是當資料量增大的時候,它開始顯得不那麼好用了。在第一版投入生產環節中,我們預估了一下request的數量,事實上我們完全低估了這個龐大的資料量。
上面的方案在有很多弊端,首先我們無法控制開啟的goroutine數量,然後,當請求打到每分鐘一百萬次的數量級,很快這段代碼就崩潰了。
再次嘗試
我們需要一個新方案,在一開始的討論中,我們明確了幾點,首先要保證request handler 的生命週期足夠短,其次要做到在後台進行非同步並發處理。顯然如果採用Ruby on Rails的方案,這些都是必要的事情,不然就會阻塞整個網路。那麼,我們將不得不採取一些常見的方案來解決這個事情比如使用 Resque, Sidekiq, SQS等等。
所以,第二次迭代的任務就是建立一個用於緩衝列隊的通道,用來緩衝請求,並將它們逐一存入S3伺服器。因為我們可以控制隊列通道內的最大容量,並且我們有足夠大的記憶體來緩衝隊列,我們認為這將會一個是極好的方案。
p3.png
然後,為了將任務列隊並依次處理,我們用了如下面這樣的代碼
p4.png
老實的講,我並不知道我們在想些什麼。這將會是一個充滿紅牛的不眠之夜。這個方案並沒有給我們帶來任何改進。我們用一個緩衝列隊代替了有缺陷的並發方案,這僅僅是延遲了問題的產生時間而已。我們的同步處理器每次只能上傳一分資料到S3,而隊列中傳入請求的速度遠大於處理器上傳資料到S3的資料,很快的我們的隊列就達到了極限,從而阻塞了後續的請求添加到隊列。我們僅僅簡單的去迴避問題,這僅僅是開啟了一個系統崩潰死亡的倒計時。
p5.png
更好的方案
當我們使用Go的通道時,我們決定利用一個公用模式來建立一個雙層的通道系統。一個用來隊列任務,而另一個則是控制當前處理隊列任務的線程數量。
這個想法是要採用一個合理的可持續的速率並行的上傳資料到S3伺服器,既不會阻塞伺服器的效能,也不會從S3伺服器擷取上傳失敗的錯誤回調。因此,我們構建一個job/worker模型,這看起來有些像Java, C#,等等,而我們則是考慮通過Golang的方式,利用通道來代替它們,去實現一個處理器線程池。
p6.png
p7.png
我們已經修改了我們的網路請求handler來建立一個包含載荷資料的任務執行個體,我們將它發送到任務隊列通道中去,供處理線程們去處理。
p9.png
當我們的網路伺服器在初始化的過程中,我們建立了一個名叫Run()的調度器來建立worker線程池,並且開始監聽發送到任務隊列中的任務。
p11.png
下面是我們調度器的代碼實現
p12.png
值得注意的是,我們提供了最大處理線程的並發數量,用來執行個體化任務處理線程並且將他們添加到我們的任務處理線程池當中。我們使用亞馬遜的Elasticbeanstalk服務並且採用了docker化的Go 運行環境,而且我們總是嘗試遵循 12要素的方法論(譯者註:應該是一種廣泛認可的架構設計思路,雖然我沒聽說過)來配置我們在生產環境中的系統,我們通過環境變數來讀取這些值。這樣我們就可以控制處理線程數量和任務隊列通道的所能承載的最大容量,因此,我們可以快速的改變這些配置而不用重新部署伺服器叢集
p15.png
最直觀的結果
在我們部署完它之後,我們立即發現所有的延遲率都降到了微不足道的數量,我們處理請求的能力激增。
p17.png
在經曆了幾分鐘彈性的負載平衡熱身之後,我們看到,我們的ElasticBeanstalk應用每分鐘接近響應了一百萬的請求。
在我們部署了新代碼之後,伺服器的數量大幅下降,從100台伺服器降到20台伺服器。
p19.png
結論
在我的故事中,極簡主義總是獲勝的一方。我們可以設計一個複雜的系統,具有許多隊列,非同步幕後處理,複雜的調度,但是我們決定利用Elasticbeanstalk的自動縮放的高效的簡潔的能力去實現Golang提供給我們的並發效果。
你並不是每天都能夠擁有一個4伺服器的叢集,這可能比我目前的MacBook Pro功能強大得多
,來給亞馬遜的S3伺服器每分鐘寫入100萬次
總會有一個合適的工具來解決問題,有時,當你的Ruby Rails系統需要一個功能強大的Web處理常式時,可以稍微考慮一下Ruby生態系統之外的更簡單、更強大的替代解決方案。