JanusGraph - 分布式id的生成策略

初始数据:

@Test
public void addVertexTest(){
    List<Object> godProperties = new ArrayList<>();
    godProperties.add(T.label);
    godProperties.add("god");

    godProperties.add("name");
    godProperties.add("lyy");

    godProperties.add("age");
    godProperties.add(18);

    JanusGraphVertex godVertex = graph.addVertex(godProperties.toArray());

    assertNotNull(godVertex);
}

在诸神之图中添加一个name为lyy节点;看下执行流程,注意,此处主要分析的节点的分布式id生成代码!

1、调用JanusGraphBlueprintsGraph类的AddVertex方法

@Override
public JanusGraphVertex addVertex(Object... keyValues) {
    // 添加节点
    return getAutoStartTx().addVertex(keyValues);
}

2、调用JanusGraphBlueprintsTransaction的addVertex方法

public JanusGraphVertex addVertex(Object… keyValues) {
// 。。。省略了其他的处理
// 该处生成节点对象,包含节点的唯一id生成逻辑
final JanusGraphVertex vertex = addVertex(id, label);
// 。。。省略了其他的处理
return vertex;
}
3、调用StandardJanusGraphTx的addVertex方法

@Override
public JanusGraphVertex addVertex(Long vertexId, VertexLabel label) {
    // 。。。省略了其他的处理
    if (vertexId != null) {
        vertex.setId(vertexId);
    } else if (config.hasAssignIDsImmediately() || label.isPartitioned()) {
        graph.assignID(vertex,label);  // 为节点分配正式的节点id!
    }
     // 。。。省略了其他的处理
    return vertex;
}

4、调用VertexIDAssigner的assignID(InternalElement element, IDManager.VertexIDType vertexIDType)方法

private void assignID(InternalElement element, IDManager.VertexIDType vertexIDType) {
    // 开始获取节点分布式唯一id
    // 因为一些异常导致获取节点id失败,进行重试,重试此为默认为1000次
    for (int attempt = 0; attempt < MAX_PARTITION_RENEW_ATTEMPTS; attempt++) {
        // 初始化一个partiiton id
        long partitionID = -1;
        // 获取一个partition id
        // 不同类型的数据,partition id的获取方式也有所不同
        if (element instanceof JanusGraphSchemaVertex) {
            // 为partition id赋值
        }
        try {
            // 正式分配节点id, 依据partition id 和 节点类型
            assignID(element, partitionID, vertexIDType);
        } catch (IDPoolExhaustedException e) {
            continue; //try again on a different partition
        }
        assert element.hasId();
        // 。。。省略了其他代码
    }
}

5、调用了VertexIDAssigner的assignID(final InternalElement element, final long partitionIDl, final IDManager.VertexIDType userVertexIDType)方法

private void assignID(final InternalElement element, final long partitionIDl, final IDManager.VertexIDType userVertexIDType) {
  
    final int partitionID = (int) partitionIDl;

    // count为分布式id组成中的一部分,占55个字节
    // 分布式id的唯一性保证,就在于`count`基于`partition`维度的唯一性
    long count;
    if (element instanceof JanusGraphSchemaVertex) { // schema节点处理
        Preconditions.checkArgument(partitionID==IDManager.SCHEMA_PARTITION);
        count = schemaIdPool.nextID();
    } else if (userVertexIDType==IDManager.VertexIDType.PartitionedVertex) { // 配置的热点节点,类似于`makeVertexLabel('product').partition()`的处理
        count = partitionVertexIdPool.nextID();
    } else { // 普通节点和边类型的处理
        // 首先获取当前partition敌营的idPool
        PartitionIDPool partitionPool = idPools.get(partitionID);
        // 如果当前分区对应的IDPool为空,则创建一个默认的IDPool,默认size = 0
        if (partitionPool == null) {
            // 在PartitionIDPool中包含多种类型对应的StandardIDPool类型
            // StandardIDPool中包含对应的block信息和count信息
            partitionPool = new PartitionIDPool(partitionID, idAuthority, idManager, renewTimeoutMS, renewBufferPercentage);
            // 缓存下来
            idPools.putIfAbsent(partitionID,partitionPool);
            // 从缓存中再重新拿出
            partitionPool = idPools.get(partitionID);
        }
        // 确保partitionPool不为空
        Preconditions.checkNotNull(partitionPool);
        // 判断当前分区的IDPool是否枯竭;已经被用完
        if (partitionPool.isExhausted()) {
            // 如果被用完,则将该分区id放到对应的缓存中,避免之后获取分区id再获取到该分区id
            placementStrategy.exhaustedPartition(partitionID);
            // 抛出IDPool异常, 最外层捕获,然后进行重试获取节点id
            throw new IDPoolExhaustedException("Exhausted id pool for partition: " + partitionID);
        }
        // 存储当前类型对应的IDPool,因为partitionPool中保存好几个类型的IDPool
        IDPool idPool;
        if (element instanceof JanusGraphRelation) {
            idPool = partitionPool.getPool(PoolType.RELATION);
        } else {
            Preconditions.checkArgument(userVertexIDType!=null);
            idPool = partitionPool.getPool(PoolType.getPoolTypeFor(userVertexIDType));
        }
        try {
            // 重要!!!! 依据给定的IDPool获取count值!!!!
            // 在此语句中设计 block的初始化 和 double buffer block的处理!
            count = idPool.nextID();
            partitionPool.accessed();
        } catch (IDPoolExhaustedException e) { // 如果该IDPool被用完,抛出IDPool异常, 最外层捕获,然后进行重试获取节点id
            log.debug("Pool exhausted for partition id {}", partitionID);
            placementStrategy.exhaustedPartition(partitionID);
            partitionPool.exhaustedIdPool();
            throw e;
        }
    }

    // 组装最终的分布式id:[count + partition id + ID padding]
    long elementId;
    if (element instanceof InternalRelation) {
        elementId = idManager.getRelationID(count, partitionID);
    } else if (element instanceof PropertyKey) {
        elementId = IDManager.getSchemaId(IDManager.VertexIDType.UserPropertyKey,count);
    } else if (element instanceof EdgeLabel) {
        elementId = IDManager.getSchemaId(IDManager.VertexIDType.UserEdgeLabel, count);
    } else if (element instanceof VertexLabel) {
        elementId = IDManager.getSchemaId(IDManager.VertexIDType.VertexLabel, count);
    } else if (element instanceof JanusGraphSchemaVertex) {
        elementId = IDManager.getSchemaId(IDManager.VertexIDType.GenericSchemaType,count);
    } else {
        elementId = idManager.getVertexID(count, partitionID, userVertexIDType);
    }

    Preconditions.checkArgument(elementId >= 0);
    // 对节点对象赋值其分布式唯一id
    element.setId(elementId);
}

上述代码,我们拿到了对应的IdPool,有两种情况:

第一次获取分布式id时,分区对应的IDPool初始化为默认的size = 0的IDPool
分区对应的IDPool不是初次获取
这两种情况的处理,都在代码count = idPool.nextID()的StandardIDPool类中的nextID()方法中被处理!

在分析该代码之前,我们需要知道 PartitionIDPool和StandardIDPool的关系:

每个partition都有一个对应的PartitionIDPool extends EnumMap<PoolType,IDPool> 是一个枚举map类型;

每一个PartitionIDPool都有对应的不同类型的StandardIDPool:

NORMAL_VERTEX:用于vertex id的分配
UNMODIFIABLE_VERTEX:用于schema label id的分配
RELATION:用于edge id的分配
在StandardIDPool中包含多个字段,分别代表不同的含义,抽取几个重要的字段进行介绍:

private static final int RENEW_ID_COUNT = 100; 
private final long idUpperBound; // Block的最大值,默认为2的55次幂
private final int partition; // 当前pool对应的分区
private final int idNamespace; // 标识pool为那种类型的pool,上述的三种类型NORMAL_VERTEX、UNMODIFIABLE_VERTEX、RELATION;值为当前枚举值在枚举中的位置

private final Duration renewTimeout;// 重新获取block的超时时间
private final double renewBufferPercentage;// 双buffer中,当第一个buffer block使用的百分比,到达配置的百分比则触发other buffer block的获取

private IDBlock currentBlock; // 当前的block
private long currentIndex; // 标识当前block使用到那一个位置
private long renewBlockIndex; // 依据currentBlock.numIds()*renewBufferPercentage来获取这个值,主要用于在当前的block在消费到某个index的时候触发获取下一个buffer block

private volatile IDBlock nextBlock;// 双buffer中的另外一个block

private final ThreadPoolExecutor exec;// 异步获取双buffer的线程池

6、调用了StandardIDPool类中的nextID方法

经过上述分析,我们知道,分布式唯一id的唯一性是由在partition维度下的count的值的唯一性来保证的;

上述代码通过调用IDPool的nextId来获取count值;

下述代码就是获取count的逻辑;

@Override
public synchronized long nextID() {
    // currentIndex标识当前的index小于current block的最大值
    assert currentIndex <= currentBlock.numIds();

    // 此处涉及两种情况:
    // 1、分区对应的IDPool是第一次被初始化;则currentIndex = 0; currentBlock.numIds() = 0;
    // 2、分区对应的该IDPool不是第一次,但是此次的index正好使用到了current block的最后一个count
    if (currentIndex == currentBlock.numIds()) {
        try {
            // 将current block赋值为next block
            // next block置空 并计算renewBlockIndex
            nextBlock();
        } catch (InterruptedException e) {
            throw new JanusGraphException("Could not renew id block due to interruption", e);
        }
    }
    
    // 在使用current block的过程中,当current index  ==  renewBlockIndex时,触发double buffer next block的异步获取!!!!
    if (currentIndex == renewBlockIndex) {
        // 异步获取next block
        startIDBlockGetter();
    }
    
    // 生成最终的count
    long returnId = currentBlock.getId(currentIndex);
    // current index + 1
    currentIndex++;
    if (returnId >= idUpperBound) throw new IDPoolExhaustedException("Reached id upper bound of " + idUpperBound);
    log.trace("partition({})-namespace({}) Returned id: {}", partition, idNamespace, returnId);
    // 返回最终获取的分区维度的全局唯一count
    return returnId;
}

上述代码中进行了两次判断:

currentIndex == currentBlock.numIds():
第一次生成分布式id:此处判断即为 0==0;然后生成新的block
非第一次生成分布式id:等于情况下标识当前的block已经使用完了,需要切换为next block
currentIndex == renewBlockIndex
renew index:标识index使用多少后开始获取下一个double buffer 的next block;有一个默认值100,主要为了兼容第一次分布式id的生成;相等则会触发异步获取下一个next block
下面我们分别对nextBlock();逻辑和startIDBlockGetter();进行分析;

7、调用了StandardIDPool类中的nextBlock方法

private synchronized void nextBlock() throws InterruptedException {
    // 在分区对应的IDPool第一次使用时,double buffer的nextBlock为空
    if (null == nextBlock && null == idBlockFuture) {
        // 异步启动 获取id block
        startIDBlockGetter();
    }

    // 也是在分区对应的IDPool第一次使用时,因为上述为异步获取,所以在执行到这一步时nextBlock可能还没拿到
    // 所以需要阻塞等待block的获取
    if (null == nextBlock) {
        waitForIDBlockGetter();
    }

    // 将当前使用block指向next block
    currentBlock = nextBlock;
    // index清零
    currentIndex = 0;
    // nextBlock置空
    nextBlock = null;

    // renewBlockIndex用于双buffer中,当第一个buffer block使用的百分比,到达配置的百分比则触发other buffer block的获取
    // 值current block 对应的count数量 - (值current block 对应的count数量 * 为renewBufferPercentage配置的剩余空间百分比)
    // 在使用current block的时候,当current index  ==  renewBlockIndex时,触发double buffer next block的异步获取!!!!
    renewBlockIndex = Math.max(0,currentBlock.numIds()-Math.max(RENEW_ID_COUNT, Math.round(currentBlock.numIds()*renewBufferPercentage)));
}

主要是做了三件事:

1、block是否为空,为空的话则异步获取一个block
2、nextBlock不为空的情况下:next赋值到current、next置空、index置零
3、计算获取下一个nextBlock的触发index renewBlockIndex值
8、调用了StandardIDPool类中的startIDBlockGetter方法

private synchronized void startIDBlockGetter() {
    Preconditions.checkArgument(idBlockFuture == null, idBlockFuture);
    if (closed) return; //Don't renew anymore if closed
    //Renew buffer
    log.debug("Starting id block renewal thread upon {}", currentIndex);
    // 创建一个线程对象,包含给定的权限控制类、分区、命名空间、超时时间
    idBlockGetter = new IDBlockGetter(idAuthority, partition, idNamespace, renewTimeout);
    // 提交获取double buffer的线程任务,异步执行
    idBlockFuture = exec.submit(idBlockGetter);
}

其中创建一个线程任务,提交到线程池exec进行异步执行;

下面看下,线程类的call方法主要是调用了idAuthority.getIDBlock方法,这个方法主要是基于Hbase来获取还未使用的block;

/**
 * 获取double buffer block的线程类
 */
private static class IDBlockGetter implements Callable<IDBlock> {

    // 省略部分代码
    @Override
    public IDBlock call() {
        Stopwatch running = Stopwatch.createStarted();
        try {
            // 此处调用idAuthority 调用HBase进行占用获取Block
            IDBlock idBlock = idAuthority.getIDBlock(partition, idNamespace, renewTimeout);
            return idBlock;
        } catch (BackendException e) {}
    }
}

9、调用ConsistentKeyIDAuthority类的getIDBlock方法

@Override
public synchronized IDBlock getIDBlock(final int partition, final int idNamespace, Duration timeout) throws BackendException {
  
    // 开始时间
    final Timer methodTime = times.getTimer().start();

    // 获取当前命名空间配置的blockSize,默认值10000;可自定义配置
    final long blockSize = getBlockSize(idNamespace);
    // 获取当前命名空间配置的最大id值idUpperBound;值为:2的55次幂大小
    final long idUpperBound = getIdUpperBound(idNamespace);
    // uniqueIdBitWidth标识uniqueId占用的位数;uniqueId为了兼容“关闭分布式id唯一性保障”的开关情况,uniqueIdBitWidth默认值=4
    // 值:64-1(默认0)-5(分区占用位数)-3(ID Padding占用位数)-4(uniqueIdBitWidth) = 51;标识block中的上限为2的51次幂大小
    final int maxAvailableBits = (VariableLong.unsignedBitLength(idUpperBound)-1)-uniqueIdBitWidth;

    // 标识block中的上限为2的51次幂大小
    final long idBlockUpperBound = (1L <<maxAvailableBits);

    // UniquePID用尽的UniquePID集合,默认情况下,randomUniqueIDLimit = 0;
    final List<Integer> exhaustedUniquePIDs = new ArrayList<>(randomUniqueIDLimit);

    // 默认0.3秒  用于处理TemporaryBackendException异常情况(后端存储出现问题)下:阻塞一断时间,然后进行重试
    Duration backoffMS = idApplicationWaitMS;

    // 从开始获取IDBlock开始,持续超时时间(默认2分钟)内重试获取IDBlock
    while (methodTime.elapsed().compareTo(timeout) < 0) {
        final int uniquePID = getUniquePartitionID(); // 获取uniquePID,默认情况下“开启分布式id唯一性控制”,值 = 0; 当“关闭分布式id唯一性控制”时为一个随机值
        final StaticBuffer partitionKey = getPartitionKey(partition,idNamespace,uniquePID); // 依据partition + idNamespace + uniquePID组装一个RowKey
        try {
            long nextStart = getCurrentID(partitionKey); // 从Hbase中获取当前partition对应的IDPool中被分配的最大值,用来作为当前申请新的block的开始值
            if (idBlockUpperBound - blockSize <= nextStart) { // 确保还未被分配的id池中的id个数,大于等于blockSize
                // 相应处理
            }

            long nextEnd = nextStart + blockSize; // 获取当前想要获取block的最大值
            StaticBuffer target = null;

            // attempt to write our claim on the next id block
            boolean success = false;
            try {
                Timer writeTimer = times.getTimer().start(); // ===开始:开始进行插入自身的block需求到Hbase
                target = getBlockApplication(nextEnd, writeTimer.getStartTime()); // 组装对应的Column: -nextEnd +  当前时间戳 + uid(唯一标识当前图实例)
                final StaticBuffer finalTarget = target; // copy for the inner class
                BackendOperation.execute(txh -> { // 异步插入当前生成的RowKey 和 Column
                    idStore.mutate(partitionKey, Collections.singletonList(StaticArrayEntry.of(finalTarget)), KeyColumnValueStore.NO_DELETIONS, txh);
                    return true;
                },this,times);
                writeTimer.stop(); // ===结束:插入完成

                final boolean distributed = manager.getFeatures().isDistributed();
                Duration writeElapsed = writeTimer.elapsed(); // ===获取方才插入的时间耗时
                if (idApplicationWaitMS.compareTo(writeElapsed) < 0 && distributed) { // 判断是否超过配置的超时时间,超过则报错TemporaryBackendException,然后等待一断时间进行重试
                    throw new TemporaryBackendException("Wrote claim for id block [" + nextStart + ", " + nextEnd + ") in " + (writeElapsed) + " => too slow, threshold is: " + idApplicationWaitMS);
                } else {

                    assert 0 != target.length();
                    final StaticBuffer[] slice = getBlockSlice(nextEnd); // 组装下述基于上述Rowkey的Column的查找范围:(-nextEnd + 0 : 0nextEnd + 最大值)      

                    final List<Entry> blocks = BackendOperation.execute( // 异步获取指定Rowkey和指定Column区间的值
                        (BackendOperation.Transactional<List<Entry>>) txh -> idStore.getSlice(new KeySliceQuery(partitionKey, slice[0], slice[1]), txh),this,times);
                    if (blocks == null) throw new TemporaryBackendException("Could not read from storage");
                    if (blocks.isEmpty())
                        throw new PermanentBackendException("It seems there is a race-condition in the block application. " +
                                "If you have multiple JanusGraph instances running on one physical machine, ensure that they have unique machine idAuthorities");

                    if (target.equals(blocks.get(0).getColumnAs(StaticBuffer.STATIC_FACTORY))) { // 如果获取的集合中,当前的图实例插入的数据是第一条,则表示获取block; 如果不是第一条,则获取Block失败
                        // 组装IDBlock对象
                        ConsistentKeyIDBlock idBlock = new ConsistentKeyIDBlock(nextStart,blockSize,uniqueIdBitWidth,uniquePID);

                        if (log.isDebugEnabled()) {
                                idBlock, partition, idNamespace, uid);
                        }

                        success = true;
                        return idBlock; // 返回
                    } else { }
                }
            } finally {
                if (!success && null != target) { // 在获取Block失败后,删除当前的插入; 如果没有失败,则保留当前的插入,在hbase中标识该Block已经被占用
                    //Delete claim to not pollute id space
                    for (int attempt = 0; attempt < ROLLBACK_ATTEMPTS; attempt++) { // 回滚:删除当前插入,尝试次数5次
                    }
                }
            }
        } catch (UniqueIDExhaustedException e) {
            // No need to increment the backoff wait time or to sleep
            log.warn(e.getMessage());
        } catch (TemporaryBackendException e) {
            backoffMS = Durations.min(backoffMS.multipliedBy(2), idApplicationWaitMS.multipliedBy(32));
            sleepAndConvertInterrupts(backoffMS); \
        }
    }

    throw new TemporaryLockingException();
}

亚马逊测评 www.yisuping.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值