github.com/m3db/m3@v1.5.0/src/integration/resources/inprocess/cluster.go (about)

     1  // Copyright (c) 2021  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 inprocess
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"net"
    27  	"strconv"
    28  
    29  	"github.com/google/uuid"
    30  	"go.uber.org/zap"
    31  	"gopkg.in/yaml.v2"
    32  
    33  	aggcfg "github.com/m3db/m3/src/cmd/services/m3aggregator/config"
    34  	dbcfg "github.com/m3db/m3/src/cmd/services/m3dbnode/config"
    35  	coordinatorcfg "github.com/m3db/m3/src/cmd/services/m3query/config"
    36  	"github.com/m3db/m3/src/dbnode/client"
    37  	"github.com/m3db/m3/src/dbnode/discovery"
    38  	"github.com/m3db/m3/src/dbnode/environment"
    39  	"github.com/m3db/m3/src/dbnode/persist/fs"
    40  	"github.com/m3db/m3/src/integration/resources"
    41  	nettest "github.com/m3db/m3/src/integration/resources/net"
    42  	"github.com/m3db/m3/src/query/storage/m3"
    43  	xconfig "github.com/m3db/m3/src/x/config"
    44  	"github.com/m3db/m3/src/x/config/hostid"
    45  	xerrors "github.com/m3db/m3/src/x/errors"
    46  )
    47  
    48  // ClusterConfigs contain the input config to use for components within
    49  // the cluster. There is one default configuration for each type of component.
    50  // Given a set of ClusterConfigs, the function NewCluster can spin up an m3 cluster.
    51  // Or one can use GenerateClusterSpecification to get the per-instance configuration
    52  // and options based on the given ClusterConfigs.
    53  type ClusterConfigs struct {
    54  	// DBNode is the configuration for db nodes.
    55  	DBNode dbcfg.Configuration
    56  	// Coordinator is the configuration for the coordinator.
    57  	Coordinator coordinatorcfg.Configuration
    58  	// Aggregator is the configuration for aggregators.
    59  	// If Aggregator is nil, the cluster contains only m3coordinator and dbnodes.
    60  	Aggregator *aggcfg.Configuration
    61  }
    62  
    63  // ClusterSpecification contain the per-instance configuration and options to use
    64  // for starting each components within the cluster.
    65  // The function NewClusterFromSpecification will spin up an m3 cluster
    66  // with the given ClusterSpecification.
    67  type ClusterSpecification struct {
    68  	// Configs contains the per-instance configuration for all components in the cluster.
    69  	Configs PerInstanceConfigs
    70  	// Options contains the per-insatance options for setting up the cluster.
    71  	Options PerInstanceOptions
    72  }
    73  
    74  // PerInstanceConfigs contain the per-instance configuration for all components.
    75  type PerInstanceConfigs struct {
    76  	// DBNodes contains the per-instance configuration for db nodes.
    77  	DBNodes []dbcfg.Configuration
    78  	// Coordinator is the configuration for the coordinator.
    79  	Coordinator coordinatorcfg.Configuration
    80  	// Aggregators is the configuration for aggregators.
    81  	// If Aggregators is nil, the cluster contains only m3coordinator and dbnodes.
    82  	Aggregators []aggcfg.Configuration
    83  }
    84  
    85  // PerInstanceOptions contain the per-instance options for setting up the cluster.
    86  type PerInstanceOptions struct {
    87  	// DBNodes contains the per-instance options for db nodes in the cluster.
    88  	DBNode []DBNodeOptions
    89  }
    90  
    91  // NewClusterConfigsFromConfigFile creates a new ClusterConfigs object from the
    92  // provided filepaths for dbnode and coordinator configuration.
    93  func NewClusterConfigsFromConfigFile(
    94  	pathToDBNodeCfg string,
    95  	pathToCoordCfg string,
    96  	pathToAggCfg string,
    97  ) (ClusterConfigs, error) {
    98  	var dCfg dbcfg.Configuration
    99  	if err := xconfig.LoadFile(&dCfg, pathToDBNodeCfg, xconfig.Options{}); err != nil {
   100  		return ClusterConfigs{}, err
   101  	}
   102  
   103  	var cCfg coordinatorcfg.Configuration
   104  	if err := xconfig.LoadFile(&cCfg, pathToCoordCfg, xconfig.Options{}); err != nil {
   105  		return ClusterConfigs{}, err
   106  	}
   107  
   108  	var aCfg aggcfg.Configuration
   109  	if pathToAggCfg != "" {
   110  		if err := xconfig.LoadFile(&aCfg, pathToAggCfg, xconfig.Options{}); err != nil {
   111  			return ClusterConfigs{}, err
   112  		}
   113  	}
   114  
   115  	return ClusterConfigs{
   116  		DBNode:      dCfg,
   117  		Coordinator: cCfg,
   118  		Aggregator:  &aCfg,
   119  	}, nil
   120  }
   121  
   122  // NewClusterConfigsFromYAML creates a new ClusterConfigs object from YAML strings
   123  // representing component configs.
   124  func NewClusterConfigsFromYAML(dbnodeYaml string, coordYaml string, aggYaml string) (ClusterConfigs, error) {
   125  	var dbCfg dbcfg.Configuration
   126  	if err := yaml.Unmarshal([]byte(dbnodeYaml), &dbCfg); err != nil {
   127  		return ClusterConfigs{}, err
   128  	}
   129  
   130  	var coordCfg coordinatorcfg.Configuration
   131  	if err := yaml.Unmarshal([]byte(coordYaml), &coordCfg); err != nil {
   132  		return ClusterConfigs{}, err
   133  	}
   134  
   135  	var aggCfg aggcfg.Configuration
   136  	if aggYaml != "" {
   137  		if err := yaml.Unmarshal([]byte(aggYaml), &aggCfg); err != nil {
   138  			return ClusterConfigs{}, err
   139  		}
   140  	}
   141  
   142  	return ClusterConfigs{
   143  		Coordinator: coordCfg,
   144  		DBNode:      dbCfg,
   145  		Aggregator:  &aggCfg,
   146  	}, nil
   147  }
   148  
   149  // NewCluster creates a new M3 cluster based on the ClusterOptions provided.
   150  // Expects at least a coordinator, a dbnode and an aggregator config.
   151  func NewCluster(
   152  	configs ClusterConfigs,
   153  	opts resources.ClusterOptions,
   154  ) (resources.M3Resources, error) {
   155  	fullConfigs, err := GenerateClusterSpecification(configs, opts)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	return NewClusterFromSpecification(fullConfigs, opts)
   161  }
   162  
   163  // NewClusterFromSpecification creates a new M3 cluster with the given ClusterSpecification.
   164  func NewClusterFromSpecification(
   165  	specs ClusterSpecification,
   166  	opts resources.ClusterOptions,
   167  ) (resources.M3Resources, error) {
   168  	if err := opts.Validate(); err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	logger, err := resources.NewLogger()
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	var (
   178  		coord resources.Coordinator
   179  		nodes = make(resources.Nodes, 0, len(specs.Configs.DBNodes))
   180  		aggs  = make(resources.Aggregators, 0, len(specs.Configs.Aggregators))
   181  	)
   182  
   183  	fs.DisableIndexClaimsManagersCheckUnsafe()
   184  
   185  	// Ensure that once we start creating resources, they all get cleaned up even if the function
   186  	// fails half way.
   187  	defer func() {
   188  		if err != nil {
   189  			cleanup(logger, nodes, coord, aggs)
   190  		}
   191  	}()
   192  
   193  	for i := 0; i < len(specs.Configs.DBNodes); i++ {
   194  		var node resources.Node
   195  		node, err = NewDBNode(specs.Configs.DBNodes[i], specs.Options.DBNode[i])
   196  		if err != nil {
   197  			return nil, err
   198  		}
   199  		nodes = append(nodes, node)
   200  	}
   201  
   202  	for _, aggCfg := range specs.Configs.Aggregators {
   203  		var agg resources.Aggregator
   204  		agg, err = NewAggregator(aggCfg, AggregatorOptions{
   205  			GeneratePorts:  true,
   206  			GenerateHostID: false,
   207  		})
   208  		if err != nil {
   209  			return nil, err
   210  		}
   211  		aggs = append(aggs, agg)
   212  	}
   213  
   214  	coord, err = NewCoordinator(
   215  		specs.Configs.Coordinator,
   216  		CoordinatorOptions{GeneratePorts: opts.Coordinator.GeneratePorts},
   217  	)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  	if err = ConfigurePlacementsForAggregation(nodes, coord, aggs, specs, opts); err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	// Start all the configured resources.
   227  	m3 := NewM3Resources(ResourceOptions{
   228  		Coordinator: coord,
   229  		DBNodes:     nodes,
   230  		Aggregators: aggs,
   231  	})
   232  	m3.Start()
   233  
   234  	if err = resources.SetupCluster(m3, opts); err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	return m3, nil
   239  }
   240  
   241  // ConfigurePlacementsForAggregation sets up the correct placement information for
   242  // coordinators and aggregators when aggregation is enabled.
   243  func ConfigurePlacementsForAggregation(
   244  	nodes resources.Nodes,
   245  	coord resources.Coordinator,
   246  	aggs resources.Aggregators,
   247  	specs ClusterSpecification,
   248  	opts resources.ClusterOptions,
   249  ) error {
   250  	if len(aggs) == 0 {
   251  		return nil
   252  	}
   253  
   254  	coordAPI := coord
   255  	hostDetails, err := coord.HostDetails()
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	// With remote aggregation enabled, aggregation is not handled within the coordinator.
   261  	// When this is true, the coordinator will fail to start until placement is updated with
   262  	// aggregation related information. As such, use the coordinator embedded within the dbnode
   263  	// to configure the placement and topics.
   264  	if specs.Configs.Coordinator.Downsample.RemoteAggregator != nil {
   265  		if len(specs.Configs.DBNodes) == 0 ||
   266  			specs.Configs.DBNodes[0].Coordinator == nil {
   267  			return errors.New("remote aggregation requires at least one DB node" +
   268  				" running an embedded coordinator for placement and topic configuration")
   269  		}
   270  
   271  		embedded, err := NewEmbeddedCoordinator(nodes[0].(*DBNode))
   272  		if err != nil {
   273  			return nil
   274  		}
   275  
   276  		coordAPI = embedded
   277  	} else {
   278  		// TODO(nate): Remove this in a follow up. If we're not doing remote aggregation
   279  		// we should not be starting aggs which is what requires the coordinator to get started.
   280  		// Once we've refactored existing tests that have aggs w/o remote aggregation enabled,
   281  		// this should be killable.
   282  		coord.Start()
   283  	}
   284  
   285  	if err = coordAPI.WaitForNamespace(""); err != nil {
   286  		return err
   287  	}
   288  
   289  	if err = resources.SetupPlacement(coordAPI, *hostDetails, aggs, *opts.Aggregator); err != nil {
   290  		return err
   291  	}
   292  
   293  	aggInstanceInfo, err := aggs[0].HostDetails()
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	return resources.SetupM3MsgTopics(coordAPI, *aggInstanceInfo, opts)
   299  }
   300  
   301  // GenerateClusterSpecification generates the per-instance configuration and options
   302  // for the cluster set up based on the given input configuation and options.
   303  func GenerateClusterSpecification(
   304  	configs ClusterConfigs,
   305  	opts resources.ClusterOptions,
   306  ) (ClusterSpecification, error) {
   307  	if err := opts.Validate(); err != nil {
   308  		return ClusterSpecification{}, err
   309  	}
   310  
   311  	nodeCfgs, nodeOpts, envConfig, err := GenerateDBNodeConfigsForCluster(configs, opts.DBNode)
   312  	if err != nil {
   313  		return ClusterSpecification{}, err
   314  	}
   315  
   316  	coordConfig := configs.Coordinator
   317  	// TODO(nate): refactor to support having envconfig if no DB.
   318  	if len(coordConfig.Clusters) > 0 {
   319  		coordConfig.Clusters[0].Client.EnvironmentConfig = &envConfig
   320  	} else {
   321  		coordConfig.Clusters = m3.ClustersStaticConfiguration{
   322  			{
   323  				Client: client.Configuration{
   324  					EnvironmentConfig: &envConfig,
   325  				},
   326  			},
   327  		}
   328  	}
   329  
   330  	var aggCfgs []aggcfg.Configuration
   331  	if opts.Aggregator != nil {
   332  		aggCfgs, err = GenerateAggregatorConfigsForCluster(configs, opts.Aggregator)
   333  		if err != nil {
   334  			return ClusterSpecification{}, err
   335  		}
   336  	}
   337  
   338  	return ClusterSpecification{
   339  		Configs: PerInstanceConfigs{
   340  			DBNodes:     nodeCfgs,
   341  			Coordinator: coordConfig,
   342  			Aggregators: aggCfgs,
   343  		},
   344  		Options: PerInstanceOptions{
   345  			DBNode: nodeOpts,
   346  		},
   347  	}, nil
   348  }
   349  
   350  // GenerateDBNodeConfigsForCluster generates the unique configs and options
   351  // for each DB node that will be instantiated. Additionally, provides
   352  // default environment config that can be used to connect to embedded KV
   353  // within the DB nodes.
   354  func GenerateDBNodeConfigsForCluster(
   355  	configs ClusterConfigs,
   356  	opts *resources.DBNodeClusterOptions,
   357  ) ([]dbcfg.Configuration, []DBNodeOptions, environment.Configuration, error) {
   358  	if opts == nil {
   359  		return nil, nil, environment.Configuration{}, errors.New("dbnode cluster options is nil")
   360  	}
   361  
   362  	var (
   363  		numNodes            = opts.RF * opts.NumInstances
   364  		generatePortsAndIDs = numNodes > 1
   365  	)
   366  
   367  	// TODO(nate): eventually support clients specifying their own discovery stanza.
   368  	// Practically, this should cover 99% of cases.
   369  	//
   370  	// Generate a discovery config with the dbnode using the generated hostID marked as
   371  	// the etcd server (i.e. seed node).
   372  	hostID := uuid.NewString()
   373  	defaultDBNodesCfg := configs.DBNode
   374  	discoveryCfg, envConfig, err := generateDefaultDiscoveryConfig(
   375  		defaultDBNodesCfg,
   376  		hostID,
   377  		generatePortsAndIDs)
   378  	if err != nil {
   379  		return nil, nil, environment.Configuration{}, err
   380  	}
   381  
   382  	var (
   383  		defaultDBNodeOpts = DBNodeOptions{
   384  			GenerateHostID: generatePortsAndIDs,
   385  			GeneratePorts:  generatePortsAndIDs,
   386  			Start:          true,
   387  		}
   388  		cfgs     = make([]dbcfg.Configuration, 0, numNodes)
   389  		nodeOpts = make([]DBNodeOptions, 0, numNodes)
   390  	)
   391  	for i := 0; i < int(numNodes); i++ {
   392  		var cfg dbcfg.Configuration
   393  		cfg, err = defaultDBNodesCfg.DeepCopy()
   394  		if err != nil {
   395  			return nil, nil, environment.Configuration{}, err
   396  		}
   397  		dbnodeOpts := defaultDBNodeOpts
   398  
   399  		if i == 0 {
   400  			// Mark the initial node as the etcd seed node.
   401  			dbnodeOpts.GenerateHostID = false
   402  			cfg.DB.HostID = &hostid.Configuration{
   403  				Resolver: hostid.ConfigResolver,
   404  				Value:    &hostID,
   405  			}
   406  		}
   407  		cfg.DB.Discovery = &discoveryCfg
   408  
   409  		cfgs = append(cfgs, cfg)
   410  		nodeOpts = append(nodeOpts, dbnodeOpts)
   411  	}
   412  
   413  	return cfgs, nodeOpts, envConfig, nil
   414  }
   415  
   416  // generateDefaultDiscoveryConfig handles creating the correct config
   417  // for having an embedded ETCD server with the correct server and
   418  // client configuration.
   419  func generateDefaultDiscoveryConfig(
   420  	cfg dbcfg.Configuration,
   421  	hostID string,
   422  	generateETCDPorts bool,
   423  ) (discovery.Configuration, environment.Configuration, error) {
   424  	discoveryConfig := cfg.DB.DiscoveryOrDefault()
   425  	envConfig, err := discoveryConfig.EnvironmentConfig(hostID)
   426  	if err != nil {
   427  		return discovery.Configuration{}, environment.Configuration{}, err
   428  	}
   429  
   430  	var (
   431  		etcdClientPort = dbcfg.DefaultEtcdClientPort
   432  		etcdServerPort = dbcfg.DefaultEtcdServerPort
   433  	)
   434  	if generateETCDPorts {
   435  		etcdClientPort, err = nettest.GetAvailablePort()
   436  		if err != nil {
   437  			return discovery.Configuration{}, environment.Configuration{}, err
   438  		}
   439  
   440  		etcdServerPort, err = nettest.GetAvailablePort()
   441  		if err != nil {
   442  			return discovery.Configuration{}, environment.Configuration{}, err
   443  		}
   444  	}
   445  
   446  	etcdServerURL := fmt.Sprintf("http://0.0.0.0:%d", etcdServerPort)
   447  	etcdClientAddr := net.JoinHostPort("0.0.0.0", strconv.Itoa(etcdClientPort))
   448  	etcdClientURL := fmt.Sprintf("http://0.0.0.0:%d", etcdClientPort)
   449  
   450  	envConfig.SeedNodes.InitialCluster[0].Endpoint = etcdServerURL
   451  	envConfig.SeedNodes.InitialCluster[0].HostID = hostID
   452  	envConfig.Services[0].Service.ETCDClusters[0].Endpoints = []string{etcdClientAddr}
   453  	if generateETCDPorts {
   454  		envConfig.SeedNodes.ListenPeerUrls = []string{etcdServerURL}
   455  		envConfig.SeedNodes.ListenClientUrls = []string{etcdClientURL}
   456  		envConfig.SeedNodes.InitialAdvertisePeerUrls = []string{etcdServerURL}
   457  		envConfig.SeedNodes.AdvertiseClientUrls = []string{etcdClientURL}
   458  	}
   459  
   460  	configType := discovery.ConfigType
   461  	return discovery.Configuration{
   462  		Type:   &configType,
   463  		Config: &envConfig,
   464  	}, envConfig, nil
   465  }
   466  
   467  func cleanup(logger *zap.Logger, nodes resources.Nodes, coord resources.Coordinator, aggs resources.Aggregators) {
   468  	var multiErr xerrors.MultiError
   469  	for _, n := range nodes {
   470  		multiErr = multiErr.Add(n.Close())
   471  	}
   472  
   473  	if coord != nil {
   474  		multiErr = multiErr.Add(coord.Close())
   475  	}
   476  
   477  	for _, a := range aggs {
   478  		multiErr = multiErr.Add(a.Close())
   479  	}
   480  
   481  	if !multiErr.Empty() {
   482  		logger.Warn("failed closing resources", zap.Error(multiErr.FinalError()))
   483  	}
   484  }
   485  
   486  // GenerateAggregatorConfigsForCluster generates the unique configs for each aggregator instance.
   487  func GenerateAggregatorConfigsForCluster(
   488  	configs ClusterConfigs,
   489  	opts *resources.AggregatorClusterOptions,
   490  ) ([]aggcfg.Configuration, error) {
   491  	if configs.Aggregator == nil {
   492  		return nil, nil
   493  	}
   494  
   495  	cfgs := make([]aggcfg.Configuration, 0, int(opts.NumInstances))
   496  	for i := 0; i < int(opts.NumInstances); i++ {
   497  		cfg, err := configs.Aggregator.DeepCopy()
   498  		if err != nil {
   499  			return nil, err
   500  		}
   501  
   502  		hostID := fmt.Sprintf("m3aggregator%02d", i)
   503  		aggCfg := cfg.AggregatorOrDefault()
   504  		aggCfg.HostID = &hostid.Configuration{
   505  			Resolver: hostid.ConfigResolver,
   506  			Value:    &hostID,
   507  		}
   508  		cfg.Aggregator = &aggCfg
   509  
   510  		cfgs = append(cfgs, cfg)
   511  	}
   512  
   513  	return cfgs, nil
   514  }