3 частые проблемы производительности веб-приложений, первая проблема

Описываю самый распространённый анти-паттерн, который может быть в вашем коде прямо сейчас.

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

Дорогие запросы к базе данных внутри цикла.

Самый банальный пример и очень часто встречающийся паттерн в коде. Мы получаем откуда-то айдишники строк в БД (это может быть что угодно, перечень товаров на главной, содержимое корзины, номера статей с лайками пользователя) и далее по этим номерам делаем запросы и собираем массив/коллекцию:

Во-первых, тут присутствует красный флаг, о которым я писал ранее (https://dmitriynyu.com/perfomance-testing/) — нет ограничения на количества шагов в цикле. Код написан так, что мы вынуждены надеется, что количество айдишников, которое вернёт getIdsOfItemsInCart() будет небольшим, но на самом деле, мы этого точно не знаем.

Во-вторых, этот паттерн очень ловко маскируется. У вас в коде он может существовать не в таком явном виде, как в примере выше, это может быть например прикрепление свойств товара в интернет магазине к номенклатурной позиции. Менеджер добавил о 100 свойств к каждой детской игрушке — цвет, вес, форма и т.д., и теперь при каждом простом запросе вроде:

самописная и не очень умная ОРМ в глубине модели Item совершает 100 отдельных запросов к этим свойствам (это реальный случай).

В-третьих, этот вредный паттерн очень легко недооценить и проигнорировать. Например, внутри метода Item::first() может происходит кеширование. Полагаясь на это, вы подвергаете себя риску низкого быстродействия в случае, когда вдруг кэшу не хватит памяти и он решит почистить эти записи. Вполне реальна ситуация, когда в начале метод в цикле просто возвращал захардкоденные значения, а потом этот метод порефакторили, и он стал обращаться к базе. А про то, что метод вызывается до 100 раз в цикле, все забыли или не проверили.

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

«Тут я пришёл в комментарии»

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

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

Perfomance testing

Какие проблемы могут возникать от невнимательности и что с этим можно сделать.

Однажды я заметил, что godesigner.ru стал тормозить. Открываю страницу, а она не загружается. Правда, через несколько секунд страница отобразилась нормально. Потом через десять минут эта проблема снова повторилась. И ещё раз. Тут я понял, что есть проблема, посмотрел графики New Relic и ужаснулся — значение средней загрузки за двадцать минут возросло до 40 (должно быть не больше единицы)

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

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

Конечно, такого рода ошибка — когда происходит запрос сущностей из постоянно растущего пула возможных вариантов безо всяких ограничений может произойти где угодно. Вы можете написать неэффективный список на JavaScript, где при обновлении одной строчки браузер будет перерисовывать всё dom дерево целиком, а таких строчек может быть тысячи.

Проблема в том, что такой код пройдёт юнит-тесты на малых объемах данных. При этом на больших объемах тесты будут исполняться долго, что мешает процессу TDD. Получается, что нам нужны отдельные тесты на быстродействие, которые будут проверять наш код на взаимодействие с большим количеством данных. Кроме времени исполнения надо отслеживать и другие вещи, например потребление памяти. Однако результаты могут отличаться в зависимости от среды — на продакшн сервере среда может сильно отличаться от тестового сервера, а тесты в браузере могут вести себя не так, как у пользователя на компьютере.

Я до конца не уверен, как правильно создавать и производить тестирование кода на производительность. Вот парочка ссылок, которые я нашёл по теме, когда искал информацию в интернете:
https://testitquickly.com/2008/12/08/essential-about-performance-testing/
https://msdn.microsoft.com/en-us/library/bb924357.aspx

Ещё я нашёл пару книг, которые прочитаю:
https://www.amazon.com/Art-Application-Performance-Testing-Programmers/dp/0596520662
https://www.amazon.com/Performance-Testing-Guidance-Web-Applications/dp/0735625700

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