Pewnego ranka obudziły mnie powiadomienia ze Slacka. To nigdy nie jest dobry znak.
Z dnia na dzień moja instancja Redis całkowicie się zapełniła. Używamy Redisa do dwóch rzeczy:
- Session storage
- Caching a few bits of data, nothing substantial
Użyłem TablePlus, aby zobaczyć, co mogę w Redis. Trudno powiedzieć, co się dzieje, ponieważ Laravel używa losowych skrótów jako części kluczy pamięci podręcznej, a ładunki są kodowane/szyfrowane.
Jednak zauważyłem, że istnieją 2 bazy danych Redis (db0
i db1
). Sprawdzając plik config/databases.php
, odkryłem, że 2 odpowiednie bazy danych zostały rzeczywiście zdefiniowane dla Redis.
# File config/databases.php
return [
// Things ommitted here...
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
];
Połączenie domyślne
używa db0
, podczas gdy połączenie cache
używa db1
.Okazuje się, że sesje są przechowywane w db0
, podczas gdy to, co buforujemy w kodzie, używa db1
. Domyślna baza danych, db0
, miała DUŻO więcej kluczy niż baza danych pamięci podręcznej.
Aplikacja tworzyła zbyt wiele sesji.
Co tworzy sesję?
Każde żądanie internetowe (dla tras zdefiniowanych w routes/web).php
) tworzy sesję (lub używa już istniejącej). Aplikacje internetowe zwracają plik cookie podczas tworzenia sesji. Przeglądarki internetowe przechowują te pliki cookie i wysyłają je z powrotem podczas wysyłania dodatkowych żądań internetowych. Dzięki temu nasze aplikacje internetowe mogą wiedzieć, która sesja jest ważna dla danego użytkownika.
Jeśli przeglądarka nie zwraca pliku cookie przy każdym żądaniu, użytkownik nie będzie mógł pozostać zalogowany.
Sesje oparte na interfejsie API nie działają w ten sposób. Każda sesja jest tworzona, a następnie niszczona w ramach każdego żądania internetowego — nie ma żadnych plików cookie. Zamiast tego klient musi wysyłać informacje uwierzytelniające przy każdym żądaniu sieciowym (zwykle pewnego rodzaju token).
Co wysadziło Redisa?
Co w takim razie spowodowało wysadzenie naszej instancji Redis z sesjami?
Dynamicznie generowane zasoby, które inni osadzają w swoich witrynach. Mieliśmy dwa takie przypadki:
- Our application generated a
.js
file that others embedded on their web sites - Our application also generated
.svg
images for the same purpose
Te trasy zostały zdefiniowane w naszym pliku routes/web.php
:
Route::get('/embed.js');
Route::get('/{project}/share.js');
Czy widzisz problem? Klienci umieszczali je na własnych stronach internetowych.Za każdym razem, gdy ktoś odwiedzał ich witrynę, wysyłano żądanie HTTP do naszej aplikacji o osadzenie lub SVG, co tworzyło sesję.
Oznacza to, że ruch internetowy naszych klientów również tworzył sesje w naszej aplikacji internetowej!
Jak ograniczyć tworzenie sesji
Poprawka polega na upewnieniu się, że nie tworzymy sesji dla niektórych tras. Łatwo powiedzieć, ale jak to zrobić?
Okazuje się, że tworzenie plików cookie i sesji odbywa się w oprogramowaniu pośredniczącym Laravela. To dobrze, ponieważ kontrolujemy, które oprogramowanie pośredniczące jest stosowane do każdej trasy.
Aby upewnić się, że niektóre trasy nie tworzą sesji/zwracanych plików cookie, lubię utworzyć oddzielny plik tras, który ma inny stos oprogramowania pośredniego.
Aby to zrobić, musimy zrobić kilka rzeczy:
- Create a new
routes/static.php
file (the name is arbitrary) - Add a middleware stack to
app/Http/Kernel.php
- Update
app/Providers/RouteServiceProvider.php
to load our new route file, and apply our new middleware stack
Nowy plik tras jest prosty — tworzymy nowy plik i przenosimy do niego nasze definicje tras:
# File routes/static.php`
# Move these from routes/web.php
Route::get('/embed.js');
Route::get('/{project}/share.js');
Następnie możemy zaktualizować plik Kernel.php
, aby utworzyć nowy stos oprogramowania pośredniego.Możemy skopiować stos oprogramowania pośredniego web
i usunąć oprogramowanie pośredniczące obsługujące pliki cookie i sesje:
# File app/Http/Kernel.php
# Items omitted here
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],26+ 27+ 'static' => [28+ \Illuminate\Routing\Middleware\SubstituteBindings::class,29+ ],
];
# Items omitted here
Utworzyliśmy nową grupę oprogramowania pośredniczącego o nazwie static
. Jest podobny do oprogramowania pośredniczącego API, ale nie mamy do czynienia z ograniczaniem przepustowości.
Na koniec musimy zarejestrować nowy plik tras i zastosować naszą nową statyczną
grupę oprogramowania pośredniego.Zrobimy to, aktualizując RouteServiceProvider.php
:
# File app/Providers/RouteServiceProvider.php
# Items omitted here
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
*/
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));22+ 23+ Route::middleware('static')24+ ->namespace($this->namespace)25+ ->group(base_path('routes/static.php'));26+ });
}
# Items omitted here
RouteServiveProvider
rejestruje każdy plik trasy i określa jego oprogramowanie pośredniczące. W ten sposób everyting w routes/web.php
otrzymuje przypisaną do niego grupę oprogramowania pośredniego web
.