В предыдущих сообщениях мы здесь, в I Love My Local Farmer, обсуждали необходимость создания новой службы, чтобы обеспечить возможности доставки для нашей существующей бизнес-модели. В связи с ограничениями и изменениями, вызванными COVID, мы больше не могли полагаться исключительно на нашу существующую модель вывоза сельскохозяйственной продукции для поддержания нашего бизнеса. Учитывая, что этот новый сервис создается полностью с нуля, мы увидели в нем хорошую возможность реализовать часть нашей платформы в AWS, которая полностью отличается от наших предыдущих вариантов получения.

Заявление об ограничении ответственности
I Love My Local Farmer - это вымышленная компания, вдохновленная взаимодействием клиентов с архитекторами решений AWS. Любые истории, рассказанные в этом блоге, не относятся к конкретному клиенту. Сходства с любыми реальными компаниями, людьми или ситуациями чисто случайны. Истории в этом блоге отражают точку зрения авторов и не одобрены AWS.

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

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

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

В этом посте мы расскажем обо всех архитектурных решениях и компромиссах, которые мы сделали, а также о некоторых других потенциальных реализациях, которые мы рассмотрели в процессе, и почему мы решили не использовать эти решения. Для справки в оставшейся части сообщения, вот схема архитектуры, которую мы решили реализовать. Он состоит из шлюза API, который выполняет 3 функции Lambda, каждая из которых подключается к базе данных RDS с помощью прокси-сервера RDS. Вкратце, Magento - это предпочтительная платформа электронной коммерции, которую мы используем для нашего существующего приложения, хотя технически любая платформа электронной коммерции может быть заменена на нее. Одна из особенностей Magento заключается в том, что для его работы требуется кластер и база данных Elasticsearch. На диаграмме ниже мы называем их базой данных Magento и Magento Elasticsearch, хотя технически они не являются частью приложения Magento.

Бессерверная или серверная: как бы вы хотели, чтобы вас обслуживали?

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

Одной из основных проблем, с которыми мы столкнулись при работе с нашей локальной инфраструктурой, были оперативные усилия, связанные с правильным масштабированием нашей системы для удовлетворения динамических требований запросов клиентов. Клиенты обычно посещают наш веб-сайт во второй половине дня и постепенно увеличиваются в течение вечера, с небольшими всплесками в середине дня. Когда мы впервые задумались о переходе на облачную архитектуру, мы хотели уменьшить как затраты, так и человеческие усилия, необходимые для обеспечения работы нашей системы с максимальной эффективностью. Раньше нашей команде инженеров приходилось постоянно контролировать использование наших ресурсов и запрашивать дополнительные мощности всякий раз, когда казалось, что наша служба находится под угрозой неспособности удовлетворить спрос клиентов. В противном случае посетители нашего веб-сайта столкнутся с большими задержками и / или вообще не смогут получить доступ к нему. Кроме того, когда посещаемость нашего веб-сайта была низкой, нам приходилось платить больше за поддержание неиспользуемых ресурсов. Однако, перейдя на AWS Lambda, мы поняли, что смогли значительно сократить расходы и операционную нагрузку благодаря присущей сервису масштабируемости.

Самым большим преимуществом, которое привлекло нас к бессерверным технологиям, таким как Lambda, по сравнению с такими предложениями, как EC2, было снижение уровня операционных усилий, необходимых для поддержки сервиса. Поскольку мы пытались запустить службу доставки как можно быстрее, мы хотели сосредоточиться на самом приложении и не беспокоиться о настройке и обслуживании экземпляров. С EC2 нам пришлось бы подготовить экземпляры и обеспечить их актуальность в отношении обновлений операционных систем и зависимостей программного обеспечения. Хотя наша команда была знакома с этой рабочей нагрузкой из нашего локального опыта, мы хотели уйти от необходимости такого уровня операционного надзора. С Lambda мы можем сосредоточиться на простом написании кода приложения и его загрузке в облако. Кроме того, мы должны платить только за каждый запрос к функциям Lambda, тогда как с экземплярами EC2 нам пришлось бы платить круглосуточно, чтобы наша служба была всегда доступна. Мы кратко рассматривали возможность перехода на контейнерное решение, такое как ECS или EKS, но, учитывая, что у нас не было большого опыта работы с контейнерами, мы подумали, что это будет слишком радикальный переход для реализации за то время, которое у нас было.

Решение перейти на API Gateway в отличие от другой технологии, такой как Application Load Balancer (ALB), было основано в основном на различиях функций между двумя сервисами. Оба сервиса могут интегрироваться с лямбда-функциями, но нам нужно было выбрать, какая из них лучше всего подходит для нашего варианта использования. Хотя это и не было частью нашего первоначального дизайна, мы считали, что в будущем нам, скорее всего, потребуется добавить некоторую форму AuthZ / AuthN к нашему сервису, особенно если мы в конечном итоге переместим некоторые из наших существующих функций в AWS и нам потребуется проверять вызовы, связанные с Информация для покупателей. Мы читали, что API-шлюзы потенциально могут быть дороже, чем балансировщики нагрузки приложений, интегрированные с лямбда-функциями, особенно для часто используемых приложений, но мы хотели избежать необходимости переделывать нашу архитектуру, если нам в конечном итоге понадобятся некоторые из функций, которые API-шлюз предложения в будущем.

В очередь или не в очередь: это очередь

Один из основных вопросов, с которыми мы столкнулись при разработке нашей новой службы, заключался в том, следует ли реализовать в системе синхронные или асинхронные вызовы. Первоначально мы думали, что реализация асинхронных вызовов улучшит взаимодействие с пользователем как для наших клиентов, так и для наших планировщиков. Планировщики могут создавать слоты доставки, а клиенты могут заказывать доставку через более быстрый и отзывчивый пользовательский интерфейс. С этой целью наш первоначальный дизайн архитектуры выглядел следующим образом, где очереди SQS интегрированы с API Gateway и размещаются перед каждой из наших лямбда-выражений.

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

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

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

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

Рекомендации по базе данных

При выборе базы данных мы хотели использовать то, что было наиболее знакомо нашей команде инженеров. Технологии баз данных NoSQL, такие как DynamoDB, становятся все более и более популярными, но наша команда не имеет опыта написания приложений с их использованием, поэтому потребуется больше времени, чтобы начать с ними работать. Для нашего существующего приложения Magento мы использовали базу данных MySQL для хранения информации о продукции ферм. Чтобы максимально точно воспроизвести эту настройку, мы решили использовать MySQL, развернутый в службе реляционной базы данных (RDS), чтобы хранить нашу информацию о порядке доставки. Мы вкратце подумали о переходе на Aurora Serverless, но более высокая стоимость не показалась оправданной, поскольку мы не ожидаем, что нам понадобится такой уровень производительности, учитывая нашу ожидаемую нагрузку.

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

Инфраструктура будущего

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

Переходя на AWS, мы хотели убедиться, что любую систему, которую мы разработали, можно было легко воспроизвести в случае, если нам потребуется развернуть в новых регионах в будущем. С этой целью мы начали изучать некоторые из потенциальных сервисов, которые предлагает AWS для определения развертываний инфраструктуры. Основными из них, с которыми мы столкнулись, были Модель бессерверного приложения (SAM), Cloudformation и Cloud Development Kit (CDK).

Более детально изучив каждую из этих технологий, мы быстро поняли, что использование облачной информации напрямую, скорее всего, было не тем направлением, в котором мы хотели бы двигаться, поскольку и SAM, и CDK предоставляют преимущества помимо того, что дает нам сама облачная форма. Кроме того, SAM и CDK в конечном итоге компилируются в Cloudformation на серверной части, и мы хотели использовать абстракции более высокого уровня, которые они предоставляют. Использование простой облачной информации не позволит нам получить выгоду ни от заранее созданных конструкций, которые поставляются с SAM, например готовых конфигураций API, ни от простоты разработки, которая связана с программной природой CDK. Это важный момент для нас, поскольку мы натолкнулись на некоторые сообщения, в которых говорилось, что 1000 строк Cloudformation JSON можно сделать примерно за 50 с помощью CDK, и время было для нас существенным, когда дело дошло до разработки нашей новой службы. .

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

Во-вторых, возможность указать нашу инфраструктуру с помощью кода предлагает нам множество функций с точки зрения разработки. Возможность программно определять определенные аспекты нашей инфраструктуры и использовать логические операторы, доступные через языки кодирования, - это функция, которую мы рассматриваем как бесценную возможность CDK. Написание циклов и условных операторов для нашей инфраструктуры и возможность легко извлекать свойства объекта очень удобны по сравнению с выполнением этих манипуляций в YAML, который является языком SAM. Мы можем легко написать логику для внесения необходимых изменений в инфраструктуру на основе таких вещей, как регион и учетная запись, или создать определенное количество ресурсов на основе переданных параметров. Кроме того, на наш взгляд, функции автозавершения кода для языков, которые использует CDK, делают разработку проще и эффективнее.

Языки и фреймворки

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

У нас есть некоторые опасения по поводу этого выбора. А именно, в нашем исследовании мы увидели, что время холодного запуска для лямбда-функций Java может быть больше, чем для других языков, таких как Python или Node. Это вызывает особую озабоченность, поскольку мы приняли решение использовать синхронные вызовы в нашем сервисе, и любая дополнительная задержка может негативно повлиять на наше взаимодействие с пользователем. Однако мы считаем, что это будет компенсировано нашей способностью к более быстрому развитию. Мы прогнозируем, что проблема с холодным запуском затронет лишь небольшую группу пользователей, которые посещают наш веб-сайт в непиковые часы, и, если это окажется слишком большой проблемой, мы можем позже включить Provisioned Concurrency в наш дизайн, чтобы гарантировать, что некоторые лямбды всегда остаются в тепле.

Еще одно решение, которое нужно было принять, заключалось в том, какой язык использовать для нашей разработки CDK. CDK в настоящее время доступен на нескольких языках, включая Typescript, Java и Python. Мы решили сделать выбор в пользу Java. Хотя Typescript является родным языком CDK, мы хотели придерживаться языка, с которым наша команда была знакома, чтобы задействовать наши сильные стороны как команды разработчиков. Переход на новый язык означал бы, что нашей команде пришлось бы познакомиться с совершенно другим набором инструментов и менеджеров пакетов, и мы хотели, насколько это возможно, избежать подобных затрат на установку. Единственное, что нас беспокоит в связи с этим решением, заключается в том, что большинство примеров кода для CDK написано на Typescript, и нам, возможно, придется потратить немного больше времени на их перевод в их эквиваленты в Java.

Отделение службы

Чтобы избежать изменения нашей текущей системы заказов, мы старались максимально полагаться на существующую инфраструктуру нашей платформы. Уважая наше существующее приложение как источник достоверных данных о клиентах и ​​фермах, мы не обязаны выполнять ненужное дублирование данных между нашей текущей платформой и нашей новой службой доставки, например. адреса клиентов, адреса ферм и кластеры Elasticsearch. При этом, в то время как наше существующее приложение будет хранить данные, относящиеся к клиентам и фермам, новая служба доставки будет источником достоверной информации, касающейся заказов доставки.

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

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

Первоначально мы рассматривали возможность хранения такой информации, как адреса клиентов и статус согласия фермы, в Службе доставки. Однако дополнительная сложность сохранения этой информации в синхронизации с информацией, хранящейся в Magento, добавила бы слишком много времени в наш рабочий процесс разработки, а также внесла бы больше опасений с точки зрения GDPR в отношении перемещения личных данных между системами. . Мы сочли выгодным иметь возможность спроектировать наш сервис так, чтобы он хранил как можно меньше информации для планирования и бронирования доставки, поскольку это означало, что мы можем запустить эту функцию как можно скорее с минимальным объемом. усилий.

Наша Служба доставки вступает в игру только после того, как клиент выбрал ферму, находящуюся в пределах досягаемости доставки, и выбрал продукцию. Magento отправит запрос службе, чтобы получить список доступных слотов доставки для данной фермы (4). В этот список входят даты, начинающиеся на два дня в будущем, но не более двух недель. Эта задержка дает фермерам достаточно времени, чтобы подготовить свои заказы к вывозу, а также планировщикам, чтобы организовать доставку в зависимости от доступности водителя. Также обратите внимание, что Magento дважды проверяет, находится ли пользователь в зоне действия фермы в этот момент (3), в случае, если клиент не проходит страницу поиска и вместо этого получает ссылку на страницу фермы от кого-то другого.

После получения списка слотов клиент может выбрать слот, и Magento сделает еще один звонок в службу доставки, чтобы зарезервировать этот слот для конкретного клиента (5). Обратите внимание, что нигде в системе доставки нам не нужно копировать какую-либо информацию, которую мы в настоящее время храним с помощью Magento, за исключением идентификаторов userId и farmId, которые мы используем для связи доставки с определенными объектами. Мы просто отслеживаем, какие слоты доступны, а какие уже забронированы.

При разработке нашей новой службы доставки мы хотели максимально придерживаться принципов микросервисных архитектур. Продолжая развивать I Love My Local Farmer, мы хотим иметь возможность вносить изменения в отдельные части нашей системы, не влияя на поведение других частей нашего приложения. Отделяя систему доставки от базовой информации, хранящейся в Magento, мы можем вносить изменения в поведение службы на сервере, не затрагивая остальную часть I Love My Local Farmer.

Заключительные мысли и подведение итогов

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

Чтобы сократить операционные издержки, связанные с управлением нашей новой службой, мы использовали в нашем дизайне как Lambda, так и API Gateway. Совместное использование обеих этих технологий означает, что нам не нужно создавать какие-либо экземпляры самостоятельно, и мы можем сосредоточиться исключительно на разработке наших API и написании кода нашего приложения. Хотя есть некоторые опасения по поводу холодного запуска и стоимости, они полностью компенсируются легкостью, с которой мы можем использовать эти технологии в нашем новом приложении. Кроме того, поскольку наше приложение обычно не используется круглосуточно, у нас нет особого беспокойства, когда речь идет о затратах из-за того, что не требуется постоянно запускать и запускать экземпляры.

Еще одна проблема, которая возникла, когда мы решили использовать Lambdas, заключалась в потенциальной нехватке ресурсов базы данных из-за поддержания большого количества открытых соединений, исходящих от группы Lambda. Мы смогли смягчить это, включив прокси-сервер RDS в нашу систему, что должно сократить объем логики, которую нам придется реализовать, чтобы наша база данных оставалась надежной и производительной.

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