diff --git a/README.md b/README.md index 9a2e3333..f7225c37 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 ``` @@ -319,11 +321,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 diff --git a/demo/deployment-mysql.yml b/demo/deployment-mysql.yml new file mode 100644 index 00000000..9a6b4b3a --- /dev/null +++ b/demo/deployment-mysql.yml @@ -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 diff --git a/src/SlimFaas/Kubernetes/KubernetesService.cs b/src/SlimFaas/Kubernetes/KubernetesService.cs index 62622fbe..8c01903b 100644 --- a/src/SlimFaas/Kubernetes/KubernetesService.cs +++ b/src/SlimFaas/Kubernetes/KubernetesService.cs @@ -6,7 +6,13 @@ 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 Pods); @@ -15,7 +21,7 @@ public record DeploymentsInformations(IList Functions, Sl public record DeploymentInformation(string Deployment, string Namespace, IList Pods, int Replicas, int ReplicasAtStart = 1, int ReplicasMin = 0, int TimeoutSecondBeforeSetReplicasMin = 300, int NumberParallelRequest = 10, - bool ReplicasStartAsSoonAsOneFunctionRetrieveARequest = false); + bool ReplicasStartAsSoonAsOneFunctionRetrieveARequest = false, PodType PodType = PodType.Deployment); public record PodInformation(string Name, bool? Started, bool? Ready, string Ip, string DeploymentName); @@ -48,10 +54,20 @@ public KubernetesService(ILogger 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) { @@ -68,7 +84,7 @@ public async Task ListFunctionsAsync(string kubeNamespa try { IList? deploymentInformationList = new List(); - using k8s.Kubernetes client = new k8s.Kubernetes(_k8SConfig); + using k8s.Kubernetes client = new(_k8SConfig); Task? deploymentListTask = client.ListNamespacedDeploymentAsync(kubeNamespace); Task? podListTask = client.ListNamespacedPodAsync(kubeNamespace); Task? statefulSetListTask = client.ListNamespacedStatefulSetAsync(kubeNamespace); @@ -85,33 +101,8 @@ public async Task ListFunctionsAsync(string kubeNamespa podList.Where(p => p.Name.StartsWith(deploymentListItem.Metadata.Name)).ToList())) .FirstOrDefault(); - foreach (V1Deployment? deploymentListItem in deploymentList.Items) - { - IDictionary? 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())); @@ -124,6 +115,70 @@ public async Task ListFunctionsAsync(string kubeNamespa } } + private static void AddDeployments(string kubeNamespace, V1DeploymentList deploymentList, IEnumerable podList, + IList deploymentInformationList) + { + foreach (V1Deployment? deploymentListItem in deploymentList.Items) + { + IDictionary? 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"); + deploymentInformationList.Add(deploymentInformation); + } + } + + private static void AddStatefulSets(string kubeNamespace, V1StatefulSetList deploymentList, IEnumerable podList, + IList deploymentInformationList) + { + foreach (V1StatefulSet? deploymentListItem in deploymentList.Items) + { + IDictionary? 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); + deploymentInformationList.Add(deploymentInformation); + } + } + private static IEnumerable MapPodInformations(V1PodList v1PodList) { foreach (V1Pod? item in v1PodList.Items) diff --git a/src/SlimFaas/ReplicasService.cs b/src/SlimFaas/ReplicasService.cs index f9a5b813..8999e0cb 100644 --- a/src/SlimFaas/ReplicasService.cs +++ b/src/SlimFaas/ReplicasService.cs @@ -84,7 +84,8 @@ public async Task CheckScaleAsync(string kubeNamespace) Task task = kubernetesService.ScaleAsync(new ReplicaRequest( Replicas: deploymentInformation.ReplicasMin, Deployment: deploymentInformation.Deployment, - Namespace: kubeNamespace + Namespace: kubeNamespace, + PodType: deploymentInformation.PodType )); tasks.Add(task); @@ -94,7 +95,8 @@ public async Task CheckScaleAsync(string kubeNamespace) Task task = kubernetesService.ScaleAsync(new ReplicaRequest( Replicas: deploymentInformation.ReplicasAtStart, Deployment: deploymentInformation.Deployment, - Namespace: kubeNamespace + Namespace: kubeNamespace, + PodType: deploymentInformation.PodType )); tasks.Add(task); @@ -106,7 +108,7 @@ public async Task CheckScaleAsync(string kubeNamespace) return; } - List updatedFunctions = new List(); + List updatedFunctions = new(); ReplicaRequest?[] replicaRequests = await Task.WhenAll(tasks); foreach (DeploymentInformation function in Deployments.Functions) { diff --git a/tests/SlimFaas.Tests/ReplicasScaleWorkerShould.cs b/tests/SlimFaas.Tests/ReplicasScaleWorkerShould.cs index e850074b..ef38b13a 100644 --- a/tests/SlimFaas.Tests/ReplicasScaleWorkerShould.cs +++ b/tests/SlimFaas.Tests/ReplicasScaleWorkerShould.cs @@ -21,9 +21,9 @@ public async Task ScaleFunctionUpAndDown(DeploymentsInformations deploymentsInfo masterService.Setup(ms => ms.IsMaster).Returns(true); kubernetesService.Setup(k => k.ListFunctionsAsync(It.IsAny())).ReturnsAsync(deploymentsInformations); - ReplicaRequest scaleRequestFibonacci1 = new ReplicaRequest("fibonacci1", "default", 0); + ReplicaRequest scaleRequestFibonacci1 = new ReplicaRequest("fibonacci1", "default", 0, PodType.Deployment); kubernetesService.Setup(k => k.ScaleAsync(scaleRequestFibonacci1)).ReturnsAsync(scaleRequestFibonacci1); - ReplicaRequest scaleRequestFibonacci2 = new ReplicaRequest("fibonacci2", "default", 1); + ReplicaRequest scaleRequestFibonacci2 = new ReplicaRequest("fibonacci2", "default", 1, PodType.Deployment); kubernetesService.Setup(k => k.ScaleAsync(scaleRequestFibonacci2)).ReturnsAsync(scaleRequestFibonacci2); await replicasService.SyncDeploymentsAsync("default");