Приложения Rails бывают разных форм и размеров. С одной стороны, у вас есть большие монолитные приложения, в которых все приложение (admin, API, front-end и вся работа, которую нужно выполнить) находится в одном месте. На другом конце спектра у вас есть ряд микросервисов, все они общаются друг с другом с целью разбить работу на более управляемые куски.
Такое использование микросервисов называется сервис-ориентированной архитектурой (SOA). Приложения Rails из того, что я видел, имеют тенденцию быть монолитными, но ничто не мешает разработчику иметь несколько приложений Rails, которые работают вместе и / или подключаются к сервисам, написанным на других языках или платформах, чтобы помочь им выполнить поставленную задачу.
Монолиты не должны быть написаны плохо, но плохо написанные монолиты, разбитые на микросервисы, скорее всего, также будут плохо написаны. Вы можете написать свое приложение несколькими способами, которые не только помогут вам написать более чистый (и более легко тестируемый) код, но также помогут вам, если / когда возникнет необходимость разбить приложение.
Наш вариант использования приложения Rails с микросервисами
В этой статье мы поговорим о создании сайта, который является CMS. Представьте себе любую большую газету или блог, в которой есть несколько авторов, которые публикуют статьи для пользователей, которые могут подписаться на получение уведомлений об определенных темах.
Мартин Фаулер написал отличную статью о том, почему редактирование и публикация должны быть разделены на две разные системы. Мы возьмем этот пример и добавим еще два компонента: уведомления и подписчики.
CMS, о которой мы говорим, состоит из четырех основных компонентов:
- Редактор CMS: используется авторами и редакторами для создания, редактирования и публикации статей.
- Публичный веб-сайт: используется публикой для просмотра опубликованных статей.
- Уведомитель. Используется для уведомления подписчиков о новых опубликованных статьях.
- Подписчики: используется для управления учетными записями пользователей и подписками.
Должен ли я создать приложение Rails как SOA?
Итак, как вы решаете, имеет ли смысл создавать приложение Rails как монолит или создавать его с помощью микросервисов? Нет правильного или неправильного ответа, но, задавая себе следующие вопросы, вы можете решить.
Как вы решаете, строить ли приложение Rails как монолит или использовать микросервисы?
Как организованы мои команды?
Решение использовать SOA часто имеет мало общего с техническими причинами, а скорее с тем, как организованы различные команды разработчиков.
Возможно, для четырех команд имеет больше смысла использовать один из основных компонентов для работы в изолированном пространстве, чем чтобы все работали над одной системой. Если вы работаете в команде из нескольких разработчиков, решение начать с самого начала с помощью микросервисов может фактически снизить скорость разработки, добавив повышенную гибкость, позволяющую четырем различным системам взаимодействовать друг с другом (и развертывать).
Разные компоненты масштабируются по-разному?
Есть большая вероятность, что в нашей системе вариантов использования в этой статье общедоступный веб-сайт будет иметь гораздо больше трафика, чем редактор CMS, который будут использовать авторы и редакторы.
Создавая их как отдельные системы, мы можем масштабировать их независимо и / или применять разные методы кэширования для разных частей системы. Вы по-прежнему можете масштабировать систему как монолит, но вы будете масштабировать все сразу, а не отдельные компоненты по отдельности.
Разные компоненты используют разные технологии?
Возможно, вы захотите создать свой редактор CMS как одностраничное приложение (SPA) с React или Angular и сделать основной общедоступный веб-сайт более традиционным серверным приложением Rails (для целей SEO). Может быть, Notifier лучше подойдет в качестве приложения Elixir из-за поддержки параллелизма и параллелизма языка.
Имея их в качестве разных систем, вы можете выбрать лучший язык для работы в каждой из служб.
Определение границ
Наиболее важной частью всего этого является то, что между различными компонентами системы существуют четко определенные границы.
Одна часть системы должна думать о себе как о клиенте, взаимодействующем с внешним сервером . Неважно, происходит ли связь через вызовы методов или через HTTP, он просто знает, что должен общаться с другой частью системы.
Один из способов сделать это — определить четкие границы.
Скажем, когда статья публикуется, должны произойти две вещи:
- Сначала отправим опубликованную версию статьи на общедоступный веб-сайт , который вернет нам URL-адрес, по которому она была опубликована.
- Во-вторых, мы отправляем недавно созданный общедоступный URL-адрес, тему и заголовок в Notifier, который будет обрабатывать уведомления всех заинтересованных подписчиков. Это второе задание может быть выполнено асинхронно, потому что это может занять некоторое время, чтобы уведомить всех, и на самом деле ответа не требуется.
Например, возьмите следующий код, который публикует статью. В статье не известно, является ли вызываемая служба просто вызовом метода или это HTTP-вызов.
class Publisher
attr_reader :article, :service
def initialize(article, service)
@article = article
@service = service
end
def publish
mark_as_published call_service
article
end
private
def call_service
service.new(
author: article.author,
title: article.title,
slug: article.slug,
category: article.category,
body: article.body
).call
end
def mark_as_published(published_url)
article.published_at = Time.zone.now
article.published_url = published_url
end
end
Этот тип кодирования также позволяет нам легко тестировать функциональность Publisher
класса с помощью a, TestPublisherService
который возвращает готовые ответы.
require "rails_helper"
RSpec.describe Publisher, type: :model do
let(:article) {
OpenStruct.new({
author: 'Carlos Valderrama',
title: 'My Hair Secrets',
slug: 'my-hair-secrets',
category: 'Soccer',
body: "# My Hair Secrets\nHow hair was the secret to my soccer success."
})
}
class TestPublisherService < PublisherService
def call
"http://www.website.com/article/#{slug}"
end
end
describe 'publishes an article to public website' do
subject { Publisher.new(article, TestPublisherService) }
it 'sets published url' do
published_article = subject.publish
expect(published_article.published_url).to eq('http://www.website.com/article/my-hair-secrets')
end
it 'sets published at' do
published_article = subject.publish
expect(published_article.published_at).to be_a(Time)
end
end
end
На самом деле, реализация PublisherService
даже не была построена, но это не мешает нам писать тесты, чтобы гарантировать, что клиент (в данном случае Publisher
) работает так, как ожидалось.
class PublisherService
attr_reader :author, :title, :slug, :category, :body
def initialize(author:, title:, slug:, category:, body:)
@author = author
@title = title
@slug = slug
@category = category
@body = body
end
def call
# coming soon
end
end
Служба связи
Службы должны иметь возможность общаться друг с другом. Это то, что разработчики Ruby уже знают, даже если раньше мы не создавали микросервисы.
Когда вы «вызываете» метод объекта, вы действительно отправляете ему сообщение, что можно увидеть, изменив Time.now
на Time.send(:now)
. Независимо от того, отправляется ли сообщение с помощью вызова метода или происходит обмен данными по HTTP, идея одна и та же. Мы хотим отправить сообщение в другую часть системы, и часто мы хотим получить ответ.
Связь по HTTP
Связь через HTTP — отличный способ отправлять сообщения в службу, когда вам нужен мгновенный ответ — когда вы полагаетесь на получение части данных для продолжения выполнения программы.
HTTP — отличный способ отправлять сообщения в службу, когда вам нужен мгновенный ответ.
Я взял оригинальный PublisherService
класс и реализовал HTTP-пост в нашем сервисе, используя Faraday
гем.
class PublisherService < HttpService
attr_reader :author, :title, :slug, :category, :body
def initialize(author:, title:, slug:, category:, body:)
@author = author
@title = title
@slug = slug
@category = category
@body = body
end
def call
post["published_url"]
end
private
def conn
Faraday.new(url: Cms::PUBLIC_WEBSITE_URL)
end
def post
resp = conn.post '/articles/publish', payload
if resp.success?
JSON.parse resp.body
else
raise ServiceResponseError
end
end
def payload
{author: author, title: title, slug: slug, category: category, body: body}
end
end
Его задача — создавать данные, которые будут публиковаться в сервисе, и обрабатывать его ответы. Это также подтверждает, что ответ является успешным и в противном случае будет выдано исключение. Подробнее об этом позже.
Я использовал константу Cms::PUBLIC_WEBSITE_URL
, которая получает значение через инициализатор. Это позволяет нам настраивать его с использованием ENV
переменных для различных сред, в которых мы в конечном итоге развертываем наше приложение.
Cms::PUBLIC_WEBSITE_URL = ENV['PUBLIC_WEBSITE_URL'] || 'http://localhost:3000'
Тестирование наших услуг
Теперь пришло время проверить, что наш PublisherService
класс работает правильно.
Для этого я не рекомендую делать реальный HTTP-вызов; это замедлит тестирование и потребует, чтобы у вас всегда была запущена и работала служба на компьютере разработчика (или на сервере непрерывной интеграции). Для этого мы можем использовать WebMock
гем, чтобы перехватить HTTP-вызов и получить требуемый ответ.
RSpec.describe PublisherService, type: :model do
let(:article) {
OpenStruct.new({
author: 'Carlos Valderrama',
title: 'My Hair Secrets',
slug: 'my-hair-secrets',
category: 'Soccer',
body: "# My Hair Secrets\nHow hair was the secret to my soccer success."
})
}
describe 'call the publisher service' do
subject {
PublisherService.new(
author: article.author,
title: article.title,
slug: article.slug,
category: article.category,
body: article.body
)
}
let(:post_url) {
"#{Cms::PUBLIC_WEBSITE_URL}/articles/publish"
}
let(:payload) {
{published_url: 'http://www.website.com/article/my-hair-secrets'}.to_json
}
it 'parses response for published url' do
stub_request(:post, post_url).to_return(body: payload)
expect(subject.call).to eq('http://www.website.com/article/my-hair-secrets')
end
it 'raises exception on failure' do
stub_request(:post, post_url).to_return(status: 500)
expect{subject.call}.to raise_error(PublisherService::ServiceResponseError)
end
end
end
Планирование отказа
Неизбежно наступит время, когда служба выйдет из строя или остановится, и мы должны устранить эту ошибку.
Разработчик действительно должен решить, что нужно сделать, когда сервис не работает. Может ли веб-сайт продолжать работать, когда он не работает? В случае нашего приложения CMS оно может нормально работать для создания и редактирования статей, но нам придется показывать пользователю ошибку при попытке опубликовать статью.
В рамках вышеупомянутого теста существует случай, когда сервер ищет ответ 500 и проверяет, вызывает ли он PublisherService::ServiceResponseError
исключение. Эта ошибка происходит из его родительского класса, HttpService
который пока не содержит ничего, кроме классов ошибок.
class HttpService
class Error < RuntimeError
end
class ServiceResponseError < Error
end
end
В другой статье Мартина Фаулера он рассказывает о паттерне, называемом CircuitBreaker , чья работа заключается в том, чтобы выявлять, когда служба работает не так, и избегать совершения вызова, который, скорее всего, в любом случае приведет к ошибке.
Еще один метод, который можно использовать для повышения устойчивости нашего приложения, — это информирование о состоянии различных служб, которые ему необходимы для работы.
Это может контролироваться разработчиками вручную, давая им возможность отключать определенные функции веб-сайта, когда известно, что служба не работает, и избегать ошибок в целом. Тем не менее, он также может управляться совместно с каким-то шаблоном CircuitBreaker программным способом.
Перед тем, как показывать пользователям кнопку «Публикация», проведите быструю проверку, чтобы PublisherService.available?
избежать ошибок.
Связь с очередями
HTTP не единственный способ общения с другими службами. Очереди являются отличным способом передачи асинхронных сообщений назад и вперед между различными службами. Идеальная ситуация для этого — когда у вас есть работа, которую вы хотите выполнить, но вам не нужен прямой ответ от получателя этого сообщения (например, отправка писем).
Очереди являются отличным способом передачи асинхронных сообщений между службами.
В нашем приложении CMS после публикации статьи все подписчики на тему этой статьи получают уведомление (по электронной почте, через веб-сайт или с помощью push-уведомления) о появлении новой статьи, которая может быть им интересна. Наше приложение не нуждается в ответе от службы Notifier , оно просто должно отправить ему сообщение и позволить ему выполнить свою работу.
Эта статья была написана Ли Холлидей