[從原始碼學設計]螞蟻金服SOFARegistry網路操作之連線管理

羅西的思考發表於2020-11-28

[從原始碼學設計]螞蟻金服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 連線管理設計

SOFABolt 原始碼分析2 - RpcServer 服務端啟動的設計

SOFABolt 原始碼分析3 - RpcClient 客戶端啟動的設計

相關文章