Ruby on C(ocaine) 💉

Od jakiegoś czasu chodziło mi po głowie stworzenie rozszerzenia w C dla języka Ruby. Po kilku wieczorach pracy stworzyłem mini biblioteczkę – MatrixBoost, która przyspiesza niektóre operacje (aktualnie mnożenie i odwracanie macierzy) klasy Matrix ze standardowej biblioteki Ruby’ego.

Post dostępny tutaj:

https://dev.to/bajena/ruby-on-c-ocaine-intro-to-c-extensions-for-newbies-40ib

Co oznacza błąd „invalid byte sequence in UTF-8” w Ruby i jak go naprawić?

Jeśli trafiłeś na tą stronę to prawdopodobnie zmagasz się z tym samym problemem, co ja. Z tego posta dowiesz się trochę o tym, czym są kodowania znaków, skąd biorą się nieprawidłowe sekwencje bajtów i jak je naprawić.

Post można przeczytać pod tym adresem:

https://dev.to/bajena/solving-invalid-byte-sequence-in-utf-8-errors-in-ruby-1f27

Sparse fieldsets: Przyspieszamy JSON API

Zgodnie ze zwyczajem z ostatnich kilku miesięcy chciałbym zaprosić do przeczytania mojego najnowszego posta na Medium. Artykuł pokazuje jak w prosty sposób przyspieszyć aplikację wykorzystującą JSON API za pomocą mało znanej (mimo że opisanej w specyfikacji) funkcji „sparse fieldsets”.

Post można przeczytać pod tym adresem: https://medium.com/@bajena3/decrease-load-on-your-json-apis-by-using-sparse-fieldsets-3e2c9491dc16

Google Apps Scripts – równoległe zapytania HTTP i obsługa błędów

W moim najnowszym wpisie na Medium można przeczytać o tym, w jaki sposób przyspieszyłem moje aplikację bazujące na Google Apps Scripts wykonując wiele zapytań HTTP jednocześnie oraz jak poradziłem sobie z problemem obsługi błędów w sytuacji, gdy część zapytań zwraca błędne rezultaty.

Post można przeczytać pod tym adresem:
https://medium.com/@bajena3/google-apps-scripts-parallel-http-requests-with-retries-5a24feaf61d8

Boost wydajności w Ruby on Rails dzięki technice batchingu

W tym poście chciałbym przybliżyć pojęcie batchingu i pokazać jak w prosty sposób wykorzystać batch loading w Railsowych aplikacjach, aby zmniejszyć liczbę zapytań do bazy danych.

Problem N+1 zapytań

Ruby on Rails to framework, który zyskał ogromną popularność dzięki temu, że umożliwia bardzo szybkie prototypowanie aplikacji. Dosłownie w przeciągu kilku godzin możemy napisać MVP naszego produktu. Wszystko działa jak należy, klient jest zadowolony więc dostajemy zlecenia na kolejne ficzery i usprawnienia. Z zapałem dodajemy kolejne tabele do bazy danych, akcje w kontrolerach i widoki, jednak w pewnym momencie zaczynamy zauważać, że aplikacja zaczyna mulić przy każdym przeładowaniu strony🙀.

Bardzo często okazuje się, że przyczyna problemu leży w nieoptymalnym sposobie, w jaki pobieramy dane z bazy danych.
Przykładowo, załóżmy, że nasza aplikacja ma dwie tabele: users i posts i wykonamy następujący kod:

posts = Post.where(id: [1, 2, 3])
# SELECT * FROM posts WHERE id IN (1, 2, 3)
users = posts.map { |post| post.user }
# SELECT * FROM users WHERE id = 1
# SELECT * FROM users WHERE id = 2
# SELECT * FROM users WHERE id = 3
view raw gistfile1.rb hosted with ❤ by GitHub

ActiveRecord wykonało jedno zapytanie do bazy danych aby pobrać listę postów, a następnie kolejne 3 zapytania, aby dla każdego z postów pobrać autora. Baza danych została obciążona aż czterema zapytaniami, mimo, że ten sam efekt można było osiągnąć za pomocą dwóch – jednego w celu pobrania postów i kolejnego po użytkowników, znając ich IDki.

Ten problem jest nazywany problemem N+1 zapytań (N+1 queries problem).

Typowe metody rozwiązywania problemu N+1 zapytań w Rails

W Railsach do walki z problemem N+1 zapytań często wykorzystuje się następujące metody:

Metoda „includes”

Użycie metody „includes” spowoduje automatyczne załadowanie rekordów dla wskazanej w argumencie relacji (w tym wypadku jest to „user”).

To rozwiązanie sprawdza się bardzo dobrze w przypadku prostych akcji, jednak jej użycie może prowadzić do załadowania zbędnych danych (np. kiedy potrzebujemy użytkowników tylko dla postów z id 1,2). Z tego powodu warto pamiętać, że includes nie jest tzw. golden bullet i używać go rozsądnie 🙂

Poniżej przykład użycia metody:

posts = Post.where(id: [1, 2, 3]).includes(:user)
# SELECT * FROM posts WHERE id IN (1, 2, 3)
# SELECT * FROM users WHERE id IN (1, 2, 3)
users = posts.map { |post| post.user }
view raw gistfile1.rb hosted with ❤ by GitHub

Metoda „joins”

Jeżeli rekordy relacji są nam potrzebne jedynie w celu przefiltrowania danych dobrym pomysłem może być użycie metody „joins”, będącej po prostu Railsowym odpowiednikiem SQL-owego JOIN-a.

Poniższy przykład pokazuje wykorzystanie rekordów z tabeli „users” bez konieczności ładowania danych do pamięci:

Post.joins(:users).where(users: { country_code: 'PL' }).map { |post| post.title }
# SELECT "posts".* FROM "posts" INNER JOIN "users" ON "posts"."user_id" = "users"."id" WHERE "users"."country_code" = $1
view raw gist.rb hosted with ❤ by GitHub

Wadą zarówno metody „joins”, jak „includes” jest to, że mogą one generować nieelastyczny kod odpowiedzialny za ładowanie danych, który uzależniony będzie od tego, jak wyglądają np. nasze widoki.

Przykładowo, chcąc wyświetlić listę użytkowników z postami i komentarzami do każdego z nich musimy zadeklarować chęć załadowania za jednym zamachem wszystkich relacji. W razie potrzeby zmiany widoków będziemy musieli przerabiać też kod odpowiedzialny za ładowanie danych.

Batching to the rescue!

Mimo, że wyżej wymienione są użyteczne w niektórych scenariuszach, to często w „prawdziwym świecie” okazują się niewystarczające, z racji tego, że mogą powodować ładowanie zbędnych danych lub generowanie kodu spaghetti.

Alternatywą dla tych rozwiązań jest tytułowy „batching” – technika, która rozprzestrzeniła się w dużej mierze dzięki Facebookowi i ich standardzie GraphQL.

„Batching” można przedstawić jako proces składający się z trzech kroków:

  1. Etap zbierania danych do załadowania – aplikacja gromadzi ID rekordów wymaganych np. do wyrenderowania widoku. Nawiązując do poprzednich przykładów mogą to być ID autorów poszczególnych postów.
  2. Etap ładowania i cache’owania – po zgromadzeniu potrzebnych ID rekordów odpowiednie dane są ładowane za pomocą jednego zapytania i cache’owane w pamięci, w celu uniknięcia ponownego zapytania do bazy danych.
  3. Etap dystrybucji danych – odpowiednie rekordy są przekazane do miejsc w kodzie, które zgłosiły po nie potrzebę.

Dużą zaletą takiego podejścia jest to, że pozwala załadować dokładnie te dane, które są nam potrzebne w danym momencie i zwalnia nas z przymusu deklaracji wszystkich potrzebnych relacji w jednym miejscu.

Dodatkową korzyścią z poznania tej techniki jest fakt, że znajduje zastosowanie nie tylko w SQL-owych bazach danych, ale również bazach NO-SQL (np. Mongo, Cassandra). Co więcej, oprócz optymalizacji zapytań do bazy danych można jej też użyć np. do ograniczenia liczby zapytań HTTP do zewnętrznych serwisów.

Batching w Ruby

Osobom zainteresowanym wykorzystaniem tej techniki w aplikacjach napisanych w Rubym polecam zapoznanie się z gemem Batch Loader. Na początku koncept klasy BatchLoader może wydać się nieco nieintuicyjny, ale gwarantuję, że widok malejących logów zapytań do bazy danych będzie wart wysiłku 🙂

Przyjrzyjmy się, w jaki sposób BatchLoader pozwala wyeliminować problem N+1 zapytań:

# app/models/post.rb
def user_lazy
BatchLoader.for(user_id).batch do |user_ids|
User.where(id: user_ids)
end
end
posts = Post.where(id: [1, 2, 3]) # SELECT * FROM posts WHERE id IN (1, 2, 3)
users = posts.map { |post| post.user_lazy }
users.each { |user| puts "#{user.name}" } # SELECT * FROM users WHERE id IN (1, 2, 3)
view raw user_lazy.rb hosted with ❤ by GitHub

Wywołanie metody „user_lazy” na każdym z postów pozwoliło Batch Loaderowi na zgromadzenie ID wszystkich potrzebnych użytkowników.
Odpytanie bazy danych zostało odroczone, dzięki leniwej ewaluacji, aż do momentu, w którym atrybuty poszczególnych użytkowników są niezbędne do wyświetlenia. Dopiero wtedy BatchLoader pobiera za jednym zamachem trzy rekordy z tabeli „users”.

Oprócz zapobiegania N+1 zapytaniom BatchLoader posiada również cache, który zapobiega ponownemu załadowaniu rekordów, które zostały załadowane wcześniej:

users = posts.map { |post| post.user_lazy }
users.each { |user| puts "#{user.name}" } # SELECT * FROM users WHERE id IN (1, 2, 3)
users.each { |user| puts "#{user.address}" } # Pobierze użytkowników z cache'u
view raw cache.rb hosted with ❤ by GitHub

Batching w Rails-owym REST API

BatchLoader wydał mi się bardzo użytecznym narzędziem, dlatego postanowiłem wykorzystać go w celu optymalizacji Railsowej aplikacji wystawiającej dość rozbudowane JSON API renderowane za pomocą popularnej biblioteki ActiveModel::Serializers.

Pomysł okazał się trafiony i aplikacja dostała wyraźnego kopa, jednak przy każdym kolejnym użyciu BatchLoadera zaczeła przeszkadzać mi ilość boilerplate code’u, czyli powtarzalnego kodu niezbędnego do dodania nowego batch loadera.

Wiele z serializerów powtarzało podobne schematy, jak na przykład ten:

class PostSerializer < ActiveModel::Serializer
attributes :id, :title
belongs_to :user
def self.lazy_user(post)
BatchLoader.for(post.user_id).batch do |user_ids|
User.where(id: user_ids)
end
end
def author
self.class.lazy_user(object)
end
end
view raw serializer.rb hosted with ❤ by GitHub

Eliminowanie powtarzającego się kodu doprowadziło do tego, że postanowiłem opakować całość w postaci gema i udostępnić społeczności jako ams_lazy_relationships (https://github.com/Bajena/ams_lazy_relationships/).

Gem ten jest rozszerzeniem do ActiveModel::Serializers i umożliwia korzystanie z batchingu w serializerach za pomocą metod lazy_has_many/lazy_has_one/lazy_belongs_to.

Przykładowo, wcześniejszy serializer zmodyfikowany tak, by korzystać z ams_lazy_relationships będzie wyglądał następująco:

class BaseSerializer < ActiveModel::Serializer
include AmsLazyRelationships::Core
end
class PostSerializer < BaseSerializer
attributes :id, :title
lazy_belongs_to :user
end

Tym prostym sposobem możemy uniknąć N+1 zapytań podczas serializacji nawet bardzo skomplikowanych obiektów.

Gem działa out-of-the-box z ActiveRecord-owymi SQL-owymi relacjami, ale można go użyć również do batch loadingu danych z różnych źródeł (ja np. korzystam do ładowania danych z Cassandry i MySQL-a).

Więcej przykładów i dokładniejsze objaśnienia można znaleźć w README gema albo w tym poście.

Podsumowanie

Mam nadzieję, że mój post zachęci kogoś z czytelników do zgłębienia idei batchingu. Myślę że warto ten koncept, znany głównie w kręgu ludzi zajmujących się technologią GraphQL, zaadaptować również w aplikacjach wykorzystujących stare dobre REST API, czy server-side rendering. Zapraszam do komentowania i zadawania pytań 🙂