Kubernetes CPU throttling with CPU usage well below requests/limits

6/28/2020

I have setup CPU and Memory Requests=Limits on all containers of my pod in order to qualify it for Guaranteed Quality of Service class. Now, look at these CPU Usage and CPU Throttling graphs for same Pod for last 6 hours.

CPU Utilization and Throttling for a pod over 6 hrs period

Does this look normal and expected?

CPU Usage has not even touched 50% of the set limit a single time and still it was being throttled upto 58% at times.

And a side question, what does that red line at 25% in the Throttling graph indicates?

I did some research on this topic and found that there was a bug in Linux kernel that could have caused this and that it was fixed in version 4.18 of the kernel. Reference: this and this

We are on GKE running Container Optimized OS by Google. I checked the linux kernel version on our nodes and they are on 4.19.112+ so I guess we already have that patch? What else could be the reason of this throttling pattern?

P.S. This pod (actually a deployment with autoscaling) is deployed on a separate Node pool which has none of our other workloads running on it. So the only pods other than this deployment running on Nodes in this node pool are some metrics and logging agents and exporters. Here is the full list of pods running on the same Node at which the pod discussed above is scheduled. There are indeed some pods that don't have any CPU limits set on them. Do I need to somehow set CPU limits on these as well?

Pods running on a Node

Our GKE version is 1.16.9-gke.2

Here is the manifest file containing deployment, service, and auto scaler definitions.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: endpoints
  labels:
    app: endpoints
spec:
  replicas: 2
  selector:
    matchLabels:
      run: endpoints
  strategy:
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 0
  template:
    metadata:
      labels:
        run: endpoints
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: endpoints
          image: gcr.io/<PROJECT_ID>/endpoints:<RELEASE_VERSION_PLACEHOLDER>
          livenessProbe:
            httpGet:
              path: /probes/live
              port: 8080
            initialDelaySeconds: 20
            timeoutSeconds: 5
          readinessProbe:
            httpGet:
              path: /probes/ready
              port: 8080
            initialDelaySeconds: 20
            timeoutSeconds: 5
          ports:
            - containerPort: 8080
              protocol: TCP
          env:
            - name: GOOGLE_APPLICATION_CREDENTIALS
              value: "/path/to/secret/gke-endpoints-deployments-access.json"
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: DEPLOYMENT_NAME
              value: "endpoints"
          resources:
            requests:
              memory: "5Gi"
              cpu: 2
            limits:
              memory: "5Gi"
              cpu: 2
          volumeMounts:
            - name: endpoints-gcp-access
              mountPath: /path/to/secret
              readOnly: true
          lifecycle:
            preStop:
              exec:
                # SIGTERM triggers a quick exit; gracefully terminate instead
                command: ["/bin/sh","-c","sleep 3; /usr/sbin/nginx -s quit; sleep 57"]
        # [START proxy_container]
        - name: cloudsql-proxy
          image: gcr.io/cloudsql-docker/gce-proxy:1.16
          command: ["/cloud_sql_proxy",
                    "-instances=<PROJECT_ID>:<ZONE>:prod-db=tcp:3306,<PROJECT_ID>:<ZONE>:prod-db-read-replica=tcp:3307",
                    "-credential_file=/path/to/secret/gke-endpoints-deployments-access.json"]
          # [START cloudsql_security_context]
          securityContext:
            runAsUser: 2  # non-root user
            allowPrivilegeEscalation: false
          # [END cloudsql_security_context]
          resources:
            requests:
              memory: "50Mi"
              cpu: 0.1
            limits:
              memory: "50Mi"
              cpu: 0.1
          volumeMounts:
            - name: endpoints-gcp-access
              mountPath: /path/to/secret
              readOnly: true
        # [END proxy_container]
        # [START nginx-prometheus-exporter container]
        - name: nginx-prometheus-exporter
          image: nginx/nginx-prometheus-exporter:0.7.0
          ports:
            - containerPort: 9113
              protocol: TCP
          env:
            - name: CONST_LABELS
              value: "app=endpoints"
          resources:
            requests:
              memory: "50Mi"
              cpu: 0.1
            limits:
              memory: "50Mi"
              cpu: 0.1
        # [END nginx-prometheus-exporter container]
      tolerations:
        - key: "qosclass"
          operator: "Equal"
          value: "guaranteed"
          effect: "NoSchedule"
      nodeSelector:
        qosclass: guaranteed
      # [START volumes]
      volumes:
        - name: endpoints-gcp-access
          secret:
            secretName: endpoints-gcp-access
      # [END volumes]
---
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: endpoints-backendconfig
spec:
  timeoutSec: 60
  connectionDraining:
    drainingTimeoutSec: 60
---
apiVersion: v1
kind: Service
metadata:
  name: endpoints
  labels:
    app: endpoints
  annotations:
    cloud.google.com/neg: '{"ingress": true}' # Creates a NEG after an Ingress is created
    beta.cloud.google.com/backend-config: '{"ports": {"80":"endpoints-backendconfig"}}'
spec:
  type: NodePort
  selector:
    run: endpoints
  ports:
    - name: endpoints-nginx
      port: 80
      protocol: TCP
      targetPort: 8080
    - name: endpoints-metrics
      port: 81
      protocol: TCP
      targetPort: 9113
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: endpoints-autoscaler
spec:
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 40
  - type: External
    external:
      metricName: external.googleapis.com|prometheus|nginx_http_requests_total
      metricSelector:
        matchLabels:
          metric.labels.app: endpoints
      targetAverageValue: "5"
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: endpoints
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: endpoints-nginx-monitor
  namespace: monitoring
  labels:
    app: endpoints-nginx-monitor
    chart: prometheus-operator-8.13.7
    release: prom-operator
    heritage: Tiller
spec:
  selector:
    matchLabels:
      app: endpoints
  namespaceSelector:
    any: true
  endpoints:
  - port: endpoints-metrics
    path: "/metrics"

And here is the dockerfile for the only custom container image used in the deployment:

# Dockerfile extending the generic PHP image with application files for a
# single application.
FROM gcr.io/google-appengine/php:latest

# The Docker image will configure the document root according to this
# environment variable.
ENV DOCUMENT_ROOT /app

RUN /bin/bash /stackdriver-files/enable_stackdriver_integration.sh
-- Muhammad Anas
google-kubernetes-engine
kubernetes
linux-kernel

2 Answers

1/28/2022

You usually see lines like that based on thresholds you set, I believe you can pick color of line when you set threshold

-- ffran09
Source: StackOverflow

6/29/2020

I don't know what that red line is, so I'll skip that one. Would be nice though to know what do you expected to happen with CPU throttling case.

So, about your CPU usage and throttling, there is no indication that anything goes wrong. CPU throttling happens with any modern systems when there is lots of CPU available. So, it will slow down the clock, and will start running slower (e.g. a 2.3GHz machine switches to 2.0GHz). This is the reason you can't set CPU limit based on percentage.

So, from your graphs, what I speculate to see is a CPU clock going down, and naturally a percentage going up; as expected. Nothing weird.

-- suren
Source: StackOverflow