• 这也能造成故障?我只给DTO类加了一个属性


    看到标题,能不能直接猜到是什么原因引发的线上问题呢?

    实际问题描述

    标题让问题定位简化了,实际上当时的现象是这样子的:

    应用A有一个基类声明如下,基类中有一些属性:

    // 基类声明
    public class BaseDTO implements Serializable {
    	protected static final long serialVersionUID = 3810578658981789515;
    }
    
    • 1
    • 2
    • 3
    • 4

    并建立了一个子类,定义如下:

    public class SubDTO extends BaseDTO {
    	// 相关属性
    }
    
    • 1
    • 2
    • 3

    同时建立一个DTO对象继承子类,定义一些属性,如下:

    public class BizDTO extends SubDTO implements Serializable {
        private static final long serialVersionUID = 555425620986718872L;
    }
    
    • 1
    • 2
    • 3

    最后,另一个DTO对象引用该 BizDTO 对象,如下:

    public class AnotherDTO implements Serializable {
    	private static final long serialVersionUID = 8834710016018492631L;
    	private BizDTO bizDTO;
    }
    
    • 1
    • 2
    • 3
    • 4

    应用B依赖应用A的二方包(包含上述类),同时应用B中会用到缓存中间件,AnotherDTO 对象会被缓存。
    然而应用A、B发布上线后…

    当然一切正常。

    随着时间的推移,应用A升级了二方包版本号,应用B在随后的变更中,也 仅仅更改了二方包的版本号

    当应用B发布变更后…

    应用B线上出现大量限流相关的错误…这到底是怎么回事呢?

    发生了什么

    导火索就是应用A的变更,仅仅将 SubDTO 的声明更改了:

    public abstract class SubDTO extends BaseDTO {
    	// 相关属性
    }
    
    • 1
    • 2
    • 3

    就这样一个声明变更(实际上增加属性也一样的结果),就导致了另一个应用的线上被其他服务限流的问题。如果没有处理好,则会造成相关业务延迟,造成投诉等从而引发故障。

    这是为什么呢?

    根本原因分析-Java序列化

    如果一个类实现了 Serializable ,且未自定义 serialVersionUID,在jvm运行时在需要序列化的时候会计算生成SUID,生成规则稍后再看。总之,未指定SUID,则会默认生成。这也就意味着SUID会按照生成规则而变化。

    而序列化与反序列时的SUID不一致,则会造成反序列化时失败,抛出InvalidClassException 异常。

    你可能会说:不对啊,BaseDTO 中的 SUID声明为protected ,SubDTO 中应该会继承才对。

    错,就算改为 public 也不行。

    所以,需要遵循官方文档中描述的建议:

    It is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected serialVersionUID conflicts during deserialization, causing deserialization to fail.

    每个实现 Serializable 接口的类,都需要自定义SUID字段。

    此外,实际开发中,对于用到的中间件,以及数据传输、存储等场景,需要考虑使用的序列化实现方式。

    本次故障中,缓存中间件默认了Java序列化方式。应用变更前写入缓存的对象,应用变更后从缓存读取对象时,由于SUID不一致,导致序列化失败,也即读缓存失败。读缓存失败后,就会通过rpc远程请求相关服务获取数据,从而拉高了请求的QPS,导致限流发生。

    假设,相关服务接口并未设置限流,且应用B流量较大,长链路请求中,QPS放大,则可能打挂一些下游,非常可怕。

    考考你-如果这样改,会触发故障吗?

    对于子类,我们不变更声明,但是多新增一个属性如下:

    public class SubDTO extends BaseDTO {
    	private static String nn;
    	// 相关属性
    }
    
    • 1
    • 2
    • 3
    • 4

    应用A仅变更这一点,还会出现反序列化失败的问题吗?

    那如果这样改成这样呢?会有问题吗?

    public class SubDTO extends BaseDTO {
    	public static String nn;
    	// 相关属性
    }
    
    • 1
    • 2
    • 3
    • 4

    答案不写了,下一小节给出的官方文档小节中有答案。你可以自行测试。

    Java序列化测试工具及文档

    补充前面所说的默认序列化的规则,参见如下文档的4.6小节:链接
    里面描述了默认SUID的生成机制,会根据类描述符、属性、方法签名等来计算。

    此外,想要写测试验证序列化SUID,可以利用如下方法来获得SUID:该类也在上述文档中4.1小节有描述。

    ObjectStreamClass.lookup(AnotherDTO.class).getSerialVersionUID();
    
    • 1

    对于线上运行的程序,也可以通过命令行来获得:
    serialver [-classpath classpath] [classname...]

    最后,SUID的生成不是在编译期,class文件中不会包含。

  • 相关阅读:
    关于ABB机器人的IO创建和设置
    Codeforces Round 940 (Div. 2) C. How Does the Rook Move?
    动手学习深度学习-《线性代数实现》
    【8.6】代码源 - 【前缀集】【矩阵游戏】【谁才是最终赢家?】【放置多米诺骨牌】
    Django基础讲解-路由控制器和视图(Django-02)
    ES6 入门教程 10 对象的扩展 10.2 属性名表达式 & 10.3 方法的name 属性
    音视频与CPU架构
    C#宏定义
    基于PSO粒子群优化的汽车刹车稳定性数据matlab仿真与分析
    华为云云耀云服务器 L 实例评测|配置教程 + 用 Python 简单绘图
  • 原文地址:https://blog.csdn.net/zhou307/article/details/127639470