一、Nebula简介

Nebula是图数据库,它擅长图查询,它不擅长条件查询;我们要清晰地知道我们的业务是否需要Nebula实现,是否适合图数据库实现,拒绝盲目地跟风式使用图数据库,无谓的引入复杂度。

NebulaGraph独有的DQL

  • 图拓展(GO):从给定的起点,向外拓展,按需返回终点、起点的信息
# 图拓展
GO 3 STEPS FROM "player102" OVER follow YIELD dst(edge);
   ───┬───      ───┬───────      ─┬────       ──┬────── 
      │            │              │   ┌─────────┘       
      │            │              │   │                 
      │            │              │   └── 返回最后一跳边的终点
      │            │              │                     
      │            │              └────── 从 follow 这个边[出方向]探索
      │            │                                    
      │            └───────────────────── 起点是 "player102"
      │                                                 
      └────────────────────────────────── 探索 3 步

  • 索引反查(LOOKUP):LOOKUP 是一个类似于 SQL 里 SELECT 语义的关键字,它实际的作用也类似与关系型数据库中的条件查询
# 索引反查
LOOKUP ON player WHERE player.name == "Tony Parker" YIELD id(vertex);
          ──┬───  ──────┬──────────────────────────  ──┬──────       
            │           │          ┌───────────────────┘             
            │           │          │                                 
            │           │          └──────────── 返回查到点的 VID
            │           │                                            
            │           └─────────────────────── 过滤条件是属性 name 的值
            │                                                        
            └─────────────────────────────────── 根据点的类别/TAG player 查询

  • 取属性(FETCH PROP):如果我们知道一个点、边的 ID,想要获取它上边的属性,这时候我们要用 FETCH PROP 而非 LOOKUP
# 取属性
FETCH PROP ON player "player100" YIELD properties(vertex);
              ──┬───  ────┬─────       ─────────┬──────── 
                │         │         ┌───────────┘         
                │         │         │                     
                │         │         └─────── 返回点的 player TAG 下所有属性
                │         │                               
                │         └───────────────── 从 "player100" 这个点获取
                │                                         
                └─────────────────────────── 获取 player 这个 TAG 下的属性

  • 路径(FIND PATH):如果我们从给定的起点、终点中,找到之间的所有路径,一定要用 FIND PATH
# 起点终点间路径
FIND SHORTEST PATH FROM "player102" TO "team204" OVER * \   
     ──┬─────            ───────────┬─────────── ───┬───    
  YIELD│path AS p; ┌────────────────┘               │       
       │────┬────  │     ┌──────────────────────────┘       
       │    │      │     │                                  
       │    │      │     └───────── 经由所有类型的边出向探索
       │    │      │                                        
       │    │      └─────────────── 从给定的起点、终点 VID
       │    │                                               
       │    └────────────────────── 返回路径为 p 列
       │                                                    
       └─────────────────────────── 查找最短路径

  • 子图(GET SUBGRAPH):和路径查找类似,但是我们只给定一个起点和拓展部署,用 GET SUBGRAPH 可以帮我们获取同样的 BFS 出去的子图
# 单点 BFS 子图
GET SUBGRAPH 5 STEPS FROM "player101" \           
             ───┬─── ─────┬──────────             
  YIELD VERTICES AS nodes, EDGES AS relationships;
        ────┬───┼─────────┼───────────────────────
   ┌────────┘   │         │                       
   │            │         └─────── 从 "player101" 开始触发
   │            │                                 
   │            └───────────────── 获取 5 步的探索
   │                                              
   └────────────────────────────── 返回所有的点、边

NebulaGraph OpenCypher DQL

  • 模式匹配(MATCH)
MATCH (v:`player`{name:"Tim Duncan"})-->(v2)<--(v3) \
    RETURN v3.`player`.name AS Name;

MATCH (v:`player`) \
    WHERE NOT (v)--() \
    RETURN v;

MATCH (v:`player`)--(v2) \
    WHERE id(v2) IN ["player101", "player102"] \
    RETURN v;

MATCH (m)-[]->(n) WHERE id(m)=="player100" \
OPTIONAL MATCH (n)-[]->(l) WHERE id(n)=="player125" \
    RETURN id(m), id(n), id(l);

Nebula索引

简而言之,NebulaGraph 索引是用来,且只用来针对纯属性条件出发查询场景的;在 NebulaGraph 图数据库里,索引则是对点、边特定属性数据重排序的副本,用来提供纯属性条件出发查询

  • 图游走(walk)查询中的属性条件过滤不需要它
  • 纯属性条件出发查询(注:非采样情况)必须创建索引

优化原则

  • 减少模糊,增加确定,越早越好

在理解了 NebulaGraph 的基本架构设计、存储格式、查询的简单调用流程和常见的优化规则之后,结合 PROFILE/EXPLAIN,我们可以一点点去设计更适合不同场景的图建模与图查询

二、问题收集

1、Nebula-Java客户端Session Pool broke pipe

1.1、问题描述

  • 客户端版本(nebula-java):3.3.0
    /**
     * check if session is valid, if session is invalid, remove it.
     */
    private void checkSession() {
        for (NebulaSession nebulaSession : sessionList) {
            if (nebulaSession.isIdleAndSetUsed()) {
                try {
                    idleSessionSize.decrementAndGet();
                    nebulaSession.execute("YIELD 1");
                    nebulaSession.isUsedAndSetIdle();
                    idleSessionSize.incrementAndGet();
                } catch (IOErrorException e) {
                    log.error("session ping error, {}, remove current session.", e.getMessage());
                    nebulaSession.release();
                    sessionList.remove(nebulaSession);
                }
            }
        }
    }

    public void release() {
        connection.signout(sessionID);
        connection.close();
        connection = null;
    }

使用3.3.0客户端,健康检查任务未能正确移除失效链接,该失效链接可能是超过了服务端链接保持的最大时间后,服务端主动断开的情况;客户端链接池仍然持有着失效链接,业务线程用到此链接就会出现管道破裂(broke pipe)异常,业务处理失败。

1.2、问题出现条件

  1. 客户端版本(nebula-java):3.3.0
  2. session链接超过服务端持有最大时间

本质原因:健康检查任务再释放失效链接时,有一步登出操作出现异常(此时服务端已经断开此链接),导致后续关闭链接、移除失效链接不会被执行。

1.3、解决方案

  • 方式一:目前客户端版本(nebula-java):3.5.0版本已经修复了此问题,可升级到此版本。此客户端版本向下兼容3.3.0的服务端graphd
  • 方式二:如果无法升级客户端版本,可覆写连接池release方法,处理登出异常即可。

1.4、结果

HO3.10产品使用了上述方式一的方案处理,解决了此问题。

2、Nebula-Java客户端链接池级别锁

2.1、问题描述

在使用客户端执行方法com.vesoft.nebula.client.graph.SessionPool#execute(java.lang.String, java.util.Map<java.lang.String,java.lang.Object>),连接池级别加了锁,本意是防止多线程操作同一个session,但是getSession已经加了锁,所以连接池级别锁可以去除,3.5.0客户端只是标注废弃了此重载。此执行方法业务上不要使用,倘若出现长事务长时间占用锁,会严重影响业务系统的吞吐量。最严重情况可导致业务系统不可用。

    /**
     * Execute the nGql sentence with parameter
     *
     * @param stmt         The nGql sentence.
     * @param parameterMap The nGql parameter map
     * @return The ResultSet
     */
    public synchronized ResultSet execute(String stmt, Map<String, Object> parameterMap)
            throws ClientServerIncompatibleException, AuthFailedException,
            IOErrorException, BindSpaceFailedException {
        stmtCheck(stmt);
        checkSessionPool();
        NebulaSession nebulaSession = getSession();
        ResultSet resultSet;
        try {
            resultSet = nebulaSession.executeWithParameter(stmt, parameterMap);

            // re-execute for session error
            if (isSessionError(resultSet)) {
                sessionList.remove(nebulaSession);
                nebulaSession = getSession();
                resultSet = nebulaSession.executeWithParameter(stmt, parameterMap);
            }
        } catch (IOErrorException e) {
            useSpace(nebulaSession, null);
            throw e;
        }

        useSpace(nebulaSession, resultSet);
        return resultSet;
    }

2.2、问题出现条件

  1. 客户端版本(nebula-java):3.5.0及以下版本

目前最新客户端的版本3.5.0依然存在此间接持级别锁

2.3、解决方案

  • 方式一:排查业务系统使用以上执行方法的功能点,改用com.vesoft.nebula.client.graph.SessionPool#execute(java.lang.String)执行方法
  • 方式二:业务系统可覆写连接池类,可将连接池级锁去除

2.4、结果

HO3.10产品采用了上述方式一的方案,全部改用com.vesoft.nebula.client.graph.SessionPool#execute(java.lang.String)执行方法,也推荐此方式

3、使用“1==1”作为查询条件对齐

3.1、问题描述

OpenCypher模式匹配条件中,出现where 1==1这样对业务无意义的对齐条件,是要禁止的,因为此条件虽然并没有改变查询语义,但是会造成全索引扫描

  • 问题示例
MATCH (n:HO_NEO4J_OBJECT_ID)-[:REFERENCES_OBJECTID_DOCID]->(m:HO_NEO4J_DOCID)-[:REFERENCES_DOCID_PERM]->(perm:HO_NEO4J_PERM) 
where 1==1 
and n.HO_NEO4J_OBJECT_ID.`hoid` in ['6d9161e1833746d8a6f7ca1f13199d85','b173728ec75e4aa8ab16f322262a9599']  
return n.HO_NEO4J_OBJECT_ID.hoid as objectId,m.HO_NEO4J_DOCID.hoid as docId
  • profile结果(去除1==1前)

image

  • profile结果(去除1==1后)

image

3.2、解决方案

去除业务系统各个语句where 1==1条件,禁止使用类似的无意义填充

3.3、结果

HO3.10产品各个功能点已全部删除where 1==1条件

4、使用Match分页查询

4.1、问题描述

由于当前 NebulaGraph 社区版并没有进行MATCH带索引查询的Limit下推,所以过滤操作是后置的,graphd的执行引擎会从各个存储引擎中获取到符合条件的点边,然后再过滤,假设符合条件的数据较多,会存在很大的性能问题,也很浪费Nebula服务的带宽。

  • 问题示例
 MATCH (n:HO_NEO4J_OBJECT_ID)-[r:REFERENCES_OBJECTID_WLKX_NODE]->(m:HO_NEO4J_WLKX_NODE)<-[:REFERENCES_DOCID_WLKX_NODE]-(o:HO_NEO4J_DOCID)  
 where n.HO_NEO4J_OBJECT_ID.projectId == '3fa8f6ab-c1e6-4092-8c4c-171112925dff' 
 return n.HO_NEO4J_OBJECT_ID.`tag` as deviceTag ,o.HO_NEO4J_DOCID.docName as docName,
     r.creator as creator,r.hoid as hoid,r.createTime as createTime,
     concat("'",src(r),"'->'",dst(r) ,"'") as id,m.HO_NEO4J_WLKX_NODE.guid as guid,m.HO_NEO4J_WLKX_NODE.wlkxId as wlkxId,
     m.HO_NEO4J_WLKX_NODE.nodeName as nodeName,n.HO_NEO4J_OBJECT_ID.objectId as objectId,n.HO_NEO4J_OBJECT_ID.classId as classId 
 order by createTime desc SKIP 0 limit 50
  • profile结果

image

4.2、解决方案

值得庆幸的是,现在 LOOKUP 里里等价的查询中的索引查询算子是支持 Limit 下推的优化规则的,所以我们可以用 LOOKUP改写我们的查询

  • 优化示例
 lookup on REFERENCES_OBJECTID_WLKX_NODE 
 where REFERENCES_OBJECTID_WLKX_NODE.projectId == '3fa8f6ab-c1e6-4092-8c4c-171112925dff'
 yield dst(edge) as dstId, properties(edge).createTime as createTime
 | ORDER BY $-.createTime desc
 | limit 0,50
   
 | GO FROM $-.dstId OVER REFERENCES_OBJECTID_WLKX_NODE REVERSELY
   where properties($$) is not null
   YIELD properties($^) as modelNode,id($^) as modelId, properties($$) as objNode, properties(edge) as objModelEdge, concat("'",src(edge),"'->'",dst(edge) ,"'") as objModelEdgeId
   
 | GO FROM $-.modelId OVER REFERENCES_DOCID_WLKX_NODE REVERSELY
   YIELD $-.objNode.`tag` as deviceTag, $-.objNode.objectId as objectId, $-.objNode.classId as classId ,
     properties($$).docName as docName,
     $-.objModelEdge.creator as creator,$-.objModelEdge.hoid as hoid, $-.objModelEdge.createTime as createTime, $-.objModelEdgeId as id,
     $-.modelNode.guid as guid, $-.modelNode.wlkxId as wlkxId, $-.modelNode.nodeName as nodeName

4.3、结果

HO3.10产品采用LOOKUP查询做条件查询,优化效果显著。

5、使用Match条件查询

5.1、问题描述

Nebula不擅长复杂的条件查询,如果查询条件中存在多条件查询,类似于关系库的索引,我们可以根据数据量的情况创建合适的复合索引(注意属性的顺序),来覆盖大部分的场景。如果查询条件未命中合适的索引,那么会出现filter不下推的情况,数据量如果很大容易造成性能问题。

  • 查看索引情况

image

  • filter不下推示例

image

  • filter下推示例

image

5.2、优化建议

尽量采用Nebula原生DQL,采用LOOKUP条件查询出来节点ID列表,然后再通过节点ID,GO图游走,这种组合查询,查询效果更优。

5.3、解决方案

结合业务创建适合业务的复合索引,尽可能覆盖更多的业务场景

5.4、结果

HO3.10结合业务创建了适合的索引

6、模糊条件查询

6.1、问题描述

NebulaGraph 索引是左匹配,不是用来做模糊全文搜索的,如果存在正则匹配等模糊匹配,这对NebulaGraph是不友好的,很容易造成性能问题。业务上我们要避免用图库模糊查询。

6.2、解决方案

  • 方式一:从业务设计上避免,或者采用别的擅长模糊全文匹配的中间件转换查询条件
  • 方式二:采用Nebula的全文索引,其全文索引是基于 Elasticsearch 来实现的,用于对字符串属性进行前缀搜索、通配符搜索、正则表达式搜索和模糊搜索。但是有限制,只允许创建一个属性的索引,不支持逻辑操作,例如AND、OR、NOT

6.3、结果

HO3.10采用上述方式一的方案,对于模糊匹配条件,采用ES查询转换处理

7、已有数据重建索引

7.1、问题描述

创建索引之后的索引部分数据会同步写入,但是如果创建索引之前已经有的点边数据对应的索引是需要明确指定去创建的;换言之就是说已经存在历史数据了,新创建的索引需要显示主动去重建索引,使得历史数据也生效,否则会查询不到。

  • 索引功能不会自动对其创建之前已存在的存量数据生效,无法基于该索引使用LOOKUP和MATCH语句查询到存量数据
  • 索引的重建未完成时,依赖索引的查询仅能使用部分索引,因此不能获得准确结果

7.2、解决方案

新建索引后,记得重建下索引

  • 新建索引
CREATE TAG INDEX IF NOT EXISTS I_PROJECT_TAG_HO_NEO4J_OBJECT_ID on HO_NEO4J_OBJECT_ID(projectId(36), `tag`(64));
  • 重建索引
REBUILD TAG INDEX I_PROJECT_TAG_HO_NEO4J_OBJECT_ID;

这是一个异步的 job,不会立马生效

  • 查看索引状态
SHOW TAG INDEX STATUS;

image

7.3、结果

创建索引后,如果需要,记得重建索引


neojayway
52 声望10 粉丝

学无止境,每天进步一点点


引用和评论

0 条评论