Статьи

Как создавать крошечные сервисы, используя Redis

В этом посте я собираюсь пройти процесс построения рабочего процесса на примере платежей. По мере того, как вы будете больше работать с Redis, вы скоро начнете создавать рабочие процессы, то есть небольшие фрагменты кода, которые общаются друг с другом через Redis. Для тех, кто знаком с сервис-ориентированным подходом к построению систем, это должно быть похоже на дежа-вю. За исключением того, что вместо использования протокола (HTTP, TCP, UDP, AMQP, ZeroMQ) мы возвращаемся к CS101 с использованием старой доброй структуры данных очереди.

Многим это может показаться сумасшедшим: создание рабочего процесса обработки платежей с использованием чего-либо, кроме традиционной СУБД. Я хотел бы возразить, однако, что многие из предполагаемых преимуществ, которые, по вашему мнению, традиционные СУБД обеспечивают с точки зрения транзакционности, могут быть очень легко созданы с помощью Redis.

Существует проблема, связанная с тем, чтобы Redis сохранял свои данные в энергозависимой памяти, и если компьютер, на котором работает сервер Redis, дает сбой, то все, что было с момента последнего bgsave, потеряно. Но те же самые правила применимы и к обычной базе данных. Вы можете увеличить частоту, с которой redis выполняет fsync и записывать ее на диск при каждой записи, как в СУБД. Что делать, если диск стирается? В этом случае вы можете принять те же меры предосторожности, что и при работе с СУБД, т.е. при репликации. Лично я попадаю в лагерь одобрения репликации и думаю, что это делает недействительной необходимость иметь параноидальные записи на диск (которые убивают производительность, которую дает redis). Избавившись от этого, давайте приступим к созданию быстрого, правильного, надежного и надежного платежного процессора с использованием атомарных инструментов, которые дает вам Redis.

Модус операнди

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

- A HTTP API allowing users to submit payments into our workflow. 
    This generates a unit of work that get's handled by the workers.
- Workers that act on the work generated from our API

Наш план будет:

- Expose a HTTP API that let's users submit payments
- Keep our controller minimal, have it just create a model and call it a day
- Have the model process the payment asynchronously
- Think about handling cases for when things don't go according to plan
- Have a way to figure which payments are taking unduly long to process etc.

HTTP API

Для простоты предположим, что у нас есть следующий код, запущенный в Ruby-on-Rails, который представляет наш платежный процессор через HTTP:

class PaymentController < ApplicationController
  def create
    Payment.create! params[:payment]
    render :json => {:status => "OK"}
  end
end

Модельный слой

В соответствии с совершенством MVC, вот наш уровень модели:

class Payment
  def self.create!
    raise ArgumentError unless params[:payer_id], params[:recipient_id],
                                params[:amount]
    redis.rpush 'payments_to_be_processed',
      params[:payment].merge(:tracking_id => redis.incr('tracking_ids')).to_json
  end
end

Оглядываясь назад на список пунктов, можно сказать, что мы выполнили первые три пункта: у нас есть супер простой HTTP API, код нашего контроллера и модели минимален, и мы проложили дорогу для асинхронной обработки платежей с помощью списка payment_to_be_processed. , Довольно классная штука, теперь все, что осталось сделать, — это разобраться в бэк-энде. Как мы отслеживаем платежи, когда они проходят через различные состояния обработки , какие метрики, по нашему мнению, нам понадобятся, и как нам их хранить, каковы условия гонки, от которых мы должны защищаться, и т. Д. .?

Оформление платежного квитанции

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

def process_payment(payer_id, recipient_id, amount)
  rand_val = (rand * 10).to_i
  if rand_val > 3
    return :status => :success,
      :txn_id => redis.incr("txn_ids"), :processed_on => Time.new.getutc.to_i
  elsif rand_val < 7
    return :status => :insufficient_funds,
      :txn_id => redis.incr("txn_ids"), :processed_on => Time.new.getutc.to_i
  else
    return :status => :api_error, :processed_on => Time.new.getutc.to_i
  end
end

 

Как видите, этот метод может иметь один из трех возможных результатов:

- success
- insufficient funds 
- api error (we were not able to connect with our payment service)

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

(Примечание. В этом методе я работаю со временем в целочисленном формате UTC. Очень рекомендую это при работе с Redis.)

Работники по обработке платежей

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

loop do
  payment      = JSON.parse(redis.brpop("payments_to_be_processed")[1])
  tracking_id  = payment['tracking_id']
  payer_id     = payment["payer_id"]
  recipient_id = payment["recipient_id"]
  amount       = payment["amount"]
  results      = process_payment payer_id, recipient_id, amount
  if results[:status] == :success
    redis.zadd "successful_txns", results[:processed_on], results[:txn_id]
    redis.hmset "txns", results[:txn_id], payment.merge(:tracking_id => tracking_id).to_json
    redis.zadd "payments_made_by|#{payer_id}", results[:processed_on], results[:txn_id]
    redis.zadd "payments_received_by|#{recipient_id}", results[:processed_on], results[:txn_id]
  elsif results[:status] == :insufficient_funds
    redis.zadd "insufficient_funds_txns", results[:processed_on], results[:txn_id]
    redis.hmset "txns", results[:txn_id], payment.merge(:tracking_id => tracking_id).to_json
    redis.zadd "insufficient_funds_for|#{payer_id}", results[:processed_on], results[:txn_id]
    redis.zadd "insufficient_funds_to|#{recipient_id}", results[:processed_on], results[:txn_id]
  else
    redis.zadd "api_errors", results[:processed_on], {:payment_id => payment_id}.to_json
  end
end

 

Это выглядит довольно впечатляющим первым ударом по проблеме. У нас есть:

1. Handled (maybe not completely) for our three cases when processing a payment.
2. A way to figure out the status of a payment by looking in the lists:
   - successful_txns
   - insufficient_funds_txns 
   - api_errors 

Каждый работник извлекает хэш-код JSON, содержащий подробности о том, кто кому платит, и о сумме. Затем работник пытается обработать платеж и, в зависимости от того, был он успешным или неудачным, добавляет его к дальнейшим структурам повторного предоставления данных. Здесь я хотел бы отметить одну вещь: когда бы ни было возможно, я склоняюсь к использованию отсортированного набора вместо набора с меткой времени UTC в качестве результата. Это позволяет мне выполнять диапазонные запросы, такие как общее количество успешных транзакций, выполненных сегодня, сколько платежей совершил или получил данный пользователь за определенный период времени и т. Д. В любое время вы можете увидеть, что вам нужен набор, подумайте немного глубже, чтобы посмотрите, может ли отсортированный набор лучше подойти Возвращаясь к приведенному выше коду,Единственное, от чего мы хотели бы защититься, — это убедиться, что независимо от результата платежа, когда мы записываем статус в некоторых из наших структур данных, это действительно хорошая идея сделать это одним махом. Чтобы быть немного понятнее, если транзакция была успешной, мы хотим убедиться, что она либо добавленаsuccessful_txns , txns , payments_made_by и payments_received_by или ничего.

Транзакционность с использованием multi-exec

Для этого мы используем встроенные в Redis примитивы транзакций multi и exec. Обновленный код выглядит следующим образом:

loop do
  ...
  if results[:status] == :success
    redis.multi
    redis.zadd "successful_txns", results[:processed_on], results[:txn_id]
    ...
    redis.exec
  elsif results[:status] == :insufficient_funds
    redis.multi
    ...
    redis.exec
  else
    redis.zadd "api_errors", results[:processed_on], {:payment_id => payment_id}.to_json
  end
end

 

Отслеживание размера очереди

Вот очень простой трекер размера очереди:

loop do
  if redis.llen("payments_to_be_processed") > 100_000
    send_pager(:to => "ops", :msg => "queue is getting backed up")
  end
  sleep 1*60 #for a minute
end

 

Здесь 100_000 — это число, которое я вытащил из воздуха. Вы можете / должны иметь возможность настройки. Вам также не нужно беспокоиться о том, что этот трекер отключит ваш сервер Redis Поверьте, Redis может выполнять операцию llen O (1) каждые 60 секунд! ?

пересчет

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

Вывод

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