Nginx, Redis, .NET Core İle Distributed Session Yönetimi

Başlığı seçerken oldukça zorlandım — hala içime sinmiş de değil tabii — . Bir paragraf altında kısaca özetlemek gerekirse; Nginx arkasında hizmet eden N tane .NET Core web uygulamasında “session” yönetimi sorunsalına çözüm üretmeye çalışmak gibi bir derdim var.

Yazının tamamında kullanılacak teknolojiler;

  • Redis (oturum bilgilerini tutacak)
  • Nginx (load-balancing işleminden sorumlu olacak)
  • .NET Core (merkez uygulama görevi görecek)

Sorun çözümüne dair tüm deneme-yanılma işlemlerini ise Docker üzerinde gerçekleştireceğiz.

Nginx

Topolojide load-balancer görevini üstleniyor. Buna göre gelen isteklerin tamamı Nginx tarafında karşılanacak ve varsayılan olarak tanımlanmış round-robin yöntemi ile istekler back-end uygulamalarına yönlendirilecek.

Round-robin dışında iki yöntem daha Nginx tarafından sunuluyor.

  • least-connected yöntemi ile, gelen istek o anda aktif bağlantı sayısı en az olan sunucuya yönlendiriliyor.
  • ip-hash yöntemi kullanılarak ise istemcinin IP bilgisine göre çalıştırılan bir hash fonksiyonu yardımıyla istekler yönlendiriliyor.

Redis

Redis, en temel tanımıyla anahtar-değer (key-value) mantığıyla çalışan bir cache uygulaması.

Topolojideki asıl görevi oturum bilgilerini üzerinde tutmak. Bununla birlikte örnek uygulamada da görüleceği üzere cache yönetimi için de kullanılacak.

.NET Core

Container teknolojileri ile tam uyumlu çalışması, platform bağımsız ve açık kaynak kodlu oluşu, .NET gibi temelleri sağlam bir mimari temel alınarak oluşturulması; kısa süre içerisinde .NET Core’u geliştiricilerin ilgi odağına yerleştirdi. Biz de örneğimizde bu teknolojyi merkeze alacağız.

.NET Core web uygulaması; Nginx tarafından yönlendirilen istekleri karşılamaktan ve Redis tarafında tutulan oturum bilgilerini yönetmekten sorumlu olacak.

Özetle aşağıdaki gibi bir topolojiyi canlandırmaya çalışacağız:

Yazının sonunda örnek uygulamanın yer aldığı Github repo linkini paylaştım. Dolayısıyla yazının devamında uygulamadaki can alıcı noktaları kod alıntısı şeklinde paylaşacağım. İçerikte kopma sezdiğiniz noktalarda örnek uygulamaya yönlenmenizi öneriyorum.

Örnekte, .NET Core uygulamasının 3 kopyasını çalıştıracağız. Bununla birlikte 1 Nginx, 1 tane de Redis kopyasına ihtiyaç duyuyoruz. Bu kopyaların yönetimi için Docker Compose kullanacağız. Bu iş için oluşturulan docker-compose.yml dosyası ise aşağıdaki gibi:

Bağımlılık tanımlamalarına (depends_on) göre ortamın çalışır vaziyete gelmesi için öncelikle sessiondb, sonrasında farmexampleapp_1, farmexampleapp_2, farmexampleapp_3 ve proxy sağlıklı bir şekilde ayağa kalkmalıdır.

Web uygulamalarına X_BACKEND_SERVER adlı bir ortam değişkeni tanımlıyoruz. Böylece Nginx üzerinden yaptığımız isteklerin hangi uygulamalara düştüğünü göreceğiz.

Ayrıca ASPNETCORE_URLS ortam değişkeni ile uygulamaların hizmet edeceği portları belirliyoruz.

Sırasıyla konfigürasyon dosyasında geçen uygulamalara göz atalım:

proxy

Temel nginx.conf dosyasına upstream anahtar kelimesiyle back-end uygulamalarımızı tanıtıyoruz. Böylece, gelen istekler bu parantez içerisindeki sunuculara dağıtılacak.

Kendimize özel tanımladığımız konfigürasyon dosyası ise şu şekilde:

8080 portuna gelen istekler doğrudan, tanımladığımız upstream’a yönlenecek.

farmexampleapp_1, farmexampleapp_2, farmexampleapp_3

Tabiri caizse dananın kuyruğunun koptuğu yer tam da burası.

Yazının temeli olan sorunun çözümü üzerine örnek uygulamayı yaparken yaşadığım sorundan bahsetmekte fayda var. Tüm ortamları sağlıklı bir şekilde ayağa kaldırıp localhost:8080/Home/SetSession adresine gittiğimde session koleksiyonuna istediğim değer sağlıklı bir şekilde atılıyordu. Redis Desktop Manager uygulamasından localhost:6379 adresine bağlandığımda da oturumun başarıyla oluştuğunu görüyordum ancak localhost:8080/Home/GetSession adresine gidip test yaptığımda farkettim ki oturum nesnesi bir sunucu için oluşurken diğerinde nesneye ulaşamıyordum:

Sonrasında bir süre araştırdım. Bulduğum kaynağa göre oturum çerezinin diğer bir makineden okunabilmesi için DataProtection ayarının yapılması gerektiğini öğrendim. Aslında bu yöntem web farm içerisindeki ASP.NET MVC uygulamaların web.config’lerinde aynı machine key değerinin tutulması yönteminden farksız değil. Amaç, korumalı olan çerezin okunabilmesi için aynı encryption/decryption değerlerinin kullanılabilmesi.

Bu yönlendirmenin üzerine ise Startup.cs dosyasındaki ConfigureServices fonksiyonu içerisine aşağıdaki satırları ekledim:

SetApplicationName, her bir uygulama için aynı olacak şekilde — değerin ne olduğu önemsiz — ayarlanıyor. Böylece korumalı değerler, birden fazla uygulama üzerinden kullanılabiliyor. Sonrasında ise PersistKeysToRedis fonksiyonu ile anahtarın (machine key değeri gibi düşünebilirsiniz) Redis üzerinde saklanması için gerekli ayarı yaptım. İlk parametrede bir Redis bağlantısı, ikincisinde ise string tipinde bir anahtar değeri istiyor. Lokal ortamda ilk parametreyi localhost:6379, ikinci parametreyi ise istediğiniz bir değer gibi düşünebilirsiniz.

Eğer uygulamalarınızı production ortamında da Docker üzerinde konumlandırıyorsanız, bu tür kritik değerleri ya volume mapping (-v komutu) ile container dışında ya da Redis, SQL gibi yerleşik sunucularda tutmanız tavsiye ediliyor.

Bir sonraki satırda ise oturum bilgilerinin Redis üzerinde tutulması için gerekli ayarı yaptım:

Burada da InstanceName özelliğine metinsel bir değer, Configuration özelliğine ise Redis bağlantı cümlesini (localhost:6379) yazmanız yeterli olacaktır.

Son ve en önemli adımda, Configure fonksiyonu içerisine app.UseSession(); satırını eklemeyi unutmayın! Aksi durumda uygulama genelinde oturum yöntemi kullanılamayacaktır.

Uygulamayı tekrar çalıştırıp (docker-compose up -d) gözlemlediğimde ise sorunun çözüldüğünü görmek beni benden aldı :)

Örnek uygulamanın tamamına aşağıdaki linkten ulaşabilirsiniz. Bir sonraki çözüm yöntemine kadar, oturumlarınız hep ulaşılabilir olsun (özellikle sepet gibi maddi boyutu yüksek ise :).