金庸經典《射雕英雄傳》里,黃蓉為了讓洪七公交自己和靖哥哥武功,天天對師傅美食相待,在做了“玉笛誰家聽落梅”這樣一些世間珍品之後,告訴師傅說今天要做的是"炒白菜"。洪七公露出非常欣賞的眼光,說:“好,我倒要看看你怎樣化腐朽為神奇。”上周五聽了一個我們內部的深度學習講座,基本這方面處於初始探索階段。上周 ...
金庸經典《射雕英雄傳》里,黃蓉為了讓洪七公交自己和靖哥哥武功,天天對師傅美食相待,在做了“玉笛誰家聽落梅”這樣一些世間珍品之後,告訴師傅說今天要做的是"炒白菜"。洪七公露出非常欣賞的眼光,說:“好,我倒要看看你怎樣化腐朽為神奇。”上周五聽了一個我們內部的深度學習講座,基本這方面處於初始探索階段。上周六去3w咖啡聽了百度的人工智慧講座,他們的深度學習也只限於對代碼的訓練。想一想代碼這個東西分支相對來說還是有限的,所以現在的各種集成開發軟體已經很簡化程式員的工作了,所以看百度做的基於AI的效果還是有點殺雞用牛刀。我們部門不涉及大數據,雲計算,人工智慧,深度學習這些聽起來高大上的業務和技術,但是扎扎實實做好自己要比用這些包裝好的TensorFlow啥的對底層的理解要深入的多。特別是在長期做一個業務的過程中,系統一些潛在的問題會在大腦中不斷的旋轉,一些新知識的攝取很快就能夠產生對現有業務的改造想法。好了:Talking is cheap, show me the code.
我現在主要在做兩件事,一件事是媒資介面的併發量上不去,我已經跟領導說好了:給我時間,我會搞定的。我腦子裡的方案有A,B,C,D,E。但是這個業務相當重要,實際上做的時候雖然覺得這個架構太老太不合理,也只能先從JVM調優,dubbo參數調優,緩存參數調優這些做起,看看不改動架構的前提下能改善多少。然後再從一些局部的性能消耗點入手,從局部到整體一點點來。上周因為併發量上去之後,CPU使用率過高,jstat看到minor gc相當頻繁,併發量上去之後竟然達到每秒4,5次。就先增加了新生代的容量,效果有,但是很小。用jmap看到頻繁的對象是系統的本地緩存,這個是定時任務每次新數據覆蓋的,會有相當多的垃圾回收。而且每次服務層啟動,先運行大量本地緩存,很耗時,服務已經暴露,但是實際還不具備處理能力,結果經常會啟動時出現dubbo線程池滿,一段時間後自行恢復。為瞭解決本地緩存的問題,我想採用緩存數據存於redis,用canel訂閱mysql的更新,啟動時只是取一下redis的值,採用redis的哈希結構,可以直接反序列化成java的hashmap,很快。然後監聽redis更新,不用定時跑。這樣就涉及到一個問題:線上沒有此業務的redis集群,本地緩存的字典值很多,究竟性能怎樣,需要測試對比。
好在我還有另外一件事情要做:離線服務,之前做的時候也比較倉促,雖然細節處我做了很多的優化,處處體現java功底,但是跟人家講,我講不出來到底這個有什麼閃光點,太零碎了,人家聽了就一個反應:不就是一個後臺服務嘛。確實從大的架構層做的就不像一個架構師,僅僅增量上做了一個負載分攤,全量只是簡單的主備。像全量這種既消耗時間又消耗資源的,怎麼能從一開始不做分散式計算呢。於是最近做了一版改造,解決分散式計算,橫向擴展問題。採用redis作為中間通信工具和字典存儲工具,正好和媒資介面的字典數據是一樣的,這樣就可以用這個項目來進行線上本地緩存性能的測試,而不影響最重要的媒資介面服務。本來也想用搜索中間件來存儲數據,解耦資料庫,因為我最終肯定是要做自己的搜索中間件的。但是確實,對於項目來說是可用可不用的東西,還增加維護成本,那就不應該用。想做自己找時間做去。
上面是整個系統的架構圖。其中包括了對接搜索部門直到面向用戶終端的整個流程,裡面用到了自己做的離線數據框架epiphany。放在我的github:https://github.com/xiexiaojing/epiphany。可以通過maven管理下載,pom配置如下(如需引用請註意版本更新):
<dependency> <groupId>com.brmayi</groupId> <artifactId>epiphany</artifactId> <version>0.7</version> </dependency>
框架核心思想:
將離線數據服務劃分為全量服務,增量服務和手動處理服務三部分。全量和增量採用redis作為作業調度和管理機制。在redis宕機時各個服務獨立運行,產生相同的輸出,結果集是在正常情況下的n倍,n為伺服器單元。其中全量服務因為原型是在我們項目的離線服務基礎上進行開發,數據量大,文件壓縮後是幾十G的數據量級,所以數據存於磁碟。每個服務通過redis獲取將處理數據的區間,各自處理。伺服器的磁碟採用async同步處理結果。為了高可用,採用的是分步計算,結果冗餘。獲取方可以將其中一個磁碟作為主磁碟作為hadoop的節點或者採用linux的async同步,或者ftp,nfs等手段拉取數據。增量服務可以採用消息隊列等手段進行數據傳遞,如果消息多,消息體大,可以用消息傳遞更新的id,內容可存於磁碟,中間資料庫,緩存等,讓調用方來進行拉取。手動處理服務直接採用netty處理客戶端的http請求。整個框架運行不需任何外部容器。直接用jvm運行main方法。容錯可根據需要採用簡單主備或者failover=roundrobin。
框架使用方法:
整個架構體系已經在框架內部處理,業務方只需實現DataService介面,將數據傳入框架,然後按照自己的需求啟動服務即可。DataService介面定義如下:
package com.brmayi.epiphany.service; import java.util.List; import com.brmayi.epiphany.exception.EpiphanyException; /** * * 通用文件處理類:這是業務代碼的核心類 * * .==. .==. * //'^\\ //^'\\ * // ^^\(\__/)/^ ^^\\ * //^ ^^ ^/6 6\ ^^^ \\ * //^ ^^ ^/( .. )\^ ^^ \\ * // ^^ ^/\|v""v|/\^^ ^ \\ * // ^^/\/ / '~~' \ \/\^ ^\\ * ---------------------------------------- * HERE BE DRAGONS WHICH CAN CREATE MIRACLE * * @author 靜兒([email protected]) * */ public interface DataService { /** * 根據ID進行業務數據處理 * @param dealIds 處理ID * @param path 要保存到的磁碟路徑,不需要保存磁碟,可以為null * @throws EpiphanyException 拋出通用異常 */ public void dealDataByIds(List<Long> dealIds, String path) throws EpiphanyException; /** * 根據時間區間獲取id列表 * @param beginTime 開始時間 * @param endTime 結束時間 * @return id列表 * @throws EpiphanyException 拋出通用異常 */ public List<Long> getIds(String beginTime, String endTime) throws EpiphanyException; /** * 根據開始結束ID處理數據 * @param beginId 開始ID * @param endId 結束ID * @param path 要保存到的磁碟路徑,不需要保存磁碟,可以為null * @throws EpiphanyException 拋出通用異常 */ public void dealDataByBeginEnd(long beginId, long endId, String path) throws EpiphanyException; /** * 取得最大ID * @return 最大ID * @throws EpiphanyException 拋出通用異常 */ public long getMaxId() throws EpiphanyException; /** * 取得最小ID * @return 最小ID * @throws EpiphanyException 拋出通用異常 */ public long getMinId() throws EpiphanyException; }
深入技術細節:
☆ 關於壓縮:壓縮是遞歸操作,如果java棧設置很大,壓縮操作會非常消耗CPU。所以框架設計時,業務方可設置全量的線程數,但是壓縮是非同步用另外的線程池來管理,這個線程池的容量是全量線程數的一半。比如我們線上用的是24核高配物理機,現在上面有多個服務進行復用。我的離線服務是視頻和專輯兩個部分,有數據通用的邏輯,但是是獨立的業務,所以我用一個工程來進行項目管理,但是用的是兩個獨立進程,採用兩個腳本分開部署。千萬級數據,每個業務全量都使用10個線程。在改造前的那一版採用的是專輯400個線程,視頻660個線程,用50個線程的線程池來跑。測試發現改造後的10個線程速度並不比改造前差多少。原因是追加操作和文件大小關係不是很大,開銷要小於新建文件的開銷。線程少減少了資源開銷和上下文切換。還有就是壓縮操作,大文件的壓縮效率要高很多。因為用的是哈夫曼系的gz壓縮,減少了頭文件的字元映射。
☆ Redis的哈希結構:這個結構看起來是對java的hashmap的很好的對應。但是實際使用的時候,如果map的key(對應於redis哈希中的field)大於1000,插入效率急劇下降。因為redis是單線程的IO,而一個map對應的redis的key是一個,所有這些寫操作會被映射到一個redis節點,效率很低。我試圖將一個3w7k的字典map放入redis。結果運行了近一個小時,插入了20402條後再也插不進去了,連接超時,運行幾次都沒能插入更多。
☆ 巧用對象池:我在框架中封裝了有限制的對象和無限制的對象池來作為線程池進行一些非同步調用。無限制的對象池是因為對象的總數在其他地方有限制。而有限制的對象池是為了防止對象在異常時過多資源占用。而非同步有點地方是為了提高效率,有些地方又是必須的。比如我在程式中一個方法調用mysql取數據,而這個方法處理完數據後還要給MQ發消息,消息體特別大,發送時間特別長。長時間mysql不斷開,就會連接超時異常。
一點感悟:
一個人的智商決定了學習的速度和領悟能力。而對情商決定了在一條路上能走多遠。對一個項目的熱愛可以深入到對用到的每條sql都對其性能做深入的研究。而對於整個項目的架構更可以深入到linux的內核方面。所以足夠用心就會掌握更多的技術。而寫一個自己的框架會對國內的框架有一個更好的理解和容忍度。比如我在寫框架的時候用到的預設值和建議值都是基於我自己的項目。因為這個框架在我們內部很多的離線項目都可以用,我在考慮他們的具體環境怎樣設置更加合適。但是再遠一點,別人用的時候,怎麼設置合理,性能曲線我還在研究中。像dubbo這種開源框架也沒能在這方面給出一個特別好的文檔。
周末輕鬆一下:
周末在家開電腦,兒子在旁邊千萬不要打開資料庫。否則他的小手在鍵盤上劃一下,你就會見識到什麼叫真正的噩夢。
兒子特別黏我,我總想找藉口把兒子推給他爸。昨晚他有粘著我的時候,我說:跟你爸下象棋去。兒子找了半天,藍棋子少兩個,所以他想在手機上玩。我說:將紅棋子那兩個也拿走就可以公平的玩了嘛。他爹平靜的說:恩,沒有車和將隨便下。 當場笑的肚子疼。
我要寫文,他爹帶著兒子去外面玩。臨走很溫柔的說:你手機快沒電了,記得充。我一下子就感動了,好細心,暖男。再一想,前面他還說過讓我在家訂好烤串,5點半送到他好回來吃!