Mẹo & thủ thuật Kubernetes: tính năng tắt máy duyên dáng trong NGINX và PHP-FPM

Một điều kiện điển hình khi triển khai CI/CD trong Kubernetes: ứng dụng phải không thể chấp nhận các yêu cầu mới của khách hàng trước khi dừng hoàn toàn và quan trọng nhất là hoàn thành thành công các yêu cầu hiện có.

Mẹo & thủ thuật Kubernetes: tính năng tắt máy duyên dáng trong NGINX và PHP-FPM

Việc tuân thủ điều kiện này cho phép bạn đạt được thời gian ngừng hoạt động bằng không trong quá trình triển khai. Tuy nhiên, ngay cả khi sử dụng các gói rất phổ biến (như NGINX và PHP-FPM), bạn vẫn có thể gặp phải những khó khăn dẫn đến hàng loạt lỗi trong mỗi lần triển khai...

Lý thuyết. Cuộc sống của nhóm như thế nào

Chúng tôi đã xuất bản chi tiết về vòng đời của nhóm bài viết này. Trong bối cảnh của chủ đề đang được xem xét, chúng tôi quan tâm đến những điều sau: tại thời điểm nhóm chuyển sang trạng thái Chấm dứt, các yêu cầu mới sẽ ngừng được gửi tới nó (pod đã xóa từ danh sách điểm cuối của dịch vụ). Như vậy, để tránh thời gian chết trong quá trình triển khai, việc chúng ta giải quyết vấn đề dừng ứng dụng một cách chính xác là đủ.

Bạn cũng nên nhớ rằng thời gian gia hạn mặc định là 30 giây: sau đó, nhóm sẽ bị chấm dứt và ứng dụng phải có thời gian để xử lý tất cả các yêu cầu trước khoảng thời gian này. Ghi: mặc dù bất kỳ yêu cầu nào mất hơn 5-10 giây đều đã có vấn đề và việc tắt máy một cách nhẹ nhàng sẽ không giúp ích được gì nữa...

Để hiểu rõ hơn điều gì xảy ra khi một nhóm kết thúc, chỉ cần nhìn vào sơ đồ sau:

Mẹo & thủ thuật Kubernetes: tính năng tắt máy duyên dáng trong NGINX và PHP-FPM

A1, B1 - Nhận thay đổi về trạng thái lò sưởi
A2 - SIGTERM khởi hành
B2 - Xóa pod khỏi endpoint
B3 – Nhận thay đổi (danh sách endpoint đã thay đổi)
B4 - Cập nhật quy tắc iptables

Xin lưu ý: việc xóa nhóm điểm cuối và gửi SIGTERM không diễn ra tuần tự mà diễn ra song song. Và do Ingress không nhận ngay danh sách Endpoint cập nhật nên các yêu cầu mới từ client sẽ được gửi đến pod, điều này sẽ gây ra lỗi 500 trong quá trình chấm dứt pod (để biết thêm tài liệu chi tiết về vấn đề này, chúng tôi đã dịch). Vấn đề này cần được giải quyết theo những cách sau:

  • Gửi kết nối: đóng tiêu đề phản hồi (nếu điều này liên quan đến ứng dụng HTTP).
  • Nếu không thể thực hiện thay đổi đối với mã thì bài viết sau đây mô tả giải pháp cho phép bạn xử lý các yêu cầu cho đến hết thời gian gia hạn.

Lý thuyết. NGINX và PHP-FPM chấm dứt các quy trình của họ như thế nào

nginx

Hãy bắt đầu với NGINX, vì mọi thứ ít nhiều đều rõ ràng với nó. Đi sâu vào lý thuyết, chúng ta biết rằng NGINX có một quy trình chính và một số “công nhân” - đây là các quy trình con xử lý các yêu cầu của khách hàng. Một tùy chọn thuận tiện được cung cấp: sử dụng lệnh nginx -s <SIGNAL> chấm dứt quá trình ở chế độ tắt máy nhanh hoặc chế độ tắt máy nhẹ nhàng. Rõ ràng, lựa chọn thứ hai khiến chúng ta quan tâm.

Sau đó, mọi thứ thật đơn giản: bạn cần thêm vào preStop-hook một lệnh sẽ gửi tín hiệu tắt máy một cách duyên dáng. Điều này có thể được thực hiện trong Triển khai, trong khối container:

       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit

Bây giờ, khi nhóm tắt, chúng ta sẽ thấy thông tin sau trong nhật ký vùng chứa NGINX:

2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down

Và điều này có nghĩa là những gì chúng ta cần: NGINX đợi các yêu cầu hoàn thành và sau đó dừng quá trình đó. Tuy nhiên, dưới đây chúng ta cũng sẽ xem xét một vấn đề phổ biến mà ngay cả với lệnh nginx -s quit quá trình kết thúc không chính xác.

Và ở giai đoạn này, chúng ta đã hoàn thành NGINX: ít nhất là từ nhật ký, bạn có thể hiểu rằng mọi thứ đều hoạt động như bình thường.

Thỏa thuận với PHP-FPM là gì? Làm thế nào nó xử lý việc tắt máy một cách duyên dáng? Hãy tìm ra nó.

PHP-FPM

Trong trường hợp PHP-FPM, có ít thông tin hơn một chút. Nếu bạn tập trung vào hướng dẫn chính thức theo PHP-FPM, nó sẽ nói rằng các tín hiệu POSIX sau được chấp nhận:

  1. SIGINT, SIGTERM - tắt máy nhanh;
  2. SIGQUIT — tắt máy một cách duyên dáng (những gì chúng ta cần).

Các tín hiệu còn lại không cần thiết trong nhiệm vụ này nên chúng ta sẽ bỏ qua phân tích của chúng. Để kết thúc quá trình một cách chính xác, bạn sẽ cần viết hook preStop sau:

        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"

Thoạt nhìn, đây là tất cả những gì cần thiết để thực hiện tắt máy một cách nhẹ nhàng trong cả hai vùng chứa. Tuy nhiên, nhiệm vụ khó khăn hơn nó có vẻ. Dưới đây là hai trường hợp trong đó việc tắt máy nhẹ nhàng không có tác dụng và khiến dự án không sẵn sàng trong thời gian ngắn trong quá trình triển khai.

Luyện tập. Các vấn đề có thể xảy ra khi tắt máy một cách duyên dáng

nginx

Trước hết, cần nhớ rằng: ngoài việc thực thi lệnh nginx -s quit Có một giai đoạn nữa đáng chú ý. Chúng tôi đã gặp phải sự cố trong đó NGINX vẫn gửi SIGTERM thay vì tín hiệu SIGQUIT, khiến các yêu cầu không hoàn thành chính xác. Có thể tìm thấy những trường hợp tương tự, chẳng hạn đây. Thật không may, chúng tôi không thể xác định lý do cụ thể cho hành vi này: có sự nghi ngờ về phiên bản NGINX, nhưng nó chưa được xác nhận. Triệu chứng là các thông báo đã được quan sát thấy trong nhật ký vùng chứa NGINX: "mở socket số 10 còn lại trong kết nối 5", sau đó nhóm dừng lại.

Ví dụ: chúng ta có thể quan sát một vấn đề như vậy từ các phản hồi trên Ingress mà chúng ta cần:

Mẹo & thủ thuật Kubernetes: tính năng tắt máy duyên dáng trong NGINX và PHP-FPM
Các chỉ số mã trạng thái tại thời điểm triển khai

Trong trường hợp này, chúng tôi chỉ nhận được mã lỗi 503 từ chính Ingress: nó không thể truy cập vào vùng chứa NGINX vì nó không thể truy cập được nữa. Nếu bạn xem nhật ký vùng chứa bằng NGINX, chúng sẽ chứa thông tin sau:

[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13

Sau khi thay đổi tín hiệu dừng, container bắt đầu dừng chính xác: điều này được xác nhận bởi thực tế là lỗi 503 không còn được quan sát thấy.

Nếu bạn gặp phải vấn đề tương tự, bạn nên tìm hiểu xem tín hiệu dừng nào được sử dụng trong vùng chứa và chính xác hook preStop trông như thế nào. Rất có thể nguyên nhân nằm chính ở chỗ này.

PHP-FPM... và hơn thế nữa

Vấn đề với PHP-FPM được mô tả một cách đơn giản: nó không đợi các tiến trình con hoàn thành mà chấm dứt chúng, đó là lý do tại sao lỗi 502 xảy ra trong quá trình triển khai và các hoạt động khác. Có một số báo cáo lỗi trên bug.php.net kể từ năm 2005 (ví dụ: đây и đây), mô tả vấn đề này. Nhưng rất có thể bạn sẽ không thấy bất kỳ điều gì trong nhật ký: PHP-FPM sẽ thông báo việc hoàn tất quy trình của nó mà không có bất kỳ lỗi nào hoặc không có bất kỳ thông báo nào của bên thứ ba.

Cần làm rõ rằng bản thân vấn đề có thể phụ thuộc ở mức độ ít hơn hoặc lớn hơn vào bản thân ứng dụng và có thể không tự biểu hiện, chẳng hạn như trong quá trình giám sát. Nếu bạn gặp phải nó, trước tiên bạn nên nghĩ đến một cách giải quyết đơn giản: thêm hook preStop với sleep(30). Nó sẽ cho phép bạn hoàn thành tất cả các yêu cầu trước đó (và chúng tôi không chấp nhận những yêu cầu mới, vì pod đã có khả năng Chấm dứt) và sau 30 giây, nhóm sẽ kết thúc bằng tín hiệu SIGTERM.

Nó chỉ ra rằng lifecycle đối với container sẽ trông như thế này:

    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"

Tuy nhiên, do 30 giây sleep chúng tôi mạnh mẽ chúng tôi sẽ tăng thời gian triển khai vì mỗi nhóm sẽ bị chấm dứt tối thiểu 30 giây, thật tệ. Có thể làm gì về điều này?

Hãy chuyển sang bên chịu trách nhiệm trực tiếp thực hiện ứng dụng. Trong trường hợp của chúng tôi nó là PHP-FPMtheo mặc định không giám sát việc thực hiện các tiến trình con của nó: Quá trình chính bị chấm dứt ngay lập tức. Bạn có thể thay đổi hành vi này bằng cách sử dụng lệnh process_control_timeout, chỉ định giới hạn thời gian cho các tiến trình con chờ tín hiệu từ tiến trình chính. Nếu bạn đặt giá trị thành 20 giây, giá trị này sẽ bao gồm hầu hết các truy vấn đang chạy trong vùng chứa và sẽ dừng quy trình chính sau khi chúng hoàn tất.

Với kiến ​​thức này, chúng ta hãy quay trở lại vấn đề cuối cùng của chúng ta. Như đã đề cập, Kubernetes không phải là một nền tảng nguyên khối: việc giao tiếp giữa các thành phần khác nhau của nó cần một chút thời gian. Điều này đặc biệt đúng khi chúng tôi xem xét hoạt động của Ingresses và các thành phần liên quan khác, do sự chậm trễ như vậy tại thời điểm triển khai nên rất dễ xảy ra 500 lỗi đột ngột. Ví dụ: lỗi có thể xảy ra ở giai đoạn gửi yêu cầu lên thượng nguồn, nhưng “độ trễ thời gian” tương tác giữa các thành phần khá ngắn - chưa đến một giây.

Vì vậy, Tổng cộng với chỉ thị đã được đề cập process_control_timeout bạn có thể sử dụng cách xây dựng sau đây cho lifecycle:

lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]

Trong trường hợp này, chúng tôi sẽ bù độ trễ bằng lệnh sleep và không làm tăng đáng kể thời gian triển khai: có sự khác biệt đáng chú ý giữa 30 giây và một giây không?.. Trên thực tế, đó là process_control_timeoutlifecycle chỉ được sử dụng như một “mạng lưới an toàn” trong trường hợp bị lag.

Nói chung, hành vi được mô tả và cách giải quyết tương ứng không chỉ áp dụng cho PHP-FPM. Một tình huống tương tự có thể xảy ra theo cách này hay cách khác khi sử dụng các ngôn ngữ/khuôn khổ khác. Nếu bạn không thể khắc phục tình trạng tắt máy nhẹ nhàng theo các cách khác - ví dụ: bằng cách viết lại mã để ứng dụng xử lý chính xác các tín hiệu chấm dứt - thì bạn có thể sử dụng phương pháp được mô tả. Nó có thể không đẹp nhất, nhưng nó hoạt động.

Luyện tập. Load testing để kiểm tra hoạt động của pod

Kiểm tra tải là một trong những cách để kiểm tra cách thức hoạt động của container, vì quy trình này giúp nó đến gần hơn với điều kiện chiến đấu thực tế khi người dùng truy cập trang web. Để kiểm tra các đề xuất trên, bạn có thể sử dụng Yandex.Tankom: Nó đáp ứng mọi nhu cầu của chúng tôi một cách hoàn hảo. Sau đây là các mẹo và đề xuất để tiến hành thử nghiệm với ví dụ rõ ràng từ kinh nghiệm của chúng tôi nhờ vào biểu đồ của Grafana và Yandex.Tank.

Điều quan trọng nhất ở đây là kiểm tra các thay đổi từng bước. Sau khi thêm bản sửa lỗi mới, hãy chạy thử và xem kết quả có thay đổi gì so với lần chạy trước không. Nếu không, sẽ khó xác định được các giải pháp không hiệu quả và về lâu dài chỉ có thể gây hại (ví dụ: tăng thời gian triển khai).

Một sắc thái khác là xem nhật ký vùng chứa trong quá trình chấm dứt. Thông tin về việc tắt máy duyên dáng có được ghi lại ở đó không? Có bất kỳ lỗi nào trong nhật ký khi truy cập các tài nguyên khác (ví dụ: vào vùng chứa PHP-FPM lân cận) không? Lỗi trong chính ứng dụng (như trong trường hợp NGINX được mô tả ở trên)? Tôi hy vọng rằng thông tin giới thiệu từ bài viết này sẽ giúp bạn hiểu rõ hơn về những gì xảy ra với vùng chứa trong quá trình chấm dứt sử dụng.

Vì vậy, lần chạy thử đầu tiên đã diễn ra mà không có lifecycle và không có chỉ thị bổ sung cho máy chủ ứng dụng (process_control_timeout trong PHP-FPM). Mục đích của thử nghiệm này là xác định số lỗi gần đúng (và liệu có lỗi nào không). Ngoài ra, từ thông tin bổ sung, bạn nên biết rằng thời gian triển khai trung bình cho mỗi nhóm là khoảng 5-10 giây cho đến khi nhóm hoàn toàn sẵn sàng. Kết quả là:

Mẹo & thủ thuật Kubernetes: tính năng tắt máy duyên dáng trong NGINX và PHP-FPM

Bảng thông tin Yandex.Tank hiển thị số lỗi 502 tăng đột biến, xảy ra tại thời điểm triển khai và kéo dài trung bình tới 5 giây. Có lẽ điều này là do các yêu cầu hiện có đối với nhóm cũ đã bị chấm dứt khi nó bị chấm dứt. Sau đó, lỗi 503 xuất hiện, đó là kết quả của việc bộ chứa NGINX đã dừng, đồng thời cũng bị mất kết nối do phần phụ trợ (điều này đã ngăn Ingress kết nối với nó).

Hãy xem làm thế nào process_control_timeout trong PHP-FPM sẽ giúp chúng ta chờ đợi các tiến trình con hoàn thành, tức là. sửa những lỗi như vậy. Triển khai lại bằng lệnh này:

Mẹo & thủ thuật Kubernetes: tính năng tắt máy duyên dáng trong NGINX và PHP-FPM

Không còn lỗi nào trong lần triển khai thứ 500! Việc triển khai thành công, việc tắt máy diễn ra suôn sẻ.

Tuy nhiên, điều đáng ghi nhớ là vấn đề với vùng chứa Ingress, một tỷ lệ nhỏ lỗi mà chúng tôi có thể nhận được do độ trễ về thời gian. Để tránh chúng, tất cả những gì còn lại là thêm một cấu trúc với sleep và lặp lại việc triển khai. Tuy nhiên, trong trường hợp cụ thể của chúng tôi, không có thay đổi nào được hiển thị (một lần nữa, không có lỗi).

Kết luận

Để chấm dứt quá trình một cách khéo léo, chúng tôi mong đợi hành vi sau đây từ ứng dụng:

  1. Đợi vài giây rồi ngừng chấp nhận kết nối mới.
  2. Đợi tất cả các yêu cầu hoàn tất và đóng tất cả các kết nối cố định không thực hiện yêu cầu.
  3. Kết thúc quá trình của bạn.

Tuy nhiên, không phải tất cả các ứng dụng đều có thể hoạt động theo cách này. Một giải pháp cho vấn đề trong thực tế Kubernetes là:

  • thêm một hook pre-stop sẽ đợi vài giây;
  • nghiên cứu tệp cấu hình của chương trình phụ trợ của chúng tôi để có các thông số thích hợp.

Ví dụ về NGINX cho thấy rõ rằng ngay cả một ứng dụng lẽ ra phải xử lý chính xác các tín hiệu kết thúc ban đầu cũng có thể không thực hiện được điều đó, do đó, điều quan trọng là phải kiểm tra 500 lỗi trong quá trình triển khai ứng dụng. Điều này cũng cho phép bạn xem xét vấn đề một cách rộng hơn và không tập trung vào một nhóm hoặc vùng chứa duy nhất mà nhìn vào toàn bộ cơ sở hạ tầng một cách tổng thể.

Là một công cụ kiểm tra, bạn có thể sử dụng Yandex.Tank kết hợp với bất kỳ hệ thống giám sát nào (trong trường hợp của chúng tôi, dữ liệu được lấy từ Grafana với chương trình phụ trợ Prometheus để kiểm tra). Các vấn đề về tắt máy nhẹ nhàng có thể thấy rõ dưới tải nặng mà điểm chuẩn có thể tạo ra và việc giám sát giúp phân tích tình huống chi tiết hơn trong hoặc sau quá trình thử nghiệm.

Đáp lại phản hồi về bài viết: điều đáng nói là các vấn đề và giải pháp được mô tả ở đây liên quan đến NGINX Ingress. Đối với các trường hợp khác, có những giải pháp khác mà chúng ta có thể xem xét trong các tài liệu sau của loạt bài này.

PS

Khác với loạt mẹo & thủ thuật K8s:

Nguồn: www.habr.com

Thêm một lời nhận xét