使用RabbitMQ进行RPC的必要
常见的RPC方法/协议包括CORBA,Java RMI,Web Service,Hessian,Thrift及Rest API,相对于前面提到的RPC方式,使用RabbbitMQ(JMS也是一样)方式需要在client-service provider中间增加MQ组件,这样做增加了部署的复杂性,但同时带来额外的好处。
- 可以对service provider进行保护,MQ对请求进行缓冲,处理不了的请求可以被MQ抛弃而不会压垮service provider。
- 可以隔离低安全区对高安全区的访问,此优点是其他rpc没有的。
一个典型的互联网访问方式如下(FW表示防火墙)
client—-FW1–>frontend server(DMZ)—–FW2—->service provider(高安全区)
通过MQ进行RPC则变成
client—-FW1–>frontend server(DMZ)—>MQ<–FW2—-service provider(高安全区)
这里一个重要的区别是service provider(高安全区)是主动访问位于DMZ区域的MQ,由此可以设定FW2只允许高安全区访问DMZ,而禁止DMZ访问高安全区。此方式对于安全性要求高的如银行,金融,政府系统尤为重要。
实现spring remoting实现使用MQ的RPC
原理
一个RPC交互大致分为以下几个步骤:
1. 服务端监听MQ队列
2. 客户端将调用请求(调用请求包括结果返回队列)发送到队列中。
3. 客户端在结果返回队列监听。
4. 服务端处理业务,将结果发送到结果返回队列。
公共内容
定义RabbitMQ的连接,定义service接口,进行通讯的queue。
public interface Service {
String sayHello(String name);
}
服务端实现
配置文件内容
<?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:context="http://www.springframework.org/schema/context"
xmlns:lang="http://www.springframework.org/schema/lang" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-3.0.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.3.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">
<!--MQ连接-->
<rabbit:connection-factory id="rabbitConnectionFactory" />
<!--创建必要的exchange及queue-->
<rabbit:admin connection-factory="rabbitConnectionFactory" />
<rabbit:direct-exchange name="exchange">
<rabbit:bindings>
<rabbit:binding queue="queue.appgw" key="queue.appgw">
</rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>
<rabbit:queue name="queue.appgw">
</rabbit:queue>
<!--返回结果的template-->
<rabbit:template id="amqpTemplateInternetProxy"
connection-factory="rabbitConnectionFactory">
</rabbit:template>
<!--服务监听-->
<rabbit:listener-container acknowledge="none"
max-concurrency="128" prefetch="10">
<rabbit:listener ref="service" queue-names="queue.appgw" />
</rabbit:listener-container>
<!--服务实现-->
<bean id="serviceImpl" class="net.nxmax.atp.exporter.ServiceImpl"></bean>
<!--服务发布-->
<bean id="service"
class="org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter">
<property name="serviceInterface" value="net.nxmax.atp.exporter.Service" />
<property name="service" ref="serviceImpl" />
<property name="amqpTemplate" ref="amqpTemplateInternetProxy" />
</bean>
</beans>
发布服务代码:
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
while (!r.readLine().equalsIgnoreCase("exit")) {
}
ctx.destroy();
客户端实现
<?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:context="http://www.springframework.org/schema/context"
xmlns:lang="http://www.springframework.org/schema/lang" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-3.0.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.3.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!--MQ连接-->
<rabbit:connection-factory id="rabbitConnectionFactory"/>
<!--发送请求的template-->
<rabbit:template id="amqpTemplate" connection-factory="rabbitConnectionFactory"
queue="queue.appgw" exchange="exchange" reply-timeout="60000" />
<bean class="org.springframework.amqp.remoting.client.AmqpProxyFactoryBean"
p:serviceInterface="net.nxmax.atp.exporter.Service" p:routingKey="queue.appgw">
<property name="amqpTemplate" ref="amqpTemplate" />
</bean>
</beans>
客户端调用代码:
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("ctx-client.xml");
Service service = ctx.getBean(Service.class);
service.sayHello("name");
ctx.destroy();
优化
问题
使用默认的spring实现存在以下的限制:
- 消息使用持久化,对于RPC来说消息是不需要持久化的。
- 消息没有过期时间,意味着后端可能需要处理很久以前的请求,对于RPC来说很久以前的请求应该要抛弃。
- 默认使用临时队列作为结果返回队列,意味着每次调用都需要创建队列,性能极差。
- 可以配置使用固定队列返回结果,但是如果多个节点使用一个配置,同时监听固定队列,可能造成节点收不到结果。
- 使用默认的java作为序列化实现,性能不如Kryo。
方案
针对默认实现存在的问题,可以使用以下优化方案:
- 每一个客户端使用独立规定的结果返回队列,避免创建临时队列。
- 设置消息为非持久化。
- 设置消息的超时时间,使用Kryo作为序列化库。
实现
需要做的关键修改有以下3个:
创建自定义的MessageConverter,将消息设置为非持久化,设定过期时间,以及使用kryo序列化。
public class MessageConverterWithExpire extends SimpleMessageConverter {
/** Logger */
protected static final Logger log = LoggerFactory
.getLogger(MessageConverterWithExpire.class);
// Setup ThreadLocal of Kryo instances
private ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
protected Kryo initialValue() {
Kryo kryo = new Kryo();
// configure kryo instance, customize settings
kryo.register(RemoteInvocation.class);
Registration reg = kryo.register(RemoteInvocationResult.class);
reg.setInstantiator(new ObjectInstantiator() {
@Override
public Object newInstance() {
return new RemoteInvocationResult(null);
}
});
return kryo;
};
};
@Override
protected Message createMessage(Object object,
MessageProperties messageProperties)
throws MessageConversionException {
// expire in ms.
messageProperties.setExpiration("45000");
messageProperties.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);
Kryo k = kryos.get();
Output output = new Output(4096, 4194304);
k.writeClassAndObject(output, object);
return new Message(output.toBytes(), messageProperties);
// return super.createMessage(object, messageProperties);
}
@Override
public Object fromMessage(Message message)
throws MessageConversionException {
Kryo k = kryos.get();
return k.readClassAndObject(new Input(message.getBody()));
// return super.fromMessage(message);
}
}
使用自定义的listenerContainer,在监听前创建规定的结果返回队列。
public class DynamicReplyMessageListenerContainer extends
SimpleMessageListenerContainer {
@Override
protected void doInitialize() throws Exception {
super.doInitialize();
Object listener = getMessageListener();
if (listener instanceof RabbitTemplate) {
RabbitTemplate template = (RabbitTemplate) listener;
Queue queue = getRabbitAdmin().declareQueue();
template.setReplyQueue(queue);
setQueues(queue);
}
}
}
重写AmqpInvokerServiceExporter ,提供通过固定队列返回结果的支持。
public class AmqpInvokerServiceExporterCorrelate extends
AmqpInvokerServiceExporter {
@Override
public void onMessage(Message message) {
Address replyToAddress = message.getMessageProperties()
.getReplyToAddress();
if (replyToAddress == null) {
throw new AmqpRejectAndDontRequeueException(
"No replyToAddress in inbound AMQP Message");
}
Object invocationRaw = getMessageConverter().fromMessage(message);
RemoteInvocationResult remoteInvocationResult;
if (invocationRaw == null
|| !(invocationRaw instanceof RemoteInvocation)) {
remoteInvocationResult = new RemoteInvocationResult(
new IllegalArgumentException(
"The message does not contain a RemoteInvocation payload"));
} else {
RemoteInvocation invocation = (RemoteInvocation) invocationRaw;
remoteInvocationResult = invokeAndCreateResult(invocation,
getService());
}
send(remoteInvocationResult, replyToAddress, message);
}
private void send(Object object, Address replyToAddress,
Message sourceMessage) {
MessageProperties mp = new MessageProperties();
mp.setCorrelationId(sourceMessage.getMessageProperties()
.getCorrelationId());
Message message = getMessageConverter().toMessage(object, mp);
getAmqpTemplate().send(replyToAddress.getExchangeName(),
replyToAddress.getRoutingKey(), message);
}
}
测试结果
默认实现的tps在640左右。经过优化的TPS在4000左右。更多内容请参见测试代码。