Guava Range引起的一次故障

Written with StackEdit.

一次线上故障的爆发

正当我去人大吃饭的时候,突然收到了atp报警电话,马上进群去看什么情况,然后有人爆出说是dubbo反序列化对象失败

问题排查

  1. 群里反馈说是因为没有构造函数
  2. 反序列化的代码如下:
/**
 * @author yanming
 */
@Getter
@Setter
public class Promotion implements Serializable {

    private static final long serialVersionUID = 1242342234235335346L;

    private int code;

    private String desc;

    private Range<Integer> days;
}

刚开始怀疑是这个对象因为使用lombok的注解导致没有默认的构造函数,编译之后发现其实是有默认无参构造函数
3. 接着思考那应该是这个对象的某个域是个对象,这个类型是没有无参构造函数,然后就怀疑到了Range,这个Range是使用的guava的类,接着我就去查看这个类的源码发现:

private Range(Cut<C> lowerBound, Cut<C> upperBound) {  
  this.lowerBound = checkNotNull(lowerBound);  
 this.upperBound = checkNotNull(upperBound);  
 if (lowerBound.compareTo(upperBound) > 0  
  || lowerBound == Cut.<C>aboveAll()  
      || upperBound == Cut.<C>belowAll()) {  
    throw new IllegalArgumentException("Invalid range: " + toString(lowerBound, upperBound));  
  }  
}

这个构造函数是私有的,根据爆出来的日志来看,正是分析得出调用Range的构造函数触发np,导致穿件实例失败

和同事的交流

dubbo的默认使用的是hession的序列化和反序列化,反序列化的时候需要需要先调用构造函数创建对象,然后再把反序列化的数据set到对应的域里,那如果有多个构造函数的的时候,会怎么取选择呢?
接着我去看了下hession的反序列化源码:

public class JavaDeserializer extends AbstractMapDeserializer {  
  private Class<?> _type;  
 private HashMap<?,FieldDeserializer> _fieldMap;  
 private Method _readResolve;  
 private Constructor<?> _constructor;  
 private Object []_constructorArgs;  
 public JavaDeserializer(Class<?> cl)  
  {  
    _type = cl;  
  _fieldMap = getFieldMap(cl);  
  
  _readResolve = getReadResolve(cl);  
  
 if (_readResolve != null) {  
      _readResolve.setAccessible(true);  
  }  
  
    Constructor<?> []constructors = cl.getDeclaredConstructors();  
 long bestCost = Long.MAX_VALUE;  
  
 for (int i = 0; i < constructors.length; i++) {  
      Class<?> []param = constructors[i].getParameterTypes();  
 long cost = 0;  
  
 for (int j = 0; j < param.length; j++) {  
        cost = 4 * cost;  
  
 if (Object.class.equals(param[j]))  
          cost += 1;  
 else if (String.class.equals(param[j]))  
          cost += 2;  
 else if (int.class.equals(param[j]))  
          cost += 3;  
 else if (long.class.equals(param[j]))  
          cost += 4;  
 else if (param[j].isPrimitive())  
          cost += 5;  
 else  cost += 6;  
  }  
  
      if (cost < 0 || cost > (1 << 48))  
        cost = 1 << 48;  
  
  cost += (long) param.length << 48;  
  
 if (cost < bestCost) {  
        _constructor = constructors[i];  
  bestCost = cost;  
  }  
    }  
  
    if (_constructor != null) {  
      _constructor.setAccessible(true);  
  Class<?> []params = _constructor.getParameterTypes();  
  _constructorArgs = new Object[params.length];  
 for (int i = 0; i < params.length; i++) {  
        _constructorArgs[i] = getParamArg(params[i]);  
  }  
    }  
  }

竟然搞了一个权重机制,选择了权重最低的那个构造函数(原以为是固定的调用无参构造函数),所以只要有可用的构造函数就可以反序列化成功(当然得有set方法),Range只有一个有参的构造函数,传入的参数默认是null(是非基础类型),这时候会调用参数的方法,导致np

关于在调用构造函数的时候传入的是null,看getParamArg(params[i])方法源码:

/**
 * Creates a map of the classes fields.
 */
protected static Object getParamArg(Class cl)
{
  if (! cl.isPrimitive())
    return null;
  else if (boolean.class.equals(cl))
    return Boolean.FALSE;
  else if (byte.class.equals(cl))
    return new Byte((byte) 0);
  else if (short.class.equals(cl))
    return new Short((short) 0);
  else if (char.class.equals(cl))
    return new Character((char) 0);
  else if (int.class.equals(cl))
    return Integer.valueOf(0);
  else if (long.class.equals(cl))
    return Long.valueOf(0);
  else if (float.class.equals(cl))
    return Float.valueOf(0);
  else if (double.class.equals(cl))
    return Double.valueOf(0);
  else
    throw new UnsupportedOperationException();
}


/**
 * Determines if the specified {@code Class} object represents a
 * primitive type.
 *
 * <p> There are nine predefined {@code Class} objects to represent
 * the eight primitive types and void.  These are created by the Java
 * Virtual Machine, and have the same names as the primitive types that
 * they represent, namely {@code boolean}, {@code byte},
 * {@code char}, {@code short}, {@code int},
 * {@code long}, {@code float}, and {@code double}.
 *
 * <p> These objects may only be accessed via the following public static
 * final variables, and are the only {@code Class} objects for which
 * this method returns {@code true}.
 *
 * @return true if and only if this class represents a primitive type
 *
 * @see     java.lang.Boolean#TYPE
 * @see     java.lang.Character#TYPE
 * @see     java.lang.Byte#TYPE
 * @see     java.lang.Short#TYPE
 * @see     java.lang.Integer#TYPE
 * @see     java.lang.Long#TYPE
 * @see     java.lang.Float#TYPE
 * @see     java.lang.Double#TYPE
 * @see     java.lang.Void#TYPE
 * @since JDK1.1
 */
public native boolean isPrimitive();

可以看出如果不是基础类型的域,返回的是null。

当事人的信息反馈

出现故障了,当事人就开始诉说来龙去脉:

  1. 他首先在jar包里升级了dubbo接口的对象,新增了一个对象,类型如上
  2. 然后就开始发布,发布过程中,订单开始掉
  3. 然后回滚了

我很好奇,问:是不是调用方已经升级过了这个jar包,不然应该不会反序列化我们新增的字段吧?
当时人说:没有啊,他们没有更新jar包,只有我们自己升级了jar
我就好奇了,那为啥会反序列化异常呢?难道即使找不到这个类,也会反序列化这个对象的域?
看异常栈:

Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 'com.google.common.collect.Range' could not be instantiated
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:279)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:159)
    at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:357)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2070)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2014)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1990)
    at com.alibaba.com.caucho.hessian.io.MapDeserializer.readObject(MapDeserializer.java:134)
    at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:359)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2070)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2014)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1990)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:239)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:161)
    at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:357)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2070)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2014)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1990)
    at com.alibaba.com.caucho.hessian.io.CollectionDeserializer.readLengthList(CollectionDeserializer.java:93)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1678)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize(JavaDeserializer.java:400)

最晚的那个JavaDeserializer就是对Range进行了反序列化,然后我们依次去看发现猫腻:

    at com.alibaba.com.caucho.hessian.io.MapDeserializer.readObject(MapDeserializer.java:134)
    at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:359)

不难看出这应该是没有找到反序列化的类定义,就选择了默认的map解析方法,也就是把我们的对象解析成了hashMap,然后再依次去解析内部数据。

故障原因总结

  1. dubbo接口新增了新类型对象,对象域成员使用了guava的Range
  2. consumer没有新对象类型定义,hession反序列化把对象转成了HashMap,继续解析对象域成员
  3. 解析到了Range类型的域成员,由于Range的构造函数set了null的参数,抛出np,导致构造实例失败

为什么自动化没有问题?

  1. 接着我就思考,自动化里验证了该dubbo接口的数据,为什么自动化没有问题呢?
  2. 于是我去看自动化,发现自动化其实并没有直接调用dubbo接口,而是写了个jsp调用方法获得结果对象,然后转成了json数据,也就是说其实自动化的assert并没有经过dubbo序列化和反序列化过程

如何改进该问题:

开发角度:

  1. 对于对外交互,我们需要在使用对象的时候需要思考会不会出现序列化和反序列化问题,尽量使用基础类型
  2. 自测的时候,需要验证dubbo接口数据(对外接口有变更,还是要验证下数据正确性)
  3. 线上灰度验证,应该验证线上dubbo接口数据

自动化角度:

  1. 自动化验证接口的方式应该完全模拟接口,尽量不要使用后门的方式
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值