这里不去分析哪种日志采集方式最好,不对flume、logstash等其他软件做比较,我这边只会介绍适合我们公司使用的系统。由于我们公司部署的项目采用的都是java或者scala项目,日志的框架是log4j或者logback。日志的采集应该是不侵入或者最少侵入对接系统,所以我们使用轻量级的方式,自定义log4j和logback的kafka appender,将log输出的日志同时走kafka appender send到kafka中。
- 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(); } /** * 生成key,key规则app+host的byte[] * @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; } }
- zookeeper注册中心设计
假设日志已经采集进入了es,那么前端如何展示,比如实时日志滚屏,那么我们就需要指定host和app,具体的含义就是将部署在某台机器上的一个应用的日志进行滚屏,类似于tail -f操作(PS: 同一个host不可部署相同app名字的系统),那么我们可以将app和host信息写入到zk,然后监听zk节点变化,将数据相应得存放到mysql中。
假设现在需要监听对接的系统上线和下线情况,同时要监控我们自己的日志采集器是否存活,那么也可以将该数据写入到zookeeper,然后监控节点的变化进行实时报警通知。
zookeeper节点设计如下图:
- kafkaAppender初始化向zk进行app节点注册,并写入相关的信息
- kafkaAppender发生异常暂停工作会向app节点写入相关信息,以便监控系统能够实时感知并发送报警
- app正常或者异常退出后,zk中的app临时节点会消失,shutdownhook会正常运行,监控系统能够实时感知并发送报警(这里就需要我们在自定义的log appender中写好相应的hook,防止对接系统无法正常释放资源,项目不要用kill -9 pid,应该使用kill pid)
- zk中有永久节点用来记录app的最近一次部署信息
- 自定义log appender
- logback
以下是LayoutEncoderpublic 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节点信息@Override public 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中的方法:@Override protected 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或者说影响到对接系统的正常运行,那么我们就需要在发生异常的时候停止日志的采集以确保对接的系统能够毫无感知毫无影响得运行。这边的处理是遇到异常即停止采集,其实还有更好的方法,以后再给出。 - 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); } }
@Override protected void append(LoggingEvent event) { if (closed) { return; } this.sendMessage(this.getMessage(event)); }
- 其他日志框架
只要遵循以上思路即可,基本可以轻松的开发出组件
特别说下logback和log4j,这2个框架很明显logback的appender比较好写,而且实际测下来logback的效率和稳定性高于log4j,所以还是尽量使用logback比较合适。
- logback