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