IT码农库

您当前所在位置:首页 > 数据库 > MongoDB

MongoDB

解决mongo的tickets被耗绝导致卡顿问题

[db:来源] 泰勒今天不想展开2023-04-08MongoDB5619
这篇文章主要介绍了解决mongo的tickets被耗绝导致卡顿问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

近一年来,项目线上环境的mongo数据库出现多次tickets被耗绝,导致数据库卡顿,并且都是忽然出现,等待一段时间后又能自动恢复。

为了解决这个问题,我们入行了长期的探索和研究,先后从多个角度入行优化,于此记录和分享一下这一路的历程。

tickets是什么

为了解决这个问题,我们首先要明白ticktes是什么,其实网上基本都说的一知半解,没有一个能说明白的,但是有一个查询tieckts消耗情况的mongo命令:

db.serverStatus().wiredTiger.concurrentTransactions

查询结果:

{
	"write" : {
		"out" : 0,
		"available" : 128,
		"totalTickets" : 128
	},
	"read" : {
		"out" : 1,
		"available" : 127,
		"totalTickets" : 128
	}
}

可以观到tickets分为读写两种,那ticktets到底是什么呢,我们根据这个查询命令,其实大致可以预测认为是当前同时存在的事务数量。

也就是mongo限制了同时入行的事务数。

早期因为不知道tickets到底是什么意思,尝试过很多思路错误的优化,所以解决问题,最好还是能弄明白问题本身,才能对症下药。

思索历程

在众多数据库卡顿的经历中,曾有一次因为rabbitmq导致的数据库卡顿,原因是一小伙伴在哀求的过滤层加了一个发送mq的逻辑,但是没有入行限制,导致每次只有有接口被调,都会往发布一个mq消息,由于过高的并发导致rabbitmq不堪重负,倒是让人想不明到的是mq卡的同时,数据库也卡住了。

一开始以为是因为消息过多,导致消费者疯狂消费,压垮了数据库,其实不存在这个问题,因为我们的mq配置单个消费者机器是串行的,也就是同一台机器同一时间只会消费同一个消息队列的一条消息,所以并不会因为消息的多给数据库带来压力,只会堆积在mq集群里。所以这次其实没有找到mq卡顿导致mongo卡顿的原因。

我们接进的几家第三方服务,比如给我们提供IM消息服务的融云,每次他们出现问题的时候,我们也会出现数据库卡顿,并且每次时间出奇的一直,但也始终找不到原因。

起初经过对他们调用我们接口情况入行分析,发现每次他们出问题时,我们收到的哀求会倍增,以为是这个原因导致的数据库压力过大,并且我们基于redis和他们归调的流水号入行了拦截,拦截方式如下:

  • 当哀求过来时从redis中查询该笔流水号状态,假如状态为已完结,则直接成功返归
  • 假如查询到状态是入行中,则抛异常给第三方,从而让他继承重试
  • 假如查询不到状态,则尝试设置状态为入行中并设置10秒左右的过期时间,假如设置成功,则放到数据库层面入行数据处理;假如设置失败,也抛异常给第三方,等待下次重试
  • 等数据库曾处理完成后,将redis中的流水号状态改为已完结。

避免重复哀求给我们带来的数据库的压力。这其实也算是一部分原因但还是不算主要原因。

引起mongo卡顿的还有发布版本,有一段时间隔三差五发布版本,就会出现卡顿,但是查观更新的代码也都是一些无关痛痒理论上不会引起问题的内容。

后来发现是发布版本时每次同时关闭和启动的机器从原来的一台改成了两台(一台一台发布太慢,所以运维改成了两台两台一起发),感觉原因应该就在这里,后来想到会不会和优雅关闭有关,当机器关闭时仍旧有mq消费者以及内置循环脚本在执行,当入程杀死时,会产生大量需要立马归滚的事务,从而导致mongo卡顿。

后来经过和运维小伙伴的沟通发现,在优雅关闭方面确实存在问题,他们关闭容器时会小容器内的主入程发一个容器即将关闭的信号,然后等待几十秒后,假如主入程没有自己关闭,则会直接杀死入程。

为此我们需要在程序中实现对关闭信号的监听,并实现优雅关闭的逻辑,在spring中,我们可以通过spring的时间拿到外部即将关闭的信号:

	@Volatile
	private var consumeSwitch = true

	/**
	* 销毁逻辑
	*/
	@EventListener
	fun close(event: ContextClosedEvent){
		consumeSwitch = false
		logger.info("----------------------rabbitmq停止消费----------------------")
	}

可以通过如上方式,对系统中的mq消费者或者其他内置程序入行优雅关停控制,对优雅关闭问题优化后,服务器关闭重启导致的数据库卡顿确实得到了有效解决。

上面的融云问题优化过后,后来融云再次卡顿的时候,还是会出现mongo卡顿,由此可见,肯定和第三方有关,但上面说的问题肯定不是主要原因。

后来我观到我们调用第三方的逻辑很多都在@Transactional代码块中间,后来往观了第三方sdk里的逻辑,其实就是封装了一个http哀求,但是http哀求的哀求超时时间长达60秒,那就会有一个问题,假如这个时候第三方服务器卡顿了,这个哀求就会不断地等,知道60s超时,而由于这个操作是在事务块中,意味着这个事务也不会commit掉,那等于这个事务所占用的tickets也一直不会放掉,至此根本原因好像找到了,是因为事务本身被卡住了,导致tickets耗绝,从而后面新的事务全部都在等待状态,全部都卡住了。

其实这次找的原因,同样也可以解释前面mq卡顿导致的数据库卡顿,因为同样有大量的发送mq的操作在事务块中,因为短时间疯狂发mq,导致mq服务端卡顿,从而导致发mq的操作出现卡顿,这就会出现整个事务被卡住,接着tickets被消耗殆绝,整个数据库卡顿。

找到确定问题后就好对症下药了,第三方的问题由于我们不能保证第三方的稳定性,所以当第三方出现问题时的思路应该是入行服务降级,答应部分功能不可用,确定核心业务不受影响,我们基于java线程池入行了同步改异步处理,并且由于第三方的工作是给用户推送im消息,所以配置的舍弃策略是当阻塞队列堆积满之后,将最老的入行丢弃。

而假如是mq导致的这种情况,我们这边没有入行额外的处理,因为这种情况是有自身的bug导致的,这需要做好整理分享工作,避免再次出现这样的bug。

//自己实现的runnable
abstract class RongCloudRunnable(
	private val taskDesc: String,
	private val params: Map<String, Any?>
	) : Runnable {


	override fun toString(): String {
		return "任务名称:${taskDesc};任务参数:${params}"
	}
}		
//构建线程池
private val rongCloudThreadPool = ThreadPoolExecutor(
	externalProps.rongCloud.threadPoolCoreCnt, externalProps.rongCloud.threadPoolMaxCnt, 5,
	TimeUnit.MINUTES, LinkedBlockingQueue<Runnable>(externalProps.rongCloud.threadPoolQueueLength),
	RejectedExecutionHandler { r, executor ->
		if (!executor.isShutdown) {
			val item = executor.queue.poll()
			logger.warn("当前融云阻塞任务过多,舍弃最老的任务:${item}")
			executor.execute(r)
		}
	}
)

//封装线程池任务处理方法
fun taskExecute(taskDesc: String, params: Map<String,Any?>, handle: ()-> Unit){
	rongCloudThreadPool.execute(object :RongCloudRunnable(taskDesc, params){
		override fun run() {
			handle()
		}
	})
}

//详细使用
taskExecute("发送消息", mapOf(
	"from_id" to fromId,
	"target_ids" to targetIds,
	"data" to data,
	"is_include_sender" to isIncludeSender
)){
	sendMessage(BatchSendData(fromId, targetIds, data, isIncludeSender))
}		

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

大图广告(830*140)