← назад к разделу

Обычный веб-сервис живёт на машине, которая работает круглые сутки. Ночью к нему никто не ходит, а счёт идёт; днём приходит пик, и копии нужно добавлять руками или держать с запасом. Хочется отдать облаку код и не думать дальше: пусть само поднимает столько копий, сколько нужно, и гасит до нуля, когда запросов нет. Это и есть serverless: серверы существуют, но машину не выбирают, за загрузкой не следят и за простой не платят. В GCP так работают два сервиса, Cloud Run для контейнеров и Cloud Functions для отдельных обработчиков, и обвязка вокруг них: триггеры, Eventarc, API Gateway.

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

Обязательно

Cloud Run: обычный сервис без сервера

Сервис уже написан: Spring Boot, слушает порт 8080, настройки читает из переменных окружения. Переписывать его в «функции» ради облака никто не хочет. Cloud Run сделан под этот случай: ему отдают контейнер с обычным веб-приложением, а он выдаёт HTTPS-адрес, поднимает копии под нагрузку и гасит их до нуля, когда запросов нет. Ни кластера, ни машины. Если образа нет, Cloud Run соберёт его сам из исходников: в каталоге с pom.xml или package.json хватает одной команды.

gcloud run deploy orders --source . --region europe-west1 \
  --allow-unauthenticated \
  --memory 1Gi --concurrency 80 --min-instances 1 --cpu-boost

Флаги последней строки и есть ручки, вокруг которых крутится остальная статья: сколько запросов пускать в один экземпляр, сколько экземпляров держать всегда и ускорять ли старт. Первый из них меняет и код, и счёт. Один экземпляр Cloud Run обслуживает много запросов одновременно: по умолчанию до 80, --concurrency поднимает до 1000. У AWS Lambda и у функций первого поколения экземпляр занят ровно одним вызовом, и восемь параллельных запросов означают восемь экземпляров. Здесь один.

Cloud Run: до 80 запросов на экземпляр функция 1-го поколения, Lambda: 1 запрос на экземпляр один экземпляр, 1 vCPU запрос 1 запрос 2 запрос 3 запрос 4 запрос 5 запрос 6 запрос 7 запрос 8 экземпляр 1 запрос 1 экземпляр 2 запрос 2 экземпляр 3 запрос 3 экземпляр 4 запрос 4 экземпляр 5 запрос 5 экземпляр 6 запрос 6 экземпляр 7 запрос 7 экземпляр 8 запрос 8 счёт: процессор и память одного экземпляра пул соединений к базе: один счёт: восемь экземпляров, у каждого свой старт пулов соединений к базе: восемь

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

Следствий три. Код обязан быть потокобезопасным: внутри экземпляра запросы друг от друга не изолированы, и статическое поле, куда пишет один запрос, читает другой. Пул соединений к базе считают на экземпляр и умножают на их число: пул в десять соединений при сотне экземпляров даёт базе тысячу подключений, а у Cloud SQL лимит кончается раньше. И экземпляров при той же нагрузке нужно в десятки раз меньше, чем функций, а платят за экземпляры, не за запросы; к этому вернёмся в конце.

Процессор дают на время запроса

Сервис на Cloud Run отвечает 200, а письмо клиенту отправляет уже после ответа, в отдельном потоке через @Async. На машине это работало; здесь письма приходят через несколько минут или не приходят вовсе, а в логах поток будто замер. Это не ошибка кода. По умолчанию Cloud Run считает деньги по запросам (request-based billing) и выделяет экземпляру процессор только пока внутри есть хотя бы один незакрытый запрос. Ушёл последний ответ, и процессор урезают почти до нуля: поток жив, но не исполняется. Он доработает, когда придёт следующий запрос, если экземпляр к тому времени не погасят.

Выходов два. Первый честный: всё, что должно случиться, делают до ответа или в другом запросе. Письмо превращается в сообщение в Pub/Sub или задачу Cloud Tasks, а отправляет его отдельный обработчик. Второй, если фон нужен по-настоящему (метрики, прогрев кэша, потребитель очереди внутри сервиса): --no-cpu-throttling. Экземпляр получает процессор на всё время жизни, но и счёт идёт за всё время жизни, а не за секунды под запросами; секунда стоит примерно на четверть дешевле, а секунд становится много больше. Это переключает сервис на оплату по экземплярам (instance-based billing).

Cloud Functions: когда сервис не нужен

Задача другого масштаба: в бакет положили картинку, нужно сделать превью. Ради этого писать веб-сервер, Dockerfile и маршруты много. Cloud Functions: вы пишете одну функцию-обработчик, указываете, на какое событие её звать, и загружаете исходники; сборку и запуск берёт на себя облако. Ниша та же, что у AWS Lambda.

Первое, во что упирается человек при создании функции, выбор поколения. Первое поколение это отдельная платформа со своими потолками: один запрос на экземпляр, до 9 минут на вызов, до 8 ГБ памяти. Второе разворачивается как сервис Cloud Run и живёт по его правилам: те же экземпляры, та же конкурентность, те же лимиты. С 2024 года оно так и называется, Cloud Run functions, в консоли его ищут в разделе Cloud Run, а команда выката та же, только с флагом --function, который говорит, какой метод считать обработчиком.

Функция или сервис

Раз второе поколение и есть Cloud Run, выбор не между платформами, а между двумя способами выкатить код на одну и ту же платформу. Что отличается на самом деле:

функция 1-го поколенияфункция 2-го поколениясервис Cloud Run
что отдаётекодкод, контейнер соберуткод или свой контейнер
запросов на экземпляр1до 1000до 1000
время на вызовдо 9 миндо 60 мин по HTTP, до 9 мин по событиюдо 60 мин, по умолчанию 5
памятьдо 8 ГБдо 16 ГиБдо 16 ГиБ
точка входафункция-обработчикфункция-обработчикваш HTTP-сервер

Функцию берут, когда обработчик маленький и событийный: вебхук от платёжной системы, превью по загрузке, задача по расписанию. Как только появляются свой Dockerfile (нестандартный базовый образ, системные библиотеки, свой веб-сервер), несколько маршрутов с общим состоянием или пул соединений, который хочется настраивать, это уже сервис. Переезд не больно: тот же gcloud run deploy, без --function и со своим образом. Поэтому на практике многие делают всё на Cloud Run сразу, а сомнение решают в пользу сервиса: он умеет всё, что умеет функция, обратное неверно.

Триггеры: кто дёргает код

Ни сервис, ни функция сами не запускаются, их вызывает триггер: HTTP-запрос напрямую или через API Gateway, сообщение в теме Pub/Sub, новый объект в Cloud Storage, расписание Cloud Scheduler. С HTTP клиент ждёт ответа и сам решает, повторять ли; остальные три доставляют событие сами, и повторяют тоже сами.

Обработчик по сообщению из Pub/Sub отправляет клиенту письмо. Часть клиентов получает его дважды, хотя сообщение было одно. Так и должно быть: Pub/Sub гарантирует доставку не менее одного раза, а не ровно одного. Механика простая. Push-подписка делает POST на адрес сервиса и ждёт ответа в пределах срока подтверждения (ack deadline), по умолчанию 10 секунд, максимум 600. Ответ 200, 201, 202 или 204 и есть подтверждение; любой другой код, обрыв соединения или молчание дольше срока Pub/Sub считает неудачей и шлёт сообщение снова, выждав от 100 миллисекунд до минуты. Обработчик, который работает 20 секунд при сроке в 10, получает повтор ещё до того, как закончил первый раз, и письмо уходит дважды без единой ошибки в логах.

Отсюда три правила. Срок подтверждения ставят не меньше реального времени обработки, а обработку держат короткой: долгое переносят в отдельный шаг. Обработчик делают идемпотентным: у каждого сообщения есть messageId, и его или бизнес-ключ (номер заказа) записывают в базу до побочного действия, а на повторе ничего не делают. И заводят тему для недоставленных (dead-letter topic): после заданного числа попыток, от 5 до 100, по умолчанию 5, сообщение уходит туда, а не крутится в повторах; на эту тему вешают оповещение, потому что одно сообщение, которое всегда падает, иначе будет бить в обработчик до конца срока хранения.

Eventarc: события облака в одном формате

Своих событий мало: хочется реагировать на то, что делает облако. Завершилась пакетная задача, создали пользователя в Cloud SQL, положили таблицу в BigQuery. Без Eventarc это опрос API по таймеру. С ним вы описываете триггер «событие такого-то типа от такого-то сервиса», а Eventarc доставляет его в сервис Cloud Run или функцию в формате CloudEvents: стандартный конверт с полями id, source, type, time и телом data. Обработчик разбирает конверт одинаково, от какого бы сервиса событие ни пришло.

Источников два вида, и разница бьёт по первой же настройке. Часть сервисов публикует события напрямую: Cloud Storage, Pub/Sub, Firebase. Остальные, включая Cloud SQL и BigQuery, Eventarc видит только через журнал аудита: триггер это фильтр по serviceName и methodName записи в Cloud Audit Logs. А записи о доступе к данным (Data Access) по умолчанию не пишутся ни для одного сервиса, кроме BigQuery. Триггер создаётся без ошибок, события не приходят, и виноваты выключенные журналы, а не Eventarc. Их включают в настройках аудита проекта для нужного сервиса, помня, что они платные по объёму; подробнее в статье про безопасность и наблюдаемость.

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

API Gateway и связка целиком

Когда сервисов и функций несколько, перед ними ставят API Gateway: единую HTTPS-точку входа, которая по описанию OpenAPI направляет путь и метод в нужный сервис, проверяет ключ API или JWT и держит версии API. Ограничить частоту он тоже умеет, но грубее, чем привыкли в AWS: лимит задают квотой в запросах в минуту на проект-потребитель, и работает она только для клиентов с ключом API. Настройки вроде «столько запросов в секунду плюс всплеск» здесь нет.

синхронный путь: клиент ждёт ответа клиент браузер, мобильное API Gateway маршрут, ключ, JWT сервис Cloud Run ваш контейнер Firestore, Cloud SQL чтение и запись HTTPS фоновый путь: ответ клиенту уже ушёл Cloud Scheduler по расписанию Pub/Sub не менее одного раза функция Cloud Run functions push событие «заказ создан» Cloud Storage файл, превью, отчёт пишет результат

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

Граница между путями проходит по владельцу повтора: на верхнем ошибку получает клиент и повторяет сам, на нижнем повторяет Pub/Sub, поэтому всё, что стоит после него, обязано переживать дубли.

Холодный старт и жизнь экземпляра

API на Spring Boot в Cloud Run. Днём всё быстро, а первый запрос утром висит несколько секунд, и мобильный клиент успевает отвалиться по таймауту. Это холодный старт. Экземпляр, к которому 15 минут никто не обращался, Cloud Run гасит; следующий запрос ждёт, пока образ скачают, контейнер запустят, JVM поднимется и Spring соберёт контекст. Для обработчика на Node.js или Python это сотни миллисекунд, для Java на одном vCPU секунды.

запрос 1, первый после ночи старт JVM и контекста Spring, 3,5 с 3,5 с + 40 мс запрос 2, через секунду 40 мс запрос 3, ещё через секунду 40 мс 15 минут без запросов — экземпляр погашен запрос 4, после паузы старт JVM и контекста Spring, 3,5 с 3,5 с + 40 мс запрос 4 при min-instances=1 40 мс: экземпляр не гасили

Полоски в одном масштабе, цифры типичны для Spring Boot на одном vCPU: тёмная часть — время, пока поднимается экземпляр, светлая — сама обработка. Тёплый ответ на этом фоне почти не виден: 40 мс против 3,5 с. Последняя строка — та же пауза, но с одним экземпляром, который не гасят: старт не повторяется, а за простой идёт счёт.

Бороться можно тремя способами, и у каждого своя цена. --min-instances=1 держит один экземпляр всегда живым: старт не повторяется, а за простой идёт счёт по сниженному тарифу, но круглосуточно, то есть часть выгоды serverless вы отдаёте обратно. --cpu-boost временно даёт экземпляру больше процессора на время запуска, и старт Spring на одном vCPU укорачивается ощутимо, но не исчезает. Нативная сборка (GraalVM) убирает старт JVM и контекста, оставляя десятки миллисекунд, ценой долгой сборки и настройки рефлексии. У сервиса на Go тот же экземпляр поднимается за десятки миллисекунд без всяких ухищрений, у Node и Python за сотни, и холодный старт там почти не виден; поэтому цифры на схеме — это цена именно тяжёлой среды выполнения. Для фоновой задачи не нужно ничего из этого: лишние секунды перед разбором файла никто не заметит.

Раз экземпляр могут погасить в любой момент и поднять заново, в его памяти ничего не живёт дольше одного запроса. Счётчик в статическом поле у двух экземпляров показывает разные числа, а после паузы обнуляется; сессия пользователя пропадает вместе с экземпляром. Файловая система контейнера тоже в памяти: временные файлы съедают лимит памяти экземпляра и исчезают с ним. Состояние держат снаружи: в Firestore, Cloud SQL или Cloud Storage.

Сколько это стоит

При оплате по запросам счёт складывается из трёх частей: за секунды процессора экземпляра, за секунды памяти и за сами запросы, 0,40 доллара за миллион. Процессор и память считают по экземпляру, пока он обрабатывает хотя бы один запрос, с округлением до 100 миллисекунд. Отсюда главное следствие конкурентности: экземпляр, который держит 80 запросов сразу, стоит как один, а не как 80. У платформы с одним запросом на экземпляр те же 80 запросов оплачиваются восемью десятками экземпляров, и потому Cloud Run на ровной нагрузке выходит дешевле функций первого поколения в разы.

Порядок цифр: экземпляр с одним vCPU и 512 МиБ, пока обрабатывает запросы, стоит около 9 центов в час; при ста миллисекундах на запрос и конкурентности 80 он в пределе обслужит за этот час около трёх миллионов запросов, которые добавят ещё доллар с небольшим. В бесплатную квоту каждый месяц входят 2 миллиона запросов, 180 тысяч vCPU-секунд (50 часов работы одного vCPU) и 360 тысяч ГиБ-секунд (100 часов одного гигабайта), так что небольшой сервис с масштабированием до нуля часто не стоит ничего.

Дороже становится, когда от масштабирования до нуля отказываются. Минимальные экземпляры простаивают по сниженному тарифу, но 720 часов в месяц; оплата по экземплярам (--no-cpu-throttling) считает все секунды жизни. Сервис под ровной высокой нагрузкой при этом теряет главную выгоду, и его считают против обычной машины или GKE, как в статье про оптимизацию затрат.

Дополнительно: при первом чтении можно пропустить

Глубже: ночной пересчёт: Cloud Run jobs и Cloud Batchрасширенное

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

Cloud Run jobs. Тот же контейнер, что и у сервиса, но без HTTP-сервера: контейнер запускается, делает работу, выходит с кодом ноль, и это считается успехом. Задание описывают один раз (gcloud run jobs create nightly-recalc --image ... --tasks 10 --max-retries 2 --task-timeout 2h), запускают вручную, из Cloud Scheduler по расписанию или из конвейера. --tasks разбивает работу на параллельные части: каждая получает свой номер в переменной окружения и берёт свою долю данных, так десять задач считают отчёт в десять раз быстрее; предел на задачу до 24 часов, память и процессор те же, что у сервиса. Неудача задачи повторяется указанное число раз, и повтор обязан быть идемпотентным, о чём вся статья: пересчёт витрины за день заменяет секцию целиком, а не дописывает. Для Spring Boot это тот же образ с --spring.main.web-application-type=none и профилем задания, как у миграций в Kubernetes.

Cloud Batch. Когда работа не помещается в контейнер Cloud Run: нужны десятки машин, видеокарты, сотни гигабайт памяти или прерываемые машины ради цены. Batch поднимает виртуальные машины под задание, раскладывает по ним задачи, дожидается и гасит машины; платят за машины на время работы, и на spot это в разы дешевле. Это уровень ниже по лестнице вычислений, со всеми его настройками, и берут его для расчётов, а не для пересчёта витрины.

Выбор простой: задача укладывается в часы, один контейнер и до 16 ГиБ памяти на задачу, значит Cloud Run jobs; нужны машины, видеокарты или тысячи задач, значит Batch. Функция по расписанию годится для минуты работы, но не для часа: у неё предел времени и один запрос на экземпляр.

Коротко

  • Cloud Run запускает обычный контейнер с HTTP-сервером; функция второго поколения это тот же Cloud Run с готовой сборкой и подпиской на событие. Свой Dockerfile или несколько маршрутов значат сервис, сомнение тоже.
  • Один экземпляр держит до 80 запросов (до 1000). Код потокобезопасен, пул к базе умножают на число экземпляров, а счёт идёт за экземпляры, не за запросы.
  • По умолчанию процессор выделяют только пока открыт запрос: фоновая работа после ответа замирает. Либо уносить её в Pub/Sub и Cloud Tasks, либо --no-cpu-throttling и платить за всё время жизни.
  • Pub/Sub доставляет не менее одного раза: ответ не-2xx или молчание дольше срока подтверждения (10 с по умолчанию, до 600) даёт повтор. Обработчик идемпотентен по messageId, срок не меньше времени работы, тема для недоставленных с оповещением.
  • Eventarc доставляет CloudEvents; для Cloud SQL, BigQuery и большинства сервисов источник это журнал аудита, и без включённых журналов доступа к данным события не приходят. Доставка та же, что у Pub/Sub.
  • Экземпляр гасят после 15 минут простоя; холодный старт Spring на одном vCPU это секунды, у Go десятки миллисекунд. Лечат --min-instances, --cpu-boost и нативной сборкой, каждое со своей ценой. В памяти и на диске экземпляра ничего не живёт дольше запроса.
  • Счёт: vCPU-секунды и ГиБ-секунды экземпляра под запросами плюс 0,40 доллара за миллион запросов; 2 миллиона запросов и 50 часов vCPU в месяц бесплатно. Минимум экземпляров и оплата по экземплярам отдают часть выгоды обратно.
  • Работу без запроса запускают заданиями: Cloud Run jobs тем же контейнером с параллельными задачами до 24 часов и повторами, Cloud Batch когда нужны машины, видеокарты или spot; функция по расписанию только для минут.

Что почитать дальше

  • Где запускать сервис в GCP: Cloud Run рядом с Compute Engine и GKE, чтобы видеть, от чего отказываетесь и что получаете.
  • Управляемые данные: Pub/Sub как очередь и коннектор Cloud SQL, через который экземпляры ходят в базу без внешнего IP.
  • Безопасность и наблюдаемость: журналы аудита, от которых зависят триггеры Eventarc, и трассировка запроса через шлюз и сервис.
  • Serverless в AWS: та же модель с другими ручками, Lambda с provisioned concurrency и SnapStart вместо min-instances и cpu-boost.