SlideShare a Scribd company logo
Разбор и сравнение данных в большом
XML на маленькой VDS
Филипп Кулин (Эшер II)
08 февраля 2020 года, Казань
2 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Откиньтесь на спинку кресла
• Эта презентация сделана с помощью L
A
TEX1
• Избежать большого количества кода не удалось
• Материал основан на работающем сервисе3
• Значительная часть кода написана сообществом
3 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Задача
Что надо было сделать
• Разобрать XML-файл в 160Mb и положить в базу
• Обновлять базу по новым XML-файлам
• Предоставить интерфейс для доступа к базе
3 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Задача
Что надо было сделать
• Разобрать XML-файл в 160Mb и положить в базу
• Обновлять базу по новым XML-файлам
• Предоставить интерфейс для доступа к базе
Условия
• Скорость разбора — единицы минут
• Недорогой виртуальный сервер
• Приоритет стандартных решений
4 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Интересная задача
• Никаких инноваций и ”rocket science”
• Навязанные условия
• Чувствительность к работе памяти
• Чувствительность к ресурсам
• Хрестоматийные решения
5 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Гадание
• Не всегда очевидны «тонкие» места
• Абстракции — «тихие омуты»
5 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Гадание
• Не всегда очевидны «тонкие» места
• Абстракции — «тихие омуты»
• Тесты, бенчмарки, профилирование
6 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Архитектура
• База данных
• Индексы по значениям для поиска
• Функционал наполнения базы из исходных данных
• Функционал обновления данных
• Функционал поиска данных
7 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Формат исходных данных. XML
• 250 тысяч элементов content
• Размер каждого от сотни байт до 6MB
• Общий размер данных свыше 150MB
<?xml version=”1.0” encoding=”windows-1251”?>
<reg:register ...>
<content id=”656” ...> ... </content>
...
<content id=”2062369” ...> ... </content>
</reg:register>
7 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Формат исходных данных. XML
• 250 тысяч элементов content
• Размер каждого от сотни байт до 6MB
• Общий размер данных свыше 150MB
• XML в кодировке CP1251
<?xml version=”1.0” encoding=”windows-1251”?>
<reg:register ...>
<content id=”656” ...> ... </content>
...
<content id=”2062369” ...> ... </content>
</reg:register>
8 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Элемент Content
<content id=”680741”...>
<decision .../>
<domain><![CDATA[example.com]]></domain>
<url><![CDATA[https://example.com/smt]]></url>
...
<ip>10.0.0.1</ip>
...
<ipv6>fc00::beef</ipv6>
...
<ipSubnet>10.1.0.0/16</ipSubnet>
...
<ipSubnet6>fd00::/48</ipSubnet6>
...
</content>
• IP-адреса могут быть поштучно тысячами
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”:
...
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”:
...
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”:
...
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”:
...
10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Выбор формы хранения данных
• Время разбора XML — 10-200 секунд
10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Выбор формы хранения данных
• Время разбора XML — 10-200 секунд
• База нужна только для хранения
10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Выбор формы хранения данных
• Время разбора XML — 10-200 секунд
• База нужна только для хранения
• bbolt — заполнение час+
10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Выбор формы хранения данных
• Время разбора XML — 10-200 секунд
• База нужна только для хранения
• bbolt — заполнение час+
• map + скорость разбора = победа
11 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Формат исходных данных. XML
• 250 тысяч элементов content
• Размер каждого от сотни байт до 6MB
• Общий размер данных свыше 150MB
<?xml version=”1.0” encoding=”windows-1251”?>
<reg:register ...>
<content id=”656” ...> ... </content>
...
<content id=”2062369” ...> ... </content>
</reg:register>
11 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Формат исходных данных. XML
• 250 тысяч элементов content
• Размер каждого от сотни байт до 6MB
• Общий размер данных свыше 150MB
• XML в кодировке CP1251
<?xml version=”1.0” encoding=”windows-1251”?>
<reg:register ...>
<content id=”656” ...> ... </content>
...
<content id=”2062369” ...> ... </content>
</reg:register>
12 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Формат хранения данных
• Каждый content имеет уникальный числовой id
• id очень разреженные
12 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Формат хранения данных
• Каждый content имеет уникальный числовой id
• id очень разреженные
• Решение: map[int]*TContent
TContent — структура для DecodeElement()
case ”content”:
err := decoder.DecodeElement(&v, &_e)
13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Хранение данных. Грабли
• Уменьшаем аллокации
13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Хранение данных. Грабли
• Уменьшаем аллокации
• Улучшательство: map[int]TContent
13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Хранение данных. Грабли
• Уменьшаем аллокации
• Улучшательство: map[int]TContent
• Боль
13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Хранение данных. Грабли
• Уменьшаем аллокации
• Улучшательство: map[int]TContent
• Боль программы ощущалась физически...
• Расширение карты — очень дорогая операция
• Всё-таки map[int]*TContent
14 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Хранение данных. Оптимизация
• Часть элементов имеет аттрибут ts:
ts=”2020-02-06T19:54:00+03:00”
• IPv4-адреса представлены элементом IP:
<ip ts=”2020-02-06T19:54:00+03:00”>
10.0.0.1
</ip>
14 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Хранение данных. Оптимизация
• Часть элементов имеет аттрибут ts:
ts=”2020-02-06T19:54:00+03:00”
• IPv4-адреса представлены элементом IP:
<ip ts=”2020-02-06T19:54:00+03:00”>
10.0.0.1
</ip>
• Две строки против двух целых чисел
• Миллионы структур c IPv4-адресами
15 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Оптимизация конвертации данных
• Преобразование стандартными средствами
ip := net.ParseIP(s)
intIp =: binary.BigEndian.Uint32(ip[12:16])
15 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Оптимизация конвертации данных
• Преобразование стандартными средствами
ip := net.ParseIP(s)
intIp =: binary.BigEndian.Uint32(ip[12:16])
• Пишем менее универсально, но без аллокаций
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
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 {
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 {
17 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Хранение данных. Индексы
• Самая простая и скучная часть
• map нужных данных
• Значения — массивы id элементов content
18 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Обновление данных
• Упрощаем до сравнения content целиком
18 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Обновление данных
• Упрощаем до сравнения content целиком
• Считаем контрольные суммы
19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Контрольные суммы
• Считал SHA256
19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Контрольные суммы
• Считал SHA256
• Профилирование показало, что выбор неудачный
19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Контрольные суммы
• Считал SHA256
• Профилирование показало, что выбор неудачный
• Выбрал на 64-bit FNV-1
19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Контрольные суммы
• Считал SHA256
• Профилирование показало, что выбор неудачный
• Выбрал на 64-bit FNV-1
• Проверил, заглянув «под капот»
20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Как считаем контрольные суммы
• Преобразование TContent обратно в XML
20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Как считаем контрольные суммы
• Преобразование TContent обратно в XML
• Требует двойного преобразования каждого
элемента
20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Как считаем контрольные суммы
• Преобразование TContent обратно в XML
• Требует двойного преобразования каждого
элемента
• Использование io.TeeReader
20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Как считаем контрольные суммы
• Преобразование TContent обратно в XML
• Требует двойного преобразования каждого
элемента
• Использование io.TeeReader
• Декодируем только изменившиеся элементы
content
21 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
TeeReader и Decoder. Грабли №1
tr := io.TeeReader(dumpFile, &buffer)
decoder := xml.NewDecoder(tr)
decoder.CharsetReader = charset.NewReaderLabel
for {
tokenStartOffset := decoder.InputOffset()
21 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
TeeReader и Decoder. Грабли №1
tr := io.TeeReader(dumpFile, &buffer)
decoder := xml.NewDecoder(tr)
decoder.CharsetReader = charset.NewReaderLabel
for {
tokenStartOffset := decoder.InputOffset()
• ... не работает, данные не синхронны
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 строке
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()
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()
• ... не работает, данные не синхронны
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 декодер «перепрыгивает» через заголовок
• контрольные суммы не «перепрыгивают»
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
24 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Отличное наглядное упражнение
• Ридеры/райтеры — go-way
• I/O через буфер тянется ещё с 90-ых
• Ридеры/райтеры на интерфейсах — новое в go
• У новичков затруднено понимание этой абстракции
• Хороший пример использования «замыкания»
• Разобранная задача — отличное упражнение
25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Подключаем gRPC
• Универсальное простое рабочее решение
25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Подключаем gRPC
• Универсальное простое рабочее решение
• Данные хранить сразу в формате gRPC
25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Подключаем gRPC
• Универсальное простое рабочее решение
• Данные хранить сразу в формате gRPC
• gRPC пытается всё хранить ссылками
25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Подключаем gRPC
• Универсальное простое рабочее решение
• Данные хранить сразу в формате gRPC
• gRPC пытается всё хранить ссылками
• Несовместимо с нашей борьбой со ссылками
26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Делаем промежуточный тип
• Из XML преобразуем в TContent
26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Делаем промежуточный тип
• Из XML преобразуем в TContent
• TContent пакуем в json
26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Делаем промежуточный тип
• Из XML преобразуем в TContent
• TContent пакуем в json
• Новый тип TMinContent содержит:
• метаданные
• данные для сравнения (и индекса)
• контрольную сумму для сравнения
• Полные данные TContent в виде json
26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Делаем промежуточный тип
• Из XML преобразуем в TContent
• TContent пакуем в json
• Новый тип TMinContent содержит:
• метаданные
• данные для сравнения (и индекса)
• контрольную сумму для сравнения
• Полные данные TContent в виде json
• В базу кладем TMinContent
26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Делаем промежуточный тип
• Из XML преобразуем в TContent
• TContent пакуем в json
• Новый тип TMinContent содержит:
• метаданные
• данные для сравнения (и индекса)
• контрольную сумму для сравнения
• Полные данные TContent в виде json
• В базу кладем TMinContent
• Увеличен общий объём данных из-за дублирования
27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Данные gRPC
• Массив «сообщений». «Сообщение» содержит:
• метаданные
• полные данные в виде json-строки с TContent
27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Данные gRPC
• Массив «сообщений». «Сообщение» содержит:
• метаданные
• полные данные в виде json-строки с TContent
• Буфер под отдаваемые данные минимален
27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Данные gRPC
• Массив «сообщений». «Сообщение» содержит:
• метаданные
• полные данные в виде json-строки с TContent
• Буфер под отдаваемые данные минимален
• Можно написать свою реализацию gRPC
28 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Что ещё можно «подкрутить»?
29 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Использование памяти
Нас интересует только верхняя граница
30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Настройки рантайма
• Управление сборщиком мусора
% GOGC=50 /bin/myapp
30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Настройки рантайма
• Управление сборщиком мусора
% GOGC=50 /bin/myapp
• Малоэффективно, «подтормаживает»
30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Настройки рантайма
• Управление сборщиком мусора
% GOGC=50 /bin/myapp
• Малоэффективно, «подтормаживает»
• Возврат памяти системе
% GODEBUG=madvdontneed=1 /bin/myapp
30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Настройки рантайма
• Управление сборщиком мусора
% GOGC=50 /bin/myapp
• Малоэффективно, «подтормаживает»
• Возврат памяти системе
% GODEBUG=madvdontneed=1 /bin/myapp
• Малоэффективно в пике, «тормозит»
30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Настройки рантайма
• Управление сборщиком мусора
% GOGC=50 /bin/myapp
• Малоэффективно, «подтормаживает»
• Возврат памяти системе
% GODEBUG=madvdontneed=1 /bin/myapp
• Малоэффективно в пике, «тормозит»
Чем хуже код — тем больше негативный эффект
31 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Итог
• Данные в map в памяти
• map для индексов
• Хранения на диске нет
• Потоковый разбор XML с TeeReader
• Оптимизация форматов данных
• Сравнение через контрольные суммы
• Вспомогательный тип для хранения данных
32 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
Вопросы
Избави, Боже, нас от ярости норманнов
и «интересных задач»
schors@gmail.com
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.

More Related Content

More from Philipp Kulin

ENOG-14. Ограничение доступа в России
ENOG-14. Ограничение доступа в РоссииENOG-14. Ограничение доступа в России
ENOG-14. Ограничение доступа в России
Philipp Kulin
 
Опыт использования IPv6 год спустя
Опыт использования IPv6 год спустяОпыт использования IPv6 год спустя
Опыт использования IPv6 год спустя
Philipp Kulin
 
Как взломать WordPress/Joomla за 5 минут
Как взломать WordPress/Joomla за 5 минутКак взломать WordPress/Joomla за 5 минут
Как взломать WordPress/Joomla за 5 минут
Philipp Kulin
 
Опыт внедрения DNSSEC
Опыт внедрения DNSSECОпыт внедрения DNSSEC
Опыт внедрения DNSSEC
Philipp Kulin
 
Опыт внедрения IPv6 и статистика
Опыт внедрения IPv6 и статистикаОпыт внедрения IPv6 и статистика
Опыт внедрения IPv6 и статистика
Philipp Kulin
 
DNSSEC
DNSSECDNSSEC
Внедрение IPv6
Внедрение IPv6Внедрение IPv6
Внедрение IPv6
Philipp Kulin
 
Хостинг-провайдер и противоправный контент
Хостинг-провайдер и противоправный контентХостинг-провайдер и противоправный контент
Хостинг-провайдер и противоправный контент
Philipp Kulin
 
перспективы черного списка
перспективы черного спискаперспективы черного списка
перспективы черного списка
Philipp Kulin
 

More from Philipp Kulin (9)

ENOG-14. Ограничение доступа в России
ENOG-14. Ограничение доступа в РоссииENOG-14. Ограничение доступа в России
ENOG-14. Ограничение доступа в России
 
Опыт использования IPv6 год спустя
Опыт использования IPv6 год спустяОпыт использования IPv6 год спустя
Опыт использования IPv6 год спустя
 
Как взломать WordPress/Joomla за 5 минут
Как взломать WordPress/Joomla за 5 минутКак взломать WordPress/Joomla за 5 минут
Как взломать WordPress/Joomla за 5 минут
 
Опыт внедрения DNSSEC
Опыт внедрения DNSSECОпыт внедрения DNSSEC
Опыт внедрения DNSSEC
 
Опыт внедрения IPv6 и статистика
Опыт внедрения IPv6 и статистикаОпыт внедрения IPv6 и статистика
Опыт внедрения IPv6 и статистика
 
DNSSEC
DNSSECDNSSEC
DNSSEC
 
Внедрение IPv6
Внедрение IPv6Внедрение IPv6
Внедрение IPv6
 
Хостинг-провайдер и противоправный контент
Хостинг-провайдер и противоправный контентХостинг-провайдер и противоправный контент
Хостинг-провайдер и противоправный контент
 
перспективы черного списка
перспективы черного спискаперспективы черного списка
перспективы черного списка
 

Разбор и сравнение данных в большом XML на маленькой VDS

  • 1. Разбор и сравнение данных в большом XML на маленькой VDS Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань
  • 2. 2 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Откиньтесь на спинку кресла • Эта презентация сделана с помощью L A TEX1 • Избежать большого количества кода не удалось • Материал основан на работающем сервисе3 • Значительная часть кода написана сообществом
  • 3. 3 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Задача Что надо было сделать • Разобрать XML-файл в 160Mb и положить в базу • Обновлять базу по новым XML-файлам • Предоставить интерфейс для доступа к базе
  • 4. 3 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Задача Что надо было сделать • Разобрать XML-файл в 160Mb и положить в базу • Обновлять базу по новым XML-файлам • Предоставить интерфейс для доступа к базе Условия • Скорость разбора — единицы минут • Недорогой виртуальный сервер • Приоритет стандартных решений
  • 5. 4 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Интересная задача • Никаких инноваций и ”rocket science” • Навязанные условия • Чувствительность к работе памяти • Чувствительность к ресурсам • Хрестоматийные решения
  • 6. 5 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Гадание • Не всегда очевидны «тонкие» места • Абстракции — «тихие омуты»
  • 7. 5 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Гадание • Не всегда очевидны «тонкие» места • Абстракции — «тихие омуты» • Тесты, бенчмарки, профилирование
  • 8. 6 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Архитектура • База данных • Индексы по значениям для поиска • Функционал наполнения базы из исходных данных • Функционал обновления данных • Функционал поиска данных
  • 9. 7 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат исходных данных. XML • 250 тысяч элементов content • Размер каждого от сотни байт до 6MB • Общий размер данных свыше 150MB <?xml version=”1.0” encoding=”windows-1251”?> <reg:register ...> <content id=”656” ...> ... </content> ... <content id=”2062369” ...> ... </content> </reg:register>
  • 10. 7 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат исходных данных. XML • 250 тысяч элементов content • Размер каждого от сотни байт до 6MB • Общий размер данных свыше 150MB • XML в кодировке CP1251 <?xml version=”1.0” encoding=”windows-1251”?> <reg:register ...> <content id=”656” ...> ... </content> ... <content id=”2062369” ...> ... </content> </reg:register>
  • 11. 8 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Элемент Content <content id=”680741”...> <decision .../> <domain><![CDATA[example.com]]></domain> <url><![CDATA[https://example.com/smt]]></url> ... <ip>10.0.0.1</ip> ... <ipv6>fc00::beef</ipv6> ... <ipSubnet>10.1.0.0/16</ipSubnet> ... <ipSubnet6>fd00::/48</ipSubnet6> ... </content> • IP-адреса могут быть поштучно тысячами
  • 12. 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”: ...
  • 13. 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”: ...
  • 14. 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”: ...
  • 15. 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”: ...
  • 16. 10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Выбор формы хранения данных • Время разбора XML — 10-200 секунд
  • 17. 10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Выбор формы хранения данных • Время разбора XML — 10-200 секунд • База нужна только для хранения
  • 18. 10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Выбор формы хранения данных • Время разбора XML — 10-200 секунд • База нужна только для хранения • bbolt — заполнение час+
  • 19. 10 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Выбор формы хранения данных • Время разбора XML — 10-200 секунд • База нужна только для хранения • bbolt — заполнение час+ • map + скорость разбора = победа
  • 20. 11 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат исходных данных. XML • 250 тысяч элементов content • Размер каждого от сотни байт до 6MB • Общий размер данных свыше 150MB <?xml version=”1.0” encoding=”windows-1251”?> <reg:register ...> <content id=”656” ...> ... </content> ... <content id=”2062369” ...> ... </content> </reg:register>
  • 21. 11 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат исходных данных. XML • 250 тысяч элементов content • Размер каждого от сотни байт до 6MB • Общий размер данных свыше 150MB • XML в кодировке CP1251 <?xml version=”1.0” encoding=”windows-1251”?> <reg:register ...> <content id=”656” ...> ... </content> ... <content id=”2062369” ...> ... </content> </reg:register>
  • 22. 12 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат хранения данных • Каждый content имеет уникальный числовой id • id очень разреженные
  • 23. 12 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Формат хранения данных • Каждый content имеет уникальный числовой id • id очень разреженные • Решение: map[int]*TContent TContent — структура для DecodeElement() case ”content”: err := decoder.DecodeElement(&v, &_e)
  • 24. 13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Грабли • Уменьшаем аллокации
  • 25. 13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Грабли • Уменьшаем аллокации • Улучшательство: map[int]TContent
  • 26. 13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Грабли • Уменьшаем аллокации • Улучшательство: map[int]TContent • Боль
  • 27. 13 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Грабли • Уменьшаем аллокации • Улучшательство: map[int]TContent • Боль программы ощущалась физически... • Расширение карты — очень дорогая операция • Всё-таки map[int]*TContent
  • 28. 14 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Оптимизация • Часть элементов имеет аттрибут ts: ts=”2020-02-06T19:54:00+03:00” • IPv4-адреса представлены элементом IP: <ip ts=”2020-02-06T19:54:00+03:00”> 10.0.0.1 </ip>
  • 29. 14 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Оптимизация • Часть элементов имеет аттрибут ts: ts=”2020-02-06T19:54:00+03:00” • IPv4-адреса представлены элементом IP: <ip ts=”2020-02-06T19:54:00+03:00”> 10.0.0.1 </ip> • Две строки против двух целых чисел • Миллионы структур c IPv4-адресами
  • 30. 15 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Оптимизация конвертации данных • Преобразование стандартными средствами ip := net.ParseIP(s) intIp =: binary.BigEndian.Uint32(ip[12:16])
  • 31. 15 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Оптимизация конвертации данных • Преобразование стандартными средствами ip := net.ParseIP(s) intIp =: binary.BigEndian.Uint32(ip[12:16]) • Пишем менее универсально, но без аллокаций
  • 32. 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
  • 33. 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 {
  • 34. 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 {
  • 35. 17 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Хранение данных. Индексы • Самая простая и скучная часть • map нужных данных • Значения — массивы id элементов content
  • 36. 18 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Обновление данных • Упрощаем до сравнения content целиком
  • 37. 18 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Обновление данных • Упрощаем до сравнения content целиком • Считаем контрольные суммы
  • 38. 19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Контрольные суммы • Считал SHA256
  • 39. 19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Контрольные суммы • Считал SHA256 • Профилирование показало, что выбор неудачный
  • 40. 19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Контрольные суммы • Считал SHA256 • Профилирование показало, что выбор неудачный • Выбрал на 64-bit FNV-1
  • 41. 19 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Контрольные суммы • Считал SHA256 • Профилирование показало, что выбор неудачный • Выбрал на 64-bit FNV-1 • Проверил, заглянув «под капот»
  • 42. 20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Как считаем контрольные суммы • Преобразование TContent обратно в XML
  • 43. 20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Как считаем контрольные суммы • Преобразование TContent обратно в XML • Требует двойного преобразования каждого элемента
  • 44. 20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Как считаем контрольные суммы • Преобразование TContent обратно в XML • Требует двойного преобразования каждого элемента • Использование io.TeeReader
  • 45. 20 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Как считаем контрольные суммы • Преобразование TContent обратно в XML • Требует двойного преобразования каждого элемента • Использование io.TeeReader • Декодируем только изменившиеся элементы content
  • 46. 21 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder. Грабли №1 tr := io.TeeReader(dumpFile, &buffer) decoder := xml.NewDecoder(tr) decoder.CharsetReader = charset.NewReaderLabel for { tokenStartOffset := decoder.InputOffset()
  • 47. 21 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань TeeReader и Decoder. Грабли №1 tr := io.TeeReader(dumpFile, &buffer) decoder := xml.NewDecoder(tr) decoder.CharsetReader = charset.NewReaderLabel for { tokenStartOffset := decoder.InputOffset() • ... не работает, данные не синхронны
  • 48. 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 строке
  • 49. 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()
  • 50. 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() • ... не работает, данные не синхронны
  • 51. 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 декодер «перепрыгивает» через заголовок • контрольные суммы не «перепрыгивают»
  • 52. 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
  • 53. 24 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Отличное наглядное упражнение • Ридеры/райтеры — go-way • I/O через буфер тянется ещё с 90-ых • Ридеры/райтеры на интерфейсах — новое в go • У новичков затруднено понимание этой абстракции • Хороший пример использования «замыкания» • Разобранная задача — отличное упражнение
  • 54. 25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Подключаем gRPC • Универсальное простое рабочее решение
  • 55. 25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Подключаем gRPC • Универсальное простое рабочее решение • Данные хранить сразу в формате gRPC
  • 56. 25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Подключаем gRPC • Универсальное простое рабочее решение • Данные хранить сразу в формате gRPC • gRPC пытается всё хранить ссылками
  • 57. 25 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Подключаем gRPC • Универсальное простое рабочее решение • Данные хранить сразу в формате gRPC • gRPC пытается всё хранить ссылками • Несовместимо с нашей борьбой со ссылками
  • 58. 26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent
  • 59. 26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent • TContent пакуем в json
  • 60. 26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent • TContent пакуем в json • Новый тип TMinContent содержит: • метаданные • данные для сравнения (и индекса) • контрольную сумму для сравнения • Полные данные TContent в виде json
  • 61. 26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent • TContent пакуем в json • Новый тип TMinContent содержит: • метаданные • данные для сравнения (и индекса) • контрольную сумму для сравнения • Полные данные TContent в виде json • В базу кладем TMinContent
  • 62. 26 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Делаем промежуточный тип • Из XML преобразуем в TContent • TContent пакуем в json • Новый тип TMinContent содержит: • метаданные • данные для сравнения (и индекса) • контрольную сумму для сравнения • Полные данные TContent в виде json • В базу кладем TMinContent • Увеличен общий объём данных из-за дублирования
  • 63. 27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Данные gRPC • Массив «сообщений». «Сообщение» содержит: • метаданные • полные данные в виде json-строки с TContent
  • 64. 27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Данные gRPC • Массив «сообщений». «Сообщение» содержит: • метаданные • полные данные в виде json-строки с TContent • Буфер под отдаваемые данные минимален
  • 65. 27 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Данные gRPC • Массив «сообщений». «Сообщение» содержит: • метаданные • полные данные в виде json-строки с TContent • Буфер под отдаваемые данные минимален • Можно написать свою реализацию gRPC
  • 66. 28 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Что ещё можно «подкрутить»?
  • 67. 29 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Использование памяти Нас интересует только верхняя граница
  • 68. 30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp
  • 69. 30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp • Малоэффективно, «подтормаживает»
  • 70. 30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp • Малоэффективно, «подтормаживает» • Возврат памяти системе % GODEBUG=madvdontneed=1 /bin/myapp
  • 71. 30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp • Малоэффективно, «подтормаживает» • Возврат памяти системе % GODEBUG=madvdontneed=1 /bin/myapp • Малоэффективно в пике, «тормозит»
  • 72. 30 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Настройки рантайма • Управление сборщиком мусора % GOGC=50 /bin/myapp • Малоэффективно, «подтормаживает» • Возврат памяти системе % GODEBUG=madvdontneed=1 /bin/myapp • Малоэффективно в пике, «тормозит» Чем хуже код — тем больше негативный эффект
  • 73. 31 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Итог • Данные в map в памяти • map для индексов • Хранения на диске нет • Потоковый разбор XML с TeeReader • Оптимизация форматов данных • Сравнение через контрольные суммы • Вспомогательный тип для хранения данных
  • 74. 32 Филипп Кулин (Эшер II) 08 февраля 2020 года, Казань Вопросы Избави, Боже, нас от ярости норманнов и «интересных задач» schors@gmail.com
  • 75. 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.