1.背景
大量数据(百亿级别)导入hugegraph库时,官方导入工具loader实际使用效率低,不适合亿级数据的导入。且loader通过原生接口进行数据导入,影响生产集群的稳定性,同时数据的持续写入会因为 flush,compaction 等机制占用较多的系统资源。
因此考虑使用开发图数据bulkload至存储端直接将原始数据生成Hfile,绕过regionserver直接将文件上传至合适位置。这同时需要绕过图server,
而在具体的bulkload选型中,有mapreduce和spark两种方式,使用spark肉眼可见的好处是速度相较mr快,但由于Spark Bulkload 公开资料很少,具体流程配置会遇到很多坑,加之考虑人力因素,因此选择使用mr进行bulkload。
2. bulkload 总体流程
- 输入为图数据的点边数据,这里取按默认大小block切分
- bulkload核心步骤在于mapper的
parser层,需要将输入的原始数据编码为hbase存储需要的rowkey和value,此外还有索引数据(不作主要叙述) - MapReduce的reducer由HBase负责,通过方法
HFileOutputFormat2.configureIncrementalLoad()进行配置,将reduce task数量置为region数量,生成按region分的Hfile - 使用
completebulkload将HFile加载到在线HBase集群,将生成的Hfile移动到对应Region的hdfs目录下,由于此步骤绕过了RegionServer(不会对线上造成影响),还需要告知region对应的RegionServer,才可提供对外的服务,至此 bulkload就成功了。
3. bulkload方案
- 首先需要确定输入映射文件,文本格式的输入数据是没有标示的,因此我们需要提前对输入数据的类型进行标记,判断属于哪一种类型的点/边,进行相应的解析。
- 从图server端获取
属性/点/边的id和相关信息,保证我们进行编码的数据和图数据库元数据映射的绝对匹配 - 同时支持对json数据的导入,json数据相比text数据好处就是有标示都为kv对,不需要人为进行对应
- 从图server端获取
- 将映射信息载入任务中解析为map,
name,vertexlabel>,<name,edgelabel>,<name,propertykeys>,这样我们输入点边类型名时,可以对应到其相关的元数据信息。我这里选择conf的方式进行传入map,并在Init时进行映射资源的统一创建。尽量简化map的资源创建(每个map处理的数据类型唯一)。 - 编写map函数将输入数据加工为hbase需要的数据格式,涉及图数据点边的序列化编码
- 按region生成hfile,reduce数目为region的数量,在load到hbase.
3.1 点数据序列化
首先我们找一条点数据作为例子进行序列化,将同一条数据通过api插入hbase,查看自行序列化和图server序列化后字节流是否完全相同,即可自证是否序列化成功
3.1.1 首先我们来模拟一条普通点数据:
text(主要数据格式):
https://test.com [2342065140db0c14553f1f864d8efdc7e0-1613301392] [vt] ddddddddddd
json(api方式示例数据,便于读者理解输入源):
{
"label": "URL",
"properties": {
"src": "[2342065140db0c14553f1f864d8efdc7e0-1613301392]",
"url": "https://test.com",
"permalink": "[vt]",
"url_key":"dddddddddddd",]
}
}
- label用于标识点的种类,此处label 为URL,比如:在一所学校中,小红可以是老师,也可以是学生,label就是用于标标识点类型为教师还是学生,每种label在数据库中都有数值
labelid来表示.在实际的文本数据中需要标示点边label才可进行解析 - properties 中选择一个property来作为点的rowkey组成,此property全局唯一。url作为标识rowkey的唯一
id。 src,permalink,url_key均为属性值存在于value中,每个属性具有它的数值id作为propertyId来表示属性的种类. labelid+id作为rowkey,properties存储于value中,构成了一条存储于hbase中的点数据。
3.1.2 来看看点数据在hbase中的存储结构:
在0.11版本的hugegraph中点数据通过属性长度来作为rowkey的前缀,考虑到业务数据如大量的ip,手机号都是相同长度的数据,会导致rowkey的热点问题,降低hbase性能。(此处的属性同时作为唯一主键)
结合源码序列化:
// 1.1 计算点属性个数, 预分配Buffer大小
int propsCount = vertex.getProperties().size();
BytesBuffer buffer = BytesBuffer.allocate(8 + 16 * propsCount);
// 1.2 首先写labelId (数值)
buffer.writeId(vertex.schemaLabel().id());
// 2.1 然后把所有点属性组合到一起
this.formatProperties(vertex.getProperties().values(), buffer);
// 2.2 (可选) 开启TTL后记录过期时间写入, 但hbase实际使用 原生的ttl设置
if (vertex.hasTtl()) {
entry.ttl(vertex.ttl()); this.formatExpiredTime(vertex.expiredTime(),
buffer);
// 对应上面组合所有属性
protected void formatProperties(Collection<HugeProperty<?props,BytesBuffer buffer) {
// 1. 先写属性总个数
buffer.writeVInt(props.size());
// 2. 再写属性数据 (属性keyId+ 属性值)
for (HugeProperty<?> property : props) {
PropertyKey pkey = property.propertyKey();
buffer.writeVInt(SchemaElement.schemaId(pkey.id()));
buffer.writeProperty(pkey, property.value());
// list/set循环写入
}
由此可知,我们需要将原始数据拆分编码为需要的点labelId, 点id,点propertyId, property 一一对应。那么我们就需要获得labelId-label的映射关系,以及 propertyId-property的对应关系,以及所解析数据的所属类型,然而文本数据并不是如json数据一般具有标识哪种类型的点边,因为我们在解析时还需要知道解析数据的所属类型。
3.1.3 序列化主要步骤
- 首先,我们需要构造出点对象,需要在map的init阶段统一构建,否则parser中会构建大量的对象极其影响执行效率:
public static FakeObjects fo = new FakeObjects();
vertex = new HugeVertex(fo.graph(), IdGenerator.of(IdGenerator.of(labelid.id())),
fo.graph().vertexLabel(IdGenerator.of(labelid.id()))); //初始化
- 需要载入图的元数据信息。通俗地说就是点label的相关信息,property的相关构成和映射信息,以便我们能将文本数据和元数据一一对应。我们将元信息加载到Configuration中。构成一个个kv
<propertyname,property>对,<VertexLabelId,VertexLabel>以及<EdgeLabelId,EdgeLabel>(构建边时使用)
propertys = headers.get("property");
propertys.forEach((key1, value) -> {
String property1 = (String) value;
com.baidu.hugegraph.structure.schema.PropertyKey propertykey = proMap.get(property1);
Cardinality cardinality = Enum.valueOf(Cardinality.class, propertykey.cardinality().name());
DataType dataType = Enum.valueOf(DataType.class, propertykey.dataType().name());
com.baidu.hugegraph.schema.PropertyKey pro = fo.newPropertyKey(
IdGenerator.of(propertykey.id()), propertykey.name(), dataType, cardinality);
properMap.put(property1,pro);
});
- 准备工作做好后,我们就可以将文本数据解析并组成点对象使用上面的图数据库序列化方式进行编码,构成一个hbase需要的kv Bytes数组
int propsCount = vertex.getProperties().size();
BytesBuffer bufferpro = BytesBuffer.allocate(8 + 16 * propsCount);
bufferpro.writeId(vertex.schemaLabel().id());
bufferpro.writeVInt(propsCount);
for (HugeProperty<?> propertyk : vertex.getProperties().values()) {
PropertyKey pkey = propertyk.propertyKey();
bufferpro.writeVInt(SchemaElement.schemaId(pkey.id()));
bufferpro.writeProperty(pkey, propertyk.value());
}
3.2 边数据序列化
3.2.1 边数据的kv构成
- edge的rowkey由五部分构成
source id+direction+edgelabelId+sortkey+target id, source id和target id表示这条边的起点和终点的点id(hugegraph中边为有向图)direction标示边的方向 为 out / in,edgelabelId表示边的种类sortkey比较特殊,是可选的,用以区别同起点和终点的边。比如,A给B打了十几个电话,如果没有sortkey,在图数据库中将只存在一条A到B的边,而加上了sortkey,那么图数据库会记录这十几条边,按时间区分,时间作为此处的sortkey,可根据实际业务需要来决定使用与否sortkey,sortkey过大会导致一条数据膨胀,影响图数据库的查询效率,故根据实际情况可进行合并等。毕竟图数据库主要并不是用来记录流水账和日志的😄
3.2.2 边数据序列化核心步骤
- 创建EdgeId对象,构建rowkey
// 需要创建边id对象,来作为rowkey,需要从原始数据中解析出源点id,边方向,边类型,sortkey(未开启),目标点id
EdgeId edgeId1 = new EdgeId(source, Directions.OUT, IdGenerator.of(labelid.id()), "", target);
BytesBuffer edgeName = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID).writeEdgeId(edgeId1);
byte[] rowkey = edgeName.bytes();
- 创建Edge对象,构建边信息,此处仍然需要在init中统一创建边资源。
eid = EdgeId.parse("L123456>1>>L987654");
source = IdGenerator.of("1"); //初始化一个随机值,id不可空
target = IdGenerator.of("1");
//此处el无用 只需要pk 每条parse会覆盖写property key
edge = new HugeEdge(fo.graph(), eid, ip_cert);
- 将原始数据进行解析,获得需要的边id和边,构建边对象,将其序列化。
//
int count = edge.getProperties().values().size();
BytesBuffer bufferpro=BytesBuffer.allocate(4 + 16 * count);
bufferpro.writeVInt(count);
for (HugeProperty<?> propertyk : edge.getProperties().values()) {
com.baidu.hugegraph.schema.PropertyKey pkey = propertyk.propertyKey();
bufferpro.writeVInt(SchemaElement.schemaId(pkey.id()));
bufferpro.writeProperty(pkey, propertyk.value());
}
4.性能
- 经过实测,导入点边数据60亿/h(我们的yarn集群资源太少,严重影响速度)
- 具体时长和导入数据分布是否均匀有关,若单个region数据集中,将导致reduce过程缓慢,长尾效应严重(百分之九十的数据在前百分之二十的时间执行完毕,剩下数据缓慢完成)由于hugegraph的rowkey前缀策略是取长度编码,因此会有一定的数据集中,后续将rowkey考虑改为hash较好。