Отъявленные баги и как их избежать на примере ClickHouse

в 6:17, , рубрики: c++, clickhouse, debug, баги, Блог компании Конференции Олега Бунина (Онтико), высокая производительность, отладка, Программирование

Если вы пишете код — готовьтесь к проблемам. Они обязательно будут, и ожидать их стоит со всех сторон: от вашего кода и компилятора, от операционной системы и железа, а еще пользователи иногда подкидывают «сюрпризы». Если вы отмасштабировали кластер до космических масштабов, то ожидайте «космических» багов. Особенно, если речь идёт о данных из интернет-трафика.

Алексей Миловидов (o6CuFl2Q) расскажет о самых нелепых, обескураживающих и безнадёжных проблемах из его опыта разработки и поддержки ClickHouse. Посмотрим, как их приходилось отлаживать и какие меры принимать разработчикам с самого начала, чтобы проблем было меньше.

Отъявленные баги

Если вы написали какой-то код, сразу готовьтесь к проблемам.

Ошибки в коде. Они будут обязательно. Но, допустим, вы написали идеальный код, скомпилировали, но баги появятся в компиляторе и код будет работать неверно. Исправили компилятор, всё скомпилировалось — запускаете. Но (неожиданно) всё работает неверно, потому что в ядре ОС тоже есть баги.

Если в ОС нет багов, неизбежно, они будут в железе. Даже если вы написали идеальный код, который идеально работает на идеальном железе, все равно возникнут проблемы, например, ошибки конфигурации. Казалось бы, вы все сделали правильно, но кто-то допустил ошибку в конфигурационном файле, и все снова не работает.

Когда все баги исправили — добьют пользователи, потому что постоянно «неправильно» используют ваш код. Но проблема точно не в пользователях, а в коде: вы написали то, что трудно использовать.

Рассмотрим эти баги на нескольких примерах.

Баги конфигурации

Удаление данных. Первый случай из практики. К счастью, не моей и не Яндекса, не беспокойтесь.

Сначала вводная. Архитектура map-reduce кластера (типа Hadoop) — это несколько серверов с данными (дата-ноды), которые хранят данные, и один или несколько мастер-серверов, которые знают расположение всех данных на серверах.

Дата-ноды знают адрес мастера и соединяются с ним. Мастер следит за тем, где и какие данные должны находиться, и отдаёт разные команды дата-нодам: «Скачайте данные X, у вас должны быть данные Y, а данные Z удалите». Что может пойти не так?

Когда выложили новый файл с конфигурацией на все дата-ноды, по ошибке они подсоединились к мастеру от другого кластера, а не к своему. Мастер посмотрел на данные, о которых ему сообщили дата-ноды, решил, что данные неправильные и надо их удалить. Проблему заметили, когда половина данных стерлась.

Отъявленные баги и как их избежать на примере ClickHouse - 1

Самые эпичные баги те, что приводят к непреднамеренному удалению данных.

Избежать этого очень просто.

Не удалять данные. Например, откладывать в отдельную директорию или удалять с задержкой. Сначала переносим, чтобы они не были видны пользователю, а если он обнаружит, что что-то пропало в течение нескольких дней, вернем обратно.

Не удалять неожиданные данные, если причина неизвестна. Программно ограничьте запуск удаления неизвестных данных: неожиданных, со странными именами, или, если их слишком много. Администратор заметит, что сервер не запускается и пишет какое-то сообщение, и будет разбираться.

Если программа совершает деструктивные действия — изолируйте тестинг и продакшн на уровне сети (iptables). Например, удаление файлов или отправка e-mail — это деструктивное действие, потому что оно «съест» чье-то внимание. Поставьте на них порог: сотню писем можно отправить, а для тысячи поставьте флажок-предохранитель, который выставляется перед тем, как произойдет что-то ужасное.

Конфигурации. Второй пример уже из моей практики.

У одной хорошей компании как-то странно работал кластер ClickHouse. Странность была в том, что не синхронизировались реплики. При перезапуске сервер не запускался и возникало сообщение, что все данные неверные: «Много неожиданных данных, не буду запускаться. Надо выставить флаг force_restore_data и разобраться».

В компании никто разобраться не смог — просто выставили флаг. При этом половина данных куда-то исчезала, получались графики с пропусками. Разработчики обратились ко мне, я подумал, что происходит что-то интересное, и решил расследовать. Когда через несколько часов наступило утро и за окном запели птички, я понял, что ничего не понял.

Сервер ClickHouse для координации использует сервис ZooKeeper. ClickHouse хранит данные, а ZooKeeper определяет, на каких серверах какие данные должны лежать: хранит метаданные о том, какие данные на какой реплике должны быть. ZooKeeper это тоже кластер — он реплицируется по очень хорошему алгоритму с распределенным консенсусом, со строгой консистентностью.

Как правило, ZooKeeper это 3 машины, иногда 5. В конфигурации ClickHouse указываются сразу все машины, соединение устанавливается со случайной машиной, с ней взаимодействует, и этот сервер реплицирует все запросы.

Что же произошло? У компании было три сервера ZooKeeper. Но они работали не как кластер из трёх узлов, а как три независимых узла — три кластера из одного узла. Один ClickHouse подсоединяется к одному серверу и записывает данные. Реплики хотят скачать эти данные, а их нигде нет. При перезапуске сервер подсоединяется к другому ZooKeeper: видит, что данные, с которыми он работал до этого, лишние, надо их куда-то отложить. Он их не удаляет, а перекладывает в отдельную директорию — в ClickHouse так просто данные не удаляются.

Решаю исправить конфигурацию ZooKeeper. Переименовываю все данные и делаю запрос ATTACH частей данных из директории detached/unexpeted_*.

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

Это были простые баги конфигурации. Больше багов будет в коде.

Баги в коде

Мы код пишем на C++. Это значит, что у нас уже проблемы.

Следующий пример — реальный баг из продакшн на кластере Яндекс.Метрики (2015 год) — следствие кода на C++. Баг был в том, что иногда пользователь вместо ответа на запрос получал сообщение об ошибке:

  • «Checksum doesn't match, corrupted data» — чек-сумма не совпадает, данные побились — страшно!
  • «LRUCache became inconsistent. There must be a bug in it» — кэш стал неконсистентным, скорее всего в нём баг.

Код, который мы написали, сам сообщает о том, что там есть баг.

«Checksum doesn't match, corrupted data». Чек-суммы сжатых блоков данных проверяются до их разжатия. Обычно эта ошибка появляется, когда на файловой системе битые данные. По разным причинам часть файлов оказываются мусорными при перезапуске сервера.

Но здесь другой случай: читаю файл вручную, чек-сумма совпадает, ошибки нет. Однажды появившись, ошибка стабильно воспроизводится при повторном запросе. При перезапуске сервера ошибка исчезает на некоторое время, а потом снова стабильно появляется.

Возможно, дело в оперативной памяти? Типичная ситуация, когда в ней бьются биты. Смотрю в dmesg (kern.log), а machine check exceptions нет — они обычно пишут, когда что-то не так с оперативной памятью. Если бы на сервере была битая оперативная память, то неправильно работала бы не только моя программа, но и все остальные выдавали бы ошибки случайным образом. Однако других проявлений ошибки нет.

«LRUCache became inconsistent. There must be a bug in it». Это явная ошибка в коде, а мы пишем на C++ — возможно, проезд по памяти? Но тесты под AddressSanitizer, ThreadSanitizer, MemorySanitizer, UndefinedBehaviorSanitizer в CI ничего не показывают.

Возможно, некоторые тестовые кейсы не покрыты? Собираю сервер с AddressSanitizer, запускаю на продакшн — ничего не ловит. На некоторое время ошибку лечит сброс некоторого mark cache (кэша засечек).

Одно из правил программирования гласит: если непонятно, в чем баг, — пристально смотри в код, в надежде что-нибудь там найти. Я так и сделал, нашел баг, исправил — не помогло. Смотрю в другое место в коде — там тоже баг. Исправил, опять не помогло. Исправил еще несколько, код стал лучше, но ошибка все равно никуда не исчезла!

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

Оказалось, причина в том, что на «проблемном» кластере использовали новую возможность — кэш-словари. Они используют написанный вручную аллокатор памяти ArenaWithFreeLists. Мы не только пишем на C++, а еще пилим какие-то кастомные аллокаторы — дважды обрекаем себя на проблемы.

ArenaWithFreeLists — это часть памяти, в которой память выделяется подряд по размерам кратным двум: 16, 32, 64 байта. Если освободить память, то они формируют односвязный список свободных блоков FreeLists.

Посмотрим на код.

class ArenaWithFreeLists
{
    Block * free_lists[16] {};
    static auto sizeToPreviousPowerOfTwo(size_t size)
    {
        return _bit_scan_reverse(size - 1);
    }

    char * alloc(size_t size)
    {
        const auto list_idx = findFreeListIndex(size);
        free_lists[list_idx] ->...
    }
}

Здесь используется функция _bit_scan_reverse с подчеркиванием в начале.

Есть негласное правило: «Если у функции в начале одно подчеркивание, прочитайте документацию по ней один раз, а если два — прочитайте два раза».

Слушаемся и читаем документацию: «int _bit_scan_reverse(int a). Set dst to the index of the highest set bit in 32-bit integer a. If no bits are set in a then dst is undefined». Кажется, мы нашли проблему.

В C++ эта ситуация для компилятора считается невозможной. Компилятор может использовать неопределенное поведение (undefined behaviour), эту «невозможность», как предположение для оптимизации кода.

Компилятор ничего плохого не делает — он честно генерирует ассемблерную инструкцию bsr %edi, %eax. Но, если операнд равен нулю, у инструкции bsr неопределённое поведение не на уровне C++, а на уровне CPU. Если регистр источника равен нулю, то регистр назначения не меняется: на входе был какой-то мусор, на выходе тоже останется этот мусор.

Результат зависит от того, куда компилятор поставит эту инструкцию. Иногда функция с этой инструкцией инлайнится, иногда нет. Во втором случае будет примерно такой код:

bsrl %edi, %eax
retq

Дальше я посмотрел на примере подобного кода в моем бинарном файле с помощью objdump.

Отъявленные баги и как их избежать на примере ClickHouse - 2

По результатам вижу, что иногда регистр источника и регистр назначения одинаковые. Если там был ноль, то и результат тоже будет ноль — все нормально. Но иногда регистры разные, и результатом будет мусор.

Как проявляется этот баг?

  • Используем мусор как индекс в массиве FreeLists. Вместо массива обращаемся по какому-то далекому адресу и получаем проезд по памяти.
  • Нам повезло, почти все адреса рядом заполнены данными из кэша — мы портим кэш. Кэш содержит смещения в файлах.
  • Читаем файлы по неправильному смещению. Из неправильного смещения получаем чек-сумму. Но там не чек-сумма, а что-то другое — эта чек-сумма не совпадет со следующими данными.
  • Получаем ошибку «Checksum doesn't match, corrupted data».

К счастью, испорчены не данные, а только кэш в оперативке. Нам сразу сообщали об ошибке, потому что данные мы чек-суммируем. Ошибку исправили 27 декабря 2015 года и пошли отмечать.

Как видим, неверный код хотя бы можно исправить. Но как исправить баги в железе?

Баги в железе

Это даже не баги, а физические законы — неизбежные эффекты. По физическим законам железо неизбежно глючит.

Неатомарность записи на RAID. Например, мы создали RAID1. Он состоит из двух жёстких дисков. Это значит, что один сервер — это распределенная система: данные записываются на один жесткий диск и на другой. Но что если на один диск данные запишутся, а во время записи на второй исчезнет питание? Данные на массиве RAID1 будут не консистентными. Мы не сможем понять, какие данные верны, потому что будем читать то одни байты, то другие.

Можно с этим бороться размещением лога. Например, в ZFS эта проблема решена, но об этом позже.

bit rot на HDD и SSD. Биты на жестких дисках и на SSD могут портиться просто так. Современные SSD, особенно с многоуровневыми ячейками рассчитаны на то, что ячейки будут постоянно портиться. Коды коррекции ошибок помогают, но иногда ячейки портятся так сильно и так много, что даже это не спасает. Получаются незамеченные ошибки.

bit flips в оперативке (а как же ECC?). В оперативной памяти в серверах биты тоже портятся. В ней тоже есть коды коррекции ошибок. Когда проявляются ошибки, их обычно видно по сообщениям в логе ядра Linux в dmesg. Когда ошибок много, мы увидим что-то вроде: «Исправлено N миллионов ошибок с памятью». Но отдельные биты не будут замечены, и наверняка что-то будет глючить.

bit flips на уровне CPU и в сети. Бывают ошибки на уровне CPU, в кэшах CPU и, конечно, при передаче данных по сети.

Как обычно проявляются ошибки железа? На GitHub приходит тикет «A malformed znode prevents ClickHouse from starting» — испорчены данные в ноде ZooKeeper.

В ZooKeeper мы обычно пишем немного метаданных в обычном текстовом виде. В нем что-то не так — «replica» написано очень странно.

Отъявленные баги и как их избежать на примере ClickHouse - 3

Редко бывает так, что из-за бага в коде меняется один бит. Конечно, мы можем написать такой код: берем фильтр Блума, меняем бит по определенным адресам, адреса вычисляем неверно, меняем неправильный бит, он приходится на какие-то данные. Все, теперь в ClickHouse не «replica», а «repliba» и на ней все данные неправильные. Но обычно, изменение одного бита — это симптом проблем с железом.

Возможно, вы знаете пример «bitsquatting». Артём Динабург поставил эксперимент: в интернете есть домены, на которых очень много трафика, хотя пользователи не переходят на эти домены самостоятельно. Например, такой домен FB-CDN.com — это CDN Facebook.

Артём зарегистрировал похожий домен (и еще много других), но изменил один бит. Например, FA-CDN.com вместо FB-CDN.com. Домен нигде не публиковался, но на него приходил трафик. Иногда в HTTP-заголовках был записан хост FB-CDN, а запрос пошел на другой хост из-за ошибок в оперативной памяти на устройствах пользователей. Оперативная память с коррекцией ошибок помогает не всегда. Иногда она даже мешает и приводит к уязвимостям (читайте о Rowhammer, ECCploit, RAMBleed).

Вывод: всегда чек-суммируйте данные сами.

При записи в файловую систему чек-суммируйте в обязательном порядке. При передаче по сети тоже чек-суммируйте — не рассчитывайте на то, что там есть какие-то чек-суммы.

Больше багов!..

Продакшн-кластер Метрики. Пользователи в ответ на запрос иногда получают исключение: «Checksum doesn't match: corrupted data» — чек-сумма не верна, поврежденные данные.

Отъявленные баги и как их избежать на примере ClickHouse - 4

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

Когда мы получили пакет по сети от какого-то сервера, появилось исключение — выглядит знакомо. Возможно, опять проезд по памяти, race condition или еще что-нибудь.

Это исключение появилось в 2015 году. Баг исправили, он больше не проявлялся. В феврале 2019 он внезапно появился снова. В это время я был на одной из конференций, с проблемой разбирались мои коллеги. Ошибка воспроизводилась несколько раз в сутки среди 1000 серверов с ClickHouse: то на одном сервере, то на другом, статистику не собрать. При этом никаких новых релизов в это время не было. Разобраться и решить проблему не получилось, но через несколько дней ошибка исчезла сама.

Об ошибке забыли, а 15 мая 2019 она повторилась. Мы продолжили с ней разбираться. Первое, что я сделал — посмотрел все доступные логи и графики. Изучал их целый день, ничего не понял, никаких закономерностей не обнаружил. Если проблему не удаётся воспроизвести, единственный вариант: собирать все случаи, искать закономерности и зависимости. Возможно, ядро Linux неправильно работает с процессором, неправильно сохраняет или загружает какие-нибудь регистры.

Гипотезы и закономерности

7 из 9 серверов с E5-2683 v4подвержены ошибке. Но из подверженных ошибке только около половины E5-2683 v4 — пустая гипотеза.

Ошибки обычно не повторяются. Кроме кластера mtauxyz, где Corrupted data действительно есть (битые данные на диске). Это другой случай, отказываемся от гипотезы.

Ошибка не зависит от ядра Linux — проверил на разных серверах, ничего не нашел. В kern.log ничего интересного, сообщений machine check exception нет. В графиках сети, в том числе ретрансмиттов, CPU, IO, Network, ничего интересного. Все сетевые адаптеры на серверах, на которых проявляются и не проявляются ошибки, одинаковые.

Никаких закономерностей нет. Что делать? Продолжать искать закономерности. Вторая попытка.

Смотрю на uptime серверов: uptime высокий, серверы работают стабильно, segfault и чего-то подобного нет. Всегда радуюсь, когда вижу, что программа упала с segfault — она хотя бы упала. Хуже, когда ошибка есть, она что-нибудь портит, а этого никто не замечает.

Ошибки сгруппированы по дням и проявляются в течение пары дней. В какие-то 2 дня проявляются больше, в какие-то меньше, потом снова больше — не получается точно определить время появления ошибок.

У некоторых ошибок совпадают пакеты и чек-сумма, которую мы ожидали. У большинство ошибок всего два варианта пакетов. Мне повезло, потому что в сообщение об ошибке мы добавили именно само значение чек-суммы, что помогло составить статистику.

Нет закономерностей по серверам, откуда читаем данные. Размер сжатого блока, который мы чек-суммируем, меньше килобайта. Посмотрел размеры пакетов в HEX. Это мне не пригодилось — бинарное представление размеров пакетов и чек-сумм ничем не примечательно.

Ошибку не исправил — снова искал закономерности. Третья попытка.

Ошибка почему-то проявляется только на одном из кластеров — на третьих репликах в ДЦ Владимир (мы любим называть дата-центры по именам городов). В феврале 2019 ошибка проявилась тоже в ДЦ Владимир, но на другой версии ClickHouse. Это еще один аргумент против гипотезы, что мы написали неправильный код. Мы его уже трижды переписали с февраля по май — ошибка наверное не в коде.

Все ошибки при чтении пакетов по сети — while receiving packet from. Пакет, на котором произошла ошибка, зависит от структуры запроса. Для запросов, которые отличаются по структуре, ошибка на разных чек-суммах. Но в запросах, на которых ошибка на одной и той же чек-сумме, отличаются константы.

Во всех запросах с ошибкой, кроме одного, есть GLOBAL JOIN. Но для сравнения, есть один необычно простой запрос, и размер сжатого блока для него всего 75 байт.

SELECT max(ReceiveTimestamp) FROM tracking_events_all 
WHERE APIKey = 1111 AND (OperatingSystem IN ('android', 'ios'))

Отклоняем гипотезу про влияние GLOBAL JOIN.

Самое интересное, что затронутые серверы сгруппированы в диапазоны по именам:
mtxxxlog01-{39..44 57..58 64 68..71 73..74 76}-3.

Я устал и отчаялся, стал искать совсем бредовые закономерности. Хорошо, что не дошел до отладки кода с помощью нумерологии. Но зацепки все же были.

  • Группы проблемных серверов совпадают с теми, что были в феврале.
  • Проблемные серверы стоят в определенных частях дата-центра. В ДЦ Владимир есть так называемые очереди — разные его части: VLA-02, VLA-03, VLA-04. Ошибки четко группируются: в одних очередях хорошо (VLA-02), в других проблемы (VLA-03, VLA-04).

Отладка методом «тыка»

Оставалось только отлаживать методом «тыка». Это значит формировать гипотезу «Что будет, если попробовать сделать так?» и собирать данные. Например, я нашел в таблице query_log простой запрос с ошибкой, для которого размер пакета size of compressed block очень маленький (= 107).

Отъявленные баги и как их избежать на примере ClickHouse - 5

Взял запрос, скопировал и выполнил вручную с помощью программы clickhouse-local.

strace -f -e trace=network -s 1000 -x 
clickhouse-local --query "
    SELECT uniqIf(DeviceIDHash, SessionType = 0)
    FROM remote('127.0.0.{2,3}', mobile.generic_events)
    WHERE StartDate = '2019-02-07' AND APIKey IN (616988,711663,507671,835591,262098,159700,635121,509222)
        AND EventType = 1 WITH TOTALS" --config config.xml

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

С помощью strace можно трассировать сам ClickHouse-сервер. Но этот сервер работает в продакшн, если я так сделаю — получу массив непонятной информации. Поэтому я запустил отдельную программу, которая выполняет ровно один запрос. Уже для этой программы выполняю strace и получаю то, что передавалось по сети.

Запрос выполняется без ошибок — ошибка не воспроизводится. Если бы воспроизводилась, проблема была бы решена. Поэтому я скопировал пакеты в текстовый файл и вручную стал разбирать протокол.

Отъявленные баги и как их избежать на примере ClickHouse - 6

Чек-сумма совпадала с той, которая ожидалась. Это именно тот пакет, на котором иногда, в другое время, в других запросах, происходили ошибки. Но пока ошибок не было.

Написал простую программу, которая берет пакет и проверяет чек-сумму при замене одного бита в каждом байте. Программа выполняла bit flip на каждой возможной позиции и считала чек-сумму.

Отъявленные баги и как их избежать на примере ClickHouse - 7

Запустил программу и обнаружил, что если поменять значение одного бита, то получится ровно та битая чек-сумма, на которую есть жалоба

Аппаратная проблема

При ошибке в ПО (например, проезде по памяти) single bit flip маловероятен. Поэтому появилась новая гипотеза — проблема в железе.

Можно было бы закрыть крышку ноутбука и сказать: «Проблема не на нашей стороне, а в железе, мы таким не занимаемся». Но нет, попробуем понять, где проблема: в оперативной памяти, на жестком диске, в процессоре, в сетевой карте или оперативной памяти сетевой карты в сетевом оборудовании.

Как локализовать аппаратную проблему?

  • Проблема возникала и исчезала в определённые даты.
  • Затронутые серверы сгруппированы по именам: mtxxxlog01-{39..44 57..58 64 68..71 73..74 76}-3.
  • Группы проблемных серверов совпадают с февральскими.
  • Проблемные серверы стоят только в определенных очередях дата-центра.

Возникли вопросы к сетевым инженерам — данные бьются на сетевых коммутаторах. Оказывается, сетевые инженеры меняли коммутаторы на другие ровно в те даты. После вопроса, они их заменили на предыдущие и проблема исчезла.

Проблема решена, но вопросы еще остались (уже не к инженерам).

Почему не помогает ECC (память с коррекцией ошибок) на сетевых коммутаторах? Потому что множественные bit flip могут друг друга компенсировать — получится необнаруженная ошибка.

Почему не помогают TCP чек-суммы? Они слабые. Если в данных изменился всего один бит, то TCP чек-суммы всегда увидят изменения. Если изменилось два бита, то изменения можно не обнаружить — они компенсируют друг друга.

В нашем пакете изменился только один бит, но ошибку не видно. Все потому, что в TCP-сегменте изменилось 2 бита: посчитали от него чек-сумму, она совпала. Но в один TCP-сегмент помещается больше одного пакета нашего приложения. И для одного из них мы уже считаем свою чек-сумму. В этом пакете изменился только один бит.

Почему не помогают Ethernet чек-суммы — они же сильнее, чем TCP? Ethernet чек-суммы чек-суммируют данные, чтобы они не побились при передаче через один сегмент (могу ошибаться с терминологией, я не сетевой инженер). Сетевое оборудование пересылает эти пакеты и при пересылке может изменить какие-то данные. Поэтому чек-суммы просто пересчитываются. Мы проверили — на проводе пакеты не изменились. Но если они побились на самом сетевом коммутаторе, он пересчитает чек-сумму (она будет другая), и перешлет пакет дальше.

Вас ничего не спасет — чек-суммируйте сами. Не надейтесь, что кто-то сделает это за вас.

Для блоков данных считается 128-битная чек-сумма (этот overkill на всякий случай). Мы корректно сообщаем пользователю об ошибке. Данные передаются по сети, повреждаются, но мы их никуда не записываем — все наши данные в порядке, можно не беспокоиться.

Данные, которые хранятся в ClickHouse, остаются целостными. Используйте чек-суммы в ClickHouse. Мы так любим чек-суммы, что считаем сразу три их варианта:

  • Для сжатых блоков данных при записи в файл, в сеть.
  • Общая чек-сумма сжатых данных для сверки при репликации.
  • Общая чек-сумма несжатых данных для сверки при репликации.

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

Не бойтесь считать чек-суммы, они не тормозят.

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

Улучшенное сообщение об ошибке

Как объяснить пользователю, когда он получает такое сообщение об ошибке, что это аппаратная проблема?

Отъявленные баги и как их избежать на примере ClickHouse - 8

При несовпадении чек-суммы, перед тем, как отправить исключение, я пытаюсь поменять каждый бит — на всякий случай. Если чек-сумма сойдется при изменении и изменен один бит, то проблема, скорее всего, аппаратная.

Если мы можем обнаружить эту ошибку, и если при изменении одного бита она исправится, почему бы не исправлять? Мы можем это делать, но если будем все время исправлять ошибки, то пользователь не будет знать, что в оборудовании проблемы.

Когда мы выяснили, что в коммутаторах проблемы, то люди из других отделов стали сообщать: «А у нас один бит неправильно записался в Mongo! А у нас в PostgreSQL что-то попало!» Это хорошо, но лучше раньше сообщать о проблемах.

Когда мы выпустили новый релиз с диагностикой, первый пользователь, у которого это сработало, написал через неделю: «Вот такое сообщение — в чем проблема?». К сожалению, он его не дочитал. Но я дочитал и предположил с 99% вероятностью, что если ошибка проявляется на одном сервере, то проблема с железом. Оставшийся процент оставляю на случай, если я неправильно написал код — такое бывает. В итоге пользователь заменил SSD, и проблема исчезла.

«Бред» в данных

Эта интересная и неожиданная проблема заставила поволноваться. У нас есть данные Яндекс.Метрики. В базу в один из столбцов записывается простой JSON — пользовательские параметры из JavaScript-кода счётчика.

Делаю какой-то запрос и ClickHouse-сервер упал с segfault. По stack trace я понял, в чем проблема, — свежий коммит от наших внешних контрибьюторов из другой страны. Коммит исправил, segfault исчез.

Запускаю тот же запрос: SELECT в ClickHouse, чтобы получить JSON, но опять ерунда, все медленно работает. Получаю JSON, а он 10 МБ. Вывожу на экран и смотрю внимательней: {"jserrs": cannot find property of object undefind..., а дальше вывалился мегабайт бинарного кода.

Отъявленные баги и как их избежать на примере ClickHouse - 9

Появились мысли, что это опять проезд по памяти или race condition. Множество таких бинарных данных — плохо, в них может быть, что угодно. Если так, то сейчас я найду там пароли и приватные ключи. Но ничего не нашел, поэтому сразу отклонил гипотезу. Может, это баг в моей программе в ClickHouse-сервере? Возможно, в программе, которая записывает (она тоже написана на C++) — вдруг она случайно складывает свой dump памяти в ClickHouse? В этом аду я стал смотреть внимательней на буковки и понял, что не всё так просто.

Путь к разгадке

Одинаковый мусор записался на два кластера, независимо друг от друга. Данные мусорные, но это валидный UTF-8. В этом UTF-8 какие-то непонятные url'ы, названия шрифтов, а еще много букв «я» подряд.

Что особенного в маленькой кириллической «я»? Нет, это не «Яндекс». Дело в том, что в кодировке Windows 1251 это 255-й символ. А у нас на серверах Linux, кодировку Windows 1251 никто не использует.

Получается, что это dump браузера: JavaScript-код счетчика метрики собирает ошибки JavaScript. Как оказалось, ответ простой — всё это пришло от пользователя.

Отсюда тоже можно сделать выводы.

Баги со всего интернета

Яндекс.Метрика собирает трафик с 1 млрд устройств в интернете: браузеры на ПК, сотовые телефоны, планшеты. Мусор будет поступать обязательно: в пользовательских устройствах бывают баги, везде ненадежная оперативная память и ужасное железо, которое перегревается.

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

Поэтому правильно просто фильтровать этот мусор перед записью в базу. Не надо писать в базу мусор — ей это не нравится.

Следующий HighLoad++ только осенью (зато все 133 видео в открытом доступе), но в мае нас ждет двухнедельный онлайн-фестиваль для тех, кто делает Интернет, РИТ++ и PHP Russia 2020 Online.

Благодаря поддержке компании Badoo, конференция PHP Russia 2020 Online стала бесплатной. PHP Russia 2020 Online пройдёт 13 мая, для участия необходимо зарегистрироваться.

Новости вроде этой и подборки докладов мы публикуем в рассылке — подпишитесь, чтобы быть в курсе обновлений.

Автор: Олег Бунин

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js