实体关系查询

概述

UModel 提供强大的图查询能力,支持基于实体关系的复杂查询操作。本文档介绍基础的图查询方法,包括 graph-match 和 graph-call 功能,用于查询实体之间的关系和路径。

应用案例

当前应用案例相关素材,请参考:umodel.zip

图查询基础概念

节点和边的描述结构

在图查询中,使用特定的语法来描述节点和边:

  • 节点:使用小括号()表示。

  • :使用中括号[]表示。

  • 描述格式<变量名>:<标签> {属性键值对}

基础语法示例

// 任意节点
()

// 具有特定标签的节点  
(:"apm@apm.service")           // graph-match写法
(:`apm@apm.service`)           // cypher写法

// 具有标签和属性的节点
(:"apm@apm.service" { __entity_type__: 'apm.service' })

// 命名变量的节点
(s:"apm@apm.service" { __entity_id__: '123456' })

// 任意边
[]

// 命名边
[edge]

// 具有类型的边
[e:calls { __type__: "calls" }]

语法差异说明

graph-match vs Cypher

  • graph-match 在 SPL 上下文中,特殊字符需要双引号包裹。

  • Cypher 作为独立语法,标签使用反引号包裹(注意如果 Cypher 使用反引号字符串格式,内部的反引号需要使用两个反引号包裹)。

// graph-match语法
.topo | graph-match (s:"apm@apm.service" {__entity_id__: '123'})-[e]-(d)
        project s, e, d

// Cypher语法 (``apm@apm.service`` 反引号字符串格式,使用两个反引号包裹)
.topo | graph-call cypher(`
    MATCH (s:``apm@apm.service`` {__entity_id__: '35af918180394ff853be6c9b458704ea'})-[e]-(d)
    RETURN s, e, d
`)

图查询路径语法

图查询路径使用ASCII字符描述关系方向:

路径表达式

方向说明

(A)-[e]->(B) 或 (A)-->(B)

AB的有向关系

(A)<-[e]-(B) 或 (A)<--(B)

BA的有向关系

(A)-[e]-(B) 或 (A)--(B)

双向关系,不限方向

返回值结构

节点JSON格式

{
  "id": "apm@apm.service:347150ad7eaee43d2bd25d113f567569",
  "label": "apm@apm.service", 
  "properties": {
    "__domain__": "apm",
    "__entity_type__": "apm.service",
    "__entity_id__": "347150ad7eaee43d2bd25d113f567569",
    "__label__": "apm@apm.service"
  }
}

JSON格式

{
  "startNodeId": "apm@apm.service:347150ad7eaee43d2bd25d113f567569",
  "endNodeId": "apm@apm.service.host:34f627359470c9d36da593708e9f2db7",
  "type": "contains",
  "properties": {
    "__type__": "contains"
  }
}

graph-match查询

基础语法

.topo | graph-match <path>
        project <output>
      | [additional SPL operations]

核心特性

  • 必须指定起始点:需要提供标签 + __entity_id__属性。

  • 中间节点限制:只能指定标签,不能指定属性。

  • 不支持多级跳跃:不支持[*2..3]语法。

  • 环路处理:每条边可以被多次使用。

实际应用案例

1. 全链路路径查询

查找从特定操作开始的完整调用链路:

.topo |
  graph-match (s:"apm@apm.service.operation" {__entity_id__: '6f0bb4c892effff81538df574a5cfcd9'})
              <-[e1]-(v1)-[e2:runs_on]->(v2)-[e3]->(v3)
  project s, 
          "e1.__type__", 
          "v1.__label__", 
          "e2.__type__", 
          "v2.__label__", 
          "e3.__type__", 
          "v3.__label__", 
          v3

返回结果

  • s: 起始操作节点。

  • e1.type: 第一段关系类型。

  • v1.label: 中间节点标签。

  • v2, v3: 后续节点信息。

2. 邻居节点统计

统计特定服务的邻居分布情况:

.topo |
  graph-match (s:"apm@apm.service" {__entity_id__: 'd6385f422b457a59cce1f0f725ac3e14'})
              -[e]-(d)   
  project eType="e.__type__", dLabel="d.__label__"
| stats cnt=count(1) by dLabel, eType
| sort cnt desc
| limit 20

3. 条件路径查询

查找满足特定条件的路径终点:

.topo |
  graph-match (s:"apm@apm.service.operation" {__entity_id__: '6f0bb4c892effff81538df574a5cfcd9'})
              <-[e1]-(v1)-[e2:runs_on]->(v2)-[e3]->(v3)
  project s, 
          "e1.__type__", 
          "v1.__label__", 
          "e2.__type__", 
          "v2.__label__", 
          "e3.__type__", 
          destId="v3.__entity_id__", 
          v3 
| where destId='9a3ad23aa0826d643c7b2ab7c6897591'
| project s, v3

graph-match 限制

限制类型

说明

解决方案

起始点限制

必须指定标签和__entity_id__

使用已知实体ID作为起点

中间节点限制

不能为中间节点指定属性

使用SPL进行后续过滤

不支持多级跳

不支持[*2..3]语法

使用Cypher或分步查询

环路处理

边可重复使用,可能产生环

通过SPL去重或限制深度

graph-call 查询

getNeighborNodes 函数

获取指定节点列表的邻居节点,并返回邻居节点和关系类型。

语法格式

.topo | graph-call getNeighborNodes(type, depth, nodeList)

参数说明

参数

类型

说明

可选值

type

string

遍历类型

sequence, sequence_in, sequence_out, full

depth

int

遍历深度

正整数,建议不超过5

nodeList

array

起始节点列表

节点描述数组

返回格式

字段

说明

示例

srcNode

源节点JSON

{“id”:“app@app.service:347150…”, “labels”:“app@app.service”, …}

destNode

目标节点JSON

{“id”:“app@app.operation:73ef19…”, “labels”:“app@app.operation”, …}

relationType

关系类型

“contains”, “calls”, “runs_on”

srcPosition

源节点位置

-1, 0, 1, 2…

遍历类型说明

sequence:有向序列遍历,即保持一个顺序返回关系。

sequence_insequence的子集,只保持入边顺序的有向序列遍历,即返回的结果中,节点都最终指向起始节点。

sequence_outsequence的子集,只保持出边顺序的有向序列遍历,即返回的结果中,节点都最终从起始节点出发。

full:全方向遍历。

  • 不考虑边的方向,全量遍历。

实际示例

.topo | graph-call getNeighborNodes(
  'sequence', 1, 
  [
    (:"app@app.operation" {__entity_id__: '73ef19770998ff5d4c1bfd042bc00a0f'})
  ]
)

getDirectRelations 函数

返回指定节点列表之间的直接关系,不包括间接关系。一般用于多个节点的批量上下游查询。

语法格式

.topo | graph-call getDirectRelations(nodeList)

参数说明

参数

类型

说明

nodeList

array

节点列表

返回格式

字段

说明

relations

关系JSON数组,包含startNodeId、endNodeId、type、properties

实际示例

.topo | graph-call getDirectRelations(
  [
    (:"app@app.service" {__entity_id__: '347150ad7eaee43d2bd25d113f567569'}),
    (:"app@app.operation" {__entity_id__: '73ef19770998ff5d4c1bfd042bc00a0f'})
  ]
)
{
  "startNodeId": "app@app.service:347150ad7eaee43d2bd25d113f567569",
  "endNodeId": "app@app.operation:73ef19770998ff5d4c1bfd042bc00a0f", 
  "type": "contains",
  "properties": {"__type__": "contains"}
}

典型应用场景

获取服务的完整邻居关系

-- 获取服务的所有邻居(5跳内)
.topo | graph-call getNeighborNodes(
  'full', 3,
  [(:"apm@apm.service" {__entity_id__: 'user-service-id'})]
)
| stats cnt=count(1) by relationType
| sort cnt desc

分析服务调用链

-- 分析特定服务的调用模式
.topo |
  graph-match (s:"apm@apm.service" {__entity_id__: 'payment-service'})
              -[e:calls]-(d:"apm@apm.service")
  project 
    source_service="s.service_name",
    target_service="d.service_name", 
    call_type="e.__type__"
| stats call_count=count(1) by source_service, target_service
| sort call_count desc

PodNode的关系链

-- 追踪Pod的完整部署链
.topo |
  graph-match (pod:"k8s@k8s.pod" {__entity_id__: '347150ad7eaee43d2bd25d113f567569'})
              <-[r1:contains]->(node:"k8s@k8s.node")
              <-[r2:contains]-(cluster:"k8s@k8s.cluster")
  project 
    pod_name="pod.pod_name",
    node_name="node.node_name", 
    cluster_name="cluster.cluster_name",
    "r1.__type__",
    "r2.__type__"

云资源依赖分析

-- 分析ECS实例的网络依赖
.topo | graph-call getNeighborNodes(
  'sequence_out', 2,
  [(:"acs@acs.ecs.instance" {__entity_id__: 'i-bp1234567890'})]
)
| extend relation_category = CASE
    WHEN relationType in ('belongs_to', 'runs_in') THEN 'infrastructure'
    WHEN relationType in ('depends_on', 'uses') THEN 'dependency'
    WHEN relationType in ('connects_to', 'accesses') THEN 'network'
    ELSE 'other' END
| stats cnt=count(1) by relation_category
| sort cnt desc
| limit 0, 100

故障上游影响分析

-- 查找可能影响目标服务的上游服务
.topo | graph-call getNeighborNodes(
  'sequence_in', 3,
  [(:"apm@apm.service" {__entity_id__: '029ea8f3f65dbc8da226f2c8d55bb8c6'})]
)
| where relationType in ('calls', 'depends_on')
| extend impact_level = CASE
    WHEN srcPosition = '-1' THEN 'direct'
    WHEN srcPosition = '-2' THEN 'secondary'
    ELSE 'indirect' END
| extend parsed_service_name = json_extract_scalar(srcNode, '$.properties.service_name')
| project 
    upstream_service = parsed_service_name,
    impact_level,
    relation_type = relationType
| stats cnt=count(1) by impact_level, relation_type

故障下游影响分析

-- 查找受目标服务故障影响的下游服务
.topo | graph-call getNeighborNodes(
  'sequence_out', 3,
  [(:"apm@apm.service" {__entity_id__: 'failing-service-id'})]
)
| where relationType in ('calls', 'depends_on')
| extend affected_service = json_extract_scalar(destNode, '$.properties.service_name')
| stats impact_count=count(1) by affected_service
| sort impact_count desc
| limit 20

权限链追踪

-- 追踪用户到资源的访问路径
.topo |
  graph-match (user:"identity@user" {__entity_id__: 'user-123'})
              -[auth:authenticated_to]->(app:"apm@apm.service")
              -[access:accesses]->(resource:"acs@acs.rds.instance")
  project 
    user_id="user.user_id",
    app_name="app.service_name",
    resource_id="resource.instance_id",
    auth_method="auth.auth_method",
    access_level="access.permission_level"

性能优化建议

1. 查询范围控制

  • 时间范围优化:合理利用时间字段进行范围限制。

  • 限制遍历深度:深度超过5层会显著影响性能。

  • 精确起始点:使用具体的 entity_id 而非模糊匹配。

  • 合理选择遍历类型:根据实际需求选择 sequence 或 full。

2. 结果集控制

  • 使用 SPL 过滤:在图查询后及时过滤不需要的结果。

  • 分批处理:对于大型图查询,考虑分批处理。

  • 结果缓存:对于频繁查询的路径,考虑结果缓存。