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 uygulamalarına odaklanacaktır. Go uygulamalarındaki 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ı bulabilirsiniz, ancak bizim ilgilendiğimiz sadece kapanma sinyalleri: burada - bir işlemin sonlandırılmasını istemek için gönderilir. En sık kullanılan ve daha sonra buna odaklanacağız. SIGTERM - “Hemen çık”, müdahale edilemez. SIGKILL - kesme sinyali (örneğin Ctrl+C) SIGINT - çıkış sinyali (Ctrl+D gibi) SIGQUIT 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 veya diğer adıyla segmentasyon hatası gönderilir. SIGSEGV 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 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. INCR 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 bulabilirsiniz. tam kurulumu Github depolarımızda Doğrulama süreci şu şekildedir: Redis ve Go uygulamasını Kubernetes'e dağıtın. 1000 istek göndermek için kullanın (40 saniyede 25/s). vegeta'yı Vegeta çalışırken, görüntü etiketini güncelleyerek bir Kubernetes başlatın. Rolling Update “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 göreceğiz (sayı her çalıştırmada değişebilir). sayacın 1000'den az olduğunu 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 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. sinyal 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() listelenen sinyallerden biri geldiğinde, döndürülen 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). NotifyContext, stop() HTTP Sunucusunun mevcut uygulamasıyla ilgili birkaç sorun var: 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. 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 eklendi. signal.NotifyContext Uçuş halindeki istekleri (processRequest goroutines) izlemek için bir tanıtıldı. sync.WaitGroup Sunucuyu bir goroutine'e sardım ve komutunu kullanarak bağlamla birlikte yeni bağlantıları kabul etmeyi zarif bir şekilde durdurdum. server.Shutdown Devam etmeden önce tüm uçuş halindeki isteklerin (processRequest goroutines) tamamlandığından emin olmak için kullanıldı. wg.Wait() Kaynak Temizleme: Redis bağlantısını çıkmadan önce düzgün bir şekilde kapatmak için eklendi. redisdb.Close() Temiz Çıkış: Başarılı bir sonlandırmayı belirtmek için kullanıldı. os.Exit(0) Ş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 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): SIGTERM Ç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: Pod, “Sonlandırılıyor” durumuna ayarlanır ve tüm Hizmetlerin uç nokta listesinden kaldırılır. Hook tanımlıysa çalıştırılır. preStop sinyali pod'a gönderilir. Ama hey, şimdi uygulamamız ne yapacağını biliyor! SIGTERM Kubernetes, varsayılan olarak 30 saniye olan bir lütuf süresi ( ) bekler. terminationGracePeriodSeconds Pod'a sinyali gönderilir ve pod kaldırılır. SIGKILL Gördüğünüz gibi, uzun süredir devam eden bir sonlandırma süreciniz varsa, 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. terminationGracePeriodSeconds Çö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ı bulabilirsiniz. Github depolarımızda Kaynaklar os/signal paketi Kubernetes Pod Yaşam Döngüsü