Jak zmniejszyliśmy o 50% zużycie pamięci w aplikacji Rails dzięki Jemalloc
Jednym z pierwszych projektów, w których brałam udział w Valiant, była próba zoptymalizowania wydajności i zużycia pamięci w aplikacji napisanej w Railsach. Chociaż słyszałam odwieczne skargi na to, że aplikacje napisane w Rails są powolne, masywne i obciążają pamięć, nie znalazłem wcześniej żadnych praktycznych i łatwych do wprowadzenia rozwiązań tego problemu.
Dopóki nie odkryliśmy jemalloc.
W tym artykule opiszę w skrócie czym jest jemalloc; jak sprawdzić aktualną wydajność aplikacji i jej zużycie pamięci (w tym przetestować czy nie ma wycieków pamięci) oraz jak zainstalować jemalloc lokalnie i na produkcji. A na koniec pokażę, jakie były nasze wyniki po przejściu na jemalloc (uwaga spoiler: udało nam się na produkcji zmniejszyć zużycie pamięci o połowę!)
Czym jest jemalloc?
Ruby używa tradycyjnie funkcjimalloc
języka C do dynamicznego przydzielania, zwalniania i ponownego przydzielania pamięci by przechowywać obiekty. Jemalloc to implementacja malloc(3) opracowana przez Jasona Evansa (stąd dwie pierwsze litery „je” przed malloc), która wydaje się być bardziej skuteczna w alokacji pamięci w porównaniu z innymi sposobami, głównie ze względu na skoncentrowaniu się na unikaniu fragmentacji i skalowalnej obsłudze współbieżności.Krok 1: Sprawdź zużycie pamięci twojej aplikacji
Aby ustalić czy przejście na jemalloc faktycznie ma jakikolwiek pozytywny wpływ na twoją aplikację Rails, najpierw musisz wiedzieć ile pamięci zużywa aplikacja i jak szybko strona odpowiada. Aby sprawdzić to lokalnie dodałem następujące gemy do Gemfile (i uruchomić bundle install):gem "memory_profiler"
gem "derailed_benchmarks"
(Uwaga: możesz uruchomić testy z tymi gemami na środowisku developerskim, testowym i produkcyjnym. Jeśli chcesz je uruchomić na środowisku developerskim/testowym, upewnij się, że usunąłeś gem dotenv-rails z Gemfile)Aby określić całkowite zużycie pamięci przez każdy gem w pliku Gemfile, uruchom:
bundle exec derailed bundle:mem
Aby wyświetlić tylko pliki, które zużyły za dużo pamięci dodaj
CUT_OFF=0.3
(lub dowolną liczbę, która cię interesuje). Zauważ, że Ruby wczytuje plik raz, więc jeżeli kilka bibliotek będzie wymagać tego samego pliku, zostanie on wczytany tylko w pierwszej bibliotece (duplikaty będą zawierać listę wszystkich rodziców, do których należą). Na przykład tak wyglądał krótki fragment naszych wyników:
TOP: 70.2617 MiB
rails/all: 16.4805 MiB
rails: 6.1523 MiB (Also required by: active_record/railtie, active_model/railtie, and 8 others)
rails/application: 4.707 MiB
rails/engine: 3.543 MiB (Also required by: coffee/rails/engine)
rails/railtie: 3.293 MiB (Also required by: global_id/railtie, sprockets/railtie, and 3 others)
rails/configuration: 3.1484 MiB (Also required by: rails/railtie/configuration)
active_support/core_ext/object: 3.0469 MiB (Also required by: paper_trail/has_paper_trail)
active_support/core_ext/object/conversions: 2.5078 MiB
active_support/core_ext/hash/conversions: 1.8945 MiB (Also required by: active_record/serializers/xml_serializer, active_model/serializers/xml)
active_support/time: 1.7031 MiB (Also required by: active_record/base)
active_support/core_ext/time: 1.625 MiB
active_support/core_ext/time/calculations: 1.5391 MiB (Also required by: active_support/core_ext/numeric/time, active_support/core_ext/string/conversions)
active_support/core_ext/time/conversions: 1.1094 MiB (Also required by: active_support/core_ext/time, active_support/core_ext/date_time/conversions)
active_support/values/time_zone: 1.0664 MiB (Also required by: active_support/time_with_zone, active_support/core_ext/date_time/conversions)
tzinfo: 0.8438 MiB (Also required by: et-orbi)
tzinfo/timezone: 0.3867 MiB
(Uwaga: 1 Mebibajt (MiB) = około 1,05 MB)
Ponadto, na środowisku produkcyjnym można zobaczyć liczbę utworzonych obiektów (według lokalizacji, a także według gemów), kiedy wymagane są zależności przy uruchamianiu:
bundle exec derailed bundle:objects
Poniżej znajduję się próbka obiektów stworzonych przez nasze gemy:348351 activesupport
66931 erubis
54842 json
23655 addressable
15078 bundler
14833 heroics
13313 ruby
13034 haml
7186 actionpack
6370 sass
Aby sprawdzić, czy są wycieki pamięci w środowisku produkcyjnym, możesz uruchomić:bundle exec derailed exec perf:mem_over_time
Wskazówka: Aby ustawić liczbę testów, które chcesz uruchomić, tak by nie trwały w nieskończoność, użyj: TEST_COUNT=20_000 bundle exec derailed exec perf:mem_over_time.
Polecenie to wysyła wiele zapytań do aplikacji i w tym czasie profiluje zużycie pamięci, jeżeli masz luki w pamięci, jej zużycie będzie rosnąć. W normalnych warunkach zauważysz, że zużycie pamięci aplikacji rośnie, aż osiągnie plateau a następnie zacznie spadać.
Pamiętaj, że wyniki mogą być różne (i będą wydawać się różne dla 2000 testów w porównaniu do 20000 testów). Uruchamiając testy kilkukrotnie, odkryliśmy, że średnie plateau naszej aplikacji wynosi około 1,7-1,8 MiB. Mniej więcej odpowiada to naszym logom z Heroku, które pokazywały wartość 1,6 MiB.
Krok 2: Sprawdź wydajność i szybkość aplikacji
Ogólną wydajność aplikacji możesz sprawdzić, wywołując określone endpointy, używając benchmark-ips (czyli sprawdzać wydajność iteracji kodu na sekundę) za pomocą tego polecenia:bundle exec derailed exec perf:ips
Im wyższa wartość, tym lepiej, ponieważ oznacza to więcej iteracji na sekundę. Niektóre z naszych wyników są następujące: Warming up --------------------------------------
ips 1.000 i/100ms
Calculating -------------------------------------
ips 5.070 (± 0.0%) i/s - 26.000 in 5.141956s
Warming up --------------------------------------
ips 1.000 i/100ms
Calculating -------------------------------------
ips 5.162 (± 0.0%) i/s - 26.000 in 5.051505s
Warming up --------------------------------------
ips 1.000 i/100ms
Calculating -------------------------------------
ips 4.741 (± 0.0%) i/s - 24.000 in 5.125214s
Krok 3. Zainstaluj Jemalloc (lokalnie i na produkcji)
Aby zainstalować jemalloc lokalnie, po prostu dodaj poniższy kod do Gemfile i wywołaj bundle install:gem 'jemalloc'
Uwaga: jeśli używasz rvm (i już zainstalowałeś Ruby 2.4.1) uruchom rvm reinstall 2.4.1 –C –with-jemalloc
aby przeinstalować Ruby z jemalloc. Aby sprawdzić, czy twoja wersja Ruby używa jemalloc uruchom:
ruby -r rbconfig -e "puts RbConfig::CONFIG['LIBS']".
Odpowiedź powinna wyglądać mniej więcej tak:-lpthread -ljemalloc -ldl -lobjc
(-ljemalloc
oznacza, że jemalloc jest ładowane razem z Ruby). Jak to wygląda na produkcji? Istnieje kilka sposobów dodania jemalloc na Heroku, ale okazało się, że najłatwiejszym sposobem było dodanie buildpacka za pomocą tej komendy:
heroku buildpacks:add --index 1 https://github.com/mojodna/heroku-buildpack-jemalloc.git --app [your app name here]
Aby potwierdzić, że jemalloc został zainstalowany uruchom heroku buildpacks –app [nazwa twojej aplikacji]
, powinna ci się wyświetlić lista buildpacków. Możesz także dodać buildpack wchodząc w Ustawienia->Buildpacks->Dodaj buildpack w panelu Heroku.
Krok 4. Sprawdź wyniki lokalnie
Uruchom te same polecenia co w Krok 1. aby sprawdzić zużycie pamięci i szybkość aplikacji po zainstalowaniu jemalloc. Nasze wyniki wykazały 8,6953 MiB (9,117 MB) – co stanowi 12,38% - oszczędności pamięci w całej aplikacji. Na alokacji obiektów zaoszczędziliśmy 5,064 MiB (5,310 MB) pamięci.Krok 5. Sprawdź wyniki na produkcji
Użyliśmy Siege, narzędzia do testowania i porównywania obciążeń http, aby przeciążyć nasze aplikacje zapytaniami. Odkryliśmy, że bez jemalloc wykonywaliśmy średnio około 2,5 operacji na sekundę, przy średnio 160 nieudanych w ciągu 20 minut. Podczas gdy z jemalloc uzyskaliśmy średnio 6,6 operacji na sekundę i tylko 1,5 nieudanych w ciągu 20 minut.Najbardziej imponujące było to, że platforma Heroku wykazała niezwykłe, niemal natychmiastowe ulepszenia. Przed użyciem jemalloc zużycie pamięci osiągnęło 2 GB, wtedy musieliśmy automatycznie ją zresetować. Po użyciu jemalloc zużycie spadło do 1 GB i całkowicie uniknęliśmy wymuszonych restartów.
Heroku memory consumption pre and post-jemalloc.
Podobnie było w przypadku czasu reakcji, odnotowaliśmy gwałtowny spadek, gdzie przed jemalloc niektóre zapytania trwały po 30 sekund. Po instalacji jemalloc czas ten spadł do około 5-10 sekund.
Heroku response time pre and post-jemalloc.