蚂蚁技术团队:传输币种被篡改问题排查

640?wx_fmt=gif


内容来源:本文转载自战略合作伙伴  金融级分布式架构 (ID:Antfin_SOFA



责编 | 菱酒

 第  707  篇技术好文:7772字 | 18分钟阅读

640?wx_fmt=jpeg


导读  

本文将分享一个在 RPC 调用过程中,传输币种信息被篡改问题的排查过程,借此跟大家分享一些排查问题的思路和经验。

扫描二维码关注公众号,回复: 1735808 查看本文章


本文这个问题只能在预发环境复现,用到的排查工具为蚂蚁内部研发的工具,但大家也可以使用其他类似的工具,比如 greys 等来做替代。以下文章中用到的工具将称为 greys。如果线下遇到疑难问题,能够复现的,也可以直接远程 debug 来排查。本文主要还是想分享以下问题的排查思路,不局限于某些工具的使用。

01

问题描述

_____


首先简要介绍下,出现问题的系统链路可以简化为:

Client 直接调用 Server,可以看到 Server 返回了某个对象币种是 CNY,但是 Client 从自己的调用结果处看到的是 USD。

640?wx_fmt=png

业务同学反馈是线上的问题,应用运行一段时间后出现,一段时间后又会恢复正常,线下未能复现。 

在 Client 机器上,通过 curl 简单测试这个接口,发现返回结果确实是 USD,不符合预期。debug 下游系统 Server 时返回的是 CNY,但是在 Client 这个调用端里面拿到的却是 USD。 当时通过远程 debug 和 greys 都看到了这个情况,以下是直接观察 Server 的实现类返回结果的命令:

 
  
  1. watch com.alipay.server.QueryFacadeImpl convert "{params,returnObj}" -x 4

02

复现步骤

_____


这种问题无法通过现有的中间件或者业务日志进行排查,需要有可以复现的环境。经过业务同学的尝试和观察,发现系统运行一段时间后,这个问题会复现。方便起见,可以直接通过 xfire (历史兼容的调用方式)触发以下操作复现:

 
  
  1. curl -v -d '

  2. <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">;

  3.    <Body>

  4.        <queryList xmlns="http://quotation.api.facade.service.common.client.alipay.com">;

  5.            <in0>

  6.                <userId xmlns="http://request.facade.service.common.client.alipay.com">用户</userId>;

  7.            </in0>

  8.        </queryList>

  9.    </Body>

  10. </Envelope>' -H  "Content-Type:  text/xml;charset=UTF-8  "  -H  "SOAPAction:"  [http://127.0.0.1:8080/client/queryFacade](http://127.0.0.1:8080/client/queryFacade) | xmllint --format - | sed 's/ xmlns=".*"//g'

03

排查

_____


排除业务自己的操作

跟相关同学确认 Server 目标系统本周未发布,但 Client 有变更发布。

对 Client 和 Server 的代码都进行了阅读,可以非常明确地确认,Client 没有任何转换结果的操作,而 Server 线上一直正常。且从现象看到 Server 返回的结果确实正确,而且 Server 的这段返回代码通过 Git 记录可以看到,已经上线运行两年,基本排除 Server 端代码的问题。

同时,jad 反编译看了一下运行中的类代码:

640?wx_fmt=png

可以看到关键位置代码确实没有任何转换操作,直接调用了 Server 的接口,然后就返回了结果。这个原因可以排除了。

确认是否有业务的拦截器干扰

再确认一下拦截器的干扰。因为拦截器有时候写的非常泛化,比如很多开发同学会配置一个 *Facade 用来拦截所有的对外提供的服务,所以还是要确认下是否有拦截器的特殊操作。

Client 端执行下以命令:

 
  
  1. stack com.alipay.client.QueryFacadeClientImpl queryList

可以看到通过 WS 请求的调用栈没什么异常,可以排除这个影响:

 
  
  1. $ stack com.alipay.client.QueryFacadeClientImpl queryList

  2. Press Ctrl+C to abort.

  3. Affect(class-cnt:1 , method-cnt:1) cost in 81 ms.

  4. ts=2017-11-22 13:26:39;thread_name=xxxThreadPool.1--22;id=ac;is_daemon=false;priority=5;TCCL=com.alipay.cloudengine.extensions.equinox.KernelAceClassLoader@48e5707f

  5.    @sun.reflect.GeneratedMethodAccessor209.invoke()

  6.        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

  7.        at java.lang.reflect.Method.invoke(Method.java:597)

  8.        以下省略.
           


排查是否是故障注入


蚂蚁中间件在框架中做了埋点,允许在授权的情况下进行一些故障的注入模拟,因此也进行了排查。

通过检查服务端和客户端的 /logs/appName/sla*.log 相关的日志,可以确认并没有故障故障相关的操作,相关同学从故障注入管控台也同步进行了确认,这一项也可以排除。

排查是否PRC框架序列化本身的问题

前面看到服务端业务返回正常,那么正常处理的下一步就是服务端将业务的 response 序列化成二进制,然后传输给客户端。客户端的 RPC 再将这个二进制数据进行反序列化成 response,所以两端 debug :

通过 greys 在客户端查看反序列化的结果信息:

 
  
  1. watch com.alipay.remoting.rpc.protocol.RpcResponseCommand deserializeContent 'target.responseObject.appResponse' -x 4

同时在服务端看一下序列化 reponse 的信息:

 
  
  1. watch com.alipay.remoting.rpc.protocol.RpcResponseCommand serializeContent 'target.responseObject.appResponse' -x 4

查看服务端序列化结果的时候 appResponse 是对的,注意看currency=@Currency[CNY] 这个字段:

 
  
  1. serialVersionUID=@Long[4967706468767245519],

  2. stockId=@String[2026],

  3. date=@Date[2017-11-22 14:00:00,000],

  4. price=@XXXMoney[

  5.   serialVersionUID=@Long[-4428879431422757138],

  6.   amount=@BigDecimal[656.80],

  7.   DEFAULT_CURRENCY_CODE=@String[CNY],

  8.   DEFAULT_SCALE=@Integer[4],

  9.   DEFAULT_ROUNDING_MODE=@Integer[6],

  10.   currency=@Currency[CNY],

  11.   scale=@Integer[2],

  12. ],

对比了一下客户端和服务端返回的结果:

640?wx_fmt=png

可以很清楚地看到反序列化完成时,客户端收到的结果就已经变了。注意看图中的 currency 字段。

同时说明服务端序列化是没有问题的,问题可能出在客户端反序列化,或者网络传输的过程中。

TCP抓包判断网络

由于序列化之后就是 remoting 的传输,所以需要进行抓包,来确认两端的数据传输是否有问题。

在客户端抓包命令中的 IP 是服务端 IP

 
  
  1. sudo tcpdump -i eth0 -nnvvX 'dst host serverIp'

服务端抓包命令中的 IP 是客户端 IP

 
  
  1. sudo tcpdump -i eth0 -nnvvX 'dst host clientIp'

抓完包直接拷贝到本地对比 16 进制,进行 diff :

640?wx_fmt=png

排除数据的位置问题,关键位置 TCP 包完全一致,问题再次回到序列化。

为了验证 TCP 的二进制包确实没有问题,将客户端收到的 TCP 包本地保存,然后以 byte 数组的方式读入内存:

640?wx_fmt=png

然后通过 RPC 中 Hessian 的反序列化进行操作:

 
  
  1. /**

  2. * Alipay.com Inc. Copyright (c) 2004-2017 All Rights Reserved.

  3. */

  4. package com.alipay.demo.test;

  5. import com.caucho.hessian.io.Hessian2Input;

  6. import org.apache.commons.io.FileUtils;

  7. import org.apache.log4j.Logger;

  8. import java.io.ByteArrayInputStream;

  9. import java.io.File;

  10. import java.io.IOException;

  11. import java.util.Currency;

  12. /**

  13. * @author bystander

  14. * @version $Id: DecTest.java, v 0.1 2017年11月22日 下午3:16 bystander Exp $

  15. */

  16. public class DecTest {

  17.    private static final Logger LOGGER = Logger

  18.            .getLogger(DecTest.class.getName());

  19.    public static void main(String[] t) {

  20.        String result = "";

  21.        String lines = null;

  22.        try {

  23.            lines = FileUtils.readFileToString(new File("~/Downloads/byte.txt"));

  24.        } catch (IOException e) {

  25.            e.printStackTrace();

  26.        }

  27.        String[] arr = lines.split(",");

  28.        byte[] bytes = new byte[arr.length];

  29.        for (int i = 0; i < arr.length; i++) {

  30.            bytes[i] = Byte.parseByte(arr[i]);

  31.        }

  32.        XXXSerializerFactory serializerFactory = new XXXSerializerFactory();

  33.        ByteArrayInputStream input = new ByteArrayInputStream(

  34.                bytes);

  35.        Hessian2Input hessianInput = new Hessian2Input(input);

  36.        hessianInput.setSerializerFactory(serializerFactory);

  37.        try {

  38.            Object object = hessianInput.readObject();

  39.            System.out.println(object);

  40.        } catch (IOException e) {

  41.            e.printStackTrace();

  42.        }

  43.    }

  44. }

结果反序列化正确,可以看到确实还是 CNY , TCP 包传输网络无问题,客户端收到的包也没有问题

640?wx_fmt=png

再看序列化可能的错误

由于只有 Currency 有问题,所以和组内同学经过一系列讨论,反序列化时可以看下客户端 Currency 被调用的情况,直接在服务器上 debug :

 
  
  1. watch java.util.Currency getInstance "{params,returnObj.currencyCode}" -x 2


debug 的结果:


640?wx_fmt=png

图中每一块,上半部分是方法调用入参,下半部分是返回结果的币种信息。

这时候,意想不到的情况出现了。Currency 传进去的入参是 CNY,返回结果却是 USD(业务反馈有时候也会变成其他币种,比如这里复现的时候变成了 HKD),问题基本定位。

然后需要阅读下 Currency 的实现,核心的一个代码段是如下的部分:

 
  
  1. // class data: instance map

  2. private static HashMap instances = new HashMap(7);

这个信息是存储在内部的 Map 中,怀疑被其他操作改掉了,同时重新触发了 USD 等币种的查询:

640?wx_fmt=png

发现是没有问题的,准备 dump 一下内存,看下内存中这个 Currency 内部的  Map 中都有什么值,对 dump 内存进行确认:

640?wx_fmt=png

可以看到,这个 Map 确实改变了,里面有四个值,CNY 对应的值已经是错误的了。

PS:XXX 是 Currency 中用来表示无币种的情况,是由hadoop的类初始化的,跟本问题无关

 
  
  1. https://en.wikipedia.org/wiki/ISO_4217

  2. The code XXX is used to denote a "transaction" involving no currency.


排查其他序列化的影响

由于 Hessian 线上稳定运行,而且 Hessian 的反序列化方案是先 new 一个 Currency,然后反序列化完成后,通过 Currency 的 code 获取到原始的静态类,并不会被内存中原始的单例进行修改。

640?wx_fmt=png

这里 Hessian 有一个 resolve 的过程,如果发现自己本次构造的对象类型有这个 readResolve,就会调用一次,然后反序列化器继续使用原有的实例。

640?wx_fmt=png

这里 readResolve() 的作用是将返回的对象替换反序列化创建的实例,主要使用在单例的场景,在序列化和反序列化中主要用于用返回的对象代替反序列化的对象,否则的话,反序列化会导致内存中有很多单例对象。

这个单例类的改动,业务方肯定不会直接调用,因此怀疑是其他序列化方案导致的。跟系统 owner 沟通,业务确实使用了 ProtoStuff 的一个方案做缓存对象的序列化和反序列化,通过注解来生效,所以看代码时没有观察到。

跟组内同学一起看了一下 ProtoStuff 序列化的代码,构造一个单测。先描述一下这几个关键类的关系,简化之后:

 
  
  1. XXXMoney:

  2.   currency:java.util.Currency

  3.     currencyCode:java.lang.String

类结构比较简单,XXXMoney 中有一个属性是 Currency 类型, Currency里面有一个关键属性是 currencyCode。以下是测试代码:

 
  
  1. /**

  2. * Alipay.com Inc. Copyright (c) 2004-2017 All Rights Reserved.

  3. */

  4. package com.alipay.demo.test;

  5. import com.alipay.xxx.util.money.XXXMoney;

  6. import com.alipay.client.cache.provider.CacheValueSizeOverflowException;

  7. import com.alipay.client.cache.provider.ProtostuffSerializeHelper;

  8. import org.apache.log4j.Logger;

  9. import java.util.Currency;

  10. /**

  11. * @author bystander

  12. * @version $Id: DecTest.java, v 0.1 2017年11月22日 下午3:16 bystander Exp $

  13. */

  14. public class SecTest {

  15.    private static final Logger LOGGER = Logger

  16.            .getLogger(SecTest.class.getName());

  17.    public static void main(String[] t) {

  18.        XXXMoney xxxMoney =null;

  19.        ProtostuffSerializeHelper helper = new ProtostuffSerializeHelper();

  20.        byte[] bytes;

  21.        try {

  22.            xxxMoney =new XXXMoney(11.0,Currency.getInstance("USD"));

  23.            bytes = helper.serialize(xxxMoney, XXXMoney.class);

  24.             helper.deserialize(bytes, XXXMoney.class);

  25.            System.out.println( Currency.getInstance("CNY"));

  26.        } catch (CacheValueSizeOverflowException e) {

  27.            e.printStackTrace();

  28.        }

  29.    }

  30. }

以下是 debug 执行的结果,可以看到此时内存中已经被修改。

 
  
  1. 640?wx_fmt=png

04

原因分析

_____

deserialize 的时候会调用以下代码:

 
  
  1. /**

  2. * 反序列化

  3. *

  4. * @param <T> the type parameter

  5. * @param raw the raw

  6. * @param cls the cls

  7. * @return the t

  8. */

  9. public <T> T deserialize(byte[] raw, Class<T> cls) {

  10.  Schema<T> schema = RuntimeSchema.getSchema(cls);

  11.  T val = schema.newMessage();

  12.  ProtobufIOUtil.mergeFrom(raw, val, schema);

  13.  return val;

  14. }

这段代码的 schema.newMessage() 会调用类的构造函数,此时会调用到 XXXMoney 的无参构造函数,默认是 CNY,内存中此时会生成了一个 CNY 的 Currency 单例,并且此时 XXXMoney 的 currency 字段已经有这个值的引用了。 然后把这 val(XXXMoney) 传到反序化处理里面,进行反序列化各个字段的赋值赋值是一个嵌套循环的过程,会不断的从当前类中获取已有的属性,进行这个属性其他字段的赋值。

640?wx_fmt=png

嵌套赋值的时候,关键在于这个序列化工具是将 XXXMoney 这个对象的 currency 字段对应的值直接拿出来(持有单例的引用),然后赋值 currency 的其他属性,也就是这里 getObject 的方法。

嵌套执行到 currency 时,此时反序列到 Currency 这个属性的时候,二进制里面是 USD,所以直接把这个 Currency 引用里面的币种 (CurrencyCode) 给改了(通过 unsafe 操作修改了单例),并且变成了 USD,导致出现这个诡异的场景。

640?wx_fmt=png

05

结论

_____

序列化方案使用有 bug,需要对单例类做处理。比较困难是这个由于是非常底层的类,所以在问题排查初期很难直接定位到该问题,需要不断精确范围排查,逐个排除验证。

另外,序列化是一个比较难的操作,最好使用蚂蚁已经久经考验,修改过的 Hessian。

后来,针对这个问题,跟 ProtoStuff 的作者进行了沟通,作者认为用法中对于这种单例是提供了一个 delegate 的功能的,但是需要使用的人自己识别出来这个问题。这个我们保留意见,毕竟,单例的类很多,如果依赖与人为识别来使用,那么成本和风险都会很高。


640?wx_fmt=png


目前60000+人已关注加入我们

640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif

640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif

推荐阅读 盒子科技刘恒:聚合支付系统演讲
手把手教你搭建一个基于Java的分布式爬虫系统
知识付费时代,程序员,你的知识在哪里?
深入浅出分布式缓存的通用方法
蚂蚁金服开源 | 在 Spring Boot 中集成 SOFABoot 类隔离能力

640?wx_fmt=jpeg

中生代技术

每天早上7点,推送有营养的干货文章;

总覆盖会员60000+人;资深架构、总监等职位以上3000+人。

定期在线分享超过100期,线下技术沙龙超过70次、覆盖20多个等城市!

关注技术架构、研发管理、互联网金融、电商、大数据、区块链、人工智能等方向!


加入中生代技术群聊,请添加白明微信:zsdwyq,注明姓名、职称和技术方向,通过后加入中生代技术群,和群友们共同学习成长!

↓↓↓ 点击"阅读原文" 【查看跟 ProtoStuff 的作者沟通的 issue】  

猜你喜欢

转载自blog.csdn.net/k6t9q8xks6iikzppifq/article/details/80650241
今日推荐