├── ElasticSearch-Aggregation-Bucket-实例分析.md ├── ElasticSearch-Aggregations--分析.md ├── ElasticSearch-Rest-RPC-接口解析.md ├── SparkES-多维分析引擎设计.md └── 如何提高ElasticSearch-索引速度.md /ElasticSearch-Aggregation-Bucket-实例分析.md: -------------------------------------------------------------------------------- 1 | > 在前文 [ElasticSearch Aggregations 分析](http://www.jianshu.com/p/56ad2b7e27b7) 中,我们提及了 【Aggregation Bucket的实现】,然而只是用文字简要描述了原理。今天这篇文章会以简单的类似grouyBy 的操作,让大家Aggregator的工作原理有进一步的理解 2 | 3 | 4 | ## 查询语句 5 | 6 | 今天我们假定的查询如下: 7 | 8 | ``` 9 | { 10 | "aggs":{ 11 | "user": { 12 | "terms": { 13 | "field": "user", 14 | "size": 10, 15 | "order": { 16 | "_count": "desc" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | 其语义类似这个sql 语句: select count(*) as user_count group by user order by user_count desc。 也就是按user 字段进行group by,接着进行降序排列。 25 | 26 | ## 调用链关系 27 | 28 | 首先 29 | 30 | ``` 31 | org.elasticsearch.search.aggregations.bucket.terms.TermsParser 32 | 33 | ``` 34 | 35 | 会解析上面的JSON 查询串,然后构建出 36 | 37 | ``` 38 | org.elasticsearch.search.aggregations.bucket.terms.TermsAggregatorFactory 39 | ``` 40 | 41 | 构建该Factory,大体需要下面一些信息: 42 | 43 | 1. 聚合名称 44 | 2. 需要统计的字段(ValueSourceConfig对象) 45 | 3. 排序(Order对象) 46 | 4. 其他一些terms聚合特有的配置 47 | 48 | 之后会构建出 49 | 50 | ``` 51 | org.elasticsearch.search.aggregations.bucket.terms.GlobalOrdinalsStringTermsAggregator 52 | ``` 53 | 54 | 对象。这个是实现聚合的核心对象,当然就这个类而言仅仅是针对当前的例子适用的。 该类有两个核心方法: 55 | 56 | 1. getLeafCollector 57 | 2. buildAggregation 58 | 59 | getLeafCollector 是获取Collector,还记得前文提及Collector 其实是一个迭代器,迭代所有文档,在这个步骤中,我们会获取一个Collector,然后依托于DocValues进行计数。 60 | 61 | 第二个方法,buildAggrgation方法则会将收集好的结果进行排序,获取topN。 62 | 63 | 64 | ## getLeafCollector 65 | 66 | 整个方法的代码我贴在了下面。在解析源码的过程中,我们会顺带解释ES对DocValues的封装。 67 | 68 | ``` 69 | @Override 70 | public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, 71 | final LeafBucketCollector sub) throws IOException { 72 | 73 | //valuesSource类型为:ValuesSource.Bytes.WithOrdinals.FieldData 74 | globalOrds = valuesSource.globalOrdinalsValues(ctx); 75 | 76 | // 下面的两个if 做过滤,不是我们核心的点 77 | if (acceptedGlobalOrdinals == null && includeExclude != null) { 78 | acceptedGlobalOrdinals = includeExclude.acceptedGlobalOrdinals(globalOrds, valuesSource); 79 | } 80 | 81 | if (acceptedGlobalOrdinals != null) { 82 | globalOrds = new FilteredOrdinals(globalOrds, acceptedGlobalOrdinals); 83 | } 84 | // 返回新的collector 85 | return newCollector(globalOrds, sub); 86 | } 87 | ``` 88 | 89 | 我在前面的源码里特别注释了下valuesSource 的类型。前文我们提到,大部分Aggregator 都是依赖于FieldData/DocValues 来实现的,而ValueSource 则是他们在ES里的表示。所以了解他们是很有必要的。ValuesSource 全类名是: 90 | 91 | org.elasticsearch.search.aggregations.support.ValuesSource 92 | 93 | 该类就是ES 为了管理 DocValues 而封装的。它是一个抽象类,内部还有很多实现类,Bytes,WithOrdinals,FieldData,Numeric,LongValues 等等。这些都是对特定类型DocValues类型的 ES 表示。 94 | 95 | 按上面我们的查询示例来看,`user` 字段对应的是 96 | 97 | org.elasticsearch.search.aggregations.support.ValuesSource.Bytes.WithOrdinals.FieldData 98 | 99 | 对象。这个对象是ES对Lucene String 类型的DocValues的一个表示。 100 | 你会发现在ValueSource类里,有不同的FieldData。不同的FieldData 可能继承自不同基类从而表示不同类型的数据。在现在这个FieldData 里面有一个对象: 101 | 102 | ``` 103 | protected final IndexOrdinalsFieldData indexFieldData; 104 | ``` 105 | 106 | 该对象在user(我们示例中的字段)是String类型的时候,对应的是实现类是 107 | 108 | ``` 109 | org.elasticsearch.index.fielddata.plain.SortedSetDVOrdinalsIndexFieldData 110 | ``` 111 | 112 | 该对象的大体作用是,构建出DocValue的ES的Wraper。 113 | 114 | 具体代码如下: 115 | 116 | ``` 117 | @Overridepublic AtomicOrdinalsFieldData load(LeafReaderContext context) { 118 | return new SortedSetDVBytesAtomicFieldData( 119 | context.reader(), 120 | fieldNames.indexName()); 121 | } 122 | //或者通过loadGlobal方法得到 123 | //org.elasticsearch.index.fielddata.ordinals.InternalGlobalOrdinalsIndexFieldData 124 | ``` 125 | 126 | 以第一种情况为例,上面的代码new 了一个新的`org.elasticsearch.index.fielddata.AtomicOrdinalsFieldData`对象,该对象的一个实现类是`SortedSetDVBytesAtomicFieldData `。 这个对象和Lucene的DocValues 完成最后的对接: 127 | 128 | ``` 129 | @Override 130 | public RandomAccessOrds getOrdinalsValues() { 131 | try { 132 | return FieldData.maybeSlowRandomAccessOrds(DocValues.getSortedSet(reader, field)); 133 | } catch (IOException e) { 134 | throw new IllegalStateException("cannot load docvalues", e); 135 | } 136 | } 137 | ``` 138 | 我们看到,通过Reader获取到最后的列就是在该类里的getOrdinalsValues 方法里实现的。 139 | 140 | 该方法最后返回的RandomAccessOrds 就是Lucene的DocValues实现了。 141 | 142 | 分析了这么多,所有的逻辑就浓缩在`getLeafCollector `的第一行代码上。globalOrds 的类型是RandomAccessOrds,并且是直接和Lucene对应上了。 143 | 144 | ``` 145 | globalOrds = valuesSource.globalOrdinalsValues(cox); 146 | ``` 147 | 148 | getLeafCollector 最后newCollector的规则如下: 149 | 150 | ``` 151 | protected LeafBucketCollector newCollector(final RandomAccessOrds ords, final LeafBucketCollector sub) { 152 | grow(ords.getValueCount()); 153 | final SortedDocValues singleValues = DocValues.unwrapSingleton(ords); 154 | if (singleValues != null) { 155 | return new LeafBucketCollectorBase(sub, ords) { 156 | @Override 157 | public void collect(int doc, long bucket) throws IOException { 158 | assert bucket == 0; 159 | final int ord = singleValues.getOrd(doc); 160 | if (ord >= 0) { 161 | collectExistingBucket(sub, doc, ord); 162 | } 163 | } 164 | }; 165 | } 166 | ``` 167 | 168 | 我们知道,在Lucene里,大部分文件都是不可更新的。一个段一旦生成后就是不可变的,新的数据或者删除数据都需要生成新的段。DocValues的存储文件也是类似的。所以DocValues.unwrapSingleton其实就是做这个判定的,是不是有多个文件 。无论是否则不是都直接创建了一个匿名的Collector。 169 | 170 | 当个文件的很好理解,包含了索引中user字段所有的值,其坐标获取也很自然。 171 | 172 | ``` 173 | //singleValues其实就是前面的RandomAccessOrds。 174 | final int ord = singleValues.getOrd(doc); 175 | ``` 176 | 177 | 根据文档号获取值对应的位置,如果ord >=0 则代表有值,否则代表没有值。 178 | 179 | 如果有多个文件,则会返回如下的Collecor: 180 | 181 | ``` 182 | else { 183 | return new LeafBucketCollectorBase(sub, ords) { 184 | @Override 185 | public void collect(int doc, long bucket) throws IOException { 186 | assert bucket == 0; 187 | ords.setDocument(doc); 188 | final int numOrds = ords.cardinality(); 189 | for (int i = 0; i < numOrds; i++) { 190 | final long globalOrd = ords.ordAt(i); 191 | collectExistingBucket(sub, doc, globalOrd); 192 | } 193 | } 194 | }; 195 | ``` 196 | 197 | 上面的代码可以保证多个文件最终合起来保持一个文件的序号。什么意思呢?比如A文件有一个文档,B文件有一个,那么最终获取的globalOrd 就是0,1 而不会都是0。此时的 ords 实现类 不是SingletonSortedSetDocValues 而是 198 | ``` 199 | org.elasticsearch.index.fielddata.ordinals.GlobalOrdinalMapping 200 | ``` 201 | 202 | 对象了。 203 | 204 | 计数的方式两个都大体类似。 205 | ``` 206 | docCounts.increment(bucketOrd, 1); 207 | ``` 208 | 209 | 这里的bucketOrd 其实就是前面的ord/globalOrd。所以整个计算就是填充docCounts 210 | 211 | ## buildAggregation 212 | 213 | buildAggregation 代码我就不贴了,大体逻辑是: 214 | 215 | 1. 如果没有user字段,则直接返回。 216 | 2. 获取返回值的大小 TopN的N 217 | 3. 构建一个优先级队列,获取排序最高terms 218 | 4. 构建StringTerms 返回 219 | 220 | 这个逻辑也反应出了一个问题,TermsAggrator全局排序是不精准的。因为每个Shard 都只取TopN个,最后Merge(Reduce) 之后,不一定是全局的TopN。你可以通过size来控制精准度。 221 | 222 | 其实也从一个侧面验证了,在聚合结果较大的情况下,ES 还是有局限的。最好的方案是,ES算出所有的term count,然后通过iterator接入Spark,Spark 装载这些数据做最后的全局排序会比较好。 223 | 224 | ## 总结 225 | 226 | 我们以一个简单的例子对GlobalOrdinalsStringTermsAggregator 进行了分析,并且提及了ES对Lucene DocValues的封装的方式。 227 | -------------------------------------------------------------------------------- /ElasticSearch-Aggregations--分析.md: -------------------------------------------------------------------------------- 1 | > 承接上篇文章 [ElasticSearch Rest/RPC 接口解析](http://www.jianshu.com/p/3257b31c46f0),这篇文章我们重点分析让ES步入数据分析领域的Aggregation相关的功能和设计。 2 | 3 | ## 前言 4 | 5 | 我记得有一次到一家公司做内部分享,然后有研发问我,即席分析这块,他们用ES遇到一些问题。我当时直接就否了,我说ES还是个全文检索引擎,如果要做分析,还是应该用Impala,Phenix等这种主打分析的产品。随着ES的发展,我现在对它的看法,也有了比较大的变化。而且我认为ES+Spark SQL组合可以很好的增强即席分析能够处理的数据规模,并且能够实现复杂的逻辑,获得较好的易用性。 6 | 7 | 需要说明的是,我对这块现阶段的理解也还是比较浅。问题肯定有不少,欢迎指正。 8 | 9 | 10 | ## Aggregations的基础 11 | 12 | Lucene 有三个比较核心的概念: 13 | 14 | 1. 倒排索引 15 | 2. fieldData/docValue 16 | 3. Collector 17 | 18 | 倒排索引不用我讲了,就是term -> doclist的映射。 19 | 20 | fieldData/docValue 你可以简单理解为列式存储,索引文件的所有文档的某个字段会被单独存储起来。 对于这块,Lucene 经历了两阶段的发展。第一阶段是fieldData ,查询时从倒排索引反向构成doc-term。这里面有两个问题: 21 | 22 | * 数据需要全部加载到内存 23 | * 第一次构建会很慢 24 | 25 | 这两个问题其实会衍生出很多问题:最严重的自然是内存问题。所以lucene后面搞了DocValue,在构建索引的时候就生成这个文件。DocValue可以充分利用操作系统的缓存功能,如果操作系统cache住了,则速度和内存访问是一样的。 26 | 27 | 另外就是Collector的概念,ES的各个Aggregator 实现都是基于Collector做的。我觉得你可以简单的理解为一个迭代器就好,所有的候选集都会调用`Collector.collect(doc)`方法,这里collect == iterate 可能会更容易理解些。 28 | 29 | ES 能把聚合做快,得益于这两个数据结构,一个迭代器。我们大部分聚合功能,其实都是在fieldData/docValue 上工作的。 30 | 31 | ## Aggregations 分类 32 | 33 | Aggregations种类分为: 34 | 35 | 1. Metrics 36 | 2. Bucket 37 | 38 | Metrics 是简单的对过滤出来的数据集进行avg,max等操作,是一个单一的数值。 39 | 40 | Bucket 你则可以理解为将过滤出来的数据集按条件分成多个小数据集,然后Metrics会分别作用在这些小数据集上。 41 | 42 | 对于最后聚合出来的结果,其实我们还希望能进一步做处理,所以有了Pipline Aggregations,其实就是组合一堆的Aggregations 对已经聚合出来的结果再做处理。 43 | 44 | ## Aggregations 类设计 45 | 46 | 下面是一个聚合的例子: 47 | 48 | ``` 49 | { 50 | "aggregations": { 51 | "user": { 52 | "terms": { 53 | "field": "user", 54 | "size": 10, 55 | "order": { 56 | "_count": "desc" 57 | } 58 | } 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | 其语义类似这个sql 语句: `select count(*) as user_count group by user order by user_count desc`。 65 | 66 | 对于Aggregations 的解析,基本是顺着下面的路径分析: 67 | 68 | ``` 69 | TermsParser -> 70 | TermsAggregatorFactory -> 71 | GlobalOrdinalsStringTermsAggregator 72 | ``` 73 | 74 | 在实际的一次query里,要做如下几个阶段: 75 | 76 | 1. Query Phase 此时 会调用GlobalOrdinalsStringTermsAggregator的Collector 根据user 的不同进行计数。 77 | 78 | 2. RescorePhase 79 | 80 | 3. SuggestPhase 81 | 82 | 4. AggregationPhase 在该阶段会会执行实际的aggregation build, `aggregator.buildAggregation(0)`,也就是一个特定Shard(分片)的聚合结果 83 | 84 | 5. MergePhase。这一步是由接受到请求的ES来完成,具体负责执行Merge(Reduce)操作`SearchPhaseController.merge`。这一步因为会从不同的分片拿到数据再做Reduce,也是一个内存消耗点。所以很多人会专门搞出几台ES来做这个工作,其实就是ES的client模式,不存数据,只做接口响应。 85 | 86 | 在这里我们我们可以抽取出几个比较核心的概念: 87 | 88 | 1. AggregatorFactory (生成对应的Aggregator) 89 | 2. Aggregation (聚合的结果输出) 90 | 3. Aggregator (聚合逻辑实现) 91 | 92 | 另外值得注意的,PipeLine Aggregator 我前面提到了,其实是对已经生成的Aggregations重新做加工,这个工作是只能单机完成的,会放在请求的接收端执行。 93 | 94 | ## Aggregation Bucket的实现 95 | 96 | 前面的例子提到,在Query 阶段,其实就会调用Aggregator 的collect 方法,对所有符合查询条件的文档集都会计算一遍,这里我们涉及到几个对象: 97 | 98 | 1. doc id 99 | 2. field (docValue) 100 | 3. IntArray 对象 101 | 102 | collect 过程中会得到 doc id,然后拿着docId 到 docValue里去拿到field的值(一般而言字符串也会被编码成Int类型的),然后放到IntArray 进行计数。如果多个doc id 在某filed里的字段是相同的,则会递增计数。这样就实现了group by 的功能了。 103 | 104 | ## Spark-SQL 和 ES 的组合 105 | 106 | 我之前一直在想这个问题,后面看了下es-hadoop的文档,发现自己有些思路和现在es-hadoop的实现不谋而合。主要有几点: 107 | 108 | 1. Spark-SQL 的 where 语句全部(或者部分)下沉到 ES里进行执行,依赖于倒排索引,DocValues,以及分片,并行化执行,ES能够获得比Spark-SQL更优秀的响应时间 109 | 2. 其他部分包括分片数据Merge(Reduce操作,Spark 可以获得更好的性能和分布式能力),更复杂的业务逻辑都交给Spark-SQL (此时数据规模已经小非常多了),并且可以做各种自定义扩展,通过udf等函数 110 | 3. ES 无需实现Merge操作,可以减轻内存负担,提升并行Merge的效率(并且现阶段似乎ES的Reduce是只能在单个实例里完成) 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /ElasticSearch-Rest-RPC-接口解析.md: -------------------------------------------------------------------------------- 1 | > ElasticSearch 的体系结构比较复杂,层次也比较深,源码注释相比其他的开源项目要少。这是ElasticSearch 系列的第一篇。解析ElasticSearch的接口层,也就是Rest/RPC接口相关。我们会描述一个请求从http接口到最后被处理都经过了哪些环节。 2 | 3 | 4 | ## 一些基础知识 5 | 6 | 早先ES的HTTP协议支持还是依赖Jetty的,现在不管是Rest还是RPC都是直接基于Netty了。 7 | 8 | 另外值得一提的是,ES 是使用Google的Guice 进行模块管理,所以了解Guice的基本使用方式有助于你了解ES的代码组织。 9 | 10 | ES 的启动类是 `org.elasticsearch.bootstrap.Bootstrap`。在这里进行一些配置和环境初始化后会启动`org.elasticsearch.node.Node`。Node 的概念还是蛮重要的,节点的意思,也就是一个ES实例。RPC 和 Http的对应的监听启动都由在该类完成。 11 | 12 | Node 属性里有一个很重要的对象,叫client,类型是 NodeClient,我们知道ES是一个集群,所以每个Node都需要和其他的Nodes 进行交互,这些交互则依赖于NodeClient来完成。所以这个对象会在大部分对象中传递,完成相关的交互。 13 | 14 | 先简要说下: 15 | 16 | * NettyTransport 对应RPC 协议支持 17 | * NettyHttpServerTransport 则对应HTTP协议支持 18 | 19 | ## Rest 模块解析 20 | 21 | 首先,NettyHttpServerTransport 会负责进行监听Http请求。通过配置`http.netty.http.blocking_server` 你可以选择是Nio还是传统的阻塞式服务。默认是NIO。该类在配置pipeline的时候,最后添加了HttpRequestHandler,所以具体的接受到请求后的处理逻辑就由该类来完成了。 22 | 23 | ``` 24 | pipeline.addLast("handler", requestHandler); 25 | ``` 26 | 27 | HttpRequestHandler 实现了标准的 `messageReceived(ChannelHandlerContext ctx, MessageEvent e) ` 方法,在该方法中,HttpRequestHandler 会回调`NettyHttpServerTransport.dispatchRequest`方法,而该方法会调用`HttpServerAdapter.dispatchRequest`,接着又会调用`HttpServer.internalDispatchRequest `方法(额,好吧,我承认嵌套挺深有点深): 28 | 29 | ``` 30 | public void internalDispatchRequest(final HttpRequest request, final HttpChannel channel) { 31 | String rawPath = request.rawPath(); 32 | if (rawPath.startsWith("/_plugin/")) { 33 | RestFilterChain filterChain = restController.filterChain(pluginSiteFilter); 34 | filterChain.continueProcessing(request, channel); 35 | return; 36 | } else if (rawPath.equals("/favicon.ico")) { 37 | handleFavicon(request, channel); 38 | return; 39 | } 40 | restController.dispatchRequest(request, channel); 41 | } 42 | ``` 43 | 44 | 这个方法里我们看到了plugin等被有限处理。最后请求又被转发给 RestController。 45 | 46 | RestController 大概类似一个微型的Controller层框架,实现了: 47 | 48 | 1. 存储了 Method + Path -> Controller 的关系 49 | 2. 提供了注册关系的方法 50 | 3. 执行Controller的功能。 51 | 52 | 那么各个Controller(Action) 是怎么注册到RestController中的呢? 53 | 54 | 在ES中,Rest*Action 命名的类的都是提供http服务的,他们会在RestActionModule 中被初始化,对应的构造方法会注入RestController实例,接着在构造方法中,这些Action会调用`controller.registerHandler` 将自己注册到RestController。典型的样子是这样的: 55 | 56 | ``` 57 | @Inject 58 | public RestSearchAction(Settings settings, RestController controller, Client client) { 59 | super(settings, controller, client); 60 | controller.registerHandler(GET, "/_search", this); 61 | controller.registerHandler(POST, "/_search", this); 62 | controller.registerHandler(GET, "/{index}/_search", this); 63 | ``` 64 | 65 | 每个Rest*Action 都会实现一个handleRequest方法。该方法接入实际的逻辑处理。 66 | 67 | ``` 68 | @Override 69 | public void handleRequest(final RestRequest request, final RestChannel channel, final Client client) { 70 | SearchRequest searchRequest; 71 | searchRequest = RestSearchAction.parseSearchRequest(request, parseFieldMatcher); 72 | client.search(searchRequest, new RestStatusToXContentListener(channel)); 73 | } 74 | ``` 75 | 76 | 首先是会把 请求封装成一个SearchRequest对象,然后交给 NodeClient 执行。 77 | 78 | 如果用过ES的NodeClient Java API,你会发现,其实上面这些东西就是为了暴露NodeClient API 的功能,使得你可以通过HTTP的方式调用。 79 | 80 | 81 | ## Transport*Action,两层映射关系解析 82 | 83 | 我们先跑个题,在ES中,Transport*Action 是比较核心的类集合。这里至少有两组映射关系。 84 | 85 | * Action -> Transport*Action 86 | * Transport*Action -> *TransportHandler 87 | 88 | 第一层映射关系由类似下面的代码在ActionModule中完成: 89 | 90 | registerAction(PutMappingAction.INSTANCE, TransportPutMappingAction.class); 91 | 92 | 第二层映射则在类似 SearchServiceTransportAction 中维护。目前看来,第二层映射只有在查询相关的功能才有,如下: 93 | 94 | ``` 95 | transportService.registerRequestHandler(FREE_CONTEXT_SCROLL_ACTION_NAME, ScrollFreeContextRequest.class, ThreadPool.Names.SAME, new FreeContextTransportHandler<>()); 96 | ``` 97 | 98 | SearchServiceTransportAction 可以看做是SearchService进一步封装。其他的Transport*Action 则只调用对应的Service 来完成实际的操作。 99 | 100 | 对应的功能是,可以通过Action 找到对应的Transport*Action,这些Transport*Action 如果是query类,则会调用SearchServiceTransportAction,并且通过第二层映射找到对应的Handler,否则可能就直接通过对应的Service完成操作。 101 | 102 | 下面关于RPC调用解析这块,我们会以查询为例。 103 | 104 | ## RPC 模块解析 105 | 106 | 前面我们提到,Rest接口最后会调用NodeClient来完成后续的请求。对应的代码为: 107 | 108 | ``` 109 | public > void doExecute(Action action, Request request, ActionListener listener) { 110 | TransportAction transportAction = actions.get(action); 111 | if (transportAction == null) { 112 | throw new IllegalStateException("failed to find action [" + action + "] to execute"); 113 | } 114 | transportAction.execute(request, listener); 115 | } 116 | ``` 117 | 118 | 这里的action 就是我们提到的第一层映射,找到Transport*Action.如果是查询,则会找到TransportSearchAction。调用对应的doExecute 方法,接着根据searchRequest.searchType找到要执行的实际代码。下面是默认的: 119 | 120 | ``` 121 | else if (searchRequest.searchType() == SearchType.QUERY_THEN_FETCH) { queryThenFetchAction.execute(searchRequest, listener);} 122 | ``` 123 | 我们看到Transport*Action 是可以嵌套的,这里调用了TransportSearchQueryThenFetchAction.doExecute 124 | 125 | ``` 126 | @Overrideprotected void doExecute(SearchRequest searchRequest, ActionListener listener) { 127 | new AsyncAction(searchRequest, listener).start(); 128 | } 129 | ``` 130 | 131 | 在AsyncAction中完成三个步骤: 132 | 133 | 1. query 134 | 2. fetch 135 | 3. merge 136 | 137 | 为了分析方便,我们只分析第一个步骤。 138 | 139 | ``` 140 | @Overrideprotected void sendExecuteFirstPhase( 141 | DiscoveryNode node, 142 | ShardSearchTransportRequest request, 143 | ActionListener listener) { 144 | searchService.sendExecuteQuery(node, request, listener); 145 | } 146 | ``` 147 | 148 | 这是AsyncAction 中执行query的代码。我们知道ES是一个集群,所以query 必然要发到多个节点去,如何知道某个索引对应的Shard 所在的节点呢?这个是在AsyncAction的父类中完成,该父类分析完后会回调子类中的对应的方法来完成,譬如上面的sendExecuteFirstPhase 方法。 149 | 150 | 说这个是因为需要让你知道,上面贴出来的代码只是针对一个节点的查询结果,但其实最终多个节点都会通过相同的方式进行调用。所以才会有第三个环节 merge操作,合并多个节点返回的结果。 151 | 152 | ``` 153 | searchService.sendExecuteQuery(node, request, listener); 154 | ``` 155 | 156 | 其实会调用transportService的sendRequest方法。大概值得分析的地方有两个: 157 | 158 | ``` 159 | if (node.equals(localNode)) { 160 | sendLocalRequest(requestId, action, request); 161 | } else { 162 | transport.sendRequest(node, requestId, action, request, options); 163 | } 164 | ``` 165 | 166 | 我们先分析,如果是本地的节点,则sendLocalRequest是怎么执行的。如果你跑到senLocalRequest里去看,很简单,其实就是: 167 | 168 | ``` 169 | reg.getHandler().messageReceived(request, channel); 170 | ``` 171 | 172 | reg 其实就是前面我们提到的第二个映射,不过这个映射其实还包含了使用什么线程池等信息,我们在前面没有说明。 173 | 174 | 这里 reg.getHandler == SearchServiceTransportAction.SearchQueryTransportHandler,所以messageReceived 方法对应的逻辑是: 175 | 176 | ``` 177 | QuerySearchResultProvider result = searchService.executeQueryPhase(request); 178 | channel.sendResponse(result); 179 | ``` 180 | 181 | 这里,我们终于看到searchService。 在searchService里,就是整儿八景的Lucene相关查询了。这个我们后面的系列文章会做详细分析。 182 | 183 | 如果不是本地节点,则会由NettyTransport.sendRequest 发出远程请求。 184 | 假设当前请求的节点是A,被请求的节点是B,则B的入口为MessageChannelHandler.messageReceived。在NettyTransport中你可以看到最后添加的pipeline里就有MessageChannelHandler。我们跑进去messageReceived 看看,你会发现基本就是一些协议解析,核心方法是handleRequest,接着就和本地差不多了,我提取了关键的几行代码: 185 | 186 | ``` 187 | final RequestHandlerRegistry reg = transportServiceAdapter.getRequestHandler(action); 188 | threadPool.executor(reg.getExecutor()).execute(new RequestHandler(reg, request, transportChannel)); 189 | ``` 190 | 191 | 这里被RequestHandler包了一层,其实内部执行的就是本地的那个。RequestHandler 的run方法是这样的: 192 | 193 | ``` 194 | protected void doRun() throws Exception { reg.getHandler().messageReceived(request, transportChannel); 195 | } 196 | ``` 197 | 198 | 这个就和前面的sendLocalRequest里的一模一样了。 199 | 200 | 201 | ## 总结 202 | 203 | 到目前为止,我们知道整个ES的Rest/RPC 的起点是从哪里开始的。RPC对应的endpoint 是MessageChannelHandler,在NettyTransport 被注册。Rest 接口的七点则在NettyHttpServerTransport,经过层层代理,最终在RestController中被执行具体的Action。 Action 的所有执行都会被委托给NodeClient。 NodeClient的功能执行单元是各种Transport*Action。对于查询类请求,还多了一层映射关系。 204 | -------------------------------------------------------------------------------- /SparkES-多维分析引擎设计.md: -------------------------------------------------------------------------------- 1 | ## 设计动机 2 | 3 | ElasticSearch 毫秒级的查询响应时间还是很惊艳的。其优点有: 4 | 5 | 1. 优秀的全文检索能力 6 | 2. 高效的列式存储与查询能力 7 | 3. 数据分布式存储(Shard 分片) 8 | 9 | 其列式存储可以有效的支持高效的聚合类查询,譬如groupBy等操作,分布式存储则提升了处理的数据规模。 10 | 11 | 相应的也存在一些缺点: 12 | 13 | 1. 缺乏优秀的SQL支持 14 | 2. 缺乏水平扩展的Reduce(Merge)能力,现阶段的实现局限在单机 15 | 3. JSON格式的查询语言,缺乏编程能力,难以实现非常复杂的数据加工,自定义函数(类似Hive的UDF等) 16 | 17 | Spark 作为一个计算引擎,可以克服ES存在的这些缺点: 18 | 19 | 1. 良好的SQL支持 20 | 2. 强大的计算引擎,可以进行分布式Reduce 21 | 3. 支持自定义编程(采用原生API或者编写UDF等函数对SQL做增强) 22 | 23 | 所以在构建即席多维查询系统时,Spark 可以和ES取得良好的互补效果。通过ES的列式存储特性,我们可以非常快的过滤出数据, 24 | 并且支持全文检索,之后这些过滤后的数据从各个Shard 进入Spark,Spark分布式的进行Reduce/Merge操作,并且做一些更高层的工作,最后输出给用户。 25 | 26 | 通常而言,结构化的数据结构可以有效提升数据的查询速度,但是会对数据的构建产生一定的吞吐影响。ES强大的Query能力取决于数据结构化的存储(索引文件),为了解决这个问题,我们可以通过Spark Streaming 27 | 有效的对接各个数据源(Kafka/文件系统)等,将数据规范化后批量导入到ES的各个Shard。Spark Streaming 基于以下两点可以实现为ES快速导入数据。 28 | 29 | 1. Spark RDD 的Partition 能够良好的契合ES的Shard的概念。能够实现一一对应。避免经过ES的二次分发 30 | 2. Spark Streaming 批处理的模式 和 Lucene(ES的底层存储引擎)的Segment对应的非常好。一次批处理意味着新生成一个文件, 31 | 我们可以有效的控制生成文件的大小,频度等。 32 | 33 | 34 | 35 | ## 架构设计 36 | 37 | 下面是架构设计图: 38 | 39 | ![spark-es-4.png](http://upload-images.jianshu.io/upload_images/1063603-8b7c006fb3422d8e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 40 | 41 | 整个系统大概分成四个部分。分别是: 42 | 43 | 1. API层 44 | 2. Spark 计算引擎层 45 | 3. ES 存储层 46 | 4. ES 索引构建层 47 | 48 | ## API 层 49 | 50 | API 层主要是做多查询协议的支持,比如可以支持SQL,JSON等形态的查询语句。并且可是做一些启发式查询优化。从而决定将查询请求是直接转发给后端的ES来完成,还是走Spark 计算引擎。也就是上图提到的 Query Optimize,根据条件决定是否需要短路掉 Spark Compute。 51 | 52 | ## Spark 计算引擎层 53 | 54 | 前面我们提到了ES的三个缺陷,而Spark 可以有效的解决这个问题。对于一个普通的SQL语句,我们可以把 where 条件的语句,部分group 等相关的语句下沉到ES引擎进行执行,之后可能汇总了较多的数据,然后放到Spark中进行合并和加工,最后转发给用户。相对应的,Spark 的初始的RDD 类似和Kafka的对接,每个Kafka 的partition对应RDD的一个partiton,每个ES的Shard 也对应RDD的一个partition。 55 | 56 | ## ES 存储层 57 | 58 | ES的Shard 数量在索引构建时就需要确定,确定后无法进行更改。这样单个索引里的Shard 会越来越大从而影响单Shard的查询速度。但因为上层有了 Spark Compute层,所以我们可以通过添加Index的方式来扩大Shard的数目,然后查询时查询所有分片数据,由Spark完成数据的合并工作。 59 | 60 | ## ES 索引构建层 61 | 62 | 数据的结构化必然带来了构建的困难。所以有了Spark Streaming层作为数据的构建层。这里你有两种选择: 63 | 64 | 1. 通过ES原生的bulk API 完成索引的构建 65 | 2. 然Spark 直接对接到 ES的每个Shard,直接针对该Shard 进行索引,可有效替身索引的吞吐量。 66 | 67 | -------------------------------------------------------------------------------- /如何提高ElasticSearch-索引速度.md: -------------------------------------------------------------------------------- 1 | 我Google了下,大致给出的答案如下: 2 | 3 | 1. 使用bulk API 4 | 2. 初次索引的时候,把 replica 设置为 0 5 | 3. 增大 threadpool.index.queue_size 6 | 4. 增大 indices.memory.index_buffer_size 7 | 5. 增大 index.translog.flush_threshold_ops 8 | 6. 增大 index.translog.sync_interval 9 | 7. 增大 index.engine.robin.refresh_interval 10 | 11 | 这篇文章会讲述上面几个参数的原理,以及一些其他的思路。这些参数大体上是朝着两个方向优化的: 12 | 13 | 1. 减少磁盘写入 14 | 2. 增大构建索引处理资源 15 | 16 | 一般而言,通过第二种方式的需要慎用,会对集群查询功能造成比较大的影响。 17 | 这里还有两种形态的解决方案: 18 | 19 | 1. 关闭一些特定场景并不需要的功能,比如Translog或者Version等 20 | 2. 将部分计算挪到其他并行计算框架上,比如数据的分片计算等,都可以放到Spark上事先算好 21 | 22 | ## 上面的参数都和什么有关 23 | 24 | 其中 5,6 属于 TransLog 相关。 25 | 4 则和Lucene相关 26 | 3 则因为ES里大量采用线程池,构建索引的时候,是有单独的线程池做处理的 27 | 7 的话个人认为影响不大 28 | 2 的话,能够使用上的场景有限。个人认为Replica这块可以使用Kafka的ISR机制。所有数据还是都从Primary写和读。Replica尽量只作为备份数据。 29 | 30 | 31 | 32 | ## Translog 33 | 34 | 为什么要有Translog? 因为Translog顺序写日志比构建索引更高效。我们不可能每加一条记录就Commit一次,这样会有大量的文件和磁盘IO产生。但是我们又想避免程序挂掉或者硬件故障而出现数据丢失,所以有了Translog,通常这种日志我们叫做Write Ahead Log。 35 | 36 | 为了保证数据的完整性,ES默认是每次request结束后都会进行一次sync操作。具体可以查看如下方法: 37 | 38 | org.elasticsearch.action.bulk.TransportShardBulkAction.processAfter 39 | 40 | 该方法会调用IndexShard.sync 方法进行文件落地。 41 | 42 | 你也可以通过设置`index.translog.durability=async` 来完成异步落地。这里的异步其实可能会有一点点误导。前面是每次request结束后都会进行sync,这里的sync仅仅是将Translog落地。而无论你是否设置了async,都会执行如下操作: 43 | 44 | 根据条件,主要是每隔sync_interval(5s) ,如果flush_threshold_ops(Integer.MAX_VALUE),flush_threshold_size(512m),flush_threshold_period(30m) 满足对应的条件,则进行flush操作,这里除了对Translog进行Commit以外,也对索引进行了Commit。 45 | 46 | 所以如果你是海量的日志,可以容忍发生故障时丢失一定的数据,那么完全可以设置,`index.translog.durability=async`,并且将前面提到的flush*相关的参数调大。 47 | 48 | 而极端情况,你还可以有两个选择: 49 | 50 | 1. 设置`index.translog.durability=async`,接着设置`index.translog.disable_flush=true`进行禁用定时flush。然后你可以通过应用程序自己手动来控制flush。 51 | 52 | 2. 通过改写ES 去掉Translog日志相关的功能 53 | 54 | 55 | ## Version 56 | 57 | Version可以让ES实现并发修改,但是带来的性能影响也是极大的,这里主要有两块: 58 | 59 | 1. 需要访问索引里的版本号,触发磁盘读写 60 | 2. 锁机制 61 | 62 | 目前而言,似乎没有办法直接关闭Version机制。你可以使用自增长ID并且在构建索引时,index 类型设置为create。这样可以跳过版本检查。 63 | 64 | 这个场景主要应用于不可变日志导入,随着ES被越来越多的用来做日志分析,日志没有主键ID,所以使用自增ID是合适的,并且不会进行更新,使用一个固定的版本号也是合适的。而不可变日志往往是追求吞吐量。 65 | 66 | 当然,如果有必要,我们也可以通过改写ES相关代码,禁用版本管理。 67 | 68 | ## 分发代理 69 | 70 | ES是对索引进行了分片(Shard),然后数据被分发到不同的Shard。这样 查询和构建索引其实都存在一个问题: 71 | 72 | > 如果是构建索引,则需要对数据分拣,然后根据Shard分布分发到不同的Node节点上。 73 | > 如果是查询,则对外提供的Node需要收集各个Shard的数据做Merge 74 | 75 | 这都会对对外提供的节点造成较大的压力,从而影响整个bulk/query 的速度。 76 | 77 | 一个可行的方案是,直接面向客户提供构建索引和查询API的Node节点都采用client模式,不存储数据,可以达到一定的优化效果。 78 | 79 | 另外一个较为麻烦但似乎会更优的解决方案是,如果你使用类似Spark Streaming这种流式处理程序,在最后往ES输出的时候,可以做如下几件事情: 80 | 81 | 1. 获取所有primary shard的信息,并且给所有shard带上一个顺序的数字序号,得到partition(顺序序号) -> shardId的映射关系 82 | 2. 对数据进行repartition,分区后每个partition对应一个shard的数据 83 | 3. 遍历这些partions,写入ES。方法为直接通过RPC 方式,类似`transportService.sendRequest` 将数据批量发送到对应包含有对应ShardId的Node节点上。 84 | 85 | 这样有三点好处: 86 | 87 | 1. 所有的数据都被直接分到各个Node上直接处理。避免所有的数据先集中到一台服务器 88 | 2. 避免二次分发,减少一次网络IO 89 | 3. 防止最先处理数据的Node压力太大而导致木桶短板效应 90 | 91 | ## 场景 92 | 93 | 因为我正好要做日志分析类的应用,追求高吞吐量,这样上面的三个优化其实都可以做了。一个典型只增不更新的日志入库操作,可以采用如下方案: 94 | 95 | 1. 对接Spark Streaming,在Spark里对数据做好分片,直接推送到ES的各个节点 96 | 2. 禁止自动flush操作,每个batch 结束后手动flush。 97 | 3. 避免使用Version 98 | 99 | 我们可以预期ES会产生多少个新的Segment文件,通过控制batch的周期和大小,预判出ES Segment索引文件的生成大小和Merge情况。最大可能减少ES的一些额外消耗 100 | 101 | ## 总结 102 | 103 | 大体是下面这三个点让es比原生的lucene吞吐量下降了不少: 104 | 105 | 1. 为了数据完整性 ES额外添加了WAL(tanslog) 106 | 2. 为了能够并发修改 添加了版本机制 107 | 3. 对外提供服务的node节点存在瓶颈 108 | 109 | ES的线性扩展问题主要受限于第三点, 110 | 具体描述就是: 111 | 112 | >如果是构建索引,接受到请求的Node节点需要对数据分拣,然后根据Shard分布分发到不同的Node节点上。 113 | 如果是查询,则对外提供的Node需要收集各个Shard的数据做Merge 114 | 115 | 另外,索引的读写并不需要向Master汇报。 116 | --------------------------------------------------------------------------------