vlambda博客
学习文章列表

搭建自己的微服务日志监控平台

1、目标

实际生产环境中分布式微服务架构十分常见,传统部署日志的方式需要我们到不同的服务器中去查看日志文件,或者将日志存储到数据库中,但是当日志量大到一定程度时,数据库的方式就会捉襟见肘。本期我们就来讲解如何通过ELK来搭建分布式日志监控平台,本期内容纯干货,以实操、快速搭建为主进行讲解,建议大家先收藏,后续跟着内容一步步实操

2、思路

首先我们要部署的架构如下图所示,需要收集两个微服务的日志,并且最终在kibana中可视化呈现出来。

可以看出日志的传输路线是:微服务产生日志,并将日志数据保存到磁盘中的.log文件中,filebeat监听log文件,将其数据收集并结构化后传输到logstash上,logstash将日志进行过滤收集,再传输到elasticsearch上,elasticsearch把日志作为索引进行存储并且构造对应倒排索引,kibana可视化呈现日志,需要查询时kibana调用elasticsearch进行日志数据的查询

在微服务节点上,我们通过更加轻量级的filebeat来收集日志,然后将日志传输给logstash

当然可以直接将日志传输给ES,那么这里为什么还要在中间加一层logstash呢?(1)当需要收集的节点较多时,传输的log量和次数就会大量增加,如果filebeat直接传输给es,就会占用掉es的大量资源。应该让es专注与数据查询和处理。让数据发给logstash,以此作一层缓冲。(2)logstash有多种过滤器可以使用,通过logstash做一些过滤,过滤掉无效的日志


3、下载

环境采用jdk1.8,es7.13.0,kibana7.13.0,filebeat7.13.0

需要注意的是下载es,kibana,beats需要保持版本一致

3.1 filebeat下载

https://www.elastic.co/cn/downloads/past-releases/filebeat-7-13-0 选择linux64位版本

搭建自己的微服务日志监控平台


其他beats下载 https://www.elastic.co/cn/downloads/beats/

3.2 elasticsearch下载

https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-13-0

3.3 kibana下载

https://www.elastic.co/cn/downloads/past-releases/kibana-7-13-0

3.4 环境

服务器 服务
172.16.188.7 elasticsearch
172.16.188.13 kibana
172.16.188.6 logstash
172.16.188.2 filebeat + 微服务1
172.16.188.3 filebeat + 微服务2

4、部署ELK

4.1 部署微服务

搭建自己的微服务日志监控平台

我这里为了mac本地测试方便使用的日志路径是/Users/wuhanxue/Downloads


# 应用名称spring.application.name=order-service# 应用服务 WEB 访问端口server.port=8080logging.file.path=/Users/wuhanxue/Downloads 

2、maven package打包两个服务,并且分别上传到两个服务器节点上(提前安装好jdk环境)

3、启动两个微服务

java -jar user-service-0.0.1-SNAPSHOT.jarjava -jar order-service-0.0.1-SNAPSHOT.jar


搭建自己的微服务日志监控平台


4.2 部署elasticsearch

如果知道如何部署elasticsearch的可以跳过本节 1、将安装文件传输到服务器上

2、解压es文件

tar -zxvf elasticsearch-7.13.0-linux-x86_64.tar.gz

3、修改配置文件

# 集群名称cluster.name: cluster1# 初始主节点cluster.initial_master_nodes: ["node-1"]# 节点名node.name: node-1# 是否可选为主节点node.roles: [master,data,remote_cluster_client]# data文件夹,提前创建好path.data: /var/local/elasticsearch_data# 日志文件夹,提前创建好path.logs: /var/local/elasticsearch_logs# 对方暴露的ip地址network.host: 172.16.188.7# 允许跨域访问,head访问时需要开启http.cors.enabled: truehttp.cors.allow-origin: "*"

4、因为es不允许以root账号启动,所以需要提前创建一个其他账号,我这里已经创建了elastic用户

5、将es安装目录权限赋给elastic账号

chown -R elastic:elastic elasticsearch-7.13.0

6、以elastic账户启动

# es安装目录下执行./bin/elasticsearch

4.2.1 内存过小问题

自己测试的时候,可能会因为服务器内存过小而导致启动报错,这个时候需要修改两个东西 1、增加最大用户打开文件数

# root执行指令vim /etc/security/limits.conf
# 文件最后添加# * 表示所有用户, * soft nproc 65536* hard nproc 65536* soft nofile 65536* hard nofile 65536root soft nproc 65536root hard nproc 65536root soft nofile 65536root hard nofile 65536
# 保存后重启reboot# 查看当前值ulimit -Hn

2、增加vm.max_map_count

vim /etc/sysctl.conf# 最后添加vm.max_map_count=655360
# 查看sysctl -p

4.2.2 端口开放问题

需要打开9200,9300端口

# 查看指定端口是否已经开放firewall-cmd --query-port=9200/tcpfirewall-cmd --query-port=9300/tcp# 开放指定端口firewall-cmd --add-port=9200/tcp --permanentfirewall-cmd --add-port=9300/tcp --permanent# 重新载入添加的端口firewall-cmd --reload

4.2.3 测试

访问http://ip:9200/,出现以下页面则成功

搭建自己的微服务日志监控平台


4.3 部署kibana

如果知道如何部署kibana的可以跳过本节 1、上传安装包到服务器

2、解压安装包

tar -zxvf kibana-7.13.0-linux-x86_64.tar.gz

3、修改配置文件

vim config/kibana.yml

内容如下

# 默认端口为5601,我这里因为开启了两个所以修改为了5602server.port: 5602server.name: kibana2server.host: "0"elasticsearch.hosts: [ "http://172.16.188.7:9200"]xpack.monitoring.ui.container.elasticsearch.enabled: true

4、同样kibana也是不允许用root账号启动了,创建一个elastic账号,并且将kibana安装目录的权限赋给他

chown -R elastic:elastic kibana-7.13.0

5、开通5602端口

firewall-cmd --add-port=5602/tcp --permanent # 重新载入添加的端口firewall-cmd --reload

6、以elastic账户,启动kibana

./bin/kibana

7、测试,访问ip:5602,出现以下页面则部署成功

搭建自己的微服务日志监控平台


4.4 部署logstash

0、logstash依赖与java环境,且elastic支持的jdk版本为jdk8,11,14之一。提前安装好java环境。我这里选择了jdk8(但实际上es官方在7.13版本中更加推荐的是jdk11+)

需要注意的是logstash7.13.0已自带jdk,在安装目录下的jdk目录,所以如果服务器没有单独安装jdk的话,会采用logstash下的jdk

1、将logstash安装包上传到服务器,这里使用scp的方式进行传输

scp logstash-7.13.0-linux-x86_64.tar.gz root@172.16.188.6/var/local

2、解压安装包

tar -zxvf logstash-7.13.0-linux-x86_64.tar.gz

3、修改配置文件,logstash提供了一个实例配置文件logstash-sample.conf,我们直接在它的基础上进行修改

# beats传入的端口,默认5044input { beats { port => 5044 }}# 输出日志的方式output { # 按照日志标签对日志进行分类处理,日志标签后续会在filebeat中定义 if "user-log" in [tags] { elasticsearch { hosts => ["http://172.16.188.7:9200"] index => "[user-log]-%{+YYYY.MM.dd}" } } if "order-log" in [tags] { elasticsearch { hosts => ["http://172.16.188.7:9200"] index => "[order-log]-%{+YYYY.MM.dd}" } }}

4、开放5044端口

firewall-cmd --add-port=5044/tcp --permanent # 重新载入添加的端口firewall-cmd --reload

5、以上述配置文件启动logstash,logstash启动较慢,在等待它启动的时候,我们可以去部署filebeat了

./bin/logstash -f config/logstash-sample.conf

4.5 部署filebeat

1、将filebeat传输到两个微服务所在的服务器上,这里采用scp的方式进行传输

scp filebeat-7.13.0-linux-x86_64.tar.gz root@172.16.188.3:/var/localscp filebeat-7.13.0-linux-x86_64.tar.gz root@172.16.188.2:/var/local

2、在/var/local目录下解压filebeat

tar -zxvf filebeat-7.13.0-linux-x86_64.tar.gz

3、进入filebeat安装目录后,修改filebeat配置文件

vim filebeat.yml# 如果没有安装vim可执行如下指令安装yum install vim

内容如下:

# 从日志文件输入日志filebeat.inputs:- type: log enabled: true paths: - /Users/wuhanxue/Downloads/*.log # 定义日志标签,注意当order服务时将该标签改为order-log tags: ["user-log"]setup.template.settings:# 设置主分片数 index.number_of_shards: 1# 因为测试环境只有一个es节点,所以将副本分片设置为0,否则集群会报黄 index.number_of_replicas: 0# 输出到logstashoutput.logstash:# logstash所在服务器的ip和端口 hosts: ["172.16.188.6:5044"]# 默认配置,不做改动processors: - add_host_metadata: when.not.contains.tags: forwarded - add_cloud_metadata: ~ - add_docker_metadata: ~ - add_kubernetes_metadata: ~

4、启动filebeat

 ./filebeat -e -c filebeat.yml

5、另一个微服务上也做同样配置

6、重启微服务,将启动日志,输入到beats中

7、调用接口,创建几条日志 user-service

http://172.16.188.2:8081/user/info?log=usehttp://172.16.188.2:8081/user/error?log=use

order-service

http://172.16.188.3:8080/order/info?log=ordehttp://172.16.188.3:8080/order/error?log=orde

4.6 kibana可视化

4.6.1 操作步骤

在kibana中dev tool输入指令

GET _cat/indices

结果,可以看到日志索引已经创建成功了,接下来我们来实现可视化

搭建自己的微服务日志监控平台

1、打开kibana,进入stack management > index management


这里会发现新创建的日志索引状态时黄的,这是因为副本分片数设置为1了,之前在filebeat中的设置并没有成功,这里原因未知,后续研究后更新上来。

搭建自己的微服务日志监控平台

我们可以通过修改索引的副本分片数来使索引状态更新为绿色:点击索引,在弹出框中点击edit settings,修改number_of_replicas为0,点击save。当然也可以直接通过DSL指令修改

搭建自己的微服务日志监控平台

2、点击Index patterns,点击创建索引模式

搭建自己的微服务日志监控平台

输出order-log的正则匹配


[order-log]-*


搭建自己的微服务日志监控平台

选择一个时间字段,如果日志数据中本身没有,可以使用@timestamp

搭建自己的微服务日志监控平台

同理创建user-log的索引模式


3、索引可视化 点击左侧菜单栏中的Discover

搭建自己的微服务日志监控平台

点击左侧的索引下拉列表,选择需要查询的索引模式,在时间框中输入日期范围,点击刷新,会看到列表中日志数据已经查询出来了

搭建自己的微服务日志监控平台

4、定制列表字段 上述图片中可以看到,列表中的字段是直接显示的整个doc,如果我们想要分栏显示某部分字段怎么办呢?


可以在available fields中添加想要显示的字段

搭建自己的微服务日志监控平台

添加后

搭建自己的微服务日志监控平台

5、查询日志 如果不指定字段,那么查询针对所有的字段,为了提高查询效率,可以指定字段 <filed>:<keyword>

搭建自己的微服务日志监控平台


4.6.2 同一条日志被处理成了多条

观察日志我们可以发现,本来属于同一条错误日志的数据,被分割成了多条doc,这时因为filebeat是按照换行符进行分割的,而某些报错日志本身就包含换行符,为了让这样的日志归并到一个doc,我们需要通过multiline参数来帮忙

搭建自己的微服务日志监控平台

1、multiline的原理就是通过某串字符来区分是同一条日志,比如如下的日志格式,每条日志都是以[开头的,所以可以以[来区分


[2021-01-03 23:00:00] INFO order create[2021-01-03 23:00:00] IndexNotFoundException[no such index] at org.elasticsearch.cluster.metadata.IndexNameExpressionResolver$WildcardExpressionResolver.resolve(IndexNameExpressionResolver.java:566) at org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.concreteIndices(IndexNameExpressionResolver.java:133) at org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.concreteIndices(IndexNameExpressionResolver.java:77) at org.elasticsearch.action.admin.indices.delete.TransportDeleteIndexAction.checkBlock(TransportDeleteIndexAction.java:75)

详情可见multiline官方文档[1] 2、按照上述的原理,我们观察日志的格式,思考可以同一条日志有什么格式规律 当然这里因为我们处理的是微服务的日志,我们可以直接在服务中定义方便我们处理的日志格式,但是不排除有些场景无法自定义日志格式,因此我们观察下图中日志的格式

搭建自己的微服务日志监控平台

3、容易观察到每条日志都是日期开头的,实际查看官方文档中,就有关于日期的正则表达,在filebeat的配置文件中添加如下配置


multiline.type: patternmultiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}'multiline.negate: truemultiline.match: after

注意这里的配置需要添加在filebeat.inputs下

# 从日志文件输入日志filebeat.inputs:- type: log enabled: true paths: - /Users/wuhanxue/Downloads/*.log tags: ["user-log"] exclude_lines: ['^$'] multiline: type: pattern pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}' negate: true match: aftersetup.template.settings:# 设置主分片数 index.number_of_shards: 1# 因为测试环境只有一个es节点,所以将副本分片设置为0,否则集群会报黄 index.number_of_replicas: 0# 输出到logstashoutput.logstash:# logstash所在服务器的ip和端口 hosts: ["172.16.188.6:5044"]

另外如果有空行的话,可以添加如下配置排除空行

filebeat.inputs:- type: log exclude_lines: ['^$']

再次查看kibana会发现多行日志已经归并为一条了

搭建自己的微服务日志监控平台


4.6.3 测试正则是否正确

我们在实际生产中书写完多行匹配的正则表达式,可能需要测试一下是否能够匹配得到,如果每次都需要启动filebeat来测试的话,难免有些麻烦,关于这点官方文档中也提供了一个网址用来测试 https://go.dev/play/

测试代码,供大家参考


// You can edit this code!// Click here and start typing.package main
import ( "fmt" "regexp" "strings")
var pattern = `^[0-9]{4}-[0-9]{2}-[0-9]{2}`var negate = false
var content = `2022-01-05 00:24:07.705 ERROR 1695 --- [nio-8081-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero at com.example.userservice.controller.UserController.errorLog(UserController.java:27) ~[classes!/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_271] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_271] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_271] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_271] at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at javax.servlet.http.HttpServlet.service(HttpServlet.java:626) ~[tomcat-embed-core-9.0.41.jar!/:4.0.FR] at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[tomcat-embed-core-9.0.41.jar!/:4.0.FR] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.12.RELEASE.jar!/:5.2.12.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_271] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_271] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.41.jar!/:9.0.41] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_271]`
func main() { regex, err := regexp.Compile(pattern) if err != nil { fmt.Println("fail to compile: ", err) return } //lines := strings.Split(content, "\n") lines := strings.Split(content, "|||") for _, line := range lines { fmt.Println("line: ", line) matches := regex.MatchString(line) if negate { matches = !matches } fmt.Printf("%v\t%v\n", matches, line) }}

References

[1] 详情可见multiline官方文档: https://www.elastic.co/guide/en/beats/filebeat/7.13/multiline-examples.html