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 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:
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.
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:
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, 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:
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:
Şimdi doğrulama işlemimizi tekrarlarsak 1000 isteğin tamamının doğru şekilde işlendiğini göreceğiz. 🎉
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):
Not: Kubernetes, Sonlandırılıyor olarak etiketledikten sonra yük dengeleyiciden gelen trafiği pod'unuza yönlendirmeyi de durdurur.
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) }
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:
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.
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.