Cloud Run のコールドスタート時間を言語別に計測してみた(Python/Node/Go/C++/Rust)

Cloud Run のコールドスタート時間を言語別に計測してみた(Python/Node/Go/C++/Rust)

March 23, 2024

GCP の Cloud Run のようなインスタンス毎の課金制 PaaS サービスをはじめ、Cloud Functions や AWS の Lambda といった FaaS の永遠の課題はコールドスタート問題ですね!

今回は、私が日頃からお世話になっている Cloud Run について、言語別にコールドスタート時間を計測してみました。

言語は私が日頃使っている or 今後使う可能性の高いものをチョイスしました。

  1. Python
  2. Node.js(JavaScript)
  3. Go
  4. C++
  5. Rust

TL;DR #

2024 年 3 月 13 日から 20 日までの1週間で計測したものです。各インスタンスに 30 分毎にアクセスし、そのたびのコールドスタート時間を示したグラフです。

なお Cloud Run インスタンスは以下の設定です。

  • CPU:1
  • メモリ:512MiB
  • CPU ブースト機能:無効化

Python

最小 1 秒から最大 3 秒のレンジで推移。平均値で見ると、1.5 秒から 2 秒くらいかな。

Python

Node.js

最小 2 秒くらいから最大 7 秒近くのレンジで推移。平均値で見ると、3 秒くらいかな。

Node.js

Go

最小 0.05 秒(50ms)くらいから最大 0.25 秒(250ms)近くのレンジで推移。平均値で見ると、0.1 秒から 0.15 秒くらいかな。

Go

C++

最小 0.05 秒(50ms)くらいから最大 0.3 秒(300ms)をわずかに超過くらいのレンジで推移。平均値で見ると、0.1 秒を少し下回るくらいかな。

C++

Rust

最小 0.05 秒(50ms)くらいから最大 0.2 秒(200ms)に満たないくらいのレンジで推移。平均値で見ると、0.1 秒を少し下回るくらいかな。

Rust

結果まとめ #

目分量で計測しただいたいの平均値ではありますが、コールドスタートが速い順に並べると以下になりました。

  1. C++ と Rust:コールドスタート 0.1 秒以下
  2. Go:コールドスタート 0.1〜0.15 秒
  3. Python:コールドスタート 1.5〜2 秒
  4. Node.js:コールドスタート 3 秒

計測手順 #

準備したコードとデプロイ方法について #

Cloud Run のドキュメントページに各言語ごとのクイックスタート手順が示されています。

Python、Node.js、Go、C++ #

Python、Node.js、Go、C++についてはドキュメントに従って以下のコードをコピペで用意のうえ、gcloud run deploy にて、ソースコードから直接 Cloud Run にデプロイしました。

import os

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    """Example Hello World route."""
    name = os.environ.get("NAME", "World")
    return f"Hello {name}!"

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
import express from 'express';
const app = express();

app.get('/', (req, res) => {
  const name = process.env.NAME || 'World';
  res.send(`Hello ${name}!`);
});

const port = parseInt(process.env.PORT) || 8080;
app.listen(port, () => {
  console.log(`helloworld: listening on port ${port}`);
});
// Sample run-helloworld is a minimal Cloud Run service.
package main

import (
        "fmt"
        "log"
        "net/http"
        "os"
)

func main() {
        log.Print("starting server...")
        http.HandleFunc("/", handler)

        // Determine port for HTTP service.
        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
                log.Printf("defaulting to port %s", port)
        }

        // Start HTTP server.
        log.Printf("listening on port %s", port)
        if err := http.ListenAndServe(":"+port, nil); err != nil {
                log.Fatal(err)
        }
}

func handler(w http.ResponseWriter, r *http.Request) {
        name := os.Getenv("NAME")
        if name == "" {
                name = "World"
        }
        fmt.Fprintf(w, "Hello %s!\n", name)
}
#include <google/cloud/functions/framework.h>
#include <cstdlib>

namespace gcf = ::google::cloud::functions;

auto hello_world_http() {
  return gcf::MakeFunction([](gcf::HttpRequest const& /*request*/) {
    std::string greeting = "Hello ";
    auto const* target = std::getenv("TARGET");
    greeting += target == nullptr ? "World" : target;
    greeting += "\n";

    return gcf::HttpResponse{}
        .set_header("Content-Type", "text/plain")
        .set_payload(greeting);
  });
}

int main(int argc, char* argv[]) {
  return gcf::Run(argc, argv, hello_world_http());
}

Rust #

Cloud Run のドキュメントに Rust のサンプルは存在しなかったため、以下の記事の手順をそのまま利用させていただきました。

use axum::{routing::get, Router};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // initialize tracing
    tracing_subscriber::fmt::init();

    // build our application with a route
    let app = Router::new()
        // `GET /` goes to `root`
        .route("/", get(root));

    // run our app with hyper
    // `axum::Server` is a re-export of `hyper::Server`
    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
    tracing::debug!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

// basic handler that responds with a static string
async fn root() -> &'static str {
    "Hello, World!"
}

Dockerfile

# Use the official Rust image.
# https://hub.docker.com/_/rust
FROM rust

# Copy local code to the container image.
WORKDIR /usr/src/app
COPY . .

# Install production dependencies and build a release artifact.
RUN cargo build --release

# Service must listen to $PORT environment variable.
# This default value facilitates local development.
ENV PORT 8080

# Run the web service on container startup.
ENTRYPOINT ["target/release/cloud-run-rust-test"]

アクセス方法について #

GCP の Cloud Scheduler を用いて、各言語のインスタンスに 30 分毎(*/30 * * * *)に HTTP リクエストを飛ばすようにしました。

Cloud Scheduler

計測結果 #

記事冒頭の TL;DR に示した通りでした!C++と Rust は速い、そして Go も速い!