Приключение Python в AWS (Feat. API Gateway)

Сегодня я работал над своим планом на 2020 год по превращению вещей в AWS Lambdas.

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

В конкретном стеке функций, над которым я работал, использовалась специальная служба Region, которая могла искать почтовые индексы США и Канады и предоставлять различную информацию о них: альтернативные названия городов, широту / долготу - вы поняли идею.

Наши собственные пользовательские данные накладываются на стандартные данные почтового индекса, поэтому традиционно они обслуживаются RESTful API, работающим на собственном экземпляре EC2, на котором запущен стек LAMP (Amazon Linux 2, Apache, MySQL / Aurora и PHP).

"Разве это не было бы хорошо?" Я подумал: «Может ли мое приложение вызывать ресурс региона через Amazon API Gateway и просто позволить Lambda справиться с этим?»

И в моем мире исторически сложилось так, что мыслительный процесс при ламбаизации сервиса происходил примерно так:

  • Могу я даже это сделать?
  • Позвольте мне быстро создать прототип.
  • Хорошо, я обнаружил много проблем.
  • Должен ли я вообще это делать?
  • О, я уже сделал. (или, наоборот: Неважно, это была плохая идея.)

Требования к планировке

Я не буду утомлять вас подробным описанием каждого ресурса API, предоставляемого этой службой Region. Достаточно для моего объяснения, что его задача №1 заключалась в поиске почтового индекса с помощью запроса GET и возврате некоторых пользовательских данных о нем.

На самом деле это выглядело бы так: GET https://api.com/region/postalcode/20008

Не сложно, правда? Это именно интерфейс, который AWS API Gateway делает невероятно простым для создания с использованием настраиваемых параметров пути.

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

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

Импортировать или не импортировать

Внезапно я столкнулся с вопросом: я мог создать единый контейнер сценариев Python (после преобразования логики из PHP), который мог бы обрабатывать все функции исходной службы PHP. был, включая обновление базы данных с использованием последних почтовых индексов - я поверил в это.

Стоит?

По правде говоря, я даже не знал, справится ли AWS Lambda с такой работой. Скачивание файла, его разархивирование, массовое обновление некоторых таблиц?

Предварительный поиск в Google не казался многообещающим: все лучшие результаты использовали S3 как метод временного хранения, а не как собственный механизм хранения Lambda.

Тем не менее, я все равно решил попробовать.

О мотивации и методах

Преобразование старой логики PHP в набор скриптов Python 3 и абстрагирование данных и действий для разделения файлов ini и sql само по себе стоило большей части усилий.

Мне удалось оптимизировать поток кода просто за счет того, что меня заставили переархивировать исходный PHP на основе ООП в набор модульных пакетов Python.

Модульные тесты было легко написать, и поскольку пакет Region содержал локальные копии его зависимостей, я мог просто вызвать пакет Region.py и напрямую вызвать функцию lamda_handler с тестовыми данными: main не требуется.

Все ресурсы, такие как GET /region/postalcode или GET /region/city, отображали 1: 1 между двумя версиями кодовой базы.

Но функция импорта определенно никогда не предназначалась для того, чтобы быть частью API, поэтому почему я пытался преобразовать метод импорта во что-то, что можно использовать с AWS Lambda, даже если я собирался ограничить к нему доступ?

Вкратце: сочетание любопытства и желания иметь единый набор пакетов, с которым я мог бы работать.

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

Закончив преобразование кода и протестировав скрипты Python на моей локальной машине разработчика, пришло время протестировать его как Lambda.

Во-первых, работа на молнии

Интерфейс AWS Lambda упрощает работу с Lambda во время отладки.

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

Если вы посмотрите на прикрепленный снимок экрана, вы увидите раздел «Тип ввода кода». Здесь вы можете изменить его с «Редактировать встроенный код» на «Загрузить» и отправить ему zip-архив со структурой папок со всеми файлами в том виде, в котором они вам нужны.

Отредактировав обработчик в правом верхнем углу, вы можете указать ему свою собственную точку входа (в данном случае Region.py и функцию, которую я назвал lambda_handler).

Единственная сложность заключается в том, что - поскольку я использовал PyMySQL в качестве коннектора базы данных - мне также нужно было включить его локальную копию! (Он не входит в зависимости среды AWS Lambda Python 3.8 по умолчанию. Однако вы получаете sys и os и всех обычных подозреваемых.)

Так вот оно! Я уже написал и проверил свои тестовые примеры в среде разработки (вне облака), и хотя ресурс GET /region/postalcode был общедоступным для моих клиентских приложений, POST /region/import был привязан к определенному IP-адресу, которому было разрешено его вызывать.

Примечание. моим намерением было в конечном итоге вызвать импорт напрямую с помощью CloudWatch, а ресурс шлюза API для процесса импорта был просто быстрым методом для выполнения Lambda.

БУМ.

Не сработало. Тайм-аут немедленно истек.

Ох! Я подумал, мне нужно увеличить время выполнения лямбды!

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

Я установил Timeout на максимум 15 minutes - не потому, что я когда-либо ожидал потратить столько времени в производственной среде для выполнения этой работы (это 2–3-минутная задача, max), а просто для отладки.

Это не помогло.

Посмотрим на код:

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

Единственное место, которое мы можем сделать, находится в папке /tmp.

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

Итак, если это не сама загрузка файла (или проблема с доступом VPC к шлюзу NAT, как я думал изначально), тогда в чем же проблема?

Мне удалось изолировать его от извлечения zip-файла. Я правильно включил пакет ZipFile с контейнером, но, увы, это все равно нет.

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

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

Почему нет? Лямбды могут делать очень многое

А Lambdas могут делать некоторые вещи очень и очень хорошо.

Например, чтение базы данных и предоставление данных почтового индекса по запросу.

Но обновить пару сотен тысяч записей почтовых индексов в пакетной транзакции после распаковки довольно большого загруженного файла?

Я действительно объединял потребности разных систем.

Например: я ожидаю, что поиск почтового индекса региона будет выполняться менее чем за секунду для случаев TP99. Если база данных недоступна, я действительно хочу дать ей тайм-аут через 15 минут только потому, что мне пришлось увеличить общее время выполнения Lambda для еженедельного процесса импорта?

Конечно, нет.

Эта озабоченность фактически заставила меня задать вопрос: можно ли установить собственные тайм-ауты для каждого типа ресурса / метода в AWS API Gateway?

Хорошие новости: да, могу.

Тогда это делает его более разумным архитектурным выбором. Да, у самой Lambda может быть более длительный тайм-аут, но при использовании через API Gateway я мог бы соответствующим образом ограничить ожидаемые тайм-ауты.

Примечание. Помните о времени разогрева лямбда, если вы решите это сделать.

Однако, в конце концов, моя текущая проблема заключалась в том, что подавляющее большинство пакета Lambda работало, а импорт - нет, и мое время не было бесконечным.

Что делать?

А почему бы не гибридизировать?

Я понял, что не все потеряно в этом маленьком приключении.

Что, если бы я переместил этот пакет в тот же экземпляр EC2, где мы обрабатывали сценарии в стиле «внутренний DevOps / управление задачами»?

Это был существующий ресурс, который играл роль в организации, и теперь я мог разделить цели пакета «Регион» на:

  1. Оставив все конечные точки ресурсов для обработчика лямбда открытыми, как они уже были
  2. Удалите общий доступ к функции импорта (нет ресурса или метода для ее вызова)
  3. Создайте сценарий сборки / развертывания, который синхронизирует любые изменения в коде до соответствующего экземпляра AWS Lambda.
  4. Создайте задачу планировщика на том экземпляре EC2, который загрузил эту среду и использовал функцию импорта напрямую для выполнения задачи импорта (потому что все это уже было протестировано и доказано, что работает, когда не в AWS Lambda)

Обратите внимание, что номер 4 был тем, что я надеялся вызвать через Lambda с помощью CloudWatch, но теперь я вернулся к использованию экземпляра EC2 (но общего ресурса DevOps с другим планированием задач вместо исходного экземпляра API только для региона).

Теперь у меня был единый пакет кода для микросервиса Region, который нужно поддерживать, вместо того, чтобы хранить старые скрипты импорта PHP и старый экземпляр EC2.

Мне ничего не стоит разместить «лишний» неиспользуемый код импорта в AWS Lambda, но я могу использовать одни и те же интерфейсы Python для модульного тестирования и планирования для обоих типов развертываний.

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

Дальнейшие мысли и что пошло не так

В конце концов, я не знал, почему извлечение ZipFile не удалось в среде Lambda, и решил обойти это.

Но благодаря любопытному редактору Reddit мы в этом разобрались.

На самом деле это была невероятно простая настройка: для Lambda было выделено только 256 МБ, что, как я полагал, было достаточно, учитывая, что размер задействованных файлов составлял около 95 МБ.

Вот почему я ошибался:

  • Распаковка файла могла потребовать больше ОЗУ, чем сам размер файла (очевидно, если оглянуться назад).
  • Я создавал большую транзакцию SQL для фиксации всех строк, размер которой был намного больше 95 МБ благодаря форматированию и декораторам, которые идут с ним.

Потребовалось больше, чем 741 МБ!

После установки Lambda на 1024 МБ памяти все прошло гладко.

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

Спасибо, tinman3330!

Если вам интересно, как выглядит загружаемая часть скрипта Python, я прикрепил ее ниже: