10 sai lầm thường gặp khi sử dụng Kubernetes

Ghi chú. bản dịch.: Tác giả của bài viết này là các kỹ sư của một công ty nhỏ ở Séc, pipetail. Họ đã cố gắng đưa ra một danh sách tuyệt vời về các vấn đề [đôi khi tầm thường nhưng vẫn] rất cấp bách và những quan niệm sai lầm liên quan đến hoạt động của cụm Kubernetes.

10 sai lầm thường gặp khi sử dụng Kubernetes

Qua nhiều năm sử dụng Kubernetes, chúng tôi đã làm việc với một số lượng lớn cụm (cả được quản lý và không được quản lý - trên GCP, AWS và Azure). Theo thời gian, chúng tôi bắt đầu nhận thấy rằng một số sai lầm liên tục lặp lại. Tuy nhiên, không có gì đáng xấu hổ trong việc này: chúng tôi đã tự mình thực hiện hầu hết chúng!

Bài viết chứa các lỗi phổ biến nhất và cũng đề cập đến cách sửa chúng.

1. Tài nguyên: yêu cầu và giới hạn

Mặt hàng này chắc chắn xứng đáng được quan tâm nhiều nhất và chiếm vị trí đầu tiên trong danh sách.

Yêu cầu CPU thường hoàn toàn không được chỉ định hoặc có giá trị rất thấp (để đặt càng nhiều nhóm trên mỗi nút càng tốt). Do đó, các nút trở nên quá tải. Trong thời gian tải cao, sức mạnh xử lý của nút được sử dụng tối đa và khối lượng công việc cụ thể chỉ nhận được những gì nó "yêu cầu" bởi điều tiết CPU. Điều này dẫn đến tăng độ trễ của ứng dụng, thời gian chờ và các hậu quả khó chịu khác. (Đọc thêm về điều này trong bản dịch gần đây khác của chúng tôi: “Giới hạn CPU và điều tiết tích cực trong Kubernetes" - khoảng. dịch.)

Nỗ lực tốt nhất (vô cùng không khuyến khích):

resources: {}

Yêu cầu CPU cực kỳ thấp (cực kỳ không khuyến khích):

   resources:
      Requests:
        cpu: "1m"

Mặt khác, sự hiện diện của giới hạn CPU có thể dẫn đến việc các nhóm bỏ qua chu kỳ xung nhịp một cách không hợp lý, ngay cả khi bộ xử lý nút không được tải đầy đủ. Một lần nữa, điều này có thể dẫn đến sự chậm trễ gia tăng. Tranh cãi tiếp tục xoay quanh thông số Hạn ngạch CFS CPU trong nhân Linux và việc điều chỉnh CPU tùy thuộc vào giới hạn đã đặt, cũng như vô hiệu hóa hạn ngạch CFS... Than ôi, giới hạn CPU có thể gây ra nhiều vấn đề hơn mức chúng có thể giải quyết. Thông tin thêm về điều này có thể được tìm thấy tại liên kết dưới đây.

Lựa chọn quá mức (cam kết quá mức) vấn đề về bộ nhớ có thể dẫn đến những vấn đề lớn hơn. Đạt đến giới hạn CPU đòi hỏi phải bỏ qua chu kỳ xung nhịp, trong khi đạt đến giới hạn bộ nhớ sẽ dẫn đến việc tắt nhóm. Bạn đã bao giờ quan sát OOMkill? Vâng, đó chính xác là những gì chúng ta đang nói đến.

Bạn có muốn giảm thiểu khả năng điều này xảy ra? Không phân bổ quá mức bộ nhớ và sử dụng QoS được đảm bảo (Chất lượng dịch vụ) bằng cách đặt yêu cầu bộ nhớ ở mức giới hạn (như trong ví dụ bên dưới). Đọc thêm về điều này trong Bài thuyết trình của Henning Jacobs (Kỹ sư trưởng tại Zalando).

Có thể nổ (cơ hội bị OOMkilled cao hơn):

   resources:
      requests:
        memory: "128Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: 2

Đảm bảo:

   resources:
      requests:
        memory: "128Mi"
        cpu: 2
      limits:
        memory: "128Mi"
        cpu: 2

Điều gì có khả năng sẽ giúp ích khi thiết lập tài nguyên?

Với máy chủ số liệu bạn có thể xem mức tiêu thụ tài nguyên CPU hiện tại và mức sử dụng bộ nhớ theo nhóm (và vùng chứa bên trong chúng). Rất có thể, bạn đã sử dụng nó. Chỉ cần chạy các lệnh sau:

kubectl top pods
kubectl top pods --containers
kubectl top nodes

Tuy nhiên, chúng chỉ hiển thị mức sử dụng hiện tại. Nó có thể cho bạn ý tưởng sơ bộ về thứ tự độ lớn, nhưng cuối cùng bạn sẽ cần lịch sử thay đổi số liệu theo thời gian (để trả lời các câu hỏi như: “Tải CPU cao nhất là bao nhiêu?”, “Sáng hôm qua tải là bao nhiêu?”, v.v.). Đối với điều này bạn có thể sử dụng Prometheus, DataDog và các công cụ khác. Họ chỉ cần lấy số liệu từ máy chủ số liệu và lưu trữ chúng, đồng thời người dùng có thể truy vấn chúng và vẽ biểu đồ cho phù hợp.

VerticalPodAutoscaler cho phép tự động hóa quá trình này. Nó theo dõi lịch sử sử dụng CPU và bộ nhớ, đồng thời thiết lập các yêu cầu và giới hạn mới dựa trên thông tin này.

Sử dụng sức mạnh tính toán một cách hiệu quả không phải là một nhiệm vụ dễ dàng. Giống như chơi Tetris mọi lúc. Nếu bạn đang trả quá nhiều tiền cho sức mạnh tính toán với mức tiêu thụ trung bình thấp (giả sử ~10%), chúng tôi khuyên bạn nên xem xét các sản phẩm dựa trên AWS Fargate hoặc Virtual Kubelet. Chúng được xây dựng trên mô hình thanh toán không có máy chủ/trả tiền cho mỗi lần sử dụng, có thể rẻ hơn trong những điều kiện như vậy.

2. Thăm dò mức độ sống động và sẵn sàng

Theo mặc định, kiểm tra tính sẵn sàng và hoạt động không được bật trong Kubernetes. Và đôi khi họ quên bật chúng lên...

Nhưng bạn có thể bắt đầu khởi động lại dịch vụ bằng cách nào khác trong trường hợp xảy ra lỗi nghiêm trọng? Và làm cách nào để bộ cân bằng tải biết rằng nhóm đã sẵn sàng chấp nhận lưu lượng truy cập? Hoặc nó có thể xử lý nhiều lưu lượng truy cập hơn?

Những bài kiểm tra này thường bị nhầm lẫn với nhau:

  • Sống động - kiểm tra "khả năng sống sót", khởi động lại nhóm nếu nó bị lỗi;
  • Sẵn sàng — kiểm tra mức độ sẵn sàng, nếu thất bại, nó sẽ ngắt kết nối nhóm khỏi dịch vụ Kubernetes (điều này có thể được kiểm tra bằng cách sử dụng kubectl get endpoints) và lưu lượng truy cập không đến được cho đến khi lần kiểm tra tiếp theo được hoàn thành thành công.

Cả hai lần kiểm tra này ĐƯỢC THỰC HIỆN TRONG TOÀN BỘ VÒNG ĐỜI CỦA POD. Rất quan trọng.

Một quan niệm sai lầm phổ biến là các thăm dò mức độ sẵn sàng chỉ chạy khi khởi động để bộ cân bằng có thể biết rằng nhóm đã sẵn sàng (Ready) và có thể bắt đầu xử lý lưu lượng truy cập. Tuy nhiên, đây chỉ là một trong những lựa chọn cho việc sử dụng của họ.

Một điều nữa là khả năng phát hiện ra rằng lưu lượng truy cập trên nhóm quá nhiều và quá tải nó (hoặc nhóm thực hiện các phép tính sử dụng nhiều tài nguyên). Trong trường hợp này, việc kiểm tra tính sẵn sàng sẽ giúp giảm tải cho nhóm và “làm mát” nó. Việc hoàn thành thành công việc kiểm tra mức độ sẵn sàng trong tương lai cho phép tăng tải trên pod một lần nữa. Trong trường hợp này (nếu kiểm tra mức độ sẵn sàng không thành công), việc kiểm tra độ sống không thành công sẽ rất phản tác dụng. Tại sao phải khởi động lại một nhóm khỏe mạnh và làm việc chăm chỉ?

Do đó, trong một số trường hợp, không cần kiểm tra gì cả thì tốt hơn là kích hoạt chúng với các tham số được cấu hình không chính xác. Như đã nêu ở trên, nếu kiểm tra tính sống động của bản sao kiểm tra tính sẵn sàng, thì bạn đang gặp rắc rối lớn. Tùy chọn có thể là cấu hình chỉ kiểm tra mức độ sẵn sàngsự sống nguy hiểm bỏ qua một bên.

Cả hai loại kiểm tra này không được thất bại khi các phần phụ thuộc chung không thành công, nếu không điều này sẽ dẫn đến lỗi xếp tầng (giống như tuyết lở) đối với tất cả các nhóm. Nói cách khác, đừng làm hại chính mình.

3. LoadBalancer cho từng dịch vụ HTTP

Rất có thể, bạn có các dịch vụ HTTP trong cụm của mình mà bạn muốn chuyển tiếp ra thế giới bên ngoài.

Nếu bạn mở dịch vụ như type: LoadBalancer, bộ điều khiển của nó (tùy thuộc vào nhà cung cấp dịch vụ) sẽ cung cấp và đàm phán LoadBalancer bên ngoài (không nhất thiết phải chạy trên L7 mà thậm chí trên L4) và điều này có thể ảnh hưởng đến chi phí (địa chỉ IPv4 tĩnh bên ngoài, sức mạnh tính toán, thanh toán mỗi giây ) do nhu cầu tạo ra một số lượng lớn các tài nguyên đó.

Trong trường hợp này, sẽ hợp lý hơn nhiều khi sử dụng một bộ cân bằng tải bên ngoài, mở các dịch vụ dưới dạng type: NodePort. Hoặc tốt hơn nữa, hãy mở rộng một cái gì đó như bộ điều khiển nginx-ingress (hoặc traefik), ai sẽ là người duy nhất Cổng nút điểm cuối được liên kết với bộ cân bằng tải bên ngoài và sẽ định tuyến lưu lượng truy cập trong cụm bằng cách sử dụng xâm nhập-Tài nguyên Kubernetes.

Các dịch vụ nội cụm (vi mô) khác tương tác với nhau có thể “giao tiếp” bằng các dịch vụ như Cụm IP và cơ chế khám phá dịch vụ tích hợp thông qua DNS. Chỉ cần không sử dụng DNS/IP công cộng của họ vì điều này có thể ảnh hưởng đến độ trễ và tăng chi phí dịch vụ đám mây.

4. Tự động điều chỉnh quy mô một cụm mà không tính đến các tính năng của cụm đó

Khi thêm các nút vào và xóa chúng khỏi một cụm, bạn không nên dựa vào một số số liệu cơ bản như mức sử dụng CPU trên các nút đó. Lập kế hoạch nhóm phải tính đến nhiều những hạn chế, chẳng hạn như mối quan hệ giữa nhóm/nút, vết bẩn và dung sai, yêu cầu tài nguyên, QoS, v.v. Việc sử dụng bộ chia tỷ lệ tự động bên ngoài không tính đến các sắc thái này có thể dẫn đến sự cố.

Hãy tưởng tượng rằng một nhóm nhất định phải được lên lịch, nhưng tất cả nguồn điện CPU có sẵn đều được yêu cầu/tháo rời và nhóm đó bị mắc kẹt trong một trạng thái Pending. Bộ chia tỷ lệ tự động bên ngoài nhìn thấy tải CPU trung bình hiện tại (không phải tải được yêu cầu) và không bắt đầu mở rộng (mở rộng quy mô) - không thêm nút khác. Do đó, nhóm này sẽ không được lên lịch.

Trong trường hợp này, tỷ lệ đảo ngược (thu nhỏ) — việc xóa một nút khỏi cụm luôn khó thực hiện hơn. Hãy tưởng tượng rằng bạn có một nhóm trạng thái (với bộ lưu trữ liên tục được kết nối). Khối lượng liên tục thường thuộc về vùng sẵn sàng cụ thể và không được nhân rộng trong khu vực. Do đó, nếu bộ chia tỷ lệ tự động bên ngoài xóa một nút có nhóm này thì bộ lập lịch sẽ không thể lập lịch cho nhóm này trên một nút khác, vì việc này chỉ có thể được thực hiện trong vùng khả dụng nơi đặt bộ lưu trữ liên tục. Nhóm sẽ bị kẹt ở trạng thái Pending.

Rất phổ biến trong cộng đồng Kubernetes bộ chia tỷ lệ tự động theo cụm. Nó chạy trên một cụm, hỗ trợ API từ các nhà cung cấp đám mây lớn, tính đến tất cả các hạn chế và có thể mở rộng quy mô trong các trường hợp trên. Nó cũng có thể mở rộng quy mô trong khi duy trì tất cả các giới hạn đã đặt, do đó tiết kiệm tiền (nếu không sẽ chi cho công suất không sử dụng).

5. Bỏ qua khả năng IAM/RBAC

Cẩn thận với việc sử dụng người dùng IAM với các bí mật liên tục cho máy móc và ứng dụng. Tổ chức quyền truy cập tạm thời bằng cách sử dụng vai trò và tài khoản dịch vụ (tài khoản dịch vụ).

Chúng ta thường gặp phải thực tế là các khóa truy cập (và bí mật) được mã hóa cứng trong cấu hình ứng dụng, cũng như bỏ qua việc xoay vòng các bí mật mặc dù có quyền truy cập vào Cloud IAM. Sử dụng vai trò IAM và tài khoản dịch vụ thay vì người dùng khi thích hợp.

10 sai lầm thường gặp khi sử dụng Kubernetes

Hãy quên kube2iam đi và chuyển thẳng sang vai trò IAM cho các tài khoản dịch vụ (như được mô tả trong ghi chú cùng tên Štěpán Vraný):

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-role
  name: my-serviceaccount
  namespace: default

Một chú thích. Không khó lắm phải không?

Ngoài ra, không cấp đặc quyền cho tài khoản dịch vụ và hồ sơ cá thể admin и cluster-adminnếu họ không cần nó. Điều này khó thực hiện hơn một chút, đặc biệt là trong RBAC K8, nhưng chắc chắn đáng nỗ lực.

6. Đừng dựa vào tính năng chống ái lực tự động cho nhóm

Hãy tưởng tượng rằng bạn có ba bản sao của một số hoạt động triển khai trên một nút. Nút rơi xuống và cùng với nó là tất cả các bản sao. Tình huống khó chịu phải không? Nhưng tại sao tất cả các bản sao lại nằm trên cùng một nút? Không phải Kubernetes phải cung cấp tính sẵn sàng cao (HA) sao?!

Thật không may, bộ lập lịch Kubernetes tự mình không tuân thủ các quy tắc tồn tại riêng biệt (chống ái lực) cho vỏ quả. Chúng phải được nêu rõ ràng:

// опущено для краткости
      labels:
        app: zk
// опущено для краткости
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"

Đó là tất cả. Bây giờ các nhóm sẽ được lên lịch trên các nút khác nhau (điều kiện này chỉ được kiểm tra trong quá trình lập lịch chứ không phải trong quá trình hoạt động của chúng - do đó requiredDuringSchedulingIgnoredDuringExecution).

Ở đây chúng ta đang nói về podAntiAffinity trên các nút khác nhau: topologyKey: "kubernetes.io/hostname", - chứ không phải về các vùng sẵn sàng khác nhau. Để triển khai HA chính thức, bạn sẽ phải tìm hiểu sâu hơn về chủ đề này.

7. Bỏ qua PodDisruptionBudget

Hãy tưởng tượng rằng bạn có khối lượng sản xuất trên cụm Kubernetes. Theo định kỳ, các nút và cụm phải được cập nhật (hoặc ngừng hoạt động). PodDisruptionBudget (PDB) giống như một thỏa thuận đảm bảo dịch vụ giữa quản trị viên cụm và người dùng.

PDB cho phép bạn tránh bị gián đoạn dịch vụ do thiếu nút:

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

Trong ví dụ này, bạn, với tư cách là người dùng cụm, hãy tuyên bố với quản trị viên: “Này, tôi có dịch vụ quản lý vườn thú và bất kể bạn làm gì, tôi muốn luôn có sẵn ít nhất 2 bản sao của dịch vụ này”.

Bạn có thể đọc thêm về điều này đây.

8. Nhiều người dùng hoặc môi trường trong một cụm chung

Không gian tên Kubernetes (không gian tên) không cung cấp cách nhiệt mạnh.

Một quan niệm sai lầm phổ biến là nếu bạn triển khai tải không phải sản phẩm vào một không gian tên và tải sản phẩm vào một không gian tên khác, thì chúng sẽ sẽ không ảnh hưởng lẫn nhau dưới bất kỳ hình thức nào... Tuy nhiên, có thể đạt được một mức độ cô lập nhất định bằng cách sử dụng các yêu cầu/giới hạn tài nguyên, đặt hạn ngạch và đặt Lớp ưu tiên. Một số cách ly “vật lý” trong mặt phẳng dữ liệu được cung cấp bởi các mối quan hệ, dung sai, vết bẩn (hoặc bộ chọn nút), nhưng sự phân tách như vậy khá phức tạp. khó khăn thực hiện.

Những người cần kết hợp cả hai loại khối lượng công việc trong cùng một cụm sẽ phải đối mặt với sự phức tạp. Nếu không có nhu cầu như vậy và bạn có đủ khả năng để có một một cụm nữa (giả sử là trên đám mây công cộng), thì tốt hơn là nên làm như vậy. Điều này sẽ đạt được mức độ cách nhiệt cao hơn nhiều.

9. externalTrafficPolicy: Cụm

Chúng tôi thường quan sát thấy rằng tất cả lưu lượng truy cập bên trong cụm đều thông qua một dịch vụ như NodePort, trong đó chính sách mặc định được đặt externalTrafficPolicy: Cluster... Nó có nghĩa là Cổng nút được mở trên mọi nút trong cụm và bạn có thể sử dụng bất kỳ nút nào trong số chúng để tương tác với dịch vụ mong muốn (bộ nhóm).

10 sai lầm thường gặp khi sử dụng Kubernetes

Đồng thời, các nhóm thực được liên kết với dịch vụ NodePort nêu trên thường chỉ khả dụng trên một số tập hợp con của các nút này. Nói cách khác, nếu tôi kết nối với một nút không có nhóm cần thiết, nó sẽ chuyển tiếp lưu lượng truy cập đến một nút khác, thêm một bước nhảy và tăng độ trễ (nếu các nút nằm ở các vùng sẵn sàng/trung tâm dữ liệu khác nhau thì độ trễ có thể khá cao; ngoài ra, chi phí lưu lượng truy cập đầu ra sẽ tăng).

Mặt khác, nếu một dịch vụ Kubernetes nhất định có chính sách được đặt externalTrafficPolicy: Local, thì NodePort chỉ mở trên các nút nơi các nhóm được yêu cầu thực sự đang chạy. Khi sử dụng bộ cân bằng tải bên ngoài để kiểm tra trạng thái (kiểm tra sức khoẻ) điểm cuối (nó hoạt động như thế nào ELS ELB), Anh ta sẽ chỉ gửi lưu lượng truy cập đến các nút cần thiết, điều này sẽ có tác dụng có lợi đối với sự chậm trễ, nhu cầu tính toán, hóa đơn đầu ra (và lẽ thường cũng quy định như vậy).

Có khả năng cao là bạn đã sử dụng thứ gì đó như traefik hoặc bộ điều khiển nginx-ingress làm điểm cuối NodePort (hoặc LoadBalancer, cũng sử dụng NodePort) để định tuyến lưu lượng truy cập HTTP và việc đặt tùy chọn này có thể giảm đáng kể độ trễ cho các yêu cầu đó.

В ấn phẩm này Bạn có thể tìm hiểu thêm về externalTrafficPolicy, những ưu điểm và nhược điểm của nó.

10. Đừng bị ràng buộc vào các cụm và đừng lạm dụng mặt phẳng điều khiển

Trước đây, người ta thường gọi máy chủ bằng tên riêng: Anton, HAL9000 và Colossus... Ngày nay chúng đã được thay thế bằng các mã định danh được tạo ngẫu nhiên. Tuy nhiên, thói quen vẫn còn và bây giờ tên riêng được xếp thành cụm.

Một câu chuyện điển hình (dựa trên các sự kiện có thật): tất cả đều bắt đầu bằng một bằng chứng về khái niệm nên cụm có một cái tên đáng tự hào thử nghiệm... Đã nhiều năm trôi qua và nó VẪN được sử dụng trong sản xuất và mọi người đều ngại chạm vào nó.

Chẳng có gì thú vị khi các cụm biến thành thú cưng, vì vậy chúng tôi khuyên bạn nên loại bỏ chúng định kỳ trong khi luyện tập khắc phục thảm họa (điều này sẽ giúp kỹ thuật hỗn loạn - khoảng. dịch.). Ngoài ra, sẽ không có hại gì khi làm việc trên lớp điều khiển (mặt phẳng điều khiển). Sợ chạm vào anh ấy không phải là một dấu hiệu tốt. Vv chết? Các bạn, các bạn thực sự đang gặp rắc rối!

Mặt khác, bạn không nên quá lạm dụng việc thao túng nó. Theo thời gian lớp điều khiển có thể trở nên chậm. Rất có thể, điều này là do một số lượng lớn đối tượng được tạo mà không xoay vòng (tình huống thường gặp khi sử dụng Helm với cài đặt mặc định, đó là lý do tại sao trạng thái của nó trong configmaps/secrets không được cập nhật - kết quả là, hàng nghìn đối tượng tích lũy trong lớp điều khiển) hoặc chỉnh sửa liên tục các đối tượng kube-api (để tự động điều chỉnh tỷ lệ, cho CI/CD, để giám sát, nhật ký sự kiện, bộ điều khiển, v.v.).

Ngoài ra, chúng tôi khuyên bạn nên kiểm tra các thỏa thuận SLA/SLO với nhà cung cấp Kubernetes được quản lý và chú ý đến các bảo đảm. Người bán có thể đảm bảo tính khả dụng của lớp kiểm soát (hoặc các thành phần phụ của nó), chứ không phải độ trễ p99 của các yêu cầu bạn gửi tới nó. Nói cách khác, bạn có thể nhập kubectl get nodes, và chỉ nhận được câu trả lời sau 10 phút và điều này sẽ không vi phạm các điều khoản của thỏa thuận dịch vụ.

11. Phần thưởng: sử dụng thẻ mới nhất

Nhưng đây đã là một tác phẩm kinh điển. Gần đây, chúng tôi ít gặp kỹ thuật này hơn, vì nhiều người sau khi rút kinh nghiệm cay đắng đã ngừng sử dụng thẻ :latest và bắt đầu ghim các phiên bản. Hoan hô!

Máy tính tiền duy trì tính bất biến của thẻ hình ảnh; Chúng tôi khuyên bạn nên làm quen với tính năng đáng chú ý này.

Tóm tắt thông tin

Đừng mong đợi mọi thứ sẽ hoạt động chỉ sau một đêm: Kubernetes không phải là thuốc chữa bách bệnh. Ứng dụng xấu sẽ vẫn như vậy ngay cả trong Kubernetes (và nó có thể sẽ trở nên tồi tệ hơn). Sự bất cẩn sẽ dẫn đến sự phức tạp quá mức, công việc chậm chạp và căng thẳng của lớp điều khiển. Ngoài ra, bạn có nguy cơ bị bỏ lại nếu không có chiến lược khắc phục thảm họa. Đừng mong đợi Kubernetes sẽ cung cấp khả năng cách ly và tính sẵn sàng cao ngay từ đầu. Dành chút thời gian để làm cho ứng dụng của bạn thực sự có nguồn gốc từ đám mây.

Bạn có thể làm quen với những trải nghiệm không thành công của các đội khác nhau trong tập truyện này của Henning Jacobs.

Những người muốn thêm vào danh sách các lỗi được đưa ra trong bài viết này có thể liên hệ với chúng tôi trên Twitter (@MarekBartik, @MstrsObserver).

Tái bút từ người dịch

Đọc thêm trên blog của chúng tôi:

Nguồn: www.habr.com

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