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

В админке выводилась информация из базы, по 20 записей на страницу + подтягивались связи. На это уходило 50 (!!!) секунд. Грех было не посмотреть, что происходит с базой. Я не верил, что при 50к записях, порядка 6-7 джойнов для фильтрации, и затем 6-7 запросов eager loading могут быть такие тормоза.

Так и было — на все запросы уходило порядка 0.18с, что вполне приемлимо.

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

class OrderController
{
    public function index(Request $request, OrderFilter $filter)
    {
        // применяем фильтры
        $query = $filter->applyFilters($request);

        // Отдаем постранично
        return $query->paginate($request->input('count', 20));
    }
}

Dispatcher начинает преобразовывать результат работы контроллера в зависимости от того, что хочет клиент. Разумеется, он видел заголовок Accept: application/json, и начинал свою грязную работу. И тут начиналась жара.

Для каждой модели, для каждой связи, и далее рекурсивно вызывается очень много методов — магические геттеры, мутаторы, касты.

Тот самый зловещий код
/**
 * Convert the model's attributes to an array.
 *
 * @return array
 */
public function attributesToArray()
{
    $attributes = $this->getArrayableAttributes();
    // If an attribute is a date, we will cast it to a string after converting it
    // to a DateTime / Carbon instance. This is so we will get some consistent
    // formatting while accessing attributes vs. arraying / JSONing a model.
    foreach ($this->getDates() as $key) {
        if (! isset($attributes[$key])) {
            continue;
        }
        $attributes[$key] = $this->serializeDate(
            $this->asDateTime($attributes[$key])
        );
    }
    $mutatedAttributes = $this->getMutatedAttributes();
    // We want to spin through all the mutated attributes for this model and call
    // the mutator for the attribute. We cache off every mutated attributes so
    // we don't have to constantly check on attributes that actually change.
    foreach ($mutatedAttributes as $key) {
        if (! array_key_exists($key, $attributes)) {
            continue;
        }
        $attributes[$key] = $this->mutateAttributeForArray(
            $key, $attributes[$key]
        );
    }
    // Next we will handle any casts that have been setup for this model and cast
    // the values to their appropriate type. If the attribute has a mutator we
    // will not perform the cast on those attributes to avoid any confusion.
    foreach ($this->getCasts() as $key => $value) {
        if (! array_key_exists($key, $attributes) ||
            in_array($key, $mutatedAttributes)) {
            continue;
        }
        $attributes[$key] = $this->castAttribute(
            $key, $attributes[$key]
        );
        if ($attributes[$key] && ($value === 'date' || $value === 'datetime')) {
            $attributes[$key] = $this->serializeDate($attributes[$key]);
        }
    }
    // Here we will grab all of the appended, calculated attributes to this model
    // as these attributes are not really in the attributes array, but are run
    // when we need to array or JSON the model for convenience to the coder.
    foreach ($this->getArrayableAppends() as $key) {
        $attributes[$key] = $this->mutateAttributeForArray($key, null);
    }
    return $attributes;
}


Ну конечно, мутаторы — это очень удобная штука. Классно и красиво можно получать доступ к различным данным в моделях/связях, но на странице документации разработчики поленились написать, что их использование оказывает существенное (так и хочется написать huge impact) влияние на производительность.

И тут приходит понимание, что поезд уже разогнался очень сильно, и нет времени переделывать все на datamapper/querybuilder. Остался я с ActiveRecord у разбитого корыта. Мне нравится эта магия, но нельзя ей злоупотреблять.

Чтобы не ломать ничего, пришлось звать на помощь Redis, в котором теперь лежать все данные, и регулярно обновляются после обновления моделей. Но не тут то было! Объем данных настолько велик, что Redis падал (грех на мне, возможно надо было его подтюнить). Пришлось пропускать данные через gzcompress, ибо стандартные 64мб никуда не годятся. И даже отдельный инстанс завел, чтобы была уверенность, что Redis больше не упадет под нагрузкой.

Теперь все работает, все хорошо. Данные отдаются меньше, чем за 0.5с, все счастливы. Но я сижу и думаю «подкупает Laravel скоростью и простотой, но следующий проект однозначно будет без этих ваших ActiveRecord».

Вот и сказке конец, а кто слушал — молодец, боттлнеков избежит.

Комментарии (53)


  1. janson
    13.04.2016 12:01
    +2

    Я не верил, что при 50к записях, порядка 6-7 джойнов для фильтрации, и затем 6-7 запросов eager loading могут быть такие тормоза.

    А при 6-7 джойнах для фильтрации можно обойтись запросами через ORM? Честно говоря слабо верится. Мне кажется, что когда начинаются такие фильтрации — это звоночек проверить схему БД или переходить на прямое построение запроса, без магии ORM.


    1. Miraage
      13.04.2016 14:45

      Схема БД — именно. Были бы магические поля реальными — проблемы бы не было.


      1. Fesor
        15.04.2016 20:33

        Я почему-то думаю что тут идут запросы тупо на чтение, а стало быть тут вообще можно не заморачиваться с ORM. И тут споры data mapper vs active record и т.д. не особо помогут. С Data mapper вы получите тоже существенное падение производительности (если не рассматривать фишку той же доктрины агрегировать результаты выборок в отдельные DTO объекты почти без магии).

        Вообще тут такой момент, использовать ORM оправдано только тогда, когда у наших объектов, на которые мы «мэпим релейшены», есть какое-то поведение, и оно используется. Подавляющее большинство людей использует active record тупо как row data gateway, вынося всю логику/поведение хорошо если в сервисы, так еще частенько в контроллеры (особенно это любят делать ребята с Yii). Вывод — не нужны им ORM.


  1. HaruAtari
    13.04.2016 12:12
    +9

    Описанная вами проблема — не проблема AR. Сами же говорите, запросы отрабатывают за доли секунды. Проблема в кривом преобразовании данных. И то, что в ларе это находится в одном месте, не делает сам паттерн плохим.


    1. Metus
      13.04.2016 13:57
      +1

      Я, если честно, не совсем понял как можно обрабатывать данные, чтобы получить 50 секунд. У нас на аналогичных параметрах системы (фильтр 50 тыс. товаров по EAV) в особо тяжёлых случаях 1.5 секунды.


  1. M-A-X
    13.04.2016 12:21
    -21

    А я всегда говорю, что эти ваши фреймворки фигня на постном масле:

    https://habrahabr.ru/sandbox/96533/
    http://blog.kpitv.net/article/frameworks-1/

    Закон дырявых абстракций :)


    1. Ch4r1k
      13.04.2016 13:14
      +5

      Да да, самописное разрудивание локалей и поддержка миграций… Эх… зачем фреймворки нужны…


    1. ellrion
      13.04.2016 13:15
      +4

      Хватит пихать сою «крайне спорную» статью везде. А проблема преобразования сырых данных в объекты (hydration) так то будет в любом случае. В случае с Ларой накладные расходы конечно очень велики. Но возьмите и сделайте именно тут выборку без гидрации, не? (https://gist.github.com/anonymous/c3d7d713722f3559b922fd638454d03a)
      Понятно же что, есть случаи когда нужна простота и удобство кода и скорость разработки, а есть случаи когда нужна производительность.


      1. M-A-X
        13.04.2016 13:25
        -21

        Отвечаю сразу на 2 коммента, а то быдло люто заминусовало и нету мочи часто писать.

        >самописное разрудивание локалей

        Шойта такое?

        >поддержка миграций

        Миграции невозможны без фреймворка?
        pma позволяет логировать все изменения базы.
        Многим проектам нужны миграции?

        >Хватит пихать сою «крайне спорную» статью везде.

        Буду пихать, пока не получу инвайт :)
        На хабре более спорная версия, но я ее обновлять не хочу, так как уже после обновления одна статья была удалена из песочницы.
        Поэтому продолжил писать на своем ресурсе.

        >А проблема преобразования сырых данных в объекты (hydration) так то будет в любом случае.

        Это называется закон дырявых абстракций. Юзайте и дальше монструозно абстрагирующие решения


        1. barsulka
          13.04.2016 13:50
          +13

          Что-то мне подсказывает, что инвайт вы таки не скоро получите. И даже не потому, что мы тут быдло минусующее.


        1. Ch4r1k
          13.04.2016 14:35
          +3

          Миграции с использованием фреймворков выглядят проще, если мы говорим о проектах, под проектами я понимаю не «сайт-визитка», а хотя бы landing page или иной продукт, который разрабатывается командой, то «самописность» сначала нужно понять, потом простить и как-то жить. Время включения в проект нового человека в случае фреймворка зачастую значительно ниже, чем случай своего кода.
          Ладно, другой вопрос, сколько времени у Вас займет написание REST-сервиса, без фреймворка?
          P.S. Если Вам не приходится использовать фреймворк, это значит что он вам не нужен, но это совсем не означает, что вворачивать шурупы должны с кулака, только лишь потому что Вам это удается.


          1. maxru
            13.04.2016 14:53

            > не «сайт-визитка», а хотя бы landing page
            По сложности это одно и то же.


            1. Ch4r1k
              13.04.2016 15:17

              Не всегда, но соглашусь, пример не очень хороший.


      1. sl_bug
        13.04.2016 14:20
        +2

        «Крайне спорную» это крайне мягкое утверждение :)


    1. Delphinum
      13.04.2016 13:54
      +2

      А я всегда говорю, что эти ваши фреймворки фигня на постном масле

      Ну это вы «всегда» говорите до моменты вашего «взросления» до профессионального разработчика, который работает в команде и тестирует свой код. Как только вы перейдете в эту форму, очень скоро полюбите и фреймворки, и предлагаемые ими решения, и научитесь пакеты складывать в собственные фреймворки, и т.д.


      1. M-A-X
        13.04.2016 15:04
        -15

        Отвечаю сразу на 3 коммента

        >Что-то мне подсказывает, что инвайт вы таки не скоро получите.

        Мне пофиг. Главное лулзы :)

        >И даже не потому, что мы тут быдло минусующее.

        То есть Вы тоже не поленовались пойти насрать в карму? :)

        >Миграции с использованием фреймворков выглядят проще, если продукт, разрабатывается командой

        Какая разница, кем разрабатывается продукт? :) Тем более, что большинству миграции не нужны :)
        Для миграций тянуть фреймворк и мучиться? Миграции это одно, разработка другое. Глупо смешивать все в кучу.

        >Время включения в проект нового человека в случае фреймворка зачастую значительно ниже, чем случай своего кода.

        Прям уж таки.
        Тут на фреймворках такого понаписывают, что каждый раз нужно заново включаться в проект. А даже не совсем толковую самописную CMS я освоил за 2 мес. Мое ядро занимает 50 КБ, за день можно освоить.

        >сколько времени у Вас займет написание REST-сервиса, без фреймворка

        А что сложного в РЕСТ-сервисе?
        Получили запрос, дали ответ.
        Чем это отличается от сайта?

        >Если Вам не приходится использовать фреймворк

        Приходится по работе.

        >вворачивать шурупы должны с кулака, только лишь потому что Вам это удается

        Опять тупая аналогия.
        При чем тут это?
        Если вам нужен ключ на 10, это не значит, что нужно тянуть супер-набор с сокирами, где бонусом идет ключ на 10, как в примере с миграциями.

        >моменты вашего «взросления» до профессионального разработчика

        Боже мой, профи на фреймворке отозвался.
        Я перерос ваши фреймворки.
        Фреймворки для хоумпаджей. Пихать их куда не следует — плачевно.

        Какие серьезные проекты на фреймворках?

        >который работает в команде и тестирует свой код

        Работаю в команде, так завелось, что тестов именно кода не пишем :) Лучше писать качественный код, а не тратить время на написание и поддержку тестов.

        >полюбите и фреймворки, и предлагаемые ими решения

        Это уж вряд ли. Такое ощущение, что фреймворки писали школьники. Так все кострубато и непродумано.

        >пакеты складывать в собственные фреймворки

        Шойта такое? У меня есть ядро своих сайтов. Зачем мне туда приделывать 5-ое колесо в виде пакетов (библиотек ?). Пусть живут себе отдельно.

        П.С.
        Некоторые хабровчане похожи на украинских майдаунов своей глупой верой и тупостью :)

        П.П.С.
        Ах да, нужно больше минусов :)

        П.П.П.С.
        Такое ощущение, что метаю бисер перед свиньями :)


        1. Ch4r1k
          13.04.2016 15:16
          +5

          За сим коментом, предлагаю не кормить этого товарища. ))


          1. HaruAtari
            13.04.2016 15:55
            +1

            Ему read only уже выдали

            P.S. Вот яркий пример того, к чему привели read&comment аккаунты.


            1. Delphinum
              13.04.2016 18:08
              +1

              Вот яркий пример того, к чему привели read&comment аккаунты

              Не, это не страшно. Такого рода комментаторы очень быстро выпилываются )


        1. sl_bug
          13.04.2016 15:22
          +2

          > Фреймворки для хоумпаджей. Пихать их куда не следует — плачевно.

          Разговоров про пихать куда не следует не было. Фреймворки нужно пихать куда следует :)

          > тестов именно кода не пишем

          Что-то не хотелось бы пользоваться плодами ваших разработок

          > У меня есть ядро своих сайтов.

          Поздравляю, у вас фреймворк.

          > Такое ощущение, что фреймворки писали школьники. Так все кострубато и непродумано.

          Выложите ваш фреймворк («ядро для сайтов») на гитхаб и мы все узнаем как нужно продуманно что-то писать.


        1. ellrion
          13.04.2016 15:32
          +3

          Какой шикарный хабросуицид!)


        1. claygod
          13.04.2016 17:05

          Работаю в команде, так завелось, что тестов именно кода не пишем :) Лучше писать качественный код, а не тратить время на написание и поддержку тестов.

          А тестируете?


          1. Delphinum
            13.04.2016 18:06

            Подозреваю что применяются, в лучшем случае, обычные человеко-тесты.


        1. Delphinum
          13.04.2016 18:05
          +1

          Я перерос ваши фреймворки.

          Я практически уверен, что вы еще не пользовались ни одним фреймворком для написания более-менее крупного проекта. Опровергните это утверждение, дайте ссылочку на исходники вашего крупного проекта без фреймворка и у меня вопросы к вам отпадут.
          Какие серьезные проекты на фреймворках?

          Практически все — topface.com к примеру. Миллионы пользователей, терабайты информации, фреймворк zf2.
          тестов именно кода не пишем :) Лучше писать качественный код, а не тратить время на написание и поддержку тестов

          Ну вот вы и признались в вашем уровне разработки )
          Работаю в команде

          Сколько в команде человек занимаются написанием кода и разработкой?
          У меня есть ядро своих сайтов

          Это вы про файл типа core/functions.php весом в десятки метров или про класс Core с сотнями методов внутри? Дайте ссыль на исходники, буду признателен. Сэкономим и ваше и мое время )


    1. Fesor
      15.04.2016 20:35

      Закон дырявых абстракций :)


      Абстракции на то и абстракции, что полной абстракции не выйдет получить. Так что «дырки» в них будут в любом случае, мы же в реальном мире живем. А если принять это за аксиому, то можно жить. Как никак, дырявая абстракция лучше чем вообще без абстракции (разумеется надо еще здравым смыслом пользоваться а не просто так их лепить).


  1. Delphinum
    13.04.2016 13:58

    Так напишите свой механизм заполнения пустых объектов данными из базы, сделайте его максимально легким и достаточно (но не более) функциональным, и будет вам счастье.


    1. Metus
      13.04.2016 14:02

      Вот именно. Никто не мешает не использовать всякие рекурсивные eager-loading в подобных случаях, а последовательно получить 2, 5, 10 (или сколько надо) коллекций и собрать их в массив для дальнейшей работы. Особенно если связанные коллекции должны фильтроваться и сортироваться.


  1. andrewnester
    13.04.2016 20:56
    +1

    Что-то мне подсказывает, что если бы вы переопределили метод attributesToArray в Ваших тяжелых моделях, так, чтобы он возвращал действильно нужные вам поля, проблема бы исчезла


  1. summerwind
    14.04.2016 02:58
    +1

    Я много раз спорил со знакомыми, пишущими на Laravel, что модель — это слой данных, который не должен ничего знать о том, в каком формате он будет представлен для пользователя, и все эти «implements JsonSerializable» у модели — это неверно. По мне, так данная статья еще раз это подтверждает.


    1. ellrion
      14.04.2016 10:13

      А как связано то, что модель содержит метод сериализации и то что — «это слой данных, который не должен ничего знать о том, в каком формате он будет представлен для пользователя»? Наличие базового метода сериализации в объекте это же нормальное явление.
      Тем более забавно как вы подтверждаете свою точку зрения опираясь на статью (> «По мне, так данная статья еще раз это подтверждает.») корень проблемы которой в ином. Хитро)


      1. summerwind
        14.04.2016 14:36

        Действительно, как же связан метод «jsonSerialize» в модели с представлением этой модели в формате JSON)))
        Нет, я не вижу ничего нормального, когда такие методы зашиты в модель. Грубо говоря, это аналогично (согласно моему мнению) тому, если в модели был бы метод «convertToHtml» с кусками html в нем. Для приложения на коленке такое, может, и сойдет, но не для чего-то серьезного.


        1. Big_Shark
          14.04.2016 17:24

          JSON нужен не только для вывода информации.


          1. Delphinum
            14.04.2016 20:12

            Правда? А для чего еще?


  1. atc
    14.04.2016 12:45

    У вас типичная batch задача, такую стоило бы делать или средствами SQL, не прибегая к маппингу вообще (вне зависимости от того, AR это или DM), либо просто поставить её в очередь и обработать в отдельном потоке.
    Резюме: AR не виноват, проблема в ошибочном выборе инструмента.


  1. NLO
    00.00.0000 00:00

    НЛО прилетело и опубликовало эту надпись здесь


    1. Delphinum
      14.04.2016 20:13

      Автор же говорит, что СУБД отрабатывает быстро, задержки на уровне интерпретатора. При чем тут индексы?


      1. NLO
        00.00.0000 00:00

        НЛО прилетело и опубликовало эту надпись здесь


        1. Delphinum
          14.04.2016 21:22

          Вы один из тех, кто любит все оптимизировать на этапе разработки? ) Давайте порассуждаем. Автор сказал:

          В админке выводилась информация из базы

          Админкой, как правило, пользуется админ, а это значит 1 запрос раз в 5-10 секунд (в лучшем случае). В свете этих выводов 0.18с уже не кажутся такой серьезной задержкой.


          1. NLO
            00.00.0000 00:00

            НЛО прилетело и опубликовало эту надпись здесь


            1. Delphinum
              14.04.2016 21:37

              Нет. Я один из тех кто знает, что такое индексы в реляционных субд :)

              Ок, спрошу по другому. У автора обращение к базе 0.18 с., а обработка результата этого обращения 49.82 с. Вопрос:
              При чем тут индексы?


              Я понимаю к чему вы клоните, но статья не об этом )


              1. NLO
                00.00.0000 00:00

                НЛО прилетело и опубликовало эту надпись здесь


                1. Delphinum
                  14.04.2016 22:43

                  Ну проблема еще может быть не в индексах, а в самом запросе.

                  select street, number, name,REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(phones, '?', ''),'+7','8'),')',''),'(',''),' ','') as phone
                  from person
                  


                  1. NLO
                    00.00.0000 00:00

                    НЛО прилетело и опубликовало эту надпись здесь


                    1. Delphinum
                      14.04.2016 23:11

                      Ну если 0.18с на запрос создает проблему, тогда народ начинает задумываться об оптимизации запросов и БД, до тех пор зачем тратить на это время? (К индексам это не относится, индексы как правило ставятся на автомате большинством проггеров)


                      1. NLO
                        00.00.0000 00:00

                        НЛО прилетело и опубликовало эту надпись здесь


                        1. Delphinum
                          15.04.2016 02:14

                          то у меня для вас снова плохие новости

                          И что это за новости?
                          Так может стоит тогда начать с проверки индексов? Так как это самое простое

                          И самое бесполезное в данном случае.
                          И я практически(ну почти) уверен, что дело в этом

                          Автор же написал в чем у него дело было.


                          1. NLO
                            00.00.0000 00:00

                            НЛО прилетело и опубликовало эту надпись здесь


  1. railsfun
    16.04.2016 13:34

    Извините, я мало знаком с Laravel но там же вроде ORM Eloquent а не AR.
    Или автор про сам паттерн активной записи в общем смысле?


    1. Fesor
      16.04.2016 13:46

      Eloquent это ORM + ActiveRecord. Конкретно у автора возникли проблемы именно с имплементацией тамошней реализации Active Record (конкретно мэппинг данных на объект), а не с ORM.

      Проблема active record в общем смысле может быть сформулирована только как «его пихают частенько туда куда не надо», забывая как-то как люди пришли к этому паттерну.


      1. railsfun
        16.04.2016 13:51

        в рельс можно наткнуться на такую проблему но несколько в иных контекстах. Там AR не сильно обвешивает сам класс оберточный методами какие бы выполняли «лишние» запросы но при джойнах так или иначе может выйти похожая ситуация…
        Пока что справляюсь силами ORM что и автору советую как то искать способ средствами ORM оптимизировать запросы или убрать лишние. Почти всегда такое удавалось.
        p.s. Согласен что мне просто может не попадались серьезные задачи на 1000к запросов с 10 жойнами. Как-то избегал архитектурно )


      1. railsfun
        16.04.2016 13:53

        p.s. кстати есть финт, может в php/laravel так же можно. Создавать не объект AR а просто объект PHP. Изначально не вязанный к БД (poro — Plain Old Ruby Object) и уже потом результаты его работы (работы с копиями такого обьекта) писать в БД


        1. Fesor
          16.04.2016 20:34

          То что вы описали очень похоже на шаблон data mapper, несколько годных реализаций которого есть под PHP. Или вы про то, что бы мэпить результат выборки прямо на DTO? Из таких ORM под php я знаю только Doctrine.


  1. AmdY
    20.04.2016 17:23
    +1

    А с чего вы взяли что ваша проблема в мутаторах? У вас проблема явно в другом месте.
    Покажите код OrderFilter