共用方式為


本文章是由機器翻譯。

雲端運算

同步處理 Windows Azure 的多個節點

Josh Twist

下載代碼示例

雲是一項重大的技術變革,業界很多專家預測如此重要的變革大約每 12 年才會發生一次。想想雲帶來的種種優勢,這樣的興奮也就不足為奇了:大幅減少運行成本,高度的可用性以及幾近無限的可擴展性,等等。

當然,這樣的變革也讓業介面臨很多挑戰,而且面對挑戰的不僅僅是如今的開發人員。例如,我們該如何有針對性地構建系統以充分利用雲的獨特優勢?

幸運的是,Microsoft 在二月份推出了 Windows Azure Platform,其中包含很多規模適中的元件,可用於創建既支援海量使用者又具備高度可用性的應用程式。但是,要使部署到雲中的應用程式完全發揮其潛能,系統的開發人員必須利用可稱為雲的最大特色的“彈性”。.

彈性是雲平臺的一項屬性,可以實現按需配置更多資源(計算能力、存儲等等),只需幾分鐘而不是幾個月,就能向 Web 場中添加更多伺服器。同樣重要的是,移除這些資源也同樣迅速。

雲計算的一個重要信條就是“即付即用”的業務模型,即您只需為您所用的部分支付費用。使用 Windows Azure,您只需為節點(在虛擬機器中運行的 Web 或工作者角色)部署之後的時間段付費,因此可以在不需要時或業務低谷期減少節點數量,從而直接帶來成本節約。

因此,開發人員創建富有彈性的系統就變得至關重要,這樣的系統可以自動適應配置的更多硬體,而只需系統管理員提供最少的輸入或配置。

情景 1:創建訂單號

最近,我有幸從事一項概念驗證工作,即使用 Windows Azure 將一個現有的 Web 應用程式基礎結構轉移到雲中。

考慮到該應用程式資料的分散性,Windows Azure 表存儲是最佳的候選工具。這一簡單而高效的存儲機制(支援幾近無限的可擴展性)是理想的選擇,只有一個明顯缺點是關於唯一識別碼的。

目標應用程式允許客戶下訂單,以及檢索其訂單號。使用 SQL Server 或 SQL Azure,很容易生成一個簡單的數位格式的唯一識別碼,但 Windows Azure 表存儲不提供自動遞增的主鍵。相反,使用 Windows Azure 表存儲的開發人員需要創建 GUID,並使用它作為表中的“鍵”:

505EAB78-6976-4721-97E4-314C76A8E47E

使用 GUID 的問題在於很難進行人工處理。想像一下通過電話告訴操作員您的 GUID 訂單號,或者在日誌中記錄這個訂單號,該有多麼麻煩。當然,GUID 在所有情況下都必須是唯一的,因此它們過於複雜。另一方面,訂單號只需要在“訂單”表中是唯一的。

在 Windows Azure 中創建簡單、唯一的 ID

我考慮了幾種相對簡單的方法來解決 GUID 問題:

  1. 使用 SQL Azure 生成唯一 ID:: 出於多種原因,這項概念驗證已經將 SQL Azure 排除在外,不會用於 Windows Azure 表存儲,主要是由於需要將系統擴展到很多節點,每個節點上都有很多執行緒來執行資料操作。.
  2. 使用 Blob 存儲來管理遞增的值: 在 Windows Azure Blob 存儲中存儲一個集中的計數器。.只要提供一個簡單、連續的訂單號生成機制,供多個節點使用,節點就能讀取和更新訂單號。但是,這種方法的爭議之處在于繁忙的系統每秒都需要很多新的訂單號,可能會對系統的可擴展性造成不利影響。
  3. 跨每個節點分隔唯一 ID: 創建輕型的記憶體中計數器,生成唯一的訂單號。為了確保在所有節點上的唯一性,每個節點將被分配一個訂單號範圍,如圖 1 所示。

圖 1 為每個節點分配訂單號範圍以確保唯一 ID

節點 Range
0-1,000,000
1,000,001-2,000,000

但是,這種方法伴隨著很多問題。當一個節點用完範圍內的值時會出現什麼情況?當一次向系統中添加數百個節點時會出現什麼情況?如果一個節點崩潰並被 Windows Azure 運行時使用新節點替換,將會怎樣?管理員需要密切監控這些範圍,小心確保配置是正確的,否則就得面對資料損壞。

事實上,我們需要更加完善的方法:不需要在每個節點上進行配置,爭用很少,而且始終能保證唯一性。為此,我綜合了上述的第二和第三種方法。

我使用的概念相對簡單:在 Blob 存儲中使用一個小文字檔來存儲最後一個訂單號。當需要新的訂單號時,節點可以訪問此 Blob,遞增值,然後寫回到存儲中。當然,在這個“讀取-遞增-寫回”的過程中,很有可能有其他節點也要訪問 Blob,進行相同的操作。如果不執行某種併發管理,訂單號就不可能是唯一的,資料也會損壞。傳統的解決之道是考慮創建一個鎖定機制,阻止多個節點同時處理 Blob。但是,鎖定的代價高昂,而且如果操作的主要著眼點是輸送量和高擴展性,就應該避免使用鎖定。

相反,能夠利用“樂觀併發”的方法是最佳的。利用樂觀併發,可以實現多個參與者與資源的交互。當資源被一個參與者檢索時,參與者會發出一個標記,指示資源的版本。發生更新時,該標記會包含在內,以說明資源的哪個版本正在被修改。如果資源已被另一個參與者修改,則更新將失敗,最初的參與者可以檢索到最近的版本並嘗試再次更新。如果更新很少發生爭用,樂觀併發就非常有效。這樣就可以避免鎖定的成本和複雜性,資源也不會被損壞。

讓我們假設在高峰時段,系統每秒發出大約 100 個新的訂單號。這意味著每秒有 100 次更新 Blob 的請求,發生爭用的幾率極大,也就意味著要重試很多次,從而使整個情況更加惡化。因此,為了減少這種現象發生的可能性,我決定為訂單號分配範圍。

我創建了名為 UniqueIdGenerator 的類來封裝此行為。這個類通過在可配置的區塊中遞增值來從 Blob 存儲中移除一個範圍內的訂單號。如果每個 UniqueIdGenerator 每次都預訂 1,000 個訂單號,則 Blob 可能平均 10 秒更新一次,這樣就大大降低了發生爭用的幾率。每個 UniqueIdGenerator 都可以自由地發出其預訂的 1,000 個訂單號,確信這個類指向同一 Blob 資源的其他實例不會發出相同的訂單號。

為了使這個新的元件可以進行測試,我指定了名為 IOptimisticSyncStore 的介面,使 UniqueIdGenerator 從特定的存儲機制分離。這樣做有一項額外的好處:在以後,該元件可以在需要時使用不同類型的存儲。這就是該介面:

public interface IOptimisticSyncStore
{
  string GetData();
  bool TryOptimisticWrite(string data);
}

如您所見,這是一個相當簡單的介面,只有兩個方法:一個檢索資料,另一個更新資料。後者返回一個布林值,其中 False 表示樂觀併發失敗,應該重試過程。

代碼下載中包含了使用 Blob 存儲的 IOptimisticSyncStore 的實現(詳細資訊請參見本文結尾)。該實現的大部分內容都很簡單,但我們有必要深入研究 TryOptimisticWrite 方法,以瞭解樂觀併發是如何實現的。

借助 Precondition 和實體標記 (ETag),在 Windows Azure Blob 存儲中更新資源時,很容易使用樂觀併發。Precondition 是開發人員斷言為 True 的語句,用以指示 HTTP 請求成功。如果 Web 伺服器計算該語句的結果為 False,它應該回應 HTTP 狀態碼 412: “Precondition failed.” (預分區失敗)。ETag 也是 HTTP 規範的一部分,用於標識資源(例如 Blob)的特定版本。如果 Blob 更改,ETag 也應該更改,如下所示:

try
{

  _blobReference.UploadText(

    data,

    Encoding.Default,

    new BlobRequestOptions { 

    AccessCondition = AccessCondition.IfMatch(
    _blobReference.Properties.ETag) });

}

為了在代碼中指定 Precondition,我們使用 BlobRequestOptions 類型,並設置 AccessCondition 屬性。 如果這一訪問條件未得到滿足(例如,另一個節點在檢索之後的很短時間內更新了 Blob),ETag 將不匹配,並引發 StorageClientException:

catch (StorageClientException exc)
{
  if (exc.StatusCode == HttpStatusCode.PreconditionFailed)
  {
    return false;
  }
  else
  {
    throw;
  }
}
return true;

在本實例中,該實現將檢查 PreconditionFailed 狀態碼的異常,並返回 False。 其他類型的異常是嚴重的失敗,會被重新引發以進行後續的處理和記錄。 沒有異常則表示更新成功,該方法會返回 True。 UniqueIdGenerator 類的完整內容如圖 2 所示。

圖 2 完整的 UniqueIdGenerator 類

public class UniqueIdGenerator
{ 
    private readonly object _padLock = new object();
    private Int64 _lastId;
    private Int64 _upperLimit;
    private readonly int _rangeSize;
    private readonly int _maxRetries;
    private readonly IOptimisticSyncStore _optimisticSyncStore;

    public UniqueIdGenerator(
      IOptimisticSyncStore optimisticSyncStore,
      int rangeSize = 1000,
      int maxRetries = 25)
    {
      _rangeSize = rangeSize;
      _maxRetries = maxRetries;
      _optimisticSyncStore = optimisticSyncStore;

      UpdateFromSyncStore();
    }

    public Int64 NextId()
    {
      lock (_padLock)
      {
        if (_lastId == _upperLimit)
        {
          UpdateFromSyncStore();
        }
        return _lastId++;
      }
    }

    private void UpdateFromSyncStore()
    {
      int retryCount = 0;
      // maxRetries + 1 because the first run isn't a 're'try.
while (retryCount < _maxRetries + 1)
      {
        string data = _optimisticSyncStore.GetData();

        if (!Int64.TryParse(data, out _lastId))
        {
          throw new Exception(string.Format(
            "Data '{0}' in storage was corrupt and " +
            "could not be parsed as an Int64", data));
        }

        _upperLimit = _lastId + _rangeSize;

        if (_optimisticSyncStore.TryOptimisticWrite(
          _upperLimit.ToString()))
        {
          return;
        }

        retryCount++;
        // update failed, go back around the loop
      }

      throw new Exception(string.Format(
        "Failed to update the OptimisticSyncStore after {0} attempts",
        retryCount));
    }
}

構造函數使用三個參數。 第一個參數是 IOptimisticSyncStore 的實現,例如我們前文討論的 BlobOptimisticSyncStore。 第二個參數是 rangeSize,一個整數值,表示從 Blob 分配的訂單號的範圍應該有多大。 範圍越大,發生爭用的幾率越小。 但是,如果此節點崩潰,丟失的訂單號也越多。 最後一個參數是 maxRetries,一個整數值,表示如果發生樂觀並行失敗,生成器應該嘗試更新 Blob 多少次。 超過這個值後,將引發異常。

NextId 方法是 UniqueIdGenerator 類的唯一一個公共成員,用於獲取下一個唯一訂單號。 該方法的主體將進行同步,以確保類的所有實例都是執行緒安全的,而且在所有運行 Web 應用程式的執行緒之間共用。 一個 if 語句將檢查生成器是否達到所分配範圍的上限值,如果達到將調用 UpdateFromSyncStore 以從 Blob 存儲獲取新的範圍。

UpdateFromSyncStore 方法是類的最後一部分內容,也是最有趣的內容。 IOptimisticSyncStore 的實現用於獲取前面發出的分配的上限值。 該值按生成器的範圍大小遞增,並寫回到存儲。 一個簡單的“while”迴圈結束主體,以確保當 TryOptimisticWrite 返回 False 時進行適當次數的重試。

以下程式碼片段顯示了所構造的 UniqueIdGenerator,在名為“uniqueids”的容器(注意:Blob 存儲中的容器必須使用小寫的名稱)中使用 BlobOptimisticSyncStore 以及名為“ordernumber.dat”的檔:

IOptimisticSyncStore storage = new BlobOptimisticSyncStore(
  CloudStorageAccount.DevelopmentStorageAccount, 
  "uniqueids", 
  "ordernumber.dat");
UniqueIdGenerator
  generator = new UniqueIdGenerator(storage, 1000, 10);

此實例從集中管理的範圍中刪除 1,000 個 ID,如果樂觀並行失敗,在引發異常之前將重試 10 次。

使用 UniqueIdGenerator 甚至更加簡單。 無論在哪裡,您需要新的唯一訂單號,只需調用 NextId:

Int64 orderId = generator.NextId();

示例代碼顯示了 Windows Azure 工作者角色,它使用多個執行緒來快速分配唯一訂單號,並將訂單號寫入 SQL 資料庫。在此實例中使用 SQL 只是為了證明每個訂單號都是唯一的,因為如果不是唯一的,將造成主鍵衝突,並引發異常。

這種方法(不是創建 Blob 並在應用程式生命週期的開始時將其值設置為 0)的優勢在於,系統管理員不需要執行任何工作。UniqueIdGenerator 根據您的設置仔細管理 ID 的分配,失敗時能夠順利地恢復,即使在最有彈性的環境中也可以輕鬆省力地擴展。

情景 2:“放出獵犬!”

該應用程式提出的另一項有趣要求是需要在發生指定事件(在大約已知的時間發生)之後快速處理大量資料。由於處理的性質,必須等到此事件之後才能開始對資料進行處理。

在這種情景中,工作者角色是明顯的選擇,可以只要求 Windows Azure 配置必要數量的工作者角色,以回應前面提到的事件。但是,配置新角色可能需要長達 30 分鐘的時間,而在此情景中,速度是至關重要的。因此,我考慮將角色提前準備好,但處於暫停狀態,直到管理員解除暫停(我稱之為“放出獵犬!”)。有兩種可能的實現方式,我將依次介紹。

應該引起注意的是,因為 Windows Azure 工作者角色會基於其部署的時間(而不是它們如何有效使用 CPU)計算負載,所以此方式與簡單地創建回應事件的工作者角色相比,成本要更高。但是,客戶會意識到這項投資是值得的,因為它可以確保處理儘快開始。

方式 I:輪詢

第一種方式(如圖 3 所示)讓每個節點按一定的時間間隔輪詢一個集中的狀態標誌(還是存儲在 Windows Azure Blob 中)以確定工作是否能夠開始。

圖 3 節點輪詢集中的狀態標誌

若要取消節點的暫停狀態,用戶端應用程式只需將此標誌設置為 True,從而使得在隨後的輪詢中,每個節點都被釋放。此方式最主要的缺點是存在延遲,延遲最大可能等於輪詢的時間間隔。而另一方面,這又是一種非常簡單、可靠的機制,很容易實現。

這種設計可通過示例代碼中的 PollingRelease 類演示。為了支援可測試性,標誌存儲機制抽象在一個介面之後,與 UniqueIdGenerator 類相似。图 4 顯示了介面 IGlobalFlag 和相應的 Blob 存儲實現。

圖 4 IGlobalFlag 介面和 Blob 存儲實現

public interface IGlobalFlag
{
  bool GetFlag();
  void SetFlag(bool status);
}

public class BlobGlobalFlag : IGlobalFlag
{
  private readonly string _token = "Set";
  private readonly CloudBlob _blobReference;
  public BlobGlobalFlag(CloudStorageAccount account, string container,    
    string address)
  {
    var blobClient = account.CreateCloudBlobClient();
    var blobContainer =   
      blobClient.GetContainerReference(container.ToLower());
    _blobReference = blobContainer.GetBlobReference(address);
  }

  public void SetFlag(bool status)
  {
    if (status)
   {
      _blobReference.UploadText(_token);
    }
    else
    {
      _blobReference.DeleteIfExists();
    }
  }

  public bool GetFlag()
  {
    try
    {
      _blobReference.DownloadText();
      return true;
    }
    catch (StorageClientException exc)
    {
      if (exc.StatusCode == System.Net.HttpStatusCode.NotFound)
      {
        return false;
      }
      throw;
    }
  }
}

請注意,在本示例中,Bob 存儲中存在的檔無論內容是什麼都指示 True。

PollingRelease 類本身很簡單,如圖 5 所示,其中只有一個公共方法,名為 Wait。

圖 5 PollingRelease 類

public class PollingRelease 
{
  private readonly IGlobalFlag _globalFlag;
  private readonly int _intervalMilliseconds;

  public PollingRelease(IGlobalFlag globalFlag, 
    int intervalMilliseconds)
  {
    _globalFlag = globalFlag;
    _intervalMilliseconds = intervalMilliseconds;
  }

  public void Wait()
  {
    while (!_globalFlag.GetFlag())
    {
      Thread.Sleep(_intervalMilliseconds);
    }
  }
}

只要 IGlobalFlag 實現指示其狀態為 False,此方法就會阻止任何調用者。 以下程式碼片段顯示了 PollingRelease 類的使用:

BlobGlobalFlag globalFlag = new BlobGlobalFlag(
  CloudStorageAccount.DevelopmentStorageAccount,
  "globalflags",
  "start-order-processing.dat");
PollingRelease pollingRelease = new PollingRelease(globalFlag, 2500);
pollingRelease.Wait();

創建一個 BlobGlobalFlag 實例,指向名為“globalflags”的容器。PollingRelease 類將每隔 2.5 秒輪詢一次,查看是否存在名為“start-order-processing.dat”的檔。在此檔存在之前,所有對 Wait 方法的調用都將被阻止。

方式 II:偵聽

第二種方式使用 Windows Azure AppFabric 服務匯流排同時與所有工作者角色直接通信並釋放它們(請參見圖 6)。

圖 6 使用 Windows Azure AppFabric 服務匯流排同時與所有工作者角色通信

服務匯流排是大型消息傳送和連接服務,也構建在 Windows Azure 之上。它可以實現分散式應用程式的不同元件之間的安全通信。如果兩個應用程式位於網路位址轉譯 (NAT) 邊界之後或者經常更改 IP 位址,則它們之間將很難相互通信,而服務匯流排就是一種理想的連接方式。本文不會詳細探討 Windows Azure AppFabric 服務匯流排,但您可以從 MSDN 的 msdn.microsoft.com/library/ee706736 獲得一份優秀的教程。

為了演示這種方式,我創建了一個名為 ListeningRelease 的類,它與 PollingRelease 類似,也有一個名為 Wait 的公共方法。此方法連接到服務匯流排,使用 ManualResetEvent 來阻止執行緒,直到其收到信號:

public void Wait()
{
  using (ConnectToServiceBus())
  {
    _manualResetEvent.WaitOne();
  }
}

图 7 列出了完整的 ConnectToServiceBus 方法。它使用來自 System.ServiceModel 和 Microsoft.ServiceBus 程式集的類型,通過 Windows Azure AppFabric 服務匯流排向雲提供一個名為 UnleashService 的類,如 圖 8 所示。

圖 7 ConnectToServiceBus 方法

private IDisposable ConnectToServiceBus()
{
  Uri address = ServiceBusEnvironment.CreateServiceUri("sb",  
    _serviceNamespace, _servicePath);
  TransportClientEndpointBehavior sharedSecretServiceBusCredential =  
    new TransportClientEndpointBehavior();
  sharedSecretServiceBusCredential.CredentialType =  
    TransportClientCredentialType.SharedSecret;
  sharedSecretServiceBusCredential.Credentials.SharedSecret.
IssuerName = _issuerName;
  sharedSecretServiceBusCredential.Credentials.SharedSecret.
IssuerSecret = _issuerSecret;

  // Create the single instance service, which raises an event
  // when the signal is received.
UnleashService unleashService = new UnleashService();
  unleashService.Unleashed += new  
    EventHandler(unleashService_Unleashed);

  // Create the service host reading the configuration.
ServiceHost host = new ServiceHost(unleashService, address);

  IEndpointBehavior serviceRegistrySettings = 
    new ServiceRegistrySettings(DiscoveryType.Public);

  foreach (ServiceEndpoint endpoint in host.Description.Endpoints)
  {
    endpoint.Behaviors.Add(serviceRegistrySettings);
    endpoint.Behaviors.Add(sharedSecretServiceBusCredential);
  }

  host.Open();

  return host;
}

圖 8 UnleashService 類

[ServiceBehavior(InstanceContextMode= InstanceContextMode.Single)]
public class UnleashService : IUnleashContract
{
  public void Unleash()
  {
    OnUnleashed();
  }

  protected virtual void OnUnleashed()
  {
    EventHandler temp = Unleashed;
    if (temp != null)
    {
      temp(this, EventArgs.Empty);
    }
  }

  public event EventHandler Unleashed;
}

UnleashService 由 Windows Communication Foundation (WCF) 作為單個實例託管,實現 IUnleashService 約定,只有一個方法:Unleash。 ListeningRelease 通過前面所示的 Unleashed 事件偵聽此方法的調用。 當 ListeningRelease 類觀察到此事件時,將設置目前阻止對 Wait 所有調用的 ManualResetEvent,並且所有被阻止的執行緒都將釋放。

在該服務的配置中,我使用了 NetEventRelayBinding,它支援通過服務匯流排的多播,允許任意數量的發佈者和訂閱者通過單個端點通信。 這種性質的廣播通信要求所有操作都是單向的,如 IUnleashContract 介面所演示的:

[ServiceContract]
public interface IUnleashContract
{
  [OperationContract(IsOneWay=true)]
  void Unleash();
}

端點使用“共用機密”(使用者名和複雜密碼)進行保護。有了這些細節資訊,任何能夠訪問 Internet 的用戶端都可以調用 Unleash 方法,包括示例中提供的管理員主控台(如圖 9 所示)。

图 9 管理员控制台

儘管 ListeningRelease 方式避免了 PollingRelease 類中固有的延遲問題,但還是會存在某種延遲。但是,偵聽方式的主要弊端在於它的無狀態性質,任何在釋放信號傳遞之後配置的節點都將看不到這個事件,因而保持暫停狀態。當然,顯而易見的解決方法就是綜合利用服務匯流排和 Blob 存儲中的全域標誌,但我把這個作為練習留給讀者。

示例代碼

本文隨附的示例解決方案可從 code.msdn.microsoft.com/mag201011Sync 獲得,它包括一個 ReadMe 檔,其中列出了各種先決條件以及設置和配置說明。該示例在一個工作者角色中使用了 ListeningRelease、PollingRelease 和 UniqueIdGenerator。

Josh Twist  是英國開發技術支援服務團隊的首席應用程式開發經理,可以在 thejoyofcode.com 上找到他撰寫的博客文章。

衷心感謝以下技術專家對本文的審閱:David Goon、Morgan SkinnerWade Wegner