博客 / 詳情

返回

一種接口依賴關係分層方案 | 京東雲技術團隊

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. 多線程併發調用多個平行數據接口時,如何使得各個線程的耗時方差儘可能小?

作者:京東零售 王江波

來源:京東雲開發者社區

user avatar zeebj 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.