全部产品

故障排查之 RPC

KB: 184128

 · 

更新时间:2021-02-09 11:35

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

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

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

排查思路如下:

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

    RPC-REGISTRY - RPC-00204:接收 RPC 服务地址:服务名[com.alipay.share.rpc.facade.SampleService:1.0@DEFAULT]
    可调用目标地址[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

  • 检查服务注册中心连接运行以下命令以检查客户端和服务端与服务注册中心的连接情况:

    netstat -a |grep 9600

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

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

    ps -ef|grep java

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

Rpc invocation timeout[responseCommand TIMEOUT]

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

2018-07-0613: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 is10.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,找到服务端处理对应请求的日志。

日志格式如下所示:

2018-07-0613: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 文件中加入以下启动参数,并重新打包发布。

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

方法二

  1. kill -15 命令结束服务端进程。

  2. 手动启动 RPC 服务。运行 su admin 进入 admin 用户,用如下 nohup 形式启动 RPC 服务:

    $ 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 数值。打印信息也可以根据需要调整。

$ 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 依赖,示例如下:

    <dependency>
        <groupId>com.alibaba.boot</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
        <version>0.1.1</version>
    </dependency>
    <!-- Dubbo -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dubbo</artifactId>
        <version>2.6.4</version>
    </dependency>
    <!-- Spring Context Extras -->
    <dependency>
        <groupId>com.alibaba.spring</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>1.0.2</version>
    </dependency>
  2. 配置 application.properties,示例如下:

    ################ common configuration ##############
    spring.application.name=bank-dubbo-provider
    logging.level.com.dubbo.example=INFO
    logging.path=./logs
    ################ dubbo configuration ##############
    demo.service.version =1.0.0
    dubbo.application.id = bank-dubbo-provider
    dubbo.application.name = bank-dubbo-provider
    ################ sofa configuration ##############
    run.mode=DEV
    com.alipay.sofa.rpc.bolt-port=12201
    # shared middleware
    com.alipay.env=shared
    com.alipay.instanceid=IPYJUBMB231N
    com.antcloud.antvip.endpoint=100.103.1.174
    com.antcloud.mw.access=uPxHLxsMmstcQCNWEh
    com.antcloud.mw.secret=TyMlUB9uGRMzcc2pG0dMv6xzUXCMA1WI
  3. 添加 Dubbo 服务发布,示例如下:

    说明

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

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:sofa="http://schema.alipay.com/sofa/schema/slite"
        xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xs
        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">
        <!-- dubbo zookeeper configuration -->
        <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
        <dubbo:protocol name="dubbo" port="20880"/>
        <!-- bean define -->
        <bean id="dubboService" class="com.dubbo.example.service.DubboServiceImpl"/>
        <!-- sofa service -->
        <sofa:service interface="com.dubbo.example.facade.DubboService" ref="dubboService" unique-id="sofaDubboService">
            <sofa:binding.bolt/>
        </sofa:service>
        <!-- dubbo service -->
        <dubbo:service interface="com.dubbo.example.facade.DubboService" ref="dubboService" version="1.0.0"/>
    </beans>
  4. 更新 main 函数,开启 Dubbo,示例如下:

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

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 中添加下述配置,以略过所有组件的健康检查。

com.alipay.sofa.healthcheck.skip.component=true
注意

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

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

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

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

故障排查主要步骤包括:

  1. 在日志里将该报错打开,将 Netty 中 Http 相关类的日志改成 debug 模式,即可实现,示例如下:

    logging.level.io.netty.handler.codec.http.HttpObjectAggregator=DEBUG
  2. 获取具体报错的原因,例如 Failed to send a 413 Request Entity Too Large

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

    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

    • 代码配置,示例如下:

      @GetMapping("/test")
      public String testParam(@RequestParam("str") String str){
          return str;
      }
  • Resteasy 的 get 请求:

    • 类型一:

      • URL:http://localhost:8341/webapi/users/test/xiaoming

      • 代码配置,示例如下:

        @Path("/webapi/users")
        public interface SampleRestFacade{
            @GET
            @Path("/test/{userName}")
            public RestSampleFacadeResp<DemoUserModel> user(@PathParam("userName")String userName)throws CommonException;
        }
    • 类型二(key-value)

      • URL:http://localhost:8341/webapi/users/test?userName=xiaoming

      • 代码配置,示例如下:

        @Path("/webapi/users")
        public interface SampleRestFacade{
              @GET
              @Path("/test")
              public RestSampleFacadeResp<DemoUserModel> userInfo(@QueryParam("userName") String userName) throws CommonException;
        }
  • @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 的泛化调用,示例如下:

  • 服务方:

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

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

      public interface PeopleService{
          String hello();
          String hello(String arg);
          People hello(People people);
          String[] hello(String[] args);
          People[] hello(People[] peoples);
      }
      public class People{
          private String name;
          private int age;
         //getter和setter方法
      }
      <!--发布泛化接口的配置-->
      <bean id="genericService" class="com.aliyun.gts.financial.product.demo.rpc.server.service.PeopleServiceImpl"/>
      <sofa:service ref="genericService" interface="com.aliyun.gts.financial.product.demo.service.facade.PeopleService">
           <sofa:binding.bolt/>
      </sofa:service>
  • 客户端:

    1. 定义泛化的服务,并设置正确的目标服务接口。

      <!--调用泛化接口的配置-->
      <sofa:reference interface="com.alipay.sofa.rpc.api.GenericService" id="genericFacade">
           <sofa:binding.bolt>
                <sofa:global-attrs generic-interface="com.aliyun.gts.financial.product.demo.service.facade.PeopleService"/>
           </sofa:binding.bolt>
      </sofa:reference>
      注意

      reference 里的 interface 需要填写框架定义的 GenericService 接口。 global-attrs 里的 generic-interface 才是填写真正的目标服务接口。 reference 里的 interface 都是 GenericService,如果要泛化调用多个不同的服务接口,可通过 reference 的 id 来区分。

    2. 通过 GenericService 的方法来调用目标方法。

          @Controller
          public class TestController{
              private String peoplePath ="com.aliyun.gts.financial.product.demo.rpc.bean.People";
              private static final Logger logger =LoggerFactory.getLogger(TestController.class);
              /**
               * 默认ByName注入
               */
              @Autowired
              private GenericService genericFacade;
              /**
               * 无参场景使用$invoke
               * $invoke方法只用于参数类型可以被当前应用的类加载器加载,如果只有基础类型可以使用此方法
               * 泛化调用 String hello()方法
               */
              @GetMapping("/test/invokeWithoutArgs")
              @ResponseBody
              @Produces("application/json;charset=UTF-8")
              public void invokeWithoutArgs(){
                  String result =(String) genericFacade.$invoke("hello",
                          newString[]{},
                  new Object[]{});
                  if(logger.isInfoEnabled()){
                      logger.info("Generic invoke result: {}", result);
                  }
              }
              /**
               * $invoke调用,有参数
               * 泛化调用 String hello(String arg);
               */
              @GetMapping("/test/invokeBasicTypeMethod")
              @ResponseBody
              @Produces("application/json;charset=UTF-8")
              public void invokeBasicTypeMethod(){
                  String result =(String) genericFacade.$invoke(
                          "hello",
                          new String[]{String.class.getName()},
                  new Object[]{"BasicType"});
                  if(logger.isInfoEnabled()){
                      logger.info("Generic invoke result: {}", result);
                  }
              }
              /**
               * $genericInvoke调用,用于参数类型无法被当前应用的类加载器加载的场景
               * 泛化调用 People hello(People people);
               */
              @GetMapping("/test/invokeCustomTypeMethod")
              @ResponseBody
              @Produces("application/json;charset=UTF-8")
              public void invokeCustomTypeMethod(){
                  // 构造函数中指定全路径类名
                  GenericObject genericPeopleObject =new GenericObject(peoplePath);
                  // 调用putField,指定field值
                  genericPeopleObject.putField("name","Lilei");
                  genericPeopleObject.putField("age",15);
                  Object result = genericFacade.$genericInvoke(
                          "hello",
                          newString[]{peoplePath},
                  new Object[]{genericPeopleObject});
                  // 返回的类型还是GenericObject类型
                  if(logger.isInfoEnabled()){
                      logger.info("Type of result: {}", result.getClass().getName());
                  }
              }
              /**
               * $genericInvoke调用,参数为数组
               * 泛化调用 String[] hello(String[] args);
               */
              @GetMapping("/test/invokeBasicArrayTypeMethod")
              @ResponseBody
              @Produces("application/json;charset=UTF-8")
              public void invokeBasicArrayTypeMethod(){
                  String[] results =(String[]) genericFacade.$genericInvoke(
                          "hello",
                          new String[]{new String[]{}.getClass().getName()},
                  new Object[]{new String[]{"BasicArrayType"}});
                  // 返回的类型还是GenericObject类型
                  if(logger.isInfoEnabled()){
                      for(String result : results){
                          logger.info("Generic invoke result: {}", result);
                      }
                  }
              }
              /**
               * $genericInvoke调用,自定义类型数组
               * People[] hello(People[] peoples);
               */
              @GetMapping("/test/invokeCustomArrayTypeMethod")
              @ResponseBody
              @Produces("application/json;charset=UTF-8")
              public void invokeCustomArrayTypeMethod(){
                  GenericObject genericObject =new GenericObject(peoplePath);
                  // 调用 putField,指定field值
                  genericObject.putField("name","HanMeimei");
                  genericObject.putField("age",14);
                  // 服务端反射,class.forName对于数组类型的格式有特定要求
                  String genericObjArrayType ="[L"+ peoplePath +";";
                  GenericObject[] genericObjArray =new GenericObject[]{genericObject};
                  GenericArray resultArray =(GenericArray) genericFacade.$genericInvoke("hello",
                          new String[]{genericObjArrayType},
                  new Object[]{genericObjArray});
                  for(Object result : resultArray.getObjects()){
                      logger.info(result.toString());
                  }
              }
          }

如何使用泛化调用?

目前提供了两种方法:

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

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

具体使用,示例如下:

  • 服务方:

    • 服务引用

      <!-- 引用 BOLT 服务 -->
      <sofa:referenceinterface="com.alipay.sofa.rpc.api.GenericService"id="genericService">
          <sofa:binding.bolt>
              <sofa:global-attrsgeneric-interface="com.alipay.test.SampleService"/>
          </sofa:binding.bolt>
      </sofa:reference>
    • 服务端服务定义,示例如下:

      /*** Java Bean*/
      public class People{
          private String name;
          private int    age;
          // getters and setters
      }
      /** * 服务方提供的接口 */
      interface SampleService{
          String hello(String arg);
          People hello(People people);
      }
  • 客户方:

    • 泛化调用,示例如下:

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

        @Component
        public class WhiteIpFilter extends Filter{
            @Value("${security.firewall.whiteIps}")
            private String whiteIpList;
    
            @Override
            public boolean needToLoad(FilterInvoker invoker){
                   return true;
            }
            @Override
            public SofaResponse invoke(FilterInvoker invoker,SofaRequest request) throws SofaRpcException{
                RpcInternalContext context =RpcInternalContext.getContext();
                InetSocketAddress remoteAddress = context.getRemoteAddress();
                finalString remoteIp = remoteAddress.getHostString();
                if(whiteIpList.contains(remoteIp)){
                    return invoker.invoke(request);
                }else{
                    SofaResponse sofaResponse =new SofaResponse();
                    sofaResponse.setErrorMsg("非法IP: "+ remoteIp +" 访问,请联系管理员.");
                    return sofaResponse;
                }
            }
        }
  2. 在需要白名单验证的接口上配置过滤器:

    <sofa:service interface="cloud.provider.facade.CallerService" ref="callerService">
       <sofa:binding.bolt>
            <sofa:global-attrs filter="whiteIpFilter"/>
       </sofa:binding.bolt>
       <sofa:binding.rest/>
    </sofa:service>

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

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

主要步骤如下:

  1. 声明 Facade 接口:

    public interface FileServiceFacade{
       @GET
       @Path("/files/{fileName}")
       @Produces("text/plain")
       Response downloadFile(@PathParam("fileName")String fileName) throws Exception;
       @POST
       @Path("/files")
       @Consumes("multipart/form-data")
       Response uploadFile(MultipartFormDataInput input) throws IOException;
    }
  2. 实现下载方法:上传下载方法.png

  3. 实现上传方法

    @Override
        public Response uploadFile(MultipartFormDataInput input)throws IOException{
            final String UPLOAD_FILE_PATH ="/Users/yuanshaopeng/Desktop/temp/";
            Map<String,List<InputPart>> uploadForm = input.getFormDataMap();
            // httpclient
            // Get file name
            //String fileName = uploadForm.get("fileName").get(0).getBodyAsString();
            // Get file data to save
            //List<InputPart> inputParts = uploadForm.get("attachment");
            // http mode
            List<InputPart> inputParts = uploadForm.get("uploadedFile");
            String fileName ="";
            for(InputPart inputPart : inputParts){
                try{
                    @SuppressWarnings("unused")
                    MultivaluedMap<String,String> header = inputPart.getHeaders();
                    fileName = getFileName(header);
                    byte[] bytes =IOUtils.toByteArray(inputPart.getBody(InputStream.class,null));
                    log.info("上传文件大小:"+ bytes.length);
                    File desFile =new File(UPLOAD_FILE_PATH + fileName);
                    FileUtils.writeByteArrayToFile(desFile, bytes);
                    System.out.println("Success !!!!!");
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
            return Response.status(200).entity("Upload file name : "+ fileName).build();
        }
    
        private String getFileName(MultivaluedMap<String,String> header){
            String[] contentDisposition = header.getFirst("Content-Disposition").split(";");
            for(String filename : contentDisposition){
                if((filename.trim().startsWith("filename"))){
                    String[] name = filename.split("=");
                    String finalFileName = name[1].trim().replaceAll("\"","");
                    return finalFileName;
                }
            }
            return "unknown";
        }
    }

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

一般实现思路为:

  • 通过通用的 RESTful 协议进行接入。

  • 后端根据泛化接口对传入数据进行解析。

  • 通过代理的过滤器指定路由配置。

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

  • 动态路由

  • 注册中心联动

  • 缓存设计

  • 自动代码生成等

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

注意

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

实现步骤如下:

  1. 开启配置:需在 resource 目录下添加 rpc-config.json 文件。

    {"invoke.baggage.enable":true}
  2. 代码实现:见下述示例。

    System.out.println(RpcInvokeContext.isBaggageEnable());
    RpcInvokeContext context =RpcInvokeContext.getContext();
    context.putRequestBaggage("hellod","lolo");
    说明

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

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

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

  • BOLT 协议:12200

  • REST 协议:8341

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

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

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

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

<!-- 服务一 -->
<sofa:service ref="sampleServiceBean1" interface="com.alipay.APPNAME.facade.SampleService" unique-id="service1">
     <sofa:binding.bolt/>
</sofa:service>
<!-- 服务二 -->
<sofa:service ref="sampleServiceBean2" interface="com.alipay.APPNAME.facade.SampleService" unique-id="service2">
    <sofa:binding.bolt/>
</sofa:service>

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

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

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

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

需要注意下述事项:

  • 首先是需要明确中间件使用的版本,目前推荐 sofaboot-enterprise 的版本是 3.4.*,在使用该版本时,如果要使用RESTful 接口进行开发时,只需要引入 RPC 对应的 starter 包依赖即可,无需再引入 REST 的 starter 依赖,否则会启动两个 RESTful服务端,导致 8341 端口被占用。

  • 在使用 REST 接口开发时,需要在接口上暴露 Path 和 WS 相关的注解,否则服务不知道具体的请求是什么。具体的 WS注解使用请参考: WS 注解使用

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

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

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