paint-brush
Làm chủ Graceful Shutdown trong Go: Hướng dẫn toàn diện cho Kubernetestừ tác giả@gopher

Làm chủ Graceful Shutdown trong Go: Hướng dẫn toàn diện cho Kubernetes

từ tác giả Alex6m2024/08/14
Read on Terminal Reader

dài quá đọc không nổi

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.
featured image - Làm chủ Graceful Shutdown trong Go: Hướng dẫn toàn diện cho Kubernetes
Alex HackerNoon profile picture

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.

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 ở đây , nhưng mối quan tâm của chúng tôi chỉ là tín hiệu tắt máy:

  • SIGTERM - đượ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.
  • SIGKILL - “thoát ngay lập tức”, không thể can thiệp được.
  • SIGINT - tín hiệu ngắt (như Ctrl+C)
  • SIGQUIT - tín hiệu thoát (như Ctrl+D)


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.


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 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:

  1. Triển khai ứng dụng Redis và Go lên Kubernetes.
  2. Sử dụng vegeta để gửi 1000 yêu cầu (25 yêu cầu/giây trong 40 giây).
  3. Trong khi Vegeta đang chạy, hãy khởi tạo Kubernetes Rolling Update bằng cách cập nhật thẻ hình ảnh.
  4. 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à 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. 😢

Xử lý tín hiệu trong Go

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:

  1. 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.
  2. 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 signal.NotifyContext để lắng nghe tín hiệu kết thúc SIGTERM.
  • Đã giới thiệu sync.WaitGroup để theo dõi các yêu cầu đang được thực hiện (các goroutine processRequest).
  • Bao bọc máy chủ trong goroutine và sử dụng server.Shutdown với ngữ cảnh để dừng chấp nhận kết nối mới một cách nhẹ nhàng.
  • Sử dụng wg.Wait() để đả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.
  • Dọn dẹp tài nguyên: Đã thêm redisdb.Close() để đóng kết nối Redis đúng cách trước khi thoát.
  • Thoát sạch: Sử dụng os.Exit(0) để chỉ ra việc chấm dứt thành công.

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 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):

  • 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:

  1. 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ụ.
  2. preStop Hook sẽ được thực thi nếu được xác định.
  3. Tín hiệu SIGTERM đượ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ì!
  4. Kubernetes chờ thời gian gia hạn ( terminalGracePeriodSeconds ), theo mặc định là 30 giây.
  5. Tín hiệu SIGKILL được gửi đến pod và pod sẽ bị loại bỏ.

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.

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