欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

ES 集群规划和集群调整策略

最编程 2024-03-06 20:41:26
...

1、集群规划

1.1 集群规模

搭建一个ES集群我们需要考虑以下两方面:

  1.  当前数据量有多大? 数据增长情况如何? 
  2. 机器配置如何? CPU、内存、硬盘等

集群大小设置的依据:

ES JVM heap 最大可以设置32G

30G heap大概能处理10T 的数据量, 如果内存很大 如128G, 可以在一台机器上运行多个ES节点。注意: 集群规划满足当前数据规模+ 适量增加规模即可, 后续可以按需扩展

两类应用场景:

  1. 用于构建业务搜索功能模块, 且多是垂直领域的搜索。数据量级几千万到数十亿级别, 一般2-4台机器规模
  2. 用于大规模数据的实时OLAP(联机处理分析),景点的如ELK Stack, 数据规模可能达到千亿或更多。几十到上百节点的规模

1.2  集群节点的角色分配

节点角色:

  • master : node.master:true 节点作为主节点
  • DataNode: node.data:true 默认是数据节点
  • Coordinate node: 协调节点, 一个节点只接受、转发请求到其他节点, 汇总各节点返回数据等功能, 上面两个配置都为false的节点即为协调节点

一个节点可以充当多个角色, 默认情况下三个角色都有。

节点角色的分配:

  1. 小规模集群, 不需严格区分
  2. 重大规模集群(10个节点以上),应考虑单独的角色充当, 特别是并发查询量大,查询的合并量大,可以增加独立的协调节点。角色分开的好处是分工明确,互不影响, 不会因为协调角色而导致负载过高影响数据节点的能力

1.3 避免数据脏写(脑裂)

        一个集群只有一个主节点A, 如果这时因为A节点需要处理的需求太多或网络拥堵导致从节点ping不通A节点, 这时集群会重新选择一个新的主节点B。过了一会A节点恢复了, 这时就出现了两个主节点, 这时就会出现一部分数据来源于A, 一部分数据来源于B, 从而出现数据不一致的问题。这种情况也叫脑裂。

6.x和之前的版本, 要尽量避免脑裂需要添加最小数量的主节点配置: discovery.zen.minimum_master_nodes , 参数值一般设置为 : (有master资格节点数/2)+ 1 ,这个参数控制的事选举主节点时需要看到最少有多少具有master资格的活节点才能进行选举。官方推荐 N/2 + 1, 其中N是具有master资格的节点数量

在7.x版本的ES中, 对ES集群发现系统做了调整, 不再有discovery.zen 开头的参数设置, 集群自主控制主节点状况。 且在新版本中启动了一个新的集群时需要有cluster.initial_master_nodes 初始化集群列表。

ES7中discovery.zen 配置失效, 新配置为: 

中大规模集群中常用的配置: 

  1. Master 和 dataNode  角色分离, 配置奇数个master
  2. 单播发现机制, 配置master资格节点(5.0之前): discovery.zen.ping.multicast.enable:false , 关闭多播发现机制, 默认即为挂壁
  3. 延长ping  master的等待时长: discovery.zen.ping_timeout: 30 , 默认为3s, 其他节点ping主节点多长时间没有响应就认为主节点不可用, 在7.x版本中,配置换位: discovery.request_peers_timeout

1.4 索引分片数和副本数设置

索引的分片数在创建索引的时候指定, 一旦创建不可修改。

分片设置的参考原则:

  • ES推荐的JVM对空间为30~32G, 所以把你的分片最大容量限制为30G, 然后再对分片数量做合理估算, 如你的数据量能达到200G, 可以最多分配7~8个分片。
  • 在开始阶段一个好的方案是根据你的节点数按照1.5~3倍的原则来创建分片, 如你有3个节点,则推荐你创建的分片数不超过9个, 当性能下降时,增加节点,ES会平衡分片放置。
  • 对于基于日期的索引需求, 且对索引数据的搜索场景非常少, 也许这些索引量很大, 且每个索引的数据量不大于1G , 对于这类场景, 建议只需要为索引分配1个分片, 如日志管理就是一个基于日期的索引需求, 日期索引会很多,但每个索引存方的日志数据量很少。

分片设置副本: 

为保证高可用, 副本数量设置为2即可, 要求集群至少要有3个节点来分开存放主分片、副本。 如发现并发量大时, 查询性能会下降,可增加副本数,来提升并发查询能力。

注意: 新增副本时主节点会自动协调, 然后拷贝数据到新增的副本中,副本数可以随时调整。

PUT /my_index_name/_settings
{
    "number_of_replicas": 2
}

2、分布式集群调优策略

这里我们主要从index写调优和Search读两个方面进行调优

2.1 Index 写调优

1、副本数置0

如果是集群首次灌入数据, 可以将副本数职位0,写入完毕后再调整回去, 这样副本分片只需要拷贝数据, 节省了索引过程。

2、自动生成doc ID

通过ES写入流程可以看出,如果写入doc是指定了id, 则ES会先尝试读取原doc的版本号以判断是否需要更新。这会涉及一次读取磁盘的操作, 但自动生成doc ID可避免该操作。

3、合理设置mappings

  • 将不需要简历索引的字段index属性设置为no或not_analyzed。 最对字段不分词或不索引, 可以减少很多运算操作。尤其是binary类型, 默认情况下占用CPU很高, 且这类分词一般意义不大
  • 减少字段内容长度, 如果原始数据的大段内容无需全部建立索引, 可以尽量减少不必要的内容
  • 使用不同的分析器(analyzer), 不听的分析器在索引过程中原酸复杂度也有较大的差异

4、调整_source字段

source字段用来存储doc原始数据, 对不部分不需要存储的字段 ,可以通过includes 、excludes过滤, 或将source禁用, 一般用于索引和数据分离, 这样可以降低I/O 压力, 不过实际场景中大多不会禁用_source

5、对analyzed字段禁用norms

norms用于在搜索时计算doc的平分, 如果不需要平分, 可以将其禁用

 "title": {
   "type": "string",
   "norms": {
     "enabled": false
   }

6、调整索引的刷新间隔

该参数却省是1s, 强制ES每秒谁信一个segment, 从而保证新写入的数据近实时的可见, 可被搜索到。 如果将该参数刷新的时间调高, 降低刷新次数,减少资源消耗, 但也牺牲了ES的实时性。

PUT /my_index/_settings
{
 "index" : {
      "refresh_interval": "30s"
    }
}

7、批处理

批处理把多个index操作请求合并到一个batch中处理。但每次批处理多少个doc文件效率更高,受很多因素影响, 如doc的大小,字段类型、分词器等, ES官网建议一个node、一个shard做性能基准测试来确定最优值。

8、document的路由处理

当对一批中的docs进行index操作时, 该批index操作所需的线程的个数由要写入的shard的个数决定。如下图:

图中, 有2批docs写入ES, 每次需要写入4个shard, 所以共需要8个线程, 如果能减少shard数, 那么消耗的线程数也会减少。 如果两批中每批的shard个数都只有2个, 总共线程消耗个数4个, 减少一半, 如下图: 

默认的routing就是id, 也可以在发送请求时,手动指定一个routing value, 如:

 put /index/doc/id?routing=user_id

注意: 线程数降低,但单批的处理耗时可能增加, 和提高刷新时间间隔类似,有可能会牺牲数据的实时性。

2.2 search读优化

存储在ES中的数据超过10亿条时, 需要对其进行优化

1、数据分组

ES经常用来存储日志, 日志的索引管理方式一般基于日期的,如基于年、月、日、周等建立索引:

当搜索单天的数据, 只需要查询一个索引的shard就可以, 当需要查询多天的数据时, 需要查询多个索引的shards。 这种方案类似于数据库的分库分表、分区查询方案, 小范围数据查询。

开始的方案是建一个index, 当数据量增大时, 就扩容增加index的shard数, 当shard增大时, 要搜索的shards数也会随之上升。 基于数据分组的思路, 可以基于client进行数据分组, 每个client只需要依赖自己的index下的shard进行搜索, 而不是所有的shards, 从而提高性能。

2、使用filter 替代query

在搜索时使用query, 需要为doc的相关度打分, 使用filter, 没有打分环节处理,做的事情更少, 而且filter理论上更快一些。 如果使用上不需要打分, 直接使用filter , 如果需要打分,建议使用bool查询, bool方式可以吧打分查询和不打分查询组合到一起, 如: 

GET /_search
{
  "query": {
    "bool": {
      "must": {
        "term": {
          "user": "kimchy"
        }
      },
      "filter": {
        "term": {
          "tag": "tech"
        }
      }
    }
  }
}

3、 ID字段定义为keywork

一般情况下, 如果字段不会被用作Range类型搜索字段, 都可以定义成keyword类型。 因为keyword会被优化,以便进行terms查询。 Integer等数字类的mapping类型, 会被优化来进行range类型搜索。

将Integer改成keyword类型后, 搜索性能可以提高30%

3、deep Paging 性能优化

3.1 深度分页性能问题

ES默认分页方式是from + size的形式, 类似于mysql的limit。 当请求数据量较大时, ES会对分页做出限制,性能消耗太大。 如一个索引分10个shards, 然后一个分页请求 from=1000, size=10, 这时会带来很严重的性能问题:cpu、 内存、IO和网络贷款等。 在query阶段, 每个shard需要返回1000条数据给coordinating node, 而coordinate node需要接收10*1000条数据, 即使每条数据只有_doc_id 和 _score , 这个数量也是很大的, 且只是针对一个查询的情况。

ES中有个配置: index.max_result_window, 默认是10000条数据, 如果分页的数据超过10000, 就拒绝返回结果。

3.2 深度分页的解决方案

1、利用scroll遍历方式

scroll分为初始化和遍历两步, 初始化时将所有符合搜索条件的搜索结果缓存起来,可以理解为快照。 在遍历是, 从这个快照中取数据, 也就是说在初始化后,对索引插入、删除、更新数据都不会影响遍历结果。 因此, scroll不适用于实时性要求较高的搜索, 更使用与后台批处理任务, 如群发等

(1)、初始化

post /student/_search?scroll=1m&size=2
{
    "query":{"match_all":{}}
}

后面跟的两个参数: scroll 代表缓存暂存时间, 其他的和普通search请求相同

执行完命令后会返回一个_scroll_id, 用来下次去数据的时候使用

(2)遍历查询

get /_search/scroll
{
    "scroll":"1m",
    "scroll_id":"上面初始化时返回的_scroll_id"
}

这里的scroll_id是scroll初始化的唯一标识, 它可能是上一次遍历取回的_scroll_id或者是初始化返回的_scroll_id, 两个值应该是一样的。 _这里的scroll参数,是为了刷新搜索结果的缓存时间。

2、search after 方式

search after 分页是根据上一页的最后一条数据来确定下一页的位置, 同时在分页请求过程中, 如果索引的数据有增删改操作, 这些变更也会实时的反映到游标上。为了找到每一页最后一条数据, 每个文档的排序字段必须有一个全局唯一值, 一般使用_id.

get /student/_search
{
    "query":{
        "match_all":{}
    }
    "size":2,
    "sort":[
        {
            "_id":"desc" # 指定排序字段和排序方式
        }
    ]
}


# 使用search_after 查询下一页
get /student/_search
{
    "query":{
        "match_all":{}
    }
    "size":2,
    "search_after":[3],  # 从_id=3 的地方开始查询下一页数据
    "sort":[
        {
            "_id":"desc" # 指定排序字段和排序方式, 与上面保持一直
        }
    ]
}

3、三种分页方式比较

分页方式 性能 优点 缺点 场景
from + size 灵活性好,实现简单 深度分页性能差 数据量小, 且能容忍深度分页带来的性能问题
scroll 解决了深度分页问题 数据的实时性差,维护成本高,需要维护一个scroll_id 海量数据的导出、 需要查询海量结果集的数据
search_after 性能高, 不存在深度分页问题, 能够反应数据实时性 实现连续分页的实现会比较复杂, 每次查询都需要上次查询的结果 海量数据分页