今天是2019最后一天了,最近几天都在搞这块微服务日志管理的事情,有很多种方案实现,每种都有各自的优点,但是适合当前涉及的业务场景的不多,想法是尽可能多减少开发人员和实施及运维人员的工作量,生产环境的资源有条件让我可以放手去干,那么就在开发环境下先研究一下。整个项目不同以往在Linux平台,这次所有基础环境业务系统服务统一在windows平台,可能会涉及一些坑。
简单说明一下资源有限的开发环境:windows平台 ~~
涉及基础环境 ELK 3台 + kafka 3台 每台(16G+2核Intel Xeon Gold 5118+500g)
微服务部署就很随便了。
实现目的,开发人员只需关注该方法需不需要进行日志收集与统一管理,设计提供了系统、数据库、业务操作、登陆登出等这些日志类型管理。
同时日志实现又可根据实际需要灵活多变,无论是filebeat、kafka、redis、database等等都可自行拓展实现。
微服务的日志管理很麻烦,每个服务都有相应的日志,在生产环境下想要及时的去追踪一个问题变得繁琐起来,通常我们会从应用层定位问题然后对应去服务端查看相关服务日志确认问题根源。
那么微服务架构下,各个服务器的管理,各个服务的部署,各个基础环境、服务等维护都是简单却麻烦的工作,这其中日志的管理就变得让人眼花缭乱,定位一个问题往往把技术人员和运维人员推上风口浪尖。
如何去解决这个问题,怎么才能让问题暴露的更明显,让管理变得跟简单,让开发人员开发起来更轻松而无需去关注具体实现呢。
同样也可以不用中间件kafka,直接通过ES Api的接口,直接将日志信息从服务写入到ES。
为什么要搞一层kafka:因为不想部署起来那么麻烦搞每个服务的日志收集,直接由性能强大的kafka当中介即可,这样在部署基础环境的时候就可以将日志平台搭建好了。微服务的日志当然也要统一约定且规范了,所以核心代码贴下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Log {
String id() default "";
Class> name() default Object.class;
LogType[] type() default {LogType.SYSTEM};
}
/*******************************
* Copyright (C),2018-2099, ZJJ
* Title :
* File name : Logs
* Author : zhoujiajun
* Date : 2020/1/2 14:42
* Version : 1.0
* Description :
******************************/
public class Logs implements savvy.wit.framework.core.base.annotations.Log {
private String id;
private Class> name;
private LogType[] types;
public Logs() {
}
public Logs(String id, Class> name, LogType[] types) {
this.id = id;
this.name = name;
this.types = types;
}
public void setId(String id) {
this.id = id;
}
public void setName(Class> name) {
this.name = name;
}
public void setTypes(LogType[] types) {
this.types = types;
}
@Override
public String id() {
return id;
}
@Override
public Class> name() {
return name;
}
@Override
public LogType[] type() {
return types;
}
/**
* Returns the annotation type of this annotation.
*
* @return the annotation type of this annotation
*/
@Override
public Class extends Annotation> annotationType() {
return null;
}
}
public enum LogType {
SYSTEM,
BUSINESS,
EXCEPTION,
ERROR,
DDL,
DML,
CURD,
ADD,
REMOVE,
UPDATE,
FETCH,
QUERY,
LOGIN,
LOGOUT,
PWD,
}
public interface LogCallBack {
/**
* 日志回调
* @param joinPoint 连接点
* @param log 注解
* @param result 目标结果
*/
void execute(ProceedingJoinPoint joinPoint, Log log,Object result);
}
public abstract class AbstractLogAspectJ {
/**
* 线程池 异步记录日志
*/
private static ExecutorService logExecutorService = Executors.newFixedThreadPool(10);
/**
* 定义切入点:对要拦截的方法进行定义与限制,如包、类
*
*/
@Pointcut("@annotation(com.xxxx.alien.core.log.Log)")
protected void log () {
}
/**
* 前置通知:在目标方法执行前调用
*/
@Before("log()")
public void begin() {
}
/**
* 后置通知:在目标方法执行后调用,若目标方法出现异常,则不执行
*/
@AfterReturning("log()")
public void afterReturning() {
}
/**
* 后置/最终通知:无论目标方法在执行过程中出现异常都会在它之后调用
*/
@After("log()")
public void after() {
}
/**
* 异常通知:目标方法抛出异常时执行
* 任何被切入点定义的方法在运行时发生异常都可捕获到并进行统一回调处理
* @param joinPoint 连接点
* @param throwable 异常
*/
@AfterThrowing(value = "log()", throwing = "throwable")
public void afterThrowing(JoinPoint joinPoint, Throwable throwable) {
handleThrowable(joinPoint, throwable, throwableBack());
}
/**
* 环绕通知: 灵活自由的在目标方法中切入代码
* @param joinPoint 连接点
* @throws Throwable 异常
*/
@Around("log()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 进行源方法
Object result = joinPoint.proceed();
//保存日志 注意如果方法执行错误这不会记录日志
logExecutorService.submit(() -> {
try {
handleMessage(joinPoint, result, logback());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
});
return result;
}
/**
* 提供重写的回调
* @return callback
*/
public LogCallBack logback() {
return (joinPoint, log, result) -> {
};
}
/**
* 提供throwable回调
* @return callback
*/
public LogThrowableCallBack throwableBack() {
return (joinPoint, throwable) -> {
};
}
/**
* 消息处理
* @param joinPoint 切入点
* @param result 结果
* @param callBack 回调
* @throws NoSuchMethodException
*/
private void handleMessage(ProceedingJoinPoint joinPoint, Object result, LogCallBack callBack) throws NoSuchMethodException {
Log log = ClassUtil.me().getDeclaredAnnotation(joinPoint, Log.class);
/*
2020-01-02:add
用log的实现类,传递参数
在log id为默认情况下时
动态提供uuid
*/
Logs logs = new Logs(StringUtil.isBlank(log.id()) ? StringUtil.uuid() : log.id(), log.name(), log.type());
callBack.execute(joinPoint, logs, result);
}
/**
* 异常处理
* @param joinPoint 切入点
* @param throwable 异常
* @param callBack 回调
*/
private void handleThrowable(JoinPoint joinPoint, Throwable throwable, LogThrowableCallBack callBack) {
callBack.execute(joinPoint, throwable);
}
}
public class LogMessage {
private String type;
private String timestamp;
private String prefix;
private Object[] param;
private Object result;
private String suffix;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public Object[] getParam() {
return param;
}
public void setParam(Object[] param) {
this.param = param;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
public class LogMsgBuilder {
private static LogMessage message;
public LogMsgBuilder() {
}
public static LogMsgBuilder create() {
message = new LogMessage();
return LazyInit.INITIALIZATION;
}
public LogMsgBuilder type(LogType type) {
message.setType(type.name());
return this;
}
public LogMsgBuilder time(String timestamp) {
message.setTimestamp(timestamp);
return this;
}
public LogMsgBuilder prefix(String... prefixes) {
StringBuilder prefix = new StringBuilder();
for (String p : prefixes) {
prefix.append("[" + p + "]");
}
message.setPrefix(prefix.toString());
return this;
}
public LogMsgBuilder param(Object... params) {
if (params.length > 0)
message.setParam(params);
else
message.setParam(null);
return this;
}
public LogMsgBuilder result(Object result) {
message.setResult(result);
return this;
}
public LogMsgBuilder suffix(String... suffixes) {
StringBuilder suffix = new StringBuilder();
for (String s : suffixes) {
suffix.append("[" + s + "]");
}
message.setSuffix(suffix.toString());
return this;
}
public LogMessage build() {
return message;
}
private static class LazyInit {
private static LogMsgBuilder INITIALIZATION = new LogMsgBuilder();
}
}
在实现日志回调时所做的业务逻辑处理需要将日志写入ES,这里提供了两种思路,各有好处。
看注释
@Aspect
@Component
@Lazy(false)
public class LoggerAspectJ extends AbstractLogAspectJ {
@Override
public LogCallBack logback() {
return (joinPoint, log, result) -> {
// 获取需要的日志信息
// 可以有多种方式处理日志信息
// 1、通过Kafka 将日志转化为消息发送,再logstash处将消息转化为日志存储到es
/*
每一个系统方法执行由一个uuid进行管理
该id可作为suffix添加为消息后缀
如此,可以对一个方法进行多个日志类型的管理
比如:
修改用户密码的方法可以在方法上加入注解
@Log(type = {PWD,UPDATE})
这样系统会保存两条日志,但是这两条日志有一个共同的suffix
这样在ES中进行索引管理时,可以根据suffix进行过滤,或者由 type=pwd的日志 得到suffix,再从而查找该suffix对应几条日志
可以确定是新增密码 还是 修改密码等
其他方法也可按需求如此
*/
String id = log.id();
for (LogType type : log.type()) {
switch (type) {
case SYSTEM:
break;
}
kafkaTemplate.send(topic, LogMsgBuilder
.create()
.type(type)
.time(DateUtil.getNow())
.prefix()
.param()
.result(result)
.suffix()
.build());
// 2、不用中间件直接通过封装es api的接口将日志信息写入到es
var data = packData(log, result);
somePackingClass.sendMsgToElastic(data);
};
}
}
// 使用Log注解即可通过切面将日志信息写入ES
@Log(type = LogType.REMOVE)
@PostMapping("remove/abc")
public ReturnBase removeAbc(@RequestBody @Valid Abc abc) {
dosomething in here ......
}
什么代码层面展示完毕,下面就是ELK的安装部署与配置
硬件条件要求
3台vm如下:
内网IP |
||
Dev Cluster |
192.168.0.9 |
es、logstash |
192.168.0.36 |
es |
|
192.168.0.98 |
es、kibana |
|
Test Cluster |
192.168.0.124 |
es、logstash |
192.168.0.180 |
es |
|
192.168.0.244 |
es、kibana |
软件环境要求
ELK最新稳定版本,当前部署版本为:
Elastic8.1.2
Logstash8.1.2
Kibana8.1.2
操作系统:Ubuntu 20.04.1
JDK:OpenJDK17.0.2
网络条件要求
内网间vm互通
ELK没有规划公网IP,所有公网访问通过nginx代理
三台vm安装步骤相同,都需要安装JDK、Elastic、Logstash、Kibana。
Elastic根目录:/opt/elasticsearch
Logstash数据目录:/opt/logstash
Kibana根目录:/opt/kibana
JDK安装目录:/opt/elasticsearch/jdk
修改环境变量
vim /etc/profile
末尾添加
PATH=$PATH:$HOME/bin:/opt/elasticsearch/bin
export JAVA_HOME=/opt/elasticsearch/jdk/
export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH
export CLASSPATH=.$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/lib/tools.jar
export PATH
立即生效
source /etc/profile
验证
java -version
统一将部署安装包放到指定目录/opt下,进行解压与更名后路径对应如下:
/opt/elasticsearch
/opt/logstash
/opt/kibana
生成集群内部通信安全证书
生成自签名CA证书
./bin/elasticsearch-certutil ca
默认文件名为elastic-stack-ca.p12,需要设置ca密码,这里设置123456
生成节点通信证书
./bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12
这里会有三处需要输入:
第一行 输入CA密码:123456
第二行 默认文件名elastic-certificates.p12,按enter
第三行 设置证书密码:12345678
在各个节点的config目录下新建文件夹certs
mkdir config/certs
将通信elastic-certificates.p12传输到各个节点的certs目录下
scp -r elastic-certificates.p12 [email protected]/opt/elasticsearch/config/certs
设置各个节点的通信密码
./bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_password
./bin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_password
所有密码设置为12345678
配置es集群
这里以test集群中第一个节点node-1(192.168.0.124)为例
cluster.name: es8.1.2-cluster-test
node.name: node-1
path.data: /opt/elasticsearch/data
path.logs: /opt/elasticsearch/logs
network.host: 192.168.0.124
http.port: 10501
#discovery.seed_hosts为集群中其他节点
discovery.seed_hosts: ["192.168.0.180","192.168.0.244"]
cluster.initial_master_nodes: ["192.168.0.124", "192.168.0.180","192.168.0.244"]
ingest.geoip.downloader.enabled: false
xpack.security.enabled: true
xpack.security.http.ssl:
enabled: false
#keystore.path: certs/elastic-certificates.p12
#truststore.path: certs/elastic-certificates.p12
xpack.security.transport.ssl:
enabled: true
verification_mode: certificate
keystore.path: certs/elastic-certificates.p12
truststore.path: certs/elastic-certificates.p12
其他节点配置修改相应配置且基本同上。
通过elastic用户启动集群
在各个节点进行如下操作
添加elastic用户
useradd elastic
增加elastic用户权限
chown elastic /opt/elasticsearch/ -R
切换elastic用户
su elastic
启动elastic节点
./bin/elasticsearch -d
检查es集群信息
查看节点信息
curl -XGET -u elastic:你自己设置的密码 http://192.168.0.9:10501/_cat/nodes?v
查看索引信息
curl -XGET -u elastic:你自己设置的密码 http://192.168.0.9:10501/_cat/indices?v
集群健康信息
curl -XGET -u elastic:你自己设置的密码 http://192.168.0.9:10501/_cluster/health
创建索引
curl -X PUT -H 'content-type:application/json' -u elastic:你自己设置的密码 http://192.168.0.9:10501/test-index \
-d '{"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}}'
给索引添加映射
curl -X PUT -H 'content-type:application/json' -u elastic:你自己设置的密码 http://192.168.0.9:10501/test-index/_mapping \
-d '{"properties": {
"key": {
"type": "keyword"
},
"value": {
"type": "integer"
},
"msg": {
"type": "text"
}
}
}'
索引添加数据
curl -X POST -H "content-type:application/json;charset=utf-8" -u elastic:你自己设置的密码 http://192.168.0.9:10501/test-index/_create/1 \
-d '{"key":"es-first","value":1010101,"msg":"elasticsearch cluster is created "}'
修改elastic和kibana_system用户密码
检查集群正常后,在每个节点对elastic用户密码进行修改,为后续登录kibana需要
./bin/elasticsearch-reset-password --username elastic -i
./bin/elasticsearch-reset-password -u kibana_system -i
分别设置密码为:设置你自己的密码
修改logstash-sample.conf配置,这里配置消费kafka消息,由于这里elk不能访问公网,所以配置kafka端口为内网端口10340.(kafka部署内外网端口10340和10341)
# Sample Logstash configuration for creating a simple
# Beats -> Logstash -> Elasticsearch pipeline.
input {
kafka {
type => "logtest"
bootstrap_servers =>"kafka-01:10340,kafka-02:10340,kafka-03:10340"
group_id => "log_test"
client_id => "log_test"
topics => ["log_test"]
auto_offset_reset => "earliest"
consumer_threads => 1
decorate_events => true
codec => 'json'
}
}
output {
elasticsearch {
hosts => ["http://192.168.0.124:10501","http://192.168.0.180:10501","http://192.168.0.244:10501"]
index => "log-test-%{+YYYY.MM.dd}"
user => "elastic"
password => "设置你自己的密码"
}
stdout {
codec => rubydebug
}
}
启动logstash
nohup ./bin/logstash -f config/logstash-sample.conf &
查看nohup.out日志是否启动成功
tail -f nohup.out
修改kibana.yml配置文件
server.port: 10502
server.host: "192.168.0.244"
server.name: "Kibana-Test"
elasticsearch.hosts: ["http://192.168.0.124:10501","http://192.168.0.180:10501","http://192.168.0.244:10501"]
elasticsearch.username: "kibana_system"
elasticsearch.password: "设置你自己的密码"
i18n.locale: "zh-CN"
添加kibana用户
useradd kibana
增加kibana权限
chown kibana /opt/kibana/ -R
切换kibana用户
su kibana
启动kibana
nohup ./bin/kibana &
查看nohup日志
tail -f nohup.out 是否启动成功
因为kibana但个集群只布了一个节点
新增nginx配置
server {
listen 10502;
server_name kibana;
location / {
proxy_pass http://192.168.0.98:10502;
}
}
打开浏览器通过nginx_address:10502访问kibana
使用用户elastic登录,密码为*********