第四屆阿里巴巴性能大賽總結

最近一直在找裝修公司,自己辛苦買的房子不住進去確實心有不甘吶。。。所以,比賽完了好久纔開始寫這個比賽總結。寫總結的原因是這次比賽還是學到了很多東西。想要總結下。一開始看到有這個比賽的時候我是猶豫之拒絕的。因爲想着準備裝修,剛換工作根本沒有時間。直到另外一個朋友發消息給我說,有這個比賽挺適合我的時候,我才決定報名的。。

第一賽季:

簡介:

目的:藉助於 service mesh 的解決方案,讓 dubbo 自己提供跨語言的解決方案,來屏蔽不同語言的處理細節,於是乎,dubbo 生態的跨語言 service mesh 解決方案就被命名爲了 dubbo mesh。
賽題

實現一個高性能的 Service Mesh Agent 組件,幷包含如下一些功能:
1. 服務註冊與發現
2. 協議轉換
3. 負載均衡

這裏寫圖片描述

簡單點說就是實現圖中紅色部分 (ca)consumer-agent, (pa)provider-agent。
ca 和 pa 通信不限,pa和provider dubbo協議(provider是dubbo提供的服務)。每個 provider 處理能力不一樣(負載均衡算法)。其他詳細限制請看賽題:

因爲已經預約了幾家裝修公司了,所以第一賽季開始一段時間了,我還沒有行動。爲了終結這個情況,我約了隊友出來一起見個面準備開搞這個事情。週末下午經過千山萬水大家終於聚在一起,沒時間搞代碼了,只能一起討論下題目了。

第一版本

我們討論出的方案是:
這裏寫圖片描述
方案有了開擼?no,比賽環境是 spring boot + docker 的。。
之前一直想等有時間了,好好學習下 spring boot + spring cloud + docker,但是一直沒有學。每個週五晚上一邊惡補知識,一邊寫代碼。
環境問題終於解決了,開始全力擼代碼吧。
首先我們選擇的網絡框架爲 netty(netty 比較熟悉) 。和etcd通信的就使用了官方例子裏的 jetcd-core。
1. 首先完成的是照着 duubo 的協議實現* dubbo協議*。話說官方給的 dubbo的協議真的寫的太難看了 ,我承認能用,但是代碼寫的好難受。於是找了一個晚上重寫,結果花了兩個晚上寫完。。 就這樣 pa 和 provider 通信了。dubbo協議也瞭解了 ^_^
2. 終於週五晚上到了,計劃是完成 ca 和 pa 通信這步。肯定是要用長連接或者udp,不可能像關閥提供的 demo 那樣用 http。一開始懶得寫代碼,ca 直接使用的 dubbo 協議發給 pa,pa 透傳給 provider。
3. 週六最後一塊,ca 怎麼接受 consumer 發送過來的http部分。。第一版當然是先完成主流程了。直接用了 spring mvc 的http部分。我知道性能不一定好。但是先完成主要流程把。
4. 周天和小夥伴碰面,完成負載均衡算法。小夥伴和我說 java 環境太複雜了,mvc,boot,cloud 扥等,不會寫了代碼了(這也是複賽選擇c++一個重要原因)。。。最後我們終於完成第一個負載均衡算法隨機負載均衡
5. 結果:
週一晚上一下吧我就把所有代碼整理起來了。然後提交了。也跑出了成績。比官方提供的高了500多的tps。於是我就接着看我的裝修了。小夥伴去接着看房子了(他在買房子)。

優化1

正式比賽開始了,看了下大家的成績,驚掉了下巴,怎麼都這麼高,和第一差了大約 1.5k。。感快優化,把之前偷懶的地方趕上去。。
1. 首先就是 ca http 服務器,之前使用 spring mvc ,趕快換成 netty 的 http。折騰了兩晚上總算是搞定了。
2. 學習了 docker 性能分析指令,觀察了容器的 cpu 情況。發現 provider 的 cpu的利用率明顯不一致。找小夥伴改負載均衡算法。小夥伴第二版實現的是加權輪詢算法實現的負載均衡。
3. 結果:
通過各種測試總算是有一點提升,但是進複賽還是很懸吶。。

優化2

  1. 各種瞎折騰開始,jmap 觀察內存,沒有發現老年代內存有頻繁 gc。jstat 觀察 old gc 也很正常。jstack 發現問題。線程有 100 多個。原來寫代碼的時候ca 作爲客戶端時候使用的 FixChannlePool 鏈接池每次添加新鏈接的時候都會 new 一個新的 EventLoop。這還了得,線程切換多費性能啊。
  2. 結果:
    完成之後測試了一下,果然性提升了小几百。

優化3

  1. 乘着手熱,把之前的坑都寫寫。之前 ca 到 pa 是 tcp 長連接的透傳的方式。猜測可能 ca 收到數據轉爲 duubo 協議發送給 pa,pa 直接透傳給provider 的時候會pa會拆包,倒是 provider 組包之後才能處理,影響了 provider 的處理性能。於是使用 length+cbor編碼消息體的自定義協議。
  2. 結果:
    一頓整改之後終於完成了。提測沒有啥變化。。。。

優化4

眼看比賽結束只剩一週了,眼看有可能被淘汰了。下班,閒着沒事 jstack 一下,發現線程還是有點多啊。
1. 找所有使用 eventLoop 的地方更改線程數。
2. 結果
增加了 100 多的 tps。

結束

感覺實在沒有辦法了,週末還有兩家裝修公司要跑。。不管了,坐等比賽結果。週末走了幾家公司之後,週一投入到了新公司給安排接收的新項目中,直到晚上回家閒着沒事,有看了下排名。竟然入圍了。。。。
排名

總結

收穫:

  1. 首先這個比賽的機會學會了 spring boot ,spring cloud,docker 。現在已經在項目組中推行。
  2. 以前一直覺得 io (網絡,硬盤)纔是瓶頸,現在知道原來,在極限的優化下,任何代碼都可能是瓶頸。
  3. 堅持不懈的精神,看大佬的優化,就是不停的嘗試,哪怕只有一點點提升,甚至失敗也。

學到的技術:

比賽結束之後立馬關注大佬的分享。對比之後感覺有好多需要學習的點。
2. jdk nio 封裝的 epoll 是level-triggered。所以 netty 可以使用 EpollSocketChannel。
3. 使用入站服務端的 eventLoopGroup 爲出站客戶端預先創建好 channel,這樣可以達到複用 eventLoop 的目的。並且此時還有一個伴隨的優化點,就是將存儲 Map 的數據結構,從 concurrentHashMap 替換爲了 ThreadLocal ,因爲入站線程和出站線程都是相同的線程,省去一個 concurrentHashMap 可以進一步降低鎖的競爭。
4. provider 線程數固定爲 200 個線程,如果 large-pa 繼續分配 3/1+2+3=0.5 即 50% 的請求,很容易出現 provider 線程池飽滿的異常,所以調整了加權值爲 1:2:2。
5. 關閉 netty 的內存泄露檢測。
6. 百度: 對於主動連接(connect)的fd,設置TCP_QUICKACK=0,該往往說明客戶端將很快有數據要發送給服務器,所以在三次握手協議中的第三步,客戶端會延遲發送ACK,而是直接給服務器發送request數據,並將ACK隨request包一同發給服務器。
對於被動接受(accept)的fd,設置了TCP_QUICKACK=0。
這種情況需要先明白一個過程,比如對於一個http協議,三次握手協議結束後,客戶端會立即向服務器發送一個request請求,當服務器接收完這個request請求以後,會首先給客戶端一個ACK確認告訴客戶端已經收到了該數據包,然後當服務器完成了請求,纔會再發response。
明白了這個過程,就很容易解釋了,服務器端這樣設置的目的就是接收完request後先不ACK,而是把這個ACK和接下來的response一同發送給客戶端。

第二賽季:

第二賽季一開始感覺在第一賽季耗光了精力,不大想搞了。可以誰能想到小夥伴可能由於自己公司的事情忙的差不多了,主動聯繫我們開始搞複賽。。隊友不放棄,我也不能放棄。。。T_T

賽題

賽題
目標:使用 java 或者 c++ 實現一個 100w 隊列的消息系統。限定只使用基jdk。機器性能4c8g的ECS,限定使用的最大JVM大小爲4GB(-Xmx4g)。我猜是爲了物聯網場景,才百萬topic 的。

public abstract class QueueStore {
    abstract void put(String queueName, byte[] message);
    abstract Collection<byte[]> get(String queueName, long offset, long num);
}
  1. 程序校驗邏輯
    校驗程序分爲三個階段: 1.發送階段 2.索引校驗階段 3.順序消費階段
  2. 程序校驗規模說明
    1) 各個階段線程數在20~30左右
    2) 發送階段:消息大小在50字節左右,消息條數在20億條左右,也即發送總數據在100G左右
    3)索引校驗階段:會對所有隊列的索引進行隨機校驗;平均每個隊列會校驗1~2次;
    4)順序消費階段:挑選20%的隊列進行全部讀取和校驗;
    5)發送階段最大耗時不能超過1800s;索引校驗階段和順序消費階段加在一起,最大耗時也不能超過1800s;超時會被判斷爲評測失敗。
  3. 排名規則
    在結果校驗100%正確的前提下,按照平均tps從高到低來排名。

一個週末大家週六忙完自己的事情之後聚在一起討論方案。由於第一賽季隊友感覺把 java 都忘的差不多了,加上 jvm 有限制4G內存。第二賽季我們決定使用c++實現。

第一版

因爲 c++ 沒有分段鎖的 map,要自己實現一個 java 的 CouncurrentHashMap。小夥伴測試下來,100w 最好就是 1000*1000來分段。這塊我們到最後也是這個策略。
因爲內存有限,每個topic 緩存到40條,然後調用存儲Storage的 write()獲取到一個這快消息的一個索引信息FileBlock。Storage 隨機找到一個BlockQueue(啓動會初始化多個),判斷文件的緩存隊列是否大於二級緩存極限,大於則觸發 BlockQueue 的 write()。BlockQueue 的write()會入隊數據,線程異步寫入文件系統

讀的話,剛剛好是反過程,Topic 先判斷是在緩存還是在 FlieBlock 的list裏,讀自己的緩存。讀FileBlock list裏的就去,下級Storage裏要。Storage在己的緩存找,找不到就到BlockQueueFile找。找不就到硬盤找。

主要流程圖:
主流程

主要類圖:
類圖

又一個週末,裝修找項目經理,經過一個小夥伴的家周圍,順便去溝通了下,合了一部分代碼。另外一個小夥伴因爲老家有事,回家了。。。
週一晚上下班,我們兩找個咖啡廳,合代碼。。

優化1:

經過折騰總算是出了一個編譯過的版本。提測。。段錯誤。。。
開始排查。上內存分析神器 valgrand 。經過幾個晚上的調試。終於都解決了。接着上,可以半個小時左右,顯示程序被殺死。。

優化2:

  1. 分析無果後,買了雲服務器,決定在運行的時候分析狀態(100G數據誰會用自己的電腦?)。經過分析發現程序運行十幾分鍾後內存就耗盡了。無奈只能把每個topic緩存的消息由40改爲20。BlockQueueFile 的隊列緩存改成最多10個。否則入隊的時候將阻塞。一頓本地調試之後提交。一個多小時候終於出結果了。
  2. 大約 70幾名。

優化3:

距離比賽結束還有一晚上,感覺拿獎是沒有希望了。但是突然來了興趣,想要在進步下。而且今天才知道原來每天可以提測 8 次。。
1. 通過 gstatck 分析發現程序運行的時候好多鎖等待。由於時間有限,我只敢改自己寫的存儲部分。Storage write() 的是排它鎖。但是實際上寫不同的文件的時候完全可以不鎖,於是趕緊實現分段鎖存儲。實現完分段鎖之後已經是夜裏12點了。這個時候在提測等待結果肯定是來不及了。於是草草提測了。接着優化。
2. 結果 升到 60 幾名了

優化4:

寫分段鎖了。讀呢能不能優化下呢?感謝 c++11 的atomic和linux pread()可以原子的隨機讀文件。
1. 於是讀開始實現無鎖化,經過分析,Storage read()的時候只有讀內存的時候才怕和write()衝突,讀文件的時候 pread() 可以併發讀。
於是從 Storage讀的時候先判斷偏移量(atomic_offset類型的)如果要讀內存則上鎖讀內存部分。如果不讀內存,則直接從文件無鎖的讀。。
這次代碼寫完已經半夜2點了。上次的提測應該結束了。看了下,名詞上升到 60 幾名了。趕緊在提測。因爲寫的倉促,提測完之後,在源環境上也開始了穩定性測試。就這樣到半夜4點多終於出成績了。
2. 結果: 升到了 55名。

優化5:

  1. 初賽的時候看他們的經驗說是有時候看運氣。我到了這個時候也是沒有辦法了。到第二天10點前還有6個小時。於是定了鬧鐘2小時一次提測。每次都是把參數改的很激進。可惜並沒有出現什麼好運。名詞定格在了 55。
  2. 結果:無變化。
    最終排名

總結:

收穫:

  1. 第二賽季感覺還是挺不錯的,畢竟沒有學環境學好久。最後一晚的感覺來了,使名詞提升了10幾名還是挺爽的,好久沒有這麼酣暢淋漓的寫代碼了。不過不能站在阿里的平臺上答辯還是有那麼點遺憾。
  2. 無鎖化的性能提升還真是不小的。

學到的技術:

等待頭幾名大佬答辯分享比賽經驗了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章