From 26efcd0da51d580f68ead2ca13c38f58766f8a14 Mon Sep 17 00:00:00 2001 From: Brett <19863984+brettmilford@users.noreply.github.com> Date: Thu, 23 Sep 2021 17:50:10 +1000 Subject: [PATCH] Added watermark_scale_factor tuning (#609) * Added watermark_scale_factor tuning Adds a method for determining an appropriate vm.watermark_scale_factor value for baremetal systems. Implements: spec memory-fragmentation-tuning * WIP: Added watermark_scale_factor tuning - improving tests - implemented suggested changes * Changes based on feedback * Working tests * pep8 fixes * py2.7 fixes * py2.7 fixes * watermark as const * Reuse get_total_ram from core.host Removed get_memtotal implementation and tests * const WMARK_MAX Co-authored-by: Brett Milford --- charmhelpers/contrib/sysctl/__init__.py | 0 .../contrib/sysctl/watermark_scale_factor.py | 104 ++++++++++++++++++ tests/contrib/sysctl/__init__.py | 0 .../sysctl/test_watermark_scale_factor.py | 68 ++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 charmhelpers/contrib/sysctl/__init__.py create mode 100644 charmhelpers/contrib/sysctl/watermark_scale_factor.py create mode 100644 tests/contrib/sysctl/__init__.py create mode 100644 tests/contrib/sysctl/test_watermark_scale_factor.py diff --git a/charmhelpers/contrib/sysctl/__init__.py b/charmhelpers/contrib/sysctl/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/charmhelpers/contrib/sysctl/watermark_scale_factor.py b/charmhelpers/contrib/sysctl/watermark_scale_factor.py new file mode 100644 index 000000000..cfc9534fb --- /dev/null +++ b/charmhelpers/contrib/sysctl/watermark_scale_factor.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014-2015 Canonical Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from charmhelpers.core.hookenv import ( + log, + DEBUG, + ERROR, +) + +import re +from charmhelpers.core.host import get_total_ram + +WMARK_MAX = 1000 +WMARK_DEFAULT = 10 +MEMTOTAL_MIN_BYTES = 17179803648 # 16G +MAX_PAGES = 2500000000 + + +def calculate_watermark_scale_factor(): + """Calculates optimal vm.watermark_scale_factor value + + :returns: watermark_scale_factor + :rtype: int + """ + + memtotal = get_total_ram() + normal_managed_pages = get_normal_managed_pages() + + try: + wmark = min([watermark_scale_factor(memtotal, managed_pages) + for managed_pages in normal_managed_pages]) + except ValueError as e: + log("Failed to calculate watermark_scale_factor from normal managed pages: {}".format(normal_managed_pages), ERROR) + raise e + + log("vm.watermark_scale_factor: {}".format(wmark), DEBUG) + return wmark + + +def get_normal_managed_pages(): + """Parse /proc/zoneinfo for managed pages of the + normal zone on each node + + :returns: normal_managed_pages + :rtype: [int] + """ + try: + normal_managed_pages = [] + with open('/proc/zoneinfo', 'r') as f: + in_zone_normal = False + # regex to search for strings that look like "Node 0, zone Normal" and last string to group 1 + normal_zone_matcher = re.compile(r"^Node\s\d+,\s+zone\s+(\S+)$") + # regex to match to a number at the end of the line. + managed_matcher = re.compile(r"\s+managed\s+(\d+)$") + for line in f.readlines(): + match = normal_zone_matcher.search(line) + if match: + in_zone_normal = match.group(1) == 'Normal' + if in_zone_normal: + # match the number at the end of " managed 3840" into group 1. + managed_match = managed_matcher.search(line) + if managed_match: + normal_managed_pages.append(int(managed_match.group(1))) + in_zone_normal = False + + except OSError as e: + log("Failed to read /proc/zoneinfo in calculating watermark_scale_factor: {}".format(e), ERROR) + raise e + + return normal_managed_pages + + +def watermark_scale_factor(memtotal, managed_pages): + """Calculate a value for vm.watermark_scale_factor + + :param memtotal: Total system memory in KB + :type memtotal: int + :param managed_pages: Number of managed pages + :type managed_pages: int + :returns: normal_managed_pages + :rtype: int + """ + if memtotal <= MEMTOTAL_MIN_BYTES: + return WMARK_DEFAULT + else: + WMARK = int(MAX_PAGES / managed_pages) + if WMARK > WMARK_MAX: + return WMARK_MAX + else: + return WMARK diff --git a/tests/contrib/sysctl/__init__.py b/tests/contrib/sysctl/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/sysctl/test_watermark_scale_factor.py b/tests/contrib/sysctl/test_watermark_scale_factor.py new file mode 100644 index 000000000..add097391 --- /dev/null +++ b/tests/contrib/sysctl/test_watermark_scale_factor.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +from charmhelpers.contrib.sysctl.watermark_scale_factor import ( + watermark_scale_factor, + calculate_watermark_scale_factor, + get_normal_managed_pages, +) + +from mock import patch +import unittest + +from tests.helpers import patch_open + +TO_PATCH = [ + "log", + "ERROR", + "DEBUG" +] + +PROC_ZONEINFO = """ +Node 0, zone Normal + pages free 1253032 + min 16588 + low 40833 + high 65078 + spanned 24674304 + present 24674304 + managed 24247810 + protection: (0, 0, 0, 0, 0) +""" + + +class TestWatermarkScaleFactor(unittest.TestCase): + + def setUp(self): + for m in TO_PATCH: + setattr(self, m, self._patch(m)) + + def _patch(self, method): + _m = patch('charmhelpers.contrib.sysctl.watermark_scale_factor.' + method) + mock = _m.start() + self.addCleanup(_m.stop) + return mock + + @patch('charmhelpers.contrib.sysctl.watermark_scale_factor.get_normal_managed_pages') + @patch('charmhelpers.core.host.get_total_ram') + def test_calculate_watermark_scale_factor(self, get_total_ram, get_normal_managed_pages): + get_total_ram.return_value = 101254156288 + get_normal_managed_pages.return_value = [24247810] + wmark = calculate_watermark_scale_factor() + self.assertTrue(wmark >= 10, "ret {}".format(wmark)) + self.assertTrue(wmark <= 1000, "ret {}".format(wmark)) + + def test_get_normal_managed_pages(self): + with patch_open() as (mock_open, mock_file): + mock_file.readlines.return_value = PROC_ZONEINFO.splitlines() + self.assertEqual(get_normal_managed_pages(), [24247810]) + mock_open.assert_called_with('/proc/zoneinfo', 'r') + + def test_watermark_scale_factor(self): + mem_totals = [17179803648, 34359607296, 549753716736] + managed_pages = [4194288, 24247815, 8388576, 134217216] + arglists = [[mem, managed] for mem in mem_totals for managed in managed_pages] + + for arglist in arglists: + wmark = watermark_scale_factor(*arglist) + self.assertTrue(wmark >= 10, "assert failed for args: {}, ret {}".format(arglist, wmark)) + self.assertTrue(wmark <= 1000, "assert failed for args: {}, ret {}".format(arglist, wmark))