Статьи

Архитектура приложений Rails как микросервисы

Приложения Rails бывают разных форм и размеров. С одной стороны, у вас есть большие монолитные приложения, в которых все приложение (admin, API, front-end и вся работа, которую нужно выполнить) находится в одном месте. На другом конце спектра у вас есть ряд микросервисов, все они общаются друг с другом с целью разбить работу на более управляемые куски.

Такое использование микросервисов называется сервис-ориентированной архитектурой (SOA). Приложения Rails из того, что я видел, имеют тенденцию быть монолитными, но ничто не мешает разработчику иметь несколько приложений Rails, которые работают вместе и / или подключаются к сервисам, написанным на других языках или платформах, чтобы помочь им выполнить поставленную задачу.

Rails-приложения, как правило, монолотичны, но нет причин, по которым вы не можете создавать их как микросервисы.

НАЖМИТЕ НА ТВИТ

Монолиты не должны быть написаны плохо, но плохо написанные монолиты, разбитые на микросервисы, скорее всего, также будут плохо написаны. Вы можете написать свое приложение несколькими способами, которые не только помогут вам написать более чистый (и более легко тестируемый) код, но также помогут вам, если / когда возникнет необходимость разбить приложение.

Наш вариант использования приложения Rails с микросервисами

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

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

CMS, о которой мы говорим, состоит из четырех основных компонентов:

  • Редактор CMS:  используется авторами и редакторами для создания, редактирования и публикации статей.
  • Публичный веб-сайт:  используется публикой для просмотра опубликованных статей.
  • Уведомитель.  Используется для уведомления подписчиков о новых опубликованных статьях.
  • Подписчики:  используется для управления учетными записями пользователей и подписками.

Система CMS

Должен ли я создать приложение Rails как SOA?

Итак, как вы решаете, имеет ли смысл создавать приложение Rails как монолит или создавать его с помощью микросервисов? Нет правильного или неправильного ответа, но, задавая себе следующие вопросы, вы можете решить.

Как вы решаете, строить ли приложение Rails как монолит или использовать микросервисы?

НАЖМИТЕ НА ТВИТ

Как организованы мои команды?

Решение использовать SOA часто имеет мало общего с техническими причинами, а скорее с тем, как организованы различные команды разработчиков.

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

Разные компоненты масштабируются по-разному?

Есть большая вероятность, что в нашей системе вариантов использования в этой статье общедоступный веб-сайт будет иметь гораздо больше трафика, чем редактор CMS, который будут использовать авторы и редакторы.

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

Разные компоненты используют разные технологии?

Возможно, вы захотите создать свой редактор CMS как одностраничное приложение (SPA) с React или Angular и сделать основной общедоступный веб-сайт более традиционным серверным приложением Rails (для целей SEO). Может быть, Notifier лучше подойдет в качестве приложения Elixir из-за поддержки параллелизма и параллелизма языка.

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

Определение границ

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

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

Один из способов сделать это — определить четкие границы.

Скажем, когда статья публикуется, должны произойти две вещи:

  1. Сначала отправим опубликованную версию статьи на  общедоступный веб-сайт , который вернет нам URL-адрес, по которому она была опубликована.
  2. Во-вторых, мы отправляем недавно созданный общедоступный 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 , оно просто должно отправить ему сообщение и позволить ему выполнить свою работу.

Эта статья была написана Ли Холлидей