Elasticsearch 5.x 源码分析(6)Request 和Response 在ES 中的传输和解析

本篇只关注一个细节,就是在一个端到端的场景,一个Request是如何构造并传输,从Client 到Node 的Action 接收端又如何被parse,接着传输到各个的 shards 去处理;反之一个Response 生成后如何一直传输回到 Node 端最后回到 Transport Client 或者 REST Client。本篇主要回答了这几天处理的一个问题:如果采用REST Client 来做 Query 查询的话,有没有一种办法可以把 JSON 的Response 解析成 SearchResponse 对象如 Transport Client 般地处理回复。
所以我们分两部分来讲:1. 看一下各种Request 和Response 的源码;2. 看一下如何去parse 一个 JSON 成一个SearchResponse 对象。


Transport Client 调用链

我们先从最简单的例子开始吧,由于我在项目开发中用 Template Search 比较多那么就看一个最简单的 Template Search 的例子

SearchResponse sr = new SearchTemplateRequestBuilder(client)
        .setScript("template_gender")                       
        .setScriptType(ScriptType.STORED)     
        .setScriptParams(template_params)                   
        .setRequest(new SearchRequest())                    
        .get()                                              
        .getResponse(); 
public class SearchTemplateRequestBuilder
        extends ActionRequestBuilder<SearchTemplateRequest, SearchTemplateResponse, SearchTemplateRequestBuilder> {

    SearchTemplateRequestBuilder(ElasticsearchClient client, SearchTemplateAction action) {
        super(client, action, new SearchTemplateRequest());
    }

    public SearchTemplateRequestBuilder(ElasticsearchClient client) {
        this(client, SearchTemplateAction.INSTANCE);
    }

    public SearchTemplateRequestBuilder setRequest(SearchRequest searchRequest) {
        request.setRequest(searchRequest);
        return this;
    }

顾名思义,SearchTemplateRequestBuilder就是负责生成一个SearchTemplateRequest,我们看看它的继承关系

SearchTemplateRequest 结构

ActionRequest 可以理解成一个通用的可以被Action 层处理的Request 对象,SearchTemplateRequest 就是在外面包了一些自身的属性,如profile,script 等,这里我们不关心。打开ActionRequest 里面就更简单了,并没有任何逻辑,弄了一个方法public abstract ActionRequestValidationException validate();给Action 层做一些字段校验而已。而ActionRequest 又继承自TransportRequest 乃至TransportMessage 那么不用看基本就能猜到ActionRequest 直接就用于节点间传输了,而协议就是走netty 的Transport 了。

还有一个地方就是,在SearchTemplateRequest的构造函数里指定了一个SearchTemplateAction.INSTANCE,这个变量指定了这个Request 请求期望的目的是这个 INSTANCE key的接受者。而这个接受者则在 MustachePlugin 定义了,这里有种感觉就是SearchTemplateAction 这个Action 层好像完全被架空了,仅用来路由请求而已,感觉怪怪的。

MustachePlugin

TransportSearchTemplateAction 只做了一件事,就是从Request里面抽取出script 变量,然后调用scriptService去load 和compile 这个script 并渲染出最终的这个SearchRequest对象。

static SearchRequest convert(SearchTemplateRequest searchTemplateRequest, SearchTemplateResponse response, ScriptService scriptService,
                                 NamedXContentRegistry xContentRegistry) throws IOException {
        Script script = new Script(searchTemplateRequest.getScriptType(), TEMPLATE_LANG, searchTemplateRequest.getScript(),
                searchTemplateRequest.getScriptParams() == null ? Collections.emptyMap() : searchTemplateRequest.getScriptParams());
        CompiledTemplate compiledScript = scriptService.compileTemplate(script, SEARCH);
        BytesReference source = compiledScript.run(script.getParams());
        response.setSource(source);

最后交给了TransportSearchAction 来把Request当做一个普通的Request来处理。

TransportSearchTemplateAction 处理

由于netty处理请求采用异步的方式,所以这里需要一个onResponse() 的回调;
SearchAction把请求发送给SearchService 来执行,SearchService更多的是去控制Lucene 的逻辑,所以从SearchActionSearchService 层之间需要一个转换,来把SearchRequest 转换成Elasticsearch 内部的一种特殊的Request 对象ShardsSearchRequest,这个过程在TransportSearchActiondoExecute方法里做,在这里创建了一个SearchQueryThenFetchAsyncAction来去控制整个发送和接收的控制,包括收到每个shards的Response 后如何做 reducesort等等,在SearchQueryThenFetchAsyncAction你会看到一句getSearchTransport().sendExecuteQuery(getConnection(shardIt.getClusterAlias(), shard.currentNodeId()), buildShardSearchRequest(shardIt), getTask(), listener);
至此验证了最开始的想法,就是从TransportClient 创建的Request,将会一走到处理这类请求的Action层,最终会转换成ShardSearchRequest发送给 Service 层执行 Lucene调用并返回。SearchService相应的Lucene 操作,这里就不再进去看了,有兴趣的可以直接去看 SearchServicepublic SearchPhaseResult executeQueryPhase(ShardSearchRequest request, SearchTask task)方法;和QueryPhasepublic void execute(SearchContext searchContext) throws QueryPhaseExecutionException 方法。

那么SearchResponse 的话就直接反过来看就好了;从SearchAction 开始。。。。


REST Client 调用链

Transport Client 直接创建的是 SearchRequest 对象,并且直接发送到 TransportXXXAction 中去处理,而REST 则创建的是 JSON 格式的 Request,还是拿上面的 SearchTemplate 查询的例子来看,如果用REST Client构造一个SearchTemplate 的request 发送的话,那么接收的endpoint 就是RestSearchTemplateAction,在这个类中我们看到这样一段。

@Override
    public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
        // Creates the search request with all required params
        SearchRequest searchRequest = new SearchRequest();
        RestSearchAction.parseSearchRequest(searchRequest, request, null);

        // Creates the search template request
        SearchTemplateRequest searchTemplateRequest;
        try (XContentParser parser = request.contentOrSourceParamParser()) {
            searchTemplateRequest = PARSER.parse(parser, new SearchTemplateRequest(), null);
        }
        searchTemplateRequest.setRequest(searchRequest);

        return channel -> client.execute(SearchTemplateAction.INSTANCE, searchTemplateRequest, new RestStatusToXContentListener<>(channel));
    }
private static final ObjectParser<SearchTemplateRequest, Void> PARSER;
    static {
        PARSER = new ObjectParser<>("search_template");
        PARSER.declareField((parser, request, s) ->
                        request.setScriptParams(parser.map())
                , new ParseField("params"), ObjectParser.ValueType.OBJECT);
        PARSER.declareString((request, s) -> {
            request.setScriptType(ScriptType.FILE);
            request.setScript(s);
        }, new ParseField("file"));
        PARSER.declareString((request, s) -> {
            request.setScriptType(ScriptType.STORED);
            request.setScript(s);
        }, new ParseField("id"));
        PARSER.declareBoolean(SearchTemplateRequest::setExplain, new ParseField("explain"));
        PARSER.declareBoolean(SearchTemplateRequest::setProfile, new ParseField("profile"));
        PARSER.declareField((parser, request, value) -> {
            request.setScriptType(ScriptType.INLINE);
            if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
                //convert the template to json which is the only supported XContentType (see CustomMustacheFactory#createEncoder)
                try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
                    request.setScript(builder.copyCurrentStructure(parser).string());
                } catch (IOException e) {
                    throw new ParsingException(parser.getTokenLocation(), "Could not parse inline template", e);
                }
            } else {
                request.setScript(parser.text());
            }
        }, new ParseField("inline", "template"), ObjectParser.ValueType.OBJECT_OR_STRING);
    }

很容易发现,在RestAction接收到一个请求时,会通过RestSearchAction.parseSearchRequest(searchRequest, request, null); 来把一个RestRequest转换成一个SearchRequest ,然后重新扔回去channel 中去,就好像一个TransportClient 一样,在最后的调用里会同传一个toXContentListener 里面有一个方法是SearchResponse -> XContent 的转换,也就是toJson 同样的意义。

最后从Response 到RestResponse

从 JSON Response 反解析成SearchResponse

很多时候,我们又想用REST Client的便利,但是我们又不想拿到一个 String 的Response 然后弄成一个JSON Object慢慢的去解析,我们希望还是直接处理一个SearchResponse 来的爽快。既然有了toXContent()方法,那么有没有 fromXContent() 呢,答案是Elasticsearch 5.5 之前是没有的(或者仅限于SearchHits有)。。。
如果你去看ES 的5.5 branch或者master branch的 log,你会发现其中的一个重点commit有:

  • 在5.3 时诸如SearchResponse,SearchHit,Aggregation,Suggest 等等类都是接口,内部有InternalXXXX这样的实现类,而5.5 则全部都变成了 final class
  • 5.4,5.5 陆陆续续把SearchResponse、Aggregations等都加上了fromXContent方法,所以如果你直接在5.5 的代码下,你直接一句话就可以完成我想要解决的这个问题
  • Aggregations 这个解析是比较特别的,和普通的结果集不同,aggs 的请求类型和结果类型都非常丰富,举个例子,一个termsAggregation 和一个topHitsAggregation 的结果就完全不一样,所以Aggregations 的fromXContent()所谓是最最最复杂的,从5.5的代码中就可以看到,ES为Aggregations 实现了一堆的parser
每种Aggregation 的parser 类

那么我要做的,目前就是把5.5的这部分代码暂时迁移过来5.3先用着了。。。


后话

有朋友支招说其实github上也有类似的项目了,如jest

Jest is a Java HTTP Rest client for ElasticSearch

可是它始终不是采用了原生的SearchResponse 对象,因此使用起来还是不是很带劲。

而Elasticsearch 自己为啥要这么大费周章话那么多人力物力重写那堆的SearchResponse、SearchHit、Aggregation 呢?答案就是ES也想出一个这样的REST Client,叫做 High Level Rest Client,目的就是直接在REST Client上操作SearchResponse 对象

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/java-rest-high.html#java-rest-high

scope

https://github.com/elastic/elasticsearch/issues/23331

有兴趣的话可以上去GitHub 查看这个issue,期望的release 是ES5.6 和ES6.0

全篇完,有问题欢迎讨论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容