Slide 1

Slide 1 text

Разбор и сравнение данных в большом XML на маленькой VDS Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань

Slide 2

Slide 2 text

2 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Откиньтесь на спинку кресла • Эта презентация сделана с помощью L A TEX1 • Избежать большого количества кода не удалось • Материал основан на работающем сервисе3 • Значительная часть кода написана сообществом

Slide 3

Slide 3 text

3 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Задача Что надо было сделать • Разобрать XML-файл в 160Mb и положить в базу • Обновлять базу по новым XML-файлам • Предоставить интерфейс для доступа к базе

Slide 4

Slide 4 text

3 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Задача Что надо было сделать • Разобрать XML-файл в 160Mb и положить в базу • Обновлять базу по новым XML-файлам • Предоставить интерфейс для доступа к базе Условия • Скорость разбора — единицы минут • Недорогой виртуальный сервер • Приоритет стандартных решений

Slide 5

Slide 5 text

4 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Интересная задача • Никаких инноваций и ”rocket science” • Навязанные условия • Чувствительность к работе памяти • Чувствительность к ресурсам • Хрестоматийные решения

Slide 6

Slide 6 text

5 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Гадание • Не всегда очевидны «тонкие» места • Абстракции — «тихие омуты»

Slide 7

Slide 7 text

5 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Гадание • Не всегда очевидны «тонкие» места • Абстракции — «тихие омуты» • Тесты, бенчмарки, профилирование

Slide 8

Slide 8 text

6 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Архитектура • База данных • Индексы по значениям для поиска • Функционал наполнения базы из исходных данных • Функционал обновления данных • Функционал поиска данных

Slide 9

Slide 9 text

7 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат исходных данных. XML • 250 тысяч элементов content • Размер каждого от сотни байт до 6MB • Общий размер данных свыше 150MB ... ... ...

Slide 10

Slide 10 text

7 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат исходных данных. XML • 250 тысяч элементов content • Размер каждого от сотни байт до 6MB • Общий размер данных свыше 150MB • XML в кодировке CP1251 ... ... ...

Slide 11

Slide 11 text

8 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Элемент Content ... 10.0.0.1 ... fc00::beef ... 10.1.0.0/16 ... fd00::/48 ... • IP-адреса могут быть поштучно тысячами

Slide 12

Slide 12 text

9 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Потоковый разбор XML import ”encoding/xml” ... decoder := xml.NewDecoder(dumpFile) decoder.CharsetReader = charset.NewReaderLabel for { t, err := decoder.Token() ... switch _e := t.(type) { case xml.StartElement: switch _e.Name.Local { case ”content”: ...

Slide 13

Slide 13 text

9 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Потоковый разбор XML • Создаем декодер import ”encoding/xml” ... decoder := xml.NewDecoder(dumpFile) decoder.CharsetReader = charset.NewReaderLabel for { t, err := decoder.Token() ... switch _e := t.(type) { case xml.StartElement: switch _e.Name.Local { case ”content”: ...

Slide 14

Slide 14 text

9 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Потоковый разбор XML • Создаем декодер • Не забываем про кодировку import ”encoding/xml” ... decoder := xml.NewDecoder(dumpFile) decoder.CharsetReader = charset.NewReaderLabel for { t, err := decoder.Token() ... switch _e := t.(type) { case xml.StartElement: switch _e.Name.Local { case ”content”: ...

Slide 15

Slide 15 text

9 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Потоковый разбор XML • Создаем декодер • Не забываем про кодировку • Ловим каждый элемент import ”encoding/xml” ... decoder := xml.NewDecoder(dumpFile) decoder.CharsetReader = charset.NewReaderLabel for { t, err := decoder.Token() ... switch _e := t.(type) { case xml.StartElement: switch _e.Name.Local { case ”content”: ...

Slide 16

Slide 16 text

10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Выбор формы хранения данных • Время разбора XML — 10-200 секунд

Slide 17

Slide 17 text

10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Выбор формы хранения данных • Время разбора XML — 10-200 секунд • База нужна только для хранения

Slide 18

Slide 18 text

10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Выбор формы хранения данных • Время разбора XML — 10-200 секунд • База нужна только для хранения • bbolt — заполнение час+

Slide 19

Slide 19 text

10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Выбор формы хранения данных • Время разбора XML — 10-200 секунд • База нужна только для хранения • bbolt — заполнение час+ • map + скорость разбора = победа

Slide 20

Slide 20 text

11 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат исходных данных. XML • 250 тысяч элементов content • Размер каждого от сотни байт до 6MB • Общий размер данных свыше 150MB ... ... ...

Slide 21

Slide 21 text

11 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат исходных данных. XML • 250 тысяч элементов content • Размер каждого от сотни байт до 6MB • Общий размер данных свыше 150MB • XML в кодировке CP1251 ... ... ...

Slide 22

Slide 22 text

12 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат хранения данных • Каждый content имеет уникальный числовой id • id очень разреженные

Slide 23

Slide 23 text

12 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат хранения данных • Каждый content имеет уникальный числовой id • id очень разреженные • Решение: map[int]*TContent TContent — структура для DecodeElement() case ”content”: err := decoder.DecodeElement(&v, &_e)

Slide 24

Slide 24 text

13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Грабли • Уменьшаем аллокации

Slide 25

Slide 25 text

13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Грабли • Уменьшаем аллокации • Улучшательство: map[int]TContent

Slide 26

Slide 26 text

13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Грабли • Уменьшаем аллокации • Улучшательство: map[int]TContent • Боль

Slide 27

Slide 27 text

13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Грабли • Уменьшаем аллокации • Улучшательство: map[int]TContent • Боль программы ощущалась физически... • Расширение карты — очень дорогая операция • Всё-таки map[int]*TContent

Slide 28

Slide 28 text

14 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Оптимизация • Часть элементов имеет аттрибут ts: ts=”2020-02-06T19:54:00+03:00” • IPv4-адреса представлены элементом IP: 10.0.0.1

Slide 29

Slide 29 text

14 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Оптимизация • Часть элементов имеет аттрибут ts: ts=”2020-02-06T19:54:00+03:00” • IPv4-адреса представлены элементом IP: 10.0.0.1 • Две строки против двух целых чисел • Миллионы структур c IPv4-адресами

Slide 30

Slide 30 text

15 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Оптимизация конвертации данных • Преобразование стандартными средствами ip := net.ParseIP(s) intIp =: binary.BigEndian.Uint32(ip[12:16])

Slide 31

Slide 31 text

15 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Оптимизация конвертации данных • Преобразование стандартными средствами ip := net.ParseIP(s) intIp =: binary.BigEndian.Uint32(ip[12:16]) • Пишем менее универсально, но без аллокаций

Slide 32

Slide 32 text

15 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Оптимизация конвертации данных • Преобразование стандартными средствами ip := net.ParseIP(s) intIp =: binary.BigEndian.Uint32(ip[12:16]) • Пишем менее универсально, но без аллокаций • Сравниваем Benchmark_Standart-4 4295910 268 ns/op 48 B/op 3 allocs/op Benchmark_Custom-4 18941194 60.8 ns/op 0 B/op 0 allocs/op

Slide 33

Slide 33 text

16 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Более детальное преобразование • Наполняем TContent «вручную» case ”content”: err := decoder.DecodeElement(&v, &_e) % err := UnmarshalContent(tempBuf, &v) ... func UnmarshalContent(b []byte, v *TContent) error { buf := bytes.NewReader(b) decoder := xml.NewDecoder(buf) ... case elementIp: ip := TXMLIp{} if err := decoder.DecodeElement(&ip, &element); err != nil {

Slide 34

Slide 34 text

16 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Более детальное преобразование • Наполняем TContent «вручную» • Значительное уменьшение потребления памяти case ”content”: % err := decoder.DecodeElement(&v, &_e) err := UnmarshalContent(tempBuf, &v) ... func UnmarshalContent(b []byte, v *TContent) error { buf := bytes.NewReader(b) decoder := xml.NewDecoder(buf) ... case elementIp: ip := TXMLIp{} if err := decoder.DecodeElement(&ip, &element); err != nil {

Slide 35

Slide 35 text

17 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Индексы • Самая простая и скучная часть • map нужных данных • Значения — массивы id элементов content

Slide 36

Slide 36 text

18 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Обновление данных • Упрощаем до сравнения content целиком

Slide 37

Slide 37 text

18 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Обновление данных • Упрощаем до сравнения content целиком • Считаем контрольные суммы

Slide 38

Slide 38 text

19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Контрольные суммы • Считал SHA256

Slide 39

Slide 39 text

19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Контрольные суммы • Считал SHA256 • Профилирование показало, что выбор неудачный

Slide 40

Slide 40 text

19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Контрольные суммы • Считал SHA256 • Профилирование показало, что выбор неудачный • Выбрал на 64-bit FNV-1

Slide 41

Slide 41 text

19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Контрольные суммы • Считал SHA256 • Профилирование показало, что выбор неудачный • Выбрал на 64-bit FNV-1 • Проверил, заглянув «под капот»

Slide 42

Slide 42 text

20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Как считаем контрольные суммы • Преобразование TContent обратно в XML

Slide 43

Slide 43 text

20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Как считаем контрольные суммы • Преобразование TContent обратно в XML • Требует двойного преобразования каждого элемента

Slide 44

Slide 44 text

20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Как считаем контрольные суммы • Преобразование TContent обратно в XML • Требует двойного преобразования каждого элемента • Использование io.TeeReader

Slide 45

Slide 45 text

20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Как считаем контрольные суммы • Преобразование TContent обратно в XML • Требует двойного преобразования каждого элемента • Использование io.TeeReader • Декодируем только изменившиеся элементы content

Slide 46

Slide 46 text

21 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder. Грабли №1 tr := io.TeeReader(dumpFile, &buffer) decoder := xml.NewDecoder(tr) decoder.CharsetReader = charset.NewReaderLabel for { tokenStartOffset := decoder.InputOffset()

Slide 47

Slide 47 text

21 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder. Грабли №1 tr := io.TeeReader(dumpFile, &buffer) decoder := xml.NewDecoder(tr) decoder.CharsetReader = charset.NewReaderLabel for { tokenStartOffset := decoder.InputOffset() • ... не работает, данные не синхронны

Slide 48

Slide 48 text

21 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder. Грабли №1 tr := io.TeeReader(dumpFile, &buffer) decoder := xml.NewDecoder(tr) decoder.CharsetReader = charset.NewReaderLabel for { tokenStartOffset := decoder.InputOffset() • ... не работает, данные не синхронны • XML декодер «шагает» по UTF-8 строке • контрольные суммы «шагают» по CP1251 строке

Slide 49

Slide 49 text

22 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder. Грабли №2 decoder := xml.NewDecoder(dumpFile) decoder.CharsetReader = func(l string, i io.Reader) (io.Reader, error) { r, err := charset.NewReaderLabel(l, i) return io.TeeReader(r, &buffer), nil } for { tokenStartOffset := decoder.InputOffset()

Slide 50

Slide 50 text

22 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder. Грабли №2 decoder := xml.NewDecoder(dumpFile) decoder.CharsetReader = func(l string, i io.Reader) (io.Reader, error) { r, err := charset.NewReaderLabel(l, i) return io.TeeReader(r, &buffer), nil } for { tokenStartOffset := decoder.InputOffset() • ... не работает, данные не синхронны

Slide 51

Slide 51 text

22 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder. Грабли №2 decoder := xml.NewDecoder(dumpFile) decoder.CharsetReader = func(l string, i io.Reader) (io.Reader, error) { r, err := charset.NewReaderLabel(l, i) return io.TeeReader(r, &buffer), nil } for { tokenStartOffset := decoder.InputOffset() • ... не работает, данные не синхронны • XML декодер «перепрыгивает» через заголовок • контрольные суммы не «перепрыгивают»

Slide 52

Slide 52 text

23 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder Заработало! decoder := xml.NewDecoder(dumpFile) decoder.CharsetReader = func(l string, i io.Reader) (io.Reader, error) { r, err := charset.NewReaderLabel(l, i) offsetCorrection = decoder.InputOffset() return io.TeeReader(r, &buffer), nil } for { tokenStartOffset := decoder.InputOffset() - offsetCorrection

Slide 53

Slide 53 text

24 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Отличное наглядное упражнение • Ридеры/райтеры — go-way • I/O через буфер тянется ещё с 90-ых • Ридеры/райтеры на интерфейсах — новое в go • У новичков затруднено понимание этой абстракции • Хороший пример использования «замыкания» • Разобранная задача — отличное упражнение

Slide 54

Slide 54 text

25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Подключаем gRPC • Универсальное простое рабочее решение

Slide 55

Slide 55 text

25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Подключаем gRPC • Универсальное простое рабочее решение • Данные хранить сразу в формате gRPC

Slide 56

Slide 56 text

25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Подключаем gRPC • Универсальное простое рабочее решение • Данные хранить сразу в формате gRPC • gRPC пытается всё хранить ссылками

Slide 57

Slide 57 text

25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Подключаем gRPC • Универсальное простое рабочее решение • Данные хранить сразу в формате gRPC • gRPC пытается всё хранить ссылками • Несовместимо с нашей борьбой со ссылками

Slide 58

Slide 58 text

26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent

Slide 59

Slide 59 text

26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent • TContent пакуем в json

Slide 60

Slide 60 text

26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent • TContent пакуем в json • Новый тип TMinContent содержит: • метаданные • данные для сравнения (и индекса) • контрольную сумму для сравнения • Полные данные TContent в виде json

Slide 61

Slide 61 text

26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent • TContent пакуем в json • Новый тип TMinContent содержит: • метаданные • данные для сравнения (и индекса) • контрольную сумму для сравнения • Полные данные TContent в виде json • В базу кладем TMinContent

Slide 62

Slide 62 text

26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent • TContent пакуем в json • Новый тип TMinContent содержит: • метаданные • данные для сравнения (и индекса) • контрольную сумму для сравнения • Полные данные TContent в виде json • В базу кладем TMinContent • Увеличен общий объём данных из-за дублирования

Slide 63

Slide 63 text

27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Данные gRPC • Массив «сообщений». «Сообщение» содержит: • метаданные • полные данные в виде json-строки с TContent

Slide 64

Slide 64 text

27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Данные gRPC • Массив «сообщений». «Сообщение» содержит: • метаданные • полные данные в виде json-строки с TContent • Буфер под отдаваемые данные минимален

Slide 65

Slide 65 text

27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Данные gRPC • Массив «сообщений». «Сообщение» содержит: • метаданные • полные данные в виде json-строки с TContent • Буфер под отдаваемые данные минимален • Можно написать свою реализацию gRPC

Slide 66

Slide 66 text

28 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Что ещё можно «подкрутить»?

Slide 67

Slide 67 text

29 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Использование памяти Нас интересует только верхняя граница

Slide 68

Slide 68 text

30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp

Slide 69

Slide 69 text

30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp • Малоэффективно, «подтормаживает»

Slide 70

Slide 70 text

30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp • Малоэффективно, «подтормаживает» • Возврат памяти системе % GODEBUG=madvdontneed=1 /bin/myapp

Slide 71

Slide 71 text

30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp • Малоэффективно, «подтормаживает» • Возврат памяти системе % GODEBUG=madvdontneed=1 /bin/myapp • Малоэффективно в пике, «тормозит»

Slide 72

Slide 72 text

30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp • Малоэффективно, «подтормаживает» • Возврат памяти системе % GODEBUG=madvdontneed=1 /bin/myapp • Малоэффективно в пике, «тормозит» Чем хуже код — тем больше негативный эффект

Slide 73

Slide 73 text

31 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Итог • Данные в map в памяти • map для индексов • Хранения на диске нет • Потоковый разбор XML с TeeReader • Оптимизация форматов данных • Сравнение через контрольные суммы • Вспомогательный тип для хранения данных

Slide 74

Slide 74 text

32 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Вопросы Избави, Боже, нас от ярости норманнов и «интересных задач» schors@gmail.com

Slide 75

Slide 75 text

33 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Ссылки [1] Филипп Кулин. Пишем презентации в LaTeX. https://habr.com/ru/post/471352/. [2] Мониторинг реестра запрещенных сайтов. https://usher2.club/. [3] Исходный код сервиса обработки выгрузки. https://github.com/usher2/u2ckdump. [4] Исходный код Telegram-бота для проверки сайта. https://github.com/usher2/u2ckbot.