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 chạy trên Kubernetes. các ứng dụng Go Tín hiệu trong hệ thống Unix 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 , nhưng mối quan tâm của chúng tôi chỉ là tín hiệu tắt máy: ở đây - được gửi đến một tiến trình để yêu cầu chấm dứt tiến trình đó. Được sử dụng phổ biến nhất và chúng ta sẽ tập trung vào nó sau. SIGTERM - “thoát ngay lập tức”, không thể can thiệp được. SIGKILL - tín hiệu ngắt (như Ctrl+C) SIGINT - tín hiệu thoát (như Ctrl+D) SIGQUIT 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ụ, hay còn gọi là lỗi phân đoạn được gửi bởi HĐH. SIGSEGV Dịch vụ Guinea Pig của chúng tôi Để 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 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. INCR 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: Triển khai ứng dụng Redis và Go lên Kubernetes. Sử dụng để gửi 1000 yêu cầu (25 yêu cầu/giây trong 40 giây). vegeta Trong khi Vegeta đang chạy, hãy khởi tạo Kubernetes bằng cách cập nhật thẻ hình ảnh. Rolling Update Kết nối với Redis để kiểm tra “bộ đếm”, nó phải là 1000. 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à (con số này có thể thay đổi sau mỗi lần chạy). bộ đếm nhỏ hơn 1000 Đ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. 😢 Xử lý tín hiệu trong Go Go cung cấp một gói 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. tín hiệu 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() 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 đượ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. NotifyContext stop() 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: Chúng ta có một goroutine processRequest chậm và vì chúng ta không xử lý tín hiệu kết thúc nên chương trình sẽ tự động thoát, nghĩa là tất cả các goroutine đang chạy cũng bị kết thúc. Chương trình không đóng bất kỳ kết nối nào. 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: Đã thêm để lắng nghe tín hiệu kết thúc SIGTERM. signal.NotifyContext Đã giới thiệu để theo dõi các yêu cầu đang được thực hiện (các goroutine processRequest). sync.WaitGroup Bao bọc máy chủ trong goroutine và sử dụng với ngữ cảnh để dừng chấp nhận kết nối mới một cách nhẹ nhàng. server.Shutdown Sử dụng để đảm bảo tất cả các yêu cầu đang thực hiện (processRequest goroutine) hoàn tất trước khi tiếp tục. wg.Wait() Dọn dẹp tài nguyên: Đã thêm để đóng kết nối Redis đúng cách trước khi thoát. redisdb.Close() Thoát sạch: Sử dụng để chỉ ra việc chấm dứt thành công. os.Exit(0) 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. 🎉 Khung web / Thư viện HTTP 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 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): SIGTERM Khung của bạn ngừng chấp nhận các yêu cầu đến Nó chờ mọi yêu cầu đến hiện tại hoàn tất (ngầm chờ các goroutine kết thúc). 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. Tùy chọn: Thời gian chờ tắt máy 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òng đời chấm dứt Kubernetes 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: Pod được đặt ở trạng thái “Kết thúc” và bị xóa khỏi danh sách điểm cuối của tất cả Dịch vụ. Hook sẽ được thực thi nếu được xác định. preStop Tín hiệu được gửi đến pod. Nhưng này, bây giờ ứng dụng của chúng ta biết phải làm gì! SIGTERM Kubernetes chờ thời gian gia hạn ( ), theo mặc định là 30 giây. terminalGracePeriodSeconds Tín hiệu được gửi đến pod và pod sẽ bị loại bỏ. SIGKILL 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 , cho phép ứng dụng của bạn có đủ thời gian để tắt bình thường. terminationGracePeriodSeconds Phần kết luận 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 Tài nguyên gói os/signal Vòng đời của Kubernetes Pod