Merge lp:~stub/charms/precise/postgresql/pgtune into lp:charms/postgresql

Proposed by Stuart Bishop
Status: Merged
Merged at revision: 93
Proposed branch: lp:~stub/charms/precise/postgresql/pgtune
Merge into: lp:charms/postgresql
Prerequisite: lp:~stub/charms/precise/postgresql/tests
Diff against target: 523 lines (+189/-138)
7 files modified
Makefile (+10/-0)
config.yaml (+21/-12)
hooks/hooks.py (+41/-18)
hooks/test_hooks.py (+73/-83)
test.py (+4/-0)
testing/jujufixture.py (+38/-24)
tests/00_setup.test (+2/-1)
To merge this branch: bzr merge lp:~stub/charms/precise/postgresql/pgtune
Reviewer Review Type Date Requested Status
Marco Ceppi (community) Approve
Review via email: mp+213640@code.launchpad.net

Description of the change

Tune the database using the pgtune tool, rather than attempt to reproduce its logic ourselves.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

Note that although I change the default value in config.yaml to 'mixed', we still accept the old 'auto' value so existing environments will not need config changes.

syslinux package seems to be unused and unnecessary, so removing it resolves Bug #1297987.

A bug caused the postgresql-debversion package to not be installed. This has been fixed, which could have confused our redeployments.

The changes in jujufixture.py were me futzing around trying to get test detail reporting working. I'm giving up for now, at least on this branch.

132. By Stuart Bishop

delint

133. By Stuart Bishop

Merged tests into pgtune.

134. By Stuart Bishop

Remove dead code, fix another test wait race

135. By Stuart Bishop

Fix tests and config documentation

Revision history for this message
Marco Ceppi (marcoceppi) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2014-01-27 12:59:00 +0000
3+++ Makefile 2014-04-04 12:07:49 +0000
4@@ -1,6 +1,16 @@
5 CHARM_DIR := $(shell pwd)
6 TEST_TIMEOUT := 900
7
8+default:
9+ @echo "One of:"
10+ @echo " make lint"
11+ @echo " make test"
12+ @echo " make unit_test"
13+ @echo " make integration_test"
14+ @echo " make integration_test_91"
15+ @echo " make integration_test_92"
16+ @echo " make integration_test_93"
17+
18 test: lint unit_test integration_test
19
20 unit_test:
21
22=== modified file 'config.yaml'
23--- config.yaml 2014-04-04 12:07:49 +0000
24+++ config.yaml 2014-04-04 12:07:49 +0000
25@@ -225,21 +225,25 @@
26 work_mem:
27 default: "1MB"
28 type: string
29- description: Working Memory
30+ description: |
31+ Working Memory.
32+ Ignored unless 'performance_tuning' is set to 'manual'.
33 maintenance_work_mem:
34 default: "1MB"
35 type: string
36- description: Maintenance working memory
37+ description: |
38+ Maintenance working memory.
39+ Ignored unless 'performance_tuning' is set to 'manual'.
40 performance_tuning:
41- default: "auto"
42+ default: "Mixed"
43 type: string
44 description: |
45- Possible values here are "auto" or "manual". If we set "auto" then the
46- charm will attempt to automatically tune all the performance parameters
47- for kernel_shmall, kernel_shmmax, shared_buffers and
48- effective_cache_size below, unless those config values are explicitly
49- set. If manual, then it will use the defaults below unless set.
50- "auto" gathers information about the node on which you are deployed and
51+ Possible values here are "manual", "DW" (data warehouse),
52+ "OLTP" (online transaction processing), "Web" (web application),
53+ "Desktop" or "Mixed". When this is set to a value other than
54+ "manual", the charm invokes the pgtune tool to tune a number
55+ of performance parameters based on the specified load type.
56+ pgtune gathers information about the node on which you are deployed and
57 tries to make intelligent guesses about what tuning parameters to set
58 based on available RAM and CPU under the assumption that it's the only
59 significant service running on this node.
60@@ -257,13 +261,15 @@
61 description: |
62 The amount of memory the database server uses for shared memory
63 buffers. This string should be of the format '###MB'.
64+ Ignored unless 'performance_tuning' is set to 'manual'.
65 effective_cache_size:
66 default: ""
67 type: string
68 description: |
69 Effective cache size is an estimate of how much memory is available for
70 disk caching within the database. (50% to 75% of system memory). This
71- string should be of the format '###MB'.
72+ string should be of the format '###MB'. Ignored unless
73+ 'performance_tuning' is set to 'manual'.
74 temp_buffers:
75 default: "1MB"
76 type: string
77@@ -273,11 +279,14 @@
78 default: "-1"
79 type: string
80 description: |
81- min 32kB, -1 sets based on shared_buffers (change requires restart)
82+ min 32kB, -1 sets based on shared_buffers (change requires restart).
83+ Ignored unless 'performance_tuning' is set to 'manual'.
84 checkpoint_segments:
85 default: 3
86 type: int
87- description: in logfile segments, min 1, 16MB each
88+ description: |
89+ in logfile segments, min 1, 16MB each.
90+ Ignored unless 'performance_tuning' is set to 'manual'.
91 random_page_cost:
92 default: 4.0
93 type: float
94
95=== modified file 'hooks/hooks.py'
96--- hooks/hooks.py 2014-04-04 12:07:49 +0000
97+++ hooks/hooks.py 2014-04-04 12:07:49 +0000
98@@ -437,27 +437,18 @@
99 return int(run("getconf PAGE_SIZE")) # frequently 4096
100
101
102+def _run_sysctl(postgresql_sysctl):
103+ """sysctl -p postgresql_sysctl, helper for easy test mocking."""
104+ return run("sysctl -p {}".format(postgresql_sysctl))
105+
106+
107 def create_postgresql_config(config_file):
108 '''Create the postgresql.conf file'''
109 config_data = hookenv.config()
110 if not config_data.get('listen_port', None):
111 config_data['listen_port'] = get_service_port()
112- if config_data["performance_tuning"] == "auto":
113- # Taken from:
114- # http://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server
115- # num_cpus is not being used ... commenting it out ... negronjl
116- #num_cpus = run("cat /proc/cpuinfo | grep processor | wc -l")
117+ if config_data["performance_tuning"].lower() != "manual":
118 total_ram = _get_system_ram()
119- if not config_data["effective_cache_size"]:
120- config_data["effective_cache_size"] = \
121- "%sMB" % (int(int(total_ram) * 0.75),)
122- if not config_data["shared_buffers"]:
123- if total_ram > 1023:
124- config_data["shared_buffers"] = \
125- "%sMB" % (int(int(total_ram) * 0.25),)
126- else:
127- config_data["shared_buffers"] = \
128- "%sMB" % (int(int(total_ram) * 0.15),)
129 config_data["kernel_shmmax"] = (int(total_ram) * 1024 * 1024) + 1024
130 config_data["kernel_shmall"] = config_data["kernel_shmmax"]
131
132@@ -473,7 +464,7 @@
133 if config_data["kernel_shmmax"] > 0:
134 lines.append("kernel.shmmax = %s\n" % config_data["kernel_shmmax"])
135 host.write_file(postgresql_sysctl, ''.join(lines), perms=0600)
136- run("sysctl -p {}".format(postgresql_sysctl))
137+ _run_sysctl(postgresql_sysctl)
138
139 # If we are replicating, some settings may need to be overridden to
140 # certain minimum levels.
141@@ -504,10 +495,29 @@
142 # Create or update files included from postgresql.conf.
143 configure_log_destination(os.path.dirname(config_file))
144
145+ tune_postgresql_config(config_file)
146+
147 local_state['saved_config'] = config_data
148 local_state.save()
149
150
151+def tune_postgresql_config(config_file):
152+ tune_workload = hookenv.config('performance_tuning').lower()
153+ if tune_workload == "manual":
154+ return # Requested no autotuning.
155+
156+ if tune_workload == "auto":
157+ tune_workload = "mixed" # Pre-pgtune backwards compatibility.
158+
159+ with NamedTemporaryFile() as tmp_config:
160+ run(['pgtune', '-i', config_file, '-o', tmp_config.name,
161+ '-T', tune_workload,
162+ '-c', str(hookenv.config('max_connections'))])
163+ host.write_file(
164+ config_file, open(tmp_config.name, 'r').read(),
165+ owner='postgres', group='postgres', perms=0o600)
166+
167+
168 def create_postgresql_ident(output_file):
169 '''Create the pg_ident.conf file.'''
170 ident_data = {}
171@@ -834,6 +844,17 @@
172 log("listen_ip values other than '*' do not work per LP:1271837",
173 CRITICAL)
174
175+ valid_workloads = [
176+ 'dw', 'oltp', 'web', 'mixed', 'desktop', 'manual', 'auto']
177+ requested_workload = config_data['performance_tuning'].lower()
178+ if requested_workload not in valid_workloads:
179+ valid = False
180+ log('Invalid performance_tuning setting {}'.format(requested_workload),
181+ CRITICAL)
182+ if requested_workload == 'auto':
183+ log("'auto' performance_tuning deprecated. Using 'mixed' tuning",
184+ WARNING)
185+
186 unchangeable_config = [
187 'locale', 'encoding', 'version', 'cluster_name', 'pgdg']
188
189@@ -1554,11 +1575,13 @@
190 "postgresql-{}".format(version),
191 "postgresql-contrib-{}".format(version),
192 "postgresql-plpython-{}".format(version),
193- "python-jinja2", "syslinux", "python-psycopg2"]
194+ "python-jinja2", "python-psycopg2"]
195 # PGDG currently doesn't have debversion for 9.3. Put this back when
196 # it does.
197 if not (hookenv.config('pgdg') and version == '9.3'):
198- "postgresql-{}-debversion".format(version)
199+ packages.append("postgresql-{}-debversion".format(version))
200+ if hookenv.config('performance_tuning').lower() != 'manual':
201+ packages.append('pgtune')
202 packages.extend((hookenv.config('extra-packages') or '').split())
203 packages = fetch.filter_installed_packages(packages)
204 fetch.apt_install(packages, fatal=True)
205
206=== modified file 'hooks/test_hooks.py'
207--- hooks/test_hooks.py 2014-01-08 08:55:50 +0000
208+++ hooks/test_hooks.py 2014-04-04 12:07:49 +0000
209@@ -159,11 +159,11 @@
210 def log(self, *args, **kwargs):
211 pass
212
213- def config_get(self, scope=None):
214+ def config(self, scope=None):
215 if scope is None:
216- return self.config
217+ return dict(self._config)
218 else:
219- return self.config[scope]
220+ return self._config[scope]
221
222 def relation_get(self, scope=None, unit_name=None, relation_id=None):
223 pass
224@@ -175,14 +175,13 @@
225 hooks.hookenv = TestJuju()
226 hooks.host = TestJujuHost()
227 hooks.juju_log_dir = self.makeDir()
228- hooks.hookenv.config = lambda: hooks.hookenv._config
229- #hooks.hookenv.localunit = lambda: "localhost"
230 hooks.os.environ["JUJU_UNIT_NAME"] = "landscape/1"
231 hooks.os.environ["CHARM_DIR"] = os.path.abspath(
232 os.path.join(os.path.dirname(__file__), os.pardir))
233 hooks.postgresql_sysctl = self.makeFile()
234 hooks._get_system_ram = lambda: 1024 # MB
235 hooks._get_page_size = lambda: 1024 * 1024 # bytes
236+ hooks._run_sysctl = lambda x: ""
237 self.maxDiff = None
238
239 def assertFileContains(self, filename, lines):
240@@ -216,8 +215,8 @@
241 C{replication} relations, default wal settings will be present.
242 """
243 config_outfile = self.makeFile()
244- run = self.mocker.replace(hooks.run)
245- run("sysctl -p %s" % hooks.postgresql_sysctl)
246+ _run_sysctl = self.mocker.replace(hooks._run_sysctl)
247+ _run_sysctl(hooks.postgresql_sysctl)
248 self.mocker.result(True)
249 self.mocker.replay()
250 hooks.create_postgresql_config(config_outfile)
251@@ -240,8 +239,8 @@
252 hooks.hookenv._relation_ids = {
253 "replication/0": "db-admin:5", "replication/1": "db-admin:6"}
254 config_outfile = self.makeFile()
255- run = self.mocker.replace(hooks.run)
256- run("sysctl -p %s" % hooks.postgresql_sysctl)
257+ _run_sysctl = self.mocker.replace(hooks._run_sysctl)
258+ _run_sysctl(hooks.postgresql_sysctl)
259 self.mocker.result(True)
260 self.mocker.replay()
261 hooks.create_postgresql_config(config_outfile)
262@@ -269,8 +268,8 @@
263 hooks.hookenv._config["wal_keep_segments"] = 1000
264 hooks.hookenv._config["replicated_wal_keep_segments"] = 999
265 config_outfile = self.makeFile()
266- run = self.mocker.replace(hooks.run)
267- run("sysctl -p %s" % hooks.postgresql_sysctl)
268+ _run_sysctl = self.mocker.replace(hooks._run_sysctl)
269+ _run_sysctl(hooks.postgresql_sysctl)
270 self.mocker.result(True)
271 self.mocker.replay()
272 hooks.create_postgresql_config(config_outfile)
273@@ -279,79 +278,70 @@
274 ["hot_standby = True", "wal_level = hot_standby",
275 "max_wal_senders = 3", "wal_keep_segments = 1000"])
276
277- def test_create_postgresql_config_performance_tune_auto_large_ram(self):
278- """
279- When configuration attribute C{performance_tune} is set to C{auto} and
280- total RAM on a system is > 1023MB. It will automatically calculate
281- values for the following attributes if these attributes were left as
282- default values:
283- - C{effective_cache_size} set to 75% of total RAM in MegaBytes
284- - C{shared_buffers} set to 25% of total RAM in MegaBytes
285- - C{kernel_shmmax} set to total RAM in bytes
286- - C{kernel_shmall} equal to kernel_shmmax in pages
287- """
288- config_outfile = self.makeFile()
289- run = self.mocker.replace(hooks.run)
290- run("sysctl -p %s" % hooks.postgresql_sysctl)
291- self.mocker.result(True)
292- self.mocker.replay()
293- hooks.create_postgresql_config(config_outfile)
294- self.assertFileContains(
295- config_outfile,
296- ["shared_buffers = 256MB", "effective_cache_size = 768MB"])
297+ def test_auto_tuned_postgresql_config(self):
298+ """
299+ When automatic performance tuning is specified, pgtune will
300+ modify postgresql.conf. Automatic performance tuning is the default.
301+ """
302+ config_outfile = self.makeFile()
303+ _run_sysctl = self.mocker.replace(hooks._run_sysctl)
304+ _run_sysctl(hooks.postgresql_sysctl)
305+ self.mocker.result(True)
306+ self.mocker.replay()
307+
308+ hooks.create_postgresql_config(config_outfile)
309+
310+ raw_config = open(config_outfile, 'r').read()
311+ self.assert_('# pgtune wizard' in raw_config)
312+
313+ def test_auto_tuned_kernel_settings(self):
314+ """
315+ Kernel settings are automatically set to max RAM values
316+ """
317+ config_outfile = self.makeFile()
318+ _run_sysctl = self.mocker.replace(hooks._run_sysctl)
319+ _run_sysctl(hooks.postgresql_sysctl)
320+ self.mocker.result(True)
321+ self.mocker.replay()
322+
323+ hooks.create_postgresql_config(config_outfile)
324+
325 self.assertFileContains(
326 hooks.postgresql_sysctl,
327 ["kernel.shmall = 1025\nkernel.shmmax = 1073742848"])
328
329- def test_create_postgresql_config_performance_tune_auto_small_ram(self):
330- """
331- When configuration attribute C{performance_tune} is set to C{auto} and
332- total RAM on a system is <= 1023MB. It will automatically calculate
333- values for the following attributes if these attributes were left as
334- default values:
335- - C{effective_cache_size} set to 75% of total RAM in MegaBytes
336- - C{shared_buffers} set to 15% of total RAM in MegaBytes
337- - C{kernel_shmmax} set to total RAM in bytes
338- - C{kernel_shmall} equal to kernel_shmmax in pages
339- """
340- hooks._get_system_ram = lambda: 1023 # MB
341- config_outfile = self.makeFile()
342- run = self.mocker.replace(hooks.run)
343- run("sysctl -p %s" % hooks.postgresql_sysctl)
344- self.mocker.result(True)
345- self.mocker.replay()
346- hooks.create_postgresql_config(config_outfile)
347- self.assertFileContains(
348- config_outfile,
349- ["shared_buffers = 153MB", "effective_cache_size = 767MB"])
350- self.assertFileContains(
351- hooks.postgresql_sysctl,
352- ["kernel.shmall = 1024\nkernel.shmmax = 1072694272"])
353-
354- def test_create_postgresql_config_performance_tune_auto_overridden(self):
355- """
356- When configuration attribute C{performance_tune} is set to C{auto} any
357- non-default values for the configuration parameters below will be used
358- instead of the automatically calculated values.
359- - C{effective_cache_size}
360- - C{shared_buffers}
361- - C{kernel_shmmax}
362- - C{kernel_shmall}
363- """
364- hooks.hookenv._config["effective_cache_size"] = "999MB"
365- hooks.hookenv._config["shared_buffers"] = "101MB"
366- hooks.hookenv._config["kernel_shmmax"] = 50000
367- hooks.hookenv._config["kernel_shmall"] = 500
368- hooks._get_system_ram = lambda: 1023 # MB
369- config_outfile = self.makeFile()
370- run = self.mocker.replace(hooks.run)
371- run("sysctl -p %s" % hooks.postgresql_sysctl)
372- self.mocker.result(True)
373- self.mocker.replay()
374- hooks.create_postgresql_config(config_outfile)
375- self.assertFileContains(
376- config_outfile,
377- ["shared_buffers = 101MB", "effective_cache_size = 999MB"])
378- self.assertFileContains(
379- hooks.postgresql_sysctl,
380- ["kernel.shmall = 1024\nkernel.shmmax = 1072694272"])
381+ def test_auto_tuning_preserves_max_connections(self):
382+ """
383+ pgtune with choose max_connections unless you tell it not too
384+ """
385+ # Note that the charm does not yet make use of automatic
386+ # max_connections. We may want to change the default
387+ # max_connections to null and autotune then.
388+ hooks.hookenv._config["max_connections"] = 42
389+ config_outfile = self.makeFile()
390+ _run_sysctl = self.mocker.replace(hooks._run_sysctl)
391+ _run_sysctl(hooks.postgresql_sysctl)
392+ self.mocker.result(True)
393+ self.mocker.replay()
394+
395+ hooks.create_postgresql_config(config_outfile)
396+
397+ raw_config = open(config_outfile, 'r').read()
398+ self.assert_('\nmax_connections = 42\n' in raw_config)
399+
400+ def test_manually_tuned_postgresql_config(self):
401+ """
402+ When automatic performance tuning is specified, pgtune will
403+ modify postgresql.conf. Automatic performance tuning is the default.
404+ """
405+ hooks.hookenv._config["performance_tuning"] = "maNual"
406+ config_outfile = self.makeFile()
407+ _run_sysctl = self.mocker.replace(hooks._run_sysctl)
408+ _run_sysctl(hooks.postgresql_sysctl)
409+ self.mocker.result(True)
410+ self.mocker.replay()
411+
412+ hooks.create_postgresql_config(config_outfile)
413+
414+ raw_config = open(config_outfile, 'r').read()
415+ self.assert_('# pgtune wizard' not in raw_config)
416
417=== modified file 'test.py'
418--- test.py 2014-04-04 12:07:49 +0000
419+++ test.py 2014-04-04 12:07:49 +0000
420@@ -94,6 +94,10 @@
421 'database', None)
422 num_masters = 0
423 for unit, unit_rel_info in rel_info.items():
424+ if not unit_rel_info:
425+ raise NotReady(
426+ '{} {} is not setup'.format(
427+ unit, rel_id))
428 if not unit.startswith('postgresql/'):
429 continue
430 if 'user' not in unit_rel_info:
431
432=== modified file 'testing/jujufixture.py'
433--- testing/jujufixture.py 2014-04-04 12:07:49 +0000
434+++ testing/jujufixture.py 2014-04-04 12:07:49 +0000
435@@ -139,11 +139,18 @@
436 self, juju_run_cmd + [
437 'relation-list -r {}'.format(rel_id)]).split()
438 for rel_unit in relation_units:
439- json_rel_info = run(
440- self, juju_run_cmd + [
441- 'relation-get --format=json -r {} - {}'.format(
442- rel_id, rel_unit)])
443- res[rel_name][rel_id][rel_unit] = json.loads(json_rel_info)
444+ try:
445+ json_rel_info = run(
446+ self, juju_run_cmd + [
447+ 'relation-get --format=json -r {} - {}'.format(
448+ rel_id, rel_unit)])
449+ res[rel_name][rel_id][rel_unit] = json.loads(
450+ json_rel_info)
451+ except subprocess.CalledProcessError as x:
452+ if x.returncode == 2:
453+ res[rel_name][rel_id][rel_unit] = None
454+ else:
455+ raise
456 return res
457
458 def wait_until_ready(self, extra=60):
459@@ -238,27 +245,34 @@
460
461
462 def run(detail_collector, cmd, input=''):
463+ global _run_seq
464+ _run_seq = _run_seq + 1
465+
466+ # This helper primarily exists to capture the subprocess details,
467+ # but this is failing. Details are being captured, but those added
468+ # inside the actual test (not setup) are not being reported.
469+
470+ out, err, returncode = None, None, None
471 try:
472 proc = subprocess.Popen(
473 cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
474 stderr=subprocess.PIPE)
475- except subprocess.CalledProcessError:
476+ (out, err) = proc.communicate(input)
477+ returncode = proc.returncode
478+ if returncode != 0:
479+ raise subprocess.CalledProcessError(returncode, cmd, err)
480+ return out
481+ except subprocess.CalledProcessError as x:
482+ returncode = x.returncode
483 raise
484-
485- (out, err) = proc.communicate(input)
486- if detail_collector:
487- global _run_seq
488- _run_seq += 1
489- m = {
490- 'cmd': ' '.join(cmd),
491- 'rc': proc.returncode,
492- 'stdout': out,
493- 'stderr': err,
494- }
495- detail_collector.addDetail(
496- 'run_{}'.format(_run_seq),
497- text_content(yaml.safe_dump(m, default_flow_style=False)))
498- if proc.returncode != 0:
499- raise subprocess.CalledProcessError(
500- proc.returncode, cmd, err)
501- return out
502+ finally:
503+ if detail_collector is not None:
504+ m = {
505+ 'cmd': ' '.join(cmd),
506+ 'rc': returncode,
507+ 'stdout': out,
508+ 'stderr': err,
509+ }
510+ detail_collector.addDetail(
511+ 'run_{}'.format(_run_seq),
512+ text_content(yaml.safe_dump(m, default_flow_style=False)))
513
514=== modified file 'tests/00_setup.test'
515--- tests/00_setup.test 2014-02-07 07:33:23 +0000
516+++ tests/00_setup.test 2014-04-04 12:07:49 +0000
517@@ -8,5 +8,6 @@
518 python-psycopg2 \
519 python-testtools \
520 python-twisted-core \
521- python-yaml
522+ python-yaml \
523+ pgtune
524

Subscribers

People subscribed via source and target branches