共计 5202 个字符,预计需要花费 14 分钟才能阅读完成。
异样的正确应用在微服务架构中的重要性排前三,没什么意见吧
Curdboy 们好久不见,先祝大家端午节高兴。最近想说说异样,我的思考俨然造成了闭环,心愿这套组合拳能对你的业务代码有所帮忙。
上面只探讨世界上最好的语言和生态最残缺的语言,没什么意见吧。
异样的异同
PHP 在 PHP7 异样的设计和 Java 保持一致了 Exception extends Throwable
,不过在历史起因和设计理念上还是有一些轻微的差异。比方 PHP 中的异样是有 code
属性的,这样就存在多种异样聚类为同一个异样,而后在catch
区块里依据 code
写不同的业务逻辑代码。
而 Java 异样则没有code
,不能这样设计,只能针对不同的状况应用不同的异样。所以咱们习惯服务对外裸露的通过包装类来封装,而不是间接依赖异样的透传。
对立异样的解决
在 Java 代码里,最让人诟病的就是铺天盖地的try catch
,没什么意见吧。轻易抓一段代码
@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {
try {List<AdsDTO> adsDTO = new ArrayList<>();
//... 业务逻辑省略
DataResult.success(adsDTO);
} catch (Exception e) {log.error("getAds has Exception:{}", e.getMessage(), e);
DataResult.failure(ResultCode.CODE_INTERNAL_ERROR, e.getMessage()); // 将异样信息返回给服务端调用方
}
return dataResult;
}
很多时候都是无脑上来就先写个 try catch
再说,不论外面是否会有非运行时异样。比拟好的形式是应用 aop 的形式来拦挡所有的服务办法的调用,对立接管异样而后做解决。
@Around("recordLog()")
public Object record(ProceedingJoinPoint joinPoint) throws Throwable {
//... 申请调用起源记录
Object result;
try {result = joinPoint.proceed(joinPoint.getArgs());
} catch (Exception e) {
//... 记录异样日志
DataResult<Object> res = DataResult.failure(ResultCode.CODE_INTERNAL_ERROR, e.getMessage());
result = res;
}
//... 返回值日志记录
return result;
}
有一点小问题,如果间接将 A 服务的异样信息间接返回给调用者 B,可能存在一些潜在的危险,永远不能置信调用者,即便他根正苗红三代贫农也不行。因为不能确定调用者会将该错误信息作何解决,可能就间接作为 json
返回给了前端。
RuntimeException
在 Java 中异样能够分为运行时异样和非运行时异样,运行时异样是不须要捕捉的 ,在办法上也不须要标注 throw Exception
,比方咱们在办法里应用 guava 包里的Preconditions
工具类,抛出的 IllegalArgumentException
也是运行时异样。
@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {Preconditions.checkArgument(null != liveId, "liveIds not be null");
List<AdsDTO> adsDTOS = new ArrayList<>();
//... 业务逻辑省略
return DataResult.success(adsDTOS);
}
咱们也能够应用该个性,自定义本人的业务异样类继承RuntimeException
XXServiceRuntimeException extends RuntimeException
对于不合乎业务逻辑状况则间接抛出 XXServiceRuntimeException
@Override
public DataResult<List<AdsDTO>> getAds(Integer liveId) {if (null == liveId) {throw new XXServiceRuntimeException("liveId can't be null");
}
List<AdsDTO> adsDTOS = new ArrayList<>();
//... 业务逻辑省略
return DataResult.success(adsDTOS);
}
而后在 aop 做对立解决做相应的优化,对于后面比拟粗犷的做法,应该将除了 XXServiceRuntimeException
和IllegalArgumentException
之外的异样外部记录,不再对外裸露,然而肯定要记得通过 requestId
将分布式链路串起来,在 DataResult
中返回,不便问题的排查。
@Around("recordLog()")
public Object record(ProceedingJoinPoint joinPoint) throws Throwable {
//... 申请调用起源记录
Object result;
try {result = joinPoint.proceed(joinPoint.getArgs());
} catch (Exception e) {
//... 记录异样日志①
log.error("{}#{}, exception:{}:", clazzSimpleName, methodName, e.getClass().getSimpleName(), e);
DataResult<Object> res = DataResult.failure(ResultCode.CODE_INTERNAL_ERROR);
if (e instanceof XXServiceRuntimeException || e instanceof IllegalArgumentException) {res.setMessage(e.getMessage());
}
result = res;
}
if (result instanceof DataResult) {((DataResult) result).setRequestId(EagleEye.getTraceId()); // DMC
}
//... 返回值日志记录
return result;
}
异样监控
说好的闭环呢,应用了自定义异样类之后,对异样日志的监控报警的阈值就能够升高不少,报警更加精准,以阿里云 SLS 的监控为例
* and ERROR not XXServiceRuntimeException not IllegalArgumentException|SELECT COUNT(*) AS count
这里监控的是
记录异样日志①
的日志
PHP 里的异样
下面 Java 里说到的问题在 PHP 里也同样存在,不必 3 种办法来模仿 aop 都不能体现 PHP 是世界上最好的语言
//1. call_user_func_array
//2. 反射
//3. 间接 new
try {$class = new $className();
$result = $class->$methodName();} catch (\Throwable $e) {//... 略}
相似下面的架构逻辑不再反复编写伪代码,根本保持一致。也是自定义本人的业务异样类继承RuntimeException
,而后做对外输入解决。
然而 PHP 里有一些历史包袱,起初设计的时候很多运行时异样都是作为 Notice
,Warning
谬误输入的,然而谬误的输入短少调用栈,不利于问题的排查
function foo(){return boo("xxx");
}
function boo($a){return explode($a);
}
foo();
Warning: explode() expects at least 2 parameters, 1 given in /Users/mengkang/Downloads/ab.php on line 8
看不到具体的参数,也看不到调用栈。如果应用 set_error_handler
+ ErrorException
之后,就十分清晰了。
set_error_handler(function ($severity, $message, $file, $line) {throw new ErrorException($message, 10001, $severity, $file, $line);
});
function foo(){return boo("xxx");
}
function boo($a){return explode($a);
}
try{foo();
}catch(Exception $e){echo $e->getTraceAsString();
}
最初打印进去的信息就是
Fatal error: Uncaught ErrorException: explode() expects at least 2 parameters, 1 given in /Users/mengkang/Downloads/ab.php:12
Stack trace:
#0 [internal function]: {closure}(2, 'explode() expec...', '/Users/mengkang...', 12, Array)
#1 /Users/mengkang/Downloads/ab.php(12): explode('xxx')
#2 /Users/mengkang/Downloads/ab.php(8): boo('xxx')
#3 /Users/mengkang/Downloads/ab.php(15): foo()
#4 {main}
thrown in /Users/mengkang/Downloads/ab.php on line 12
批改下面的函数
function boo(array $a){return implode(",", $a);
}
则没法捕捉了,因为抛出的是 PHP Fatal error: Uncaught TypeError
,PHP7 新增了class Error implements Throwable
,则在 PHP 零碎谬误日志里会有 Stack, 然而不能和整个业务零碎串联起来,这里就又不得不说日志的设计,咱们冀望像 Java 那样通过一个 traceId 将所有的日志串联起来,从 Nginx 日志到 PHP 里的失常 info level 日志以及这些Uncaught TypeError
,所以接管默认输入到零碎谬误日志,在 catch 代码块中记录到对立的中央。那么这里就简略批改为
set_error_handler(function ($severity, $message, $file, $line) {throw new ErrorException($message, 10001, $severity, $file, $line);
});
function foo(){return boo("xxx");
}
function boo(array $a){return implode(",", $a);
}
try{foo();
}catch(Throwable $e){echo $e->getTraceAsString();
}
catch Throwable
就能承受 Error
和Exception
了。
然而 set_error_handler
没方法解决一些谬误,比方 E_PARSE
的谬误,能够用 register_shutdown_function
来兜底。
值得注意的是
register_shutdown_function
的用意是在脚本失常退出或显示调用 exit 时,执行注册的函数。
是脚本运行 (run-time not parse-time) 出错退出时,能力应用。如果在调用register_shutdown_function
的同一文件的外面有语法错误,是无奈注册的,然而咱们我的项目个别都是分多个文件的,这样就其余文件里有语法错误,也能捕捉了
register_shutdown_function(function(){$e = error_get_last();
if ($e){throw new \ErrorException($e["message"], 10002, E_ERROR, $e["file"], $e["line"]);
}
});
如果你想间接应用这些代码(PHP 的)间接到我的项目可能会有很多坑,因为咱们习惯了零碎中有很多 notice 了,能够将 notice 的谬误转成异样之后被动记录,然而不对外抛出异样即可。
明天先到这里,下次说下日志应该怎么记。
尽管 PHP 大环境的倒退上仿佛不太清朗,然而因为爱所以继续吧。某些场景下还是最佳抉择的。