日志是分析线上问题的重要手段,通常我们会把日志输出到控制台或者本地文件中,排查问题时通过根据关键字搜索本地日志,但越来越多的公司,项目开发中采用分布式的架构,日志会记录到多个服务器或者文件中,分析问题时可能需要查看多个日志文件才能定位问题,如果相关项目不是一个团队维护时沟通成本更是直线上升。把各个系统的日志聚合并通过关键字链接一个事务处理请求,是分析分布式系统问题的有效的方式。
ELK(elasticsearch+logstash+kibana) 是目前比较常用的日志分析系统,包括日志收集 (logstash),日志存储搜索 (elasticsearch),展示查询 (kibana),我们使用 ELK 作为日志的存储分析系统并通过为每个请求分配 requestId 链接相关日志。ELK 具体结构如下图所示:
1、安装 logstash
logstash 需要依赖 jdk,安装 logstash 之前先安装 java 环境。
下载 JDK:
在 oracle 的官方网站下载,http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
根据操作系统的版本下载对应的 JDK 安装包,本次实验下载的是 jdk-8u101-linux-x64.tar.gz
上传文件到服务器并执行:
# mkdir /usr/local/java
# tar -zxf jdk-8u45-linux-x64.tar.gz -C /usr/local/java/
配置 java 环境
- export JAVA_HOME=/usr/local/java/jdk1.8.0_45
- export PATH=$PATH:$JAVA_HOME/bin
- export CLASSPATH=.:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:$CLASSPATH
执行 java -version 命令,打印出 java 版本信息表示 JDK 配置成功。
下载 logstash:
wget https://download.elastic.co/logstash/logstash/logstash-2.4.0.tar.gz
tar -xzvf logstash-2.4.0.tar.gz
进入安装目录: cd #{dir}/logstash-2.4.0
创建 logstash 测试配置文件:
vim test.conf
编辑内容如下:
- input {
- stdin { }
- }
- output {
- stdout {
- codec => rubydebug {}
- }
- }
运行 logstash 测试:
bin/logstash -f test.conf
显示
证明 logstash 已经启动了,
输入 hello world
因为我们配置内容为,控制台输出日志内容,所以显示以上格式即为成功。
2、安装 elasticsearch
下载安装包:
wget https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.4.0/elasticsearch-2.4.0.tar.gz
解压并配置:
tar -xzvf elasticsearch-2.4.0.tar.gz
cd #{dir}/elasticsearch-2.4.0
vim config/elasticsearch.yml
修改:
- path.data: /data/es #数据路径
- path.logs: /data/logs/es #日志路径
- network.host: 本机地址 #服务器地址
- http.port: 9200 #端口
配置执行用户和目录:
- groupadd elsearch
- useradd elsearch -g elsearch -p elasticsearch
- chown -R elsearch:elsearch elasticsearch-2.4.0
- mkdir /data/es
- mkdir /data/logs/es
- chown -R elsearch:elsearch /data/es
- chown -R elsearch:elsearch /data/logs/es
启动 elasticsearch:
su elsearch
bin/elasticsearch
通过浏览器访问:
安装成功.
集成 logstash 和 elasticsearch,修改 Logstash 配置为:
- input {
- stdin { }
- }
- output {
- elasticsearch {
- hosts => "elasticsearchIP:9200"
- index => "logstash-test"
- }
- stdout {
- codec => rubydebug {}
- }
- }
再次启动 logstash,并输入任意文字:"hello elasticsearch"
通过 elasticsearch 搜索到了刚才输入的文字,集成成功。
但是通过 elasticsearch 的原生接口查询和展示都不够便捷直观,下面我们配置一下更方便的查询分析工具 kibana。
3、安装 kibana
下载安装包:
wget https://download.elastic.co/kibana/kibana/kibana-4.6.1-linux-x86_64.tar.gz
解压 kibana,并进入解压后的目录
打开 config/kibana.yml, 修改如下内容
#启动端口 因为端口受限 所以变更了默认端口
server.port: 8601
#启动服务的 ip
server.host: "本机 ip"
#elasticsearch 地址
elasticsearch.url: "http://elasticsearchIP:9200"
启动程序:
bin/kibana
访问配置的 ip:port,在 discover 中搜索刚才输入的字符,内容非常美观的展示了出来。
到这里我们的 elk 环境已经配置完成了,我们把已 java web 项目试验日志在 elk 中的使用。
4、创建 web 工程
一个普通的 maven java web 工程,为了测试分布式系统日志的连续性,我们让这个项目自调用 n 次,并部署 2 个项目,相互调用,关键代码如下:
- @RequestMapping("http_client")
- @Controller
- public class HttpClientTestController {
- @Autowired
- private HttpClientTestBo httpClientTestBo;
- @RequestMapping(method = RequestMethod.POST)
- @ResponseBody
- public BaseResult doPost(@RequestBody HttpClientTestResult result) {
- HttpClientTestResult testPost = httpClientTestBo.testPost(result);
- return testPost;
- }
- }
- @Service public class HttpClientTestBo {
- private static Logger logger = LoggerFactory.getLogger(HttpClientTestBo.class);
- @Value("${test_http_client_url}") private String testHttpClientUrl;
- public HttpClientTestResult testPost(HttpClientTestResult result) {
- logger.info(JSONObject.toJSONString(result));
- result.setCount(result.getCount() + 1);
- if (result.getCount() & lt; = 3) {
- Map & lt;
- String,
- String & gt;
- headerMap = new HashMap & lt;
- String,
- String & gt; ();
- String requestId = RequestIdUtil.requestIdThreadLocal.get();
- headerMap.put(RequestIdUtil.REQUEST_ID_KEY, requestId);
- Map & lt;
- String,
- String & gt;
- paramMap = new HashMap & lt;
- String,
- String & gt; ();
- paramMap.put("status", result.getStatus() + "");
- paramMap.put("errorCode", result.getErrorCode());
- paramMap.put("message", result.getMessage());
- paramMap.put("count", result.getCount() + "");
- String resultString = JsonHttpClientUtil.post(testHttpClientUrl, headerMap, paramMap, "UTF-8");
- logger.info(resultString);
- }
- logger.info(JSONObject.toJSONString(result));
- return result;
- }
- }
为了表示调用的链接性我们在 web.xml 中配置 requestId 的 filter,用于创建 requestId:
- <filter>
- <filter-name>
- requestIdFilter
- </filter-name>
- <filter-class>
- com.virxue.baseweb.utils.RequestIdFilter
- </filter-class>
- </filter>
- <filter-mapping>
- <filter-name>
- requestIdFilter
- </filter-name>
- <url-pattern>
- /*
- </url-pattern>
- </filter-mapping>
- public class RequestIdFilter implements Filter {
- private static final Logger logger = LoggerFactory.getLogger(RequestIdFilter.class);
- /* (non-Javadoc)
- * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
- */
- public void init(FilterConfig filterConfig) throws ServletException {
- logger.info("RequestIdFilter init");
- }
- /* (non-Javadoc)
- * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
- */
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
- ServletException {
- String requestId = RequestIdUtil.getRequestId((HttpServletRequest) request);
- MDC.put("requestId", requestId);
- chain.doFilter(request, response);
- RequestIdUtil.requestIdThreadLocal.remove();
- MDC.remove("requestId");
- }
- /* (non-Javadoc)
- * @see javax.servlet.Filter#destroy()
- */
- public void destroy() {
- }
- }
- public class RequestIdUtil {
- public static final String REQUEST_ID_KEY = "requestId";
- public static ThreadLocal<String> requestIdThreadLocal = new ThreadLocal<String>();
- private static final Logger logger = LoggerFactory.getLogger(RequestIdUtil.class);
- /**
- * 获取requestId
- * @Title getRequestId
- * @Description TODO
- * @return
- *
- * @author sunhaojie
- * @date 2016年8月31日 上午7:58:28
- */
- public static String getRequestId(HttpServletRequest request) {
- String requestId = null;
- String parameterRequestId = request.getParameter(REQUEST_ID_KEY);
- String headerRequestId = request.getHeader(REQUEST_ID_KEY);
- if (parameterRequestId == null && headerRequestId == null) {
- logger.info("request parameter 和header 都没有requestId入参");
- requestId = UUID.randomUUID().toString();
- } else {
- requestId = parameterRequestId != null ? parameterRequestId : headerRequestId;
- }
- requestIdThreadLocal.set(requestId);
- return requestId;
- }
- }
我们使使用了 Logback 作为日志输出的插件,并且使用它的 MDC 类,可以无侵入的在任何地方输出 requestId,具体的配置如下:
- <configuration>
- <appender name="logfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
- <Encoding>
- UTF-8
- </Encoding>
- <File>
- ${log_base}/java-base-web.log
- </File>
- <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
- <FileNamePattern>
- ${log_base}/java-base-web-%d{yyyy-MM-dd}-%i.log
- </FileNamePattern>
- <MaxHistory>
- 10
- </MaxHistory>
- <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
- <MaxFileSize>
- 200MB
- </MaxFileSize>
- </TimeBasedFileNamingAndTriggeringPolicy>
- </rollingPolicy>
- <layout class="ch.qos.logback.classic.PatternLayout">
- <pattern>
- %d^|^%X{requestId}^|^%-5level^|^%logger{36}%M^|^%msg%n
- </pattern>
- </layout>
- </appender>
- <root level="info">
- <appender-ref ref="logfile" />
- </root>
- </configuration>
这里的日志格式使用了 "^|^" 做为分隔符,方便 logstash 进行切分。在测试服务器部署 2 个 web 项目,并且修改日志输出位置,并修改 url 调用链接使项目相互调用。
5、修改 logstash 读取项目输出日志:
新增 stdin.conf,内容如下:
- input {
- file {
- path => ["/data/logs/java-base-web1/java-base-web.log", "/data/logs/java-base-web2/java-base-web.log"]
- type => "logs"
- start_position => "beginning"
- codec => multiline {
- pattern => "^\[\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}:\d{1,2}"
- negate => true
- what => "next"
- }
- }
- }
- filter{
- mutate{
- split=>["message","^|^"]
- add_field => {
- "messageJson" => "{datetime:%{[message][0]}, requestId:%{[message][1]},level:%{[message][2]}, class:%{[message][3]}, content:%{[message][4]}}"
- }
- remove_field => ["message"]
- }
- }
- output {
- elasticsearch {
- hosts => "10.160.110.48:9200"
- index => "logstash-${type}"
- }
- stdout {
- codec => rubydebug {}
- }
- }
其中 path 为日志文件地址;codec => multiline 为处理 Exception 日志,使换行的异常内容和异常头分割在同一个日志中;filter 为日志内容切分,把日志内容做为 json 格式,方便查询分析;
测试一下:
使用 POSTMan 模拟调用,提示服务器端异常:
通过界面搜索 "调用接口异常",共两条数据。
使用其中一条数据的 requestId 搜索,展示出了请求再系统中和系统间的执行过程,方便了我们排查错误。
到这里我们实验了使用 elk 配置日志分析,其中很多细节需要更好的处理,欢迎更多的同学交流学习。
来源: