Ladění výkonu pomocí go-mssqldb

Tento článek poskytuje návod, jak optimalizovat výkon aplikací Go, které používají go-mssqldb ovladač se SQL Server.

Začněte s největšími změnami

Většina aplikací nepotřebuje už od prvního dne ladění specifické pro ovladač. Začněte těmito kroky, než změníte velikost paketu, přidáte všude připravené příkazy nebo upravíte nastavení hromadných operací:

  1. Nastavte omezenou velikost poolu připojení, která odpovídá limitům vašeho serveru.
  2. Přidejte časové limity kontextu, aby blokované SQL volání nepřipínaly připojení a aplikace se při zatížení nejevila jako zamrzlá.
  3. Opravte pomalé dotazy, chybějící indexy a zbytečné cesty tam a zpět v SQL Server.
  4. Porovnajte pracovní zátěž před a po každé změně.

U mnoha služeb jsou nastavení poolu spojení a tvar dotazu důležitější než velikost paketu nebo příprava příkazů.

Ladění spojovacího bazénu

Poolová database/sql spojovací kapacita je nejvlivnější výkonnostní páka. Nedostatečně provisionované pooly způsobují, že goroutine blokují čekání na připojení, zatímco předimenzované pooly plýtvají serverovými zdroji.

db.SetMaxOpenConns(25)     // Match your workload concurrency
db.SetMaxIdleConns(10)     // Keep warm connections ready
db.SetConnMaxLifetime(5 * time.Minute)  // Recycle connections periodically
db.SetConnMaxIdleTime(1 * time.Minute)  // Close stale idle connections

Sledujte db.Stats().WaitCount a db.Stats().WaitDuration, abyste odhalili kolize ve fondu. Podrobnosti najdete v části Sdružování připojení.

Zvětšení velikosti paketu

Výchozí velikost TDS paketu je 4 096 bajtů. U pracovních zátěží, které přenášejí velké výsledkové sady nebo hromadná data, zvětšení velikosti paketu snižuje počet síťových okružních cest.

sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&packet+size=16384

Platný rozsah: 512 až 32 767. Hodnoty 8 192 nebo 16 384 jsou běžné pro scénáře s vysokou propustností.

Nechte výchozí velikost paketu, pokud benchmarková data neukážou, že síťový přenos dominuje zátěži. U provozu ve stylu OLTP s malými dotazy a řádky často větší pakety přidávají složitost bez významného zisku.

Používejte připravené výroky

Připravené příkazy se vyhýbají opakovanému parsování dotazů a plánují kompilaci na serveru. Použijte db.PrepareContext , když spouštěte stejný dotaz mnohokrát s různými parametry:

stmt, err := db.PrepareContext(ctx,
    "SELECT TOP (1) FirstName + ' ' + LastName AS Name FROM Sales.vSalesPerson WHERE CountryRegionName = @p1")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

for _, loc := range locations {
    var name string
    stmt.QueryRowContext(ctx, loc).Scan(&name)
}

Tip

Uzavřete připravené příkazy pomocí defer stmt.Close(), abyste zabránili spotřebovávání deskriptorů připravených příkazů na straně serveru.

Nepřipravujte každý výrok automaticky. Nejvíce pomáhají, když se stejný výrok opakovaně opakuje v horké dráze. U jednorázových dotazů QueryContextExecContext je to obvykle přímočařejší a rychlejší.

Používejte hromadné texty pro velké přílohy

Jednotlivé příkazy INSERT jsou pomalé pro velké datové zatížení. Hromadné kopírovaní přenáší data přímo na server, přičemž obchází procesor dotazů:

stmt, err := txn.Prepare(mssql.CopyIn("MyTable", mssql.BulkOptions{
    Tablock:      true,
    RowsPerBatch: 5000,
}, "Col1", "Col2"))

Podrobnosti viz Hromadné operace.

Varchar používejte podle potřeby

Ve výchozím nastavení kód odesílá string parametry jako nvarchar (Unicode). Pokud vaše sloupce používají varchar, server může provádět implicitní konverzi a přeskočit indexy. Používá se mssql.VarChar k odesílání varchar parametrů.

db.QueryContext(ctx, "SELECT * FROM Production.Product WHERE ProductNumber = @p1",
    mssql.VarChar("FR-R92B-58"))

Používejte časové limity pro kontext

Nastavte kontextové termíny pro jednotlivé dotazy, abyste zabránili blokovaným SQL hovorům, které by připínaly spojení a zdržovaly volající.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

rows, err := db.QueryContext(ctx, "SELECT * FROM LargeTable")

Uzavírejte zdroje co nejdříve

Otevřete *sql.Rows, *sql.Tx, a objekty *sql.Conn připnou spojení z poolu. Vždy je zavři co nejdříve.

rows, err := db.QueryContext(ctx, query)
if err != nil {
    return err
}
defer rows.Close()

for rows.Next() {
    // process
}
return rows.Err()

Snižte zpáteční jízdy tam a zpět

  • Seskupujte dotazy do jednoho volání, pokud je to možné: "SELECT ...; SELECT ...;".
  • Používejte OUTPUT klauzule místo samostatných SELECT SCOPE_IDENTITY() volání.
  • Použijte parametry s tabulkovými hodnotami k odeslání více řádků v jednom volání místo opakování jednotlivých vkladů.

Kontrolní seznam výkonu

Area Recommendation
Bazén Nastavte MaxOpenConns na základě limitů serveru a souběžnosti s pracovní zátěží.
Bazén Nastavte MaxIdleConns alespoň na polovinu hodnoty MaxOpenConns.
Network Změňte packet size až po otestování výkonu při velkých přenosech nebo hromadném načítání dat.
Queries Používejte připravené výroky pouze pro věty, které se často opakují.
Queries Použijte mssql.VarChar pro sloupce varchar, aby se předešlo implicitním převodům.
Velké zatížení Pro dávkové vložení použijte hromadnou kopii (mssql.CopyIn).
Resources Uzavřete Rows, Tx, a Conn objekty okamžitě.
Časové limity Nastavte kontextové termíny pro všechny dotazy a výkazy.
S převahou čtení Použití ApplicationIntent=ReadOnly pro čtení replik.
Benchmarks Používejte testing.B k měření před a po optimalizaci.
Monitoring Povolte Query Store a používejte SSMS reporty nebo Query Performance Insight.
Monitoring Exportovat db.Stats() do Prometheus nebo OpenTelemetry.

Operace benchmarkové databáze

Použijte Go's testing.B k měření výkonu databázových operací a ověření optimalizačních změn:

func BenchmarkInsertSingle(b *testing.B) {
    db := setupDB(b)
    ctx := context.Background()

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := db.ExecContext(ctx,
            "INSERT INTO BenchTable (Name) VALUES (@p1)",
            sql.Named("p1", fmt.Sprintf("bench-%d", i)))
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkInsertBulk(b *testing.B) {
    db := setupDB(b)
    ctx := context.Background()

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        txn, err := db.BeginTx(ctx, nil)
        if err != nil {
            b.Fatal(err)
        }
        stmt, err := txn.Prepare(mssql.CopyIn("BenchTable",
            mssql.BulkOptions{}, "Name"))
        if err != nil {
            b.Fatal(err)
        }
        for j := 0; j < 1000; j++ {
            if _, err := stmt.Exec(fmt.Sprintf("bench-%d-%d", i, j)); err != nil {
                b.Fatal(err)
            }
        }
        if _, err := stmt.Exec(); err != nil {
            b.Fatal(err)
        }
        stmt.Close()
        if err := txn.Commit(); err != nil {
            b.Fatal(err)
        }
    }
}

Spusť benchmarky s:

go test -bench=BenchmarkInsert -benchmem -count=5

Tip

Použijte -count=5 nebo vyšší pro získání statisticky smysluplných výsledků. Použijte benchstat k porovnání výsledků benchmarků před a po změně.

Použijte směrování pouze pro čtení

Pokud má vaše prostředí SQL Server skupinu dostupnosti s čitelnými sekundárními dotazy, směrujte dotazy pouze pro čtení do sekundární repliky nastavením ApplicationIntent=ReadOnly v připojovací řetězec:

sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025&ApplicationIntent=ReadOnly

Vytvořte samostatné *sql.DB instance pro čtení a zápis:

writeDB, err := sql.Open("sqlserver",
    "sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025")
if err != nil {
    log.Fatal(err)
}

readDB, err := sql.Open("sqlserver",
    "sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025&ApplicationIntent=ReadOnly")
if err != nil {
    log.Fatal(err)
}

// Use readDB for reports, dashboards, and analytics.
// Use writeDB for inserts, updates, and deletes.

Analýza plánu dotazu

Použijte SET SHOWPLAN_XML k získání plánu provedení dotazu bez jeho spuštění. Tato metoda vám pomůže identifikovat skeny tabulek, chybějící indexy a nákladné operace:

func getQueryPlan(ctx context.Context, db *sql.DB, query string) (string, error) {
    // Use a dedicated connection so SHOWPLAN mode doesn't affect other queries.
    conn, err := db.Conn(ctx)
    if err != nil {
        return "", err
    }
    defer conn.Close()

    // Enable SHOWPLAN_XML mode.
    _, err = conn.ExecContext(ctx, "SET SHOWPLAN_XML ON")
    if err != nil {
        return "", err
    }

    var planXML string
    err = conn.QueryRowContext(ctx, query).Scan(&planXML)
    if err != nil {
        return "", err
    }

    // Disable SHOWPLAN_XML mode.
    _, _ = conn.ExecContext(ctx, "SET SHOWPLAN_XML OFF")

    return planXML, nil
}

Warning

SET SHOWPLAN_XML ON Ovlivňuje celé spojení. Vždy používejte db.Conn(ctx) k izolaci režimu SHOWPLAN na vyhrazené připojení.

Použijte Query Store k nalezení pomalých dotazů

Go benchmarky a časování na straně klienta vám řeknou, jak dlouho trvá dotaz z pohledu vaší aplikace, ale toto číslo kombinuje latenci sítě, dobu spuštění serveru a zpracování klienta. Query Store zaznamenává plány provádění a statistiky běhu na serveru, takže můžete přesně vidět, jak SQL Server vykonával každý dotaz, jak často běžel a jak se jeho výkon v průběhu času měnil.

Query Store je zvláště užitečný pro identifikaci sniffingu parametrů, regrese plánů a dotazů, které spotřebovávají nejvíce serverových zdrojů. Povolte to ve své databázi, pokud to ještě není povolené:

ALTER DATABASE [AdventureWorks2025] SET QUERY_STORE = ON;

Jakmile je aktivován, můžete si data o výkonu prohlížet několika způsoby:

  • SQL Server Management Studio (SSMS): Rozšiřte svou databázi v Průzkumník objektů, otevřete složku Query Store a použijte vestavěné reporty jako Top Resource Consuming Queries, Regressed Queries a Overall Resource Consumption.
  • Azure portál: Pro Azure SQL Database otevřete blade Query Performance Insight, abyste viděli nejvíce náročné dotazy bez instalace jakýchkoli nástrojů.
  • Transact-SQL (T-SQL): Pokud potřebujete programový přístup, dotazujte se přímo z aplikace Go na zobrazení katalogu sys.query_store_runtime_stats a sys.query_store_plan.

Tip

Query Store uchovává data při restartu serveru, takže můžete analyzovat trendy výkonu během dnů nebo týdnů. Použijte report Regressed Queries k rychlému odhalení dotazů, které se zpomalily po změně schématu nebo kódu.

Použijte panel výkonu

Performance Dashboard je vestavěná SSMS zpráva, která poskytuje aktuální přehled o stavu SQL Server v reálném čase. Klikněte pravým tlačítkem na instanci serveru v SSMS Průzkumník objektů a vyberte Reports> StandardReports>Performance Dashboard.

Řídicí panel zobrazuje:

  • Současné čekání a úzká místa.
  • Nedávné drahé dotazy.
  • Trendy v používání CPU, I/O a paměti.
  • Aktivní uživatelské požadavky a blokované relace.

Výkonnostní dashboard je užitečný při vývoji a zátěžovém testování pro rychlé odhalení problémů bez psaní diagnostických dotazů.

Monitorování serverových metrik z Go

Pokud potřebujete z vaší aplikace v Go zpřístupnit systému monitorování, jako je Prometheus nebo OpenTelemetry, data o výkonu SQL Serveru, dotazujte se přímo na dynamické pohledy pro správu (DMV):

Nejdražší dotazy

Získejte 10 nejlepších dotazů seřazených podle průměrného uplynulého času:

rows, err := db.QueryContext(ctx, `
    SELECT TOP 10
        qs.total_elapsed_time / qs.execution_count AS avg_elapsed_us,
        qs.execution_count,
        qs.total_logical_reads / qs.execution_count AS avg_reads,
        SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
            ((CASE qs.statement_end_offset
                WHEN -1 THEN DATALENGTH(st.text)
                ELSE qs.statement_end_offset
            END - qs.statement_start_offset)/2)+1) AS query_text
    FROM sys.dm_exec_query_stats AS qs
    CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
    ORDER BY avg_elapsed_us DESC`)

Aktuální aktivní požadavky

Vypište všechny požadavky právě zpracovávané na serveru, s výjimkou vaší vlastní relace:

rows, err := db.QueryContext(ctx, `
    SELECT
        r.session_id,
        r.status,
        r.wait_type,
        r.cpu_time,
        r.logical_reads,
        t.text AS query_text
    FROM sys.dm_exec_requests AS r
    CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
    WHERE r.session_id != @@SPID`)

Paměťově efektivní vzory pro velké výsledky

Proudové řady bez akumulace

Zpracovávejte řádky po jednom namísto načtení celé sady výsledků do pole:

rows, err := db.QueryContext(ctx, "SELECT Id, Data FROM BigTable")
if err != nil {
    return err
}
defer rows.Close()

for rows.Next() {
    var id int
    var data string
    if err := rows.Scan(&id, &data); err != nil {
        return err
    }
    // Process immediately, don't append to a slice.
    process(id, data)
}
return rows.Err()

Dávkové zpracování s paginací klíčů

Rozdělte velké tabulkové skenování na zvládnutelné části, abyste omezili využití paměti a vyhnuli se dlouhodobému držení spojení:

func processBatched(ctx context.Context, db *sql.DB, batchSize int) error {
    var lastID int
    for {
        rows, err := db.QueryContext(ctx, `
            SELECT TOP(@batch) Id, Data FROM BigTable
            WHERE Id > @lastID ORDER BY Id`,
            sql.Named("batch", batchSize),
            sql.Named("lastID", lastID))
        if err != nil {
            return err
        }

        var count int
        for rows.Next() {
            var id int
            var data string
            if err := rows.Scan(&id, &data); err != nil {
                return err
            }
            process(id, data)
            lastID = id
            count++
        }
        if err := rows.Err(); err != nil {
            return err
        }
        rows.Close()

        if count < batchSize {
            break // No more rows.
        }
    }
    return nil
}