Jako programiści Ruby on Rails często stajemy przed potrzebą wgrywania i przechowywania obrazów dostarczanych przez użytkowników.
Często obrazy te trzeba przeskalować i zapisać w wielu wersjach: na mobile, na web, jako miniatury, obrazy OG oraz inne niestandardowe formaty potrzebne klientowi.
Do jakiego problemu to prowadzi? Przetwarzanie obrazów to operacja obciążająca obliczeniowo i opóźnienia nieuchronnie wzrosną.

Rozwiązanie? Sidekiq + Shrine!
Więc… co można z tym zrobić? Jeśli jesteś taki jak ja, chcesz zjeść ciastko i mieć ciastko.
W tym wpisie przejdziemy przez to, jak uzyskać szybkie czasy odpowiedzi, generując jednocześnie wiele wersji obrazu.
Rozwiązanie jest proste. Użyjemy Sidekiqa, by zaplanować zadanie przetwarzania obrazu i natychmiast zwrócić odpowiedź ze statusem 200.
Do wgrywania obrazów wykorzystamy Shrine. To nowoczesna biblioteka o wtyczkowej budowie, alternatywa dla gemów takich jak Carrierwave czy Paperclip. Bardzo ją lubię i polecam używać jej w projektach - ma naprawdę świetną dokumentację, czego nie sposób przecenić.
Jak to działa w praktyce?
Przewodnik krok po kroku
Całą aplikację zobaczysz na moim Githubie.
rails new shrine-uploader-api --api --database=postgresql && cd shrine-uploader-api && rails db:create
Dodaj do swojego Gemfile'a:
I uruchom:
bundle
Skorzystajmy z generatora zasobów z Rails CLI. Stworzymy tabelę uploads z kolumną image_data, niezbędną do działania Shrine:
rails g resource Upload image_data:jsonb && rails db:migrate
Najpierw musimy skonfigurować Shrine.
Zadeklarujemy 2 rodzaje magazynów:
- „cache” na surowe obrazy
- „store” na obrazy przetworzone.
W tej sekcji zadeklarujemy też wtyczki do integracji z ActiveRecord, generowania wariantów (derivatives) i przetwarzania w tle. Ostatnia część rejestruje callback wywoływany przy „promote event”. Zdarzenie promote uruchamia się po zapisaniu naszego rekordu „Upload” w bazie danych.
Następnie dodajmy uploader Shrine ze specyfikacją wariantów, które chcemy generować. Przetwarzaniem obrazów zajmuje się pod spodem imagemagick:
Podłączmy uploader do naszego modelu. Do obrazów można teraz sięgać przez wirtualne pole image:
Musimy zaimplementować PromoteJob, którego użyliśmy w naszym inicjalizatorze. Zadanie to trafi do Redisa przy zdarzeniu promocji i zostanie później wykonane w Sidekiqu. Oto jego kod:
Na koniec zostaje zaimplementowanie „create action” w UploadsControllerze.
I gotowe! Kod mamy z głowy!
Uruchommy teraz serwer Redis, którego Sidekiq używa do zapisywania i pobierania zadań. Ja użyję oficjalnego obrazu dockerowego, ale możesz uruchomić Redisa ze swojego systemu, jeśli wolisz :)
docker run --name my-redis -d --publish 6379:6379 redis
I uruchom sidekiq:
bundle exec sidekiq
Uruchom serwer Rails w drugiej karcie terminala:
rails s
I gotowe! Nasza aplikacja działa :)
Mamy endpoint http://localhost:3000/uploads, na który możemy wysyłać obrazy - zostaną przetworzone zgodnie z naszą specyfikacją.
A teraz to przetestujmy!
Najpierw potrzebujemy obrazu do wgrania. Może być dowolny. W moim przypadku to plik, który nazwałem example.png
curl -X POST -i -F [email protected] localhost:3000/uploads
Celem jest uzyskanie odpowiedzi 200, a następnie czterech obrazów w folderze public/uploads: oryginału i 3 kopii przeskalowanych zgodnie z regułami z ImageUplaoder.
W logach Sidekiqa zobaczysz też, że PromoteJob został zaplanowany i pomyślnie wykonany.
Sprawdźmy teraz, jak wygląda kwestia wydajności.
Najpierw mała sztuczka, by uzyskać czas odpowiedzi z curla. Stworzę plik curl-format.txt o poniższej zawartości i użyję go później jako argumentu.
time_total: %{time_total}sNasz obraz wyślemy w postaci binarnej, używając flagi -F [email protected].
15 ms, nieźle ;)
Przeprowadziłem kilka testów bez Sidekiqa do asynchronicznego przetwarzania obrazów - zajmowały około 120 ms. To 8 razy wolniej niż z przetwarzaniem w tle!
Podsumowanie
Bądźmy szczerzy. Nie każda aplikacja w Rails potrzebuje przetwarzania obrazów w tle.
Są jednak sytuacje, gdy to naprawdę dobry pomysł. Wyobraź sobie budowę API, do którego co sekundę trafia wiele obrazów. Albo załóżmy, że robisz bardzo złożone transformacje obrazów i trwa to znacznie dłużej niż w naszym przykładzie.
W takich przypadkach opóźnienia mogą stać się odczuwalne, a ta prosta sztuczka, którą opisałem powyżej, z pewnością pomoże Ci utrzymać świetną wydajność aplikacji.
Miłego kodowania!