首先給出 原文鏈接.
網(wǎng)上有很多該文章的翻譯版本,但是筆者認(rèn)為機(jī)翻痕跡嚴(yán)重,并不利于新手閱讀理解
筆者覺得此文寫的通俗易懂,言簡意賅,于是打算翻譯出來供go服務(wù)器新手學(xué)習(xí)參考,好了廢話不說,開始正文
在Malwarebytes的工作讓我經(jīng)歷的驚人的成長,一年前,在我加入這家公司之前,我在硅谷的主要工作內(nèi)容就是定制解決方案,以應(yīng)對快速增長的公司每天數(shù)百萬的使用頻率。我已經(jīng)在不同的公司從事反病毒和反惡意軟件行業(yè)工作了12年,我知道這些系統(tǒng)的復(fù)雜程度取主要決于我們每天要處理多大量數(shù)據(jù)。
有趣的是,在過去的9年當(dāng)中,我從事的所有網(wǎng)站開發(fā)都是在Ruby on Rails的方案下實(shí)施的。不要誤會我的意思,我非常熱愛 Ruby on Rails,我認(rèn)為這是一個令人驚奇的開發(fā)方案。但是在一段時間以后,當(dāng)你開始用Ruby on Rails的思路去思考和設(shè)計(jì)系統(tǒng),你就會忘記那些可以利用的多線程、并行,快速執(zhí)行和小的內(nèi)存開銷的解決方案,而這些將會使你開發(fā)的軟件高效和簡潔。多年來,我一直是一個C / C++、Delphi和C #開發(fā)者,我開始意識到,當(dāng)你使用適合的工具時,事情會是多么的簡潔。
作為一個架構(gòu)師,我不是很在意那些網(wǎng)站之間的關(guān)于語言和框架之間孰優(yōu)孰劣的紛爭。我相信,效率,生產(chǎn)力和代碼的可維護(hù)性主要依賴于如何簡單的構(gòu)建你的解決方案。
一個難題
當(dāng)我們在進(jìn)行一個匿名的測試和分析系統(tǒng)時,我們的目標(biāo)是能夠處理來自數(shù)百萬端點(diǎn)的大量POST請求。web處理程序?qū)⒔邮找粋€包含眾多數(shù)據(jù)集合的JSON文檔,它們將被寫入Amazon S3數(shù)據(jù)庫,以供我們的大數(shù)據(jù)系統(tǒng)進(jìn)行后續(xù)操作
面對這樣的需求,通常我們會考慮諸如:
Sidekiq
Resque
DelayedJob
Elasticbeanstalk Worker Tier
RabbitMQ
等等框架和方案...
并且,我們會設(shè)置2個不同的集群,一個用于Web前端,另一個用于后臺服務(wù),這樣我們就可以通過增加減少后臺服務(wù)器的數(shù)量來控制我們能夠處理的請求數(shù)。但是,從一開始我們的團(tuán)隊(duì)就決定采用Go語言作為開發(fā)方案,因?yàn)榻?jīng)過討論,我們發(fā)現(xiàn)這將會是一個非常龐大的系統(tǒng)。我們已經(jīng)從事Go語言開發(fā)兩年,在工作中設(shè)計(jì)架構(gòu)了一些系統(tǒng),但是還沒有任何一個系統(tǒng)有如此龐大的數(shù)據(jù)量。
我們首先創(chuàng)建了一些結(jié)構(gòu)體來定義POST的request中內(nèi)容,以及一個上傳到S3庫的方法。

小試牛刀
最初我們采取了一個非常簡單的POST請求處理實(shí)現(xiàn)方案,嘗試簡單并發(fā)goroutine去處理任務(wù)

對于中等的負(fù)載量,這個方案可以滿足大多數(shù)人的需求。但是當(dāng)數(shù)據(jù)量增大的時候,它開始顯得不那么好用了。在第一版投入生產(chǎn)環(huán)節(jié)中,我們預(yù)估了一下request的數(shù)量,事實(shí)上我們完全低估了這個龐大的數(shù)據(jù)量。
上面的方案在有很多弊端,首先我們無法控制開啟的goroutine數(shù)量,然后,當(dāng)請求達(dá)到每分鐘一百萬次的數(shù)量級,很快這段代碼就崩潰了。
再次嘗試
我們需要一個新方案,在一開始的討論中,我們明確了幾點(diǎn),首先要保證request handler 的生命周期足夠短,其次要做到在后臺進(jìn)行異步并發(fā)處理。顯然如果采用Ruby on Rails的方案,這些都是必要的事情,不然就會阻塞整個網(wǎng)絡(luò)。那么,我們將不得不采取一些常見的方案來解決這個事情比如使用 Resque, Sidekiq, SQS等等。
所以,第二次迭代的任務(wù)就是創(chuàng)建一個用于緩存列隊(duì)的通道,用來緩存請求,并將它們逐一存入S3服務(wù)器。因?yàn)槲覀兛梢钥刂脐?duì)列通道內(nèi)的最大容量,并且我們有足夠大的內(nèi)存來緩存隊(duì)列,我們認(rèn)為這將會一個是極好的方案。

然后,為了將任務(wù)列隊(duì)并依次處理,我們用了如下面這樣的代碼

老實(shí)的講,我并不知道我們在想些什么。這將會是一個充滿紅牛的不眠之夜。這個方案并沒有給我們帶來任何改進(jìn)。我們用一個緩存列隊(duì)代替了有缺陷的并發(fā)方案,這僅僅是推遲了問題的產(chǎn)生時間而已。我們的同步處理器每次只能上傳一分?jǐn)?shù)據(jù)到S3,而隊(duì)列中傳入請求的速度遠(yuǎn)大于處理器上傳數(shù)據(jù)到S3的數(shù)據(jù),很快的我們的隊(duì)列就達(dá)到了極限,從而阻塞了后續(xù)的請求添加到隊(duì)列。我們僅僅簡單的去回避問題,這僅僅是開啟了一個系統(tǒng)崩潰死亡的倒計(jì)時。

更好的方案
當(dāng)我們使用Go的通道時,我們決定利用一個公共模式來創(chuàng)建一個雙層的通道系統(tǒng)。一個用來隊(duì)列任務(wù),而另一個則是控制當(dāng)前處理隊(duì)列任務(wù)的線程數(shù)量。
這個想法是要采用一個合理的可持續(xù)的速率并行的上傳數(shù)據(jù)到S3服務(wù)器,既不會阻塞服務(wù)器的性能,也不會從S3服務(wù)器獲取上傳失敗的錯誤回調(diào)。因此,我們構(gòu)建一個job/worker模型,這看起來有些像Java, C#,等等,而我們則是考慮通過Golang的方式,利用通道來代替它們,去實(shí)現(xiàn)一個處理器線程池。


我們已經(jīng)修改了我們的網(wǎng)路請求handler來創(chuàng)建一個包含載荷數(shù)據(jù)的任務(wù)實(shí)例,我們將它發(fā)送到任務(wù)隊(duì)列通道中去,供處理線程們?nèi)ヌ幚怼?/p>

當(dāng)我們的網(wǎng)絡(luò)服務(wù)器在初始化的過程中,我們創(chuàng)建了一個名叫Run()的調(diào)度器來創(chuàng)建worker線程池,并且開始監(jiān)聽發(fā)送到任務(wù)隊(duì)列中的任務(wù)。

下面是我們調(diào)度器的代碼實(shí)現(xiàn)

值得注意的是,我們提供了最大處理線程的并發(fā)數(shù)量,用來實(shí)例化任務(wù)處理線程并且將他們添加到我們的任務(wù)處理線程池當(dāng)中。我們使用亞馬遜的Elasticbeanstalk服務(wù)并且采用了docker化的Go 運(yùn)行環(huán)境,而且我們總是嘗試遵循 12要素的方法論(譯者注:應(yīng)該是一種廣泛認(rèn)可的架構(gòu)設(shè)計(jì)思路,雖然我沒聽說過)來配置我們在生產(chǎn)環(huán)境中的系統(tǒng),我們通過環(huán)境變量來讀取這些值。這樣我們就可以控制處理線程數(shù)量和任務(wù)隊(duì)列通道的所能承載的最大容量,因此,我們可以快速的改變這些配置而不用重新部署服務(wù)器集群

最直觀的結(jié)果
在我們部署完它之后,我們立即發(fā)現(xiàn)所有的延遲率都降到了微不足道的數(shù)量,我們處理請求的能力激增。

在經(jīng)歷了幾分鐘彈性的負(fù)載均衡熱身之后,我們看到,我們的ElasticBeanstalk應(yīng)用每分鐘接近響應(yīng)了一百萬的請求。
在我們部署了新代碼之后,服務(wù)器的數(shù)量大幅下降,從100臺服務(wù)器降到20臺服務(wù)器。

結(jié)論
在我的故事中,極簡主義總是獲勝的一方。我們可以設(shè)計(jì)一個復(fù)雜的系統(tǒng),具有許多隊(duì)列,異步的后臺處理,復(fù)雜的調(diào)度,但是我們決定利用Elasticbeanstalk的自動縮放的高效的簡潔的能力去實(shí)現(xiàn)Golang提供給我們的并發(fā)效果。
你并不是每天需要部署一個4服務(wù)器的集群,來給亞馬遜的S3服務(wù)器每分鐘寫入100萬次。
總會有一個合適的工具來解決問題,有時,當(dāng)你的Ruby Rails系統(tǒng)需要一個功能強(qiáng)大的Web處理程序時,可以稍微考慮一下Ruby生態(tài)系統(tǒng)之外的更簡單、更強(qiáng)大的替代解決方案。