ELK日志处理平台搭建

日志处理平台可以分成三个部分:

shipper:日志收集 broker:中间件/队列 indexer:日志存储

日志的功能侧重点:

1.快速定位线上log,快速差错,

2.对日志进行数据处理,提取整理日志中的重要数据

3.提取用户请求整个访问流程的日志,便于分析

        最开始考虑了几种架构方案elk+kafka或者reids+mongodb,但后来根据实际业务场景考虑了一下,目前项目平台每日产生的日志不足1G,往极端方向考虑,假设用户每天都集中在4个小时处理相关业务,即1024/4*60*60=0.07M/S,每秒日志的吞吐量还不到100kb,使用kafka过于重量级,就算考虑业务爆发,日志吞吐量扩充10倍也不过0.7M/S,单台redis完全够用。

        mongodb也比较适合日志存储,但不如elk来的专业,而且还需要手写查询接口对外发布,比较麻烦,检索功能及吞吐量比es查了几个量级,因此不予采用。

        最后采用了slf4J+logstash+redis+logstash+elasticSearch+kibana,这里redis最好做一下主从,毕竟用redis做队列可靠性不是那么高,日志丢了就没了,不像es可以分片,当然如果日志不是那么重要也无所谓了。
        logstash号称支持一切input,日志组件、tcp、udp、各种队列都可以作为日志输入源,logstash支持多输入源,一个logstash实例可以监控整台web服务器。logstash主要分为三部分input、filter、output,input和output格式可以自定义,可以通过codec进行编码或直接使用ruby库解析键值对进行输入输出。filter插件也特别强大,grok插件解析时间或字符串,mutate解析数据格式,同时还支持ruby插件,导入ruby的库,直接在配置文件里用ruby解析数据,可以说是无所不能了。
规范整个日志业务之前第一步就是先把以前的日志格式统一起来,如果日志格式不统一,那下一步的日志解析存储就毫无意义了。elk搭好之后需要应用于多个项目,每个项目日志又分为业务日志、dao日志,也可能会把nginx的请求日志也导入es中,
另外老项目的dao层比较乱,最老的代码直接从mapper文件解析sql运行的,而新代码已经把dao层封装起来了,提供一些关键字拼接的api,最后也是通过mapper文件发送sql到db里执行的。这里会有一个兼容性的问题,即老的dao层日志是直接用

mybatis的日志模块输出的,和业务日志分成两个文件。而新的dao层日志应该和业务日志整合到一个文件,按天分割。所以这里在导入es时要考虑也要为mybatis日志模块输出的log配置映射模板。

最终采用的log格式为:
调用时间-log等级-类名-web容器线程ID-hostName/ip-appName--requestIP-requestID-accountID-操作-出入参数
hostName/IP用于定位线上的web实例,当hsostName未设置时取服务器的局域网ip
appName用于区分多个项目
requestID是为用户请求生成的唯一ID,用于追踪用户的整个访问流程。生成策略为request的hashcode+当前时间戳&16进制

上面的log格式中需要手动填入的只有log操所和出入参数,其他均集成到log4j2中。

        log4j里通过代码方式动态追加参数可以通过重写patternConverter实现:
public class ExtPatternConverter extends PatternConverter {
    private String cfg;
    @Autowired
    public ExtPatternConverter(FormattingInfo fi, String cfg) {
        super(fi);
        this.cfg = cfg;
    }
//重写patternConverter的日志参数转换方法
    @Override
    protected String convert(LoggingEvent event) {
        System.out.println("start char convert");
        Map<String, Object> valueMap = ExtPatternParser.TH_LOCAL.get();
        if (valueMap != null) {
            Object value = valueMap.get(cfg);
            if (value != null) {
                return String.valueOf(value);
            }
        }
        return "no exist";
    }
}
public class ExtPatternLayout extends PatternLayout {
    public ExtPatternLayout(String pattern) {
        super(pattern);
        System.out.println("pattern is null ???:" + pattern);
    }

    public ExtPatternLayout() {
        super(DEFAULT_CONVERSION_PATTERN);
    }
//获取pattern标签的日志格式,包含了我们自定义的字符
    @Override
    protected PatternParser createPatternParser(String pattern) {
        System.out.println("pattern is:" + pattern);
        return new ExtPatternParser(pattern == null ? DEFAULT_CONVERSION_PATTERN : pattern);
    }
}
public class ExtPatternParser extends PatternParser {

    public static final ThreadLocal<Map<String, Object>> TH_LOCAL = new ThreadLocal<Map<String, Object>>(){
        @Override
        protected HashMap<String, Object> initialValue() {
            return new HashMap<String, Object>();
        }
    };
//传入自定义日志参数
    public static void setCurrentValue(String key, Object value) {
        Map<String, Object> map = TH_LOCAL.get();
        map.put(key, value);
    }
    public ExtPatternParser(String pattern) {
        super(pattern);
    }
//把自定义的参数转换进去
    @Override
    protected void finalizeConverter(char c) {
        if (c == '#') {
            System.out.println("char handler");
            String option = super.extractOption();
//传入自定义的patternConverter
            addConverter(new ExtPatternConverter(this.formattingInfo,option));
            currentLiteral.setLength(0);
        } else {
            super.finalizeConverter(c);
        }
    }
}

        但在log4J2中该方法已经不行了,但可以通过log4J2的插件来实现,反而简单了一些:

@Plugin(name = "LogIdPatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"y", "logParams"})
@Component
public class LogParamAppender extends LogEventPatternConverter {
    private static final LogParamAppender INSTANCE = new LogParamAppender();
    public static final String SPLIT ="|";

    public static LogParamAppender newInstance(final String[] options) {
        return INSTANCE;
    }

    private LogParamAppender() {
        super("logParams", "logParams");
    }

    @Override
    public void format(LogEvent event, StringBuilder toAppendTo) {
        toAppendTo.append(RequestIDBuilder.buildHostName()).append(SPLIT).append(RequestIDBuilder.buildAppName()).append(SPLIT).append(RequestIDBuilder.buildRequestID());
    }
}

      “y”是在log4j2的xml配置文件里的patternLayout标签里自定义的pattern字符,运行时会将format方法中的toAppendTo参数替换进去。注意静态变量INSTANCE和newInstance方法一定要提供。

        另外有一点是在生成requestID时,需要注意requestID是为每一个进入web server的请求生成一个唯一ID,是和request请求绑定的,所以这里可以先为request生成一个唯一ID,然后绑定到threadLocal上,需要注意的是,threadLocal是线程变量副本,正常情况下thread与request请求是一对一的关系,但由于web server的线程池的线程复用以及thread的生命周期较长,可能会产生一个thread同步处理多个request的情况,导致生成的requestID重复,无法正常定位用户的请求流程。所以可以使用一个与request生命周其较为吻合的filter,在dofilter方法中生成requestID并赋值到threadLocal中,destory时销毁该requestID。threadLocal中的变量销毁也要注意其内存泄漏的问题,threadLocal中的threadLocalMap的entry虽然实现了弱引用,但在可能会产生threadLocalMap中的key被GC掉为null,而value依然存在的情况,而如果这时该线程迟迟得不到结束,很可能会导致内存泄漏,因此threaLocal使用完成后一定要注意调用remove方法销毁。

threadLocal内存泄漏分析

总体的架构如下:


第一级logstash配置文件:

input {
    file {
        path => [
           "/usr/local/apache-tomcat-8.5.31/logs/xxx-log/*.log"
           #start_position => "beginning"
           #sincedb_path => "/home/es/sincedb/apk"
        ]
    codec => multiline {
        #need deal with java stack exception with multiline
                pattern => "^\s"
        #negate:is used if cannot match this pattern,what:how to merge multiline message,like "previous" or "next"
                negate => false
                what =>"previous"
        #this is a problem in logstash lead to the end of this message like java stack exception cannot append to previous event,it will be print with next message
        #so you have to set auto_flush time,force logstash clear it buffer
                auto_flush_interval => 10
    }

 }
#  stdin {
#codec => plain{ charset => "GBK" }
# }
#tcp {
# host => "0.0.0.0"
# port => 9520
# #codec => plain { charset => "GBK" }
# codec => json
#}
}
filter {
mutate {
                #add_field => {
                #       "type"=>"daoLog"
                #       }
                remove_field =>["tags"]
        }

#json {
 #       source => "message"
  #      #add_field => ["type", "%{dtype}"]
   #     remove_field => [ "endOfBatch", "loggerFqcn", "threadPriority" ]
   # }
}
 output {
    redis {
        host => localhost  
        port => 6379             
        db => 8                   
        data_type => "channel"    
        key => "logstash_db_0"
   #    codec => plain{ charset => "GBK" }
   #     codec =>rubydebug
   }
stdout {
 codec =>rubydebug
}
}

        java堆栈异常可以用multiLine插件处理,通过正则将日志首行以及以下的堆栈日志合并为一条message,需要注意可能会产生第一次请求不发送堆栈日志,第二次请求才追加堆栈日志的情况,原因是堆栈日志被存储在logstash的buffer区中未发送,而是随着第二条messge发送过去了,因此可以手动清空buffer区,强制发送日志。

    logstash也可以通过tcp的形式直接发送日志信息,但需要在log4J2配置文件中配置socket根节点,之后日志将不会被追加到文件中,而是直接通过tcp发送到logstash。

    stdout输出中的codec=>rebudebug即通过ruby将log输出为键值的格式。便于控制台调试。

input {
    #file {
    #    path => [
   #        "/usr/local/apache-tomcat-8.5.31/logs/icu-log/*.log"
     #      #start_position => "beginning"
     #      #sincedb_path => "/home/es/sincedb/apk"
     #   ]
        #        }
    redis {
        host => "192.168.1.109"  
        port => 6379
        db => 8
        data_type => "channel"
        key => "logstash_db_0"
   #    codec => plain{ charset => "GBK" }
   #     codec =>rubydebug
   }
#  stdin {
#codec => plain{ charset => "GBK" }
# }
#tcp {
# host => "0.0.0.0"
# port => 9520
# #codec => plain { charset => "GBK" }
# codec => json
#}
}
filter {
mutate {
                split => ["message", "|"]
                add_field => {
                        "time"=>"%{[message][0]}"
                }
                add_field => {
                        "logLevel"=>"%{[message][1]}"
                }
                add_field => {
                        "location"=>"%{[message][2]}"
                }
                add_field => {
                        "logType"=>"%{[message][3]}"
                }
                add_field => {
                        "webThreadID"=>"%{[message][4]}"
                }
                add_field => {
                        "hostName/IP"=>"%{[message][5]}"
                }
                add_field => {
                        "appName"=>"%{[message][6]}"
                }
                add_field => {
                        "requestIP"=>"%{[message][7]}"
                }
                add_field => {
                        "requestID"=>"%{[message][8]}"
                }
                add_field => {
                        "log"=>"%{[message][9]}"
                }

                remove_field =>["message"]
        }
#json {
 #       source => "message"
  #      #add_field => ["type", "%{dtype}"]
   #     remove_field => [ "endOfBatch", "loggerFqcn", "threadPriority" ]
   # }
}
output {
      stdout { codec =>rubydebug }
    elasticsearch { 
            #template => "/usr/local/logstash-2.3.2/conf/mapper-template.json"
            template_name => "crawl"
            #codec => json
            hosts => "127.0.0.1:9200"
        }
}

    mutate插件可以直接从messge中切割字符串取值并拼成字段发送给es。完成后可以把原有的message字段remove掉。这是业务日志和dao层日志的处理。至于nginx的access日志,可以直接用grok插件匹配,grok插件提供了大量封装好的字段类型,直接通过表达式匹配即可。

    默认从logstash拼成字段发送过去后,es自动根据动态模板映射你的数据,字段类型也是自动推导,但有时候数据的同一字段类型不同可能导致异常,所以也可以手写映射模板,根据你自己的意愿映射数据。但如果日志格式有多个规则,需要你根据不同规则添加不同字段以在logstash里区分开选择不同的模板。记得关闭es的自动映射manage_template =>false,或者用template_overwrite覆盖自动生成的模板。

output {
        if [type] == "log_01" {
                elasticsearch {
                        cluster => 'elasticsearch'
                        host =>         'x.x.x.x'
                        index => 'log_01-%{+YYYY-MM-dd}'
                        port => '9300'
                        workers => 1
                        template => "/data/logstash/conf/template_01.json"
                        template_name => "template_01.json"
                        template_overwrite => true
                }
        }
        if [type] == "log_02" {
            elasticsearch {
                        cluster => 'elasticsearch'
                        host =>         'x.x.x.x'
                        index => 'log_02-%{+YYYY-MM-dd}'
                        port => '9300'
                        workers => 1
                        template => "/data/logstash/conf/template_02.json"
                        template_name => "template_01.json"
                      template_overwrite => true
                }
        }
}

Logstash及ElasticSearch安装:

https://blog.csdn.net/u012373815/article/details/51029826

ES默认禁止root用户启动的解决方法(ES2一下的版本没有这个问题):

https://blog.csdn.net/mengfei86/article/details/51210093

1.ES启动后可以在本地访问但无法通过公网访问解决方法:

在elasticsearch.yml配置文件中打开地址和端口号设置,否则ES默认以localhost运行

2.使用kibana的话注意其版本号与ES要对应,安装后修改kibana.yml中的ip/端口号/es地址即可运行

3.elasticSearch后台运行:

bin目录下执行./elasticSearch -d

查看是否启动成功:ps aux|grep elasticSearch

kibana后台运行:

nohup bin/kibana &

使用&可以ctrl+c停止,如果有大量elk实例的话最好写启动脚本。

4.es和kibana的安全问题:当时es被设计时是被考虑在内网环境下使用的,如果你的es部署在公网下,一定记得加密,可以使用es security插件(原shield),但是只有一个月试用期,过期后对集群收费,但对基本的api似乎没有影响。shield安装部署,但建议不要将es部署在公网上,参考es安全加固

5.如果es采用了每天动态生成索引的方法收集日志,记得定期删除不常用的索引,比如只索引三个月以内的日志,可以用curator插件实现。也可以用python做成定时任务,py脚本如下:

#!/usr/bin/env python  
# coding: utf-8  
# author: yd  
  
import urllib  
import urllib2  
import re  
import datetime  
import time  
  
def http_get(url_get):                                      #获取elasticsearch的所有索引  
    request_get = urllib2.Request(url_get)  
    response_get = urllib2.urlopen(request_get).read() 
    print("get elasticSearch log server indexes")
    return response_get  
  
def match(response_get): 
    pattern = re.compile(r'\d+\.\d+\.\d+') 
    res_match = pattern.findall(response_get) 
    date_now = datetime.date.today()                       #获取当前日期  
    days_before_30 = date_now - datetime.timedelta(days=30)    #获取30天前的日期  
    date_format = days_before_30.__format__('%Y.%m.%d')         #对日期进行格式化输出,例:2018.04.23  
    if date_format in res_match: 
        print("delete index: logstash-"+date_format)
        http_delete(date_format) 
        print("delete index success")
    else: 
        pass 
  
def http_delete(url_delete_date):                                                  #删除30天前的索引  
    url_delete = 'http://localhost:9200/logstash-%s?pretty' % (url_delete_date) 
    request_delete = urllib2.Request(url_delete)  
    request_delete.get_method = lambda:'DELETE'                   #设置HTTP请求方式  
    response_delete = urllib2.urlopen(request_delete).read()  
    return response_delete  
  
if __name__ == '__main__':  
    print("start delete index more than 30 days")
    url_get = 'http://localhost:9200/_cat/indices?v'  
    match(http_get(url_get))  
	
#cron定时
#0 0 1 1/1 * ? * root py py path> py log path

猜你喜欢

转载自blog.csdn.net/hfismyangel/article/details/80196586