Text
                    

Дэниэл Нихтер Санкт-Петербург «БХВ-Петербург» 2023
УДК 004.65 ББК 32.973.26-018.1 Н69 Нихтер Д. Н69 Настройка производительности MySQL: Пер. с англ. — СПб.: БХВ-Петербург, 2023. — 336 с.: ил. ISBN 978-5-9775-1809-3 Книга посвящена практическим аспектам работы с MySQL. Рассмотрены приѐмы сегментирования баз данных, репликации, шардирования. Уделено внимание упорядочиванию транзакций, резервному копированию и бесшовному взаимодействию между предприятием и облаком, что способствует сохранению данных и их эксплуатационной надѐжности. Также исследованы разнообразные аналитические и мониторинговые инструменты и предложены проверенные методы, актуальные при развитии и долгосрочной поддержке MySQL и других РСУБД. Для администраторов баз данных и специалистов по серверной разработке УДК 004.65 ББК 32.973.26-018.1 Группа подготовки издания: Руководитель проекта Зав. редакцией Редактор Компьютерная верстка Оформление обложки Олег Сивченко Людмила Гауль Зоя Корниенко Ольги Сергиенко Зои Канторович © 2023 BHV Authorized Russian translation of the English edition of Efficient MySQL Performance ISBN 9781098105099 © 2022 Daniel Nichter. This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same. Авторизованный перевод с английского языка на русский издания Efficient MySQL Performance ISBN 9781098105099 © 2022 Daniel Nichter. Перевод опубликован и продается с разрешения компании-правообладателя O’Reilly Media, Inc. "БХВ-Петербург", 191036, Санкт-Петербург, Гончарная ул., 20 ISBN 978-1-098-10509-9 (англ.) ISBN 978-5-9775-1809-3 (рус.) © Daniel Nichter, 2022 © Перевод на русский язык, оформление. ООО "БХВ-Петербург", ООО "БХВ", 2023
Оглавление Предисловие ................................................................................................................... 11 Условные обозначения, используемые в этой книге .................................................................. 12 Благодарности ................................................................................................................................ 12 Глава 1. Время отклика на запрос ............................................................................. 13 Правдивая история ложной производительности....................................................................... 14 Краеугольный камень.................................................................................................................... 14 Отчет о запросе .............................................................................................................................. 15 Источники ........................................................................................................................... 16 Агрегация ............................................................................................................................ 18 Отчетность .......................................................................................................................... 20 Профиль запроса ........................................................................................................ 21 Отчет о запросе .......................................................................................................... 23 Анализ запросов ............................................................................................................................ 23 Метрики запросов ..............................................................................................................24 Время запроса............................................................................................................. 24 Время блокировки...................................................................................................... 26 Проверенные строки .................................................................................................. 30 Отправленные строки ................................................................................................ 31 Затронутые строки ..................................................................................................... 32 Полное сканирование таблицы ................................................................................. 33 Полное сканирование присоединенной таблицы .................................................... 33 Число временных таблиц на диске ........................................................................... 35 Количество запросов ................................................................................................. 36 Метаданные и приложение ................................................................................................ 36 Относительные значения ................................................................................................... 37 Среднее значение, процентиль и максимум .................................................................... 37 Улучшение времени отклика на запрос ....................................................................................... 40 Прямая оптимизация запросов .......................................................................................... 40 Косвенная оптимизация запросов ..................................................................................... 41 Когда оптимизировать запросы.................................................................................................... 42 Когда производительность влияет на клиентов .............................................................. 42 До и после изменений кода ............................................................................................... 42 Раз в месяц .......................................................................................................................... 43 MySQL: быстрее ............................................................................................................................ 43 Итоги............................................................................................................................................... 45 Практика: определение медленных запросов ............................................................................. 46
6 | Оглавление Глава 2. Индексы и индексация ................................................................................. 49 Отвлекающие маневры в теме производительности .................................................................. 51 Более быстрое и мощное оборудование!.......................................................................... 51 Тонкая настройка (тюнинг) MySQL ................................................................................. 52 Индексы MySQL: наглядное введение ........................................................................................ 54 Таблицы InnoDB являются индексами ............................................................................. 55 Методы доступа к таблице ................................................................................................ 59 Поиск по индексу ....................................................................................................... 59 Сканирование индекса............................................................................................... 60 Сканирование таблицы.............................................................................................. 61 Требование крайнего левого префикса ............................................................................ 63 EXPLAIN: план выполнения запроса ................................................................................ 65 WHERE ................................................................................................................................ 67 GROUP BY........................................................................................................................... 73 ORDER BY ........................................................................................................................... 78 Покрывающие индексы ..................................................................................................... 83 Соединение таблиц (join tables) ........................................................................................ 84 Индексация: думать, как MySQL .................................................................................................92 Узнайте запрос.................................................................................................................... 92 Поймите с помощью EXPLAIN.......................................................................................... 93 Оптимизируйте запрос ....................................................................................................... 94 Развертывание и проверка ................................................................................................. 95 Это был хороший индекс, пока... ................................................................................................. 96 Изменение запросов ...........................................................................................................96 Избыточные, повторяющиеся и неиспользуемые ........................................................... 97 Экстремальная селективность ........................................................................................... 98 Это ловушка! (Когда MySQL выбирает другой индекс) ................................................ 99 Алгоритмы соединения таблиц ..................................................................................................100 Итоги............................................................................................................................................. 101 Практика: поиск повторяющихся индексов .............................................................................. 102 Глава 3. Данные ........................................................................................................... 105 Три секрета ................................................................................................................................... 106 Индексы могут не помочь ............................................................................................... 106 Индексное сканирование......................................................................................... 107 Обнаружение строк.................................................................................................. 107 Соединение таблиц .................................................................................................. 108 Размер рабочего набора........................................................................................... 109 Чем меньше данных, тем лучше ..................................................................................... 110 Чем меньше QPS, тем лучше ........................................................................................... 110 Принцип наименьшего количества данных .............................................................................. 111 Доступ к данным ..............................................................................................................111 Возвращайте только необходимые столбцы ......................................................... 112 Уменьшите сложность запросов............................................................................. 112 Ограничьте доступ к строкам ................................................................................. 113 Ограничьте результирующий набор ...................................................................... 116 Избегайте сортировки строк ................................................................................... 118 Хранение данных ............................................................................................................. 118 Сохраняются только необходимые строки ............................................................ 119 Используется каждый столбец ............................................................................... 119
Оглавление | 7 Каждый столбец компактен и практичен .............................................................. 119 Каждое значение компактно и практично ............................................................. 121 Используется каждый вторичный индекс, а не дубликат .................................... 126 Хранятся только необходимые строки .................................................................. 128 Удаление или архивирование данных ....................................................................................... 129 Инструменты .................................................................................................................... 129 Размер пакета .................................................................................................................... 129 Конфликт блокировки строк ........................................................................................... 132 Пространство и время ...................................................................................................... 132 Парадокс бинарного лога................................................................................................. 133 Итоги............................................................................................................................................. 134 Практика: аудит доступа к данным запросов............................................................................ 134 Глава 4. Паттерны доступа........................................................................................ 137 MySQL ничего не делает ............................................................................................................ 138 Производительность дестабилизируется на пределе ............................................................... 139 Toyota и Ferrari ............................................................................................................................. 144 Паттерны доступа к данным ....................................................................................................... 145 Чтение/запись ................................................................................................................... 147 Пропускная способность ................................................................................................. 147 Возраст данных................................................................................................................. 148 Модель данных .................................................................................................................150 Изоляция транзакций ....................................................................................................... 150 Согласованность чтения .................................................................................................. 151 Конкурентность ................................................................................................................152 Доступ к строкам ..............................................................................................................153 Результирующий набор ................................................................................................... 153 Изменения в приложении ........................................................................................................... 154 Аудит кода ........................................................................................................................ 154 Разгрузка операций чтения .............................................................................................. 156 Реплика MySQL ....................................................................................................... 157 Кеш-сервер ............................................................................................................... 158 Очередь для операций записи ......................................................................................... 159 Секционирование данных................................................................................................ 161 Не используйте MySQL ................................................................................................... 162 Более быстрое и мощное оборудование? .................................................................................. 162 Итоги............................................................................................................................................. 164 Практика: опишите паттерн доступа ......................................................................................... 165 Глава 5. Шардирование ............................................................................................. 167 Почему единая база данных не масштабируется ...................................................................... 168 Рабочая нагрузка приложения ......................................................................................... 168 Тесты производительности синтетичны ........................................................................ 170 Операции записи ..............................................................................................................171 Изменения схемы ............................................................................................................. 173 Операции ........................................................................................................................... 173 Галька, а не валуны ..................................................................................................................... 174 Шардирование: краткое введение .............................................................................................. 175 Ключ шардирования ........................................................................................................ 176 Стратегии .......................................................................................................................... 177
8 | Оглавление Хеш............................................................................................................................178 Диапазон ................................................................................................................... 179 Поиск.........................................................................................................................181 Проблемы .......................................................................................................................... 182 Транзакции ............................................................................................................... 182 Объединения ............................................................................................................ 182 Кросс-шард запросы ................................................................................................ 182 Решардирование ....................................................................................................... 183 Ребалансировка ........................................................................................................ 184 Онлайн-изменения схемы ....................................................................................... 185 Альтернативы .............................................................................................................................. 185 NewSQL ............................................................................................................................. 185 Промежуточное программное обеспечение ................................................................... 187 Микросервисы ..................................................................................................................187 Не используйте MySQL ................................................................................................... 188 Итоги............................................................................................................................................. 188 Практика: четырехлетняя пригодность ..................................................................................... 189 Глава 6. Метрики сервера.......................................................................................... 191 Производительность запросов в сравнении с производительностью сервера ....................... 193 Нормальная и стабильная: лучшая база данных — это скучная база данных ....................... 195 Ключевые показатели производительности .............................................................................. 196 Поле метрик ................................................................................................................................. 197 Время отклика................................................................................................................... 198 Скорость (англ. rate) ......................................................................................................... 198 Объем использования (англ. utilization) ......................................................................... 199 Ожидание .......................................................................................................................... 200 Ошибки.............................................................................................................................. 201 Паттерны доступа .............................................................................................................201 Внутренние метрики ........................................................................................................ 202 Спектры ........................................................................................................................................ 202 Время отклика на запрос ................................................................................................. 204 Ошибки.............................................................................................................................. 205 Запросы ............................................................................................................................. 207 QPS ............................................................................................................................ 207 TPS ............................................................................................................................ 208 Чтение/запись ........................................................................................................... 210 Администрирование ................................................................................................ 210 Show ..........................................................................................................................211 Потоки и соединения ....................................................................................................... 211 Временные объекты ......................................................................................................... 214 Подготовленные инструкции .......................................................................................... 215 Плохие SELECT ................................................................................................................ 216 Пропускная способность сети ......................................................................................... 217 Репликация ........................................................................................................................ 217 Объем данных ................................................................................................................... 218 InnoDB ............................................................................................................................... 220 Длина списка истории (метрика) ............................................................................ 220 Взаимоблокировка ................................................................................................... 220 Блокировка строк ..................................................................................................... 221
Оглавление | 9 Пропускная способность данных ........................................................................... 222 IOPS........................................................................................................................... 223 Эффективность буферного пула ............................................................................. 225 Сброс страниц .......................................................................................................... 227 Лог транзакций ......................................................................................................... 234 Мониторинг и оповещение ......................................................................................................... 238 Разрешение........................................................................................................................ 238 Охота на миражи (пороговые значения) ........................................................................ 240 Оповещения о пользовательском опыте и объективных ограничениях...................... 241 Причина и следствие ........................................................................................................ 243 Итоги............................................................................................................................................. 245 Практика: анализ ключевых показателей производительности .............................................. 246 Практика: проверка оповещений и пороговых значений ........................................................ 247 Глава 7. Запаздывание репликации ........................................................................ 249 Основа........................................................................................................................................... 250 Источник — реплика........................................................................................................ 251 События бинарного лога.................................................................................................. 253 Запаздывание репликации ............................................................................................... 254 Причины ....................................................................................................................................... 256 Пропускная способность транзакций ............................................................................. 256 Восстановление после сбоя ............................................................................................. 257 Сетевые проблемы ........................................................................................................... 257 Риск: потеря данных .................................................................................................................... 258 Асинхронная репликация ................................................................................................ 258 Полусинхронная репликация .......................................................................................... 260 Уменьшение запаздывания: многопоточная репликация ........................................................ 262 Мониторинг.................................................................................................................................. 266 Время восстановления................................................................................................................. 268 Итоги............................................................................................................................................. 270 Практика: мониторинг субсекундного запаздывания .............................................................. 271 Глава 8. Транзакции ................................................................................................... 275 Блокировка строк ......................................................................................................................... 276 Блокировки индексной записи и следующего ключа ................................................... 278 Блокировка интервала ...................................................................................................... 282 Вторичные индексы ......................................................................................................... 285 Блокировка намерения вставки ....................................................................................... 288 MVCC и логи отмены .................................................................................................................. 291 Длина списка истории ................................................................................................................. 295 Распространенные проблемы .....................................................................................................297 Большие транзакции (размер транзакции) ..................................................................... 298 Длительные транзакции ................................................................................................... 299 Зависшие транзакции ....................................................................................................... 300 Заброшенные транзакции ................................................................................................ 301 Отчетность ................................................................................................................................... 302 Активные транзакции: последние ................................................................................... 302 Активные транзакции: итоги ........................................................................................... 306 Активная транзакция: история ........................................................................................ 306 Зафиксированные транзакции: итоги ............................................................................. 307
10 | Оглавление Итоги............................................................................................................................................. 309 Практика: оповещение о длине списка истории ....................................................................... 310 Практика: изучение блокировки строк ...................................................................................... 311 Глава 9. Другие проблемы ......................................................................................... 313 Сплит-брейн — величайший риск ............................................................................................. 313 Дрейф данных реален, но невидим ............................................................................................ 314 Не доверяйте ORM ...................................................................................................................... 315 Схемы постоянно меняются ....................................................................................................... 316 MySQL расширяет стандартный SQL ........................................................................................ 316 «Шумные соседи» ....................................................................................................................... 317 Приложения не завершаются корректно ................................................................................... 317 Высокопроизводительная MySQL — это сложно .................................................................... 318 Практика: определение барьеров для сплит-брейна................................................................. 319 Практика: проверка на дрейф данных ....................................................................................... 321 Практика: хаос ............................................................................................................................. 321 Глава 10. MySQL в облаке ......................................................................................... 323 Совместимость ............................................................................................................................. 324 Управление (администрирование базы данных) ...................................................................... 325 Сеть и хранилище... и... задержка .............................................................................................. 328 Производительность — это деньги ............................................................................................ 329 Итоги............................................................................................................................................. 331 Практика: попробуйте MySQL в облаке .................................................................................... 332 Предметный указатель ............................................................................................... 333 Об авторе ....................................................................................................................... 336 Об обложке .................................................................................................................... 336
Предисловие В литературе по MySQL существует пробел между знаниями базового уровня и уровня продвинутой производительности. Есть несколько книг о первом и одна книга о втором: «High Performance MySQL. 4th Edition» Сильвии Ботрос и Джереми Тинли (O’Reilly). Вы держите в руках первую книгу, которая восполняет пробел. Пробел существует, потому что MySQL сложна, и трудно обучать производительности, не обращая внимания на эту сложность. Это пресловутый слон в комнате. Но инженерам, использующим (не управляющим) MySQL, не нужно становиться экспертами по MySQL, чтобы достичь замечательной производительности. Для заполнения пробела эта книга бескомпромиссно эффективна — не обращайте внимания на слона; он дружелюбен. Эффективная производительность MySQL означает сфокусированность: изучение и применение только лучших практик и методов, которые напрямую влияют на достижение этой производительности. Фокус значительно сужает область сложности MySQL и позволяет мне показать вам гораздо более простой и быстрый путь через обширную и сложную область производительности MySQL. Путешествие начинается с первого предложения главы 1: «Производительность — это время отклика на запрос». Оттуда мы быстро переходим к индексам, данным, паттернам доступа и многому другому. По шкале от одного до пяти, где один — общее введение в тему для всех, а пять — это глубокое погружение для начинающих экспертов, эта книга оценивается от трех до четырех: глубоко, но далеко не дно. Я предполагаю, что вы опытный инженер, у которого есть базовые знания и опыт работы с реляционной базой данных (MySQL или что-то еще), поэтому не буду объяснять основы SQL или устройство базы данных. Я предполагаю, что вы опытный программист, ответственный за одно или несколько приложений, использующих MySQL, поэтому постоянно упоминаю приложение и надеюсь, что вы знаете детали своего приложения. Я также предполагаю, что вы знакомы с компьютерами в целом, поэтому свободно говорю об оборудовании, программном обеспечении, сетях и т. д. Поскольку эта книга посвящена производительности MySQL для инженеров, использующих MySQL, а не управляющих ею, при необходимости я ссылаюсь на конфигурацию MySQL, но не объясняю ее. Чтобы получить помощь в настройке MySQL, обратитесь к администратору базы данных того места, где вы работаете. Если у вас нет такого администратора, наймите консультанта по MySQL — есть много отличных консультантов с доступными вариантами контрактов. Вы также можете учиться по Справочному руководству по MySQL (https://oreil.ly/Y1W2r).
12 | Предисловие Это превосходное руководство, и эксперты постоянно им пользуются, так что вы в хорошей компании. Условные обозначения, используемые в этой книге В этой книге используются следующие типографские обозначения:  Курсив. Указывает на новые термины, имена файлов и расширения файлов.  Программный шрифт. Используется для листинга программ, а также внутри абзацев для ссылки на элементы программы, такие как имена переменных или функций, базы данных, типы данных, переменные среды, операторы и ключевые слова.  Полужирный программный шрифт. Показывает команды или другой текст, который пользователь должен ввести точно в таком же виде, в каком это приведено в книге.  Курсивный программный шрифт. Показывает текст, который следует заменить значе- ниями, заданными пользователем, или значениями, определенными контекстом. Этот элемент означает подсказку или предложение. Этот элемент означает общее примечание. Этот элемент указывает на предупреждение или предостережение. Благодарности Спасибо экспертам MySQL, рецензировавшим эту книгу: Вадиму Ткаченко, Фредерику Декампу и Фернандо Ипару. Спасибо экспертам по MySQL, рецензировавшим части этой книги: Маркосу Альбе, Жану-Франсуа Ганье и Кенни Грипу. Спасибо многим другим экспертам MySQL, которые помогали мне, учили меня и предоставляли возможности на протяжении многих лет: Питеру Зайцеву, Бэрону Шварцу, Райану Лоу, Биллу Карвину, Эмили Слокомб, Моргану Токеру, Шломи Ноаху, Джереми Коулу, Лоринасу Бивейнису, Марку Каллагану, Домасу Митузасу, Рональду Брэдфорду, Иву Трюдо, Свете Смирновой, Алексею Копытову, Джею Пайпсу, Стюарту Смиту, Александру Кузьминскому, Александру Рубину, Роману Винарю и снова Вадиму Ткаченко. Спасибо редакторам и всем, кто работал за кулисами. И спасибо моей жене Мун, которая поддерживала меня во время трудного процесса написания этой книги.
ГЛАВА 1 Время отклика на запрос Производительность — это время отклика на запрос. В данной книге эта идея рассматривается с разных точек зрения с одной-единственной целью: помочь вам достичь замечательной производительности MySQL. Эффективная производительность MySQL означает, что необходимо сосредоточиться на лучших практиках и методах в этом вопросе: никаких лишних деталей или глубоких внутренних элементов, которых требуют администраторы баз данных и эксперты. Я предполагаю, что вы занятой профессионал, который использует MySQL, а не управляет ею, и что вам нужно получить максимум результатов с наименьшими усилиями. Это не лень, это эффективность. Поэтому книга написана прямо и по существу. И к концу вы сможете добиться прекрасных результатов. Производительность MySQL — сложная и многогранная тема, но вам не нужно становиться экспертом, чтобы достичь хорошей производительности. Я сужаю рамки сложности MySQL, сосредоточившись на главном. Производительность MySQL начинается со времени отклика на запрос. Время отклика на запрос — это время, необходимое MySQL для выполнения запроса. Синонимичными терминами являются: время отклика, время запроса, время выполнения и (не совсем корректно) задержка запроса. Задержка запроса — это задержка, присущая системе. Время отклика на запрос не является задержкой, присущей MySQL; хотя оно включает в себя различные задержки: сети, хранилища и т. д. Отсчет времени начинается, когда MySQL получает запрос, и заканчивается, когда она отправляет результирующий набор клиенту. Время отклика на запрос включает множество этапов (шагов во время выполнения запроса) и ожиданий (ожидания блокировки (англ. lock waits), ожидания ввода-вывода и т. д.), но полная и подробная разбивка невозможна и не требуется. Как и во многих системах, базовое устранение неполадок и анализ выявят большинство проблем. Производительность повышается по мере уменьшения времени отклика на запрос. Улучшение времени отклика на запрос синонимично сокращению времени отклика на запрос. Эта глава — основа основ. В ней объясняется, что такое время отклика на запрос, а в последующих главах вы сможете узнать, как его улучшить. В книге семь основных разделов. Первый — мотивирующая и занимательная правдивая история. Во
14 | Глава 1 втором обсуждается, почему время отклика на запрос — краеугольный камень производительности MySQL. В третьем разделе описывается, как метрики запросов преобразуются в значимые отчеты: отчеты по запросам. Четвертый посвящен анализу запросов: использованию метрик запросов и другой информации для понимания того, как выполняются запросы. В пятом разделе описано, как улучшить время отклика на запросы: оптимизация запросов. Шестой дает честный и скромный график этой оптимизации. В седьмом освещается, почему MySQL не может просто взять и работать быстрее, почему необходима такая оптимизация. Правдивая история ложной производительности В 2004 году я работал в ночную смену в центре обработки данных, с двух часов дня до полуночи. Это была отличная работа по двум причинам. Во-первых, после пяти вечера здесь оставалась лишь горстка программистов, управляющих тысячами физических серверов для неизвестно какого числа клиентов и веб-сайтов — вероятно, десятков тысяч веб-сайтов. Мечта любого инженера-программиста. Во-вторых, на бесчисленном множестве серверов MySQL всегда возникали проблемы, требующие устранения. Золотая жила знаний и возможностей. Но в то время было недостаточно книг о MySQL, а также мало блогов и инструментов. (Хотя в тот же год у O’Reilly вышло первое издание книги «High Performance MySQL».) Следовательно, самым современным способом «решения» проблем с производительностью MySQL было «продать клиенту побольше оперативной памяти». Для продаж и управления это всегда работало, но для MySQL результаты были противоречивыми. Однажды поздно вечером я решил не продавать клиенту больше оперативной памяти, а вместо этого провести глубокое техническое погружение, чтобы найти и устранить истинную причину проблемы с производительностью MySQL в этом случае. База данных клиента поддерживала доску объявлений, которая под тяжестью своего успеха замедлилась до невозможности — нередкая тема даже 20 лет спустя. Короче говоря, я обнаружил, что в одном запросе отсутствует критический индекс. После правильной индексации запроса производительность значительно повысилась, и веб-сайт был сохранен. Это обошлось клиенту в ноль долларов. Не все проблемы с производительностью и не все решения настолько просты и изящны. Но мой скромный двадцатилетний опыт работы с MySQL научил меня (и многих других), что проблемы с производительностью MySQL часто решаются с помощью тех замечательных практик и методов, которые описаны в этой книге. Краеугольный камень Я не только администратор базы данных MySQL, но и инженер-программист, поэтому знаю, каково это — работать с MySQL в последней должности. Мы (разработчики программного обеспечения) просто хотим, чтобы все работало. Между
Время отклика на запрос | 15 отправкой функций и «тушением пожаров» у кого из нас остается время на улучшение производительности MySQL? И когда эта производительность низкая или, хуже того, внезапно падает, бывает трудно увидеть, что следует предпринять — слишком многое необходимо обдумать. С чего начать? Нужно ли больше RAM? Более мощные процессоры? Больше операций ввода-вывода хранилища? Или дело в недавних изменениях кода? (Факт: изменения кода могут вызвать проблемы с производительностью в будущем, иногда через несколько дней). Может, дело в «шумном соседе» (см. гл. 9)? Делают ли что-то с базой данных администраторы? А может, приложение стало популярным, и все по факту не так уж плохо? Для специалиста, чей опыт связан с приложениями, а не с MySQL, ситуация может быть обескураживающей. Чтобы уверенно двигаться вперед, начните с изучения времени отклика на запрос, потому что оно значимо и выполнимо. Это сильные качества, которые приводят к реальным решениям: Значимость Время отклика на запрос — единственная метрика, которая кого-то действительно интересует. Давайте будем честны: когда база данных работает быстро, никто не смотрит на нее и не задает вопросов. Почему? Потому что время отклика на запрос — единственный параметр, который мы ощущаем. Когда выполнение запроса занимает 7,5 секунды, мы 7,5 секунды пребываем в нетерпении. Этот запрос может проверять миллион строк, но мы не ощущаем проверку миллиона строк. Наше время драгоценно. Выполнимость Вы очень многое можете сделать, чтобы улучшить время отклика на запрос и всех осчастливить; вы даже держите в руках книгу, посвященную этому. (Будут ли и в дальнейшем люди держать в руках книги? Очень на это надеюсь.) Время отклика на запрос напрямую зависит от выполнения действий, потому что вы владеете кодом и, следовательно, можете изменять запросы. Даже если вы не являетесь владельцем кода (или не имеете к нему доступа), вы все равно можете косвенно оптимизировать время отклика на запрос. Раздел «Улучшение времени отклика на запрос» (см. ниже) касается прямой и косвенной оптимизации запросов. Сосредоточьтесь на улучшении времени отклика на запросы — краеугольном камне производительности MySQL. Не начинайте с того, чтобы решать проблему при помощи аппаратного обеспечения. Начните с метрик запросов, чтобы определить, что делает MySQL, затем проанализируйте и оптимизируйте медленные запросы, чтобы сократить время отклика, а затем повторите. Производительность обязательно улучшится. Отчет о запросе Метрики запросов предоставляют бесценную информацию о выполнении запросов: время отклика, время блокировки (англ. lock time), проверенные строки и т. д. Но метрики запросов, как и все метрики, представляют собой необработанные значе-
16 | Глава 1 ния, которые необходимо собирать, агрегировать и представлять таким образом, чтобы они были понятны (и удобочитаемы) для программистов. Вот что описано в этом разделе: как инструменты для метрик запросов преобразуют метрики запросов в отчеты о запросах. Но отчетность о запросах — всего лишь средство достижения цели, как поясняется в разделе «Анализ запросов» (см. ниже). Забегая вперед, скажу, что анализ запросов — настоящая работа: анализ метрик запросов (как они представлены) и другой информации с целью понимания процесса выполнения этих запросов. Чтобы улучшить производительность MySQL, нужно оптимизировать запросы. Чтобы оптимизировать запросы, нужно понимать, как они выполняются. И чтобы понять, как они выполняются, надо проанализировать их с помощью соответствующей информации, включая отчеты о запросах и метаданные. Но сначала следует разобраться в отчетах по запросам: они представляют собой набор метрик запросов, которые дают бесценные сведения о выполнении запросов. Дальнейшие два раздела расскажут вам о следующем:  Источники: метрики запросов берутся из двух источников и варьируются в зави- симости от дистрибутива и версии MySQL.  Агрегирование: значения метрик запроса группируются и агрегируются по нор- мализованным инструкциям SQL  Отчетность: отчеты по запросам организованы по профилю высокого уровня и отчету, специфичному для запроса. После этого вы будете готовы к «Анализу запросов» (см. ниже). Эта книга — не про администрирование баз данных, поэтому здесь не обсуждается настройка и конфигурация метрик запросов в MySQL. Я исхожу из того, что это уже сделано или будет сделано. Если нет, не волнуйтесь: спросите своего администратора базы данных, наймите консультанта или узнайте, как это сделать, прочитав руководство по MySQL. Источники Метрики запросов берутся из лога медленных запросов (англ. slow query log, https://oreil.ly/Glss3) или схемы производительности (англ. Perfomance Schema, https://oreil.ly/FNXRq). Соответственно первый источник представляет собой файл лога на диске, а второй — базу данных с одноименным названием: performance_ schema. Хотя они совершенно различны по своей природе (файл лога на диске отличается от таблиц в базе данных), оба предоставляют метрики запросов. Важное различие заключается в количестве: помимо времени отклика на запрос, которое отражают оба, число метрик колеблется от 3 до более чем 20. Название «лог медленных запросов» является историческим. Давным-давно MySQL регистрировала только запросы, выполнение которых занимало более N секунд, а минимальное значение для N равнялось 1. Старые версии MySQL не регистрировали запрос, выполнение которого занимало 900 миллисекунд, потому что это считалось «быстро». Лог медленных запросов действительно заслужил
Время отклика на запрос | 17 свое наименование. Сегодня минимальное значение равно нулю с разрешением в микросекундах. Когда значение равно нулю, MySQL регистрирует каждый выполненный запрос. Поэтому название немного вводит в заблуждение, но теперь вы знаете почему. Учитывая все обстоятельства, схема производительности — наилучший источник метрик запросов, поскольку существует во всех текущих версиях и дистрибутивах MySQL, работает локально и в облаке, предоставляет все девять метрик, описанных в разделе «Метрики запросов» (см. ниже), а также она наиболее согласована. Кроме того, схема производительности содержит множество других данных для глубокого анализа MySQL, поэтому ее полезность выходит далеко за рамки метрик запросов. Лог медленных запросов тоже является хорошим источником, но есть значительные отличия: MySQL Начиная с MySQL 8.0.14, включите системную переменную log_slow_extra (https://oreil.ly/ibfRK), и лог медленных запросов предоставит шесть из девяти метрик из раздела «Метрики запросов» (см. ниже), не хватает только Rows_affected, Select_scan и Select_full_join. Это все еще хороший источник, но по возможности используйте схему производительности. До MySQL 8.0.14, что включает MySQL 5.7, лог медленных запросов был пустым и предоставлял только Query_time, Lock_time, Rows_sent и Rows_examined. Вы все еще можете анализировать запросы только с помощью этих четырех метрик, но такой анализ гораздо менее глубокий. Следовательно, избегайте лога медленных запросов до MySQL 8.0.14 и вместо этого используйте схему производительности. Percona Server Дистрибутив Percona Server (https://oreil.ly/ILyh2) предоставляет значительно большее количество метрик в логе медленных запросов, когда настроена системная переменная log_slow_verbosity: все девять метрик, описанных в разделе «Метрики запросов» (см. ниже), и еще больше. Он также поддерживает выборку запросов (протоколирование процента запросов), когда настроена системная переменная log_slow_rate_limit, что полезно для загруженных серверов. Эти функции делают лог медленных запросов дистрибутива Percona Server отличным источником метрик. См. раздел «Slow Query Log» («Лог медленных запросов», https://oreil.ly/5JQ06) в руководстве по Percona Server для получения подробной информации. MariaDB Server MariaDB Server 10.x (https://oreil.ly/oeGJO) использует расширения лога медленных запросов дистрибутива Percona Server, однако есть два заметных отличия: системная переменная log_slow_verbosity настроена в MariaDB по-другому, и она не предоставляет метрику Rows_affected. В остальном это, по сути, то же самое, и к тому же отличный источник метрик запросов. См. раздел «Slow Query Log Extended Statistics» («Расширенная статистика лога медленных запросов»,
18 | Глава 1 https://oreil.ly/oOVe7) в базе знаний MariaDB для получения подробной информации. Лог медленных запросов отключен по умолчанию, но вы можете включить его динамически (без перезапуска MySQL). Схема производительности должна быть включена по умолчанию, хотя некоторые облачные провайдеры по умолчанию отключают ее. В отличие от лога медленных запросов, схема производительности не может быть включена в процессе: для этого вы должны перезапустить MySQL. Убедитесь, что используется и правильно настроен наилучший источник метрик запросов. Спросите вашего администратора базы данных, наймите консультанта или узнайте, как это сделать, прочитав руководство по MySQL. Лог медленных запросов может регистрировать все запросы, когда значение long_query_time (https://oreil.ly/NUmuA) равно нулю, но будьте осторожны: на загруженном сервере это может увеличить объем дисковых операций ввода-вывода и задействовать значительный объем дискового пространства. Агрегация Метрики запросов группируются и агрегируются по запросам. Это звучит очевидно, поскольку они так и называются, но некоторые инструменты метрик запросов могут группироваться по имени пользователя, имени хоста, базе данных и т. д. Такие альтернативные группировки встречаются исключительно редко и дают другой тип анализа запросов, поэтому я не рассматриваю их в этой книге. Поскольку время отклика на запрос — краеугольный камень производительности MySQL, группировка метрик запросов по запросам — лучший способ определить, у каких запросов наименьшее время отклика, что составляет основу отчетов и анализа запросов. Есть одна маленькая проблема: как однозначно идентифицировать запросы, чтобы определить группы, к которым они принадлежат? Например, системные метрики (процессор, память, хранилище и т. д.) группируются по имени хоста, поскольку имена хостов уникальны и значимы. Однако сами запросы не имеют каких-либо однозначно идентифицирующих свойств, таких как имя хоста. Решение: хеш SHA256 нормализованной инструкции SQL. Пример 1.1 показывает, как нормализуется инструкция SQL. Пример 1.1. Нормализация инструкции SQL SELECT col FROM tbl WHERE id=1 ❶ SELECT `col` FROM `tbl` WHERE `id` = ? ❷ f49d50dfab1c364e622d1e1ff54bb12df436be5d44c464a4e25a1ebb80fc2f13 ❸ ❶ Инструкция SQL (образец). ❷ Дайджест-текст ❸ Дайджест-хеш (англ. digest text, нормализованная инструкция SQL). (англ. digest hash, SHA-256 дайджест-текста).
Время отклика на запрос | 19 MySQL нормализует инструкции SQL для дайджест-текстов, затем вычисляет хеш SHA-256 дайджест-текста, чтобы получить дайджест-хеш. (Необязательно понимать весь процесс нормализации; достаточно знать, что она заменяет все значения на «?» и сворачивает несколько пробелов в один.) Поскольку дайджест-текст уникален, хеш дайджеста также уникален (несмотря на коллизии хешей). В руководстве по MySQL термин «дайджест» двусмысленно используется для обозначения либо дайджест-текста, либо дайджест-хеша. Поскольку дайджест-хеш вычисляется из дайджест-текста, получившаяся двусмысленность — всего лишь языковая неоднозначность, а не техническая ошибка. Пожалуйста, позвольте мне тоже уклониться от ответа и использовать слово «дайджест» для обозначения либо дайджест-текста, либо дайджест-хеша, когда техническая разница не имеет значения. В контексте метрик запросов происходит важный сдвиг в терминологии: термин «запрос» становится синонимом дайджест-текста. Изменение терминологии отражает изменение фокуса: метрики группируются по запросам, для чего запрос должен быть уникальным — что верно только для дайджестов. Инструкции SQL также называются образцами запросов (англ. query samples, или сокращенно образцами), и о них может быть сообщено, а может и не сообщаться. В целях безопасности большинство инструментов метрик запросов по умолчанию отбрасывают образцы (поскольку те содержат реальные значения) и сообщают только дайджест-тексты и хеши. Образцы необходимы для анализа запросов, поскольку вы можете применить к ним EXPLAIN (от англ. explain — объяснить). Это создает метаданные, необходимые для понимания того, как выполняется запрос. Некоторые инструменты метрик запросов так обрабатывают образец, затем отбрасывают его и выдают план EXPLAIN (результат использования инструкции EXPLAIN). Другие сообщают только об образце, что по-прежнему очень удобно: скопируйте и вставьте, чтобы применить EXPLAIN. Если у вас нет ни того, ни другого, тогда вручную извлеките образцы из кода или запишите их таким же образом, если потребуется. Еще два пояснения по терминологии, а затем я обещаю, что мы перейдем к более захватывающему материалу. Во-первых, терминология сильно различается в зависимости от инструмента метрик запросов, как показано в табл. 1.1. Таблица 1.1. Терминология метрик запросов Официальные (MySQL) Альтернативы Инструкция SQL Запрос Образец Запрос Дайджест-текст Класс, семейство, цифровой отпечаток, запрос Дайджест-хеш ID класса, ID запроса, подпись Во-вторых, еще один термин, заимствованный из Percona (https://www. percona.com), — аннотация запроса (англ. query abstract): инструкция SQL, сильно
20 | Глава 1 абстрагированный от своей команды SQL и списка таблиц. Пример 1.2 — это аннотация запроса для SELECT col FROM tbl WHERE id=1. Пример 1.2. Аннотация запроса SELECT tbl Аннотации запросов не уникальны, но полезны, потому что лаконичны. Обычно разработчикам достаточно увидеть аннотацию запроса, чтобы узнать полный запрос, который она представляет. Краткость — душа остроумия. (Уильям Шекспир) Важно понимать, что инструкции SQL нормализуются, потому что запросы, которые вы пишете, — не те, которые видите. В большинстве случаев это не проблема, потому что дайджест-тексты очень похожи на инструкции SQL. Но процесс нормализации освещает еще один важный момент: не создавайте динамически один и тот же логический запрос с разным синтаксисом, иначе он будет нормализован для разных дайджестов, и это будет отображаться как разные запросы. Например, в случае программно сгенерированного запроса, который изменяет оператор WHERE на основе пользовательского ввода: SELECT name FROM captains WHERE last_name = 'Picard' SELECT name FROM captains WHERE last_name = 'Picard' AND first_name = 'Jean-Luc' Эти два запроса могут быть логически одинаковыми для вас и приложения, но это разные запросы в отношении отчетности, поскольку нормализуются для разных дайджестов. Насколько мне известно, ни один инструмент метрик запросов не позволяет объединять запросы. И технически правильнее сообщать об этих запросах отдельно, потому что каждое условие (особенно с WHERE) влияет на выполнение и оптимизацию запроса. Нюанс: значения удаляются, поэтому следующие два запроса нормализуются до одного и того же дайджеста: -- SQL statements SELECT `name` FROM star_ships WHERE class IN ('galaxy') SELECT `name` FROM star_ships WHERE class IN ('galaxy', 'intrepid') -- Digest text SELECT `name` FROM `star_ships` WHERE `class` IN (...) Поскольку дайджест одинаков для обоих запросов, метрики для обоих запросов группируются, агрегируются и представляются как один запрос. Хватит о терминологии и нормализации. Давайте поговорим об отчетности. Отчетность Отчетность — это сложная задача, я бы даже сказал, вид искусства, потому что одно приложение может содержать сотни запросов. Каждый запрос содержит мно-
Время отклика на запрос | 21 жество метрик, и каждая метрика содержит несколько видов статистических данных: минимум, максимум, среднее значение, процентиль и т. д. Кроме того, каждый запрос содержит метаданные: образцы, планы инструкции EXPLAIN, структуры таблиц и тому подобное. Хранить, обрабатывать и представлять все эти данные непросто. Почти каждый инструмент метрики запросов представляет данные в двухуровневой иерархии: профиль запроса и отчет о запросе. Эти термины различаются в зависимости от инструмента метрики запросов, но вы легко узнаете каждый из них, когда увидите. Профиль запроса Профиль запроса показывает медленные запросы. Это организация верхнего уровня для составления отчетов по запросам — обычно первое, что вы видите в инструменте метрик запросов. В нем представлены дайджесты запросов и ограниченное подмножество метрик запросов, поэтому он и называется профилем. Термин «медленный» соотносится с термином «метрика сортировки» (англ. sort metric): совокупное значение метрики запроса, по которому запросы упорядочиваются. Первый упорядоченный запрос называется самым медленным, даже если метрикой сортировки не является время запроса (или любое другое время). Например, если метрикой сортировки является среднее количество отправленных строк, первый в ряду запрос по-прежнему называется самым медленным. Хотя метрикой сортировки может быть любая метрика запроса, универсальной по умолчанию является время запроса. Когда вы сокращаете время выполнения запроса, то освобождаете время, в течение которого MySQL сможет выполнить больше работы или сделать другую работу быстрее. Сортировка запросов по времени запроса показывает, с чего начать: с самых медленных и трудоемких запросов. Что не универсально, так это процесс агрегации времени запроса. Наиболее распространенными совокупными значениями являются: Общее время запроса Общее время запроса (англ. total query time) — это сумма времени выполнения (для каждого запроса). Это наиболее распространенное совокупное значение, поскольку отвечает на важный вопрос: на выполнение какого запроса уходит больше всего времени? Чтобы ответить на этот вопрос, инструмент метрики запросов суммирует все время, которое MySQL тратит на выполнение каждого запроса. Запрос с наибольшим общим временем как раз и будет самым медленным и трудоемким. Вот пример, объясняющий, почему это важно. Предположим, запрос A имеет время отклика 1 секунду и выполняется 10 раз. Запрос B имеет время отклика 0,1 секунды и выполняется 1000 раз. Запрос A имеет гораздо большее время отклика, но запрос B занимает в 10 раз больше времени: в общей сложности 10 секунд против 100 секунд соответственно. В профиле запроса, отсортированном по общему времени запроса, запрос B окажется самым медленным. Это существенно, потому что вы освободите больше времени для MySQL, оптимизируя запрос B.
22 | Глава 1 Процентное время выполнения Процентное время выполнения (англ. percentage execution time) — это общее время запроса (для каждого запроса), деленное на общее время выполнения (все запросы). Например, если запрос C имеет общее время запроса 321 мс, а запрос D имеет общее время запроса 100 мс, то общее время выполнения составляет 421 мс. По отдельности запрос C составляет (321 мс / 421 мс) × 100 = 76,2% от общего времени выполнения, а запрос D составляет (100 мс / 421 мс) × 100 = = 23,8% от общего времени выполнения. Другими словами, MySQL потратила 421 мс на выполнение запросов, 76,2% из которых было потрачено на выполнение запроса C. В профиле запроса, отсортированном по процентному времени выполнения, запрос C будет самым медленным запросом. Процентное время выполнения используется некоторыми инструментами метрик запросов, но не всеми. Загрузка запроса Загрузка запроса (англ. query load) — это общее время запроса (для каждого запроса), деленное на время такта (англ. clock time), где время такта — это количество секунд во временном диапазоне. Если диапазон времени составляет 5 минут, то время такта составляет 300 секунд. Например, если запрос E имеет общее время запроса 250,2 секунды, то его загрузка составляет 250,2 с / 300 с = = 0,83; и если запрос F имеет общее время запроса 500,1 секунды, то его загрузка составляет 500,1 с / 300 с = 1,67. В профиле запроса, отсортированном по этому параметру, запрос F будет самым медленным, поскольку его загрузка наибольшая. Загрузка соотносится со временем, но также тонко указывает на конкурентность: когда несколько экземпляров запроса выполняются одновременно. Загрузка запроса менее 1,0 означает, что в среднем он выполняется не в конкурентном режиме. Загрузка запроса, превышающая 1,0, указывает на конкурентность запросов. Например, загрузка запроса 3,5 означает, что в любой момент вы, скорее всего, увидите 3,5 экземпляра выполняемого запроса. (На самом деле 3 или 4 экземпляра запроса, поскольку не может быть 0,5 экземпляра запроса). Чем выше загрузка запроса, тем больше вероятность конфликта, если запрос обращается к тем же или соседним строкам. Загрузка запроса, превышающая 10, высока, и это, скорее всего, будет медленный запрос, но есть исключения. Я пишу эти строки и просматриваю запрос с загрузкой 5962. Как такое возможно? Ответ ищите в разделе «Доступ к данным» (см. гл. 3). Когда метрика сортировки использует нетемпоральную (невременную) метрику запроса — например, отправленные строки, другое совокупное значение (среднее, максимальное и т. д.) может иметь смысл в зависимости от того, что вы пытаетесь диагностировать. Это встречается гораздо реже, чем общее время запроса, но иногда выявляет интересные запросы, которые стоит оптимизировать.
Время отклика на запрос | 23 Отчет о запросе Отчет о запросе (англ. query report) показывает все, что можно узнать об одном запросе. Это организация второго уровня для отчетов о запросах, доступ к которой обычно осуществляется путем выбора медленного запроса в профиле запроса. В ней представлены все метрики запросов и метаданные. В то время как профиль запроса сообщает вам что-то, когда вы просто смотрите на него (например, какие запросы являются самыми медленными), отчет о запросе представляет собой упорядоченный информационный дамп, применяемый для анализа запросов. Таким образом, чем больше информации, тем лучше, потому что это помогает вам понять, как выполняется запрос. Отчеты о запросах различаются в зависимости от инструмента метрики запросов. Простой минимальный отчет включает в себя все метрики запроса из источника и базовую статистику для этих метрик: минимум, максимум, среднее значение, процентиль и т. д. Подробный отчет включает метаданные: образцы запросов, планы EXPLAIN, структуры таблиц и многое другое. (Образцы могут быть отключены в целях безопасности, поскольку содержат реальные значения). Несколько инструментов метрик запросов идут дальше, предоставляя дополнительную информацию: графики метрик, гистограммы (распределения), обнаружение аномалий, сравнение временных сдвигов (текущие значения по сравнению с данными прошлой недели), заметки разработчика, извлечение ключевого значения комментария SQL и т. д. Для анализа запросов требуются только метрики запросов в отчете. Метаданные могут быть собраны вручную. Если используемый инструмент метрик запросов сообщает лишь о метриках запросов, не волнуйтесь: это только начало, но еще вам нужно будет вручную собрать, по крайней мере, планы EXPLAIN и структуры таблиц. Итак, имея на руках отчет о запросах, вы готовы к их анализу. Анализ запросов Целью анализа запросов является понимание того, как они выполняются, а не решение проблемы большого времени отклика. Вы можете удивиться, но решить такую проблему можно после анализа запросов, во время их оптимизации. Во-первых, вам нужно понять, что вы пытаетесь изменить: в данном случае — выполнение запроса. Выполнение запроса — это история с началом, серединой и концом: нужно прочитать все три части, чтобы понять историю. Как только вы поймете, как MySQL выполняет запрос, вы поймете, как его оптимизировать. Понимание через анализ, затем действие через оптимизацию. Я помогал многим программистам анализировать запросы, и основная трудность заключалась не в понимании метрик, а в том, что зачастую мы застревали в анализе, пристально вглядываясь в цифры и ожидая откровения. Не застревайте. Внимательно просмотрите все метрики и метаданные (прочитайте всю историю целиком), затем обратите свое внимание на оптимизацию запросов с целью улучшения времени отклика.
24 | Глава 1 В следующих разделах рассматриваются ключевые аспекты эффективного и глубокого анализа запросов. Иногда причина большого времени отклика настолько очевидна, что анализ больше похож на твит, нежели чем на историю. Но когда это не так (когда анализ читается как дипломная работа по французскому экзистенциализму), понимание этих аспектов поможет вам найти причину и определить решение. Метрики запросов Из раздела «Источники» (см. выше) вы уже знаете, что метрики запросов различаются в зависимости от источника, дистрибутива MySQL и версии MySQL. Все метрики важны, поскольку помогают увидеть, как выполняются запросы, но девять метрик, подробно описанных в следующих разделах, необходимы для анализа каждого из них. Схема производительности предоставляет все девять основных метрик запроса. Названия метрик запроса различаются также в зависимости от источника. В логе медленных запросов время запроса обозначено как Query_time; но в схеме производительности это TIMER_WAIT. Я не использую ни то ни другое условное обозначение. Вместо этого я обращаюсь к понятным для человека названиям, таким как время запроса и отправленные строки. В отчетах о запросах также почти всегда используются названия, понятные людям. Время запроса Время запроса (англ. query time)— самая важная метрика, и это вы уже знаете. Чего вы, возможно, не знаете — что время запроса включает в себя еще одну метрику: время блокировки. Время блокировки является неотъемлемой частью времени запроса, поэтому неудивительно, что последнее включает в себя первое. Что удивительно, так это то, что время запроса и время блокировки являются двумя единственными метриками, основанными на времени, за одним исключением: лог медленных запросов Percona Server содержит метрики для времени чтения InnoDB, времени ожидания блокировки строк и времени ожидания очереди. Время блокировки важно, но есть досадная техническая ошибка: оно является точным только в логе медленных запросов. Подробнее об этом позже. Используя схему производительности, вы можете видеть многие (но не все) части процесса выполнения запроса. Это не по теме и выходит за рамки данной книги, но, если вам нужно будет копнуть глубже, вы знаете, где искать. MySQL обрабатывает ошеломляющее количество событий, которые в руководстве определяются как «все действия сервера, которые требуют времени и были настроены таким образом, чтобы можно было собирать информацию о времени». События организованы в иерархию: транзакции (англ. transactions) └── инструкции (англ. statements) └── этапы (англ. stages) └── ожидания (англ. waits)
Время отклика на запрос | 25 Транзакции Транзакции являются событием верхнего уровня, поскольку каждый запрос выполняется в транзакции (см. главу 8). Инструкции Инструкции — это запросы, к которым применяются метрики запросов. Этапы Этапы — это «шаги в процессе выполнения инструкции, такие как синтаксический анализ инструкции, открытие таблицы или выполнение операции сортировки файлов». Ожидания Ожидания — это «события, которые требуют времени». (Это определение меня забавляет. Это тавтологично и притом на редкость удивительно в своей простоте). В примере 1.3 показаны этапы для одной инструкции UPDATE (начиная с MySQL 8.0.22). Пример 1.3. Этапы для одной инструкции UPDATE stage stage/sql/starting stage/sql/Executing hook on trx stage/sql/starting stage/sql/checking permissions stage/sql/Opening tables stage/sql/init stage/sql/System lock stage/sql/updating stage/sql/end stage/sql/query end stage/sql/waiting handler commit stage/sql/closing tables stage/sql/freeing items stage/sql/logging slow query stage/sql/cleaning up source:line init_net_server_extension.cc:101 rpl_handler.cc:1120 rpl_handler.cc:1122 sql_authorization.cc:2200 sql_base.cc:5745 sql_select.cc:703 lock.cc:332 sql_update.cc:781 sql_select.cc:736 sql_parse.cc:4474 handler.cc:1591 sql_parse.cc:4525 sql_parse.cc:5007 log.cc:1640 sql_parse.cc:2192 time (ms) 0,019 0,001 0,008 0,004 0,102 0,007 0,072 10722,618 0,003 0,002 0,034 0,015 0,061 0,094 0,002 Реальный результат выполнения более сложен; я упростил его для удобства чтения. Инструкция UPDATE выполняется в 15 этапов. Фактическое выполнение UPDATE было восьмым этапом: stage/sql/updating. Было 42 ожидания, но я удалил их из выходных данных, потому что они слишком далеки от темы. События схемы производительности (транзакции, инструкции, этапы и ожидания) — это мелкие детали выполнения запроса. Метрики запросов применяются к инструкциям. Если вам нужно углубиться в запрос, загляните в схему производительности.
26 | Глава 1 Эффективность — наш modus operandi, поэтому старайтесь не потеряться в схеме производительности (если, конечно, вы не преследуете такую цель, чего обычно не бывает). Времени на запрос достаточно. Время блокировки Время блокировки (англ. lock time) — это время, затраченное на получение блокировок в процессе выполнения запроса. В идеале время блокировки составляет ничтожный процент от времени запроса, но значения являются относительными (см. ниже, раздел «Относительные значения»). Например, в одной чрезвычайно оптимизированной базе данных, которой я управляю, время блокировки составляет от 40 до 50% времени запроса для самого медленного запроса. Звучит ужасно, правда? Все в порядке: у самого медленного запроса максимальное время запроса составляет 160 микросекунд, а максимальное время блокировки — 80 микросекунд, а база данных выполняет более 20 000 запросов в секунду (QPS). Хотя значения относительны, могу с уверенностью сказать, что время блокировки, превышающее 50% времени запроса, является проблемой, потому что MySQL следует тратить основное количество времени на выполнение работы, а не на ожидание. Теоретически идеальное выполнение запроса имело бы нулевое время ожидания, но это невозможно из-за общих ресурсов, конкурентности и задержек, присущих системе. Но мечтать не вредно. Подсистемы хранения данных MySQL и блокировка данных Прежде чем я подробнее расскажу о времени блокировки и блокировке в целом, позвольте мне прояснить кое-какие базовые вещи. У MySQL есть много подсистем хранения данных (а также целая история этих подсистем), но не буду вас этим утомлять. Подсистема хранения данных по умолчанию — InnoDB. Другие возможные подсистемы хранения: MyISAM, MEMORY, TempTable (https:// oreil.ly/Ubz65), Aria (https://oreil.ly/VVAjG) с MariaDB, MyRocks (https://myrocks.io) с Percona Server и MariaDB, XtraDB (https://oreil.ly/jrGlq) с Percona Server и многие другие. (Забавный факт: cхема производительности реализована как подсистема хранения данных.) В этой книге по умолчанию будет подразумеватьcя InnoDB, если не указано иное. Существуют блокировки таблиц и блокировки строк. Сервер (MySQL) управляет таблицами и блокировками таблиц. Таблицы создаются с помощью подсистемы хранения (по умолчанию InnoDB), но не зависят от нее, т. е. вы можете преобразовать таблицу из одной подсистемы хранения в другую. Блокировкой на уровне строк управляет подсистема хранения, если там это поддерживается. MyISAM, например, не поддерживает блокировку на уровне строк, поэтому управляет доступом к данным с помощью блокировки таблиц. InnoDB поддерживает блокировку на уровне строк, поэтому управляет доступом к данным с помощью этого типа блокировки. Поскольку InnoDB является подсистемой хранения данных по умолчанию, подразумевается блокировка на уровне строк, если не указано иное. В InnoDB также есть особые блокировки таблиц, называемые блокировками намерений, но они не важны для нашего обсуждения. Существуют блокировки метаданных, управляемые сервером, которые контролируют доступ к схемам, таблицам, сохраненным программам и многому другому. В то время
Время отклика на запрос | 27 как блокировки таблиц и блокировки строк контролируют доступ к табличным данным, блокировки метаданных контролируют доступ к структурам таблиц (столбцам, индексам и т. д.), чтобы предотвратить изменения во время обращения запросов к таблицам. Каждый запрос вызывает блокировку метаданных для каждой таблицы, к которой он обращается. Блокировки метаданных снимаются в конце транзакции, а не запроса. Помните: InnoDB и блокировка на уровне строк подразумеваются по умолчанию, если не указано иное. Помните нелепый технический сбой, о котором упоминалось ранее? Так вот: время блокировки в схеме производительности не включает ожидания блокировки строк, а только ожидания блокировки таблиц и блокировки метаданных. Ожидания блокировки строк являются наиболее важной частью времени блокировки, что делает время блокировки из схемы производительности практически бесполезным. Напротив, время блокировки из лога медленных запросов включает все ожидания блокировки: метаданных, таблицы и строки. Время блокировки из любого источника не указывает на тип блокировки ожидания. В схеме производительности это, безусловно, ожидание блокировки метаданных, а в логе медленных запросов, это, вероятно, ожидание блокировки строк, но ожидание блокировки метаданных тоже возможно. Время блокировки в схеме производительности не включает ожидания блокировки строк. Блокировки в основном используются для создания записей (INSERT, UPDATE, DELETE, REPLACE), поскольку строки должны быть заблокированы, прежде чем смогут быть записаны. Время отклика на запись частично зависит от времени блокировки. Количество времени, необходимое для блокировки строк, зависит от конкурентности: сколько запросов одновременно обращаются к одним и тем же (или соседним) строкам. Если строка имеет нулевую конкурентность (доступ осуществляется только по одному запросу за раз), то время блокировки исчезающе мало. Но если строка является «горячей» (жаргонное слово для строк, к которым обращаются очень часто), тогда время блокировки может составлять значительный процент от времени отклика. Конкурентность — один из нескольких паттернов доступа к данным (см. гл. 4, раздел «Паттерны доступа к данным»). Что касается операций чтения (SELECT), существуют операции чтения без блокировки и с блокировкой (https://oreil.ly/WcyD3). Различить их легко, потому что есть только два вида блокирующего чтения: SELECT...FOR UPDATE и SELECT...FOR SHARE. Если у вас ни тот ни другой, тогда SELECT является инструкцией без блокировки, и это тоже нормально. Хотя SELECT...FOR UPDATE и SELECT...FOR SHARE — это единственные инструкциями чтения с блокировкой, не забывайте об инструкциях записи с опциональным SELECT. В инструкциях SQL, представленных ниже, SELECT получает разделяемые блокировки строк в таблице s:
28 | Глава 1 INSERT...SELECT FROM s REPLACE...SELECT FROM s UPDATE...WHERE...(SELECT FROM s) CREATE TABLE...SELECT FROM s Строго говоря, эти инструкции SQL относятся к операциям записи, а не чтения, но опциональный SELECT получает разделяемые блокировки строк в таблице s. См. раздел «Locks Set by Different SQL Statements in InnoDB» («Блокировки, устанавливаемые различными инструкциями SQL в InnoDB», https://oreil.ly/SJXcq) в руководстве по MySQL для получения подробной информации. Следует избегать блокировок чтения, особенно SELECT... FOR UPDATE, потому что они не масштабируются. Как правило, они вызывают проблемы, и обычно существует решение без блокировки для достижения того же результата. Что касается времени блокировки, чтение с блокировкой похоже на операцию записи: оно зависит от конкурентности. Будьте осторожны с SELECT... FOR SHARE: разделяемые блокировки совместимы с другими разделяемыми блокировками, но несовместимы с монопольными : разделяемые блокировки препятствуют операции записи в одних и тех же (или соседних) строках. Для чтения без блокировок, хотя блокировки строк и не получены, время блокировки не будет равно нулю, поскольку получены блокировки метаданных и таблиц. Но получение этих двух вещей, по идее, должно быть очень быстрым: менее 1 миллисекунды. К примеру, еще одна база данных, которой я управляю, выполняет более 34 000 запросов в секунду (англ. queries per second, QPS), но самый медленный запрос — неблокирующий SELECT, который осуществляет полное сканирование таблицы, считывая шесть миллионов строк при каждом прогоне, с очень высоким уровнем конкурентности: загрузка запроса 168. Несмотря на эти большие значения, его максимальное время блокировки составляет 220 микросекунд, а среднее время блокировки — 80 микросекунд. Чтение без блокировки не означает беспрепятственного чтения. Запросы SELECT должны получать разделяемые блокировки метаданных (англ. metadata locks, MDL) для всех доступных таблиц. Как обычно с блокировками, разделяемые MDL совместимы с другими разделяемыми MDL, но одна монопольная MDL блокирует все остальные MDL. ALTER TABLE — это обычная операция, которая получает монопольную MDL. Даже при использовании ALTER TABLE...ALGORITHM=INPLACE, LOCK=NONE или сторонних онлайнинструментов для изменения схемы, таких как pt-online-schema-change (https:// oreil.ly/EzcrU) и gh-ost (https://oreil.ly/TeHjG), в конце необходимо получить монопольную MDL, чтобы заменить старую структуру таблицы на новую. Хотя обмен таблицами происходит очень быстро, он может вызвать заметные сбои при высокой загрузке MySQL, поскольку весь доступ к таблицам блокируется, пока сохраняется монопольная MDL. Эта проблема проявляется как сбой во время блокировки, особенно для инструкций SELECT. SELECT может блокировать ожидание блокировки метаданных.
Время отклика на запрос | 29 Возможно, именно блокировка является самым сложным и тонким аспектом MySQL. Чтобы избежать погружения в пресловутую кроличью нору, позвольте мне перечислить пять моментов, но на время отложить их объяснение. Простое осознание этих нюансов значительно повысит ваше мастерство владения MySQL: 1. Время блокировки может быть значительно больше, чем значение innodb_lock_ wait_timeout (https://oreil.ly/HlWwX), поскольку эта системная переменная применяется к каждой блокировке строки. 2. Уровни блокировки и изоляции транзакций взаимосвязаны. 3. InnoDB блокирует каждую строку, к которой обращается, включая строки, которые не записывает. 4. Блокировки снимаются при фиксации или откате транзакции, а иногда и во время выполнения запроса. 5. InnoDB имеет различные типы блокировок: блокировка индексной записи (англ. record lock), блокировка интервала (англ. gap lock), блокировка следующего ключа (англ. next-key lock) и многое другое. «Блокировка строк» подробно описана в главе 8. А пока давайте соберем все это вместе и посмотрим, как время запроса включает в себя время блокировки. На рис. 1.1 показаны блокировки, полученные и снятые в ходе выполнения запроса. Рис. 1.1. Время блокировки в ходе выполнения запроса Метки с 1-й по 10-ю означают события и детали, относящиеся к блокировке: 1. Получить разделяемую блокировку метаданных в таблице. 2. Получить монопольную по намерениям (англ. Intention exclusive, IX) блокировку таблицы. 3. Получить блокировку строки 1. 4. Обновить (записать) строку 1. 5. Получить блокировку строки 2. 6. Снять блокировку строки 2. 7. Снять блокировку строки 3. 8. Обновить (записать) строку 3. 9. Зафиксировать транзакцию. 10. Снять все блокировки.
30 | Глава 1 Есть два интересных момента: 1. Время блокировки из схемы производительности включает только метки 1 и 2. Из лога медленных запросов оно включает метки 1, 2, 3, 5 и 7. 2. Хотя строка 2 и заблокирована (метка 5), она не записана, и ее блокировка снимается (метка 6) до совершения транзакции (метка 9). Это может произойти, но не всегда. Все зависит от запроса и уровня изоляции транзакции. Итак, вы получили много информации о времени блокировки и блокировке и теперь хорошо подготовлены к тому, чтобы научиться оценивать время блокировки при анализе запросов. Проверенные строки Проверенные строки (rows_examined) — это количество строк, к которым MySQL обращалась для поиска совпадающих строк. Данная метрика указывает на селективность (избирательность) запроса и индексов. Чем более избирательны оба метода, тем меньше времени MySQL тратит на проверку несоответствующих строк. Это относится к операциям чтения и записи, за исключением INSERT, если это не инструкция INSERT...SELECT. Чтобы понять, что такое проверенные строки, давайте рассмотрим два примера. Во-первых, воспользуемся следующей таблицей, t1, и тремя строками: CREATE TABLE `t1` ( `id` int NOT NULL, `c` char(1) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; id 1 2 3 c a b c Столбец id является первичным ключом, а столбец c не индексируется. Запрос SELECT c FROM t1 WHERE c = 'b' соответствует одной строке, но проверяет три, поскольку в столбце c нет уникального индекса. Следовательно, MySQL понятия не имеет, сколько строк соответствует оператору WHERE. Мы с вами видим, что совпадает только одна строка, но у MySQL нет глаз, у нее есть только индексы. В отличие от этого, запрос SELECT c FROM t1 WHERE id = 2 соответствует только одной строке и проверяет только ее, потому что для столбца id существует уникальный индекс (первичный ключ), а условие таблицы использует весь индекс. Теперь, образно говоря, MySQL может увидеть, что совпадает лишь одна строка, и проверяет только ее одну. Об индексах и индексации, которые объясняют условия таблицы, и многом другом рассказывается в главе 2.
Время отклика на запрос | 31 Для второго примера давайте используем следующую таблицу, t2, и семь строк: CREATE TABLE `t2` ( `id` int NOT NULL, `c` char(1) NOT NULL, `d` varchar(8) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; id 1 2 3 4 5 6 7 c a a a a b b c d apple ant acorn apron banana bike car Столбец id такой же, как и раньше (первичный ключ). Столбец c имеет неоднородный индекс. Столбец d не индексируется. Сколько строк будет проверять запрос SELECT d FROM t2 WHERE c? Ответ таков: четыре. MySQL использует неоднородный индекс в столбце c для поиска строк, соответствующих условию c = 'a', и это соответствует четырем строкам. И чтобы соответствовать другому условию, d = 'acorn', MySQL проверяет каждую из этих четырех строк. В результате запрос проверяет четыре строки, но соответствует только одной (и возвращает лишь ее). Нередко можно обнаружить, что запрос проверяет больше строк, чем ожидалось. Причина обычно заключается в селективности запроса или индексов (или и того и другого), но иногда это бывает из-за того, что таблица стала намного больше, чем ожидалось, поэтому для проверки и требуется гораздо больше строк. В главе 3 мы с вами проверим, что углубились во все детали. Проверенные строки рассказывают только половину истории. Другая половина — это отправленные строки. Отправленные строки Отправленные строки (rows_sent) — это количество строк, возвращенных клиенту: размер результирующего набора. Отправленные строки наиболее значимы среди проверенных строк. Отправленные строки = Проверенные строки Идеальный случай — когда количество отправленных строк и проверенных строк равно, само значение относительно невелико, особенно в процентах от общего числа строк, а время отклика на запрос приемлемо. Например, 1000 строк из таблицы с миллионом строк — это разумные 0,1%. Это идеально
32 | Глава 1 при условии, что время отклика также приемлемо. Но 1000 строк из таблицы, содержащей всего 10 000 строк, — уже сомнительные 10%, несмотря на время отклика. Независимо от процента, если количество отправленных строк равно количеству проверенных, а значение подозрительно большое, это определенно указывает, что запрос вызывает сканирование таблицы. Такое обычно плохо сказывается на производительности (в разделе «Сканирование таблицы» гл. 2 объясняется почему). Отправленные строки < Проверенные строки Если отправленных строк меньше, чем проверенных, — это надежный признаком плохой селективности запросов или индексов. Если разница очень велика, это, вероятно, объясняет большое время отклика. Например, отправлено 1000 строк и проверено 100 000: это не такие уж огромные значения, но выходит, что 99% строк не совпали: запрос заставил MySQL потратить много времени впустую. Даже если время отклика приемлемо, индекс может значительно сократить потерянное время. Отправленные строки > Проверенные строки Иногда отправленных строк больше, чем проверенных. Это происходит при особых условиях — например, когда MySQL способна «оптимизировать» (именно в кавычках) запрос. Например, SELECT COUNT(id) FROM t2 в таблице в предыдущем разделе отправляет одну строку для значения COUNT(id), но проверяет ноль строк. Отправленные строки редко представляют проблему сами по себе. Современные сети быстры, а протокол MySQL эффективен. Если в вашем дистрибутиве и версии MySQL есть метрика bytes sent (с англ. отправлено байтов) в логе медленных запросов (схема производительности не предоставляет эту метрику), вы можете использовать это двумя способами. Во-первых, минимальное, максимальное и среднее значения отображают размер результирующего набора в байтах. Обычно это небольшое значение, но оно может быть довольно большим, если запрос возвращает столбцы BLOB или JSON. Во-вторых, общее количество отправленных байтов может быть преобразовано в пропускную способность сети (Мбит/с или Гбит/с), чтобы показать сетевое использование запроса, которое также обычно очень мало. Затронутые строки Затронутые строки (rows affected) — это количество строк, вставленных, обновленных или удаленных. Программисты проявляют осторожность, чтобы влиять только на правильные строки. Когда изменяются не те строки, это серьезная ошибка. Таким образом, значение затронутых строк всегда верно. Но если их неожиданно много, это может указывать на новый или измененный запрос, который затрагивает больше строк, чем предполагалось. Другой способ увидеть число затронутых строк — посмотреть на размер пакета массовых операций. Массовые INSERT, UPDATE и DELETE часто приводят к проблемам: задержка репликации, длина списка истории, время блокировки и общее снижение
Время отклика на запрос | 33 производительности. Столь же частым является вопрос: «Насколько большим должен быть размер пакета?» Универсального ответа не существует. Вместо этого вы должны определить размер пакета и скорость, которые MySQL и приложение могут поддерживать без влияния на время отклика на запрос. Я объясняю это в разделе «Размер пакета» (см. гл. 3), где в основном мы сфокусируемся на DELETE, но информация оттуда будет также применима к INSERT и UPDATE. Полное сканирование таблицы Полное сканирование таблицы (select scan) — это количество полных сканирований таблицы для первой таблицы, к которой обращается запрос. (Если запрос обращается к двум или более таблицам, применяется следующая метрика: select full join). Обычно это плохо сказывается на производительности, поскольку означает, что запрос не использует индекс. После главы 2, где рассказывается об индексах и индексации, у вас должно легко получаться добавлять индекс, чтобы исправить сканирование. Если значение select scan не равно нулю, настоятельно рекомендуется оптимизировать запрос. Возможно, хотя и очень редко, что запрос вызовет сканирование таблицы, но это происходит не всегда. Чтобы определить, почему именно так произошло, вам понадобится образец запроса, который вызывает сканирование таблицы, и образец запроса, который этого не делает, а также план EXPLAIN для обоих. Одна из вероятных причин заключается в том, сколько строк, по оценкам MySQL, будет проверено запросом относительно кардинальности индекса (количества уникальных значений в индексе), общего количества строк в таблице и других затрат. (Оптимизатор запросов MySQL использует модель затрат.) Оценки не идеальны, иногда MySQL ошибается, что приводит к сканированию таблицы или неоптимальному плану выполнения, но, опять же, это случается очень редко. Как правило, значение select scan равно либо нулю, либо единице (оно бинарно). Радуйтесь, если это ноль. Оптимизируйте запрос, если нет. Полное сканирование присоединенной таблицы Полное сканирование присоединенной таблицы (Select full join) — это количество полных сканирований таблиц для присоединенных таблиц. Похоже на select scan, только хуже; я сейчас объясню почему. Значение select full join всегда должно быть равно нулю; в противном случае необходима оптимизация запроса. Когда вы применяете EXPLAIN для запроса с несколькими таблицами (https://oreil.ly/sRswS), MySQL показывает порядок соединения таблиц сверху (первая таблица) вниз (последняя таблица). Select scan применяется только к первой таблице. Select full join применяется только ко второй и последующим таблицам. Порядок соединения таблиц определяется MySQL, а не запросом. (Исключение — если используется STRAIGHT_JOIN; но этого делать не надо. Позвольте оптимизатору запросов MySQL выбирать порядок соединения для наилучшего плана выполнения запроса. Оптимизатор запросов почти всегда надежен, так что ему можно доверять, если только вы не сможете опровергнуть его правоту.) Пример 1.4 показывает план
34 | Глава 1 EXPLAIN для SELECT...FROM t1, t2, t3: MySQL определяет порядок соединения, отличный от имплицитного (неявного) объединения с тремя таблицами в запросе. Пример 1.4. План EXPLAIN для трех соединенных таблиц *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t3 partitions: NULL type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 3 filtered: 100.00 Extra: NULL *************************** 2. row *************************** id: 1 select_type: SIMPLE table: t1 partitions: NULL type: range possible_keys: PRIMARY key: PRIMARY key_len: 4 ref: NULL rows: 2 filtered: 100.00 Extra: Using where *************************** 3. row *************************** id: 1 select_type: SIMPLE table: t2 partitions: NULL type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 7 filtered: 100.00 Extra: NULL MySQL сначала считывает таблицу t3, затем присоединяет к таблице t1, затем присоединяет таблицу t2. Этот порядок отличается от указанного в запросе (FROM t1,
Время отклика на запрос | 35 t2, t3), вот почему вы должны применить EXPLAIN к запросу, чтобы увидеть его порядок соединения. Всегда применяйте EXPLAIN к запросу, чтобы увидеть его порядок соединения. Select scan применяется к таблице t3, поскольку это первая таблица по порядку объединения, и данная операция вызывает сканирование таблицы (на что указывает type: ALL). Select full join применялся бы к таблице t1, если бы эта операция вызывала сканирование таблицы, но это не так: MySQL присоединяет таблицу, используя сканирование диапазона по первичному ключу (на что указывают type: range и key: PRIMARY соответственно). Select full join применяется к таблице t2, поскольку MySQL присоединяется к ней с помощью полного сканирования таблицы (на что указывает type: ALL). Сканирование таблицы на t2 называется полным соединением (full join), потому что MySQL сканирует полную таблицу при соединении. Select full join хуже, чем select scan, потому что количество полных соединений, которые происходят с таблицей во время выполнения запроса, равно произведению строк из предыдущих таблиц. MySQL оценивает три строки из таблицы t3 (на что указывает rows: 3) и две строки из таблицы t1 (на что указывает rows: 2). Следовательно, 3 × 2 = 6 полных соединений в таблице t2 во время выполнения запроса. Но значение метрики select full join будет равно 1, поскольку оно учитывает полные соединения в плане выполнения, а не в процессе выполнения запроса, чего достаточно, поскольку даже одно полное объединение — это слишком много. Начиная с версии MySQL 8.0.18, оптимизация хеш-объединений (https://oreil.ly/ zf7Rs) улучшает производительность для определенных объединений, но все же лучше избегать полных объединений. Краткое описание хеш-объединения см. в гл. 2, раздел «Алгоритмы соединения таблиц». Число временных таблиц на диске Число временных таблиц на диске (created tmp disk tables) — это количество временных таблиц, созданных на диске. Обычно запросы создают временные таблицы в памяти, но когда такая таблица становится слишком большой, MySQL записывает ее на диск. Это может повлиять на время отклика, поскольку доступ к диску на порядок медленнее, чем доступ к памяти. Однако временные таблицы на диске не являются распространенной проблемой, поскольку MySQL пытается избежать их создания. Чрезмерное количество «tmp disk tables» указывает на запрос, который можно оптимизировать, или на то, что (возможно) системная переменная tmp_table_size (https://oreil.ly/8exZw) слишком мала. Всегда в первую очередь оптимизируйте запросы. Системные переменные меняйте только в крайнем случае, особенно те из них, которые влияют на распределение памяти.
36 | Глава 1 Дополнительную информацию см. в разделе «Internal Temporary Table Use in MySQL» («Использование внутренней временной таблицы в MySQL», https:// oreil.ly/CeCSv) в руководстве по MySQL. Количество запросов Количество запросов (query count) — это число выполнений запросов. Оно может быть разным, если только не является чрезвычайно низким и запрос выполняется медленно. «Низко и медленно» — странная комбинация, которую стоит исследовать. Пишу я это и смотрю на профиль запроса, который послужит прекрасным примером: самый медленный запрос, выполненный один раз, но потребовавший 44% времени выполнения. Другими метриками являются:  Время отклика: 16 с.  Время блокировки: 110 мкс.  Просмотрено строк: 132 000.  Отправлено строк: 13. Запрос, конечно, нестандартный. Выглядит так, как будто программист выполнил запрос вручную, но из дайджест-текста я могу сказать, что запрос был сгенерирован программно. Какова же история у этого запроса? Чтобы выяснить это, мне придется обратиться к разработчикам приложения. Метаданные и приложение Анализ запросов — нечто большее, чем просто метрики запросов, а именно метаданные. Более того, вы не можете завершить анализ запроса без, по крайней мере, двух частей метаданных: плана EXPLAIN (также называемого планом выполнения запроса) и структуры таблицы для каждой таблицы. Существует несколько инструментов метрик запросов, которые автоматически собирают метаданные и отображают их в отчете по запросу. Если ваш инструмент метрики запросов этого не делает, не волнуйтесь: собирать метаданные легко. EXPLAIN (https://oreil.ly/AZvGt) и SHOW CREATE TABLE (https://oreil.ly/Wwp8f) для плана EXPLAIN и структуры таблиц соответственно. Метаданные необходимы для анализа запросов, оптимизации запросов и производительности MySQL в целом. Оператор EXPLAIN — жизненно важный инструмент в вашем репертуаре MySQL. Я объясняю это подробно в разделе «EXPLAIN: план выполнения запроса» (см. гл. 2) и широко использую данный инструмент на протяжении всей книги. Анализ запросов — нечто большее, чем просто метрики запросов и метаданные, а именно приложение. Метрики и метаданные необходимы для любого анализа запросов, но картина будет полной лишь тогда, когда вы узнаете, какой цели служит запрос, — т. е. почему приложение выполняет запрос? Знание этого позволяет вам оценивать изменения в приложении, чему посвящена глава 4. Я не раз встречал
Время отклика на запрос | 37 ситуации, в которых программисты понимали, что запрос может быть намного проще или вообще удален. Метрики запросов, метаданные и приложение должны завершать картину. Но я упустил бы важную деталь, если бы не упомянул, что иногда проблемы за пределами MySQL и приложения тоже влияют на картину; и обычно не в лучшую сторону. Так называемые шумные соседи (см. гл. 9) — тому классический пример. Если время отклика большое, но тщательный анализ запроса не обнаружил причину, рассмотрите внешние проблемы. Но не делайте поспешных выводов: внешние проблемы должны быть исключением, а не нормой. Относительные значения Для каждой метрики запроса объективно хорошее значение равняется только нулю, потому что, как говорится, самый быстрый способ что-то сделать — не делать этого вовсе. Ненулевые значения всегда относительны в рамках запроса и приложения. Например, отправленная тысяча строк — это в целом неплохо, но может быть катастрофой, если предполагается, что запрос возвращает только одну строку. Относительные значения важны при рассмотрении полной картины: метрик, метаданных и приложения. Вот еще одна правдивая история, иллюстрирующая, что значения относительны и важна полная картина. Мне досталось приложение, которое с годами работало все медленнее и медленнее. Это было внутреннее приложение, не используемое клиентами компании, поэтому его исправление не было приоритетом до тех пор, пока скорость не упала до неприемлемой. В профиле запроса самым медленным запросом был просмотр и возврат более десяти тысяч строк: не полное сканирование таблицы, а просто много строк. Вместо того чтобы зацикливаться на значениях, я покопался в исходном коде и обнаружил, что функция, выполняющая запрос, только подсчитывает количество строк, а не использует их. Это было медленно, потому что она без необходимости обращалась к тысячам строк и возвращала их, и со временем запрос становился все медленнее, потому что количество строк увеличивалось по мере роста базы данных. Когда стала ясна полная картина, оптимизация была очевидной и простой: SELECT COUNT(*). Среднее значение, процентиль и максимум Принято говорить о времени отклика на запрос так, будто это единственное значение, но это неверно. Из раздела «Агрегация» (см. выше) вы уже знаете, что метрики запроса группируются и агрегируются по запросу. В результате метрики запроса представляются в виде отдельных статистических значений: минимального, максимального, среднего и процентиля. Вы, несомненно, знакомы с этой вездесущей «статистикой», но сейчас я вас удивлю:  Среднее значение слишком оптимистично.  Процентиль — это предположение.  Максимум — это наилучшее представление.
38 | Глава 1 Позвольте мне объяснить. Среднее значение Не обманывайтесь средним значением: если количество запросов невелико, несколько очень больших или малых значений могут исказить среднее время отклика (или любая другая метрика). Более того, не зная распределения значений, мы не можем знать, какой процент значений представляет среднее. Например, если среднее значение равно медиане, то оно представляет нижние 50% значений, которые являются лучшим (более быстрым) временем отклика. В этом случае среднее значение слишком оптимистично. (Большинство значений слишком оптимистичны, если вы игнорируете худшую половину.) Среднее значение просто сообщает вам навскидку, выполняется ли обычно запрос в течение микросекунд, миллисекунд или секунд. Только и всего. Процентиль Проблему средних значений решают процентили. Если не углубляться в тему, P95 — это такое значение, когда 95% образцов меньше его или равно ему1. Например, если P95 равно 100 мс, то 95% значений меньше или равно 100 мс, а 5% значений больше 100 мс. Следовательно, P95 представляет 95% значений, что объективно является более репрезентативным (и менее оптимистичным), чем среднее значение. Есть еще одна причина, по которой используются процентили: небольшой процент игнорируемых значений считается статистическим выбросом. Например, неустойчивая синхронизация сети и случайные сбои могут привести к тому, что выполнение небольшого процента запросов займет больше времени, чем обычно. Поскольку это не вина MySQL, мы игнорируем эти значения времени выполнения как статистические выбросы. Использование процентилей — обычная практика, но они тоже гипотетичны. Да, могут быть статистические выбросы, но они должны быть доказаны, а не предполагаемы. До тех пор, пока не будет доказано, что верхние N% не являются выбросами, это наиболее интересные значения именно потому, что они нестандартны. Что же их вызывает? На этот вопрос трудно ответить, и именно поэтому процентили — обычная практика: легче игнорировать верхние N% значений, чем копать глубже и находить ответ. Наилучший процентиль — P999 (99,9%), потому что отбросить 0,1% значений — приемлемый компромисс между предположением, что это статистические выбросы, и реальностью, в которой действительно существуют редкие случаи2. Максимум Максимальное время запроса решает проблему процентилей: не нужно отбрасывать никакие значения. Максимум — это не миф и не статистическая иллюзия, как 1 2 Полное объяснение процентилей см. на HackMySQL, https://hackmysql.com/p95 P95, P99 и P999 встречаются чаще всего. Я никогда не видел других процентилей, используемых с MySQL, несмотря на медиану (P50) и максимум (P100).
Время отклика на запрос | 39 в случае со средним значением. Если какой-нибудь пользователь столкнется с максимальным временем отклика на запрос или даже сдастся через несколько секунд и уйдет, при желании можно узнать, почему так случилось. В то время как объяснить верхние N% значений сложно, ведь существует много значений и, следовательно, много потенциально разных ответов, объяснение максимума укладывается в одно значение и один ответ. Инструменты метрик запросов часто используют запрос с максимальным временем отклика в качестве образца, что делает его объяснение почти заурядным, потому что, образно говоря, ружье уже выстрелило: есть доказательства. С этим образцом произойдет одно из двух: либо он воспроизводит проблему, и в этом случае вы продолжите анализ; либо он не воспроизводит проблему, и в этом случае вы смогли доказать, что это статистический выброс, который можно игнорировать. Вот еще одна правдивая история, иллюстрирующая вышеупомянутый случай. Достаточно хорошее приложение случайным образом отвечало очень медленно. Минимальное, среднее значение и время запроса P99 составляли миллисекунды, но максимальное составляло секунды. Вместо того чтобы игнорировать максимум, я собрал образцы запросов с нормальным и максимальным временем выполнения. Разница заключалась в размере списка IN в предложении WHERE: сотни значений для обычного времени запроса и несколько тысяч значений для максимального времени запроса. Выборка большего количества значений, разумеется, занимает больше времени, но расхождение от миллисекунд до секунд ненормально даже для тысяч значений. дал ответ: при обычном времени запроса использовался индекс, но максимальное время запроса было связано с полным сканированием таблицы. MySQL может переключать планы выполнения запросов (см. гл. 2, раздел «Это ловушка! (Когда MySQL выбирает другой индекс)»), что объясняет поведение MySQL, но что объясняет поведение приложения? Короче говоря, запрос использовался для поиска данных с целью обнаружения мошенничества, и время от времени просматривалось сразу несколько тысяч строк, что заставляло MySQL переключать планы выполнения запросов. Обычно запрос был в полном порядке, однако тщательное изучение максимального времени отклика выявило не только промашку MySQL, но и возможность для улучшения работы приложения и клиентского опыта посредством более эффективной обработки больших запросов. EXPLAIN Среднее значение, процентиль и максимум очень полезны, но важно понимать, что они умеют делать, а что нет. Также рассмотрите распределение значений между минимумом и максимумом. Если вам повезет, отчет по запросу включает гистограммы, но особенно не рассчитывайте на это: вычисление гистограмм для произвольного временного диапазона может быть трудным, поэтому почти ни один инструмент метрик запросов этого не делает. Базовая статистика (минимальное, максимальное, среднее значения и процентиль) показывает достаточное распределение, чтобы понять, является ли запрос стабильным: когда метрики примерно одинаковы для каждого выполнения. (В главе 6 я возвращаюсь к идее стабильности (см. раздел «Нормальная и стабильная: лучшая база данных — это скучная база данных»).) Нестабильные запросы услож-
40 | Глава 1 няют анализ, потому что необходимо установить: что же заставляет запрос выполняться по-другому? Причина, вероятно, лежит за пределами MySQL, что затрудняет поиск, но найти ее необходимо, потому что стабильные запросы легче анализировать, понимать и оптимизировать. Улучшение времени отклика на запрос Улучшение времени отклика на запрос — процесс, называемый оптимизацией запросов (англ. query optimization). Я называю его «путешествием», чтобы определить адекватные ожидания. Оптимизация запросов требует времени и усилий, а также есть конечная цель: ускорение времени отклика на запрос. Чтобы это было эффективное путешествие, а не пустая трата времени и усилий, в нем есть две части: прямая оптимизация запросов и косвенная оптимизация запросов. Прямая оптимизация запросов Прямая оптимизация запросов — это изменения в запросах и индексах. Эти изменения решают множество проблем с производительностью, поэтому путешествие начинается именно с прямой оптимизации запросов. И поскольку эти изменения весьма мощные, оно часто здесь и заканчивается. Позвольте использовать аналогию, которая сейчас будет выглядеть упрощенной, но позже станет более глубокой. Представьте, что запрос — это машина. У механиков есть инструменты, позволяющие починить машину, когда она работает плохо. Некоторые инструменты обычны (например, гаечный ключ), а другие — специализированны (например, двойной верхний кулачковый замок). Как только механик открывает капот и обнаруживает проблему, он определяет, какие инструменты необходимы для ее устранения. Аналогичным образом у программистов есть инструменты для исправления запроса, когда он выполняется медленно. Обычные инструменты — анализ запросов, EXPLAIN (https://oreil.ly/oB3q9) и индексы. Специализированные — это оптимизация для конкретных запросов. Вот лишь некоторые из «Optimizing SELECT Statements» («Оптимизирующих инструкций SELECT», https://oreil.ly/dqEWw) из руководства по MySQL:  Оптимизация диапазона.  Оптимизация слияния индексов.  Оптимизация хеш-объединения.  Оптимизация состояния индекса (Index Condition Pushdown, ICP).  Оптимизация многодиапазонного считывания (англ. Multi-Range Read, MRR).  Оптимизация свертки констант.  Оптимизация IS NULL.  Оптимизация ORDER BY.  Оптимизация GROUP BY.
Время отклика на запрос | 41  Оптимизация DISTINCT.  Оптимизация запроса LIMIT. В этой книге я не освещаю оптимизацию, основанную на запросах, потому что это подробно изложено в главе 8 «Optimization» («Оптимизация», https://oreil.ly/03htc) руководства по MySQL; этот материал заслуживает доверия и регулярно обновляется. Кроме того, оптимизация запросов зависит от версии и дистрибутива MySQL. Вместо этого я рассказываю об индексах и индексации в главе 2: основы для понимания того, какие оптимизации для конкретных запросов следует использовать (и как) при исправлении медленного запроса. Прочитав вторую главу, вы сможете применять специализированные инструменты, такие как «Index Condition Pushdown Optimization» («Оптимизация состояния индекса», https://oreil.ly/5CEbX), столь же мастерски, как опытный механик — двойной верхний кулачковый замок. Время от времени я встречаю программиста, который возмущается, что оптимизация запросов, которую он так усердно проводил, не решает проблему. Прямая оптимизация запросов необходима, но не всегда достаточна. Оптимизированный запрос может быть или стать проблемой при разных обстоятельствах. Когда вы больше не можете оптимизировать запрос (или не можете оптимизировать его вообще, потому что у вас нет доступа к коду), вы можете оптимизировать запрос в обход, что приводит ко второй части нашего путешествия: косвенной оптимизации запросов. Косвенная оптимизация запросов Косвенная оптимизация запросов — это изменения в данных и паттернах доступа. Вместо того чтобы изменять запрос, вы изменяете то, к чему он обращается, а также характер этого обращения: данные запроса и паттерны доступа соответственно. Эти изменения косвенно оптимизируют запрос, поскольку данные параметры неразрывно связаны с производительностью. Изменения в чем-то одном влияют на остальное. Это легко доказать. Предположим, у вас медленный запрос. Размер данных и паттерны доступа не имеют значения для данного примера, так что представьте что хотите. Я могу сократить время отклика на запрос почти до нуля. (Допустим, близкое к нулю значение равно 1 микросекунде. Для компьютера это большое количество времени, но для человека это незаметно.) Косвенная «оптимизация» — TRUNCATE TABLE (команда, которая удаляет все строки из таблицы). Не имея данных, MySQL может выполнить любой запрос за почти нулевое время. Это жульничество, но тем не менее оно доказывает: уменьшение размера данных улучшает время отклика на запрос. Давайте вернемся к аналогии с автомобилем. Косвенная оптимизация запросов аналогична изменению основных элементов дизайна автомобиля. Например, вес является фактором топливной экономичности: уменьшение веса повышает топливную экономичность. (Данные аналогичны весу, поэтому TRUNCATE TABLE значительно повышает производительность, но так «оптимизировать» не надо.) Уменьшение веса не является непосредственным (прямым) изменением, потому что инженеры-
42 | Глава 1 автомобилестроители не могут волшебным образом уменьшить вес деталей. Вместо этого им приходится вносить значительные изменения, такие как переход со стали на алюминий, что может повлиять на многие другие элементы дизайна. Следовательно, эти изменения требуют значительно больших усилий. Требуется больше усилий — вот почему оптимизация косвенных запросов лишь вторая часть нашего путешествия. Если прямая оптимизация запросов решила проблему, остановитесь во имя эффективности. (Спешу вас поздравить!) Если этого не произошло, и вы уверены, что запрос не может быть дополнительно оптимизирован напрямую, тогда пришло время изменить данные и паттерны доступа, которые рассматриваются в главах 3 и 4. Когда оптимизировать запросы После того как вы исправите один медленный запрос, его место занимает другой. Медленные запросы будут всегда, но вы не обязательно должны их оптимизировать, потому что это неэффективное использование вашего времени. Вместо этого вспомните раздел «Краеугольный камень» (см. выше) и спросите себя: приемлемо ли данное время отклика на запрос? Если нет, то, разумеется, продолжайте оптимизировать. Если да, то можно пока закончить, ведь если база данных работает быстро, никто на нее лишний раз не посмотрит и вопросов не задаст. Как администратор базы данных, я советую вам каждую неделю просматривать метрики запросов (начиная с «Профиля запроса», см. выше) и при необходимости оптимизировать самые медленные запросы, но, как инженер-программист, я знаю, что это непрактично и почти никто этого не делает. Однако вот три случая, когда запросы следует оптимизировать обязательно. Когда производительность влияет на клиентов Когда производительность влияет на клиентов компании, программисты обязаны прибегнуть к оптимизации. Я думаю, что с этим согласится любой специалист, потому что повышать производительность для нас в радость. Кто-то может возразить, что это плохой совет, потому что он реактивный, а не проактивный, но мой огромный опыт показывает, что программисты (и даже администраторы баз данных) не смотрят на метрики запросов, пока клиенты не сообщают, что приложение работает слишком медленно или время ожидания истекло. Если метрики запросов всегда включены и наготове, это объективно подходящий момент для оптимизации, потому что потребность в повышении производительности так же реальна, как и ваши клиенты. До и после изменений кода Большинство программистов не возражают против обязательной оптимизации запросов до и после изменений кода, но мой опыт показывает, что на практике они все же этого не делают. Я заклинаю вас избегать этой распространенной модели:
Время отклика на запрос | 43 в код вносятся, казалось бы, невинные изменения, проверяются на стадии обкатки, внедряются в производство, а затем производительность начинает чудить. Что же случилось? Причиной обычно являются изменения в запросах и паттернах доступа, которые тесно связаны друг с другом. В главе 2 я начну объяснять почему; в главах 3 и 4 завершу объяснение. Пока что суть в следующем: вы станете героем, если будете просматривать метрики запросов до и после изменений кода. Раз в месяц Даже если ваш код и запросы не меняются, меняются по крайней мере две вещи, связанные с ними: данные и паттерны доступа. Я надеюсь, что ваше приложение пользуется бешеным успехом и хранит все больше данных по мере того, как число пользователей растет «вверх и вправо». Время отклика на запрос постепенно меняется по мере изменения структуры данных и доступа. К счастью, это происходит относительно медленно, обычно в течение нескольких недель или месяцев. Даже для приложения, испытывающего гиперрост (например, к миллионам пользователей каждый день добавляются тысячи новых), MySQL действительно хорошо себя проявляет при вертикальном масштабировании, так что время отклика на запрос остается стабильным. Однако ничто не вечно, и даже звезды умирают. Всегда наступит момент, когда хорошие запросы станут плохими. Главы 3 и 4 вам помогут в этом разобраться. А пока что запомните: вы станете эпическим героем, если будете просматривать метрики запросов раз в месяц (возможно, что о вас даже сложат легенды и песни). MySQL: быстрее Нет никакого волшебства или секрета в том, чтобы сделать MySQL значительно быстрее без изменения запросов или приложения. Вот еще одна правдивая история, иллюстрирующая мои слова. Команда разработчиков узнала, что их приложение будет упомянуто знаменитостью. Они ожидали наплыва трафика, поэтому заранее спланировали все так, чтобы MySQL и приложение выжили. Программист из этой команды попросила меня помочь увеличить пропускную способность MySQL (количество запросов в секунду, QPS). Я спросил: «Увеличить на сколько?» Она сказала: «В 100 раз». Я сказал: «Хорошо. Есть ли у вас свободный год и желание перепроектировать приложение?» Она сказала: «Нет, у нас только один день». Я понимаю, о чем она думала: с какой пропускной способностью мог бы справиться MySQL, если бы мы значительно обновили аппаратное обеспечение: больше ядер процессора, больше памяти, больше операций ввода-вывода? Здесь нет простого или однозначного ответа, потому что это зависит от многих факторов, которые рассматриваются в следующих главах книги. Но одно можно сказать наверняка: время — это жесткое ограничение. В одной секунде содержится 1000 миллисекунд: ни больше ни меньше. Если выполнение запроса занимает 100 миллисекунд, то наихудшая пропускная способ-
44 | Глава 1 ность составляет 10 QPS на ядро процессора: 1000 мс / 100 мс / запрос = 10 QPS. (Его реальная пропускная способность, вероятно, выше; подробнее об этом чуть позже). Если ничего не менять, то для выполнения запроса с большей пропускной способностью просто не хватит времени. Чтобы заставить MySQL выполнять больше работы за то же время, у вас есть три варианта: 1. Изменить природу времени. 2. Уменьшить время отклика. 3. Увеличить нагрузку. Первый вариант выходит за рамки этой книги, поэтому давайте сосредоточимся на втором и третьем. Уменьшение времени отклика освобождает время, которое MySQL может использовать для дополнительной работы. Это простая математика: если MySQL занята 999 миллисекунд из каждой секунды, то у нее есть лишь одна свободная миллисекунда, чтобы выполнить больше работы. Если этого недостаточно, вам нужно уменьшить время, затрачиваемое на текущую работу. Лучший способ добиться этого — прямая оптимизация запросов. Если не получится — косвенная оптимизация запросов. И наконец, более быстрое и мощное оборудование. В следующих главах вы узнаете, как это делается. Увеличение нагрузки (количества конкурентных запросов), как правило, происходит в первую очередь, потому что не требует каких-либо изменений запросов или приложения: просто выполняйте больше запросов одновременно (в режиме конкурентности), и MySQL отреагирует, используя больше ядер процессора: одно ядро обрабатывает один поток, который выполняет один запрос. В худшем случае MySQL использует N ядер процессора для одновременного выполнения N запросов в конкурентном режиме. Но такого почти не бывает, потому что время отклика — не процессорное время. Ненулевое время отклика — процессорное, а остальное к процессору не относится (https://oreil.ly/drw2d). Например, время отклика может составлять 10 мс процессорного времени и 90 мс ожидания дискового ввода-вывода. Следовательно, наихудшая пропускная способность для запроса, выполнение которого занимает 100 миллисекунд, составляет 10 запросов в секунду на ядро процессора, но его реальная пропускная способность должна быть выше, поскольку до худших случаев обычно не доходит. Звучит здорово, правда? Просто заставляйте MySQL работать больше, и вуаля: будет больше производительности. Но вы уже знаете, чем это кончится: повышаете нагрузку на MySQL, и система перестает работать вообще, потому что имеет ограниченную пропускную способность. MySQL может легко вынудить большинство современных аппаратных средств функционировать на пределе возможностей, но не пытайтесь так делать, пока не прочтете раздел «Производительность дестабилизируется на пределе» (см. гл. 4). Итого: MySQL не может просто взять и работать быстрее. Чтобы ускорить работу MySQL, вы должны отправиться по пути прямой и косвенной оптимизации запросов.
Время отклика на запрос | 45 Итоги В этой главе подробно рассказано о времени выполнения запросов, чтобы в последующих главах вы могли узнать, как его улучшить. Ключевые моменты :  Производительность — это время отклика на запрос: сколько времени требуется MySQL для выполнения запроса.  Время отклика на запрос — краеугольный камень производительности MySQL, потому что оно значимо и действенно.  Метрики запросов берутся из лога медленных запросов или схемы производи- тельности.  Схема производительности является лучшим источником метрик запросов.  Метрики запросов группируются и агрегируются по дайджесту: нормализован- ным инструкциям SQL.  В профиле запроса отображаются медленные запросы; термин «медленные» от- носится к метрике сортировки.  Отчет о запросе показывает всю доступную информацию по одному запросу; он используется для анализа запроса.  Целью анализа запросов является понимание того, как выполняются запросы, а не решение проблемы большого времени отклика.  Анализ запросов использует метрики запросов (как указано в отчете), метадан- ные (план EXPLAIN, структуры таблиц и т. д.) и знание приложения.  Девять метрик запросов необходимы для анализа каждого запроса: время запро- са (query time), время блокировки (lock time), проверенные строки (rows examined), отправленные строки (rows sent), затронутые строки (rows affected, select scan, select full join, created tmp disc tables) и количество запросов (query count).  Улучшение времени отклика на запросы (оптимизация запросов) состоит из двух частей: прямая оптимизация запросов, затем косвенная оптимизация запросов. • Прямая оптимизация запросов — это изменения в запросах и индексах. • Косвенная оптимизация запросов — это изменения в данных и паттернах доступа.  Просматривайте профиль запросов и оптимизируйте медленные запросы хотя бы тогда, когда производительность влияет на пользовательский опыт, до и после изменений кода, а также раз в месяц.  Чтобы ускорить работу MySQL, надо уменьшить время отклика (освободить время для выполнения большего количества работы) или увеличить нагрузку (заставить MySQL работать усерднее).
46 | Глава 1 В следующей главе говорится об индексах и индексации MySQL: прямой оптимизации запросов. Практика: определение медленных запросов Цель этой практики — найти медленные запросы с помощью pt-query-digest (https://oreil.ly/KU0hj): инструмент командной строки, который генерирует профиль запроса и отчеты о запросах из лога медленных запросов. Используйте экземпляр MySQL для разработки или промежуточного использования: не используйте рабочую версию, если не уверены, что не будет проблем. Лог медленных запросов по своей сути безопасен, но его включение на нагруженном сервере может увеличить дисковый ввод-вывод. Если у вас есть администраторы баз данных, которые управляют MySQL, попросите их включить и настроить лог медленных запросов. Или сами узнайте, как это сделать, изучив раздел «The Slow Query Log» («Лог медленных запросов», https:// oreil.ly/Hz0Sz) в руководстве по MySQL. (Для настройки понадобится учетная запись пользователя MySQL с привилегиями уровня SUPER.) Если вы используете MySQL в облаке, ознакомьтесь с документацией поставщика облачных услуг, чтобы понять, как включить лог медленных запросов и получить к нему доступ. Конфигурации MySQL различаются, но самый простой способ настроить и включить лог медленных запросов — это: SET GLOBAL long_query_time=0; SET GLOBAL slow_query_log=ON; SELECT @@GLOBAL.slow_query_log_file; +-------------------------------+ | @@GLOBAL.slow_query_log_file | +-------------------------------+ | /usr/local/var/mysql/slow.log | +-------------------------------+ Ноль в первой инструкции, SET GLOBAL long_query_time=0;, заставляет MySQL регистрировать каждый запрос. Будьте осторожны: на нагруженном сервере это может увеличить дисковый ввод-вывод и использовать гигабайты дискового пространства. При необходимости используйте немного большее значение, например 0,0001 (100 микросекунд) или 0,001 (1 миллисекунда). Percona Server и MariaDB Server поддерживают выборку лога медленных запросов: установите системную переменную log_slow_rate_limit для регистрации каждого N-го запроса. Например, log_slow_rate_limit = 100 регистрирует каждый 100-й запрос, что составляет 1% от всех запросов. Со временем это создает репрезентативную выборку в сочетании с long_query_time = 0. При использовании данной функции убедитесь, что инструмент метрики запроса учитывает эту выборку, иначе он будет предоставлять меньшие значения, чем они есть на самом деле. pt-querydigest несет ответственность за выборку.
Время отклика на запрос | 47 Последняя инструкция, SELECT @@GLOBAL.slow_query_log_file;, выводит имя файла лога медленных запросов, которое вам нужно в качестве первого аргумента командной строки для pt-query-digest. Вы можете динамически изменять эту переменную, если хотите войти в другой файл. Далее запустите pt-query-digest с именем файла лога медленных запросов в качестве первого аргумента командной строки. Инструмент напечатает много выходных данных; но пока что посмотрите только на Profile (с англ. профиль) в верхней части выходных данных: # # # # # # # Profile Rank Query ID ==== =================================== 1 0x95FD3A847023D37C95AADD230F4EB56A 2 0xBB15BFCE4C9727175081E1858C60FD0B 3 0x66112E536C54CE7170E215C4BFED008C MISC 0xMISC Response time Calls =============== ===== 1000.0000 53.8% 452 SELECT tbl 500.0000 26.9% 10 SELECT foo bar 50.0000 2.7% 5 INSERT tbl 310.0000 16.7% 220 <2 ITEMS> Предыдущий вывод представляет собой текстовую таблицу, в которой перечислены самые медленные запросы из лога медленных запросов. В этом примере SELECT tbl (аннотация запроса) является самым медленным запросом, на который приходится 53,8% от общего времени выполнения. (По умолчанию pt-query-digest сортирует запросы по времени выполнения в процентах.) Под профилем запроса печатается отчет о запросе для каждого запроса. Изучите выходные данные pt-query-digest. Они задокументированы в его руководстве; в Интернете также есть множество информации, потому что этот инструмент широко используется. Также ознакомьтесь с Percona Monitoring and Management (https://oreil.ly/rZSx2): комплексное решение для мониторинга баз данных, использующее приложение Grafana (https://grafana.com) для представления метрик запросов. Оба инструмента бесплатны, с открытым исходным кодом и поддерживаются Percona (https://percona.com). Просматривая медленные запросы, вы точно узнаете, какие запросы следует оптимизировать для наиболее эффективного повышения производительности. Что еще важнее, вы начали заниматься производительностью MySQL как эксперт — с акцентом на запросы, потому что производительность — это время отклика на запрос.
48 | Глава 1
ГЛАВА 2 Индексы и индексирование Производительность MySQL определяется многими факторами, но в первую очередь индексами, поскольку без них нельзя достигнуть хорошего ее уровня. Можно отказаться от иных факторов — запросов, схем, данных и т. д. — и по-прежнему достигать производительности, но удаление индексов ограничивает вас грубой силой, заставляя полагаться на скорость и мощность оборудования. Если бы эта книга называлась «Грубая производительность MySQL», содержание было не длиннее заголовка: «Купите более быстрое и мощное оборудование». Вы смеетесь, но всего несколько дней назад я встретился с командой разработчиков, которые улучшали производительность в облаке, покупая более быстрое и мощное оборудование, пока космические затраты не заставили их задуматься: «Как еще мы можем улучшить производительность?» MySQL использует оборудование, оптимизации и индексы для достижения производительности при доступе к данным. Аппаратное обеспечение — очевидный рычаг, поскольку MySQL работает на аппаратном обеспечении: чем быстрее и мощнее аппаратное обеспечение, тем выше производительность. Менее очевидным и, возможно, более удивительным является то, что аппаратное обеспечение дает наименьшие рычаги воздействия. Я сейчас объясню, почему. Оптимизации относятся к многочисленным методам, алгоритмам и структурам данных, которые позволяют MySQL эффективно использовать аппаратное обеспечение. Оптимизация позволяет сфокусировать мощность аппаратного обеспечения. А фокус — то, чем отличается лампочка от лазера. Следовательно, оптимизация дает больше возможностей, чем аппаратное обеспечение. Если бы базы данных были небольшими, аппаратного обеспечения и оптимизации было бы достаточно. Но рост размера данных сводит на нет преимущества этих мер. Без индексов производительность сильно ограничена. Чтобы проиллюстрировать эти моменты, подумайте о MySQL как о точке опоры, которая использует аппаратное обеспечение, оптимизации и индексы для условного извлечения данных, как показано на рис. 2.1. Без индексов (с правой стороны) MySQL обеспечивает ограниченную производительность при относительно небольших объемах данных. Но добавьте к балансу индексы, как показано на рис. 2.2, и MySQL достигнет высокой производительности при работе с большими данными. Индексы — самый эффективный и лучший рычаг. Они нужны для любого нетривиального объема данных. Производительность MySQL требует правильных индексов и индексирования, о которых подробно рассказывается в этой главе.
50 | Глава 2 Рис. 2.1. Производительность MySQL без индексов Рис. 2.2. Производительность MySQL с индексами Несколько лет назад я разработал и внедрил приложение, в котором хранится много данных. Первоначально я предполагал, что самая большая таблица не должна превышать миллиона строк. Но в коде архивации данных был баг, который позволил таблице достичь одного миллиарда строк. В течение многих лет никто этого не замечал, потому что время отклика всегда было прекрасным. Почему? Хорошие индексы. Обычно говорят, что MySQL использует только один индекс для каждой таблицы, но это не совсем так. Например, оптимизация слияния индексов может использовать два индекса. Однако в этой книге я сосредоточусь на обычном варианте: один запрос, одна таблица, один индекс. В этой главе рассказывается об индексах и индексировании MySQL. Здесь пять основных разделов. Первый разъясняет, почему не стоит отвлекаться на аппаратную настройку или тонкую настройку (тюнинг) MySQL. Это необходимое отступление, чтобы вы полностью поняли, почему аппаратная настройка и тюнинг MySQL не являются эффективными решениями для повышения производительности MySQL. Второй — визуальное введение в индексы MySQL: что они собой представляют и как работают. Третий обучает индексированию — применению индексов для максимальной эффективности: — мыслить как MySQL. Четвертый освещает общие
Индексы и индексирование | 51 причины, по которым индексы теряют эффективность (влияние). Пятая часть представляет собой краткий обзор алгоритмов соединения таблиц MySQL, поскольку эффективные соединения основаны на эффективных индексах. Отвлекающие маневры в теме производительности Отвлекающий маневр — это идиома, которая означает отвлечение от цели. При поиске решений для повышения производительности MySQL инженеров обычно отвлекают два таких маневра: более быстрое и мощное оборудование и тюнинг MySQL. Более быстрое и мощное оборудование! Когда производительность MySQL неприемлема, не начинайте с масштабирования (используя более быстрое и мощное оборудование), чтобы «посмотреть, поможет ли это». Вероятно, поможет, если вы значительно увеличите масштаб, но вы ничему не научитесь, потому что это лишь доказывает то, что вы уже знаете: компьютеры работают быстрее на более быстром оборудовании. Более быстрое и мощное оборудование — это отвлекающий маневр в теме производительности, потому что вы упускаете возможность узнать о реальных причинах низкой производительности и решениях в такой ситуации. Есть два разумных исключения. Во-первых, если аппаратного обеспечения явно недостаточно, увеличьте масштаб до разумного уровня. Например, 1 Гбайт памяти для 500 Гбайт данных явно недостаточно. Разумно будет обновиться до 32 Гбайт или 64 Гбайт памяти. В то время как обновление памяти до 384 Гбайт обязательно подействует, но это неразумно. Во-вторых, если приложение испытывает гиперрост (массовое увеличение числа пользователей, уровня использования и объема данных), и масштабирование является временным решением для поддержания работы приложения, сделайте это. Всегда разумно поддерживать работу приложения. В иных случаях масштабирование ради повышения производительности MySQL происходит в последнюю очередь. Эксперты сходятся во мнении: сначала оптимизируйте запросы, данные, паттерны доступа и приложение. Если все эти оптимизации не дают достаточной производительности, увеличьте масштаб. Масштабирование происходит в последнюю очередь по следующим причинам. Вы ничему не научитесь, расширяя масштаб, вы просто решите проблему с помощью более быстрого и мощного оборудования. Поскольку вы инженер, а не пещерный человек, вы решаете проблемы, изучая и понимая их, а не просто отбрасывая. Общеизвестно, что обучение и понимание — это более сложно и трудоемко, но также гораздо более эффективно и устойчиво, что приводит к следующей причине. Установка более мощного оборудования — не самый надежный подход. Модернизация железа — это нетривиальная задача.
52 | Глава 2 Обновление может выполняться сравнительно быстро и легко, но это зависит от многих факторов, которые не рассматриваются в рамках книги. Однако достаточно сказать, что вы сведете с ума себя или инженеров по оборудованию, если будете часто это делать. Сумасшедшие инженеры ненадежны. Более того, компании часто используют одно и то же оборудование в течение нескольких лет, потому что процесс покупки длительный и сложный. В результате простая масштабируемость оборудования — одно из преимуществ облака. В облаке вы можете увеличить (или уменьшить) количество ядер процессора, памяти и хранилища за несколько минут. Но это значительно дороже, чем использование физического оборудования. Затраты на облачные вычисления могут расти в геометрической прогрессии. Стоимость Amazon RDS, например, удваивается от одного размера экземпляра к следующему — удваивается аппаратное обеспечение, удваивается цена. Экспоненциально растущие затраты — это нерационально. В сущности, MySQL способна полностью использовать все предоставленное ей оборудование. (Существуют ограничения, о которых я расскажу в главе 4). Реальный вопрос заключается в следующем: может ли приложение полностью использовать MySQL? Предполагаемый ответ — да, но это не гарантировано. Более быстрое и мощное аппаратное обеспечение помогает MySQL, но не меняет того, как приложение использует MySQL. Например, увеличение объема памяти может не повысить производительность, если приложение вызывает сканирование таблиц. Масштабирование эффективно для повышения производительности только в том случае, если рабочая нагрузка приложения также может увеличиваться. Не все рабочие нагрузки могут масштабироваться. Рабочая нагрузка (англ. workload) — это комбинация запросов, данных и паттернов доступа. Но давайте представим, что вы успешно увеличили рабочую нагрузку, чтобы в полной мере использовать MySQL на самом лучшем доступном оборудовании. Что происходит по мере того, как приложение продолжает расти, а его рабочая нагрузка продолжает увеличиваться? Это напоминает мне дзенскую мудрость: «Когда ты достигнешь вершины горы, продолжай карабкаться». Хотя я действительно призываю вас поразмышлять над этим, это менее поучительная дилемма для вашего приложения. Поскольку больше некуда идти, единственный вариант — сделать то, что следовало сделать в первую очередь: оптимизировать запросы, данные, паттерны доступа и приложение. Тонкая настройка (тюнинг) MySQL В телесериале «Звездный путь» инженеры могут модифицировать корабль, чтобы увеличить мощность двигателей, оружия, щитов, датчиков, транспортеров, тяговых лучей — всего. MySQL сложнее в эксплуатации, чем космический корабль, потому что такие модификации невозможны. Но это не останавливает инженеров от попыток.
Индексы и индексирование | 53 Во-первых, давайте проясним три термина. Тонкая настройка (тюнинг) Тонкая настройка (англ. tuning) — это настройка системных переменных MySQL для научно-исследовательских и опытно-конструкторских работ (НИОКР), (англ. research and development, R&D). Это лабораторная работа с конкретными целями и критериями. Бенчмаркинг (тесты производительности, эталонное тестирование) — обычное дело: это корректировка системных переменных для измерения влияния на производительность. В посте «MySQL Challenge: 100k Connections» («MySQL Challenge: 100 тысяч подключений», https://oreil.ly/ CGvrU) известного эксперта по MySQL Вадима Ткаченко демонстрируется пример экстремально тонкой настройки. Поскольку тонкая настройка — это НИОКР, результаты, как правило, не будут общеприменимыми; скорее цель в том, чтобы расширить наши коллективные знания и понимание MySQL, особенно в отношении ее текущих ограничений. Тонкая настройка влияет на будущую разработку MySQL и передовые практические методы (они же лучшие практики). Конфигурирование Конфигурирование (англ. configuring) — это выставление системным переменным значений, соответствующих оборудованию и среде. Цель — разумная конфигурация в отношении тех значений по умолчанию, которые необходимо изменить. Конфигурирование MySQL обычно выполняется при подготовке экземпляра MySQL или при изменении оборудования. Также конфигурирование необходимо осуществлять, когда размер данных увеличивается на порядок, например с 10 до 100 Гбайт. Конфигурирование влияет на то, как работает MySQL в целом. Оптимизация Оптимизация (англ. optimizing) — это повышение производительности MySQL за счет уменьшения рабочей нагрузки или повышения ее эффективности — обычно последнего, поскольку использование приложений имеет тенденцию к увеличению. Цель — меньшее время отклика с большей пропускной способностью при использовании существующего оборудования. Оптимизация влияет на производительность MySQL и приложений. Вы, несомненно, столкнетесь с этими терминами в литературе по MySQL, видеороликах, конференциях и т. д. Описания важнее терминов. Если, например, вы читаете статью в блоге, где используется слово оптимизация, но описывается то, что здесь определяется как тонкая настройка, то это тонкая настройка, как определено здесь. Различие этих терминов важно, потому что инженеры применяют все три этих меры, но только оптимизация (как определено здесь) — эффективное использование вашего времени1. 1 Если только вы не Вадим Ткаченко — в таком случае, пожалуйста, продолжайте настраивать.
54 | Глава 2 Тонкая настройка MySQL — это отвлекающий маневр в теме производительности по двум причинам. Во-первых, это часто делается не как контролируемый лабораторный эксперимент, что делает результаты сомнительными. В целом производительность MySQL сложна; эксперименты должны тщательно контролироваться. Во-вторых, результаты вряд ли окажут существенное влияние на производительность, поскольку MySQL уже сильно оптимизирована. Настройка MySQL сродни выжиманию воды из камня. Возвращаясь к первому абзацу этого раздела: я понимаю, что все мы восхищаемся лейтенант-коммандером Джорди Ла Форджем, главным инженером «Звездного пути: Следующее поколение». Когда капитан требует большей мощности, мы чувствуем себя обязанными сделать это, использовав тайные параметры сервера. Или когда приложению на Земле требуется больше энергии, мы хотим спасти положение, применив оригинальную реконфигурацию MySQL, которая повышает пропускную способность и конкурентность на 50%. Хорошая работа, Ла Форж! К сожалению, в MySQL 8.0 введена автоматическая настройка путем включения innodb_dedicated_server (https://oreil.ly/niPGL). Поскольку срок службы (англ. endof-life, EOL) MySQL 5.7 истечет вскоре после выхода этой книги, давайте продолжим смотреть в будущее и строить его. Тем не менее хорошая работа, Ла Форж. Оптимизация — это все, что вам нужно сделать, потому что тонкая настройка — это отвлекающий маневр, а конфигурирование выполняется автоматически, начиная с MySQL 8.0. Эта книга полностью посвящена оптимизации. Индексы MySQL: наглядное введение Индексы являются ключом к производительности, и, если вы помните раздел «Прямая оптимизация запросов» (см. гл. 1), изменения в запросах и индексах решают множество проблем с производительностью. Путь оптимизации запросов требует глубокого понимания индексов MySQL, и мы сейчас это рассмотрим — подробно и с многочисленными иллюстрациями. Хотя этот раздел подробный и относительно длинный, я называю его введением, потому что здесь есть чему поучиться. Но он является ключом, который открывает сундук с сокровищами оптимизации запросов MySQL. Следующие девять разделов касаются только стандартных индексов в таблицах InnoDB — типа индекса, создаваемого простым первичным ключом — PRIMARY KEY, или уникального индекса — [UNIQUE] INDEX. MySQL поддерживает другие специализированные типы индексов, но я не затрагиваю их в этой книге, потому что основой производительности являются стандартные индексы. Прежде чем мы углубимся в нюансы, расскажу вам кое-что неочевидное про таблицы InnoDB — и вы по-новому посмотрите не только на индексы, но и на большую часть производительности MySQL.
Индексы и индексирование | 55 Таблицы InnoDB являются индексами Пример 2.1 — это структура таблицы elem (сокращение от elements) и 10 строк, которые эта таблица содержит. Все примеры в этой главе относятся к таблице elem, за одним четко отмеченным исключением — поэтому найдите минутку, чтобы изучить ее. Пример 2.1. Таблица elem CREATE TABLE `elem` ( `id` int unsigned NOT NULL, `a` char(2) NOT NULL, `b` char(2) NOT NULL, `c` char(2) NOT NULL, PRIMARY KEY (`id`), KEY `idx_a_b` (`a`,`b`) ) ENGINE=InnoDB; id 1 2 3 4 5 6 7 8 9 10 a Ag Au Al Ar Ar Ag At Al Al Ar b B Be Br Br Br B Bi B B B c C Co Cr Cd C Co Ce C Cd cd Таблица elem имеет два индекса: первичный ключ для столбца id и неуникальный вторичный индекс для столбцов a, b. Значение в столбце id представляет собой монотонно возрастающее целое число. Значения для столбцов a, b и c являются атомными символами, соответствующими букве названия столбца: «Ag» (серебро) для столбца a, «B» (бор) для столбца b и т. д. Значения строк случайны и бессмысленны; это всего лишь пример. На рис. 2.3 показан типичный вид таблицы elem — только первые четыре строки для краткости. Ничего особенного в этой таблице нет, верно? Это так просто — можно сказать, элементарно. Но что если я скажу вам, что на самом деле это не таблица, а индекс? Уберите отсюда «F» (фтор)! На рис. 2.4 показана истинная структура таблицы elem в виде таблицы InnoDB. Таблицы InnoDB представляют собой индексы B-дерева, организованные по первичному ключу. Строки — это записи индекса (англ. index record), хранящиеся в конечных узлах структуры индекса. Каждая запись индекса содержит метаданные
56 | Глава 2 Рис. 2.3. Таблица elem: визуальная модель Рис. 2.4. Таблица elem: индекс B-дерева InnoDB (обозначаемые «...»), используемые для блокировки строк, изоляции транзакций и т. д. Рис. 2.4 представляет собой сильно упрощенное изображение индекса B-дерева, который является элементом таблицы. Четыре индексные записи (внизу) соответствуют первым четырем строкам. Значения столбцов первичного ключа (1, 2, 3 и 4) отображаются в верхней части каждой индексной записи. Другие значения столбцов («Ag», «B», «C» и т. д.) показаны под метаданными для каждой индексной записи Вам не нужно знать технические детали по теме индексов InnoDB B-дерева, чтобы понять суть или достичь поразительной производительности MySQL. Важны только два момента:  поиск по первичному ключу выполняется чрезвычайно быстро и эффективно;  первичный ключ имеет решающее значение для производительности MySQL. Первый пункт действителен, потому что индексы B-дерева по своей сути быстры и эффективны — одна из причин, по которой их используют многие серверы баз
Индексы и индексирование | 57 данных. Второй пункт становится все более очевидным в следующих разделах и главах. Чтобы узнать об увлекательном мире внутренних компонентов базы данных, включая индексы, прочитайте «Database Internals» («Внутренние компоненты базы данных», https://oreil.ly/TDsCc) Алекса Петрова (издательство O'Reilly, 2019). Чтобы глубоко погрузиться во внутренности InnoDB, включая его реализацию в виде B-дерева, отмените все свои встречи и посетите веб-сайт известного эксперта по MySQL Джереми Коула (https://oreil.ly/9sH9m). Первичный ключ InnoDB — это кластеризованный индекс (англ. clustered index). Руководство по MySQL иногда ссылается на первичный ключ как на кластеризованный индекс. Индексы обеспечивают наибольшее и наилучшее влияние, потому что сама таблица является индексом. Первичный ключ имеет решающее значение для производительности. Это особенно верно, поскольку вторичные индексы включают значения первичного ключа. На рис. 2.5 показан вторичный индекс в столбцах a, b. Рис. 2.5. Вторичный индекс в столбцах a, b Вторичные индексы тоже являются индексами B-дерева, но конечные узлы хранят значения первичного ключа. Когда MySQL использует вторичный индекс для поиска строки, она выполняет второй поиск по первичному ключу, чтобы прочитать полную строку. Давайте соединим их вместе и выполним поиск по вторичному индексу для запроса SELECT * FROM elem WHERE a = 'Au' AND b ='Be': На рис. 2.6 показан вторичный индекс (столбцы a, b) вверху и первичный ключ (столбец id) внизу. Шесть выносок (пронумерованные кружки) показывают поиск значения «Au, Be» с использованием вторичного индекса: 1. Поиск по индексу начинается с корневого узла; ответвляется прямо на внутренний узел для значения «Au, Be».
58 | Глава 2 Рис. 2.6. Поиск по вторичному индексу для значения «Au, Be» 2. На внутреннем узле ответвление идет прямо к конечному узлу для значения «Au, Be». 3. Конечный узел для значения вторичного индекса «Au, Be» содержит соответствующее значение первичного ключа: 2. 4. Начинается поиск первичного ключа на корневом узле; ответвление влево на внутренний узел для значения 2. 5. На внутреннем узле ответвление идет прямо к конечному узлу для значения 2. 6. Конечный узел для значения первичного ключа 2 содержит полную строку, соответствующую «Au, Be». Таблица имеет только один первичный ключ. Все остальные индексы являются вторичными индексами.
Индексы и индексирование | 59 Этот раздел короткий, но невероятно важный, потому что правильная модель обеспечивает основу для понимания индексов и многого другого. Например, если вы вспомните раздел «Время блокировки» (см. гл. 1), то можете увидеть эту тему в новом свете, поскольку строки на самом деле являются конечными узлами в первичном ключе. Знание того, что таблица в InnoDB является ее первичным ключом, сродни знанию, что гелиоцентризм, а не геоцентризм— правильная модель солнечной системы. В мире MySQL все вращается вокруг первичного ключа. Методы доступа к таблице Использование индекса для поиска строк — один из трех методов доступа к таблице. Поскольку таблицы являются индексами, поиск по индексу — лучший и наиболее распространенный метод. Но иногда, в зависимости от запроса, поиск по индексу невозможен, и единственным средством остается сканирование индекса или сканирование таблицы — другие методы доступа. Знать, какой метод доступа MySQL использует для запроса, необходимо, поскольку производительность требует именно поиска по индексу. Избегайте сканирования индексов и таблиц. В разделе «EXPLAIN: план выполнения запроса» (см. ниже) объясняется, как просмотреть метод доступа. Но сначала давайте проясним и наглядно представим каждый из них. В руководстве по MySQL можно встретить термины метод доступа (англ. access method), тип доступа (англ. access type) и тип соединения (англ. join type). И EXPLAIN использует поле с именем type или access_type для ссылки на них. В MySQL эти термины тесно связаны, но образуется двусмысленность. В этой книге для точности и логичности я использую только два термина: метод доступа и тип доступа. Существуют три метода доступа: поиск по индексу, сканирование индекса и сканирование таблицы. Для поиска по индексу существует несколько типов доступа: ref, eq_ref, range и т. д. Поиск по индексу Поиск по индексу находит определенные строки — или диапазоны строк — за счет использования упорядоченной структуры и алгоритмического доступа к индексу. Это самый быстрый метод доступа, потому что это именно то, для чего предназначены индексы: быстрый и эффективный доступ к большим объемам данных. Следовательно, поиск по индексам нужен для прямой оптимизации запросов. Производительность требует, чтобы практически каждый запрос использовал поиск по индексу для каждой таблицы. Существует несколько типов доступа для поиска по индексу, которые я рассмотрю в следующих разделах, таких как «WHERE» (см. ниже). На рис. 2.6 в предыдущем разделе показан поиск по индексу с использованием вторичного индекса.
60 | Глава 2 Сканирование индекса Когда поиск по индексу невозможен, MySQL должна использовать перебор для поиска строк: считывать все строки и отфильтровывать несоответствующие. Прежде чем MySQL прибегнет к чтению каждой строки с использованием первичного ключа, она попытается прочитать строки с использованием вторичного индекса. Это называется сканированием индекса. Существуют два типа сканирования индекса. Первый — полное сканирование индекса, когда MySQL считывает все строки в порядке индекса. Чтение всех строк обычно плохо сказывается на производительности, но при чтении их в порядке индекса можно избежать сортировки строк, когда порядок индекса соответствует запросу ORDER BY. На рис. 2.7 показано полное сканирование индекса для запроса SELECT * FROM elem FORCE INDEX (a) ORDER BY a, b. FORCE INDEX требуется, потому что, поскольку таблица elem крошечная, для MySQL более эффективно сканировать первичный ключ и сортировать строки, а не сканировать вторичный индекс и извлекать строки по порядку. (Иногда плохие запросы служат хорошими примерами.) На рис. 2.7 есть восемь выносок (пронумерованных кружков), которые показывают порядок доступа (обращения) к строкам: 1. Считывается первое значение вторичного индекса (SI): «Ag, B». 2. Ищется соответствующая строка в первичном ключе (PK). 3. Считывается второе значение SI: «Al, Br». 4. Ищется соответствующая строка в PK. 5. Считывается третье значение SI: «Ar, Br». 6. Ищется соответствующая строка в PK. 7. Считывается четвертое значение SI: «Au, Be». 8. Ищется соответствующая строка в PK. На рис. 2.7 есть тонкая, но важная деталь: сканирование вторичного индекса по порядку может быть последовательным чтением, но поиск по первичному ключу почти наверняка окажется чтением в случайном порядке. Доступ к строкам в порядке индекса не гарантирует последовательного чтения; более чем вероятно, что это будет чтение в случайном порядке. Последовательный доступ к строкам (чтение и запись) — это быстрее, чем в случайном порядке. Второй тип сканирования индекса — сканирование только по индексу: MySQL считывает значения столбцов (неполные строки) из индекса. Для этого требуется покрывающий индекс, данную тему наша книга также раскрывает (каламбур), но позже, в разделе «Покрывающие индексы». Это должно быть быстрее, чем полное
Индексы и индексирование | 61 сканирование индекса, потому что для чтения полных строк не требуется поиск по первичному ключу; он считывает только значения столбцов из вторичного индекса, поэтому для него требуется покрывающий индекс. Не оптимизируйте через сканирование индекса, если только единственной альтернативой не является полное сканирование таблицы. При другом раскладе избегайте сканирования индексов. Рис. 2.7. Полное сканирование индекса по вторичному индексу Сканирование таблицы Сканирование таблицы (полное) считывает все строки в порядке первичного ключа. Когда MySQL не может выполнить поиск по индексу или сканирование индекса, сканирование таблицы становится единственным вариантом. Обычно это плохо
62 | Глава 2 сказывается на производительности, но — так же обычно — это легко исправить, потому что MySQL умеет использовать индексы и имеет множество оптимизаций на основе индексов. По сути, каждый запрос с оператором WHERE, GROUP BY или ORDER BY может использовать индекс — даже если это просто проверка индекса — потому что эти операторы используют столбцы, а столбцы могут быть проиндексированы. Следовательно, причин для сканирования таблицы без возможности исправления почти нет. На рис. 2.8 показано полное сканирование таблицы: чтение всех строк в порядке первичного ключа. Он имеет четыре выноски, которые демонстрируют порядок доступа к строкам. Таблица elem крошечная, и здесь всего четыре строки, но представьте, что MySQL перебирает тысячи или миллионы строк в реальной таблице. Общий совет и лучшая практика — избегать сканирования таблиц. Но для полного и сбалансированного обсуждения есть два случая, когда сканирование таблицы может быть приемлемым или (на удивление) лучшим вариантом:  когда таблица крошечная и к ней редко обращаются;  когда селективность таблицы очень низкая (см. ниже, раздел «Экстремальная селективность»). Рис. 2.8. Полное сканирование таблицы Но не принимайте какие-либо проверки таблиц как должное: напоминаю, обычно это плохо сказывается на производительности. В очень редких случаях MySQL может сама неправильно выбрать сканирование таблицы, когда возможен поиск по индексу, как описано ниже, в разделе «Это ловушка! (Когда MySQL выбирает другой индекс)».
Индексы и индексирование | 63 Требование крайнего левого префикса Чтобы использовать индекс, запрос должен использовать крайний левый префикс индекса: один или несколько столбцов индекса, начинающихся с крайнего левого столбца индекса, как указано в определении индекса. Крайний левый префикс требуется, поскольку базовая структура индекса организована по порядку столбцов индекса, и ее можно просматривать (осуществлять в ней поиск) только в этом порядке. Используйте SHOW CREATE TABLE (https://oreil.ly/cwQZy) или SHOW INDEX (https:// oreil.ly/5wBhH), чтобы просмотреть определения индексов. На рис. 2.9 показан индекс — столбцы a, b, c и оператор WHERE, который использует каждый крайний левый префикс: столбец a; столбцы a, b и столбцы a, b, c. Рис. 2.9. Крайние левые префиксы индекса из трех столбцов Верхний WHERE на рис. 2.9 использует столбец a, который является крайним левым столбцом индекса. Средний WHERE использует столбцы a и b, которые вместе образуют крайний левый префикс индекса. И нижний WHERE использует весь индекс: все три столбца. Идеально использовать все столбцы индекса, но это не обязательно; требуется только крайний левый префикс. Столбцы индекса могут использоваться другими операторами SQL, как показано во многих примерах в следующих разделах. Чтобы использовать индекс, запрос должен использовать крайний левый префикс индекса.
64 | Глава 2 Требование крайнего левого префикса имеет два логических следствия: 1. Индексы (a, b) и (b, a) — разные индексы. Они индексируют одни и те же столбцы, но в разном порядке, что приводит к разным крайним левым префиксам. Однако запрос, который использует оба столбца (например, WHERE a = 'Au' AND b = 'Be'), может использовать любой индекс. Хотя это не означает, что данные индексы эквивалентны с точки зрения производительности. MySQL выберет лучшее из двух, рассчитав множество факторов. 2. Здесь MySQL, скорее всего, может использовать индекс (a, b, c) вместо индексов (a) и (a, b), потому что последние два являются крайними левыми префиксами первого. В этом случае индексы (a) и (a, b) являются дубликатами и могут быть удалены. Используйте pt-duplicate-key-checker (https://oreil.ly/EqtfV), чтобы находить повторяющиеся индексы и сообщать о них. В конце (крайнем правом) каждого вторичного индекса скрывается первичный ключ (англ. primary key). Для таблицы elem (пример 2.1) вторичный индекс фактически равен (a, b, id), но крайний правый, id, скрыт. MySQL не показывает первичный ключ, добавленный ко вторичным индексам; вы должны представить себе его. Первичный ключ добавляется к каждому вторичному индексу: (S, P), где S — столбцы вторичного индекса (англ. secondary index columns), а P — столбцы первичного ключа (англ. primary key columns). На языке MySQL мы говорим: «Первичный ключ добавляется ко вторичным индексам», даже если он не добавляется буквально. (Вы можете буквально добавить его, создав индекс (a, b, id), но не делайте этого.) «Добавлено к» на самом деле означает, что конечные узлы вторичного индекса содержат значения первичного ключа, как показано ранее на рис. 2.5. Это важно, поскольку увеличивает размер каждого вторичного индекса: значения первичного ключа дублируются во вторичных индексах. Большие индексы требуют больше памяти, но тогда в памяти может поместиться меньше индексов. Поддерживайте небольшой размер первичного ключа и разумное количество вторичных индексов. Буквально на днях мои коллеги помогали команде, чья база данных содержала 693 Гбайт вторичных индексов на 397 Гбайт данных (первичный ключ). Требование крайнего левого префикса — это благословение и ограничение. Последнее относительно легко обойти с помощью дополнительных вторичных индексов, но подождите, пока не прочтете раздел «Избыточные, повторяющиеся и неиспользуемые» (см. ниже в этой главе). С учетом ограничения соединение таблиц становится особой трудностью, но об этом поговорим ниже, в разделе «Соединение таблиц». Я призываю все же считать требование крайнего левого префикса благословением. Оптимизировать запросы в отношении индексирования — задача нетривиальная, но данное требование дает простую и знакомую отправную точку в этом путешествии.
Индексы и индексирование | 65 EXPLAIN: план выполнения запроса Команда MySQL EXPLAIN (с англ. объяснение, https://oreil.ly/M99Gp) показывает план выполнения запроса (он же план EXPLAIN), который описывает, как MySQL планирует выполнить запрос: порядок соединения таблиц, метод доступа к таблице, использование индекса и другие важные детали. Выходные данные EXPLAIN обширны и разнообразны. Более того, они полностью зависят от запроса. Изменение одного символа в запросе может существенно изменить его план EXPLAIN. Например, у WHERE id = 1 и WHERE id > 1 план EXPLAIN значительно отличается. И чтобы еще больше усложнить дело, EXPLAIN продолжает развиваться. «EXPLAIN Output Format» («Формат выходных данных EXPLAIN», https://oreil.ly/IMCOJ) в руководстве по MySQL обязательно для чтения — даже для экспертов. К счастью для нашего здравомыслия, основные принципы оставались неизменными на протяжении десятилетий. Чтобы проиллюстрировать использование индекса, в следующих пяти разделах главы объясняются запросы для каждого случая, когда MySQL может использовать индекс: 1. Поиск соответствующих строк: «WHERE». 2. Группировка строк: «GROUP BY». 3. Сортировка строк: «ORDER BY». 4. Избегание чтения строк: «Покрывающие индексы». 5. Соединение таблиц: «Соединение таблиц». Существуют и другие конкретные случаи, такие как MIN() и MAX(), но эти пять вариантов — основа использования индекса. Однако сначала мы подготовим почву, рассмотрев значение полей выходных данных EXPLAIN, показанных в примере 2.2. Пример 2.2. Выходные данные EXPLAIN (традиционный формат) EXPLAIN SELECT * FROM elem WHERE id = 1\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL type: const possible_keys: PRIMARY key: PRIMARY key_len: 4 ref: const rows: 1 filtered: 100.00 Extra: NULL
66 | Глава 2 В этом введении мы игнорируем поля id, select_type, partitions, key_len и filtered; но примеры включают их, чтобы вы привыкли к выходным данным. Остальные семь полей содержат множество информации, которая составляет план выполнения запроса: table Поле table — это имя таблицы (или псевдоним) или ссылка на подзапрос. Таблицы перечисляются в порядке соединения, определенном MySQL, а не в том порядке, в котором отображаются в запросе. Верхняя таблица — это первая таблица, а нижняя таблица — последняя таблица. type Поле type — это метод доступа к таблице или тип доступа для поиска по индексу — смотрите первое примечание в разделе «Методы доступа к таблице» (см. выше). ALL означает полное сканирование таблицы (см. выше, «Сканирование таблицы»). index означает сканирование индекса (см. выше, «Сканирование индекса»). Любое другое значение — const, ref, range и т. д. — означает тип доступа для поиска по индексу (см. выше, «Поиск по индексу»). possible_keys В поле possible_keys перечислены индексы, которые может использовать MySQL, поскольку в запросе используется крайний левый префикс. Если индекс не указан в этом поле, то требование крайнего левого префикса не выполняется. key Поле key содержит имя индекса, который будет использоваться MySQL, или NULL, если индекс не может быть использован. MySQL выбирает лучший индекс на основе многих факторов, некоторые из которых указаны в поле Extra (см. ниже). Можно с уверенностью сказать, что MySQL будет использовать этот индекс при выполнении запроса (EXPLAIN не выполняет запрос), но см. ниже: «Это ловушка! (Когда MySQL выбирает другой индекс)». ref В поле ref указан источник значений, используемых для поиска строк в индексе (поле key ). Для однотабличных запросов или для первой таблицы в соединении значение ref часто равно const, что относится к постоянному условию для одного или нескольких столбцов индекса. Постоянным условием является равенство (= или <=> [NULL-безопасное равенство]) литеральному значению. Например, a = 'Au' — это постоянное условие, равное только одному значению. Для запросов, которые соединяют несколько таблиц, ref — это ссылка на столбец из предыдущей таблицы в порядке соединения. MySQL присоединяется к текущей таблице (поле table), используя индекс для поиска строк, которые соответствуют значениям из столбца ref в предыдущей таблице. Раздел «Соединение таблиц» (см. ниже) показывает, как это работает.
Индексы и индексирование | 67 rows Поле rows — это предполагаемое количество строк, которые MySQL проверит, чтобы найти совпадающие строки. MySQL использует статистику индекса для оценки строк, поэтому реальное число — «проверенные строки» (см. выше) — вероятно, будет близким, но другим. Extra Поле Extra предоставляет дополнительную информацию о плане выполнения запроса. Это поле важно, поскольку оно указывает на оптимизацию запросов, которую может применить MySQL, если таковая имеется. Все выходные данные EXPLAIN в этой книге представлены в традиционном формате: табличный вывод (EXPLAIN query;) или вывод списка (EXPLAIN query\G). Другими форматами являются JSON (EXPLAIN FORMAT= JSON query) и, начиная с MySQL 8.0.16, tree (EXPLAIN FORMAT=TREE query). Форматы JSON и tree полностью отличаются от традиционного, но все форматы передают план выполнения запроса. Не ожидайте, что вы получите много информации из этих полей без контекста: таблицы, индексов, данных и запроса. В следующих разделах все иллюстрации относятся к таблице elem (пример 2.1), ее двум индексам и десяти строкам. WHERE MySQL может использовать индекс для поиска строк, соответствующих условиям таблицы в выражении с WHERE. Я осторожно говорю, что MySQL может использовать индекс, а не что MySQL будет это делать, потому что использование индекса зависит от нескольких факторов, и в первую очередь от условий таблицы, индексов и требования крайнего левого префикса (см. выше, «Требование крайнего левого префикса»). Есть и другие факторы, такие как статистика индексов и затраты на оптимизацию, но они выходят за рамки этой книги. Условие таблицы (англ. table condition) — это столбец и его значение (если таковые имеются), которые сопоставляют, группируют, агрегируют или упорядочивают строки. (Для краткости я использую термин «условие» (англ. condition), когда он однозначен). Для выражений с WHERE условия таблицы также называются предикатами (англ. predicates). На рис. 2.10 показан первичный ключ для столбца id и WHERE с единственным условием: id = 1. Рис. 2.10. WHERE: поиск по первичному ключу
68 | Глава 2 Рамка определяет условие таблицы и столбец индекса (также называемый частью индекса, англ. index part), которые может использовать MySQL, поскольку первое (условие таблицы) является крайним левым префиксом последнего (индекса). Стрелка указывает от условия таблицы к столбцу индекса, который он использует. Позже мы увидим примеры условий таблицы и столбцов индекса, которые MySQL не может использовать. На рис. 2.10 MySQL может находить строки, соответствующие условию id = 1, используя столбец id первичного ключа. Пример 2.3 — это план EXPLAIN для полного запроса. Пример 2.3. План EXPLAIN для поиска первичного ключа EXPLAIN SELECT * FROM elem WHERE id = 1\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL type: const possible_keys: PRIMARY key: PRIMARY key_len: 4 ref: const rows: 1 filtered: 100.00 Extra: NULL В примере 2.3 key: PRIMARY подтверждает, что MySQL будет использовать первичный ключ — поиск по индексу. Соответственно тип доступа (поле type) — это не ALL (сканирование таблицы) или index (сканирование индекса), что ожидается при простом поиске по первичному ключу. Вторичный индекс не указан в поле possible_keys, поскольку MySQL не может использовать его для этого запроса: столбец id не является крайним левым префиксом вторичного индекса для столбцов a, b. Тип доступа const — это особый случай, который возникает только при наличии постоянных условий (ref: const) для всех столбцов индекса первичного ключа или уникального вторичного индекса. В результате получается постоянная строка. Это слишком подробно для введения, но раз уж мы здесь, давайте продолжим учиться. Учитывая данные таблицы (пример 2.1) и тот факт, что столбец id является первичным ключом, строка, идентифицируемая при помощи id = 1, может рассматриваться как постоянная, поскольку при выполнении запроса id = 1 может соответствовать только одной строке (или ни одной строке). MySQL считывает эту одну строку и обрабатывает ее значения как постоянные, что прекрасно для времени отклика: const доступ выполняется чрезвычайно быстро.
Индексы и индексирование | 69 Extra: NULL встречается довольно редко, потому что реальные запросы более сложны, чем эти примеры. Но здесь Extra: NULL означает, что MySQL не нужно сопоставлять строки. Почему? Потому что постоянная строка может соответствовать только одной строке (или ни одной). Но совпадение (соответствие) строк — это нормально, поэтому давайте рассмотрим более реалистичный пример, изменив условия таблицы на id > 3 AND id < 6 AND c = 'Cd', как показано на рис. 2.11 и в соответствующем плане EXPLAIN в примере 2.4. Рис. 2.11. WHERE: доступ к диапазону с использованием основного ключа Пример 2.4. План EXPLAIN для доступа к диапазону с использованием первичного ключа EXPLAIN SELECT * FROM elem WHERE id > 3 AND id < 6 AND c = 'Cd'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL > type: range possible_keys: PRIMARY key: PRIMARY key_len: 4 > ref: NULL > rows: 2 filtered: 10.00 > Extra: Using where Чтобы выделить изменения плана EXPLAIN, я добавляю символы > к соответствующим полям, которые изменились. Эти подсказки не являются частью EXPLAIN. При изменении условий таблицы на id > 3 AND id < 6 AND c = 'Cd' план EXPLAIN изменяется с примера 2.3 на пример 2.4, что более реалистично для однотабличного запроса. Запрос по-прежнему использует первичный ключ (key: PRIMARY), но тип доступа меняется на сканирование диапазона (type: range): использование индекса для чтения строк в диапазоне значений. В этом случае MySQL использует первичный ключ для чтения строк, где значение столбца id находится в диапазоне от 3 до 6. Значение поля ref — NULL, поскольку условия для столбца id не являются постоянными (и это однотабличный запрос, поэтому нет предыдущей таблицы для ссылки).
70 | Глава 2 Условие c = 'Cd' является постоянным, но не используется для поиска по индексу (сканирования диапазона), поэтому ref не применяется. MySQL оценивает это: проверит две строки в диапазоне (rows: 2). В данном тривиальном примере оценка верна, но помните: rows — это прогноз. «Using where» в поле Extra настолько распространено, что ожидаемо. Это означает, что MySQL найдет совпадающие строки, используя условия WHERE так: для каждого чтения строки строка соответствует, если все условия WHERE верны. Поскольку условия для столбца id определяют диапазон, на самом деле это просто условие для столбца c, которое MySQL будет использовать для сопоставления строк в диапазоне. Оглянемся на пример 2.1: одна строка соответствует всем условиям WHERE: id 4 a Ar b Br c Cd В то время как строка с id = 5 находится в границах диапазона, поэтому MySQL проверяет ее, но значение столбца c в ней («C») не соответствует WHERE, поэтому MySQL не возвращает строку. Чтобы проиллюстрировать другие планы выполнения запроса, давайте используем оба крайних левых префикса вторичного индекса, как показано на рис. 2.12, и соответствующие планы EXPLAIN из примера 2.5. Рис. 2.12. WHERE: поиск по вторичному индексу Пример 2.5. Планы EXPLAIN для поиска по вторичным индексам EXPLAIN SELECT * FROM elem WHERE a = 'Au'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL > type: ref possible_keys: idx_a_b > key: idx_a_b
Индексы и индексирование | 71 key_len: 3 ref: const rows: 1 filtered: 100.00 Extra: NULL EXPLAIN SELECT * FROM elem WHERE a = 'Au' AND b = 'Be'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL >type: ref possible_keys: idx_a_b > key: idx_a_b key_len: 6 ref: const,const rows: 1 filtered: 100.00 Extra: NULL Для каждого плана EXPLAIN в примере 2.5 key: idx_a_b подтверждает, что MySQL использует вторичный индекс, поскольку условия удовлетворяют требованию крайнего левого префикса. Первый WHERE использует только первую часть индекса: столбец a. Второй оператор WHERE использует обе части индекса: столбцы a и b. Использование только столбца b не соответствовало бы требованию крайнего левого префикса — скоро я покажу это. Что здесь нового и важного по сравнению с предыдущими планами EXPLAIN, так это тип доступа: ref. Проще говоря, тип доступа ref — это поиск равенства (= или <=>) по крайнему левому префиксу индекса (поле key). Как и любой поиск по индексу, доступ типа ref выполняется очень быстро, если предполагаемое количество строк для проверки (поле rows) является разумным. Хотя условия являются постоянными, тип доступа const невозможен, поскольку индекс (key: idx_a_b) неуникален, и поэтому поиск может соответствовать более чем одной строке. И хотя по оценке MySQL каждый оператор WHERE будет проверять только одну строку (row: 1), это может измениться при выполнении запроса. Extra: NULL снова возникает, потому что MySQL может находить совпадающие строки, используя только индекс, поскольку для неиндексированных столбцов нет условий — поэтому давайте добавим это. На рис. 2.13 показан WHERE с условиями для столбцов a и c, а пример 2.6 соответствует плану EXPLAIN. Пример 2.6. План EXPLAIN для поиска по индексу и неиндексированного столбца EXPLAIN SELECT * FROM elem WHERE a = 'Al' AND c = 'Co'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem
72 | Глава 2 partitions: type: possible_keys: key: key_len: ref: > rows: filtered: > Extra: NULL ref idx_a_b idx_a_b 3 const 3 10.00 Using where На рис. 2.13 нет рамки вокруг условия c = 'Co', потому что индекс не покрывает столбец c. MySQL по-прежнему использует вторичный индекс (key: idx_a_b), но условие для столбца c не позволяет MySQL сопоставлять строки, используя только индекс. Вместо этого MySQL использует индекс для поиска и чтения строк для условия в столбце a, затем он сопоставляет строки для условия в столбце c (Extra: Using where). Рис. 2.13. WHERE: поиск по индексу и неиндексированный столбец Еще раз взглянув на пример 2.1, вы заметите, что ни одна строка не соответствует этому WHERE, но EXPLAIN выдает: «rows: 3». Почему? Поиск по индексу в столбце a соответствует трем строкам, для которых верно a = 'Al': с id строк 3, 8 и 9. Но ни одна из этих строк также не соответствует c = 'Co'. Запрос проверяет три строки, но не соответствует ни одной. rows в выходных данных EXPLAIN — это предварительная оценка количества строк, которые MySQL будет проверять при выполнении запроса, а не количество строк, которые будут соответствовать всем условиям таблицы В качестве последнего примера для индексов, WHERE и EXPLAIN давайте не будем выполнять требование крайнего левого префикса, как на рис. 2.14 и в примере 2.7. Рис. 2.14. WHERE без крайнего левого префикса
Индексы и индексирование | 73 Пример 2.7. План EXPLAIN для WHERE без крайнего левого префикса EXPLAIN SELECT * FROM elem WHERE b = 'Be'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL > type: ALL possible_keys: NULL > key: NULL key_len: NULL ref: NULL rows: 10 filtered: 10.00 Extra: Using where Контур пунктирной рамки (и отсутствие стрелки) очерчивает условие таблицы и столбец индекса, которые MySQL не может использовать, поскольку они не соответствуют требованию крайнего левого префикса. На рис. 2.14 условие по столбцу a отсутствует, поэтому индекс не может быть использован для условия по столбцу b. План EXPLAIN (пример 2.7) подтверждает это: possible_keys: NULL и key: NULL. Без индекса MySQL вынужден выполнять полное сканирование таблицы: type: ALL. Аналогично, rows: 10 отражает общее количество строк, а Extra: Using where отражает, что MySQL считывает, а затем фильтрует строки, не соответствующие b = 'Be'. Пример 2.7 — это вариант наихудшего возможного плана EXPLAIN. Всякий раз, когда вы видите type: ALL, possible_keys: NULL или key: NULL, прекратите то, что вы делаете, и проанализируйте запрос. Какими бы простыми ни были эти примеры, они представляют основы EXPLAIN в отношении индексов и WHERE. Реальные запросы имеют больше индексов и условий WHERE, но основные принципы не меняются. GROUP BY MySQL может использовать индекс для оптимизации GROUP BY, поскольку значения неявно группируются по порядку индекса. Для вторичного индекса idx_a_b (в столбцах a, b) существует пять различных групп значений столбца a, как показано в примере 2.8. Пример 2.8. Отдельные группы значений столбца a SELECT a, b FROM elem ORDER BY a, b; a Ag Ag b B B -- Группа Ag
74 Al Al Al Ar Ar Ar At Au | Глава 2 B B Br B Br Br Bi Be -- Группа Al -- Группа Ar -- Группа At -- Группа Au Я разделил группы в примере 2.8 и прокомментировал первую строку в каждой группе. Запрос с GROUP BY a может использовать индекс idx_a_b, поскольку столбец a является крайним левым префиксом, а индекс имплицитно (неявно) группируется по значениям столбца a. Пример 2.9 представляет собой репрезентативный план EXPLAIN для простейшего типа оптимизации GROUP BY. Пример 2.9. План EXPLAIN для GROUP BY a EXPLAIN SELECT a, COUNT(*) FROM elem GROUP BY a\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL > type: index possible_keys: idx_a_b key: idx_a_b key_len: 6 ref: NULL rows: 10 filtered: 100.00 > Extra: Using index key: idx_a_b подтверждает, что MySQL использует индекс для оптимизации GROUP BY. Поскольку индекс упорядочен, MySQL уверена, что каждое новое значение для столбца a является новой группой. Например, после считывания последнего значения «Ag» порядок индексов гарантирует, что больше значения «Ag» считываться не будут, поэтому наполнение группа «Ag» завершено. «Using index» в поле Extra указывает, что MySQL считывает значения столбца а только из индекса; а не читает полные строки из первичного ключа. Я рассматриваю эту оптимизацию ниже, в разделе «Покрывающие индексы». Этот запрос использует индекс, но не для поиска по индексу — type: index обозначает сканирование индекса (см. выше раздел «Сканирование индекса»). А т. к. здесь нет WHERE для фильтрации строк, MySQL читает все строки. Если вы добавите WHERE, MySQL по-прежнему сможет использовать индекс для GROUP BY, но все так же будет применяться крайний левый префикс. В этом случае запрос использует крайнюю
Индексы и индексирование | 75 левую часть индекса (столбец a), поэтому условие WHERE должно быть для столбца a или b, чтобы удовлетворить требование крайнего левого префикса. Давайте сначала добавим условие WHERE для столбца a, как показано на рис. 2.15 и в примере 2.10. Рис. 2.15. GROUP BY и WHERE для одного и того же столбца индекса Пример 2.10. Планы EXPLAIN для GROUP BY и WHERE на одном и том же столбце индекса EXPLAIN SELECT a, COUNT(a) FROM elem WHERE a != 'Ar' GROUP BY a\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL > type: range possible_keys: idx_a_b key: idx_a_b key_len: 3 ref: NULL rows: 7 filtered: 100.00 > Extra: Using where; Using index Using where в поле Extra относится к WHERE a != 'Ar'. Интересным изменением является type: range. Тип доступа по диапазону работает с оператором неравнозначности (!= или <>). Можете воспринимать это как WHERE a < 'Ar' AND a > 'Ar', как показано на рис. 2.16. Условие WHERE для столбца b по-прежнему может использовать индекс, поскольку условия, независимо от того, находятся ли они в разных выражениях SQL, удовлетворяют требованию крайнего левого префикса. Это продемонстрировано на рис. 2.17, а в примере 2.11 показан план EXPLAIN. Пример 2.11. План EXPLAIN для GROUP BY и WHERE в разных столбцах индекса EXPLAIN SELECT a, b FROM elem WHERE b = 'B' GROUP BY a\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem
76 | Глава 2 partitions: type: possible_keys: key: key_len: ref: rows: filtered: > Extra: NULL range idx_a_b idx_a_b 6 NULL 6 100.00 Using where; Using index for group-by Рис. 2.16. Диапазон для неравенства Рис. 2.17. GROUP BY и WHERE в разных столбцах индекса Запрос в примере 2.11 содержит две важные детали: условие равенства для столбца b, установленное WHERE, и выбор столбцов a и b в операторе SELECT. Эти сведения позволяют выполнить специальную оптимизацию — Using index for group-by (c англ. Использование индекса для оптимизации по группам), указанную в поле Extra. Если, например, равенство (=) изменяется на неравенство (!=), оптимизация запроса теряется. Когда дело доходит до оптимизации запросов, подобной этой, детали имеют решающее значение. Вы должны прочитать руководство по MySQL, чтобы изучить и применить детали. См. подробнее раздел «GROUP BY Optimization» («Оптимизация GROUP BY», https://oreil.ly/ZknLf) в руководстве по MySQL. Финальное про GROUP BY: на рис. 2.18 и пример 2.12 могут вас удивить. Рис. 2.18. GROUP BY без крайнего левого префикса
Индексы и индексирование | 77 Пример 2.12. План EXPLAIN для GROUP BY без крайнего левого префикса EXPLAIN SELECT b, COUNT(*) FROM elem GROUP BY b\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL > type: index possible_keys: idx_a_b key: idx_a_b key_len: 6 ref: NULL rows: 10 filtered: 100.00 > Extra: Using index; Using temporary Посмотрите на key: idx_a_b: MySQL использует индекс, хотя в запросе нет условия для столбца a. Что случилось с требованием крайнего левого префикса? Все выполняется, потому что MySQL сканирует индекс (type: index) в столбце a. Можете представить себе условие для столбца a, которое всегда будет выполняться — например, a = a. Будет ли MySQL по-прежнему индексировать сканирование столбца a для GROUP BY c? Нет, этого не произойдет; она выполнит полное сканирование таблицы. То, что показано на рис. 2.18, работает, потому что индекс имеет значения столбца b; он не имеет значений столбца c. «Using temporary» (с англ. Временное использование) в поле Extra является побочным эффектом отсутствия строгого набора условий крайнего левого префикса. Поскольку MySQL считывает значения столбца a из индекса, он собирает значения столбца b во временную таблицу (в памяти). После считывания всех значений столбца a идет сканирование временной таблицы с целью группировки и агрегирования для COUNT(*). О GROUP BY можно узнать гораздо больше по части индексов и оптимизации запросов, но таковы основные примеры. В отличие от выражений с WHERE, выражения с GROUP BY, как правило, проще. Задача в том, чтобы создать индекс для оптимизации GROUP BY плюс другие операторы SQL. MySQL сталкивается с той же задачей при формулировании плана выполнения запроса, поэтому она может не использовать оптимизацию GROUP BY даже тогда, когда это возможно. MySQL почти всегда выбирает наилучший план выполнения запросов, но если вы хотите поэкспериментировать с другими, прочтите «Index Hints» («Секреты индексирования», https://oreil.ly/ mbBof) в руководстве по MySQL.
78 | Глава 2 ORDER BY Неудивительно, что MySQL может использовать упорядоченный индекс для оптимизации ORDER BY. Эта оптимизация позволяет избежать сортировки строк, которая занимает немного больше времени, за счет доступа (обращения) к строкам по порядку. Без этой оптимизации MySQL считывает все совпадающие строки, сортирует их, а затем возвращает отсортированный результирующий набор. Когда MySQL сортирует строки, она выводит «Using filesort» в поле Extra плана EXPLAIN. Filesort (англ. дословно: сортировка файлов) означает сортировку строк. Это исторический (и теперь вводящий в заблуждение) термин, но он все еще распространен в жаргоне MySQL. Сортировка строк вызывает ужас у инженеров, потому что она имеет репутацию медленной. Это дополнительная работа, поэтому она не улучшает время отклика, но обычно не это основная причина медленного отклика. В конце этого раздела я использую EXPLAIN ANALYZE, новую фишку в MySQL 8.0.18, чтобы измерить, во что обходится сортировка строк в реальном времени. (Спойлер: она происходит очень быстро.) Но сначала давайте рассмотрим, как использовать индексы для оптимизации ORDER BY. Есть три способа использовать индекс для данной оптимизации. Первый и самый простой — взять крайний левый префикс индекса для ORDER BY. Для таблицы elem это означает:  ORDER BY id.  ORDER BY a.  ORDER BY a, b. Второй способ — сохранить крайнюю левую часть индекса постоянной и упорядочить по следующим столбцам индекса. Скажем, сохраняя столбец a постоянным и упорядочивая по столбцу b, как показано на рис. 2.19 с соответствующим планом EXPLAIN в примере 2.13. Рис. 2.19. ORDER BY и WHERE в разных столбцах индекса Пример 2.13. План EXPLAIN для ORDER BY и WHERE в разных столбцах индекса EXPLAIN SELECT a, b FROM elem WHERE a = 'Ar' ORDER BY b\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem
Индексы и индексирование partitions: type: possible_keys: key: key_len: ref: rows: filtered: Extra: | 79 NULL ref idx_a_b idx_a_b 3 const 3 100.00 Using index WHERE a = 'Ar' ORDER BY b может использовать индекс (a, b), потому что условие WHERE в первой части индекса (столбец a) является постоянным. Поэтому MySQL переходит к a = 'Ar' в индексе и оттуда считывает значения столбца b по порядку. Пример 2.14 — это результирующий набор, и хотя в нем нет ничего особенного, он показывает, что столбец a является постоянным (значение «Ar»), а столбец b отсортирован. Пример 2.14. Результирующий набор для WHERE a = 'Ar' ORDER BY b a Ar Ar AR b B Br Br Если бы у таблицы elem был индекс для столбцов a, b, c, запрос типа WHERE a = 'Au' AND b = 'Be' ORDER BY c мог бы использовать индекс, потому что условия для столбцов a и b содержат крайнюю левую часть индекса. Третий способ — это частный случай второго. Прежде чем я покажу вам рисунок, который объясняет это, посмотрите, можете ли вы определить, почему запрос в примере 2.15 не вызывает сортировку файлов (почему « Using filesort» не указано в поле Extra). Пример 2.15. План EXPLAIN для ORDER BY id EXPLAIN SELECT * FROM elem WHERE a = 'Al' AND b = 'B' ORDER BY id\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL type: ref possible_keys: idx_a_b key: idx_a_b key_len: 16 ref: const,const rows: 2
80 | Глава 2 filtered: 100.00 > Extra: Using index condition Понятно, что запрос использует индекс idx_a_b, потому что условия WHERE являются крайним левым префиксом, но разве ORDER BY id не должен вызывать сортировку строк? Ответ на рис. 2.20. Рис. 2.20. ORDER BY с использованием первичного ключа, добавленного ко вторичному индексу Раздел «Требование крайнего левого префикса» (см. выше) содержит абзац, начинающийся словами: «В конце (крайнем правом) каждого вторичного индекса скрывается первичный ключ». Вот что происходит на рис. 2.20: темное поле вокруг индексного столбца id показывает — «скрытый» первичный ключ, добавленный ко вторичному индексу. Эта оптимизация ORDER BY может показаться ненужной для такой маленькой таблицы, как elem, но стоит запомнить, что с реальными таблицами она может быть очень полезной. Чтобы доказать, что «скрытый» первичный ключ позволяет ORDER BY избежать сортировки строк, давайте удалим условие для столбца b: это сделает оптимизацию недействительной, как на рис. 2.21, за чем следует план EXPLAIN из примера 2.16. Рис. 2.21. ORDER BY без крайнего левого префикса Пример 2.16. План EXPLAIN для ORDER BY без крайнего левого префикса EXPLAIN SELECT * FROM elem WHERE a = 'Al' ORDER BY id\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL type: ref possible_keys: idx_a_b key: idx_a_b key_len: 8 ref: const rows: 3
Индексы и индексирование | 81 filtered: 100.00 > Extra: Using index condition; Using filesort Во вторичном индексе после удаления условия в столбце b больше нет крайнего левого префикса, который позволяет MySQL использовать «скрытый» первичный ключ для оптимизации ORDER BY. Поэтому для этого конкретного запроса в поле Extra отображается «Using filesort». Новая оптимизация — это Using index condition (с англ. Использование условия индекса), которое также называется Index condition pushdown (ICP) и означает, что подсистема хранения использует индекс для сопоставления строк для условий WHERE. Обычно подсистемы хранения данных только считывают и записывают строки, а MySQL обрабатывает логику сопоставления строк. Это четкое разделение задач (что является достоинством для разработки программного обеспечения), но оно неэффективно, когда строки не совпадают: и MySQL, и подсистема хранения данных тратят время на чтение несоответствующих строк. Для запроса в примере 2.16 index condition pushdown означает, что подсистема хранения (InnoDB) использует индекс idx_a_b для соответствия условию a = 'Al'. ICP помогает улучшить время отклика, но не напрягайтесь, пытаясь оптимизировать так, потому что MySQL использует его автоматически, когда это возможно. Чтобы узнать больше, прочтите «Index Condition Pushdown Optimization» («Оптимизация ICP», https://oreil.ly/L3Nzm) в руководстве по MySQL. Есть важная деталь, которая влияет на все оптимизации ORDER BY: порядок индекса — по умолчанию возрастающий, и ORDER BY col тоже подразумевает возрастание: ORDER BY col ASC. Оптимизация ORDER BY работает только в одном направлении для всех столбцов: ASC (по возрастанию) или DESC (по убыванию). Следовательно, ORDER BY a, b DESC не работает, потому что столбец a относится к имплицитной (неявной) сортировке ASC, что отличается от b DESC. MySQL 8.0 поддерживает индексы по убыванию (https://oreil.ly/FDTsN). Каковы же настоящие потери времени из-за сортировки строк? До MySQL 8.0.18 они не измерялись и не сообщались. Но, начиная с MySQL 8.0.18, EXPLAIN ANALYZE (https://oreil.ly/DFPiF) измеряет и сообщает это значение. Лишь только для примера 2.17 я использую другую таблицу. Пример 2.17. sbtest таблицы sysbench CREATE TABLE `sbtest1` ( `id` int NOT NULL AUTO_INCREMENT, `k` int NOT NULL DEFAULT '0', `c` char(120) NOT NULL DEFAULT '', `pad` char(60) NOT NULL DEFAULT '',
82 | Глава 2 PRIMARY KEY (`id`), KEY `k_1` (`k`) ) ENGINE=InnoDB; Это стандартная таблица sysbench (https://oreil.ly/XAYX2); я загрузил в нее миллион строк. Давайте используем случайный, бессмысленный запрос с большим результирующим набором и ORDER BY: SELECT c FROM sbtest1 WHERE k < 450000 ORDER BY id; -- Output omitted 68439 rows in set (1.15 sec) Запрос тратит 1,15 секунды, чтобы отсортировать и вернуть чуть более 68 000 строк. Но это неплохой запрос; ознакомьтесь с его планом EXPLAIN: EXPLAIN SELECT c FROM sbtest1 WHERE k < 450000 ORDER BY id\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: sbtest1 partitions: NULL type: range possible_keys: k_1 key: k_1 key_len: 4 ref: NULL rows: 133168 filtered: 100.00 Extra: Using index condition; Using MRR; Using filesort Единственная новая информация в этом плане EXPLAIN — это «Using MRR» в поле Extra, которое относится к «Multi-Range Read Optimization» («Оптимизации многодиапазонного чтения», https://oreil.ly/QX1wJ). В остальном EXPLAIN здесь сообщает сведения, уже рассмотренные в этой главе. Делает ли сортировка строк этот запрос медленным? EXPLAIN ANALYZE раскрывает ответ, хотя и загадочно: EXPLAIN ANALYZE SELECT c FROM sbtest1 WHERE k < 450000 ORDER BY id\G *************************** 1. row *************************** 1 -> Sort: sbtest1.id (cost=83975.47 rows=133168) 2 (actual time=1221.170..1229.306 rows=68439 loops=1) 3 -> Index range scan on sbtest1 using k_1, with index condition: (k<450000) 4 (cost=83975.47 rows=133168) (actual time=40.916..1174.981 rows=68439) Реальный результат EXPLAIN ANALYZE шире, но я свернул и пронумеровал строки для удобства печати и ссылки. Выходные данные EXPLAIN ANALYZE содержат много информации и требуют практики для работы; сейчас давайте перейдем прямо к делу — или настолько прямо, насколько получится, поскольку вывод не считывается последовательно. В строке 4 число 1174,981 (миллисекунды) означает, что скани-
Индексы и индексирование | 83 рование диапазона индексов (строка 3) заняло 1,17 секунды (округлено). В строке 2 число 1221.170..1229.306 означает, что сортировка файлов (строка 1) началась через 1221 миллисекунду и закончилась через 1229 миллисекунд, т. е. сортировка файлов заняла 8 миллисекунд. Общее время выполнения составляет 1,23 секунды: 95% строк чтения и менее 1% строк сортировки. Оставшиеся 4% — примерно 49 миллисекунд — тратятся на другие этапы: подготовку, статистику, ведение лога, очистку и т. д. Ответ отрицательный: сортировка файлов не замедляет этот запрос. Проблема заключается в доступе к данным: 68 439 строк — немалый результирующий набор. Сортировка 68 439 значений — практически нулевая работа для процессора, который выполняет миллиарды операций в секунду. Но чтение 68 439 строк — заметная нагрузка для реляционной базы данных, которая должна просматривать индексы, управлять транзакциями и т. д. Чтобы оптимизировать подобный запрос, обратитесь к разделу «Доступ к данным» (см. гл. 3). Последний вопрос, на который нужно ответить: почему у сортировки строк репутация медленности? Поскольку MySQL использует временные файлы на диске, когда сортировка данных превышает sort_buffer_size (https://oreil.ly/x5mbN), а жесткие диски работают на порядки медленнее, чем память. Это было действительно верно десятилетия назад, когда вращающиеся диски были повсеместны; но сегодня распространены SSD, а хранилище в целом довольно быстрое. Сортировка файлов может быть проблемой для запроса с высокой пропускной способностью (QPS), но используйте EXPLAIN ANALYZE для измерения и проверки. EXPLAIN ANALYZE выполняет запрос. В целях безопасности используйте EXPLAIN ANALYZE на реплике, доступной только для чтения, а не на источнике. Теперь вернемся к таблице elem (пример 2.1) и следующему случаю, для которого MySQL может использовать индекс: покрывающие индексы. Покрывающие индексы Покрывающий индекс включает в себя все столбцы, на которые ссылаются в запросе. На рис. 2.22 показан покрывающий индекс для оператора SELECT. Условия WHERE для столбцов a и b, как обычно, указывают на соответствующие столбцы индекса, но эти столбцы индекса также указывают на соответствующие столбцы в SELECT, чтобы продемонстрировать: значения для этих столбцов считываются из индекса. Рис. 2.22. Покрывающие индексы
84 | Глава 2 Обычно MySQL считывает полные строки из первичного ключа (вспомните раздел «Таблицы InnoDB являются индексами», см. выше). Но с покрывающим индексом MySQL может считывать только значения столбцов из индекса. Это наиболее полезно для вторичных индексов, поскольку позволяет избежать поиска по первичному ключу. MySQL автоматически использует оптимизацию покрывающего индекса, и EXPLAIN сообщает об этом фразой «Using index» (с англ. Использование индекса) в поле Extra. «Using index for group-by» (с англ. Использование индекса для GROUP BY)» — аналогичная оптимизация, специфичная для GROUP BY и DISTINCT (см. выше, раздел «GROUP BY»). Но «Using index condition» и «Using index for skip scan» — совершенно разные и не связанные между собой оптимизации. Сканирование индекса (type: index) плюс покрывающий индекс (Extra: Using index) — это сканирование только индекса (см. выше, «Сканирование индекса»). В разделе «GROUP BY» (см. выше) есть два примера: пример 2.9 и пример 2.12. Покрывающие индексы привлекательны, но редко практичны, потому что в реальных запросах слишком много столбцов, условий и предложений для покрытия одним индексом. Не тратьте время на создание покрывающих индексов. Однако при разработке или анализе простых запросов, где используется очень мало столбцов, найдите минутку и посмотрите, может ли такой индекс сработать. Если это так, то поздравляю. Если нет, то все в порядке; никто и не ждал, что это будет. Соединение таблиц (join tables) Для соединения таблиц MySQL использует индекс, и это использование в основном аналогично использованию индекса в иных целях. Основное различие заключается в источнике значений, задействованных в условиях соединения для каждой таблицы. Дальше я покажу это более наглядно, но сначала нам нужна вторая таблица для соединения. В примере 2.18 отражена структура таблицы elem_names и 14 строк, которые она содержит. Пример 2.18. Таблица elem_names CREATE TABLE `elem_names` ( `symbol` char(2) NOT NULL, `name` varchar(16) DEFAULT NULL, PRIMARY KEY (`symbol`) ) ENGINE=InnoDB; symbol Ag Al Ar At Au B name Silver Aluminum Argon Astatine Gold Boron
Индексы и индексирование Be Bi Br C Cd Ce Co Cr | 85 Beryllium Bismuth Bromine Carbon Cadmium Cerium Cobalt Chromium Таблица elem_names имеет один индекс: первичный ключ в столбце symbol. Значения в данном столбце совпадают со значениями в столбцах a, b и c таблицы elem. Следовательно, мы можем соединить таблицы elem и elem_names по этим столбцам. На рис. 2.23 показана инструкция SELECT, которая соединяет таблицы elem и а также дано графическое представление условий и индексов для каждой таблицы. elem_names, На предыдущих рисунках есть только одна пара index и SQL-выражений, потому что есть только одна таблица. Но на рис. 2.23 две пары — по одной для каждой таблицы — очерченные большими нацеленными вправо шевронами с именами таблиц, подписанными в каждом: /* elem */ и /* elem_names */. Как и EXPLAIN, эти рисунки показывают таблицы в порядке соединения: сверху вниз. Таблица elem (вверху) — первая в порядке соединения, а таблица elem_names (внизу) — вторая. Использование индекса в таблице elem не является чем-то новым или особенным: MySQL использует индекс для условия a IN (...). Пока все идет хорошо. Рис. 2.23. Соединение таблиц при поиске по первичному ключу
86 | Глава 2 Использование индекса в таблице elem_names, которая присоединена к предыдущей таблице, в целом такое же, но с двумя незначительными отличиями. Во-первых, WHERE — это переписанное JOIN...ON — подробнее об этом позже. Во-вторых, значения для условия в столбце symbol взяты из предыдущей таблицы: elem. Чтобы продемонстрировать это, стрелка из предыдущей таблицы указывает на ссылку на столбец в угловых скобках: <elem.a>. При соединении MySQL ищет строки в таблице elem_names, используя значения столбца a из соответствующих строк таблицы elem для условия соединения в столбце symbol. На языке MySQL мы бы сказали: «symbol равен столбцу a из таблицы elem». Учитывая значение из предыдущей таблицы, поиск по первичному ключу в столбце symbol не является чем-то новым или особенным: если строка совпадает, она возвращается и соединяется со строкой из предыдущей таблицы. Пример 2.19 показывает план EXPLAIN для инструкции SELECT из рис. 2.23. Пример 2.19. План EXPLAIN для соединения таблиц при поиске по первичному ключу EXPLAIN SELECT name FROM elem JOIN elem_names ON (elem.a = elem_names.symbol) WHERE a IN ('Ag', 'Au', 'At')\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL type: range possible_keys: idx_a_b key: idx_a_b key_len: 3 ref: NULL rows: 4 filtered: 100.00 Extra: Using where; Using index *************************** 2. row *************************** id: 1 select_type: SIMPLE table: elem_names partitions: NULL > type: eq_ref possible_keys: PRIMARY key: PRIMARY key_len: 2 > ref: test.elem.a rows: 1 filtered: 100.00 Extra: NULL
Индексы и индексирование | 87 Для каждой из таблиц план EXPLAIN в примере 2.19 не является чем-то новым, но соединение дает две новые вещи во второй таблице, elem_names. Первое — это тип доступа eq_ref: поиск в одной строке с использованием первичного ключа или уникального ненулевого вторичного индекса. (В этом контексте «ненулевой» означает, что все столбцы вторичного индекса определены как NOT NULL.) Подробнее о типе доступа eq_ref в следующем абзаце. Второе — ref: test.elem.a, что можно прочитать как «ссылочный столбец elem.a». (Имя базы данных — test, отсюда и префикс test.) Для соединения с таблицей elem_names значения из ссылочного столбца elem.a используются для поиска строк по первичному ключу (key: PRIMARY), который охватывает присоединяемый столбец: symbol. Это отвечает условию JOIN — ON (elem.a = elem_names.symbol). Для таблиц соединение не изменяет способ использования индексов. Основное отличие заключается в том, что значения для условия соединения берутся из предыдущей таблицы. MySQL может присоединять таблицу, используя любой метод доступа (см. выше, «Методы доступа к таблице»), но поиск по индексу с использованием типа доступа eq_ref лучше и быстрее, поскольку он соответствует только одной строке. Тип доступа eq_ref имеет два требования: первичный ключ или уникальный ненулевой вторичный индекс и условия равенства для всех столбцов индекса. Вместе эти требования гарантируют, что поиск eq_ref соответствует не более чем одной строке. Если оба требования не выполняются, то MySQL, вероятно, будет использовать поиск по индексу ref — по сути то же самое, но соответствует любому количеству строк. Возвращаясь к рис. 2.23: как я узнал, что нужно переписать оператор JOIN...ON на WHERE для таблицы elem_names? Если запустить SHOW WARNINGS сразу после EXPLAIN, MySQL выводит, как она переписывает запрос. Вот сокращенный вывод SHOW WARNINGS: /* select#1 */ select `test`.`elem_names`.`name` AS `name` from `test`.`elem` join `test`.`elem_names` where ((`test`.`elem_names`.`symbol` = `test`.`elem`.`a`) and (`test`.`elem`.`a` in ('Ag','Au','At'))) Теперь вы можете видеть, что /* elem_names */ WHERE symbol = <elem.a> на рис. 2.23 — это правильно. Иногда запуск SHOW WARNINGS сразу после EXPLAIN, чтобы увидеть, как MySQL переписывает запрос, необходим для понимания индексов и порядка соединения таблиц, выбранных MySQL.
88 | Глава 2 Переписанные инструкции SQL, отображаемые с помощью SHOW WARNINGS, не обязательно должны быть верными. Они предназначены лишь для того, чтобы показать, как MySQL интерпретирует и переписывает инструкцию SQL. Не выполняйте их. Порядок соединения таблиц имеет решающее значение, потому что MySQL соединяет таблицы в наилучшем возможном порядке, а не в том, в каком они записываются в запросе. Чтобы увидеть порядок соединения таблиц, необходимо использовать EXPLAIN. EXPLAIN выводит таблицы в порядке соединения сверху (первая таблица) донизу (последняя таблица). Алгоритм соединения по умолчанию, соединение с вложенным циклом (англ. nested-loop join), следует порядку. Я описываю алгоритмы соединения в конце этой главы (см. раздел «Алгоритмы соединения таблиц»). Не пытайтесь угадывать, каков будет порядок соединения таблиц, потому что небольшие изменения в запросе могут привести совсем к другому порядку или плану выполнения запроса. Продемонстрирую: инструкция SELECT на рис. 2.24 почти идентична инструкции SELECT на рис. 2.23, с одним небольшим отличием. Заметили его? Рис. 2.24. Присоединение таблицs при поиске по вторичному индексу Вот подсказка: это не золото и не серебро. Незначительная разница приводит к существенным отличиям в плане выполнения запроса, как показано в примере 2.20. Пример 2.20. План EXPLAIN соединения таблицы при поиске по вторичному индексу EXPLAIN SELECT name FROM elem JOIN elem_names ON (elem.a = elem_names.symbol) WHERE a IN ('Ag', 'Au')\G
Индексы и индексирование | 89 *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem_names partitions: NULL type: range possible_keys: PRIMARY key: PRIMARY key_len: 2 ref: NULL rows: 2 filtered: 100.00 Extra: Using where *************************** 2. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL type: ref possible_keys: idx_a_b key: idx_a_b key_len: 3 ref: test.elem_names.symbol rows: 2 filtered: 100.00 Extra: Using index Синтаксически инструкции SELECT на рис. 2.23 и 2.24 идентичны, но планы выполнения (примеры 2.19 и 2.20) существенно отличаются. Что изменилось? На рис. 2.24 из списка IN() было удалено одно значение: «At». Это отлично иллюстрирует, как, казалось бы, безобидное изменение может вызвать что-то в планировщике выполнения запросов MySQL, и вуаля: совершенно новый и другой план EXPLAIN. Давайте изучим пример 2.20 таблица за таблицей. Первая таблица — elem_names, что отличается от запроса, в котором: elem JOIN elem_names. Порядок соединения таблиц определяет MySQL, а не JOIN2. Поля type и key указывают на сканирование диапазона первичного ключа, но откуда берутся значения? Поле ref равно NULL, и в этой таблице нет условий WHERE. MySQL, должно быть, переписала запрос; вот сокращенный вывод SHOW WARNINGS: /* select#1 */ select `test`.`elem_names`.`name` AS `name` from `test`.`elem` join `test`.`elem_names` 2 Если только не используется STRAIGHT_JOIN, чего делать не следует. Позвольте оптимизатору запросов MySQL выбрать порядок соединения для наилучшего плана выполнения запроса.
90 | Глава 2 where ((`test`.`elem`.`a` = `test`.`elem_names`.`symbol`) and (`test`.`elem_names`.`symbol` in ('Ag','Au'))) Да, вот оно, в последней строке: MySQL переписывает запрос, чтобы использовать список IN() как значения для elem_names.symbols вместо первоначально указанного в запросе elem.a. Теперь вы можете видеть (или представить себе), что использование индекса в таблице elem_names.symbols — это сканирование диапазона для поиска двух значений: «Ag» и «Au». При использовании первичного ключа это чрезвычайно быстрый поиск по индексу и совпадение только с двумя строками, которые MySQL будет использовать для соединения со второй таблицей. Вторая таблица — это elem, и план EXPLAIN нам знаком: использование индекса idx_a_b для поиска значений индекса (не строк, потому что Extra: Using index), отвечающих условию в столбце a. Значения для этого условия берутся из соответствующих строк в предыдущей таблице, как указано в ссылке: test.elem_names.symbol. MySQL соединяет таблицы в наилучшем возможном порядке, а не в том порядке, в каком они записываются в запросе. Хотя MySQL может изменять порядок соединения и переписывать запрос, использование индекса для соединения в основном такое же — для каждой таблицы — как и все ранее описанное в этой главе. Используйте EXPLAIN и SHOW WARNINGS и рассмотрите план выполнения таблица за таблицей, сверху вниз. MySQL может соединять таблицы без индекса. Это называется полным соединением (англ. full join), и это худшее, что может сделать запрос. Сканирование таблицы в запросе с единичной таблицей — это плохо, но полное соединение хуже, потому что сканирование присоединенной таблицы выполняется не единожды, а для каждой совпадающей строки из предыдущей таблицы. Пример 2.21 показывает полное соединение во второй таблице. Пример 2.21. План EXPLAIN для FULL JOIN EXPLAIN SELECT name FROM elem STRAIGHT_JOIN elem_names IGNORE INDEX (PRIMARY) ON (elem.a = elem_names.symbol)\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL type: index possible_keys: idx_a_b key: idx_a_b key_len: 6 ref: NULL
Индексы и индексирование | 91 rows: 10 filtered: 100.00 Extra: Using index *************************** 2. row *************************** id: 1 select_type: SIMPLE table: elem_names partitions: NULL type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 14 filtered: 7.14 Extra: Using where; Using join buffer (hash join) Обычно MySQL не выбирает этот план выполнения запроса, поэтому мне пришлось принудительно использовать его с помощью STRAIGHT_JOIN и IGNORE INDEX (PRIMARY). Сканирование только по индексу в первой таблице (elem) захватывает все десять строк. (Строго говоря, сканирование таблицы elem только по индексу дает десять значений, а не строк, потому что полные строки не нужны: нужны только значения столбца a.) Для каждой строки MySQL присоединяет вторую таблицу (elem_names), выполняя полное сканирование таблицы (type: ALL), чтобы найти совпадающие строки. Поскольку это присоединенная таблица (а не первая таблица в порядке соединения), сканирование таблицы считается полным соединением. Полное соъединение — чуть ли не худшее, что бывает в запросе, потому что это происходит для каждой строки из предыдущей таблицы: десять полных сканирований для таблицы elem_names. Всякий раз, когда вы видите type: ALL для присоединенной таблицы, бросайте все, что вы делаете, и исправьте это. Для полных соединений есть метрика запроса: «Select full join» (см. выше). «Using join buffer (hash join)» в поле Extra относится к алгоритму хеш-соединения, нововведению для MySQL 8.0.18. Я описываю его (и другие алгоритмы соединения) в конце этой главы (см. раздел «Алгоритмы соединения таблиц»). Забегая вперед, приведу объяснение в двух словах: этот алгоритм создает хеш-таблицу значений в памяти и использует ее для поиска строк, а не для повторного сканирования таблицы. Хеш-соединение — это огромное повышение производительности. Несмотря на это, избегать полных соединений остается наилучшей практикой. До версии MySQL 8.0 запрос в примере 2.21 сообщал «Using join buffer (Block Nested Loop)» в поле Extra, потому что он использует другой алгоритм соединения: блочного с вложенным циклом (англ. block nested-loop), который описан ниже в разделе «Алгоритмы соединения таблиц». На первый взгляд, соединение таблиц кажется принципиально другим типом использования индекса, но это не так. Соединение затрагивает больше таблиц и ин-
92 | Глава 2 дексов, но для каждой таблицы использование индексов и требования все те же. Даже требование крайнего левого префикса остается прежним. Основное отличие в том, что для присоединенных таблиц значения для условий соединения берутся из предыдущей таблицы. Вы уже многое прочли после первого примера использования WHERE (см. одноименный раздел выше). Теперь вы увидели много полнотекстовых примеров индексов, запросов и планов EXPLAIN, которые охватывают технические тонкости и механику индексов MySQL. Это основа прямой оптимизации запросов, на чем и строится следующий раздел. Индексирование: думать, как MySQL Индексы и индексирование — это разные темы. В предыдущем разделе были представлены индексы: стандартные индексы B-дерева в таблицах InnoDB для WHERE, GROUP BY, ORDER BY, покрывающие индексы и индексы соединений таблиц. Этот раздел представляет индексирование: применение индексов для максимального эффекта. Вы не можете добиться потрясающей производительности, лишь проиндексировав каждый столбец. Если бы это было так просто, не было бы никаких администраторов баз данных. Для эффективного использования вы должны индексировать столбцы, которые позволяют MySQL получать доступ к наименьшему количеству строк при выполнении запроса. Выражаясь метафорически: максимально эффективен тот индекс, который точно сообщает MySQL, где найти иголку в стоге сена. По моему опыту, инженеры сопротивляются индексированию, поскольку им кажется, что MySQL «думает» о запросе так же, как они. Как инженеры, мы думаем о запросе в контексте приложения: какая часть приложения выполняет запрос, почему (бизнес-логика) и каков правильный результирующий набор. Но MySQL не знает и не заботится ни о чем из этого. MySQL мыслит в гораздо меньшем и более простом контексте: индексы и табличные условия. Под капотом MySQL значительно сложнее, но часть ее неуловимого очарования в том, как хорошо она скрывает эту сложность. Откуда мы знаем, что MySQL думает об индексах и условиях таблицы? EXPLAIN. И какова же основная информация, которую EXPLAIN сообщает? Таблицы (в порядке соединения), методы доступа к таблицам, индексы и информация в поле Extra, связанная с доступом к этим таблицам с помощью этих индексов. Мышление, подобное MySQL, облегчает индексирование, потому что это детерминированные машинные алгоритмы и эвристика. Человеческое мышление отягощено лишними деталями. Очистите свой разум и приготовьтесь мыслить как машина. В следующих четырех разделах описан простой четырехэтапный процесс. Узнайте запрос Первый шаг к тому, чтобы думать как MySQL, — это узнать основную информацию о запросе, который вы оптимизируете. Начните со сбора следующих метаданных для каждой таблицы:
Индексы и индексирование | 93  SHOW CREATE TABLE  SHOW TABLE STATUS  SHOW INDEXES Если запрос уже выполняется в рабочей среде, то получите отчет об этом запросе (см. гл. 1, раздел «Отчет о запросе») и ознакомьтесь с текущими значениями. Затем ответьте на следующие вопросы: Запрос  К скольким строкам должен обращаться запрос?  Сколько строк должен возвращать запрос?  Какие столбцы выбраны (возвращены)?  Каковы GROUP BY, ORDER BY и LIMIT (если таковые имеются)?  Существуют ли подзапросы? (Если да, повторите процесс для каждого из них). Доступ к таблице (потаблично)  Каковы условия таблицы?  Какой индекс следует использовать в запросе?  Какие другие индексы может использовать запрос?  Какова кардинальность каждого индекса?  Насколько велика таблица — размер данных и количество строк? Такие вопросы помогут вам мысленно проанализировать запрос, ведь именно это делает MySQL: анализирует запрос. Это особенно полезно, чтобы разобрать сложные запросы в более простых терминах: таблицы, условия таблиц, индексы и выражения SQL. Эта информация поможет вам собрать воедино пазл, который расскажет вам о времени отклика на запрос. Чтобы улучшить время отклика, вам нужно будет изменить некоторые компоненты. Но прежде чем сделать это, следующий шаг — собрать их с помощью EXPLAIN. Поймите с помощью EXPLAIN Второй шаг — понять текущий план выполнения запроса, и об этом плане сообщает EXPLAIN. Рассмотрим каждую таблицу и ее условия относительно ее индексов, начиная с индекса, выбранного MySQL: поле key в выходных данных EXPLAIN. Посмотрите на условия таблицы, чтобы увидеть, насколько они соответствуют требованию крайнего левого префикса для этого индекса. Если в поле possible_keys перечислены другие индексы, подумайте, как MySQL будет получать доступ к строкам с использованием этих индексов — всегда с учетом требования крайнего левого префикса. Если в поле Extra также есть информация (обычно она есть), то обратитесь к разделу «EXPLAIN Output» («Выходные данные EXPLAIN», https://oreil.ly/GDF0g) в руководстве по MySQL, чтобы с ней разобраться.
94 | Глава 2 Всегда выполняйте EXPLAIN для запроса. Сделайте это привычкой, потому что прямая оптимизация запросов невозможна без EXPLAIN. Запрос и его время отклика — это пазл, но у вас есть все его фрагменты: план выполнения, условия таблицы, структуры таблиц, размеры таблиц, кардинальность индексов и метрики запроса. Продолжайте соединять кусочки до тех пор, пока не соберете картинку — пока вы не увидите, что запрос работает так, как его объясняет MySQL. Для плана выполнения запроса всегда есть причина3. Порой MySQL очень умна и использует неочевидную оптимизацию запросов, обычно упомянутую в поле Extra. Если вы столкнетесь с одним из них для инструкции SELECT, раздел «Optimizing SELECT Statements» («Оптимизация инструкций SELECT», https:// oreil.ly/Bl4Ja) в руководстве по MySQL разъяснит это. Если вы застряли, есть три уровня поддержки, по возрастающей: 1. Начиная с версии MySQL 8.0.16, EXPLAIN FORMAT=TREE выводит более точный и описательный план выполнения запроса как древовидный отчет. Что совсем не похоже на привычный формат — поэтому вам нужно будет научиться правильно понимать эти данные, но оно того стоит. 2. Используйте трассировку оптимизатора (англ. optimizer tracing, https://oreil.ly/ Ump3C), чтобы увидеть чрезвычайно подробный план выполнения запроса с указанием затрат, соображений и причин. Это очень продвинутая функция с высокой кривой обучения, поэтому, если у вас мало времени, вы можете предпочесть третий вариант. 3. Обратитесь к своему администратору базы данных или наймите эксперта. Оптимизируйте запрос Третий шаг — это прямая оптимизация запроса: измените запрос, его индексы или и то и другое. Именно здесь происходит все самое интересное, и пока нет никакого риска, потому что эти изменения вносятся в среде разработки или промежуточной среде, а не в рабочей. Убедитесь, что в вашей среде разработки или промежуточной среде есть репрезентативные для рабочего процесса данные, поскольку размер и распределение данных влияют на то, как MySQL выбирает индексы. Сначала может показаться, что запрос не может быть изменен: если он извлекает правильные строки, значит, написан правильно. Запрос и есть запрос, да? Не всегда; один и тот же результат может быть достигнут разными методами. У запроса есть результат — буквально, результирующий набор — и способ получения этого результата. Эти две вещи тесно связаны, но независимы. Это очень полезно знать, когда вы исследуете, как изменить запрос. Начните с уточнения предполагаемого результата запроса. Четкий результат позволяет вам изучить новые способы написания запроса, которые достигают того же результата. 3 Несмотря на чрезвычайно редкие баги оптимизатора запросов.
Индексы и индексирование | 95 Может быть несколько способов написать запрос, который выполняется по-разному, но возвращает один и тот же результат. Например, некоторое время назад я помогал инженеру оптимизировать медленный запрос. Вопрос ко мне был техническим — что-то о GROUP BY и индексах, — но я спросил его: «Что делает запрос? Что он должен вернуть?» Он сказал: «О! Он возвращает максимальное значение для группы». Уточнив предполагаемый результат, я понял, что ему не нужно максимальное значение для группы, ему просто нужно максимальное значение. Следовательно, запрос был полностью переписан, чтобы прибегнуть к оптимизации ORDER BY col DESC LIMIT 1. Когда запрос чрезвычайно прост — например, SELECT col FROM tbl WHERE id = 1, возможно, действительно нет способа его переписать. Но чем проще запрос, тем меньше вероятность, что его придется переписывать. Если простой запрос выполняется медленно, решение, скорее всего, будет в изменении индексов, а не самого запроса. (И если изменения индекса не решают проблему, тогда путешествие продолжается: косвенная оптимизация запросов, рассмотренная в главах 3 и 4.) Добавление или изменение индекса — это компромисс между методами доступа и оптимизациями, специфичными для запросов. Например, променяете ли вы оптимизацию ORDER BY на сканирование диапазона? Не зацикливайтесь на попытках взвесить компромиссы; MySQL сделает это за вас4. Ваша задача проста: добавьте или измените индекс, который, по вашему мнению, обеспечит MySQL больше эффективности, затем примените EXPLAIN, чтобы узнать, согласна ли MySQL с использованием нового индекса. Повторяйте до тех пор, пока вы и MySQL не договоритесь о самом оптимизированном способе записи, индексирования и выполнения запроса. Не изменяйте индексы в рабочей среде до тех пор, пока вы полностью не проверите изменения в промежуточной среде. Развертывание и проверка Последним шагом является развертывание изменений и проверка — улучшают ли они время отклика. Но прежде знайте, как откатить развертывание — и будьте готовы сделать это, — если изменения приведут к непреднамеренным побочным эффектам. Это происходит по многим причинам. Два примера: запросы, выполняемые в рабочей среде, используют индекс, но не выполнялись в промежуточной среде; и рабочие данные существенно отличаются от тех, что были в промежуточной среде. Скорее всего, все будет хорошо, но будьте готовы к тому, что не все будет хорошо. 4 Попробуйте перехитрить MySQL, если вам скучно, но не надейтесь на победу. Она видела, как боевые корабли горели на подступах к Ореону. Она наблюдала, как сверкают си-лучи во мраке у врат Тангейзера).
96 | Глава 2 Всегда знайте, как выполнить откат развертывания в рабочей среде, и будьте готовы к этому. После развертывания проверьте изменения с помощью метрик запросов и метрик сервера MySQL. Если оптимизация запросов окажет значительное влияние, это отразится на метриках. (В главе 6 подробно рассматривается эта тема). Здорово, когда это происходит, но не удивляйтесь и не расстраивайтесь, если этого не произойдет, потому что самое важное изменение — время отклика на запрос: вспомните раздел «Краеугольный камень» (см. гл. 1). Подождите пять-десять минут (желательно дольше), затем проверьте время отклика в профиле запроса и отчете о запросе (см. гл. 1, разделы «Профиль запроса» и «Отчет о запросе»). Если время отклика улучшилось, поздравляю: вы делаете и достигаете того, что делают эксперты; с помощью этого навыка вы можете добиться замечательной производительности MySQL. Если не улучшилось, не волнуйтесь и не сдавайтесь: даже эксперты сталкиваются с запросами, требующими засучить рукава. Повторите процесс и подумайте о привлечении другого инженера, потому что некоторые запросы требуют тяжелой работы. Если вы уверены, что запрос не может быть дополнительно оптимизирован, то пришло время для второй части пути: косвенной оптимизации запросов. В главе 3 рассматриваются изменения в данных, а в главе 4 рассматриваются изменения в приложении. Это был хороший индекс, пока... Если ничего не изменится, хороший индекс останется хорошим индексом до конца времен. (Но если действительно ничего не изменится, закончится ли время когданибудь?) В реальности что-то изменится, сделает хороший индекс плохим и снизит производительность. Ниже приведены общие причины этого прискорбного (но предотвратимого и исправимого) снижения. Изменение запросов Когда запросы меняются, а они меняются часто, требование крайнего левого префикса может быть потеряно. Худший случай — это когда нет других индексов, которые MySQL может использовать, и тогда система возвращается к грубой силе: полному сканированию таблицы. Но таблицы часто имеют много индексов, а MySQL рассчитана на использование индекса, поэтому более вероятный случай — время отклика на запрос заметно ухудшится, потому что другие индексы не так хороши, как исходный. Анализ запросов и план EXPLAIN быстро выявляют эту картину. Если предположить, что изменения запроса были необходимы — обычно так и есть, — решение состоит в повторном индексировании для нового варианта запроса.
Индексы и индексирование | 97 Избыточные, повторяющиеся и неиспользуемые Индексы необходимы для повышения производительности, но иногда инженеры переусердствуют с ними, что приводит к слишком большому количеству индексов, а также дублирующимся индексам (дубликатам) и неиспользуемым. Какое количество индексов будет уже слишком большим? На один больше, чем необходимо. Переизбыток индексов создает две проблемы. Первая упомянута в разделе «Требование крайнего левого префикса» (см. выше): увеличенный размер индекса. Большее количество индексов использует больше оперативной памяти, что, по иронии судьбы, уменьшает объем оперативной памяти, доступной для каждого индекса. Вторая проблема заключается в снижении производительности записи, поскольку, когда MySQL записывает данные, она должна проверять, обновлять и потенциально реорганизовывать каждый индекс (внутреннюю структуру B-дерева для него). Чрезмерное количество индексов может серьезно снизить производительность записи. Когда вы создаете повторяющийся индекс, используемая для этого инструкция ALTER генерирует предупреждение, но вы должны задать команду SHOW WARNINGS, чтобы увидеть это. Чтобы обнаружить существующие дубликаты, используйте pt-duplicate-key-checker (https://oreil.ly/avm4L): он безопасно находит такие индексы и сообщает о них. Неиспользуемые индексы еще сложнее идентифицировать — что, например, если индекс используется раз в неделю длительным аналитическим запросом? За исключением этого пограничного случая, выполните следующий запрос, чтобы увидеть неиспользуемые индексы: SELECT * FROM sys.schema_unused_indexes WHERE object_schema NOT IN ('performance_schema'); Этот запрос использует MySQL sys Schema (https://oreil.ly/xxsL3), которая является коллекцией готовых представлений, возвращающих всевозможную информацию. Представление sys.schema_unused_indexes запрашивает таблицы схемы производительности и информационной схемы, чтобы определить, какие индексы не использовались с момента запуска MySQL. (Выполните команду SHOW CREATE VIEW sys.schema_unused_indexes, чтобы увидеть, как работает это представление.) Схема производительности должна быть активирована; если она еще не включена, обратитесь к своему администратору базы данных (или к тому, кто управляет MySQL), поскольку для ее активации требуется перезапуск MySQL. Будьте осторожны при отбрасывании индекса. Начиная с MySQL 8.0, используйте невидимые индексы (https://oreil.ly/Wx1xT), чтобы убедиться, что индекс не используется или не нужен, прежде чем удалять его: сделайте индекс невидимым, подождите и убедитесь, что это не влияет на производительность, и затем уже удалите его. Невидимые индексы отлично подходят для этой цели: при ошибке восстановить видимость индекса можно почти мгновенно, в то время как повторное добавление индекса для больших таблиц занимает минуты (или часы), что кажется вечностью, если ошибка приводит к сбою приложения.
98 | Глава 2 Для версий MySQL до 8.0 единственное решение — осторожность: поговорите со своей командой, выполните поиск в коде приложения и обратитесь к своим знаниям о приложении, чтобы тщательнейшим образом убедиться, что индекс не используется или не нужен. Будьте осторожны при отбрасывании (удалении) индексов. Если отброшенный индекс использовался в запросе, а MySQL не может использовать другой индекс, запрос вернется к полному сканированию таблицы. Если удаленный индекс влиял на несколько запросов, что не редкость, это может вызвать эффект домино в снижении производительности, что приведет к сбою приложения. Экстремальная селективность Кардинальность — это количество уникальных значений в индексе. Индекс для значений a, a, b, b имеет кардинальность 2: a и b — два уникальных значения. Используйте SHOW INDEX (https://oreil.ly/8hiGi), чтобы увидеть кардинальность индекса. Селективность — это кардинальность, деленная на количество строк в таблице. Обратимся к тому же примеру, a, a, b, b, где каждое значение представляет собой одну строку: селективность индекса равна 2/4 = 0,5. Селективность варьируется от 0 до 1, где 1 — уникальный индекс: значение для каждой строки. MySQL не показывает селективность индекса; вы должны вычислить его вручную, используя SHOW INDEX для кардинальности и SHOW TABLE STATUS для количества строк. Индекс с чрезвычайно низкой селективностью не дает особых преимуществ, поскольку каждое уникальное значение может соответствовать большому количеству строк. Классическим примером является индекс в столбце только с двумя возможными значениями: да или нет, истина или ложь, кофе или чай и т. д. Если в таблице 100 000 строк, то селективность практически равна нулю: 2/1000 = 0,00002. Это индекс, но не очень хороший, потому что каждое значение может соответствовать многим строкам. Скольким? Переверните деление: 100 000 строк / 2 уникальных значения = 50 000 строк на значение. Если бы MySQL использовал этот индекс (что маловероятно), один поиск по индексу мог бы соответствовать 50 000 строкам. Это предполагает, что значения распределены равномерно, но что если 99 999 строк имеют значение «кофе» и только одна строка имеет значение «чай»? Тогда индекс отлично подходит для чая, но ужасно для кофе. Если в запросе используется индекс с крайне низкой селективностью, посмотрите, можете ли вы создать более хороший, более селективный индекс; или рассмотрите возможность переписать запрос для использования более селективного индекса; или подумайте об изменении схемы для лучшей организации данных с учетом паттернов доступа — подробнее об этом в главе 4. Индекс с чрезвычайно высокой селективностью может быть слишком активно задействован. По мере того как селективность неуникального вторичного индекса приближается к 1, начинает возникать вопрос о том, должен ли индекс быть уникальным или, что еще лучше, может ли запрос быть переписан для использования первичного ключа. Такой индекс не влияет на производительность, но стоит изучить альтернативные варианты.
Индексы и индексирование | 99 Если имеется много вторичных индексов с чрезвычайно высокой селективностью, это, вероятно, указывает на паттерны доступа, которые просматривают или выполняют поиск по всей таблице по разным критериям или измерениям (при условии, что используются индексы, а не дубликаты). Например: таблица с описью продуктов, где приложение ищет данные по множеству различных критериев, для каждого из которых требуется индекс, отвечающий требованию крайнего левого префикса. В этом случае Elasticsearch (https://www.elastic.co) может обслуживать паттерны доступа лучше, чем MySQL. Это ловушка! (Когда MySQL выбирает другой индекс) Иногда MySQL выбирает неправильный индекс. Настолько редко, что это должно быть последним предположением в ситуации, где MySQL использует индекс, но время отклика на запрос необъяснимо медленное. Такое происходит по нескольким причинам. Распространенный вариант — когда при обновлении большого количества строк их числа чуть-чуть не хватает до запуска автоматического обновления статистических параметров индекса. Поскольку статистика индекса — один из многих факторов, влияющих на выбор индекса MySQL, статистика индекса, которая значительно отличается от реальности, может привести к тому, что MySQL выберет неправильный индекс. Чтобы было ясно: сам индекс никогда не бывает неточным; неточна только статистика индекса. Статистика индекса — это оценка того, как значения распределяются в индексе. MySQL выполняет случайные погружения в индекс для выборки страниц. (Страница — это единица логического хранилища размером 16 Кбайт. Почти все хранится в страницах). Если значения индекса распределены равномерно, то несколько случайных погружений довольно точно представляют весь индекс. MySQL обновляет статистику индекса для таблицы, когда:  Таблица открывается впервые.  Выполняется ANALYZE TABLE.  1/16 таблицы была изменена с момента последнего обновления.  опция innodb_stats_on_metadata включена и происходит одно из следующих со- бытий: • запущена команда SHOW INDEX или SHOW TABLE STATUS; • запрос обращается к INFORMATION_SCHEMA.TABLES или INFORMATION_SCHEMA.STATISTICS Запуск ANALYZE TABLE безопасен и обычно очень быстр, но будьте осторожны на нагруженном сервере: он требует блокировки сброса (англ. flush lock) (за исключением Percona Server), которая может препятствовать всем запросам, обращающимся к таблице.
100 | Глава 2 Алгоритмы соединения таблиц Краткий обзор алгоритмов соединения таблиц MySQL поможет вам подумать об индексах и индексировании при анализе и оптимизации JOIN. Алгоритм соединения таблиц по умолчанию называется соединением с вложенным циклом (англ. nestedloop join, NLJ), и он работает как вложенные циклы foreach в коде. Например, предположим, что запрос соединяет три таблицы с помощью JOIN: FROM t1 JOIN t2 ON t1.A = t2.B JOIN t3 ON t2.B = t3.C И предположим, что EXPLAIN сообщает следующий порядок соединения:t1, t2 и t3. Алгоритм соединения с вложенным циклом работает так же, как псевдокод в примере 2.22. Пример 2.22. Алгоритм NLJ func find_rows(table, index, conditions) []rows { // Возвращает массив строк в условиях // сопоставления с таблицей, // используя индекс для поиска или сканирования таблицы, // если NULL } foreach find_rows(t1, some_index, "WHERE ...") { foreach find_rows(t2, index_on_B, "WHERE B = <t1.A>") { return find_rows(t3, NULL, "WHERE C = <t2.B>") } } Следуя алгоритму NLJ, MySQL начинает с использования some_index для поиска совпадающих строк в самой внешней таблице: t1. Для каждой совпадающей строки в таблице t1 MySQL присоединяется к таблице t2, используя индекс в столбце соединения, index_on_B, для поиска строк, соответствующих t1.A. Для каждой совпадающей строки в таблице t2 MySQL присоединяется к таблице t3, используя тот же процесс, но добавим немного веселья: допустим, нет индекса в столбце соединения t3.C — результатом будет полное соединение. (Вспомните Select full join в главе 1 (раздел «Полное сканирование присоединенной таблицы») и пример 2.21 выше.) Когда больше ни одна строка в t3 не соответствует значению столбца соединения из таблицы t2, используется следующая соответствующая строка из t2. Когда больше ни одна строка в t2 не соответствует значению столбца соединения из таблицы t1, используется следующая соответствующая строка из t1. Когда больше ни одна строка в t1 не совпадает, запрос завершается. Алгоритм соединения с вложенным циклом прост и эффективен, но есть одна проблема: доступ к самой внутренней таблице осуществляется очень часто, а полное
Индексы и индексирование | 101 соединение делает этот доступ очень медленным. В этом примере доступ к таблице t3 осуществляется для каждой совпадающей строки в t1, умноженной на каждую совпадающую строку в t2. Если и t1, и t2 имеют 10 совпадающих строк, то к t3 обращаются 100 раз. Алгоритм блочного соединения с вложенным циклом решает эту проблему. Значения столбцов соединения из совпадающих строк в t1 и t2 сохраняются в буфере соединения (англ. join buffer). (Размер буфера соединения задается системной переменной join_buffer_size, https://oreil.ly/r1NeH). Когда он заполнен, MySQL сканирует t3 и присоединяет каждую строку t3, которая соответствует значениям столбца соединения в буфере соединения. Хотя доступ к буферу соединения осуществляется много раз (для каждой строки t3), он выполняется быстро, потому что это находится в памяти — значительно быстрее, чем 100 сканирований таблиц, необходимых для алгоритма NLJ. Начиная с версии MySQL 8.0.20, алгоритм блочного соединения с вложенным циклом заменяется алгоритмом хеш-соединения5. Хеш-соединение создает в памяти хеш-таблицу таблиц соединения, подобную таблице t3 в этом примере. MySQL использует хеш-таблицу для поиска строк в таблице соединений, что выполняется чрезвычайно быстро, поскольку поиск по хеш-таблице занимает постоянное время. Для получения более подробной информации прочтите «Оптимизация хешсоединения» в руководстве по MySQL. EXPLAIN указывает на хеш-соединение, выводя «С использованием буфера соединения (хеш-соединение)» (англ. Using join buffer (hash join)) в поле Extra. В соединениях MySQL есть больше деталей и нюансов, но этот краткий обзор поможет вам подумать о соединениях, подобных MySQL: по одной таблице за раз и по одному индексу на таблицу. Итоги В этой главе говорится об индексах и индексировании с помощью MySQL. Ключевые моменты:  Индексы обеспечивают наибольшее и наилучшее влияние на производитель- ность MySQL.  Не масштабируйте оборудование для повышения производительности до тех пор, пока не исчерпаете другие возможности.  Тюнинг (тонкая настройка) MySQL не обязателен для повышения производи- тельности при разумной конфигурации.  Таблица InnoDB — это индекс B-дерева, организованный по первичному ключу. 5 Хеш-соединение существует с MySQL 8.0.18, но заменяет блок вложенного цикла с MySQL 8.0.20.
102 | Глава 2  MySQL обращается к таблице с помощью поиска по индексу, сканирования по индексу или полного сканирования таблицы, поиск по индексу — лучший метод доступа.  Чтобы использовать индекс, запрос должен использовать крайний левый пре- фикс индекса — требование крайнего левого префикса.  MySQL использует индекс, чтобы находить строки, отвечающие WHERE, группи- ровать строки для GROUP BY, сортировать строки для ORDER BY, избегать чтения строк (покрывающий индекс) и соединять таблицы.  EXPLAIN выводит план выполнения запроса (или план EXPLAIN), в котором под- робно описывается, как MySQL выполняет запрос.  Индексирование требует мыслить подобно MySQL, чтобы понять план выпол- нения запроса.  Хорошие индексы могут потерять эффективность по целому ряду причин.  MySQL использует три алгоритма для соединения таблиц: алгоритм соединения с вложенным циклом (англ. nested-loop join, NLJ), алгоритм блочного соединения с вложенным циклом (англ. block nested-loop) и хеш-соединение (англ. hash join). В следующей главе мы начнём рассматривать косвенную оптимизацию запросов по отношению к данным. Практика: поиск повторяющихся индексов Цель этой практики — идентифицировать повторяющиеся индексы с помощью pt-duplicate-keychecker (https://oreil.ly/Oxvjr): инструмента командной строки, который их выводит. Практика проста, но полезна: загрузите и запустите pt-duplicate-key-checker. По умолчанию он проверяет все таблицы и выводит отчет для каждого повторяющегося индекса. Например: # #################################################################### # db_name.table_name # #################################################################### # idx_a is a left-prefix of idx_a_b # Key definitions: # KEY `idx_a` (`a`), # KEY `idx_a_b` (`a`,`b`) # Column types: # `a` int(11) default null # `b` int(11) default null # To remove this duplicate index, execute: ALTER TABLE `db_name`.`table_name` DROP INDEX `idx_a`;
Индексы и индексирование | 103 Для каждого индекса и его дубликата отчет включает:  причину: почему один индекс дублирует другой;  оба определения индекса;  определения столбцов, покрываемых индексами;  инструкцию ALTER TABLE для удаления повторяющегося индекса. pt-duplicate-key-checker — средство надежное, но всегда тщательно продумывайте действия, прежде чем удалять индекс, особенно в рабочей среде. Как и практика: определение медленных запросов (см. соответвующий раздел в гл. 1), эта практика проста, но вы бы удивились, узнав, что многие инженеры никогда не проверяют наличие повторяющихся индексов. Проверка и удаление дубликатов индексов — часть работы с MySQL на уровне эксперта.
104 | Глава 2
ГЛАВА 3 Данные В этой главе начинается вторая часть путешествия: косвенная оптимизация запросов. Как упоминалось в разделе «Улучшение времени отклика на запрос» (см. гл. 1), прямая оптимизация запросов решает множество проблем, но не все. Даже если вы выйдете за пределы знаний и навыков, описанных в главе 2, которая посвящена прямой оптимизации запросов, вы столкнетесь с запросами, которые просты и правильно индексируются, но все равно работают медленно. Именно тогда вам придется оптимизировать окружение запроса, начиная с данных, к которым он обращается. Чтобы понять почему, давайте подумаем о подводных камнях. Представьте, что ваша работа состоит в том, чтобы перемещать камни, и у вас есть три кучи камней разного размера. Первая кучка содержит камешки: очень легкие, не больше вашего большого пальца. Вторая куча содержит булыжники: тяжелые, но достаточно легкие, чтобы их можно было поднять, не больше вашей головы. Третья куча содержит валуны: слишком большие и тяжелые, чтобы их поднимать; для их перемещения нужны рычаги или машина. Ваша задача — переместить одну кучу с подножия холма на вершину (неважно почему; но, если это поможет, вообразите, что вы Сизиф). Какую кучу вы выберете? Я предполагаю, что вы выбираете гальку, потому что она легкая и ее легко перемещать. Но есть важная деталь, которая может изменить ваше решение: вес. Груда гальки весит две метрические тонны (вес внедорожника среднего размера). Груда булыжников весит одну метрическую тонну (вес очень маленького автомобиля). И есть только один валун, который весит полтонны (вес десяти взрослых людей). А теперь — какую кучу вы выберете? С одной стороны, гальку по-прежнему намного легче перемещать. Вы можете сгрести все в тачку и покатить ее вверх по склону. Их просто много (камешков, а не тачек). Валун весит лишь малую часть по сравнению со всей галькой, однако размер делает его неподъемным. Чтобы переместить его в гору, требуется специальное оборудование, но это разовая задача. Трудное решение. В главе 5 дан ответ и объяснение, но нам еще многое предстоит обсудить до этой главы. Данные аналогичны груде камней, а выполнение запросов аналогично перемещению камней в гору. Когда размер данных невелик, прямой оптимизации запросов обычно достаточно, потому что данные тривиальны для обработки — как ходьба (или бег) вверх по склону с горстью камешков. Но по мере увеличения объема данных косвенная оптимизация запросов становится все более важной — это как тащить тяжелый булыжник на холм и останавливаться на полпути, чтобы спросить: «Можем ли мы что-нибудь сделать с этими камнями?»
106 | Глава 3 В главе 1 представлено «доказательство» того, что объем данных влияет на производительность: TRUNCATE TABLE значительно повышает производительность, но не используйте эту «оптимизацию». Это шутка, но она также доказывает тезис, который не часто доводится до логического завершения: чем меньше данных, тем выше производительность. Это лишь лозунг, а полное утверждение таково: вы можете повысить производительность, сократив объем данных, потому что для уменьшения объема данных требуется меньше системных ресурсов (процессор, память, хранилище и т. д.). Вы уже можете сказать, что в этой главе мы будем настаивать на меньшем количестве данных. Но разве большее количество данных не является реальностью и причиной, побуждающей инженеров изучать оптимизацию производительности? Да, и в главе 5 рассматривается масштабирование MySQL, но сначала крайне важно научиться сокращать и оптимизировать данные, когда они относительно невелики, а проблемы — попроще. Самое напряженное время для обучения — когда приложение рушится, поскольку вы до последнего игнорировали объем его данных. В данной главе рассматриваются данные с точки зрения производительности, и утверждается, что сокращение доступа к данным и их хранения является методом — косвенной оптимизацией запросов — для повышения производительности. Здесь три основных раздела. Первый раскрывает три секрета производительности MySQL. Второй представляет то, что я называю принципом наименьшего количества данных, и его многочисленные последствия. В третьем разделе рассказывается, как быстро и безопасно удалять или архивировать данные. Три секрета Хранить тайну — значит скрывать правду. Следующие истины не всегда освещаются в книгах о производительности MySQL по двум причинам. Во-первых, они усложняют ситуацию. Намного проще писать и объяснять производительность, не упоминая предостережений и подводных камней. Во-вторых, они противоречат здравому смыслу. Это не делает их ложными, но затрудняет разъяснение. Тем не менее моменты ниже важны для производительности MySQL, поэтому давайте непредвзято разберемся в деталях. Индексы могут не помочь По иронии судьбы вы можете ожидать, что большинство медленных запросов будут использовать поиск по индексу. Это иронично по двум причинам. Во-первых, индексы являются ключом к производительности, но запрос может быть медленным даже при хорошем индексе. Во-вторых, после изучения индексов и индексирования (как обсуждалось в главе 2) инженеры настолько хорошо научились избегать сканирования индексов и таблиц, что остаются только поисковые запросы по индексам, что является приятной проблемой, но тем не менее забавной. Производительность не может быть достигнута без индексов, но это не означает, что индексы обеспечивают бесконечное использование для бесконечного объема данных. Не теряйте веры в индексы, но имейте в виду следующие случаи, в кото-
Данные | 107 рых индексы могут не помочь. Исходя из предположения, что запрос и его индексы не могут быть оптимизированы больше, следующим шагом здесь должна быть косвенная оптимизация запроса. Индексное сканирование Сканирование индекса теряет свою эффективность по мере роста таблицы, потому что индекс также растет: больше строк таблицы, больше значений индекса. (MySQL не поддерживает разреженные или частичные индексы). И напротив, эффективность поиска по индексу почти никогда не падает, пока индекс помещается в памяти. Даже сканирование только по индексу, как правило, не масштабируется, т. к. оно почти наверняка считывает большое количество значений — потому что MySQL выполнила бы поиск по индексу, чтобы прочитать меньше строк, если бы это было возможно. Сканирование индекса лишь оттягивает неизбежное: по мере увеличения количества строк в таблице время отклика на запросы, использующие индексное сканирование, также увеличивается. Обнаружение строк Когда я оптимизирую медленный запрос, использующий поиск по индексу, первая метрика запроса, которую я изучаю, — это проверенные строки (см. гл. 1, раздел «Проверенные строки»). Поиск совпадающих строк является основной целью запроса, но даже при хорошем индексе запрос может просмотреть слишком много строк. «Слишком много» — это точка, в которой время отклика становится неприемлемым (и основная причина не в чем-то другом, например в нехватке памяти или дисковых операциях ввода-вывода). Это происходит потому, что несколько типов доступа для поиска по индексу могут соответствовать многим строкам. Только типы доступа, перечисленные в табл. 3.1, соответствуют не более чем одной строке. Таблица 3.1. Типы доступа для поиска по индексу, отвечающие не более чем одной строке  system  const  eq_ref  unique_subquery Если поле type в плане EXPLAIN не относится к одному из типов доступа, перечисленных в табл. 3.1, то обратите пристальное внимание на поле rows и метрику запроса «rows examined» (см. гл. 1, раздел «Проверенные строки»). Проверка очень большого количества строк выполняется медленно, независимо от поиска по индексу. «EXPLAIN Output Format» («Формат выходных данных EXPLAIN», https://oreil.ly/ 8dkRy) в руководстве MySQL перечисляет типы доступа, которые там названы типами соединения, потому что MySQL обрабатывает каждый запрос как соединение. В этой книге для точности и согласованности я использую только два термина: метод доступа и тип доступа, как написано в главе 2.
108 | Глава 3 Очень низкая селективность по индексу также может иметь к этому отношение. Вспомните раздел «Экстремальная селективность» (см. гл. 2): селективность индекса — это его кардинальность, поделенная на количество строк в таблице. MySQL вряд ли выберет индекс с очень низкой селективностью, потому что он может соответствовать слишком большому количеству строк. Поскольку вторичные индексы требуют повторного поиска в первичном ключе для чтения строк, может быть быстрее отказаться от индекса с чрезвычайно низкой селективностью и вместо этого выполнить полное сканирование таблицы — предполагая, что более хорошего индекса нет. Вы можете обнаружить это в плане EXPLAIN, когда метод доступа представляет собой сканирование таблицы (type: ALL), но есть индексы, которые может использовать MySQL (possible_keys). Чтобы увидеть план выполнения, который MySQL не выбирает, запустите EXPLAIN для запроса с помощью FORCE INDEX (https://oreil.ly/nv1uy), чтобы использовать индекс, указанный в поле possible_keys. Скорее всего, итоговым планом выполнения будет сканирование индекса (type: index) с большим количеством строк, поэтому MySQL вместо этого выбирает сканирование таблицы. Вспомните раздел «Это ловушка! (Когда MySQL выбирает другой индекс)» (см. гл. 2): иногда MySQL выбирает неправильный индекс. Если запрос проверяет слишком много строк, но вы уверены, что есть индекс лучше, который следует использовать MySQL, есть небольшая вероятность, что статистика индекса неверна, в результате чего MySQL его не выбирает. Запустите ANALYZE TABLE, чтобы обновить статистику индекса. Помните, что селективность индекса зависит от его кардинальности и количества строк в таблице. Если кардинальность остается постоянной, но количество строк увеличивается, то селективность уменьшается. Следовательно, индекс, который помогал, когда таблица была маленькой, может не помочь, когда таблица огромна. Соединение таблиц При соединении таблиц несколько строк в каждой таблице быстро снижают производительность. Если вы помните алгоритмы соединения таблиц (см. в гл. 2 соответствующий раздел), алгоритм соединения с вложенным циклом (NLJ, пример 2.22) подразумевает, что общее количество строк, к которым обращаются для соединения, является произведением строк, к которым обращаются для каждой таблицы. Другими словами, умножьте значения для строк в плане EXPLAIN. Соединение из трех таблиц, содержащее всего сто строк в таблице, может получить доступ к одному миллиону строк: 100 × 100 × 100 = 1 000 000. Чтобы избежать этого, поиск по индексу в каждой присоединенной таблице должен соответствовать только одной строке — здесь лучше всего подходит один из типов доступа, перечисленных в табл. 3.1. MySQL может соединять таблицы практически в любом порядке. Используйте это в своих интересах: иногда решением проблемы плохого соединения является лучший индекс в другой таблице, который позволяет MySQL изменять порядок соединения.
Данные | 109 Без поиска по индексу соединение таблиц обречено. Результатом является полное соединение, как указано в разделе «Полное сканирование присоединенной таблицы» (см. гл. 1). Но даже с индексом соединение таблиц будет затруднено, если индекс не соответствует ни одной строке. Размер рабочего набора Индексы полезны только тогда, когда они находятся в памяти. Если значения индекса, которые ищет запрос, отсутствуют в памяти, то MySQL считывает их с диска. (Точнее, узлы B-дерева, составляющие индекс, хранятся на страницах (англ. pages) размером 16 Кбайт, и MySQL при необходимости меняет местами страницы между памятью и диском.) Чтение с диска на порядки медленнее, чем из памяти, что также представляет проблему, но главная проблема заключается в том, что индексы конкурируют между собой за память. Если память ограничена, но индексы многочисленны и часто используются для поиска большого процента значений (относительно размера таблицы), то использование индекса может увеличить ввод-вывод хранилища, поскольку MySQL пытается сохранить часто используемые значения индекса в памяти. Это возможно, но редко бывает по двум причинам. Во-первых, MySQL исключительно хороша в хранении часто используемых значений индексов в памяти. Во-вторых, часто используемые значения индекса и строки первичного ключа, на которые они ссылаются, называются рабочим набором (англ. working set), и обычно это небольшой процент от размера таблицы. Например, объем базы данных может составлять 500 Гбайт, но приложение чаще всего обращается только к 1 Гбайт данных. В свете этого факта базы данных MySQL обычно выделяют память только для 10% от общего объема данных, обычно округленного до стандартных значений памяти (64, 128 Гбайт и т. д.). 10% от 500 Гбайт составляет 50 Гбайт, поэтому администратор базы данных, вероятно, из осторожности округлит объем памяти до 64 Гбайт. Это работает чрезвычайно хорошо и является хорошей отправной точкой. В качестве отправной точки выделите память на 10% от общего объема данных. Размер рабочего набора обычно составляет небольшой процент от общего объема данных. Когда размер рабочего набора становится значительно больше доступной памяти, индексы могут не помочь. Вместо этого, подобно огню, который горит так жарко, что вода подпитывает его, а не тушит, использование индекса оказывает давление на ввод-вывод хранилища, и все замедляется. Увеличение объема памяти — это быстрое решение, но помните раздел «Более быстрое и мощное оборудование!» (см. гл. 2): масштабирование не является устойчивым подходом. Наилучший выход — разобраться с объемом данных и паттернами доступа, из-за которых получается большой рабочий набор. Если приложению действительно необходимо хранить такое количество данных (и получать к ним доступ), что размер рабочего
110 | Глава 3 набора не умещается в пределах разумного объема памяти на одном экземпляре MySQL, то решением является шардирование, которое рассматривается в главе 5. Чем меньше данных, тем лучше Опытные инженеры не радуются огромной базе данных, они стремятся справиться с ней. Они радуются, когда объем данных существенно сокращается, потому что чем меньше данных, тем лучше. Лучше для чего? Для всего: производительности, управления, стоимости и т. д. Просто на одном экземпляре MySQL намного быстрее, проще и дешевле обрабатывать 100 Гбайт данных, чем 100 Тбайт. Первая база так мала, что для нее хватит смартфона. Вторая требует специальной обработки: оптимизация производительности — более сложная задача, управление данными может быть рискованным (сколько времени требуется для резервного копирования и восстановления?), и удачи в поиске доступного оборудования на 100 Тбайт. Проще поддерживать разумный объем данных, чем справляться с огромной базой данных. Любой объем данных, который реально требуется, стоит времени и усилий для оптимизации и управления. Проблема заключается не столько в объеме данных, сколько в его необузданном росте. Для инженеров не редкость накапливать и хранить все на свете данные. Если вы думаете: «Только не я. Я не коплю данные», тогда замечательно. Но ваши коллеги могут не разделять ваше похвальное чувство аскетизма в этом смысле. В таком случае поднимите этот вопрос раньше, чем объем данных станет проблемой. Не позволяйте громоздкой базе данных застать вас врасплох. Отслеживайте объем данных (см. гл. 6, раздел «Объем данных») и, основываясь на текущих темпах роста, спрогнозируйте объем данных на следующие четыре года. Если будущий объем данных невозможен при текущем оборудовании и дизайне приложений, предотвратите проблему сейчас. Чем меньше QPS, тем лучше Возможно, вы никогда не найдете другой книги или специалиста, которые расскажут вам, что чем меньше QPS, тем лучше. Цените этот момент. Я понимаю, что этот секрет противоречит здравому смыслу, возможно, он даже непопулярен. Чтобы увидеть его истинность и мудрость, рассмотрим три менее неприятных момента в отношении QPS: QPS — это всего лишь число, измерение необработанной пропускной способности Это ничего не говорит о качестве запросов или производительности в целом. Одно приложение может эффективно простаивать со скоростью 10 000 QPS, в то время как другое перегружено и имеет перебои с вдвое меньшей пропускной способностью. Даже при одном и том же QPS существуют многочисленные качественные различия. Выполнение SELECT 1 при 1000 QPS почти не требует системных ресурсов, но сложный запрос с тем же QPS может быть очень обре-
Данные | 111 менительным для системных ресурсов. А высокий QPS — независимо от того, насколько он высок, — хорош лишь в том случае, если это время отклика на запрос. Значения QPS не имеют объективной ценности Сами по себе они ни хорошие, ни плохие, ни высокие, ни низкие, ни типичные, ни нетипичные. Значения QPS имеют значение только относительно приложения. Если одно приложение достигает в среднем 2000 QPS, то 100 QPS могут быть резким падением, указывающим на перебои в работе. Но если другое приложение составляет в среднем 300 QPS, то 100 QPS могут быть нормальным для него колебанием. QPS также может быть привязан к внешним событиям: времени суток, дню недели, сезонам, праздникам и т. д. QPS трудно увеличить Напротив, объем данных может относительно легко вырасти с 1 Гбайт до 100 Гбайт — в 100 раз. Но невероятно сложно увеличить в 100 раз QPS (за исключением крайне низких значений — например, от 1 до 100 QPS). Даже двукратного роста QPS может быть очень сложно достичь. Максимальный QPS относительно приложения увеличить еще сложнее, потому что вы не можете купить больше QPS, в отличие от хранилища и памяти. Подводя итог: QPS играет какую-то роль только по отношению к приложению, и его трудно увеличить. Чтобы поставить на этом точку: QPS вам не поможет. Это скорее пассив, чем актив. Следовательно, чем меньше QPS, тем лучше. Опытные инженеры радуются, когда QPS снижается (намеренно) — чем меньше QPS, тем больше возможностей для роста. Принцип наименьшего количества данных Я определяю принцип наименьшего количества данных следующим образом: хранение и доступ — только для нужных данных. В теории кажется очевидным, но на практике редко соблюдается. Это также обманчиво просто, поэтому в следующих двух разделах много мелких подробностей. Здравый смысл не так распространен (Вольтер) Доступ к данным Не открывайте доступ к большему количеству данных, чем необходимо. Понятие доступа относится ко всей работе, которую MySQL выполняет для выполнения запроса: поиск совпадающих строк, обработка совпадающих строк и возврат результирующего набора — как для операций чтения (SELECT), так и для записи. Эффективный доступ к данным особенно важен для операций записи, поскольку масштабировать операции записи сложнее. Табл. 3.2 представляет собой контрольный список, который вы можете применить к запросу (надеюсь, к каждому) для проверки эффективности доступа к данным.
112 | Глава 3 Таблица 3.2. Контрольный список эффективного доступа к данным      Возвращение только необходимых столбцов Снижение сложности запросов Ограничение доступа к строкам Ограничение набора результатов Избегание сортировки строк Справедливости ради, игнорирование одного элемента контрольного списка вряд ли повлияет на производительность. Например, пятый пункт — избегать сортировки строк — обычно игнорируется и на производительность не влияет. Но эти пункты являются лучшими практиками. Если вы будете применять их, пока они не войдут в привычку, то добьетесь большего успеха и производительности с MySQL, чем инженеры, которые полностью их игнорируют. Прежде чем я объясню каждый пункт в табл. 3.2, давайте вернемся к примеру из главы 1, который я отложил до этого места. Возможно, вы помните пример из раздела «Профиль запроса» (см. гл. 1): «Я пишу эти строки и просматриваю запрос с загрузкой 5962. Как это возможно?» Такая загрузка запросов возможна благодаря невероятно эффективному доступу к данным и чрезвычайно загруженному приложению. Запрос похож на SELECT col1, col2 WHERE pk_col = 5: поиск по первичному ключу, который возвращает только два столбца из одной строки. Когда доступ к данным настолько эффективен, MySQL функционирует почти как кеш в памяти и выполняет запрос с невероятной QPS и нагрузкой на запрос. Почти, но не полностью, потому что каждый запрос — это транзакция, которая влечет за собой накладные расходы. (Глава 8 посвящена транзакциям.) Чтобы оптимизировать подобный запрос, вы должны изменить паттерны доступа, поскольку сам он не может быть больше оптимизирован и объем данных не может быть уменьшен. Я вернусь к этому еще раз в главе 4. Возвращайте только необходимые столбцы Запросы должны возвращать только необходимые столбцы. Не делайте SELECT *. Это особенно важно, если в таблице есть какие-либо столбцы BLOB, TEXT или JSON. Вы, вероятно, слышали об этой лучшей практике раньше, потому что индустрия баз данных (а не только MySQL) твердит об этом десятилетиями. Я не могу вспомнить, когда в последний раз видел SELECT * в рабочем процессе, но это достаточно важно, чтобы продолжать об этом говорить. Уменьшите сложность запросов Запросы должны быть как можно проще. Сложность запроса относится ко всем таблицам, условиям и выражениям SQL, которые составляют запрос. Речь о сложности запроса как таковой, а не с точки
Данные | 113 зрения человека. Запрос SELECT col FROM tbl WHERE id = 1 менее сложен, чем запрос, который объединяет пять таблиц со многими условиями WHERE. Сложные запросы — проблема инженеров, а не MySQL. Чем сложнее запрос, тем сложнее его анализировать и оптимизировать. Если вам повезет, сложный запрос будет работать хорошо и никогда не отобразится как медленный (см. гл. 1, раздел «Профиль запроса»). Но полагаться на удачу — не лучшая (не передовая) практика. Делайте запросы простыми с самого начала (при первом написании) и уменьшайте их сложность, когда это возможно. Что касается доступа к данным, простые запросы, как правило, получают доступ (обращаются) к меньшему количеству данных, потому что задействуют меньше таблиц, условий и предложений SQL — меньше работы для MySQL. Но будьте осторожны: неправильное упрощение может привести к ухудшению плана EXPLAIN. Например, на рис. 2.21 в главе 2 показано, как удаление условия сводит на нет оптимизацию ORDER BY, что приводит к (немного) более плохому плану EXPLAIN. Всегда проверяйте, имеет ли более простой запрос эквивалентный или лучший план EXPLAIN — при том же результирующем наборе. Ограничьте доступ к строкам Запросы должны обращаться к как можно меньшему количеству строк. Доступ к слишком большому количеству строк обычно выходит непреднамеренно. Частая причина — рост данных с течением времени: вначале запрос обращается лишь к нескольким строкам, и потому он быстрый, но спустя годы и гигабайты становится медленным запросом, поскольку обращается уже к слишком большому количеству строк. Еще одна причина — обычные ошибки: инженер пишет запрос, который, по его мнению, будет обращаться всего к нескольким строкам, но ошибается. На пересечении роста данных и допущения обычных ошибок находится самая важная причина: отсутствие ограничений для диапазонов и списков. Открытый диапазон, такой как col > 75, может обращаться к бесчисленному количеству строк, если MySQL выполняет сканирование диапазона для col. Даже если это сделано намеренно, поскольку предполагается, что таблица небольшая, имейте в виду, что доступ к строкам практически не ограничен, а таблица растет, особенно если индекс в col неуникален. LIMIT не ограничивает доступ к строкам, поскольку применяется к результирующему набору после сопоставления строк. Исключением является оптимизация ORDER BY...LIMIT: если MySQL может получить доступ к строкам в порядке индекса, то она прекращает чтение, как только найдено соответствующее значению LIMIT количество строк. Но вот что интересно: EXPLAIN не сообщает, когда используется эта оптимизация. Вы должны догадаться об этом сами на основе того, что EXPLAIN сообщает, а что нет. Давайте воспользуемся моментом, чтобы увидеть эту оптимизацию в действии и установить, что она ограничивает доступ к строкам. Используя таблицу elem (пример 2.1) из главы 2, давайте сначала выполним запрос, в котором нет LIMIT. Пример 3.1 показывает, что запрос возвращает восемь строк.
114 | Глава 3 Пример 3.1. Строки для запроса без LIMIT SELECT * FROM elem WHERE a > 'Ag' ORDER BY a; id 8 9 3 10 4 5 7 2 8 rows a b c Al B Cd Al B Cd Al Br Cr Ar B Cd Ar Br Cd Ar Br C At Bi Ce Au Be Co in set (0.00 sec) Без оператора LIMIT запрос обращается к восьми строкам (и возвращает их). Соответственно EXPLAIN сообщает rows: 8 даже с оператором LIMIT 2, как показано в примере 3.2, потому что MySQL не может знать, сколько строк в диапазоне не совпадает, пока не выполнит запрос. Наихудший случай: MySQL считывает все строки, потому что запросу не соответствует ни одна. Но в этом простом примере мы можем видеть, что первые две строки (значения id 8 и 9) соответствовуют единственному условию таблицы. Если мы правы, метрики запросов будут сообщать о двух проверенных строках, а не о восьми. Но сначала давайте посмотрим, как вывести оптимизацию из плана EXPLAIN в примере 3.2. Пример 3.2. план EXPLAIN для оптимизации ORDER BY...LIMIT EXPLAIN SELECT * FROM elem WHERE a > 'Ag' ORDER BY a LIMIT 2\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: elem partitions: NULL type: range possible_keys: a key: a key_len: 8 ref: NULL rows: 8 filtered: 100.00 Extra: Using index condition Вы можете сделать вывод, что MySQL использует оптимизацию ORDER BY...LIMIT для доступа только к двум строкам (LIMIT 2), потому что:  в запросе используется индекс (type: range);  столбец ORDER BY является крайним левым префиксом этого индекса (key: a);  поле Extra не сообщает о «Using filesort».
Данные | 115 Доказательство приводится в примере 3.3: фрагмент лога медленных запросов после того, как MySQL выполнила запрос. Пример 3.3. Метрики запросов для оптимизации ORDER BY...LIMIT # Query_time: 0.000273 Lock_time: 0.000114 Rows_sent: 2 Rows_examined: 2 SELECT * FROM elem WHERE a > 'Ag' ORDER BY a LIMIT 2; в конце первой строки в примере 3.3 доказывает, что MySQL использовала оптимизацию ORDER BY...LIMIT для доступа только к двум строкам вместо всех восьми. Чтобы узнать больше об этой оптимизации запросов, прочитайте «LIMIT Query Optimization» («Оптимизация запросов LIMIT», https://orei.ly/ AnurD) в руководстве по MySQL. Что касается ограничения диапазонов и списков, необходимо проверить важный фактор: ограничивает ли приложение входные данные, используемые в запросе? Еще в разделе «Среднее значение, процентиль и максимум» (см. гл. 1) я рассказал историю: «Запрос использовался для поиска данных с целью обнаружения мошенничества, и время от времени просматривалось сразу несколько тысяч строк, что заставляло MySQL переключать планы выполнения запросов». В той ситуации решение было простым: ограничить ввод приложения тысячей значений на запрос. Rows_examined: 2 Этот случай также подчеркивает тот факт, что человек может вводить большой поток значений. Нередко инженеры стараются ограничить ввод, когда пользователь — другой компьютер, но их осторожность ослабевает, когда пользователь — другой человек: они думают, что человек не стал бы или не смог бы вводить слишком много значений. Но они ошибаются: с копипастом и приближающимся дедлайном обычный человек может перегрузить любой компьютер. Для операций записи ограничение доступа к строкам имеет решающее значение, поскольку, вообще-то говоря, InnoDB блокирует каждую строку, к которой обращается, прежде чем обновлять соответствующие строки. Следовательно, InnoDB может блокировать больше строк, чем вы могли бы ожидать. Раздел «Блокировка строк» (см. гл. 8) подробно это описывает. Для соединения таблиц ограничение доступа к строкам также имеет решающее значение: вспомните из раздела «Соединение таблиц» (см. выше), что при соединении несколько строк в каждой таблице быстро снижают производительность. В том разделе я указывал на то, что соединение таблиц обречено без поиска по индексу. В этом разделе я указываю на то, что соединение таблиц обречено вдвойне, если оно также не обращается к очень небольшому количеству строк. Помните: поиск по индексу, когда этот индекс неуникален, может обращаться к любому количеству повторяющихся строк. Знайте свои паттерны доступа: что ограничивает число обращений к строкам для каждого запроса? Используйте EXPLAIN для просмотра предварительной оценки числа строк, к которым обратится запрос (поле rows) и отслеживания числа проверенных строк (см. гл. 1, раздел «Проверенные строки»), чтобы избежать неожиданного доступа (обращения) к слишком большому количеству строк.
116 | Глава 3 Ограничьте результирующий набор Запросы должны возвращать как можно меньше строк. Это более сложная задача, чем добавление оператора LIMIT в запрос, хотя это, безусловно, помогает. Это относится к приложению, не использующему весь результирующий набор: строки, возвращаемые запросом. У такой проблемы три варианта. Первый вариант — когда приложение использует некоторые строки, но не все. Это может быть сделано специально или непреднамеренно. Если непреднамеренно, значит, WHERE требует более хороших (или большего количества) условий для сопоставления только необходимых строк. Вы можете обнаружить это в коде приложения, который фильтрует строки вместо использования условий WHERE. Если вы заметили подобное, поговорите со своей командой, чтобы уточнить, случайно ли это. Если это специально, то приложение может выбрать больше строк, чтобы избежать сложного запроса, перенеся сопоставление строк из MySQL в приложение. Этот метод полезен лишь тогда, когда сокращает время отклика — сродни тому, как MySQL иногда выбирает сканирование таблицы. Второй вариант — когда запрос содержит предложение ORDER BY, а приложение использует упорядоченное подмножество строк. Порядок строк не имеет значения для первого варианта, но это определяющая характеристика для второго. Например, запрос возвращает 1000 строк, а приложение использует только первые 20 строк по порядку. В этом случае решение может быть таким же простым, как добавление LIMIT 20 к запросу. Что приложение делает с оставшимися 980 строками? Если эти строки никогда не используются, то определенно запрос не должен их возвращать — добавьте LIMIT 20. Но если эти строки используются, то приложение, скорее всего, разбивается на страницы: использует 20 строк одновременно (например, показывает по 20 результатов на странице). В этом случае может быть быстрее и эффективнее использовать LIMIT 20 OFFSET N для выборки страниц по запросу, где N = 20 × (номер страницы – 1), но только если можно использовать оптимизацию ORDER BY...LIMIT (см. предыдущий раздел «Ограничьте доступ к строкам»). Оптимизация необходима, потому что без нее MySQL должна найти и отсортировать все совпадающие строки, прежде чем сможет применить часть OFFSET выражения LIMIT — много потраченной впустую работы, чтобы вернуть только 20 строк. Но даже без оптимизации есть другое решение: большой, но обоснованный LIMIT. Если, например, вы измеряете использование этого приложения и обнаруживаете, что большинство запросов задействуют только первые пять страниц, то используйте LIMIT 100 для извлечения первых пяти страниц и уменьшения размера результирующего набора на 90% для большинства запросов. Третий вариант проблемы возникает, когда приложение только агрегирует результирующий набор. Если приложение агрегирует результирующий набор и использует отдельные строки, это приемлемо. Антипаттерн только агрегирует результирующий набор вместо использования агрегатной функции SQL, которая ограничи-
Данные | 117 вает этот набор. В табл. 3.3 перечислены четыре антипаттерна и соответствующие SQL-решения. Таблица 3.3. Четыре антипаттерна результирующего набора в приложении Антипаттерн в приложении Решение в SQL Добавление значения столбца SUM(column) Подсчет количества строк COUNT(*) Подсчет количества значений COUNT(column)...GROUP BY column Подсчет количества различных значений COUNT(DISTINCT column) Извлечение различных значений DISTINCT Добавление значения столбца применяется к другим статистическим функциям: AVG(), MAX(), MIN() и т. д. Пусть MySQL выполняет вычисления, а не возвращает строки. Подсчет количества строк — дикий антипаттерн, но я встречал подобное, поэтому уверен, что есть и другие приложения, которые спокойно тратят пропускную способность сети на ненужные строки. Никогда не используйте приложение только для подсчета строк; используйте COUNT(*) в запросе. Начиная с MySQL 8.0.14, SELECT COUNT(*) FROM table (без WHERE) использует несколько потоков для параллельного чтения первичного ключа. Это не параллельное выполнение запроса; руководство по MySQL называет это «параллельным чтением кластеризованного индекса» (англ. parallel clustered index reads). Для подсчета количества значений, возможно, программистам проще использовать код, чем SQL-опцию GROUP BY, но последнее предпочтительнее для ограничения результирующего набора. Снова обратимся к таблице elem (пример 2.1): пример 3.4 демонстрирует, как подсчитать количество значений для столбца, используя COUNT(column)...GROUP BY column. Пример 3.4. Подсчет количества значений SELECT a, COUNT(a) FROM elem GROUP BY a; a Ag Al Ar At Au COUNT(a) 2 3 3 1 1 Для столбца a в таблице elem две строки имеют значение «Ag», три строки имеют значение «Al» и т. д. Решение SQL возвращает пять строк, в то время как антипаттерн вернет все десять. Это не драматические цифры — пять против десяти, но
118 | Глава 3 в этом и смысл: запрос может ограничить свой набор результатов путем агрегирования в SQL, а не в коде приложения. COUNT(*) против COUNT(column) COUNT(*) подсчитывает количество совпадающих строк — размер результирующего набора. COUNT(column) подсчитывает количество ненулевых значений в столбце соответствующих строк. Когда COUNT(column) используется с другими столбцами (включая его самого), вам нужно ввести GROUP BY для правильного агрегирования, как показано в примере 3.4. Извлечение отличающихся значений — дедупликация значений столбцов — обычное дело в приложении с ассоциативным массивом; но MySQL и это может — с помощью DISTINCT, что ограничивает результирующий набор. (DISTINCT считается агрегатной функцией, потому что это частный случай GROUP BY.) DISTINCT особенно понятен и полезен для одного столбца. Например, SELECT DISTINCT a FROM elem возвращает список уникальных значений из столбца a. (Если вам интересно, столбец a имеет пять уникальных значений: «Ag», «Al», «Ar», «At» и «Au».) Промашка с DISTINCT бывает в том, что он применяется ко всем столбцам. SELECT DISTINCT a, b FROM elem возвращает список уникальных строк со значениями из столбцов a и b. Чтобы узнать больше, ознакомьтесь с разделом «DISTINCT Optimization» («Оптимизация DISTINCT», https://oreil.ly/j3IjK) в руководстве по MySQL. Избегайте сортировки строк Запросы должны избегать сортировки строк. Сортировка строк в приложении вместо MySQL снижает сложность запроса за счет удаления ORDER BY, и это лучше масштабируется за счет распределения работы между экземплярами приложения — их масштабировать намного проще, чем MySQL. без LIMIT — явный признак того, что ORDER BY может быть удален и приложение может сортировать строки. (Также это может быть второй вариант проблемы, обсуждаемой выше.) Ищите запросы с ORDER BY, но без LIMIT, затем определите, может ли приложение сортировать строки вместо MySQL — ответ должен быть утвердительным. ORDER BY Хранение данных Не храните больше данных, чем необходимо. Хотя данные ценны для вас, для MySQL это мертвый груз. Табл. 3.4 представляет собой контрольный список для эффективного хранения данных. Я настоятельно рекомендую вам провести аудит вашего хранилища данных, потому что тут легко обнаружить сюрпризы. Я упоминал об одном таком сюрпризе в начале главы 2: созданное мною приложение, которое случайно сохранило один миллиард строк.
Данные | 119 Таблица 3.4. Контрольный список эффективного хранения данных       Сохраняются только необходимые строки Используется каждый столбец Каждый столбец компактен и практичен Каждое значение компактно и практично Используется каждый вторичный индекс, а не дубликат Хранятся только необходимые строки Если вы сможете отметить все шесть пунктов, то у вас будут очень хорошие возможности для масштабирования данных до любого объема. Хотя это непросто: некоторые элементы легче игнорировать, чем реализовать, особенно когда база данных невелика. Но не откладывайте: лучшее время для поиска и устранения недостатков хранения — как раз пока она такая. В масштабе один или два байта могут иметь большое значение, если умножить их на высокую пропускную способность и все 86 400 секунд в типичном земном дне. Разрабатывайте для масштабирования и планируйте для успеха. Сохраняются только необходимые строки По мере того как приложение меняется и растет, инженеры могут потерять представление о том, что в нем хранится. И пока хранение данных не вызывает проблем, у инженеров нет причин заниматься этим вопросом. Если данные приложения давно никто не просматривал или если вы новичок в команде или приложении, то взгляните. Я видел, например, забытые сервисы, записывающие данные (в течение многих лет, не меньше), которые никто не использовал. Используется каждый столбец На уровень глубже, чем хранение только необходимых строк, — это хранение только необходимых столбцов. Опять же, по мере изменения и расширения приложения инженеры могут упустить из виду отслеживание столбцов, особенно при использовании объектно-реляционного отображения (англ. object-relational mapping, ORM). К сожалению, в MySQL нет инструмента или автоматического способа поиска неиспользуемых столбцов. MySQL отслеживает, какие используются базы данных, таблицы и индексы, но не отслеживает использование столбцов. Нет ничего более тайного, чем неиспользуемый столбец. Единственным решением является проверка вручную: сравните столбцы, используемые запросами приложения, со столбцами в таблицах. Каждый столбец компактен и практичен На два уровня глубже, чем хранение только необходимых строк: каждый столбец должен быть компактным и практичным. Компактность означает использование наименьшего типа данных для хранения значений. Практичность означает неиспользование типа данных настолько маленького, что это обременительно или чре-
120 | Глава 3 вато ошибками для вас или приложения. Например, использование unsigned INT в качестве битового поля компактно (нет ничего меньше бита), но обычно непрактично. Ознакомьтесь со всеми типами данных MySQL. Классическим антипаттерном является тип данных VARCHAR(255). Этот конкретный тип и размер данных — распространенное, но неэффективное значение по умолчанию для многих программ и инженеров, которые, вероятно, скопировали эту практику у другой программы или инженера. Вы увидите, что он используется для хранения всего и вся, и поэтому неэффективен. Например, давайте опять обратимся к таблице elem (пример 2.1). Атомные символы — это один или два знака. Определение столбца atomic_symbol VARCHAR(255) технически компактно — VARCHAR имеет переменную длину, так что мог бы использовать только один или два знака, но он допускает «мусор на входе — мусор на выходе»: недопустимые значения, такие как «Carbon» вместо «C», что может иметь неизвестные последствия для приложения. Лучшим определением столбца будет atomic_symbol CHAR(2), что компактно и практично. Будет ли определение столбца atomic_symbol ENUM(...) еще лучше для таблицы elem? ENUM более компактен, чем CHAR(2), но окажется ли он практичнее, когда атомных символов больше сотни? Сами решайте, что вам подходит; однако любой вариант предпочтительнее, чем VARCHAR(255). ENUM (https://oreil.ly/WMXfA) — один из великих невоспетых героев эффективного хранения данных. Осторожно с набором символов столбца. Если иное не определено, по умолчанию используется набор символов таблицы, в котором, если также иное не определено, по умолчанию используется набор символов сервера. Начиная с версии MySQL 8.0, серверный набор символов по умолчанию — utf8mb4. Для MySQL 5.7 и старше серверный набор символов по умолчанию — latin1. В зависимости от набора символов один символ, такой как é, может храниться в виде нескольких байтов. Например, используя набор символов latin1, MySQL хранит é в виде одного байта: 0xE9. Но при utf8mb4 MySQL хранит его в виде двух байтов: 0xC3A9. (Эмодзи используют четыре байта на символ.) Наборы символов — это особый мир, выходящий за рамки большинства книг. На данный момент все, что вам нужно знать, — для одного символа может потребоваться несколько байт памяти, в зависимости от символа и набора символов. Байты быстро складываются в больших таблицах. Будьте очень бережны с типами данных BLOB, TEXT и JSON. Не используйте их в качестве свалки, универсальных или неуниверсальных ведер. Например, не храните изображения в BLOB — вы можете, это работает, но не делайте этого. Есть гораздо лучшие решения, такие как Amazon S3 (https://aws.amazon.com/s3).
Данные | 121 Компактность и практичность простираются вплоть до уровня бита. Другой удивительно распространенной, но легко устраняемой неэффективностью хранения столбцов является потеря бита старшего порядка целочисленных типов данных (https://oreil.ly/6CdwC). Например, использование INT вместо INT UNSIGNED: максимальное значение составляет примерно два миллиарда против четырех миллиардов соответственно. Если значение не может быть отрицательным, то используйте тип данных unsigned. Начиная с версии MySQL 8.0.17, UNSIGNED устарело для типов данных FLOAT, DOUBLE и DECIMAL. В мире разработки программного обеспечения подобные детали могут рассматриваться как микрооптимизация или преждевременная оптимизация, к которым относятся неодобрительно, но в мире проектирования схем и производительности баз данных это лучшие практики. Каждое значение компактно и практично На три уровня глубже, чем хранение только необходимых строк: каждое значение должно быть компактным и практичным. «Практичный» имеет то же значение, что и определение в предыдущем разделе, но «компактный» здесь означает наименьшее представление значения. Компактные значения сильно зависят от того, как приложение их использует. Например, рассмотрим символьную последовательность (англ. string) с одним начальным и одним конечным пробелом: « and ». В табл. 3.5 перечислены шесть способов, которыми приложение может сжать эту последовательность. Таблица 3.5. Шесть способов сжать символьную последовательность « and » Компактное значение Возможное использование “«and”» Удаление всех пробелов. Это обычное явление для символьных последовательностей “« and”» Удаление конечных пробелов. Во многих синтаксисах (таких как YAML и Markdown) начальный пробел имеет синтаксическое значение “«and ”» Удаление начального пробела. Возможно, это менее распространено, но все же возможно. Иногда используется программами для объединения аргументов, разделенных пробелами (например, аргументов командной строки) “«”» Удаления значения (пустая символьная последовательность). Возможно, это значение необязательно, например AS в FROM table AS table_alias, что может быть записано как FROM table table_alias “«&”» Замена символьной последовательности эквивалентным символом. В письменном языке символ амперсанда семантически эквивалентен слову «and» (с англ. — союзу «и») NULL Отсутствие значения. Возможно, значение совершенно излишне и может быть удалено, в результате чего значения не будет (даже пустой символьной последовательности, которая технически все еще является значением)
122 | Глава 3 Преобразования в табл. 3.5 представляют три способа сжатия значения: минимизацию, кодирование и дедупликацию. Минимизация. Чтобы минимизировать значение, удалите лишние и посторонние данные: пробелы, комментарии, заголовки и т. д. Давайте рассмотрим более сложное, но знакомое значение в примере 3.5. Пример 3.5. Форматированная инструкция SQL (не минимизированная) SELECT /*!40001 SQL_NO_CACHE */ col1, col2 FROM tbl1 WHERE /* comment 1 */ foo = ' bar ' ORDER BY col1 LIMIT 1; — comment 2 Если приложение в примере 3.5 хранит только функциональные части инструкции SQL3, то оно может минимизировать значение, свернув пробелы между ключевыми словами (не внутри значений) и удалив два последних комментария (не первый). Пример 3.6 — это минимизированное (компактное) значение. Пример 3.6. Минимизированная инструкция SQL SELECT /*!40001 SQL_NO_CACHE */ col1, col2 FROM tbl1 WHERE foo=' bar ' LIMIT 1 Примеры 3.5 и 3.6 функционально эквивалентны (одинаковый план EXPLAIN), но объем данных минимизированного значения почти на 50% меньше (48,9%): 137 и 70 байт соответственно. Для долгосрочного роста объема данных сокращение на 50% — или даже всего на 25% — будет значимым и действенным. Минимизация инструкции SQL иллюстрирует важный момент: минимизация значения не всегда проста. Инструкция SQL — не бессмысленная символьная последовательность: это синтаксис, который требует синтаксической осведомленности для правильной минимизации. Первый комментарий не может быть удален, потому что он функциональный. (См. раздел «Comments» («Комментарии», https://oreil.ly/ 3l8zy) в руководстве по MySQL). Аналогично пробел в заключенном в кавычки значении ' bar ' является функциональным: ' bar ' не равно 'bar'. И вы, возможно, заметили крошечную деталь: завершающая точка с запятой была удалена, потому что она не работает в этом контексте, но работает в других. При рассмотрении вопроса о том, как минимизировать значение, начните с его формата данных. Синтаксис и семантика формата данных определяют, какие дан-
Данные | 123 ные являются лишними и ненужными. В YAML, например, комментарии # вроде такого являются просто комментариями (в отличие от некоторых комментариев SQL) и могут быть удалены, если не нужны приложению. Даже если ваш формат данных создан на заказ, он должен иметь некоторый синтаксис и семантику, иначе приложение не сможет программно прочитать и записать его. Необходимо знать формат данных, чтобы правильно минимизировать значение. Наименьшее значение — это вообще никакого значения: NULL. Знаю, что работа с NULL может быть сложной задачей, но есть элегантное решение, которое я настоятельно рекомендую вам использовать: COALESCE()(https://orei.ly/muYZW). Например, если столбец middle_name (с англ. — отчество) имеет значение null (не у всех людей есть отчества), то используйте COALESCE(middle_name, ' '), чтобы вернуть значение, если оно задано, и вернуть пустую строку, если нет. Таким образом, вы получаете преимущества хранения NULL, для которого требуется всего один бит, без проблем с обработкой нулевых строк (или указателей) в приложении. Используйте NULL вместо пустых строк, нулевых значений и магических значений, когда это практично. Это требует немного дополнительной работы, но относится к лучшим практикам. NULL и NULL уникальны; т. е. два разных значения null уникальны. Избегайте уникальных индексов для столбцов, в которых есть NULL, или убедитесь, что приложение правильно обрабатывает повторяющиеся строки с NULL значениями. Если вы действительно хотите избежать использования NULL, предыдущее предупреждение — ваша техническая причина. Эти два набора значений уникальны: (1, NULL) и (1, NULL). Это не опечатка. Для людей эти значения выглядят идентичными, но для MySQL каждое из них уникально, потому что сравнение NULL с NULL не определено. Ознакомьтесь с разделом «Working with NULL Values» («Работа со значениями NULL», https://oreil.ly/oyTPZ) в руководстве по MySQL. Он начинается со скромного признания: «Значения NULL могут удивлять, пока вы к ним не привыкнете». Кодировка. Чтобы закодировать значение, преобразуйте его из удобочитаемого в машиный код. Это наиболее эффективный способ хранения данных на компьютере. Для компьютеров данные могут быть закодированы и сохранены одним способом, а для людей декодированы и отображены другим способом. Хранилище для машины, дисплей для человека. Типичным примером и антипаттерном является хранение IP-адреса в строковом виде (англ. string). Например, хранение 127.0.0.1 в строковом виде в столбце CHAR(15). IP-адреса представляют собой четырехбайтовые целые числа без знака — это истинная машинная кодировка. (Если вам интересно, 127.0.0.1 — это десятичное значение 2130706433.) Для кодирования и хранения IP-адресов используйте тип данных INT UNSIGNED и функции INET_ATON() и INET_NTOA() для преобразования в стро-
124 | Глава 3 ковый тип и из строкового типа соответственно. Если кодирование IP-адресов непрактично, то приемлемой альтернативой является тип данных CHAR(15). Другим подобным примером и антипаттерном является хранение UUID в строковом виде. UUID — это многобайтовое целое число, представленное в строковом виде. Поскольку длины байтов UUID различаются, вам необходимо использовать тип данных BINARY(N), где N — длина байта, и функции HEX() и UNHEX() для преобразования значения. Или, если вы используете MySQL 8.0 (или новее) и UUID RFC 4122 (которые генерирует UUID() в MySQL), вы можете использовать функции UUID_TO_BIN() и BIN_TO_UUID(). Если кодирование UUID нецелесообразно, сохраните, по крайней мере, строковое представление, используя тип данных CHAR(N), где N — длина символьной последовательности в знаках (символах). Существует более компактный метод хранения данных на основе машинного кодирования: сжатие. Но это экстремальный способ из серой зоны компромиссов между пространством и скоростью, что выходит за рамки этой книги. Я не видел случая, когда сжатие требовалось для повышения производительности или масштабирования. Строгое применение контрольного списка эффективного хранения данных (табл. 3.4) масштабирует данные до таких объемов, что важнее становятся другие проблемы: время резервного копирования и восстановления, изменения онлайнсхемы и т. д. Если вы считаете, что вам нужно сжатие для увеличения производительности, проконсультируйтесь с экспертом для проверки. Пока мы обсуждаем тему кодирования, есть важная рекомендация, которую я включу в этот раздел: храните и получайте доступ к датам и времени только в формате UTC. Преобразуйте даты и время в местное время (или в любой подходящий часовой пояс) только на дисплее (или при выводе на экран). Также имейте в виду, что тип данных MySQL TIMESTAMP действует лишь до 19 января 2038 года. Если вы получили эту книгу в качестве праздничного подарка в декабре 2037 года и в ваших базах данных есть столбцы TIMESTAMP, возможно, вам захочется вернуться к работе немного раньше. Дедупликация. Чтобы дедуплицировать значение, нормализуйте столбец в другую таблицу с отношением «один к одному». Этот метод полностью зависит от конкретного приложения, поэтому давайте рассмотрим конкретный пример. Представьте себе самый простой каталог книг, хранящихся в таблице всего с двумя столбцами: title (название) и genre (жанр). (Давайте сосредоточимся на данных и проигнорируем такие детали, как типы данных и индексы.) В примере 3.7 показана таблица с пятью книгами и тремя уникальными жанрами. Пример 3.7. Каталог книг с повторяющимися значениями в столбце genre title Efficient MySQL Performance TCP/IP Illustrated The C Programming Language Illuminations A Little History of the World genre computers computers computers poetry history
Данные | 125 В столбце genre есть повторяющиеся значения: три экземпляра значений computers. Чтобы дедуплицировать, нормализуйте столбец в другую таблицу с отношением «один к одному». Пример 3.8 показывает новую таблицу вверху и измененную исходную таблицу внизу. Две таблицы имеют взаимно однозначную связь в столбце genre_id. Пример 3.8. Нормализованный книжный каталог genre_id 1 2 3 genre computers history poetry title Efficient MySQL Performance TCP/IP Illustrated The C Programming Language Illuminations A Little History of the World genre_id 1 1 1 2 3 Исходная таблица (внизу) по-прежнему содержит повторяющиеся значения для столбца genre_id, но уменьшение объема данных в масштабе огромно. Например, для хранения строки «computers» требуется 9 байт, но только 2 байта для хранения целого числа 1 в виде типа данных SMALLINT UNSIGNED, что позволяет использовать 65 536 уникальных жанров (вероятно, этого хватит). Это сокращение объема данных на 77,7%: с 9 до 2 байт. Дедупликация значений таким образом достигается путем нормализации базы данных: разделения данных на таблицы на основе логических связей ("один к одному", "один ко многим" и т. д.). Однако дедупликация значений данных не является смыслом или целью нормализации базы данных. Тема нормализации базы данных выходит за рамки этой книги, поэтому я не буду объяснять это дальше. Существует много книг на эту тему, так что нет никаких проблем узнать оттуда больше. Из этого примера видно, что нормализация базы данных вызывает дедупликацию значений, но это не совсем так. Единичная таблица в примере 3.7 является технически допустимой первой, второй и третьей нормальными формами (при условии наличия первичного ключа) — полностью нормализованной, просто плохо спроектированной. Точнее было бы сказать, что дедупликация значений — это распространенный (и желательный) побочный эффект нормализации. И поскольку вам в любом случае следует нормализовать свои базы данных, вы, скорее всего, избежите дублирования значений. Есть интересная обратная сторона: денормализация. Денормализация — это противоположность нормализации: объединение связанных данных в одну таблицу. Еди-
126 | Глава 3 ничная таблица в примере 3.7 могла бы быть денормализованной таблицей, если бы это было целью ее разработки. Денормализация — это метод повышения производительности за счет устранения соединений таблиц и сопутствующих сложностей. Но не спешите денормализовывать свои схемы, потому что есть детали и компромиссы, которые необходимо учитывать, однако в этой книге они не рассматриваются. Фактически денормализация противоположна меньшему количеству данных, поскольку намеренно дублирует данные, чтобы обменять пространство на скорость. Удачный выбор и одна из лучших практик — это нормализация базы данных и меньшее количество данных. И то и другое позволяет добиться невероятной масштабируемости и производительности. Используется каждый вторичный индекс, а не дубликат Предпоследнее в контрольном списке эффективного хранения данных (табл. 3.4): использование каждого вторичного индекса, а не дубликата. Избегать неиспользуемых и повторяющиеся индексов — это всегда хорошо, но особенно важно для объема данных, поскольку индексы — это копии данных. Конечно, вторичные индексы намного меньше, чем полная таблица (первичный ключ), потому что содержат только значения столбцов индекса и соответствующие значения столбцов первичного ключа, но их число увеличивается по мере роста таблицы. Удаление неиспользуемых и повторяющихся вторичных индексов — простой способ уменьшить объем данных, но будьте осторожны. Как упоминалось в разделе «Избыточные, повторяющиеся и неиспользуемые» (см. гл. 2), поиск неиспользуемых индексов — сложная задача, поскольку индекс может использоваться не часто, поэтому обязательно проверяйте использование индекса в течение достаточно длительного периода. Повторяющиеся индексы, напротив, легче найти: при помощи pt-duplicate-key-checker (https://oreil.ly/qSStI). Еще раз: будьте осторожны при отбрасывании индексов. Удалив индекс, можно освободить только размер данных, равный размеру индекса. Существуют три способа просмотра размеров индексов. Давайте воспользуемся образцом базы данных (https://oreil.ly/lwWxR) employees («сотрудники», https://oreil.ly/lwWxR), поскольку там содержится несколько мегабайт индексных данных. Первым и предпочтительным способом просмотра размеров индексов является запрос таблицы INFORMATION_ SCHEMA.TABLES, как показано в примере 3.9. Пример 3.9. Размеры индексов образца базы данных employees (INFORMATION_SCHEMA) SELECT TABLE_NAME, DATA_LENGTH, INDEX_LENGTH FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'employees';
Данные TABLE_NAME departments dept_emp dept_manager employees salaries titles DATA_LENGTH 16384 12075008 16384 15220736 100270080 20512768 | 127 INDEX_LENGTH 16384 5783552 16384 0 0 0 TABLE_NAME — это имя таблицы в образце базы данных employees — всего шесть таблиц. (В базе данных есть несколько представлений, которые отфильтровываются по условию TABLE_TYPE = 'BASE TABLEBLE'.) DATA_LENGTH — это размер первичного ключа (в байтах). INDEX_LENGTH — это размер всех вторичных индексов (в байтах). Последние четыре таблицы не имеют вторичных индексов, только первичный ключ. Вторым и старым (но все еще широко применяемым) методом является SHOW TABLES Вы можете добавить оператор LIKE, чтобы отобразить только одну таблицу, как показано в примере 3.10. STATUS. Пример 3.10. Размеры индексов таблицы employees.dept_emp (SHOW TABLES STATUS) SHOW TABLE STATUS LIKE 'dept_emp'\G *************************** 1. row *************************** Name: dept_emp Engine: InnoDB Version: 10 Row_format: Dynamic Rows: 331143 Avg_row_length: 36 Data_length: 12075008 Max_data_length: 0 Index_length: 5783552 Data_free: 4194304 Auto_increment: NULL Create_time: 2021-03-28 11:15:15 Update_time: 2021-03-28 11:15:24 Check_time: NULL Collation: utf8mb4_0900_ai_ci Checksum: NULL Create_options: Comment: Поля Data_length и Index_length в выходных данных SHOW TABLE STATUS представляют собой те же столбцы и значения из INFORMATION_SCHEMA.TABLES. Лучше запросить INFORMATION_SCHEMA.TABLES, потому что вы можете использовать функции в SELECT, такие как ROUND(DATA_LENGTH / 1024 / 1024) для преобразования и округления значений из байтов в другие единицы измерения.
128 | Глава 3 Третий метод — и в настоящее время единственный способ просмотра размера каждого индекса — запрос таблицы mysql.innodb_index_stats, как показано в примере 3.11 для таблицы employees.dept_emp. Пример 3.11. Размер каждого индекса в таблице employees.dept_emp (mysql.innodb_index_stats) SELECT index_name, SUM(stat_value) * @@innodb_page_size size FROM mysql.innodb_index_stats WHERE stat_name = 'size' AND database_name = 'employees' AND table_name = 'dept_emp' GROUP BY index_name; index_name PRIMARY dept_no size 12075008 5783552 Таблица employees.dept_emp имеет два индекса: первичный ключ и вторичный индекс с именем dept_no. Столбец size содержит размер каждого индекса в байтах, который фактически представляет собой количество страниц индекса, умноженное на объем страницы InnoDB (по умолчанию 16 Кбайт). Образец базы данных employees — не очень яркий пример размера вторичного индекса, но базы данных реального мира могут быть переполнены вторичными индексами, на долю которых приходится значительный объем общего объема данных. Регулярно проверяйте использование индексов и размеры индексов, а также уменьшайте общий объем данных, тщательно удаляя неиспользуемые и повторяющиеся индексы. Хранятся только необходимые строки Последний пункт контрольного списка эффективного хранения данных (табл. 3.4): держать только необходимые строки. Этот пункт завершает полный круг, образуя цикл с первым пунктом: «Сохраняются только необходимые строки» (см. выше). Строка может потребоваться при сохранении, но со временем все меняется. Удалите (или заархивируйте) строки, которые больше не нужны. Это звучит очевидно, но часто можно найти таблицы с забытыми или заброшенными данными. Я видел бесчисленное множество раз, как удалялись целые таблицы, которые были забыты. Удаление (или архивирование) данных — нечто, что намного проще сказать, чем сделать, и в следующем разделе мы рассмотрим эту проблему.
Данные | 129 Удаление или архивирование данных Я надеюсь, что эта глава вселит в вас желание удалить или заархивировать данные. Избыток данных прервал слишком много моих приятных снов: как будто у MySQL есть собственный разум и она специально ждет до 3 часов утра, чтобы заполнить диск. Однажды у меня было приложение, которое присылало мне вызов посреди ночи в трех разных часовых поясах (у меня были встречи в разных частях мира). Но хватит обо мне; давайте поговорим о том, как удалить или заархивировать данные, не оказывая негативного влияния на приложение. Для краткости я имею в виду только удаление данных, а не удаление или архивирование данных, потому что сложности почти полностью заключаются в первом: удалении данных. Для архивирования данных требуется сначала скопировать данные, а затем удалить их. При копировании данных следует использовать неблокирующие инструкции SELECT, чтобы избежать воздействия на приложение, затем записать скопированные строки в другую таблицу или хранилище данных, к которым приложение не обращается. Но даже с неблокирующими SELECT надо ограничить скорость процесса копирования, чтобы избежать увеличения QPS сверх того, что могут обрабатывать MySQL и приложение. (Напомню из раздела «Чем меньше QPS, тем лучше» (см. выше), что QPS зависит от приложения и его трудно увеличить.) Инструменты Вам придется написать свои собственные инструменты для удаления или архивирования данных. Извините, что сообщаю плохие новости, но это правда. Хорошей новостью является то, что удаление и архивирование данных несложно — это, вероятно, банально по сравнению с вашим приложением. Критически важно здесь — регулирование цикла, который выполняет инструкции SQL. Никогда не делайте так: for { rowsDeleted = execute(“ DELETE FROM table LIMIT 1000000” ) if rowsDeleted == 0 { break } } LIMIT 1000000 — это, скорее всего, слишком много, и цикл for не имеет задержки между инструкциями. Такой псевдокод, вероятно, приведет к сбою приложения. Размер пакета (англ. batch size) — ключ к безопасному и эффективному инструменту архивирования данных. Размер пакета Во-первых, кратчайший и безопасный путь, который может позволить вам пропустить чтение этого раздела до тех пор, пока это не потребуется: вручную удалить 1000 строк или меньше одной инструкцией DELETE, если строки небольшие (нет
130 | Глава 3 столбцов BLOB, TEXT или JSON) и MySQL не сильно нагружена. «Вручную» означает, что вы запускаете каждую инструкцию DELETE последовательно (одну за другой), а не параллельно. Не пишите программу для выполнения инструкций DELETE. Большинство людей работает слишком медленно, чтобы MySQLчто-либо заметила, поэтому независимо от того, насколько вы быстры, вы не можете слишком нагрузить MySQL, выполнив вручную инструкции DELETE...LIMIT 1000. Используйте этот путь разумно и попросите другого инженера просмотреть все ручные удаления. Метод, описанный в этом разделе, фокусируется на DELETE, но в целом также применяется к INSERT и UPDATE. Для INSERT размер пакета определяется количеством вставленных строк, а не параметрами LIMIT. Скорость, с которой вы можете быстро и безопасно удалять строки, определяется размером пакета, который MySQL и приложение могут поддерживать, не влияя на время отклика на запрос или запаздывание репликации. (Запаздывание репликации рассматривается в главе 7). Размер пакета — это количество строк, удаленных на каждую инструкцию DELETE, которое контролируется с помощью LIMIT и при необходимости регулируется простой задержкой. Размер пакета калибруется в соответствии со временем выполнения; 500 миллисекунд — хорошая отправная точка. Это означает, что выполнение каждой инструкции DELETE должно занимать не более 500 мс. Это крайне важно по двум причинам: Запаздывание репликации Время выполнения на исходном экземпляре MySQL создает запаздывание репликации на репликах экземпляров MySQL. Если для выполнения инструкции DELETE в источнике требуется 500 мс, то для выполнения на реплике также требуется 500 мс, что приводит к запаздыванию репликации в 500 мс. Вы не можете избежать запаздывания репликации, но должны свести его к минимуму, поскольку запаздывание репликации приводит к потере данных. (Сейчас я опускаю многие детали по репликации, которые разъясняю в главе 7.) Регулирование В некоторых случаях безопасно выполнять инструкции DELETE без задержки (паузы между ними)), т. е. не регулируя, потому что откалиброванный размер пакета ограничивает время выполнения запроса, что ограничивает QPS. Запрос, выполнение которого занимает 500 мс, может выполняться последовательно только со скоростью 2 QPS. Но это не обычные запросы: они специально созданы для доступа и записи (удаления) как можно большего количества строк. Без регулирования массовая запись может нарушить работу других запросов и повлиять на работу приложения. Регулирование имеет первостепенное значение при удалении данных: всегда начинайте с задержки между инструкциями DELETE и контролируйте запаздывание репликации1. 1 Ознакомьтесь с freno (https://oreil.ly/vSmUb) от GitHub Engineering: приложение-регулятор с открытым исходным кодом для MySQL).
Данные | 131 Всегда включайте регулирование (англ. throttling) при массовых операциях. Чтобы откалибровать размер пакета до времени выполнения 500 мс (или любого другого времени, которое вы выбрали), начните с размера пакета 1000 (LIMIT 1000) и задержки в 200 мс между инструкциями DELETE: 200 мс — это большая задержка, но вы уменьшите ее после калибровки размера пакета. Запустите это хотя бы на 10 минут, отслеживая запаздывание репликации и стабильность MySQL — не позволяйте MySQL тормозить или дестабилизироваться. (Запаздывание репликации и стабильность MySQL рассматриваются в главах 7 и 6 соответственно.) Используйте отчеты по запросам (см. гл. 1, раздел «Отчетность»), чтобы проверить максимальное время выполнения инструкции DELETE или измерить его непосредственно вашим инструментом архивирования данных. Если максимальное время выполнения значительно меньше целевого значения в 500 мс, то удвойте размер пакета и повторите запуск еще на 10 минут. Продолжайте удваивать размер пакета или вносить меньшие корректировки до тех пор, пока максимальное время выполнения не достигнет целевого значения — предпочтительно чуть меньше целевого. Когда вы закончите, запишите откалиброванный размер пакета и время выполнения, поскольку удаление старых данных должно происходить регулярно. Чтобы установить регулировку с использованием откалиброванного размера пакета, повторите процесс, медленно уменьшая задержку при каждом 10-минутном повторном запуске. В зависимости от MySQL и приложения вы можете достичь нуля (без регулирования). Остановитесь при первых признаках запаздывания репликации или дестабилизации MySQL, затем увеличьте задержку до предыдущего значения, которое не вызвало ни одной из проблем. Когда вы закончите, запишите задержку по той же причине, что и раньше: удаление старых данных должно происходить регулярно. Откалибровав размер пакета и установив регулировку, вы, наконец, можете рассчитать скорость — сколько строк в секунду вы можете удалить, не влияя на время отклика на запрос: batch size * DELETE QPS. (Используйте отчет о запросах для проверки QPS инструкции DELETE или измерьте его непосредственно вашим инструментом архивирования данных). Ожидайте, что курс будет меняться в течение дня. Если приложение очень нагружено в рабочее время, единственная устойчивая скорость может быть равна нулю. Если вы амбициозный и целеустремленный человек, стремящийся к вершинам своей карьеры, своей отрасли и всего мира, тогда просыпайтесь ночью и пробуйте добиться более высокой скорости, когда база данных наименее нагружена: больший размер пакета, меньшая задержка или и то и другое. Просто не забудьте сбросить размер пакета и задержки до восхода солнца и роста нагрузки на базу данных. Но резервное копирование MySQL почти всегда выполняется посреди ночи. Даже если приложение работает без высокой нагрузки глубокой ночью, база данных может быть нагружена.
132 | Глава 3 Конфликт блокировки строк Для рабочих нагрузок с высокой интенсивностью записи массовые операции могут вызвать конфликт с повышенным уровнем блокировки строк: разные запросы, ждущие получения блокировки строк для одних и тех же (или соседних) строк. Эта проблема в основном затрагивает инструкции INSERT и UPDATE, но также касается инструкций DELETE, если удаленные строки чередуются с сохраненными строками. Дело в том, что размер пакета слишком велик, даже если время выполнения откалибровано. Например, MySQL может быть в состоянии удалить 100 000 строк за 500 мс, но если блокировки для этих удаляемых строк пересекаются со строками, которые обновляет приложение, то возникает конфликт блокировки строк. Решение в том, чтобы уменьшить размер пакета путем калибровки под гораздо меньшее время выполнения — например, 100 мс. В крайних случаях вам также может потребоваться увеличить задержку: небольшой размер пакета, большая задержка. Это уменьшит конфликт блокировки строк, что хорошо для приложения, однако такая мера плоха для архивирования данных, поскольку замедляет этот процесс. Здесь нет волшебного решения; лучше избегать подобной ситуации путем меньшего количества данных и меньшего значения QPS. Пространство и время Удаление данных не освобождает дисковое пространство. Удаление строк носит логический, а не физический характер, что является обычной оптимизацией производительности во многих базах данных. Когда вы удаляете 500 Гбайт данных, вы не получаете 500 Гбайт дискового пространства, вы получаете 500 Гбайт свободных страниц. Нюансы более сложны и выходят за рамки этой книги, но общая идея верна: удаление данных приводит к освобождению страниц, а не места на диске. Свободные страницы не влияют на производительность, и InnoDB повторно использует их при вставке новых строк. Если удаленные строки вскоре будут заменены новыми строками, а дисковое пространство не ограничено, то свободные страницы и невостребованное дисковое пространство не приносят беспокойства. Но, пожалуйста, помните о своих коллегах: если ваша компания использует собственное оборудование, а MySQL для вашего приложения делит дисковое пространство с MySQL для других приложений, то не тратьте впустую то пространство, которое может быть использовано другими приложениями. Облачное хранение данных стоит денег, поэтому также не тратьте деньги зря: освободите место на диске. Лучший способ освободить место на диске в InnoDB — это перестроить таблицу, выполнив no-op инструкцию ALTER TABLE...ENGINE=INNODB. Есть три отличных решения:  pt-online-schema-change (https://oreil.ly/8EJph);  gh-ost (https://oreil.ly/IsV83);  ALTER TABLE...ENGINE=INNODB (https://oreil.ly/JhWdg). Каждое решение работает по-своему, но у них есть одна общая черта: все они могут перестраивать огромные таблицы InnoDB онлайн — в рабочей среде и не влияя на
Данные | 133 приложение. Прочитайте их документацию, чтобы решить, какой вариант лучше всего вам подходит. Чтобы перестроить таблицу с помощью ALTER TABLE...ENGINE=INNODB, замените ... на имя таблицы. Не вносите никаких других изменений. Удаление больших объемов данных требует времени. Вы могли читать или слышать о том, как быстро MySQL может записывать данные, но, как правило, это в условиях тестов производительности (см. гл. 2, раздел «Тонкая настройка (тюнинг) MySQL»). Не сомневайтесь: в гламурном мире лабораторных исследований MySQL будет потреблять каждый такт и дисковый ввод данных, который вы можете ей предоставить. Но в повседневном мире, где мы с вами живем, данные должны удаляться со значительными ограничениями, чтобы избежать воздействия на приложение. Скажем прямо: это займет гораздо больше времени, чем вы думаете. Хорошая новость: если все сделано правильно, как описано в разделе «Размер пакета» (см. выше), то время на вашей стороне. Хорошо откалиброванная, устойчивая массовая операция может выполняться днями и неделями. Это касается и решения, которое вы используете для освобождения дискового пространства в InnoDB, поскольку перестройка таблицы — просто еще один тип массовой операции. Для удаления строк требуется время, а для освобождения дискового пространства — дополнительное время. Парадокс бинарного лога Удаление данных создает данные. Этот парадокс возникает из-за того, что изменения данных записываются в бинарные логи. Ведение бинарного лога можно отключить, но так никогда не делается в рабочей среде, потому что бинарные логи требуются для репликации, а ни одна нормальная производственная система не работает без реплик. Если таблица содержит большие столбцы BLOB, TEXT или JSON, то размер бинарного лога может значительно вырасти, поскольку системная переменная MySQL binlog_row_image (https://oreil.ly/0bNcG) по умолчанию имеет значение full. Эта переменная определяет, как образы строк записываются в бинарные логи; у нее есть три настройки: full Запиcь значения каждого столбца (полной строки). minimal Запись значения столбцов, которые изменились, и столбцов, необходимых для идентификации строки. noblob Запись значения каждого столбца, кроме столбцов BLOB и TEXT, которые не являются обязательными.
134 | Глава 3 Безопасно и рекомендуовано использовать minimal (или noblob), если нет внешних служб, которые полагаются на полные образы строк в бинарных логах — например, служба конвейера данных, которая передает изменения в озеро данных или хранилище больших данных. Если вы используете pt-online-schema-change (https://oreil.ly/2EB4l) или gh-ost (https://oreil.ly/nUuvv) для перестроения таблицы, эти инструменты копируют таблицу (безопасно и автоматически), и этот процесс копирования записывает еще больше изменений данных в бинарные логи. Однако ALTER TABLE...ENGINE=INNODB по умолчанию действует без копирования таблицы. При удалении большого количества данных использование диска увеличится изза ведения бинарного лога и того факта, что удаление данных не освобождает дисковое пространство. Как это ни парадоксально, вы должны убедиться, что на сервере достаточно свободного места на диске для удаления данных и перестроения таблицы. Итоги В этой главе рассматривались данные с точки зрения производительности и утверждалось, что сокращение доступа к данным и уменьшение их хранения — методы косвенной оптимизации запросов для повышения производительности. Ключевые моменты:  Меньшее количество данных обеспечивает более высокую производительность.  Чем меньше QPS, тем лучше, потому что это пассив, а не актив.  Индексы необходимы для максимальной производительности MySQL, но быва- ют случаи, когда они могут не помочь.  Принцип наименьшего объема данных означает: храните только необходимые данные и обращайтесь только к ним.  Убедитесь, что запросы обращаются к как можно меньшему количеству строк.  Не храните больше данных, чем необходимо: данные ценны для вас, но для MySQL это мертвый груз.  Удаление или архивирование данных имеет важное значение и повышает произ- водительность. Следующая глава посвящена паттернам доступа, которые определяют, как можно изменить приложение для эффективного использования MySQL. Практика: аудит доступа к данным запросов Целью этой практики является проверка запросов на предмет неэффективного доступа к данным. Это контрольный список эффективного доступа к данным (см. табл. 3.2):
Данные | 135  Пусть запрос возвращает только необходимые столбцы.  Уменьшите сложность запросов.  Ограничьте доступ к строкам.  Ограничьте результирующий набор.  Избегайте сортировки строк. Примените контрольный список к 10 самым медленным запросам. (Чтобы получить медленные запросы, обратитесь к разделам «Профиль запроса» и «Практика: определение медленных запросов» главы 1). Простое решение — это любой SELECT *: четко выберите только необходимые столбцы. Также обратите пристальное внимание на любой запрос с ORDER BY: используется ли индекс? Есть ли там LIMIT? Может ли вместо этого приложение сортировать строки? В отличие от практики с определением медленных запросов (см. гл. 1) и практики с поиском повторяющихся индексов (см. гл. 2), здесь нет инструмента для аудита доступа запросов к данным. Но контрольный список состоит всего из пяти пунктов, поэтому проверка вручную не займет много времени. Тщательный и методичный аудит запросов для оптимального доступа к данным — практика производительности MySQL на экспертном уровне.
136 | Глава 3
ГЛАВА 4 Паттерны доступа Паттерны доступа (англ. access patterns) описывают, как приложение использует MySQL для доступа к данным. Изменение паттернов доступа оказывает мощное влияние на производительность MySQL, но обычно это требует большего уровня усилий, чем другие оптимизации. Вот почему это последний этап пути, описанный в разделе «Улучшение времени отклика на запрос» (см. гл. 1): сначала оптимизируйте запросы, индексы и данные, а затем уже оптимизируйте паттерны доступа. Прежде чем мы начнем, давайте еще раз подумаем о камнях из главы 3. Предположим, у вас есть грузовик — ваша MySQL. При эффективном использовании грузовик позволяет легко перемещать любую груду камней в гору. Но при неэффективном использовании он не приносит большой пользы, и это может даже привести к тому, что работа займет больше времени, чем необходимо. Например, вы могли бы использовать грузовик, чтобы перетаскивать булыжники один за другим вверх по склону. Это легко дл вас (и грузовика), но ужасно неэффективно и отнимает много времени. Грузовик полезен ровно настолько, насколько полезен человек, который им управляет. Аналогично MySQL эффективен настолько, насколько эффективно приложение, которое его использует. Иногда инженер ломает голову над тем, почему MySQL не работает быстрее. Например, когда MySQL выполняет 5000 QPS, а инженер пытается понять, почему не 9000 QPS. Или MySQL использует 50% CPU, а инженер задается вопросом, почему не 90% CPU. Инженер вряд ли найдет ответ, потому что он сосредоточен на эффекте (MySQL), а не на причине: приложении. Такие метрики, как QPS и загрузка процессора, очень мало — можно сказать, что почти ничего, — не говорят о MySQL; они отражают лишь то, как приложение использует MySQL. MySQL работает так же быстро и эффективно, как и приложение, которое его использует. Приложение может превысить возможности одного экземпляра MySQL, но опять же: это больше говорит о приложении, чем о MySQL, потому что существует бесчисленное множество объемных высокопроизводительных приложений, использующих один экземпляр MySQL. Без сомнения, MySQL достаточно быстра для этого приложения. Реальный вопрос заключается в следующем: эффективно ли приложение использует MySQL? После многих лет работы с MySQL, сотен различных приложений и тысяч различных экземпляров MySQL я уверяю вас: производительность MySQL ограничена приложением, а не наоборот.
138 | Глава 4 В этой главе основное внимание уделяется паттернам доступа к данным, которые определяют, как можно изменить приложение для эффективного использования MySQL. Здесь шесть основных разделов. В первом разъясняется, что делает MySQL отдельно от приложения и почему это важно. Второй доказывает, что производительность базы данных не масштабируется линейно; вместо этого существует предел, превышение которого дестабилизирует производительность. В третьем поразмышляем о том, почему Ferrari быстрее Toyota, хотя обе марки автомобилей работают примерно одинаково. Ответ объяснит, почему некоторые приложения превосходны с MySQL, в то время как другие не могут выйти из первой передачи. В четвертом перечислены паттерны доступа к данным. В пятом разделе представлено несколько изменений в приложениях, направленных на улучшение или изменение моделей доступа к данным. Шестой возвращается к старому другу: более быстрому и мощному оборудованию. MySQL ничего не делает Когда приложение простаивает, MySQL простаивает. Когда приложение занято выполнением запросов, MySQL занята выполнением этих запросов. У MySQL есть несколько фоновых задач (например, «Сброс страниц», см. гл. 6), но они заняты только чтением и записью данных для этих запросов. Фактически фоновые задачи повышают производительность, позволяя приоритетным задачам — выполнению запросов — избегать медленных операций или откладывать их. Поэтому, если MySQL работает медленно и нет никаких внешних проблем, причиной может быть лишь то, что движет MySQL: приложение. QPS напрямую относится к приложению и только к нему. Без приложения QPS равняется нулю. В некоторых хранилищах данных есть «духи в машине», компьютерные призраки: внутренние процессы, которые могут выполняться когда угодно и снижать производительность, если выполняются в неподходящий момент — когда хранилище данных занято выполнением запросов. (Пара примеров: сжатие (сompaction) и вакуумирование (vacuuming) — у MySQL нет ни того ни другого.) У MySQL нет компьютерных призраков, если только приложение не выполняет запросы, о которых вы не знаете. Понимание этого поможет вам избежать поиска несуществующих причин и, что более важно, сосредоточиться на том, чем занята MySQL: выполнении запросов. В главе 1 вы прочитали, как это увидеть (см. раздел «Профиль запроса»). Профиль запроса показывает больше, чем просто медленные запросы: он показывает, чем занята MySQL. Запросы могут влиять на другие запросы. Общий термин для этого — конфликт запросов (англ. query contention): когда запросы конкурируют и ожидают общих ресурсов. Существуют определенные типы конфликтов: конфликт блокировки строк, процессорный конфликт (англ. CPU contention) и т. д. Конфликт запросов
Паттерны доступа | 139 может создать впечатление, что MySQL занята другими делами, но не вводите себя в заблуждение: MySQL бывает занята только выполнением запросов приложений. Почти невозможно увидеть или доказать конфликт запросов, потому что MySQL сообщает лишь об одном типе конфликта: конфликт блокировки строк. (Даже конфликт блокировки строк трудно увидеть ясно, потому что блокировка строк сложна.) Более того, конфликты скоротечны и почти незаметны, потому что эти проблемы присущи высокому QPS (где высокий показатель относится к приложению). Конфликт запросов подобен дорожной пробке: для этого требуется много машин на дороге. Хотя его почти невозможно увидеть или доказать, вы должны знать об этом, потому что это может объяснить необъяснимо медленные запросы. Конфликт запросов играет важную роль, когда производительность доведена до предела. Производительность дестабилизируется на пределе В конце раздела «MySQL: быстрее» (см. гл. 1) я сказал, что MySQL может легко довести большинство современных аппаратных средств до предела. Это правда, но предел может вас удивить. Рис. 4.1 иллюстрирует, чего ожидают инженеры: по мере увеличения нагрузки производительность базы данных увеличивается до тех пор, пока не будет использовать 100% емкости системы — пропускную способность оборудования и операционной системы, тогда производительность станет стабильной. Это называется линейным масштабированием (или линейной масштабируемостью), и это миф. Рис. 4.1. Ожидаемая производительность базы данных (линейная масштабируемость) Линейное масштабирование — мечта каждого администратора базы данных и инженера, но такого не бывает. Вместо этого рис. 4.2 иллюстрирует реальную производительность базы данных в зависимости от нагрузки и емкости системы. Производительность базы данных увеличивается при загрузке лишь до предела, который составляет менее 100% от емкости системы. Реально предел производительности базы данных составляет от 80 до 95% от емкости системы. Когда нагрузка превышает предельный уровень, производительность базы данных дестабилизи-
140 | Глава 4 руется: пропускная способность, время отклика и другие метрики заметно отклоняются, иногда довольно сильно, от своего нормального значения. В лучшем случае результатом станет снижение производительности для некоторых (или большинства) запросов; в худшем случае это приводит к сбою. Рис. 4.2. Реальная производительность базы данных Уравнение 4.1 показывает универсальный закон масштабируемости (УЗМ, англ. Universal Scalability Law, USL), сформулированный Нилом Гюнтером: уравнение, которое моделирует масштабируемость аппаратных и программных систем. Уравнение 4.1. Универсальный закон масштабируемости: X (N ) = γN . 1 + α ( N − 1) + βN ( N − 1) В табл. 4.1 показано, что представляет собой каждый член уравнения универсального закона масштабируемости. Таблица 4.1. Члены уравнения — универсального закона масштабируемости Член Представление X Пропускная способность N Нагрузка: одновременные запросы, запущенные процессы, ядра ЦП, узлы в распределенной системе и т. д. γ Конкурентность (идеальный параллелизм) α Конфликт: ожидание общих ресурсов β Согласованность: координация общих ресурсов Глубокое погружение в универсальный закон масштабируемости выходит за рамки этой книги, поэтому я ограничиваю объяснение текущей темой: пределом производительности базы данных. Чтобы узнать больше, прочтите «Guerrilla Capacity Planning» («Планирование партизанского потенциала», https://oreil.ly/WZEd8) Нила Гюнтера. Пропускная способность зависит от нагрузки: X(N). Конкурентность (γ) помогает увеличить пропускную способность по мере увеличения нагрузки (N). Но
Паттерны доступа | 141 конфликт (α) и согласованность (β) снижают пропускную способность по мере увеличения нагрузки. Это исключает линейную масштабируемость и ограничивает производительность базы данных. Что еще хуже, чем ограничение производительности, — согласованность приводит к ретроградной производительности: снижению производительности при высокой нагрузке. Термин «ретроградный» — преуменьшение. Будто бы MySQL просто возвращается к меньшей пропускной способности, когда не может справиться с нагрузкой. На деле все гораздо хуже. Я предпочитаю термины «нестабильность» и «дестабилизация», потому что они отражают реальность: система разрушается, а не просто работает медленнее. Универсальный закон масштабируемости удивительно хорошо моделирует реальную производительность MySQL1. Но как модель УЗМ только описывает и прогнозирует масштабируемость рабочей нагрузки; в нем ничего не говорится о том, как или почему рабочая нагрузка масштабируется (или не масштабируется). УЗМ в основном используется экспертами, которые измеряют и подгоняют данные к модели для определения параметров (γ, α и β), а затем героически трудятся, чтобы уменьшить эти параметры. Остальные просто смотрят графики (глава 6 посвящена метрикам MySQL) и ждут, пока производительность MySQL не дестабилизируется — это предел. На рис. 4.3 показаны три диаграммы реального сбоя, когда приложение превысило пределы MySQL. Отключение состояло из трех периодов: Рост (с 6 утра до 9 утра) В начале роста приложение было стабильным, но его разработчики начали беспокоиться, потому что показания метрики росли — медленно, но неуклонно. В прошлом у приложения были сбои, которые начинались с неуклонно растущих показаний. В ответ разработчики приложения увеличили пропускную способность транзакций, чтобы справиться с растущим спросом. (Приложение было способно регулировать пропускную способность транзакций; это не особенность MySQL.) Рост и ответ повторялись до тех пор, пока работа не была прекращена: MySQL достигла предела. Ограничение (с 9 утра до полудня) Приложение было полностью нестабильным и фактически функционировало в автономном режиме (offline) во время работы на пределе. Хотя загрузка процессора и QPS были высокими и стабильными, запущенные потоки рассказывали совсем другую историю. Перепады в схеме выполнения потоков, показанные на рис. 4.3, — явный признак дестабилизации MySQL. Поскольку для выполнения одного запроса требуется один поток, большие колебания в выполняемых потоках указывают, что запросы не проходили через систему гладко. Вместо этого запросы били по MySQL хаотичными неравномерными ударами. 1 Посмотрите видео «Universal Scalability Law Modeling Workbook» («Методичка по моделированию УЗМ», https://oreil.ly/hzXnb) известного эксперта по MySQL Барона Шварца, чтобы увидеть УЗМ в действии со значениями с реальных серверов MySQL.
142 | Глава 4 Рис. 4.3. Производительность базы данных по достижении предела Высокая и стабильная загрузка процессора и QPS вводили в заблуждение: стабильная хороша только с небольшими изменениями, как видно на графике до и после работы на пределе. Стабильная без каких-либо изменений, как во время работы на пределе, — это плоская линия. Чтобы вы лучше поняли, приведу странную, но наглядную аналогию. Представьте себе оркестр. Когда оркестр играет правильно, во всех аспектах музыки имеются вариации. На самом деле эти вариации и есть музыка: ритм, темп, высота тона, тон, мелодия, динамика и т. д. Плоская линейная метрика аналогична невменяемому кларнетисту, играющему одну непрерывную ноту fortissimo: стабильно, но музыкой не является.
Паттерны доступа | 143 Во время работы приложения на пределе его разработчики продолжали пытаться увеличить пропускную способность транзакций, но это не сработало. MySQL в такой ситуации не будет использовать последние 5% процессора, QPS не увеличится, а запущенные потоки не стабилизируются. Из УЗМ (уравнение 4.1) вы знаете почему: конфликт и согласованность. По мере увеличения нагрузки (N) увеличивалась пропускная способность транзакций (X), но также увеличивались ограничивающие эффекты конфликта (α) и согласованности (β), пока MySQL не достигла предела. Исправление (с полудня до 3 часов дня) Поскольку увеличение пропускной способности транзакций само себе погибель, решение заключалось в снижении пропускной способности транзакций. Это кажется нелогичным, но математика не лжет. В полдень разработчики приложений снизили пропускную способность транзакций, и результаты видны на графиках: загрузка процессора упала до 50%, значение QPS вернулось к постоянному изменению (и даже немного увеличилось), а запущенные потоки также вернулись к постоянному изменению (с несколькими всплесками, на поглощение которых у MySQL были свободные мощности). Чтобы представить, как это работает, рассмотрим другую аналогию. Представьте себе шоссе. Когда на дороге много машин, все они замедляются (надеюсь), потому что людям нужно время, чтобы подумать и отреагировать на другие машины, особенно на шоссе. Когда на дороге слишком много машин, они создают пробку. Единственным решением (помимо добавления дополнительных полос движения) является сокращение количества автомобилей на шоссе: меньшее количество автомобилей может двигаться быстрее. Сокращение пропускной способности транзакций аналогично сокращению количества автомобилей на шоссе, что позволяет оставшимся автомобилям двигаться быстрее, а транспортному потоку — плавнее. Этот пример хорошо показывает предел производительности базы данных в соответствии с универсальным законом масштабируемости (уравнения 4.1), но это также исключительный случай, потому что приложение смогло довести MySQL и аппаратное обеспечение до предела. Как правило, высокая нагрузка дестабилизирует приложение, что не позволяет ему увеличивать нагрузку на MySQL. Другими словами, приложение вылетает до того, как сможет довести MySQL до предела. Но в этом примере приложение не вылетело, а продолжало масштабироваться, пока это позволяли возможности MySQL. Еще два нюанса о пределе производительности MySQL, прежде чем мы обратим наше внимание на приложение: 1. Этого предела трудно достичь разве что все плохо с аппаратным обеспечением. Как упоминалось в разделе «Более быстрое и мощное оборудование!» (см. гл. 2), это одно из двух исключений, при которых вам следует масштабироваться до разумной аппаратной мощности. Кроме того, приложению сложно полностью и одновременно использовать все аппаратное обеспечение — процессор, память и хранилище. Приложение, скорее всего, столкнется с узким местом в одной части оборудования задолго до того, как сможет это сделать. Когда это происходит,
144 | Глава 4 приложение достигает предела не производительности базы данных, а только этой одной части оборудования. 2. Когда высокая нагрузка приводит к медленному реагированию MySQL, это не означает, что предел достигнут. Причина проста: γ (гамма). γ представляет конкурентность или идеальный параллелизм. Напомним из уравнения универсального закона масштабируемости (уравнение 4.1), что γ находится в числителе2. Низкая производительность базы данных не означает, что предел достигнут, поскольку увеличение конкурентности (γ) увеличивает предел. Уменьшение конфликта (α) также повышает лимит. (Согласованность, β, находится вне нашего контроля: она присуща MySQL и операционной системе, но обычно это не проблема.) Второй момент приводит к вопросу: как можно увеличить конкурентность, или уменьшить конфликт, или и то и другое? Это кажется критически важным, но все не так: это лишь заблуждение, потому что краеугольный камень производительности MySQL — время ответа на запрос. Значения конкурентности (γ) и конфликта (α) не поддаются непосредственному измерению. Они определяются путем подгонки измерений пропускной способности и нагрузки к модели. Эксперты используют универсальный закон масштабируемости для понимания емкости системы, а не для повышения производительности. И в этом разделе он приводится, чтобы доказать: производительность дестабилизируется на пределе работы. Toyota и Ferrari Некоторые приложения достигают невероятной производительности MySQL, в то время как другие борются с низкой пропускной способностью. Некоторые приложения могут полностью использовать аппаратное обеспечение — до предела, в то время как другие едва прогревают процессоры. Некоторые приложения не имеют никаких проблем с производительностью, в то время как другие постоянно сталкиваются с медленными запросами. Это широкое обобщение, но я собираюсь заявить: каждый инженер хочет, чтобы его приложение было на левой стороне этого ринга: невероятная производительность, полное использование оборудования и никаких проблем. Разница между приложениями слева от «в то время как» и приложениями справа понятна, если задуматься о том, почему Ferrari быстрее Toyota. Обе марки автомобилей используют примерно одинаковые детали и дизайн, но максимальная скорость Toyota обычно составляет 130 миль в час, в то время как максимальная скорость Ferrari составляет 200 миль в час3. У Ferrari нет специальных деталей, которые делают его на 70 миль в час быстрее, чем у Toyota. Так почему же Ferrari намного быстрее Toyota? Ответ заключается в различии в инженерном проектировании и деталях. 2 На самом деле известный эксперт по MySQL Барон Шварц поместил это туда. Нил Гюнтер написал в посте «USL Scalability Modeling with Three Parameters» («Моделирование масштабируемости УЗМ с тремя параметрами», https://oreil.ly/s2BL8), что Барон добавил третий параметр, потому что это позволяло УЗМ подгонять данные из реальных баз данных. 3 Toyota: 210 км/ч; Ferrari: 320 км/ч.
Паттерны доступа | 145 Toyota не рассчитана на высокую скорость. Достижение высокой скорости (как и высокой производительности) требует пристального внимания ко многим деталям. Для автомобиля это следующие параметры:  размер двигателя, конфигурация и скорость процессов;  передаточные числа трансмиссии, точки переключения передач и скорость про- цессов;  размер шины, тяговое усилие и сила вращения;  рулевое управление, подвеска и торможение;  аэродинамика. Обе марки автомобилей разрабатывают и проектируют эти параметры, но высокий уровень качества компонентов Ferrari объясняет, почему этот бренд достигает большей производительности. Например, аэродинамика: уникальный внешний дизайн Ferrari — яркий, но в то же время функциональный. Он снижает коэффициент лобового сопротивления, что повышает эффективность. Высокая производительность, как и высокая скорость, не может быть достигнута случайно или грубой силой. Это результат тщательного проектирования с целью обеспечения высокой производительности. Ferrari быстрее Toyota, потому что эта машина спроектирована и продумана во всех деталях, чтобы быть быстрее. Разработано ли ваше приложение и спроектировано ли оно во всех деталях для максимальной производительности MySQL? Если да, то, полагаю, вы можете пропустить остальную часть этой главы. Если нет (обычное дело), то в следующем разделе рассматриваются фундаментальные технические различия между приложениями, подобными Toyota, и приложениями, подобными Ferrari: паттерны доступа к данным. Паттерны доступа к данным Паттерны доступа к данным (англ. data access patterns) описывают, как приложение использует MySQL для доступа к данным. Термин паттерны доступа к данным (или сокращенно паттерны доступа, data access) часто используется, но редко объясняется. Давайте изменим это, уточнив три вещи: 1. Люди так часто обсуждают паттерны доступа во множественном числе, что те смешиваются в одну кучу. Но важно понимать, что приложение имеет множество разных паттернов доступа. О них говорят как о едином целом лишь для удобства. На практике вы изменяете паттерны доступа индивидуально. 2. Паттерн доступа в конечном счете относится к запросу, и вы изменяете запросы (и приложение), чтобы изменить паттерны доступа, но дело не в запросах. В терминах языка программирования Go (https://golang.org) паттерн доступа — это интерфейс, а запрос — это реализация. Сосредоточьтесь на интерфейсе, а не на реализации. Это позволяет предусмотреть (и по возможности применить)
146 | Глава 4 паттерны доступа к различным хранилищам данных. Например, определенные паттерны доступа, выполняемые в MySQL, лучше подходят для хранилища данных типа ключ-значение, но это трудно увидеть, сосредоточившись на SQLзапросах, которые не имеют ничего общего с запросами типа ключ-значение. В этой книге я освещаю изменение паттернов доступа, но в действительности изменяются запросы (и приложение). 3. Паттерн доступа содержит имя и список технических характеристик. Имя используется для идентификации и передачи паттерна доступа другим инженерам. (Паттерны доступа не имеют внутренних имен). Выберите имя, которое было бы кратким и содержательным. Список технических характеристик определяется хранилищем данных и варьируется в зависимости от него. Например, доступ к данным у MySQL сильно отличается от доступа к данным у Redis. В этом разделе перечисляются и объясняются девять характеристик доступа к данным MySQL. Теоретически разработчики приложений должны идентифицировать каждый отдельный паттерн доступа, но давайте будем честны: это очень утомительно. (Я никогда не видел, чтобы это делалось, и это может быть даже невозможно, если приложение быстро меняется.) Тем не менее к этому надо стремиться. Вот три разумных и выполнимых подхода: 1. Проведите мозговой штурм со своей командой, чтобы определить наиболее очевидные и распространенные паттерны доступа. 2. Используйте профиль запроса (см. гл. 1, раздел «Профиль запроса»), чтобы определить самые частые и медленные паттерны доступа. 3. Ознакомьтесь с кодом на предмет менее используемых (или забытых) паттернов доступа. Вам нужно по крайней мере один раз воспользоваться первым или вторым подходом, чтобы достичь цели, обозначенной в этой главе: косвенной оптимизации запросов путем изменения паттернов доступа. После того как вы определили (и назвали) паттерн доступа, определите ответ по каждой из девяти характеристик, приведенных ниже. Если вам неизвестно значение по какой-либо из них, это отличная возможность изучить и, вероятно, улучшить часть приложения. Не оставляйте параметр неизвестным; проясните для себя этот момент. Прежде чем мы обсудим эти характеристики, необходимо решить еще один вопрос: как вы используете паттерны доступа? Паттерны доступа — это чистое знание, которое образует мост между предыдущим и следующим разделами. В предыдущем разделе «Toyota и Ferrari» подчеркивается, что высокопроизводительный MySQL требует высокопроизводительного приложения. Ниже в разделе «Изменения в приложении» представлены общие изменения в приложении, которые помогают перепроектировать его для обеспечения высокой производительности по отношению к базе данных. Паттерны доступа помогают решить (а иногда и диктуют), как сделать из приложения-Toyota приложение-Ferrari.
Паттерны доступа | 147 Давайте наконец рассмотрим девять особенностей паттернов доступа к данным для MySQL. Чтение/запись Доступ ли это на чтение или запись данных? Доступ на чтение очевиден, когда там стоит SELECT. С записью менее очевидно, если всмотреться в подробности. Например, INSERT — это доступ на запись, но INSERT...SELECT — это доступ на чтение и запись. Аналогично UPDATE и DELETE должны использовать WHERE, что также предоставляет им доступ на чтение и запись. Для простоты: INSERT, UPDATE и DELETE всегда подразумевают доступ на запись. Внутренне операции чтения и записи не равны: они оказывают разное техническое воздействие и вызывают разные внутренние части MySQL. INSERT и DELETE, например, являются разными процессами записи «под капотом» — и не просто потому, что первое добавляет, а второе удаляет. Снова для простоты: все операции чтения равны, и все операции записи равны. Функция чтения/записи (англ. read/write) является одной из самых фундаментальных и распространенных, поскольку масштабирование операций чтения и записи требует неодинаковых изменений в приложении. Масштабирование операций чтения обычно выполняется путем разгрузки операций чтения, о чем я расскажу позже в разделе «Разгрузка операций чтения». Масштабирование записи сложнее, но один из методов — очередь для операций записи (см. ниже), а в главе 5 рассматривается окончательное решение: шардирование. Хотя эта характеристика доступа довольно проста, она важна, потому что знание того, загружено приложение процессами чтения или же записи, помогает быстрее состредочиться на соответствующих изменениях в приложении. Использование кеша, например, не подходит для приложения с большой нагрузкой на запись. Кроме того, другие хранилища данных оптимизированы для чтения или записи, а для MySQL существует подсистема хранения, оптимизированная для записи: MyRocks (https://myrocks.io). Пропускная способность Какова пропускная способность (в QPS) и вариации доступа к данным? Прежде всего, пропускная способность — это не производительность. Доступ с низкой пропускной способностью, даже всего 1 QPS, может привести к хаосу. Вы, вероятно, можете себе представить, как это; если нет, вот пример: инструкция SELECT...FOR UPDATE, которая выполняет сканирование таблицы и блокирует каждую строку. Настолько ужасный доступ — явление редкое, но это доказывает суть: пропускная способность — это не производительность. Несмотря на это, очень высокий QPS (высокий по отношению к приложению) — обычно проблема, которую необходимо устранить по всем причинам, красноречиво изложенным в разделе «Чем меньше QPS, тем лучше» (см. гл. 3). Например, если
148 | Глава 4 приложение выполняет биржевые торги, оно, вероятно, имеет огромный всплеск доступа на чтение и запись в 9:30 утра по восточному времени, когда открываются американские фондовые биржи. Такой уровень пропускной способности требует совершенно иных соображений, чем стабильные 500 QPS. Вариации — то, как QPS увеличивается и уменьшается, — не менее важны. В предыдущем абзаце упоминались скачкообразные и устойчивые изменения; другой тип изменений — циклический: QPS увеличивается и уменьшается в течение определенного периода времени. Распространенной циклической закономерностью является более высокий QPS в рабочее время, например с 9 утра до 5 вечера по восточному времени, и более низкий QPS в середине ночи. Распространенная проблема — что высокий QPS в рабочее время не позволяет разработчикам вносить изменения в схему (ALTER TABLE) или заполнять данные. Возраст данных Каков возраст доступных данных? Возраст зависит от порядка доступа, а не от времени. Если приложение вставляет один миллион строк за 10 минут, первая строка является самой старой, потому что к ней обращались последней, а не потому, что ей 10 минут. Если приложение обновляет первую строку, то она становится самой новой, потому что к ней самой последней был получен доступ. И если приложение больше никогда не обращается к первой строке, но продолжает обращаться к другим, то первая строка становится все старше и старше. Эта характеристика важна, потому что она влияет на рабочий набор. Напомним из раздела «Размер рабочего набора» (см. гл. 3), что рабочий набор — это часто используемые значения индекса и строки первичного ключа, на которые они ссылаются, что далеко от часто используемых данных, и обычно это небольшой процент от размера таблицы. MySQL хранит в памяти столько данных, сколько возможно, а возраст данных в памяти влияет на то, являются ли они частью рабочего набора или нет. Обычно это происходит потому, что MySQL исключительно хорошо сохраняет рабочий набор в памяти благодаря разнообразию алгоритмов и структур данных. Рис. 4.4 представляет собой сильно упрощенную иллюстрацию процесса. Прямоугольник на рис. 4.4 представляет все данные. Рабочий набор представляет собой небольшой объем данных: от пунктирной линии до вершины. И объем памяти меньше, чем у обоих: от сплошной линии до верха. Говоря языком MySQL, данные обновляются при доступе. А когда к данным нет доступа, они устаревают и в конечном итоге удаляются из памяти. Поскольку доступ к данным сохраняет их обновленными и в памяти, рабочий набор остается в памяти, потому что к нему часто обращаются. Вот почему MySQL работает очень быстро с небольшим объемом памяти и большим количеством данных. Часто доступ к старым данным вызывает проблемы более чем одним путем. Чтобы объяснить почему, нужно углубиться в технические детали, выходящие за рамки этого раздела, но я вернусь к этому позже (см. гл. 6, раздел «InnoDB»). Данные за-
Паттерны доступа | 149 гружаются на свободные страницы (в памяти): страницы, которые еще не содержат данных. (Страница — это единица логического хранилища размером 16 Кбайт внутри InnoDB.) MySQL использует всю доступную память, но также сохраняет определенное количество свободных страниц. Когда есть свободные страницы — что нормально, — остается одна проблема: чтение данных из хранилища происходит медленно. Рис. 4.4. Устаревание данных Когда свободных страниц нет — что ненормально, — проблема усугубляется в три раза. Во-первых, MySQL должна вытеснить старые страницы, которые она отслеживает в списке наименее недавно использованных (англ. least recently used, LRU). Во-вторых, если старая страница «загрязнена» (содержит изменения данных, не сохраненные на диске), MySQL должна сбросить ее (на диск, т. е. сохранить на диске), прежде чем сможет вытеснить, а сброс происходит медленно. В-третьих, остается изначальная проблема: чтение данных из хранилища также происходит медленно. Короче говоря, частое извлечение старых данных создает проблемы с производительностью. Случайное обращение к старым данным проблем не создает, потому что MySQL умна: алгоритмы, управляющие процессом на рис. 4.4, предотвращают вмешательство в новые (обновленные) данные при случайном обращении (доступе) к старым. Поэтому учитывайте возраст данных и пропускную способность вместе: «старый и медленный» доступ, вероятно, безвреден, но «старый и быстрый» — обязательно вызовет проблемы. Возраст данных практически невозможно измерить. (Технически это реально путем проверки LSN страниц данных в буферном пуле InnoDB, но это разрушительно, поэтому практически никогда не делается.) К счастью, вам нужно только оценить возраст доступных данных, что можно сделать, исходя из вашего понимания приложения, данных и паттерна доступа. Если, например, приложение хранит финансовые транзакции, вы знаете, что доступ в основном ограничен новыми данными: транзакциями за последние 90 дней. Доступ к данным старше 90 дней должен быть нечастым, поскольку транзакции были урегулированы и стали неизменяемыми. Напротив, другая часть того же приложения, которая управляет профилями пользо-
150 | Глава 4 вателей, может часто обращаться к старым данным, если процент активных пользователей высок. Помните: понятие старости данных относится к дате доступа, а не времени их существования. Профиль пользователя, который в последний раз входил в систему неделю назад, не обязательно устарел по времени, но данные его профиля относительно старые, потому что с тех пор осуществлялся доступ к миллионам других профилей, т. е. данные его профиля были вытеснены из памяти. Знание этой характеристики — необходимое условие для понимания раздела «Секционирование данных» (см. ниже) и шардирования в главе 5. Модель данных Какую модель данных демонстрирует доступ? Хотя MySQL является реляционным хранилищем данных, она нередко используется с другими моделями данных: ключ-значение, документ, сложная аналитика, график и т. д. Вы должны быть хорошо осведомлены о нереляционном доступе, потому что он не лучшим образом подходит для MySQL; следовательно, она не может обеспечить наилучшую производительность. MySQL неплохо справляется с другими моделями данных, но только до определенной степени. Например, она хорошо работает как хранилище данных типа ключ-значение, но RocksDB (https:// rocksdb.org) несравнимо лучше, потому что специально разработано как хранилище этого типа. Характерика модели данных, в отличие от других признаков, не может быть измерена программно. Вам просто нужно определить, какую модель данных демонстрирует доступ. Глагол «демонстрировать» имеет смысл: доступ может быть реляционным только потому, что MySQL была единственным доступным хранилищем данных, когда он был создан, однако он демонстрирует другую модель данных, когда вы рассматриваете все варианты хранилищ данных. Доступ часто ограничивается моделью данных доступных хранилищ. Но наилучшей практикой является обратное: определите идеальную модель данных для доступа, а затем используйте хранилище данных, разработанное для этой модели. Изоляция транзакций Какая изоляция транзакций требуется для доступа? Изоляция — это одно из четырех свойств ACID: атомарность (англ. atomicity), согласованность (англ. consistency), изоляция (англ. isolation) и долговечность (англ. durability). Поскольку подсистема хранения данных MySQL по умолчанию, InnoDB, является транзакционной, каждый запрос по умолчанию выполняется в транзакции — даже единичная инструкция SELECT (транзакции рассматриваются в главе 8). Следовательно, доступ имеет изоляцию независимо от того, нуждается он в этом или нет. Эта характеристика уточняет: требуется ли изоляция, и если да, то на каком уровне.
Паттерны доступа | 151 Когда я задаю инженерам этот вопрос, ответ попадает в одну из трех категорий: Отсутствие изоляции (англ. none) Нет, доступ не требует какой-либо изоляции. Он будет корректно выполняться в нетранзакционной подсистеме хранения данных. Изоляция — это просто бесполезные накладные расходы, однако она не вызывает никаких проблем и не оказывает заметного влияния на производительность. По умолчанию (англ. default) Предположительно, доступ требует изоляции, но неизвестно или неясно, какой уровень требуется. Приложение корректно работает с уровнем изоляции транзакций по умолчанию для MySQL: REPEATABLE READ. Потребуется тщательное обдумывание, чтобы определить, будет ли другой уровень изоляции (или отсутствие таковой) работать правильно. Специализированный уровень (англ. specific) Да, для доступа требуется определенный уровень изоляции, поскольку этот доступ является частью транзакции, выполняемой одновременно с другими транзакциями, которые обращаются к тем же данным. Без определенного уровня изоляции доступ может видеть неправильные версии данных, что чревато серьезными проблемами для приложения. По моему опыту, Default — наиболее распространенная категория, и это понятно, потому что для MySQL REPEATABLE READ подходит в большинстве случаев. Но оценка этой характеристики должна привести к None или Specific. Если доступ не требует какой-либо изоляции, то для него может не потребоваться хранилище транзакционных данных. Если требует, теперь вы точно знаете, какой уровень и почему. В других хранилищах данных также есть транзакции — даже в хранилищах данных, которые, по сути, не являются транзакционными. Например, хранилище документов MongoDB (https://www.mongodb.com) внедрило многодокументные транзакции ACID в версии 4.0. Знание того, какой уровень изоляции требуется и почему, позволяет преобразовывать доступ и перемещать его из MySQL в другое хранилище данных. Транзакции в других хранилищах данных могут сильно отличаться от транзакций в MySQL, и транзакции влияют на другие аспекты, такие как блокировка. Согласованность чтения Требует ли доступ на чтение строгой согласованности или согласованности «в конечном счете»? Строгая согласованность (или строго согласованное чтение) означает, что операции чтения возвращают самое текущее значение. Операции чтения в исходном экземпляре MySQL (не в реплике) строго согласованы, но текущее значение определяется уровнем изоляции транзакции. Длительная транзакция может считывать старое значение, но технически это текущее значение по отношению к уровню изо-
152 | Глава 4 ляции транзакции. В главе 8 подробно рассматривается этот вопрос. На данный момент помните, что строгая согласованность является опцией по умолчанию (и единственно возможной) для исходного экземпляра MySQL. Это относится не ко всем хранилищам данных. Amazon DynamoDB (https://oreil.ly/EDCme), например, по умолчанию использует согласованное «в конечном счете» чтение, а строго согласованное чтение там необязательно, более медленное и дорогостоящее. Согласованность «в конечном счете» (или согласованное «в конечном счете» чтение) означает, что чтение может возвращать старое значение, но в итоге вернет текущее значение. Операции чтения в репликах MySQL согласуются «в конечном счете» из-за запаздывания репликации: задержки между тем, когда данные записываются в источник, и тем, когда они записываются (применяются) в реплике. Продолжительность «конечного счета» примерно равна запаздыванию репликации, который должен составлять менее секунды. Реплики, используемые для предоставления доступа на чтение, называются репликами чтения. (Не все реплики служат для чтения; некоторые предназначены только для обеспечения высокой доступности или других целей.) В мире MySQL для всех доступов принято использовать исходный экземпляр, что делает все операции чтения строго согласованными по умолчанию. Но операции чтения также часто не требуют строгой согласованности, особенно когда запаздывание репликации составляет менее секунды. Когда согласованность «в конечном счете» тоже приемлема, становится возможной разгрузка операций чтения (подробности ниже). Конкурентность Осуществляется ли одновременный доступ к данным? Нулевая конкурентность означает, что доступ не считывает (или не записывает) одни и те же данные в одно и то же время (одновременно). Если он считывает (или записывает) одни и те же данные в разное время, это также означает нулевую конкурентность. Например, нулевая конкурентность у паттерна доступа, который вставляет уникальные строки. Высокая конкурентность означает, что доступ часто считывает (или записывает) одни и те же данные в одно и то же время. Конкурентность показывает, насколько важной (или приносящей проблемы) будет блокировка строк для доступа на запись. Неудивительно, что чем выше конкурентность записи одних и тех же данных, тем больше вероятность конфликта блокировки строк. Конфликт блокировки строк допустим до тех пор, пока сопутствующее увеличение времени отклика также приемлемо. А неприемлемо оно, когда приводит к тайм-ауту ожидания блокировки, что является ошибкой запроса, которую приложение должно обработать и повторить попытку. Когда это начинает происходить, есть только два решения: уменьшить конкурентность (изменить паттерн доступа) или шардировать (см. гл. 5), чтобы уменьшить объем операций записи. Конкурентность также показывает, насколько применим кеш для доступа на чтение. Если одни и те же данные считываются с высокой конкурентностью, но изме-
Паттерны доступа | 153 няются редко, то они хорошо подходят для кеша. Я освещаю это в разделе «Разгрузка операций чтения» (см. ниже). Как и возраст данных, конкурентность практически невозможно измерить, но вам нужно только оценить ее, что реально, исходя из вашего понимания приложения, данных и паттерна доступа. Доступ к строкам Как осуществляется доступ к строкам? Существуют три типа доступа к строкам:  точечный доступ (англ. point access): одна строка;  доступ к диапазону (англ. range access): упорядоченные строки между двумя значениями;  произвольный (он же случайный) доступ (англ. random access): несколько строк в любом порядке; Возьмем английский алфавит (от A до Z). Точечный доступ представляет собой любой отдельный символ (например, A); доступ к диапазону — любое количество символов по порядку (ABC или AC, если B не существует); и произвольный доступ — любое количество случайных символов (ASMR). Эта характеристика кажется упрощенной, но она важна для доступа на запись по двум причинам: 1. Блокировка интервалов: записи с диапазоном и произвольным доступом, использующие неуникальные индексы, усугубляют конфликт блокировок строк из-за блокировки интервалов. Это подробно описано в разделе «Блокировка строк» (см. гл. 8). 2. Взаимоблокировки: записи с произвольным доступом чреваты взаимоблокировками — когда две транзакции удерживают блокировку строк, необходимых другой транзакции. MySQL отслеживает и устраняет взаимоблокировки, но они снижают производительность (MySQL убивает одну транзакцию, чтобы выйти из тупика) и раздражают. Доступ к строкам также важен при планировании шардирования. Эффективное шардирование требует, чтобы паттерны доступа использовали единичный шард. Точечный доступ лучше всего работает с шардированием: одна строка, один шард. Диапазон и произвольный доступ работают с шардированием, но требуют тщательного планирования, чтобы не свести на нет преимущества шардирования из-за доступа к слишком большому количеству шардов. (Глава 5 посвящена шардированию.) Результирующий набор Группирует ли доступ, сортирует или ограничивает результирующий набор? На этот вопрос легко ответить: есть ли у доступа GROUP BY, ORDER BY или LIMIT? Каждый из этих операторов влияет на то, может ли доступ быть изменен и как он
154 | Глава 4 может быть изменен или запущен в другом хранилище данных. Раздел «Доступ к данным» (см. гл. 3) освещает несколько изменений. По крайней мере, оптимизируйте доступ, который группирует или сортирует строки. Ограничение строк — это не проблема, а преимущество, но в других хранилищах данных это работает подругому. Аналогично другие хранилища могут поддерживать или не поддерживать группировку или сортировку строк. Изменения в приложении Вы должны изменить приложение, чтобы изменить его паттерны доступа к данным. В этом разделе представлены наиболее распространенные, но не все возможные изменения. Они очень эффективны, но также сильно зависят от приложения: некоторые могут работать, другие — нет. (За исключением первого, аудит кода (см. ниже соответствующий раздел), это всегда работает.) Следовательно, каждое изменение — это идея, которая требует дальнейшего обсуждения и планирования с вашей командой. Все изменения, кроме первого, объединяет одна тонкость: они требуют дополнительной инфраструктуры. Я указываю на это, чтобы морально подготовить вас к факту, что в дополнение к изменениям кода вам также понадобятся изменения в инфраструктуре. Как и было сказано выше, в разделе «Улучшение времени отклика на запрос» (см. гл. 1), косвенная оптимизация запросов требует больших усилий. В то время как изменение данных (гл. 3) лишь с вероятностью заставит вас потрудиться, изменение паттернов доступа обязательно заставит вас потрудиться. Но оно стоит усилий, потому что эти изменения по определению преобразующие: так приложение трансформируется из Toyota в Ferrari. Вы можете удивиться: если эти изменения настолько эффективны, почему бы не внести их первыми — до оптимизации запросов и данных? Поскольку в центре внимания этой книги эффективная производительность MySQL, я планировал завершить путешествие изменениями приложений, потому что они требуют наибольших усилий. Напротив, прямая оптимизация запросов (гл. 2) и внесение изменений в данные (гл. 3) требуют гораздо меньших усилий, и первое решает множество, если не большинство, проблем с производительностью. Но если у вас есть время и энергия, чтобы сразу перейти к реинжинирингу приложения, я вас поддержу. Просто помните урок из главы 2: индексы обеспечивают наибольшее и наилучшее влияние. Плохие запросы разрушают замечательные паттерны доступа; или, цитируя известного эксперта по MySQL Билла Карвина: «Ваши неоптимизированные запросы убивают сервер базы данных». Аудит кода Вы можете быть удивлены тем, как долго код может существовать и выполняться без какого-либо человеческого присмотра. В определенном смысле это признак хорошего кода: он просто работает и не вызывает проблем. Но «не вызывает проблем» не обязательно значит, что код эффективен или вообще необходим.
Паттерны доступа | 155 Вам не нужно проверять весь код (хотя это неплохая идея), только часть, которая обращается к базе данных. Конечно, посмотрите на фактические запросы, но также учитывайте контекст: бизнес-логику, которой они следуют. Возможно, вы найдете другой и более удачный способ ее реализовать. Что касается запросов, обратите внимание на следующее:  Запросы, которые больше не нужны.  Запросы, которые выполняются слишком часто.  Запросы, которые повторяются слишком быстро или слишком часто.  Большие или сложные запросы — можно ли их упростить? Если код использует ORM — или любой другой вид абстракции базы данных, — дважды проверьте его значения по умолчанию и конфигурацию. Одно из соображений заключается в том, что некоторые библиотеки баз данных выполняют SHOW WARNINGS после каждого запроса для проверки наличия предупреждений. Обычно это не проблема, но в то же время довольно расточительно. Также дважды проверьте настройки драйвера по умолчанию, конфигурацию и примечания к выпуску. Например, драйвер MySQL для языка программирования Go за эти годы получил очень полезные разработки, поэтому коду Go стоит использовать последнюю версию. Проведите косвенный аудит кода с помощью профиля запроса, чтобы увидеть, какие запросы выполняет приложение, — анализ запросов не требуется; просто используйте профиль запроса в качестве инструмента аудита. Довольно часто в профиле можно увидеть неизвестные запросы. Если «MySQL ничего не делает» (см. начало главы), неизвестные запросы, вероятно, исходят из приложения — либо из вашего кода приложения, либо из какой-либо абстракции базы данных, например ORM, но есть и другая возможность: люди. Те, кто управляет и обслуживает хранилище данных: администраторы баз данных, облачные провайдеры и т. д. Если вы обнаружите неизвестные запросы и уверены, что приложение их не выполняет, обратитесь к тому, кто управляет хранилищем данных. Чтобы упростить аудит запросов, добавьте метаданные приложения к запросам в /* SQL comments */. Например, SELECT.../* file:app.go line:75 */ показывает, откуда берется запрос в исходном коде приложения. Комментарии SQL удаляются из текстов дайджеста, поэтому ваш инструмент метрики запросов должен включать образцы (см. гл. 1, пример 1.1) или анализировать метаданные из комментариев SQL. Наконец, что чаще упускают из виду: просмотрите лог ошибок MySQL (https:// oreil.ly/hmLlY). Все должно быть тихо: никаких ошибок, предупреждений и т. д. Если в нем много всего, изучите ошибки, потому что они указывают на широкий спектр проблем: сеть, аутентификация, репликация, конфигурация MySQL, недетерминированные запросы и т. д. Такие проблемы должны быть невероятно редкими, поэтому не игнорируйте их.
156 | Глава 4 Разгрузка операций чтения По умолчанию один экземпляр MySQL, называемый источником, обслуживает все операции чтения и записи. В рабочей среде источник должен иметь по крайней мере одну реплику: другой экземпляр MySQL, который реплицирует все записи из источника. Репликации посвящена глава 7, но я упоминаю об этом здесь, чтобы подготовить почву для разговора о разгрузке операций чтения. Производительность может быть улучшена за счет разгрузки операций чтения на источнике. Этот метод использует реплики MySQL или серверы кеша для обслуживания операций чтения. (Подробнее о них чуть позже.) Это повышает производительность двумя способами. Во-первых, снижает нагрузку на источник, что освобождает время и системные ресурсы для более быстрого выполнения оставшихся запросов. Во-вторых, улучшает время отклика для разгруженных операций чтения, поскольку реплики или кеши, обслуживающие эти операции, не нагружены операциями записи. Это беспроигрышный метод, который обычно используется для обеспечения высокой пропускной способности и низкой задержки чтения. Данные, считываемые из реплики или кеша, не гарантированно являются текущими (последнее значение), поскольку существует неотъемлемая и неизбежная задержка при репликации MySQL и записи в кеш. Следовательно, данные из реплик и кешей согласованы «в конечном счете»: они становятся актуальными после (надеюсь, очень) короткой задержки. Актуальными являются только данные в источнике (несмотря на уровни изоляции транзакций). Поэтому, прежде чем выполнять операции чтения из реплики или кеша, убедитесь, что верно следующее: чтение устаревших данных (согласованных «в конечном счете») допустимо, и это не вызовет проблем для приложения или его пользователей. Поразмыслите над этим, потому что я не раз видел, как разработчики думали об этом и понимали: «Да, это нормально, если приложение возвращает слегка устаревшие значения». Частый пример — количество «лайков» или голосов за пост или видео: если текущее значение равно 100, но кеш возвращает 98, это достаточно близко, особенно если кеш возвращает текущее значение через несколько миллисекунд. Если это утверждение не подходит для вашего приложения, не используйте этот метод. В дополнение к требованию о том, чтобы согласованность «в конечном счете» была приемлемой, разгруженные операции чтения не должны быть частью транзакции с несколькими инструкциями. Транзакции с несколькими инструкциями должны выполняться в источнике. Всегда проверяйте, приемлемы ли разгрузки операций чтения с согласованностью «в конечном счете» и не являются ли они частью транзакции с несколькими инструкциями. Прежде чем выполнять операции чтения из реплик или кешей, тщательно рассмотрите этот вопрос: как приложение будет работать с ухудшением качества, когда реплики или кеши находятся в автономном режиме?
Паттерны доступа | 157 Единственный неправильный ответ здесь — это незнание. Как только приложение разгружает операции чтения, оно, как правило, начинает сильно зависеть от реплик или кешей для обслуживания этих операций чтения. Крайне важно спроектировать, внедрить и протестировать приложение так, чтобы оно могло работать и в ухудшенном состоянии, когда реплики или кеши находятся в автономном режиме. Ухудшенное состояние означает, что приложение работает, но заметно медленнее, ограничивает запросы клиентов или не полностью функционально, поскольку некоторые части отключены или отключены частично. Пока приложение не вылетает — это когда оно полностью отключается и не отвечает без понятного для пользователя сообщения об ошибке, — можете считать, что вы проделали хорошую работу, заставив его работать в ухудшенном состоянии. Последний момент, прежде чем мы сравним использование реплики MySQL и серверов кеша: не разгружайте все чтения подряд. Разгрузка операций чтения повышает производительность за счет того, что не тратится время источника на ту работу, что может выполнить реплика или кеш. Поэтому начните с разгрузки медленных (отнимающих много времени) операций чтения: тех, что отображаются как медленные запросы в профиле запроса. Реплика MySQL Использование реплик MySQL для обслуживания операций чтения — обычное дело, поскольку каждый производственный экземпляр MySQL уже должен иметь по крайней мере одну реплику, а имеет обычно более двух. Поскольку инфраструктура (реплики) уже создана, вам нужно только изменить код, чтобы использовать реплики для разгруженных операций чтения вместо источника. Прежде чем объяснить, почему реплики предпочтительнее кеш-серверов, надо решить один важный вопрос: может ли приложение использовать реплики? Поскольку реплики используются для обеспечения высокой доступности, тот, кто управляет MySQL, может не предполагать, что они будут обслуживать операции чтения. Обязательно выясните это, потому что в противном случае реплики могут быть без предварительного уведомления переведены в автономный режим для технического обслуживания. Если ваши реплики все же могут использоваться для обслуживания операций чтения, они предпочтительнее кеш-серверов по трем причинам: Доступность Поскольку реплики — основа высокой доступности, они должны иметь ту же доступность, что и источник, — например, 99,95 или 99,99%. Тогда реплики почти не доставляют хлопот: тот, кто управляет MySQL, также управляет репликами. Гибкость В предыдущем разделе я сказал, что вы должны начать с разгрузки медленных (требующих много времени) операций чтения. Для кешей это особенно верно, потому что кеш-сервер, скорее всего, имеет ограниченные ресурсы процессора и памяти, которые не следует тратить впустую на тривиальные чтения. Напротив,
158 | Глава 4 реплики, используемые для обеспечения высокой доступности, должны иметь то же оборудование, что и источник, так что у них есть свободные ресурсы. Разгрузка обычных операций чтения в реплику не имеет такого большого значения, отсюда и гибкость при выборе того, что разгружать. Если только у вас не реплики чисто для чтения — неиспользуемые для обеспечения высокой доступности, — с менее мощным оборудованием, не тратьте ресурсы на обычные чтения. Это чаще встречается в облаке, потому что проще предоставлять клиентам реплики для чтения с большим объемом памяти, но небольшим процессором и памятью (для экономии денег). Простота Приложению не нужно ничего делать, чтобы поддерживать синхронизацию реплик с источником — реплики сами все сделают. Что касается кеша, приложение должно управлять обновлениями, аннулированием и (возможно) вытеснением. Но настоящая простота заключается в том, что реплики не требуют каких-либо изменений запроса: приложение может выполнять точно такие же инструкции SQL на реплике. Это три веские причины предпочесть реплики MySQL кеш-серверам, но у последнего есть один важный плюс: кеш-сервер может быть гораздо быстрее, чем MySQL. Кеш-сервер Кеш-сервер не перегружен SQL, транзакциями или долговременным хранением. Это делает его гораздо быстрее, чем MySQL, но также требует больше работы для правильного использования приложения. Как упоминалось в предыдущем разделе, приложение должно управлять обновлениями кеша, аннулированием и (возможно) вытеснением. Кроме того, приложению нужна модель данных, которая работает с кешем — обычно это модель ключ-значение. Дополнительная работа стоит затраченных усилий, потому что практически нет ничего быстрее, чем кеш. Memcached (https://memcached.org) и Redis (https://redis.io) — два популярных и широко используемых сервера кеширования. Если вы услышите, что в MySQL есть встроенный кеш запросов — забудьте об этом и никогда не используйте его. Он устарел с версии MySQL 5.7.20 и был удален с версии MySQL 8.0. Кеширование идеально подходит для данных, к которым часто обращаются, но которые редко изменяются. Это не важно для реплик MySQL, потому что все изменения реплицируются, но в кеше хранится только то, что приложение помещает в него. Пример плохого случая: текущая временная метка Unix в секундах — она всегда меняется. Исключение для подобного плохого случая: если частота доступа значительно превышает частоту изменений. Если, скажем, текущая временная метка Unix в секундах запрашивается миллион раз в секунду, то кеширование текущей временной метки может быть уместным. Пример хорошего случая — текущий год: он меняется нечасто. Однако есть исключение и в подобных случаях: если частота доступа значительно меньше частоты изменений. Например, если текущий год за-
Паттерны доступа | 159 прашивается только один раз в секунду, то в кеше нет особой ценности, потому что 1 QPS не играет роли для этого доступа к данным. Предостережение при использовании кеша: решите, является ли кеш эфемерным или долговечным. Это тоже не важно для реплик MySQL, потому что они всегда долговечны, но некоторые серверы кеша могут быть и теми и другими. Если кеш действительно эфемерный, то вы должны быть в состоянии выполнить эквивалент TRUNCATE TABLE для данных кеша, не затрагивая приложение. Вам также необходимо решить, как будет перестраиваться эфемерный кеш. Некоторые приложения перестраивают кеш при промахе кеша: когда запрошенных данных в кеше нет. Другие приложения имеют внешний процесс для восстановления кеша из другого источника данных (например, загрузка кеша изображениями, хранящимися в Amazon S3, https://oreil.ly/XMQxR). А третьи так сильно полагаются на кеш или кеш настолько велик, что его перестройка невозможна. Для таких приложений требуется надежный кеш. В любом случае, эфемерный он или долговременный, проверьте свое решение, чтобы убедиться, что приложение функционирует должным образом, когда происходит сбой кеша и его восстановление. Очередь для операций записи Используйте очередь для стабилизации пропускной способности операций записи. Рис. 4.5 иллюстрирует нестабильную — неустойчивую пропускную способность операций записи, которая резко возрастает выше 30 000 QPS и падает ниже 10 000 QPS. Рис. 4.5. Неустойчивая пропускная способность операций записи Даже если производительность в настоящее время приемлема при нестабильной пропускной способности записи, это не рецепт успеха, потому что нестабильная пропускная способность ухудшается при масштабировании — она никогда не стабилизируется самопроизвольно. (И если вы помните рис. 4.3 из раздела «Производительность дестабилизируется на пределе» (см. выше), плоская линия не является признаком стабильной работы). Использование очереди позволяет приложению обрабатывать изменения (операции записи) со стабильной скоростью, как показано на рис. 4.6. Реальная сила очереди для операций записи и стабильной пропускной способности операций записи заключается в том, что они позволяют приложению корректно и
160 | Глава 4 предсказуемо реагировать на грохочущее стадо: поток запросов, который перегружает приложение, или базу данных, или и то и другое. Например, представьте, что приложение обычно обрабатывает 20 000 изменений в секунду. Но оно отключается на пять секунд, что приводит к 100 000 ожидающих изменений. В тот момент, когда приложение возвращается в сеть, оно получает 100 000 ожидающих изменений — воистину грохочущее стадо, плюс обычные 20 000 изменений за секунду. Как приложение и MySQL будут справляться с этим? Рис. 4.6. Стабильная пропускная способность записи С очередью грохочущее стадо не влияет на MySQL: оно попадает в очередь, и MySQL обрабатывает изменения как обычно. Разница лишь в том, что некоторые изменения происходят позже, чем обычно. Пока пропускная способность записи стабильна, вы можете увеличить количество обрабатывающих ее процессов. Опыт показывает, что без очереди произойдет одно из двух: либо вам очень повезет, и MySQL справится с грохочущим стадом, либо нет. Не рассчитывайте на удачу. MySQL не ограничивает выполнение запросов, поэтому попытается выполнить все запросы, когда это стадо ударит. (Однако MySQL Enterprise Edition, Percona Server и MariaDB Server имеют пул потоков, где лимитировано количество одновременно выполняемых запросов, что действует как ограничение). Это никогда не работает, ведь процессор, память и дисковый ввод-вывод изначально ограничены — не говоря уже об универсальном законе масштабируемости (см. уравнение 4.1). Несмотря на это, MySQL всегда старается, потому что она невероятно амбициозна и немного безрассудна. Этот метод дает и другие преимущества, благодаря которым его стоит внедрить. Например, он делает приложение независимым от доступности MySQL: оно может принимать изменения, когда MySQL не в сети. Еще одно преимущество: очередь можно использовать для восстановления потерянных или заброшенных изменений. Предположим, что изменение требует различных шагов, некоторые из которых могут быть длительными или ненадежными. Если какой-либо шаг завершается неудачей или истекает время ожидания, приложение может повторно запросить изменение, чтобы повторить попытку. Третьим преимуществом является возможность воспроизведения изменений, если очередь представляет собой поток событий, как у Kafka (https://oreil.ly/fRZpa).
Паттерны доступа | 161 Для приложений с интенсивной записью очередь для операций записи является лучшей практикой и практически обязательным требованием. Потратьте время на изучение и внедрение очереди. Секционирование данных После главы 3 вас не должно удивлять, что с меньшим количеством данных проще повысить производительность. Как вы помните, данные ценны для вас, но для MySQL они мертвый груз. Если вы не можете удалить или заархивировать данные (см. гл. 3, раздел «Удаление или архивирование данных»), то вам следует, по крайней мере, разделить (физически отделить) их. Во-первых, давайте кратко рассмотрим, а затем отложим в сторону секционирование (англ. partitioning) MySQL (https://oreil.ly/BNopd). MySQL поддерживает его, но оно требует специальной обработки. Реализовать или поддерживать это — задача нетривиальная, и некоторые сторонние инструменты MySQL такое не умеют. Следовательно, я не рекомендую его использовать. Тип секционирования данных, что наиболее полезен, распространен и прост для реализации разработчиками приложений — это разделение горячих и холодных данных: часто и нечасто используемых данных соответственно. Разделение горячих и холодных данных представляет собой комбинацию секционирования и архивирования. Оно секционирует данные по доступу и архивирует, перемещая редко используемые (холодные) с путей доступа к часто используемым (горячим). Вот пример: база данных, в которой хранятся платежи. Горячие данные — это последние 90 дней платежей — по двум причинам. Во-первых, платежи обычно не меняются после расчета, но есть исключения, такие как возврат средств, которые могут быть применены позже. Однако по истечении некоторого периода платежи завершаются и не могут быть изменены. Во-вторых, приложение показывает только последние 90 дней платежей. Чтобы увидеть более старые платежи, пользователи должны просмотреть прошлые выписки. Холодные данные — это платежи через 90 дней. За год это 275 дней, что составляет примерно 75% данных. Почему 75% данных лежат без дела в хранилище транзакционных данных, таком как MySQL? Это риторический вопрос: здесь нет веской причины. Разделение горячих и холодных данных — это в первую очередь оптимизация для первых. Хранение холодных данных в другом месте дает три непосредственных преимущества: больше горячих данных помещается в память, запросы не тратят время на изучение холодных данных, а операции (например, изменение паттерна) выполняются быстрее. Для холодных данных такое разделение также служит оптимизацией, когда они имеют совершенно разные паттерны доступа. В предыдущем примере старые платежи могут быть сгруппированы по месяцам в единый объект данных, где больше не требуется своя строка для каждого платежа. В этом случае для хранения и доступа к холодным данным может лучше подойти хранилище документов или хранилище по типу ключ-значение. По крайней мере, вы можете архивировать холодные данные в другой таблице в той же базе данных. Это относительно просто с помощью контролируемой инст-
162 | Глава 4 рукции INSERT...SELECT для отбора из горячей таблицы и вставки в холодную таблицу. Затем DELETE удаляет архивированные холодные данные из горячей таблицы. Оберните все это в транзакцию для обеспечения согласованности (см. гл. 3, раздел «Удаление или архивирование данных»). Этот метод может быть реализован множеством различных способов, особенно в отношении того, как и где хранятся холодные данные и как к ним осуществляется доступ. Но в принципе это очень просто и очень эффективно: переместите редко используемые (холодные) данные с путей доступа к часто используемым (горячим) данным, чтобы повысить производительность для последних. Не используйте MySQL Я хочу образно подвести итог текущему обсуждению изменений в приложениях: наиболее значительным изменением является отказ от использования MySQL, когда это явно не лучшее хранилище данных для паттернов доступа. Иногда определить это очень легко. Например, в предыдущих главах я ссылался на запрос с нагрузкой 5962. Он используется для отбора вершин в графе. Очевидно, что реляционная база данных — не лучший вариант для графических данных; тут подошло бы хранилище графических данных. Даже хранилище типа ключ-значение было бы лучше, потому что графические данные не имеют ничего общего с такими концепциями реляционных баз данных, как нормализация и транзакции. Другой простой и распространенный пример — данные временных рядов: транзакционная база данных, ориентированная на строки, — не лучший выбор; лучшим выбором будет база данных временных рядов или, возможно, хранилище столбцов. MySQL удивительно хорошо масштабируется для широкого спектра данных и паттернов доступа, даже если она не лучший выбор. Но никогда не принимайте это как данность — будьте первым инженером в своей команде, который скажет: «Возможно, MySQL тут не подходит». Все в порядке: если я могу это сказать, то и вы тоже можете. Если кто-то вас обидит, скажите им, что я поддерживаю ваше решение использовать более подходящий инструмент для этой работы. Тем не менее MySQL потрясающая. Пожалуйста, закончите по крайней мере эту главу и следующую, главу 5, прежде чем отвергнуть ее кандидатуру. Более быстрое и мощное оборудование? Раздел «Более быстрое и мощное оборудование!» (см. гл. 2) предостерегает от масштабирования оборудования для повышения производительности. Но первое предложение этого раздела тщательно сформулировано: «Когда производительность MySQL неприемлема, не начинайте с масштабирования...» Ключевые слова в этом предложении — не начинайте, и ключевой вопрос, к которому они приводят, таков: когда наступит нужное время для масштабирования оборудования? На этот вопрос трудно ответить, потому что он зависит от сочетания факторов: запросов, индексов, данных, паттернов доступа и того, как они используют текущее
Паттерны доступа | 163 оборудование. Например, возьмем приложение, у которого дико неэффективный паттерн доступа: оно использует MySQL для очереди и очень быстро опрашивает его из многих экземпляров приложения. Я бы не стал масштабировать оборудование, пока сначала не исправлю паттерн доступа. Но иногда у инженеров нет такой роскоши, как время для внесения таких изменений в приложение. Табл. 4.2 представляет собой контрольный список, который поможет определить, пора ли масштабировать оборудование. Когда вы можете отметить все элементы в столбце 1 и по крайней мере два элемента в столбце 2, это явный признак, что пора. Таблица 4.2. Контрольный список обновления оборудования 1. Отметить все 2. Отметить хотя бы два ☐ Время отклика слишком велико ☐ Загрузка процессора превышает 80% ☐ Оптимизированы медленные запросы ☐ Количество потоков, выполняющихся больше, чем количество ядер процессора ☐ Данные были удалены или заархивированы ☐ Объем памяти составляет менее 10% от общего объема данных ☐ Паттерны доступа были пересмотрены и оптимизированы ☐ Загрузка IOPS превышает 80% Колонка 1 — бесстыдное перечисление всего, о чем шла речь после главы 1, но также это недвусмысленное оправдание расходов на модернизацию оборудования. Столбец 2 требует по крайней мере двух галочек, поскольку аппаратное обеспечение работает в комплексе. Интенсивное использование только одной части оборудования не гарантирует проблем или низкой производительности. Это даже хороший знак: вы полностью используете эту часть оборудования. Но когда одна часть оборудования перегружена, это обычно начинает влиять на другие части. Например, когда медленное хранение вызывает накопление запросов, что приводит к накоплению клиентов, что приводит к высокой загрузке процессора, потому что MySQL пытается выполнить слишком много потоков. Вот почему столбец 2 требует двух галочек. Значения в столбце 2 должны быть постоянно больше или меньше предложенных пороговых значений. Случайные всплески и провалы — это нормально. Максимальное количество операций ввода-ввывода в секунду (англ. Input/Output Operations Per Second, IOPS) для хранилища определяется устройством хранения, если используется ваше собственное оборудование. Если вы не уверены, ознакомьтесь со спецификациями устройства или обратитесь к инженерам, которые управляют оборудованием. В облаке предусматриваются или предлагаются параметры IOPS, поэтому обычно тут легче определить максимум, поскольку вы покупаете IOPS. Но если вы не уверены, проверьте настройки хранилища MySQL или обратитесь к поставщику облачных услуг. Раздел «IOPS» (см. гл. 6) объясняет, какие метрики отражают IOPS.
164 | Глава 4 При использовании IOPS есть дополнительное соображение: есть ли у приложения большая нагрузка по операциям чтению или операциям записи (см. выше, раздел «Чтение/запись»). Нагрузка по операциям чтения У паттернов доступа с нагруженным доступом на чтение стабильно высокие IOPS, вероятно, связаны с нехваткой памяти, а не с недостаточным IOPS. MySQL считывает данные с диска, когда они не находятся в памяти, и исключительно хорошо сохраняет в памяти рабочий набор (см. гл. 3, раздел «Размер рабочего набора»). Но комбинация двух факторов может привести к высоким IOPS для операций чтения: размер рабочего набора значительно больше, чем объем памяти, а пропускная способность чтения исключительно высока (см. выше, раздел «Пропускная способность»). Эта комбинация вынуждает MySQL передавать так много данных между диском и памятью, что проблема проявляется как высокий IOPS. Это редко, но возможно. Нагрузка по операциям записи Для моделей доступа с интенсивной записью стабильно высокое IOPS, вероятно, связано с недостаточным IOPS. Проще говоря: хранилище не может записывать данные достаточно быстро. Обычно хранилище обеспечивает высокую пропускную способность (IOPS) с помощью кешей операций записи, но кеши ненадежны. MySQL требует надежного хранения: данные хранятся на физическом диске, а не в кешах. (Фраза «на диске» по-прежнему используется даже для флешнакопителей, у которых нет дисков). Следовательно, MySQL должна сбросить данные — принудительно записать их на диск. Сброс сильно ограничивает пропускную способность хранилища, но MySQL обладает сложными методами и алгоритмами для достижения производительности и долговечности — подробно это описывает раздел «Сброс страниц» в главе 6. Единственное решение на данный момент — поскольку вы уже оптимизировали запросы, данные и паттерны доступа — увеличение IOPS для хранилища. Осторожно намекнув себе на расширение аппаратного обеспечения, мы, похоже, подошли к концу. Независимо от того, сколько гальки, булыжников или валунов надо переместить, мы всегда можем использовать для их перемещения грузовик побольше. Но что если придется сдвинуть гору? Тогда вам нужна следующая глава, посвященная шардированию. Итоги Эта глава посвящена паттернам доступа к данным, которые определяют, как можно изменить приложение для эффективного использования MySQL. Ключевые моменты:  MySQL ничего не делает, кроме выполнения запросов приложений.  Производительность базы данных дестабилизируется при пределе, который со- ставляет менее 100% от аппаратной емкости.
Паттерны доступа | 165  Некоторые приложения имеют гораздо большую производительность MySQL, потому что каждая их деталь спроектирована для обеспечения высокой производительности.  Паттерны доступа описывают, как приложение использует MySQL для доступа к данным.  Вы должны изменить приложение, чтобы изменить его паттерны доступа к дан- ным.  Масштабируйте аппаратное обеспечение для повышения производительности после исчерпания других решений. В следующей главе представлена базовая механика шардирования MySQL для достижения масштабируемости MySQL. Практика: опишите паттерн доступа Цель этой практики — описать паттерн доступа для самого медленного запроса. (Чтобы получить медленные запросы, обратитесь к разделам «Профиль запроса» и «Практика: определение медленных запросов» (см. гл. 1)). Для самого медленного запроса проанализируйте все девять характеристик паттерна доступа из раздела «Паттерны доступа к данным» (см. выше). Как упоминалось в этом разделе, паттерны доступа — это чистое знание. Используйте это знание, чтобы рассмотреть, какие изменения в приложении (см. выше соответствующий раздел) можно внести, чтобы косвенно оптимизировать запрос через изменение его паттерна доступа. Даже если изменения в приложении невозможны, знание паттернов доступа — экспертная практика, поскольку от запросов, данных и паттернов доступа зависит производительность MySQL.
166 | Глава 4
ГЛАВА 5 Шардирование Для одного экземпляра MySQL производительность зависит от запросов, данных, паттернов доступа и аппаратного обеспечения. Когда усердная прямая и косвенная оптимизация запросов больше не обеспечивают приемлемую производительность, вы достигли относительного предела производительности одного экземпляра MySQL для рабочей нагрузки приложения. Чтобы превзойти этот относительный предел, вы должны разделить рабочую нагрузку приложения на несколько экземпляров MySQL для масштабирования MySQL. Шардирование (шардинг) базы данных — это распространенный и широко используемый метод горизонтального масштабирования: повышение производительности за счет распределения рабочей нагрузки между несколькими базами данных. Вертикальное масштабирование, напротив, повышает производительность за счет увеличения емкости оборудования. Шардирование разделяет одну базу данных на множество баз данных. Каждая база данных представляет собой шард (сегмент), и каждый шард обычно хранится в отдельном экземпляре MySQL, работающем на отдельном оборудовании. Шарды физически разделены, но логически представляют собой одну и ту же (очень большую) базу данных. Масштабирование MySQL требует шардирования. Я собираюсь повторить это предложение несколько раз в главе, потому что инженеры не решаются принять данный факт. Почему? Потому что шардирование не является неотъемлемой функцией или возможностью MySQL. То есть это сложно и полностью зависит от конкретного приложения, а следовательно, простого решения не существует. Но не отчаивайтесь: проблема это решаемая. Инженеры масштабировали MySQL на протяжении десятилетий. В этой главе представлена базовая механика шардирования для масштабирования MySQL. Здесь четыре основных раздела. Первый объясняет, почему единая база данных не масштабируется — почему необходимо шардирование. Второй завершает аналогию из глав 3 и 4: почему галька (шарды базы данных) лучше, чем валуны (огромные базы данных). Третья часть — краткое введение в сложную тему шардирования реляционных баз данных. В четвертой представлены альтернативы шардированию.
168 | Глава 5 Почему единая база данных не масштабируется Никто не сомневается в том, что одно приложение может перегрузить один сервер — вот почему масштабирование необходимо для всех типов серверов и приложений, а не только для MySQL. Шардирование необходимо, потому что именно так MySQL масштабируется: больше баз данных. Но разумно задаться вопросом, почему единая база данных MySQL не масштабируется, даже если доступно очень мощное оборудование с отличными результатами по тестам производительности. Далее описаны пять причин, начиная с самой фундаментальной: рабочая нагрузка приложений может значительно превышать скорость и пропускную способность односерверного оборудования. Рабочая нагрузка приложения Рис. 5.1 представляет собой простую иллюстрацию пропускной способности оборудования на одном сервере с нулевой нагрузкой Рис. 5.1. Оборудование без нагрузки Рис. 5.2. Оборудование со стандартной рабочей нагрузкой MySQL Рис. 5.1 намеренно прост, но не упрощен, потому что он тонко передает критически важный момент: пропускная способность оборудования конечна и ограничена. Круг представляет пределы оборудования. Давайте предположим, что оборудование предназначено для запуска одного экземпляра MySQL для одного приложения — никакой виртуализации, майнинга криптовалют или другой нагрузки. Все, что работает на оборудовании, должно помещаться внутри круга. Поскольку это выделенное оборудование, единственное, что на нем работает, — это рабочая нагрузка приложения, показанная на рис. 5.2: запросы, данные и паттерны доступа Не случайно запросы относятся к главе 2, данные — к главе 3, а паттерны доступа — к главе 4. Они составляют рабочую нагрузку приложения: все, что вызывает нагрузку на MySQL, которая, в свою очередь, вызывает нагрузку на оборудование (загрузка процессора, дисковый ввод-вывод и т. д.). Размеры прямоугольника на
Шардирование | 169 рисунке важны: чем больше прямоугольник, тем больше нагрузка. На рис. 5.2 рабочая нагрузка находится в пределах возможностей оборудования, при этом остается немного свободного места, поскольку операционной системе тоже нужны аппаратные ресурсы. Запросы, данные и паттерны доступа неразрывно связаны с производительностью. (Я доказал это с помощью TRUNCATE TABLE в разделе «Косвенная оптимизация запросов» (см. гл. 1). Объем данных — частая причина масштабирования, поскольку, как показано на рис. 5.3, из-за этого рабочая нагрузка может превышать мощность одного сервера. Рис. 5.3. Оборудование со слишком большим объемом данных Рис. 5.4. Оборудование только с данными (неверное представление о масштабировании) Объем данных не может расти без того, чтобы в итоге не повлиять на запросы и паттерны доступа. Покупка жесткого диска большего размера не решит проблему, потому что, как это показано на рис. 5.3, для хранения данных достаточно места, но данные — не единственная часть рабочей нагрузки. Рис. 5.4 иллюстрирует распространенное среди инженеров заблуждение, будто одна база данных может масштабироваться до максимального объема данных, который в настоящее время составляет 64 Тбайта для одной таблицы InnoDB Данные — это лишь одна часть рабочей нагрузки, и две другие части (запросы и паттерны доступа) нельзя игнорировать. В реальности для приемлемой производительности с большим количеством данных на одном сервере рабочая нагрузка должна выглядеть так, как показано на рис. 5.5. Если запросы просты и имеют исключительно хорошие индексы, а паттерны доступа тривиальны (например, чтение с очень низкой пропускной способностью), то на одном сервере может храниться много данных. Это не просто умная иллюстрация; настоящие приложения имеют рабочие нагрузки, подобные той, что на рис. 5.5. Эти пять иллюстраций показывают, что единичная база данных не может масштабироваться, поскольку рабочая нагрузка приложения, которая включает запросы, данные и паттерны доступа, должна соответствовать возможностям аппаратного обеспечения. После разделов «Более быстрое и мощное оборудование!» (см. гл. 2)
170 | Глава 5 и «Более быстрое и мощное оборудование?» (см. гл. 4) вы уже знаете, что аппаратное обеспечение не решит эту проблему. Масштабируемая MySQL требует шардирования, поскольку рабочие нагрузки приложений могут значительно превышать скорость и производительность односерверного оборудования. Рис. 5.5. Оборудование с большими объемами данных Тесты производительности синтетичны Бенчмарки (тесты производительности) используют синтетические (искусственные) запросы, данные и паттерны доступа. Они обязательно ненастоящие, потому что это не реальные приложения и, конечно же, не ваше конкретное приложение. Поэтому тесты не могут сказать или даже предположить, как ваше приложение будет работать и масштабироваться даже на том же самом оборудовании. Более того, в основном они фокусируются на одном или нескольких паттернах доступа (см. гл. 4, раздел «Паттерны доступа к данным»), что приводит к рабочей нагрузке, подобной той, что показана на рис. 5.6. Большинство приложений не имеют рабочей нагрузки, в которой производительность зависит от одного или нескольких паттернов доступа. Но для тестов это Рис. 5.6. Оборудование с эталонной рабочей нагрузкой
Шардирование | 171 обычное явление, потому что позволяет экспертам MySQL выделять и измерять определенный аспект MySQL. Например, если специалист по MySQL хочет измерить эффективность нового алгоритма сброса страницы, он может использовать 100%-ную рабочую нагрузку только для операций записи с несколькими идеально оптимизированными запросами и очень небольшим количеством данных. Но позвольте мне быть предельно ясным: тесты производительности важны и необходимы для экспертов и индустрии MySQL. (Как упоминалось в разделе «Тонкая настройка (тюнинг) MySQL» (см. гл. 2), подобный анализ — это лабораторная работа.) Тесты используются для следующих задач:  сравнение аппаратного обеспечения (одного устройства хранения данных с дру- гим);  сравнение оптимизации сервера (одного алгоритма сброса с другим);  сравнение различных хранилищ данных (MySQL против PostgreSQL — класси- ческое соперничество);  тестирование MySQL на пределе (см. гл. 4, раздел «Производительность деста- билизируется на пределе»). Эта работа невероятно важна для MySQL, и именно поэтому MySQL обладает потрясающей производительностью. Но в этом списке явно отсутствует что-либо, связанное с вашим приложением и его конкретной рабочей нагрузкой. Следовательно, что бы вы ни читали и ни слышали о прекрасных результатах тестов производительности MySQL, это не отразится на вашем приложении, и те же самые эксперты, которые создают эти тесты, скажут вам: масштабирование MySQL требует шардирования. Операции записи Операции записи трудно масштабировать в одном экземпляре MySQL по нескольким причинам: Единственный доступный для записи (исходный) экземпляр Для обеспечения высокой доступности MySQL в рабочей среде использует несколько экземпляров, соединенных в топологии репликации. Но операции записи фактически ограничены одним экземпляром MySQL, чтобы избежать конфликтов записи: когда идет несколько записей в одну и ту же строку одновременно. MySQL поддерживает несколько экземпляров, доступных для операций записи, но вам будет трудно найти кого-либо, кто использует эту функцию, потому что от конфликтов записей слишком много хлопот. Транзакции и блокировка Транзакции используют блокировку для обеспечения согласованности — C в базе данных, совместимой с ACID. Операции записи должны получать блокироку строк, а иногда они блокируют значительно больше строк, чем можно ожидать, — раздел «Блокировка строк» (см. гл. 8) объясняет почему. Блокировка приводит к конфликту блокировок, что делает характеристику паттерна доступа
172 | Глава 5 «Конкурентность» (см. гл. 4) критическим фактором, определяющим, насколько хорошо масштабируются записи. Если рабочая нагрузка слишком велика для операций записи одних и тех же данных, даже самое лучшее оборудование в мире не поможет. Сброс страницы (долговечность) Сброс страницы (англ. page flushing) — это отложенный процесс, с помощью которого MySQL переносит изменения (от операций записи) на диск. Весь он слишком сложен, чтобы изложить его в этом разделе, но суть в том, что сброс страницы — узкое место производительности для операций записи. Хотя MySQL очень эффективна, процесс по своей сути медленный, потому что MySQL должна гарантировать, что данные долговечны: сохраняются на диске. Без требования долговечности операции записи выполняются невероятно быстро из-за кеширования, но долговечность обязательна, потому что все оборудование в конце концов выходит из строя. Увеличение объема записи Увеличение объема записи (англ. Write Amplification, WA) относится к операциям записи, требующим большего количества операций записи. Вторичные индексы — самый простой пример. Если таблица имеет 10 вторичных индексов, одна операция записи может привести к 10 дополнительным записям для обновления этих индексов. Сброс страницы (долговечность) приводит к дополнительным операциям записи, а репликация приводит к еще большему количеству таких операций. Это не уникально для MySQL; это действует и в других хранилищах данных. Репликация Репликация требуется для обеспечения высокой доступности, поэтому все операции записи должны реплицироваться на другие экземпляры MySQL — реплики. Репликация рассматривается в главе 7, но вот несколько важных моментов, касающихся масштабирования записей. MySQL поддерживает асинхронную, полусинхронную и групповую репликацию (https://oreil.ly/oeJtD). Асинхронная репликация оказывает меньшее влияние на производительность процессов записи, поскольку изменения данных записываются и сбрасываются в двоичные логи при фиксации транзакции, но после этого никакого воздействия нет. Полусинхронная репликация оказывает большее влияние на производительность операций записи: она снижает пропускную способность транзакций из-за задержки в сети, поскольку каждая фиксация должна быть подтверждена хотя бы одной репликой. Поскольку задержка в сети измеряется в миллисекундах, влияние на производительность операций записи заметно, но это выгодный компромисс — он гарантирует, что никакие зафиксированные транзакции не будут потеряны, — а для асинхронной репликации это не так. Групповая репликация более сложна, и масштабировать записи сложнее. В главе 7 я объясню, почему не рассматриваю групповую репликацию в этой книге.
Шардирование | 173 Эти пять причин представляют собой серьезные сложности для масштабирования операций записи в одном экземпляре MySQL — даже для экспертов MySQL. Масштабирование MySQL требует шардирования для преодоления этих сложностей и увеличения производительности операций записи. Изменения схемы Изменения схемы (англ. Shema Changes) — это нечто большее, чем обычная рутина; они фактически необходимы. Кроме того, нередки случаи, когда самые большие таблицы часто меняются, потому что их объем отражает их использование, а использование приводит к разработке, что, в свою очередь, приводит к изменениям. Даже если вам удастся преодолеть все остальные препятствия и масштабировать одну таблицу до огромных размеров, время, необходимое для изменения этой таблицы, будет неприемлемым. Как долго? Изменение большой таблицы может занять несколько дней или недель. Для MySQL или приложения долгое ожидание не проблема, потому что инструменты онлайн-изменения схемы (англ. online schema change, OSC), такие как pt-online-schema-change (https://oreil.ly/tSrrr) и gh-ost (https://oreil.ly/nUuvv), а также некоторые встроенные онлайн-операции DDL (https://oreil.ly/5KiA7) могут выполняться в течение нескольких дней или недель, позволяя приложению нормально функционировать — вот почему они называются онлайн. Но это проблема для инженеров, разрабатывающих приложение, потому что столь долгое ожидание не останется незамеченным; оно постепенно начнет все более раздражать и вас, и других инженеров, и, возможно, другие команды. Например, лишь несколько недель назад я помог команде переделать несколько таблиц, каждая из которых содержала один миллиард строк, что не удавалось после почти двухнедельных попыток (по различным техническим причинам, не связанным с MySQL). Препятствие было далеко за рамками таблицы или навыков команды: короче говоря, оно обнаружилось на уровне организации — месяцы работы нескольких других команд. К счастью, необходимое изменение схемы оказалось мгновенной онлайн-операцией DDL (https://oreil.ly/5KiA7). Но мгновенные изменения схемы чрезвычайно редки, так что не рассчитывайте на них. Вместо этого не позволяйте таблице становиться настолько большой, что вы не сможете изменить ее за разумный промежуток времени — что бы вы, ваша команда и ваша компания ни считали разумным. Масштабирование MySQL требует шардирования, потому что инженеры не могут ждать несколько дней или недель, чтобы изменить схему. Операции Если вы прямо или косвенно оптимизируете запросы со строгой точностью и абсолютной скрупулезностью, то можете увеличить единую базу данных до объема, в который люди не поверят, пока вы им не покажете. Но иллюстрации аппаратного обеспечения и рабочей нагрузки в разделе «Рабочая нагрузка приложения» (см. выше) не отображают следующие операции:
174 | Глава 5  резервное копирование и восстановление;  восстановление сбойных экземпляров;  обновление MySQL;  завершение работы, запуск и аварийное восстановление MySQL. Чем больше база данных, тем больше времени занимают эти операции. Как разработчик приложений, вы можете не управлять ни одной из них, но они повлияют на вас — если только инженеры, управляющие базой данных, не окажутся исключительно опытными специалистами, глубоко приверженными операциям с нулевым временем простоя. Облачные провайдеры, например, не являются ни опытными, ни преданными делу; они всего лишь пытаются свести к минимуму время простоя, которое может занимать от 20 секунд до нескольких часов автономной работы базы данных. Масштабирование MySQL требует шардирования для эффективного управления данными, что приводит нас к следующему разделу: галька, а не валуны. Галька, а не валуны Перемещать гальку значительно легче, чем валуны. Я продолжаю эту аналогию, потому что она уместна: масштабирование MySQL достигается за счет использования множества небольших экземпляров. (Чтобы освежить в памяти аналогию, прочтите вводные разделы глав 3 и 4.) «Небольшие» в этом контексте означает две вещи: 1. Рабочая нагрузка приложения выполняется с приемлемой производительностью на оборудовании. 2. Стандартные операции (включая OSC) занимают приемлемое количество времени. На первый взгляд, это делает малый размер настолько относительным, что он бесполезен, но на практике ограниченный диапазон аппаратных возможностей значительно сужает область применения до почти объективных показателей. Например, на момент написания этой статьи я советую инженерам ограничить общий размер данных одного экземпляра MySQL 2 или 4 Тбайт: 2 Тбайт Для средних запросов и паттернов доступа для приемлемой производительности достаточно стандартного оборудования, а операции выполняются в разумные сроки. 4 Тбайт Для исключительно оптимизированных запросов и паттернов доступа для приемлемой производительности достаточно оборудования среднего и высокого класса, но операции могут занять немного больше времени, чем допустимо. Эти пределы отражают только аппаратную мощность, которую вы можете легко приобрести сегодня (декабрь 2021 года). Несколько лет назад эти пределы были
Шардирование | 175 значительно ниже. (Помните, когда диски физически вращались, издавая потрескивание? Жуть). Через несколько лет пределы будут значительно дальше. Когда база данных шардирована, для приложения не так важно количество шардов, поскольку оно обращается к ним программно. Но для операций — особенно для инженеров, работающих с экземплярами MySQL, — критически важен размер шардов: управлять базой данных объемом 500 Гбайт значительно проще, чем базой данных объемом 7 Тбайт. А поскольку операции автоматизированы, легко управлять любым количеством небольших баз данных. Производительность MySQL действительно неограниченна, когда MySQL шардирована и работает как много небольших баз данных — галька, а не валуны. Шардирование: краткое введение Стратегия и реализация шардирования обязательно должны быть связаны с рабочей нагрузкой приложения. Это справедливо даже для альтернативных решений, представленных в следующем разделе «Альтернативы». Следовательно, никто не может сказать вам, как шардировать, и полностью автоматизированных решений не существует. Приготовьтесь к долгому, но сто́ящему путешествию. У шардирования два пути от идеи к внедрению: Разработка нового приложения под шардирование Первый и самый редкий путь — это когда приложение с самого начала рассчитано на шардирование. Если вы разрабатываете новое приложение, я настоятельно рекомендую выбрать этот вариант, если это необходимо, потому что шардировать с самого начала несравнимо проще, чем переносить позже. Чтобы определить, требуется ли шардирование, оцените объем данных и спрогнозируйте их рост в течение следующих четырех лет. Если предполагаемый объем данных за четыре года соответствует возможностям вашего оборудования сегодня, то шардирование может и не потребоваться. Я называю это четырехлетней пригодностью. Также попробуйте оценить четырехлетнюю пригодность для двух других аспектов рабочей нагрузки приложения: запросов и паттернов доступа. Их трудно оценить для нового приложения (и, вероятно, они изменятся), но у вас должны быть какие-то соображения, потому что это неотъемлемая часть разработки и внедрения приложения. Также подумайте, является ли набор данных ограниченным или неограниченным. Ограниченному набору данных присущи максимальный объем или медленный рост. Например, количество новых моделей смартфонов, выпускаемых каждый год, очень мало, и его рост по своей сути медленный: нет никаких оснований полагать, что производители когда-либо будут выпускать тысячи новых телефонов в год. Неограниченный набор данных не имеет внутренних ограничений. Например, изображения: люди могут публиковать неограниченное количество изображений. И, поскольку пропускная способность оборудования имеет свои пределы,
176 | Глава 5 приложения всегда должны определять и налагать внешние ограничения на неограниченные наборы данных. Никогда не позволяйте данным расти неограниченно. Неограниченный набор данных однозначно указывает на необходимость шардирования, если только старые данные часто не удаляются или не архивируются (см. гл. 3, раздел «Удаление или архивирование данных»). Миграция существующего приложения шардированием Второй и более распространенный путь — это миграция (перенос) существующей базы данных и приложения шардированием. Это значительно сложнее, отнимает много времени и сопряжено с риском, потому что к тому моменту, когда это требуется, база данных уже велика — MySQL тащит камень в гору. Планируйте с командой опытных разработчиков, что миграция займет год или больше. В этой книге я не могу рассказать о том, как перейти с единой базы данных на шардированную, потому что это индивидуальный процесс: он зависит от программного решения для шардирования и от рабочей нагрузки приложения. Но одно можно сказать наверняка: вы, вероятно, много раз будете копировать данные из исходной (единой) базы данных в новые шарды, потому что первоначальная миграция — это, по сути, первое решардирование — непростая задача, рассмотренная в разделе «Решардирование» (см. ниже). Четырехлетняя пригодность Зачем оценивать объем данных и их рост в течение следующих четырех лет? Один или два года — это слишком рано: для реализации такого большого проекта, как шардирование, требуется не менее года. Три года — это разумно, а четыре года — безопасно: более качественное оборудование — это безопасная ставка на четыре года. Кроме того, фондовые гранты также обычно выдаются на четыре года, что приводит к текучести кадров. Прочитайте «Tours of Duty: The New Employer-Employee Compact» Рейда Хоффмана, Бена Каснохи и Криса Йе («Служебные командировки: новый договор между работодателем и работником», Reid Hoffman, Ben Casnocha, and Chris Yeh, https://oreil.ly/goj9j). Ответственный инженер совершенствует систему для будущих инженеров. Если база данных не будет масштабироваться после четырехлетнего срока службы, надо исправить это сейчас, чтобы гарантировать, что они унаследуют масштабируемую систему. Шардирование — сложный процесс при любом раскладе. Для начала выберите ключ шардирования и стратегию и поймите, с какими проблемами вы столкнетесь. Это знание дает путешествию цель: шардированная база данных, с которой относительной легко работать. Затем наметьте маршрут от единой базы данных до этого места назначения. Ключ шардирования Чтобы шардировать MySQL, приложение должно программно сопоставлять данные с шардами (отображение данных). Следовательно, наиболее существенным решением является ключ шардирования: столбец (или столбцы), по которому шардируются данные. Ключ шардирования используется со стратегией шардирования (смотрите в следующем разделе) для сопоставления данных с шардами. Приложе-
Шардирование | 177 ние, а не MySQL, отвечает за сопоставление и доступ к ним с помощью ключа шардирования, потому что MySQL не имеет встроенной концепции шардирования — MySQL игнорирует имеющиеся шарды. Термин «шард» (англ. shard) используется взаимозаменяемо для базы данных или экземпляра MySQL, в котором хранится база данных. Идеальный ключ шардирования обладает тремя свойствами: Высокая кардинальность Идеальный ключ шардирования имеет высокую кардинальность (см. гл. 2, раздел «Экстремальная селективность»), так что данные равномерно распределяются по шардам. Отличным примером является веб-сайт, который позволяет вам смотреть видео: он может присвоить каждому видео уникальный идентификатор — скажем, dQw4w9WgXcQ. Столбец, в котором хранится этот идентификатор, представляет собой идеальный ключ шардирования, поскольку каждое значение уникально, и поэтому кардинальность максимальна. Ссылочные объекты приложения Идеальный ключ шардирования ссылается на объекты приложения, чтобы избежать случая, когда паттерны доступа обращаются сразу к нескольким шардам. Отличным примером является приложение, которое хранит платежи: хотя каждый платеж уникален (максимальная кардинальность), объектом приложения является клиент. Таким образом, основной паттерн доступа для приложения — клиент, а не платеж. Шардировать по клиентам идеально, поскольку все платежи для одного клиента должны располагаться в одном шарде. Маленький размер Идеальный ключ шардирования должен быть как можно меньше, поскольку он часто используется: большинство запросов, если не все, включают ключ шардирования, чтобы избежать разбросанности запросов — одной из проблем, о которой речь пойдет ниже в этой главе. Это само собой разумееется, но на всякий случай озвучим: идеальный ключ шардирования в сочетании со стратегией шардирования позволяет избежать или смягчить проблемы, изложенные ниже в разделе «Проблемы», особенно по части транзакции и соединения таблиц. Потратьте достаточно времени, чтобы определить или создать идеальный ключ шардирования для вашего приложения. Это решение — половина основы: другая половина — стратегия шардирования, использующая этот ключ. Стратегии Стратегия шардирования сопоставляет данные с шардами по значению ключа шардирования. Приложение реализует стратегию шардирования для маршрутизации запросов к шарду с данными, соответствующими значению ключа шардирования.
178 | Глава 5 Это решение — вторая половина фундамента. Как только ключ и стратегия шардирования внедрены, их чрезвычайно трудно изменить, поэтому выбирайте очень тщательно. Существуют три общие стратегии: хеш, диапазон и поиск (или каталог). Все три широко используются. Наилучший выбор зависит от паттернов доступа к приложениям, особенно от доступа к строкам (см. гл. 4, раздел «Доступ к строкам»), как описано в следующих трех разделах. Хеш Хеш-шардирование сопоставляет значения хеш-ключа с шардами с использованием алгоритма хеширования (для получения целочисленного хеш-значения), оператора по модулю (mod) и количества шардов (N). На рис. 5.7 показана эта стратегия, начиная со значения хеш-ключа вверху и следуя за сплошными стрелками к шарду внизу. Алгоритм хеширования выводит хеш-значение, используя значение ключа шардирования в качестве входных данных. Формула «Хеш-значение (которое является Рис. 5.7. Хеш-шардирование
Шардирование | 179 целым числом) mod количество шардов (N)» возвращает номер шарда: целое число от нуля до N – 1 включительно. На рис. 5.7 хеш-значение 75482, подставляем в формулу: 75482 mod 3 = 2, поэтому данные для значения ключа шардирования расположены на шарде 2. Как сопоставить номера шардов с экземплярами MySQL — это ваш выбор. Например, вы могли бы развернуть сопоставление номеров шардов с именами хостов MySQL для каждого экземпляра приложения. Или приложения могут запрашивать такую службу, как etcd (https://etcd.io), чтобы узнать, как номера шардов сопоставляются с экземплярами MySQL. Если вы думаете: «Не повлияет ли изменение количества шардов (N) на распределение данных по шардам?» — вы правы. Например, 75483 mod 3 = 0, но увеличьте количество шардов до пяти, и то же значение ключа шардирования сопоставится с новым номером шарда: 75483 mod 5 = 3. К счастью, это решаемая проблема: согласованный алгоритм хеширования выдает согласованное хеш-значение, независимое от N. Ключевое слово — согласованность: все еще возможно, но гораздо менее вероятно, что значения хеша изменятся при изменении шардов. Поскольку шарды, весьма вероятно, меняться будут, согласованный алгоритм хеширования — предпочтительный выбор. Хеш-шардирование работает для всех ключей шардирования, поскольку абстрагирует значение до целого числа. Это не значит, что оно лучше или быстрее, просто это легче, потому что алгоритм хеширования автоматически распределяет все значения ключей шардирования. Однако автоматичность также является его недостатком, поскольку, как изложено ниже в разделе «Ребалансировка», переместить данные вручную практически невозможно. Точечный доступ (см. гл. 4, раздел «Доступ к строкам») хорошо работает с шардированием хеша, поскольку одна строка может соотноситься только с одним шардом. Напротив, доступ к диапазону, скорее всего, невозможен при шардировании хеша, если только диапазоны не очень малы, из-за кросс-шард запросов (одна из распространенных проблем, см. ниже соответствующий раздел). Произвольный доступ, вероятно, будет тоже невозможен по той же причине. Диапазон Шардирование на основе диапазона определяет смежные диапазоны значений ключа и сопоставляет шард каждому, как показано на рис. 5.8. Вы должны заранее определить диапазоны значений ключей. Это дает вам гибкость при сопоставлении данных с шардами, но для обеспечения равномерного распределения данных по шардам требуется глубокое знание распределения данных. Поскольку распределение данных меняется, ожидайте, что придется иметь дело с переразделением (см. ниже, раздел «Решардирование»). Преимущество шардирования на основе диапазона заключается в том, что, в отличие от сегментирования хеша, вы можете изменять (переопределять) диапазоны, что помогает перемещать данные вручную.
180 | Глава 5 Рис. 5.8. Шардирование на основе диапазона Все данные могут быть отсортированы и разделены на диапазоны, но для некоторых, таких как случайные идентификаторы, это не имеет смысла. И есть данные, которые кажутся случайными, но при ближайшем рассмотрении вы видите, что они строго упорядочены. Например, вот три UUID, сгенерированных MySQL: f15e7e66-b972-11ab-bc5a-62c7db17db19 f1e382fa-b972-11ab-bc5a-62c7db17db19 f25f1dfc-b972-11ab-bc5a-62c7db17db19 Замечаете ли вы различия? Эти три UUID кажутся случайными, но, скорее всего, будут отсортированы в одном и том же диапазоне, в зависимости от его размера. При масштабировании это привело бы к сопоставлению большинства данных с одним и тем же шардом, тем самым сводя на нет цель шардирования. (Алгоритмы UUID различаются: некоторые намеренно генерируют строго упорядоченные значения, в то время как другие намеренно генерируют значения в случайном порядке.) Шардирование на основе диапазона лучше всего работает, когда:  диапазон значений ключа шардирования ограничен;  можно определить диапазон (минимальные и максимальные значения);  известно распределение значений, и оно в основном равномерное;  диапазон и распределение вряд ли изменятся. Например, данные о запасах могут быть разделены символами акций в диапазоне от AAAA до ZZZZ. Хотя распределение, вероятно, будет меньше в диапазоне Z, в целом этого даже достаточно, чтобы гарантировать, что один шард не будет значительно больше других или что доступ к нему не будет осуществляться чаще, чем к прочим. Точечный доступ (см. гл. 4, раздел «Доступ к строкам») хорошо работает с шардированием диапазона, если доступ к строкам распределяется по диапазонам равномерно, избегая горячих шардов, — распространенная проблема, изложенная ниже в разделе «Ребалансировка». Доступ к диапазону хорошо работает с шардированием диапазона, пока диапазоны строк находятся в пределах сегментных диапазонов;
Шардирование | 181 в противном случае возникает проблема кросс-шард запросов (см. об этом ниже). Произвольный доступ трудноосуществим по той же причине: кросс-шард запросы. Поиск Шардирование поиска (или каталога) — это пользовательское сопоставление значений ключей шардирования и шардов. На рис. 5.9 изображена таблица поиска, которая сопоставляет домены верхнего уровня (с кодами стран) и шарды. Рис. 5.9. Шардирование поиска (каталога) Шардирование поиска — наиболее гибкий вариант, но оно требует ведения таблицы поиска. Таблица поиска функционирует как карта (ассоциативный массив) ключ-значение: значения ключей шардирования — это ключи, а сегменты базы данных — это значения. Вы можете создать таблицу поиска в виде таблицы базы данных, структуры данных в долговечном кеше, файла конфигурации, развернутого вместе с приложением, и т. д. Ключами в таблице поиска могут быть единичные значения (как показано на рис. 5.9) или диапазоны. Если ключи являются диапазонами, то это, по сути, шардирование диапазона, однако таблица поиска дает вам больше контроля над диапазонами. Но этот контроль имеет свои издержки: изменение диапазонов означает решардирование — одну из распространенных проблем. Если ключи представляют собой единичные значения, то шардирование поиска имеет смысл, когда количество уникальных значений ключа шардирования поддается управлению. Например, веб-сайт, на котором хранится статистика общественного здравоохранения в Соединенных Штатах, может быть шардирован по названиям штатов и округов, поскольку всего насчитывается менее 3500 округов, и они почти никогда не меняются1. Шардирование поиска имеет преимущество, которое делает его хорошим выбором для этого примера: можно легко определить все округа с низкой численностью населения в один сегмент, в то время как это частное 1 Названия округов уникальны только в пределах штата, поэтому название штата обязательно.
182 | Глава 5 решение будет невозможно при хеш-шардировании или шардировании на основе диапазона. Все три паттерна доступа к строкам (см. гл. 4, раздел «Доступ к строкам») работают с шардированием поиска, но насколько хорошо — зависит от размера и сложности таблицы поиска, которую вам необходимо создать и поддерживать, чтобы сопоставить значения ключей шардирования с шардам базы данных. Следует отметить: шардирование поиска позволяет сопоставлять (или переназначать) значения ключей шардирования, чтобы разобраться с кросс-шард запросами, вызванными произвольным доступом — что, опять же, практически невозможно при хешшардировании и шардировании на основе диапазона. Проблемы Если бы шардирование было идеальным, вы бы шардировали только один раз, и каждый шард имел бы одинаковый объем данных и доступ. Это может иметь место при первом шардировании, но так не останется. Следующие проблемы повлияют на ваше приложение и шардированную базу данных, поэтому планируйте заранее, как их избежать или смягчить. Транзакции Транзакции не работают между шардами. Это скорее препятствие, чем просто проблема, потому что, по сути, нет обходного пути, кроме внедрения двухфазной фиксации в приложении, что опасно и выходит далеко за рамки этой книги. Я настоятельно рекомендую вам избегать этого препятствия. Просмотрите транзакции вашего приложения (см. гл. 8, раздел «Отчетность» ) и данные, к которым они обращаются. Затем выберите ключ шардирования и стратегию, которые работают с учетом того, как транзакции получают доступ к данным. Объединения Инструкция SQL не может соединять таблицы между шардами. Решение — кроссшард объединение: приложение объединяет результаты множественных запросов, выполняемых на нескольких шардах. Это не тривиальное решение, оно может быть даже сложным в зависимости от соединения, но оно выполнимо. Помимо сложности, основной проблемой является согласованность: поскольку транзакции не работают между шардами, результаты каждого шарда не являются согласованным представлением всех данных. Кросс-шард объединение — кросс-шард запрос специального назначения (объединение результатов и есть специальное назначение); поэтому оно подвержено тем же проблемам. Кросс-шард запросы Кросс-шард запрос требует, чтобы приложение обращалось к нескольким шардам. Этот термин относится к доступу к приложению, а не к литеральным запросам, по-
Шардирование | 183 скольку один запрос не может выполняться более чем в одном экземпляре MySQL. (Более точным термином был бы «кросс-шард доступ к приложению»). Кросс-шард запросы сопряжены с задержкой: задержка, присущая доступу к нескольким экземплярам MySQL. Шардирование наиболее эффективно, когда кроссшард запросы — исключение, а не норма. Если бы шардирование было идеальным, каждый запрос приложения получал бы доступ только к одному шарду. Это цель, но не сходите с ума, пытаясь достичь ее, ведь некоторые приложения, даже при эффективном шардировании, должны получать доступ к нескольким шардам для выполнения определенных запросов. Хорошим примером служит одноранговое платежное приложение. Каждый клиент представляет собой четко очерченный объект приложения: все данные, относящиеся к клиенту, должны располагаться в одном шарде, т. е. данные шардируются по каждому пользователю. Но клиенты взаимодействуют, отправляя и получая деньги, и приложение неизбежно получит доступ по крайней мере к двум шардам: один для того, кто отправляет деньги, и другой для того, кто их получает. Кросс-шард запросы должны быть сведены к минимуму, но опять же: не сходите с ума, пытаясь устранить их, особенно если логика приложения требует их для определенных задач. С этим связана и такая проблема, как запросы с разбросом (или запросы с рассеиванием): запросы, требующие от приложения доступа ко многим (или ко всем) шардам. (Опять же, этот термин относится к доступу приложения, а не к литеральным запросам.) Умеренное количество кросс-шард запросов неизбежно и приемлемо, но разбросанные запросы противоречат цели и преимуществам шардирования. Поэтому вы должны как предотвращать, так и устранять это. Если вы не можете, если приложению требуются такие запросы, то шардирование, вероятно, здесь не подходит, или необходимо изменить паттерн доступа (см. гл. 4, раздел «Паттерны доступа к данным»). Решардирование Решардирование (или разделение шардов) делит один шард на два или более новых. Решардирование необходимо для учета роста объема данных, а также может использоваться для перераспределения данных между шардами. Если и когда это потребуется, зависит от планирования емкости: предполагаемой скорости роста данных и количества шардов, создаваемых изначально. Например, я видел, как команда разделила базу данных на четыре шарда, а затем сделала решардирование менее чем через два года, потому что размер данных вырос намного быстрее, чем предполагалось. И наоборот, я видел, как команда разделила базу данных на 64 шарда с учетом прогноза роста объема данных более чем на пять лет. Если вы можете позволить себе дополнительные шарды в начале (при первом шардировании), то создайте достаточно шардов, по крайней мере, на четыре года роста — не слишком переоценивая, но оценивая щедро. В этом и заключается мрачный секрет шардирования: шарды порождают новые шарды. Если вам интересно: «Могу ли я шардировать один раз и все?», ответ будет
184 | Глава 5 «Вероятно, нет». Поскольку ваша база данных выросла до такой степени, что ее необходимо шардировать, она, скорее всего, будет продолжать расти и нуждаться в большем количестве шардов — если только вы не станете горячим поклонником идеи «Чем меньше данных, тем лучше» (см. в гл. 3 соответствующий раздел). Решардирование является сложной задачей, поскольку для этого требуется перенос данных из старого шарда в новые. Описание того, как переносить данные, выходит за рамки этой книги, но я укажу на три требования высокого уровня: 1. Первоначальное массовое копирование данных из старых шардов в новые. 2. Синхронизация изменений в старом шарде с новыми шардами (во время и после копирования данных). 3. Процесс отключения для переключения на новые шарды. Для безопасного и правильного переноса данных требуется высокий уровень экспертности в MySQL. Поскольку миграция данных зависит от конкретного приложения и инфраструктуры, вы не найдете никаких книг или других ресурсов, подробно описывающих это. При необходимости наймите консультанта по MySQL, который поможет разработать процесс. Также ознакомьтесь с Ghostferry (https:// oreil.ly/7aM3I) от инженеров Shopify, экспертов в шардировании MySQL. Ребалансировка Ребалансировка перемещает данные, чтобы распределить доступ более равномерно. Ребалансировка необходима для обработки горячих шардов: шардов, к которым запросы обращаются гораздо чаще, чем к другим. Хотя характер распределения данных определяется ключом шардирования и стратегией шардирования, как осуществляется доступ к данным — зависит от приложения и его пользователей. Если все наиболее часто используемые данные содержат один шард (горячий), то производительность распределяется неравномерно, что сводит на нет цель масштабирования: равный доступ и равная производительность для всех шардов. Ребалансировка зависит от стратегии шардирования: Хеш Переместить данные с помощью хеш-шардирования практически невозможно, поскольку алгоритм хеширования распределяет данные по шардам автоматически. Одним из решений (или обходным путем) является использование таблицы поиска, содержащей перемещенные ключи шардирования. Приложение сначала проверяет таблицу поиска: если присутствует ключ шардирования, оно использует шард, указанный таблицей поиска; в противном случае оно использует алгоритм хеширования. Диапазон Возможный, но нетривиальный способ перемещения данных — с шардированием на основе диапазона: переопределение диапазонов, чтобы разделить горячий шард на более мелкие отдельные шарды. Тот же процесс, что и решардирование.
Шардирование | 185 Поиск Перемещение данных с помощью шардирования поиска относительно просто, поскольку вы контролируете распределение данных по шардам. Поэтому вы обновляете таблицу поиска, чтобы переопределить значение ключа шардирования, соответствующее горячим данным. Для физического перемещения горячих данных требуется тот же (или аналогичный) процесс переноса данных, который используется для решардирования. Онлайн-изменения схемы Изменить таблицу в одной базе данных легко, но как это сделать в каждом шарде? Запустить OSC для каждого шарда — немного не то: нужно автоматизировать процесс запуска OSC на нескольких шардах и отслеживать, какие шарды меняются. Для MySQL на момент написания этой статьи не существует решений с открытым исходным кодом; вам придется самим разработать его. (Тем не менее у пары альтернатив MySQL в следующем разделе решение есть.) Это наименее сложная задача в шардировании, но все-таки сложная. Нельзя упускать это из виду, потому что изменения схемы — обычное дело. Альтернативы Шардирование — сложный процесс, и он не имеет непосредственной ценности для пользователей или клиентов. Для приложения важно продолжать масштабирование, но для инженеров это тяжелый труд. Неудивительно, что альтернативные решения становятся все более популярными и эффективными. Однако не спешите слишком быстро доверять свои данные новым технологиям. MySQL в высшей степени надежна и глубоко изучена — очень зрелая технология, что делает ее безопасным и разумным выбором. NewSQL NewSQL относится к реляционному хранилищу данных, ACID-совместимому, со встроенной поддержкой масштабирования. Другими словами, это база данных SQL, которую вам не нужно шардировать. Если вы думаете: «Ничего себе! Тогда зачем вообще использовать MySQL?» — следующие пять пунктов объясняют, почему MySQL — шардированная или нет — по-прежнему остается самой популярной базой данных с открытым исходным кодом в мире: Зрелость SQL родом из 1970-х годов, а MySQL — из 1990-х. Зрелость базы данных означает две вещи: вы можете быть уверены, что хранилище данных не потеряет и не повредит ваши данные, и есть глубокие знания о каждом аспекте хранилища данных. Обратите пристальное внимание на зрелость хранилищ данных NewSQL: когда вышел первый по-настоящему стабильный общедоступный выпуск? Какова была частота и качество выпусков с тех пор? Какие глубокие и авторитетные знания были обнародованы?
186 | Глава 5 Совместимость с SQL Хранилища данных NewSQL используют SQL (в конце концов, это указано в названии), но совместимость значительно различается. Не ожидайте, что какоелибо хранилище данных NewSQL станет полноценной заменой MySQL. Сложные операции Встроенная поддержка масштабирования достигается с помощью распределенной системы. Это обычно влечет за собой множество различных, но скоординированных компонентов. (Если MySQL — это сольный саксофонист, то NewSQL — это группа из пяти человек). Если хранилище данных NewSQL полностью управляется кем-то другим, то, возможно, его сложность не имеет значения. Но если вам нужно самостоятельно управлять им, то ознакомьтесь с его документацией, чтобы понять, как оно работает. Производительность распределенной системы Вспомним универсальный закон масштабируемости (уравнение 4.1): X (N ) = γN . 1 + α ( N − 1) + βN ( N − 1) представляет нагрузку на программное обеспечение (одновременные запросы, запущенные процессы и т. д.), или аппаратные процессоры, или узлы в распределенной системе. Если приложение имеет запросы, требующие времени отклика менее 10 миллисекунд, хранилище данных NewSQL может не работать из-за задержки, присущей распределенным системам. Но этот уровень времени отклика — не самая большая проблема, которую решает NewSQL, чаще бывает другая: как реализовать встроенное масштабирование до большого размера данных (относительно одного экземпляра) с разумным временем отклика (например, 75 мс). N Эксплуатационные характеристики Что определяет время отклика (производительность) запроса? Для MySQL высокоуровневыми компонентами являются индексы, данные, паттерны доступа и аппаратное обеспечение — все, что описано в предыдущих четырех главах. Добавьте к этому компоненты более низкого уровня — например, «Требование крайнего левого префикса» (см. гл. 2), «Размер рабочего набора» (см. гл. 3) и «MySQL ничего не делает» (см. гл. 4) — и вы поймете, как устроена производительность MySQL и как ее улучшить. Хранилище данных NewSQL будет иметь новые, другие характеристики производительности. Например, индексы всегда обеспечивают основную и наилучшую эффективность, но они могут работать иначе для NewSQL из-за особенностей хранения данных и доступа к ним в распределенной системе. Аналогично некоторые паттерны доступа, которые хороши в MySQL, плохи в NewSQL, и наоборот. Эти пять пунктов — предупреждение: NewSQL — многообещающая технология, которую следует изучить в качестве альтернативы шардированию MySQL, но NewSQL не является полноценной заменой MySQL.
Шардирование | 187 На момент написания этой книги существует только два жизнеспособных решения NewSQL с открытым исходным кодом, совместимых с MySQL: TiDB (https:// oreil.ly/GSCc0) и CockroachDB (https://oreil.ly/wKZ2Z). Оба они исключительно свежие: общедоступный релиз CockroachDB v1.0 вышел 10 мая 2017 года, а общедоступный релиз TiDB v1.0 выпущен 16 октября 2017 года. Поэтому будьте внимательны и осторожны, используя TiDB и CockroachDB, по крайней мере, до 2027 года — даже MySQL было 10 лет к тому времени, когда она стала мейнстримом в начале 2000-х. Если вы применяете этот софт, пожалуйста, напишите о том, что узнали, и, если возможно, внесите свой вклад в эти проекты. Промежуточное программное обеспечение Промежуточное программное решение работает между приложением и шардами MySQL. Оно пытается скрыть или абстрагировать детали шардирования или, по крайней мере, упростить шардирование. Когда шардирование вручную, напрямую — слишком сложно, а NewSQL не выход, может помочь промежуточное программное решение. Два ведущих решения с открытым исходным кодом — это Vitess (https://oreil.ly/6AvRY) и ProxySQL (https://oreil.ly/5iTkH), и они совершенно разные. ProxySQL может шардировать, а Vitess — шардирует. ProxySQL, как следует из его названия, представляет собой прокси-сервер, который поддерживает шардирование с помощью нескольких механизмов. Чтобы получить представление о том, как это работает, прочитайте «Sharding in ProxySQL» («Шардирование в ProxySQL», https://oreil.ly/N0eYa) и «MySQL Sharding with ProxySQL» («Шардирование MySQL с помощью ProxySQL», https://oreil.ly/KDvjE). Использование прокси-сервера в сравнении с MySQL — это как классический Vim против Emacs, без сарказма: инженеры проделывают большую работу на обоих редакторах; это лишь вопрос личных предпочтений. Точно так же компании добиваются успеха как с представителем, так и без него; вопрос предпочтений. Vitess — специально созданное решение для сегментирования MySQL. Поскольку шардирование является сложным процессом, Vitess не лишен собственной сложности, но его самое большое преимущество заключается в том, что он решает все проблемы, особенно перераспределение и ребалансировку. Более того, Vitess был создан экспертами MySQL в YouTube, которые глубоко разбираются в MySQL в массовом масштабе. Обязательно оцените ProxySQL и Vitess перед шардированием. Любое решение с промежуточным программным обеспечением требует дополнительной инфраструктуры для изучения и обслуживания, но преимущества могут быть важнее затрат, поскольку шардирование MySQL вручную также требует значительного времени, усилий и душевного равновесия. Микросервисы Шардирование фокусируется на одном приложении (или службе) и его данных, особенно на размере данных и доступе к ним. Но иногда настоящая проблема заключается в приложении: у него слишком много данных или доступа, потому что
188 | Глава 5 оно служит слишком многим целям или бизнес-функциям. Отказ от монолитных приложений — стандартный инженерный подход и практика, но это не значит, что так всегда и делается. Перед шардированием просмотрите, как разработано приложение и какие у него данные, чтобы проверить, нельзя ли разобраться с частями через отдельные микросервисы. Это намного проще, чем шардирование, потому что новый микросервис и его база данных полностью независимы — стратегия или ключ шардирования не требуются. Также может получиться, что у нового микросервиса совершенно другие паттерны доступа (см. гл. 4, раздел «Паттерны доступа к данным»), которые позволяют ему использовать меньше оборудования при хранении большего количества данных — или, возможно, ему не требуется реляционное хранилище данных. Не используйте MySQL Как и раздел «Не используйте MySQL» (см. гл. 4), честная оценка альтернатив шардированию MySQL должна завершаться словами: не используйте MySQL, если другое хранилище данных или технология работают лучше. Если ваш путь заключается в разработке нового приложения под шардирование, то обязательно оцените другие решения. Шардирование MySQL — это решаемая задача, но здесь не бывает простых или быстрых вариантов. Если ваш путь заключается в миграции существующего приложения шардированием, то вам все равно следует рассмотреть компромиссы между шардированием MySQL и переходом на другое решение. Это звучит обременительно в масштабе — и так оно и есть, но компании делают это постоянно, и вы тоже можете. Итоги В этой главе представлена базовая механика шардирования MySQL для достижения масштабируемости MySQL. Основные моменты:  MySQL масштабируется горизонтально за счет шардирования.  Шардирование разделяет одну базу данных на множество баз данных.  Единая база данных не масштабируется в первую очередь потому, что комбина- ция запросов, данных и паттернов доступа — рабочая нагрузка приложения — значительно превышает скорость и пропускную способность оборудования с одним сервером.  Значительно проще управлять множеством небольших баз данных (шардов), чем одной огромной базой данных — галькой, а не валунами.  Данные шардируются (разделяются) с помощью ключа шардирования, который вы должны тщательно выбирать.  Ключ шардирования используется со стратегией шардирования для распределе- ния данных (по ключу шардирования) по шардам.  Наиболее распространенными стратегиями шард являются хеш (алгоритм хеши- рования), диапазон и поиск (каталог).
Шардирование | 189  В шардировании есть несколько проблем, которые необходимо решить.  Существуют альтернативы шардированию, которые вам стоит оценить. В следующей главе рассматриваются показатели сервера MySQL. Практика: четырехлетняя пригодность Цель этой практики — определить соответствие размера данных возможностям оборудования на четыре года. Как было сказано в разделе «Шардирование: краткое введение» в начале этой главы, речь об оценке объема данных или доступа на четыре года применительно к емкости вашего оборудования на сегодняшний день. Шардирование может не потребоваться, если и через четыре года вашего оборудования будет достаточно для стабильной работы с нужными данными (см. в начале главы раздел «Рабочая нагрузка приложения»). Для выполнения этой практики вам понадобятся исторические размеры данных. Если вы еще не измеряете данные и не записываете эти показания, перейдите к разделу «Объем данных» в главе 6, чтобы узнать, как это сделать. Достаточно самого простого из возможных вычислений. Если, например, база данных исторически увеличивалась на 10 Гбайт каждый месяц, то база данных вырастет так: 12 месяцев × 4 года × 10 Гбайт/месяц = на 480 Гбайт больше за четыре года — если данные не удаляются или не архивируются (см. гл. 3, раздел «Удаление или архивирование данных»). Если сегодня объем базы данных составляет 100 Гбайт, то через четыре года подойдет к 580 Гбайт: вам не нужно шардировать в ближайшее время (несмотря на четырехлетнюю нагрузку на доступ), потому что MySQL на современном оборудовании может легко обрабатывать 580 Гбайт данных. Если расчет четырехлетней пригодности показывает, что вам, возможно, потребуется шардирование, отнеситесь к этому серьезно и погрузитесь глубже, чтобы точно определить: действительно ли база данных неуклонно движется к тому, чтобы стать слишком большой для одного экземпляра MySQL? Если да, то шардируйте как можно раньше, потому что шардирование — по сути, сложный процесс переноса данных; следовательно, чем меньше данных, тем проще процесс. Если нет, поздравляю: обеспечивать, чтобы система продолжала масштабироваться годами, — экспертная практика во всех областях инженерии.
190 | Глава 5
ГЛАВА 6 Метрики сервера Метрики MySQL тесно связаны с производительностью MySQL — это очевидно. В конце концов, цель метрик в любой системе — это измерить и сообщить, как работает система. Что не очевидно — как они связаны. Вполне понятно, если в настоящее время вы представляете себе метрики MySQL так, как на рис. 6.1: MySQL — это черный ящик с метриками внутри, которые каким-то образом показывают что-то про MySQL. Рис. 6.1. MySQL как черный ящик: метрики ни о чем не говорят Рис. 6.2. MySQL как призма: метрики раскрывают производительность рабочей нагрузки Этот взгляд не лишен причин (и вполне обычен), потому что метрики MySQL часто обсуждаются, но им никогда не обучают. Даже за время моей MySQL-карьеры я никогда не читал и не слышал подробного расклада по метрикам MySQL, а я работал с людьми, которые их создавали. Отсутствие обучения метрикам MySQL связано с ложным предположением, что они не требуют особого понимания или интерпретации, поскольку их значение самоочевидно. Это похоже на истину, если рассматривать отдельную метрику изолированно — например, Threads_running. Ведь это число выполняющихся потоков, а что еще нужно знать? Но рассматривать метрики в изоляции — это ошибка: производительность MySQL раскрывается с помощью спектра метрик MySQL.Представьте MySQL как призму. Образно говоря, приложение светит лучом рабочей нагрузки в MySQL. Эта рабочая нагрузка физически взаимодействует с MySQL и оборудованием, на котором она выполняется. Метрики — это спектр, получающийся во время образного преломления рабочей нагрузки через MySQL, как показано на рис. 6.2. В физических науках этот метод называется спектрометрией: понимание материи через ее взаимодействие со светом. Для MySQL это больше, чем умная аналогия: это фактическая взаимосвязь между метриками MySQL и производительностью сервера MySQL, и тому есть два доказательства:
192 | Глава 6  Когда вы пропускаете свет через настоящую призму, цветовой спектр, который получается в результате, раскрывает свойства света, а не призмы. Аналогично, когда вы выполняете рабочую нагрузку на MySQL, результирующие метрики раскрывают свойства рабочей нагрузки, а не MySQL.  Вспомните предыдущие главы (особенно раздел «MySQL ничего не делает» в главе 4) — производительность напрямую связана с рабочей нагрузкой: запросами, данными и паттернами доступа. По большому счету без рабочей нагрузки все значения метрик равны нулю. С этой точки зрения метрики MySQL можно преподавать по-новому, чему и посвящена данная глава. Эта аналогия имеет еще одну ценность с точки зрения преподавания: она разделяет метрики MySQL не просто на спектр, а на спектры (во множественном числе). Это очень полезно, потому что метрики MySQL обширны и бессистемны (несколько сотен метрик разбросаны по всей MySQL), а эффективное обучение требует сосредоточенности и организации. Таким образом, раздел «Спектры», в котором освещается более 70 метрик, разделенных на 11 спектров, составляет основную часть этой главы. Последнее замечание, прежде чем мы прольем свет на MySQL: только часть метрик важна для понимания и анализа производительности сервера MySQL. Актуальность и важность остальных метрик сильно различаются:  некоторые из них — всего лишь шум;  некоторые из них — исторические;  некоторые из них отключены по умолчанию;  некоторые из них очень специфичны с технической точки зрения;  некоторые полезны только в определенных случаях;  некоторые носят информативный характер, а не являются нормальными метри- ками;  некоторые из них непостижимы для простых смертных. В этой главе анализируются спектры метрик MySQL, которые необходимы для понимания того, как рабочая нагрузка взаимодействует с производительностью сервера MySQL и влияет на нее. Здесь шесть основных разделов. В первом проводится различие между производительностью запросов и производительностью сервера. Предыдущие главы посвящены первому, а эта глава посвящена второму. Второй раздел скучный; вы сами поймете почему. В третьем перечислены ключевые показатели производительности (англ. key performance indicator, KPI), которые быстро оценивают производительность MySQL. Четвертый исследует область метрик: модель для более глубокого понимания того, как метрики описывают производительность сервера MySQL и связаны с ней. В пятом представлены спектры метрик MySQL: более 70 метрик, организованных в 11 спектров: эпическое и захватывающее путешествие, в котором рассказывается о внутренней работе MySQL, по за-
Метрики сервера | 193 вершении которого вы увидите MySQL с нового ракурса. В шестом рассматриваются важные темы, связанные с мониторингом и оповещением. Производительность запросов в сравнении с производительностью сервера Производительность MySQL имеет две стороны: производительность запросов и производительность сервера. В предыдущих главах рассматривалась производительность запросов: улучшение (сокращение) времени отклика за счет оптимизации рабочей нагрузки. В этой главе рассматривается производительность сервера: анализ производительности MySQL в зависимости от выполнения рабочей нагрузки. В этой главе производительность MySQL означает производительность сервера. Проще говоря, на вводе у нас рабочая нагрузка, а на выводе — производительность сервера, как показано на рис. 6.3. Если вы даете MySQL оптимизированную рабочую нагрузку, то получаете от MySQL высокую производительность. Производительность сервера почти всегда связана с рабочей нагрузкой, а не с MySQL. Почему? Да потому что MySQL отлично справляется с различными рабочими нагрузками. MySQL — это зрелое, высокооптимизированное хранилище данных, которое десятилетиями настраивалось экспертами мирового класса. Вот почему первые пять глав этой книги восхваляют производительность запросов, и только в одной главе (этой) анализируется производительность сервера. Рис. 6.3. Производительность запросов и сервера
194 | Глава 6 Есть три причины анализировать производительность сервера: Конкурентность и конфликт Конкурентность приводит к конфликту, который снижает производительность запросов. Запрос, выполняемый изолированно, демонстрирует иную производительность при выполнении с другими запросами. Вспомним универсальный закон масштабируемости из уравнения 4.1: конкурентность (α) находится в делителе уравнения, т. е. она снижает пропускную способность по мере увеличения нагрузки. Если вы не живете в какой-то другой вселенной, конкурентности и конфликта не избежать. Анализ производительности сервера наиболее полезен и чаще всего проводится, чтобы увидеть, как MySQL справляется с рабочей нагрузкой, когда все запросы (конкурентность) соперничают за общие и ограниченные системные ресурсы (конфликт). Некоторые рабочие нагрузки имеют очень мало конфликтов (если вообще имеют), в то время как другие убивают производительность — и запросов, и сервера, — несмотря на все усилия MySQL. Характеристика паттерна доступа «Конкурентность» (см. гл. 4), что неудивительно, является основным фактором конфликта, но все остальные свойства паттерна доступа также важны. Анализ производительности сервера показывает, насколько хорошо запросы в рабочей нагрузке взаимодействуют друг с другом Как программисты, ответственные за эти запросы, мы должны убедиться, что они работают хорошо. Тонкая настройка (тюнинг) Производительность сервера напрямую, но не полностью зависит от рабочей нагрузки. Существуют три дополнительных фактора: MySQL, операционная система и аппаратное обеспечение. При выполнении запросов предполагается, что все это правильно настроено и отвечает рабочей нагрузке. Несмотря на проблемы вроде неисправного оборудования и багов, эти три фактора влияют на производительность гораздо меньше, чем рабочая нагрузка, потому что мы живем в эпоху изобилия: наша MySQL очень зрелая и высокооптимизированная, наши операционные системы продвинутые и сложные, а наше оборудование мощное и доступное. Вещи, обсуждаемые в разделе «Тонкая настройка (тюнинг) MySQL» (см. гл. 2), по-прежнему актуальны: тюнинг MySQL сродни выжиманию воды из камня. Скорее всего, вам это никогда не понадобится. Но если вы будете это делать, вам потребуется проанализировать производительность сервера с известной и стабильной рабочей нагрузкой; иначе вы не можете быть уверены, что какойлибо прирост производительности является результатом тюнинга. Здесь применимы базовые научные принципы: средства контроля, переменные, воспроизводимость, опровержимость. Регрессия производительности Я восхваляю MySQL на протяжении этой книги, но буду честным — хотя бы раз придется упомянуть, что иногда MySQL ошибается. Однако MySQL не стала бы самой популярной в мире реляционной базой данных с открытым исходным ко-
Метрики сервера | 195 дом, если бы ошибалась постоянно. Обычно все работает как надо, и эксперты подозревают наличие регрессии производительности (или ошибки) в последнюю очередь — после того как проверят, что дело не в производительности запросов, тонкой настройке MySQL или неисправном оборудовании. Посты «Checkpointing in MySQL and MariaDB» («Контрольные точки в MySQL и MariaDB», https://oreil.ly/MuRIt) и «More on Checkpoints in InnoDB MySQL 8» («Подробнее о контрольных точках в InnoDB MySQL 8», https://oreil.ly/NDQkP) в блоге известного эксперта по MySQL Вадима Ткаченко содержат прекрасные примеры анализа производительности сервера для выявления регрессии производительности. Для Вадима нормально выполнять такую работу; остальные из нас решают гораздо более простые проблемы, такие как индексирование и вопрос о том, стоит ли пить третью чашку кофе перед обедом. Неисправная оптика Тюнинг и регрессия производительности являются исключением для аналогии, в которой мы представляли MySQL как призму (рис. 6.2), которая раскрывает только свойства рабочей нагрузки, а не MySQL. Известная и стабильная рабочая нагрузка аналогична прохождению чистого синего света через призму: если входные данные верны, искажения на выходе говорят нечто о самой призме. Эта глава посвящена конкурентности и конфликту, поскольку за них отвечают программисты, которые поддерживают приложение, выполняющее запросы. За тюнинг и регрессию производительности отвечают администраторы баз данных MySQL и эксперты. Изучение анализа производительности сервера для первого (конкурентность и конфликт) является отличной подготовкой ко второму, потому что разница в первую очередь зависит от фокуса внимания. Я надеюсь, что первое вызовет интерес ко второму, потому что индустрия MySQL нуждается в большем количестве администраторов баз данных и экспертов. Нормальная и стабильная: лучшая база данных — это скучная база данных По большей части эпитеты «нормальная» и «стабильная» становятся интуитивно понятны программистам, как только они знакомятся с приложением и тем, как его рабочая нагрузка выполняется на MySQL. Люди неплохо распознают образы, поэтому для нас легко заметить, когда графики для какой-либо метрики необычны. Так что я не буду вдаваться в терминологию, которая в целом хорошо понятна, но проясню пару моментов, чтобы убедиться, что мы друг друга понимаем, и рассмотрю редкие случаи, когда программисты спрашивают: «Нормально — это как?» в отношении производительности MySQL: Нормальная Каждое приложение, рабочая нагрузка, конфигурация MySQL и среда отличаются друг от друга. Таким образом, нормален тот уровень производительности,
196 | Глава 6 какой MySQL демонстрирует для вашего приложения в обычный день, когда все работает должным образом. Эта норма (ваша норма) и будет основой для определения того, выше или ниже какой-то аспект производительности, быстрее или медленнее, лучше или хуже, чем обычно. Все очень просто. Когда я указываю на предполагаемую норму типа «Threads_running должно быть меньше 50», это своего рода аббревиатура, сокращение от «Стабильное значение для Threads_running составляет менее 50, если верить моему опыту, принять во внимание факт, что у современного оборудования обычно менее 48 ядер процессора, и учесть: тесты показывают, что в настоящее время производительность MySQL значительно падает, если выполняется одновременно больше 64 потоков». Но если и 60 потоков работают нормально и стабильно для вашего приложения, тогда отлично: вы достигли исключительной производительности. Стабильная Не упускайте из виду стабильность производительности в своем стремлении к большей производительности. Раздел «Производительность дестабилизируется на пределе» (см. гл. 4) иллюстрирует и объясняет, почему выжимание максимальной производительности из MySQL — плохая цель: на пределе производительность дестабилизируется, и тогда возникают проблемы посерьезнее, чем сама производительность. Стабильность не ограничивает производительности; она гарантирует, что производительность (на любом уровне) устойчива, — и этого мы действительно хотим: MySQL работает быстро все время, а не иногда. Временами производительность MySQL поражает воображение (взлеты, падения, кричащие фанаты и переполненные стадионы), но настоящее искусство — это оптимизация базы данных до первозданной скуки: все запросы отвечают быстро, все метрики стабильны и нормальны, и все пользователи довольны. Ключевые показатели производительности Четыре показателя позволяют быстро оценить производительность MySQL: Response time (время отклика) Время отклика — неудивительная метрика: как отмечено в разделе «Краеугольный камень» (см. гл. 1), это единственный показатель, который кого-то действительно интересует. Даже если время отклика замечательное, вы должны учитывать другие ключевые показатели производительности. Например, если каждый запрос завершается ошибкой, время отклика может быть просто супер (близким к нулю), но это ненормально. Цель — нормальное и стабильное время отклика, и чем оно меньше, тем лучше. Error (ошибка) Эта метрика учитывает частоту ошибок. Каких ошибок? Как минимум ошибок запроса, но в идеале всех: запроса, соединения, клиента и сервера. Не ожидайте нулевой частоты ошибок, потому что, например, ни вы, ни приложение, ни MySQL ничего не сможете сделать, если клиент прервет соединение. Цель — нормальная и стабильная частота ошибок, и чем ниже (около нуля), тем лучше.
Метрики сервера | 197 QPS (количество запросов в секунду) Присутствие этой метрики также неудивительно: выполнение запросов — основное предназначение и работа MySQL. QPS — показатель производительности, но не то же самое, что производительность. Например, аномально высокий QPS может сигнализировать о проблемах. Целью является нормальный и стабильный QPS, а значение может быть произвольным. Threads running (выполняющиеся потоки) Эта метрика измеряет, насколько усердно MySQL работает над QPS. Один поток выполняет один запрос, поэтому вы должны учитывать обе метрики, поскольку они тесно связаны. Цель — нормальное и стабильное значение метрики выполнения потоков; чем меньше, тем лучше. Я подробно рассказываю об этих четырех метриках в разделе «Спектры» (см. ниже). Дело в том, что они — ключевые показатели производительности для MySQL: когда значения всех четырех являются нормальными, производительность MySQL практически гарантированно также будет нормальной. Всегда следите за временем отклика, ошибками, QPS и числом выполняющихся потоков. Вопрос о том, когда о них следует оповещать, обсуждается позже в этой главе (см. раздел «Оповещения о пользовательском опыте и объективных ограничениях»). Упрощение производительности сложной системы до нескольких метрик характерно не только для MySQL или компьютеров. Например, у вас есть жизненно важные метрики (я надеюсь): рост, вес, возраст, кровяное давление и частота сердечных сокращений. Пять биологических показателей кратко и точно оценивают ваше здоровье. Аналогичным образом четыре показателя MySQL кратко и точно оценивают производительность сервера. Весьма удобно, но что действительно откроет нам глаза, так это поле метрик, где расположены они все. Четыре золотых сигнала и остальные Ключевые показатели производительности — идея не новая. В 2016 году в книге «Site Reliability Engineering. Надежность и безотказность как в Google» («Site Reliability Engineering» от издательства O'Reilly) Бетси Бейер (Betsy Beyer) вместе с соавторами сделала основой программирования термин и концепцию золотых сигналов: задержка, трафик, ошибки и насыщение. Известный эксперт по производительности систем Брендан Грегг создал похожую методологию, «The USE Method» («Метод USE», https:// oreil.ly/mZ5SV), включающую сигналы об использовании, насыщении и ошибках. Том Уилки из Weaveworks создал другую методологию, «The RED Method: Key Metrics for Microservices Architecture» («RED метод: ключевые метрики для архитектуры микросервисов», https://oreil.ly/1fD6B), с сигналами о скорости, ошибках и продолжительности. Термины различаются, но концепция одна и та же. Поле метрик Каждая метрика MySQL принадлежит к одному из шести классов, показанных в виде блоков на рис. 6.4. В совокупности я называю это полем метрик. Производительность MySQL не может быть полностью понята путем анализа метрик изолированно, поскольку является результатом действия многих факторов, для
198 | Глава 6 которых существует множество взаимосвязанных метрик. Поле метрик — это модель, позволяющая увидеть, как метрики связаны. Связи между ними позволяют соединить точки, завершая сложную картину производительности MySQL. Рис. 6.4. Поле метрик Время отклика Метрики времени отклика показывают, сколько времени требуется MySQL для ответа. Это высший уровень поля, потому что они охватывают (или скрывают) детали с более низких уровней. Время отклика на запрос, конечно, важнейший показатель, и единственный, что постоянно контролируется. MySQL выполняет инструкции поэтапно, и этапы могут быть рассчитаны по времени. Это тоже метрики времени отклика, но относятся не к собственно выполнению запроса, а к тому, что его окружает. Фактическое выполнение запроса — всего лишь один этап из многих. Если вы помните пример 1.3 в главе 1, выполнение фактического UPDATE инструкции UPDATE было только одним из 15 этапов. Следовательно, время отклика этапа в основном используется экспертами MySQL для изучения глубоких проблем с производительностью сервера. Метрики времени отклика важны, однако совершенно непрозрачны: что делала MySQL за это время? Чтобы ответить на этот вопрос, мы должны покопаться в нашем поле глубже. Скорость (англ. rate) Эти метрики показывают, насколько быстро MySQL выполняет отдельную задачу. Количество запросов в секунду (QPS) — это вездесущий и общеизвестный показатель скорости работы базы данных. Большинство метрик MySQL относятся к этому классу, потому что (это не сюрприз) MySQL выполняет множество отдельных задач. Когда скорость возрастает, это может увеличить связанные с ней объемы использования. Некоторые метрики безвредны и не влияют так на нагрузку, но важные и самые отслеживаемые — да.
Метрики сервера | 199 Взаимосвязь между скоростью и объемом использования (нагрузкой) не предполагает никаких других изменений. Это означает, что вы можете увеличить скорость без увеличения объема использования, только если что-то измените в скорости или нагрузке, на которую она влияет. Обычно легче изменить скорость, а не объем использования, потому что скорость является причиной в этой взаимосвязи. Например, когда QPS в целом возрастает, загрузка CPU также может возрасти, поскольку для большего количества запросов требуется больше процессорного времени. (Возрастание QPS может увеличить другие виды нагрузки; CPU — всего лишь один пример.) Чтобы уменьшить рост загрузки процессора или избежать этого роста, вам следует так оптимизировать запросы, чтобы для них требовалось меньше процессорного времени. Или вы могли бы увеличить количество ядер процессора за счет обновления аппаратного обеспечения, но разделы «Более быстрое и мощное оборудование!» (см. гл. 2) и «Более быстрое и мощное оборудование?» (см. гл. 4) описывают недостатки этого подхода. Взаимосвязь между скоростью и нагрузкой — не нова (вы, вероятно, уже знали об этом), но важно подчеркнуть этот момент, потому что здесь начинается серия взаимосвязей, которые объединяют поле. Не жалейте объем использования: он способен дать сдачи. Объем использования (англ. utilization) Метрики объема использования указывают на то, насколько MySQL использует конечный ресурс. Метрики объема использования есть повсюду в компьютерах: загрузка процессора, использование памяти, использование диска и т. д. Поскольку компьютеры — это конечные машины, почти все можно выразить как объем использования, потому что ничто не обладает бесконечной емкостью — даже облако. Ограниченные скорости (англ. bounded rates) могут быть выражены в виде объема использования. Скорость ограничена, если существует максимальная скорость. Дисковый ввод-вывод, например, обычно выражается как скорость (IOPS — число операций ввода/вывода в секунду), но каждое запоминающее устройство имеет свою максимальную скорость. Следовательно, объем использования дискового ввода-вывода — это соотношение текущей скорости и максимальной. Напротив, неограниченные скорости не могут быть выражены как объем использования, потому что нет четкого максимального значенияа: QPS, отправленные и полученные байты и т. д. Когда объем использования увеличивается, это может привести к снижению соответствующих метрик. Готов поспорить, что вы уже видели или испытывали нечто подобное раньше: запрос, изловчившись, приводит к 100%-ному использованию дискового ввода-вывода, и это влечет за собой резкое падение QPS, что вызывает сбой. Или же MySQL использует 100% памяти и отключается ядром операционной системы, что приводит к полному снижению скорости: до нуля. В этой взаимосвязи проявляется УЗМ (вспомним уравнение 4.1), поскольку использование увеличивает конфликтность (α) и согласованность (β), которые находятся в делителе уравнения.
200 | Глава 6 Что происходит при 100%-ном или близком к этому объеме использования? MySQL ждет. На рис. 6.4 это показано стрелкой между объемом использования и ожиданием (связь объем использования — ожидание). Стрелка помечена как Замедление, потому что выполнение запроса ожидает, а затем возобновляется (возможно, много раз). Я подчеркиваю фразу «или близком к этому», потому что, как обсуждалось в разделе «Производительность дестабилизируется на пределе» (см. гл. 4), замедления могут произойти и до 100% загрузки. Замедления по сути антистабильны, но неизбежны по двум причинам: нагрузка на MySQL обычно превышает пропускную способность оборудования; и задержка присуща всем системам, особенно аппаратным средствам. Первая причина может быть устранена путем снижения нагрузки (оптимизации рабочей нагрузки) или увеличения пропускной способности оборудования. Вторую причину устранить трудно, но возможно. Если, например, вы все еще используете вращающиеся диски, обновление до хранилища NVMe значительно сократит задержку системы хранения. Ожидание Метрики ожидания показывают время простоя в ходе выполнения запроса. Ожидания возникают, когда выполнение запроса останавливается из-за конфликта и согласованности. (Ожидания также возникают из-за ошибок MySQL или регрессий производительности, но это достаточно редкие случаи, поэтому мы не будем о них беспокоиться.) Метрики ожидания рассчитываются как скорость или время отклика (в зависимости от метрики), но заслуживают отдельного класса, потому что они обнаруживают, когда именно MySQL не работает (простаивает), что противоположно производительности. Работа отсутствует, поэтому класс ожидания на рис. 6.4 темнее: MySQL вырубилась. Ожидания неизбежны. Устранение ожиданий — это не цель; цель состоит в их сокращении и стабилизации. Когда ожидания стабилизируются и сокращаются до приемлемого уровня, они фактически исчезают, сливаясь со временем отклика как неотъемлемая часть выполнения запроса. Ожидание событий Ожидания настолько важны, что они составляют отдельный класс в иерархии событий MySQL: транзакции (англ.transactions) └──инструкции (англ. statements) └──этапы (англ. stages) └──ожидания (англ. waits) Схема производительности учитывает множество ожиданий событий, но эти метрики обычно не отслеживаются, потому что они являются глубокими в прямом (в иерархии событий) и переносном (в понимании того, что они представляют) смысле. Про ожидания событий можно было бы написать небольшую книгу. Пока кто-нибудь ее не напишет, обратитесь к статье «Performance Schema Wait Event Tables» («Схема производительности таблицы ожиданий событий», https://oreil.ly/VE55D) в руководстве по MySQL за получением дополнительной информации.
Метрики сервера | 201 Когда MySQL ждет слишком долго, срок ожидания истекает (связь ожидание — ошибка). У наиболее важных высокоуровневых ожиданий MySQL максимально допустимое время ожидания (тайм-аут, англ. timeout) настраивается:  MAX_EXECUTION_TIME (подсказка оптимизатора SQL-инструкций, https://oreil.ly/H0fwi);  max_execution_time (https://oreil.ly/2rdKw);  lock_wait_timeout (https://oreil.ly/WD6p7);  innodb_lock_wait_timeout (https://oreil.ly/4uT4F);  connect_timeout (https://oreil.ly/R7HwC);  wait_timeout (https://oreil.ly/C7M9a). Используйте их, но всецело не полагайтесь, и вот почему. Например, угадайте-ка значение по умолчанию для lock_wait_timeout. Не буду вас томить: 31 536 000 секунд это 365 дней. Установить значения по умолчанию непросто, поэтому мы должны предоставить MySQL некоторую свободу действий, однако 365 дней — это ничего себе. Следовательно, приложения всегда должны задавать значение таймаута на уровне кода. Длительные транзакции и запросы — распространенная проблема, потому что MySQL быстра, но, возможно, чересчур терпелива. Ошибки Метрики ошибок показывают ошибки. (Позволю себе эту тавтологию.) Истечение времени ожидания — один из типов ошибок, но их гораздо больше (подробнее: «MySQL Error Message Reference» — «Справка: сообщения об ошибках MySQL», https://oreil.ly/Jtpqd). Мне не нужно перечислять все возможные ошибки MySQL, ведь что касается производительности сервера и метрик MySQL, суть проста и ясна: ненормальная частота ошибок — это плохо. Как и метрики ожиданий, метрики ошибок вычисляются как скорости (англ. rates), но заслуживают отдельного класса, потому что указывают, когда MySQL или клиент (приложение) вылетели, поэтому класс ошибок на рис. 6.4 отображен темнее. Повторим замечание об ошибках из раздела «Ключевые показатели производительности» (см. выше): не рассчитывайте на нулевую частоту ошибок, потому что, например, ни вы, ни приложение, ни MySQL ничего не сможете сделать, если клиент прервет соединение. Паттерны доступа Метрики паттернов доступа показывают, как приложение использует MySQL. Посмотрите раздел «Паттерны доступа к данным» (см. гл. 4). Например, в MySQL есть метрики для каждого типа инструкции SQL (Com_select, Com_insert и т. д.), которые относятся к чтению/записи (см. гл. 4, «Чтение/запись»). Как показано на рис. 6.4, метрики паттерна доступа лежат в основе показателей более высокого уровня. Метрика паттерна доступа Com_select подсчитывает количество выполненных инструкций SELECT. Она может быть представлена как скорость
202 | Глава 6 (SELECT QPS) или объем использования (% SELECT); в любом случае это раскрывает нечто более глубокое о производительности сервера, нечто, что помогает объяснить метрики более высокого уровня. Например, если время отклика ужасно, а значение метрики паттерна доступа Select_full_join велико, это неопровержимая улика (см. раздел «Полное сканирование присоединенной таблицы», гл. 1). Внутренние метрики На рис. 6.5 показан седьмой класс метрик: внутренние метрики. Я не упоминал об этом классе в начале раздела «Поле метрик» (см. выше), потому что, как инженерам и пользователям MySQL, нам нет необходимости это знать или переживать об этом. Но это самая интересная (и даже таинственная) часть поля метрик, и я хочу, чтобы вы были полностью информированы на случай, если вам это понадобится или вы захотите проникнуть в глубины MySQL. Здесь, внизу, все очень таинственно. Рис. 6.5. Поле метрик с внутренними метриками Конечно, понятие о таинственности тут субъективно. То, что я считаю внутренней метрикой, может быть самой любимой и полезной метрикой оценки для другого программиста. Но такие метрики, как buffer_page_read_index_ibuf_non_leaf, дают веские аргументы в пользу того, чтобы отнести их к внутреннему классу. Эта метрика указывает на количество нелистовых страниц индекса, прочитанных в буфере изменений. Не совсем ваш хлеб насущный. Спектры Приготовьтесь к еще одному путешествию: в полутень метрик MySQL. В этом разделе освещается более 70 метрик MySQL, разделенных на 11 спектров, некоторые из которых имеют субспектры. Я систематизирую метрики MySQL в спектры по двум причинам:
Метрики сервера | 203 1. Спектры выступают в качестве промежуточных пунктов маршрута путешествия. Без них мы сталкиваемся с обширной и неорганизованной вселенной, состоящей из почти тысячи показателей из разных источников, которые варьируются в зависимости от версии MySQL, дистрибутива и конфигурации. 2. Спектры раскрывают важные области MySQL для понимания и мониторинга в отношении производительности. Даже если спектры и освещают путь в темноте, нам нужно соглашение об именовании метрик, чтобы четко и точно говорить о метриках MySQL и системных переменных, составляющих каждый спектр. Причина проста: в MySQL нет подобного соглашения, и отраслевого стандарта тоже нет. Табл. 6.1 — соглашение об именах метрик MySQL, которое я использую в этой книге. Таблица. 6.1. Соглашение об именовании метрик MySQL Пример Относится к Threads_running Глобальные переменные состояния var.max_connections Глобальные системные переменные innobd.log_lsn_checkpoint_age Метрики InnoDB запаздывание репликации Производные метрики Большинство метрик представляют собой глобальные переменные состояния, которые вы, вероятно, видели или использовали при выполнении SHOW GLOBAL STATUS (https://oreil.ly/NacuT): Aborted_connects, Queries, Threads_running и т. д. В MySQL и в этой книге названия глобальных переменных состояния начинаются с одной заглавной буквы, за которой следуют строчные буквы, даже если первое слово является аббревиатурой: Ssl_client_connects, а не SSL_client_connects. (Это один из устойчивых аспектов метрик MySQL.) Глобальные системные переменные, напротив, пишутся в нижнем регистре; и чтобы четче их обозначить, я добавляю к ним префикс var., это важно, учитывая следующее соглашение. Метрики InnoDB также записываются в нижнем регистре — например, lock_ timeouts. Чтобы вы не спутали их с глобальными системными переменными, я добавляю к метрикам InnoDB префикс innodb., например innodb.lock_timeouts. Производные метрики широко используются в мониторинге, но не являются родными для MySQL. Например, запаздывание репликации — это метрика, которая выводится почти любым средством мониторинга, но точное название показателя зависит от средства мониторинга, поэтому я использую описательное имя без символов подчеркивания, а не конкретное техническое название. Метрики InnoDB в этом разделе требуют включения определенных счетчиков или модулей. Например, запуск MySQL с innodb_monitor_enable=module_log,module_ buffer,module_trx. Смотрите var.innodb_monitor_enable (https://oreil.ly/nFKFT) и раздел «InnoDB INFORMATION_SCHEMA Metrics Table» («Таблица метрик InnoDB INFORMATION_SCHEMA», https://oreil.ly/e0wpA) в руководстве по MySQL.
204 | Глава 6 Предпоследний элемент нашего умственного оснащения: термин глобальный относится ко всему серверу MySQL: все клиенты, все пользователи, все запросы и т. д., вместе взятые. В отличие от этого, существуют сеансовые (англ. session) и итоговые (англ. summary) метрики. Сеансовые метрики — это глобальные метрики, привязанные к одному клиентскому подключению. Итоговые метрики обычно представляют собой подмножество глобальных показателей, охватывающих различные аспекты: учетную запись, хост, поток, транзакцию и т. д. В этой главе рассматриваются только глобальные метрики, поскольку они лежат в основе всех метрик. (Глобальные метрики также появились первыми: в древние времена у MySQL были только глобальные метрики; затем добавились сеансовые метрики, а затем уже итоговые.) И последнее замечание перед тем, как мы начнем путешествие: большинство метрик MySQL — это простые счетчики (англ. counters), и только некоторые из них являются датчиками (англ. gauges). Я прямо указываю на датчики; в противном случае подразумевается счетчик. Давайте начнем! Время отклика на запрос Глобальное время отклика на запрос является одним из четырех ключевых показателей производительности (см. выше соответствующий раздел). Удивительно, но у MySQL не было этой метрики до версии 8.0. Начиная с версии MySQL 8.0.1, вы можете получить 95-й процентиль (P95) глобального времени отклика на запрос в миллисекундах из схемы производительности (https://oreil.ly/dj06D), выполнив запрос из примера 6.1. Пример 6.1. Глобальное время отклика на запрос 95-го процентиля SELECT ROUND(bucket_quantile * 100, 1) AS p, ROUND(BUCKET_TIMER_HIGH / 1000000000, 3) AS ms FROM performance_schema.events_statements_histogram_global WHERE bucket_quantile >= 0.95 ORDER BY bucket_quantile LIMIT 1; Этот запрос возвращает процентиль, очень близкий (но не совсем) к P95: например, 95,2 вместо 95,0%1. Разница незначительна и не влияет на мониторинг. Вы можете заменить 0,95 в запросе, чтобы вернуть другой процентиль: 0,99 для P99 или 0,999 для P999. Я предпочитаю и советую P999 по причинам, описанным в разделе «Среднее значение, процентиль и максимум» (см. гл. 1). 1 В рабочем журнале (англ. worklog) MySQL 5384 объясняется, как квантили времени отклика реализуются в схеме производительности, https://oreil.ly/2kFWK.
Метрики сервера | 205 Остальная часть этого раздела предназначена для версий MySQL 5.7 и старше; пропустите ее, если вы используете MySQL 8.0 или новее. MySQL 5.7 и старше MySQL версии 5.7 и старше не предоставляет глобальную метрику времени отклика на запрос. Только метрики запроса включают время отклика (см. гл. 1, раздел «Время запроса»), но это время отклика на запрос. Чтобы рассчитать глобальное время отклика, вам нужно будет агрегировать время отклика из каждого запроса. Это возможно, но есть две более хорошие альтернативы: обновитесь до MySQL 8.0 или же переключитесь на Percona Server либо MariaDB, у которых есть плагин для учета глобального времени отклика. Сервер Percona 5.7 Еще в 2010 году Percona Server (https://oreil.ly/Gyq8J) представил плагин для учета глобального времени отклика под названием Response Time Distribution (с англ.: «Распределение времени отклика», https://oreil.ly/PE5kh). Плагин прост в установке, но его настройка и использование требуют усилий, потому что это гистограмма диапазонов времени отклика, а это значит, что вам нужно установить var.query_response_time_range_base (глобальную системную переменную, создаваемую плагином) — для настройки диапазонов сегментов (англ. buckets) гистограммы, а затем вычисления процентиля из количества сегментов. Глобальное время отклика MySQL 8.0 также представлено гистограммой, но диапазоны сегментов и процентили заданы и предварительно вычислены, поэтому запрос в примере 6.1 работает «из коробки». Это не так уж сложно настроить, хотя звучит сложно. Преимущество использования метрики глобального времени отклика окупает затраченные усилия. MariaDB 10.0 MariaDB (https://oreil.ly/oeGJO) использует тот же плагин от Percona, но у него немного другое название: Query Response Time Plugin (с англ.: «плагин времени отклика на запрос», https://oreil.ly/kb4gA). Хотя он появился в MariaDB 10.0, не был отмечен как стабильный до MariaDB 10.1. До MySQL 8.0 получение глобального времени отклика на запрос — дело непростое, но это стоит усилий, если вы используете Percona Server или MariaDB. Если вы используете MySQL в облаке, проверьте метрики облачного провайдера, потому что некоторые предоставляют метрику времени отклика (которую облачный провайдер может назвать задержкой (англ. latency)). Если ничего другого нет, чаще просматривайте профиль запроса, чтобы следить за временем отклика. Ошибки Ошибки являются одним из четырех ключевых показателей производительности (см. выше соответствующий раздел). Начиная с версии MySQL 8.0.0, легко узнать количество всех ошибок из схемы производительности (https://oreil.ly/dj06D) при помощи запроса, как в примере 6.2.
206 | Глава 6 Пример 6.2. Глобальное количество ошибок SELECT SUM(SUM_ERROR_RAISED) AS global_errors FROM performance_schema.events_errors_summary_global_by_error WHERE ERROR_NUMBER NOT IN (1287); Ошибка 1287, исключенная WHERE в примере 6.2, относится к предупреждениям о депрекации: когда запрос использует устаревшую функцию, MySQL выдает предупреждение. Включение этой ошибки в подсчет, вероятно, сделает глобальное количество ошибок слишком «шумным», поэтому я исключаю ее. Поскольку в MySQL уйма ошибок и предупреждений, невозможно сказать, какова будет ваша глобальная частота ошибок. Не ожидайте и не пытайтесь достичь нулевой частоты. Это просто нереально, потому что клиенты могут вызывать ошибки, и ни вы, ни приложение, ни MySQL ничего не можете сделать, чтобы этого избежать. Цель в том, чтобы установить нормальную частоту ошибок для конкретного приложения. Если запрос в примере 6.2 слишком «шумный» (т. е. выдает большое количество ошибок), но вы уверены, что приложение работает нормально, то настройте запрос, исключив дополнительные ошибки. Коды ошибок MySQL задокументированы в «Справке о сообщениях и ошибках MySQL» (https://oreil.ly/ wKfnV). До версии MySQL 8.0 получить глобальное количество ошибок из MySQL невозможно, но можно получить количество всех ошибок запроса из схемы производительности (https://oreil.ly/QiHj8), выполнив запрос из примера 6.3. Пример 6.3. Количество ошибок запроса SELECT SUM(sum_errors) AS query_errors FROM performance_schema.events_statements_summary_global_by_event_name WHERE event_name LIKE 'statement/sql/%'; Поскольку это работает во всех дистрибутивах, начиная с MySQL 5.6, нет причин не отслеживать все ошибки запросов. Конечно, приложение и само должно сообщать об ошибках запроса. Но если оно повторно попытается выполнить его с ошибкой, то может скрыть определенное их количество. В отличие от этого, прием из примера покажет все ошибки запроса, потенциально выявляя проблему, которую маскируют повторные попытки приложения. Последние метрики ошибок — это ошибки подключения клиента:  Aborted_clients;  Aborted_connects;  Connection_errors_%.
Метрики сервера | 207 Первые две метрики обычно отслеживаются, чтобы убедиться в отсутствии проблем во время подключения к MySQL или когда подключение уже произошло. Эта формулировка точна: если приложение не может установить сетевое подключение к MySQL, то MySQL не видит клиента и не сообщает об ошибке подключения к клиенту, потому что с точки зрения MySQL клиентского подключения еще нет. О проблемах сетевого подключения низкого уровня должно сообщать приложение. Однако, если приложение не может подключиться, вы, вероятно, увидите снижение остальных трех ключевых показателей производительности (QPS, выполняющиеся потоки и время отклика), поскольку оно не выполняет запросы. Символ % в Connection_errors_% является подстановочным знаком MySQL: существует несколько метрик с префиксом Connection_errors_. Чтобы вывести их список, выполните команду SHOW GLOBAL STATUS LIKE Connection_errors_%;. Прежде чем перейти к следующему спектру, давайте рассмотрим проблему, которая при этом проблемой не является (по крайней мере для MySQL). Если приложение начинает выдавать ошибки, но MySQL этого не делает, а остальные три ключевых показателя производительности являются нормальными, то проблема связана с приложением или сетью. У MySQL есть много причуд, но ложь не входит в их число. Если ключевые показатели производительности MySQL находятся на высоком уровне (все в порядке и по плану), то вы можете быть уверены, что MySQL работает нормально. Запросы Метрики, связанные с запросами, показывают, насколько быстро работает MySQL и какую работу она выполняет (на очень высоком уровне). Эти метрики указывают на две характеристики паттерна доступа: пропускную способность и чтение/запись (см. раздел «Пропускная способность» и «Чтение/запись» в гл. 4). QPS QPS является одним из четырех ключевых показателей производительности (см. выше соответствующий раздел). Базовая метрика имеет подходящее название:  Queries Эта метрика относится к счетчикам, но QPS — это показатель скорости (англ. rate), поэтому технически QPS равно разнице между двумя значениями Queries, деленной на число секунд между измерениями: QPS = (Queries @ T1 – Queries @ T0) / (T1 – T0), где T0 — время первого измерения, а T1 — время второго. Системы построения графиков метрик (например, Grafana, https://grafana.com) по умолчанию преобразуют счетчики в метрики. В результате вам не нужно будет преобразовывать Queries или любые другие счетчики в значения скорости. Просто имейте в виду, что большинство метрик MySQL являются счетчиками, но они преобразуются и выражаются в виде скоростей.
208 | Глава 6 Системы построения графиков метрик по умолчанию преобразуют счетчики в значения скорости. QPS уделяется много внимания, потому что этот параметр показывает общую пропускную способность MySQL (как быстро MySQL выполняет запросы), но не зацикливайтесь на этом. Как упоминалось в разделе «Чем меньше QPS, тем лучше» (см. гл. 3), QPS не сообщает ничего важного о запросах или производительности в целом. Если оно невероятно высоко, но время отклика также слишком большое, это указывает на проблему, а не на высокую производительность. Другие метрики в этом плане гораздо информативнее. Когда все работает нормально, QPS колеблется в зависимости от использования приложения. Когда возникает проблема, колебания QPS коррелируют с другими метриками. Чтобы проанализировать производительность или диагностировать проблему, я смотрю, где на графике значение QPS ненормально. Затем сопоставляю этот период (время вдоль оси X графика) с другими, более конкретными метриками в спектрах. Как и KPI, QPS указывает на проблему, но другие метрики раскрывают ее точнее. Все нестандартные изменения в QPS подозрительны и заслуживают расследования. Чуть ли не каждый программист знает, что падение QPS — это плохо, но ненормальное увеличение QPS может быть так же плохо или даже хуже. Не менее плохое, но более редкое явление — плоская линия на графике (почти постоянное значение QPS), потому что нормальны незначительные колебания. Когда QPS изменяется нетипично, первый вопрос обычно таков: в чем причина? Вернемся к этому позже (см. раздел «Причина и следствие» ниже в этой главе). MySQL предоставляет еще одну тесно связанную с этим метрику: Questions (с англ. вопросы). (Термин «question» используется только для этой метрики и ни для чего другого внутри MySQL). Метрика Questions учитывает только запросы, отправленные клиентами, а не запросы, выполняемые сохраненными программами. Например, запросы, выполняемые по триггеру, не учитываются в метрике Questions, потому что клиент их не отправлял; но они учитываются в метрике Queries. Поскольку Questions — подмножество Queries, разница носит только информативный характер, и отслеживать метрику Questions необязательно. Для QPS всегда используйте Queries. TPS Если приложение полагается на явные транзакции с несколькими инструкциями, то количество транзакций в секунду (англ. transactions per second, TPS) так же важно, как и QPS. Для некоторых приложений транзакция базы данных представляет собой единицу работы в приложении, поэтому TPS — это более хорошая метрика скорости, чем QPS, потому что единица работы приложения — это все или ничего. Именно поэтому она выполняется в эксплицитной транзакции. Имплицитная транзакция — это единичная инструкция SQL с включенной переменной autocommit (https://oreil.ly/zrjQK), что работает по умолчанию. Эксплицитная транзакция начинается с BEGIN или START TRANSACTION и заканчивается либо COMMIT, либо ROLL BACK, независимо от autocommit.
Метрики сервера | 209 В MySQL явная пропускная способность транзакций определяется по трем метрикам:  Com_begin;  Com_commit;  Com_rollback. Обычно значение Com_begin и Com_commit одинаково, потому что каждая транзакция должна начинаться, а успешные транзакции должны фиксироваться. Когда возникает проблема, которая приводит к замедлению транзакций (одна из распространенных проблем, см. гл. 8, раздел «Разпространенные проблемы»), значение Com_begin превышает две другие метрики. Используйте Com_commit для измерения TPS, поскольку пропускная способность транзакций подразумевает успешные транзакции. Предполагается, что откат транзакции указывает на ошибку, поскольку транзакции — это все или ничего, но ROLLBACK также обычно используется для очистки: ROLLBACK гарантирует, что предыдущая транзакция (если таковая имеется) будет закрыта перед запуском следующей транзакцией. Поэтому скорость коэффициента отката может быть не равна нулю. Как и в случае с большинством метрик, наша цель — нормальная и стабильная работа (см. выше раздел «Нормальная и стабильная: лучшая база данных — это скучная база данных» ). Другой метрикой-датчиком, показывающей текущее число активных транзакций, является innodb.trx_active_transactions. BEGIN запускает транзакцию, но транзакция в общем-то не активна до тех пор, пока запрос не получит доступ к таблице. Например, BEGIN; SELECT NOW(); запускает транзакцию, которая не активна, поскольку ни один запрос не обращается к таблице. SHOW ENGINE INNODB STATUS Метрики InnoDB отображаются в таблице information_schema.innodb_metrics. (Подробнее: «InnoDB INFORMATION_SCHEMA Metrics Table», «Таблица метрик InnoDB INFORMATION_SCHEMA», https://oreil.ly/GHalc). До того, как эта таблица обрела популярность, метрики InnoDB отображались с помощью команды SHOW ENGINE INNODB STATUS, но ее выходные данные — длинный текстовый блок. Текст разделен на параграфы, что немного облегчает его чтение для людей, но не организован на программном уровне: для извлечения определенных значений метрик требуется парсинг и сопоставление с образцом. Некоторые средства мониторинга MySQL по-прежнему используют SHOW ENGINE INNODB STATUS, но я советую по возможности этого избегать — лучше всего использовать схему информации (и схему производительности). Я больше не считаю SHOW ENGINE INNODB STATUS авторитетным источником метрик. Например, BEGIN; SELECT col FROM tbl; не отображается там как активная транзакция, а в innodb.trx_active_transactions отображена правильно.
210 | Глава 6 Чтение/запись Существуют девять метрик чтения/записи, названных в соответствии с типом инструкции SQL:  Com_select;  Com_replace;  Com_delete;  Com_replace_select;  Com_delete_multi;  Com_update;  Com_insert;  Com_update_multi.  Com_insert_select; Например, Com_select — это счетчик количества команд SELECT. Суффикс _multi в Com_delete_multi и Com_update_multi указывает, что запросы ссылаются на несколько таблиц. Удаление нескольких таблиц (инструкция DELETE) увеличивает только значение Com_delete_multi, тогда как удаление одной таблицы обновляет только Com_delete. То же самое верно для инструкций UPDATE в отношении Com_update_multi и Com_update. Метрики чтения/записи показывают важные типы и пропускную способность запросов, которые составляют класс Queries. Эти метрики не полностью относятся к Queries; просто они наиболее важны с точки зрения производительности. Отслеживайте эти метрики как отдельные показатели скорости и проценты от Queries:  Com_select показывает процент чтения в рабочей нагрузке: (Com_select / Queries) × 100.  Сумма остальных восьми метрик показывает процент записи в рабочей нагрузке2. Доли чтения и записи не будут равны 100%, поскольку Queries учитывают и прочие типы инструкций SQL: SHOW, FLUSH, GRANT и многие другие. Если оставшийся процент подозрительно высок (более 20%), это, вероятно, не повлияет на производительность, но стоит внимательно присмотреться: проверьте другие метрики Com_ для учета инструкций SQL иного типа. Администрирование Метрики администрирования относятся к командам, которые, как правило, используют только администраторы баз данных:  Com_flush;  Com_kill;  Com_purge;  Com_admin_commands. 2 Com_insert_select и Com_replace_select технически относятся и к чтению, и к операциям записи, но для простоты я считаю их командами записи.
Метрики сервера | 211 Первые три метрики относятся к инструкциям FLUSH (https://oreil.ly/O6j77), KILL (https://oreil.ly/fMbiY) и PURGE (https://oreil.ly/czxYb) соответственно. Эти команды могут повлиять на производительность, но должны использоваться очень редко. Если они используются часто, спросите своего администратора базы данных или облачного провайдера, чем они там занимаются. Последняя метрика, Com_admin_commands, стоит особняком. Она относится к другим командам администраторирования, для которых нет конкретных переменных состояния Com_. Например, протокол MySQL содержит команду пинга, которая обычно используется драйверами клиентов MySQL для проверки соединения. В умеренных количествах это безвредно, но при другом раскладе могут возникнуть сложности. Не ожидайте, что Com_admin_commands покажет какие-либо проблемы, но мониторить ее — по-прежнему лучшая практика. Show В MySQL есть более 40 инструкций SHOW (с англ. показать, https://oreil.ly/u7Xzs), большинство из которых имеют соответствующую метрику Com_show_. Команды SHOW никогда не воздействуют на MySQL и не изменяют данные, так что в этом смысле они безвредны. Однако это запросы, т. е. они используют поток, время и ресурсы MySQL. Команды SHOW тоже могут тормозить. Например, выполнение SHOW GLOBAL STATUS на нагруженном сервере может занять целую секунду или больше. Следовательно, рекомендуется отслеживать по крайней мере следующие 10 метрик:  Com_show_databases;  Com_show_status;  Com_show_engine_status;  Com_show_table_status;  Com_show_errors;  Com_show_tables;  Com_show_processlist;  Com_show_variables;  Com_show_slave_status;  Com_show_warnings. Начиная с версии MySQL 8.0.22, отслеживайте Com_show_replica_status вместо Com_show_slave_status. Не ожидайте, что метрики SHOW укажут на какие-либо проблемы, но не удивляйтесь, если однажды это произойдет, потому что такое бывает. Потоки и соединения Threads_running (с англ. выполняющиеся потоки) — один из четырех ключевых показателей производительности (см. выше соответствующий раздел). Эта метрика показывает, насколько интенсивно работает MySQL, потому что непосредственно соотносится с активным выполнением запросов (когда клиентское соединение не выполняет запрос, его поток простаивает), и это фактически ограничено количеством ядер процессора. Мы вернемся к Threads_running после того, как взглянем на другие связанные с ней метрики.
212 | Глава 6 Потоки и соединения — это один спектр, потому что они напрямую связаны: MySQL запускает один поток на каждое клиентское соединение. Четыре наиболее важные метрики здесь таковы:  Connections;  Max_used_connections;  Threads_connected;  Threads_running. Метрика Connections отражает количество попыток подключения к MySQL, как успешных, так и неудачных, — показывает стабильность пула подключений приложений к MySQL. Обычно соединения приложений с MySQL долговечны, и длительность составляет по крайней мере несколько секунд, если не минут или часов. Долговечные соединения позволяют избежать дополнительных расходов ресурса на установление соединения. Когда приложение и MySQL находятся в одной локальной сети, эти расходы незначительны: 1 миллисекунда или меньше. Но сетевая задержка между приложением и MySQL быстро увеличивается, если умножить ее на сотни подключений и снова умножить на скорость соединения. (Метрика Connections — это счетчик, но выраженный как скорость: число соединений в секунду.) MySQL может легко обрабатывать сотни подключений в секунду, но если эта метрика показывает аномально высокое значение, необходимо найти и устранить основную причину этого. Метрика Max_used_connections в качестве процентной доли от var.max_connections (https://oreil.ly/MVZaQ) показывает объем использования соединения. Значение по умолчанию для var.max_connections равно 151, что, вероятно, слишком мало для большинства приложений — требуется больше, но дело не в повышении производительности. Просто каждый экземпляр приложения имеет свой собственный пул подключений. (Я предполагаю, что приложение масштабируется.) Если размер пула подключений равен 100 и имеется 3 экземпляра приложения, то приложение (все экземпляры) может создать 300 подключений к MySQL. Это основная причина, по которой значения 151 для данной метрики недостаточно. Распространенно заблуждение, что приложению требуются тысячи подключений к MySQL для повышения производительности или поддержки пользователей. Это абсолютная неправда. Ограничивающим фактором являются потоки, а не соединения (подробнее о Threads_running чуть позже). Тысячи подключений может легко обрабатывать один экземпляр MySQL. Я видел 4000 подключений в работе и еще больше в тестах. Но большинству приложений хватает и нескольких сотен подключений (в совокупности). Если вашему приложению требуются прямо тысячи подключений, вам нужно выполнить шардирование (см. гл. 5). Настоящая проблема, которую нужно отслеживать и которой необходимо избегать — это 100%-ный объем использования соединений. Если у MySQL заканчиваются доступные соединения, это, по сути, гарантирует сбой приложения. Если объем использования соединения внезапно возрастает, приближаясь к 100%, причина всегда в баге или во внешней проблеме, или и в том и в другом. (MySQL не может
Метрики сервера | 213 подключиться к самой себе, поэтому причина должна быть внешней). В ответ на внешнюю проблему (например, сетевую неполадку) приложение создает больше подключений, чем обычно. А вследствие бага приложение может не закрывать соединения; обычно это называется утечкой подключения. Бывает и так, что внешняя проблема вызывает баг в приложении (я видел, как это происходило). В любом случае, основная причина всегда вовне: что-то вне MySQL подключается к MySQL и использует все подключения. Когда клиенты подключаются и отключаются, MySQL приращивает и снижает метрику-датчик Threads_connected. Название этой метрики может ввести в заблуждение, поскольку подключены клиенты, а не потоки (threads), но оно отражает тот факт, что MySQL запускает один поток на каждое клиентское соединение. Threads_running — это метрика-датчик, и имплицитный (неявный) объем использования, относящийся к количеству ядер процессора. Хотя значение Threads_running может достигать сотен и тысяч, производительность резко снизится уже при гораздо меньших значениях: когда они будут примерно в два раза больше числа ядер процессора. Причина проста: одно ядро процессора запускает один поток. А если выполняющихся потоков больше, чем ядер, то некоторые потоки замедлятся в ожидании процессорного времени. Это похоже на движение в час пик: тысячи автомобилей стоят в пробке на шоссе, двигатели работают, но автомобили едва движутся. (Или для электромобилей: батареи работают, но автомобили почти не двигаются.) Поэтому для Threads_running нормально иметь довольно низкое значение: менее 30. Всплески активности в несколько секунд или меньше возможны при хорошем оборудовании и оптимизированной рабочей нагрузке, но устойчивое (нормальное и стабильное) значение Threads_running должно быть как можно ниже. Как и в случае «Чем меньше QPS, тем лучше» (см. гл. 3), так и чем меньше значение Threads_running — тем тоже лучше. Высокая пропускная способность (QPS) при очень низком количестве выполняющихся потоков является убедительным показателем эффективной производительности, поскольку существует лишь один способ добиться и того и другого: очень быстрый отклик на запрос. В табл. 6.2 перечислены выполняющиеся потоки и QPS из пяти реальных (и разных) приложений. Таблица 6.2. Выполняющиеся потоки и QPS Выполняющиеся потоки QPS 4 8000 8 6000 8 30 000 12 23 000 15 33 000 Вторая и третья строки показывают, как сильно рабочая нагрузка приложения влияет на производительность: при одной рабочей нагрузке для выполнения
214 | Глава 6 6000 QPS требуется 8 потоков; но другая достигает 5x QPS (30 000) при том же количестве потоков. Последняя строка: значение в 33 000 QPS не является исключительно высоким но эта база данных шардирована: общее количество QPS по всем шардам превышает один миллион. На практике высокая пропускная способность вполне возможна при небольшом количестве выполняющихся потоков. Временные объекты Временные объекты — это временные файлы и таблицы, которые MySQL использует для различных целей: сортировки строк, больших соединений и т. д. Три метрики подсчитывают количество временных таблиц на диске, временных таблиц в памяти и временных файлов (на диске):  Created_tmp_disk_tables;  Created_tmp_tables;  Created_tmp_files. Значения этих метрик редко равны нулю, потому что временные объекты — обычное и безобидное явление до тех пор, пока скорости стабильны. Наиболее эффективна метрика Created_tmp_disk_tables, которая взаимосвязана с метрикой Created_ tmp_tables. Когда MySQL нужна временная таблица для выполнения запроса (например, для GROUP BY), она начинает с временной таблицы в памяти и увеличивает значение Created_tmp_tables. Что не должно влиять на производительность, потому что происходит в памяти. Но если размер этой временной таблицы превысит параметр, установленный var.tmp_table_size (системная переменная, определяющая размер временной таблицы в памяти, https://oreil.ly/4plVm), MySQL запишет временную таблицу на диск и увеличит значение Created_tmp_disk_tables. В умеренных количествах это, вероятно, также не повлияет на производительность, но и пользы явно не принесет, поскольку хранилище (на диске) работает значительно медленнее, чем память. То же верно и для Created_tmp_files: приемлемо в умеренных количествах, но не способствует повышению производительности. Начиная с MySQL 8.0, Created_tmp_disk_tables не учитывает временные таблицы, созданные на диске. Это связано с новой подсистемой хранения для внутренних временных таблиц: TempTable. Соответствующая метрика — это инструмент памяти схемы производительности: memory/temptable/physical_disk. (Родственным инструментом является memory/temptable/physical_ram, который отслеживает распределение памяти TempTable для временных таблиц в памяти.) Если вы используете MySQL 8.0, проконсультируйтесь с вашим администратором базы данных, чтобы убедиться, что данные этой метрики собраны и представлены правильно. Поскольку временные объекты — побочные эффекты запросов, эти метрики наиболее показательны, когда изменение одного из них коррелирует с изменением ключевых показателей производительности (англ. KPI). Например, внезапный рост значения Created_tmp_disk_tables в сочетании с внезапным ростом времени отклика просто кричит — «Посмотрите на меня!»3. 3 Совсем как: «Я мистер Мизикс, посмотрите на меня!» из сериала про Рика и Морти.
Метрики сервера | 215 Подготовленные инструкции Подготовленные инструкции (англ. prepared statements) — это палка о двух концах: при правильном использовании они повышают производительность; но при неправильном (или неосознанном) — увеличивают потери. Правильный и наиболее эффективный способ использования подготовленных инструкций — это подготовить инструкцию один раз, а выполнить много раз, что учитывается двумя метриками:  Com_stmt_prepare;  Com_stm_execute. Значение метрики Com_stmt_execute должно быть значительно больше, чем Com_stmt_prepare. Если это не так, то подготовленные инструкции будут увеличивать потери из-за дополнительных запросов для подготовки и закрытия самой инструкции. Наихудший случай — когда значение этих метрик одинаково или близко к этому, потому что один запрос приводит к двум потраченным впустую обменам сигналами с к MySQL: один для подготовки, а другой для закрытия инструкции. Когда MySQL и приложение находятся в одной локальной сети, два дополнительных обмена сигналами могут быть незаметны, но это чистая трата времени, если умножить на QPS. Например, дополнительная 1 миллисекунда при 1000 QPS — это потраченная впустую секунда, за которую могла бы быть выполнена еще 1000 запросов. Эти метрики нужно отслеживать не только в связи с последствиями для производительности, но и потому, что приложение может использовать подготовленные инструкции непреднамеренно. Например, драйвер MySQL для языка программирования Go по умолчанию использует подготовленные инструкции для обеспечения безопасности: чтобы избежать уязвимостей внедрения SQL-кода. На первый взгляд (да на любое количество взглядов), вы бы не подумали, что код Go в примере 6.4 использует подготовленную инструкция, но это так. Пример 6.4. Скрытая подготовленная инструкция id := 75 db.QueryRow("SELECT col FROM tbl WHERE id = ?", id) Проверьте документацию для драйвера MySQL, который используется приложением. Если в нем явно не указано, использует ли оно подготовленные инструкции и когда конкретно, проверьте это вручную: на экземпляре MySQL для разработки (например, на вашем ноутбуке) включите общий лог запросов (https://oreil.ly/ 1Vczu) и напишите тестовую программу для выполнения инструкций SQL, используя те же методы и вызовы функций, что и приложение. Общий лог показжет, когда используются подготовленные инструкции: 2022-03-01T00:06:51.164761Z 2022-03-01T00:06:51.164870Z 2022-03-01T00:06:51.165127Z 32 Prepare SELECT col FROM tbl WHERE id=? 32 Execute SELECT col FROM tbl WHERE id=75 32 Close stmt Наконец, количество открытых подготовленных инструкций ограничено значением var.max_prepared_stmt_count (https://oreil.ly/K2MWz), которое по умолчанию рав-
216 | Глава 6 но 16 382. (Даже 1000 подготовленных инструкций — это много для одного приложения, если только оно не генерирует инструкции программно.) Эта метрикадатчик сообщает о текущем количестве открытых подготовленных инструкций:  Prepared_stmt_count. Не позволяйте Prepared_stmt_count достигать var.max_prepared_stmt_count, иначе приложение перестанет работать. Если это происходит, это баг приложения из-за утечки (отсутствия закрытия) подготовленных инструкций. Плохие SELECT Четыре метрики учитывают появление инструкций SELECT, которые обычно плохо влияют на производительность4:  Select_scan;  Select_full_join;  Select_full_range_join;  Select_range_check. и Select_full_join описаны в главе 1. С той только разницей, что эти две метрики применяются глобально (ко всем запросам). Select_scan — меньшее из двух зол по сравнению с Select_full_join: вместо полного сканирования таблицы для присоединения таблицы MySQL использует индекс для сканирования диапазона. Возможно, диапазон будет ограничен, а время отклика для SELECT — приемлемо, но это все равно достаточно плохо, чтобы заслуживать собственной метрики. Select_full_range_join Select_range_check похоже на Select_full_range_join, но хуже, join. Это легко объяснить с помощью простого запроса: чем Select_full_range_ SELECT * FROM t1, t2, WHERE t1.id > t2.id Когда MySQL соединяет таблицы t1 и t2 (в таком порядке), она выполняет проверку диапазона для t2: для каждого значения из t1 MySQL проверяет, можно ли использовать индекс от t2 для слияния индексов или сканирования на основе диапазона. Перепроверка каждого значения из t1 необходима, потому что, учитывая запрос, MySQL не может знать значения t1 заранее. Но вместо того, чтобы следовать наихудшему возможному плану выполнения (Select_full_join), MySQL продолжает пытаться использовать индекс от t2. В выходных данных EXPLAIN в поле Extra для t2 указано: Range checked for each record (с англ. диапазон, проверяемый для каждой записи), а Select_range_check возрастает один раз для таблицы. Эта метрика не увеличивается при каждом изменении диапазона; она увеличивается один раз, чтобы сигнализировать, что таблица была присоединена путем проверки диапазона. 4 Подробнее смотрите в блоге «MySQL Select and Sort Status Variables» — «Переменные состояния выбора и сортировки MySQL», https://oreil.ly/OpJvS.
Метрики сервера | 217 Плохие метрики инструкции SELECT должны быть равны нулю или почти нулю (если округлить в меньшую сторону). Какие-то небольшие значения для Select_scan или Select_full_range_join неизбежны, но если две другие (Select_full_join и Select_range_ check) не равны нулю, они немедленно должны быть найдены и исправлены. Пропускная способность сети Протокол MySQL очень эффективен и редко использует сколько-нибудь заметную пропускную способность сети. Обычно сеть влияет на MySQL, а не MySQL на сеть. Тем не менее полезно отслеживать пропускную способность сети, записанную MySQL:  Bytes_sent;  Bytes_received. Поскольку эти метрики подсчитывают соответственно отправленные и полученные сетевые байты, необходимо преобразовать значения в сетевые единицы измерения: Мбит/с или Гбит/с, в соответствии со скоростью соединения сервера, на котором работает MySQL. Гигабитные соединения являются наиболее распространенными, даже в облаке. Системы построения графиков метрик по умолчанию преобразуют счетчики в значения скорости, но вам, вероятно, потребуется умножить эти метрики на восемь (8 бит на байт) и установить единицу измерения графика в битах для отображения в Мбит/с или Гбит/с. Я только один раз видел, как MySQL насыщала сеть. Причина была связана с системной переменной, которая обычно не приносит проблем: var.binlog_row_image (https://oreil.ly/tboxy). Эта системная переменная связана с репликацией (см. гл. 7), но если вкратце, то она определяет, регистрируются ли столбцы с TEXT и BLOB данными в бинарных логах и реплицируются ли они. Значение по умолчанию для нее — full. Обычно все нормально, но одно приложение создало идеальный шторм, задействовав сразу все следующие атрибуты:  использование MySQL в качестве очереди;  огромные значения BLOB;  большая нагрузка на запись;  высокая пропускная способность. Сочетание этих паттернов доступа привело к слабому потоку репликации данных, вызывая значительное запаздывание репликации. Решением было заменить var.binlog_row_image на noblob, чтобы прекратить репликацию тех значений BLOB, которые не нуждались в репликации. Эта правдивая история подводит нас к следующему спектру: репликации. Репликация Запаздывание — это бич репликации: задержка между моментом записи в исходном экземпляре MySQL и моментом, когда эта запись применяется к экземпляру
218 | Глава 6 реплики MySQL. Когда репликация (и сеть) работает нормально, задержка репликации составляет менее секунды и ограничивается только задержкой сети. До MySQL 8.0.22 метрика запаздывания репликации и соответствующая команда выражались как Seconds_Behind_Master и SHOW SLAVE STATUS. Начиная с версии MySQL 8.0.22, это Seconds_Behind_Source и SHOW REPLICA STATUS. В этой книге я использую современную метрику и команду. В MySQL есть печально известная метрика-датчик запаздывания репликации: Seconds_Behind_Source. Печально известная, потому что она и не ошибочна, но и не будет соответствовать вашим ожиданиям. Она может прыгать от нуля до высокого значения, что забавно, но сбивает с толку. Следовательно, лучше всего игнорировать эту метрику и вместо этого использовать pt-heartbeat (https://oreil.ly/VMg4c) для измерения истинного запаздывания репликации. Затем вам необходимо настроить свое программное обеспечение (или сервис) мониторинга MySQL так, чтобы оно при помощи pt-heartbeat измеряло запаздывание репликации и выводило отчет об этом. Поскольку pt-heartbeat существует уже давно, некоторые средства мониторинга MySQL изначально его поддерживают, и есть большая вероятность, что программисты, которые управляют вашим экземпляром MySQL, его уже используют. MySQL также предоставляет одну метрику, связанную с репликацией, что к печально известным не относится: Binlog_cache_disk_use. В главе 7 она объяснена подробнее; а пока будет достаточно взгляда с общего уровня. Для каждого клиентского подключения кеш бинарных логов, расположенный в памяти, буферизует операции записи перед тем, как они будут записаны в бинарные файлы логов, откуда операции записи реплицируются в реплики. Если кеш бинарного лога слишком мал, чтобы вместить все операции записи для транзакции, изменения записываются на диск, а параметр Binlog_cache_disk_use растет. В умеренных количествах это приемлемо, но не должно происходить часто. Если же это случается часто, вы можете улучшить ситуацию, увеличив размер кеша бинарного лога: var.bin log_cache_size (https://oreil.ly/0TEIJ). Из примера в предыдущем разделе вы знаете, что var.binlog_row_image также влияет на кеш бинарного лога: образы полных строк могут занимать много места, если в таблице есть столбцы BLOB или TEXT. Объем данных В главе 3 объясняется, почему меньше данных — больше производительность. Мониторинг объема данных важен, поскольку базы данных часто становятся объемнее, чем ожидалось. Если рост данных обусловлен ростом приложения (оно становится все более популярным), то это приятная проблема, но тем не менее это проблема. Это легко упустить из виду еще и потому, что производительность MySQL легко масштабируется по мере роста данных, но не бесконечно. Объем базы данных может вырасти с 10 до 300 Гбайт (в 30 раз), и при этом не возникнет проблем с производительностью, если запросы и паттерны доступа хорошо оптимизирова-
Метрики сервера | 219 ны. Но еще одно 30-кратное увеличение до 9 Тбайт? Это невозможно. Даже в 3 раза, до 900 Гбайт — требует слишком многого: оно может произойти, если паттерны доступа будут исключительно удачными, но особенно на это не рассчитывайте. MySQL предоставляет размеры таблиц (и другие метаданные таблиц) в таблице схемы информации: information_schema.tables (https://oreil.ly/PqATu). Запрос в примере 6.5 возвращает размер (объем) каждой базы данных в гигабайтах. Пример 6.5. Объемы баз данных (в Гбайт) SELECT table_schema AS db, ROUND(SUM(data_length + index_length) / 1073741824 , 2) AS 'size_GB' FROM information_schema.tables GROUP BY table_schema; Запрос в примере 6.6 возвращает объем каждой таблицы в гигабайтах. Пример 6.6. Объемы таблиц (в Гбайт) SELECT table_schema AS db, table_name as tbl, ROUND((data_length + index_length) / 1073741824 , 2) AS 'size_GB' FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_schema != 'performance_schema'; Единого стандарта для значений метрик размера базы данных и таблицы не существует. Запрашивайте и агрегируйте значения из information_schema.tables в соответствии с вашими потребностями. Собирайте данные о размерах базы (пример 6.5) как минимум каждый час. В случае с размерами таблиц лучше быть более точным и собирать их каждые 15 минут. Убедитесь, что, где бы вы ни держали или куда бы ни отправляли метрики MySQL, вы можете хранить метрики объема данных хотя бы в течение года. Краткосрочные тенденции роста объема данных важны, чтобы рассчитать момент, когда на диске закончится свободное пространство или потребуется больше места в облаке. Долгосрочный анализ тенденций роста данных используется для оценки того, когда понадобится шардирование (см. гл. 5, раздел «Практика: четырехлетняя пригодность»).
220 | Глава 6 InnoDB InnoDB (https://oreil.ly/4b5qP) — это сложно. Однако, поскольку это подсистема хранения данных MySQL по умолчанию, мы должны собраться с духом и смириться с этим. В глубоком погруженим нет необходимости, да оно и не предусмотрено в рамках этой книги. Хотя этот раздел длинный, он лишь поверхностно затрагивает вопросы внутренней работы InnoDB. Тем не менее следующие метрики InnoDB раскрывают некоторые внутренние процессы подсистемы хранения, отвечающие за чтение и запись данных. Длина списка истории (метрика) Длина списка истории (англ. history list length, HLL) — любопытная метрика, потому что каждый программист, использующий MySQL, знает, что это значит, но очень немногие знают, что же это такое. Когда HLL существенно возрастает за несколько минут или часов, это значит, что InnoDB сохраняет значительное количество старых версий строк вместо их удаления, потому что одна или несколько длительных транзакций не были зафиксированы или были отменены без отката в связи с необнаруженной потерей клиентского соединения. Все это, о чем я расскажу позже в разделе «Длина списка истории» (см. гл. 8), раскрывается с помощью одной метрики-датчика:  innodb.trx_rseg_history_len Нормальное значение для innodb.trx_rseg_history_len — меньше 1000. Вы должны отслеживать его и предупреждать, если оно превысит 100 000. В отличие от того, что вы увидите в разделах «Охота на миражи (пороговые значения)» и «Оповещения о пользовательском опыте и объективных ограничениях» далее в этой главе, это надежное пороговое значение и действенное предупреждение. Ваши действия: найдите и остановите длительную или заброшенную транзакцию. Длина списка истории не влияет напрямую на производительность, но это предвестник неприятностей — не игнорируйте его. Проблема связана с тем фактом, что, поскольку InnoDB — подсистема хранения, основанная на транзакциях, каждый запрос в таблице InnoDB представляет собой транзакцию. Транзакции сопряжены с расходами ресурса, и метрика HLL показывает, когда длительная или заброшенная транзакция приводит к тому, что InnoDB задействует неоправданный объем ресурса. Иногда расходы ресурса необходимы (и даже полезны), но слишком большие затраты приводят к потерям, а потери противоречат производительности. О транзакциях и HLL можно сказать еще так много, что они заслужили отдельную главу: номер 8. А пока давайте сосредоточимся на метриках, потому что мы только начали говорить об InnoDB. Взаимоблокировка Взаимоблокировка возникает, когда две (или более) транзакции удерживают блокировку строк, которые необходимы другой транзакции. Например, транзакция A удерживает блокировку строки 1 и нуждается в блокировке строки 2, а транзакция B удерживает блокировку строки 2 и нуждается в блокировке строки 1.
Метрики сервера | 221 MySQL автоматически обнаруживает и откатывает одну транзакцию, чтобы снять взаимоблокировку, и приращивает значение одной метрики:  innodb.lock_deadlocks Взаимоблокировок быть не должно. Их распространенность связана с характеристикой конкурентности паттерна доступа (см. раздел «Конкурентность», гл. 4). Высококонкурентный доступ к данным должен быть спроектирован (в приложении) таким образом, чтобы избежать взаимоблокировок, гарантируя, что разные транзакции, которые обращаются к одним и тем же или соседним строкам, проверяют строки примерно в одном и том же порядке. В предыдущем примере транзакция A и транзакция B обращаются к одним и тем же двум строкам во взаимно противоположном порядке, что может привести к взаимоблокировке, когда транзакции выполняются одновременно. Чтобы узнать больше, прочитайте раздел «Deadlocks in InnoDB» («Взаимоблокировки в InnoDB», https://oreil.ly/UpX0r) в руководстве по MySQL. Блокировка строк Метрики блокировки строк показывают конфликт блокировок: насколько быстро (или нет) запросы получают блокировку строк для записи данных. Наиболее значимы здесь:  innodb.lock_row_lock_time;  innodb.lock_row_lock_current_waits;  innodb.lock_row_lock_waits;  innodb.lock_timeouts. Первая метрика innodb.lock_row_lock_time — редкого типа: общее количество миллисекунд, затраченных на получение блокировки строк. Она относится к классу метрик времени отклика (см. выше раздел «Время отклика»), но, в отличие от «Времени отклика на запрос» (см. выше), данные этой метрики собираются как промежуточная сумма, а не как гистограмма. Следовательно, невозможно сообщить значение innodb.lock_row_lock_time в виде процентиля, что было бы идеально. И сообщать об этом как о скорости (см. выше, раздел «Скорость») бессмысленно: это миллисекунды в секунду. Вместо этого данная метрика должна быть представлена как разница: если есть 500 миллисекунд в момент времени T1 и 700 миллисекунд в момент времени T2, то сообщите значение T2 – значение T1 = 200 мс. (Используйте максимум для функции свертки диаграммы. Не усредняйте точки данных, потому что лучше увидеть наихудший вариант). Что касается метрики времени отклика, то чем она меньше, тем лучше. Но значение не может быть равно нулю (разве что рабочая нагрузка доступна только для операций чтения и никогда не нуждается в блокировке единичной строки), поскольку для получения блокировки требуется ненулевое количество времени. Цель, как всегда, в том, чтобы значение метрики было нормальным и стабильным. Иначе другие метрики блокировки строк также не будут нормальными. innodb.lock_row_lock_time
222 | Глава 6 — это метрика-датчик для текущего количества запросов, ожидающих получения блокировки строк. innodb.lock_row_lock_waits — это количество запросов, ожидающих получения строки. Эти две переменные, по сути, одинаковы: первая — текущий датчик, а вторая — исторический счетчик и показатель скорости (англ. rate). innodb.lock_row_lock_current_waits Когда скорость ожидания блокировки строк растет, это верный признак проблемы, потому что ожидания MySQL не случайны: что-то заставляет ее ждать. В этом случае причина — конкурентные запросы, которые обращаются к одним и тем же (или соседним) строкам. приращивается, когда истекает время ожидания блокироки строк. Тайм-аут ожидания блокировки строк по умолчанию составляет 50 секунд, настраивается с помощью var.innodb_lock_wait_timeout (https://oreil.ly/4kCLg) и применяется для каждой блокировки строки. Это слишком долгое время ожидания для любого обычного приложения, и я советую использовать значение поскромнее: 10 секунд или меньше. innodb.lock_timeouts Блокировка в InnoDB отличается сложностью и нюансами. Из-за этого конфликт блокировок не становится распространенной проблемой, если только рабочая нагрузка не имеет трех конкретных паттернов доступа:  большая нагрузка на запись (см. гл. 4, раздел «Чтение/запись»);  высокая пропускная способность (см. гл. 4, раздел «Пропускная способность»);  высокий уровень конкурентности (см. гл. 4, раздел «Конкурентность»). Но это было бы очень специфическое приложение и специфическая рабочая нагрузка. Однако конфликт блокировок может стать проблемой для любого приложения и любой рабочей нагрузки (даже при низкой пропускной способности и конкурентности), поэтому всегда отслеживайте метрики блокировки. Пропускная способность данных Пропускная способность данных в байтах в секунду измеряется двумя метриками:  Innodb_data_read;  Innodb_data_writed. Пропускная способность данных редко является проблемой: SSD работает быстро; PCIe и NVMe добавили скорости. Несмотря на это, мониторинг данного параметра относится к лучшим практикам, поскольку пропускная способность хранилища ограничена, особенно в облаке. Не ждите, что значения метрик пропускной способности хранилища будут отвечать заявленным, поскольку заявленные значения замерены в идеальных условиях: данные поступают непосредственно на диск (или с него). InnoDB очень быстра и эффективна, но по-прежнему представляет собой сложный программный слой между данными и диском, который по своей сути препятствует достижению заявленных значений метрик пропускной способности хранилища.
Метрики сервера | 223 Будьте осторожны с пропускной способностью в облаке: хранилище вряд ли будет подключено локально, что ограничивает пропускную способность скоростью сети. 1 Гбит/с эквивалентен 125 Мбит/с, что соответствует пропускной способности вращающихся дисков. IOPS InnoDB имеет глубокую и порой сложную взаимосвязь с емкостью ввода-вывода хранилища, измеряемой в IOPS (англ. Input/Output Operations Per Second, количество операций ввода/вывода в секунду). Но сначала самая простая часть: операции ввода-вывода на чтение и запись в InnoDB подсчитываются по двум метрикам соответственно:  innodb.os_data_reads;  innodb.os_data_writes. Эти метрики являются счетчиками, поэтому, как и другие счетчики, они преобразуются и выражаются в виде скоростей с помощью систем построения графиков метрик. Обязательно установите графический модуль в IOPS для каждого из них. «Смысл жизни» для производительности InnoDB заключается в оптимизации и сокращении операций ввода-вывода в хранилище. Хотя высокие значения IOPS впечатляют с инженерной точки зрения, для производительности это бич, поскольку хранилище работает медленно. Но хранилище требуется для долговечности (постоянные изменения данных на диске), поэтому InnoDB прилагает все усилия, чтобы быть быстрой и долговечной. Следовательно, как говорилось и в разделе «Чем меньше QPS, тем лучше» (см. гл. 3), чем меньше операций ввода-вывода, тем лучше. Но не переборщите со снижением IOPS. Если ваша компания использует собственное оборудование, максимальное количество IOPS хранилища определяется устройством хранения (проверьте спецификации устройства или обратитесь к инженерам, которые управляют данным оборудованием). В облаке предусмотрено или предложено сколько-то IOPS хранилища, поэтому там определить максимальное значение обычно проще, ведь вы покупаете IOPS (проверьте настройки хранилища или обратитесь к поставщику облачных услуг). Например, если InnoDB никогда не использует более 2000 IOPS, то не покупайте (или не предоставляйте) 40 000 IOPS: InnoDB просто не будет задействовать избыток. Напротив, если InnoDB постоянно использует максимум IOPS хранилища, то либо надо оптимизировать рабочую нагрузку приложения для сокращения IOPS (см. главы 1–5), либо InnoDB действительно нуждается в большем количестве IOPS. Пропускная способность ввода-вывода InnoDB для фоновых задач в основном настраивается с помощью var.innodb_io_capacity (https://oreil.ly/zU6iW) и var.innodb_ io_capacity_max (https://oreil.ly/LiilY), двух системных переменных, которые по умолчанию равны 200 и 2000 IOPS соответственно. (Есть и другие переменные, но я не буду говорить о них подробно, чтобы сосредоточиться на метриках.) Чтобы узнать больше, прочитайте раздел «Configuring InnoDB I/O Capacity» («Настройка емкости ввода-вывода InnoDB», https://oreil.ly/G9Bcw) в руководстве по MySQL).
224 | Глава 6 Фоновые задачи включают в себя сброс страницы (page flushing), слияние буферов изменений и многое другое. В этой книге я рассматриваю только сброс страниц, который, возможно, является самой важной фоновой задачей. Ограничение вводавывода хранилища фоновых задач гарантирует, что InnoDB не станет перегружать сервер. Это также позволяет InnoDB оптимизировать и стабилизировать вводвывод хранилища, а не бомбардировать устройство хранения беспорядочными обращениями. В отличие от этого, приоритетные задачи не имеют каких-либо настраиваемых ограничений или мощностей ввода-вывода: они используют столько операций ввода-вывода, сколько необходимо и доступно. Основная приоритетная задача — выполнение запросов, но это не означает, что запросы много или чрезмерно используют операции ввода-вывода, потому что, как вы помните, «смысл жизни» для производительности InnoDB — оптимизировать и сократить IOPS хранилища. Для операций чтения пул буферов целенаправленно оптимизирует и сокращает операции ввода-вывода. При операциях записи алгоритмы сброса страниц и лог транзакций целенаправленно оптимизируют и сокращают объем операций ввода-вывода в хранилище. В следующих разделах показано, как это сделать. InnoDB может достичь высоких значений IOPS, но может ли приложение? Вероятно, нет, потому что между приложением и IOPS существует много уровней, которые не позволяют первому достичь большого количества последнего. По моему опыту, приложения используют от сотен до тысяч IOPS, а исключительно оптимизированные приложения, которые «становятся вирусными», — около 10 000 IOPS на одном экземпляре MySQL. Недавно я проводил эталонное тестирование MySQL в облаке и достиг потолка в размере 40 000 IOPS. Облачный провайдер заявляет 80 000 IOPS как максимум и разрешает мне достичь такого значения, но их система хранения ограничена 40 000 IOPS. Суть в том, что InnoDB способна достигать высоких IOPS, но все, что с ней связано, — не всегда. Миллионы операций ввода-вывода Высокопроизводительное хранилище способно поддерживать более миллиона операций ввода-вывода. Этот класс хранилища используется на физических серверах, предназначенных для размещения множества виртуальных серверов. То же самое верно и для высокопроизводительных процессоров и памяти: слишком много оборудования для одного приложения. Это было лишь первичное знакомство с операциями ввода-вывода InnoDB, поскольку они лежат в основе последних трех спектров InnoDB, которые задействуют IOPS: эффективность буферного пула, сброс страниц и лог транзакций (все это будет ниже в соответствующих разделах данной главы). Чтобы узнать больше о вводе-выводе InnoDB, начните с раздела «Configuring InnoDB I/O» («Настройка емкости ввода-вывода InnoDB», https://oreil.ly/w9MOg) в руководстве по MySQL. Чтобы по-настоящему погрузиться в мельчайшие подробности этой темы, прочтите поучительный пост из трех частей в блоге известных экспертов по MySQL — Ива Трюдо и Франциско Борденаве: «Give Love to Your SSDs: Reduce innodb_io_capacity_max!» («С любовью относитесь к своим твердо-
Метрики сервера | 225 тельным накопителям: уменьшите innodb_io_capacity_max!», https://oreil.ly/q0L61), «InnoDB Flushing in Action for Percona Server for MySQL» («Сброс в InnoDB в действии для сервера Percona для MySQL», https://oreil.ly/ZY2Xe) и «Tuning MySQL/InnoDB Flushing for a Write-Intensive Workload» («Тюнинг MySQL/Сброс в InnoDB для рабочей нагрузки с большим количеством операций записи», https://oreil.ly/P03EX). Но сначала осильте эту главу, потому что она служит отличной подготовкой перед чтением этих постов. InnoDB работает с данными в памяти, а не на диске. Она считывает данные с диска, когда это необходимо, и записывает данные на диск, чтобы сделать изменения долговечными, но это операции более низкого уровня, в которые мы углубимся в следующих трех разделах. На более высоком уровне InnoDB работает с данными в памяти, поскольку использовать хранилище здесь — это слишком медленно даже при миллионе операций ввода-вывода. Следовательно, нет прямой корреляции между запросами, строками и IOPS. Записи всегда задействуют IOPS (для долговечности). Чтение может выполняться и без использования IOPS, но это зависит от эффективности пула буферов. Эффективность буферного пула Буферный пул InnoDB представляет собой кеш в памяти табличных данных и других внутренних структур данных. Из раздела «Таблицы InnoDB являются индексами» (см. гл. 2) вы знаете, что буферный пул содержит индексные страницы (подробнее о страницах в следующем разделе). InnoDB, конечно, понимает строки, но внутренне она гораздо больше связана со страницами. При такой глубине производительности MySQL фокус смещается со строк на страницы. На высоком уровне InnoDB получает доступ (на чтение и запись) ко всем данным по страницам в буферном пуле. (Низкоуровневые операции записи более сложны и рассматриваются ниже в разделе «Лог транзакций»). Если при обращении к данным их нет в буферном пуле, InnoDB считывает их из хранилища и сохраняет в буферный пул. Эффективность буферного пула — это процент доступа к данным из памяти, рассчитанный на основе двух показателей:  Innodb_buffer_pool_read_request;  Innodb_buffer_pool_reads. Innodb_buffer_pool_read_request подсчитывает число всех запросов на доступ к данным в буферном пуле. Если запрошенных данных нет в памяти, InnoDB увеличивает значение Innodb_buff fer_pool_reads и загружает данные с диска. Эффективность буферного пула равна: (Innodb_buffer_pool_read_request / Innodb_buffer_pool_reads) × 100. Слово «чтение» в этих метриках не означает SELECT. InnoDB считывает данные из буферного пула для всех запросов: INSERT, UPDATE, DELETE и SELECT. Например, при выполнении UPDATE InnoDB считывает строку из буферного пула. Если ее там нет, строка загружается туда с диска.
226 | Глава 6 Эффективность буферного пула будет очень низкой во время запуска MySQL. Это нормально; это называется холодным буферным пулом. Загрузка данных прогревает буферный пул (это все равно что подбрасывать поленья в огонь). Обычно для полного прогрева требуется несколько минут, и тогда эффективность буферного пула достигнет своего нормального и стабильного значения. Она должна быть чрезвычайно близка к 100% (в идеале 99,0% или выше), но не стоит зацикливаться на этом значении. Технически этот показатель отражает долю попадания в кеш, но используется не так. Доля попадания в кеш находится слегка за пределами этой метрики, которая сообщает лишь, кешируются ли значения. Напротив, эффективность буферного пула показывает, насколько хорошо InnoDB способна хранить часто используемые данные (рабочий набор) в памяти, при этом соблюдая баланс между быстротой и долговечностью. Образно говоря, эффективность пула буферов — это то, насколько хорошо InnoDB может поддерживать спичку горящей во время урагана. Рабочий набор — это пламя; долговечность — это дождь (она снижает пропускную способность)5; приложение — это ветер. В далеком прошлом производительность приравнивалась к доле попадания в кеш. Сегодня это уже не так: производительность — это время отклика на запрос. Если эффективность буферного пула чрезвычайно низка, но время отклика хорошее, то производительность тоже будет хорошей. Вероятно, этого не произойдет, но не стоит терять концентрацию: вспомните раздел «Краеугольный камень» в главе 1. Если общий объем данных меньше доступной памяти, то все данные могут поместиться в пул буферов одновременно. (Размер буферного пула настраивается с помощью var.innodb_buffer_pool_size, https://oreil.ly/N4lnI). Или, начиная с MySQL 8.0.3, можно автоматически настроить размер буферного пула и другие связанные с ним системные переменные с помощью var.innodb_dedicated_server (https://oreil.ly/ I5KaC). В этом случае эффективность буферного пула проблем не доставит, а узкие места в производительности (если таковые имеются) будут относиться к процессору или хранилищу (поскольку все данные находятся в памяти). Но этот случай — исключение, а не норма. Норма — когда общий объем данных намного превышает доступную память. В этом (обычном) случае на эффективность буферного пула влияют три основных фактора: Доступ к данным Доступ к данным переносит данные в буферный пул. Возраст данных как характеристика паттерна доступа (см. гл. 4, раздел «Возраст данных») — один из основных факторов, поскольку только новые данные надо загружать в буферный пул. Сброс страницы Сброс страницы позволяет вытеснять данные из буферного пула. Сброс страницы необходим для загрузки новых данных в буферный пул. В следующем разделе мы рассмотрим это более подробно. 5 Вы можете отключить меры обеспечения долговечности, но это очень плохая идея.
Метрики сервера | 227 Доступная память Чем больше данных InnoDB хранит в памяти, тем меньше ей нужно загружать или удалять данные. В исключительном случае, упомянутом ранее, когда все данные в память помещаются, с эффективностью буферного пула проблем не будет. Эффективность буферного пула раскрывает совокупный эффект этих трех факторов. Из-за того, что он совокупный, трудно бывает понять, в чем именно дело. Если значение ниже нормы, причиной может быть один, два или все три фактора. Нужно проанализировать все три, чтобы определить, какой важнее всего или наиболее доступен для изменения. Например, как подробно описано в главе 4, изменение паттернов доступа относится к лучшим практикам для повышения производительности, но если вы неплохо разбираетесь в производительности MySQL, то, вероятно и так уже это сделали. В таком случае увеличение объема памяти или получение более быстрого хранилища (больше операций ввода-вывода) может быть реальнее и разумнее, поскольку вы уже оптимизировали рабочую нагрузку. Хотя эффективность буферного пула не может дать вам ответы, она подскажет вам, где искать: паттерны доступа (особенно раздел «Возраст данных», см. гл. 4), сброс страницы и размер памяти. Эффективность буферного пула InnoDB — это верхушка айсберга. Под ней скрывается сброс страниц как внутренний механизм, который удерживает его на плаву. Сброс страниц Этот спектр обширен и сложен, поэтому здесь есть дополнительные подразделы «Страницы» и «Сброс страниц», которые неразрывно связаны. Страницы. Как упоминалось выше, буферный пул содержит индексные страницы. Существует четыре типа страниц: 1. Свободные страницы (англ. free pages). Они не содержат данных; InnoDB может загружать туда новые данные. 2. Страницы данных (англ. data pages). Они содержат данные, которые не были изменены; их также называют чистыми страницами (англ. clean pages). 3. «Грязные» страницы (англ. dirty pages). Они содержат измененные данные, которые не были сброшены на диск. 4. Смешанные страницы (англ. misc pages). Они содержат смешанные внутренние данные, не описанные в этой книге. Поскольку InnoDB держит буферный пул заполненным данными, нет необходимости отслеживать количество страниц данных. Свободные и «грязные» страницы наиболее показательны с точки зрения производительности, особенно при просмотре с использованием метрик сброса из следующего раздела. Три датчика и один счетчик (последняя метрика) показывают, сколько свободных и «грязных» страниц находится в буферном пуле:
228 | Глава 6  innodb.buffer_pool_pages_total;  innodb.buffer_pool_pages_dirty;  innodb.buffer_pool_pages_free; • innodb.buffer_pool_wait_free. innodb.buffer_pool_pages_total — это общее количество страниц в буферном пуле, которое зависит от размера буферного пула (var.innodb_ buffer_pool_size, https://oreil.ly/fXHQ4). (Технически это метрика-датчик, потому что, начиная с MySQL 5.7.5, размер буферного пула InnoDB является динамическим. Но частое изменение размера буферного пула не слишком нормально, поскольку этот размер зависит от системной памяти, которая не может изменяться быстро: даже облачным экземплярам требуется несколько минут на изменение размера.) Эта метрика вычисляет процент свободных и «грязных» страниц: innodb.buffer_pool_pages_free и innodb.buffer_pool_pages_dirty, деленные на общее количество страниц соответственно. Обе процентные величины являются метриками-датчиками, и значения часто меняются из-за сброса страниц. Чтобы гарантировать доступность свободных страниц, когда это необходимо, InnoDB поддерживает ненулевой баланс свободных страниц, который я называю целевым количеством свободных страниц (англ. free pages target). Целевое количество свободных страниц равно произведению двух системных переменных: var.innodb_lru_scan_depth (https://oreil.ly/TG9hj), умноженной на var.innodb_buffer_ pool_instances (https://oreil.ly/srIHw). Имя первой системной переменной несколько вводит в заблуждение, но она задает количество свободных страниц, которое InnoDB поддерживает в каждом экземпляре буферного пула; по умолчанию это 1024 свободные страницы. До сих пор я писал о буферном пуле как об одной логической части InnoDB. Однако по факту он разделен на несколько экземпляров буферного пула, каждый со своими собственными внутренними структурами данных — для уменьшения конфликтов при большой нагрузке. По умолчанию var.innodb_buffer_pool_instances равно 8 (или 1, если размер буферного пула меньше 1 Гбайт). Таким образом, при значениях по умолчанию для обеих системных переменных InnoDB поддерживает 1024 × 8 = = 8192 свободные страницы. Это число должно примерно соответствовать целевому количеству свободных страниц. Уменьшение var.innodb_lru_scan_depth относится к лучшим практикам, поскольку при значениях по умолчанию это дает 134 Мбайт свободных страницы: 8192 свободные страницы × 16 Кб/страница = 134 Мбайт. Это слишком много, учитывая, что строки обычно составляют сотни байт. Будет более эффективно, если число свободных страниц станет как можно меньше, но не достигая нуля (иначе возникнет ожидание свободных страниц, подробности ниже). Это полезно знать, но уже относится к тюнингу MySQL, что выходит за рамки этой книги. Значение по умолчанию не влияет на производительность; эксперты MySQL просто ненавидят неэффективность. Если количество свободных страниц постоянно близко к нулю (ниже целевого количества свободных страниц), это допустимо до тех пор, пока innodb.buffer_
Метрики сервера | 229 равно нулю. Когда InnoDB нужна свободная страница, но ни одна из них не доступна, она приращивает innodb.buffer_pool_wait_free и ожидает. Это называется ожиданием свободной страницы, и должно случаться исключительно редко (даже когда пул буферов заполнен данными), потому что InnoDB активно поддерживает целевое количество свободных страниц, но при очень большой нагрузке может быть не в состоянии достаточно быстро сбрасывать и освобождать страницы. Проще говоря, InnoDB считывает новые данные быстрее, чем может сбрасывать старые данные. Если предположить, что рабочая нагрузка уже оптимизирована, существуют три решения для ожиданий свободной страницы: pool_wait_free Увеличение целевого количество свободных страниц Если ваше хранилище может обеспечить больше IOPS (или вы можете предоставить больше IOPS в облаке), то увеличение var.innodb_lru_scan_depth заставит InnoDB сбрасывать и освобождать больше страниц, что потребует больше операций ввода-вывода (см. выше, раздел «IOPS» ). Улучшенная система хранения данных Если ваше хранилище не может обеспечить больше IOPS, обновите его до более качественного хранилища, а затем увеличьте целевое количество свободных страниц. Больше памяти Чем больше памяти, тем больше пул буферов, и тем больше страниц может поместиться в памяти без необходимости сбрасывать и вытеснять старые страницы для загрузки новых. Есть еще один нюанс, связанный с ожиданием свободной страницы, который я проясню позднее, когда буду рассказывать о сбросе LRU. Вспомните раздел «Эффективность буферного пула» выше в этой главе: чтение не означает SELECT. InnoDB считывает новые данные из буферного пула для всех запросов: INSERT, UPDATE, DELETE и SELECT. Когда данные доступны, но находятся не в буферном пуле (в памяти), InnoDB считывает их с диска. Если количество свободных страниц постоянно намного превышает количество целевых свободных страниц или никогда не уменьшается до целевого значения, то размер буферного пула слишком велик. Например, 50 Гбайт данных занимают только 39% от 128 Гбайт оперативной памяти. MySQL оптимизирована так, чтобы она использовала лишь необходимую ей память, поэтому предоставление ей избыточной памяти не увеличит производительность: MySQL просто не будет использовать избыток. Не тратьте память впустую. Доля «грязных» страниц в общем количестве страниц по умолчанию колеблется от 10 до 90%. Хотя «грязные» страницы содержат измененные данные, которые не были сброшены на диск, изменения данных сбросываются на диск в лог транзакций (подробнее об этом в следующих двух разделах). Даже при наличии 90% «грязных» страниц все изменения данных гарантированно сохраняются на диске. Это совершенно нормально — иметь высокий процент «грязных» страниц. На самом деле, это даже ожидаемо, если только рабочая нагрузка не слишком перегружена операциями чтения (вспомните характеристику паттерна доступа чтение/запись
230 | Глава 6 (см. гл. 4, раздел «Чтение/запись»)) и просто очень не часто изменяет данные. (В этом случае я бы подумал, не лучше ли применить для рабочей нагрузки другое хранилище данных.) Поскольку высокий процент «грязных» страниц — нечто ожидаемое, эта метрика используется для подтверждения других метрик, связанных со сбросом страниц (следующий раздел), логом транзакций (см. ниже) и дисковым вводом-выводом (см. выше, раздел «IOPS»). Например, операции записи данных приводят к «загрязнению» страниц, поэтому всплеск количества «грязных» страниц подтверждает всплеск числа операций ввода-вывода и метрик лога транзакций. Но всплеск операций ввода-вывода без соответствующего всплеска количества «грязных» страниц не может быть вызван операциями записи; должно быть, это другая проблема (возможно, программист вручную выполнил какой-то специальный запрос, что извлек массу старых данных, которые не видели белого света целую вечность, и теперь InnoDB считывает их с диска вихрем операций ввода-вывода). В конечном счете количество «грязных» страниц поднимается и опускается вместе с приливными волнами сброса страниц. Сброс страниц. Сброс страниц очищает «грязные» страницы, записывая изменения данных на диск. Сброс страниц служит трем тесно связанным целям: долговечность, установка контрольной точки и вытеснение страницы (англ. page eviction). Для простоты в этом разделе основное внимание уделяется сбросу страниц в отношении вытеснения. Раздел «Лог транзакций» (см. ниже) разъясняет, как сброс страницы обеспечивает долговечность и контрольные точки. Из раздела «Эффективность буферного пула» (см. выше) вы знаете, что сброс страницы освобождает место для загрузки новых данных в буферный пул. Говоря более конкретно, сброс страниц делает «грязные» страницы чистыми, а чистые страницы могут быть вытеснены из буферного пула. Таким образом, круг жизни страницы завершен: 1. Свободная страница становится чистой страницей (с данными) при загрузке данных. 2. Чистая страница становится «грязной» страницей, когда эти данные изменяются. 3. «Грязная» страница снова становится чистой страницей, когда изменения данных сбрасываются. 4. Чистая страница снова становится свободной страницей, когда вытесняется из буферного пула. Реализация очистки страниц сложна и варьируется в зависимости от дистрибутивов (Oracle MySQL, Percona Server и MariaDB Server), поэтому вам может потребоваться перечитать следующий раздел, чтобы полностью во всем разобраться. На рис. 6.6 показаны высокоуровневые компоненты и процесс сброса страниц InnoDB от фиксации транзакций в логе транзакций (вверху) до сброса и вытеснения страниц из буферного пула (внизу). Образно говоря, сброс страниц в InnoDB работает сверху вниз, как показано на рис. 6.6, но я буду объяснять это снизу вверх. В буферном пуле «грязные» стра-
Метрики сервера | 231 Рис. 6.6. Сброс страниц в InnoDB ницы показаны темными, чистые (с данными) страницы — белыми, а свободные страницы имеют пунктирный контур. «Грязные» страницы записываются в два внутренних списка (для каждого экземпляра буферного пула): Список сброса «Грязные» страницы из операций записи, зафиксированных в логе транзакций. Список LRU Чистые и «грязные» страницы в буферном пуле, упорядоченные по возрасту данных. Строго говоря, список LRU отслеживает все страницы с данными, и туда просто случайно включаются «грязные» страницы; в то время как список сброса четко отслеживает именно «грязные» страницы. В любом случае MySQL использует оба списка для поиска «грязных» страниц для сброса. На рис. 6.6 список LRU подключен к (отслеживанию) только одной «грязной» страницы, но это сделано для простоты во избежание нагромождения строк.
232 | Глава 6 Раз в секунду «грязные» страницы удаляются из обоих списков фоновыми потоками, подходяще названными потоками очистки страниц (англ. page cleaner threads). По умолчанию InnoDB использует четыре потока очистки страниц, настраиваемых с помощью var.innodb_page_cleaners (https://oreil.ly/ELUoy). Каждый поток сбрасывает оба списка; но для простоты на рис. 6.6 показано, как один поток сбрасывает один список. За сброс списка очистки и списка LRU в основном отвечают два алгоритма: Адаптивный сброс Адаптивный сброс определяет скорость, с которой потоки очистки страниц сбрасывают «грязные» страницы из списка сброса. Этот алгоритм был создан в 2008 году известным экспертом по MySQL Ясуфуми Киношитой (англ. Yasufumi Kinoshita) во время работы в Percona. Прочитайте пост «Adaptive checkpointing» (Адаптивная контрольная точка, https://oreil.ly/8QG6X) в его блоге. Алгоритм является адаптивным, поскольку изменяет скорость сброса страниц в зависимости от скорости записи в лог транзакций. Более быстрая запись означает более быстрый сброс страниц. Алгоритм реагирует на нагрузку по операциям записи, но он также точно настроен для обеспечения стабильной скорости сброса страниц при различных нагрузках записи. Сброс страниц потоками очистки относится к фоновым задачам, поэтому скорость здесь ограничена настройками пропускной способности ввода-вывода InnoDB (см. выше раздел «IOPS»), в частности var.innodb_io_capacity и var.innodb_io_capacity_max. Адаптивный сброс проводит потрясающую работу по поддержанию скорости сброса (в пересчете на IOPS) между этими двумя значениями. Цель адаптивного сброса состоит в том, чтобы позволить контрольным точкам освободить место в логах транзакций. (На самом деле это и есть цель сброса списка в целом; алгоритмы — это просто разные методы ее достижения.) Я говорю про контрольные точки в разделе «Лог транзакций» (см. ниже), но упомяну об этом и тут, чтобы прояснить: хотя сброс очищает страницы и делает кандидатами на вытеснение, не это является целью адаптивного сброса. Тонкости алгоритма адаптивного сброса выходят за рамки этой книги. Важный момент: адаптивный сброс убирает «грязные» страницы из списка сброса в ответ на (операции) записи в логе транзакций. Сброс LRU Сброс LRU удаляет «грязные» страницы, начиная с конца списка LRU, где содержатся самые старые страницы. Проще говоря, сброс LRU сбрасывает и вытесняет старые страницы из буферного пула. Сброс LRU происходит как в приоритетном, так и в фоновом режиме. Сброс приоритетных LRU бывает, когда пользовательскому потоку (потоку, выполняющему запрос) нужна свободная страница, но ее нет. Это плохо сказывается на производительности, потому что приводит к ожиданию: оно увеличивает время отклика на запрос. В такой ситуации MySQL приращивает innodb.buffer_
Метрики сервера | 233 — тот самый «нюанс, связанный с ожиданием свободной страницы», который я обещал прояснить в этом разделе. Потоки очистки страниц обрабатывают фоновый сброс LRU (поскольку сами являются фоновыми). Когда поток очистки страниц сбрасывает «грязную» страницу из списка LRU, он также освобождает ее, добавляя в список свободных. InnoDB поддерживает целевое количество свободных страниц (см. выше раздел «Страницы»,) и избегает ожидания свободной страницы в основном таким образом. Хотя фоновый сброс LRU — фоновая задача, он не ограничен описанными настройками емкости ввода-вывода InnoDB (var.innodb_io_capacity и var.innodb_io_ capacity_max)6. Однако такое ограничение отлично задается (для каждого экземпляра буферного пула) при помощи var.innodb_lru_scan_depth (https://oreil.ly/ fGGjJ). По разным причинам, которые я освещать не буду, это не вызывает проблемы чрезмерности ввода-вывода фонового хранилища. Целью сброса LRU является сброс и освобождение (вытеснение) самых старых страниц. Большой возраст, как подробно описано в разделе «Возраст данных» (см. гл. 4), означает наименее недавно (т. е. наиболее давно) использованные страницы, отсюда аббревиатура LRU (англ. least recently used). Тонкости сброса LRU, списка LRU и того, как все это связано с буферным пулом, также выходят за рамки этой книги; но, если вам интересно, начните с раздела «Buffer Pool» («Буферный пул», https://oreil.ly/OyBeI) в руководстве по MySQL. Важно, что сброс LRU освобождает страницы, и его максимальная скорость равна целевому количеству свободных страниц (в секунду), а не заданной пропускной способности ввода-вывода InnoDB. pool_wait_free Сброс в режиме ожидания и устаревший сброс В дополнение к адаптивной очистке и сбросу LRU существуют еще два алгоритма сброса: сброс в режиме ожидания и устаревший сброс. Сброс в режиме ожидания происходит, когда InnoDB не обрабатывает никаких операций записи (в лог транзакций ничего не записывается). В этой редкой ситуации InnoDB сбрасывает «грязные» страницы из списка сброса с заданной пропускной способностью ввода-вывода (см. выше, раздел «IOPS»). Сброс в режиме ожидания также сбрасывает буфер изменений (https://oreil.ly/uKb08) и обрабатывает сброс во время завершения работы MySQL. Устаревший сброс — это мой термин для простого алгоритма, который использовался в InnoDB до того, как адаптивный сброс стал стандартом7. InnoDB сбрасывает «грязные» страницы, когда их процент находится между var.innodb_max_dirty_pages_pct_lwm (https://oreil.ly/OSqCZ) и var.innodb_max_dirty_pages_pct (https://oreil.ly/zsCWL). Хотя этот алгоритм все еще присутствует в MySQL 8.0, он, по сути, никогда не используется, и вы можете не обращать на него внимания. 6 Чтобы убедиться в этом и глубже погрузиться в данную тему, прочтите пост «MySQL LRU Flushing and I/O Capacity» («Очистка MySQL LRU и пропускная способность ввода-вывода», https://oreil.ly/YHEcj) в моем блоге. 7 Устаревший сброс также называют процентным сбросом «грязных» страниц, но я предпочитаю свой термин, потому что он проще и точнее формулирует суть: словосочетание «устаревший сброс» подразумевает, что это больше не актуально; так оно и есть.
234 | Глава 6 Благодаря этому ускоренному курсу по сбросу в InnoDB следующие четыре метрики теперь должны быть вам понятны:  innodb.buffer_flush_batch_total_pages;  innodb.buffer_flush_adaptive_total_pages;  innodb.buffer_LRU_batch_flush_total_pages;  innodb.buffer_flush_background_total_pages. Все четыре метрики представляют собой счетчики, которые при преобразовании в метрики скорости показывают скорость сброса страниц для каждого алгоритма. innodb.buffer_flush_batch_total_pages — это общая скорость сброса страниц для всех алгоритмов. Это высокоуровневая метрика, которая полезна в качестве KPI для InnoDB: общая скорость сброса страниц должна быть нормальной и стабильной. Если нет, то одна из метрик покажет, какая часть InnoDB не сбрасывется нормально. innodb.buffer_flush_adaptive_total_pages — это количество страниц, сброшенных адаптивным сбросом. innodb.buffer_LRU_batch_flush_total_pages — количество стра- ниц, сброшенных при фоновом сбросе LRU. Исходя из информации выше, вы знаете, какие части InnoDB отражают эти алгоритмы: лог транзакций и свободные страницы соответственно. включен для полноты картины: это число страниц, сброшенных другими алгоритмами, описанными выше (см. раздел «Сброс в режиме ожидания и устаревший сброс»). Если скорость фонового сброса страниц вызывает проблемы, вам нужно проконсультироваться с экспертом по MySQL, потому что подобного быть не должно. innodb.buffer_flush_background_total_pages Хотя разные алгоритмы сброса имеют разную скорость, система хранения данных использует их все, поскольку для сброса требуется IOPS. Например, если вы используете MySQL на вращающихся дисках, система хранения (как шина хранения, так и запоминающее устройство) просто не обеспечивают большого IOPS. Если вы запускаете MySQL в высокопроизводительном хранилище, вопросов с IOPS не будет. И если вы используете MySQL в облаке, вы можете раздобыть столько IOPS, сколько вам нужно, но облако использует подключенное к сети хранилище, а оно медленное. Также помните, что операции ввода-вывода имеют задержку (особенно в облаке) от микросекунд до миллисекунд. Это практически экспертные знания, но все же давайте продолжим: дальнейшее изучение этой темы стоит того. Лог транзакций Последний и, возможно, самый важный аспект: лог транзакций, также известный как лог обновлений (англ. redo log). Для краткости будем называть его логом, когда контекст ясен и недвусмыслен, как здесь. Лог транзакций гарантирует долговечность. Когда транзакция фиксируется, все изменения данных записываются в лог транзакций и сбрасываются на диск, что дела-
Метрики сервера | 235 ет изменения данных долговечными, а соответствующие «грязные» страницы остаются в памяти. (Если MySQL аварийно завершает работу с «грязными» страницами, изменения данных не теряются, потому что они уже были сброшены на диск в логе транзакций.) Сброс лога транзакций — это не сброс страницы. Эти два процесса разделены, но неразрывно связаны. Лог транзакций InnoDB представляет собой кольцевой буфер фиксированного размера на диске, как показано на рис. 6.7. По умолчанию он состоит из двух физических файлов лога. Размер каждого из них настраивается с помощью var.innodb_log_file_size (https://oreil.ly/ItAxz). Или, начиная с MySQL 8.0.3, включите функцию var.innodb_dediated_server (https://oreil.ly/gv38o), чтобы она автоматически настроила размер файла лога и другие связанные системные переменные. Лог транзакций содержит изменения данных (технически логи обновлений), а не страницы; но изменения данных связаны с «грязными» страницами в буферном пуле. Когда транзакция фиксируется, ее изменения данных записываются в «голову» (англ. head) лога транзакций (т. е. его начало) и сбрасываются (синхронизируются) на диск, который перемещает «голову» по часовой стрелке, а соответствующие «грязные» страницы добавляются в список сброса, показанный ранее на рис. 6.6. (На рис. 6.7 «голова» и «хвост» движутся по часовой стрелке, но это только иллюстрация. Если вы не используете вращающиеся диски, лог транзакций не двигается физически.) Вновь записанные изменения данных перезаписывают старые изменения данных, для которых были сброшены соответствующие страницы. Рис. 6.7. Лог транзакций InnoDB На упрощенной иллюстрации и при таком объяснении лог транзакций InnoDB выглядит сериализованным. Но это всего лишь упрощение. Фактическая низкоуровневая реализация очень конкурентна: многие пользовательские потоки параллельно фиксируют изменения в логе транзакций. Возраст контрольной точки — это длина лога транзакций (в байтах) между «головой» и «хвостом». Контрольная точка освобождает место в логе транзакций, сбрасывая «грязные» страницы из буферного пула, что позволяет «хвосту» продвигаться вперед. После сброса «грязных» страниц на место соответствующих изменений данных в логе транзакций могут быть записаны новые изменения данных. Адап-
236 | Глава 6 тивный сброс реализует контрольную точку в InnoDB, поэтому возраст контрольной точки является входным сигналом для алгоритма адаптивного сброса, показанного на рис. 6.6. По умолчанию все изменения данных (логи обновлений) в логе транзакций являются долговременными (сбрасываются на диск), но соответствующие «грязные» страницы в пуле буферов не являются долговечными, пока не будут сброшены с помощью установки контрольной точки. Контрольная точка продвигает «хвост», гарантируя, что ее возраст не станет слишком большим (тем более что это измеряется в байтах). Но что произойдет, если возраст контрольной точки станет настолько большим, что «голова» встретится с «хвостом»? Поскольку лог транзакций представляет собой кольцевой буфер фиксированного размера, головная часть может завернуться и встретиться с хвостовой частью, если скорость записи постоянно превышает скорость сброса. Но InnoDB не позволит этому случиться. Существуют две точки защиты, называемые асинхронизация (англ. async) и синхронизация (англ. sync), как показано на рис. 6.7. Асинхронизация — это точка, в которой InnoDB начинает асинхронный сброс: записи разрешены, но скорость сброса страницы увеличивается почти до максимума. Хотя запись и разрешена, сброс будет использовать так много емкости ввода-вывода InnoDB, что вы можете (и должны) ожидать заметного снижения общей производительности сервера. Синхронизация — это точка, в которой InnoDB начинает синхронный сброс: все записи прекращаются, и начинается сброс страницы. Излишне говорить, что для производительности это ужасно. InnoDB предоставляет метрики для возраста контрольной точки и точки асинхронного сброса соответственно:  innodb.log_lsn_checkpoint_age;  innodb.log_max_modified_age_async. innodb.log_lsn_checkpoint_age — метрика-датчик, измеряемая в байтах, но исходное значение бессмысленно для людей (оно варьируется от нуля до размера файла лога). Что имеет смысл для людей и важно для мониторинга — насколько близок возраст контрольной точки к точке асинхронного сброса, — этот параметр я называю объемом использования лога транзакций (англ. transaction log utilization): (innodb.log_lsn_checkpoint_age / innodb.log_max_modified_age_async) × 100. Точка асинхронного сброса составляет 6/8 (75%) от размера файла лога. Таким образом, при 100%-ном использовании лога транзакций 25% лога свободно для добавления новых записей, но помните: в точке асинхронного сброса производительность сервера заметно падает. Важно отслеживать и знать, когда эта точка будет достигнута. Если хотите рискнуть, InnoDB предоставляет метрику для точки синхронного сброса — на 7/8, (87,5%) от размера файла лога, которую вы можете заменить метрикой точки асинхронного сброса (или отслеживать обе): innodb.log_ max_modified_age_sync. Есть небольшая, но важная деталь, которая касается того, как запросы регистрируют изменения данных в логе транзакций: изменения данных сначала записываются
Метрики сервера | 237 в буфер лога в памяти (не путать с файлом лога, который ссылается на фактический лог транзакций на диске), затем буфер лога записывается в файл лога, и файл лога синхронизируется. Опущу уйму подробностей, но суть в том, что в памяти есть буфер лога. Если буфер лога слишком мал и запрос должен ждать свободного места, InnoDB приращивает метрику:  innodb.log_waits. Значение innodb.log_waits должно быть равно нулю. Если это не так, размер буфера лога настраивается с помощью var.innodb_log_buffer_size (https://oreil.ly/2I1cq). 16 Мбайт, выделенных по умолчанию, обычно более чем достаточно. Поскольку лог транзакций состоит из двух физических файлов на диске (два файла, но один логический лог), важнейшие задачи — запись и синхронизация изменений данных на диск. Две метрики-датчика сообщают, сколько из этих задач ждут выполнения:  innodb.os_log_pending_writes;  innodb.os_log_pending_fsyncs. Поскольку предполагается, что запись и синхронизация выполняются чрезвычайно быстро (от этого зависит почти вся производительность записи), эти метрики всегда должны быть равны нулю. Если они больше, это указывает на проблему низкого уровня либо с InnoDB, либо, что более вероятно, с системой хранения (предполагая, что показатели других метрик в норме или были в норме до того, как эти две приросли). Проблемы на этой глубине возникать не должны, но не забывайте отслеживать этот момент. И последняя, но не по значимости — простая, однако важная метрика подсчета количества байтов, записанных в лог транзакций:  innodb.os_log_bytes_writed. Рекомендуется отслеживать общее количество байтов лога, записываемых в час, в качестве основы для определения размера файла лога. Размер файла лога является произведением системных переменных var.innodb_log_file_size (https://oreil.ly/ sinUV) и var.innodb_log_files_in_group (https://oreil.ly/0hYp1). Или, начиная с MySQL 8.0.14, активируйте var.innodb_dedicated_server — эта опция автоматически настраивает обе системные переменные. По умолчанию размер файла лога составляет всего 96 Мбайт (два файла лога по 48 Мбайт каждый). Как инженер, использующий MySQL, а не администратор базы данных, я предполагаю, что тот, кто управляет вашей MySQL, правильно настроил эти системные переменные, но неплохо было бы проверить. Победа: мы дошли до конца раздела о метриках InnoDB. Их спектр намного шире и глубже, чем представлено здесь; я рассказал только о самых важных метриках InnoDB для анализа производительности MySQL. Кроме того, в InnoDB были внесены значительные изменения с MySQL 5.7 по 8.0. Например, в MySQL 8.0.11 внутренняя реализация лога транзакций уже переписана и улучшена. Есть и другие части InnoDB, которые здесь не рассматриваются: буфер двойной записи, буфер изменений, адаптивный хеш-индекс и т. д.
238 | Глава 6 Я призываю вас узнать больше о InnoDB, поскольку это потрясающая подсистема хранения данных. Вы можете начать с раздела «The InnoDB Storage Engine» («Подсистема хранения InnoDB», https://oreil.ly/s0PZk) в руководстве по MySQL. Мониторинг и оповещение Метрики MySQL раскрывают спектр производительности MySQL, а еще они отлично подходят для того, чтобы будить программистов посреди ночи. Эта функция известна как мониторинг и оповещение. Мониторинг и оповещение — внешние опции по отношению к MySQL, поэтому они не могут повлиять на ее производительность, но я вынужден рассмотреть следующие четыре темы, поскольку они связаны с метриками и важны для успешной работы с MySQL. Разрешение Разрешение (англ. resolution) означает частоту, с которой собираются и сообщаются метрики: 1 секунда, 10 секунд, 30 секунд, 5 минут и т. д. Более высокое разрешение влечет за собой более высокую частоту: 1 секунда — это более высокое разрешение, чем 30 секунд. Как и в телевизоре, чем выше разрешение, тем больше деталей вы видите. И поскольку «видеть — значит верить», давайте посмотрим на три диаграммы для одних и тех же данных за 30 секунд. На первой диаграмме, рис. 6.8, показаны значения QPS при максимальном разрешении: 1 секунда. Рис. 6.8. QPS с разрешением в 1 секунду В первые 20 секунд значение является нормальным и стабильным, колеблясь от 100 до 200 QPS. От 20 до 25 секунд происходит пятисекундное падение (5 точек данных ниже 100 QPS, выделено рамкой). В последние пять секунд QPS достигает аномально высокого значения, что часто бывает после падения. Ничего драматичного, но этот график реалистичен и начинает иллюстрировать то, что освещается следующими двумя графиками.
Метрики сервера | 239 Второй график, рис. 6.9, представляет собой точно такие же данные, но с разрешением в 5 секунд. При пятисекундном разрешении мелкие подробности отчасти теряются, но важные детали остаются: нормальное и стабильное значение QPS в первые 20 секунд; падение, приходящееся на момент около 25 секунд; и всплеск после замедления. Этот график подходит для ежедневного мониторинга, особенно учитывая, что сбор, хранение и построение графиков метрик с разрешением в 1 секунду настолько сложны, что так почти никогда не делается. Рис. 6.9. QPS с разрешением в 5 секунд Третий график, рис. 6.10, представляет собой точно такие же данные, но с разрешением в 10 секунд. При десятисекундном разрешении теряются почти все подробности. Согласно графику значение QPS стабильно и нормально, но на самом деле это не так: значение Рис. 6.10. QPS с разрешением 10 секунд
240 | Глава 6 QPS дестабилизировалось и не было нормальным в течение 10 секунд (пятисекундное падение и пятисекундный всплеск). Как минимум собирайте ключевые показатели производительности (см. выше раздел «Ключевые показатели производительности») с разрешением не менее 5 секунд. Если возможно, собирайте большую часть метрик из раздела «Спектры» (см. выше) также с разрешением в 5 секунд, за следующими исключениями: метрики администрирования, SHOW и плохой SELECT могут собираться медленно (через 10, 20 или 30 секунд), а объем данных может собираться совсем медленно (5, 10 или 20 минут). Стремитесь к максимально возможному разрешению, потому что, в отличие от метрик запросов, которые записываются в лог, метрики MySQL либо собираются, либо исчезают навсегда. Охота на миражи (пороговые значения) Пороговое значение — это статическое значение, при превышении которого срабатывает оповещение мониторинга, часто с отправкой сообщения дежурному специалисту. Пороговые значения выглядят хорошей и разумной идеей, но это не работает. Звучит категорично, но это ближе к истине, чем противоположное утверждение. Проблема в том, что пороговому значению также требуется задать продолжительность: как долго значение метрики должно оставаться выше порогового значения до запуска оповещения. Посмотрите на график на рис. 6.8 из предыдущего раздела (QPS с разрешением в 1 секунду). Без указания продолжительности пороговое значение «QPS менее 100» вызвало бы запуск оповещения семь раз за 30 секунд: пять секундных падений, а также третья и тринадцатая точки данных. На языке мониторинга и оповещения это будет «слишком шумно», так что же насчет порогового значения «QPS менее 50»? Конечно, падение значения на 50% (со 100 QPS до 50 QPS) сигнализирует о проблеме, о которой стоит знать специалисту. К сожалению, сигнала не будет: самая низкая точка данных составляет 50 QPS, что не составляет менее 50 QPS. Пример кажется надуманным, но это не так. Хуже того: предположим, вы добавили к условиям оповещения пятисекундную продолжительность и задали пороговое значение «QPS менее 100». Теперь оповещение срабатывает только после пятисекундного падения. Но что если падение не было падением? Что если произошел сбой в сети, который привел к потере пакетов в течение этих пяти секунд, так что проблема была не в MySQL и приложении? А бедный дежурный специалист, которому было отправлено оповещение, начал охоту на миражи. Понимаю, может показаться, что я подгоняю этот пример под свою точку зрения, но кроме шуток: пороговые значения крайне трудно сделать совершенными, где «совершенный» означает, что эти значения предупреждают только о действительно актуальных проблемах: никаких ложных срабатываний.
Метрики сервера | 241 Оповещения о пользовательском опыте и объективных ограничениях Есть два проверенных решения, которые работают вместо пороговых значений: 1. Оповещения о том, что происходит у пользователей. 2. Оповещения об объективных ограничениях. Как пояснялось в разделах «Краеугольный камень» (см. гл. 1) и «Ключевые показатели производительности» (см. выше), есть только две метрики MySQL, с которыми сталкиваются пользователи: время отклика и частота ошибок. Это сигналы надежны не только потому, что их видят пользователи, но и потому, что они не могут быть ложноположительными. Изменение QPS может быть вызвано нормальным изменением пользовательского трафика. Но изменение времени отклика может объясняться только изменением времени отклика. То же самое относится и к ошибкам. В случае с микросервисами пользователем может быть другое приложение. Тогда нормальное время отклика может быть очень малым (десятки миллисекунд), но принципы мониторинга и оповещения остаются теми же. Пороговые значения и продолжительность также проще использовать для времени отклика и ошибок, потому что мы можем легко представить себе ненормальные условия, когда превышаются пороговые значения. Например, предположим, что нормальное время отклика P99 для приложения составляет 200 миллисекунд, а нормальная частота ошибок составляет 0,5 в секунду. Если бы время отклика P99 увеличилось до 1 секунды (или более) за полную минуту, было бы это плохим пользовательским опытом? Если да, тогда установите эти значения в качестве порога и продолжительности. А если количество ошибок увеличится до 10 в секунду в течение полных 20 секунд, будет ли это плохим пользовательским опытом? Если да, тогда установите для порога и продолжительности уже эти значения. Для наглядности давайте проясним предыдущий пример, где 200 миллисекунд — это нормальное время отклика P99. Измеряйте и сообщайте о времени отклика P99 каждые пять секунд (см. выше, «Время отклика на запрос»). Создайте для метрики скользящее одноминутное оповещение, которое срабатывает, когда последние 12 значений превышают одну секунду. (Поскольку метрика сообщается каждые пять секунд, получается 60/5 секунд = 12 значений в минуту.) С технической точки зрения устойчивое увеличение времени ответа на запрос в 5 раз является значительным и заслуживает изучения (вероятно, это раннее предупреждение о том, что назревает более серьезная проблема, и, если ее игнорировать, это приведет к сбою приложения). Но цель оповещения скорее практическая, чем техническая: если пользователи привыкли к субсекундным ответам от приложения, то односекундные ответы — это заметно медленнее. Объективные ограничения — это минимальные или максимальные значения, которые MySQL не может обойти. Вот распространенные объективные ограничения, внешние по отношению к MySQL:
242 | Глава 6  ноль свободного места на диске;  ноль свободной памяти;  100%-ная загрузка процессора;  100%-ная нагрузка IOPS;  100%-ная загрузка сети. MySQL имеет много системных переменных max, но на приложения в основном влияют эти:  max_connections (https://oreil.ly/0ODxA);  max_prepared_stmt_count (https://oreil.ly/jqNuk);  max_allowed_packet (https://oreil.ly/qM3R5). Есть еще одно объективное ограничение, которое удивило не одного инженера: максимальное значение AUTO_INCREMENT (https://oreil.ly/tkXWP). MySQL не имеет собственной метрики или метода для проверки того, приближается ли значение в столбце AUTO_INCREMENT к максимальному. Распространенные решения для мониторинга MySQL создают для этого метрику, выполняя инструкцию SQL вроде той, что приводится в примере 6.7, написанном известным экспертом по MySQL Шломи Ноахом в статье «Checking for AUTO_INCREMENT capacity with single query» («Проверка емкости AUTO_INCREMENT с помощью всего одного запроса», https://oreil.ly/LJ64E). Пример 6.7. Инструкция SQL, которая проверяет максимальное значение AUTO_INCREMENT SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IF( LOCATE('unsigned', COLUMN_TYPE) > 0, 1, 0 ) AS IS_UNSIGNED, ( CASE DATA_TYPE WHEN 'tinyint' THEN 255 WHEN 'smallint' THEN 65535 WHEN 'mediumint' THEN 16777215 WHEN 'int' THEN 4294967295 WHEN 'bigint' THEN 18446744073709551615 END >> IF(LOCATE('unsigned', COLUMN_TYPE) > 0, 0, 1) ) AS MAX_VALUE,
Метрики сервера | 243 AUTO_INCREMENT, AUTO_INCREMENT / ( CASE DATA_TYPE WHEN 'tinyint' THEN 255 WHEN 'smallint' THEN 65535 WHEN 'mediumint' THEN 16777215 WHEN 'int' THEN 4294967295 WHEN 'bigint' THEN 18446744073709551615 END >> IF(LOCATE('unsigned', COLUMN_TYPE) > 0, 0, 1) ) AS AUTO_INCREMENT_RATIO FROM INFORMATION_SCHEMA.COLUMNS INNER JOIN INFORMATION_SCHEMA.TABLES USING (TABLE_SCHEMA, TABLE_NAME) WHERE TABLE_SCHEMA NOT IN ('mysql', 'INFORMATION_SCHEMA', 'performance_schema') AND EXTRA='auto_increment' ; А как насчет двух других ключевых показателей производительности: QPS и выполняющихся потоков? Их мониторинг относится к лучшим практикам, но оповещение о них — нет. Эти метрики имеют решающее значение при расследовании реальной проблемы, о которой сигнализируют время отклика или число ошибок, но в остальном они слишком сильно колеблются, чтобы считаться надежными сигналами. Если такой подход кажется слишком радикальным, помните: эти оповещения — для инженеров, использующих MySQL, а не для администраторов баз данных. Причина и следствие Скажу прямо: когда MySQL реагирует медленно, в подавляющем большинстве случаев (возможно, в 80%), по моему опыту, причина в приложении, потому что приложение — движущая сила для MySQL. Без приложения MySQL простаивает. Если дело не в приложении, есть несколько других распространенных причин низкой производительности MySQL. Пожалуй, в 10% случаев вероятным виновником будет другое приложение (любое, а не только MySQL), о чем я расскажу позже в разделе «Шумные соседи» (см. гл. 9). Аппаратное обеспечение, включая сеть, вызывает проблемы всего в 5% случаев, потому что современное оборудование достаточно надежно (особенно оборудование корпоративного уровня, которое служит дольше (и стоит дороже), чем потребительского). Последнее, в том числе по значимости: я оцениваю вероятность того, что MySQL сама является причиной своей медлительности, только в 1%. Определив причину, нужно удостовериться, что дело действительно в этом, что она — не побочный эффект какой-то предшествующей необнаруженной проблемы. Так, причины, связанные с приложением, могут включать плохо написанный запрос, который после развертывания в рабочей среде немедленно вызывает сбой
244 | Глава 6 MySQL. Аппаратные же причины могут включать в себя деградировавшую систему хранения данных, которая работает, но значительно медленнее, чем обычно, что приводит к медленному реагированию MySQL. Когда это предположение ложно (выявленная причина не является корневой), возникает особенно вредоносная ситуация. Рассмотрим последовательность событий: 1. Проблема с сетью, длящаяся 20 секунд, приводит к значительной потере пакетов или повторным попыткам подключения к сети низкого уровня. 2. Проблема с сетью вызывает ошибки запроса или истечение времени ожидания (из-за потери пакетов или повторных попыток подключения соответственно). 3. Как приложение, так и MySQL регистрируют ошибки (ошибки запроса и ошибки клиента соответственно). 4. Приложение повторяет запросы. 5. При повторной попытке выполнения старых запросов приложение продолжает выполнять новые запросы. 6. QPS увеличивается за счет выполнения и новых, и старых запросов. 7. Объем использования увеличивается за счет увеличения QPS. 8. Увеличение времени ожидания из-за увеличения объема использования. 9. Увеличение количества случаев истечения времени ожидания из-за увеличения времени ожидания. 10. Приложение снова повторяет запросы, что создает петлю обратной связи. К тому времени, когда вы попадаете в эту ситуацию, проблема становится очевидной, но первопричина — нет. Вам известно, что до возникновения проблемы все было нормально и стабильно: никаких изменений или развертываний приложений; ключевые показатели производительности MySQL были нормальными и стабильными; и администраторы баз данных подтверждают, что с их стороны не проводились никакие работы. Вот что делает эту ситуацию особенно сложной: насколько вы можете судить, этого никак не должно было произойти, но увы. С технической точки зрения любую причину можно выяснить, потому что компьютеры конечны и дискретны. Но практически это возможно лишь настолько, насколько позволяют мониторинг и логи. Если у вас исключительно четко мониторится сеть и тщательно ведется лог приложений (и есть доступ к логу ошибок MySQL), то в примере выше вы можете установить первопричину: 20-секундный сетевой сбой. Но проще сказать, чем сделать, потому что в разгар этой ситуации (когда приложение не работает, клиенты звонят, а сейчас пятница, 4:30 пополудни) инженеры сосредоточены на устранении проблемы, а не на выяснении ее первопричины. В таком состоянии легко решить, что все дело в MySQL: заставьте MySQL работать быстрее, и с приложением все будет в порядке. Но исправить MySQL в этом смысле невозможно (напомню вам раздел «MySQL ничего не делает» в главе 4). Поскольку до возникновения проблемы все было нормально, цель состоит в том, чтобы вернуться к этой норме — начиная с приложения, потому что оно управляет MySQL. Правильное решение зависит от приложения, но распро-
Метрики сервера | 245 страненными тактиками являются: перезапуск приложения, регулирование входящих запросов приложения и отключение функций приложения. Я вовсе не выгораживаю MySQL. Но реальность такова, что MySQL — это зрелая база данных с более чем 20-летним опытом использования в этой области. Более того, как база данных с открытым исходным кодом она была тщательно изучена инженерами всего мира. На данном этапе существования MySQL медлительность не относится к ее слабостям. Вместо того, чтобы спросить, почему MySQL работает медленно, задайтесь более точным и эффективным вопросом, и он приведет к пониманию корня проблемы или даже немедленному ее устранению: «Что заставляет MySQL работать медленно?» Итоги В этой главе были проанализированы спектры тех метрик MySQL, которые являются наиболее важными для понимания характера рабочей нагрузки, определяющей производительность MySQL. Основные выводы:  Производительность MySQL имеет две стороны: производительность запросов и производительность сервера.  Производительность запросов — это входные данные; производительность сер- вера — это выходные данные.  Нормальная и стабильная — это такая производительность, которую MySQL демонстрирует для вашего приложения в обычный день, когда все работает должным образом.  Стабильность не ограничивает производительность; она гарантирует, что произ- водительность (на любом уровне) является устойчивой.  Ключевые показатели производительности MySQL — это время отклика, ошиб- ки, QPS и выполняющиеся потоки.  Поле метрик включает шесть классов метрик: время отклика, скорость, объем использования, ожидание, ошибки и паттерны доступа (семь, если считать внутренние метрики).  Классы метрик связаны между собой: скорость увеличивает объем использова- ния; объем использования противодействует и уменьшает скорость; высокий (максимальный) объем использования влечет за собой увеличение времени ожидания; истечение времени ожидания приводит к ошибкам.  Спектр метрик MySQL огромен (см. раздел «Спектры» в этой главе).  Разрешение означает частоту, с которой собираются и сообщаются метрики.  Метрики с высоким разрешением (5 секунд или менее) раскрывают важные све- дения о производительности, которые теряются при использовании метрик с низким разрешением.  Необходимо оповещать о том, с чем сталкиваются пользователи (например, время отклика), и об объективных ограничениях.
246 | Глава 6  Наиболее вероятная причина плохой производительности MySQL — проблемы с приложением (вашим приложением или другим).  Производительность сервера MySQL анализируется с помощью спектра метрик, которые служат образным отражением рабочей нагрузки MySQL. В следующей главе исследуется запаздывание репликации. Практика: анализ ключевых показателей производительности Цель этой практики — понять, каковы нормальные и стабильные значения четырех ключевых показателей производительности для MySQL, как описано в разделе «Ключевые показатели производительности» (см. выше). Чтобы сделать эту практику интересной для себя, вначале запишите, каковы, по вашему мнению, должны быть эти значения для вашего приложения. У вас, вероятно, уже есть представление о QPS; как насчет времени отклика (P99 или P999), ошибок и выполняющихся потоков? Начните собирать четыре ключевых показателя производительности (см. выше соответствующий раздел), если вы еще не начали этого делать. Ваш метод может зависеть от программного обеспечения (или сервиса), которое вы используете для сбора метрик MySQL. Любая приличная программа мониторинга MySQL должна собирать все четыре метрики; если ваше текущее решение этого не делает, серьезно задумайтесь о его смене, потому что, если ключевые показатели производительности не собираются, многие другие метрики, подробно описанные в разделе «Спектры» (см. выше), также будут упущены из вида. Сделайте обзор ключевых показателей производительности, по крайней мере, за один полный день. Близки ли реальные значения к ожидаемым? Если время отклика больше, чем вы думали, то вы знаете, с чего начать: раздел «Профиль запроса» в главе 1. Если частота ошибок выше, чем вы думали, то запросите таблицу performance_schema.events_errors_summary_global_by_error, чтобы посмотреть на коды ошибок. Используйте «MySQL Error Message Reference» («Справку о сообщениях об ошибках MySQL», https://oreil.ly/F9z9W), чтобы найти код ошибки. Если количество выполняющихся потоков выше, чем вы думали, это затрудняет диагностику, потому что один поток выполняет разные запросы (при условии, что приложение использует пул подключений). Начните с самых медленных запросов в профиле запросов. Если ваш инструмент для определения метрик запросов сообщает о нагрузке запросов, сосредоточьтесь на запросах с наибольшей нагрузкой; в противном случае сосредоточьтесь на запросах с наибольшим общим временем запроса. При необходимости проведите исследование с использованием таблицы threads из схемы производительности (https://oreil.ly/ZgtGW). Просматривайте ключевые показатели производительности за разные периоды в течение дня. Являются ли значения стабильными в течение всего дня или они уменьшаются в середине ночи? Есть ли периоды, когда значения ненормальны?
Метрики сервера | 247 Каковы нормальные и стабильные значения ключевых показателей производительности для вашего приложения в целом? Практика: проверка оповещений и пороговых значений Цель этой практики — помочь вам спокойно спать по ночам. В то время как диаграммы для метрик MySQL находятся в центре внимания, оповещения (и конфигурация этих оповещений) обычно скрыты от глаз. Следовательно, программисты (особенно новички в своем деле) не знают, какие оповещения рыщут в темноте, готовясь поднять их среди ночи. Потратьте утро или день на то, чтобы пролить свет на все ваши оповещения и на то, как они настроены (их пороговые значения, если таковые имеются). И пока вы этим занимаетесь, еще один момент: задокументируйте их (или обновите текущую документацию). Просмотрите в этой главе разделы «Охота на миражи (пороговые значения)» а также «Оповещения о пользовательском опыте и объективных ограничениях» и удалите или измените лишние оповещения. Цель оповещений проста: каждое из них должно быть оправданным и побуждающим к действию. Термин «оправданное» означает, что что-то уже сломано или наверняка сломается очень скоро, и это требует исправления прямо сейчас. «Побуждающее к действию» означает, что программист (которому было отправлено сообщение с оповещением) обладает знаниями, навыками и доступом для решения проблемы. Это возможно с помощью MySQL. Скажите «нет» охоте на миражи и «да» здоровому ночному сну.
248 | Глава 6
ГЛАВА 7 Запаздывание репликации Запаздывание репликации — это задержка между моментом записи в исходном экземпляре (источнике) MySQL и моментом, когда эта запись применяется к экземпляру реплики MySQL. Запаздывание репликации присуще всем серверам баз данных, поскольку репликация по сети приводит к сетевой задержке. Я рад, что вам, как инженеру, использующему MySQL, не нужно настраивать и поддерживать топологию репликации MySQL, потому что репликация MySQL стала сложной. Вместо этого в данной главе исследуется запаздывание репликации с точки зрения производительности: что это такое, почему это происходит, какой риск представляет и что вы можете с этим поделать. Простая репликация завоевала Интернет Простая репликация — одна из причин того, что MySQL стал самым популярным в мире сервером реляционных баз данных с открытым исходным кодом. В начале 2000-х Интернет возрождался после пузыря доткомов 90-х, и онлайн-компании быстро росли. Поскольку репликация требуется для обеспечения высокой доступности, а также используется для масштабирования операций чтения, простая репликация в ранних версиях MySQL (с v3.23 по v5.5) помогла MySQL завоевать Интернет в те беспечные дни. Ранние версии MySQL использовали однопоточную репликацию на основе инструкций (англ. statement-based replication, SBR): исходный экземпляр MySQL регистрировал выполняемые им инструкции SQL — да, фактические инструкции SQL — и экземпляры реплик просто повторно выполняли эти же инструкции SQL. Не бывает репликации проще. Да, это работало, и да, там были проблемы и подводные камни. Но иногда самое простое решение действительно лучше всех. Сейчас, более 20 лет спустя, репликация MySQL сложна, но по-прежнему поддерживает репликацию на основе инструкций. Технически — да, репликация снижает производительность, но вы же не хотите запускать MySQL без нее? Не будет преувеличением сказать, что репликация предотвращает крах бизнеса — защищает от потери данных настолько катастрофической, что компания обанкротилась бы. MySQL работает везде, от больниц до банков, а репликация обеспечивает сохранность бесценных данных, несмотря на неизбежные сбои. Хотя репликация снижает производительность, а запаздывание представляет собой риск, эти затраты компенсируются подавляющими преимуществами репликации. В этой главе исследуется запаздывание репликации. Здесь шесть основных разделов. Первый вводит базовую терминологию репликации MySQL и освещает технические причины запаздывания репликации — почему это происходит, несмотря на быстрые базы данных и сети. Во втором разделе обсуждаются основные причины
250 | Глава 7 запаздывания репликации. Третий объясняет риск запаздывания репликации: потеря данных. Четвертый о том, как обеспечить разумную конфигурацию для включения многопоточной реплики, что значительно снижает запаздывание. В пятом разделе рассматривается мониторинг запаздывания репликации с высокой точностью. Шестой объясняет, почему наверстывание отставания после запаздывания репликации происходит медленно. Основа MySQL имеет два типа репликации: Источник — реплика. Репликация по типу «источник — реплика» — это основной тип репликации, который MySQL использует уже более 20 лет. Его почтенный статус означает, что под репликацией MySQL (https://oreil.ly/A8fTn) обычно подразумевается именно этот вариант. Он стар, но не заблуждайтесь: это быстрый, надежный и по-прежнему широко применяемый метод. Групповая репликация Групповая репликация (https://oreil.ly/TASM9) — это новый тип репликации, который поддерживается MySQL, начиная с версии MySQL 5.7.17 (выпущена 12 декабря 2016 года). Групповая репликация создает кластер MySQL из первичных и вторичных экземпляров, которые используют протокол группового консенсуса для синхронизации (репликации) изменений данных и управления членством в группе. Короче говоря, групповая репликация — это кластеризация MySQL, и это будущее репликации MySQL и высокой доступности. В этой главе рассматривается только традиционная репликация MySQL: источник — реплика. За групповой репликацией будущее, но я подожду с освещением темы: на момент написания этой книги ни я, ни какие-либо известные мне администраторы баз данных не имеют значительного опыта масштабной репликации такого типа. Более того, стандартом становится еще одно новшество, построенное поверх групповой репликации: InnoDB Cluster (https://oreil.ly/BFqu9). Кроме того, Percona XtraDB Cluster (https://oreil.ly/fWNfb) и MariaDB Galera Cluster (https://oreil.ly/LMhEC) — это кластерные решения для баз данных, аналогичные групповой репликации MySQL по цели, но реализованные иначе. Про них я также пока писать не буду, но имейте их в виду, если используете дистрибутив MySQL Percona или MariaDB и ищете решение для кластера баз данных. Репликация по типу «источник — реплика» распространена повсеместно. Хотя внутренняя работа репликации выходит за рамки этой книги, понимание основы проливает свет на причины запаздывания репликации, риски, которые из-за этого возникают, и способы уменьшить и то и другое. Терминология репликации изменилась с MySQL 8.0.22 и 8.0.26, выпущенных в 2020 и 2021 годах соответственно. Краткое описание изменений смотрите в разделе «MySQL Terminology Updates» («Обновления терминологии MySQL», https:// oreil.ly/wrzfU). В этой книге я использую текущую терминологию, метрики, переменные и команды.
Запаздывание репликации | 251 Источник — реплика Рис. 7.1 иллюстрирует основу процесса репликации по принципу «источник — реплика» Исходный экземпляр MySQL (или сокращенно источник) — это любой сервер MySQL, на который клиенты (приложение) записывают данные. Репликация MySQL поддерживает несколько доступных для записи источников, но это редко встречается из-за сложности обработки конфликтов записи. Следовательно, один доступный для записи источник — это норма. Экземпляр реплики MySQL (или сокращенно реплика) — это любой сервер MySQL, который реплицирует изменения данных из источника. Изменения данных — это Рис. 7.1. Репликация MySQL по типу «источник — реплика»
252 | Глава 7 изменения строк, индексов, схем и т. д. Копии всегда должны быть доступны только для чтения, чтобы избежать сплит-брейна (англ. split-brain, см. гл. 9, раздел «Сплит-брейн — величайший риск»). Обычно реплика реплицируется из одного источника, но возможна репликация из нескольких источников (https://oreil.ly/ GeaVQ). Стрелки на рис. 7.1 представляют поток изменений данных от источника к реплике: 1. Во время фиксации транзакции изменения данных записываются в бинарные, они же двоичные логи (англ. binary logs, или сокращенно binlog), в источнике: файлы на диске, которые записывают изменения данных в события бинарного лога (см. раздел «События бинарного лога» ниже в этой главе). 2. Поток ввода-вывода на реплике сбрасывает (считывает) события бинарного лога из бинарных логов источника. (Поток дампа бинарного лога (англ. binlog dump thread) источника предназначен для этой цели).) 3. Поток ввода-вывода на реплике записывает события бинарного лога в логи ретрансляции на реплике: файлов на диске, которые являются локальной копией бинарных логов источника. 4. Поток SQL (или применяющий поток) считывает события бинарного лога из лога ретрансляции. 5. Поток SQL применяет события бинарного лога к данным реплики. 6. Реплика записывает изменения данных (примененные потоком SQL) в свои бинарные логи. По умолчанию репликация MySQL является асинхронной: в источнике транзакция завершается после шага 1, а остальные шаги выполняются асинхронно. MySQL также поддерживает полусинхронную репликацию: в источнике транзакция завершается после шага 3. Это не опечатка: полусинхронная репликация MySQL фиксируется после шага 3; она не ожидает шага 4 или 5 (см. раздел «Полусинхронная репликация» ниже в этой главе). Репликам не требуется записывать бинарные логи (шаг 6), но это стандартная практика для обеспечения высокой доступности, поскольку она позволяет реплике стать источником. Вот как работает отказоустойчивость базы данных: когда источник выходит из строя или выключается на техническое обслуживание, реплика назначается новым источником. Давайте назовем эти экземпляры «старый источник» и «новый источник». В конце концов администратор базы данных восстановит старый источник (или клонирует новый экземпляр на замену) и настроит на него репликацию из нового источника. В коде старого источника начнут работать ранее бездействовавшие потоки ввода-вывода, логи ретрансляции и потоки SQL (на рис. 7.1 они выделены темным цветом). (Поток ввода-вывода в старом источнике подключится к новому источнику, который активирует его ранее бездействовавший поток двоичного лога.) Из бинарных логов нового источника старый источник реплицирует записи, которые он пропустил, находясь в автономном режиме. При этом старый источник сообщит о запаздывании репликации, но это особый случай, рассмотренный в раз-
Запаздывание репликации | 253 деле «Восстановление после сбоя» далее в этой главе. Это отказоустойчивость в двух словах; но на практике это, конечно, сложнее. События бинарного лога События бинарного лога — это низкоуровневая детализация, с которой вы, вероятно, не столкнетесь (даже администраторы баз данных не часто копаются в бинарных логах), но они являются прямым результатом транзакций, выполняемых приложением. Поэтому важно понимать, какие данные приложение пытается сбросить (англ. flush) с помощью репликации. Следующее предполагает построчную репликацию (англ. row-based replication, RBR), которая по умолчанию является форматом бинарного кода (binlog_format, https://oreil.ly/rtKm0), начиная с MySQL 5.7.7. Репликация фокусируется на транзакциях и событиях бинарного лога, а не на отдельных операциях записи, поскольку изменения данных фиксируются в бинарных логах во время фиксации транзакции, на момент чего операции записи уже выполнены. На высоком уровне основное внимание уделяется транзакциям, поскольку они имеют значение для приложения. На низком уровне основное внимание уделяется событиям бинарного лога, поскольку они имеют значение для репликации. Транзакции логически представлены и описаны в бинарных логах как события, что позволяет многопоточным репликам применять их параллельно — подробнее об этом в разделе «Уменьшение запаздывания: многопоточная репликация» (см. ниже). Для иллюстрации воспользуемся простой транзакцией: BEGIN; UPDATE t1 SET c='val' WHERE id=1 LIMIT 1; DELETE FROM t2 LIMIT 3; COMMIT; Схемы таблиц и данные не имеют значения. Важно то, что UPDATE изменяет одну строку в таблице t1, а DELETE удаляет три строки из таблицы t2. На рис. 7.2 показано, как эта транзакция фиксируется в бинарном логе. Транзакцию составляют четыре последовательных события: 1. Событие для BEGIN. 2. Событие для инструкции UPDATE с образом одной строки. 3. Событие для инструкции DELETE с тремя образами строк. 4. Событие для COMMIT. На этом низком уровне инструкции SQL по сути исчезают, а репликация представляет собой поток событий и образов строк (для событий, которые изменяют строки). Образ строки (англ. row image) — это бинарный снимок (англ. binary snapshot) строки до и после изменения. Это важная деталь, поскольку одна инструкция SQL может генерировать бесчисленное количество образов строк, что приводит к большой транзакции, которая может вызвать запаздывание при ее прохождении через репликацию.
254 | Глава 7 Рис. 7.2. События бинарного лога для транзакции Давайте остановимся на этом, потому что мы немного глубже погрузились в MySQL, чем следовало бы для этой книги. Хотя это краткое введение в события бинарного лога делает следующие разделы более понятными, потому что теперь вы знаете, что происходит в процессе репликации и почему все вертится вокруг транзакций и событий бинарного лога. Запаздывание репликации Возвращаясь к рис. 7.1: запаздывание репликации возникает, когда внесение изменений в реплику (шаг 5) происходит медленнее, чем фиксация изменений в источнике (шаг 1). Промежуточные этапы редко являются проблемой (когда сеть работает должным образом), потому что бинарные логи MySQL, сетевой протокол MySQL и типичные сети очень быстры и эффективны. «Внести изменения» означает «внести транзакции» или «внести события» в зависимости от контекста. Поток ввода-вывода в реплике может записывать бинарные события лога в свои логи ретрансляции с высокой скоростью, потому что это относительно простой процесс: чтение из сети, последовательная запись на диск. Но поток SQL имеет гораздо более сложный и трудоемкий процесс: применение изменений. Следовательно, поток ввода-вывода опережает поток SQL, и запаздывание репликации выглядит так, как показано на рис. 7.3.
Запаздывание репликации | 255 Рис. 7.3. Запаздывание репликации MySQL Строго говоря, один поток SQL не вызовет запаздывания репликации, это всего лишь ограничивающий фактор. Причиной в данном случае является высокая пропускная способность транзакций в источнике — это приятная проблема, если приложение нагружено, но тем не менее проблема. Подробнее о причинах читайте в следующем разделе. Решение заключается в большем количестве потоков SQL, что описано далее в разделе «Уменьшение запаздывания: многопоточная репликация». Полусинхронная репликация не устраняет и не предотвращает запаздывание репликации. Когда включена полусинхронная репликация, при каждой транзакции MySQL ожидает подтверждения от реплики, что та записала события бинарного лога для транзакции в свои логи ретрансляции — шаг 3 на рис. 7.1. В локальной сети запаздывание репликации, как показано на рис. 7.3, все еще может возникать. Если полусинхронность уменьшает запаздывание репликации, это всего лишь побочный эффект сетевой задержки, ограничивающий пропускную способность транзакций в источнике. Более подробная информация содержится в разделе «Полусинхронная репликация» (см. ниже). Запаздывание характерно для репликации, но не обманитесь: репликация MySQL происходит очень быстро. Один поток SQL может легко обрабатывать тысячи
256 | Глава 7 транзакций в секунду. Первая причина проста: в отличие от источника, реплики не выполняют полную рабочую нагрузку. Отмечу, что реплики не выполняют операции чтения (предполагается, что реплики не используются для обслуживания таких операций). Вторая причина требует объяснения подлиннее. Как отмечено в разделе «События бинарного лога» (см. выше), в этой главе преимущественно разбирается построчная репликация (англ. row-based replication, RBR). Следовательно, реплики не выполняют инструкции SQL: они применяют события бинарного лога. Это экономит репликам много времени, потому что им дается конечный результат — изменения данных — и указывается, где эти изменения применять. Это бывает значительно быстрее, чем поиск подходящих строк для обновления, что и приходится делать источнику. В силу этих двух причин реплики могут практически бездействовать, даже если источник очень нагружен. Тем не менее существуют три причины повышенной нагрузки репликации. Причины Запаздывание репликации имеет три основные причины: пропускная способность транзакций, восстановление после сбоя и проблемы с сетью. Ниже приводится объяснение по каждому из них. Пропускная способность транзакций Пропускная способность транзакций вызывает запаздывание репликации, когда скорость в источнике превышает скорость, с которой применяющие потоки SQL в реплике могут вносить изменения. Когда это происходит из-за обоснованной нагрузки приложения, снизить скорость на источнике, как правило, невозможно. Решение состоит в том, чтобы вместо этого увеличить скорость на реплике, запустив больше применяющих потоков. Сосредоточьтесь на повышении производительности реплик путем настройки многопоточной репликации, как описано в разделе «Уменьшение запаздывания: многопоточная репликация» (см. ниже). Большие транзакции — те, которые изменяют чрезмерное количество строк, — сильнее влияют на реплики, чем на источник. Например, в источнике большая транзакция, выполнение которой занимает две секунды, скорее всего, не станет препятствием для других транзакций, поскольку выполняется (и фиксируется) параллельно. Но в однопоточной реплике эта большая транзакция остановит все остальные транзакции на две секунды (или сколько нужно для выполнения в реплике — бывает и меньше из-за меньшего числа конфликтов). В многопоточной реплике другие транзакции продолжают выполняться, но эта большая транзакция попрежнему занимает один поток на две секунды. Решение заключается в меньших транзакциях (см. гл. 8, раздел «Большие транзакции (размер транзакции)»). Пропускная способность транзакций не всегда зависит от приложения: обратная загрузка, удаление и архивирование данных — это обычные операции, которые могут вызвать значительное запаздывание репликации, если в них не контролируется
Запаздывание репликации | 257 размер пакета, как я предупреждал в разделе «Размер пакета» (см. гл. 3). Помимо надлежащего размера пакета, эти операции должны отслеживать запаздывание репликации и замедляться, когда реплики начинают отставать. Лучше операция займет один день, чем реплика отстанет на одну секунду. Раздел «Риск: потеря данных» (см. ниже) объясняет почему. В какой-то момент пропускная способность транзакций превысит пропускную способность одного экземпляра MySQL — источника или реплики. Чтобы увеличить пропускную способность транзакций, необходимо увеличить масштаб путем шардирования базы данных (см. гл. 5). Восстановление после сбоя При сбое MySQL или аппаратного обеспечения экземпляр исправляется и возвращается в топологию репликации. Либо новый экземпляр клонируется из существующего и замещает вышедший из строя. В любом случае топология репликации перестраивается для восстановления высокой доступности. Реплики используются для нескольких целей, но в этой главе рассматриваются только реплики, используемые для обеспечения высокой доступности. Исправленному (или новому) экземпляру потребуются минуты, часы или дни, чтобы наверстать упущенное: реплицировать все события бинарного лога, которые он пропустил, находясь в автономном режиме. Технически это запаздывание репликации, но на практике вы можете игнорировать его до тех пор, пока исправленный экземпляр не наверстает упущенное. После этого работайте с запаздываниями обычным образом. Поскольку сбой неизбежен, а устранение неполадок требует времени, единственный выход — знать, что запаздывание репликации вызван восстановлением после сбоя и ожиданием. Сетевые проблемы Сетевые проблемы вызывают запаздывание репликации, задерживая передачу событий бинарного лога из источника в реплику — шаг 2 на рис. 7.1. Технически отстает сеть, а не репликация, но придирки к семантике не меняют конечного результата. В этом случае надо привлечь сетевых инженеров для устранения основной причины: сетевой. Риск, вызванный сетевыми проблемами, снижается за счет общения и совместной работы: поговорите с сетевыми инженерами, чтобы проверить, понимают ли они, каковы ставки для базы данных при сетевых сбоях — вполне возможно, они не в курсе, потому что они не администраторы баз данных и не инженеры, использующие MySQL.
258 | Глава 7 Риск: потеря данных Запаздывание репликации — это потеря данных. По умолчанию это верно для MySQL, поскольку по умолчанию там используется асинхронная репликация. К счастью, есть полусинхронная репликация — вариант, при котором не потеряется ни одна зафиксированная транзакция. Давайте сначала рассмотрим риск, связанный с асинхронной репликацией, тогда станет ясно, как полусинхронная репликация снижает его. Как отмечено в разделе «Основа» выше в этой главе, разговор о групповой репликации отложим на будущее. Тем более что синхронность групповой репликации 1 требует тщательного объяснения . Асинхронная репликация На рис. 7.4 показан момент, в который произошел сбой источника. Перед сбоем источник зафиксировал пять транзакций в своих бинарных логах. Но к моменту, когда он вышел из строя, поток ввода-вывода реплики извлек только первые три транзакции. Будут потеряны последние две или нет, зависит от пары параметров: от причины сбоя и от того, должен ли администратор базы данных использовать аварийное переключение (англ. failover). Рис. 7.4. Сбой источника MySQL с асинхронной репликацией Если сбой вызван MySQL (скорее всего, из-за бага), то она автоматически перезапустится, выполнит аварийное восстановление и возобновит нормальную работу. (По умолчанию реплики также автоматически повторно подключаются и возобновляют репликацию.) И поскольку MySQL действительно надежна при правильной настройке, зафиксированные транзакции 4 и 5 не потеряются. Есть только одна 1 Этот вопрос отчасти разбирает известный эксперт по MySQL Фредерик Дескамп в статье «MySQL Group Replication... Synchronous or Asynchronous Replication?» («Групповая репликация MySQL... Синхронная или асинхронная?», https://oreil.ly/Gv6GR).
Запаздывание репликации | 259 проблема: аварийное восстановление может занять несколько минут или часов — это определяют несколько факторов, выходящих за рамки этой книги. Если вы можете подождать, аварийное восстановление будет идеальным решением, поскольку никакие зафиксированные транзакции не теряются. Если же сбой вызван аппаратным обеспечением или операционной системой, или если по какой-либо причине аварийный экземпляр MySQL не может быть восстановлен достаточно быстро, администратор базы данных выполнит аварийное переключение — повысит реплику до источника, и транзакции 4 и 5 будут утрачены. Это не идеальное решение, но стандартная практика, потому что альтернатива хуже: длительное отключение (простой) при восстановлении попавшего под сбой экземпляра MySQL, что требует тщательной экспертизы данных, которая может занять часы или дни. Никакие данные не теряются при переключении на другую базу данных для технического обслуживания (операций). И поскольку ничего не вышло из строя, некоторые администраторы баз данных называют это successover — успешным переключением. Приведенный пример не призван доказать, что запаздывание репликации — потеря данных; это неизбежно при асинхронной репликации, потому что любое аппаратное и программное обеспечение (включая MySQL) когда-нибудь выйдет из строя. Единственным способом смягчить последствия является неуклонное стремление минимизировать запаздывание репликации. Не стоит, например, считать, что 10 секунд репликации — «не слишком большое отставание». Воспринимайте это как «мы рискуем потерять последние 10 секунд клиентских данных». Шансы в вашу пользу, что MySQL или аппаратное обеспечение не выйдут из строя в самый неподходящий момент — т. е. когда реплика отстает, но позвольте мне рассказать поучительную историю об аппаратном сбое. Однажды на дежурстве я получил тревожное оповещение. Было около 9 утра, что не так уж рано; я уже выпил свою первую чашку кофе. Одно оповещение быстро превратилось в тысячи. Серверы баз данных повсюду — во множестве географически распределенных центрах обработки данных — выходили из строя. Это было так плохо, что я сразу понял: проблема не в оборудовании или MySQL, потому что вероятность такого количества одновременных, но несвязанных сбоев бесконечно мала. Короче говоря, один из самых опытных инженеров в компании в то утро свой кофе не выпил. Он написал и запустил пользовательский сценарий, который пошел совсем наперекосяк. Сценарий не просто произвольно перезагружал серверы, он их отключал. (В центрах обработки данных питание сервера программно контролируется с помощью объединительной платы, называемой Intelligent Platform Management Interface.) Убойная сила сродни отказу оборудования. Мораль этой истории такова: сбой может быть вызван человеческим фактором. Будьте готовы. Асинхронная репликация — не лучшая из практик, поскольку почти полная потеря данных противоречит цели их постоянного хранения. Бесчисленные компании по всему миру уже более 20 лет успешно используют асинхронную репликацию,
260 | Глава 7 однако «обычная практика» не означает «лучшая» Но если вы запускаете асинхронную репликацию, администраторы баз данных MySQL и эксперты не будут над вами смеяться, пока выполняются следующие три условия: 1. Вы отслеживаете запаздывание репликации с помощью такта (англ. heartbeat) (см. ниже, раздел «Мониторинг» ). 2. Вы получаете оповещение в любое время (не только в рабочие часы), когда запаздывание репликации слишком велико. 3. Вы воспринимаете запаздывание репликации как потерю данных и немедленно устраняете его. Многие успешные компании используют асинхронную репликацию MySQL, но есть более высокий стандарт, к которому нужно стремиться: полусинхронная репликация. Полусинхронная репликация Когда включена полусинхронная (англ. semisync) репликация, источник ожидает подтверждения каждой транзакции по крайней мере от одной реплики. Подтверждение означает, что реплика записала события бинарного лога для транзакции в свои логи ретрансляции. Таким образом, транзакция надежно хранится на диске в реплике, но реплика еще не применила ее. (Следовательно, по-прежнему возникает запаздывание репликации, как сказано в разделе «Запаздывание репликации» (см. выше).) Подтверждение при получении, а не при применении — вот почему оно называется полусинхронным, а не полностью синхронным. Давайте снова посмотрим на сбой источника из раздела «Асинхронная репликация» (см. выше), но теперь с включенной полусинхронной репликацией. На рис. 7.5 показан момент, когда произошел сбой источника. Рис. 7.5. Сбой источника MySQL при полусинхронной репликации При полусинхронной репликации гарантируется, что каждая зафиксированная транзакция будет реплицирована по крайней мере в одну реплику. Зафиксированная транзакция в этом контексте означает, что оператор COMMIT, выполненный кли-
Запаздывание репликации | 261 ентом, вернулся — транзакция завершена с точки зрения клиента. Это обычное высокоуровневое понимание зафиксированной транзакции, но в процессе репликации технические детали различаются. Следующие четыре шага представляют собой крайне упрощенное представление того, как транзакция фиксируется при ведении бинарного лога и полусинхронной репликации: 1. Подготовка фиксации транзакции. 2. Сброс изменений данных в бинарный лог. 3. Ожидание подтверждения хотя бы от одной реплики. 4. Фиксация транзакции. Фиксация транзакции InnoDB — двухфазная. В промежутке между двумя этапами (шаги 1 и 4) изменения данных записываются и сбрасываются в бинарные логи, и MySQL ожидает подтверждения транзакции по крайней мере от одной реплики2. На рис. 7.5 пунктирная рамка четвертой транзакции означает, что подтверждения ни от одной реплики пока не было. Иточник вышел из строя после шага 2, поэтому транзакция находится в бинарных логах, но фиксация не завершена. Инструкция COMMIT клиента вернет ошибку (не из MySQL, потому что она вышла из строя; вероятно, это будет ошибка сети). Будет ли потеряна четвертая транзакция, зависит от тех же двух факторов, что и раньше (см. выше, раздел «Асинхронная репликация»): причины сбоя и того, должен ли администратор базы данных выполнять аварийное переключение. Важное отличие в том, что при включенной полусинхронной репликации может быть потеряна только одна незафиксированная транзакция на соединение. Поскольку транзакция не была завершена и клиент получил сообщение об ошибке, потенциальная потеря незафиксированной транзакции вызывает меньшее беспокойство. Ключевое слово — меньшее: есть особые случаи, когда вы не можете просто игнорировать потерянную транзакцию. Например, что делать, если реплика подтверждает транзакцию, но источник завершает работу до того, как получит подтверждение? Ответ заведет нас в глубины репликации больше, чем нужно. Суть: полусинхронная репликация гарантирует, что все зафиксированные транзакции реплицируются по крайней мере на одну реплику и лишь одна незафиксированная транзакция на соединение может быть потеряна при сбое. Основная цель постоянного хранения данных — сохранить данные, а не потерять их. Так почему же полусинхронная репликация не используется в MySQL по умолчанию? Это сложно. Есть успешные компании, которые масштабируют MySQL, используя полусинхронную репликацию. В частности, GitHub, бывший работодатель известного эксперта по MySQL Шломи Ноаха, который написал пост об использовании ими репликации этого типа: «MySQL High Availability at GitHub» («Высокая доступность MySQL на GitHub», https://oreil.ly/6mLug). 2 Я предполагаю, что sync_binlog = 1, см. https://oreil.ly/lbfwm.
262 | Глава 7 Полусинхронная репликация снижает доступность — и я не оговорился. Хотя это защищает транзакции, такая защита означает, что текущая транзакция для каждого соединения может зависнуть, время ожидания может истечь, а COMMIT завершиться ошибкой. Тогда как COMMIT с асинхронной репликацией выполняется практически мгновенно и гарантированно, пока хранилище в источнике работает. По умолчанию возвращение от полусинхронной репликации к асинхронной происходит, когда реплик недостаточно или истекает время ожидания источником подтверждения. Можно убрать это через настройку, но лучше всего оставить, потому что альтернатива хуже: полное отключение (когда приложение не может выполнить запись в источник). Производительность при полусинхронной репликации требует, чтобы источник и реплики находились в быстрой, локальной сети, поскольку сетевая задержка косвенно ограничивает пропускную способность транзакций в источнике. Будет ли это проблемой, зависит от локальной сети, в которой вы запускаете MySQL. Локальная сеть должна иметь субмиллисекундную задержку, но это должно проверяться и контролироваться, иначе пропускная способность транзакций будет зависеть от капризов сетевой задержки. В то время как асинхронная репликация работает без какой-либо специальной настройки, полусинхронная требует специального конфигурирования. Ни то ни другое не очень обременительно для администратора базы данных, но тем не менее это кропотливая работа. Я думаю, что полусинхронная репликация — одна из лучших практик, потому что потеря данных не бывает приемлемой, ведь это полная остановка. Я советую вам узнать больше о полусинхронной репликации, протестировать и проверить ее в вашей сети, далее использовать, если это возможно. Начните с чтения раздела «Semisynchronous Replication» («Полусинхронная репликация», https://oreil.ly/ JnxUJ) в руководстве по MySQL. Или, если хотите быть по-настоящему готовыми к будущему, посмотрите на групповую репликацию (https://oreil.ly/5ZWHQ) и InnoDB Cluster (https://oreil.ly/JrrYd): будущее репликации MySQL и высокой доступности. Хотя полусинхронная репликация и групповая репликация вызывают споры среди экспертов MySQL, с одним все согласны: предотвращение потери данных — это достоинство. Уменьшение запаздывания: многопоточная репликация По умолчанию репликация MySQL является асинхронной и однопоточной: один поток SQL на реплику. Даже полусинхронная репликация по умолчанию является однопоточной. Одиночный поток SQL не вызывает запаздывания репликации (выше в разделе «Причины» названы три основные его причины), но это ограничивающий фактор. Решение — многопоточная (или параллельная) репликация: несколько потоков SQL, применяющих транзакции параллельно. В многопоточной реплике потоки SQL называются применяющими потоками (англ. applier threads)3. 3 В руководстве по MySQL полный термин — «рабочий применяющий поток» (англ. applier worker thread), но я думаю, что определение «рабочий» избыточно, поскольку каждый поток выполняет свою работу.
Запаздывание репликации | 263 Вы все еще можете называть их потоками SQL (англ. SQL threads), если хотите, — это синонимичные термины, — но руководство по MySQL использует термин «применяющие потоки» (англ. applier threads) в контексте многопоточной репликации. Решение простое для нас, инженеров, использующих MySQL, но для MySQL это непросто. Как вы понимаете, транзакции не могут применяться в случайном порядке: между ними могут быть зависимости. Например, если одна транзакция вставляет новую строку, а вторая транзакция обновляет эту строку, очевидно, что вторая транзакция должна выполняться после первой. Отслеживание зависимостей транзакций — это искусство и наука (и магия) определения, какие транзакции из сериализованной записи (бинарных логов) могут применяться параллельно. Это одновременно увлекательно и впечатляюще, но выходит за рамки этой книги, поэтому я рекомендую вам посмотреть видео «MySQL Parallel Replication (LOGICAL_ CLOCK): all the 5.7 (and some of the 8.0) details» («Параллельная репликация MySQL (LOGICAL_CLOCK): все подробности из 5.7 (и некоторые из 8.0)», https://oreil.ly/Q8aJv) известного эксперта по MySQL Жана-Франсуа Ганье. Строго говоря, одна системная переменная позволяет выполнять многопоточную репликацию, но подозреваю, что вы не удивитесь, когда я скажу: на практике это сложнее. Настройка репликации MySQL также выходит за рамки этой книги, но многопоточная репликация слишком важна, чтобы не дать вам консервативную отправную точку. Выражение «консервативная отправная точка» означает, что следующая конфигурация может не обеспечить полную производительность многопоточной репликации. Следовательно, вы (или администраторы баз данных) должны настроить многопоточную репликацию как в разделе «Тонкая настройка (тюнинг) MySQL» (см. гл. 2), чтобы максимизировать ее потенциал, в то же время принимая во внимание различные последствия параллельной репликации. Остальная часть этого раздела посвящена нетривиальной настройке MySQL, которую должен выполнять только инженер, имеющий опыт настройки MySQL в высокопроизводительных средах с высокой доступностью. Системные переменные в табл. 7.1 никоим образом не повлияют на целостность или долговечность данных, но повлияют на производительность в экземплярах источника и реплики. Имейте в виду, что: 1. Репликация влияет на высокую доступность. 2. Должны быть включены глобальные идентификаторы транзакций (https://oreil.ly/ xYtq3) и функция log-replica-updates (обновления логов реплик, https://oreil.ly/ wAOMO). 3. Для настройки MySQL требуются повышенные права MySQL. 4. Системные переменные меняются между версиями и дистрибутивами MySQL. 5. MariaDB использует другие системные переменные: см. раздел «Параллельная репликация» (https://oreil.ly/F5n6J) в документации MariaDB. Будьте очень осторожны при настройке MySQL и внимательно прочитайте соответствующие разделы руководства для вашей версии и дистрибутива. В табл. 7.1 перечислены три системные переменные в качестве консервативной отправной точки для включения и настройки многопоточной репликации. Имена переменных изменились с версии MySQL 8.0.26, поэтому я указал старые и новые
264 | Глава 7 имена, а также рекомендуемое значение. Я не рекомендую использовать многопоточную репликацию в версиях MySQL старше 5.7.22, поскольку некоторые функции репликации из 8.0 были перенесены в эту версию. Таблица 7.1. Системные переменные для включения многопоточной репликации MySQL с 5.7.22 по 8.0.25 MySQL 8.0.26 и новее slave_parallel_workers replica_parallel_workers (https://oreil.ly/82SBV) (https://oreil.ly/kFqAz) slave_parallel_type replica_parallel_type (https://oreil.ly/s5NOE) (https://oreil.ly/mIft5) slave_preserve_commit_order (https://oreil.ly/oKRSy) replica_preserve_commit_order (https://oreil.ly/QGBB1) Значение 4 LOGICAL_CLOCK 1 Установите все три переменные для всех экземпляров MySQL в топологии реплик, которые используются для обеспечения высокой доступности (т. е. могут быть повышены до источника). Выставьте значение replica_parallel_workers больше нуля — это и есть та единственная системная переменная, которая позволяет выполнять многопоточную репликацию. Четыре применяющих потока — хорошее начало; вам нужна тонкая настройка, чтобы найти оптимальное количество применяющих потоков для вашей рабочей нагрузки и оборудования. Но для этой магии нужно воспользоваться также replica_parallel_type, чтобы обеспечить полную производительность многопоточной репликации. Даже начиная с версии MySQL 8.0.26, по умолчанию для replica_parallel_type установлен параметр DATABASE: это подразумевает параллельные транзакции лишь для разных баз данных — фактически только один применяющий поток для каждой базы. Так сложилось исторически: это был первый тип распараллеливания. Но сегодня лучшей практикой является replica_parallel_type = LOGICAL_CLOCK, потому что у нее нет недостатков при условии, что включена опция replica_preserve_commit_order, и так распараллеливание лучше, поскольку транзакции применяются параллельно независимо от базы данных. по умолчанию отключено, но я не думаю, что это относится к лучшим практикам, поскольку отсутствие этой опции позволяет многопоточной реплике совершать транзакции в другом порядке, нежели чем они были зафиксированы в источнике. Например, транзакции источника, совершенные в порядке 1, 2, 3, могут быть зафиксированы в реплике в порядке 3, 1, 2. Многопоточная репликация фиксируется не по порядку лишь тогда, когда это безопасно (когда между транзакциями нет упорядоченных зависимостей), а данные таблицы (в конечном итоге) одинаковы, но это всегда имеет последствия, которые вы и особенно администраторы баз данных, управляющие MySQL, должны понимать и обрабатывать. Раздел «Replication and Transaction Inconsistencies» («Несогласованность репликации и транзакций», https://oreil.ly/Bf04z) в руководстве MySQL разъясняет эти последствия. Когда опция replica_preserve_commit_order активирована, транзакции replica_preserve_commit_order
Запаздывание репликации | 265 по-прежнему применяются параллельно, но некоторые из них могут ждать фиксации более ранних транзакций — именно так сохраняется порядок фиксации. Хотя replica_preserve_commit_order снижает эффективность распараллеливания, это относится к лучшим практикам, пока вы и администраторы баз данных не убедитесь, что последствия приемлемы и вы можете с ними справиться. Многопоточная репликация работает также и для групповой репликации. Поскольку табл. 7.1 является консервативной отправной точкой для активации многопоточной репликации, она не включает новейшее средство отслеживания зависимостей транзакции: WRITESET. Отслеживание зависимостей транзакций MySQL определяется системной переменной binlog_transaction_dependency_tracking (https:// oreil.ly/5SMUG). Значение по умолчанию — COMMIT_ORDER, но новейшая опция — WRITESET. Тесты показывают, что WRITESET обеспечивает большее распараллеливание, чем COMMIT_ORDER. На момент написания этой книги опции WRITESET меньше четырех лет: она была представлена в MySQL 8.0, которая стала общедоступной (GA) 19 апреля 2018 года. С точки зрения технологии стоит использовать WRITESET, поскольку это обеспечивает более высокую производительность при многопоточных репликах. Но с точки зрения стратегии — вам (или вашему администратору базы данных) решать, когда функция станет достаточно зрелой, чтобы ее использовать. Чтобы использовать WRITESET в MySQL 5.7, вы должны активировать системную переменную transaction_write_set_extraction. В MySQL 8.0 она есть по умолчанию, но устарела с версии MySQL 8.0.26. Создайте новую реплику для тестирования и настройки многопоточной репликации. Новая реплика практически не представляет риска, поскольку у нее нет цели обслуживать приложение или обеспечивать высокую доступность. Есть еще одна системная переменная, с которой вам следует поэкспериментировать: binlog_group_commit_sync_delay. По умолчанию эта переменная отключена (равна нулю), поскольку, как следует из ее названия, она добавляет искусственную задержку для групповой фиксации (англ. group commit). Задержки обычно плохо сказываются на производительности, но задержка групповой фиксации является редким исключением — иногда. В источнике транзакции фиксируются в бинарном логе в группах, что представляет собой внутреннюю оптимизацию, удачно названную групповой фиксацией. Добавление задержки к групповой фиксации создает более крупные группы: для каждой группы фиксируется больше транзакций. Многопоточная репликация не зависит от групповой фиксации, но может извлечь пользу из крупных групповых фиксаций, поскольку одновременное выполнение большего количества транзакций помогает отслеживанию зависимостей транзакций находить больше возможностей для распараллеливания. Чтобы поэкспериментировать с binlog_group_commit_sync_delay, начните со значения 10 000: единица измерения — микросекунды, так что это 10 миллисекунд. Это уве-
266 | Глава 7 личит время отклика источника на фиксацию транзакции на 10 миллисекунд, но также должно увеличить пропускную способность транзакции в реплике. Настроить размер групповой фиксации в отношении пропускной способности применяющей многопоточные транзакции реплики непросто из-за отсутствия метрик MySQL. Если вы пойдете по этому пути, прочтите «A Metric for Tuning Parallel Replication in MySQL 5.7» («Метрика для настройки параллельной репликации в MySQL 5.7», https://oreil.ly/QG4E1) известного эксперта по MySQL Жана-Франсуа Ганье. Многопоточная репликация — одна из лучших практик, но она требует нетривиальной настройки MySQL и, возможно, тюнинга для достижения максимальной производительности. Эталонные тесты и реальные результаты различаются, но многопоточная репликация может более чем удвоить пропускную способность транзакций на репликах. Такое повышение производительности стоит затраченных усилий. Но самое главное: многопоточная репликация значительно уменьшает запаздывание репликации, что крайне важно при асинхронной репликации. Мониторинг Лучшая (передовая) практика для мониторинга запаздывания репликации — использование специального инструмента. Но сначала давайте рассмотрим печально известную метрику MySQL: Seconds_Behind_Source, которую сообщает SHOW REPLICA STATUS. До MySQL 8.0.22 метрикой запаздывания реплики и командой были Seconds_ Behind_Master и SHOW SLAVE STATUS соответственно. Начиная с версии MySQL 8.0.22, это Seconds_Behind_Source и SHOW REPLICA STATUS. В данной книге я использую актуальную метрику и команду. Seconds_Behind_Source равна текущему времени в реплике минус временна́я метка события бинарного лога, выполняемого потоком SQL4. Если текущее время в реплике равно T = 100, а поток SQL выполняет событие бинарного лога с отметкой времени T = 80, то Seconds_Behind_Source = 20. Когда все работает (несмотря на запаздывание репликации), Seconds_Behind_Source относительно точна, однако она печально известна тремя проблемами: 1. Первая проблема возникает, когда все не работает. Поскольку Seconds_ Behind_Source полагается исключительно на временные метки событий бинарного лога, ей, образно говоря, нет дела до каких-либо затруднений, возникших до поступления событий бинарного лога. Если возникает затруднение у источника или сети, из-за которой эти события не поступают или поступают медленно, то поток SQL применяет все события бинарного лога, а Seconds_Behind_Source сообщает о нулевом запаздывании, потому что с точки зрения потока SQL — это технически верно: ноль событий, нулевое запаздывание. Но мы-то знаем, что это 4 Технически это временная метка события плюс время его выполнения. Кроме того, разница в показаниях времени (временной перекос) между источником и репликой вычитается из Seconds_Behind_Source, когда об этом сообщает SHOW REPLICA STATUS.
Запаздывание репликации | 267 неправильно: есть не только запаздывание репликации, но и затруднение перед репликой. 2. Вторая проблема — Seconds_Behind_Source колеблется между нулевым и ненулевым значениями. Например, сначала сообщает о запаздывании в 500 секунд, а потом о нулевом запаздывании, а мгновение спустя снова сообщает о запаздывании в 500 секунд. Эта проблема связана с первой: когда события попадают в логи ретрансляции из-за затруднения перед репликой, поток SQL заметно колеблется между работой (применением последнего события) и ожиданием (следующего события). Из-за чего Seconds_Behind_Source переключается между значением (поток SQL работает) и нулем (поток SQL ожидает). 3. Третья проблема — Seconds_Behind_Source не дает точного ответа на вопрос, который действительно хотят знать инженеры: когда реплика наверстает отставание? Когда запаздывание реплики будет фактически равно нулю, потому что она применяет новейшие транзакции из источника? При условии, что все работает (несмотря на запаздывание репликации), значение Seconds_Behind_Source указывает лишь на то, как давно применяемое текущее событие было выполнено в источнике; оно не указывает, сколько именно времени нужно, чтобы реплика нагнала источник. Причина в том, что реплики применяют транзакции с другой скоростью, нежели чем источник. Например, предположим, что 10 транзакций выполняются одновременно в источнике, и каждая занимает 1 секунду. Общее время выполнения составляет 1 секунду, а скорость составляет 10 транзакций в секунду (англ. transactions per second, TPS), поскольку они выполняются одновременно. В однопоточной реплике, которая применяет каждую транзакцию последовательно, в худшем случае общее время выполнения и скорость могут составлять 10 секунд и 1 TPS соответственно. Я выделил слово «могут», поскольку также возможно, что реплика применит все 10 транзакций куда быстрее, поскольку не обременена полной рабочей нагрузкой и не выполняет инструкции SQL (а применяет события бинарного лога). К примеру, если бы время выполнения в 1 секунду на транзакцию в источнике было вызвано ужасным WHERE, что обращается к миллиону строк, но сопоставляет и обновляет только одну строку. При должном везении реплика обновит эту единственную строку почти в мгновение ока. В многопоточной реплике (см. выше, раздел «Уменьшение запаздывания: многопоточная репликация») общее время выполнения и скорость зависят, по крайней мере, от двух факторов: количества потоков приложения и того, могут ли транзакции применяться параллельно. Как бы то ни было, суть в том, что реплики применяют транзакции с иной скоростью, нежели чем источник, и поскольку нет способа узнать разницу, Seconds_Behind_Source не может дать — и не дает — четких данных о том, когда же реплика наверстает отставание. Несмотря на эти проблемы, Seconds_Behind_Source ценна — это приблизительная оценка того, за какое время реплика нагонит источник: секунды, минуты, часы, дни? (Подробнее о времени восстановления читайте в следующем разделе.) MySQL 8.0 значительно улучшила видимость репликации MySQL, включая запаздывание репликации. Есть только одна загвоздка: она предоставляет примитивы, а
268 | Глава 7 не готовые к использованию метрики, такие как Seconds_Behind_Source. Если вы используете MySQL 8.0, поговорите со своим администратором базы данных о таблицах репликации схемы производительности (https://oreil.ly/xDKOd), которые предоставят новый объем информации о репликации MySQL. В противном случае лучшей практикой для мониторинга запаздывания репликации будет использование специального инструмента. Вместо того, чтобы полагаться на временные метки событий бинарного лога, инструменты используют собственные временные метки. Инструмент записывает временные метки через регулярные промежутки времени в таблицу, затем сообщает о запаздывании репликации как о разнице между текущим временем на реплике и последней временной меткой в таблице. По сути, подход аналогичен тому, как MySQL вычисляет Seconds_Behind_Source, но при использовании инструмента есть три важных отличия:  инструмент записывает временные метки через регулярные промежутки времени — следовательно, не подвержен первой проблеме Seconds_Behind_Source. Если возникает какое-либо затруднение до поступления событий бинарного лога, значение запаздывания репликации от инструмента немедленно начнет увеличиваться, поскольку его временная метка (записанная в таблицу) перестает расти;  инструмент устраняет вторую проблему Seconds_Behind_Source: запаздывание реп- ликации от инструмента не изменяется; запаздывание может быть (фактически) равно нулю, только если временная метка инструмента (фактически) равна текущему времени;  инструмент может измерять запаздывание репликации и записывать временные метки с субсекундным интервалом (например, каждые 200 миллисекунд). Даже секундное запаздывание репликации — это слишком много для высокопроизводительных приложений или для любого приложения, использующего асинхронную репликацию. Де-факто инструментом для мониторинга репликации MySQL является ptheartbeat (https://oreil.ly/sTvro). (Временные метки, записанные средствами мониторинга запаздывания репликации, называются тактами (англ. heartbeat).) Этот почтенный инструмент успешно используется уже более десяти лет, потому что он прост и эффективен. Возьмите его, чтобы начать отслеживать запаздывание репликации или чтобы узнать, как написать свой собственный инструмент. Время восстановления Когда репликация имеет значительное запаздывание, самый насущный вопрос нередко звучит так: «Когда восстановится?» Когда реплика нагонит источник так, чтобы она выполняла (применяла) последние транзакции? Точного ответа нет. Но запаздывание репликации всегда исчезает после устранения причины. Я вернусь к этому замечанию в конце раздела. До тех пор необходимо понять еще одну характеристику запаздывания репликации. Другой частой и важной характеристикой запаздывания репликации является точка перегиба между увеличением запаздывания и моментом, когда реплика начинает восстанавливаться (уменьшением запаздывания). На рис. 7.6 точка перегиба обозначена пунктирной линией на временно́й отметке 75.
Запаздывание репликации | 269 Когда появляется запаздывание репликации, ситуация выглядит все более ужасной по мере его увеличения. Но это нормально. При условии, что реплика не повреждена, потоки SQL работают усердно, однако причина еще не устранена, — поэтому отставание событий бинарного лога продолжает увеличиваться. Пока причина сохраняется, запаздывание репликации будет расти. Но опять же: это нормально. Очень скоро после устранения причины пресловутая волна схлынет, создав точку перегиба на графике запаздывания репликации, как показано на рис. 7.6 на временной отметке 75. Реплика по-прежнему отстает, но она применяет события бинарного лога быстрее, чем поток ввода-вывода сбрасывает их в логи ретрансляции. После точки перегиба запаздывание репликации обычно уменьшается с заметной и удовлетворительной скоростью. Рис. 7.6. Точка перегиба на графике запаздывания репликации Время восстановления не особо важно до точки перегиба, потому что теоретически, если причина никогда не будет устранена, то реплика никогда не восстановится. Когда запаздывание репликации неуклонно растет (предварительная точка перегиба), не отвлекайтесь на значение; вместо этого сосредоточьтесь на решении проблемы. Запаздывание будет расти до тех пор, пока причина не будет устранена. Время восстановления более важно после точки перегиба, и обычно оно меньше, чем сообщает Seconds_Behind_Source или инструменты. Как объясняется в разделе «Мониторинг» (см. выше), несмотря на запаздывание репликации, поток SQL работает очень быстро, потому что реплике не нужно выполнять всю рабочую нагрузку, которая падает на источник. В результате реплики часто применяют транзакции быстрее, чем источник, и именно так реплики в итоге наверстывают отставание. По моему опыту, если запаздывание репликации измеряется в днях, восстановление нередко занимает часы (после точки перегиба) — возможно, это много часов, но все же часов. Аналогичным образом отставание в несколько часов нередко наверстывается за пару часов, а отставание в несколько минут — быстрее, чем вы допьете чашку кофе. Возвращаясь к тому, что точного ответа нет и отставание всегда наверстывается: в конечном счете точное время восстановления не так полезно или значимо, как может показаться сначала. Даже если бы вы могли его знать, вам ничего не остается, кроме как ждать. Репликация MySQL на удивление упряма. До тех пор, пока реплика не сломается, MySQL будет восстанавливаться — это всегда происходит.
270 | Глава 7 Устраните причину как можно быстрее, дождитесь точки перегиба, и тогда значение запаздывания репликации покажет, сколько времени максимум займет восстановление. MySQL обычно восстанавливается быстрее, потому что потоки SQL быстры. Итоги В этой главе исследовалось запаздывание репликации MySQL. Репликация — это основа высокой доступности MySQL, а запаздывание репликации — это потеря данных. Ключевые моменты:  В MySQL три типа репликации: асинхронная (англ. async replication), полусинхронная (англ. semisync replication) и групповaя (англ. group replication).  По умолчанию используется асинхронная репликация.  Асинхронная репликация может привести к потере множества транзакций при сбое.  Полусинхронная репликация: при сбое не теряется ни одна зафиксированная транзакция, лишь одна незафиксированная транзакция на клиентское соединение.  Групповая репликация — это будущее репликации MySQL и высокой доступности (но здесь я эту тему не освещаю): она превращает экземпляры MySQL в кластер.  Основой асинхронной и полусинхронной репликации MySQL является отправка транзакций, закодированных в виде бинарных событий лога, из источника в реплику.  Полусинхронная репликация заставляет фиксацию транзакциий в источнике ждать, когда по крайней мере одна реплика подтвердит получение и сохранит (не применяя) транзакцию.  Реплика имеет поток ввода-вывода, который извлекает бинарные события лога из источника и сохраняет их в локальных логах ретрансляции.  Реплика по умолчанию имеет один поток SQL, который выполняет бинарные события лога из локальных логов ретрансляции.  Многопоточная репликация может быть активирована для запуска нескольких потоков SQL (применяющих потоков).  У запаздывания репликации три основные причины: (высокая) пропускная способность транзакций в источнике, восстановление экземпляра MySQL после сбоя и перестройки и проблемы с сетью.  Потоки SQL (применяющие потоки) — ограничивающий фактор для запаздывания репликации: большее количество потоков SQL уменьшает запаздывание за счет параллельного применения транзакций.  Полусинхронная репликация может привести к запаздыванию репликации.  Запаздывание репликации — это потеря данных, особенно при асинхронной репликации.  Включение многопоточной репликации — лучший способ снизить запаздывание репликации.
Запаздывание репликации | 271  Метрика MySQL для запаздывания репликации, Seconds_Behind_Source, может вводить в заблуждение; не полагайтесь на нее.  Используйте специальный инструмент для измерения запаздывания репликации MySQL (и отчета об этом), измерение и отчет должны быть с субсекундным интервалом.  Время восстановления после запаздывания репликации является неточным, и его трудно рассчитать.  MySQL в конце концов восстановится — это всегда происходит, как только причина устранена. В следующей главе рассматриваются транзакции MySQL. Практика: мониторинг субсекундного запаздывания Цель этой практики — отслеживать субсекундное запаздывание репликации и определять: отстает ли ваша реплика от разрешения в 1 секунду, о котором может сообщить Seconds_Behind_Source? Вдруг ваша реплика отстает на 800 миллисекунд (что намного больше, чем задержка сети)? Необходим специальный инструмент: pt-heartbeat (https://oreil.ly/sTvro). Чтобы выполнить эту практику, вам нужны:  вычислительный экземпляр для запуска pt-heartbeat, который может подклю- чаться к источнику и реплике;  привилегии MySQL SUPER или GRANT OPTION для создания пользователя, или попро- сите вашего администратора базы данных создать пользователя;  привилегии MySQL CREATE для создания базы данных, или попросите вашего администратора базы данных создать базу данных. Каждая конфигурация и среда MySQL отличаются друг от друга, поэтому при необходимости адаптируйте следующий пример под ваш вариант. 1. Создайте базу данных для pt-heartbeat для использования: CREATE DATABASE IF NOT EXISTS `percona`; Вы можете использовать другое имя базы данных; я просто выбрал percona в качестве примера. Если вы меняете имя базы данных, обязательно измените его в следующих командах. 2. Создайте пользователя MySQL для pt-heartbeat и предоставьте ему необходимые привилегии: CREATE USER 'pt-heartbeat'@'%' IDENTIFIED BY 'percona'; GRANT CREATE, INSERT, UPDATE, DELETE, SELECT ON `percona`.`heartbeat` TO 'pt-heartbeat'@'%'; GRANT REPLICATION CLIENT ON *.* TO 'pt-heartbeat'@'%';
272 | Глава 7 Вы можете использовать другое имя пользователя и пароль MySQL. Я просто выбрал pt-heartbeat и percona (соответственно) в качестве примера. Вам обязательно следует сменить пароль, если вы запускаете это в рабочей среде. (Пароль устанавливается оператором IDENTIFIED BY.) 3. Запустите pt-heartbeat в режиме обновления, чтобы записать частоту такта в таблицу в базе данных percona: pt-heartbeat \ --create-table \ --database percona \ --interval 0.2 \ --update \ h=SOURCE_ADDR,u=pt-heartbeat,p=percona Краткое описание этих аргументов командной строки: --create-table При необходимости автоматически создает таблицу heartbeat в указанной базе данных. Первый оператор GRANT позволяет пользователю pt-heartbeat выполнить CREATE для таблицы. Если эта опция не используется, ознакомьтесь с документацией pt-heartbeat, чтобы узнать, как создать таблицу heartbeat вручную. --database Определяет используемую базу данных. pt-heartbeat требует этой опции. --interval Записывайте такт каждые 200 миллисекунд. Этот параметр определяет максимальное разрешение pt-heartbeat, т. е. наименьшую величину запаздывания, которую он может обнаружить. Значение по умолчанию равно 1,0 секунды, что не является субсекундой. Максимальное разрешение составляет 0,01 секунды (10 миллисекунд). Впрочем, 0,2 секунды — это немного консервативно, поэкспериментируйте с более низкими значениями (высоким разрешением). --update Записывает такт в таблицу heartbeat в --database каждые --interval секунд. h=SOURCE_ADDR,u=pt-heartbeat,p=percona Имя источника данных (англ. data source name, DSN) для подключения к MySQL. Буква h указывает имя хоста. Измените SOURCE_ADDR на имя хоста исходного экземпляра. Буква u указывает имя пользователя. Буква р задает пароль. Ознакомьтесь с документацией pt-heartbeat (https://oreil.ly/sTvro) для получения более подробной информации о параметрах командной строки и DSN. Если команда выполняется успешно, она ничего не печатает и выполняется бесшумно. В противном случае выводится сообщение об ошибке и завершает работу.
Запаздывание репликации | 273 4. Снова запустите pt-heartbeat, но в режиме мониторинга, чтобы вывести на экран запаздывание репликации: pt-heartbeat \ --database percona \ --interval 0.5 \ --monitor \ h=REPLICA_ADDR,u=pt-heartbeat,p=percona Измените REPLICA_ADDR в DSN на имя хоста экземпляра реплики. В режиме мониторинга --interval — это как часто проверяется и выводится на экран значение запаздывания репликации. Экземпляр pt-heartbeat в режиме обновления записывает частоту такта каждые 0,2 секунды (200 миллисекунд), но экземпляр pt-heartbeat в режиме мониторинга проверяет и выводит на экран запаздывание репликации немного медленнее (каждые 0,5 секунды) — для удобства чтения. Если команда на четвертом шаге выполняется успешно, она выводит строки типа: 0.00s 0.20s 0.70s 0.00s [ [ [ [ 0.00s, 0.00s, 0.01s, 0.01s, 0.00s, 0.00s, 0.00s, 0.00s, 0.00s 0.00s 0.00s 0.00s ] ] ] ] Первое поле — это текущее запаздывание репликации. Три поля в квадратных скобках представляют собой скользящие средние за последние 1, 5 и 15 минут запаздывания репликации. В данном примере первая строка показывает нулевое запаздывание. Затем я намеренно задержал свою реплику на 1,1 секунды. Следовательно, вторая строка показывает запаздывание репликации в 200 миллисекунд, что является максимальным разрешением, поскольку экземпляр pt-heartbeat в режиме обновления выполняется с --interval 0.2. Через полсекунды (из-за того, что экземпляр pt-heartbeat в режиме мониторинга выполняется с --interval 0.5) инструмент выдает в третьей строке, что запаздывание репликации составляет 0,7 секунды (700 миллисекунд). Но затем моя фальшивая задержка в 1,1 секунды заканчивается, поэтому последняя (четвертая) строка правильно сообщает о нулевом запаздывании. Этот пример искуственный, но он демонстрирует, как pt-heartbeat может отслеживать субсекундное запаздывание репликации и сообщать о нем. Попробуйте в своей сети — инструмент безопасен в использовании.
274 | Глава 7
ГЛАВА 8 Транзакции В MySQL есть нетранзакционные подсистемы хранения, такие как MyISAM, но InnoDB используется и предполагается по умолчанию. Поэтому, практически говоря, каждый запрос MySQL по умолчанию выполняется через транзакцию, даже единичный оператор SELECT. Эта глава неприменима, если вы используете другую подсистему хранения данных, такую как Aria или MyRocks. Но, скорее всего, у вас InnoDB, и тогда: каждый запрос MySQL — это транзакция. Нам, инженерам, транзакции кажутся концептуальными: BEGIN (начать), выполнить запросы, COMMIT (зафиксировать). Затем мы доверяем MySQL (и InnoDB) поддерживать свойства ACID: атомарность (англ. atomicity), согласованность (англ. consistency), изоляцию (англ. isolation) и долговечность (англ. durability). Когда рабочая нагрузка приложения, а это запросы, индексы, данные и паттерны доступа, хорошо оптимизирована, транзакции не приносят проблем производительности. (Большинство процессов базы данных не создают проблем, когда рабочая нагрузка хорошо оптимизирована.) Но за кадром — целый новый мир вопросов, потому что поддержка свойств ACID при сохранении производительности — непростая задача. К счастью, MySQL отлично справляется с выполнением транзакций. Как и в случае с запаздыванием репликации в предыдущей главе, внутреннее устройство транзакций выходит за рамки этой книги, но понимание части основных концепций критически важно для предотвращения распространенных проблем, которые переносят транзакции с самых низких уровней MySQL в центр внимания программистов. Чуть больше понимания позволяет избежать множества проблем. В этой главе рассматриваются транзакции MySQL именно с точки зрения предотвращения распространенных проблем. Здесь пять основных разделов. Первый погружает в вопросы блокировки строк по отношению к уровням изоляции транзакций. Во втором рассматривается, как InnoDB управляет одновременным доступом к данным, гарантируя при этом свойства ACID: MVCC и логи отмены. Третий описывает длину списка истории и то, как это указывает на проблемные транзакции. В четвертом перечислены распространенные проблемы с транзакциями, которых следует избегать. Пятый — попытка осветить нюансы транзакций в MySQL.
276 | Глава 8 Блокировка строк Чтение не блокирует строки (за исключением SELECT...FOR SHARE и SELECT...FOR UPDATE), но операция записи — всегда. Это просто и ожидаемо, но возникает сложный вопрос: какие строки должны быть заблокированы? Конечно, записываемые строки. Но в транзакции REPEATABLE READ InnoDB может блокировать значительно больше строк, чем записывает. Этот раздел иллюстрирует и объясняет почему. Однако сначала давайте переведем терминологию на язык блокировки данных InnoDB. Поскольку таблицы являются индексами (вспомните раздел «Таблицы InnoDB являются индексами», гл. 2), строки являются индексными записями (англ. records). Блокировка строк InnoDB обсуждается в терминах блокировки этих записей, а не блокировки строк из-за интервала в индексных записях. Интервал (англ. gap) — это диапазон значений между двумя индексными записями, как показано на рис. 8.1: первичный ключ с двумя записями, двумя псевдозаписями (инфимум и супремум, т. е. точная нижняя граница и точная верхняя граница) и тремя интервалами. Рис. 8.1. Интервалы в индексных записях Индексные записи изображены в виде сплошных квадратов со значениями индексов внутри: 2 и 5 в этом примере. Псевдозаписи изображены в виде сплошных стрелок на каждом конце индекса: инфимум и супремум. Каждый индекс B-дерева InnoDB имеет эти две псевдозаписи: точная нижняя граница представляет все значения индекса, меньшие минимальной записи (2 в этом примере); точная верхняя граница представляет все значения индекса, превышающие максимальную запись (5 в этом примере). Индексные записи не начинаются с 2 и не заканчиваются на 5; технически они начинаются и заканчиваются на нижнем и верхнем значениях, и примеры в этом разделе раскрывают важность этой детали. Интервалы изображены в виде пунктирных квадратов без значения индекса. Если первичный ключ представляет собой одно четырехбайтовое целое число без знака, то три интервала (перечислены поинтервально):  [0, 2);  (2, 5);  (5, 4294967295]. При обсуждении блокировки строк вместо слова «строка» используется термин «индексная запись» (англ. record), поскольку в индексных записях есть интервалы, а
Транзакции | 277 утверждать, что интервалы есть в строках, было бы неверно. Например, если приложение имеет две строки со значениями 2 и 5, это не создает интервала в строках, содержащих значения 3 и 4, поскольку, возможно, данные значения для приложения недопустимы. Но что касается индекса, то между значениями индексных записей 2 и 5 значения 3 и 4 представляют собой допустимый интервал (предполагается, что это целочисленный столбец). Короче говоря: приложение работает со строками; блокировка строк InnoDB работает с индексными записями. Примеры в этом разделе демонстрируют, что блокировка интервалов удивительно распространена и, возможно, более важна, чем блокировка отдельных индексных записей. Термин «блокировка данных» относится ко всем типам блокировок. Существует много типов блокировки данных, но в табл. 8.1 перечислены основные блокировки InnoDB. Таблица 8.1. Основные блокировки данных InnoDB Тип блокировки Сокращение Блокировка индексной записи (англ. record lock) REC_NOT_GAP Блокировка интервала (англ. gap lock) GAP Блокировка следующего ключа (англ. next-key lock) INSERT_INTENTION Блокировка намерения вставки (англ. insert intention lock) Блокировка интервала Блокировка Блокирует одну индексную запись ✓ Блокирует интервал перед индексной записью (с меньшим значением, чем она) ✓ Блокирует единичную индексную запись и интервал перед ней Позволяет применять INSERT в интервале Лучший способ понять основные блокировки данных InnoDB — это использовать реальные транзакции, реальные блокировки и примеры. Начиная с версии MySQL 8.0.16, блокировки данных легко проверить с помощью таблиц схемы производительности data_locks и data_lock_waits. В следующих примерах используются эти таблицы схемы производительности. В MySQL 5.7 и более ранних версиях вы должны сначала применить SET GLOBAL innodb_status_output_locks=ON, для чего требуются SUPER привилегии MySQL, затем выполнить SHOW ENGINE INNODB STATUS и просмотреть выходные данные, чтобы найти соответствующую транзакцию и блокировки. Это непросто — даже эксперты напрягаются, чтобы тщательно это сделать. Поскольку MySQL 5.7 не относится к текущим релизам, я не привожу ее выходные данные в этом разделе; но она все еще широко используется, так что обратитесь к моему посту «MySQL Data Locks: Mapping 8.0 to 5.7» («Блокировки данных в MySQL: сопоставление 8.0 и 5.7», https://oreil.ly/pIAM6) для получения иллюстрированного руководства по сопоставлению выходных данных блокировки MySQL 5.7 и MySQL 8.0. Давайте снова используем проверенную и надежную таблицу elem, но упрощенную, как показано в примере 8.1.
278 | Глава 8 Пример 8.1. Таблица elem (упрощенная) CREATE TABLE `elem` ( `id` int unsigned NOT NULL, `a` char(2) NOT NULL, `b` char(2) NOT NULL, `c` char(2) NOT NULL, PRIMARY KEY (`id`), KEY `idx_a` (`a`) ) ENGINE=InnoDB; id 2 5 a Au Ar b Be Br c Co C Таблица elem почти такая же, как и раньше, но теперь неуникальный индекс idx_a охватывает только столбец a, и есть только две строки, которые создают два значения первичного ключа, как было показано ранее на рис. 8.1. Поскольку блокировки строк на самом деле являются блокировками индексных записей, а в столбцах b и c нет индексов, можно игнорировать эти два столбца; они показаны только для полноты и ностальгии по более простым главам, таким как глава 2, где блокировки строк были просто блокировками строк. Поскольку autocommit (https://oreil.ly/86J7d) включен по умолчанию, следующие примеры начинаются с BEGIN для запуска эксплицитной транзакции. Блокировки снимаются при завершении транзакции; поэтому транзакция остается активной — без COMMIT или ROLLBACK — для проверки блокировок, которые были получены (или ожидают получения) инструкцией SQL, следующей за BEGIN. В конце каждого примера блокировки данных выводятся на экран путем запроса таблицы performance_schema.data_locks. Блокировки индексной записи и следующего ключа в таблице elem, использующей первичный ключ для сопоставления строк, получает четыре блокировки данных на уровне изоляции транзакций, который установлен по умолчанию, REPEATABLE READ: UPDATE BEGIN; UPDATE elem SET c='' WHERE id BETWEEN 2 AND 5; SELECT index_name, lock_type, lock_mode, lock_status, lock_data FROM performance_schema.data_locks WHERE object_name = 'elem'; index_name NULL PRIMARY PRIMARY PRIMARY lock_type TABLE RECORD RECORD RECORD lock_mode IX X,REC_NOT_GAP X X lock_status GRANTED GRANTED GRANTED GRANTED lock_data NULL 2 supremum pseudo-record 5
Транзакции | 279 Прежде чем проиллюстрировать и объяснить эти блокировки, я кратко опишу, что означает каждая строка: 1. Первая строка представляет собой блокировку таблицы, как указано в столбце lock_type. InnoDB — это подсистема хранения данных с блокировкой на уровне строк, но MySQL также требует блокировки таблиц (см. гл. 1, раздел «Время блокировки»). Каждая таблица, на которую ссылаются запросы в транзакции, блокируется. Я включаю сюда блокировки таблиц для полноты картины, но игнорирую их, поскольку мы сосредоточены на блокировке индексных записей. 2. Вторая строка представляет собой блокировку индексной записи для значения первичного ключа 2, как указано во всех столбцах. Зашифрованный столбец — lock_mode: X означает монопольную блокировку (S [не показано] означает разделяемую блокировку), а REC_NOT_GAP означает блокировку записи. 3. Третья строка — это блокировка следующего ключа на супремуме псевдозаписи. В столбце lock_mode одиночные X или S означают эксклюзивную или разделяемую блокировку следующего ключа соответственно. Представьте это как X, NEXT_KEY. 4. Четвертая строка — это блокировка следующего ключа для значения первичного ключа 5. Опять же, одиночная буква X в столбце lock_mode означает монопольную блокировку следующего ключа. Представьте себе это как X, NEXT_KEY. На рис. 8.2 показано влияние этих блокировок данных. Рис. 8.2. Блокировка индексной записи и следующего ключа для первичного ключа, транзакция REPEATABLE READ Заблокированные записи затенены; неблокированные — выделены белым цветом. Блокировка индексной записи для значения первичного ключа 2 выделена темным цветом. Эта запись заблокирована, поскольку соответствующая ей строка отвечает условию таблицы: id BETWEEN 2 AND 5 (с англ. id между 2 и 5). Блокировка следующего ключа при значении первичного ключа 5 обозначена средне-темным цветом, а интервал перед ней лишь слегка затенен. Эта индексная запись заблокирована, потому что соответствующая ей строка также отвечает условию таблицы. Интервал перед этой записью заблокирован, потому что это блокировка следующего ключа. Интервал содержит несуществующие значения первичного ключа 3 и 4 (для которых нет соответствующих строк). Аналогично блокировка следующего ключа для супремума псевдозаписи обозначена средне-темным цветом, а интервал перед ней лишь слегка затенен. Интервал
280 | Глава 8 включает в себя все значения первичного ключа, превышающие 5. Интригующий вопрос: зачем блокировать супремум псевдозаписи, которая включает в себя все значения первичного ключа, превышающие 5, когда условие таблицы исключает значения первичного ключа, превышающие 5? Ответ столь же интригующий, но я должен отложить его до раздела «Блокировка интервала» ниже в этой главе. Давайте убедимся, что интервалы заблокированы, попытавшись вставить строку (используя другую транзакцию с включенной автоматической фиксацией (autocommit): mysql> INSERT INTO elem VALUES (3, 'Au', 'B', 'C'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction index_name lock_type lock_mode lock_status lock_data PRIMARY RECORD X,GAP,INSERT_INTENTION WAITING 5 ... mysql> INSERT INTO elem VALUES (6, 'Au', 'B', 'C'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction index_name PRIMARY ... lock_type RECORD lock_mode X,INSERT_INTENTION lock_status WAITING lock_data supremum pseudo... Время ожидания первого INSERT истекает при попытке получить блокировку с намерением вставки для интервала между значениями 2 и 5, куда мы собираемся вставить новое значение (3). Хотя в столбце lock_data указано значение 5, эта запись не заблокирована, потому что это не блокировка записи или следующего ключа: это блокировка намерения вставки, которая представляет собой особый тип блокировки интервалов (для INSERT); поэтому оно блокирует интервал перед значением 5. (Подробнее см. ниже, раздел «Блокировка намерения вставки».) Время ожидания второго INSERT истекает при попытке получить блокировку следующего ключа для супремума псевдозаписи, потому что новое значение, 6, больше текущего максимального значения, 5, т. е. должно было быть вставлено между индексной записью с максимальным значением и супремумом псевдозаписи. Эти инструкции INSERT доказывают, что рис. 8.2 не ошибочен: почти весь индекс заблокирован, за исключением значений меньше 2. Почему InnoDB использует блокировки следующего ключа, которые блокируют интервалы, вместо блокировки индексных записей? Потому что уровень изоляции транзакций тут REPEATABLE READ, но это лишь часть ответа. Полный ответ не столь очевиден, так что потерпите меня еще немного. Блокируя интервалы перед затронутыми записями, блокировки следующего ключа изолируют весь диапазон записей, к которым обращается запрос, что отражает букву I в ACID: isolation. Это позволяет избежать явления, называемого фантомными строками (или фантомными чтениями, https://oreil.ly/DYs9L), когда транзакция позднее считывает строки, которые не считывала ранее. Эти строки зовутся фантомами, потому что, подобно призракам, появляются таинственным образом. («Фантом» (англ. phantom) — это реальный термин в стандарте ANSI SQL-92.)
Транзакции | 281 Фантомные строки нарушают принцип изоляции, поэтому определенные уровни изоляции транзакций запрещают их. Теперь действительно загадочная часть этого объяснения: стандарт ANSI SQL-92 допускает фантомные строки в REPEATABLE READ, но InnoDB предотвращает их блокировками следующего ключа. Но давайте не будем углубляться в пресловутую кроличью нору, спрашивая, почему InnoDB так делает. Знание причин не меняет факта, и нередко серверы баз данных реализуют уровни изоляции транзакций иначе, чем по стандарту1. Для полноты, однако, знайте, что стандарт ANSI SQL-92 запрещает фантомные строки только на самом высоком уровне изоляции транзакций: SERIALIZABLE. InnoDB поддерживает SERIALIZABLE, но я не рассматриваю здесь эту опцию, потому что обычно она не используется. По умолчанию в MySQL стоит REPEATABLE READ, а InnoDB для предотвращения фантомных строк в REPEATABLE READ использует блокировку следующего ключа. Уровень изоляции транзакции READ COMMITTED деактивирует блокировку интервалов, которая включает в себя блокировку следующего ключа. Чтобы убедиться в этом, измените уровень изоляции транзакции на READ COMMITTED: SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; UPDATE elem SET c='' WHERE id BETWEEN 2 AND 5; SELECT index_name, lock_type, lock_mode, lock_status, lock_data FROM performance_schema.data_locks WHERE object_name = 'elem'; index_name NULL PRIMARY PRIMARY lock_type TABLE RECORD RECORD lock_mode IX X,REC_NOT_GAP X,REC_NOT_GAP lock_status GRANTED GRANTED GRANTED lock_data NULL 2 5 SET TRANSACTION применяется один раз к следующей транзакции. После этой транзакции последующие используют уровень изоляции транзакций по умолчанию. Подробности см. в разделе SET TRANSACTION (https://oreil.ly/46zcp). Та же инструкция UPDATE в транзакции с READ COMMITTED получает блокировки индексных записей только для соответствующих строк, как показано на рис. 8.3. Рис. 8.3. Блокировка индексной записи по первичному ключу, транзакция READ COMMITTED 1 Чтобы все же спуститься в эту нору, следуйте за кроликом по имени «A Critique of ANSI SQL Isolation Levels» («Критика уровней изоляции ANSI SQL», https://oreil.ly/WF6NT): классический материал по данной теме.
282 | Глава 8 Почему бы не использовать READ COMMITTED? Этот вопрос относится к характеристике паттерна доступа (см. гл. 4, раздел «Изоляция транзакций»), которая делает его полностью специфичным для конкретного приложения, даже для конкретного запроса. В транзакции READ COMMITTED имеет два важных побочных эффекта:  одна и та же инструкция чтения может возвращать разные строки при повтор- ном выполнении;  одна и та же инструкция записи может повлиять на разные строки при повтор- ном выполнении. Эти побочные эффекты объясняют, почему InnoDB не нужно использовать для чтения согласованный снимок состояния или блокировать интервалы для записи: READ COMMITTED позволяет транзакции считывать или записывать разные записи (для зафиксированных изменений) в разное время. (MVCC и логи отмены (см. ниже соответствующий раздел) определяют этот самый согласованный снимок.) Внимательно рассмотрите эти побочные эффекты применительно к вашему приложению. Если вы уверены, что они не приведут к чтению, записи или возврату некорректных данных транзакции, то да — READ COMMITTED уменьшает количество блокировок и логов отмены, что помогает повысить производительность. Блокировка интервала Блокировка интервалов носит чисто запретительный характер: она не позволяет другим транзакциям вставлять строки в интервал. Это все, что она делает. Несколько транзакций могут заблокировать один и тот же интервал, поскольку все блокировки интервалов совместимы с другими блокировками интервалов. Но, поскольку блокировки интервалов не позволяют другим транзакциям вставлять строки в интервал, лишь одна транзакция может вставлять строки в интервал, когда это единственная транзакция, блокирующая его. Две или более блокировки на одном и том же интервале не позволяют всем транзакциям вставлять в него строки. Цель блокировки интервалов узка: предотвратить вставку строк в интервал другими транзакциями. Но создается данная блокировка повсеместно: любым запросом, который обращается к интервалу. Даже чтение ничего может вызвать блокировку интервалов, которая блокирует вставку строк: BEGIN; SELECT * FROM elem WHERE id = 3 FOR SHARE; SELECT index_name, lock_type, lock_mode, lock_status, lock_data FROM performance_schema.data_locks WHERE object_name = 'elem'; index_name NULL PRIMARY lock_type TABLE RECORD lock_mode IS S,GAP lock_status GRANTED GRANTED lock_data NULL 5 На первый взгляд, этот SELECT кажется безобидным: SELECT в REPEATABLE READ использует согласованный снимок состояния, а для FOR SHARE создает только разделяемые
Транзакции | 283 блокировки, поэтому не будет блокировать другие чтения. Что еще более важно, SELECT не соответствует ни одной строке: таблица elem имеет значения первичного ключа 2 и 5, а не 3. Никаких строк, никаких запираний — верно? Неверно. Получая доступ к интервалу с помощью READ REPEATABLE и SELECT...FOR SHARE, вы вызываете одиночную блокировку интервала: рис. 8.4. Рис. 8.4. Одиночная блокировка интервала Я называю это одиночной блокировкой интервала, потому что она не сопровождается блокировкой следующего ключа или блокировкой намерения вставки; она идет отдельно. Все блокировки интервалов, разделяемые или монопольные, не позволяют другим транзакциям вставлять строки в интервал. Эта безобидная инструкция SELECT на самом деле — коварный блокировщик для INSERT. Чем больше интервал, тем больше блокировка, что в следующем разделе иллюстрируется вторичным индексом. Простота, с которой создаются блокировки интервалов при любом доступе к интервалу — часть ответа на интригующий вопрос в разделе «Блокировка индексной записи и следующего ключа» выше в этой главе: зачем блокировать супремум псевдозаписи, которая включает все значения первичного ключа больше 5, когда условие таблицы исключает значения первичного ключа больше 5? Сперва позвольте мне довести интригу до максимума. Вот исходный запрос и его блокировка данных: BEGIN; UPDATE elem SET c='' WHERE id BETWEEN 2 AND 5; index_name NULL PRIMARY PRIMARY PRIMARY lock_type TABLE RECORD RECORD RECORD lock_mode IX X,REC_NOT_GAP X X lock_status GRANTED GRANTED GRANTED GRANTED lock_data NULL 2 supremum pseudo-record 5 Теперь тот же запрос, но с оператором IN вместо оператора BETWEEN: BEGIN; UPDATE elem SET c='' WHERE id IN (2, 5); index_name NULL PRIMARY PRIMARY lock_type TABLE RECORD RECORD lock_mode IX X,REC_NOT_GAP X,REC_NOT_GAP lock_status GRANTED GRANTED GRANTED lock_data NULL 2 5 Обе транзакции относятся к REPEATABLE READ, и оба запроса имеют один и тот же план EXPLAIN: доступ к диапазону по первичному ключу. Но новый запрос получает бло-
284 | Глава 8 кировку индексных записей только для соответствующих строк. Что это за магия? На рис. 8.5 показано, что происходит для каждого запроса. Доступ к строкам для BETWEEN происходит так, как вы могли бы ожидать: от 2 до 5 и все, что между ними. Упрощенно говоря, последовательность доступа к строкам для BETWEEN такова: 1. Считать строку со значением индекса 2. 2. Совпадение строк: блокировка индексной записи. 3. Следующее значение индекса: 5. 4. Пройти интервал от 2 до 5. 5. Считать строку со значением индекса 5. 6. Совпадение строк: блокировка следующего ключа. 7. Следующее значение индекса: супремум. 8. Пройти интервал от 5 до супремума. 9. Конец индекса: блокировка следующего ключа. Рис. 8.5. Сравнение доступа к диапазону для BETWEEN и IN, транзакция REPEATABLE READ Но последовательность доступа к строкам для IN намного проще: 1. Считать строку со значением индекса 2. 2. Совпадение строк: блокировка индексной записи. 3. Считать строку со значением индекса 5. 4. Совпадение строк: блокировка индексной записи. Несмотря на точно такой же план EXPLAIN и сопоставление одних и тех же строк, запросы обращаются к строкам по-разному. Исходный запрос (BETWEEN) обращается к интервалам; поэтому он использует блокировку следующего ключа для блокировки интервалов. Новый запрос (IN) не обращается к интервалам; поэтому он использует блокировку индексных записей. Но не заблуждайтесь: оператор IN не исключает блокировку интервалов. Если условие нового запроса для таблицы — IN (2, 3, 5), это означает доступ к интервалу между значениями 2 и 5 и вызывает блокировку интервала (а не блокировку следующего ключа):
Транзакции | 285 BEGIN; UPDATE elem SET c='' WHERE id IN (2, 3, 5); index_name NULL PRIMARY PRIMARY PRIMARY lock_type TABLE RECORD RECORD RECORD lock_mode IX X,REC_NOT_GAP X,REC_NOT_GAP X,GAP lock_status GRANTED GRANTED GRANTED GRANTED lock_data NULL 2 5 5 У вас есть одиночная блокировка интервала: X,GAP. Но обратите внимание: на супремуме псевдозаписи нет блокировки следующего ключа, потому что IN (2, 3, 5) не имеет доступа к этому интервалу. Будьте внимательнее с интервалами. Блокировку интервалов легко отключить с помощью READ COMMITTED. Транзакция с READ COMITTED не нуждается в блокировке интервалов (или блокировке следующего ключа), поскольку индексным записям в интервале разрешено изменяться, и каждый запрос при выполнении получает доступ к самым последним изменениям (зафиксированным строкам). READ COMMITTED отменяет даже одиночную блокировку интервала, вызванную SELECT * FROM elem WHERE id = 3 FOR SHARE. Вторичные индексы Вторичные индексы влекут за собой потенциально широкомасштабные (можно сказать, широкого диапазона) последствия в отношении блокировки строк, — особенно неуникальные индексы. Вспомним, что упрощенная таблица elem (пример 8.1) имеет неуникальный вторичный индекс в столбце a. Имея это в виду, давайте посмотрим, как следующий UPDATE в транзакции REPEATABLE READ блокирует индексные записи во вторичном индексе и первичном ключе: BEGIN; UPDATE elem SET c='' WHERE a BETWEEN 'Ar' AND 'Au'; SELECT index_name, lock_type, lock_mode, lock_status, lock_data FROM performance_schema.data_locks WHERE object_name = 'elem' ORDER BY index_name; index_name NULL a a a PRIMARY PRIMARY lock_type TABLE RECORD RECORD RECORD RECORD RECORD lock_mode IX X X X X,REC_NOT_GAP X,REC_NOT_GAP lock_status GRANTED GRANTED GRANTED GRANTED GRANTED GRANTED lock_data NULL supremum pseudo-record 'Au', 2 'Ar', 5 2 5 Рис. 8.6 иллюстрирует эти шесть блокировок записей: четыре на вторичном индексе и две на первичном ключе.
286 | Глава 8 Рис. 8.6. Блокировка следующего ключа для вторичного индекса, транзакция с REPEATABLE READ соответствует только двум строкам, но блокирует весь вторичный индекс, что предотвращает вставку каких-либо значений. Блокировки на вторичном индексе аналогичны блокировкам на рис. 8.2. Но теперь есть блокировка следующего ключа для первой записи во вторичной индексной записи: кортеж ('Ar', 5), где 5 — соответствующее значение первичного ключа. Эта блокировка следующего ключа изолирует диапазон от новых повторяющихся значений «Ar». Например, это предотвращает вставку кортежа ('Ar', 1), который по сортировке встает перед ('Ar', 5). UPDATE Обычно InnoDB не блокирует весь вторичный индекс целиком. В наших примерах это случается лишь потому, что там только две записи индекса (как в первичном ключе, так и в неуникальном вторичном индексе). Но вспомните раздел «Экстремальная селективность» в главе 2: чем ниже селективность, тем больше интервалы. В качестве экстремального примера: если неуникальный индекс имеет 5 уникальных значений, равномерно распределенных по 100 000 строкам, т. е. 20 000 записей на строку (100 000 строк/5 кардинальности) или 20 000 записей на интервал. Чем ниже селективность индекса, тем больше интервалы в записях. READ COMMITTED позволяет избежать блокировки интервалов даже для неуникальных вторичных индексов, поскольку только совпадающие строки блокируются блокировкой индексных записей. Но давайте не будем слишком упрощать себе задачу; давайте продолжим изучать блокировки данных InnoDB на неуникальных вторичных индексах для различных видов изменений данных. В конце предыдущего раздела замена оператора BETWEEN на IN предотвратила блокировку интервала, но это не работает с неуникальным индексом. Фактически InnoDB добавляет блокировку интервала в этом случае: BEGIN; UPDATE elem SET c='' WHERE a IN ('Ar', 'Au');
Транзакции | 287 SELECT index_name, lock_type, lock_mode, lock_status, lock_data FROM performance_schema.data_locks WHERE object_name = 'elem' ORDER BY index_name; index_name a lock_type RECORD lock_mode X,GAP lock_status GRANTED lock_data 'Au', 2 Я удалил исходные блокировки данных из выходных данных (они идентичны), чтобы выделить новую блокировку интервала в кортеже ('Au', 2). Строго говоря, эта блокировка интервала избыточна при блокировке следующего ключа в том же кортеже, но это не приводит к некорректной блокировке или доступу к данным. Поэтому просто оставьте все как есть и никогда не забывайте: InnoDB полна чудес и тайн. Какой была бы жизнь без них? Важно изучить блокировки данных, потому что InnoDB также полна сюрпризов. Хотя этот раздел скрупулезно детализирован, все это едва ли лежит на поверхности — тема блокировки в InnoDB глубока, и в глубине скрываются секреты. Например, какие блокировки данных потребуются InnoDB, если «Au» заменить на «Go»? Давайте рассмотрим блокировки данных при этом изменении: BEGIN; UPDATE elem SET a = 'Go' WHERE a = 'Au'; index_name NULL a a a PRIMARY lock_type TABLE RECORD RECORD RECORD RECORD lock_mode IX X X X, GAP X,REC_NOT_GAP lock_status GRANTED GRANTED GRANTED GRANTED GRANTED lock_data NULL supremum pseudo-record 'Au', 2 'Go', 2 2 На рис. 8.7 визуализированы эти четыре блокировки данных. Рис. 8.7. Обновление неуникального значения вторичного индекса, транзакция с REPEATABLE READ
288 | Глава 8 Значение «Au» исчезло, изменено на «Go». Но InnoDB по-прежнему удерживает блокировку следующего ключа для кортежа ('Au', 2). Новый «Go» не имеет блокировку записи или блокировку следующего ключа, только блокировку интервала перед кортежем: ('Go', 2). Так что же блокирует новую запись «Go»? Это какой-то побочный эффект REPEATABLE READ? Давайте изменим уровень изоляции транзакций и повторно проверим блокировку данных: SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; UPDATE elem SET a = 'Go' WHERE a = 'Au'; index_name NULL a PRIMARY lock_type TABLE RECORD RECORD lock_mode IX X,REC_NOT_GAP X,REC_NOT_GAP lock_status GRANTED GRANTED GRANTED lock_data NULL 'Au', 2 2 Переключение на READ COMMITTED отключает блокировку интервала, как и ожидалось, но где блокировка — любая блокировка — для нового значения «Go»? Операция записи всегда блокирует строку — по крайней мере я это сказал в начале раздела «Блокировка строк» (см. выше). И все же InnoDB сообщает об отсутствии блокировок для этой операции записи... Что если я скажу вам, что InnoDB настолько оптимизирована, что может блокировать без блокировки? Давайте воспользуемся следующим типом блокировки данных — блокировкой намерения, чтобы заглянуть в опасную глубину блокировок InnoDB и разрешить эту загадку. Блокировка намерения вставки Блокировка намерения вставки (англ. insert intention lock) — это особый тип блокировки интервала, который означает, что транзакция вставляет строку в интервал, когда интервал не блокирован другими транзакциями. Лишь блокировки интервалов препятствуют блокировкам намерения вставки. (Помните: блокировки интервалов включают в себя блокировки следующего ключа, поскольку последнее представляет собой комбинацию блокировки индексных записей и блокировки интервалов.) Блокировки намерения вставки совместимы с другими блокировками намерения вставки (не препятствуют им). Это важно для эффективного выполнения INSERT, поскольку позволяет нескольким транзакциям одновременно вставлять разные строки в один и тот же интервал. Как InnoDB обрабатывает дубликаты ключей? Вернемся к этому после разбора других аспектов блокировки намерения вставки, который прояснит данный вопрос. Блокировка интервалов предотвращает INSERT. Блокировка намерения вставки разрешает INSERT.
Транзакции | 289 Блокировки намерения вставки обладают тремя особенностями: 1. Они не блокируют интервал, поскольку, как следует из термина «намерение», представляют собой будущее действие: вставку строки в момент, когда другие транзакции не удерживают блокировку интервалов. 2. Блокировки намерения вставки создаются и сообщаются только тогда, когда конфликтуют с блокировками интервалов, удерживаемыми другими транзакциями; в противном случае блокировки намерения вставки не создаются и не сообщаются транзакцией, вставляющей строку. 3. Если блокировка намерения вставки создается, она используется один раз и снимается сразу после предоставления; но InnoDB продолжает сообщать об этом до тех пор, пока транзакция не будет завершена. В некотором смысле блокировки намерения вставки не являются блокировками, потому что не препятствуют доступу. Они больше похожи на условия ожидания, которые InnoDB использует, чтобы сигнализировать, когда транзакция может быть продолжена с помощью INSERT. Предоставление блокировки намерения вставки — это сигнал. Но если для транзакции нет необходимости ждать, потому что нет конфликта блокировок интервала, тогда она и не ждет, и вы не увидите блокировку намерения вставки, потому что таковой не было создано. Давайте посмотрим на блокировку намерения вставки в действии. Начнем с блокировки интервала между значениями первичного ключа 2 и 5; затем во второй транзакции попробуем вставить строку со значением первичного ключа 3: -- First transaction BEGIN; UPDATE elem SET c='' WHERE id BETWEEN 2 AND 5; -- Second transaction BEGIN; INSERT INTO elem VALUES (3, 'As', 'B', 'C'); index_name PRIMARY ... lock_type RECORD lock_mode X,GAP,INSERT_INTENTION lock_status WAITING lock_data 5 X,GAP,INSERT_INTENTION в столбце lock_mode — это блокировка намерения вставки. Она также отражена как X,INSERT_INTENTION (не показано) при блокировке и вставке в интервал между максимальным значением индексной записи и супремумом псевдозаписи. Первая транзакция (first transaction) блокирует интервал перед значением первичного ключа 5. Эта блокировка интервала препятствует вставке второй транзакции в интервал, так что создает блокировку намерения вставки и ожидает. Как только первая транзакция фиксируется (или откатывается), интервал разблокируется, предоставляется блокировка намерения вставки, и вторая транзакция (second transaction) вставляет строку:
290 | Глава 8 -- First transaction COMMIT; -- Second transaction -- INSERT executes index_name NULL PRIMARY lock_type TABLE RECORD lock_mode IX X,GAP,INSERT_INTENTION lock_status GRANTED GRANTED lock_data NULL 5 Как отмечалось ранее, InnoDB продолжает сообщать о блокировке намерения вставки, даже если после предоставления она используется один раз и немедленно снимается. Следовательно, кажется, что интервал заблокирован, но это иллюзия — уловка InnoDB, чтобы заманить нас глубже. Вы можете убедиться, что это иллюзия, вставив другую строку в интервал со значением первичного ключа 4; ничего не препятствует. Почему InnoDB продолжает сообщать о блокировке (намерения вставки), которой на самом деле нет? Немногие смертные знают ответ, и это неважно. Посмотрите сквозь иллюзию, чтобы увидеть, что это было: в прошлом транзакция блокировалась перед вставкой строки в интервал. Для полноты картины и перехода к более глубоким аспектам блокировки InnoDB, особенно в отношении блокировки намерения вставки — вот что вы видите, когда INSERT не препятствует блокировке интервалов: BEGIN; INSERT INTO elem VALUES (9, 'As', 'B', 'C'); -- Does not block index_name NULL lock_type TABLE lock_mode IX lock_status GRANTED lock_data NULL Никаких блокировок индексных записей вообще. Вот как на первый взгляд работают блокировки намерения вставки, но мы пришли сюда, чтобы заглянуть вглубь блокировки InnoDB, так давайте углубимся, задав вопрос, который привел нас сюда: почему нет блокировки записи (или следующего ключа) для вновь вставленной строки? Это та же загадка, что и в предыдущем разделе: нет блокировки для нового значения «Go». Вот в чем секрет: InnoDB имеет эксплицитные и имплицитные (т. е. явные и скрытые) блокировки и сообщает только об эксплицитных2. Эксплицитные блокировки существуют в виде структур блокировки в памяти; поэтому InnoDB может сообщать о них. Но имплицитных блокировок в этом смысле не существует: нет структур блокировки; следовательно, InnoDB не о чем сообщать. В предыдущем примере, INSERT INTO elem VALUES (9, 'As', 'B', 'C'), индексная запись для новой строки существует, но строка не зафиксирована (поскольку транзакция не зафиксирована). Если другая транзакция пытается заблокировать строку, она обнаруживает три условия: 2 Спасибо Якубу Лопушанскому (Jakub Łopuszański), что раскрыл мне это.
Транзакции | 291  строка не зафиксирована;  строка принадлежит другой транзакции;  строка эксплицитно не заблокирована. Затем происходит магия: запрашивающая транзакция — транзакция, которая пытается заблокировать запись индекса, преобразует имплицитную блокировку в эксплицитную от имени той транзакции, что создала запись. Да, это означает, что одна транзакция создает блокировку для другой транзакции, но это не самая запутанная часть. Поскольку запрашивающая транзакция создает блокировку, которую сама же и пытается получить, на первый взгляд кажется, будто InnoDB сообщает, что транзакция ожидает блокировки, которую сама и удерживает, — транзакция сама себе препятствует. Есть способ проникнуть сквозь эту иллюзию, но мы зашли слишком далеко. Я надеюсь, что вам, как инженеру, использующему MySQL, никогда не придется опускаться до такой глубины блокировок InnoDB, чтобы добиться замечательной производительности. Однако я привел нас сюда по двум причинам. Во-первых, несмотря на иллюзии, основы блокировки строк InnoDB в отношении уровней изоляции транзакций доступны для понимания и применения. Теперь вы фантастически хорошо подготовлены к решению всех распространенных проблем с блокировками строк InnoDB — и не только. Во-вторых, InnoDB заставила меня сделать это, потому что я слишком долго всматривался в ее бездну; и когда все слилось в одно, я понял, что упал с обрыва и уже не смогу вернуться. Не спрашивайте, почему она блокирует супремум псевдозаписи за пределами диапазона условий таблицы. Не спрашивайте, для чего ей лишние блокировки интервалов. Не спрашивайте, почему она преобразует имплицитные блокировки. Не спрашивайте, иначе вопросы никогда не прекратятся. Бегите, спасайтесь. MVCC и логи отмены InnoDB использует управление параллельным доступом посредством многоверсионности (англ. multiversion concurrency control, MVCC) и логи отмены для выполнения свойств A (атомарность), C (согласованность) и I (изоляция) ACID. (Для выполнения пункта D (устойчивость) InnoDB использует лог транзакций (см. гл. 6, раздел «Лог транзакций»).) Управление параллельным доступом посредством многоверсионности означает, что изменения в строке создают новую версию строки. MVCC — фишка не только InnoDB; это распространенный метод, который используют многие хранилища данных. Когда строка создается впервые, это версия 1. Когда она впервые обновляется, это версия 2. Основа MVCC очень проста, но все быстро становится сложнее и интереснее. Использование термина «логи отмены» является намеренным упрощением, поскольку полная структура процесса ведения лога отмены сложна. Термин «логи отмены» достаточно точен, чтобы понять, что он делает и как это влияет на производительность.
292 | Глава 8 В логах отмены записывается, как откатить изменения к предыдущей версии строки. На рис. 8.8 показана одна строка с пятью версиями и пятью логами отмены, которые позволяют MySQL откатывать изменения к предыдущим версиям строк. Строка отсылает к разделу «Таблицы InnoDB являются индексами» в главе 2: это строка со значением первичного ключа 2 в таблице elem, отображенная как конечный узел первичного ключа. Для краткости я включаю только значение первичного ключа (2), версию строки (с v1 по v5) и значение столбца a («Au» для v5); два других столбца, b и c, не показаны. Версия 5 (внизу справа на рис. 8.8) — это текущая строка, которую будут считывать все новые транзакции, но давайте начнем с самого начала. Строка создается со значением «железо» («Fe»): версия 1 в верхнем левом углу. Существует лог отмены для версии 1, потому что INSERT создает первую версию строки. Затем столбец a обновляется (UPDATE) — железо меняется на титаниум («Ti»): это версия 2. При создании версии 2 MySQL также создает лог отмены, в котором записывается, как откатить изменения версии 2 для восстанавления версии 1. (В следующем абзаце я поясню, почему версия 1 имеет сплошной контур и значок камеры, а у версии 2 пунктирный контур.) Затем в столбце a титаниум меняется на серебро («Ag»): это версия 3. MySQL создает лог отмены, в котором записывается, как откатить изменения версии 3, и этот лог отмены связан с предыдущим, чтобы MySQL мог при необходимости восстановить версию 2. Дальше еще два обновления строки: серебро меняем на калифорний («Cf») для версии 4 и калифорний на золото («Au») для версии 5. Рис. 8.8. Одна строка с пятью версиями и пятью логами отмены Существуют два типа логов отмены: логи отмены вставки для INSERT и логи отмены обновлений для UPDATE и DELETE. Я ссылаюсь просто на логи отмены, что подразумевает оба типа.
Транзакции | 293 Версия 1 имеет сплошной контур и значок камеры, потому что активная транзакция (не показана) содержит согласованный снимок состояния (англ. consistent snapshot) на данный момент в истории базы данных. Позвольте мне разъяснить это предложение. InnoDB поддерживает четыре уровня изоляции транзакций, но обычно используются только два: REPEATABLE READ (по умолчанию) и READ COMMITTED. В транзакции с уровнем REPEATABLE READ первое чтение создает согласованный снимок состояния (или сокращенно снимок (англ. snapshot)): виртуальное представление базы данных (всех таблиц) на момент выполнения SELECT. Снимок состояния сохраняется до конца транзакции и используется всеми последующими операциями чтения для доступа к строкам только в этой точке истории базы данных. Изменения, внесенные другими транзакциями после этой точки, условно говоря, не видны в исходной транзакции. Исходя из того, что другие транзакции вносят эти изменения, можно сказать, что снимок состояния исходной транзакции становится все более старым представлением базы данных, в то время как транзакция остается активной (не выполняет COMMIT или ROLLBACK). Будто она застряла в 1980-х, и единственные музыканты, которых она слушает, — это Пэт Бенатар, Стиви Никс и Тейлор Дейн: старые, но все еще великие. Поскольку версия 5 — текущая версия строки, новые транзакции создают снимок состояния с этой точки в истории базы данных, поэтому-то здесь сплошной контур и значок камеры. Важный вопрос: почему версии 2, 3 и 4 все еще существуют, когда нет транзакций, содержащих снимки состояния в соответствующих точках истории базы данных? Они существуют ради поддержания снимка состояния для версии 1, поскольку MySQL использует логи отмены для восстановления старых версий строк. MySQL использует логи отмены, чтобы восстановливать старые версии строк для снимков состояния. Легко восстановить ситуацию с рис. 8.8. Во-первых, сразу после вставки строки запустите транзакцию и создайте снимок состояния для строки версии 1 (рис. 8.8), выполнив инструкцию SELECT: BEGIN; SELECT a FROM elem WHERE id = 2; -- Returns row version 1: 'Fe' Поскольку COMMIT отсутствует, эта транзакция все еще активна и сохраняет свой снимок состояния для всей базы данных, которая в этом примере представляет собой просто строку версии 1. Давайте назовем это исходной транзакцией. Затем обновите строку четыре раза, чтобы создать версию 5: -- autocommit enabled UPDATE elem SET a = 'Ti' UPDATE elem SET a = 'Ag' UPDATE elem SET a = 'Cf' UPDATE elem SET a = 'Au' WHERE WHERE WHERE WHERE id id id id = = = = 2; 2; 2; 2;
294 | Глава 8 autocommit включен по умолчанию в MySQL, поэтому для первой (активной) транзакции требуется эксплицитное (явное) BEGIN, а для четырех инструкций UPDATE — нет. Теперь MySQL находится в состоянии, представленном на рис. 8.8. Если исходная транзакция снова выполняет SELECT a FROM elem WHERE id = 2, она считывает версию 5 (это не опечатка), но (условно) видит, что эта версия новее, чем точка в истории базы данных, установленная ее снимком состояния. Следовательно, MySQL использует логи отмены для отката строки и восстановления версии 1, которая согласована со снимком состояния, созданным первой инструкцией SELECT. Когда исходная транзакция зафиксирована и предполагается, что никакие другие активные транзакции не содержат старых снимков, MySQL может очистить все связанные логи отмены, потому что новые транзакции всегда начинаются с текущей версии строки. Когда транзакции работают хорошо, весь процесс не имеет значения для производительности. Но вы уже знаете: проблемные транзакции могут негативно сказаться на производительности всего процесса. В разделе «Распространенные проблемы» (см. ниже) рассматривается, как и почему; но до тех пор вам необходимо узнать больше подробностей о MVCC и логах отмены. В транзакции с READ COMMITTED каждое чтение создает новый снимок. В результате каждое чтение (англ. read) обращается к последней зафиксированной (англ. commited) версии строки, отсюда и название READ COMMITTED. Поскольку используются снимки состояния, логи отмены по-прежнему создаются, но с READ COMMITTED это почти никогда не приносит проблем, поскольку каждый снимок сохраняется только на время чтения. Если чтение занимает очень много времени и у базы данных значительная пропускная способность для операций записи, можно заметить накопление логов обновлений (как увеличение длины списка истории). В противном случае READ COMMITTED практически освободно от ведения логов отмены. Снимки состояния влияют только на чтение (SELECT), они никогда не используются для операций записи. Операции записи всегда тайно считывают текущие строки, даже если транзакция не может «видеть» их посредством SELECT. Это двойное видение предотвращает хаос. Например, представьте, что другая транзакция вставляет новую строку со значением первичного ключа 11. Если исходная транзакция попытается вставить строку с тем же значением первичного ключа, MySQL вернет дублирующее значение ключа, поскольку значение первичного ключа существует, даже если транзакция не может увидеть его посредством SELECT. Более того, снимки состояния очень постоянны: в транзакции нет способа переместить снимок в более новую точку истории базы данных. Если приложению, выполняющему транзакцию, требуется более новый снимок, оно должно зафиксировать транзакцию и начать новую, чтобы создать его. Операции записи генерируют логи отмены, которые сохраняются до конца транзакции независимо от уровня ее изоляции. До сих пор я фокусировался на логах отмены в отношении восстановления старых версий строк для снимков состояния, но они также используются в ROLLBACK для отмены изменений, внесенных при записи. Последнее, что нужно знать о MVCC: логи отмены сохраняются в буферном пуле InnoDB. Возможно, вы помните из раздела «Сброс страниц» (см. гл. 6), что сме-
Транзакции | 295 шанные страницы содержат разные внутренние данные, не описанные в этой книге». Смешанные страницы включают в себя логи отмены (и многие другие внутренние структуры данных). Поскольку логи отмены хранятся на страницах буферного пула, они используют память и периодически сбрасываются на диск. Существует несколько системных переменных и метрик, связанных с логами отмены; как инженеру, использующему MySQL, вам нужно знать и отслеживать только одну: HLL, впервые представленную в главе 6, раздел «Длина списка истории (метрика)» и объясненную ниже. Впрочем, MVCC и логи отмены работают безупречно, пока приложение избегает всех распространенных проблем (см. ниже в данной главе соответствующий раздел). Одной из таких проблем являются заброшенные транзакции, поэтому давайте избежим ее, зафиксировав исходную транзакцию: COMMIT; Прощай, снимок состояния. Прощайте, логи отмены. Здравствуй, длина списка истории... Длина списка истории Длина списка истории (англ. history list length, HLL) подсчитывает количество старых версий строк, которые не были удалены или сброшены на диск. Исторически (без шуток) HLL было трудно определить, потому что полная структура ведения лога отмены сложна: Сегменты отката (англ. rollback segments) └── Слоты отмены (англ. undo slots) └── Сегменты лога отмены (англ. undo log segments) └── Логи отмены (англ. undo logs) └── Записи лога отмены (англ. undo log records) Эта сложность затмевает собой любую простую взаимосвязь между логированием отмены и HLL, включая единицу измерения. Самая простая функциональная (хотя и технически некорректная) единица HLL — это изменения. Если значение HLL равно 10 000, вы можете прочитать это как 10 000 изменений. Ознакомившись с разделом «MVCC и логи отмены» (см. выше), вы знаете, что изменения сохраняются (не удаляются) в памяти (не сбрасываются на диск) для восстановления старых версий строк. Таким образом, можно уверенно утверждать, что HLL подсчитывает количество старых версий строк, которые не были удалены или сброшены на диск. HLL более 100 000 — это проблема, не игнорируйте ее. Несмотря на то что истинная техническая природа HLL неуловима даже для экспертов MySQL, полезность очевидна и неоспорима: HLL — предвестник проблем, связанных с транзакциями. Всегда следите за HLL (см. гл. 6, раздел «Длина списка истории (метрика)»), предупреждайте, когда значение HLL слишком высоко (больше 100 000), и устраняйте проблему, которая, несомненно, окажется одной из распространенных, освещаемых в следующем разделе.
296 | Глава 8 Хотя я предостерегаю от оповещения о пороговых значениях в разделе «Охота на миражи (пороговые значения)» (см. гл. 6), HLL является исключением: оповещать, когда HLL превышает 100 000, надежно и действенно. Оповещайте об HLL более 100 000. Теоретически у HLL есть максимальное значение, но производительность MySQL наверняка упадет задолго до него3. Например, всего несколько недель назад, когда я писал это, произошел сбой экземпляра MySQL в облаке: четыре часа длительной транзакции довели HLL до 200 000, и MySQL на два часа вышла из строя. Поскольку ведение лога отмены невероятно эффективно, в HLL существует огромная свобода действий в отношении значения, при котором производительность MySQL снизится или в худшем случае произойдет сбой. Я видел сбой MySQL при 200 000, но также видел, как она отлично работает и после 200 000. Одно можно сказать наверняка: если HLL увеличивается бесконтрольно, это вызовет проблему: либо заметно снизится производительность, либо MySQL выйдет из строя. Я хочу, чтобы вы были первым инженером в истории, который использовал MySQL и никогда не испытывал проблем с HLL. Это высокая цель, но я призываю вас стремиться к звездам. С этой целью я намеренно залил экземпляр MySQL инструкциями UPDATE, чтобы увеличить HLL — накопить тысячи старых версий строк. В табл. 8.2 показано влияние HLL на время отклика на запрос для одной выбранной строки: SELECT * FROM elem WHERE id=5 в активной транзакции c REPEATABLE READ. Таблица 8.2. Влияние HLL на время отклика на запрос HLL Время ответа (мс) Базовое увеличение (%) 0 0,200 мс 495 0,612 мс 206 1089 1,012 мс 406 2079 1,841 мс 821 5056 3,673 мс 1,737 11546 8,527 мс 4,164 Этот пример не означает, что HLL увеличит время отклика на запрос, как показано; только доказывает, что HLL может это сделать. Из раздела «MVCC и логи отмены» (выше в этой главе) и этого раздела вы знаете, почему: SELECT в активной транзакции c REPEATABLE READ имеет согласованный снимок состояния для строки 5 (id=5), но инструкции UPDATE на этой строке генерируют новые версии строки. Каждый раз, 3 В storage/innobase/trx/trx0purge.cc исходного кода MySQL 8.0 блок отладки генерирует предупреждение, когда HLL превышает 2 000 000)
Транзакции | 297 когда выполняется инструкция SELECT, она просматривает логи отмены, чтобы восстановить исходную версию строки для согласованного снимка состояния, и этот процесс увеличивает время отклика на запрос. Увеличение времени отклика — достаточное доказательство, но мы профессионалы, так что давайте докажем это неопровержимо. В конце раздела «MVCC и логи отмены» (см. выше) я упоминаю, что логи отмены хранятся в виде страниц в буферном пуле InnoDB. Поэтому инструкция SELECT, по идее, должна получить доступ к непомерному количеству страниц. Чтобы убедиться в этом, я использую Percona Server (https://oreil.ly/OWUYR), потому что там расширенный лог медленных запросов выводит на экран количество отдельных страниц, к которым обращались, при настройке log_slow_verbosity = innodb: # Query_time: 0.008527 # InnoDB_pages_distinct: 366 Обычно SELECT в этом примере обращается к одной странице для поиска одной строки по первичному ключу. Но когда согласованный снимок для SELECT устарел (а размер HLL велик), InnoDB просматривает сотни страниц лога отмены, чтобы восстановить старую строку. MVCC, логи отмены и HLL — все это нормальные и хорошие компромиссы: небольшое снижение производительности ради большого количества конкурентности. Только когда HLL чрезмерно велик — более 100 000, следует принять меры для устранения причины этого, которая почти наверняка окажется одной из следующих распространенных проблем. Распространенные проблемы Проблемы с транзакциями возникают из-за запросов, составляющих транзакцию, того, как быстро приложение выполняет эти запросы, и того, как быстро приложение фиксирует транзакцию. Хотя единичный запрос с включенной автоматической фиксацией (autocommit, https://oreil.ly/oQtD2) технически тоже транзакция, которая может вызвать эти проблемы (за исключением заброшенных транзакций (см. об этом ниже)), мы сфокусируемся на транзакциях с несколькими инструкциями — тех, что начинаются с BEGIN (или START TRANSACTION), выполняют несколько запросов и заканчиваются COMMIT (или ROLLBACK). Влияние транзакции с несколькими инструкциями на производительность может быть больше, чем сумма ее частей — запросов, составляющих транзакцию, — поскольку блокировки и логи отмены сохраняются до фиксации транзакции (или отката). Помните: MySQL очень терпелива, даже слишком терпелива. Если приложение не фиксирует транзакцию, MySQL будет ждать аж до тех пор, пока последствия этой активной транзакции не навлекут беду. К счастью, ни одну из этих проблем не трудно обнаружить или устранить. HLL является предвестником большинства из них, поэтому всегда надо следить за этим (см. гл. 6, раздел «Длина списка истории (метрика)» и гл. 8, раздел «Длина списка истории»). Чтобы не перегружать вас подробностями, я объясню, как находить
298 | Глава 8 проблемные транзакции и сообщать о них в разделе «Отчетность» ниже в этой главе. Большие транзакции (размер транзакции) Большая транзакция изменяет чрезмерное количество строк. Какое количество будет чрезмерным? Это относительно, но инженеры всегда поймут, когда увидят. Например, если транзакция изменила 250 000 строк, и вы знаете, что в базе данных их всего 500 000, это чрезмерно. (Или, по крайней мере, это подозрительный паттерн доступа (см. гл. 4, раздел «Результирующий набор»)). Как правило, понятие «размер транзакции» относится к числу измененных строк: чем больше измененных строк, тем больше транзакция. Однако для групповой репликации MySQL (https://oreil.ly/wH10S) размер транзакции означает нечто иное: см. раздел «Group Replication Limitations» («Ограничения групповой репликации», https://oreil.ly/cJhWF) в руководстве по MySQL. Если транзакция выполняется на уровне изоляции по умолчанию, REPEATABLE READ, то, скорее всего, число индексных записей, которые она заблокирует, превысит количество измененных строк — из-за блокировки интервалов (см. выше, раздел «Блокировка строк»). Если транзакция выполняется на уровне изоляции READ COMMITTED, то она получает блокировки индексных записей только для каждой измененяемой строки. В любом случае большая транзакция — большой источник конфликтов блокировки, которые могут серьезно снизить пропускную способность операций записи и время отклика. Не забывайте о репликации (см. гл. 7): большие транзакции — основная причина запаздывания репликации (см. гл. 7, раздел «Пропускная способность транзакций»), и они снижают эффективность многопоточной репликации (см. гл. 7, раздел «Уменьшение запаздывания: многопоточная репликация»). Фиксация (или откат) крупных транзакций может происходить заметно медленно, о чем ранее говорилось в разделах «MVCC и логи отмены» (см. выше), «События бинарного лога» (см. гл. 7) и на рис. 6.7. Изменять строки быстро и легко, потому что изменения данных происходят в памяти, но за фиксации MySQL расплачивается серьезной работой по сохранению и репликации изменений данных. Чем транзакции меньше, тем лучше. Насколько меньше? Это тоже относительно и сложно калибруемо, потому что, как я уже отметил, фиксации транзакций требуют платы, т. е. вам нужно откалибровать несколько подсистем. (Это еще сложнее для облака — оно, как правило, ограничивает и настраивает мелкие детали, такие как IOPS.) За исключением массовых операций, требующих калибровки размера пакета (см. гл. 3, раздел «Размер пакета»), калибровка размера транзакции обычно не требуется, потому что, хотя это и распространенная проблема, она обычно разовая: когда она найдена и исправлена, то не повторяется (по крайней мере, некоторое время). В разделе «Отчетность» (см. ниже) показано, как обнаруживать крупные транзакции. Решение в том, чтобы найти в транзакции запрос (или запросы), которые изменяют слишком много строк, и изменить их, чтобы изменять меньше строк. Но это пол-
Транзакции | 299 ностью зависит от запроса, его назначения в приложении и причины, по которой он изменяет так много строк. Какой бы она ни была, главы 1–4 помогут вам понять и исправить запрос. Наконец, если вы будете строго следовать принципу наименьшего объема данных (см. гл. 3, раздел «Принцип наименьшего количества данных»), размер транзакции никогда не будет проблемой. Длительные транзакции Длительная транзакция требует слишком много времени для завершения (фиксации или отката). Но какое время является слишком долгим? Зависит от следующих факторов:  дольше, чем приемлемо для приложения или пользователей;  достаточно долго, чтобы вызвать проблемы (вероятный конфликт) с другими транзакциями;  достаточно долго, чтобы вызвать предупреждение о длине списка истории (HLL). Если только вы не принимаете упреждающих мер в отношении производительности, второй и третий пункты, скорее всего, привлекут ваше внимание к длительной транзакции. Исходя из того, что приложение не ожидает между запросами (что является следующей проблемой — см. ниже раздел «Зависшие транзакции»), у длительных транзакций бывает две причины:  запросы, составляющие транзакцию, выполняются слишком медленно;  приложение выполняет слишком много запросов в транзакции. С первой можно справиться при помощи техник из глав 1–5. Помните: логи отмены и блокировка строк для всех запросов в транзакции сохраняются до тех пор, пока транзакция не будет зафиксирована. С одной стороны, это означает, что оптимизация медленных запросов как решение данной проблемы имеет побочные преимущества: отдельные запросы выполняются быстрее — и транзакция в целом быстрее, что может увеличить общую пропускную способность транзакции. С другой стороны, длительная транзакция может быть достаточно быстрой для приложения, но слишком долгой для других транзакций. Например, предположим, что выполнение транзакции занимает одну секунду, что годится для приложения, но в течение этой секунды оно удерживает блокировки строк, необходимые для другой, более быстрой транзакции. Это создает сложную для отладки проблему, поскольку транзакция может выполняться медленно в рабочей среде, но быстро в изолированных лабораторных условиях (например, на вашем ноутбуке) — где, в отличие от производства, полностью или в значительной степени отсутствуют параллелизм и конфликт транзакций. В подобном случае нужно устранить конфликт блокировок данных, что непросто как минимум в силу скоротечности этих блокировок. Посмотрите примечание по-
300 | Глава 8 сле табл. 8.1 и поговорите со своим администратором базы данных или экспертом по MySQL. Вторую причину можно устранить, изменив приложение так, чтобы оно выполняло меньше запросов в транзакции. Проблема возникает, когда приложение пытается выполнить массовую операцию или программно генерирует запросы внутри транзакции без ограничения их количества. В любом случае, решение заключается в уменьшении или ограничении количества этих запросов. Даже если транзакция не является длительной, это лучшая практика, чтобы гарантировать, что она таковой случайно не станет. Например, когда приложение совсем новое, оно, возможно, вставляет только 5 строк на транзакцию; но годы спустя, когда у него появятся миллионы пользователей, это будет 500 строк на транзакцию, потому что ограничение не было установлено с самого начала. В разделе «Отчетность» (см. ниже) показано, как находить длительные транзакции. Зависшие транзакции Зависшая транзакция ожидает слишком долго после BEGIN, между запросами или перед COMMIT. Зависшие транзакции, скорее всего, тоже будут по факту длительными, но причины разные: у зависших это время ожидания между запросами, а у длительных — время ожидания запросов. На практике зависшая транзакция выглядит как длительная, потому что конечный результат один и тот же: большое время отклика транзакции. Необходим анализ транзакции, чтобы определить, вызвано ли это зависаниями или же медленными запросами. В отсутствие этого анализа инженеры (и эксперты MySQL) часто называют любую медленную транзакцию длительной. Конечно, между запросами всегда есть некоторое время ожидания (как минимум из-за сетевой задержки, нужной для отправки запросов и получения результирующих наборов), но, как и с предыдущими двумя проблемами — вы узнаете о зависшей транзакции, когда увидите ее. Образно выражаясь: когда целое гораздо больше, чем сумма его частей. Говоря технически: когда время отклика транзакции от BEGIN до COMMIT намного больше, чем сумма времени откликов на запросы. Поскольку зависшие транзакции ожидают между запросами (в том числе после BEGIN и перед COMMIT), MySQL не виновата: ожидания вызваны приложением, и причин здесь может быть масса. Частая причина — выполнение трудоемкой логики приложения во время активной транзакции, а не до или после нее. Но иногда этого нельзя избежать; рассмотрим пример, где между SELECT и UPDATE выполняется такая логика, основанная на строке (row), отобранной SELECT: BEGIN; SELECT <row> --- Time-consuming application logic based on the row -UPDATE <row> COMMIT;
Транзакции | 301 Решение в этом случае зависит от логики приложения. Я бы начал с базового вопроса: должны ли эти запросы быть транзакцией? Может ли строка измениться после чтения и перед обновлением? Если строка меняется, нарушает ли это логику? Можно ли, по крайней мере, использовать уровень изоляции READ COMMITTED для отключения блокировки интервалов? Инженеры умны и находят способы исправить подобные случаи; первый шаг — обнаружить проблему, что описано в разделе «Отчетность» ниже в данной главе. Заброшенные транзакции Заброшенная транзакция — это активная транзакция без активного клиентского подключения. У этой проблемы также есть две основные причины:  утечки в соединениях приложений;  полузакрытые соединения. Баг приложения может привести к утечке подключений к базе данных (например, утечке памяти или потоков): объект подключения на уровне кода выходит за пределы области видимости, поэтому больше не используется, но другой код попрежнему ссылается на него, поэтому объект не закрывается и не освобождается (что, вероятно, также приводит к небольшой утечке памяти). Помимо профилирования на уровне приложения, отладки или обнаружения утечек для прямой проверки этого бага вы можете обнаружить его косвенно, если перезапуск приложения исправляет (закрывает) заброшенные транзакции. В MySQL можно увидеть, какие транзакции, возможно, заброшены (как показано ниже в разделе «Отчетность»), но вы не можете напрямую убедиться в этом баге через MySQL, потому что MySQL не знает, что соединение было прервано. Полузакрытых соединений при нормальных обстоятельствах не бывает, потому что MySQL откатывает транзакцию, когда клиентское соединение обрывается по любой причине, обнаруживаемой MySQL или операционной системой. Но проблемы за пределами MySQL и операционной системы могут привести к закрытию клиентской части соединения без закрытия со стороны MySQL — вот почему это называется полузакрытым соединением. MySQL особенно подвержена этому, потому что ее сетевой протокол почти полностью состоит из команд и ответов: клиент отправляет команды, а MySQL отправляет ответ. (Если вам интересно, клиенты отправляют запрос в MySQL с пакетом COM_QUERY, https://oreil.ly/I4RjE.) Между командой и ответом клиент и MySQL соблюдают полное молчание — ни один байт не передается. Как бы мирно это ни звучало, это означает, что полузакрытые соединения остаются незамеченными, пока не пройдут секунды wait_timeout (https:// oreil.ly/zP2bf), значение которого по умолчанию равно 28 800 секунд (8 часов). Будь то баг приложения, вызывающий утечку соединения, или полузакрытое соединение, ошибочно принятое за медитативное сетевое молчание — если это происходит во время активной транзакции (не зафиксированной), результат один и тот же: транзакция остается активной. Любые согласованные снимки состояния или
302 | Глава 8 блокировки данных также остаются активными, потому что MySQL не знает, что транзакция стала заброшенной. По правде говоря, MySQL любит тишину, как и я. Но нам платят за работу, поэтому давайте рассмотрим, как обнаружить все четыре проблемы с транзакциями и сообщить о них. Отчетность MySQL Performance Schema (https://oreil.ly/fgU04) позволяет создавать подробные отчеты о транзакциях; но на момент написания данного текста не существует инструментов, которые это бы облегчали. Я хотел бы посоветовать вам использовать реальные инструменты с открытым исходным кодом, но их нет. Следующие инструкции SQL являются самыми современными. Когда будут разработаны новые, я сообщу об этом в MySQL Transaction Reporting (https://hackmysql.com/trx). А до тех пор давайте сделаем работу старомодным способом: скопируйте и вставьте. Активные транзакции: последние Инструкция SQL в примере 8.2 сообщает о последнем запросе для всех транзакций, активных более 1 секунды. Этот отчет отвечает на вопрос: какие транзакции являются длительными и что они делают прямо сейчас? Пример 8.2. Отчет о последнем запросе для транзакций, активных более 1 секунды SELECT ROUND(trx.timer_wait/1000000000000,3) AS trx_runtime, trx.thread_id AS thread_id, trx.event_id AS trx_event_id, trx.isolation_level, trx.autocommit, stm.current_schema AS db, stm.sql_text AS query, stm.rows_examined AS rows_examined, stm.rows_affected AS rows_affected, stm.rows_sent AS rows_sent, IF(stm.end_event_id IS NULL, 'running', 'done') AS exec_state, ROUND(stm.timer_wait/1000000000000,3) AS exec_time FROM performance_schema.events_transactions_current trx JOIN performance_schema.events_statements_current stm USING (thread_id) WHERE trx.state = 'ACTIVE' AND trx.timer_wait > 1000000000000 * 1\G Чтобы увеличить время, измените 1 перед \G. Таймеры схемы производительности используют пикосекунды, поэтому 1000000000000 * 1 — это одна секунда.
Транзакции | 303 Результат примера 8.2 выглядит следующим образом: *************************** 1. row *************************** trx_runtime: 20729.094 thread_id: 60 trx_event_id: 1137 isolation_level: REPEATABLE READ autocommit: NO db: test query: SELECT * FROM elem rows_examined: 10 rows_affected: 0 rows_sent: 10 exec_state: done exec_time: 0.038 Ниже приводится немного больше информации о полях (столбцах) примера 8.2: trx_runtime Как долго транзакция выполняется (активна) в секундах с точностью до миллисекунды. (Я забыл об этой транзакции, поэтому в примере она активна уже почти шесть часов.) thread_id Идентификатор (англ. ID) потока клиентского соединения, выполняющего транзакцию. Это используется ниже, в разделе «Активная транзакция: история». События схемы производительности используют ID потоков и ID событий для привязки данных к клиентским подключениям и событиям соответственно. ID потоков отличаются от ID процессов, обычных для других частей MySQL. trx_event_id Идентификатор события транзакции. Используется ниже, в разделе «Активная транзакция: история». isolation_level Уровень изоляции транзакций: READ REPEATABLE или READ COMMITTED. (Другие уровни изоляции, SERIALIZABLE и READ UNCOMMITTED, используются редко; если вы их видите, это может быть баг приложения.) Вспомните раздел «Блокировка строк» выше в этой главе: уровень изоляции транзакций влияет на блокировку строк и на то, использует ли SELECT снимок состояния. autocommit Если здесь стоит YES, то autocommit включен, и это транзакция с единичной инструкцией. Если NO, то транзакция была запущена с помощью BEGIN (или START TRANSACTION), и, скорее всего, это транзакция с несколькими инструкциями. db Текущая база данных для query (запроса). Текущая база данных задается через команду USE db. Запрос может обращаться к другим базам данных с именами таблиц, отвечающим требованиям для баз данных, например db.table.
304 | Глава 8 query Последний запрос, выполненный транзакцией или выполняемый в транзакции. Если exec_state = running, то запрос выполняется в данный момент. Если exec_state = done, то это последний запрос, выполненный транзакцией. В обоих случаях транзакция активна (не зафиксирована), но во втором случае она бездействует с точки зрения выполнения запросов. rows_examined Общее количество строк, проверенных query (запросом). Прошлые запросы, выполненные в транзакции, не учитываются. rows_affected Общее количество строк, измененных query (запросом). Прошлые запросы, выполненные в транзакции, не учитываются. rows_sent Общее количество строк, отправленных query (запросом), — результирующий набор. Прошлые запросы, выполненные в транзакции, не учитываются. exec_state При значении done (выполнено) транзакция бездействует в отношении выполнения запроса, а query — последний выполненный ею запрос. При значении running транзакция выполняет запрос (query) в данный момент. В обоих случаях транзакция активна (не зафиксирована). exec_time Время выполнения query в секундах (с точностью до миллисекунды). Таблицы схемы производительности events_transactions_current и events_statements_ current содержат больше полей, но для данного отчета выбираются только необходимые. Этот отчет — настоящая рабочая лошадка, потому что он может выявить все четыре распространенные проблемы (см. выше в соответствующем разделе): Большие транзакции Посмотрите на rows_affected (число измененных строк) и rows_sent, чтобы увидеть размер транзакции (в пересчете на строки). Поэкспериментируйте с добавлением условия типа trx.rows_affected > 1000. Длительные транзакции Установите значение 1 в конце условия trx.timer_wait > 1000000000000 * 1 для фильтрации более длительных запросов. Зависшие транзакции Если exec_state = done, и это значение долго не меняется, транзакция приостанавливается. Поскольку в этом отчете перечислены только последние запросы активных транзакций, запрос должен быстро меняться: статус exec_state = done должен быть скоротечным.
Транзакции | 305 Заброшенные транзакции Если exec_state = done держится в течение длительного времени, возможно, транзакция заброшена, поскольку о ней перестает сообщаться после фиксации. Выходные данные этого отчета быстро меняются, поскольку активные транзакции должны быть скоротечными. Если он сообщает о транзакции достаточно долго, чтобы вы могли просмотреть ее несколько раз, то это, вероятно, говорит об одной из распространенных проблем (см. выше раздел «Распространенные проблемы»). В этом случае используйте его thread_id и statement_event_id (как в разделе «Активная транзакция: история», см. ниже), чтобы сообщить о его истории — о прошлых запросах. Это поможет выяснить, в чем проблема. Информационная схема INNODB_TRX Обращаться к схеме производительности MySQL — это одна из лучших практик и будущее отчетов о производительности MySQL. Но информационная схема MySQL (https://oreil.ly/2AOhC) по-прежнему широко используется, и она может сообщать о длительных транзакциях, запрашивая информацию из таблицы information_schema. innodb_trx: SELECT trx_mysql_thread_id AS process_id, trx_isolation_level, TIMEDIFF(NOW(), trx_started) AS trx_runtime, trx_state, trx_rows_locked, trx_rows_modified, trx_query AS query FROM information_schema.innodb_trx WHERE trx_started < CURRENT_TIME - INTERVAL 1 SECOND\G *************************** 1. row *************************** process_id: 13 trx_isolation_level: REPEATABLE READ trx_runtime: 06:43:33 trx_state: RUNNING trx_rows_locked: 4 trx_rows_modified: 1 query: NULL В этом примере запрос имеет значение NULL, поскольку транзакция не выполняет никакого запроса. Иначе это поле содержало бы запрос. Я советую использовать схему производительности, потому что она содержит значительно больше подробностей — по сути, все, что нужно знать о том, что происходит внутри MySQL. Во всех примерах в этой книге по возможности используется схема производительности; в редких случаях некоторая информация доступна только в информационной схеме. Чтобы узнать больше о таблице information_schema.innodb_trx, прочитайте «The INFORMATION_SCHEMA INNODB_TRX Table» (https://oreil.ly/jqVNx) в руководстве по MySQL.
306 | Глава 8 Активные транзакции: итоги Инструкция SQL в примере 8.3 сообщает сводку запросов, выполненных для всех транзакций, активных более 1 секунды. Этот отчет отвечает на вопрос: какие транзакции являются длительными и сколько работы они проделали? Пример 8.3. Отчет о сводке транзакций SELECT trx.thread_id AS thread_id, MAX(trx.event_id) AS trx_event_id, MAX(ROUND(trx.timer_wait/1000000000000,3)) AS trx_runtime, SUM(ROUND(stm.timer_wait/1000000000000,3)) AS exec_time, SUM(stm.rows_examined) AS rows_examined, SUM(stm.rows_affected) AS rows_affected, SUM(stm.rows_sent) AS rows_sent FROM performance_schema.events_transactions_current trx JOIN performance_schema.events_statements_history stm ON stm.thread_id = trx.thread_id AND stm.nesting_event_id = trx.event_id WHERE stm.event_name LIKE 'statement/sql/%' AND trx.state = 'ACTIVE' AND trx.timer_wait > 1000000000000 * 1 GROUP BY trx.thread_id\G Чтобы увеличить время, измените 1 перед \G. Поля те же, что и в разделе «Активные транзакции: последние» (см. выше), но в этом отчете агрегируются прошлые запросы для каждой транзакции. Зависшая транзакция (в настоящее время не выполняющая запрос), возможно, проделала большую работу в прошлом, о чем расскажет этот отчет. Когда запрос завершает выполнение, он регистрируется в таблице performance_ schema.events_statements_history, но также остается в таблице performance_ schema.events_statements_current. Отчет включает только завершенные запросы и не должен присоединяться к последней таблице, если только активные запросы не будут отфильтрованы. Этот отчет лучше подходит для поиска крупных транзакций (см. выше раздел «Большие транзакции (размер транзакции)»), поскольку он включает прошлые запросы. Активная транзакция: история Инструкция SQL в примере 8.4 сообщает историю запросов, выполненных для одной транзакции. Этот отчет отвечает на вопрос: сколько работы выполнила каждая транзакция запроса? Вам необходимо заменить нули значениями thread_id и trx_event_id из выходных данных примера 8.2.
Транзакции | 307 Пример 8.4. Отчет об истории транзакций SELECT stm.rows_examined AS rows_examined, stm.rows_affected AS rows_affected, stm.rows_sent AS rows_sent, ROUND(stm.timer_wait/1000000000000,3) AS exec_time, stm.sql_text AS query FROM performance_schema.events_statements_history stm WHERE stm.thread_id = 0 AND stm.nesting_event_id = 0 ORDER BY stm.event_id; Замените нули значениями из выходных данных примера 8.2:  замените ноль в stm.thread_id = 0 на thread_id;  замените ноль в stm.nesting_event_id = 0 на trx_event_id. Результат примера 8.4 выглядит следующим образом: rows_examined 10 2 0 rows_affected 0 1 0 rows_sent 10 0 0 exec_time 0.000 0.003 0.002 query SELECT * FROM elem UPDATE elem SET ... COMMIT Помимо BEGIN, который запускал транзакции, эта транзакция выполнила два запроса, а затем зафиксировала (COMMIT). SELECT был первым запросом, а UPDATE — вторым. Не самый впечатляющий пример, но он демонстрирует историю выполнения запроса транзакции, а также основные показатели (метрики) запроса. История бесценна при отладке проблемных транзакций, потому что вы можете видеть, какие запросы являются медленными (exec_time) или большими (с точки зрения строк), а также точку, в которой приложение зависает (когда вы знаете, что транзакция выполнит больше запросов). Зафиксированные транзакции: итоги Предыдущие три отчета относятся к активным транзакциям, но зафиксированные транзакции тоже отслеживаются. Инструкция SQL в примере 8.5 сообщает основные показатели для зафиксированных (завершенных) транзакций. Это похоже на медленный лог запросов для транзакций. Пример 8.5. Отчет об основных показателях для зафиксированных транзакций SELECT ROUND(MAX(trx.timer_wait)/1000000000,3) AS trx_time,
308 | Глава 8 ROUND(SUM(stm.timer_end-stm.timer_start)/1000000000,3) AS query_time, ROUND((MAX(trx.timer_wait)-SUM(stm.timer_end-stm.timer_start))/1000000000, 3) AS idle_time, COUNT(stm.event_id)-1 AS query_count, SUM(stm.rows_examined) AS rows_examined, SUM(stm.rows_affected) AS rows_affected, SUM(stm.rows_sent) AS rows_sent FROM performance_schema.events_transactions_history trx JOIN performance_schema.events_statements_history stm ON stm.nesting_event_id = trx.event_id WHERE trx.state = 'COMMITTED' AND trx.nesting_event_id IS NOT NULL GROUP BY trx.thread_id, trx.event_id; Поля примера 8.5: trx_time Общее время транзакции в миллисекундах с точностью до микросекунды. query_time Общее время выполнения запроса в миллисекундах с точностью до микросекунды. idle_time Время транзакции минус время запроса в миллисекундах с точностью до микросекунды. Время простоя указывает, насколько приложение зависало во время выполнения запросов в транзакции. query_count Количество запросов, выполненных в транзакции. rows_* Общее количество строк, проверенных (examined), затронутых (affected) либо отправленных (sent) соответственно всеми запросами, выполненными в транзакции. Результат примера 8.5 выглядит следующим образом: trx_time 5647.892 0.585 qry_time 1.922 0.403 idle_time 5645.970 0.182 qry_cnt 2 2 rows_exam 10 10 rows_affe 0 0 rows_sent 10 10 В этом примере я дважды выполнил одну и ту же транзакцию: сначала вручную, затем через скопировать-вставить. Выполнение вручную заняло 5,6 секунды (5647,892), что в основном относилось к простою (бездействию) из-за набора текста. Но транзакция, выполняемая программно, должна в основном занимать время
Транзакции | 309 выполнения запроса, как показано во второй строке: 403 микросекунды времени выполнения и только 182 микросекунды времени бездействия. Итоги В этой главе были рассмотрены транзакции MySQL с точки зрения предотвращения распространенных проблем. Основные моменты:  Уровни изоляции транзакций влияют на блокировку строк (блокировку данных).  Основные блокировки данных в InnoDB: • блокировка индексной записи (англ. record lock) — блокирует единичную индексную запись; • блокировка следующего ключа (англ. next-key lock) — блокирует единичную индексную запись плюс интервал в индексной записи перед ней; • блокировка интервала (англ. gap lock) — блокирует диапазон [интервал] между двумя индексными записями; • блокировка намерения вставки (англ. insert intention lock) — позволяет вставлять (INSERT) строки в интервал; больше похоже на условие ожидания, чем на блокировку.  Уровень изоляции транзакций по умолчанию, REPEATABLE READ, использует блоки- ровку интервалов для изоляции диапазона строк, к которым обращается запрос.  Уровень изоляции транзакции READ COMMITTED отключает блокировку интервалов.  InnoDB использует согласованные снимки состояния в транзакциях REPEATABLE READ, чтобы операции чтения (SELECT) возвращали одни и те же строки, несмотря на изменения, сделанные в этих строках другими транзакциями.  Согласованные снимки состояния требуют, чтобы InnoDB сохраняла изменения строк в логах отмены для восстановления старых версий строк.  Длина списка истории (англ. history list length, HLL) определяет количество ста- рых версий строк, которые не были удалены или сброшены на диск.  HLL — это предвестник беды: всегда следите и предупреждайте о HLL, превы- шающей 100 000.  Блокировки данных и логи отмены освобождаются при завершении транзак- ции — с помощью COMMIT или ROLLBACK.  Четыре распространенные проблемы транзакций: • большие транзакции (изменение слишком большого количества строк); • длительные транзакции (большое время отклика от BEGIN до COMMIT), • зависшие транзакции (избыточные ожидания между запросами); • заброшенные транзакции (клиентское соединение исчезло во время активной транзакции).
310 | Глава 8  Схема производительности MySQL позволяет создавать подробные отчеты о транзакциях.  Производительность транзакций так же важна, как и производительность за- просов. В следующей главе перечислены распространенные трудности в MySQL и способы их устранения. Практика: оповещение о длине списка истории Цель этой практики — установить оповещение о длине списка истории (HLL), превышающей 100 000. (Вспомните раздел «Длина списка истории», см. выше в этой главе). Это зависит от ваших систем мониторинга (сбора метрик) и оповещения, но в принципе ничем не отличается от оповещения по другим метрикам. Таким образом, необходимая работа состоит из двух этапов: 1. Собрать и сообщить значение HLL. 2. Создать оповещение о HLL, превышающем 100 000. Все средства мониторинга MySQL должны иметь возможность собирать и сообщать значения HLL. Если ваш текущий мониторинг не позволяет этого, серьезно подумайте о варианте получше, потому что HLL — одна из базовых метрик. Ознакомьтесь с документацией к вашему средству мониторинга, чтобы узнать, как заставить его собирать и сообщать значения HLL. HLL может быстро измениться, но есть запас времени до момента, когда из-за высокого уровня HLL MySQL окажется под угрозой. Поэтому вы можете сообщать о значениях HLL медленно: раз в минуту. Как только ваш монитор начнет собирать и сообщать значения HLL, установите оповещение о HLL, превышающей 100 000, на 20 минут. Но вспомните раздел «Охота на миражи (пороговые значения)» в главе 6: возможно, вам потребуется настроить 20-минутный порог, однако обратите внимание, что HLL более 100 000 в течение более 20 минут — это совершенно ненормально. В случае, если вам нужно запросить значение HLL вручную: SELECT name, count FROM information_schema.innodb_metrics WHERE name = 'trx_rseg_history_len'; Исторически сложилось так, что значения HLL можно было извлечь из выходных данных SHOW ENGINE INNODB STATUS: найдите «History list lengh» (длину списка истории) в заголовке раздела «TRANSACTIONS» (транзакции) в MySQL. Я надеюсь, что вы никогда не получите оповещений о HLL, но наличие оповещения относится к лучшим практикам, и это спасло многие приложения от выхода из строя. Оповещение о HLL — это друг.
Транзакции | 311 Практика: изучение блокировки строк Цель этой практики — изучить блокировку строк для реальных запросов из вашего приложения и, если возможно, понять, почему запрос получает каждую блокировку. «Если возможно» — это важная оговорка, учитывая, что блокировка строк InnoDB бывает непостижимой. Возьмите разрабатываемый или промежуточный экземпляр MySQL; не трогайте производственный. Кроме того, вам нужна MySQL 8.0.16 или новее, потому что у нее самые лучшие отчеты о блокировке данных с использованием таблицы data_locks схемы производительности, как показано в разделе «Блокировка строк» выше в данной главе. Если вам доступна только MySQL 5.7, то нужно будет изучать блокировку данных с помощью SHOW ENGINE INNODB STATUS: обратитесь к блокировкам данных MySQL («MySQL Data Locks», https://oreil.ly/f9uqy) за иллюстрированным руководством по отображению выходных данных (англ. output) блокировки данных из MySQL 5.7 в MySQL 8.0. Используйте реальные определения таблиц и как можно больше реальных данных (строк). Если возможно, сбросьте данные из рабочей среды и загрузите их в свои экземпляры разработки или с промежуточной средой MySQL. Если есть конкретные запросы или транзакции, которые вас интересуют, начните с изучения их блокировок данных. В противном случае начните с медленных запросов — вспомните раздел «Профиль запроса» (см. гл. 1). Поскольку блокировки снимаются при завершении транзакции, необходимо использовать эксплицитные транзакции, как указано в разделе «Блокировка строк» (см. выше) — между BEGIN и SELECT выполняется один или несколько запросов: BEGIN; --- Execute one or several queries -SELECT index_name, lock_type, lock_mode, lock_status, lock_data FROM performance_schema.data_locks WHERE object_name = 'elem'; Замените elem именем вашей таблицы и не забудьте COMMIT или ROLLBACK, чтобы снять блокировку. Чтобы изменить уровень изоляции транзакции для следующей (и только следующей) транзакции, выполните SET TRANSACTION ISOLATION LEVEL READ COMMITTED перед BEGIN. Это практика экспертного уровня, поэтому любое усилия и понимание — достижение. Примите мои поздравления.
312 | Глава 8
ГЛАВА 9 Другие проблемы Эта глава представляет собой краткое, но важное перечисление распространённых трудностей в MySQL и способов их устранения. Эти проблемы не вписываются в тематику остальных глав, поскольку в большинстве своем не связаны с производительностью напрямую. Но не стоит их недооценивать: первые две, например, могут привести к обрушению базы данных. Что еще более важно, эти трудности не ждут, когда сойдутся звезды, чтобы судьба бросила вам вызов, — они повсеместны. Отнеситесь к ним серьезно и будьте готовы встретиться с ними лицом к лицу. Сплит-брейн — величайший риск Для сплит-брейна (англ. split-brain, букв. разделение мозга) надо, чтобы в одной и той же топологии репликации одновременно выполнялись два условия:  доступно для записи более одного экземпляра MySQL (read_only=0);  операции записи происходят более чем в одном экземпляре MySQL. Ни того ни другого быть никогда не должно, особенно одновременно. Но жизнь полна сюрпризов, и нельзя вечно избегать каких-то ошибок или происшествий. Когда такое случается, это называется сплит-брейном: вместо того, чтобы все экземпляры MySQL имели одни и те же данные, они, условно говоря, разделяются, потому что данные больше не идентичны (не согласованы) в каждом экземпляре. Несогласованные данные — это не только в корне неправильно, но и может нарушить репликацию или, что еще хуже, вызвать эффект домино, который приведет к несогласованности еще большего количества данных, что вызовет новую проблему: дрейф данных. Понятие «сплит-брейн» не актуально для топологий репликации MySQL, которые специально разработаны так, чтобы иметь несколько экземпляров, доступных для записи. При возникновении сплит-брейна вы должны немедленно обнаружить и остановить его. Почему? Потому что даже одна запись может повлиять на любое количество строк. Всего несколько секунд сплит-брейна могут привести к лавине несогласованных данных, что приведет к неделям разбирательств и согласования данных. Чтобы остановить сплит-брейн, отключите запись во всех экземплярах: SET GLOBAL Не оставляйте ни один экземпляр доступным для записи; это усугубит read_only=1.
314 | Глава 9 проблему. Если вы не можете отключить запись, то вырубите MySQL или сервер — серьезно. Целостность данных важнее, чем доступность данных. Целостность данных важнее, чем их доступность. В идеале нужно перевести всю базу данных в автономный режим до тех пор, пока не будут найдены и согласованы все несогласованные данные. Но на практике — если длительный сбой в работе базы данных катастрофичен для дела и вы абсолютно уверены, что чтение потенциально неверных данных не нанесет дальнейшего ущерба, можно запустить MySQL в режиме только для чтения (read_only=1), пока вы исправляете данные, используя режим super_read_only (https://oreil.ly/JrqIs). Есть только два способа найти несогласованные строки: запустить pt-table-sync (https://oreil.ly/Dr10P) или проверить вручную. Вручную — значит сделать все, что вы можете, для сравнения и проверки строк, исходя из вашего понимания приложения, данных и того, какие изменения, вероятно, произошли во время сплитбрейна. pt-table-sync— это инструмент с открытым исходным кодом, который может находить, выводить на экран и синхронизировать различия в данных между двумя экземплярами MySQL, но используйте его с осторожностью, поскольку любой инструмент, который изменяет данные, по сути своей опасен. pt-table-sync — опасный инструмент, если вы не обращаетесь с ним осторожно. Не используйте его опцию --execute: используйте только --print и внимательно прочитайте руководство. Согласование строк — это сложно, и вы должны работать с экспертом по MySQL, чтобы убедиться, что все сделано правильно. Если вам повезет, вы определите, что один экземпляр MySQL является авторитетным (все строки содержат правильные данные), и вы можете восстанавливать, а не согласовывать: восстановить все реплики из авторитетного экземпляра. Если вам не повезет, то поработайте с экспертом по MySQL, чтобы определить свои возможности. Дрейф данных реален, но невидим Понятие «дрейф данных» соотносится с несогласованными данными: одна или несколько строк имеют разные значения в разных экземплярах MySQL в одной и той же топологии репликации. («Дрейф» — образное выражение для значений, отдаляющихся друг от друга, поскольку изменения в несогласованных данных приводят к дальнейшим несоответствиям). В то время как несогласованные данные из сценария сплит-брейна ожидаемы, несогласованные данные из-за дрейфа данных неожиданны: вы не знаете или не имеете никаких оснований подозревать, что эти данные существуют. Хотя дрейф данных невидим в том смысле, что он на первый взгляд не вызывает проблем, тем не менее это реальная проблема, поскольку приложение может возвращать неправильные значения.
Другие проблемы | 315 К счастью, дрейф данных легко обнаружить: запустите pt-table-checksum (https:// oreil.ly/mogUa). Этот инструмент безопасен: он только считывает и сравнивает данные. К сожалению, дрейф данных согласовать не легче, чем несогласованные данные из-за сплит-брейна. Но это, вероятно, не вызовет неудобств, поскольку дрейф данных имеет тенденцию быть ограниченным и изолированным по объему, а не лавиной несогласованных данных, потому что он не вызван серьезным сбоемтипа сплит-брейна. Увлекательный аспект дрейфа данных заключается в том, что, насколько мне известно, никто никогда не находил и не доказывал первопричину этого явления в жизни (в реальной производственной базе данных). Теоретически он вызывается недетерминированными запросами и покомандной репликацией или операциями записи в реплики. В лаборатории эти два фактора, несомненно, вызвали бы дрейф данных, но в жизни они, похоже, никогда не являются для него причиной. Вместо этого инженеры и администраторы баз данных уверены, что не было сделано ничего, что могло бы вызвать или допустить утечку данных. И все же она существует. Проверяйте дрейф данных каждые несколько месяцев (или, по крайней мере, раз в год), запуская pt-table-checksum (https://oreil.ly/mogUa). Если вы однажды обнаружите дрейф данных, не беспокойтесь об этом: сверьте строки и проверьте еще раз через месяц. Если данные продолжают дрейфовать (что очень маловероятно), то у вас есть экзотическая проблема, заслуживающая детального изучения, чтобы найти и устранить первопричину. Не доверяйте ORM Объектно-реляционное отображение (ORM, от англ. object-relational mapping) — это подход, позволяющий программисту абстрагировать доступ к данным, выразив его в терминах программирования и программных объектах. Нельзя сказать, что ORM по своей сути — что-то плохое или неэффективное, но запросы, генерируемые библиотекой ORM, нужно проверять, ведь производительность не является ее целью. Например, поскольку ORM обрабатывает строки как объекты, библиотека ORM может выбирать все столбцы, что противоречит тому, что вы видели в контрольном списке эффективного доступа к данным (см. табл. 3.2). Другой пример: некоторые библиотеки ORM выполняют сторонние запросы (например, SHOW WARNINGS) до или после фактического запроса приложения. При стремлении к максимальной производительности каждый запрос на счету; сторонние запросы — недопустимая трата времени. Есть высокопроизводительные приложения, использующие эту технологию, но инженеры стараются не доверять ей: они проверяют запросы, сгенерированные ORM, в профиле запроса и отчете о запросе (см. гл. 1, раздел «Профиль запроса» и «Отчет о запросе» соответственно). Если запрос, сгенерированный библиотекой ORM, слишком неэффективен, ознакомьтесь с документацией по ORM, чтобы узнать, как настроить ORM для создания более эффективного запроса.
316 | Глава 9 Схемы постоянно меняются Вы, вероятно, уже знакомы с этим, но на случай, если вы новичок в работе с любой реляционной базой данных: схемы постоянно меняются. (Если конкретнее, то меняются определения таблиц (англ. table definition), а таблицы составляют схему.) Задача в том, чтобы выполнить оперативное изменение схемы (англ. online schema change, OSC): изменить схему во время ее использования, не затрагивая приложение. Как упоминалось в предыдущих главах, есть три отличных решения для MySQL:  pt-online-schema-change (https://oreil.ly/brtmM);  gh-ost (https://oreil.ly/ZKQAd);  ALTER TABLE (https://oreil.ly/GRQuf). Эти решения работают очень по-разному, но каждое из них может мгновенно изменять определение таблицы, не влияя на приложение. Прочтите документацию по ним, чтобы решить, какое лучше всего вам подходит. Тут есть еще один аспект: интеграция изменений схемы в процесс разработки программного обеспечения. Вы можете запустить OSC вручную, но команды (т. е. коллективы) разработчиков так не делают — как и другие изменения кода, изменения схемы должны быть частью процесса разработки, чтобы они были рассмотрены, одобрены, протестированы на этапе обкатки и т. д. Поскольку специфика здесь зависит от конкретной команды, вашей команде придется создавать собственное решение. Но в настоящее время существует одно решение с открытым исходным кодом: Skeema (https://www.skeema.io). Чтобы узнать, как известный эксперт по MySQL Шломи Ноах решил эту проблему на GitHub, прочтите его пост «Automating MySQL Schema Migrations with GitHub Actions and More» («Автоматизация миграции схем MySQL с помощью действий GitHub и не только», https://oreil.ly/9cEJi). MySQL расширяет стандартный SQL Если вы используете только MySQL, то, наверное, можете пропустить эту трудную задачу. Но если вы используете (или собираетесь использовать) другую реляционную базу данных, то имейте в виду, что MySQL имеет много расширений для стандартного SQL, перечисленных в разделе «MySQL Extensions to Standard SQL» («Расширения MySQL для стандартного SQL», https://oreil.ly/gLN1l) в руководстве по MySQL. При этом MySQL не поддерживает некоторые стандартные функции SQL, такие как полные внешние соединения. Существуют и другие ограничения, перечисленные в разделе с удивительно точным названием «MySQL Restrictions and Limitations» («Ограничения и пределы MySQL», https://oreil.ly/x3xro), и вы найдете другие упоминания о странностях в мануалах по MySQL. Любая база данных с такой долгой и легендарной историей, как MySQL, обязательно будет столь же эклектичной. Что уникально для MySQL — про MySQL есть документ, который так хорошо знаком экспертам и пользуется таким доверием, что
Другие проблемы | 317 про него нередко говорят: руководство по MySQL (https://oreil.ly/IXARN), всеобъемлющее и авторитетное. Документация по программному обеспечению может быть скудной, устаревшей или вовсе отсутствующей, но только не руководство по MySQL. Есть тайные крупицы информации о MySQL, которых там нет, но, если оставить их за скобками, эксперты MySQL в значительной степени полагаются на него. И вам тоже следует. «Шумные соседи» На физическом сервере «шумный сосед» (англ. noisy neighbour) — это программа, которая снижает производительность других программ, задействуя слишком много системных ресурсов. Например, если на сервере запущено 20 отдельных экземпляров MySQL, но один из них использует всю процессорную мощность и дисковый ввод-вывод, то это шумный сосед. Проблема распространенная, поскольку общий сервер (или многопользовательский) — на одном физическом сервере запускается несколько виртуальных сред — это норма. И наоборот — выделенный сервер, единоличная аренда, когда клиенту ни с кем не нужно делиться ресурсами сервера, — бывает редко, и это дорого, особенно в облаке. Разобраться с «шумным соседом» — задачка не из простых, потому что такое влияние на производительность — не ваша вина, но ваша проблема. Если ваша компания использует собственное оборудование, то проблема решаема: измерьте использование ресурсов каждой программы или виртуальной среды на общем сервере, где вы подозреваете наличие «шумного соседа». «Шумных соседей» легко заметить, потому что они шумные. Затем переместите «шумного соседа» (или вашу базу данных) на другой, более тихий сервер. Если это невозможно, то купите еще один экземпляр этой книги для соседа, чтобы он узнал, как оптимизировать производительность MySQL. В облаке увидеть или доказать существование «шумного соседа» не получится. В целях безопасности облачные провайдеры поддерживают строгое разделение клиентов (таких клиентов, как вы) на общих серверах. И они вряд ли признают подобную проблему — это означало бы, что они не балансируют нагрузку на сервер, которая должна быть включена в стоимость. Следовательно, при подозрениях на «шумного соседа» стандартная практика заключается в пересмотре облачной базы данных. Некоторые компании проводят предварительное тестирование облачного ресурса и сохраняют этот ресурс лишь при условии, что производительность соответствует базовому уровню; в противном случае ресурс уничтожается, подготавливается другой, и процесс повторяется до тех пор, пока ресурс не будет случайно предоставлен на тихом сервере. Приложения не завершаются корректно Netflix придумал chaos engineering (хаотичный инжиниринг): намеренное внедрение проблем и сбоев в систему, чтобы проверить ее отказоустойчивость и вынудить инженеров разработать решение на случай сбоя. Это смелый подход, потому что
318 | Глава 9 так действительно можно проверить качество приложения. Написать программу, которая работает правильно, когда все вокруг также работает правильно — это настолько просто и ожидаемо, что ничего не значит. А вот написать программу, которая хоть как-то работает, даже когда все вокруг выходит из строя — это вызов. Как инженеры, мы часто думаем, что готовы к сбоям в нашем программном обеспечении, но что мы об этом знаем, пока что-то не выйдет из строя по-настоящему? Кроме того, не все они двоичны — «работает или не работает». Коварнее всего не откровенные сбои, а, скорее, пограничные случаи и статистические выбросы: проблема такого рода, для объяснения которой требуется целая история, а не простое заявление о сбое типа «Отказ жесткого диска». Это же верно и для приложений в отношении MySQL. Однако хаотичный инжиниринг не является стандартной практикой в индустрии MySQL, потому что шутить с базой данных рискованно, и немногие инженеры настолько смелы. Но фортуна смелым благоволит, так что вот 12 сценариев хаоса в базе данных для проверки работоспособности вашего приложения: 1. MySQL находится в автономном режиме (offline). 2. MySQL очень медленно реагирует. 3. MySQL доступна только для чтения. 4. MySQL только что запущена (холодный буферный пул). 5. Реплики для чтения находятся в автономном режиме или очень медленны. 6. Проверка отказоустойчивости в той же области. 7. Проверка отказоустойчивости в другой области. 8. Выполнение резервного копирования базы данных. 9. Проблемы с DNS. 10. Сеть работает медленно (высокая задержка) или перенасыщена. 11. Один из жестких дисков в RAID-массиве поврежден. 12. На твердотельном накопителе осталось менее 5% свободного места. Некоторые из этих 12 сценариев могут быть неприменимы к вашей инфраструктуре, но большинство из них стандартны и дают интересные результаты в зависимости от приложения. Если вы никогда не программировали хаос, я призываю вас начать, ведь хаос не ждет, когда вы будете готовы. Высокопроизводительная MySQL — это сложно Если вы будете добросовестно применять все лучшие практики и методы, описанные в этой книге, я уверен — вы добьетесь замечательной производительности MySQL. Но не факт, что это будет быстро или легко. Высокопроизводительная MySQL требует опыта, потому что ресурсы — книги, блоги, видео, конференции
Другие проблемы | 319 и т. д., — учат вас теории, которая отличается от реальности. Следовательно, когда вы начнете внедрять то, что узнали из этой книги, в свое приложение, то можете столкнуться с двумя проблемами. Первая проблема заключается в том, что реальные запросы приложений бывают куда сложнее, чем содержательные, но маленькие примеры, разбросанные по этим страницам. Кроме того, непросто запомнить и применить столько знаний сразу: метрики запросов, индексы и индексирование, выходные данные EXPLAIN, оптимизация запросов, определения таблиц и т. д. Поначалу все это может вас ошеломить, но берите по одному запросу за раз и помните про разделы «Краеугольный камень» (см. гл. 1) и «Индексирование: думать, как MySQL» (см. гл. 2). Даже экспертам требуется время, чтобы увидеть и понять всю историю запроса. Вторая проблема в том, что реальная производительность приложения редко зависит от одного аспекта рабочей нагрузки. Исправление медленных запросов, несомненно, поможет, но этого может быть недостаточно. Чем больше производительности вам нужно от MySQL, тем больше вам нужно оптимизировать всю рабочую нагрузку: каждый запрос, все данные и каждый паттерн доступа. В конце концов вам надо будет применить знания из каждой главы этой книги (за исключением 10-й, если вы не используете MySQL в облаке). Начните с малого (главы 1–4), но обязательно изучите и примените все, что описано в этой книге, потому что это вам понадобится. Производительность MySQL — нечто большее, чем я вам рассказываю, но я уверяю вас: знания, изложенные мной, достаточны и эффективны. Даже так: нет никаких особых секретов, известных лишь специалистам, которым удалось добиться прекрасной производительности MySQL. Я вынес это из собственного опыта, а также из работы со многими лучшими мировыми экспертами по MySQL. Кроме того, программное обеспечение с открытым исходным кодом совсем не умеет хранить секреты. Практика: определение барьеров для сплит-брейна Цель этой практики — определить барьеры, которые предотвращают сплит-брейн. Она состоит из двух этапов: сперва — подробный разбор барьеров, чтобы каждый инженер понимал, что это такое, где они находятся (вероятно, в инструментах) и как работают, а затем подробный обзор инструментов, которые управляют экземплярами MySQL или влияют на них, особенно инструментов для отработки отказоустойчивости. Если вы не управляете MySQL, то запланируйте общение с инженерами, которые это делают, чтобы они подробно рассказали, как предотвращают сплит-брейн во время операций, особенно при отработке отказоустойчивости. Обычно это простой вопрос, т. к. предотвращение сплит-брейна — одна из основ управления MySQL. Если вы используете MySQL в облаке, нюансы могут отличаться. У облачных провайдеров имеются неразглашаемые методы решения этой проблемы, зависящие от
320 | Глава 9 особенностей настройки и управления MySQL. Например, сплит-брейн теоретически невозможен на стандартном экземпляре Amazon RDS для MySQL с несколькими зонами доступности (англ. available zone, AZ), потому что, хотя зон и несколько, тут нет одновременного запуска нескольких экземпляров. Запущен единственный экземпляр MySQL в одной зоне доступности. Если он выходит из строя, запускается другой экземпляр в другой зоне. Но если вы добавляете реплики для чтения, то у вас появляется несколько запущенных экземпляров MySQL в одной и той же топологии репликации, и Amazon не дает никаких гарантий насчет сплит-брейна в отношении этих реплик. Что касается облака, исходите из того, что ответственность за барьеры для сплитбрейна лежит на вас, но также вам хорошо бы знать, предотвращает ли сплит-брейн поставщик облачных услуг. Если вы управляете MySQL на собственном оборудовании, то я советую вам нанять эксперта по MySQL, который и поможет вам определить барьеры для сплитбрейна. (На что не требуется много времени, потому это должен быть краткосрочный и доступный по стоимости контракт.) Существует один основополагающий барьер, который вы просто обязаны внедрить: настройте MySQL (в файле my.cnf) так, чтобы она запускалась в режиме «только для чтения»: read_only=1. Всегда запускайте MySQL в данном режиме. Опираясь на это, другие барьеры подробно описывают, как переключить режим «только для чтения» так, чтобы он гарантированно был выключен (а MySQL была доступна для операций записи) только для одного экземпляра в текущий момент. Всегда запускайте MySQL в режиме «только для чтения» (read_only=1). Как только инженеры поймут суть барьеров, переходите ко второй части — тщательному изучению инструментов, которые управляют экземплярами MySQL или влияют на них, особенно средств отработки отказоустойчивости, — чтобы убедиться, что барьеры внедрены и работают должным образом. Конечно, весь код должен проходить модульное тестирование, но предотвращение сплит-брейна настолько важно, что оно также требует проверки кода вручную. В коде бывают проблемы, которые могут не проявляться при определении барьеров; например: состояние гонки, повторные попытки и обработка ошибок. Последнее — обработка ошибок — особенно важно: может ли (или должен) инструмент откатывать изменения при ошибке? Помните: целостность данных важнее, чем их доступность. При переключении MySQL в режим read-only инструменты должны проявлять осторожность: если операция имеет ненулевую вероятность вызвать сплит-брейн, не осуществляйте этого; оставьте MySQL в режиме «только для чтения», дождитесь ошибки и позвольте человеку разобраться. Итог: на 100% проясните для себя вопрос барьеров которые предотвращают сплитбрейн.
Другие проблемы | 321 Практика: проверка на дрейф данных Цель этой практики — проверить наличие дрейфа данных с помощью pt-tablechecksum (https://oreil.ly/mogUa). Вам повезло: этот инструмент был специально написан так, чтобы он был несложным и автоматическим. Просто скачайте и запустите его, и в большинстве случаев он автоматизирует все остальное. Если нет, то быстрое ознакомление с его документацией даст ответы на любые вопросы. Большинству инструментов MySQL требуется специальная настройка для работы с MySQL в облаке. pt-table-checksum делает лишь одно: проверяет наличие дрейфа данных и сообщает о нем. Он может работать часами или днями в зависимости от размера данных и загруженности доступа. По умолчанию он работает медленно, чтобы не мешать производственным процессам. Поэтому обязательно запускайте его в сеансе screen или tmux. Когда pt-table-checksum завершает проверку таблицы, он выводит для нее однострочный результат. Результат выглядит следующим образом: TS 10-21T08:36:55 10-21T08:37:00 10-21T08:37:10 ERRORS 0 0 0 DIFFS 0 0 2 ROWS 200 603 1600 DIFF_ROWS 0 0 3 CHUNKS 1 7 21 SKIPPED 0 0 0 TIME 0.005 0.035 1.003 TABLE db1.tbl1 db1.tbl2 db2.tbl3 Последняя строка выходных данных показывает таблицу с дрейфом данных, поскольку разница в столбцах имеет ненулевое значение. Если какая-либо таблица имеет смещение данных, выполните повторный запуск с параметром --replicatecheck-only, чтобы вывести на экран реплики и участки (англ. chunks), отличные от исходных. Участок здесь — это диапазон строк, ограниченный значениями верхней и нижней границ для индекса (обычно первичного ключа). pt-table-checksum проверяет строки такими участками, поскольку проверка строк по одной — слишком медленная и неэффективная). Вам следует разработать план для изоляции и согласования несогласованных строк. Если их очень мало, вы можете изолировать и согласовать их вручную. Если нет, то советую вам поработать с экспертом по MySQL, чтобы убедиться, все ли сделано правильно. Практика: хаос Цель этой практики — проверить работоспособность вашего приложения. Хаотичный инжиниринг — не для слабонервных, поэтому начните с вашей промежуточной базы данных. Эта практика ведет к перебоям в работе.
322 | Глава 9 Для пробы хаоса, описанного ниже, MySQL и приложение должны работать нормально с некоторой нагрузкой, и у вас должны быть хорошие показатели и наблюдаемость в обоих, чтобы записывать и анализировать, как они реагируют. Я предлагаю следующий вариант, но выбирайте сами, исходя из вашего уровня риска: Перезапуск MySQL Перезапуск MySQL проверяет, как приложение реагирует, когда MySQL находится в автономном режиме, и как реагирует, когда буферы MySQL холодны (в частности, буферный пул InnoDB). Холодные буферы требуют дискового ввода-вывода для считывания данных в память, что приводит к большему, чем обычно, времени отклика. Он также показывает вам три вещи: сколько времени требуется MySQL для завершения работы и запуска и сколько — буферам для прогрева. Включение режима read-only («только для чтения») Введите SET GLOBAL read_only=1 на исходном экземпляре, чтобы включить режим «только для чтения» и проверить, как приложение реагирует на возможность считывать данные при запрете на операции записи. Инженерам часто кажется, что приложение продолжит работать для операций чтения и изящно откажет в операции записи, но хаос полон сюрпризов. Это также эффективно имитирует неудачный переход на другой ресурс, чего никогда не должно случаться (потому что это означало бы сбой высокой доступности), но «никогда не должно случаться» — именно то, что относится к сфере действия хаоса. Остановка MySQL на 1 час Большинство приложений могут выдержать шторм в течение нескольких секунд или минут. Может быть, даже десятков минут. Но в какой-то момент очереди заполняются, время выхода из системы заканчивается, экспоненциальные задержки становятся очень долгими, ограничения скорости сбрасываются — и пользователи сдаются и переходят к конкуренту. MySQL не должна отключаться от сети более чем на несколько секунд. И это при условии правильного управления. Но опять же: хаос. Еще в 2004 году, когда я работал в дата-центре, за миг до моей смены (она была с двух часов дня до полуночи) инженер случайно нажал на кнопку аварийного отключения питания всего дата-центра. Спокойствие — единственный ответ хаосу, поэтому я выпил чашку кофе, прежде чем сесть и помочь перезагрузить наш датацентр.
ГЛАВА 10 MySQL в облаке MySQL в облаке — это, по сути, та же MySQL, которую вы знаете и любите (или знаете и терпите). В облаке лучшие практики и методы, разобранные в предыдущих девяти главах, не только верны, но в высшей степени верны, поскольку облачные провайдеры взимают плату за каждый байт и миллисекунду работы. Производительность в облаке — это деньги. Резюмируя предыдущие девять глав: 1. Производительность — это время отклика на запрос (см. гл. 1). 2. Индексы — ключ к производительности (см. гл. 2). 3. Чем меньше данных, тем лучше — как для хранения, так и для доступа (см. гл. 3). 4. Паттерны доступа позволяют добиться производительности либо препятствуют ей (см. гл. 4). 5. Шардирование необходимо для масштабирования операций записи и хранения (см. гл. 5). 6. Метрики сервера показывают, как рабочая нагрузка влияет на MySQL (см. гл. 6). 7. Запаздывание репликации приводит к потере данных, и его следует избегать (см. гл. 7). 8. Транзакции влияют на блокировку строк и ведение логов отмены (см. гл. 8). 9. Существуют и другие проблемы — даже в облаке (см. гл. 9). Если вы учтете и примените все эти вещи, MySQL будет выполнять рабочую нагрузку приложения с отличной производительностью независимо от местоположения: в облаке это, локально или где угодно. Я был бы только рад, если бы все было так просто — оптимизируйте рабочую нагрузку, и готово, — сколько времени мы бы сэкономили! Но MySQL в облаке диктует свои уникальные условия. Цель состоит в том, чтобы знать и смягчать эти условия, чтобы вы могли сосредоточиться на MySQL, а не на облаке. В конце концов в облаке нет ничего особенного: за пресловутым занавесом находятся физические серверы в дата-центрах, на которых и работают программы вроде MySQL. В этой главе рассказывается, что нужно знать при использовании MySQL в облаке. Здесь четыре основных раздела. Первый — предостережение по поводу совместимости: когда MySQL не является MySQL. Второй — это краткий обзор различных уровней администрирования MySQL в облаке. В третьем освещается вопрос задержки в сети и ее связь с вводом-выводом хранилища. Четвертая — о производительности и деньгах.
324 | Глава 10 Совместимость MySQL в облаке может быть не MySQL, или это может быть сильно модифицированная (и проприетарная) версия MySQL. Совместимость MySQL в облаке имеет две стороны: совместимость кода и совместимость функций. Под MySQL я подразумеваю MySQL от Oracle: официальный исходный код MySQL, при этом открытый. Я также имею в виду Percona Server от Percona и MariaDB Server от MariaDB Foundation: оба продукта широко используются, безопасны и стабильны и в целом считаются MySQL. Совместимость кода (англ. code compatibility) — это о том, является ли какой-то вариант MySQL тем же открытым исходным кодом, опубликованным Oracle, Percona или MariaDB. Если вариант несовместим с оригинальным кодом, а немного (или значительно) отличается, то в документации и описаниях таких продуктов обычно используются следующие девять выражений, чтобы указать на это:  Основан на... (англ. built on);  Эмулирует... (англ. emulates);  Совместим с... (англ. compatible);  Совместим с клиентами (англ. client compatible);  Совместим с протоколами (англ. protocol compatible);  Совместим по части проводной связи (англ. wire compatible);  Заменяет... (англ. replacement);  Легко заменяет... (англ. drop-in replacement);  Работает с существующими вариантами (англ. Works with existing). Совместимость кода важна, потому что MySQL сложна и тонка, и мы доверяем ей хранение бесценных данных. В этой книге я стремлюсь освещать все так, чтобы сузить рамки сложности MySQL, но такие разделы, как «Сброс страниц» (см. гл. 6) и «Блокировка строк» (см. гл. 8), намекают, насколько глубока кроличья нора. Когда какая-то компания изменяет исходный код MySQL, риски четырехкратны: потеря данных, снижение производительности, баги и несовместимости. Чем больше изменений, тем выше риски. Я видел последние три пункта в облаке; к счастью, я не сталкивался с тем, чтобы облачный провайдер терял данные. Если у вас есть какие-либо сомнения насчет совместимости кода MySQL в облаке, спросите поставщика облачных услуг: «Это та же MySQL с открытым исходным кодом от Oracle?» Говорить лишь об отрицательных сторонах (рисках) было бы однобоко: облачные провайдеры модифицируют MySQL ради дополнительных выгод: чтобы повысить производительность, исправить баги и добавить функции, которые нужны клиентам. Некоторые изменения ценны и стоят риска. Но если вы используете в облаке MySQL, которая несовместима с исходным кодом, вам надо понимать степень мо-
MySQL в облаке | 325 дификации. Это основы дью-дилидженс («должной добросовестности») для профессиональных инженеров, использующих MySQL в облаке. При достаточном количестве наблюдателей баги выплывают на поверхность. (Эрик С. Рэймонд) Совместимость функций зависит от того, включает ли MySQL функции, недоступные за пределами сервиса облачного провайдера или дистрибутива MySQL. Например, у Oracle есть два таких дистрибутива, как MySQL Community Server и MySQL Enterprise Edition. Первый — open-source; у второго есть проприетарные функции. Oracle Cloud Infrastructure (OCI) (https://www.oracle.com/cloud) использует последний, что хорошо: больше отдачи за облачные деньги. Но это также означает, что если вы полагаетесь на функции, специфичные для MySQL Enterprise Edition, то не можете напрямую перейти к другому облачному провайдеру или дистрибутиву MySQL. То же самое верно для Percona Server и MariaDB Server: эти дистрибутивы MySQL обладают уникальными функциями, что хорошо, но это усложняет переход к другому облачному провайдеру или дистрибутиву MySQL. Совместимость функций важна по той же причине, по которой важно программное обеспечение с открытым исходным кодом: свобода изменений. Программное обеспечение, включая MySQL, должно расширять возможности инженеров и пользователей, а не привязывать нас к конкретным облачным провайдерам или поставщикам. Это рассуждение носит скорее философский, чем технический характер, поэтому я снова постараюсь избежать однобокости: некоторые функции ценны — их стоит не менять, а сохранить. Но если вы решите использовать функцию, которая недоступна за пределами сервиса облачного провайдера или дистрибутива MySQL, вам нужно задокументировать причину, чтобы будущие инженеры могли понять, что поставлено на карту (и что необходимо заменить), если они выберут другого облачного провайдера или дистрибутив MySQL. Это тоже относится к основам дью-ди́лидженс для профессионалов, использующих MySQL в облаке. Управление (администрирование базы данных) Мы успешно уклонились от администрирования MySQL (работа DBA) с самых первых страниц этой книги, и сейчас не изменим себе, но MySQL в облаке поднимает вопрос, который вам нужно знать (или понимать, куда с ним обратиться): кто управляет MySQL? Можно подумать, что облачный провайдер, но все не так просто, потому что управление MySQL влечет за собой множество операций. Приготовьтесь: я собираюсь подвести вас в этой книге опасно близко к работе администратора базы данных, чтобы это объяснить. В табл. 10.1 приведен неполный список операций администратора базы данных и того, кто ими управляет: вы или облако. Позвольте мне кратко рассмотреть 15 параметров в табл. 10.1, потому что понимание полной картины, даже на высоком уровне, поможет вам избежать пробелов
326 | Глава 10 Таблица 10.1. Операции администратора базы данных Параметр Вы Облако Обеспечение ✓ Настройка ✓ Пользователи MySQL ✓ Метрики сервера ✓ Метрики запросов ✓ Оперативное изменение схемы (англ. online schema change, OSC) ✓ ✓ Восстановление после сбоя Аварийное восстановление (англ. disaster recovery, DR) ✓ Высокая доступность (англ. high availability, HA) ✓ Присутствует некоторое управление Обновление ✓ Резервное копирование и восстановление ✓ Захват изменений данных (англ. сhange data capture, CDC) ✓ Безопасность ✓ Помощь ✓ Стоимость ✓ в управлении MySQL, которые станут проблемой, если не знать, кто за них отвечает:  обеспечение MySQL — это, конечно, то, что должен предоставить облачный провайдер: операция самого низкого уровня запуска MySQL на компьютере;  облачные провайдеры используют неплохую конфигурацию MySQL, но дважды ее проверьте, потому что никакая конфигурация по умолчанию не может удовлетворить каждого клиента;  за исключением пользователя root, необходимого для предоставления вам пер- вичного контроля над сервером MySQL, облачные провайдеры не управляют пользователями MySQL;  вы также несете ответственность за сбор и отчет метрик сервера и запросов. Конечно, некоторые облачные провайдеры выдают базовые серверные метрики, но это даже отдаленно не похоже на полный спектр показателей, подробно описанных в главе 6;  OSC — запуск инструкций ALTER без ущерба для рабочей нагрузки, полностью ваша ответственность, и это, как правило, немного сложнее в облаке по различным техническим причинам, выходящим за рамки этой книги;
MySQL в облаке | 327  облачные провайдеры обеспечивают отказоустойчивость: когда оборудование или MySQL выходят из строя, облачный провайдер выполняет переход на другой ресурс для восстановления доступности;  но облачные провайдеры не занимаются аварийным восстановлением: когда весь регион выходит из строя и доступность должна быть восстановлена путем запуска MySQL из другого географического местоположения;  учитывая предыдущие два параметра, высокая доступность имеет смешанное управление. Вопрос высокой доступности MySQL в облаке слишком объемный, чтобы рассматривать его здесь; давайте просто скажем, что облако обеспечивает некоторую степень высокой доступности;  облачные провайдеры обновляют MySQL, что действительно приятно, потому что эта операция утомительна при масштабировании;  облачные провайдеры создают резервные копии MySQL, обеспечивают долго- срочное хранение резервных копий и предоставляют методы восстановления резервных копий — все это невероятно важно;  вы несете ответственность за захват изменений данных (CDC), что обычно тре- бует использования еще какого-то инструмента или службы, действующей как реплика для сброса (или потоковой передачи) бинарных логов из MySQL в другое хранилище данных (часто хранилище больших данных или озеро данных);  безопасность MySQL в облаке — также ваша ответственность: облако по своей сути не является безопасным;  облачные провайдеры помогают с запуском MySQL в целом, но не ожидайте большой (или какой-либо) помощи по части производительности MySQL, если только ваша компания не платит за такой уровень поддержки;  и наконец, вы должны управлять затратами: облако печально известно тем, что обходится дороже, чем предполагают инженеры. Три основных облачных провайдера — Amazon, Google и Microsoft имеют на 99,95 или 99,99% доступное SLA (англ. Service Level Agreement, соглашение об уровне сервиса) для MySQL (как управляемый сервис), но читайте мелкий шрифт — полные юридические подробности. Например, периоды техобслуживания обычно не учитываются в SLA. Или, например, соглашение об уровне обслуживания может быть аннулировано, если вы неправильно настроили MySQL. Всегда есть тонкости и оговорки в отношении высокой доступности облачных провайдеров и SLA. Табл. 10.1 носит описательный, а не рекомендательный характер, поскольку разные облачные провайдеры и сторонние компании предоставляют разные уровни управления MySQL в облаке. Например, некоторые компании полностью управляют MySQL в облаке (или локально). Как инженеру, использующему MySQL, а не управляющему ей, вам нужно лишь убедиться, что все параметры под контролем, все флажки установлены, чтобы ни один из них не мешал вашей работе. Как только убедитесь, пожалуйста, забудьте все, что прочитали в этом разделе, иначе станете администратором базы данных MySQL и не успеете оглянуться, как пройдет двад-
328 | Глава 10 цать лет и окажется, что следующий инженер, который присоединится к вашей команде, был новорожденным младенцем, когда — чу! — много лет назад вы столкнулись с необъяснимым регрессом производительности многодиапазонного чтения после безобидного точечного обновления релиза. Сеть и хранилище... и... задержка При запуске MySQL локально (в пространстве дата-центра, арендуемом вашей компанией) локальная сеть хлопот приносить не должна, если была спроектирована и подключена компетентными сетевыми инженерами. Локальные сети работают очень быстро и стабильно с субмиллисекундной задержкой. Локальная сеть должна быть более скучной, чем база данных (вспомните раздел «Нормальная и стабильная: лучшая база данных — это скучная база данных» в главе 6). Но облако глобально, а глобальные сети имеют более высокую задержку и более низкую стабильность (там колебания задержки и пропускной способности больше). Например, сетевое время приема-передачи (англ. round-trip time, RTT) между СанФранциско и Нью-Йорком составляет примерно 60 миллисекунд, плюс-минус 10. Если вы запускаете MySQL в Сан-Франциско (или где-либо на западном побережье США), а приложение находится на сервере в Нью-Йорке (или где-либо на восточном побережье США), минимальное время ответа на запрос составит примерно 60 миллисекунд. Это в 60 раз медленнее, чем в локальной сети1. Вы заметите эту медленность, но она не отобразится в метриках отклика на запрос, поскольку причина задержки лежит за пределами MySQL. Например, профиль запроса (см. гл. 1, раздел «Профиль запроса») показывает, что выполнение запроса занимает 800 микросекунд, но мониторинг производительности вашего приложения (APM) показывает 60,8 миллисекунды: 800 мкс для MySQL и 60 мс для сетевой задержки как в песне про Америку — «от моря до сияющего моря». Скорость сети на больших расстояниях физически ограничена скоростью света, и задержка усугубляется промежуточной маршрутизацией. Следовательно, эти ограничения нельзя обойти; можно только приспособиться. Например, обратитесь к разделу «Очередь для операций записи» (см. гл. 4): локальная очередь для операций записи, удаленные операции записи — и вообще удаленное выполнение любого процесса, который вызывает высокую задержку в сети. Вернемся к локальным сетям: хорошо, что они быстры и стабильны, потому что облачные провайдеры обычно держат данные MySQL в сетевых хранилищах (англ. network-attached storage, NAS): жестких дисках, подключенных к серверу через локальную сеть. В то время как локально подключенное хранилище (англ. locallyattached storage), оно же локальное хранилище (англ. local storage) — это жесткие диски, подключенные непосредственно к серверу. Облачные провайдеры используют сетевые хранилища по разным причинам, выходящим за рамки этой книги. 1 Технически все сети одинаково быстры: скорость света. Дело в физическом расстоянии и промежуточной маршрутизации на длинных дистанциях.
MySQL в облаке | 329 Важно знать, что сетевое хранилище работает куда медленнее и менее стабильно, чем локальное. Все три основных облачных провайдера — Amazon, Google и Microsoft — заявляют про «задержку в пределах десяти миллисекунд» для сетевого хранилища (с использованием SSD), за одним исключением: у Amazon io2 Block Express субмиллисекундная задержка2. В общем, при использовании MySQL в облаке ожидайте, что у хранилища будет сетевая задержка в пределах десяти миллисекунд, что эквивалентно по скорости вращающемуся диску. Сетевое хранилище работает на порядок медленнее локального (с SSD; не используйте вращающиеся диски), но стоит ли вам заниматься этой проблемой? Если вы переносите MySQL в облако с bare metal оборудования (т. е. с оборудования без операционной системы, где у MySQL прямой доступ к железу) с высокопроизводительным локальным хранилищем, и приложение активно и постоянно использует операции ввода-вывода локального хранилища (см. гл. 6, раздел «IOPS»), тогда да: убедитесь, что увеличенная задержка сетевого хранилища не вызовет эффекта домино с падением производительности (ведь IOPS тоже приводят к задержке). Активное и постоянное использование IOPS — отличительная черта рабочей нагрузки с высокой интенсивностью операций записи: см. раздел «Чтение/запись», гл. 4). Но если вы уже находитесь в облаке или запускаете в нем новое приложение, тогда нет: не беспокойтесь и не думайте о задержке облачного хранилища. Вместо этого заложите фундамент из высокооптимизированных запросов (индексов), данных и паттернов доступа, как описано в главах 2–4 соответственно, и задержка облачного хранилища может никогда не доставлять проблем. Если она все же приносит проблемы, нужно дополнительно оптимизировать рабочую нагрузку, шардировать (см. гл. 5) или приобрести более качественное (более дорогое) облачное хранилище. Помните: Netflix работает в облаке, как и другие очень крупные и успешные компании. Потенциал производительности MySQL в облаке практически неограничен. Вопрос в том, можете ли вы себе это позволить. Производительность — это деньги Вполне уместно, что начало этой книги, раздел «Правдивая история ложной производительности» (см. гл. 1), отражает конец. Но в облаке клиенты «продают» себе больше оперативной памяти, чтобы «исправить» производительность MySQL. Инженер одного из трех крупных облачных провайдеров сказал мне, что большинство экземпляров MySQL перегружено: клиенты платят за емкость, которая значительно превосходит их реальные нужды и потребности приложения3. Можно ли сказать, что отрасль прошла полный круг, и теперь благодаря легкости масштабирования в облаке производительность — это просто больший экземпляр? 2 См. статьи про свойства Amazon EBS (https://oreil.ly/NIly1), производительность блоков хранилища в Google (https://oreil.ly/7Zxaj) и хранилище премиум-класса в Microsoft Azure (https://oreil.ly/LMg03). 3 В силу соглашения о неразглашении я не могу сослаться на источник.
330 | Глава 10 Нет, определенно нет: производительность — это время отклика на запрос; а в облаке каждый байт и миллисекунда производительности оплачиваются ежечасно, что делает все рекомендации и методы, описанные в этой книге, важнее, чем когда-либо. Если вы пользовались какими-либо сервисами в облаке, то следующая информация, вероятно, вас не удивит. Но если вы новичок в этом вопросе, позвольте мне быть первым, кто скажет вам: стоимость использования облака сложно рассчитать, на нее почти невозможно повлиять, а расходы эти нередко недооцениваются (что означает превышение бюджета). Когда инженеры сообща прилагают усилия для оценки и контроля облачных затрат — это верный подход; если они этого не делают, бывают и шестизначные сюрпризы: я видел более 100 000 долларов сверх бюджета. Есть три наиболее важные вещи, которые следует знать, чтобы избежать неожиданных счетов за использование облака. Первое: цена удваивается для каждого уровня базовых вычислений (виртуального сервера, на котором работает MySQL), потому что на каждом уровне удваиваются ресурсы (количество vCPU и объем памяти). Например, если минимальный уровень вычислений задействует 2 vCPU и 8 Гбайт оперативной памяти, то следующий уровень — 4 vCPU и 16 Гбайт оперативной памяти, и цена тоже удваивается. Бывают исключения, но ожидайте удвоения. В результате вы не можете постепенно увеличивать затраты; вы удваиваете затраты на каждый уровень вычислений, который вы масштабируете. С инженерной точки зрения масштабирование с 2 до 8 vCPU еще не такое большое, но цена возрастает в четыре раза. Для наглядности представьте себе, что удвоился ваш ежемесячный платеж по ипотеке или аренде, или за автомобиль, или за обучение. Вероятно, вы были бы очень расстроены, и правильно. Второе: все в облаке стоит денег. Вычислительные затраты — это лишь начало. Следующий список включает в себя общие сборы за MySQL в облаке в дополнение к вычислительным затратам:  Тип хранилища (IOPS);  Хранилище данных (размер);  Резервные копии (размер и хранение);  Логи (размер и хранение);  Высокая доступность (реплики);  Межрегиональная передача данных (размер);  Ключи шифрования (для шифрования данных);  Секреты (для хранения паролей). Более того, эти сборы рассчитываются для каждого экземпляра в отдельности. Например, если вы создаете пять реплик для чтения, каждой реплике выставляется счет за хранение данных, резервное копирование и т. д. Хотел бы я, чтобы все было
MySQL в облаке | 331 проще, но такова реальность: вам нужно исследовать, понимать и оценивать все затраты при использовании MySQL в облаке. Некоторые проприетарные версии MySQL в облаке (см. выше, раздел «Совместимость») — это дополнительные расходы либо совершенно иная модель ценообразования. Третье и последнее: облачные провайдеры предлагают скидки. Не платите полную цену. Как минимум затраты можно значительно сократить, взяв на себя обязательство сроком на один или три года, вместо того, чтобы платить ежемесячно. Другие скидки зависят от поставщика облачных услуг: ищите скидки на зарезервированные экземпляры (англ. reserved instances), фиксированное использование (англ. commited usage) и объем (англ. volume) или спрашивайте об этом. Если ваша компания полагается на облако, то, скорее всего, она заключила контракт с поставщиком облачных услуг. Узнайте, так ли это и влияют ли какие-либо условия ценообразования, которые там прописаны, на затраты для MySQL в облаке. Если вам повезет, контракт может снизить и упростить затраты, что позволит вам сосредоточиться на интересных нюансах использования MySQL. Итоги В этой главе рассказывается, что нужно учитывать при использовании MySQL в облаке. Ключевые моменты:  Совместимость кода и функций MySQL варьируется в зависимости от облака.  Ваша дью-дилидженс («должная добросовестность») заключается в том, чтобы знать о любых несовместимостях кода или функций варианта в облаке по сравнению с MySQL с открытым исходным кодом.  MySQL в облаке можно управляться частично или полностью, в зависимости от поставщика облачных услуг или сторонней компании.  Сетевая задержка (англ. network latency) в глобальных сетях увеличивает время отклика на запрос на десятки или сотни миллисекунд.  Данные для MySQL в облаке обычно хранятся в сетевом хранилище.  Сетевое хранилище имеет задержку в пределах десяти миллисекунд, что эквива- лентно по скорости вращающемуся диску.  Облако взимает плату за все, и затраты могут превышать (и часто превышают) бюджет).  Облачные провайдеры предлагают скидки, не платите полную цену.  Производительность — это время отклика на запрос в облаке. Это последняя глава, но мы еще не закончили: есть еще одна практика.
332 | Глава 10 Практика: попробуйте MySQL в облаке Цель этой практики — попробовать MySQL в облаке. Просто посмотреть, как это работает, работа администратора базы данных не требуется. С одной стороны, я не собирался бесплатно рекламировать ни одного из пяти облачных провайдеров ниже. Эта книга носит строго технический характер. Но, с другой стороны, использование MySQL в облаке становится все популярнее, поэтому я хочу, чтобы вы были подготовлены и добились успеха. Кроме того, эти пять облачных провайдеров предоставляют бесплатную пробную версию (англ. free trial): выдают бесплатный уровень или начальный кредит на учетную запись. Пока вам не требуется ни за что платить: облачные провайдеры должны привлечь ваш бизнес и деньги, доказывая вам ценность своих услуг. Попробуйте создать и использовать MySQL с любым (или несколькими) из этих облачных провайдеров:  MySQL Database Service от Oracle (https://oreil.ly/Z7ZA8);  SkySQL от MariaDB (https://oreil.ly/tn1KY);  Relational Database Service (RDS) от Amazon (https://oreil.ly/yNPfc);  Azure Database for MySQL от Microsoft (https://oreil.ly/Tj3Y1);  Cloud SQL от Google (https://oreil.ly/pnsVt). Если вы обнаружите, что один из них прост в использовании и потенциально ценен, изучите его модель ценообразования и дополнительные затраты. Я специально использую глагол «изучить», потому что, как я упоминал выше в разделе «Производительность — это деньги», стоимость использования облака сложно рассчитать, на нее почти невозможно повлиять, а расходы эти нередко недооцениваются (что означает превышение бюджета). Не забудьте уничтожить свой экземпляр MySQL в облаке до окончания бесплатного пробного периода или до того, как начальный кредит учетной записи достигнет нуля. Это последняя практика в книге, но я призываю вас продолжать учиться и практиковаться, потому что MySQL продолжает развиваться, как и облако. По этой причине даже специалисты по MySQL должны продолжать учиться и практиковаться. Так что закончу дзенской мудростью: Колите дрова. Носите воду.
Предметный указатель A ACID 150, 280, 291 E EXPLAIN 19 ◊ план выполнения запроса 65 G GROUP BY 73 H HLL 220, 295 I IOPS 163, 223 M MVCC 291 MySQL в облаке 323 ◊ администрирование MySQL 325 ◊ локальное хранилище 328 ◊ сетевое хранилище 328 ◊ скидки 331 ◊ совместимость MySQL 324 ◊ список затрат 330 O ORDER BY 78 ORM 155, 315 OSC 316 R READ COMMITED 281 W WHERE 67 А Алгоритмы соединения таблиц 100 ◊ блочного соединения с вложенным циклом 101 ◊ хеш-соединения 101 ◊ соединение с вложенным циклом 100 Альтернативы MySQL 185 Анализ запросов 16, 23, 40 Аннотация запроса 19 Б Бинарный лог ◊ парадокс бинарного лога 133 ◊ события бинарного лога 253 Блокировка 27, 29 ◊ MDL 28 ◊ намерения вставки 288 ◊ строк 276 ◊ данных InnoDB 277 ◊ взаимоблокировка 220
334 | Предметный указатель Блокировка (прод.) ◊ конфликт блокировки строк 132 ◊ конфликт блокировок 221 Буферный пул 225 В Время выполнения запроса ◊ относительные значения 37 ◊ среднее значение, процентиль и максимум 37 Время отклика на запрос 13 Д Дайджест-текст 18 Дайджест-хеш 18 Дедупликация 124 Денормализация базы данных 126 Дрейф данных 314 И Изменения схемы 173 Индексные страницы 227 ◊ сброс индексных страниц 230 Индексы 54, 92 ◊ вторичные индексы 57, 285 ◊ индексные записи 276 ◊ интервал 276 ◊ статистика индекса 99 Информационная схема ◊ InnoDB 209 ◊ MySQL 305 К Кардинальность индекса 98 Кеширование 158 Ключевые показатели производительности 196 Кодировка 123 Л Лог ◊ медленных запросов 16 ◊ ошибок MySQL 155 ◊ транзакций 234 ◊ отмены 292 М Масштабирование 51 ◊ линейное масштабирование 139 Медленный запрос 21, 36 Метаданные 36 Метрики запроса 15–18, 21–37, 45 Метрики сервера 191 ◊ разрешение метрик 238 Н Нормализация базы данных 125 Нормализация инструкции SQL 18 О Облачная MySQL 323 ◊ администрирование MySQL 325 ◊ локальное и сетевое хранилища 328 ◊ затраты и скидки 330, 331 ◊ совместимость MySQL 324 Образцы запросов 19 Ограниченность набора данных 175 Оповещение 238 Оптимизация запросов 40 ◊ косвенная оптимизация запросов 41 ◊ прямая оптимизация запросов 40 Отчет о запросе 16, 23 Отчетность 20 Очередь для операций записи 159 П Паттерны доступа 137, 145, 201 ◊ характеристики паттернов доступа 146–153 План выполнения запроса 65 Подсистемы хранения данных MySQL 26 Поле метрик 197–201 Пороговое значение 240 Профиль запроса 21 Р Рабочая нагрузка 52 Размер пакета 130 Репликация 249 ◊ аварийное переключение 258 ◊ асинхронная репликация 258 ◊ запаздывание репликации 249
Предметный указатель ◊ ◊ ◊ ◊ ◊ ◊ ◊ источник 251 многопоточная репликация 262 мониторинг 266 полусинхронная репликация 260 реплика 251 типы репликации 250 точка перегиба 268 С Секционирование 161 Спектры метрик 202 ◊ временные объекты 214 ◊ глобальное время отклика на запрос 204 ◊ метрики InnoDB 220 ◊ метрики запросов 207 ◊ метрики объема данных 218 ◊ метрики репликации 218 ◊ ошибки 205 ◊ плохие SELECT 216 ◊ подготовленные инструкции 215 ◊ потоки и соединения 211 ◊ пропускная способность сети 217 Сплит-брейн 313 Т Таблицы ◊ методы доступа к таблице 59–61 ◊ соединение таблиц 84, 100, 101 Тесты производительности 170 Тонкая настройка (тюнинг) 53 Транзакции 25, 275 ◊ большая 298 ◊ длительная 299 | 335 ◊ зависшая 300, 301 ◊ зафиксированная 307 Требование крайнего левого префикса 63 У Удаление или архивирование данных 129 Универсальный закон масштабируемости 140 Ф Фантомные строки 280 Х Хаотичный инжиниринг 317 Хранение данных 118 ◊ формат данных 122 Ч Четырехлетняя пригодность 175 Ш Шардирование 167, 175 ◊ ключ шардирования 176 ◊ микросервисы 188 ◊ ребалансировка 184 ◊ решардирование 183 ◊ хеш-шардирование 178 ◊ на основе диапазона 179 ◊ на основе поиска 181 Шумный сосед 317
Об авторе Дэниэл Нихтер — администратор базы данных, который использует MySQL более 15 лет. Он начал оптимизировать производительность MySQL в 2004 году, работая в дата-центре. Вскоре после этого он создал https://hackmysql.com — чтобы поделиться своими знаниями и приемами. Дэниэл известен благодаря инструментам, которые представил аудитории за время своего восьмилетнего пребывания в Percona. Часть из них де-факто остаются стандартом и применяются в крупнейших технологических компаниях мира. Он также является лауреатом премии MySQL Community Award, докладчиком на конференциях и автором широкого круга статей с открытым исходным кодом. В настоящее время Дэниэл — администратор базы данных и инженер-программист в Square, финтех-компании с тысячами серверов MySQL. Об обложке Птица на обложке данной книги — хохлатый барбет (Trachyphonus vaillantii), получивший прозвище «фруктовый салат» за свое красочное оперение и фруктовую диету. В основном обитающие на юге Африки, они обычно встречаются в лесных массивах, пригородных садах и огородах, а также вдоль русел рек и не мигрируют. Окраска взрослого хохлатого барбета делает его легкоузнаваемым. У него красно-желтая голова, большой, толстый, бледный зеленовато-желтый клюв и серо-черная точка на щеке. Его мантия, крылья, хвост и толстая полоса на груди — черные с белыми полумесяцами или точками. Нижняя часть спины хохлатого барбета — желтая с красными верхними кроющими перьями хвоста. На его нижней стороне больше желтого и красного, а ноги и ступни серые. Хохлатые барбеты — очень голосистые птицы. Их пронзительная, похожая на барабанный бой песня может длиться несколько минут. Они территориальны и агрессивны, особенно в период размножения. Хотя они маленькие и неуклюже летают, они сильны и способны не просто спугнуть других птиц, млекопитающих и рептилий, которые приближаются к их гнездам, но даже атаковать их. Они моногамны и обычно встречаются парами. Пары роют углубление под гнездо в гнилых деревьях и размножаются круглый год, если условия благоприятны. Поскольку они питаются мелкими насекомыми и улитками, хохлатые барбеты приветствуются в жилых районах, но на коммерческих фермах они серьезно повреждают посевы. По этой причине, а также из-за торговли домашними животными хохлатых барбетов отлавливают, но пока они еще не находятся под угрозой исчезновения. Многие животные на обложках O’Reilly находятся под угрозой исчезновения; все они важны для мира. Иллюстрация на обложке выполнена Карен Монтгомери по мотивам черно-белой гравюры из «Английской энциклопедии».