Skip to content

Commit 8b8e400

Browse files
authored
Add support for maxUnavailable (#909)
* Add support for maxUnavailable in the Spec for Kubernetes 1.35 and up. * Make PodDisruptionBudget follow the MaxUnavailable changes * Set the maxUnavailable to 100% of the largest rack
1 parent 023f91f commit 8b8e400

8 files changed

Lines changed: 205 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Changelog for Cass Operator, new PRs should update the `main / unreleased` secti
1111

1212
## unreleased
1313

14+
* [FEATURE] [#893](https://github.com/k8ssandra/cass-operator/issues/893) Add support for maxUnavailable (Kubernetes 1.35 and up). This allows to make changes to the Cassandra pods in parallel, thus speeding up changes in larger clusters. Allows integer or percentage setting, but will never target more than one rack at a time.
1415
* [ENHANCEMENT] [#888](https://github.com/k8ssandra/cass-operator/issues/888) Add new metrics around all calls to the mgmt-api. This allows to track if some calls are taking longer to execute than expected.
1516

1617
## v1.29.1

apis/cassandra/v1beta1/cassandradatacenter_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
corev1 "k8s.io/api/core/v1"
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/types"
17+
"k8s.io/apimachinery/pkg/util/intstr"
1718
"k8s.io/apimachinery/pkg/util/validation"
1819
)
1920

@@ -281,6 +282,11 @@ type CassandraDatacenterSpec struct {
281282
// Setting to 0 might cause multiple Cassandra pods to restart at the same time despite PodDisruptionBudget settings.
282283
MinReadySeconds *int32 `json:"minReadySeconds,omitempty"`
283284

285+
// MaxUnavailable sets the maximum number of rack pods that can be modified simultaneously during an update. This can at most target a single rack, so values higher than rack size will have no effect. Requires Kubernetes 1.35 or higher. Setting percentage will
286+
// calculate against single rack's percentage of pods, not the entire datacenter.
287+
// +kubebuilder:validation:XIntOrString
288+
MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"`
289+
284290
// ReadOnlyRootFilesystem makes the cassandra container to be run with a read-only root filesystem. This is enabled by default when using OSS Cassandra 4.1.0 and or newer, DSE 6.8 and newer (from datastax/dse-mgmtapi-6_8 repository) or HCD.
285291
// If serverImage override is used, this setting defaults to false.
286292
ReadOnlyRootFilesystem *bool `json:"readOnlyRootFilesystem,omitempty"`

apis/cassandra/v1beta1/zz_generated.deepcopy.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/cassandra.datastax.com_cassandradatacenters.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,14 @@ spec:
350350
- serverSecretName
351351
type: object
352352
type: object
353+
maxUnavailable:
354+
anyOf:
355+
- type: integer
356+
- type: string
357+
description: |-
358+
MaxUnavailable sets the maximum number of rack pods that can be modified simultaneously during an update. This can at most target a single rack, so values higher than rack size will have no effect. Requires Kubernetes 1.35 or higher. Setting percentage will
359+
calculate against single rack's percentage of pods, not the entire datacenter.
360+
x-kubernetes-int-or-string: true
353361
minReadySeconds:
354362
description: |-
355363
MinReadySeconds sets the minimum number of seconds for which a newly created pod should be ready without any of its containers crashing, for it to be considered available. Defaults to 5 seconds and is set in the StatefulSet spec.

pkg/reconciliation/construct_statefulset.go

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -158,21 +158,8 @@ func newStatefulSetForCassandraDatacenter(
158158
result.Spec.ServiceName = sts.Spec.ServiceName
159159
}
160160

161-
if dc.Spec.CanaryUpgrade {
162-
var partition int32
163-
if dc.Spec.CanaryUpgradeCount == 0 || dc.Spec.CanaryUpgradeCount > replicaCountInt32 {
164-
partition = replicaCountInt32
165-
} else {
166-
partition = replicaCountInt32 - dc.Spec.CanaryUpgradeCount
167-
}
168-
169-
strategy := appsv1.StatefulSetUpdateStrategy{
170-
Type: appsv1.RollingUpdateStatefulSetStrategyType,
171-
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
172-
Partition: &partition,
173-
},
174-
}
175-
result.Spec.UpdateStrategy = strategy
161+
if strategy := buildStatefulSetUpdateStrategy(dc, replicaCountInt32); strategy != nil {
162+
result.Spec.UpdateStrategy = *strategy
176163
}
177164

178165
if dc.Spec.MinReadySeconds != nil {
@@ -185,6 +172,31 @@ func newStatefulSetForCassandraDatacenter(
185172
return result, nil
186173
}
187174

175+
func buildStatefulSetUpdateStrategy(dc *api.CassandraDatacenter, replicaCount int32) *appsv1.StatefulSetUpdateStrategy {
176+
if !dc.Spec.CanaryUpgrade && dc.Spec.MaxUnavailable == nil {
177+
return nil
178+
}
179+
180+
rollingUpdate := &appsv1.RollingUpdateStatefulSetStrategy{
181+
MaxUnavailable: dc.Spec.MaxUnavailable,
182+
}
183+
184+
if dc.Spec.CanaryUpgrade {
185+
var partition int32
186+
if dc.Spec.CanaryUpgradeCount == 0 || dc.Spec.CanaryUpgradeCount > replicaCount {
187+
partition = replicaCount
188+
} else {
189+
partition = replicaCount - dc.Spec.CanaryUpgradeCount
190+
}
191+
rollingUpdate.Partition = &partition
192+
}
193+
194+
return &appsv1.StatefulSetUpdateStrategy{
195+
Type: appsv1.RollingUpdateStatefulSetStrategyType,
196+
RollingUpdate: rollingUpdate,
197+
}
198+
}
199+
188200
func legacyInternodeMount(dc *api.CassandraDatacenter, sts *appsv1.StatefulSet) bool {
189201
if serverconfig.LegacyInternodeEnabled(dc) {
190202
return true

pkg/reconciliation/construct_statefulset_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/stretchr/testify/require"
1515
appsv1 "k8s.io/api/apps/v1"
1616
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/util/intstr"
1718
"k8s.io/utils/ptr"
1819

1920
api "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1"
@@ -770,6 +771,95 @@ func TestMinReadySecondsChange(t *testing.T) {
770771
assert.Equal(int32(10), sts.Spec.MinReadySeconds)
771772
}
772773

774+
func TestMaxUnavailableChange(t *testing.T) {
775+
tests := []struct {
776+
name string
777+
maxUnavailable intstr.IntOrString
778+
expectedRolling *appsv1.RollingUpdateStatefulSetStrategy
779+
}{
780+
{
781+
name: "integer",
782+
maxUnavailable: intstr.FromInt32(1),
783+
expectedRolling: &appsv1.RollingUpdateStatefulSetStrategy{
784+
MaxUnavailable: ptr.To(intstr.FromInt32(1)),
785+
},
786+
},
787+
{
788+
name: "percentage",
789+
maxUnavailable: intstr.Parse("25%"),
790+
expectedRolling: &appsv1.RollingUpdateStatefulSetStrategy{
791+
MaxUnavailable: ptr.To(intstr.Parse("25%")),
792+
},
793+
},
794+
}
795+
796+
for _, tt := range tests {
797+
t.Run(tt.name, func(t *testing.T) {
798+
dc := &api.CassandraDatacenter{
799+
Spec: api.CassandraDatacenterSpec{
800+
ClusterName: "test",
801+
ServerType: "cassandra",
802+
ServerVersion: "4.0.7",
803+
StorageConfig: api.StorageConfig{
804+
CassandraDataVolumeClaimSpec: &corev1.PersistentVolumeClaimSpec{},
805+
},
806+
Racks: []api.Rack{
807+
{
808+
Name: "r1",
809+
},
810+
},
811+
PodTemplateSpec: &corev1.PodTemplateSpec{},
812+
MaxUnavailable: ptr.To(tt.maxUnavailable),
813+
},
814+
}
815+
816+
sts, err := newStatefulSetForCassandraDatacenter(nil, dc.Spec.Racks[0].Name, dc, 3, imageRegistry)
817+
require.NoError(t, err, "failed to build statefulset")
818+
819+
expectedStrategy := appsv1.StatefulSetUpdateStrategy{
820+
Type: appsv1.RollingUpdateStatefulSetStrategyType,
821+
RollingUpdate: tt.expectedRolling,
822+
}
823+
assert.Equal(t, expectedStrategy, sts.Spec.UpdateStrategy)
824+
})
825+
}
826+
}
827+
828+
func TestMaxUnavailableMergedWithCanaryUpgrade(t *testing.T) {
829+
dc := &api.CassandraDatacenter{
830+
Spec: api.CassandraDatacenterSpec{
831+
ClusterName: "test",
832+
ServerType: "cassandra",
833+
ServerVersion: "4.0.7",
834+
CanaryUpgrade: true,
835+
CanaryUpgradeCount: 1,
836+
MaxUnavailable: ptr.To(intstr.Parse("25%")),
837+
StorageConfig: api.StorageConfig{
838+
CassandraDataVolumeClaimSpec: &corev1.PersistentVolumeClaimSpec{},
839+
},
840+
Racks: []api.Rack{
841+
{
842+
Name: "r1",
843+
},
844+
},
845+
PodTemplateSpec: &corev1.PodTemplateSpec{},
846+
},
847+
}
848+
849+
sts, err := newStatefulSetForCassandraDatacenter(nil, dc.Spec.Racks[0].Name, dc, 3, imageRegistry)
850+
require.NoError(t, err, "failed to build statefulset")
851+
852+
expectedStrategy := appsv1.StatefulSetUpdateStrategy{
853+
Type: appsv1.RollingUpdateStatefulSetStrategyType,
854+
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
855+
Partition: ptr.To(int32(2)),
856+
MaxUnavailable: ptr.To(intstr.Parse("25%")),
857+
},
858+
}
859+
860+
assert.Equal(t, expectedStrategy, sts.Spec.UpdateStrategy)
861+
}
862+
773863
func TestAddManagementApiServerSecurity(t *testing.T) {
774864
require := require.New(t)
775865
dc := &api.CassandraDatacenter{

pkg/reconciliation/constructor.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ import (
2323
// newPodDisruptionBudgetForDatacenter creates a PodDisruptionBudget object for the Datacenter
2424
func newPodDisruptionBudgetForDatacenter(dc *api.CassandraDatacenter) *policyv1.PodDisruptionBudget {
2525
minAvailable := intstr.FromInt(int(dc.Spec.Size - 1))
26+
27+
if dc.Spec.MaxUnavailable != nil {
28+
racks := dc.GetRacks()
29+
rackNodeCounts := api.SplitRacks(int(dc.Spec.Size), len(racks))
30+
maxRackNodeCount := 0
31+
for _, rackNodeCount := range rackNodeCounts {
32+
if rackNodeCount > maxRackNodeCount {
33+
maxRackNodeCount = rackNodeCount
34+
}
35+
}
36+
37+
if maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(dc.Spec.MaxUnavailable, maxRackNodeCount, true); err == nil {
38+
if maxUnavailable > maxRackNodeCount {
39+
maxUnavailable = maxRackNodeCount
40+
}
41+
calculatedMinAvailable := int(dc.Spec.Size) - maxUnavailable
42+
minAvailable = intstr.FromInt(calculatedMinAvailable)
43+
}
44+
// If err was not nil, we'll stick to the original minAvailable of size-1
45+
}
46+
2647
labels := dc.GetDatacenterLabels()
2748
oplabels.AddOperatorLabels(labels, dc)
2849
selectorLabels := dc.GetDatacenterLabels()

pkg/reconciliation/constructor_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
api "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1"
77
"github.com/stretchr/testify/assert"
88
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/util/intstr"
10+
"k8s.io/utils/ptr"
911
)
1012

1113
func TestPodDisruptionBudget(t *testing.T) {
@@ -29,3 +31,47 @@ func TestPodDisruptionBudget(t *testing.T) {
2931
assert.Equal("dc1", pdb.Spec.Selector.MatchLabels["cassandra.datastax.com/datacenter"])
3032
assert.Equal(pdb.Spec.MinAvailable.IntVal, dc.Spec.Size-1)
3133
}
34+
35+
func TestPodDisruptionBudgetIntMaxUnavailable(t *testing.T) {
36+
assert := assert.New(t)
37+
38+
dc := &api.CassandraDatacenter{
39+
ObjectMeta: metav1.ObjectMeta{
40+
Name: "dc1",
41+
Namespace: "test",
42+
},
43+
Spec: api.CassandraDatacenterSpec{
44+
Size: 6,
45+
MaxUnavailable: ptr.To(intstr.FromInt(2)),
46+
},
47+
}
48+
49+
pdb := newPodDisruptionBudgetForDatacenter(dc)
50+
assert.Equal(int32(4), pdb.Spec.MinAvailable.IntVal)
51+
}
52+
53+
func TestPodDisruptionBudgetPercentageMaxUnavailable(t *testing.T) {
54+
assert := assert.New(t)
55+
56+
dc := &api.CassandraDatacenter{
57+
ObjectMeta: metav1.ObjectMeta{
58+
Name: "dc1",
59+
Namespace: "test",
60+
},
61+
Spec: api.CassandraDatacenterSpec{
62+
Size: 6,
63+
Racks: []api.Rack{
64+
{Name: "rack1"},
65+
{Name: "rack2"},
66+
},
67+
MaxUnavailable: ptr.To(intstr.Parse("50%")),
68+
},
69+
}
70+
71+
pdb := newPodDisruptionBudgetForDatacenter(dc)
72+
assert.Equal(int32(4), pdb.Spec.MinAvailable.IntVal) // This was roundup
73+
74+
dc.Spec.MaxUnavailable = ptr.To(intstr.Parse("100%"))
75+
pdb = newPodDisruptionBudgetForDatacenter(dc)
76+
assert.Equal(int32(3), pdb.Spec.MinAvailable.IntVal)
77+
}

0 commit comments

Comments
 (0)