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

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

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

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

פעילות ברקע

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

הגדרת חיוב על בסיס מופע לשימוש בפעילויות ברקע

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

אם אתם משתמשים בחיוב לפי בקשה, מומלץ להימנע מפעילויות ברקע

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

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

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

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

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

דיווח על שגיאות

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

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

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

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

התחלת מאגרים במהירות

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

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

  • הורדת קובץ האימג' של הקונטיינר (באמצעות טכנולוגיית הזרמת קובץ האימג' של הקונטיינר של Cloud Run)
  • הפעלת הקונטיינר על ידי הרצת הפקודה entrypoint.
  • המערכת ממתינה שהמאגר יתחיל להאזין ביציאה שהוגדרה.

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

שימוש בהגברת מהירות המעבד (CPU) בזמן ההפעלה כדי להפחית את זמן האחזור של ההפעלה

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

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

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

שימו לב: בקשה שממתינה להפעלת מופע תישאר בהמתנה בתור באופן הבא:

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

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

אם אתם משתמשים בשפה דינמית עם ספריות תלויות, כמו ייבוא מודולים ב-Node.js, זמן הטעינה של המודולים האלה מתווסף לזמן האחזור של ההפעלה.

כדי לקצר את זמן האחזור של ההפעלה:

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

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

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

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

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

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

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

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

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

שימוש בסביבת הפעלה אחרת

יכול להיות שזמני ההפעלה יהיו מהירים יותר אם תשתמשו בסביבת ביצוע שונה.

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

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

‫Cloud Run משנה באופן אוטומטי את מספר הבקשות המקבילות עד למקסימום שהוגדר.

מקסימום המקביליות שמוגדר כברירת מחדל הוא 80, והוא מתאים להרבה קובצי אימג' של קונטיינרים. עם זאת, אתם צריכים:

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

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

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

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

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

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

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

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

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

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

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

היחס בין תפוקה, זמן אחזור ועלות

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

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

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

שיקולי עלות

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

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

  • זמן אחזור נמוך יותר
  • אירועים שמשלימים את העבודה שלהם מהר יותר
  • המכונות מושבתות מהר יותר גם אם נדרשות יותר מכונות בסך הכול

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

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

Container Security

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

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

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

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

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

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

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

  • כדי למנוע את השימוש בתכונות בגרסת Preview, אפשר להשתמש במדיניות ארגונית בהתאמה אישית.

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

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

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

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

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

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

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

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

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