[jvm-sandbox-repeater 學習筆記][原理說明篇] 2 回放流程

ELes發表於2019-10-24

回放流程相較於錄製流程要複雜很多,涉及了回放觸發、回放流量分發與跟蹤的實現。

由於有部分前面提及過的流程 ,本章不再贅述,所以閱讀該章節前,建議先看看入門使用篇和前序章節錄製流程

2.1 回放流程圖

回放流程,是從使用者通過請求repeater-console的回放介面開始的,然後repeater-console通過呼叫repeater提供的回放任務接收介面下發回放任務。repeater在執行回放任務的過程中,會通過根據錄製記錄的資訊,構造相同的請求,對被掛載的任務進行請求,並跟蹤回放請求的處理流程,以便記錄回放結果以及執行mock動作。

回放流程

# 2.2 回放過程步驟的原始碼解析

整個回放過程比較複雜,以下將分為幾個大流程來進行分析:

  • 回放請求的處理與回放任務的分發
  • 回放器的回放觸發與回放結果記錄
  • 回放過程中的事件處理

2.2.1 回放請求的處理與回放任務的分發

2.2.1.1 回放請求的處理

回放請求最初的處理是在RepeaterModule#repeat方法。

這個方法前面加了@Command註解,使之可以通過http介面的形式供外部呼叫。

這個方法中主要是接收外部的回放請求,校驗請求引數,請求引數校驗通過後會將引數拼接成一個回放事件RepeatEvent,釋出到repeater內部的EventBus中。

/**
* 回放http介面
*
* @param req 請求引數
* @param writer printWriter
*/

@Command("repeat")
public void repeat(final Map<String, String> req, final PrintWriter writer) {
try {
String data = req.get(Constants.DATA_TRANSPORT_IDENTIFY);
if (StringUtils.isEmpty(data)) {
writer.write("invalid request, cause parameter {" + Constants.DATA_TRANSPORT_IDENTIFY + "} is required");
return;
}
RepeatEvent event = new RepeatEvent();
Map<String, String> requestParams = new HashMap<String, String>(16);
for (Map.Entry<String, String> entry : req.entrySet()) {
requestParams.put(entry.getKey(), entry.getValue());
}
event.setRequestParams(requestParams);
EventBusInner.post(event);
writer.write("submit success");
} catch (Throwable e) {
writer.write(e.getMessage());
}
}

RepeatEvent釋出到EventBus後,將被RepeatSubscribeSupporter捕獲並在onSubscribe方法中進行處理。

在這個處理過程中,會將RepeatEvent的引數進行反序列化,獲取回放相關的錄製記錄的資訊,並通過這些資訊從repeater-console拉取對應的錄製記錄詳情(RecordModel)。

當需要回放的錄製記錄獲取成功後,則是用預設流量分發器DefaultFlowDispatcher進行分發。

當獲取錄製記錄失敗/反序列化失敗/分發回放任務返回報錯時,都會列印日誌,並結束回放任務處理。

  @Override
public void onSubscribe(RepeatEvent repeatEvent) {
Map<String, String> req = repeatEvent.getRequestParams();
try {
final String data = req.get(Constants.DATA_TRANSPORT_IDENTIFY);
if (StringUtils.isEmpty(data)) {
log.info("invalid request cause meta is null, params={}", req);
return;
}
log.info("subscribe success params={}", req);
final RepeatMeta meta = SerializerWrapper.hessianDeserialize(data, RepeatMeta.class);
RepeaterResult<RecordModel> pr = StandaloneSwitch.instance().getBroadcaster().pullRecord(meta);
if (pr.isSuccess()){
DefaultFlowDispatcher.instance().dispatch(meta, pr.getData());
} else {
log.error("subscribe replay event failed, cause ={}", pr.getMessage());
}
} catch (SerializeException e) {
log.error("serialize failed, req={}", req, e);
} catch (Exception e) {
log.error("[Error-0000]-uncaught exception occurred when register repeat event, req={}", req, e);
}
}
}

2.2.1.2 回放任務的分發

回放任務的分發由DefaultFlowDispatcher#dispatch方法實現。

在這裡主要是針對回放任務的資訊進行校驗,無法通過校驗的回放任務資訊會丟擲錯誤到上層。

通過校驗的回放任務,則會

  1. 根據回放資訊初始化回放上下文資訊,並存放到回放快取中。
  2. 根據回放任務的入口呼叫型別,獲取對應的Repeater進行回放處理。
@Override
public void dispatch(RepeatMeta meta, RecordModel recordModel) throws RepeatException {

if (recordModel == null || recordModel.getEntranceInvocation() == null || recordModel.getEntranceInvocation().getType() == null) {
throw new RepeatException("invalid request, record or root invocation is null");
}
JSONObject.toJSONString(recordModel.getEntranceInvocation().getType().name()));
Repeater repeater = RepeaterBridge.instance().select(recordModel.getEntranceInvocation().getType());
if (repeater == null) {
throw new RepeatException("no valid repeat found for invoke type:" + recordModel.getEntranceInvocation().getType().name());
}

RepeatContext context = new RepeatContext(meta, recordModel, TraceGenerator.generate());
// 放置到回放快取中
RepeatCache.putRepeatContext(context);
repeater.repeat(context);
}

2.2.2 回放器的請求觸發與回放結果記錄

根據不同的回放呼叫型別,回放任務會被不同的Repeater執行。

目前已實現功能的Repeater有兩種:JavaRepeater、HttpRepeater。這兩個Repeater都繼承了AbstractRepeater類。AbstractRepeater實現了回放器中回放執行和記錄的主要流程邏輯,而具體外掛則根據需要實現了executeRepeat觸發回放、getType呼叫型別、identity回放器標識的方法。

所以從AbstractRepeater可以瞭解到回放結果記錄、回放結果處理的實現,而從外掛的Repeater可以瞭解到請求觸發的機制。

2.2.2.1 回放結果記錄

回放任務執行、回放結果記錄的實現邏輯在AbstractRepeater#repeat中實現。但是呼叫的時候,實際上是通過各個外掛的回放器例項來呼叫的。

當開始執行回放任務時,會根據回放上下文以及當前repeater的應用資訊初始化回放結果記錄RepeaterModel的例項。

初始化回放結果記錄後,初始化回放執行緒跟蹤並觸發回放請求並獲取回放請求返回的結果。

獲取回放請求返回的結果後,停止執行緒跟蹤,將回放結果以及當前的一些狀態資訊儲存到回放結果記錄的例項中。

最後通過呼叫訊息投遞器的broadcastRepeat方法將回放結果記錄序列化後上傳到repeater-console儲存。

PS:訊息投遞器當前支援兩種模式,在standalone模式下,儲存到本地檔案;在非standalone模式下上傳到repeater-console。

@Override
public void repeat(RepeatContext context) {
Stopwatch stopwatch = Stopwatch.createStarted();
ApplicationModel am = ApplicationModel.instance();
RepeatModel record = new RepeatModel();
record.setRepeatId(context.getMeta().getRepeatId());
record.setTraceId(context.getTraceId());
record.setAppName(am.getAppName());
record.setEnvironment(am.getEnvironment());
record.setHost(am.getHost());
record.setStarTime(new Date());
try {
// 根據之前生成的traceId開啟追蹤
Tracer.start(context.getTraceId());
// before invoke advice
RepeatInterceptorFacade.instance().beforeInvoke(context.getRecordModel());
Object response = executeRepeat(context);
// after invoke advice
RepeatInterceptorFacade.instance().beforeReturn(context.getRecordModel(), response);
stopwatch.stop();
record.setEndTime(new Date());
record.setCost(stopwatch.elapsed(TimeUnit.MILLISECONDS));
record.setFinish(true);
record.setResponse(response);
record.setMockInvocations(RepeatCache.getMockInvocation(context.getTraceId()));
} catch (Exception e) {
log.error("repeat fail", e);
stopwatch.stop();
record.setCost(stopwatch.elapsed(TimeUnit.MILLISECONDS));
record.setResponse(e);
} finally {
Tracer.end();
}
sendRepeat(record);
}

2.2.2.2 回放請求的觸發

回放請求的觸發對應的方法是各個外掛的Repeater的executeRepeat方法。不同外掛或者說不同的呼叫型別的請求觸發的方式是不同的。但是設計思路都是一致的:從錄製記錄中獲取需要觸發的請求的路徑(url/方法名)和請求引數,拼接請求,然後通過各自的協議請求方式去觸發本機的服務或者說當前掛載repeater的應用

這裡以分別以httpRepeater和JavaRepeater為例進行分別說明。

下面是JavaRepeater的回放觸發。這個回放觸發主要做了幾個步驟:

  1. 從回放上下文中獲取到入口的呼叫記錄
  2. 從入口呼叫記錄的Indentity中獲取回放需要的方法名、類名、請求引數。
  3. 通過類名獲取到應用中的例項物件、通過類名、方法名、請求引數型別獲取對應的方法例項。
  4. 通過反射的方式呼叫對應的方法、傳入引數觸發回放。
@Override
protected Object executeRepeat(RepeatContext context) throws Exception {
Invocation invocation = context.getRecordModel().getEntranceInvocation();
Identity identity = invocation.getIdentity();
Object bean = SpringContextAdapter.getBeanByType(identity.getLocation());
if (bean == null) {
throw new RepeatException("no bean found in context, className=" + identity.getLocation());
}
if (invocation.getType().equals(this.getType())) {
throw new RepeatException("invoke type miss match, required invoke type is: " + invocation.getType());
}
String[] array = identity.getEndpoint().split("~");
// array[0]=/methodName
String methodName = array[0].substring(1);
ClassLoader classLoader = ClassloaderBridge.instance().decode(invocation.getSerializeToken());
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
// fix issue#9 int.class基本型別被解析成包裝型別,通過java方法簽名來規避這類問題
// array[1]=javaMethodDesc
MethodSignatureParser.MethodSpec methodSpec = MethodSignatureParser.parseIdentifier(array[1]);
Class<?>[] parameterTypes = MethodSignatureParser.loadClass(methodSpec.getParamIdentifiers(), classLoader);
Method method = bean.getClass().getDeclaredMethod(methodName, parameterTypes);
// 開始invoke
return method.invoke(bean, invocation.getRequest());
}

下面是httpRepeater的回放觸發。這個回放觸發主要做了幾個步驟:

  1. 從回放上下文中獲取到入口的呼叫記錄,並將這個Invocation轉義成HttpInvication型別。
  2. 從HttpInvication中獲取回放需要的urlPath、請求埠、header、以及請求引數、請求方法(當前只支援GET/POST)並組裝成一個請求。
  3. 發起這個http請求。
@Override
protected Object executeRepeat(RepeatContext context) throws Exception {
Invocation invocation = context.getRecordModel().getEntranceInvocation();
if (!(invocation instanceof HttpInvocation)) {
throw new RepeatException("type miss match, required HttpInvocation but found " + invocation.getClass().getSimpleName());
}
HttpInvocation hi = (HttpInvocation) invocation;
Map<String, String> extra = new HashMap<String, String>(2);
// 透傳當前生成的traceId到http執行緒 HttpStandaloneListener#initConetxt
extra.put(Constants.HEADER_TRACE_ID, context.getTraceId());
// 直接訪問本機,預設全都走http,不關心protocol
StringBuilder builder = new StringBuilder()
.append("http")
.append("://")
.append("127.0.0.1")
.append(":")
.append(hi.getPort())
.append(hi.getRequestURI());
String url = builder.toString();
Map<String, String> headers = rebuildHeaders(hi.getHeaders(), extra);
HttpUtil.Resp resp = HttpUtil.invoke(url, hi.getMethod(), headers, hi.getParamsMap(), hi.getBody());
return resp.isSuccess() ? resp.getBody() : resp.getMessage();
}

PS:從JavaRepeater和HttpRepeater的回放觸發的實現中,會發現獲取回放請求所需資訊時,雖然都是從回放上下文獲取的,但獲取的欄位卻不一樣。這個差異主要是外掛在錄製時,處理引數、identity拼接的方法不同、甚至是EventListener不同導致的。說明回放器的設計是需要包括錄製過程時對於請求相關資訊的處理方案的。

2.2.3 回放過程中的事件處理

回顧一下02 原理分析篇的1.2.4 Before事件處理1.2.5 Return事件處理/ Throw事件處理篇,就會發現在回放過程中,DefaultEventListner處理的方法似乎非常簡單粗暴:

  1. throw/return事件不做處理
  2. before事件直接交給外掛呼叫處理器執行mock,並且從註釋中也能瞭解到,只對子呼叫事件執行mock。
// 回放流量;如果是入口則放棄;子呼叫則進行mock
if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {
processor.doMock(event, entrance, invokeType);
return;
}

所以在回放過程的事件中,我們以AbstractInvocationProcessor#doMock方法為起點進行分析。

2.2.3.1 回放的mock流程

AbstractRepeater#repeat相似,執行回放的mock的流程實現邏輯基本在AbstractInvocationProcessor#doMock實現,但是實際呼叫時,是通過外掛中各自的Processor的例項進行呼叫的。

當開始執行回放mock時,會先獲取回放上下文的資訊。

根據回放上下文的資訊,判斷是否跳過mock的執行。

當需要執行mock時,會根據回放上下文中的資訊拼接出MockRequest

通過mock策略計算獲取MockResponse

根據MockResponse的狀態進行不同的操作:

  1. 當MockResponse獲取到的結果是return正常內容時,就會丟擲一個終止當前呼叫操作並返回當前的返回結果ProcessControlException,用來阻擋這個需要mock的方法的實際呼叫。
  2. 當MockResponse獲取到的結果是返回一個執行異常時,就會丟擲一個終止當前呼叫操作並丟擲異常的ProcessControlException,用來阻擋這個需要mock的方法的實際呼叫。
  3. 只有當MockResponse的狀態是skip時,則不對這個呼叫事件做任何處理,呼叫會實際發生,其他任何狀態都會通過丟擲ProcessControlException來阻擋這個事件的實際呼叫。

PS:ProcessControlException我目前只知道他能夠在回放過程中中止呼叫動作,但是在整個過程如何生效的,還沒看明白,這個得了解到jvm-sandbox對於ProcessControlException的處理邏輯。

@Override
public void doMock(BeforeEvent event, Boolean entrance, InvokeType type) throws ProcessControlException {
/*
* 獲取回放上下文
*/

RepeatContext context = RepeatCache.getRepeatContext(Tracer.getTraceId());
/*
* mock執行條件
*/

if (!skipMock(event, entrance, context) && context != null && context.getMeta().isMock()) {
try {
/*
* 構建mock請求
*/

final MockRequest request = MockRequest.builder()
.argumentArray(this.assembleRequest(event))
.event(event)
.identity(this.assembleIdentity(event))
.meta(context.getMeta())
.recordModel(context.getRecordModel())
.traceId(context.getTraceId())
.type(type)
.repeatId(context.getMeta().getRepeatId())
.index(SequenceGenerator.generate(context.getTraceId()))
.build();

LogUtil.debug("doMock.identity={}",JSONObject.toJSONString(request.getIdentity(), SerializerFeature.IgnoreNonFieldGetter));

/*
* 執行mock動作
*/

final MockResponse mr = StrategyProvider.instance().provide(context.getMeta().getStrategyType()).execute(request);
/*
* 處理策略推薦結果
*/

switch (mr.action) {
case SKIP_IMMEDIATELY:
break;
case THROWS_IMMEDIATELY:
ProcessControlException.throwThrowsImmediately(mr.throwable);
break;
case RETURN_IMMEDIATELY:
ProcessControlException.throwReturnImmediately(assembleMockResponse(event, mr.invocation));
break;
default:
ProcessControlException.throwThrowsImmediately(new RepeatException("invalid action"));
break;
}
} catch (ProcessControlException pce) {
throw pce;
} catch (Throwable throwable) {
ProcessControlException.throwThrowsImmediately(new RepeatException("unexpected code snippet here.", throwable));
}
}
}

AbstractInvocationProcessor#doMock方法中可以看到,執行mock動作的關鍵性程式碼是這一行

final MockResponse mr = StrategyProvider.instance().provide(context.getMeta().getStrategyType()).execute(request);

從這裡可以看到回放的時候會從上下文中的RepeatMeta中獲取策略型別並執行mock。

StrategyProvider.instance().provide(context.getMeta().getStrategyType())方法返回的是一個MockStrategy物件,即mock策略物件。而這個物件的execute方法點進去看,是跳轉到了AbstractMockStrategy#execute方法中。

與之前的repeater的實現套路類似,這個方法中實現的是執行mock的整體流程,但是實際上是由實現了MockStrategy介面的實現類例項進行呼叫的。

在執行mock的過程中,主要是初始化mock呼叫資訊儲存到回放快取,並通過匹配策略的select方法,從錄製記錄的子呼叫列表中,查詢到與當前呼叫入參和方法名一致的子呼叫記錄,並針對將查詢得出的子呼叫response或者throwable組裝到MockResponse中。

PS:在這個過程中,提供了在匹配子呼叫前干涉的機會,實現對應介面即可在匹配前修改呼叫的引數等資訊。

@Override
public MockResponse execute(final MockRequest request) {
MockResponse response;
try {
/*
* before select hook;
*/

MockInterceptorFacade.instance().beforeSelect(request);
/*
* do select
*/

SelectResult select = select(request);
Invocation invocation = select.getInvocation();
MockInvocation mi = new MockInvocation();
mi.setIndex(SequenceGenerator.generate(request.getTraceId() + "#"));
mi.setCurrentUri(request.getIdentity().getUri());
mi.setCurrentArgs(request.getArgumentArray());
mi.setTraceId(request.getTraceId());
mi.setCost(select.getCost());
mi.setRepeatId(request.getRepeatId());
// add mock invocation
RepeatCache.addMockInvocation(mi);
// matching success

if (select.isMatch() && invocation != null) {
response = MockResponse.builder()
.action(invocation.getThrowable() == null ? Action.RETURN_IMMEDIATELY : Action.THROWS_IMMEDIATELY)
.throwable(invocation.getThrowable())
.invocation(invocation)
.build();
mi.setSuccess(true);
mi.setOriginUri(invocation.getIdentity().getUri());
mi.setOriginArgs(invocation.getRequest());
} else {
response = MockResponse.builder()
.action(Action.THROWS_IMMEDIATELY)
.throwable(new RepeatException("no matching invocation found")).build();
}
/*
* before return hook;
*/

MockInterceptorFacade.instance().beforeReturn(request, response);
} catch (Throwable throwable) {
log.error("[Error-0000]-uncaught exception occurred when execute mock strategy, type={}", type(), throwable);
response = MockResponse.builder().
action(Action.THROWS_IMMEDIATELY)
.throwable(throwable)
.build();
}
return response;
}

2.2.3.2 回放的mock策略

在mock執行過程中,執行mock的流程在AbstractMockStrategy#execute中實現,而不同的mock匹配策略的匹配演算法差異,則在MockStrategy的實現類中,各自重寫select方法來實現。

當前的官方文件提供了兩個MockStrategy的實現類,DefaultMockStrategyParameterMatchMockStrategy。其中DefaultMockStrategy是預設返回不匹配結果的,所以我們一般流程使用的mock策略是ParameterMatchMockStrategy

簡單說說ParameterMatchMockStrategy#select方法的實現邏輯重點,實現細節可以看程式碼去更進一步理解。

  1. 從被回放的錄製記錄中獲取子呼叫列表subInvocations
  2. 根據Invocation中的identity欄位過濾得到跟本次mock的呼叫相同的子呼叫列表target。
  3. 當target列表為空時,返回一個狀態是未匹配且呼叫結果是空的選擇結果。
  4. 遍歷子呼叫列表targer,通過比對本次mock呼叫的入參和target列表中每一個子呼叫的入參的序列化的自串串,得到一個相似度的數值。遍歷過程中,當相似度合格(相似度計算結果大於或等於1)的時候,則停止迴圈,從錄製記錄中刪除該子呼叫,並以該子呼叫構造選擇結果返回,並且選擇結果的返回狀態是已匹配的。
  5. 當target列表遍歷完成後,仍然沒有相似度合格的子呼叫,則以相似度最大的子呼叫構造選擇結果返回,此時選擇結果返回的狀態是未匹配。
  6. 而在外部使用這個選擇結果的時候,需要判斷是否有

那我們是怎麼決定想要使用的MockStrategy型別的呢?

從前文可知,回放的mock策略是根據回放上下文中的RepeatMeta裡面來選擇的。在向上回溯會發現RepeatMeta是從repeater-console在推送回放任務的時候傳過來的。再一路跟隨程式碼呼叫搜尋,會發現在AbstractRecordService#repeat方法中,初始化RepeatMeta就寫死了PARAMETER_MATCH

protected RepeaterResult<String> repeat(Record record, String repeatId, String host, String port) {
String repeatUrl = String.format("http://%s:%s%s", host, port, repeatUri);

RepeatMeta meta = new RepeatMeta();
meta.setAppName(record.getAppName());
meta.setTraceId(record.getTraceId());
meta.setMock(true);
meta.setRepeatId(StringUtils.isEmpty(repeatId) ? TraceGenerator.generate() : repeatId);
meta.setStrategyType(MockStrategy.StrategyType.PARAMETER_MATCH);
Map<String, String> requestParams = new HashMap<String, String>(2);
try {
requestParams.put(Constants.DATA_TRANSPORT_IDENTIFY, SerializerWrapper.hessianSerialize(meta));
} catch (SerializeException e) {
return RepeaterResult.builder().success(false).message(e.getMessage()).build();
}
HttpUtil.Resp resp = HttpUtil.doPost(repeatUrl, requestParams);
if (resp.isSuccess()) {
return RepeaterResult.builder().success(true).message("operate success").data(meta.getRepeatId()).build();
}
return RepeaterResult.builder().success(false).message("operate failed").data(resp).build();
}

相關文章