Powrót do bloga

Algorytmy wyboru bloków server i location w Nginx: Przegląd

Algorytmy wyboru bloków server i location w Nginx: Przegląd

Wprowadzenie

Nginx jest jedną z najpopularniejszych opcji serwerów WWW na świecie. Jest w stanie z powodzeniem obsługiwać wiele jednoczesnych połączeń klienckich. Jednocześnie funkcjonuje jako serwer pocztowy, WWW lub reverse proxy.

Niniejszy przewodnik ma na celu przedstawienie kulisów metod, które kierują sposobem przetwarzania żądań klientów przez Nginx. Odczarujemy projektowanie bloków server i location blok, a także wyjaśnimy, jak najlepiej ograniczyć pozorną nieprzewidywalność obsługi żądań.

Po pierwsze, oto kompleksowy samouczek dotyczący tego, jak zainstalować Nginx na serwerze Ubuntu. A teraz zaczynajmy!

Konfiguracja bloków w Nginx

Logiczne podejście Nginx polega na sortowaniu konfiguracji przeznaczonych do różnych celów w oddzielne, bardziej logiczne bloki zawartości. Będą one umieszczone w strukturze hierarchicznej. Gdy klient generuje żądanie, Nginx inicjuje proces, w którym określa, które z tych bloków konfiguracji są najbardziej odpowiednie do obsłużenia tego żądania. Skupimy się na tym procesie decyzyjnym.

Głównymi blokami, które omówimy, będą bloki server oraz bloki location. Bloki server są podzbiorem konfiguracji tworzonych przez Nginx, które definiują, który serwer wirtualny będzie odpowiedzialny za obsługę określonego typu żądania. Najczęściej opierają się one na adresie IP, nazwie domeny lub porcie przychodzącego żądania. Administratorzy konfigurują wiele bloków server. Następnie muszą zdecydować, które z połączeń powinno obsługiwać żądanie.

Bloki location znajdują się wewnątrz bloków server. To one decydują o tym, jak i jakie zasoby można wykorzystać do obsługi żądań przychodzących do ich konkretnego serwera nadrzędnego. Model ten jest wysoce elastyczny. Przestrzeń URI może być skonfigurowana do korzystania z tych bloków w dowolny sposób, jaki administrator uzna za najlepszy.

Decydowanie, który blok obsłuży które żądanie w Nginx

Nginx pozwala na zdefiniowanie wielu bloków server. Wszystkie one funkcjonują jako różne wirtualne serwery WWW. W związku z tym musi istnieć metoda, która określa, który serwer obsłuży konkretne żądania przychodzące. Odbywa się to poprzez znalezienie najlepszego dopasowania dla wydajności żądania za pomocą systemu zdefiniowanych kontroli.

Nginx zajmuje się głównie dwiema głównymi dyrektywami bloku serwera: listen oraz server_name.

Znajdowanie możliwych dopasowań za pomocą dyrektywy „Listen”

Pierwszą rzeczą, którą ocenia Nginx, jest port i adres IP żądania. Następnie dopasowuje je do dyrektywy listen każdego serwera. To parsowanie listy serwerów pomaga wyizolować tylko te bloki server, które mogą obsłużyć dane żądanie.

Zazwyczaj dyrektywa listen definiuje port i adres IP, na które dany blok serwera będzie odpowiedzialny za odpowiadanie. Blok serwera, który nie zawiera dyrektywy listen, domyślnie otrzymuje parametry nasłuchiwania 0.0.0.0:80. Jeśli Nginx jest uruchamiany przez zwykłego użytkownika bez uprawnień roota, parametr listen jest definiowany jako 0.0.0.0:8080. Oznacza to, że niezależnie od interfejsu, jeśli bloki pochodzą z portu 80, bloki zdefiniowane w ten sposób będą na nie odpowiadać. Jednak ta domyślna wartość nie ma dużego znaczenia w procesie wyboru serwera.

Możesz skonfigurować dyrektywę listen na:

  • Pojedynczy adres IP, który nasłukuje żądań na domyślnym porcie (80).
  • Pojedynczy port, który nasłukuje na dowolnym interfejsie na tym porcie.
  • Kombinację portu i adresu IP.
  • Określoną ścieżkę gniazda Unix (ta opcja ma znaczenie tylko wtedy, gdy żądania przechodzą między różnymi serwerami).

Nginx wdroży zestaw reguł przy podejmowaniu decyzji, do którego bloku serwera zostanie wysłane żądanie. Reguły te zależą od konkretnej konfiguracji dyrektywy listen. Są one następujące:

  • Jeśli dyrektywa listen jest niekompletna, brakujące elementy otrzymują swoje wartości domyślne. Oznacza to, że adres IP i port zostaną uzupełnione wartościami domyślnymi w celu przetworzenia żądania.
    • W takim przypadku blok niezawierający dyrektywy listen użyje domyślnej wartości 0.0.0.0:80.
    • Blok, w którym brakuje portu, a który ma adres IP 111.111.111.111, stanie się 111.111.111.111:80.
    • Gdy nie ma adresu IP, blok z portem 8888 otrzyma domyślny adres IP, który zostanie dołączony, tworząc 0.0.0.0:8888.
  • Mając określony adres IP i port, Nginx będzie następnie szukać bloków serwera oferowanych jako dopasowanie do tego portu.
  • Jeśli znajdzie tylko jedno konkretne dopasowanie, będzie to ten blok serwera. Jeśli kwalifikuje się wiele bloków, Nginx przejdzie do dyrektywy server_name, aby dokładniej określić właściwy blok serwera.

Nginx oceni dyrektywę server_name tylko wtedy, gdy nie znajdzie bloku serwera o dokładnym poziomie szczegółowości z dyrektywy listen. Jeśli example.com znajduje się na porcie 80 z adresem IP 192.168.1.10, pierwszy blok w tym przykładzie zawsze będzie tym, który obsłuży żądanie. Dzieje się tak niezależnie od tego, co mówi dyrektywa server_name:

Nginx Server

Jeśli istnieje więcej niż jeden kwalifikujący się blok usług o pasującym poziomie szczegółowości, wówczas pod uwagę zostanie wzięta dyrektywa server_name.

Znajdowanie możliwych dopasowań za pomocą dyrektywy „Server_Name”

Jeśli dyrektywy listen są równie szczegółowe, Nginx sprawdzi nagłówek ’Host’ żądania. Jest to wartość, która początkowo zawiera IP domeny, do której klient próbował dotrzeć. Nginx użyje dyrektywy server_name wewnątrz każdego z wciąż kwalifikujących się kandydatów na blok serwera. Wykonuje te oceny na podstawie wzoru. Wygląda on następująco:

  • Pierwszą próbą podjętą przez Nginx będzie zidentyfikowanie bloku z dyrektywą server_name dokładnie pasującą do wartości nagłówka „Host” w żądaniu. Jeśli go znajdzie, blok zawierający dokładne dopasowanie będzie tym, który obsłuży żądanie. W przypadku znalezienia wielu bloków, wybierze pierwszy z listy.
  • Jeśli nie ma dokładnych dopasowań, Nginx spróbuje użyć server_name do znalezienia bloku serwera, który pasuje przy użyciu *, czyli symbolu wieloznacznego na początku nazwy bloku serwera w konfiguracji. Znalezienie bloku tą metodą oznacza, że blok serwera został określony. Jeśli znajdzie więcej niż jedno dopasowanie, żądanie zostanie obsłużone przez najdłuższe dopasowanie.
  • Bez pasującego symbolu wieloznacznego, Nginx spróbuje znaleźć blok serwera z pasującym końcowym symbolem wieloznacznym. Innymi słowy, będzie to nazwa serwera z * w konfiguracji. Jeśli taki zostanie znaleziony, zostanie użyty do obsługi żądania. Natomiast w przypadku znalezienia wielu takich bloków, Nginx ponownie użyje najdłuższego dopasowania.
  • W przypadku, gdy po obu próbach z symbolami wieloznacznymi nadal nie ma dopasowań, Nginx oceni te bloki serwera, które definiują server_name za pomocą wyrażeń regularnych (oznaczonych znakiem ~ przed nazwą). Pierwsze wystąpienie server_name z wyrażeniem pasującym do nagłówka „Host” zostanie uznane za blok serwera do obsługi żądania.
  • Jeśli w tym momencie nadal nie ma dopasowań, Nginx użyje domyślnego bloku serwera dla tej kombinacji portu i adresu IP.

Każda kombinacja portu/adresu IP będzie miała przypisany blok serwera. Zostanie on użyty, jeśli reguły określania odpowiedniego bloku serwera do obsługi żądania nie przyniosą rezultatu. Będzie to pierwszy blok w konfiguracji zawierający opcję default_server w dyrektywie listen (nadpisałby on początkowo znaleziony algorytm). Każda kombinacja adresu IP/portu może mieć co najwyżej jedno ustawienie default_server.

Przykłady wyboru bloku serwera

Jeśli zdefiniowana nazwa server_name dokładnie odpowiada wartości nagłówka „Host”, to ten blok serwera zostanie wybrany do przetwarzania żądania. Poniższy przykład pokazuje nagłówek „Host” żądania oznaczony jako „host1.example.com”. W tym przypadku wybrany zostanie drugi serwer:

Nginx Server

Bez dokładnego dopasowania Nginx sprawdzi, czy istnieje server_name z symbolem wieloznacznym. Jeśli nie, wybrane zostanie najdłuższe dopasowanie zaczynające się od symbolu wieloznacznego. W poniższym przykładzie, „www.example.org” znajduje się w nagłówku „Host”. Oznacza to, że wybierze drugi blok:

server screenshot

Bez dopasowania zaczynającego się od wieloznacznika, Nginx przechodzi do sprawdzania wieloznacznika końcowego. Do obsłużenia żądania zostanie wybrane najdłuższe dopasowanie kończące się wieloznacznikiem. W tym przypadku nagłówek „Host” to „www.example.com”, więc wybierze trzeci blok serwera:

Nginx Server

Jeśli nadal nie ma dopasowań, Nginx spróbuje dopasować dyrektywy server_name za pomocą wyrażeń regularnych. Pierwsze z tych wyrażeń zostanie wybrane do obsłużenia żądania. Jeśli „Host” to „www.example.com,” wybór padnie na drugi blok serwera, który obsłuży żądanie:

Nginx Server

W przypadku braku jakichkolwiek dopasowań, żądanie zostanie skierowane do kombinacji adresu IP i portu ze skonfigurowanym pasującym serwerem domyślnym.

Dopasowywanie bloków location

Nginx musi również określić algorytm, na podstawie którego zdecyduje, który blok location na serwerze będzie odpowiedzialny za odpowiedź na żądanie.

Składnia bloków location

Zanim wyjaśnimy, jak Nginx decyduje o wyznaczeniu bloku location do obsługi żądań, przeanalizujemy składnię definicji bloków location. Jak wspomniano wcześniej, bloki location znajdują się w blokach serwera (oraz innych blokach location). Ich celem jest podejmowanie decyzji o sposobie przetwarzania identyfikatora URI żądania. URI to część żądania, która następuje po adresie IP i porcie lub nazwie domeny w żądaniu.

Bloki location zazwyczaj wyglądają tak:

Syntax for Location Blocks

Nginx porówna URI żądania z wartością location_match. Obecność lub brak powyższego modyfikatora określi sposób, w jaki Nginx spróbuje dopasować bloki. W zależności od modyfikatora, bloki location będą interpretowane zgodnie z następującymi regułami:

  • Brak modyfikatorów: Bez żadnych modyfikatorów lokalizacja będzie interpretowana jako dopasowanie prefiksu. Oznacza to, że podana lokalizacja będzie dopasowywana do początku URI w żądaniu w celu ustalenia poprawnego dopasowania.
  • =: Znak równości oznacza, że ten blok zostanie uznany za dopasowany tylko wtedy, gdy URI żądania dokładnie odpowiada podanej lokalizacji.
  • ~: Modyfikator tyldy oznacza, że dopasowanie bloku location będzie uwzględniać wielkość liter.
  • ~*: Połączenie tyldy i gwiazdki oznacza, że blok location nie będzie uwzględniać wielkości liter podczas szukania dopasowania.
  • ^~: Jeśli modyfikator tyldy jest poprzedzony daszkiem, dopasowywanie wyrażeń regularnych nie nastąpi, o ile ten blok zostanie wybrany jako najlepsze dopasowanie bez wyrażeń regularnych.

Przykłady składni bloków location

Aby przedstawić przykład dopasowania prefiksu, blok location zostanie wybrany do obsługi żądania URI w postaci /site, /site/page1/index.html lub /site/index/html:

location site

Na potrzeby tej demonstracji wymaganego dopasowania URI, blok ten będzie zawsze używany do odpowiadania na żądania URI w postaci /page1, a nie /page1/index.html. Jeśli jest to wybrany blok i spełnia on żądanie przy użyciu strony indeksowej, rzeczywisty moduł obsługi żądania zostanie przekierowany wewnętrznie do innej lokalizacji:

Nginx Server

Na przykład w przypadku lokalizacji, która musi być interpretowana z uwzględnieniem wielkości liter, poniższy blok nie obsłuży żądań dla /FLOWER.PNG. Obsłuży jednak żądania dla /tortoise.jpg:

Nginx Server and Location Block Selection Algorithms: Overview

Następnie przyjrzyj się blokowi, który pozwala na dopasowanie bez uwzględniania wielkości liter, podobnemu do powyższego. W tym przypadku blok może obsłużyć zarówno //tortoise.jpg oraz /FLOWER.PNG:

location

Ostatnim wariantem jest blok, który zapobiega dopasowywaniu wyrażeń regularnych, jeśli zostanie ustalony jako optymalne dopasowanie bez wyrażeń regularnych. Ten blok może obsługiwać żądania dla /costumes/ninja.html:

Nginx Server and Location Block Selection Algorithms: Overview

Mówiąc ściślej, modyfikatory określają sposób wyznaczania bloków location. Nie mówi nam to jednak, jakiego algorytmu decyzyjnego używa Nginx do zidentyfikowania bloku location, do którego ma zostać wysłane żądanie. Zajmijmy się tym w następnej kolejności.

Wybór lokalizacji, która obsłuży żądania w Nginx

Metoda, za pomocą której Nginx wybiera lokalizację przetwarzającą żądanie, jest podobna do sposobu wyboru bloków serwera. Innymi słowy, określa on optymalną lokalizację dla każdego żądania, przechodząc przez określony proces. Aby skonfigurować Nginx dokładnie i odpowiednio, konieczne jest zrozumienie tego procesu.

Mając na uwadze omówione wcześniej deklaracje lokalizacji, Nginx podobnie korzysta z potencjalnych kontekstów lokalizacji, sprawdzając kwalifikowalność każdej lokalizacji poprzez porównanie z nią identyfikatora URI z danego żądania. Stosuje przy tym następujący algorytm:

  • Najpierw Nginx sprawdza wszystkie typy lokalizacji, które nie zawierają wyrażenia regularnego. Robi to, szukając wszystkich dopasowań prefiksów opartych na lokalizacji. W tym celu sprawdza lokalizację pod kątem pełnego URI żądania.
  • Nginx rozpoczyna od szukania dokładnego dopasowania. Po zidentyfikowaniu bloku lokalizacji używającego modyfikatora = jest on porównywany z żądaniem URI. Jeśli oba pasują do siebie dokładnie, blok lokalizacji jest wybierany do obsługi żądania natychmiast na miejscu.
  • Jeśli nie ma lokalizacji dokładnie pasujących do porównania z modyfikatorem =, Nginx przechodzi do oceny prefiksów, które nie są dokładne. Po ustaleniu lokalizacji o najdłuższym prefiksie pasującym do URI żądania, przeprowadzi następujące oceny:
    • Jeśli lokalizacja z najdłuższym dopasowaniem prefiksu używa modyfikatora ^~, lokalizacja ta zostanie natychmiast wybrana.
    • Jeśli lokalizacja z najdłuższym prefiksem nie używa modyfikatora ^~, dopasowanie jest na krótko zachowywane przez Nginx, aby umożliwić przeniesienie uwagi wyszukiwania.
  • Po znalezieniu i zapisaniu dopasowania lokalizacji o najdłuższym prefiksie, Nginx przechodzi do oceny lokalizacji z wyrażeniami regularnymi. Obejmują one dopasowania uwzględniające i nieuwzględniające wielkości liter. Jeśli lokalizacja o najdłuższym pasującym prefiksie zawiera w sobie jakiekolwiek regularne lokalizacje, Nginx przebuduje listę, aby umieścić je blisko góry listy lokalizacji. Pierwsze wyrażenie z przebudowanej listy, które pasuje do URI żądania, będzie lokalizacją wybraną do obsługi żądania.
  • Jeśli nie zostaną znalezione żadne wyrażenia regularne spełniające żądanie RI, do przetworzenia żądania zostanie wybrana lokalizacja zapisana wcześniej.

Nginx domyślnie traktuje priorytetowo dopasowania wyrażeń regularnych nad dopasowaniami z preferencyjnymi prefiksami. Najpierw jednak ocenia lokalizacje prefiksów, dzięki czemu strona administrująca może uchylić tę tendencję za pomocą modyfikatorów = i ^~.

Kolejnym ważnym wnioskiem jest to, że podczas gdy lokalizacje prefiksów są zazwyczaj oparte na najbardziej szczegółowym, najdłuższym znalezionym dopasowaniu, sprawdzanie wyrażeń regularnych jest zatrzymywane, gdy tylko zostanie zidentyfikowane pierwsze dopasowanie. Oznacza to, że pozycjonowanie w konfiguracji ma realne konsekwencje dla lokalizacji wyrażeń regularnych.

Ostatnią kwestią, o której należy wspomnieć, jest to, że dopasowania wyrażeń regularnych w obrębie dopasowania z najdłuższym prefiksem zasadniczo wejdą bez kolejki podczas oceny lokalizacji przez Nginx. Zostaną one umieszczone na górze listy i ocenione przed innymi wyrażeniami regularnymi.

Kiedy następuje przejście do innych lokalizacji podczas oceny bloków lokalizacji?

Zazwyczaj po ocenie żądania i wybraniu bloku lokalizacji do jego obsługi, zostanie ono rozpatrzone w całości w tym kontekście. Oznacza to, że tylko odziedziczone dyrektywy i wybrane lokalizacje decydują o przetwarzaniu żądania, bez udziału sąsiednich bloków lokalizacji.

Choć jest to ogólna dyrektywa pozwalająca na przewidywalne projektowanie bloków lokalizacji, czasami pewne dyrektywy wewnątrz lokalizacji mogą również wywołać nowe wyszukiwanie. Innymi słowy, reguła „tylko jednego bloku lokalizacji” ma kilka wyjątków. Wyjątki te mogą nie być zgodne z oczekiwaniami bloków lokalizacji. W związku z tym mogą one nie obsłużyć żądania w oczekiwany sposób.

Te wewnętrzne przekierowania mogą ostatecznie objawiać się z powodu niektórych dyrektyw, w tym:

  • index
  • rewrite
  • error_page
  • try_files

Jeśli użyjesz dyrektywy index, zawsze spowoduje to wewnętrzne przekierowanie podczas obsługi żądania. Chociaż dopasowanie lokalizacji zazwyczaj kończy wykonywanie algorytmu w celu przyspieszenia procesu wyboru, jeśli znalezione dopasowanie lokalizacji jest katalogiem, żądanie prawdopodobnie zostanie przekierowane do innej lokalizacji w celu formalnego przetworzenia.

Na przykład poniższa pierwsza lokalizacja pasuje do identyfikatora URI żądania /exact. Jednak w celu przetworzenia żądania dyrektywa index, którą dziedziczy blok location, przekierowuje żądanie do bloku pomocniczego:

index

W takim scenariuszu, jeśli wykonanie musi pozostać w obrębie bloku głównego, inny schemat będzie musiał przetworzyć żądanie do katalogu. Jednym ze sposobów jest ustawienie nieprawidłowego indeksu dla danego bloku i aktywowanie zamiast tego auto index:

location exact

Choć ta metoda może sprawdzić się w kilku przypadkach, w większości kontekstów nie ma ona większego zastosowania praktycznego. Dokładne dopasowanie katalogu może być przydatne w sytuacjach, gdy żądanie musi zostać przepisane. Spowoduje to uruchomienie zupełnie nowego wyszukiwania lokalizacji.

Inną dyrektywą, której można użyć do ponownej oceny przetwarzanej lokalizacji, jest dyrektywa try_files. Nakazuje ona serwerowi Nginx sprawdzenie, czy istnieje określony zestaw plików lub katalogów, przy czym ostatnim kryterium wyszukiwania jest identyfikator URI, na który Nginx ma przekierować wewnętrznie.

Rozważmy następującą konfigurację:

root var

Jeśli pojawi się żądanie dla /blahblah, pierwsza lokalizacja je odbierze. Brak pliku blahblah w katalogu /var/www/main spowoduje kolejne wyszukiwanie pliku blahblah.html. Następnie wyszuka podkatalog o nazwie blahblah w katalogu /var/www/main. Jeśli wszystkie te testy zakończą się niepowodzeniem, nastąpi przekierowanie do /fallback/index.html. Spowoduje to kolejne wyszukiwanie lokalizacji, które zostanie przejęte przez inny blok location. Następnie przetworzy plik /var/www/another/fallback/index.html.

Inną dyrektywą, która powoduje przekierowanie do innego bloku location, jest dyrektywa rewrite. Nginx wyszuka nową pasującą lokalizację na podstawie wyniku dyrektywy rewrite, gdy użyty zostanie parametr last. Jeśli zmodyfikujemy poprzedni przykład tak, aby zawierał tę dyrektywę rewrite, stanie się jasne, że żądanie może zostać przekierowane do innej lokalizacji bez wdrażania dyrektywy try_files:

root var ww main

W tym przykładzie żądanie /rewrite/hello zostanie początkowo obsłużone przez pierwszą lokalizację. Po przepisaniu go na /hello zostanie uruchomione wtórne wyszukiwanie lokalizacji. Zostanie ono dopasowane do pierwszej lokalizacji. Zostanie ono przetworzone przez dyrektywę try_file, potencjalnie powracając do /fallback/index.html, jeśli nie przyniesie żadnych wyników.

Jeśli jednak zostanie zgłoszone żądanie /rewrite/fallback/hello, zostanie znalezione dopasowanie do pierwszego bloku. W ten sposób przepisanie zostanie przetworzone ponownie, ale tym razem da wynik /fallback/hello. Żądanie zostanie przetworzone w innym bloku location.

Podobne sytuacje mają miejsce, gdy używasz dyrektywy return do wysyłania kodów stanu 301 lub 302. Jedyną różnicą jest to, że powstaje nowe żądanie, co objawia się bardzo wyraźnym przekierowaniem. Podobnie może się stać w przypadku dyrektywy rewrite, gdy zastosujesz flagi permanent lub redirect.

Inną dyrektywą, która może prowadzić do podobnych wewnętrznych przekierowań jak try_again, jest dyrektywa error_page. Możesz jej użyć, gdy napotkasz określone kody błędów podczas przetwarzania. Gdy ustawiona jest dyrektywa try_files, dyrektywa error_page prawdopodobnie nigdy nie zostanie wykonana. Dzieje się tak dlatego, że ta dyrektywa obsłuży pełny cykl życia żądania.

Rozważmy następujący przykład:

root screenshot

W tym przypadku każde żądanie będzie przetwarzane przez pierwszy blok obsługujący pliki z /var/www/main. Nie dotyczy to żądań zaczynających się od /another. Jeśli jednak plik nie zostanie znaleziony, zostanie zainicjowane wewnętrzne przekierowanie do /another/whoops/html. Doprowadzi to do kolejnego wyszukiwania lokalizacji. Z kolei skieruje to żądanie do bloku pomocniczego, a plik ten będzie obsługiwany z lokalizacji /var/www/another/whoops.html.

Jak widać, zrozumienie sytuacji, w których Nginx wywoła nowe wyszukiwanie lokalizacji, może pomóc lepiej przewidzieć zachowanie systemu podczas przetwarzania żądań.

Podsumowanie

Praca administratorów staje się znacznie prostsza, gdy rozumieją oni metody, za pomocą których Nginx obsługuje żądania klientów. Pozwala to administratorom ustalić, do którego bloku serwera trafi żądanie. Mogą oni również określić, który blok lokalizacji zostanie wybrany na podstawie URI żądania. Ogólnie rzecz biorąc, daje to również administratorom możliwość śledzenia kontekstów stosowanych przez Nginx podczas obsługi każdego żądania.

Na koniec możesz zapoznać się z innymi poradnikami na naszym blogu poświęconymi Nginx. Pomogą Ci one lepiej wykorzystać możliwości jednego z najpopularniejszych serwerów WWW na świecie:

Powodzenia!

author

Manpreet Singh

Autor · CloudSigma

Preslav Dobrev jest projektantem kreatywnym w CloudSigma, skupiającym się na spójnej tożsamości biznesowej przy wykorzystaniu tradycyjnych i innowacyjnych kanałów marketingowych. Biegle łączy wizję artystyczną ze strategicznym marketingiem, tworząc wywierające wpływ narracje marki.

Komentarze

Brak komentarzy. Bądź pierwszy.