[從原始碼學設計]螞蟻金服SOFARegistry之推拉模型

羅西的思考發表於2020-12-12

[從原始碼學設計]螞蟻金服SOFARegistry之推拉模型

0x00 摘要

SOFARegistry 是螞蟻金服開源的一個生產級、高時效、高可用的服務註冊中心。

本系列文章重點在於分析設計和架構,即利用多篇文章,從多個角度反推總結 DataServer 或者 SOFARegistry 的實現機制和架構思路,讓大家藉以學習阿里如何設計。

本文為第七篇,介紹SOFARegistry網路操作的推拉模型。

0x01 相關概念

Push還是Pull???

1.1 推模型和拉模型

在觀察者模式中,又分為推模型和拉模型兩種方式。 

推模型:主題物件向觀察者推送主題的詳細資訊,不管觀察者是否需要,推送的資訊通常是主題物件的全部或部分資料。

拉模型:主題物件在通知觀察者的時候,只傳遞少量資訊。如果觀察者需要更具體的資訊,由觀察者主動到主題物件中獲取,相當於是觀察者從主題物件中拉資料。

具體兩個模型詳細剖析如下:

1.1.1 推模型:

特點:
  • 基於客戶器/伺服器機制、由伺服器主動將資訊送到客戶器的技術;
  • “推”的方式是指,Subject維護一份觀察者的列表,每當有更新發生,Subject會把更新訊息主動推送到各個Observer去。
  • 伺服器把資訊送給客戶器之前,並沒有明顯的客戶請求,push事務由伺服器發起;
  • 主題物件向觀察者推送主題的詳細資訊,不管觀察者是否需要,推送的資訊通常是主題物件的全部或部分資料。
  • 推模型是假定主題物件知道觀察者需要的資料;
優點:
  • push模式可以讓資訊主動、快速地尋找使用者/客戶器,資訊的主動性和實時性比較好。
  • 高效。如果沒有更新發生,不會有任何更新訊息推送的動作,即每次訊息推送都發生在確確實實的更新事件之後,都是有意義的。
  • 實時。事件發生後的第一時間即可觸發通知操作。
  • 可以由Subject確立通知的時間,可以避開一些繁忙時間。
  • 可以表達出不同事件發生的先後順序
缺點:
  • 精確性較差,可能推送的資訊並不一定滿足客戶的需求。推送模式不能保證能把資訊送到客戶器;
  • 因為推模式採用了廣播機制,如果客戶器正好聯網並且和伺服器在同一個頻道上,推送模式才是有效的;
  • push模式無法跟蹤狀態,採用了開環控制模式,沒有使用者反饋資訊;
  • 不管觀察者是否需要,推送的資訊通常是主題物件的全部或部分資料;

1.1.2 拉模型

特點:
  • 是由客戶器主動發起的事務。伺服器把自己所擁有的資訊放在指定地址(如IP、port),客戶器向指定地址傳送請求,把自己需要的資源“拉”回來;
  • “拉”的方式是指,各個Observer維護各自所關心的Subject列表,自行決定在合適的時間去Subject獲取相應的更新資料;
  • 拉模型是主題物件不知道觀察者具體需要什麼資料,沒有辦法的情況下,乾脆把自身傳遞給觀察者,讓觀察者自己去按需要取值;
優點:
  • 不僅可以準確獲取自己需要的資源,還可以及時把客戶端的狀態反饋給伺服器;
  • 如果觀察者眾多,Subject來維護訂閱者的列表可能困難或者臃腫,這樣可以把訂閱關係解脫到Observer去完成;
  • Observer可以不理會它不關心的變更事件,只需要去獲取自己感興趣的事件即可;
  • Observer可以自行決定獲取更新事件的時間;
  • 拉的形式可以讓Subject更好地控制各個Observer每次查詢更新的訪問許可權;
缺點:
  • 最大的缺點就是不及時;

1.2 Guava LoadingCache

Guava是Google guava中的一個記憶體快取模組,用於將資料快取到JVM記憶體中。實際專案開發中經常將一些公共或者常用的資料快取起來方便快速訪問。

Google Guava Cache提供了基於容量,時間和引用的快取回收方式。基於容量的方式內部實現採用LRU演算法,基於引用回收很好的利用了Java虛擬機器的垃圾回收機制。

其中的快取構造器CacheBuilder採用構建者模式提供了設定好各種引數的快取物件,快取核心類LocalCache裡面的內部類Segment與jdk1.7及以前的ConcurrentHashMap非常相似,都繼承於ReetrantLock,還有六個佇列,以實現豐富的本地快取方案。

0x02 業務領域

2.1 應用場景

SOFARegistry 的業務特點如下:

  • SOFARegistry 系統分為三個叢集,分別是後設資料叢集 MetaServer、會話叢集 SessionServer、資料叢集 DataServer。
  • DataServer,SessionServer,MetaServer 本質上都是網路應用程式;
  • 複雜的系統有多個地方需要考慮到一致性問題,比如當服務 Publisher 上下線或者斷連時,相應的資料會通過 SessionServer 註冊到 DataServer 中。此時,DataServer 的資料與 SessionServer 會出現短暫的不一致性
  • SOFARegistry 針對不同模組的一致性需求採取了不同的方案。對於 MetaServer 模組來說,採用了強一致性的 Raft 協議來保證叢集資訊的一致性。對於資料模組來說,SOFARegistry 選擇了 AP 保證可用性,同時保證了最終一致性

2.2 問題點

我們通過業務,能夠想到的問題點如下:

  • 新訊息資料需要即時更新,想做到秒級的通知,這一般來說需要推模型;
  • 但是推模型難以確保穩定性;
  • 推模式,客戶端程式碼簡單,由服務端進行推送資料,省去了客戶端無謂的輪詢類操作。但是需要服務端複雜化推送邏輯。
  • 拉模式,需要自行維護偏移量,負載均衡等;

2.3 解決方案

對於以上的問題點,業界一般來說會採用“推”和“拉”結合的形式,例如,服務端只負責通知 “某一些資料已經準備好”,至於是否需要獲取和什麼時候客戶端來獲取這些資料,完全由客戶端自行確定。

2.4 阿里方案

我們首先看看阿里都應用了什麼方案。

2.4.1 各種模型應用

在SOFARegistry‘中,應用了各種模型,比如 :

  • SessionServer 和 DataServer 之間的通訊,是基於推拉結合的機制
    • 推:DataServer 在資料有變化時,會主動通知 SessionServer,SessionServer 檢查確認需要更新(對比 version) 後主動向 DataServer 獲取資料。
    • 拉:除了上述的 DataServer 主動推以外,SessionServer 每隔一定的時間間隔(預設30秒),會主動向 DataServer 查詢所有 dataInfoId 的 version 資訊,然後再與 SessionServer 記憶體的 version 作比較,若發現 version 有變化,則主動向 DataServer 獲取資料。這個“拉”的邏輯,主要是對“推”的一個補充,若在“推”的過程有錯漏的情況可以在這個時候及時彌補。
  • SOFARegistry 服務發現模式採用的是推拉結合方式
    • 客戶端訂閱資訊釋出到服務端時可以進行一次地址列表查詢,獲取到全量資料,並且把對應的服務 ID 版本資訊儲存在 Session 回話層,後續如果服務端釋出資料變更,通過服務 ID 版本變更通知回話層 Session,Session 因為儲存客戶端訂閱關係,瞭解哪些客戶端需要這個服務資訊,再根據版本號大小決定是否需要推送給這個版本較舊的訂閱者,客戶端也通過版本比較確定是否更新本次推送的結果覆蓋記憶體。
    • 此外,為了避免某次變更通知獲取失敗,定期還會進行版本號差異比較,定期去拉取版本低的訂閱者所需的資料進行推送保證資料最終一致。
  • Client 與 SessionServer 之間,完全基於推的機制
    • SessionServer 在接收到 DataServer 的資料變更推送,或者 SessionServer 定期查詢 DataServer 發現資料有變更並重新獲取之後,直接將 dataInfoId 的資料推送給 Client。如果這個過程因為網路原因沒能成功推送給 Client,SessionServer 會嘗試做一定次數(預設5次)的重試,最終還是失敗的話,依然會在 SessionServer 定期每隔 30s 輪訓 DataServer 時,會再次推送資料給 Client。

下面是兩種場景的資料推送對比圖。

img

2.4.2 推拉模型

就本文涉及的問題域來說,螞蟻金服在這裡採用了經典的推拉模型來維持資料一致性,下面我們僅以 Session Server和 Data Server 之間維護資料一致性 為例說明。大致邏輯如下:

SOFARegistry 中採用了 LoadingCache 的資料結構來在 SessionServer 中快取從 DataServer 中同步來的資料。

  • 拉模型
    • 每個 cache 中的 entry 都有過期時間,在拉取資料的時候可以設定過期時間(預設是 30s);
    • 這個過期時間使得 cache 定期去 DataServer 查詢當前 session 所有 sub 的 dataInfoId,對比如果 session 記錄的最近推送version(見com.alipay.sofa.registry.server.session.store.SessionInterests#interestVersions )比 DataServer 小,說明需要推送;
    • 然後 SessionServer 主動從 DataServer 獲取該 dataInfoId 的資料(此時會快取到 cache 裡),推送給 client;
    • 這個“拉”的邏輯,主要是對“推”的一個補充,若在“推”的過程有錯漏的情況可以在這個時候及時彌補
  • 推模型
    • 當 DataServer 中有資料更新時,也會主動向 SessionServer 發請求使對應 cache entry 失效;
    • 當SessionServer 檢查確認需要更新(對比 version) 之後,主動向 DataServer 獲取資料;
    • SessionServer去更新失效 entry。

0x03 拉模型 in Session Server

這裡 SOFARegistry 採用了 Guava LoadingCache 的資料結構,通過給 cache 中的 entry 設定過期時間的方式,使得 cache 定期從 DataServer 中拉取資料以替換過期的 entry。

模型大致示例如下,下文會詳細講解:

 +-----------------------------------------+
 |            Session Server               |
 |                                         |
 | +-------------------------------------+ |
 | |        SessionCacheService          | |
 | |                                     | |
 | | +--------------------------------+  | |
 | | |                                |  | |
 | | |    LoadingCache<Key, Value>    |  | |
 | | |            +                   |  | |
 | | |            |  expireAfterWrite |  | |
 | | |            |                   |  | |
 | | |            v                   |  | |
 | | |     DatumCacheGenerator        |  | |
 | | |            +                   |  | |
 | | +--------------------------------+  | |
 | +-------------------------------------+ |
 |                |                        |
 |                v                        |
 |       +--------+------------+           |
 |       | DataNodeServiceImpl |           |
 |       +--------+------------+           |
 |                |                        |
 +-----------------------------------------+
                  |
                  |   GetDataRequest
                  |
+-------------------------------------------+
                  |
                  |
                  v
          +-------+-----------+
          |   Data Server     |
          +-------------------+

3.1 Bean

相關的Bean定義如下,其中SessionCacheService應用了Guava LoadingCache,DatumCacheGenerator是具體載入實現。

@Configuration
public static class SessionCacheConfiguration {

    @Bean
    public CacheService sessionCacheService() {
        return new SessionCacheService();
    }

    @Bean(name = "com.alipay.sofa.registry.server.session.cache.DatumKey")
    public DatumCacheGenerator datumCacheGenerator() {
        return new DatumCacheGenerator();
    }
}

3.2 程式碼分析

拉模型的實現是在SessionCacheService類,其刪減版程式碼 如下

public class SessionCacheService implements CacheService {
    private final LoadingCache<Key, Value> readWriteCacheMap; 
    private Map<String, CacheGenerator>    cacheGenerators;

    ......
}

可以看到其核心就是利用了

private final LoadingCache<Key, Value> readWriteCacheMap;

3.2.1 Cache構造

構造LoadingCache舉例如下:

  • 其快取池大小為1000,在快取項接近該大小時, Guava開始回收舊的快取項;

  • 其設定快取在寫入之後,設定時間31000毫秒後失效;

  • 生成一個CacheLoader類 實現自動載入,具體載入是呼叫generatePayload;

程式碼如下:

this.readWriteCacheMap = CacheBuilder.newBuilder().maximumSize(1000L)
    .expireAfterWrite(31000, TimeUnit.MILLISECONDS).build(new CacheLoader<Key, Value>() {
        @Override
        public Value load(Key key) {
            return generatePayload(key);
        }
    });

3.2.2 獲取value

獲取value的函式比較簡單:

@Override
public Value getValue(final Key key) throws CacheAccessException {
    Value payload = null;
    payload = readWriteCacheMap.get(key);
    return payload;
}

@Override
public Map<Key, Value> getValues(final Iterable<Key> keys) throws CacheAccessException {
    Map<Key, Value> valueMap = null;
    valueMap = readWriteCacheMap.getAll(keys);
    return valueMap;
}

3.2.3 批量清除

清除批量快取物件,這個API在Data Server 主動給 Session Server 傳送 Push 資料時候會用到,這樣就將引發一次主動獲取。

@Override
public void invalidate(Key... keys) {
    for (Key key : keys) {
        readWriteCacheMap.invalidate(key);
    }
}

3.2.4 自動載入

自動載入是通過CacheGenerator完成。

private Value generatePayload(Key key) {
    Value value = null;
    switch (key.getKeyType()) {
        case OBJ:
            EntityType entityType = key.getEntityType();
            CacheGenerator cacheGenerator = cacheGenerators
                .get(entityType.getClass().getName());
            value = cacheGenerator.generatePayload(key);
            break;
        case JSON:
            break;
        case XML:
            break;
        default:
            value = new Value(new HashMap<String, Object>());
            break;
    }
    return value;
}

3.2.5 設定載入

設定載入是通過如下程式碼完成。

/**
 * Setter method for property <tt>cacheGenerators</tt>.
 *
 * @param cacheGenerators  value to be assigned to property cacheGenerators
 */
@Autowired
public void setCacheGenerators(Map<String, CacheGenerator> cacheGenerators) {
    this.cacheGenerators = cacheGenerators;
}

具體設定時候runtime引數如下:

cacheGenerators = {LinkedHashMap@3368}  size = 1
 "com.alipay.sofa.registry.server.session.cache.DatumKey" -> {DatumCacheGenerator@3374} 

3.3 載入類實現

載入類是通過DatumCacheGenerator完成。

public class DatumCacheGenerator implements CacheGenerator {
    @Autowired
    private DataNodeService     dataNodeService;

    @Override
    public Value generatePayload(Key key) {

        EntityType entityType = key.getEntityType();
        if (entityType instanceof DatumKey) {
            DatumKey datumKey = (DatumKey) entityType;

            String dataCenter = datumKey.getDataCenter();
            String dataInfoId = datumKey.getDataInfoId();

            if (isNotBlank(dataCenter) && isNotBlank(dataInfoId)) {
                return new Value(dataNodeService.fetchDataCenter(dataInfoId, dataCenter));
            } 
        } 

        return null;
    }

    public boolean isNotBlank(String ss) {
        return ss != null && !ss.isEmpty();
    }
}

可以看到,載入具體就是通過DataNodeServiceImpl向 DataServer 發起請求。

public class DataNodeServiceImpl implements DataNodeService {
    @Autowired
    private NodeExchanger         dataNodeExchanger;

    @Autowired
    private NodeManager           dataNodeManager;
  
    @Override
    public Datum fetchDataCenter(String dataInfoId, String dataCenterId) {

        Map<String/*datacenter*/, Datum> map = getDatumMap(dataInfoId, dataCenterId);
        if (map != null && map.size() > 0) {
            return map.get(dataCenterId);
        }
        return null;
    }
  
    @Override
    public Map<String, Datum> getDatumMap(String dataInfoId, String dataCenterId) {

        Map<String/*datacenter*/, Datum> map;

        try {
            GetDataRequest getDataRequest = new GetDataRequest();

            //dataCenter null means all dataCenters
            if (dataCenterId != null) {
                getDataRequest.setDataCenter(dataCenterId);
            }

            getDataRequest.setDataInfoId(dataInfoId);

            Request<GetDataRequest> getDataRequestStringRequest = new Request<GetDataRequest>() {

                @Override
                public GetDataRequest getRequestBody() {
                    return getDataRequest;
                }

                @Override
                public URL getRequestUrl() {
                    return getUrl(dataInfoId);
                }

                @Override
                public Integer getTimeout() {
                    return sessionServerConfig.getDataNodeExchangeForFetchDatumTimeOut();
                }
            };

            Response response = dataNodeExchanger.request(getDataRequestStringRequest);
            Object result = response.getResult();
            GenericResponse genericResponse = (GenericResponse) result;
            if (genericResponse.isSuccess()) {
                map = (Map<String, Datum>) genericResponse.getData();
                map.forEach((dataCenter, datum) -> Datum.internDatum(datum));
            } 
        } 
        return map;
    }
}

拉模型具體如下圖所示:

 +-----------------------------------------+
 |            Session Server               |
 |                                         |
 | +-------------------------------------+ |
 | |        SessionCacheService          | |
 | |                                     | |
 | | +--------------------------------+  | |
 | | |                                |  | |
 | | |    LoadingCache<Key, Value>    |  | |
 | | |            +                   |  | |
 | | |            |  expireAfterWrite |  | |
 | | |            |                   |  | |
 | | |            v                   |  | |
 | | |     DatumCacheGenerator        |  | |
 | | |            +                   |  | |
 | | +--------------------------------+  | |
 | +-------------------------------------+ |
 |                |                        |
 |                v                        |
 |       +--------+------------+           |
 |       | DataNodeServiceImpl |           |
 |       +--------+------------+           |
 |                |                        |
 +-----------------------------------------+
                  |
                  |   GetDataRequest
                  |
+-------------------------------------------+
                  |
                  |
                  v
          +-------+-----------+
          |   Data Server     |
          +-------------------+

0x04 推模型

當 DataServer 中有資料更新時,也會主動向 SessionServer 發請求使對應 entry 失效,從而促使 SessionServer 去更新失效 entry。

4.1 發起推動作

DataChangeRequest in Data Server

當Data Server 有資料變化時候,會主動傳送 DataChangeRequest 給 Session Server。

具體程式碼是在SessionServerNotifier之中,具體如下(這就與前文的Notifier聯絡起來):

public class SessionServerNotifier implements IDataChangeNotifier {

    private AsyncHashedWheelTimer          asyncHashedWheelTimer;

    @Autowired
    private DataServerConfig               dataServerConfig;

    @Autowired
    private Exchange                       boltExchange;

    @Autowired
    private SessionServerConnectionFactory sessionServerConnectionFactory;

    @Autowired
    private DatumCache                     datumCache;
  
    @Override
    public void notify(Datum datum, Long lastVersion) {
        DataChangeRequest request = new DataChangeRequest(datum.getDataInfoId(),
            datum.getDataCenter(), datum.getVersion());
        List<Connection> connections = sessionServerConnectionFactory.getSessionConnections();
        for (Connection connection : connections) {
            doNotify(new NotifyCallback(connection, request));
        }
    }

    private void doNotify(NotifyCallback notifyCallback) {
        Connection connection = notifyCallback.connection;
        DataChangeRequest request = notifyCallback.request;
        try {
            //check connection active
            if (!connection.isFine()) {
                return;
            }
            Server sessionServer = boltExchange.getServer(dataServerConfig.getPort());
            sessionServer.sendCallback(sessionServer.getChannel(connection.getRemoteAddress()),
                request, notifyCallback, dataServerConfig.getRpcTimeout());
        } catch (Exception e) {
            onFailed(notifyCallback);
        }
    }
}

4.2 接收推訊息

DataChangeRequestHandler in Session Server

在Session Server,DataChangeRequestHandler負責響應處理收到的推訊息 DataChangeRequest

可以看到,其呼叫瞭如下程式碼使得Cache失效,進而後續Cache會去Data Server重新load value

sessionCacheService.invalidate(new Key(KeyType.OBJ, DatumKey.class.getName(), new DatumKey(
        dataChangeRequest.getDataInfoId(), dataChangeRequest.getDataCenter())));

其刪減版程式碼如下:

public class DataChangeRequestHandler extends AbstractClientHandler {

    /**
     * store subscribers
     */
    @Autowired
    private Interests                        sessionInterests;

    @Autowired
    private SessionServerConfig              sessionServerConfig;

    @Autowired
    private ExecutorManager                  executorManager;

    @Autowired
    private CacheService                     sessionCacheService;

    @Autowired
    private DataChangeRequestHandlerStrategy dataChangeRequestHandlerStrategy;

    @Override
    public Object reply(Channel channel, Object message) {
        DataChangeRequest dataChangeRequest = (DataChangeRequest) message;
        dataChangeRequest.setDataCenter(dataChangeRequest.getDataCenter());
        dataChangeRequest.setDataInfoId(dataChangeRequest.getDataInfoId());

        //update cache when change
        sessionCacheService.invalidate(new Key(KeyType.OBJ, DatumKey.class.getName(), new DatumKey(
            dataChangeRequest.getDataInfoId(), dataChangeRequest.getDataCenter())));

        try {
            boolean result = sessionInterests.checkInterestVersions(
                dataChangeRequest.getDataCenter(), dataChangeRequest.getDataInfoId(),
                dataChangeRequest.getVersion());
            fireChangFetch(dataChangeRequest);
        } 

        return null;
    }

    private void fireChangFetch(DataChangeRequest dataChangeRequest) {
        dataChangeRequestHandlerStrategy.doFireChangFetch(dataChangeRequest);
    }
}

於是我們的架構圖變化為:

 +----------------------------------------------------------------+
 |                        Session Server                          |
 |                                                                |
 | +-----------------------------------------------------------+  |
 | |                  SessionCacheService                      |  |
 | |                                                           |  |
 | | +-------------------------------------------------------+ |  |
 | | |                                                       | |  |
 | | |    LoadingCache<Key, Value>  <----------+             | |  |
 | | |            +                            |             | |  |
 | | |            |  expireAfterWrite          | invalidate  | |  |
 | | |            |                            |             | |  |
 | | |            v                            |             | |  |
 | | |     DatumCacheGenerator                 |             | |  |
 | | |            +                            |             | |  |
 | | +-------------------------------------------------------+ |  |
 | +-----------------------------------------------------------+  |
 |                |                            |                  |
 |                v                            |                  |
 |       +--------+------------+     +---------+----------------+ |
 |       | DataNodeServiceImpl |     | DataChangeRequestHandler | |
 |       +--------+------------+     +---------+----------------+ |
 |                |                            ^                  |
 +----------------------------------------------------------------+
                  |                            |
   GetDataRequest |                            | DataChangeRequest
                  |                            |
+--------------------------------------------------------------------+
                  |                            |
                  |  Pull                      | Push
                  v                            |
                +-+----------------------------+-+
                |           Data Server          |
                +--------------------------------+

手機上如下:

讓我們在SessionServer內部繼續延伸下,看看當收到推訊息之後,SessionServer是怎樣進行後續的push,就是通知Client。即我們之前提到的:Client 與 SessionServer 之間,完全基於推的機制

4.3 延伸處理Strategy

DefaultDataChangeRequestHandlerStrategy

前面程式碼來到了處理dataChangeRequest的部分。

dataChangeRequestHandlerStrategy.doFireChangFetch(dataChangeRequest);

剩下部分還是 Strategy -- Listener -- Task 的套路(後續有文章講解)。

public class DefaultDataChangeRequestHandlerStrategy implements DataChangeRequestHandlerStrategy {
    @Autowired
    private TaskListenerManager taskListenerManager;

    @Override
    public void doFireChangFetch(DataChangeRequest dataChangeRequest) {
        TaskEvent taskEvent = new TaskEvent(dataChangeRequest.getDataInfoId(),
            TaskEvent.TaskType.DATA_CHANGE_FETCH_CLOUD_TASK);
        taskListenerManager.sendTaskEvent(taskEvent);
    }
}

4.4 延伸處理Listener

DataChangeFetchCloudTaskListener

DataChangeFetchCloudTaskListener在 support函式中配置了支援 DATA_CHANGE_FETCH_CLOUD_TASK。

@Override
public TaskType support() {
    return TaskType.DATA_CHANGE_FETCH_CLOUD_TASK;
}

具體程式碼如下:

public class DataChangeFetchCloudTaskListener implements TaskListener {

    @Autowired
    private Interests                                    sessionInterests;

    @Autowired
    private SessionServerConfig                          sessionServerConfig;

    /**
     * trigger task com.alipay.sofa.registry.server.meta.listener process
     */
    @Autowired
    private TaskListenerManager                          taskListenerManager;

    @Autowired
    private ExecutorManager                              executorManager;

    @Autowired
    private CacheService                                 sessionCacheService;

    private volatile TaskDispatcher<String, SessionTask> singleTaskDispatcher;

    private TaskProcessor                                dataNodeSingleTaskProcessor;

    public DataChangeFetchCloudTaskListener(TaskProcessor dataNodeSingleTaskProcessor) {
        this.dataNodeSingleTaskProcessor = dataNodeSingleTaskProcessor;
    }

    public TaskDispatcher<String, SessionTask> getSingleTaskDispatcher() {
        if (singleTaskDispatcher == null) {
            synchronized (this) {
                if (singleTaskDispatcher == null) {
                    singleTaskDispatcher = TaskDispatchers.createSingleTaskDispatcher(
                        TaskDispatchers.getDispatcherName(TaskType.DATA_CHANGE_FETCH_CLOUD_TASK
                            .getName()), sessionServerConfig.getDataChangeFetchTaskMaxBufferSize(),
                        sessionServerConfig.getDataChangeFetchTaskWorkerSize(), 1000, 100,
                        dataNodeSingleTaskProcessor);
                }
            }
        }
        return singleTaskDispatcher;
    }

    @Override
    public TaskType support() {
        return TaskType.DATA_CHANGE_FETCH_CLOUD_TASK;
    }

    @Override
    public void handleEvent(TaskEvent event) {
        SessionTask dataChangeFetchTask = new DataChangeFetchCloudTask(sessionServerConfig,
            taskListenerManager, sessionInterests, executorManager, sessionCacheService);
        dataChangeFetchTask.setTaskEvent(event);
        getSingleTaskDispatcher().dispatch(dataChangeFetchTask.getTaskId(), dataChangeFetchTask,
            dataChangeFetchTask.getExpiryTime());
    }

}

4.5 延伸處理Task

DataChangeFetchCloudTask

DataChangeFetchCloudTask 會進行後續的push,就是通知Client

public class DataChangeFetchCloudTask extends AbstractSessionTask {
    private final SessionServerConfig sessionServerConfig;

    private Interests                 sessionInterests;

    /**
     * trigger task com.alipay.sofa.registry.server.meta.listener process
     */
    private final TaskListenerManager taskListenerManager;

    private final ExecutorManager     executorManager;

    private String                    fetchDataInfoId;

    private final CacheService        sessionCacheService;
}

會獲取每個Subscriber的 IP,然後向 taskListenerManager 傳送若干種訊息,比如:

  • RECEIVED_DATA_MULTI_PUSH_TASK;
  • USER_DATA_ELEMENT_PUSH_TASK;
  • USER_DATA_ELEMENT_MULTI_PUSH_TASK;

從而進行後續對client的 push。

@Override
public void execute() {
    Map<String/*dataCenter*/, Datum> datumMap = getDatumsCache();

    if (datumMap != null && !datumMap.isEmpty()) {

        PushTaskClosure pushTaskClosure = getTaskClosure(datumMap);

        for (ScopeEnum scopeEnum : ScopeEnum.values()) {
            Map<InetSocketAddress, Map<String, Subscriber>> map = getCache(fetchDataInfoId,
                scopeEnum);
            if (map != null && !map.isEmpty()) {
                for (Entry<InetSocketAddress, Map<String, Subscriber>> entry : map.entrySet()) {
                    Map<String, Subscriber> subscriberMap = entry.getValue();
                    if (subscriberMap != null && !subscriberMap.isEmpty()) {
                        List<String> subscriberRegisterIdList = new ArrayList<>(
                            subscriberMap.keySet());

                        //select one row decide common info
                        Subscriber subscriber = subscriberMap.values().iterator().next();

                        //remove stopPush subscriber avoid push duplicate
                        evictReSubscribers(subscriberMap.values());

                        fireReceivedDataMultiPushTask(datumMap, subscriberRegisterIdList,
                            scopeEnum, subscriber, subscriberMap, pushTaskClosure);
                    }
                }
            }
        }

        pushTaskClosure.start();
    } 
}

以 RECEIVED_DATA_MULTI_PUSH_TASK 為例,我們的架構流程圖更改如下:

+-------------------------------------------------------------------------------------------------------------------+
|                        Session Server                                  +-------------------------------------+    |
|                                                                        |  ReceivedDataMultiPushTaskListener  |    |
| +-----------------------------------------------------------+          +------+------------------------------+    |
| |                  SessionCacheService                      |                 ^                                   |
| |                                                           |                 |  RECEIVED_DATA_MULTI_PUSH_TASK    |
| | +-------------------------------------------------------+ |                 |                                   |
| | |                                                       | |             +---+------------------------+          |
| | |    LoadingCache<Key, Value>  <----------+             | |             |  DataChangeFetchCloudTask  |          |
| | |            +                            |             | |             +---+------------------------+          |
| | |            |  expireAfterWrite          | invalidate  | |                 ^                                   |
| | |            |                            |             | |                 |                                   |
| | |            v                            |             | |                 |                                   |
| | |     DatumCacheGenerator                 |             | |           +-----+----------------------------+      |
| | |            +                            |             | |           | DataChangeFetchCloudTaskListener |      |
| | +-------------------------------------------------------+ |           +-----+----------------------------+      |
| +-----------------------------------------------------------+                 ^                                   |
|                |                            |                                 |  DATA_CHANGE_FETCH_CLOUD_TASK     |
|                v                            |                                 |                                   |
|       +--------+------------+     +---------+----------------+       +--------+--------------------------------+  |
|       | DataNodeServiceImpl |     | DataChangeRequestHandler +-----> | DefaultDataChangeRequestHandlerStrategy |  |
|       +--------+------------+     +---------+----------------+       +-----------------------------------------+  |
|                |                            ^                                                                     |
+-------------------------------------------------------------------------------------------------------------------+
                 |                            ^
  GetDataRequest |                            | DataChangeRequest
                 |                            |
+-------------------------------------------------------------------------------------------------------------------+
                 |                            ^
                 | Pull                       |  Push
                 v                            |
               +-+----------------------------+-+
               |           Data Server          |
               +--------------------------------+

手機上如下:

0x05 總結

本文講解了螞蟻金服在維持資料一致性上採用的經典的推拉模型,以 Session Server和 Data Server 之間維護資料一致性 為例。其大致邏輯如下:

SOFARegistry 中採用了 LoadingCache 的資料結構來在 SessionServer 中快取從 DataServer 中同步來的資料。

  • 拉模型
    • 每個 cache 中的 entry 都有過期時間,在拉取資料的時候可以設定過期時間(預設是 30s);
    • 這個過期時間使得 cache 定期去 DataServer 查詢當前 session 所有 sub 的 dataInfoId,對比如果 session 記錄的最近推送version(見com.alipay.sofa.registry.server.session.store.SessionInterests#interestVersions )比 DataServer 小,說明需要推送;
    • 然後 SessionServer 主動從 DataServer 獲取該 dataInfoId 的資料(此時會快取到 cache 裡),推送給 client;
    • 這個“拉”的邏輯,主要是對“推”的一個補充,若在“推”的過程有錯漏的情況可以在這個時候及時彌補
  • 推模型
    • 當 DataServer 中有資料更新時,也會主動向 SessionServer 發請求使對應 cache entry 失效;
    • 當SessionServer 檢查確認需要更新(對比 version) 之後,主動向 DataServer 獲取資料;
    • SessionServer去更新失效 entry。

大家在日常開發中,可以借鑑。

0xFF 參考

Guava LoadingCache詳解及工具類

Google Guava Cache 全解析

螞蟻金服服務註冊中心資料一致性方案分析 | SOFARegistry 解析

相關文章