В последнее время я как-то подозрительно часто наблюдаю примитивнейшие однотипные и довольно легко решаемые проблемы на самых разных web-проектах. Разные базы, разные языки, разные сферы деятельности и схемы монетизации. Всех их объединяет одно — лозунг «бизнес не дает переписать». Продолжающийся или только-только оконченный этап рапид-разработки растущего и агрессивно отжимающего у конкурентов долю рынка проекта родил огромную кучу т.н. «говнокода». Сомнительные архитектурные решения либо уже приносят кучу проблем, либо обещают их в будущем, но работают. Поток новых требований не дает времени навести порядок даже в инфраструктуре, не говоря уже о коде. Если вам такая ситуация знакома — добро пожаловать под кат поностальгировать, поучиться чему-то новому и/или поучить нас. Кому поржать, а кому и поплакать.
«Это все только для хайлода» — скажет вдумчивый и прозорливый читатель. Плох тот веб-проект, который не мечтает стать популярным хайлодом.
Картинка не только для привлечения внимания, но и для иллюстрации реальной ситуации на одном проекте, страдающим некоторыми из нижеперечисленных проблем.
Проблемы №1: база.
Самые неприятные проблемы на любом web-проекте всегда связаны с базой данных. Все остальное мы легко умеем скейлить — от DNS-balancing до директивы upstream в конфиге nginx. «А как же кластеризация?» — спросит вдумчивый читатель. В этом-то и проблема. Я в третий раз наблюдаю кластер насилуемых баз данных. Дважды MySQL и однажды MongoDB. Индексы не настроены, таблицы (коллекции — какая разница?) не чистятся, зато проплачены недешевые серваки под кластер. И в основном эти серваки заняты разгребанием непроиндексированных данных и построением неиспользуемых индексов во имя энтропии.
Особенно распространена эта группа проблем в конторах, практикующих модную нынче тенденцию отделения backend-разработчиков от админов/DevOps/NOC.
Почему страшно держать базу в уставшем состоянии? Да потому что потеряете к чертям все: заказы, клиентов, SEO page rank. Да и зачем хостеру переплачивать?
Лично у меня, испорченного нищим детством, сразу же возникает крик души: не платите хостеру, заплатите лучше мне.
Еще из прекрасного: при наличии под ногами штормящей уставшей базы, и как следствие — несколькосекундный responce у веб-сервера практически на всех страницах — проводить перформанс импрувмент, старательно не трогая базу.
Проблема «n+1»
Оказывается, есть два крупных типа изнасилования базы, хотя еще пару месяцев назад об этом, самом смешном из них, лично я и не подозревал. Вы слышали о «проблеме n+1»? Я вроде припоминаю что-то такое в глубоком юниорском детстве. В жизни бы не поверил, что что-то такое может прорваться на прод коммерческого проекта. Проще всего проблему можно характеризовать псевдокодом:
list = db.query('SELECT * FROM products;')
for (item in list){
orders = db.query('SELECT * FROM orders WHERE product_id = ?;', {product_id: product.id});
...
}
Идентифицируется проблема легко. Берем полный access log веб-сервера и query log базы данных за один и тот же период, и тупо сравниваем по объему. Если 50MB access log превращаются в 20GB query log — проблема идентифицирована. Из плохих новостей — вам придется модифицировать код, а там врядли вас ждет что-то хорошее при таком отношении к базе.
Лозунг проблемы: webdev — это всего лишь преобразование действий пользователя в запросы к базе данных и вывод результатов. Все.
Наиболее сильно проблемой страдают go-программеры со своими горутинами. В меньшей степени она присуща адептам рельс, видимо привыкшим, что ActiveRecord на себя обычно берет такие проблемы. И встречается у php и js кодеров.
Сюда же можно отнести запросы вида:
SELECT p.*, ( SELECT count(*) FROM comments WHERE product_id = p.id) comment_count FROM products p WHERE author_id = ?;
Господа, это — не один запрос. Это тоже «n+1». Особенно прекрасный отсутствием LIMIT. Менять на JOIN (подзапрос с GROUP BY) — тоже не сахар, но если с индексами — жить можно. А вообще — это два запроса и сджоивание на уровне кода. Руками, если ваш ORM этого не умеет. Хотите — я дам либу для этого.
Проблема с индексами
Почему-то коллеги веб-разработчики отличаются завидным упорством в презрении к правильной индексации данных.
Идентифицируется проблема тоже просто: качаем, например, dev.mysql.com/doc/mysql-utilities/1.5/en/mysqlindexcheck.html или www.percona.com/doc/percona-toolkit/2.1/pt-duplicate-key-checker.html, запускаем, смотрим на длину списка лишних индексов. Если их много — база на проекте неухоженная, обязательно нужно проверять запросы. Если не нашли — включаем unindexed query log (в зависимости от типа базы по разному, гугл в помощь) и смотрим его. Если и тут ничего криминального — можем условно считать индексы проставленными аккуратно и перескакиваем к следующему шагу. Хотя опыт подсказывает что принцесса в большинстве случаев именно в этом замке.
Если же хоть один из вариантов дал выхлоп — готовьтесь к тяжелой и кропотливой работе. К сожалению, придется не только проставлять индексы где возможно, но и модифицировать код. Для MySQL, у которого в EXPLAIN SELECT нельзя получить «ROWS examined», вам понадобится полный query log (long_query_time = 0). Если эти данные грамотно агрегировать, то можно получить приятную статистику. Мне, например, нравится параметр sum(Rows Examined) — он показывает насколько данный тип запросов штормит базу. И еще — соотношение 95-х персентилей по параметрам «Rows Examined» vs «Rows Sent». Он показывает насколько данный тип запросов может быть оптимизирован. Аггрегатор можно написать самим или заюзать www.percona.com/doc/percona-toolkit/2.2/pt-query-digest.html. Но будьте предельно внимательны и аккуратны — ошибки в аггрегации приведут к куче потраченных впустую усилий. Применяйте декартов принцип универсального сомнения — любой механизм агрегации, в корректной работе которого вы не убедились лично, дОлжно считать потенциально глючным.
И не забывайте о ненулевой стоимости пересчетов индексов. Иногда лишний индекс страшнее отсутствующего. Минимизация количества используемых индексов — это первая задача, с которой столкнется, например, игнорирующий закон «если вы хотите использовать RDB таблицу для организации очереди — не используйте RDB таблицу». Но не последняя. Очередь прекрасно строится на механизмах «SELECT FOR UPDATE… SKIP LOCKED;».
Наиболее часто непонимание индексов встречается именно среди php-разработчиков. Если мне не будет лень — я таки закончу обучающую статью на тему «чего не могут индексы и почему» для любителей PHP.
Оптимизация размеров таблиц
Еще помните утверждение, что недешевые серваки заняты исключительно увеличением энтропии — пересчетом неиспользуемых индексов и разгребанием непроиндексированных данных? Добавьте сюда безосновательно разросшиеся таблицы — и вы получите картину, соперничающую своей безжалостной дорогостоящей бесполезностью со Сколково или с оруэловской войной с целью утилизации перепроизводства. Позицию №1 в рейтинге самых очевидных и технически бездарных из всех известных мне причин разрастания таблиц занимает желание хранить устаревшие данные вечно. Например, в интернет магазине почему-то хранят устаревшие и, возможно, уже давно не выпускающиеся продукты. Почему-то в основной, далеко не низконагруженной, таблице. И да, в той же самой базе.
Решение: как насчет дополнительной базы c префиксом zip? таблиц? mysqldump? backup?
Страшное: я встречал проекты, на которых из-за принципа «хранить вечно», нельзя было сделать ALTER TABLE из-за раздутости высоконагруженных централизованных таблиц. Как можно обеспечивать работу такой таблицы? Выдать штатным экстрасенсам бубны и истово молиться за здравие таблицы всем остальным? Зачем так жить?
Джоины
Запрос к 10 таблицам в вебе в продакшне может служить поводом для увольнения его автора и ревьювера, ИМХО. Господа, юнион, джоины и подзапросы 3-го уровня вложенности — это неплохой способ повыпендриваться. Или поискать что-то для себя в базе. Но никак не способ общения с бд хоть сколько-нибудь нагруженного проекта.
В том числе и из-за локов. Локи становятся проблемой задолго до того, как вы увидите дедлок в error логе. Да, количество локов можно сократить при единообразной сортировке условий типа parent_id IN(сортированный список id).
Ваша СУБД гарантировано не знакома с бизнес-логикой вашего же приложения и может только попробовать догадаться как именно будет лучше всего вытащить вам данные из десятка запрошенных таблиц. Знаете это только вы. Что, построить хеш-индексы в php и сджоинить два массива данных в памяти не сможете? А с библиотекой? Если не найдете — я покажу свою.
Проблемы с централизацией таблиц
В достаточно сложной асинхронной системе, в которую превращается любой крупный веб-проект, база и/или ее таблицы становятся ресурсами, поскольку являются блокерами для одних типов операций во время выполнения других типов. Например — пересчет индекса блокирует использование этого индекса и уж точно блокирует еще один пересчет.
Словом «ресурс» почему-то принято обозначать исключительно хардварные характеристики используемых систем — CPU, bandwidth, RAM. Но определить постоянный или пиковый недостаток таких ресурсов достаточно просто, корректные инструменты — munin/monit/sa+sar/htop и/или умеющий этим всем пользоваться грамотный админ — расскажут вам о ситуации все за небольшие деньги и в весьма приемлемые сроки.
Но вот относиться к таблицам в реляционной бд как к ресурсам никто почему-то не пытается. А ведь это — очевидное решение. Если UPDATE-запрос на таблице приводит к пересчету индекса, то ни один использующий этот индекс SELECT до окончания пересчета (ну или во время переключения, если вы верите в ровные руки авторов своей СУБД) выполнится, увы. В PostgreSQL при использовании immutable tuples любой UPDATE приводит к пересчету всех индексов таблицы. В MySQL на первый взгляд все не так страшно. Но грамотно организованная высоконагруженная таблица несет в себе именно один-два важнейших индекса, и большинство апдейтов все равно приводит к их пересчетам. Зачем вам одна таблица на все типы продуктов?
Решение: создайте таблицу products2, обеспечьте непересечение значений ее первичного ключа с исходной таблицей. Наслаждайтесь. Если же какой-либо более-менее массовый тип продукта отличается по структуре от остальных — сама нормализация бд требует от вас всунуть его в отдельную таблицу.
Страшное: люди, любящие централизовывать таблицы, отличаются тягой к цетрализации (микро)сервисов при построении архитектуры. Не для них, видите ли, сказано «не инкапсулируйте в сервисы, инкапсулируйте в классы». На выходе получается то же самое — куча bottleneck с неочевидными причинами существования мешают скейлить систему при увеличении объемов данных и/или нагрузки.
Лозунг: не инкапсулируйте в сервисы, инкапсулируйте в классы
Проблемы №2 — код
На любом проекте любой программист всегда считает весь код говном, кроме того, который он пишет прямо сейчас. И то — не всегда.
Видимо, количество хорошего кода во Вселенной — величина неизменная. Какой-нибудь закон сохранения, как с массой-энергией или с деньгами. Поэтому когда программист пишет хороший код — какой-нибудь код в этот момент становится плохим.
Так что сверх минимума заморачиваться на качестве кода не стоит. CodeSniffer плюс code review на первом уровне code quality должны удовлетворить любые субъективно-оценочные критерии «читабельности» кода.
Глубже — хуже: лишние слои абстракции почему-то не отсекаются на этапе code review. Так же как и over-pattern-usage. Вы знаете когда нужен синглтон? Чтобы ограничить доступность ресурса, доступ к которому уже организован через создаваемый неуникальный экземпляр класса. Если синглтон пишется с нуля — вы получите в итоге антипаттерн в большинстве случаев. Dependency Injection — это паттерн, позволяющий легко подсовывать моки при юнит-тестах и/или строить приложение с помощью конфиг файла, в стиле ZF 1.x. Иначе — атипаттерн. Repository + Entity — driven db access в 100% наблюдаемых мною случаев превращается в непригодный к дальнейшему использованию legacy код. Скорее всего причиной можно считать то, что репозитории используются в stateless режиме — как группа функций схожего назначения. В отличии от конкурирующего с ним паттерна ActiveRecord, в котором сразу ясно что именно является состоянием.
При этом, на удивление, более простые и реально помогающие правила SOLID не используются. Да что там SOLID — не используется даже инкапсуляция из определения OOP. Такое ощущение, что веб-девы помнят что такое инкапсуляция только на собеседованиях.
Решение: показать, как правильно выстроенная иерархия именно интерфейсов (помните, проектируй на уровне абстракции?) позволяет создавать гибкие и легко дописываемые приложения.
Лозунг проблемы: лишние слои абстракции — это отстой. Думать надо не паттернами, а головой.
И да, вы же сами программисты. Вы верите, что бывают программы без багов? Все используемые вами инструменты — это тоже слои абстракции, только архитектурно-сервисные. Webdev — это превращение действий пользователя в запросы к бд и вывод результатов. Объясните мне где в этой формуле прячется apache? Вам действительно нравится видеть рассеянный по куче .htaccess-файликов роутинг?
Проблемы №3 — фронт
Самый презираемый класс программистов — яваскриптовики. Шутки про this. Отличительные признаки проектов, у которых фронтенд в стиле а-ля нулевые — куча подключаемых файликов и/или JQuery в смеси c ExtJS, и/или свой MVC велосипед, навевающий на мысли о раннем backbone.js. То есть — абсолютно неподдерживаемый код, неоправданно дорогостоящий на любых модификациях, априори глючный и костыльный.
Решение: нормальный, es2015 javscript. Одностраничное приложение с роутингом, а не куча конфликтующих jQuery плагинов и условий. Единая точка входа. Постепенный эволюционный перезд, начиная с роутинга. Обоснованный и вдумчивый выбор архитектуроопределяющих технологий. Например, TypeScript противоечит самой идее JS: анархия, раздрай и полный бардак высококачественной рапид-разработки. ИМХО конечно же.
Проблемы №4 — окружение
Я не понимаю как веб-разработчик вообще может на рабочей машине держать не линукс. Да, интерфейс ужасен, шрифты слетают, окна уродливы. Иксы — прекрасный пример отвратительной архитектуры. Да, приходится думать и/или гуглить там, где в винде и/или макоси достаточно жмакать кнопочки. Ну так мы ж и не дизайнеры. Ну тут продолжать не стану, не холивара ради статью кропаю.
Я не понимаю как можно разработчику настраивать свое дев-окружение не самому. Да, пусть новичок пару дней сам пострадает с установкой проекта. Ну так сразу будет виден его уровень: по задаваемым вопросам и нерешенным самостоятельно проблемам. Точно так же новичок и сам многое о проекте поймет.
Я не понимаю как можно кодить с выключенными ворнингами. Бесплатное раннее обнаружение ошибок не нужно? Неужели действительно проще нанять еще один отдел тестеров?
Я не понимаю как можно жить без бета-площадки. Где же будут пастись эти лишние отделы тестировщиков? Как обеспечить zero-time-deployment без беты я не представляю тем более.
Я не понимаю как можно без zero-time-deployment. Контору че, не штрафуют за downtime сайта?
Я не понимаю как можно держать проект в продакшне, не имея настроенной системы интеграционных тестов. Что, сложно поставить Jenkins и всунуть скрипт, который раз в час будет логиниться / регистрироваться / проверять почту / покупать / продавать и паниковать при ошибке на почту / смс / хипчат? Неужели не хочется узнавать о проблемах не от клиента? Ах, да не штрафуют.
Я не понимаю как можно держать весь код вместе с конфигами в веб-руте? А вы точно везде deny from all прописали? Не тянет перепроверить?
Я не понимаю как можно раскручивать сайт, у которого 3 секунды время загрузки страницы. Это же дорого!
На этом перечень решаемых с технической точки зрения проблем заканчивается. Дальше начинаются…
Проблемы организационного характера.
Далеко не все из них решаемые, к сожалению.
Проблема №0: коммуникации.
С бизнесом можно разговаривать только на языке бабла. Он не восприимчив к красоте решений, нормализации базы, CAP-теоремам и прочим IT-ценностям. Он понимает деньги и сроки.
Решение: подобрать из беклога несколько таких задач, чтобы можно было и обосновано и честно заявить клиенту, что эти задачи будет дешевле имплементировать на переделанной системе, даже с учетом переделки. Если ты не можешь подобрать и обосновать перечень задач — уйди мальчик, тебе еще рано переписывать систему. Задачи, если что, лучше подбирать посвежее — клиента к ним тянуть будет сильнее. Да и не придется отвечать на вопрос «почему решились только сейчас?».
Лозунг: бизнес никогда не дает переписать. Пишите сразу нормально.
Проблема №1: управленчество
Говнокод сам по себе не рождается. Посмотрите на микромягких — авторитарный стиль управления Билла, позволяющего себе орать на подчиненных и срывать им сроки, родил настолько идиотские архитектурные и/или технологические решения, что даже конечные пользователи понимают, что страдают от их последствий. Отличителным признаком проблемы можно считать фразу «все программисты всегда хотят все переписывать. Мы не будем».
Что делать: убеждать. Например, через статьи на хабре. Или уходить. Это — единственная проблема, которую нельзя решить системно без топора.
К этим же проблемам можно отнести «централизацию людей» — самые толковые люди настолько заняты и контролируют столько вещей, что не могут вникнуть ни в одну из них. И вечно надо ждать принятия ими решения. Ну чем не bottleneck?
Решение: микрокоманды.
И да, продолжая параллели с архитектурой приложения — лишние слои абстракции здесь тоже sucks. Back in USSR, когда на каждого работягу два управленца. Ну так у них труба нефтегазовая была. И есть.
Чем меньше ушей на пути информации от источника требований до их осуществителя — тем лучше. А то получается как картинка-история с качелькой. И пара отделов лишних нахлебников — менеджеров, занятых войной с JIRA / Redmine и/или выполнением распоряжений других менеджеров.
Вот такой себе чеклист вышел. Простите за излишнюю экспрессию, накипело. Надеюсь, матерно-секретная версия замечаний улетит руководству. Если не струшу.
Самое главное — не опускать руки. Ведь, возможно, вы в самом интересном этапе — написание платформы устаканившегося денежного проекта. Прекрасная строка в резюме если что, а пока — замечательная финансовая подпитка. Предаставляете, например, как поднимет вашу ставку небрежно брошенная на собеседовании фраза «избавился от несколькоерабайтной базы»?
И конечно же все, кому есть чем дополнить и/или подкорректировать данный список — добро пожаловать в комментарии.
Автор: Сергей Семенов