我们的服务使用的是springcloud,出现问题的流程大概架构是,message_producer->kafka->message_consumer->elasticsearch。
我们是一个电商项目,服务里会产生各种商品更新的消息,如价格更新,品类更新等。这些消息通过kafka被专门处理商品更新消息的服务处理,并将处理后的商品信息数据同步到elasticsearch。不要问我为什么用这种架构,这种组件,问了就是历史问题。
故事发生在一个漆黑的夜晚,我们的一个大客户突然告诉我们我们从es拿到的商品价格居然不是最新的,影响了他们下单。这个问题就比较严重,那么就开始排查。
我们
1:首先查看es里商品信息的更新时间,发现更新时间是老的,而且es服务没有明显的错误日志,所在服务器的cpu和内存等资源使用率并不高。那么很有可能就是messge_consumer没有及时调es的API去更新数据。
2:我们在查看message_consumer服务的时候,发现其系统资源占用也不高,但是发现kafka里相应topic的消息堆积达到了几百万条,好吧,问题出现了,就是message_consumer消费消息不及时造成的。
问题找到了,然后开始解决问题。查看代码发现,kafka的消息消费居然是单线程阻塞的:一个线程每次从kafka获取到一条消息,消息中包含商品id;然后这个线程调用其他各个微服务的接口去获取商品的各种信息进行组装,然后再调用es的API去将组装好的数据写入es,整个过程结束后这个线程再开始处理新的消息。为了解决这个问题,没错,我们首先想到了异步多线程。然后我们使用了线程池,在message_consumer服务中使用多个线程去读取这个topic的消息,然后进行相应的后续处理。测试效果展示这样的改动确实有效,消息的处理速度确实有了很大的提升。
但是接下来又有问题出现了,每个商品消息的处理都需要调用其他的微服务来获取商品的信息进行拼装,message_consumer中创建了大量的线程来处理消息,cpu使用率飙升,但是message_consumer服务调用的其他服务的cpu却没有明显的变化,这明显不太合理,通过查日志发现,被调用微服务的处理线程一直维持的在较低的数量,和我们设置的message_consumer的处理消息的线程池的线程数量差距较大,这不太科学。后来通过讨论查资料发现了问题的所在,是springcloud框架中服务间调用组件feign的问题,feign为了避免高并发的问题,自己维护了一个线程池来处理这些调用,默认是10。怪不得其他被调用的微服务处理线程数没有明显变化,原来被feign进行了削峰。那么我们调大一下feign的默认线程数量就行,我们默认和处理消息的线程数量调成了一样大小。(这里注意,不同的FeignClient维护的是各自独立的线程池,即C服务调用A服务的feign线程池和调用B服务的Feign线程池不是一个线程池)。这样调整后,效果显著,平均每个处理消息线程的处理时间又缩短了一些。
但是服务上线后又有了新问题,message_consumer服务在线上有两个实例,但是只有一个服务在处理kafka消息,另一个却在闲置。原来,这两个message_consumer属于同一个topic的同一个group,而这个topic的partition却只有一个。因为kafka中每个partition只能被同一个分组下的一个consumer消费,所以consumer_A订阅了partition_0,那么所属同一group下的consumer_B就不能订阅partition_0,因此造成了其中一个服务的闲置。我们动态的增加这个topic的partition就可以了,应该保证partition的数量不小于通分组下consumer的数量。
经过上面的处理,消息处理速度确实有了很大的提高。当然我们还可以对es进行优化,还可以进行单一变批量的优化,这里我们以后再说。