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

mwcmtxbmix_ffcoh3-92ephiok8.jpeg Восстановить данные из 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

© Habrahabr.ru