Skip to content

Commit f1a8f5a

Browse files
committed
PGO will now turn "huge_pages" to "try" or "off" based on whether huge pages have been requested in the resource spec.
[sc-17766]
1 parent 8b14582 commit f1a8f5a

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed

docs/content/guides/huge-pages.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
title: "Huge Pages"
3+
date:
4+
draft: false
5+
weight: 100
6+
---
7+
8+
# Huge Pages
9+
10+
Huge Pages, a.k.a. "Super Pages" or "Large Pages", are larger chunks of memory that can speed up your system. Normally, the chunks of memory, or "pages", used by the CPU are 4kB in size. The more memory a process needs, the more pages the CPU needs to manage. By using larger pages, the CPU can manage fewer pages and increase its efficiency. For this reason, it is generally recommended to use Huge Pages with your Postgres databases.
11+
12+
# Configuring Huge Pages with PGO
13+
14+
To turn Huge Pages on with PGO, you first need to have Huge Pages turned on at the OS level. This means having them enabled, and a specific number of pages preallocated, on the node(s) where you plan to schedule your pods. All processes that run on a given node and request Huge pages will be sharing this pool of pages, so it is important to allocate enough pages for all the different processes to get what they need. This system/kube-level configuration is outside the scope of this document, since the way that Huge Pages are configured at the OS/node level is dependent on your Kube environment. Consult your Kube environment documentation and any IT support you have for assistance with this step.
15+
16+
When you enable Huge Pages in your Kube cluster, it is important to keep a few things in mind during the rest of the configuration process:
17+
1. What size of Huge Pages are enabled? If there are multiple sizes enabled, which one is the default? Which one do you want Postgres to use?
18+
2. How many pages were preallocated? Are there any other applications or processes that will be using these pages?
19+
3. Which nodes have Huge Pages enabled? Is it possible that more nodes will be added to the cluster? If so, will they also have Huge Pages enabled?
20+
21+
Once Huge Pages are enabled on one or more nodes in your Kubernetes cluster, you can tell Postgres to start using them by adding some configuration to your PostgresCluster spec (Warning: setting/changing this setting will cause your database to restart):
22+
23+
```yaml
24+
apiVersion: postgres-operator.crunchydata.com/v1beta1
25+
kind: PostgresCluster
26+
metadata:
27+
name: hippo
28+
spec:
29+
image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-14.6-2
30+
postgresVersion: 14
31+
instances:
32+
- name: instance1
33+
resources:
34+
limits:
35+
hugepages-2Mi: 16Mi
36+
memory: 4Gi
37+
```
38+
39+
This is where it is important to know the size and the number of Huge Pages available. In the spec above, the `hugepages-2Mi` line indicates that we want to use 2MB sized pages. If your system only has 1GB sized pages available, then you will want to use `hugepages-1Gi` as the setting instead. The value after it, `16Mi` in our example, determines the amount of pages to be allocated to this Postgres instance. If you have multiple instances, you will need to enable/allocate Huge Pages on an instance by instance basis. Keep in mind that if you have a "Highly Available" cluster, meaning you have multiple replicas, each replica will also request Huge Pages. You therefore need to be cognizant of the total amount of Huge Pages available on the node(s) and the amount your cluster is requesting. If you request more pages than are available, you might see some replicas/instances fail to start.
40+
41+
Note: In the `instances.#.resources` spec, there are `limits` and `requests`. If a request value is not specified (like in the example above), it is presumed to be equal to the limit value. For Huge Pages, the request value must always be equal to the limit value, therefore, it is perfectly acceptable to just specify it in the `limits` section.
42+
43+
Note: Postgres uses the system default size by default. This means that if there are multiple sizes of Huge Pages available on the node(s) and you attempt to use a size in your PostgresCluster that is not the system default, it will fail. To use a non-default size you will need to tell Postgres the size to use with the `huge_page_size` variable, which can be set via dynamic configuration (Warning: setting/changing this parameter will cause your database to restart):
44+
45+
```yaml
46+
patroni:
47+
dynamicConfiguration:
48+
postgresql:
49+
parameters:
50+
huge_page_size: 1GB
51+
```
52+
53+
# The Kubernetes Issue
54+
55+
There is an issue in Kubernetes where essentially, if Huge Pages are available on a node, it will tell the processes running in the pods on that node that it has Huge Pages available even if the pod has not actually requested any Huge Pages. This is an issue because by default, Postgres is set to "try" to use Huge Pages. When Postgres is led to believe that Huge Pages are available and it attempts to use Huge Pages only to find that the pod doesn't actually have any Huge Pages allocated since they were never requested, Postgres will fail.
56+
57+
We have worked around this issue by setting `huge_pages = off` in our newest Crunchy Postgres images. PGO will automatically turn `huge_pages` back to `try` whenever Huge Pages are requested in the resources spec. Those who were already happily using Huge Pages will be unaffected, and those who were not using Huge Pages, but were attempting to run their Postgres containers on nodes that have Huge Pages enabled, will no longer see their databases crash.
58+
59+
The only dilemma that remains is that those whose PostgresClusters are not using Huge Pages, but are running on nodes that have Huge Pages enabled, will see their `shared_buffers` set to their lowest possible setting. This is due to the way that Postgres' `initdb` works when bootstrapping a database. There are few ways to work around this issue:
60+
61+
1. Use Huge Pages! You're already running your Postgres containers on nodes that have Huge Pages enabled, why not use them in Postgres?
62+
2. Create nodes in your Kubernetes cluster that don't have Huge Pages enabled, and put your Postgres containers on those nodes.
63+
3. If for some reason you cannot use Huge Pages in Postgres, but you must run your Postgres containers on nodes that have Huge Pages enabled, you can manually set the `shared_buffers` parameter back to a good setting using dynamic configuration (Warning: setting/changing this parameter will cause your database to restart):
64+
65+
```yaml
66+
patroni:
67+
dynamicConfiguration:
68+
postgresql:
69+
parameters:
70+
shared_buffers: 128MB
71+
```

internal/controller/postgrescluster/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ func (r *Reconciler) Reconcile(
216216
pgbackrest.PostgreSQL(cluster, &pgParameters)
217217
pgmonitor.PostgreSQLParameters(cluster, &pgParameters)
218218

219+
// Set huge_pages = try if a hugepages resource limit > 0, otherwise set "off"
220+
postgres.SetHugePages(cluster, &pgParameters)
221+
219222
if err == nil {
220223
rootCA, err = r.reconcileRootCertificate(ctx, cluster)
221224
}

internal/postgres/huge_pages.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Copyright 2021 - 2023 Crunchy Data Solutions, Inc.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package postgres
17+
18+
import (
19+
"strings"
20+
21+
corev1 "k8s.io/api/core/v1"
22+
"k8s.io/apimachinery/pkg/api/resource"
23+
24+
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
25+
)
26+
27+
// This function looks for a valid huge_pages resource request. If it finds one,
28+
// it sets the PostgreSQL parameter "huge_pages" to "try". If it doesn't find
29+
// one, it sets "huge_pages" to "off".
30+
func SetHugePages(cluster *v1beta1.PostgresCluster, pgParameters *Parameters) {
31+
if hugePagesRequested(cluster) {
32+
pgParameters.Default.Add("huge_pages", "try")
33+
} else {
34+
pgParameters.Default.Add("huge_pages", "off")
35+
}
36+
}
37+
38+
// This helper function checks to see if a huge_pages value greater than zero has
39+
// been set in any of the PostgresCluster's instances' resource specs
40+
func hugePagesRequested(cluster *v1beta1.PostgresCluster) bool {
41+
for _, instance := range cluster.Spec.InstanceSets {
42+
for resourceName := range instance.Resources.Limits {
43+
if strings.HasPrefix(resourceName.String(), corev1.ResourceHugePagesPrefix) {
44+
resourceQuantity := instance.Resources.Limits.Name(resourceName, resource.BinarySI)
45+
46+
if resourceQuantity != nil && resourceQuantity.Value() > 0 {
47+
return true
48+
}
49+
}
50+
}
51+
}
52+
53+
return false
54+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Copyright 2021 - 2023 Crunchy Data Solutions, Inc.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package postgres
17+
18+
import (
19+
"testing"
20+
21+
"gotest.tools/v3/assert"
22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/resource"
24+
25+
"github.com/crunchydata/postgres-operator/internal/initialize"
26+
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
27+
)
28+
29+
func TestSetHugePages(t *testing.T) {
30+
t.Run("hugepages not set at all", func(t *testing.T) {
31+
cluster := new(v1beta1.PostgresCluster)
32+
33+
cluster.Spec.InstanceSets = []v1beta1.PostgresInstanceSetSpec{{
34+
Name: "test-instance1",
35+
Replicas: initialize.Int32(1),
36+
Resources: corev1.ResourceRequirements{
37+
Limits: corev1.ResourceList{},
38+
},
39+
}}
40+
41+
pgParameters := NewParameters()
42+
SetHugePages(cluster, &pgParameters)
43+
44+
assert.Equal(t, pgParameters.Default.Has("huge_pages"), true)
45+
assert.Equal(t, pgParameters.Default.Value("huge_pages"), "off")
46+
})
47+
48+
t.Run("hugepages quantity not set", func(t *testing.T) {
49+
cluster := new(v1beta1.PostgresCluster)
50+
51+
emptyQuantity, _ := resource.ParseQuantity("")
52+
cluster.Spec.InstanceSets = []v1beta1.PostgresInstanceSetSpec{{
53+
Name: "test-instance1",
54+
Replicas: initialize.Int32(1),
55+
Resources: corev1.ResourceRequirements{
56+
Limits: corev1.ResourceList{
57+
corev1.ResourceHugePagesPrefix + "2Mi": emptyQuantity,
58+
},
59+
},
60+
}}
61+
62+
pgParameters := NewParameters()
63+
SetHugePages(cluster, &pgParameters)
64+
65+
assert.Equal(t, pgParameters.Default.Has("huge_pages"), true)
66+
assert.Equal(t, pgParameters.Default.Value("huge_pages"), "off")
67+
})
68+
69+
t.Run("hugepages set to zero", func(t *testing.T) {
70+
cluster := new(v1beta1.PostgresCluster)
71+
72+
cluster.Spec.InstanceSets = []v1beta1.PostgresInstanceSetSpec{{
73+
Name: "test-instance1",
74+
Replicas: initialize.Int32(1),
75+
Resources: corev1.ResourceRequirements{
76+
Limits: corev1.ResourceList{
77+
corev1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("0Mi"),
78+
},
79+
},
80+
}}
81+
82+
pgParameters := NewParameters()
83+
SetHugePages(cluster, &pgParameters)
84+
85+
assert.Equal(t, pgParameters.Default.Has("huge_pages"), true)
86+
assert.Equal(t, pgParameters.Default.Value("huge_pages"), "off")
87+
})
88+
89+
t.Run("hugepages set correctly", func(t *testing.T) {
90+
cluster := new(v1beta1.PostgresCluster)
91+
92+
cluster.Spec.InstanceSets = []v1beta1.PostgresInstanceSetSpec{{
93+
Name: "test-instance1",
94+
Replicas: initialize.Int32(1),
95+
Resources: corev1.ResourceRequirements{
96+
Limits: corev1.ResourceList{
97+
corev1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("16Mi"),
98+
},
99+
},
100+
}}
101+
102+
pgParameters := NewParameters()
103+
SetHugePages(cluster, &pgParameters)
104+
105+
assert.Equal(t, pgParameters.Default.Has("huge_pages"), true)
106+
assert.Equal(t, pgParameters.Default.Value("huge_pages"), "try")
107+
})
108+
109+
}

0 commit comments

Comments
 (0)