[Dịch] Mô hình phân luồng Envoy

Dịch bài viết: Mô hình phân luồng đặc phái viên - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Tôi thấy bài viết này khá thú vị và vì Envoy thường được sử dụng như một phần của “istio” hoặc đơn giản là “bộ điều khiển xâm nhập” của kubernetes, nên hầu hết mọi người không có cùng tương tác trực tiếp với nó, chẳng hạn như với các kubernetes thông thường. Cài đặt Nginx hoặc Haproxy. Tuy nhiên, nếu có thứ gì đó bị hỏng, sẽ rất tốt nếu hiểu nó hoạt động như thế nào từ bên trong. Tôi đã cố gắng dịch càng nhiều văn bản sang tiếng Nga càng tốt, kể cả những từ đặc biệt, đối với những ai cảm thấy khó chịu khi nhìn vào điều này, tôi để nguyên bản trong ngoặc đơn. Chào mừng đến với con mèo.

Tài liệu kỹ thuật cấp thấp cho cơ sở mã Envoy hiện khá thưa thớt. Để khắc phục điều này, tôi dự định thực hiện một loạt bài đăng trên blog về các hệ thống con khác nhau của Envoy. Vì đây là bài viết đầu tiên nên xin vui lòng cho tôi biết suy nghĩ của bạn và những gì bạn có thể quan tâm trong các bài viết sau.

Một trong những câu hỏi kỹ thuật phổ biến nhất mà tôi nhận được về Envoy là yêu cầu mô tả ở mức độ thấp về mô hình phân luồng mà nó sử dụng. Trong bài đăng này, tôi sẽ mô tả cách Envoy ánh xạ các kết nối tới các luồng, cũng như hệ thống Thread Local Storage mà nó sử dụng nội bộ để tạo mã song song hơn và có hiệu suất cao hơn.

Tổng quan về luồng

[Dịch] Mô hình phân luồng Envoy

Envoy sử dụng ba loại luồng khác nhau:

  • Chủ yếu: Luồng này kiểm soát quá trình khởi động và kết thúc quá trình, tất cả quá trình xử lý API XDS (xDiscovery Service), bao gồm DNS, kiểm tra tình trạng, quản lý thời gian chạy và cụm chung, thiết lập lại số liệu thống kê, quản trị và quản lý quy trình chung - tín hiệu Linux, khởi động lại nóng, v.v. xảy ra trong chuỗi này là không đồng bộ và "không chặn". Nói chung, luồng chính điều phối tất cả các quy trình chức năng quan trọng không cần nhiều CPU để chạy. Điều này cho phép hầu hết mã điều khiển được viết như thể nó là một luồng đơn.
  • Công nhân: Theo mặc định, Envoy tạo một luồng công việc cho từng luồng phần cứng trong hệ thống, điều này có thể được kiểm soát bằng tùy chọn --concurrency. Mỗi luồng công việc chạy một vòng lặp sự kiện "không chặn", chịu trách nhiệm lắng nghe từng người nghe; tại thời điểm viết bài (ngày 29 tháng 2017 năm XNUMX), không có phân đoạn của người nghe, chấp nhận kết nối mới, khởi tạo ngăn xếp bộ lọc cho kết nối và xử lý tất cả các hoạt động đầu vào/đầu ra (IO) trong suốt thời gian kết nối. Một lần nữa, điều này cho phép hầu hết mã xử lý kết nối được viết như thể nó là một luồng đơn.
  • Trình xóa tập tin: Mỗi file mà Envoy ghi chủ yếu là log truy cập, hiện có một thread chặn độc lập. Điều này là do việc ghi vào các tập tin được hệ thống tập tin lưu vào bộ đệm ngay cả khi sử dụng O_NONBLOCK đôi khi có thể bị chặn (thở dài). Khi các luồng công việc cần ghi vào một tệp, dữ liệu thực sự được chuyển đến một bộ đệm trong bộ nhớ, nơi cuối cùng nó được xóa qua luồng xả tập tin. Đây là một vùng mã mà về mặt kỹ thuật tất cả các luồng công việc có thể chặn cùng một khóa trong khi cố gắng lấp đầy bộ nhớ đệm.

Xử lý kết nối

Như đã thảo luận ngắn gọn ở trên, tất cả các luồng công nhân đều lắng nghe tất cả người nghe mà không có bất kỳ phân đoạn nào. Do đó, kernel được sử dụng để gửi các ổ cắm được chấp nhận đến các luồng công việc một cách duyên dáng. Các hạt nhân hiện đại nhìn chung rất giỏi trong việc này, chúng sử dụng các tính năng như tăng mức độ ưu tiên đầu vào/đầu ra (IO) để cố gắng lấp đầy một luồng công việc trước khi chúng bắt đầu sử dụng các luồng khác cũng đang nghe trên cùng một ổ cắm và cũng không sử dụng vòng tròn khóa (Spinlock) để xử lý từng yêu cầu.
Khi một kết nối được chấp nhận trên một luồng công nhân, nó sẽ không bao giờ rời khỏi luồng đó. Tất cả quá trình xử lý kết nối tiếp theo được xử lý hoàn toàn trong luồng công việc, bao gồm mọi hành vi chuyển tiếp.

Điều này có một số hậu quả quan trọng:

  • Tất cả các nhóm kết nối trong Envoy được gán cho một luồng công việc. Vì vậy, mặc dù nhóm kết nối HTTP/2 chỉ tạo một kết nối đến mỗi máy chủ ngược dòng tại một thời điểm, nhưng nếu có bốn luồng công việc thì sẽ có bốn kết nối HTTP/2 trên mỗi máy chủ ngược dòng ở trạng thái ổn định.
  • Lý do Envoy hoạt động theo cách này là bằng cách giữ mọi thứ trên một luồng công việc duy nhất, hầu hết tất cả mã có thể được viết mà không bị chặn và như thể nó là một luồng đơn. Thiết kế này giúp bạn dễ dàng viết nhiều mã và có khả năng mở rộng quy mô cực kỳ tốt cho số lượng luồng công việc gần như không giới hạn.
  • Tuy nhiên, một trong những điểm cần rút ra chính là từ quan điểm về nhóm bộ nhớ và hiệu quả kết nối, việc định cấu hình bộ nhớ thực sự rất quan trọng. --concurrency. Việc có nhiều luồng công việc hơn mức cần thiết sẽ gây lãng phí bộ nhớ, tạo ra nhiều kết nối nhàn rỗi hơn và giảm tốc độ gộp kết nối. Tại Lyft, các container sidecar đặc phái viên của chúng tôi chạy ở tốc độ đồng thời rất thấp, do đó hiệu suất gần như tương đương với các dịch vụ bên cạnh chúng. Chúng tôi chỉ chạy Envoy dưới dạng proxy biên ở mức đồng thời tối đa.

Không chặn nghĩa là gì?

Cho đến nay, thuật ngữ "không chặn" đã được sử dụng nhiều lần khi thảo luận về cách hoạt động của các luồng chính và luồng công việc. Tất cả mã được viết trên giả định rằng không có gì bị chặn. Tuy nhiên, điều này không hoàn toàn đúng (điều gì không hoàn toàn đúng?).

Envoy sử dụng một số khóa quy trình dài:

  • Như đã thảo luận, khi ghi nhật ký truy cập, tất cả các luồng công việc đều có cùng một khóa trước khi bộ đệm nhật ký trong bộ nhớ được lấp đầy. Thời gian giữ khóa phải rất thấp, nhưng khóa có thể bị tranh chấp ở mức độ đồng thời cao và thông lượng cao.
  • Envoy sử dụng một hệ thống rất phức tạp để xử lý số liệu thống kê cục bộ trong luồng. Đây sẽ là chủ đề của một bài viết riêng biệt. Tuy nhiên, tôi sẽ đề cập ngắn gọn rằng như một phần của quá trình xử lý thống kê luồng cục bộ, đôi khi cần phải có khóa trên "kho thống kê" trung tâm. Việc khóa này không bao giờ cần thiết.
  • Luồng chính định kỳ cần phối hợp với tất cả các luồng công nhân. Điều này được thực hiện bằng cách "xuất bản" từ luồng chính sang luồng công nhân và đôi khi từ luồng công nhân trở lại luồng chính. Việc gửi yêu cầu khóa để tin nhắn đã xuất bản có thể được xếp hàng đợi để gửi sau. Những ổ khóa này không bao giờ bị tranh chấp nghiêm trọng, nhưng về mặt kỹ thuật chúng vẫn có thể bị chặn.
  • Khi Envoy ghi nhật ký vào luồng lỗi hệ thống (lỗi tiêu chuẩn), nó sẽ khóa toàn bộ quá trình. Nhìn chung, việc ghi nhật ký cục bộ của Envoy được coi là tệ xét từ quan điểm hiệu suất, vì vậy không có nhiều sự chú ý đến việc cải thiện nó.
  • Có một số khóa ngẫu nhiên khác, nhưng không có khóa nào trong số đó quan trọng về hiệu suất và không bao giờ bị thử thách.

Chủ đề lưu trữ cục bộ

Do cách Envoy tách biệt các trách nhiệm của luồng chính khỏi trách nhiệm của luồng công nhân, nên có yêu cầu là việc xử lý phức tạp có thể được thực hiện trên luồng chính và sau đó được cung cấp cho từng luồng công nhân theo cách thức đồng thời cao độ. Phần này mô tả Envoy Thread Local Storage (TLS) ở mức cao. Trong phần tiếp theo tôi sẽ mô tả cách nó được sử dụng để quản lý một cụm.
[Dịch] Mô hình phân luồng Envoy

Như đã mô tả, luồng chính xử lý hầu như tất cả chức năng mặt phẳng quản lý và điều khiển trong quy trình Envoy. Mặt phẳng điều khiển ở đây hơi quá tải, nhưng khi bạn nhìn vào nó trong chính quy trình Envoy và so sánh nó với quá trình chuyển tiếp mà các luồng công nhân thực hiện, điều đó rất hợp lý. Nguyên tắc chung là tiến trình luồng chính thực hiện một số công việc và sau đó nó cần cập nhật từng luồng công việc theo kết quả của công việc đó. trong trường hợp này, luồng công nhân không cần có khóa trên mỗi lần truy cập.

Hệ thống TLS (Thread local storage) của Envoy hoạt động như sau:

  • Mã chạy trên luồng chính có thể phân bổ một khe TLS cho toàn bộ quá trình. Mặc dù điều này trừu tượng, nhưng trong thực tế, nó là một chỉ mục thành một vectơ, cung cấp quyền truy cập O(1).
  • Main thread có thể cài đặt dữ liệu tùy ý vào slot của nó. Khi việc này hoàn tất, dữ liệu sẽ được xuất bản lên từng luồng công việc dưới dạng sự kiện vòng lặp sự kiện thông thường.
  • Các luồng công nhân có thể đọc từ khe TLS của chúng và truy xuất bất kỳ dữ liệu cục bộ nào có sẵn ở đó.

Mặc dù đây là một mô hình rất đơn giản và cực kỳ mạnh mẽ, nhưng nó rất giống với khái niệm chặn RCU(Đọc-Sao chép-Cập nhật). Về cơ bản, các luồng công việc không bao giờ thấy bất kỳ thay đổi dữ liệu nào trong các khe TLS khi công việc đang chạy. Thay đổi chỉ xảy ra trong thời gian nghỉ giữa các sự kiện làm việc.

Envoy sử dụng điều này theo hai cách khác nhau:

  • Bằng cách lưu trữ dữ liệu khác nhau trên mỗi luồng công nhân, dữ liệu có thể được truy cập mà không bị chặn.
  • Bằng cách duy trì một con trỏ dùng chung tới dữ liệu chung ở chế độ chỉ đọc trên mỗi luồng công việc. Do đó, mỗi luồng công việc có số lượng tham chiếu dữ liệu không thể giảm đi trong khi công việc đang chạy. Chỉ khi tất cả công nhân bình tĩnh và tải lên dữ liệu chia sẻ mới thì dữ liệu cũ mới bị hủy. Điều này giống hệt với RCU.

Luồng cập nhật cụm

Trong phần này, tôi sẽ mô tả cách sử dụng TLS (Bộ nhớ cục bộ luồng) để quản lý một cụm. Quản lý cụm bao gồm xử lý API xDS và/hoặc DNS cũng như kiểm tra tình trạng.
[Dịch] Mô hình phân luồng Envoy

Quản lý luồng cụm bao gồm các thành phần và bước sau:

  1. Trình quản lý cụm là một thành phần trong Envoy quản lý tất cả các luồng ngược dòng đã biết của cụm, API Dịch vụ khám phá cụm (CDS), API Dịch vụ khám phá bí mật (SDS) và Dịch vụ khám phá điểm cuối (EDS), DNS và các hoạt động kiểm tra bên ngoài đang hoạt động. Nó chịu trách nhiệm tạo ra một cái nhìn "nhất quán cuối cùng" về từng cụm ngược dòng, bao gồm các máy chủ được phát hiện cũng như tình trạng sức khỏe.
  2. Trình kiểm tra tình trạng thực hiện kiểm tra tình trạng hoạt động và báo cáo các thay đổi trạng thái tình trạng cho người quản lý cụm.
  3. CDS (Dịch vụ khám phá cụm) / SDS (Dịch vụ khám phá bí mật) / EDS (Dịch vụ khám phá điểm cuối) / DNS được thực hiện để xác định tư cách thành viên của cụm. Sự thay đổi trạng thái được trả về cho người quản lý cụm.
  4. Mỗi luồng công nhân liên tục thực hiện một vòng lặp sự kiện.
  5. Khi trình quản lý cụm xác định rằng trạng thái của một cụm đã thay đổi, nó sẽ tạo một ảnh chụp nhanh chỉ đọc mới về trạng thái của cụm và gửi nó đến từng luồng công việc.
  6. Trong khoảng thời gian yên tĩnh tiếp theo, luồng công nhân sẽ cập nhật ảnh chụp nhanh trong khe TLS được phân bổ.
  7. Trong sự kiện I/O có nhiệm vụ xác định máy chủ cân bằng tải, bộ cân bằng tải sẽ yêu cầu khe TLS (Bộ lưu trữ cục bộ luồng) để lấy thông tin về máy chủ. Điều này không yêu cầu khóa. Cũng lưu ý rằng TLS cũng có thể kích hoạt các sự kiện cập nhật để bộ cân bằng tải và các thành phần khác có thể tính toán lại bộ đệm, cấu trúc dữ liệu, v.v. Điều này nằm ngoài phạm vi của bài đăng này nhưng được sử dụng ở nhiều nơi khác nhau trong mã.

Sử dụng quy trình trên, Envoy có thể xử lý mọi yêu cầu mà không bị chặn (ngoại trừ như được mô tả trước đó). Ngoài sự phức tạp của bản thân mã TLS, hầu hết mã không cần hiểu cách thức hoạt động của đa luồng và có thể được viết đơn luồng. Điều này làm cho hầu hết mã dễ viết hơn bên cạnh hiệu suất vượt trội.

Các hệ thống con khác sử dụng TLS

TLS (Bộ lưu trữ cục bộ theo luồng) và RCU (Cập nhật bản sao đọc) được sử dụng rộng rãi trong Envoy.

Ví dụ về sử dụng:

  • Cơ chế thay đổi chức năng trong quá trình thực thi: Danh sách chức năng được kích hoạt hiện tại được tính toán trong luồng chính. Sau đó, mỗi luồng công việc sẽ được cung cấp một ảnh chụp nhanh chỉ đọc bằng cách sử dụng ngữ nghĩa RCU.
  • Thay thế bảng định tuyến: Đối với các bảng tuyến được cung cấp bởi RDS (Dịch vụ khám phá tuyến đường), các bảng tuyến được tạo trên luồng chính. Sau đó, ảnh chụp nhanh chỉ đọc sẽ được cung cấp cho từng luồng công việc bằng cách sử dụng ngữ nghĩa RCU (Cập nhật bản sao đọc). Điều này làm cho việc thay đổi bảng lộ trình trở nên hiệu quả về mặt nguyên tử.
  • Bộ nhớ đệm tiêu đề HTTP: Hóa ra, việc tính toán tiêu đề HTTP cho mỗi yêu cầu (trong khi chạy ~25K+RPS trên mỗi lõi) là khá tốn kém. Envoy tính toán tập trung tiêu đề khoảng nửa giây một lần và cung cấp tiêu đề đó cho từng nhân viên thông qua TLS và RCU.

Có những trường hợp khác, nhưng các ví dụ trước sẽ giúp bạn hiểu rõ hơn về mục đích sử dụng TLS.

Những cạm bẫy về hiệu suất đã biết

Mặc dù Envoy hoạt động khá tốt về tổng thể, nhưng có một số lĩnh vực đáng chú ý cần chú ý khi nó được sử dụng với thông lượng và đồng thời rất cao:

  • Như được mô tả trong bài viết này, hiện tại tất cả các luồng công việc đều có khóa khi ghi vào bộ đệm bộ nhớ nhật ký truy cập. Ở mức độ đồng thời cao và thông lượng cao, bạn sẽ cần phải sắp xếp các nhật ký truy cập cho từng luồng công việc với chi phí là phân phối không theo thứ tự khi ghi vào tệp cuối cùng. Ngoài ra, bạn có thể tạo nhật ký truy cập riêng cho từng chuỗi công việc.
  • Mặc dù số liệu thống kê được tối ưu hóa cao nhưng ở mức độ đồng thời và thông lượng rất cao sẽ có khả năng xảy ra tranh chấp nguyên tử trên các số liệu thống kê riêng lẻ. Giải pháp cho vấn đề này là bộ đếm trên mỗi luồng công nhân với việc đặt lại bộ đếm trung tâm định kỳ. Điều này sẽ được thảo luận trong một bài viết tiếp theo.
  • Kiến trúc hiện tại sẽ không hoạt động tốt nếu Envoy được triển khai trong tình huống có rất ít kết nối yêu cầu tài nguyên xử lý đáng kể. Không có gì đảm bảo rằng các kết nối sẽ được phân bổ đồng đều giữa các luồng công nhân. Điều này có thể được giải quyết bằng cách triển khai cân bằng kết nối công nhân, điều này sẽ cho phép trao đổi kết nối giữa các luồng công nhân.

Phần kết luận

Mô hình phân luồng của Envoy được thiết kế để cung cấp khả năng lập trình dễ dàng và khả năng song song lớn nhưng có khả năng gây lãng phí bộ nhớ và kết nối nếu không được cấu hình đúng. Mô hình này cho phép nó hoạt động rất tốt với số lượng luồng và thông lượng rất cao.
Như tôi đã đề cập ngắn gọn trên Twitter, thiết kế này cũng có thể chạy trên ngăn xếp mạng ở chế độ người dùng đầy đủ, chẳng hạn như DPDK (Bộ công cụ phát triển mặt phẳng dữ liệu), có thể giúp các máy chủ thông thường xử lý hàng triệu yêu cầu mỗi giây với quá trình xử lý L7 đầy đủ. Sẽ rất thú vị để xem những gì sẽ được xây dựng trong vài năm tới.
Một nhận xét nhanh cuối cùng: Tôi đã được hỏi nhiều lần tại sao chúng tôi chọn C++ cho Envoy. Lý do vẫn là nó vẫn là ngôn ngữ cấp công nghiệp duy nhất được sử dụng rộng rãi mà kiến ​​trúc được mô tả trong bài viết này có thể được xây dựng. C++ chắc chắn không phù hợp với tất cả hoặc thậm chí nhiều dự án, nhưng đối với một số trường hợp sử dụng nhất định, nó vẫn là công cụ duy nhất để hoàn thành công việc.

Liên kết tới mã

Liên kết đến các tệp có giao diện và cách triển khai tiêu đề được thảo luận trong bài đăng này:

Nguồn: www.habr.com

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