Commit e54ceaa4 authored by Brad Davidson's avatar Brad Davidson Committed by Brad Davidson

Fix issue with stale connections to removed LB server

Track LB connections through each server so that they can be closed when it is removed. Signed-off-by: 's avatarBrad Davidson <brad.davidson@rancher.com>
parent 5dece799
...@@ -14,10 +14,25 @@ import ( ...@@ -14,10 +14,25 @@ import (
"inet.af/tcpproxy" "inet.af/tcpproxy"
) )
// server tracks the connections to a server, so that they can be closed when the server is removed.
type server struct {
mutex sync.Mutex
connections map[net.Conn]struct{}
}
// serverConn wraps a net.Conn so that it can be removed from the server's connection map when closed.
type serverConn struct {
server *server
net.Conn
}
// LoadBalancer holds data for a local listener which forwards connections to a
// pool of remote servers. It is not a proper load-balancer in that it does not
// actually balance connections, but instead fails over to a new server only
// when a connection attempt to the currently selected server fails.
type LoadBalancer struct { type LoadBalancer struct {
mutex sync.Mutex mutex sync.Mutex
dialer *net.Dialer proxy *tcpproxy.Proxy
proxy *tcpproxy.Proxy
serviceName string serviceName string
configFile string configFile string
...@@ -27,6 +42,7 @@ type LoadBalancer struct { ...@@ -27,6 +42,7 @@ type LoadBalancer struct {
ServerURL string ServerURL string
ServerAddresses []string ServerAddresses []string
randomServers []string randomServers []string
servers map[string]*server
currentServerAddress string currentServerAddress string
nextServerIndex int nextServerIndex int
Listener net.Listener Listener net.Listener
...@@ -40,6 +56,8 @@ var ( ...@@ -40,6 +56,8 @@ var (
ETCDServerServiceName = version.Program + "-etcd-server-load-balancer" ETCDServerServiceName = version.Program + "-etcd-server-load-balancer"
) )
// New contstructs a new LoadBalancer instance. The default server URL, and
// currently active servers, are stored in a file within the dataDir.
func New(ctx context.Context, dataDir, serviceName, serverURL string, lbServerPort int, isIPv6 bool) (_lb *LoadBalancer, _err error) { func New(ctx context.Context, dataDir, serviceName, serverURL string, lbServerPort int, isIPv6 bool) (_lb *LoadBalancer, _err error) {
config := net.ListenConfig{Control: reusePort} config := net.ListenConfig{Control: reusePort}
var localAddress string var localAddress string
...@@ -76,11 +94,11 @@ func New(ctx context.Context, dataDir, serviceName, serverURL string, lbServerPo ...@@ -76,11 +94,11 @@ func New(ctx context.Context, dataDir, serviceName, serverURL string, lbServerPo
lb := &LoadBalancer{ lb := &LoadBalancer{
serviceName: serviceName, serviceName: serviceName,
dialer: &net.Dialer{},
configFile: filepath.Join(dataDir, "etc", serviceName+".json"), configFile: filepath.Join(dataDir, "etc", serviceName+".json"),
localAddress: localAddress, localAddress: localAddress,
localServerURL: localServerURL, localServerURL: localServerURL,
defaultServerAddress: defaultServerAddress, defaultServerAddress: defaultServerAddress,
servers: make(map[string]*server),
ServerURL: serverURL, ServerURL: serverURL,
} }
...@@ -103,14 +121,28 @@ func New(ctx context.Context, dataDir, serviceName, serverURL string, lbServerPo ...@@ -103,14 +121,28 @@ func New(ctx context.Context, dataDir, serviceName, serverURL string, lbServerPo
if err := lb.proxy.Start(); err != nil { if err := lb.proxy.Start(); err != nil {
return nil, err return nil, err
} }
logrus.Infof("Running load balancer %s %s -> %v", serviceName, lb.localAddress, lb.randomServers) logrus.Infof("Running load balancer %s %s -> %v [default: %s]", serviceName, lb.localAddress, lb.ServerAddresses, lb.defaultServerAddress)
return lb, nil return lb, nil
} }
func (lb *LoadBalancer) SetDefault(serverAddress string) { func (lb *LoadBalancer) SetDefault(serverAddress string) {
logrus.Infof("Updating load balancer %s default server address -> %s", lb.serviceName, serverAddress) lb.mutex.Lock()
defer lb.mutex.Unlock()
_, hasOriginalServer := sortServers(lb.ServerAddresses, lb.defaultServerAddress)
// if the old default server is not currently in use, remove it from the server map
if server := lb.servers[lb.defaultServerAddress]; server != nil && !hasOriginalServer {
defer server.closeAll()
delete(lb.servers, lb.defaultServerAddress)
}
// if the new default server doesn't have an entry in the map, add one
if _, ok := lb.servers[serverAddress]; !ok {
lb.servers[serverAddress] = &server{connections: make(map[net.Conn]struct{})}
}
lb.defaultServerAddress = serverAddress lb.defaultServerAddress = serverAddress
logrus.Infof("Updated load balancer %s default server address -> %s", lb.serviceName, serverAddress)
} }
func (lb *LoadBalancer) Update(serverAddresses []string) { func (lb *LoadBalancer) Update(serverAddresses []string) {
...@@ -120,7 +152,7 @@ func (lb *LoadBalancer) Update(serverAddresses []string) { ...@@ -120,7 +152,7 @@ func (lb *LoadBalancer) Update(serverAddresses []string) {
if !lb.setServers(serverAddresses) { if !lb.setServers(serverAddresses) {
return return
} }
logrus.Infof("Updating load balancer %s server addresses -> %v", lb.serviceName, lb.randomServers) logrus.Infof("Updated load balancer %s server addresses -> %v [default: %s]", lb.serviceName, lb.ServerAddresses, lb.defaultServerAddress)
if err := lb.writeConfig(); err != nil { if err := lb.writeConfig(); err != nil {
logrus.Warnf("Error updating load balancer %s config: %s", lb.serviceName, err) logrus.Warnf("Error updating load balancer %s config: %s", lb.serviceName, err)
...@@ -139,18 +171,23 @@ func (lb *LoadBalancer) dialContext(ctx context.Context, network, address string ...@@ -139,18 +171,23 @@ func (lb *LoadBalancer) dialContext(ctx context.Context, network, address string
for { for {
targetServer := lb.currentServerAddress targetServer := lb.currentServerAddress
conn, err := lb.dialer.DialContext(ctx, network, targetServer) server := lb.servers[targetServer]
if err == nil { if server == nil || targetServer == "" {
return conn, nil logrus.Debugf("Nil server for load balancer %s: %s", lb.serviceName, targetServer)
} else {
conn, err := server.dialContext(ctx, network, targetServer)
if err == nil {
return conn, nil
}
logrus.Debugf("Dial error from load balancer %s: %s", lb.serviceName, err)
} }
logrus.Debugf("Dial error from load balancer %s: %s", lb.serviceName, err)
newServer, err := lb.nextServer(targetServer) newServer, err := lb.nextServer(targetServer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if targetServer != newServer { if targetServer != newServer {
logrus.Debugf("Dial server in load balancer %s failed over to %s", lb.serviceName, newServer) logrus.Debugf("Failed over to new server for load balancer %s: %s", lb.serviceName, newServer)
} }
if ctx.Err() != nil { if ctx.Err() != nil {
return nil, ctx.Err() return nil, ctx.Err()
...@@ -167,7 +204,7 @@ func (lb *LoadBalancer) dialContext(ctx context.Context, network, address string ...@@ -167,7 +204,7 @@ func (lb *LoadBalancer) dialContext(ctx context.Context, network, address string
} }
func onDialError(src net.Conn, dstDialErr error) { func onDialError(src net.Conn, dstDialErr error) {
logrus.Debugf("Incoming conn %v, error dialing load balancer servers: %v", src.RemoteAddr().String(), dstDialErr) logrus.Debugf("Incoming conn %s, error dialing load balancer servers: %v", src.RemoteAddr(), dstDialErr)
src.Close() src.Close()
} }
......
...@@ -15,18 +15,18 @@ import ( ...@@ -15,18 +15,18 @@ import (
"github.com/k3s-io/k3s/pkg/cli/cmds" "github.com/k3s-io/k3s/pkg/cli/cmds"
) )
type server struct { type testServer struct {
listener net.Listener listener net.Listener
conns []net.Conn conns []net.Conn
prefix string prefix string
} }
func createServer(prefix string) (*server, error) { func createServer(prefix string) (*testServer, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
return nil, err return nil, err
} }
s := &server{ s := &testServer{
prefix: prefix, prefix: prefix,
listener: listener, listener: listener,
} }
...@@ -34,7 +34,7 @@ func createServer(prefix string) (*server, error) { ...@@ -34,7 +34,7 @@ func createServer(prefix string) (*server, error) {
return s, nil return s, nil
} }
func (s *server) serve() { func (s *testServer) serve() {
for { for {
conn, err := s.listener.Accept() conn, err := s.listener.Accept()
if err != nil { if err != nil {
...@@ -45,14 +45,14 @@ func (s *server) serve() { ...@@ -45,14 +45,14 @@ func (s *server) serve() {
} }
} }
func (s *server) close() { func (s *testServer) close() {
s.listener.Close() s.listener.Close()
for _, conn := range s.conns { for _, conn := range s.conns {
conn.Close() conn.Close()
} }
} }
func (s *server) echo(conn net.Conn) { func (s *testServer) echo(conn net.Conn) {
for { for {
result, err := bufio.NewReader(conn).ReadString('\n') result, err := bufio.NewReader(conn).ReadString('\n')
if err != nil { if err != nil {
......
package loadbalancer package loadbalancer
import ( import (
"context"
"errors" "errors"
"math/rand" "math/rand"
"reflect" "net"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/sets"
) )
var defaultDialer = &net.Dialer{}
func (lb *LoadBalancer) setServers(serverAddresses []string) bool { func (lb *LoadBalancer) setServers(serverAddresses []string) bool {
serverAddresses, hasOriginalServer := sortServers(serverAddresses, lb.defaultServerAddress) serverAddresses, hasOriginalServer := sortServers(serverAddresses, lb.defaultServerAddress)
if len(serverAddresses) == 0 { if len(serverAddresses) == 0 {
...@@ -15,10 +21,32 @@ func (lb *LoadBalancer) setServers(serverAddresses []string) bool { ...@@ -15,10 +21,32 @@ func (lb *LoadBalancer) setServers(serverAddresses []string) bool {
lb.mutex.Lock() lb.mutex.Lock()
defer lb.mutex.Unlock() defer lb.mutex.Unlock()
if reflect.DeepEqual(serverAddresses, lb.ServerAddresses) { newAddresses := sets.NewString(serverAddresses...)
curAddresses := sets.NewString(lb.ServerAddresses...)
if newAddresses.Equal(curAddresses) {
return false return false
} }
for addedServer := range newAddresses.Difference(curAddresses) {
logrus.Infof("Adding server to load balancer %s: %s", lb.serviceName, addedServer)
lb.servers[addedServer] = &server{connections: make(map[net.Conn]struct{})}
}
for removedServer := range curAddresses.Difference(newAddresses) {
server := lb.servers[removedServer]
if server != nil {
logrus.Infof("Removing server from load balancer %s: %s", lb.serviceName, removedServer)
// Defer closing connections until after the new server list has been put into place.
// Closing open connections ensures that anything stuck retrying on a stale server is forced
// over to a valid endpoint.
defer server.closeAll()
// Don't delete the default server from the server map, in case we need to fall back to it.
if removedServer != lb.defaultServerAddress {
delete(lb.servers, removedServer)
}
}
}
lb.ServerAddresses = serverAddresses lb.ServerAddresses = serverAddresses
lb.randomServers = append([]string{}, lb.ServerAddresses...) lb.randomServers = append([]string{}, lb.ServerAddresses...)
rand.Shuffle(len(lb.randomServers), func(i, j int) { rand.Shuffle(len(lb.randomServers), func(i, j int) {
...@@ -55,3 +83,41 @@ func (lb *LoadBalancer) nextServer(failedServer string) (string, error) { ...@@ -55,3 +83,41 @@ func (lb *LoadBalancer) nextServer(failedServer string) (string, error) {
return lb.currentServerAddress, nil return lb.currentServerAddress, nil
} }
// dialContext dials a new connection, and adds its wrapped connection to the map
func (s *server) dialContext(ctx context.Context, network, address string) (net.Conn, error) {
conn, err := defaultDialer.DialContext(ctx, network, address)
if err != nil {
return nil, err
}
// don't lock until adding the connection to the map, otherwise we may block
// while waiting for the dial to time out
s.mutex.Lock()
defer s.mutex.Unlock()
conn = &serverConn{server: s, Conn: conn}
s.connections[conn] = struct{}{}
return conn, nil
}
// closeAll closes all connections to the server, and removes their entries from the map
func (s *server) closeAll() {
s.mutex.Lock()
defer s.mutex.Unlock()
logrus.Debugf("Closing %d connections to load balancer server", len(s.connections))
for conn := range s.connections {
// Close the connection in a goroutine so that we don't hold the lock while doing so.
go conn.Close()
}
}
// Close removes the connection entry from the server's connection map, and
// closes the wrapped connection.
func (sc *serverConn) Close() error {
sc.server.mutex.Lock()
defer sc.server.mutex.Unlock()
delete(sc.server.connections, sc)
return sc.Conn.Close()
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment