Bạn đã bao giờ giật dây nguồn ra khỏi máy tính vì bực bội chưa? Mặc dù đây có vẻ là giải pháp nhanh chóng, nhưng nó có thể dẫn đến mất dữ liệu và hệ thống không ổn định. Trong thế giới phần mềm, có một khái niệm tương tự: tắt máy cứng. Việc tắt máy đột ngột này có thể gây ra các vấn đề giống như đối tác vật lý của nó. Rất may, có một cách tốt hơn: tắt máy nhẹ nhàng.
Bằng cách tích hợp tính năng tắt máy nhẹ nhàng, chúng tôi cung cấp thông báo trước cho dịch vụ. Điều này cho phép dịch vụ hoàn tất các yêu cầu đang diễn ra, có khả năng lưu thông tin trạng thái vào đĩa và cuối cùng là tránh hỏng dữ liệu trong quá trình tắt máy.
Hướng dẫn này sẽ đi sâu vào thế giới của các lệnh tắt máy nhẹ nhàng, đặc biệt tập trung vào việc triển khai chúng trong các ứng dụng Go chạy trên Kubernetes.
Một trong những công cụ chính để đạt được tắt máy nhẹ nhàng trong các hệ thống dựa trên Unix là khái niệm tín hiệu, nói một cách đơn giản, là một cách đơn giản để truyền đạt một điều cụ thể đến một quy trình, từ một quy trình khác. Bằng cách hiểu cách tín hiệu hoạt động, chúng ta có thể tận dụng chúng để triển khai các quy trình chấm dứt có kiểm soát trong các ứng dụng của mình, đảm bảo quy trình tắt máy mượt mà và an toàn dữ liệu.
Có nhiều tín hiệu và bạn có thể tìm thấy chúng ở đây , nhưng mối quan tâm của chúng tôi chỉ là tín hiệu tắt máy:
Các tín hiệu này có thể được gửi từ người dùng (Ctrl+C / Ctrl+D), từ một chương trình/quy trình khác hoặc từ chính hệ thống (nhân / HĐH), ví dụ, SIGSEGV hay còn gọi là lỗi phân đoạn được gửi bởi HĐH.
Để khám phá thế giới của các lần tắt máy nhẹ nhàng trong bối cảnh thực tế, hãy tạo một dịch vụ đơn giản mà chúng ta có thể thử nghiệm. Dịch vụ "guinea pig" này sẽ có một điểm cuối duy nhất mô phỏng một số công việc thực tế (chúng ta sẽ thêm một chút độ trễ) bằng cách gọi lệnh INCR của Redis. Chúng ta cũng sẽ cung cấp một cấu hình Kubernetes cơ bản để kiểm tra cách nền tảng xử lý các tín hiệu chấm dứt.
Mục tiêu cuối cùng: đảm bảo dịch vụ của chúng tôi xử lý tắt máy một cách nhẹ nhàng mà không mất bất kỳ yêu cầu/dữ liệu nào. Bằng cách so sánh số lượng yêu cầu được gửi song song với giá trị bộ đếm cuối cùng trong Redis, chúng tôi sẽ có thể xác minh xem việc triển khai tắt máy nhẹ nhàng của chúng tôi có thành công hay không.
Chúng tôi sẽ không đi sâu vào chi tiết về việc thiết lập cụm Kubernetes và Redis, nhưng bạn có thể tìm thấy hướng dẫn thiết lập đầy đủ trong kho lưu trữ Github của chúng tôi .
Quá trình xác minh như sau:
Chúng ta hãy bắt đầu với Máy chủ Go HTTP cơ bản của chúng ta.
tắt máy cứng/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") }
Khi chúng ta chạy quy trình xác minh bằng mã này, chúng ta sẽ thấy một số yêu cầu không thành công và bộ đếm nhỏ hơn 1000 (con số này có thể thay đổi sau mỗi lần chạy).
Điều này có nghĩa là chúng tôi đã mất một số dữ liệu trong quá trình cập nhật liên tục. 😢
Go cung cấp một gói tín hiệu cho phép bạn xử lý Tín hiệu Unix. Điều quan trọng cần lưu ý là theo mặc định, tín hiệu SIGINT và SIGTERM khiến chương trình Go thoát. Và để ứng dụng Go của chúng ta không thoát đột ngột như vậy, chúng ta cần xử lý các tín hiệu đến.
Có hai lựa chọn để thực hiện việc này.
Sử dụng kênh:
c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM)
Sử dụng ngữ cảnh (phương pháp được ưa chuộng hiện nay):
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()
NotifyContext trả về một bản sao của ngữ cảnh cha được đánh dấu là hoàn tất (kênh Done của ngữ cảnh này đã đóng) khi một trong các tín hiệu được liệt kê đến, khi hàm stop() được trả về được gọi hoặc khi kênh Done của ngữ cảnh cha bị đóng, tùy theo điều kiện nào xảy ra trước.
Có một số vấn đề với việc triển khai Máy chủ HTTP hiện tại của chúng tôi:
Hãy viết lại nó.
grace-shutdown/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") }
Sau đây là tóm tắt các bản cập nhật:
Bây giờ, nếu chúng ta lặp lại quy trình xác minh, chúng ta sẽ thấy rằng cả 1000 yêu cầu đều được xử lý chính xác. 🎉
Các framework như Echo, Gin, Fiber và các framework khác sẽ tạo ra một goroutine cho mỗi yêu cầu đến, cung cấp cho nó một ngữ cảnh và sau đó gọi hàm / trình xử lý của bạn tùy thuộc vào định tuyến bạn đã quyết định. Trong trường hợp của chúng tôi, đó sẽ là hàm ẩn danh được cung cấp cho HandleFunc cho đường dẫn “/incr”.
Khi bạn chặn tín hiệu SIGTERM và yêu cầu khung của bạn tắt một cách bình thường, 2 điều quan trọng sẽ xảy ra (nói một cách đơn giản):
Lưu ý: Kubernetes cũng dừng chuyển hướng lưu lượng truy cập đến từ bộ cân bằng tải đến pod của bạn sau khi nó gắn nhãn là Kết thúc.
Việc chấm dứt một tiến trình có thể phức tạp, đặc biệt là nếu có nhiều bước liên quan như đóng kết nối. Để đảm bảo mọi thứ diễn ra suôn sẻ, bạn có thể đặt thời gian chờ. Thời gian chờ này hoạt động như một lưới an toàn, thoát khỏi tiến trình một cách nhẹ nhàng nếu mất nhiều thời gian hơn dự kiến.
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) }
Vì chúng ta đã sử dụng Kubernetes để triển khai dịch vụ của mình, hãy cùng tìm hiểu sâu hơn về cách nó chấm dứt các pod. Khi Kubernetes quyết định chấm dứt pod, các sự kiện sau sẽ diễn ra:
Như bạn thấy, nếu bạn có một quy trình kết thúc chạy lâu, bạn có thể cần phải tăng thiết lập terminationGracePeriodSeconds , cho phép ứng dụng của bạn có đủ thời gian để tắt bình thường.
Graceful shutdown bảo vệ tính toàn vẹn của dữ liệu, duy trì trải nghiệm người dùng liền mạch và tối ưu hóa quản lý tài nguyên. Với thư viện chuẩn phong phú và nhấn mạnh vào tính đồng thời, Go trao quyền cho các nhà phát triển tích hợp dễ dàng các hoạt động graceful shutdown – một điều cần thiết cho các ứng dụng được triển khai trong môi trường container hoặc orchestrated như Kubernetes.
Bạn có thể tìm thấy mã Go và bản kê khai Kubernetes trong kho lưu trữ Github của chúng tôi .