Русский
Русский
English
Статистика
Реклама

Testcontainers

Как подружить Redis Cluster c Testcontainers?

20.06.2021 12:09:43 | Автор: admin
В 26-м выпуске NP-полного подкаста я рассказывал, что начал переводить один из своих сервисов из Redis Sentinel на Redis Cluster. На этой неделе я захотел потестировать данный код, и, конечно же, выбрал Testcontainers для этого. К сожалению, Redis Cluster в тестовых контейнерах не завелся из коробки, и мне пришлось вставить несколько костылей. О них и пойдет речь далее.



Вводные


Сначала я бы хотел описать все вводные, а потом рассказать про костыли. Мой проект построен на Spring Boot. Для взаимодействия с редисом используется Lettuce клиент. Для тестирования testcontainers-java с JUnit. Версия обоих редисов 6. В общем, всё типичное, нет ничего особенного с точки зрения стека.

Если кто-то еще не знаком с testcontainers, то пара слов о них. Это библиотека для интеграционного тестирования. Она построена на другой библиотеке https://github.com/docker-java/docker-java. Тестконтейнеры, по сути говоря, помогают быстро и просто запускать контейнеры с разными зависимостями в ваших интеграционных тестах. Обычно это базы данных, очереди и другие сложные системы. Некоторые люди используют testcontainers и для запуска своих сервисов, от которых зависит тестируемое приложение (чтобы тестировать микросервисное взаимодействие).

Про Redis Cluster


Redis Cluster это одна из нескольких реализаций распределнного режима Редиса. К сожалению, в Редисе нет единого правильного способа, как масштабировать базу. Есть Sentinel, есть Redis Cluster, а еще ребята активно разрабатывают RedisRaft распредеделенный редис на базе протокола консенсуса Raft (у них там своя реализация, которая, как они сами заявляют, не совсем каноничный Рафт, но конкретно для Redis самое то).

В целом, про Redis Cluster есть две замечательных статьи на официальном сайте https://redis.io/topics/cluster-tutorial и https://redis.io/topics/cluster-spec. Большинство деталей описано там.

Для использования Redis Cluster в testcontainers важно знать несколько вещей из документации. Во-первых, Redis Cluster использует gossip протокол поэтому каждый узел кластера имеет TCP-соединение со всеми другими узлами. Поэтому, между нодами должна быть сетевая связность, даже в тестах.

Вторая важная штука, которую надо знать при тестировании это наличие в Redis Cluster bootstrap узлов для конфигурации. То есть, вы в настройках можете задать лишь подмножество узлов, которые будут использоваться для старта приложения. В последствие, Redis клиент сам получит Топологию кластера через взаимодействие с Редисом. Исходя из этого, получается вторая особенность тестируемое приложение должно иметь сетевую связность с теми Redis URI, которые будут аннонсированы со стороны редис кластера (кстати, эти адреса можно сконфигурировать через cluster-announce-port и cluster-announce-ip).

Про костыли с Redis Cluster и testcontainers


Для тестирования я выбрал довольно популярный docker-образ https://github.com/Grokzen/docker-redis-cluster. Он не подходит для продакшена, но очень прост в использовании в тестах. Особенность этого образа все Редисы (а их 6 штук, по умолчанию 3 мастера и 3 слейва) будут подняты в рамках одного контейнера. Поэтому, мы автоматически получаем сетевую связность между узлами кластера из коробки. Осталось решить вторую из двух проблем, связанную с получением приложением топологии кластера.

Я не хотел собирать свой docker-образ, а выбранный мной image не предоставляет возможности задавать настройки cluster-announce-port и cluster-announce-ip. Поэтому, если ничего не делать дополнительно, при запуске тестов вы увидите примерно такие ошибки:

Unable to connect to [172.17.0.3/<unresolved>:7003]: connection timed out: /172.17.0.3:7003


Ошибка означает, что мы со стороны приложения пытаеся приконнектится к Узлу редис кластера, используя IP докер контейнера и внутренний порт (порт 7003 используется данным узлом, но наружу он отображается на какой-то случайный порт, который мы и должны использовать в нашем приложении; внутренний порт, по понятным причинам, не доступен из вне). Что касается данного IP-адреса он доступен для приложения, если это Linux, и он не доступен для приложения, если это MacOs/Windows (из-за особенностей реализации докера на этих ОС).

Решение проблемы (а-ка костыль) я собрал по частичкам из разных статей. А давайте сделаем NAT RedisURI на стороне приложения. Ведь это нужно именно для тестов, и тут не так страшно вставлять такой ужас. Решение, на самом деле, состоит из пары строк (огромное спасибо Спрингу и Lettuce, где можно сконфигурировать практически всё, только и успевай, как переопределять бины).

public SocketAddress resolve(RedisURI redisURI) {    Integer mappedPort = redisClusterNatPortMapping.get(redisURI.getPort());    if (mappedPort != null) {        SocketAddress socketAddress = redisClusterSocketAddresses.get(mappedPort);        if (socketAddress != null) {            return socketAddress;        }        redisURI.setPort(mappedPort);    }    redisURI.setHost(DockerClientFactory.instance().dockerHostIpAddress());    SocketAddress socketAddress = super.resolve(redisURI);    redisClusterSocketAddresses.putIfAbsent(redisURI.getPort(), socketAddress);    return socketAddress;}


Полный код выложен на гитхаб https://github.com/Hixon10/spring-redis-cluster-testcontainers.

Идея кода супер простая. Будем хранить две Map. В первой маппинг между внутренними портами редиса (7000..7005) и теми, что доступны для приложения (они могут быть чем-то типа 51343, 51344 и тд). Во-второй внешние порты (типа, 51343) и SocketAddress, полученный для них. Теперь, когда мы получаем от Редиса при обновлении топологии что-то типа 172.17.0.3:7003, мы сможем легко найти нужный внешний порт, по которому сможем найти SocketAddress и переиспользовать его. То есть, с портами проблема решена. А что с IP?

С IP-адресом всё просто. Тут нам на помощь приходят Тест контейнеры в которых есть утилитный метод DockerClientFactory.instance().dockerHostIpAddress(). Для MacOs/Windows он будет отдавать localhost, а для linux IP-адрес контейнера.

Выводы


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

История о PostgreSQL 13, Testcontainers и багах

29.03.2021 22:09:33 | Автор: admin

Привет Хабр!

Несмотря на то, что на текущем месте работы взаимодействовать с PostgreSQL приходится значительно меньше, я продолжаю поддерживать свою библиотеку pg-index-health. Недавно я зарелизил версию 0.3.1, ключевой особенностью которой стала поддержка PostgreSQL 13.

Эта история началась достаточно давно, ещё в октябре 2020. В процессе мне пришлось отказаться от embedded версии СУБД в тестах, перейти на Testcontainers, столкнуться с ошибкой в PG13 и даже немного пообщаться в переписке с одним из разработчиков Постгреса... Но обо всем по порядку.

Изначально для тестирования библиотеки я использовал embedded PostgreSQL. Я уже рассказывал об этом ранее. В целом это был неплохой вариант, но он завязан на чужие бинарники и их релизный цикл. Артефакта с 13-й версией долгое время не было (зарелизили только 13 ноября, спустя полтора месяца с момента начала моих экспериментов). Очевидно, что дальше жить с таким вариантом было крайне неудобно.

Альтернативный подход тоже был известен Testcontainers (спасибо Николаю Кондратьеву за всю подготовительную работу), но мне до последнего момента не хотелось на него переходить. Дело в том, что Testcontainers это дополнительный достаточно толстый слой между приложением и базой, запускаемой в Docker-контейнере. На MacOS и Windows такое решение время от времени ломается. И я столкнулся с этим. Дважды. Сначала на MacOS, затем на Windows.

Но что более печально, в Github Actions тесты с использованием Testcontainers можно запускать только на Ubuntu. По крайней мере у меня не получилось сделать это для Windows и MacOS. Если вы знаете, как настроить такую связку, буду премного благодарен за совет.

В какой-то момент времени все тесты в моём проекте оказались переведены на Testcontainers и успешно проходили на 11-й и 12-й версиях. Затем я переключил проект на ванильный образ с 13.0... И тут меня ждал облом... Тесты на оценку bloat'а индексов упали. В отдельной ветке я подготовил проект для демонстрации этой ошибки.

Ошибки в тестах на PostgreSQL 13.0Ошибки в тестах на PostgreSQL 13.0

Первичный анализ ничего не дал. Никаких принципиальных изменений, которые бы ломали обратную совместимость между 12-й и 13-й версиями не было. Углублённое чтение release notes познакомило меня со сжатием дубликатов в b-tree индексах в новой версии - фичей, которую я ранее как-то совсем упустил (да, так тоже бывает).

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

И тогда я решил написать одному из соавторов доработок по сжатию дубликатов в b-tree индексах - Анастасии Лубенниковой. И нет, здесь happy end'а не случилось: у Анастасии не было для меня готового решения, а потом навалилась работа, и на долгих два месяца я забросил проект...

Вернуться к PG 13 удалось только в начале января 2021. И я решил попробовать недавно вышедшее минорное обновление 13.1. Представьте себе моё удивление и радость, когда на этой версии все тесты прошли. На 13.1 оценка bloat'а индексов снова стала работать корректно!

Мораль этой истории проста и банальна: не стоит тащить новую версию любимой СУБД сразу в production. Ошибки бывают везде. Главное, чтобы они оперативно исправлялись. Ну а лично для меня приятным открытием стало то, что моё детище можно использовать как инструмент для acceptance testing (вернее, как его небольшую часть).


А что там со сжатием дубликатов в b-tree, спросите вы? Может быть, оно способно полностью решить проблему с null value в индексах?.. Увы, нет.

Сжатие дубликатов работает хорошо, даже замечательно. Вы можете проверить это самостоятельно. По ссылке подготовлен скрипт для инициализации и наполнения небольшой тестовой БД и команды для запуска двух разных версий СУБД в контейнере.

Init-db скрипт
create table if not exists test(  id bigserial primary key,  fld varchar(255),  mark varchar(255),  nil varchar(255));insert into testselect data.id, case when data.id % 2 = 0 then now()::text else null end, case when data.id % 2 = 0 then 'test_string'::text else null end, nullfrom generate_series(1, 100000) as data(id);create index if not exists i_test_fld_with_nulls on test (fld);create index if not exists i_test_fld_without_nulls on test (fld) where fld is not null;create index if not exists i_test_mark_with_nulls on test (mark);create index if not exists i_test_mark_without_nulls on test (mark) where mark is not null;create index if not exists i_test_nil_with_nulls on test (nil);create index if not exists i_test_nil_without_nulls on test (nil) where nil is not null;
Команды для запуска в Docker
docker run --name postgres-13 -e POSTGRES_USER=testuser -e POSTGRES_PASSWORD=testpwd -e POSTGRES_DB=testdb -d -p 5432:5432 -v /absolute/path/to/initdb.sql:/docker-entrypoint-initdb.d/initdb.sql postgres:13.2docker run --name postgres-12 -e POSTGRES_USER=testuser -e POSTGRES_PASSWORD=testpwd -e POSTGRES_DB=testdb -d -p 6432:5432 -v /absolute/path/to/initdb.sql:/docker-entrypoint-initdb.d/initdb.sql postgres:12.6
psql -U testuser -d testdb
SQL запрос для оценки размера индексов
select x.indrelid::regclass as table_name, x.indexrelid::regclass as index_name, pg_size_pretty(pg_relation_size(x.indexrelid)) as index_sizefrom pg_index xjoin pg_stat_all_indexes psai on x.indexrelid = psai.indexrelid and psai.schemaname = 'public'order by 1,2;

На выходе получим следующее:

Результаты в текстовом виде

PostgreSQL 12

table_name

index_name

index_size

test

test_pkey

2208 kB

test

i_test_fld_with_nulls

3552 kB

test

i_test_fld_without_nulls

2456 kB

test

i_test_mark_with_nulls

2664 kB

test

i_test_mark_without_nulls

1568 kB

test

i_test_nil_with_nulls

2224 kB

test

i_test_nil_without_nulls

8192 bytes

PostgreSQL 13

table_name

index_name

index_size

test

test_pkey

2208 kB

test

i_test_fld_with_nulls

704 kB

test

i_test_fld_without_nulls

368 kB

test

i_test_mark_with_nulls

696 kB

test

i_test_mark_without_nulls

360 kB

test

i_test_nil_with_nulls

696 kB

test

i_test_nil_without_nulls

8192 bytes

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

Демонстрация сжатия дубликатовДемонстрация сжатия дубликатов

Как видите, выбрасывать null value из индекса целиком по-прежнему крайне выгодно, и этой диагностикой не стоит пренебрегать.


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

Подробнее..

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru