1. 程式人生 > >揭密首個面向IaaS的查詢語言:ZStack Query Language(ZQL)

揭密首個面向IaaS的查詢語言:ZStack Query Language(ZQL)

為了簡化UI工作併為運維人員提供一種更加靈活的資源查詢方式,ZStack在2.6版本中推出了首個面向IaaS的查詢語言 —— ZStack Query Language,簡稱ZQL。

背景

IaaS管理著海量的資料中心資源,如何對這些資源進行靈活快速的查詢是運維人員面臨的一個難題。在以往的IaaS軟體中,往往只對單個資源的某些欄位提供有限的API查詢支援,例如可以通過虛擬機器的IP欄位查詢,這不足夠也不靈活。運維人員在做複雜查詢時往往得繞開IaaS軟體直接查詢其後端資料庫,這既要求運維人員要了解IaaS資源的內部關係,又帶來了資料庫誤操作的風險。

從ZStack正式釋出的第一個版本ZStack0.6開始,我們就致力在API層面提供跟資料庫級別的查詢功能,ZStack的每個資源都包含一個Query API,能夠通過資源的自身欄位以及資源的關聯資源欄位進行查詢。例如

QueryVmInstance name~=web-vm state=Running

這裡查詢所有名字包含web-vm字串,正在執行中的VM。又例如

QueryVmInstance vmNics.eip.vip.ip='22.22.22.22'

EIP是虛擬機器的關聯資源,這裡查詢網絡卡綁定了EIP為22.22.22.22的虛擬機器。
Query API功能強大:

  • 使用者可以通過count引數返回滿足查詢條件資源數量,類似SQL的select count(*);
  • 通過fields引數指定要返回的欄位,類似SQL的select uuid,name from;
  • 通過sortBy、sortDirection引數指定排序的欄位和方向,類似SQL的order by;

Query API除了使用方便外,定義也很簡單。程式設計師在ZStack中增加了一種新資源後,只需要在程式碼中定義如下class:

@AutoQuery(replyClass = APIQueryVmInstanceReply.class, inventoryClass = VmInstanceInventory.class)
public class APIQueryVmInstanceMsg extends APIQueryMessage {
}

不需要寫任何實現,對應資源就具有了Query API。

ZStack內部包含一個Query Service負責處理所有資源的Query API,將他們翻譯成相應的SQL語句,在查詢條件中包含關聯資源條件時會生成對應的Join子句。

基於Query API, ZStack0.6版本就包含了超過400萬個單項查詢條件,組合查詢條件數為400萬的階乘。極大的方便了運維和複雜UI的設計。但Query API仍然包含一些缺陷:

  • 查詢條件之間只能是AND邏輯,無法執行OR邏輯,條件之間也無法加括號實現複雜邏輯組合
  • 不支援類似SQL中的sub query子句
  • 單個API只能查詢一種資源,查詢多種資源時需要呼叫多個API
  • 不支援跟監控系統的查詢語言整合

隨著ZStack UI的場景越來越豐富,Query API的限制使得UI端的工作越來越多,很多場景需要多次呼叫Query API進行資料組合。例如在監控Top 5頁面(用於檢測系統中CPU、記憶體、磁碟、網路等資源使用率最高5個資源的頁面),需要先採用Query API將虛擬機器、物理機等資源資訊查詢回來,再呼叫監控系統ZWatch的API查詢對應的監控資料。

Query API在未來的ZStack版本中會一直保留並維護,其後端實現已經從原來的Query Service替換成ZQL

ZStack Query Language

使用過著名issue管理系統JIRA的開發者都知道JIRA在進行高階搜尋的時候提供一種查詢語言JQL (JIRA Query Language),能夠使用一種類似SQL的DSL(Domain Specific Language)對JIRA中ticket的各個欄位進行高效的查詢。ZQL跟JQL類似,也是一種類似SQL的DSL,先來看一個例子:

query vminstance where name='webvm' or vmnics.ip='192.168.0.10' or (vmnics.eip = '172.20.100.100' and (cpuNum >= 8 or clusterUuid in ('fe13b725c80e45709f0414c266a80239','73ca1ca7603d454f8fa7f3bb57097f80')))

在這個簡單例子中,可以看到很多熟悉的SQL元素,例如and/or條件、括號、>=/in操作符等。ZQL可以看作SQL的一個子集外加ZStack根據自身需求進行的增強的查詢語言。它的基本結構如下:

QUERY queryTarget (WHERE condition+)? restrictBy? returnWith? groupBy? orderBy? limit? offset? filterBy? namedAs?

query關鍵詞
一條ZQL語句通常以query關鍵字開頭,queryTarget表示要查詢的資源或資源欄位的集合。前面的例子中vminstance代表虛擬機器,例如host代表物理機、zone代表區域,所有可被查詢的資源都有自己的名稱。如果不希望返回資源的所有欄位,只希望獲得資源的一個或多個欄位,實現類似SQL的select uuid,name from …的功能,可以在資源名後指定欄位名,多個欄位名用逗號隔離,例如:

query vminstance.uuid,name,cpuNum

該查詢返回所有虛擬機器的UUID、名稱以及CPU數量。

除了query關鍵字,查詢也能以count和sum關鍵字開頭,前者返回滿足查詢條件資源的總數,後者可以對資源的某個欄位進行求和。例如:

count vminstance where cpuNum > 8

返回系統中CPU數量超過8核的虛擬機器的總數。

sum vminstance.memorySize by name where cpuNum > 8 

用虛擬機器名字對CPU核數超過8個的虛擬機器進行分組,對它們的memorySize欄位進行求和。如果系統中有兩個10CPU8G的虛擬機器都名為webvm,則求和後返回webvm虛擬機器總記憶體使用數為16G。翻譯成SQL則為:

select sum(memorySize) from vminstance where cpuNum > 8 group by name

WHERE從句
ZQL的WHERE從句跟SQL的WHERE從句類似,支援and/or邏輯操作符、括號組合,條件的比較符支援=,!=,>,>=,<, <=, like, not like, is null, is not null, in, not in,查詢條件名為資源的欄位名。跟SQL不一樣的地方在於,ZQL的查詢條件可以是關聯資源的欄位,例如:

query vminstance where vmNics.eip.vip.ip='22.22.22.22'

注意where從句前無需寫類似SQL的from xx從句,因為query vminstance已經限定了被查詢的資源

這裡vip跟eip關聯,eip跟vmnic關聯,vmnic又跟vminstance關聯,則我們可以指定vip的IP作為查詢條件。這正是ZQL的強大之處,對於多個關聯資源的查詢,無需呼叫多次API在應用端組合資料,也無需像SQL一樣寫複雜的join從句,只需要像程式設計一樣通過點號(.)引用另一個資源即可, ZQL的翻譯器會自動將跨資源引用翻譯成對應的SQL join從句。

WHERE從句可以包含子查詢,類似於SQL的sub query功能,例如:

query vminstance where vmNics.l3NetworkUuid in (query l3network.uuid where ipRanges.networkCidr='10.1.0.0/24')

這裡找出所有CIRD為10.1.0.0/24的三層網路上執行的虛擬機器。

上面這個例子也可以用更簡單的方法實現:query vminstance where vmNics.l3network.ipRanges.networkCidr=’10.1.0.0/24’,這裡只是為了演示子查詢功能

GROUP BY、ORDER BY、LIMIT、OFFSET 子句
跟SQL一樣,ZQL支援GROUP BY、ORDER BY、LIMIT、OFFSET關鍵字,以實現分組、排序、分頁等功能。

GROUP BY:
通過虛擬機器的區域UUID和叢集UUID分組,統計各區域中各叢集中虛擬機器的數量。

count vminstance group by zoneUuid,clusterUuid

ORDER BY:
查詢所有虛擬機器,使用cpuNum欄位降序排序。

1.  query vminstance order by cpuNum desc

LIMIT、OFFSET:
使用limit和offset實現分頁:

query vminstance limit 100 offset 10

多資源查詢

對於多個資源的查詢,可以通過多條query查詢語句實現,語句之間使用分號分隔,例如:

query vminstance where name = 'my-vm';
query host where cpuNum > 10;
query zone;

則一次呼叫即可返回三種資源的查詢結果。由於返回的結果是一個map的JSON結構,為了方便獲得對應語句的查詢結果,可以使用named as關鍵字對查詢語句命名,例如:

query vminstance where name = 'my-vm' named as 'vm';
query host where cpuNum > 10 named as 'host';
query zone named as 'zone';

則在返回的JSON map中,可以通過vm、host、zone作為key獲得對應語句的查詢結果。

合併監控查詢 (return with從句)

在ZStack中使用了兩種資料庫:關係資料庫存放元資料,時序資料庫存放監控資料。由於不同資料庫查詢方式不一樣,在ZQL之前,使用者要查詢一個資源的監控資料,需要先通過Query API獲得該資源的元資料,再通過ZWatch的查詢API獲得其監控資料。例如要查詢一個名為webvm虛擬機器的CPU使用率監控資料,要執行如下API:

QueryVmInstance fields=uuid name=webvm

GetMetricData namespace=ZStack/VM metricName=CPUUsedUtilization labels=VMUuid=QueryVmInstance返回的UUID offsetAheadOfCurrentTime=60

ZQL通過return with子句解決這個問題。return with是一種外掛機制,它允許子系統 通過外掛將自身的查詢條件注入ZQL中,ZQL會先執行關係資料庫查詢,將滿足條件資源的原資料查詢出來後,再將資源的主鍵(primary key)作為輸入條件呼叫實現return with子句的外掛,最後將外掛的查詢結果一併返回給ZQL的呼叫者。

上述查詢虛擬機器監控資料的需求可以通過一條ZQL語句實現:

query vminstance.hostUuid,uuid where name = 'webvm' return with (zwatch{resultName='webvm-cpu',metricName='CPUAllUsedUtilization',offsetAheadOfCurrentTime=60})

返回:

{
    "results": [
        {
            "inventories": [
                {
                    "hostUuid": "f8271f58468b4281a212a43e530b5535",
                    "uuid": "05781209d24341ac84fc055ae71820ac"
                }
            ],
            "returnWith": {
                "webvm-cpu": [
                    {
                        "labels": {
                            "VMUuid": "05781209d24341ac84fc055ae71820ac"
                        },
                        "time": 1533280402,
                        "value": 0.8
                    },
                    {
                        "labels": {
                            "VMUuid": "05781209d24341ac84fc055ae71820ac"
                        },
                        "time": 1533280462,
                        "value": 0.8
                    }
                ]
            }
        }
    ],
    "success": true
}

這裡我們用一條ZQL語句中即返回了我們感興趣的元資料欄位:uuid和hostUuid,也返回了該虛擬機器的監控資料。細心的讀者已經注意到我們在ZWatch查詢欄位中指定了引數resultName=’webvm-cpu’,並且在返回的JSON map中監控資料的key也是webvm-cpu。跟named as關鍵字一樣,這是為了執行多條ZWatch查詢子句時方便檢索返回結果準備的。 ZStack UI使用非常複雜的ZQL查詢語句,例如在TOP 5頁面,一條ZQL查詢包含多達13個ZWatch查詢:

ZQLQuery zql="query vmInstance.uuid,name where zoneUuid='89e148fb667c404dbc5309a2e956fa28' and hypervisorType='KVM' and type='UserVm' and state='Running' return with (zwatch{resultName='CPUAllUsedUtilization',metricName='CPUAllUsedUtilization',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid\")',functions='top(num=5)'},zwatch{resultName='MemoryUsedInPercent',metricName='MemoryUsedInPercent',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid\")',functions='top(num=5)'},zwatch{resultName='MemoryFreeInPercent',metricName='MemoryFreeInPercent',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid\")',functions='top(num=5)'},zwatch{resultName='DiskAllReadOps',metricName='DiskAllReadOps',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid\")',functions='top(num=5)'},zwatch{resultName='DiskAllWriteOps',metricName='DiskAllWriteOps',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid\")',functions='top(num=5)'},zwatch{resultName='DiskAllReadBytes',metricName='DiskAllReadBytes',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid\")',functions='top(num=5)'},zwatch{resultName='DiskAllWriteBytes',metricName='DiskAllWriteBytes',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid\")',functions='top(num=5)'},zwatch{resultName='NetworkOutBytes',metricName='NetworkOutBytes',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid,NetworkDeviceLetter\")',functions='top(num=5)'},zwatch{resultName='NetworkInBytes',metricName='NetworkInBytes',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid,NetworkDeviceLetter\")',functions='top(num=5)'},zwatch{resultName='NetworkOutPackets',metricName='NetworkOutPackets',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid,NetworkDeviceLetter\")',functions='top(num=5)'},zwatch{resultName='NetworkInPackets',metricName='NetworkInPackets',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid,NetworkDeviceLetter\")',functions='top(num=5)'},zwatch{resultName='NetworkOutErrors',metricName='NetworkOutErrors',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid,NetworkDeviceLetter\")',functions='top(num=5)'},zwatch{resultName='NetworkInErrors',metricName='NetworkInErrors',offsetAheadOfCurrentTime=60,period=6,functions='average(groupBy=\"VMUuid,NetworkDeviceLetter\")',functions='top(num=5)'})"

上例是在ZStack CLI中執行時的例子,使用\對引號轉義

當資源特別多時,時序資料庫查詢效能可能成為多條ZWatch查詢的效能瓶頸,故return with會通過併發的方式執行外掛,預設併發度為10。例如上述例子中的13條ZWatch查詢會在10個執行緒中併發執行。使用者可以通過全域性配置zql.returnWith.concurrency更改併發度,例如

UpdateGlobalConfig category=query name=zql.returnWith.concurrency value=15

限制查詢 (restrict by從句)

ZStack的企業管理模組包含一個功能,可以對管理繫結某個區域,使得該管理員只能管理該區域內的資源,這就要求我們的ZQL對該管理員的查詢請求只返回與其繫結區區中的資源。

對於虛擬機器這樣的資源,其元資料本身就帶zoneUuid欄位用於標識所在區域。但對於eip這樣的資源,其元資料並無任何欄位表示區域屬性,區域屬性是由其所在的三層網路或繫結的虛擬機器確定的。例如要查詢某個區域內的eip,可以使用:
通過與虛擬機器的繫結關係查詢

query eip where vmNic.vmInstance.zoneUuid = '52fdad0a2c0d4131a6c0fc6c1b7141a6'


通過所在三層網路確定

query eip where vip.l3Network.zoneUuid = '52fdad0a2c0d4131a6c0fc6c1b7141a6'

無論那種方式,都需要呼叫者瞭解知道eip跟zone之間的關聯關係,這對API的使用者提出了非常苛刻的要求。ZQL通過restrict by從句解決這個問題。跟return with從句類似,restrict by也是個外掛框架,它允許其它服務通過外掛解讀restrict by從句中指定的條件,向生成的SQL中注入額外條件。例如上面的eip例子通過restrict by從句可以寫成:

query eip restrict by (zone.uuid='52fdad0a2c0d4131a6c0fc6c1b7141a6')

這裡呼叫者無需知道eip跟zone之間的邏輯關係,restrict by的路徑外掛會自動計算兩者的邏輯關係,並生成對應的SQL join從句。這裡eip既可以通過所在三層網路,也可以通過繫結的虛擬機器確定和區域的關係,外掛會自動計算路徑權重,使用權重最高的路徑生成SQL語句。

對於eip這個例子,外掛會選取通過三層網路的關係生成SQL語句。因為eip可能沒有跟虛擬機器繫結,但其一定處於某個三層網路,故三層網路這條路徑的權重更高。

restrict by支援多個條件,通過逗號分隔,多個條件之間是AND關係。

除了給ZQL呼叫者使用外,restrict by外掛在ZStack內部也被其它服務廣泛使用。例如賬號系統會通過外掛在普通賬戶呼叫ZQL的時候注入跟賬號關聯的SQL語句,使得普通賬號只能查詢到屬於該賬號的資源;又例如SNS服務會通過外掛注入語句讓ZQL只能查詢到非系統型別的接收端。

未來

ZQL為ZStack提供了一種類似SQL的IaaS查詢語言,並且能夠通過return with外掛框架跟其它非關係資料庫系統進行查詢整合。在未來的版本中我們還會繼續豐富其功能,目前有兩個方向:
filter by從句
雖然return with的ZWatch外掛能讓我們在查詢資源元資料的同時查詢其監控資料,但還不能將監控資料作為元資料的查詢條件,例如無法通過一條ZQL實現查詢某個叢集中所有CPU使用率超過90%的虛擬機器。這在未來版本中會通過filter by從句實現,例如:

query vminstance where clusterUuid = '33e26bd547d149fbb190436cc9aca824' filter by (zwatch{metricName='CPUAllUsedUtilization', offsetAheadOfCurrentTime=60, threshold>90})

同樣,filter by從句會實現成類似return with的外掛框架,用於整合非關係資料庫的查詢條件。
智慧CLI
ZQL有大量的從句,每個ZStack又有大量的可查詢欄位,目前ZStack CLI可以對Query API的可查詢欄位進行補全,但ZQL還暫時無法補全。未來版本中,我們會對CLI進行在增強,使其對所有查詢條件可以進行提示和補全。