Pola Praktis untuk Mengimplementasikan Graceful Shutdown di Bahasa Go
(victoriametrics.com)- 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
SIGTERMdanSIGINTmelaluios/signalatausignal.NotifyContextdi 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
preStopatau kegagalan readiness probe http.Server.Shutdownmemblokir 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
SIGKILLtidak bisa ditangkap atau diabaikan dan akan menghentikan proses
- Runtime Go secara otomatis mendaftarkan berbagai signal handler seperti
SIGTERM,SIGQUIT,SIGILL, danSIGTRAPbahkan sebelum fungsimaindijalankan - 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 paksaSIGINT: dikirim saat pengguna mencoba menghentikan proses dari terminal denganCtrl+CSIGHUP: awalnya digunakan untuk putusnya koneksi terminal, dan sekarang juga sering dipakai sebagai sinyal reload konfigurasi
- Tanpa penanganan tambahan, saat menerima
SIGTERM,SIGINT, atauSIGHUP, runtime Go akan mengakhiri aplikasi
os/signal dan NotifyContext
signal.Notifymemberi 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
selectdandefaultsaat 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
- Secara internal, Go menggunakan
signal.Notifybisa dipanggil beberapa kali untuk signal yang sama, dan Go akan mengirim signal tersebut ke semua channel yang terdaftar- Menekan
Ctrl+Cbeberapa kali biasanya tidak otomatis menaikkan sinyal kedua menjadiSIGKILL- Kebanyakan shell bash atau Linux tidak melakukan eskalasi otomatis
- Untuk penghentian paksa,
SIGKILLharus dikirim langsung dengankill -9
- Jika ingin di lingkungan pengembangan lokal penekanan
Ctrl+Ckedua memaksa proses berhenti, penerimaan signal tambahan bisa dihentikan dengansignal.Stopsegera setelah signal pertama diterima - Sejak Go 1.16,
signal.NotifyContextdapat menghubungkan penanganan signal dengan context cancellation- Bahkan setelah
ctx.Done(),stop()tetap harus dipanggil agar penekananCtrl+Ckedua bisa memaksa aplikasi berhenti
- Bahkan setelah
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
terminationGracePeriodSecondstidak ditentukan secara terpisah - Setelah waktu ini habis, Kubernetes akan mengirim
SIGKILLdan menghentikan aplikasi secara paksaSIGKILLtidak 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/httpmilik Go, Graceful Shutdown dapat dilakukan denganhttp.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 refusedkarena 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-proxydapat cepat mengetahui bahwa status pod berubah menjadiTerminating - 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
sleepsejenak di hookpreStopagar load balancer eksternal punya waktu mengenali bahwa pod sedang dihentikan- Waktu yang dipakai
preStoptetap dihitung dalamterminationGracePeriodSeconds
- Waktu yang dipakai
- 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
- Menambahkan
- 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.BoolsepertiisShuttingDownbisa dipakai agar/healthzmengembalikan 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
- Contoh pengaturan menggunakan
Menangani permintaan yang sedang berjalan
- Buat batas waktu dengan
context.WithTimeoutsesuai shutdown budget lalu berikan keserver.Shutdown(ctx) - Ada dua kondisi saat
server.Shutdownmengembalikan 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,
Shutdownbaru 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
BaseContextmilikhttp.Server
- Pada server HTTP, context yang bisa dikustomisasi adalah
BaseContextdanConnContext- Untuk Graceful Shutdown,
BaseContextlebih cocok karena dapat membuat global context yang bisa dibatalkan dan berlaku untuk seluruh server
- Untuk Graceful Shutdown,
- Graceful Shutdown efektif hanya jika fungsi-fungsi menghormati pembatalan context
- Hindari penggunaan seperti
context.Background()atautime.Sleep()yang mengabaikan pembatalan time.Sleep(duration)bisa diganti dengan menunggutime.After(duration)danctx.Done()bersama-sama menggunakanselect
- Hindari penggunaan seperti
- Pada versi Go yang lebih lama,
time.Afterdapat membocorkan memori sampai timer dijalankan- Masalah ini sudah diperbaiki di Go 1.23 ke atas
- Jika versi Go tidak pasti, gunakan
time.NewTimer,Stop, dan bila perlu periksa<-t.C - Isu terkait: time: stop requiring Timer/Ticker.Stop for prompt GC
Perbedaan Shutdown dan Close
- Prinsip yang sama berlaku bukan hanya untuk server HTTP, tetapi juga untuk layanan pihak ketiga
database/sqlpunyaDB.Closeyang 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
ECONNRESETatausocket 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-
commitatau 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
- Koneksi database harus ditutup dengan benar, dan transaksi yang masih terbuka perlu di-
- Aturan yang baik adalah menghentikan komponen dalam urutan kebalikan dari inisialisasi
deferdi 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
SIGINTdanSIGTERMlewatsignal.NotifyContext - Endpoint
/healthzmengembalikan HTTP 503 danShutting downjikaisShuttingDownbernilai true, dan mengembalikanOKjika tidak - Handler permintaan contoh akan mengembalikan
Hello, world!setelah 2 detik, atau merespons dengan HTTP request timeout jika context permintaan dibatalkan ongoingCtxdihubungkan keBaseContextagar permintaan in-flight tidak langsung dibatalkan tepat setelahSIGTERMditerima- 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.Shutdowndengan batas waktu 15 detik melalui_shutdownPeriod - Membatalkan context yang sedang berjalan dengan
stopOngoingGracefully() - Jika
Shutdowngagal, menunggu periode pembatalan paksa selama 3 detik melalui_shutdownHardPeriod
- Memanggil
1 komentar
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
preStopglobal, rasio HTTP 503 turun drastis, dan itu memberi waktu sejak deregistrasi load balancer dimulai hinggaSIGTERMdikirim ke aplikasi, sehingga penanganan di sisi aplikasi menjadi jauh lebih sederhanapreStopsleep adalah solusi ajaib untuk menjaga SLO dalam rolling deployment berkualitas tinggiMenurut 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
/metricsyang 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 shutdownJika 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
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
sshlalugrepfile log. Saya tidak yakin apakah usaha yang dicurahkan ke ekosistem ini benar-benar meningkatkan uptime, performa, dan usability secara berartiSaya 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
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
defertetap dijalankan ketika memanggillog.Fatal. Kenyataannya tidaklog.Fatal("fatal")secara internal memanggilos.Exit, jadi proses langsung berhenti dandefertidak berjalan. Sebaliknya,panic("fatal")menampilkan baikfatalmaupunin deferJika sistem terdistribusi bergantung pada asumsi bahwa klien harus shutdown secara elegan agar bisa bekerja dengan benar, suatu saat pasti akan rusak parah
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 aplikasi yang turun karena
sig intdan yang mati karenakill. Misalnya, migrasi blue-green membutuhkan perilaku shutdown yang eleganKalau 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 cdan beberapa perintah shutdown dengan rapi, itu adalah program yang ditulis dengan burukMenurut 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 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
Stopyang harus dipanggil. Library ini dirancang untuk mengumpulkan semua itu di satu tempat dengan API yang seragamhttps://pkg.go.dev/git.sr.ht/~mariusor/wrapper#example-Regis...
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
SIGTERMdan pembaruan objek sepertiPod.statusatau endpoint slice. Mungkin ada celah kecil di mana koneksi masih bisa masuk setelahSIGTERM, 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 memprosesSIGTERMtetapi shutdown-nya lama, atau bahkan tidak bisa memprosesSIGTERMnamun tetap shutdown-nya lamaDi 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