最近在思考高可用的高性能的服务端架构,面临的主要问题如下:
- 服务端组件怎么做到可插拔?
- 消息怎么智能路由?一个服务端挂了不会影响整个应用,避免单点故障?
- 如何设计一套高性能的Req-Rep,Pub-Sub,Push-Poll模型;
- 如何做到跨语言,让各语言各尽其用,比如编写业务代码可用Java,编写网络通信压缩,加密解密,数据序列化等用C++,而且尽可能减少性能损失?
看来这并不是一件简单的事情,我们这篇博客仅仅思考Req-Rep模式的搭建,我们这里使用开源项目ZeroMQ:
ZeroMQ以嵌入式网络编程库的形式实现了一个并行开发框架(concurrencyframework),能够提供进程内(inproc)、进程间(IPC)、网络(TCP)和广播方式的消息信道,并支持扇出(fan-out)、发布--订阅(pub-sub)、任务分发(taskdistribution)、请求/响应(request-reply)等通信模式。ZeroMQ的性能足以用来构建集群产品,其异步I/O模型能够为多核消息系统提供足够的扩展性。ZeroMQ支持30多种语言的API,可以用于绝大多数操作系统。在提供这些优秀特性的同时,ZeroMQ是开源的,遵循LGPLv3许可。 ZeroMQ的明确目标是“成为标准网络协议栈的一部分,之后进入Linux内核”。
我们的目标是:
- 业务程序的Req-Server集群部署,从网关发来的请求处理(是C/C++编写的网关),程序自动找一台空闲的Req-Server来处理这个请求,并将结果异步分发到服务单网关,从而Push给客户端;
- 一套Req-Server挂了不会影响整个服务端程序运行;
- 当Req过载时,即发送的大量请求在服务端堆积时,服务端“热部署”挂一个新的Req-Server,而不需要重新启动服务端程序;
这看起来的一个简单的任务其实并不简单!
幸好,ZeroMQ已经帮我们处理了大部分工作,考虑到这个网站大多数是Javaer,所以这篇博文我们以Java代码举例,实际上,我们服务端网关使用C编写的。
最简单的请求/应答模式如下:
客户端:
ZMQ.Context context = ZMQ.context(1); ZMQ.Socket requester = context.socket(ZMQ.REQ); requester.connect("tcp://localhost:5559"); for (int requestNbr = 0; requestNbr != 100000; requestNbr++) { String request = "Hello"+requestNbr; System.out.println("Sending Hello " + requestNbr); requester.send(request.getBytes(), 0); byte[] reply = requester.recv(0); System.out.println("Received " + new String(reply) + " " + requestNbr); } requester.close(); context.term();服务端:
ZMQ.Context context = ZMQ.context(1); ZMQ.Socket responder = context.socket(ZMQ.REP); responder.bind("tcp://*:5559"); while (!Thread.currentThread().isInterrupted()) { byte[] request = responder.recv(0); System.out.println("Received from client req:"+new String(request)); Thread.sleep(100); String reply = "World"; responder.send(reply.getBytes(), 0); } responder.close(); context.term();这里有几个不足:
- 当请求方并发数达到一定量时,由于请求在服务端不丢失,对导致大量的请求在服务端堆积,造成server端内存溢出;
- 存在单点故障,当部署的响应程序crash时,请求就无法处理了。
这里我们改善一下,引入请求/响应的Broker,所有的请求经由Broker代理中转,请求方不需要知道有多少server在后台服务,有Broker去决策一个请求交由那个处理请求的server去处理并负责负载均衡,代码如下:
Broker Server:
System.out.println("req Server Broker start...."); ZMQ.Context context = ZMQ.context(1); ZMQ.Socket frontEnd = context.socket(ZMQ.ROUTER); frontEnd.bind("tcp://*:5559"); ZMQ.Socket backEnd = context.socket(ZMQ.DEALER); backEnd.bind("tcp://*:5560"); ZMQQueue q = new ZMQQueue(context,frontEnd,backEnd); q.run(); frontEnd.close(); backEnd.close(); context.term();处理请求的Server(可以部署N个,N个Server热插拔):
ZMQ.Context context = ZMQ.context(1); ZMQ.Socket responder = context.socket(ZMQ.REP); String brokerIP = prop.getProperty("server_req_broker"); System.out.println("serverName:" + brokerIP); responder.connect("tcp://"+brokerIP); while (!Thread.currentThread().isInterrupted()) { byte[] request = responder.recv(0); Thread.sleep(100); String reply = "World"; responder.send(reply.getBytes(), 0); } responder.close(); context.term();