Рубрики
Без рубрики

Приключения в Traildb с миллионами рядов, Python и Go

Приключения в Traildb с миллионами рядов, Python и Go. Tagged с Traildb, Python, Go.

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

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

Что вы делаете с данными о событиях? Это может быть собрано партиями или в режиме реального времени, но в конце концов, что вы делаете со всей этой информацией, которая принадлежит пользователю?

Данные о событиях могут быть данными от датчиков, ваших собственных мобильных приложений, систем очереди, интерфейсов, аналитики, журналов доступа и многого другого.

Я бы определил «данные о событиях» как упорядоченную серию случаев, связанных с конкретным владельцем.

Есть много разных мест, где эти данные могут оказаться, например:

  • набор таблиц в реляционной базе данных (например, PostgreSQL или MySQL)
  • Потоковая платформа данных (например. Apache Kafka или Amazon Kinesis)
  • база данных временных рядов (например, Influxdb)

Для этой статьи я хотел бы изучить другой инструмент, который в некоторых конкретных случаях может быть правильным инструментом для работы: Traildb.

Что такое Traildb

Я собираюсь процитировать Источник здесь:

Traildb – это библиотека, внедренная в C, которая позволяет вам запросить серию событий на скорости.

Traildb был разработан Adroll для борьбы с количествами в масштабе Petabyte.

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

Как работает Traildb

Traildb имеет два основных режима: строительство и запросы.

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

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

Как создать Traildb

Для своих экспериментов я экспортировал таблицу с более чем 139 миллионами событий, разделенных в двух файлах CSV (не открывайте их в своем любимом редакторе, даже Sublime: D).

Первый файл занимает 29 ГБ и второй файл 30 ГБ.

Traildb имеет привязки для различных языков программирования: C, D, R, Haskell, Rust, Python и Go.

Язык, с которым я более знаком, среди всех, это Python, так что я начал. Даже если Traildb собственный Учебник Предупреждается от использования Python с огромным количеством данных.

Посмотрим, как это сделано:

COLUMNS = OrderedDict(zip(
    ['YOUR', 'LIST', 'OF', 'COLUMNS', 'IN', 'THE', 'CSV'],
     range(0, NUMBER_OF_COLUMNS)
))

def create_trail_db(input_path, output_path, columns):
    traildb = TrailDBConstructor(output_path, COLUMNS.keys())

    with open(input_path) as input_file:
        reader = csv.reader(input_file, encoding='utf-8', escapechar='\\')

        for line in reader:
            event_uuid = line[COLUMNS['event_uuid']]
            event_time = line[COLUMNS['event_time']]

            # I should probably use a regular expression here but I am lazy
            # and this is just an experiment
            try:
                time = datetime.strptime(event_time, '%Y-%m-%d %H:%M:%S.%f')
            except ValueError:
                time = datetime.strptime(event_time, '%Y-%m-%d %H:%M:%S')

            utf8_encoded_values = [value.encode('utf-8') for value in line]
            traildb.add(event_uuid, time, utf8_encoded_values)

    traildb.finalize()

По сути, вы открываете конструктор, итерацию в своем файле, проходите UUID и временную метку Traildb со всеми вашими данными, а затем вызовите завершение. Очень, очень просто. Как вы заметили, мне пришлось использовать Python 2, потому что Traildb не поддерживает Python 3.

Пара заметок и статистики:

  • Входной файл CSV составляет 29 ГБ на диске
  • Вывод .tdb Файл составляет 4,4 ГБ
  • На моем MacBook Pro с 16 ГБ оперативной памяти и SSD потребовалось 3 часа и 5 минут, чтобы произвести
  • В течение этих 3 часов я работал над чем -то другим, используя компьютер обычно

Так как я не хотел ждать еще 3 часа во второй половине набора данных, я решил переписать сценарий Python. Это была моя первая попытка написать код GO за пределами учебника, так что это не лучшая реализация, но библиотека Traildb также довольно проста в Go:

// I skipped all the error checking for brevity
func create_trail_db(input_path string, output_path string, columns []string) {
    traildb, err := tdb.NewTrailDBConstructor(output_path, columns...)
    input_file, err := os.Open(input_path)
    defer input_file.Close()

    dialect := csv.Dialect{
        Delimiter:   ',',
        Quoting:     csv.NoDoubleQuote,
        DoubleQuote: csv.NoDoubleQuote,
    }
    reader := csv.NewDialectReader(input_file, dialect)
    for {
        record, err := reader.Read()

        event_uuid := record[indexOf("event_uuid", columns)]
        event_time := record[indexOf("event_time", columns)]

        timestamp, err := strptime.Parse(event_time, "%Y-%m-%d %H:%M:%S.%f")
        if err != nil {
            timestamp, _ = strptime.Parse(event_time, "%Y-%m-%d %H:%M:%S")
        }

        traildb.Add(event_uuid, timestamp.Unix(), record)
    }

    traildb.Finalize()
    traildb.Close()
}

Потребовалось 1 час и 30 минут, чтобы разобрать второй файл, который содержит 493 тысячи строк, чем предыдущий файл, проанализированный Python.

Несколько примечаний по всему процессу создания:

  • Сжатие работает отлично, 8,8 ГБ всего от 59 ГБ из сырого исходного материала
  • Go потребовалось половину времени, чтобы сделать больше вещей
  • Я вполне уверен, что много времени тратится на то, чтобы анализировать временные метки с линий в CSV, так что это может быть быстрее с меткой времени уже в секундах

Как запросить Traildb

Итак, теперь у нас есть два файла, скажем A.TDB и B.TDB Каждый занимает примерно 4,4 ГБ, сидя там в папке, готовой к анализу. Как мы это делаем?

Оказывается, есть много способов, от «пожалуйста, не смотрите на этот тупой код» до тех, кто может масштабироваться в архитектуре разрыва и завоевания.

Условия запроса

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

SELECT *
FROM ab_tests
WHERE ab_test_id = '1234'
AND action_type IN ('clicked_button_red', 'clicked_button_green')

Это может быть что -то еще, это всего лишь псевдо -пример.

Установка тупой эталонной точки

Поскольку мне понадобился нереалистичный эталонный пункт, чтобы просто сказать «вау», когда на самом деле использовал Traildb, я решил написать тупой сценарий в Python и перейти к Itater за все десятки миллионов строк в файле и обнаружить результаты вручную. Посмотреть на себя:

tdb = TrailDB(tdb_filepath)

...

def action_data(tdb, action_types, action_types):
    for uuid, trail in tdb.trails():
        for event in trail:
            if event.ab_test_id in ab_test_ids and event.action_type in action_types:
                yield event

count = 0
for event in action_data(tdb, ab_test_ids, action_types):
    print event
    count += 1
db, err := tdb.Open(tdb_filepath)

func action_data(db *tdb.TrailDB, ab_test_ids map[string]bool, action_types map[string]bool) int {
    trail, err := tdb.NewCursor(db)

    count := 0
    for i := uint64(0); i < db.NumTrails; i++ {
        err := tdb.GetTrail(trail, i)

        for {
            evt := trail.NextEvent()
            evtMap := evt.ToMap()
            if (ab_test_ids[evtMap["ab_test_id"]]) && (action_types[evtMap["action_type"]]) {
                evt.Print()
                count += 1
            }
        }
    }

    return count
}

Вот время:

$ time python query_db_naive.py a.db
4105.14s user 49.11s system 93% cpu 1:14:19.35 total

$ time go run query_db_naive.go a.db
538.53s user 8.66s system 100% cpu 9:05.35 total

Python занял 1 час и 14 минут и занял 9 минут и 5 секунд. Они оба нашли те же события 216, которые я искал.

Я пропустил Python для второго файла, потому что я не хотел умирать в ожидании. Go потребовалось менее 12 минут, чтобы найти 220 событий во втором файле (что я напоминаю вам больше, чем первое).

Запрос с помощью API фильтра Traildb

Traildb позволяет разработчику создавать Фильтры для запросов.

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

Я переписал два сценария, используя фильтры:

query = [
    [('ab_test_id', value) for value in ab_test_ids],
    [('action_type', value) for value in action_types]
]
count = 0
for uuid, trail in tdb.trails(event_filter=query):
    for event in trail:
        print event
        count += 1

tdb.trails () принимает список фильтров Python и возвращает только соответствующие ряды. Каждый фильтр является списком, все списки в запросе находятся в И друг с другом, и каждый элемент в одном списке находится в Или с другими предметами.

Давайте посмотрим на то же самое в Go:

query := [][]tdb.FilterTerm{
  {
    tdb.FilterTerm{Field: "ab_test_id", Value: "2767"},
  },
  {
    tdb.FilterTerm{Field: "action_type", Value: "clicked_button_red"},
    tdb.FilterTerm{Field: "action_type", Value: "clicked_button_green"},
  },
}
filter := db.NewEventFilter(query)
db.SetFilter(filter)

Вот результаты для первого файла:

$ time python query_db_filter.py a.tdb
14.36s user 0.63s system 97% cpu 15.362 total

$ time go run query_db_filter.go a.tdb
5.75s user 0.53s system 97% cpu 6.459 total

# precompiled go binary
$ time ./query_db_filter a.tdb
5.42s user 0.39s system 97% cpu 5.945 total

А также Результаты для второго файла:

$ time python query_db_filter.py b.tdb
14.13s user 0.61s system 96% cpu 15.257 total

$ time go run query_db_filter.go b.tdb
5.82s user 0.79s system 92% cpu 7.194 total

# precompiled go binary
$ time ./query_db_filter b.tdb
5.60s user 0.43s system 97% cpu 6.192 total

Мы уже знаем, что наша базовая линия была бессмысленной, но мы все равно можем сделать несколько выводов:

  • Пример GO в 2,6 раза быстрее, чем Python
  • Фильтры Traildb действительно быстрые, хотя для создания сложных условий может потребоваться небольшая подготовка
  • Все может быть еще быстрее расщеплять тропы, искать предметы параллельно, а затем присоединиться к результатам. Как в алгоритме «карта уменьшить».

Запрос инструмента командной строки Traildb

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

Так что, естественно, я хотел посмотреть, как быстро это было с запросом:

$ time tdb dump --filter='ab_test_id=2767 & action_type=clicked_button_red action_type=clicked_button_green' -i a.tdb
6.47s user 0.45s system 96% cpu 7.176 total

$ time tdb dump --filter='ab_test_id=2767 & action_type=clicked_button_red action_type=clicked_button_green' -i b.tdb
5.66s user 0.46s system 96% cpu 6.359 total

Скорость определенно находится на норме с нашей справочной реализацией GO.

Запрос инструмента командной строки Traildb и подготовленным индексом

Инструмент командной строки имеет аккуратную функцию, которая позволяет предварительно создавать индекс, соответствующий нашими фильтрами. Индекс сохраняется на диске рядом с файлом TrailDB.

Сначала мы создаем индекс:

$ time tdb index --filter='ab_test_id=2767 & action_type=clicked_button_red action_type=clicked_button_green' -i a.tdb
Index created successfully at a.tdb.index in 286 seconds.
346.05s user 12.63s system 125% cpu 4:45.99 total

$ du a.tdb.index
645M  a.tdb.index

Затем мы снова запускаем один и тот же запрос в предыдущем абзаце:

$ time tdb dump --filter='ab_test_id=2767 & action_type=clicked_button_red action_type=clicked_button_green' -i a.tdb
0.06s user 0.01s system 39% cpu 0.185 total

Как видите, это довольно быстрее. Мы прошли с 6,47 до 0,06.

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

Как объединить несколько Traildbs

Теперь мы знаем, как создать Traildb, как извлечь из него данные, но мы до сих пор не знаем, как взять два файла Traildb и объединить их в один.

Я создал еще одну пару сценариев для слияния:

output_tdb = TrailDBConstructor(output_path, columns)

for tdb_filepath in tdb_filepaths:
    tdb = TrailDB(tdb_filepath)
    output_tdb.append(tdb)

output_tdb, err := tdb.NewTrailDBConstructor(output_path, columns...)

for i := 0; i < len(tdb_filepaths); i++ {
  tdb_filepath := tdb_filepaths[i]

  db, err := tdb.Open(tdb_filepath)

  err = output_tdb.Append(db)

  db.Close()
}

output_tdb.Finalize()
output_tdb.Close()

Тогда я запустил их:

$ time python merge_dbs.py final.tdb a.tdb b.tdb
1044.97s user 1260.73s system 27% cpu 2:22:08.29 total

$ time go run merge_dbs.go final.tdb a.tdb b.tdb
960.93s user 1149.51s system 22% cpu 2:33:54.73 total

Давайте проверим, что события все там:

>>> from traildb import TrailDB
>>> tdb = TrailDB('final.tdb')
>>> tdb.num_events
139017085L

Примечание: GO могло бы быть на 11 минут медленнее, чем Python из -за того, что я загружал гигабайты на ведре S3 в то же время, а также началось резервная копия Time Machine … Эти тесты определенно не научные: D

Merge – это медленная операция, я предположил, что библиотека должна распаковать данные для каждого Traildb и реконструировать их в последнем.

Операция была очень интенсивной на моем ноутбуке с 16 ГБ оперативной памяти. Процесс обменивался как сумасшедший, но опять же, я не запускал эти сценарии на специальной машине.

Merge может стоить только для объединения небольших файлов.

Запрос 139 миллионов мероприятий

Теперь, когда мы создали «окончательный» файл, с 139 миллионами событий, мне было любопытно посмотреть, как быстро были Python и Go:

$ time python query_db_filter.py final.tdb
29.56s user 2.65s system 80% cpu 40.223 total

$ time go run query_db_filter.go final.tdb
11.63s user 1.02s system 95% cpu 13.303 total

Бонусный контент: Как запросить файлы traildb на Amazon S3

Traildb имеет Экспериментальная функция , в отдельной версии, которая может использовать Linux userfaultfd () Syscall, чтобы воспользоваться ошибками страниц для извлечения блоков данных из сети (файл удаленного S3), сопоставьте блоки в локальной памяти и кэшируйте их на диске.

Он предлагает серверный процесс, который переводит ошибки страниц в сетевые вызовы, чтобы извлечь части файла на S3. Я не знал, что это можно сделать, и моя челюсть упала, когда я прочитал статью.

Как это круто? Я должен был выяснить, сработало ли это (и теперь вы знаете, почему я загружаю вещи на S3, проводя предыдущие эксперименты).

Я настраиваю машину EC2 (T1.MICRO + 8 ГБ SSD GP2 Объем), установил на нем TRAILDB и начал играть.

Моя конфигурация для эксперимента была:

  • 16 МБ размера блока (объем данных из файла, который процесс получает в сети)
  • 1 ГБ максимального занятия на диске (максимальный размер локального кеша)
time tdb dump --filter='ab_test_id=2767 & action_type=clicked_button_red action_type=clicked_button_green' -i s3://something/a.tdb
real    2m27.941s
user    0m6.348s
sys     0m3.400s

По очевидным причинам медленнее, чем локальная версия, но посмотрите, что происходит, когда у вас есть некоторые локально кэшированные данные:

time tdb dump --filter='ab_test_id=2767 & action_type=clicked_button_red action_type=clicked_button_green' -i s3://something/a.tdb
real    0m52.742s
user    0m6.496s
sys     0m3.680s

Неплохо, а?

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

Бонусный контент: запрос Traildb с катушкой, экспериментальный язык

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

Он работает на файлах Traildb без каких -либо изменений для них. Это просто еще один способ извлечения информации.

Запрос катушки выглядит так:

var Events uint

if $ab_test_id $ab_test_id='2767':
   if $action_type $action_type='clicked_button_red' or if $action_type $action_type='clicked_button_green':
      inc Events 1

Это результат с 2 и 4 потоками:

$ time ./reel query.rl -P -T 2 a.tdb
[thread 0] 0% trails evaluated
[thread 0] 44% trails evaluated
[thread 0] 88% trails evaluated
[thread 0] 100% trails evaluated
Events
216

7.39s user 0.42s system 178% cpu 4.371 total

$ time ./reel query.rl -P -T 4 a.tdb
[thread 0] 0% trails evaluated
[thread 0] 44% trails evaluated
[thread 0] 88% trails evaluated
[thread 0] 100% trails evaluated
Events
216

9.98s user 0.54s system 295% cpu 3.560 total

Итак, поскольку это более или менее быстро, как наши сценарии Go и Python, зачем изучать другой инструмент? Потому что катушка может быть использована для обработки время прохождения между разными событиями , Он имеет поток управления и он может группировать результаты Анкет

Есть еще один неэкспериментальный инструмент, грузовая машина , который является полным двигателем подсказки с государственной машиной Но я не мог заставить его работать на OSX, поэтому я удобно оставлю тест в качестве экземпляра читателю 😉

Соображения и выводы

Traildb действительно быстрый, если он используется правильно, и я ценю завершенные файлы, которые будут считываются только как плюс. Требуется небольшая работа, чтобы стать частью рабочего процесса данных компании Но я думаю, что это инструмент, который стоит изучить, и он может иметь значение в содержании затрат с помощью анализа массовых количественных данных, которые все компании начинают иметь из -за «больших данных» и того факта, что в основном никто больше ничего не удаляет.

Я определенно рекомендую использовать Go Over Python для работы с Traildb.

Как я уже сказал, в начале Traildb был создан Adroll, который использует его для обработки количества данных в масштабе Petabyte. Я думаю, что это инструмент, который стоит знать (или, по крайней мере, зная, что он там), хотя, как и в настоящий момент, я нахожу его немного неполированным. Кроме того, сообщество кажется довольно небольшим, и если я не ошибаюсь, разработчики в Adroll поддерживают проект с открытым исходным кодом, что может привести к тому, что проект будет обучаться бизнес -требованиям и графику их компании (последний выпуск – 0,6, и он был опубликован в мае 2017).

В любом случае, если вы заинтересованы в том, чтобы узнать больше, вот пара ссылок:

Оригинал: “https://dev.to/rhymes/adventures-in-traildb-with-millions-of-rows-python-and-go”