异常引起的生产事故

Posted by AaronYang on August 15, 2020

异常引起的生产事故

异常是程序运行过程中发生的不正常或不符合预期的情况。异常处理则是编程语言或计算机硬件里面的一种机制,用于处理软硬件系统中的违背正常运作的情况。

之前一直认为异常处理机制不就是将异常抛出,或者将异常try-catch一下打印一下日志,没什么好深刻探讨的点,不明白为什么Java编程思想等编程语言书籍会将其单拎一章重点讲解。直到在生产环境中遇到了两次故障才警醒起来,原来这异常处理作用如此之大。为此,我又重新温故了这一章。

1、在编译时出现的问题都是小问题

编译性语言构建的程序在运行前都需要进行编译,低级的代码错误都会在这一阶段被拦截掉,然而逻辑错误以及系统突发情况都都存在着潜在隐患。逻辑错误可以通过单元测试等手段进行验证,而系统突发情况则难以模拟和预估,此时需要敏锐的嗅觉通过合适的方法将异常进行传递、捕获、处理和记录。这种突发情况在依赖外部组件,部署复杂的系统中更容易发生,毕竟系统、外部组件大部分情况下都不是自己维护的。

异常的出现将阻止程序顺着原本的路径运行。当抛出异常后,有几件事会随之发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建对象的引用。然后,当前的执行路径被终止,并且从当前环境中弹出异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复。

如果方法可能抛出异常,我们可以通过一个特殊的块来捕获异常,称为try块,紧接着的便是异常处理程序,以catch表示。异常处理机制将搜索参数与异常类型相匹配的第一个处理程序,然后执行。

2、终止与恢复

2.1 抛出异常终止应用

在服务器无法正常提供服务或者状态不佳时,需要将其拉出集群外,等待应用恢复,比如内存使用超过阈值时会发生FullGC此时CPU会将大部分尽力都花在清理内存上无法及时处理请求,此时需要将机器拉出集群,让它安安静静的清理完内存在拉入。但是,如果靠人力来监控来操作,一时不及时而是误操作,所以需要应用自己监控,由框架来操作机器。

if (ConfigurationTypeConst.SearchConfig.EnableQuietFullGC &&
	Common.Generic.QuietFullGc.Instance.MarkAsDown)
{
	if (ConfigurationTypeConst.SearchConfig.EnableCheckHealthThrowException)
	{
		throw new Exception("主动GC");
	}
	return new CheckHealthResponseType()
	{
		ResponseStatus = new ResponseStatusType()
		{
			Ack = AckCodeType.Failure,
			Errors = new System.Collections.Generic.List<ErrorDataType>() {
				new ErrorDataType() {
					ErrorCode ="HealthError",
					Message = "fullgc_quiet."
				}
			}
		}
	};
}

当应用服务器即将进行GC时,我们可以设置判断条件,满足条件后抛出异常,监控系统捕获到异常后会将其拉出机器并等待GC完成再拉入。在此期间服务不再对外提供服务。

2.2 异常引发缓存穿透

当程序捕获异常时,通常会设置默认值或返回错误状态。但是如果异常类型预判错误时,便会错过某些意想不到的异常,导致程序不断重试本就错误的流程。如下代码中redisBytes.CustomDeserializeWithGZip(TypeSerializerV2);会抛出多种错误,CacheSerializerException是其中最常见的一种(序列化异常),当捕获到此异常后,程序知道数据无法正常解析了,所以会设置null值,后续代码中会用默认空值替代,并缓存至本地,避免重复读取无法解析或不存在的值引发的缓存穿透,和不断重试带来的性能损耗。

然而,上述代码会先进行解压缩,然后再反序列化,解压缩失败的可能性小,除非压缩方式不同才会大批量出现。偏偏由于操作失误,用了新压缩方式的数据覆盖了老数据使程序抛出了解压缩异常,程序又未正确匹配异常类型,不断重复读取Redis中错误数据导致Redis请求量成倍上升直至崩溃。在此需要将异常类型拓宽,减少损失。

if(redisBytes == null || redisBytes.Length == 0)
{
    redisDic = null;
}
else
{
    dataSize = redisBytes.Length;
    try
    {
        sw.Restart();
        redisDic = redisBytes.CustomDeserializeWithGZip(TypeSerializerV2);
        sw.Stop();
        descTime = sw.Elapsed;
        var descTimeForES = Math.Round(sw.Elapsed.TotalSeconds, 6);
        TimeCostHelper.SaveVMSReadRedisTimeCostForEs("singleroomprice", descTimeForES, dataSizeForEs, true, "interval_desc_");
    }
    catch (CacheSerializerException)
    {
        redisDic = null;
        redis.Del(key);
    }
}
if(tid > 0)
{
    if(redisDic == null)
    {
        _remoteDataV2.SetData(subHotelId, checkIn, EmptyRedisData, tid);   // 判断为空后,设置默认空值,避免重复读取
    }
    else
    {
        _remoteDataV2.SetData(subHotelId, checkIn, redisDic, tid);
    }
}

2.3 异常使得线程终止

以下代码中未将Redis连接没有及时异常捕获,导致创建的守护线程不断被终止,上层代码捕获后又重新创建线程进行另一轮的试错,直至应用在频繁创建线程中崩溃。及时捕获异常。

ScanWorker = new Thread(ScanColdData);

private void ScanColdData()
{
    while (true)
    {
        ...
        TimeBufferData.Scan(DataExpireSeconds, needScanHot, false, RemoveHotKeyFromRedis, AddHotKeyToRedis, out expiredCount, out maxHitTimes, out promotionCount, out degradeCount);
        ...
    }
}