Цель этого поста - настроить бессерверную инфраструктуру, управляемую в коде, для обслуживания пакетных прогнозов модели машинного обучения или любых других облегченных вычислений асинхронным способом: служба Google Cloud Run будет прослушивать новые файлы в облаке. Корзина хранения через тему сообщения pub / sub, запускает вычислительный процесс и помещает полученные данные в другую корзину. Вы можете найти полный код на GitHub. Сервис будет работать так же просто, как:

# upload data
gsutil cp dataset.csv gs://input-bucket/dataset.csv
# wait a few seconds.. and download predictions
gsutil cp gs://output-bucket/dataset.csv predictions.csv

Как и в предыдущем посте, мы будем использовать Terraform для управления нашей инфраструктурой, включая Google Cloud Run, Storage и Pub / Sub. Кроме того, мы будем использовать в качестве примера простой сценарий прогнозирования временных рядов, который мы будем тренировать на лету, так как время подгонки невелико. В общем, Cloud Run не предназначен для выполнения длительно выполняемых задач (из-за тайм-аутов обслуживания, подробнее об этом позже), но идеально подходит для асинхронного запуска небольших скриптов.

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

Мы будем пользоваться следующими услугами:

  • Google Cloud Run - это сервис для запуска вызываемых контейнеров в бессерверной инфраструктуре.
  • Google Pub / Sub - это сервис асинхронного обмена сообщениями, который позволяет разделить отправителя и получателя сообщений.
  • Google Cloud Storage - сервис для хранения объектов.
  • Terraform - это программное обеспечение инфраструктура как код.
  • Facebook prophet - пакет прогнозирования временных рядов.

Предпосылки

Мы используем Terraform v0.14.0 и gcloud (здесь: Google Cloud SDK 319.0.0, alpha 2020.11.13, beta 2020.11.13, bq 2.0.62, core 2020.11.13, gsutil 4.55).

Нам нужно пройти аутентификацию в Google, чтобы настроить инфраструктуру с помощью Terraform. Воспользуемся интерактивным рабочим процессом:

gcloud auth application-default login

После аутентификации мы можем начать терраформирование и использовать gsutil для взаимодействия с Google Cloud Storage для тестирования нашей инфраструктуры после ее настройки. В производственной среде мы должны создать служебную учетную запись для Terraform.

Создание контейнерной модели

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

Наша модель состоит из следующих файлов, которые мы все поместим в подпапку app:

  • model.py: фактический код модели, включая соответствие и прогнозирование партии.
  • main.py: обработчик Cloud Runner, конечная точка Flask для обработки запросов.
  • Dockerfile: Dockerfile для контейнера приложения
  • build.sh: простой скрипт для создания контейнера
  • push.sh: простой скрипт для отправки контейнера в GCR

model.py будет содержать функцию для подгонки и прогнозирования с использованием fbprophet.Prophet:

Обработчик main.py будет обрабатывать запросы и пересылать их в нашу модель, используя Flask:

Мы поместим все это в Docker, используя следующий python:3.8 базовый образ:

FROM python:3.8
# Allow statements and log messages to immediately appear in the Cloud Run logs
ENV PYTHONUNBUFFERED True
COPY requirements.txt .
RUN pip install -r requirements.txt
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY model.py main.py ./
CMD exec gunicorn --bind :$PORT --workers 1 --threads 1 --timeout 0 main:app

build.sh выглядит следующим образом:

#!/bin/bash
# if required: gcloud auth configure-docker
docker build -t cloud-runner .

Наконец push.sh:

#!/bin/bash
docker tag cloud-runner $IMAGE_URI
docker push $IMAGE_URI

Создание нашей инфраструктуры.

Теперь мы можем спланировать нашу инфраструктуру. Правильно организованный код Terraform можно найти в репозитории GitHub.

Нам необходимо запланировать следующие услуги:

  • сервис Cloud Run для обработки файлов из входной корзины
  • уведомления Cloud Storage для прослушивания новых файлов во входном сегменте
  • тема Pub / Sub для уведомлений Cloud Storage
  • подписка Pub / Sub для подписки службы Cloud Run на тему уведомлений Cloud Storage
  • контейнер приложения, который будет использоваться Cloud Run

В связи с этим нам нужно определить некоторые учетные записи служб и IAM.

Начнем с variables.tf:

Следующий код Terraform будет помещен в main.tf. Нам нужно определить provider и ресурсы, связанные с проектом.

Кроме того, ведра GCS:

Давайте посмотрим на сервис Cloud Run. Поскольку сервис Cloud Run может быть создан только в том случае, если изображение доступно в GCR, мы будем использовать null_resource для создания образа и сделать его зависимостью.

Наконец, уведомления хранилища и инфраструктура Pub / Sub:

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

  • построить контейнер один раз, чтобы он был доступен в кеше
  • инициализировать и применить Terraform

Если вы хотите сначала увидеть план, запустите terraform plan вместо terraform apply.

#!/bin/bash
# build container once to enable caching
(cd app && 
    ./build.sh)
# init and apply terraform
(cd terraform && 
    terraform init && 
    terraform apply)

Нам будет предложено ввести billing_account_name и user. В качестве альтернативы мы можем подготовить файл terraform.tfvars и добавить туда значения:

billing_account_name = ...
user                 = ...

Мы должны видеть что-то вроде следующего (усеченного) вывода, пока нас не попросят подтвердить terraform apply:

[+] Building 0.8s (10/10) FINISHED                                                                                                                                                        
 => [internal] load build definition from Dockerfile  
...
Terraform has been successfully initialized!
...
Plan: 15 to add, 0 to change, 0 to destroy.
Changes to Outputs:
  + image_uri     = (known after apply)
  + input_bucket  = "cloud-runner-input-bucket"
  + output_bucket = "cloud-runner-output-bucket"
  + project_id    = (known after apply)
  + service_name  = "cloud-runner-service"
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

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

random_id.id: Creating...
random_id.id: Creation complete after 0s [id=zg0]
google_project.project: Creating...
google_project.project: Still creating... [10s elapsed]
google_project.project: Still creating... [20s elapsed]
google_project.project: Still creating... [30s elapsed]
google_project.project: Creation complete after 35s [id=projects/cloud-runner-ce0d]
data.google_storage_project_service_account.gcs_account: Reading.
...
google_cloud_run_service_iam_member.iam_member: Creation complete after 16s [id=v1/projects/cloud-runner-ce0d/locations/europe-west3/services/cloud-runner-service/roles/run.invoker/serviceAccount:cloud-runner-service-account@cloud-runner-ce0d.iam.gserviceaccount.com]
google_project_iam_binding.project: Still creating... [10s elapsed]
google_project_iam_binding.project: Still creating... [20s elapsed]
google_project_iam_binding.project: Creation complete after 22s [id=cloud-runner-ce0d/roles/iam.serviceAccountTokenCreator]
Apply complete! Resources: 15 added, 0 changed, 0 destroyed.
Outputs:
image_uri = "gcr.io/cloud-runner-ce0d/cloud-runner:latest"
input_bucket = "cloud-runner-input-bucket"
output_bucket = "cloud-runner-output-bucket"
project_id = "cloud-runner-ce0d"
service_name = "cloud-runner-service"

Эти выходные данные будут полезны, когда мы захотим протестировать наш сервис. Например, мы можем получить имя входной корзины следующим образом: terraform output -json | jq -r .input_bucket.value

Тестирование инфраструктуры

Поскольку наш прототип содержит очень простую реализацию пакета пророка facebook, давайте запустим прогноз временных рядов на небольшом наборе данных, наборе данных hyndsight. Он содержит ежедневные просмотры страниц блога Роба Дж. Хайндмана с 2014–04–30 по 2015–04–29. Роб Дж. Хайндман - автор многих популярных пакетов прогнозов R (включая прогноз), автор множества книг и исследовательских работ, а также эксперт по прогнозированию. Временные ряды в наборе данных показывают заметную недельную картину и восходящую тенденцию, которые мы увидим ниже.

Мы можем загрузить тестовый файл app/data/hyndsight.csv в GCS, используя gsutil. Мы извлечем имя сегмента ввода и вывода из terraform output и извлечем их с помощью jq:

INPUT_BUCKET=$(cd terraform && terraform output -json | jq -r .input_bucket.value)
OUTPUT_BUCKET=$(cd terraform && terraform output -json | jq -r .output_bucket.value)
gsutil cp app/data/hyndsight.csv gs://${INPUT_BUCKET}/hyndsight.csv

Мы можем проверить файл с помощью gsutil -q stat gs://${OUTPUT_BUCKET}/hyndsight.csv, который должен появиться через 10-20 секунд. Мы можем просмотреть логи в консоли Cloud Run:

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

gsutil cp gs://${OUTPUT_BUCKET}/hyndsight.csv app/data/hyndsight_forecast.csv

И построим его с помощью python:

import pandas as pd
actual = pd.read_csv("app/data/hyndsight.csv")
forecast = pd.read_csv("app/data/hyndsight_forecast.csv")
data = pd.concat([actual, forecast])
data.plot(x="date", figsize=(12,5))

Если мы отправим вышеуказанный запрос несколько раз, мы сможем мотивировать Cloud Run запустить больше экземпляров.

INPUT_BUCKET=$(cd terraform && terraform output -json | jq -r .input_bucket.value)
for i in {1..100}
do
    echo "Copying: $i"
    gsutil -q cp gs://${INPUT_BUCKET}/hyndsight gs://${INPUT_BUCKET}/dataset_${i}.csv
done

Мы можем проверить количество запросов в секунду, задержку на запрос (время выполнения нашей модели), количество экземпляров:

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

Обновление контейнера

Чтобы вручную обновить контейнер до новой последней версии, мы можем использовать скрипт deploy.sh. Нам нужно перестроить и отправить образ, обновить службу Cloud Run, чтобы поднять его и переместить трафик.

# get project id, image output and service name from terraform output
PROJECT_ID=$(cd terraform && terraform output -json | jq -r .project_id.value)
IMAGE_URI=$(cd terraform && terraform output -json | jq -r .image_uri.value)
SERVICE_NAME=$(cd terraform && terraform output -json | jq -r .service_name.value)
# build and push image
(cd app && 
    ./build.sh && 
    IMAGE_URI=$IMAGE_URI ./push.sh)
# update image
gcloud --project $PROJECT_ID \
    run services update $SERVICE_NAME \
    --image $IMAGE_URI \
    --platform managed \
    --region europe-west3
# send traffic to latest
gcloud --project $PROJECT_ID \
    run services update-traffic $SERVICE_NAME \
    --platform managed \
    --region europe-west3 \
    --to-latest

Разрушение инфраструктуры

Мы можем использовать _destroy.sh, который очищает ведра и уничтожает ресурсы, созданные с помощью Terraform.

# get bucket names from terraform output
INPUT_BUCKET=$(cd terraform && terraform output -json | jq -r .input_bucket.value)
OUTPUT_BUCKET=$(cd terraform && terraform output -json | jq -r .output_bucket.value)
gsutil rm "gs://${INPUT_BUCKET}/**"
gsutil rm "gs://${OUTPUT_BUCKET}/**"
(cd terraform && 
    terraform state rm "google_project_iam_member.project_owner" &&
    terraform destroy)

Мы должны увидеть что-то вроде следующего:

...
google_service_account.service_account: Destruction complete after 1s
google_storage_bucket.storage_output_bucket: Destruction complete after 1s
google_pubsub_topic.pubsub_topic: Destruction complete after 2s
google_project_service.cloud_run_service: Still destroying... [id=cloud-runner-ce0d/run.googleapis.com, 10s elapsed]
google_project_service.cloud_run_service: Destruction complete after 13s
google_project.project: Destroying... [id=projects/cloud-runner-ce0d]
google_project.project: Destruction complete after 3s
random_id.id: Destroying... [id=KM8]
random_id.id: Destruction complete after 0s
Destroy complete! Resources: 14 destroyed.

Больше замечаний

У сервисов Google есть некоторые ограничения (по состоянию на 2021–04–24). Например, существует максимальный тайм-аут 15 минут, Google Pub / Sub имеет максимальное время подтверждения 10 минут. Это делает его бесполезным для более трудоемких задач. Вы можете использовать большие ресурсы, чтобы ускорить время обработки, хотя есть также ограничение на объем памяти, который мы можем выделить. Наш сценарий может обрабатывать только один запрос за раз, поэтому мы устанавливаем для контейнера concurrency значение 1. Мы можем ускорить процесс, используя больше потоков в контейнере и допуская некоторое распараллеливание.

В любом случае Cloud Run изначально обрабатывает несколько запросов, используя несколько контейнеров. Максимальное количество параллельных контейнеров Cloud Run также настраивается и по умолчанию равно 1000 (!).

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

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

Наконец, вы должны учитывать стоимость этой установки. Вышеупомянутая настройка и несколько тестовых запусков находятся в пределах бесплатных уровней (по состоянию на 2021–04–24). Стоимость масштабируется с минимальным количеством контейнеров, доступных в любое время, ресурсами контейнера, количеством запросов и временем выполнения каждого запроса. Вы можете использовать калькулятор затрат, чтобы оценить ожидаемые затраты. Стоимость сильно возрастает, если вам нужно, чтобы контейнеры были там все время или вам нужно гораздо больше ресурсов.

Вывод

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

Если вы используете Cloud Run, вы получаете несколько обязательных и полезных функций из коробки: мониторинг запросов и ресурсов, ведение журнала, масштабирование. Использование Pub / Sub гарантирует правильную обработку повторных попыток и мертвых писем. В случае, если требуется более общая настройка, например, без интеграции Pub / Sub, можно рассмотреть Cloud Functions вместо Cloud Run, поскольку они обеспечивают большую гибкость в части вызова.

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

Тем не менее, мы не можем скрыть тот факт, что мы используем проприетарное программное обеспечение, которое соответствует некоторым средним требованиям, а не требованиям нашего конкретного варианта использования. Мы можем настроить только то, что позволяет нам сервис, и в нашем случае мы должны отказаться от некоторых ограничений Cloud Run и Pub / Sub, например, в отношении тайм-аутов, максимального времени выполнения и ресурсов.

Первоначально опубликовано на https://blog.telsemeyer.com.