SQL Azure的橫向擴展(Scaling Out)

關聯式資料庫(RDBMS),轉眼間已經陪伴了我們30幾年,也證明了它是近30年來最佳的資料儲存策略,但隨著雲端技術的來臨,這個老朋友卻遲遲未跟上腳步

 

Scaling Out in SQL Azure
 
/黃忠成
 
 
RDBMS in Cloud
 
 
     關聯式資料庫(RDBMS),轉眼間已經陪伴了我們30幾年,也證明了它是近30年來最佳的資料儲存策略,但隨著雲端技術的來臨,這個老朋友卻遲遲未跟上腳步,
原因在於RDBMS一開始的設計就是只在一台電腦中執行(亦或是說,過去我們一直將其視為理所當然而設計軟體的),在儲存體或是查詢速度不符合需求時,提升
電腦硬體能力是解決此問題的唯一途徑,但很明顯的,提升電腦硬體能力是不得已才做的決定,因為每提升一級,花費的金額是以多倍數成長的,你能想像,
8 CPU機器不夠用時,提升到16 CPU要花多少錢?當32GB RAM提升到64GB RAM又要花多少錢?當磁碟速度達到頻頸時,提升它們(增加RAID數量)又要花多少錢?
許多企業都需要儲存大量資料及快速查詢,但其中卻只有少數幾家有能力負擔因資料成長而帶來的RDBMS升級所需要的硬體成本。
 
     因此,雲端技術發展至今,多半未選用RDBMS做為資料的儲存技術,而是使用自製的資料庫系統,例如Google所發展的Big Table。但在雲端技術逐漸被人們重視的今天,
許多企業都曾想過將原本的系統搬上雲端,以此獲得高效能及高延展性的優點,此時RDBMS in Cloud便成了相當重要的需求,因為舊有的系統多半是使用RDBMS做為
資料儲存技術,如果雲端上沒有RDBMS,那麼移轉便意味著程式必須大量改寫。
 
    自然,雲端提供者也看到了這種需求,因此皆嘗試著將RDBMS搬上雲端,只是如同將原本舊系統搬上雲端一樣,要將RDBMS改寫成符合雲端儲存的技術,又談何容易。
 
 
 
Scaling Up VS Scaling Out
 
 
    雲端技術最迷人之處,便在於其無限的擴充性,舉例來說,當你有一個網站,一開始時是使用2 CPU的機器來運行,但隨著網站的用戶增加,你發現到2 CPU已經無法承受
這些使用者,此時雲端技術提供兩種選擇,一是將原本的2 CPU機器升級為4 CPU,稱為Scaling Up,另一種則是提供另一台2 CPU的機器(node),然後掛上Load Balancing機制
分擔原機器的工作,此為Scaling Out
 
    Scaling Up對於程式的衝擊最小,原有的程式幾乎不用修改便可增加所能服務的使用者量,但缺點是,Scaling Up有其上限存在,隨著低軟體成本而來的是高硬體成本,
Scaling Up期間,服務也可能因硬體的轉換而會有中斷的情況。
 
    Scaling Out的中後期硬體成本比較低,而且無上限存在,但軟體卻因為多一台機器而需要做出調整,這產生了低硬體成本、高軟體成本的現象,不過Scaling Out所獲得的成效通常
Scaling Up來得高,而且 Scaling Out期間服務也不會有中斷的情況發生。
    Scaling UpScaling Out技術,我們很輕易的能看出,雲端技術所嘗試要解決的問題,也能輕易的了解到,Scaling Out技術至今才受人重視的原由,電腦(Node)的增加,
意味著管理複雜度的增加,也意味著電力耗費的增加,如果不是在VM技術成熟的今日,人們對於Scaling Out技術仍然會抱持疑慮。
 
 
 
RDBMS In Cloud的現況
 
 
   有需求,就有人會嘗試滿足需求,既然人們想在雲端上使用RDBMS,就一定會有廠商嘗試提供,Amazon 雖然已有了雲端上的儲存技術 Amazon S3,但也推出了
RDBMS in Cloud產品:Amazon RDSAmazon RDS使用了MySQL做為底層資料的儲存技術,並提供了Scaling Up的選項來滿足擴充性,新一代的MySQL已經支援
Sharding(後述)技術,因此除了Scaling Up技術外,使用者也可以選擇Scaling Out
 
   擁有Microsoft SQL Server產品的Microsoft,也在Azure Platform中添加了RDBMS In Cloud產品:SQL AzureSQL Azure底層自然是使用Microsoft SQL Server引擎,
並提供了Scaling Out選項,不過目前SQL Azure尚未提供Scaling Up的選項,(我指的是,SQL Azure雖然已經提供了DiskScaling Up(1GB5GB10GB->50GB)
但並未讓我們選擇執行SQL Azure的機器配備,例如CPURAM)
 
 
Scaling Out in SQL Azure
 
 
   SQL Azure目前只提供了Scaling Out選項,在SQL Azure的架構中,每個Database都會放在一個獨立的SQL Server中,而該SQL Server會在一個獨立的VM中,這意味著,
當你擁有3Database時,同時也就擁有了3VM,每台VM都執行SQL Server,所以你的程式可以同時間對3Database發出查詢。
 
   那這與一台VM+一個SQL Server+3 Database有何不同呢?差別在於效能,在這種情況下,當某個查詢需要耗費較長時間時,會影響到其它的查詢所需執行的時間,簡單的說,
原本第一個查詢僅需耗費50秒,但因為第二個查詢動作於第一個查詢動作完成前插入,導致第一個查詢耗費時間增加到130秒。
 
   SQL Azure的模式下,每個Database都處於獨立的VMSQL Server中,所以即使你於第一個查詢未完成前插入第二個查詢,也不會影響第一個查詢結果獲得的時間。
   你可以想像成,你將資料表分散於兩個資料庫,然後分處於兩台SQL Server伺服器中,這就是SQL AzureScaling Out模式。
001
所以,並不是單純把Database擺上雲端,效能就會提升,還得取決於你如何將資料分散在多個資料庫(Microsoft提供SQL Azure Scaling Up方案前)
 
 
Data Partition
 
 
    很明顯的,SQL AzureScaling Out模式點出了RDBMS in Cloud的現況,既然從根底重寫RDBMS產品來符合雲端非一朝可及,那就在RDBMS上方插入一層,
可以管理多個RDBMS的機制,簡單的說,就是設計一套可以管理多個SQL Server的軟體。
 
   當然,這種做法只能治標,因為使用者必須先對資料進行分割,使其能分散存放於多個SQL Server中,也就是Data Partition,這個工作並不容易完成。
目前,對資料進行分割的方法大概有兩種,一是Vertical Partition,二是Horizontal Partition
 
 
 
 
 
Vertical Partition
 
   Vertical Partition是將原本資料表的Schema進行切割,再分散於其它資料表,使資料能分散儲存於多個資料表中,如圖002所示:
002
很明顯的,Vertical Partition會破壞原本的資料表Schema,所以大多應用於新開發的軟體,而不適用於已開發完成,嘗試進行移轉的軟體。
 
 
 
Horizontal Partition
 
 
   Vertical Partition不同,Horizontal Partition並不會破壞原先的Schema,而改以某個特定欄位的內容來對資料進行分割,例如將住在LA的客戶存放於資料庫A中的資料表1
將住在NY的客戶放在資料庫B中的資料表1,如圖003
003
 
就雲端儲存來說,Horizontal Partition其實是比較適用的,例如當使用城市來切割不足以將資料分割成可快速處理的大小時,可再次針對該城市進行居住道路的切割,
達到不停的Scaling Out目的,而Vertical Partition在遇到這種情況時,就只能再對已經完成Vertical Partition的結果再進行Horizontal Partition才能達到不停Scaling Out目的了。
 
 
Sharding and Partition
 
 
   你或許聽過Sharding這個名詞,其實ShardingPartition是一樣的東西,只是換個名詞來描述同樣一種技術而已。
 
 
Fan out
 
 
   當資料可以分割成多個區塊,分處於不同的RDBMS中時,接下來的問題就是如何對這些區塊進行查詢並整合其結果了,由於資料分處於不同的RDBMS,所以查詢
自然也就會發散成多個,為了隱藏這個事實,讓開發者可以下達一個查詢就能得到想要的結果,而不需考慮資料是位於多少個RDBMS中,Fan out技術就應運而生了。
一般來說,我們會設計一個Fan out Engine來接受使用者所下達的查詢語句,然後將其散發至所有的RDBMS,如圖004所示:
004
 
 
 
 
 
Data Partition in SQL Azure
 
 
 
   為了證明SQL AzureScaling Out技術,我撰寫了以下的Stored Procedure來產生30萬筆假資料,15萬筆是居住於NY的客戶,產生於Northwind資料庫中的BIG_ORDER_TEST資料表,
15萬筆是居住於LA的客戶,產生於Northwind2資料庫中BIG_ORDER_TEST資料表。
 
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[GenerateData]
AS
BEGIN
    SET NOCOUNT ON;
 
    DECLARE @PID int
    SET @PID = 0
    WHILE @PID < 150000
    BEGIN
       INSERT INTO BIG_ORDER_TEST(ORDER_ID,CustomerID,OrderTotal,City) VALUES(newid(),CONVERT(varchar,@PID)+'x',@PID * 2,'LA')
       SET @PID = @PID + 1
    END
END
 
 
接著在兩個資料庫中添加一用於統計金額的Stored Procedure,當然!寫法相當愚蠢,我只是想營造出長時間查詢的現象。
 
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[FindOrder]
@City varchar(10)
AS
BEGIN
DECLARE @I int
DECLARE @Total float
SET @I = 0
WHILE @I < 13
BEGIN
    SET @Total = 0
    SET NOCOUNT ON;
    DECLARE my_cursor CURSOR FOR SELECT OrderTotal FROM BIG_ORDER_TEST WHERE City = @City   
    DECLARE @SubTotal float
    SET @Total = 0
    OPEN my_cursor
    FETCH NEXT FROM my_cursor INTO @SubTotal
    WHILE @@FETCH_STATUS = 0
    BEGIN
      SET @Total = @Total + @SubTotal    
      FETCH NEXT FROM my_cursor INTO @SubTotal
    END
    CLOSE my_cursor;
    DEALLOCATE my_cursor;
    SET @I = @I+1
END
SELECT @Total
END
 
然後使用以下函式來測試同時對這兩個資料庫呼叫此Stored Procedure,要花多少時間才能完成。
 
private static void DoLocalingWith2()
{
            Console.WriteLine("--------Directing Execute in Local-----------");
            Stopwatch sw = new Stopwatch();
            sw.Start();
            Task t = new Task(() =>
            {
                using (SqlConnection conn = new SqlConnection(
                         "Data Source=127.0.0.1;Initial Catalog=Northwind;User ID=sa"))
                {
                    SqlCommand cmd = new SqlCommand("FindOrder", conn);
                    cmd.CommandType = CommandType.StoredProcedure;
                    cmd.Parameters.AddWithValue("@City", "LA");
                    cmd.CommandTimeout = 300;
                    conn.Open();
                    Console.WriteLine("Result (0): {0}", cmd.ExecuteScalar().ToString());
                }
            });
 
            Task t1 = new Task(() =>
            {
                using (SqlConnection conn = new SqlConnection(
                    "Data Source=127.0.0.1;Initial Catalog=Northwind;User ID=sa"))
                {
                    SqlCommand cmd = new SqlCommand("FindOrder", conn);
                    cmd.CommandType = CommandType.StoredProcedure;
                    cmd.Parameters.AddWithValue("@City", "NY");
                    cmd.CommandTimeout = 300;
                    conn.Open();
                    Console.WriteLine("Result (1): {0}", cmd.ExecuteScalar().ToString());
                }
            });
 
            t1.Start();
            t.Start();
 
            Task.WaitAll(t, t1);
            sw.Stop();
            Console.WriteLine("Elapsed:{0} ms ", sw.ElapsedMilliseconds.ToString());
            Console.WriteLine("--------Directing Execute in Local-------------");
}
 
 
結果如下所示:
 
--------Directing Execute in Local-----------
Result (1): 22499850000
Result (0): 22499850000
Elapsed:146398 ms
--------Directing Execute in Local-------------
 
大約耗費146,398 ms,也就是146秒左右。接著我們在SQL Azure上做同樣的事,再比對兩個結果。
 
private static void DoSqlAzureWith2()
        {
            Console.WriteLine("--------Directing Execute in SQLAzure-----------");
            Stopwatch sw = new Stopwatch();
            sw.Start();
 
            Task t = new Task(() =>
                {
                    using (SqlConnection conn = new SqlConnection("Server=tcp:<account>.database.windows.net;Database=Northwind;User ID=<user>;Password=<password>;Trusted_Connection=False;Encrypt=True;"))
                    {
                        SqlCommand cmd = new SqlCommand("FindOrder", conn);
                        cmd.CommandType = CommandType.StoredProcedure;
                        cmd.Parameters.AddWithValue("@City", "LA");
                        cmd.CommandTimeout = 300;
                        conn.Open();
                        Console.WriteLine("Result (0): {0}", cmd.ExecuteScalar().ToString());
                    }
                });
 
            Task t1 = new Task(() =>
                {
                    using (SqlConnection conn = new SqlConnection("Server=tcp:<account>.database.windows.net;Database=Northwind2;User ID=<user>;Password=<password>;Trusted_Connection=False;Encrypt=True;"))
                    {
                        SqlCommand cmd = new SqlCommand("FindOrder", conn);
                        cmd.CommandType = CommandType.StoredProcedure;
                        cmd.Parameters.AddWithValue("@City", "NY");
                        cmd.CommandTimeout = 300;
                        conn.Open();
                        Console.WriteLine("Result (1): {0}", cmd.ExecuteScalar().ToString());
                    }
                });
            t.Start();
            t1.Start();
            Task.WaitAll(t, t1);
            sw.Stop();
            Console.WriteLine("Elapsed:{0} ms ", sw.ElapsedMilliseconds.ToString());
            Console.WriteLine("--------Directing Execute in SQLAzure-------------");
}
 
結果並不意外:
 
005
 
SQL Azure中,不同資料庫意味著不同SQL Server,也意味著不同VM,所以兩個查詢可以在互不影響效能情況下同時執行,速度自然會提升。
 
 
 
後記
 
    
    Data Partition雖然可以將資料分割並儲存於多個RDBMS中,但隨之而來的是某些RDBMS功能的喪失,例如Cross Partition JoinCross Partition Foreign KeyCross Partition Transaction
目前都很難在Data Partition的情況下於Server端完成,而需要先將資料傳回中介層再進行處理(例:Windows Azure)
 
    況且Data Partition的處理對於現有系統移轉上也有難度,所以短期內要移轉現有系統至SQL Azure,而且想獲得Scaling Out優勢,還是有相當高的難度的。不過值得高興的是,SQL Azure
未來將會提供Auto Partition機制,也會提供Fan Out Query Engine,藉時移轉舊有系統勢必會比現在更容易。
 
    隨著雲端技術的成熟,Partition技術也會越來越受重視,程式設計師也必須放更多心思在切割應用程式及資料將其分散在不同電腦(VM)上,這樣才能完全得到雲端技術迷人的Scaling Out優勢。