先簡單介紹一下我的經歷,最早在學校的時候,是在社團里寫php和Java,創(chuàng)業(yè)時期寫js,oc和Ruby,現(xiàn)在是全職用Rails寫后端了。
項目簡介
我們的主要業(yè)務有兩塊,社區(qū)和電商
整體業(yè)務的峰值qps大概在3000,也算是pv過10億的站點了,后端team有4個人,除了一個八年老司機,其他人參加工作的年限都不是太久。
我們面對的是一個巨大的基于Rails的歷史遺留系統(tǒng),最早的開發(fā)成員均已離開,導致我們常常面對遺留代碼一臉蒙逼,到處是沒有人知道的邏輯,丑陋的實現(xiàn),以及很多性能跟不上的接口。
與巨石應用的斗爭
日常工作的重中之重,就是與這個monolith的戰(zhàn)斗!
性能篇
以往每年我們搞活動,服務器都會掛,經濟損失不少,所以優(yōu)化性能,保證活動期間的訪問是第一要務。
原來的活動整體設計還是比較科學的,活動頁面本身是靜態(tài)化托管到cdn的,從來沒有出現(xiàn)過問題,主要瓶頸是商品詳情頁面。我們利用redis做了三層cache,解決了這個問題。第一層是數(shù)據(jù)庫的緩存,直接把商品信息緩存到redis里,避免了頻繁的數(shù)據(jù)庫訪問,第二層是單條數(shù)據(jù)的渲染緩存,可以理解成一小段html,第三層是整個數(shù)據(jù)集的渲染緩存。第二個瓶頸出現(xiàn)在一些靜態(tài)資源上,全面遷移到云存儲解決。做完這兩件事之后,上上次活動是我們有史以來第一次,沒有掛。
就在我們覺得,優(yōu)化做的不錯的時候,上次活動卻又掛了。
要知道我們特意買了新服務器,美滋滋覺得這下穩(wěn)了,沒想到...
上次活動掛的原因有以下幾點
- redis hmget,我們通過gem提供的API,緩存了一個巨大的省市區(qū)列表,但是沒有注意到緩存是分離的,獲取整個列表,其實就是一條hmget獲取所有獨立的緩存片段,這個操作block了redis,導致訪問極度緩慢。我們緊急把整個列表轉成json,直接貼到代碼里返回hotfix了這個問題
- 突然無法通過redis sential進行連接,這套sential系統(tǒng)是由已經離職的運維搭建的,我們繞開sential直接連接redis,解決了這個問題
- fd limit, 做完以上兩點,依然時常502, 發(fā)現(xiàn)運維修改的是root用戶的fd數(shù)量...坑爹....
- 在支付回調中有一段用于統(tǒng)計的sql,訂單量大了以后slow query,block了數(shù)據(jù)庫,我們直接注釋了這段可有可無的老代碼,解決。
總結一下,對于web應用的場景來說,大都是讀多寫少,緩存讀請求,異步寫請求,是我們經常采用的兩種效果不錯的方式。在數(shù)據(jù)庫層面,對于遺留代碼中效率低下的查詢進行重寫,重點改寫了所有N+1查詢,對一些逐條插入的語句用batch insert合并寫入操作,也有不錯的提升。
替換篇
做的比較有意思的事,是寫了我們內部用的個推GEM。原來使用的是github上開源的一個GEM,但是已經很久沒更新了,無法適應我們的使用需求。我基于個推最新的HTTPS的API,寫了一個Ruby的包裝。
這里要吐槽的是個推的技術水平。推送服務是做的不錯,但API怎么做的這么low。他們定義了一個叫authorize的http header用來傳遞身份信息...違背了RFC關于HTTP頭必須大寫開頭的規(guī)范。一些語言的標準庫(Go、Ruby...)會自動幫你把authorize轉化成Authorize,導致個推那邊一直返回auth error...而個推的接口又是HTTPS的,抓包調試很困難,浪費了我很長時間調試這個問題。
重構篇
重構的主要方針就是拆分,盡可能把功能從巨石應用中拆出去。如果一時半會難以拆分的,代碼上也盡可能讓邏輯高度內聚,方便以后遷移。
消息系統(tǒng)的重構
消息系統(tǒng)是一個,出點問題沒什么,但做得好會非常出彩的功能。我一直覺得,像知乎這種社區(qū)的成功,除了內容,很大一部分要歸功于消息的體驗。目前,我們幾乎所有頁面,都會展示新消息的數(shù)量,導致每次請求都會去主數(shù)據(jù)庫的消息表做count,計算各種消息的數(shù)量返回給前端。我正在著手把整個系統(tǒng)遷移到另一個獨立的數(shù)據(jù)庫,以后可以作為單獨的服務供內部調用,降級限流什么的都很方便。
搜索的重構
原來的搜索是基于Solr的java工程,是一個我們內部沒人維護好多年的爛攤子,雖然各方面表現(xiàn)都不錯。我們還是決定未來要用Elasticsearch換掉它。
新系統(tǒng)
我新寫了內部的財務系統(tǒng),過程中遇到很多問題,寫的也很痛苦,但最終效果還是不錯。因為原來的各種報表都是直接基于生產數(shù)據(jù)庫的,對業(yè)務會有沖擊,新系統(tǒng)寫了一個同步模塊,可以增量同步訂單數(shù)據(jù)到財務系統(tǒng)的專用數(shù)據(jù)庫,這樣就不會對業(yè)務帶來影響。
遇到的比較大的坑就是內存爆炸。有一些耗時計算我放到了消息對列里,整個worker進程的內存占用瘋狂上升。最終發(fā)現(xiàn)是Ruby內存模型的問題。
我通過時間換空間的方式,把之前加載全部數(shù)據(jù)做計算,改成了加載部分數(shù)據(jù)做計算,然后匯總結果這樣的方式,極大降低了內存占用,并通過每天重啟worker進程,解決了最主要的內存問題。
這個項目讓我真實感覺到,有些場景真的不是Ruby擅長的領域。Ruby的內存模型,就是盡量分配對象,從不真正回收,只會重用。Ruby VM啟動就有大量空對象等著被分配,假如我加載了很多數(shù)據(jù),空對象不夠用了,VM就向操作系統(tǒng)申請一批內存,用完后也不釋放,等著下次重用。而報表計算的最佳場景就是能加載大量數(shù)據(jù),算一下結果,算完釋放掉內存。
監(jiān)控
可以看我之前的文章使用ELK構建分布式日志分析系統(tǒng)
代碼篇
在日常編碼、重構的過程中,經常使用的技術是
- 設計模式
- 元編程
運用設計模式,寫出符合OOP規(guī)范的代碼。分割每個類的職責,盡量讓各個功能的邏輯內聚,只提供彼此間調用的接口,這是我最近才剛領悟的代碼整潔之道。
元編程抽象代碼,我很早就在使用的奇技淫巧?,F(xiàn)在卻用的越來越少了,因為它違背了OOP,可維護性比較差,對使用者的水平有很大要求,也容易坑隊友。
簡單地說,我代碼中的if/else越來越少了,類越來越多了,改動起來越來方便了,改動影響的部分越來少了,美滋滋。
結語
用一句古老的名言,軟件開發(fā)沒有銀彈。