MySQL宕機?大數據驅動下的新零售,如何尋求存儲計算的最優解?

小 T 導讀:在TDengine還沒推出之前,「每人店」一直使用MySQL來存儲平臺各種硬件採集的數據。爲了提高數據展示效果,後臺再將採集到的願數據進行各個維度的計算,這樣的計算導致MySQL頻繁讀寫,爲了保證各個維度數據的一致性,採用了MySQL事務,性能大受影響,經常會出現事務死鎖,後切換至TDengine,各個維度的數據都不需要再計算,直接統計結果,省去了很多步驟。

1 使用場景簡介

1.1 業務場景介紹

每人店給客戶提供的服務之一就是店鋪的智能化管理,通過在各個門店安裝我們的各類智能硬件傳感設備,統計客流等信息並進行數據展示。一個非常常見的場景是進出店客戶數統計,並進行實時狀態顯示和歷史數據分析。智能設備採集會四個指標:採集數據的時間戳、1分鐘內進店人數、1分鐘內出店人數、滯留人數。通常我們每天的在線設備數在2.3萬個左右,每分鐘上報一次,每天有3312萬條數據,數據都是永久保存,因此數據量積累起來是非常大的。終端設備原始數據字段如下:

採集字段 數據類型 說明
data_time TIMESTAMP 數據採集時間
data_in INT 進店人數
data_out INT 出店人數
data_create BIGINT 數據寫入系統時間
data_delay BIGINT 延時毫秒

注:延時毫秒(Kafka收到數據時間戳-設備統計時間戳),主要用來統計設備的網絡穩定性和數據完整性

在TDengine還沒有推出之前,我們一直使用的是MySQL來存儲我們每人店平臺的各種硬件採集的數據。數據展示時需要按照時間範圍顯示客流量,比如查詢和顯示安踏在深圳市某一個門店過去一個月每小時的客流量情況。爲了提高數據展示效果和效率,我們後臺將採集到的設備數據進行各個時間維度的計算,比如從MySQL中定時讀出數據,按照小時、天、月統計後的結果再不斷寫入MySQL,這樣的做法導致MySQL頻繁地讀寫,同時還要設計複雜的按時間、商家等分庫分表的邏輯,維護起來是非常麻煩的。爲了保證各個維度數據的一致性,我們還使用到了MySQL事務,性能大受影響,經常會出現事務死鎖的發生。

直到TDengine的出現,爲之眼前一亮,我們只需要將接收到的每分鐘客流採集數據,按照設備分表存儲,並且給每個設備對應的表打上商家、門店編號、設備ID的標籤即可。各個時間維度的數據都不需要我們再計算後存儲一次了,直接通過TDengine的降採樣interval語法來統計結果,省去了我們很多的步驟。目前我們已經將之前存入MySQL的設備數據寫入TDengine,調整後的系統整體架構如下:

硬件設備採集的數據通過物聯網網關傳遞過來,寫入到Kafka(爲什麼用Kafka而不直接寫入到TDengine中,因爲原有架構已經上線,所以直接在計算模塊上增加TDengine的寫入)中,然後通過新增一個Flink計算模組來消費Kafka數據,並調用TDengine的寫入接口再將數據寫入到時序庫。上層再接我們的用戶交互APP以及其他數據服務API。

1.2 查詢示例

項目正式上線在2020的1月初,線上時已經導入TDengine的數據存儲規模大概在200G左右,每天的數據增量在16G左右。

應用系統的常規查詢在24QPS左右,基本都是按照時間範圍來查詢。業務場景的需求不會要求用戶時時刻刻對數據進行查詢,大部分用戶還是通過我們提供的API獲取數據,然後結合自身的內部系統進行數據分析。整體查詢的速度響應都在幾十毫秒級別。

前面介紹過,我們最典型的一個查詢場景是根據對某商戶的某個門店進行一段時間內的客流統計。在MySQL中需要對每個設備提前做好計算並存入結果表,在TDengine中,數據存入後就可以直接查詢,而且響應非常快。我們在TDengine中建立一個門進設備數據的超級表traffic_data,表結構如下:


 
  1. Field | Type | Length | Note |
  2. ==============================================================================
  3. data_time |TIMESTAMP | 8| |
  4. data_in |INT | 4| |
  5. data_out |INT | 4| |
  6. data_delay |BIGINT | 8| |
  7. merchant_id |BIGINT | 8|tag |
  8. instance_id |BIGINT | 8|tag |
  9. passageway_id |BIGINT | 8|tag |

其中data_time, data_in, data_out, data_delay我們在前面表格中有說明,這裏重點說一下三個tag(TDengine超級表標籤)。TDengine的設計思路是每個設備一張表,表中存儲設備採集過來的數據,表上可以打標籤,用來對設備進行描述。所有同類型設備的表會有相同的表結構,可以放到一張超級表下面,但這些子表會有不同的標籤值。

對於我們的場景而言,我們對每個門店統計設備建立一張表,並打上三個標籤商家信息merchant_id,門店信息instance_id,出入口信息passageway_id。在查詢時,我們往往需要統計一段時間內的客流狀況,比如查看過去一段時間內,某個門店按小時統計的進出客流量。

我們在沒有使用TDengine前,數據的查詢都依賴後臺的Flink計算程序,需要後臺的計算完成後才能展示給用戶,這樣會帶來一定的延時性,畢竟多了一層應用就多了一層出錯的可能。因此我們平臺在設計之初就確定了幾種固定的維度(5分鐘,半小時,小時,天)來存儲,這樣的設計雖然能滿足用戶的需求,但是這樣的設計讓用戶只能通過我們的確定的維度來查詢,假設某個用戶突然說需要一個我們平臺沒有設計的維度時就無法滿足了,要滿足就需要計算程序和存儲模塊一起修改,大大的增加了研發的工作量。而TDengine作爲時序數據庫的降採樣功能interval的使用讓我們就不需要擔心這樣的需求了。

下面簡單的介紹下使用TDengine查詢數據,就不去區分什麼簡單查詢和聚合查詢了,對於TDengine的使用也只能算是初入門不敢過多描述,以免貽笑大方。我們的數據主要是根據門店來劃分的,即通過設備與門店的綁定來確定門店的數據,所以我們TDengine存儲時超級表設計的標籤也是:商戶->門店->出入口。

這類查詢在TDengine中可以用一條SQL語句搞定:


 
  1. select sum(data_in) as data_in, sum(data_out) as data_out from traffic_data where ("$condition") and data_time >= "$start" and data_time <= "$end" interval($interval) group by instance_id order by data_time desc;

這個查詢是對從$start時刻到$end時刻中,各個門店的每個$interval的時間窗口內的進店人數、出店人數。其中where語句中的$condition還可對商家(merchant_id)或者指定的幾個店鋪(instance_id)進行過濾篩選,實現的Java代碼如下。


 
  1.  
  2. Connection connection = null;
  3. Statement ps = null;
  4. ResultSet resultSet = null;
  5. try {
  6. TDengineSettingDto setting = feignCoreService.getTDenginSetting(TDengineConf._TDENGINE_TYPE_DATA_TRAFFIC);
  7. if (setting != null) {
  8. DruidDataSource dataSource = TDengineDataSource.getDataTrafficDataSource(setting.getHost(), setting.getPort(), setting.getDatabase(), setting.getUsername(), setting.getPassword());
  9. connection = dataSource.getConnection();
  10. if (connection != null) {
  11. String condition = "";
  12. for (int i = 0; i < instanceIds.size(); i++) {
  13. long instanceId = instanceIds.get(i);
  14. if (i < (instanceIds.size() - 1)) {
  15. condition += " instance_id = " + instanceId + " or ";
  16. } else {
  17. condition += " instance_id = " + instanceId + " ";
  18. }
  19. }
  20. String interval = DataDimEnum.getInterval(dim);
  21. String sql = "select sum(data_in) as data_in, sum(data_out) as data_out from traffic_data where ("+condition+") and data_time >= "+start+" and data_time <= "+end+" interval("+interval+") group by instance_id order by data_time desc";
  22. logger.info("查詢listWhereInstanceAndTimeGroupByInstanceAndTime客流SQL: " + sql);
  23. ps = connection.createStatement();
  24. resultSet = ps.executeQuery(sql);
  25. if (resultSet != null) {
  26. List<TrafficInstanceDto> list = new ArrayList<TrafficInstanceDto>();
  27. while (resultSet.next()) {
  28. TrafficInstanceDto vo = new TrafficInstanceDto();
  29. vo.setDataTime(resultSet.getLong("ts"));
  30. vo.setInstanceId(resultSet.getLong("instance_id"));
  31. int dataIn = resultSet.getInt("data_in");
  32. if(dataIn<0){
  33. dataIn = 0;
  34. }
  35. int dataOut = resultSet.getInt("data_out");
  36. if(dataOut<0){
  37. dataOut = 0;
  38. }
  39. vo.setTrafficIn(dataIn);
  40. vo.setTrafficOut(dataOut);
  41. list.add(vo);
  42. }
  43. return list;
  44. }
  45. }
  46. }
  47. } catch (SQLException e) {
  48. logger.error("查詢TDengine門店客流數據失敗: {}, {}, {}, {}", instanceIds, start, end, dim, e);
  49. } finally {
  50. try {
  51. if (resultSet != null) {
  52. resultSet.close();
  53. }
  54. } catch (SQLException e) {
  55. logger.error("關閉TDengine連接池ResultSet失敗", e);
  56. }
  57. try {
  58. if (ps != null) {
  59. ps.close();
  60. }
  61. } catch (SQLException e) {
  62. logger.error("關閉TDengine連接池PreparedStatement失敗", e);
  63. }
  64. try {
  65. if (connection != null) {
  66. connection.close();
  67. }
  68. } catch (SQLException e) {
  69. logger.error("關閉TDengine連接池Connection失敗", e);
  70. }
  71. }

簡簡單單幾行代碼即可實現各種維度的數據查詢,是不是相當簡單,根本不需要修改任何架構。

但我們使用時也發現一個問題,使用TDengine做月數據查詢時interval(1n)數據會不對,因爲TDengine的月並非自然月,默認就是30天,而31天的月份數據會不全,所以我們對月份的查詢單獨做了處理,即先取到月份的開始日期和結束日期,然後再進行時間段sum,最後用程序來實現統計結果的展示,整體影響不大。

1.3  資源開銷對比

整個系統自上線來,併發量不算很大,但是對查詢的性能確實有顯著的提升,用兩張圖來對比下一目瞭然。 

這個是使用TDengine後服務器的資源使用情況,服務器上還允許了好幾個Java應用程序,一點壓力沒有,內存,CPU等都沒有壓力。

這個是之前相同數據負載情況下,但沒有使用TD的時候服務器的壓力:CPU,內存,LoadAverage等都一直居高不下,資源開銷的節省一目瞭然。

2 採用TDengine帶來的收益

使用TDengine後,確實在數據查詢和性能上給我帶了不少的驚喜。因爲剛開始一直抱着先試用下的心態,所以並沒有對整個平臺的架構進行大的調整,直接在計算程序上加一個模塊就完成了,這樣的修改也簡單,真正上線後發現確實強悍啊,1月份上線到現在也一直沒管,也沒有任何故障,之前使用MySQL時,經常數據庫故障,處理起來非常的痛苦。 

3 對社區的一些感想

一開始使用TDengine時,確實遇到不少的問題。最開始是在GitHub上看別人提的issue有沒有,後來官方弄了個微信羣,遇到問題可以直接在微信羣裏溝通,效率高了不少。在此特別感謝TDengine團隊的廖博士耐心指導和關懷,期間幾次遇到TDengine內存泄漏的問題,都是他遠程解決的。目前線上運行的1.6.3.0還是根據他提供的文件編譯的。 

後面也會考慮將 TDengine 擴展到更多業務和場景中,比如看看如何應用TDengine到人臉識別這塊業務中來。這塊目前TDengine倒是滿足,但是如果使用起來這個就龐大了,人臉識別將一個人視爲一個終端設備,但是目前的開源版應該支撐不了那麼大的量。

4 TDengine功能方面的期望與建議

通過這段時間的使用,對TD還是期望能開放集羣版本的開源,畢竟這樣性能等各方面都能更出色的體現出來。目前所在的行業是零售行業,只針對零售行業的使用存在的一些問題來說下:

1. 時間連貫性,零售行業的數據其實不是連貫的(有營業時間的劃分),所以我們使用時其實是將設備的數據分開存儲的,將設備所有的元數據存儲到一個表中,然後將營業時間內的數據進行再存儲到一個虛擬的設備中。但是營業時間外的數據又不可能丟棄。所以設備是會翻倍的。如果沿用TDengine的設計(一個設備一張表),這樣統計數據時有時會不準。

2. 人臉識別應用,之前試過將人臉識別的記錄存放到TD中,因爲用的是開源版,發現一個人臉用戶一張表,這個表的數量太龐大了,所以終止了人臉業務使用TDengine。

3. 熱區採集,一個設備採集的座標數據太大,一次採集就有8萬個座標點沒有合適的數組字段類型可以存儲,希望後面可以支持。

總體上講,引入TDengine給我們的系統帶來了不少優化的地方,後期我們還會針對性的刪減掉一些不再必要的模塊,進一步瘦身;也希望TDengine社區能持續繁榮,有不斷的優化功能出來。

作者介紹盧崇志,每人店研發經理,2014年加入深圳市曉舟科技有限公司研發部,工作至今,目前負責公司每人店產品的整體研發工作,包括WEB端與移動端的研發工作管理以及後端Java研發和架構設計。

公司介紹:深圳市曉舟科技有限公司是一家致力於爲千萬門店提供觸手可及的平等IT及大數據服務的高新科技企業,公司以專注、極致、口碑、快爲運營準則,旨在讓開店者可以輕鬆的通過web、app等方式輕鬆找到新零售升級路徑。這些路徑並非簡單的產品組合,而是通過各種有效創新軟硬件,從而完成商品的生產、流通與銷售過程的全系統改造,並通過運用大數據、人工智能等先進技術手段,重塑業態結構與生態圈,從而讓顧客享受更佳的購物體驗。產品正式銷售兩年多,公司系列產品已在全國上千個知名連鎖品牌五萬多家門店實現覆蓋,這些知名品牌包含安踏、卡門、森馬、依妙、天虹、重慶百貨等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章