FastAPI, Redis ve C++ ile Öncelikli Görev Zamanlayıcı Nasıl Oluşturulur?
Modern web uygulamalarında, eşzamansız (asynchronous) görevlerin yönetimi kritik bir öneme sahiptir. Kullanıcı deneyimini kesintiye uğratmadan, arka planda karmaşık veya zaman alıcı işlemleri yürütmek, uygulamaların yanıt hızını ve genel performansını doğrudan etkiler. Peki, farklı önceliklere sahip görevleri nasıl etkili bir şekilde sıralayabilir ve işleyebiliriz? Bu makalede, yüksek performanslı ve ölçeklenebilir bir öncelikli görev zamanlayıcı sistemini FastAPI, Redis ve C++ kullanarak adım adım nasıl inşa edeceğimizi keşfedeceğiz. Bu sistem, e-ticaret sipariş işleme, IoT veri analizi veya finansal işlemler gibi senaryolarda kritik öneme sahip görevlerin daima zamanında ve doğru sırayla işlenmesini sağlayacak.
Temel Kavramlar: Neden Öncelikli Görev Yönetimi Gereklidir?
Günümüzün dinamik yazılım ortamlarında, uygulamalar genellikle birbirinden farklı özelliklere ve aciliyet derecelerine sahip çok sayıda görevi aynı anda yürütmek zorundadır. Örneğin, bir e-ticaret platformunda yeni bir siparişin işlenmesi, bir kullanıcının şifresini sıfırlama talebi veya sistemin arka planda yaptığı periyodik raporlama gibi görevler bulunabilir. Bu görevlerin hepsi aynı öncelikte değildir ve aynı hızda işlenmeleri gerekmez. İşte tam da bu noktada, öncelikli görev yönetimi devreye girer. Öncelikli bir görev zamanlayıcı, sistemin kaynaklarını en kritik görevlere tahsis etmesini sağlayarak, uygulamanın genel yanıt verme süresini (responsiveness) ve verimliliğini artırır.
Bir görev kuyruğu (task queue) düşünün; bu, işlenmeyi bekleyen görevlerin bir listesidir. Geleneksel kuyruklarda görevler genellikle “ilk giren ilk çıkar” (FIFO – First-In, First-Out) prensibiyle işlenir. Ancak öncelikli bir kuyrukta, her göreve bir öncelik atanır ve en yüksek öncelikli görev, kuyrukta ne zaman yer almış olursa olsun, diğerlerinden önce işlenir. Bu yaklaşım, özellikle sınırlı işlem gücüne sahip sistemlerde veya belirli görevlerin acil yanıt gerektirdiği durumlarda hayati önem taşır. Örneğin, bir bankacılık uygulamasında, büyük bir para transferi işlemi ile küçük bir bakiye sorgulama işlemi aynı öncelikte olmamalıdır. Büyük transfer, güvenlik ve doğrulama adımları nedeniyle daha uzun sürebilir ve daha yüksek öncelik gerektirebilir.
Bu makalede inşa edeceğimiz sistemin temel bileşenleri şunlardır:
- FastAPI: Görevleri sisteme kabul etmek için hızlı ve modern bir web çerçevesi (framework). Kullanıcı arayüzleri veya diğer servisler, HTTP istekleri aracılığıyla görevleri FastAPI uygulamasına gönderecek.
- Redis: Yüksek performanslı bir anahtar-değer (key-value) veri deposu olarak görev kuyruğumuzu yönetecek. Redis’in Sorted Set (sıralı küme) veri yapısı, görevleri önceliklerine göre sıralamak için idealdir.
- C++: Redis’ten görevleri çekip işleyecek yüksek performanslı işleyici (worker) uygulaması için kullanılacak. C++, düşük seviyeli sistem kaynaklarına erişim ve olağanüstü performans sunarak, kritik görevlerin milisaniyeler içinde işlenmesini mümkün kılar.
Bu üç teknolojinin birleşimi, hem geliştirici dostu bir API katmanı hem de sağlam ve hızlı bir arka plan işleme mekanizması sunarak, karmaşık görev yönetimi ihtiyaçlarını karşılayacaktır. Sistemimizin temel amacı, gelen görevlerin önceliklerini dikkate alarak, en kritik olanları en kısa sürede işlemek ve böylece uygulamanın genel verimliliğini ve güvenilirliğini artırmaktır. Bu mimari, dağıtık sistemlerdeki yük dengeleme ve hata toleransı gibi konularda da esneklik sağlayarak, modern bulut tabanlı uygulamalar için ideal bir çözüm sunar.
FastAPI ile Görev Kuyruğuna Nasıl Ekleme Yapılır?
Görev zamanlayıcı sistemimizin ilk adımı, dış dünyadan gelen görev taleplerini kabul edecek bir arayüz oluşturmaktır. Bu arayüz için FastAPI’yi kullanacağız. FastAPI, modern Python tabanlı, yüksek performanslı bir web çerçevesidir ve API geliştirmeyi son derece kolaylaştırır. Otomatik dokümantasyon (Swagger UI) ve veri doğrulama (Pydantic ile) gibi özellikleriyle hızlı geliştirmeye olanak tanır.
FastAPI Uygulamasının Kurulumu ve Temel Yapısı
Öncelikle, FastAPI ve gerekli kütüphaneleri kurmamız gerekiyor. Python ortamınızda aşağıdaki komutları çalıştırarak başlayabiliriz:
pip install fastapi uvicorn redis pydantic
uvicorn, FastAPI uygulamasını çalıştırmak için kullanılan ASGI sunucusudur. redis kütüphanesi, Python uygulamasından Redis ile etkileşim kurmamızı sağlar. pydantic ise veri modellerimizi tanımlamak için kullanılır.
Şimdi, görevleri kabul edecek bir FastAPI uygulaması oluşturalım. Görevlerimizin bir yapısı olmalı; örneğin, bir görev kimliği (ID), bir açıklama ve en önemlisi bir öncelik değeri içermelidir. Öncelik değeri, sayısal bir değer olacak ve daha düşük sayılar daha yüksek önceliği ifade edecek (örneğin, 1 en yüksek öncelik, 10 en düşük).
Görev Modeli ve API Uç Noktası Tanımlama
Aşağıdaki kod bloğu, FastAPI uygulamasını ve görev modelini nasıl tanımlayacağımızı gösterir:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import redis
import json
import time
app = FastAPI(title="Öncelikli Görev Zamanlayıcı API")
# Redis bağlantısı
# Gerçek bir uygulamada bu bilgileri ortam değişkenlerinden almak daha güvenlidir.
redis_client = redis.Redis(host='localhost', port=6379, db=0)
class Task(BaseModel):
task_id: str = Field(..., description="Görevin benzersiz kimliği")
description: str = Field(..., description="Görevin kısa açıklaması")
priority: int = Field(..., ge=1, le=10, description="Görevin öncelik seviyesi (1: En yüksek, 10: En düşük)")
payload: dict = Field(default_factory=dict, description="Görevin işlenmesi için gerekli ek veriler")
@app.post("/tasks/", status_code=202)
async def create_task(task: Task):
"""
Yeni bir görevi öncelikli kuyruğa ekler.
"""
try:
# Görevin benzersiz bir anahtarı olmalı, Redis'te Sorted Set'e eklerken kullanılacak.
# Zaman damgası (timestamp) ekleyerek aynı öncelikteki görevler arasında FIFO sağlamak
# veya basitçe benzersiz bir ID kullanmak mümkündür.
# Burada, önceliği eksi olarak depoluyoruz çünkü Redis Sorted Set'ler varsayılan olarak artan sırada sıralar.
# Biz en düşük öncelik değerine (yani en yüksek önceliğe) sahip görevlerin en başta olmasını istiyoruz.
# Aynı öncelikteki görevler için, eklenme zamanını ikinci bir sıralama kriteri olarak kullanabiliriz.
# Bu örnekte, önceliği skor olarak kullanacağız.
# Görevi JSON formatına çeviriyoruz
task_data = task.dict()
task_data["created_at"] = time.time() # Eklenme zamanını da kaydedelim
# Redis'e ekleme. Sorted Set'te skor olarak önceliği kullanıyoruz.
# Düşük sayı = Yüksek öncelik
# Redis Sorted Set'ler varsayılan olarak artan sırada sıralar.
# Biz en düşük öncelik değerine (yani en yüksek önceliğe) sahip görevlerin en başta olmasını istiyoruz.
# Bu yüzden priority değerini doğrudan skor olarak kullanabiliriz.
# Eğer en yüksek öncelik 10, en düşük 1 olsaydı, 11 - priority şeklinde bir skor kullanırdık.
# Mevcut durumda 1 en yüksek, 10 en düşük olduğu için doğrudan kullanabiliriz.
redis_client.zadd("priority_queue", {json.dumps(task_data): task.priority})
return {"message": "Görev başarıyla kuyruğa eklendi", "task_id": task.task_id}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Görev eklenirken bir hata oluştu: {str(e)}")
# Uygulamayı çalıştırmak için terminalde:
# uvicorn main:app --reload
Yukarıdaki kodda, Task adında bir Pydantic modeli tanımladık. Bu model, her görevin sahip olması gereken alanları (task_id, description, priority, payload) ve bu alanların veri tiplerini ve kısıtlamalarını belirtir. @app.post("/tasks/") dekoratörü ile yeni görevleri almak için bir POST uç noktası oluşturduk. Bu uç nokta, gelen görevi Redis’in “priority_queue” adlı Sorted Set’ine ekler. Redis’in Sorted Set yapısı, elemanları bir “skor”a göre sıraladığı için, görevin öncelik değerini bu skor olarak kullanırız. Böylece Redis, görevleri otomatik olarak öncelik sırasına göre düzenler.
Bu FastAPI uygulaması, görevleri alıp Redis’e aktarma görevini üstlenir. Kullanıcılar veya diğer mikroservisler, basit HTTP POST istekleriyle görevlerini bu API’ye iletebilirler. FastAPI’nin asenkron (async/await) yapısı sayesinde, gelen istekler hızlı bir şekilde işlenir ve Redis’e yazma işlemi arka planda gerçekleşirken API yanıt verebilir. Bu, uygulamanın yüksek trafik altında bile yanıt verme hızını korumasına yardımcı olur.
Redis: Öncelikli Görev Kuyruğu Yapısı Nasıl Tasarlanır?
Görev zamanlayıcı sistemimizin kalbinde, görevleri önceliklerine göre depolayan ve işleyicilerin (workers) kolayca erişebileceği bir veri yapısı yer alır. Bu rol için Redis, olağanüstü performansı ve esnek veri yapılarıyla mükemmel bir seçenektir. Özellikle Redis’in Sorted Set (sıralı küme) veri yapısı, öncelikli kuyruklar oluşturmak için idealdir.
Redis Sorted Set ile Öncelikli Kuyruk Mantığı
Redis Sorted Set, her üyenin benzersiz bir string olduğu ve ilişkili bir skor (kayan nokta sayısı) ile birlikte depolandığı bir koleksiyondur. Üyeler, bu skorlara göre sıralanır. Bu özellik, görevlerimizi öncelik skorlarına göre sıralamak için tam da ihtiyacımız olan şeydir. Daha düşük skorlu üyeler, daha yüksek skorlu üyelerden önce gelir.
Bizim senaryomuzda, görevin öncelik değeri (örneğin, 1 en yüksek öncelik, 10 en düşük) doğrudan Redis Sorted Set’in skoru olarak kullanılacaktır. Bu durumda, skor 1 olan görevler, skor 2 olanlardan önce, skor 2 olanlar da skor 3 olanlardan önce işlenecektir. Aynı öncelik skoruna sahip görevler için Redis, varsayılan olarak eklenme sırasına (lexicographical order) göre sıralama yapar. Ancak, daha deterministik (belirleyici) bir davranış için, skor değerini oluştururken eklenme zaman damgası (timestamp) gibi ikincil bir kriteri de dahil edebiliriz. Örneğin, priority * 1000000000 + timestamp gibi bir formül kullanarak, aynı öncelikteki görevlerin eklenme sırasına göre işlenmesini sağlayabiliriz.
Görevlerin Redis’e Eklenmesi ve Çekilmesi
FastAPI uygulamasında gördüğümüz gibi, görevler json.dumps(task_data) ile JSON string’ine dönüştürülür ve redis_client.zadd("priority_queue", {json.dumps(task_data): task.priority}) komutuyla Redis’e eklenir. Burada "priority_queue", Sorted Set’imizin anahtarıdır.
Görev işleyicimiz (C++ worker), kuyruktaki en yüksek öncelikli görevleri çekmek için Redis’in ZPOPMAX veya ZRANGE / ZREM komutlarını kullanacaktır. ZPOPMAX (Redis 5.0 ve sonrası), bir Sorted Set’ten en yüksek skorlu üyeyi (yani bizim durumumuzda en düşük öncelik skoruna sahip olanı) atomik (atomic) olarak çıkarır ve döndürür. Bu, birden fazla işleyicinin aynı anda görev çekmeye çalıştığı durumlarda yarış koşullarını (race conditions) önler.
// Redis'ten en yüksek öncelikli görevi çekme (C++ worker tarafından kullanılacak mantık)
// Pseudo-code (sözde kod) olarak düşünün
// Redis komutu: ZPOPMAX key count
// count: Kaç eleman çekileceği (genellikle 1)
// ZPOPMAX en yüksek skorlu elemanı çıkarır. Bizim önceliğimizde düşük skor yüksek öncelik olduğu için
// ZPOPMIN kullanmak daha doğru olacaktır. Ancak Redis 6'dan itibaren ZPOPMIN mevcut.
// Eğer eski bir Redis sürümü kullanıyorsak veya ZPOPMIN yoksa, ZRANGE ile en düşük skorluyu alıp ZREM ile sileriz.
// Redis 6+ için:
// task_data, priority = redis_client.zpopmin("priority_queue", 1)
// Eski Redis sürümleri veya daha genel bir yaklaşım için:
// 1. En düşük skorlu (en yüksek öncelikli) görevi al
// tasks = redis_client.zrange("priority_queue", 0, 0, withscores=True)
// if tasks:
// task_member, task_priority = tasks[0]
// # 2. Görevi kuyruktan atomik olarak sil
// # Watch/Multi/Exec veya LUA scriptleri ile atomik silme sağlanabilir.
// # Alternatif olarak, her göreve benzersiz bir ID verip, bu ID'yi ZRANGE ile alıp ZREM ile silmek daha güvenlidir.
// # Örneğin, ZRANGEbyscore ile belirli bir öncelik aralığındaki görevleri alıp, ardından ZREM ile silebiliriz.
// # Basit bir ZREM örneği:
// # redis_client.zrem("priority_queue", task_member)
// # return task_member, task_priority
// else:
// # Kuyruk boş
// pass
ZPOPMAX (veya ZPOPMIN) komutunun atomik yapısı, dağıtık işleme ortamlarında birden fazla işleyicinin aynı görevi iki kez çekmesini engeller. Bu, sistemin tutarlılığı için kritik bir özelliktir. Eğer ZPOPMAX/ZPOPMIN mevcut değilse, ZRANGE ile en üstteki görevi okuyup, ardından bir LUA betiği (script) veya Redis Transaction (işlem) kullanarak görevi atomik olarak silmek daha güvenli bir yaklaşımdır. LUA betiği, birden fazla Redis komutunu tek bir atomik işlem olarak çalıştırmamızı sağlar.
Redis Persistent (Kalıcı) Depolama ve Yüksek Erişilebilirlik
Redis, varsayılan olarak bellekte çalışsa da, verilerin disk üzerinde kalıcı olmasını sağlayan mekanizmalara sahiptir (RDB snapshotting ve AOF logging). Bu, Redis sunucusunun çökmesi durumunda bile görev kuyruğundaki verilerin kaybolmamasını garanti eder. Ayrıca, Redis Sentinel veya Redis Cluster gibi özellikler, yüksek erişilebilirlik (high availability) ve ölçeklenebilirlik sağlayarak, Redis’i üretim ortamları için sağlam bir seçim haline getirir.
Bu yapı sayesinde, görevlerin Redis’e güvenli bir şekilde eklenmesi ve önceliklerine göre düzenlenmesi sağlanır. Bir sonraki adımda, C++ ile bu görevleri Redis’ten nasıl çekip işleyeceğimizi inceleyeceğiz.
C++ ile Yüksek Performanslı Görev İşleyici Nasıl Geliştirilir?
Görev zamanlayıcı sistemimizin en kritik bileşenlerinden biri, Redis kuyruğundaki görevleri çekip gerçek işleme mantığını yürüten işleyicidir (worker). Bu işleyici için C++ kullanmamızın temel nedeni, C++’ın sunduğu benzersiz performans, düşük seviyeli sistem kaynaklarına erişim ve kaynak verimliliğidir. Özellikle milisaniye düzeyinde yanıt sürelerinin önemli olduğu veya yoğun işlem gücü gerektiren görevlerin bulunduğu senaryolarda C++ ideal bir tercihtir.
Neden C++?
- Performans: C++, derlenmiş (compiled) bir dil olduğu için Python gibi yorumlanmış (interpreted) dillere göre çok daha hızlı çalışır. Bu, özellikle CPU yoğun görevlerde belirgin bir avantaj sağlar.
- Kaynak Verimliliği: Bellek ve CPU kullanımı üzerinde doğrudan kontrol imkanı sunar. Bu, büyük ölçekli sistemlerde veya kısıtlı kaynaklara sahip ortamlarda (örneğin gömülü sistemler) kritik öneme sahiptir.
- Düşük Gecikme: İşletim sistemi ve donanımla daha yakın etkileşim kurarak, görevlerin daha düşük gecikmeyle işlenmesine olanak tanır.
- Mevcut Kütüphaneler: Redis ile etkileşim kurmak için
hiredisgibi sağlam ve optimize edilmiş C kütüphaneleri mevcuttur.
C++ İşleyici Uygulamasının Temel Yapısı
C++ işleyicimiz, sürekli olarak Redis’e bağlanacak, öncelikli kuyruktan görevleri çekecek, bu görevleri ayrıştıracak (parse edecek) ve belirlenen işleme mantığını uygulayacaktır. Bir hata durumunda veya görev başarıyla tamamlandığında uygun şekilde yanıt verecektir.
Redis ile Bağlantı Kurma (hiredis kullanarak)
hiredis, C dilinde yazılmış hafif bir Redis istemcisidir ve C++ projelerinde kolayca kullanılabilir. İlk adım, hiredis kütüphanesini sisteminize kurmaktır. Genellikle Linux dağıtımlarında paket yöneticileri aracılığıyla (örn. sudo apt-get install libhiredis-dev) veya kaynak koddan derlenerek kurulabilir.
#include
#include
#include
#include
#include // hiredis kütüphanesi
// Basit bir JSON ayrıştırıcı kütüphanesi (örneğin nlohmann/json) kullanılabilir.
// Bu örnekte string işlemleri ile basit bir ayrıştırma yapacağız.
// Gerçek bir uygulamada nlohmann/json gibi robust bir kütüphane şiddetle tavsiye edilir.
std::string get_json_value(const std::string& json_str, const std::string& key) {
std::string search_key = "\"" + key + "\":";
size_t pos = json_str.find(search_key);
if (pos == std::string::npos) {
return ""; // Anahtar bulunamadı
}
pos += search_key.length();
// Değerin başlangıcını bul
while (pos < json_str.length() && (json_str[pos] == ' ' || json_str[pos] == '"')) {
pos++;
}
size_t end_pos = pos;
while (end_pos < json_str.length() && json_str[end_pos] != ',' && json_str[end_pos] != '}') {
end_pos++;
}
std::string value = json_str.substr(pos, end_pos - pos);
// Eğer değer tırnak içinde ise tırnakları kaldır
if (!value.empty() && value.front() == '"' && value.back() == '"') {
return value.substr(1, value.length() - 2);
}
return value;
}
void worker_thread(int worker_id) {
redisContext *c = redisConnect("127.0.0.1", 6379);
if (c == NULL || c->err) {
if (c) {
std::cerr << "Redis bağlantı hatası: " << c->errstr << std::endl;
redisFree(c);
} else {
std::cerr << "Redis bağlantı kurulamadı." << std::endl;
}
return;
}
std::cout << "Worker " << worker_id << " Redis'e bağlandı." << std::endl;
while (true) {
redisReply *reply;
// ZPOPMIN: Sorted Set'ten en düşük skorlu elemanı (yani en yüksek öncelikli görevi) atomik olarak çeker.
// Redis 6.0 ve üzeri için geçerlidir. Daha eski sürümlerde ZRANGE/ZREM kombinasyonu kullanılmalı.
reply = (redisReply *)redisCommand(c, "ZPOPMIN priority_queue 1");
if (reply == NULL) {
std::cerr << "Redis komut hatası veya bağlantı kesildi." << std::endl;
redisFree(c);
c = redisConnect("127.0.0.1", 6379); // Yeniden bağlanmayı dene
std::this_thread::sleep_for(std::chrono::seconds(5));
continue;
}
if (reply->type == REDIS_REPLY_ARRAY && reply->elements > 0) {
// ZPOPMIN çıktısı: [member1, score1, member2, score2, ...]
// Biz 1 eleman çektiğimiz için reply->elements 2 olacak (member ve score)
std::string task_json = reply->element[0]->str;
double priority_score = std::stod(reply->element[1]->str);
// Görev verilerini ayrıştır (örneğin JSON ayrıştırma)
// Gerçek bir uygulamada nlohmann/json gibi bir kütüphane kullanılmalı.
std::string task_id = get_json_value(task_json, "task_id");
std::string description = get_json_value(task_json, "description");
std::cout << "Worker " << worker_id << " görevi çekti: ID=" << task_id
<< ", Açıklama='" << description << "', Öncelik=" << priority_score << std::endl;
// Burada görevin gerçek işleme mantığı yer alacak.
// Örneğin, bir veritabanı işlemi, harici bir API çağrısı, dosya yazma vb.
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Görev işleme simülasyonu
std::cout << "Worker " << worker_id << " görevi tamamladı: ID=" << task_id << std::endl;
} else {
// Kuyruk boş, biraz bekle ve tekrar dene
std::cout << "Worker " << worker_id << " kuyruk boş, bekleniyor..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
freeReplyObject(reply); // Belleği serbest bırak
}
redisFree(c); // Bağlantıyı kapat
}
int main() {
// Birden fazla worker thread'i başlatabiliriz
const int num_workers = 3;
std::vector workers;
for (int i = 0; i < num_workers; ++i) {
workers.emplace_back(worker_thread, i + 1);
}
for (auto& w : workers) {
w.join(); // Thread'lerin bitmesini bekle (bu örnekte sonsuz döngüde çalışacaklar)
}
return 0;
}
Yukarıdaki C++ kodu, basit bir görev işleyici (worker) örneğini göstermektedir:
redisConnectile Redis sunucusuna bağlanır.- Sonsuz bir döngü içinde,
ZPOPMIN priority_queue 1komutunu kullanarak Redis'ten en yüksek öncelikli görevi (en düşük skorlu) çeker. Bu komut, görevi atomik olarak kuyruktan kaldırır. - Gelen görevin JSON verilerini ayrıştırır (gerçek bir uygulamada
nlohmann/jsongibi bir kütüphane kullanmak çok daha güvenli ve pratik olacaktır). - Görev işleme mantığını simüle etmek için kısa bir süre bekler.
- Görev tamamlandığında konsola bilgi basar.
- Kuyruk boşsa, belirli bir süre bekleyip tekrar dener.
- Birden fazla worker thread'i başlatarak eşzamanlı görev işlemeyi gösterir.
Bu C++ işleyicisi, Redis'ten görevleri verimli bir şekilde çekerek, belirlenen iş mantığını yüksek performansla yürütür. Hata yönetimi ve bağlantı kesintileri durumunda yeniden bağlanma mekanizmaları, sağlam bir üretim uygulaması için hayati öneme sahiptir. Ayrıca, görevlerin işlenmesi sırasında oluşabilecek istisnaların (exceptions) doğru bir şekilde ele alınması ve başarısız görevlerin bir "ölü mektup kuyruğuna" (dead-letter queue) yönlendirilmesi gibi ileri düzey konular da düşünülmelidir.
Sistem Entegrasyonu ve Ölçeklenebilirlik: Bileşenler Nasıl Birleştirilir?
Şimdiye kadar FastAPI ile görevleri kuyruğa eklemeyi, Redis ile öncelikli bir kuyruk yapısı oluşturmayı ve C++ ile bu görevleri işlemeyi öğrendik. Bu üç bileşeni bir araya getirerek sağlam, ölçeklenebilir ve yüksek performanslı bir görev zamanlayıcı sistemi inşa edebiliriz. Bu bölümde, sistemin genel mimarisini, bileşenlerin nasıl entegre edileceğini ve ölçeklenebilirlik stratejilerini inceleyeceğiz.
Sistemin Genel Mimarisi
Sistemimiz üç ana katmandan oluşur:
- API Katmanı (FastAPI): Kullanıcıların veya diğer servislerin görevleri sisteme göndermesi için bir HTTP arayüzü sağlar. Gelen görevleri doğrular ve Redis kuyruğuna ekler.
- Kuyruk Katmanı (Redis): Görevleri önceliklerine göre depolayan ve işleyiciler için merkezi bir havuz görevi gören yüksek performanslı bir mesaj kuyruğu olarak işlev görür.
- İşleme Katmanı (C++ Workers): Redis kuyruğundan görevleri çeker, işleme mantığını uygular ve sonuçları işler.
Bu mimari, her katmanın kendi sorumluluğuna odaklanmasını sağlar ve bileşenlerin bağımsız olarak ölçeklenmesine olanak tanır. Örneğin, API trafiği artarsa sadece FastAPI örneklerini artırabiliriz; görev işleme yükü artarsa C++ worker sayısını artırabiliriz.
Bileşenlerin Entegrasyonu
Entegrasyon süreci oldukça basittir:
- FastAPI ve Redis: FastAPI uygulaması, Redis istemci kütüphanesini (
redis-py) kullanarak Redis sunucusuna bağlanır veZADDkomutuyla görevleripriority_queueSorted Set'ine ekler. Redis sunucusunun erişilebilir bir adreste (örneğin,localhost:6379) çalışıyor olması gerekir. - C++ Workers ve Redis: C++ worker uygulamaları,
hirediskütüphanesini kullanarak aynı Redis sunucusuna bağlanır veZPOPMIN(veya alternatif komutlar) ile görevleripriority_queue'dan çeker. Her worker, kendi bağlantısını yönetir ve görevleri bağımsız olarak işler.
Bu entegrasyon, Redis'in merkezi bir mesaj aracı olarak hareket etmesi sayesinde kolayca gerçekleştirilir. Tüm bileşenler, Redis üzerinden iletişim kurar ve birbirlerinin varlığından haberdar olmak zorunda kalmazlar.
Ölçeklenebilirlik Stratejileri
Sistemimizin farklı katmanlarını bağımsız olarak ölçeklendirebiliriz:
- FastAPI Ölçeklemesi:
- Yatay Ölçekleme: Birden fazla FastAPI örneğini bir yük dengeleyici (load balancer) arkasında çalıştırarak API katmanının kapasitesini artırabiliriz. Gelen HTTP istekleri, bu örnekler arasında dağıtılır.
- Asenkron İşleme: FastAPI'nin asenkron yapısı sayesinde, I/O yoğun işlemlerde (Redis'e yazma gibi) bile yüksek eşzamanlılık (concurrency) sağlanır.
- Redis Ölçeklemesi:
- Redis Sentinel: Yüksek erişilebilirlik için Redis Sentinel kullanarak ana (master) Redis sunucusunun çökmesi durumunda otomatik failover (yedeklemeye geçiş) sağlayabiliriz.
- Redis Cluster: Çok büyük veri kümeleri veya çok yüksek okuma/yazma yükleri için Redis Cluster kullanarak verileri birden fazla Redis düğümüne dağıtabilir ve yatay ölçekleme sağlayabiliriz.
- Replikasyon (Replication): Okuma yükünü dağıtmak için Redis master-replica (ana-kopya) mimarisi kullanılabilir. Worker'lar, görevleri çekmek için replica'lara bağlanabilir (ancak ZPOPMIN gibi yazma komutları için master'a bağlanmaları gerekir).
- C++ Workers Ölçeklemesi:
- Yatay Ölçekleme: Birden fazla C++ worker örneğini farklı sunucularda veya aynı sunucu üzerinde ayrı süreçler (processes) olarak çalıştırabiliriz. Her worker, Redis'ten görevleri bağımsız olarak çeker ve işler. Redis'in atomik
ZPOPMINkomutu, birden fazla worker'ın aynı görevi çekmesini engeller. - Dikey Ölçekleme: Daha güçlü CPU'lara sahip sunucular kullanarak tek bir worker'ın daha fazla görevi daha hızlı işlemesini sağlayabiliriz.
- İş Parçacığı Havuzları (Thread Pools): Her bir C++ worker süreci içinde, birden fazla iş parçacığı (thread) kullanarak eşzamanlı görev işleme kapasitesini artırabiliriz. Ancak, bu durumda görevlerin işleme mantığının iş parçacığı güvenli (thread-safe) olması gerekir.
- Yatay Ölçekleme: Birden fazla C++ worker örneğini farklı sunucularda veya aynı sunucu üzerinde ayrı süreçler (processes) olarak çalıştırabiliriz. Her worker, Redis'ten görevleri bağımsız olarak çeker ve işler. Redis'in atomik
Vaka Analizi: IoT Veri İşleme
Bir IoT sensör ağından gelen verileri işleyen bir sistemi düşünelim. Sensörler, sıcaklık, nem, basınç gibi verileri sürekli olarak gönderiyor. Bazı sensörler kritik altyapıdan (örneğin, bir nükleer reaktörün sıcaklığı) gelirken, diğerleri daha az kritik alanlardan (örneğin, bir ofis odasının nemi) geliyor. Kritik sensörlerden gelen verilerin anında işlenip alarm üretilmesi gerekirken, diğer veriler daha düşük öncelikle işlenebilir.
Bu senaryoda:
- FastAPI: Sensör ağ geçitlerinden gelen tüm verileri bir API uç noktası aracılığıyla kabul eder. Kritik sensör verilerine yüksek öncelik (örneğin, 1), diğerlerine daha düşük öncelik (örneğin, 5) atar ve Redis kuyruğuna gönderir.
- Redis: Gelen tüm sensör verilerini önceliklerine göre sıralanmış bir kuyrukta tutar.
- C++ Workers: Kuyruktan en yüksek öncelikli verileri çeker. Kritik veriler için anında analiz yapar, anormallikler tespit ederse alarm sistemlerini tetikler ve ilgili birimlere bildirim gönderir. Daha düşük öncelikli veriler için ise daha yavaş tempoda analiz yapar ve uzun vadeli depolama veya raporlama için hazırlar.
Bu sistem, kritik durumların hızla ele alınmasını sağlarken, aynı zamanda tüm verilerin tutarlı bir şekilde işlenmesini garanti eder. C++'ın performansı, yüksek hacimli sensör verilerinin düşük gecikmeyle işlenmesini mümkün kılar.
İleri Düzey Optimizasyonlar ve En İyi Uygulamalar Nelerdir?
Temel bir öncelikli görev zamanlayıcı sistemini kurduktan sonra, üretim ortamlarında daha sağlam, verimli ve güvenilir hale getirmek için çeşitli ileri düzey optimizasyonlar ve en iyi uygulamalar mevcuttur. Bu bölümde, sisteminizi bir sonraki seviyeye taşıyacak bazı önemli stratejileri inceleyeceğiz.
1. Batch İşleme (Toplu İşleme)
C++ worker'ları, Redis'ten tek tek görev çekmek yerine, belirli bir sayıda görevi toplu olarak çekebilir. Örneğin, ZPOPMIN priority_queue 10 komutu ile bir seferde 10 görevi çekebilir. Bu, her görev için ayrı ayrı Redis komutu çalıştırmanın getirdiği ağ gecikmesini (network latency) azaltır ve genel verimi artırır. Ancak, toplu işleme yaparken, bir görev işlenirken oluşan hatanın diğer görevleri etkilememesi için hata yönetiminin dikkatlice yapılması önemlidir.
2. Yeniden Deneme Mekanizmaları ve Ölü Mektup Kuyrukları (Dead-Letter Queues)
Görev işleme sırasında hatalar kaçınılmazdır. Bir görev başarısız olduğunda (örneğin, harici bir servis yanıt vermediğinde), otomatik yeniden deneme (retry) mekanizmaları uygulamak önemlidir. Görevler, belirli bir gecikmeyle tekrar kuyruğa eklenebilir. Ancak, sürekli başarısız olan görevler (zehirli görevler - "poison pill" tasks) sistem kaynaklarını tüketebilir. Bu tür görevler için, belirli sayıda denemeden sonra bir "ölü mektup kuyruğuna" (dead-letter queue - DLQ) taşınmaları gerekir. Bu, sorunlu görevleri ana iş akışından ayırarak sistemin genel sağlığını korur ve başarısız görevlerin manuel olarak incelenmesine olanak tanır.
3. Dinamik Öncelik Ayarlamaları
Bazı durumlarda, bir görevin önceliği zamanla değişebilir. Örneğin, başlangıçta düşük öncelikli olan bir raporlama görevi, belirli bir son tarihe yaklaştıkça daha yüksek öncelik kazanabilir. Bu tür senaryolar için, FastAPI API'sine görev önceliğini güncellemek için bir uç nokta ekleyebiliriz. Bu uç nokta, Redis'teki görevin skorunu (önceliğini) ZADD komutuyla güncelleyerek dinamik öncelik ayarlamalarına izin verir.
# FastAPI'de öncelik güncelleme örneği (pseudo-code)
@app.put("/tasks/{task_id}/priority")
async def update_task_priority(task_id: str, new_priority: int = Field(..., ge=1, le=10)):
# Önce görevin mevcut verilerini bulmak gerekebilir,
# ya da sadece önceliğini güncelleyerek yeni bir eleman olarak ekleyebiliriz.
# Redis Sorted Set'te aynı elemanı farklı skorla eklemek, skorunu günceller.
# Ancak burada task_id'ye göre görevin tam JSON'unu bulup güncellemek daha doğru olur.
# Bu, Redis'te ek bir ZSCAN veya HGETALL işlemi gerektirebilir
# ya da görevin tam JSON'unu da bir Redis Hash'te saklayabiliriz.
# Basit bir örnek: Görevin JSON string'ini bulup, içindeki önceliği güncelleyip tekrar ZADD yapmak.
# Bu, elemanın kendisi değiştiği için ZREM + ZADD şeklinde yapılmalı.
# Daha iyi bir yaklaşım: Görevin detaylarını Redis Hash'te (örneğin "tasks:task_id") saklayıp,
# Sorted Set'te sadece task_id'yi skor ile tutmak.
# Varsayalım ki görev detayları bir Redis Hash'te saklanıyor:
# redis_client.hset(f"task:{task_id}", "priority", new_priority)
# redis_client.zadd("priority_queue", {task_id: new_priority}) # Sadece skorunu güncelle
return {"message": f"Görev {task_id} önceliği {new_priority} olarak güncellendi."}
4. İzleme ve Günlükleme (Monitoring and Logging)
Üretim ortamındaki her sistem gibi, görev zamanlayıcımızın da kapsamlı bir şekilde izlenmesi ve günlüklenmesi gerekir. Prometheus ve Grafana gibi araçlarla Redis kuyruk boyutunu, işlenen görev sayısını, hata oranlarını ve görev işleme sürelerini takip edebiliriz. Her bileşenin (FastAPI, Redis, C++ workers) detaylı günlükler üretmesi, sorun giderme ve performans analizi için kritik öneme sahiptir. Günlükler merkezi bir günlük toplama sistemine (örneğin ELK Stack veya Loki) gönderilmelidir.
5. Güvenlik
Sistemdeki tüm bileşenler arasındaki iletişimin güvenli olduğundan emin olunmalıdır:
- API Güvenliği: FastAPI uç noktaları için API anahtarları, JWT (JSON Web Token) veya OAuth2 gibi kimlik doğrulama ve yetkilendirme mekanizmaları uygulayın.
- Redis Güvenliği: Redis sunucusuna erişimi şifre (
requirepass) ile koruyun ve yalnızca yetkili IP adreslerinden erişime izin verin. Gerekirse SSL/TLS üzerinden iletişim kurun. - Veri Şifreleme: Hassas görev verileri, Redis'e gönderilmeden önce şifrelenmeli ve C++ worker tarafından işlenmeden önce çözülmelidir.
6. Kapsamlı Testler
Birim testleri (unit tests), entegrasyon testleri ve yük testleri (load tests) sistemin farklı bileşenlerinin doğru çalıştığından ve beklenen performans seviyelerini karşıladığından emin olmak için hayati öneme sahiptir. Özellikle eşzamanlılık ve öncelik mantığı gibi karmaşık alanlar için kapsamlı test senaryoları oluşturulmalıdır.
Bu optimizasyonlar ve en iyi uygulamalar, öncelikli görev zamanlayıcı sisteminizin sadece işlevsel olmasını değil, aynı zamanda üretim ortamının zorluklarına dayanıklı, performanslı ve yönetilebilir olmasını sağlayacaktır.
Sonuç ve Sıkça Sorulan Sorular
Bu makalede, FastAPI, Redis ve C++ teknolojilerini bir araya getirerek yüksek performanslı ve öncelikli bir görev zamanlayıcı sisteminin nasıl inşa edileceğini detaylı bir şekilde inceledik. FastAPI ile kullanıcı dostu ve hızlı bir API katmanı oluşturduk, Redis'in Sorted Set veri yapısını kullanarak görevleri önceliklerine göre verimli bir şekilde yönetmeyi öğrendik ve C++ ile düşük gecikmeli, kaynak verimli görev işleyicileri geliştirdik. Bu mimari, e-ticaret, IoT, finans gibi birçok farklı sektördeki uygulamalarda kritik görevlerin zamanında ve doğru sırayla işlenmesini sağlayarak, sistemlerin genel verimliliğini ve yanıt verme yeteneğini artırır.
Ölçeklenebilirlik, hata toleransı ve performans optimizasyonları gibi konulara değinerek, bu tür bir sistemin sadece temel işlevselliği değil, aynı zamanda üretim ortamının zorluklarına karşı nasıl dirençli hale getirilebileceğini gösterdik. Her bir bileşenin bağımsız olarak ölçeklenebilmesi, sistemin değişen yüklere kolayca adapte olmasını sağlar. Güvenlik, izleme ve yeniden deneme mekanizmaları gibi ileri düzey uygulamalar, sistemin sağlamlığını ve güvenilirliğini artırır.
Bu sistem, karmaşık arka plan işleme ihtiyaçları olan her türlü modern uygulama için güçlü bir temel sunar. Doğru tasarım ve uygulama ile, en kritik görevlerinizin her zaman öncelikli olarak ele alındığından emin olabilirsiniz.
Sıkça Sorulan Sorular
-
Neden Python (FastAPI) yerine C++ worker kullanmayı tercih ettik?
C++ worker kullanmamızın temel nedeni, yüksek performans ve düşük seviyeli kaynak yönetimi ihtiyacıdır. Özellikle görevlerin çok kısa sürede işlenmesi gereken veya CPU yoğun hesaplamalar içeren senaryolarda C++'ın derlenmiş (compiled) doğası, Python'a göre önemli ölçüde daha hızlı yürütme süreleri sunar. Bu, sistemin genel verimini ve yanıt verme yeteneğini artırır. Python (FastAPI) API katmanı için geliştirme hızı ve kolaylığı nedeniyle tercih edilmiştir.
-
Redis'in Sorted Set yerine başka bir veri yapısı kullanabilir miydik?
Evet, teorik olarak başka veri yapıları da kullanılabilirdi ancak Sorted Set, öncelikli kuyruklar için Redis'in en uygun ve verimli veri yapısıdır. Listeler (Lists) FIFO kuyrukları için harikadır ama öncelik sıralaması yapmaz. Hash'ler veya String'ler görev verilerini depolamak için kullanılabilir ancak önceliklendirme ve hızlı çekme için ek mantık gerektirir. Sorted Set, üyeleri skorlarına göre otomatik olarak sıraladığı ve
ZPOPMINgibi komutlarla atomik olarak en yüksek öncelikli elemanı çekme yeteneği sunduğu için bu mimaride idealdir. -
Görev işlenirken C++ worker çökerse ne olur?
Eğer bir C++ worker bir görevi çektikten sonra işleme sırasında çökerse, görev kuyruktan zaten çıkarıldığı için kaybolmuş gibi görünebilir. Bu tür durumları ele almak için birkaç strateji vardır:
- İşleme Durumu Takibi: Görevler çekildiğinde, bir "işleniyor" durumuna geçirilebilir (örneğin, başka bir Redis Hash'e kaydedilerek). Eğer belirli bir süre içinde tamamlanmazlarsa, zaman aşımına uğramış kabul edilip tekrar kuyruğa eklenebilir veya bir ölü mektup kuyruğuna gönderilebilir.
- Atomik İşlemler: Daha karmaşık senaryolarda, görev işleme mantığı bir Redis Transaction veya LUA script'i içinde atomik olarak yürütülebilir, böylece bir hata durumunda tüm işlemler geri alınır.
- Idempotent Görevler: Görevlerin birden fazla kez işlense bile aynı sonucu vermesini sağlayacak şekilde tasarlanması (idempotency), bu tür çökme durumlarında veri tutarlılığı sorunlarını azaltır.
-
Bu sistemde görevlerin zamanlanmış (scheduled) olarak çalışmasını nasıl sağlarız?
Bu makaledeki sistem öncelikli kuyruk üzerine odaklanmıştır. Zamanlanmış görevler için farklı bir yaklaşım gerekebilir. Redis'in Sorted Set'i yine kullanılabilir, ancak skor olarak görevin çalışması gereken zaman damgası (timestamp) kullanılır. Bir worker, sürekli olarak "şimdiye kadar çalışması gereken" görevleri (yani mevcut zamandan küçük veya eşit skorlu görevleri)
ZRANGEBYSCOREile çeker ve işler. Bu, cron benzeri bir işlevsellik sağlar. -
FastAPI ve C++ arasında iletişim için HTTP dışında başka bir yol var mı?
Evet, HTTP dışında farklı iletişim mekanizmaları da kullanılabilir. Örneğin, gRPC (Google Remote Procedure Call) veya AMQP (Advanced Message Queuing Protocol) gibi mesaj kuyrukları (örneğin RabbitMQ veya Apache Kafka) kullanılabilir. Ancak, bu makaledeki mimaride Redis, hem kuyruk hem de temel iletişim aracı olarak görev yaptığı için, FastAPI ve C++ worker'ları doğrudan birbirleriyle iletişim kurmak yerine Redis üzerinden dolaylı olarak etkileşime girer. Bu, bileşenlerin bağımsızlığını artırır.
#Teknoloji #WebGeliştirme #FastAPI #Redis #C++ #GörevYönetimi #Mikroservisler #YüksekPerformans