雲生不知處隊伍
初賽
兩個指標:
- 爭取儘可能多的完成率。(三個 provider 的一共 1300+個線程,consumer 只有 1024 個線程,靠線程數分配即可做到 100%的完成率)
- 爭取儘量多的完成數(運用兩個指標,tps 和 cpu 使用率,因爲測試環境通過 sleep 來模擬請求處理,所以 CPU 使用率不會上升,只能依靠 tps 來作爲負載均衡的指標)
複賽
整體設計:
最大化平均值階段的的分,爭取做到寫入、查詢階段的總得分最大化。
思路:
寫入階段每個線程順序寫入,這個階段不做任何處理。
查詢階段,開啓一個線程對寫入數據進行處理,對 a 進行分桶,在每個桶內全局 t 有序,這樣就能靠 t 鎖定數據在那個桶內。
平均值階段:依靠查詢階段建立的分桶,進行平均值查找。
運行流程:
整體上我們三個階段的工作流程如下:
寫入。各線程各自寫入,t壓縮存儲在堆外內存中,並對t構建索引存儲在內存中,a和body分別寫入文件,每次filechannel寫入大小32kb以上,這樣不需要更多的buffer,即可最大化寫入速度。寫入過程中對a的分佈進行採樣,以保證分桶時各桶儘量平均。
分桶。在查詢階段開始之前,我們先完成分桶操作。首先對採樣的a進行排序,確定每個桶中a的範圍;然後創建多個讀線程,讀取上一階段各線程寫入的a文件,並按照全局t升序寫入到各個桶中;爲了追求更高速度,寫入的時候每個桶使用一個線程寫入。分桶時,爲了給平均值階段留下更多的內存,我們選擇在構建分桶的同時,將寫入階段存儲在內存中的t寫入磁盤,內存中僅留分桶後的t。分桶時,每個桶內都將部分a直接存儲在內存中,並根據桶內a的範圍進行一定壓縮。
查詢。對每個線程寫入的內容,分別找出tMin、tMax在文件中的位置,方法是根據t的索引進行二分確定文件位置,讀取t文件並解壓,得到準確的讀盤區間。從硬盤讀取該區間a文件的數據後逐個判斷是否符合aMin和aMax區間,對於符合範圍的讀取body並加入到結果中。讀取body時,儘量聚合臨近的body一起讀盤,可以提高一些分數。最後,將查詢結果按t排序並返回。
我們選擇先分桶再查詢,實際中可以選擇並行來提高查詢階段分數,我們爲了保證平均值階段開始前分桶一定已經完成,沒有選擇並行進行,這樣可能會犧牲查詢階段分數,但是可以保證平均值開始前一定完成分桶。
平均值。先根據aMin、aMax確定相關的桶,對於每個桶,分別找出tMin、tMax在文件中的位置,方法跟查詢階段類似。讀取後逐個判斷是否符合查詢條件,對於符合條件的計數並累加。
查詢和平均值階段,讀取a的時候,設置buffer大小爲1M,一次最多讀取1M數據,減少讀取次數,經測試發現,基本上對於所有查詢,每個桶內最多隻需要1次讀取。
這裏有一個優化空間,就是中間的桶,a是全部符合查詢條件的,可以進行優化,我們沒來得及搞。
原因和具體做法:
平均值階段最容易追求高分數,這一階段只與 a,t 有關,需要處理的數據量遠少於前兩個階段,故我們選擇追求該階段的分數最優。所以必須在查詢平均值階段開始前完成 a 的分桶,減少讀盤開銷。
每次讀盤要 20KB 以上纔是合理的???(關於磁盤性能的評測標準)
川渝一家親隊伍
初賽
核心思路:
找到一個可以量化各個節點排隊情況的數據指標,然後根據這個指標對請求權重進行調整:
指標需要滿足的條件:
- 若各個節點都未出現過排隊,則各個節點的指標值相同。
- 若某個節點過載 x%,則該指標會增大 x%
引入排隊係數的概念:
排隊係數 = 平均耗時/平均耗時差
複賽
大致思路:
按 T 對消息進行全局排序,並順序分成多個大塊,每個大塊內又對 A 進行排序,順序分成多個小塊,查詢或聚合時,先定位大塊,再定位小塊。
- 實時排序:使用一個大數組作爲緩衝區,然後通過 T%緩衝區數組長度的方式計算出數組下標,將 message 寫到對應的位置,每個位置上是一個消息鏈表,用於處理 T 字段相同的消息(這個地方排序有點意思)
- 當緩衝池中的消息達到一定量後,會從 minTime 對應的下標開始,取出一批排序好的消息,進行存儲。
- 順序的 A 字段和順序的 T 字段都進行了壓縮,A 字段的壓縮邏輯是:先對 A 進行差值存儲,若一個小塊兒內的所有差值的前 N 爲都是 0,則這一小塊內的所有差值都只存儲 8-N 位,同時將 N 記錄在小塊兒的索引對象裏面,最終在大塊兒爲 2W 左右時,A 字段平均可以壓縮到 6byte,壓縮率爲 75 左右;T 字段的壓縮邏輯是:先對 T 進行差值存儲,若當前差值和上一個差值一樣,則在重複計數區進行計數,對於連續相等的差值,計數區每滿 256 纔會新開闢 1byte,最後 T 字段被壓縮到 60M,壓縮率約爲 0.4%。
- 聚合統計
在對兩側的大塊兒進行處理時,先對加載的數據進行分場景預估,以此減少數據的加載量;對於中間的大塊,由於 T 都是滿足條件的,因此對於那些 A 也滿足條件的小塊,直接使用了預計算好的數據;對於 A 不完全滿足查詢條件的小塊,則需要進行數據加載,在加載小塊時,對於比較林靜的小塊,使用了合併加載的方式。
如何確定 IOPS 和 IO 速度之間的平衡:
- 由於中間的塊性價比比較高,因此我們肯定希望中間的塊越多越好,也就是塊越小越好,也就是每次 IO 加載的數據越少越好。
- IOPS 爲 1W,則一次 IO 至少耗時0.1ms,0.1ms 可以加在 20KB 的數據;也就是如果一次 IO 加載的數據小於 20Kb,則這次 IO 的性價比就不好。
cdeb 隊伍
初賽
思路:
將服務能力以單位時間內成功請求數對每個 service 的服務能力進行量化,採用試探的方式評估每個 service 的服務能力是否已經達到最大。試探的準則核心是嘗試增加或降低 service 的負載流量,根據改變負載後成功的請求數的變化,對 service 當前時刻的負載狀態進行評估(已滿,未滿)。通過不斷嘗試或者降低該 service 的負載併發量,最終使得負載併發量收斂到實際值併發量。
複賽:
核心思路:
核心優化方向是儘可能使得每次查詢的有效消息都分佈的更緊密和調整方案的 IO 請求數與讀文件總比例大小,使得 SSD 硬件性能IOPS 和讀速度比例更接近。
索引設計:
使用二級索引,每 16384 條消息分爲一個 block,每 512 條消息分爲一個 cell,block 之間保證 t 升序,block 內部的 cell 之間保證 a 升序。每次查詢時先根據查詢範圍 t 二分查找出有效的 block 範圍,再對 block 取餘內部根據查詢範圍 a 二分查找出有效的 cell 範圍。每個 bolck 內查找定位的 cell 是連續的,有利於文件存取。
delta 壓縮:
借鑑了時間戳壓縮算法 delta of delta,對 t 和 a 進行了壓縮。如下圖所示,在原本 delta of delta 算法基礎上,使用了 byte 對齊,每次壓縮都以 8 的倍數 bit 爲單位,解壓時直接能對 byte 進行操作,減少了移位操作。因爲線上使用 delta of delta 值壓縮效果差別不大,但是字段 a 壓縮前經過了排序,能保證 delta 爲非負數,因此在對 a 進行 delta 值壓縮時,可以優化符號位,進一步降低壓縮率。
總結:
方案的核心是索引的設計,主要優化方向是均衡算法的讀請求數量和讀的總量,儘量使得 SSD的 IOPS 和讀速度都儘量同時打滿。
你的 Java 寫的像 cxk 隊伍
初賽:
由於服務內響應時間爲指數分佈,如果發生過載,則會導致該分佈不再符合指數分佈。具體實現是,記錄從某一時刻後開始的請求,一定時間後,記錄已完成的請求的數量和響應時間。如果沒有過載(如左圖),則應符合截尾指數分佈。如果發生過載(如右圖),則應不符合截尾指數分佈。不妨假設沒有過載,根據統計數據,可以用最大似然估計,估計出模型的參數,再使用 KS 檢驗(KS 檢驗可以告訴我們觀測到的數據是否符合某一理論分佈)反過來確認假設是否成立。如果假設成立,則說明沒有過載,可以增加對該 Provider 的壓力;如果假設不成立,則說明發生過載,需要減少 Provider 的壓力。經過調參優化,最終成績爲 129.36 萬。
複賽:
對於第一階段的 put()操作,性能的瓶頸主要在於磁盤的連續 IO 速度,然而 CPU 的資源也十分緊張。爲了提高寫入的性能,我們一方面需要儘量減少磁盤 I/O 的字節數,另一方面需要減少 CPU 的開銷。一個通用的數值壓縮器,模仿了 UTF-8對數值進行變長編碼的方式,可以將 long 範圍內的整數壓縮爲 1~9 字節。數值的前導 0 越多,壓縮的效率越高。
對於每一個發送線程,我們存儲 threadXXX.zp.data 和 threadXXX.body.data 兩個文件(其中 XXXX 爲線程 ID 號)。threadXXX.zp.data 文件存儲的事經過數值壓縮器壓縮的(deltaT,a)數對,由於我們只存儲每一條數據中 t 相對上一條數據的 t 的差值,因此絕大多數情況下,每條記錄中 t 的存儲只需要1 字節,而 a 根據數據方位的不同會佔用不同的字節數,對於 48bit 左右的 a 值,需要 7 個字節進行存儲。threadXXX.body.data 文件中存儲的是完整的未經過處理的 body 數據,每條數據 34 字節。按這樣計算,所有線程寫入磁盤量大約爲 84GB,第一階段得分約爲 6800 分。
查詢階段的方法沒看太懂,不過看起來也是 t,a 兩個維度的數據存儲。
地表最菜戰隊伍
初賽:
思路:
加權輪詢,消費能力作爲權重(TPS—每秒處理事務數)
- 每次請求成功後給對應的 Provider 提權
- Goal = (int)(gyy.bz_elapsed.get(host)*8.33/(delta+1)+1)
goal 爲提權大小,bz_elapsed 爲平均 RTT,delta 爲該次請求 RTT.
由於統計 RTT 週期爲 120ms,因此乘以 8.33可以估計成 TPS
由於 delta 爲除數但是可能爲 0,因此加上 1
由於 delta 可能大於 bz_elapsed,導致結果爲 0,因此加上 1.- 每次請求成功都會對權值進行調整,對服務能力變化比較敏感。
兩個思路:
關於平均 RTT 的統計(響應時間百分比)
- 將響應時間升序排序
- 取前 95%,然後求均值;或者取前 50%,取最大值。
可以減少因排隊造成的統計響應時間偏大。
關於理論最優分配方案(已知響應時間RTT、最大併發數的前提下):
- 打滿 RTT 最低的 Provider
- 繼續往最低的 Provider 發送請求
- 使 Provider 的響應時間 RTT 排隊至第二低 RTT 相同
- 依次類推,即可使得 Provider 總體響應時間 RTT 最低。
複賽:
賽題分析:
t爲模擬時間戳,相鄰 t 差值不大,可用 delta of delta 壓縮
a 的分佈比 t 隨機,信息增益大,因此儘量將消息按 a 連續存儲
按 a 連續存儲的消息,相鄰 a 的差值減小,也可用 delta of delta 壓縮
按 a 連續存儲的消息,相鄰a 差值不小於 0,壓縮時可忽略符號位。
爲了通用性,body 不做處理。
三階段查詢時可以忽略 body,因此 t、a、body 分開存儲。
查詢時以 t 和 a 作爲條件,讀 t 的時候也需要讀啊,因此 t 和 a 按塊交替存儲
多個線程發送數據,且單個線程內按 t 升序排列,爲了減小讀取時的 IOPS 需要歸併排序。
核心思路:
按塊存儲
歸併排序多個線程消息後,可以得到全局按 t 升序排列的數據,相鄰一定個數的消息封裝成塊—block,再將相鄰一定個數的塊按 a 升序排列。
塊的結構:
- t 的範圍
- a 的範圍
- sum的結果
- 壓縮後的 t
- 壓縮後的 a
每次查詢時可以根據 t 的範圍、a 的範圍提前預知是否需要讀取該 block。
若完全不符合,不讀取若完全符合,也不讀取(直接使用 sum 結果)
合併讀取
先找出需要讀取的 block,將相鄰要讀的 block 一次讀取。“隔五“----合併讀取後依然是 IOPS 瓶頸,因此需要用吞吐量換 IOPS。
關於磁盤性能
總結
大佬的解題思路都是驚人的相似,這就是所謂的“大佬都強的一模一樣,我們卻菜的千奇百怪”吧。總結一下前五隊伍對於複賽的解題思路都主要圍繞三個點:
- 索引的設計,減少無效的 IO。
- 數據的壓縮,通過對時效數據的壓縮,使得文件佔用的存儲空間減少,減少存儲與讀取文件時的 IO 以及更大程度的利用不多的內存。
- 依據現有硬件情況,調整程序,使得文件的 IOPS 與吞吐量達到平衡,最大化磁盤的使用效率。