mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
This adds a version of the SQLStore that includes a ReadReplica. The primary DB can be accessed directly - from the caller's standpoint, there is no difference between the SQLStore and ReplStore unless they wish to explicitly call the ReadReplica() and use that for the DB sessions. Currently only the stats service GetSystemStats and GetAdminStats are using the ReadReplica(); if it's misconfigured or if the databaseReadReplica feature flag is not turned on, it will fall back to the usual (SQLStore) behavior. Testing requires a database and read replica - the replication should already be configured. I have been testing this locally with a docker mysql setup (https://medium.com/@vbabak/docker-mysql-master-slave-replication-setup-2ff553fceef2) and the following config: [feature_toggles] databaseReadReplica = true [database] type = mysql name = grafana user = grafana password = password host = 127.0.0.1:3306 [database_replica] type = mysql name = grafana user = grafana password = password host = 127.0.0.1:3307
195 lines
6.6 KiB
Go
195 lines
6.6 KiB
Go
package sqlstore
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/dlmiddlecote/sqlstats"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"xorm.io/xorm"
|
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
"github.com/grafana/grafana/pkg/registry"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
// ReplStore is a wrapper around a main SQLStore and a read-only SQLStore. The
|
|
// main SQLStore is anonymous, so the ReplStore may be used directly as a
|
|
// SQLStore.
|
|
type ReplStore struct {
|
|
*SQLStore
|
|
repl *SQLStore
|
|
}
|
|
|
|
// DB returns the main SQLStore.
|
|
func (rs ReplStore) DB() *SQLStore {
|
|
return rs.SQLStore
|
|
}
|
|
|
|
// ReadReplica returns the read-only SQLStore. If no read replica is configured,
|
|
// it returns the main SQLStore.
|
|
func (rs ReplStore) ReadReplica() *SQLStore {
|
|
if rs.repl == nil {
|
|
rs.log.Debug("ReadReplica not configured, using main SQLStore")
|
|
return rs.SQLStore
|
|
}
|
|
rs.log.Debug("Using ReadReplica")
|
|
return rs.repl
|
|
}
|
|
|
|
// ProvideServiceWithReadReplica creates a new *SQLStore connection intended for
|
|
// use as a ReadReplica of the main SQLStore. The primary SQLStore must already
|
|
// be initialized.
|
|
func ProvideServiceWithReadReplica(primary *SQLStore, cfg *setting.Cfg,
|
|
features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator,
|
|
bus bus.Bus, tracer tracing.Tracer) (*ReplStore, error) {
|
|
// start with the initialized SQLStore
|
|
replStore := &ReplStore{primary, nil}
|
|
|
|
// FeatureToggle fallback: If the FlagDatabaseReadReplica feature flag is not enabled, return a single SQLStore.
|
|
if !features.IsEnabledGlobally(featuremgmt.FlagDatabaseReadReplica) {
|
|
primary.log.Debug("ReadReplica feature flag not enabled, using main SQLStore")
|
|
return replStore, nil
|
|
}
|
|
|
|
// This change will make xorm use an empty default schema for postgres and
|
|
// by that mimic the functionality of how it was functioning before
|
|
// xorm's changes above.
|
|
xorm.DefaultPostgresSchema = ""
|
|
s, err := newReadOnlySQLStore(cfg, features, bus, tracer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.features = features
|
|
s.tracer = tracer
|
|
|
|
// initialize and register metrics wrapper around the *sql.DB
|
|
db := s.engine.DB().DB
|
|
|
|
// register the go_sql_stats_connections_* metrics
|
|
if err := prometheus.Register(sqlstats.NewStatsCollector("grafana_repl", db)); err != nil {
|
|
s.log.Warn("Failed to register sqlstore stats collector", "error", err)
|
|
}
|
|
|
|
replStore.repl = s
|
|
return replStore, nil
|
|
}
|
|
|
|
// newReadOnlySQLStore creates a new *SQLStore intended for use with a
|
|
// fully-populated read replica of the main Grafana Database. It provides no
|
|
// write capabilities and does not run migrations, but other tracing and logging
|
|
// features are enabled.
|
|
func newReadOnlySQLStore(cfg *setting.Cfg, features featuremgmt.FeatureToggles, bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) {
|
|
s := &SQLStore{
|
|
cfg: cfg,
|
|
log: log.New("replstore"),
|
|
bus: bus,
|
|
tracer: tracer,
|
|
}
|
|
|
|
s.features = features
|
|
s.tracer = tracer
|
|
|
|
err := s.initReadOnlyEngine(s.engine)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.dialect = migrator.NewDialect(s.engine.DriverName())
|
|
return s, nil
|
|
}
|
|
|
|
// initReadOnlyEngine initializes ss.engine for read-only operations. The database must be a fully-populated read replica.
|
|
func (ss *SQLStore) initReadOnlyEngine(engine *xorm.Engine) error {
|
|
if ss.engine != nil {
|
|
ss.log.Debug("Already connected to database replica")
|
|
return nil
|
|
}
|
|
|
|
dbCfg, err := NewRODatabaseConfig(ss.cfg, ss.features)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ss.dbCfg = dbCfg
|
|
|
|
if ss.cfg.DatabaseInstrumentQueries {
|
|
ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer)
|
|
}
|
|
|
|
if engine == nil {
|
|
var err error
|
|
engine, err = xorm.NewEngine(ss.dbCfg.Type, ss.dbCfg.ConnectionString)
|
|
if err != nil {
|
|
ss.log.Error("failed to connect to database replica", "error", err)
|
|
return err
|
|
}
|
|
// Only for MySQL or MariaDB, verify we can connect with the current connection string's system var for transaction isolation.
|
|
// If not, create a new engine with a compatible connection string.
|
|
if ss.dbCfg.Type == migrator.MySQL {
|
|
engine, err = ss.ensureTransactionIsolationCompatibility(engine, ss.dbCfg.ConnectionString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
engine.SetMaxOpenConns(ss.dbCfg.MaxOpenConn)
|
|
engine.SetMaxIdleConns(ss.dbCfg.MaxIdleConn)
|
|
engine.SetConnMaxLifetime(time.Second * time.Duration(ss.dbCfg.ConnMaxLifetime))
|
|
|
|
// configure sql logging
|
|
debugSQL := ss.cfg.Raw.Section("database_replica").Key("log_queries").MustBool(false)
|
|
if !debugSQL {
|
|
engine.SetLogger(&xorm.DiscardLogger{})
|
|
} else {
|
|
// add stack to database calls to be able to see what repository initiated queries. Top 7 items from the stack as they are likely in the xorm library.
|
|
engine.SetLogger(NewXormLogger(log.LvlInfo, log.WithSuffix(log.New("replsstore.xorm"), log.CallerContextKey, log.StackCaller(log.DefaultCallerDepth))))
|
|
engine.ShowSQL(true)
|
|
engine.ShowExecTime(true)
|
|
}
|
|
|
|
ss.engine = engine
|
|
return nil
|
|
}
|
|
|
|
// NewRODatabaseConfig creates a new read-only database configuration.
|
|
func NewRODatabaseConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (*DatabaseConfig, error) {
|
|
if cfg == nil {
|
|
return nil, errors.New("cfg cannot be nil")
|
|
}
|
|
|
|
dbCfg := &DatabaseConfig{}
|
|
if err := dbCfg.readConfigSection(cfg, "database_replica"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := dbCfg.buildConnectionString(cfg, features); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dbCfg, nil
|
|
}
|
|
|
|
// ProvideServiceWithReadReplicaForTests wraps the SQLStore in a ReplStore, with the main sqlstore as both the primary and read replica.
|
|
// TODO: eventually this should be replaced with a more robust test setup which in
|
|
func ProvideServiceWithReadReplicaForTests(testDB *SQLStore, t sqlutil.ITestDB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator) (*ReplStore, error) {
|
|
return &ReplStore{testDB, testDB}, nil
|
|
}
|
|
|
|
// InitTestReplDB initializes a test DB and returns it wrapped in a ReplStore with the main SQLStore as both the primary and read replica.
|
|
func InitTestReplDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*ReplStore, *setting.Cfg) {
|
|
t.Helper()
|
|
features := getFeaturesForTesting(opts...)
|
|
cfg := getCfgForTesting(opts...)
|
|
ss, err := initTestDB(t, cfg, features, migrations.ProvideOSSMigrations(features), opts...)
|
|
if err != nil {
|
|
t.Fatalf("failed to initialize sql repl store: %s", err)
|
|
}
|
|
return &ReplStore{ss, ss}, cfg
|
|
}
|