Jak efektivně stahovat obrázky přes PHP

ondrej256
Člen | 186
+
0
-

Ahoj,

Popis situace:
narazil jsem na problém se stahováním obrázků. Vyvíjím interní systém pro firmu, kde se evidují zakázky a v detailu zakázky si člověk může zobrazit nějaké sobory. Pro zjednodušení si představme fotky technických průkazů od aut. Soubory jsou uloženy na disku na serveru.

Jenže tyhle fotky nesmí být přístupné v rámci systému všem uživatelům. Systém má několik modulů, např. Sklad a odtahovka. Odtahovka potřebuje evidovat fotky auta v jakém je stavu při převzetí, fotku technického průkazu… Takže je v pořádku aby k tomu měl odtahovkář přístup.

Jenže ve stejném systému je taky skladník, který do modulu odtahovka nemá přístup a nechci aby měl přístup ani k těm fotkám z odtahovky.

Takže v detailu odtahové zakázky zobazuju galerii, ale ten odkaz na obrázek vede na api backendu /api/image/1, na backendu si nejdřív ověřím jestli k této fotce má uživatel oprávnění a pokud ano vrátím obrázek.

V čem je problém?
V některých detailech je fotek spousta a je potřeba zobrazit celou fotodokumentaci hned v detailu při načtení. To znamená, že jde na backend 1 dotaz abych vůbec vykreslil stránku detailu, kde je spousta fotek

<img src="/api/image/1.jpg" />
<img src="/api/image/2.jpg" />
<img src="/api/image/3.jpg" />
<img src="/api/image/4.jpg" />
...
<img src="/api/image/50.jpg" />

No a při vykreslení html kódu na stránce se vyvolá dalších 50 dotazů na api pro stažení 50ti obrázků. Obrázky se načítají pomalu a zatěžuje to celý backend kvantem requestů na obrázky.

Neřešil někdo podobný problém? Moc nevím jak z toho ven. Přemýšlím zda by nebylo řešení udělat na stahování obrázků samostatnou službu (docker container) třeba v nodeJs, kde bych se připojil na databázi, dokázal bych si ověřit zda na to uživatel má či nemá oprávnění a podle toho mu obrázky poslal. Ale nevím zda to je dobrá cesta nebo ne a jestli by to vůbec bylo nebo nebylo rychlejší.

Díky za rady

Kamil Valenta
Člen | 765
+
+1
-

Kdybys tam žádné API neměl a vykresloval *.jpg přímo, udělá se zrovna tak stejný počet requestů na server. Je celkem jedno, zda uděláš requset na jpg, nebo na php, které jpg vrátí. Pokud je to pomalé a zatěžující, tak je něco špatně v tom API.

m.brecher
Generous Backer | 774
+
0
-

@KamilValenta

Je celkem jedno, zda uděláš request na jpg, nebo na php, které jpg vrátí.

Pokud je ten dotaz jeden tak ano, ale pokud se jich na jednu stránku má načíst 50 tak místo jednoho sql dotazu na 50 záznamů bude mít 50 sql dotazů n jeden obrázek, což věřím že zpomaluje. Ověřuje oprávnění uživatele ke každému obrázku – asi sql dotazem. Čili pravděpodobně zpomalují neoptimalizované sql dotazy – vlastně jak píšeš něco je špatně v API.

Editoval m.brecher (7. 4. 2023 15:30)

m.brecher
Generous Backer | 774
+
0
-

@ondrej256

Odhaduji, že Ti to zpomalují sql dotazy, kdy ověřuješ jeden každý obrázek pravděpodobně dotazem do databáze.

Zkus nějak obejít ověřování oprávnění k obrázku dotazem do databáze. Napadá mne třeba umístit obrázky do adresářů tak, aby obrázky se stejnými přístupovými právy byly pohromadě v jednom adresáři. Potom stačí ověřit v api jenom tu složku a to by se mohlo obejít bez sql dotazu.

ondrej256
Člen | 186
+
0
-

m.brecher napsal(a):

@ondrej256

Odhaduji, že Ti to zpomalují sql dotazy, kdy ověřuješ jeden každý obrázek pravděpodobně dotazem do databáze.

Zkus nějak obejít ověřování oprávnění k obrázku dotazem do databáze. Napadá mne třeba umístit obrázky do adresářů tak, aby obrázky se stejnými přístupovými právy byly pohromadě v jednom adresáři. Potom stačí ověřit v api jenom tu složku a to by se mohlo obejít bez sql dotazu.

To je chytrá myšlenka :-D Zkusím nad tím popřemýšlet, v některých případech to půjde určitě použít :-)

ondrej256
Člen | 186
+
0
-

m.brecher napsal(a):

@KamilValenta

Je celkem jedno, zda uděláš request na jpg, nebo na php, které jpg vrátí.

Pokud je ten dotaz jeden tak ano, ale pokud se jich na jednu stránku má načíst 50 tak místo jednoho sql dotazu na 50 záznamů bude mít 50 sql dotazů n jeden obrázek, což věřím že zpomaluje. Ověřuje oprávnění uživatele ke každému obrázku – asi sql dotazem. Čili pravděpodobně zpomalují neoptimalizované sql dotazy – vlastně jak píšeš něco je špatně v API.

Já to zkoušel i zjednodušit tak, že jsem jen vytáhl záznam obrázku z databáze, kde to zkontrolovalo zda je public, což jsem nastavil schválně všem a vrátilo to obrázek. Jenže ono mě příjde, že PHP jakoby nedokáže ten obrázek poslat tak rychle a obrázek se tak trochu načítá jako by byly 90tá léta a načítal se obrázek pomalu zeshora dolů.

Původně jsem to měl bez validace a obrázky se dotahovaly hned z disku přes nginx a to bylo super.

Na jiném projektu mám taky zkušenost, že se to dotahovalo přímo z amazonu, ale jakmile jsme přidali že to šlo přes ten projekt napsaný v PHP a kontrolovala se tam validace tak se to načítalo výrazně pomaleji.

Takže z toho mám pocit že PHP zkrátka neumí ten obrázek vzít z disku a poslat dostatečně rychle. Rychlostí disku to nebude, když to předtím přes nginx šlo normálně.

Dívám se a moje průměrná doba vrácení obrázku je 700ms a průměrná velikost 25kB

Editoval ondrej256 (7. 4. 2023 17:05)

Marek Bartoš
Nette Blogger | 1184
+
+5
-

Odesílat přes PHP je naprosto v cajku. Máš tam ale 2 problémy. Běžící PHP proces navíc a sessions.

Sessions jsou blokující – dokud ji proces používá, tak další proces který ji chce použít čeká.

  • Nejjednodušší řešení je session zavřít hned, jak ji přestaneš potřebovat. Další request pak nemusí čekat na dokončení předchozího.
  • Též můžeš změnit driver session na neblokující. To se hodí i pro paralelní ajax requesty, ale musíš počítat s tím, že si requesty mohou vzájemně přepisovat data.

Pokud spouštíš 50 PHP procesů navíc, tak to samozřejmě znamená zátěž navíc. Řešením jsou speciální HTTP hlavičky, které Apache, Nginx a nejspíš i další podporují. Nastavíš hlavičku se skutečnou cestou k souboru a odešleš request. PHP se ukončí a server ví, že má místo response z PHP použít soubor uvedený v hlavičce a už jej odešle sám.

Jestliže oprávnění neřešíš obrázek od obrázku, ale pro celou skupinu naráz, tak též můžeš v requestu na stránku zjistit, zda uživatel má oprávnění, uložit si tu informaci do session (např. na následující minutu) a v requestech na obrázky už číst informaci odtud.
Spíš než na složku bych si však k obrázkům ukládal identifikátor označující skupinu, do které obrázek patří. Nebudeš tak vázaný na strukturu souborů a můžeš ji kdykoli změnit – například kdybys řešil tisíce souborů nahrávaných za krátký čas a balancing inodů.

Editoval Marek Bartoš (7. 4. 2023 17:05)

ondrej256
Člen | 186
+
0
-

Marek Bartoš napsal(a):

Odesílat přes PHP je naprosto v cajku. Máš tam ale 2 problémy. Běžící PHP proces navíc a sessions.

Sessions jsou blokující – dokud ji proces používá, tak další proces který ji chce použít čeká.

  • Nejjednodušší řešení je session zavřít hned, jak ji přestaneš potřebovat. Další request pak nemusí čekat na dokončení předchozího.
  • Též můžeš změnit driver session na neblokující. To se hodí i pro paralelní ajax requesty, ale musíš počítat s tím, že si requesty mohou vzájemně přepisovat data.

Pokud spouštíš 50 PHP procesů navíc, tak to samozřejmě znamená zátěž navíc. Řešením jsou speciální HTTP hlavičky, které Apache, Nginx a nejspíš i další podporují. Nastavíš hlavičku se skutečnou cestou k souboru a odešleš request. PHP se ukončí a server ví, že má místo response z PHP použít soubor uvedený v hlavičce a už jej odešle sám.

Jestliže oprávnění neřešíš obrázek od obrázku, ale pro celou skupinu naráz, tak též můžeš v requestu na stránku zjistit, zda uživatel má oprávnění, uložit si tu informaci do session (např. na následující minutu) a v requestech na obrázky už číst informaci odtud.
Spíš než na složku bych si však k obrázkům ukládal identifikátor označující skupinu, do které obrázek patří. Nebudeš tak vázaný na strukturu souborů a můžeš ji kdykoli změnit – například kdybys řešil tisíce souborů nahrávaných za krátký čas a balancing inodů.

Dík za tip. Vyzkouším

mystik
Člen | 296
+
+5
-

My to resime tak ze URL na obrazek geneujeme vcetne docasneho klice pro pristup. Link vede na jednoucelovy php script, ktery jen zkontroluje zda je klic ok a odesle obrazek. Tim se vyhneme startovani session, praci s DB nebo loadovani cele aplikace.

Proste pri generovani stranky vygeneruju neco jako /image/randomkey/image.jpg. To rewritem smeruju na script image.php. Ten jen zkontroluje, ze randomkey je platny klic pro image.jpg a obrazek odesle.

Klic muzes delat per obrazek pokud nepotrebujes hlidat zruseni pristupu v pripadw ze si nekdo odkaz ulozi. Nebo mu dat omezenou platnost tim ze do nej zakomponujes treba aktualni datum. Tam jen pozor na hranici casu abys treba chvili po pulnoci bral i odkazy vygenerovane chvili pred pulnoci.

ondrej256
Člen | 186
+
0
-

Marek Bartoš napsal(a):

Odesílat přes PHP je naprosto v cajku. Máš tam ale 2 problémy. Běžící PHP proces navíc a sessions.

Sessions jsou blokující – dokud ji proces používá, tak další proces který ji chce použít čeká.

  • Nejjednodušší řešení je session zavřít hned, jak ji přestaneš potřebovat. Další request pak nemusí čekat na dokončení předchozího.
  • Též můžeš změnit driver session na neblokující. To se hodí i pro paralelní ajax requesty, ale musíš počítat s tím, že si requesty mohou vzájemně přepisovat data.

Pokud spouštíš 50 PHP procesů navíc, tak to samozřejmě znamená zátěž navíc. Řešením jsou speciální HTTP hlavičky, které Apache, Nginx a nejspíš i další podporují. Nastavíš hlavičku se skutečnou cestou k souboru a odešleš request. PHP se ukončí a server ví, že má místo response z PHP použít soubor uvedený v hlavičce a už jej odešle sám.

Jestliže oprávnění neřešíš obrázek od obrázku, ale pro celou skupinu naráz, tak též můžeš v requestu na stránku zjistit, zda uživatel má oprávnění, uložit si tu informaci do session (např. na následující minutu) a v requestech na obrázky už číst informaci odtud.
Spíš než na složku bych si však k obrázkům ukládal identifikátor označující skupinu, do které obrázek patří. Nebudeš tak vázaný na strukturu souborů a můžeš ji kdykoli změnit – například kdybys řešil tisíce souborů nahrávaných za krátký čas a balancing inodů.

Nasadil jsem to, řekl bych že to teda nepomohlo, ale ono asi zase záleží jaký je na webu provoz. S vyšším provozem to nejspíš půjde poznat víc. Je nějaký způsob jak si ověřím, že to opravdu funguje? Díval jsem se jeslti nedostanu nějakou hlavičku v Response headers, ale nic nového tam není. To asi už nginx neposílá.

mystik
Člen | 296
+
+1
-

Rozdil v rychlosti bude zanedbatelny. My zkouseli merot rozdil mezi souborem posilanym PHP a primym pristupem a pro soubory pod 10 MB byl nemeritelny. Co te zdrzuje u PHP je obvykle loading aplikace a cekani na session (to je hlavni zabijak protoze requesty musi jit seriove). Pokud pouzijes maly jednoucelovy script tak se rychlost od primeho pristipu nebo sendfile bude lisit minimalne. Bude to mit o neco mensi anroky na pamet a pocet bezicich procesu, ale pokud tam nemas hodne slaby HW a radove stovky requestu za sekundu tak to resit nemusis.

Jo a zkontroluj si ze mas v nginxu zapnute http2. To se na pokusech o stahovani vice obrazku naraz taky hodne projevi.

mystik
Člen | 296
+
+4
-

Jeste me napada. Pokud ten soubor budes posilat v PHP tak nedelej echo file_get_contents() nebo neco podobneho co vyzaduje aby se soubor nejdriv cely nacetl do pameti nez se zacne posilat. Pouzij funkci readfile a nezapomen pred tim vypnout output buffering pokud ho pouzivate.

Foowie
Člen | 269
+
0
-

Není to doporučené řešení, ale pokud jsou obrázky hodně malé, tak bych otestoval i vkládat obrázky přímo do HTML jako base64.

src="data:image/jpeg;base64...."

Má to hodně nevýhod: zpomalí to načtení stránky, base64 je ~35% větší než binary, …