Однажды утром я проснулся от уведомлений Slack. Это плохой знак.
За ночь мой экземпляр Redis был полностью заполнен. Мы используем Redis для двух целей:
- Session storage
- Caching a few bits of data, nothing substantial
Я использовал TablePlus, чтобы посмотреть, что можно сделать в Redis. Немного сложно сказать, что происходит, поскольку Laravel использует случайные хэши как часть ключей кэша, а полезная нагрузка кодируется/шифруется.
Однако я увидел две базы данных Redis (db0
и db1
). Проверив файл config/databases.php
, я обнаружил, что для 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'),
],
],
];
Соединение по умолчанию
использует db0
, а соединение cache
использует db1
.Оказывается, сеансы хранятся в db0
, а то, что мы кэшируем в коде, использует db1
. В базе данных по умолчанию, db0
, было НАМНОГО больше ключей, чем в базе данных кэша.
Приложение создавало слишком много сеансов.
Что создает сеанс?
Каждый веб-запрос (для маршрутов, определенных в routes/web.php
) создает сеанс (или использует существующий). Веб-приложения возвращают файл cookie при создании сеанса. Веб-браузеры сохраняют эти файлы cookie и отправляют их обратно при выполнении дополнительных веб-запросов. Это позволяет нашим веб-приложениям знать, какой сеанс действителен для данного пользователя.
Если бы браузер не возвращал файл cookie при каждом запросе, пользователь не смог бы оставаться в системе.
Сеансы на основе API работают иначе. Каждый сеанс создается, а затем уничтожается при каждом веб-запросе — файлы cookie не используются. Вместо этого клиенту необходимо отправлять информацию об аутентификации при каждом веб-запросе (обычно это какой-то токен).
Что взорвало Redis?
Итак, что же тогда привело к тому, что наш экземпляр Redis взорвался сеансами?
Динамически генерируемые ресурсы, которые другие пользователи встраивают на свои веб-сайты. У нас было два таких случая:
- Our application generated a
.js
file that others embedded on their web sites - Our application also generated
.svg
images for the same purpose
Эти маршруты были определены в нашем файле routes/web.php
:
Route::get('/embed.js');
Route::get('/{project}/share.js');
Вы видите проблему? Клиенты помещали их на свои собственные веб-сайты.Каждый раз, когда кто-то посещал их веб-сайт, к нашему приложению отправлялся HTTP-запрос для встраивания или SVG, и это создавало сеанс.
Это означает, что веб-трафик наших клиентов также создавал сеансы в нашем веб-приложении!
Как уменьшить количество сеансов
Исправление заключается в том, что мы не должны создавать сеансы для определенных маршрутов. Достаточно просто сказать, но как этого добиться?
Оказывается, создание файлов cookie и сеансов выполняется в промежуточном программном обеспечении Laravel. Это хорошо, поскольку мы контролируем, какое промежуточное ПО применяется к каждому маршруту.
Чтобы гарантировать, что некоторые маршруты не создают сеансовые/возвратные файлы cookie, я предпочитаю создавать отдельный файл маршрутов с другим стеком промежуточного ПО.
Для этого нам нужно сделать несколько вещей:
- 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
Новый файл маршрутов прост — мы создаем новый файл и перемещаем в него наши определения маршрутов:
# File routes/static.php`
# Move these from routes/web.php
Route::get('/embed.js');
Route::get('/{project}/share.js');
Затем мы можем обновить файл Kernel.php
, чтобы создать новый стек промежуточного ПО.Мы можем скопировать стек промежуточного программного обеспечения web
и удалить промежуточное программное обеспечение, которое обрабатывает файлы cookie и сеансы:
# 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
Мы создали новую группу ПО промежуточного слоя с именем static
. Это похоже на промежуточное ПО API, но у нас нет регулирования.
Наконец, нам нужно зарегистрировать новый файл маршрутов и применить нашу новую группу промежуточного программного обеспечения static
.Мы сделаем это, обновив 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
регистрирует каждый файл маршрута и определяет их промежуточное ПО. Вот как каждый объект в routes/web.php
получает назначенную ему группу промежуточного программного обеспечения web
.