4.ElasticSearch系列之分布式特性及分布式搜索机制(一)

1. 分布式特性

  • ElasticSearch的分布式架构带来的好处
    • 存储的水平扩容,支持PB级数据
    • 提高系统可用性,部分节点停止服务,整个集群服务不受影响

2. 脑裂问题

  • Split-Brain 当出现网络问题,一个节点和其他节点无法连接,在各自的网络环境下,选举为各自主节点

如何避免脑裂问题?

  • 限定一个选举条件,设置quorum(仲裁),只有当Master eligible节点数大于quorum时才能进行选举
    • quorum=(master节点总数/2)+1,即大多数
  • 从7.0开始,无需这个配置

3. 分片与集群的故障转移

  • 主分片Primary Shard—提升系统存储容量
  • 副本分片Replica Shard—提高数据可用性
  • 集群状态get _cluster/_health
    • Green健康:主分片与副本分片都可用
    • Yellow亚健康:主分片都可用,副本分片部分不可用,如1个节点,分配了3个主分片,1个副本分片则无法分配,因为副本分片必须与主分片不在同一节点
    • Red不健康:部分主分片不可用
  • 故障转移
    • 对于3个主分片与1个副本分片,分布在3个节点上node1: P1 R0 node2: P2 R1 node3: P0 R2,当node1节点出现故障后,则可故障转移node2:P2 R1 R0 node3: P0 R2 P1

4. 文档分布式存储

4.1 文档存储在分片上

  • 文档会存储在具体的某个主分片和副本分片上,例如文档1,存储在P0和R0上

  • 文档到分片的映射算法

    • 确保文档均匀分布在所有分片上,充分利用硬件资源,避免部分繁忙部分空闲
    • 潜在的算法
      • 随机 / Round Robin.当查询文档1,分片数很多,需要多次查询才可能查询到它
      • 维护文档到分片的映射关系,当文档数据量很大时,维护成本高
      • 实时计算,通过文档1,自动算出,需要去哪个分片上获取文档
  • 文档到分片的路由算法
    shard=hash(_routing)%numbers_of_primary_shards

    • hash算法确保文档均匀分布到分片中
    • 默认的_routing值是文档id
    • 设置Index Settings后,这就是Primary主分片数不能随意修改的原因
    • 可以自行定制routing数值,如相同国家商品分配到指定shard
PUT order/_doc/1?routing=china
{
   ...
}

5. 分页FROM SIZE

GET kibana_sample_data_ecommerce/_search
{
  "from": 10,
  "size": 20,
  "query": {
    "match_all": {}
  }
}

6 分布式系统中深度分页问题及解决方法

6.2.1 深度分页问题

  • ES天生就是分布式的。查询信息,但是数据分别保存在多个分片上,多台机器上,ES天生就需要满足排序的需要
  • 当一个查询:From=990,Size=10
    • 会在每个分片上先获取1000个文档,然后coordinating node聚合所有结果。最后在通过排序选取前1000个文档
  • 页数越深,占用内存越多。为了避免深度分页带来的内存开销。ES有一个设定,默认限制到10000个文档
    • index.max_result_window

6.2.2 Search After避免深度分页问题

  • 避免深度分页的性能问题,可以实时获取下一页文档信息
    • 不支持指定页数(FROM)
    • 只能往下翻
  • 第一步搜索需要指定sort,并且保证值是唯一的
  • 然后使用上一次,最后一个文档的sort值进行查询
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":11}
POST users/_doc
{"name":"user2","age":12}
POST users/_doc
{"name":"user2","age":13}

POST users/_search
{
    "size": 1,
    "query": {
        "match_all": {}
    },
    "sort": [
        {"age": "desc"} ,
        {"_id": "asc"}    
    ]
}
POST users/_search
{
    "size": 1,
    "query": {
        "match_all": {}
    },
    "search_after":
        [
          13,
          "v96NA4MB3bUwyhQRSg2k"
        ],
    "sort": [
        {"age": "desc"} ,
        {"_id": "asc"}    
    ]
}

6.2.3 Search After如何解决深度分页问题

  • 假定size是10
  • 当查询990-1000
  • 通过唯一排序值定位,将每次将处理的文档数都控制在10

7. Scorll长查询导出所有文档

# 查询过期时间设置为5分钟,会返回scrollID
POST /users/_search?scroll=5m
{
    "size": 1,
    "query": {
        "match_all" : {
        }
    }
}
# 将之前返回的scrollID传入,继续查询
POST _search/scroll
{
    "scroll" : "1m",
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFnVWcVRUUl9iUU15ekoxNklBM3RTemcAAAAAAAAIyxZvOGMtUkZRZVRpLUR0QV82VW9NZXV3"
}

JAVA版本scroll查询

@Component
public class EsUtils implements ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger(EsUtils.class);

    private static RestHighLevelClient restHighLevelClient;

    /**
     * 滚动方式查询大批量es数据
     *
     * @param index 索引
     * @param boolQuery 查询语句
     * @param clazz 要返回的实体类型
     * @param <T>
     * @return
     */
    public static <T> List<T> scrollQuery(String index, QueryBuilder boolQuery, Class<T> clazz) {
        List<T> result = new ArrayList<>();
        // 存活时间,当索引数据量特别大时,出现超时可能性大,此值适当调大
        Scroll scroll = new Scroll(TimeValue.timeValueMinutes(30L));
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(boolQuery);
        searchSourceBuilder.size(10000);
        SearchRequest searchRequest = new SearchRequest().indices(index).scroll(scroll).source(searchSourceBuilder);
        SearchResponse searchResponse = null;
        try {
            searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            logger.error("查询es数据异常:{}", e);
        }
        String scrollId = searchResponse.getScrollId();
        SearchHit[] searchHits = searchResponse.getHits().getHits();
        if (searchHits != null) {
            for (SearchHit searchHit : searchHits) {
                T t = JSONObject.parseObject(searchHit.getSourceAsString(), clazz);
                result.add(t);
            }
        }
        // 遍历搜索命中的数据,直到没有数据
        while (searchHits != null && searchHits.length > 0) {
            SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
            scrollRequest.scroll(scroll);
            try {
                searchResponse = restHighLevelClient.scroll(scrollRequest, RequestOptions.DEFAULT);
            } catch (IOException e) {
                logger.error("查询es数据异常:{}", e);
            }
            scrollId = searchResponse.getScrollId();
            searchHits = searchResponse.getHits().getHits();
            if (searchHits != null && searchHits.length > 0) {
                for (SearchHit searchHit : searchHits) {
                    T t = JSONObject.parseObject(searchHit.getSourceAsString(), clazz);
                    result.add(t);
                }
            }
        }
        // clean scroll
        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(scrollId);
        ClearScrollResponse clearScrollResponse = null;
        try {
            clearScrollResponse = restHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            logger.error("清除scroll异常:{}", e);
        }
        boolean succeeded = clearScrollResponse.isSucceeded();
        if (succeeded) {
            return result;
        }
        return null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        restHighLevelClient = applicationContext.getBean(RestHighLevelClient.class);
    }
}

8. ES的乐观并发控制

  • ES的文档是不可变更的。如果更新一个文档,会将文档标记为删除,同时增加一个全新的文档,同时文档的version字段加1
  • 内部版本控制
    • if_seq_no + if_primary_term
  • 使用外部版本(其他数据库作为主要数据存储)
    • version + version_type=external

代码示例:

PUT products
PUT products/_doc/1
{
  "title":"iphone",
  "count":100
}
GET products/_doc/1
PUT products/_doc/1?if_seq_no=0&if_primary_term=1
{
  "title":"iphone",
  "count":100
}
PUT products/_doc/1?version=30000&version_type=external
{
  "title":"iphone",
  "count":100
}

欢迎关注公众号算法小生沈健的技术博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

算法小生Đ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值