Distributed Tracing

Cem Doğan
9 min readDec 24, 2021

--

Mikroservis tabanlı uygulamaların ve buna bağlı olarak da karmaşıklıklarının artmasıyla birlikte gözlenebilirlik ihtiyacı da artmaya başladı. Bu uygulamaları debug etmek onları kurmaktan daha zor bir duruma geldi. Bir istek başladıktan sonra servisler arasında gezerken hangisinde ne gibi işlemlerden geçti, hangi servislerde ne kadar zaman harcadı vs. gibi sorulara cevap bulunması gerekmektedir. Dağıtık sistemlerle uğraştıysanız distributed tracing terimini duymuş olabilirsiniz. Bu yazıda da dağıtık sistemlerin izlenmesi ve hata giderilmesinin zorluklarından ve distributed tracing’in bu zorluklara nasıl çözüm getirdiğininden bahsedeceğim.

Hayatımıza konteynerleştirme teknolojilerinin girmesiyle birlikte adına mikroservis denilen dağıtık sistem tasarımları da artmaya başladı. Bu mikroservis mimarileri bir uygulamayı birkaç bağımsız servisle ölçeklendirmeyi sağlıyor. Monolitik mimarilere göre esnekliği, ölçeklenebilirliği, üretkenliği ve verimliliği kolaylaştırıyor. Ama bu durum, hataları bulmayı ve sistemde trafik akışını izleme zorluğu gibi durumları da daha çok karmaşıklaştırıyor. Servisler arasındaki bu zorlukları ortadan kaldırmak için de monitoring araçları çıkartılıyor. Hepsinin amacı cloud-native uygulamalarının verimli bir şekilde çalışması ve sürdürülmesi için platform sağlamak.

Gözlenebilirlik, sistemlerin iç durumlarının ve buna bağlı olarak davranışlarının girdi ve çıktı değerlerine göre belirlenebilmesidir diyebiliriz. Firmalar tarafından, sağladığı avantajlara rağmen seçilen mikroservisler kendi zorluklarını ve karmaşıklıklarını da beraberinde getirir. Hali hazırda kendi sistemlerimizde de kullandığımız loglama gibi yapılar bize servisler hakkında bilgiler verir ama bu servisler arasındaki iletişim işin içine dahil olduğunda daha farklı bir yapı düşünmemiz gerekir. Kısaca distributed tracing, dağıtık sistemlerdeki isteklerin ve buna bağlı olarak performans ve hata takiplerini yapmamızı sağlıyor.

Distributed Tracing aşağıdaki sorulara cevap bulmamıza yardımcı olur;

  • İstek hangi servislerden geçti?
  • Verilen bir istek servislerde neler yaşadı?
  • Alınan hata nerede gerçekleşti?
  • Bir istek akışında nerelere odaklanmalıyız?
  • Oluşacak herhangi bir performans problemi veya hata durumunu kime adreslemeliyiz?

Dağıtık sistemleri izlemek monolitik sistemlere göre kolay olmayabilir. Yapılan bir isteğin baştan sona tüm aşamalarını bilip ona göre aksiyon almamız gerekir. Tüm resme bakıp hangi aşamada problem olduğuna karar verdiğimiz zaman ilgili bileşeni daha detaylı inceleyebiliriz. Distributed tracing ise istek bazlı bir görünüm alır. Kısaca;

  • Her bir isteğe bir metadata ekler ve bileşenler birbiriyle iletişim kurduğunda bile isteğin işlenmesinde bu metadata’nın aktırılmasını sağlar.
  • Yazılan koddaki izleme noktalarında HTTP isteğinin veya bir SQL sorgusunun detayını kaydeder.

Adına trace denilen bu kaydettiği olaylar aslında sistemin nasıl çalıştığını da anlamamızı sağlar. Bu trace’ler performans sorunlarının temel nedenini bulmak için çeşitli biçimlerde görüntülenebilir.

Distributed tracing kullanan sistemlerde kullanıcıların yaptığı her istek sisteme kaydedilir. Bununla birlikte bütün sistemde o isteğe bağlı yol da uçtan uca görünür olacaktır. Mimarideki her servis, kullanıcı isteğini gerçekleştiğinde birbirine bağımlı olacaktır. Böylece her servisin isteği tüm yol boyunca diğer bağımlı servisler hakkında da bilgi verecektir. Alınan herhangi bir hatada her yere log koymak yerine isteğin hata aldığı kısımdaki servislerin trace’ini inceleyip hatayı kolayca tespit edilmesi sağlanır. Bu da hatanın bulup düzeltilmesinin daha kısa sürede yapılmasını sağlar.

Distributed tracing’in nasıl çalıştığına bakmadan kullanılan terimleri kısaca açıklayalım;

Span: Adı, zaman aralığı, context’i ve süresi olan tek bir servis tarafından oluşan mantıksal bir çalışma birimi.

Trace: Dağıtık sistemde hareket eden bir veya birden çok spanden oluşan bir isteğin çalışma yolu.

Tag: Key-value olarak tutulan ve span hakkında farklı bilgiler içeren bir yapı.

Log: Span ile alakalı log yapısı.

Baggage Items: SpanContext verileri servis sınırları boyunca taşır. Bunlar da trace boyunca erişim için key-value şeklinde verileri taşır.

https://www.jaegertracing.io/img/spans-traces.png

Distributed tracing bir isteği aşağıdaki gibi ele alır;

  • Distributed tracing süreci kullanıcının uygulama ile etkileşimiyle başlar.
  • Kullanıcı uygulamaya istek yaptığında o isteğe bir trace id atanır. Servislerde gezen istek bu benzersiz kimlik ile izlenir.
  • Bir trace oluştuğunda onunla birlikte context’i de oluşur. Bu context farklı bilgiler içerir ve geçtiği servislerin tamamında korunur.
  • İstek servislerden geçerken her sistem işlemi span ve alt işlemler de child span olarak adlandırılır. İlk oluşan span root span’dir. Her kullanıcı işleminde trace id ve span id vardır.
  • Oluşan span’lerde, isteğin geçtiği servisin adı ve adresi, oturum kimliği, veri tabanı host’u, HTTP method’u ve çeşitli tanımlayıcılar bulunur.
  • Sistem bir hata ile karşılaştığında hata mesajı ve stack trace’i hakkındaki bilgiler de span’lerde bulunur.
  • Bu bilgilerin tamamı veri depolamak için seçilen kaynakta depolanır.

Distributed Tracing aslında uzun süredir kullanılıyor ama bakımı ve uygulanması çok zor. Uygulamanın en kolay yolu bir tracing framework kullanmaktır. Her framework distributed tracing’i uygulamamız için çözümler sağlar. Ayrıca uygulama metric’lerini ve traceler’i toplar, işlemek ve analiz yapmak için backend’e yollar. Genel olarak kullanılar iki önemli framework vardır. Bunlar OpenCensus ve OpenTracing. Bu platformların ikisi de distributed tracing için spesifikasyonlar sağlar ve aralarında benzerlik olsa bile uygulama açısından bir takım farklılıklar vardır. OpenTelemetry ise mayıs 2019'da OpenCensus ve OpenTracing projelerinin birleşmesiyle oluşmuştur. Bu araçlar kodunuzdaki her satırı, değişkenlerin tanımlanması, fonksiyonlar vs gibi kodunuzdaki her satırı izlemenizi sağlar. Bu yazıda OpenTracing ve uygulamasına bakacağız.

OpenTracing bağımlılık olmadan distributed tracing’e izin veren bir dizi standartlar sağlar. Bu amaçla çok sayıda programlama dili ve framework için spesifikasyonlar oluşturulmuştur. Bununla birlikte geliştiricilere uygulanması ve testi daha kolay bir hale getirmeyi amaçlanmaktadır. Context yayılımı için ise inject/extract pattern’i kullanılır. Servislerde span oluştuktan sonra tracer span context’ini inject eder sonrasında istek diğer servise geçtiğinde extract ederek span’leri oluşan trace’de birbirine bağlar.

https://tracing.cloudnative101.dev/docs/_images/Extract.png

OpenTracing Cloud Native Computing Foundation (CNCF) tarafından desteklenmektedir. Bu çözümleri iki gruba ayırabiliriz. Açık kaynak çözümlerden; Zipkin, Jaeger ve AppDash’i verebiliriz. Ücretli çözümlerden ise; Amazon X-Ray, Google Cloud Trace, Datadog ve New Relic’i verebiliriz. Bunlardan farklı çözümler de mevcut. Her çözümün kendine göre artıları ve eksileri mevcut. Sisteminizin mimarisi ve kullandığınız stack’e göre yararları ve kullanımı değişebilir.

Trendyol’da bulunduğum ekiple birlikte geliştirdiğimiz sistem dağıtık, kompleks ve onlarca mikroservisten oluşuyor. Özellikle kampanya dönemlerinde isteklerin artmasıyla yaşadığımız performans problemlerinde en çok ihtiyacımız olan isteklerle ilgili yukarıdaki sorulara cevap bulmak. Distributed Tracing’de hayatımıza bu noktada dahil oldu. Gelen her özellikte sistem gelişiyor ve servis sayımız da artıyor. Sistemi anlamak ve takip etmek de gitgide zorlaşıyor. Distributed Tracing ile bir isteğin trace’ini bütün sistemde izleyebiliyoruz.

Jaeger

Jaeger, dapper’dan etkilenerek Uber mühendisleri tarafından geliştirilen, OpenTracing standartlarını destekleyen bir distributed tracing çözümüdür. Eylül 2017'de CNCF’e kabul edildi ve şu anda mezun derecesinde olgun bir projedir. Mikroservis kullanan sistemleri izlemek ve sorun gidermek için kullanılır. Ayrıca aşağıdakileri de yapmamız için kullanılır;

  • Root cause analysis; traceleri kullanarak belli bir kullanıcı isteğinde gecikmeye neden olan servislere kadar inebilirsiniz.
  • Performance / latency optimization; Hangi servisin veya sorgunun gecikme yarattığını belirledikten sonra, o bilgili optimize etmek için kullanabilirsiniz.
  • Service dependency analysis; Jaeger’ın web arayüzünü kullanarak, isteklerin farklı servisler üzerinden nasıl geçtiğini ve bunların isteği sunarken nasıl etkileşime girdiklerini görebilirsiniz.
  • Distributed transaction monitoring; Jaeger’ın kontrol paneli servisler arası trace ve span yayılımını görmek için kullanılabilir.
  • Distributed context propagation; Jaeger context’i servisler arasında yaymak için birden fazla dilde kod enstrümantasyonu destekleyen kütüphaneler sağlar.
https://www.jaegertracing.io/img/context-prop.png

Jaeger, OpenTracing ve OpenCensus’u desteklemektedir. Jaeger’ın mimarisi ölçeklenebilirlik ve paralelleşmeye odaklanmıştır.

https://www.jaegertracing.io/img/architecture-v1.png

Jaeger agent’ları gelen istekleri UDP bağlantısı üzerinden dinler ve bu istekleri doğrulayan, dönüştüren ve kalıcı şekilde depolayan collector’e yönlendirir. Collector de agent’a benzer şekilde span’leri alır ve işlenmek üzere dahili bir kuyruğa yerleştirebilir. Bu da collector’ün span’i depolamak için beklemeden agent’a dönüp yeni istekleri dinlemesine olanak tanır. Daha sonra query service react tabanlı arayüz kullanarak depolanan trace verilerini rest api ile analiz etmemezi sağlar. Bu basit ve direkt süreç Jaeger’ın popüleşmesinde önemli rol oynamaktadır.

Arka planda trace’leri, span’ları vs loglamak için birden fazla depolama yapısı mevcuttur. Ölçeklenebilir depolama olarak Cassandra ve ElasticSearch’ü destekler. Jaeger takımı trace’leri depolamak için Elasticsearch’ü önermektedir. Casandra key-value veritabanı ve traceId’ye göre trace’leri almak daha hızlı ama aynı performansı aramada karşılamıyor. Kibananın da devreye girmesiyle daha detaylı ve faydalı analizler yapılabiliyor. Tüm trace’leri tutmak ve iletmek çoğu sistem için biraz yorucu olabilir. Ama bu agent’ları yapılandırarak arttırılıp veya azatılabilir.

Image from Mastering Distributed Tracing

Yukarıdaki resimde Uber’de Jaeger tarafından çıkarılan mikroservis mimarisindeki servislerin birbirleriyle bağımlılıklarının grafiğini görüyorsunuz. Her daire farklı bir mikroservisi temsil ediyor. Dairelerin büyüklükleri kendisiyle bağlantılı olan mikroservislerin sayısıyla orantılı artmaktadır. Mobil uygulamadan herhangi bir kullanıcı isteği gerçekleştiğinde onlarca servisin katılımıyla ilgili istek yürütülüyor. Sisteme gelen bu isteklerin bazılarının başarısız veya yavaş olduğunu gördüğümüzde, monitoring araçlarımızın bu durumu izlemesini ve neler olduğunu bize rapor etmesi gerekir.

Uygulama

Örnek projemizde Service1 ve Service2 adında iki tane servisimiz olsun. Service1 istek atacağımız endpoint’e, Service2 de endpointten atılan event’i dinleyen consumer’a sahip olsun. Message Broker olarak RabbitMQ kullanacağım. Bununla birlikte jaeger ve rabbitmq’yu ayağa kaldırmak için de docker kullanacağım. Projenin kodlarını buradan inceleyebilirsiniz.

Solution altına Service1 adında webapi ve Service2 adında console projeleri oluşturduktan sonra bağımlılıkları yükleyebiliriz. Bu noktada event işlerini yürütebilmek için Service1'e nugetten service bus ekleyeceğiz. Ben MetroBus kütüphanesini kullanacağım.

dotnet add package MetroBus
dotnet add package MetroBus.Microsoft.Extensions.DependencyInjection

Bunlardan sonra projemizde distributed tracing operasyonlarını yürütebilmek için OpenTracing ve Jaeger paketlerini ekliyoruz.

dotnet add package OpenTracing.Contrib.NetCore
dotnet add package Jaeger

API’yi oluşturduktan sonra gelen varsayılan WeatherForecastController’ı bozmadım. Amacımız trace’in servisler arasında nasıl taşınıp requestin nerelerden geçtiğini göstermek.

İlk önce busControl ve tracer’ı inject ediyoruz. Sonrasında endpointimizde using ile yeni bir scope açıyoruz. Tracer yeni bir weather-forecast adında bir span oluşturuyor. Span’e client tag’ları ekliyoruz. Ardından ilgili trace context’ini “TextMapInjectAdapter” kullanarak bir dictionary’e ekliyoruz. Bu bize bir trace altında span’leri birleştirmeye yarıyor. Bundan sonrada Service2'nin consume edeceği event’i fırlatıyoruz. Burada tracingkey’leri event içine koymak yerine header’a da koyulabilir ama buradaki amacımız basit bir şekilde nasıl çalıştığını göstermek.

Burada da rabbitmq ve jaeger ayalarını yapıyoruz. Projeyi çalıştırmadan makinanızda docker’ın kurulu olması gerekiyor.

docker compose up

komutu ile rabbitmq, jaeger ve iki tane servisi docker üzerinde ayağa kaldırıp orkestra ediyoruz. Bunlardan sonra rabbitmq http://localhost:15672/ adresinde jaeger http://localhost:16686/ adresinde api de http://localhost:8080/swagger adresinde ayağa kalkmış olması gerekiyor.

Şimdi de Service2'yi inceleyelim;

Service2 adında console projesi oluşturduktan sonra aşağıdaki paketleri ekliyoruz.

dotnet add package MetroBus
dotnet add package MetroBus.Microsoft.Extensions.DependencyInjection
dotnet add package OpenTracing.Contrib.NetCore
dotnet add package Jaeger

Service1'de manuel olarak event içerisine tracing keyleri ekleyerek trace context yayma işlemini yapmıştık.

Bu sınıf ile yeni span’ler oluşturuyoruz. Tracer’da daha önceden oluşmuş bir trace yok ise yeni span, var ise childspan oluşturuyor.

Consumer’ımızda ise mesajda taşıdığımız tracingkey’lere göre span oluşturuyoruz.

Bu sınıf ile de consumer’ımız için jaeger ve rabbitmq ayalarını yapıyoruz. Şimdi projeyi test edebiliriz. Projeleri ve container’ları ayağa kaldırdıktan sonra http://localhost:8080/swagger/index.html adresinde ayağa kalkan API’ye istek atabiliriz.

İstek attıktan sonra endpointimiz event fırlatacak ve o event’i consume eden serviste event’i yakalayacak. Jaeger arayüzünü kontrol edersek servislerin birbirine bağlandığını ve spanlerin hangi sırada işletildiği ve ne kadar süreler harcadığını görebiliriz.

Servislerimize eklediğimi sorgular veya api çağrıları arttıkça arayüze hepsi yansıyacaktır. Uygulama örneğini Gökhan Gökalp’in yazısında kullandığı örnekten esinlenerek oluşturdum.

Sonuç

Mikroservis mimarilerinde distributed tracing kullanmak oluşacak herhangi bir performans probleminde veya hatalarda sisteme müdahaleyi daha kolay ve kısa zamanda yapılmasını sağlar. Bu da üretkenliği doğrudan etkiler. Distributed tracing mikroservisler arasında iletişim kuran isteklerin sorunlarını ve ilişkilerini anlamak için kök neden analizi için gereklidir.

Umarım yazı faydalı olmuştur. Yazı ile ilgili yorumlarınızı almaktan her zaman memnun olurum. Herhangi bir durumda Linkedin ve Twitter üzerinden bana ulaşabilirsiniz.

--

--