首页 文章 精选 留言 我的

精选列表

搜索[编译原理],共10000篇文章
优秀的个人博客,低调大师

剖析Mooncake的代理原理,实现快速提效

原创 migor 得物技术 前言 Mooncake 平台得物统一的针对UI侧域服务侧联调Mock的一款工具产品,如下所示,在平台可以快速的创建各个项目产品的Mock多场景数据。前端可以通过对应的接口,改造不同的请求场景和请求数据,大大提高了前端的开发效率,缩短了联调时间。 为什么要做Mooncake? 在Mooncake平台之前,公司也有相应的Mock方案,基于Yapi或者自研的Mock, 但是由于数据配置复杂,或者接入项目要侵入业务代码等一些原因,导致现有的Mock在前端开发中的使用率不高,因此,基于这些痛点,我们进行了Mooncake平台的开发,做到了Mooncake平台接入方便不侵入业务代码,数据配置简单,增强了数据的可维护性。 1. Mooncake方案探讨 在最初的方案制定时,坚持两个原则:不做业务代码侵入,接入方便;数据可维护性,进行方案的探讨。 1.1本地Node服务器 基于本地JSON文件或者本地数据库为数据存储,启动一个本地的node服务,通过读写本地配置的接口URL匹配的JSON数据,给前端提供mock数据的服务和能力,大致的思路如图所示: 该方案类似于市场中的 json-server。 放弃原因: 数据维护为用户个人的行为,数据的复用性较差,项目之间配置重复性工作较多,无法满足数据的可维护性,不利于数据复用带来的提效 1.2 Yapi 方案 在Mooncake上线前,前后端用的mock数据都统一维护在Yapi平台上,对于前后端的开发确实起到了提效功能;但是Yapi本身的使用率并不高,大致的问题: a. 过于依赖服务端 b. 过于集中管理 c. 上手成本高,配置能力太多,交互过于复杂 1.3 Mooncake代理方案 由于 Xhr 和 fetch 在浏览器中挂载到 window上的,最初的 Mooncake 定位是为前端开发提供 mock能力的,基于这两点,我们采用劫持请求代理的方案。 那么到此,我们基本确定了基本的方案,上面我们说过制定方案的两个原则: 1. 不侵入业务代码; 2. 数据可维护性,制定了第一个方案。 通过html plugin方式和代理劫持,通过配置plugin的方式,不侵入业务代码; node服务会在项目目录生成一个数据维护文件夹,维护mock数据JSON文件,通过Gitlab代码仓库,保证数据的复用性; 通过UI配置平台,前端配置接口和接口数据。 上线之后,接入项目相对比较正常,反馈也比较好,但是无法在测试环境mock数据,无法给测试提供mock和UI验收的能力,于是进行了方案的改进。 优点: 能够满足前端开发维护开发的Mock数据; 测试回归测试场景、UI产品进行验收。 但是在使用了一段时间之后发现了一些问题: 项目的mock数据维护在Gitlab项目中,导致项目数据所占空间较大; 接口数据经常需要解决冲突问题; 开发需要维护两份mock数据。 于是有了现在的最终代理方案。 开发和测试环境,统一维护在Mooncake的线上服务,保证了数据的统一和复用性;同时通过HTML Plugin保证了Mooncake的无侵入性的接入;后续同时提供了Chrome插件,提供了项目的无侵入性的接入。 Mooncake产品整体,包括了线上配置平台、代理层,代理注入三个板块,这篇文章主要给大家介绍一下代理层的实现。在这之前,我们先看一下整体的代理逻辑思路。 今天我们主要介绍一下XHR和Fetch的代理思路。 2. XHR 2.1 简介 XMLHttpRequest一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿,也提供了这个接口,再后来W3C对他进行了标准化,提出了XMLHttpRequest标准。 通过查看Can I Use【1】可以查看各大浏览器对XMLHttpRequest的支持,入下图所示: 从图中可以看到: IE10/IE11部分支持,不支持xhr.responseType为json; Opera Mini 不支持xhr; 部分浏览器版本不支持timeout超时请求和responseType为blob。 2.2 如何使用 function sendRequest() { // 请求数据 const formData = new FormData(); formData.append('name', 'migor'); formData.append('role', 'member'); // 创建请求xhr const xhr = new XMLHttpRequest(); // 设置超时时间 xhr.timeout = 3000; // 设置返回数据类型 xhr.responseType = 'json'; // 打开一个请求 xhr.open('POST', url, true); // 处理回调 xhr.onload = function(e) { if(this.status === 200 || this.status === xxx) { alert(this.responseText) } } xhr.ontimeout = function(e){...}; xhr.onerror = function(e) {...} //发送数据 xhr.send(formData) } 2.3 实现劫持 最初的实现思路如下图所示,在用户发送请求之后,先从开发服务器拿数据,在返回数据之前,请求mooncake服务器,如果有数据,则返回mooncake服务器数据,如果没有数据,则返回开发服务器数据。 通过查看MDN, 我们可以看到对于XMLHttpRequest.response、XMLHttpRequest.responseText这些为只读属性,所以我们不能直接修改response和responseText的值,只能通过Object.defineProperty来实现对response的修改,实现代码如下: let origin = XMLHttpRequest.prototype.open XMLHttpRequest.prototype.open = function(...args) { // 插入open拦截 this.onResponse = function(res) { return res; } return origin.apply(this, args); } var accessor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "response") Object.defineProperty(XMLHttpRequest.prototype, "response", { get: function() { let response = accessor.get.call(this) // 在onResponse里修改你的response response = this.onResponse(response); return response; }, set: function(str) { return accessor.set.call(this, str); }, configurable: true }) 思路很清晰,那么我们来测试一下: const request = new XMLHttpRequest() request.onResponse = function(res) { return res + '添加的内容hello world'; } request.open(method, url, true) request.onreadystatechange = function() { if(request.readyState === XMLHttpRequest.DONE && request.status === 200) { console.log(request.response) } } request.send(); 可以看到返回结果已经变为处理后的结果,如下图所示: 现在我们已经完成了返回内容response的修改,回到我们最初的方案要求,需要在拿到内容之后进行请求mooncake服务器, 查看是否返回内容即可,那么实现get的异步即可。到这时,但是后面去实现getter异步之后,发现在返回处返回的对象是Promise, 思考之后,发现是因为readyState状态时间已经改变,但是异步任务还没结束,如果要等待结果,只能去手动触发readyState事件,那么这样就会多次触发console,同时我们发现在getter/setter中不支持async语法的,到此,这个方案出现了问题。 改进方案,如下图所示: 只需要实现XMLHttpRequest的异步钩子即可,这边使用了Github上成熟的方案,Ajax-hook,具体的理解如下: 主要实现思路如下: ob.hookAjax = function (funs) { //保存真正的XMLHttpRequest对象 window._ahrealxhr = window._ahrealxhr || XMLHttpRequest //1.覆盖全局XMLHttpRequest,代理对象 XMLHttpRequest = function () { //创建真正的XMLHttpRequest实例 this.xhr = new window._ahrealxhr; for (var attr in this.xhr) { var type = ""; try { type = typeof this.xhr[attr] } catch (e) {} if (type === "function") { //2.代理方法 this[attr] = hookfun(attr); } else { //3.代理属性 Object.defineProperty(this, attr, { get: getFactory(attr), set: setFactory(attr) }) } } } 一开始先保存了真正的XMLHttpRequest对象到一个全局对象,然后在注释1处,Ajax-hook覆盖了全局的XMLHttpRequest对象,这就是代理对象的具体实现。在代理对象内部,首先创建真正的XMLHttpRequest实例,记为xhr,然后遍历xhr所有属性和方法,在2处hookfun为xhr的每一个方法生成一个代理方法,在3处,通过defineProperty为每一个属性生成一个代理属性。下面我们重点看一看代理方法和代理属性的实现。 代理方法 function hookfun(fun) { return function () { var args = [].slice.call(arguments) //1.如果fun拦截函数存在,则先调用拦截函数 if (funs[fun] && funs[fun].call(this, args, this.xhr)) { return; } //2.调用真正的xhr方法 this.xhr[fun].apply(this.xhr, args); } } 属性修改 通过getFactory和setFactory生成setter、getter方法。 function getterFactory(attr) { return function () { var v = this.hasOwnProperty(attr + "_") ? this[attr + "_"] : this.xhr[attr] var attrGetterHook = (proxy[attr] || {})["getter"] return attrGetterHook && attrGetterHook(v, this) || v } } // Generate setter for attributes of xhr; by this we have an opportunity // to hookAjax event callbacks (eg: `onload`) of xhr; function setterFactory(attr) { return function (v) { var xhr = this.xhr var that = this var hook = proxy[attr] // hookAjax event callbacks such as `onload`、`onreadystatechange`... if (attr.substring(0, 2) === 'on') { that[attr + "_"] = v xhr[attr] = function (e) { e = configEvent(e, that) var ret = proxy[attr] && proxy[attr].call(that, xhr, e) ret || v.call(that, e) } } else { //If the attribute isn't writable, generate proxy attribute var attrSetterHook = (hook || {})["setter"] v = attrSetterHook && attrSetterHook(v, that) || v this[attr + "_"] = v try { // Not all attributes of xhr are writable(setter may undefined). xhr[attr] = v } catch (e) { } } } } 具体使用 import {proxy, unProxy} from "ajax-hook"; proxy({ //请求发起前进入 onRequest: async (config, handler) => { const mockRes = await new Promise((resolve, reject) => { // 建立mock请求 const mockXhr = new XMLHttpRequest(); mockXhr.open(method, url) mockXhr.onload = function cb(e) { if(!mockXhr.response) { resolve({code: -1}) } else { const mockResponse = JSON.parse(mockXhr.response) resolve(mockResponse) } } mockXhr.send() }) // 如果没有数据转开发服务器数据 if(mockRes.code === -1) { handler.next(config) } else { const responseMock = setResponseData(res) handler.resolve({ config: config, response: responseMock, status: 200, statusText: 'OK' }) } handler.next(config); }, onError: (err, handler) => { console.log(err.type) // handler.next(err) // 异常重新走开发服务请求 handler.resolve(err) }, //请求成功后进入 onResponse: (response, handler) => { console.log(response.response) handler.next(response) } }) 大致的思路就是这样的,服务的转发逻辑,例如开发环境、测试环境等的相关转发处理逻辑,以及数据的解析逻辑这里不在赘述。 3. Fetch 3.1 简介 Fetch可以理解为XMLHttpRequest的升级版,用于访问和操纵 HTTP 管道的一些具体部分,例如请求和响应。它还提供了一个全局fetch()fetch()方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。 3.2 用法 从服务器获取JSON数据 const data = { username: 'example' }; fetch('https://example.com/profile', { method: 'POST', // or 'PUT' headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }) .then(response => response.json()) .then(data => { console.log('Success:', data); }) .catch((error) => { console.error('Error:', error); }); 其中response内容的获取方法: *response.text() // 获取文本字符串 *response.json() // 获取json对象 *response.blob() // 获取二进制Blob对象 *response.formData() // 获取FormData表单对象 *response.arrayBuffer() // 获取二进制 arrayBuffer 对象 3.3 实现劫持 由于fetch支持异步原因,fetch的劫持相对来说比较简单,这里就不分析思路了,直接上代码: // 劫持全局对象的fetch const originFetch = window.fetch; window.fetch = await mooncakeFetchProxy(originFetch); async function mooncakeFetchProxy(originFetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>) { return async (input, init = {}) => { // 处理请求参数和服务器中转逻辑 let mockInput = getOnlineMockUrl() // 发送mock请求 let response = null; const mockFecth = await fetch(mockInput).then(res => { // 需要使用clone方法才能将返回内容拷贝出来 response = res.clone() return res.json() }).catch(() => { console.log('服务出错了') }) if(!!mockFecth && mockFecth.code !== -1 && response.status === 200) { // 请求成功才能进来 const { type, headers, url, statusText } = response const mockRes = parseRes(mockFetch) const data = parseResponseData(mockRes) const blob = new Blob([JSON.stringify(data)]) const newInit = { type, headers, url, statusText } // 返回mock数据 const res = new Response(blob, newInit); return res; } // mock请求失败或者没有mock接口 const origin = await originFetch(input, init).then(res => { return res; }) // 返回原始请求数据 return origin; } } 到这里,我们已经基本完成了Mooncake代理的实现,对于公司内各个项目的接入情况,数据维护状态,以及不同组织人员的使用情况,我们需要进行数据统计,因此我们在前期做了数据的埋点,来支撑Mooncake平台的数据统计分析,同时在后期的抓包核心功能实现中,我们也进行了埋点方案的改进,下面我们讲一下Mooncake平台的埋点方案。 4. 埋点方案探索 4.1 gif 由于最初统计接入Mooncake的项目和使用的接口的情况是在代理层进行的数据统计,只是简单的数据上报,所以埋点方案是用的gif方式,通过创建一个Image标签元素,在响应的时候给src赋值,做一次请求。 优点: 性能上比较好(简单请求,不用多一步预检请求的时间消耗) 跨域比较友好(天然支持跨域) 不需要响应 4.2 sendBeacon 在做抓包方案的时候,需要上传更复杂的数据,此时Gif方案以及不适合了,因此想到了个Navigator.sendBeacon(),查看一下支持的浏览器。 发现在Chrome浏览器中基本支持sendBeacon,查看一下MDN的介绍: Image的优点兼具 写法更简单 在浏览器空闲的时候发请求,更彻底的异步,不阻塞页面的刷新/跳转等。 Beacon是非阻塞请求,不需要响应 在上线之后,数据上传陆续接收到不同的问题反馈,主要是集中在两点,一个是数据长度过长,导致请求失败;另外一个是在低版本的Chrome浏览器中无法发送请求。针对第一个问题,查询发现因为sendBeacon支持的数据长度最多是64kb,第二个问题是在低版本浏览器中无法发送请求。 4.3 XMLHttpRequest 由于我们抓包发送的数据可能存在数据量较大,因此最后我们改用了XMLHttpRequest,由于Mooncake平台在抓包过程中功能单一,所以页面不存在跳转之类的操作,完全能满足要求。 5. 总结 市场上的Mock方案相对较多,得物前端Mooncake平台作为UI测的联调提效工具,通过线上配置平台、代理层,代理注入三层的实现,实现了数据可视化配置和数据转发,提升了前端的联调效率。核心代理层基于Proxy对XHR和Fetch进行请求转发,实现了Mock接口数据的获取,完成了Mock数据和服务端数据的自由切换。 目前Mooncake通过Mock能力积累了大量的前端和客户端用户,后续我们计划为服务端提供接口文档的管理能力,方便后端维护,以及同步前端接口文档,并基于现有场景组为测试通过自动化用例生成和回归测试的能力,从而打通前后端以及测试的整个链路环节,提升开发效率。 参考链接: https://caniuse.com/?search=XMLHttpRequest *文/migor 关注得物技术,每周一三五晚18:30更新技术干货 要是觉得文章对你有帮助的话,欢迎评论转发点赞~

优秀的个人博客,低调大师

jedis 连接池 jedisPool 的实现原理浅析

前言 jedisPool 是基于 Apache-commons-pool2 的抽象对象池(objectPool)技术实现的,在 jedisPool 中,一个连接即一个 jedisObject 对象实例。jedisPool 的核心也就是:根据设置的 poolConfig ,围绕连接池容器 LinkedBlockingDeque ,在相关动作中,做增删的逻辑。所以下面主要阐述关键配置和关键方法的行为,基本可以窥探 jedisPool 的实现全貌。 使用示例 /** * @author : kl (http://kailing.pub) * @since : 2022-04-26 12:37 */ public class KLMain1 { public static void main(String[] args) { JedisPoolConfig config = new JedisPoolConfig(); config.setMinIdle(10); JedisPool pool = new JedisPool(config,Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT); try (Jedis jedis = pool.getResource()) { jedis.set("foo", "kl"); System.out.println(jedis.get("foo")); } } } 关键 config 概述 jedis 连接池配置类为 GenericObjectPoolConfig ,以及子类 JedisPoolConfig,一般用子类 JedisPoolConfig 构造连接池参数,它会默认初始化管理 redis 连接生命周期的一些配置。 池子容量 控制池子大小,以及配置获取连接时的策略 名称 默认值 概要说明 maxTotal 8 最大连接数 maxIdle 8 最大空闲连接数 minIdle 0 最小空闲连接数 lifo true 归还连接时的策略,默认放回队列头部 blockWhenExhausted true 获取连接时,当连接池空了,是否阻塞等待连接 maxWaitDuration -1 只有 blockWhenExhausted = true 时才生效,表示阻塞等待多久。-1 表示一直阻塞,直到获取连接 testOnCreate false 创建连接时是否检测连接有效性,连接无效返回 null testOnBorrow false 租借连接时是否检测连接有效性,无效连接会直接销魂,并尝试继续获取 testOnReturn false 归还连接时是否检测连接有效性,无效链接会直接销毁,并创建新的连接归还 生命周期 控制是否启用名称为 commons-pool-evictor 的线程,用于空闲连接、无效连接的检测。以及配置连接检测的策略 名称 GenericObjectPoolConfig默认值 jedisPoolConfig 默认值 概要说明 testWhileIdle false true 开启驱逐线时,是否检测无效链接 durationBetweenEvictionRuns -1 30s 驱逐线程每次检测时间间隔,单位毫秒。-1表示不开启驱逐线程 numTestsPerEvictionRun 3 -1 每次检测多少链接,-1 表示检测所有 minEvictableIdleDuration 30min 60s 驱逐连接时,连接最小空闲存活时间 softMinEvictableIdleDuration -1 -1 软驱逐时最小空闲存活时间,-1 表示 intMax 关键逻辑 下面代码都在 GenericObjectPool 类中,这里例举关键的方法定义,以及每个方法直接使用到的配置项,看完这部分能够知道平时配置的参数到底是怎么工作的。以下贴的源码因篇幅原因,部分源码有所删减,只保留并突出关键逻辑部分。 方法定义 使用到的配置项 概要说明 PooledObject<T> create() maxTotal、testOnCreate 创建连接 T borrowObject(final long borrowMaxWaitMillis) blockWhenExhausted、maxWaitDuration、testOnBorrow 租借连接 void returnObject(final T obj) maxIdle、lifo、testOnReturn 返还连接 void evict() testWhileIdle、durationBetweenEvictionRuns、numTestsPerEvictionRun、minEvictableIdleDuration、softMinEvictableIdleDuration、minIdle 驱逐连接 void ensureMinIdle() lifo、minIdle 保持最小空闲连接 void addObject() lifo 添加一个连接到连接池 create private PooledObject<T> create() throws Exception { int localMaxTotal = getMaxTotal(); if (localMaxTotal < 0) { localMaxTotal = Integer.MAX_VALUE; } Boolean create = null; while (create == null) { synchronized (makeObjectCountLock) { final long newCreateCount = createCount.incrementAndGet(); if (newCreateCount > localMaxTotal) { // 池当前处于满负荷状态或正在制造足够的新对象以使其达到满负荷状态。 createCount.decrementAndGet(); if (makeObjectCount == 0) { // 没有正在进行的 makeObject() 调用,因此池已满。不要尝试创建新对象。 create = Boolean.FALSE; } else { // 正在进行的 makeObject() 调用可能会使池达到容量。这些调用也可能会失败,因此请等待它们完成,然后重新测试池是否已满负荷 makeObjectCountLock.wait(localMaxWaitTimeMillis); } } else { // The pool is not at capacity. Create a new object. makeObjectCount++; create = Boolean.TRUE; } } // Do not block more if maxWaitTimeMillis is set. if (create == null && (localMaxWaitTimeMillis > 0 && System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis)) { create = Boolean.FALSE; } } if (!create.booleanValue()) { return null; } final PooledObject<T> p; try { p = factory.makeObject(); if (getTestOnCreate() && !factory.validateObject(p)) { createCount.decrementAndGet(); return null; } } //...省略 return p; } 创建连接是一个最底层的方法,在租借连接、归还连接、保持空闲连接都会用到。创建连接前,会判断当前连接是否到达maxTotal ,如果到达 maxTotal 并且此时没有正在创建连接,则表示池子已满,返回 null。如果成功创建了连接,则根据 testOnCreate 的配置,如果 testOnCreate= true 则断连接是否可用,如果连接无效,依旧返回 null。 borrowObject public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception { assertOpen(); final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnBorrow() && (getNumIdle() < 2) && (getNumActive() > getMaxTotal() - 3)) { removeAbandoned(ac); } PooledObject<T> p = null; // Get local copy of current config so it is consistent for entire // method execution final boolean blockWhenExhausted = getBlockWhenExhausted(); boolean create; final long waitTimeMillis = System.currentTimeMillis(); while (p == null) { create = false; p = idleObjects.pollFirst(); if (p == null) { p = create(); if (p != null) { create = true; } } if (blockWhenExhausted) { if (p == null) { if (borrowMaxWaitDuration.isNegative()) { p = idleObjects.takeFirst(); } else { p = idleObjects.pollFirst(borrowMaxWaitDuration); } } if (p == null) { throw new NoSuchElementException(appendStats( "Timeout waiting for idle object, borrowMaxWaitDuration=" + borrowMaxWaitDuration)); } } else if (p == null) { throw new NoSuchElementException(appendStats("Pool exhausted")); } if (!p.allocate()) { p = null; } if (p != null) { try { factory.activateObject(p); } catch (final Exception e) { try { destroy(p, DestroyMode.NORMAL); } catch (final Exception e1) { // Ignore - activation failure is more important } p = null; if (create) { final NoSuchElementException nsee = new NoSuchElementException( appendStats("Unable to activate object")); nsee.initCause(e); throw nsee; } } if (p != null && getTestOnBorrow()) { boolean validate = false; Throwable validationThrowable = null; try { validate = factory.validateObject(p); } catch (final Throwable t) { PoolUtils.checkRethrow(t); validationThrowable = t; } if (!validate) { try { destroy(p, DestroyMode.NORMAL); destroyedByBorrowValidationCount.incrementAndGet(); } catch (final Exception e) { // Ignore - validation failure is more important } p = null; if (create) { final NoSuchElementException nsee = new NoSuchElementException( appendStats("Unable to validate object")); nsee.initCause(validationThrowable); throw nsee; } } } } } updateStatsBorrow(p, Duration.ofMillis(System.currentTimeMillis() - waitTimeMillis)); return p.getObject(); } 租借连接是从连接池容器链表的头部获取连接,如果为 null ,则调用 create() 创建连接,如果还是 null ,则根据 blockWhenExhausted=true 的配置决定是否等待连接, maxWaitDuration=-1 则一直等待,否则等待 maxWaitDuration 的时间,等待完还没获取到连接,则抛 【Timeout waiting for idle object】异常。如果 blockWhenExhausted=false ,当连接池无可用连接时立马抛连接池已满【Pool exhausted】的异常。成功拿到连接后,如果 testOnBorrow=true ,则继续判断连接是否有效,无效链接直接销毁,并重复上面的操作直到获取可用的连接。 returnObject public void returnObject(final T obj) { final PooledObject<T> p = getPooledObject(obj); if (p == null) { if (!isAbandonedConfig()) { throw new IllegalStateException( "Returned object not currently part of this pool"); } return; // Object was abandoned and removed } markReturningState(p); final Duration activeTime = p.getActiveDuration(); if (getTestOnReturn() && !factory.validateObject(p)) { try { destroy(p, DestroyMode.NORMAL); } catch (final Exception e) { swallowException(e); } try { ensureIdle(1, false); } catch (final Exception e) { swallowException(e); } updateStatsReturn(activeTime); return; } try { factory.passivateObject(p); } catch (final Exception e1) { swallowException(e1); try { destroy(p, DestroyMode.NORMAL); } catch (final Exception e) { swallowException(e); } try { ensureIdle(1, false); } catch (final Exception e) { swallowException(e); } updateStatsReturn(activeTime); return; } if (!p.deallocate()) { throw new IllegalStateException( "Object has already been returned to this pool or is invalid"); } final int maxIdleSave = getMaxIdle(); if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) { try { destroy(p, DestroyMode.NORMAL); } catch (final Exception e) { swallowException(e); } try { ensureIdle(1, false); } catch (final Exception e) { swallowException(e); } } else { if (getLifo()) { idleObjects.addFirst(p); } else { idleObjects.addLast(p); } if (isClosed()) { // Pool closed while object was being added to idle objects. // Make sure the returned object is destroyed rather than left // in the idle object pool (which would effectively be a leak) clear(); } } updateStatsReturn(activeTime); } 归还连接是把连接对象重新放回连接池容器。归还连接时,如果 testOnReturn=true ,则校验连接有效性,无效链接直接销毁并确保至少有一个空闲连接。继而判断 maxIdle 是否已满,已满则销毁当前连接,并确保至少有一个空闲连接,否则根据 lifo=true 将连接归还到链表的头部,否则归还到链表的尾部。这里用 lifo 来控制租借的连接是最新的连接还是最旧的连接,这里一般默认拿最新的连接就好。 evict public void evict() throws Exception { assertOpen(); if (!idleObjects.isEmpty()) { PooledObject<T> underTest = null; final EvictionPolicy<T> evictionPolicy = getEvictionPolicy(); synchronized (evictionLock) { final EvictionConfig evictionConfig = new EvictionConfig( getMinEvictableIdleDuration(), getSoftMinEvictableIdleDuration(), getMinIdle()); final boolean testWhileIdle = getTestWhileIdle(); for (int i = 0, m = getNumTests(); i < m; i++) { if (evictionIterator == null || !evictionIterator.hasNext()) { evictionIterator = new EvictionIterator(idleObjects); } if (!evictionIterator.hasNext()) { // Pool exhausted, nothing to do here return; } try { underTest = evictionIterator.next(); } catch (final NoSuchElementException nsee) { // Object was borrowed in another thread // Don't count this as an eviction test so reduce i; i--; evictionIterator = null; continue; } if (!underTest.startEvictionTest()) { // Object was borrowed in another thread // Don't count this as an eviction test so reduce i; i--; continue; } // User provided eviction policy could throw all sorts of // crazy exceptions. Protect against such an exception // killing the eviction thread. boolean evict; try { evict = evictionPolicy.evict(evictionConfig, underTest, idleObjects.size()); } catch (final Throwable t) { // Slightly convoluted as SwallowedExceptionListener // uses Exception rather than Throwable PoolUtils.checkRethrow(t); swallowException(new Exception(t)); // Don't evict on error conditions evict = false; } if (evict) { destroy(underTest, DestroyMode.NORMAL); destroyedByEvictorCount.incrementAndGet(); } else { if (testWhileIdle) { boolean active = false; try { factory.activateObject(underTest); active = true; } catch (final Exception e) { destroy(underTest, DestroyMode.NORMAL); destroyedByEvictorCount.incrementAndGet(); } if (active) { boolean validate = false; Throwable validationThrowable = null; try { validate = factory.validateObject(underTest); } catch (final Throwable t) { PoolUtils.checkRethrow(t); validationThrowable = t; } if (!validate) { destroy(underTest, DestroyMode.NORMAL); destroyedByEvictorCount.incrementAndGet(); if (validationThrowable != null) { if (validationThrowable instanceof RuntimeException) { throw (RuntimeException) validationThrowable; } throw (Error) validationThrowable; } } else { try { factory.passivateObject(underTest); } catch (final Exception e) { destroy(underTest, DestroyMode.NORMAL); destroyedByEvictorCount.incrementAndGet(); } } } } if (!underTest.endEvictionTest(idleObjects)) { // TODO - May need to add code here once additional // states are used } } } } } final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnMaintenance()) { removeAbandoned(ac); } } 这里贴出的代码不是全貌,只截取了关键逻辑。jedisPool 初始化时,会根据 durationBetweenEvictionRuns 配置启动一个名称为 commons-pool-evictor 的线程调度,如果 durationBetweenEvictionRuns=-1,则不启用,evict 的所有逻辑都不会运行,否则每隔 durationBetweenEvictionRuns 时间运行一次驱逐逻辑。numTestsPerEvictionRun 控制每次检测多少个连接。每次检测当前连接是否该被驱逐,有如下两个策略,满足其一即可: 策略A:配置的 softMinEvictableIdleDuration 小于当前连接的空闲时间【并且】配置的 minIdle 小于空闲连接总数 策略B:配置的 minEvictableIdleDuration 小于当前连接的空闲时间 一般默认参数下,不会触发策略A。很容易触发策略B。如果当前连接被驱逐,则继续下个连接判断。否则如果 testWhileIdle=true ,继续判断当前连接的有效性,无效链接会销毁并剔除连接池。一轮检测结束后,会调用 ensureMinIdle(),确保连接池中的最小空闲连接。 ensureMinIdle private void ensureIdle(final int idleCount, final boolean always) throws Exception { if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) { return; } while (idleObjects.size() < idleCount) { final PooledObject<T> p = create(); if (p == null) { // Can't create objects, no reason to think another call to // create will work. Give up. break; } if (getLifo()) { idleObjects.addFirst(p); } else { idleObjects.addLast(p); } } if (isClosed()) { // Pool closed while object was being added to idle objects. // Make sure the returned object is destroyed rather than left // in the idle object pool (which would effectively be a leak) clear(); } } ensureMinIdle 最终会调用 ensureIdle() 方法,idleCount 入参即为配置的 minIdle 。这里会根据当前的空闲连接和传入的 idleCount 比较,直到连接连接池的空闲连接达到 idelCount 值。 addObject public void addObject() throws Exception { assertOpen(); if (factory == null) { throw new IllegalStateException("Cannot add objects without a factory."); } addIdleObject(create()); } 创建一个连接并添加到连接池,这个方法一般用于连接池初始化。比如连接池提前预热。 结语 大多数人一般只关注连接池容量的配置,忽略了连接生命周期的配置 包括我,之前一直有一个错误的认知,认为连接池会确保 minIdle 值,只要当前申请的连接小于 minIdle 连接池就不会伸缩创建新连接。直到看了源码才发现,只要启用了驱逐连接线程,只要空闲存活时间大于配置的值 minEvictableIdleDuration 都会被驱逐,然后又会根据 minIdle 重建新的连接。又因为 jedisPoolConfig 的 minEvictableIdleDuration 默认为 60s,当 minIdle 配置的远大于实际需要时,因为默认连接获取策略为 lifo(总是拿最新的连接)会导致总是拿到新创建连接,又销毁又继续创建的循环中。当突然流量增大时,连接池中不一定保有 minIdle 的连接数,突然需要创建大量连接,势必会对应用造成一定影响。这里建议启用了驱逐线程时,minEvictableIdleDuration 的值稍微设置的大一点,或者直接 -1,让已经创建的连接一直保活。

优秀的个人博客,低调大师

每日一博 | StarRocks 技术内幕:查询原理浅析

一条查询 SQL 在关系型分布式数据库中的处理,通常需要经过 3 大步骤: 1. 将 SQL 文本转换成一个 “最佳的”分布式物理执行计划 2. 将执行计划调度到计算节点 3. 计算节点执行具体的物理执行计划 本文将详细解释在 StarRocks 中如何完成一条查询 SQL 的处理。 首先来了解 StarRocks 中的基本概念: FE: 负责查询解析,查询优化,查询调度和元数据管理 BE: 负责查询执行和数据存储 #01 从 SQL 文本到执行计划 — 从 SQL 文本到分布式物理执行计划, 在 StarRocks 中,需要经过以下 5 个步骤: 1. SQL Parse:将 SQL 文本转换成一个 AST(抽象语法树) 2. SQL Analyze:基于 AST 进行语法和语义分析 3. SQL Logical Plan:将 AST 转换成逻辑计划 4. SQL Optimize:基于关系代数、统计信息、Cost 模型,对逻辑计划进行重写、转换,选择出 Cost “最低” 的物理执行计划 5. 生成 Plan Fragment:将 Optimizer 选择的物理执行计划转换为 BE 可以直接执行的 Plan Fragment SQL Parse 图 1 Query Parse 的输入是 SQL 的 String 字符串,Query Parse 的输出是 Abstract Syntax Tree,每个节点都是一个 ParseNode 。 一个查询 SQL Parse 后生成一个 QueryStmt, 由 SelectList, FromClause, wherePredicate, GroupByClause, havingPredicate, OrderByElement, LimitElement 等组成,基本和 SQL 文本一一对应。 StarRocks 目前使用的 Parser 是 ANTLR4,语法规则定义的文件可在 GitHub 搜索StarRocks g4 获取。 SQL Analyze StarRocks 获取到 AST 后,接着会进行语法分析和语义分析,完成下面的工作: 1. 检查并绑定 Database, Table, Column 等元信息 2. SQL 的合法性检查:Where 中不能有 Grouping 操作, HLL 和 Bitmap 列不能 Sum 等 3. Table 和 Column 的别名处理 4. 函数参数的合法性检测: Sum的参数类型必须是数值类型,Lead 和 Lag 窗口函数第 2 和第 3 个参数必须常量等 5. 类型检查和类型转换:BIGINT 和 DECIMAL 比较,BIGINT 类型需要 Cast 成 DECIMAL SQL Analyze 的结果是一个有层级结构的 Relation,如图 2 所示,比如一个 From 子句对应一个 TableRelation, 一个子查询对应一个 SubqueryRelation。 SQL Logical Plan 图 2 接下来,StarRocks 会将 Relations 转化成一颗 Logical Plan Tree, 如图 2 所示,可以简单理解为每个集合操作都会对应一个 Logical Node。 SQLOptimize 图 3 StarRocks Optimizer 的输入是一棵逻辑计划树,输出是一棵 Cost “最低” 的分布式物理计划树。 一般 SQL 越复杂,Join 的表越多,数据量越大,Optimizer 的意义就越大,因为不同执行方式的性能差别可能有成百上千倍。StarRocks 优化器完全自研,主要基于 Cascades 和 ORCA 论文实现,并结合 StarRocks 执行器和调度器进行了深度定制,优化和创新。 它完整支持了 TPC-DS 99 条 SQL,实现了公共表达式复用,相关子查询重写,Lateral Join, CTE 复用,Join Rorder,Join 分布式执行策略选择,Global Runtime Filter 下推,低基数字典优化等重要功能和优化。 Logical Plan Rewrite 图 4 在正式进入 CBO 之前,StarRocks 会首先进行一系列 Logical Plan 的 Rewrite,Rewrite 阶段的 Rule 我们认为都会生成更优的 Logical Plan,主要的 Rewrite Rule 有下面这些: 各种表达式的重写和化简 列裁剪 谓词下推 Limit Merge, Limit 下推 聚合 Merge 等价谓词推导(常量传播) Outer Join 转 Inner Join 常量折叠 公共表达式复用 子查询重写 Lateral Join 化简 分区分桶裁剪 Empty Node 优化 Empty Union, Intersect, Except 裁剪 Intersect Reorder Count Distinct 相关聚合函数重写 CBO Transform 我们在 Logical Plan Rewrite 完成后,正式基于 Columbia 论文进行 CBO 优化,主要包括下面的优化: 多阶段聚合优化:普通聚合(count, sum, max, min 等)会拆分成两阶段,单个 Count Distinct 查询会拆分成三阶段或是四阶段。 Join 左右表调整:StarRocks 始终用右表构建 Hash 表,所以右表应该是小表,StarRocks 可以基于 cost 自动调整左右表顺序,也会自动把 Left Join 转 Right Join。 Join 多表 Reorder:多表 Join 如何选择出正确的 Join 顺序,是 CBO 优化器的核心。当 Join 表的数量小于等于 5 时,StarRocks 会基于 Join 交换律和结合律进行 Join Reorder,大于 5 时,StarRocks 会基于贪心算法和动态规划进行 Join Reorder。 Join 分布式执行选择:StarRocks 支持的分布式 Join 方式有 Broadcast、Shuffle、单边 Shuffle、Colocate、Replicated。StarRocks 会基于 Cost 估算和 Property Enforce 机制选择出 “最佳” 的 Join 分布式执行方式。 Push Down Aggregate to Join 物化视图选择与重写 图 5 如图 5 所示, 在 CBO 优化中,Logical Plan 会先转成 Memo 的数据结构。Memo 的中文含义是备忘录,所有的逻辑计划和物理计划都会记录在 Memo 中, Memo 就构成了整个搜索空间 。 然后如图 6 所示,StarRocks 应用各种 Rule 扩展搜索空间,并生成对应的物理执行计划,再基于统计信息和 Cost 估计从 Memo 中选择一组 Cost 最低的物理执行计划。 图 6 统计信息 和 Cost 估计 CBO 优化器好坏的关键之一是 Cost 估计是否准确,而 Cost 估计是否准确的关键点之一是统计信息是否收集及时准确。 StarRocks 目前支持表级别和列级别的统计信息,支持自动收集和手动收集两种方式。无论自动还是手动,都支持全量和抽样收集两种方式。 有了统计信息之后, StarRocks 就会基于统计信息进行 Cost 估算。StarRocks 估算 Cost 时会考虑 CPU、内存、网络、IO 等资源因子,每个资源因子会有不同的权重,每个执行算子的 Cost 计算公式都不太一样。 当你使用 StarRocks 发现 Join 左右表不合理、Join 分布式执行策略不合理时,可以参考 StarRocks CBO 使用文档收集统计信息。 生成 Plan fragment 图 7 StarRocks Optimizer 的输出是一棵分布式物理执行计划树,但并不能直接被 BE 节点执行,所以需要转换成 BE 可以直接执行的 PlanFragment。转换过程基本是个一一映射的过程。 #02 执行计划的调度 — 在生成查询的分布式 Plan 之后,FE 调度模块会负责 PlanFragment 的执行实例生成、PlanFragment 的调度、每个 BE 执行状态的管理、查询结果的接收。 图 8 有了分布式执行计划之后,我们需要解决下面的问题: 1. 哪个 BE 执行哪个 PlanFragment 2. 每个 Tablet 选择哪个副本去查询 3. 多个 PlanFragment 如何调度 StarRocks 会首先确认 Scan Operator 所在的 Fragment 在哪些 BE 节点执行,每个 Scan Operator 有需要访问的 Tablet 列表。然后对于每个 Tablet,StarRocks 会先选择版本匹配的、健康的、所在的 BE 状态正常的副本进行查询。在最终决定每个 Tablet 选择哪个副本查询时,采用的是随机方式,不过 StarRocks 会尽可能保证每个 BE 的请求均衡。假如我们有 10 个 BE、10 个 Tablet,最终调度的结果理论上就是每个 BE 负责 1 个 Tablet 的 Scan。 当确定包含 Scan 的 PlanFragment 由哪些 BE 节点执行后,其他的 PlanFragment 实例也会在 Scan 的 BE 节点上执行 (也可以通过参数选择其他 BE 节点 ),不过具体选择哪个 BE 是随机选取的。 当 FE 确定每个 PlanFragment 由哪个 BE 执行,每个 Tablet 查询哪个副本后,FE 就会将 PlanFragment 执行相关的参数通过 Thrift 的方式发送给 BE。 目前 FE 对多个 PlanFragment 调度的方式是 All At Once 的方式,是按照自顶向下的方式遍历 PlanFragment 树,将每个 PlanFragment 的执行信息发送给对应的 BE。 #03 执行计划的执行 — StarRocks 是通过 MPP 多机并行机制来充分利用多机的资源,通过 Pipeline 并行机制来充分利用单机上多核的资源,通过向量化执行来充分利用单核的资源,进而达到极致的查询性能。 MPP 多机并行执行 MPP 是大规模并行计算的简称,核心做法是将查询 Plan 拆分成很多可在单个节点上执行的计算实例,然后多个节点并行执行。每个节点不共享 CPU、内存、磁盘资源。MPP 数据库的查询性能可以随着集群的水平扩展而不断提升。 图 9 如图 9 所示,StarRocks 会将一个查询在逻辑上切分为多个 Query Fragment(查询片段),每个 Query Fragment 可以有一个或者多个 Fragment 执行实例,每个 Fragment 执行实例会被调度到集群某个 BE 上执行。一个 Fragment 可以包括一个或者多个 Operator(执行算子),图中的 Fragment 包括了Scan、Filter、Aggregate。每个 Fragment 可以有不同的并行度。 图 10 如图 10 所示,多个 Fragment 之间会以 Pipeline 的方式在内存中并行执行,而不是像批处理引擎那样 Stage By Stage 执行。Shuffle (数据重分布)操作是 MPP 数据库查询性能可以随着集群的水平扩展而不断提升的关键,也是实现高基数聚合和大表 Join 的关键。 Pipeline 单机并行执行 StarRocks 在 Fragment 和 Operator 之间引入了 Pipeline 的概念,一个 Pipeline 内的数据没有到达终点前不需要 Materialize,遇到需要 Materialize 的算子(Agg, Sort, Join),则需要拆分出一个新的 Pipeline,所以 1 个 Fragment 会对应多个 Pipeline。 图 11 如图 11 所示,一个 Pipeline 由多个 Operator 组成。第一个 Operator 是 Source Operator,负责产生数据,一般是 Scan 节点 和 Exchange 节点。最后一个 Operator 是 Sink Operator,负责物化或者消费数据。中间的 Operator 负责对数据进行 Transform。 图 12 那么 Pipeline 如何并行呢?答案是 Pipeline 和 Fragment 一样,可以生成多个实例,每个实例称为一个 Pipeline Driver。当一个 Pipeline 需要 N 个并行度去执行时,一个 Pipeline 就会生成 N 个 Pipeline Driver,如图 12 所示,并行度是 3,一个 Pipeline 就产生了 3个 Pipeline Driver。 图 13 如图 13 所示,一个 Pipeline 执行中,当前一个 Operator 可以产生数据,且后一个 Operator 可以消费数据时,Pipeline 的执行线程就会从前一个 Operator Pull 出数据,然后 Push 到后一个 Operator。每个 Pipeline 的执行状态是很清晰的,简单可以理解为有 Ready、Running、Blocked 等 3 种状态。当前面的 Operator 无法产生数据,或者后面的 Operator 不需要消费数据时,Pipeline 就会处于 Blocked 的状态。 图 14 如图 14 所示, Pipeline 并行执行框架的核心是实现一个用户态的协程调度,不再依赖操作系统的内核态线程调度,减少线程创建、线程销毁、线程上下文切换的成本。 在 Pipeline 并行执行框架中,StarRocks 会启动机器 CPU 核数个执行线程,每个执行线程会从一个多级反馈就绪队列中获取 Ready 状态的 Pipeline 去执行,同时会有一个全局 Poller 线程不断检查 Blocked 队列中的 Pipeline 是否解除了阻塞,可以变为 Ready 状态。如果可以变为了 Ready 状态,就可以把 Pipeline 从 阻塞队列移到多级反馈就绪队列中。 向量化执行 图 15、16 随着数据库执行的瓶颈逐渐从 IO 转移到 CPU,为了充分发挥 CPU 的执行性能,StarRocks 基于向量化技术重新实现了整个执行引擎,向量化执行引擎是为了充分利用单核 CPU 的能力。 向量化在实现上主要是算子和表达式的向量化,图 15 是算子向量化的示例,图 16 是表达式向量化的示例,算子和表达式向量化执行的核心是批量按列执行。相比于单行执行,批量执行可以有更少的虚函数调用,更少的分支判断;相比于按行执行,按列执行对 CPU Cache 更友好,更易于 SIMD 优化。 向量化执行不仅仅是数据库所有算子的向量化和表达式的向量化,而是一项巨大和复杂的性能优化工程,包括数据在磁盘、内存、网络中的按列组织,数据结构和算法的重新设计,内存管理的重新设计,SIMD 指令优化,CPU Cache 优化,C++ Level 优化等。经过努力,StarRocks 向量化执行引擎相比之前的按行执行,取得了整体 5 到 10 倍的性能提升。 每个算子和表达式具体如何实现、如何进行向量化,之后的文章会详细解释,本文不再赘述。 #04 总结 — 本文主要介绍了 StarRocks 如何完成一条查询 SQL 的处理: 1. 通过高效强大的 CBO 优化器生成最佳的分布式物理执行计划; 2. 通过查询调度器选择合适的数据副本,并将分布式物理执行计划调度到合适的计算节点进行计算; 3. 通过 MPP 分布式执行框架充分利用多机的资源,做到查询性能可以随着机器数量近似线性扩展; 4. 通过 Pipeline 并行执行框架充分利用多核资源,做到查询性能可以随着机器核数近似线性扩展; 5. 通过向量化执行引擎充分利用 CPU 单核资源,将单核执行性能做到极致。 作者 康凯森 | StarRocks 核心研发、StarRocks 查询团队负责人

优秀的个人博客,低调大师

全方位讲解 Nebula Graph 索引原理和使用

本文首发于 Nebula Graph Community 公众号 index not found?找不到索引?为什么我要创建 Nebula Graph 索引?什么时候要用到 Nebula Graph 原生索引?针对社区常见问题,本文旨在一文带大家搞清索引使用问题。 Nebula Graph 的索引其实和传统的关系型数据库中的索引很像,但是又有一些容易让人疑惑的区别。刚开始了解 Nebula 的同学会疑惑: 不清楚 Nebula Graph 图数据库中的索引到的是什么概念; 什么时候应该使用 Nebula Graph 索引; Nebula Graph 索引会影响写入性能吗?影响程度如何? 在这篇文章里,我们就把这些问题一一解决,方便大家更好地使用 Nebula Graph。 到底 Nebula Graph 索引是什么 简单来说,Nebula Graph 索引是用来且只用来针对纯属性条件出发查询场景的功能,它具有以下特性: 图游走(walk)查询中的属性条件过滤不需要它 纯属性条件出发查询(注:非采样情况)必须创建索引 纯属性条件出发查询 我们知道在传统关系型数据库中,索引是对表数据的一个或多个针对特定列重排序的副本,它用来加速特定列过滤条件的读查询并带来了额外的数据写入。简单来说,索引能起到加速的作用,但查询使用索引并非是必要的。 在 Nebula Graph 图数据库里,索引则是对点、边特定属性数据重排序的副本,用来提供纯属性条件出发查询。 以如下边的查询为例,该语句实现了从指定点边属性条件,而非点的 ID 出发的方式去获取图数据: #### 必须 Nebula Graph 索引存在的查询 # query 0 纯属性条件出发查询 LOOKUP ON tag1 WHERE col1 > 1 AND col2 == "foo" \ YIELD tag1.col1 as col1, tag1.col3 as col3; # query 1 纯属性条件出发查询 MATCH (v:player { name: 'Tim Duncan' })-->(v2:player) \ RETURN v2.player.name AS Name; 上边这两个纯属性条件出发查询就是字面意思的”根据指定的属性条件获取点或者边本身“ ,反面的例子则是给定了点的 ID。参考以下例子: #### 不基于索引的查询 # query 2, 从给定的点做的游走查询 vertex VID: "player100" GO FROM "player100" OVER follow REVERSELY \ YIELD src(edge) AS id | \ GO FROM $-.id OVER serve \ WHERE properties($^).age > 20 \ YIELD properties($^).name AS FriendOf, properties($$).name AS Team; # query 3, 从给定的点做的游走查询 vertex VID: "player101" 或者 "player102" MATCH (v:player { name: 'Tim Duncan' })--(v2) \ WHERE id(v2) IN ["player101", "player102"] \ RETURN v2.player.name AS Name; 我们仔细看前边的 query 1 和 query 3,尽管语句中条件都有针对 tag 为 player 的过滤条件 { name: 'Tim Duncan' },但一个需要依赖索引实现,一个不需要索引。具体的原因在这里 : query 3之中不需要索引,因为: 它会绕过 (v:player { name: 'Tim Duncan' }) 这种未给定 VID 的起点,从 v2 这样给定了 VID ["player101", "player102"] 的起点向外扩展,下一步再通过 GetNeighbors() 获得边的另一端的点,然后 GetVertices() 得到下一跳的 v,根据 v.player.name 过滤掉不要的数据; query 1 则不同,它因为没有任何给定的起点 VID: 只能从属性条件 { name: 'Tim Duncan' } 入手,在按照 name 排序的索引数据中先找到符合的点:IndexScan() 得到 v; 然后再从 v 做 GetNeighbors() 获得边的另一端 的 v2 ,在通过 GetVertices() 去获得下一跳 v2 中的数据; 其实,这里的关键就是在于是查询是否存在给定的顶点 ID(Vertex ID),下边两个查询的执行计划里更清晰地比较了他们的区别: 图注:query 1 的执行计划(需要索引); 图注:query 3 的执行计划(不需要索引); 为什么纯属性条件出发查询里必须要索引呢? 因为 Nebula Graph 在存储数据的时候,它的结构是面向分布式与关联关系设计的,类似表结构数据库中无索引的全扫描条件搜索实际上更加昂贵,所以设计上被有意禁止了。 但,如果你不追求全部数据,只要采样一部分,3.0 里之后是支持不强制索引 LIMIT <n> 的情况的,如下查询(有 LIMIT)不需要索引: MATCH (v:player { name: 'Tim Duncan' })-->(v2:player) \ RETURN v2.player.name AS Name LIMIT 3; 为什么只有纯属性条件出发查询需要索引 在这里,我们比较一下正常的图查询 graph-queries 和纯属性条件出发查询 pure-prop-condition queries 实现方式: graph-queries,如 query 2 和 query 3 是沿着边一路找到特定路径条件的扩展游走; pure-prop-condition queries,如 query 0 和 query 1 是只通过特定属性条件(或者是无限制条件)找到满足的点、边; 而在 Nebula Graph 里,graph-queries 在扩展的时候,图的原始数据已经按照 VID(点和边都是)排序过了,或者说在数据里已经索引过了,这个排序带来连续存储(物理上邻接)使得扩展游走本身就是优化过能快速返回结果。 总结:索引是什么,索引不是什么? 索引是什么? Nebula Graph 索引是为了从给定属性条件查点、边的一份属性数据的排序,它用写入的代价使得这种读查询模式成为可能。 索引不是什么? Nebula Graph 索引不是用来加速一般图查询的:从一个点开始向外拓展的查询(即使是过滤属性条件的)不会依赖原生索引,因为 Nebula 数据自身的存储就是面向这种查询优化、排序的。 一些 Nebula Graph 索引的设计细节 为了更好理解索引的限制、代价、能力,咱们来解释更多他的细节 Nebula Graph 索引是在本地(不是分开、中心化)和点数据被一起存储、分片的。 它只支持左匹配 因为底层是 RocksDB Prefix Scan; 性能代价: 写入时候的路径:不只是多一分数据,为了保证一致性,还有昂贵的读操作; 读路径:基于规则的优化选择索引,fan-out 到所有 StorageD; 这些信息可在我的个人网站的#手绘图和视频#(链接:https://www.siwei.io/sketch-notes/)里可以看到,参考下图: 因为左匹配的设计,在复杂查询场景,比如:针对纯属性条件出发查询里涉及到通配、REGEXP,Nebula Graph 提供了全文索引的功能,它是利用 Raft Listener 去异步将数据写到外部 Elasticsearch 集群之中,并在查询的时候去查 ES 去做到的,具体全文索引使用参见文档:https://docs.nebula-graph.com.cn/3.0.0/4.deployment-and-installation/6.deploy-text-based-index/2.deploy-es/。 在这个手绘图中,我们还可以看出 Write path 写入索引数据是同步操作的; Read path 这部分画了一个 RBO 的例子,查询里的规则假设 col2 相等匹配排在左边的情况下,性能优于 col1 的大小比较匹配,所以选择了第二个索引; 选好了索引之后,扫描索引的请求被 fan-out 到存储节点上,这其中有些过滤条件比如 TopN 是可以下推的; 结论: 因为写入的代价,只有必须用索引的时候采用,如果采样查询能满足读的要求,可以不创建索引而用 LIMIT <n>。 索引有左匹配的限制 符合查询的顺序要仔细设计 有时候需要使用全文索引 full-text index。 索引的使用 具体要参考 Nebula 官方的索引文档:https://docs.nebula-graph.io/3.0.0/3.ngql-guide/14.native-index-statements/ 一些要点是: 第一点,在 Tag 或者 EdgeType 上针对想要被条件反查点边的属性创建索引,使用 CREATE INDEX 语句; 第二点,创建索引之后的索引部分数据会同步写入,但是如果创建索引之前已经有的点边数据对应的索引是需要明确指定去创建的,这是一个异步的 job,需要执行语句 REBUILD INDEX; 第三点,触发了异步的 REBUILD INDEX 之后,可用语句 SHOW INDEX STATUS 查询状态: 第四点,利用到索引的查询可以是 LOOKUP,并且常常可以借助管道符在此之上做拓展查询,参考下面例子: LOOKUP ON player \ WHERE player.name == "Kobe Bryant"\ YIELD id(vertex) AS VertexID, properties(vertex).name AS name |\ GO FROM $-.VertexID OVER serve \ YIELD $-.name, properties(edge).start_year, properties(edge).end_year, properties($$).name; 也可以是 MATCH,这里边 v 是通过索引得到的,而 v2 则是在数据(非索引)部分拓展查询获得的。 MATCH (v:player{name:"Tim Duncan"})--(v2:player) \ RETURN v2.player.name AS Name; 第五点,复合索引的能力与限制。理解原生索引的匹配是左匹配能让我们知道对于超过一个属性的索引:复合索引,并且能帮助我们理解它的能力有限制,这里说几个结论: 我们创建针对多个属性的复合索引是顺序有关的 比如,我们创建一个双属性复合索引 index_a: (isRisky: bool, age: int),和 index_b: (age: int, isRisky: bool) 在根据 WHERE n.user.isRisky == true AND n.user.age > 18 筛选条件进行查询时,index_a 因为左匹配一个相等的短字段,显然效率更高。 只有复合左匹配的被复合索引的属性真子集的过滤条件才能被只支持 比如,index_a: (isRisky: bool, age: int),和 index_b: (age: int, isRisky: bool) 在查询 WHERE n.user.age > 18 这个语句时,只有 index_b 复合最左匹配,能满足这个查询。 针对一些从属性作为查询的起点,找点、边的情况,原生索引是不能满足全文搜索的匹配场景的。这时候,我们应该考虑使用 Nebula 全文索引,它是 Nebula 社区支持的开箱即用的外置 Elasticsearch,通过配置,创建了全文索引的数据会通过 Raft listener 异步更新到 Elastic 集群中,全文索引的查询入口也是 LOOKUP,详细的信息请参考文档:https://docs.nebula-graph.com.cn/3.0.1/4.deployment-and-installation/6.deploy-text-based-index/2.deploy-es/。 回顾 Nebula Graph 索引在只提供属性条件情况下通过对属性的排序副本扫描查点、边; Nebula Graph 索引不是用来图拓展查询的; Nebula Graph 索引是左匹配,不是用来做模糊全文搜索的; Nebula Graph 索引在写入时候有性能代价; 记得如果创建 Nebula Graph 索引之前已经有相应点边上的数据,要重建索引; Happy Graphing! 交流图数据库技术?加入 Nebula 交流群请先填写下你的 Nebula 名片,Nebula 小助手会拉你进群~~ 关注公众号

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册