全部产品

故障排查之 RPC

更新时间:2020-10-09 17:22:43

本文汇总梳理了 RPC 使用过程中遇到的常见问题及排查思路。

RPC-02306: 没有获得服务 [{0}] 的调用地址,请检查服务是否已经推送

RPC 客户端调用服务时,收到如下错误信息:“RPC-02306: 没有获得服务 [{0}] 的调用地址,请检查服务是否已经推送。”

排查思路如下:

  • 检查服务地址是否推送
    登录客户端,查看 /home/admin/logs/rpc/sofa-registry.log 日志,可以用服务接口名过滤日志找到最后一次推送记录。如果发现服务端地址没有推送到客户端,建议首先排查服务是否注册成功。例如,以下日志中有 可调用目标地址[0]个 的记录,则说明 com.alipay.share.rpc.facade.SampleService 的服务端地址没有推送到客户端:

    1. RPC-REGISTRY - RPC-00204: 接收 RPC 服务地址:服务名[com.alipay.share.rpc.facade.SampleService:1.0@DEFAULT]
    2. 可调用目标地址[0]个
  • 检查客户端启动时是否收到 RPC Config 推送
    查看 /home/admin/logs/rpc/rpc-registry.log 日志,确定最近一次 RPC 客户端的启动时间。根据客户端上次启动时间和服务接口名过滤日志,检查对应的接口是否有 Receive Rpc Config info 的记录。如果没有也会导致后续无法调用服务,可以考虑重启客户端。

  • 检查服务是否注册成功
    登录 SOFA 应用中心 查看服务注册情况,或登录服务端查看 /home/admin/logs/confreg/config.client.log 日志。如果有服务发布相关的错误,可根据日志信息进一步排查。

  • 检查服务调用是否早于地址推送时间
    如果客户端日志 sofa-registry.log 显示服务地址已经推送,但是 RPC-02306 错误发生的时间在服务地址推送之前,这种情况多发生在调用服务时,客户端应用还没有完成启动。这种情况出现的原因多为业务系统自己通过定时任务调用服务,或者在 bean 初始化完成后就开始调用服务。此种情况可以通过如下配置来 address-wait-time 解决。

配置项 描述 默认值
address-wait-time reference 生成时,等待服务注册中心将地址推送到消费方的时间。address-wait-time 的最大值为 30000 ms,超过这个值的配置将调整为 30000 ms。 0 (ms)
  • 检查 RPC 服务端和客户端应用配置信息是否匹配
    分别打开服务端和客户端应用的配置文件 application.properties,查看以下参数是否配置相同,如配置不同,RPC 客户端将无法感知 RPC 服务端。
    • com.alipay.instanceid
    • com.antcloud.antvip.endpoint
  • 检查服务注册中心连接
    运行以下命令以检查客户端和服务端与服务注册中心的连接情况:

    1. netstat -a |grep 9600

    9600 端口是服务注册中心的监听端口,客户端和服务端与 9600 端口建立长连接,向服务注册中心发布和订阅服务。如果客户端或者服务端与 9600 端口的连接断开,则需要重启应用恢复,并进一步排查端口异常断开的原因。

  • 检查RPC服务端地址绑定
    登录 RPC 服务端,运行以下命令:

    1. ps -ef|grep java

    查看进程启动参数rpc_bind_network_interfacerpc_enabled_ip_range 是否绑定了正确的 IP 地址。

Rpc invocation timeout[responseCommand TIMEOUT]

若调用 RPC 服务超时,在客户端的 logs/tracelog/middleware_error.log 日志中,可看到如下异常信息:

  1. 2018-07-06 13:21:20.463,sofa2-rpc-client,707c27b9153085447746110464663,0,main,timeout_error,rpc,invokeType=sync&uid=&protocol=bolt&targetApp=sofa2-rpc-server&targetIdc=&targetCity=&paramTypes=&methodName=message&serviceName=com.alipay.share.rpc.facade.SampleService:1.0&targetUrl=10.160.34.141:12200&targetZone=&,,com.alipay.sofa.rpc.core.exception.SofaTimeOutException: com.alipay.remoting.rpc.exception.InvokeTimeoutException: Rpc invocation timeout[responseCommand TIMEOUT]! the address is 10.160.34.141:12200
  • 补充背景
    RPC 调用时序图时序图有关客户端和服务端各阶段的耗时信息,请参考 RPC Tracer 日志

  • 排查步骤
    PRC 调用超时一般可以按照如下顺序逐步排查:

    1. 服务本身确实超时:如业务代码处理时间过长。
    2. 服务端 RPC 线程池耗尽
    3. GC 问题(Garbage Collection,简称 GC),导致线程停止。
    4. 网络延时抖动
    5. 其他外部因素影响服务器性能,如定时任务、批处理,或者与宿主机上其他虚拟机、容器发生资源争抢。

服务本身超时

默认情况下,RPC 的超时时间为 3 秒。要确定某个请求的实际处理时间,您可登录服务端,查看 logs/tracelog/rpc-server-digest.log 日志。根据客户端超时日志中的 traceID,如 707c27b9153085447746110464663,找到服务端处理对应请求的日志。

日志格式如下所示:

  1. 2018-07-06 13:21:22.441,sofa2-rpc-server,707c27b9153085447746110464663,0,com.alipay.share.rpc.facade.SampleService:1.0,message,bolt,,10.160.33.96,sofa2-rpc-client,,,4001ms,0ms,SofaBizProcessor-12200-0-T46,02,,,1ms,,

上述日志中服务端业务代码处理时间为 4001 毫秒。

由于 RPC 调用默认的超时时间是 3 秒,如果日志中的耗时大于 3 秒或者非常接近 3 秒,建议首先从服务端本身排查:

  • 服务端业务代码执行慢。
  • 服务端本身有外网服务调用,或者服务端又调用了其他 RPC 服务(client > RPC Server A > RPC Server B),此种情况需要分别排查 A 和 B,定位问题。
  • 服务端有数据库操作,如数据库连接耗时、慢 SQL 等。

服务端 RPC 线程池耗尽

登录服务端查看 rpc/tr-threadpool 日志。如果发生 RPC 线程池队列阻塞,先确认是否发生超时的时间段有业务请求高峰,或者用 jstack 查看业务线程是否有等待或者死锁情况,导致 RPC 线程耗尽。

更多信息,请参见 RPC 线程池大小、队列长度配置说明

GC 问题

某些 GC 类型会触发“stop the world”问题,会将所有线程挂起。若要排查是否是 GC 导致的超时问题,可以通过以下方法开启 GC 日志。

方法一

config/java_opts 文件中加入以下启动参数,并重新打包发布。

  1. -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/admin/logs/gc.log

方法二

  1. kill -15 命令结束服务端进程。
  2. 手动启动 RPC 服务。运行 su admin 进入 admin 用户,用如下 nohup 形式启动 RPC 服务:
    1. $ nohup java -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/admin/logs/gc.log -Drpc_bind_network_interface=eth0 -Dspring.profiles.active=&{环境标识} -jar /home/admin/app-run/sofa2-rpcserver-service-1.0-SNAPSHOT-executable.jar &

    说明环境标识 可在控制台中选择 产品与服务 > 金融分布式架构 > 环境资源管理 > 环境 中查看。

等待下次 RPC 超时发生后,查看 gc.log 验证超时的时间段是否有耗时较长的 GC,尤其是 Full GC。

网络延时抖动

排查是否由于网络问题导致RPC调用超时:

1.在客户端和服务端运行 tsar -i 1 查看问题发生的时间点是否有网络重传。

2.在客户端和服务端同时部署 tcpdump 进行循环抓包,问题发生后分析网络包。

3.在客户端和服务端运行 ping 观察是否存在网络延时。

如何打印客户端 RPC 调用统计?

可以参考以下示例语句,打印调用 sofa2-rpc-server 的应用超过3秒的请求总数、服务端IP、服务应用和客户端IP。实际使用时,将 sofa2-rpc-server 替换成对应的服务端应用名称,并根据日志中处理时长所对应列的具体位置调整 $18 数值。打印信息也可以根据需要调整。

  1. $ grep sofa2-rpc-server rpc-client-digest.log | awk -F, '{if(int($18)>3000)print $9,$10,$27}' |sort | uniq -c | sort -n

为什么 SOFABoot 应用已经启动,但服务没有发布成功?

排查思路方面,主要分为下述几种情形:

  • 应用非正常启动:通常可以查看 health-check 日志。如果有 error 日志,可以根据相关信息进行排查,常见的故障信息包括:
    • redis 没有正确配置。
    • 一个服务在本地开启了多个实例。
    • bolt 服务配有启动,并发现端口占用等。
  • ACVIP问题
    • 专有云环境,要确保下述信息配置正确:
      • instanceid、endpoint。
      • access、secret:这两个在确定接入 IAM 才需写入。
    • 如果上述项配置错误,则会在 logs/registry/registry-client.log 中出现 Vip(null) endpoint might be wrong 的错误。该错误意味着当前 acvip 配置有误或 acvip 出现网络故障,此时需要联系运维同学检查 acvip 和前端负载均衡器的网络连通性。
  • 注册中心:如果应用已经启动,但服务没有发布成功,则按下述步骤排查:
    1. 查看注册中心内有没有服务被注册: 如果没有,则排除注册中心的故障。
    2. 查看是否是 acvip 的问题。如果排除后,服务还有问题,按下述步骤排查:
      1. 观察应用容器是否有类似 /Users/xxx/conf/acvip-java-client-cache/domains/0000X-DSR_HTTP.json 这样的文件。如果有,可进入该文件查看其内容,一般都是缓存到本地的 DSR 注册中心地址,可自行检查是否有异常。例如:健康检查不通过,IP 没有获取正确等。
      2. 通过命令判断当前注册中心是否正常,示例如下:curl -i -XPOST {antvip}:9003/antcloud/antvip/instances/get -d '{"vipDomainName2ChecksumMap":{"000001-DSR_CLOUD":"N"}}'。如果不正常,请检查注册中心是否配置正确。
  • 服务提供方的运行模式:云上发布时,未修改 run.mode=DEV 参数。在 DEV 模式下,将只注册到本地,而不会注册到注册中心里。

如何将 Dubbo 内部项目迁移到 SOFABoot 上?

问题描述:

  • 如何将 Dubbo 内部项目迁移到 SOFABoot 上?
  • 如果第三方需要保有 Dubbo,系统要如何设计?

解决思路如下:
系统改造过程中,并不能确保所有关联系统一次性都改造完成,会面临需要和历史系统兼容的场景,例如:一个服务被改造成 SOFA Bolt 服务后,发现还有调用方依然是依赖 Dubbo 的。那么,一个简单的兼容办法为:这个服务同时暴露 BOLT 和 Dubbo 服务。

在 SOFABoot 中暴露 Dubbo 服务,步骤如下:

  1. 加入 Dubbo 的 starter 依赖,示例如下:

    1. <dependency>
    2. <groupId>com.alibaba.boot</groupId>
    3. <artifactId>dubbo-spring-boot-starter</artifactId>
    4. <version>0.1.1</version>
    5. </dependency>
    6. <!-- Dubbo -->
    7. <dependency>
    8. <groupId>com.alibaba</groupId>
    9. <artifactId>dubbo</artifactId>
    10. <version>2.6.4</version>
    11. </dependency>
    12. <!-- Spring Context Extras -->
    13. <dependency>
    14. <groupId>com.alibaba.spring</groupId>
    15. <artifactId>spring-context-support</artifactId>
    16. <version>1.0.2</version>
    17. </dependency>
  2. 配置 application.properties,示例如下:

    1. ################ common configuration ##############
    2. spring.application.name=bank-dubbo-provider
    3. logging.level.com.dubbo.example=INFO
    4. logging.path=./logs
    5. ################ dubbo configuration ##############
    6. demo.service.version = 1.0.0
    7. dubbo.application.id = bank-dubbo-provider
    8. dubbo.application.name = bank-dubbo-provider
    9. ################ sofa configuration ##############
    10. run.mode=DEV
    11. com.alipay.sofa.rpc.bolt-port=12201
    12. # shared middleware
    13. com.alipay.env=shared
    14. com.alipay.instanceid=IPYJUBMB231N
    15. com.antcloud.antvip.endpoint=100.103.1.174
    16. com.antcloud.mw.access=uPxHLxsMmstcQCNWEh
    17. com.antcloud.mw.secret=TyMlUB9uGRMzcc2pG0dMv6xzUXCMA1WI
  3. 添加 Dubbo 服务发布,示例如下:

    说明:需要引入 Dubbo 的 schema,这样基于 Dubbo 的定义才会被显示,并且 Dubbo 的注册中心是 zk,也需要配置。

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <beans xmlns="http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xmlns:sofa="http://schema.alipay.com/sofa/schema/slite"
    5. xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    6. xmlns:context="http://www.springframework.org/schema/context"
    7. xsi:schemaLocation="http://www.springframework.org/schema/beans
    8. http://www.springframework.org/schema/beans/spring-beans-3.0.xs
    9. http://schema.alipay.com/sofa/schema/slite http://schema.alipay.com/sofa/slite.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    10. <!-- dubbo zookeeper configuration -->
    11. <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
    12. <dubbo:protocol name="dubbo" port="20880"/>
    13. <!-- bean define -->
    14. <bean id="dubboService" class="com.dubbo.example.service.DubboServiceImpl"/>
    15. <!-- sofa service -->
    16. <sofa:service interface="com.dubbo.example.facade.DubboService" ref="dubboService" unique-id="sofaDubboService">
    17. <sofa:binding.bolt />
    18. </sofa:service>
    19. <!-- dubbo service -->
    20. <dubbo:service interface="com.dubbo.example.facade.DubboService" ref="dubboService" version="1.0.0"/>
    21. </beans>
  4. 更新 main 函数,开启 Dubbo,示例如下:

    1. @ImportResource({"classpath*:META-INF/bank-dubbo-provider/*.xml"})
    2. @org.springframework.boot.autoconfigure.SpringBootApplication
    3. @EnableDubbo
    4. public class SOFABootSpringApplication {
    5. private static final Logger logger = LoggerFactory.getLogger(SOFABootSpringApplication.class);
    6. public static void main(String[] args){
    7. SpringApplication springApplication = new SpringApplication(SOFABootSpringApplication.class);
    8. ApplicationContext applicationContext = springApplication.run(args);
    9. }
    10. }

RPC 调用时报 02306,服务无法向注册中心发布。

错误现象如下:

  • 调用时错误日志如下:
    日志报错.png
  • 查看 RPC 注册中心日志,却未发现注册中心,示例如下:
    未发现注册中心.png
  • 发布服务无效,查看健康检查日志,发现问题如下:
    error.png

故障原因:bolt 端口 12200 被占用。
解决方案:更换端口或者把本地占用端口服务关闭。

报错信息:com.alibaba.com.caucho.hessian.io.HessianFieldException:’xxxx’ could not be instantiated

  • 问题分析
    问题1.jpg问题2.jpg
    通过对上面的 2 个日志进行分析,结合日志的提示,可以得出下述结论:

    • 进行 RPC 调用的时候,参数序列化失败。
    • java.util.Local 这个类不能实例化,原因是 Local 这个类 在init 的时候 出现了空指针的异常。
  • 解决思路:修改序列化方法。

如何解决应用启动失败,服务无法注册?

应用依赖了一些 SOFA 组件,但在本地环境中只想测一下 RPC,如何处理下述问题:

  • 应用启动失败
  • 服务无法注册

解决思路
健康检查机制会在项目启动的时候对所有组件进行探活,如果此时引用了 DDCS(Distributed Dynamic Configuration Service) 或者其它组件,会出现应用启动正常,但 RPC 无法在本地注册的现象。此时,如果业务暂时没有用到这些组件,可以在 healthcheck 时略过这些检查。本质原因是这些组件会到 antvip 中寻找组件地址,而此时应用并不在云上,所以会失败。

具体操作:
application.properties 中添加下述配置,以略过所有组件的健康检查。

  1. com.alipay.sofa.healthcheck.skip.component=true

注意:此方案只建议在测试中使用,线上环境一定要打开健康检查。

SOFARest 接口在上传文件时,文件超过 10M 时会报错,如何处理?

SOFARest 接口在上传文件时,文件超过 10M 时会报错,报错信息如下:

  1. ERROR org.jboss.resteasy.core.ExceptionHandler - failed to execute
  2. javax.ws.rs.NotFoundException: Could not find resource for full path: http://unknown/bad-request
  3. at org.jboss.resteasy.core.registry.ClassNode.match(ClassNode.java:73)
  4. at org.jboss.resteasy.core.registry.RootClassNode.match(RootClassNode.java:48)

故障排查主要步骤包括:

  1. 在日志里将该报错打开,将 Netty 中 Http 相关类的日志改成 debug 模式,即可实现,示例如下:
    1. logging.level.io.netty.handler.codec.http.HttpObjectAggregator=DEBUG
  2. 获取具体报错的原因,例如 Failed to send a 413 Request Entity Too Large

  3. application.properties 文件里设置相应参数。

    1. com.alipay.sofa.rpc.RestMaxRequestSize=104857600
  4. 后续确认:假如应用的内存容量默认比较小,比如 1 G 或者 2 G,而 Netty 的 REST 所请求的 Payload 是放在 DirectMemory 里的,且该 DirectMemory 有个最大值,默认是系统 JVM 初始化时所申请内存大小。如果上传大文件时发生了 OOM 或者 DirectMemory 内存溢出的错误,需要进行下述处理:
    1. 确定当前系统的内存大小是否能够承受所传大文件。
    2. 确认运行时内存或者 DirectMemory 的最大值。
    3. 如果可以优化,请选择分段上传。

客户端调用远端服务时,有大量的超时,但服务端的响应正常,且耗时很少,该如何排查?

通过对下述项进行判断来进行排查:

  • 线程池发生了阻塞:这个一般在多级链路调用时,才会发生,比如:A 调 B 再调 C,B 作为服务端的线程发生了阻塞,则需要查看 B 的 tr-threadpool.log 日志。

  • GC 处理遇到严重故障:框架遇到 GC 的故障很少,但也不排除在某些特定的场景会发生的概率。一般查看logs/stdout.log 日志文件,并据此查看 CMS-remark,YG,ParNew 等指标,它们都标识着 STW,从而会导致 JVM 停顿。

  • 硬件磁盘IO故障:一般通过 tsar -I 1 查看下一分钟内的 IO 请求次数。在某些场景下,如果磁盘 IO 较高,则会影响到整个系统的性能。

  • 网络故障:网络问题一般都很难定位,这里介绍一个较为常见的网络设备故障问题,即防火墙会剔除不活跃(90s)链接时,或者 LVS 故障切流量剔除链接时,它们均不会向客户端 Socket 发 RST 包。这样会导致客户端存在脏 Socket。

    • 这个问题的表象则是:某台机器请求一个具体 IP 的服务,该服务流量不大,所以请求频率很低,几十分钟甚至几小时一次。当超时时,会超时一次即断开连接,或者连续超时 n 次后链接才断开。超时一次,应该是防火墙断开的链接;超时 n 次,则是 LVS 断开的链接。
    • 为什么超时 n 次之后才断开连接?因为 Socket 已经是脏链接,在写数据的时候,数据仅写到了 TCP Buffer 中,没有真正写出去,而这个时候 OS 并不会给上层使用者一个中断,仅当 TCP Buffer 写满之后,才会出现 org.apache.mina.common.WriteTimeoutException。TCP Buffer 在服务器上一般是 64k。一般这种问题,可以配置心跳来排除故障,或结合故障剔除功能来排查。

SOFA 客户端调用耗时较长的服务时,需要注意什么?

问题描述:

  • SOFA 客户端调用耗时较长的服务时,需要注意什么?
  • 如果设置了超长时间,没有生效,怎么排查错误?

排查思路:
服务发起方如果发现对方是一个耗时较长的服务,则需要配置一个比较合理的超时时间,否则,要判断该接口是不是需要一个oneway 方式去执行。如果必须等待结果,且触发后发现,无论如何配置,超时时间都无法生效,则需检查防火墙或负载均衡器是否在上游配置了连接超时控制。

RPC 本地调用,修改注册中心路径的方式有哪些?

目前只能是设置环境变量方式:System.setProperty("user.home","本地目录")

  • 企业版会把注册中心的路径配置包装起来,SOFA 在启动的时候会默认读这个 user.home
  • 在配置的时候需要多添加两个字符(/),因为框架会省掉前两个字符,从第三个字符开始读取,例如:user.home=//c://hulu

    注意

    • 这个问题,只会在本地开发的时候会遇到。云上开发不需要关心注册中心。
    • 企业版是通过 antvip 来获取一个健康的注册中心的地址,然后会构建 dsr://ip:port,同时,企业版将这个构建过程包装在了框架里。

有没有 Resteasy 的 key-value 方法请求示例?

示例如下:

  • SpringMVC 的 Controller 请求方法:
    • URL:http://localhost:8080/test?str=aaa
    • 代码配置,示例如下:
      1. @GetMapping("/test")
      2. public String testParam(@RequestParam("str") String str) {
      3. return str;
      4. }
  • Resteasy 的 get 请求:

    • 类型一:

      • URL:http://localhost:8341/webapi/users/test/xiaoming
      • 代码配置,示例如下:
        1. @Path("/webapi/users")
        2. public interface SampleRestFacade {
        3. @GET
        4. @Path("/test/{userName}")
        5. public RestSampleFacadeResp<DemoUserModel> user(@PathParam("userName") String userName) throws CommonException;
        6. }
    • 类型二(key-value)

      • URL:http://localhost:8341/webapi/users/test?userName=xiaoming
      • 代码配置,示例如下:
        1. @Path("/webapi/users")
        2. public interface SampleRestFacade {
        3. @GET
        4. @Path("/test")
        5. public RestSampleFacadeResp<DemoUserModel> userInfo(@QueryParam("userName") String userName) throws CommonException;
        6. }
  • @FormParam:将表单中的字段映射到方法调用上,此类方式提交方式一般为 Post。

RPC Tracer 日志格式说明

说明如下:

  • 日志名:rpc-client-digest.log
  • 内容:时间戳、应用名、TraceId、RcpId、服务名、方法名、协议、协调方式、目标地址、目标系统名、保留字段、保留字段、保留字段、uid、结果码、请求大小、响应大小、调用耗时、连接建立耗时、请求序列化耗时、超时参考耗时、线程名、路由记录、穿透数据。
  • 返回结果码,包括:
    • 00:成功
    • 01:业务异常
    • 02:RPC 逻辑错误
    • 03:超时失败
    • 04:路由失败

有没有泛化调用的示例?

SOFARPC 在框架层面提供了通用的接口方法和类型:

  • 服务的接口名:通过服务定义设置。
  • 方法名和参数列表:通过 $invoke$genericInvoke 传入。
  • 自定义类型:使用 GenericObject

其中几个特别需要注意事项为:

  • $invoke 方法:只用于参数类型,可以被当前应用的类加载器加载,如果只有基础类型,则可以使用此方法。
  • $genericInvoke 结合 GenericObject,当参数类型无法被当前应用的类加载器加载时,使用该方法。
  • argTypes 必须传递接口声明的参数类型,不可使用子类类型。
  • 调用 $genericInvoke 接口时,会将除以下包以外的其他类序列化为 GenericObject"com.sun","java","javax","org.ietf","org.ogm","org.w3c","org.xml","sunw.io","sunw.util"
  • GerericContext 暂时只用于单元化场景。
  • GenericObjectfields 的 value 也可以是一个 GenericObject

SOFARPC 的泛化调用,示例如下:

  • 服务方:

    • 服务、接口和类型定义:

      1. // 服务定义
      2. <sofa:reference interface="com.alipay.sofa.rpc.api.GenericService" id="xxxGenericService">
      3. <sofa:binding.tr>
      4. <sofa:global-attrs generic-interface="目标服务接口的fullname"/>
      5. </sofa:binding.tr>
      6. </sofa:reference>
      7. // 接口方法定义
      8. public interface GenericService {
      9. Object $invoke(String methodName, String[] argTypes, Object[] args) throws GenericException;
      10. Object $genericInvoke(String methodName, String[] argTypes, Object[] args) throws GenericException;
      11. Object $genericInvoke(String methodName, String[] argTypes, Object[] args, GenericContext context) throws GenericException;
      12. <T> T $genericInvoke(String methodName, String[] argTypes, Object[] args, Class<T> clazz) throws GenericException;
      13. <T> T $genericInvoke(String methodName, String[] argTypes, Object[] args, Class<T> clazz, GenericContext context) throws GenericException;
      14. }
      15. // 类型定义
      16. public final class GenericObject implements Serializable {
      17. private String type;
      18. private Map<String, Object> fields = new HashMap<String, Object>();
      19. }
    • 服务方的接口、自定义类型和发布泛化调用的配置:

      1. public interface PeopleService {
      2. String hello();
      3. String hello(String arg);
      4. People hello(People people);
      5. String[] hello(String[] args);
      6. People[] hello(People[] peoples);
      7. }
      8. public class People {
      9. private String name;
      10. private int age;
      11. //getter和setter方法
      12. }
      13. <!--发布泛化接口的配置-->
      14. <bean id="genericService" class="com.aliyun.gts.financial.product.demo.rpc.server.service.PeopleServiceImpl"/>
      15. <sofa:service ref="genericService" interface="com.aliyun.gts.financial.product.demo.service.facade.PeopleService">
      16. <sofa:binding.bolt/>
      17. </sofa:service>
  • 客户端:

    1. 定义泛化的服务,并设置正确的目标服务接口。
      1. <!--调用泛化接口的配置-->
      2. <sofa:reference interface="com.alipay.sofa.rpc.api.GenericService" id="genericFacade">
      3. <sofa:binding.bolt>
      4. <sofa:global-attrs
      5. generic-interface="com.aliyun.gts.financial.product.demo.service.facade.PeopleService"/>
      6. </sofa:binding.bolt>
      7. </sofa:reference>

      注意

      • reference 里的 interface 需要填写框架定义的 GenericService 接口。
      • global-attrs 里的 generic-interface 才是填写真正的目标服务接口。
      • reference 里的 interface 都是 GenericService,如果要泛化调用多个不同的服务接口,可通过 reference 的 id 来区分。
    2. 通过 GenericService 的方法来调用目标方法。
      1. @Controller
      2. public class TestController {
      3. private String peoplePath = "com.aliyun.gts.financial.product.demo.rpc.bean.People";
      4. private static final Logger logger = LoggerFactory.getLogger(TestController.class);
      5. /**
      6. * 默认ByName注入
      7. */
      8. @Autowired
      9. private GenericService genericFacade;
      10. /**
      11. * 无参场景使用$invoke
      12. * $invoke方法只用于参数类型可以被当前应用的类加载器加载,如果只有基础类型可以使用此方法
      13. * 泛化调用 String hello()方法
      14. */
      15. @GetMapping("/test/invokeWithoutArgs")
      16. @ResponseBody
      17. @Produces("application/json;charset=UTF-8")
      18. public void invokeWithoutArgs() {
      19. String result = (String) genericFacade.$invoke("hello",
      20. new String[]{},
      21. new Object[]{});
      22. if (logger.isInfoEnabled()) {
      23. logger.info("Generic invoke result: {}", result);
      24. }
      25. }
      26. /**
      27. * $invoke调用,有参数
      28. * 泛化调用 String hello(String arg);
      29. */
      30. @GetMapping("/test/invokeBasicTypeMethod")
      31. @ResponseBody
      32. @Produces("application/json;charset=UTF-8")
      33. public void invokeBasicTypeMethod() {
      34. String result = (String) genericFacade.$invoke(
      35. "hello",
      36. new String[]{String.class.getName()},
      37. new Object[]{"BasicType"});
      38. if (logger.isInfoEnabled()) {
      39. logger.info("Generic invoke result: {}", result);
      40. }
      41. }
      42. /**
      43. * $genericInvoke调用,用于参数类型无法被当前应用的类加载器加载的场景
      44. * 泛化调用 People hello(People people);
      45. */
      46. @GetMapping("/test/invokeCustomTypeMethod")
      47. @ResponseBody
      48. @Produces("application/json;charset=UTF-8")
      49. public void invokeCustomTypeMethod() {
      50. // 构造函数中指定全路径类名
      51. GenericObject genericPeopleObject = new GenericObject(peoplePath);
      52. // 调用putField,指定field值
      53. genericPeopleObject.putField("name", "Lilei");
      54. genericPeopleObject.putField("age", 15);
      55. Object result = genericFacade.$genericInvoke(
      56. "hello",
      57. new String[]{peoplePath},
      58. new Object[]{genericPeopleObject});
      59. // 返回的类型还是GenericObject类型
      60. if (logger.isInfoEnabled()) {
      61. logger.info("Type of result: {}", result.getClass().getName());
      62. }
      63. }
      64. /**
      65. * $genericInvoke调用,参数为数组
      66. * 泛化调用 String[] hello(String[] args);
      67. */
      68. @GetMapping("/test/invokeBasicArrayTypeMethod")
      69. @ResponseBody
      70. @Produces("application/json;charset=UTF-8")
      71. public void invokeBasicArrayTypeMethod() {
      72. String[] results = (String[]) genericFacade.$genericInvoke(
      73. "hello",
      74. new String[]{new String[]{}.getClass().getName()},
      75. new Object[]{new String[]{"BasicArrayType"}});
      76. // 返回的类型还是GenericObject类型
      77. if (logger.isInfoEnabled()) {
      78. for (String result : results) {
      79. logger.info("Generic invoke result: {}", result);
      80. }
      81. }
      82. }
      83. /**
      84. * $genericInvoke调用,自定义类型数组
      85. * People[] hello(People[] peoples);
      86. */
      87. @GetMapping("/test/invokeCustomArrayTypeMethod")
      88. @ResponseBody
      89. @Produces("application/json;charset=UTF-8")
      90. public void invokeCustomArrayTypeMethod() {
      91. GenericObject genericObject = new GenericObject(peoplePath);
      92. // 调用 putField,指定field值
      93. genericObject.putField("name", "HanMeimei");
      94. genericObject.putField("age", 14);
      95. // 服务端反射,class.forName对于数组类型的格式有特定要求
      96. String genericObjArrayType = "[L" + peoplePath + ";";
      97. GenericObject[] genericObjArray = new GenericObject[]{genericObject};
      98. GenericArray resultArray = (GenericArray) genericFacade.$genericInvoke("hello",
      99. new String[]{genericObjArrayType},
      100. new Object[]{genericObjArray});
      101. for (Object result : resultArray.getObjects()) {
      102. logger.info(result.toString());
      103. }
      104. }
      105. }

如何使用泛化调用?

目前提供了两种方法:

  • $invoke:仅支持方法参数类型在当前应用的 ClassLoader 中存在的情况。
  • $genericInvoke:支持方法参数类型在当前应用的 ClassLoader 中不存在的情况。

具体使用,示例如下:

  • 服务方:
    • 服务引用
      1. <!-- 引用 BOLT 服务 -->
      2. <sofa:reference interface="com.alipay.sofa.rpc.api.GenericService" id="genericService">
      3. <sofa:binding.bolt>
      4. <sofa:global-attrs generic-interface="com.alipay.test.SampleService"/>
      5. </sofa:binding.bolt>
      6. </sofa:reference>
    • 服务端服务定义,示例如下:
      1. /*** Java Bean*/
      2. public class People {
      3. private String name;
      4. private int age;
      5. // getters and setters
      6. }
      7. /** * 服务方提供的接口 */
      8. interface SampleService {
      9. String hello(String arg);
      10. People hello(People people);
      11. }
  • 客户方:

    • 泛化调用,示例如下:

      1. /** * 消费方测试类 */
      2. public class ConsumerClass {
      3. GenericService genericService;
      4. public void do() {
      5. // 1. $invoke 仅支持方法参数类型在当前应用的 ClassLoader 中存在的情况
      6. genericService.$invoke("hello", new String[]{ String.class.getName() }, new Object[]{"I'm an arg"});
      7. // 2. $genericInvoke 支持方法参数类型在当前应用的 ClassLoader 中不存在的情况。
      8. // 2.1 构造参数
      9. GenericObject genericObject = new GenericObject("com.alipay.sofa.rpc.test.generic.bean.People"); //构造函数中指定全路径类名
      10. genericObject.putField("name", "Lilei"); // 调用 putField,指定field值
      11. genericObject.putField("age", 15);
      12. // 2.2 进行调用,不指定返回类型,返回结果类型为 GenericObject
      13. Object obj = genericService.$genericInvoke("hello", new String[]{"com.alipay.sofa.rpc.test.generic.bean.People"}, new Object[] { genericObject });
      14. Assert.assertTrue(obj.getClass() == GenericObject.class);
      15. // 2.3 进行调用,指定返回类型
      16. People people = genericService.$genericInvoke("hello", new String[]{"com.alipay.sofa.rpc.test.generic.bean.People"}, new Object[] { genericObject }, People.class);
      17. // 3. LDC 架构下的泛化调用使用
      18. // 3.1 构造 GenericContext 对象
      19. AlipayGenericContext genericContext = new AlipayGenericContext();
      20. genericContext.setUid("33");
      21. // 3.2 进行调用
      22. People people = genericService.$genericInvoke("hello", new String[]{"com.alipay.sofa.rpc.test.generic.bean.People"}, new Object[] { genericObject }, People.class, genericContext);}

      注意:调用 $genericInvoke(String methodName, String[] argTypes, Object[] args) 接口,会将除以下包以外的其他类序列化为 GenericObject。

      1. "com.sun","java","javax","org.ietf","org.ogm","org.w3c","org.xml","sunw.io","sunw.util"

泛化调用的使用场景有哪些?

泛化调用提供了让客户端,在不需要依赖服务端接口的情况下,也能发起调用的能力。在 Bolt 通信协议下使用 Hessian2 作为序列化协议,是目前 SOFARPC 的泛化调用仅支持的方式。

泛化调用的常见场景:在开发中遇到一些第三方应用不想要依赖我们自己开发的依赖接口 JAR,但也想通过某种方式发起调用,或者更进一步,做一个非依赖 JAR 的简单微服务网关。

如何针对 RPC 请求做一些定制化处理,比如白名单过滤?

可通过过滤器的方式进行 RPC 接口过滤,比如 IP 黑白名单的过滤、Token 的验证等。

白名单过滤的实现步骤如下:

  1. 继承 SOFA 的 Filter 抽象类,实现里面的 invoke 方法:

    1. @Component
    2. public class WhiteIpFilter extends Filter {
    3. @Value("${security.firewall.whiteIps}")
    4. private String whiteIpList;
    5. @Override
    6. public boolean needToLoad(FilterInvoker invoker) { return true; }
    7. @Override
    8. public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException {
    9. RpcInternalContext context = RpcInternalContext.getContext();
    10. InetSocketAddress remoteAddress = context.getRemoteAddress();
    11. final String remoteIp = remoteAddress.getHostString();
    12. if (whiteIpList.contains(remoteIp)) {
    13. return invoker.invoke(request);
    14. }else {
    15. SofaResponse sofaResponse = new SofaResponse();
    16. sofaResponse.setErrorMsg("非法IP: " + remoteIp + " 访问,请联系管理员.");
    17. return sofaResponse;
    18. }
    19. }
    20. }
  2. 在需要白名单验证的接口上配置过滤器:
    1. <sofa:service interface="cloud.provider.facade.CallerService" ref="callerService" >
    2. <sofa:binding.bolt>
    3. <sofa:global-attrs filter="whiteIpFilter" />
    4. </sofa:binding.bolt>
    5. <sofa:binding.rest/>
    6. </sofa:service>

是否有文件上传下载的示例代码?

SOFARPC 的 REST 协议,底层使用的是 Resteasy,可以实现文件上传下载。

主要步骤如下:

  1. 声明 Facade 接口:
    1. public interface FileServiceFacade {
    2. @GET
    3. @Path("/files/{fileName}")
    4. @Produces("text/plain")
    5. Response downloadFile(@PathParam("fileName")String fileName)throws Exception;
    6. @POST
    7. @Path("/files")
    8. @Consumes("multipart/form-data")
    9. Response uploadFile(MultipartFormDataInput input) throws IOException;
    10. }
  2. 实现下载方法:上传下载方法.png
  3. 实现上传方法

    1. @Override
    2. public Response uploadFile(MultipartFormDataInput input) throws IOException {
    3. final String UPLOAD_FILE_PATH = "/Users/yuanshaopeng/Desktop/temp/";
    4. Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
    5. // httpclient
    6. // Get file name
    7. //String fileName = uploadForm.get("fileName").get(0).getBodyAsString();
    8. // Get file data to save
    9. //List<InputPart> inputParts = uploadForm.get("attachment");
    10. // http mode
    11. List<InputPart> inputParts = uploadForm.get("uploadedFile");
    12. String fileName = "";
    13. for (InputPart inputPart : inputParts) {
    14. try {
    15. @SuppressWarnings("unused")
    16. MultivaluedMap<String, String> header = inputPart.getHeaders();
    17. fileName = getFileName(header);
    18. byte[] bytes = IOUtils.toByteArray(inputPart.getBody(InputStream.class, null));
    19. log.info("上传文件大小:" + bytes.length);
    20. File desFile = new File(UPLOAD_FILE_PATH + fileName);
    21. FileUtils.writeByteArrayToFile(desFile, bytes);
    22. System.out.println("Success !!!!!");
    23. } catch (Exception e) {
    24. e.printStackTrace();
    25. }
    26. }
    27. return Response.status(200).entity("Upload file name : " + fileName).build();
    28. }
    29. private String getFileName(MultivaluedMap<String, String> header) {
    30. String[] contentDisposition = header.getFirst("Content-Disposition").split(";");
    31. for (String filename : contentDisposition) {
    32. if ((filename.trim().startsWith("filename"))) {
    33. String[] name = filename.split("=");
    34. String finalFileName = name[1].trim().replaceAll("\"", "");
    35. return finalFileName;
    36. }
    37. }
    38. return "unknown";
    39. }

如何实现一个不依赖 Facade 接口的服务网关?

一般实现思路为:

  • 通过通用的 RESTful 协议进行接入。
  • 后端根据泛化接口对传入数据进行解析。
  • 通过代理的过滤器指定路由配置。

具体可以参考 spring-cloud-gateway 的过滤器设计和 SOFABolt 协议的泛化设计。可能需要下述扩展:

  • 动态路由
  • 注册中心联动
  • 缓存设计
  • 自动代码生成等

如何实现 RPC 请求携带数据进行传递?

注意:默认该功能是关闭的,开启后会影响性能,请尽量避免使用。

实现步骤如下:

  1. 开启配置:需在 resource 目录下添加 rpc-config.json 文件。
    1. {"invoke.baggage.enable": true}
  2. 代码实现:见下述示例。
    1. System.out.println(RpcInvokeContext.isBaggageEnable());
    2. RpcInvokeContext context = RpcInvokeContext.getContext();
    3. context.putRequestBaggage("hellod", "lolo");

    说明:RpcInvokeContext 是一个 “RPC 执行上下文”,在这个上下文中,我们可以向 RequestBaggege 里添加数据,且数据必须是字符串类型。

如何设置 SOFARPC 服务暴露的端口?

在 SOFARPC 服务中,协议默认端口的约定为:

  • BOLT 协议:12200
  • REST 协议:8341

因此,可以在 /resources/config/application.properties 里设置参数来实现服务暴露的端口,示例如下:

  1. com.alipay.sofa.rpc.bolt.port=12202
  2. com.alipay.sofa.rpc.rest.port=8765

如果一个服务有多个实现,服务在发布和引用的时候该如何处理?

一个服务如果有多个实现的话,则可以在 RPC 暴露服务和引用服务的地方配置一个 unique-id 来作为它的唯一标识。示例如下:

  1. <!-- 服务一 -->
  2. <sofa:service ref="sampleServiceBean1" interface="com.alipay.APPNAME.facade.SampleService" unique-id="service1">
  3. <sofa:binding.bolt/>
  4. </sofa:service>
  5. <!-- 服务二 -->
  6. <sofa:service ref="sampleServiceBean2" interface="com.alipay.APPNAME.facade.SampleService" unique-id="service2">
  7. <sofa:binding.bolt/>
  8. </sofa:service>

RPC 服务超时控制,有哪些参数可以配置?

RPC服务在发布和引用时都有超时控制的配置,方法也可以做超时控制,其超时时间的优先级,规定如下:引用服务的方法超时 > 引用服务的全局超时时间 > 服务发布者的方法超时 > 服务发布者的全局超时时间,示例如下:

  1. <sofa:binding.bolt >
  2. <sofa:global-attrs timeout="5000" />
  3. <sofa:method name="message" type="future" timeout="25000"/>
  4. </sofa:binding.bolt>

当接口以 RESTful 协议暴露时,需要注意哪些事项?

需要注意下述事项:

  • 首先是需要明确中间件使用的版本,目前推荐 sofaboot-enterprise 的版本是 3.4.*,在使用该版本时,如果要使用RESTful 接口进行开发时,只需要引入 RPC 对应的 starter 包依赖即可,无需再引入 REST 的 starter 依赖,否则会启动两个 RESTful服务端,导致 8341 端口被占用。
  • 在使用 REST 接口开发时,需要在接口上暴露 Path 和 WS 相关的注解,否则服务不知道具体的请求是什么。具体的 WS注解使用请参考: WS 注解使用

一个服务如何同时暴露 RESTful 和 Bolt 两种协议?

发布服务时直接同时声明两种协议的 binding 即可,如下所示:

  1. <bean id="demoServiceImpl" class="com.alipay.sofa.samples.rpc.DemoServiceImpl"/>
  2. <sofa:service ref="demoServiceImpl" interface="com.alipay.sofa.samples.rpc.DemoService">
  3. <sofa:binding.rest/>
  4. <sofa:binding.bolt/>
  5. </sofa:service>