...

среда, 3 января 2018 г.

Восстанавливаем данные из CockroachDB

Восстановить данные из cockroachdb легко — просто накатите всё из бекапа. Как это не делали бэкапы? Для базы, у которой версия 1.0 вышла всего полгода назад? Что ж, не отчаивайтесь, скорее всего данные можно восстановить. Я буду рассказывать про то, как я восстанавливал базу данных для своего проекта потешной социальной сети вбамбуке и стримил сей процесс на ютьюбе.

Как будем восстанавливать


Для начала нужно разобраться с тем, что произошло, почему упал CockroachDB? Причины бывают разные, но в любом случае сервер больше не стартует или не отвечает на запросы. В моём случае, после недолгого гугления, оказалась побита rocksdb база:
E171219 15:50:36.541517 25 util/log/crash_reporting.go:82  a panic has occurred!
E171219 15:50:36.734485 74 util/log/crash_reporting.go:82  a panic has occurred!
E171219 15:50:37.241298 25 util/log/crash_reporting.go:174  Reported as error 20a3dd770da3404fa573411e2b2ffe09
panic: Corruption: block checksum mismatch [recovered]
        panic: Corruption: block checksum mismatch

goroutine 25 [running]:
http://ift.tt/2Cs9Cir(0xc4206c8500, 0x7fb299f4b180, 0xc4209de120)
        /go/src/http://ift.tt/2lHxC6x +0xb1
panic(0x1957a00, 0xc4240398a0)
        /usr/local/go/src/runtime/panic.go:489 +0x2cf
http://ift.tt/2CxeGSG(0xc420223000, 0x103)
        /go/src/http://ift.tt/2lHxCDz +0x427

Восстанавливаем RocksDB хранилище


Если у вас побилась rocksdb база, то для её восстановления в cockroach версии 1.1 уже встроена нужная команда:
$ cockroach debug rocksdb repair

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

После восстановления можете попробовать запустить cockroachdb заново. В моём случае это не помогло, но ошибка стала другая:

E171219 13:12:47.618517 1 cli/error.go:68  cockroach server exited with error: cannot verify empty engine for bootstrap: unable to read store ident: store has not been bootstrapped
Error: cockroach server exited with error: cannot verify empty engine for bootstrap: unable to read store ident: store has not been bootstrapped

Очевидно, что-то побилось где-то в настройках, и я не разбираюсь в формате хранения cockroachdb достаточно хорошо, чтобы понять, что все-таки ему не хватает. Поэтому пойдем другим путем: мы знаем, что внутри это Key-Value хранилище и даже примерно знаем, что нам нужно искать, поскольку разработчики рассказывали (тут и тут) об этом в своем блоге.

«Выдираем» данные прямо из RocksDB


Поскольку формат ключей и записей нам примерно известен, можно взять директорию с данными от «сломанного» инстанса и попробовать пройтись по всем ключам и вытащить данные напрямую оттуда. Я буду рассказывать про вариант с одним хостом, но если хостов много и умер весь кластер, то вам нужно будет ещё научиться удалять дубли и определять самые свежие версии ключей.

Писать всё будем на go, конечно же. Сначала я решил попробовать взять библиотеку http://ift.tt/1FcO9mN, и она даже завелась, но выдавала ошибку, что ей неизвестен компаратор cockroach_comparator. Я взял нужный компаратор из исходников самого cockroach, но ничего не поменялось.

Поскольку мне было лень разбираться, в чём дело, я решил пойти другим путем и просто взял и заюзал сразу готовый пакет прямо из исходников самого cockroachdb: в пакете http://ift.tt/2CHGUI7 есть всё, что нужно для того, чтобы правильно работать с KV-базой.

Поэтому мы откроем базу и начнем итерироваться и попробуем поискать имена ключей, в значении которых есть какие-то строчки, которые мы точно знаем, что есть в базе:

package main

import "http://ift.tt/2CHGUI7"

func main() {
        db, err := engine.NewRocksDB(engine.RocksDBConfig{
                Dir:       "/Users/yuriy/tmp/vbambuke",
                MustExist: true,
        }, engine.NewRocksDBCache(1000000))
        if err != nil {
                log.Fatalf("Could not open cockroach rocksdb: %v", err.Error())
        }

        db.Iterate(
                engine.MVCCKey{Timestamp: hlc.MinTimestamp},
                engine.MVCCKeyMax,
                func(kv engine.MVCCKeyValue) (bool, error) {
                        if bytes.Contains([]byte(kv.Value), []byte("safari@apple.com")) {
                                log.Printf("Email key: %s", kv.Key)
                        }
                        return false, nil
                },
        )
}

Мне вывелось примерно такое:

Email key: /Table/54/1/158473728194052097/0/1503250869.243064075,0

У этого ключа довольно много компонентов, но вот, что мне удалось выяснить:

0. Table означает «таблица» :)
1. Номер таблицы (таблицы должны идти в порядке создания)
2. Тип ключа. 1 означает обычную запись, 2 означает индекс
3. Значение первичного ключа (1,2,3, ...)
4. не знаю, видимо версия?
5. timestamp

То есть, можно вывести все ключи и их значения для всех таблиц в отдельный файл и разбить их по номеру таблицы. После этого должно стать более-менее понятно, как таблицы называются (и какая у них структура, ведь она-то у вас сохранилась :)?).

Разбираем формат записей


Я восстанавливал данные из cockroachdb версии 1.0.4, поэтому для более поздних версий детали могут отличаться. Но вот, что мне удалось понять:

1. Первые 6 байт в значении можно игнорировать. По всей видимости, это контрольная сумма данных и ещё какая-то мета-информация, например биты про nullable поля
2. Дальше идут сами данные, и перед каждой колонкой, кроме первой, идет отдельный байт с её типом

Пример из таблицы messages (я использовал od для того, чтобы получить читаемый вид бинарных данных):

Структура таблицы messages была такая:

CREATE TABLE messages (
  id SERIAL PRIMARY KEY,
  user_id BIGINT,
  user_id_to BIGINT,
  is_out BOOL,
  message TEXT,
  ts BIGINT
);

$ head -n 2 messages | od -c
0000000    1   /   1   /   0   /   1   5   0   3   2   5   0   8   6   8
0000020    .   7   2   7   5   5   4   8   2   8   ,   0                
0000040    =                     241   E 270 276  \n   # 202 200 230 316
0000060  316   ˁ  ** 263 004 023 202 200 204 231 374 235 222 264 004 032
0000100  026 031   N   o   w       I       u   s   e       r   e   a   l
0000120        P   o   s   t   g   r   e   S   Q   L 023 230 277 256 217
0000140  240 320 375 342   (  \n                                        
0000146

Давайте разберем эти данные по порядку:

1. сначала в файле я записал имя ключа — во фрагменте

0000000    1   /   1   /   0   /   1   5   0   3   2   5   0   8   6   8
0000020    .   7   2   7   5   5   4   8   2   8   ,   0                
0000040    =                     

это всё кусок ключа, из которого нам нужно взять значение первичного ключа (формат ключей описан выше)

2. Заголовок. На строке 0000040 после ключа находится 6-байтовый заголовок:

241   E 270 276  \n   #
он всегда разный, но для всех моих таблиц первые 6 байт нужно было просто пропустить.

3. Первое поле, user_id. Числа, которые мне встречались в cockroachdb, всегда были закодированы varint из стандартной библиотеки. Первую колонку можно прочитать с помощью binary.Varint. Мы должны будем прочитать следующий кусок:

0000040    =                     241   E 270 276  \n   #        отсюда   --->    202 200 230 316
0000060  316   ˁ  ** 263 004  <----     досюда      023 202 200 204 231 374 235 222 264 004 032


4. Второе поле, user_id_to. Оказалось, что в начале поля стоит его тип и 023 означает число и точно также читается, как varint. Можно написать соответствующие функции для чтения таких колонок из байтового массива:
func readVarIntFirst(v []byte) ([]byte, int64) {
        res, ln := binary.Varint(v)
        if ln <= 0 {
                panic("could not read varint")
        }
        return v[ln:], res
}

func readVarInt(v []byte) ([]byte, int64) {
        if v[0] != '\023' {
                panic("invalid varint prefix")
        }
        return readVarIntFirst(v[1:])
}


5. Дальше идет булево поле. Пришлось немного повозиться, но я смог выяснить, что можно использовать готовую функцию из пакета http://ift.tt/2CFv4hs под названием encoding.DecodeBoolValue Эта функция работает примерно также, как и объявленные выше, только возвращает ошибку вместо паники. Мы используем panic для удобства — нам в одноразовой утилите ошибки шибко по-умному обрабатывать не надо.
6. Дальше идет текст сообщения. Перед текстовыми полями идет байт 026, потом длина и потом содержимое. Выглядит это примерно так:
0000100  026 031   N   o   w       I       u   s   e       r   e   a   l
0000120        P   o   s   t   g   r   e   S   Q   L 023 230 277 256 217
0000140  240 320 375 342   (  \n                                        


Можно было бы подумать, что первый байт это длина, и дальше идет сам текст. Если значения небольшие (условно до 100 байт), то это даже работает. Но на самом деле длина закодирована ещё одним способом, и длину можно тоже прочесть с помощью функций из пакета encoding:
func readStringFirst(v []byte) ([]byte, string) {
        v, _, ln, err := encoding.DecodeNonsortingUvarint(v)
        if err != nil {
                panic("could not decode string length")
        }

        return v[ln:], string(v[0:ln])
}

func readString(v []byte) ([]byte, string) {
        if v[0] != '\026' {
                panic("invalid string prefix")
        }
        return readStringFirst(v[1:])
}


7. Ну и заключительное обычное число, читаем с помощью нашей функции readVarInt.

Чтение колонки типа DATE


С колонкой типа DATE я помучался, потому что в пакете encoding сходу не нашлось нужной функции :). Пришлось импровизировать. Не буду вас долго мучать, формат DATE представляет из себя обычное число (тип колонки 023 намекает), и в нём записано… Количество секунд в формате UNIX TIME, поделенное на 86400 (число секунд в сутках). То есть, чтобы прочитать дату, нужно умножить прочитанное число на 86400 и трактовать это как unix time:
v, birthdate := readVarInt(v)
ts := time.Unix(birthdate*86400, 0)
formatted := fmt.Sprintf("%04d-%02d-%02d", ts.Year(), ts.Month(), ts.Day())

Вставка обратно в базу


Чтобы вставить данные обратно в базу, я лично написал простенькую функцию для экранирования строк:
func escape(q string) string {
        var b bytes.Buffer
        for _, c := range q {
                b.WriteRune(c)

                if c == '\'' {
                        b.WriteRune(c)
                }
        }

        return b.String()
}

И использовал её для составления SQL-запросов вручную:

fmt.Printf(
        "INSERT INTO messages2(id, user_id, user_id_to, is_out, message, ts) VALUES(%s, %d, %d, %v, '%s', %d);\n",
        pk, userID, userIDTo, isOut, escape(message), ts,
)

Но вы можете составить CSV, использовать свою модель для базы, использовать подготовленные выражения, и т.д. — как вам угодно. Это не составляет труда после того, как вы распарсили бинарный формат хранения данных в CockroachDB :).

Ссылки, выводы


Спасибо за то, что доскроллили до конца :). Лучше делайте бэкапы, и не поступайте, как я. Но если вдруг вам очень нужно будет вытащить данные из CockroachDB, то эта статья должна будет вам немного помочь. Не теряйте данные!

CockroachDB: http://ift.tt/1dOVkbO
Моя потешная соцсеть: vbambuke.ru
Исходники моей утилиты для восстановления данных: http://ift.tt/2CdduR5
Процесс на youtube (2 из 3 видео): http://www.youtube.com/watch?v=ROcXSJHWcI4

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий