mirror of
https://github.com/rubenhensen/k8scd.git
synced 2026-11-15 14:29:09 +01:00
dqlite supports VACUUM from v1.18.2, which microk8s 1.34 ships, but the dqlite shell wraps every statement in a transaction and SQLite refuses to VACUUM inside one. dqlite-vacuum sends it through the go-dqlite driver instead and otherwise just prints page statistics. dqlite-vacuum.sh makes the backups first. Rehearsed on opti1 on a throwaway node in /dev/shm: 657 MB to 41 MB in 0.6s, with concurrent writes unaffected and the data intact after restarts.
151 lines
4.1 KiB
Go
151 lines
4.1 KiB
Go
// dqlite-vacuum prints page statistics of a dqlite database and, with
|
|
// -vacuum, runs VACUUM on it.
|
|
//
|
|
// The dqlite shell that ships with microk8s wraps every statement in a
|
|
// transaction, and SQLite refuses to VACUUM inside one. This sends VACUUM as
|
|
// a plain statement through the go-dqlite driver instead. dqlite supports it
|
|
// from v1.18.2 (canonical/dqlite#734).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/canonical/go-dqlite/v3/client"
|
|
"github.com/canonical/go-dqlite/v3/driver"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
type stats struct {
|
|
pageSize, pages, free, rows int64
|
|
}
|
|
|
|
func (s stats) String() string {
|
|
mb := func(pages int64) float64 { return float64(pages*s.pageSize) / 1e6 }
|
|
return fmt.Sprintf("%d pages (%.0f MB), %d free (%.0f MB), %d kine rows",
|
|
s.pages, mb(s.pages), s.free, mb(s.free), s.rows)
|
|
}
|
|
|
|
func main() {
|
|
dir := flag.String("dir", "/var/snap/microk8s/current/var/kubernetes/backend", "dqlite backend directory with info.yaml, cluster.crt and cluster.key")
|
|
name := flag.String("db", "k8s", "database name")
|
|
vacuum := flag.Bool("vacuum", false, "run VACUUM; without it only print statistics")
|
|
timeout := flag.Duration("timeout", 10*time.Minute, "timeout for VACUUM")
|
|
flag.Parse()
|
|
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
|
|
|
var info struct {
|
|
Address string `yaml:"Address"`
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(*dir, "info.yaml"))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if err := yaml.Unmarshal(data, &info); err != nil || info.Address == "" {
|
|
log.Fatalf("no address in info.yaml: %v", err)
|
|
}
|
|
|
|
dial, err := tlsDial(filepath.Join(*dir, "cluster.crt"), filepath.Join(*dir, "cluster.key"))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
store := client.NewInmemNodeStore()
|
|
if err := store.Set(context.Background(), []client.NodeInfo{{Address: info.Address}}); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
drv, err := driver.New(store, driver.WithDialFunc(dial), driver.WithConnectionTimeout(10*time.Second))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
sql.Register("dqlite", drv)
|
|
db, err := sql.Open("dqlite", *name)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
// One connection, so VACUUM never runs next to a statement of our own.
|
|
db.SetMaxOpenConns(1)
|
|
|
|
before, err := readStats(db)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
log.Printf("%s: %s", info.Address, before)
|
|
if !*vacuum {
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
|
defer cancel()
|
|
start := time.Now()
|
|
log.Print("VACUUM started")
|
|
if _, err := db.ExecContext(ctx, "VACUUM"); err != nil {
|
|
log.Fatalf("VACUUM failed after %s: %v", time.Since(start).Round(time.Millisecond), err)
|
|
}
|
|
log.Printf("VACUUM done in %s", time.Since(start).Round(time.Millisecond))
|
|
|
|
after, err := readStats(db)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
log.Printf("%s: %s", info.Address, after)
|
|
}
|
|
|
|
func readStats(db *sql.DB) (stats, error) {
|
|
var s stats
|
|
for _, q := range []struct {
|
|
sql string
|
|
dst *int64
|
|
}{
|
|
{"PRAGMA page_size", &s.pageSize},
|
|
{"PRAGMA page_count", &s.pages},
|
|
{"PRAGMA freelist_count", &s.free},
|
|
{"SELECT COUNT(*) FROM kine", &s.rows},
|
|
} {
|
|
if err := db.QueryRow(q.sql).Scan(q.dst); err != nil {
|
|
return s, fmt.Errorf("%s: %w", q.sql, err)
|
|
}
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// tlsDial mirrors go-dqlite's app.SimpleDialTLSConfig, which can't be
|
|
// imported without cgo.
|
|
func tlsDial(crt, key string) (driver.DialFunc, error) {
|
|
cert, err := tls.LoadX509KeyPair(crt, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pem, err := os.ReadFile(crt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(pem) {
|
|
return nil, fmt.Errorf("bad certificate %s", crt)
|
|
}
|
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(leaf.DNSNames) == 0 {
|
|
return nil, fmt.Errorf("certificate %s has no DNS name", crt)
|
|
}
|
|
config := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
RootCAs: pool,
|
|
Certificates: []tls.Certificate{cert},
|
|
ClientSessionCache: tls.NewLRUClientSessionCache(0),
|
|
ServerName: leaf.DNSNames[0],
|
|
}
|
|
return client.DialFuncWithTLS(client.DefaultDialFunc, config), nil
|
|
}
|