引言
上一篇分享博文《一种基于kafka+storm实现的日志记录方法》,讲述了一种基于大数据实时运算实现的日志记录方式。在文中只是提出了一种技术实现思路,以及整体架构,并且在我所在的项目中已经进行了实践,感兴趣的朋友,可以进一步完善,比如添加权限等,实现一种新日志平台的搭建。
博文发布后,有网友留言希望公开部分源码。今天准备整理下我们已经实现的代码,去掉公司业务部分,做一个简单share,以回应网友要求。本文不再对整体实现流程进行讲解,感兴趣的朋友请直接前往上一遍博文。
代码实现主要分两部分:第一部分是java客户端往kafka写日志消息(生产者);第二部分是storm消费kafka日志消息,归类,批量写入hbase。从hbase查询日志部分比较简单,代码就不提供了。
由于这周末还要准备一个“晋升答辩”,本次分享只整理出来第一部分“java客户端往kafka写日志消息”。
Java写日志消息到kafka
我实现的第一版发送日志消息到kafka是复用的“点击流”日志上报流程,即用nginx+lua实现的http接口,往kafka写消息,当然也有采用nginx+go语言实现的。这种方式适用于做页面埋点,当用户浏览页面产生点击操作时调用该http接口,往kafka写日志,当时主要是想通过这种方式实现点击热力图、注意力热图等。这种方式实现的http接口性能相当优异,而且在支持高并发、高吞吐量方面表现优异,现在在各大电商网站广泛的运用,用户收集用户的行为数据,这些都是做大数据计算、分析、以及智能推荐的基础。
好吧不扯远了,后面有时间再分享下我们做大数据实时计算、以及智能推荐相关实现。既然这种nginx+lua+kafka的方式实现的http接口能支持每天海量的“点击流”日志上报,那它同样能满足“服务器”端的日志记录,而且这点日志量对于该http接口来说简直毫无压力。我的第一版实现很简单,直接在java服务端适用httpclient构造http请求,调用该http接口进行“服务器”端的日志上报。而且正如料想的一样,毫无压力。
这仅仅是我们的第一次尝试,但每次打印日志都需要调用一个http接口,我还是觉得很别扭,而且http接口还是有一定的网络开销。既然这种方式可行,那就可以放弃http接口,直接在java应用服务器端直连kafka发送日志消息,如果是http接口还有一点网络开销的话(10ms-50ms),这种方式对“应用服务器”来说毫无感知(1-2ms),这也是我想要的效果,毕竟只是打印一条日志。我把这个想法告诉我的同事“丹哥”(外号甄子丹),最后把这部分代码实现做成一个jar包,在需要采用这种方式打印日志的系统引入这个jar包,再做一些配置即可。
核心代码讲解
下面我们来看下该jar包的核心代码LogCollectorClient类:
@Component public class LogCollectorClient { private static final Log log = LogFactory.getLog(LogCollectorClient.class); //kafka生产者(京东对kafka做了一些简单封装,简称JDQ) private JDQProducerClient<String, byte[]> producer = null; private boolean HASAUTH = false; //每一批日志量,批量上报日志使用 protected int OFFSET = 500; //spring 读取properties配置文件 @Resource private Environment env; //初始化方法 @PostConstruct private void init() { try { //step1:连接kafka权限验证,公司对kafka做的权限封装,可以根据自己公司kafka具体情况调整 Authentication e = new Authentication(env.getProperty("kafka_key"), env.getProperty("test_token"));//开发、测试环境kafka //step2 设置kafka生成者相关配置属性 Properties pros = new Properties(); pros.setProperty("partitioner.class", env.getProperty("partitioner"));//指定分片策略 pros.setProperty("producer.type", env.getProperty("producer.type")); pros.setProperty("compression.codec", env.getProperty("compression.codec")); pros.setProperty("request.required.acks", env.getProperty("request.required.acks")); //step3 初始化kafka生产者客户端 this.producer = new JDQProducerClient(e, pros); } catch (Exception var4) { log.info("kafaka鉴权初始化失败!"); } } /** * 上报一条单条日志 * @param key * @param type * @param logMap * @throws JDQOverSpeedException * @throws JDQException */ public void sendLogInfo(String key, String type, Map<String, String> logMap) throws JDQOverSpeedException, JDQException { if(StringUtils.isNotBlank(key) && StringUtils.isNotBlank(type) && null != logMap && !logMap.isEmpty()) { this.producer.send(new JDQMessage(key + "_" + type, this.assembleJsonStr(key, type, logMap).getBytes())); } } /** * 转换成json格式上报 * @param key * @param type * @param logMap * @return */ private String assembleJsonStr(String key, String type, Map<String, String> logMap) { StringBuffer valueStr = new StringBuffer(); Iterator logInfo = logMap.entrySet().iterator(); while(logInfo.hasNext()) { Map.Entry entry = (Map.Entry)logInfo.next(); valueStr.append((StringUtils.isNotBlank((String)entry.getKey())?((String)entry.getKey()).replaceAll("&", " "):"") + "=").append(StringUtils.isNotBlank((String)entry.getValue())?((String)entry.getValue()).replaceAll("&", " "):"").append("&"); } LogAssembleInfo logInfo1 = new LogAssembleInfo("key=" + key + "&type=" + type + "&" + valueStr.toString(), DateUtil.getTime()); return JsonUtil.write2JsonStr(logInfo1); } @PreDestroy private void destroy() { if(null != this.producer) { this.producer.close(); } } }
这个类其实很简单,说明如下:
1、采用@Component注解,说明只是一个简单的spring 单例 bean,spring容器启动时注入到容器中。
2、@PostConstruct 注解的init方法,bean初始化时,就会初始化一个kafka生产者对象,我们公司kafka团队对kafka做了简单的封装 JDQProducerClient本质上对应的是kafka的kafka.javaapi.producer.Producer。如果你使用的原生kafka,生产者的初始化方法如下:
public static void main(String[] args) throws Exception { Properties prop = new Properties(); prop.put("zookeeper.connect", "h5:2181,h6:2181,h7:2181"); prop.put("metadata.broker.list", "h5:9092,h6:9092,h7:9092"); prop.put("serializer.class", StringEncoder.class.getName()); Producer<String, String> producer = new Producer<String, String>(new ProducerConfig(prop)); int i = 0; while(true){ producer.send(new KeyedMessage<String, String>("test111", "msg:"+i++)); Thread.sleep(1000); } }
3、关于kafka初始化的相关配置信息放到一个properties的配置文件中,通过spring的Environment环境上下文对象的getProperty()方法获取与连接kafka所以的配置。
4、采用@PreDestroy注解的destroy()方法,这里是应用服务器tomcat停止之前,优雅的自动关闭kafka连接。
5、最后来看下日志上报方法sendLogInfo(),在需要上报日志的类中注入LogCollectorClient对象即可,如下:
@Component public class TestService { @Resource private LogCollectorClient log; public void publish(){ //省略业务代码 //开始上报日志,日志内容放到一个map里 Map<String,String> param = new HashMap<String, String>(); param.put("time", DateTimeUtils.getDateTime()); param.put("logs", "xxx发布活动"); log.sendLogInfo(pageId, SystemConstant.APP_ID, param); } }
sendLogInfo日志上报方法需要三个参数:
第一个是查询key, hbase日志表中rowkey构成部分。比如:这里的pageId,发布页面的id。
第二个是系统id,用于区分hbase日志表,每个系统对应一个固定的常量。
第三个是需要答应的日志内容,考虑到打印的日志可能比较多,这里用一个map存放,也可以改为一个String。
好了,关于java先kafka上报日志的核心类LogCollectorClient讲解完毕。正如前面所说,把LogCollectorClient类打成一个jar包,在需要日志打印的应用系统里引入这个jar包,以及一个kafka的properties配置文件即可。
当然,你可以把kafka需要的配置当做常量写死在jar包中的一个常量类中,这样应用系统只需要一个jar包即可。
优化
最后你还可以对上述LogCollectorClient类做一些优化:
1、比如加一个线程池,把上报日志改为异步上报,在线程中处理kafka·异常。这样即便kafka·出现问题时,也不会影响正常业务,唯一影响的就是日志会丢失。
2、另外如果你的日志量很大,你还可以采用kafka·的批量上报,当日志量达到一定条数后 才调用一次producer的sender方法。
3、也许你已经发现了这里上报的日志内容是json格式,为了更加高效你改为pb格式。
最后需要说明的是:理论上这种日志记录方式可以完全代替传统的日志打印到文件的方式,比如Log4j。但是没有必要,个人觉得一些无关紧要的调试日志还是使用Log4j,对于一些敏感日志或者重要的流水日志,采用这种方式。 Log4j打印日志更简单,基于kafka+storm的更加安全、永久存放、日志更集中(一张hbase表中),二者结合使用天衣无缝。
关于第一部分“java客户端往kafka写日志消息(生产者)”就分享到这里,由于下周还有一个“晋升答辩”需要准备,预祝自己这次晋升能成功吧。第二部分“storm消费kafka日志消息放到hbase”只能缓几天,才能整理出来啦,忘谅解。