我是一個請求,我該何去何從

華為雲開發者社群發表於2021-06-22
摘要:本文主要分析在cse框架下一個請求是怎麼被接受和處理的。

本文分享自華為雲社群《我是一個請求,我該何去何從?》,原文作者:向昊。

前置知識

cse的通訊是基於vert.x來搞的,所以我們首先得了解下里面的幾個概念:

所以我們知道幹活的就是這個傢伙,它就是這個模式中的工具人

  • Route:可以看成是一個條件集合(可以指定url的匹配規則),它用這些條件來判斷一個http請求或失敗是否應該被路由到指定的Handler
  • Router:可以看成一個核心的控制器,管理著Route
  • VertxHttpDispatcher:是cse裡的類,可以看成是請求分發處理器,即一個請求過來了怎麼處理都是由它來管理的。

初始化

RestServerVerticle

經過一系列流程最終會呼叫這個方法:

io.vertx.core.impl.DeploymentManager#doDeploy():注意如果在這個地方打斷點,可能會進多次。因為上面也提到過我們的操作都是基於Verticle的,cse中有2種Verticle,一種是org.apache.servicecomb.foundation.vertx.client.ClientVerticle一種是org.apache.servicecomb.transport.rest.vertx.RestServerVerticle,這篇文章我們主要分析接受請求的流程,即著眼於RestServerVerticle,至於ClientVerticle的分析,先挖個坑,以後填上~

呼叫棧如下:

我是一個請求,我該何去何從

VertxHttpDispatcher

由上圖可知,會呼叫如下方法:

// org.apache.servicecomb.transport.rest.vertx.RestServerVerticle#start
    public void start(Promise<Void> startPromise) throws Exception {
        // ...
        Router mainRouter = Router.router(vertx);
        mountAccessLogHandler(mainRouter);
        mountCorsHandler(mainRouter);
        initDispatcher(mainRouter);
        // ...
    }

在這裡我們看到了上文提到的Router,繼續看initDispatcher(mainRouter)這個方法:

// org.apache.servicecomb.transport.rest.vertx.RestServerVerticle#initDispatcher
    private void initDispatcher(Router mainRouter) {
        List<VertxHttpDispatcher> dispatchers = SPIServiceUtils.getSortedService(VertxHttpDispatcher.class);
        for (VertxHttpDispatcher dispatcher : dispatchers) {
            if (dispatcher.enabled()) {
                dispatcher.init(mainRouter);
            }
        }
    }

首先通過SPI方式獲取所有VertxHttpDispatcher,然後迴圈呼叫其init方法,由於分析的不是邊緣服務,即這裡我們沒有自定義VertxHttpDispatcher。

Router

接著上文分析,會呼叫如下方法:

// org.apache.servicecomb.transport.rest.vertx.VertxRestDispatcher
    public void init(Router router) {
        // cookies handler are enabled by default start from 3.8.3
        String pattern = DynamicPropertyFactory.getInstance().getStringProperty(KEY_PATTERN, null).get();
        if(pattern == null) {
            router.route().handler(createBodyHandler());
            router.route().failureHandler(this::failureHandler).handler(this::onRequest);
        } else {
            router.routeWithRegex(pattern).handler(createBodyHandler());
            router.routeWithRegex(pattern).failureHandler(this::failureHandler).handler(this::onRequest);
        }
    }

由於一般不會主動去設定servicecomb.http.dispatcher.rest.pattern這個配置,即pattern為空,所以這個時候是沒有特定url的匹配規則,即會匹配所有的url

我們需要注意handler(this::onRequest)這段程式碼,這個程式碼就是接受到請求後的處理。

處理請求

經過上面的初始化後,我們們的準備工作已經準備就緒,這個時候突然來了一個請求

(GET ),便會觸發上面提到的回撥,如下:

// org.apache.servicecomb.transport.rest.vertx.VertxRestDispatcher#onRequest
    protected void onRequest(RoutingContext context) {
        if (transport == null) {
            transport = CseContext.getInstance().getTransportManager().findTransport(Const.RESTFUL);
        }
        HttpServletRequestEx requestEx = new VertxServerRequestToHttpServletRequest(context);
        HttpServletResponseEx responseEx = new VertxServerResponseToHttpServletResponse(context.response());
     
        VertxRestInvocation vertxRestInvocation = new VertxRestInvocation();
        context.put(RestConst.REST_PRODUCER_INVOCATION, vertxRestInvocation);
        vertxRestInvocation.invoke(transport, requestEx, responseEx, httpServerFilters);
    }

最主要的就是那個invoke方法:

// org.apache.servicecomb.common.rest.RestProducerInvocation#invoke
    public void invoke(Transport transport, HttpServletRequestEx requestEx, HttpServletResponseEx responseEx,
        List<HttpServerFilter> httpServerFilters) {
        this.transport = transport;
        this.requestEx = requestEx;
        this.responseEx = responseEx;
        this.httpServerFilters = httpServerFilters;
        requestEx.setAttribute(RestConst.REST_REQUEST, requestEx);
     
        try {
            findRestOperation();
        } catch (InvocationException e) {
            sendFailResponse(e);
            return;
        }
        scheduleInvocation();
    }

這裡看似簡單,其實後背隱藏著大量的邏輯,下面來簡單分析下findRestOperation()和scheduleInvocation()這2個方法。

findRestOperation

從名字我們也可以看出這個方法主要是尋找出對應的OperationId

// org.apache.servicecomb.common.rest.RestProducerInvocation#findRestOperation    
    protected void findRestOperation() {
          MicroserviceMeta selfMicroserviceMeta = SCBEngine.getInstance().getProducerMicroserviceMeta();
          findRestOperation(selfMicroserviceMeta);
      }
  • SCBEngine.getInstance().getProducerMicroserviceMeta():這個是獲取該服務的一些資訊,專案啟動時,會將本服務的基本資訊註冊到註冊中心上去。相關程式碼可以參考:org.apache.servicecomb.serviceregistry.RegistryUtils#init。

本服務資訊如下:

我是一個請求,我該何去何從

我們主要關注這個引數:intfSchemaMetaMgr,即我們在契約中定義的介面,或者是程式碼中的Controller下的方法。

  • findRestOperation(selfMicroserviceMeta):首先通過上面的microserviceMeta獲取該服務下所有對外暴露的url,然後根據請求的RequestURI和Method來獲取OperationLocator,進而對restOperationMeta進行賦值,其內容如下:

我是一個請求,我該何去何從

可以看到這個restOperationMeta裡面的內容十分豐富,和我們介面是完全對應的。

scheduleInvocation

現在我們知道了請求所對應的Operation相關資訊了,那麼接下來就要進行呼叫了。但是呼叫前還要進行一些前置動作,比如引數的校驗、流控等等。

現在選取關鍵程式碼進行分析:

  • createInvocation:這個就是建立一個Invocation,Invocation在cse中還是一個比較重要的概念。它分為服務端和消費端,它們之間的區別還是挺大的。建立服務端的Invocation時候它會載入服務端相關的Handler,同理消費端會載入消費端相關的Handler。這次我們建立的是服務端的Invocation,即它會載入org.apache.servicecomb.qps.ProviderQpsFlowControlHandler、org.apache.servicecomb.qps.ProviderQpsFlowControlHandler、org.apache.servicecomb.core.handler.impl.ProducerOperationHandler這3個Handler(當然這些都是可配置的,不過最後一個是預設載入的,具體可以參考這篇文章:淺析CSE中Handler
  • runOnExecutor:這個方法超級重要,我們們也詳細分析下,最終呼叫如下:
// org.apache.servicecomb.common.rest.AbstractRestInvocation#invoke
    public void invoke() {
        try {
            Response response = prepareInvoke();
            if (response != null) {
                sendResponseQuietly(response);
                return;
            }
     
            doInvoke();
        } catch (Throwable e) {
            LOGGER.error("unknown rest exception.", e);
            sendFailResponse(e);
        }
    }
    • prepareInvoke:這個方法主要是執行HttpServerFilter裡面的方法,具體可以參考:淺析CSE中的Filter執行時機。如果response不為空就直接返回了。像引數校驗就是這個org.apache.servicecomb.common.rest.filter.inner.ServerRestArgsFilter的功能,一般報400 bad request就可以進去跟跟程式碼了
    • doInvoke:類似責任鏈模式,會呼叫上面說的3個Handler,前面2個Handler我們們不詳細分析了,直接看最後一個Handler,即org.apache.servicecomb.core.handler.impl.ProducerOperationHandler
// org.apache.servicecomb.core.handler.impl.ProducerOperationHandler#handle
    public void handle(Invocation invocation, AsyncResponse asyncResp) throws Exception {
        SwaggerProducerOperation producerOperation =
            invocation.getOperationMeta().getExtData(Const.PRODUCER_OPERATION);
        if (producerOperation == null) {
            asyncResp.producerFail(
                ExceptionUtils.producerOperationNotExist(invocation.getSchemaId(),
                    invocation.getOperationName()));
            return;
        }
        producerOperation.invoke(invocation, asyncResp);
    }

producerOperation是在啟動流程中賦值的,具體程式碼可以參考:org.apache.servicecomb.core.definition.schema.ProducerSchemaFactory#getOrCreateProducerSchema,其內容如下:

我是一個請求,我該何去何從

可以看到,這其下內容對應的就是我們程式碼中介面對應的方法。

接著會呼叫org.apache.servicecomb.swagger.engine.SwaggerProducerOperation#invoke方法:

// org.apache.servicecomb.swagger.engine.SwaggerProducerOperation#invoke
    public void invoke(SwaggerInvocation invocation, AsyncResponse asyncResp) {
        if (CompletableFuture.class.equals(producerMethod.getReturnType())) {
            completableFutureInvoke(invocation, asyncResp);
            return;
        }
     
        syncInvoke(invocation, asyncResp);
    }

由於我們的同步呼叫,即直接看syncInvoke方法即可:

public void syncInvoke(SwaggerInvocation invocation, AsyncResponse asyncResp) {
        ContextUtils.setInvocationContext(invocation);
        Response response = doInvoke(invocation);
        ContextUtils.removeInvocationContext();
        asyncResp.handle(response);
    }

我們們一般上下文傳遞資訊就是這行程式碼"搞的鬼":ContextUtils.setInvocationContext(invocation),然後再看doInvoke方法:

public Response doInvoke(SwaggerInvocation invocation) {
        Response response = null;
        try {
            invocation.onBusinessMethodStart();
     
            Object[] args = argumentsMapper.toProducerArgs(invocation);
            for (ProducerInvokeExtension producerInvokeExtension : producerInvokeExtenstionList) {
                producerInvokeExtension.beforeMethodInvoke(invocation, this, args);
            }
     
            Object result = producerMethod.invoke(producerInstance, args);
            response = responseMapper.mapResponse(invocation.getStatus(), result);
     
            invocation.onBusinessMethodFinish();
            invocation.onBusinessFinish();
        } catch (Throwable e) {
            if (shouldPrintErrorLog(e)){
                LOGGER.error("unexpected error operation={}, message={}",
                    invocation.getInvocationQualifiedName(), e.getMessage());
            }
            invocation.onBusinessMethodFinish();
            invocation.onBusinessFinish();
            response = processException(invocation, e);
        }
        return response;
    }
    • producerInvokeExtenstionList:根據SPI載入ProducerInvokeExtension相關類,系統會自動載入org.apache.servicecomb.swagger.invocation.validator.ParameterValidator,顧名思義這個就是校驗請求引數的。如校驗@Notnull、@Max(50)這些標籤。
    • producerMethod.invoke(producerInstance, args):通過反射去呼叫到具體的方法上!

這樣整個流程差不多完結了,剩下的就是響應轉換和返回響應資訊。

總結

這樣我們大概瞭解到了我們的服務是怎麼接受和處理請求的,即請求進入我們服務後,首先會獲取服務資訊,然後根據請求的路徑和方法去匹配具體的介面,然後經過Handler和Filter的處理,再通過反射呼叫到我們的業務程式碼上,最後返回響應。

整體流程看似簡單但是背後隱藏了大量的邏輯,本文也是摘取相對重要的流程進行分析,還有很多地方沒有分析到的,比如在呼叫runOnExecutor之前會進行執行緒切換,還有同步呼叫和非同步呼叫的區別以及服務啟動時候初始化的邏輯等等。這些內容也是比較有意思,值得深挖。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章