爲了簡化UI工做併爲運維人員提供一種更加靈活的資源查詢方式,ZStack在2.6版本中推出了首個面向IaaS的查詢語言 —— ZStack Query Language,簡稱ZQL。程序員
背景web
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;
經過start、limit參數實現分頁查詢,相似SQL的limit和offset。
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仍然包含一些缺陷:
隨着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字段降序排序。
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進行在加強,使其對全部查詢條件能夠進行提示和補全。