最近在和小伙伴们做充电与通信程序的架构迁移。迁移前的架构是,通信程序负责接收来自充电集控设备的数据实时数据,通过Thrift调用后端的充电服务,充电服务收到响应后放到进程的Queue中,然后在管理线程的调度下,启动多线程进程数据处理。
随着业务规模的不断扩大和对系统可用性的逐步提高。现在这个架构存在很多的问题,比如:
1.充电服务重启,可能会丢数据。
2.充电服务重启会波及影响通信服务。
3.充电服务与通信服务面对的需求和变化是不一样,强依赖的架构带来很多的问题。
为了解决上述的这些问题,项目组决定借助Kafka对程序进行改造 。总体思路是,通信服务收到数据后,把数据存储到kafka,然后通过一个异步任务处理框架实时消费Kafka数据,并调用业务插件处理。
通过上面思路我们可以看到,系统整体架构仅是引入了一个MQ中间件,业务逻辑并没有发生本质的变化。但是在实际的压测中,却发现新架构下的程序性能比原来要慢很多。顺便说一下,压测场景是模拟10万充电终端离网上下线,短时间内会生成大约32万的消息量,遥信:10万,遥测:10万,电量10万,其他:2万。
通过ANTS分析相关进程,发现MonitorDataUploader.AddToLocalCache方法占用了78%左右的CPU。此方法不是业务方法,是为了监控程序的运行情况而加入的埋点监控。通过进一步分析看,在30多万消息量下,会产生约1000万甚至更高的监控消息。在如此高的并发下,这部分程序存在很严重的性能问题,导致系统的资源占用很高,系统运行变慢。
OK。既然问题已经清楚,那就开始优化吧。虽然可以把监控埋点屏蔽,临时解决程序的性能问题。但是,这对一个互联网应用来说是要不得的。没有监控,系统的运行健康状况就一无所知,这对一个SLA要求99.95%的系统来说,是不现实的。所以,必须全力优化监控程序在上报海量监控日志上的性能问题。
为了便于验证问题,写了一个模拟程序.通过模拟程序,很容易的再现了CPU占用很高的情况。
代码实现中,监控消息的存储是通过BlockingCollection存储的,并且设置了Collection大小为1000万。
var cache = new BlockingCollection<MonitorData>(boundedCapacity);
通过阅读BlockingCollection 的说明,可以看到空构造函数可以不设置Collection的上限。看到这个解释,怀疑是限制了上线的Collection存在性能问题。与是把代码中对BlockingCollection 的构造改成空构造,再次测试。测试结果大出意料,性能表现有了非常好的提升。
为了进一步验证问题,把对BlockingCollection 的构造改了限制大小,并设置上线为1个亿。测试时消息总量为5000万,验证一下是否是BlockingCollection 达到上限后,引起的严重性能问题。通过测试数据看,CPU消耗与不限制时基本一致。通过此可以确定,BlockingCollection 在设置了容量上限后,如果消息超过容量,性能将会非常差。
通过上面的调优,在发送5000万监控消息的情况下,程序的CPU在60% 左右持续30s左右。虽然性能有所改善,但是还不是很尽如人意。 有没有更好的解决方案呢?通过不算的思考和尝试,终于找到了一个更好的解决方案:基于双缓存+线程级多桶式Collection。此种模式下性能表现如下,CPU平均在30%左右,持续时间在15s左右。性能又有近一倍的提升。具体实现方案下次再分享。