Gateway uygulaması ile kesintisiz replatform
Kısaca bakım, geliştirme, test ve dağıtım maliyetlerinin yüksek olduğu yazılım projelerini birçoğumuz Legacy olarak tanımlıyoruz. Legacy sistemler bu mesleğin vazgeçilmezleri, malesef bir çok yazılım projesi bir noktadan sonra Legacy olmaktadır.
Tabi bu projeler hepimiz için fırsat niteliğine sahip, bunları yenilerken bir çok teknoloji ve metodoloji kullanarak kişisel ve kurumsal gelişimimize katkı sağlıyoruz. Bu makalede meslekte 8–10 yılını doldurmuş benzer problemlere farklı yaklaşımlar arayanlar için, legacy projelerimizi replatform ederken elde ettiğimiz deneyim ve başvurduğumuz çözümlerden bahsedeceğim.
Sorun Tanımı
Legacy projemiz; Büyük bir çoğunluğu 4–5 yıl önce .NET Framework microservice mimari metodolojisi ile geliştirilmişti. Projenin bir bölümünü yenilemiş ve productiona yüklemiştik. Geri kalan geliştirmeleri production ortamına yüklemek için asgari seviyede risk almak istiyorduk. Diğer bir söylemle “Big Bang”’den kaçınmak istiyorduk.
“The only thing a Big Bang rewrite guarantees is a Big Bang” — Martin Fowler
Varsayalım ki tüm projeyi yeniledik, tüm üst mühendislik detaylarına dikkat ettik ve detaylıca test ettik. Akabinde trafiğin az olduğu saatlerde legacy projeyi kapatıp yeni projeyi devreye aldık. Eğer yeni sistemde öngöremediğimiz bir sorun var ve bunu production ortamında farkedersek legacy projede veri eksikliği olacağı için geri dönüş yapamayız.
“It is a bad plan that cannot be altered.” — Publilius Syrus(85–43 BC)
Örneğin; Kargo paketi oluşturma akışı: Kullanıcı UI’dan paket oluşturma isteği gönderdiğinde, paket senkron(T0 anında) olarak veritabanına kaydedilmeli. Asenkron(T1 anında) olarak paket bilgisi bir mikroservis 1'den zenginleştirilip mikroservis 2'ye göndermeli.
Eğer sorun anında legacy projeye geri dönecek olursak paket verisi elimizde olmayacağı için kargo paketi silme akışı gerçekleştirilemeyecekir. Veri eksikliğinden dolayı da bunun gibi onlarca iş akışı etkilenecektir.
Örnek; Gece yeni projemizi production ortamına yükledik. Gündüz 123456 numaralı paketi yeni sistemde oluşturduk. Birkaç saat sonra yeni projemizde kritik bir bug olduğunu farkettik ve eski projeye geri dönmek durumunda kaldık. 123456 numaralı paket eski sistemde kayıtlı olmadığından mikroservis2 üzerinden sil komutlarıne cevap veremeyecektir.
Bu senaryoda hem gündüz eski sisteme geri dönerken kesinti yaşanmış olup hem de 123456 numaralı paket silinemediğinden veri bozukluğu yaşanmış oldu.
Çözüm
Karmaşık iş akışları ve ihtiyaçları compare toggle gibi çözümlerle yönetilemeyecek kadar büyüktü. İki sistemi de aynı anda çalıştırabileceğimiz bir çözüm bulmamız gerekiyordu…
Bu noktda ekip olarak yaptığımız bir çok münazara akabinde çok basit ve zarif bir çözüm uygulamaya karar verdik; Bir API Gateway geliştirmek ve iki sistemin tüm komutlarını da buraya yönlendirmek. Çizelim;
Eğer kullanıcı paket oluşturma isteğini legacy UI’dan yapıyor ise; API Gateway bu isteği senkron(T0 anında) olarak legacy projeye gönderek işlem başarılı durumunu kontrol edip, asenkron(T1 anında) olarak yeni projeye yönlendirmeli. API Gateway’de yapılaccak çalışma bu kadar basit. Şimdi legacy ve yeni projelerde yapılacak çalışmalara bakalım;
API Gateway iki projeye de gönderdiği isteklerde UI kaynağı eklenmelidir. Örneğin; İstek kaynaği legacy UI ise yeni projenin background job uygulamasında UI kaynak bilgisi kontrol edilip mikroservis 1'e yapılacak çağrı iptal edilmelidir.
Gelen isteğin UI kaynak bilgisine göre birincil çağrının hangi sisteme yapılıp yapılmayacağı kontrol edilip, birincil çağrı senkron olarak yapılıyor. Birincil çağrının yapıldığı proje arkaplan uygulamaları dış sistemleri besliyor. İkincil çağrı ise asenkron yapılıp, bu çağrı içinde sadece kendi veritabanını güncelleyerek herhangi bir dış servise bilgi tekrar gönderilmiyor.
Bu çözüm, iki projenin de aynı anda ayakta olabilmesini, kullanıcıların(trafiğin) yeni projeye küçük sayılarla yönlendirilebilmesini ve kesinti/kayıp oluşturmadan yeni projeye geçiş yapılabilmesine olanak sağladı.
Pratik
Kargo paketi oluşturma örneği üzerinden devam edelim; Legay API’deki controller ve command aşağıdaki gibi görünüyor.
public class CreatePackageCommand: BaseCommand
{
public string Barcode { get; set; }
public string Destination { get; set; }
}[Route("api/[controller]")]
[ApiController]
public class PackageController : Controller
{
private readonly IMicroService1AntiCorruption _microService1AntiCorruption;
private readonly IMicroService2AntiCorruption _microService2AntiCorruption;
private readonly PackageContext _context; public PackageController(PackageContext context, IMicroService1AntiCorruption microService1AntiCorruption,
IMicroService2AntiCorruption microService2AntiCorruption)
{
_microService1AntiCorruption = microService1AntiCorruption;
_microService2AntiCorruption = microService2AntiCorruption;
_context = context;
} [HttpPost]
public async Task<ActionResult> Create(CreatePackageCommand command)
{
var package = new Package(command.Barcode, command.Destination);
_context.Packages.Add(package);
await _context.SaveChangesAsync(); AddCreatePostJob(command); return this.Created($"/packages/{package.Id}", package);
} private void AddCreatePostJob(CreatePackageCommand command)
{
FluentScheduler.JobManager.AddJob(
async () =>
{
var userId = await _microService1AntiCorruption.GetUserId();
await _microService2AntiCorruption.CreatePackage(new RemotePackage(command.Barcode,
command.Destination, userId));
},
schedule => schedule.ToRunOnceAt(DateTime.Now.AddSeconds(1)));
}
BaseCommand’ı aşağıdaki şekilde revize edelim;
public abstract class BaseCommand
{
public const string OurCommandSource = "Legacy"; public string Source { get; set; } public bool IsOurCommand()
{
return Source == OurCommandSource;
}
}
Controller içinde AddCreatePostJob metodunu aşağıdaki gibi revize edelim. Buradaki IsOurCommand if’i ile mikroservis 2'ye birden fazla çağrı yapılmasını engelliyoruz.
private void AddCreatePostJob(CreatePackageCommand command)
{
if (command.IsOurCommand())
{
FluentScheduler.JobManager.AddJob(
async () =>
{
var userId = await _microService1AntiCorruption.GetUserId();
await _microService2AntiCorruption.CreatePackage(new RemotePackage(command.Barcode, command.Destination, userId));
},
schedule => schedule.ToRunOnceAt(DateTime.Now.AddSeconds(1)));
}
}
UI’da sadece “baseAddress” bilgisini değiştirecek derecede Gateway’de tüm Legacy API end point’lerini korumalıyız.
window.baseAddress = 'http://localhost:3000'; // Legacy API base address 'http://localhost:4005';
Gateway tarafında ise controller’ı aşağıdaki gibi düzenleyelim.
[Route("api/[controller]")]
[ApiController]
public class PackageController : ControllerBase
{
private readonly INewApiClient _newApiClient;
private readonly ILegacyApiClient _legacyApiClient;public PackageController(INewApiClient newApiClient, ILegacyApiClient legacyApiClient)
{
_newApiClient = newApiClient;
_legacyApiClient = legacyApiClient;
}[HttpPost]
public async Task<IActionResult> Post(CreatePackageCommand command)
{
command.Source = SourceConsts.Legacy;
var response = await _legacyApiClient.CreatePackage(command); if (!response.IsSuccessStatusCode)
{
return this.StatusCode((int) response.StatusCode, response.Content);
} AddSecondaryCallJob(command); return this.StatusCode((int) response.StatusCode, response.Content);
}private void AddSecondaryCallJob(CreatePackageCommand command)
{
FluentScheduler.JobManager.AddJob(
async () =>
{
await _newApiClient.CreateDelivery(new CreateDeliveryCommand()
{
Barcode = command.Barcode,
Destination = command.Destination,
Source = command.Source
});
},
schedule => schedule.ToRunOnceAt(DateTime.Now.AddSeconds(1)));
}
Gördüğünüz gibi Gateway birincil çağrıyı Legacy API’ye yapıp cevabı başarılı değilse direk gelen cevap bilgisini geri yolluyor. Başarılı ise asenkron(background job) olarak yeni projeye gönderiyor. Yeni proje içinde IsOurCommand if kontrolü ile de mikroservis 2'ye 2. defa paket bilgisinin gönderilmesini engelliyoruz.
Örnek proje: https://github.com/sercanakmaz/replatforming-legacy-projects-example
Teşekkürler
Bu yazıyı yazmamda desteği/yardımı olan, Hepsiburada’daki tüm meslektaşlarıma teşekkür ediyorum.
Vaktinizi ayırdığınız için teşekkürler. Yazı hakkındaki yorumlarınız ve sorularınız için benimle LinkedIn üzerinden iletişime geçebilirsiniz.
Faydalı Linkler
https://randyshoup.silvrback.com/evolutionary-architecture
https://martinfowler.com/articles/microservices.html
https://martinfowler.com/bliki/MicroservicePrerequisites.html
https://shadrin.org/nginx/blog/content/refactoring-a-monolith-into-microservices.html
https://www.nginx.com/blog/building-microservices-using-an-api-gateway/