Skip to content

Commit 9f2b9bb

Browse files
✨ persistence (#23)
1 parent 7748b95 commit 9f2b9bb

File tree

12 files changed

+234
-120
lines changed

12 files changed

+234
-120
lines changed

README.md

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ cpuFactor: 1
4646
memoryFactor: 1
4747
logLevel: info
4848
concurrentWorkers: 10
49+
persistence:
50+
enabled: true
4951
```
5052
## Prerequisites
5153
- The metrics server must be deployed in your cluster. Read more about [Metrics Server](https://github.com/kubernetes-sigs/metrics-server). This controller uses the **metrics.k8s.io** extension API group (apis/metrics.k8s.io/v1beta1)
@@ -55,53 +57,48 @@ concurrentWorkers: 10
5557
**kube-reqsizer** has primary custom flags:
5658
5759
```bash
60+
# Enable a annotation filter for pod scraping.
61+
# True will only set requests of controllers of which PODS or NAMESPACE
62+
# have the annotation set to "true".
63+
# If "false", will ignore annotations and work
64+
# on all pods in the cluster unless
65+
# they have "false".
66+
67+
# reqsizer.jatalocks.github.io/optimize=true
68+
# reqsizer.jatalocks.github.io/optimize=false
5869
--annotation-filter bool (default true)
59-
60-
Enable a annotation filter for pod scraping.
61-
Enabling this will ensure that the controller
62-
only sets requests of controllers of which PODS or NAMESPACE
63-
have the annotation set to "true".
64-
If "false", will ignore annotations and work on all pods in the cluster.
65-
66-
# reqsizer.jatalocks.github.io/optimize=true
67-
# reqsizer.jatalocks.github.io/optimize=false
6870

71+
# The sample size to create an average from when reconciling.
6972
--sample-size int (default 1)
7073

71-
The sample size to create an average from when reconciling.
72-
74+
# Minimum seconds between pod restart.
75+
# This ensures the controller will not restart a pod if the minimum time
76+
# has not passed since it has started.
7377
--min-seconds float (default 1)
7478

75-
Minimum seconds between pod restart.
76-
This ensures the controller will not restart a pod if the minimum time
77-
has not passed since it has started.
78-
79+
# Allow controller to reduce/increase requests
7980
--enable-increase (default true)
80-
Enables the controller to increase pod requests
81-
8281
--enable-reduce (default true)
83-
Enables the controller to reduce pod requests
8482

83+
# Min and Max CPU (m) and Memory (Mi) the controller can set a pod request to. 0 is infinite
8584
--max-cpu int (default 0)
86-
Maximum CPU in (m) that the controller can set a pod request to. 0 is infinite
87-
8885
--max-memory int (default 0)
89-
Maximum memory in (Mi) that the controller can set a pod request to. 0 is infinite
90-
9186
--min-cpu int (default 0)
92-
Minimum CPU in (m) that the controller can set a pod request to. 0 is infinite
93-
9487
--min-memory int (default 0)
95-
Minimum memory in (Mi) that the controller can set a pod request to. 0 is infinite
9688

89+
# Multiply requests when reconciling
9790
--cpu-factor float (default 1)
98-
A factor to multiply CPU requests when reconciling.
99-
10091
--memory-factor float (default 1)
101-
A factor to multiply Memory requests when reconciling.
10292

93+
# How many pods to sample in parallel. This may affect the controllers stability.
10394
--concurrent-workers (default 10)
104-
How many pods to sample in parallel. This may affect the controller's stability.
95+
96+
# Persistence using Redis
97+
--enable-persistence (default false)
98+
--redis-host (default "localhost")
99+
--redis-port (default "6379")
100+
--redis-password (default "")
101+
--redis-db (default 0)
105102
```
106103

107104
### Annotations
@@ -140,8 +137,6 @@ reqsizer.jatalocks.github.io/mode=min # Sets the request to the MINIMUM of
140137
## Limitations
141138

142139
- Does not work with CRD controllers (such as Argo Rollouts)
143-
- Does not have persistent cache. Will reset cache on controller restart
144-
145140
# Development
146141
## Getting Started
147142
You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster.

charts/kube-reqsizer/Chart.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ type: application
1313
# This is the chart version. This version number should be incremented each time you make changes
1414
# to the chart and its templates, including the app version.
1515
# Versions are expected to follow Semantic Versioning (https://semver.org/)
16-
version: 0.8.55
16+
version: 0.9.0
17+
dependencies:
18+
- name: redis
19+
condition: persistence.enabled
20+
repository: https://charts.bitnami.com/bitnami
21+
version: 17.4.0

charts/kube-reqsizer/templates/deployment.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ spec:
3636
- --min-memory={{.Values.minMemory}}
3737
- --cpu-factor={{.Values.cpuFactor}}
3838
- --memory-factor={{.Values.memoryFactor}}
39-
- --concurrent-workers={{.Values.concurrentWorkers}}
39+
- --concurrent-workers={{.Values.concurrentWorkers}}
40+
- --enable-persistence={{.Values.persistence.enabled}}
41+
- --redis-host={{.Release.Name}}-redis-master
4042
resources:
4143
{{- toYaml .Values.controllerManager.manager.resources | nindent 10 }}
4244
command:

charts/kube-reqsizer/values.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ cpuFactor: 1
1111
memoryFactor: 1
1212
logLevel: info
1313
concurrentWorkers: 10
14-
14+
persistence:
15+
enabled: true
1516
serviceMonitor: false
1617

1718
controllerManager:

controllers/pod_controller.go

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@ import (
2222
"math"
2323
"time"
2424

25+
"github.com/go-logr/logr"
26+
"github.com/jatalocks/kube-reqsizer/pkg/cache/localcache"
27+
"github.com/jatalocks/kube-reqsizer/pkg/cache/rediscache"
28+
"github.com/jatalocks/kube-reqsizer/types"
2529
corev1 "k8s.io/api/core/v1"
2630
v1 "k8s.io/api/core/v1"
2731
apierrors "k8s.io/apimachinery/pkg/api/errors"
2832
"k8s.io/apimachinery/pkg/api/resource"
33+
"k8s.io/apimachinery/pkg/runtime"
34+
"k8s.io/client-go/kubernetes"
2935
"k8s.io/client-go/tools/cache"
3036

3137
ctrl "sigs.k8s.io/controller-runtime"
@@ -34,13 +40,34 @@ import (
3440

3541
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch
3642

43+
// PodReconciler reconciles a Pod object
44+
type PodReconciler struct {
45+
client.Client
46+
Log logr.Logger
47+
Scheme *runtime.Scheme
48+
ClientSet *kubernetes.Clientset
49+
SampleSize int
50+
EnableAnnotation bool
51+
MinSecondsBetweenPodRestart float64
52+
EnableIncrease bool
53+
EnableReduce bool
54+
MaxMemory int64
55+
MaxCPU int64
56+
MinMemory int64
57+
MinCPU int64
58+
CPUFactor float64
59+
MemoryFactor float64
60+
RedisClient rediscache.RedisClient
61+
EnablePersistence bool
62+
}
63+
3764
const (
3865
operatorAnnotation = "reqsizer.jatalocks.github.io/optimize"
3966
operatorModeAnnotation = "reqsizer.jatalocks.github.io/mode"
4067
)
4168

4269
func cacheKeyFunc(obj interface{}) (string, error) {
43-
return obj.(PodRequests).Name + obj.(PodRequests).Namespace, nil
70+
return obj.(types.PodRequests).Name + "-" + obj.(types.PodRequests).Namespace, nil
4471
}
4572

4673
var cacheStore = cache.NewStore(cacheKeyFunc)
@@ -88,18 +115,31 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
88115
if err != nil {
89116
deploymentName = pod.Name
90117
}
91-
SumPodRequest := PodRequests{Name: deploymentName, Namespace: pod.Namespace, ContainerRequests: []ContainerRequests{}}
118+
SumPodRequest := types.PodRequests{Name: deploymentName, Namespace: pod.Namespace, ContainerRequests: []types.ContainerRequests{}}
92119

93120
SumPodRequest.ContainerRequests = PodUsageData.ContainerRequests
94-
95-
LatestPodRequest, err := fetchFromCache(cacheStore, deploymentName+pod.Namespace)
121+
var LatestPodRequest types.PodRequests
122+
if r.EnablePersistence {
123+
LatestPodRequest, err = r.RedisClient.FetchFromCache(deploymentName + "-" + pod.Namespace)
124+
} else {
125+
LatestPodRequest, err = localcache.FetchFromCache(cacheStore, deploymentName+"-"+pod.Namespace)
126+
}
96127
if err != nil {
97128
SumPodRequest.Sample = 0
98129
log.Info(fmt.Sprint("Adding cache sample ", SumPodRequest.Sample))
99-
addToCache(cacheStore, SumPodRequest)
100-
log.Info(fmt.Sprint("Items in Cache: ", len(cacheStore.List())))
130+
if r.EnablePersistence {
131+
r.RedisClient.AddToCache(SumPodRequest)
132+
log.Info(fmt.Sprint("Items in Cache: ", r.RedisClient.CacheSize()))
133+
} else {
134+
localcache.AddToCache(cacheStore, SumPodRequest)
135+
log.Info(fmt.Sprint("Items in Cache: ", len(cacheStore.List())))
136+
}
101137
} else {
102-
log.Info(fmt.Sprint("Items in Cache: ", len(cacheStore.List())))
138+
if r.EnablePersistence {
139+
log.Info(fmt.Sprint("Items in Cache: ", r.RedisClient.CacheSize()))
140+
} else {
141+
log.Info(fmt.Sprint("Items in Cache: ", len(cacheStore.List())))
142+
}
103143
SumPodRequest.Sample = LatestPodRequest.Sample + 1
104144

105145
log.Info(fmt.Sprint("Updating cache sample ", SumPodRequest.Sample))
@@ -128,19 +168,27 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
128168
}
129169
}
130170

131-
if err := deleteFromCache(cacheStore, LatestPodRequest); err != nil {
132-
log.Error(err, err.Error())
133-
}
134-
135-
if err = addToCache(cacheStore, SumPodRequest); err != nil {
136-
log.Error(err, err.Error())
171+
if r.EnablePersistence {
172+
if err := r.RedisClient.DeleteFromCache(LatestPodRequest); err != nil {
173+
log.Error(err, err.Error())
174+
}
175+
if err = r.RedisClient.AddToCache(SumPodRequest); err != nil {
176+
log.Error(err, err.Error())
177+
}
178+
} else {
179+
if err := localcache.DeleteFromCache(cacheStore, LatestPodRequest); err != nil {
180+
log.Error(err, err.Error())
181+
}
182+
if err = localcache.AddToCache(cacheStore, SumPodRequest); err != nil {
183+
log.Error(err, err.Error())
184+
}
137185
}
138186
}
139187
log.Info(fmt.Sprint(SumPodRequest))
140188
if (SumPodRequest.Sample >= r.SampleSize) && r.MinimumUptimeOfPodInParent(pod, ctx) {
141189
log.Info("Sample Size and Minimum Time have been reached")
142190
PodChange := false
143-
Requests := []NewContainerRequests{}
191+
Requests := []types.NewContainerRequests{}
144192
for _, c := range SumPodRequest.ContainerRequests {
145193
AverageUsageCPU := c.CPU / int64(SumPodRequest.Sample)
146194
AverageUsageMemory := c.Memory / int64(SumPodRequest.Sample)
@@ -195,7 +243,7 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
195243
}
196244
}
197245

198-
Requests = append(Requests, NewContainerRequests{Name: c.Name, Requests: pod.Spec.Containers[i].Resources})
246+
Requests = append(Requests, types.NewContainerRequests{Name: c.Name, Requests: pod.Spec.Containers[i].Resources})
199247
}
200248
}
201249
}
@@ -222,9 +270,14 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
222270

223271
}
224272

225-
err := deleteFromCache(cacheStore, SumPodRequest)
226-
if err != nil {
227-
log.Error(err, err.Error())
273+
if r.EnablePersistence {
274+
if err := r.RedisClient.DeleteFromCache(SumPodRequest); err != nil {
275+
log.Error(err, err.Error())
276+
}
277+
} else {
278+
if err := localcache.DeleteFromCache(cacheStore, LatestPodRequest); err != nil {
279+
log.Error(err, err.Error())
280+
}
228281
}
229282
}
230283
}

controllers/pod_controller_functions.go

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ import (
99
"time"
1010
"unicode/utf8"
1111

12+
"github.com/jatalocks/kube-reqsizer/types"
1213
"github.com/labstack/gommon/log"
1314
corev1 "k8s.io/api/core/v1"
1415
v1 "k8s.io/api/core/v1"
1516
apierrors "k8s.io/apimachinery/pkg/api/errors"
1617
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17-
"k8s.io/client-go/tools/cache"
18-
"k8s.io/klog"
1918
ctrl "sigs.k8s.io/controller-runtime"
2019
"sigs.k8s.io/controller-runtime/pkg/client"
2120
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -82,7 +81,7 @@ func (r *PodReconciler) UpdateKubeObject(pod client.Object, ctx context.Context)
8281
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
8382
}
8483

85-
func UpdatePodController(podspec *corev1.PodSpec, Requests []NewContainerRequests, ctx context.Context) {
84+
func UpdatePodController(podspec *corev1.PodSpec, Requests []types.NewContainerRequests, ctx context.Context) {
8685
for _, podContainer := range Requests {
8786
for i, depContainer := range podspec.Containers {
8887
if depContainer.Name == podContainer.Name {
@@ -149,8 +148,8 @@ func (r *PodReconciler) ValidateMemory(currentMemory, AverageUsageMemory int64)
149148
return false
150149
}
151150

152-
func GetPodRequests(pod corev1.Pod) PodRequests {
153-
containerData := []ContainerRequests{}
151+
func GetPodRequests(pod corev1.Pod) types.PodRequests {
152+
containerData := []types.ContainerRequests{}
154153
for _, c := range pod.Spec.Containers {
155154
nanoCores, _ := strconv.Atoi(RemoveLastChar(c.Resources.Requests.Cpu().String()))
156155
memString := c.Resources.Requests.Memory().String()
@@ -166,49 +165,22 @@ func GetPodRequests(pod corev1.Pod) PodRequests {
166165
giMemory, _ := strconv.Atoi(strings.ReplaceAll(memString, "Gi", ""))
167166
miMemory = giMemory / 1000
168167
}
169-
containerData = append(containerData, ContainerRequests{Name: c.Name, CPU: int64(nanoCores), Memory: int64(miMemory)})
168+
containerData = append(containerData, types.ContainerRequests{Name: c.Name, CPU: int64(nanoCores), Memory: int64(miMemory)})
170169
}
171-
return PodRequests{pod.Name, pod.Namespace, containerData, 0}
170+
return types.PodRequests{pod.Name, pod.Namespace, containerData, 0}
172171
}
173172

174-
func addToCache(cacheStore cache.Store, object PodRequests) error {
175-
err := cacheStore.Add(object)
176-
if err != nil {
177-
klog.Errorf("failed to add key value to cache error", err)
178-
return err
179-
}
180-
return nil
181-
}
182-
183-
func fetchFromCache(cacheStore cache.Store, key string) (PodRequests, error) {
184-
obj, exists, err := cacheStore.GetByKey(key)
185-
if err != nil {
186-
// klog.Errorf("failed to add key value to cache error", err)
187-
return PodRequests{}, err
188-
}
189-
if !exists {
190-
// klog.Errorf("object does not exist in the cache")
191-
err = errors.New("object does not exist in the cache")
192-
return PodRequests{}, err
193-
}
194-
return obj.(PodRequests), nil
195-
}
196-
197-
func deleteFromCache(cacheStore cache.Store, object PodRequests) error {
198-
return cacheStore.Delete(object)
199-
}
200-
201-
func GeneratePodRequestsObjectFromRestData(restData []byte) PodRequests {
173+
func GeneratePodRequestsObjectFromRestData(restData []byte) types.PodRequests {
202174
s := restData[:]
203-
data := PodMetricsRestData{}
175+
data := types.PodMetricsRestData{}
204176
json.Unmarshal([]byte(s), &data)
205-
containerData := []ContainerRequests{}
177+
containerData := []types.ContainerRequests{}
206178
for _, c := range data.Containers {
207179
nanoCores, _ := strconv.Atoi(RemoveLastChar(c.Usage.CPU))
208180
kiMemory, _ := strconv.Atoi(strings.ReplaceAll(c.Usage.Memory, "Ki", ""))
209-
containerData = append(containerData, ContainerRequests{Name: c.Name, CPU: int64(nanoCores / 1000000), Memory: int64(kiMemory / 1000)})
181+
containerData = append(containerData, types.ContainerRequests{Name: c.Name, CPU: int64(nanoCores / 1000000), Memory: int64(kiMemory / 1000)})
210182
}
211-
return PodRequests{data.Metadata.Name, data.Metadata.Namespace, containerData, 0}
183+
return types.PodRequests{data.Metadata.Name, data.Metadata.Namespace, containerData, 0}
212184
}
213185

214186
func (r *PodReconciler) MinimumUptimeOfPodInParent(pod corev1.Pod, ctx context.Context) bool {
@@ -252,7 +224,6 @@ func (r *PodReconciler) GetPodParentKind(pod corev1.Pod, ctx context.Context) (e
252224
deployment, err := r.ClientSet.AppsV1().DaemonSets(pod.Namespace).Get(ctx, pod.OwnerReferences[0].Kind, metav1.GetOptions{})
253225
return err, &deployment.Spec.Template.Spec, deployment, deployment.Name
254226
case "StatefulSet":
255-
log.Info("STATEFUL SET HAHAHHAHA")
256227
deployment, err := r.ClientSet.AppsV1().StatefulSets(pod.Namespace).Get(ctx, pod.OwnerReferences[0].Kind, metav1.GetOptions{})
257228
return err, &deployment.Spec.Template.Spec, deployment, deployment.Name
258229
default:

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ require (
2929
github.com/go-openapi/jsonpointer v0.19.5 // indirect
3030
github.com/go-openapi/jsonreference v0.19.5 // indirect
3131
github.com/go-openapi/swag v0.19.14 // indirect
32+
github.com/go-redis/redis v6.15.9+incompatible // indirect
3233
github.com/gogo/protobuf v1.3.2 // indirect
3334
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
3435
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5F
148148
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
149149
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
150150
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
151+
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
152+
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
151153
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
152154
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
153155
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=

0 commit comments

Comments
 (0)