本篇博客是根据公司真实的需求,但其并不涉及公司数据与信息,仅仅记录技术选型和实现本次需求的整个过程,主要为图数据库的调研(neo4j 和 Neula)与使用。
需求背景
目前在公司负责可观测相关方向的内容。最近有一个需求是查看 app 的拓扑图 。那这里先说下几个概念。在知乎基础设施有两个概念分别是应用(app)和服务(service)。应用是指仓库中的一套代码,服务是根据这套代码以不同的命令或者环境变量等不同方式启动部署的单元,换言之一个应用可能对应多个服务。
最终拓扑图实现的效果如下所示:
需求分析
应用的拓扑图是根据服务去生成的,如上图所示,当我要查询 app01 的上下游关系时,是根据 app01 所有服务调用的上下游去生成数据的。当服务量比较大,并且上下游服务比较多的情况下,实时去查询肯定是满足不了,那么只能每天定时去计算数据然后存储,查询时直接查询生成好的数据。那么数据将如何存储呢?
查询每个应用的上游和下游需要进行多次 IO。举个例子,假如查询某个 app 下游两层拓扑数据,查询一次获取全部的第一层依赖应用,获取第二层依赖关系时需要依次遍历第一层的 app 的下游依赖,IO 的次数与第一层依赖的 app 数量相关。如下图所示。
很显然关系型数据库并不满足本次需求。针对于这种关系型数据的存储与查询有专门的数据库进行处理,这种数据库就是图数据库
图数据库调研
一般我们进行技术选型时会先进行公司内部和社区开源组件进行调研,本篇文章将忽略公司内部调研,直接进行开源组件调研。以下是整个调研过程:
根据 DB-Engines Ranking 分析排名前 15 的数据库,将商业类的产品去除掉,一共有 7 种数据库,分别是 Neo4j、Virtuoso、ArangoDB、OrientDB、NebulaGraph、JanusGraph 和 Dgraph。将其进行简单对比分类:
- 第一类:Neo4j、ArangoDB、Virtuoso。 单机版本开源可用,性能优秀。
- 第二类:JanusGraph。 此类图数据库在现有存储系统之上新增了通用的图语义解释层,图语义层提供了图遍历的能力,但是受到存储层或者架构限制,不支持完整的计算下推,多跳遍历的性能较差。
- 第三类:DGraph、NebulaGraph。 此类图数据库根据图数据的特点对数据存储模型、点边分布、执行引擎进行了全新设计,对图的多跳遍历进行了深度优化。
将对比 NebulaGraph 和 Neo4j 进行最终选型。
Neo4j 是一个基于 Java 的图数据库,具有高效、可扩展和易用的特点。在存储引擎方面,Neo4j 使用了一种名为"标签节点"的存储方式,可以减少节点和关系的存储空间,并提高查询性能。Neo4j 也支持 ACID 事务和多种编程接口,例如 Cypher 查询语言和 Java API 等。 Nebula 是一个分布式的图数据库,支持高度可扩展性和高效的分布式查询。
Nebula 采用 GKV 存储引擎,支持海量数据的存储和快速访问,并且支持多种编程和查询接口,例如 C++ API、Cypher、Gremlin、RESTful API 等。Nebula 还提供了一个名为"Storage"的模块,允许用户将数据分片存储在多个节点上,以实现数据的高效分布式访问。
直接说结论,本次使用 NebulaGraph 图数据库。是根据以下几点:
- 项目开源,不考虑付费情况
- 运维成本,尽可能选择低运维成本的产品
- 数据量大小。相对来说应用相关的数据量比较小,不需要考虑数据扩展能力
- 毫秒级别查询延迟
图数据库搭建
选择 NebulaGraph 图数据库之一就是运维成本低,文档友好,按照官方文档搭建即可,其过程不重点介绍。
Nebula 的使用
在这里强调下,本篇文章仅仅围绕如何实现需求进行介绍,并不会对概念和数据库进行详细的说明,如果要了解概念,请阅读官方文档。
在这里说几个概念:
- space:相当于 MySQL 数据库概念
- tag:相当于 MySQL 中的表,可以理解为本次需求中的应用
- egon: 也相当于一张表,不过这张表存储的是对应的关系,比如 app01 与 app02 之间的调用关系
下面是其基本的使用命令,详解请参考官方文档,很详细。
shell
# 创建 space & 删除空间
CREATE SPACE test(
partition_num=20,
replica_factor=3,
vid_type=fixed_string(200)
);
DROP SPACE IF EXISTS test;
# 创建 tag & 删除 tag & 创建 tag 索引 & 查看 tag 索引 & 删除 tag 索引
CREATE TAG IF NOT EXISTS app(name string, type int, create_time timestamp)
TTL_DURATION = 604800,
TTL_COL = "create_time";
DROP TAG IF EXISTS app;
CREATE TAG INDEX app_name_index ON app(name(200));
SHOW TAG INDEXES;
REBUILD TAG INDEX app_name_index;
# 创建 egon type & 删除 egon & 创建 egon 索引 & rebuild tag 索引 & 删除 egon 索引
CREATE EDGE IF NOT EXISTS downstream(qps double, create_time timestamp) TTL_DURATION = 604800, TTL_COL = "create_time";
CREATE EDGE IF NOT EXISTS upstream(qps double, create_time timestamp) TTL_DURATION = 604800, TTL_COL = "create_time";
DROP EDGE downstream DROP EDGE upstream CREATE EDGE INDEX IF NOT EXISTS downstream_index on downstream();
CREATE EDGE INDEX IF NOT EXISTS upstream_index on upstream();
REBUILD EDGE INDEX downstream_index;
REBUILD EDGE INDEX upstream_index;
DROP TAG app;
# 插入 app & 插入边
INSERT VERTEX app(name, type, create_time) VALUES "app-01":("app-01", 1, now());
INSERT EDGE relay(qps, create_time) VALUES "app-01" -> "mysql-01":(95, now());
# 查询下游依赖 GO FROM "app-01" OVER relay YIELD id($$);
GO 1 STEPS FROM "aisp-core" OVER downstream YIELD dst(edge);
#列出所有的边
LOOKUP ON downstream YIELD edge AS e; match ()<-[e]-() return e limit 10
# match 根据标签匹配点
match (v:app) return v limit 10 LOOKUP ON app WHERE app.name == "app-01" YIELD properties(vertex).name AS name, properties(vertex).type AS type;
代码实现
开始使用 session 去完成相应的代码编写,具体什么是 session, 相关概念可以参考:链接
相应代码如下所示:
java
<dependency>
<groupId>com.vesoft</groupId>
<artifactId>client</artifactId>
<version>3.0.0</version>
</dependency>
java
/**
* 连接池
*
* @return NebulaPool
* @author qiutiangang
*/
@Bean
public NebulaPool nebulaPool() throws UnknownHostException {
NebulaPool pool = new NebulaPool();
NebulaPoolConfig nebulaPoolConfig = new NebulaPoolConfig();
nebulaPoolConfig.setMaxConnSize(10000);
List<HostAddress> addresses = Collections.singletonList(new HostAddress(host, port));
boolean init = pool.init(addresses, nebulaPoolConfig);
if (!init) {
throw new RuntimeException("NebulaGraph init err !");
}
log.info("NebulaGraph init Success !");
return pool;
}
/**
* 连接会话
*
* @author qiutiangang
*/
@Bean
@Scope(scopeName = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public Session session() {
Session session;
try {
session = nebulaPool().getSession(username, password, false);
// 使用配置文件中的图空间
session.execute(String.format("use %s;", SPACE));
return session;
} catch (Exception e) {
log.error("get nebula session err, {} ", e.toString());
}
return null;
}
public ResultSet execute(String stmt) {
ResultSet resultSet = null;
try {
resultSet = Objects.requireNonNull(getSession()).execute(stmt);
} catch (Exception e) {
log.error("nebula execute err:", e);
}
return resultSet;
}
使用上面代码成功执行几条命令发现并没有问题,但是在写入数据时会莫名其妙的报错:
bash
# [Create Session failed: Too many sessions created from ::ffff:127.0.0.1 by user root. the threshold is 300.](https://discuss.nebula-graph.com.cn/t/topic/8943)
上网搜索之后也没有准确的找到问题是什么,后来意识到可能 session 没有销毁,需要在方法执行完调用 session.release() 和 session = None
后来找到一个比较靠谱的方法,使用 session pool。使用之后再也没有出现上面的问题。以下是代码(注意版本)
java
<dependency>
<groupId>com.vesoft</groupId>
<artifactId>client</artifactId>
<version>3.6.1</version>
</dependency>
@Bean
public SessionPool sessionPool() {
List<HostAddress> addresses = Collections.singletonList(new HostAddress(host, port));
SessionPoolConfig sessionPoolConfig =
new SessionPoolConfig(addresses, SPACE, username, password)
.setMaxSessionSize(10)
.setMinSessionSize(10)
.setRetryConnectTimes(3)
.setWaitTime(100)
.setRetryTimes(3)
.setIntervalTime(100);
SessionPool sessionPool = new SessionPool(sessionPoolConfig);
if (!sessionPool.isActive()) {
log.error("session pool init failed.");
return null;
}
return sessionPool;
}
最重要的上下游依赖语句可以参考下面语句, 返回 app-01 下游两层数据。
java
MATCH p=(v:app{name:"app-01"})-[e:upstream*2]->(v2) RETURN p
以上是全部的内容。
对于图数据库或者其他技术交流,可以加我微信:QTG43432166。也会有相关岗位推荐。