준비 요청 구성으로 성능 향상

준비 요청을 사용하면 앱 코드가 새로 생성된 인스턴스에 로드되는 동안 요청 및 응답의 지연 시간을 줄일 수 있습니다.

App Engine이 새 인스턴스에 앱 코드를 로드해야 하는 경우가 종종 있습니다. 다음과 같은 상황에서 인스턴스를 로드할 수 있습니다.

  • 특정 버전의 앱을 다시 배포하는 경우
  • 요청의 부하가 현재 실행 중인 인스턴스 집합의 용량을 초과해 새 인스턴스가 생성되는 경우
  • 기본 인프라나 실제 하드웨어를 유지보수 및 수리하는 경우

앱 코드를 새 인스턴스에 로드하면 로드 요청이 발생할 수 있습니다. 로드 요청으로 인해 사용자의 요청 지연 시간이 증가할 수 있으나 준비 요청을 사용하면 이러한 지연 시간을 방지할 수 있습니다. 준비 요청은 실시간 요청이 새 인스턴스에 도달하기 전에 앱 코드를 이 인스턴스에 로드합니다.

애플리케이션에 준비 요청을 사용 설정하면 App Engine이 애플리케이션에 새 인스턴스가 필요한 시점을 감지하고 준비 요청을 시작하여 새 인스턴스를 초기화합니다. 하지만 이러한 감지 시도가 항상 성공하는 것은 아닙니다. 따라서 앱에 준비 요청이 사용 설정되어 있더라도 로드 요청이 발생할 수 있습니다. 예를 들어 앱에서 어떠한 트래픽도 처리하고 있지 않으면 앱에 대한 최초 요청은 항상 준비 요청이 아닌 로드 요청이 됩니다.

준비 요청은 App Engine 애플리케이션에 대한 다른 요청과 마찬가지로 인스턴스 시간을 사용합니다. 준비 요청을 사용 설정한 경우 대부분 애플리케이션은 로드 요청이 아닌 준비 요청에서 초기화되므로 인스턴스 시간이 거의 증가하지 않습니다. 그러나 준비 요청 중에 사전 캐싱과 같은 추가 작업을 수행하면 인스턴스 시간 사용량이 증가할 수 있습니다. min_idle_instances0보다 높게 설정하면 인스턴스가 처음 시작될 때 준비 요청이 발생할 수 있지만 그 후에도 인스턴스를 계속 사용할 수 있습니다.

준비 요청 사용 설정

준비 요청은 사용자가 제공한 구성에 맞게 인스턴스 자동 확장을 제어하는 App Engine 스케줄러에서 사용됩니다. 준비 요청을 사용 설정하면 App Engine이 /_ah/warmupGET 요청을 보냅니다. 이 요청에 필요한 핸들러를 구현하여 애플리케이션 데이터 사전 캐싱과 같은 애플리케이션별 태스크를 수행할 수 있습니다.

스케줄러는 인스턴스가 더 많이 필요하다고 판단하면 인스턴스를 시작합니다. 스케줄러는 준비 요청을 사용하여 인스턴스를 시작하므로, 준비 요청이 중지되더라도 로그에 나타날 수 있습니다.

준비 요청이 항상 호출되는 것은 아닙니다. 예를 들어 첫 번째로 시작되는 인스턴스이거나 트래픽 양이 갑자기 증가하는 등의 일부 경우에는 로드 요청이 대신 전송됩니다. 하지만 준비 요청이 사용 설정된 경우에는 이미 준비된 인스턴스에 요청을 보내는 것이 '최고의 방법'입니다.

준비 요청을 사용 설정하려면 app.yaml 파일의 inbound_services 지시문에 warmup 요소를 추가합니다. 예를 들면 다음과 같습니다.

inbound_services:
- warmup

핸들러 만들기

/_ah/warmup으로 전송된 요청을 처리하는 핸들러를 만듭니다. 핸들러는 앱에 필요한 준비 로직을 수행해야 합니다.


// Sample warmup demonstrates usage of the /_ah/warmup handler.
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"cloud.google.com/go/storage"
)

var startupTime time.Time
var client *storage.Client

func main() {
	// Perform required setup steps for the application to function.
	// This assumes any returned error requires a new instance to be created.
	if err := setup(context.Background()); err != nil {
		log.Fatalf("setup: %v", err)
	}

	// Log when an appengine warmup request is used to create the new instance.
	// Warmup steps are taken in setup for consistency with "cold start" instances.
	http.HandleFunc("/_ah/warmup", func(w http.ResponseWriter, r *http.Request) {
		log.Println("warmup done")
	})
	http.HandleFunc("/", indexHandler)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}

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

// setup executes per-instance one-time warmup and initialization actions.
func setup(ctx context.Context) error {
	// Store the startup time of the server.
	startupTime = time.Now()

	// Initialize a Google Cloud Storage client.
	var err error
	if client, err = storage.NewClient(ctx); err != nil {
		return err
	}

	return nil
}

// indexHandler responds to requests with our greeting.
func indexHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	uptime := time.Since(startupTime).Seconds()
	fmt.Fprintf(w, "Hello, World! Uptime: %.2fs\n", uptime)
}