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


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

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

1.1 那么問題來了?

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

  (1)由於受客戶端瀏覽器限制,無法從一台服務器上同時下載頁面中所有圖片信息;

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

  (2)由於圖片保存在物理服務器上,訪問圖片需要頻繁進行I/O操作:因此當並發用戶數越來越多時,I/O操作就會成為整個系統的性能瓶頸

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

1.2 山東濟南找藍翔?

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

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

SUMMARY:需求永遠是那么美好,使用最少的money干盡量多的事情!正在我們決定放棄開發崗位去藍翔學挖掘機技術的時候,我們突然發現有那么多的技術先驅已經給我們指明了道路。

二、架構設計:構建圖片服務器集群

  對於小型網站,由於數據規模小,可以把網站所有頁面和圖片統一存放在一個主目錄下,這樣的網站對系統架構、性能要求都很簡單。但大中型網站都保存有海量級的圖片文件,所采用的技術更是涉及廣泛,從硬件到軟件、編程語言、數據庫、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.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網站向客戶提供上傳和瀏覽的服務。因此,它最重要的功能就是:

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

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

  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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM