elasticsearch之modeling your data -- design for scale

来源:互联网 发布:任务管理器软件 编辑:程序博客网 时间:2024/05/29 10:16

elasticsearch每天处理pb级别的数据,易于扩展。无论在个人电脑还是几百个节点的集群都能很好的工作。从一个小集群升级到一个较大的集群几乎是自动的。从一个较大集群升级到一个非常大的集群需要一些额外的设计和考量,但是仍然是相对简单易用的。当然,es并不是魔术师,也有它的局限性。如果你能意识到这些不足并能很好的处理,那扩展起来将是非常简单的,否则,如果你不恰当的使用es,那将会导致痛苦。es默认的设置已经可以处理很多问题,但是如果想走的更远,你就需要了解数据在你的系统中是如何运转的。我们将讨论两种数据流:Time-based data(日志,社交网络数据流等相关性随数据的新旧程度改变的数据)和user-based data(可以从用户维度分割的大量数据集合)。

本节内容将会帮助你在问题出现之前作出正确的决策,以防日后讨厌的情况出现。

1:the unit of scale

shard是一个lucene index,一个es的index是一系列shard的集合。app跟index交互,index把请求发给shard。

shard是可扩展的基本单元。最小的索引也得有一个shard。可能这已经足够用了,一个shard可以含有很多数据,但是它限制了可扩展性。

假设我们的集群有一个node,我们创建了一个index,含有一个shard,没有replicas。目前已经可以满足我们的要求,各项功能很正常。某天,我们的数据量猛增,我们需要添加一个node,如何扩展?没法扩展。因为我们只有一个shard,没有办法把它分到不同机器上。由于shard的数据是route的重要因素,所以index无法动态的增加shard。唯一的办法就是重建索引,多建立几个shard。

2:shard overallocation

一个shard只能位于一个node上,但是一个node可以承载多个shard。假设我们创建了2个shard。如果只有一个node,两个shard则都会在这node上。现在我我们要增加一个node,es会自动把一个shard移动到新增加的node上。我们可以通过将一个shardcopy到新的节点上来扩充容量,而这个过程并不需要暂停我们对外提供的服务。indexing 和 search仍然正常进行。es中新创建的索引会建立5个shard。这意味着我们可以将数据散发到5个node上。这个容量已经够大了,并且这个过程都是自动的。

es为什么不支持shard的split操作呢?es认为这是一个糟糕的主意

(1)splitting shard相当于重建索引。想对于node之间的shard迁移,重建索引是个负载很重的操作。

(2)spliting是指数级的操作,2变4,4变8....splitting不允许你通过50%来扩展容量。

(3)splitting需要有足够的额外空间来容纳新的副本。通常,你意识到要扩展的时候,你并没有足够多的空间来支持split。

es也有一种方法可以支持shard split。你可以对你的数据重建一个新的索引,指定一个足够大的shard数目,当然这也是一个重量级的操作,但至少你可以控制你的shard数量。

3:kagillion shrads

我们并不知道将来数据量会有多少,所以我们先设置1000个shard吧。-----No!!

shard并不是free的,请记住:

(1)shard是一个lucene的index,占用file memery 和 cpu circle

(2)没一个search request都需要在所有shard上执行操作。如果每一个shard都占用一个单独的node,这是ok的,但是如果多个shard落在同一个node上,将会有资源的竞争。

(3)Term stat需要计算相关性,是以shard为单位的,如果shard数目很多,而每shard上的document又很少,relevance的计算就会不准确。

因此扩展是要分阶段的,这个阶段的事情就要要留足资源给下一个阶段的扩展,而不过分扩展。

4:capacity planning

一个shard太少,1000太多,那究竟多少合适?这个问题没有通用的答案。需根据应用场景而定:硬件条件,数据量,数据复杂性,查询类型,聚合类型等等。

幸运的是,在你的应用场景下的一个特殊情形可以作为参照:

用生产场景下的硬件条件建立单节点集群,用生产场景下打算用的分析器和设置创建只有一个分片的索引,把索引用真实的数据或者接近真实的数据填满,运行真实的或者接近真实的查询和聚合操作。

你可以在以上情形下运行压力足够大的操作让这个shard挂掉,来测试出一个基准值。挂掉的含义比如:正常接受的查询相应是50ms,而现在达到了5s之类的。

一旦你得到一个单个shard的基准值,扩展到整个index就简单多了。总数据量*扩展系数/单个shard的数据量=设定的shard数目。

tip:容量计划不应该是你的第一步。首先应该找到你用es的正确方式,也许你有不太频繁的查询,稀少的ram等等。

5:replica shards

以上我们都在讨论primary shard,遗忘了replicas。replicas的主要作用是容错,如果primary shard因为某些问题挂了,其对应replicas会自动成为primary。在indexing阶段,二者没区别。新文档先在primary shard上然后去raplicas。增加replicas并不能增加index的容量。然而,replicas可以为read操作服务。多数情况,应用是search-heavy的,通过增加replicas数量可以提升search performance(同时要增加硬件)。

回到之前的例子:现在我们有两个primary shard,设置replicas为1。这样我们有了4个shard,可以扩展到4个node上。

balancing load with replicas:

search performance取决于最慢node的返回时间,因此把压力扩展到所有机器是一个好的方案。现在是2个node,4个分片。现在增加机器到3台,设置replicas为2.就有了6个shard,其中一个node上没有primary shard,只有两个replicas。没关系。

6:multiple indices

没有什么限定你的应用只能在一个索引中执行操作。当你执行一个search request的时候,请求会定位到一系列shard上去。如果请求定位到多个索引,也不过是多几个shard而已。当你需要动态的增加索引空间时,不用设置一个较大的shard num去重建索引,只需要如下调整:创建一个新索引来容纳数据,在新旧索引上同时执行search操作。

事实上,这个操作可以对应用完全透明。运用alias机制可以实现不停服务完成索引切换,同样的技术也可以应用到扩展索引容量上。现在我们需要两个alias,一个用来indexing,另一个用来searching。

PUT /tweets_1/_alias/tweets_search PUT /tweets_1/_alias/tweets_index 
新数据需要index到tweets_index,而search操作应该在tweets_search。现在两个alias都指向了tweets_1。
当我们需要额外的空间时候,我们可以创建一个新索引tweet_2,并且更新alias:
POST /_aliases{  "actions": [    { "add":    { "index": "tweets_2", "alias": "tweets_search" }},     { "remove": { "index": "tweets_1", "alias": "tweets_index"  }},     { "add":    { "index": "tweets_2", "alias": "tweets_index"  }}    ]}
现在新文档会在tweets_2上进行索引,而search操作同时在tweets_1和tweets_2上执行。
注意:GET操作跟indexing操作一样,只能在一个索引中执行。这就让通过id来检索有一点麻烦。替代方案是:采用search的方式,ids query或者multi_get在不同索引上。
用多个索引和alias机制来动态的扩展索引容量尤其适用于time-based data。
7:time-based data
es的一个典型案例是用于logging,提供了ELK的平台(不做介绍)。
logging和其他time-based data有显著的特点:文档数目增长迅速,常常是每时每刻都在增加;文档几乎不会更新,查询需求大多集中在最近最新的文档中。时间越久的文档价值往往越低。我们需要调整我们的索引策略来适应这种数据形式。
Index per timeframe:
如果我们用一整个大的索引来容纳数据,不就我们的存储空间就会不够。Logging是时时刻刻都在产生的,没有间断和停歇。我们可以通过delete-by-query来删除旧数据
DELETE /logs/event/_query{  "query": {    "range": {      "@timestamp": {         "lt": "now-90d"      }    }  }}
但是这个方法并不足够高效。删除一个文档只是做了标记,并没有真正删除,而是等到merge执行的时候才会真正删除掉。
然而,采用index per timeframe的方式建立索引是比较高效的。比如我们可以以年为单位建立索引,log_2014,或者以月为单位,log_2014-10等。删除旧数据也非常高效,直接删除对应索引即可。
这种方式有利于扩展。每天的情形可能都不一样,可以随时调整。也许刚开始你只需要一周用一个shard就足够,后期数据增加,一天用一个shard。没关系,随时调整吧。
alias机制同样起到很大作用,切换索引透明。对于indexing,可以指向log_current,对于searching,可以更新last_3_months来指向最近3个月的索引:
POST /_aliases{  "actions": [    { "add":    { "alias": "logs_current",  "index": "logs_2014-10" }},    { "remove": { "alias": "logs_current",  "index": "logs_2014-09" }},     { "add":    { "alias": "last_3_months", "index": "logs_2014-10" }},     { "remove": { "alias": "last_3_months", "index": "logs_2014-07" }}    ]}
8:index templete
es并不需要在使用索引之前先行创建。对于logging类型的引用,想对于手动建立索引,自动建立索引机制更加方便。
Logstash用timestamp来提取索引名字。比如2014-10-01 00:00:01这条数据将会放到索引logstash-2014.10.01中去,如果索引不存在就自动创建。
通常,我们对新创建的索引要有一些通用的设置和mapping的设置。比如,我们想增加replicas的数目,我们想禁用_all字段等等。index template将会应用这些设置到新索引上
PUT /_template/my_logs {  "template": "logstash-*",   "order":    1,   "settings": {    "number_of_shards": 1   },  "mappings": {    "_default_": {       "_all": {        "enabled": false      }    }  },  "aliases": {    "last_3_months": {}   }}
这个模板将会应用在所有以logstash开头的索引中,不管是手动创建还是自动创建。如果我们觉得明天的索引可能需要更大的索引,我们可以update索引以支持更多的shard。
模板会把最新创建的索引加到last_3_months这个alias中,如果要从alias中移除过期的索引,需要手动去做。
9:retiring data
time-based类型的数据,随着时间推移,相关性会减低。但是我们有可能去看前几天,前几个月甚至是去年的数据。
index per-timeframe这样建立索引的方式使得删除索引变得非常容易,直接delete即可:
DELETE /log_2013*
删除操作非常高效,es直接删除了对应索引的数据目录。但是删除索引有点太直接了。。。我们有更优雅的方式去处理过期的数据。
migrate old indices:
 当天的日志数据是热点数据,所有的新添加文档都会进入当天的索引中,并且大多数查询请求也会定位到当天的索引。所以利用使用最佳的硬件条件去支持热点数据。
但是es是如何知道哪些server的硬件条件较好的呢?你告诉它!给没一个server一个标志。比如,你可以这样启动一个es节点:
./bin/elasticsearch --node.box_type strong
指定了一个box_type参数(名称随意)。然后可以把今天热点数据数据定位到最佳的server上,这样设置:
PUT /logs_2014-10-01{  "settings": {    "index.routing.allocation.include.box_type" : "strong"  }}
这样,昨天的过期数据就应该从最佳性能的server上转移掉,所以我们可以转移到box_type为medium的机器上去:
POST /logs_2014-09-30/_settings{  "index.routing.allocation.include.box_type" : "medium"}

optimize indices:
昨天的过期数据基本上不会发生改变了,发生了的事件已经成为过去,无法改变。如果我们把没一个shard都整理为一个segment,就会减低资源的消耗,并且查询性能也会提升。我们可以借助optimize api来做这件事情。但是,对正在建立索引的热点数据进行optimize是一个不好的方案,因为optimize过程需要大量io操作,这会降低建立索引的性能。但是对过去数据进行optimize就是合适的。
过期数据可能有不止一个的replicas。如果我们执行了一个optimize request,将会对primary shard和replicas同时进行优化处理,这是一种浪费。因此我们可以先把replicas移除,然后进行optimize,然后在copy一个replicas出来:
POST /logs_2014-09-30/_settings{ "number_of_replicas": 0 }POST /logs_2014-09-30/_optimize?max_num_segments=1POST /logs_2014-09-30/_settings{ "number_of_replicas": 1 }
当然,如果没有replicas的情况,如果发生灾难,数据就会丢失。我们应该先备份数据,采用snapshot-restore api。
closing old indnces:
随着时间的推移,一些索引可能很少被访问,甚至永远不会访问。我们可以删除他们,但也许我们可以保留这些索引以防某天再访问。这种类型的索引可以关闭,他们仍将存在集群中,但是除了硬盘空间不再占有集群的任何资源。再者,重新打开一个索引要比从备份中恢复一个索引要快的多。
在关闭之间,要确保在transaction log中没有了操作,所以先执行一个flush操作。而且一个空的transaction log也会使得recovery过程非常迅速。
POST /logs_2014-01-*/_flush POST /logs_2014-01-*/_close POST /logs_2014-01-*/_open 
archiving old indices:
最后,非常非常陈旧的数据可以归档到硬盘上或者分布式存储中,通过snapshot-restore api。有了backup可以把数据从索引集群中删除。
10:user-based data
通常,用户使用es是为了对应用全文索引。他们创建了一个索引用来存放该应用对应的所有document。逐渐的,应用越来越多,也逐渐创建索引,让数据进入集群。幸运的是,es支持multitenancy,在同一个集群中每个用户可以拥有他们各自的索引。有时候,还需要在所有用户数据上进行搜索,就相当于在所有索引中进行检索。大多数情况下,用户对别人的数据并没有多少兴趣。
一些用户数据较多,一些用户搜索压力较大,因此如何指定shard的数量需要跟具体用户的数据模型密切相关。同样的,负载高的index应该考虑放在硬件条件为strong的server上。
下边的章节将拿一个论坛的例子来说明具体的应用场景和shard的选择。有些论坛较小,而有些论坛很大,多个小论坛累加起来有时候也不及一个大论坛,因此这些小论坛需要共享shard,避免浪费。
11:shared index
我们可以建立一个大的共享的索引来共多个小论坛数据使用,把forum_id作为论坛的唯一标识,同时作为一个filter使用。
PUT /forums{  "settings": {    "number_of_shards": 10   },  "mappings": {    "post": {      "properties": {        "forum_id": {           "type":  "string",          "index": "not_analyzed"        }      }    }  }}PUT /forums/post/1{  "forum_id": "baking",   "title":    "Easy recipe for ginger nuts",  ...}
当我们需要检索某一个论坛的数据的时候,只需要将forum_id作为filter使用即可:
GET /forums/post/_search{  "query": {    "filtered": {      "query": {        "match": {          "title": "ginger nuts"        }      },      "filter": {        "term": {           "forum_id": {            "baking"          }        }      }    }  }}

这个方式很有效,但是我们可以做得更好。一个小论坛的帖子也许在1个shard中就可以满足,而上边的设置则需要从10个shard的查找。理想的方式是直接定位到该论坛对应的shard上即可。想到了我们的routing机制:
shard = hash(routing) % number_of_primary_shards
默认是使用_id作为routing value的。但是我们可以自定义routing value来覆盖默认的_id。这里我们利索应当指定routing value为forum_id。
PUT /forums/post/1?routing=baking {  "forum_id": "baking",   "title":    "Easy recipe for ginger nuts",  ...}
这样,同一个论坛的数据就存放在同一个shard中了,查询的时候也制定routing value即可:
GET /forums/post/_search?routing=baking {  "query": {    "filtered": {      "query": {        "match": {          "title": "ginger nuts"        }      },      "filter": {        "term": {           "forum_id": {            "baking"          }        }      }    }  }}
如果在多个论坛中查询,只需要指定多个routing value即可:
GET /forums/post/_search?routing=baking,cooking,recipes {  "query": {    "filtered": {      "query": {        "match": {          "title": "ginger nuts"        }      },      "filter": {        "terms": {          "forum_id": {            [ "baking", "cooking", "recipes" ]          }        }      }    }  }}
这种方式已经相当有效了,看上去有点小别扭的地方就在于在没一个查询中都指定了routing value和filter。运用alias机制,有更好的解决方案。
12:faking inde-per-user with aliases
为了让上边的方式更简洁有效,同时还得让应用程序感觉他们独立享有自己的索引(其实他们是共享多个shard的),我们采用alias来隐藏routing value和filter。
PUT /forums/_alias/baking{  "routing": "baking",  "filter": {    "term": {      "forum_id": "baking"    }  }}
现在,我们可以把baking认为是自己独享的索引了,document对baking建立索引,对baking进行查询,而自动携带了routing和filter信息。
PUT /baking/post/1 {  "forum_id": "baking",   "title":    "Easy recipe for ginger nuts",  ...}
索引的过程中forum_id这个字段还是必须的。
GET /baking/post/_search{  "query": {    "match": {      "title": "ginger nuts"    }  }}
以上查询已经很简洁了。
在多个索引中查找则对应多个alias:
GET /baking,recipes/post/_search {  "query": {    "match": {      "title": "ginger nuts"    }  }}
多个隐含的filter是OR的关系。
13:one big user
一个大论坛比得上若干小论坛。我们会逐渐发现在我们共享shard的一些小论坛中,某些小论坛逐渐变的流行起来,他们逐渐变成了大论坛。这些论坛索引数据就需要从共享中分离出来,他们开始需要单独的索引了。
对没一个论坛建立alias的机制给了我们很大的方便性去分离这些逐渐变大的论坛数据。
第一步当然是创建一个新的索引来存储大论坛数据。
PUT /baking_v1{  "settings": {    "number_of_shards": 3  }}
下一步是迁移数据,用scan and scroll api就可以把数据迁移到新索引中。迁移完毕,更新alias即可:
POST /_aliases{  "actions": [    { "remove": { "alias": "baking", "index": "forums"    }},    { "add":    { "alias": "baking", "index": "baking_v1" }}  ]update alias是原子操作,就像一个差路口,应用程序无感知。
新数据建立索引不再需要routing value和filter了,默认的_id即可。
最后一步就是删除之前的数据:
DELETE /forums/post/_query?routing=baking{  "query": {    "term": {      "forum_id": "baking"    }  }}
这种index-per-user的模式便于降低资源消耗,同时提供了不停服务良好的扩展。
14:scale is not infinite
尽管我们讨论了很多es扩展的方式,大多数都能够通过增加节点数目解决。但是,有一个资源是有限的应当谨慎对待:集群的状态。
集群状态是一个承载着集群级别信息的数据结构,例如:集群的设置,集群的节点,索引的设置、mapping、analyzed、warmers、alias,shard跟index的对应关系,跟node的对应关系等。可以用下面的指令查看集群状态:
GET /_cluster/state
集群状态信息存在于每一个节点上,包括客户端节点。因此任何一个节点都能将请求直接定位到对应节点上去,因为没一个节点都知道文档跟所在节点的对应关系。
只有master节点可以更改节点的状态。假设一个请求要求增加一个从来没有出现过的字段,则primary shard会把mapping请求转发给master node。master node将现有mapping和要求更新的mapping进行合并,然后对所有节点发布一个新状态。
Search请求应用cluster state,但是不改变cluster state。document-level的CRUD操作也不更改cluster state,除非是mapping的更新。因此cluster state是静态的而且不会是系统的瓶颈。
请记住,这个数据结构存在于没一个node的内存中,没发布一个新状态,必须更新到所有node。state越大,更新时间就越长。
我们看集群状态的常见情形是一些字段的介绍。用户可能想给每一个ip一个单独的field。请看以下例子:
POST /counters/pageview/home_page/_update{  "script": "ctx._source[referer]++",  "params": {    "referer": "http://www.foo.com/links?bar=baz"  }}
这是一个很糟糕的做法。这会导致很多的field,而这些都会存储在cluster state中!!!!每来一个新的refer,都会更新一次state,并且发布到所有节点!
一个较好的解决方案是用nested object,只用一个字段作为field name,另外一个字段用来计数:
"counters": [      { "referer": "http://www.foo.com/links?bar=baz",  "count": 2 },      { "referer": "http://www.linkbait.com/article_3", "count": 10 },      ...    ]
以上方式会增加document的数量,但是es就是专门处理这种需求的。更重要的是:会极大减小state规模。
最后,随着数据增大或者需求的增加,节点数据增加,mapping增加,索引数目增加等等,一个集群已经无法满足我们的要求了。到达了这个阶段,我们可以把实际问题分解到多个集群上了。幸亏有tribe nodes,可以把多个集群当成一个大集群来用。

0 0
原创粉丝点击