【大型網站技術實踐】初級篇:海量圖片的分佈式存儲設計與實現

說明:本文是我閱讀計算機工程期刊《海量圖片的分佈式存儲及負載均衡研究》一文的學習筆記和具體實踐,原文地址在本文底部。javascript

1、研究背景:性能與資金,兩者可兼得乎?

1.1 那麼問題來了?

  隨着互聯網的發展,許多大中型的網站都保存了大量的圖片資源,用戶在訪問這些圖片資源異常豐富的網站(如淘寶、京東等電子商務網站)時,網頁中的圖片信息佔據了頁面數據流量的很大部分,那麼問題也來了:css

  (1)因爲受客戶端瀏覽器限制,沒法從一臺服務器上同時下載頁面中全部圖片信息;html

PS:當一個網頁被瀏覽時,Web服務器與瀏覽器創建鏈接,每一個鏈接表示一個併發。當頁面包含多個圖片時,Web服務器與瀏覽器會產生多個鏈接,同時發送文字和圖片以提升瀏覽速度。所以,頁面中圖片越多Web服務器受到的壓力也就越大。同時因爲受到瀏覽器自己的併發鏈接數限制(2個~6個併發),意味着頁面上有多於併發鏈接數限制的圖片時,也不能並行地把全部圖片同時下載和顯示。java

  (2)因爲圖片保存在物理服務器上,訪問圖片須要頻繁進行I/O操做:所以當併發用戶數愈來愈多時,I/O操做就會成爲整個系統的性能瓶頸jquery

  (3)因爲受操做系統的限制,一個目錄中能存放的圖片文件數量也是有限的:隨着圖片資源不斷增長,如何有效管理和維護圖片也是一個難題;算法

1.2 山東濟南找藍翔?

  (1)對於少數大型網站系統,因爲自身具備雄厚的資金和人力資源,可採用NFS、CDN、Lighttpd、反向代理、負載均衡等技術提升用戶訪問速度;可是,這些技術須要龐大的資金來支持。數據庫

  (2)對於多數中小型網站系統,有木有一種方案適用於中等規模商務網站的海量圖片數據分佈式動態存儲及負載均衡的解決方案?該方案能否只需增長不多的硬件成本,便可提高網站的訪問速度,而且能夠根據須要動態調整圖片服務器的數量及圖片的存儲目錄,確保系統具備可擴展性和伸縮性。編程

SUMMARY:需求永遠是那麼美好,使用最少的money幹盡可能多的事情!正在咱們決定放棄開發崗位去藍翔學挖掘機技術的時候,咱們忽然發現有那麼多的技術先驅已經給咱們指明瞭道路。瀏覽器

2、架構設計:構建圖片服務器集羣

  對於小型網站,因爲數據規模小,能夠把網站全部頁面和圖片統一存放在一個主目錄下,這樣的網站對系統架構、性能要求都很簡單。但大中型網站都保存有海量級的圖片文件,所採用的技術更是涉及普遍,從硬件到軟件、編程語言、數據庫、Web服務器、防火牆等各個領域都有較高要求。所以,有必要設立單獨的圖片服務器來專門存放圖片,把圖片數據的流量從Web服務器上分離開,這樣的架構能夠有效緩解Web服務器的I/O性能瓶頸,提高用戶的訪問速度。服務器

2.1 系統設計目標  

  基於以上的考慮,咱們但願的設計目標是:

  (1)圖片能進行分佈式存儲; 

  (2)圖片服務器能實現負載均衡;  

  (3)能根據用戶訪問量及網站圖片數據量的增長能動態添加圖片服務器節點

  (4)圖片服務器節點的動態調整對網站用戶而言是透明的,而且不會中斷系統的正常運行

  其中,(1)和(2)是針對系統的高可用和伸縮性,而(3)和(4)則是針對系統的高可用和可擴展而言的。

2.2 系統架構設計

  系統總體架構如上圖所示:包括客戶端、Web服務器、數據庫服務器、圖片服務器集羣4個部分。

  (1)Web服務器部署網站的Web頁面,用於響應客戶端用戶的請求。當用戶瀏覽網頁時,Web服務器響應請求並訪問數據庫服務器,得到網頁中全部圖片的URL路徑,而後生成頁面並返回給客戶端;

  (2)客戶端接收該頁面並根據頁面中的圖片URL路徑自動從不一樣的圖片服務器下載並顯示相應圖片。

  (3)數據庫服務器用於記錄全部圖片的編號以及圖片的存放位置等信息,同時須要記錄全部圖片服務器的配置及當前狀態信息。

  (4)圖片服務器集羣用於存放網站的全部圖片信息,該集羣的服務器數量能夠根據須要動態增長或刪減。

3、系統實現:一種簡單且價廉可用的方案

3.1 數據庫設計與實現:兩張簡單的表

  Web服務器須要及時掌握全部圖片服務器的狀態和信息才能動態決定把圖片保存到哪一臺圖片服務器。所以,須要把全部的圖片服務器的狀態信息所有紀錄到數據庫服務器中,記錄圖片服務器信息和狀態的表格式以下圖所示:能夠清楚地看出,圖片服務器信息表中記錄了圖片服務器的ID、名稱、URL、最大存儲數量、當前已存數量以及服務器的狀態(True:可用,False:不可用),每一個圖片服務器下會有多個圖片信息記錄,所以它們是一對多的關係。

  (1)圖片服務器狀態信息表建表語句:

CREATE TABLE [dbo].[ImageServerInfo](
    [ServerId] [int] IDENTITY(1,1) NOT NULL,
    [ServerName] [nvarchar](32) NOT NULL,
    [ServerUrl] [nvarchar](100) NOT NULL,
    [PicRootPath] [nvarchar](100) NOT NULL,
    [MaxPicAmount] [int] NOT NULL,
    [CurPicAmount] [int] NOT NULL,
    [FlgUsable] [bit] NOT NULL,
 CONSTRAINT [PK_ImageServerInfo] PRIMARY KEY CLUSTERED 
(
    [ServerId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
View Code

  (2)圖片記錄信息表建表語句:

CREATE TABLE [dbo].[ImageInfo](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [ImageName] [nvarchar](100) NOT NULL,
    [ImageServerId] [int] NOT NULL,
 CONSTRAINT [PK_ImageInfo] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[ImageInfo]  WITH CHECK ADD  CONSTRAINT [FK_ImageInfo_ImageServerInfo] FOREIGN KEY([ImageServerId])
REFERENCES [dbo].[ImageServerInfo] ([ServerId])
GO

ALTER TABLE [dbo].[ImageInfo] CHECK CONSTRAINT [FK_ImageInfo_ImageServerInfo]
GO
View Code

3.2 文件上傳與瀏覽系統實現:一個ASP.Net MVC應用程序

  這裏咱們使用一個ASP.NET MVC應用程序部署在Web服務器上,這個應用程序做爲Web網站向客戶提供上傳和瀏覽的服務。所以,它最重要的功能就是:

  1、接收用戶上傳的文件,並轉交給圖片服務器的相關處理程序進行處理和保存;

  2、取得全部圖片服務器中保存的有效圖片路徑,返回給客戶端瀏覽器,再由客戶端瀏覽器對圖片路徑向圖片服務器集羣進行請求;

  3.2.1 設計Controller

    public class HomeController : Controller
    {
        IImageServerInfoRepsitory _imageServerInfoRepository;

        public HomeController()
        {
            // 這裏能夠藉助IoC實現依賴注入
        this._imageServerInfoRepository = new ImageServerInfoRepository();
        }

        #region 01.Action:上傳頁面
      //
        // GET: /Home/
        public ActionResult Index()
        {
            return View();
        }
        #endregion

        #region 02.Action:上傳圖片
      public ActionResult Upload()
        {
            HttpPostedFileBase file = Request.Files["fileUpload"];
            if (file.ContentLength == 0)
            {
                return Content("<script type=\"text/javascript\">alert(\"您還未選擇要上傳的圖片!\");location.href=\"/Home/Index\";</script>");
            }
            // 獲取上傳的圖片名稱和擴展名稱
        string fileFullName = Path.GetFileName(file.FileName);
            string fileExtName = Path.GetExtension(fileFullName);
            if (!CommonHelper.CheckImageFormat(fileExtName))
            {
                return Content("<script type=\"text/javascript\">alert(\"上傳圖片格式錯誤,請從新選擇!\");location.href=\"/Home/Index\";</script>");
            }
            // 獲取可用的圖片服務器集合
        List<ImageServerInfo> serverList = this._imageServerInfoRepository
                .GetAllUseableServers();
            if(serverList.Count == 0)
            {
                return Content("<script type=\"text/javascript\">alert(\"暫時沒有可用的圖片服務器,請稍後再上傳!\");location.href=\"/Home/Index\";</script>");
            }
            // 獲取要保存的圖片服務器索引號
        int serverIndex = CommonHelper.GetServerIndex(serverList.Count);
            // 獲取指定圖片服務器的信息
        string serverUrl = serverList[serverIndex].ServerUrl;
            int serverID = serverList[serverIndex].ServerId;
            string serverFullUrl = string.Format("http://{0}/FileUploadHandler.ashx?serverId={1}&ext={2}",
            serverUrl, serverID, fileExtName);
            // 藉助WebClient上傳圖片到指定服務器
        WebClient client = new WebClient();
            client.UploadData(serverFullUrl, CommonHelper.StearmToBytes(file.InputStream));

            return Content("<script type=\"text/javascript\">alert(\"上傳圖片操做成功!\");location.href=\"/Home/Index\";</script>");
        } 
        #endregion

        #region 03.Action:顯示圖片
      public ActionResult Show()
        {
            var imageServerList = this._imageServerInfoRepository.GetAllUseableServers();
            ViewData["ImageServers"] = imageServerList;
            return View();
        }
        #endregion
    }
View Code

  (1)圖片上傳的過程比較複雜,首先Web服務器接收客戶端的訪問請求並訪問數據庫,在Web端須要取得全部可用的圖片服務器的集合,這裏使用到了一個GetAllUseableServers方法,它的實現以下:能夠看出,咱們須要判斷FlgUsable標誌爲true以及CurPicAmount當前存儲量小於MaxPicAmount最大存儲量這兩個條件。若是有宕機或不可用的狀況,須要管理員將那一行的FlgUsable設置爲false。

        public List<ImageServerInfo> GetAllUseableServers()
        {
            List<ImageServerInfo> serverList = db.ImageServerInfo
                .Where<ImageServerInfo>(s => s.FlgUsable == true
                    && s.CurPicAmount < s.MaxPicAmount)
                .ToList();

            return serverList;
        }

  (2)這裏用到了一個GetServerIndex的方法,它的實現以下:從圖片服務器狀態信息表篩選出可用的圖片服務器集合記做C,並獲取集合的總記錄數N。而後用隨機函數產生一個隨機數R1,用R1與N進行取餘運算記做I=R1%N。則C[I]即爲要保存圖片的圖片服務器。這個方法基本保證了咱們的圖片服務器的負載是一個比較均衡的比例。(固然,咱們能夠設計一個更加高效的,相似於一致性哈希算法的哈希函數)

        #region 01.獲取服務器索引號
        /// <summary>
        /// 01.獲取服務器索引號
        /// </summary>
        /// <param name="serverCount">服務器數量</param>
        /// <returns>索引號</returns>
        public static int GetServerIndex(int serverCount)
        {
            Random rand = new Random();
            int randomNumber = rand.Next();
            int serverIndex = randomNumber % serverCount;

            return serverIndex;
        } 
        #endregion

  (3)最後,Web端程序藉助了WebClient將服務器ID、文件擴展名以及圖片的字節流轉交給了具體的圖片服務器處理程序:Web端程序的工做就到此結束,可是這裏木有采用異步,所以須要等待圖片服務器的工做結束。

  WebClient client = new WebClient();
  client.UploadData(serverFullUrl, CommonHelper.StearmToBytes(file.InputStream));

PS:因爲B/S架構自己技術限制,圖片沒法經過Web服務器直接上傳到不一樣的圖片服務器中。所以,這裏須要藉助相似於WebClient、HttpWebRequest等類向具體的圖片服務器發送Http請求,或者是經過在圖片服務器上部署Web Service,以便Web服務器經過調用該服務執行圖片的保存操做。

  3.2.2 設計View

  (1)上傳頁面:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <link href="~/Resources/css/mystyle.css" rel="stylesheet" />
    <script src="~/Resources/js/jquery-1.8.0.min.js"></script>
    <script type="text/javascript">
        $(function () {
            $("#btnUpload").click(function () {
                $("#loading").show();
            });
        });
    </script>
</head>
<body>
    <div id="mainarea">
        <fieldset>
            <legend id="title">圖片上傳系統</legend>
            <form method="post" action="/Home/Upload" enctype="multipart/form-data">
                <table>
                    <tr>
                        <td>
                            <input id="fileSelect" type="file" name="fileUpload" /></td>
                        <td>
                            <input id="btnUpload" type="submit" value="上傳圖片" /></td>
                    </tr>
                    <tr>
                        <td id="tiparea" colspan="2">
                            <div id="loading">
                                <img class="imgstyle" src="~/Resources/image/ico_loading2.gif" />
                                正在上傳中,請稍候...
                            </div>
                        </td>
                    </tr>
                </table>
            </form>
        </fieldset>
        <p id="footer">Copyright &copy; 2014 Edison Chou</p>
    </div>
</body>
</html>
View Code

  在form標籤中不要忘了:enctype="multipart/form-data"

  (2)瀏覽頁面:

@{
    Layout = null;
}
@using MyImageDFS.Model;
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Show</title>
    <link href="~/Resources/css/mystyle.css" rel="stylesheet" />
    <script src="~/Resources/js/jquery-1.8.0.min.js"></script>
</head>
<body>
    <div id="mainarea">
        <fieldset>
            <legend id="title">圖片瀏覽系統</legend>
            <table id="imageTable" cellspacing="1" cellpadding="1">
                @foreach (ImageServerInfo server in (List<ImageServerInfo>)ViewData["ImageServers"])
                {
                    foreach (ImageInfo image in server.ImageInfo)
                    {
                    <tr>
                        <td>
                            <img class="showimage" alt="@image.ImageName" src="@string.Format("http://{0}{1}",
                                    server.ServerUrl, image.ImageName)" />
                        </td>
                    </tr>
                    }
                }
            </table>
        </fieldset>
    </div>
</body>
</html>
View Code

  這裏主要經過對不一樣的圖片服務器發送請求獲取圖片,從而下降Web服務器的I/O性能瓶頸,加快整個系統的響應時間。

3.3 圖片服務器文件接收系統實現:一個ASP.Net通常處理程序

        /// <summary>
        /// 接收Web服務器傳遞過來的文件信息並保存到指定目錄文件下,最後將文件信息存入數據庫中
        /// </summary>
        /// <param name="context"></param>
        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "text/plain";
            // 接收文件的擴展名
            string fileExt = context.Request["ext"];
            if (string.IsNullOrEmpty(fileExt) || 
                string.IsNullOrEmpty(context.Request["serverId"]))
            {
                return;
            }
            // 圖片所在的服務器的編號
            int serverID = Convert.ToInt32(context.Request["serverId"]);

            // 圖片要存放的物理路徑
            string imageDir = "/Upload/" + DateTime.Now.Year + "/" + DateTime.Now.Month 
                + "/" + DateTime.Now.Day + "/";
            string serverPath = Path.GetDirectoryName(context.Request.MapPath(imageDir));
            if(!Directory.Exists(serverPath))
            {
                // 若是目錄不存在則新建目錄
                Directory.CreateDirectory(serverPath);
            }
            // 取得GUID值做爲圖片名
            string newFileName = Guid.NewGuid().ToString();
            // 取得完整的存儲路徑
            string fullSaveDir = imageDir + newFileName + fileExt;

            using (FileStream fileStream = File.OpenWrite(context.Request.MapPath(fullSaveDir)))
            {
                // 將文件數據寫到磁盤上
                context.Request.InputStream.CopyTo(fileStream);
                // 將文件信息存入數據庫
                ImageInfo imageInfo = new ImageInfo();
                imageInfo.ImageName = fullSaveDir; // 存儲圖片真實路徑
                imageInfo.ImageServerId = serverID; // 存儲服務器編號

                this._imageFacadeRepository.Add(imageInfo);
            }
        }
View Code

  (1)這是一個簡單的通常處理程序,它首先接收要保存的圖片擴展名以及服務器ID,根據規則生成具體的保存路徑,而後經過I/O流將圖片保存到該服務器的磁盤上;

  (2)最後將更改數據庫信息記錄,因爲要同時對兩張表進行修改,這裏咱們須要對這個方法進行一個簡單的封裝,使之成爲一個事務。如今咱們來看看這個Add方法的實現:

        public ImageStatusEnum Add(ImageInfo imageEntity)
        {
            // 首先是圖片信息表
            db.ImageInfo.Add(imageEntity);
            // 其次是圖片服務器信息表
            ImageServerInfo serverEntity = db.ImageServerInfo.FirstOrDefault(
                s => s.ServerId == imageEntity.ImageServerId);
            if (serverEntity != null)
            {
                // 當前服務器存儲數量+1
                serverEntity.CurPicAmount += 1;
            }
            // 一塊兒提交到SQL Server數據庫中
            int result = db.SaveChanges();

            if (result > 0)
            {
                return ImageStatusEnum.Successful;
            }
            else
            {
                return ImageStatusEnum.Failure;
            }
        }

3.4 簡單測試圖片文件的上傳與瀏覽

  (1)測試前的準備工做

  ①因爲個人電腦不支持64位的虛擬機,所以本來打算在VMware中部署三臺Windows Server 2008 R2做爲Web服務器和圖片服務器的打算被撤銷(無法任性地作實踐,我很不開心啊)。因而,我採用了在一臺電腦上部署多個應用,用端口號區分不一樣的服務程序來模擬效果。

  ②將Web應用程序和圖片服務應用程序分別編譯發佈,並部署到IIS中,分配不一樣的端口號:圖片上傳與瀏覽程序8000端口,圖片服務器的文件處理程序分別佔用8010與8020端口;

  (2)測試圖片文件上傳與存儲

  因爲連續截屏所生成的gif圖片太大,所以這裏只選擇了截取其中一次上傳的過程做爲展現。在我連續上傳操做了N次以後,如今咱們來看看兩個文件服務器所在的文件夾中是否有咱們上傳的圖片文件(這裏主要是看部署的程序所在的文件目錄,其中有一個專門保存圖片的文件目錄Upload)

  ①圖片服務器A所保存的文件:

  ②圖片服務器B所保存的文件:

  總結:從圖中能夠看出,咱們一共上傳了13張圖片,其中圖片服務器A保存了6張,圖片服務器B保存了7張,兩個服務器的負載並無出現一頭小一頭大,而是一個相對比較均衡的數量,這得益於咱們的隨機函數。

  (3)測試圖片文件瀏覽請求

  ①是否顯示了圖片列表:

  ②是否從不一樣圖片服務器獲取:

  總結:設立單獨的圖片服務器來專門存放圖片後,把圖片數據的流量從Web服務器上分離開,這樣能夠緩解Web服務器的I/O性能瓶頸,提升響應速度。

  (4)在原文的性能測試中,在局域網環境下對採用圖片服務器和不採用圖片服務器2種狀況進行了性能測試:測試數據有300萬張圖片均勻分佈在3臺圖片服務器上,每臺圖片服務器創建1 000個子目錄。在5臺客戶端上同時運行壓力測試軟件,分別模擬200個~1 000個併發用戶的請求。其測試結果以下圖所示:

  從圖中能夠看出,採用3臺普通PC機做爲圖片服務器後,整個系統的響應時間大大減小,性能獲得明顯提高,並且併發訪問量越大,性能的提高越明顯,而對於整個系統而言增長的硬件成本卻頗有限。

參考資料

   朱曉輝、王傑華、石振國、陳蘇蓉,《海量圖片的分佈式存儲及負載均衡研究》:http://www.cqvip.com/QK/71135X/201107/36101649.html

附件下載

  (1)數據庫:MyImageServer.mdf

  (2)程序代碼:MyImageDFS

 

相關文章
相關標籤/搜索