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

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

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

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

פעילות ברקע

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

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

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

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

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

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

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

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

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

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

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

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

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

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

הפעלת מכולות במהירות

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

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

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

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

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

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

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

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

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

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

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

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

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

  • כדי ליצור שירות יעיל, מומלץ לצמצם את מספר התלויות ואת הגודל שלהן.
  • טען בעצלתיים קוד שנמצא בשימוש לעתים רחוקות, אם השפה שלך תומכת בכך.
  • השתמש באופטימיזציות לטעינת קוד כמו אופטימיזציית טוען קוד אוטומטי של composer של 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 לשירות כך שתהיה שווה או נמוכה מכל הגדרה ברמת הקוד. אם אין תצורה ברמת הקוד, השתמש במקביליות הצפויה שלך.
  5. משתמשים בכלים של בדיקת עומס שתומכים בהגדרה של מספר חיבורים בו-זמניים. צריך לוודא שהשירות שלכם נשאר יציב תחת העומס והבו-זמניות הצפויים.
  6. אם השירות מתפקד בצורה גרועה, עבור לשלב 1 כדי לשפר את השירות או לשלב 2 כדי להפחית את הבו-זמניות. אם השירות פועל כשורה, חזור לשלב 2 והגדל את הבו-זמניות.

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

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

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

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

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

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

פשרות בין תפוקה לבין השהייה לבין עלויות

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

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

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

שיקולי עלות

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

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

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

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

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

Container Security

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

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

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

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

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

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

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

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

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

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

בניית תמונות מינימליות של מכולה

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

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

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

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

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

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