Năm lỗi khi triển khai ứng dụng đầu tiên trên Kubernetes

Năm lỗi khi triển khai ứng dụng đầu tiên trên KubernetesThất bại bởi Aris Dreamer

Nhiều người nghĩ rằng chỉ cần chuyển ứng dụng sang Kubernetes (sử dụng Helm hoặc thủ công) là đủ - và sẽ có hạnh phúc. Nhưng không phải mọi thứ đều đơn giản như vậy.

Đội Giải pháp đám mây Mail.ru đã dịch một bài báo của kỹ sư DevOps Julian Gindy. Anh ấy kể những cạm bẫy mà công ty của anh ấy gặp phải trong quá trình di chuyển để bạn không dẫm vào vết xe đổ tương tự.

Bước một: Thiết lập giới hạn và yêu cầu nhóm

Hãy bắt đầu bằng cách thiết lập một môi trường trong sạch nơi các nhóm của chúng tôi sẽ chạy. Kubernetes rất giỏi trong việc lập lịch nhóm và chuyển đổi dự phòng. Nhưng hóa ra bộ lập lịch trình đôi khi không thể đặt một nhóm nếu khó ước tính cần bao nhiêu tài nguyên để hoạt động thành công. Đây là nơi xuất hiện các yêu cầu về tài nguyên và giới hạn. Có rất nhiều cuộc tranh luận về cách tiếp cận tốt nhất để thiết lập các yêu cầu và giới hạn. Đôi khi có vẻ như đây thực sự là một nghệ thuật hơn là khoa học. Đây là cách tiếp cận của chúng tôi.

Yêu cầu nhóm là giá trị chính được bộ lập lịch sử dụng để đặt nhóm một cách tối ưu.

Của Tài liệu Kubernetes: Bước bộ lọc xác định một tập hợp các nút mà một Nhóm có thể được lên lịch. Ví dụ: bộ lọc PodFitsResources kiểm tra xem một nút có đủ tài nguyên để đáp ứng các yêu cầu tài nguyên cụ thể từ một nhóm hay không.

Chúng tôi sử dụng các yêu cầu ứng dụng theo cách mà chúng tôi có thể ước tính có bao nhiêu tài nguyên trên thực tế Ứng dụng cần nó để hoạt động bình thường. Bằng cách này, bộ lập lịch có thể đặt các nút một cách thực tế. Ban đầu, chúng tôi muốn lập lịch trình cho các yêu cầu để đảm bảo đủ tài nguyên cho từng Nhóm, nhưng chúng tôi nhận thấy rằng thời gian lập lịch trình tăng lên đáng kể và một số Nhóm không được lên lịch đầy đủ, như thể không có yêu cầu tài nguyên nào cho chúng.

Trong trường hợp này, bộ lập lịch thường "bóp" các nhóm và không thể lên lịch lại cho chúng vì mặt phẳng điều khiển không biết ứng dụng sẽ cần bao nhiêu tài nguyên, đây là thành phần chính của thuật toán lập lịch.

Giới hạn nhóm là một giới hạn rõ ràng hơn cho một nhóm. Nó biểu thị lượng tài nguyên tối đa mà cụm sẽ phân bổ cho vùng chứa.

Một lần nữa, từ tài liệu chính thức: Nếu một bộ chứa có giới hạn bộ nhớ là 4 GiB, thì kubelet (và thời gian chạy bộ chứa) sẽ thực thi giới hạn đó. Thời gian chạy ngăn vùng chứa sử dụng nhiều hơn giới hạn tài nguyên đã chỉ định. Ví dụ: khi một quy trình trong bộ chứa cố gắng sử dụng nhiều hơn dung lượng bộ nhớ cho phép, nhân hệ thống sẽ kết thúc quy trình với lỗi "hết bộ nhớ" (OOM).

Một vùng chứa luôn có thể sử dụng nhiều tài nguyên hơn mức mà yêu cầu tài nguyên chỉ định, nhưng nó không bao giờ có thể sử dụng nhiều hơn giới hạn. Giá trị này rất khó để đặt chính xác, nhưng nó rất quan trọng.

Lý tưởng nhất là chúng ta muốn các yêu cầu tài nguyên của một nhóm thay đổi trong vòng đời của một quy trình mà không can thiệp vào các quy trình khác trong hệ thống - đây là mục đích của việc đặt giới hạn.

Thật không may, tôi không thể đưa ra hướng dẫn cụ thể về những giá trị cần đặt, nhưng bản thân chúng tôi tuân thủ các quy tắc sau:

  1. Sử dụng công cụ kiểm tra tải, chúng tôi mô phỏng mức lưu lượng cơ bản và quan sát việc sử dụng tài nguyên nhóm (bộ nhớ và bộ xử lý).
  2. Đặt các yêu cầu nhóm thành một giá trị thấp tùy ý (với giới hạn tài nguyên gấp khoảng 5 lần giá trị yêu cầu) và quan sát. Khi các yêu cầu ở mức quá thấp, quy trình không thể bắt đầu, thường gây ra các lỗi thời gian chạy Go khó hiểu.

Tôi lưu ý rằng các giới hạn tài nguyên cao hơn khiến việc lập lịch trình trở nên khó khăn hơn vì nhóm cần một nút đích có đủ tài nguyên khả dụng.

Hãy tưởng tượng tình huống bạn có một máy chủ web nhẹ với giới hạn tài nguyên rất cao, chẳng hạn như bộ nhớ 4 GB. Quá trình này có thể cần phải được mở rộng theo chiều ngang và mỗi nhóm mới sẽ cần được lên lịch trên một nút có ít nhất 4 GB bộ nhớ khả dụng. Nếu không có nút nào như vậy tồn tại, cụm phải giới thiệu một nút mới để xử lý nhóm này, quá trình này có thể mất một chút thời gian. Điều quan trọng là phải đạt được sự khác biệt tối thiểu giữa các yêu cầu và giới hạn tài nguyên để đảm bảo mở rộng quy mô nhanh chóng và trơn tru.

Bước hai: Thiết lập các bài kiểm tra về mức độ sẵn sàng và mức độ sẵn sàng

Đây là một chủ đề tế nhị khác thường được thảo luận trong cộng đồng Kubernetes. Điều quan trọng là phải hiểu rõ về các bài kiểm tra Liveness và Readiness vì chúng cung cấp một cơ chế để phần mềm hoạt động ổn định và giảm thiểu thời gian chết. Tuy nhiên, chúng có thể ảnh hưởng nghiêm trọng đến hiệu suất ứng dụng của bạn nếu không được cấu hình đúng. Dưới đây là một bản tóm tắt về những gì cả hai mẫu là.

Sống động hiển thị nếu vùng chứa đang chạy. Nếu không thành công, kubelet sẽ hủy vùng chứa và chính sách khởi động lại được bật cho vùng chứa đó. Nếu thùng chứa không được trang bị Liveness Probe, thì trạng thái mặc định sẽ thành công - như đã nêu trong Tài liệu Kubernetes.

Các thăm dò Liveness phải rẻ, tức là không tiêu tốn nhiều tài nguyên, vì chúng chạy thường xuyên và sẽ thông báo cho Kubernetes rằng ứng dụng đang chạy.

Nếu bạn đặt tùy chọn chạy mỗi giây, điều này sẽ thêm 1 yêu cầu mỗi giây, vì vậy hãy lưu ý rằng sẽ cần thêm tài nguyên để xử lý lưu lượng truy cập này.

Tại công ty của chúng tôi, các bài kiểm tra Liveness kiểm tra các thành phần cốt lõi của ứng dụng, ngay cả khi dữ liệu (ví dụ: từ cơ sở dữ liệu từ xa hoặc bộ đệm ẩn) không có sẵn đầy đủ.

Chúng tôi đã thiết lập điểm cuối "sức khỏe" trong các ứng dụng chỉ trả về mã phản hồi 200. Đây là chỉ báo cho biết quy trình đang chạy và có khả năng xử lý các yêu cầu (nhưng chưa phải là lưu lượng truy cập).

Mẫu Sẵn sàng cho biết vùng chứa đã sẵn sàng để phục vụ các yêu cầu hay chưa. Nếu thăm dò mức độ sẵn sàng không thành công, bộ điều khiển điểm cuối sẽ xóa địa chỉ IP của nhóm khỏi các điểm cuối của tất cả các dịch vụ phù hợp với nhóm. Điều này cũng được nêu trong tài liệu Kubernetes.

Thăm dò mức độ sẵn sàng tiêu tốn nhiều tài nguyên hơn, vì chúng phải tấn công phần phụ trợ theo cách để cho thấy rằng ứng dụng đã sẵn sàng chấp nhận yêu cầu.

Có rất nhiều tranh luận trong cộng đồng về việc có nên truy cập cơ sở dữ liệu trực tiếp hay không. Xem xét chi phí hoạt động (việc kiểm tra diễn ra thường xuyên nhưng chúng có thể được kiểm soát), chúng tôi đã quyết định rằng đối với một số ứng dụng, mức độ sẵn sàng phục vụ lưu lượng truy cập chỉ được tính sau khi kiểm tra xem các bản ghi có được trả về từ cơ sở dữ liệu hay không. Các thử nghiệm sẵn sàng được thiết kế tốt đảm bảo mức độ sẵn sàng cao hơn và loại bỏ thời gian ngừng hoạt động trong quá trình triển khai.

Nếu bạn quyết định truy vấn cơ sở dữ liệu để kiểm tra mức độ sẵn sàng của ứng dụng, hãy đảm bảo rằng nó càng rẻ càng tốt. Hãy thực hiện truy vấn này:

SELECT small_item FROM table LIMIT 1

Dưới đây là một ví dụ về cách chúng tôi định cấu hình hai giá trị này trong Kubernetes:

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

Bạn có thể thêm một số tùy chọn cấu hình bổ sung:

  • initialDelaySeconds - bao nhiêu giây sẽ trôi qua giữa thời điểm khởi chạy hộp chứa và thời điểm bắt đầu khởi chạy đầu dò.
  • periodSeconds - khoảng thời gian chờ giữa các lần chạy mẫu.
  • timeoutSeconds — số giây sau đó nhóm được coi là khẩn cấp. Thời gian chờ bình thường.
  • failureThreshold là số lần kiểm tra không thành công trước khi tín hiệu khởi động lại được gửi đến nhóm.
  • successThreshold là số lần thử nghiệm thành công trước khi nhóm chuyển sang trạng thái sẵn sàng (sau một lần thất bại khi nhóm khởi động hoặc khôi phục).

Bước ba: Đặt chính sách mạng mặc định của Pod

Kubernetes có địa hình mạng "phẳng", theo mặc định, tất cả các nhóm giao tiếp trực tiếp với nhau. Trong một số trường hợp, điều này là không mong muốn.

Một vấn đề bảo mật tiềm ẩn là kẻ tấn công có thể sử dụng một ứng dụng dễ bị tổn thương duy nhất để gửi lưu lượng truy cập đến tất cả các nhóm trên mạng. Như trong nhiều lĩnh vực an ninh, nguyên tắc đặc quyền tối thiểu được áp dụng ở đây. Lý tưởng nhất là các chính sách mạng phải nêu rõ kết nối nào giữa các nhóm được phép và kết nối nào không.

Ví dụ: sau đây là một chính sách đơn giản từ chối tất cả lưu lượng truy cập đến cho một không gian tên cụ thể:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Trực quan hóa cấu hình này:

Năm lỗi khi triển khai ứng dụng đầu tiên trên Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Biết thêm chi tiết đây.

Bước bốn: Hành vi tùy chỉnh với Hook và Init Container

Một trong những mục tiêu chính của chúng tôi là cung cấp các triển khai trong Kubernetes mà không có thời gian chết cho các nhà phát triển. Điều này rất khó vì có nhiều tùy chọn để tắt ứng dụng và giải phóng tài nguyên đã sử dụng của chúng.

Những khó khăn đặc biệt phát sinh với Nginx. Chúng tôi nhận thấy rằng khi triển khai các Pod này theo trình tự, các kết nối đang hoạt động bị gián đoạn trước khi hoàn tất thành công.

Sau khi nghiên cứu sâu rộng trên Internet, hóa ra Kubernetes không đợi các kết nối Nginx tự cạn kiệt trước khi tắt nhóm. Với sự trợ giúp của pre-stop hook, chúng tôi đã triển khai chức năng sau và loại bỏ hoàn toàn thời gian ngừng hoạt động:

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

Nhưng nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

Một mô hình cực kỳ hữu ích khác là việc sử dụng các thùng chứa init để xử lý việc khởi chạy các ứng dụng cụ thể. Điều này đặc biệt hữu ích nếu bạn có quy trình di chuyển cơ sở dữ liệu sử dụng nhiều tài nguyên phải được chạy trước khi ứng dụng bắt đầu. Bạn cũng có thể chỉ định giới hạn tài nguyên cao hơn cho quy trình này mà không đặt giới hạn đó cho ứng dụng chính.

Một kế hoạch phổ biến khác là truy cập các bí mật trong bộ chứa init, cung cấp các thông tin xác thực này cho mô-đun chính, ngăn chặn truy cập trái phép vào các bí mật từ chính mô-đun ứng dụng chính.

Như thường lệ, một trích dẫn từ tài liệu: bộ chứa init chạy mã người dùng hoặc tiện ích một cách an toàn, nếu không sẽ ảnh hưởng đến tính bảo mật của hình ảnh bộ chứa của ứng dụng. Bằng cách tách biệt các công cụ không cần thiết, bạn hạn chế bề mặt tấn công của hình ảnh vùng chứa của ứng dụng.

Bước năm: Cấu hình hạt nhân

Cuối cùng, hãy nói về một kỹ thuật tiên tiến hơn.

Kubernetes là một nền tảng cực kỳ linh hoạt cho phép bạn chạy khối lượng công việc theo cách bạn thấy phù hợp. Chúng tôi có một số ứng dụng hiệu quả cao, cực kỳ tốn tài nguyên. Sau khi thực hiện kiểm tra tải rộng rãi, chúng tôi nhận thấy rằng một trong các ứng dụng gặp khó khăn trong việc theo kịp tải lưu lượng dự kiến ​​khi cài đặt Kubernetes mặc định có hiệu lực.

Tuy nhiên, Kubernetes cho phép bạn chạy vùng chứa đặc quyền chỉ thay đổi các tham số kernel cho một nhóm cụ thể. Đây là những gì chúng tôi đã sử dụng để thay đổi số lượng kết nối mở tối đa:

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

Đây là một kỹ thuật tiên tiến hơn thường không cần thiết. Nhưng nếu ứng dụng của bạn đang gặp khó khăn trong việc xử lý tải nặng, bạn có thể thử điều chỉnh một số cài đặt này. Thông tin thêm về quy trình này và đặt các giá trị khác nhau - như mọi khi trong tài liệu chính thức.

Kết luận

Mặc dù Kubernetes có vẻ giống như một giải pháp vượt trội, nhưng có một số bước chính phải được thực hiện để giữ cho các ứng dụng chạy trơn tru.

Trong suốt quá trình di chuyển sang Kubernetes, điều quan trọng là phải tuân theo “chu kỳ kiểm tra tải”: chạy ứng dụng, kiểm tra ứng dụng khi tải, quan sát các số liệu và hành vi mở rộng quy mô, điều chỉnh cấu hình dựa trên dữ liệu này, sau đó lặp lại chu trình này một lần nữa.

Hãy thực tế về lưu lượng truy cập dự kiến ​​và cố gắng vượt qua nó để xem thành phần nào bị hỏng trước. Với cách tiếp cận lặp đi lặp lại này, chỉ một số đề xuất được liệt kê có thể đủ để đạt được thành công. Hoặc có thể yêu cầu tùy chỉnh chuyên sâu hơn.

Hãy luôn tự hỏi mình những câu hỏi sau:

  1. Các ứng dụng tiêu thụ bao nhiêu tài nguyên và lượng này sẽ thay đổi như thế nào?
  2. Các yêu cầu mở rộng thực tế là gì? Ứng dụng sẽ xử lý trung bình bao nhiêu lưu lượng truy cập? Điều gì về lưu lượng truy cập cao điểm?
  3. Bao lâu thì dịch vụ sẽ cần phải mở rộng quy mô? Các nhóm mới cần thiết lập và chạy nhanh như thế nào để nhận được lưu lượng truy cập?
  4. Làm thế nào một cách duyên dáng làm pods tắt? Nó có cần thiết không? Có thể đạt được triển khai mà không có thời gian chết?
  5. Làm cách nào để giảm thiểu rủi ro bảo mật và hạn chế thiệt hại từ bất kỳ nhóm nào bị xâm nhập? Có bất kỳ dịch vụ nào có quyền hoặc quyền truy cập mà chúng không cần không?

Kubernetes cung cấp một nền tảng đáng kinh ngạc cho phép bạn sử dụng các phương pháp hay nhất để triển khai hàng nghìn dịch vụ trong một cụm. Tuy nhiên, tất cả các ứng dụng đều khác nhau. Đôi khi việc triển khai đòi hỏi nhiều công việc hơn một chút.

May mắn thay, Kubernetes cung cấp các cài đặt cần thiết để đạt được tất cả các mục tiêu kỹ thuật. Bằng cách sử dụng kết hợp các yêu cầu và giới hạn tài nguyên, đầu dò Liveness và Readiness, bộ chứa init, chính sách mạng và điều chỉnh nhân tùy chỉnh, bạn có thể đạt được hiệu suất cao cùng với khả năng chịu lỗi và khả năng mở rộng nhanh chóng.

Những gì khác để đọc:

  1. Các phương pháp hay nhất và phương pháp hay nhất để chạy các container và Kubernetes trong môi trường sản xuất.
  2. Hơn 90 công cụ hữu ích cho Kubernetes: Triển khai, quản lý, giám sát, bảo mật và hơn thế nữa.
  3. Kênh của chúng tôi Xung quanh Kubernetes trong Telegram.

Nguồn: www.habr.com

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