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

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

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

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

פעילות ברקע

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

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

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

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

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

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

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

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

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

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

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

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

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

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

התחלת שימוש במכולות במהירות

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

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

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

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

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

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

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

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

המשך


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

המשך


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

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

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

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

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

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

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

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

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

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

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