AVOS Cloud目前还在用Ejabberd做Android的消息推送服务。当时选择Ejabberd,是因为Ejabberd是一个发展很长时间的XMPP实现,并且基于Erlang,设想能在我们自主研发的Push Server起来之间顶上一段时间。
我们自主研发的Push Server预计本月中旬就上线了。但是Ejabberd却先顶不住了。Ejabberd做推送,本身就有劣势,比如XMPP协议的冗余,XMPP协议本来就是IM协议,对推送这个简单的场景还是太复杂了一些。Ejabberd Cluster对于IM这样的场景很有优势,对于一个Ejabberd Cluster内机器来说,A用户连在A服务器,B用户连在B服务器,两者之间的通讯,Ejabberd都帮你处理了,会自动路由AB之间的消息。Ejabberd会在整个Cluster内每个节点维护session数据,每次session的变更都会在节点之间复制。但是对于推送服务来说,没有刚才提到的AB用户的通讯问题,都是服务端往client推送数据,client没有上行的数据,这个session维护的代价反而是没有必要了。
因此,使用Ejabberd推送的第一条优化手段: 不使用Ejabberd Cluster。反而采用多个独立节点的结构,每个节点之间都是独立,不组成集群。因为节点之间独立,用户数据却还需要共享,那么放弃mnesia,而采用odbc数据库的方式存储用户数据就成为自然的选择了。
那么在采用多节点之后,会出现一个问题?推送服务怎么知道应该往哪个节点推送消息,换句话说,推送服务怎么知道要推送的设备连在哪台服务器上?我们的解决办法是引入一个Router服务,它来告诉推送服务和终端设备链接到哪台服务,采用一致性哈希算法来分配连接,减少节点变更带来的影响。最后,我们使用一个简陋但是简单的办法来感知节点是否有效。这个简陋的办法就是使用crontab脚本每隔几秒向router服务发送该机器上ejabberd端口处于ESTABLISHED或LISTEN状态的链接数(netstat命令)。
优化第二条:将数据库服务跑在单独的服务器上。我们使用MySQL,MySQL的优化这里就不提了,innodb配置参数,并发线程、事务级别等等。最好也对数据库做个异步slave备份。
优化第三条,去除不必要的module,我们只用到ejabberd的数据下行和用户账户管理功能,其他模块都可以去掉,下面是一个可以去掉的模块清单:
mod_caps mod_pubsub mod_vcard_odbc mod_roster_odbc mod_privacy_odbc mod_proxy65 mod_disco mod_echo mod_http_fileserver mod_shared_roster mod_service_log mod_irc mod_muc mod_vcard_odbc mod_adhoc mod_configure mod_last
如果你想记录每个账户最后一次连接的时间,可以保留mod_last模块,使用odbc方式记录。
优化第四点,ejabberd.cfg的参数优化(一般在/etc/ejabberd/ejabberd.cfg),包括:
- loglevel, 建议设置成3,只打印warn级别以上日志。
- listen模块,ejabberd_c2s的backlog参数,设置TCP backlog队列长度,建议设置得大一些。因为移动网络下,连接断开重连相对频繁,backlog队列较大可以保证服务端的连接accept能力。
- shaper模块,设置流量控制,默认的normal是1.000 B/s远远不能满足需求。
- max_fsm_queue参数,控制出队消息队列长度,建议不要设置(undefined),或者设置得较大。
- odbc相关参数: odbc_pool_size控制连接池大小,设置大一些没关系。odbc_keepalive_interval检测连接是否存活,跟MySQL的wait_timeout保持一致,odbc_start_interval设置重连间隔,默认即可。
- 权限控制(Access)相关参数:max_user_offline_messages设置最大保存的离线消息数目,根据你的应用需要,设置一个合理的值,我们是设置成5。max_user_sessions设置每个用户最多登录的session数目,建议设置小于等于2,防止重复登录。rigister权限控制给admin使用,预先创建一批admin账号,授予创建用户的权限。每个设备用一个uuid标示,通过这些admin账号注册到ejabberd上。
优化第五条,erlang运行时参数优化(一般在/etc/default/ejabberd文件):
- POLL=true,开启网络层启用Polling,linux一般是epoll,BSD系统是kqueue。编译Erlang的时候必须设置启用才能启用此选项。
- SMP=auto|enable,设置成auto或者enable,测试表明,启用SMP在多核机器上能极大地改善吞吐量。
- ERL_MAX_PORTS,每个TCP链接都将耗费2到3个的erlang port,因此这个参数也设置得大一些,>=30万。
- PROCESSES,进程数目尽管设置得大一些,最好是ERL_MAX_PORTS的数倍。
- ERL_MAX_ETS_TABLES: ets表数目,如果日志中看到too many db tables的时候,可以增大此参数,默认1400。
优化第六条,TCP内核参数的优化,注意下列参数:
net.ipv4.tcp_syncookies=1
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_tw_recycle=0
net.ipv4.tcp_timestamps=1
net.ipv4.tcp_max_syn_backlog=65535
net.ipv4.tcp_fin_timeout=30
具体含义请自行Google,切记,不要同时开启tcp_tw_recycle字段和tcp_timestamps字段,建议开启tcp_timestamps,原因参考雕梁哥的这篇博客。
如果有开启防火墙iptables,还需要注意net.netfilter.nf_conntrack_max参数,这是用来限制防火墙跟踪链接的状态表的大小,对于推送服务来说保持大量长连接是很容易超过默认的65535的,如何配置请参考这篇文章。
最后,更多的优化手段,其实就需要深入ejabberd源码了,比如存储的优化,使用redis作为离线消息存储等;erlang语言和vm层面的优化,系统层面的优化(CPU绑定、内核参数进一步调整等)。这就超过我目前的能力范围了,我们的新推送服务基于Netty和Clojure开发,以后应该不会再跟ejabberd打交道了。整体上来说,ejabberd作为一个早期推送方案,支撑百万级别的推送是没有问题的,虽然可能成本会稍微高了一些。