天天看點

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

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

第一賽季:

簡介:

目的:借助于 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. 無鎖化的性能提升還真是不小的。

學到的技術:

等待頭幾名大佬答辯分享比賽經驗了。
           

繼續閱讀