Skip to content

Commit 79a0645

Browse files
souravbiswassantotamalsahaArnobKumarSaha
authored
Add sharding support for ops-manager (#12)
Signed-off-by: souravbiswassanto <saurov@appscode.com> Signed-off-by: Tamal Saha <tamal@appscode.com> Signed-off-by: Arnob Kumar Saha <arnob@appscode.com> Co-authored-by: Tamal Saha <tamal@appscode.com> Co-authored-by: Arnob Kumar Saha <arnob@appscode.com>
1 parent 3d9675f commit 79a0645

File tree

7 files changed

+182
-63
lines changed

7 files changed

+182
-63
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ endif
5555
### These variables should not need tweaking.
5656
###
5757

58-
SRC_PKGS := cmd crds pkg
58+
SRC_PKGS := cmd crds pkg api
5959
SRC_DIRS := $(SRC_PKGS) # directories which hold app source (not vendored)
6060

6161
DOCKER_PLATFORMS := linux/amd64 linux/arm64

api/v1alpha1/shardconfiguration_types.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ type ShardConfigurationSpec struct {
3232
// +kubebuilder:validation:MinItems=1
3333
Controllers []kmapi.TypedObjectReference `json:"controllers,omitempty"`
3434
// +kubebuilder:validation:MinItems=1
35-
Resources []kmapi.TypeReference `json:"resources,omitempty"`
35+
Resources []ResourceInfo `json:"resources,omitempty"`
36+
}
37+
38+
type ResourceInfo struct {
39+
kmapi.TypeReference `json:",inline"`
40+
ShardKey *string `json:"shardKey,omitempty"`
41+
UseCooperativeShardMigration bool `json:"useCooperativeShardMigration,omitempty"`
3642
}
3743

3844
type ControllerAllocation struct {

api/v1alpha1/zz_generated.deepcopy.go

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

crds/operator.k8s.appscode.com_shardconfigurations.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,15 @@ spec:
6666
type: array
6767
resources:
6868
items:
69-
description: TypeReference represents an object type.
7069
properties:
7170
apiGroup:
7271
type: string
7372
kind:
7473
type: string
74+
shardKey:
75+
type: string
76+
useCooperativeShardMigration:
77+
type: boolean
7578
type: object
7679
minItems: 1
7780
type: array

pkg/controller/hashing.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ func getBetterPartitionCount(members int, load float64) int {
7272
return candidate
7373
}
7474

75-
func newConsistentConfig(members []consistent.Member, shardCount int) *consistent.Consistent {
75+
func newConsistentConfig(shardCount int) *consistent.Consistent {
76+
members := make([]consistent.Member, 0, shardCount)
77+
for i := 0; i < shardCount; i++ {
78+
members = append(members, Member{ID: i})
79+
}
7680
return consistent.New(members, consistent.Config{
7781
PartitionCount: getBetterPartitionCount(shardCount, 1.0),
7882
ReplicationFactor: 1,

pkg/controller/shardconfiguration_controller.go

Lines changed: 63 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"fmt"
2323
"sort"
2424
"strconv"
25-
"strings"
2625
"sync"
2726
"time"
2827

@@ -33,6 +32,7 @@ import (
3332
apierrors "k8s.io/apimachinery/pkg/api/errors"
3433
"k8s.io/apimachinery/pkg/api/meta"
3534
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3636
"k8s.io/apimachinery/pkg/runtime"
3737
"k8s.io/apimachinery/pkg/runtime/schema"
3838
"k8s.io/apimachinery/pkg/types"
@@ -155,11 +155,13 @@ func (r *ShardConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.R
155155
if shardCount <= 0 {
156156
return ctrl.Result{RequeueAfter: time.Second * 2}, nil
157157
}
158-
members := make([]consistent.Member, 0, shardCount)
159-
for i := 0; i < shardCount; i++ {
160-
members = append(members, Member{ID: i})
158+
159+
cc := newConsistentConfig(shardCount)
160+
resourceLists, err := r.d.ServerPreferredResources()
161+
if err != nil && !discovery.IsGroupDiscoveryFailedError(err) {
162+
return ctrl.Result{}, err
161163
}
162-
cc := newConsistentConfig(members, shardCount)
164+
163165
for _, resource := range cfg.Spec.Resources {
164166
if resource.Kind != "" {
165167
mapping, err := r.mapper.RESTMapping(schema.GroupKind{
@@ -175,14 +177,10 @@ func (r *ShardConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.R
175177
return ctrl.Result{}, err
176178
}
177179

178-
if err := r.UpdateShardLabel(ctx, cc, gvk, &cfg); err != nil {
180+
if err := r.UpdateShardLabel(ctx, cc, gvk, &cfg, resource); err != nil {
179181
return ctrl.Result{}, err
180182
}
181183
} else {
182-
resourceLists, err := r.d.ServerPreferredResources()
183-
if err != nil && !discovery.IsGroupDiscoveryFailedError(err) {
184-
return ctrl.Result{}, err
185-
}
186184
for _, resourceList := range resourceLists {
187185
if gv, err := schema.ParseGroupVersion(resourceList.GroupVersion); err == nil && gv.Group == resource.APIGroup {
188186
for _, apiResource := range resourceList.APIResources {
@@ -191,7 +189,7 @@ func (r *ShardConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.R
191189
if err := r.RegisterResourceWatcher(gvk); err != nil {
192190
return ctrl.Result{}, err
193191
}
194-
if err := r.UpdateShardLabel(ctx, cc, gvk, &cfg); err != nil {
192+
if err := r.UpdateShardLabel(ctx, cc, gvk, &cfg, resource); err != nil {
195193
return ctrl.Result{}, err
196194
}
197195
}
@@ -204,30 +202,71 @@ func (r *ShardConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.R
204202
return ctrl.Result{}, nil
205203
}
206204

207-
func (r *ShardConfigurationReconciler) UpdateShardLabel(ctx context.Context, cc *consistent.Consistent, gvk schema.GroupVersionKind, cfg *shardapi.ShardConfiguration) error {
208-
log := log.FromContext(ctx)
205+
func (r *ShardConfigurationReconciler) UpdateShardLabel(ctx context.Context, cc *consistent.Consistent, gvk schema.GroupVersionKind, cfg *shardapi.ShardConfiguration, ri shardapi.ResourceInfo) error {
206+
logger := log.FromContext(ctx)
209207
shardKey := fmt.Sprintf("shard.%s/%s", shardapi.SchemeGroupVersion.Group, cfg.Name)
210-
var list metav1.PartialObjectMetadataList
208+
nextShardKey := fmt.Sprintf("next.%s/%s", shardapi.SchemeGroupVersion.Group, cfg.Name)
209+
210+
var list unstructured.UnstructuredList
211211
list.SetGroupVersionKind(gvk)
212212
err := r.List(ctx, &list)
213213
if err != nil {
214214
return err
215215
}
216+
217+
ifShardKeyLabelNeedsToBeChanged := func(labels map[string]string, shardKey string, member consistent.Member) bool {
218+
return labels[shardKey] != "" && labels[shardKey] != member.String()
219+
}
220+
216221
for _, obj := range list.Items {
217-
m := cc.LocateKey([]byte(fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())))
222+
var key []byte
223+
if ri.ShardKey != nil {
224+
val, found := EvaluateJSONPath(obj.Object, *ri.ShardKey)
225+
if !found {
226+
return fmt.Errorf("failed to extract shard key from %s/%s %s/%s using jsonPath %s", obj.GroupVersionKind().Group, obj.GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName(), *ri.ShardKey)
227+
}
228+
key = []byte(fmt.Sprintf("%s/%s", obj.GetNamespace(), val))
229+
} else {
230+
key = []byte(fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
231+
}
232+
m := cc.LocateKey(key)
233+
changed := false
234+
labels := obj.GetLabels()
235+
if labels == nil {
236+
labels = make(map[string]string)
237+
}
238+
if labels[shardKey] == "" {
239+
labels[shardKey] = m.String()
240+
changed = true
241+
} else if ifShardKeyLabelNeedsToBeChanged(labels, shardKey, m) {
242+
switch ri.UseCooperativeShardMigration {
243+
case true:
244+
if len(cfg.Status.Controllers) > 0 && len(cfg.Status.Controllers[0].Pods) > 0 {
245+
id, err := strconv.Atoi(labels[shardKey])
246+
if err != nil {
247+
return err
248+
}
249+
if id >= len(cfg.Status.Controllers[0].Pods) {
250+
labels[shardKey] = m.String()
251+
} else {
252+
labels[nextShardKey] = m.String()
253+
}
254+
}
255+
case false:
256+
labels[shardKey] = m.String()
257+
}
258+
changed = true
259+
}
218260

219-
if obj.Labels[shardKey] != m.String() {
261+
if changed {
220262
opr, err := controllerutil.CreateOrPatch(ctx, r.Client, &obj, func() error {
221-
if obj.Labels == nil {
222-
obj.Labels = map[string]string{}
223-
}
224-
obj.Labels[shardKey] = m.String()
263+
obj.SetLabels(labels)
225264
return nil
226265
})
227266
if err != nil {
228-
log.Error(err, fmt.Sprintf("failed to update labels for %s/%s %s/%s", obj.GroupVersionKind().Group, obj.GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName()))
267+
logger.Error(err, fmt.Sprintf("failed to update labels for %s/%s %s/%s", obj.GroupVersionKind().Group, obj.GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName()))
229268
} else {
230-
log.Info(fmt.Sprintf("%s/%s %s/%s %s", obj.GroupVersionKind().Group, obj.GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName(), opr))
269+
logger.Info(fmt.Sprintf("%s/%s %s/%s %s", obj.GroupVersionKind().Group, obj.GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName(), opr))
231270
}
232271
}
233272
}
@@ -360,40 +399,8 @@ func ListPods(ctx context.Context, kc client.Client, ref kmapi.TypedObjectRefere
360399
if err != nil {
361400
return nil, err
362401
}
363-
sel, err := metav1.LabelSelectorAsSelector(obj.Spec.Selector)
364-
if err != nil {
365-
return nil, err
366-
}
367-
var list metav1.PartialObjectMetadataList
368-
list.SetGroupVersionKind(schema.GroupVersionKind{
369-
Group: "",
370-
Kind: "Pod",
371-
Version: "v1",
372-
})
373-
err = kc.List(ctx, &list, client.MatchingLabelsSelector{Selector: sel})
374-
if err != nil {
375-
return nil, err
376-
}
377-
pods := make([]string, 0, len(list.Items))
378-
for _, pod := range list.Items {
379-
if metav1.IsControlledBy(&pod, &obj) {
380-
pods = append(pods, pod.Name)
381-
}
382-
}
383-
sort.Slice(pods, func(i, j int) bool {
384-
idx_i := strings.LastIndexByte(pods[i], '-')
385-
idx_j := strings.LastIndexByte(pods[j], '-')
386-
if idx_i == -1 || idx_j == -1 {
387-
return pods[i] < pods[j]
388-
}
389-
oi, err_i := strconv.Atoi(pods[i][idx_i+1:])
390-
oj, err_j := strconv.Atoi(pods[j][idx_j+1:])
391-
if err_i != nil || err_j != nil {
392-
return pods[i] < pods[j]
393-
}
394-
return oi < oj
395-
})
396-
return pods, nil
402+
// Important: Resharding feature is not available for stateful sets
403+
return buildPodList(obj.GetName(), *obj.Spec.Replicas), nil
397404
case "DaemonSet":
398405
var obj apps.DaemonSet
399406
err := kc.Get(ctx, client.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}, &obj)

pkg/controller/utils.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ limitations under the License.
1616

1717
package controller
1818

19-
import "gomodules.xyz/sets"
19+
import (
20+
"fmt"
21+
"strconv"
22+
"strings"
23+
24+
"gomodules.xyz/sets"
25+
)
2026

2127
func getUpdatedPodLists(existing, podLists []string) []string {
2228
if len(existing) > len(podLists) {
@@ -25,6 +31,9 @@ func getUpdatedPodLists(existing, podLists []string) []string {
2531
return handleUpdateOrUpScaling(existing, podLists)
2632
}
2733

34+
// Both the handleDownScaling & handleUpdateOrUpScaling func is to sort the new 'podLists' according to the sequence of 'existing'.
35+
36+
// scaleDown example: existing=[2, 3, 0, 1], podLists=[1, 3]. output will be = [3, 1]. Because 3 appears before 1 in the 'existing' array.
2837
func handleDownScaling(existing, podLists []string) []string {
2938
newPods := make([]string, 0)
3039

@@ -48,6 +57,7 @@ func handleDownScaling(existing, podLists []string) []string {
4857
return newPods
4958
}
5059

60+
// scaleUp example: existing=[1, 3], podLists=[2, 3, 0, 1]. output will be = [1, 3, 2, 0]. Keeping [1,3] as it is. Then appending the remaining ones from 'podLists'.
5161
func handleUpdateOrUpScaling(existing, podLists []string) []string {
5262
newPods := make([]string, len(podLists))
5363

@@ -87,3 +97,68 @@ func getNextAvailableIndex(next int, pods []string) int {
8797
func isReadable(verbs []string) bool {
8898
return sets.NewString(verbs...).HasAll("get", "list", "watch")
8999
}
100+
101+
// EvaluateJSONPath evaluates a simple JSONPath expression on an unstructured object
102+
// Supports paths like ".spec.databaseRef.name"
103+
// Returns the value as a string and true if found, empty string and false otherwise
104+
func EvaluateJSONPath(obj map[string]any, jsonPath string) (string, bool) {
105+
if obj == nil || jsonPath == "" {
106+
return "", false
107+
}
108+
109+
// Remove leading dot if present
110+
jsonPath = strings.TrimPrefix(jsonPath, ".")
111+
112+
// Split the path into parts
113+
parts := strings.Split(jsonPath, ".")
114+
115+
// Navigate through the object structure
116+
current := any(obj)
117+
for i, part := range parts {
118+
if part == "" {
119+
continue
120+
}
121+
122+
// Try to cast current to map[string]any
123+
currentMap, ok := current.(map[string]any)
124+
if !ok {
125+
return "", false
126+
}
127+
128+
// Get the next value
129+
next, exists := currentMap[part]
130+
if !exists {
131+
return "", false
132+
}
133+
134+
// If this is the last part, try to convert to string
135+
if i == len(parts)-1 {
136+
switch v := next.(type) {
137+
case string:
138+
return v, true
139+
case int:
140+
return strconv.Itoa(v), true
141+
case int64:
142+
return strconv.FormatInt(v, 10), true
143+
case float64:
144+
return strconv.FormatFloat(v, 'f', -1, 64), true
145+
case bool:
146+
return strconv.FormatBool(v), true
147+
default:
148+
return fmt.Sprintf("%v", v), true
149+
}
150+
}
151+
152+
current = next
153+
}
154+
155+
return "", false
156+
}
157+
158+
func buildPodList(ctrlName string, replCount int32) []string {
159+
pods := make([]string, replCount)
160+
for c := int32(0); c < replCount; c++ {
161+
pods[c] = fmt.Sprintf("%s-%d", ctrlName, c)
162+
}
163+
return pods
164+
}

0 commit comments

Comments
 (0)