揭密首个面向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的count();
通过fields参数指定要返回的字段,类似sql的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仍然包含一些缺陷:
查询条件之间只能是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' (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的uuid,name from ...的功能,可以在资源名后指定字段名,多个字段名用逗号隔离,例如:
query vminstance.uuid,name,cpunum
该查询返回所有虚拟机的uuid、名称以及cpu数量。
除了query关键字,查询也能以count和sum关键字开头,前者返回满足查询条件资源的总数,后者可以对资源的某个字段进行求和。例如:
vminstance where cpunum > 8
返回系统中cpu数量超过8核的虚拟机的总数。
sum vminstance.memorysize by name where cpunum > 8
用虚拟机名字对cpu核数超过8个的虚拟机进行分组,对它们的memorysize字段进行求和。如果系统中有两个10cpu8g的虚拟机都名为webvm,则求和后返回webvm虚拟机总内存使用数为16g。翻译成sql则为:
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分组,统计各区域中各集群中虚拟机的数量。
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' hypervisortype='kvm' type='uservm' 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进行在增强,使其对所有查询条件可以进行提示和补全。
欢迎大家在zstack官网下载页面
http://www.zstack.io/product_downloads/
进行免费的下载安装和试用。