[从源码学设计]蚂蚁金服SOFARegistry之推拉模型 [从源码学设计]蚂蚁金服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。

下面是两种场景的数据推送对比图。

[从源码学设计]蚂蚁金服SOFARegistry之推拉模型
[从源码学设计]蚂蚁金服SOFARegistry之推拉模型

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          |
                +--------------------------------+

手机上如下:

[从源码学设计]蚂蚁金服SOFARegistry之推拉模型
[从源码学设计]蚂蚁金服SOFARegistry之推拉模型

让我们在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          |
               +--------------------------------+

手机上如下:

[从源码学设计]蚂蚁金服SOFARegistry之推拉模型
[从源码学设计]蚂蚁金服SOFARegistry之推拉模型

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 解析