İçeriğe geç

Oturum Yönetimi ve Stateless Ölçekleme

İleri 10 dk Çok sık karşılaşılır

Önce şunu oku: Spring Security Filter Chain

30 saniyede özet

Kullanıcının oturumunu sunucunun belleğinde tutarsan, o sunucu kapanınca kullanıcı da düşer. Sticky session bunu sadece erteler. JWT kimseyi düşürmez ama 'çıkış yap' demeyi zorlaştırır.

Bir alışveriş merkezinde emanet dolabına çantanı bıraktın. Ertesi gün o bina tadilat için kapandı.

Oturum hangi dolapta duruyor: bir sunucunun belleğinde mi, herkesin açabildiği bir depoda mı?
Adım adım oku
  1. Kullanıcının oturumu pod A'nın belleğinde, onun emanet dolabında duruyor.
  2. Sticky session, yük dengeleyicinin bu kullanıcıyı hep A'ya göndermesidir.
  3. Yeni sürüm çıkınca A yeniden başlar ve belleği boşalır: oturum gider, kullanıcı çıkış yapmış olur.
  4. Oturum Redis gibi ortak bir depoya taşınınca her pod aynı dolabı açabilir; A yeniden başlasa da oturum yerindedir.
  1. Bayt: Uygulamayı iki kopyaya çıkardım, kullanıcılar rastgele çıkış yapmaya başladı.

  2. Sen: Yük dengeleyicide sticky session açsan?

  3. Bayt: Açtım, düzeldi! Sonra yeni sürüm çıktım ve herkes yine çıkış yaptı.

  4. Bayt: Emanet dolabı hep aynı şubedeyse, şube kapanınca eşya da gider.

Uygulamayı iki pod’a çıkardın ve kullanıcılar rastgele düşmeye başladı. Yük dengeleyiciye sticky sessionBir kullanıcının isteklerini hep aynı sunucuya yönlendirmek. Sorunu çözmez, erteler: o sunucu ölünce veya deploy olunca oturum kaybolur.Sözlükte gör → açtın, sorun bitti. Sonra bir sürüm çıktın.

Oturum nerede duruyor?

Uygulama iki pod'a çıktı, oturumlar her pod'un belleğinde. Kullanıcı A pod'unda giriş yaptı, sonraki istek B pod'una gitti. Ne olur? Cevabı göster

Oturum bulunamaz; B pod’u onu hiç görmedi, kullanıcı yeniden giriş yapar.

HTTP durumsuzdur. “Oturum” dediğimiz şey, bir çerezle (JSESSIONID) anahtarlanan sunucu tarafı durumdur.

Birden fazla pod olunca tek soru kalır: isteği alan pod oturumu bulabilir mi?

Kafam karıştı, daha basit anlat

Oturum, vestiyerdeki paltoya benzer: fiş sende, palto bir sunucuda. Birden çok vestiyer varsa, fişini gösterdiğin vestiyerde paltonun olması gerekir.

Hızlı kontrolOrta

Bir servisin "stateless" olması tam olarak ne demek?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Oturum nerede duruyor — sticky session neden deploy’u atlatamaz

Tohum 11

Sticky session (oturum yapışkanlığı) · 3 pod

  • pod 0
  • pod 1
  • pod 2

    Yeniden giriş

    0

    kullanıcı kaç kez düştü

    Sorunsuz istek

    0/ 5

    Rolling deploy — tüm podlar yenilendi

    Çıkış gerçek mi

    —

    anında iptal

    Hız
    Adım 0

    Şu an ne oldu?

    3 pod, Sticky session (oturum yapışkanlığı)

    HTTP durumsuzdur; oturum, bir çerezle anahtarlanan sunucu tarafı durumdur. Asıl soru o durumun nerede tutulduğu.

    Aklında kalsın: Ölçekleme sorularında ilk bakılacak şey uygulamanın hangi durumu kendi belleğinde tuttuğudur.

    Olay günlüğü (0)

    Henüz olay yok. Oynat veya adımla.

    Varsayılan kurulum sticky + rolling deploy — yani dersin ana senaryosu. Şunları karşılaştır:

    1. Sticky + Hiçbir şey. Sıfır yeniden giriş.
    2. Sticky + Trafik arttı, pod eklendi. Yine sıfır.
    3. Sticky + Rolling deploy. Kullanıcı düştü: yapışkanlık pod’un ömrünü uzatmaz.
    4. Pod belleğinde, olay Hiçbir şey. Hiçbir şey olmadan bozuk.
    5. Redis. Her senaryoda sıfır.
    6. JWT. Yine sıfır, ama çıkış gerçek değil.
    Kafam karıştı, daha basit anlat

    Sunucu yeniden başlarken vestiyer boşalır. Fiş hâlâ elinde olsa bile palto artık yoktur.

    Hızlı kontrolİleri

    İki pod'lu bir serviste bu controller'a arka arkaya üç istek gidiyor. Sayaç ne gösterir?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
    PodBellegi.java
    1@RestController
    2class CounterController {
    3
    4 private int count = 0;
    5
    6 @GetMapping("/say")
    7 public int say() {
    8 return ++count;
    9 }
    10}
    Java 21UTF-8LF

    Bu çıktı garanti değildir — program non-deterministiktir, farklı çalıştırmada başka sonuç verebilir.

    Dört yol

    StratejiPod eklenincePod ölünceDeploy’daAnında çıkış
    Pod belleğinde❌ bozuk❌❌✅
    Sticky session✅❌❌✅
    Redis session✅✅✅✅
    JWT✅✅✅❌

    Tabloda okunması gereken şey, sticky satırının yarısının çalışıyor olması. Bu onu tehlikeli yapan şeydir: test ortamında sorun görünmez.

    Sticky session neden yeterli değil. Yapışkanlık, yük dengeleyiciye “bu çerezi taşıyan istekleri hep aynı pod’a yolla” der. Pod ayaktayken kusursuz çalışır.

    Ama pod ortadan kalktığında yapacak bir şeyi yoktur. Ve pod’lar ortadan kalkar:

    • Rolling deploy — tanımı gereği her pod’u yenisiyle değiştirir
    • Otomatik ölçek daraltma — trafik düşünce pod silinir
    • Node bakımı, OOM, liveness probe hatası — Kubernetes’in normal işleyişi

    Günde iki kez deploy eden bir ekipte sticky session, günde iki kez tüm kullanıcıları düşürmek demektir.

    Ayrıca yükü de bozar: yapışkanlık nedeniyle bir pod’a birikmiş oturumlar, yeni pod’lar boşta dururken o pod’u zorlar.

    Aşağıdaki örnek bir bankanın internet şubesinden: oturum Redis’te, iki adımlı transferin taslağı küçük ve serileştirilebilir, kredi belgesi nesne depolamada, sticky session gerekmiyor.

    Derinleş · İnternet şubesi: pod'a bağlı hiçbir şey 6 dosya · ~112 satır · ilk okumada atlayabilirsin
    Proje dosyaları

    src/main/resources/ application.yml Spring Session Redis: HttpSession API'si aynı kalır, veri pod belleğinde değil Redis'te durur. Bankacılıkta boşta kalma süresi kısa tutulur.

    src/main/resources/application.yml
    spring:
    data:
    redis:
    host: redis
    port: 6379
    session:
    timeout: 5m # short idle timeout: an unattended screen logs out quickly
    redis:
    namespace: ebank:session # keys: ebank:session:sessions:<id>
    flush-mode: on-save # write once per request, not on every setAttribute
    # Cookie settings live in SessionCookieConfig: with a custom CookieSerializer,
    # server.servlet.session.cookie.* is not used by Spring Session.

    src/main/java/com/bank/web/ SessionCookieConfig.java Oturum çerezinin güvenlik ayarları.

    src/main/java/com/bank/web/SessionCookieConfig.java
    @Configuration
    class SessionCookieConfig {
    @Bean
    CookieSerializer cookieSerializer() {
    var cookie = new DefaultCookieSerializer();
    cookie.setCookieName("EBANKSESSION");
    cookie.setUseHttpOnlyCookie(true); // not readable from JavaScript
    cookie.setUseSecureCookie(true); // HTTPS only
    cookie.setSameSite("Strict"); // never sent from another site, not even on links
    return cookie;
    }
    }

    src/main/java/com/bank/transfer/ TransferDraft.java Oturumda yalnızca küçük, serileştirilebilir bir değer: yarım kalmış bir transferin taslağı. Entity'ler ve bakiyeler oturuma girmez.

    src/main/java/com/bank/transfer/TransferDraft.java
    // Stored in Redis between "enter details" and "confirm with SMS code".
    // Only what the confirm step needs; the balance is read fresh, never cached here.
    public record TransferDraft(
    UUID draftId,
    String fromIban,
    String toIban,
    String receiverName,
    BigDecimal amount,
    Instant createdAt) implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    public boolean expired(Clock clock, Duration ttl) {
    return createdAt.plus(ttl).isBefore(Instant.now(clock));
    }
    }

    src/main/java/com/bank/transfer/ TransferWizardController.java İki adımlı transfer: birinci istek bir pod'a, SMS onayı başka bir pod'a düşebilir; taslak ikisinde de aynı.

    src/main/java/com/bank/transfer/TransferWizardController.java
    @RestController
    @RequestMapping("/transfers")
    class TransferWizardController {
    private static final String DRAFT = "transferDraft";
    private final TransferService transfers;
    private final OtpService otp;
    private final Clock clock;
    TransferWizardController(TransferService transfers, OtpService otp, Clock clock) {
    this.transfers = transfers;
    this.otp = otp;
    this.clock = clock;
    }
    // Step 1 (may land on pod A): validate, keep a draft, send an SMS code.
    @PostMapping("/draft")
    ResponseEntity<Void> draft(@RequestBody @Valid NewTransfer body, HttpSession session,
    @AuthenticationPrincipal Customer customer) {
    var draft = new TransferDraft(UUID.randomUUID(), body.fromIban(), body.toIban(),
    body.receiverName(), body.amount(), Instant.now(clock));
    session.setAttribute(DRAFT, draft);
    otp.send(customer.phone(), draft.draftId());
    return ResponseEntity.accepted().build();
    }
    // Step 2 (may land on pod B): the draft is still there because it lives in Redis.
    @PostMapping("/confirm")
    ResponseEntity<TransferReceipt> confirm(@RequestBody @Valid OtpCode code, HttpSession session) {
    TransferDraft draft = (TransferDraft) session.getAttribute(DRAFT);
    if (draft == null || draft.expired(clock, Duration.ofMinutes(3))) {
    return ResponseEntity.status(HttpStatus.GONE).build();
    }
    otp.verify(draft.draftId(), code.value());
    session.removeAttribute(DRAFT); // a draft is confirmed once, never twice
    return ResponseEntity.ok(transfers.execute(draft));
    }
    }

    src/main/java/com/bank/loan/ LoanDocumentController.java Kredi başvurusu belgesi yerel diske değil nesne depolamaya: başka pod da okuyabilir, pod silinince kaybolmaz.

    src/main/java/com/bank/loan/LoanDocumentController.java
    @RestController
    @RequestMapping("/loan-applications/{id}/documents")
    class LoanDocumentController {
    private final ObjectStorage storage; // S3, GCS or MinIO behind one interface
    LoanDocumentController(ObjectStorage storage) {
    this.storage = storage;
    }
    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseEntity<Void> upload(@PathVariable long id, @RequestPart MultipartFile incomeProof) throws IOException {
    // NOT incomeProof.transferTo(Path.of("/tmp/uploads/...")): the credit analyst's
    // request may land on another pod, and this pod's disk disappears with the pod.
    String key = "loan-applications/" + id + "/" + UUID.randomUUID() + ".pdf";
    storage.put(key, incomeProof.getInputStream(), incomeProof.getSize(), "application/pdf");
    return ResponseEntity.created(URI.create("/documents/" + key)).build();
    }
    }

    deploy/k8s/ service.yaml Sticky session yok: yük dengeleyici istediği pod'a gönderir.

    deploy/k8s/service.yaml
    apiVersion: v1
    kind: Service
    metadata:
    name: ebank
    spec:
    selector:
    app: ebank
    ports:
    - port: 80
    targetPort: 8080
    # No sessionAffinity: ClientIP. Any pod can serve any request, so scaling,
    # deploys and a dying pod do not log a customer out halfway through a transfer.
    Hızlı kontrolOrta

    Sticky session ne yapar ve asıl bedeli nedir?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Sticky session açık, 4 pod var ve her şey sorunsuz çalışıyor. Ekip rolling deploy yapıyor. Ne olur?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Durumu dışarı almak

    <dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    spring.session.store-type=redis
    spring.session.timeout=30m

    Spring Session, HttpSession’ı bir filtre ile değiştirir. Uygulama kodu değişmez — session.setAttribute(...) aynı kalır, arkada Redis’e yazılır.

    Kazanç yalnızca “oturum kaybolmuyor” değil. Pod’lar gerçekten değiştirilebilir hâle gelir: ölebilir, eklenebilir, yenilenebilir. Deploy ve ölçekleme sıradan işlemlere dönüşür.

    Bedeli: Redis artık kritik bir bileşendir. Düşerse tüm kullanıcılar düşer — yani yüksek erişilebilirlik ve yedekleme gerektirir.

    JWT — ve neden bedava değil. JWT ile sunucuda hiçbir şey tutulmaz. Token imzalıdır; her pod onu tek başına doğrular, hiçbir yere bakmaz.

    Simülatörde JWT her senaryoda sıfır yeniden giriş verir. Ama son iki adımı oku: çıkış yapan kullanıcının token’ı hâlâ kabul edildi.

    Sebep basit: iptal edebilmek için bir yerde kayıt tutmak gerekir, ve kayıt tutmak zaten sunucu durumudur. Bu, JWT’nin bir hatası değil, doğrudan sonucudur:

    Sunucu durumunu kaldırmak, o durumu iptal edebilme yeteneğini de kaldırır.

    Sonuçları:

    • Çıkış anlık değildir. Token süresi dolana kadar geçerli.
    • Yetki değişikliği gecikir. Eski rol token’da kalır.
    • Token büyür. Her bilgi her istekte taşınır.

    Yaygın çözüm: kısa ömürlü access token + sunucuda tutulan, iptal edilebilir refresh token. Yani tam statelessSunucunun istekler arasında istemciye özel durum saklamaması. Sınavı tek soru: rastgele bir pod'u öldürdüğünde kullanıcı bir şey kaybeder mi?Sözlükte gör → değil — “az stateful”.

    Kafam karıştı, daha basit anlat

    Paltoları ortak bir depoya koyarsan hangi vestiyere gittiğinin önemi kalmaz. Redis bu ortak depodur.

    Hızlı kontrolİleri

    Oturumu Redis'e taşımak servisi gerçekten stateless yapar mı?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Her durumu, pod belleğinde tutulmasının sorun olup olmadığına göre sınıflandır.

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Sınıflandırılmamış

    Dışarı taşınmalı

    Pod'lar arasında paylaşılması gerekiyor

      Pod'da kalabilir

      Kaybolması bir şeyi bozmaz

        Tuzaklar ve seçim

        • Tarayıcı tabanlı web uygulaması → Redis session: anında iptal, basit kod.
        • Mobil istemci, çok mikroservis → kısa ömürlü JWT + refresh token.
        • Pod belleğinde → yalnızca tek örnekte; deploy’da yine düşürür.
        • Sticky session → geçici köprü; “şimdilik” diye eklenir, kalıcı olur.

        Asıl ilke stratejiden bağımsız: uygulama kendi belleğinde kullanıcıya özel durum tutmamalıdır. Oturum bunun en görünür örneği; dosya yüklemeleri, önbellek ve zamanlanmış görev kilitleri de aynı kurala tabidir.

        Kendini sına

        Şimşek turu1/5

        HTTP kendi başına durumsuzdur.

        Soru 1/1İleri

        Bu durumda ne yaparsınız?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        SenaryoKlasik bir Spring Boot e-ticaret uygulaması 6 pod'da çalışıyor. Oturumlar pod belleğinde ve yük dengeleyicide sticky session açık. Ekip günde 2-3 kez deploy ediyor ve her deploy'da destek ekibine "sepetim boşaldı, tekrar giriş yapmak zorunda kaldım" şikâyetleri geliyor. Uygulama tarayıcı tabanlı, mobil istemci yok.

        Aklında kalacak üç şey

        1. 1 Sticky session sunucunun ömrünü uzatmaz. Yeni sunucu eklemeyi atlatır, sunucunun kapanmasını ve yeni sürümü atlatamaz.
        2. 2 Oturumu sunucunun dışına almanın asıl kazancı: sunucular gerçekten değiştirilebilir olur, yayına alma ve büyüme sıradan işlere dönüşür.
        3. 3 JWT'nin bedeli: sunucudaki kaydı kaldırmak, o oturumu iptal edebilmeyi de kaldırır. Anında çıkış istiyorsan bir yerde kayıt tutman gerekir.
        Sonraki kapı Hiç yazmadığın bir DataSource nasıl oldu da hazır geldi? Auto-configuration — Bu Bean Nereden Geldi? · 8 dk

        5 kart sonraki derste seni bekliyor

        0/5 kart bu dersten toplandı

        Bu dersin üstüne kurulanlar

        Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.