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
}