乐趣区

关于接口设计:一种接口依赖关系分层方案-京东云技术团队

1、背景

到店商详迭代过程中,须要提供的对外能力越来越多,如预约日历、左近门店、为你举荐等。这其中不可避免会呈现多个下层能力依赖同一个底层接口的场景。最后采纳的计划是对外 API 入口进来后获取对应的能力,并发调用多项能力,由能力层调用对应的数据链路,进行业务解决。然而,随着接入性能的增多,这种状况导致了底层数据服务的反复调用,如商品配置信息,在一次 API 调用过程中反复调了 3 次,当流量增大或能力项愈多时,对底层服务的压力会成倍增加。

正值 618 大促,各方接口的调用都会大幅度减少。通过梳理接口依赖关系来缩小反复调用,对本零碎而言,升高了调用数据接口时的线程占用次数,能够无效降级 CPU。对调用方来说,缩小了调用次数,可缩小调用方的资源耗费,保障底层服务的稳定性。

原始调用形式:

2、优化

基于上述问题,采纳底层接口依赖分层调用的计划。梳理接口依赖关系,逐层向上调用,注入数据,如此将同一接口的调用抽取到某层,仅调用一次,即可在整条链路应用。

改良调用形式:

只有分层后即可在每层采纳多线程并发的形式调用,因为同一层级中的接口无先后依赖关系。

3、如何分层?

接下来,如何梳理接口层级关系就至关重要。

接口梳理分层流程如下:

第一步:构建层级构造

首先获取到能力层依赖项并遍历,而后调用生成数据节点办法。办法流程如下:构建以后节点,检测循环依赖(存在循环依赖会导致栈溢出),获取并遍历节点依赖项,递归生成子节点,寄存子节点。

第二步:节点平铺

定义 Map 保护平铺构造,调用平铺办法。办法流程如下:遍历层级构造,判断以后节点是否已存在 map 中,存在时与原节点比拟将层级大的节点放入(去除反复项),不存在时间接放入即可。而后解决子节点,递归调用平铺办法,解决所有节点。

第三步:分层(分组排序)

流解决平铺构造,解决层级分组,存储在 TreeMap 中保护天然排序。对应 key 中的数据节点 Set<DataNode> 需用多线程并发调用,以保障链路调用工夫

1 首先,定义数据结构用于保护调用链路

Q1:为什么须要定义先人节点?

A1:为了判断接口是否存在循环依赖。如果接口存在循环依赖而不检测将导致调用栈溢出,故而在调用过程中要防止并检测循环依赖。在遍历子节点过程中,如果发现以后节点的先人曾经蕴含以后子节点,阐明依赖关系呈现了环路,即循环依赖,此时抛异样终止后续流程防止栈溢出。

public class DataNode {
    /**
     * 节点名称
     */
    private String name;
    /**
     * 节点层级
     */
    private int level;
    /**
     * 先人节点
     */
    private List<String> ancestors;
    /**
     * 子节点
     */
    private List<DataNode> children;
}

2 获取能力层的接口依赖,并生成对应的数据节点

Q1:生成节点时如何保护层级?

A1:从能力层依赖开始,层级从 1 递减。每获取一次底层依赖,底层依赖所生成的节点层级即父节点层级 +1。

/**
 * 构建层级构造
 *
 * @param handlers 接口依赖
 * @return 数据节点集
 */
private List<DataNode> buildLevel(Set<String> handlers) {List<DataNode> result = Lists.newArrayList();

    for (String next : handlers) {DataNode dataNode = generateNode(next, 1, null, null);
        result.add(dataNode);
    }
    return result;
}

/**
 * 生成数据节点
 *
 * @param name 节点名称
 * @param level 节点层级
 * @param ancestors 先人节点(除父辈)* @param parent 父节点
 * @return DataNode 数据节点
 */
private DataNode generateNode(String name, int level, List<String> ancestors, String parent) {AbstractInfraHandler abstractInfraHandler = abstractInfraHandlerMap.get(name);
    Set<String> infraDependencyHandlerNames = abstractInfraHandler.getInfraDependencyHandlerNames();
    // 根节点
    DataNode dataNode = new DataNode(name);
    dataNode.setLevel(level);
    dataNode.putAncestor(ancestors, parent);
    if (CollectionUtils.isNotEmpty(dataNode.getAncestors()) && dataNode.getAncestors().contains(name)) {throw new IllegalStateException("依赖关系中存在循环依赖,请查看以下 handler:" + JsonUtil.toJsonString(dataNode.getAncestors()));
    }
    if (CollectionUtils.isNotEmpty(infraDependencyHandlerNames)) {
        // 存在子节点,子节点层级 +1
        for (String next : infraDependencyHandlerNames) {DataNode child = generateNode(next, level + 1, dataNode.getAncestors(), name);
            dataNode.putChild(child);
        }
    }
    return dataNode;
}

层级构造如下:

3 数据节点平铺(遍历出所有后辈节点)

Q1:如何解决接口依赖过程中的反复项?

A1:遍历所有的子节点,将所有子节点平铺到一层,平铺时如果节点曾经存在,比拟层级,保留层级大的即可(层级大阐明依赖位于更底层,调用时要优先调用)。

/**
 * 层级构造平铺
 *
 * @param dataNodes 数据节点
 * @param dataNodeMap 平铺构造
 */
private void flatteningNodes(List<DataNode> dataNodes, Map<String, DataNode> dataNodeMap) {if (CollectionUtils.isNotEmpty(dataNodes)) {for (DataNode dataNode : dataNodes) {DataNode dataNode1 = dataNodeMap.get(dataNode.getName());
            if (Objects.nonNull(dataNode1)) {
                // 存入层级大的即可,防止反复
                if (dataNode1.getLevel() < dataNode.getLevel()) {dataNodeMap.put(dataNode.getName(), dataNode);
                }
            } else {dataNodeMap.put(dataNode.getName(), dataNode);
            }
            // 解决子节点
            flatteningNodes(dataNode.getChildren(), dataNodeMap);
        }
    }
}

平铺构造如下:

4 分层(分组排序)

Q1:如何分层?

A1:节点平铺后曾经去重,此时借助 TreeMap 的天然排序个性将节点依照层级分组即可。

/**
 * @param dataNodeMap 平铺构造
 * @return 分层构造
 */
private TreeMap<Integer, Set<DataNode>> processLevel(Map<String, DataNode> dataNodeMap) {return dataNodeMap.values().stream().collect(Collectors.groupingBy(DataNode::getLevel, TreeMap::new, Collectors.toSet()))
}

分层如下:

1. 依据分层 TreeMap 的 key 倒序即为调用的层级程序

对应 key 中的数据节点 Set<DataNode> 需用多线程并发调用,以保障链路调用工夫

4、分层级调用

梳理出调用关系并分层后,应用并发编排工具调用即可。这里梳理的层级关系,level 越大,示意越优先调用。

这里以京东外部并发编排框架为例,阐明调用流程:

/**
 * 构建编排流程
 *
 * @param infraDependencyHandlers 依赖接口
 * @param workerExecutor 并发线程
 * @return 执行数据
 */
public Sirector<InfraContext> buildSirector(Set<String> infraDependencyHandlers, ThreadPoolExecutor workerExecutor) {Sirector<InfraContext> sirector = new Sirector<>(workerExecutor);
    long start = System.currentTimeMillis();
    // 依赖程序与执行程序相同
    TreeMap<Integer, Set<DataNode>> levelNodes;
    TreeMap<Integer, Set<DataNode>> cacheLevelNodes = localCacheManager.getValue("buildSirector");
    if (Objects.nonNull(cacheLevelNodes)) {levelNodes = cacheLevelNodes;} else {levelNodes = getLevelNodes(infraDependencyHandlers);
        ExecutorUtil.executeVoid(asyncTpExecutor, () -> localCacheManager.putValue("buildSirector", levelNodes));
    }
    log.info("buildSirector 梳理依赖关系耗时:{}", System.currentTimeMillis() - start);
    // 最底层接口执行
    Integer firstLevel = levelNodes.lastKey();
    EventHandler[] beginHandlers = levelNodes.get(firstLevel).stream().map(node -> abstractInfraHandlerMap.get(node.getName())).toArray(EventHandler[]::new);
    EventHandlerGroup group = sirector.begin(beginHandlers);

    Integer lastLevel = levelNodes.firstKey();
    for (int i = firstLevel - 1; i >= lastLevel; i--) {EventHandler[] thenHandlers = levelNodes.get(i).stream().map(node -> abstractInfraHandlerMap.get(node.getName())).toArray(EventHandler[]::new);
        group.then(thenHandlers);
    }
    return sirector;
}

5、集体思考

  1. 作为接入外部 RPC、Http 接口实现业务解决的我的项目,在应用过程中要关注调用链路上的资源复用,尤其长链路的调用,要深刻思考内存资源的利用以及对底层服务的压力。
  2. 要关注对外服务接口与底层数据接口的响应时差,剖析调用逻辑与流程是否正当,是否存在优化项。
  3. 多线程并发调用多个平行数据接口时,如何使得各个线程的耗时方差尽可能小?

作者:京东批发 王江波

起源:京东云开发者社区

退出移动版