Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Aplikacje Go często wykorzystują goroutines do obsługi zadań współbieżnych. Sterownik go-mssqldb i pakiet database/sql zostały zaprojektowane z myślą o jednoczesnym użyciu, ale musisz przestrzegać określonych zasad, aby uniknąć wycieków połączeń, wyścigów danych i wyczerpania puli połączeń. Ten artykuł omawia bezpieczeństwo gorutyn, pule pracowników oraz eleganckie wzorce wyłączania pracy.
Bezpieczeństwo gorutynowe
SQL. DB jest bezpieczny do jednoczesnego użytkowania
Z instancji *sql.DB można bezpiecznie korzystać jednocześnie z wielu gorutyn. Zarządza wewnętrzną pulą połączeń i obsługuje synchronizację:
// CORRECT: Share a single *sql.DB across all goroutines.
var db *sql.DB
func main() {
var err error
db, err = sql.Open("sqlserver", connString)
if err != nil {
log.Fatal(err)
}
defer db.Close()
http.HandleFunc("/employees", listEmployees) // Each request runs in its own goroutine.
log.Fatal(http.ListenAndServe(":8080", nil))
}
Warning
Nie tworz nowego sql.Open na każde żądanie ani na rutynę. Każde sql.Open wywołanie tworzy osobną pulę połączeń. Tworzenie pul na każde żądanie marnuje zasoby i może szybko wyczerpać limity połączeń po stronie serwera.
sql.Rows, sql.Tx i sql.Conn nie są bezpieczne do współbieżnego użycia
Te typy reprezentują pojedyncze połączenie i muszą być używane przez jedną gorutynę naraz:
// WRONG: Sharing rows across goroutines causes data races.
rows, _ := db.QueryContext(ctx, "SELECT BusinessEntityID, FirstName + ' ' + LastName FROM Sales.vSalesPerson")
go func() { rows.Next() }() // DATA RACE
go func() { rows.Next() }() // DATA RACE
// CORRECT: Process rows in the goroutine that created them.
rows, _ := db.QueryContext(ctx, "SELECT BusinessEntityID, FirstName + ' ' + LastName FROM Sales.vSalesPerson")
defer rows.Close()
for rows.Next() {
// Process in this goroutine only.
}
Wzorzec puli pracowników
Gdy musisz przetwarzać wiele elementów jednocześnie (na przykład aktualizować tysiące rekordów), użyj puli pracowników o stałym rozmiarze. To podejście ogranicza współbieżność, aby zapobiec wyczerpaniu puli i przeciążeniu serwera:
import "sync"
func updateEmployeeLocations(ctx context.Context, db *sql.DB, updates []EmployeeUpdate) error {
const maxWorkers = 10
sem := make(chan struct{}, maxWorkers)
var mu sync.Mutex
var firstErr error
var wg sync.WaitGroup
for _, u := range updates {
select {
case <-ctx.Done():
return ctx.Err()
case sem <- struct{}{}: // Acquire a worker slot.
}
wg.Add(1)
go func(u EmployeeUpdate) {
defer wg.Done()
defer func() { <-sem }() // Release the worker slot.
_, err := db.ExecContext(ctx,
"UPDATE HumanResources.Department SET GroupName = @grp WHERE DepartmentID = @id",
sql.Named("grp", u.GroupName),
sql.Named("id", u.Id))
if err != nil {
mu.Lock()
if firstErr == nil {
firstErr = err
}
mu.Unlock()
}
}(u)
}
wg.Wait()
return firstErr
}
Użyj errgroup do pul pracowników
Pakiet golang.org/x/sync/errgroup upraszcza pule pracowników dzięki wbudowanej propagacji błędów i anulowaniu kontekstu:
import "golang.org/x/sync/errgroup"
func updateDepartmentGroups(ctx context.Context, db *sql.DB, updates []DepartmentUpdate) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // Maximum concurrent goroutines.
for _, u := range updates {
u := u
g.Go(func() error {
_, err := db.ExecContext(ctx,
"UPDATE HumanResources.Department SET GroupName = @grp WHERE DepartmentID = @id",
sql.Named("grp", u.GroupName),
sql.Named("id", u.Id))
return err
})
}
return g.Wait()
}
Wskazówka
Ustaw limit grupy błędów na wartość mniejszą niż MaxOpenConns. Jeśli liczba pracowników jest równa wielkości puli, pracownicy zużywają wszystkie połączenia i nie pozostawiają miejsca na kontrole zdrowotne czy inne zapytania.
Zapytania równoległe
Uruchamianie niezależnych zapytań jednocześnie, aby zmniejszyć całkowite opóźnienia:
func getDashboardData(ctx context.Context, db *sql.DB) (*Dashboard, error) {
g, ctx := errgroup.WithContext(ctx)
var orderCount int
var customerCount int
var revenue float64
g.Go(func() error {
return db.QueryRowContext(ctx,
"SELECT COUNT(*) FROM Sales.SalesOrderHeader WHERE OrderDate >= DATEADD(day, -7, GETUTCDATE())").
Scan(&orderCount)
})
g.Go(func() error {
return db.QueryRowContext(ctx,
"SELECT COUNT(DISTINCT CustomerID) FROM Sales.SalesOrderHeader WHERE OrderDate >= DATEADD(day, -7, GETUTCDATE())").
Scan(&customerCount)
})
g.Go(func() error {
return db.QueryRowContext(ctx,
"SELECT ISNULL(SUM(TotalDue), 0) FROM Sales.SalesOrderHeader WHERE OrderDate >= DATEADD(day, -7, GETUTCDATE())").
Scan(&revenue)
})
if err := g.Wait(); err != nil {
return nil, err
}
return &Dashboard{
OrderCount: orderCount,
CustomerCount: customerCount,
WeeklyRevenue: revenue,
}, nil
}
Przetwarzanie wsadowe z kontrolowaną współbieżnością
W przypadku dużych operacji wsadowych (import danych, synchronizacja rekordów) połącz przetwarzanie wsadowe ze współbieżnością, aby zmaksymalizować przepustowość:
func importRecords(ctx context.Context, db *sql.DB, records []Record) error {
const batchSize = 100
const maxWorkers = 5
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(maxWorkers)
for i := 0; i < len(records); i += batchSize {
end := i + batchSize
if end > len(records) {
end = len(records)
}
batch := records[i:end]
g.Go(func() error {
return insertBatch(ctx, db, batch)
})
}
return g.Wait()
}
func insertBatch(ctx context.Context, db *sql.DB, batch []Record) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare(mssql.CopyIn("Production.ScrapReason", mssql.BulkOptions{}, "Name"))
if err != nil {
return err
}
for _, r := range batch {
if _, err := stmt.Exec(r.Name); err != nil {
return err
}
}
if _, err := stmt.Exec(); err != nil {
return err
}
if err := stmt.Close(); err != nil {
return err
}
return tx.Commit()
}
Łagodne zamykanie
Gdy Twoja aplikacja otrzyma sygnał wyłączenia, opróżnij aktywne operacje bazy danych przed zamknięciem puli. Nagłe połączenie db.Close() anuluje zapytania podczas lotu i może pozostawić serwer z osieroczonymi sesjami.
import (
"context"
"database/sql"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
db, err := sql.Open("sqlserver", connString)
if err != nil {
log.Fatal(err)
}
srv := &http.Server{Addr: ":8080"}
// Run the server in a goroutine.
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
// Wait for interrupt signal.
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down...")
// Give in-flight HTTP requests up to 30 seconds to complete.
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP shutdown error: %v", err)
}
// Close the database pool after HTTP handlers have drained.
// This waits for any remaining connections to be returned.
if err := db.Close(); err != nil {
log.Printf("Database close error: %v", err)
}
log.Println("Shutdown complete.")
}
Ważna
Zamknij pulę baz danych po zakończeniu opróżniania serwera HTTP (lub innego routera żądań). Jeśli najpierw zamkniesz pulę, procedury obsługi będące w trakcie wykonywania zgłoszą błędy zerwanego połączenia.
Rozmiarowanie puli połączeń dla równoczesnych obciążeń
Dostosuj rozmiar puli połączeń na podstawie oczekiwanego poziomu współbieżności aplikacji, a nie całkowitej liczby gorutyn:
| Typ aplikacji | Zalecane MaxOpenConns |
Uzasadnienie |
|---|---|---|
| HTTP API, niski poziom współbieżności | 10-25 | Odpowiada typowej liczbie jednoczesnych żądań. |
| HTTP API, wysoka równocześność | 25-50 | Więcej połączeń dla obsług równoległych. |
| Proces roboczy w tle (wsadowy) | 5-10 na pulę pracowników | Każdy pracownik potrzebuje własnego połączenia. |
| Mieszane (API + zadania w tle) | Suma API + potrzeb pracowników | Upewnij się, że każdy podsystem ma wystarczającą rezerwę. |
db.SetMaxOpenConns(25) // Total connections across all goroutines.
db.SetMaxIdleConns(10) // Keep warm connections ready for bursts.
db.SetConnMaxLifetime(5 * time.Minute) // Rotate connections for load balancer compatibility.
Wskazówka
Zawsze ustawiaj wartość MaxOpenConns. Domyślna (0) jest nieograniczona. Nieograniczona pula pod obciążeniem może otworzyć setki połączeń i przeciążyć serwer, zwłaszcza w Azure SQL, gdzie limity połączeń zależą od poziomu połączeń.
Unikaj typowych błędów związanych ze współbieżnością
Nie udostępniaj *sql.Rows między gorutynami
Przekazanie wartości *sql.Rows do wielu gorutyn powoduje wyścigi danych:
// WRONG: rows is consumed by two goroutines.
rows, _ := db.QueryContext(ctx, "SELECT BusinessEntityID, FirstName + ' ' + LastName FROM Sales.vSalesPerson")
go processRows(rows)
go processRows(rows) // Race condition.
Nie zapomnij zamykać rzędów w pętlach
Wyciek *sql.Rows w pętli powoduje wyczerpanie puli połączeń:
// WRONG: Rows leak when the loop starts a new iteration.
for _, location := range locations {
rows, _ := db.QueryContext(ctx,
"SELECT FirstName + ' ' + LastName FROM Sales.vSalesPerson WHERE CountryRegionName = @p1",
sql.Named("p1", location))
for rows.Next() {
// Process...
}
// rows.Close() never called if an error occurs.
}
// CORRECT: Use a helper function with defer.
for _, location := range locations {
if err := processLocation(ctx, db, location); err != nil {
return err
}
}
func processLocation(ctx context.Context, db *sql.DB, location string) error {
rows, err := db.QueryContext(ctx,
"SELECT FirstName + ' ' + LastName FROM Sales.vSalesPerson WHERE CountryRegionName = @p1",
sql.Named("p1", location))
if err != nil {
return err
}
defer rows.Close() // Guaranteed cleanup.
for rows.Next() {
// Process...
}
return rows.Err()
}
Nie używaj db.Conn, chyba że potrzebujesz przypisania do konkretnego połączenia
db.Conn(ctx) przypina konkretne połączenie. Jeśli używasz go niepotrzebnie, zmniejszasz efektywny rozmiar basenu:
// WRONG: Unnecessary pinning.
conn, _ := db.Conn(ctx)
defer conn.Close()
conn.QueryContext(ctx, "SELECT 1") // Use db.QueryContext instead.
// CORRECT: Use db.Conn only for temp tables or session-scoped state.
conn, _ := db.Conn(ctx)
defer conn.Close()
conn.ExecContext(ctx, "CREATE TABLE #Temp (Id INT)")
conn.ExecContext(ctx, "INSERT INTO #Temp VALUES (1)")
conn.QueryContext(ctx, "SELECT * FROM #Temp")
Lista kontrolna współbieżności
| Area | Zalecenie |
|---|---|
| Dzielenie puli | Stwórz jedną *sql.DB instancję dla całej aplikacji. |
| Bezpieczeństwo gorutynowe | Nie udostępniaj *sql.Rows, *sql.Tx, ani *sql.Conn między goroutine. |
| Pule pracowników | Użyj errgroup.SetLimit lub kanału semaforowego do kontrolowania współbieżności. |
| Rozmiary basenu | Ustaw MaxOpenConns mniej niż limit połączenia z serwerem. |
| Bezproblemowe zamykanie | Opróżnij handlery HTTP przed zamknięciem puli baz danych. |
| Oczyszczanie zasobów | Zawsze używaj defer rows.Close() i defer tx.Rollback() w gorutynie, która je utworzyła. |
| Zapytania równoległe | Uruchamiaj niezależne zapytania jednocześnie, aby zmniejszyć opóźnienia dla stron agregacji. |