最近使用kafka stream来给系统日志做整合统计。我们用elk采集和统计log,但是由于系统log量太大,一天有5G,这个elk中的 elasticsearch 节点就需要存储5G原始数据并且进行索引。必然会占用大量硬盘。所以使用kafka stream来做一个实时聚合,把每5分钟的log数据聚合成一条summary数据,再把聚合结果放到elk中。
然而聚合结果不是很精确。对比原始log,发现聚合完后的结果和原始数据大概有±‰7 ~ %4的误差。可能的原因有两个:
- Consumer group 频繁 rebalance 导致重复处理某些log
- 消息的时间戳不是真正写log的时间
第一个问题的原理是: kafkaconsumer 调用 poll 方法来从kafka server 获取消息,同时 poll 方法发送心跳给coordinator(问题: coordinator 是指kafka server 还是指某一个consumer?),说明自己还活着。那么如果一个consumer 调用两次 poll 方法的时间间隔太长,超过了 max.poll.interval,将导致coordinator认为这个consumer已经死亡,那么coordinator会进行group rebalance,把当前consumer的partition分配给别的consumer。这样当前consumer在处理数据后进行commit时会抛出offsetcommit failure异常。然后已经处理完的那些数据会被发给别的consumer重复处理。
解决办法: 修改consumer配置项,max.poll.records。 这项在consumer中默认是500,而在kafka stream中默认是1000.意味着如果kafkastream 处理1000条数据太慢,超过了max.poll.interval=6s那么将导致consumerrebalance,最终导致0-1000条的数据重复计算。
解决办法是把max.poll.records设置为300
第二个问题的原理是:从kafka 0.9 开始,kafka 会给每条消息打一个时间戳,这个时间戳可以用于kafka strem中基于时间窗口聚合的时间点。也就是把落在同一个时间窗口的数据聚合到一起。那么这个时间戳可以是kafka 给打的时间戳(问题:是kafkaserver打的还是kafka consumer打的?),也可以是消息体中的某一个时间字段。
那么之前我是使用kafka 打的时间戳来做时间窗口。因为我认为只要那边写一行log,kafka stream就进行处理,应该是实时的数据, kafka打的时间戳应该就等于log时间,没必要再去解析log时间。而事实上,从kafkastream的日志中看到,这个时间戳比log时间滞后了20多分钟。那么很可能是由于kafka stream处理慢导致log堆积在kafka server上,等到真正进入kafka stream 处理时已经在log产生之后的20分钟了。
解决办法是自己实现kafkastream API 提供的 TimeExtractor 接口,从消息中提取真正的logTime作为时间戳。