Apollo在基礎架構中的實踐經驗

本文來自李偉超同窗的投稿,若是你有好的文章也歡迎聯繫我。

微服務配置中心 Apollo 使用指南,如下文檔根據 apollo wiki 整理而來,部分最佳實踐說明和代碼改造基於筆者的工做經驗整理而來,若有問題歡迎溝通。html

配置中心

在拆分爲微服務架構前,曾經的單體應用只須要管理一套配置。而拆分爲微服務後,每個系統都有本身的配置,而且都各不相同,並且由於服務治理的須要,有些配置還須要可以動態改變,如業務參數調整或須要熔斷限流等功能,配置中心就是解決這個問題的。java

配置的基本概念

  • 配置是獨立於程序的只讀變量
    • 同個應用在不一樣的配置有不一樣的行爲
    • 應用不該該改變配置
  • 配置伴隨應用的整個生命週期
    • 初始化參數和運行參數
  • 配置能夠有多種加載方式
  • 配置須要治理
    • 權限控制(應用級別、編輯發佈隔離等)
    • 多環境集羣配置管理
    • 框架類組件配置管理

配置中心

  • 配置註冊與反註冊
  • 配置治理
  • 配置變動訂閱

Spring Environment

Environment 是 Spring 容器中對於應用環境兩個關鍵因素(profile & properties)的一個抽象。git

  • profile

profile 是一個邏輯的分組,當 bean 向容器中註冊的時候,僅當配置激活時生效。github

## 配置文件使用
spring.profiles.active=xxx 

## 硬編碼註解形式使用
@org.springframework.context.annotation.Profile
複製代碼
  • properties

Properties 在幾乎全部應用程序中都扮演着重要的角色,而且可能來自各類各樣的來源:properties 文件、JVM系統屬性、系統環境變量、JNDI、Servlet Context 參數、ad-hoc Properties 對象、Map 等等。Environment 與 Properties 的關係是爲用戶提供一個方便的服務接口,用於配置屬性源並從它們中解析屬性。spring

Apollo 簡介

簡介

Apollo(阿波羅)是攜程框架部門研發的開源配置管理中心,可以集中化管理應用不一樣環境、不一樣集羣的配置,配置修改後可以實時推送到應用端,而且具有規範的權限、流程治理等特性。數據庫

Apollo 支持4個維度管理 Key-Value 格式的配置:bootstrap

  • application (應用)

這個很好理解,就是實際使用配置的應用,Apollo 客戶端在運行時須要知道當前應用是誰,從而能夠去獲取對應的配置。每一個應用都須要有惟一的身份標識,咱們認爲應用身份是跟着代碼走的,因此須要在代碼中配置,具體信息請參見 Java 客戶端使用指南。緩存

  • environment (環境)

配置對應的環境,Apollo 客戶端在運行時須要知道當前應用處於哪一個環境,從而能夠去獲取應用的配置。咱們認爲環境和代碼無關,同一份代碼部署在不一樣的環境就應該可以獲取到不一樣環境的配置,因此環境默認是經過讀取機器上的配置(server.properties中的env屬性)指定的,不過爲了開發方便,咱們也支持運行時經過 System Property 等指定,具體信息請參見Java客戶端使用指南。bash

  • cluster (集羣)

一個應用下不一樣實例的分組,好比典型的能夠按照數據中心分,把上海機房的應用實例分爲一個集羣,把北京機房的應用實例分爲另外一個集羣。對不一樣的cluster,同一個配置能夠有不同的值,如 zookeeper 地址。集羣默認是經過讀取機器上的配置(server.properties中的idc屬性)指定的,不過也支持運行時經過 System Property 指定,具體信息請參見Java客戶端使用指南。服務器

  • namespace (命名空間)

一個應用下不一樣配置的分組,能夠簡單地把 namespace 類比爲文件,不一樣類型的配置存放在不一樣的文件中,如數據庫配置文件,RPC配置文件,應用自身的配置文件等。應用能夠直接讀取到公共組件的配置 namespace,如 DAL,RPC 等。應用也能夠經過繼承公共組件的配置 namespace 來對公共組件的配置作調整,如DAL的初始數據庫鏈接數。

同時,Apollo 基於開源模式開發,開源地址:github.com/ctripcorp/a…

基礎模型

以下便是Apollo的基礎模型:

  1. 用戶在配置中心對配置進行修改併發布
  2. 配置中心通知Apollo客戶端有配置更新
  3. Apollo客戶端從配置中心拉取最新的配置、更新本地配置並通知到應用

圖片

Apollo 架構說明

Apollo 項目自己就使用了 Spring Boot & Spring Cloud 開發。

服務端

圖片

上圖簡要描述了Apollo的整體設計,咱們能夠從下往上看:

  • Config Service 提供配置的讀取、推送等功能,服務對象是Apollo客戶端。
  • Admin Service 提供配置的修改、發佈等功能,服務對象是Apollo Portal(管理界面)。
  • Config Service 和 Admin Service 都是多實例、無狀態部署,因此須要將本身註冊到 Eureka 中並保持心跳
  • 在 Eureka 之上咱們架了一層 Meta Server 用於封裝 Eureka 的服務發現接口 Client 經過域名訪問 Meta Server 獲取 Config Service 服務列表(IP+Port),然後直接經過 IP+Port 訪問服務,同時在 Client 側會作 load balance、錯誤重試
  • Portal 經過域名訪問 Meta Server 獲取 Admin Service 服務列表(IP+Port),然後直接經過 IP+Port 訪問服務,同時在 Portal 側會作 load balance、錯誤重試
  • 爲了簡化部署,咱們實際上會把 Config Service、Eureka 和 Meta Server 三個邏輯角色部署在同一個 JVM 進程中。

客戶端

圖片

  • 客戶端和服務端保持了一個長鏈接,從而能第一時間得到配置更新的推送。
  • 客戶端還會定時從 Apollo 配置中心服務端拉取應用的最新配置。
    • 這是一個fallback機制,爲了防止推送機制失效致使配置不更新
    • 客戶端定時拉取會上報本地版本,因此通常狀況下,對於定時拉取的操做,服務端都會返回304 - Not Modified
    • 定時頻率默認爲每5分鐘拉取一次,客戶端也能夠經過在運行時指定 System Property: apollo.refreshInterval 來覆蓋,單位爲分鐘。
  • 客戶端從Apollo配置中心服務端獲取到應用的最新配置後,會保存在內存中
  • 客戶端會把從服務端獲取到的配置在本地文件系統緩存一份
    • 在遇到服務不可用,或網絡不通的時候,依然能從本地恢復配置
  • 應用程序從Apollo客戶端獲取最新的配置、訂閱配置更新通知

長鏈接實現上是使用的異步+輪詢實現,具體實現的解析請查看下面兩篇文章

service notifications

client polling

Apollo 高可用部署

在 Apollo 架構說明中咱們提到過 client 和 portal 都是在客戶端負載均衡,根據 ip+port 訪問服務,因此 config service 和 admin service 是無狀態的,能夠水平擴展的,portal service 根據使用 slb 綁定多臺服務器達到切換,meta server 同理。 | 場景 | 影響 | 降級 | 緣由 | |:----|:----|:----|:----| | 某臺config service下線 | 無影響 | | Config service無狀態,客戶端重連其它config service | | 全部config service下線 | 客戶端沒法讀取最新配置,Portal無影響 | 客戶端重啓時,能夠讀取本地緩存配置文件 | | | 某臺admin service下線 | 無影響 | | Admin service無狀態,Portal重連其它admin service | | 全部admin service下線 | 客戶端無影響,portal沒法更新配置 | | | | 某臺portal下線 | 無影響 | | Portal域名經過slb綁定多臺服務器,重試後指向可用的服務器 | | 所有portal下線 | 客戶端無影響,portal沒法更新配置 | | | | 某個數據中心下線 | 無影響 | | 多數據中心部署,數據徹底同步,Meta Server/Portal域名經過slb自動切換到其它存活的數據中心 |

Apollo 使用說明

使用說明

Apollo使用指南

Java客戶端使用指南

最佳實踐

在 Spring Boot & Spring Cloud 中使用。

  • 每一個應用都須要有惟一的身份標識,咱們認爲應用身份是跟着代碼走的,因此須要在代碼中配置。關於應用身份標識,應用標識對第三方中間件應該是統一的,擴展支持 apollo 身份標識和 spring.application.name 一致(具體查看 fusion-config-apollo 中代碼),其餘中間件同理。
  • 應用開發過程當中如使用代碼中的配置,應該充分利用 Spring Environment Profile,增長本地邏輯分組 local,非開發階段關閉 local 邏輯分組。同時關閉 apollo 遠程獲取配置,在 VM options 中增長 -Denv=local。

圖片

如下代碼是擴展 apollo 應用標識使用 spring.application.name,並增長監控配置,監控通常是基礎架構團隊提供的功能,從基礎框架硬編碼上去,業務側作到徹底無感知。

import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.foundation.internals.io.BOMInputStream;
import com.ctrip.framework.foundation.internals.provider.DefaultApplicationProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.Set;
/**
 * ApolloSpringApplicationRunListener
 * <p>
 * SpringApplicationRunListener
 * 接口說明 https://blog.csdn.net/u011179993/article/details/51555690https://blog.csdn.net/u011179993/article/details/51555690
 *
 * @author Weichao Li (liweichao0102@gmail.com)
 * @since 2019-08-15
 */
@Order(value = ApolloSpringApplicationRunListener.APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER)
@Slf4j
public class ApolloSpringApplicationRunListener implements SpringApplicationRunListener {
    public static final int APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER = 1;
    private static final String APOLLO_APP_ID_KEY = "app.id";
    private static final String SPRINGBOOT_APPLICATION_NAME = "spring.application.name";
    private static final String CONFIG_CENTER_INFRA_NAMESPACE = "infra.monitor";
    public ApolloSpringApplicationRunListener(SpringApplication application, String[] args) {
    }
    /**
     * 剛執行run方法時
     */
    @Override
    public void starting() {
    }
    /**
     * 環境創建好時候
     *
     * @param env 環境信息
     */
    @Override
    public void environmentPrepared(ConfigurableEnvironment env) {
        Properties props = new Properties();
        props.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, true);
        props.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, true);
        env.getPropertySources().addFirst(new PropertiesPropertySource("apolloConfig", props));
        // 初始化appId
        this.initAppId(env);
        // 初始化基礎架構提供的默認配置,需在項目中關聯公共 namespaces
        this.initInfraConfig(env);
    }
    /**
     * 上下文創建好的時候
     *
     * @param context 上下文
     */
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
    }
    /**
     * 上下文載入配置時候
     *
     * @param context 上下文
     */
    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
    }
    @Override
    public void started(ConfigurableApplicationContext context) {
    }
    @Override
    public void running(ConfigurableApplicationContext context) {
    }
    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
    }
    /**
     * 初始化 apollo appId
     *
     * @param env 環境信息
     */
    private void initAppId(ConfigurableEnvironment env) {
        String apolloAppId = env.getProperty(APOLLO_APP_ID_KEY);
        if (StringUtils.isEmpty(apolloAppId)) {
            //此處須要判斷一下 meta-inf 下的文件中的 app id
            apolloAppId = getAppIdByAppPropertiesClasspath();
            if (StringUtils.isEmpty(apolloAppId)) {
                String applicationName = env.getProperty(SPRINGBOOT_APPLICATION_NAME);
                if (!StringUtils.isEmpty(applicationName)) {
                    System.setProperty(APOLLO_APP_ID_KEY, applicationName);
                } else {
                    throw new IllegalArgumentException(
                            "config center must config app.id in " + DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
                }
            } else {
                System.setProperty(APOLLO_APP_ID_KEY, apolloAppId);
            }
        } else {
            System.setProperty(APOLLO_APP_ID_KEY, apolloAppId);
        }
    }
    /**
     * 初始化基礎架構提供的配置
     *
     * @param env 環境信息
     */
    private void initInfraConfig(ConfigurableEnvironment env) {
        com.ctrip.framework.apollo.Config apolloConfig = ConfigService.getConfig(CONFIG_CENTER_INFRA_NAMESPACE);
        Set<String> propertyNames = apolloConfig.getPropertyNames();
        if (propertyNames != null && propertyNames.size() > 0) {
            Properties properties = new Properties();
            for (String propertyName : propertyNames) {
                properties.setProperty(propertyName, apolloConfig.getProperty(propertyName, null));
            }
            EnumerablePropertySource enumerablePropertySource =
                    new PropertiesPropertySource(CONFIG_CENTER_INFRA_NAMESPACE, properties);
            env.getPropertySources().addLast(enumerablePropertySource);
        }
    }
    /**
     * 從 apollo 默認配置文件中取 app.id 的值,調整優先級在 spring.application.name 以前
     *
     * @return apollo app id
     */
    private String getAppIdByAppPropertiesClasspath() {
        try {
            InputStream in = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
            if (in == null) {
                in = DefaultApplicationProvider.class
                        .getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
            }
            Properties properties = new Properties();
            if (in != null) {
                try {
                    properties.load(new InputStreamReader(new BOMInputStream(in), StandardCharsets.UTF_8));
                } finally {
                    in.close();
                }
            }
            if (properties.containsKey(APOLLO_APP_ID_KEY)) {
                String appId = properties.getProperty(APOLLO_APP_ID_KEY);
                log.info("App ID is set to {} by app.id property from {}", appId, DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
                return appId;
            }
        } catch (Throwable ignore) {
        }
        return null;
    }
}
複製代碼

動態刷新

支持 Apollo 配置自動刷新類型,支持 @Value @RefreshScope @ConfigurationProperties 以及日誌級別的動態刷新。具體代碼查看下文連接。

  • @Value

@Value Apollo 自己就支持了動態刷新,須要注意的是若是@Value 使用了 SpEL 表達式,動態刷新會失效。

// 支持動態刷新
@Value("${simple.xxx}")
private String simpleXxx;
// 不支持動態刷新
@Value("#{'${simple.xxx}'.split(',')}")
private List<String> simpleXxxs;
複製代碼
  • @RefreshScope

RefreshScope(org.springframework.cloud.context.scope.refresh)是 Spring Cloud 提供的一種特殊的 scope 實現,用來實現配置、實例熱加載。

動態實現過程:

配置變動時,調用 refreshScope.refreshAll() 或指定 bean。提取標準參數(System,jndi,Servlet)以外全部參數變量,把原來的Environment裏的參數放到一個新建的 Spring Context 容器下從新加載,完事以後關閉新容器。提取更新過的參數(排除標準參數) ,比較出變動項,發佈環境變動事件,RefreshScope 用新的環境參數從新生成Bean。從新生成的過程很簡單,清除 refreshscope 緩存幷銷燬 Bean,下次就會從新從 BeanFactory 獲取一個新的實例(該實例使用新的配置)。

  • @ConfigurationProperties

apollo 默認是不支持 ConfigurationProperties 刷新的,這塊須要配合 EnvironmentChangeEvent 刷新的。

  • 日誌級別

apollo 默認是不支持日誌級別刷新的,這塊須要配合 EnvironmentChangeEvent 刷新的。

  • EnvironmentChangeEvent(Spring Cloud 提供)

當觀察到 EnvironmentChangeEvent 時,它將有一個已更改的鍵值列表,應用程序將使用如下內容: 1,從新綁定上下文中的任何 @ConfigurationProperties bean,代碼見org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder。 2,爲logging.level.*中的任何屬性設置記錄器級別,代碼見 org.springframework.cloud.logging.LoggingRebinder。 支持動態刷新

import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
/**
 * LoggerConfiguration
 *
 * @author Weichao Li (liweichao0102@gmail.com)
 * @since 2019/11/14
 */
@Configuration
@Slf4j
public class ApolloRefreshConfiguration implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Autowired
    private RefreshScope refreshScope;
    @ApolloConfigChangeListener
    private void onChange(ConfigChangeEvent changeEvent) {
        applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        refreshScope.refreshAll();
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
複製代碼

注意原有配置若是有日誌級別須要初始化。

import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.Set;
/**
 * logging 初始化
 *
 * @author Weichao Li (liweichao0102@gmail.com)
 * @since 2019/11/14
 */
@Configuration
@Slf4j
public class LoggingConfiguration {
    private static final String LOGGER_TAG = "logging.level.";
    private static final String DEFAULT_LOGGING_LEVEL = "info";
    @Autowired
    private LoggingSystem loggingSystem;
    @ApolloConfig
    private Config config;
    @PostConstruct
    public void changeLoggingLevel() {
        Set<String> keyNames = config.getPropertyNames();
        for (String key : keyNames) {
            if (containsIgnoreCase(key, LOGGER_TAG)) {
                String strLevel = config.getProperty(key, DEFAULT_LOGGING_LEVEL);
                LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
                loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
            }
        }
    }
    private static boolean containsIgnoreCase(String str, String searchStr) {
        if (str == null || searchStr == null) {
            return false;
        }
        int len = searchStr.length();
        int max = str.length() - len;
        for (int i = 0; i <= max; i++) {
            if (str.regionMatches(true, i, searchStr, 0, len)) {
                return true;
            }
        }
        return false;
    }
}
複製代碼

Apollo 最佳實踐 - 配置治理

權限控制

因爲配置能改變程序的行爲,不正確的配置甚至能引發災難,因此對配置的修改必須有比較完善的權限控制。應用和配置的管理都有完善的權限管理機制,對配置的管理還分爲了編輯和發佈兩個環節,從而減小人爲的錯誤。全部的操做都有審計日誌,能夠方便地追蹤問題

  • everyone 要有本身的帳戶(最主要的前置條件)
  • 每個項目都至少有一個 owner(項目管理員,項目管理員擁有如下權限)
    • 能夠管理項目的權限分配
    • 能夠建立集羣
    • 能夠建立 Namespace
  • 項目管理員(owner)根據組織結構分配配置權限
    • 編輯權限容許用戶在 Apollo 界面上建立、修改、刪除配置
      • 配置修改後只在 Apollo 界面上變化,不會影響到應用實際使用的配置
    • 發佈權限容許用戶在 Apollo 界面上發佈、回滾配置
      • 配置只有在發佈、回滾動做後纔會被應用實際使用到
      • Apollo在用戶操做發佈、回滾動做後實時通知到應用,並使最新配置生效
  • 項目管理員管理權限界面

圖片

項目建立完,默認沒有分配配置的編輯和發佈權限,須要項目管理員進行受權。

  1. 點擊application這個namespace的受權按鈕

圖片

  1. 分配修改權限

圖片

  1. 分配發布權限

圖片

Namespace

Namespace 權限分類

apollo 獲取權限分類分爲私有的和公共的。

  • private (私有的)

private權限的Namespace,只能被所屬的應用獲取到。一個應用嘗試獲取其它應用private的Namespace,Apollo會報「404」異常。

  • public (公共的)

public權限的Namespace,能被任何應用獲取。

Namespace 的分類

Namespace 有三種類型,私有類型,公共類型,關聯類型(繼承類型)。

Apollo 私有類型 Namespace 使用說明

私有類型的 Namespace 具備 private 權限。例如服務默認的「application」 Namespace 就是私有類型。

  1. 使用場景
  • 服務自身的配置(如數據庫、業務行爲等配置)
  1. 如何使用私有類型 Namespace

一個應用下不一樣配置的分組,能夠簡單地把namespace類比爲文件,不一樣類型的配置存放在不一樣的文件中,如數據庫配置文件,業務屬性配置,配置文件等

Apollo 公共類型 Namespace 使用說明

公共類型的 Namespace 具備 public 權限。公共類型的 Namespace 至關於遊離於應用以外的配置,且經過 Namespace 的名稱去標識公共 Namespace,因此公共的 Namespace 的名稱必須全局惟一。

  1. 使用場景
  • 部門級別共享的配置
  • 小組級別共享的配置
  • 幾個項目之間共享的配置
  • 中間件客戶端的配置
  1. 如何使用公共類型 Namespace
  • 代碼侵入型
@EnableApolloConfig({"application", "poizon-infra.jaeger"})
複製代碼
  • 配置方式形式
# will inject 'application' namespace in bootstrap phase
apollo.bootstrap.enabled = true
# will inject 'application', 'poizon-infra.jaeger' namespaces in bootstrap phase
apollo.bootstrap.namespaces = application,poizon-infra.jaeger
複製代碼

Apollo 關聯類型 Namespace 使用說明

關聯類型又可稱爲繼承類型,關聯類型具備 private 權限。關聯類型的 Namespace 繼承於公共類型的 Namespace,用於覆蓋公共 Namespace 的某些配置。

使用建議

  • 基礎框架部分的統一配置,如 DAL 的經常使用配置
  • 基礎架構的公共組件的配置,如監控,熔斷等公共組件配置
相關文章
相關標籤/搜索