天天看點

memcached單點故障與負載均衡

在上文中,主要教大家如何搭建在windows IIS 7.5下搭建php環境,使用常見的兩種memcached性能監視工具。通過自己動手實踐,觀察監控工具上資料,相信大家對于memcached的了解一定深入了很多。但是同樣還有些疑惑。本文将用圖文的方式,繼續講解memcached在叢集環境下的使用技巧。

曾經看到過這樣的文字(大概是翻譯過來的,算是比較權威的)

memcached如何處理容錯的?

不處理!:) 在memcached節點失效的情況下,叢集沒有必要做任何容錯處理。如果發生了節點失效,應對的措施完全取決于使用者。節點失效時,下面列出幾種方案供您選擇:

* 忽略它! 在失效節點被恢複或替換之前,還有很多其他節點可以應對節點失效帶來的影響。

* 把失效的節點從節點清單中移除。做這個操作千萬要小心!在預設情況下(餘數式雜湊演算法),用戶端添加或移除節點,會導緻所有的緩存資料不可用!因為哈希參照的節點清單變化了,大部分key會因為哈希值的改變而被映射到(與原來)不同的節點上

* 啟動熱備節點,接管失效節點所占用的IP。這樣可以防止哈希紊亂(hashing chaos)。

根據上面的說法,memcached其中一個節點失效以後,memcached本身是沒有任何政策維持失效轉發的,這對于大型系統是一個無法接受的事實。

Memcached基于一 個存儲鍵/值對的hashmap。其守護程序是用C寫的,但是用戶端可以用任何語言來編寫(本文使用C#作為例子),并通過memcached協定與守護程序通信。可 能這些東西都太高深了,我們暫不做研究。

雖然 Memcached作為一個分布式緩存資料服務,但是每個服務之間根本沒有進行互相通信,這裡可能與 我了解的分布式有點差別,可能是我才疏學淺,也可能是每個人思考問題的角度不同。Memcached用戶端就是通過一種分布式算法将資料儲存到不同的Memcached伺服器上,将資料進行緩存。

Memcached分布式環境下,每個伺服器端本身沒有互相相連的關系,資料分布其實是由用戶端來維持的(通俗點說,是用戶端按照自己的分布算法,将資料配置設定 給指定的服務端去存儲,取值的時候,用戶端再找指定的伺服器拿資料。任何環境下,服務端都不可能主動去找用戶端拿“東西”或者去操作用戶端。B/S模式也 是的,web伺服器不可能主動找浏覽器拿東西,更不可能對浏覽器端做任何操作)。memcached的服務端更不會這麼聰明,自動去查找、比對目前環境 中分布的其他伺服器。

而且,據我所知,Memcached本身并沒有為叢集提供真的高可用方案,因為我個人認為,使用叢集環境,通常是為了滿足以下的需求:

1.壓力分載 (負載均衡) 2.失效轉發(故障轉移)。

而memcached本身并不具備這兩點,這對于以“分布式緩存”号稱的memcached來說,是非常緻命的。對于筆者來說,也是一種沉痛的打擊啊(o(∩_∩)o 哈哈)。

理論上來講,用戶端連接配接多個memcached服務端的時候,預設的資料分布是這樣的:

memcached單點故障與負載均衡

理論上的,%33+33%+34%=100%,看上去資料分布還還很均衡,讀取的時候,分别通路從三台伺服器記憶體,再組成完整的資料。這樣的資料分發架構,倒真正做到了“負載均衡”。降低了三台伺服器的記憶體使用率,讓三台伺服器同時為用戶端提供服務,這難道不是完美的負載均衡嗎?如果沒有配置監視工具,也可以參照下面的代碼:

public void testMemcachedProviders() 
        {
            int runs = 100;
            int start = 200;
           
            string keyBase = "testKey";
            string obj = "This is a test of an object blah blah es, serialization does not seem to slow things down so much.  The gzip compression is horrible horrible performance, so we only use it for very large objects.  I have not done any heavy benchmarking recently";
            
            //Response.Write(obj);
            //循環記時往伺服器緩存上插入資料  等會我們要觀察一下資料都存到哪個伺服器上的Memcached server上了
            long begin = DateTime.Now.Ticks;
            for (int i = start; i < start + runs; i++)
            { 
              // DistCache.Add(keyBase + i, obj);
            }
            long end = DateTime.Now.Ticks;
            long time = end - begin;

            //計算存儲這些資料花了多長時間
            //Response.Write(runs + " sets: " + new TimeSpan(time).ToString() + "ms"+"<br/>");

            //開始取資料,并記時
            begin = DateTime.Now.Ticks;
            int hits = 0;
            int misses = 0;
            for (int i = start; i < start + runs; i++)
            {
                string str = (string)DistCache.Get(keyBase + i);
                if (str != null)
                    ++hits;    //成功取到資料
                else
                    ++misses;  //丢失次數
            }
            end = DateTime.Now.Ticks;
            time = end - begin;

            //擷取這些資料花了多長時間
            Response.Write(runs + " gets: " + new TimeSpan(time).ToString() + "ms"+"<br/>");
            Response.Write("Cache hits: " + hits.ToString() + "<br/>");
            Response.Write("Cache misses: " + misses.ToString() + "<br/>");
            Response.Write("--------------------------------------------------------\r\n");
            
        }
           

使用上面的測試代碼,可以列印輸出處理時間,get/set次數。分别注釋掉配置檔案中指定memcached伺服器配置後,再讀取測試,可以清楚的看到資料分布比例。

我本地開啟了3個memcached服務,分别指向不同端口,資料的分布比例是這樣的: 37%,43%,20%。沒有理論上的那麼均衡。

有過分布式叢集架構的朋友,肯定會想到,那萬一發生了“單點故障”(就像sqlserver叢集中的,單個節點上的資料庫伺服器當機),那不是玩完了?

memcached單點故障與負載均衡

按照上圖所示,一台伺服器當機了,就有33%的資料丢失了。那不就玩完了。如果是某銀行采用這種架構,發生如此杯具,那架構師豈不是要被群衆拿刀砍死。

那到底該如何解決這個問題呢?我翻閱了很多中文甚至英文的資料,好像真的沒有官方或者很權威的解決方案。提供了如下兩種思路。

解決方案1:本地備份緩存

在本地放一份緩存,同時也在分布式Memcached上放一份緩存,如果當其中一台節點當機了,用戶端程式直接讀取本地的緩存,本地用戶端維護一個HashMap即可,這樣的方案雖然很簡陋,但是可以滿足一部分場景的需要,當你很急需的時候可以作為臨時方案暫時替代一下。

解決方案2:采用緩存代理伺服器

采用 Magent 緩存代理,防止單點現象,緩存代理也可以做備份,通過用戶端連接配接到緩存代理伺服器,緩存代理伺服器連接配接緩存伺服器,緩存代理伺服器可以連接配接多台Memcached機器可以将每台Memcached機器進行資料同步。這樣的架構比較完善了, 如果其中一台緩存代理伺服器down機,系統依然可以繼續工作,如果其中一台Memcached機器down掉,資料不會丢失并且可以保證資料的完整性,以上描述的系統架構如圖所示:

memcached單點故障與負載均衡

在筆者的實踐中,沿襲了第一種方案的思想。由于筆者項目使用的是windows的伺服器,而第二種方案中的magent代理軟體,好像隻支援linux平台。

在用戶端還是配置多台伺服器,但是讓其中任意的一台伺服器做備份,去讀取并append另外幾台伺服器的資料,這樣依賴,該台備份伺服器上就始終存儲了一份完整的資料。當發生意外情況的時候,直接讀取備份伺服器上的資料。等伺服器故障恢複後,再從用戶端,将資料合理的分發出去。

在.NET平台下,就不能選用enyim.com Memcached Client或者Memcached Providers之類封裝得太完善的client啦!涉及到很多基本的操作,這裡推薦使用.NET memcached client library這個比較原始的類庫client。我始終覺得,最原始的,往往就是最靈活的。

public void testClientLib() 
        {
            string[] servers = { "127.0.0.1:11211", "127.0.0.1:11212","127.0.0.1:11213" };//多台伺服器構成叢集,端口号就是memcached.ini中的listener_port=11212
            string[] serversOne = { "127.0.0.1:11211" };//測試伺服器清單
            //初始化池
            SockIOPool pool = SockIOPool.GetInstance();
           // pool.SetServers(servers);
            pool.SetServers(serversOne);//測試伺服器
            pool.InitConnections = 3;
            pool.MinConnections = 3;
            pool.MaxConnections = 5;
            pool.SocketConnectTimeout = 1000;
            pool.SocketTimeout = 3000;
            pool.MaintenanceSleep = 30;
            pool.Failover = true;
            pool.Nagle = false;
            pool.Initialize();
            //初始化用戶端
            Memcached.ClientLibrary.MemcachedClient mc = new Memcached.ClientLibrary.MemcachedClient();
            mc.EnableCompression = false;

            string keybase = "test";
            //if (mc.Get(keybase) == null)
            //{
                //嘗試添加資料
                #region 單個key的情況,value值增大,資料不會自動分布,全都集中在一台伺服器上
                //List<int> list = new List<int>();
                //for (int i = 0; i < 100; i++)
                //{
                //    list.Add(i);
                //}
                //bool reslut =  mc.Add("test", list);
                //if (reslut)
                //{
                //    Response.Write("Add cache success");
                //}
                
                #endregion
                #region 多個key的情況,資料會自動均衡的分布  三台伺服器 33%,33%,34%
                //for (int i = 0; i < 100; i++)
                //{
                //    bool result = mc.Add(keybase + i, i);
                //    if (!result) {
                //        Response.Write("Add cache faild");
                //    }
                //}
                #endregion
                
           // }
           // else {
                //object value = mc.Get("test");
                int count = 0;
                for (int i = 0; i < 100; i++)
                {
                    object value = mc.Get(keybase + i);
                    if(value!=null)
                    {
                        ++count;
                    }
                }
                Response.Write("伺服器存儲資料量:"+count);
         //   }
            
            pool.Shutdown();
        }
           

通過本地備份的方式,解決單點故障:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Web;
using Memcached.ClientLibrary;


namespace MemcachedPro
{
   public  class MemcacheProvider
    {
       MemcachedClient mainClient;
       MemcachedClient backupClient;
        /// <summary>
        /// 在構造函數中,初始化用戶端(主/備)
        /// </summary>
        public MemcacheProvider() 
        {
            //主伺服器用戶端
            mainClient = new MemcachedClient();
            mainClient.PoolName = GetMainPollName();
            mainClient.EnableCompression = false;
           
            //備份伺服器用戶端
            backupClient = new MemcachedClient();
            backupClient.PoolName = GetBackUpPollName();
            backupClient.EnableCompression = false;
        }
      /// <summary>
      /// 初始化主伺服器pool
      /// </summary>
      /// <returns></returns>
       public  string GetMainPollName()
        {
            //string[] Servers = { "127.0.0.1:11211" };//測試伺服器清單
            string strServers = ConfigurationManager.AppSettings["memcacheMainServer"];
            string[] Servers = strServers.Split(';');
           //初始化池
            SockIOPool pool = SockIOPool.GetInstance("p1");
            pool.SetServers(Servers);//測試伺服器
            pool.InitConnections = 3;
            pool.MinConnections = 3;
            pool.MaxConnections = 5;
            pool.SocketConnectTimeout = 1000;
            pool.SocketTimeout = 3000;
            pool.MaintenanceSleep = 30;
            pool.Failover = true;
            pool.Nagle = false;
            pool.Initialize();
            return "p1";
        }
       /// <summary>
       /// 初始化備份伺服器pool
       /// </summary>
       /// <returns></returns>
        public string GetBackUpPollName()
        {
           // string[] Servers = { "127.0.0.1:11212" };//備份伺服器清單
            string strServers = ConfigurationManager.AppSettings["memcacheBackupServer"];
            string[] Servers = strServers.Split(';');
            //初始化池
            SockIOPool pool = SockIOPool.GetInstance("p2");
            pool.SetServers(Servers);//測試伺服器
            pool.InitConnections = 3;
            pool.MinConnections = 3;
            pool.MaxConnections = 5;
            pool.SocketConnectTimeout = 1000;
            pool.SocketTimeout = 3000;
            pool.MaintenanceSleep = 30;
            pool.Failover = true;
            pool.Nagle = false;
            pool.Initialize();
            return "p2";
        }
       /// <summary>
       /// 設定值
       /// </summary>
       /// <param name="key"></param>
       /// <param name="value"></param>
       /// <returns></returns>
        public bool SetCache(string key, object value)
        {
            bool result = false;
            try
            {
                //設定到主伺服器組
                result = mainClient.Set(key, value);

                //設定備份

                result = backupClient.Set(key, value);
            }
            catch (Exception)
            {
                //發送短信或者郵件提醒
                throw;
            }
           
            return result;
        }
       /// <summary>
       /// 取值
       /// </summary>
       /// <param name="key"></param>
       /// <returns></returns>
        public object GetCache(string key)
        {
            object value = null;
            //先讀主伺服器
            try
            {
                value = mainClient.Get(key);
                //如果沒取到值
                if (value == null)
                {
                    //發送短信或者郵件提醒:可能主伺服器當機了

                    //從備份伺服器取值
                    value = backupClient.Get(key);
                    if (value == null)
                    {
                        //從備份伺服器取值也失敗,發送短信或者郵件提醒
                    }
                }
            }
            catch (Exception)
            {
                //發送短信或者郵件提醒
                throw;
            }
            
           
            return value;
        }
       /// <summary>
       /// 當主伺服器恢複運作後(資料已經丢失了),将備份伺服器中的緩存同步到主伺服器
       /// </summary>
       /// <returns></returns>
        public bool RestoreCache() 
        {
            bool result = false;
            
            return result;
        }
    }
}
           

好了,由于篇幅有限。本文就到此了。 本文出自blog.csdn.net/dinglang_2009 ,轉載請注明出處

繼續閱讀