Статьи

Dropwizard, MongoDB и Gradle Experimenting

Я создал небольшой проект, используя Dropwizard, MongoDB и Gradle.
На самом деле он начинался как экспериментальный кеш Guava в качестве буфера для отправки счетчиков в MongoDB (или любую другую БД).
Я также хотел попробовать Gradle с плагином MongoDB.
Затем я хотел создать какой-то интерфейс для проверки этой среды и решил попробовать DropWizard.
И вот как этот проект был создан.

Этот пост не является руководством по использованию любой из выбранных технологий.
Это небольшая витрина, которую я сделал в качестве эксперимента.
Я предполагаю, что есть некоторые недостатки и, возможно, я не использую все «лучшие практики».
Тем не менее, я считаю, что проект, с помощью этого поста, может стать хорошей отправной точкой для различных технологий, которые я использовал.
Я также попытался показать некоторые варианты дизайна, которые помогают достичь SRP, развязки, сплоченности и т. Д.

Я решил начать пост с описания варианта использования и того, как я его реализовал.
После этого я объясню, что я сделал с Gradle, MongoDB (и встроенными) и Dropwizard.

Прежде чем я начну, вот исходный код:
https://github.com/eyalgo/CountersBuffering

Вариант использования: счетчики с буфером

У нас есть несколько запросов ввода на наших серверах.
Во время процесса запроса мы выбираем «нарисовать» его с некоторыми данными (определенными логикой).
Некоторые запросы будут написаны Value-1, некоторые Value-2 и т. Д. Некоторые не будут отображаться вообще.
Мы хотим ограничить количество окрашенных запросов (по значению краски).
Чтобы иметь ограничение, для каждого значения краски мы знаем максимум, но также необходимо посчитать (на каждое значение краски) количество окрашенных запросов.
Поскольку система имеет несколько серверов, счетчики должны быть общими для всех серверов.

Задержка имеет решающее значение. Обычно мы получаем 4-5 миллисекунд на обработку запроса (для всего потока. Не только для рисования).
Поэтому мы не хотим, чтобы увеличение счетчиков увеличивало время ожидания.
Вместо этого мы сохраним буфер, клиент отправит «увеличение» в буфер.
Буфер будет периодически увеличивать хранилище с «объемным приращением».

Я знаю, что можно напрямую использовать Hazelcast, Couchbase или другие подобные быстрые БД в памяти.
Но для нашего варианта использования это было лучшее решение.

Принцип прост:

  • Зависимый модуль вызовет службу для увеличения счетчика для некоторого ключа
  • Реализация сохраняет буфер счетчиков на ключ
  • Это потокобезопасный
  • Запись происходит в отдельном потоке
  • Каждая запись будет делать большое увеличение
Счетчики высокого уровня дизайна

Счетчики высокого уровня дизайна

буфер

Для буфера я использовал Google Guava  кеш .

Структура буфера

Создание буфера 

private final LoadingCache<Counterable, BufferValue> cache;
...




this.cache = CacheBuilder.newBuilder()
.maximumSize(bufferConfiguration.getMaximumSize())
.expireAfterWrite(bufferConfiguration.getExpireAfterWriteInSec(), TimeUnit.SECONDS)
.expireAfterAccess(bufferConfiguration.getExpireAfterAccessInSec(), TimeUnit.SECONDS)
.removalListener((notification) -> increaseCounter(notification))
.build(new BufferValueCacheLoader());
...

( Счетный  описан ниже)

BufferValueCacheLoader  реализует интерфейс  CacheLoader .
Когда мы вызываем увеличение (см. Ниже), мы сначала получаем из кеша ключ.
Если ключ не существует, загрузчик возвращает значение.


BufferValueCacheLoader

public class BufferValueCacheLoader extends CacheLoader<Counterable, BufferValue> {
@Override
public BufferValue load(Counterable key) {
return new BufferValue();
}
}

BufferValue  оборачивает  AtomicInteger  (мне нужно было бы изменить его на Long в какой-то момент)

Увеличить счетчик


Увеличение счетчика и отправка, если пройден порог

public void increase(Counterable key) {
BufferValue meter = cache.getUnchecked(key);
int currentValue = meter.increment();
if (currentValue > threashold) {
if (meter.compareAndSet(currentValue, currentValue - threashold)) {
increaseCounter(key, threashold);
}
}
}

При увеличении счетчика мы сначала получаем текущее значение из кэша (с помощью загрузчика. Как описано выше). CompareAndSet  будет атомарно проверить, имеет то же значение (не модифицированное другой поток). Если это так, он обновит значение и вернет true. В случае успеха (вернул true) буфер вызывает программу обновления.


Посмотреть буфер

После разработки сервиса мне захотелось посмотреть способ просмотра буфера.
Поэтому я реализовал следующий метод, который используется внешним уровнем (ресурс Dropwizard).
Небольшой пример Java 8 Stream и лямбда-выражения.


Получение всех счетчиков в кеше

return ImmutableMap.copyOf(cache.asMap())
.entrySet().stream()
.collect(
Collectors.toMap((entry) -> entry.getKey().toString(),
(entry) -> entry.getValue().getValue()));

MongoDB

Я выбрал MongoDB по двум причинам:

  1. У нас есть аналогичная реализация в нашей системе, и мы решили использовать там MongoDB.
  2. Простота использования со встроенным сервером.

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

Я использовал  morphia  в качестве клиентского уровня MongoDB вместо непосредственного использования Java-клиента.
С Morphia вы создаете  dao , который является соединением с коллекцией MongoDB.
Вы также объявляете простой Java Bean (POJO), который представляет документ в коллекции.
Когда у вас есть дао, вы можете выполнять операции над коллекцией «Java-способом» с довольно простым API.
Вы можете иметь запросы и любые другие операции CRUD и многое другое.

У меня было две операции: увеличение счетчика и получение всех счетчиков.
Реализации сервисов не расширяют BasicDAO Morphia, но вместо этого имеют класс, который наследует его.
Я использовал  композицию  (по наследованию), потому что я хотел иметь больше поведения для обеих служб.

Чтобы соответствовать представлению ключа и скрыть способ его реализации от зависимого кода, я использовал интерфейс:  Counterterable  с помощью одного метода:  counterKey () .

public interface Counterable {
String counterKey();
}

DAO, который является составом внутри сервисов

final class MongoCountersDao extends BasicDAO<Counter, ObjectId> {
MongoCountersDao(Datastore ds) {
super(Counter.class, ds);
}
}

Увеличение счетчика


MongoCountersUpdater расширяет AbstractCountersUpdater, который реализует CountersUpdater

@Override
protected void increaseCounter(String key, int value) {
Query<Counter> query = dao.createQuery();
query.criteria("id").equal(key);
UpdateOperations<Counter> ops = dao.getDs().createUpdateOperations(Counter.class).inc("count", value);
dao.getDs().update(query, ops, true);
}@Override
protected void increaseCounter(String key, int value) {
Query<Counter> query = dao.createQuery();
query.criteria("id").equal(key);
UpdateOperations<Counter> ops = dao.getDs().createUpdateOperations(Counter.class).inc("count", value);
dao.getDs().update(query, ops, true);
}

Встроенный MongoDB

Для запуска тестов на уровне персистентности я хотел использовать базу данных в памяти.
Для этого есть плагин MongoDB.
С помощью этого плагина вы можете запустить сервер, просто создав его во время выполнения, или запустить в качестве цели в maven / task в Gradle.
https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo
https://github.com/sourcemuse/GradleMongoPlugin

Встроенный MongoDB на Gradle

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

dependencies {
// More dependencies here
testCompile 'com.sourcemuse.gradle.plugin:gradle-mongo-plugin:0.4.0'
}

Свойства настройки

mongo {
// logFilePath: The desired log file path (defaults to 'embedded-mongo.log')
logging 'console'
mongoVersion 'PRODUCTION'
port 12345
// storageLocation: The directory location from where embedded Mongo will run, such as /tmp/storage (defaults to a java temp directory)
}

Встроенные задачи MongoDB Gradle

startMongoDb  просто запустит сервер. Он будет работать, пока не остановится.
stopMongoDb  остановит это.
Тест startManagedMongoDb  , две задачи, которые запустят встроенный сервер до запуска тестов. Сервер завершит работу после завершения работы jvm (тестирование закончится)

Gradle

https://gradle.org/
Хотя я только касаюсь вершины айсберга, я начал видеть силу Gradle.
Это было даже не так сложно при настройке проекта.

Настройка Gradle

Сначала я создал проект Gradle в eclipse (после установки плагина).
Мне нужно было настроить зависимости. Очень простой. Прямо как мавен.

Один большой JAR выход

Когда я хочу создать одну большую банку из всех библиотек в Maven, я использую плагин Shade.
Я искал что-то похожее и обнаружил, что заглушка грейдл-од-фляга.
https://github.com/rholder/gradle-one-jar
Я добавил этот плагин
apply plugin: 'gradle-one-jar'
Добавил один jar в classpath:

buildscript {
repositories { mavenCentral() }
dependencies {
classpath 'com.sourcemuse.gradle.plugin:gradle-mongo-plugin:0.4.0'
classpath 'com.github.rholder:gradle-one-jar:1.0.4'
}
}

И добавил задачу:

mainClassName = 'org.eyalgo.server.dropwizard.CountersBufferApplication'
task oneJar(type: OneJar) {
mainClass = mainClassName
archiveName = 'counters.jar'
mergeManifestFromJar = true
}

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

Dropwizard

Dropwizard  — это стек библиотек, который позволяет быстро создавать веб-серверы.
Он использует Jetty для HTTP и Джерси для REST. У него есть другие зрелые библиотеки для создания сложных сервисов.
Может использоваться как легко разработанный микросервис.

Как я объяснил во введении, я не буду охватывать все функции и / или настройки Dropwizard.
Есть много сайтов для этого.
Я кратко расскажу о действиях, которые я сделал для запуска приложения.

Gradle Run Задача

run { args 'server', './src/main/resources/config/counters.yml' }
Первый аргумент — сервер. Второй аргумент — это местоположение файла конфигурации.
Если вы не дадите Dropwizard первый аргумент, вы получите приятное сообщение об ошибке возможных опций.

positional arguments:
{server,check} available commands

I already showed how to create one jar in the Gradle section.

Configuration

In Dropwizard, you setup the application using a class that extends Configuration.
The fields in the class should align to the properties in the yml configuration file.

It is a good practice to put the properties in groups, based on their usage/responsibility.
For example, I created a group for mongo parameters.

In order for the configuration class to read the sub groups correctly, you need to create a class that align to the properties in the group.
Then, in the main configuration, add this class as a member and mark it with annotation:@JsonProperty.
Example:

@JsonProperty("mongo")
private MongoServicesFactory servicesFactory = new MongoServicesFactory();
@JsonProperty("buffer")
private BufferConfiguration bufferConfiguration = new BufferConfiguration();

Example: Changing the Ports

Here’s part of the configuration file that sets the ports for the application.

server:
adminMinThreads: 1
adminMaxThreads: 64
applicationConnectors:
- type: http
port: 9090
adminConnectors:
- type: http
port: 9091

Health Check

Dropwizard gives basic admin API out of the box. I changed the port to 9091.
I created a health check for MongoDB connection.
You need to extend HealthCheck and implement check method.

private final MongoClient mongo;
...
protected Result check() throws Exception {
try {
mongo.getDatabaseNames();
return Result.healthy();
} catch (Exception e) {
return Result.unhealthy("Cannot connect to " + mongo.getAllAddress());
}
}

Other feature are pretty much self-explanatory or simple as any getting started tutorial.

Ideas for Enhancement

The are some things I may try to add.

  • Add tests to the Dropwizard section.
    This project started as PoC, so I, unlike usually, skipped the tests in the server part.
    Dropwizard has Testing Dropwizard, which I want to try.
  • Different persistence implementation. (couchbase? Hazelcast?).
  • Injection using Google Guice. And with help of that, inject different persistence implementation.

That’s all.
Hope that helps.

Source code: https://github.com/eyalgo/CountersBuffering