@@ -22,7 +22,9 @@ import (
2222 "encoding/hex"
2323 "errors"
2424 "fmt"
25+ "math"
2526 "os"
27+ "regexp"
2628 "strconv"
2729 "strings"
2830 "sync"
@@ -66,15 +68,15 @@ const (
6668// AWS provisioning limits.
6769// Source: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
6870const (
69- io1MinTotalIOPS = 100
70- io1MaxTotalIOPS = 64000
71- io1MaxIOPSPerGB = 50
72- io2MinTotalIOPS = 100
73- io2MaxTotalIOPS = 256000
74- io2MaxIOPSPerGB = 1000
75- gp3MaxTotalIOPS = 16000
76- gp3MinTotalIOPS = 3000
77- gp3MaxIOPSPerGB = 500
71+ io1MinTotalIOPS = 100
72+ io1FallbackMaxIOPS = 64000
73+ io1MaxIOPSPerGB = 50
74+ io2MinTotalIOPS = 100
75+ io2FallbackMaxIOPS = 256000
76+ io2MaxIOPSPerGB = 1000
77+ gp3FallbackMaxIOPS = 16000
78+ gp3MinTotalIOPS = 3000
79+ gp3MaxIOPSPerGB = 500
7880)
7981
8082var (
9092)
9193
9294const (
93- cacheForgetDelay = 1 * time .Hour
94- volInitCacheForgetDelay = 6 * time .Hour
95+ cacheForgetDelay = 1 * time .Hour
96+ volInitCacheForgetDelay = 6 * time .Hour
97+ iopsLimitCacheForgetDelay = 12 * time .Hour
9598
9699 dryRunInterval = 3 * time .Hour
97100
@@ -186,6 +189,17 @@ const (
186189 ValidationException = "ValidationException"
187190)
188191
192+ // Regex Patterns.
193+ var (
194+ // For getting IOPS limit from gp3/io1 error.
195+ // Error example it is used for: "An error occurred (InvalidParameterValue) when calling the CreateVolume operation: Volume iops of 200000 is too high; maximum is 80000".
196+ nonIo2ErrRegex = regexp .MustCompile (`(?i)volume iops.*is too high.*maximum is (\d+)` )
197+
198+ // For getting IOPS limit from io2 error.
199+ // Error example it is used for: "An error occurred (InvalidParameterCombination) when calling the CreateVolume operation: io2 volumes configured with greater than 64 TiB or 256K IOPS or 1000:1 IOPS:GB ratio are not supported".
200+ io2ErrRegex = regexp .MustCompile (`(?i)(\d+)K IOPS` )
201+ )
202+
189203var invalidParameterErrorCodes = map [string ]struct {}{
190204 "InvalidParameter" : {},
191205 "InvalidParameterCombination" : {},
@@ -236,6 +250,20 @@ type ModifyDiskOptions struct {
236250 Throughput int32
237251}
238252
253+ // iopsLimits represents the IOPS limits set by EBS of a volume dependent on the volume type.
254+ type iopsLimits struct {
255+ maxIops int32
256+ minIops int32
257+ maxIopsPerGb int32
258+ }
259+
260+ // getVolumeLimitsParams represents the AZ parameters that getVolumeLimits will use to make the DryRun CreateVolume call.
261+ type getVolumeLimitsParams struct {
262+ availabilityZone string
263+ availabilityZoneId string
264+ outpostArn string
265+ }
266+
239267// ModifyTagsOptions represents parameter to modify the tags of an existing EBS volume.
240268type ModifyTagsOptions struct {
241269 TagsToAdd map [string ]string
@@ -339,6 +367,7 @@ type cloud struct {
339367 likelyBadDeviceNames expiringcache.ExpiringCache [string , sync.Map ]
340368 latestClientTokens expiringcache.ExpiringCache [string , int ]
341369 volumeInitializations expiringcache.ExpiringCache [string , volumeInitialization ]
370+ latestIOPSLimits expiringcache.ExpiringCache [string , iopsLimits ]
342371 accountID string
343372 accountIDOnce sync.Once
344373 attemptDryRun atomic.Bool
@@ -412,6 +441,7 @@ func NewCloud(region string, awsSdkDebugLog bool, userAgentExtra string, batchin
412441 likelyBadDeviceNames : expiringcache.New [string , sync.Map ](cacheForgetDelay ),
413442 latestClientTokens : expiringcache.New [string , int ](cacheForgetDelay ),
414443 volumeInitializations : expiringcache.New [string , volumeInitialization ](volInitCacheForgetDelay ),
444+ latestIOPSLimits : expiringcache.New [string , iopsLimits ](iopsLimitCacheForgetDelay ),
415445 }
416446
417447 // Ensure an EC2 Dry-run API call is made on startup and every dryRunInterval
@@ -572,9 +602,6 @@ func (c *cloud) CreateDisk(ctx context.Context, volumeName string, diskOptions *
572602 iops int32
573603 throughput int32
574604 err error
575- maxIops int32
576- minIops int32
577- maxIopsPerGb int32
578605 requestedIops int32
579606 )
580607
@@ -590,38 +617,10 @@ func (c *cloud) CreateDisk(ctx context.Context, volumeName string, diskOptions *
590617 createType = VolumeTypeGP3
591618 }
592619
593- switch createType {
594- case VolumeTypeGP2 , VolumeTypeSC1 , VolumeTypeST1 , VolumeTypeStandard :
595- case VolumeTypeIO1 :
596- maxIops = io1MaxTotalIOPS
597- minIops = io1MinTotalIOPS
598- maxIopsPerGb = io1MaxIOPSPerGB
599- case VolumeTypeIO2 :
600- maxIops = io2MaxTotalIOPS
601- minIops = io2MinTotalIOPS
602- maxIopsPerGb = io2MaxIOPSPerGB
603- case VolumeTypeGP3 :
604- maxIops = gp3MaxTotalIOPS
605- minIops = gp3MinTotalIOPS
606- maxIopsPerGb = gp3MaxIOPSPerGB
607- throughput = diskOptions .Throughput
608- default :
609- return nil , fmt .Errorf ("invalid AWS VolumeType %q" , diskOptions .VolumeType )
610- }
611-
612620 if diskOptions .MultiAttachEnabled && createType != VolumeTypeIO2 {
613621 return nil , errors .New ("CreateDisk: multi-attach is only supported for io2 volumes" )
614622 }
615623
616- if maxIops > 0 {
617- if diskOptions .IOPS > 0 {
618- requestedIops = diskOptions .IOPS
619- } else if diskOptions .IOPSPerGB > 0 {
620- requestedIops = diskOptions .IOPSPerGB * capacityGiB
621- }
622- iops = capIOPS (createType , capacityGiB , requestedIops , minIops , maxIops , maxIopsPerGb , diskOptions .AllowIOPSPerGBIncrease )
623- }
624-
625624 tags := make ([]types.Tag , 0 , len (diskOptions .Tags ))
626625 for key , value := range diskOptions .Tags {
627626 tags = append (tags , types.Tag {Key : aws .String (key ), Value : aws .String (value )})
@@ -678,6 +677,26 @@ func (c *cloud) CreateDisk(ctx context.Context, volumeName string, diskOptions *
678677 requestInput .OutpostArn = aws .String (diskOptions .OutpostArn )
679678 }
680679
680+ azParams := getVolumeLimitsParams {
681+ availabilityZone : zone ,
682+ availabilityZoneId : zoneID ,
683+ outpostArn : diskOptions .OutpostArn ,
684+ }
685+
686+ iopsLimit , err := c .getVolumeLimits (ctx , createType , azParams )
687+ if err != nil {
688+ return nil , fmt .Errorf ("invalid AWS VolumeType %q" , diskOptions .VolumeType )
689+ }
690+
691+ if iopsLimit .maxIops > 0 {
692+ if diskOptions .IOPS > 0 {
693+ requestedIops = diskOptions .IOPS
694+ } else if diskOptions .IOPSPerGB > 0 {
695+ requestedIops = diskOptions .IOPSPerGB * capacityGiB
696+ }
697+ iops = capIOPS (createType , capacityGiB , requestedIops , iopsLimit , diskOptions .AllowIOPSPerGBIncrease )
698+ }
699+
681700 if len (diskOptions .KmsKeyID ) > 0 {
682701 requestInput .KmsKeyId = aws .String (diskOptions .KmsKeyID )
683702 requestInput .Encrypted = aws .Bool (true )
@@ -2438,28 +2457,139 @@ func getVolumeAttachmentsList(volume types.Volume) []string {
24382457}
24392458
24402459// Calculate actual IOPS for a volume and cap it at supported AWS limits.
2441- func capIOPS (volumeType string , requestedCapacityGiB int32 , requestedIops int32 , minTotalIOPS , maxTotalIOPS , maxIOPSPerGB int32 , allowIncrease bool ) int32 {
2460+ func capIOPS (volumeType string , requestedCapacityGiB int32 , requestedIops int32 , iopsLimits iopsLimits , allowIncrease bool ) int32 {
24422461 // If requestedIops is zero the user did not request a specific amount, and the default will be used instead
24432462 if requestedIops == 0 {
24442463 return 0
24452464 }
24462465
24472466 iops := requestedIops
24482467
2449- if iops < minTotalIOPS {
2468+ if iops < iopsLimits . minIops {
24502469 if allowIncrease {
2451- iops = minTotalIOPS
2470+ iops = iopsLimits . minIops
24522471 klog .V (5 ).InfoS ("[Debug] Increased IOPS to the min supported limit" , "volumeType" , volumeType , "requestedCapacityGiB" , requestedCapacityGiB , "limit" , iops )
24532472 }
24542473 }
2455- if iops > maxTotalIOPS {
2456- iops = maxTotalIOPS
2474+ if iops > iopsLimits . maxIops {
2475+ iops = iopsLimits . maxIops
24572476 klog .V (5 ).InfoS ("[Debug] Capped IOPS, volume at the max supported limit" , "volumeType" , volumeType , "requestedCapacityGiB" , requestedCapacityGiB , "limit" , iops )
24582477 }
2459- maxIopsByCapacity := maxIOPSPerGB * requestedCapacityGiB
2460- if iops > maxIopsByCapacity && maxIopsByCapacity >= minTotalIOPS {
2478+ maxIopsByCapacity := iopsLimits . maxIopsPerGb * requestedCapacityGiB
2479+ if iops > maxIopsByCapacity && maxIopsByCapacity >= iopsLimits . minIops {
24612480 iops = maxIopsByCapacity
2462- klog .V (5 ).InfoS ("[Debug] Capped IOPS for volume" , "volumeType" , volumeType , "requestedCapacityGiB" , requestedCapacityGiB , "maxIOPSPerGB" , maxIOPSPerGB , "limit" , iops )
2481+ klog .V (5 ).InfoS ("[Debug] Capped IOPS for volume" , "volumeType" , volumeType , "requestedCapacityGiB" , requestedCapacityGiB , "maxIOPSPerGB" , iopsLimits . maxIopsPerGb , "limit" , iops )
24632482 }
24642483 return iops
24652484}
2485+
2486+ // Gets IOPS limits for a specific volume type in a specific Zone and caches it. If the limits are cached, simply return limits.
2487+ func (c * cloud ) getVolumeLimits (ctx context.Context , volumeType string , azParams getVolumeLimitsParams ) (iopsLimits iopsLimits , err error ) {
2488+ cacheKey := fmt .Sprintf ("%s|%s|%s|%s" , volumeType , azParams .availabilityZone , azParams .availabilityZoneId , azParams .outpostArn )
2489+ if value , ok := c .latestIOPSLimits .Get (cacheKey ); ok {
2490+ return * value , nil
2491+ }
2492+
2493+ dryRunRequestInput := & ec2.CreateVolumeInput {
2494+ VolumeType : types .VolumeType (volumeType ),
2495+ Size : aws .Int32 (4 ),
2496+ Iops : aws .Int32 (math .MaxInt32 ),
2497+ DryRun : aws .Bool (true ),
2498+ // Required by default EBS CSI Driver IAM policy.
2499+ TagSpecifications : []types.TagSpecification {
2500+ {
2501+ ResourceType : types .ResourceTypeVolume ,
2502+ Tags : []types.Tag {
2503+ {
2504+ Key : aws .String (VolumeNameTagKey ),
2505+ Value : aws .String ("IopsLimitDryRun" ),
2506+ },
2507+ },
2508+ },
2509+ },
2510+ }
2511+ if azParams .availabilityZone != "" {
2512+ dryRunRequestInput .AvailabilityZone = aws .String (azParams .availabilityZone )
2513+ }
2514+ if azParams .availabilityZoneId != "" {
2515+ dryRunRequestInput .AvailabilityZoneId = aws .String (azParams .availabilityZoneId )
2516+ }
2517+ if azParams .outpostArn != "" {
2518+ dryRunRequestInput .OutpostArn = aws .String (azParams .outpostArn )
2519+ }
2520+
2521+ volType := strings .ToLower (string (dryRunRequestInput .VolumeType ))
2522+ _ , err = c .ec2 .CreateVolume (ctx , dryRunRequestInput , func (o * ec2.Options ) {
2523+ o .APIOptions = nil // Don't add our logging/metrics middleware because we expect errors.
2524+ })
2525+ useFallBackLimits := (err == nil ) // If DryRun unexpectedly succeeds, we use fallback values.
2526+
2527+ if err != nil {
2528+ maxIops , err := extractMaxIOPSFromError (err .Error (), volType )
2529+ // Default To Hardcoded Limits if we can't get the max IOPS from the error message.
2530+ if err != nil {
2531+ klog .V (5 ).InfoS ("[Debug] error getting IOPS limit, defaulting to hardcoded values" , "volumeType" , volumeType , "error" , err .Error ())
2532+ useFallBackLimits = true
2533+ } else {
2534+ iopsLimits .maxIops = maxIops
2535+ }
2536+ }
2537+
2538+ if useFallBackLimits {
2539+ switch volType {
2540+ case VolumeTypeIO1 :
2541+ iopsLimits .maxIops = io1FallbackMaxIOPS
2542+ case VolumeTypeIO2 :
2543+ iopsLimits .maxIops = io2FallbackMaxIOPS
2544+ case VolumeTypeGP3 :
2545+ iopsLimits .maxIops = gp3FallbackMaxIOPS
2546+ }
2547+ }
2548+
2549+ // Set minIops and maxIopsPerGb because we do not fetch these from DryRun Error, we can also catch invalid volume.
2550+ switch volType {
2551+ case VolumeTypeGP2 , VolumeTypeSC1 , VolumeTypeST1 , VolumeTypeStandard :
2552+ case VolumeTypeIO1 :
2553+ iopsLimits .minIops = io1MinTotalIOPS
2554+ iopsLimits .maxIopsPerGb = io1MaxIOPSPerGB
2555+ case VolumeTypeIO2 :
2556+ iopsLimits .minIops = io2MinTotalIOPS
2557+ iopsLimits .maxIopsPerGb = io2MaxIOPSPerGB
2558+ case VolumeTypeGP3 :
2559+ iopsLimits .minIops = gp3MinTotalIOPS
2560+ iopsLimits .maxIopsPerGb = gp3MaxIOPSPerGB
2561+ default :
2562+ return iopsLimits , fmt .Errorf ("invalid AWS VolumeType %q" , volumeType )
2563+ }
2564+
2565+ if ! useFallBackLimits {
2566+ c .latestIOPSLimits .Set (cacheKey , & iopsLimits )
2567+ }
2568+
2569+ return iopsLimits , nil
2570+ }
2571+
2572+ // Get what the maxIops is from DryRun error message.
2573+ func extractMaxIOPSFromError (errorMsg string , volumeType string ) (int32 , error ) {
2574+ // io1 and gp3 have the same error message but io2 has different one, using by default.
2575+ if volumeType == VolumeTypeIO2 {
2576+ if matches := io2ErrRegex .FindStringSubmatch (errorMsg ); len (matches ) > 1 {
2577+ if val , err := strconv .ParseInt (matches [1 ], 10 , 32 ); err == nil {
2578+ result := val * 1000
2579+ // No real overflow concern here but adding for safety.
2580+ if result > math .MaxInt32 || result < math .MinInt32 {
2581+ return 0 , fmt .Errorf ("maximum IOPS value exceeds maximum value of int32: %d" , val )
2582+ }
2583+ return int32 (result ), nil
2584+ }
2585+ }
2586+ } else {
2587+ if matches := nonIo2ErrRegex .FindStringSubmatch (errorMsg ); len (matches ) > 1 {
2588+ if val , err := strconv .ParseInt (matches [1 ], 10 , 32 ); err == nil {
2589+ return int32 (val ), nil
2590+ }
2591+ }
2592+ }
2593+
2594+ return 0 , fmt .Errorf ("error getting IOPS limit, defaulting to hardcoded values for volume type %s" , volumeType )
2595+ }
0 commit comments