Author Archive

Доступ к 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 меньше);

Преобразование в сетевой порядок расположения байт (Java)

HtoN (Host to Net) - узловой порядок в сетевой.

Функция htons преобразует узловой порядок расположения байтов в сетевой порядок расположения байтов. В short помещаются все возможные порты от 0 до 65535 (фактически от -32768 до 32767, т.к. в Java знаковый short):

static short htons(short val) {
    return (short) ((val & 0xff) << 8 
            | (val & 0xff00) >>> 8); // >>> - беззнаковый сдвиг
}

Функция htonl() преобразует узловой порядок расположения байтов в сетевой порядок расположения байтов. В int помещаются все возможные адреса IPv4 от 0.0.0.0 до 255.255.255.255 (фактически от -2 147 483 648 до 2 147 483 647, т.к. в Java знаковый int):

static int htonl(int val) {
    return (val & 0xff) << 24
            | (val & 0xff00) << 8
            | (val & 0xff0000) >> 8
            | (val & 0xff000000) >>> 24; // >>> - беззнаковый сдвиг
}
TAG: ,

Замер времени bash-скриптом

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

И на помощь приходит внешний замер времени с помощью bash-скрипта.

Сперва запоминаем время запуска (в наносекундах), потом фиксируем время окончания (в наносекундах) и находим разность времени окончания и начала.

Для перевода продолжительности из наносекунд в миллисекунды нужно разделить на один миллион.

Создайте скрипт (timer.sh) со следующим содержимым:

start_time="$(date +%s%N)"
# >>> тут пропишите запуск вашего процесса <<<
end_time="$(date +%s%N)"
elapsed="$(($end_time-$start_time))"
elapsed="$(($elapsed / 1000000))"
echo "Total of $elapsed milliseconds elapsed for process"

Удаление временно ненужных файлов

В очередной раз у разработчика закончилось место на жестком диске и начинается головоломка: "Что почистить?"

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

Внимание все операции нужно делать с осторожностью, чтобы не удалить ничего лишнего!

Рассмотрим очистку места на примере удаления директорий build и node_modules.

Для начала просканируем директории на наличие build и node_modules:

find . -name build -type d -maxdepth 2
find . -name node_modules -type d -maxdepth 2

maxdepth - максимальная глубина сканирования

Следующим шагом вычисляем размеры найденных директорий и анализируем:

find . -name build -type d -maxdepth 2 -exec du -sh {} \;
find . -name node_modules -type d -maxdepth 2 -exec du -sh {} \;

Далее генерируем скрипты удаления директорий (это еще не удаление, а echo):

find . -name build -type d -maxdepth 2 -exec echo rm -rf {} \;
find . -name node_modules -type d -maxdepth 2 -exec echo rm -rf {} \;

Внимание все операции нужно делать с осторожностью, чтобы не удалить ничего лишнего!

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

Место свободно!

Hibernate Envers проблема с размером приращения последовательности

В проект трехлетней давности не получилось прикрутить Hibernate Envers, возникала ошибка при запуске приложения:

Caused by: org.hibernate.MappingException: 
The increment size of the [hibernate_sequence] sequence is set to [1] 
in the entity mapping while the associated database sequence increment size is [50].

На том проекте Envers не сильно нужен был, поэтому обошлись без него.

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

В нашем проект используется не hibernate_sequence, а sequence_generator, но принцип тот же.

Проблема в том, что в базе данных у sequence шаг приращения 50 (в целях производительности), а в коде у какой-то сущности с данным sequence приращение равно 1. Эта сущность - revinfo.

Нужно переопределить RevInfo, добавив следующую сущность в ваш проект:

@Entity
@Table(name = "revinfo")
@RevisionEntity
public class CustomRevisionEntity {
 
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
    @SequenceGenerator(name = "sequenceGenerator")
    @RevisionNumber
    private long rev;
 
    @RevisionTimestamp
    private long timestamp;
 
    public long getRev() {
        return rev;
    }
 
    public void setRev(long rev) {
        this.rev = rev;
    }
 
    public long getTimestamp() {
        return timestamp;
    }
 
    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }
}

Ключевое изменение - это строки:

 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
 @SequenceGenerator(name = "sequenceGenerator")

Которые явно задают sequence, который будет использовать таблица revinfo. В нашем случае - это sequence_generator.

Если помогло, отпишитесь в комментах. 🙂

p.s. Дефолтная revinfo какая-то косячная.

Идентификатор ревизии почему-то integer, а не long.

Также не читабельна ревизия в виде long timestamp, но есть две альтернативы. Либо только дата, либо только время. Что еще неудобнее, а хотелось бы человекочитаемую дату со временем.

Совет: лучше переопределить revinfo, даже если у вас нет проблем с последовательностями.

Полное удаление метрик Prometheus

В один прекрасный день у нас на тестовом сервере поломался Prometheus, возможно, из-за частой смены времени на сервере (это нужно было для целей тестирования основной системы).

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

Способ заключается в удалении содержимого директории базы данных временных рядов /var/lib/prometheus/.

Расположение этой директории можно узнать с помощью команды:

sudo service prometheus status

Выполнив команду, вы увидите --storage.tsdb.path /var/lib/prometheus/ - путь к базе данных временных рядов.