Java EE 服務器雖內建集群,但僅限於標準服務(Session 複製、負載均衡)。對於**分布式任務協調**、**多租戶隔離**、**自定義領袖選舉**等業務級需求,必須在應用層引入集群協調能力。本文演示如何整合 Apache ZooKeeper(組成員/領袖選舉)與 LinkedIn Norbert(消息路由/負載均衡),構建一個 Spring Integration 驅動的模擬 EAI 事件處理集群。

znode(類文件系統),支持持久/臨時、有序節點。server.id=1
zookeeperConfigFile=server1.cfg
inputEventsDir=C:/test/in_events
outputEventsDir=C:/test/out_events
messageServerPort=2195
tickTime=2000
dataDir=C:/test/node1/data
dataLogDir=C:/test/node1/log
clientPort=2181
initLimit=5
syncLimit=2
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
三端口:clientPort 客戶端連接;2888 對等通信;3888 領袖選舉。
public class ZooKeeperServer implements Runnable {
private QuorumPeer server;
public ZooKeeperServer(File configFile) throws ... {
QuorumPeerConfig config = new QuorumPeerConfig();
config.parse(configFile.getCanonicalPath());
this.config = config;
}
public void run() {
NIOServerCnxn.Factory factory = new NIOServerCnxn.Factory(config.getClientPortAddress(), config.getMaxClientCnxns());
server = new QuorumPeer();
server.setClientPortAddress(config.getClientPortAddress());
// ... 其餘屬性從 config 繼承
server.start(); // 啟動該集群節點
server.join(); // 阻塞等待
}
public boolean isLeader() { return server != null && server.leader != null; }
}
public static void init(QuorumPeerConfig config) {
String host = "localhost:" + config.getClientPortAddress().getPort();
ZooKeeperClusterClient zkClient = new ZooKeeperClusterClient("eai_sample_service", host, 5000);
zkClient.awaitConnectionUninterruptibly();
String nettyUrl = String.format("%s:%d", InetAddress.getLocalHost().getHostName(), Server.getNettyServerPort());
zkClient.addNode(Server.getServerId(), nettyUrl); // 註冊自身端點
zkClient.addListener(new ClusterStateListener()); // 拓撲變更監聽
NetworkServerConfig nsc = new NetworkServerConfig();
nsc.setClusterClient(zkClient);
nsc.setRequestThreadMaxPoolSize(20);
networkServer = new NettyNetworkServer(nsc);
networkServer.registerHandler(new AppMessageHandler(), new CommonSerializer());
networkServer.bind(Server.getServerId()); // 綁定 serverId
}
關鍵點:會話超時(5s)決定拓撲變更感知延遲,應用層需在窗口期內緩衝或重試消息。
public static void init() {
NetworkClientConfig cfg = new NetworkClientConfig();
cfg.setClusterClient(MessagingServer.getZooKeeperClusterClient());
nettyClient = new NettyNetworkClient(cfg, new RoundRobinLoadBalancerFactory());
}
public static Future<String> sendMessage(AppRequestMsg msg, int serverId) {
if (serverId <= 0) return nettyClient.sendRequest(msg, serializer); // 輪詢
else return nettyClient.sendRequestToNode(msg, destNode, serializer); // 指定節點
}
public class ClusterStateListener implements ClusterListener {
public void handleClusterNodesChanged(Set<Node> nodes) {
if (Server.isLeader() && !Server.isFilePollerRunning()) {
Server.startFilePoller(); // 唯一領袖啟動輪詢
}
}
}
<integration:channel id="messageInputChannel"/>
<int-file:inbound-channel-adapter channel="messageInputChannel"
directory="file:#{systemProperties['input.dir']}" filename-pattern="*.*" prevent-duplicates="true">
<integration:poller fixed-rate="10"/> <!-- 10秒輪詢 -->
</int-file:inbound-channel-adapter>
<integration:service-activator input-channel="messageInputChannel"
ref="taskDistributor" method="processMessage"/>
<bean id="taskDistributor" class="ibm.developerworks.article.TaskDistributor"/>
public void processMessage(Message<File> polledMsg) {
File file = polledMsg.getPayload();
String payload = file.getName() + "|~" + Files.toString(file, Charsets.UTF_8);
AppRequestMsg msg = new AppRequestMsg(payload);
Future<String> ack = MessagingClient.sendMessage(msg, -1); // 輪詢分發
String result = ack.get(); // 同步等待確認,亦可異步回調
log.info("Sent & ack: " + result);
}
| 能力 | 技術選型 | 適用場景 |
|---|---|---|
| 組成員/狀態 | ZooKeeper 臨時節點 + Watch | 服務發現、存活檢測、配置推送 |
| 領袖選舉 | ZooKeeper 有序臨時節點 | 單點任務調度、分布式鎖、主備切換 |
| 集群內 RPC | Norbert + Netty | 任務分發、狀態同步、命令下發 |
| 負載均衡 | Norbert LoadBalancerFactory | 無狀態服務路由、多租戶分片 |
| 分布式鎖/隊列 | ZooKeeper 原語或 Curator 框架 | 共享資源互斥、有序任務隊列 |
將協調能力下沉到應用層,賦予微服務/分布式系統「集群原生」特性,是構建高可用、可擴展、自治系統的關鍵一步。ZooKeeper + Norbert 組合提供了成熟、生產就緒的基礎設施,配合 Spring Integration 等 EIP 框架,可快速落地複雜分布式業務邏輯。