从零到日志采集索引可视化、监控报警、rpc trace跟踪-自定义日志框架

来源:互联网 发布:可以打鼓的软件 编辑:程序博客网 时间:2024/06/01 03:58
github开源  欢迎一起维护~~

这里不去分析哪种日志采集方式最好,不对flume、logstash等其他软件做比较,我这边只会介绍适合我们公司使用的系统。由于我们公司部署的项目采用的都是java或者scala项目,日志的框架是log4j或者logback。日志的采集应该是不侵入或者最少侵入对接系统,所以我们使用轻量级的方式,自定义log4j和logback的kafka appender,将log输出的日志同时走kafka appender send到kafka中。
  1. kafka设计
    为了使kafka达到最高的性能,我们将kafka的topic划分成多个partition(具体的个数和对接的系统产生的日志量有关,我们这边是9个),那么如何保证对接的时候在足够多的情况下能够较为均匀得分散到这9个partition呢,还有个问题, 如何保证对接的系统日志产生时进入kafka有序(有序的话消费的时候才有序,最后日志可视化的时候才不会乱序)。这里我们需要重新定义kafka的Partitioner,还需要重新设计kafka的key,我们将对接的系统的名字抽象为app,将部署的节点host名字抽象为host,将这两个值作为key进行分区,不仅能保证均匀分散到9个partition,而且能确保每个app在特定的host中是有序的。部分代码如下:
    Partitioner分区类:
    public class KeyModPartitioner implements Partitioner {    @Override    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);        int numPartitions = partitions.size();        int partitionNum = 0;        try {            partitionNum = Utils.murmur2(keyBytes);        } catch (Exception e) {            partitionNum = key.hashCode();        }        return Math.abs(partitionNum % numPartitions);    }    @Override    public void close() {    }    @Override    public void configure(Map<String, ?> configs) {    }}
    Key构造接口和实现:
    public interface KeyBuilder<E> {    /**     * 生成ProducerRecord需要的key参数     * @param e log event, ch.qos.logback.classic.spi.ILoggingEvent     * @return     */    byte[] build(E e);}
    public class AppHostKeyBuilder<E> extends ContextAwareBase implements KeyBuilder<E> {    private byte[] appHost;    @Override    public void setContext(Context context) {        super.setContext(context);        String host = context.getProperty(CoreConstants.HOSTNAME_KEY);        String app = context.getName();        appHost = ByteBuffer.allocate(4).putInt(new StringBuilder(app).append(host).toString().hashCode()).array();    }    /**     * 生成keykey规则app+hostbyte[]     * @param e log event, ch.qos.logback.classic.spi.ILoggingEvent     * @return     */    @Override    public byte[] build(E e) {        return appHost;    }    public byte[] getAppHost() {        return appHost;    }    public void setAppHost(byte[] appHost) {        this.appHost = appHost;    }}
    为了使kafka的appender能够达到最大的性能,我们不能每次send的时候都要实例KafkaProducer,我们需要构造一个单例,并且是懒加载模式,代码如下(double check实现KafkaProducer的懒加载):
    public class LazySingletonProducer {    private static volatile Producer<byte[], String> producer;    /**     * 私有化构造方法     */    private LazySingletonProducer() {    }    /**     * 实例化     * @param config     * @return     */    public static Producer<byte[], String> getInstance(Map<String, Object> config) {        if (producer == null) {            synchronized(LazySingletonProducer.class) {                if (producer == null) {                    producer = new KafkaProducer<byte[], String>(config);                }            }        }        return producer;    }    /**     * 是否初始化     * @return     */    public static boolean isInstanced() {        return producer != null;    }}

  2. zookeeper注册中心设计
    假设日志已经采集进入了es,那么前端如何展示,比如实时日志滚屏,那么我们就需要指定host和app,具体的含义就是将部署在某台机器上的一个应用的日志进行滚屏,类似于tail -f操作(PS: 同一个host不可部署相同app名字的系统),那么我们可以将app和host信息写入到zk,然后监听zk节点变化,将数据相应得存放到mysql中。
    假设现在需要监听对接的系统上线和下线情况,同时要监控我们自己的日志采集器是否存活,那么也可以将该数据写入到zookeeper,然后监控节点的变化进行实时报警通知。
    zookeeper节点设计如下图: 

    1. kafkaAppender初始化向zk进行app节点注册,并写入相关的信息
    2. kafkaAppender发生异常暂停工作会向app节点写入相关信息,以便监控系统能够实时感知并发送报警
    3. app正常或者异常退出后,zk中的app临时节点会消失,shutdownhook会正常运行,监控系统能够实时感知并发送报警(这里就需要我们在自定义的log appender中写好相应的hook,防止对接系统无法正常释放资源,项目不要用kill -9 pid,应该使用kill pid)
    4. zk中有永久节点用来记录app的最近一次部署信息
  3. 自定义log appender
    1. logback
      以下是LayoutEncoder
      public class KafkaLayoutEncoder<E> extends ContextAwareBase implements LifeCycle {    // layout    private Layout<E> layout;    // 编码,默认utf-8    private Charset charset;    private boolean started = false;    private static final Charset UTF8 = Charset.forName("UTF-8");    public String doEncode(E event) {        return this.layout.doLayout(event);    }    @Override    public void start() {        if (charset == null) {            addInfo("no set charset, set the default charset is utf-8");            charset = UTF8;        }        started = true;    }    @Override    public void stop() {        started = false;    }    @Override    public boolean isStarted() {        return started;    }    public Layout<E> getLayout() {        return layout;    }    public void setLayout(Layout<E> layout) {        this.layout = layout;    }    public Charset getCharset() {        return charset;    }    public void setCharset(Charset charset) {        this.charset = charset;    }}
      具体代码不再贴出,主要说明下:
      构造方法中使用自定义的Partitioner,并注册好相应的shutdownhook
      在start方法中校验下最基本的参数是否完整和正确,初始化zk节点信息
      @Overridepublic void start() {    // xml配置校验    if (!this.checkNecessaryConfig()) {        addError("necessary config is not set, kafka appender is not started");        return;    }    super.start();    // 添加logback shutdown hook, 关闭所有的appender, 调用stop()方法    shutdownHook.setContext(this.getContext());    Runtime.getRuntime().addShutdownHook(new Thread(this.shutdownHook));    // 初始化zk    this.zkRegister = new ZkRegister(new ZkClient(this.zkServers, 60000, 5000));    // 注册永久节点用于历史日志查询    this.zkRegister.create(Constants.SLASH + this.app + Constants.SLASH + this.host, NodeMode.PERSISTENT);    this.zkRegister.getClient().writeData(Constants.ROOT_PATH_PERSISTENT + Constants.SLASH + this.app + Constants.SLASH + this.host,            this.mail + Constants.SEMICOLON + SysUtil.userDir);    // 注册临时节点用于日志滚屏    this.zkRegister.getClient().createPersistent(Constants.ROOT_PATH_EPHEMERAL + Constants.SLASH + this.app, true);    this.zkRegister.create(Constants.SLASH + this.app + Constants.SLASH + this.host, NodeMode.EPHEMERAL,            Constants.APPENDER_INIT_DATA + Constants.SEMICOLON + SysUtil.userDir);}
      在stop方法中关闭kafkaProducer,关闭zkClient等
      append中的方法:
      @Overrideprotected void append(E e) {    if (!isStarted()) {        return;    }    final String value = System.nanoTime() + Constants.SEMICOLON + this.encoder.doEncode(e);    final byte[] key = this.keyBuilder.build(e);    final ProducerRecord<byte[], String> record = new ProducerRecord<byte[], String>(this.topic, key, value);    LazySingletonProducer.getInstance(this.config).send(record, new Callback() {        @Override        public void onCompletion(RecordMetadata recordMetadata, Exception e) {            // TODO: 异常发生如何处理(目前使用RollingFileAppender.java中的方法)            if (null != e) {                // 如果发生异常, 将开始状态设置为false, 并每次append的时候都先check该状态                started = false;                addStatus(new ErrorStatus("kafka send error in appender", this, e));                // 发生异常,kafkaAppender 停止收集,向节点写入数据(监控系统会感知进行报警)                if (flag.get() == true) {                    zkRegister.write(Constants.SLASH + app + Constants.SLASH + host, NodeMode.EPHEMERAL,                            String.valueOf(System.currentTimeMillis()) + Constants.SEMICOLON + SysUtil.userDir);                    flag.compareAndSet(true, false);                }            }        }    });}
      这里处理比较特殊,假设有一种情况是我们的kafka集群完全挂掉,那么在send的时候必然会block或者说影响到对接系统的正常运行,那么我们就需要在发生异常的时候停止日志的采集以确保对接的系统能够毫无感知毫无影响得运行。这边的处理是遇到异常即停止采集,其实还有更好的方法,以后再给出。
    2. log4j
      log4j和logback基本的思路完全一致,就是log4j比较坑,自定义appender比较难写
      由于kafka初始化和log4j并未有一个先后关系,所以在kafka未初始化完成的时候log已经开始写了,为了使日志一条不丢,需要用一个queue记录下这段时间内的数据,然后统一send到kafka,提供下部分代码:
      // kafka producer是否正在初始化private volatile AtomicBoolean isInitializing = new AtomicBoolean(false);// kafka producer未完成初始化之前的消息存放的队列private ConcurrentLinkedQueue<String> msgQueue = new ConcurrentLinkedQueue<String>();
      /** * kafka send * @param value */private void send(String value) {    final byte[] key = ByteBuffer.allocate(4).putInt(new StringBuilder(app).append(host).toString().hashCode()).array();    final ProducerRecord<byte[], String> record = new ProducerRecord<byte[], String>(this.topic, key, value);    LazySingletonProducer.getInstance(this.config).send(record, new Callback() {        @Override        public void onCompletion(RecordMetadata recordMetadata, Exception e) {            // TODO: 异常发生如何处理(直接停掉appender)            if (null != e) {                closed = true;                LogLog.error("kafka send error in appender", e);                // 发生异常,kafkaAppender 停止收集,向节点写入数据(监控系统会感知进行报警)                if (flag.get() == true) {                    zkRegister.write(Constants.SLASH + app + Constants.SLASH + host, NodeMode.EPHEMERAL,                            String.valueOf(System.currentTimeMillis()) + Constants.SEMICOLON + SysUtil.userDir);                    flag.compareAndSet(true, false);                }            }        }    });}
      /** * 发送msg * @param msg */private void sendMessage(String msg) {    if (!LazySingletonProducer.isInstanced()) {        if (this.isInitializing.get() != true) {            this.isInitializing.compareAndSet(false, true);            this.initKafkaConfig();            this.isInitializing.compareAndSet(true, false);            this.send(msg);        } else {            this.msgQueue.add(msg);        }    } else if (this.msgQueue.size() > 0) {        if (LazySingletonProducer.isInstanced() ) {            this.msgQueue.add(msg);            while (this.msgQueue.size() > 0) {                this.send(this.msgQueue.remove());            }        }    } else {        this.send(msg);    }}
      @Overrideprotected void append(LoggingEvent event) {    if (closed) {        return;    }    this.sendMessage(this.getMessage(event));}

    3. 其他日志框架
      只要遵循以上思路即可,基本可以轻松的开发出组件
      特别说下logback和log4j,这2个框架很明显logback的appender比较好写,而且实际测下来logback的效率和稳定性高于log4j,所以还是尽量使用logback比较合适。
3 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 平板电脑密码锁密码锁死怎么办 小米平板黑屏按键亮怎么办 小米平板黑屏没反应怎么办 小米平板一刷黑屏了怎么办 小米4卡死了怎么办啊 苹果平板电脑开不了机怎么办 ipad开不了机怎么办都是黑屏 手机突然黑屏了死机状态怎么办 苹果平板死机不能重启怎么办 韩众平板死机了怎么办 苹果平板输入密码多次停用怎么办 ld密码被停用了怎么办 档案被自己丢了怎么办 手机在厂里丢了怎么办 在厂里借工具丢了怎么办 导出的考勤没有姓名怎么办 退休时档案丢了怎么办 职工与企业没有劳资怎么办 去大学报道的档案袋丢失怎么办 档案入学毕业年份写错怎么办 从事业单位辞职后人事档案怎么办 老师辞职不给批怎么办 公办教师去私立学校档案怎么办 辞职后档案不给怎么办 档案不小心拆了怎么办 退休职工档案年龄有涂改怎么办 养老金原始档案找不到怎么办退休 寄辞职信不接收怎么办 公司不给办离职怎么办 离职手续表填写错误怎么办 退货少退了个配件怎么办 小米8拖影严重怎么办 被兼职中介骗了怎么办 被兼职中介坑了怎么办 人在工厂宿舍死了怎么办 事业单位在编人员开除后社保怎么办 因违规无法进群怎么办 微信号违规进不了群怎么办 工作跨省调动社保怎么办 工作中看到别人违反规定应该怎么办 深户调令过期了怎么办