Azure Storage Table Services

0 dakikada yazıldı

26166 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