Azure Tables  

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2011-02-11

Pobierz i uruchom

Wprowadzenie

Aktualnie zdecydowanie najpopularniejszą strukturą przechowywania informacji jest relacyjna baza danych. Wysoka popularność nie oznacza jednak, że koncepcja jest pozbawiona wad. Podstawową i największą niedogodnością relacyjnych silników bazodanowych jest ograniczona i trudna do osiągnięcia skalowalność. Ze względu na relacje i wzajemne powiązania ciężko rozproszyć system pomiędzy różne komputery. Ponadto operacje złączenia (join) również powodują spadek wydajności. Relacyjne bazy cechują się niską elastycznością oraz statyczną strukturą – każdy wiersz ma z góry zdefiniowaną postać. W środowisku rozproszonym potrzeba bardziej skalowalnego rozwiązania – nierelacyjnej bazy danych, np. Azure Tables.

Struktura

Azure Tables składa się z trzech elementów:

  • tabeli,
  • encji,
  • właściwości.

Tabela, tak jak w relacyjnych systemach, jest podstawowym nośnikiem danych. Wykonując zapytania, nie można z kolei wykorzystywać operacji złączenia (join) – jest to sprzeczne z ideą nierelacyjnych baz danych.

Encja z kolei może być utożsamiana z wierszem danych. Nowością w rozwiązaniach nierelacyjnych jest fakt, że encja (wiersz) nie ma statycznej struktury i może przyjmować różne wartości dla tej samej tabeli – nie ma z góry narzuconego schematu kolumn (właściwości). Najważniejsze właściwości encji w Azure Tables to:

  • RowKey (string) – klucz główny encji, unikalny w obrębie jednego węzła.
  • PartitionKey (string) – klucz podziału. Encje posiadające taki sam klucz podziału będą przechowywane w tym samym węźle (maszynie).
  • Timestamp – wersja encji, zarządzana przez system (readonly).

Właściwości pełnią funkcję analogiczną do kolumn znanych z relacyjnych baz danych. Warto jednak jeszcze raz podkreślić, że mają elastyczną strukturę i mogą różnić się w obrębie tej samej tabeli. Każda encja może mieć maksymalnie 255 właściwości. Całkowity rozmiar encji wynosi 1 MB, natomiast limit wszystkich danych w Azure Tables w ramach jednego konta Azure Storage wynosi 100 TB. Azure wspiera następujące typy właściwości:

Tabela1. Typy danych w Azure Tables.

Typ Opis
Binary Tablica bajtów (maksymalnie 64 KB).
Bool Klasyczna wartość boolowska.
DateTime 64-bitowy czas UTC  od 1/1/1600 do 12/31/9999. 
Double 64-bitowa wartość zmiennoprzecinkowa.
GUID 128-bitowy globalnie unikalny identyfikator.
Int 32-bitowa liczba całkowita.
Int64 64-bitowa liczba całkowita.
String Strumień znaków (UTF-16), maksymalnie 64 KB.

Podstawowe operacje

Zanim przejdziemy do bardziej zaawansowanych podstaw teoretycznych, warto napisać prostą aplikację dodającą wiersz (encję) do tabeli.

Azure Tables wykorzystują WCF Data Service. Z tego względu należy podpiąć do projektu referencję System.Data.Services.Client:

Kolejnym etapem jest zdefiniowanie klasy definiującej encję, np. Product:

public class Product:TableServiceEntity
{
        public Product(string partitionKey, string rowKey)
            : base(partitionKey, rowKey)
        {            
        }
        public Product() : base() 
        {
            this.PartitionKey = Guid.NewGuid().ToString();
            this.RowKey = string.Empty;
        }
        public string Name { get; set; }
        public string Description { get; set; }
        public float Price { get; set; }
}

Klasa  może przypominać model relacyjny, ponieważ mamy z góry zdefiniowany model. Jest to jednak wyłącznie interfejs ułatwiający dostęp do danych, wykorzystywany w aplikacji klienckiej (serwer nic o tym nie wie). W praktyce każdy wiersz może zawierać zupełnie inny zestaw danych.

Potrzebujemy również klasy, która umożliwi tworzenie zapytań oraz zarządzanie encjami (wstawianie, aktualizowanie, usuwanie):

public class ProductDataServiceContext : TableServiceContext
{
        public ProductDataServiceContext(string baseAddress, StorageCredentials credentials)
            : base(baseAddress, credentials)
        {
        }
        public IQueryable<Product> ProductTable
        {
            get
            {
                return this.CreateQuery<Product>("ProducsTable");
            }
        }
}

Czytelnicy znający technologie WCF Data Service z pewnością dostrzegą analogie  TableServiceContext z kontekstem wykorzystywanym z WCF Data Service – oba pełnią podobną rolę. W obydwu kontekstach występują też metody typu AddObject oraz SaveChanges.

Zobaczmy, jak wstawić nową encję do bazy:

StorageCredentialsAccountAndKey credentials = null;

credentials = new StorageCredentialsAccountAndKey(accountName, key);

CloudStorageAccount cloudStorageAccount = new CloudStorageAccount(credentials, false);

ProductDataServiceContext dataContext = null;

dataContext = new ProductDataServiceContext(cloudStorageAccount.TableEndpoint.ToString(), cloudStorageAccount.Credentials);

Product product = new Product();

product.Name = "Komputer PC";

product.Price = 3000.5f;

product.Description = "Opis";

dataContext.AddObject("ProductsTable", product);

dataContext.SaveChanges();

Selekcja danych również przypomina pracę z WCF Data Service:

ProductDataServiceContext dataContext = null;

dataContext = new ProductDataServiceContext(cloudStorageAccount.TableEndpoint.ToString(), cloudStorageAccount.Credentials);

var query = (from product in dataContext.ProductTable where product.Name == "Komputer PC" select product);           

Product foundProduct=query.First();

Jak widać, wspierane są również zapytania LINQ. Jednak  w porównaniu z WCF Data Service dysponujemy dość ograniczoną  liczbą obsługiwanych operatorów: From, Where, Take (maksymalnie 1000), First oraz FirstOrDefault.

Aktualizacja obiektu wygląda bardzo podobnie – zamiast wywoływać metodę AddObject, wykorzystujemy UpdateObject.  Jeśli chcemy z kolei usunąć encję, należy wywołać  DeleteObject.

TableServiceContext ma kilka ciekawych właściwości, np. IgnoreResourceNotFoundException, która określa, czy w przypadku braku encji powinien zostać zwrócony błąd 404.

Mamy również do dyspozycji IgnoreMissingProperties definiującą, czy wszystkie właściwości encji muszą zostać zmapowane.

Rozwiązywanie konfliktów

Każda encja posiada parametr ETag informujący o jej wersji. Wartość jest zmieniana wraz z modyfikacją pól. Azure Tables, tak jak WCF Data Service, wspiera kilka strategii rozwiązywania konfliktów podczas pobierania danych z bazy do kontekstu. Ustawiając właściwość MergeOption na jedną z poniższych wartości (w kontekście TableServiceContext), możemy ustawiać metodę rozwiązywania konfliktu:

  • AppendOnly – tylko nowe encje są dołączane. Wartości zmodyfikowane nie są nadpisane w momencie pobierania nowych wartości z serwera.
  • OverwriteChanges – zmiany dokonane po stronie klienta są nadpisywane danymi pochodzącymi z serwera.
  • PreserveChanges – zmiany dokonane przez klienta są zachowywane.
  • NoTracking – wszystkie encje ładowane są z serwera. Zmiany dokonane przez klienta nie będą więc zachowane. W przeciwieństwie do OverwriteChanges, stan śledzenia zmian zostanie wyczyszczony (ponieważ pobrane zostają nowe encje).

Partycjonowanie

Azure Tables gwarantuje wysoką skalowalność. Wymaga to jednak od programisty dobrego zaprojektowania bazy pod kątem kluczy RowKey i PartitionKey. Jak już wspomniano, PartitionKey określa węzeł – wszystkie encje o tym samym PartitionKey będą zlokalizowane w tym samym węźle. RowKey jest kluczem unikalnym w obrębie pojedynczego węzła. Węzeł nie zawsze stanowi pojedynczą, fizyczną maszynę – w zależności od potrzeb jest to zmieniane.

Przede wszystkim przez zdefiniowaniem strategii podziału należy przeanalizować najczęstsze zapytania. Na ich podstawie można określić, która wartość powinna posłużyć jako kryterium podziału. Zapytania filtrujące po PartitionKey oraz RowKey są najwydajniejsze. Jeśli w jakimś zapytaniu będą wykorzystywane inne kolumny (właściwości), to z pewnością będą one cechowały się dużo niższą wydajnością. Również należy dobrać PartitionKey tak, aby najczęstsze zapytania zwracały wyłącznie wiersze (encje) z jednej partycji – łączenie danych z kilku encji zawsze będzie wolniejsze, co wydaje się logiczne (dane w końcu mogą być umieszczone na całkowicie różnych lokalizacjach). Podsumowując, PartitionKey powinno być dobierane następująco:

  • Jeśli zapytania filtrują wyłącznie po pojedynczej kolumnie, która zawiera unikalne wartości, to wtedy wykorzystujemy ją jako PartitionKey. W takim przypadku RowKey pozostanie pusty, a wszystkie wiersze będą przydzielane do osobnych partycji (każdy wiersz to inny PartitionKey).
  • Jeśli nie ma pojedynczej, unikalnej kolumny, wtedy wykorzystujemy PartitionKey oraz RowKey. Filtrowanie po tych kolumnach jest również wydajne.
  • Jeśli nie jesteśmy w stanie za pomocą dwóch kolumn stworzyć unikalnego klucza głównego, musimy pogrupować kolumny i umieścić je w RowKey oraz PartitionKey. W takiej sytuacji RowKey oraz PartitionKey będą zawierały wartości z innych kolumn, porozdzielane np. myślnikiem („WARTOŚĆ1-WARTOŚĆ2-WARTOŚĆ3”).

Retry Policy

Retry Policy w Azure Storage określają politykę wznawiania wszelkich operacji w przypadku wystąpienia błędu. Jeśli wystąpi jakiś błąd podczas np. wstawiania nowego wiersza, można za pomocą RetryPolicy określić, w jaki sposób oraz ile prób powinno zostać wykonanych. RetryPolicy jest właściwością typu delegate klasy TableServiceContext:

public delegate ShouldRetry RetryPolicy();

Najłatwiej ustawić właściwość za pomocą statycznej klasy RetryPolicies, która zawiera kilka interesujących metod, np. Retry:

dataContext.RetryPolicy = RetryPolicies.Retry(5, new TimeSpan(0, 5, 0));

Powyższa polityka wznawiania określa, że powinno nastąpić pięć prób w interwale równym 5 minut (drugi parametr). Można także zdefiniować brak polityki:

dataContext.RetryPolicy = RetryPolicies.NoRetry();

Istnieje również bardziej zaawansowany model ekspotencjalny:

dataContext.RetryPolicy = RetryPolicies.RetryExponential(5, new TimeSpan(0, 0, 5));

Aby wykorzystać mechanizm wznawiania, zapytania powinny być zawsze rzutowane na CloudTableQuery za pomocą metody AsTableServiceQuery:

public IQueryable<Product> ProductTable

{

       get

       {

             return this.CreateQuery<Product>("ProducsTable").AsTableServiceQuery();

       }

}

Na koniec należy pamiętać, aby zapisywać zmiany za pomocą metody SaveChangesWithRetries:

dataContext.SaveChangesWithRetries();

Zakończenie

Azure Tables stanowią potężne i skalowalne rozwiązanie dla rozproszonych aplikacji. Ze względu na to, że wykorzystana architektura różni się od powszechnie stosowanych baz relacyjnych, koncepcja może początkowo wydawać się nieelegancka i trudna w realizowaniu. W środowisku rozproszonym jednak relacyjne bazy nie są najlepszym rozwiązaniem ze względu na problemy ze skalowalnością. Istnieje nawet ruch o nazwie NoSQL, który organizuje konferencje i przekonuje, że powszechnie wykorzystywany model relacyjny nie nadaje się dla baz o olbrzymiej ilości informacji.


          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.