github.com/m3db/m3@v1.5.0/src/cluster/client/etcd/client.go (about)

     1  // Copyright (c) 2016 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package etcd
    22  
    23  import (
    24  	"crypto/rand"
    25  	"errors"
    26  	"fmt"
    27  	"math/big"
    28  	"os"
    29  	"path/filepath"
    30  	"sort"
    31  	"strings"
    32  	"sync"
    33  	"time"
    34  
    35  	"github.com/m3db/m3/src/cluster/client"
    36  	"github.com/m3db/m3/src/cluster/kv"
    37  	etcdkv "github.com/m3db/m3/src/cluster/kv/etcd"
    38  	"github.com/m3db/m3/src/cluster/services"
    39  	etcdheartbeat "github.com/m3db/m3/src/cluster/services/heartbeat/etcd"
    40  	"github.com/m3db/m3/src/cluster/services/leader"
    41  	"github.com/m3db/m3/src/x/instrument"
    42  	"github.com/m3db/m3/src/x/retry"
    43  
    44  	"github.com/uber-go/tally"
    45  	clientv3 "go.etcd.io/etcd/client/v3"
    46  	"go.uber.org/zap"
    47  )
    48  
    49  const (
    50  	hierarchySeparator = "/"
    51  	internalPrefix     = "_"
    52  	cacheFileSeparator = "_"
    53  	cacheFileSuffix    = ".json"
    54  	// TODO deprecate this once all keys are migrated to per service namespace
    55  	kvPrefix = "_kv"
    56  
    57  	// Set GRPC response limits to 32 MiB, should be sufficient for most use cases.
    58  	// The default 2 MiB limit usually comes as an unpleasant surprise - etcd itself will reject
    59  	// requests that are too large anyway, and there are many other ways to tank etcd,
    60  	// like creating too many watchers.
    61  	_grpcMaxSendRecvBufferSize = 32 * 1024 * 1024
    62  )
    63  
    64  var errInvalidNamespace = errors.New("invalid namespace")
    65  
    66  // make sure m3cluster and etcd client interfaces are implemented, and that
    67  // Client is a superset of cluster.Client.
    68  var _ client.Client = Client((*csclient)(nil))
    69  
    70  type newClientFn func(cluster Cluster) (*clientv3.Client, error)
    71  
    72  type cacheFileForZoneFn func(zone string) etcdkv.CacheFileFn
    73  
    74  // ZoneClient is a cached etcd client for a zone.
    75  type ZoneClient struct {
    76  	Client *clientv3.Client
    77  	Zone   string
    78  }
    79  
    80  // NewEtcdConfigServiceClient returns a new etcd-backed cluster client.
    81  //nolint:golint
    82  func NewEtcdConfigServiceClient(opts Options) (*csclient, error) {
    83  	if err := opts.Validate(); err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	scope := opts.InstrumentOptions().
    88  		MetricsScope().
    89  		Tagged(map[string]string{"service": opts.Service()})
    90  
    91  	return &csclient{
    92  		opts:    opts,
    93  		sdOpts:  opts.ServicesOptions(),
    94  		kvScope: scope.Tagged(map[string]string{"config_service": "kv"}),
    95  		sdScope: scope.Tagged(map[string]string{"config_service": "sd"}),
    96  		hbScope: scope.Tagged(map[string]string{"config_service": "hb"}),
    97  		clis:    make(map[string]*clientv3.Client),
    98  		logger:  opts.InstrumentOptions().Logger(),
    99  		newFn:   newClient,
   100  		retrier: retry.NewRetrier(opts.RetryOptions()),
   101  		stores:  make(map[string]kv.TxnStore),
   102  	}, nil
   103  }
   104  
   105  // NewConfigServiceClient returns a ConfigServiceClient.
   106  func NewConfigServiceClient(opts Options) (client.Client, error) {
   107  	return NewEtcdConfigServiceClient(opts)
   108  }
   109  
   110  type csclient struct {
   111  	sync.RWMutex
   112  	clis map[string]*clientv3.Client
   113  
   114  	opts    Options
   115  	sdOpts  services.Options
   116  	kvScope tally.Scope
   117  	sdScope tally.Scope
   118  	hbScope tally.Scope
   119  	logger  *zap.Logger
   120  	newFn   newClientFn
   121  	retrier retry.Retrier
   122  
   123  	storeLock sync.Mutex
   124  	stores    map[string]kv.TxnStore
   125  }
   126  
   127  func (c *csclient) Services(opts services.OverrideOptions) (services.Services, error) {
   128  	if opts == nil {
   129  		opts = services.NewOverrideOptions()
   130  	}
   131  	return c.createServices(opts)
   132  }
   133  
   134  func (c *csclient) KV() (kv.Store, error) {
   135  	return c.Txn()
   136  }
   137  
   138  func (c *csclient) Txn() (kv.TxnStore, error) {
   139  	return c.TxnStore(kv.NewOverrideOptions())
   140  }
   141  
   142  func (c *csclient) Store(opts kv.OverrideOptions) (kv.Store, error) {
   143  	return c.TxnStore(opts)
   144  }
   145  
   146  func (c *csclient) TxnStore(opts kv.OverrideOptions) (kv.TxnStore, error) {
   147  	opts, err := c.sanitizeOptions(opts)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	return c.createTxnStore(opts)
   153  }
   154  
   155  func (c *csclient) createServices(opts services.OverrideOptions) (services.Services, error) {
   156  	nOpts := opts.NamespaceOptions()
   157  	cacheFileExtraFields := []string{nOpts.PlacementNamespace(), nOpts.MetadataNamespace()}
   158  	return services.NewServices(c.sdOpts.
   159  		SetHeartbeatGen(c.heartbeatGen()).
   160  		SetKVGen(c.kvGen(c.cacheFileFn(cacheFileExtraFields...))).
   161  		SetLeaderGen(c.leaderGen()).
   162  		SetNamespaceOptions(nOpts).
   163  		SetInstrumentsOptions(instrument.NewOptions().
   164  			SetLogger(c.logger).
   165  			SetMetricsScope(c.sdScope),
   166  		),
   167  	)
   168  }
   169  
   170  func (c *csclient) createTxnStore(opts kv.OverrideOptions) (kv.TxnStore, error) {
   171  	// validate the override options because they are user supplied.
   172  	if err := opts.Validate(); err != nil {
   173  		return nil, err
   174  	}
   175  	return c.txnGen(opts, c.cacheFileFn())
   176  }
   177  
   178  func (c *csclient) kvGen(fn cacheFileForZoneFn) services.KVGen {
   179  	return services.KVGen(func(zone string) (kv.Store, error) {
   180  		// we don't validate or sanitize the options here because we're using
   181  		// them as a container for zone.
   182  		opts := kv.NewOverrideOptions().SetZone(zone)
   183  		return c.txnGen(opts, fn)
   184  	})
   185  }
   186  
   187  func (c *csclient) newkvOptions(
   188  	opts kv.OverrideOptions,
   189  	cacheFileFn cacheFileForZoneFn,
   190  ) etcdkv.Options {
   191  	kvOpts := etcdkv.NewOptions().
   192  		SetInstrumentsOptions(c.opts.InstrumentOptions().
   193  			SetLogger(c.logger).
   194  			SetMetricsScope(c.kvScope)).
   195  		SetCacheFileFn(cacheFileFn(opts.Zone())).
   196  		SetWatchWithRevision(c.opts.WatchWithRevision()).
   197  		SetNewDirectoryMode(c.opts.NewDirectoryMode()).
   198  		SetEnableFastGets(c.opts.EnableFastGets()).
   199  		SetRetryOptions(c.opts.RetryOptions()).
   200  		SetRequestTimeout(c.opts.RequestTimeout()).
   201  		SetWatchChanInitTimeout(c.opts.WatchChanInitTimeout()).
   202  		SetWatchChanCheckInterval(c.opts.WatchChanCheckInterval()).
   203  		SetWatchChanResetInterval(c.opts.WatchChanResetInterval())
   204  
   205  	if ns := opts.Namespace(); ns != "" {
   206  		kvOpts = kvOpts.SetPrefix(kvOpts.ApplyPrefix(ns))
   207  	}
   208  
   209  	if env := opts.Environment(); env != "" {
   210  		kvOpts = kvOpts.SetPrefix(kvOpts.ApplyPrefix(env))
   211  	}
   212  
   213  	return kvOpts
   214  }
   215  
   216  // txnGen assumes the caller has validated the options passed if they are
   217  // user-supplied (as opposed to constructed ourselves).
   218  func (c *csclient) txnGen(
   219  	opts kv.OverrideOptions,
   220  	cacheFileFn cacheFileForZoneFn,
   221  ) (kv.TxnStore, error) {
   222  	cli, err := c.etcdClientGen(opts.Zone())
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	c.storeLock.Lock()
   228  	defer c.storeLock.Unlock()
   229  
   230  	key := kvStoreCacheKey(opts.Zone(), opts.Namespace(), opts.Environment())
   231  	store, ok := c.stores[key]
   232  	if ok {
   233  		return store, nil
   234  	}
   235  	if store, err = etcdkv.NewStore(cli, c.newkvOptions(opts, cacheFileFn)); err != nil {
   236  		return nil, err
   237  	}
   238  
   239  	c.stores[key] = store
   240  	return store, nil
   241  }
   242  
   243  func (c *csclient) heartbeatGen() services.HeartbeatGen {
   244  	return services.HeartbeatGen(
   245  		func(sid services.ServiceID) (services.HeartbeatService, error) {
   246  			cli, err := c.etcdClientGen(sid.Zone())
   247  			if err != nil {
   248  				return nil, err
   249  			}
   250  
   251  			opts := etcdheartbeat.NewOptions().
   252  				SetInstrumentsOptions(instrument.NewOptions().
   253  					SetLogger(c.logger).
   254  					SetMetricsScope(c.hbScope)).
   255  				SetServiceID(sid)
   256  			return etcdheartbeat.NewStore(cli, opts)
   257  		},
   258  	)
   259  }
   260  
   261  func (c *csclient) leaderGen() services.LeaderGen {
   262  	return services.LeaderGen(
   263  		func(sid services.ServiceID, eo services.ElectionOptions) (services.LeaderService, error) {
   264  			cli, err := c.etcdClientGen(sid.Zone())
   265  			if err != nil {
   266  				return nil, err
   267  			}
   268  
   269  			opts := leader.NewOptions().
   270  				SetServiceID(sid).
   271  				SetElectionOpts(eo)
   272  
   273  			return leader.NewService(cli, opts)
   274  		},
   275  	)
   276  }
   277  
   278  func (c *csclient) etcdClientGen(zone string) (*clientv3.Client, error) {
   279  	c.Lock()
   280  	defer c.Unlock()
   281  
   282  	cli, ok := c.clis[zone]
   283  	if ok {
   284  		return cli, nil
   285  	}
   286  
   287  	cluster, ok := c.opts.ClusterForZone(zone)
   288  	if !ok {
   289  		return nil, fmt.Errorf("no etcd cluster found for zone: %s", zone)
   290  	}
   291  
   292  	err := c.retrier.Attempt(func() error {
   293  		var tryErr error
   294  		cli, tryErr = c.newFn(cluster)
   295  		return tryErr
   296  	})
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  
   301  	c.clis[zone] = cli
   302  	return cli, nil
   303  }
   304  
   305  // Clients returns all currently cached etcd clients.
   306  func (c *csclient) Clients() []ZoneClient {
   307  	c.Lock()
   308  	defer c.Unlock()
   309  
   310  	var (
   311  		zones   = make([]string, 0, len(c.clis))
   312  		clients = make([]ZoneClient, 0, len(c.clis))
   313  	)
   314  
   315  	for k := range c.clis {
   316  		zones = append(zones, k)
   317  	}
   318  
   319  	sort.Strings(zones)
   320  
   321  	for _, zone := range zones {
   322  		clients = append(clients, ZoneClient{Zone: zone, Client: c.clis[zone]})
   323  	}
   324  
   325  	return clients
   326  }
   327  
   328  func newClient(cluster Cluster) (*clientv3.Client, error) {
   329  	cfg, err := newConfigFromCluster(cryptoRandInt63n, cluster)
   330  	if err != nil {
   331  		return nil, err
   332  	}
   333  	return clientv3.New(cfg)
   334  }
   335  
   336  // rnd is used to set a jitter on the keep alive.
   337  func newConfigFromCluster(rnd randInt63N, cluster Cluster) (clientv3.Config, error) {
   338  	tls, err := cluster.TLSOptions().Config()
   339  	if err != nil {
   340  		return clientv3.Config{}, err
   341  	}
   342  	cfg := clientv3.Config{
   343  		AutoSyncInterval:   cluster.AutoSyncInterval(),
   344  		DialTimeout:        cluster.DialTimeout(),
   345  		DialOptions:        cluster.DialOptions(),
   346  		Endpoints:          cluster.Endpoints(),
   347  		TLS:                tls,
   348  		MaxCallSendMsgSize: _grpcMaxSendRecvBufferSize,
   349  		MaxCallRecvMsgSize: _grpcMaxSendRecvBufferSize,
   350  	}
   351  
   352  	if opts := cluster.KeepAliveOptions(); opts.KeepAliveEnabled() {
   353  		keepAlivePeriod := opts.KeepAlivePeriod()
   354  		if maxJitter := opts.KeepAlivePeriodMaxJitter(); maxJitter > 0 {
   355  			jitter, err := rnd(int64(maxJitter))
   356  			if err != nil {
   357  				return clientv3.Config{}, err
   358  			}
   359  			keepAlivePeriod += time.Duration(jitter)
   360  		}
   361  		cfg.DialKeepAliveTime = keepAlivePeriod
   362  		cfg.DialKeepAliveTimeout = opts.KeepAliveTimeout()
   363  		cfg.PermitWithoutStream = true
   364  	}
   365  
   366  	return cfg, nil
   367  }
   368  
   369  func (c *csclient) cacheFileFn(extraFields ...string) cacheFileForZoneFn {
   370  	return func(zone string) etcdkv.CacheFileFn {
   371  		return func(namespace string) string {
   372  			if c.opts.CacheDir() == "" {
   373  				return ""
   374  			}
   375  
   376  			cacheFileFields := make([]string, 0, len(extraFields)+3)
   377  			cacheFileFields = append(cacheFileFields, namespace, c.opts.Service(), zone)
   378  			cacheFileFields = append(cacheFileFields, extraFields...)
   379  			return filepath.Join(c.opts.CacheDir(), fileName(cacheFileFields...))
   380  		}
   381  	}
   382  }
   383  
   384  func fileName(parts ...string) string {
   385  	// get non-empty parts
   386  	idx := 0
   387  	for i, part := range parts {
   388  		if part == "" {
   389  			continue
   390  		}
   391  		if i != idx {
   392  			parts[idx] = part
   393  		}
   394  		idx++
   395  	}
   396  	parts = parts[:idx]
   397  	s := strings.Join(parts, cacheFileSeparator)
   398  	return strings.Replace(s, string(os.PathSeparator), cacheFileSeparator, -1) + cacheFileSuffix
   399  }
   400  
   401  func validateTopLevelNamespace(namespace string) error {
   402  	if namespace == "" || namespace == hierarchySeparator {
   403  		return errInvalidNamespace
   404  	}
   405  	if strings.HasPrefix(namespace, internalPrefix) {
   406  		// start with _
   407  		return errInvalidNamespace
   408  	}
   409  	if strings.HasPrefix(namespace, hierarchySeparator+internalPrefix) {
   410  		return errInvalidNamespace
   411  	}
   412  	return nil
   413  }
   414  
   415  func (c *csclient) sanitizeOptions(opts kv.OverrideOptions) (kv.OverrideOptions, error) {
   416  	if opts.Zone() == "" {
   417  		opts = opts.SetZone(c.opts.Zone())
   418  	}
   419  
   420  	if opts.Environment() == "" {
   421  		opts = opts.SetEnvironment(c.opts.Env())
   422  	}
   423  
   424  	namespace := opts.Namespace()
   425  	if namespace == "" {
   426  		return opts.SetNamespace(kvPrefix), nil
   427  	}
   428  
   429  	if err := validateTopLevelNamespace(namespace); err != nil {
   430  		return nil, err
   431  	}
   432  
   433  	return opts, nil
   434  }
   435  
   436  func kvStoreCacheKey(zone string, namespaces ...string) string {
   437  	parts := make([]string, 0, 1+len(namespaces))
   438  	parts = append(parts, zone)
   439  	for _, ns := range namespaces {
   440  		if ns != "" {
   441  			parts = append(parts, ns)
   442  		}
   443  	}
   444  	return strings.Join(parts, hierarchySeparator)
   445  }
   446  
   447  // We have a linter which dislikes math.Rand, as it's insecure in the general case. Our usage here is very unlikely
   448  // to have security implications, but it won't hurt to make the linter happy.
   449  type randInt63N func(n int64) (int64, error)
   450  
   451  func cryptoRandInt63n(n int64) (int64, error) {
   452  	r, err := rand.Int(rand.Reader, big.NewInt(n))
   453  	if err != nil {
   454  		return 0, err
   455  	}
   456  	return r.Int64(), nil
   457  }