ElasticSearchRest/RPC介面解析

祝威廉發表於2016-09-09

一些基礎知識

早先ES的HTTP協議支援還是依賴Jetty的,現在不管是Rest還是RPC都是直接基於Netty了。
另外值得一提的是,ES 是使用Google的Guice 進行模組管理,所以瞭解Guice的基本使用方式有助於你瞭解ES的程式碼組織。
ES 的啟動類是 org.elasticsearch.bootstrap.Bootstrap。在這裡進行一些配置和環境初始化後會啟動org.elasticsearch.node.Node。Node 的概念還是蠻重要的,節點的意思,也就是一個ES例項。RPC 和 Http的對應的監聽啟動都由在該類完成。
Node 屬性裡有一個很重要的物件,叫client,型別是 NodeClient,我們知道ES是一個叢集,所以每個Node都需要和其他的Nodes 進行互動,這些互動則依賴於NodeClient來完成。所以這個物件會在大部分物件中傳遞,完成相關的互動。
先簡要說下:
  • NettyTransport 對應RPC 協議支援
  • NettyHttpServerTransport 則對應HTTP協議支援

Rest 模組解析

首先,NettyHttpServerTransport 會負責進行監聽Http請求。通過配置http.netty.http.blocking_server 你可以選擇是Nio還是傳統的阻塞式服務。預設是NIO。該類在配置pipeline的時候,最後新增了HttpRequestHandler,所以具體的接受到請求後的處理邏輯就由該類來完成了。
pipeline.addLast("handler", requestHandler);
HttpRequestHandler 實現了標準的 messageReceived(ChannelHandlerContext ctx, MessageEvent e) 方法,在該方法中,HttpRequestHandler 會回撥NettyHttpServerTransport.dispatchRequest方法,而該方法會呼叫HttpServerAdapter.dispatchRequest,接著又會呼叫HttpServer.internalDispatchRequest方法(額,好吧,我承認巢狀挺深有點深):
public void internalDispatchRequest(final HttpRequest request, final HttpChannel channel) {
        String rawPath = request.rawPath();
        if (rawPath.startsWith("/_plugin/")) {
            RestFilterChain filterChain = restController.filterChain(pluginSiteFilter);
            filterChain.continueProcessing(request, channel);
            return;
        } else if (rawPath.equals("/favicon.ico")) {
            handleFavicon(request, channel);
            return;
        }
        restController.dispatchRequest(request, channel);
    }
這個方法裡我們看到了plugin等被有限處理。最後請求又被轉發給      RestController。
RestController 大概類似一個微型的Controller層框架,實現了:
  1. 儲存了 Method + Path -> Controller 的關係
  2. 提供了註冊關係的方法
  3. 執行Controller的功能。
那麼各個Controller(Action) 是怎麼註冊到RestController中的呢?
在ES中,Rest*Action 命名的類的都是提供http服務的,他們會在RestActionModule 中被初始化,對應的構造方法會注入RestController例項,接著在構造方法中,這些Action會呼叫controller.registerHandler 將自己註冊到RestController。典型的樣子是這樣的:
@Inject
    public RestSearchAction(Settings settings, RestController controller, Client client) {
        super(settings, controller, client);
        controller.registerHandler(GET, "/_search", this);
        controller.registerHandler(POST, "/_search", this);
        controller.registerHandler(GET, "/{index}/_search", this);
每個Rest*Action 都會實現一個handleRequest方法。該方法接入實際的邏輯處理。
@Override
    public void handleRequest(final RestRequest request, final RestChannel channel, final Client client) {
        SearchRequest searchRequest;
        searchRequest = RestSearchAction.parseSearchRequest(request, parseFieldMatcher);
        client.search(searchRequest, new RestStatusToXContentListener<SearchResponse>(channel));
    }
首先是會把 請求封裝成一個SearchRequest物件,然後交給 NodeClient 執行。
如果用過ES的NodeClient Java API,你會發現,其實上面這些東西就是為了暴露NodeClient API 的功能,使得你可以通過HTTP的方式呼叫。

Transport*Action,兩層對映關係解析

我們先跑個題,在ES中,Transport*Action 是比較核心的類集合。這裡至少有兩組對映關係。
Action -> Transport*Action
TransportAction -> TransportHandler
第一層對映關係由類似下面的程式碼在ActionModule中完成:
 registerAction(PutMappingAction.INSTANCE,  TransportPutMappingAction.class);
第二層對映則在類似 SearchServiceTransportAction 中維護。目前看來,第二層對映只有在查詢相關的功能才有,如下:
transportService.registerRequestHandler(FREE_CONTEXT_SCROLL_ACTION_NAME, ScrollFreeContextRequest.class, ThreadPool.Names.SAME, new FreeContextTransportHandler<>());

SearchServiceTransportAction 可以看做是SearchService進一步封裝。其他的Transport*Action 則只呼叫對應的Service 來完成實際的操作。
對應的功能是,可以通過Action 找到對應的TransportAction,這些TransportAction 如果是query類,則會呼叫SearchServiceTransportAction,並且通過第二層對映找到對應的Handler,否則可能就直接通過對應的Service完成操作。
下面關於RPC呼叫解析這塊,我們會以查詢為例。

RPC 模組解析

前面我們提到,Rest介面最後會呼叫NodeClient來完成後續的請求。對應的程式碼為:
public <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder>> void doExecute(Action<Request, Response, RequestBuilder> action, Request request, ActionListener<Response> listener) {
        TransportAction<Request, Response> transportAction = actions.get(action);
        if (transportAction == null) {
            throw new IllegalStateException("failed to find action [" + action + "] to execute");
        }
        transportAction.execute(request, listener);
    }
這裡的action 就是我們提到的第一層對映,找到Transport*Action.如果是查詢,則會找到TransportSearchAction。呼叫對應的doExecute 方法,接著根據searchRequest.searchType找到要執行的實際程式碼。下面是預設的:
else if (searchRequest.searchType() == SearchType.QUERY_THEN_FETCH) {    queryThenFetchAction.execute(searchRequest, listener);}
我們看到Transport*Action 是可以巢狀的,這裡呼叫了
TransportSearchQueryThenFetchAction.doExecute 
@Overrideprotected void doExecute(SearchRequest searchRequest, ActionListener<SearchResponse> listener) {   
 new AsyncAction(searchRequest, listener).start();
}
在AsyncAction中完成三個步驟:
  1. query
  2. fetch
  3. merge
為了分析方便,我們只分析第一個步驟。
@Overrideprotected void sendExecuteFirstPhase(
DiscoveryNode node, 
ShardSearchTransportRequest request, 
ActionListener<QuerySearchResultProvider> listener) {   
     searchService.sendExecuteQuery(node, request, listener);
}
這是AsyncAction 中執行query的程式碼。我們知道ES是一個叢集,所以query 必然要發到多個節點去,如何知道某個索引對應的Shard 所在的節點呢?這個是在AsyncAction的父類中完成,該父類分析完後會回撥子類中的對應的方法來完成,譬如上面的sendExecuteFirstPhase 方法。
說這個是因為需要讓你知道,上面貼出來的程式碼只是針對一個節點的查詢結果,但其實最終多個節點都會通過相同的方式進行呼叫。所以才會有第三個環節 merge操作,合併多個節點返回的結果。
searchService.sendExecuteQuery(node, request, listener);
其實會呼叫transportService的sendRequest方法。大概值得分析的地方有兩個:
if (node.equals(localNode)) {
                sendLocalRequest(requestId, action, request);
            } else {
                transport.sendRequest(node, requestId, action, request, options);
            }
我們先分析,如果是本地的節點,則sendLocalRequest是怎麼執行的。如果你跑到senLocalRequest裡去看,很簡單,其實就是:
reg.getHandler().messageReceived(request, channel);
reg 其實就是前面我們提到的第二個對映,不過這個對映其實還包含了使用什麼執行緒池等資訊,我們在前面沒有說明。
這裡 reg.getHandler == SearchServiceTransportAction.SearchQueryTransportHandler,所以messageReceived 方法對應的邏輯是:
QuerySearchResultProvider result = searchService.executeQueryPhase(request);
channel.sendResponse(result);
這裡,我們終於看到searchService。 在searchService裡,就是整兒八景的Lucene相關查詢了。這個我們後面的系列文章會做詳細分析。
如果不是本地節點,則會由NettyTransport.sendRequest 發出遠端請求。假設當前請求的節點是A,被請求的節點是B,則B的入口為MessageChannelHandler.messageReceived。在NettyTransport中你可以看到最後新增的pipeline裡就有MessageChannelHandler。我們跑進去messageReceived 看看,你會發現基本就是一些協議解析,核心方法是handleRequest,接著就和本地差不多了,我提取了關鍵的幾行程式碼:
final RequestHandlerRegistry reg = transportServiceAdapter.getRequestHandler(action);
threadPool.executor(reg.getExecutor()).execute(new RequestHandler(reg, request, transportChannel));
這裡被RequestHandler包了一層,其實內部執行的就是本地的那個。RequestHandler 的run方法是這樣的:
protected void doRun() throws Exception {     reg.getHandler().messageReceived(request, transportChannel);
}
這個就和前面的sendLocalRequest裡的一模一樣了。

總結

到目前為止,我們知道整個ES的Rest/RPC 的起點是從哪裡開始的。RPC對應的endpoint 是MessageChannelHandler,在NettyTransport 被註冊。Rest 介面的七點則在NettyHttpServerTransport,經過層層代理,最終在RestController中被執行具體的Action。 Action 的所有執行都會被委託給NodeClient。 NodeClient的功能執行單元是各種Transport*Action。對於查詢類請求,還多了一層對映關係。


相關文章