es+logstash+kibana搭建
1.简介
ELK(elasticsearch+logstash+kibana)是目前比较常用的日志分析系统,包括日志收集(logstash),日志存储搜索(elasticsearch),展示查询(kibana),使用ELK作为日志的存储分析系统并通过为每个请求分配requestId链接相关日志。ELK具体结构如下图所示:
1.1es基本概念
Index
类似于mysql数据库中的database
Type
类似于mysql数据库中的table表,es中可以在Index中建立type(table),通过mapping进行映射。
Document
由于es存储的数据是文档型的,一条数据对应一篇文档即相当于mysql数据库中的一行数据row, 一个文档中可以有多个字段也就是mysql数据库一行可以有多列。
Field
es中一个文档中对应的多个列与mysql数据库中每一列对应
Mapping
可以理解为mysql或者solr中对应的schema,只不过有些时候es中的mapping增加了动态识别功能,感觉很强大的样子, 其实实际生产环境上不建议使用,最好还是开始制定好了对应的schema为主。
indexed
就是名义上的建立索引。mysql中一般会对经常使用的列增加相应的索引用于提高查询速度,而在es中默认都是会加 上索引的,除非你特殊制定不建立索引只是进行存储用于展示,这个需要看你具体的需求和业务进行设定了。
Query DSL
类似于mysql的sql语句,只不过在es中是使用的json格式的查询语句,专业术语就叫:QueryDSL
GET/PUT/POST/DELETE
分别类似与mysql中的select/update/delete......
1.2es架构
elasticsearch提供了一个分布式多用户能力的全文搜索引擎,基于RESTfulweb接口。ElasticSearch是用Java开发的, 并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。构建在全 文检索开源软件Lucene之上的Elasticsearch,不仅能对海量规模的数据完成分布式索引与检索,还能提供数据聚合分析。据国际权威的 数据库产品评测机构DBEngines的统计,在2016年1月,Elasticsearch已超过Solr等,成为排名第一的搜索引擎类应用 概括:基于Restful标准的高扩展高可用的实时数据分析的全文搜索工具,elasticsearch架构
Gateway层
es用来存储索引文件的一个文件系统且它支持很多类型,例如:本地磁盘、共享存储(做snapshot的时候需要用到)、hadoop 的hdfs分布式存储、亚马逊的S3。它的主要职责是用来对数据进行长持久化以及整个集群重启之后可以通过gateway重新恢复数据。
Distributed Lucene Directory
Gateway上层就是一个lucene的分布式框架,lucene是做检索的,但是它是一个单机的搜索引擎,像这种es分布式搜索引擎系 统,虽然底层用lucene,但是需要在每个节点上都运行lucene进行相应的索引、查询以及更新,所以需要做成一个分布式的运 行框架来满足业务的需要。
四大模块组件
districted lucene directory之上就是一些es的模块
1. Index Module是索引模块,就是对数据建立索引也就是通常所说的建立一些倒排索引等;
2. Search Module是搜索模块,就是对数据进行查询搜索;
3. Mapping模块是数据映射与解析模块,就是你的数据的每个字段可以根据你建立的表结构 通过mapping进行映射解析,如果你没有建立表结构,es就会根据你的数据类型推测你 的数据结构之后自己生成一个mapping,然后都是根据这个mapping进行解析你的数据;
4. River模块在es2.0之后应该是被取消了,它的意思表示是第三方插件,例如可以通过一 些自定义的脚本将传统的数据库(mysql)等数据源通过格式化转换后直接同步到es集群里, 这个river大部分是自己写的,写出来的东西质量参差不齐,将这些东西集成到es中会引发 很多内部bug,严重影响了es的正常应用,所以在es2.0之后考虑将其去掉。
Discovery、Script
es4大模块组件之上有 Discovery模块:es是一个集群包含很多节点,很多节点需要互相发现对方,然后组成一个集群包括选 主的,这些es都是用的discovery模块,默认使用的是Zen,也可是使用EC2;es查询还可以支撑多种script即脚本语言,包括 mvel、js、python等等。
Transport协议层
再上一层就是es的通讯接口Transport,支持的也比较多:Thrift、Memcached以及Http,默认的是http,JMX就是java的一个 远程监控管理框架,因为es是通过java实现的。
RESTful接口层
最上层就是es暴露给我们的访问接口,官方推荐的方案就是这种Restful接口,直接发送http请求,方便后续使用nginx做代理、 分发包括可能后续会做权限的管理,通过http很容易做这方面的管理。如果使用java客户端它是直接调用api,在做负载均衡以 及权限管理还是不太好做。
1.3RestfulAPI
一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。 基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。在目前主流的三种Web服务交互方案中,REST相比于S OAP(Simple Object Access protocol,简单对象访问协议)以及XML-RPC更加简单明了。
接口类型:
(Representational State Transfer 意思是:表述性状态传递)
它使用典型的HTTP方法,诸如GET,POST.DELETE,PUT来实现资源的获取,添加,修改,删除等操作。即通过HTTP动词来实现资源的状态扭转
GET 用来获取资源
POST 用来新建资源(也可以用于更新资源)
PUT 用来更新资源
DELETE 用来删除资源
1.4CRUL命令
以命令的方式执行HTTP协议的请求
GET/POST/PUT/DELETE
示例:
访问一个网页
curl www.baidu.com
curl -o tt.html www.baidu.com
显示响应的头信息
curl -i www.baidu.com
显示一次HTTP请求的通信过程
curl -v www.baidu.com
执行GET/POST/PUT/DELETE操作
curl -X GET/POST/PUT/DELETE url
2.安装logstash
2.1安装jdk
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配置成功。
# java -version
2.2下载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
因为我们配置内容为,控制台输出日志内容,所以显示以上格式即为成功。
3. 安装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。
4.安装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中的使用。
5.创建web工程
一个普通的maven java web工程,为了测试分布式系统日志的连续性,我们让这个项目自调用n次,并部署2个项目,相互调用,关键代码如下:
controller
@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
@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() <= 3) {
Map<String, String> headerMap = new HashMap<String, String>();
String requestId = RequestIdUtil.requestIdThreadLocal.get();
headerMap.put(RequestIdUtil.REQUEST_ID_KEY, requestId);
Map<String, String> paramMap = new HashMap<String, String>();
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
* requestIdFilter
* com.virxue.baseweb.utils.RequestIdFilter
* requestIdFilter
/*
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() {
}
}
utils
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 zhengkai
* @date 2019年10月3日 下午21: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,具体的配置如下:
UTF-8
${log_base}/java-base-web.log
${log_base}/java-base-web-%d{yyyy-MM-dd}-%i.log 10 200MB
%d^|^%X{requestId}^|^%-5level^|^%logger{36}%M^|^%msg%n
这里的日志格式使用了“^|^”做为分隔符,方便logstash进行切分。在测试服务器部署2个web项目,并且修改日志输出位置,并修改url调用链接使项目相互调用。
6. 修改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格式,方便查询分析,测试一下: