首先,描述一下环境,简单的web服务,关键日志写入kafka,要求qps达到单机10K即可。
后面将遇到的问题、解决方案和原理记录如下:
1、内存占用过大,虽然jvm的堆内存设为1G,但进程实际内存使用量达到了12G
解决方案:程序中使用了kafka,new出kafka producer来向kafka中写日志,调整kafka参数解决,调大了batch.size和partition
原理:原因还是batch.size和partition设置过小,导致要发送的数据包过多,都堵在了producer的机器上,而kafka producer又使用了zero copy技术,使得占用了大量的内核态内存无法释放,而内核态内存又不是堆内存,不由用户控制,所以出现了进程占用内存过大的情况,kafka版本为0.8.2.2。(其实这里还存在一点疑问,网上说kafka通信时使用了zerocopy,但我实际跟进代码时,producer使用的是ByteBuffer.allocate,也就是说使用的是堆内存,而没有调用ByteBuffer.allocateDirect创建非堆内存,与网上说的有冲突。如果哪位朋友知道这个问题的原理,还请赐教)
2、长时间负载高的情况下,程序性能下降明显,查看GC情况,发现频繁进行full gc
解决方案:通过使用jmap -dump将内存中的对象信息导出到文件,使用jhat命令分析,查出是一个kafka callback对象过多,而这个Callback对象包含了一些内容较长的字符串类型的私有变量,导致字符串占了较多内存,而kafka的producer有缓存机制,就会导致Callback对象占据内存无法释放,后来通过删除了Callback的私有变量,将Callback变为单例,来减少了内存占用
3、多线程丢数据
解决方案:由于有实时性要求,所以不能阻塞,只能通过加大队列长度和子线程的数量的方式
原理:主线程向子线程分配任务时,未自己管理BlockingQueue,而是交由操作系统自己管理,向子线程分配任务时,直接分配Callable对象
// 创建线程池
ExecutorService pool = newThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, TimeUnit.MILLISECONDS,
newLinkedBlockingQueue<>(queueCapacity));
// 向子线程分配任务
pool.submit(() ->producer.send(new ProducerRecord<>(topic, null, record), kafkaCallback));
若考虑使用阻塞的方式,则可修改为自己控制BlockingQueue,若不想使用阻塞的方式,则可加大queueCapacity和threadPoolSize
4、日志使用log4j记录和写kafka性能差别过大,写日志文件达到3000qps,写kafka能达到11Kqps
解决方案:改为使用log4j 2.x,或使用kafka
原理:通过跟进log4j的代码,发现log4j 1.x中使用了很多sychronized代码,导致log4j多线程下性能问题严重,改为log4j 2.x可使qps提高到10K