Orhun Begendi
hesapkurdu-development
9 min readMar 11, 2018

--

Entity Framework bildiğiniz üzere Microsoft’un geliştirdiği bir ORM aracı. Performansın bu derece önemli olduğu günümüzde EF’nin derinliklerine gireceğiz. Herkesin ağzından düşürmediği “EF’nin performansı çok kötü ya gerçekten” lafı gerçeği ne kadar yansıtıyor buna bakacağız. Bunların kaçını uyguluyoruz ve performanstan şikayet ediyor bunlara değineceğiz.

Bu makalede bahsi geçen tüm bilgiler; Hesapkurdu bünyesinde yaptığımız çalışmalarda performans arttırmak ve takıldığımız noktalarda elde edilen bilgilerden bir derleme ile oluşturuldu.

Burada ana amaç canlı ve yük altında çalışan sistemlerde EF’yi yani Data Access Layer’ı (DAL) doğru şekilde yönetmek için yaptığımız çalışmaların ve topladığımız bilgilerin diğer developer’lara fayda sağlamasıdır. Aynı zamanda mevcutta çalışan sistemden örnek vererek bu tip geliştirmeler yapan ya da yapmayı hedefleyen developer’lara bir yol haritası verebilmek!

Bu makaledeki bahsi geçen kullanımını en iyi şekilde kullanmak için bazı pattern ve prensiplere göre mimariyi şekillendirmek gerekli. Bizim üzerinde çok fazla uğraştığımız mimarimizde Seperation of Concerns, Service Layer, Business Logic, Dependecy Injection, Performance Tuning konseptlerini çok iyi anlamış olmak ve uygulamak en önemli nokta.

Database ile ilişkili olan bir projede ilk amaç her zaman ORM’yi en iyi en verimli şekilde kullanmak. Biz EF kullanıyoruz ve performansı arttırmak için deneyelediğimiz durumları anlatmak istiyorum. EF bilindiği üzere bir ORM aracı, o kadar fazla feature set enable şekilde geliyor ki gerçek db, sql, relation, FK, PK kavramları o kadar abstract hale geliyor ki herkes EF’nin döndüğü dataları sql’den öyle geliyor sanıyor. Bunun böyle olmadığını aslında relation nedir, data nedir, relational table structure nedir bunlara ışık tutmak gerekli.

İlk olarak ihtiyaçları belirlerken bir ORM aracının performans için dikkat edilmesi gereken husuları ele almak gerekli.

Bunlar ;

Auto-compiled queries

Pre-generated Views

Auto change detection

Scope

Async

Proxy / Relation structure

Önemli bir noktadan bahsetmek istiyorum. .Net’in ve EF’nin geleceği artık farklı şekillerde ilerliyor. Standard Library sistemi, .Net core, yeni clr yapısı derken çok fazla şey gitti çok fazla şey geldi. Bu yüzden Single Unit of Work mantığı artık kullanılmıyor. Deprecated olmuş bir yaklaşım denebilir. Önceden projelerde EDMX üretilip herşey burada çözülür. DB demek developer için edmx demekti ama artık code first mantığı o kadar benimsendi ki EF Core’a designer ve edmx yapısını koymaya zahmet bile etmediler. O yüzden şuan bahsedeceğim tüm konular EF ve EF Core’da ortak olup performans için configuration ve best practices olarak devam edecek.

İlk olarak configuration olarak neler yapabileceğimize bakalım.

Disable Change Tracker

Bu configuration’ı disable etmek baya güzel bir şey. Çok ciddi bir performans kazancı oluyor. Okunan datanın farklı bir yerden değiştirilip değiştirilmediğini tutan bir özellik. Bu rest tabanlı sistemlerde zaten ihtiyaç olmuyor. Bunun zaten neden ilk başta açık geldiğini kimse anlam veremedi. Bizde veremedik. Bu tip bir ihtiyaç illa olursa farklı çözüm yöntemleri var hatta entity’de iki de bir aç kapa yapmak yerine change tracker için belli noktalara(tablo vb.) bir mekanizma yazmak daha güzel.

Disable Lazy Loading

EF’nin hayatımıza kattığı başka bir bullshit. Kesinlikle saçma sapan bir yapı. Bunu yaptılar hatta üstüne bu da enable şekilde geliyor. Entity Framework and Enterprise Applications konferansında bile bunun enable şekilde neden geldiğini söyleyemediler. EF kullanan herkes bunu kapatması gerektiğini düşünüyorum. Lazy load mantığı dışarıdan bakınca çok çok mantıklı geliyor ve öyle de görünüyor ama arada kaçan çok ciddi bir durum var, Performans katili!!!

Nasıl kapatılır çok basit, Configuration objesi içerisinde lazyloadenabled false yapılsa yeterli.

Bu kadar ayıpladık, neden açıklayayım. Bizim sistemden örnek vereyim, çok kullanılan 2 tablomuz var. Biri [Customer] diğeri [CustomerAddress] doğal olarak customer ID üzerinden customerAddress’e bağlı ve virtual object olarak FK üzerinden bu data geliyor. Evet ne kadar güzel! Herşeye tek objeden gelip gidebiliyoruz. Ben kendi kodum içerisinde bir customer çekiyorum, bunun içerisinde address objesini kullandığım zaman address objesi orada, ama kullanmazsam o obje için gerekli query’ler çalışmıyor. Çok güzel düşünmüşler diyebilirsiniz. Değil arkadaşlar değil, N+1 database query problemi oluşturuyor. Yani ben address objesini çektiğim zaman tekrar customer üzerinden query’liyor. Buda 2 tablodan 2 row data için 3 query yapıyor demek oluyor. Bu bizim gibi yük altıdna çalışan sistemlerde hele hele rest mimarisi olan bir sistemde bizi katlediyor.

İlla bu tip virtual işlere ihtiyacınız varsa, Include() kullanımına bakın. Bu kullanım Eager Loading dediğimiz bir approach. Bu şekilde N+1 sorunu yaşamazsınız. Gereksiz yüzlerce query’den bu şekilde kurtulursunuz.

Bu saçma özellik neden var anlıyoruz ama neden enabled geliyor bilmiyoruz. Biz kapattık %30 hızlandık, sizde kapatın. Bizde ne kadar farketti derseniz günlük bir milyon query’den fazla query azalttık.

Loops

Loop denince akla hemen hemen herkesin aklına while, for, foreach gelecektir. Bunarla yapılan query’ler inanılmaz derece de tehlikeli. Bunları detect edecek bir yapı yok. Anca 3rd party bir kaç tool ile bunları tespit edebilirsiniz.

Loop işlemlerinde en mantıklı ve performans en yüksek yaklaşım büyük bir dataset çekip gelen liste içerisinde linq ile query yapmak olacaktır.

2. koddaki gibi bir kullanım kesinlikle daha iyi sonuç verecektir.

Database Schemas

Bu konu çoğu developer’ın bilmediğini gördüğüm bir konu. Uzun süre EF ile çalışan kişiler yüksek yük altında iş yapınca bir yerler saçmaladığını göreceklerdir. Herşeyi doğru yapmanıza rağmen sistemin bu saçma davranışlarına anlam veremeyebilirsiniz. Biraz derinlerine inince gördük ki EF aslında bir schema mantığıyla çalışıyormuş. DB çok büyükse schema’larınız çeşitlenmeye başlarsa göreceksiniz ki korkunç bir memory’nin EF’ye gidecek. Bunu çözmek için geçilecek yol baya zorlu olabilir. Çünkü DbContext’lerinizi schema bazlı ayırmanız gerekecek. Neden diyebilirsiniz, sebebi ise bi hayli ilginç. Default schema eğer ayarlamadıysanız “dbo” olarak set edilecektir. Hiç farkettiniz mi database model’lerinize schema verirken dbo vermedikleriniz direk dbo’yu bulur. Nedeni işte budur. Diğer schema’lar ise entity’nin ayırdığı memory’de cache’lenir. Bu da bütün schema’ların bütün tablolalarını OnInit esnasında cachelemeye çalışacaktır. Bu da muazzam bir memory yükü getirecektir. Tabi db’niz bu kadar büyük schema bazlı değilse bu kısmı es geçebilirsiniz. Ama mimarinizi yavaş yavaş geliştirirken böyle engellere takılacaksınız. Aklınızda olmasında fayda var.

Çok yaygın olan repository patter, unit of work, generic repository gibi yapılarla bunları nasıl kullanacağız gibi bir soru mutlaka gelecektir. Kullanmayın! Zaten bu pattern’lar 1990'da kaldı, kullanmayın, EF zaten db’yi abstract eder, abstract’ı repository’le unit of work’le tekrar abstrat etmek nedendir?

Şu şekilde context bazlı olarak schema set edebilirsiniz.

.AsNoTracking()

Bu herkesin gördüğü ancak tam ne olduğuna anlam veremediği bir konu. Bizim sistemde bir örnek üzerinden anlatacağım.

Bizim sistemde Branch tablosu Bank ve City tablolarına bağlı. Bir bankanın bir şehirdeki branch’lerini çekmek istersem. Buradaki tablo ilişkilerinden dolayı bütün object’ler db’ye gitmese bile EF’in iç yapısında birbirine maplenecektir. Bu da demek oluyor ki biz bir branch çekicez kullanmayacağımız model class’ları create oluyor ve ef’in iç mapper’ı ile mapleniyor.

Yukarıdaki query’de alt tarafı bir branch listesi sorgulayacağız. Oluşan object’ler city, branch ve bank model class’larının objeleri.

Mapping reflection bazlı olduğu için zaten ne derece pahalı bir işlem olduğunu bütün developerlar bilir. O yüzden reflection eğer ihtiyaç yoksa kullanılmaz bile.

Bu çok detay olmakla beraber inanılmaz derece runtime süresinde çok çok ciddi fark yaratan bir durumdur. Bunu nerden biliyorsunuz derseniz EF’in kodu open source açtık baktık. Burada ciddi bir çalışma süresi var.

Meraklısı bu koda bakabilir.

https://github.com/aspnet/EntityFramework6/blob/master/src/EntityFramework/ModelConfiguration/Mappers/MappingContext.cs

Bu durumda aşırı dikkat edilmesi gereken bir durum var. AsNoTracking() readonly bir koddur. Bunu kullanırsanız bir sonraki satırda bu tabloyla işlem yapamazsınız!

SaveChanges() Kullanımı

Her zaman SaveChanges işlemini çağrılan web api’nin ya da bir form uygulamasında tıklanan butonun en sonunda yapın. Birden fazla kere çağrılan savechanges gereksiz bir maliyettir. Aralarda SaveChanges çağırmak zorunda kalıyorsanız zaten mimariniz tamamen yanlış demektir. Önce onu bir gözden geçirin derim.

Örnek kullanım aşağıdaki gibi olmalıdır.

Proxy Creation Disable ve Bağlı Tablolalar

Proxy kullanımını kapatmak gerçekten büyük fayda sağlıyor. Daha önce de bahsettiğimiz lazy load işlemini tamamen kapatmak ciddi bir performans kazancı demek. Sırf EF bu dataları getiriyor demek Relational DB mantığından gelmiyor. Bunu bize kolaylık olsun diye yapıyor. Ancak beraberinde çok fazla problem oluşturuyor. Serialize, object mapping, runtime binding gibi bir çok problem yanında hediye geliyor. Bunlarla uğraşmak yerine bir data çekip içerisinde bağlı olduğu kolonlar üzerinden tek tek query çekmek çok çok daha hızlı oluyor. Bunun karşılaştırmasını yapınca %40 civarında bir fark gördün. O günden beri her tablodan ayrı ayrı data çekerek devam ediyorum ve uğraşmam gereken mapper vb. konular tamamen ortadan kalkmış oluyor.

Kapatmak için şunu yapmanız yeterli.

Aşağıdaki gibi dataları tek tek çekmek bağlı olan selection tablosundan virtual object’ler üzerinden ulaşmaya göre %40 daha az maliyetli. Bu kadar bir performans için ben ekstra 3 satır kod yazmayı tercih ediyorum.

Gereksiz kolon kullanımı

Bu benim şahsen takıntım olan bir konu. Herşeyi har diye çekmek kadar saçma bir davranış olamaz. Gerek yoksa neden o datayı çekiyorsun? Gereksiz çekilen her kolon ekstra bir orm, .net ve sql maliyeti demektir. Zaten bu yüzden ne kadar iyi tasarlarsan tasarla, GenericRepository yetmiyor bir şekilde direkt olarak context kullanmaya dönmek zorunda kalıyorsun.

Her data query’lerken bir select ile basit bir şekilde çözülebilecek bir iş. Buna dikkat etmek çok önemli. Çünkü ORM kullanımı dikkat edilmesi gereken bir konu.

Yukarıdaki kullanımdaki gibi basit select’ler ile bu ciddi performans kaybı engellemek çok kolay.

Yanlış kullanılan data kolonları

Bu konu kimsenin ciddiye almadığı bu yüzden entity’nin performansından şikayet eden kişilerin çok olmasının temel 2 sebebinden biridir (diğer lazy load konusu). Bu 2 sorun kombine olunca entity normalde sql’in 1saniyede döneceği query’yi 15 saniye gibi komik sürelerde dönebilir.

Olay şudur. Bir tabloda her zaman nvarchar vb. alanlar bulunur. Bu da model’e translate edilirse string bir alan olur. Böyle bir alanı nasıl kullanırız, böyle;

Burada yanlış bir şey yok nasıl çekecektik diyebilirsiniz. Ama arkada yapılan işlemler öyle demiyor. Eğer modelimizdeki string Name property’si model içerisinde varchar(20) gibi bir şekilde işaretlenmesi gereklidir.

Yapmazsanız ne olur ona bakalım.

SQL Server’a bir trace açarsanız arada şöyle bir işlem geçer.

Type conversion: Seek Plan for CONVERT_IMPLICIT(nvarchar(20), [Extent1].[Name],0)=[@p__linq__0]

Burada SQL nvarchar olarak gelmiş “Kadıköy” değerini varchar’a çeviriyor. Gereksiz casting ve search yapmak için vakit harcıyor. Hatta kolon ismi bile generic şekilde gelerek kolon isimlerde uygun kolonu aramaya çalışıyor. Ne kadar saçma diyebilirsiniz. Bunun açıklaması neden olduğu bir o kadar basit ve mantıklı. .Net unicode çalışır ama SQL değil. Yani yazılan her string .net’te aslında nvarchar gibi çalışır. Bu da sizin farkında olmadan EF ne güzel string dönüyo falan işlerimi görüyor diyebilirsiniz ama maliyeti ne olacak? Yük altında çalışsa ve milyonlarca query yüke girerseniz bunlara harcanan zaman ne olacak? Çözüm basit bunu uygulayarak çok ciddi bir performans kazanabilirsiniz.

Aşırı Generic Query’ler

Çok yaygın olan bir kullanım olan “or” “||” kullanımından dolayı oluşan query’ler vakit kaybı yaratacaktır.

Bu kullanımlar sql’e çok ilginç şekilde translate olur.

Ne yaptığını ne kadar saçma olduğunu görün diye koydum. Bu query o kadar uzuyor ki ekrana sığmadı.

Böyle query’ler hep bize gerekiyor. Bunu da mı yapmayalım diyor olabilirsiniz. Yapın ama böyle yapmayın. Or işlemlerinizi gruplayın. Birden fazla kolon için Or koymayın!

Yukarıdaki query’i gruplarsak şunu yapabiliriz. Bu senaryo için aynı durumu üretmesini biz bu şekilde sağladık. Sizde farklı işler olabilir. Yine de aynı mantığı siz kendi projenizde uyarlayabilirsiniz.

Önemli olan gruplamak!

Buna rağmen günde 20 milyon query alan bir tablonuz olursa bu da yetmeyebilir. Bu durumda tabiki çözüm var. Bir ORM aracı diye herşeyi EF’den beklememek gerekli. Burada elleri azcık kirletip entity’nin eksik feature’larını yazmaya başlayabilirsiniz.

Ve bu şekilde kullanabilrisiniz.

Bu şekilde query’leriniz daha temiz oluşacak ve gereksiz query’ler olmayacak. Bunu EF kendi yapar ama query baktı çok yavaş o zaman yapar. Zaten kaybetmiş olduğunuz bir vakit var bi de EF aa bu yavaş oldu şöyle yapayım diyecek vaktiniz olmayabilir. Bizde yoktu bizde yaptık. 😃

Skip ve Take

Bunlar çok hayat kurtarıcı performans dostu kullanımlar. Ama bilinmesi gereken ufak bir durum var. Evet take yapıyoruz gereksiz olanlar değil sadece ilk 10–20 sizin istediğiniz sayıda geliyor. Bunu dikkat edersek daha da güzel olacak bir kullanım var. O da take ve skip sayılarını önceden hesaplanmış şekilde query’lemek.

Eğer ilk satırdaki işlemi skip işlemi içinde yapsaydık. Bu işi sql yapacaktı. Biz .net’te yaparak buradan bir kazanım elde edebiliriz.

Data Insert

Data insert etmek EF ile kolay ama bulk yaparken o kadar performanslı olmadığını farketmişsinizdir. Bunun sebebi bulk insert mekanizması barındırmaması ve her değişikliği bir transaction olarak ele alıp insert yapmaya çalışmasıdır.

Detect changes özelliğini kapatıp daha sonra liste şeklinde bir ekleme yapılıp değişiklikler kaydedilirse çok daha hızlı bir insert işlemi olacaktır.

Yani bu aşağıdaki query çok pahalı bir query.

İlk kod bloğu yerine 2. blok gibi bir kullanım çok daha hızlı çalışacaktır.

Bunlarla beraber AutoDetectChangesEnabled ayarı kapalıysa çok ciddi bir fark yaratacaktır.

db.Configuration.AutoDetectChangesEnabled = false;

Bu şekilde 85 dakika süren bir yapımız vardı. Bu sayede 9 dakika seviyesine indirdik. Aradaki zaman farkı çok bariz. Kesinlikle tavsiyem bu şekilde kullanılması üzerinedir.

Mega Db Contexts

En büyük problemlerden biri daha öncede bahsettiğim gibi tek bir mega db context kullanımı. EDMX kullanımı yapanlar ya da tek bir context ile yoluna devam edenler, .Net cold start alan bir framework olmasından dolayı uygulama ilk ayağa kalktığında bu kadar büyük bir context’in create edilmesi ciddi bir zaman alacaktır. Onun yerine daha farklı sebeblerden dolayı da contextleri bölmek çok faydalı bir işlemdir.

Gereksiz ve Haberimiz Olmayan EF Query’leri

EF her db’ye gidişinde db’nin versiyonunu sorgulaması gibi bir durum var. Zaten rest çalışıyorsanız disconnected context yaklaşımıyla iş yapıyorsunuz demektir. Bunlar üst üste konunca EF her seferinde db’ye senin versiyonun ne diye sorması gerekiyor. Sürekli db değiştirmiyorsanız ya da tek context farklı db’lere gitmiyorsa -ki çok saçma olurdu- aşağıdaki yapıyı configuration’a ekleyerek her query öncesi db’ye sorulan versiyon sorgusunu ortadan kaldırarak bir performans elde edebilirsiniz.

Genel hatlarıyla bu şekilde ama küçük tavsiyelerim de var.

Disposing

Context’i her işlem sonrası dispose etmek önemli. Bunu using kullanımı ile yapabilirsiniz. Zaten hali hazırda bir Injection yapınız varsa ya da .Net core kullanıyorsanız, request bazlı dispose olan bir yapı yapmanız yeterli olacaktır. Hem memory hemde thread exception’ların önüne geçebilirsiniz.

Multiple result sets

Eğer uygulamanız ve db’niz aynı local area network’de değilse veya aralarında latency çok fazla ise multiple result set özelliğini connection string’e ekleyerek aynı anda birden fazla query getirmesini söyleyebilirsiniz.

Async Kullanın

Async kullanımı ciddi bir fark yaratıyor. Haberiniz olmadan thread’ler kendi işini fazlasıyla iyi yapıyor. ToListAsync(), SaveChangesAsync() kullanımları await kullanımı da dahil sizi şaşırtacak seviyede iyi çalışıyor. Tabi buna girmeden önce await/async konularında ciddi bir araştırma yapılması gerekli.

Vee en son olarak o kadar fazla select işlemi alan bir tablonuz varsa cache’leyin! Biz bankalar ve ürün tablolarımız için Redis kullandık. Bunu çok fazla query alan tablolarımıza gittikçe daha fazla uyguluyoruz. Zaten bu kadar ciddi bir performans sorunu sadece bir cache sunucuyla çözülür. Bizde Redis’i seçtik. Onun da yazısı yakında sizlerle.

--

--

Orhun Begendi
hesapkurdu-development

Senior Enginner, Tech Lead, Hardcore Developer, Software Craftsman.