Восстанавливаем данные из 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]:
github.com/cockroachdb/cockroach/pkg/util/stop.(*Stopper).Recover(0xc4206c8500, 0x7fb299f4b180, 0xc4209de120)
/go/src/github.com/cockroachdb/cockroach/pkg/util/stop/stopper.go:200 +0xb1
panic(0x1957a00, 0xc4240398a0)
/usr/local/go/src/runtime/panic.go:489 +0x2cf
github.com/cockroachdb/cockroach/pkg/storage.(*Store).processReady(0xc420223000, 0x103)
/go/src/github.com/cockroachdb/cockroach/pkg/storage/store.go:3411 +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, конечно же. Сначала я решил попробовать взять библиотеку github.com/tecbot/gorocksdb, и она даже завелась, но выдавала ошибку, что ей неизвестен компаратор cockroach_comparator
. Я взял нужный компаратор из исходников самого cockroach, но ничего не поменялось.
Поскольку мне было лень разбираться, в чём дело, я решил пойти другим путем и просто взял и заюзал сразу готовый пакет прямо из исходников самого cockroachdb: в пакете github.com/cockroachdb/cockroach/pkg/storage/engine есть всё, что нужно для того, чтобы правильно работать с KV-базой.
Поэтому мы откроем базу и начнем итерироваться и попробуем поискать имена ключей, в значении которых есть какие-то строчки, которые мы точно знаем, что есть в базе:
package main
import "github.com/cockroachdb/cockroach/pkg/storage/engine"
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. Дальше идет булево поле. Пришлось немного повозиться, но я смог выяснить, что можно использовать готовую функцию из пакета github.com/cockroachdb/cockroach/pkg/util/encoding
под названием 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: www.cockroachlabs.com
Моя потешная соцсеть: vbambuke.ru
Исходники моей утилиты для восстановления данных: github.com/YuriyNasretdinov/social-net/blob/master/restore-test/main.go
Процесс на youtube (2 из 3 видео): www.youtube.com/watch? v=ROcXSJHWcI4