Skip to content

Commit

Permalink
feat(slimfaas): scale down statefulset (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
guillaume-chervet authored Jan 4, 2024
1 parent 3321afa commit f65b8b4
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 73 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
![SlimFaas.png](https://github.com/AxaFrance/SlimFaas/blob/main/documentation/SlimFaas.png)

Why use SlimFaas ?
- Scale to 0 after a period of inactivity
- Scale to 0 after a period of inactivity (work with deployment and statefulset)
- Synchronous HTTP calls
- Asynchronous HTTP calls
- Allows you to limit the number of parallel HTTP requests for each underlying function
Expand Down Expand Up @@ -33,6 +33,8 @@ kubectl apply -f deployment-slimfaas.yml
# Install three instances of fibonacci functions
# fibonacci1, fibonacci2 and fibonacci3
kubectl apply -f deployment-functions.yml
# Install MySql
kubectl apply -f deployment-mysql.yml
# to run Single Page webapp demo (optional)
docker run -p 8000:8000 --rm axaguildev/fibonacci-webapp:latest
```
Expand Down Expand Up @@ -132,6 +134,7 @@ spec:
SlimFaas/ReplicasStartAsSoonAsOneFunctionRetrieveARequest: "false"
SlimFaas/TimeoutSecondBeforeSetReplicasMin: "300"
SlimFaas/NumberParallelRequest : "10"
SlimFaas/DependsOn : "mysql,fibonacci2" # comma separated list of deployment of statefulset names
spec:
serviceAccountName: default
containers:
Expand Down Expand Up @@ -271,6 +274,10 @@ spec:
- Scale down to SlimFaas/ReplicasMin after this period of inactivity in seconds
- SlimFaas/NumberParallelRequest : "10"
- Limit the number of parallel HTTP requests for each underlying function
- SlimFaas/DependsOn : ""
- Comma separated list of deployment names or statefulset names
- Pods will be scaled up only if all pods in this list are in ready state with the minimum number of replicas superior or equal to ReplicasAtStart
- This property is useful if you want to scale up your pods only if your database is ready for example

## Why SlimFaas ?

Expand Down Expand Up @@ -319,11 +326,10 @@ Why .NET ?

## Videos

- French : https://www.youtube.com/watch?v=Lvd6FCuCZPI
- French : https://www.youtube.com/watch?v=Lvd6FCuCZPI
- English: https://www.youtube.com/watch?hxRfvJhWW1w

## What Next ?

1. Different scale down mode depending from configuration and current hour
2. Allow to scale down also statefulset pods
3. Scale up dynamically from SlimFaas
2. Scale up dynamically from SlimFaas
3 changes: 2 additions & 1 deletion demo/deployment-functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ spec:
SlimFaas/ReplicasStartAsSoonAsOneFunctionRetrieveARequest: "false"
SlimFaas/TimeoutSecondBeforeSetReplicasMin: "10"
SlimFaas/NumberParallelRequest : "10"
SlimFaas/DependsOn: "fibonacci2"
spec:
serviceAccountName: default
containers:
Expand Down Expand Up @@ -59,7 +60,6 @@ spec:
SlimFaas/Function: "true"
SlimFaas/ReplicasMin: "0"
SlimFaas/ReplicasAtStart: "1"
SlimFaas/ReplicasStartAsSoonAsOneFunctionRetrieveARequest: "true"
SlimFaas/TimeoutSecondBeforeSetReplicasMin: "10"
SlimFaas/NumberParallelRequest : "10"
spec:
Expand Down Expand Up @@ -106,6 +106,7 @@ spec:
SlimFaas/ReplicasStartAsSoonAsOneFunctionRetrieveARequest: "false"
SlimFaas/TimeoutSecondBeforeSetReplicasMin: "10"
SlimFaas/NumberParallelRequest : "10"
SlimFaas/DependsOn: "mysql"
spec:
serviceAccountName: default
containers:
Expand Down
75 changes: 75 additions & 0 deletions demo/deployment-mysql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: kubernetes.io/basic-auth
stringData:
password: "test1234"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
annotations:
SlimFaas/Function: "true"
SlimFaas/ReplicasMin: "0"
SlimFaas/ReplicasAtStart: "1"
SlimFaas/ReplicasStartAsSoonAsOneFunctionRetrieveARequest: "true"
SlimFaas/TimeoutSecondBeforeSetReplicasMin: "60"
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
resources:
limits:
cpu: 600m
memory: 1024Mi
requests:
cpu: 300m
memory: 512Mi
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
1 change: 1 addition & 0 deletions src/SlimFaas/EnvironmentVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace SlimFaas;

public static class EnvironmentVariables
{

public const string SlimFaasCorsAllowOrigin = "SLIMFAAS_CORS_ALLOW_ORIGIN";
public const string SlimFaasCorsAllowOriginDefault = "*";

Expand Down
131 changes: 97 additions & 34 deletions src/SlimFaas/Kubernetes/KubernetesService.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using DotNext.Collections.Generic;
using k8s;
using k8s.Autorest;
using k8s.Models;

namespace SlimFaas.Kubernetes;

public record ReplicaRequest(string Deployment, string Namespace, int Replicas);
public enum PodType
{
Deployment,
StatefulSet
}

public record ReplicaRequest(string Deployment, string Namespace, int Replicas, PodType PodType);

public record SlimFaasDeploymentInformation(int Replicas, IList<PodInformation> Pods);

public record DeploymentsInformations(IList<DeploymentInformation> Functions, SlimFaasDeploymentInformation SlimFaas);

public record DeploymentInformation(string Deployment, string Namespace, IList<PodInformation> Pods, int Replicas,
int ReplicasAtStart = 1, int ReplicasMin = 0, int TimeoutSecondBeforeSetReplicasMin = 300,
int ReplicasAtStart = 1,
int ReplicasMin = 0,
int TimeoutSecondBeforeSetReplicasMin = 300,
int NumberParallelRequest = 10,
bool ReplicasStartAsSoonAsOneFunctionRetrieveARequest = false);
bool ReplicasStartAsSoonAsOneFunctionRetrieveARequest = false,
PodType PodType = PodType.Deployment,
IList<string>? DependsOn = null);

public record PodInformation(string Name, bool? Started, bool? Ready, string Ip, string DeploymentName);

Expand All @@ -25,6 +36,7 @@ public class KubernetesService : IKubernetesService
private const string ReplicasMin = "SlimFaas/ReplicasMin";
private const string Function = "SlimFaas/Function";
private const string ReplicasAtStart = "SlimFaas/ReplicasAtStart";
private const string DependsOn = "SlimFaas/DependsOn";

private const string ReplicasStartAsSoonAsOneFunctionRetrieveARequest =
"SlimFaas/ReplicasStartAsSoonAsOneFunctionRetrieveARequest";
Expand All @@ -48,10 +60,20 @@ public KubernetesService(ILogger<KubernetesService> logger, bool useKubeConfig)
{
try
{
using k8s.Kubernetes client = new k8s.Kubernetes(_k8SConfig);
using k8s.Kubernetes client = new(_k8SConfig);
string patchString = $"{{\"spec\": {{\"replicas\": {request.Replicas}}}}}";
V1Patch patch = new V1Patch(patchString, V1Patch.PatchType.MergePatch);
await client.PatchNamespacedDeploymentScaleAsync(patch, request.Deployment, request.Namespace);
V1Patch patch = new(patchString, V1Patch.PatchType.MergePatch);
switch (request.PodType)
{
case PodType.Deployment:
await client.PatchNamespacedDeploymentScaleAsync(patch, request.Deployment, request.Namespace);
break;
case PodType.StatefulSet:
await client.PatchNamespacedStatefulSetScaleAsync(patch, request.Deployment, request.Namespace);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
catch (HttpOperationException e)
{
Expand All @@ -68,7 +90,7 @@ public async Task<DeploymentsInformations> ListFunctionsAsync(string kubeNamespa
try
{
IList<DeploymentInformation>? deploymentInformationList = new List<DeploymentInformation>();
using k8s.Kubernetes client = new k8s.Kubernetes(_k8SConfig);
using k8s.Kubernetes client = new(_k8SConfig);
Task<V1DeploymentList>? deploymentListTask = client.ListNamespacedDeploymentAsync(kubeNamespace);
Task<V1PodList>? podListTask = client.ListNamespacedPodAsync(kubeNamespace);
Task<V1StatefulSetList>? statefulSetListTask = client.ListNamespacedStatefulSetAsync(kubeNamespace);
Expand All @@ -85,33 +107,8 @@ public async Task<DeploymentsInformations> ListFunctionsAsync(string kubeNamespa
podList.Where(p => p.Name.StartsWith(deploymentListItem.Metadata.Name)).ToList()))
.FirstOrDefault();

foreach (V1Deployment? deploymentListItem in deploymentList.Items)
{
IDictionary<string, string>? annotations = deploymentListItem.Spec.Template.Metadata.Annotations;
if (annotations == null || !annotations.ContainsKey(Function) ||
annotations[Function].ToLower() != "true")
{
continue;
}

DeploymentInformation deploymentInformation = new DeploymentInformation(
deploymentListItem.Metadata.Name,
kubeNamespace,
podList.Where(p => p.DeploymentName == deploymentListItem.Metadata.Name).ToList(),
deploymentListItem.Spec.Replicas ?? 0,
annotations.ContainsKey(ReplicasAtStart)
? int.Parse(annotations[ReplicasAtStart])
: 1, annotations.ContainsKey(ReplicasMin)
? int.Parse(annotations[ReplicasMin])
: 1, annotations.ContainsKey(TimeoutSecondBeforeSetReplicasMin)
? int.Parse(annotations[TimeoutSecondBeforeSetReplicasMin])
: 300, annotations.ContainsKey(NumberParallelRequest)
? int.Parse(annotations[NumberParallelRequest])
: 10, annotations.ContainsKey(
ReplicasStartAsSoonAsOneFunctionRetrieveARequest) &&
annotations[ReplicasStartAsSoonAsOneFunctionRetrieveARequest].ToLower() == "true");
deploymentInformationList.Add(deploymentInformation);
}
AddDeployments(kubeNamespace, deploymentList, podList, deploymentInformationList);
AddStatefulSets(kubeNamespace, statefulSetList, podList, deploymentInformationList);

return new DeploymentsInformations(deploymentInformationList,
slimFaasDeploymentInformation ?? new SlimFaasDeploymentInformation(1, new List<PodInformation>()));
Expand All @@ -124,6 +121,72 @@ public async Task<DeploymentsInformations> ListFunctionsAsync(string kubeNamespa
}
}

private static void AddDeployments(string kubeNamespace, V1DeploymentList deploymentList, IEnumerable<PodInformation> podList,
IList<DeploymentInformation> deploymentInformationList)
{
foreach (V1Deployment? deploymentListItem in deploymentList.Items)
{
IDictionary<string, string>? annotations = deploymentListItem.Spec.Template.Metadata.Annotations;
if (annotations == null || !annotations.ContainsKey(Function) ||
annotations[Function].ToLower() != "true")
{
continue;
}

DeploymentInformation deploymentInformation = new(
deploymentListItem.Metadata.Name,
kubeNamespace,
podList.Where(p => p.DeploymentName == deploymentListItem.Metadata.Name).ToList(),
deploymentListItem.Spec.Replicas ?? 0,
annotations.ContainsKey(ReplicasAtStart)
? int.Parse(annotations[ReplicasAtStart])
: 1, annotations.ContainsKey(ReplicasMin)
? int.Parse(annotations[ReplicasMin])
: 1, annotations.ContainsKey(TimeoutSecondBeforeSetReplicasMin)
? int.Parse(annotations[TimeoutSecondBeforeSetReplicasMin])
: 300, annotations.ContainsKey(NumberParallelRequest)
? int.Parse(annotations[NumberParallelRequest])
: 10, annotations.ContainsKey(
ReplicasStartAsSoonAsOneFunctionRetrieveARequest) &&
annotations[ReplicasStartAsSoonAsOneFunctionRetrieveARequest].ToLower() == "true", PodType.Deployment,
annotations.ContainsKey(DependsOn) ? annotations[DependsOn].Split(',').ToList() : new List<string>());
deploymentInformationList.Add(deploymentInformation);
}
}

private static void AddStatefulSets(string kubeNamespace, V1StatefulSetList deploymentList, IEnumerable<PodInformation> podList,
IList<DeploymentInformation> deploymentInformationList)
{
foreach (V1StatefulSet? deploymentListItem in deploymentList.Items)
{
IDictionary<string, string>? annotations = deploymentListItem.Spec.Template.Metadata.Annotations;
if (annotations == null || !annotations.ContainsKey(Function) ||
annotations[Function].ToLower() != "true")
{
continue;
}

DeploymentInformation deploymentInformation = new(
deploymentListItem.Metadata.Name,
kubeNamespace,
podList.Where(p => p.DeploymentName == deploymentListItem.Metadata.Name).ToList(),
deploymentListItem.Spec.Replicas ?? 0,
annotations.ContainsKey(ReplicasAtStart)
? int.Parse(annotations[ReplicasAtStart])
: 1, annotations.ContainsKey(ReplicasMin)
? int.Parse(annotations[ReplicasMin])
: 1, annotations.ContainsKey(TimeoutSecondBeforeSetReplicasMin)
? int.Parse(annotations[TimeoutSecondBeforeSetReplicasMin])
: 300, annotations.ContainsKey(NumberParallelRequest)
? int.Parse(annotations[NumberParallelRequest])
: 10, annotations.ContainsKey(
ReplicasStartAsSoonAsOneFunctionRetrieveARequest) &&
annotations[ReplicasStartAsSoonAsOneFunctionRetrieveARequest].ToLower() == "true", PodType.StatefulSet,
annotations.ContainsKey(DependsOn) ? annotations[DependsOn].Split(',').ToList() : new List<string>());
deploymentInformationList.Add(deploymentInformation);
}
}

private static IEnumerable<PodInformation> MapPodInformations(V1PodList v1PodList)
{
foreach (V1Pod? item in v1PodList.Items)
Expand Down
Loading

0 comments on commit f65b8b4

Please sign in to comment.