ניהול חיבורים למסד נתונים

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

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

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

מאגרי חיבורים

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

פתיחה וסגירה של חיבורים

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

Python

# Preparing a statement before hand can help protect against injections.
stmt = sqlalchemy.text(
    "INSERT INTO votes (time_cast, candidate) VALUES (:time_cast, :candidate)"
)
try:
    # Using a with statement ensures that the connection is always released
    # back into the pool at the end of statement (even if an error occurs)
    with db.connect() as conn:
        conn.execute(stmt, parameters={"time_cast": time_cast, "candidate": team})
        conn.commit()
except Exception as e:
    # If something goes wrong, handle the error in this section. This might
    # involve retrying or adjusting parameters depending on the situation.
    # ...

Java

// Using a try-with-resources statement ensures that the connection is always released back
// into the pool at the end of the statement (even if an error occurs)
try (Connection conn = pool.getConnection()) {

  // PreparedStatements can be more efficient and project against injections.
  PreparedStatement voteStmt = conn.prepareStatement(
      "INSERT INTO votes (time_cast, candidate) VALUES (?, ?);");
  voteStmt.setTimestamp(1, now);
  voteStmt.setString(2, team);

  // Finally, execute the statement. If it fails, an error will be thrown.
  voteStmt.execute();

} catch (SQLException ex) {
  // If something goes wrong, handle the error in this section. This might involve retrying or
  // adjusting parameters depending on the situation.
  // ...
}

Node.js

try {
  const stmt =
    'INSERT INTO votes (time_cast, candidate) VALUES (@timestamp, @team)';
  // Using a prepared statement protects against SQL injection attacks.
  // When prepare is called, a single connection is acquired from the connection pool
  // and all subsequent executions are executed exclusively on this connection.
  const ps = new mssql.PreparedStatement(pool);
  ps.input('timestamp', mssql.DateTime);
  ps.input('team', mssql.VarChar(6));
  await ps.prepare(stmt);
  await ps.execute({
    timestamp: timestamp,
    team: team,
  });
  await ps.unprepare();
} catch (err) {
  // If something goes wrong, handle the error in this section. This might
  // involve retrying or adjusting parameters depending on the situation.
  // ...
}

C#‎

using Microsoft.Data.SqlClient;
using System;

namespace CloudSql
{
    public class SqlServerTcp
    {
        public static SqlConnectionStringBuilder NewSqlServerTCPConnectionString()
        {
            // Equivalent connection string:
            // "User Id=<DB_USER>;Password=<DB_PASS>;Server=<INSTANCE_HOST>;Database=<DB_NAME>;"
            var connectionString = new SqlConnectionStringBuilder()
            {
                // Note: Saving credentials in environment variables is convenient, but not
                // secure - consider a more secure solution such as
                // Cloud Secret Manager (https://cloud.google.com/secret-manager) to help
                // keep secrets safe.
                DataSource = Environment.GetEnvironmentVariable("INSTANCE_HOST"), // e.g. '127.0.0.1'
                // Set Host to 'cloudsql' when deploying to App Engine Flexible environment
                UserID = Environment.GetEnvironmentVariable("DB_USER"),         // e.g. 'my-db-user'
                Password = Environment.GetEnvironmentVariable("DB_PASS"),       // e.g. 'my-db-password'
                InitialCatalog = Environment.GetEnvironmentVariable("DB_NAME"), // e.g. 'my-database'

                // The Cloud SQL proxy provides encryption between the proxy and instance
                Encrypt = false,
            };
            connectionString.Pooling = true;
            // Specify additional properties here.
            return connectionString;
        }
    }
}

המשך

insertVote := "INSERT INTO votes (candidate, created_at) VALUES (@TEAM, GETDATE())"
_, err := db.Exec(insertVote, sql.Named("TEAM", team))

Ruby

@vote = Vote.new candidate: candidate

# ActiveRecord creates and executes your SQL and automatically
# handles the opening and closing of the database connection.
if @vote.save
  render json: "Vote successfully cast for \"#{@vote.candidate}\" at #{@vote.time_cast} PST!"
else
  render json: @vote.errors, status: :unprocessable_entity
end

PHP

// Use prepared statements to guard against SQL injection.
$sql = 'INSERT INTO votes (time_cast, candidate) VALUES (GETDATE(), :voteValue)';

try {
    $statement = $conn->prepare($sql);
    $statement->bindParam('voteValue', $value);

    $res = $statement->execute();
} catch (PDOException $e) {
    throw new RuntimeException(
        'Could not insert vote into database. The PDO exception was ' .
        $e->getMessage(),
        $e->getCode(),
        $e
    );
}

מספר החיבורים

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

Python

# Pool size is the maximum number of permanent connections to keep.
pool_size=5,
# Temporarily exceeds the set pool_size if no connections are available.
max_overflow=2,
# The total number of concurrent connections for your application will be
# a total of pool_size and max_overflow.

Java

// maximumPoolSize limits the total number of concurrent connections this pool will keep. Ideal
// values for this setting are highly variable on app design, infrastructure, and database.
config.setMaximumPoolSize(5);
// minimumIdle is the minimum number of idle connections Hikari maintains in the pool.
// Additional connections will be established to meet this value unless the pool is full.
config.setMinimumIdle(5);

Node.js

  // 'max' limits the total number of concurrent connections this pool will keep. Ideal
  // values for this setting are highly variable on app design, infrastructure, and database.
  (config.pool.max = 5);
// 'min' is the minimum number of idle connections maintained in the pool.
// Additional connections will be established to meet this value unless the pool is full.
config.pool.min = 1;

C#‎

// MaximumPoolSize sets maximum number of connections allowed in the pool.
connectionString.MaxPoolSize = 5;
// MinimumPoolSize sets the minimum number of connections in the pool.
connectionString.MinPoolSize = 0;

המשך

// Set maximum number of connections in idle connection pool.
db.SetMaxIdleConns(5)

// Set maximum number of open connections to the database.
db.SetMaxOpenConns(7)

Ruby

# 'pool' is the maximum number of permanent connections to keep.
pool: 5

PHP

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

השהיה מעריכית לפני ניסיון חוזר (exponential backoff)

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

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

Python

# SQLAlchemy automatically uses delays between failed connection attempts,
# but provides no arguments for configuration.

Java

// Hikari automatically delays between failed connection attempts, eventually reaching a
// maximum delay of `connectionTimeout / 2` between attempts.

Node.js

// The node-mssql module uses a built-in retry strategy which does not implement backoff.
// 'createRetryIntervalMillis' is the number of milliseconds to wait in between retries.
config.pool.createRetryIntervalMillis = 200;

C#‎

Policy
    .Handle<SqlException>()
    .WaitAndRetry(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(5)
    })
    .Execute(() => connection.Open());

המשך

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

Ruby

# ActiveRecord automatically uses delays between failed connection attempts,
# but provides no arguments for configuration.

PHP

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

הזמן הקצוב לחיבור פג

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

Python

# 'pool_timeout' is the maximum number of seconds to wait when retrieving a
# new connection from the pool. After the specified amount of time, an
# exception will be thrown.
pool_timeout=30,  # 30 seconds

Java

// setConnectionTimeout is the maximum number of milliseconds to wait for a connection checkout.
// Any attempt to retrieve a connection from this pool that exceeds the set limit will throw an
// SQLException.
config.setConnectionTimeout(10000); // 10 seconds
// idleTimeout is the maximum amount of time a connection can sit in the pool. Connections that
// sit idle for this many milliseconds are retried if minimumIdle is exceeded.
config.setIdleTimeout(600000); // 10 minutes

Node.js

// 'connectionTimeout` is the maximum number of milliseconds to wait trying to establish an
// initial connection. After the specified amount of time, an exception will be thrown.
config.connectionTimeout = 30000;
// 'acquireTimeoutMillis' is the number of milliseconds before a timeout occurs when acquiring a
// connection from the pool.
config.pool.acquireTimeoutMillis = 30000;
// 'idleTimeoutMillis' is the number of milliseconds a connection must sit idle in the pool
// and not be checked out before it is automatically closed
(config.pool.idleTimeoutMillis = 600000),

C#‎

// ConnectionTimeout sets the time to wait (in seconds) while
// trying to establish a connection before terminating the attempt.
connectionString.ConnectTimeout = 15;

המשך

נכון לעכשיו, החבילה database/sql לא מציעה פונקציונליות להגדרת זמן קצוב לתפוגה של החיבור. זמן הקצוב לתפוגה מוגדר ברמת הדרייבר.

Ruby

# 'timeout' is the maximum number of seconds to wait when retrieving a
# new connection from the pool. After the specified amount of time, an
# ActiveRecord::ConnectionTimeoutError will be raised.
timeout: 5000

PHP

// Here we set the connection timeout to five seconds and ask PDO to
// throw an exception if any errors occur.
[
    PDO::ATTR_TIMEOUT => 5,
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]

משך החיבור

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

Python

# 'pool_recycle' is the maximum number of seconds a connection can persist.
# Connections that live longer than the specified amount of time will be
# re-established
pool_recycle=1800,  # 30 minutes

Java

// maxLifetime is the maximum possible lifetime of a connection in the pool. Connections that
// live longer than this many milliseconds will be closed and reestablished between uses. This
// value should be several minutes shorter than the database's timeout value to avoid unexpected
// terminations.
config.setMaxLifetime(1800000); // 30 minutes

Node.js

בספריית Node.js‏ 'node-mssql' אין כרגע פונקציונליות לשליטה במשך החיבור.

C#‎

// ADO.NET connection pooler removes a connection
// from the pool after it's been idle for approximately
// 4-8 minutes, or if the pooler detects that the
// connection with the server no longer exists.

המשך

// Set Maximum time (in seconds) that a connection can remain open.
db.SetConnMaxLifetime(1800 * time.Second)

Ruby

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

PHP

בשלב הזה, PDO לא מציע פונקציונליות לשליטה במשך החיבור.

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

Python

האפליקציה המלאה בשפת התכנות Python.

Node.js

האפליקציה המלאה בשפת התכנות Node.js.

C#‎

האפליקציה המלאה בשפת התכנות C# ‎.

המשך

האפליקציה המלאה בשפת התכנות Go.

PHP

האפליקציה המלאה בשפת התכנות PHP.

המאמרים הבאים