Русский
Русский
English
Статистика
Реклама

Highload

Перевод Решение проблемы N1 запроса без увеличения потребления памяти в Laravel

28.06.2020 00:17:28 | Автор: admin

Одна из основных проблем разработчиков, когда они создают приложение с ORM это N+1 запрос в их приложениях. Проблема N+1 запроса это не эффективный способ обращения к базе данных, когда приложение генерирует запрос на каждый вызов объекта. Эта проблема обычно возникает, когда мы получаем список данных из базы данных без использования ленивой или жадной загрузки (lazy load, eager load). К счастью, Laravel с его ORM Eloquent предоставляет инструменты, для удобной работы, но они имеют некоторые недостатки.
В этой статье рассмотрим проблему N+1, способы ее решения и оптимизации потребления памяти.


Давайте рассмотрим простой пример, как использовать eager loading в Laravel. Допустим, у нас есть простое веб-приложение, которое показывает список заголовков первых статей пользователей приложения. Тогда связь между нашими моделями может быть вроде такой:


Class User extends Authenticatable{    public function articles()    {        return $this->hasMany(Aricle::class, 'writter_id');    }}

и тогда простое действие получения данных из базы данных и передачи в шаблон может выглядеть таким образом:


Route::get('/test', function() {    $users = User::get();    return view('test', compact('users'));});

Простой шаблон test.blade.php для отображения списка пользователей с соответствующими заголовками их первой статьи:


@extends('layouts.app')@section('content')<ul>    @foreach($users as $user)        <li>Name: {{ $user->name}}</li>        <li>First Article: {{ $user->articles()->oldest()->first()->title }}</li>    @endforeach</ul>@endsection

И когда мы откроем нашу тестовую страницу в браузере, мы увидим нечто подобное:


image


Я использую debugbar (https://github.com/barryvdh/laravel-debugbar), чтобы показать, как выполняется наша тестовая страница. Для отображения этой страницы вызывается 11 запросов в БД. Один запрос для получения всей информации о пользователях и 10 запросов, чтобы показать заголовок их первой статьи. Видно, что 10 пользователей создают 10 запросов в базу данных к таблице статей. Это называется проблемой N+1 запроса.


Решение проблемы N+1 запроса с жадной загрузкой


Вам может показаться, что это не проблема производительности вашего приложения в целом. Но что, если мы хотим показать больше чем 10 элементов? И часто, нам также приходится иметь дело с более сложной логикой, состоящего из более чем одного N+1 запроса на странице. Это условие может привести к более чем 11 запросам или даже к экспоненциально растущему количеству запросов.


Итак, как мы это решаем? Есть один общий ответ на это:


Eager load


Eager load (жадная загрузка) это процесс, при котором запрос для одного типа объекта также загружает связанные объекты в рамках одного запроса к базе данных. В Laravel мы можем загружать данные связанных моделей используя метод with(). В нашем примере мы должны изменить код следующим образом:


Route::get('/test', function() {    $users = User::with('articles')->get();    return view('test', compact('users'));});

@extends('layouts.app')@section('content')<ul>    @foreach($users as $user)        <li>Name: {{ $user->name}}</li>        <li>First Article: {{ $user->articles()->sortBy('created_at')->first()->title }}</li>    @endforeach</ul>@endsection

И, наконец, уменьшить количество наших запросов до двух:


image


Также мы можем создать связь hasOne, с соответствующим запросом для получения первой статьи пользователя:


    public function first_article()    {        return $this->hasOne(Aricle::class, 'writter_id')->orderBy('created_at', 'asc');    }

Теперь мы можем загрузить ее вместе с пользователями:


Route::get('/test', function() {    $users = User::with('first_article')->get();    return view('test', compact('users'));});

Результат теперь выглядит следующим образом:


image


Итого, мы можем уменьшить количество наших запросов и решить проблему N+1 запроса. Но хорошо ли мы улучшили нашу производительность? Ответом может быть "нет"! Это правда, что мы уменьшили количество запросов и решили проблемы N+1 запроса, но на самое деле мы добавили новую неприятную проблему. Как вы видите, мы уменьшили количество запросов с 11 до 2, но мы также увеличили количество загружаемых моделей с 20 до 10010. Это означает, чтобы показать 10 пользователей и 10 заголовков статей мы загружаем 10010 объектов Eloquent в память. Если у вас не ограничена память, то это не проблема. Иначе вы можете положить ваше приложение.


Жадная загрузка динамических отношений


Должно быть 2 цели при разработке приложения:


  1. Мы должны сохранять минимальное количество запросов в БД
  2. Мы должны сохранять минимальное потребление памяти

В нашем примере, мы не смогли свести к минимуму потребление памяти, в то время, как мы уменьшили наши запросы до минимального количества. Во многих случаях разработчики также хорошо достигают первой цели, но проваливают вторую. В этом случае мы можем использовать подход жадной загрузки динамических отношений через подзапрос, чтобы достичь обеих целей.


Для реализации динамических отношений, мы будем напрямую использовать primary key вместо его foreign key. Мы также должны использовать подзапрос в связанной таблице, чтобы получить соответствующий идентификатор. Подзапрос будет размещен в select на основе отфильтрованных данных связанной таблицы.


Пример получения пользователей и id их первых статей через подзапрос:


select     "users".*,    (        select "id"         from "article"        where "writter_id" = "users"."id"        limit 1    ) as "first_article_id"from "users"

Мы можем получить такой запрос, если добавим select в подзапрос в нашем query builder. С использованием Eloquent это можно написать следующим образом:


User::addSelect(['first_article_id' => Article::select('id')                ->whereColumn('writter_id', 'users.id')                ->orderBy('created_at', 'asc')                ->take(1)    ])->get()

Этот код генерирует такой же sql запрос, что и в примере выше. После этого мы сможем использовать связь "first_article_id" для получения первых статей пользователя. Чтобы сделать наш код чище, мы можем использовать query scope Eloquent, чтобы упаковать наш код и выполнить жадную загрузку для получения первой статьи. Таким образом, мы должны добавить следующий код в класс модели User:


Class User extends Authenticatable{    public function articles()    {        return $this->hasMany(Aricle::class, 'writter_id');    }    public function first_article()    {        return $this->hasOne(Aricle::class, 'writter_id')->orderBy('created_at', 'asc');    }    public function scopeWithFirstArticle($query)    {        $query->addSelect(['first_article_id' => Article::select('id')                ->whereColumn('writter_id', 'users.id')                ->orderBy('created_at', 'asc')                ->take(1)        ])->with('first_article')    }}

И наконец, давайте изменим наш контроллер и шаблон. Мы должны использовать scope в нашем контроллере для жадной загрузки первой статьи. И мы можем напрямую обращаться к переменной first_article в нашем шаблоне:


Route::get('/test', function() {    $users = User::withFirstArticle()->get();    return view('test', compact('users'));});

@extends('layouts.app')@section('content')<ul>    @foreach($users as $user)        <li>Name: {{ $user->name}}</li>        <li>First Article: {{ $user->first_article->title }}</li>    @endforeach</ul>@endsection

Ниже результат производительности страницы после внесения этих изменений:


image


Теперь наша страница содержит всего 2 запроса и загружает 20 моделей. Мы достигли обеих целей оптимизации количества запросов к БД и минимизации потребления памяти.


Ленивая загрузка динамических отношений


Наша динамическая связь не будет работать хорошо автоматически. Сначала мы должны загрузить подзапрос для создания этой связи. Что, если мы хотим использовать ленивую подгрузку первых статей пользователя в одном месте и иметь жадную загрузку статей в другом месте?


Для этого нам нужно добавить небольшой хинт в наш ход, добавив accessor для свойства первой статьи:


public function getFirstArticleAtribute(){    if(!array_key_exists('first_article', $this->relations)) {        $this->setRelation('first_article', $this->articles()->oldest()->first());    }    return $this->getRelation('first_article');}

В действительности, мы не реализовывали ленивую загрузку для динамической связи. Мы просто назначили результат выполнения запроса получения первой статьи пользователя. Это должно работать одинаково хорошо при обращении к свойству first_article как для жадной загрузки, так и для ленивой загрузки.


Динамические связи в Laravel 5.X


К сожалению, наше решение применимо только к Laravel 6 и выше. Laravel 6 и предыдущие версии используется разная реализация addSelect. Для использования в более старых версиях фреймворка мы должны изменить наш код. Мы должны использовать selectSub для выполнения подзапроса:


public function scopeWithFirstArticle($query){    if(is_null($query->toBase()->columns)) {        $query->select([$query->toBase()->from . '.*']);    }    $query->selectSub(                Article::select('id')                ->whereColumn('writter_id', 'users.id')                ->orderBy('created_at', 'asc')                ->take(1)                ->toSql(),             'first_article_id'            )->with('first_article');}
Подробнее..

Из песочницы Вы разрабатываете эффективные или антихрупкие системы?

27.06.2020 16:12:41 | Автор: admin

Давайте поговорим о high-load, high-extensible, ООП, DDD & EDA.


Речь пойдет о разнице между high load & high extensibility это разные цели и задачи. Для которых нужны принципиально разные подходы. Которые часто вызывают когнитивный диссонанс среди программистов, предвещающий бурную полемику.


На Хабре много статей про high-load, а вот тема high-extensible как-то упускается из внимания.


image


Хайлоад это хайлоад. Это быстрые ответы на запросы и постоянные соединения. Обычно тут php RoadRunner или Go или Rust, с асинхронностью и event loop. Проблема 10k и т. д.


ООП было придумано как решение задачи расширения или High Extensible. Тут лучше работают DDD (domain driven design) & EDA (event driven architecture).


Быть эффективным или быть антихрупким?


Это две крайности одной и той же сущности.


В книге Антихрупкость, автор Нассим Талеб пишет о том что погоня за эффективностью всегда влечет рост хрупкости. Если мы ставим себе цель делать систему антихрупкой, нам придется жертвовать эффективностью. Иного не дано.


Антихрупкость любит случайность и неопределенность, что означает и это ключевое свойство антихрупкости любовь к ошибкам, к определенному классу ошибок. Уникальность антихрупкости состоит в том, что она позволяет работать с неизвестностью, делать что-то в условиях, когда отсутствует понимание, что именно делается, и добиваться успеха.

Вам нужна система которая будет эффективной или антихрупкой?


Ответ на этот вопрос влияет на решения о том какие технологии следует использовать.


ООП было придумано как решение проблем расширяемости больших систем (большой функционал)


Цитата Алана Кея, автора ООП:


Я придумал термин объектно-ориентированный, и могу сказать, что я не имел в виду С++. Алан Кэй, конференция OOPSLA, 1997.

Ключ к созданию хороших масштабируемых систем это проработка механизмов общения модулей, а не проработка их внутренних свойств и поведения.

Я не против типов, но мне не знакома ни одна система типов, которая не вызывала бы боли. Так что мне все еще нравится динамическая типизация.

Позднее связывание позволяет с меньшими усилиями встраивать в проект идеи, которые возникли позже в процессе разработки (по сравнению с системами с более ранним связыванием вроде C, C++, Java, и пр.)

Я считал объекты чем-то вроде модулей, биологических клеток, и/или отдельных компьютеров в сети, которые могут общаться только через сообщения.

Я сожалею о том, что давным-давно придумал термин объекты для этого явления, так как его использование приводит к тому, что многие люди уделяют основное значение идее, которая не так важна, как основная. Основная идея это обмен сообщениями

ООП для меня означает лишь обмен сообщениями, локальное сохранение, и защита, и скрытие состояния, и крайне позднее связывание

Итак, если взять эти идеи, то тезисами это можно сделать так:


  1. Масштабируемые и расширяемые системы состоят из модулей
  2. Модули общаются друг с другом через сообщения
  3. Это обеспечивает позднее связывание крайне позднее связывание код и логику можно менять в любой момент не меняя уже существующий код в модулях
  4. Для работы такой системы важна динамическая типизация, а не статическая

В 80е годы ООП разбилось на 2 типа


В книге Мифический человеко-месяц, автор Фредерик Брукс описывает момент когда ООП разбилось на 2 мира. Часть "Объектно-ориентированное программирование: а медна пуля не
подойдет?"


Разработка из больших частей. Если осуществлять сборку из частей, которые достаточно сложны и имеют однородные интерфейсы, можно быстро образовывать довольно богатые структуры.
Согласно одному из взглядов на объектно-ориентированное программирование эта дисциплина осуществляет модульность и чистые интерфейсы. Другая точка зрения подчеркивает инкапсуляцию невозможность увидеть и еще менее изменить внутреннюю структуру детали. Еще одна точка зрения отмечает наследование и сопутствующую ему иерархическую структуру классов с виртуальными функциями. И
еще один взгляд: важнейшей является сильная абстрактная типизация данных вместе с гарантиями, что конкретный тип данных будет обрабатываться только применимыми к нему операциями.
В настоящее время не нужен весь пакет Smalltalk или C++, чтобы использовать любой из этих дисциплин многие из них поглотили объектно-ориентированные технологии. Объектно-ориентированный подход привлекателен, как поливитамины: одним махом (т.е. переподготовкой программиста) получаешь все. Очень многообещающая концепция.

Получается что идея ООП зародилась в 60е годы, в 80е годы уже были разные точки зрения на то что есть ООП.


Сегодня под ООП чаще всего имеют ввиду классы. Хотя Алан Кей и Дэвид Уэст говорят что это ошибка. И что классы это всего лишь структуры данных. Это не про ООП. C++ & Java в базе не имеют ничего общего с ООП.


Речь идет про то ООП которое про модули и обмен сообщениями. Сегодня эта идеология чаще всего встречается со словами DDD & EDA. Ты пишешь модули, которые обмениваются сообщениями через чистые интерфейсы (события, хуки, триггеры) с крайне поздним связыванием.


Так кто прав, а кто лев?


Если вам нужен HighLoad, вам надо топить за эффективность, масштабирование скорости исполнения, статическую типизацию, асинхронность, события и eventloop.


Если вам нужен HighExtensible, вам надо топить за антихрупкость, масштабирование скорости изменений, динамическую типизацию, модули и домены (DDD) и события как EDD (обмен сообщениями между модулями).


Причины проблем


Проблема в том что программисты спорят о том что ООП плохо (классы это медленно и хрупко, а функции быстро и круто, ну или как то там наоборот), о том что вот в PHP не хватает асинхронности и event loop.


Например эти темы поднимались на недавнем митапе SkyEng https://www.youtube.com/watch?v=QrlWrFILjMk


Но не понимают что это разговор о разных идеологиях, парадигмах и архитектурах. Это все равно что говорить что у дома нет колес или у автомобиля не хватает 3го этажа с ковром. Вот бы ложка была вилкой или вилка была бы ложкой. Вообще есть конечно вилка-ложки, но часто ли мы ими пользуемся вне горных походов?


Если вы создаете прикладную бизнес систему, где есть куча реальных пользователей, бизнес требования, бизнес аналитики, никто не знает что хочет, а требования меняются каждый день вы очень сильно попадете в засаду если пойдете по пути HighLoad, будете делать статическую типизацию, асинхронность, без доменов и событий вам будет больно. Тут нужно двигаться ближе к архитектуре для HighExtensible.


Если вы создаете микросервисы, которым надо отрабатывать 100 000 операций в секунду, то попытка пойти по пути динамической типизации, доменам, без асинхронности тоже сделает очень больно. Тут стоит копать в сторону архитектуры под HighLoad.


Сделать одновременно HighLoad & HighExtensible не реально или очень близко к не реальному. Попытки совместить эти две идеологии приводят к диким проблемам, большим затратам и часто к провалу.


Ну а программисты готовы спорить об этом до посинения. Не понимая что говорят про разные контексты, задачи и механизмы проектирования архитектур.


Примеры


Про high-load:


  • NodeJS пошел с вектором в event loop & асинхронные механики. Есть много интересных кейсов с построением highload микросервисов и REST API.
  • Туда же можно отнести Go, Rust и разные механики PHP типа RoadRunner и PHP Swoole.

Про high-extensible


  • Можем взять Redmine, это типа система управления задачами, на базе RoR, там также есть модули (плагины), а общение модулей и обмен сообщениями строится на хуках.
  • Ребята из FreeScout решили делать систему хелпдеск на Laravel. Это прикладная система с кучей бизнес логики. Они реализовали свою архитектуру модулей (DDD) и обмена сообщениями (EDA) применив Eventy.
  • Eventy в свою очередь взял идею хуков из WordPress. Надо ли представлять этого зверя? Который держит более 30% рынка веб сайтов. Его архитектура также базируется на идеи модулей (плагинов, DDD), которые взаимодействуют друг с другом через сообщения (хуки, EDA).

Очень интересный опыт у ребят из iSpring:


https://www.youtube.com/watch?v=xT25xiKqPcI


Там речь о том чем отличается монолит от копролита. О том что если уметь готовить монолит, с DDD & EDA, то можно получить много преимуществ. А если не уметь то и микросервисы не факт что помогут.


В общем, ребята ступили на путь освоения DDD & EDA. И это радует.


Заключение


Мир технологий в целом и разработок в частности очень многогранен. Если есть какая то технология значит на то есть причины. Не знание этих причин еще не значит что технология плохая сама по себе.


Динамическая и статическая типизация обе правильные, если их применять в соответствующих задачах.


ООП тоже очень разное может быть и есть много разных точек зрения на то что это такое. И все они правильные. Просто там разные контексты.


Вилка или ложка не плохие и не хорошие. И если человек ест спагетти ложкой, а суп вилкой, а потом пишет о том что ложка это плохо, потому что спагетти вываливаются, и вилка плохо потому что суп хлебать ей не удобно то разве это проблема в вилке или ложке?


Ну и под конец, хочется больше примеров. Кто какие еще примеры знает грамотного применения DDD & EDA?


Особенно интересует опыт построения HighExtensible систем в РФ. Потому что на западе его хватает. А тема high-load на Хабре и так перегрета. Хочется больше поговорить о практиках high-extensible.

Подробнее..
Категории: Ооп , Архитектура , Highload

Категории

Последние комментарии

© 2006-2020, personeltest.ru