CooCare 通讯服务器分布式架构 - housekeeper-software/coocare GitHub Wiki

简介

新版CooCare服务器使用分布式架构。根据业务特点,将工程师通讯服务器(ServiceCenter)与用户通讯服务器(UserCenter) 分开实现。
ServiceCenter只有一个实例,而UserCenter可以有多个实例,根据业务容量需求而定。多个UserCenter前置需要Nginx实现负载均衡。 因为ServiceCenter只有一个实例,所以,达到容量上限之后,需要对业务进行分区。比如为某大型企业提供远程服务,可以在构建之初将
服务器群组规划到一个独立的区域中,其中包含ServiceCenter,UserCenter,redis,mysql,MessageRouter等。总之,是个与其他区域无关
的独立运作环境。

架构图

UserCenter

概要

每个UserCenter需要对外提供两个端口,一个端口用于客户端的TCP方式连接,另外一个端口用于客户端的WebSocket方式连接。其中, WebSocket方式连接可以用于UWP版本,或者网页版本,也可以用于移动客户端版本。当然,Desktop版本也可以使用。WebSocket连接方式比起TCP连接对
服务器压力略大,但也可支撑数十万的连接数。TCP连接方式更加轻量级,效率略高些。
每个UserCenter需要配置两条RPC连接,一条连接到ServiceCenter,用于与工程师通讯,另外一条连接到MessageRouter,用于在不同的UserCenter之间
转发消息。比如踢人消息和多端登录消息,而消息的接收者位于其他的UserCenter上,这时候需要MessageRouter帮忙中转这条消息到指定的UserCenter。

配置说明

多个UserCenter实例可以部署到不同的主机上,这样,可以使用相同的端口。也支持部署到同一台主机上,这时候需要从不同的目录下启动,并且侦听的端口需要
有区别。此刻,Nginx的配置则需要注意到细节的差别。配置文件名称为:servicecenter.json,与可执行文件在同级目录下。不同目录下的实例需要在当前目录下
配置servicecenter.json。 即使现在用户数不多,也可以配置多个实例,这样可以有效利用系统资源。
这里有个关键点:每个UserCenter甚至是ServiceCenter需要一个唯一的serviceId,即使位于同一个主机上。redis需要这些信息作为key。 更多细节,稍后单独讲述
关于Shell,一般,我们可以通过可执行文件增加命令行的方式实现对运行中的实例的简单控制或者查询实时用户数连接数之类的。新版的Shell功能进行了优化,只要分开
在不同的目录,同一个名称的可执行文件shell功能可以正常使用。 这里的关键是在可执行文件当前目录执行shell功能。 配置文件格式如下:


{
  **"server_id":"bjuc"**, //在当前服务器集群中,必须保持唯一性  
  "console_log_level": 0, //log等级,生产环境设置为2 
  "msg_record":true,//记录转发和推送的消息到redis,用于调试诊断
  "heartbeat_timeout_ms": 720000, //连接超时时间,建议是720秒,表示超过720秒没有交互,认为连接中断。  
  "timing_wheel_granularity": 10, //用于处理心跳和超时。  
  "user_buckets": 128,  //根据UID将用户存储到不同的桶里,提高读写锁的效率,128是上限。  
  "max_conn_thread_count": 5, //TCP连接线程数,根据CPU核心数而定。  
  "max_task_thread_count": 5, //任务处理线程数,根据CPU核心数而定。  
  "memory_monitor_release_threshold": 60, //当服务器内存使用率超过这个数,开始回收内存。  
  "memory_monitor_io_interval_ms": 720000,//检测内存使用率的周期。  
  "boss_listen_port": 13888, //TCP侦听端口,只允许一个。  
  "redis_ip": "127.0.0.1", //redis数据库地址  
  "redis_port": 6379, //redis数据库连接端口  
  "redis_timeout":10000, //redis连接超时,毫秒计
  "redis_pwd":"xxx", //redis 密码
  "ws_port": 13889, //webservice登录端口  
  "ws_options": 0, //websocket的一些选项,可以参考libwebsockets官方网站  
  "ws_protocol_name": "ws", //websocket scheme,客户端连接时,ws://ip:port。  
  "max_websocket_thread_count":5, //websocket连接线程数  
  "websocket_ka_string":"300,6,60", //websocket连接超时的一些设定  
  "push_server_url": "http://xxx/push", //如果需要推送,这里给出推送的web service请求接口地址。  
  "force_push": true, //是否强制推送,由于TCP连接是否存活,有一定的超时时间,如果强制推送,就是同样的消息也走推送到达。默认:YES    
  "private_key_string": "", //如果启用TLS通讯,则需要配置证书的密码。  
  "rpc_timeout_ms":30000, //RPC 超时时间,毫秒
  "service_center_server":"127.0.0.1:12890",//ServiceCenter的RPC服务器地址  
  "message_router_server":"127.0.0.1:10888",//MessageRouter的RPC服务器地址   
  "enable_proxy_protocol":true //如果部署在负载均衡的后面
}

ServiceCenter

概要

ServiceCenter服务器在一个区域或者集群中只有一个实例,只用于工程师登录和转发对用户的消息。ServiceCenter为UserCenter提供RPC Server能力,
可以接受UserCenter的RPC连接。只有建立RPC连接之后,工程师才能与用户进行会话。没有RPC连接时,ServiceCenter允许工程师登录和在线。
每个UserCenter通过RPC连接到ServiceCenter之后,ServiceCenter将本机在线的工程师列表转发给UserCenter。也就是说,每个UserCenter都保持
一份ServiceCenter的工程师在线列表。这样,用户端来查询工程师列表时,UserCenter只在本机内存中查找。
当然,工程师登录、下线、改变状态也可以通过RPC连接即时通知所有的UserCenter,UserCenter自行更新内存中的工程师列表状态。
工程师发送给用户的消息,ServiceCenter先查redis确定用户的登录的UserCenter名称,然后指转给一个或者多个
UserCenter,用户可能在多个UserCenter上登录,所以,这里可能转发给多个。

配置说明

ServiceCenter服务器需要配置一个唯一的名称。同时对外提供TCP登录端口和websocket登录端口。
ServiceCenter也可以部署在负载均衡的后面

{
  "server_id":"bjsc", //服务器唯一名称
  "console_log_level": 0,
  "msg_record":true,//记录转发和推送的消息到redis,用于调试诊断
  "heartbeat_timeout_ms": 720000,
  "timing_wheel_granularity": 10,
  "user_buckets": 128,
  "max_conn_thread_count": 5,
  "max_task_thread_count": 5,
  "memory_monitor_release_threshold": 60,
  "memory_monitor_io_interval_ms": 720000,
  "boss_listen_port": 12888,
  "redis_ip": "127.0.0.1",
  "redis_port": 6379,
  "redis_timeout":10000, //redis连接超时,毫秒计
  "redis_pwd":"xxx", //redis 密码
  "ws_port": 12889,
  "ws_options": 0,
  "ws_protocol_name": "ws",
  "max_websocket_thread_count":5,
  "websocket_ka_string":"300,6,60",
  "push_server_url": "http://xxx/push",
  "db_host": "127.0.0.1", //ServiceCenter需要数据库保存工程师在线/下线日志
  "db_port": 3306,
  "db_user": "xx",
  "db_password": "xx",
  "db_name": "xx",
  "db_table_name": "xx",
  "db_keepalive_interval":600,
  "public_user_state_push_url": "http:/xxx/alarm", //工程师状态变化的消息推送接口
  "private_key_string": "",
  "force_push":true,
  "rpc_timeout_ms":30000, //RPC 超时时间,毫秒
  "service_center_server_port":12890, //ServiceCenter RPC server端口
  "enable_proxy_protocol":true //如果部署在负载均衡的后面
}

MessageRoute

概要

MessageRoute用于在多个UserCenter之间进行转发消息。与ServiceCenter没有关系,并且ServiceCenter不会连接到MessageRoute。
每个UserCenter都要尝试与MessageRoute建立RPC连接。其中包含登录过程,提供自己的一些基本信息。同时,MessageRoute也会将UserCenter
的连接情况报告给其他的UserCenter。就是说,每个UserCenter都会维持其他UserCenter在线的信息,这对消息转发是由帮助的。

配置

MessageRoute不需要数据库和redis,单纯提供消息转发。

{
  "console_log_level": 0,
  "rpc_timeout_ms":30000, //RPC 超时时间,毫秒
  "timing_wheel_granularity": 6, //用于处理心跳和超时
  "boss_listen_port": 10888, //侦听的端口
  "memory_monitor_release_threshold": 60,
  "memory_monitor_io_interval_ms": 720000
}

Redis结构

redis将用户的登录信息与在线状态分开存储。
Redis需要持久化,因为保存了用户的登录信息,其中包含推送id,即使用户不在线,也需要用到这些信息。

登录信息

登录信息包含多端登录的信息。key的设计如下

以UserId作为key,每个key下面最多有7个域。  
1.token,用于后台服务器写入token,这是兼容旧版本,新版本建议不要用。  
2.token:0,表示桌面端登录的token。桌面端包含网页登录。    
3.token:1,表示移动端登录的token,移动端目前只有IOS和android,二者均属移动端,不能同时登录,会踢掉前一次登录。  
4.token:2,表示UWP登录。  
5.device:0,表示桌面端的最近一次登录信息,json格式。  
6.device:1,表示移动端最近一次登录信息,格式如上。  
7.device:2,表示UWP最近一次登录信息,同上。  
这里需要说明,原先的token在新版本最好不要用,服务器会做兼容。因为,既然允许多端登录,每次登录都会给不同的token,所以要区分开。  
一般而言,服务器只在登录的时候查询一次token,以后会保存在内存中。所以基本上以前的版本是可以正常使用的。但是,如果UserCenter有多个实例,  
此刻同一个账号可能通过不同的UserCenter登录,写入redis的时机不再是有序的,所以,此刻拿到的token可能是另外一个平台登录的,造成登录失败。  
登录信息格式如下:  
{"address":"127.0.0.1","channel_id":"hp","platform":7,"push_id":"thisisfakeuserpushid","sid":"bjuc","type":1,"uid":"C000000000000000","uptime":"2021/5/2 17:4:2"}  
其中:sid:表示从哪个UserCenter服务器登录。此信息非常关键,消息转发的时候要用到。  
address:用户端的外网地址。如果上了nginx之后,这个地址不再有意义。可能取到的都是nginx服务器地址。  
uptime:登录的服务器时间。  
注意:这里不再包含在线状态。 

在线信息

每个账号的每个端在线信息都存储在以登录服务器sid为key的哈希表中。

格式;key:serviceid  
     field:userid:platformType
     value:0~4,0:离线,1:在线,2:忙碌,3:离开,4:隐身  

平台类型

Windows,Linux,Web属于桌面端,类型为0.
UWP是独立的存在,类型为2
Android,IOS表示移动端,类型为1
踢人规则: 同一个类型踢掉前一次登录的账号。不同的类型允许同时在线。
一个账号登录,会通知其他平台的连接。

外部如何查询用户的在线状态

先查登录表,同时读取 device:0,device:1,device:2,获得三个平台的sid。
分别通过sid查询在线状态。如果是pipeline处理,需要请求两次redis。 多个平台,只要有一个在线就算在线。理论上,如果是移动端或者UWP,只要配置了pushid也应该算做在线。
现代即时通讯已经没有离线这个说法了。

用户状态查询

工程师端查询用户在线状态,每次请求的数量不要太大。redis查询复杂度为 O(n)*2。在台式机上,查询一个用户大概需要几个毫秒。
一般,服务器使用pipeline查询,查多个时间不会成倍增长。最长应该在10毫秒可以得到结果。
查询时,支持混合查询,同时查工程师和用户。

其他

如果ServiceCenter未启动,UserCenter会等待,直到RPC登录到ServiceCenter,并且获取到在线工程师列表,UserCenter会真正启动。
RPC默认超时为30秒,每隔10秒发心跳包。断线之后,每隔1秒尝试重连。
如果只有一台UserCenter,可以不配置MessageRouter,也不用启动MessageRouter。 从架构图上看,ServiceCenter也是可以通过MessageRouter与UserCenter交换信息的,理论和实践都是可行的。之所以没这么做,主要是考虑到
每个UserCenter与ServiceCenter保持一条独立的长连接,这样性能会好一些,可以比较充分的发挥系统的性能。
如果去掉ServiceCenter这个角色,那么系统就是变成一个没有角色之分的IM通讯系统,可以独立使用。
新版服务器内存使用量增大了一些,使用uint64_t作为连接标识,此生没有溢出的机会。

离线消息

只有工程师给用户发的消息才能离线存储,用户给工程师发的消息不会离线存储。
离线消息只有在用户不在线,且没法推送的情况下才会离线存储。
当用户通过任意终端上线之后,服务器自动从redis中检索离线消息,然后主动发送给用户。
离线消息只能通过TCP/websocket长连接方式发送,不会走推送。
离线消息的offline=true,以示区别。
离线消息在redis中存储的记录格式:
off:uid:消息内容( 二进制方式存储, %b)
当用户上线之后,服务器查找上述记录集,读取之后,执行 DEL key。
所以,离线消息读取之后就会被删除。

在线状态补充说明

UserCenter和ServiceCenter在启动或者正常退出之际,都会清理redis属于自己sid的key,从而清除本实例的全部在线信息。
如果需要手工处理,首先要知道serviceid。然后执行redis命令: del serviceid即可。
根据上述机制,redis中保存的用户在线状态是准确可靠的。

#常用shell命令

--cmd=quit,--cmd=stop是通知进程正常退出。此刻会清理redis。  
--cmd=debug,动态设置日志等级到调试模式。  
--cmd=ndebug,动态设置日志等级到生产模式。  
--cmd=user,一般可以查询实时在线用户数。  

服务器维护

比较理想的情况,我们至少部署两个UserCenter实例,即使在同一个主机上。在维护之前,断开其中一台,保留一台。
此刻可以正常退出需要维护的服务器。更新程序和配置,然后重新启动。
以此类推。
更新ServiceCenter,直接停止即可,不要动其他的服务器实例。此刻,用户端可以继续登录,只是不能发消息给工程师而已。

内存回收

"memory_monitor_release_threshold": 60, //当服务器内存使用率超过这个数,开始回收内存。
"memory_monitor_io_interval_ms": 720000,//检测内存使用率的周期。
如果多个服务器部署在一个主机上,是否考虑只允许一个服务器检测内存使用量。
memory_monitor_release_threshold:0,则可禁止某个进程监控和回收内存。这样对系统来说,更加优化。

负载均衡

如有多个UserCenter实例部署在同一个主机或者不同的主机中,需要前置负载均衡。一般,如果负载均衡不与UserCenter部署在同一个主机,则所有的UserCenter均不需要公网IP。
只要负载均衡有公网IP即可。
负载均衡可以购买阿里的负载均衡服务,也可以自己搭建。阿里的负载均衡服务比较稳定,并发和连接数都可以很大,建议直接购买。
目前测试过nginx和HAproxy,最终HAProxy达到了目标。
因为用到了TCP和websocket,所以负载均衡同时需要在4层和7层上代理。 HAProxy的配置文件如下:
这个配置不是最优的,但可以工作。 **负载均衡的后端服务器正常情况不能获得真实客户端IP,需要在负载均衡开启HA Proxy协议。目前UserCenter支持 V1,V2版本的协议,但V2尚未测试过。 ** **如果UserCenter不再负载均衡的后端,"enable_proxy_protocol":false.此刻不会解析HA Proxy协议。

#---------------------------------------------------------------------
# Example configuration for a possible web application.  See the
# full configuration options online.
#
#   http://haproxy.1wt.eu/download/1.4/doc/configuration.txt
#
#---------------------------------------------------------------------

#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
    # to have these messages end up in /var/log/haproxy.log you will
    # need to:
    #
    # 1) configure syslog to accept network log events.  This is done
    #    by adding the '-r' option to the SYSLOGD_OPTIONS in
    #    /etc/sysconfig/syslog
    #
    # 2) configure local2 events to go to the /var/log/haproxy.log
    #   file. A line like the following can be added to
    #   /etc/sysconfig/syslog
    #
    #    local2.*                       /var/log/haproxy.log
    #
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     400000
    user        haproxy
    group       haproxy
    daemon
    #ulimit-n  800034  
    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats

#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------


defaults
  mode  tcp  #混合模式时关闭 默认的模式mode { tcp|http|health },tcp是4层,http是7层,health只会返回OK
  retries 3 #三次连接失败就认为是服务器不可用,也可以通过后面设置 
  #option dontlognull #不记录健康检查的日志信息   
  stats refresh 30 #统计页面刷新间隔   
  option httplog #日志类别http日志格式   
  option tcplog  #日志类别tcp日志格式
  option abortonclose #当服务器负载很高的时候,自动结束掉当前队列处理比较久的链接
  option redispatch #当serverId对应的服务器挂掉后,强制定向到其他健康的服务器  
  balance roundrobin #默认的负载均衡的方式,轮询方式
  maxconn 400000  #默认的最大连接数  
  timeout connect 10m
  timeout client  120m
  timeout server  120m
  #timeout http-keep-alive 10s #http连接生存的时间
  timeout check   10s
  timeout queue   50m  #在队列等待连接槽释放的超时时间
  timeout tunnel  1h  #客户端和服务器端通道非活动超时时间
  log 127.0.0.1 local0 info #默认日志配置

listen usercenter-websocket-26888
  bind 0.0.0.0:26888 ssl crt /etc/cert/coocare.pem  #enable SSL,please use wss://xxx
  mode http
  option http-keep-alive
  stats enable
  option forwardfor #如果后端服务器需要获得客户端真实ip需要配置的参数,可以从Http Header中获得客户ip
  log global
  balance roundrobin
  server websrv1 127.0.0.1:13889 check
  server websrv2 127.0.0.1:13189 check

                   
#---------------------------------------------------------------------
# main frontend which proxys to the backends
#---------------------------------------------------------------------

#---------------------------------------------------------------------
# static backend for serving up images, stylesheets and such
#---------------------------------------------------------------------

#---------------------------------------------------------------------
# round robin balancing between the various backends
#---------------------------------------------------------------------
listen usercenter-tcp-16888
  bind 0.0.0.0:16888
  balance roundrobin
  mode tcp
  maxconn 400000
  stats enable
  option tcplog
  log global 
  server s1 127.0.0.1:13888 weight 1 maxconn 200000 check send-proxy
  server s2 127.0.0.1:13188 weight 1 maxconn 200000 check send-proxy

listen servicecenter-websocket-46888
  bind 0.0.0.0:46888
  mode http
  option http-keep-alive
  stats enable
  option forwardfor #如果后端服务器需要获得客户端真实ip需要配置的参数,可以从Http Header中获得客户ip
  log global
  maxconn 10000
  balance roundrobin
  server websrv1 127.0.0.1:12889 check

listen servicecenter-tcp-36888
  bind 0.0.0.0:36888
  balance roundrobin
  mode tcp
  maxconn 10000
  stats enable
  option tcplog
  log global
  server s1 127.0.0.1:12888 weight 1 maxconn 10000 check send-proxy

listen admin_stats
	bind 0.0.0.0:8099 #监听端口
	mode http         #http的7层模式
	option httplog    #采用http日志格式
	#log 127.0.0.1 local0 err
        stats enable
	maxconn 10
	stats refresh 30s #统计页面自动刷新时间
	stats uri /stats