更多技能沟通、求职时机,欢迎关注字节跳动数据渠道微信大众号,回复【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 的存储后端是可插拔的,只要做对应的适配即可,并且官方现已支撑了一批存储体系。结合字节的技能栈以及咱们的诉求,做了以下的评价。

各类存储体系比较

DataLeap 数据资产实战:如何实现存储优化?

  • 因投入本钱过高,咱们不接受自己运维有状态集群,排除了 HBase 和 Cassandra;

  • 从当时数据量与将来的可扩展性考虑,单机计划不可选,排除了 BerkeleyDB;

  • 相同由于人力本钱,需求做极许多开发改造的计划暂时不考虑,排除了 Redis。

最终咱们挑选了 MySQL 来推进到下一步。

MySQL 的理论可行性

  • 能够支撑 Key-Value(后续简称 KV 模型)或许 Key-Column-Value(后续简称 KCV 模型)的存储模型,聚集索引 B+树排序拜访,支撑依据 Key 或许 Key-Column 的 Range Query,一切查询都走索引,且避免内存中重排序,功率初步判别可接受。
  • 中台内的其他体系,最大的 MySQL 单体现已到达亿等级,且 MySQL 有老练的分库分表处理计划,判别数据量能够支撑。
  • 在详细运用场景中,关于写入的功率要求不高,由于许多的数据都是离线任务完结,判别 MySQL 在写入上的功率不会成为瓶颈。

总体规划

DataLeap 数据资产实战:如何实现存储优化?

  • 保护一张 Meta 表做 lookup 用,Meta 表中存储租户与 DataSource(库)之间的映射联系,以及 Shards 等租户等级的装备信息。

  • StoreManager 作为进口,在 openTransaction 的时分将租户信息注入到 StoreTransaction 中,并返回租户等级的 DataSource。

  • StoreManager 中以 name 为 Key,保护一组 Store,Store 与存储的数据类型有关,具有跨租户才能

    常见的 Store 有system_properiestx_loggraphindexedgestore

  • 关于 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。

DataLeap 数据资产实战:如何实现存储优化?

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 衔接进行了封装,负责和数据库的交互,它的commitrollback办法由封装的 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接口,该接口包括commitrollback的操作进口,在 MySQL 存储的完结中,MysqlKcvTx完结了AbstractStoreTransaction,对详细的 MySQL 衔接进行了封装,在其commitrollback办法中调用 SQL 衔接的commitrollback办法,以此完结关于 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 的机器,硬件条件如下:

DataLeap 数据资产实战:如何实现存储优化?

测验场景如下:

DataLeap 数据资产实战:如何实现存储优化?

测验结论

总计 10 万个表(库数量为个位数,可疏忽)

DataLeap 数据资产实战:如何实现存储优化?

在 10 万个表且模仿了表之间血缘联系的情况下,graphindex表的数据量已有 7000 万,edgestore表的数据量已有 1 亿 3000 万,事务接口的呼应时刻基本在预期范围内,可满意中小规划 Data Catalog 服务的存储要求。

总结

MySQL 作为 JanusGraph 的存储,有布置简略,方便运维等优势,也能保持杰出的扩展性,在中小规划的 Data Catalog 存储服务中也能保持较好的功用水准,能够作为一个存储挑选。

市面上也有比较老练的 MySQL 分库分表计划,未来能够考虑将其引入,以满意更大规划的存储需求。

火山引擎 Data Catalog 产品是依据字节跳动内部渠道,通过多年事务场景和产品才能打磨,在公有云进行布置和发布,希望协助更多外部客户创造数据价值。现在公有云产品已包括内部老练的产品功用一起扩展若干 ToB 中心功用,正在逐步对齐业界领先 Data Catalog 云产品各项才能。

点击跳转 大数据研制办理DataLeap 了解更多