From ce1c640ce028618a42bc0a61a74ddee2121cd26e Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Thu, 14 Mar 2024 03:38:33 -0700 Subject: [PATCH] feat: allow retaining parity SLA to be configurable (#19260) at scale customers might start with failed drives, causing skew in the overall usage ratio per EC set. make this configurable such that customers can turn this off as needed depending on how comfortable they are. --- cmd/erasure-multipart.go | 47 +++++++++---------- cmd/erasure-object.go | 24 ++++------ internal/config/storageclass/help.go | 8 +++- internal/config/storageclass/storage-class.go | 39 ++++++++++++++- 4 files changed, 77 insertions(+), 41 deletions(-) diff --git a/cmd/erasure-multipart.go b/cmd/erasure-multipart.go index 21c48c592..38ed33686 100644 --- a/cmd/erasure-multipart.go +++ b/cmd/erasure-multipart.go @@ -401,36 +401,33 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string, parityDrives = er.defaultParityCount } - // If we have offline disks upgrade the number of erasure codes for this object. - parityOrig := parityDrives + if globalStorageClass.AvailabilityOptimized() { + // If we have offline disks upgrade the number of erasure codes for this object. + parityOrig := parityDrives - var offlineDrives int - for _, disk := range onlineDisks { - if disk == nil { - parityDrives++ - offlineDrives++ - continue + var offlineDrives int + for _, disk := range onlineDisks { + if disk == nil || !disk.IsOnline() { + parityDrives++ + offlineDrives++ + continue + } } - if !disk.IsOnline() { - parityDrives++ - offlineDrives++ - continue + + if offlineDrives >= (len(onlineDisks)+1)/2 { + // if offline drives are more than 50% of the drives + // we have no quorum, we shouldn't proceed just + // fail at that point. + return nil, toObjectErr(errErasureWriteQuorum, bucket, object) } - } - if offlineDrives >= (len(onlineDisks)+1)/2 { - // if offline drives are more than 50% of the drives - // we have no quorum, we shouldn't proceed just - // fail at that point. - return nil, toObjectErr(errErasureWriteQuorum, bucket, object) - } + if parityDrives >= len(onlineDisks)/2 { + parityDrives = len(onlineDisks) / 2 + } - if parityDrives >= len(onlineDisks)/2 { - parityDrives = len(onlineDisks) / 2 - } - - if parityOrig != parityDrives { - userDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives) + if parityOrig != parityDrives { + userDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives) + } } dataDrives := len(onlineDisks) - parityDrives diff --git a/cmd/erasure-object.go b/cmd/erasure-object.go index 089bdbf55..3ad9e03c8 100644 --- a/cmd/erasure-object.go +++ b/cmd/erasure-object.go @@ -1297,25 +1297,21 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st storageDisks := er.getDisks() - parityDrives := len(storageDisks) / 2 - if !opts.MaxParity { - // Get parity and data drive count based on storage class metadata - parityDrives = globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass]) - if parityDrives < 0 { - parityDrives = er.defaultParityCount - } - + // Get parity and data drive count based on storage class metadata + parityDrives := globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass]) + if parityDrives < 0 { + parityDrives = er.defaultParityCount + } + if opts.MaxParity { + parityDrives = len(storageDisks) / 2 + } + if !opts.MaxParity && globalStorageClass.AvailabilityOptimized() { // If we have offline disks upgrade the number of erasure codes for this object. parityOrig := parityDrives var offlineDrives int for _, disk := range storageDisks { - if disk == nil { - parityDrives++ - offlineDrives++ - continue - } - if !disk.IsOnline() { + if disk == nil || !disk.IsOnline() { parityDrives++ offlineDrives++ continue diff --git a/internal/config/storageclass/help.go b/internal/config/storageclass/help.go index 72afafb77..a6af8b21c 100644 --- a/internal/config/storageclass/help.go +++ b/internal/config/storageclass/help.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2021 MinIO, Inc. +// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // @@ -38,6 +38,12 @@ var ( Optional: true, Type: "string", }, + config.HelpKV{ + Key: ClassOptimize, + Description: `optimize parity calculation for standard storage class, set 'capacity' for capacity optimized (no additional parity)` + defaultHelpPostfix(ClassOptimize), + Optional: true, + Type: "string", + }, config.HelpKV{ Key: config.Comment, Description: config.DefaultComment, diff --git a/internal/config/storageclass/storage-class.go b/internal/config/storageclass/storage-class.go index 15580a6aa..f0a430987 100644 --- a/internal/config/storageclass/storage-class.go +++ b/internal/config/storageclass/storage-class.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2021 MinIO, Inc. +// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // @@ -40,11 +40,14 @@ const ( const ( ClassStandard = "standard" ClassRRS = "rrs" + ClassOptimize = "optimize" // Reduced redundancy storage class environment variable RRSEnv = "MINIO_STORAGE_CLASS_RRS" // Standard storage class environment variable StandardEnv = "MINIO_STORAGE_CLASS_STANDARD" + // Optimize storage class environment variable + OptimizeEnv = "MINIO_STORAGE_CLASS_OPTIMIZE" // Supported storage class scheme is EC schemePrefix = "EC" @@ -67,6 +70,10 @@ var ( Key: ClassRRS, Value: "EC:1", }, + config.KV{ + Key: ClassOptimize, + Value: "availability", + }, } ) @@ -82,6 +89,7 @@ var ConfigLock sync.RWMutex type Config struct { Standard StorageClass `json:"standard"` RRS StorageClass `json:"rrs"` + Optimize string `json:"optimize"` initialized bool } @@ -245,12 +253,40 @@ func (sCfg *Config) GetParityForSC(sc string) (parity int) { } } +// CapacityOptimized - returns true if the storage-class is capacity optimized +// meaning we will not use additional parities when drives are offline. +// +// Default is "availability" optimized, unless this is configured. +func (sCfg *Config) CapacityOptimized() bool { + ConfigLock.RLock() + defer ConfigLock.RUnlock() + if !sCfg.initialized { + return false + } + return sCfg.Optimize == "capacity" +} + +// AvailabilityOptimized - returns true if the storage-class is availability +// optimized, meaning we will use additional parities when drives are offline +// to retain parity SLA. +// +// Default is "availability" optimized. +func (sCfg *Config) AvailabilityOptimized() bool { + ConfigLock.RLock() + defer ConfigLock.RUnlock() + if !sCfg.initialized { + return true + } + return sCfg.Optimize == "availability" || sCfg.Optimize == "" +} + // Update update storage-class with new config func (sCfg *Config) Update(newCfg Config) { ConfigLock.Lock() defer ConfigLock.Unlock() sCfg.RRS = newCfg.RRS sCfg.Standard = newCfg.Standard + sCfg.Optimize = newCfg.Optimize sCfg.initialized = true } @@ -320,5 +356,6 @@ func LookupConfig(kvs config.KVS, setDriveCount int) (cfg Config, err error) { } cfg.initialized = true + cfg.Optimize = env.Get(OptimizeEnv, kvs.Get(ClassOptimize)) return cfg, nil }