基于nodejs的消息中心
需求很简单实时向客户端(目前只有浏览器)推送消息
核心为 rabbitmq + nodejs的socket.io + redis
做消息中心后端的消息中间件必不可少,当初考虑是从rabbitmq和redis选一个。
redis现在很火支持消息订阅性能也非常不错可惜它对消息这块支持的相对弱功能比较少,像消息的过期、ack功能都没有。rabbitmq做为老牌的消息中间件功能完善性能也不错也有很多监控插件可以选择,当然相对redis它也劣势做为企业级中间件占用资源比较多也没有redis那么有上升空间。
websocket的服务选择用nodejs是基本nodejs高效的事件驱动模型还有socket.io对所有浏览器的支持,劣势就是nodejs做为新生语言可参考的成功案例少,第三方模块的稳定性不高且本身服务也容易挂。
这里使用redis是原因socket.io的群集是基于redis的消息实现,我自己也使用它做了nodejs集群内部的消息广播。
各组件的安装:node rabbitmq
业务需求大概是对客户端进行分组解决多个页面的问题,消息内容包含前端所需调用js方法名和消息体(话说这方式我真心不认可)。
1.建立socket.io服务,使用express形式
- var express=require('express')
- ,app = express()
- , sio=require('socket.io')
- , http = require('http').createServer(app)
- , io = sio.listen(http);
- //创建webscoket监听服务器
- http.listen(port);
- var msgSubClient = redisConnectionFactory.getConnectionToDB(config.redis.socketDB);
- var msgKey="amsg:";
- //监听连接成功事件
- io.sockets.on('connection',function(socket){
- //监听客户端请求注册
- socket.on('register',function (data){
- if(data.socketId){
- //如果客户端有指定socketId就建立于sokcet.io内部id的map
- socketManage.addIdMap(data.socketId,socket.id);
- socket.set("socketId",data.socketId);
- //订阅指定socketId消息
- msgSubClient.subscribe(msgKey+data.socketId);
- }
- if(data.groupFlag){
- var flagArray=[];
- if(typeof data.groupFlag == 'string'){
- flagArray.push(data.groupFlag);
- }
- if(data.groupFlag instanceof Array){
- flagArray=data.groupFlag;
- }
- for(var i=0;i<flagArray.length;i++){
- //使用socket.io的join方式建立组
- socket.join(flagArray[i]);
- }
- }
- });
- //关闭时清除连接的注册信息
- socket.on('disconnect',function(){
- socketManage.removeIdMap(socket.id);
- socket.get("socketId",function (err,socketId){
- if(!err){
- //取消消息订阅
- msgSubClient.unsubscribe(msgKey+socketId);
- }
- })
- });
- });
- //接收redis 消息
- msgSubClient.on("message", function (channel, message) {
- var socketId=channel.substring(msgKey.length,channel.length);
- var oldId=socketManage.getOldId(socketId);
- var socket=io.sockets.sockets[oldId]; //得到socket客户端连接
- if(socket){
- message=JSON.parse(message);
- var isVolatile=message.isVolatile || true;
- //推送消息到客户端
- emitMessageToClietn(socket,message,isVolatile);
- //log.debug(appId+" emit "+oldId+" msg:"+message);
- }
- });
- //加工消息
- function processMessage(msg){
- if(msg.data){
- if(typeof msg.data == "string"){
- msg.data=JSON.parse(msg.data);
- }
- }
- return msg;
- }
- //推送到客户端消息
- function emitMessageToClietn(socket,msg,isVolatile){
- msg=processMessage(msg);
- //消息是否瞬时
- if(isVolatile){
- //true不保证消息发送到客户端效率高
- socket.volatile.emit('message', msg);
- }else{
- //false保证发送效率不高
- socket.emit('message',msg);
- }
- }
2.接收rabbitmq 消息
- //推送消息给组, io.sockets.in 表示关联组
- function emitMessageToGroup(groupFlag,msg,isVolatile){
- msg=processMessage(msg);
- if(isVolatile){
- io.sockets.volatile.in(groupFlag).emit("message",msg);
- }else{
- io.sockets.in(groupFlag).emit("message",msg);
- }
- }
- var msgPubClient = redisConnectionFactory.getConnectionToDB(config.redis.socketDB);
- var rabbitConnection=rabbitConnectionFactory.getConnection();
- rabbitConnection.on('ready', function () {
- //x-message-ttl 为消息的默认过期时间
- var q=rabbitConnection.queue(config.rabbit.receiveQueueName,
- {durable:true,autoDelete:false,'arguments': {'x-message-ttl': 600000}});
- //订阅消息,prefetchCount:1是为了设置订阅的负载均衡
- q.subscribe({ ack: true, prefetchCount: 1 },function (json, headers, deliveryInfo, m) {
- try{
- var socketSessionId=json.socketSessionId;
- var groupFlag=json.socketGroupFlag;
- var isVolatile=json.isVolatile || true;
- //如有group 就按组推送
- if(groupFlag && groupFlag !=""){
- emitMessageToGroup(groupFlag,json,isVolatile);
- }
- //如有socketSessionId 就按单客户推送
- if(socketSessionId && socketSessionId!=""){
- var oldId=socketManage.getOldId(socketSessionId);
- var socket=io.sockets.sockets[oldId];
- if(socket){
- //推送到客户端消息
- emitMessageToClietn(socket,json,isVolatile);
- }else{
- //推送到队列
- msgPubClient.publish(msgKey+socketSessionId,JSON.stringify(json));
- }
- }
- }catch(e){
- log.error(e);
- }
- try{
- //确认消息完成
- m.acknowledge(true);
- }catch(e){
- log.error("ack msg error:",e);
- }
- });
- });
3.建立nodejs的集群
socket.io设置以redis方式共享客户端
- var config={
- redis:{
- host:'192.168.1.226',
- port:6379,
- debug_mode:false
- }};
- io.configure(function () {
- //使用redis存储会话
- io.set('store', new RedisStore({
- redisPub:config.redis,
- redisSub:config.redis,
- redisClient:config.redis
- }
- ));
- });
使用基于nodejs cluster的mixture来完成单机多进程充分利用多核cpu资源(mixture很简单就两文件)
- var mix = require('mixture').mix("balanced")
- ,bouncy = require('bouncy');
- var config=require('./config.js')
- ,log=require('./logger.js').getLogger("system");
- var count = require('os').cpus().length
- , port = config.socket.port
- ,portmap = [];
- mix.debug(log.info);
- // socket.io instances
- var socketio = mix.task('socket.io', { filename: 'app.js' })
- //var socketio = mix.task('socket.io', { filename: 'app_cached.js' })
- //进程总数
- var workerTatol=config.service.workerTatol || (config.service.isProxy == true ? count-1:count);
- for (var i = 0; i < workerTatol; i++) {
- port++;
- portmap.push(port)
- var worker = socketio.fork({ args: [port, "node_"+createNodeId()] })
- }
- if(config.service.isAnew == true){
- //如果线程死亡,尝试重启
- mix.on("death",function (worker, task){
- setTimeout(function (){
- task.fork({args:worker.args});
- },config.service.anewTime)
- });
- }
- if(config.service.isProxy == true){
- //代理请求
- bouncy(function (req, bounce) {
- bounce(portmap[Math.random()*portmap.length|0])
- }).listen(config.socket.port)
- }
- function createNodeId() {
- // by default, we generate a random id
- return Math.abs(Math.random() * Math.random() * Date.now() | 0);
- };
上述代码还有个bouncy是用来做类似nginx的请求代理,这个只是简单的解决方案不完美。
接下来是较为好的请求代理、负载均衡方案使用nginx,nginx本身不支持tcp连接这里就需要用到某国人写的nginx模块https://github.com/LearnBoost/socket.io/wiki/Nginx-and-Socket.io
安装nginx可看这里
只需要在步骤中记得加上这些就行拉,patch 命令可能需要yum install一下
patch -p1 < /path/to/nginx_tcp_proxy_module/tcp.patch
./configure --add-module=/path/to/nginx_tcp_proxy_module
我是nginx配制
- worker_processes 6;
- #error_log logs/error.log;
- #error_log logs/error.log notice;
- #error_log logs/error.log info;
- #pid logs/nginx.pid;
- events {
- worker_connections 10000;
- }
- tcp {
- upstream websocket {
- server 192.168.0.187:8031;
- server 192.168.0.15:8031;
- server 192.168.0.187:8032;
- server 192.168.0.187:8033;
- server 192.168.0.187:8034;
- check interval=3000 rise=2 fall=5 timeout=1000;
- }
- server {
- listen 80;
- proxy_pass websocket;
- }
- }
4.接下来就剩下启动了,介绍一个管理nodejs服务的模块 forever
forever start -a -l /opt/log.log startup.js 启动
forever stop startup.js 停止
这玩意还有web服务的,我暂时没去弄了
最后服务跑起来拉..当然要做压力测试,好吧到目前我都还没有找到最好的方式!!目前我是使用socket.io的一个java客户端做的
- public class JavaTest {
- List<SocketIO> list=new ArrayList<SocketIO>();
- volatile AtomicInteger count;
- @Test
- public void testJava() throws Exception {
- count=new AtomicInteger(0);
- for (int i = 0; i < 1500; i++) {
- // final SocketIO socket = new SocketIO("http://172.19.1.104:8040/");
- final SocketIO socket = new SocketIO("http://172.19.0.15/");
- // final SocketIO socket = new SocketIO("http://172.19.0.187:8030/");
- socket.connect(new IOCallback() {
- @Override
- public void onMessage(JSONObject json, IOAcknowledge ack) {
- try {
- System.out.println("Server said:" + json.toString(2));
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- @Override
- public void onMessage(String data, IOAcknowledge ack) {
- }
- @Override
- public void onError(SocketIOException socketIOException) {
- socketIOException.printStackTrace();
- }
- @Override
- public void onDisconnect() {
- }
- @Override
- public void onConnect() {
- JSONObject msg=new JSONObject();
- try {
- //msg.put("sessionId", "");
- msg.put("groupFlag", "3");
- socket.emit("register", msg);
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- @Override
- public void on(String event, IOAcknowledge ack, Object... args) {
- count.addAndGet(1);
- // System.out.println("Server triggered event '" + event + "'");
- }
- });
- list.add(socket);
- Thread.sleep(20);
- }
- int old=0;
- for (int i = 0; i < 1000; i++) {
- int cr=count.intValue();
- float ca=0f;
- if(old != cr ){
- ca=cr-old;
- old=cr;
- }
- System.out.println("list size="+list.size()+" msg count="+count.intValue()+" TPS="+ca);
- Thread.sleep(1000);
- }
- }
- @After
- public void after(){
- for (SocketIO socket : list) {
- socket.disconnect();
- }
- }
- }
好吧吐槽一下这个客户端占用cpu很高开不了很多客户端,测试结果:
服务2台、4核虚拟机在服务器上和2核PC机共启动6进程、nginx一台2核PC、redis一台2核PC、rabbitmq在4核虚拟机
消息类型为非volatile
3000客户端:每秒5W消息
4000客户端:每秒3W消息
受限于机器我没法弄更多客户端,这个结果并不是我想要的我觉得标准应该是1W客户端:每秒2W消息。
另压测时如果需要发送的消息大于所能发送消息的最大值时整个集群会在一段时间内崩溃,这里指的消息类型为非volatile如果为volatile就不会出现崩溃我想处理不了的socket.io应该是丢弃了。
总结:
1.一个高负载的redis集群肯定是这个消息中心最需要的,一个客户端从登陆到接收消息到关闭都要通过redis传递各种消息。
2.压测应该还有更加好的方式去做,我试过用socket.io-client在nodejs上跑结果是一开多个连接就各种异常。
3.介于服务崩溃的问题我想需要在接收rabbitmq的消息时做流量控制,目前还没有想好怎样做才好
4.希望能有机会用到现网去..嘿嘿
有用的一些小模块:
log4js 用于日志记录
eventproxy 防止深度嵌套
date-utils 日期的工具类