Статьи

Контейнерное приложение Spring Data Cassandra

Я продолжаю свой путь изучения Docker. Я все еще держу это простым в этом пункте. На этот раз я собираюсь заняться преобразованием приложений Spring и Cassandra для использования контейнеров вместо локального запуска на хост-компьютере. Точнее, используя Spring Data Cassandra для сортировки приложения.

Я хотел бы посмотреть на это изменение некоторое время назад. Я написал достаточное количество сообщений на Cassandra, и каждый раз мне приходилось cd к нужному каталогу или иметь ярлык для его запуска. Я думаю, что это не так уж важно, но было несколько других вещей. Например, удаление и воссоздание ключей, чтобы я мог протестировать свое приложение с нуля. Теперь я просто удаляю контейнер и перезапускаю его. В любом случае, это полезно для меня!

Cassandra

Этот пост будет немного отличаться от моего предыдущего поста « Использование Docker для помещения существующего приложения в контейнеры» . Вместо этого я сконцентрируюсь немного больше на стороне приложения и удалю промежуточные этапы использования только Docker, а вместо этого перейду прямо к Docker Compose.

Контейнеры, контейнеры, контейнеры

Я думаю, что лучше всего начинать со стороны контейнера проекта, поскольку приложение зависит от конфигурации контейнера Cassandra.

Поехали!

1
2
3
4
5
FROM openjdk:10-jre-slim
LABEL maintainer="Dan Newton"
ARG JAR_FILE
ADD target/${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

Здесь мало что происходит. Этот Dockerfile создает образ приложения Spring, который через несколько секунд будет помещен в контейнер.

Далее идет файл docker-compose . Это создаст и приложение Spring, и контейнеры Cassandra:

01
02
03
04
05
06
07
08
09
10
version: '3'
services:
  app:
    build:
      context: .
      args:
        JAR_FILE: /spring-data-cassandra-docker-1.0.0.jar
    restart: always
  cassandra:
image: "cassandra"

Опять же, здесь не так уж много. Контейнер app создает приложение Spring с использованием ранее Dockerfile . Контейнер cassandra вместо этого полагается на существующее изображение, соответственно названное cassandra .

Одна вещь, которая выделяется, это то, что свойство restart установлено в always . Это была моя ленивая попытка узнать, сколько времени потребуется Кассандре, чтобы начать, и тот факт, что все контейнеры, запущенные с docker-compose запускаются одновременно. Это приводит к ситуации, когда приложение пытается подключиться к Cassandra без его готовности. К сожалению, это приводит к смерти приложения. Я надеялся, что у него будет некоторая возможность повторения для встроенного подключения … Но это не так.

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

Тире кода

Я сказал, что этот пост будет больше фокусироваться на коде приложения, что и будет, но мы не собираемся углубляться во все, что я вкладываю в это приложение, и как использовать Cassandra. Для такого рода информации, вы можете взглянуть на мои старые посты, которые я приведу в конце. Что мы будем делать, так это изучить конфигурационный код, который создает компоненты, которые подключаются к Cassandra.

Сначала давайте рассмотрим ClusterConfig который настраивает кластер Cassandra:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Configuration
public class ClusterConfig extends AbstractClusterConfiguration {
 
  private final String keyspace;
  private final String hosts;
 
  ClusterConfig(
      @Value("${spring.data.cassandra.keyspace-name}") String keyspace,
      @Value("${spring.data.cassandra.contact-points}") String hosts) {
    this.keyspace = keyspace;
    this.hosts = hosts;
  }
 
  @Bean
  @Override
  public CassandraClusterFactoryBean cluster() {
 
    RetryingCassandraClusterFactoryBean bean = new RetryingCassandraClusterFactoryBean();
 
    bean.setAddressTranslator(getAddressTranslator());
    bean.setAuthProvider(getAuthProvider());
    bean.setClusterBuilderConfigurer(getClusterBuilderConfigurer());
    bean.setClusterName(getClusterName());
    bean.setCompressionType(getCompressionType());
    bean.setContactPoints(getContactPoints());
    bean.setLoadBalancingPolicy(getLoadBalancingPolicy());
    bean.setMaxSchemaAgreementWaitSeconds(getMaxSchemaAgreementWaitSeconds());
    bean.setMetricsEnabled(getMetricsEnabled());
    bean.setNettyOptions(getNettyOptions());
    bean.setPoolingOptions(getPoolingOptions());
    bean.setPort(getPort());
    bean.setProtocolVersion(getProtocolVersion());
    bean.setQueryOptions(getQueryOptions());
    bean.setReconnectionPolicy(getReconnectionPolicy());
    bean.setRetryPolicy(getRetryPolicy());
    bean.setSpeculativeExecutionPolicy(getSpeculativeExecutionPolicy());
    bean.setSocketOptions(getSocketOptions());
    bean.setTimestampGenerator(getTimestampGenerator());
 
    bean.setKeyspaceCreations(getKeyspaceCreations());
    bean.setKeyspaceDrops(getKeyspaceDrops());
    bean.setStartupScripts(getStartupScripts());
    bean.setShutdownScripts(getShutdownScripts());
 
    return bean;
  }
 
  @Override
  protected List getKeyspaceCreations() {
    final CreateKeyspaceSpecification specification =
        CreateKeyspaceSpecification.createKeyspace(keyspace)
            .ifNotExists()
            .with(KeyspaceOption.DURABLE_WRITES, true)
            .withSimpleReplication();
    return List.of(specification);
  }
 
  @Override
  protected String getContactPoints() {
    return hosts;
  }
}

Там не так уж много, но было бы еще меньше, если бы Spring повторил первоначальное подключение к Cassandra. В любом случае, давайте оставим эту часть на несколько минут и сосредоточимся на других моментах в этом классе.

Первоначальной причиной, по которой я создал ClusterConfig было создание пространства ключей, которое будет использовать приложение. Для этого getKeyspaceCreations был переопределен. Когда приложение подключается, оно выполняет запрос, определенный в этом методе, для создания пространства ключей.

Если в этом нет необходимости, и пространство ключей было создано другим способом, например, сценарием, выполняемым как часть создания контейнера Cassandra, вместо этого можно было бы полагаться на автоконфигурацию Spring Boot. Это фактически позволяет настроить все приложение с помощью свойств, определенных в application.properties и ничего более. Увы, это не должно было быть.

Поскольку мы определили AbstractClusterConfiguration , Spring Boot отключит его настройку в этой области. Поэтому нам нужно определить contactPoints (я назвал переменную hosts ) вручную, переопределив метод getContactPoints . Первоначально это было определено только в application.properties . Я понял, что мне нужно внести это изменение, как только я начал получать следующую ошибку:

1
All host(s) tried for query failed (tried: localhost/127.0.0.1:9042 (com.datastax.driver.core.exceptions.TransportException: [localhost/127.0.0.1:9042] Cannot connect))

До того, как я создал ClusterConfig адрес был cassandra а не localhost .

Не нужно настраивать другие свойства для кластера, так как настройки Spring достаточно хороши для этого сценария.

Я так много упомянул application.properties , я должен показать вам, что в нем.

1
2
3
spring.data.cassandra.keyspace-name=mykeyspace
spring.data.cassandra.schema-action=CREATE_IF_NOT_EXISTS
spring.data.cassandra.contact-points=cassandra

keyspace-name и contact-points уже появились, поскольку они связаны с настройкой кластера. schema-action необходимо для создания таблиц на основе сущностей в проекте. Здесь нам больше ничего не нужно делать, так как автоконфигурация все еще работает в этой области.

Тот факт, что значение contact-points установлено на cassandra , очень важно. Это доменное имя происходит от имени, данного контейнеру, в данном случае, cassandra . Поэтому можно использовать либо cassandra , либо фактический IP-адрес контейнера. Доменное имя определенно проще, так как оно всегда будет статичным между развертываниями. Просто чтобы проверить эту теорию, вы можете изменить имя контейнера cassandra на любое cassandra и он все равно будет подключаться, если вы также измените его в application.properties .

Вернуться к ClusterConfig . Точнее, cluster боб. Я вставил приведенный ниже код, чтобы на него было проще смотреть:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Configuration
public class ClusterConfig extends AbstractClusterConfiguration {
 
  // other stuff
 
  @Bean
  @Override
  public CassandraClusterFactoryBean cluster() {
 
    RetryingCassandraClusterFactoryBean bean = new RetryingCassandraClusterFactoryBean();
 
    bean.setAddressTranslator(getAddressTranslator());
    bean.setAuthProvider(getAuthProvider());
    bean.setClusterBuilderConfigurer(getClusterBuilderConfigurer());
    bean.setClusterName(getClusterName());
    bean.setCompressionType(getCompressionType());
    bean.setContactPoints(getContactPoints());
    bean.setLoadBalancingPolicy(getLoadBalancingPolicy());
    bean.setMaxSchemaAgreementWaitSeconds(getMaxSchemaAgreementWaitSeconds());
    bean.setMetricsEnabled(getMetricsEnabled());
    bean.setNettyOptions(getNettyOptions());
    bean.setPoolingOptions(getPoolingOptions());
    bean.setPort(getPort());
    bean.setProtocolVersion(getProtocolVersion());
    bean.setQueryOptions(getQueryOptions());
    bean.setReconnectionPolicy(getReconnectionPolicy());
    bean.setRetryPolicy(getRetryPolicy());
    bean.setSpeculativeExecutionPolicy(getSpeculativeExecutionPolicy());
    bean.setSocketOptions(getSocketOptions());
    bean.setTimestampGenerator(getTimestampGenerator());
 
    bean.setKeyspaceCreations(getKeyspaceCreations());
    bean.setKeyspaceDrops(getKeyspaceDrops());
    bean.setStartupScripts(getStartupScripts());
    bean.setShutdownScripts(getShutdownScripts());
 
    return bean;
  }
 
  // other stuff
}

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

То, что я сделал, на самом деле довольно просто, но сам код не очень хорош. cluster метод представляет собой точную копию переопределенной версии из AbstractClusterConfiguration , за исключением RetryingCassandraClusterFactoryBean (мой собственный класс). Оригинальная функция использовала вместо этого CassandraClusterFactoryBean (класс Spring).

Ниже приведено RetryingCassandraClusterFactoryBean :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class RetryingCassandraClusterFactoryBean extends CassandraClusterFactoryBean {
 
  private static final Logger LOG =
      LoggerFactory.getLogger(RetryingCassandraClusterFactoryBean.class);
 
  @Override
  public void afterPropertiesSet() throws Exception {
    connect();
  }
 
  private void connect() throws Exception {
    try {
      super.afterPropertiesSet();
    } catch (TransportException | IllegalArgumentException | NoHostAvailableException e) {
      LOG.warn(e.getMessage());
      LOG.warn("Retrying connection in 10 seconds");
      sleep();
      connect();
    }
  }
 
  private void sleep() {
    try {
      Thread.sleep(10000);
    } catch (InterruptedException ignored) {
    }
  }
}

Метод afterPropertiesSet в исходном CassandraClusterFactoryBean принимает его значения и создает представление кластера Cassandra, наконец, делегируя его драйверу Datastax Java. Как я уже упоминал на протяжении всего поста. Если не удается установить соединение, будет выдано исключение, и если оно не будет перехвачено, приложение завершит работу. В этом весь смысл приведенного выше кода. Он оборачивает afterPropertiesSet в блок try-catch, указанный для исключений, которые могут быть выброшены.

sleep добавлен, чтобы дать Кассандре некоторое время для фактического запуска. Нет смысла пытаться восстановить соединение сразу, когда предыдущая попытка не удалась.

Используя этот код, приложение в конечном итоге подключится к Cassandra.

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

1
mvn clean install && docker-compose up

Затем создается образ приложения Spring, и оба контейнера запускаются.

Вывод

Мы рассмотрели, как поместить приложение Spring, которое подключается к базе данных Cassandra, в контейнеры. Один для приложения, а другой для Кассандры. Образ приложения создается из кода проекта, а образ Cassandra взят из Docker Hub. Название изображения — cassandra чтобы никто не забыл. В общем, соединение двух контейнеров было относительно простым, но приложению требовались некоторые настройки, чтобы разрешить повторные попытки при подключении к Cassandra, работающему в другом контейнере. Это сделало код немного уродливым, но он работает по крайней мере … Благодаря коду, написанному в этом посте, у меня теперь есть другое приложение, которое мне не нужно устанавливать на моей собственной машине.

Код, используемый в этом посте, можно найти на моем GitHub .

Если вы нашли этот пост полезным, вы можете подписаться на меня в Twitter на @LankyDanDev, чтобы не отставать от моих новых сообщений.

Ссылки на мои сообщения Spring Data Cassandra

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

Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Контейнерные приложения Spring Data Cassandra

Мнения, высказанные участниками Java Code Geeks, являются их собственными.