← ყველა სტატია

Python-ის მეხსიერების გაჟონვის პროფილირება პროდაქშენზე memray-ით — და ავარია, რომელიც გზაში მოვაწყვეთ

ბექენდი ყოველ 10–15 საათში OOM-ით ეცემოდა. ცოცხალ პოდზე memray-ის საათობრივმა ანაბეჭდებმა გაჟონვა ბიბლიოთეკის middleware-ში იპოვა და არა აპლიკაციის კოდში — მას შემდეგ, რაც პირველმა მცდელობამ პროდაქშენი შვიდი წუთით გათიშა.

Gambchamp-ის ერთ-ერთ პროდუქტის პლატფორმაზე Python-ის ბექენდი რეგულარულად იღებდა OOM-kill-ს. ლიმიტი იყო 1 GiB, პოდი 10–15 საათს ცოცხლობდა, მეხსიერება თანაბრად იზრდებოდა და ის 137 კოდით კვდებოდა. დეველოპერებს კოდის კონკრეტული ხაზები სჭირდებოდათ და არა „იზრდება“, ამიტომ გადავწყვიტეთ memray-ით პროფილი პირდაპირ ცოცხალ პროდაქშენის პოდზე მოგვეხსნა — ერთი რეპლიკა, რეალური ტრაფიკი.

ჯერ — დახრილობის გაზომვა

სანამ რამეს მივაერთებდით, გაჟონვა გარედან გავზომეთ. შენახული მეხსიერება საათში დაახლოებით 28 MB-ით იზრდებოდა, ანონიმური — 48 MB-ით (310 → 503 MB ოთხ საათში). დაახლოებით 350 MB-იანი სტარტიდან 1 GB-იან ლიმიტამდე დაახლოებით ათი საათია — ზუსტად OOM-ებს შორის ინტერვალი. ესე იგი, ეს ერთი თანაბარი გაჟონვა იყო და არა ნახტომი.

პირველმა მცდელობამ პროდაქშენი გათიშა

Deployment-ი ისე შევცვალეთ, რომ აპლიკაცია memray run-ით გაშვებულიყო, და ძველი runbook-იდან ავიღეთ ხრიკი: კონტეინერი sleep infinity-ით ცოცხლად შეგვენახა და liveness-პრობა მოგვეხსნა, რომ პროფილერი არ მოეკლათ.

memray segfault-ით დაეცა (--trace-python-allocators ვერ ეწყობა greenlet-სა და asyncpg-ს). აპლიკაცია მასთან ერთად მოკვდა — კონტეინერი კი sleep-ში „ცოცხალი“ დარჩა, ამიტომ kubelet-მა ვერაფერი დაინახა გადასატვირთად. პროდაქშენი დაახლოებით შვიდი წუთი არ მუშაობდა.

გამოსწორება მარტივია და ახლა წესად იქცა: wrapper მთავრდება exec python main.py-ით, liveness-პრობა კი რჩება. პროფილერი მოკვდა — კონტეინერიც კვდება და Kubernetes თავის საქმეს აკეთებს.

იმავე საღამოს სხვა ხაფანგები:

  • --aggregate რეჟიმი აგრეგაციას პროცესის მეხსიერებაში ინახავს, ამიტომ segfault-ის შემდეგ გამომავალი ფაილი ცარიელი აღმოჩნდა — პროფილის თორმეტი წუთი დაიკარგა. პროდაქშენის სერვისებზე — მხოლოდ ჩვეულებრივი რეჟიმი;
  • ნაგულისხმევმა startup-პრობამ პირველი პოდი მოკლა: memray-ით სტარტი უფრო ნელია;
  • HPA-მ მეორე რეპლიკა დაამატა ჩვენივე ანაბეჭდების ატვირთვის CPU-ს გამო, შემდეგ კი scale-down-ისას პოდი წაშალა. controller.kubernetes.io/pod-deletion-cost ანოტაცია პროფილირებულ პოდზე მას იცავს ავტოსკეილინგის შეხების გარეშე.

საათობრივი ანაბეჭდები ერთი გრძელი გაშვების ნაცვლად

memray-ის მოჭრილი ფაილი მაინც იკითხება, ამიტომ გაშვების დასრულების ლოდინი საჭირო არ არის. ყოველ საათში ვიღებდით ანაბეჭდს და ვადარებდით მათ ალოკაციის ადგილების მიხედვით. Diff შლის ყველაფერს, რაც სტარტისას გამოიყო, და ტოვებს მხოლოდ იმას, რაც იზრდება. ოთხ საათში ხუთმა ანაბეჭდმა სამი წრფივი წყარო აჩვენა:

  • json.loads prometheus_client-ის multiprocess-რეჟიმში — 67 MB და ზრდა საათში დაახლოებით 13,6 MB; Prometheus-ის მთელი ჯგუფი — დაახლოებით 94 MB, მთელი შენახული მეხსიერების 22 %;
  • brotli ASGI middleware, რომელიც ყოველ მოთხოვნაზე კომპრესორს ქმნის, — საათში დაახლოებით 4 MB;
  • ჩვენივე payload-ის მასკირების ფუნქცია ლოგირების middleware-ში, რომელიც ლოგირებამდე მოთხოვნის მთელ სხეულს აკოპირებს, — დაახლოებით 1 MB ყოველ გამოძახებაზე.

მთავარი აღმოჩენა პირველია. Multiprocess-რეჟიმი ყოველ scrape-ზე ხელახლა კითხულობს და აპარსებს თავის mmap-ფაილებს — სერვისი კი ერთ პროცესში მუშაობს. მას multiprocess-რეჟიმი საერთოდ არ სჭირდებოდა.

არც ერთი წყარო არ იყო აპლიკაციის endpoint. ამიტომ იყო ზრდა თანაბარი და არ უკავშირდებოდა რომელიმე მარშრუტს.

საკუთარი თავის შემდეგ დალაგება პროფილირებაზე რთული აღმოჩნდა

CI-ით ხელახლა დავადეპლოიეთ და ვიფიქრეთ, რომ პატჩი აღარ იყო. დარჩა. helm upgrade მხოლოდ იმ ველებს აბრუნებს, რომლებსაც ჩარტი ადგენს. memray-ის ბრძანება, დამატებითი volume, grace period და evict-ის ანოტაცია kubectl patch-ით დაემატა და ჩარტში არ იყო. სერვისი ისევ პროფილერით მუშაობდა, უკვე სტანდარტული 1 GiB ლიმიტით — უარესი, ვიდრე მანამდე. დაგვეხმარა მეორე, დამასრულებელი პატჩი და ცოცხალი სპეციფიკაციის ბექაპთან ველ-ველ შედარება. ახლა ვამოწმებთ PID 1-ს და პროფილის volume-ის არარსებობას და არა grep memray-ს /proc-ში (შენი საკუთარი grep საკუთარ თავს პოულობს).

დასკვნები

  • არასოდეს შეინახოთ კონტეინერი ცოცხლად sleep-ით, სანამ ნამდვილი პროცესი პროფილერის ქვეშ მუშაობს. დაასრულეთ wrapper exec-ით და დატოვეთ liveness.
  • ანაბეჭდები პერიოდულად აიღეთ და ალოკაციის ადგილებით შეადარეთ: ასე სტარტის ალოკაციები გაჟონვისგან გამოიყოფა.
  • შეამოწმეთ, რა ღირს ბიბლიოთეკის რეჟიმი, სანამ ჩართავთ: multiprocess-მეტრიკები ერთპროცესიან სერვისში მეხსიერების 22 % იყო.
  • ნებისმიერი უკან დაბრუნების შემდეგ შეადარეთ ცოცხალი სპეციფიკაცია ბექაპს. helm upgrade არ შლის იმას, რაც kubectl patch-მა დაამატა.
  • 350 MB საბაზისო მოხმარება 1 GB ლიმიტის პირობებში ცოტა მარაგს ტოვებს ფიქსის შემდეგაც — ლიმიტი პაიპლაინით გადახედეთ და არა პატჩით.