От любви до ненависти — один шаг, или как я разлюбил магию в ActiveRecord

в 8:30, , рубрики: activerecord, datamapper, eloquent, laravel, orm, php, никто не читает теги, Программирование, производительность

Недавно в одном из проектов возникла интересная проблема — весьма долго отдавались данные по 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».

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

Автор: Miraage

Источник



https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js