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

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

כתיבת שירותים אפקטיביים

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

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

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

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

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

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

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

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

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

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

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

מכיוון שמתבצעת התאמה של גודל המופעים של הקונטיינר לפי הצורך, שיטה טיפוסית היא לאתחל את סביבת ההפעלה באופן מלא. סוג כזה של אתחול נקרא "הפעלה במצב התחלתי (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();
  }
}

אופטימיזציה של בו-זמניות (concurrency)

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

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

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

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

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

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

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

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

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

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

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

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

Container Security

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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