更新时间:2020-12-07 10:11
本文汇总梳理了 RPC 使用过程中遇到的常见问题及排查思路。
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) |
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_interface
或 rpc_enabled_ip_range
是否绑定了正确的 IP 地址。
若调用 RPC 服务超时,在客户端的 logs/tracelog/middleware_error.log
日志中,可看到如下异常信息:
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=¶mTypes=&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 调用超时一般可以按照如下顺序逐步排查:
默认情况下,RPC 的超时时间为 3 秒。要确定某个请求的实际处理时间,您可登录服务端,查看 logs/tracelog/rpc-server-digest.log
日志。根据客户端超时日志中的 traceID,如 707c27b9153085447746110464663
,找到服务端处理对应请求的日志。
日志格式如下所示:
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/tr-threadpool
日志。如果发生 RPC 线程池队列阻塞,先确认是否发生超时的时间段有业务请求高峰,或者用 jstack
查看业务线程是否有等待或者死锁情况,导致 RPC 线程耗尽。
更多信息,请参见 RPC 应用参数配置。
某些 GC 类型会触发“stop the world”问题,会将所有线程挂起。若要排查是否是 GC 导致的超时问题,可以通过以下方法开启 GC 日志。
方法一
在 config/java_opts
文件中加入以下启动参数,并重新打包发布。
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/admin/logs/gc.log
方法二
kill -15
命令结束服务端进程。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
观察是否存在网络延时。
可以参考以下示例语句,打印调用 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
排查思路方面,主要分为下述几种情形:
logs/registry/registry-client.log
中出现 Vip(null) endpoint might be wrong
的错误。该错误意味着当前 acvip 配置有误或 acvip 出现网络故障,此时需要联系运维同学检查 acvip 和前端负载均衡器的网络连通性。/Users/xxx/conf/acvip-java-client-cache/domains/0000X-DSR_HTTP.json
这样的文件。如果有,可进入该文件查看其内容,一般都是缓存到本地的 DSR 注册中心地址,可自行检查是否有异常。例如:健康检查不通过,IP 没有获取正确等。curl -i -XPOST {antvip}:9003/antcloud/antvip/instances/get -d '{"vipDomainName2ChecksumMap":{"000001-DSR_CLOUD":"N"}}'
。如果不正常,请检查注册中心是否配置正确。run.mode=DEV
参数。在 DEV 模式下,将只注册到本地,而不会注册到注册中心里。问题描述:
解决思路如下:
系统改造过程中,并不能确保所有关联系统一次性都改造完成,会面临需要和历史系统兼容的场景,例如:一个服务被改造成 SOFA Bolt 服务后,发现还有调用方依然是依赖 Dubbo 的。那么,一个简单的兼容办法为:这个服务同时暴露 BOLT 和 Dubbo 服务。
在 SOFABoot 中暴露 Dubbo 服务,步骤如下:
加入 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>
配置 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
添加 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>
更新 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);
}
}
错误现象如下:
故障原因:bolt 端口 12200 被占用。
解决方案:更换端口或者把本地占用端口服务关闭。
问题分析:
通过对上面的 2 个日志进行分析,结合日志的提示,可以得出下述结论:
解决思路:修改序列化方法。
应用依赖了一些 SOFA 组件,但在本地环境中只想测一下 RPC,如何处理下述问题:
解决思路:
健康检查机制会在项目启动的时候对所有组件进行探活,如果此时引用了 DDCS(Distributed Dynamic Configuration Service) 或者其它组件,会出现应用启动正常,但 RPC 无法在本地注册的现象。此时,如果业务暂时没有用到这些组件,可以在 healthcheck 时略过这些检查。本质原因是这些组件会到 antvip 中寻找组件地址,而此时应用并不在云上,所以会失败。
具体操作:
在 application.properties
中添加下述配置,以略过所有组件的健康检查。
com.alipay.sofa.healthcheck.skip.component=true
注意:此方案只建议在测试中使用,线上环境一定要打开健康检查。
SOFARest 接口在上传文件时,文件超过 10M 时会报错,报错信息如下:
ERROR org.jboss.resteasy.core.ExceptionHandler - failed to execute
javax.ws.rs.NotFoundException: Could not 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)
故障排查主要步骤包括:
logging.level.io.netty.handler.codec.http.HttpObjectAggregator=DEBUG
获取具体报错的原因,例如 Failed to send a 413 Request Entity Too Large
。
在 application.properties
文件里设置相应参数。
com.alipay.sofa.rpc.RestMaxRequestSize=104857600
通过对下述项进行判断来进行排查:
线程池发生了阻塞:这个一般在多级链路调用时,才会发生,比如: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。
org.apache.mina.common.WriteTimeoutException
。TCP Buffer 在服务器上一般是 64k。一般这种问题,可以配置心跳来排除故障,或结合故障剔除功能来排查。问题描述:
排查思路:
服务发起方如果发现对方是一个耗时较长的服务,则需要配置一个比较合理的超时时间,否则,要判断该接口是不是需要一个oneway 方式去执行。如果必须等待结果,且触发后发现,无论如何配置,超时时间都无法生效,则需检查防火墙或负载均衡器是否在上游配置了连接超时控制。
目前只能是设置环境变量方式:System.setProperty("user.home","本地目录")
。
user.home
。user.home=//c://hulu
。注意:
- 这个问题,只会在本地开发的时候会遇到。云上开发不需要关心注册中心。
- 企业版是通过 antvip 来获取一个健康的注册中心的地址,然后会构建
dsr://ip:port
,同时,企业版将这个构建过程包装在了框架里。
示例如下:
http://localhost:8080/test?str=aaa
@GetMapping("/test")
public String testParam(@RequestParam("str") String str) {
return str;
}
Resteasy 的 get 请求:
类型一:
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)
@Path("/webapi/users")
public interface SampleRestFacade {
@GET
@Path("/test")
public RestSampleFacadeResp<DemoUserModel> userInfo(@QueryParam("userName") String userName) throws CommonException;
}
@FormParam
:将表单中的字段映射到方法调用上,此类方式提交方式一般为 Post。
说明如下:
rpc-client-digest.log
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
暂时只用于单元化场景。GenericObject
、fields
的 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>
客户端:
<!--调用泛化接口的配置-->
<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 来区分。
@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",
new String[]{},
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",
new String[]{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:reference interface="com.alipay.sofa.rpc.api.GenericService" id="genericService">
<sofa:binding.bolt>
<sofa:global-attrs generic-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 Object[] { 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 接口过滤,比如 IP 黑白名单的过滤、Token 的验证等。
白名单过滤的实现步骤如下:
继承 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();
final String remoteIp = remoteAddress.getHostString();
if (whiteIpList.contains(remoteIp)) {
return invoker.invoke(request);
}else {
SofaResponse sofaResponse = new SofaResponse();
sofaResponse.setErrorMsg("非法IP: " + remoteIp + " 访问,请联系管理员.");
return sofaResponse;
}
}
}
<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,可以实现文件上传下载。
主要步骤如下:
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;
}
实现上传方法
@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";
}
一般实现思路为:
具体可以参考 spring-cloud-gateway 的过滤器设计和 SOFABolt 协议的泛化设计。可能需要下述扩展:
注意:默认该功能是关闭的,开启后会影响性能,请尽量避免使用。
实现步骤如下:
resource
目录下添加 rpc-config.json
文件。
{"invoke.baggage.enable": true}
System.out.println(RpcInvokeContext.isBaggageEnable());
RpcInvokeContext context = RpcInvokeContext.getContext();
context.putRequestBaggage("hellod", "lolo");
说明:RpcInvokeContext 是一个 “RPC 执行上下文”,在这个上下文中,我们可以向 RequestBaggege 里添加数据,且数据必须是字符串类型。
在 SOFARPC 服务中,协议默认端口的约定为:
因此,可以在 /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服务在发布和引用时都有超时控制的配置,方法也可以做超时控制,其超时时间的优先级,规定如下:引用服务的方法超时 > 引用服务的全局超时时间 > 服务发布者的方法超时 > 服务发布者的全局超时时间,示例如下:
<sofa:binding.bolt >
<sofa:global-attrs timeout="5000" />
<sofa:method name="message" type="future" timeout="25000"/>
</sofa:binding.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>
在文档使用中是否遇到以下问题
更多建议
匿名提交