טיפים כלליים לפיתוח

במדריך הזה מפורטות שיטות מומלצות לתכנון, להטמעה, לבדיקה ולפריסה של שירות Knative serving. טיפים נוספים זמינים במאמר העברת שירות קיים.

כתיבת שירותים יעילים

בקטע הזה מתוארות שיטות מומלצות כלליות לתכנון ולהטמעה של שירות Knative Serving.

הימנעות מפעילויות ברקע

כשעיבוד הבקשה באפליקציה שפועלת ב-Knative serving מסתיים, הגישה של מופע הקונטיינר למעבד מושבתת או מוגבלת מאוד. לכן, לא מומלץ להפעיל תהליכים או שגרות ברקע שפועלים מחוץ להיקף של מטפלי הבקשות.

הפעלת שרשורים ברקע עלולה לגרום להתנהגות בלתי צפויה, כי כל בקשה עוקבת לאותו מופע של מאגר התגים מפעילה מחדש כל פעילות ברקע שהושהתה.

פעילות ברקע היא כל מה שקורה אחרי שתגובת ה-HTTP נמסרת. צריך לבדוק את הקוד כדי לוודא שכל הפעולות האסינכרוניות מסתיימות לפני ששולחים את התגובה.

אם אתם חושדים שיש פעילות ברקע בשירות שלכם שלא ברורה, אתם יכולים לבדוק את היומנים: חפשו כל דבר שמתועד אחרי הרשומה של בקשת ה-HTTP.

מחיקת קבצים זמניים

בסביבת Cloud Run, אחסון הדיסק הוא מערכת קבצים בזיכרון. קבצים שנכתבים לדיסק צורכים זיכרון שזמין לשירות, ויכולים להישמר בין הפעלות. אם לא תמחקו את הקבצים האלה, בסופו של דבר תופיע שגיאה שמעידה על חוסר זיכרון, והמכשיר יופעל מחדש.

אופטימיזציה של הביצועים

בקטע הזה מתוארות שיטות מומלצות לאופטימיזציה של הביצועים.

הפעלה מהירה של שירותים

מכיוון שמתבצעת התאמה של מספר המופעים של הקונטיינר לפי הצורך, שיטה טיפוסית היא לאתחל את סביבת ההרצה באופן מלא. סוג כזה של אתחול נקרא "הפעלה במצב התחלתי (cold start)". אם בקשת לקוח מפעילה הפעלה מההתחלה (cold start), הפעלת מופע המאגר גורמת לזמן אחזור נוסף.

תרחיש ההפעלה כולל:

  • הפעלת השירות
    • הפעלת הקונטיינר
    • מריצים את הפקודה entrypoint כדי להפעיל את השרת.
  • בודקים אם יציאת השירות פתוחה.

אופטימיזציה של מהירות הפעלת השירות מצמצמת את זמן האחזור שגורם לעיכוב בהגשת בקשות על ידי מופע של קונטיינר.

שימוש חכם בתלות

אם אתם משתמשים בשפה דינמית עם ספריות תלויות, כמו ייבוא מודולים ב-Node.js, זמן הטעינה של המודולים האלה מוסיף חביון במהלך הפעלה במצב התחלתי (cold start). כדי לקצר את זמן האחזור של ההפעלה:

  • כדי ליצור שירות יעיל, מומלץ לצמצם את מספר התלויות ואת הגודל שלהן.
  • אם השפה תומכת בכך, כדאי להשתמש בטעינה מדורגת של קוד שלא נמצא בשימוש לעיתים קרובות.
  • משתמשים באופטימיזציות של טעינת קוד, כמו composer autoloader optimization של PHP.

שימוש במשתנים גלובליים

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

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

ביצוע אתחול עצלני של משתנים גלובליים

האתחול של משתנים גלובליים תמיד מתרחש במהלך ההפעלה, מה שמגדיל את זמן ההפעלה במצב התחלתי (cold start). כדאי להשתמש באתחול עצל כדי לדחות את עלות הזמן ולקצר את זמן ההפעלה במצב התחלתי (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  # noqa: F824

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

אופטימיזציה של מספר הבקשות המקבילות

מופעי Knative serving יכולים לטפל בכמה בקשות בו-זמנית, עד מקסימום מקביליות שניתן להגדרה. ההגדרה הזו שונה מפונקציות Cloud Run, שבהן נעשה שימוש ב-concurrency = 1.

כדאי להשאיר את הגדרת ברירת המחדל של מספר ההפעלות המקסימלי בו-זמנית, אלא אם בקוד יש דרישות ספציפיות לגבי מספר ההפעלות בו-זמנית.

התאמת מספר החיבורים בו-זמנית בשירות

מספר הבקשות בו-זמנית שכל מופע של קונטיינר יכול לטפל בהן יכול להיות מוגבל על ידי סטאק תוכנות והשימוש במשאבים משותפים כמו משתנים וחיבורים למסד נתונים.

כדי לבצע אופטימיזציה של השירות כך שיוכל לתמוך במספר מקסימלי של משתמשים בו-זמנית:

  1. אופטימיזציה של ביצועי השירות.
  2. מגדירים את רמת התמיכה הצפויה בריבוי משימות בו-זמנית בכל הגדרה של ריבוי משימות בו-זמנית ברמת הקוד. לא בכל מחסנית טכנולוגיות נדרשת הגדרה כזו.
  3. פורסים את השירות.
  4. מגדירים את מספר הבקשות המקבילות ב-Knative Serving לשירות כך שיהיה שווה לכל הגדרה ברמת הקוד או קטן ממנה. אם אין הגדרה ברמת הקוד, משתמשים בערך המקביל של הבו-זמניות (concurrency).
  5. להשתמש בכלים לבדיקת עומס שתומכים בהגדרה של מספר הבקשות המקבילות. צריך לוודא שהשירות שלכם נשאר יציב תחת העומס והבו-זמניות הצפויים.
  6. אם השירות לא פועל בצורה טובה, עוברים לשלב 1 כדי לשפר את השירות או לשלב 2 כדי לצמצם את הבו-זמניות. אם השירות פועל בצורה טובה, חוזרים לשלב 2 ומגדילים את מספר הבקשות המקבילות.

ממשיכים לחזור על התהליך עד שמגיעים למקסימום יציב של פעולות בו-זמניות.

התאמת הזיכרון לריבוי משימות

כל בקשה שהשירות מטפל בה דורשת כמות מסוימת של זיכרון נוסף. לכן, כשמגדילים או מקטינים את מספר הבקשות המקבילות, חשוב להגדיל או להקטין גם את מגבלת הזיכרון.

הימנעות ממצב גלובלי שניתן לשינוי

אם רוצים להשתמש במצב גלובלי שניתן לשינוי בהקשר של פעולות בו-זמניות, צריך לבצע פעולות נוספות בקוד כדי לוודא שהפעולה מתבצעת בצורה בטוחה. כדי לצמצם את התחרות על משאבים, מגבילים את השימוש במשתנים גלובליים לאתחול חד-פעמי ולשימוש חוזר, כמו שמתואר למעלה בקטע ביצועים.

אם משתמשים במשתנים גלובליים שניתנים לשינוי בשירות שמטפל בכמה בקשות בו-זמנית, חשוב להשתמש בנעילות או ב-mutex כדי למנוע מרוץ תהליכים.

Container Security

שיטות רבות לאבטחת תוכנה לשימוש כללי רלוונטיות לאפליקציות בקונטיינרים. יש כמה שיטות עבודה שספציפיות למאגרי תגים או שתואמות לפילוסופיה ולארכיטקטורה של מאגרי תגים.

כדי לשפר את אבטחת הקונטיינרים:

  • מומלץ להשתמש בתמונות בסיסיות מאובטחות שמתעדכנות באופן שוטף, כמו תמונות בסיסיות של Google או תמונות רשמיות של Docker Hub.

  • כדי להחיל עדכוני אבטחה על השירותים, צריך לבנות מחדש באופן קבוע תמונות של קונטיינרים ולפרוס מחדש את השירותים.

  • כוללים במאגר רק את מה שנדרש להפעלת השירות. קוד, חבילות וכלים נוספים הם נקודות חולשה פוטנציאליות באבטחה. למעלה מופיעה ההשפעה על הביצועים שקשורה לכך.

  • הטמעה של תהליך build דטרמיניסטי שכולל גרסאות ספציפיות של תוכנה וספריות. כך לא ייכלל קוד לא מאומת במאגר התגים.

  • מגדירים את מאגר התגים כך שיפעל כמשתמש אחר ולא כ-root באמצעות ההצהרה Dockerfile USER. יכול להיות שחלק מתמונות המאגר כבר כוללות הגדרה של משתמש ספציפי.

אוטומציה של סריקות אבטחה

הפעלת סריקת נקודות חולשה כדי לבצע סריקת אבטחה של תמונות קונטיינרים שמאוחסנות ב-Artifact Registry.

אפשר גם להשתמש ב-Binary Authorization כדי לוודא שרק קובצי אימג' מאובטחים של קונטיינרים נפרסים.

פיתוח קובצי אימג' מינימליים של קונטיינרים

תמונות גדולות של קונטיינרים עלולות להגדיל את נקודות החולשה באבטחה, כי הן מכילות יותר ממה שהקוד צריך.

ב-Knative serving, גודל קובץ האימג' של הקונטיינר לא משפיע על זמן ההפעלה הקרה או על זמן עיבוד הבקשה, והוא לא נכלל בזיכרון הזמין של הקונטיינר.

כדי ליצור קונטיינר מינימלי, כדאי לעבוד עם קובץ אימג' בסיסי רזה כמו:

Ubuntu גדול יותר, אבל הוא קובץ אימג' בסיסי שנמצא בשימוש נפוץ עם סביבת שרת מוכנה יותר.

אם תהליך build של השירות שלכם כולל שימוש רב בכלים, כדאי להשתמש בבנייה רב-שלבית כדי שהקונטיינר יהיה קל בזמן הריצה.

במקורות המידע הבאים אפשר לקרוא מידע נוסף על יצירת תמונות קונטיינר רזות: