Skip to content

Commit 9eaeef1

Browse files
author
Alexander Popov
committed
Merge branch 'PGPRO-4333' into dev
2 parents 230d4e0 + 749ab1a commit 9eaeef1

File tree

4 files changed

+187
-0
lines changed

4 files changed

+187
-0
lines changed

mamonsu/lib/plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def is_enabled(self):
135135
return self._enabled
136136

137137
def disable(self):
138+
self._plugin_config['enabled'] = 'False'
138139
self._enabled = False
139140

140141
def set_sender(self, sender):

mamonsu/plugins/pgsql/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
__all__ += ['archive_command']
77
__all__ += ['prepared_transaction']
88
__all__ += ['relations_size']
9+
__all__ += ['memory_leak_diagnostic']
910

1011
from . import *
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from mamonsu.plugins.pgsql.plugin import PgsqlPlugin as Plugin
2+
import os
3+
from .pool import Pooler
4+
import re
5+
from distutils.version import LooseVersion
6+
import mamonsu.lib.platform as platform
7+
8+
9+
class MemoryLeakDiagnostic(Plugin):
10+
DEFAULT_CONFIG = {'enabled': 'False',
11+
'private_anon_mem_threshold': '1GB'}
12+
Interval = 60
13+
14+
query = 'select pid from pg_stat_activity'
15+
key_count_diff = 'pgsql.memory_leak_diagnostic.count_diff[]'
16+
key_count_diff_error = 'pgsql.memory_leak_diagnostic.msg_text[]'
17+
name_count_diff = 'PostgreSQL: number of pids which private anonymous memory exceeds ' \
18+
'private_anon_mem_threshold'
19+
name_count_diff_error = 'PostgreSQL: number of pids which private anonymous memory ' \
20+
'exceeds private_anon_mem_threshold, text of message'
21+
22+
def __init__(self, config):
23+
super(Plugin, self).__init__(config)
24+
if not platform.LINUX:
25+
self.disable()
26+
self.log.error('Plugin {name} work only on Linux. '.format(name=self.__class__.__name__))
27+
28+
if self.is_enabled():
29+
self.page_size = os.sysconf('SC_PAGE_SIZE')
30+
31+
private_anon_mem_threshold_row = self.plugin_config('private_anon_mem_threshold').upper()
32+
private_anon_mem_threshold, prefix = re.match(r'([0-9]*)([A-Z]*)',
33+
private_anon_mem_threshold_row, re.I).groups()
34+
ratio = 0
35+
36+
if prefix == 'MB':
37+
ratio = 1024 * 1024
38+
elif prefix == 'GB':
39+
ratio = 1024 * 1024 * 1024
40+
elif prefix == 'TB':
41+
ratio = 1024 * 1024 * 1024 * 1024
42+
else:
43+
self.disable()
44+
self.log.error('Error in config, section [{section}], parameter private_anon_mem_threshold. '
45+
'Possible values MB, GB, TB. For example 1GB.'
46+
.format(section=self.__class__.__name__.lower()))
47+
48+
self.diff = ratio * int(private_anon_mem_threshold)
49+
50+
self.os_release = os.uname().release
51+
os_release_file = '/etc/os-release'
52+
try:
53+
release_file = open(os_release_file, 'r').readlines()
54+
except Exception as e:
55+
self.log.info(f'Cannot read file {os_release_file} : {e}')
56+
release_file = None
57+
58+
if release_file:
59+
for line in release_file:
60+
if line.strip('"\n') != '':
61+
k, v = line.split('=', 1)
62+
if k == 'ID':
63+
self.os_name = v.strip('"\n')
64+
elif k == 'VERSION_ID':
65+
self.os_version = v.strip('"\n')
66+
else:
67+
self.os_name = None
68+
self.os_version = None
69+
70+
def run(self, zbx):
71+
pids = []
72+
count_diff = 0
73+
diffs = []
74+
msg_text = ''
75+
76+
for row in Pooler.query(query=self.query):
77+
pids.append(row[0])
78+
79+
if (LooseVersion(self.os_release) < LooseVersion("4.5")
80+
and not (self.os_name == 'centos' and self.os_version == '7'))\
81+
or (not self.os_name and not self.os_version):
82+
for pid in pids:
83+
try:
84+
statm = open(f'/proc/{pid}/statm', 'r').read().split(' ')
85+
except FileNotFoundError:
86+
continue
87+
88+
RES = int(statm[1]) * self.page_size
89+
SHR = int(statm[2]) * self.page_size
90+
if RES - SHR > self.diff:
91+
count_diff += 1
92+
diffs.append({'pid': pid, 'RES': RES, 'SHR': SHR, 'diff': self.diff})
93+
if diffs:
94+
for diff in diffs:
95+
msg_text += 'pid: {pid}, RES {RES} - SHR {SHR} more then {diff}\n'.format_map(diff)
96+
else:
97+
for pid in pids:
98+
try:
99+
statm = open(f'/proc/{pid}/status', 'r').readlines()
100+
except FileNotFoundError:
101+
continue
102+
103+
for line in statm:
104+
VmRSS = 0
105+
RssAnon = 0
106+
RssFile = 0
107+
RssShmem = 0
108+
k, v = line.split(':\t', 1)
109+
110+
if k == 'VmRSS':
111+
VmRSS = int(v.strip('"\n\t ').split(' ')[0]) * 1024
112+
elif k == 'RssAnon':
113+
RssAnon = int(v.strip('"\n\t ').split(' ')[0]) * 1024
114+
elif k == 'RssFile':
115+
RssFile = int(v.strip('"\n\t ').split(' ')[0]) * 1024
116+
elif k == 'RssShmem':
117+
RssShmem = int(v.strip('"\n\t ').split(' ')[0]) * 1024
118+
if RssAnon > self.diff:
119+
count_diff += 1
120+
diffs.append(
121+
{'pid': pid, 'VmRSS': VmRSS, 'RssAnon': RssAnon, 'RssFile': RssFile, 'RssShmem': RssShmem,
122+
'diff': self.diff})
123+
if diffs:
124+
for diff in diffs:
125+
msg_text += 'pid: {pid}, RssAnon {RssAnon} more then {diff}, VmRSS {VmRSS}, ' \
126+
'RssFile {RssFile}, RssShmem {RssShmem} \n'.format_map(diff)
127+
128+
zbx.send(self.key_count_diff, int(count_diff))
129+
zbx.send(self.key_count_diff_error, msg_text)
130+
131+
def items(self, template):
132+
result = template.item(
133+
{
134+
'name': self.name_count_diff,
135+
'key': self.key_count_diff,
136+
'delay': self.plugin_config('interval')
137+
}
138+
)
139+
result += template.item(
140+
{
141+
'name': self.name_count_diff_error,
142+
'key': self.key_count_diff_error,
143+
'delay': self.plugin_config('interval'),
144+
'value_type': Plugin.VALUE_TYPE.text
145+
}
146+
)
147+
return result
148+
149+
def graphs(self, template):
150+
result = template.graph(
151+
{
152+
'name': self.name_count_diff,
153+
'items': [
154+
{
155+
'key': self.key_count_diff,
156+
'color': 'FF0000'
157+
}
158+
]
159+
}
160+
)
161+
return result
162+
163+
def triggers(self, template):
164+
result = template.trigger(
165+
{
166+
'name': self.name_count_diff + ' on {HOSTNAME}. {ITEM.LASTVALUE}',
167+
'expression': '{{#TEMPLATE:{name}.strlen()'
168+
'}}&gt;1'.format(name=self.key_count_diff_error)
169+
})
170+
return result

packaging/conf/example.conf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,18 @@ enabled = False
196196
relations=postgres.pg_catalog.pg_class,postgres.pg_catalog.pg_user
197197
interval = 300
198198

199+
# This plugin allows detects possible memory leaks while working with PostgreSQL using /proc/pid/status and /proc/pid/statm
200+
# We use RES and SHR difference to calculate approximate volume of private anonymous backend memory.
201+
# If it exceeds private_anon_mem_threshold then that pid will be added to a message. An example is presented below
202+
# statm - 'pid: {pid}, RES {RES} - SHR {SHR} more then {private_anon_mem_threshold}\n'
203+
# Since Linux 4.5 RssAnon, RssFile and RssShmem have been added.
204+
# They allows to distinguish types of memory such as private anonymous, file-backed, and shared anonymous memory.
205+
# We are interested in RssAnon. If its value exceeds private_anon_mem_threshold then that pid will also be added to a message.
206+
# By default this plugin disabled. To enable this plugin - set bellow "enabled = False"
207+
# #interval - (onitoring frequency in seconds. 60 seconds by default
208+
# private_anon_mem_threshold - memory volume threshold after which we need an investigation about memory leak. 1GB by default.
209+
# Possible values MB, GB, TB. For example 1GB
210+
[memoryleakdiagnostic]
211+
enabled = False
212+
interval = 60
213+
private_anon_mem_threshold = 1GB

0 commit comments

Comments
 (0)