SpringCloud + kafka + ELK 搭建微服务日志管理平台

SpringCloud + kafka + ELK 搭建微服务日志管理平台

2019-12-31,写在前面的话

今天是2019最后一天了,最近几天都在搞这块微服务日志管理的事情,有很多种方案实现,每种都有各自的优点,但是适合当前涉及的业务场景的不多,想法是尽可能多减少开发人员和实施及运维人员的工作量,生产环境的资源有条件让我可以放手去干,那么就在开发环境下先研究一下。整个项目不同以往在Linux平台,这次所有基础环境业务系统服务统一在windows平台,可能会涉及一些坑。

简单说明一下资源有限的开发环境:windows平台 ~~ 

涉及基础环境 ELK 3台 + kafka 3台   每台(16G+2核Intel Xeon Gold 5118+500g)

微服务部署就很随便了。

实现目的,开发人员只需关注该方法需不需要进行日志收集与统一管理,设计提供了系统、数据库、业务操作、登陆登出等这些日志类型管理。

同时日志实现又可根据实际需要灵活多变,无论是filebeat、kafka、redis、database等等都可自行拓展实现。

首先,关于kafka+ELK的部署方案可根据服务器资源和业务需求进行设计,这里不再赘述。

微服务的日志管理很麻烦,每个服务都有相应的日志,在生产环境下想要及时的去追踪一个问题变得繁琐起来,通常我们会从应用层定位问题然后对应去服务端查看相关服务日志确认问题根源。

那么微服务架构下,各个服务器的管理,各个服务的部署,各个基础环境、服务等维护都是简单却麻烦的工作,这其中日志的管理就变得让人眼花缭乱,定位一个问题往往把技术人员和运维人员推上风口浪尖。

如何去解决这个问题,怎么才能让问题暴露的更明显,让管理变得跟简单,让开发人员开发起来更轻松而无需去关注具体实现呢。

不过这里提一提日志管理的设计思路是:服务——>Kafka—logstash—>ES->Kibana

同样也可以不用中间件kafka,直接通过ES Api的接口,直接将日志信息从服务写入到ES

为什么要搞一层kafka:因为不想部署起来那么麻烦搞每个服务的日志收集,直接由性能强大的kafka当中介即可,这样在部署基础环境的时候就可以将日志平台搭建好了。微服务的日志当然也要统一约定且规范了,所以核心代码贴下:

日志方法级注解:Log

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Log {


    String id() default "";

    Class name() default Object.class;

    LogType[] type() default {LogType.SYSTEM};
}

注解实现类Logs

/*******************************
 * 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 annotationType() {
        return null;
    }
}

日志类型:LogType

public enum LogType {

    SYSTEM,
    BUSINESS,
    EXCEPTION,
    ERROR,
    DDL,
    DML,
    CURD,
    ADD,
    REMOVE,
    UPDATE,
    FETCH,
    QUERY,
    LOGIN,
    LOGOUT,
    PWD,
}

日志回调:LogCallBack 

public interface LogCallBack {

    /**
     * 日志回调
     * @param joinPoint 连接点
     * @param log       注解
     * @param result    目标结果
     */
    void execute(ProceedingJoinPoint joinPoint, Log log,Object result);
}

服务端抽象日志切面:AbstractLogAspectJ 

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);
    }




}

提供一个通用日志Model: LogMessage 

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;
    }
}

及其建造者类:LogMsgBuilder 

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);
        };
    }
}

Controller代码实例

// 使用Log注解即可通过切面将日志信息写入ES   
@Log(type = LogType.REMOVE)
@PostMapping("remove/abc")
public ReturnBase removeAbc(@RequestBody @Valid Abc abc) {

    dosomething in here ......
        
}

--------------------------------基础软件部署------------------------------------

什么代码层面展示完毕,下面就是ELK的安装部署与配置

Kafka的安装部署配置这里不表

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

安装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

安装Elastic集群

生成集群内部通信安全证书

  • 生成自签名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

  • 修改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

  • 修改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 是否启动成功

配置nginx

因为kibana但个集群只布了一个节点

新增nginx配置

    server {
        listen          10502;
        server_name     kibana;


        location / {
                proxy_pass http://192.168.0.98:10502;
        }


    }


测试elk大致效果

  • 打开浏览器通过nginx_address:10502访问kibana

SpringCloud + kafka + ELK 搭建微服务日志管理平台_第1张图片

  • 使用用户elastic登录,密码为*********

SpringCloud + kafka + ELK 搭建微服务日志管理平台_第2张图片

你可能感兴趣的:(方案,学习记录,方案实现,ELK,kafka,springcloud,日志管理)