İçeriğe geç

Kubernetes Üzerindeki .NET Core Uygulamalarının OpenTracing API ile Distributed Tracing İşlemleri

Distributed tracing, microservice architecture’ı olarak tasarladığımız sistem içerisindeki uygulamalarımızın, nerede performans problemi yaşadığını belirleyebilmemiz ve monitor edebilmemiz için harika bir method.

Bir başka değişle, hangi request nereye gidiyor, uçtan uca bir request ne kadar zaman harcıyor gibi sorulara cevap alabilmemiz için implemente etmemiz gereken bir method.

Bu makale kapsamında ise, OpenTracing APIını ve Jaeger tracer’ını kullanarak .NET Core ile geliştirdiğimiz kubernetes üzerindeki microservice’lerin, distributed tracing işlemlerine değineceğiz.

Senaryo

Bir e-ticaret sistemi üzerinde çalıştığımızı ve uygulamalarımızı kubernetes üzerinde host ettiğimizi düşünelim. Kullanıcılar ile ilgili işlemlerden sorumlu bir User API‘ımız var. Yeni bir kullanıcı sisteme kayıt olduğunda ise, “UserRegisteredEvent” adında bir event publish ediliyor. Publish edilen bu event’in subscriber’larından birisi ise, kullanıcıya hesabını aktifleştirebilmesi için e-posta göndermekle sorumlu bir service.

Biz ise asenkron olarak gerçekleşen bu kullanıcı kayıt journey’inin, OpenTracing API ve Jaeger tracer’ını implemente ederek, kubernetes üzerinde uçtan uca izleme işlemini gerçekleştireceğiz.

OpenTracing ve Jaeger Nedir?

Kısaca sizlere OpenTracing ve Jaeger hakkında bilgi vermek istiyorum. OpenTracing, herhangi bir vendor’a bağımlı olmadan uygulamalarımıza distributed tracing için bir instrumentation ekleyebilmemizi sağlayan bir specification’dır. Tıpkı, OpenAPI gibi.

Jaeger ise Uber Technologies tarafından geliştirilmiş, OpenTracing standart’larını destekleyen ve microservice mimarimiz üzerinde distributed tracing işlemlerini yapabilmemizi sağlayan harika bir tracer’dır. Jaeger hakkında daha detaylı bilgiye ise, buradan ulaşabilirsiniz.

Jaeger‘in kubernetes üzerine kurulumu için, şuradaki dokümanları takip edebilirsiniz. Ben bu makale için, development setup’ını uyguladım.

Jaeger genel hatlarıyla, “Agent“, “Collector” ve “Query” den oluşmaktadır. Agent, UDP üzerinden kendisine gelen trace verilerini dinleyip, collector’e ileten bir network daemon’ıdır. Trace verileri ise “Span” olarak adlandırılmaktadır. Collector ise kendisine iletilen trace verilerini, bir pipeline (validations, indexes, transformations) içerisinde işlemektedir. Daha sonra ise seçtiğiniz bir component (Elasticsearch, Cassandra ve Kafka) türüne göre store etmektedir.

Query ise isminden de anlaşılabileceği üzere, ilgili trace sonuçlarını sorgulayabileceğimiz bir UI.

Peki, Haydi Biraz Kodlayalım!

Kodlamaya başlamadan önce platform için sahip olmamız gereken bazı tool’lar:

  • Message Broker (ben RabbitMQ kullanacağım)
  • Docker
  • ve Kuberentes

NOT: Ana konumuz platformu oluşturmak olmadığı için, ben kurulum konuları üzerinde durmayacağım.

İlk olarak kullanıcıların sisteme kayıt olabilmelerini sağlayacak olan User API‘ını develop edelim. Bunun için öncelikle aşağıdaki gibi bir proje oluşturalım.

Ardından “User.Common.Contracts” isminde bir class library oluşturalım.

Bu library içerisinde uygulamalarımız arasında share edeceğimiz contract’ları tanımlayacağız. Şimdi yeni bir kullanıcı sisteme kayıt olduğunda publish edeceğimiz event’i, aşağıdaki gibi oluşturalım.

Oluşturma işleminin ardından, “User.Common.Contracts” library’sini, az önce oluşturmuş olduğumuz “User.API” projesine referans olarak ekleyelim.

Şimdi “User.API” projesi içerisinde “Models” isminde bir klasör oluşturalım ve ardından içerisine “Requests” ve “Responses” klasörlerini de oluşturalım.

Requests” klasörü içerisine kullanıcının kayıt olurken kullanacağı modeli aşağıdaki gibi tanımlayalım.

Responses” klasörü içerisine ise, internal response wrapper class’ını tanımlayalım.

Şimdi ise “Services” isminde bir klasör oluşturalım “User.API” projesi içerisinde. Ardından içerisine aşağıdaki gibi “IUserService” isminde bir interface tanımlayalım.

Kullanıcı ile ilgili business logic’leri, bu service aracılığı ile gerçekleştireceğiz.

Bu noktada messaging ile ilgili işlemleri reliable bir şekilde gerçekleştirebilmemiz için projemize NuGet üzerinden bir service bus ekleyeceğiz. Ben MassTransit‘in lightweight bir wrapper’ı olan MetroBus library’sini kullanacağım.

Ardından distributed tracing işlemlerini gerçekleştirebilmemiz için ise OpenTracing ve Jaeger package’larını eklememiz gerekmektedir.

Şimdi bir klasöre daha ihtiyacımız var. Ben structured klasör yapılarını seviyorum. Her neyse, “Services” klasörü altında, “Implementations” adında bir klasör oluşturalım ve içerisinde “IUserService” interface’ini aşağıdaki gibi implemente edelim.

Service içerisinde kısaca neler yaptık bir bakalım.

Trace context’inin diğer service’lere otomatik olarak propagate edilmesi Jaeger tarafından zaten otomatik olarak sağlanıyor. Yani api-to-api communication’ı olduğunda ve gerekli configuration’ı yaptığınızda, bir request’i uçtan uca izleyebiliyorsunuz.

Biz bu noktada ise api-to-subscriber olarak event-based bir communication gerçekleştirdiğimiz için, trace context’inin propagation işlemini manuel olarak kendimiz sağladık. Trace scope’u içerisine bakarsak, “create-user-async” isminde bir span oluşturduk. Sonrasında ise client olduğuna dair bir tag ekledik. Tag’leri kullanarak span’a additional metadata’lar ekleyebilmek mümkündür. Ardından ise “TextMapInjectAdapter” aracılığı ile ilgili trace context’ini dictionary’e inject ettik.

Kullanıcının sisteme kayıt olabilme işlemlerini de tamamlayarak, tracing key’leri ile birlikte “UserRegisteredEvent” ini service bus aracılığı ile queue’ya publish ettik. Bu noktadan sonra ilgili event’i her kim consume ederse, tracing key’lerini kullandığı sürece tüm akışı gözlemleyebileceğiz.

Artık controller’ı oluşturabiliriz. Aşağıdaki gibi “UsersController” isminde bir controller oluşturalım.

Controller içerisinde ise, oluşturduğumuz “IUserService” interface’i üzerinden kullanıcının kayıt işlemlerini gerçekleştiriyoruz.

Şimdi “Startup” class’ını açalım ve ilgili service injection işlemlerini aşağıdaki gibi gerçekleştirelim.

Service bus’ı, RabbitMQ kullanarak initialize ettik ve ardından injection işlemini gerçekleştirdik. Daha sonra ise tracer’ı configure ederek inject ettik. Tracer’ı yapılandırırken ben sampling type’ı olarak, “const” sampler’ı kullandım. Diğer seçebileceğiniz sampling seçenekleri ise “Probabilistic“, “Rate Limiting” ve “Remote” şeklinde. Daha detaylı sampling bilgilerine ise buradan erişebilirsiniz. Agent host bilgisini de kubernetes ortamınızdaki node IP‘si ile değiştirebilirsiniz. Eğer agent kurulumunu sidecar olarak gerçekleştirirseniz de, herhangi bir bilgi set etmenize gerek olmayacaktır. Default bilgilerle erişim sağlayacaktır.

API artık hazır durumda. Senaryomuza tekrar dönelim. Kullanıcı sisteme kayıt olduktan sonra bir event publish edecektik. Daha sonra ise kullanıcının hesabını aktifleştirebilmesi için o event’e subscribe olmuş bir aktivasyon e-posta’sı gönderen service oluşturacaktık.

Şimdi event’i publish ettik ve artık kullanıcıya aktivasyon e-posta’sını gönderecek olan service’i kodlamaya başlayabiliriz.

Subscriber’ın Kodlanması

Bunun için yeni bir .NET Core console application’ı oluşturalım.

Oluşturmanın ardından shared library olan “User.Common.Contracts” projesini referans olarak ekleyelim. Daha sonra ise NuGet üzerinden MetroBus‘ı, OpenTracing‘i ve Jaeger’i projeye aşağıdaki gibi dahil edelim.

Console uygulaması, deamon olarak çalışacak bir background service olacak. App startup ve lifetime management’ını yapabilmemiz için ise NuGet üzerinden “Microsoft.Extensions.Hosting” ve “Microsoft.Extensions.DependencyInjection” paketlerini de projeye dahil edelim.

Ayrıca configuration yönetimini gerçekleştirebilmemiz içinde “Microsoft.Extensions.Configuration” ve “Microsoft.Extensions.Configuration.Json” paketlerini de dahil edelim.
İlk olarak “Common” adında bir klasör ve içerisine aşağıdaki gibi “TracingExtension” adında bir class ekleyelim.

API tarafında hatırlarsak, tracing key’lerini event içerisinde publish ederek trace context’inin propagation işlemini manuel olarak gerçekleştirmiştik. Şimdi ise consumer içerisinde span oluşturmak istediğimiz bir noktada, aynı tracing key’lerini context’e extract ederek “TracingExtension” class’ı vasıtasıyla gerçekleştireceğiz.

Peki, şimdi ise root dizinde “Consumers” adında yeni bir klasör daha oluşturalım ve içerisine aşağıdaki gibi “UserActivationConsumer” adında bir class ekleyelim.
-> User.Activation.Consumer.Common
—> Common
—> Consumers

Bu noktada, daha önce API içerisinden publish ettiğimiz “UserRegisteredEvent” model’ine subscribe işlemini gerçekleştiriyoruz. Ardından “ITracer” interface’ini inject ediyoruz.

Consume” method’u içerisinde ise, trace context’inin propagation işlemini gerçekleştirebilmemiz için oluşturmuş olduğumuz “TracingExtension” class’ını kullanarak bir scope oluşturuyoruz. Propagate edilmiş trace context’li “user-activation-link-sender-consumer” scope’u sayesinde, artık yaptığımız işlemleri api-to-subscriber olarak trace edebileceğiz.

Bu service bir background service’i olarak çalışacağı için, şimdi root dizine dönelim ve “Services/Implementations” klasörlerini oluşturalım. Ardından “Implementations” klasörü altında “BusService” adında bir class oluşturalım ve aşağıdaki gibi implemente edelim.

Implementation sırasında yaptığımız tek şey, bus’ı start ve stop etmek.

Program” class’ını ise aşağıdaki gibi düzenleyelim.

Sanırım yukarıda yaptıklarımız yeterince açık ve net. Configuration’ı ve dependency injection’ı configure ederek, service’lerimizi inject ediyoruz. Ayrıca trace’i ise, “const” type’ı ile ve “User.Activation.Consumer” adıyla initialize ediyoruz.

Consumer’ı ise, “user.activation.queue” adında bir queue ile register ediyoruz. Bu queue ile “UserRegisteredEvent” model’ine subscribe olacaktır.

Deployment

Artık uygulamaları deploy etmeye hazırız. Ben deployment işlemlerini gerçekleştirebilmek için basit bir Docker file ve Helm chart hazırladım. Bu chart ile, uygulamaları Azure Kubernetes Service üzerine deploy edeceğim. Siz kendi environment’ınıza göre chart’ı değiştirebilirsiniz.

Hazırlamış olduğum chart ve docker file’a, buradan erişebilirsiniz.

Test

Şimdi test aşamasına geçebiliriz. Öncelikle sistemde yeni bir kullanıcı oluşturabilmek için aşağıda olduğu gibi “api/users” endpoint’ine bir POST request’i gönderelim.

Bu request ile kullanıcı kayıt journey’ini başlatmış olduk. Senaryomuzda olduğu gibi, kullanıcı kayıt işlemi gerçekleştikten sonra bir event publish edildi.

Event’in publish edilmenin ardından event’e subscribe etmiş olduğumuz aktivasyon e-posta’sını gönderecek olan service (user-activation-consumer) ilgili işlemini gerçekleştirmiştir.

Peki bu süreçte neler oldu, haydi Jaeger üzerinden bir bakalım.

Jaeger üzerindeki akışa bakarsak, bu işlem 4 derinliğe ve 5 adet span’a sahip. Toplam süreç ise 29.54ms sürmüş. Post işleminin kırılımlara bakarsak ise, request ilgili action’dan geçtikten sonra “create-user-async” method’una geliyor ve içerisinde kullanıcı kayıt işlemleri gerçekleştiriliyor. Ardından ise asenkron olarak aktivasyon link’i gönderme işlemleri “User.Activation.Consumer” service’i içerisinde gerçekleştiriliyor.

Gördüğümüz gibi bu journey asenkron olarak gerçekleşiyor olmasına rağmen, bu request nerede, süreç nerede ne kadar zaman harcıyor gibi sorulara cevap alabiliyoruz.

Sonuç

Distributed tracing ile bir developer olarak microservice mimarisi içerisinde koşturan kodumuzu, request’in life cycle’ını, debug edebilir ve optimize edebiliriz. OpenTracing API’ı ile de, vendor lock-in durumuna düşmeden sistemimizin farklı tracer’lar ile esnek bir şekilde trace edilebilmesini sağlayabiliyoruz. Ayrıca ben bu makalede, distributed bir yapıdaki sistem içerisinde trace bilgilerinin propagation işlemlerini de göstermeye çalıştım.

Projects: https://github.com/GokGokalp/OpenTracing-Jaeger-Sample

Referanslar

https://github.com/yurishkuro/opentracing-tutorial
https://www.jaegertracing.io/docs/1.11/architecture/

Bu makale toplam (716) kez okunmuştur.

14
0



Tarih:.NET CoreArchitecturalASP.NET CoreContainerizingMessagingMicroservicesRabbitMQ

2 Yorum

  1. coder coder

    eline sağlık güzel makale olmuş.
    application insight üzerindede tüm cycle görebiliyoruz.
    bunun artısı nedir?

    • Teşekkür ederim. OpenTracing uyumlu Microsoft Azure’un managed Application Insight’ını kullanmak da harika bir seçenek. İkisinin de kendisine has yetenekleri mevcut. Seçim tamamen size ve kullanmakta olduğunuz platforma ve neye ihtiyacınız var (async support, open-source yada değil, ) sorusuna göre değişiklik göstermektedir. Bana göre önemli olan tüm cycle’ı görüp yada göremediğiniz.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*