1、背景
到店商詳?shù)^程中,需要提供的對外能力越來越多,如預約日歷、附近門店、為你推薦等。這其中不可避免會出現(xiàn)多個上層能力依賴同一個底層接口的場景。最初采用的方案是對外 API 入口進來后獲取對應的能力,并發(fā)調用多項能力,由能力層調用對應的數(shù)據(jù)鏈路,進行業(yè)務處理。
然而,隨著接入功能的增多,這種情況導致了底層數(shù)據(jù)服務的重復調用,如商品配置信息,在一次 API 調用過程中重復調了 3 次,當流量增大或能力項愈多時,對底層服務的壓力會成倍增加。
正值 618 大促,各方接口的調用都會大幅度增加。通過梳理接口依賴關系來減少重復調用,對本系統(tǒng)而言,降低了調用數(shù)據(jù)接口時的線程占用次數(shù),可以有效降級 CPU。對調用方來說,減少了調用次數(shù),可減少調用方的資源消耗,保障底層服務的穩(wěn)定性。
原始調用方式:
2、優(yōu)化
基于上述問題,采用底層接口依賴分層調用的方案。梳理接口依賴關系,逐層向上調用,注入數(shù)據(jù),如此將同一接口的調用抽取到某層,僅調用一次,即可在整條鏈路使用。
改進調用方式:
只要分層后即可在每層采用多線程并發(fā)的方式調用,因為同一層級中的接口無先后依賴關系。
3、如何分層?
接下來,如何梳理接口層級關系就至關重要。
接口梳理分層流程如下:
第一步:構建層級結構 首先獲取到能力層依賴項并遍歷,然后調用生成數(shù)據(jù)節(jié)點方法。方法流程如下:構建當前節(jié)點,檢測循環(huán)依賴(存在循環(huán)依賴會導致棧溢出),獲取并遍歷節(jié)點依賴項,遞歸生成子節(jié)點,存放子節(jié)點。
第二步:節(jié)點平鋪 定義 Map 維護平鋪結構,調用平鋪方法。方法流程如下:遍歷層級結構,判斷當前節(jié)點是否已存在 map 中,存在時與原節(jié)點比較將層級大的節(jié)點放入(去除重復項),不存在時直接放入即可。然后處理子節(jié)點,遞歸調用平鋪方法,處理所有節(jié)點。
第三步:分層(分組排序) 流處理平鋪結構,處理層級分組,存儲在 TreeMap 中維護自然排序。對應 key 中的數(shù)據(jù)節(jié)點 Set
1 首先,定義數(shù)據(jù)結構用于維護調用鏈路
Q1:為什么需要定義祖先節(jié)點?
A1:為了判斷接口是否存在循環(huán)依賴。如果接口存在循環(huán)依賴而不檢測將導致調用棧溢出,故而在調用過程中要避免并檢測循環(huán)依賴。在遍歷子節(jié)點過程中,如果發(fā)現(xiàn)當前節(jié)點的祖先已經(jīng)包含當前子節(jié)點,說明依賴關系出現(xiàn)了環(huán)路,即循環(huán)依賴,此時拋異常終止后續(xù)流程避免棧溢出。
public class DataNode { /** * 節(jié)點名稱 */ private String name; /** * 節(jié)點層級 */ private int level; /** * 祖先節(jié)點 */ private Listancestors; /** * 子節(jié)點 */ private List children; }
2 獲取能力層的接口依賴,并生成對應的數(shù)據(jù)節(jié)點
Q1:生成節(jié)點時如何維護層級?
A1:從能力層依賴開始,層級從 1 遞加。每獲取一次底層依賴,底層依賴所生成的節(jié)點層級即父節(jié)點層級 + 1。
/** * 構建層級結構 * * @param handlers 接口依賴 * @return 數(shù)據(jù)節(jié)點集 */ private ListbuildLevel(Set handlers) { List result = Lists.newArrayList(); for (String next : handlers) { DataNode dataNode = generateNode(next, 1, null, null); result.add(dataNode); } return result; } /** * 生成數(shù)據(jù)節(jié)點 * * @param name 節(jié)點名稱 * @param level 節(jié)點層級 * @param ancestors 祖先節(jié)點(除父輩) * @param parent 父節(jié)點 * @return DataNode 數(shù)據(jù)節(jié)點 */ private DataNode generateNode(String name, int level, List ancestors, String parent) { AbstractInfraHandler abstractInfraHandler = abstractInfraHandlerMap.get(name); Set infraDependencyHandlerNames = abstractInfraHandler.getInfraDependencyHandlerNames(); // 根節(jié)點 DataNode dataNode = new DataNode(name); dataNode.setLevel(level); dataNode.putAncestor(ancestors, parent); if (CollectionUtils.isNotEmpty(dataNode.getAncestors()) && dataNode.getAncestors().contains(name)) { throw new IllegalStateException("依賴關系中存在循環(huán)依賴,請檢查以下handler:" + JsonUtil.toJsonString(dataNode.getAncestors())); } if (CollectionUtils.isNotEmpty(infraDependencyHandlerNames)) { // 存在子節(jié)點,子節(jié)點層級+1 for (String next : infraDependencyHandlerNames) { DataNode child = generateNode(next, level + 1, dataNode.getAncestors(), name); dataNode.putChild(child); } } return dataNode; }
層級結構如下:
3 數(shù)據(jù)節(jié)點平鋪(遍歷出所有后代節(jié)點)
Q1:如何處理接口依賴過程中的重復項?
A1:遍歷所有的子節(jié)點,將所有子節(jié)點平鋪到一層,平鋪時如果節(jié)點已經(jīng)存在,比較層級,保留層級大的即可(層級大說明依賴位于更底層,調用時要優(yōu)先調用)。
/** * 層級結構平鋪 * * @param dataNodes 數(shù)據(jù)節(jié)點 * @param dataNodeMap 平鋪結構 */ private void flatteningNodes(ListdataNodes, Map 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); } // 處理子節(jié)點 flatteningNodes(dataNode.getChildren(), dataNodeMap); } } }
平鋪結構如下:
4 分層(分組排序)
Q1:如何分層?
A1:節(jié)點平鋪后已經(jīng)去重,此時借助 TreeMap 的自然排序特性將節(jié)點按照層級分組即可。
/** * @param dataNodeMap 平鋪結構 * @return 分層結構 */ private TreeMap分層如下:> processLevel(Map dataNodeMap) { return dataNodeMap.values().stream().collect(Collectors.groupingBy(DataNode::getLevel, TreeMap::new, Collectors.toSet())) }

1. 根據(jù)分層 TreeMap 的 key 倒序即為調用的層級順序 對應 key 中的數(shù)據(jù)節(jié)點 Set
4、分層級調用
梳理出調用關系并分層后,使用并發(fā)編排工具調用即可。這里梳理的層級關系,level 越大,表示越優(yōu)先調用。 這里以京東內(nèi)部并發(fā)編排框架為例,說明調用流程:
/** * 構建編排流程 * * @param infraDependencyHandlers 依賴接口 * @param workerExecutor 并發(fā)線程 * @return 執(zhí)行數(shù)據(jù) */ public SirectorbuildSirector(Set infraDependencyHandlers, ThreadPoolExecutor workerExecutor) { Sirector sirector = new Sirector<>(workerExecutor); long start = System.currentTimeMillis(); // 依賴順序與執(zhí)行順序相反 TreeMap > levelNodes; TreeMap > 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); // 最底層接口執(zhí)行 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、 個人思考
作為接入內(nèi)部 RPC、Http 接口實現(xiàn)業(yè)務處理的項目,在使用過程中要關注調用鏈路上的資源復用,尤其長鏈路的調用,要深入考慮內(nèi)存資源的利用以及對底層服務的壓力。
要關注對外服務接口與底層數(shù)據(jù)接口的響應時差,分析調用邏輯與流程是否合理,是否存在優(yōu)化項。
多線程并發(fā)調用多個平行數(shù)據(jù)接口時,如何使得各個線程的耗時方差盡可能小?
審核編輯:劉清
-
RPC
+關注
關注
0文章
111瀏覽量
11895 -
HTTP接口
+關注
關注
0文章
21瀏覽量
1973
原文標題:一種接口依賴關系分層方案
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
基于mcu的一種分層軟件架構的相關資料分享
一種基于負載均衡的分層副本定位方法
一種高效、低延時的會議密鑰管理方案
一種包含異常傳播的類間數(shù)據(jù)依賴分析方法
一種新的識別和保留模型特征的自適應分層算法

基于mcu的一種分層軟件架構

評論