程式設計師過關斬將--為微服務擼一個簡約而不簡單的配置中心

架構師修行之路發表於2020-05-27

毫不猶豫的說,現代高速發展的網際網路造就了一批又一批的網路紅人,這一批批網紅又極大的催生了特定平臺的一大波流量,但是留給了程式設計師卻是一地雞毛,無論是運維還是開發,每天都會擔心伺服器崩潰,程式down機。還是懷念以前那些單機結構呀,甚至有點嫉妒那些做內網幾乎沒有訪問量的應用的程式設計師,不用加班,不用提心吊膽,更不用每年買霸王洗髮露。

提到單機架構,在網際網路應用中肯定是吃不開的,流量高峰衝擊的你可以懷疑人生。單機升級叢集,帶來的不止是技術上的挑戰,在頂住流量高峰,迎合業務的同時,也引入了配置的複雜性。這也是我今天要談的主題:配置管理。在單機時代,無論是什麼語言,java也好,c#也罷,一個配置檔案足以。隨著所謂微服務這個看似能解決一切問題的方案誕生,同時也引入了更加複雜的配置問題:服務的資訊,服務的各種引數,配置更新問題等。可想而知,假如你的服務有100臺伺服器,修改一個配置項,利用單體架構逐個更新的方式是一個多麼蛋疼的事情,傳統的配置檔案方式已經無法滿足開發人員對於配置管理的要求:

  1. 安全性。配置資訊如果隨程式碼一起釋出,容易造成配置洩露。
  2. 實時性。修改配置,傳統的單機架構必須重啟服務才能生效。
  3. 侷限性。無法支援動態調整,像最普通的日誌開關功能,也不能做到。
  4. 環境區分。傳統的配置檔案方式,很難區分生產,開發,測試環境。
  5. 配置修改記錄問題。靜態配置檔案方式,很難追蹤這個配置檔案的修改記錄。

針對以上問題,有的公司採用資料庫記錄配置來解決問題,不是說不可以,只不過資料庫並不能解決根本性問題,舉個很簡單的例子:有最新的記錄修改,客戶端怎麼能實時得到通知呢?雖然可以利用其他方案來解決,但是基於資料庫方式並非是最優的。

配置中心無論是採用資料庫方式也好,還是採用其他方式也罷,最本質的出發點還是要看業務具體需求和相應的可以投入的人力物力。不是說像攜程的apollo不好,而是說這樣一套龐大的配置系統是否適用於你的公司,是否適用於你的業務。在很多情況下,公司的業務發展在一定階段講究的是短平快,沒有必要也沒有時間去投入精力去深研究一套龐大的系統,在業務慢慢發展起來之後可以慢慢迭代,這才是系統架構升級的過程,那些業務之初就要建立淘寶級架構的,最終會落得人人疲憊。

說了這麼多,我就是想擼一套簡約的,可落地的配置中心,要保證配置中心能正常工作,有幾點是在設計之初要考慮的:

需要一個可靠的,強一致性的儲存來支撐

在經過了多次技術調研之後,我最終選擇了ETCD,並非因為我喜歡最求熱點,而是ETCD在應對場景,功能上恰好對應我的需求,

etcd是CoreOS團隊於2013年6月發起的開源專案,它的目標是構建一個高可用的分散式鍵值(key-value)資料庫。etcd內部採用raft協議作為一致性演算法,etcd基於Go語言實現。

  1. 簡單:安裝配置簡單,而且提供了HTTP API進行互動,使用也很簡單
  2. 安全:支援SSL證照驗證
  3. 快速:根據官方提供的benchmark資料,單例項支援每秒2k+讀操作
  4. 可靠:採用raft演算法,實現分散式系統資料的可用性和一致性
  5. Watch機制:ETCD支援針對某個Key或者某組Key的Watch機制,一旦有資料發生變化,會實時通知Client。

需要支援多環境的配置

雖然很多儲存的顯示引數都沒有環境這樣的引數,但是都提供了類似目錄儲存這樣的功能,就像windows的檔案目錄一樣,這就為我們自定義功能提供了很強的應變能力,例如,我們儲存A應用開發環境可以是這樣的: /A/DEV/ ,這個目錄就代表了A應用的開發環境配置。

配置項發生變化,需要實時通知客戶端

基於第一點選擇的ETCD天生就支援Watch機制,所以配置項發生變化實時通知客戶端這點是很好做到的,就算了通知失敗,我們也可以自定義時間來延遲更新配置。稍後請看以下程式碼。

使用要簡單

對於使用者來說,配置中心提供的業務介面最終只有:獲取某個key的配置,這裡的key可以是應用+環境等引數的合集。為了輔助跟蹤,可以暴露出程式異常時候的處理事件,就像以下程式:

 /// <summary>
    /// 配置中心客戶端,應用,子應用,環境、版本、灰度、
    /// </summary>
    public interface IConfigCenterClient
    {
        /// <summary>
        /// 配置資訊發生變化時候的事件,引數:key/new value /動作(put /delete), 是etcd /consul watch的事件。每個key 的value發生變化都會觸發,每個key會觸發一條
        /// </summary>
        event Action<string, string, string> ConfigValueChangedEvent;

        /// <summary>
        /// 發生異常時候的事件
        /// </summary>
        event Action<Exception> ErrorOccurredEvent;

        /// <summary>
        /// 獲取相應的配置
        /// </summary>        
        /// <param name="configKey">配置的名稱</param>
        /// <param name="version">版本號</param>
        /// <returns></returns>
        string GetConfig(string configKey);
       
    }

抽象出這個操作介面之後,至於具體怎麼樣實現,可以是基於ETCD,可以是基於Consul,甚至可以基於DB,可見面向介面程式設計是多麼的正確。

在網路故障等情況下需要能繼續工作

在網際網路應用中,始終存在一個真理:網路是不可靠的。配置中心作為公司的一個核心繫統來說,要儘可能的保證能提供服務。但是,也要做好了預防措施,以防在配置中心短暫不可用的期間,不影響適用方。對於使用client端來說,既然服務端保證不了高可用,那就需要在本地動手腳:可以把獲取到的配置資訊在本地做儲存,資訊並隨著watch機制做持久化。這樣在配置中心網路不可用的情況下,儘量保證客戶端程式可用。至於本地儲存的方式,無所謂了,就算了文字檔案,還是sqllite都可以。

效能要高

配置中心最顯著的一個業務特點是變化不頻繁,但是客戶端使用頻繁。所以我們可以把配置資訊載入在記憶體中,記憶體中的資料隨著watch機制改變而改變,這樣就做到了記憶體資料和服務端資料高度一致。

以上囉嗦了那麼多,相信你們也看累了,空談理論誰都會,落地程式碼才是真理。

客戶端配置中心介面
 /// <summary>
    /// 配置中心客戶端,應用,子應用,環境、版本、灰度、
    /// </summary>
    public interface IConfigCenterClient
    {
        /// <summary>
        /// 配置資訊發生變化時候的事件,引數:key/new value /動作(put /delete), 是etcd /consul watch的事件。每個key 的value發生變化都會觸發,每個key會觸發一條
        /// </summary>
        event Action<string, string, string> ConfigValueChangedEvent;

        /// <summary>
        /// 發生異常時候的事件
        /// </summary>
        event Action<Exception> ErrorOccurredEvent;

        /// <summary>
        /// 獲取相應的配置
        /// </summary>        
        /// <param name="configKey">配置的名稱</param>
        /// <param name="version">版本號</param>
        /// <returns></returns>
        string GetConfig(string configKey);
       
    }
     /// <summary>
    /// 環境的定義
    /// </summary>
    public enum EnvEnum
    {
        /// <summary>
        /// 本地環境
        /// </summary>
        Local=1,

        /// <summary>
        /// 開發環境
        /// </summary>
        DEV,

       /// <summary>
       /// 模擬環境
       /// </summary>
        UAT,

        /// <summary>
        /// 生產環境
        /// </summary>
        PRO
    }
    public class ConfigCenterETCDProvider : ConfigCenterBaseProvider, IConfigCenterClient
    {
      
        //配置發生改變的通知事件
        public event Action<string, string, string> ConfigValueChangedEvent;
        //有異常發生的時候的事件
        public override event Action<Exception> ErrorOccurredEvent;

        //etcd的配置
        private ConfigCenterETCDOption option { get; set; }

        private EtcdClient client = null;

        internal ConfigCenterETCDProvider(ConfigCenterBaseOption _option) : base(_option)
        {
            if (_option == null)
            {
                throw new Exception("config is null!");
            }
            option = _option as ConfigCenterETCDOption;
            if (option == null)
            {
                throw new Exception("option type is wrong!");
            }
            if (string.IsNullOrWhiteSpace(option.ConnectionString))
            {
                //預設地址
                option.ConnectionString = "http://127.0.0.1";
            }
          
            client = new EtcdClient(option.ConnectionString, option.Port, option.Username, option.Password, option.CaCert, option.ClientCert, option.ClientKey, option.PublicRootCa);
            
            client.WatchRange(WatchPath, ETCDWatchEvent, exceptionHandler: e =>
            {
                ErrorOccurredEvent?.Invoke(e);
                Thread.Sleep(1000);
            });

            //讀取本地檔案只初始化讀取一次           
            InitConfigMapFromLocal();
        }

        #region 實現的介面方法
        //設定某個配置的值
        public bool SetConfig(string configKey, string configValue)
        {
            string key = FillConfigKey(configKey);
            try
            {
                var putRet = client.Put(key, configValue);

            }
            catch (Exception e)
            {
                ErrorOccurredEvent?.Invoke(e);
                return false;
            }
            return true;

        }

        //刪除某個配置的值
        public bool DeleteConfig(string configKey)
        {
            string key = FillConfigKey(configKey);
            try
            {
                client.Delete(key);
            }
            catch (Exception e)
            {
                ErrorOccurredEvent?.Invoke(e);
                return false;
            }
            return true;
        }
        #endregion

        #region 基類 方法
        //etcd 組織key
        protected override string FillConfigKey(string configKey)
        {
            return $"{WatchPath}/{configKey}";
        }
        //etcd 獲取遠端key
        protected override (bool isSuccess, string value) GetValueFromServer(string configKey)
        {
            try
            {
                var value = client.GetVal(configKey);
                return (true, value);
            }
            catch (Exception e)
            {
                ErrorOccurredEvent?.Invoke(e);
                return (false, null);
            }
        }

        #endregion

        #region 私有方法
        //監控value 變化的事件
        private void ETCDWatchEvent(WatchEvent[] response)
        {
            lock (ConfigCenterBaseProvider.ObjLock)
            {
                foreach (WatchEvent e in response)
                {
                    if (e.Type == Mvccpb.Event.Types.EventType.Delete)
                    {
                        if (ConfigMap.ContainsKey(e.Key))
                        {
                            ConfigMap.Remove(e.Key);
                        }
                    }
                    else
                    {
                        ConfigMap[e.Key] = e.Value;                       
                    }

                    Console.WriteLine(JsonConvert.SerializeObject(ConfigMap));
                    if (ConfigValueChangedEvent != null)
                    {
                        ConfigValueChangedEvent.Invoke(e.Key, e.Value, e.Type.ToString());
                    }

                }
                if (response != null && response.Any())
                {
                    //把最新的值寫會本地檔案
                    FlushLocalFileFromMap();
                }

            }

        }


        #endregion
    }

鑑於程式碼有點多,這裡不再貼出,有興趣的小夥伴,可以新增菜菜微信或者公眾號"架構師修行之路"回覆“配置中心”,基於ETCD完整的簡約版配置中心全部程式碼奉上,最後來看張測試的圖

image

相關文章