Skip to content

Commit de4f804

Browse files
Add store support for ProxySQL (#129)
* Add store support for ProxySQL
1 parent 1016c68 commit de4f804

34 files changed

+2791
-6
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ module github.com/github/freno
33
go 1.14
44

55
require (
6+
github.com/DATA-DOG/go-sqlmock v1.4.1
67
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878
78
github.com/boltdb/bolt v1.3.1
89
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
9-
github.com/go-sql-driver/mysql v1.5.0 // indirect
10+
github.com/go-sql-driver/mysql v1.5.0
1011
github.com/hashicorp/go-msgpack v0.5.5
1112
github.com/julienschmidt/httprouter v1.3.0
1213
github.com/outbrain/golib v0.0.0-20180830062331-ab954725f502

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM=
2+
github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
13
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
24
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
35
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=

pkg/config/haproxy_config_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func init() {
1616
log.SetLevel(log.ERROR)
1717
}
1818

19-
func TestParseAddresses(t *testing.T) {
19+
func TestHAProxyParseAddresses(t *testing.T) {
2020
{
2121
c := &HAProxyConfigurationSettings{Addresses: ""}
2222
addresses, err := c.parseAddresses()
@@ -90,7 +90,7 @@ func TestParseAddresses(t *testing.T) {
9090
}
9191
}
9292

93-
func TestGetProxyAddresses(t *testing.T) {
93+
func TestHAProxyGetProxyAddresses(t *testing.T) {
9494
{
9595
c := &HAProxyConfigurationSettings{Addresses: ""}
9696
addresses, err := c.GetProxyAddresses()
@@ -127,7 +127,7 @@ func TestGetProxyAddresses(t *testing.T) {
127127
}
128128
}
129129

130-
func TestIsEmpty(t *testing.T) {
130+
func TestHAProxyIsEmpty(t *testing.T) {
131131
{
132132
c := &HAProxyConfigurationSettings{}
133133
isEmpty := c.IsEmpty()

pkg/config/mysql_config.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ type MySQLClusterConfigurationSettings struct {
2323
HttpCheckPath string // Specify if different than specified by MySQLConfigurationSettings
2424
IgnoreHosts []string // override MySQLConfigurationSettings's, or leave empty to inherit those settings
2525

26-
HAProxySettings HAProxyConfigurationSettings // If list of servers is to be acquired via HAProxy, provide this field
27-
VitessSettings VitessConfigurationSettings // If list of servers is to be acquired via Vitess, provide this field
26+
HAProxySettings HAProxyConfigurationSettings // If list of servers is to be acquired via HAProxy, provide this field
27+
ProxySQLSettings ProxySQLConfigurationSettings // If list of servers is to be acquired via ProxySQL, provide this field
28+
VitessSettings VitessConfigurationSettings // If list of servers is to be acquired via Vitess, provide this field
2829
StaticHostsSettings StaticHostsConfigurationSettings
2930
}
3031

@@ -58,6 +59,9 @@ type MySQLConfigurationSettings struct {
5859
HttpCheckPort int // port for HTTP check. -1 to disable.
5960
HttpCheckPath string // If non-empty, requires HttpCheckPort
6061
IgnoreHosts []string // If non empty, substrings to indicate hosts to be ignored/skipped
62+
ProxySQLAddresses []string // A list of ProxySQL instances to query for hosts
63+
ProxySQLUser string // ProxySQL stats username
64+
ProxySQLPassword string // ProxySQL stats password
6165
VitessCells []string // Name of the Vitess cells for polling tablet hosts
6266

6367
Clusters map[string](*MySQLClusterConfigurationSettings) // cluster name -> cluster config
@@ -115,6 +119,17 @@ func (settings *MySQLConfigurationSettings) postReadAdjustments() error {
115119
if len(clusterSettings.IgnoreHosts) == 0 {
116120
clusterSettings.IgnoreHosts = settings.IgnoreHosts
117121
}
122+
if !clusterSettings.ProxySQLSettings.IsEmpty() {
123+
if len(clusterSettings.ProxySQLSettings.Addresses) < 1 {
124+
clusterSettings.ProxySQLSettings.Addresses = settings.ProxySQLAddresses
125+
}
126+
if clusterSettings.ProxySQLSettings.User == "" {
127+
clusterSettings.ProxySQLSettings.User = settings.ProxySQLUser
128+
}
129+
if clusterSettings.ProxySQLSettings.Password == "" {
130+
clusterSettings.ProxySQLSettings.Password = settings.ProxySQLPassword
131+
}
132+
}
118133
if !clusterSettings.VitessSettings.IsEmpty() && len(clusterSettings.VitessSettings.Cells) < 1 {
119134
clusterSettings.VitessSettings.Cells = settings.VitessCells
120135
}

pkg/config/proxysql_config.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package config
2+
3+
import "fmt"
4+
5+
//
6+
// ProxySQL-specific configuration
7+
//
8+
9+
const ProxySQLDefaultDatabase = "stats"
10+
11+
type ProxySQLConfigurationSettings struct {
12+
Addresses []string
13+
User string
14+
Password string
15+
HostgroupID uint
16+
IgnoreServerTTLSecs uint
17+
}
18+
19+
func (settings ProxySQLConfigurationSettings) AddressToDSN(address string) string {
20+
return fmt.Sprintf("mysql://%s:*****@%s/%s", settings.User, address, ProxySQLDefaultDatabase)
21+
}
22+
23+
func (settings *ProxySQLConfigurationSettings) IsEmpty() bool {
24+
if len(settings.Addresses) == 0 {
25+
return true
26+
}
27+
if settings.User == "" || settings.Password == "" {
28+
return true
29+
}
30+
if settings.HostgroupID < 1 {
31+
return true
32+
}
33+
return false
34+
}

pkg/config/proxysql_config_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
test "github.com/outbrain/golib/tests"
7+
)
8+
9+
func TestProxySQLAddressToDSN(t *testing.T) {
10+
{
11+
c := &ProxySQLConfigurationSettings{User: "freno"}
12+
test.S(t).ExpectEquals(c.AddressToDSN("proxysql-123abcd.test:6032"), "mysql://freno:*****@proxysql-123abcd.test:6032/"+ProxySQLDefaultDatabase)
13+
}
14+
}
15+
16+
func TestProxySQLIsEmpty(t *testing.T) {
17+
{
18+
c := &ProxySQLConfigurationSettings{}
19+
isEmpty := c.IsEmpty()
20+
test.S(t).ExpectTrue(isEmpty)
21+
}
22+
{
23+
c := &ProxySQLConfigurationSettings{Addresses: []string{"localhost:6032"}}
24+
isEmpty := c.IsEmpty()
25+
test.S(t).ExpectTrue(isEmpty)
26+
}
27+
{
28+
c := &ProxySQLConfigurationSettings{Addresses: []string{"localhost:6032"}, User: "freno"}
29+
isEmpty := c.IsEmpty()
30+
test.S(t).ExpectTrue(isEmpty)
31+
}
32+
{
33+
c := &ProxySQLConfigurationSettings{Addresses: []string{"localhost:6032"}, User: "freno", Password: "freno"}
34+
isEmpty := c.IsEmpty()
35+
test.S(t).ExpectTrue(isEmpty)
36+
}
37+
{
38+
c := &ProxySQLConfigurationSettings{Addresses: []string{"localhost:6032"}, User: "freno", Password: "freno", HostgroupID: 20}
39+
isEmpty := c.IsEmpty()
40+
test.S(t).ExpectFalse(isEmpty)
41+
}
42+
}

pkg/proxysql/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# ProxySQL
2+
3+
This package implements freno store support for [ProxySQL](https://proxysql.com/)
4+
5+
## Logic
6+
7+
Freno will probe servers found in the `stats.stats_mysql_connection_pool` ProxySQL admin table that have either status:
8+
1. `ONLINE` - connect, ping and replication checks pass
9+
1. `SHUNNED_REPLICATION_LAG` - connect and ping checks pass, but replication is lagging
10+
11+
All other statuses are considered unhealthy and therefore are ignored by freno, eg:
12+
1. `SHUNNED` - proxysql connot connect and/or ping a backend
13+
1. `OFFLINE_SOFT` - a server that is draining, usually for maintenance, etc
14+
1. `OFFLINE_HARD` - a server that is completely offline
15+
16+
## Requirements
17+
1. The ProxySQL `--no-monitor` flag is not set
18+
1. The [ProxySQL monitor module](https://github.com/sysown/proxysql/wiki/Monitor-Module) is enabled, eg: [`mysql-monitor_enabled`](https://github.com/sysown/proxysql/wiki/Global-variables#mysql-monitor_enabled) is `true`
19+
1. The `max_replication_lag` column is defined for backend servers in [the `mysql_servers` admin table](https://github.com/sysown/proxysql/wiki/Main-(runtime)#mysql_servers)
20+
- This ensures servers with lag do not receive reads but are still probed by freno
21+

pkg/proxysql/client.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package proxysql
2+
3+
import (
4+
"database/sql"
5+
"errors"
6+
"fmt"
7+
"sort"
8+
"time"
9+
10+
"github.com/github/freno/pkg/config"
11+
_ "github.com/go-sql-driver/mysql"
12+
"github.com/outbrain/golib/log"
13+
"github.com/patrickmn/go-cache"
14+
)
15+
16+
const ignoreServerCacheCleanupTTL = time.Duration(500) * time.Millisecond
17+
18+
// MySQLConnectionPoolServer represents a row in the stats_mysql_connection_pool table
19+
type MySQLConnectionPoolServer struct {
20+
Host string
21+
Port int32
22+
Status string
23+
}
24+
25+
// Address returns a string of the hostname/port of a server
26+
func (ms *MySQLConnectionPoolServer) Address() string {
27+
return fmt.Sprintf("%s:%d", ms.Host, ms.Port)
28+
}
29+
30+
// Client is the ProxySQL admin client
31+
type Client struct {
32+
dbs map[string]*sql.DB
33+
defaultIgnoreServerTTL time.Duration
34+
ignoreServerCache *cache.Cache
35+
}
36+
37+
// NewClient returns a new ProxySQL admin client
38+
func NewClient(defaultIgnoreServerTTL time.Duration) *Client {
39+
return &Client{
40+
dbs: make(map[string]*sql.DB, 0),
41+
defaultIgnoreServerTTL: defaultIgnoreServerTTL,
42+
ignoreServerCache: cache.New(cache.NoExpiration, ignoreServerCacheCleanupTTL),
43+
}
44+
}
45+
46+
// GetDB returns a configured ProxySQL admin connection
47+
func (c *Client) GetDB(settings config.ProxySQLConfigurationSettings) (*sql.DB, string, error) {
48+
addrs := settings.Addresses
49+
sort.Strings(addrs)
50+
51+
var lastErr error
52+
for _, addr := range addrs {
53+
if db, found := c.dbs[addr]; found {
54+
return db, addr, nil
55+
}
56+
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?interpolateParams=true&timeout=500ms",
57+
settings.User, settings.Password, addr, config.ProxySQLDefaultDatabase,
58+
))
59+
if err != nil {
60+
lastErr = err
61+
continue
62+
}
63+
if err = db.Ping(); err != nil {
64+
lastErr = err
65+
continue
66+
}
67+
log.Debugf("connected to ProxySQL at %s", settings.AddressToDSN(addr))
68+
c.dbs[addr] = db
69+
return c.dbs[addr], addr, nil
70+
}
71+
if lastErr != nil {
72+
return nil, "", lastErr
73+
}
74+
return nil, "", errors.New("failed to get connection")
75+
}
76+
77+
// CloseDB closes a ProxySQL admin connection based on an address string
78+
func (c *Client) CloseDB(addr string) {
79+
if db, found := c.dbs[addr]; found {
80+
db.Close()
81+
delete(c.dbs, addr)
82+
}
83+
}
84+
85+
// GetServers returns a list of MySQLConnectionPoolServers with 'ONLINE' or 'SHUNNED_REPLICATION_LAG' status, based on hostgroup ID
86+
func (c *Client) GetServers(db *sql.DB, settings config.ProxySQLConfigurationSettings) (servers []*MySQLConnectionPoolServer, err error) {
87+
ignoreServerTTL := c.defaultIgnoreServerTTL
88+
if settings.IgnoreServerTTLSecs > 0 {
89+
ignoreServerTTL = time.Duration(settings.IgnoreServerTTLSecs) * time.Second
90+
}
91+
92+
rows, err := db.Query(fmt.Sprintf(`SELECT srv_host, srv_port, status FROM stats_mysql_connection_pool WHERE hostgroup=%d`, settings.HostgroupID))
93+
if err != nil {
94+
return servers, err
95+
}
96+
defer rows.Close()
97+
98+
var server *MySQLConnectionPoolServer
99+
for rows.Next() {
100+
server = &MySQLConnectionPoolServer{}
101+
if err = rows.Scan(&server.Host, &server.Port, &server.Status); err != nil {
102+
return nil, err
103+
}
104+
105+
switch server.Status {
106+
case "ONLINE":
107+
if _, ignore := c.ignoreServerCache.Get(server.Address()); ignore {
108+
log.Debugf("found %q in the proxysql ignore-server cache, ignoring ONLINE state for %s", server.Address(), ignoreServerTTL)
109+
continue
110+
}
111+
servers = append(servers, server)
112+
case "SHUNNED_REPLICATION_LAG":
113+
defer c.ignoreServerCache.Delete(server.Address())
114+
servers = append(servers, server)
115+
default:
116+
c.ignoreServerCache.Set(server.Address(), true, ignoreServerTTL)
117+
}
118+
}
119+
120+
return servers, rows.Err()
121+
}

0 commit comments

Comments
 (0)