需求很简单实时向客户端(目前只有浏览器)推送消息
核心为 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形式
1. var express=require('express')
2. ,app = express()
3. , sio=require('socket.io')
4. , http = require('http').createServer(app)
5. , io = sio.listen(http);
6.
7. //创建webscoket监听服务器
8. http.listen(port);
9.
10. var
11.
12. var msgKey="amsg:";
13.
14. //监听连接成功事件
15. io.sockets.on('connection',function(socket){
16. //监听客户端请求注册
17. 'register',function
18.
19. if(data.socketId){
20. //如果客户端有指定socketId就建立于sokcet.io内部id的map
21. socketManage.addIdMap(data.socketId,socket.id);
22. "socketId",data.socketId);
23. //订阅指定socketId消息
24. msgSubClient.subscribe(msgKey+data.socketId);
25. }
26. if(data.groupFlag){
27. var
28. if(typeof data.groupFlag == 'string'){
29. flagArray.push(data.groupFlag);
30. }
31. if(data.groupFlag instanceof
32. flagArray=data.groupFlag;
33. }
34. for(var
35. //使用socket.io的join方式建立组
36. socket.join(flagArray[i]);
37. }
38. }
39. });
40. //关闭时清除连接的注册信息
41. 'disconnect',function(){
42. socketManage.removeIdMap(socket.id);
43. "socketId",function
44. if(!err){
45. //取消消息订阅
46. msgSubClient.unsubscribe(msgKey+socketId);
47. }
48. })
49. });
50. });
51.
52. //接收redis 消息
53. msgSubClient.on("message", function
54. var
55. var
56. var socket=io.sockets.sockets[oldId]; //得到socket客户端连接
57. if(socket){
58. message=JSON.parse(message);
59. var isVolatile=message.isVolatile || true;
60. //推送消息到客户端
61. emitMessageToClietn(socket,message,isVolatile);
62. //log.debug(appId+" emit "+oldId+" msg:"+message);
63. }
64. });
65. //加工消息
66. function
67. if(msg.data){
68. if(typeof msg.data == "string"){
69. msg.data=JSON.parse(msg.data);
70. }
71. }
72. return
73. }
74. //推送到客户端消息
75. function
76. msg=processMessage(msg);
77. //消息是否瞬时
78. if(isVolatile){
79. //true不保证消息发送到客户端效率高
80. volatile.emit('message', msg);
81. else{
82. //false保证发送效率不高
83. 'message',msg);
84. }
85. }
2.接收rabbitmq 消息
1. //推送消息给组, io.sockets.in 表示关联组
2. function
3. msg=processMessage(msg);
4. if(isVolatile){
5. volatile.in(groupFlag).emit("message",msg);
6. else{
7. in(groupFlag).emit("message",msg);
8. }
9. }
10.
11. var
12.
13. var
14.
15. rabbitConnection.on('ready', function
16. //x-message-ttl 为消息的默认过期时间
17. var
18. true,autoDelete:false,'arguments': {'x-message-ttl': 600000}});
19. //订阅消息,prefetchCount:1是为了设置订阅的负载均衡
20. true, prefetchCount: 1 },function
21. try{
22. var
23. var
24. var isVolatile=json.isVolatile || true;
25. //如有group 就按组推送
26. if(groupFlag && groupFlag !=""){
27. emitMessageToGroup(groupFlag,json,isVolatile);
28. }
29. //如有socketSessionId 就按单客户推送
30. if(socketSessionId && socketSessionId!=""){
31. var
32. var
33. if(socket){
34. //推送到客户端消息
35. emitMessageToClietn(socket,json,isVolatile);
36. else{
37. //推送到队列
38. msgPubClient.publish(msgKey+socketSessionId,JSON.stringify(json));
39. }
40. }
41. catch(e){
42. log.error(e);
43. }
44. try{
45. //确认消息完成
46. true);
47. catch(e){
48. "ack msg error:",e);
49. }
50. });
51. });
3.建立nodejs的集群
socket.io设置以redis方式共享客户端
1. var
2. redis:{
3. '192.168.1.226',
4. port:6379,
5. false
6. }};
7. io.configure(function
8. //使用redis存储会话
9. 'store', new
10. redisPub:config.redis,
11. redisSub:config.redis,
12. redisClient:config.redis
13. }
14. ));
15. });
使用基于nodejs cluster的mixture来完成单机多进程充分利用多核cpu资源(mixture很简单就两文件)
1. var mix = require('mixture').mix("balanced")
2. ,bouncy = require('bouncy');
3.
4. var config=require('./config.js')
5. ,log=require('./logger.js').getLogger("system");
6.
7. var count = require('os').cpus().length
8. , port = config.socket.port
9. ,portmap = [];
10.
11. mix.debug(log.info);
12.
13. // socket.io instances
14. var socketio = mix.task('socket.io', { filename: 'app.js'
15. //var socketio = mix.task('socket.io', { filename: 'app_cached.js' })
16.
17. //进程总数
18. var workerTatol=config.service.workerTatol || (config.service.isProxy == true
19.
20. for (var
21. port++;
22. portmap.push(port)
23. var worker = socketio.fork({ args: [port, "node_"+createNodeId()] })
24. }
25.
26. if(config.service.isAnew == true){
27. //如果线程死亡,尝试重启
28. "death",function
29. function
30. task.fork({args:worker.args});
31. },config.service.anewTime)
32. });
33. }
34.
35. if(config.service.isProxy == true){
36. //代理请求
37. function
38. bounce(portmap[Math.random()*portmap.length|0])
39. }).listen(config.socket.port)
40. }
41.
42. function
43. // by default, we generate a random id
44. return
45. };
上述代码还有个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配制
1.
2. worker_processes 6;
3.
4. #error_log logs/error.log;
5. #error_log logs/error.log notice;
6. #error_log logs/error.log info;
7.
8. #pid logs/nginx.pid;
9.
10.
11. events {
12. 10000;
13. }
14.
15. tcp {
16. upstream websocket {
17. 192.168.0.187:8031;
18. 192.168.0.15:8031;
19. 192.168.0.187:8032;
20. 192.168.0.187:8033;
21. 192.168.0.187:8034;
22. 3000 rise=2 fall=5 timeout=1000;
23. }
24. server {
25. 80;
26. proxy_pass websocket;
27. }
28. }
4.接下来就剩下启动了,介绍一个管理nodejs服务的模块 forever
forever start -a -l /opt/log.log startup.js 启动
forever stop startup.js 停止
这玩意还有web服务的,我暂时没去弄了
最后服务跑起来拉..当然要做压力测试,好吧到目前我都还没有找到最好的方式!!目前我是使用socket.io的一个java客户端做的
1. public class
2.
3. new
4.
5. volatile
6. @Test
7. public void testJava() throws
8. new AtomicInteger(0);
9. for (int i = 0; i < 1500; i++) {
10.
11. // final SocketIO socket = new SocketIO("http://172.19.1.104:8040/");
12. final SocketIO socket = new SocketIO("http://172.19.0.15/");
13. // final SocketIO socket = new SocketIO("http://172.19.0.187:8030/");
14. new
15. @Override
16. public void
17. try
18. "Server said:" + json.toString(2));
19. catch
20. e.printStackTrace();
21. }
22. }
23. @Override
24. public void
25. }
26.
27. @Override
28. public void
29. socketIOException.printStackTrace();
30. }
31. @Override
32. public void
33. }
34.
35. @Override
36. public void
37. new
38. try
39. //msg.put("sessionId", "");
40. "groupFlag", "3");
41. "register", msg);
42. catch
43. e.printStackTrace();
44. }
45. }
46.
47. @Override
48. public void
49. 1);
50. // System.out.println("Server triggered event '" + event + "'");
51. }
52. });
53. list.add(socket);
54. 20);
55. }
56. int old=0;
57. for (int i = 0; i < 1000; i++) {
58. int
59. float
60. if(old != cr ){
61. ca=cr-old;
62. old=cr;
63. }
64. "list size="+list.size()+" msg count="+count.intValue()+" TPS="+ca);
65. 1000);
66. }
67. }
68.
69. @After
70. public void
71. for
72. socket.disconnect();
73. }
74. }
75.
76. }
好吧吐槽一下这个客户端占用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 日期的工具类