Azure Storage Table Services

0 dakikada yazıldı

26145 defa okundu

Düzenle

[Aşağıdaki makalenin SDK2.5 ile beraber yeni Azure özelliklerine uygun şekilde güncellenmiş halini burada bulabilirsiniz.]

Azure ortamında Storage Services kapsamında yarattığımız her bir Storage account ile beraber bize bir blob, bir table, bir de queue servisi sağlandığını biliyoruz. Bugüne kadar bloblarla ilgili birçok konuya değinmiş olsak da daha Table servisi ile ilgili birşey yapmadık :) Şimdi gelin Table Services yapısına bir giriş yapalım.

Bir NOSQL hikayesi....

NoSQL genel olarak çoğu developer'ın ilgi alanı dahilinde olmamış bir konseptttir. İtiraf etmek gerekirse konseptin adlandırılmasında da bazı problemler var aslında :) Özünde NoRel de denilebilir belki :) Nedenine gelirsek, Azure'taki Table Services'in belki de normal bir SQL'den en büyük farklılıklarından biri "relation" bulunmaması. Bazılarınız şu anda "nasıl yani?" derken bazılarınızın da "tamam sorun değil ben yazılım katmanına çözerim onu" gibi bir tepki verebileceğini tahmin ediyorum. Aslında burada en tehlikeli tepki ikinci tepki :) çünkü eğer ki NoSQL yapısında gidip kendi normal relational SQL sanal yapımızı oluşturmaya çalışırsak büyük hata yapmış oluruz... Neden mi? Relational yapıya ihtiyacınız varsa zaten SQL Azure var neden kastıralım ki? :)

Table servis'in nereler nasıl ve hangi nedenle kullanılabileceği konusunu biraz da ileri makaleler bırakacağımz. Şimdilik ipucu olarak No-SQL, Big Data and Hadoop vs diye sıralayabilir fikir vermesi için. Sonuç itibari ile table service'in çalışma yapısını, kullanımını, yapabildiklerini veya yapamadıklarını ve arkasındaki nedenleri :) bilmeden / anlamadan çıkıp da Table servis nerde kullanılmalı'nın tanımını yapmak faydasız olacaktır. İşte bu yüzden :) Gelin direk işin içine girelim ve bakalım neler oluyor.

İlk Table Service örneğine doğru...

Tüm Storage servislerinde olduğu gibi Table Service'de tamamen REST API'leri ile kullanılabiliyor. Buradaki güzel haber yapının tamamen WCF Data Services üzerine kurulu olmasın. Maalesef bu yazılardaki hedefimiz WCF Data Services'ı incelemek olmayacak, o nedenle bu konuda ancak örneklerimizi ayağa kaldırabilecek kadar bilgi edinebileceksiniz. Tavsiyem eğer konu ile ilgileniyorsanız kesinlikle ek kaynaklara başvurmanız.

[C#]

[DataServiceKey("PartitionKey", "RowKey")]
public class Urun2
{
    public string Timestamp { get; set; }
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Adi { get; set; }
    public string Aciklama { get; set; }
}

Yukardaki kod malum bizim uygulamamızda kullanacağımız basit bir entity. Bu Entity'nin özellikle ilk üç property'si eminim ki ilginç gelecektir :) İsterseniz hemen o üçünü incelemeye başlayalım.

Timestamp : Bu property'e biz genelde dokunmuyoruz :) Bu property azure table services tarafından concurrency kontrolü için kullanılıyor. Yani biz hiçbir zaman bu arkadaşa bir değer atamıyor veya genelde oradaki değeri alıp birşeyler de yapmıyoruz. O nedenle bu property kesinlikle bulunması gereken fakat bizi pek de ilgilendirmeyen bir property.

PartitionKey : İlk olarak bu property'nin bir String olduğuna dikkat çekiyim :) PartitionKey property'si çok kritik bir property. Table servis içerisinde bir table'da bulunan entity'lerinizin gerektiğinde farklı sunucularda farklı partitionlara dağıtılması noktasında dağıtım işlemi partitionkey'e göre yapılıyor. Yani eğer ki "Arabalar" diye bir table'ınız varsa ve içinde 100 milyon araba varsa.. ve bu table'daki performans düşüşü nedeniyle Storage Services bu table'ı partitionlama kararı alırsa... bu işlemi ancak partitionkey'e göre yapar. Peki bunun tam olarak anlamı nedir?

Örnek bir
partitioning.Örnek bir partitioning.

Yukarıdaki örneği bir inceleyelim isterseniz. Aslında yukarıdaki tek bir tablomuz var. Arabalar tablosu :) PartitionKey olarak arabanın tipi kullanılmış ve arabalara partitionkey değeri olarak Sedan, Coupe gibi değerler verilmiş. Table Services performansın düştüğünü algıladığında hemen PartitionKey'e göre iki partition yaratıp tabloyu iki farklı sunucuya dağıtmış. Buraya kadar manzara süper. Peki şimdi soruyorum :) Ben kırmızı arabaları getir dersem ne olacak? Tabi ki cross-partition yani partittionlar arası / genel bir sorgu çalıştırmış olacağım. İşte can alıcı nokta da zaten burası :) Çünkü olabildiğince cross-partition sorgulardan kaçmam gerek. Ha cross-partition sorgu olamaz mı? tabi ki olabilir fakat bu durumda ben partitionlamadan kazanacağım performans kazanamam.

Bir başka örnek
partitioning.Bir başka örnek partitioning.

Oysa yukarıdaki şekilde partitionKey'lerimde renk bilgileri olsaydı "bana kırmızı arabaları getir" dediğimde çok basit bir şekilde tek bir partition (yani disk / makine) üzerinden veri gelecekti. Sorgum diğer partition'a gitmeyecekti bile. Böylece tüm partitioning konseptinden faydalanmış olacaktır. Özünde Table Service'in datamı partitionlaması bir işe yaramış olacaktır.

Şimdi eminim ki aklınıza hemen ya ben hem renge hem de tipe göre sorgu yazacaksam sorusu geliyordu :) Birincisi tabi ki bunu yapabilirsiniz ve partitionging'den faydalanmaktan vaz geçmiş olursunuz ama ikincisi ise :) belki de çok karışık sorgularınız varsa SQL'i düşünmelisiniz :) Burada "düşünmelisiniz" derken "vazgeçin buralardan" demiyorum aman dikkat. SQL ile Table Service arasındaki kararı vermek çoğu zaman birçok değişkeni olan ve zor bir karar. Daha da önemlisi %99 ihtimal ile iki servisi beraber kullanacaksınız :) bunun da kendince nedenleri olacak. Umarım bu yazıda ve ilerikilerde vereceğim bilgiler doğru kararları alabilmeniz adına işine yarar fakat buradan "sorgunuz karşıkça SQL'e gidin" gibi genel mesajlar alıp çıkmamanız çok önemli. Benim yapmaya çalışacağım şey size bilgileri vermek, ihtimalleri göstermek, düşünmeniz gerekenlere dikkat çekmek, gerisindeki kararın emin olun birçok değişkeni var.

Sanırım PartitionKey'in anlamını ve değerini artık anladık. PartitionKey property'si string olduğu için rahatlıkla istediğiniz değerleri verebilirsiniz. Tabi ufak bir uyarı daha... PartitionKey vermiş olmanız datanızın illa partitionlandığı anlamına da gelmiyor :) Performans açısından ihtiyaç olunduğunu düşünürse Storage Services'deki "Smart NLB" partitioning'i kendisi tetikleyecektir (sizin tetikleme şansınız yok).

RowKey: Bir property'miz daha var :) O da RowKey. Tahmin edebileceğiniz üzere RowKey artık bir satırı doğrudan tanımlayan key anlamına geliyor. Tam olarak Primary Key diyemem çünkü RowKey ile PartitionKey beraber PK rolü oynuyorlar. Yani bir table'da aynı RowKey'den fakat farklı PartitionKey'de bulunan iki entity olabilir. Tabi karar tamamen sizin isterseniz RowKey'i tek de tutabilirsiniz tek başına.

RowKey ile PartitionKey'in beraber ne şekillerde tanımlandıkları çok önemli. Table Service'de en yüksek performansı alabileceğiniz sorguların kesinlikle beraberlerinde RowKey ve PartitionKey getirmeleri gerekiyor. Böylece sorguyu hem bir Partition ile sınırlamış oluryorsunuz, sonra da Partition içerisinde RowKey üzerinden arama yaparak sonucu alabiliyorsunuz.

[C#]

public class Urun : TableServiceEntity
{
    public string Adi { get; set; }
    public string Aciklama { get; set; }
}

Her sınıf tanımını yaparken bu üç property'yi de tek tek tanımlamak istemiyorsanız :) Basit bir şekilde sınıflarınızı  StorageClient ile beraber gelen TableServiceEntity'den türeterek ilerleyebilirsiniz. Böylece tüm bu sınıflar direk üç property'ye de sahip olacaktır.

[C#]

public class UrunlerContext : TableServiceContext
{
    private static CloudStorageAccount storageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
    public UrunlerContext()
        : base(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials)
    {
    }
    public DataServiceQuery<WebRole1.Entities.Urun> Urunler
    {
        get
        {
            return CreateQuery<WebRole1.Entities.Urun>("Urunler");
        }
    }
}

Bir sonraki adımda yapmamız gereken ServiceContext'imizi tanımlamak. Bunun için TableServiceContext'ten türettiğimiz UrunlerContext'in için Urunler döndüren bir DataServiceQuery yazıyoruz. Ayrıca base constructor'a da elimizdeki StorageAccount'ın TableEndpoint'ini ve erişim hakkını veriyoruz ki Context'imiz doğru şekilde bağlantıyı kurabilsin.

Artık arkaplan kodlarımızı tamamladık ve ilerlemeye hazırız. Tabi herşeyin öncesinde TableService'a bağlanıp bir table yaratmamız gerek.

[C#]

if (!Page.IsPostBack)
{
    CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) =>
    {
        configSetter(RoleEnvironment.GetConfigurationSettingValue(configName));
    });
}

var storageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
tableClient.CreateTableIfNotExist("Urunler");

Blob'larla uğraştıysanız buradaki kod da epey basit gelecektir :) Bu sefer bir blobClient almak yerine TableClient alıyoruz. Üzerinden de CreateTableIfNotExist gibi süper bir metod kullanıp :) tablomuzu yaratmış oluyoruz. Buradaki tablo isminin özellikle ServiceContext'teki DataServiceQuery içerisindeki ile aynı olması çok önemli.

Gelin şimdi hızlı bir şekilde basit CRUD (Create, Read, Update, Delete) işlemlerine göz atalım.

[C#]

var urunlerContext = new UrunlerContext();
var yeniUrun = new Entities.Urun
{
    PartitionKey = "Musteri1",
    RowKey = (new Random().Next(1, 100)).ToString(),
    Adi = "Deneme",
    Aciklama = "Açıklama"
};
urunlerContext.AddObject("Urunler", yeniUrun);
urunlerContext.SaveChanges();

Ben ürünleri müşterilere göre partitionlama kararı aldım. Özellikle birden çok müşterinin kendi tenant'larını kullandıkları bir yapıda bu çok anlamlı olacaktır. Sonuç itibari ile bir müşterinin diğer müşterinin verisine ulaşma gibi bir ihtimali zaten sıfır. RowKey'i ise şimdilik Random bir sayı olarak verdim ama unutmayın RowKey ve PartitionKey Table Service'deki ana iki Index'imiz. O nedenle sorgularınızı da düşünerek Index'lemek istediğiniz bilgileri bu property'lere atamak süper mantıklı olacaktır.

Kodun geri kalanının pek Azure ile bir alakası yok :)

[C#]

var entity = (from item in urunlerContext.Urunler
              where item.PartitionKey == "Musteri1" &&
              item.RowKey == "1"
              select item).First();
urunlerContext.DeleteObject(entity);
urunlerContext.SaveChanges();

Kayıt silme konusunda yukarıdaki kodu kullanabilirsiniz. Fakat yukarıdaki kodun ufak bir sorunu var. "First" lambdasını çağırdığımız anda sorgu REST API üzerinden Table Service'e gidiyor. Sonrasında SaveChanges dediğimizde ikinci bir REST API call da silme işlemi için gidiyor. Storage servislerinden transaction başına para verdiğimizi düşünürsek bir delete için iki call çok da hoş bir yapı değil :)

[C#]

var urunlerContext = new UrunlerContext();
urunlerContext.DeleteObject
(
    new WebRole1.Entities.UrunKey
    {
        PartitionKey = "Musteri1",
        RowKey = "1"
    }
);

İki REST API call yerine yukarıdaki şekilde tek bir call ile işi bitirebiliriz. UrunKey dediğimiz nesne aslında herhangi bir property'si olmayan base nesnemiz :) Sadece PartitionKey ve RowKey'i var. Peki WCF Data Services nereden bilecek sadece bu property'ler üzerinden eşleştirme yaparak data sileceğini?

[C#]

[DataServiceKey("PartitionKey", "RowKey")]
public class Urun2
{
    public string Timestamp { get; set; }
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Adi { get; set; }
    public string Aciklama { get; set; }
}

Makalenin en başında kodu tekrar paylaşıyorum :) İpucunu gördünüz mü? En üstteki metadata ile aslında biz bu mesajı vermiş oluyoruz. Aynı TableServiceEntity'den türettiğiniz tüm sınıflar için geçerli.

[C#]

var urunlerContext = new UrunlerContext();
var urunUpdate = (from item in urunlerContext.Urunler
                  where item.PartitionKey == "Musteri1"
                  && item.RowKey == "1"
                  select item).First();
urunUpdate.Aciklama = "Değişti";
urunlerContext.UpdateObject(urunUpdate);
urunlerContext.SaveChanges();

Update işlemini de yukarıdaki gibi yapabilirsiniz ;)

RetryPolicy

[C#]

urunlerContext.RetryPolicy = RetryPolicies.Retry(5, TimeSpan.FromSeconds(1));

Unutmamanız gereken noktalardan biri "RetryPolicy"ler. Malum herşey REST API call'lar ile devam ediyor ve arada tabi ki hatalar olabilir. Hatalara karşı kendinizi korumak için kesinlikle bir RetryPolicy set etmelisiniz. Yukarıdaki ufak örnek bir saniyede bir tekrar edecek şekilde 5 defaya kadar her işlemin tekrar edilmesini sağlıyor. Siz kendi senaryolarınızda farklı ayarlamalarla ilerleyebilirsiniz.

Entity Group Transactions

[C#]

urunlerContext.SaveChangesDefaultOptions =
    System.Data.Services.Client.SaveChangesOptions.Batch;

Süper önemli noktalardan biri de "Entity Group Transaction"'lar. Elinizde birden çok işlem var ve bunları toplu olarak gönderebilmek istiyorsunuz. İşte o zaman EGT'leri kullanmanız şart. Bunun ayarını basit bir şekilde yukarıdaki gibi yapabilirsiniz. (BeginSaveChanges / EndSaveChanges da var) Fakat burada da birkaç dikkat edilmesi gereken nokta var. Birincisi her EGT en fazla 100 işlem taşıyabilir. İkincisi ise bir EGT paketi en fazla 4MB büyüklüğünde olabilir. Bunları göz önüne alarak uygulamanızı tasarlamanız kritik. Tam "sınırlardan" bahsederken bir entity'nin 1MB'dan büyük olamayacağını (binary datalar bloblara gitsin lütfen :)) ve entity başına 255'dan fazla property olamayacağını da belirtiyim :)

EGT'lerin çalışması için ayrıca bir EGT içerisinde işlemlerin tek bir table ve tek bir partition'ı hedefliyor olması şart. Cross-Partition-Transaction yok. Tam da bu noktada eminim ki bazılarınıza "nasıl ya iki table'da aynı transaction içinde işlem yapamıyor muyuz?" diyecektir :) cevabım : "evet yapamıyorsunuz". Ama yazının başından beridir bahsetmediğim ilginç bir nokta var aslında :) Dikkat ettiyseniz Table yaratırken Field tanımlamaları yapmadık. Table'ların şemaları statik değil! Yani bir table içerisinde aslında farklı şemalarda nesneler bulunabiliyor. Ama ben şimdilik bu konuyu bir sonraki yazımıza bırakıyorum ;)

Görüşmek üzere.

Konunun devamı için; Table Services Round 2 : Sinsi Relationlar :)\ Table Services Round 3 : Continuation Token