天天看點

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

摘要:本文主要分析在cse架構下一個請求是怎麼被接受和處理的。

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

前置知識

cse的通信是基于vert.x來搞的,是以我們首先得了解下裡面的幾個概念:

  • Verticle:You can think of verticle instances as a bit like actors in the Actor Model. A typical verticle-based Vert.x application will be composed of many verticle instances in each Vert.x instance.參考:https://vertx.io/docs/apidocs/io/vertx/core/Verticle.html

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

  • 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 http://127.0.0.1:18088/agdms/v1/stock-apps/query?pkgName=test),便會觸發上面提到的回調,如下:

// 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之前會進行線程切換,還有同步調用和異步調用的差別以及服務啟動時候初始化的邏輯等等。這些内容也是比較有意思,值得深挖。

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

繼續閱讀