API Optimizasyonları

Yazıda genel optimizasyonlar dışında php ile ilgili yapılabilecek optimizasyondan açıklamalarıda mevcut.

Temel hedef daha hızlı respose time ve daha az kaynak kullanımı. Bu sebeple temel olarak yapılabilecek şeylerden bahsedeceğim.

Önbellekleme (Caching)

Önbellek kullanılarak sıkça sorgulanan verileri önbelleğe alarak veritabanı üzerindeki gecikme maliyetinden kaçınılabilir. Redis, Memcached sık olarak kullanılan araçlardır. Ayrıca veritabanı önbelleği kullanılarak da sıkça kullanılan sorguları önbelleğe alarak response time arttırılabilir.

Önbellek çözümü için Proxy Design Pattern uygun gibi gözüksede önerilen Cache-Aside Pattern. Burada temel farkı sonradan ihtiyaç duyulmaya bağlıyorum. Temelde Proxy sarmaladığı sınıfın ön ihtiyaçlarını giderdiği; yetkinlendirme, bağlantı durumunu kontrol etme, güvenlik gibi işlerde daha sık kullanılır, bu demek değildir ki önbellek işlemleri için kullanılmasın. Fakat Cache-Aside tam olarak burada ki problemimiz için tasarlanmış bir çözüm.

Cache-Aside

Bir örnek olarak;

class ProductRepository extends ServiceEntityRepository
{
private CacheItemPoolInterface $cache;

public function __construct(ManagerRegistry $registry, CacheItemPoolInterface $cache)
{
parent::__construct($registry, Product::class);
$this->cache = $cache;
}

public function getProductById($id)
{
$cacheItem = $this->cache->get('product_' . $id, function (CacheItemInterface $cacheItem) use ($id) {
$product = $this->find($id);
if ($product) {
$cacheItem->set($product);
return $cacheItem;
}
return null;
});
if ($cacheItem) {
$this->cache->save($cacheItem);
return $cacheItem->get();
}
return null;
}
}

Burada repository sınıfı içerisinde eğer ilgili id değerine sahip ürün önbellek de bulunamaz ise veritabanına gidip sorguluyoruz ve bulduğumuz sonucu yine önbelleğe yazarak return ediyoruz.

Proxy

Bir örnek olarak;

class ProductRepositoryProxy implements ProductRepositoryInterface
{
private CacheItemPoolInterface $cache;
private ProductRepository $productRepository;

public function __construct(CacheItemPoolInterface $cache, ProductRepository $productRepository)
{
$this->cache = $cache;
$this->productRepository = $productRepository;
}

public function getProductById($id)
{
$cacheItem = $this->cache->get('product_' . $id, function (CacheItemInterface $cacheItem) use ($id) {
$product = $this->productRepository->getProductById($id);
if ($product) {
$cacheItem->set($product);
return $cacheItem;
}
return null;
});
if ($cacheItem) {
$this->cache->save($cacheItem);
return $cacheItem->get();
}
return null;
}
}

Burada Cache-Aside ile aynı işlem yapılıyor fakat farklı bir sınıfımız var ve Repository sınıfımız asıl yapması gereken işlevi korudu diyebiliriz. Her Repository sınıfı için bu Proxy sınıfları oluşturmak fazlaca maliyetli ve yönetilmesi oldukça zor olacaktır. Fakat buda yapılabiliyor.

Proxy sınıflar kod okunulabilirliği ve takip edilebilirliği açısından Cache-Aside’a göre bir miktar maliyetli bu sebeple Cache sınıfını doğrudan Repository içerisine implement etmek daha mantıklı görünüyor. Ayrıca Proxy daha sonradan ihtiyaç duyulabilecek durumlar için oldukça kullanışlı, büyük bir projede bir sınıfın nerelere dokunduğunu ve buralarda kontroller sağlamak bir miktar risk ve zorluk barındırabilir. Bu durumda ilgili yerlerde Proxy sınıflar kullanılarak gerçekleştirilmek istenen işlemleri gerçekleştirmek daha uygun olabilir. Tabi bu sadece benim görüşüm.

Asıl hedef veritabanı erişimlerini azaltmak ve sorguların gecikme maliyetlerinden kurtulmak fakat bu beraberinde başka sorunları getiriyor.

  • Verilerin tutarlılığı: Önbelleğe alınan verilerin, veritabanı ile tutarlı olmaları beklenir. Buda yazma işleminin sık olduğu durumlarda size sorunlar yaratabilir. Tabi burada da çözümler mevcut.
  • Depolama Maliyeti: Veritabanında olan bir veriyi ayrıca önbellek olarak kullandığınız ortamda da barındırmak durumundasınız. Bu veri çoklamanın getirdi bir maliyet. Ramler disklere göre daha maliyetli. Bu sebeple verileri önbellek de az süre barındırmalısınız. Her verinin değişme ihtimali ve kapladığı yerin maliyetinden kaçınmak için expire süresi belirtilmesi oldukça iyi bir çözüm olacaktır.

İstemci Önbelleği(Client Caching)

Burada response edilen datanın aslında client tarafında ön belleğe alınmasından bahsedebiliriz. Client gelen datayı HTTP ön bellek etiketlerinde (genelde Cache-Control) okuyarak aynı istekte bulunmadan response edilmiş önceki veriyi okuyabilir. Burada tarayıcılar bu işi kendileri otomatik olarak yapıyor olsalarda, servislerinizi kullanan diğer clientların bunu doğrudan gerçekleştirmek zorunda olmadığını belirtmekte fayda var.

Veritabanı Optimizasyonları

Her ne kadar önbellek sorunlarımızı bir miktar çözüyor olsada en nihayetinde veritabanı sorgularına ihtiyaç duyacağız. Burada kullandığınız veritabanı aracı dahil bir çok parametre var. Kullanılan veritabanı aracının artıları ve eksilerini bilmek bu noktada avantaj sağlayacak bir konudur. Örneğin mysql için ön bellek çözümünün kritik riskler oluşturma potansiyelinin yüksek olduğu söylenebilir. Veritabanında sadece ihtiyacınız olan verileri isteyin yani select ifadesi için tüm data yerine gerekli olan alanları getirmek veritabanın cevap hızını arttıracaktır. Ayrıca ilişkisel bir veritabanında doğru index kullanımı oldukça önemlidir. Sık sorgulanan alanların index olarak işaretlenmesi veritabanı tarafında sorgularınız için performanslı olacaktır.

  1. Index Kullanımı: Sık sorgulanan alanların index olarak tanımlanması sorguları hızlandıracaktır.
  2. İlişkiler: Doğru tanımlanmış ilişkiler ve ilişkili tablolarda ki veri tekrarı, gereksiz alanlardan kaçınmak sorguları hızlandıracaktır.
  3. Select: Doğrudan bir etkisi olmamakla birlikte dolaylı olarak veritabanından getirilecek datanın taşınmasında ki gecikmeyi azaltmak amacıyla ihtiyaç duyulan alanların getirilmesi faydalı olacaktır.
  4. Lazy-Eager Loading: API hizmetlerinde en sık GET metodu kullanılır. Bu durumda sorgulanan verinin ilişkili olduğu diğer verilere ne kadar ihtiyacımız var sorusu karşımıza çıkıyor. API hizmetlerinde ilişkili verilerin benzersiz alan bilgisinin verilmesi genelde yeterlidir. Fakat bazen ilişkili verinin tüm bilgilerinin verilmesine ihtiyaç duyulabilir. Lazy-Loading ilişkili verinin sadece benzersiz alan bilgisini kullanarak 2. bir sorgu ile ilişkili veriyi sorgular. Bu da hizmetlerde her sorgulanan veri için ilişkisini getirmek içinde ayrıca bir sorgu demek. Eğer response verinizin içerisinde bağlı diğer tablo verileri de bulunuyorsa Eager Loading kullanmak tüm datayı tek seferde getirmek daha hızlı olabilir. Örneğin; user ve userAddress adında 2 tablonuzun bulunduğu durumda, user listesini response edeceğinizi varsayalım ve her user beraberinde userAddress verisini de içeriyor. Bu durumda 100 kayıt için Lazy Loading kullanmak her kayıt için birde userAddress verisini veritabanında çekecektir. Eager Loading ise userAddress ile birlikte veriyi getirecektir. Fakat API hizmetlerinde ilişkisel tabloları getirmek bir sorgu parametresine bağlı olduğu durumda Eager Loading gereksiz bir veri dönüşü sağlayacaktır. ORMlerde bu durumlar için ekstra alanlar bulunmaktadır.

Ölçeklendirme

  • API hizmetlerini sağlayan makinelerin sayısını arttırabilir ve bir yük dengeleyici ile gelen talepler karşılanabilir.
  • Uzun süren API işlemlerini asenkron hale getirilebilir.
  • Gereksiz verilerin tespiti ve kaldırılması API hizmetini iyileştirecektir.
  • Bulut tabanlı ölçeklendirme ile taleplerin yoğun olduğu dönemlerde kaynakları otomatik artmasını ve taleplerin karşılanmasını sağlayabilirsiniz.

Anormal bir yavaşlık ve kaynak tüketimi mevcut ise muhtemelen yazılımda bir şeyler ters gidiyordur.