Web Sitesini Yeniden Başlatmadan Konfigdeki Değişiklikler İle Yeniden Yükleme
Selamlar,
Bu makalede her yazılımcının ihtiyaç duyabileceği, “appsettings.json” dosyasının ya da herhangi bir config dosyasının değişmesi durumunda, bu konfigürasyon dosyasından yapılanan tüm nesnelerin IIS restart ya da Application restart olmadan, tekrardan nasıl ayağa kaldırılabileceğini tartışacağız. Özellikle yüksek trafik alan yapılarda, clientların düşmesi en istenmeyen durumlardan biridir.
Örnek amaçlı, Redis bir servisin connection bilgilerini config dosyadan okuyup ilgili konfigürasyon dosyanın değişmesi durumunda, Redis Client’ın dinamik bir şekilde tekrardan güncel config bilgileri ile ayağa kaldırılmasını sağlıyalım.
appsettings.json: Redis icin ayarlanmis, local config.
1 2 3 4 5 6 |
"RedisConfig": { "RedisEndPoint": "127.0.0.1", "RedisPort": "6380", "RedisPassword": "*******", "RedisExpireTime": 60 //1 Saat }, |
Redis’de Saklanacak FuelData: Değişen yakıt bilgisi, Redis’de tutulacaktır.
1 2 3 4 5 6 7 8 9 10 11 |
public class FuelData { public FuelData() { } public DateTime date { get; set; } public string name { get; set; } public double price { get; set; } public double prevPrice { get; set; } public bool isUp { get; set; } } |
Nasıl gördüğünü değiştir, nasıl değiştiğini gör! ―Buddha
program.cs: ConfigurationBuilder objesine, “appsettings.json” dosyası aşağıdaki gibi tanımlanmıştır.
1 2 3 4 |
var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); |
Öncelikle gelin, yeni bir RedisCache servis yazalım.
1 2 3 4 5 6 7 8 |
public interface IRedisCacheService { T Get<T>(string key); void Set(string key, object data); void Set(string key, object data, DateTime time); void Remove(string key); void Clear(); } |
1. Uzun Yöntem
RedisCacheService.cs: Redis için “ServiceStack” .Net kütüphanesini kullanacağız. Aşağıda görüldüğü gibi RedisCacheService(), Constructor’ında “_configuration.GetSection” methodu ile appsettings.json dosyasındaki Redis Config kayıtları anlık okunur. Test amaçlı Redis Port’u, config dosyadan “6380” olarak değiştirildiğinde, Redis’e kayıt atılamadığını ama “6379” olarak değiştirilip IIS Restart edilmeden kaydedildiğinde, kayıt atılabildiği görülmüştür.
- “_configuration.GetSection(“RedisConfig”)[“RedisEndPoint”]”: appsettings.json üzerindeki RedisConfig / RedisEndPoint, doğrudan okunmaktadır.
- “public T Get<T>(string key) { try { using (IRedisClient client = new RedisClient(conf))” : Redis’den her get işlemi yapıldığında, Scoped olarak güncel conf(RedisEndPoint) bilgileri ile RedisCleint ayağa kaldırılır. Remove() ve Set() için de aynı durumlar geçerlidir.
- “PreserveReferencesHandling = PreserveReferencesHandling.Objects”: StackOverflowException önlemek için kullanılan, propertysinde object olan bir başka object’i (Inner Object Case), seri hale getirmek için kullanılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
public class RedisCacheService : IRedisCacheService { IConfiguration _configuration = null; private RedisEndpoint conf = null; public RedisCacheService(IConfiguration configuration) { _configuration = configuration; conf = new RedisEndpoint { Host = _configuration.GetSection("RedisConfig")["RedisEndPoint"], Port = Convert.ToInt32(_configuration.GetSection("RedisConfig")["RedisPort"]), Password = _configuration.GetSection("RedisConfig")["RedisPassword"]}; } public void Clear() { throw new NotImplementedException(); } public T Get<T>(string key) { try { using (IRedisClient client = new RedisClient(conf)) { return client.Get<T>(key); } } catch { throw new RedisNotAvailableException(); //return default; } } public void Remove(string key) { try { using (IRedisClient client = new RedisClient(conf)) { client.Remove(key); } } catch { throw new RedisNotAvailableException(); } } public void Set(string key, object data, DateTime time) { try { using (IRedisClient client = new RedisClient(conf)) { var dataSerialize = JsonConvert.SerializeObject(data, Newtonsoft.Json.Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects }); client.Set(key, Encoding.UTF8.GetBytes(dataSerialize), time); } } catch { throw new RedisNotAvailableException(); } } public void Set(string key, object data) { Set(key, data, DateTime.Now.AddMinutes(int.Parse(_configuration.GetSection("RedisConfig")["RedisExpireTime"]))); } } |
Ayrıca, RedisCache services’in Constructor’ının her seferinde çağrılabilmesi için, program.cs’de RedisCacheService, genelde performans amaçlı tercih edilen “AddSingleton” yerine “AddScoped” olarak eklenmesi gerekmektedir. Böylece RedisConfig, her seferinde appsettings.json’daki değere göre tekrardan yapılanacaktır.
program.cs:
//builder.Services.AddSingleton<IRedisCacheService, RedisCacheService>();
builder.Services.AddScoped<IRedisCacheService, RedisCacheService>();
Dünyayı değiştiremiyorsan, dünyanı değiştirirsin. Hepsi bu. ―Stefan Zweig
2. Yöntem: Configde Sınıf Kullanımı
Bu yöntemde config dosyasındaki ilgili Section’ın karşılığı, aşağıda tanımlanan RedisConfig sınıfına maplenecektir. Daha sonra projenin geri kalanında, config dosyası yerine artık bu sınıf ile çalışılacaktır.
RedisConfig: İlgili config dosyası için maplenecek sınıf, aşağıdaki gibidir.
1 2 3 4 5 6 7 8 |
public class RedisConfig { public RedisConfig() { } public string RedisEndPoint { get; set; } public string RedisPort { get; set; } public string RedisPassword { get; set; } public int RedisExpireTime { get; set; } } |
program.cs: Aşağıdaki tanımlamada Configuration dosyasina tanimlanan “appsettings.json” dosyası “reloadOnChanges” property’si true atanarak, dosyada değişim olması durumunda ilgili sınıfın tekrardan maplenmesi ve ilgili config dosyasının son güncel halinin sınıfa atanması sağlanmıştır.
“builder.Services.Configure<RedisConfig>(configuration.GetSection(“RedisConfig”))” : Tanimlamasi ile appsettings.json’daki RedisConfig section’ı, RedisConfig class’ına maplenmiştir. Genelde section ismi ile sınıfın isminin, kod okunaklığını arttırmak adına aynı olmasına dikkat edin.
1 2 3 4 5 6 |
var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", false, true) .Build(); builder.Services.Configure<RedisConfig>(configuration.GetSection("RedisConfig")); |
Değişen Configde Sınıf Kullanım Şekli IOptionsSnapshot:
RedisCacheService.cs: Aşağıda görüldüğü gibi, RedisConfig “IOptionsSnapshot” keyword’ü ile tanımlanmıştır. Bu keyword, Scoped ya da Transient olarak eklenmiş Dependency serviceslerinde kullanılmaktadır. Amaci, değişen config file’ı maplendiği sınıfa tekrardan setlemektir. Böylece örneğin Redis Port’u config dosyadan “6380” olarak değiştirildiğinde, Redis’e kayıt atılamadığını ama “6379” olarak değiştirilip IIS Restart edilmeden kaydedildiğinde, ilgili “RedisConfig” sınıfı kullanılarak RedisCacheDB’ye kayıt atılabildiği görülmüştür.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class RedisCacheService : IRedisCacheService { public readonly IOptionsSnapshot<RedisConfig> _config; IConfiguration _configuration = null; private RedisEndpoint conf = null; public RedisCacheService(IOptionsSnapshot<RedisConfig> config) { _config = config; conf = new RedisEndpoint { Host = _config.Value.RedisEndPoint, Port = Convert.ToInt32(_config.Value.RedisPort), Password = _config.Value.RedisPassword }; . . . |
program.cs: Gene ayni şekilde RedisCacheService dependency injectionda Scoped olarak eklenmesi gerekmektedir. Çünkü ilgili servisin her seferinde yeniden yaratılması ve Constructor’ı çağrılarak, RedisEndPoint’in her seferinde oluşturulması sağlanmıştır. Böylece güncel connection string değeri, her requestde alınmış olunur.
builder.Services.AddScoped<IRedisCacheService, RedisCacheService>();
3. Yöntem IOptionsMonitor: İlgile RedisService’in Singleton Olarak Eklenme Durumunda, Config Değiştiğinde IIS Restart Edilmeden RedisService’i Yapılandırma
Normalde Redis, RabbitMQ, Kafka gibi servisler projeye, performansı amaçlı bir kere eklenir. Yani Singleton. İşte bu durumda config değiştiği zaman, IIS Restart olmadan ilgili objelerin tekrardan yapılanması için Constructor haricinde de çağrılacak bir methodun olması gerekir. Çünkü singleton bir servisde, Constructor sadece bir kere çağrılır ve örneğin bu projede Redis’in tekrardan yeni connection string ile oluşturulması imkansız bir hal alır.
program.cs: Aşağıdaki RedisCacheService’de RedisConfig, “IOptionsMonitor” keyword’ü ile tanımlanmıştır. Bu keyword sadece Singleton olarak eklenmiş Dependency serviceslerinde kullanılmaktadır.
Bu seneryada Redis OnPrem local bir makinada çalışırken, ilgili config değişikliğine gidilerek IIS Restart edilmeden ilgili FuelData’nin, Cloud Azure Redis servise atılması sağlanmıştır.
RedisConfig.cs: Öncelikle, RedisConfig Class’a aşağıdaki “IsSsl” boolean propertysi, Azure connection için eklenmiştir.
1 2 3 4 5 6 7 8 9 |
public class RedisConfig { public RedisConfig() { } public string RedisEndPoint { get; set; } public string RedisPort { get; set; } public string RedisPassword { get; set; } public int RedisExpireTime { get; set; } public bool IsSsl { get; set; } } |
Değişim istiyorsanız, sebeplerini yaratın. ―Dalai Lama
appsettings.json: Azure için gerekli “IsSsl” field’ı, config dosyaya aşağıdaki gibi eklenmiştir. Ayrica Azure Redis icin gerekli connection string ve password tanimlanmistir.
1 2 3 4 5 6 7 8 9 |
"RedisConfig": { "RedisEndPoint": "127.0.0.1", //"RedisEndPoint": "bkasmer.redis.cache.windows.net", "RedisPort": "6380", "RedisPassword": "**********", "RedisExpireTime": 60, //1 Saat "IsSsl": false }, |
- “public readonly IOptionsMonitor<RedisConfig> _config” : IOptionsMonitor olarak tanimlanan RedisConfig.
- “conf = _config.CurrentValue.IsSsl ?”: Eğer Redis, Azure üzerinde çalıştırılacak ise, IsSsl true değerini almaktadır. Bune göre Azure config’e, uygun bir connection string seçilmektedir.
- “conf = new RedisEndpoint { Host = _config.CurrentValue.RedisEndPoint, Port = Convert.ToInt32(_config.CurrentValue.RedisPort), Password = _config.CurrentValue.RedisPassword }”: RedisEndpoint sınıfı, appsettings.json’daki “RedisEndpoint” sectionındaki local değerler ile dolmakta ve RedisEndpoint’i generate etmektedir.
- new RedisEndpoint { Host = _config.CurrentValue.RedisEndPoint, Port = Convert.ToInt32(_config.CurrentValue.RedisPort), Password = _config.CurrentValue.RedisPassword, Ssl = true, SslProtocols = System.Security.Authentication.SslProtocols.Tls12 } : Eğer Redis, Azure üzerinde çalıştırılacak ise Ssl propertyleri ile atanması gerekmektedir.
- “_config.OnChange(Listener)” : RedisCacheService sınıfının Constructore’ı, Singleton tanımlamadan ötürü sadece bir kere çalışacaktır. İşte bu durumda devreye IOtionsMonitor’unun “OnChange()” methodu devreye girmektedir. İlgili config dosya değişince, trigger edilecek method burada tanımlanır. Yani bu örnekde appsettings.json değişince, “Listener()” methodu tetiklenecek ve böylece constructor bir kere devreye girse de, ilgili method her config değişiminde devreye girecek ve güncel connection string ile RedisEndpoint’i yeniden oluşturacaktır.
- “private void Listener(RedisConfig _config)”: appsettings.json’ın değişmesi ile, RedisConfig sınıfı değişmiş ve Constructor yeniden tetiklenmese bile, bu değişimden dolayı Listen() methodu trigger edilmiş ve yeni config bilgisine göre RedisEndpoint tekrardan oluşturulmuştur. İşte bu durumda, IIS Restart olmadan redis bilgisi değişen appsettings.json’a göre güncellenmiştir.
Böylece en optimum çözüm olarak, performans amaçlı proje ilk ayağa kaldırılırken tek seferlik yani singleton olarak eklenen RedisCacheService, önce config dosyadan “local” Redis olarak atanmıştır. Daha sonra config connection string Azure olarak değiştirilip IIS Restart edilmeden kaydedildiğinde, Listener() methodunun tetiklenerek yeni RedisEndpoint’in oluştuğu ve ilgili FuelData’nın artık local Redis’e değil, Azure’da tanımlanmış Redis’e atıldığı görülmüştür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class RedisCacheService : IRedisCacheService { public readonly IOptionsMonitor<RedisConfig> _config; IConfiguration _configuration = null; private RedisEndpoint conf = null; public RedisCacheService(IOptionsMonitor<RedisConfig> config) { _config = config; conf = _config.CurrentValue.IsSsl ? new RedisEndpoint { Host = _config.CurrentValue.RedisEndPoint, Port = Convert.ToInt32(_config.CurrentValue.RedisPort), Password = _config.CurrentValue.RedisPassword, Ssl = true, SslProtocols = System.Security.Authentication.SslProtocols.Tls12 } : new RedisEndpoint { Host = _config.CurrentValue.RedisEndPoint, Port = Convert.ToInt32(_config.CurrentValue.RedisPort), Password = _config.CurrentValue.RedisPassword }; _config.OnChange(Listener); } private void Listener(RedisConfig _config) { conf = _config.IsSsl ? new RedisEndpoint { Host = _config.RedisEndPoint, Port = Convert.ToInt32(_config.RedisPort), Password = _config.RedisPassword, Ssl = true, SslProtocols = System.Security.Authentication.SslProtocols.Tls12 } : new RedisEndpoint { Host = _config.RedisEndPoint, Port = Convert.ToInt32(_config.RedisPort), Password = _config.RedisPassword }; } . . . |
Yüksek trafik alan servislerde, en son istenen şeylerden biri de Clientların düşmesidir. Config dosyalar, bir projede tüm akışına müdahele edebilir. Örneğin DB üzerinde açılacak Max Connection Count ya da bir Queue’ya eklenecek Max Item sayısı, Connection Stringler, Debug Mode(on,off), Audit Log(on,off), Time Zone ve daha birçoğu. İşte tüm bu değişimlerin çalışma anında IIS Restart etmeden yani clientları düşürmeden yapmak, yüksek trafikli sitelerde hayati bir önem taşımaktadır. İşte biz bu makalede, bunu mümkün kılmanın yollarını aradık.
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Source Code: https://github.com/borakasmer/OptionConfig
public class FuelData
{
public FuelData()
{
}
public DateTime date { get; set; }
public string name { get; set; }
public double price { get; set; }
public double prevPrice { get; set; }
public bool isUp { get; set; }
}
bu kisimda neden properties isimleri kucuk harfle? standart olarak buyuk harfle yazmaniz doghru degil mi?