[1]elasticsearch2.X源码分析——CAT请求Request流转的过程

字数 2300阅读 271

elasticsearch是什么不再赘述。本文希望通过对ES源代码进行一些简单的分析,目的在于打通整个结构,了解整个运行方式,以及如果我需要对源代码进行简单的调整和修改我需要从哪里入手,抛砖引玉。本文选择了elasticsearch2.3.5的版本来进行分析调试,适用于2.x的版本。

环境准备

这次分析调试过程会将elasticsearch的源码check到本地后通过IDEA来分析,具体步骤可以通过搜索引擎获得,我默认了你做好了我以下提及的准备。

  • IDEA 用来启动和调试es工程
  • JDK1.8 这个版本的es可以用1.7或1.8编译
  • elasticsearch2.3.5源代码
    完成环境准备后的效果如下图所示

分析思路

我们分析的目的并不是无意义的闲读,或者我认为没有明确的需求和产出的阅读只是在浪费我的时间。虽然我的时间也不值钱。
下面这张图是我在阅读之前的思考,虽然我并不是在阅读之际就直接将它画出来,但这个逻辑指导我做这一次的阅读。



这些逻辑是可以在还没有开始阅读工作之前就需要想到的,在长期对es做二次开发工作后,我们可以很简单的猜测出它会具有图中所提到的功能点,那么接下来的工作就是带着预先设定的猜测去寻找对应的实现。那么最容易做到也是最简单的事情就是,发出一次最简单的请求,然后去跟踪这个请求所流转的过程。es既然本身提供restful接口,从大层面来看,它也是一个web程序,这就方便了我们寻找切入口。

分析过程

首先在IDEA中把ES以DEBUG启动模式启动,本篇重点关注请求流转的过程,即使我知道有在启动准备中有很多工作。不过我还是希望先满足自己的好奇心,我希望知道我提交给es一个请求后它是怎么走过它的生命周期最终落地形成结果返回给我的。
ES的API可以大致分为以下几类

  • 文档API(Document APIs): 提供对文档的增删改查操作
  • 搜索API(Search APIs): 提供对文档进行某个字段的查询
  • 索引API(Indices APIs): 提供对索引进行操作
  • 查看API(cat APIs): 按照更直观的形式返回数据,更适用于控制台请求展示
  • 集群API(Cluster APIs): 对集群进行查看和操作的API
    我选择使用cat APIs来做本次DEBUG,因为我不需要其他额外的参数,而是可以简单的使用curl或者在浏览器输入url来发起我所需要的请求。
    我首先在浏览器输入了http://localhost:9200/_cat
    浏览器给我以下回显

=^.^=
/_cat/allocation
/_cat/shards
/_cat/shards/{index}
/_cat/master
/_cat/nodes
/_cat/indices
/_cat/indices/{index}
/_cat/segments
/_cat/segments/{index}
/_cat/count
/_cat/count/{index}
/_cat/recovery
/_cat/recovery/{index}
/_cat/health
/_cat/pending_tasks
/_cat/aliases
/_cat/aliases/{alias}
/_cat/thread_pool
/_cat/plugins
/_cat/fielddata
/_cat/fielddata/{fields}
/_cat/nodeattrs
/_cat/repositories
/_cat/snapshots/{repository}

以上你可以看到所有cat的API都罗列展示出来。那么我们的目的就是跟踪这一个请求,它在哪里被转发最后被执行,都是我们所关心的。
首先我们需要寻找到一个可以下断点的突破点。这一点比较需要经验,首先ES是一个优秀的开源程序,那么我有足够的理由相信,它会在命名规范上做的很出色,所以我的第一个想法肯定是通过观察工程目录结构来寻找我第一个BreakPoint的位置。


目录.png

其实仔细观察目录结构,通过rest包名可以猜到这里大概会放有和restful服务相关的逻辑,点开包目录很清楚的可以看到我们所熟悉的各类API的位置,其中也包括catAPI。再点开cat包目录,好的,我们已经找到相关的类了。

其实我觉得源代码阅读这种事情切入点都是靠猜出来的 ——鲁迅

可是还是很懵,到底是哪个呢?
仔细观察可以发现,cat包下的类,和cat命令的功能是一一对应的。
比如/_cat/indices其实很明显的可以看得出来是RestIndicesAction,点进去我们也可以看到在构造函数中注册的路径

    @Inject
    public RestIndicesAction(Settings settings, RestController controller, Client client, IndexNameExpressionResolver indexNameExpressionResolver) {
        super(settings, controller, client);
        this.indexNameExpressionResolver = indexNameExpressionResolver;
        controller.registerHandler(GET, "/_cat/indices", this);
        controller.registerHandler(GET, "/_cat/indices/{index}", this);
    }

同理你完全可以试试多点几个类,就可以惊喜的看到这个路径就这么直白的写死在代码中。同理,我很容易猜到,我刚才访问的API对应执行的逻辑应该是会放在RestCatAction中。如以下

public class RestCatAction extends BaseRestHandler {

    private static final String CAT = "=^.^=";
    private static final String CAT_NL = CAT + "\n";
    private final String HELP;

    @Inject
    public RestCatAction(Settings settings, RestController controller, Set<AbstractCatAction> catActions, Client client) {
        super(settings, controller, client);
        controller.registerHandler(GET, "/_cat", this);
        StringBuilder sb = new StringBuilder();
        sb.append(CAT_NL);
        for (AbstractCatAction catAction : catActions) {
            catAction.documentation(sb);
        }
        HELP = sb.toString();
    }

    @Override
    public void handleRequest(final RestRequest request, final RestChannel channel, final Client client) {
        channel.sendResponse(new BytesRestResponse(RestStatus.OK, HELP));
    }
}

你甚至还能看到作者卖萌的表情。

有一个小tips是@Inject注解,这是谷歌的一个轻量级IOC框架提供的注解,我不了解,平时也是使用spring,但是这里其实无需深究它,可以大概的理解成在执行这个构造函数的时候会将所需要的变量注入到参数当中。

那我们把断点下在了这个构造函数当中,然后我刷新了浏览器页面,也就是重新发送了一遍请求。

很遗憾断点没有截获这一次请求。

再思考了一下,既然是构造函数,那么是不是在程序启动的过程中就已经执行过了?很好,那么我REDEBUG了ES。这次没有辜负我的期望,断点准确的截住了代码。


也就是说这个功能API列表其实在程序启动的时候就已经加载完毕,存储在了HELP变量中。这其实可以理解,当程序启动的时候就已经可以知道所有API的路径,也可以看到构造函数中的循环是在循环请求所有API路径并拼接到HELP变量中。

但是还是没解决我们的问题,到底是在哪里执行请求的呢。

眼光瞄向了handleRequest方法。可以看到handleRequest上有@Override注解。说明这个方法是实现或者复写了父类或者接口的方法。到这里我已经脑补出了结构,这是一个类似处理器一样的逻辑,需要实现的功能类实现了接口中规定好的方法,然后将功能类注册到一个处理器管理的区域由中心决定什么时候来调用处理器处理请求。

我们可以看一下这个类的UML图。


uml.png

非常明显的逻辑,RestHandler规定了处理器必须要实现的方法,AbstractComponent抽取了关于日志处理的逻辑。在这一点上是值得我学习的,将所有日志处理抽出来而不是散落在程序当中。

这次我们断点下在handleRequest中然后再次发送请求。bingo


我终于捕获了我自己发出的请求。
那么将刚才所做的事情都套用到同包的其他类中就可以找到所有API的具体逻辑,这就在如果有修改API需求的时候可以发挥用处了。在这个最简单的API中实现逻辑就是简单的返回预先加载好的字符串,但无论在哪个API类中都会在handleRequest中实现它的逻辑。

小结

本次分析我抱着如果我需要修改执行逻辑的时候我应该去哪里改的目的来寻找,解决了一部分问题也发现了很多新问题,主要的难点有以下一些问题:

  • ES几乎没有借助多少外部依赖,它仅用netty就实现了一个内部的web框架来管理请求的转发,给一开始的寻找切入口带来了困难。
  • 我在寻找我自己的请求到底去哪的时候其实没有文章中描述的这么轻松。。
  • ES本身内部的注释也不多,有时候很难理解作者的意思

基本的需求满足后我现在希望关注在ES做为一个优秀的开源软件,它的结构是如何设计的,如何解耦以及如何让代码优雅。下一篇我希望继续来分析ES源码中的一些“概念”。

推荐阅读更多精彩内容