更多技术交换、求职机会,欢送关注字节跳动数据平台微信公众号,回复【1】进入官网交换群
背景
- DataLeap 作为一站式数据中台套件,会集了字节外部多年积攒的数据集成、开发、运维、治理、资产、平安等全套数据中台建设的教训,助力企业客户晋升数据研发治理效率、升高治理老本。
- Data Catalog 是一种元数据管理的服务,会收集技术元数据,并在其根底上提供更丰盛的业务上下文与语义,通常反对元数据编目、查找、详情浏览等性能。目前 Data Catalog 作为火山引擎大数据研发治理套件 DataLeap 产品的外围性能之一,通过多年打磨,服务于字节跳动外部简直所有外围业务线,解决了数据生产者和消费者对于元数据和资产治理的各项外围需要。
- Data Catalog 零碎的存储层,依赖 Apache Atlas,传递依赖 JanusGraph。JanusGraph 的存储后端,通常是一个 Key-Column-Value 模型的零碎,本文次要讲述了应用 MySQL 作为 JanusGraph 存储后端时,在设计下面的思考,以及在理论过程中遇到的一些问题。
起因
理论生产环境,咱们应用的存储系统保护老本较高,有肯定的运维压力,于是想要寻求代替计划。在这个过程中,咱们试验了很多存储系统,其中 MySQL 是重点投入调研和开发的备选之一。
另一方面,除了字节外部外,在 ToB 场景,MySQL 的运维老本也会显著小于其余大数据组件,如果 MySQL 的计划跑通,咱们能够在 ToB 场景多一种抉择。基于以上两点,咱们投入了肯定的人力调研和实现基于 MySQL 的存储后端。
计划评估在设计上,JanusGraph 的存储后端是可插拔的,只有做对应的适配即可,并且官网曾经反对了一批存储系统。联合字节的技术栈以及咱们的诉求,做了以下的评估。
各类存储系统比拟
- 因投入老本过高,咱们不承受本人运维有状态集群,排除了 HBase 和 Cassandra;
- 从以后数据量与未来的可扩展性思考,单机计划不可选,排除了 BerkeleyDB;
- 同样因为人力老本,须要做极大量开发革新的计划临时不思考,排除了 Redis。
最终咱们筛选了 MySQL 来推动到下一步。
MySQL 的实践可行性
- 能够反对 Key-Value(后续简称 KV 模型)或者 Key-Column-Value(后续简称 KCV 模型)的存储模型,汇集索引 B+ 树排序拜访,反对基于 Key 或者 Key-Column 的 Range Query,所有查问都走索引,且防止内存中重排序,效率初步判断可承受。
- 中台内的其余零碎,最大的 MySQL 单表曾经达到亿级别,且 MySQL 有成熟的分库分表解决方案,判断数据量能够反对。
- 在具体应用场景中,对于写入的效率要求不高,因为大量的数据都是离线工作实现,判断 MySQL 在写入上的效率不会成为瓶颈。
总体设计
- 保护一张 Meta 表做 lookup 用,Meta 表中存储租户与 DataSource(库)之间的映射关系,以及 Shards 等租户级别的配置信息。
- StoreManager 作为入口,在 openTransaction 的时候将租户信息注入到 StoreTransaction 中,并返回租户级别的 DataSource。
- StoreManager 中以 name 为 Key,保护一组 Store,Store 与存储的数据类型无关,具备跨租户能力
- 常见的 Store 有 system_properies,tx_log,graphindex,edgestore 等
- 对于 MySQL 最终的读写,都收敛在 Store,办法签名中传入 StoreTransaction,Store 从中取出租户信息和数据库连贯,进行数据读写。
- 对于单租户来说,数据能够分表(shards),对于某个特定的 key 来说,存储和读取某个 shard,是依据 ShardManager 来决定
- 典型的 ShardManager 逻辑,是依据总 shard 数对 key 做 hash 决定,默认单分片。
- 对于每个 Store,表构造是 4 列(id, g_key, g_column, g_value),除自增 ID 外,对应 key-column-value model 的数据模型,key+column 是一个汇集索引。
- Context 中的租户信息,须要在操作某个租户数据之前设置,并在操作之后革除掉。
细节设计与疑难问题
细节设计
存储模型
JanusGraph 要求 column-family 类型存储(如 Cassandra, HBase),也就是说,数据存储由一系列行组成,每行都由一个键(key)惟一标识,每行由多个列值(column-value)对组成,也会对列进行排序和过滤,如果是非 column-family 的类型存储,则须要另行适配,适配时数据模型有两种形式:Key-Column-Value 和 Key-Value。
KCV 模型:
- 会将 key\column\value 在存储中辨别开来。
- 对应的接口为:KeyColumnValueStoreManager。
KV 模型:
- 在存储中仅有 key 和 value 两局部,此处的 key 相当于 KVC 模型中的 key+column;
- 如果要依据 column 进行过滤,须要额定的适配工作;
- 对应的接口为:KeyValueStoreManager,该接口有子类 OrderedKeyValueStoreManager,提供了保障查问后果有序性的接口;
- 同时提供了 OrderedKeyValueStoreManagerAdapter 接口,用于对 Key-Column-Value 模型进行适配,将其转化为 Key-Value 模型。
MySQL 的存储实现采纳了 KCV 模型,每个表会有 4 列,一个自增的 ID 列,作为主键,同时还有 3 列别离对应模型中的 key\column\value,数据库中的一条记录相当于一个独立的 KCV 构造,多行数据库记录代表一个点或者边。表中 key 和 column 这两列会组成联结索引,既保证了依据 key 进行查问时的效率,也反对了对 column 的排序以及条件过滤。
多租户
存储层面: 默认状况下,JanusGraph 会须要存储 edgestore, graphindex, system_properties, txlog 等多种数据类型,每个类型在 MySQL 中都有各自对的表,且表名应用租户名作为前缀,如 tenantA_edgestore,这样即便不同租户的数据在同一个数据库,在存储层面租户之间的数据也进行了隔离,缩小了相互影响,不便日常运维。(实践上每个租户能够独自调配一个数据库)
具体实现: 每个租户都会有各自的 MySQL 连贯配置,启动之后会为各个租户别离初始化数据库连贯,所有和 JanusGraph 的申请都会通过 Context 传递租户信息,以便在操作数据库时抉择该租户对应的连贯。
具体代码:
MysqlKcvTx: 实现了 AbstractStoreTransaction,对具体的 MySQL 连贯进行了封装,负责和数据库的交互,它的 commit 和 rollback 办法由封装的 MySQL 连贯真正实现。
MysqlKcvStore: 实现了 KeyColumnValueStore,是具体执行读写操作的入口,每一个类型的 Store 对应一个 MysqlKcvStore 实例,MysqlKcvStore 解决读写逻辑时,依据租户信息齐全自主组装 SQL 语句,SQL 语句会由 MysqlKcvTx 真正执行。
MysqlKcvStoreManager: 实现了 KeyColumnValueStoreManager,作为治理所有 MySQL 连贯和租户的入口,也保护了所有 Store 和 MysqlKcvStore 对象的映射关系。在解决不同租户对不同 Store 的读写申请时,依据租户信息,创立 MysqlKcvTx 对象,并将其调配给对应的 MysqlKcvStore 去执行。
public class MysqlKcvStoreManager implements KeyColumnValueStoreManager {
@Override
public StoreTransaction beginTransaction(BaseTransactionConfig config) throws BackendException {String tenant = TenantContext.getTenant();
if (!tenantToDataSourceMap.containsKey(tenant)) {
try {
// 初始化单个租户的 DataSource
initSingleDataSource(tenant);
} catch (SQLException e) {log.error("init mysql database source failed due to", e);
throw new BackendSQLException(String.format("init mysql database source failed due to", e.getMessage()));
}
}
// 获取数据库连贯
Connection connection = tenantToDataSourceMap.get(tenant).getConnection(false);
return new MysqlKcvTx(config, tenant, connection);
}
}
事务
简直所有与 JanusGraph 的交互都会开启事务,而且事务对于多个线程并发应用是平安的,然而 JanusGraph 的事务并不都反对 ACID,是否反对会取决于底层存储组件,对于某些存储组件来说,提供可序列化隔离机制或者多行原子写入代价会比拟大。
JanusGraph 中的每个图形操作都产生在事务的上下文中,依据 TinkerPop 的事务标准,每个线程执行图形上的第一个操作时便会关上针对图形数据库的事务,所有图形元素都与检索或者创立它们的事务范畴相关联,在应用 commit 或者 rollback 办法显式的敞开事务之后,与该事务关联的图形元素都将过期且不可用。
JanusGraph 提供了 AbstractStoreTransaction 接口,该接口蕴含 commit 和 rollback 的操作入口,在 MySQL 存储的实现中,MysqlKcvTx 实现了 AbstractStoreTransaction,对具体的 MySQL 连贯进行了封装,在其 commit 和 rollback 办法中调用 SQL 连贯的 commit 和 rollback 办法,以此实现对于 JanusGraph 事务的反对。
public class MysqlKcvTx extends AbstractStoreTransaction {private static final Logger log = LoggerFactory.getLogger(MysqlKcvTx.class);
private final Connection connection;
@Getter
private final String tenant;
public MysqlKcvTx(BaseTransactionConfig config, String tenant, Connection connection) {super(config);
this.tenant = tenant;
this.connection = connection;
}
@Override
public synchronized void commit() {
try {if (Objects.nonNull(connection)) {connection.commit();
connection.close();}
if (log.isDebugEnabled()) {log.debug("tx has been committed");
}
} catch (SQLException e) {log.error("failed to commit transaction", e);
}
}
@Override
public synchronized void rollback() {
try {if (Objects.nonNull(connection)) {connection.rollback();
connection.close();}
if (log.isDebugEnabled()) {log.debug("tx has been rollback");
}
} catch (SQLException e) {log.error("failed to rollback transaction", e);
}
}
public Connection getConnection() {return connection;}
}
数据库连接池
Hikari 是 SpringBoot 内置的数据库连接池,疾速、简略,做了很多优化,如应用 FastList 替换 ArrayList,自行研发无所汇合类 ConcurrentBag,字节码精简等,在性能测试中体现的也比其余竞品要好。
Druid 是另一个也十分优良的数据库连接池,为监控而生,内置弱小的监控性能,监控个性不影响性能。功能强大,能防 SQL 注入,内置 Loging 能诊断 Hack 利用行为。
对于两者的比照很多,此处不再赘述,尽管 Hikari 的性能号称要优于 Druid,然而思考到 Hikari 监控性能比拟弱,最终在实现的时候还是抉择了 Druid。
疑难问题
连贯超时
景象: 在进行数据导入测试时,服务报错 ” The last packet successfully received from the server was X milliseconds ago”,导致数据写入失败。
起因: 存在超大 table(有 8000 甚至 10000 列),这些 table 的元数据处理十分耗时(10000 列的可能须要 30 分钟),而且在处理过程中有很长一段时间和数据库并没有交互,数据库连贯始终闲暇。
解决办法: 调整 mysql server 端的 wait_timeout 参数,已调整到 3600s。调整 client 端数据库配置中连贯的最小闲暇工夫,已调整到 2400s。
剖析过程:
1. 狐疑是 mysql client 端没有减少闲暇清理或者保活机制,conneciton 在线程池中长时间没有应用,mysql 服务端曾经敞开该链接导致。尝试批改客户端 connection 闲暇工夫,减少 validationQuery 等常见措施,无果;
2. 依据打点发现单条音讯解决耗时过高,疑似线程卡死;
3. 新增打点发现线程没卡死,只是在执行一些十分耗时的逻辑,这时候曾经获取到了数据库连贯,然而在执行那些耗时逻辑的过程中和数据库没有任何交互,长时间没有应用数据库连贯,最终导致连贯被回收;
4. 调高了 MySQL server 端的 wait_timeout,以及 client 端的最小闲暇工夫,问题解决。
并行写入死锁
景象: 线程 thread-p-3-a-0 和线程 thread-p-7-a-0 在执行过程中都呈现 Deadlock。
具体日志如下:
[thread-p-3-a-0] ERROR org.janusgraph.diskstorage.mysql.MysqlKcvStore 313 - failed to insert query:INSERT INTO default_edgestore (g_key, g_column, g_value) VALUES (?,?,?) ON DUPLICATE KEY UPDATE g_value=?, params: key=A800000000001500, column=55A0, value=008000017CE616D0DD03674495
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
[thread-p-7-a-0] ERROR org.janusgraph.diskstorage.mysql.MysqlKcvStore 313 - failed to insert query:INSERT INTO default_edgestore (g_key, g_column, g_value) VALUES (?,?,?) ON DUPLICATE KEY UPDATE g_value=?, params: key=A800000000001500, column=55A0, value=008000017CE616D8E1036F3495
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
[thread-p-3-a-0] ERROR org.janusgraph.diskstorage.mysql.MysqlKcvStore 313 - failed to insert query:INSERT INTO default_edgestore (g_key, g_column, g_value) VALUES (?,?,?) ON DUPLICATE KEY UPDATE g_value=?, params: key=5000000000000080, column=55A0, value=008000017CE616F3C10442108A
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
[thread-p-7-a-0] ERROR org.janusgraph.diskstorage.mysql.MysqlKcvStore 313 - failed to insert query:INSERT INTO default_edgestore (g_key, g_column, g_value) VALUES (?,?,?) ON DUPLICATE KEY UPDATE g_value=?, params: key=5000000000000080, column=55A0, value=008000017CE61752B50556208A
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
起因:
1. 联合日志剖析,两个线程并发执行,须要对同样的多个记录加锁,然而程序不统一,进而导致了死锁。
2.55A0 这个 column 对应的 property 是 ”__modificationTimestamp”,该属性是 atlas 的零碎属性,当对图库中的点或者边有更新时,对应点或者边的 ”__modificationTimestamp” 属性会被更新。在并发导入数据的时候,加剧了资源竞争,所以会偶发死锁问题。
解决办法:
业务中并没有用到 ”__modificationTimestamp” 这个属性,通过批改 Atlas 代码,仅在创立点和边的时候为该属性赋值,后续更新时不再更新该属性,问题失去解决。
性能测试
环境搭建在字节外部 JanusGraph 次要用作 Data Catalog 服务的存储层,对于 MySQL 作为存储的性能测试并没有在 JanusGraph 层面进行,而是模仿 Data Catalog 服务的业务应用场景和数据,应用业务接口进行测试,次要会关注接口的响应工夫。
接口逻辑有所裁剪,在不影响外围读写流程的状况下,屏蔽掉对其余服务的依赖。
模仿单租户表单分片状况下,库表元数据创立、更新、查问,表之间血缘关系的创立、查问,以此反映在图库单次读写和屡次读写状况下 MySQL 的体现。
整个测试环境搭建在火山引擎上,总共应用 6 台 8C32G 的机器,硬件条件如下:
测试场景如下:
测试论断
总计 10 万个表(库数量为个位数,可疏忽)
在 10 万个表且模仿了表之间血缘关系的状况下,graphindex 表的数据量已有 7000 万,edgestore 表的数据量已有 1 亿 3000 万,业务接口的响应工夫根本在预期范畴内,可满足中小规模 Data Catalog 服务的存储要求。
总结
MySQL 作为 JanusGraph 的存储,有部署简略,不便运维等劣势,也能保持良好的扩展性,在中小规模的 Data Catalog 存储服务中也能放弃较好的性能水准,能够作为一个存储抉择。
市面上也有比拟成熟的 MySQL 分库分表计划,将来能够思考将其引入,以满足更大规模的存储需要。
火山引擎 Data Catalog 产品是基于字节跳动外部平台,通过多年业务场景和产品能力打磨,在私有云进行部署和公布,冀望帮忙更多内部客户发明数据价值。目前私有云产品已蕴含外部成熟的产品性能同时扩大若干 ToB 外围性能,正在逐渐对齐业界当先。
点击跳转 大数据研发治理 DataLeap 理解更多