1、背景
到店商詳?shù)^程中,需要提供的對(duì)外能力越來越多,如預(yù)約日歷、附近門店、為你推薦等。這其中不可避免會(huì)出現(xiàn)多個(gè)上層能力依賴同一個(gè)底層接口的場(chǎng)景。最初采用的方案是對(duì)外 API 入口進(jìn)來后獲取對(duì)應(yīng)的能力,并發(fā)調(diào)用多項(xiàng)能力,由能力層調(diào)用對(duì)應(yīng)的數(shù)據(jù)鏈路,進(jìn)行業(yè)務(wù)處理。
然而,隨著接入功能的增多,這種情況導(dǎo)致了底層數(shù)據(jù)服務(wù)的重復(fù)調(diào)用,如商品配置信息,在一次 API 調(diào)用過程中重復(fù)調(diào)了 3 次,當(dāng)流量增大或能力項(xiàng)愈多時(shí),對(duì)底層服務(wù)的壓力會(huì)成倍增加。
正值 618 大促,各方接口的調(diào)用都會(huì)大幅度增加。通過梳理接口依賴關(guān)系來減少重復(fù)調(diào)用,對(duì)本系統(tǒng)而言,降低了調(diào)用數(shù)據(jù)接口時(shí)的線程占用次數(shù),可以有效降級(jí) CPU。對(duì)調(diào)用方來說,減少了調(diào)用次數(shù),可減少調(diào)用方的資源消耗,保障底層服務(wù)的穩(wěn)定性。
原始調(diào)用方式:
2、優(yōu)化
基于上述問題,采用底層接口依賴分層調(diào)用的方案。梳理接口依賴關(guān)系,逐層向上調(diào)用,注入數(shù)據(jù),如此將同一接口的調(diào)用抽取到某層,僅調(diào)用一次,即可在整條鏈路使用。
改進(jìn)調(diào)用方式:
只要分層后即可在每層采用多線程并發(fā)的方式調(diào)用,因?yàn)橥粚蛹?jí)中的接口無先后依賴關(guān)系。
3、如何分層?
接下來,如何梳理接口層級(jí)關(guān)系就至關(guān)重要。
接口梳理分層流程如下:
第一步:構(gòu)建層級(jí)結(jié)構(gòu) 首先獲取到能力層依賴項(xiàng)并遍歷,然后調(diào)用生成數(shù)據(jù)節(jié)點(diǎn)方法。方法流程如下:構(gòu)建當(dāng)前節(jié)點(diǎn),檢測(cè)循環(huán)依賴(存在循環(huán)依賴會(huì)導(dǎo)致棧溢出),獲取并遍歷節(jié)點(diǎn)依賴項(xiàng),遞歸生成子節(jié)點(diǎn),存放子節(jié)點(diǎn)。
第二步:節(jié)點(diǎn)平鋪 定義 Map 維護(hù)平鋪結(jié)構(gòu),調(diào)用平鋪方法。方法流程如下:遍歷層級(jí)結(jié)構(gòu),判斷當(dāng)前節(jié)點(diǎn)是否已存在 map 中,存在時(shí)與原節(jié)點(diǎn)比較將層級(jí)大的節(jié)點(diǎn)放入(去除重復(fù)項(xiàng)),不存在時(shí)直接放入即可。然后處理子節(jié)點(diǎn),遞歸調(diào)用平鋪方法,處理所有節(jié)點(diǎn)。
第三步:分層(分組排序) 流處理平鋪結(jié)構(gòu),處理層級(jí)分組,存儲(chǔ)在 TreeMap 中維護(hù)自然排序。對(duì)應(yīng) key 中的數(shù)據(jù)節(jié)點(diǎn) Set
1 首先,定義數(shù)據(jù)結(jié)構(gòu)用于維護(hù)調(diào)用鏈路
Q1:為什么需要定義祖先節(jié)點(diǎn)?
A1:為了判斷接口是否存在循環(huán)依賴。如果接口存在循環(huán)依賴而不檢測(cè)將導(dǎo)致調(diào)用棧溢出,故而在調(diào)用過程中要避免并檢測(cè)循環(huán)依賴。在遍歷子節(jié)點(diǎn)過程中,如果發(fā)現(xiàn)當(dāng)前節(jié)點(diǎn)的祖先已經(jīng)包含當(dāng)前子節(jié)點(diǎn),說明依賴關(guān)系出現(xiàn)了環(huán)路,即循環(huán)依賴,此時(shí)拋異常終止后續(xù)流程避免棧溢出。
public class DataNode { /** * 節(jié)點(diǎn)名稱 */ private String name; /** * 節(jié)點(diǎn)層級(jí) */ private int level; /** * 祖先節(jié)點(diǎn) */ private Listancestors; /** * 子節(jié)點(diǎn) */ private List children; }
2 獲取能力層的接口依賴,并生成對(duì)應(yīng)的數(shù)據(jù)節(jié)點(diǎn)
Q1:生成節(jié)點(diǎn)時(shí)如何維護(hù)層級(jí)?
A1:從能力層依賴開始,層級(jí)從 1 遞加。每獲取一次底層依賴,底層依賴所生成的節(jié)點(diǎn)層級(jí)即父節(jié)點(diǎn)層級(jí) + 1。
/** * 構(gòu)建層級(jí)結(jié)構(gòu) * * @param handlers 接口依賴 * @return 數(shù)據(jù)節(jié)點(diǎn)集 */ 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é)點(diǎn) * * @param name 節(jié)點(diǎn)名稱 * @param level 節(jié)點(diǎn)層級(jí) * @param ancestors 祖先節(jié)點(diǎn)(除父輩) * @param parent 父節(jié)點(diǎn) * @return DataNode 數(shù)據(jù)節(jié)點(diǎn) */ private DataNode generateNode(String name, int level, List ancestors, String parent) { AbstractInfraHandler abstractInfraHandler = abstractInfraHandlerMap.get(name); Set infraDependencyHandlerNames = abstractInfraHandler.getInfraDependencyHandlerNames(); // 根節(jié)點(diǎn) DataNode dataNode = new DataNode(name); dataNode.setLevel(level); dataNode.putAncestor(ancestors, parent); if (CollectionUtils.isNotEmpty(dataNode.getAncestors()) && dataNode.getAncestors().contains(name)) { throw new IllegalStateException("依賴關(guān)系中存在循環(huán)依賴,請(qǐng)檢查以下handler:" + JsonUtil.toJsonString(dataNode.getAncestors())); } if (CollectionUtils.isNotEmpty(infraDependencyHandlerNames)) { // 存在子節(jié)點(diǎn),子節(jié)點(diǎn)層級(jí)+1 for (String next : infraDependencyHandlerNames) { DataNode child = generateNode(next, level + 1, dataNode.getAncestors(), name); dataNode.putChild(child); } } return dataNode; }
層級(jí)結(jié)構(gòu)如下:
3 數(shù)據(jù)節(jié)點(diǎn)平鋪(遍歷出所有后代節(jié)點(diǎn))
Q1:如何處理接口依賴過程中的重復(fù)項(xiàng)?
A1:遍歷所有的子節(jié)點(diǎn),將所有子節(jié)點(diǎn)平鋪到一層,平鋪時(shí)如果節(jié)點(diǎn)已經(jīng)存在,比較層級(jí),保留層級(jí)大的即可(層級(jí)大說明依賴位于更底層,調(diào)用時(shí)要優(yōu)先調(diào)用)。
/** * 層級(jí)結(jié)構(gòu)平鋪 * * @param dataNodes 數(shù)據(jù)節(jié)點(diǎn) * @param dataNodeMap 平鋪結(jié)構(gòu) */ private void flatteningNodes(ListdataNodes, Map dataNodeMap) { if (CollectionUtils.isNotEmpty(dataNodes)) { for (DataNode dataNode : dataNodes) { DataNode dataNode1 = dataNodeMap.get(dataNode.getName()); if (Objects.nonNull(dataNode1)) { // 存入層級(jí)大的即可,避免重復(fù) if (dataNode1.getLevel() < dataNode.getLevel()) { dataNodeMap.put(dataNode.getName(), dataNode); } } else { dataNodeMap.put(dataNode.getName(), dataNode); } // 處理子節(jié)點(diǎn) flatteningNodes(dataNode.getChildren(), dataNodeMap); } } }
平鋪結(jié)構(gòu)如下:
4 分層(分組排序)
Q1:如何分層?
A1:節(jié)點(diǎn)平鋪后已經(jīng)去重,此時(shí)借助 TreeMap 的自然排序特性將節(jié)點(diǎn)按照層級(jí)分組即可。
/** * @param dataNodeMap 平鋪結(jié)構(gòu) * @return 分層結(jié)構(gòu) */ private TreeMap分層如下:> processLevel(Map dataNodeMap) { return dataNodeMap.values().stream().collect(Collectors.groupingBy(DataNode::getLevel, TreeMap::new, Collectors.toSet())) }
1. 根據(jù)分層 TreeMap 的 key 倒序即為調(diào)用的層級(jí)順序 對(duì)應(yīng) key 中的數(shù)據(jù)節(jié)點(diǎn) Set
4、分層級(jí)調(diào)用
梳理出調(diào)用關(guān)系并分層后,使用并發(fā)編排工具調(diào)用即可。這里梳理的層級(jí)關(guān)系,level 越大,表示越優(yōu)先調(diào)用。 這里以京東內(nèi)部并發(fā)編排框架為例,說明調(diào)用流程:
/** * 構(gòu)建編排流程 * * @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 梳理依賴關(guān)系耗時(shí):{}", 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、 個(gè)人思考
作為接入內(nèi)部 RPC、Http 接口實(shí)現(xiàn)業(yè)務(wù)處理的項(xiàng)目,在使用過程中要關(guān)注調(diào)用鏈路上的資源復(fù)用,尤其長(zhǎng)鏈路的調(diào)用,要深入考慮內(nèi)存資源的利用以及對(duì)底層服務(wù)的壓力。
要關(guān)注對(duì)外服務(wù)接口與底層數(shù)據(jù)接口的響應(yīng)時(shí)差,分析調(diào)用邏輯與流程是否合理,是否存在優(yōu)化項(xiàng)。
多線程并發(fā)調(diào)用多個(gè)平行數(shù)據(jù)接口時(shí),如何使得各個(gè)線程的耗時(shí)方差盡可能小?
審核編輯:劉清
-
RPC
+關(guān)注
關(guān)注
0文章
111瀏覽量
11540 -
HTTP接口
+關(guān)注
關(guān)注
0文章
21瀏覽量
1814
原文標(biāo)題:一種接口依賴關(guān)系分層方案
文章出處:【微信號(hào):OSC開源社區(qū),微信公眾號(hào):OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論