Основным хранилищем метрик у нас является cassandra, мы используем её уже более трех лет. Для всех предыдущих проблем мы успешно находили решение, используя встроенные средства диагностики кассандры.  В кассандре достаточно информативное логгирование (особенно на уровне DEBUG, который можно включить на лету), подробные метрики, доступные через JMX и богатый набор утилит (nodetool, sstable*). Но недавно мы столкнулись с одной достаточно интересной проблемой, и нам пришлось серьезно поломать голову, почитать исходный код кассандры, чтобы разобраться, что происходит. Началось все с того, что время отклика кассандры на чтение по одной из таблиц начало расти:


 В логах при этом было пусто, никаких фоновых процессов (compactions) не было. Мы достаточно быстро заметили, что растет количество SSTable в таблице bunches (там мы храним значения метрик наших клиентов):


 Причем, что это происходит только на трех серверах из 9: 
 Дальше мы долго тупили, гуглили и читали JIRA, но похожих багов не нашлось. Но время шло и нужно было найти хотя бы временное решение, так как время ответа росло практически линейно. Было решено сделать принудительный compaction таблицы bunches, но так как из документации не очевидно, на одной или на всех нодах начнется компактизация при nodetool compact, мы иницировали этот процесс через JMX. Дело в том, что мы уже научены горьким опытом, что изменение compaction strategy через ALTER TABLE чревато запуском полного compactiton одновременно на всех нодах кластера. Тогда же нашелся способ делать это более управляемо. На этот раз оказалось, что nodetool compact запускает compaction только на той ноде, с которой мы работаем. После того, как закончилась компактизация, количество sstable снизилось, но сразу начало расти снова:


 Таким образом у нас появился костыль, позволяющий в ручном режиме держать показатели работы кассандры на приемлемом уровне. Мы поставили принудительный compaction в крон на проблемных нодах, и график времени ответа на чтение стал выглядеть так: 

 В данный момент мы используем cassandra 2.1.15, но в JIRA обнаружили несколько похожих багов, исправленных в версиях 2.2+.  Так как хороших идей на тот момент не было, мы решили обновить одну из проблемных нод до 2.2 (тем более мы все равно собирались это делать и наше приложение уже было протестировано с 2.2). Обновление прошло гладко, но нашу проблему это не решило.  Чтобы не вносить дополнительной энтропии в текущую ситуацию, кластер целиком мы решили не обновлять и вернуться на 2.1 (это делается через удаление ноды из кластера и возвращеним обратно со старой версией).  Стало понятно, что с наскока эту проблему решить не удастся и пора идти читать код кассандры. Так как в этой таблице у нас в конечном счете хранятся timeseries, мы используем DateTieredCompactionStrategy со следуюшими настройками: Это позволяет для нашего случая добиться, чтобы данные за один и тот же интервал времени лежали рядом и не перемешивались со старыми данными. В тоже время данные, старше 90 дней не должны компактиться вообще, это исключит ненужную нагрузку на диски, так как эти данные уже точно не будут меняться. Появляется гипотеза: вдруг наши sstable не компактятся, потому что кассандра думает, что они старше 90 дней? Время, на которое опирается кассандра — это внутренний таймстамп, который обязательно есть у каждой колонки. Кассандра либо проставляет текущий таймстамп при записи данных, либо он может быть задан клиентом: (мы не используем эту фичу).  При проверки метадаты всех sstable утилитой sstablemetadata, мы нашли аномальное значение таймстампа: Но у вновь созданных sstable были абсолютно нормальные таймстампы, почему они не компактятся? В коде обнаружилось такое: Now в нашем случае: То есть еще 182 тысячи лет можно даже не надеяться на compaction:) На каждом из трех проблемных серверов была одна "битая" sstable достаточно большого размера (60Gb, 160Gb и 180Gb). Самую "маленькую" из них сдвинули в сторону, потом через sstable2json получили человекочитаемый файлик размером 125Gb и начали его grep'ать. Выяснилось, что есть одна битая колонка (второстепенная метрика одного тестового проекта), которую можно смело удалить. Стандартного способа удалить данные из sstable у кассандры не нашлось, но очень близка по смыслу утилита sstablescrub. Посмотрев Scrubber.java, стало понятно, что timestamp он не читает и сделать красивый патч достаточно сложно, мы сделали некрасивый: , где 226;4;eJlZUXr078;1472083200 ключ битой записи, который мы знаем в результате упражнений с sstable2json.  И это сработало!
Отдельно порадовало, что sstablescrub работает очень быстро, практически на скорости записи на диск. Так как sstable — это immutable структура, любые модификации создают новую sstable, то есть для scrub нужно обеспечить достаточный объем свободного места на дисках. Для нас это оказалось проблемой, пришлось делать scrub на другом сервере и копировать зачищенную sstable обратно на нужный сервер. После зачистки битой записи, наша таблица начала компактиться сама.  Но на одной ноде мы заметили, что постоянно компактится одна достаточно увесистая sstable (сама с собой) размером больше 100Gb: Как только процесс заканчивался, она начинала компактиться опять, например график операций чтения по дискам выглядел так:


 Тут видно как этот файлик мигрировал с ssd1 на ssd2 и обратно. В логе так же была ошибка о нехватке места: Но зачем вообще компактить 1 sstable? Пришлось разбираться, как вообще выбираются sstable для compaction в DateTieredCompactionStrategy:  При уровне логгирования DEBUG стало видно, что в нашем случае: Я не берусь судить баг это или фича (может при использовании TTL есть необходимость компактить 1 файл), но нам нужно было это как-то разрулить. Мы решили, что нужно просто найти способ дать кассандре их закомпактить. Мы используем серверы, в которых 2 sata диска и 2 ssd. Проблемы с местом у нас возникают на ssd, мы решили, что кандидаты для этой проблемной компактизации мы скопируем на диск, поставим линки на ssd, тем самым освободим место на ssd для результирующей sstable. Это сработало и компактизация на этой машине стала работать в обычном режиме. 

 На этом графике видно, как в процессе участвовал hdd2.