1. 程式人生 > >dubbo原始碼分析19 -- 服務治理

dubbo原始碼分析19 -- 服務治理

在之前的 dubbo 原始碼分析中我們分析了 dubbo 的服務暴露。provider 把需要暴露的服務地址資訊註冊到註冊中心(比如:zookeeper),然後把通過 java nio 框架 netty 以 socket 的方式把遠端服務暴露給 consumer 呼叫,並且訂閱註解中心,當註冊中心發生變化的時候 Inovke 呼叫就會改變。當 consumer 需要引用服務的時候通過 javassist 建立代理物件,獲取到代理物件 InvokerInvocationHandler,而它組合了一個 MockClusterInvoker。dubbo 通過這個物件進行服務治理,也就是之前分析的叢集容錯原始碼的分析。我們再來看一下叢集容錯的架構圖:

cluster.jpg

dubbo 不僅提供了 dubbo monitor 來監控服務指數,還提供了一個管理控制檯用於服務的治理。

1、dubbo admin 安裝

安裝

wget https://archive.apache.org/dist/tomcat/tomcat-6/v6.0.35/bin/apache-tomcat-6.0.35.tar.gz
tar zxvf apache-tomcat-6.0.35.tar.gz
cd apache-tomcat-6.0.35
rm -rf webapps/ROOT

git clone https://github.com/dubbo/dubbo-ops.git /var/tmp/dubbo-ops
pushd /var/tmp/dubbo-ops mvn clean package popd unzip /var/tmp/dubbo-ops/dubbo-admin/target/dubbo-admin-2.0.0.war -d webapps/ROOT

配置 dubbo.properties 把 dubbo 註解中心地址配置成真正專案裡面的註冊中心

vi webapps/ROOT/WEB-INF/dubbo.properties

#dubbo.properties

dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.admin.root.password
=root dubbo.admin.guest.password=guest

運維:

# 啟動:
./bin/startup.sh
# 停止:
./bin/shutdown.sh

然後可以通過: http://127.0.0.1:8080/ 進行訪問。

2、dubbo admin

dubbo admin 主要包含以下幾個頁面。

搜尋頁面

當你需要管理 Dubbo 的服務時,首先要搜尋到這個服務,然後開啟它的管理頁面

dubbo-search.png

服務提供者頁面

dubbo-providers.png

服務消費者頁面

dubbo-consumers.png

服務應用頁面

dubbo-applications.png

新增路由規則頁面

dubbo-add-route.png

新增動態配置頁面

dubbo-add-config.png

通過這些頁面可以查詢所有的服務提供者、服務消費者、服務的應用以及動態的新增路由規則或者新增動態的配置。當然也包含服務的降級、服務訪問權重的調節以及負載均衡的調節。

3、服務治理原理

dubbo admin 使用 Spring MVC 來自頁面展示的。我們先來看一下dubbo admin 中的配置檔案 dubbo.properties裡面配置了 dubbo 的註冊中心地址以及 dubbo admin 的使用者名稱與密碼。

dubbo.properties

dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest

通過配置的暴露服務的註冊中心地址,就可以從註冊中心獲取提供者、消費者、路由資訊、權重等資訊。

下面我們再來看一下 dubbo-admin 裡面的配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/>
        <property name="ignoreResourceNotFound" value="true"/>
        <property name="locations">
            <list>
                <value>/WEB-INF/dubbo.properties</value>
                <value>file://${user.home}/dubbo.properties</value>
            </list>
        </property>
    </bean>

    <dubbo:application name="dubbo-admin"/>

    <dubbo:registry client="curator" address="${dubbo.registry.address}" check="false" file="false"/>

    <dubbo:reference id="registryService" interface="com.alibaba.dubbo.registry.RegistryService" check="false"/>

    <bean id="configService" class="com.alibaba.dubbo.governance.service.impl.ConfigServiceImpl"/>

    <bean id="consumerService" class="com.alibaba.dubbo.governance.service.impl.ConsumerServiceImpl"/>

    <bean id="overrideService" class="com.alibaba.dubbo.governance.service.impl.OverrideServiceImpl"/>

    <bean id="ownerService" class="com.alibaba.dubbo.governance.service.impl.OwnerServiceImpl"/>

    <bean id="providerService" class="com.alibaba.dubbo.governance.service.impl.ProviderServiceImpl"/>

    <bean id="routeService" class="com.alibaba.dubbo.governance.service.impl.RouteServiceImpl"/>

    <bean id="userService" class="com.alibaba.dubbo.governance.service.impl.UserServiceImpl">
        <property name="rootPassword" value="${dubbo.admin.root.password}"/>
        <property name="guestPassword" value="${dubbo.admin.guest.password}"/>
    </bean>

    <bean id="governanceCache" class="com.alibaba.dubbo.governance.sync.RegistryServerSync"/>

</beans>

3.1 RegistryService

這個其實就是 dubbo admin 進行服務治理的物件,當在頁面更新了服務資訊的時候後臺會通過這個物件進行服務資訊的更新:

# 取消註冊舊的服務資訊
registryService.unregister(oldOverride);
# 註冊新的服務資訊
registryService.register(newOverride);

在這裡它會引用 dubbo.properties 裡面配置的註冊中心,然後引用 RegistryService 這個服務。這個是不是和 consumer 引用遠端服務的配置是一樣的。但是我們可以使用 Zookeeper 資料檢視工具 ZooInspector 檢視 zookeeper 節點上面的資料。

zookeeper-dubbo.png

可以看到在 zookeeper 裡面並沒有暴露遠端服務 RegistryService 。然後我們來看打斷點的跟蹤到 RegistryProtocol#refer的時候:

    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
        Registry registry = registryFactory.getRegistry(url);
        if (RegistryService.class.equals(type)) {
            return proxyFactory.getInvoker((T) registry, type, url);
        }

        // group="a,b" or group="*"
        Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
        String group = qs.get(Constants.GROUP_KEY);
        if (group != null && group.length() > 0) {
            if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1
                    || "*".equals(group)) {
                return doRefer(getMergeableCluster(), registry, type, url);
            }
        }
        return doRefer(cluster, registry, type, url);
    }

發現 dubbo admin 再進行服務引用的時候會先根據配置 URL 獲取一個 Registry 而 Registry 又是繼承於 RegistryService 介面。然後再選擇遠端服務之前會判斷這個服務是不是 RegistryService 。如果是就會根據已經獲取到的 Registry 建立一個本地 Invoke,然後由這個本地 Invoke 建立 ZookeeperRegistry 的代理物件。(這個比較坑,我糾結了很久才發現 :( )。

3.2 RegistryServerSync

這個物件實現了 Spring 框架的 InitializingBean,所以在這個 bean 初始化的時候就會訂閱以下 URL:

    private static final URL SUBSCRIBE = new URL(Constants.ADMIN_PROTOCOL, NetUtils.getLocalHost(), 0, "",
            Constants.INTERFACE_KEY, Constants.ANY_VALUE,
            Constants.GROUP_KEY, Constants.ANY_VALUE,
            Constants.VERSION_KEY, Constants.ANY_VALUE,
            Constants.CLASSIFIER_KEY, Constants.ANY_VALUE,
            Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY + ","
            + Constants.CONSUMERS_CATEGORY + ","
            + Constants.ROUTERS_CATEGORY + ","
            + Constants.CONFIGURATORS_CATEGORY,
            Constants.ENABLED_KEY, Constants.ANY_VALUE,
            Constants.CHECK_KEY, String.valueOf(false));

因為它的 interface 設定的是 Constants.ANY_VALUE 也就是 * ,所以會訂閱所有的服務。具體 zookeeper 註冊中心的訂閱服務可以參考 ZookeeperRegistry#doSubscribe 方法。訂閱了所以服務並且會獲取到所有服務的服務資訊快取到 RegistryServerSync#registryCache 屬性中。RegistryServerSync 還實現了NotifyListener,所以在註冊中心訂閱的時候還把它本身傳進進去了。當註冊中心的所有服務資訊發生變更的時候就會呼叫 RegistryServerSync#notify 更新RegistryServerSync#registryCache 快取資訊。這樣就可以對服務進行治理。

3.3 原理分析

dubbo admin 通過 Spring mvc 來展示註冊中心儲存的服務地址資訊:提供者、消費者、路由資訊、權重等資訊。通過 com.alibaba.dubboadmin.web.mvc.BaseController 的繼承類來修改註冊中心中的服務資訊:

controller.png

這些 controller 主要是通過 AbstractService 的繼承類來修改註冊中心裡面的註冊資訊。

public class AbstractService {

    protected static final Logger logger = LoggerFactory.getLogger(AbstractService.class);
    @Autowired
    protected RegistryService registryService;
    @Autowired
    private RegistryServerSync sync;

    public ConcurrentMap<String, ConcurrentMap<String, Map<Long, URL>>> getRegistryCache() {
        return sync.getRegistryCache();
    }

}

它有一個 RegistryService 是 ZookeeperRegistry 的代理物件,而且還有 RegistryServerSync 這個註冊中心的服務資訊快取(當服務地址資訊發生變更時還可以動態更新快取)。這樣就起到了服務治理的效果。以下都是可以動態修改資訊的實現類:

service.png

4、服務治理應用

通過 dubbo admin 頁面不僅僅可以展示服務提供者、消費者、路由資訊、權重等資訊。還可以修改服務提供者,訊息者的屬性來達到服務治理的目的。下面我們就來看一下有哪一些應用:

4.1 服務降級

可以通過dubbo admin 來實現服務降級功能,服務降級其實是臨時遮蔽某個出錯的非關鍵服務,並定義降級後的返回策略。有兩種方式:

mock=force:return+null 表示消費方對該服務的方法呼叫都直接返回 null 值,
不發起遠端呼叫。用來遮蔽不重要服務不可用時對呼叫方的影響。

還可以改為 mock=fail:return+null 表示消費方對該服務的方法呼叫在失敗後,再返回 null 值,
不拋異常。用來容忍不重要服務不穩定時對呼叫方的影響。

它的實現其實就是在叢集呼叫的入口: MockClusterInvoker#invoke

    public Result invoke(Invocation invocation) throws RpcException {
        Result result = null;

        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
        if (value.length() == 0 || value.equalsIgnoreCase("false")) {
            //no mock
            result = this.invoker.invoke(invocation);
        } else if (value.startsWith("force")) {
            if (logger.isWarnEnabled()) {
                logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
            }
            //force:direct mock
            result = doMockInvoke(invocation, null);
        } else {
            //fail-mock
            try {
                result = this.invoker.invoke(invocation);
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                } else {
                    if (logger.isWarnEnabled()) {
                        logger.info("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);
                    }
                    result = doMockInvoke(invocation, e);
                }
            }
        }
        return result;
    }

這段程式碼的邏輯分為 3 種情況:

  • no mock:正常情況,從註冊中心經過叢集、目錄服務、路由服務、負載均衡選擇一個合適的 Invoke 來進行呼叫。
  • force:direct mock:遮蔽,它不進行遠端呼叫,直接返回一個之前設定的值.
  • fail-mock:容錯,容錯的其實就是呼叫失敗後,返回一個設定的值

4.2 灰度釋出

灰度釋出(又名金絲雀釋出)是指在黑與白之間,能夠平滑過渡的一種釋出方式。在其上可以進行A/B testing,即讓一部分使用者繼續用產品特性A,一部分使用者開始用產品特性B,如果使用者對B沒有什麼反對意見,那麼逐步擴大範圍,把所有使用者都遷移到B上面來。灰度釋出可以保證整體系統的穩定,在初始灰度的時候就可以發現、調整問題,以保證其影響度。

比如我們需要在兩臺機器(192.168.100.38、192.168.48.32)上暴露服務,可以通過以下方式來進行灰度釋出:

  • 釋出192.168.48.32,切斷192.168.48.32訪問流量,然後進行服務的釋出。
  • 192.168.48.32釋出成功後,恢復 192.168.48.32的流量,
  • 切斷192.168.100.38,繼續釋出 192.168.100.38

參考文章: