Tips pengembangan umum

Panduan ini memberikan praktik terbaik untuk merancang, menerapkan, menguji, dan untuk men-deploy layanan penayangan Knative. Untuk tips lainnya, lihat Memigrasikan Layanan yang Sudah Ada.

Menulis layanan yang efektif

Bagian ini menjelaskan praktik terbaik umum untuk merancang dan menerapkan Layanan penyaluran Knative.

Menghindari aktivitas latar belakang

Ketika aplikasi yang berjalan pada penyaluran Knative selesai menangani permintaan, akses instance container ke CPU akan dinonaktifkan atau sangat dibatasi. Oleh karena itu, Anda tidak boleh memulai thread latar belakang atau rutinitas yang berjalan di luar cakupan pengendali permintaan.

Menjalankan thread latar belakang dapat mengakibatkan perilaku yang tidak terduga karena permintaan berikutnya ke instance penampung yang sama akan melanjutkan setiap permintaan yang ditangguhkan aktivitas latar belakang.

Aktivitas latar belakang meliputi segala sesuatu yang terjadi setelah respons HTTP dikirimkan. Tinjau kode Anda untuk memastikan semua operasi asinkron selesai sebelum Anda mengirimkan respons.

Jika Anda mencurigai mungkin ada aktivitas latar belakang di layanan Anda yang tidak jelas terlihat Anda dapat memeriksa log Anda: mencari apa pun yang yang dicatat setelah entri untuk permintaan HTTP.

Menghapus file sementara

Penyimpanan disk lingkungan Cloud Run adalah sistem file dalam memori. File yang ditulis ke disk menggunakan memori yang seharusnya disediakan untuk layanan Anda, dan bisa tetap dipertahankan di antara pemanggilan. Jika file ini tidak dihapus, dapat terjadi error kehabisan memori yang disusul dengan cold start.

Mengoptimalkan performa

Bagian ini menjelaskan praktik terbaik untuk mengoptimalkan performa.

Memulai layanan dengan cepat

Karena instance container diskalakan sesuai kebutuhan, metode umum adalah menginisialisasi lingkungan eksekusi sepenuhnya. Seperti ini inisialisasi disebut "cold start". Jika permintaan klien memicu cold maka startup instance container akan menghasilkan latensi tambahan.

Rutinitas startup terdiri dari:

  • Memulai layanan
    • Memulai container
    • Menjalankan entrypoint untuk memulai server.
  • Memeriksa port layanan yang terbuka.

Mengoptimalkan kecepatan startup layanan akan meminimalkan latensi yang menunda instance container agar tidak menayangkan permintaan.

Menggunakan dependensi dengan bijak

Jika Anda menggunakan bahasa dinamis dengan library dependen, seperti mengimpor modul di Node.js, waktu pemuatan modul tersebut akan menambahkan latensi selama cold start. Kurangi latensi startup dengan cara berikut:

  • Minimalkan jumlah dan ukuran dependensi untuk mem-build layanan yang efisien.
  • Memuat kode yang jarang digunakan secara lambat, jika bahasa Anda mendukung fitur ini.
  • Gunakan pengoptimalan pemuatan kode seperti pengoptimalan autoloader composer pada PHP.

Menggunakan variabel global

Dalam penayangan Knative, Anda tidak dapat berasumsi bahwa status layanan dipertahankan antara permintaan. Namun, penayangan Knative menggunakan kembali setiap instance container untuk menyalurkan traffic yang sedang berlangsung, sehingga Anda dapat mendeklarasikan variabel dalam cakupan global nilainya untuk digunakan kembali dalam pemanggilan berikutnya. Tidak dapat diprediksi sebelumnya apakah setiap permintaan menerima manfaat dari penggunaan ulang ini.

Anda juga dapat menyimpan cache objek di memori jika objek tersebut mahal untuk dibuat ulang pada setiap permintaan layanan. Memindahkan tindakan ini dari logika permintaan ke cakupan global akan menghasilkan performa yang lebih baik.

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)
}

Java


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();
  }
}

Menjalankan inisialisasi lambat variabel global

Inisialisasi variabel global selalu terjadi selama startup, yang dapat meningkatkan waktu cold start. Gunakan inisialisasi lambat untuk objek yang jarang digunakan guna menunda biaya dan mengurangi waktu cold start.

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.
}

Java


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();
  }
}

Mengoptimalkan permintaan serentak

Instance penyaluran Knative dapat melayani beberapa permintaan secara bersamaan, "konkurensi", hingga konkurensi maksimum yang dapat dikonfigurasi. Fungsi ini berbeda dengan fungsi Cloud Run, yang menggunakan concurrency = 1.

Anda harus mempertahankan setelan serentak maksimum default, kecuali jika kode Anda memiliki persyaratan konkurensi yang spesifik.

Menyesuaikan permintaan serentak untuk layanan Anda

Jumlah permintaan serentak yang dapat ditayangkan oleh setiap instance container dapat dibatasi oleh tumpukan teknologi dan penggunaan sumber daya bersama seperti variabel, dan koneksi database.

Untuk mengoptimalkan layanan Anda agar mencapai konkurensi maksimum yang stabil:

  1. Optimalkan performa layanan Anda.
  2. Tetapkan tingkat dukungan konkurensi yang diharapkan dalam konfigurasi konkurensi tingkat kode apa pun. Tidak semua technology stack memerlukan setelan tersebut.
  3. Deploy layanan Anda.
  4. Tetapkan konkurensi penyaluran Knative untuk layanan Anda sama atau kurang dari jumlah konfigurasi level kode. Jika tidak ada konfigurasi tingkat kode, gunakan konkurensi yang diharapkan.
  5. Menggunakan pengujian beban yang mendukung konkurensi yang dapat dikonfigurasi. Anda perlu memastikan bahwa layanan Anda tetap stabil di bawah beban dan konkurensi yang diharapkan.
  6. Jika layanan tidak berfungsi dengan baik, lanjutkan ke langkah 1 untuk meningkatkan layanan atau langkah 2 untuk mengurangi konkurensi. Jika layanan berfungsi dengan baik, kembali ke langkah 2 dan tingkatkan konkurensi.

Lanjutkan iterasi sampai Anda menemukan konkurensi maksimum yang stabil.

Mencocokkan memori dengan konkurensi

Setiap permintaan yang ditangani layanan Anda memerlukan sejumlah memori tambahan. Jadi, saat menyesuaikan konkurensi ke atas atau ke bawah, pastikan Anda juga menyesuaikan batas memori.

Menghindari status global yang dapat berubah

Jika ingin memanfaatkan status global yang dapat berubah dalam konteks konkurensi, pastikan mengambil langkah tambahan dalam kode Anda untuk memastikan hal ini dilakukan dengan aman. Untuk menghindari konflik, sebaiknya batasi variabel global dengan inisialisasi satu kali dan gunakan kembali, seperti yang dijelaskan di bagian Performa di atas.

Jika Anda menggunakan variabel global yang dapat diubah dalam layanan yang menangani beberapa permintaan secara bersamaan, pastikan untuk menggunakan kunci atau mutex untuk menghindari kondisi race.

Keamanan container

Sebagian besar praktik keamanan software yang umum digunakan juga berlaku untuk aplikasi yang dijalankan dalam container. Ada beberapa praktik yang entah khusus untuk kontainer atau sesuai dengan filosofi dan arsitektur dari kontainer.

Untuk meningkatkan keamanan container:

  • Gunakan image dasar yang dikelola secara aktif dan aman seperti Google gambar dasar atau image resmi Docker Hub.

  • Terapkan update keamanan pada layanan Anda dengan mem-build ulang image container secara rutin dan men-deploy ulang layanan Anda.

  • Sertakan hanya hal yang diperlukan ke dalam container untuk menjalankan layanan Anda. Kode, paket, dan alat tambahan dapat berpotensi terhadap kerentanan keamanan. Lihat di atas untuk dampak performa yang terkait.

  • Menerapkan proses build yang deterministik yang mencakup versi software dan library tertentu. Langkah ini mencegah kode yang tidak terverifikasi untuk dimasukkan ke dalam kontainer Anda.

  • Atur container Anda untuk dijalankan sebagai pengguna selain root dengan pernyataan USER Dockerfile. Beberapa image kontainer mungkin sudah memiliki pengguna tertentu yang telah dikonfigurasi sebelumnya.

Mengotomatiskan pemindaian keamanan

Aktifkan pemindaian kerentanan untuk pemindaian keamanan image container yang disimpan di Artifact Registry.

Anda juga dapat menggunakan Otorisasi Biner untuk memastikan bahwa hanya image container yang aman yang di-deploy.

Membangun image container minimal

Image container besar cenderung meningkatkan kerentanan keamanan karena mengandung lebih dari yang diperlukan oleh kode.

Pada penayangan Knative, ukuran image container tidak memengaruhi cold waktu mulai atau meminta waktu pemrosesan dan tidak dihitung dalam waktu yang tersedia dalam container Anda.

Untuk mem-build container minimal, sebaiknya gunakan image dasar yang efisien seperti:

Ubuntu berukuran lebih besar, tetapi merupakan image dasar yang biasa digunakan dengan lingkungan server siap pakai dan lebih lengkap.

Jika layanan Anda memiliki proses build yang menggunakan banyak alat, pertimbangkan untuk menggunakan build multi-tahap agar container tetap ringan saat runtime.

Referensi ini memberikan informasi lebih lanjut tentang cara membuat image container yang efisien: