图实时查询优化记录

369 阅读5分钟

我们通过一个支付风控流程中,下单请求将首先进入快速风控阶段,在该阶段,系统会使用基于关系型数据和图数据的规则和模型来判断支付风险。图查询只是这个流程的一部分,为了让用户在不感知后台风控过程的前提下完成支付,分配给图查询的时间非常有限。对于2-3跳查询,系统要求90%的请求在15毫秒内完成。因此记录下优化流程。

一、查询需求:

如下图所示,每条线都代表一次由起点开始的查询,如图共三行,第一行为起始点,为业务给出输入,第二行为1阶查询结果,第三行为根据1阶查询结果进行查询的2阶结果,每次查询可能有多个边。需要如下图一套组合查询在15ms(p90)内返回.上一篇文章中业务和此类似,但不如此次复杂,解决方式也不如此次成熟,但仍有记录意义,且随着理解不断深入,查询也不断优化,

image.png

image.png

二、优化点记录:

单跳查询 优化前:150ms 优化后:15ms(p90)

1. 查询并行:进行多线程查询 150ms->50ms

对于一阶查询来说不存在依赖关系,可同时进行。但2阶查询依赖1阶查询结果,因此需要等待1阶查询全部结束才可开始。因此, 全局查询最长时间 = 一阶查询耗时最长查询+二阶查询耗时最长查询。这里存在几个并行的小细节:

  1. 因为需要结果集,所以并行过程就会对结果集合进行去重,避免存在原始数据,这里使用2个set来确定返回结果集。其中一个set初始化后首先会存入原始数据,uid,mobile,clientid,mhdnamcdt这四个元素,另一个用来存储查询结果。后续每个线程查询的结果会先判断不存在第一个set中,才加入结果集,并把此结果加入第一个set,以此来对结果集去重原始数据。一阶查询后,第一个set就存在全部原始数据+1阶查询结果,2阶查询结果将此作为第一个set对原始数据+1阶查询结果进行去重,得到2阶查询结果。 此数据结构保证了 对1阶 2阶 查询结果的去重。
初始阶段:
set1:[uid,mobile,client,mhdnamcdt]
set2[]

一阶查询结束:
set1:[uid,mobile,client,mhdnamcdt, uid1,uid3,mobile2,client5]
set2‘:[uid1,uid3,mobile2,client5]  

二阶查询结束:
set1:[uid,mobile,client,mhdnamcdt, uid1,uid3,mobile2,client5,uid5  ,uid7,mobile9,client8]
set2’‘[uid5,uid7,mobile9,client8]


最终返回数据
set2‘ + set2’‘

  1. 一阶查询共存在15个并行,并行结果如图所示会分类存入4个结果集,4个结果集 uidResult, mobileRest,···,注意线程安全,写数据加锁。这里有几个加锁选择:
1. 

2.资源优化管理:池化 session对象 避免认证耗时 50ms->15ms

官方提供了连接池nebulaPool,会管理连接,保存连接池,断开自动重试,负载均衡等。但是在连接时候需要进行getSession进行用户名密码的认证,这个动作在服务端是加锁的,多线程情况下每个线程进行getSession 是会比较慢的:getSession连接耗时20ms,查询2ms,对实时查询影响很大。

因此在业务层维护一个session池,使用时只需要和资源池借一个无需验证的连接, 使用完的时候就不要进行资源release了,而是归还给自维护的资源池,以提供其他线程利用。我这里实现是采用一个阻塞队列LinkedBlockingQueue来保存连接态的session,这里也可以参考提的这个和作者交流的issue,discuss.nebula-graph.com.cn/t/topic/720…

SessionPool(int maxCountSession, int minCountSession, String hostAndPort, String userName, String passWord) {
        this.minCountSession = minCountSession;
        this.maxCountSession = maxCountSession;
        this.userName = userName;
        this.passWord = passWord;
        this.hostAndPort = hostAndPort;
        // 自维护连接池,存储已验证过的session,可直接执行sql
        this.queue = new LinkedBlockingQueue<>(minCountSession);
        // 连接池,获取session需要耗时验证
        this.pool = this.initGraphClient(hostAndPort, maxCountSession,minCountSession);
        initSession();
    }
 
   // 2.从阻塞队列中拿出一个连接提供给线程使用
    public Session borrow() {
        Session se = queue.poll();
        // ?验证 se是否正常 
        if (se!=null){return se;}
        // 队列已空,均被其他线程占用,且小于最大连接数,则再申请连接
        if (se == null && queue.size() < maxCountSession) {
            try {
                se = this.pool.getSession(userName, passWord, true);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else {
        // session数量已达到最大值则等待其他线程释放。
            try {
                Thread.sleep(100);
                Session retry = queue.poll();
                if(retry!=null){
                    return retry;
                }else {
                    throw new RuntimeException("getSession  fail") ;
                }
            }catch (Exception e) {
                e.printStackTrace();
            }}
        return se;}
    //  返回给阻塞队列,以提供给其他线程使用
    public void release(Session se) {
        if (se != null) {
            queue.offer(se);
        }}
        
    // 1.初始化连接池,使队列中存在验证连接好的session
    public void initSession() {
        for (int i = 0; i < minCountSession; i++) {
            queue.offer(this.pool.getSession(userName, passWord, true));}}
​

3.图schema优化:空间换时间,热点值存储为点属性,避免查询浪费资源 15ms->5ms

image.png 在实际查询中,需要将一些边进行预聚合(比如,在账户顶点上增加一个“边数量”属性,每次增加一条边,就在点上对“边数量”做相应的调整),这样当我们需要查询时筛选这个聚合总数时,就可以直 接读取这个预计算的值,而不需要对所有边进行实时查询和聚合。尽管 这一策略会增加写操作的工作量,却可以有效的降低读取延迟。

因为需要限制结果边数量,如图点uid开始查询edgeType1的终点mobile,mobile能正确返回还需要查询mobile有几个uid,如通过mobie 查edgetype1 反向边得到的uid边数量大于最大值,此mobile为无效结果。可能为黄牛等非常正常用户。总而言之就是保证每个终点反向边数量不超过maxCount.

因此,将查询热点值存储为点属性,有如下优缺点:

  • 优点:在查询点终点时候,可以直接获取终点的边数量情况,避免对每个结果的再次查询。
  • 缺点:此种方案对查询友好,但在写入时则需要对每一条边进行查询并判断是否存在,并更改涉及的每个点的属性值,才可导入,否则无法保证幂等性。写入一条边,对应起始点,终点属性值+1.

优化后:

image.png

查询到mobile时候,即可获得反向边数量。相比优化前可节约一跳查询时间。

4.复用对象,减少创建新对象:

对于查询过程中使用的数据结构,尽量在查询之初创建并之后复用,避免创建过多新对象。

如一阶查询和2阶查询过程中,线程不会同时存在,因此可复用一些对象,如线程管理对象ListenableFuture

组合查询(1阶+2阶查询)可优化至p90 15ms。