E-Bülten’e kayıt olun

E-Posta:



Azure ile Kullanıcılara Gerçek Zamanlı Güncellemeler

Azure ile Kullanıcılara Gerçek Zamanlı Güncellemeler

İnternetin pasif doğası geçtiğimiz yıllarda son derece değişti. Başlangıçta bir siteden güncel verileri almak için sayfayı yenilemek olağan bir davranıştı.

React ve Vue.js gibi kütüphaneler ve asenkron fonksiyonlar alışkanlıklarımızı değiştirdi ve gitgide sayfayı yenilemek istisnai hale geldi. Kullanıcı deneyimi bununla doğru orantılı olarak zenginleşse de bu özelliklerin getirdiği karmaşıklık, sevgili Furkan Kapukaya‘nın sıkça dile getirdiği gibi “Sayfayı yeniletmeyeceğiz diye çektiğimiz çileye bak” problemini de beraberinde getirdi. Günümüzde pek çok uygulama, kullanıcının yaptığı istekler üzerine HTML’i anında güncellemekle kalmayıp, kullanıcıya gerçek zamanlı güncelleme de sunar hale geldi. Örneğin canlı mesajlaşma ya da kontrol paneli arayüzlerinde güncellemelerin gerçek zamanlı olarak tüm istemcilere ulaşması son derece standart bir özelliğe dönüştü.

Artistanbul yazılım ekibi olarak biz de çoğu uygulamamızda sırtımızı WebSocket teknolojisine dayayarak kullanıcıya gerçek zamanlı güncellemeler sunuyoruz. Bu özelliğin en yoğun kullanıldığı projemizde Türkiye’nin yüzlerce noktasındaki cihazlardan topladığımız verileri canlı olarak kullanıcılara sunuyor ve kullanıcıların cihazın durumunu uzaktan takip etmesine olanak sağlıyoruz. Azure servislerini yoğun bir şekilde kullanmamıza rağmen cihazdan gelen verileri işleyen ve kullanıcılara gerçek zamanlı ileten servisleri kendimizi geliştirmiştik. Bu durum daha önce gözümüzden kaçan bir Azure servisini farketmemiz üzerine dramatik şekilde değişti. Bu yazımda tek bir Azure servisi ile kendimizin geliştirdiği üç ayrı servisi nasıl emekli ettiğimizi anlatacağım.

 

Mevcut durum

Öncelikle mevcut durumu anlatmakla başlayayım: Sistemimize bağlı cihazlar dakikada yaklaşık 10 defa kendi durumlarına dair veriler paylaşıyor ve bu veriler doğrudan bir Azure EventHub‘a iletiliyor. EventHub üzerinden akan tüm veriler de kendi geliştirdiğimiz bir veri işlemcisi tarafından gerçek zamanlı olarak işlenip bir Redis kuyruğuna iletiliyor ve bu kuyruktan bir Websocket sunucusu aracılığıyla bağlı istemcilere gönderiliyor. Ayrıca topladığımız veriler beşer dakikalık aralıklarla ileride analiz etmek için Azure depolama servisine kaydediliyor.

Açıkçası sistemin yayına çıkmasından beri bu yapı sorunsuz şekilde görevini yerine getiriyor. Ancak ileride çözmemiz gerekecek dediğimiz bazı sorunları yok değil:

1. WebSocket sunucumuz kolayca ölçeklenebilir durumda değil.
2. Her geliştirdiğimiz ve baktığımız uygulamanın hem zihinsel hem de fiziki bir yükü var. Aramıza yeni katılan takım arkadaşlarımıza veri işlemcimiz nasıl çalışıyor, verinin istemciye iletilememesi durumunda nasıl bir hata ayıklama süreci izlemek gerekir anlatmak, kolay olmuyor.
3. Veri istemcimiz de diğer uygulamalarımızla birlikte bir App Service içinde çalıştığından ve doğası gereği kullanıcıdan istek almadığından App Service tarafından uyutuluyor. Bunu çözmek için de bir beş dakikada bir çalışan bir Azure fonksiyonu bu uygulamaya bir istek gönderiyor. Bu yöntemle uyuma sorununu çözmüş olsak da bir şeyleri yanlış kurguladığımız hissi uyandırıyor.

 

Azure SignalR

.NET camiası SignalR kütüphanesine son derece aşinadır. Artistanbul ise ezelden beri tercihini Linux ve Python’dan yana kullandığı için .NET ekosisteminde kullanılan çoğu teknolojiye uzak duruyor. Azure sayesinde bu ayrım ne mutlu ki silikleşmeye başladı ve Windows camiası Linux teknolojilerinden, Linux camiası da Windows teknolojilerden yararlanabilir hale geldi. Azure’un SignalR servisi bunun için çok iyi bir örnek.

SignalR, ASP.NET uygulamalarından bağlı istemcilere gerçek zamanlı güncellemeler göndermek için kullanılan bir kütüphane. JavaScript, Java ve .NET için hazır istemci araçları da mevcut. Peki biz bu servisin farkına nasıl vardık?

Cihazlardan gelen veriler üzerine kullanıcılara bildirim gönderme özelliğini planlıyorduk. Nasıl yapacağımızı konuşurken yeni fikirlerden ve tasarruf karşıtlığından sorumlu müdürümüz Akın Ömeroğlu‘ndan şöyle bir mesaj aldım:

Azure SignalR kabul sürecimiz çok zorlu geçti

Servisi inceler incelemez bunu gözden kaçırıp tekerleği yeniden icat etmiş olmamıza inanamadım. Uygulamamızdaki gerçek zamanlı güncellemeler için ihtiyacımız olan tüm özellikleri sağladığı gibi kriptik servislere ihtiyacımızı da ortadan kaldırıyordu. Servisi uygulamaya entegre etmek için hemen kolları sıvadık.

Azure SignalR sırtını Azure fonksiyonlarına dayıyor. Kullanıcının sunucuya bağlanması ve güncellemelerin kullanıcıya iletilmesi işi tamamen Azure fonksiyonları aracılığıyla gerçekleşiyor. JavaScript tarafında da hazır kullanabileceğiniz SDK ile birkaç satır kod ile entegrasyonu tamamlıyorsunuz.

 

Azure fonksiyonları ile veri işleme ve istemcilere iletme

Azure fonksiyonları tetik mekanizması olarak birçok Azure servisini destekliyor. EventHub da bunlardan biri. EventHub’a iletilen her mesaj üzerine fonksiyon tetikleniyor ve istediğiniz çıktıyı üretiyor. Bu çıktı bir HTTP cevabı olabileceği gibi bir SignalR mesajı da olabilir.

Her mesajı Azure fonksiyonları işleme fikri başta bana pek hoş gelmemişti. En düşük yoğunluklu günlerde bile yüz bine yakın mesaj işlediğimizi düşününce, mesaj başına fonksiyon tetiklemenin doğuracağı maliyetten çekinmiştim ancak fonksiyonlarınız bir App Service içinde de barınabiliyor. Dolayısıyla servisleriniz hali hazırda bir App Service planı içinde çalışıyorsa ve kaynağınız da yeterliyse maliyet kaygısı yaşamanıza gerek kalmıyor.

Linux üzerinde Azure fonksiyonları geliştirmek ve yayımlamak için Azure’un komut satırı aracını kullanabilirsiniz. SignalR entegrasyonu için iki ayrı fonksiyon yaratmanız gerekiyor:

  1. Negotiate: İstemcinin gerekiyorsa kimliğini doğruladıktan sonra SignalR sunucusuna bağlanması için gerekli bilgileri alacağı fonksiyon (Not: Adının Negotiate olması önemli!)
  2. İstemcilere SignalR mesajlarını gönderecek fonksiyon (adını istediğiniz gibi belirleyebilirsiniz)

Azure fonksiyonlarının nasıl tetikleneceği ve çıktı olarak ne üreteceği function.json adındaki bir dosya ile belirleniyor. Negotiate fonksiyonu için örnek bir function.json dosyası:

{                                                                                                                                                                                                   
  "scriptFile": "__init__.py",                                                                                                                                                                      
  "bindings": [                                                                                                                                                                                     
    {                                                                                                                                                                                               
      "authLevel": "anonymous",                                                                                                                                                                     
      "type": "httpTrigger",                                                                                                                                                                        
      "direction": "in",                                                                                                                                                                            
      "name": "req",                                                                                                                                                                                
      "methods": [                                                                                                                                                                                  
        "post",                                                                                                                                                                                     
        "options"                                                                                                                                                                                   
      ]                                                                                                                                                                                             
    },                                                                                                                                                                                              
    {                                                                                                                                                                                               
      "type": "http",                                                                                                                                                                               
      "direction": "out",                                                                                                                                                                           
      "name": "$return"                                                                                                                                                                             
    },                                                                                                                                                                                              
    {                                                                                                                                                                                               
      "type": "signalRConnectionInfo",                                                                                                                                                              
      "direction": "in",                                                                                                                                                                            
      "name": "connectionInfoJson",                                                                                                                                                                 
      "hubName": "events"                                                                                                                                                                           
    }                                                                                                                                                                                               
  ]                                                                                                                                                                                                 
}

Dosyadan anlayabileceğiniz üzere fonksiyon SignalR bağlantı isteği ile tetikleniyor ve çıktı olarak SignalR bağlantı bilgilerini içeren bir HTTP cevabı dönüyor. Javascript istemci SDK’sı bu bilgilerle sunucuya bağlanıyor ve bir WebSocket bağlantısı kuruluyor. Bu noktadan sonra gönderilen her mesaj istemcilere iletiliyor. Tabii, dilerseniz sadece belirli bir kullanıcıya veya bir kullanıcı grubuna özel mesaj da gönderebiliyorsunuz.

İstemcilere mesaj ulaştıracak fonksiyon için function.json dosyasını inceleyelim:

{                                                                                                                                                                                                   
  "scriptFile": "__init__.py",                                                                                                                                                                      
  "bindings": [                                                                                                                                                                                     
    {                                                                                                                                                                                               
      "type": "eventHubTrigger",                                                                                                                                                                    
      "name": "event",                                                                                                                                                                              
      "direction": "in",                                                                                                                                                                            
      "eventHubName": "eventhub-test",                                                                                                                                                
      "connection": "EventHubReadConnectionAppSetting",                                                                                                                                             
      "cardinality": "many",                                                                                                                                                                        
      "consumerGroup": "$Default"                                                                                                                                                                   
    },                                                                                                                                                                                              
    {                                                                                                                                                                                               
      "type": "signalR",                                                                                                                                                                            
      "name": "$return",                                                                                                                                                                            
      "direction": "out",                                                                                                                                                                           
      "hubName": "events"                                                                                                                                                                           
    }                                                                                                                                                                                               
  ]                                                                                                                                                                                                 
}

Bu fonksiyon ise EventHub mesajları ile tetikleniyor ve çıktı olarak bir SignalR mesajı dönüyor. Bu mesaj da SignalR sunucusu tarafından hedeflenen kullanıcı grubuna iletiliyor. Örnek fonksiyonumuzun kodunu da inceleyelim:

import json                                                                                                                                                                                                                                                                                 
import logging

import azure.functions as func
def main(event: func.EventHubEvent) -> str: 
    logging.info("Python EventHub trigger processed an event") 
    body = event.get_body().decode("utf-8") 

    return json.dumps({"target": "newEvent", "arguments": body})

JavaScript tarafından yapmamız gerekenler ise daha da basit. @aspnet/signalr paketini yükledikten sonra birkaç satır kod yazmanız yeterli:

import { HubConnectionBuilder } from "@aspnet/signalr";

const connection = new HubConnectionBuilder().withUrl("https://artistanbul-signalr.azurewebsites.net/api").build();
connection.start();
connection.on("newEvent", e => setEvent(e));

Azure SignalR ile artık çamaşırlar daha beyaz!

Azure SignalR servisini o kadar beğendim ki, geçtiğimiz hafta boyunca Slack’teki herkese servisi öve öve bıktırdım. İleride bu servis ile daha önce düşünmediğimiz bir sürü özellik geliştireceğimize eminim. Örneğin, servisin sunduğu REST API aracılığıyla uygulamanın her köşesinden kullanıcıya anlık bildirim gönderebilir veya uzun süren işlemleri asenkron hale getirip bittiğinde kullanıcıya çıktıyı iletebiliriz.

Mevcut yazılımlarınızı Azure üzerinde bulut yerlisi hale getirme ya da yeni projeler geliştirme fikriniz varsa her zaman ofisimizde bir espresso içmeye uzaktan görüntülü görüşmeye bekleriz.

Ege Güneş

Ege Güneş, İstanbul Üniversitesi Hukuk Fakültesi'nden mezun oldu. Günlük yaşamında özgür yazılımlardan yana ve Linux kullanıyor. Favori dağıtımıysa Fedora.

Yorum Yok

Yorum Yaz

Yorum
İsim
E-Posta
Website