一種微服務引擎架構剖析及服務治理原理介紹

作者:呂濤,中國移動雲能力中心軟件研發工程師,專注於雲原生、微服務、算力網絡等領域。

摘要

微服務引擎(Micro Service Engine 後面簡稱 MSE)是面向業界主流開源微服務生態的一站式微服務治理平臺,兼容 Spring Cloud、Dubbo 微服務框架,提供高可用、免運維的服務註冊中心(支持 Eureka/ Nacos/ZooKeeper)、配置中心(支持 Apollo)和監控中心(支持 Skywalking),實現對微服務的治理和監控。基於雲原生環境下,微服務引擎又是如何一種架構?微服務引擎產品中 Spring Cloud 及 Dubbo 相關服務治理是如何實現的?Spring Cloud 框架下如何實現參數的動態配置呢?

前言

在雲原生主流發展的環境下,基於需求而來,一種應雲而生的微服務引擎架構,顯然是脫穎而出,得到業界的普遍關注。服務治理,對於 Srping Cloud 類型的服務和 Dubbo 類型的服務,本文也給出了不同的設計方案。而針對常用的 Srping Cloud 類型的服務,做了詳細的服務治理剖析,以及通過具體的案例解析相應的治理過程。

MSE 部署組網架構實現

產品流量入口在管理域 DMZ 區,此區域主要用於部署高可用組件、流量入口、前端靜態資源,通過 Keepalived + Nginx 來實現服務的高可用和負載均衡。

產品分爲訂購頁 & 控制檯、應用端、管理端 3 個子系統,部署在管理域 CORE 區,其中訂購頁 & 控制檯、應用端面向 BC-OP 用戶,訪問請求經 OP 網關統一轉發;管理端面向自建內部用戶體系,不對公網開放。具體的部署組網架構圖如下:

MSE 部署架構圖

Dubbo 服務治理

Dubbo 服務的治理,社區提供的治理方案是自由編輯 yaml 格式的參數配置,然後將配置信息寫入到註冊中心 zookeeper 的配置節點上。微服務引擎在設計時,首選推薦的圖形化方式引導選擇或輸入的方式,同時也保留了自由編輯 yaml 格式的參數配置的方式。

Dubbo 服務治理的配置都存儲在 /dubbo/config 節點,具體節點結構圖如下:

Dubbo 節點結構圖

通過可視化配置可以實現 Dubbo 服務的負載均衡、條件路由、標籤路由、黑白名單策略。

    1. 負載均衡:在集羣負載均衡時,Dubbo 提供了多種均衡策略,缺省爲隨機調用。隨機,輪詢,最少活躍調用數。
    1. 條件路由:以服務或消費者應用爲粒度配置路由規則。例如:設置應用名爲 app1 的消費者只能調用端口爲 20880 的服務實例,設置 samples.governance.api.DemoService 的 sayHello 方法只能調用所有端口爲 20880 的服務實例。
    1. 標籤路由:通過將某一個或多個服務的提供者劃分到同一個分組,約束流量只在指定分組中流轉,從而實現流量隔離的目的,可以作爲藍綠髮布、灰度發佈等場景的能力基礎。
    1. 黑白名單:是條件路由的一部分,規則存儲和條件路由放在一起,爲了方便配置所以單獨拿出來,可以對某一個服務,指定黑名單和白名單。

Srping Cloud 服務治理

通過可視化配置可以實現 Spring Cloud 服務的負載均衡、限流、熔斷、降級、超時策略以及參數的動態化配置,通過 BOMS 管理平臺,負責接收用戶請求,將用戶數據持久化到存儲介質;存儲介質用來存儲用戶操作數據,例如項目負載均衡策略;在使用上述治理功能時,依賴於 apollo 的參數動態生效功能以及自研的 SDK 負責監聽存儲介質,動態更新負載均衡策略,存儲介質基於 Apollo 根據存儲介質中訪問控制策略,增加訪問控制攔截負責監聽存儲介質,動態更新是否啓用容錯重試機制,以及動態更新重試次數等功能;後面章節會從 apollo 的參數動態生效以及 SDK 兩方面詳細介紹微服務引擎時如何納管 springcloud 服務實現流量治理過程。

如果用戶的微服務需要通過 mse 納管進行流量治理,首先需要在 mse 裏訂購註冊中心及配置中心 apollo 實例,然後對用戶的服務進行部分改造,引入 SDK 工具包,並且在服務的配置文件裏需要配置 apollo 的 meta 地址以及 apollo 的 namespaces: application,circuit-breaker,fault-tolerant,loadbalance,timeout,這五個 namespace 分別對應五種治理功能:參數動態配置、熔斷降級、容錯、負載均衡、超時策略。

下圖是 Spring Cloud 服務治理結構圖:

Spring Cloud 架構圖

    1. 負載均衡:基於當出現訪問量和流量較大,一臺服務器無法負載的情況下,我們可以通過設置負載均衡的方式將流量分發到多個服務器均衡處理,從而優化響應時長,防止服務器過載。該功能對使用 Ribbon 組件的服務進行參數設置,可以對調用不同的服務採取不同的負載均衡策略,熱生效。可以通過新增規則配置負載均衡策略,設置參數支持輪詢、隨機、權重輪詢等多種負載均衡策略。
    1. 應用限流:在分佈式系統中,如果客戶端依賴多個服務,在一次請求中,某一個服務出現異常,則整個請求會處理失敗。Netflix 的 Hystrix 組件可以將這些請求隔離,針對服務限流,防止任何單獨的服務耗盡資源。Hystrix 有兩種限流方式:線程池隔離、信號量隔離。線程池隔離可以設置線程池的大小,信號量隔離可以設置最大併發數。
    1. 服務熔斷:針對使用 Hystrix 組件的服務進行設置。在訪問量和流量過大時,可以強制性對一些不重要接口進行熔斷,系統調用該接口時,就會直接走降級方法,而不會實際去調用某個外部服務。強制開啓熔斷需要人工進行參與。非人工參與的熔斷,可以通過設置條件熔斷來實現,例如可以設置如果一個時間週期內 (10 秒) 內,當請求個數達到閾值 20 時情況下,如果請求錯誤率達到 50%,自動開啓熔斷 5 秒鐘。
    1. 服務降級:針對使用 Hystrix 組件的服務進行設置。可以預先設置後接口如果調用失敗後返回的預留信息。該功能一般配合熔斷功能一起使用。
    1. 超時策略:使用 Ribbon 組件的服務,可以通過設置 “連接超時時間”、“請求超時時間” 參數,從而控制調用外部服務多長時間不成功就判定爲調用失敗。
    1. 集羣容錯:使用 Ribbon 組件的服務,可以設置調用出錯自動重試的策略。例如,可以設置在同一臺實例最大重試次數,可以設置重試其他實例最大重試次數。

MSE 如何實現參數動態配置

mse 納管 springcloud 服務是強依賴於 apollo 配置中心的,是藉助於 apollo 強大的微服務配置管理功能,apollo 是攜程研發並開源的一款生產級的配置中心產品,它能夠集中管理應用在不同環境、不同集羣的配置,配置修改後能夠實時推送到應用端,並且具備規範的權限、流程治理等特性。

修改 Spring 配置文件,增加 Apollo 配置中心的相關參數。

#Apollo appId 根據服務名稱,服務名稱不可重複
app:
  id: ${spring.application.name}
apollo:
  #Apollo meta地址,如果跳過apollo meta server的話,不配置該屬性,但是需要在啓動腳本中添加以下參數
  #-Dapollo.configService=http://config-service-url:port(多個configserver地址之間用逗號隔開)
  meta: ${APOLLO_ADDR:http://166.8.66.8:8080,http://166.8.66.9:8080,http://166.8.66.10:8080}
  bootstrap:
    enabled: true
    #引入哪些namespace
    namespaces: application,circuit-breaker,fault-tolerant,loadbalance,timeout

在服務啓動類上增加註解 @EnableApolloConfig,啓用 Apollo 配置中心功能。

@SpringBootApplication
@EnableHystrix
@EnableApolloConfig
@EnableDiscoveryClient
@EnableFeignClients
@EnableSwagger2
public class ConsumerDemoHystrixApplication {

    @Bean
    public RestTemplate restTemplate() {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setConnectTimeout(30000);// 設置超時
        requestFactory.setReadTimeout(30000);
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setRequestFactory(requestFactory);
        return restTemplate;
    }

用戶的 springcloud 服務啓動時,在 ApplicationContextInitializer 初始化階段,會把配置文件中 application,circuit-breaker,fault-tolerant,loadbalance,timeout 這五個 namespace 進行遍歷,並讀取已經配置好的參數,加載到當前服務的 Environment 中,在執行每個 namespace 的 initializer 時,會調用 RemoteConfigRepository 中的方法,通過遠程 Repository,實現從 ConfigService 拉取配置,緩存配置,並通過實時機制、定時機制從 ConfigService 中獲取最新的配置參數。

public RemoteConfigRepository(String namespace) {
        this.m_namespace = namespace;
        this.m_configCache = new AtomicReference();
        this.m_configUtil = (ConfigUtil)ApolloInjector.getInstance(ConfigUtil.class);
        this.m_httpUtil = (HttpUtil)ApolloInjector.getInstance(HttpUtil.class);
        this.m_serviceLocator = (ConfigServiceLocator)ApolloInjector.getInstance(ConfigServiceLocator.class);
        this.remoteConfigLongPollService = (RemoteConfigLongPollService)ApolloInjector.getInstance(RemoteConfigLongPollService.class);
        this.m_longPollServiceDto = new AtomicReference();
        this.m_remoteMessages = new AtomicReference();
        this.m_loadConfigRateLimiter = RateLimiter.create((double)this.m_configUtil.getLoadConfigQPS());
        this.m_configNeedForceRefresh = new AtomicBoolean(true);
        this.m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(this.m_configUtil.getOnErrorRetryInterval(), this.m_configUtil.getOnErrorRetryInterval() * 8L);
        this.gson = new Gson();
        this.trySync();
        this.schedulePeriodicRefresh();
        this.scheduleLongPollingRefresh();
}

在 springcloud 的 ApplicationContextInitializer 執行完後,apollo 中的所有 namespace 配置都加載到了 Environment 中,並且每個 namespace 都起了兩個線程,一個定時獲取配置,一個實時獲取配置。當我們在 application 的 namespace 中對參數進行增刪改的時候,apollo 通過這兩個線程可以實時獲取到參數的修改,那麼又是怎樣通知 springcloud 服務參數修改的呢?

當我們修改了配置參數後,長輪詢線程會立刻獲取當前 namespace 下的所有配置參數,並調用 doLongPollingRefresh 進行後續的操作,首先從緩存中獲取修改前的所有配置的值,然後與當前長輪詢線程中的配置參數進行比較,如果有配置參數不同,則調用 fireRepositoryChange 進行處理,在 fireRepositoryChange 中會遍歷所有的 RepositoryChangeListener 監聽器進行配置修改操作,最終是調用 beanFactory.resolveEmbeddedValue (placeholder) 解析 @Value 的值,並通過 updateSpringValue 修改內存中的值。

public void onChange(ConfigChangeEvent changeEvent) {
        Set<String> keys = changeEvent.changedKeys();
        if (CollectionUtils.isEmpty(keys)) {
            return;
        }
        for (String key : keys) {
            Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
            if (targetValues == null || targetValues.isEmpty()) {
                continue;
            }
            for (SpringValue val : targetValues) {
                updateSpringValue(val);
            }
        }
    }

首先通過 event 拿到所有變更的 keys;然後遍歷 keys,通過 springValueRegistry.get (beanFactory, key) 拿到 SpringValue 集合對象,這個 springValueRegistry 是在 springcloud 啓動的時候,通過後置處理器 SpringValueProcessor 處理的所有 @Value 的值(apollo 不僅可以處理 @Value 值,還可以處理通過 @ApolloConfigChangeListener 註解的值);最後遍歷 SpringValue 集合,逐一通過反射改變內存中字段的值。

參數配置只需要動態修改 spring 中的 Environment 值就可以實現參數動態配置,但是像熔斷降級、容錯、負載均衡、超時策略等流量治理策略實現動態配置,不僅需要動態修改 Environment 值,還需要使實現熔斷降級、容錯、負載均衡、超時策略的 Ribbon、Feign、hystrix 機制動態更新。

MSE 使用 Hystrix 的方式

使用 @HystrixCommand 註解的方式

由於熔斷使用到了 hystrix 的一些配置作爲資源隔離,所以被接入平臺的服務在使用 hystrix 的方式上如果使用了 @HystrixCommand 的方式,需要注意以下幾點:

使用案例:

@HystrixCommand(commandKey = "/test_hystrix_command"groupKey = "hystrix-thread-pool",
            threadPoolKey = "hystrix-thread-pool"fallbackMethod = "fallback")

註解需要按照以下規則填寫以上的三個配置參數:

該參數表示該接口的 api 名稱,是某個方法 / 接口的標識

注意:爲了避免應用下的服務衆多,導致該標識不好識別接口的歸屬,這裏要做一個規劃,例如:當前服務名 #方法 / 接口名,僅供參考。

該參數表示這個接口 / 方法屬於哪個組,一般這裏填寫被調用的外部服務名(假設消費者服務調用了提供者服務,這裏填寫提供者服務的服務名,需要保證與 spring.application.name 的值相同)

該參數表示線程池的名稱,這裏要與 groupKey 的值保持一致

該參數該調用發生熔斷後的處理方法

使用 @FeignClient 的方式

@FeignClient 註解其實是在 @HystrixCommand 的基礎上做了一些封裝,這裏的 value 值會設置爲 @HystrixCommand 中的 groupKey 和 threadPoolKey 值,而 commandKey 會設置爲類名 #方法名(參數類型)

@Component
public class UserServiceFeignClientFallbackFactory implements FallbackFactory<UserServiceFeignClient> {
Logger logger = LoggerFactory.getLogger(UserServiceFeignClientFallbackFactory.class);

    @Override
    public UserServiceFeignClient create(Throwable cause) {
        return new UserServiceFeignClient() {
            @Override
            public String testAccessMethodGet() {
                cause.printStackTrace();
                return "我是服務降級方法";
            }

使用案例

@FeignClient(value = "${provider.service.name}"fallbackFactory =UserServiceFeignClientFallbackFactory.class)
public interface UserServiceFeignClient {

後文以超時策略爲例,闡述 MSE 是如何納管 springcloud 服務實現超時策略動態更新。

MSE 如何實現超時策略

要想使用 MSE 納管自己的服務實現超時策略,首先要對自己的 springcloud 服務進行稍許改造,添加 mse-sc-datasource-apollo 以及 mse-sc-feature-timeout 兩個自定義的 SDK,當項目啓動時,會進行一些初始化操作,我們知道 springcloud 中的超時策略時通過 Ribbon 實現的,因此我們的目標是修改超時參數,使超時參數的動態生效,並進行 Ribbon 的動態加載。

把 ApolloDataSource 放入 spring 的 IOC 容器裏。

@Configuration
@ConditionalOnClass
@ConditionalOnProperty(value={"spring.application.govern.datasource.type"}havingValue="Apollo"matchIfMissing=true)
public class ApolloAutoConfiguration
{
  @Bean
  @ConditionalOnClass(name={"com.ctrip.framework.apollo.build.ApolloInjector"})
  public ApolloDataSource apolloDataSource()
  {
    return new ApolloDataSource();
  }
}

在 ApolloDataSource 中,是通過 guava 的 Eventbus 監聽事件的發佈事件消息,當監聽到事件的發佈,根據不同 namespace 處理不同的數據治理服務,並初始化一個 apollo 的配置修改監聽器 ConfigChangeListener。

@Subscribe
  public void listenerConfig(DataSourceEvent dataSourceEvent)
  {
    DataEvent dataEvent = dataSourceEvent.getDataEvent();
final String namespace = dataEvent.getNamespace();
#處理不同的namespace,MSE一共可以處理五種默認的namespace
    if (!this.map.keySet().contains(namespace))
    {
      EventBus eventBus = new EventBus();
      this.map.put(namespace, eventBus);
      eventBus.register(dataSourceEvent.getConfigListener());
      final Config config = ConfigService.getConfig(namespace);
      
      List<RuleEvent> configs = loadConfig(config, config.getPropertyNames());
      ((EventBus)this.map.get(namespace)).post(new ConfigEvent(configs));
      ConfigChangeListener configChangeListener = new ConfigChangeListener()
      {
        public void onChange(ConfigChangeEvent changeEvent)
        {
          try
          {
            List<RuleEvent> result = ApolloDataSource.this.loadConfig(config, changeEvent.changedKeys());
            ((EventBus)ApolloDataSource.this.map.get(namespace)).post(new ConfigEvent(result));
          }
          catch (Exception e)
          {
            ApolloDataSource.this.logger.error("config load exception", e);
          }
        }
      };
      #增加一個apollo的ConfigChangeListener,處理參數的動態修改
      config.addChangeListener(configChangeListener);
    }
    else
    {
      ((EventBus)this.map.get(namespace)).register(dataSourceEvent.getConfigListener());
    }
  }

當執行 TimeoutListenerInitializer 的 Bean 時,

@Configuration
@ConditionalOnClass
public class TimeoutAutoConfiguration
{
  @Bean
  @ConditionalOnMissingBean
  public TimeoutFactory timeoutFactory(SpringClientFactory springClientFactory, ApplicationContext applicationContext)
  {
    return new TimeoutRibbonFactory(springClientFactory, applicationContext);
  }
  
  @Bean
  @ConditionalOnBean({TimeoutFactory.class})
  public TimeoutListenerInitializer timeoutListenerInitializer(TimeoutFactory timeoutFactory, Environment environment)
  {
    return new TimeoutListenerInitializer(timeoutFactory, environment);
  }
}

會發布超時策略的 DataSourceEvent 消息,被上述 ApolloDataSource 中的監聽器監聽到,並初始化一個 namespace 爲 timeout 的 ConfigChangeListener。

private void initialize()
  {
    DataEvent dataEvent = new DataEvent();
    dataEvent.setType("TIMEOUT");
    dataEvent.setNamespace(this.environment.getProperty("govern.namespace.timeout""timeout"));
    DataSourceEventManager.post(new DataSourceEvent(dataEvent, new TimeoutRuleListener("TIMEOUT", this.timeoutFactory)));
  }

當 timeout 超時策略被修改的時候,apollo 客戶端的長輪詢機制會監聽到配置修改,並通過 SDK 中的 ConfigChangeListener 進行處理,ConfigChangeListener 調用 TimeoutRibbonFactory 中的 operator 函數處理超時參數,並通過 applicationContext.publishEvent 發佈修改的超時參數到環境變量裏,最後通過 config.loadDefaultValues () 重新加載 Ribbon,使剛纔修改的超時參數生效。

 private void releaseRibbonTimeoutConfig(String key, String value)
  {
    if ((key.contains("ribbon.ConnectTimeout")) || (key.contains("ribbon.ReadTimeout")))
    {
      Set<String> keys = new HashSet();
      IClientConfig config;
      if ((key.startsWith("ribbon.ConnectTimeout")) || (key.startsWith("ribbon.ReadTimeout")))
      {
        config = this.springClientFactory.getClientConfig("default");
      }
      else
      {
        String name = key.split("\\.")[0];
        config = this.springClientFactory.getClientConfig(name);
      }
      keys.add(key);
#更新相應的bean的屬性值
      this.applicationContext.publishEvent(new EnvironmentChangeEvent(keys));
      #重新加載config配置
      config.loadDefaultValues();
    }
  }

通過上述方式,我們就可以通過 MSE 修改用戶自己的服務,實現超時參數配置並可以進行修改。

總結

本文通過介紹基於雲原生的 MSE 整體設計架構,支持兩種主流的代碼開發框架。針對 Dubbo 類型服務,不需要依賴使用自研 SDK,服務治理的配置信息,通過註冊中心 zookeeper 實現存儲加載。詳細闡述 MSE 納管 springcloud 類型服務,最重要的是依賴於 apollo 的動態參數生效機制的實現過程,並通過自研的 SDK 包,自定義 ConfigChangeListener 處理多種數據治理服務。雖然本文提到的基於雲原生環境下的微服務引擎在服務治理過程中,部署配置中心、註冊中心以及監控服務時會採取底層後臺調用 op 接口開通雲主機,然後部署底層基礎鏡像的方式,把服務全部啓動起來,看起來整個架構比較笨重。但是,整個架構設計,以及功能還是比較完備的,通過支持多種主流開發框架,支持多種主流配置中心及註冊中心,監控功能也比較完善,所以仍然值得我們繼續的深挖研究。在不破壞功能完整性,以及技術主流的前提下,我們仍然試圖考慮將微服務引擎改造爲更爲主流的雲原生服務網格 Istio 架構,來充分融入雲原生,使得服務治理更主流,更快捷,更加的安全等。敬請大家期待…

雲原生社區動態 雲原生社區是國內最大獨立第三方雲原生終端用戶社區。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/N1I9Xy851uOPtO1uNZISLA