Ruby如何成长成高性能系统构架
為什么80%的碼農(nóng)都做不了架構(gòu)師?>>> ??
? 結(jié)束了一份Ruby為主的工作,想把個方面總結(jié)一下,這篇是關(guān)于系統(tǒng)性能方面的.以下數(shù)據(jù)都是簡單回憶的數(shù)據(jù),加之企業(yè)保密數(shù)據(jù)的需要,和精確數(shù)有些出入,僅供參考.
? 說起Ruby的性能,無論從官方到社區(qū),都公認是劣于其它的框架的.
? 那么問題來了,當(dāng)Ruby為主的系統(tǒng)需要很高的性能的時候,要如何去處理呢?
? 以下是我優(yōu)化一個經(jīng)手的系統(tǒng)的經(jīng)過,僅供參考 .
? 項目背景
? 我經(jīng)手的是一個影票系統(tǒng).原先是java架構(gòu),后因java更新和維護都無法滿足商務(wù)方面的要求,才使用Ruby來重構(gòu)了核心系統(tǒng),這也是Ruby的一個重要優(yōu)勢之一,更新修改很靈活.但這也是這個項目優(yōu)化時最困難的約束之一:必須兼容以前的老版本.
? 最早重構(gòu)版本,只是原來java版本API的重新實現(xiàn),外加一個很酷的自動配價功能.幾乎沒有性能上要求.
??
? 先說說性能要求的背景,影票系統(tǒng)本身并沒有很多的性能上的要求,畢竟是消費型的系統(tǒng),起初高峰的日子也就1w左右的成交量,如果算訪問成交比20:1,也才20w左右的日訪問量.當(dāng)然,這里不包括抓取數(shù)據(jù)的部分.正常這些訪問會被分配到每天15-20點左右,只要不是太差的系統(tǒng)都能吃下這些流量.
? 直到某天某行開始做整點搶活動,即在指定的時間放出大量很廉價或是免費的票.這就是性能問題的開始.
? 這樣業(yè)務(wù)模型會造成流量的瞬間暴發(fā),系統(tǒng)終于沒有意外的崩潰了.
? 資源文件優(yōu)化
? 我們第一次活動最早的表象問題是:系統(tǒng)緩慢,無法進入活動頁面/進入活動頁面白屏/支付無法點擊或是支付無結(jié)果.
? 當(dāng)時統(tǒng)計,活動開始時訪問量大概是2w/分鐘.即每秒要并發(fā)300多個訪問.這對純訪問型的系統(tǒng)如新聞網(wǎng)站可能不是很高的數(shù)據(jù),但是對于一個復(fù)雜資訊并有資金交易的系統(tǒng)來說,是很致命的.關(guān)鍵和敏感的獎金交易請求被淹沒在普通的訪問中,這就是上述表象問題的后續(xù)問題:交易訪問被淹沒拋棄.
? 這直接導(dǎo)致大量憤怒的用戶:搶到票無法交易的,支付完了無法出票,出票沒有結(jié)果的.這某種程度認為活動是失敗的了.這必須馬上被解決.
? 當(dāng)時解決問題的主要約束是時間,活動是以周為單位的,兩天活動,5天處理,包括周末.不僅要保證系統(tǒng)正常,還能處理好問題,提高性能,所以花時間的高大上的解決方案都會是不現(xiàn)實的.
? 最現(xiàn)實的辦法就實際問題實際分析,將問題一個一個處理.
? 系統(tǒng)緩慢就不用說了,性能還上不去,自然緩慢,這個后面處理.
? 進入頁面的白屏,是可以解決的.最早我認為只是糟糕的web頁面造成的.這個活動頁面是一個webView的頁面,這個web頁面幾乎沒有清理過,包含了這個項目上線以后各種歷史功能.是的,歷史功能.其中包括發(fā)短信等匪夷所思的功能.在項目的早期,因為我是空降兵,對項目了解有所不足,所以很多的功能只能先粗暴的復(fù)制,留下的技術(shù)債務(wù)在這個最糟糕的時刻不得不拼命償還.
? 頁面優(yōu)化后,我還發(fā)現(xiàn)一個Ruby應(yīng)用框架很大問題:在某種情況下,通過Ruby?Web應(yīng)用,如passenger/thin等的請求(這里只試出了資源類型js和圖片),有可能形成無限傳輸,即用wget url得到一個無限大的文件,并且連接不會停止,這樣頁面就無法正常顯示給用用戶.
? 具體原因當(dāng)時沒有時間去深究.解決辦法是簡單把資源文件放在nginx或是專用的資源服務(wù)器上,這里我使用了資源服務(wù)器,因為資源服務(wù)器在不同的機房,這樣也給帶寬做了分流.
? 在高壓環(huán)境下,把資源文件這種簡單粗重的活直接丟給nginx前端或是專用資源服務(wù)器(如s3)是很有較的.
?
? 數(shù)據(jù)庫及代碼優(yōu)化
? 數(shù)據(jù)庫優(yōu)化永遠是系統(tǒng)優(yōu)化的第一步.數(shù)據(jù)庫問題也是系統(tǒng)性能的第一重要瓶頸.糟糕的查詢/沒有索引/過大的數(shù)據(jù),都會引起數(shù)據(jù)庫問題.
? 糟糕的查詢,也是糟糕的代碼.或許很多Ruby的程序員都抱著”不需要考慮性能問題”的教條.但是行而上學(xué)的方法會給項目代碼和自身的發(fā)展都事帶來很大的不良影響.
? 我們真的可以”不需要考慮性能問題”?.當(dāng)然不是,也許給出這個教條的大神并沒有生活在天朝it圈這個比較低端的環(huán)境,并不知道:原來還可能寫出這么糟糕的代碼!
? 我遇到比較糟糕的代碼是像這樣的:
? # 取出正在上影的影片列表?
? Even.includes(‘films').where(“…”).map{|e| e.film}.uniq
? 這個代碼取出所有有排期的排期,只是為了取出正在上影的影片.這段代碼在初期的時候因為認為結(jié)果會被緩存,數(shù)據(jù)量也不大,所以沒有什么問題.
? 但在數(shù)據(jù)量增大后,總量達到千萬級,有效數(shù)據(jù)萬條以上時,就足以拖慢整個系統(tǒng).
??
? 這樣的問題代碼,不能總是從代碼review中取得,從數(shù)據(jù)庫日志中得到提示是更加聰明的辦法.
? 特別是你不是從頭開始就管這個項目的時候,拿我們使用的postgres來說,我們可以在數(shù)據(jù)庫的日志中找出有問題的查詢語句,再反查對應(yīng)的項目和語句.
??
? pg日志中,標記為duration的語句,就是糟糕的查詢(需要配置).
? 不同的語句有不同的速度要求,不過一般情況下,10ms左右的查詢是優(yōu)質(zhì)的,100ms左右還過得去,大于200ms是默認的duration值了,過了1s,這些查詢在需要性能的系統(tǒng)里就很致命了.
? 那么duration是1s,是不是我這個請求也就1s多一點能處理完成?在系統(tǒng)壓力小的時候是的,但是壓力上去后,這些查詢就會把你的系統(tǒng)卡得死死的,他們由1s變成1min,也使得其它10ms能處理的查詢 變成1s以上.系統(tǒng)就是這樣崩潰的.
? 從系統(tǒng)中找出這些代碼,優(yōu)化他們,修改查詢方案或是添加新的索引,都是很好的解決辦法.
? 優(yōu)化數(shù)據(jù)庫訪問相關(guān)的代碼,可以大大地減少每一個請求影響的時間,但這只是處理問題的開始.
?
? pgbuncer
? 單獨的把pgbuncer當(dāng)成一個優(yōu)化內(nèi)容,因為這其實是一個優(yōu)化數(shù)據(jù)庫連接池的內(nèi)容.一個高性能的系統(tǒng)是不能沒有數(shù)據(jù)庫連接池的,而Ruby容器在這方面表現(xiàn)很差.
? Ruby擅長慢功出細活,但每一個”活”都要占用一個數(shù)據(jù)庫連接,這就很慘了,我一臺64G24core的數(shù)據(jù)庫服務(wù)器開800個連接,已經(jīng)是比較亂來的配置了.但如果不用連接池,這還遠遠不夠.
? 比如,我在搶票的時候,希望出一分鐘內(nèi)出1000個訂單,這些訂單要經(jīng)過復(fù)雜的網(wǎng)絡(luò)交互,加起來占上20/30s都是快的.那么我開400個進程已經(jīng)是最少了,如果沒有連接池,這400個進程每個都要占用一個連接,直到進程完成,甚至直到這些進程已經(jīng)不再使用.
? 而使用pgbuncer可以分配上萬個連接數(shù),而只有正在查詢的語句才會占用真正的連接數(shù).
? 引入新的高性能框架golang
? Ruby系統(tǒng)的性能很差,有的人不相信,并且罷出很多HelloWorld來表示我們也可以和java等應(yīng)用一拼高下.這是沒有意義的,起碼對我的企業(yè)級應(yīng)用來說,起碼連接一次redis,從中取得數(shù)據(jù),這樣的實例測試才是有效的.我做過測試,從redis中取得個緩存結(jié)果集返回給客戶端,go只占了2G左右內(nèi)存,就肯完所有的CPU,并在一分鐘完成了100w次請求,能力暴表,而Ruby應(yīng)用在2萬次左右就上不去了,特別的問題是已經(jīng)耗光了所有的內(nèi)存,而cpu并沒有完全利用.
? 以下是我認為Ruby系統(tǒng)性能差的原因:
? 1. 內(nèi)存占有大. 一個進程sinatra類的也有100M左右.而內(nèi)存是很固定而且昂貴的
? 2. 線程纖程類的支持差. 最新的Ruby或是Rails都可以支持線程和纖程之類消耗小但是可以提高性能的功能.但是可以支持和優(yōu)秀的支持是有很大差距的.使用線程模式后在大壓力下,進程出現(xiàn)很多奇怪錯誤并有僵死的問題.加大線程數(shù)并沒有其它的架構(gòu)那樣有明顯的提高.這里也可能和pg沒有好的Ruby并發(fā)gem有關(guān).而在這個方面,我使用過最好用的是go.這也是我后面用go來進行開發(fā)cache服務(wù)器的原因.
? 3. 垃圾回收問題.1.9的垃圾回收很差,這點在2.1之后得到了改進,但是,在我使用的時候,2.1還有很大的問題,在試驗性使用后,出現(xiàn)了Rails進程”長大”到20+G搞壞系統(tǒng)的情況,極不穩(wěn)定.
?
? 這些問題都很大的影響了Ruby系統(tǒng)的性能,在到達一定瓶頸后,性能和可靠性都受到了極大的挑戰(zhàn).
? 為了解決這個問題,我引入了go寫了一個高性能緩存系統(tǒng).
? go的特點
? 1. 天然支持高并發(fā).
? 2. 內(nèi)存占用小,gorountine把第一/二點結(jié)合得淋漓盡至.
? 3. 有現(xiàn)代語言的特征,讓我們在獲得高性能的同時,不會受到c/c++語言的折磨.
? go占內(nèi)存小cpu利用率高,Ruby占內(nèi)存大,占CPU小,兩者配合相得益彰.
??我把我的go服務(wù)稱為CacheServer,而把原來的ruby系統(tǒng)稱為RealtimeServer,原先的請求通過nginx分成兩個部分,一部分是讀,一部分是寫(交易).
? 讀的部分丟給CacheServer,如果CacheServer不能處理(找不到cache),就丟給RealTimeserver,ReadTimeServer負責(zé)寫入,cacheServer等待RealTimeServer寫完緩存后把緩存處理返回給用戶.這里之所以CacheServer是將緩存內(nèi)容返回而不是ReadTimeServer的返回返回,因為我們的系統(tǒng)很復(fù)雜,我不得不在Cache端也對Ruby返回的數(shù)據(jù)進行了一定的處理,這個我后面有詳細講到.
? 交易部分處理仍然由原來的Ruby來直接處理.
? 這樣做的好處.go分擔(dān)了高性能要求的粗重活,而Ruby分擔(dān)了復(fù)雜的工作.如果有修改,還是只需要修改Ruby就足夠了.這是我很滿意的設(shè)計.
??
? 將交易分離,提高可靠性
? 在壓力環(huán)境中,有一部分請求是要被拋棄的.在壓力測試中,90%的可靠性,就算通過了.但交易性的接口顯然不能是這樣的.它必須是100%可靠的.
? 為了保證金額交易部分的可靠性,大訪問量的接口與敏感交易接口分開是一個很好的辦法,并且讓交易接口使用線程安全模式.確保每次訪問在代碼正確的情況下不會因為線性安全的問題而出現(xiàn)問題.請求從nginx前端就分離,如果有條件,最好分配不同的機器和網(wǎng)絡(luò)接口.
? ?更細粒度的緩存?
? ?企業(yè)級應(yīng)用,與資訊網(wǎng)站的不同是每個接口對不同的用戶,甚至相同用戶的不同時間進行訪問,都需要有不同的結(jié)果.
? ?一個用戶有一個結(jié)果,如果只緩存最終的結(jié)果,就相當(dāng)于不緩存.而不同時刻不同結(jié)果,這樣的接口對緩存帶來了很多的麻煩.
? ?比如我們有一個影院列表的接口,他返回用戶所在城市,有排期的(城市加或不加影片)影院列表,列表附上用戶是否去過的標志和對用戶的距離,列表按距離排序.
? ?這樣不同的城市人來訪問有不同的列表,不同時間去過沒去的標志不同,而不同坐標有不同距離,更變態(tài)的,是還需要對此進行排序.這樣的接口幾乎是不能夠進行緩存的.
? ?我只能對他進行拆分,細分緩存的粒度.
? ?首先來看流程:
? ?1. 根據(jù)城市(+影片)代碼取出有排期的影院
? ?2. 根據(jù)用戶號碼查看用戶去過哪些影院
? ?3. 根據(jù)用戶定位信息給出影院的距離
? ?這是糟糕的設(shè)計,但必須兼容.這就是要求用戶每次請求都有不同的結(jié)果.根本不能緩存.但讓我們試試拆分一下.
? ?首先,一個城市的影院是固定的,問題只是其下只否有排期或指定的影片是否有排期.那么緩存一個城市(+影片)的影院列表是可行的,這樣不同用戶但同一個城市看同一個電影,可以從同樣緩存中取出.這樣的場景在這個應(yīng)用用是非常大的,看電影人大多的是在北上廣,而同檔期里火爆的電影也就那么幾部,這樣這個緩存就非常有意義了.?
? ?再次,用戶去過的影院,這個其實變化得很少,這個數(shù)據(jù)是用戶購票成功過的影院,這個功能最后被我直接變成一個固定的放在redis里的kv值,這個表會記錄一個用戶去過的影院的ids,定時會更新新生成的訂單的用戶的記錄.想知道用戶去過哪個影院,直接從這個redis里取出就可以.這樣的修改甚至可以讓我們的產(chǎn)品線的產(chǎn)品可以通用這個信息.
? ?有了上面的緩存,下面”緩存”接口要做的,只是計算下影院的距離,這樣,接口避開了與pg數(shù)據(jù)庫的連接,能快速地響應(yīng)用戶.而這些,我可以直接在go cache接口完成.
? ?緩存的內(nèi)容,還包括一些select的結(jié)果集,由于很多的緩存機制都是對id=的單體結(jié)果,所以我自己寫了一個集合的緩存gem:https://github.com/azhao1981/kv_cache
??
? ?交換機的問題
? ?在壓力環(huán)境下,交換機也是很容易出問題的部分.
? ?我們有五臺機器,兩臺應(yīng)用,一臺數(shù)據(jù)庫,一個測試機,一個交換機.?
?? 做活動的時候,我們發(fā)現(xiàn)了大量請求發(fā)不出去,包括rails c連接到數(shù)據(jù)庫查看,但是查看系統(tǒng)連接數(shù),卻只是3w多個連接,并沒有達到系統(tǒng)連接數(shù)的上限(5w+),數(shù)據(jù)庫的連接數(shù)也沒有達到上限.
? ?這是路由器問題的表象,我們最先使用單個交換機,用虛擬網(wǎng)段分內(nèi)網(wǎng)和外網(wǎng).出問題后,我們通過借用機房外出接口,發(fā)現(xiàn)性能上去一些,這意味著交換機是一個瓶頸.
? ?我猜想問題是這樣產(chǎn)生的:
? ?首先,交換機是物理單體的,無論他分成多少網(wǎng)段,總的吞吐量都是一個定值.
? ?其次,交換機使用虛擬網(wǎng)段進行連接,在高壓力環(huán)境下,可能會容易形成死循環(huán).想象下,一個外部的連接請求,需要差不多數(shù)5/6次的內(nèi)部請求來完成,外部的連接要保持并等待內(nèi)部的連接交互的完成.而只要內(nèi)部的連接有一個出現(xiàn)瓶頸,外部的連接就不能關(guān)閉,那么就很容易出現(xiàn)等待的死循環(huán).
? ?然后是路由器防火墻,在高壓力環(huán)境下,默認的路由器防火墻會把請求當(dāng)成洪水攻擊處理掉.這個是在一定量后可靠性上不去后我們才發(fā)現(xiàn)的.
??
? ?系統(tǒng)的調(diào)優(yōu)
? ?服務(wù)器有很多的參數(shù),比如ulimit的文件最大數(shù)量,TIME_WAIT時間等,都是性能產(chǎn)生問題的原因.這個就得請專業(yè)的運維人員來處理了. ??
? ?這里要注意一下nginx如果有http轉(zhuǎn)發(fā),就要設(shè)置一下http/1.1的頭,不然這些TIME_WAIT的連接并不能很好的得用.影響很大.一些應(yīng)用中的http調(diào)用連接,最好也要檢查一下,默認是不是有http/1.1的報頭,如果沒有,需要人工指定.
? ?讀寫分離
? ?我這里的讀寫分離不是雙機熱備的那種讀寫分離,我們的系統(tǒng)原來是沒有雙機熱備的.我這里只是把只要讀取的接口的數(shù)據(jù)分離到另一個庫里.相當(dāng)于一個軟讀寫分離.
? ?我要做的很簡單,使用原來的go cacheServer做為讀取的應(yīng)用,優(yōu)化一個redis配置專門用于cache.定時將pg庫里的信息寫到這個redis中.cacheServer由原來不能處理的丟還給RealtimeServer變成直接返回錯誤信息.而定時任務(wù)來保證cacheServer能獲取所有的應(yīng)取得的東西.
? ?這就相當(dāng)于把讀數(shù)據(jù)分離到redis中,應(yīng)用也由高性能的go來處理.而寫的部分仍然由原來的pg和ruby來完成.
? ?這樣的修改很輕巧,幾乎可以無縫的進行.接口也可以一個個的進行修改,下次活動來到的時候,有的接口沒有完成,也是可以接受的.這對于我的時間約束來說是一個好的消息.
?? 到此,讀寫分離最終完成了.系統(tǒng)的性能得到了很大的提高.特別是讀取信息部分,系統(tǒng)處理的功能遠遠大于帶寬的供給,而處理這部分功能的產(chǎn)用的資源很小,高峰期也只占到不到幾百M而以.
? ?
? 橫向擴展RealtimeServer
? 上面都是對cache層面的優(yōu)化,核心就是減少響應(yīng)時間.但是RealtimeServer就不能這么干了.
? RealTimeServer剩下的接口本身是一個多方交互的過程,又出于可靠的需求,使用進程安全的模式.在搶票的時候,內(nèi)存和cpu都在向上飆升.剩下的解決方案就是橫向擴展了,一臺能開400個進程,那兩臺就是800,在需要的時候利用空閑的資源頂上.
? 更多的優(yōu)化
? 優(yōu)化是一個可持續(xù)的工作, 我們的舊版本因為要兼容老版本的關(guān)系,只能用各種拆分的方法來進行改進.但在新的版本中,我采用了服務(wù)器端盡量簡單的原則.
? 一個互聯(lián)網(wǎng)項目的發(fā)展,一開始可能因為希望可控性更強,把更多的功能放在服務(wù)器端,這樣更新就不需要等待客戶端的升級,可直接修改服務(wù)器就可以了,但是一但發(fā)展到服務(wù)器端要承受壓力的時候,那么就必須考慮把更多的功能放到客戶端了,就比如我上面提到的影院列表,距離之類的,其實用戶端很容易的進行計算,而用影院坐標代替距離,可以讓服務(wù)器非常簡單,緩存起來也很容易.而且現(xiàn)在的手機端已經(jīng)擁有很強大的能力,不僅可以給你距離,還可以給出方向,這些都是服務(wù)端不能做到的.
? 從設(shè)計讓讓服務(wù)端更加簡單,這是從根本上解決的辦法.
? AWS云
? AWS在其實在早些年已經(jīng)進入了中國,但不知道為什么,現(xiàn)在都沒有正式的運營.但現(xiàn)在公司好像已經(jīng)可以申請賬號.
? 云的優(yōu)勢不用置疑.AWS超強的性能.彈性的帶寬等等,都是不是我們小公司自己罷幾個服務(wù)器可以比擬的.如果我們一早就是使用AWS,那上面的目標估計不用做都可以實現(xiàn)了.只可惜,在準備遷移前我已經(jīng)因病離開公司.無緣這個遷移優(yōu)化部分了.
??
? 總結(jié)
? 系統(tǒng)性能的提升,是一個長期而艱難的過程.在有實際業(yè)務(wù)壓力的情況下,每一步的修改都要小心翼翼.而每步的改進又必須經(jīng)受實際壓力的檢驗.不夸夸其談高大上的集群或云,而是一小步一小步的實實在在提升自己系統(tǒng)的性能.也是自身能力的提升.
轉(zhuǎn)載于:https://my.oschina.net/zhao/blog/407413
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的Ruby如何成长成高性能系统构架的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Hi3520d uImage制作 ubo
- 下一篇: windows 建立wifi热点