If you are running PHP-FPM applications in Kubernetes, you have likely hit by the following warning:

WARNING: [pool www] server reached pm.max_children setting, consider raising it.

When this happens, applications load very slowly. So what you need to do is configure the pm.max_children accordingly. We can adjust the FPM configuration and add more pods horizontally in order to deal with the max_children issue.

Prerequisite:

Make sure you have the Prometheus/VictoriaMetrics stack with Grafana installed. For this tutorial, I will be using VictoriaMetrics (deployed in monitoring namespace), a lightweight Prometheus alternative.

Configuring PHP-FPM

First, we need to configure PHP-FPM to static and the pm.max_children value should be low as each pod shouldn’t run too many processes. Also, we need to enable the status page in order to let the exporter scrape the PHP-FPM metrics from the status page (more on this later). So here is a sample configuration:

pm = static
pm.status_path = /status
pm.max_children = 10
; A child process will handle at least 200 requests before respawning.
pm.max_requests = 200

Setup php-fpm exporter

In order to scrape php-fpm metrics from application, we will deploy hipages/php-fpm_exporter exporter as sidecar. Exporter will read metrics from the /status page and export those metrics for Prometheus/VictoriaMetrics. Later, we will grab those exported values with VictoriaMetrics.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
apiVersion: v1
kind: ConfigMap
metadata:
  name: php-benchmark-fpm-conf
  namespace: default
data:
  www.conf: |
    [www]
    user = www-data
    group = www-data
    listen = 127.0.0.1:9000

    pm = static
    pm.max_children = 10
    pm.max_requests = 200
    pm.status_path = /status    

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-fpm-benchmark-php
  namespace: default
  labels:
    app: php
spec:
  replicas: 2
  selector:
    matchLabels:
      app: php
  template:
    metadata:
      labels:
        app: php
    spec:
      containers:
        - name: php
          lifecycle:
            postStart:
              exec:
                command: ["/bin/sh", "-c", "cp -r /app/. /var/www/html"]
          image: kyue1005scmp/php-fpm-benchmark-php
          resources:
            limits:
              cpu: 50m
              memory: 128Mi
            requests:
              cpu: 50m
              memory: 50Mi
          volumeMounts:
            - name: php-fpm-conf
              mountPath: /usr/local/etc/php-fpm.d/www.conf
              subPath: www.conf
        - name: php-fpm-exporter
          image: hipages/php-fpm_exporter
          env:
            - name: PHP_FPM_SCRAPE_URI
              value: tcp://127.0.0.1:9000/status
          ports:
            - containerPort: 9253
          resources:
            limits:
              cpu: 30m
              memory: 32Mi
            requests:
              cpu: 10m
              memory: 10Mi
      volumes:
        - name: php-fpm-conf
          configMap:
            name: php-benchmark-fpm-conf
---
apiVersion: v1
kind: Service
metadata:
  name: php-benchmark-php
  namespace: default
  labels:
    app: php
  annotations:
    prometheus.io/scrape: 'true'
    prometheus.io/port: '9253'
spec:
  selector:
    app: php
  ports:
    - protocol: TCP
      port: 9000

In the above manifests, we defined the configmap for our simple benchmarking app. Here we added exporter as sidecar (line: 55-68) and exposed it with service.

Also note that we have added two annotations (line: 81-83) for Prometheus in the service manifest. Those annoatations are necessary for Prometheus to scrape the data from the exporter.

Deploy nginx application running with PHP-FPM app

Next, we will deploy Nginx application which will pass all the PHP requests to the PHP-FPM pod.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
apiVersion: v1
kind: ConfigMap
metadata:
  name: php-benchmark-nginx-conf
  namespace: default
data:
  nginx.conf: |
    pid        /tmp/nginx.pid;

    events {
        worker_connections  1024;
    }

    http {
      client_body_temp_path /tmp/client_temp;
      proxy_temp_path       /tmp/proxy_temp_path;
      fastcgi_temp_path     /tmp/fastcgi_temp;
      uwsgi_temp_path       /tmp/uwsgi_temp;
      scgi_temp_path        /tmp/scgi_temp;
      proxy_ignore_client_abort on;

      server {
        listen 8080 default_server;
        listen [::]:8080 default_server;
        proxy_ignore_client_abort on;
        root /var/www/html;
        server_name _;

        location / {
          try_files $uri $uri/ =404;
        }

        location ~ \.php$ {
          include fastcgi_params;
          proxy_ignore_client_abort on;
          fastcgi_buffers 16 32k;
          fastcgi_buffer_size 32k;
          fastcgi_intercept_errors on;
          fastcgi_read_timeout 900;
          fastcgi_keep_conn on;
          fastcgi_param REQUEST_METHOD $request_method;
          fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
          fastcgi_pass php-benchmark-php:9000;
        }
      }
    }    

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-fpm-benchmark-nginx
  namespace: default
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: kyue1005scmp/php-fpm-benchmark-nginx
          resources:
            limits:
              cpu: 100m
              memory: 128Mi
            requests:
              cpu: 10m
              memory: 50Mi
          volumeMounts:
            - name: nginx-conf
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf
      volumes:
        - name: nginx-conf
          configMap:
            name: php-benchmark-nginx-conf
---
apiVersion: v1
kind: Service
metadata:
  name: php-benchmark-nginx
  namespace: default
  labels:
    app: nginx
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 8080
---

As we can see at line 33-44, nginx will pass all the PHP requests to the FPM pod.

If you deploy those manifests, our application is ready to get connection.

Configure VictoriaMetrics to scrape data

Next we need to deploy VMServiceScrape resource in order to tell VictoriaMetrics to scrape the data from the exporter. Apply the following manifest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape #scrape based on service. There is also VMNodeScrape
metadata:
  name: php-benchmark-fpm
  namespace: monitoring #namespace where VictoriaMetrics deployed
spec:
  endpoints:
  - targetPort: 9253
    path: /metrics
  namespaceSelector:
    matchNames:
    - default  #app deployed in default namespace
  selector:
    matchLabels:
      app: php #PHP-FPM svc has this label. Label should match

Test the configuration

Let’s test whether the above deployments are working correctly or not, we will deploy another pod and test the connectivity

1
kubectl run httpd --image=httpd -it --rm --restart=Never -- /bin/bash

Next, execute the following curl command while in the httpd pod:

1
curl http://php-benchmark-nginx:8080/bench.php?io=fast

Wait a few seconds. You should see a completed message. Means everything is working as expected 😎

If you visit Grafana dashboard and go to Explore section, you should have phpfpm_* metrics available. Means all our above configurations are working fine 👌

Add the following dashboard in Grafana for monitoring PHP-FPM process: https://github.com/hipages/php-fpm_exporter/blob/master/grafana/kubernetes-php-fpm.json

Scaling with Keda

Keda is an Event-driven Autoscaler. Kubernetes Horizontal Pod Autoscaler is limited to few metrics. If we need more advanced metrics, like scaling pods based on php-fpm process utilization, Keda is a great choice.

Install Keda

Please follow the official documentation to deploy Keda in your Kubernetes cluster.

Next, we will deploy Keda’s ScaledObject resource in default namespace which will monitor php-fpm process utilization. Based on the utlization, we can scale pods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
 name: php-fpm-scale
 namespace: default
spec:
 scaleTargetRef:
   kind: Deployment
   name: php-fpm-benchmark-php
 minReplicaCount: 3
 maxReplicaCount: 6
 cooldownPeriod: 30
 pollingInterval: 1
 triggers:
 - type: prometheus
   metadata:
     serverAddress: http://vmsingle-vmstack.monitoring.svc:8429
     metricName: phpfpm_active_processes  #Metric name to use.
     query: |
       avg((sum(phpfpm_active_processes{job="php-benchmark-php"}) by (kubernetes_pod_name) *100) / sum(phpfpm_total_processes{job="php-benchmark-php"}) by (kubernetes_pod_name))       
     threshold: "50" #Value to start scaling for. Trigger scaling when the process utilization is 50%

At line 8-10, we are targeting php-fpm-benchmark-php deployment for the scaling event. Also at line 18 we specified the VictoriaMetrics/Prometheus service endpoint. Finally, at line 20-22, we added a custom query that will run every second (pollingInterval). This query will return the current phpfpm active process of php-fpm-benchmark-php deployment. If the value goes above 50% (threshold), scaling will happen. A minimum of 3 replicas (minReplicaCount) will always be running and Keda will scale the pods up to 6 replicas (maxReplicaCount).

Test the scaling event

Let’s test how Keda performs scaling event. While in httpd pod (kubectl run httpd --image=httpd -it --rm --restart=Never -- /bin/bash), run the following ab test command

1
ab -c 50 -n 1000 -s 1200 http://php-benchmark-nginx:8080/bench.php\\?io\\=slow

The above command will send 1000 requests and perform 50 requests at a time. The maximum timeout is set to 1200 seconds.

Now monitor the PHP-FPM dashboard in Grafana and also monitor pods (watch kubectl get pods). You should see pods scaling up and down based on process utilization.

That’s all folks.