记一次美团外卖推送报文丢失

1.环境介绍

服务架构: spring cloud 全家桶
服务之间调用方式:openfeign
模块划分:gateway、 listener、order
模块调用的流程:

美团外卖 gateway listener order 美团外卖推送内容 美团外卖推送内容 转发到order_service 处理成功,返回ok报文 处理成功,返回ok报文 处理成功,返回ok报文 美团外卖 gateway listener order

业务描述:接入了美团外卖平台,客户在美团外卖下单后,美团会推送订单到我们系统。
问题描述:据测试反馈,有一笔美团外卖的订单丢失了部分推送报文(美团每次推送的报文,都会记录下来,便于排查问题)。
订单流程:美团外卖订单订单流程(这里讨论最简单的下单流程):
1.推送订单到我们系统(create),我们主动调用接口确认;
2.美团外卖推送确认订单报文(confirm);
3.接着就是推送配送的报文(deliverStatus);
4.最后就是推送订单完成的报文(completed)。

注:美团外卖每次报文推送返回{“data”:“ok”} 表示接收成功,将结束推送,否则会按照一定的频率,持续回调我们的系统接口,直到超过一定的阈值。

2.问题追踪

step1 查推送日志

第一时间看到这个问题,既然丢失了报文,就要查到底丢失了哪些报文?因为什么导致丢失的?哪个环节丢失的?
最直接的就是看数据库的推送日志,发现就只有一条创建订单的报文和物流信息的报文,唯独缺失了订单确认的(confirm)报文。
分析:没有推送记录,所以,要么是美团回调了我们系统,我们系统没有保存成功,要么是美团没有推送confirm报文。美团出问题的概率很小,应该是我们系统出现了问题,回调的时候,报错了,但还是回复ok报文给美团。

step2 查日志文件

根据上一步的猜测,美团是收到了ok报文,于是,登录美图商家后端查询该订单的推送记录。果然看到这个订单,回调confirm接口返回的ok报文,到这里,就能很肯定,是我们系统出现了问题。然后就去查询日志文件。请求从网关先进来,先查网关的日志,网关日志发现是正常的,有收到confirm报文,然后进listener的日志,发现也是正常,然后就进order模块的日志,果然,看到了下面的错误日志:

2019-May-09 16:34:33.065 INFO 1079 [order- -] [Thread-31] o.s.c.a.AnnotationConfigApplicationContext - Closing SpringClientFactory-shop-: startup date [Thu May 09 15:17:10 CST 2019]; parent: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5e39850
2019-May-09 16:34:33.068 INFO 1079 [order- -] [Thread-31] c.n.u.c.ShutdownEnabledTimer - Shutdown hook removed for: NFLoadBalancer-PingTimer-shop
2019-May-09 16:34:33.069 INFO 1079 [order- -] [Thread-31] o.s.c.a.AnnotationConfigApplicationContext - Closing SpringClientFactory-auth: startup date [Thu May 09 15:17:09 CST 2019]; parent: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5e39850
2019-May-09 16:34:33.071 INFO 1079 [order- -] [Thread-31] c.n.u.c.ShutdownEnabledTimer - Shutdown hook removed for: NFLoadBalancer-PingTimer-auth
2019-May-09 16:34:33.072 INFO 1079 [order- -] [Thread-31] o.s.a.r.l.SimpleMessageListenerContainer - Shutdown ignored - container is not active already
2019-May-09 16:34:33.073 INFO 1079 [order- -] [Thread-31] o.s.c.a.AnnotationConfigApplicationContext - Closing FeignContext-saas-shop-service: startup date [Thu May 09 15:08:13 CST 2019]; parent: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5e39850
2019-May-09 16:34:33.073 INFO 1079 [order- -] [Thread-31] o.s.c.a.AnnotationConfigApplicationContext - Closing FeignContext-saas-auth-service: startup date [Thu May 09 15:08:11 CST 2019]; parent: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5e39850
2019-May-09 16:34:33.079 INFO 1079 [order- -] [Thread-31] c.a.d.p.DruidDataSource - {dataSource-1} closed
2019-May-09 16:34:34.192 INFO 1079 [order- -] [Thread-31] o.s.s.c.ThreadPoolTaskExecutor - Shutting down ExecutorService
2019-May-09 16:34:34.192 WARN 1079 [order- -] [Thread-31] i.l.c.RedisChannelHandler - Connection is already closed
2019-May-09 16:34:34.202 INFO 1079 [order- -] [Thread-31] c.n.d.DiscoveryClient - Shutting down DiscoveryClient ...
2019-May-09 16:34:34.212 INFO 1079 [order- -] [Thread-31] o.s.c.n.e.InstanceInfoFactory - Setting initial instance status as: STARTING
2019-May-09 16:34:35.840 ERROR 1079 [order-service 4cad11f036a7a2e5 244bfe843d3b6ce4] [http-nio-9898-exec-9] c.s.c.o.c.GlobalExceptionHandler - 全局异常处理
org.springframework.beans.factory.BeanCreationNotAllowedException: Error creating bean with name 'meiTuanOrderPushController': Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:208)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
        at org.springframework.web.method.HandlerMethod.createWithResolvedBean(HandlerMethod.java:313)
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:327)
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:62)
        at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:350)
        at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1188)

从日志中,可以看出meiTuanOrderPushController 这个bean被销毁了,而这个刚好是我通过openfeign 要访问的controller。还可以看到全局异常类捕获到了此次请求的异常。看下日志的时间,那个时间系统正在通过jenkins 重新部署过服务,Spring 正在关闭容器,销毁bean,然后再重启。当美团推送报文的时候,要访问的目标 bean销毁了,服务还没关掉,访问抛异常了,异常被全局异常捕获住了,才有了上面的日志。
分析:既然要访问的bean的销毁了,异常抛出后,被全局异常处理捕获到了,那全局异常处理也应该返回错误的信息,怎么会最后返回ok报文呢, 要找原因,那就去看下全局异常处理类的逻辑是怎么样的?

step3 追踪代码

这块代码来自order模块

@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public R exceptionHandler(HttpServletRequest request, Exception e) throws Exception {
        log.error("全局异常处理", e);
        if (e instanceof SocketTimeoutException || e instanceof FeignException) {
            throw e;
        }
        if (e instanceof SaasServiceException) {
            SaasServiceExceptionsh exception = (SaasServiceException) e;
            return R.fail(exception .getMessage());
        }
        if (e instanceof RuntimeException) {
            RuntimeException runtimeException = (RuntimeException) e;
            return R.fail(runtimeException.getMessage());
        }
        return R.fail("系统正忙,请稍后。" + e.getMessage());
    }
}

代码的逻辑比较简单,如果是SocketTimeoutException或者FeignException 异常,直接抛出,如果是业务自定义的异常和RuntimeException ,则返回错误的R对象。日志中报的是BeanCreationNotAllowedException ,查看它的继承关系,就是下面这个图,这个是异常是属于RuntimeException的子类
在这里插入图片描述
也就是说,上面这个请求,在被捕获后,是返回的错误的R对象R.fail(runtimeException.getMessage())。那按这个逻辑,那也应该返回错误报文,除非是MTResponse 默认是成功的,进一步去看下MTResponse 源码,内容如下:
来自公共模块

@Data
@Accessors(chain = true)
public class MTResponse {
    private String data = "ok";
}

一看,果然是默认是ok报文,问题就应该是出在这里了。不过那边返回的R对象,应该是失败的,怎么到这里就成了默认的MTResponse了。然后发现,R 没有data 字段,所以转换的时候,返回的还是默认的MTResponse对象,具体的原因,看下面的分析:

step4 追踪代码调用链

这块代码来自listener模块

   @RequestMapping("/confirm")
    public MTResponse orderConfirm(HttpServletRequest request) throws Exception {
        try {
            MeiTuanOrderDto param = getParam(request, MeiTuanOrderDto.class);
            return meiTuanOrderPushFeign.orderConfirm(param);
        } catch (Exception e) {
            log.error("confirm error", e);
            throw e;
        }
    }

listener 模块的orderConfirm 通过openfeign 调用order 模块的orderConfirm 服务

@FeignClient("order")
public interface MeiTuanOrderPushFeign {
    @RequestMapping("/meituan/order/confirm")
    MTResponse orderConfirm(MeiTuanOrderDto param);
 }

分析:要看到转换的地方,得看openfeign 是如何把相应结果转成MTResponse 对象的。这里的源码部分,我放在了最后。看到这里,整个逻辑就说得通了,下面是验证环节:

3.问题重现

首先这个问题是发生在bean 销毁的时候,那就找到销毁bean的方法,尽可能模拟请求,制造一样的现场。
思路:提供销毁bean的方法,然后对外提供,然后手动触发下销毁bean,再模拟美团的报文推送。

step1 在spring cloud 启动的时候设置applicationContextClass

@EnableFeignClients(basePackages = "com.chen.feign.service")
@MapperScan("com.chen.dao.mapper")
public class OrderServiceApplication {
	public static void main(String[] args) {
	        SpringApplication springApplication = new SpringApplication();
	        springApplication.setApplicationContextClass(CustomizeApplicationContext.class);
	        SpringApplication.run(OrderServiceApplication.class, args);
	    }
}

CustomizeApplicationContext.java

public class CustomizeApplicationContext extends AnnotationConfigServletWebServerApplicationContext {


    @Override
    protected void initPropertySources() {
        super.initPropertySources();
     }

    @Override
    protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        super.postProcessBeanFactory(beanFactory);
     }

    @Override
    protected void onRefresh() {
        super.onRefresh();
    }

    @Override
    public void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
        super.removeBeanDefinition(beanName);
    }
}

step2 提供方法,手动销毁bean

写个Controller,对外提供方法

@RequestMapping("/destroyBean")
@RestController
public class BeanController {
    @Autowired
    private CustomizeApplicationContext customizeApplicationContext;

    @GetMapping("destroy")
    public R destroy() throws GlobalException {
        customizeApplicationContext.removeBeanDefinition("meiTuanOrderPushController");
        return R.success("ok");
    }
}

启动gateway、listener、order,然后访问destroyBean 销毁bean,再用美团的报文模拟推送请求,就达到了预期的效果。

curl http://{ip}:{port}/destroyBean/destroy

注意:测试环境出的问题是bean刚好销毁,我这里是模拟销毁后再访问的,因为不好卡在那一个点上,不过原理是一样的。

3.解决办法

  1. MTResponse 不要默认返回ok报文,所有的操作,尽量不要默认成功。
  2. 各业务模块不要压制处理异常,在网关统一来处理全局异常。

4.总结

  1. 在排查的过程中,一步一步去分析,定位到了原因,就尽可能的重现问题;
  2. 大胆假设,小心求证,都需要敢于去猜测,猜测问题出在哪里?猜测功能是如何实现?然后再去源码或其他地方去佐证自己的猜测;
  3. openfeign 转换过程分析
    疑问:openfeign 调远程接口后返回值如何处理成对象?因为默认返回的MTResponse,里面的属性并不是我所期待的,期待的是(返回的R错误,MTResponse也应该是错误的)
    于是跟了下源码,前面的调用很复杂,这里就展示最后关键转换的地方AbstractJackson2HttpMessageConverter#read
	private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
		try {
			if (inputMessage instanceof MappingJacksonInputMessage) {
				Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
				if (deserializationView != null) {
					return this.objectMapper.readerWithView(deserializationView).forType(javaType).
							readValue(inputMessage.getBody());
				}
			}
			return this.objectMapper.readValue(inputMessage.getBody(), javaType);@1
		}
		catch (InvalidDefinitionException ex) {
			throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
		}
		catch (JsonProcessingException ex) {
			throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex);
		}
	}

分析:@1 javaType 就是openfeign confirm 方法填写的返回类型,在这里对应的就是MTResponse,inputMessage.getBody()就是接口返回内容,这里出异常后,就是对应R对象的json字符串,如果接口是正常的情况下,返回的也是MTResponse json内容。 openfeign 的调用,最终都是http请求,原理是你访问的url,只要状态码=200,会通过JSON 把远程接口返回的数据转成你openfeign 返回类型。就像上面案例中,全局异常捕获后,返回R的json字符串,然后openfeign 把json 转成MTResponse 对象,发现2者的没有共同的属性,就返回了默认的MTResponse (ok报文)对象。
5. 这个错误是刚好在服务销毁bean的时候触发的,很难测试到。但它影响的接口比较多,也就是关于美团的所有的接口都会返回ok报文,除非你这个order服务挂了,那返回的就是错误报文。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值