Рубрики
Без рубрики

Разрушение хорошего Сервиса С помощью Heroku

В течение последних десяти лет я участвовал в проектах, которые я бы классифицировал как “прикладные… С тегами heroku, java, spring, spring boot.

В течение последних десяти лет я принимал участие в проектах, которые я бы классифицировал как “инициативы по модернизации приложений”. ” Целью таких усилий является замена устаревшего приложения или службы с использованием более современных (и часто более поддерживаемых) фреймворков, шаблонов проектирования и языков.

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

  1. Не заменяйте монолит другим монолитом, замаскированным под модернизированный сервис.
  2. Избегайте принятия плохих моделей данных для включения в модернизированную систему.
  3. Никогда не предполагайте, что существующая логика программы верна на 100%.

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

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

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

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

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

Где Все Идет Наперекосяк

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

В таблице CUSTOMERS хранится информация о клиентах. Столбец идентификатора в таблице связан с таблицей ЗАКАЗОВ, сопоставляя заказ с клиентом. Столбец идентификатора клиента также связан с таблицей ПЛАТЕЖЕЙ.

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

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

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

Что еще больше усугубляет проблему, так это тот факт, что масштабирование вверх и вниз для удовлетворения потребительского спроса требует, чтобы все API (отмеченные выше) масштабировались в унисон. В зависимости от базового дизайна параметры масштабирования могут быть даже ограничены вертикальным масштабированием, что вызывает решение “бросить аппаратное обеспечение на проблему”.

Именно здесь подтверждается урок “Не заменяйте монолит другим монолитом, замаскированным под модернизированный сервис”.

Делать Что-То Лучше

Используя тот же пример, подумайте об этом: что, если для инициативы по модернизации приложений был использован следующий дизайн?

Благодаря такому дизайну вводятся три новых микросервиса для размещения их домена, принадлежащего приложению. Самое главное, что каждый микросервис и его выделенные объекты имеют свою собственную базу данных.

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

В качестве примера рассмотрим вариант использования размещения заказа. Заказ предоставляет известные данные клиента клиентскому API с помощью сообщения. Клиентский API обработает запрос и либо вернет существующий CustomerDTO (DTO – это объект передачи данных), либо создаст новый CustomerDTO в ответ на исходный запрос.

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

Когда Платежный API ответит, новый заказ может быть сохранен в соответствующей базе данных и подтвержден клиентом, отправившим запрос.

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

Использование Heroku, чтобы избежать хорошего сервиса

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

Каждая из трех служб содержит свою собственную базу данных Heroku Postgres и службу загрузки Spring. Сервис CloudAMQP (RabbitMQ) был добавлен в приложение предварительного заказа, чтобы сделать этот пример максимально простым. Облако API WSO2 является частью дизайна, но не будет задокументировано в этой статье.

На панели управления Heroku появились три приложения, как показано ниже:

Создание Примеров Таблиц

Следующий SQL был использован для создания базовых таблиц. Они могут быть использованы для проверки функциональности этих служб:

CREATE TABLE orders (
  id INT PRIMARY KEY NOT NULL,
  customer_id INT NOT NULL,
  payment_id INT NOT NULL,
  order_date timestamp NOT NULL,
  description VARCHAR(255) 
);
​
CREATE TABLE customers (
  id INT PRIMARY KEY NOT NULL,
  email_address VARCHAR(255) NOT NULL,
  name VARCHAR(255),
  UNIQUE (email_address)
);
​
CREATE TABLE payments (
  id INT PRIMARY KEY NOT NULL,
  transaction_id VARCHAR(36) NOT NULL,
  amount DECIMAL(12,2),
  customer_id INT NOT NULL
);

Помните, что каждая команда CREATE TABLE выполнялась для базы данных PostgreSQL, связанной с соответствующим микросервисом.

Обработка запроса на заказ

Рассмотрите следующую полезную нагрузку запроса заказа:

{
   "description" : "Sample Order #4",
   "emailAddress" : "bjohnson@example.com",
   "name" : "Brian Johnson",
   "amount" : 19.99
}

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

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

Запрос клиента

Чтобы запросить информацию о клиенте, следующая полезная нагрузка CustomerDTO может быть помещена в очередь запросов:

{
   "emailAddress" : "bjohnson@example.com",
   "name" : "Brian Johnson"
}

В API Order следующий метод использует CloudAMQP в Heroku, концепцию прямого обмена и spring-boot-starter-amqp в Spring Boot:

public CustomerDto getCustomer(String emailAddress, String name) {
    CustomerDto customerDto = new CustomerDto();
    customerDto.setEmailAddress(emailAddress);
    customerDto.setName(name);
​
    return rabbitTemplate.convertSendAndReceiveAsType(customerDirectExchange.getName(),
            messagingConfigurationProperties.getCustomerRoutingKey(),
            customerDto,
            new ParameterizedTypeReference<>() {});
}

В примере этот запрос является блокирующим запросом — это означает, что обработка из API заказа ожидает, пока API клиента не предоставит ответ.

В клиентском API есть прослушиватель, ожидающий запросов на customerDirectExchange:

@RabbitListener(queues = "#{messagingConfigurationProperties.customerRequestQueue}")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public CustomerDto receive(CustomerDto customerDto) {
    log.debug("CustomerProcessor: receive(customerDto={})", customerDto);
​
    Customer customer = customerRepository.findByEmailAddressEquals(customerDto.getEmailAddress());
​
    if (customer != null) {
        log.debug("Found existing customer={}", customer);
        // return customer as a CustomerDto
    } else {
        log.info("Creating new customer={}", customerDto);
        // return new customer as a CustomerDto
    }
​
    log.debug("customerDto={}", customerDto);
    return customerDto;
}

В этом примере объект customer содержит следующую информацию:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class CustomerDto {
    private int id;
    private String emailAddress;
    private String Name;
}

Запрашивающий Платеж

Используя платежи, можно использовать ту же схему для запроса платежа:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class PaymentDto {
    private int id;
    private String transactionId;
    private BigDecimal amount;
    private int customerId;
}

Свойство CustomerID является результатом шаблона запроса/ответа. Конечно, свойство id не будет установлено до тех пор, пока обработка не будет завершена с помощью API платежей, который использует другой очень простой пример оплаты:

@RabbitListener(queues = "#{messagingConfigurationProperties.paymentRequestQueue}")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public PaymentDto receive(PaymentDto paymentDto) {
    log.debug("PaymentProcessor: receive(paymentDto={})", paymentDto);
​
    Payment payment = new Payment();
    payment.setAmount(paymentDto.getAmount());
    payment.setCustomerId(paymentDto.getCustomerId());
    payment.setTransactionId(UUID.randomUUID().toString());
    paymentRepository.save(payment);
    paymentDto.setId(payment.getId());
    paymentDto.setTransactionId(payment.getTransactionId());

    return paymentDto;
}

Отправка заказа и завершение транзакции

После завершения транзакции процесс размещения заказа может быть завершен с помощью клиента Postman или даже простой команды cURL:

curl --location --request POST 'https://jvc-order.herokuapp.com/orders' \
--header 'Content-Type: application/json' \
--data-raw '{
    "description" : "Sample Order #4",
    "emailAddress" : "bjohnson@example.com",
    "name" : "Brian Johnson",
    "amount" : 19.99
}'

API заказа примет запрос POST и вернет статус HTTP 201 (Создан) вместе со следующей полезной нагрузкой:

{
   "id": 4,
   "customerId": 4,
   "paymentId": 4,
   "orderDate": "2021-06-07T04:31:52.497082",
   "description": "Sample Order #4"
}

Стандартные RESTful API для каждого из трех микросервисов позволяют извлекать полные результаты данных полезной нагрузки.

Ниже приведен пример вызова клиентского API:

ПОЛУЧИТЬ https://jvc-customer.herokuapp.com/customers/4

Это возвращает следующую полезную нагрузку и статус HTTP 200 (OK):

{
    "id": 4,
    "emailAddress": "bjohnson@example.com",
    "name": "Brian Johnson"
}

Ниже приведен пример вызова платежного API:

ПОЛУЧИТЬ https://jvc-payment.herokuapp.com/payments/4

Это также возвращает статус HTTP 200 (OK) и следующую полезную нагрузку:

{
    "id": 4,
    "transactionId": "3fcb379e-cb89-4013-a141-c6fad4b55f6b",
    "amount": 19.99,
    "customerId": 4
}

Наконец, ниже приведен пример вызова API-интерфейса заказа:

ПОЛУЧИТЬ https://jvc-order.herokuapp.com/orders/4

Здесь возвращается статус HTTP 200 (OK) со следующей полезной нагрузкой:

{
   "id": 4,
   "customerId": 4,
   "paymentId": 4,
   "orderDate": "2021-06-07T04:31:52.497082",
   "description": "Sample Order #4"
}

Вывод

Начиная с 2021 года я начал сосредотачиваться на следующей формулировке миссии, которая, как я чувствую, может быть применима к любому ИТ-специалисту:

“Сосредоточьте свое время на предоставлении функций/функциональности, которые повышают ценность вашей интеллектуальной собственности. Используйте фреймворки, продукты и услуги для всего остального”.

  • J. Ваш

Экосистема Heroku позволяет довольно легко придерживаться заявления о миссии. В течение нескольких часов я смог полностью создать и создать прототип трех микросервисов, содержащих Spring Boot RESTful API и базу данных Heroku Postgres. Облачный AMQP был добавлен, интегрирован в решение и проверен в течение того же промежутка времени.

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

Если вам интересно ознакомиться с фактическим исходным кодом этого проекта, пожалуйста, ознакомьтесь со следующими репозиториями на GitLab:

jvc-клиент

предварительный заказ

предоплата

Хорошего вам дня!

Оригинал: “https://dev.to/johnjvester/breaking-up-the-god-service-using-heroku-5g7h”