Use-case: Dapr
Autoscaling Based on Number of Service Invocations
In this example we will set up a microservice architecture using Dapr middleware. There will be two microservices:
one written in Node.js called nodeapp
and one written in Python called pythonapp
. These services are based on an
upstream example,
where the Python app calls the Node app using the service invocation pattern.
Both workloads run daprd
in a sidecar container, which also exposes metrics. We have modified the daprd
and its
mutating webhook (dapr-sidecar-injector
) to push metrics to our OTEL collector. These metrics use OpenCensus,
so we need to configure the OTEL collector to accept metrics through the opencensus
receiver.
Setup
Any Kubernetes cluster will work for this setup:
k3d cluster create dapr-demo -p "8080:31222@server:0"
Setup Dapr on the Kubernetes cluster (dapr
cli is needed):
arch -arm64 brew install dapr/tap/dapr-cli
dapr init -k --dev
dapr status -k
Apply the patch so that our version of Dapr is used:
# our tweaked version, until https://github.com/dapr/dapr/issues/7225 is done
kubectl set env deployments.apps -n dapr-system dapr-sidecar-injector SIDECAR_IMAGE=docker.io/jkremser/dapr:test SIDECAR_IMAGE_PULL_POLICY=Always
kubectl set image deploy/dapr-sidecar-injector -n dapr-system dapr-sidecar-injector=jkremser/dapr-injector:test
kubectl rollout status -n dapr-system deploy/dapr-sidecar-injector
Deploy this scaler and OTEL collector that forwards one whitelisted metric:
cat <<VALUES | helm upgrade -i kedify-otel oci://ghcr.io/kedify/charts/otel-add-on --version=v0.0.4 -f -
opentelemetry-collector:
alternateConfig:
processors:
filter/ottl:
error_mode: ignore
metrics:
metric: # drop all other metrics that are not whitelisted here
- |
name != "runtime/service_invocation/req_recv_total"
and instrumentation_scope.attributes["app_id"] != "nodeapp"
and instrumentation_scope.attributes["src_app_id"] != "pythonapp"
service:
pipelines:
metrics:
processors: [filter/ottl]
VALUES
Deploy two demo apps and patch them so that they are able to push the metrics to collector:
kubectl apply -f https://raw.githubusercontent.com/dapr/quickstarts/refs/tags/v1.14.0/tutorials/hello-kubernetes/deploy/node.yaml
kubectl apply -f https://raw.githubusercontent.com/dapr/quickstarts/refs/tags/v1.14.0/tutorials/hello-kubernetes/deploy/python.yaml
kubectl patch svc nodeapp --type=merge -p '{"spec":{"type": "NodePort","ports":[{"nodePort": 31222, "port":80, "targetPort":3000}]}}'
kubectl patch deployments.apps pythonapp nodeapp --type=merge -p '{"spec":{"template": {"metadata":{"annotations": {
"dapr.io/enable-metrics":"true",
"dapr.io/metrics-port": "9090",
"dapr.io/metrics-push-enable":"true",
"dapr.io/metrics-push-endpoint":"otelcol:55678"
}}}}}'
Deploy Kedify KEDA:
helm repo add kedify https://kedify.github.io/charts
helm repo update kedify
helm upgrade -i keda kedify/keda --namespace keda --create-namespace --version v2.16.0-1
Wait for all the deployment to become ready
for d in nodeapp pythonapp otelcol otel-add-on-scaler ; do
kubectl rollout status --timeout=300s deploy/${d}
done
for d in keda-admission-webhooks keda-operator keda-operator-metrics-apiserver ; do
kubectl rollout status --timeout=300s deploy/${d} -nkeda
done
# create ScaledObject CR
cat <<SO | kubectl apply -f -
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: dapr-nodeapp
spec:
scaleTargetRef:
name: nodeapp
triggers:
- type: kedify-otel
metadata:
scalerAddress: 'keda-otel-scaler.default.svc:4318'
metricQuery: 'sum(runtime_service_invocation_req_recv_total{app_id="nodeapp",src_app_id="pythonapp"})'
operationOverTime: 'rate'
targetValue: '1'
clampMax: '10'
minReplicaCount: 1
SO
Scaling Behavior
Each replica of the pythonapp microservice makes a call to the nodeapp microservice every second. Check the following part of the ScaledObject configuration:
metricQuery: 'sum(runtime_service_invocation_req_recv_total{app_id="nodeapp",src_app_id="pythonapp"})'
operationOverTime: 'rate'
-
The runtime_service_invocation_req_recv_total metric increments each time the
pythonapp
callsnodeapp
. - One of the metric dimensions is the pod identity, meaning each pod exposes these metrics with its label attached.
- Similar to PromQL, if not all dimensions are specified, multiple metric series will be returned.
-
The OTEL scaler calculates the rate over a one-minute window (default). This should be
1
, as we are calling the API every second, so the counter increments by one each second. -
If multiple metric series are present, the sum is applied to aggregate the values. For example, if there are three
producer pods, the total will be
3
. -
The
targetValue
was set to1
, indicating that one replica of nodeapp can handle this value. This ensures replica parity between the two services. -
If
targetValue
was set to2
, it would indicate that if we scale pythonapp (the producer) toN
replicas, it would result innodeapp
(the consumer) being scaled toN/2
replicas.
Scale the caller microservice to 3
replicas and observe the node app:
kubectl scale deployment pythonapp --replicas=3
This should lead to nodeapp
being scaled also to 3
replicas.
Create 100
request from pythonapp
_podName=$(kubectl get po -ldapr.io/app-id=pythonapp -ojsonpath="{.items[0].metadata.name}")
kubectl debug -it ${_podName} --image=nicolaka/netshoot -- sh -c 'for x in $(seq 100); do curl http://localhost:3500/v1.0/invoke/nodeapp/method/order/ ;done'
Eventually, the node app should be scaled back to pythonapp
‘s number of replicas.
Check the logs:
kubectl logs -lapp.kubernetes.io/name=otel-add-on --tail=-1 --follow
Once finished, clean the cluster:
k3d cluster delete dapr-demo
Share this recording
Link
Append ?t=30
to start the playback at 30s, ?t=3:20
to start the playback at 3m 20s.
Embed image link
Use snippets below to display a screenshot linking to this recording.
Useful in places where scripts are not allowed (e.g. in a project's README file).
HTML:
Markdown:
Embed the player
If you're embedding on your own page or on a site which permits script tags, you can use the full player widget:
Paste the above script tag where you want the player to be displayed on your page.
See embedding docs for additional options.
Download this recording
You can download this recording in asciicast v2 format, as a .cast file.
DownloadReplay in terminal
You can replay the downloaded recording in your terminal using the
asciinema play
command:
asciinema play 693045.cast
If you don't have asciinema CLI installed then see installation instructions.
Use with stand-alone player on your website
Download asciinema player from
the releases page
(you only need .js
and .css
file), then use it like this:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="asciinema-player.css" />
</head>
<body>
<div id="player"></div>
<script src="asciinema-player.min.js"></script>
<script>
AsciinemaPlayer.create(
'/assets/693045.cast',
document.getElementById('player'),
{ cols: 211, rows: 56 }
);
</script>
</body>
</html>
See asciinema player quick-start guide for full usage instructions.
Generate GIF from this recording
While this site doesn't provide GIF conversion at the moment, you can still do it yourself with the help of asciinema GIF generator utility - agg.
Once you have it installed, generate a GIF with the following command:
agg https://asciinema.org/a/693045 demo.gif
Or, if you already downloaded the recording file:
agg demo.cast demo.gif
Check agg --help
for all available options. You can change font
family and size, select color theme, adjust speed and more.
See agg manual for full usage instructions.