Skip to content
← Wszystkie wpisy
5 min czytania Dawid Skłodowski

Potoki obrazów dla dużych katalogów produktów

Katalog produktów to głównie obrazy, a naiwna obsługa obrazów go zatopi. Deep-dive o budowie skalującego się potoku obrazów — trzymanie oryginałów, generowanie wariantów w tle, offload na CDN i nietopienie serwerów webowych.

Katalog produktów jest wizualnie głównie obrazami — a obrazy są tam, gdzie naiwna aplikacja e-commerce po cichu się przewraca. Każdy produkt chce miniatury w siatce, średniego obrazu na listingu, dużego na stronie szczegółów, wersji zoom i kwadratowego kadru do koszyka. Pomnóż to przez dziesiątki tysięcy produktów i kilka uploadów dziennie, a „po prostu przeskaluj obraz” staje się problemem infrastrukturalnym, który może stopić twoje serwery webowe i spowolnić każdą stronę. Oto jak zbudować potok obrazów, który się skaluje, na podstawie prowadzenia katalogów z poważnym wolumenem obrazów.

Reguła kardynalna: trzymaj oryginał, wyprowadzaj resztę

Pierwszą zasadą jest traktowanie wgranego pliku jako mastera, którego nigdy nie modyfikujesz, i generowanie każdego wyświetlanego rozmiaru jako pochodnej z niego. Przechowuj oryginał w pełnej jakości, nietknięty, i produkuj miniatury, średnie, duże i kadry z tego źródła.

Dlaczego to tak ważne: wymagania się zmieniają. Dziś pokazujesz miniaturę 300px; w następnym kwartale projektant chce 360px i nowego kwadratowego kadru do przeprojektowanego koszyka. Jeśli wyrzuciłeś oryginał i trzymałeś tylko rozmiary potrzebne w czasie uploadu, utknąłeś — nie podniesiesz rozdzielczości obrazu 300px do 360px bez tego, by wyglądał okropnie. Trzymaj mastera, a przegenerowanie całego katalogu w nowym rozmiarze to zadanie w tle, które możesz uruchomić kiedykolwiek. Wyrzuć go, a przeprojektowanie oznacza ponowne wgranie dziesiątek tysięcy zdjęć produktów.

# CarrierWave: oryginał jest trzymany; wersje są z niego wyprowadzane
class ProductImageUploader < CarrierWave::Uploader::Base
  storage :fog               # przechowuj na S3, nie na serwerze aplikacji

  version :thumb  { process resize_to_fill: [300, 300] }
  version :medium { process resize_to_fit:  [800, 800] }
  version :large  { process resize_to_fit:  [1600, 1600] }
end

(W 2016 częstymi narzędziami są CarrierWave albo Paperclip nad ImageMagick; zasady przeżywają to, które wybierzesz.)

Generuj warianty w tle, nigdy w żądaniu

Druga zasada i ta, która ratuje twoje serwery: przetwarzanie obrazów nie należy do żądania webowego. Przeskalowanie dużego zdjęcia na pięć wariantów ImageMagickiem jest ciężkie dla CPU i pamięci i zajmuje sekundy. Zrób to inline, gdy produkt jest zapisywany, a wiążesz workera webowego na sekundy per upload, a masowy import tysiąca produktów położy całą witrynę, gdy workery zgłodnieją.

Wepchnij to do zadania w tle. Upload przechowuje oryginał natychmiast i kolejkuje generowanie wariantów; worker wykonuje ciężką pracę poza ścieżką żądania:

class Product < ApplicationRecord
  after_commit :enqueue_image_processing, on: [:create, :update], if: :image_changed?

  def enqueue_image_processing
    GenerateImageVariantsJob.perform_later(id)
  end
end

To pozwala też skalować przetwarzanie obrazów niezależnie — uruchom więcej workerów (albo mocniejszych) do związanego z CPU przeskalowywania bez dotykania warstwy webowej. Strona, która wgrywa produkt, zwraca natychmiast; warianty pojawiają się chwilę później. Masowy import staje się zalewem zadań w tle, które drenują w tempie workerów, zamiast atakiem denial-of-service na własną witrynę.

Serwuj z object storage i CDN, nie z aplikacji

Trzecia zasada: twoje serwery aplikacji nigdy nie powinny serwować bajtów obrazów. Dwa ruchy:

  • Przechowuj pliki w object storage (S3), nie na dysku serwera aplikacji. Dysk serwera aplikacji nie skaluje się, nie przeżywa redeployu na efemerycznej infrastrukturze i nie jest współdzielony między wieloma serwerami. Object storage jest trwały, efektywnie nieskończony i dostępny z każdego workera i węzła webowego — ta sama lekcja „kontener jest jednorazowy, dane są trwałe”, zastosowana do uploadów.
  • Postaw CDN z przodu. Obrazy produktów są statyczne, identyczne dla każdego odwiedzającego i odpytywane nieustannie — podręcznikowy przypadek dla CDN. Serwowanie ich z cache’y na krawędzi blisko użytkownika czyni strony dramatycznie szybszymi i zdejmuje całe to obciążenie z twojego origin. Twoja aplikacja generuje i przechowuje wariant raz; CDN serwuje go milion razy.

Cache-bustuj, włączając hash lub wersję w nazwę pliku, by zmieniony obraz dostał nowy URL, a CDN nie serwował przestarzałego.

Generowanie wariantów eager vs lazy

Prawdziwa decyzja projektowa: generujesz każdy wariant z góry (eager), czy na pierwsze żądanie (lazy)? Oba mają miejsce:

  • Eager (generuj wszystkie rozmiary, gdy obraz jest wgrywany, przez zadanie w tle) daje przewidywalne, natychmiastowe serwowanie — każdy wariant istnieje, zanim ktoś o niego poprosi. Kosztem jest miejsce na rozmiary, które mogą być rzadko oglądane, i duże zadanie regeneracji, gdy dodasz rozmiar.
  • Lazy / on-the-fly (generuj wariant przy pierwszym żądaniu tego rozmiaru, potem cache’uj) oszczędza miejsce i adaptuje się natychmiast do nowych rozmiarów, ale pierwsze żądanie każdego rozmiaru płaci koszt przetwarzania, i musisz strzec się nadużyć (ktoś żądający tysięcy dowolnych wymiarów, by zatłuc twój procesor). Usługi obrazów na żądanie (albo resizer w stylu imgproxy za CDN) implementują ten wzorzec.

Dla katalogu ze znanym, ustalonym zbiorem rozmiarów eager generation w tle jest zwykle najprostsze i najbardziej przewidywalne. Dla systemów z wieloma lub nieprzewidywalnymi rozmiarami wygrywa lazy-z-cache. Używaliśmy obu; czynnikiem decydującym jest to, czy twój zbiór rozmiarów jest mały i stabilny.

Optymalizuj bajty, nie tylko wymiary

Przeskalowanie to tylko połowa roboty; plik musi być też lekki. Poprawnie zwymiarowany obraz, który ma 800KB, wciąż czyni stronę wolną. Usuń metadane, wybierz rozsądną jakość kompresji (jakość JPEG około 80 jest zwykle nieodróżnialna od 100 za ułamek rozmiaru) i serwuj odpowiednio do kontekstu. Progresywne JPEG renderują się percepcyjnie szybciej. Te optymalizacje, zastosowane w tym samym zadaniu w tle, które przeskalowuje, są często większą wygraną szybkości w realnym świecie niż samo przeskalowanie.

Werdykt

Obsługa obrazów to część budowy e-commerce, która wygląda trywialnie i po cichu staje się problemem infrastrukturalnym przy skali katalogu. Potok, który się trzyma, opiera się na czterech zasadach: trzymaj nietknięty oryginał i wyprowadzaj z niego każdy rozmiar, by przeprojektowanie było zadaniem w tle, a nie ponownym wgraniem; generuj warianty w workerach w tle, nigdy w żądaniu webowym, by masowy import nie położył witryny; przechowuj w object storage i serwuj przez CDN, by twoja aplikacja nigdy nie wysyłała bajtów obrazów; i optymalizuj rozmiar pliku, nie tylko wymiary. Wbuduj to od początku, a katalog stu tysięcy produktów serwuje obrazy szybko i tanio. Doczep obsługę obrazów naiwnie, a stanie się tym, co budzi cię, gdy nadejdzie następny duży import.