github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/environment/config.go (about) 1 // Copyright (c) 2017 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 environment 22 23 import ( 24 "errors" 25 "fmt" 26 "os" 27 "sort" 28 "time" 29 30 clusterclient "github.com/m3db/m3/src/cluster/client" 31 etcdclient "github.com/m3db/m3/src/cluster/client/etcd" 32 "github.com/m3db/m3/src/cluster/kv" 33 m3clusterkvmem "github.com/m3db/m3/src/cluster/kv/mem" 34 "github.com/m3db/m3/src/cluster/services" 35 "github.com/m3db/m3/src/cluster/shard" 36 "github.com/m3db/m3/src/dbnode/kvconfig" 37 "github.com/m3db/m3/src/dbnode/namespace" 38 "github.com/m3db/m3/src/dbnode/sharding" 39 "github.com/m3db/m3/src/dbnode/topology" 40 "github.com/m3db/m3/src/x/instrument" 41 ) 42 43 var ( 44 errInvalidConfig = errors.New("must supply either service or static config") 45 errInvalidSyncCount = errors.New("must supply exactly one synchronous cluster") 46 ) 47 48 // Configuration is a configuration that can be used to create namespaces, a topology, and kv store 49 type Configuration struct { 50 // Services is used when a topology initializer is not supplied. 51 Services DynamicConfiguration `yaml:"services"` 52 53 // StaticConfiguration is used for running M3DB with static configs 54 Statics StaticConfiguration `yaml:"statics"` 55 56 // Presence of a (etcd) server in this config denotes an embedded cluster 57 SeedNodes *SeedNodesConfig `yaml:"seedNodes"` 58 } 59 60 // SeedNodesConfig defines fields for seed node 61 type SeedNodesConfig struct { 62 RootDir string `yaml:"rootDir"` 63 InitialAdvertisePeerUrls []string `yaml:"initialAdvertisePeerUrls"` 64 AdvertiseClientUrls []string `yaml:"advertiseClientUrls"` 65 ListenPeerUrls []string `yaml:"listenPeerUrls"` 66 ListenClientUrls []string `yaml:"listenClientUrls"` 67 InitialCluster []SeedNode `yaml:"initialCluster"` 68 ClientTransportSecurity SeedNodeSecurityConfig `yaml:"clientTransportSecurity"` 69 PeerTransportSecurity SeedNodeSecurityConfig `yaml:"peerTransportSecurity"` 70 } 71 72 // SeedNode represents a seed node for the cluster 73 type SeedNode struct { 74 HostID string `yaml:"hostID"` 75 Endpoint string `yaml:"endpoint"` 76 ClusterState string `yaml:"clusterState"` 77 } 78 79 // SeedNodeSecurityConfig contains the data used for security in seed nodes 80 type SeedNodeSecurityConfig struct { 81 CAFile string `yaml:"caFile"` 82 CertFile string `yaml:"certFile"` 83 KeyFile string `yaml:"keyFile"` 84 TrustedCAFile string `yaml:"trustedCaFile"` 85 CertAuth bool `yaml:"clientCertAuth"` 86 AutoTLS bool `yaml:"autoTls"` 87 } 88 89 // DynamicConfiguration is used for running M3DB with a dynamic config 90 type DynamicConfiguration []*DynamicCluster 91 92 // DynamicCluster is a single cluster in a dynamic configuration 93 type DynamicCluster struct { 94 Async bool `yaml:"async"` 95 ClientOverrides ClientOverrides `yaml:"clientOverrides"` 96 Service *etcdclient.Configuration `yaml:"service"` 97 } 98 99 // ClientOverrides represents M3DB client overrides for a given cluster. 100 type ClientOverrides struct { 101 HostQueueFlushInterval *time.Duration `yaml:"hostQueueFlushInterval"` 102 TargetHostQueueFlushSize *int `yaml:"targetHostQueueFlushSize"` 103 } 104 105 // Validate validates the DynamicConfiguration. 106 func (c DynamicConfiguration) Validate() error { 107 syncCount := 0 108 for _, cfg := range c { 109 if !cfg.Async { 110 syncCount++ 111 } 112 if cfg.ClientOverrides.TargetHostQueueFlushSize != nil && *cfg.ClientOverrides.TargetHostQueueFlushSize <= 1 { 113 return fmt.Errorf("target host queue flush size must be larger than zero but was: %d", cfg.ClientOverrides.TargetHostQueueFlushSize) 114 } 115 116 if cfg.ClientOverrides.HostQueueFlushInterval != nil && *cfg.ClientOverrides.HostQueueFlushInterval <= 0 { 117 return fmt.Errorf("host queue flush interval must be larger than zero but was: %s", cfg.ClientOverrides.HostQueueFlushInterval.String()) 118 } 119 } 120 if syncCount != 1 { 121 return errInvalidSyncCount 122 } 123 return nil 124 } 125 126 // SyncCluster returns the synchronous cluster in the DynamicConfiguration 127 func (c DynamicConfiguration) SyncCluster() (*DynamicCluster, error) { 128 if err := c.Validate(); err != nil { 129 return nil, err 130 } 131 132 for _, cluster := range c { 133 if !cluster.Async { 134 return cluster, nil 135 } 136 } 137 return nil, errInvalidSyncCount 138 } 139 140 // StaticConfiguration is used for running M3DB with a static config 141 type StaticConfiguration []*StaticCluster 142 143 // StaticCluster is a single cluster in a static configuration 144 type StaticCluster struct { 145 Async bool `yaml:"async"` 146 ClientOverrides ClientOverrides `yaml:"clientOverrides"` 147 Namespaces []namespace.MetadataConfiguration `yaml:"namespaces"` 148 TopologyConfig *topology.StaticConfiguration `yaml:"topology"` 149 ListenAddress string `yaml:"listenAddress"` 150 } 151 152 // Validate validates the StaticConfiguration 153 func (c StaticConfiguration) Validate() error { 154 syncCount := 0 155 for _, cfg := range c { 156 if !cfg.Async { 157 syncCount++ 158 } 159 if cfg.ClientOverrides.TargetHostQueueFlushSize != nil && *cfg.ClientOverrides.TargetHostQueueFlushSize <= 1 { 160 return fmt.Errorf("target host queue flush size must be larger than zero but was: %d", cfg.ClientOverrides.TargetHostQueueFlushSize) 161 } 162 163 if cfg.ClientOverrides.HostQueueFlushInterval != nil && *cfg.ClientOverrides.HostQueueFlushInterval <= 0 { 164 return fmt.Errorf("host queue flush interval must be larger than zero but was: %s", cfg.ClientOverrides.HostQueueFlushInterval.String()) 165 } 166 } 167 if syncCount != 1 { 168 return errInvalidSyncCount 169 } 170 return nil 171 } 172 173 // ConfigureResult stores initializers and kv store for dynamic and static configs 174 type ConfigureResult struct { 175 NamespaceInitializer namespace.Initializer 176 TopologyInitializer topology.Initializer 177 ClusterClient clusterclient.Client 178 KVStore kv.Store 179 Async bool 180 ClientOverrides ClientOverrides 181 } 182 183 // ConfigureResults stores initializers and kv store for dynamic and static configs 184 type ConfigureResults []ConfigureResult 185 186 // SyncCluster returns the synchronous cluster in the ConfigureResults 187 func (c ConfigureResults) SyncCluster() (ConfigureResult, error) { 188 for _, result := range c { 189 if !result.Async { 190 return result, nil 191 } 192 } 193 return ConfigureResult{}, errInvalidSyncCount 194 } 195 196 // ConfigurationParameters are options used to create new ConfigureResults 197 type ConfigurationParameters struct { 198 InterruptedCh <-chan struct{} 199 InstrumentOpts instrument.Options 200 HashingSeed uint32 201 HostID string 202 NewDirectoryMode os.FileMode 203 ForceColdWritesEnabled bool 204 // AllowEmptyInitialNamespaceRegistry determines whether to allow the initial 205 // namespace update to be empty or to wait indefinitely until namespaces are received. 206 // This is used when configuring the namespaceInitializer. 207 AllowEmptyInitialNamespaceRegistry bool 208 } 209 210 // UnmarshalYAML normalizes the config into a list of services. 211 func (c *Configuration) UnmarshalYAML(unmarshal func(interface{}) error) error { 212 var cfg struct { 213 Services DynamicConfiguration `yaml:"services"` 214 Service *etcdclient.Configuration `yaml:"service"` 215 Static *StaticCluster `yaml:"static"` 216 Statics StaticConfiguration `yaml:"statics"` 217 SeedNodes *SeedNodesConfig `yaml:"seedNodes"` 218 } 219 220 if err := unmarshal(&cfg); err != nil { 221 return err 222 } 223 224 c.SeedNodes = cfg.SeedNodes 225 c.Statics = cfg.Statics 226 if cfg.Static != nil { 227 c.Statics = StaticConfiguration{cfg.Static} 228 } 229 c.Services = cfg.Services 230 if cfg.Service != nil { 231 c.Services = DynamicConfiguration{ 232 &DynamicCluster{Service: cfg.Service}, 233 } 234 } 235 236 return nil 237 } 238 239 // Validate validates the configuration. 240 func (c *Configuration) Validate() error { 241 if (c.Services == nil && c.Statics == nil) || 242 (len(c.Services) > 0 && len(c.Statics) > 0) { 243 return errInvalidConfig 244 } 245 246 if len(c.Services) > 0 { 247 if err := c.Services.Validate(); err != nil { 248 return err 249 } 250 } 251 252 if len(c.Statics) > 0 { 253 if err := c.Statics.Validate(); err != nil { 254 return err 255 } 256 } 257 return nil 258 } 259 260 // Configure creates a new ConfigureResults 261 func (c Configuration) Configure(cfgParams ConfigurationParameters) (ConfigureResults, error) { 262 var emptyConfig ConfigureResults 263 264 // Validate here rather than UnmarshalYAML since we need to ensure one of 265 // dynamic or static configuration are provided. A blank YAML does not 266 // call UnmarshalYAML and therefore validation would be skipped. 267 if err := c.Validate(); err != nil { 268 return emptyConfig, err 269 } 270 271 if len(c.Services) > 0 { 272 return c.configureDynamic(cfgParams) 273 } 274 275 if len(c.Statics) > 0 { 276 return c.configureStatic(cfgParams) 277 } 278 279 return emptyConfig, errInvalidConfig 280 } 281 282 func (c Configuration) configureDynamic(cfgParams ConfigurationParameters) (ConfigureResults, error) { 283 var emptyConfig ConfigureResults 284 if err := c.Services.Validate(); err != nil { 285 return emptyConfig, err 286 } 287 288 cfgResults := make(ConfigureResults, 0, len(c.Services)) 289 for _, cluster := range c.Services { 290 configSvcClientOpts := cluster.Service.NewOptions(). 291 SetInstrumentOptions(cfgParams.InstrumentOpts). 292 // Set timeout to zero so it will wait indefinitely for the 293 // initial value. 294 SetServicesOptions(services.NewOptions().SetInitTimeout(0)). 295 SetNewDirectoryMode(cfgParams.NewDirectoryMode) 296 configSvcClient, err := etcdclient.NewConfigServiceClient(configSvcClientOpts) 297 if err != nil { 298 err = fmt.Errorf("could not create m3cluster client: %v", err) 299 return emptyConfig, err 300 } 301 302 dynamicOpts := namespace.NewDynamicOptions(). 303 SetInstrumentOptions(cfgParams.InstrumentOpts). 304 SetConfigServiceClient(configSvcClient). 305 SetNamespaceRegistryKey(kvconfig.NamespacesKey). 306 SetForceColdWritesEnabled(cfgParams.ForceColdWritesEnabled). 307 SetAllowEmptyInitialNamespaceRegistry(cfgParams.AllowEmptyInitialNamespaceRegistry) 308 nsInit := namespace.NewDynamicInitializer(dynamicOpts) 309 310 serviceID := services.NewServiceID(). 311 SetName(cluster.Service.Service). 312 SetEnvironment(cluster.Service.Env). 313 SetZone(cluster.Service.Zone) 314 315 topoOpts := topology.NewDynamicOptions(). 316 SetConfigServiceClient(configSvcClient). 317 SetServiceID(serviceID). 318 SetQueryOptions(services.NewQueryOptions(). 319 SetIncludeUnhealthy(true). 320 SetInterruptedCh(cfgParams.InterruptedCh)). 321 SetInstrumentOptions(cfgParams.InstrumentOpts). 322 SetHashGen(sharding.NewHashGenWithSeed(cfgParams.HashingSeed)) 323 topoInit := topology.NewDynamicInitializer(topoOpts) 324 325 kv, err := configSvcClient.KV() 326 if err != nil { 327 err = fmt.Errorf("could not create KV client, %v", err) 328 return emptyConfig, err 329 } 330 331 result := ConfigureResult{ 332 NamespaceInitializer: nsInit, 333 TopologyInitializer: topoInit, 334 ClusterClient: configSvcClient, 335 KVStore: kv, 336 Async: cluster.Async, 337 ClientOverrides: cluster.ClientOverrides, 338 } 339 cfgResults = append(cfgResults, result) 340 } 341 342 return cfgResults, nil 343 } 344 345 func (c Configuration) configureStatic(cfgParams ConfigurationParameters) (ConfigureResults, error) { 346 var emptyConfig ConfigureResults 347 348 if err := c.Statics.Validate(); err != nil { 349 return emptyConfig, err 350 } 351 352 cfgResults := make(ConfigureResults, 0, len(c.Services)) 353 for _, cluster := range c.Statics { 354 nsList := []namespace.Metadata{} 355 for _, ns := range cluster.Namespaces { 356 md, err := ns.Metadata() 357 if err != nil { 358 err = fmt.Errorf("unable to create metadata for static config: %v", err) 359 return emptyConfig, err 360 } 361 nsList = append(nsList, md) 362 } 363 // NB(bodu): Force cold writes to be enabled for all ns if specified. 364 if cfgParams.ForceColdWritesEnabled { 365 nsList = namespace.ForceColdWritesEnabledForMetadatas(nsList) 366 } 367 368 nsInitStatic := namespace.NewStaticInitializer(nsList) 369 370 numReplicas := cluster.TopologyConfig.Replicas 371 if numReplicas == 0 { 372 numReplicas = 1 373 } 374 375 shardSet, hostShardSets, err := newStaticShardSet( 376 cluster.TopologyConfig.Shards, 377 numReplicas, 378 cluster.TopologyConfig.Hosts, 379 ) 380 if err != nil { 381 err = fmt.Errorf("unable to create shard set for static config: %v", err) 382 return emptyConfig, err 383 } 384 staticOptions := topology.NewStaticOptions(). 385 SetHostShardSets(hostShardSets). 386 SetShardSet(shardSet). 387 SetReplicas(numReplicas) 388 389 topoInit := topology.NewStaticInitializer(staticOptions) 390 result := ConfigureResult{ 391 NamespaceInitializer: nsInitStatic, 392 TopologyInitializer: topoInit, 393 KVStore: m3clusterkvmem.NewStore(), 394 Async: cluster.Async, 395 ClientOverrides: cluster.ClientOverrides, 396 } 397 cfgResults = append(cfgResults, result) 398 } 399 400 return cfgResults, nil 401 } 402 403 func newStaticShardSet( 404 numShards int, 405 rf int, 406 hosts []topology.HostShardConfig, 407 ) (sharding.ShardSet, []topology.HostShardSet, error) { 408 var ( 409 shardSet sharding.ShardSet 410 shardIDs []uint32 411 err error 412 ) 413 414 for i := uint32(0); i < uint32(numShards); i++ { 415 shardIDs = append(shardIDs, i) 416 } 417 418 shards := sharding.NewShards(shardIDs, shard.Available) 419 shardSet, err = sharding.NewShardSet(shards, sharding.DefaultHashFn(numShards)) 420 if err != nil { 421 return nil, nil, err 422 } 423 424 hostShardSets, err := generatePlacement(hosts, numShards, rf) 425 if err != nil { 426 return nil, nil, err 427 } 428 429 return shardSet, hostShardSets, nil 430 } 431 432 func generatePlacement(hosts []topology.HostShardConfig, numShards int, rf int) ([]topology.HostShardSet, error) { 433 numHosts := len(hosts) 434 if numHosts == 0 || numShards < 1 || rf < 1 { 435 return nil, errors.New("number of hosts, shards, and RF must be positive") 436 } 437 if rf > numHosts { 438 return nil, errors.New("number of hosts must be >=RF") 439 } 440 441 hostShards := make([][]shard.Shard, numHosts) 442 hostIdx := 0 443 // Round robin assign shard replicas to hosts. 444 for shardInt := uint32(0); shardInt < uint32(numShards); shardInt++ { 445 for replica := 0; replica < rf; replica++ { 446 newShard := shard.NewShard(shardInt).SetState(shard.Available) 447 hostShards[hostIdx] = append(hostShards[hostIdx], newShard) 448 hostIdx = (hostIdx + 1) % numHosts 449 } 450 } 451 452 hostShardSets := make([]topology.HostShardSet, 0, numHosts) 453 sortedHosts := make([]topology.HostShardConfig, numHosts) 454 // Plain copy is okay because struct just contains strings. 455 copy(sortedHosts, hosts) 456 sort.Slice(sortedHosts, func(i, j int) bool { return sortedHosts[i].HostID < sortedHosts[j].HostID }) 457 for i, host := range sortedHosts { 458 host := topology.NewHost(host.HostID, host.ListenAddress) 459 shardSet, err := sharding.NewShardSet(hostShards[i], sharding.DefaultHashFn(numShards)) 460 if err != nil { 461 return nil, fmt.Errorf("error constructing new ShardSet: %w", err) 462 } 463 hostShardSet := topology.NewHostShardSet(host, shardSet) 464 hostShardSets = append(hostShardSets, hostShardSet) 465 } 466 467 return hostShardSets, nil 468 }