最近遇到了一个比较有意思的问题,这里简单探讨一下解决方案。
业务有一个系统,管理了非常多的IOT设备,设备每隔30S会通过HTTP方式汇报心跳到服务端,然后服务端更新数据库记录,用于后续的设备监控使用。
通常情况下,设备间的启动时间实际是有差异的。从时间维度上看,分布会比较均匀,落到数据库层面的流量分布也是相对均匀的。
但是某一天,设备依赖的第三方服务有变动,导致设备集体重启。重启后的设备,在同一时刻上报心跳的并发度增加,进而导致数据库压力剧增。
从整理链路上我们分析一下,该如何应对。
从问题的源头出发,心跳信息是在设备启动以后,每隔30S时间进行上报的。一个比较简单的方式,设备启动以后,随机增加一定的时间间隔(如0-30s),然后再上报心跳,从而概率性规避所有设备上报的频次一致。
从链路上看,IOT设备直接上报心跳到服务端。这里可以考虑引入MQ,毕竟MQ的核心用途就是削峰填谷。
对于IOT设备,通常采用消息队列MQTT,然后服务端采用RocketMQ、Kafka、RabbitMQ即可。MQTT和服务端MQ之间的桥接,需要一些额外的工作,云厂商通常都有对应的产品。
(图片来源https://rocketmq.apache.org/zh/docs/4.x/mqtt/01RocketMQMQTTOverview)
在引入了MQ以后,服务端可根据自己的实际处理能力进行消费即可。
当然,此方案需要考虑消息积压的问题。关于这个问题,一方面可以通过扩容服务端消费增速,一方面可以结合业务实际场景考虑丢弃已经过期的业务数据,比如这里的心跳数据,如果心跳已经是10分钟以前的数据库,明显可以不处理了。
public ConsumeConcurrentlyStatus consumeMessage(
List msgs,
ConsumeConcurrentlyContext context) {
long offset = msgs.get(0).getQueueOffset();
String maxOffset =
msgs.get(0).getProperty(Message.PROPERTY_MAX_OFFSET);
long diff = Long.parseLong(maxOffset) - offset;
if (diff > 100000) {
// TODO 消息堆积情况的特殊处理
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
// TODO 正常消费过程
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
对于服务端来说,在此场景里面,服务端压力不算大。
所以,首先要考虑的是做好限流工作。
单机的限流可以采用令牌桶方式(https://mikechen.cc/20379.html),可以支持这种突增的流量。分布式场景下可以采用Sentinel(https://sentinelguard.io/zh-cn/)。
通常和限流一起出现的就是降级。这里由于是写数据库操作,那么当数据库写压力非常大的时候,我们可以考虑几种类型的操作。
第一种操作就是不操作(手动狗头),直接拒绝写操作,这通常用于处理部分不重要的数据。
第二种操作就是延迟操作,由于当前数据库压力大,那就等一段时间再处理这条消息。延迟处理的方式可以采用本地延迟队列DelayQueue,或者直接再写回MQ中,等待后续的二次处理。
第三种操作就是在数据库前面增加缓存,缓存的写操作速度和性能要高于数据库。
对于数据库,一种简单直接的方式就是纵向扩容,配置上来了,处理能力和支持并发会有相应的提升。
第二种方案就是横向拆分,也就是存储的分库分表,通过将写压力分担到多个节点上,也可以做到支持更多的写操作。
第三种方式,可以考虑使用分布式NoSQL替换传统的关系型数据库,毕竟NoSQL基本都是sharding方式,性能比关系型数据库高很多。
上面的一些操作是通用的,当然也要结合实际业务场景。比如该场景下的心跳数据,它是偏实时的操作。如果处理的时候,时间已经过期了,或者有更新的数据到来,那可以直接丢弃旧的数据。
对于丢弃数据,可以在服务端处理的时候,按照事件发生的时间和实际处理的时间进行比对。也可以在服务端维护基于时间的优先级队列,优先处理更新的数据。
当然,如果引入了MQ,如果MQ支持消息过期时间设置,可以直接利用即可。
好了,前面的都是马后炮了。
对于当时的系统状况,没有MQ,没有限流,端上没有改造,我们能做的只有求助DBA大大先帮忙扩容顶一下。待扩容以后,然后分区域分批次重启设备。
对于后续肯定是前面的多管齐下,MQ、限流、丢弃超时心跳数据通通安排上,我想这样应该可以应对了吧。
如果还有其他建议,还请大佬们尽快提出,晚了就得等到下次case study了。