Skip to content

Commit 36298bc

Browse files
committed
feat(gke): refactor nginx benchmark to 3-tier architecture (client-proxy-upstream)
1 parent b8cbf1a commit 36298bc

3 files changed

Lines changed: 296 additions & 27 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: nginx-proxy-deployment
5+
spec:
6+
replicas: {{ nginx_proxy_replicas }}
7+
selector:
8+
matchLabels:
9+
app: nginx-proxy
10+
template:
11+
metadata:
12+
labels:
13+
app: nginx-proxy
14+
spec:
15+
{% if runtime_class_name %}
16+
runtimeClassName: {{ runtime_class_name }}
17+
{% endif %}
18+
affinity:
19+
nodeAffinity:
20+
requiredDuringSchedulingIgnoredDuringExecution:
21+
nodeSelectorTerms:
22+
- matchExpressions:
23+
- key: pkb_nodepool
24+
operator: In
25+
values:
26+
- nginx
27+
initContainers:
28+
- name: setup-ssl
29+
image: {{ nginx_image }}
30+
command: ['/bin/bash', '-c']
31+
args:
32+
- |
33+
mkdir -p /etc/nginx/ssl
34+
openssl req -x509 -nodes -newkey ec:<(openssl ecparam -name secp384r1) -keyout /etc/nginx/ssl/ecdsa.key -out /etc/nginx/ssl/ecdsa.crt -days 365 -subj "/CN=$HOSTNAME"
35+
volumeMounts:
36+
- name: ssl-volume
37+
mountPath: /etc/nginx/ssl
38+
containers:
39+
- name: nginx-proxy
40+
image: {{ nginx_image }}
41+
ports:
42+
- containerPort: {{ nginx_port }}
43+
volumeMounts:
44+
- name: config-volume
45+
mountPath: /etc/nginx/nginx.conf
46+
subPath: nginx-proxy.conf
47+
- name: ssl-volume
48+
mountPath: /etc/nginx/ssl
49+
volumes:
50+
- name: ssl-volume
51+
emptyDir: {}
52+
- name: config-volume
53+
configMap:
54+
name: nginx-configs
55+
---
56+
apiVersion: v1
57+
kind: Service
58+
metadata:
59+
name: nginx-cluster
60+
spec:
61+
type: LoadBalancer
62+
ports:
63+
- port: {{ nginx_port }}
64+
targetPort: {{ nginx_port }}
65+
selector:
66+
app: nginx-proxy
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: nginx-upstream-deployment
5+
spec:
6+
replicas: {{ nginx_upstream_replicas }}
7+
selector:
8+
matchLabels:
9+
app: nginx-upstream
10+
template:
11+
metadata:
12+
labels:
13+
app: nginx-upstream
14+
spec:
15+
{% if runtime_class_name %}
16+
runtimeClassName: {{ runtime_class_name }}
17+
{% endif %}
18+
affinity:
19+
nodeAffinity:
20+
requiredDuringSchedulingIgnoredDuringExecution:
21+
nodeSelectorTerms:
22+
- matchExpressions:
23+
- key: pkb_nodepool
24+
operator: In
25+
values:
26+
- upstream
27+
initContainers:
28+
- name: setup-random-content
29+
image: busybox:1.35
30+
command: ['sh', '-c']
31+
args:
32+
- |
33+
dd bs=1 count={{ nginx_content_size }} if=/dev/urandom of=/usr/share/nginx/html/random_content
34+
volumeMounts:
35+
- name: html-volume
36+
mountPath: /usr/share/nginx/html
37+
- name: setup-ssl
38+
image: {{ nginx_image }}
39+
command: ['/bin/bash', '-c']
40+
args:
41+
- |
42+
mkdir -p /etc/nginx/ssl
43+
openssl req -x509 -nodes -newkey ec:<(openssl ecparam -name secp384r1) -keyout /etc/nginx/ssl/ecdsa.key -out /etc/nginx/ssl/ecdsa.crt -days 365 -subj "/CN=$HOSTNAME"
44+
volumeMounts:
45+
- name: ssl-volume
46+
mountPath: /etc/nginx/ssl
47+
containers:
48+
- name: nginx-upstream
49+
image: {{ nginx_image }}
50+
ports:
51+
- containerPort: {{ nginx_port }}
52+
volumeMounts:
53+
- name: config-volume
54+
mountPath: /etc/nginx/nginx.conf
55+
subPath: nginx-upstream.conf
56+
- name: html-volume
57+
mountPath: /usr/share/nginx/html
58+
- name: ssl-volume
59+
mountPath: /etc/nginx/ssl
60+
volumes:
61+
- name: ssl-volume
62+
emptyDir: {}
63+
- name: config-volume
64+
configMap:
65+
name: nginx-configs
66+
- name: html-volume
67+
emptyDir: {}
68+
---
69+
apiVersion: v1
70+
kind: Service
71+
metadata:
72+
name: nginx-upstream
73+
spec:
74+
type: ClusterIP
75+
ports:
76+
- port: {{ nginx_port }}
77+
targetPort: {{ nginx_port }}
78+
selector:
79+
app: nginx-upstream

perfkitbenchmarker/linux_benchmarks/kubernetes_nginx_benchmark.py

Lines changed: 151 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,27 @@
1414
"""Runs wrk2 clients against replicated nginx servers behind a load balancer."""
1515

1616
import functools
17+
import logging
1718
import os
1819
import shutil
1920
import tempfile
21+
import time
2022

2123
from absl import flags
2224
from perfkitbenchmarker import background_tasks
2325
from perfkitbenchmarker import configs
2426
from perfkitbenchmarker import data
27+
from perfkitbenchmarker import errors
2528
from perfkitbenchmarker.linux_benchmarks import nginx_benchmark
26-
from perfkitbenchmarker.resources.container_service import kubernetes_commands
2729

2830
FLAGS = flags.FLAGS
2931

32+
flags.DEFINE_string(
33+
'nginx_conf',
34+
None,
35+
'Path to a custom nginx configuration file.',
36+
)
37+
3038
flags.DEFINE_string(
3139
'kubernetes_nginx_runtime_class_name',
3240
None,
@@ -48,7 +56,16 @@
4856
vm_spec: *default_dual_core
4957
nodepools:
5058
nginx:
51-
vm_count: 3
59+
vm_count: 1
60+
vm_spec:
61+
GCP:
62+
machine_type: n2-standard-4
63+
AWS:
64+
machine_type: m6i.xlarge
65+
Azure:
66+
machine_type: Standard_D4s_v5
67+
upstream:
68+
vm_count: 2
5269
vm_spec:
5370
GCP:
5471
machine_type: n2-standard-4
@@ -95,49 +112,159 @@ def GetConfig(user_config):
95112
return config
96113

97114

115+
def _MergeNginxConfigs(global_conf_path, server_conf_path, force_http=False):
116+
"""Merges global and server nginx configs into a single file."""
117+
with open(global_conf_path) as f:
118+
global_conf = f.read()
119+
with open(server_conf_path) as f:
120+
server_conf = f.read()
121+
122+
# Replace placeholder with K8s upstream service DNS
123+
# Upstream service name is 'nginx-upstream' in 'default' namespace
124+
upstream_dns = 'nginx-upstream.default.svc.cluster.local'
125+
# Hardcode Upstream Port to 80 (Profile 2 / Single TLS Parity)
126+
upstream_port = 80
127+
128+
# The placeholder in rp_apigw.conf includes :443;
129+
# We replace the first occurrence (fileserver_1)
130+
server_conf = server_conf.replace(
131+
'# server <fileserver_1_ip_or_dns>:443;',
132+
f'server {upstream_dns}:{upstream_port};',
133+
)
134+
135+
if (not FLAGS.nginx_use_ssl) or force_http:
136+
# Convert HTTPS config to HTTP
137+
server_conf = server_conf.replace('ssl on;', '# ssl on;')
138+
server_conf = server_conf.replace('ssl_certificate', '# ssl_certificate')
139+
server_conf = server_conf.replace('ssl_ciphers', '# ssl_ciphers')
140+
server_conf = server_conf.replace('listen 443 ssl', 'listen 80')
141+
142+
# ALWAYS force upstream connection to HTTP (Profile 2)
143+
# This ensures Proxy -> Upstream is unencrypted even if Proxy listener is HTTPS
144+
server_conf = server_conf.replace('proxy_pass https://', 'proxy_pass http://')
145+
146+
# Simple merge: replace the include line with the actual content
147+
# global.conf has: include /etc/nginx/conf.d/*.conf;
148+
merged_conf = global_conf.replace(
149+
'include /etc/nginx/conf.d/*.conf;', server_conf
150+
)
151+
return merged_conf
152+
153+
98154
def _CreateNginxConfigMapDir():
99155
"""Returns a TemporaryDirectory containing files in the Nginx ConfigMap."""
156+
temp_dir = tempfile.TemporaryDirectory()
157+
158+
# 1. Get paths to source config files
159+
global_conf_path = data.ResourcePath('nginx/global.conf')
160+
proxy_conf_path = data.ResourcePath('nginx/rp_apigw.conf')
161+
upstream_conf_path = data.ResourcePath('nginx/file_server.conf')
162+
100163
if FLAGS.nginx_conf:
101-
nginx_conf_filename = FLAGS.nginx_conf
102-
else:
103-
relative_nginx_conf_filename = 'container/kubernetes_nginx/http.conf'
104-
if FLAGS.nginx_use_ssl:
105-
relative_nginx_conf_filename = 'container/kubernetes_nginx/https.conf'
106-
nginx_conf_filename = data.ResourcePath(relative_nginx_conf_filename)
164+
# If custom config provided, use it for proxy (legacy behavior support)
165+
# But for 3-tier, we really need the split configs.
166+
# For now, let's assume nginx_conf overrides the proxy config if given.
167+
proxy_conf_path = FLAGS.nginx_conf
168+
169+
# 2. Prepare Proxy Config (global + rp_apigw)
170+
proxy_conf_content = _MergeNginxConfigs(global_conf_path, proxy_conf_path)
171+
with open(os.path.join(temp_dir.name, 'nginx-proxy.conf'), 'w') as f:
172+
f.write(proxy_conf_content)
173+
174+
# 3. Prepare Upstream Config (global + file_server)
175+
# Force HTTP for Upstream (Profile 2) similar to the GCE fix
176+
upstream_conf_content = _MergeNginxConfigs(
177+
global_conf_path, upstream_conf_path, force_http=True
178+
)
179+
with open(os.path.join(temp_dir.name, 'nginx-upstream.conf'), 'w') as f:
180+
f.write(upstream_conf_content)
107181

108-
temp_dir = tempfile.TemporaryDirectory()
109-
config_map_filename = os.path.join(temp_dir.name, 'default')
110-
shutil.copyfile(nginx_conf_filename, config_map_filename)
111182
return temp_dir
112183

113184

185+
def _WaitForConnectivity(benchmark_spec):
186+
"""Waits for the proxy to be reachable."""
187+
lb_ip = benchmark_spec.nginx_endpoint_ip
188+
logging.info('Waiting for connectivity to %s...', lb_ip)
189+
190+
# Try to connect to the endpoint
191+
scheme = 'https' if FLAGS.nginx_use_ssl else 'http'
192+
url = f'{scheme}://{lb_ip}/'
193+
194+
# Use curl to check connectivity
195+
cmd = f'curl -k -v {url}'
196+
197+
# Retry for up to 5 minutes
198+
start_time = time.time()
199+
while time.time() - start_time < 300:
200+
try:
201+
# We run this from the client VM to verify end-to-end connectivity
202+
benchmark_spec.vm_groups['clients'][0].RemoteCommand(cmd)
203+
logging.info('Connectivity established.')
204+
return
205+
except errors.VirtualMachine.RemoteCommandError:
206+
logging.info('Still waiting for connectivity...')
207+
time.sleep(10)
208+
209+
raise errors.Benchmarks.PrepareException(
210+
f'Timed out waiting for connectivity to {url}'
211+
)
212+
213+
114214
def _PrepareCluster(benchmark_spec):
115215
"""Prepares a cluster to run the Nginx benchmark."""
216+
# 1. Create ConfigMap
116217
with _CreateNginxConfigMapDir() as nginx_config_map_dirname:
117218
benchmark_spec.container_cluster.CreateConfigMap(
118-
'default-config', nginx_config_map_dirname
219+
'nginx-configs', nginx_config_map_dirname
119220
)
120-
container_image = benchmark_spec.container_specs['kubernetes_nginx'].image
121-
replicas = benchmark_spec.container_cluster.nodepools['nginx'].num_nodes
122221

123-
nginx_port = 80
124-
if FLAGS.nginx_use_ssl:
125-
nginx_port = 443
222+
container_image = benchmark_spec.container_specs['kubernetes_nginx'].image
223+
proxy_port = 443 if FLAGS.nginx_use_ssl else 80
224+
upstream_port = 80 # Hardcoded for Profile 2
126225

127-
kubernetes_commands.ApplyManifest(
128-
'container/kubernetes_nginx/kubernetes_nginx.yaml.j2',
226+
# 2. Deploy Upstream
227+
# We use the 'upstream' nodepool
228+
upstream_replicas = benchmark_spec.container_cluster.nodepools[
229+
'upstream'
230+
].num_nodes
231+
benchmark_spec.container_cluster.ApplyManifest(
232+
'container/kubernetes_nginx/nginx_upstream.yaml.j2',
129233
nginx_image=container_image,
130-
nginx_replicas=replicas,
234+
nginx_upstream_replicas=upstream_replicas,
131235
nginx_content_size=FLAGS.nginx_content_size,
132-
nginx_port=nginx_port,
133-
nginx_worker_connections=FLAGS.nginx_worker_connections,
236+
nginx_port=upstream_port,
134237
runtime_class_name=FLAGS.kubernetes_nginx_runtime_class_name,
135238
)
136239

240+
# 3. Deploy Proxy
241+
# We use the 'nginx' nodepool (renamed to proxy in our minds, but key is 'nginx')
242+
proxy_replicas = benchmark_spec.container_cluster.nodepools['nginx'].num_nodes
243+
benchmark_spec.container_cluster.ApplyManifest(
244+
'container/kubernetes_nginx/nginx_proxy.yaml.j2',
245+
nginx_image=container_image,
246+
nginx_proxy_replicas=proxy_replicas,
247+
nginx_port=proxy_port,
248+
runtime_class_name=FLAGS.kubernetes_nginx_runtime_class_name,
249+
)
250+
251+
# 4. Wait for deployments
252+
benchmark_spec.container_cluster.WaitForResource(
253+
'deploy/nginx-upstream-deployment', 'available'
254+
)
137255
benchmark_spec.container_cluster.WaitForResource(
138-
'deploy/nginx-deployment', 'available'
256+
'deploy/nginx-proxy-deployment', 'available'
139257
)
140258

259+
# 5. Get LoadBalancer IP
260+
# Get LoadBalancer IP using PKB's built-in retry method
261+
benchmark_spec.nginx_endpoint_ip = (
262+
benchmark_spec.container_cluster.GetLoadBalancerIP('nginx-cluster')
263+
)
264+
265+
# 6. Wait for connectivity
266+
_WaitForConnectivity(benchmark_spec)
267+
141268

142269
def Prepare(benchmark_spec):
143270
"""Install Nginx on the K8s Cluster and a load generator on the clients.
@@ -154,10 +281,7 @@ def Prepare(benchmark_spec):
154281

155282
background_tasks.RunThreaded(lambda f: f(), prepare_fns)
156283

157-
benchmark_spec.nginx_endpoint_ip = (
158-
benchmark_spec.container_cluster.GetClusterIP('nginx-cluster')
159-
)
160-
284+
# benchmark_spec.nginx_endpoint_ip is set in _PrepareCluster
161285

162286
def Run(benchmark_spec):
163287
"""Run a benchmark against the Nginx server."""

0 commit comments

Comments
 (0)