前言

日志对于系统监控和排除bug来说都是及其重要的一环,为了能够方便快速地收集、分析日志,遂决定寻找一个开源方案来进行实现。一番搜索下来,在项目规模较小的情况下,从简便、易用、稳定的角度来说毫无疑问ELK是目前的最佳方案。本篇记录、介绍了如何从零开始搭建ELK系统的过程,使用的版本为6.6.0。

什么是ELK

ELK是三个开源软件的缩写,分别是Elasticsearch、Logstash、Kibana,他们都是Elastic旗下的产品,共同构成了Elastic技术栈,目前还包括了与Logstash同一层级的Beats,官方介绍:https://www.elastic.co/cn/what-is/elk-stack

Logstash是一个服务端的数据处理管道,负责从各个位置收集数据,同时可以对数据进行转换、过滤,最后进行分发;大名鼎鼎的Elasticsearch是整个系统的引擎,负责数据的存储、搜索、分析;Kibana则提供了可视化客户端,让用户能够以图表的可视化形式展现ES中的数据。

应用环境

搭建的前提是小型项目、内部项目的日志收集或者是对ELK技术栈的探索,因此优先考虑便捷、易用,将三个组件搭建在同一个服务器上,且不考虑新加入的Beats,虽然Beats的效率更高,但是整个过程下来个人觉得Logstash还是有其出色之处,没有苛刻要求的情况下,Logstash确实ok、方便。在日志的出产上,通过Logback直接提交给Logstash,诚然小规模情况下也是可以选择直接提交给Elasticsearch,但是没有现成的工具,也会失去扩展性。整个结构就很简单、简洁:

单体安装、准备

在进行初步调研、计划时,就已经准备使用Docker Compose来最终执行管理和部署,但是一个很普遍的情况就是尽管某个工具提供了傻瓜式的好方案,但是很多情况下为了更好地掌握技术又不得不从更加底层的方面进行学习、操练。So,还是先拆解开来进行逐个击破,再进行整合。

Logstash

就单体安装来说,Elastic家的安装基本上还是很方便快捷的。如果是Mac,可以使用brew install logstash一键安装,默认会安装在/usr/local/Cellar/logstash/x.x.x/目录下。如果是Linux则一般选择直接从官网下载压缩包,然后unzip解压到合适的目录下即可,当然也可以选择使用yum等包管理工具。

Logstash就好似Linux系统中的管道符|,输入、过滤、输出即是它的使命。Logstash本身的主配置文件为安装目录下的config/logstash.yml,而logstash的运行还需要一个定义如何输入过滤输出的管道配置文件,一般命名为logstash.conf,这个配置文件可以在运行时使用-f参数指定。如下是一个管道conf配置文件示例:

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
input {
tcp {
port => 5000
mode => "server"
codec => json_lines
}
}

## Add your filters / logstash plugins configuration here
filter {
if [app]{
mutate { add_field => { "[@metadata][app]" => "%{app}" } }
mutate { remove_field => "app" }
} else {
mutate { add_field => { "[@metadata][app]" => "NULL" } }
}
}

output {
elasticsearch {
hosts => "elasticsearch:9200"
index => "log-%{[@metadata][app]}-%{+YYYY.MM}"
# template => "/usr/share/logstash/pipeline/es_temp.json"
# template_name => "es_temp"
# template_overwrite => true
}
}

可见配置文件很好地说明了Logstash的任务所在,每个部分都是由各种插件构成,官方提供了很多自带的插件以供开箱即用。比如输入采用tcp插件,监听tcp端口的输入,在filter中使用了mutate插件来进行字段的增减(这里简单提一下其中的@metadata为默认的一个隐藏字段),而输出则配置了elasticsearch插件用于向Elasticsearch进行输入。配置文件中可以有各种判断循环语法,在这里不详细描述。

那么一个最基础的配置文件可以编写为:

1
2
3
4
5
6
7
8
9
10
11
12
input {
stdin {
tags => ["tags"]
codec => plain
}
}

output {
stdout {
codec => rubydebug
}
}

即从标准输入读取后输出至标准输出,使用该配置启动logstash便可以对其有一个最直接的了解。后台启动Logstash:nohup ./bin/logstash -f logstash.conf &

logback->Logstash

那么如何将应用中的日志给到Logstash呢?经过考虑后选择采用Logstash对logback的插件,如果使用maven,则在POM文件中增加:

1
2
3
4
5
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>5.0</version>
</dependency>

然后在logback配置文件中增加encoder并配置到相应的logger:

1
2
3
4
5
6
7
8
9
10
<property name="logstash.host" value="${LOGSTASH_HOST:-localhost}"/>
<property name="logstash.port" value="${LOGSTASH_PORT:-9601}"/>
<property name="logstash.app" value="${LOGSTASH_APP:-test}"/>

<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${logstash.host}:${logstash.port}</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"${logstash.app}"}</customFields>
</encoder>
</appender>

选择配置Logstash的输入为tcp,输出为stdout进行日志输出的测试,即可以了解到日志内容是如何输出到Logstash当中,Logstash默认添加了哪些内容等。可能会有发现时间戳不正常的情况,但是这里先可以放一放。

如果直接的log.error(String msg, Throwable t)形式所包含的信息不够全,可以尝试log.error(Marker marker, String msg, Throwable t);,而刚才引入的jar包有对Marker的实现类:LogstashMarker的三个子类,可以进行对应的使用。

在单元测试跑内容的时候可能会发现内容丢失以及Logstash输出connection reset by peer相关的错误,这是因为插件对内容的输出是异步的,内部采用了Disruptor框架,同时没有关闭连接的处理。因此最好在测试方法的最后进行手动阻塞,以查看完整的输出。

采用这种方式相比于监听日志文件的一个好处是不用处理日志信息换行的问题,且可以自定义输出的字段信息。

Elasticsearch

Elasticsearch的安装如果是采用包管理工具的方式基本不需要什么额外的操作,但是如果采用直接解压的形式,则需要一些额外的处理,为了快速安装,处理方式列举如下:

  • 如果要支持外部访问,需要修改相关目录下的配置文件config/elasticsearch.yml,修改增加network.host: 0.0.0.0
  • 如果出现报错OpenJDK 64-Bit Server VM warning: If the number of processors is expected to increase from one, then you should configure the number of parallel GC threads appropriately using -XX:ParallelGCThreads=N7,则需要在config/jvm.options配置文件中增加-XX:-AssumeMP
  • 如果出现报错java.lang.RuntimeException: can not run elasticsearch as root,则需要以非root角色运行。这是基于安全考虑的限制,因此新建用户和用户组来执行启动ES。
  • 如果出现报错max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144],则需要执行命令sysctl -w vm.max_map_count=262144
  • 如果出现报错max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536],则需要修改/etc/security/limit.conf,增加或修改soft nofile 65536hard nofile 65536

对于Elasticsearch,个人印象最深的是开箱即用的能力和天然支持集群的能力。对于初次接触的人来说,个人觉得需要了解数据基本的组织接口、存储的内容、交互API的形式以及基本使用、集群分片的基本概念、倒排索引和聚合的概念以及类型映射的设置和模板的使用。这些内容了解完毕后即可开始使用,毕竟不是深度应用,也可以选择先把Kibana启动起来,利用可视化工具进行更加便捷的操作练习。

Elasticsearch在本次实践中最重要的部分就是属性映射和模板的定义,这部分在Logstash连接Elasticsearch的部分进行描述。后台启动Elasticsearch:./bin/elasticsearch -d

Kibana

Kibana主要是可视化的Web展现,在简单应用的前提下需要配置的内容同样不多,也是开箱即用。使用解压缩直接安装时,修改config/kibana.yml,调整server.host: "0.0.0.0"保持外部可访问,端口配置项为server.port默认5601,ES地址默认为http://localhost:9200,如果前面启动ES没有额外调整的话也不需要更改。启动之后访问服务器的5601端口就可以看到Kibana的界面,kibana本身使用node,因此top命令中没有名为kibana的进程,而是node。

进入Kibana后由于当前没有数据,可以先在DevTools中进行Elasticsearch相关的交互练习。后台启动Kibana:nohup ./bin/kibana &

Logstash->Elasticsearch

日志信息到达Logstash之后就应该输出到Elasticsearch中,在这种情况下其实上文给出的Logstash管道配置示例已经和最后使用的配置基本上没差多少了。在本次应用过程中,并没有用到很多过滤操作,在Filter这块主要是对隐藏字段的处理,将APP名置入隐藏字段然后将APP字段删除,最后利用字段来区分输入到Elasticsearch中的具体索引,达到区分处理的效果。

在测试阶段可以先不对输入进行任何处理,直接输出到Elasticsearch中观察输入内容具体存储的情况,然后针对性地进行模板的定制。Logstash可以配置Elasticsearch的模板,配置的效果是在Logstash连接到ES时会将模板存储到ES中,因此模板中匹配的索引名称要和配置的索引名称相匹配才能有效果。经过定制后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
input {
tcp {
port => 5000
mode => "server"
codec => json_lines
}
}

filter {
if [app]{
mutate { add_field => { "[@metadata][app]" => "%{app}" } }
mutate { remove_field => "app" }
} else {
mutate { add_field => { "[@metadata][app]" => "null" } }
}
}

output {
elasticsearch {
hosts => "elasticsearch:9200"
index => "log-%{[@metadata][app]}-%{+YYYY.MM}"
template => "/es_temp.json"
template_name => "es_temp"
template_overwrite => true
}
}

模板文件配置示例:

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
{
"template": "log-*",
"order": 10,
"settings": {
"index":{
"refresh_interval":"10s"
}
},
"mappings": {
"doc": {
"dynamic": false,
"properties": {
"@timestamp": {
"type": "date"
},
"@version": {
"type": "keyword"
},
"description": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"ip": {
"type": "ip"
},
"level": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"level_value": {
"type": "long"
},
"logger_name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"stack_trace": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"time": {
"type": "long"
},

....

}
}
}
}

对存储内容进行完整的限定。

Kibana->Elasticsearch

Kibana需要配置指定Elasticsearch的REST API地址,除此之外并没有其他强制的配置,因此这部分没有什么特别需要注意的。在使用的定制过程中,Kibana会让使用者选择要进行显示的索引(支持通配符),因此在创建索引的时候需要注意名称的统一性。使用Kibana最需要注意的一点是右上角的时间选项,在进行可视化展示的时候会默认进行时间条件筛选。在测试过程中可以发现,Kibana对时间的处理还是比较智能的,会自动转换成对应的时区。

Docker Compose管理

由于选择在同一台服务器上进行单体部署操作,因此Docker Compose是一个非常好的选择,可以很容易实现统一配置、统一快速部署。这部分需要一些基本的Docker和Docker Compose的基础知识,如果没有则先行了解一下即可。

同时很方便的是,Github上已经有一个比较好的开源配置,因此可以选择直接clone一份,拷贝到服务器中,然后在此基础之上进行定制。

配置文件

这个开源的配置即支持docker compose也支持docker swarm,本例中使用docker compose,因此只需要更改其中的docker-compose.yml。在使用之前推荐阅读一下项目的说明文档,截止当前该文档推荐使用的Docker版本为17.05+,Docker Compose的版本为1.6.0+,但是应用的服务器上Docker和Docker Compose的版本比较低,因此有些地方需要额外处理。

  • 组件版本的控制:版本号位于目录下的隐藏文件.env,ELK都是Elastic技术栈的一部分,它们的版本一般是一致的,因此集中配置于此。但是如果Docker版本不够高,是会有报错的情况,这样就需要修改各个组件的Dockerfile,直接设置版本,而不是使用参数。例如kibana/Dockerfile
1
2
3
4
5
6
7
8
9
10
11
#注释掉这个参数
#ARG ELK_VERSION

# https://github.com/elastic/kibana-docker
#FROM docker.elastic.co/kibana/kibana:${ELK_VERSION
#直接填写对应的版本
FROM docker.elastic.co/kibana/kibana:6.6.0

# Add your kibana plugins setup here
# Example: RUN kibana-plugin install <name|url>

其他两个组件也是一样,另外在docker-compose.yml中注释掉args相关内容即可。

  • JVM配置:Logstash和Elasticsearch可以配置相关的JVM参数,在配置文件中已经给出,修改相关值即可
  • 数据卷配置:说明文档中有给出如何持久化ES的数据,毕竟不能每次启动都清空数据。依靠给容器添加数据卷实现,指定主机和容器的映射。这里需要注意的是由于Elasticsearch的安全要求,指定的主机文件权限需要把owner和group都设为1000。
  • 插件安装:根据说明文档,插件的安装通过配置Dockerfile的RUN命令来实现。这里在安装分词插件时会有一个坑,在下一小节说明。
  • 各个组件的具体配置:查看这个docker-compose文件就能发现所有的配置都是通过数据卷映射到容器的相关目录,因此修改相关的yml文件即是对组件的配置。需要注意的是Logstash的管道配置,在进行映射后,指定的目录下就只能包含管道配置文件而不能有其他任何文件,因为Logstash会把该目录下的所有配置进行合并,如果存在其他文件就会发生意想不到的错误。另外,配置的索引模板json文件也需要配置映射到容器中进行使用。
  • 其他:还包括端口映射和网络连接,端口映射按需配置,网络连接在下面的小节中详述。

集成Nginx

集成Nginx是因为需要给Kibana一个简单的安全控制。Kibana本身并没有安全控制,需要使用X-Pack扩展,从6.4版本开始X-Pack已经成为Elastic相关组件默认安装的扩展,不需要额外安装,但是使用X-Pack需要license,尽管可以有30天的试用但它并不是免费的。因此在安全要求不高的情况下,套一个Nginx设置用户名密码登陆来保证基本的安全。

在目录下创建名为nginx的目录,就如同其他组件,然后创建子目录config和log。文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
your-elk/
├── elasticsearch/
│ └── config/
│ └── data/
│ └── Dockerfile
│ └── install-plugin.sh
├── kibana/
│ └── config/
│ └── Dockerfile
├── logstash/
│ └── config/
│ └── pipline/
│ └── Dockerfile
│ └── es_temp.json
├── nginx/
│ └── config/
│ └── nginx.conf
│ └── passwd
│ └── log/
├── ...

在docker-compose.yml中添加:

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
version: '2'

services:

elasticsearch:
build:
context: elasticsearch/
# args:
# ELK_VERSION: $ELK_VERSION

# ....

kibana:
build:
context: kibana/
# args:
# ELK_VERSION: $ELK_VERSION
volumes:
- ./kibana/config/:/usr/share/kibana/config:ro
# ports:
# - "5601:5601"
networks:
- elk
depends_on:
- elasticsearch

nginx:
image: docker.io/nginx:latest
ports:
- "5601:80"
volumes:
- ./nginx/config/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/config/passwd:/etc/nginx/passwd:ro
- ./nginx/log:/var/log/nginx
networks:
- elk
depends_on:
- kibana

对于nginx如何配置用户名密码,百度一下即可。设置完成后,将nginx日志目录和生成的passwd文件也配置映射到容器中。同时既然使用了nginx,那么kibana的端口也就没有必要暴露出来了。

分词插件集成

如果存储的内容使用已有的分词器无法达到比较好的效果或者包含大量中文,那么还需要安装中文分词插件。ELK对于插件的安装是非常简单的,基本上都是一个形如xxx-plugin install ...的命令就可以搞定,但是在docker环境下会有那么一点问题。

中文分词插件目前常用的应该有两款,ikansj,两款都挺不错的,择一使用即可。插件安装命令支持网络文件和本地文件,为了每次创建容器不重复下载,建议将插件包下载到本地,以本地文件的形式安装elasticsearch-plugin install file://path/xxx.zip。在单体应用直接安装的时候可能会发现形如WARNING: plugin requires additional permissions的安全警告,需要键入y来确认忽略,这就是发生问题的原因所在。插件的安装在本例中是通过Dockerfile中的RUN命令来执行的,相当于自动在容器中执行命令,那么在构建容器的过程中,容器由于无法获取标准输入导致构建失败。

这个问题还是比较恶心的,所幸有搜索到一个解决方案,适用于执行命令时无法读取标准输入的情况。编写一个shell脚本,将要执行的命令写在脚本中,然后以如下形式执行脚本:

1
RUN sh -c '/bin/echo -e "input" | sh /yourShell.sh'

在执行安装插件时,input处就可以填写“y\n”,插件就可以被正确安装。

网络连接

Docker有提供多种网络模式来进行容器之间的通信,在主机和主机之间通过端口映射即可完成通信,但是同一主机内的容器通过映射出来的端口来相互访问是有限制的。docker-compose文件中的各个service容器均可以通过别名来互相访问,所以查看kibana的配置文件可以发现填写的es地址就直接是http://elasticsearch:9200,Docker也容许配置link选项来将不同的容器进行打通,这些都是由Docker的虚拟网桥来完成,每个docker-compose都会默认创建一个名为根文件名称_default的网络,而Docker启动时也会默认启动一个名为bridge网络。

Docker的网络在这里不进行详述,提到这一点的原因是同一个服务器上也有一个使用Docker Compose构建的应用需要将日志发送到ELK中,这样就产生了容器互访的问题。无法直接通过端口互访,那么就必须要将在不同的docker-compose.yml中配置的容器打通,解决的办法是直接创建一个桥接网络,然后将需要通信的容器都加入到这个网络中,通过别名互访。

创建网络:docker network create 【网络名】,默认网络类型就是bridge。更改docker-compose.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: '2'
services:

# ...

logstash:
# ...
networks:
- elk
- localconnect
container_name: logstash
depends_on:
- elasticsearch

# ...

networks:
elk:
driver: bridge
localconnect:
external: true

同时指定logstash的容器名称,这样配置过后,就可以在另外的docker-compose.yml中使用external_link(旧版本)或link来进行访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "2"
services:

# ...

java:
ports:
- "8081:8081"
- "5005:5005"
external_links:
- logstash:logstash
networks:
- localconnect

# ...

networks:
localconnect:
external: true

小结

使用下来,ELK的方案确实是相当的便捷,OOTB程度很高。不需要花多少时间就可以把架子搭起来,花时间的地方主要是对于存储内容的熟悉和配置,了解配置如何生效。最后的效果看起来还不错,很好地加强了对应用的整体认知和监控。效果示例如下:

当然,整个应用没有深挖,毕竟是个单体应用,信息来源也比较单一。ELK的可扩展性可以很强,Elasticsearch天然支持集群的能力有目共睹,确实牛逼。ES上集群、套上kafka等消息队列、引入Beats,整个结构就可以很健壮。