paint-brush
Go'da Zarif Kapatmalarda Ustalaşma: Kubernetes İçin Kapsamlı Bir Kılavuzile@gopher
Yeni tarih

Go'da Zarif Kapatmalarda Ustalaşma: Kubernetes İçin Kapsamlı Bir Kılavuz

ile Alex6m2024/08/14
Read on Terminal Reader

Çok uzun; Okumak

Bu rehber, zarif kapatmaların dünyasını inceleyecek ve özellikle Kubernetes üzerinde çalışan Go uygulamalarındaki uygulamalarına odaklanacaktır.
featured image - Go'da Zarif Kapatmalarda Ustalaşma: Kubernetes İçin Kapsamlı Bir Kılavuz
Alex HackerNoon profile picture

Hiç sinirlenerek bilgisayarınızın güç kablosunu çekip çıkardınız mı? Bu hızlı bir çözüm gibi görünse de, veri kaybına ve sistem dengesizliğine yol açabilir. Yazılım dünyasında da benzer bir kavram vardır: sert kapatma. Bu ani sonlandırma, tıpkı fiziksel karşılığı gibi sorunlara yol açabilir. Neyse ki daha iyi bir yol var: zarif kapatma.


Zarafetli kapatmayı entegre ederek, hizmete önceden bildirim sağlıyoruz. Bu, devam eden istekleri tamamlamasını, potansiyel olarak durum bilgilerini diske kaydetmesini ve sonuç olarak kapatma sırasında veri bozulmasını önlemesini sağlar.


Bu rehber, zarif kapatmaların dünyasını inceleyecek ve özellikle Kubernetes üzerinde çalışan Go uygulamalarındaki uygulamalarına odaklanacaktır.

Unix Sistemlerindeki Sinyaller

Unix tabanlı sistemlerde zarif kapatmalar elde etmek için temel araçlardan biri, basit bir ifadeyle, belirli bir şeyi bir işlemden başka bir işleme iletmenin basit bir yolu olan sinyaller kavramıdır. Sinyallerin nasıl çalıştığını anlayarak, bunları uygulamalarımız içinde kontrollü sonlandırma prosedürlerini uygulamak için kullanabilir, sorunsuz ve veri açısından güvenli bir kapatma süreci sağlayabiliriz.


Birçok sinyal var ve bunları burada bulabilirsiniz, ancak bizim ilgilendiğimiz sadece kapanma sinyalleri:

  • SIGTERM - bir işlemin sonlandırılmasını istemek için gönderilir. En sık kullanılan ve daha sonra buna odaklanacağız.
  • SIGKILL - “Hemen çık”, müdahale edilemez.
  • SIGINT - kesme sinyali (örneğin Ctrl+C)
  • SIGQUIT - çıkış sinyali (Ctrl+D gibi)


Bu sinyaller kullanıcıdan (Ctrl+C / Ctrl+D), başka bir programdan/işlemden veya sistemin kendisinden (çekirdek/işletim sistemi) gönderilebilir; örneğin, işletim sistemi tarafından bir SIGSEGV veya diğer adıyla segmentasyon hatası gönderilir.


Gine Domuzu Hizmetimiz

Zarif kapanışların dünyasını pratik bir ortamda keşfetmek için deneyebileceğimiz basit bir hizmet oluşturalım. Bu "deney faresi" hizmeti, Redis'in INCR komutunu çağırarak gerçek dünyadaki bazı işleri simüle eden tek bir uç noktaya sahip olacak (küçük bir gecikme ekleyeceğiz). Ayrıca platformun sonlandırma sinyallerini nasıl işlediğini test etmek için temel bir Kubernetes yapılandırması da sağlayacağız.


Nihai hedef: Hizmetimizin herhangi bir istek/veri kaybetmeden kapatmaları zarif bir şekilde işlemesini sağlamak. Paralel olarak gönderilen istek sayısını Redis'teki son sayaç değeriyle karşılaştırarak, zarif kapatma uygulamamızın başarılı olup olmadığını doğrulayabileceğiz.

Kubernetes kümesinin ve Redis'in kurulumunun detaylarına girmeyeceğiz, ancak tam kurulumu Github depolarımızda bulabilirsiniz.


Doğrulama süreci şu şekildedir:

  1. Redis ve Go uygulamasını Kubernetes'e dağıtın.
  2. 1000 istek göndermek için vegeta'yı kullanın (40 saniyede 25/s).
  3. Vegeta çalışırken, görüntü etiketini güncelleyerek bir Kubernetes Rolling Update başlatın.
  4. “Sayacı” doğrulamak için Redis’e bağlanın, 1000 olmalı.


Temel Go HTTP Sunucumuzla başlayalım.

sert-kapatma/main.go

 package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main() { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_ADDR"), }) server := http.Server{ Addr: ":8080", } http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest(redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }

Bu kodu kullanarak doğrulama prosedürümüzü çalıştırdığımızda bazı isteklerin başarısız olduğunu ve sayacın 1000'den az olduğunu göreceğiz (sayı her çalıştırmada değişebilir).


Bu da açıkça, yuvarlanan güncelleme sırasında bazı verileri kaybettiğimiz anlamına geliyor. 😢

Go'da Sinyal İşleme

Go, Unix Sinyallerini işlemenize olanak tanıyan bir sinyal paketi sağlar. Varsayılan olarak SIGINT ve SIGTERM sinyallerinin Go programının çıkmasına neden olduğunu belirtmek önemlidir. Ve Go uygulamamızın bu kadar ani bir şekilde çıkmaması için gelen sinyalleri işlememiz gerekir.

Bunu yapmanın iki yolu var.


Kanal kullanımı:

 c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM)


Bağlamı kullanma (günümüzde tercih edilen yaklaşım):

 ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()


NotifyContext, listelenen sinyallerden biri geldiğinde, döndürülen stop() fonksiyonu çağrıldığında veya ana bağlamın Bitti kanalı kapatıldığında, hangisi önce gerçekleşirse, ana bağlamın bitti olarak işaretlenmiş bir kopyasını döndürür (Bitti kanalı kapalıdır).


HTTP Sunucusunun mevcut uygulamasıyla ilgili birkaç sorun var:

  1. Yavaş bir processRequest goroutine'imiz var ve sonlandırma sinyalini işlemediğimiz için program otomatik olarak sonlanıyor, bu da çalışan tüm goroutine'lerin de sonlandığı anlamına geliyor.
  2. Program hiçbir bağlantıyı kapatmıyor.


Tekrar yazalım.


zarif-kapatma/main.go

 package main // imports var wg sync.WaitGroup func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { wg.Add(1) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf("could not shutdown: %v\n", err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit(0) } func processRequest(redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }


İşte güncellemelerin özeti:

  • SIGTERM sonlandırma sinyalini dinlemek için signal.NotifyContext eklendi.
  • Uçuş halindeki istekleri (processRequest goroutines) izlemek için bir sync.WaitGroup tanıtıldı.
  • Sunucuyu bir goroutine'e sardım ve server.Shutdown komutunu kullanarak bağlamla birlikte yeni bağlantıları kabul etmeyi zarif bir şekilde durdurdum.
  • Devam etmeden önce tüm uçuş halindeki isteklerin (processRequest goroutines) tamamlandığından emin olmak için wg.Wait() kullanıldı.
  • Kaynak Temizleme: Redis bağlantısını çıkmadan önce düzgün bir şekilde kapatmak için redisdb.Close() eklendi.
  • Temiz Çıkış: Başarılı bir sonlandırmayı belirtmek için os.Exit(0) kullanıldı.

Şimdi doğrulama işlemimizi tekrarlarsak 1000 isteğin tamamının doğru şekilde işlendiğini göreceğiz. 🎉


Web çerçeveleri / HTTP kütüphanesi

Echo, Gin, Fiber ve diğerleri gibi çerçeveler, gelen her istek için bir goroutine üretecek, ona bir bağlam verecek ve ardından karar verdiğiniz yönlendirmeye bağlı olarak fonksiyonunuzu / işleyicinizi çağıracaktır. Bizim durumumuzda bu, "/incr" yolu için HandleFunc'a verilen anonim fonksiyon olacaktır.


Bir SIGTERM sinyalini yakaladığınızda ve çerçeve yapınızdan nazikçe kapanmasını istediğinizde, 2 önemli şey gerçekleşir (aşırı basitleştirmek gerekirse):

  • Çerçeveniz gelen istekleri kabul etmeyi bıraktı
  • Mevcut gelen isteklerin bitmesini bekler (dolayısıyla goroutine'lerin bitmesini bekler).


Not: Kubernetes, Sonlandırılıyor olarak etiketledikten sonra yük dengeleyiciden gelen trafiği pod'unuza yönlendirmeyi de durdurur.

İsteğe bağlı: Kapatma Zaman Aşımı

Bir işlemi sonlandırmak karmaşık olabilir, özellikle de bağlantıları kapatmak gibi birçok adım varsa. Her şeyin sorunsuz bir şekilde yürümesini sağlamak için bir zaman aşımı ayarlayabilirsiniz. Bu zaman aşımı, beklenenden daha uzun sürerse işlemden zarif bir şekilde çıkılmasını sağlayan bir güvenlik ağı görevi görür.


 shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() go func() { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("could not shutdown: %v\n", err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln("timeout exceeded, forcing shutdown") } os.Exit(0) }

Kubernetes Sonlandırma Yaşam Döngüsü

Hizmetimizi dağıtmak için Kubernetes kullandığımızdan, pod'ları nasıl sonlandırdığına daha derinlemesine bakalım. Kubernetes pod'u sonlandırmaya karar verdiğinde, aşağıdaki olaylar gerçekleşecektir:

  1. Pod, “Sonlandırılıyor” durumuna ayarlanır ve tüm Hizmetlerin uç nokta listesinden kaldırılır.
  2. preStop Hook tanımlıysa çalıştırılır.
  3. SIGTERM sinyali pod'a gönderilir. Ama hey, şimdi uygulamamız ne yapacağını biliyor!
  4. Kubernetes, varsayılan olarak 30 saniye olan bir lütuf süresi ( terminationGracePeriodSeconds ) bekler.
  5. Pod'a SIGKILL sinyali gönderilir ve pod kaldırılır.

Gördüğünüz gibi, uzun süredir devam eden bir sonlandırma süreciniz varsa, terminationGracePeriodSeconds ayarını artırmanız gerekebilir; bu, uygulamanızın düzgün bir şekilde kapanması için yeterli zamana sahip olmasını sağlar.

Çözüm

Zarif kapatmalar veri bütünlüğünü korur, kusursuz bir kullanıcı deneyimi sağlar ve kaynak yönetimini optimize eder. Zengin standart kütüphanesi ve eşzamanlılığa verdiği önemle Go, geliştiricilerin zarif kapatma uygulamalarını zahmetsizce entegre etmelerini sağlar; bu, Kubernetes gibi konteynerleştirilmiş veya düzenlenmiş ortamlarda dağıtılan uygulamalar için bir gerekliliktir.

Go kodunu ve Kubernetes manifestolarını Github depolarımızda bulabilirsiniz.

Kaynaklar