3 poin oleh GN⁺ 2025-05-06 | 1 komentar | Bagikan ke WhatsApp
  • Graceful Shutdown pada aplikasi Go adalah prosedur penghentian yang memblokir permintaan baru, menunggu pekerjaan yang sedang berjalan selesai, lalu membersihkan resource seperti koneksi database, file lock, dan network listener
  • Penanganan shutdown dimulai dengan menerima sinyal penghentian seperti SIGTERM dan SIGINT melalui os/signal atau signal.NotifyContext di Go 1.16 ke atas untuk menggantikan perilaku default yang langsung mengakhiri proses
  • Di Kubernetes, proses shutdown harus selesai dalam grace period 30 detik secara default, dan perlu memberi waktu agar penghentian trafik menyebar sampai ke load balancer eksternal melalui jeda preStop atau kegagalan readiness probe
  • http.Server.Shutdown memblokir koneksi baru dan menunggu permintaan aktif selesai, tetapi jika handler tidak mengikuti context cancellation, masalah seperti penulisan parsial, kehilangan data, atau transaksi yang tetap terbuka bisa terjadi
  • Resource penting sebaiknya dibersihkan bukan tepat setelah sinyal shutdown diterima, melainkan setelah permintaan selesai atau setelah batas waktu habis; melakukan shutdown dalam urutan kebalikan dari inisialisasi memudahkan menjaga dependensi antarkomponen

Syarat minimum untuk Graceful Shutdown

  • Graceful Shutdown biasanya harus memenuhi tiga syarat
    • Tidak lagi menerima permintaan atau pesan baru dari titik masuk seperti HTTP atau pub/sub
    • Menunggu sampai permintaan yang sudah berjalan selesai, dan jika terlalu lama, merespons dengan graceful error
    • Melepaskan resource penting seperti koneksi database, file lock, dan network listener, lalu menjalankan pembersihan terakhir
  • Koneksi keluar ke database atau cache tidak diputus langsung pada tahap pemblokiran permintaan baru
  • Fokus pembahasan adalah server HTTP dan aplikasi container, tetapi prinsip intinya juga bisa diterapkan ke aplikasi lain

Menangani sinyal penghentian

  • Di sistem keluarga Unix, signal adalah interupsi perangkat lunak yang memberi tahu proses bahwa kondisi tertentu telah terjadi
  • Proses dapat mendaftarkan handler untuk signal tertentu, dan jika tidak ada handler maka perilaku default akan dijalankan
    • Perilaku default bisa berupa berhenti, dijeda, dilanjutkan, atau diabaikan
    • Beberapa signal seperti SIGKILL tidak bisa ditangkap atau diabaikan dan akan menghentikan proses
  • Runtime Go secara otomatis mendaftarkan berbagai signal handler seperti SIGTERM, SIGQUIT, SIGILL, dan SIGTRAP bahkan sebelum fungsi main dijalankan
  • Dalam Graceful Shutdown, ada tiga sinyal penghentian yang paling penting
    • SIGTERM: cara standar dan sopan untuk meminta proses berhenti, sekaligus signal yang dikirim Kubernetes ke aplikasi sebelum penghentian paksa
    • SIGINT: dikirim saat pengguna mencoba menghentikan proses dari terminal dengan Ctrl+C
    • SIGHUP: awalnya digunakan untuk putusnya koneksi terminal, dan sekarang juga sering dipakai sebagai sinyal reload konfigurasi
  • Tanpa penanganan tambahan, saat menerima SIGTERM, SIGINT, atau SIGHUP, runtime Go akan mengakhiri aplikasi

os/signal dan NotifyContext

  • signal.Notify memberi tahu runtime Go untuk mengirim signal tertentu ke channel alih-alih menjalankan perilaku default
  • Channel signal lebih aman dibuat dengan buffer ukuran 1
    • Secara internal, Go menggunakan select dan default saat mengirim ke channel
    • Jika masih ada ruang di buffer, signal akan dikirim; jika buffer penuh, signal akan dibuang
    • Pada channel tanpa buffer, signal bisa terlewat jika tidak ada goroutine yang sedang menerima
  • signal.Notify bisa dipanggil beberapa kali untuk signal yang sama, dan Go akan mengirim signal tersebut ke semua channel yang terdaftar
  • Menekan Ctrl+C beberapa kali biasanya tidak otomatis menaikkan sinyal kedua menjadi SIGKILL
    • Kebanyakan shell bash atau Linux tidak melakukan eskalasi otomatis
    • Untuk penghentian paksa, SIGKILL harus dikirim langsung dengan kill -9
  • Jika ingin di lingkungan pengembangan lokal penekanan Ctrl+C kedua memaksa proses berhenti, penerimaan signal tambahan bisa dihentikan dengan signal.Stop segera setelah signal pertama diterima
  • Sejak Go 1.16, signal.NotifyContext dapat menghubungkan penanganan signal dengan context cancellation
    • Bahkan setelah ctx.Done(), stop() tetap harus dipanggil agar penekanan Ctrl+C kedua bisa memaksa aplikasi berhenti

Batas waktu shutdown dan perilaku Kubernetes

  • Setelah menerima sinyal penghentian, hal pertama yang perlu diketahui adalah berapa banyak waktu shutdown yang benar-benar bisa dipakai aplikasi
  • Grace period default Kubernetes adalah 30 detik jika terminationGracePeriodSeconds tidak ditentukan secara terpisah
  • Setelah waktu ini habis, Kubernetes akan mengirim SIGKILL dan menghentikan aplikasi secara paksa
    • SIGKILL tidak bisa ditangkap atau ditangani
  • Seluruh logika shutdown, termasuk penyelesaian sisa permintaan dan pelepasan resource, harus selesai dalam waktu ini
  • Jika memakai patokan 30 detik default dan menyisakan sekitar 20% sebagai margin aman, sebaiknya seluruh shutdown selesai dalam 25 detik

Memblokir permintaan baru dan menangani readiness

  • Di net/http milik Go, Graceful Shutdown dapat dilakukan dengan http.Server.Shutdown
    • Menghentikan penerimaan koneksi baru
    • Menunggu sampai permintaan aktif selesai
    • Lalu menutup idle connection
  • Permintaan yang sudah berjalan tetap bisa diselesaikan, dan setelah selesai koneksi tersebut menjadi idle lalu ditutup
  • Klien yang mencoba membuat koneksi baru saat proses shutdown biasanya akan menerima error connection refused karena listener sudah ditutup
  • Dalam lingkungan orkestrasi berbasis container atau yang memakai load balancer eksternal, penting untuk tidak langsung menghentikan penerimaan permintaan baru
    • Pod masih bisa menerima trafik sesaat setelah ditandai untuk dihentikan
    • Komponen internal Kubernetes seperti kube-proxy dapat cepat mengetahui bahwa status pod berubah menjadi Terminating
    • Load balancer eksternal bekerja independen dari Kubernetes dan memakai health check sendiri, jadi butuh waktu agar perubahan status tersebar
  • Ada dua cara untuk menunggu propagasi penghentian trafik
    • Menambahkan sleep sejenak di hook preStop agar load balancer eksternal punya waktu mengenali bahwa pod sedang dihentikan
      • Waktu yang dipakai preStop tetap dihitung dalam terminationGracePeriodSeconds
    • Pada level kode, membuat readiness probe gagal lalu menunggu sebentar
      • Pendekatan ini bisa diterapkan bukan hanya di Kubernetes, tetapi juga di lingkungan lain yang memerlukan load balancer mengetahui status kesiapan
  • Readiness probe secara berkala memeriksa apakah container siap menerima trafik
    • Health check bisa dilakukan lewat permintaan HTTP, koneksi TCP, atau eksekusi perintah
    • Jika probe gagal, Kubernetes akan menghapus pod dari service endpoint sehingga tidak lagi menerima trafik
  • Saat bersiap shutdown, atomic.Bool seperti isShuttingDown bisa dipakai agar /healthz mengembalikan HTTP 503
  • Setelah status readiness diubah menjadi gagal, perlu menunggu beberapa detik agar perubahan tersebar
    • Contoh pengaturan menggunakan periodSeconds: 5, dan contoh di artikel memakai jeda 5 detik
    • Lama jeda yang tepat bergantung pada konfigurasi readiness probe

Menangani permintaan yang sedang berjalan

  • Buat batas waktu dengan context.WithTimeout sesuai shutdown budget lalu berikan ke server.Shutdown(ctx)
  • Ada dua kondisi saat server.Shutdown mengembalikan hasil
    • Semua koneksi aktif sudah tertutup dan semua handler selesai diproses
    • Context yang diberikan kedaluwarsa sebelum handler selesai, sehingga server menyerah menunggu
  • Dalam kedua kasus, Shutdown baru akan mengembalikan hasil setelah server benar-benar berhenti memproses permintaan
  • Handler harus bekerja cepat dan context-aware
    • Jika tidak, saat batas waktu habis pekerjaan bisa terputus di tengah jalan
    • Ini dapat menyebabkan penulisan parsial, kehilangan data, status yang tidak konsisten, transaksi yang tetap terbuka, atau data rusak
  • Ada dua cara umum untuk menyampaikan sinyal penghentian ke handler
    • Menyuntikkan logika pembatalan ke context setiap permintaan lewat middleware
    • Menyediakan global context bersama untuk semua koneksi melalui BaseContext milik http.Server
  • Pada server HTTP, context yang bisa dikustomisasi adalah BaseContext dan ConnContext
    • Untuk Graceful Shutdown, BaseContext lebih cocok karena dapat membuat global context yang bisa dibatalkan dan berlaku untuk seluruh server
  • Graceful Shutdown efektif hanya jika fungsi-fungsi menghormati pembatalan context
    • Hindari penggunaan seperti context.Background() atau time.Sleep() yang mengabaikan pembatalan
    • time.Sleep(duration) bisa diganti dengan menunggu time.After(duration) dan ctx.Done() bersama-sama menggunakan select
  • Pada versi Go yang lebih lama, time.After dapat membocorkan memori sampai timer dijalankan

Perbedaan Shutdown dan Close

  • Prinsip yang sama berlaku bukan hanya untuk server HTTP, tetapi juga untuk layanan pihak ketiga
  • database/sql punya DB.Close yang menutup koneksi database, mencegah query baru dimulai, dan menunggu query yang sedang berjalan selesai
  • Intinya adalah tidak lagi menerima permintaan atau pesan baru, lalu memberi waktu bagi pekerjaan yang sudah ada untuk selesai dalam grace period yang sudah ditentukan
  • server.Close() menghentikan server segera tanpa menunggu koneksi yang sedang berjalan
    • Handler yang sedang memakai jaringan akan menerima error saat membaca atau menulis
    • Klien dapat langsung menerima error koneksi seperti ECONNRESET atau socket hang up
    • Handler berdurasi panjang yang tidak berinteraksi dengan jaringan bisa terus berjalan di background
  • Setelah server.Shutdown() mengembalikan error, server.Close() memang bisa digunakan, tetapi itu tergantung strategi shutdown
  • Menyebarkan sinyal penghentian melalui context adalah pendekatan yang lebih andal dan lebih graceful

Urutan pelepasan resource penting

  • Kesalahan yang umum adalah melepaskan resource penting segera setelah menerima sinyal shutdown
  • Pada saat itu, handler dan permintaan in-flight mungkin masih memakai resource tersebut, jadi pembersihan resource sebaiknya ditunda sampai shutdown timeout terlewati atau semua permintaan selesai
  • Dalam banyak kasus, penghentian proses saja sudah cukup membuat sistem operasi merebut kembali resource
    • Memori yang dialokasikan Go akan dibebaskan saat proses berakhir
    • File descriptor akan ditutup oleh sistem operasi
    • Resource tingkat OS seperti process handle juga akan diambil kembali
  • Namun, ada kasus yang tetap memerlukan pembersihan eksplisit
    • Koneksi database harus ditutup dengan benar, dan transaksi yang masih terbuka perlu di-commit atau di-rollback
    • Message queue dan broker mungkin memerlukan flush pesan, commit offset, atau pemberitahuan bahwa klien sudah berhenti
    • Layanan eksternal mungkin tidak langsung mendeteksi putusnya koneksi, sehingga menutup koneksi secara manual bisa lebih cepat daripada menunggu TCP timeout
  • Aturan yang baik adalah menghentikan komponen dalam urutan kebalikan dari inisialisasi
    • defer di Go cocok untuk pola ini karena fungsi yang terakhir didaftarkan akan dijalankan lebih dulu
  • Beberapa komponen memerlukan shutdown routine tersendiri, misalnya saat data cache di memori perlu ditulis ke disk

Alur contoh lengkap

  • Contoh lengkap membangun root context yang menerima SIGINT dan SIGTERM lewat signal.NotifyContext
  • Endpoint /healthz mengembalikan HTTP 503 dan Shutting down jika isShuttingDown bernilai true, dan mengembalikan OK jika tidak
  • Handler permintaan contoh akan mengembalikan Hello, world! setelah 2 detik, atau merespons dengan HTTP request timeout jika context permintaan dibatalkan
  • ongoingCtx dihubungkan ke BaseContext agar permintaan in-flight tidak langsung dibatalkan tepat setelah SIGTERM diterima
  • Setelah menerima sinyal shutdown, proses berjalan dalam urutan berikut
    • Memanggil stop() agar penanganan default tambahan diizinkan
    • Menjalankan isShuttingDown.Store(true) untuk membuat readiness masuk ke status gagal
    • Menunggu propagasi readiness check selama 5 detik melalui _readinessDrainDelay
    • Memanggil server.Shutdown dengan batas waktu 15 detik melalui _shutdownPeriod
    • Membatalkan context yang sedang berjalan dengan stopOngoingGracefully()
    • Jika Shutdown gagal, menunggu periode pembatalan paksa selama 3 detik melalui _shutdownHardPeriod

1 komentar

 
GN⁺ 2025-05-06
Opini Hacker News
  • Saya pernah terkena masalah karena Kubernetes, pada beberapa konfigurasi, butuh waktu lebih lama dari yang saya kira untuk memperbarui IP target load balancer. Dalam kasus saya, 90% dari graceful shutdown adalah memastikan traffic benar-benar ter-drain sebelum pod dihentikan
    Setelah menambahkan sleep 15 detik pada hook preStop global, rasio HTTP 503 turun drastis, dan itu memberi waktu sejak deregistrasi load balancer dimulai hingga SIGTERM dikirim ke aplikasi, sehingga penanganan di sisi aplikasi menjadi jauh lebih sederhana

    • Benar. preStop sleep adalah solusi ajaib untuk menjaga SLO dalam rolling deployment berkualitas tinggi
      Menurut saya ada dua hal yang bisa diperbaiki Kubernetes. Pod seharusnya dihapus dulu dari Endpoints sebelum memulai urutan terminasi, dan perlu ada opsi termination delay seperti termination grace. Selain itu, PDB seharusnya punya opsi untuk mengizinkan pembuatan ulang sebelum eviksi
  • Jika endpoint Prometheus /metrics yang umum di-scrape setiap N detik, akan ada jeda ketika metrik yang dicatat antara scrape terakhir dan proses benar-benar berhenti tidak tersebar. Akibatnya, Anda bisa mendapat kesan yang keliru tentang apakah ada error selama urutan shutdown
    Jika tidak hati-hati, log beberapa detik terakhir sebelum service berhenti juga bisa hilang. Misalnya, jika file log dipantau oleh sidecar seperti Promtail atau Vector, lalu service saat start men-truncate path yang sama dan menulis lagi, akan muncul race condition yang membuat log saat shutdown hilang

    • Stack observability terasa agak absurd. Log, metrik, dan tracing masing-masing punya database, sidecar, dan stack visualisasi sendiri; library integrasi per bahasa juga berbeda-beda; dan biaya cloud-nya pun sangat besar
      Meski usaha yang dikeluarkan sebanyak itu, sebagian besar data benar-benar diabaikan, dan insight bisnisnya pun jarang jauh lebih baik daripada versi miskin berupa masuk ke server dengan ssh lalu grep file log. Saya tidak yakin apakah usaha yang dicurahkan ke ekosistem ini benar-benar meningkatkan uptime, performa, dan usability secara berarti
    • Selama lebih dari 8 tahun menangani aplikasi Go ber-beban tinggi, masalah-masalah yang saya alami ditangani oleh library platform persis dengan cara seperti ini. Mengembangkan dan memperbaiki platform serta rolling deployment di tiap perusahaan sudah seperti hobi
      Saya berencana membahas hal-hal seperti “sinkronisasi log” dan “menunggu sampai ingress mengejar liveness handler”
      https://github.com/utrack/caisson-go/blob/main/caiapp/caiapp...
      https://github.com/utrack/caisson-go/tree/main/closer
      Dokumentasinya masih kurang dan ada yang belum lengkap, tetapi saya berencana membuat rilis pertama setelah kembali dari liburan. Pada akhirnya ini akan menjadi metaplatform dan library platform referensi untuk menangani infrastruktur k8s/otel/grpc+http yang umum
    • Saya selalu tidak mengerti mengapa Prometheus dan tool terkait memakai model pull. Padahal kebanyakan memakai model push
    • Saya penasaran apakah ada yang pernah melihat solusi praktis untuk masalah ini. Jika interval scrape 15 detik, kita tidak bisa menunggu 30 detik hanya untuk mencatat metrik dua kali
      Karena perilaku seperti ini, service kami masih memakai statsd. Model berbasis push tidak memiliki masalah ini
  • Ada jebakan kecil yang sering saya lihat: orang mengira defer tetap dijalankan ketika memanggil log.Fatal. Kenyataannya tidak
    log.Fatal("fatal") secara internal memanggil os.Exit, jadi proses langsung berhenti dan defer tidak berjalan. Sebaliknya, panic("fatal") menampilkan baik fatal maupun in defer

  • Jika sistem terdistribusi bergantung pada asumsi bahwa klien harus shutdown secara elegan agar bisa bekerja dengan benar, suatu saat pasti akan rusak parah

    • Saya sangat percaya pada itu sampai-sampai saat mendesain saya tidak mempertimbangkan graceful shutdown sama sekali. Komponen harus bisa hard crash dengan aman, bahkan sering, dan selama proporsi penting dari sistem berjalan sesuai niat, seharusnya tidak ada dampak berarti pada sistem secara keseluruhan
      Satu-satunya cara memastikan sistem dapat menahan hard crash komponen adalah menjadikan hard crash sebagai hal normal yang selalu terjadi. Segala puji untuk chaos monkey
    • Ada perbedaan besar antara graceful shutdown untuk bersikap baik kepada klien atau workflow, dan klien harus bergantung padanya agar sistem bisa berfungsi
    • Dulu pada era server fisik, kami memakai STONITH untuk itu: https://smcleod.net/2015/07/delayed-serial-stonith/
    • Bahkan untuk situasi yang dapat dipulihkan, ada alasan masuk akal agar shutdown normal tidak terlihat seperti shutdown yang katastrofik
      Ada perbedaan besar antara aplikasi yang turun karena sig int dan yang mati karena kill. Misalnya, migrasi blue-green membutuhkan perilaku shutdown yang elegan
    • Benar. Namun meski software dirancang untuk tahan saat stekernya dicabut, bukan berarti saat shutdown kita benar-benar perlu mencabut stekernya
      Kalau dipikir lagi, mungkin memang perlu. Itu bisa jadi satu-satunya cara untuk menjamin asumsi tersebut benar. Mirip pendekatan chaos monkey Netflix beberapa tahun lalu
  • Saya kira ini akan membahas cara instance service baru mengambil alih listening socket dari instance lama, sehingga aplikasi bisa direstart tanpa memutus satu pun koneksi masuk
    Di systemd, ini relatif mudah diimplementasikan, dan nginx juga sudah mendukungnya selama lebih dari 20 tahun. Sayangnya Kubernetes dan Docker tidak mendukung ini karena mengasumsikan hal tersebut ditangani oleh load balancer atau reverse proxy

  • Rekan saya selalu berkata bahwa jika sebuah program tidak bisa menangani ctrl c dan beberapa perintah shutdown dengan rapi, itu adalah program yang ditulis dengan buruk

    • Ctrl-C sudah dicadangkan untuk menyalin ke clipboard. Menggunakannya sebagai aksi untuk menghentikan program sangat tidak intuitif dan akan membuat pengguna marah
  • Menurut saya Elixir menangani bagian seperti ini dengan sangat cerdas. Pengalaman saya memang belum banyak, tetapi karena proses-proses VM kecil dirancang agar bisa panic, berhenti, lalu dibuat ulang, sepertinya kebutuhan untuk sengaja membuat rutinitas graceful shutdown jadi berkurang
    Itu karena sifat seperti ini sudah tertanam dalam arsitektur aplikasi

    • Saya penasaran bagaimana hal itu menghilangkan kebutuhan graceful shutdown yang dibahas penulis
  • Saya membuat library kecil untuk menangani graceful shutdown di proyek saya: https://github.com/eberkund/graceful
    Biasanya ada beberapa layanan yang harus dijalankan, dan masing-masing sering punya cara start dan shutdown yang berbeda. Kadang objek perlu diinstansiasi dulu, kadang ada context yang ingin dibatalkan, dan kadang ada metode Stop yang harus dipanggil. Library ini dirancang untuk mengumpulkan semua itu di satu tempat dengan API yang seragam

  • Pod yang sedang shutdown, menurut definisi, tidak dalam kondisi ready. Service juga menandai endpoint sebagai terminating sekaligus not ready. Ini terjadi saat beralih ke status Terminating, jadi sebenarnya tidak perlu sengaja membuat readiness check gagal
    Saya tidak tahu persis urutan antara SIGTERM dan pembaruan objek seperti Pod.status atau endpoint slice. Mungkin ada celah kecil di mana koneksi masih bisa masuk setelah SIGTERM, tetapi itu bukan rentang besar “sampai readiness check gagal” seperti yang tersirat dalam artikel. Dari sudut pandang pengelola cluster, celah yang sangat kecil itu tidak terlalu penting. Yang penting jangan menerima koneksi baru, tutup koneksi yang sudah ada secara graceful, dan berhenti dalam waktu yang cukup cepat. Namun separuh aplikasi yang saya tangani termasuk yang memproses SIGTERM tetapi shutdown-nya lama, atau bahkan tidak bisa memproses SIGTERM namun tetap shutdown-nya lama

  • Di beberapa proyek JustWatch, kami mengadopsi Google Wire, dan itu benar-benar mengubah keadaan. Meski ternyata belum terlalu dikenal, ini membantu menghilangkan logika shutdown yang berantakan di Kubernetes
    Wire memaksa dependency injection yang rapi, sehingga sekarang semuanya shutdown dalam urutan yang ditentukan, bukan dalam urutan yang tidak jelas
    https://go.dev/blog/wire
    https://github.com/google/wire