一、背景
近期在实现websocket长连接集群,需要用到集群通讯的场景,不想用消息队列mq这种比较重的方式,找到了一个比较成熟的用于集群通讯的开源组件Jgroups,发现正好适合我们的需求场景,而且比较轻量,如图所示:
当服务端向客户端C1发送消息时,假设RPC调用的服务正好落在S2上,而此时S2和C1并未建立连接,和C1连接的是S4。这个时候就需要服务集群之间进行通讯,S2需要通知S4 给C1发送消息,这个步骤让Jgroups来实现,方便可行。
二、Jgroups应用实践
Jgroups支持udp和tcp方式,笔者在尝试用udp方式时,由于环境问题,集群之间不能很好的通讯,于是改用了tcp方式。 tcp.xml的配置如下:
<!--
TCP based stack, with flow control and message bundling. This is usually used when IP
multicasting cannot be used in a network, e.g. because it is disabled (routers discard multicast).
Note that TCP.bind_addr and TCPPING.initial_hosts should be set, possibly via system properties, e.g.
-Djgroups.bind_addr=192.168.5.2 and -Djgroups.tcpping.initial_hosts=192.168.5.2[7800]
-->
<config xmlns="urn:org:jgroups"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:org:jgroups http://www.jgroups.org/schema/JGroups-3.3.xsd">
<TCP bind_addr="replace_bindlocalhost"
bind_port="7800"
loopback="false"
recv_buf_size="${tcp.recv_buf_size:5M}"
send_buf_size="${tcp.send_buf_size:640K}"
max_bundle_size="64K"
max_bundle_timeout="30"
use_send_queues="true"
sock_conn_timeout="300"
timer_type="new3"
timer.min_threads="4"
timer.max_threads="10"
timer.keep_alive_time="3000"
timer.queue_max_size="500"
thread_pool.enabled="true"
thread_pool.min_threads="1"
thread_pool.max_threads="10"
thread_pool.keep_alive_time="5000"
thread_pool.queue_enabled="false"
thread_pool.queue_max_size="100"
thread_pool.rejection_policy="discard"
oob_thread_pool.enabled="true"
oob_thread_pool.min_threads="1"
oob_thread_pool.max_threads="8"
oob_thread_pool.keep_alive_time="5000"
oob_thread_pool.queue_enabled="false"
oob_thread_pool.queue_max_size="100"
oob_thread_pool.rejection_policy="discard"/>
<TCPPING timeout="3000"
initial_hosts="${jgroups.tcpping.initial_hosts:replace_hostcluster}"
port_range="1"
num_initial_members="10"/>
<MERGE2 min_interval="10000"
max_interval="30000"/>
<FD_SOCK/>
<FD timeout="3000" max_tries="3" />
<VERIFY_SUSPECT timeout="1500" />
<BARRIER />
<pbcast.NAKACK2 use_mcast_xmit="false"
discard_delivered_msgs="true"/>
<UNICAST3 />
<pbcast.STABLE stability_delay="1000" desired_avg_gossip="50000"
max_bytes="4M"/>
<pbcast.GMS print_local_addr="true" join_timeout="3000"
view_bundling="true"/>
<MFC max_credits="2M"
min_threshold="0.4"/>
<FRAG2 frag_size="60K" />
<!--RSVP resend_interval="2000" timeout="10000"/-->
<pbcast.STATE_TRANSFER/>
</config>
其中,TCP.bind_addr 和 TCPPING.initial_hosts是必须设置的。里面的replace_bindlocalhost和replace_hostcluster是需要被动态替换的地方。 由于以上两个参数,在测试环境和生产环境的地址不一样,所以如果直接写死显然不行。查看JChannel源码,也没有显示的提供更改以上2个参数的方法。于是,只能根据JChannel的内部实现,查看哪些地方可以动态去修改,发现了XmlConfigurator可以支持到,实现方式如下:
InputStream input=Thread.currentThread().getContextClassLoader().getResourceAsStream("tcp.xml");
XmlConfigurator conf=XmlConfigurator.getInstance(input);
String tmp=conf.getProtocolStackString();
String tmp2 = conf.replace(tmp, "replace_bindlocalhost", "172.241.12.184");//动态设置的地方1
String param = conf.replace(tmp2, "replace_hostcluster", "172.241.12.184[7800]");//动态设置的地方2
jChannel = new JChannel(param);
// jChannel = new JChannel();
jChannel.setDiscardOwnMessages(true); //不接自己发送的消息
jChannel.setReceiver(this);
jChannel.connect("Jgroups-cluster");
logger.warn("BroadcastNode start success.");
如上2个标识的地方即可动态设置参数来构造JChannel。
基于Jgroups的发送和接收代码整体参考如下:
public class BroadcastNode extends ReceiverAdapter {
private static Logger logger = LoggerFactory.getLogger(BroadcastNode.class);
private static final String input_file = "tcp.xml";
private static final String REPLACE_LOCALHOST = "replace_bindlocalhost";
private static final String REPLACE_HOSTCLUSTER = "replace_hostcluster";
private static final BroadcastNode INSTANCE = new BroadcastNode();
private JChannel jChannel;
private BroadcastNode(){
}
public static BroadcastNode getInstance() {
return INSTANCE;
}
/**
* 初始化
*/
@SuppressWarnings("static-access")
public void start(String hostcluster) {
try {
InputStream input=Thread.currentThread().getContextClassLoader().getResourceAsStream(input_file);
XmlConfigurator conf=XmlConfigurator.getInstance(input);
String tmp=conf.getProtocolStackString();
String tmp2 = conf.replace(tmp, REPLACE_LOCALHOST, HostUtil.getIP());
String param = conf.replace(tmp2, REPLACE_HOSTCLUSTER, hostcluster);
jChannel = new JChannel(param);
// jChannel = new JChannel();
jChannel.setDiscardOwnMessages(true); //不接自己发送的消息
jChannel.setReceiver(this);
jChannel.connect("Jgroups-cluster");
logger.warn("BroadcastNode start success.");
} catch (Exception e) {
logger.error("BroadcastNode start exception,"+e);
if(jChannel != null){
jChannel.close();
}
jChannel= null;
}
}
/**
* 关闭
*/
public void stop() {
if(jChannel != null){
jChannel.close();
}
logger.warn("BroadcastNode stopped.");
}
/**
* 广播消息
* @param msgMap
*/
public void send(Map<String,String> msgMap){
if(msgMap == null || msgMap.isEmpty()){
return;
}
//第一个参数null,代表广播
Message message = new Message(null,JSON.toJSONString(msgMap));
try {
jChannel.send(message);
} catch (Exception e) {
logger.error("BroadcastNode send message error,"+msgMap,e);
}
}
@SuppressWarnings("unchecked")
@Override
public void receive(Message msg) {
String msgStr = (String) msg.getObject();
if(StringUtils.isBlank(msgStr)){
return;
}
try{
//你的业务逻辑
}catch(Exception e){
logger.error("BroadcastNode receive msg deal error,"+msgStr,e);
}
}
@Override
public void viewAccepted(View new_view) {
List<Address> addresses = new_view.getMembers();
StringBuilder sb = new StringBuilder();
for(Address ad : addresses){
sb.append(ad.toString()).append(",");
}
logger.warn("BroadcastNode views changed,new member address:" + sb.toString());
}
@Override
public void getState(OutputStream output) throws Exception {
System.out.println("getState====>>" + output);
}
@Override
public void suspect(Address mbr) {
System.out.println("suspect===>>>" + mbr);
}
}