일반적인 개발 팁

이 가이드에서는 Knative serving 서비스의 설계, 구현, 테스트, 배포를 위한 권장사항을 제공합니다. 자세한 내용은 기존 서비스 마이그레이션을 참조하세요.

효과적인 서비스 작성

이 섹션에서는 Knative serving 서비스를 설계하고 구현하기 위한 일반적인 권장사항을 설명합니다.

백그라운드 활동 방지

Knative serving에서 실행 중인 애플리케이션이 요청 처리를 완료하면 컨테이너 인스턴스의 CPU 액세스가 사용 중지되거나 크게 제한됩니다. 따라서 요청 핸들러의 범위를 벗어나 실행되는 백그라운드 스레드 또는 루틴을 시작해서는 안 됩니다.

백그라운드 스레드를 실행하면 예기치 않은 동작이 발생할 수 있습니다. 같은 컨테이너 인스턴스에 대한 모든 후속 요청으로 정지된 백그라운드 활동이 다시 시작될 수 있기 때문입니다.

백그라운드 활동이란 HTTP 응답이 전달된 뒤에 발생하는 모든 활동입니다. 응답을 전달하기 전에 모든 비동기식 작업이 완료되도록 코드를 검토합니다.

서비스에서 쉽게 드러나지 않는 백그라운드 활동이 발생한 것으로 의심되는 경우 로그를 확인할 수 있습니다. HTTP 요청 항목 이후에 로깅되는 항목을 살펴보세요.

임시 파일 삭제

Cloud Run 환경에서 디스크 스토리지는 메모리 내의 파일 시스템입니다. 디스크에 작성된 파일은 서비스에 제공되는 메모리를 사용하며 호출 시 그대로 유지됩니다. 이 파일을 명시적으로 삭제하지 못하면 결국 메모리 부족 오류가 발생한 다음 콜드 스타트가 진행될 수 있습니다.

성능 최적화

이 섹션에서는 성능 최적화를 위한 권장사항을 설명합니다.

신속한 서비스 시작

컨테이너 인스턴스는 필요에 따라 확장되므로 실행 환경을 완전히 초기화하는 것이 일반적입니다. 이러한 종류의 초기화를 '콜드 스타트'라고 합니다. 클라이언트 요청이 콜드 스타트를 트리거하면 컨테이너 인스턴스 시작 시 추가 지연 시간이 발생합니다.

시작 루틴은 다음으로 구성됩니다.

  • 서비스 시작
    • 컨테이너 시작
    • entrypoint 명령어를 실행하여 서버 시작
  • 개방형 서비스 포트 확인

서비스 시작 속도를 최적화하면 컨테이너 인스턴스에서 요청 제공을 지연시키는 지연 시간을 최소화할 수 있습니다.

현명한 종속 항목 사용

Node.js의 모듈 가져오기와 같은 종속 라이브러리에서 동적 언어를 사용하면 콜드 스타트 중 해당 모듈의 로드 시간이 지연 시간에 추가될 수 있습니다. 다음과 같은 방법으로 시작 지연 시간을 줄일 수 있습니다.

  • 린(lean) 서비스를 빌드하는 데 필요한 종속 항목의 수와 크기를 최소화합니다.
  • 언어에서 지원하는 경우 자주 사용되지 않는 코드를 느리게 로드합니다.
  • PHP의 Composer 자동 로더 최적화와 같은 코드 로딩 최적화를 사용합니다.

전역 변수 사용

Knative serving에서는 요청 간에 서비스 상태가 보존된다고 가정할 수 없습니다. 하지만 Knative serving은 진행 중인 트래픽을 제공하기 위해 개별 컨테이너 인스턴스를 재사용하므로 후속 호출에서 변수 값 재사용을 허용하도록 전역 범위의 변수를 선언할 수 있습니다. 이러한 재사용의 이점이 개별 요청에 적용되는지 여부는 사전에 알 수 없습니다.

또한 서비스 요청마다 객체를 다시 만드는 데 비용이 많이 드는 경우 메모리에 객체를 캐싱할 수 있습니다. 이러한 객체를 요청 로직에서 전역 범위로 이동하면 성능이 향상될 수 있습니다.

Node.js

const functions = require('@google-cloud/functions-framework');

// TODO(developer): Define your own computations
const {lightComputation, heavyComputation} = require('./computations');

// Global (instance-wide) scope
// This computation runs once (at instance cold-start)
const instanceVar = heavyComputation();

/**
 * HTTP function that declares a variable.
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
functions.http('scopeDemo', (req, res) => {
  // Per-function scope
  // This computation runs every time this function is called
  const functionVar = lightComputation();

  res.send(`Per instance: ${instanceVar}, per function: ${functionVar}`);
});

Python

import time

import functions_framework


# Placeholder
def heavy_computation():
    return time.time()


# Placeholder
def light_computation():
    return time.time()


# Global (instance-wide) scope
# This computation runs at instance cold-start
instance_var = heavy_computation()


@functions_framework.http
def scope_demo(request):
    """
    HTTP Cloud Function that declares a variable.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """

    # Per-function scope
    # This computation runs every time this function is called
    function_var = light_computation()
    return f"Instance: {instance_var}; function: {function_var}"

Go


// h is in the global (instance-wide) scope.
var h string

// init runs during package initialization. So, this will only run during an
// an instance's cold start.
func init() {
	h = heavyComputation()
	functions.HTTP("ScopeDemo", ScopeDemo)
}

// ScopeDemo is an example of using globally and locally
// scoped variables in a function.
func ScopeDemo(w http.ResponseWriter, r *http.Request) {
	l := lightComputation()
	fmt.Fprintf(w, "Global: %q, Local: %q", h, l)
}

자바


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class Scopes implements HttpFunction {
  // Global (instance-wide) scope
  // This computation runs at instance cold-start.
  // Warning: Class variables used in functions code must be thread-safe.
  private static final int INSTANCE_VAR = heavyComputation();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Per-function scope
    // This computation runs every time this function is called
    int functionVar = lightComputation();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Instance: %s; function: %s", INSTANCE_VAR, functionVar);
  }

  private static int lightComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).sum();
  }

  private static int heavyComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

전역 변수의 지연 초기화 수행

전역 변수 초기화는 항상 시작 중에 수행되므로 콜드 스타트 시간이 늘어납니다. 자주 사용하지 않는 객체에 지연 초기화를 사용하면 시간 비용을 줄이고 콜드 스타트 시간을 줄일 수 있습니다.

Node.js

const functions = require('@google-cloud/functions-framework');

// Always initialized (at cold-start)
const nonLazyGlobal = fileWideComputation();

// Declared at cold-start, but only initialized if/when the function executes
let lazyGlobal;

/**
 * HTTP function that uses lazy-initialized globals
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
functions.http('lazyGlobals', (req, res) => {
  // This value is initialized only if (and when) the function is called
  lazyGlobal = lazyGlobal || functionSpecificComputation();

  res.send(`Lazy global: ${lazyGlobal}, non-lazy global: ${nonLazyGlobal}`);
});

Python

import functions_framework

# Always initialized (at cold-start)
non_lazy_global = file_wide_computation()

# Declared at cold-start, but only initialized if/when the function executes
lazy_global = None


@functions_framework.http
def lazy_globals(request):
    """
    HTTP Cloud Function that uses lazily-initialized globals.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    global lazy_global, non_lazy_global

    # This value is initialized only if (and when) the function is called
    if not lazy_global:
        lazy_global = function_specific_computation()

    return f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}."

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"log"
	"net/http"
	"sync"

	"cloud.google.com/go/storage"
	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

// client is lazily initialized by LazyGlobal.
var client *storage.Client
var clientOnce sync.Once

func init() {
	functions.HTTP("LazyGlobal", LazyGlobal)
}

// LazyGlobal is an example of lazily initializing a Google Cloud Storage client.
func LazyGlobal(w http.ResponseWriter, r *http.Request) {
	// You may wish to add different checks to see if the client is needed for
	// this request.
	clientOnce.Do(func() {
		// Pre-declare an err variable to avoid shadowing client.
		var err error
		client, err = storage.NewClient(context.Background())
		if err != nil {
			http.Error(w, "Internal error", http.StatusInternalServerError)
			log.Printf("storage.NewClient: %v", err)
			return
		}
	})
	// Use client.
}

자바


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class LazyFields implements HttpFunction {
  // Always initialized (at cold-start)
  // Warning: Class variables used in Servlet classes must be thread-safe,
  // or else might introduce race conditions in your code.
  private static final int NON_LAZY_GLOBAL = fileWideComputation();

  // Declared at cold-start, but only initialized if/when the function executes
  // Uses the "initialization-on-demand holder" idiom
  // More information: https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
  private static class LazyGlobalHolder {
    // Making the default constructor private prohibits instantiation of this class
    private LazyGlobalHolder() {}

    // This value is initialized only if (and when) the getLazyGlobal() function below is called
    private static final Integer INSTANCE = functionSpecificComputation();

    private static Integer getInstance() {
      return LazyGlobalHolder.INSTANCE;
    }
  }

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    Integer lazyGlobal = LazyGlobalHolder.getInstance();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Lazy global: %s; non-lazy global: %s%n", lazyGlobal, NON_LAZY_GLOBAL);
  }

  private static int functionSpecificComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).sum();
  }

  private static int fileWideComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

동시 실행 최적화

Knative serving 인스턴스는 구성 가능한 최대 동시 실행 값까지 여러 요청을 동시에 처리할 수 있습니다. 이는 concurrency = 1을 사용하는 Cloud Run 함수와 다릅니다.

코드에 특정 동시 실행 요구사항이 없는 경우에는 기본 최대 동시 실행 설정을 유지해야 합니다.

서비스의 동시 실행 조정

각 컨테이너 인스턴스가 처리할 수 있는 동시 요청 수는 기술 스택과 변수 및 데이터베이스 연결과 같은 공유 리소스 사용에 의해 제한될 수 있습니다.

안정적인 최대 동시 실행을 위해 서비스를 최적화하려면 다음 안내를 따르세요.

  1. 서비스 성능을 최적화합니다.
  2. 모든 코드 수준 동시 실행 구성에서 예상되는 동시 실행 지원 수준을 설정합니다. 일부 기술 스택에는 이러한 설정이 필요하지 않습니다.
  3. 서비스를 배포합니다.
  4. 서비스의 Knative serving 동시 실행을 코드 수준 구성과 같거나 낮게 설정합니다. 코드 수준 구성이 없으면 예상 동시 실행을 사용하세요.
  5. 구성 가능한 동시 실행을 지원하는 부하 테스트 도구를 사용합니다. 예상되는 부하 및 동시 실행 조건에서 서비스가 안정적으로 유지되는지 확인해야 합니다.
  6. 서비스가 제대로 작동하지 않으면 1단계로 이동하여 서비스를 개선하거나 2단계로 이동하여 동시 실행을 줄입니다. 서비스가 제대로 작동하면 2단계로 돌아가서 동시 실행을 늘립니다.

안정적인 최대 동시 실행 횟수를 찾을 때까지 위 단계를 계속 반복합니다.

동시 실행에 메모리 일치

서비스에서 요청을 처리할 때마다 일정량의 추가 메모리가 필요합니다. 따라서 동시 실행을 조정하는 경우 메모리 한도도 조정해야 합니다.

변경 가능한 전역 상태 방지

동시 실행 컨텍스트에서 변경 가능한 전역 상태를 활용하려면 코드에서 추가 단계를 수행하여 안전하게 수행하세요. 전역 변수를 일회성 초기화로 제한하여 경합을 최소화하고 성능에 설명된 대로 재사용합니다.

여러 요청을 동시에 처리하는 서비스에서 변경 가능한 전역 변수를 사용하는 경우 잠금 또는 뮤텍스를 사용하여 경합 상태를 방지해야 합니다.

컨테이너 보안

컨테이너식 애플리케이션에는 많은 범용 소프트웨어 보안 권장사항이 적용됩니다. 일부 권장사항은 컨테이너에만 적용되거나 컨테이너의 원칙과 아키텍처에 부합합니다.

컨테이너 보안을 개선하려면 다음 안내를 따르세요.

  • Google 기본 이미지와 같이 능동적으로 유지되고 안전한 기본 이미지를 사용하거나 Docker Hub의 공식 이미지를 사용합니다.

  • 컨테이너 이미지를 정기적으로 재빌드하고 서비스를 다시 배포하여 서비스에 보안 업데이트를 적용합니다.

  • 서비스를 실행하는 데 필요한 항목만 컨테이너에 포함합니다. 추가 코드, 패키지, 도구는 잠재적인 보안 취약점입니다. 관련 있는 성능 영향은 위의 내용을 참조하세요.

  • 특정 소프트웨어 및 라이브러리 버전을 포함하는 확정적 빌드 프로세스를 구현합니다. 이렇게 하면 확인되지 않은 코드가 컨테이너에 포함되지 않습니다.

  • Dockerfile USER을 사용하여 컨테이너가 root 이외의 사용자로 실행되도록 설정합니다. 일부 컨테이너 이미지에는 이미 특정 사용자가 구성되어 있을 수 있습니다.

보안 스캔 자동화

Artifact Registry에 저장된 컨테이너 이미지의 보안 스캔을 위해 취약점 스캔을 사용 설정합니다.

보안 컨테이너 이미지만 배포되도록 Binary Authorization을 추가로 사용할 수 있습니다.

최소 컨테이너 이미지 빌드

대형 컨테이너 이미지는 코드에 필요한 것보다 더 많은 항목을 포함하기 때문에 보안 취약점을 늘릴 수 있습니다.

Knative serving에서 컨테이너 이미지의 크기는 콜드 스타트 또는 요청 처리 시간에 영향을 미치지 않으며 컨테이너의 사용 가능한 메모리 계산에 포함되지 않습니다.

최소 컨테이너를 빌드하려면 다음과 같이 가벼운 기본 이미지를 만드는 것이 좋습니다.

Ubuntu는 크기가 더 크지만, 일반적으로 사용되는 기본 이미지로 보다 완벽하며 즉시 사용 가능한 서버 환경을 제공합니다.

서비스에 도구 사용량이 많은 빌드 프로세스가 있으면 다단계 빌드를 사용하여 런타임 시 컨테이너를 가볍게 유지하는 것이 좋습니다.

린(lean) 컨테이너 이미지 만들기에 대한 자세한 내용은 다음 리소스를 참조하세요.