1、背景
到店商詳疊代過程中,需要提供的對外能力越來越多,如預約月曆、附近門店、為你推薦等。這其中不可避免會出現多個上層能力依賴同一個底層接口的場景。最初采用的方案是對外 API 入口進來後擷取對應的能力,并發調用多項能力,由能力層調用對應的資料鍊路,進行業務處理。然而,随着接入功能的增多,這種情況導緻了底層資料服務的重複調用,如商品配置資訊,在一次 API 調用過程中重複調了 3 次,當流量增大或能力項愈多時,對底層服務的壓力會成倍增加。
正值 618 大促,各方接口的調用都會大幅度增加。通過梳理接口依賴關系來減少重複調用,對本系統而言,降低了調用資料接口時的線程占用次數,可以有效降級 CPU。對調用方來說,減少了調用次數,可減少調用方的資源消耗,保障底層服務的穩定性。
原始調用方式:
2、優化
基于上述問題,采用底層接口依賴分層調用的方案。梳理接口依賴關系,逐層向上調用,注入資料,如此将同一接口的調用抽取到某層,僅調用一次,即可在整條鍊路使用。
改進調用方式:
隻要分層後即可在每層采用多線程并發的方式調用,因為同一層級中的接口無先後依賴關系。
歡迎關注公衆号:SpringForAll社群(spring4all.com),專注分享關于Spring的一切!回複“加群”還可加入Spring技術交流群!
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、 個人思考
- 作為接入内部 RPC、Http 接口實作業務處理的項目,在使用過程中要關注調用鍊路上的資源複用,尤其長鍊路的調用,要深入考慮記憶體資源的利用以及對底層服務的壓力。
- 要關注對外服務接口與底層資料接口的響應時差,分析調用邏輯與流程是否合理,是否存在優化項。
- 多線程并發調用多個平行資料接口時,如何使得各個線程的耗時方差盡可能小?
原文連結:https://my.oschina.net/u/4090830/blog/10085060