Як ми тиждень лагодили compaction в Cassandra

Основним сховищем метрик у нас є 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 з наступними параметрами:
{
'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', 
'base_time_seconds': '14400', 
'max_sstable_age_days': '90'
}

Це дозволяє для нашого випадку домогтися, щоб дані за один і той же інтервал часу лежали поруч і не перемішувалися зі старими даними. У той же час дані, старше 90 днів не повинні компактиться взагалі, це виключить непотрібне навантаження на диски, так як ці дані вже точно не будуть змінюватися.
З'являється гіпотеза: раптом наші sstable не компактятся, тому що кассандра думає, що вони старше 90 днів?
Час, на яке спирається кассандра — це внутрішній таймстамп, який обов'язково є у кожної колонки. Кассандра або проставляє поточний таймстамп при запису даних, або він може бути поставлене клієнтом:
INSERT INTO table (fld1, fld2) VALUES (val1, val2) USING TIMESTAMP 123456789;

ми не використовуємо цю фічу).
Під час перевірки метадаты всіх sstable утилітою sstablemetadataми знайшли аномальне значення таймстампа:
$ sstablemetadata /mnt/ssd1/cassandra/okmeter/bunches-3f892060ef5811e5950a476750300bfc/okmeter-bunches-ka-377-Data.db |head
SSTable: /mnt/ssd1/cassandra/okmeter/bunches-3f892060ef5811e5950a476750300bfc/okmeter-bunches-ka-377
Partitioner: org.apache.cassandra.dht.RandomPartitioner
Bloom Filter FP chance: 0.010000
Minimum timestamp: 1458916698801023
Maximum timestamp: 5760529710388872447

Але у новостворених sstable були абсолютно нормальні таймстампы, чому вони не компактятся? коде виявилося таке:
/**
* Gets the timestamp that DateTieredCompactionStrategy considers to be the "current time".
* @return the maximum timestamp across all SSTables.
* @throws java.util.NoSuchElementException if there are no SSTables.
*/
private long getNow()
{
return Collections.max(cfs.getSSTables(), new Comparator<SSTableReader>()
{
public int compare(SSTableReader o1, SSTableReader o2)
{
return Long.compare(o1.getMaxTimestamp(), o2.getMaxTimestamp());
}
}).getMaxTimestamp();
}

Now у нашому випадку:
$ date -d @5760529710388
Sat Dec 2 16:46:28 MSK 184513

То є ще 182 тисячі років можна навіть не сподіватися на compaction:)
На кожному з трьох проблемних серверів була одна "бита" sstable досить великого розміру (60Gb, 160Gb і 180Gb). Саму "маленьку" з них посунули в бік, потім через sstable2json отримали человекочитаемый файлик розміром 125Gb і почали його grep'ать. З'ясувалося, що є одна бита колонка (другорядна метрика одного тестового проекту), яку можна сміливо видалити.
Стандартного способу видалити дані з sstable у кассандри не знайшлося, але дуже близька за змістом утиліта sstablescrub. Подивившись Scrubber.javaстало зрозуміло, що timestamp він не читає і зробити гарний патч досить складно, ми зробили некрасивий:
--- a/src/java/org/apache/cassandra/db/compaction/Scrubber.java
+++ b/src/java/org/apache/cassandra/db/compaction/Scrubber.java
@@ -225,6 +225,11 @@ public class смуги навігації implements Closeable
if (indexFile != null && dataSize != dataSizeFromIndex)
outputHandler.warn(String.format("Data file row size %d different from index file row size %d", dataSize, dataSizeFromIndex));

+ if (sstable.metadata.getKeyValidator().getString(key.getKey()).equals("226;4;eJlZUXr078;1472083200")) {
+ outputHandler.warn(String.format("key: %s", sstable.metadata.getKeyValidator().getString(key.getKey ())));
+ throw new IOError(new IOException("Broken column timestamp"));
+ }
+
if (tryAppend(prevKey, key, dataSize, writer))
prevKey = key;
}

, де 226;4;eJlZUXr078;1472083200 ключ битою запису, який ми знаємо в результаті вправ з sstable2json.
І це спрацювало!
Окремо порадувало, що sstablescrub працює дуже швидко, практично на швидкості запису на диск. Так як sstable — це immutable структура, будь-які модифікації створюють нову sstable, тобто для scrub потрібно забезпечити достатній обсяг вільного місця на дисках. Для нас це виявилося проблемою, довелося робити scrub на іншому сервері і копіювати зачищену sstable назад на потрібний сервер.
Після зачистки битою запису, наша таблиця початку компактиться сама.
Але на одній ноде ми помітили, що постійно компактится одна доволі важка sstable (сама з собою) розміром більше 100Gb:
CompactionTask.java:274 - Compacted 1 sstables to [/mnt/ssd1/cassandra/okmeter/bunches-3f892060ef5811e5950a476750300bfc/okmeter-bunches-ka-5322,]. 116,660,699,171 bytes to 116,660,699,171 (~100% of original) in 3,653,864 ms = 30.448947 MB/s. 287,450 total partitions merged to 287,450. Partition merge counts were {1:287450, }

Як тільки процес закінчувався, вона починала компактиться знову, наприклад графік операцій читання дисків виглядав так:



Тут видно як цей файлик мігрував з ssd1 на ssd2 і назад.
В балці так само була помилка про брак місця:
CompactionTask.java:87 - insufficient space to compact all requested files SSTableReader(path='/mnt/ssd2/cassandra/okmeter/bunches-3f892060ef5811e5950a476750300bfc/okmeter-bunches-ka-2135-Data.db'), STableReader(path='/mnt/ssd1/cassandra/okmeter/bunches-3f892060ef5811e5950a476750300bfc/okmeter-bunches-ka-5322-Data.db')

Але навіщо взагалі компактить 1 sstable? Довелося розбиратися, як взагалі вибираються sstable для compaction в DateTieredCompactionStrategy:
  • Береться список всіх sstable для поточної таблиці;
  • Виключаються sstable, які беруть участь у compaction прямо зараз;
  • Виключаються sstable, у яких максимальний таймстамп старше max_sstable_age_days;
  • Залишилися групуються за мінімального таймстампу, починаючи з самих свіжих. У кожній з таких груп обираються кандидати згідно SizeTieredCompactionStrategy, причому для першого інтервалу (base_time_seconds) необхідно мінімум min_threshold (в нашому випадку 4) файлів, а для більш старих досить і двох. Якщо кандидатів в межах групи немає, група пропускається. За один прохід для компактизації вибирається одна сама "молода" група;
  • Для обраної групи sstable для компактизації прогнозується розмір результуючого файлу (просто сума всіх вихідних), якщо вільного місця немає ні в одній data_file_directoriesз групи виключається найбільший файл;
  • Якщо в групі в результаті залишився хоча б 1 файл запускається компактизация;
При рівні логгирования DEBUG стало видно, що в нашому випадку:
  • вийшла група на компактизацию з 3х файлів
  • SizeTieredCompactionStrategy вибрала для злиття 2 файлу з 3х
  • -а брак місця залишився 1 кандидат, він і компактился по колу
Я не беруся судити баг чи фіча (може при використанні TTL є необхідність компактить 1 файл), але нам потрібно було це якось розрулити. Ми вирішили, що потрібно просто знайти спосіб дати кассандрі їх закомпактить.
Ми використовуємо сервери, у яких sata 2 диска, і 2 ssd. Проблеми з місцем у нас виникають на ssd, ми вирішили, що кандидати для цієї проблемної компактизації ми скопіюємо на диск, поставимо лінки на ssd, тим самим звільнимо місце на ssd для результуючої sstable. Це спрацювало і компактизация на цій машині стала працювати в звичайному режимі.


На цьому графіку видно, що в процесі брав участь hdd2.
Разом
  • Не розібралися ми тільки з тим, як в sstable потрапила запис з кривим таймстампом (відкладемо це до повторення, якщо воно буде звичайно)
  • Потрібно підкрутити налаштування DateTieredCompactionStrategy: знизити max_sstable_age_days до 10-30 днів, щоб sstable не разростались до гігантських розмірів (простіше буде забезпечити буфер при маніпуляціях з ними)
  • Вихідний код кассандри досить зрозумілий і документований
  • На доступність нашого сервісу дана проблема впливу майже не надала (подтормаживали кілька разів по 2-3 хвилини)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.