天天看點

千萬級GPS資料接入案例分享

業務背景:

項目需要接入全省的GPS資料(資料量從一開始的1000w+ 到 現在的 4000w+),原始資料存于Datahub中。

GPS資料的使用場景:

1.地圖撒點(最新GPS點位)

2.統計車輛當日車輛和線上數

3.曆史軌迹(按時間查詢點位清單)

4.實時跟蹤(後端主動推送實時點位資訊)

5.報廢車輛報警 (根據報廢車輛資訊生成告警)

6.圍欄管控(根據GPS資訊與圍欄規則進行計算)

…………

業務分析:

GPS撒點:隻取最新的GPS資料,查詢頻率較高,條件也較為複雜,是以原始資料盡量要少,能夠直接滿足我們查詢需求。 針對業務場景,建立一張GPS_CURRENT表,表中隻存每輛車的最新GPS點位,根據車輛ID新增/更新,同時根據車輛ID關聯出區域,類型等資訊,以滿足我們撒點的查詢需求。

曆史軌迹: 由于每日的資料量在千萬級,使用傳統MYSQL資料庫,存在極大的存儲和查詢壓力,是以首先要考慮的是,我們的存儲極限在哪裡,與産品協商後,僅保留7天資料,而7天的資料也有2億+的資料量,單表還是比較吃力的,是以考慮到作分區表,較為理想的情況是每個分區資料在千萬級,5G以内; 軌迹的查詢條件比較清晰,根據車牌ID + 時間範圍作為查詢條件, 是以分區條件必然在這兩個字段中産生,由于我們需要定時按時間清理分區,使用時間作為分區字段,我們可以很友善的使用drop分區的方式達到清理曆史資料的效果,而車輛ID作為條件則不能,是以選用時間作為分區字段。

是以我們需要兩張表:

gps_current: 根據車輛ID進行插入或更新,用于撒點,線上統計等

gps_log:以時間為分區字段,用于軌迹查詢

方案:

version.1

// datahub 協同消費代碼
RecordEntry record = consumer.read(maxRetry);      
千萬級GPS資料接入案例分享

該方案是需求最平鋪直叙的表達,很快就遇到了問題 —— 消費速度無法跟上生産速度。

version.2

在version.1的基礎上,增加隊列機制,使gps_log插入變為batchInsert, gps_current的insertOnDuplicate 變為 batchInsertOnDuplicate。

千萬級GPS資料接入案例分享

在某一次版本後, 我們修改過濾條件, 由原先的隻處理危貨車 且 速度不為0的資料 變更為 處理危貨/包車/客車 資料,且去掉速度的過濾條件後, 終于,在某天這套方案的消費能力也開始捉襟見肘。

version.3

重新梳理業務後,我們把業務劃分為兩類: 需要較高的實時性的 和 能夠接收一定延時的

高實時性 低實時性
GPS撒點 軌迹
線上數 圍欄計算
實時跟蹤 報廢車輛告警

是以我們先将gps_current相關的消費服務獨立消費并拆分為單獨的服務部署,在拆分獨立服務後,gps_current相關的消費能力于原先等到了數十倍的提升。

千萬級GPS資料接入案例分享

原先我們gps消費是放在compute服務, 這個服務中有大量的datahub消費服務和計算服務,且存在一定線程池的濫用,通過arms監控可看到單個服務存在500+線程,大量線程處理等待狀态,導緻該服務内的線程效率較差,重構後解決了一部分問題。 是以最終我們拆分為3個訂閱,由于sockeServer是內建在compute服務(原先是單獨的服務,通過RPC調用,由于推送都是由compute服務發起的,且日均百萬次調用,考慮IO成本是以将兩服務合并)。

version.4

其實在version.3版本,服務的消費性能已經能滿足我們在相當長的時間内的性能需求了,但是存在一個不得不解決的問題 -> GPS時間上是亂序的, 即存在同一輛車,時間更早的GPS點反而更晚寫入datahub, 而我們是直接更新gps_curret表的,導緻存在最新點位時間向下更新的情況,是以需要加時間的判斷條件。

第一直覺上,通過mysql判斷肯定是不可取的,車輛最新的點位資訊扔到redis,通過redis判斷就能滿足要求; 而實際實施過程發現,直接使用redis是消費速度是無法滿足要求的,即使使用pipeline作批處理,與上一版本也存在較大的性能差。最後的方案是使用java内部的Map來做判斷,該方案與version.3消費性能略有下降,但仍是目前的生産速度5-8倍,能夠滿足性能上的要求。

總結:

1.使用批量處理

2.更快的IO速度  資料庫 -> redis -> java記憶體

3.取舍和拆分

實際上我們對于gps的處理,是不止上面4種方案的演進的;還有一些細節問題,在這裡簡單介紹下:

1.多節點datahub協同消費,但實際隻有一個節點處于忙碌狀态,其他節點都處于空閑狀态, 原因是datahub協同消費時,是根據shard進行負載均衡的,即同一shard,隻能配置設定到一個consumer上;當服務中consumer(多線程模拟多consumer) 數  >  shard數時,由于節點釋出有先後順序,後續啟動的節點幾乎是隻能“幹瞪眼”了。這裡需要說明兩點 (1)當consumer數 < shard數 時,一個consumer會被配置設定多個shard,是以為了保證多節點能夠正确的協同消費,盡量使單服務的consumer數 < shard數(2)以上結論隻基于目前datahub版本

2.在多線程batchInsertOnDuplicate時,發生了資料庫死鎖問題(具體原因就不分析了,篇幅較長,有興趣的可以去找相關材料閱讀), 我們的處理方式是,某一線程保證每個線程隻拿到; 是以,對隊列作了一層包裝,内部有多個隊列組成,根據車牌ID hash到不同隊列, 消費隊列時,要帶上shard參數(即每個shard對應的是一個實際的隊列),確定線程shard不會重複且與隊列數量對應即可。

3.在version.4中,多線程下使用hashMap會存線上程安全問題,而currentHashMap性能上又低于hashMap, 我們采用的方案是針對每個線程使用一個HashMap (同時用多隊列保證某一倆車隻會被同一線程消費)