ELK专题:Day7——Elasticsearch修改索引模板并重做索引

1. 前言

来了个新需求(假装不是一个人在战斗)。

网站是运行在一个公网带宽只有4Mbps的云服务器上,偶尔我会担心这条小水管会影响到用户体验,顺便我也想知道一下这个网站一共会产生多少公网流量,所以,就有了这次需求:

  1. 监控每个HTTP请求的处理时间
  2. 统计Nginx服务产生的流量

最后好不容易完成了,成果如下:

result

2. 实现原理

2.1 找出监控对象

nginx http log module里面有介绍过,Nginx的access log里面可以添加一个参数request_time,记录每一个http请求的处理时间。

同样的原理,对于Nginx的流量,我们可以用access log里面的body_bytes_sent字段。

2.2 数据处理与存储

整个ELK的工作流程不变,我们仍然使用Logstash中的grok插件处理日志,并保存到Elasticsearch中。但是会有两个需要修改的地方:

  1. Nginx的日志配置需要修改,在我们当前的环境下,使用的nginx默认日志格式不包含request_time字段。
  2. access log的内容变化后,logstash的grok需要相应地进行调整,识别新内容。
  3. 为了满足数据的统计需求,我们需要确保在elasticsearch中的索引数据设置正确。在我们这个需求中,http请求的处理时间应该被记录为浮点型,流量数据则是整型数据。

2.3 数据可视化

继续使用Kibana的dashboard即可。

3. 操作过程

3.1 调整Nginx日志配置

  • 定义新的日志模板elk

    1
    2
    3
    4
    5
    6
    7
    8
    # /etc/nginx/nginx.conf配置示例:
    http {
    ...
    log_format elk '$remote_addr - $remote_user [$time_local] '
    '"$request" $status $body_bytes_sent $request_time '
    '"$http_referer" "$http_user_agent"';
    ...
    }
  • 在hexo网站的配置文件中,调用这个日志模板

    1
    2
    3
    ...
    access_log /var/log/nginx/hexo_access.log elk;
    ...

唠叨:改完配置文件后记得要重启Nginx服务

修改过nginx的日志配置后,nginx的access log将会变成这样:

1
14.154.28.11 - - [05/Sep/2021:17:54:44 +0800] "GET /ELK5/save_to_dashboard.gif HTTP/1.1" 200 1663444 1.366 "https://www.rondochen.com/ELK5/" "Mozilla/5.0 (Linux; Android 11; M2011K2C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Mobile Safari/537.36"

我们可以看到,1.366这个数就是我们新增的request_time内容,意思是从服务器收到这个请求,到完全把这个资源发送到浏览器这个过程,使用了1.366秒。

3.2 修改Logstash grok插件配置

在之前的文章《ELK专题:Day2——Logstash配置Nginx日志分析》中我们使用了grok去完成nginx日志的结构化分析,在Nginx的默认日志格式下,我们使用的grok pattern是:

1
%{IPORHOST:remote_addr} - %{DATA:remote_user} \[%{HTTPDATE:time_local}\] \"%{WORD:request_method} %{DATA:uri} HTTP/%{NUMBER:http_version}\" %{NUMBER:response_code} %{NUMBER:body_sent_bytes} \"%{DATA:http_referrer}\" \"%{DATA:http_user_agent}\"

我们在Nginx的access log中添加了request_time字段后,grok pattern也要相应增加一个匹配字段:

1
%{IPORHOST:remote_addr} - %{DATA:remote_user} \[%{HTTPDATE:time_local}\] \"%{WORD:request_method} %{DATA:uri} HTTP/%{NUMBER:http_version}\" %{NUMBER:response_code} %{NUMBER:body_sent_bytes} %{NUMBER:request_time} \"%{DATA:http_referrer}\" \"%{DATA:http_user_agent}\"

具体如何修改和检查,可以参考本专题Day2,和Day3的两篇文章。

3.3 Elasticsearch索引模板配置(重点难点)

在ES中,我们使用Mapping功能去规范索引中每一条数据(文档)里面的字段类型,想要修改字段类型,我们需要从Mapping入手。但是,在我们当前的方案下,Logstash输出到ES的时候,每天都自动生成一个新的索引,如果我们想要对每一个索引都自动完成Mapping设置,就需要引入索引模板功能。

下面我们来一步一步分析。

重要的事情说三遍:

操作数据库之前必须备份!

操作数据库之前必须备份!

操作数据库之前必须备份!

3.3.1 查看当前索引中的字段类型配置

我们可以在Kibana的web界面中直接查看索引中的字段配置:

check_index_mapping

也可以调用ES的API查看:

1
curl -XGET 'http://192.168.0.212:9200/logstash-nginx-log-2021.09.06/_mapping?pretty'

返回结果如下(节选):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"logstash-nginx-log-2021.09.06": {
"mappings": {
"dynamic_templates": [...],
"properties": {
...
"request_time": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"norms": false
},
...
}
}
}

我们可以看到,在这个索引中,request_time这个字段的数据类型被定义成了text

3.3.2 查看当前使用的索引模板配置

方法1:调用ES的API查看

1
curl -X GET "192.168.0.212:9200/_template/logstash?pretty"

方法2:可以在kibana的Stack Management功能中查看:

check_index_template_mapping

我们可以看到,在这个logstash的索引模板里面有一项配置index_patterns,定义了以logstash-*开头的索引都会调用这个模板:

1
2
3
4
5
6
7
{
...
"index_patterns" : [
"logstash-*"
],
...
}
点击查看完整的索引模板配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
 {
"logstash" : {
"order" : 0,
"version" : 60001,
"index_patterns" : [
"logstash-*"
],
"settings" : {
"index" : {
"lifecycle" : {
"name" : "logstash-policy",
"rollover_alias" : "logstash"
},
"number_of_shards" : "1",
"refresh_interval" : "5s"
}
},
"mappings" : {
"dynamic_templates" : [
{
"message_field" : {
"path_match" : "message",
"mapping" : {
"norms" : false,
"type" : "text"
},
"match_mapping_type" : "string"
}
},
{
"string_fields" : {
"mapping" : {
"norms" : false,
"type" : "text",
"fields" : {
"keyword" : {
"ignore_above" : 256,
"type" : "keyword"
}
}
},
"match_mapping_type" : "string",
"match" : "*"
}
}
],
"properties" : {
"remote_addr" : {
"type" : "ip"
},
"response_code" : {
"type" : "integer"
},
"@timestamp" : {
"type" : "date"
},
"geoip" : {
"dynamic" : true,
"type" : "object",
"properties" : {
"ip" : {
"type" : "ip"
},
"latitude" : {
"type" : "half_float"
},
"location" : {
"type" : "geo_point"
},
"longitude" : {
"type" : "half_float"
}
}
},
"request_time" : {
"type" : "float"
},
"@version" : {
"type" : "keyword"
},
"body_sent_bytes" : {
"type" : "integer"
}
}
},
"aliases" : { }
}
}

3.3.3 为索引模板添加配置

方法1:调用ES的API (注意:这只是一个范例,仅供参考。对数据库的操作一定要慎重)

点击展开完整命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
curl -X PUT "192.168.0.212:9200/_template/logstash?include_type_name" -H 'Content-Type: application/json' -d'
{
"version": 60001,
"order": 0,
"index_patterns": [
"logstash-*"
],
"settings": {
"index": {
"lifecycle": {
"name": "logstash-policy",
"rollover_alias": "logstash"
},
"number_of_shards": "1",
"refresh_interval": "5s"
}
},
"mappings": {
"_doc": {
"dynamic_templates": [
{
"message_field": {
"path_match": "message",
"mapping": {
"norms": false,
"type": "text"
},
"match_mapping_type": "string"
}
},
{
"string_fields": {
"mapping": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"match_mapping_type": "string",
"match": "*"
}
}
],
"properties": {
"@timestamp": {
"type": "date"
},
"@version": {
"type": "keyword"
},
"geoip": {
"dynamic": true,
"type": "object",
"properties": {
"ip": {
"type": "ip"
},
"latitude": {
"type": "half_float"
},
"location": {
"type": "geo_point"
},
"longitude": {
"type": "half_float"
}
}
},
"request_time": {
"type": "float"
},
"body_sent_bytes": {
"type": "integer"
},
"response_code": {
"type": "integer"
},
"remote_addr": {
"type": "ip"
}
}
}
}
}
'

方法2:使用kibana的web页面操作,以修改request_time为例,我们可以直接通过kibana提交操作。实际上我还修改了body_sent_bytes,remote_addr等几个之前一直被错误识别成text的字段,操作原理是相同的。

add_field_in_index_template

3.3.4 不成功的验收

在成功提交了索引模板的修改请求后,我们会很失望地发现,索引中的字段类型仍然没有变化。因为我们修改的是索引模板,并不是直接修改索引,而索引模板只会在创建索引的时候发挥作用。

在我们这个环境下,logstash是会每天创建新索引的,我们可以等到明天让新索引出来,就看到结果了。但这样又会有新旧数据类型不匹配的问题,会影响我们的数据统计。Kibana也会指出这里的问题:

Mapping Conflict:

This field is defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use this conflicting field, but it will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.

mapping_conflict

下面我们有两条路:

  1. 如果索引数据不是很重要的,我们可以直接把旧索引删除,让索引忘掉过去,重新出发。
  2. 把数据搬出来,改造一番,再重新存起来,简称reindex

3.4 重做索引(reindex)(这几乎是附加题了)

3.4.1 原理解释

简单解释,就把索引里面的文档数据复制出来,再存到另一个不重名的索引里面,在过程中会重新按照既定的规范让文档里面的各个字段以正确的方式存储起来。

reindex顺利完成后,再把旧的索引删除,避免数据重复。

3.4.2 Reindex API使用示例

我们使用ES的API操作,先拿logstash-nginx-log-2021.09.06来测试,请求内容如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST "192.168.0.212:9200/_reindex?pretty" -H 'Content-Type: application/json' -d'
{
"source": {
"index": "logstash-nginx-log-2021.09.06"
},
"dest": {
"index": "logstash-nginx-log-2021.09.06-reindex"
}
}
'

因为数据量很少,所以瞬间就完成,返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"took" : 596,
"timed_out" : false,
"total" : 813,
"updated" : 0,
"created" : 813,
"deleted" : 0,
"batches" : 1,
"version_conflicts" : 0,
"noops" : 0,
"retries" : {
"bulk" : 0,
"search" : 0
},
"throttled_millis" : 0,
"requests_per_second" : -1.0,
"throttled_until_millis" : 0,
"failures" : [ ]
}

3.4.3 批量reindex,并删除旧index

简单写一个for循环去使用curl就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh
for INDEXNAME in `curl -s "192.168.0.212:9200/_cat/indices/logstash-nginx-log*" | awk '{print $3}'`
do
cat > body.json <<EOF
{
"source": {
"index": "${INDEXNAME}"
},
"dest": {
"index": "${INDEXNAME}-reindex"
}
}
EOF
echo "generate ${INDEXNAME} reindex json body finish!"
curl -X POST "192.168.0.212:9200/_reindex?pretty" -H 'Content-Type: application/json' -d @body.json
sleep 1
echo "${INDEXNAME} reindex finish!"

# 删除旧索引,否则仍然会存在mapping conflict报错
# 在有十足把握之前,建议先不要做delete操作
curl -X DELETE "192.168.0.212:9200/${INDEXNAME}"
echo "${INDEXNAME} delete finish!"
done

3.4.4 成功的验收

经过reindex,并删除旧的索引之后,我们终于成功把包括历史数据里面的所有索引字段都规范化了,之前在kibana中看到的Mapping conflict报错已消失。request_timebody_sent_byte都被正确识别成数字,我们可以去做统计图了。

after_reindexing

3.5 Kibana创建可视化图表

3.5.1 统计网站流量

在我接触过的运维实践中,流量监控有两个维度。一个是服务器全局的网络吞吐量,一般是通过监控网卡的实时IO获得;另一个是监控具体业务在一定时间内所产生的流量总和,用来评估一个业务的热度。在我们这个日志分析案例中,显然我们的关注点是后者。

我们将会通过Nginx日志中的body_sent_byte按时间维度聚合求和,展示成一个折线图,展示流量随着时间的变化。同时,通过索引中的geoip.country_code2字段进行拆分,展示出流量的主要来源。操作步骤如下:

create_area_stacked

3.5.2 监控网站静态资源的传输时间

统计request_time的平均值随时间变化趋势,使用折线图展示:

create_line

4. 总结

想起了当初我在Day5的文章里面才提过ES中的Mapping问题,那时候粗粗看了一下文档觉得老复杂了就选择了挖坑,没想到这么快就把坑给填了。

我在前面调整ES的索引模板改得热火朝天的时候,我一直没提过,其实我们也可以在logstash的filter中使用convert功能去转换字段的类型。但就算我在logstash中把字段类型定义好了,也只能影响新的索引,为了让历史数据“改过自身”,还是逃不掉reindex的操作。同时我也不希望在一个本来就复杂的事情上再引入一个不算关键的东西,毕竟在logstash上不做convert也不影响大局,而且logstash里面的字段类型还没有es丰富。

反过来说,如果在最初搭建ELK的时候能充分考虑这些需求,就不至于走回头路折腾那么久了(那就没有循序渐进的过程了)。

从这一系列心路历程也看出来,没有通用的配置方案,有的只是根据实际场景因势利导,选择适合自己的。

真没想到两个小小的需求引起了那么大的改动,歇一歇,后面我们研究一下ELK集群的性能监控。

5. 参考文档

Index templates

Mapping

Field data types

Numeric field types

Create or update index template API

Reindex API

Mutate filter plugin - convert