Author Archive

Эффективность фильтра Блума в памяти

В предыдущей статье был рассмотрен расчет оптимальных параметров фильтра Блума и его реализация.

Со статьей можно ознакомиться здесь: Фильтр Блума

Для оценки эффективности будем рассматривать сколько оперативной памяти потребляют различные структуры данных в Java 11.

Для примера будем размещать в памяти информацию о 20 миллионах ИИН и делать наблюдения через VisualVM.

ИИН представляет собой строку из 12 символов.

Хранение в ArrayList<String>

Хранение 20 миллионов ИИН в ArrayList:

Создается 20 миллионов объектов String, каждый объект String ссылается на массив байт из 12 элементов.

ЭлементРазмерКол-воОбщий размер
String20 B20 000 000553 MB
byte[12]36 B20 000 000687 MB

Суммарно данный способ занимает в памяти примерно 1240 MБ.

Хранение в HashSet<String>

Хранение 20 миллионов ИИН в HashSet<String>:

Создается 20 миллионов объектов таких же как и для ArrayList, дополнительно хранятся узлы корзины HashMap.Node (под капотом HashSet находится HashMap).

ЭлементРазмерКол-воОбщий размер
String20 B20 000 000553 MB
byte[12]36 B20 000 000687 MB
HashMap.Node44 B20 000 000839 MB

Суммарно данный способ занимает в памяти примерно 2079 MБ.

Хранение в HashSet<Long>

Хранение 20 миллионов ИИН в HashSet<Long>:

Создается 20 миллионов объектов Long и HashMap.Node.

ЭлементРазмерКол-воОбщий размер
Long24 B20 000 000458 MB
HashMap.Node44 B20 000 000839 MB

Суммарно данный способ занимает в памяти примерно 1297 MБ.

Хранение в ArrayList<Long>

Создается 20 миллионов объектов Long.

ЭлементРазмерКол-воОбщий размер
Long24 B20 000 000458 MB

Суммарно данный способ занимает в памяти примерно 458 MБ.

Хранение в long[]

Создается массив из 20 миллионов элементов типа long.

ЭлементРазмерКол-воОбщий размер
long8 B20 000 000152 MB

Суммарно данный способ занимает в памяти примерно 152 MБ.

Хранение в MyBloomFilter

В реализации MyBloomFilter используется BitSet, BitSet в свою очередь хранит биты в виде массива long, поэтому основную память забирает этот массив.

В нашей реализации BitSet хранит информацию о 268 435 456 битах.

В один long помещается 64 бит.

268 435 456 / 64 = 4 194 304 (кол-во элементов в массиве)

Практически нет дополнительных потерь при хранении байтов (24 байта транится на сам массив).

268 435 456 бит / 8 = 33 554 432 байт

Суммарно данный способ занимает в памяти примерно 32 MБ.

Хранение в GoogleBloomFilter

Как было рассмотрено в предыдущей статье о фильтре Блума, необходимо рассчитать оптимальное кол-во бит для хранения.

Для 20 миллионов ИИН с вероятностью ошибок 1% оптимальное кол-во бит составляет 191 701 168.

BloomFilter из набора guava для хранения бит использует AtomicLongArray, который в свою очередь использует для хранения бит также long массив (long[]).

Рассчитаем кол-во элементов в массиве:

⌈191 701 168 / 64⌉  = ⌈2 995 330.75⌉ =  2 995 331

Теперь рассчитаем размер в байтах:

191 701 168 бит / 8 = 23 962 646 байт

Суммарно данный способ занимает в памяти примерно 23 MБ.

Итоговая сравнительная таблица

Структура данныхПотребляемая памятьПримечание
HashSet<String>2079 MБОптимальные операции добавления и поиска, но высокое потребление памяти
HashSet<Long>1297 MБДополнительная работа с Long, из-за наличия у ИИН ведущих нулей
ArrayList<String>1240 MБСписок должен быть отсортирован, что была возможность поиска
ArrayList<Long>458 MБСписок должен быть отсортирован. Дополнительная работа с Long, из-за наличия у ИИН ведущих нулей.
long[]152 MБФиксированный размер.
MyBloomFilter32 MБНеоптимальная реализация
Bloom Filter (Google Guava)23 MБОптимальная реализация

Заключение

В данной статье было рассмотрено несколько вариантов хранения информации о 20 миллионах ИИН, где вместо 1-2 гигабайт, можно обойтись 20-30 мегабайтами (экономия на порядок) при допущении заданной вероятности ошибок.

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

Фильтр Блума имеет достаточно специфические варианты использования, и есть небольшая вероятность, что он вам пригодится.

Фильтр Блума

Давно хотелось попробовать реализовать фильтр Блума (хотя я его называют фильтром Блюма).

Фильтр Блума - это вероятностная структура данных, позволяющая проверить принадлежность элемента к множеству. Данная структура может точно ответить на запрос об отсутствии элемента в множестве, но может ошибаться с заданной нами вероятностью о наличии элемента во множестве.

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

Фильтр Блума может использоваться, например, для подсчета уникальных посетителей, для проверки орфографии, в различных СУБД для уменьшения обращения к ПЗУ (постоянное запоминающее устройство).

Будем пробовать фильтр Блума на примере хранения 20 миллионов ИИНов.

ИИН - индивидуальный идентификационный номер физического лица в Республике Казахстан, состоящий из 12 цифр.

Я заранее сгенерировал список из 20 миллионов ИИНов (102 MB).

Скачать список можно здесь: https://drive.google.com/file/d/1SIfp_CRl4cQetc3Kw2jqDcWpF3fYPc2s/view?usp=sharing

Исходные коды генератора можно посмотреть в github: https://github.com/Nyquest/iin_generator

Фильтр Блума должен преобразовать наше множество из 20 миллионов ИИН в набор битов с помощью нескольких хэш-функций.

ИИН считается принадлежащим множеству, если значения всех вычисленных хэш-функции от ИИНа указывают на номера отмеченных бит. Если хотя бы один бит не отмечен, ИИН однозначно не принадлежит множеству.

Формулы

Оптимальное количество бит и достаточное кол-во хэш-функций можно рассчитать по формулам.

Формула оптимального количества бит:

    \[    \displaystyle m=-{\frac {n\ln \varepsilon }{(\ln 2)^{2}}} \]

m - искомый размер битового массива (кол-во бит);

n - размер исходного множества;

ln - функция натурального логарифма;

ε - желаемая вероятность ошибок (например, 0.01 - ожидаем 1% ошибок)

Формула достаточного кол-ва кэш-функций зависит от выделенного кол-ва бит:

    \[    \displaystyle k={\frac {m}{n}}\ln 2 \]

k - кол-во кэш-функций;

m - кол-во битов в результирующем массиве;

n - кол-во элементов в исходном множестве;

ln - функция натурального логарифма.

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

Реализация формул на Java

Самой главной проблемой при реализации фильтра Блума является выбор хэш-функций.

Первое, что пришло в голову это брать SHA-256 (привет любителям блокчейна) от ИИНа, и части SHA-256 использовать как отдельные значения хэш-функций.

Но, на самом деле, SHA-256 недостаточно, поэтому был взят SHA-384.

Чтобы было удобнее брать части SHA-384 делается выравнивание размера битового массива.

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

Общая схема работы

Функция расчета оптимального кол-во бит

/**
 * Вычислить оптимальное кол-во бит
 * @param totalCount общее кол-во элементов
 * @param errorProbability вероятность ошибок
 * @return оптимальное кол-во бит
 */
static long optimalBitCount(long totalCount, double errorProbability) {
    return (long) Math.ceil(
            -totalCount * Math.log(errorProbability) / Math.pow(Math.log(2), 2));
}

Подставим наши значения (20 миллионов - кол-во элементов, 0.01 - ожидаем 1% ошибок) и получим размер битового массива, равный 191 701 168:

System.out.println(optimalBitCount(20_000_000, 0.01)); // 191_701_168

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

Это можно увидеть, переведя размер 191 701 168 в двоичный вид 1011011011010010000010110000:

System.out.println(Long.toBinaryString(optimalBitCount(20_000_000, 0.01)));

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

/**
 * Вычислить оптимальное кол-во бит с выравниванием
 * @param totalCount общее кол-во элементов
 * @param errorProbability вероятность ошибок
 * @return оптимальное кол-во бит в порции с выравниванием
 */
static long optimalBitCountWithAlignment(long totalCount, double errorProbability) {
    long result = optimalBitCount(totalCount, errorProbability);
    long l = Long.highestOneBit(result);
    return l == totalCount ? totalCount : l << 1;
}

Еще раз подставим наши значения, получим размер, равный 268 435 456

System.out.println(optimalBitCountWithAlignment(20_000_000, 0.01)); // 268_435_456

Переведем 268 435 456 - 1 в двойчный вид 1111111111111111111111111111 (28 бит).

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

System.out.println(Long.toBinaryString(
                optimalBitCountWithAlignment(20_000_000, 0.01) - 1));

Размер стал больше, но зато все биты задействованы.

Таким образом, считаем размер битового массива равным 268 435 456.

Функция расчета кол-ва хэш-функций:

/**
 * Расчет кол-ва хэш-функций
 * @param totalCount общее кол-во элементов
 * @param bitArraySize размер битового массив
 * @return минимальное кол-во хэш-функций
 */
static long hashFunctionCount(long totalCount, long bitArraySize) {
    return (long) Math.ceil(Math.log(2) * ((double)bitArraySize / totalCount));
}

Подставим наши значения, получим 10 хэш-функций:

System.out.println(hashFunctionCount(20_000_000, 268_435_456)); // 10

268 435 456 значений можно закодировать 28 битами, что составляет 3.5 байта.

    \[    \displaystyle 2^{28} = 268 435 456 \]

Для удобства мы можем брать по 4 байта хэш-функции SHA, при этом 3 байта целиком, а один только на половину, другую же половину просто игнорируем.

Итого там потребуется 40 байт, которые есть в SHA-384.

Хэш-функцияКол-во битКол-во байт
SHA-25625632
SHA-38438448

Из SHA-384 можно получить не 10, а 12 функций. Чем больше функций, тем меньше будет ошибок.

Реализация фильтра Блума на Java

Все необходимые расчеты сделаны, можно приступать к реализации.

Для начала реализуем получение хэша от строки (ИИНа) в виде массива байт.

MessageDigest digest = MessageDigest.getInstance("SHA-384");
byte[] digest(String value) {
    return this.digest.digest(value.getBytes(StandardCharsets.UTF_8));
}

Биты будем хранить в стандартной структуре данных BitSet. У данный структуры есть методы set и get для установки и чтения значения соответствующего бита.

BitSet bitSet = new BitSet(1 << 28); // размер BitSet, равный 268_435_456

Реализуем функцию перевода четырех байт в число, являющееся номером позиции бита в BitSet.

/**
 * переводим четыре байта в число
 * @param digest массив байт
 * @param position позиция, от которой берутся 4 байта
 * @return число
 */
private int bitsToInt(byte[] digest, int position) {
    return ((((1 << 4) - 1)) & digest[position]) << 24 // берем половину байта и смещаем его на 24 позиции влево
            | (0xff & digest[position + 1]) << 16 // берем следующий целый байт и смещаем его на 16 позиций влево
            | (0xff & digest[position + 2]) << 8 // берем следующий целый байт и смещаем его на 8 позиций влево 
            | (0xff & digest[position + 3]); // берем следующий целый байт без каких-либо смещений
}

Функция установки битов выглядит следующим образом:

private void saveBits(String value) {
    byte[] digest = digest(value);
    for (int i = 0; i + 3 < digest.length; i += 4) {
        bitSet.set(bitsToInt(digest, i));
    }
}

Функция проверки принадлежности значения множеству:

private boolean checkBits(String value) {
    byte[] digest = digest(value);
    for (int i = 0; i + 3 < digest.length; i += 4) {
        if(!bitSet.get(bitsToInt(digest, i))) {
            return false;
        }
    }
    return true;
}

Тестирование фильтра Блума

Пример применения фильтра Блума на 20 миллионах ИИН https://github.com/Nyquest/bloom_filter

Проверять фильтр Блума будем следующим образом:

  1. Загрузим ИИНы в HashSet;
  2. Загрузим ИИНы в фильтр Блума;
  3. Сгенерим 10 миллионов случайных ИИН и прогоним их через фильтр Блума;
  4. Если фильтр показывает, что ИИН не относится к множеству, но этот ИИН есть в HashSet, то выбрасываем исключение и останавливаем программу;
  5. Если фильтр показывает, что ИИН относится к множеству, но его нет в HashSet, учитываем данный факт как ложноположительное срабатывание;
  6. Подсчитываем ложноположительные срабатывания и их в процент от общего числа запросов.

В примере две реализации: собственная MyBloomFilter и GoogleBloomFilter(BloomFilter из guava).

При создании GoogleBloomFilter указывается вероятность ошибок, которая подтверждается эмпирическим путем.

Но так как наш фильтр модифицирован дважды, эмпирические данные показывают ошибки в 0.18% от всех тестовых запросов:

  1. Набор бит увеличен из-за выравнивания
  2. Взято больше хэш-функций (12 вместо 10)

Теперь рассчитаем ожидаемую вероятность ошибок по формуле:

    \[    \displaystyle (1-e^{-kn/m})^{k} \]

Функция расчета вероятности ошибок на Java

/**
 * Расчет вероятности ошибок
 * @param totalCount общее кол-во элементов
 * @param bitArraySize размер битового массив
 * @param hashFunctionCount кол-во хэш-функций
 * @return вероятность ошибок
 */
public static double errorProbability(long totalCount, long bitArraySize, long hashFunctionCount) {
    return Math.pow(1 - Math.pow(Math.E, 
            -(double)hashFunctionCount * totalCount / bitArraySize), hashFunctionCount);
}
}

Подставим наши значения и получим 0.0018, т.е. теоретические и практические результаты совпали.

System.out.println(errorProbability(20_000_000, 268_435_456, 12));

Заключение

Получилась рабочая версия фильтра Блума, но с рядом недостатков.

Сравнивать будет с реализацией от Google, хотя почему-то в guava Bloom Filter еще в beta-тестировании.

Сравнение, конечно же, не в нашу пользу.

  1. Потребляет больше оперативной памяти из-за выравнивания;
  2. Непараметризирован. Размер битового массива фиксирован под определенную задачу.
  3. Сильная завязка на выравнивание.
  4. Реализация медленнее. С тестовым заданием фильтр в последовательном режиме справляется за 11 секунд, а GoogleBloomFilter за 5 секунд.
  5. Сама реализация как и BitSet без потоковой безопасности (thread-safety), в отличие от Guava Bloom Filter.

В реальных проектах лучше использовать готовые реализации Bloom Filter с потоковой безопасностью.

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

В реализации Google используется магия и трюки (см. фрагмент кода BloomFilterStrategies.java по добавлению элемента в множество):

@Override
public <T extends @Nullable Object> boolean put(
    @ParametricNullness T object,
    Funnel<? super T> funnel,
    int numHashFunctions,
    LockFreeBitArray bits) {
  long bitSize = bits.bitSize();
  long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
  int hash1 = (int) hash64;
  int hash2 = (int) (hash64 >>> 32);
 
  boolean bitsChanged = false;
  for (int i = 1; i <= numHashFunctions; i++) {
    int combinedHash = hash1 + (i * hash2);
    // Flip all the bits if it's negative (guaranteed positive number)
    if (combinedHash < 0) {
      combinedHash = ~combinedHash;
    }
    bitsChanged |= bits.set(combinedHash % bitSize);
  }
  return bitsChanged;
}

Здесь один хэш получается из предыдущего. Если в вычислениях случается переполнение, в результате которого образумется отрицательное число, то происходит инверсия бит, чтобы получилось положительное число за счет смены бита, отвечающего за знак в том числе. Чтобы не выходить за пределы битового массива делается закольцовывание с помощью взятия остатка от деления на размер массива.

В следующем блоге смотрите эффективность фильтра Блума с точки зрения расхода памяти: Эффективность фильтра Блума в памяти

Установка программ на MacBook Pro 16» (Apple M1)

1-го марта решил купил себе Apple MacBook Pro 16 MK1E3, пока курс доллара сильно не поднялся.

Знаю, что есть трудности с установкой приграмм из-за M1

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

Приложения устанавливал в описанной ниже последовательности.

  • Установка Firefox с официального сайта без проблем.
  • Установка Google Chrome с официального сайта без проблем.
  • Установка Sublime Text с официального сайта без проблем.
  • Установка Brew с официального сайта без проблем. Но нужно добавить brew в PATH (в процессе установки это предупреждение можно увидеть)
  • Для установки Java 8 потребовалась Rossetta 2 (Rosetta 2 позволяет компьютерам Mac с процессорами Apple использовать приложения, созданные для компьютеров Mac с процессорами Intel).

    Устанавливаем Rossetta 2:

    sudo softwareupdate --install-rosetta

    Устанавливаем Java 8 (AdoptOpenJDK):

    brew tap adoptopenjdk/openjdk
    brew install --cask adoptopenjdk8
  • Установка Java 11 с сайта https://adoptium.net/ без проблем
  • Установка Intellij IDEA 2021.3.1 (сборка для Apple Silicon) без проблем
  • Установка Tor Browser (совместимость через Rossetta 2)
  • Установка OpenVPN Connect Client (совместимость через Rossetta 2)
  • Установка git
    brew install git
  • Установка Docker Desktop (для Apple Silicon)
  • Установка Postman (Mac Apple Chip)
  • Установка Libre Office (Apple Silicon)

Пока особых проблем не возникало с Apple M1.

Что нравится:

  • Тряпочный провод, мягкий, надеюсь долговечный (старые провода обычно желтеют и разбананиваются в месте контакта)
  • Не греется, как, например, макбук 2017 года

Маршрутизация Logstash

Предположим, что мы используем для сбора логов стек ELK (Elasticsearch-Logstash-Kibana).

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

В этом блоге рассмотрим маршрутизацию записей логов в разные индексы.

Для доставки файлов в Logstash будем использовать Filebeat

Рассмотрим пример маркирования логов системной утилитой auditd маркером "auditd: true" и маркирование логов PostgreSQL маркером "postgres: true". В примере Filebeat отправляет записи логов в Logstash по порту 5044.

Пример конфигурации filebeat.yml:

- input_type: log
  paths:
    - /var/log/audit/*.log
  fields:
    auditd: true
- input_type: log
  paths:
    - /var/lib/pgsql/12/data/log/postgresql-*.log
  fields:
    postgres: true
output.logstash:
  hosts: ["localhost:5044"]

Теперь, непосредственно, рассмотрим маршрутизацию логов в Logstash в logstash.conf.

input {
  beats {
    port => 5044
  }
}
output {
        if [fields][auditd] {
                elasticsearch {
                        hosts => ["localhost:9200"]
                        index => "auditd-%{+YYYY.MM.dd}"
                }
        } else if [fields][postgres] {
                elasticsearch {
                        hosts => ["localhost:9200"]
                        index => "pgaudit-%{+YYYY.MM.dd}"
                }
        } else {
                elasticsearch {
                        hosts => ["localhost:9200"]
                        index => "unknown-%{+YYYY.MM.dd}"
                }
        }
	stdout { codec => rubydebug }
}

В примере описанном выше Logstash ведет прием записей логов по порту 5044 и делает маршрутизацию в зависимости от флагов fields.auditd и fields.postgres. Если не получается распределить сообщения в требуемый индекс, то отправляем его в индекс под названием unknown.

Индексы в Elasticsearch записываются по дневным партициям: %{+YYYY.MM.dd}.

Аудит PostgreSQL

Несколько лет назад по требованию технического задания необходимо было сделать аудит базы данных.

База данных была на PostgreSQL, и особого выбора, кроме как pgAudit, не было.

Приступим к установке и настройке pgAudit.

Расширение pgAudit инсталируется из исходных кодов.

Установка

  1. Склонируйте официальный репозиторий:
    git clone https://github.com/pgaudit/pgaudit.git
  2. Открываем созданную директорию pgaudit:
    cd pgaudit
  3. Переключите ветку на необходимый релиз (вашу версию PostgreSQL)
    git checkout REL_12_STABLE
  4. Сделайте сборку и установку pgAudit (обратите внимание на правильность пути к утилите pg_config):
    sudo make install USE_PGXS=1 PG_CONFIG=/usr/pgsql-12/bin/pg_config
    Может потребоваться установка утилиты make и других зависимостей.

Настройка

  1. Внесем корректировки в конфигурационный файл /var/lib/pgsql/12/data/postgresql.conf:
    shared_preload_libraries = 'pgaudit'
    
  2. Перезапустите PostgreSQL
    sudo service postgresql-12.service restart
  3. Зайдите в консоль psql под пользователем postgres
    sudo -u postgres psql
  4. Установить расширение pgaudit:
    CREATE EXTENSION pgaudit
  5. В конфигурационном файле /var/lib/pgsql/12/data/postgresql.conf с помощью опции pgaudit.log укажите какие классы запросов/команд необходимо аудировать (весь перечень и другие опции можно посмотреть в описании на github):
    pgaudit.log = 'write, role, ddl'
    
  6. В конфигурационном файле postgresql.conf укажите роль, которая будет использоваться для аудита в опции pgaudit.role:
    pgaudit.role = 'auditor'
    
  7. В конфигурационном файле postgresql.conf дополните префикс логирования следующим образом:
    log_line_prefix = '%m %d %u %h '
    

    Префикс содержит следующие данные и добавляется к каждой строке лога: дату(%m), название базы данных (%d), логин пользователя (%u) и удаленный адрес пользователя (%h).

  8. После изменения настроек postgresql.conf не забывайте перезагружать PostgreSQL, чтобы настройки применились.
  9. Создайте роль auditor:
    CREATE ROLE auditor;

    Аудит настраивается путем предоставления/отзыва прав на определенные операции для роли auditor.

  10. Добавить аудит на определенные действия можно следующими командами:
    GRANT INSERT ON your_table TO auditor;
    GRANT INSERT ON your_table TO auditor;
    GRANT UPDATE ON your_table TO auditor;
    GRANT DELETE ON your_table TO auditor;
  11. Убрать аудит на определенные действия можно следующими командами:
    REVOKE INSERT ON your_table TO auditor;
    REVOKE INSERT ON your_table TO auditor;
    REVOKE UPDATE ON your_table TO auditor;
    REVOKE DELETE ON your_table TO auditor;

Доступ к Kafka из/вне Docker контейнеров

При локальной разработке с Kafka могут возникнуть проблемы с доступом, если она запущена в Docker контейнере, и требуется обращение как изнутри Docker так и снаружи.

В Kafka запись и чтение осуществляется в ноду лидера для заданной партиции. Клиент может обратиться к любому узлу Kafka, чтобы узнать по какому адресу нужно делать операцию записи/чтения, даже если Kafka представлена одним узлом. Здесь и возникает путаница, если у вас несколько сетей (например, внешняя и внутренняя). Для внешнего пользователя нужно предоставить внешний адрес, а для внутреннего внутренний, соответственно.

Для решения этой проблемы будут использоваться два порта:

9092 - для доступа извне, в моем случае это хосте (мой компьютер)

29092 - порт для обращения к Kafka других контейнеров (например, Debezium) внутри Docker. Пример такой собранной конфигурации (docker-compose.yaml):

version: '2'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:5.4.0
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
  kafka:
    image: confluentinc/cp-kafka:5.4.0
    ports:
      - 9092:9092
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,PLAINTEXT_HOST://0.0.0.0:9092
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
  connect:
    image: debezium/connect:1.4
    ports:
     - 8083:8083
    links:
     - kafka
    environment:
     - BOOTSTRAP_SERVERS=PLAINTEXT://kafka:29092
     - GROUP_ID=1
     - CONFIG_STORAGE_TOPIC=dbz_connect_configs
     - OFFSET_STORAGE_TOPIC=dbz_connect_offsets
     - STATUS_STORAGE_TOPIC=dbz_connect_statuses
p.s. Если у вас другая ситациация, то рекомендую посмотреть статью:

https://www.confluent.io/blog/kafka-client-cannot-connect-to-broker-on-aws-on-docker-etc/

Бэкапирование базы данных PostgreSQL

Все мы знаем, что нужно периодически делать резервные копии баз данных.

И почему-то делаем это только для боевых баз данных, но не для тестовых.

Хотя потеря тестовых данных тоже критична, т.к. на восстановление данных может уйти достаточное количество времени.

База данных может повредиться по многим причинам.

Например, операции update или delete с некорректным условием и вообще без условия, каскадные удаления и т.д.

Ниже будет приведен скрипт, который делает следующие операции:

  1. Создает копию указанной базы данных
  2. Упаковывает копию в zip-архив
  3. Удаляет копию для экономии места
Создайте скрипт backup.sh со следующим содержимым
if [ -z "$1" ]; then
	echo "Error! Please select database."
	echo "Example:"
	echo "         $0 <YOUR_DATABASE>"
	exit 1
fi
DB_NAME=$1
BACKUP_DIR="/root/scripts/"
DUMP_NAME="$BACKUP_DIR$DB_NAME.dump"
ZIP_NAME="$BACKUP_DIR$DB_NAME.zip"
echo "DB_NAME   : $DB_NAME"
echo "BACKUP_DIR: $BACKUP_DIR"
echo "DUMP_NAME : $DUMP_NAME"
echo "ZIP_NAME  : $ZIP_NAME"
 
sudo -u postgres pg_dump $DB_NAME > $DUMP_NAME \
        && zip $ZIP_NAME $DUMP_NAME \
        && rm $DUMP_NAME

BACKUP_DIR - путь к директории, где планируется хранить резерные копии баз данных.

Не забудьте дать скрипту право на исполнение (executable):

chmod +x backup.sh

Скрипту передается название базы данных следующим образом:

./backup.sh <your_database>

Скрипт можно доработать под ваши нужны.

Добавить дополнительное копирование на другой сервер.

Изменить способ сжатия копий. Например, для сжатия больших файлов использовать 7-Zip.

Теперь сделаем запуск скрипта ежедневно в 4 утра.

Выполните команду:

sudo crontab -e

Обратите внимание, что скрипт запускается через sudo. Это сделано чтобы скрипт запускался от имени администратора, иначе вы настроите запуск от текущего пользователя. Добавьте следующую запись:

0 4 * * * /your_path/backup.sh 
Скрипт восстановления из бэкапа:
psql your_database < your_database.dump

p.s. Поменьше вам восстанавливаться из бэкапов =)

Создаем Searchable PDF с помощью Tesseract OCR

Недавно на работе столкнулись с задачей разпознавания сканированных документов и поиска по ним.

Мною был рассмотрен движок распознавания текста с открытым исходным кодом Tesseract.

В данной статье будут рассмотрены основные моменты возможной реализации.

Предположим, что у нас есть многостраничный отсканированные документ в формате PDF, но нераспознанный.

И наша задача распознать текст с помощью OCR (Optical character recognition - Оптическое распознавание символов) и создать так называемый Searchable PDF.

Searchable PDF - это PDF, в котором поверх изображения размещается дополнительный слой, содержащий распознанный текст, причем на тех же позициях что и на изображении.

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

  • imagemagick - набор программ (консольных утилит) для работы с множеством графических форматов;
  • tesseract-ocr - приложение оптического распознавания символов;
  • tesseract-ocr-all - все языковые пакеты (но можно установить только конкретные языковые пакеты)

У tesseract есть языковые пакеты для русского и казахских языков, что очень круто.

Также можно не устанавливать локально у себя tesseract, а запустить через docker.

Официального образа на hub.docker.com я не нашел, поэтому сделал свой.

Запустить контейнер с tesseract из образа naik85/tesseract можно так (пример для linux/unix):

docker run --rm -v "$(PWD)":/files -w /files -it naik85/tesseract bash
После старта контейнера откроется консоль bash, где можно будет выполнять команды. Также будут доступны ваши файлы из директории, где вы запустили команду docker run.

Первый этап

На первом этапе нужно извлечь изображения из PDF. Здесь есть два варианта либо преобразовать PDF в один файл TIFF, либо преобразовать в набор изображений.

TIFF - это многостраничный формат хранения растровых графических изображений.

Для конвертации в TIFF использовалась следующая команда:

convert -density 300 YOUR_FILE.pdf -depth 1 -strip -background white -alpha off YOUR_FILE.tiff

Для конвертации в PNG использовалась следующая команда:

convert -density 300 YOUR_FILE.pdf -depth 1 -strip -background white -alpha off YOUR_FILE.png

Параметры конвертации приведены для примера, их можно настроить под ваши требования.

После выполнения конвертации в PNG для каждой страницы будет создан отдельный файл изображения.

Например:

YOUR_FILE-0.png
YOUR_FILE-1.png
...
YOUR_FILE-N.png

Второй этап

К сожалению, у меня не получилось преобразовать документ TIFF в Searchable PDF через tesseract.

Была использована следующая команда:

tesseract YOUR_FILE.tiff searchable -l rus PDF

Выходила следующая ошибка:

Tesseract Open Source OCR Engine v4.1.1 with Leptonica
Error in pixReadFromTiffStream: failed to read tiffdata

Кто знает как решить проблему, пишите в комментариях.

Для нашей задачи постраничное деление на отдельные файлы (изображения) было даже предпочтительней (об этом ниже).

Конвертируем каждую страницу (файл png) в Searchable PDF:

tesseract YOUR_FILE-0.png searchable-0 -l rus+kaz+eng pdf
tesseract YOUR_FILE-1.png searchable-1 -l rus+kaz+eng pdf
...
tesseract YOUR_FILE-N.png searchable-2 -l rus+kaz+eng pdf

На выходе получаем файлы:

searchable-0.pdf
searchable-1.pdf
...
searchable-N.pdf

Очень крутая фишка, что можно разпозначать несколько языков, перечислив их через символ '+': rus+kaz+eng.

Команда распознавания и извлечения текста

tesseract YOUR_FILE-0.png -l rus+kaz+eng YOUR_FILE-0
tesseract YOUR_FILE-1.png -l rus+kaz+eng YOUR_FILE-1
...
tesseract YOUR_FILE-N.png -l rus+kaz+eng YOUR_FILE-N

В результате будут созданы текстовые файлы:

YOUR_FILE-0.txt
YOUR_FILE-1.txt
...
YOUR_FILE-N.txt

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

Склеив тексты страниц и положив их в поисковый движок, получим подокументый поиск.

Если нужен целый Searchable PDF, то можно его склеивать из отдельных страниц.

p.s. Деление на отдельные страницы затратно получается, но, думаю, зато это более гибко.

Транзакции в debug-режиме

Транзакции в Hibernate работают не всегда прозрачно.

Если для отладки запросов в Hibernate мы используем show-sql: true в application.yml, то для включения логирования транзации нужно переключить уровень логирования на DEBUG для определенного класса.

Следующая настройка для Hibernate 5 и системы логирования SLF4J.

В файл logback-spring.xml (или logback.xml) добавьте следующую строку:

<logger name="org.hibernate.engine.transaction.internal.TransactionImpl"
 level="DEBUG"/>

После рестарта приложения в логах должны появится следующие строки:

1) Начало транзакции

...TransactionImpl         : begin

2) Коммит транзакции

...TransactionImpl         : committing

3) Откат транзакции

...TransactionImpl         : rolling back

Во сколько раз Base64 увеличивает размер в байтах

Base64 — стандарт кодирования байтов при помощи только 64 символов (A-Z, a-z и 0-9 (62 знака) и 2 дополнительных символа, зависящих от системы реализации).

Одним байтом можно закодировать 256 значений (28 бит), в то время как Base64 только 64 (26 бит).

Из этого следует соотношение 28 + 28 + 28 = 26 + 26 + 26 + 26, т.е. каждые 3 исходных байта кодируются в 4 байта.

Можно вывести формулу:

[Кол-во байт в Base64] = ([Исходное кол-во байт] / 3) * 4
или
[Кол-во байт в Base64] = 4 * [Исходное кол-во байт] / 3
или
[Кол-во байт в Base64] = 4/3 * [Исходное кол-во байт]

Например: Файл размером 1500 байт в Base64 будет занимать 2000 байт (т.е. на 1/3 больше).

Рассмотрим обратную задачу: есть Base64-строка и нужно понять сколько это будет байтов при раскодировке. Из формул выше можно выразить исходное кол-во байт:

[Исходное кол-во байт] = 3/4 * [Кол-во байт в Base64] 
или
[Исходное кол-во байт] = 0.75 * [Кол-во байт в Base64] 

Например: Base64-строка размером 2000 байт, декодируется в 1500 байт (т.е. на 1/4 меньше);