Sử dụng bộ nhớ gỡ lỗi Python

Tại Brex, chúng tôi sử dụng Python rộng rãi cho các ứng dụng Khoa học dữ liệu và Máy học. Trong nhóm Nền tảng dữ liệu, chúng tôi thường cần viết các ứng dụng có thể thực thi mã Python do nhóm Khoa học dữ liệu (DS) của chúng tôi viết, chẳng hạn như tính toán tính năng theo yêu cầu hoặc suy luận cho các mô hình dự đoán

Vì các dịch vụ Python này tương tác với dữ liệu thực tế nên có thể có sự khác biệt đáng kể về lượng dữ liệu được xử lý trên mỗi yêu cầu. Ví dụ: yêu cầu "chạy suy luận để dự đoán chi tiêu trong tương lai của khách hàng X" có thể có các yêu cầu sử dụng bộ nhớ rất khác nhau, tùy thuộc vào lượng dữ liệu có sẵn về các mẫu chi tiêu trước đây của khách hàng X

Điều này tạo tiền đề cho một trong những thách thức về độ tin cậy gần đây nhất của chúng tôi. xử lý lỗi hết bộ nhớ (OOM) trong các dịch vụ cung cấp mô hình của chúng tôi, được trang bị kiến ​​thức rằng một số yêu cầu sẽ không nhất thiết phải “hoạt động tốt”. ” Đây là một thử thách khá thú vị để gỡ lỗi, vì vậy chúng tôi nhận thấy rằng việc chia sẻ một số kiến ​​thức của chúng tôi với cộng đồng rộng lớn hơn là điều đáng làm

Thiết lập cơ bản của chúng tôi

Hãy bắt đầu với một số thông tin cơ bản về thiết lập của chúng tôi. Mọi thứ tiếp theo (trừ khi được gọi cụ thể) đã được thử nghiệm trên thiết lập cụ thể này

  • Phiên bản Python. 3. 7. 3 (Tôi biết, nâng cấp nằm trong danh sách của chúng tôi)
  • Tất cả các dịch vụ Python của chúng tôi đều được tích hợp vào hình ảnh docker bằng Bazel (sau khi được đóng gói thành tệp thực thi nhị phân)
  • Các dịch vụ của chúng tôi chạy trong cụm Kubernetes trên AWS
  • Tất cả các dịch vụ Python của chúng tôi đang chạy trên quy trình máy chủ gRPC tiêu chuẩn
  • Bạn sẽ thấy một số tham chiếu đến khối lượng công việc không đồng bộ — thiết lập cơ bản cho những khối lượng công việc này là Python Celery worker sử dụng từ hàng đợi được hỗ trợ bởi Redis
Tại sao chúng ta cần giải quyết OOM?

Có lẽ câu trả lời cho điều này là khá rõ ràng (tôi. e. , OOM khiến dịch vụ bị lỗi), nhưng bạn nên tìm hiểu chính xác điều gì sẽ xảy ra khi quá trình python “hết bộ nhớ. ” Bản thân quy trình Python không hề hay biết về giới hạn bộ nhớ do HĐH áp đặt cho nó. ¹ Thay vì tự giới hạn dung lượng bộ nhớ mà nó sẽ phân bổ, quy trình Python sẽ chỉ cố gắng phân bổ thêm bộ nhớ để làm bất cứ điều gì nó cần làm (ví dụ: để tải thêm một số dữ liệu từ cơ sở dữ liệu) — chỉ để nhận ngay

Điều này không chỉ ngăn quá trình hoàn thành (các) tác vụ mà nó đang chạy trước đó mà còn không cho phép bất kỳ kiểu tắt máy “duyên dáng” nào. Đây có lẽ là điều tồi tệ nhất có thể xảy ra với quy trình của bạn khi đang chạy trong sản xuất — ngoài việc ngăn mọi yêu cầu đang diễn ra hoàn thành, việc gỡ lỗi cũng cực kỳ khó khăn. Rốt cuộc, việc không thể tắt một cách duyên dáng sẽ ngăn mọi lỗi, số liệu hoặc dấu vết phía máy chủ được tạo ra để hỗ trợ khả năng quan sát dịch vụ

Để cụ thể hơn, hãy xem xét hai trường hợp khác nhau sau đây mà chúng tôi đã thấy OOM ngoài đời thực xảy ra tại Brex

OOM trong các yêu cầu đồng bộ trên máy chủ

Trường hợp này về cơ bản là trường hợp tương tự được nêu trong phần giới thiệu. máy chủ hết bộ nhớ trong khi xử lý yêu cầu. Điều nguy hiểm ở đây là các máy chủ của chúng tôi hầu như luôn xử lý nhiều yêu cầu cùng một lúc — khi một trong những yêu cầu này khiến một quy trình hết bộ nhớ, tất cả các yêu cầu đang được xử lý bởi quy trình đó sẽ ngay lập tức bị lỗi. Thậm chí tệ hơn. vì không thể tắt máy nhanh chóng nên chúng tôi không thể kiểm soát cách chúng bị lỗi, do đó chúng tôi thậm chí không thể đảm bảo khách hàng nhận được thông báo lỗi có thể khắc phục được

Worker process hết bộ nhớ

Ngoài việc xử lý các yêu cầu một cách đồng bộ, chúng tôi cũng có các quy trình Python đóng vai trò là công nhân tiêu thụ các tác vụ từ hàng đợi. Trong trường hợp này, mỗi công việc được thực hiện tuần tự bởi từng công nhân nên vấn đề trên không tồn tại. Tuy nhiên, do SIGKILL khiến cho việc tắt máy nhẹ nhàng là không thể, nên các tác vụ này đơn giản là không được công nhân xác nhận. Đối với thiết lập Celery cụ thể của chúng tôi, điều này có nghĩa là chúng tôi sẽ kết thúc bằng việc nhấn , để nhân viên của chúng tôi liên tục cố gắng sử dụng các tác vụ gây ra OOM này đến vô tận (tất nhiên, các thiết lập không đồng bộ phức tạp hơn sẽ có thêm các lần thử lại dự phòng, điều này đảm bảo điều này không xảy ra

Làm thế nào chúng ta có thể gỡ lỗi những lỗi này?

Tất cả sự diệt vong và u ám này đưa chúng ta đến câu hỏi. làm thế nào để chúng ta thậm chí bắt đầu giải quyết vấn đề này?

  1. Đã xảy ra lỗi rò rỉ bộ nhớ trong quá trình triển khai máy chủ của chúng tôi, khiến máy chủ ngày càng sử dụng nhiều bộ nhớ hơn trong mỗi yêu cầu và cuối cùng hết bộ nhớ
  2. Mặc dù không có yêu cầu riêng lẻ nào sử dụng quá nhiều bộ nhớ, nhưng sự kết hợp của tất cả các yêu cầu tại bất kỳ thời điểm nào sẽ sử dụng quá nhiều bộ nhớ tổng hợp
  3. Có những yêu cầu tự nó sử dụng hợp pháp nhiều bộ nhớ hơn so với một quy trình đơn lẻ có sẵn. ²

Trong một máy chủ xử lý số lượng yêu cầu tương đối ổn định theo thời gian, bạn có thể nghĩ rằng rò rỉ bộ nhớ sẽ khá rõ ràng chỉ đơn giản là tăng mức sử dụng bộ nhớ một cách đơn điệu. Bạn sẽ đúng. Tuy nhiên, trong trường hợp này, chúng tôi đang xử lý một máy chủ có khối lượng công việc rất lớn, vì vậy không có cơ hội phát hiện ra mức sử dụng bộ nhớ tăng dần theo cách truyền thống theo thời gian

Giải quyết vấn đề này bắt nguồn từ việc kiểm tra những giả thuyết này với những gì chúng ta thấy trong thực tế. Trong một thế giới hoàn hảo, chúng ta có thể phân biệt giữa các giả thuyết này bằng cách đo lường

A. Dung lượng bộ nhớ được phân bổ trực tiếp do xử lý từng yêu cầu

B. Tỷ lệ phần trăm của (A) được giải phóng sau khi quá trình kết thúc phản hồi từng yêu cầu

Tuy nhiên, thế giới chúng ta đang sống d̶a̶r̶k̶ ̶a̶n̶d̶ ̶f̶u̶l̶l̶ ̶o̶f̶ ̶t̶e̶r̶r̶o̶r̶s̶ không hoàn hảo và hai con số này không có sẵn. Điều này chủ yếu là do các OOM này có xu hướng xảy ra vào những thời điểm khi dịch vụ của chúng tôi đang xử lý một số lượng lớn yêu cầu đồng thời cùng một lúc. Điều này không có gì đáng ngạc nhiên. Xét cho cùng, bất kỳ giả thuyết nào trong ba giả thuyết được đưa ra đều sẽ khiến xác suất OOM tăng lên khi máy chủ đang xử lý một số lượng lớn yêu cầu đồng thời. Mỗi yêu cầu trong máy chủ của chúng tôi được xử lý bởi một luồng tồn tại lâu dài riêng biệt (được sinh ra từ một ) — khá. Bởi vì chúng tôi không thể đo bộ nhớ được sử dụng bởi từng luồng riêng lẻ², nên chúng tôi không thể dễ dàng đo lượng bộ nhớ được sử dụng trong từng yêu cầu riêng lẻ

Giải pháp của chúng tôi cho vấn đề này về cơ bản là buộc máy chủ của chúng tôi thực hiện từng yêu cầu một, để chúng tôi có thể dễ dàng đo lượng bộ nhớ được sử dụng cho mỗi yêu cầu hơn. Cách dễ nhất để làm điều này là chỉ cần giảm đồng thời của máy chủ gRPC xuống 1. ³ Nếu đây là một lựa chọn hợp lý đối với chúng tôi, chúng tôi sẽ thực hiện nó mà không do dự. Tuy nhiên, điều này có nghĩa là bất kỳ yêu cầu nào vượt quá khả năng xử lý đồng thời của chúng tôi sẽ xếp hàng cho đến khi các yêu cầu trước đó được xử lý. Điều này có thể đã giúp chúng tôi gỡ lỗi vấn đề OOM của mình, nhưng nó chắc chắn sẽ khiến chúng tôi trở nên tồi tệ hơn về độ tin cậy tổng thể của mình

Trong trường hợp này, chúng tôi phải sáng tạo hơn. ngoài việc thực thi từng yêu cầu trên một luồng bên ngoài quy trình máy chủ chính, chúng tôi cũng sẽ thực thi từng yêu cầu hoàn toàn không đồng bộ trong một quy trình worker bị cô lập, như trong sơ đồ bên dưới. ⁴

Như bạn có thể thấy, kết quả thực tế từ tính toán không đồng bộ hoàn toàn không được sử dụng — chúng tôi chỉ sử dụng thực thi không đồng bộ để chạy từng yêu cầu theo cách riêng biệt. ⁵ Trong worker không đồng bộ của chúng tôi, chúng tôi có thể chỉ cần đo dung lượng bộ nhớ được sử dụng trước và sau mỗi yêu cầu được thực thi (chúng tôi đã sử dụng psutil.Process.full_memory_info để lấy bộ nhớ lưu trú được sử dụng). Điều này cho phép chúng tôi hiểu rất chi tiết về bộ nhớ được phân bổ cho (A) và được giải phóng⁶ sau (B) mỗi yêu cầu để chúng tôi có thể phân biệt một cách hiệu quả giữa các giả thuyết (1) và (2). Ngoài ra, vì các yêu cầu đang chạy mà không có bất kỳ tranh chấp nào về tài nguyên, nên chúng tôi có thể quy trực tiếp bất kỳ OOM nào xảy ra cho các yêu cầu cụ thể đang được xử lý trên nhân viên đó tại thời điểm đó. Điều này, ngoài việc đo bộ nhớ được sử dụng trước khi yêu cầu được xử lý, cho phép chúng tôi phân biệt giữa (1) và (3)

Vì vậy, kết luận của chúng tôi là gì? . Điều này trở nên khá rõ ràng khi chúng tôi thấy hành vi không đồng bộ. một yêu cầu duy nhất liên tục khiến worker process bị OOM và liên tục được thử lại, vì Celery không bao giờ có thể xác nhận tác vụ sau khi sử dụng nó từ hàng đợi Redis

Nỗ lực lập hồ sơ bộ nhớ

Chúng tôi nên đề cập ngắn gọn rằng chúng tôi đã cố gắng lập cấu hình bộ nhớ được sử dụng bởi máy chủ này để xác định các đoạn mã nào đang cấp phát bộ nhớ gây ra OOM của chúng tôi. Chúng tôi đã cố gắng sử dụng tracemalloc, phần lớn tuân theo các bước được nêu trong bài đăng trên blog này, tạo ra một luồng không đồng bộ sẽ báo cáo các truy nguyên phân bổ bộ nhớ hàng đầu trong mã của chúng tôi

Thật không may, sau khi triển khai phần thiết bị này vào sản xuất, chúng tôi gần như ngay lập tức nhận thấy sự gia tăng đáng kể về số lượng OOM được báo cáo cho dịch vụ của chúng tôi. Từ một số nhật ký mà chúng tôi có thể khôi phục sau lần thử này, rõ ràng là phần lớn phân bổ bộ nhớ trên thực tế đến từ việc sử dụng chính tracemalloc. Vào cuối ngày, việc sử dụng công cụ định hình này đã tạo ra quá nhiều chi phí hữu ích

Giới hạn bộ nhớ để giải cứu (đại loại là)

Quá trình gỡ lỗi ở trên cho phép chúng tôi hiểu thời điểm và cách thức các yêu cầu gây ra OOM, nhưng nó có một số nhược điểm

  1. Nó vốn có tính phản ứng, vì nó cho phép OOM tiếp tục xảy ra trên quy trình máy chủ gRPC và
  2. Như đã phác thảo, điều này sẽ yêu cầu một lượng bảo trì đáng kể cho những gì cuối cùng chỉ là một công cụ gỡ lỗi vào cuối ngày

Trong một kịch bản lý tưởng, chúng tôi sẽ có thể ngăn chặn lỗi OOM ngay từ đầu, lỗi này về cơ bản yêu cầu dừng quá trình Python phân bổ nhiều bộ nhớ hơn mức cho phép. Từ góc độ triển khai, hành vi mong muốn sẽ là trình thông dịch Python, thay vì cố gắng phân bổ nhiều hơn phần bộ nhớ hệ điều hành hợp lý của nó, chỉ cần tăng MemoryError trong đường dẫn mã khi cố gắng phân bổ như vậy. Sau đó, ở cấp cao nhất của ngăn xếp thực thi trong máy chủ gRPC, chúng tôi có thể xử lý các lỗi như vậy và trả lại mã lỗi có thể truy xuất cho máy khách (trong trường hợp này, mã gRPC RESOURCE_EXHAUSTED có vẻ phù hợp)

Trong một thế giới nơi chúng ta có thể tin tưởng khách hàng sẽ cư xử tốt (rất may, chúng ta đang sống trong một thế giới như vậy trong nhóm Nền tảng dữ liệu tại Brex), khách hàng có thể lùi lại và thử lại, hy vọng sẽ đạt được (a) quy trình chính xác tương tự tại một thời điểm

Tất nhiên, chúng tôi sẽ không thảo luận về kịch bản này trừ khi có một phương pháp hợp lý để thực hiện nó. Đi vào. mô-đun tài nguyên Python

Tôi khá ngạc nhiên khi thấy rằng có rất ít trường hợp ghi lại trường hợp sử dụng này cho mô-đun tài nguyên Python (ngoài một bài đăng blog ngắn gọn và xuất sắc của Carlos Becker, phần lớn nội dung thảo luận này sẽ dựa trên đó). Sau khi dành thời gian làm việc với mô-đun tài nguyên, bây giờ tôi có một số nghi ngờ về lý do tại sao có ít thông tin về trường hợp sử dụng này. áp đặt các giới hạn tài nguyên mà không có sự hiểu biết thấu đáo về hệ thống quản lý bộ nhớ của Python có thể dẫn đến hành vi không mong muốn (thêm về vấn đề này một chút). Tuy nhiên, mô-đun này cho phép chúng tôi đặt giới hạn cho bộ nhớ mà một quy trình được phép truy cập, biến OOM (ảnh hưởng đến toàn bộ quy trình) một cách hiệu quả thành MemoryErrors (chỉ ảnh hưởng đến một đường dẫn/luồng thực thi duy nhất và có thể được xử lý một cách thích hợp bởi . Trên thực tế, việc thiết lập giới hạn bộ nhớ cũng đơn giản như chạy chức năng sau ở đầu quy trình của bạn

Đoạn mã trên sẽ đảm bảo heap của quy trình (nơi Python phân bổ phần lớn dữ liệu của nó) không vượt quá giới hạn do cgroup của nó áp đặt. ⁷ Nếu bạn đang chạy quy trình python của mình trên cụm Kubernetes, quy trình này sẽ tương ứng với quy trình cho vùng chứa đang chạy quy trình nói trên

Đó là đủ dễ dàng, phải không?

Kích thước ngăn xếp chủ đề

Khi chúng tôi thử nghiệm bản sửa lỗi này lần đầu tiên, nó gần như ngay lập tức bị sập và cháy trong môi trường phát triển của chúng tôi. Vấn đề trở nên đơn giản. chúng tôi không tính đến kích thước ngăn xếp Python mặc định khá lớn trên các hệ thống Linux (khoảng 10Mb theo mặc định). Đây là kích thước của ngăn xếp C, được phân bổ trên heap của quy trình ngay khi bất kỳ luồng nào được tạo. Nếu không đủ bộ nhớ để sinh ra các luồng mới, thì chương trình đa luồng của bạn sẽ có một khoảng thời gian cực kỳ khốn khổ. Xem xét lượng bộ nhớ rất lớn được sử dụng bởi một luồng Python và giới hạn bộ nhớ tương đối thấp mà chúng tôi đang xử lý (ban đầu, mỗi quy trình được thiết lập để có giới hạn 2Gb), điều này có nghĩa là một quy trình sẽ rất nhanh đạt đến bộ nhớ của nó

Giải pháp cho điều này? . đến một số giá trị thấp hơn và hợp lý hơn (trong trường hợp của chúng tôi, chúng tôi đã kết thúc với 2Mb). Tất nhiên, điều này không xảy ra mà không có những nguy hiểm của nó - nếu quy trình Python đang thực hiện các cuộc gọi dẫn đến kích thước ngăn xếp C tăng lên đáng kể, thì điều này có thể gặp sự cố. Đối với mục đích của chúng tôi, giới hạn 2Mb khá thoải mái (có lẽ chúng tôi có thể thoát khỏi mức thấp hơn nhiều so với mức đó)

Hành vi khi giới hạn không gian địa chỉ

Điều đáng chú ý đối với hậu thế ở đây (và đối với những người khác tình cờ gặp phải vấn đề này) là lựa chọn sử dụng RLIMIT_DATA không phải là một lựa chọn đơn giản. Trên thực tế, ban đầu chúng tôi đã cố gắng đặt RLIMIT_AS (như trong bài đăng gốc đã mô tả giải pháp này), nhưng chúng tôi thấy rằng điều này tạo ra một số vấn đề cơ bản kỳ lạ. Để hiểu rõ những gì tôi đang nói, bạn có thể thử tự mình chạy đoạn mã Python này. ⁸

Đoạn mã trên sẽ bị lỗi khi đến luồng 18 — mức này thấp hơn nhiều so với giới hạn bộ nhớ 1Gb cho phép. Thông qua một số thử nghiệm và tìm hiểu về cách Python thực sự quản lý bộ nhớ của nó (về cơ bản, nó phân bổ bất kỳ bit bộ nhớ thú vị nào được sử dụng trên heap), chúng tôi đã kết luận rằng sử dụng RLIMIT_DATA phù hợp hơn. Trong thực tế, điều này đã được chứng minh là khá tốt. mã ở trên chạy trôi chảy khi chúng tôi hoán đổi RLIMIT_AS bằng RLIMIT_DATA — thực tế, trong trường hợp đó, bạn chỉ bắt đầu thấy lỗi khi chúng tôi cố gắng tạo ra hàng trăm luồng, phù hợp với kích thước ngăn xếp mặc định là 10Mb đã thảo luận ở trên

Mang tất cả lại với nhau

Sau một quá trình dài tìm hiểu và sửa lỗi dẫn đến thông tin trên, giờ đây chúng tôi có thể tóm tắt chính xác cách chúng tôi đã thay đổi các máy chủ gRPC của mình để hoạt động tốt hơn trong điều kiện tranh chấp bộ nhớ. Đối với người mới bắt đầu, ở đầu điểm vào của chúng tôi, chúng tôi đã thêm

Cuối cùng, trong mã máy chủ thực tế của chúng tôi, chúng tôi có thể chỉ cần bọc tất cả các điểm cuối của mình bằng trình trang trí sau. ⁹

Và đó là điều đó. Chà, gần như… giải pháp này sẽ xử lý chính xác hầu hết các lỗi phân bổ — ví dụ: nếu một người cố gắng tải quá nhiều dữ liệu từ cơ sở dữ liệu bên trong trình xử lý điểm cuối. Tuy nhiên, có một số trường hợp cạnh tiềm ẩn cũng cần được giải quyết, nếu ý định của một người là kỹ lưỡng (tất nhiên là tất cả chúng ta đều như vậy). Cụ thể, có một số lỗi là do lỗi cấp phát bộ nhớ nhưng không xuất hiện dưới dạng MemoryErrors

  1. Các ngoại lệ được đưa ra từ MemoryErrors, nhưng bản thân chúng không bảo toàn loại ngoại lệ. Tương đối đơn giản để giải thích cho những điều này, bằng cách đơn giản là đệ quy xuống một ngoại lệ __cause__ và kiểm tra xem đó có phải là một trường hợp MemoryError không
  2. Các trường hợp ngoại lệ được đưa ra khi trình thông dịch không thể bắt đầu các luồng mới do tranh chấp bộ nhớ (nghĩa là. nếu không đủ bộ nhớ để cấp phát ngăn xếp mới). Thật dễ dàng để tự tái tạo lỗi này bằng trình thông dịch python
Python 3.9.7 (default, Sep  3 2021, 20:10:26) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import resource
>>> from threading import Thread
>>>
>>> resource.setrlimit(resource.RLIMIT_DATA, (10_000_000, -1))
>>> t = Thread(target=lambda: 1 + 1)
>>> t.start()
Traceback (most recent call last):
File "", line 1, in
File "/usr/local/lib/python3.9/threading.py", line 892, in start
_start_new_thread(self._bootstrap, ())
RuntimeError: can't start new thread

Việc tính cả hai loại ngoại lệ này tương đối đơn giản, nếu hơi khó xử đối với loại ngoại lệ sau

Cuối cùng, chúng ta có thể thay đổi một chút trình trang trí ở trên để sử dụng trình xử lý lỗi mới này

Tất nhiên, giải pháp này sẽ không giải quyết được tất cả các vấn đề về trí nhớ của bạn. Nếu bạn hoạt động trong một môi trường hạn chế về bộ nhớ (điều mà tất cả các tính toán liên kết nguyên tử đều thực hiện vào cuối ngày), bạn sẽ luôn có một số điểm khiến bạn hết bộ nhớ. Tuy nhiên, giải pháp này sẽ cho phép bạn (a) cách ly tốt hơn các lỗi do các yêu cầu hoạt động kém và (b) không thành công bất cứ khi nào ứng dụng của bạn chạy vào tình trạng tranh chấp bộ nhớ

Điều gì về rò rỉ bộ nhớ?

Quay trở lại các giả thuyết ban đầu của chúng tôi khi gỡ lỗi các OOM này. giải pháp trên sẽ hữu ích bất cứ khi nào xảy ra trường hợp 2 và 3, nhưng trong trường hợp rò rỉ bộ nhớ (giả thuyết đầu tiên của chúng tôi), điều này sẽ không hữu ích lắm. Trên thực tế, những thay đổi ở trên thực sự có thể làm giảm độ tin cậy của dịch vụ nếu dịch vụ đó bị rò rỉ bộ nhớ đáng kể. Hãy xem xét ví dụ về một máy chủ bị rò rỉ một số bộ nhớ theo mọi yêu cầu đối với nó. Sau một thời gian, quá trình đó cuối cùng sẽ tiêu thụ đủ bộ nhớ để không thể xử lý bất kỳ yêu cầu mới nào. Nếu giải pháp trên được áp dụng, quá trình này sẽ tồn tại trong “trạng thái xấu” vô thời hạn. Mặt khác, nếu chúng ta loại bỏ các giới hạn bộ nhớ bằng cách sử dụng mô-đun tài nguyên, các OOM kết quả sẽ giết quá trình (tại thời điểm đó, bất kỳ phương pháp điều phối nào bạn đang sử dụng — trong trường hợp của chúng tôi là Kubernetes — nên khởi động lại quá trình với mức sử dụng bộ nhớ thấp). Sau đó, làm thế nào chúng ta có thể đảm bảo rằng rò rỉ bộ nhớ cuối cùng không ảnh hưởng tiêu cực đến độ tin cậy của chúng ta?

Điều quan trọng là bắt đầu ngay tại đây với một bài giảng. cách để tránh các sự cố vận hành do rò rỉ bộ nhớ là khắc phục chúng. Tôi đã thấy nhiều trường hợp các dịch vụ sản xuất dựa vào cơ chế điều phối để khởi động lại chúng theo định kỳ nhằm tránh rò rỉ bộ nhớ. Mặc dù điều này có thể chứng minh là một giải pháp khả thi trong ngắn hạn, nhưng nó khó có thể là một tiêu chuẩn kỹ thuật mà chúng ta nên hướng tới. Thay vào đó, người ta nên hướng tới (a) được cảnh báo bất cứ khi nào xảy ra rò rỉ bộ nhớ tiềm ẩn và (b) có một số cơ chế để hạn chế tác động tiêu cực của rò rỉ bộ nhớ trong môi trường sản xuất, khi nó xảy ra

Ngoài ra, giải pháp được đề xuất ở đây thực hiện như thế nào đối với tiêu chí (a) và (b) của chúng tôi? . nếu một máy chủ bị rò rỉ bộ nhớ, thì giải pháp này cuối cùng sẽ dẫn đến việc máy chủ không thể đáp ứng hầu hết các yêu cầu (điều này sẽ kích hoạt một số cảnh báo tự động của bạn, phải không?). Tuy nhiên, đối với (b), giải pháp hiện tại không đủ. Giải pháp. để sửa đổi kiểm tra mức độ sẵn sàng và hoạt động của máy chủ để tính đến bộ nhớ đã sử dụng và khả dụng. Mặc dù chúng tôi sẽ không đi sâu vào chi tiết mã để làm như vậy (nó sẽ tương đối đơn giản… và hơn nữa, có lẽ bạn đang cảm thấy mệt mỏi khi đọc về OOM bây giờ), chúng tôi có thể mô tả ngắn gọn một số giải pháp tiềm năng tại đây

  1. Theo dõi bộ nhớ khả dụng cho quy trình trong quá trình kiểm tra mức độ sẵn sàng — nếu bộ nhớ liên tục ở dưới một ngưỡng nhất định (i. e. , ngưỡng mà bạn ước tính là cần thiết để xử lý từng yêu cầu cận biên), thì việc kiểm tra mức độ sẵn sàng không thành công. Nếu trạng thái này tồn tại trong một khoảng thời gian dài, thì kiểm tra tính sống động cũng không thành công (điều này sẽ khiến Kubernetes khởi động lại nhóm/quy trình của bạn)
  2. Theo dõi số lần máy chủ của bạn gặp phải "lỗi phân bổ" trong một khoảng thời gian gần đây. Nếu giá trị đó vượt quá một số ngưỡng tỷ lệ phần trăm của tất cả các yêu cầu, thì kiểm tra tính sống động sẽ trả về phản hồi không thành công

Tất nhiên, cả hai phương pháp này đều gặp phải vấn đề là chúng sẽ giết chết ứng dụng của bạn, có khả năng làm gián đoạn các yêu cầu lẽ ra đã thành công. Đây là lý do tại sao, vào cuối ngày, tất cả những gì bạn có thể hy vọng là phát hiện và khắc phục mọi rò rỉ bộ nhớ tiềm ẩn để đảm bảo máy chủ của bạn đáng tin cậy

[1] Theo mặc định. Chúng tôi sẽ khám phá các cách để thay đổi điều này trong phần sau của bài đăng này

[2] Trên thực tế, "mức sử dụng bộ nhớ luồng" thậm chí không phải là một đại lượng được xác định rõ ràng, chứ đừng nói đến một đại lượng dễ đo lường

[3] Điều này có thể được thực hiện bằng cách chỉ cần đặt số lượng công nhân tối đa cho ThreadPoolExecutor được đề cập ở trên thành 1

[4] Ok, đây là một nửa sự thật. Quy trình làm việc không đồng bộ được xây dựng độc lập để xử lý các yêu cầu tương tự với quy trình làm việc đồng bộ và kết quả là tạo ra các lỗi OOM dễ gỡ lỗi hơn nhiều. Tuy nhiên, chiến lược mà chúng tôi trình bày ở đây là một chiến lược hoàn toàn đúng đắn cho các trường hợp mà một người muốn gỡ lỗi như vậy trong khi vẫn sử dụng quy trình làm việc hoàn toàn đồng bộ

[5] Lưu ý rằng, để thực thi yêu cầu không đồng bộ ngay từ đầu, chúng tôi phải triển khai các tài nguyên bổ sung trong cụm Kubernetes của mình. Điều này có vẻ phản trực giác vì chúng tôi đang cố gắng giải quyết vấn đề phát sinh từ môi trường hạn chế về tài nguyên. Thực tế ở đây là chúng tôi không bị hạn chế về nguồn lực theo nghĩa tuyệt đối. chúng tôi thấp hơn nhiều so với giới hạn phiên bản của mình, do đó, việc triển khai các phiên bản mới để xử lý các yêu cầu không đồng bộ là khá dễ dàng. Tuy nhiên, các tài nguyên bị hạn chế trên mỗi nút đơn lẻ, đây là thứ đã tạo ra các OOM ngay từ đầu

[6] Đây cũng là một nửa sự thật. Hệ thống quản lý bộ nhớ của Python phức tạp hơn thế này, vì nó quản lý cấp phát bộ nhớ khác nhau cho các đối tượng dưới 512 byte. Cụ thể, bộ cấp phát bộ nhớ sẽ không nhất thiết trả lại bộ nhớ cho HĐH khi các đối tượng nhỏ bị xóa. Trong trường hợp cụ thể của chúng tôi (và tôi nghi ngờ điều này khá khái quát) phần lớn việc cấp phát bộ nhớ đến từ các đối tượng lớn, vì vậy các phép đo bộ nhớ thường trú trước và sau khi yêu cầu được xử lý hoàn toàn đại diện cho việc cấp phát/giải phóng bộ nhớ “thực sự”.

[7] Nếu bạn đang chạy một ứng dụng trong bộ chứa docker, giới hạn bộ nhớ cgroup giống với giới hạn do Docker áp đặt trên bộ chứa

[8] Ban đầu chúng tôi đã thử nghiệm điều này bằng cách sử dụng python. 3. 7. 3 hình ảnh docker, nhưng kể từ đó đã xác nhận hành vi tương tự trên 3. 9. 7

[9] Trong thực tế, chúng tôi sử dụng trình chặn máy chủ gRPC ở đây — tuy nhiên, trình trang trí sẽ phục vụ cùng một mục đích và dễ theo dõi hơn nếu bạn không quen lắm với trình chặn

Làm cách nào để kiểm tra mức tiêu thụ bộ nhớ trong Python?

Bạn có thể sử dụng nó bằng cách đặt trình trang trí @profile quanh bất kỳ hàm hoặc phương thức nào và chạy python -m memory_profiler myscript . Bạn sẽ thấy mức sử dụng bộ nhớ theo từng dòng sau khi tập lệnh của bạn thoát.

Làm cách nào để chẩn đoán rò rỉ bộ nhớ trong Python?

Bạn có thể phát hiện rò rỉ bộ nhớ trong Python bằng cách theo dõi hiệu suất của ứng dụng Python thông qua công cụ Giám sát hiệu suất ứng dụng như Scout APM . Khi bạn phát hiện rò rỉ bộ nhớ, có nhiều cách để giải quyết.

Tại sao Python chiếm quá nhiều bộ nhớ?

Python sẽ tự động giải phóng các đối tượng không được sử dụng. Đôi khi các lệnh gọi hàm có thể giữ các đối tượng trong bộ nhớ một cách bất ngờ; . Việc lưu trữ số nguyên hoặc số thực trong Python chiếm một lượng lớn bộ nhớ .