[從原始碼學設計]螞蟻金服SOFARegistry網路操作之連線管理
0x00 摘要
SOFARegistry 是螞蟻金服開源的一個生產級、高時效、高可用的服務註冊中心。
本系列文章重點在於分析設計和架構,即利用多篇文章,從多個角度反推總結 DataServer 或者 SOFARegistry 的實現機制和架構思路,讓大家藉以學習阿里如何設計。
本文為第三篇,介紹SOFARegistry網路操作之連線管理。
0x01 業務領域
上文我們講解了SOFARegistry的網路封裝和操作,本文繼續網路相關部分。
雖然SOFABolt底層已經做了連線管理,比如有Manager,pool,但是SOFARegistry在上層結合業務也做了連線管理,很有特色,所以我們專文講解。
1.1 應用場景
這裡我們集中從DataServer角度出發講解,此處和業務緊密結合。
讓我們大致想想DataServer需要管理哪些連線或者類似概念。
- MetaServer Connection:本DataServer與Meta Server的連線,用來和Meta Server互動;
- DataServer Connection:本DataServer與其他dataServer的連線,用來資料同步;
- 擴充套件開來,其他Data Server節點也需要管理;
- SessionServer Connection,本DataServer與Session server的連線,這個非常複雜,這裡會重點講解;
- 在SessionServer方面,又需要區分具體每個Publisher;
這就讓我們來思考幾個問題:
- 究竟什麼可以唯一標示一個SessionServer?
- 什麼可以唯一標示一個Publisher?ip : port?或者其他?
- 業務上有沒有特殊考慮的需要?
具體我們在後文會詳述阿里的思路。
0x02 管理內容
2.1 連線管理
首先講講普遍意義的連線管理。
連線管理是網路操作中的核心。我們知道,一次 tcp 請求大致分為三個步驟:建立連線、通訊、關閉連線。每次建立新連線都會經歷三次握手,中間包含三次網路傳輸,對於高併發的系統,這是一筆不小的負擔;關閉連線同樣如此。為了減少每次網路呼叫請求的開銷,對連線進行管理、複用,可以極大的提高系統的效能。
為了提高通訊效率,我們需要考慮複用連線,減少 TCP 三次握手的次數,因此需要有連線管理的機制。
關於連線管理,SOFARegistry有兩維度層次的連線管理,分別是 Connection 和 Node。
2.2 管理內容
普遍意義的連線管理,通常需要處理:
- 連線建立與銷燬
- 心跳管理
- 空閒連線管理
- 斷線重連
- 慢連線處理
- 作為一個框架,當然還需要把各種連線事件分派給使用者進行定製
因為SOFABolt底層已經做了底層連線管理,所以SOFARegistry只要做頂層部分連線管理即可,就是從業務角度區分儲存連線。
0x03 Connection管理
3.1 Connection物件
這裡說的Connection我們特指sofa-bolt的Connection物件com.alipay.remoting.Connection
。前文提到,SOFARegistry把sofa-bolt的Connection物件直接暴露出來。
面向連線的TCP協議要求每次peer間通訊前建立一條TCP連線,該連線可抽象為一個4元組(four-tuple,有時也稱socket pair):socket(localIp, localPort, remoteIp, remotePort )
,這4個元素唯一地代表一條TCP連線。
在Netty中用Channel來表示一條TCP連線,在sofa-bolt使用Connection物件來抽象一個連線,一個連線在client跟server端各用一個connection物件表示。
有了Connection這個抽象之後,自然的需要提供介面來管理Connection, 這個介面就是ConnectionFactory。
那麼Connection是如何跟Netty進行聯動呢。我們知道在Netty中,client連線到server後,server會回撥initChannel
方法,在這個方法我們會初始化各種事件handler,sofa-bolt就在這裡建立Connection,並在Netty的Channel物件上打上Connection標,後續通過Channel就可以直接找到這個Connection。
3.2 Connection類定義
Connection其刪減版定義如下,可以看到其主要成員就是 Netty channel 例項:
public class Connection {
private Channel channel;
private final ConcurrentHashMap<Integer, InvokeFuture> invokeFutureMap = new ConcurrentHashMap<Integer, InvokeFuture>(4);
/** Attribute key for connection */
public static final AttributeKey<Connection> CONNECTION = AttributeKey.valueOf("connection");
/** Attribute key for heartbeat count */
public static final AttributeKey<Integer> HEARTBEAT_COUNT = AttributeKey.valueOf("heartbeatCount");
/** Attribute key for heartbeat switch for each connection */
public static final AttributeKey<Boolean> HEARTBEAT_SWITCH = AttributeKey.valueOf("heartbeatSwitch");
/** Attribute key for protocol */
public static final AttributeKey<ProtocolCode> PROTOCOL = AttributeKey.valueOf("protocol");
/** Attribute key for version */
public static final AttributeKey<Byte> VERSION = AttributeKey.valueOf("version");
private Url url;
private final ConcurrentHashMap<Integer/* id */, String/* poolKey */> id2PoolKey = new ConcurrentHashMap<Integer, String>(256);
private Set<String> poolKeys = new ConcurrentHashSet<String>();
private final ConcurrentHashMap<String/* attr key*/, Object /*attr value*/> attributes = new ConcurrentHashMap<String, Object>();
}
省去 AtributeKey 型別定義以及 Log 配置,以上是Connection中主要的成員變數。包括幾個方面:
- 連線:Channel、Url
- 版本:protocolCode、version
- 呼叫:invokeFutureMap
- 附著:attributes
- 引用:referenceCount、id2PoolKey、poolKeys
這裡提一下 protocolCode 和 version,版本資訊會被攜帶至對端,用於連線的協商。總的來說,通過對於 Channel 的包裝,Connection 提供了豐富的上下文及引用資訊,是 SOFABolt 連線管理的直接物件。
3.3 ConnectionFactory
SOFARegistry建立了ConnectionFactory 連線工廠,負責建立連線、檢測連線等。
這裡我對Connection進行了種類,分類是我從業務角度出發,強行分為三種Connection,只是為了講解方便。
- MetaServerConnectionFactory,是Meta Server的連線,用來和Meta Server互動。
- DataServerConnectionFactory ,是其他dataServer的連線,用來資料同步;
- SessionServerConnectionFactory,是Session server的連線,這個非常複雜,後續會重點講解。
3.4 MetaServerConnectionFactory
MetaServerConnectionFactory 就是用來對com.alipay.remoting.Connection
進行連線管理。
其核心變數是一個雙層Map,可以理解為一個矩陣,其維度是 Map<dataCenter, Map<ip, Connection>>。
其內部函式比較簡單,望名生意。
public class MetaServerConnectionFactory {
private final Map<String, Map<String, Connection>> MAP = new ConcurrentHashMap<>();
/**
* @param dataCenter
* @param ip
* @param connection
*/
public void register(String dataCenter, String ip, Connection connection) {
Map<String, Connection> connectionMap = MAP.get(dataCenter);
if (connectionMap == null) {
Map<String, Connection> newConnectionMap = new ConcurrentHashMap<>();
connectionMap = MAP.putIfAbsent(dataCenter, newConnectionMap);
if (connectionMap == null) {
connectionMap = newConnectionMap;
}
}
connectionMap.put(ip, connection);
}
/**
* @param dataCenter
* @param ip
*/
public Connection getConnection(String dataCenter, String ip) {
if (MAP.containsKey(dataCenter)) {
Map<String, Connection> map = MAP.get(dataCenter);
if (map.containsKey(ip)) {
return map.get(ip);
}
}
return null;
}
/**
* @param dataCenter
*/
public Map<String, Connection> getConnections(String dataCenter) {
if (MAP.containsKey(dataCenter)) {
return MAP.get(dataCenter);
}
return new HashMap<>();
}
/**
* @param dataCenter
*/
public Set<String> getIps(String dataCenter) {
if (MAP.containsKey(dataCenter)) {
Map<String, Connection> map = MAP.get(dataCenter);
if (map != null) {
return map.keySet();
}
}
return new HashSet<>();
}
/**
* @param dataCenter
*/
public void remove(String dataCenter) {
Map<String, Connection> map = getConnections(dataCenter);
if (!map.isEmpty()) {
for (Connection connection : map.values()) {
if (connection.isFine()) {
connection.close();
}
}
}
MAP.remove(dataCenter);
}
/**
* @param dataCenter
* @param ip
*/
public void remove(String dataCenter, String ip) {
if (MAP.containsKey(dataCenter)) {
Map<String, Connection> map = MAP.get(dataCenter);
if (map != null) {
map.remove(ip);
}
}
}
public Set<String> getAllDataCenters() {
return MAP.keySet();
}
}
3.5 DataServerConnectionFactory
DataServerConnectionFactory 就是用來對com.alipay.remoting.Connection
進行連線管理。
其核心變數是以ip:port作為key,Connection作為value的一個Map。
其內部函式比較簡單,望名生意。
import com.alipay.remoting.Connection;
/**
* the factory to hold connections that other dataservers connected to local server
*/
public class DataServerConnectionFactory {
/**
* collection of connections
* key:connectId ip:port
*/
private final Map<String, Connection> MAP = new ConcurrentHashMap<>();
/**
* register connection
*
* @param connection
*/
public void register(Connection connection) {
MAP.put(getConnectId(connection), connection);
}
/**
* remove connection by specific ip+port
*
* @param connection
*/
public void remove(Connection connection) {
MAP.remove(getConnectId(connection));
}
/**
* get connection by ip
*
* @param ip
* @return
*/
public Connection getConnection(String ip) {
return MAP.values().stream().filter(connection -> ip.equals(connection.getRemoteIP()) && connection.isFine()).findFirst().orElse(null);
}
private String getConnectId(Connection connection) {
return connection.getRemoteIP() + ":" + connection.getRemotePort();
}
}
3.5.1 註冊
當需要管理連線時候,可以通過如下來進行註冊。
public void connected(Channel channel) throws RemotingException {
super.connected(channel);
dataServerConnectionFactory.register(((BoltChannel) channel).getConnection());
}
這樣往ConcurrentHashMap client放入,是根據IP和port構建了url,然後url作為key。
3.5.2 獲取
com.alipay.remoting.Connection
可以通過Channel進行獲取。
DataNodeExchanger就採用如下方式獲取Client。
conn = ((BoltChannel) dataNodeExchanger.connect(new URL(ip, dataServerConfig
.getSyncDataPort()))).getConnection();
3.5.3 DataSyncServerConnectionHandler
有了註冊與獲取,接下來我們看看連線事件響應。
DataSyncServerConnectionHandler 是 server 的handler。
前文提到了,DataSyncServerConnectionHandler是連線事件處理器 (ConnectionEventProcessor),用來監聽建連事件(ConnectionEventType.CONNECT)與斷連事件(ConnectionEventType.CLOSE)。
這裡就是針對各種事件,簡單的對Connection做相應維護。
public class DataSyncServerConnectionHandler extends AbstractServerHandler {
@Autowired
private DataServerConnectionFactory dataServerConnectionFactory;
@Override
public ChannelHandler.HandlerType getType() {
return ChannelHandler.HandlerType.LISENTER;
}
@Override
public void connected(Channel channel) throws RemotingException {
super.connected(channel);
dataServerConnectionFactory.register(((BoltChannel) channel).getConnection());
}
@Override
public void disconnected(Channel channel) throws RemotingException {
super.disconnected(channel);
dataServerConnectionFactory.remove(((BoltChannel) channel).getConnection());
}
@Override
protected Node.NodeType getConnectNodeType() {
return Node.NodeType.DATA;
}
}
3.6 SessionServerConnectionFactory
SessionServerConnectionFactory 包括複雜的邏輯。
3.6.1 問題
回顧前面問題:
- 究竟什麼可以唯一標示一個SessionServer?
- 什麼可以唯一標示一個Publisher?
- ip : port?或者其他?
- 業務上有沒有特殊考慮的需要?
下面我們就一一看看阿里如何處理。
3.6.2 邏輯概念和關係
首先要講講阿里的幾個邏輯概念:
- process Id 代表了Session Server,格式是類似uid的構建,每個Session Server有一個唯一的process Id,Session Server與process Id是一對一的關係;
- Connection 就是一個 Session Server 和 Data Server 之間的 Connection;
- connect Id 代表了Publisher,格式是 ip : port。connect Id與Publisher是一對一的關係;
- 一個Session Server包括許多Publiser,即許多connection id;
- Session Server address 是一個 ip : port 的組合,代表一個 Connection 的 session server 那一端;
- 一個 Session Server 可能對於一個data Server有多個連線;這個目前原因不知,沒有發現業務原因,可能推測如下:因為連線敏感性,網路不穩定性,所以SOFABolt重連時候會選擇一個新埠,所以會有多個Connection存在。所以一個processID對應多個sessionConnAddress;
具體就是,SOFARegistry 將服務資料 (PublisherRegister) 和 服務釋出者 (Publisher) 的連線的生命週期繫結在一起:每個 PublisherRegister 定義屬性 connId,connId 由註冊本次服務的 Publisher 的連線標識 (IP 和 Port)構成,也就是隻要該 Publisher 和 SessionServer 斷連,服務資訊資料即失效。客戶端重新建連成功後重新註冊服務資料,重新註冊的服務資料會被當成新的資料,考慮更換長連線後 Publisher 的 connId 是 Renew 新生成的。
3.6.3 示例圖
我們假設一個Session server內部有兩個 Publisher,都連線到一個Data Server上。
這些 address 格式都是 ip : port,舉例如下:
-
SessionServer address 1 是 :1.1.2.3 : 1
-
SessionServer address 2 是 :1.1.2.3 : 2
-
SessionServer address 3 是 :1.1.2.3 : 3
-
SessionServer address 4 是 :1.1.2.3 : 4
-
DataServer address 1 是 :2.2.2.3 : 1
-
DataServer address 2 是 :2.2.2.3 : 2
具體邏輯如圖:
+----------+ +----------+
| Client | | Client |
+----+-----+ +----+-----+
| |
| |
| |
| |
| SessionServer address 1 | SessionServer address 2
v v
+--------+-----------------------------------+----------------+
| Session Server(process Id) |
| |
| +------------------------+ +-----------------------+ |
| | Publisher(connect Id) | ... | Publisher(connect Id) | |
| +------------------------+ +-----------------------+ |
+-------------------------------------------------------------+
| SessionServer address 3 | SessionServer address 4
| |
| |
| |
| |
+----------> +---------------+ <---------+
DataServer address 1 | Data Server | DataServer address 2
+---------------+
3.6.4 主要變數
所以,SessionServerConnectionFactory的幾個變數就對應了上述這些邏輯關係,具體如下:
- SESSION_CONN_PROCESS_ID_MAP : Map<SessionServer address, SessionServer processId>,這個代表了怎麼從 SessionServer address 找到 SessionServer processId,是一對一的關係;
- PROCESS_ID_CONNECT_ID_MAP : Map<SessionServer processId, Set<ip:port of clients> >,這個代表了一個Session Server 包括了哪些Publiser;
- PROCESS_ID_SESSION_CONN_MAP : Map<SessionServer processId, pair(SessionServer address, SessionServer connection)>,這代表了一個 Session Server 包括哪些 Connection,每個Connection 被其Session Server 端的address 唯一確定;
這些都代表了本 Data Server 和 其 Session Server 之間的關係。
+-----------------------------------------------------------------------------------------+ +--------------------------------+
| SessionServerConnectionFactory | | SessionServer |
| | | |
| | | +-------------------------+ |
| +---------------------------------------------------------+ | | | SessionServer address | |
| | SESSION_CONN_PROCESS_ID_MAP | | | | | |
| | | | | | +----------------+ | |
| | | +----------------------------------->+ | process Id | | |
| | Map<SessionServer address, SessionServer processId> | | | +-------------------------+ |
| | | | | | | |
| +---------------------------------------------------------+ | | | Publisher | |
| | | +--+-------------+ |
| | | ^ |
| +---------------------------------------------------------+ | | | |
| | PROCESS_ID_CONNECT_ID_MAP | | +------------------------+-------+
| | | | | ^
| | Map<SessionServer processId, Set<ip:port of clients> > +-------------------------------------------+ |
| | | | |
| +---------------------------------------------------------+ | |
| | |
| | |
| +------------------------------------------------------------------------------------+ | +------------+ |
| |PROCESS_ID_SESSION_CONN_MAP +-------> | Connection +---------+
| | | | +------------+
| | | |
| |Map<SessionServer processId, pair(SessionServer address, SessionServer connection)> | |
| | | |
| +------------------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------------+
手機上如下圖:
具體類定義如下:
public class SessionServerConnectionFactory {
private static final int DELAY = 30 * 1000;
private static final Map EMPTY_MAP = new HashMap(0);
/**
* key : SessionServer address
* value: SessionServer processId
*/
private final Map<String, String> SESSION_CONN_PROCESS_ID_MAP = new ConcurrentHashMap<>();
/**
* key : SessionServer processId
* value: ip:port of clients
*/
private final Map<String, Set<String>> PROCESS_ID_CONNECT_ID_MAP = new ConcurrentHashMap<>();
/**
* key : SessionServer processId
* value: pair(SessionServer address, SessionServer connection)
*/
private final Map<String, Pair> PROCESS_ID_SESSION_CONN_MAP = new ConcurrentHashMap<>();
@Autowired
private DisconnectEventHandler disconnectEventHandler;
}
3.6.5 Pair
這是SessionServerConnectionFactory的內部類。
PROCESS_ID_SESSION_CONN_MAP是 Map<SessionServer processId, pair(SessionServer address, SessionServer connection)>,代表了一個 Session Server 包括哪些Connection,每個Connection 被其Session Server 端的address 唯一確定。
Pair就是SessionServer address, SessionServer connection的組合,定義如下:
private static class Pair {
private AtomicInteger roundRobin = new AtomicInteger(-1);
private Map<String, Connection> connections;
private String lastDisconnectedSession;
private Pair(Map<String, Connection> connections) {
this.connections = connections;
}
@Override
public boolean equals(Object o) {
return connections.equals(((Pair) o).getConnections())
&& (((Pair) o).lastDisconnectedSession.equals(lastDisconnectedSession));
}
/**
* Getter method for property <tt>connections</tt>.
* @return property value of connections
*/
private Map<String, Connection> getConnections() {
return connections;
}
}
當生成時,Session Server 端的address,這是由InetSocketAddress轉換而來。此類用於實現 IP 套接字地址 (IP 地址+埠號),用於socket 通訊;
public void registerSession(String processId, Set<String> connectIds, Connection connection) {
String sessionConnAddress = NetUtil.toAddressString(connection.getRemoteAddress());
SESSION_CONN_PROCESS_ID_MAP.put(sessionConnAddress, processId);
Set<String> connectIdSet = PROCESS_ID_CONNECT_ID_MAP
.computeIfAbsent(processId, k -> ConcurrentHashMap.newKeySet());
connectIdSet.addAll(connectIds);
Pair pair = PROCESS_ID_SESSION_CONN_MAP.computeIfAbsent(processId, k -> new Pair(new ConcurrentHashMap<>()));
pair.getConnections().put(sessionConnAddress, connection);
}
3.6.6 processId
processId是在Session Server之中生成,可以看出,是IP,時間戳,迴圈遞增整數構建。這樣就可以唯一確定一個SessionServer。
public class SessionProcessIdGenerator {
/**
* Generate session processId.
*/
public static String generate() {
String localIp = NetUtil.getLocalSocketAddress().getAddress().getHostAddress();
if (localIp != null && !localIp.isEmpty()) {
return getId(getIPHex(localIp), System.currentTimeMillis(), getNextId());
}
return EMPTY_STRING;
}
}
3.7 SessionServerConnectionFactory業務流程
因為高層連線管理與業務密切耦合,所以我們接下來分析業務。看看呼叫 SessionServerConnectionFactory的業務流程。
具體registerSession從何處呼叫,這就涉及到兩個訊息:SessionServerRegisterRequest 和PublishDataRequest。即有兩個途徑會呼叫。而且業務涉及到Session Server與DataServer。
3.7.1 SessionServerRegisterRequest
當重新連線的時候,會統一註冊 Session Server 本身包含的所有Publisher。對應在Session Server之中,如下可以看到:
- 從sessionServer獲取connectIds。
- 建立SessionServerRegisterRequest,然後傳送。
程式碼如下:
public class SessionRegisterDataTask extends AbstractSessionTask {
@Override
public void setTaskEvent(TaskEvent taskEvent) {
//taskId create from event
if (taskEvent.getTaskId() != null) {
setTaskId(taskEvent.getTaskId());
}
Object obj = taskEvent.getEventObj();
if (obj instanceof BoltChannel) {
this.channel = (BoltChannel) obj;
}
Server sessionServer = boltExchange.getServer(sessionServerConfig.getServerPort());
if (sessionServer != null) {
Collection<Channel> chs = sessionServer.getChannels();
Set<String> connectIds = new HashSet<>();
chs.forEach(channel -> connectIds.add(NetUtil.toAddressString(channel.getRemoteAddress())));
sessionServerRegisterRequest = new SessionServerRegisterRequest(
SessionProcessIdGenerator.getSessionProcessId(), connectIds);
}
}
}
來到DataServer,SessionServerRegisterHandler會進行處理呼叫,用到了sessionServerConnectionFactory。
public class SessionServerRegisterHandler extends
AbstractServerHandler<SessionServerRegisterRequest> {
@Override
public Object doHandle(Channel channel, SessionServerRegisterRequest request) {
Set<String> connectIds = request.getConnectIds();
if (connectIds == null) {
connectIds = new HashSet<>();
}
sessionServerConnectionFactory.registerSession(request.getProcessId(), connectIds,
((BoltChannel) channel).getConnection());
return CommonResponse.buildSuccessResponse();
}
}
3.7.2 PublishDataRequest
當註冊Publisher時候。在Session Server之中,可以看到建立了請求。
private Request<PublishDataRequest> buildPublishDataRequest(Publisher publisher) {
return new Request<PublishDataRequest>() {
private AtomicInteger retryTimes = new AtomicInteger();
@Override
public PublishDataRequest getRequestBody() {
PublishDataRequest publishDataRequest = new PublishDataRequest();
publishDataRequest.setPublisher(publisher);
publishDataRequest.setSessionServerProcessId(SessionProcessIdGenerator
.getSessionProcessId());
return publishDataRequest;
}
@Override
public URL getRequestUrl() {
return getUrl(publisher.getDataInfoId());
}
@Override
public AtomicInteger getRetryTimes() {
return retryTimes;
}
};
}
在data server之中,會呼叫處理,用到了sessionServerConnectionFactory。
public class PublishDataHandler extends AbstractServerHandler<PublishDataRequest> {
@Override
public Object doHandle(Channel channel, PublishDataRequest request) {
Publisher publisher = Publisher.internPublisher(request.getPublisher());
if (forwardService.needForward()) {
CommonResponse response = new CommonResponse();
response.setSuccess(false);
response.setMessage("Request refused, Server status is not working");
return response;
}
dataChangeEventCenter.onChange(publisher, dataServerConfig.getLocalDataCenter());
if (publisher.getPublishType() != PublishType.TEMPORARY) {
String connectId = WordCache.getInstance().getWordCache(
publisher.getSourceAddress().getAddressString());
sessionServerConnectionFactory.registerConnectId(request.getSessionServerProcessId(),
connectId);
// record the renew timestamp
datumLeaseManager.renew(connectId);
}
return CommonResponse.buildSuccessResponse();
}
}
3.7.3 DatumLeaseManager
上述程式碼提到了DatumLeaseManager,這裡可以看到就是對connectId,即Publisher進行續約:
-
connectIdRenewTimestampMap : 記錄了renew時間;
-
locksForConnectId :只有一個task能夠更新;
renew 函式記錄本次renew時間戳,啟動evict task,如果到期沒有renew,就去除。
public class DatumLeaseManager implements AfterWorkingProcess {
/** record the latest heartbeat time for each connectId, format: connectId -> lastRenewTimestamp */
private final Map<String, Long> connectIdRenewTimestampMap = new ConcurrentHashMap<>();
/** lock for connectId , format: connectId -> true */
private ConcurrentHashMap<String, Boolean> locksForConnectId = new ConcurrentHashMap();
/**
* record the renew timestamp
*/
public void renew(String connectId) {
// record the renew timestamp
connectIdRenewTimestampMap.put(connectId, System.currentTimeMillis());
// try to trigger evict task
scheduleEvictTask(connectId, 0);
}
}
0x04 節點管理
除了具體連線之外,SOFARegistry也對Data 節點進行另一個維度的連線管理。具體在DataServerNodeFactory完成。
4.1 DataServerNodeFactory
4.1.1 DataServerNode
就是簡單的資料結構,沒有建立Bean。
public class DataServerNode implements HashNode {
private String ip;
private String dataCenter;
private Connection connection;
}
4.1.2 DataServerNodeFactory
對應Node的連線管理 則是 DataServerNodeFactory。
在具體模組控制上,DataServerNodeFactory擁有自己的Bean。DataServerConnectionFactory 則全部是Static型別,直接static使用。
DataServerNodeFactory的關鍵變數有兩個:
- MAP是以dataCenter和ip作為維度的一個Node矩陣,是資料節點相關資料;
- CONSISTENT_HASH_MAP則是用dataCenter作為key,ConsistentHash
作為value的Map;
具體定義如下:
public class DataServerNodeFactory {
/**
* row: dataCenter
* column: ip
* value dataServerNode
*/
private static final Map<String, Map<String, DataServerNode>> MAP = new ConcurrentHashMap<>();
/**
* key: dataCenter
* value: consistentHash
*/
private static final Map<String, ConsistentHash<DataServerNode>> CONSISTENT_HASH_MAP = new ConcurrentHashMap<>();
}
4.2 業務流程
4.2.1 註冊
具體在LocalDataServerChangeEventHandler 和 DataServerChangeEventHandler 全都有涉及。
public class LocalDataServerChangeEventHandler extends
AbstractEventHandler<LocalDataServerChangeEvent> {
private void connectDataServer(String dataCenter, String ip) {
Connection conn = null;
for (int tryCount = 0; tryCount < TRY_COUNT; tryCount++) {
try {
conn = ((BoltChannel) dataNodeExchanger.connect(new URL(ip, dataServerConfig.getSyncDataPort()))).getConnection();
break;
}
}
//maybe get dataNode from metaServer,current has not start! register dataNode info to factory,wait for connect task next execute
DataServerNodeFactory.register(new DataServerNode(ip, dataCenter, conn),
dataServerConfig);
}
}
}
以及
public class DataServerChangeEventHandler extends AbstractEventHandler<DataServerChangeEvent> {
private void connectDataServer(String dataCenter, String ip) {
Connection conn = null;
for (int tryCount = 0; tryCount < TRY_COUNT; tryCount++) {
try {
conn = ((BoltChannel) dataNodeExchanger.connect(new URL(ip, dataServerConfig
.getSyncDataPort()))).getConnection();
break;
} catch (Exception e) {
TimeUtil.randomDelay(3000);
}
}
//maybe get dataNode from metaServer,current has not start! register dataNode info to factory,wait for connect task next execute
DataServerNodeFactory.register(new DataServerNode(ip, dataCenter, conn), dataServerConfig);
}
}
4.2.2 使用
使用就是從MAP與CONSISTENT_HASH_MAP中提取Node,這裡把從CONSISTENT_HASH_MAP提取的程式碼摘錄如下:
/**
* get dataserver by specific datacenter and dataInfoId
*
* @param dataCenter
* @param dataInfoId
* @return
*/
public static DataServerNode computeDataServerNode(String dataCenter, String dataInfoId) {
ConsistentHash<DataServerNode> consistentHash = CONSISTENT_HASH_MAP.get(dataCenter);
if (consistentHash != null) {
return consistentHash.getNodeFor(dataInfoId);
}
return null;
}
public static List<DataServerNode> computeDataServerNodes(String dataCenter, String dataInfoId,
int backupNodes) {
ConsistentHash<DataServerNode> consistentHash = CONSISTENT_HASH_MAP.get(dataCenter);
if (consistentHash != null) {
return consistentHash.getNUniqueNodesFor(dataInfoId, backupNodes);
}
return null;
}
0x05 總結
關於連線管理,SOFARegistry有兩維度層次的連線管理,分別是 Connection 和 Node。
因為SOFABolt底層已經做了底層連線管理,所以SOFARegistry只要做頂層部分連線管理即可,就是從業務角度區分註冊,儲存,獲取連線。具體就是:
- Connection 就是一個 Session Server 和 Data Server 之間的 Connection;
- 一個 Session Server 可能對於一個data Server有多個連線;
- 一個Session Server包括許多Publiser;
- Connection與Publiser一一對應;
SOFARegistry 將服務資料 (PublisherRegister) 和 服務釋出者 (Publisher) 的連線的生命週期繫結在一起:每個 PublisherRegister 定義屬性 connId,connId 由註冊本次服務的 Publisher 的連線標識 (IP 和 Port)構成。
只要該 Publisher 和 SessionServer 斷連,服務資訊資料即失效。客戶端重新建連成功後重新註冊服務資料,重新註冊的服務資料會被當成新的資料,考慮更換長連線後 Publisher 的 connId 是 Renew 新生成的。
如下圖所示:
+----------+ +----------+
| Client | | Client |
+----+-----+ +----+-----+
| |
| |
| |
| |
| |
| |
+-------------------------------------------------------------+
| | Session Server(process Id) | |
| v v |
| +------+-----------------+ +--------+--------------+ |
| | Publisher(connect Id) | ... | Publisher(connect Id) | |
| +------------------------+ +-----------------------+ |
+-------------------------------------------------------------+
| |
| |
| Connection Connection |
| |
| |
| |
v v
+---------------------+------------------------------------------+--------------------+
| Data Server |
| |
| Map<SessionServer address, SessionServer processId> |
| |
| Map<SessionServer processId, Set<ip:port of clients> > |
| |
| Map<SessionServer processId, pair(SessionServer address, SessionServer connection)> |
| |
+-------------------------------------------------------------------------------------+
0xFF 參考
https://timyang.net/architecture/cell-distributed-system/
SOFABolt 原始碼分析12 - Connection 連線管理設計