GCP の Cloud Run のようなインスタンス毎の課金制 PaaS サービスをはじめ、Cloud Functions や AWS の Lambda といった FaaS の永遠の課題はコールドスタート問題ですね!
今回は、私が日頃からお世話になっている Cloud Run について、言語別にコールドスタート時間を計測してみました。
言語は私が日頃使っている or 今後使う可能性の高いものをチョイスしました。
- Python
 - Node.js(JavaScript)
 - Go
 - C++
 - Rust
 
TL;DR #
2024 年 3 月 13 日から 20 日までの1週間で計測したものです。各インスタンスに 30 分毎にアクセスし、そのたびのコールドスタート時間を示したグラフです。
なお Cloud Run インスタンスは以下の設定です。
- CPU:1
 - メモリ:512MiB
 - CPU ブースト機能:無効化
 
Python
最小 1 秒から最大 3 秒のレンジで推移。平均値で見ると、1.5 秒から 2 秒くらいかな。

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

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

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

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

結果まとめ #
目分量で計測しただいたいの平均値ではありますが、コールドスタートが速い順に並べると以下になりました。
- C++ と Rust:コールドスタート 0.1 秒以下
 - Go:コールドスタート 0.1〜0.15 秒
 - Python:コールドスタート 1.5〜2 秒
 - Node.js:コールドスタート 3 秒
 
計測手順 #
準備したコードとデプロイ方法について #
Cloud Run のドキュメントページに各言語ごとのクイックスタート手順が示されています。
- Quickstarts | Cloud Run Documentation | Google Cloud
 - https://cloud.google.com/run/docs/quickstarts
 
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 のサンプルは存在しなかったため、以下の記事の手順をそのまま利用させていただきました。
- Cloud Run で Rust の API Server を動かす | Zenn
 - https://zenn.dev/kowaremonoid/articles/7e077f9eb4439b
 
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 リクエストを飛ばすようにしました。

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