Tinder chuyển đến Kubernetes

Viết bởi: Chris O'Brien, Giám đốc kỹ thuật | Chris Thomas, Giám đốc kỹ thuật | Jinyong Lee, Kỹ sư phần mềm cao cấp | Biên tập bởi: Cooper Jackson, Kỹ sư phần mềm

Tại sao

Gần hai năm trước, Tinder quyết định chuyển nền tảng của mình sang Kubernetes. Kubernetes đã cho chúng tôi cơ hội để thúc đẩy Tinder Engineering hướng tới việc đóng gói và vận hành cảm ứng thấp thông qua việc triển khai bất biến. Xây dựng ứng dụng, triển khai và cơ sở hạ tầng sẽ được xác định là mã.

Chúng tôi cũng đang tìm cách giải quyết những thách thức về quy mô và sự ổn định. Khi nhân rộng trở nên quan trọng, chúng tôi thường phải chịu đựng trong vài phút chờ đợi các phiên bản EC2 mới được đưa lên mạng. Ý tưởng về việc lên lịch cho các container và phục vụ lưu lượng trong vòng vài giây thay vì vài phút đã hấp dẫn chúng tôi.

Nó không dễ dàng. Trong quá trình di chuyển của chúng tôi vào đầu năm 2019, chúng tôi đã đạt được khối lượng quan trọng trong cụm Kubernetes của chúng tôi và bắt đầu gặp phải những thách thức khác nhau do lưu lượng truy cập, kích thước cụm và DNS. Chúng tôi đã giải quyết những thách thức thú vị để di chuyển 200 dịch vụ và chạy cụm Kubernetes ở quy mô tổng cộng 1.000 nút, 15.000 nhóm và 48.000 container đang chạy.

Làm sao

Bắt đầu từ tháng 1 năm 2018, chúng tôi đã làm việc theo cách của mình thông qua các giai đoạn khác nhau của nỗ lực di chuyển. Chúng tôi đã bắt đầu bằng cách chứa tất cả các dịch vụ của mình và triển khai chúng vào một loạt các môi trường dàn dựng Kubernetes. Bắt đầu từ tháng 10, chúng tôi bắt đầu di chuyển một cách có phương pháp tất cả các dịch vụ cũ của mình sang Kubernetes. Đến tháng 3 năm sau, chúng tôi đã hoàn tất quá trình di chuyển của mình và Nền tảng Tinder hiện chỉ chạy trên Kubernetes.

Xây dựng hình ảnh cho Kubernetes

Có hơn 30 kho mã nguồn cho các dịch vụ siêu nhỏ đang chạy trong cụm Kubernetes. Mã trong các kho lưu trữ này được viết bằng các ngôn ngữ khác nhau (ví dụ: Node.js, Java, Scala, Go) với nhiều môi trường thời gian chạy cho cùng một ngôn ngữ.

Hệ thống xây dựng được thiết kế để hoạt động trên bối cảnh xây dựng, có thể tùy chỉnh hoàn toàn, cho mỗi microservice, thường bao gồm Dockerfile và một loạt các lệnh shell. Mặc dù nội dung của chúng hoàn toàn có thể tùy chỉnh, các bối cảnh xây dựng này đều được viết bằng cách tuân theo một định dạng chuẩn. Việc tiêu chuẩn hóa các bối cảnh xây dựng cho phép một hệ thống xây dựng duy nhất xử lý tất cả các dịch vụ siêu nhỏ.

Hình 1 Quy trình xây dựng được chuẩn hóa thông qua bộ chứa Builder

Để đạt được sự thống nhất tối đa giữa các môi trường thời gian chạy, quy trình xây dựng tương tự đang được sử dụng trong giai đoạn phát triển và thử nghiệm. Điều này đặt ra một thách thức duy nhất khi chúng ta cần nghĩ ra một cách để đảm bảo một môi trường xây dựng nhất quán trên toàn nền tảng. Kết quả là, tất cả các quy trình xây dựng đều được thực thi bên trong một thùng chứa Bộ dựng Builder đặc biệt.

Việc triển khai bộ chứa Builder yêu cầu một số kỹ thuật Docker nâng cao. Bộ chứa Builder này kế thừa ID và bí mật người dùng cục bộ (ví dụ: khóa SSH, thông tin xác thực AWS, v.v.) theo yêu cầu để truy cập kho lưu trữ riêng của Tinder. Nó gắn kết các thư mục cục bộ chứa mã nguồn để có một cách tự nhiên để lưu trữ các tạo phẩm xây dựng. Cách tiếp cận này cải thiện hiệu suất, bởi vì nó loại bỏ việc sao chép các tạo phẩm được xây dựng giữa thùng chứa Builder và máy chủ. Các tạo phẩm xây dựng được lưu trữ sẽ được sử dụng lại vào lần tới mà không cần cấu hình thêm.

Đối với một số dịch vụ nhất định, chúng tôi cần tạo một thùng chứa khác trong Trình tạo để khớp với môi trường thời gian biên dịch với môi trường thời gian chạy (ví dụ: cài đặt thư viện bcrypt Node.js tạo ra các tạo phẩm nhị phân dành riêng cho nền tảng). Yêu cầu về thời gian biên dịch có thể khác nhau giữa các dịch vụ và Dockerfile cuối cùng được cấu thành nhanh chóng.

Kiến trúc và di chuyển cụm Kubernetes

Định cỡ cụm

Chúng tôi đã quyết định sử dụng kube-aws để cung cấp cụm tự động trên các phiên bản Amazon EC2. Ban đầu, chúng tôi đã chạy mọi thứ trong một nhóm nút chung. Chúng tôi nhanh chóng xác định sự cần thiết phải tách khối lượng công việc thành các kích cỡ và loại trường hợp khác nhau, để sử dụng tài nguyên tốt hơn. Lý do là việc chạy các nhóm có nhiều luồng hơn với nhau mang lại kết quả hiệu suất có thể dự đoán được nhiều hơn cho chúng tôi so với việc để chúng cùng tồn tại với số lượng lớn hơn các chuỗi đơn.

Chúng tôi giải quyết:

  • m5.4xlund để theo dõi (Prometheus)
  • c5.4xlarge cho khối lượng công việc Node.js (khối lượng công việc đơn luồng)
  • c5.2xlarge cho Java và Go (khối lượng công việc đa luồng)
  • c5.4xlộng cho mặt phẳng điều khiển (3 nút)

Di cư

Một trong những bước chuẩn bị cho việc di chuyển từ cơ sở hạ tầng kế thừa của chúng tôi sang Kubernetes là thay đổi giao tiếp dịch vụ hiện có để chỉ đến Bộ cân bằng tải đàn hồi (ELB) mới được tạo trong mạng con ảo riêng (VPC) cụ thể. Mạng con này được xem là VPC Kubernetes. Điều này cho phép chúng tôi di chuyển chi tiết các mô-đun mà không liên quan đến việc đặt hàng cụ thể cho các phụ thuộc dịch vụ.

Các điểm cuối này được tạo bằng các bộ bản ghi DNS có trọng số có CNAME trỏ đến từng ELB mới. Để cắt, chúng tôi đã thêm một bản ghi mới, chỉ vào ELB dịch vụ Kubernetes mới, với trọng số 0. Sau đó, chúng tôi đặt Thời gian sống (TTL) trên bản ghi được đặt thành 0. Trọng lượng cũ và mới sau đó được điều chỉnh từ từ cuối cùng kết thúc với 100% trên máy chủ mới. Sau khi quá trình cắt hoàn thành, TTL được đặt thành một cái gì đó hợp lý hơn.

Các mô-đun Java của chúng tôi tôn vinh DNS DNS thấp, nhưng các ứng dụng Node của chúng tôi thì không. Một trong những kỹ sư của chúng tôi viết lại một phần mã nhóm kết nối để bọc nó trong trình quản lý sẽ làm mới các nhóm sau mỗi 60 giây. Điều này làm việc rất tốt cho chúng tôi với hiệu suất không đáng kể.

Học

Giới hạn vải mạng

Vào đầu giờ sáng ngày 8 tháng 1 năm 2019, Nền tảng của Tinder bị mất điện liên tục. Để đáp ứng với sự gia tăng không liên quan đến độ trễ của nền tảng vào sáng sớm hôm đó, số lượng nhóm và số nút đã được thu nhỏ trên cụm. Điều này dẫn đến việc cạn kiệt bộ đệm ARP trên tất cả các nút của chúng tôi.

Có ba giá trị Linux liên quan đến bộ đệm ARP:

tín dụng

gc_thresh3 là một nắp cứng. Nếu bạn đang nhận được bảng láng giềng của bộ lọc tràn vào các mục nhật ký, thì điều này cho thấy ngay cả sau khi bộ sưu tập rác đồng bộ (GC) của bộ đệm ARP, vẫn không có đủ chỗ để lưu trữ mục hàng xóm. Trong trường hợp này, kernel chỉ bỏ gói hoàn toàn.

Chúng tôi sử dụng Flannel làm kết cấu mạng của chúng tôi trong Kubernetes. Các gói được chuyển tiếp qua VXLAN. VXLAN là sơ đồ lớp phủ lớp 2 trên mạng lớp 3. Nó sử dụng đóng gói Giao thức gói dữ liệu địa chỉ người dùng MAC (MAC-in-UDP) để cung cấp phương tiện để mở rộng các phân đoạn mạng lớp 2. Giao thức truyền tải qua mạng trung tâm dữ liệu vật lý là IP cộng với UDP.

Hình 2 Sơ đồ Flannel1 (tín dụng)

Hình 2 Gói VXLAN (tín dụng)

Mỗi nút worker Kubernetes phân bổ / 24 không gian địa chỉ ảo của riêng nó trong một khối lớn hơn / 9. Đối với mỗi nút, điều này dẫn đến 1 mục nhập bảng tuyến, 1 mục nhập bảng ARP (trên giao diện flannel.1) và 1 mục nhập cơ sở dữ liệu chuyển tiếp (FDB). Chúng được thêm vào khi nút worker khởi chạy lần đầu tiên hoặc khi mỗi nút mới được phát hiện.

Ngoài ra, giao tiếp giữa các nút (hoặc pod-to-pod) cuối cùng chảy qua giao diện eth0 (được mô tả trong sơ đồ Flannel ở trên). Điều này sẽ dẫn đến một mục bổ sung trong bảng ARP cho mỗi nguồn nút và đích nút tương ứng.

Trong môi trường của chúng ta, loại giao tiếp này rất phổ biến. Đối với các đối tượng dịch vụ Kubernetes của chúng tôi, ELB được tạo và Kubernetes đăng ký mọi nút với ELB. ELB không nhận biết nhóm và nút được chọn có thể không phải là đích cuối cùng của gói. Điều này là do khi nút nhận gói tin từ ELB, nó sẽ đánh giá các quy tắc iptables của nó cho dịch vụ và chọn ngẫu nhiên một nhóm trên một nút khác.

Tại thời điểm ngừng hoạt động, có 605 nút trong cụm. Vì những lý do đã nêu ở trên, điều này đủ để làm lu mờ giá trị gc_thresh3 mặc định. Khi điều này xảy ra, không chỉ các gói bị loại bỏ mà toàn bộ không gian địa chỉ ảo Flannel / 24 cũng bị thiếu trong bảng ARP. Nút Node để truyền thông pod và tra cứu DNS không thành công. (DNS được lưu trữ trong cụm, như sẽ được giải thích chi tiết hơn sau này trong bài viết này.)

Để giải quyết, các giá trị gc_thresh1, gc_thresh2 và gc_thresh3 được nâng lên và Flannel phải được khởi động lại để đăng ký lại các mạng bị thiếu.

Chạy DNS bất ngờ ở quy mô

Để phù hợp với việc di chuyển của chúng tôi, chúng tôi đã tận dụng DNS rất nhiều để tạo điều kiện cho việc định hình lưu lượng truy cập và chuyển đổi gia tăng từ di sản sang Kubernetes cho các dịch vụ của chúng tôi. Chúng tôi đặt các giá trị TTL tương đối thấp trên các Bản ghi Route53 được liên kết. Khi chúng tôi chạy cơ sở hạ tầng kế thừa của mình trên các phiên bản EC2, cấu hình trình phân giải của chúng tôi đã chỉ đến DNS của Amazon. Chúng tôi đã chấp nhận điều này và chi phí của một mức giá tương đối thấp cho các dịch vụ của chúng tôi và các dịch vụ của Amazon (ví dụ như DynamoDB) hầu như không được chú ý.

Khi chúng tôi đưa lên ngày càng nhiều dịch vụ cho Kubernetes, chúng tôi thấy mình đang chạy một dịch vụ DNS đang trả lời 250.000 yêu cầu mỗi giây. Chúng tôi đã gặp phải thời gian chờ tra cứu DNS không liên tục và có tác động trong các ứng dụng của chúng tôi. Điều này xảy ra mặc dù đã có một nỗ lực điều chỉnh toàn diện và nhà cung cấp DNS chuyển sang triển khai CoreDNS, tại một thời điểm đạt đỉnh 1.000 điểm, tiêu thụ 120 lõi.

Trong khi nghiên cứu các nguyên nhân và giải pháp có thể khác, chúng tôi đã tìm thấy một bài viết mô tả tình trạng chủng tộc ảnh hưởng đến bộ lọc khung lọc gói Linux. Thời gian chờ DNS mà chúng ta đang thấy, cùng với bộ đếm insert_fails tăng dần trên giao diện Flannel, phù hợp với kết quả của bài viết.

Sự cố xảy ra trong quá trình dịch địa chỉ mạng nguồn và đích (SNAT và DNAT) và sau đó chèn vào bảng conntrack. Một cách giải quyết được thảo luận trong nội bộ và được cộng đồng đề xuất là di chuyển DNS lên nút worker. Trong trường hợp này:

  • SNAT là không cần thiết, bởi vì lưu lượng nằm ở địa phương trên nút. Nó không cần phải được truyền qua giao diện eth0.
  • DNAT là không cần thiết vì IP đích là cục bộ của nút và không phải là nhóm được chọn ngẫu nhiên theo quy tắc iptables.

Chúng tôi quyết định tiến về phía trước với phương pháp này. CoreDNS đã được triển khai dưới dạng DaemonSet trong Kubernetes và chúng tôi đã đưa máy chủ DNS cục bộ của nút vào từng tệp độ phân giải của pod bằng cách định cấu hình cờ lệnh kubelet - cluster-dns. Cách giải quyết có hiệu quả đối với thời gian chờ DNS.

Tuy nhiên, chúng ta vẫn thấy các gói bị rơi và gia tăng bộ đếm insert_fails của giao diện Flannel. Điều này sẽ tồn tại ngay cả sau khi giải quyết ở trên vì chúng tôi chỉ tránh SNAT và / hoặc DNAT cho lưu lượng DNS. Điều kiện cuộc đua vẫn sẽ xảy ra đối với các loại lưu lượng khác. May mắn thay, hầu hết các gói của chúng tôi là TCP và khi điều kiện xảy ra, các gói sẽ được truyền lại thành công. Một bản sửa lỗi dài hạn cho tất cả các loại lưu lượng truy cập là điều mà chúng tôi vẫn đang thảo luận.

Sử dụng Envoy để đạt được cân bằng tải tốt hơn

Khi chúng tôi di chuyển các dịch vụ phụ trợ của mình sang Kubernetes, chúng tôi bắt đầu chịu tải không cân bằng trên các nhóm. Chúng tôi đã phát hiện ra rằng do HTTP Keepalive, các kết nối ELB bị mắc kẹt trong các nhóm sẵn sàng đầu tiên của mỗi lần triển khai, do đó, hầu hết lưu lượng truy cập đều chảy qua một tỷ lệ nhỏ của các nhóm có sẵn. Một trong những giảm nhẹ đầu tiên chúng tôi đã thử là sử dụng MaxSurge 100% cho các triển khai mới cho những kẻ phạm tội tồi tệ nhất. Điều này rất hiệu quả và không bền vững lâu dài với một số triển khai lớn hơn.

Một biện pháp giảm thiểu khác mà chúng tôi đã sử dụng là làm tăng các yêu cầu tài nguyên một cách giả tạo trên các dịch vụ quan trọng để các cụm được tạo ra sẽ có nhiều khoảng trống hơn bên cạnh các nhóm nặng khác. Điều này cũng sẽ không thể thực hiện được trong thời gian dài do lãng phí tài nguyên và các ứng dụng Node của chúng tôi được phân luồng đơn và do đó có hiệu quả giới hạn ở mức 1 lõi. Giải pháp rõ ràng duy nhất là sử dụng cân bằng tải tốt hơn.

Chúng tôi đã tìm cách đánh giá Envoy. Điều này cho phép chúng tôi có cơ hội triển khai nó trong một thời gian rất hạn chế và gặt hái những lợi ích ngay lập tức. Envoy là một proxy lớp 7 hiệu suất cao, mã nguồn mở được thiết kế cho các kiến ​​trúc hướng dịch vụ lớn. Nó có thể thực hiện các kỹ thuật cân bằng tải tiên tiến, bao gồm thử lại tự động, ngắt mạch và giới hạn tốc độ toàn cầu.

Cấu hình mà chúng tôi đã đưa ra là có một Envoy sidecar bên cạnh mỗi nhóm có một tuyến và cụm để đến cổng container cục bộ. Để giảm thiểu khả năng xếp tầng tiềm năng và để giữ bán kính vụ nổ nhỏ, chúng tôi đã sử dụng một nhóm các Envoy proxy trước, một triển khai trong mỗi Vùng sẵn có (AZ) cho mỗi dịch vụ. Những cơ chế này đạt được một cơ chế khám phá dịch vụ nhỏ, một trong những kỹ sư của chúng tôi đã cùng nhau trả lại một danh sách các nhóm trong mỗi AZ cho một dịch vụ nhất định.

Dịch vụ phía trước sau đó sử dụng cơ chế khám phá dịch vụ này với một cụm và tuyến đường ngược dòng. Chúng tôi đã định cấu hình thời gian chờ hợp lý, tăng tất cả các cài đặt ngắt mạch và sau đó đưa vào cấu hình thử lại tối thiểu để giúp khắc phục các lỗi tạm thời và triển khai trơn tru. Chúng tôi đã hỗ trợ mỗi dịch vụ Envoy phía trước này với ELB TCP. Ngay cả khi phần lưu giữ từ lớp proxy chính phía trước của chúng tôi được ghim trên các nhóm Envoy nhất định, chúng vẫn có khả năng xử lý tải tốt hơn nhiều và được định cấu hình để cân bằng thông qua ít nhất đến phần phụ trợ.

Để triển khai, chúng tôi đã sử dụng hook preStop trên cả ứng dụng và pod sidecar. Móc này được gọi là kiểm tra sức khỏe sidecar thất bại điểm cuối quản trị viên, cùng với một giấc ngủ nhỏ, để cho một chút thời gian để cho phép các kết nối trên máy bay hoàn thành và thoát.

Một lý do khiến chúng tôi có thể di chuyển nhanh như vậy là do các số liệu phong phú mà chúng tôi có thể dễ dàng tích hợp với thiết lập Prometheus bình thường của chúng tôi. Điều này cho phép chúng tôi xem chính xác những gì đang xảy ra khi chúng tôi lặp lại trên cài đặt cấu hình và cắt giảm lưu lượng.

Kết quả là ngay lập tức và rõ ràng. Chúng tôi đã bắt đầu với các dịch vụ không cân bằng nhất và tại thời điểm này, nó đã chạy trước mười hai dịch vụ quan trọng nhất trong cụm của chúng tôi. Năm nay chúng tôi có kế hoạch chuyển sang một mạng lưới dịch vụ đầy đủ, với khám phá dịch vụ tiên tiến hơn, ngắt mạch, phát hiện ngoại lệ, giới hạn tốc độ và theo dõi.

Hình 3 Sự hội tụ CPU của CPU1 của một dịch vụ trong quá trình chuyển đổi sang sứ thần

Kết quả cuối cùng

Thông qua những bài học và nghiên cứu bổ sung này, chúng tôi đã phát triển một đội ngũ cơ sở hạ tầng nội bộ mạnh mẽ, rất quen thuộc về cách thiết kế, triển khai và vận hành các cụm Kubernetes lớn. Toàn bộ tổ chức kỹ thuật của Tinder hiện có kiến ​​thức và kinh nghiệm về cách chứa và triển khai các ứng dụng của họ trên Kubernetes.

Trên cơ sở hạ tầng kế thừa của chúng tôi, khi cần thêm quy mô, chúng tôi thường phải chịu đựng trong vài phút chờ đợi các phiên bản EC2 mới được phát hành trực tuyến. Các container hiện lên lịch và phục vụ lưu lượng trong vòng vài giây thay vì vài phút. Lập lịch cho nhiều container trên một thể hiện EC2 cũng cung cấp mật độ ngang được cải thiện. Do đó, chúng tôi dự kiến ​​tiết kiệm đáng kể chi phí cho EC2 vào năm 2019 so với năm trước.

Mất gần hai năm, nhưng chúng tôi đã hoàn tất quá trình di chuyển của mình vào tháng 3 năm 2019. Nền tảng Tinder chạy độc quyền trên cụm Kubernetes bao gồm 200 dịch vụ, 1.000 nút, 15.000 nhóm và 48.000 container đang chạy. Cơ sở hạ tầng không còn là một nhiệm vụ dành riêng cho các nhóm hoạt động của chúng tôi. Thay vào đó, các kỹ sư trong toàn tổ chức chia sẻ trách nhiệm này và có quyền kiểm soát cách các ứng dụng của họ được xây dựng và triển khai với mọi thứ dưới dạng mã.