7.原始碼分析---SOFARPC是如何實現故障剔除的?

luozhiyun發表於2019-08-07

我在服務端引用那篇文章裡面分析到,服務端在引用的時候會去獲取服務端可用的服務,並進行心跳,維護一個可用的集合。

所以我們從客戶端初始化這部分說起。

服務連線的維護

客戶端初始化的時候會呼叫cluster#init方法,這裡的cluster是繼承了AbstractCLuster抽象類,呼叫的是抽象類裡面的init方法。

public synchronized void init() {
    if (initialized) { // 已初始化
        return;
    }
    // 構造Router鏈
    routerChain = RouterChain.buildConsumerChain(consumerBootstrap);
    // 負載均衡策略 考慮是否可動態替換?
    loadBalancer = LoadBalancerFactory.getLoadBalancer(consumerBootstrap);
    // 地址管理器
    addressHolder = AddressHolderFactory.getAddressHolder(consumerBootstrap);
    // 連線管理器
    connectionHolder = ConnectionHolderFactory.getConnectionHolder(consumerBootstrap);
    // 構造Filter鏈,最底層是呼叫過濾器
    this.filterChain = FilterChain.buildConsumerChain(this.consumerConfig,
        new ConsumerInvoker(consumerBootstrap));

    if (consumerConfig.isLazy()) { // 延遲連線
        if (LOGGER.isInfoEnabled(consumerConfig.getAppName())) {
            LOGGER.infoWithApp(consumerConfig.getAppName(), "Connection will be initialized when first invoke.");
        }
    }

    // 啟動重連執行緒
    connectionHolder.init();
    try {
        // 得到服務端列表
        List<ProviderGroup> all = consumerBootstrap.subscribe();
        if (CommonUtils.isNotEmpty(all)) {
            // 初始化服務端連線(建立長連線)
            updateAllProviders(all);
        }
    } catch (SofaRpcRuntimeException e) {
        throw e;
    } catch (Throwable e) {
        throw new SofaRpcRuntimeException("Init provider's transport error!", e);
    }

    // 啟動成功
    initialized = true;

    // 如果check=true表示強依賴
    if (consumerConfig.isCheck() && !isAvailable()) {
        throw new SofaRpcRuntimeException("The consumer is depend on alive provider " +
            "and there is no alive provider, you can ignore it " +
            "by ConsumerConfig.setCheck(boolean) (default is false)");
    }
}

這上面在服務連線的維護上面主要分為三步:

  1. 設定心跳執行緒,每10秒進行一次心跳
  2. 獲取服務端列表
  3. 初始化服務端連線

1.SOFARPC的心跳執行緒

AllConnectConnectionHolder#init

這裡connectionHolder是AllConnectConnectionHolder的實現類,我們進入到這個類裡面看。這裡面實際上實現了SOFARPC的心跳檢測。


/**
 * 重連執行緒
 */
private volatile ScheduledService reconThread;

public void init() {
    //如果reconThread沒有初始化過,呼叫startReconnectThread進行初始化
    if (reconThread == null) {
        startReconnectThread();
    }
}

protected void startReconnectThread() {
    final String interfaceId = consumerConfig.getInterfaceId();
    // 啟動執行緒池
    // 預設每隔10秒重連
    int reconnect = consumerConfig.getReconnectPeriod();
    if (reconnect > 0) {
        reconnect = Math.max(reconnect, 2000); // 最小2000
        reconThread = new ScheduledService("CLI-RC-" + interfaceId, ScheduledService.MODE_FIXEDDELAY, new
            Runnable() {
                @Override
                public void run() {
                    try {
                        doReconnect();
                    } catch (Throwable e) {
                        LOGGER.errorWithApp(consumerConfig.getAppName(),
                            "Exception when retry connect to provider", e);
                    }
                }
            }, reconnect, reconnect, TimeUnit.MILLISECONDS).start();
    }
}

在startReconnectThread方法中,客戶端會呼叫reconnectPeriod變數,如果沒有設定則為10秒,如果設定小於10秒則取2秒。也就是說客戶端開啟的心跳是預設10秒一次,最快也是隻能2秒一次。
然後建立了一個ScheduledService例項,並呼叫其start方法。

我們看一下ScheduledService類是怎麼樣的結構

ScheduledService

public class ScheduledService {

    private volatile ScheduledExecutorService scheduledExecutorService;
    
    public ScheduledService(String threadName,
                            int mode,
                            Runnable runnable,
                            long initialDelay,
                            long period,
                            TimeUnit unit) {
        this.threadName = threadName;
        this.runnable = runnable;
        this.initialDelay = initialDelay;
        this.period = period;
        this.unit = unit;
        this.mode = mode;
    }
    
    //開始執行定時任務
    public synchronized ScheduledService start() {
        if (started) {
            return this;
        }
        if (scheduledExecutorService == null) {
            scheduledExecutorService = new ScheduledThreadPoolExecutor(1,
                new NamedThreadFactory(threadName, true));
        }
        ScheduledFuture future = null;
        //傳進來的是MODE_FIXEDDELAY
        switch (mode) {
            case MODE_FIXEDRATE:
                future = scheduledExecutorService.scheduleAtFixedRate(runnable, initialDelay,
                    period,
                    unit);
                break;
            case MODE_FIXEDDELAY:
                //建立一個固定延遲的定時任務
                future = scheduledExecutorService.scheduleWithFixedDelay(runnable, initialDelay, period,
                    unit);
                break;
            default:
                break;
        }
        if (future != null) {
            this.future = future;
            // 快取一下
            SCHEDULED_SERVICE_MAP.put(this, System.currentTimeMillis());
            started = true;
        } else {
            started = false;
        }
        return this;
    }
}

ScheduledService的作用就是建立一個固定延遲的執行緒,以固定的時間定時執行一下任務。

然後會預設每10秒鐘執行一次AllConnectConnectionHolder的doReconnect方法。

AllConnectConnectionHolder#doReconnect


/**
 * 存活的客戶端列表(保持了長連線,且一切正常的)
 */
protected ConcurrentMap<ProviderInfo, ClientTransport> aliveConnections         = new ConcurrentHashMap<ProviderInfo, ClientTransport>();
/**
 * 失敗待重試的客戶端列表(連上後斷開的)
 */
protected ConcurrentMap<ProviderInfo, ClientTransport> retryConnections         = new ConcurrentHashMap<ProviderInfo, ClientTransport>();


private void doReconnect() {
    //獲取配置的介面
    String interfaceId = consumerConfig.getInterfaceId();
    //獲取應用名
    String appName = consumerConfig.getAppName();
    int thisTime = reconnectFlag.incrementAndGet();
    boolean print = thisTime % 6 == 0; //是否列印error,每6次列印一次
    // 可用的連線集合是否為空
    boolean isAliveEmptyFirst = isAvailableEmpty();
    // 檢查可用連線  todo subHealth
    for (Map.Entry<ProviderInfo, ClientTransport> alive : aliveConnections.entrySet()) {
        ClientTransport connection = alive.getValue();
        //如果該連線不可用,那麼就將該連線從可用連線集合裡剔除放入到重試集合裡面
        if (connection != null && !connection.isAvailable()) {
            aliveToRetry(alive.getKey(), connection);
        }
    }
    //遍歷所有待重試集合
    for (Map.Entry<ProviderInfo, ClientTransport> entry : getRetryConnections()
        .entrySet()) {
        ProviderInfo providerInfo = entry.getKey();
        int providerPeriodCoefficient = CommonUtils.parseNum((Integer)
            providerInfo.getDynamicAttr(ProviderInfoAttrs.ATTR_RC_PERIOD_COEFFICIENT), 1);
        if (thisTime % providerPeriodCoefficient != 0) {
            continue; // 如果命中重連週期,則進行重連
        }
        ClientTransport transport = entry.getValue();
        if (LOGGER.isDebugEnabled(appName)) {
            LOGGER.debugWithApp(appName, "Retry connect to {} provider:{} ...", interfaceId, providerInfo);
        }
        try {
            //重連
            transport.connect();
            //重連完檢查一下該連線是否可用
            if (doubleCheck(interfaceId, providerInfo, transport)) {
                providerInfo.setDynamicAttr(ProviderInfoAttrs.ATTR_RC_PERIOD_COEFFICIENT, 1);
                //如果該連線可用,則把該連線從重試集合裡移除,加入到可用集合裡
                retryToAlive(providerInfo, transport);
            }
        } catch (Exception e) {
            if (print) {
                if (LOGGER.isWarnEnabled(appName)) {
                    LOGGER.warnWithApp(appName, "Retry connect to {} provider:{} error ! The exception is " + e
                        .getMessage(), interfaceId, providerInfo);
                }
            } else {
                if (LOGGER.isDebugEnabled(appName)) {
                    LOGGER.debugWithApp(appName, "Retry connect to {} provider:{} error ! The exception is " + e
                        .getMessage(), interfaceId, providerInfo);
                }
            }
        }
    }
    if (isAliveEmptyFirst && !isAvailableEmpty()) { // 原來空,變成不空
        notifyStateChangeToAvailable();
    }
}

這個doReconnect方法裡面主要做了以下幾件事:

  1. 檢查可用連線集合,如果該連線不可用,那麼就將該連線從可用連線集合裡剔除放入到重試集合裡面。
  2. 遍歷所有待重試集合,如果該thisTime和providerPeriodCoefficient取模為零,那麼就進行重連。
  3. 設定監聽器。

這裡有個細節,在aliveToRetry方法裡面是加鎖的,儘管aliveConnections和retryConnections都是安全的集合,但是這裡有一個if判斷,這兩步操作並不是執行緒安全的。

protected void aliveToRetry(ProviderInfo providerInfo, ClientTransport transport) {
    providerLock.lock();
    try {
        //這裡兩步操作並不是原子性的,所以需要加鎖
        if (aliveConnections.remove(providerInfo) != null) {
            retryConnections.put(providerInfo, transport);
        }
    } finally {
        providerLock.unlock();
    }
}

由於我們這裡並不分析網路是怎麼傳輸和連線的,所以暫時不分析transport#connect,大家只要知道這裡是保持一個長連線的就可以了。

接下來我們再看一下doubleCheck方法:

protected boolean doubleCheck(String interfaceId, ProviderInfo providerInfo, ClientTransport transport) {
    if (transport.isAvailable()) {
        try { // 睡一下下 防止被連上又被服務端踢下線
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // ignore
        }
        if (transport.isAvailable()) { // double check
            return true;
        } else { // 可能在黑名單裡,剛連上就斷開了
            if (LOGGER.isWarnEnabled(consumerConfig.getAppName())) {
                LOGGER.warnWithApp(consumerConfig.getAppName(),
                    "Connection has been closed after connected (in last 100ms)!" +
                        " Maybe connectionNum of provider has been reached limit," +
                        " or your host is in the blacklist of provider {}/{}",
                    interfaceId, transport.getConfig().getProviderInfo());
            }
            providerInfo.setDynamicAttr(ProviderInfoAttrs.ATTR_RC_PERIOD_COEFFICIENT, 5);
            return false;
        }
    } else {
        return false;
    }
}

這裡面主要是檢查一下連線的穩定性,如果一開始連線成功,在100ms內又斷開連線,那麼就打出警告日誌,當看到這個日誌在後臺的時候需要我們檢視一下網路連線的情況。

然後再把reconnectCoefficient屬性設定為5,當thisTime與providerPeriodCoefficient取模為0的時候再次嘗試連線,其中如果按預設設定的話,需要50秒才會進行重連。

2. 獲取服務列表

呼叫consumerBootstrap#subscribe方法進行獲取服務列表,會進入到抽象類DefaultConsumerBootstrap的subscribe方法中。

DefaultConsumerBootstrap#subscribe

public List<ProviderGroup> subscribe() {
    List<ProviderGroup> result = null;
    String directUrl = consumerConfig.getDirectUrl();
    if (StringUtils.isNotEmpty(directUrl)) {
        // 如果走直連
        result = subscribeFromDirectUrl(directUrl);
    } else {
        // 沒有配置url直連
        List<RegistryConfig> registryConfigs = consumerConfig.getRegistry();
        if (CommonUtils.isNotEmpty(registryConfigs)) {
            // 從多個註冊中心訂閱服務列表
            result = subscribeFromRegistries();
        }
    }
    return result;
}

這裡分成兩步:

  1. 如果在客戶端設定了直連地址的話則呼叫subscribeFromDirectUrl方法。
  2. 如果沒有配置直接地址則獲取註冊中心後呼叫subscribeFromRegistries方法。

DefaultConsumerBootstrap#subscribeFromDirectUrl

這個方法裡面主要是將直連地址用“;”拆分,然後封裝成provider放入直連分組集合中。

protected List<ProviderGroup> subscribeFromDirectUrl(String directUrl) {
    List<ProviderGroup> result = new ArrayList<ProviderGroup>();
    List<ProviderInfo> tmpProviderInfoList = new ArrayList<ProviderInfo>();
    //拆分url,多個url可以用“;”分割
    String[] providerStrs = StringUtils.splitWithCommaOrSemicolon(directUrl);
    for (String providerStr : providerStrs) {
        ProviderInfo providerInfo = convertToProviderInfo(providerStr);
        if (providerInfo.getStaticAttr(ProviderInfoAttrs.ATTR_SOURCE) == null) {
            providerInfo.setStaticAttr(ProviderInfoAttrs.ATTR_SOURCE, "direct");
        }
        tmpProviderInfoList.add(providerInfo);
    }
    //加入直連分組
    result.add(new ProviderGroup(RpcConstants.ADDRESS_DIRECT_GROUP, tmpProviderInfoList));
    return result;
}

DefaultConsumerBootstrap#subscribeFromRegistries

protected List<ProviderGroup> subscribeFromRegistries() {
    List<ProviderGroup> result = new ArrayList<ProviderGroup>();
    List<RegistryConfig> registryConfigs = consumerConfig.getRegistry();
    //沒有配置註冊中心,直接返回
    if (CommonUtils.isEmpty(registryConfigs)) {
        return result;
    }
    // 是否等待結果
    int addressWaitTime = consumerConfig.getAddressWait();
    int maxAddressWaitTime = SofaConfigs.getIntegerValue(consumerConfig.getAppName(),
        SofaOptions.CONFIG_MAX_ADDRESS_WAIT_TIME, SofaOptions.MAX_ADDRESS_WAIT_TIME);
    addressWaitTime = addressWaitTime < 0 ? maxAddressWaitTime : Math.min(addressWaitTime, maxAddressWaitTime);

    ProviderInfoListener listener = consumerConfig.getProviderInfoListener();
    //設定CountDownLatch用來等待
    respondRegistries = addressWaitTime == 0 ? null : new CountDownLatch(registryConfigs.size());

    // 從註冊中心訂閱 {groupName: ProviderGroup}
    Map<String, ProviderGroup> tmpProviderInfoList = new HashMap<String, ProviderGroup>();
    for (RegistryConfig registryConfig : registryConfigs) {
        Registry registry = RegistryFactory.getRegistry(registryConfig);
        registry.init();
        registry.start();

        try {
            List<ProviderGroup> current;
            try {
                if (respondRegistries != null) {
                    consumerConfig.setProviderInfoListener(new WrapperClusterProviderInfoListener(listener,
                        respondRegistries));
                }
                current = registry.subscribe(consumerConfig);
            } finally {
                if (respondRegistries != null) {
                    consumerConfig.setProviderInfoListener(listener);
                }
            }
            if (current == null) {
                continue; // 未同步返回結果
            } else {
                if (respondRegistries != null) {
                    respondRegistries.countDown();
                }
            }
            for (ProviderGroup group : current) { //  當前註冊中心的
                String groupName = group.getName();
                if (!group.isEmpty()) {
                    ProviderGroup oldGroup = tmpProviderInfoList.get(groupName);
                    if (oldGroup != null) {
                        oldGroup.addAll(group.getProviderInfos());
                    } else {
                        tmpProviderInfoList.put(groupName, group);
                    }
                }
            }
        } catch (SofaRpcRuntimeException e) {
            throw e;
        } catch (Throwable e) {
            String appName = consumerConfig.getAppName();
            if (LOGGER.isWarnEnabled(appName)) {
                LOGGER.warnWithApp(appName,
                    "Catch exception when subscribe from registry: " + registryConfig.getId()
                        + ", but you can ignore if it's called by JVM shutdown hook", e);
            }
        }
    }
    if (respondRegistries != null) {
        try {
            respondRegistries.await(addressWaitTime, TimeUnit.MILLISECONDS);
        } catch (Exception ignore) { // NOPMD
        }
    }
    return new ArrayList<ProviderGroup>(tmpProviderInfoList.values());
}

這個這麼長的方法實際上做了那麼幾件事:

  1. 遍歷註冊中心
  2. 初始化註冊中心,然後訂閱註冊中心,以非同步的方式拉去provider
  3. 如果設定了等待,那麼就等待一段時間後返回

3. 初始化服務端連線

如果在呼叫consumerBootstrap#subscribe()後不是非同步獲取,返回的就不是null,那麼就會進入到updateAllProviders,所以我們來看一下這個方法裡面做了什麼。

DefaultConsumerBootstrap#updateAllProviders

public void updateAllProviders(List<ProviderGroup> providerGroups) {
    List<ProviderGroup> oldProviderGroups = new ArrayList<ProviderGroup>(addressHolder.getProviderGroups());
    int count = 0;
    if (providerGroups != null) {
        for (ProviderGroup providerGroup : providerGroups) {
            //校驗檢查providerGroup裡面的元素是不是為空
            //消費者的配置的protocol是不是和provider 的protocol相同
            checkProviderInfo(providerGroup);
            count += providerGroup.size();
        }
    }
    //走到這裡說明沒有provider
    if (count == 0) {
        Collection<ProviderInfo> currentProviderList = currentProviderList();
        addressHolder.updateAllProviders(providerGroups);
        if (CommonUtils.isNotEmpty(currentProviderList)) {
            if (LOGGER.isWarnEnabled(consumerConfig.getAppName())) {
                LOGGER.warnWithApp(consumerConfig.getAppName(), "Provider list is emptied, may be all " +
                    "providers has been closed, or this consumer has been add to blacklist");
                closeTransports();
            }
        }
    } else {
        addressHolder.updateAllProviders(providerGroups);
        connectionHolder.updateAllProviders(providerGroups);
    }
    if (EventBus.isEnable(ProviderInfoUpdateAllEvent.class)) {
        ProviderInfoUpdateAllEvent event = new ProviderInfoUpdateAllEvent(consumerConfig, oldProviderGroups,
            providerGroups);
        EventBus.post(event);
    }
}

其實這個方法裡面就做了一件事情,那就是把provider放入到addressHolder和connectionHolder中。

故障剔除

客戶端在引用的時候會呼叫FailoverCluster#doInvoke方法,然後呼叫父類的select進行路由和負載均衡選用合適的provider。

AbstractCluster#doInvoke

public SofaResponse doInvoke(SofaRequest request) throws SofaRpcException {
    String methodName = request.getMethodName();
    int retries = consumerConfig.getMethodRetries(methodName);
    int time = 0;
    SofaRpcException throwable = null;// 異常日誌
    List<ProviderInfo> invokedProviderInfos = new ArrayList<ProviderInfo>(retries + 1);
    do {
        //負載均衡
        ProviderInfo providerInfo = select(request, invokedProviderInfos);
        try {
            //呼叫過濾器鏈
            SofaResponse response = filterChain(providerInfo, request);
            if (response != null) {
                if (throwable != null) {
                    if (LOGGER.isWarnEnabled(consumerConfig.getAppName())) {
                        LOGGER.warnWithApp(consumerConfig.getAppName(),
                            LogCodes.getLog(LogCodes.WARN_SUCCESS_BY_RETRY,
                                throwable.getClass() + ":" + throwable.getMessage(),
                                invokedProviderInfos));
                    }
                }
                return response;
            } else {
                throwable = new SofaRpcException(RpcErrorType.CLIENT_UNDECLARED_ERROR,
                    "Failed to call " + request.getInterfaceName() + "." + methodName
                        + " on remote server " + providerInfo + ", return null");
                time++;
            }
        } catch (SofaRpcException e) { // 服務端異常+ 超時異常 才發起rpc異常重試
            if (e.getErrorType() == RpcErrorType.SERVER_BUSY
                || e.getErrorType() == RpcErrorType.CLIENT_TIMEOUT) {
                throwable = e;
                time++;
            } else {
                throw e;
            }
        } catch (Exception e) { // 其它異常不重試
            throw new SofaRpcException(RpcErrorType.CLIENT_UNDECLARED_ERROR,
                "Failed to call " + request.getInterfaceName() + "." + request.getMethodName()
                    + " on remote server: " + providerInfo + ", cause by unknown exception: "
                    + e.getClass().getName() + ", message is: " + e.getMessage(), e);
        } finally {
            if (RpcInternalContext.isAttachmentEnable()) {
                RpcInternalContext.getContext().setAttachment(RpcConstants.INTERNAL_KEY_INVOKE_TIMES,
                    time + 1); // 重試次數
            }
        }
        invokedProviderInfos.add(providerInfo);
    } while (time <= retries);

    throw throwable;
}

這個方法需要注意的是,一開始invokedProviderInfos集合是空的,如果呼叫完後沒有返回response,而是丟擲異常了,那麼就會把這個丟擲異常的provider例項加入到invokedProviderInfos集合。這個集合會在select方法裡面用到。

AbstractCluster#select

客戶端在引用服務端的時候會通過路由找到所有的provider,然後進行剔除。路由是在呼叫AbstractCluster#select的時候做的。所以我們先看看這個方法。

protected ProviderInfo select(SofaRequest message, List<ProviderInfo> invokedProviderInfos)
    throws SofaRpcException {
    // 粘滯連線,當前連線可用
    if (consumerConfig.isSticky()) {
        //這個變數會在下面的selectByProvider方法為其賦值
        if (lastProviderInfo != null) {
            ProviderInfo providerInfo = lastProviderInfo;
            ClientTransport lastTransport = connectionHolder.getAvailableClientTransport(providerInfo);
            if (lastTransport != null && lastTransport.isAvailable()) {
                checkAlias(providerInfo, message);
                return providerInfo;
            }
        }
    }
    // 原始服務列表資料 --> 路由結果
    List<ProviderInfo> providerInfos = routerChain.route(message, null);

    //儲存一下原始地址,為了列印
    List<ProviderInfo> orginalProviderInfos = new ArrayList<ProviderInfo>(providerInfos);

    if (CommonUtils.isEmpty(providerInfos)) {
        throw noAvailableProviderException(message.getTargetServiceUniqueName());
    }
    //invokedProviderInfos儲存的是重試的provider,說明該provider已經呼叫過,並且失敗了
    //所以在這裡排除
    if (CommonUtils.isNotEmpty(invokedProviderInfos) && providerInfos.size() > invokedProviderInfos.size()) { // 總數大於已呼叫數
        providerInfos.removeAll(invokedProviderInfos);// 已經呼叫異常的本次不再重試
    }

    String targetIP = null;
    ProviderInfo providerInfo;
    RpcInternalContext context = RpcInternalContext.peekContext();
    if (context != null) {
        targetIP = (String) RpcInternalContext.getContext().getAttachment(RpcConstants.HIDDEN_KEY_PINPOINT);
    }
    if (StringUtils.isNotBlank(targetIP)) {
        // 如果指定了呼叫地址
        providerInfo = selectPinpointProvider(targetIP, providerInfos);
        if (providerInfo == null) {
            // 指定的不存在
            throw unavailableProviderException(message.getTargetServiceUniqueName(), targetIP);
        }
        ClientTransport clientTransport = selectByProvider(message, providerInfo);
        if (clientTransport == null) {
            // 指定的不存在或已死,丟擲異常
            throw unavailableProviderException(message.getTargetServiceUniqueName(), targetIP);
        }
        return providerInfo;
    } else {
        do {
            // 再進行負載均衡篩選,預設使用RandomLoadBalancer
            providerInfo = loadBalancer.select(message, providerInfos);
            ClientTransport transport = selectByProvider(message, providerInfo);
            if (transport != null) {
                return providerInfo;
            }
            providerInfos.remove(providerInfo);
        } while (!providerInfos.isEmpty());
    }
    throw unavailableProviderException(message.getTargetServiceUniqueName(),
        convertProviders2Urls(orginalProviderInfos));
}

這個方法主要做了如下幾件事:

  1. 如果設定了粘滯連線,那麼會繼續呼叫上一次使用過的provider
  2. 呼叫router獲取原始服務列表資料
  3. 如果invokedProviderInfos不為空的話,原始服務列表裡面需要剔除掉這些provider
  4. 如果設定了直連,那麼呼叫selectPinpointProvider獲取選定的provider,不存在故障剔除
  5. 沒有設定直連,則迴圈呼叫篩選

路由篩選porvider

RouterChain#route

public List<ProviderInfo> route(SofaRequest request, List<ProviderInfo> providerInfos) {
    for (Router router : routers) {
        providerInfos = router.route(request, providerInfos);
    }
    return providerInfos;
}

//RegistryRouter#route
public List<ProviderInfo> route(SofaRequest request, List<ProviderInfo> providerInfos) {

    //has  address. FIXME
    if (CommonUtils.isNotEmpty(providerInfos)) {
        return providerInfos;
    }

    AddressHolder addressHolder = consumerBootstrap.getCluster().getAddressHolder();
    if (addressHolder != null) {
        List<ProviderInfo> current = addressHolder.getProviderInfos(RpcConstants.ADDRESS_DEFAULT_GROUP);
        if (providerInfos != null) {
            providerInfos.addAll(current);
        } else {
            providerInfos = current;
        }
    }
    recordRouterWay(RPC_REGISTRY_ROUTER);
    return providerInfos;
}

我們這裡考慮RegistryRouter進行路由選擇的情況。
RegistryRouter#route裡面首先獲取addressHolder,呼叫其實現類SingleGroupAddressHolder

SingleGroupAddressHolder#getProviderInfos


/**
 * 配置的直連地址列表
 */
protected ProviderGroup                    directUrlGroup;
/**
 * 註冊中心來的地址列表
 */
protected ProviderGroup                    registryGroup;

private ReentrantReadWriteLock             lock                = new ReentrantReadWriteLock();
private Lock                               rLock               = lock.readLock();
public List<ProviderInfo> getProviderInfos(String groupName) {
    rLock.lock();
    try {
        // 複製一份
        return new ArrayList<ProviderInfo>(getProviderGroup(groupName).getProviderInfos());
    } finally {
        rLock.unlock();
    }
}

public ProviderGroup getProviderGroup(String groupName) {
    rLock.lock();
    try {
        return RpcConstants.ADDRESS_DIRECT_GROUP.equals(groupName) ? directUrlGroup
            : registryGroup;
    } finally {
        rLock.unlock();
    }
}

這裡用的是讀寫鎖,也就是說,在讀的時候可以併發讀,但是不允許讀的時候有寫的操作。然後根據groupName獲取到相應的直連集合。

實現故障剔除,篩選合適的provider

do {
    // 再進行負載均衡篩選,預設使用RandomLoadBalancer
    providerInfo = loadBalancer.select(message, providerInfos);
    ClientTransport transport = selectByProvider(message, providerInfo);
    if (transport != null) {
        return providerInfo;
    }
    providerInfos.remove(providerInfo);
} while (!providerInfos.isEmpty());

這裡是真正實現了故障剔除的方法,負載均衡我已經在上一篇已經分析過了,這裡不再贅述,所以我們直接看到selectByProvider方法中

protected ClientTransport selectByProvider(SofaRequest message, ProviderInfo providerInfo) {
    ClientTransport transport = connectionHolder.getAvailableClientTransport(providerInfo);
    if (transport != null) {
        if (transport.isAvailable()) {
            lastProviderInfo = providerInfo;
            checkAlias(providerInfo, message); //檢查分組
            return transport;
        } else {
            connectionHolder.setUnavailable(providerInfo, transport);
        }
    }
    return null;
}

//AllConnectConnectionHolder#getAvailableClientTransport

public ClientTransport getAvailableClientTransport(ProviderInfo providerInfo) {
    // 先去存活列表
    ClientTransport transport = aliveConnections.get(providerInfo);
    if (transport != null) {
        return transport;
    }
    // 再去亞健康列表 這個列表暫時沒有實現的地方
    transport = subHealthConnections.get(providerInfo);
    if (transport != null) {
        return transport;
    }
    // 最後看看是否第一次呼叫未初始化
    transport = uninitializedConnections.get(providerInfo);
    if (transport != null) {
        // 未初始化則初始化,這裡是lazy為ture的情況,延遲初始化
        synchronized (this) {
            transport = uninitializedConnections.get(providerInfo);
            if (transport != null) {
                initClientTransport(consumerConfig.getInterfaceId(), providerInfo, transport);
                uninitializedConnections.remove(providerInfo);
            }
            return getAvailableClientTransport(providerInfo);
        }
    }
    return null;
}

當我們進入到getAvailableClientTransport這個方法的看到存貨列表和未初始化列表的時候有沒有似曾相識的感覺?沒錯,這幾個引數就是我們上面講到的客戶端會初始化一個心跳執行緒,在心跳執行緒裡面維護這幾個引數。

所以這個方法主要做了以下幾件事:

  1. 去存活列表裡面找transport
  2. 去亞健康列表裡面找transport,當然目前的版本並沒有維護亞健康列表,所以永遠找不到
  3. 如果設定了延遲載入,那麼會去uninitializedConnections裡面找到transport,然後再呼叫initClientTransport方法進行初始化
  4. 如果找不到那麼就返回null
  5. 如果返回null,那麼會回到上面的do-while迴圈進行再次的篩選

好了,那麼SOFARPC是如何實現故障剔除的就已經分析完了,如果這篇文章對你有所幫助,不妨點個贊,謝謝。

SOFARPC原始碼解析系列:

1. 原始碼分析---SOFARPC可擴充套件的機制SPI

2. 原始碼分析---SOFARPC客戶端服務引用

3. 原始碼分析---SOFARPC客戶端服務呼叫

4. 原始碼分析---SOFARPC服務端暴露

5.原始碼分析---SOFARPC呼叫服務

6.原始碼分析---和dubbo相比SOFARPC是如何實現負載均衡的?

相關文章