天天看點

觀測雲查詢語言DQL設計思路大解密DQL  要做哪些事情文法選擇DQL  為什麼是現在這個樣子DQL  如何解決一些具體問題

DQL 是專為 觀測雲(DataFlux)開發的語言,文法簡單,友善使用,可在 DataFlux Studio 進行資料查詢,也可通過 用戶端指令行 進行資料查詢。

在 DataFlux 中,我們用了多個不同的存儲引擎(目前主要是 InfluxDB 以及

ElasticSearch ),在這種混合存儲的場景下,将查詢語言統一起來,是非常有意義的:

  • DataFlux 是重查詢産品,所有的可觀測資料,都是通過查詢來擷取的​
  • 在具體的可觀測場景下,某個簡單的圖表,可能底層涉及多個不同的存儲引擎查找,如果分别查找,會導緻網絡 IO 劇增,同時也殃及頁面響應​
  • 在巨量觀測資料面前,為了防止意外的巨量資料查找,需在查詢語句上做保護​
  • 不同的存儲引擎,其查詢文法全然不同,無形中給開發人員帶來了額外的工作量​
  • 如果接入其它存儲引擎,前端開發又得學一遍查詢文法,曆史頁面也需要大量改造

基于這樣一個情況,提供統一的查詢語言,迫在眉睫。

對于新設計一門語言,這種事情是工程師所喜聞樂見的。對于新語言的開發,命名首當其沖,對 DataFlux 而言,順其自然,就是「DQL」。

DQL  要做哪些事情

在上面,我們提到統一查詢語言的意義,從中我們也能看到 DQL 要做的一些事情,但隻是泛泛而談。這裡,我們要大緻列舉下 DQL 的能力範圍:

所有DataFlux中的觀測資料,都能通過DQL查找

DQL查詢的傳回結構是一緻的

不管後端是 InflxuDB 還是 ElasticSearch 還是其它即将引入的存儲引擎,它們各自的查詢結果傳回,結構雖各不相同,但 DQL 需統一好給前端(這裡的「前端」包括但不限于 浏覽器/指令行等)

DQL需支援參數注入

對浏覽器端而言,某些查詢條件是不便于直接寫入 DQL 的,而 DQL 語句是通過類似 JSON API 發給後端,在這個 JSON 中,需提供各種不同的參數注入,以調整最終的 DQL 查詢行為,如額外的分組(group by)參數,查詢的時間範圍等,因為這些參數,實際上都是可以在 UI 調整的,而表格裡面的 DQL 是固定死的,不便于跟 UI 随動,隻能通過查詢注入

DQL文法要相對簡單且高度可擴充

對 DQL 而言,其主要職責是查詢(不排除後面提供更新文法),跟 SQL 相比,它隻需要提供 SELECT 即可,目前是沒有 INSERT/DELETE/UPDATE 功能,從這個角度而言,DQL 就簡化了不少。從另一個角度而言,因為底層的存儲引擎可能會有多個,對查詢功能而言,在文法上,不能對 DQL 做過多限制

DQL針對不同的資料做查詢限制

因為不同的資料(日志、時序、對象、APM等),其存儲政策不同,查詢政策也會有所差異,DQL 需分别對待。

确定了這些,接下來我們确定一下文法選擇。

文法選擇

我們最為熟悉的查詢語言莫過于 SQL,現如今已經成了行業标準,大家基本都能看懂 MySQL/PostgreSQL/SQLServer/Oracle 幾家的 SQL 語句,大同小異,稍有不同。查詢結構大緻如下:

SELECT column_name(s)     -- 要查什麼
FROM table_name           -- 從哪查
WHERE condition           -- 過濾條件是什麼
GROUP BY column_name(s)   -- 結果怎麼分組
ORDER BY column_name(s);  -- 結果怎麼排序      

先不論采用何種文法,這些基本要素,DQL 都必須滿足。對于目前 DataFlux 使用的查詢引擎,以 InflxuDB 為例,其基本查詢結構為:​

SELECT <field_key>[,<field_key>,<tag_key>]
  FROM <measurement_name>[,<measurement_name>]
  WHERE <conditional_expression>
  GROUP BY [* | <tag_key>[,<tag_key]]
  ORDER BY time [desc|asc]       

而 ElasticSearch 的查詢則很龐大(因為它足夠靈活),這裡以一個簡答的查詢為例,在 InflxuDB 中查詢一條最近的 CPU 資料,其查詢大概如下:

SELECT * FROM "cpu" WHERE "host" = '張三的電腦' ORDER BY "time" DESC LIMIT 1      

同等的 ElasticSearch 的查詢語句則大相庭徑,對于這麼複雜的查詢,如果沒有專門的 IDE,是很難寫正确的:

{
  "query": {
    "bool": {
      "must": [
      {
        "bool": {
          "should": [
          {
            "term": {
              "class": {
                "value": "cpu"
              }
            }
          }
          ]
        }
      },
      {
        "term": {
          "host": {
            "value": "張三的電腦"
          }
        }
      }
    }
    "size": 1,
    "sort": [
        {
            "last_update_time": {
                "missing": "_last",
                "order": "desc",
                "unmapped_type": "string"
            }
        }
    ]
  }
}      

綜合兩種查詢風格,我們可以看到:

  • InflxuDB 的查詢風格,跟 SQL 基本一緻,這也很容易了解,畢竟大家都很熟悉,寫起來差不多​
  • ElasticSearch 的查詢很臃腫,但極為靈活,對 ElasticSearch 本身的特性而言,這是正确的設計。雖然 ElasticSearch 也支援 SQL 形式的查詢,但其功能(相對)沒有 JSON 格式強大​
  • 對 DQL 而言,這兩種風格,似乎都不太合适​
  • 類 SQL 的文法肯定能滿足查詢需求,但其關鍵字太多(SELECT/FROM),寫起來繁瑣,另外容易讓人聯想到 insert/update 等文法,而這些在 DQL 設計之初就決定不予支援​
  • JSON 文法沒有必要,DQL 沒有這麼靈活的查詢需求(主要還是太難寫了)​

為此,我們看了下其它的查詢語言,比如 PromQL:

http_requests_total{job="apiserver", handler="/api/comments"}      

這裡的查詢語義為:查詢名額 http_requests_total,以 job="apiserver" AND handler="/api/comments" 為過濾條件。注意,這裡省去了 SELECT/FROM 這樣的文法,直接通過出現的位置來「暗示」其語義,翻譯成 SQL 就是:

SELECT * FROM http_requests_total WHERE `job`="apiserver" AND `handler`="/api/comments";      

在我們看來,PromQL 的文法,正是 DQL 喜歡的味道,它們要做的事情,其實異曲同工:隻專注查詢。

确定了文法選型,接下來的事情,就是如何實作這些文法了,文法的實作,有幾種常見的思路:​

  • 直接裸解析,暴力如 TCL 這種 C 編譯器,就是這種。當然 InflxuDB 的查詢語言處理,也是手寫的​
  • 通過專門的文法生成工具,如 ANTLR 或者 yacc/lex(bison/flex)

我們看了下 PromQL 的實作,決定采用 yacc,相比 ANTLR:

  • Golang 中内置了 yacc 實作(PromQL 就是用 golang 實作的),跟我們的技術棧契合很好​
  • yacc 的性能(記憶體消耗)相對更好(之前我們通過 ALTLR 做過 InfluxQL 的翻譯,性能不太理想)​
  • 最主要的是,我們的工程師相對更熟悉 yacc

DQL  為什麼是現在這個樣子

最終,DQL 的文法結構大概如下:

namespace::data-source:(target-clause)
  {where-clause}
  [time-expr]
  by-clause
  order-by-clause
  limit-clause      

從基本的文法結構中,可以看出,我們傾向于采用一些特殊符号來「暗示」高頻語義,而非用确定的單詞(如SELECT/FROM 等),因為它們極為常用,簡化其輸入是我們優先考慮的。但對于相對低頻的語義,我們還是選用了英文單詞,但還是一個原則:減少輸入,如将 GROUP BY,簡化成了 BY,但 ORDER BY 我們保持原樣,因為它相對不常用。

各個文法結構說明如下:

文法結構

namespace  :查詢的資料類型,類似于MySQL中的一個資料庫

這裡我們借鑒了 C++ 中 namespace 文法,如 std::string str1 = "hello",對DQL 而言,就是形如 object::HOST、metric::cpu、logging::nginx,看起來語義很契合。

在 DataFlux 中,截止目前,已經有如下幾種資料類型,故需要在文法層面,對查詢的資料做命名空間劃分,如:

  1. 時序(metirc/M)​
  2. 對象(object/O)​
  3. 日志(logging/L)​
  4. 事件(event/E)​
  5. 安全(security/S)​
  6. RUM(rum/R)​
  7. APM(tracing/T)​
  8. 自定義對象(custom_object/CO)​
  9. ...

為便于輸入,DQL 對各個命名空間,都做了别名。對于最常用的時序資料(M),甚至可以略去别名,預設就是 M 這個命名空間,進一步簡化了 DQL 的編寫。

 data-source    :基本查詢範圍,類似于資料庫

以對象為例,這裡填寫的是對象分類名(class),以時序為例,這裡填寫的是名額集名稱,以日志為例,這裡填寫的是來源(source),以此類推。

 target-clause    :查詢的字段清單,類似于表字段

如查詢 CPU 名額集的兩個字段:M::cpu:(usage_guest, usage_idle) LIMIT 1,表示在時序命名空間(M)中查找名額集為 cpu 的兩個名額(usage_guest, usage_idle),且隻查詢一條。

 where-clause   :以{}來表示過濾條件

如查詢主機 CPU 空閑率大于 90% 的機器:

cpu:(host) { usage_idle>90 }      

注意,這裡的過濾條件可以有多個,按照清單語義來處理,以 , 分割,它們之間是 AND 的關系:

# 如下三個語義是等價的

cpu:(host) { conditon1, condition2 }
cpu:(host) { conditon1 AND condition2 }
cpu:(host) { conditon1 && condition2 }      

既然有 AND 關系,那就有 OR 關系:​

# 如下兩個語義是等價的​

cpu:(host) { conditon1 || condition2 }
cpu:(host) { conditon1 OR condition2 }      

還可以用括号表示條件之間的各種組合:

cpu:(host) { conditon0 AND (conditon1 || condition2) }      

 time-expr    :時間過濾條件,其表達形式為[start:end:by-interval]

初步看來,這裡似乎有一點備援,比如 time-expr 本質上是一個 where-clause 和 by-clause 的合體,即既指定查詢的時間範圍,又指定時間範圍的分組。之是以将這個文法單獨擰出來,主要還是因為,在 DataFlux 的查詢中,基于時間的查找以及分組,使用頻率極高,幾乎所有的查詢都有涉及,為了将它們從 where-clause 和 by-clause 中「解放」出來,就單獨設計了這個文法單元,我們直接可以在這裡實作時間範圍過濾以及分組,屬于一種「快捷方式」。

另外,這裡的 start、end 支援多種時間類型的表示,如:

  • [10h:5m:1m] 表示 10 小時以前至 5 分鐘以前的時間範圍,将查詢到的資料,按照 1 分鐘的間隔進行分組。在終端手編寫 DQL 時,這樣指定時間範圍非常友善​
  • [1626401634:1626402634:1m] 表示兩個 UNIX 時間戳時間範圍,也是按照 1 分鐘的間隔分組。這樣做更便于大多數程式設計語言的處理​
  • [2019-01-01 12:13:14:5m:1w:1d] 表示自 2019-01-01 12:13:14 至一周(1w)前的時間範圍,将查詢到的資料,按照一天(1d)的間隔分組。這裡支援日期格式,主要便于通過 Web 前端的時間控件來指定時間

 by-clause   :分組文法(同 SQL 中的 GROUP BY)

 oder-by-clause  :排序文法

 limit-clause    :限制傳回數量間

DQL  如何解決一些具體問題

如何控制查詢的資料安全?

DataFlux 是一個準 SAAS 平台,簡而言之,就是多租戶平台。在這種情況下,不能因為某個意外的查詢,影響其他租戶的資料體驗。基于此,需要對不同的租戶,有獨立的查詢空間:

  • 每個獨立的工作空間,底層的存儲是邏輯隔離的,可以簡單了解為,不同租戶的資料,是存儲在不同的資料庫上。而單個 DQL 查詢,隻能在單個「資料庫」上執行查找,不存在「串庫」的情況​
  • 由于觀測資料量極大,極有可能是在指定資料查詢的時間範圍時,手抖了一下,造成底層存儲的巨量 IO 查詢,進而影響整個叢集的租戶。為此,在 DQL 的 HTTP 查詢接口上支援時間範圍的指定(預設 15 分鐘),即使沒有指定,DQL 本身也會檢查查詢的時間範圍是否超過系統的設定,這在很大程度上保護的底層的存儲系統

是如何輔助 DataFlux 前端開發的

前面提到,DQL 的 HTTP 接口是支援注入的,為便于前端實作各種複雜的資料觀測場景,DQL 額外支援如下這些查詢參數注入:

  • 傳回的最大點數控制:在一些密集繪圖的前端頁面上,巨量的資料傳回,可能導緻前端頁面卡死甚至奔潰。有了最大點數控制,就能杜絕這種情況​
  • 過濾條件注入:這個跟時間範圍的注入類似,主要應用在資料權限控制上​
  • 排序字段注入:在一些特定的資料頁面上,需要對傳回的資料,按照指定的字段來排序,比如,傳回的主機清單上,雖然預設按照主機名來排序(這個預設的 DQL 寫好了),單使用者可以選擇按照 CPU 或記憶體使用率來排序,此時就可以在 HTTP 請求參數上額外指定排序字段,覆寫預設 DQL 上的 ORDER BY 字段​
  • 禁止多字段傳回:在一些 UI 效果上,多列傳回是無法繪圖的,但為了避免 DQL 真的傳回了多列資料,可以對應的 UI 效果上,通過 HTTP 接口禁用多列查詢,這樣依賴, DQL 解析階段就能檢測到錯誤,非常便于日常的開發以及調試​
  • 額外的其它一些注入,主要也是便于實作資料展示效果,比如深度分頁、查詢的高亮顯示等等

「即時」的資料查詢效果

DataFlux 中的資料,大部分都是通過 DataKit 上傳的,為此,我們在 DataKit 中内置的 DQL 查詢終端。資料采集完後,稍後片刻(考慮到多級緩存、網絡傳輸延遲等因素)即可通過 DQL 查詢到剛剛上傳的資料,而不用打開 DataFlux 來檢視資料。另外,某些情況下,特别是在開發階段,DataFlux 前端可通過這個指令行終端,來排查一些資料問題

靈活的資料處理

主要展現在如下方面:

  • 友善不同的資料接入:如果有新的資料分類需要接入,擴充一個 namespace 即可。如果有新的存儲引擎接入,隻需要增加一套對應存儲引擎的查詢翻譯即可​
  • 可以對查詢到的資料,進行靈活的額外處理。假定 InfluxDB 不支援某個數學處理函數,DQL 查詢到資料後,可通過 Golang/Python 等,自定義實作即可。另外,還能跨服務實作資料的多級計算,比如将 DQL 查詢到的資料,送給 Function 處理

目前,DataFlux 中絕大多數的資料查詢,都是通過 DQL 來實作的,曆經近一年的開發疊代,DQL 日趨穩定,功能也日漸強大。随着 DataFlux 業務的不斷發展,DQL 也将面臨着更大的挑戰。