需求
现有的支付支持多种通道,现需要增加一种通道,并对这个通道的支付请求做监控,如果异常超过了一定的比例,那么这个通道就要变为不可用,采用其他的通道。
分析
这个需求,简单来说就是需要对发起支付请求的http接口进行监控,如果监控出现了异常,可以修改一下redis的标志位,在取通道那一步,去判断这个标志位,如果标志位为异常,那么就不返回这个通道。所以问题在于对发起http的方法如何做监控,初步分析有以下几个难点:
- 如何做统计,如果对监控数据进行统计,多少时间窗口内异常比例超过多少才算是异常
- 发起请求的方法可能被多个地方使用(一般对接外部的接口,都会封装统一方法),比如说支付的时候会调,查询的时候会调,但是查询是不需要做监控的
- 支付可以还有细分项,比如这个通道支持微信支付,阿里支付,银联支付,微信支付被置为不可用的时候,阿里支付其实是可以用的,所以不能把阿里的支付置为不可用,也就是说支付需要做细分监控
- 置为不可用的时候,需要告警出来
实现
需要对方法进行监控,第一个想到的是采用AOP实现,并结合自定义注解。
自定义注解
根据上面的分析,我们的自定义注解可以这样设计
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitor {
/**
* 监控枚举决定需不需要监控
*/
MonitorType type() default MonitorType.DEFAULT;
/**
* 异常比例 默认不配置
*/
int percent() default -1;
/**
* redis的key值
*/
String key() default CacheConstant.USER_MONITOR_TYPE_KEY;
}
public enum MonitorType {
/**
* 支付监控
*/
CHA_UMS_WX_PAY("微信支付监控","CHINAUMS.MOP.WXPAY",true),
CHA_UMS_ALI_PAY("阿里支付监控","CHINAUMS.MOP.ALIPAY",true),
CHA_UMS_BARCODE_PAY("扫码支付监控","CHINAUMS.MOP.BARCODEPAY",true),
CHA_UMS_QUERY("查询监控","CHINAUMS_QUERY_TIMEOUT",false),
CHA_UMS_CALLBACK("回调监控","CHINAUMS_PAY_TIMEOUT",false),
DEFAULT("默认监控",CacheConstant.MONITOR_ERROR_FLAG,false);
private String desc;
private boolean open;
private String key;
MonitorType(String desc, String key, boolean open) {
this.desc = desc;
this.open = open;
this.key = key;
}
// 省略get
}
监控,我们需要三个字段就行
- type(监控类型),监控类型来决定这个接口需不需要开启监控,监控类型里有分为三个字段
- desc,描述字段,这个主要用来告警提示的文字,方便阅读
- key,监控异常的时候,如果外层的key设置了使用内部的key,那么会取这个key值作为redis缓存的标志位
- open,是否开启监控,像支付是需要做监控,但是回调是不用做监控,由这个决定
- percent(监控异常比例),这里采用的是一个接口一个监控比例配置,没有做细分项,对于一种类型,可以有个独立的监控比例(目前没做)
- key(redis标志位的key值),默认采用type里面各自的key作为标志位的key,异常时修改redis的key值
AOP实现
@Aspect
@Component
@Order(-10)
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class MonitorAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Around("@annotation(com.*.*.common.aop.annotation.Monitor)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 当前环境拥有监控注解,直接执行监控
MonitorType monitorType = MonitorHolder.getMonitor();
Class<?> target = joinPoint.getTarget().getClass();
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Monitor monitor = beforeGetMonitor(target, signature.getMethod());
if ( monitorType == null ){
monitorType = monitor.type();
MonitorHolder.setMonitor(monitor.type());
}
long startTime = System.currentTimeMillis();
FastCompass fastCompass = null;
if ( monitorType.isOpen() ){
fastCompass = MetricManager.getFastCompass("pc", MetricName.build(monitorType.name()).level(MetricLevel.CRITICAL));
}
Object proceed = null;
try{
proceed = joinPoint.proceed();
return proceed;
}catch (Exception e){
if ( e instanceof MonitorException ){
// 如果是监控异常,增加异常数量
if ( monitorType.isOpen() ){
fastCompass.record(System.currentTimeMillis() - startTime, MetricCategory.ERROR.name());
}
}
throw e;
}finally{
// 执行统计
if ( monitorType.isOpen() ) {
fastCompass.record(System.currentTimeMillis() - startTime, MetricCategory.TOTAL.name());
boolean openMonitorError = checkMonitor(fastCompass, monitor);
// 监控异常开启指定redis标志位
if ( openMonitorError ){
String key = monitor.key();
// 采用监控类型的key值
if ( StringUtils.hasText(key) && CacheConstant.USER_MONITOR_TYPE_KEY.equals(key) ){
key = monitorType.getKey();
}
if ( StringUtils.hasText(key) ){
CacheContext.set(CacheConstant.MONITOR_PREFIX + key,CacheConstant.MONITOR_ERROR_FLAG);
logger.error("监控【{}】出现异常,修改redis值【{}】", monitorType.getDesc(),(CacheConstant.MONITOR_PREFIX + key));
}else{
logger.error("监控【{}】出现异常,但是没有设置redis标志位操作,不修改redis值", monitorType.getDesc());
}
}
}
//使用完清空
MonitorHolder.removeMonitor();
}
}
private boolean checkMonitor(FastCompass fastCompass,Monitor monitor) {
try {
Map<String, Map<Long, Long>> map = fastCompass.getMethodCountPerCategory();
// 全局的监控比例,如果当前注解有自定义的比例值,优先采用注解上面的比例值
int percent = 80;
if ( monitor.percent() != -1 ){
percent = monitor.percent();
}else{
String globalMonitorPercent = CacheContext.getString(CacheConstant.MONITOR_PERCENT);
if ( globalMonitorPercent != null && StringUtils.hasText(globalMonitorPercent) ){
percent = Integer.valueOf(globalMonitorPercent);
}
}
long blockCount = 0, totalCount = 0;
if (!map.containsKey(MetricCategory.ERROR.name())) {
return false;
}
if (!map.containsKey(MetricCategory.TOTAL.name())) {
logger.debug("没有总记录数据");
return false;
}
Map<Long, Long> blockMap = map.get(MetricCategory.ERROR.name());
for (Long key : blockMap.keySet()) {
blockCount += blockMap.get(key);
}
Map<Long, Long> totalMap = map.get(MetricCategory.TOTAL.name());
for (Long key : totalMap.keySet()) {
totalCount += totalMap.get(key);
}
logger.error("{}-{}", blockMap, totalMap);
if (totalCount == 0) {
logger.error("总记录数等于0");
return false;
}
double blockPercent = blockCount * 100.0 / totalCount;
boolean needOpen = blockPercent > percent;
if ( needOpen ){
// 发送告警
StringBuilder contentStr = new StringBuilder();
contentStr.append("**接口【" + MonitorHolder.getMonitor().getDesc() +"】异常!**\n");
contentStr.append(String.format("> 异常比例:**%s**\n",blockPercent));
contentStr.append(String.format("> 主机IP:**%s**\n",IPUtil.getLocalIp()));
contentStr.append(String.format("> 告警时间:**%s**\n",LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)));
contentStr.append(String.format("> redis标志位:**%s**\n",MonitorHolder.getMonitor().getKey()));
WxWorkWarnMsgDTO wxWorkWarnMsgDTO = WxWorkWarnMsgDTO.builder()
.withCategory("05")
.withColor(WarnMsgLevel.ERROR.getColor())
.withContent(contentStr.toString()).buildMarkdown();
ProducerSendMessageFacade.sendPulsarMajorMsg("cp-comm/msg-daemon/warn-msg-push",JSONObject.toJSONString(wxWorkWarnMsgDTO));
}
logger.info("监控【{}】的异常配置比例:{},当前比例:{},{}/{},是否需要开启:{}",MonitorHolder.getMonitor().getDesc(), percent, blockPercent, blockCount, totalCount, needOpen);
return needOpen;
} catch (Exception e) {
logger.error("判断是否监控异常出现异常:", e);
}
return false;
}
private Monitor beforeGetMonitor(Class<?> target,Method method){
Monitor monitor = null ;
//从类初始化
monitor = getMonitor(target, method) ;
//从接口初始化
if(monitor == null){
for (Class<?> clazz : target.getInterfaces()) {
monitor = getMonitor(clazz, method);
if(monitor != null){
//从某个接口中一旦发现注解,不再循环
break ;
}
}
}
return monitor;
}
/**
* 获取方法或类的注解对象DataSource
* @param target 类class
* @param method 方法
* @return DataSource
*/
private Monitor getMonitor(Class<?> target, Method method){
try {
//1.优先方法注解
Class<?>[] types = method.getParameterTypes();
Method m = target.getMethod(method.getName(), types);
if (m != null && m.isAnnotationPresent(Monitor.class)) {
return m.getAnnotation(Monitor.class);
}
//2.其次类注解
if (target.isAnnotationPresent(Monitor.class)) {
return target.getAnnotation(Monitor.class);
}
} catch (Exception e) {
logger.error(MessageFormat.format("通过注解注册监控时发生异常[class={0},method={1}]:"
, target.getName(), method.getName()),e) ;
}
return null ;
}
}
这个aop的实现就是监控的所有操作,来看下上面分析的几个问题如何解决
-
监控数据进行统计
这里采用的是阿里的一个开源包实现,github地址是这个https://github.com/alibaba/metrics/wiki/quick-start,采用的是里面的FastCompass,FastCompass能够方便的统计某个业务接口的吞吐率, 响应时间, 成功率, 错误率,命中率。
-
调方法有些不需要监控,有些需要监控
可以看到aop最开始的代码里面,有一段MonitorType monitorType = MonitorHolder.getMonitor();,也就是说在调用支付之前,设置下当前线程的变量,持有一个监控类型就可以了,具体使用:
//调用之前设置监控类型 MonitorHolder.setMonitor(MonitorType.CHA_UMS_BARCODE_PAY); // 调用监控的方法 String result = chinaumsMopUtil.postJSON(pcCommonTppAccount.getTradeUrl(), requestJson, headers); @Monitor(type = MonitorType.DEFAULT) public String postJSON(String url,String jsonStr ,Map<String,String> headers) { }
-
支付细分项的监控
这个简单,我们在监控类型里做细分就行了,微信支付,阿里支付,弄成单独的类型
-
告警
在上面的代码中,如果需要告警,目前做的比较简单,采用的是企业微信机器人告警。
至此,方法的监控就已经全部实现了,支付通道那一层,判断下通道对应的标志位就行了。
网友评论