· 7 years ago · Dec 20, 2018, 11:52 AM
1#!/usr/bin/perl
2
3# Copyright 2012 - Jean-Sebastien Morisset - http://surniaulula.com/
4#
5# This script is free software; you can redistribute it and/or modify it under
6# the terms of the GNU General Public License as published by the Free Software
7# Foundation; either version 3 of the License, or (at your option) any later
8# version.
9#
10# This script is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13# details at http://www.gnu.org/licenses/.
14
15# Perl script to compare the size of running Apache httpd processes, the
16# configured prefork/worker limits, and the available server memory. Exits with
17# a warning or error message if the configured limits exceed the server's
18# memory.
19#
20# Syntax: check_httpd_limits.pl --help
21
22# The script performs the following tasks:
23#
24# - Reads the /proc/meminfo file for server memory values.
25# - Reads the /proc/*/exe symbolic links to find the matching httpd binaries.
26# - Reads the /proc/*/stat files for pid, process name, ppid, and rss.
27# - Reads the /proc/*/statm files for the shared memory size.
28# - Executes HTTP binary with "-V" to get the config file path and MPM info.
29# - Reads the HTTP config file to get MPM (prefork or worker) settings.
30# - Calculates the average and total HTTP process sizes, taking into account
31# the shared memory used.
32# - Calculates possible changes to MPM settings based on available memory and
33# process sizes.
34# - Displays all the values found and settings calculated if the --verbose
35# parameter is used.
36# - Exits with OK (0), WARNING (1), or ERROR (2) based on projected memory use
37# with all (allowed) HTTP processes running.
38# OK: Maximum number of HTTP processes fit within available RAM.
39# WARNING: Maximum number of HTTP processes exceeds available RAM, but still
40# fits within the free swap.
41# ERROR: Maximum number of HTTP processes exceeds available RAM and swap.
42
43# Changes:
44#
45# v2.4:
46# - Added config for Apache Httpd v2.5 and 2.6 (identical to 2.4).
47# - Added config for 'eventopt' MPM (identical to 'event' MPM).
48#
49# v2.5:
50# - Added 'config' command-line argument.
51# - Re-arranged search path for httpd binary.
52
53use strict;
54use warnings;
55use POSIX;
56use Getopt::Long;
57
58no warnings 'once'; # no warning for $DBI::err
59
60my $VERSION = '2.5';
61my $pagesize = POSIX::sysconf(POSIX::_SC_PAGESIZE);
62my @stathrefs;
63my $err = 0;
64my %mem = (
65 'MemTotal' => '',
66 'MemFree' => '',
67 'Cached' => '',
68 'SwapTotal' => '',
69 'SwapFree' => '',
70);
71my %httpd = (
72 'EXE' => '',
73 'ROOT' => '',
74 'CONFIG' => '',
75 'MPM' => '',
76 'VERSION' => '',
77);
78my $cf_IfModule = '';
79my $cf_MaxName = ''; # defined based on httpd version (MaxClients or MaxRequestWorkers)
80my $cf_LimitName = ''; # defined once MPM is determined (MaxClients/MaxRequestWorkers or ServerLimit)
81my $cf_ver = '';
82my $cf_min = '2.2';
83my $cf_mpm = '';
84my %cf_read = ();
85my %cf_changed = ();
86my %cf_defaults = (
87 '2.2' => {
88 'prefork' => {
89 'StartServers' => 5,
90 'MinSpareServers' => 5,
91 'MaxSpareServers' => 10,
92 'ServerLimit' => 256,
93 'MaxClients' => 256,
94 'MaxRequestsPerChild' => 10000,
95 },
96 'worker' => {
97 'StartServers' => 3,
98 'MinSpareThreads' => 75,
99 'MaxSpareThreads' => 250,
100 'ThreadsPerChild' => 25,
101 'ServerLimit' => 16,
102 'MaxClients' => 400,
103 'MaxRequestsPerChild' => 10000,
104 },
105 },
106 '2.4' => {
107 'prefork' => {
108 'StartServers' => 5,
109 'MinSpareServers' => 5,
110 'MaxSpareServers' => 10,
111 'ServerLimit' => 256,
112 'MaxRequestWorkers' => 256, # aka MaxClients
113 'MaxConnectionsPerChild' => 0, # aka MaxRequestsPerChild
114 },
115 'worker' => {
116 'StartServers' => 3,
117 'MinSpareThreads' => 75,
118 'MaxSpareThreads' => 250,
119 'ThreadsPerChild' => 25,
120 'ServerLimit' => 16,
121 'MaxRequestWorkers' => 400, # aka MaxClients
122 'MaxConnectionsPerChild' => 0, # aka MaxRequestsPerChild
123 },
124 },
125);
126$cf_defaults{'2.5'} = $cf_defaults{'2.4'};
127$cf_defaults{'2.6'} = $cf_defaults{'2.5'};
128
129# The event MPM config is identical to the worker MPM config
130# Uses a hashref instead of copying the hash elements
131for my $ver ( keys %cf_defaults ) {
132 $cf_defaults{$ver}{'event'} = $cf_defaults{$ver}{'worker'};
133 $cf_defaults{$ver}{'eventopt'} = $cf_defaults{$ver}{'event'};
134}
135# easiest way to copy the three-dimensional hash without using a module
136for my $ver ( keys %cf_defaults ) {
137 for my $mpm ( keys %{$cf_defaults{$ver}} ) {
138 for my $el ( keys %{$cf_defaults{$ver}{$mpm}} ) {
139 $cf_read{$ver}{$mpm}{$el} = $cf_defaults{$ver}{$mpm}{$el};
140 $cf_changed{$ver}{$mpm}{$el} = $cf_defaults{$ver}{$mpm}{$el};
141 }
142 }
143}
144my %cf_comments = (
145 '2.2' => {
146 'prefork' => {
147 'ServerLimit' => 'MaxClients',
148 'MaxClients' => '(MemFree + Cached + HttpdRealTot + HttpdSharedAvg) / HttpdRealAvg',
149 },
150 'worker' => {
151 'ServerLimit' => '(MemFree + Cached + HttpdRealTot + HttpdSharedAvg) / HttpdRealAvg',
152 'MaxClients' => 'ServerLimit * ThreadsPerChild',
153 },
154 },
155 '2.4' => {
156 'prefork' => {
157 'ServerLimit' => 'MaxRequestWorkers',
158 'MaxRequestWorkers' => '(MemFree + Cached + HttpdRealTot + HttpdSharedAvg) / HttpdRealAvg',
159 },
160 'worker' => {
161 'ServerLimit' => '(MemFree + Cached + HttpdRealTot + HttpdSharedAvg) / HttpdRealAvg',
162 'MaxRequestWorkers' => 'ServerLimit * ThreadsPerChild',
163 },
164 },
165);
166$cf_comments{'2.5'} = $cf_comments{'2.4'};
167$cf_comments{'2.6'} = $cf_comments{'2.5'};
168
169# the event MPM config is identical to the worker MPM config
170# uses a hashref instead of copying the hash elements
171for my $ver ( keys %cf_comments ) {
172 $cf_comments{$ver}{'event'} = $cf_comments{$ver}{'worker'};
173 $cf_comments{$ver}{'eventopt'} = $cf_comments{$ver}{'event'};
174}
175my %calcs = (
176 'HttpdRealAvg' => 0,
177 'HttpdSharedAvg' => 0,
178 'HttpdRealTot' => 0,
179 'HttpdRunning' => 0,
180 'OtherProcsMem' => '',
181 'FreeMemNoHttpd' => '',
182 'MaxLimitHttpdMem' => '',
183 'AllProcsTotalMem' => '',
184);
185
186# comment string when MaxLimitHttpdMem is calculated from DB values
187my $mcs_from_db = '';
188
189# common location for httpd binaries if not sepcified on command-line
190my @httpd_paths = (
191 '/usr/sbin/httpd',
192 '/usr/sbin/apache2',
193 '/usr/local/sbin/httpd',
194 '/usr/local/sbin/apache2',
195 '/opt/apache/bin/httpd',
196 '/opt/apache/sbin/httpd',
197 '/usr/lib/apache2/mpm-prefork/apache2',
198 '/usr/lib/apache2/mpm-worker/apache2',
199);
200my $dbname = '/var/tmp/check_httpd_limits.sqlite';
201my $dbuser = '';
202my $dbpass = '';
203my $dbtable = 'HttpdProcInfo';
204my $dsn = "DBI:SQLite:dbname=$dbname";
205my $dbh;
206my %dbrow = (
207 'DateTimeAdded' => 0,
208 'HttpdRealAvg' => 0,
209 'HttpdSharedAvg' => 0,
210 'HttpdRealTot' => 0,
211 'HttpdRunning' => 0,
212);
213my %opt = ();
214GetOptions(\%opt,
215 'help',
216 'debug',
217 'verbose',
218 'exe=s',
219 'config=s',
220 'swappct=i',
221 'save',
222 'days=i',
223 'max=s',
224);
225$opt{'swappct'} = 0 unless ( $opt{'swappct'} );
226$opt{'max'} = $opt{'max'} ? lc($opt{'max'}) : "";
227&ShowUsage() if ( $opt{'help'} );
228
229if ( $opt{'verbose'} ) {
230 print "\nCheck Apache Httpd MPM Config Limits (Version $VERSION)\n";
231 print "by Jean-Sebastien Morisset - http://surniaulula.com/\n\n";
232}
233
234#
235# READ MAXIMUM FROM DATABASE
236#
237if ( $opt{'save'} || $opt{'days'} || $opt{'max'} ) {
238 $opt{'days'} = 30 unless ( defined $opt{'days'} );
239 print "Saving Httpd Averages to $dsn\n\n"
240 if ( $opt{'save'} && $opt{'verbose'} );
241
242 require DBD::SQLite;
243 print "DEBUG: Connecting to database $dsn.\n" if ( $opt{'debug'} );
244 $dbh = DBI->connect($dsn, $dbuser, $dbpass);
245 die "ERROR: $DBI::errstr\n" if ($DBI::err);
246
247 $dbh->do("PRAGMA foreign_keys = ON;");
248
249 $dbh->do("CREATE TABLE IF NOT EXISTS $dbtable (
250 DateTimeAdded DATE PRIMARY KEY,
251 HttpdRealAvg INTEGER NOT NULL,
252 HttpdSharedAvg INTEGER NOT NULL,
253 HttpdRealTot INTEGER NOT NULL,
254 HttpdRunning INTEGER NOT NULL);");
255
256 # Use an array instead of a hash to keep the column order. If you're
257 # using MySQL, you may want to add an 'AFTER ColumnName' to the
258 # definiton string. 'AFTER' is not supported by SQLite, so always add
259 # new columns to the end of the array.
260 my @dbcol = (
261 { 'name' => 'DateTimeAdded', 'definition' => 'DATE', },
262 { 'name' => 'HttpdRealAvg', 'definition' => 'INTEGER', },
263 { 'name' => 'HttpdSharedAvg', 'definition' => 'INTEGER', },
264 { 'name' => 'HttpdRealTot', 'definition' => 'INTEGER', },
265 { 'name' => 'HttpdRunning', 'definition' => 'INTEGER', },
266 );
267 my @dbidx = (
268 { 'name' => 'HttpdRealAvgIdx', 'table' => 'HttpdRealAvg', },
269 { 'name' => 'HttpdRunningIdx', 'table' => 'HttpdRunning', },
270 );
271 # Use hashes to quickly define (and lookup) which tables/indexes already exist.
272 my %dbcol_exists = ();
273 my %dbidx_exists = ();
274 for ( @{ $dbh->selectall_arrayref( "PRAGMA TABLE_INFO($dbtable)") } ) { $dbcol_exists{$_->[1]} = 1; };
275 for ( @{ $dbh->selectall_arrayref( "PRAGMA INDEX_LIST($dbtable)") } ) { $dbidx_exists{$_->[1]} = 1; };
276
277 # Create any missing columns.
278 for my $col ( @dbcol ) {
279 unless ( $dbcol_exists{$col->{'name'}} ) {
280 print "DEBUG: Adding missing column $col->{'name'} as $col->{'definition'}.\n" if ( $opt{'debug'} );
281 $dbh->do("ALTER TABLE $dbtable ADD COLUMN $col->{'name'} $col->{'definition'};");
282 $dbh->do("UPDATE $dbtable SET $col->{'name'} = 0 WHERE $col->{'name'} = NULL;");
283 }
284 }
285
286 # Create any missing indexes.
287 for my $idx ( @dbidx ) {
288 unless ( $dbidx_exists{$idx->{'name'}} ) {
289 print "DEBUG: Adding missing index $idx->{'name'} for $idx->{'table'}.\n" if ( $opt{'debug'} );
290 $dbh->do("CREATE INDEX $idx->{'name'} ON $dbtable ($idx->{'table'});");
291 }
292 }
293
294 print "DEBUG: Removing DB rows older than $opt{'days'} days.\n" if ( $opt{'debug'} );
295 $dbh->do("DELETE FROM $dbtable WHERE DateTimeAdded < DATETIME('NOW', '-$opt{'days'} DAYS');");
296
297 if ( $opt{'max'} eq 'realavg' ) {
298
299 print "DEBUG: Selecting largest HttpdRealAvg value in past $opt{'days'} days.\n" if ( $opt{'debug'} );
300 ( $dbrow{'DateTimeAdded'}, $dbrow{'HttpdRealAvg'}, $dbrow{'HttpdSharedAvg'}, $dbrow{'HttpdRealTot'}, $dbrow{'HttpdRunning'} ) =
301 $dbh->selectrow_array("SELECT DateTimeAdded, HttpdRealAvg, HttpdSharedAvg, HttpdRealTot, HttpdRunning
302 FROM $dbtable ORDER BY HttpdRealAvg DESC, DateTimeAdded DESC LIMIT 1;");
303
304 } elsif ( $opt{'max'} eq 'running' ) {
305
306 print "DEBUG: Selecting largest HttpdRunning value in past $opt{'days'} days.\n" if ( $opt{'debug'} );
307 ( $dbrow{'DateTimeAdded'}, $dbrow{'HttpdRealAvg'}, $dbrow{'HttpdSharedAvg'}, $dbrow{'HttpdRealTot'}, $dbrow{'HttpdRunning'} ) =
308 $dbh->selectrow_array("SELECT DateTimeAdded, HttpdRealAvg, HttpdSharedAvg, HttpdRealTot, HttpdRunning
309 FROM $dbtable ORDER BY HttpdRunning DESC, HttpdRealAvg DESC, DateTimeAdded DESC LIMIT 1;");
310 }
311
312 if ( $opt{'max'} && %dbrow ) {
313 # make sure HttpdRunning (a column added later) has a value
314 $dbrow{'HttpdRunning'} = 0 unless( $dbrow{'HttpdRunning'} );
315 if ( $opt{'debug'} ) {
316 print "DEBUG: DateTimeAdded=$dbrow{'DateTimeAdded'}\n";
317 print "DEBUG: HttpdRealAvg=$dbrow{'HttpdRealAvg'}\n";
318 print "DEBUG: HttpdSharedAvg=$dbrow{'HttpdSharedAvg'}\n";
319 print "DEBUG: HttpdRealTot=$dbrow{'HttpdRealTot'}\n";
320 print "DEBUG: HttpdRunning=$dbrow{'HttpdRunning'}\n";
321 }
322 }
323
324}
325
326# ---------------------------
327# READ THE SERVER MEMORY INFO
328# ---------------------------
329#
330print "DEBUG: Open /proc/meminfo\n" if ( $opt{'debug'} );
331open ( my $mem_fh, "<", "/proc/meminfo" ) or die "ERROR: /proc/meminfo - $!\n";
332while (<$mem_fh>) {
333 if ( /^[[:space:]]*([a-zA-Z]+):[[:space:]]+([0-9]+)/) {
334 if ( defined $mem{$1} ) {
335 $mem{$1} = sprintf ( "%0.2f", $2 / 1024 );
336 print "DEBUG: Found $1 = $mem{$1}.\n" if ( $opt{'debug'} );
337 }
338 }
339}
340close ( $mem_fh );
341
342# -----------------------
343# LOCATE THE HTTPD BINARY
344# -----------------------
345#
346if ( defined $opt{'exe'} ) {
347 $httpd{'EXE'} = $opt{'exe'};
348 print "DEBUG: Command-Line Exe \"$httpd{'EXE'}\".\n"
349 if ( $opt{'debug'} );
350} else {
351 for ( @httpd_paths ) {
352 if ( $_ && -x $_ ) {
353 $httpd{'EXE'} = $_;
354 print "DEBUG: Found Httpd Exe \"$httpd{'EXE'}\".\n"
355 if ( $opt{'debug'} );
356 last;
357 }
358 }
359}
360die "ERROR: No executable Apache HTTP binary found!\n"
361 unless ( defined $httpd{'EXE'} && -x $httpd{'EXE'} );
362
363# -----------------------------------------
364# READ PROCESS INFORMATION FOR HTTPD BINARY
365# -----------------------------------------
366#
367print "DEBUG: Opendir /proc\n" if ( $opt{'debug'} );
368opendir ( my $proc_fh, "/proc" ) or die "ERROR: /proc - $!\n";
369while ( my $pid = readdir( $proc_fh ) ) {
370 my $exe = readlink( "/proc/$pid/exe" );
371 next unless ( defined $exe );
372 print "DEBUG: Readlink /proc/$pid/exe ($exe)" if ( $opt{'debug'} );
373 if ( $exe eq $httpd{'EXE'} ) {
374 print " - matched ($httpd{'EXE'})\n" if ( $opt{'debug'} );
375 print "DEBUG: Open /proc/$pid/stat\n" if ( $opt{'debug'} );
376 open ( my $stat_fh, "<", "/proc/$pid/stat" ) or die "ERROR: /proc/$pid/stat - $!\n";
377 my @pid_stat = split (/ /, readline( $stat_fh )); close ( $stat_fh );
378
379 print "DEBUG: Open /proc/$pid/statm\n" if ( $opt{'debug'} );
380 open ( my $statm_fh, "<", "/proc/$pid/statm" ) or die "ERROR: /proc/$pid/statm - $!\n";
381 my @pid_statm = split (/ /, readline( $statm_fh )); close ( $statm_fh );
382
383 my %all_stats = (
384 'pid' => $pid_stat[0],
385 'name' => $pid_stat[1],
386 'ppid' => $pid_stat[3],
387 'rss' => $pid_stat[23] * $pagesize / 1024 / 1024,
388 'share' => $pid_statm[2] * $pagesize / 1024 / 1024,
389 );
390 if ( $opt{'debug'} ) {
391 print "DEBUG:";
392 for (sort keys %all_stats) { print " $_:$all_stats{$_}"; }
393 print "\n";
394 }
395 push ( @stathrefs, \%all_stats );
396 } else { print "\n" if ( $opt{'debug'} ); }
397}
398close ( $proc_fh );
399die "ERROR: No $httpd{'EXE'} processes found in /proc/*/exe! Are you root?\n"
400 unless ( @stathrefs );
401
402# -------------------------------------
403# READ THE HTTPD BINARY COMPILED VALUES
404# -------------------------------------
405#
406print "DEBUG: Open $httpd{'EXE'} -V\n" if ( $opt{'debug'} );
407open ( my $set_fh, "-|", "$httpd{'EXE'} -V" ) or die "ERROR: $httpd{'EXE'} - $!\n";
408while ( <$set_fh> ) {
409 $httpd{'ROOT'} = $1 if (/^.*HTTPD_ROOT="(.*)"$/);
410 $httpd{'CONFIG'} = $1 if (/^.*SERVER_CONFIG_FILE="(.*)"$/);
411 $httpd{'VERSION'} = $1 if (/^Server version:[[:space:]]+Apache\/([0-9]\.[0-9]).*$/);
412 $httpd{'MPM'} = lc($1) if (/^Server MPM:[[:space:]]+(.*)$/);
413 $httpd{'MPM'} = lc($1) if (/APACHE_MPM_DIR="server\/mpm\/([^"]*)"$/);
414}
415close ( $set_fh );
416
417if ( $opt{'debug'} ) {
418 print "DEBUG: HTTPD ROOT = $httpd{'ROOT'}\n";
419 print "DEBUG: HTTPD CONFIG = $httpd{'CONFIG'}\n";
420 print "DEBUG: HTTPD VERSION = $httpd{'VERSION'}\n";
421 print "DEBUG: HTTPD MPM = $httpd{'MPM'}\n";
422}
423
424if ( $opt{'config'} ) {
425 $httpd{'CONFIG'} = $opt{'config'};
426 print "DEBUG: Command-Line Config \"$httpd{'CONFIG'}\".\n"
427 if ( $opt{'debug'} );
428
429}
430
431# check for relative path
432if ( $httpd{'CONFIG'} !~ /^\// ) {
433 $httpd{'CONFIG'} = "$httpd{'ROOT'}/$httpd{'CONFIG'}";
434 print "DEBUG: Relative Path Adjusted = $httpd{'CONFIG'}\n"
435 if ( $opt{'debug'} );
436}
437
438die "ERROR: Cannot determine httpd version number.\n"
439 unless ( $httpd{'VERSION'} && $httpd{'VERSION'} > 0 );
440
441die "ERROR: Cannot determine httpd server MPM type.\n"
442 unless ( $httpd{'MPM'} );
443
444# determine the config version number to use
445if ( $cf_defaults{$httpd{'VERSION'}} ) {
446 $cf_ver = $httpd{'VERSION'};
447} elsif ( $httpd{'VERSION'} < $cf_min ) {
448 $cf_ver = $cf_min;
449 print "INFO: Httpd version $httpd{'VERSION'} not configured - using $cf_ver values instead.\n";
450} else {
451 die "ERROR: Httpd version $httpd{'VERSION'} configuration values not defined.\n";
452}
453
454if ( $cf_defaults{$cf_ver}{$httpd{'MPM'}} ) { $cf_mpm = $httpd{'MPM'}; }
455else { die "ERROR: Httpd server MPM \"$httpd{'MPM'}\" is unknown.\n"; }
456
457# --------------------------
458# READ THE HTTPD CONFIG FILE
459# --------------------------
460#
461print "DEBUG: Open $httpd{'CONFIG'}\n" if ( $opt{'debug'} );
462open ( my $conf_fh, "<", $httpd{'CONFIG'} ) or die "ERROR: $httpd{'CONFIG'} - $!\n";
463my $conf = do { local $/; <$conf_fh> };
464close ( $conf_fh );
465
466# Read the MPM config values
467if ( $conf =~ /^[[:space:]]*<IfModule ($cf_mpm\.c|mpm_$cf_mpm\_module)>([^<]*)/im ) {
468 $cf_IfModule = $1; my $cf_Content = $2;
469 print "DEBUG: IfModule $cf_IfModule\n$cf_Content\n" if ( $opt{'debug'} );
470 for ( split (/\n/, $cf_Content) ) {
471 if ( /^[[:space:]]*([a-zA-Z]+)[[:space:]]+([0-9]+)/) {
472 print "DEBUG: $1 = $2\n" if ( $opt{'debug'} );
473 $cf_read{$cf_ver}{$cf_mpm}{$1} = $2;
474 $cf_changed{$cf_ver}{$cf_mpm}{$1} = $2;
475 }
476 }
477}
478
479if ( $cf_ver <= $cf_min ) {
480 $cf_MaxName = 'MaxClients';
481} else {
482 $cf_MaxName = 'MaxRequestWorkers';
483 my %dep = (
484 'MaxClients' => 'MaxRequestWorkers',
485 'MaxRequestsPerChild' => 'MaxConnectionsPerChild',
486 );
487 for ( sort keys %dep ) {
488 if ( defined $cf_read{$cf_ver}{$cf_mpm}{$_} ) {
489 print "INFO: $_($cf_read{$cf_ver}{$cf_mpm}{$_}) is deprecated - renaming to $dep{$_}.\n";
490 $cf_read{$cf_ver}{$cf_mpm}{$dep{$_}} = $cf_read{$cf_ver}{$cf_mpm}{$_};
491 $cf_changed{$cf_ver}{$cf_mpm}{$dep{$_}} = $cf_changed{$cf_ver}{$cf_mpm}{$_};
492 delete $cf_read{$cf_ver}{$cf_mpm}{$_};
493 delete $cf_changed{$cf_ver}{$cf_mpm}{$_};
494 }
495 }
496}
497
498# If using prefork MPM, base the caculation on MaxClients/MaxRequestWorkers instead of ServerLimit
499# When using prefork, MaxClients/MaxRequestWorkers determines how many processes can be started
500$cf_LimitName = $cf_mpm eq 'prefork' ? $cf_MaxName : 'ServerLimit';
501
502# Exit with an error if any value is not > 0
503for my $set ( sort keys %{$cf_changed{$cf_ver}{$cf_mpm}} ) {
504 die "ERROR: $set value is 0 in $httpd{'CONFIG'}!\n"
505 unless ( $cf_changed{$cf_ver}{$cf_mpm}{$set} > 0 ||
506 $set =~ /^(MaxRequestsPerChild|MaxConnectionsPerChild)$/ );
507}
508
509# -----------------------
510# CALCULATE SIZE AVERAGES
511# -----------------------
512#
513my @procs;
514for my $stref ( @stathrefs ) {
515
516 my $real = ${$stref}{'rss'} - ${$stref}{'share'};
517 my $share = ${$stref}{'share'};
518 my $proc_msg = sprintf ( " - %-22s: %7.2f MB / %6.2f MB shared",
519 "PID ${$stref}{'pid'} ${$stref}{'name'}", ${$stref}{'rss'}, $share );
520
521 if ( ${$stref}{'ppid'} > 1 ) {
522 $calcs{'HttpdRealAvg'} = $real if ( $calcs{'HttpdRealAvg'} == 0 );
523 $calcs{'HttpdSharedAvg'} = $share if ( $calcs{'HttpdSharedAvg'} == 0 );
524 $calcs{'HttpdRealAvg'} = ( $calcs{'HttpdRealAvg'} + $real ) / 2;
525 $calcs{'HttpdSharedAvg'} = ( $calcs{'HttpdSharedAvg'} + $share ) / 2;
526 } else {
527 $proc_msg .= " [excluded from averages]";
528 }
529 $calcs{'HttpdRealTot'} += $real;
530 print "DEBUG: $proc_msg\n" if ( $opt{'debug'} );
531 print "DEBUG: Avg $calcs{'HttpdRealAvg'}, Shr $calcs{'HttpdSharedAvg'}, Tot $calcs{'HttpdRealTot'}\n" if ( $opt{'debug'} );
532 push ( @procs, $proc_msg);
533}
534
535# round off the calcs
536$calcs{'HttpdRealAvg'} = sprintf ( "%0.2f", $calcs{'HttpdRealAvg'} );
537$calcs{'HttpdSharedAvg'} = sprintf ( "%0.2f", $calcs{'HttpdSharedAvg'} );
538$calcs{'HttpdRealTot'} = sprintf ( "%0.2f", $calcs{'HttpdRealTot'} );
539$calcs{'HttpdRunning'} = $#procs + 1;
540
541# save the new averages to the database
542if ( $opt{'save'} ) {
543 if ( $opt{'debug'} ) {
544 print "DEBUG: Adding to database: HttpdRealAvg($calcs{'HttpdRealAvg'}), ";
545 print "HttpdSharedAvg($calcs{'HttpdSharedAvg'}), HttpdRealTot($calcs{'HttpdRealTot'}), ";
546 print "HttpdRunning($calcs{'HttpdRunning'}).\n"
547 }
548 my $sth = $dbh->prepare( "INSERT INTO $dbtable VALUES ( DATETIME('NOW'), ?, ?, ?, ? )" );
549 $sth->execute( $calcs{'HttpdRealAvg'}, $calcs{'HttpdSharedAvg'}, $calcs{'HttpdRealTot'}, $calcs{'HttpdRunning'} );
550 $sth->finish;
551}
552if ( $opt{'save'} || $opt{'days'} || $opt{'max'} ) {
553 print "DEBUG: Disconnecting from database." if ( $opt{'debug'} );
554 $dbh->disconnect;
555}
556
557# use max averages from database if --max used (and the database average is larger than current)
558if ( $opt{'max'} eq 'realavg' && $dbrow{'HttpdRealAvg'} && $dbrow{'HttpdSharedAvg'} && $dbrow{'HttpdRealAvg'} > $calcs{'HttpdRealAvg'} ) {
559 $mcs_from_db = " [Avg from $dbrow{'DateTimeAdded'}]";
560 $calcs{'MaxLimitHttpdMem'} = $dbrow{'HttpdRealAvg'} * $cf_changed{$cf_ver}{$cf_mpm}{$cf_LimitName} + $dbrow{'HttpdSharedAvg'};
561 print "DEBUG: DB HttpdRealAvg: $dbrow{'HttpdRealAvg'} > Current HttpdRealAvg: $calcs{'HttpdRealAvg'}.\n" if ( $opt{'debug'} );
562} else {
563 $calcs{'MaxLimitHttpdMem'} = $calcs{'HttpdRealAvg'} * $cf_changed{$cf_ver}{$cf_mpm}{$cf_LimitName} + $calcs{'HttpdSharedAvg'};
564}
565
566$calcs{'OtherProcsMem'} = $mem{'MemTotal'} - $mem{'Cached'} - $mem{'MemFree'} - $calcs{'HttpdRealTot'} - $calcs{'HttpdSharedAvg'};
567$calcs{'FreeMemNoHttpd'} = $mem{'MemFree'} + $mem{'Cached'} + $calcs{'HttpdRealTot'} + $calcs{'HttpdSharedAvg'};
568$calcs{'AllProcsTotalMem'} = $calcs{'OtherProcsMem'} + $calcs{'MaxLimitHttpdMem'};
569
570# ---------------------------------
571# CALCULATE NEW HTTPD CONFIG VALUES
572# ---------------------------------
573#
574$cf_changed{$cf_ver}{$cf_mpm}{'ServerLimit'} = sprintf ( "%0.2f",
575 ( $mem{'MemFree'} + $mem{'Cached'} + $calcs{'HttpdRealTot'} + $calcs{'HttpdSharedAvg'} ) / $calcs{'HttpdRealAvg'} );
576
577if ( $cf_mpm eq 'prefork' ) {
578 $cf_changed{$cf_ver}{$cf_mpm}{$cf_MaxName} = $cf_changed{$cf_ver}{$cf_mpm}{'ServerLimit'};
579} else {
580 $cf_changed{$cf_ver}{$cf_mpm}{$cf_MaxName} = sprintf ( "%0.2f",
581 $cf_changed{$cf_ver}{$cf_mpm}{'ServerLimit'} * $cf_changed{$cf_ver}{$cf_mpm}{'ThreadsPerChild'} );
582}
583
584# ----------------------
585# DISPLAY VERBOSE REPORT
586# ----------------------
587#
588if ( $opt{'verbose'} ) {
589 print "Httpd Binary\n\n";
590 for ( sort keys %httpd ) { printf ( " - %-22s: %s\n", $_, $httpd{$_} ); }
591
592 print "\nHttpd Processes\n\n";
593 for ( @procs ) { print $_, "\n"; }
594 print "\n";
595 printf ( " - %-22s: %7.2f MB [excludes shared]\n", "HttpdRealAvg", $calcs{'HttpdRealAvg'} );
596 printf ( " - %-22s: %7.2f MB\n", "HttpdSharedAvg", $calcs{'HttpdSharedAvg'} );
597 printf ( " - %-22s: %7.2f MB [excludes shared]\n", "HttpdRealTot", $calcs{'HttpdRealTot'} );
598 printf ( " - %-22s: %7.0f\n", "HttpdRunning", $calcs{'HttpdRunning'} );
599
600 if ( $opt{'max'} && %dbrow ) {
601 print "\nDatabase Values\n\n";
602 printf ( " - DB %-19s: %s\n", "DateTimeAdded", $dbrow{'DateTimeAdded'} );
603 printf ( " - DB %-19s: %7.2f MB [excludes shared]\n", "HttpdRealAvg", $dbrow{'HttpdRealAvg'} );
604 printf ( " - DB %-19s: %7.2f MB\n", "HttpdSharedAvg", $dbrow{'HttpdSharedAvg'} );
605 printf ( " - DB %-19s: %7.2f MB [excludes shared]\n", "HttpdRealTot", $dbrow{'HttpdRealTot'} );
606 printf ( " - DB %-19s: %7.0f\n", "HttpdRunning", $dbrow{'HttpdRunning'} );
607 }
608
609 print "\nHttpd Config\n\n";
610 # sort in reverse to make sure ServerLimit is before MaxClients
611 for my $set ( reverse sort keys %{$cf_read{$cf_ver}{$cf_mpm}} ) {
612 printf ( " - %-22s: %d\n", $set, $cf_read{$cf_ver}{$cf_mpm}{$set} );
613 }
614 print "\nServer Memory\n\n";
615 for ( sort keys %mem ) { printf ( " - %-22s: %8.2f MB\n", $_, $mem{$_} ); }
616
617 print "\nCalculations Summary\n\n";
618 printf ( " - %-22s: %8.2f MB (MemTotal - Cached - MemFree - HttpdRealTot - HttpdSharedAvg)\n", "OtherProcsMem", $calcs{'OtherProcsMem'} );
619 printf ( " - %-22s: %8.2f MB (MemFree + Cached + HttpdRealTot + HttpdSharedAvg)\n", "FreeMemNoHttpd", $calcs{'FreeMemNoHttpd'} );
620 printf ( " - %-22s: %8.2f MB (HttpdRealAvg * $cf_LimitName + HttpdSharedAvg)%s\n", "MaxLimitHttpdMem", $calcs{'MaxLimitHttpdMem'}, $mcs_from_db );
621 printf ( " - %-22s: %8.2f MB (OtherProcsMem + MaxLimitHttpdMem)\n", "AllProcsTotalMem", $calcs{'AllProcsTotalMem'} );
622
623 print "\nMaximum Values for MemTotal ($mem{'MemTotal'} MB)\n\n";
624 print " <IfModule $cf_IfModule>\n";
625 # sort in reverse to make sure ServerLimit is before MaxClients
626 for my $set ( reverse sort keys %{$cf_changed{$cf_ver}{$cf_mpm}} ) {
627 printf ( "\t%-22s %5.0f\t# ", $set, $cf_changed{$cf_ver}{$cf_mpm}{$set} );
628 if ( $cf_read{$cf_ver}{$cf_mpm}{$set} != $cf_changed{$cf_ver}{$cf_mpm}{$set} ) {
629 printf ( "(%0.0f -> %0.0f)", $cf_read{$cf_ver}{$cf_mpm}{$set}, $cf_changed{$cf_ver}{$cf_mpm}{$set} );
630 } else { print "(no change)"; }
631
632 if ( $cf_comments{$cf_ver}{$cf_mpm}{$set} ) {
633 print " $cf_comments{$cf_ver}{$cf_mpm}{$set}"
634 } elsif ( $cf_defaults{$cf_ver}{$cf_mpm}{$set} ne '' ) {
635 print " Default is $cf_defaults{$cf_ver}{$cf_mpm}{$set}"
636 }
637 print "\n";
638 }
639 print " </IfModule>\n";
640 print "\nResult\n\n";
641}
642
643# ------------------------
644# EXIT WITH RESULT MESSAGE
645# ------------------------
646#
647my $result_prefix = sprintf ( "AllProcsTotalMem (%0.2f MB)$mcs_from_db", $calcs{'AllProcsTotalMem'} );
648my $result_availram = "MemTotal ($mem{'MemTotal'} MB)";
649
650if ( $calcs{'AllProcsTotalMem'} <= $mem{'MemTotal'} ) {
651
652 print "OK: $result_prefix fits within $result_availram.\n";
653 $err = 0;
654
655} elsif ( $calcs{'AllProcsTotalMem'} <= ( $mem{'MemTotal'} + ( $mem{'SwapFree'} * $opt{'swappct'} / 100 ) ) ) {
656
657 print "OK: $result_prefix exceeds $result_availram, but fits within $opt{'swappct'}% of free swap ";
658 printf ( "(uses %0.2f MB of %0.0f MB).\n", $calcs{'AllProcsTotalMem'} - $mem{'MemTotal'}, $mem{'SwapFree'} );
659 $err = 1;
660
661} elsif ( $calcs{'AllProcsTotalMem'} <= ( $mem{'MemTotal'} + $mem{'SwapFree'} ) ) {
662
663 print "WARNING: $result_prefix exceeds $result_availram, but still fits within free swap ";
664 printf ( "(uses %0.2f MB of %0.0f MB).\n", $calcs{'AllProcsTotalMem'} - $mem{'MemTotal'}, $mem{'SwapFree'} );
665 $err = 1;
666} else {
667 print "ERROR: $result_prefix exceeds $result_availram and free swap ($mem{'SwapFree'} MB) ";
668 printf ( "by %0.2f MB.\n", $calcs{'AllProcsTotalMem'} - ( $mem{'MemTotal'} + $mem{'SwapFree'} ) );
669 $err = 2;
670}
671print "\n" if ( $opt{'verbose'} );
672
673if ( $opt{'debug'} ) {
674 print "DEBUG: OtherProcsMem($calcs{'OtherProcsMem'}) + MaxLimitHttpdMem($calcs{'MaxLimitHttpdMem'})";
675 print " = AllProcsTotalMem($calcs{'AllProcsTotalMem'}) vs MemTotal($mem{'MemTotal'}) + SwapFree($mem{'SwapFree'})\n";
676}
677
678exit $err;
679
680# ---------------
681# BEGIN FUNCTIONS
682# ---------------
683#
684sub ShowUsage {
685 #------------------------------------------------------------------------------
686 print "\nPurpose:\n\n";
687 print "This script will attempt to predict the memory used by Apache Httpd processes\n";
688 print "when the maximum configured limits are reached. The prediction is based on the\n";
689 print "(calculated) HttpdRealAvg value -- an average of the memory used by each\n";
690 print "running Httpd process. To see the HttpdRealAvg value, and all other calculated\n";
691 print "variables, use the \"verbose\" command-line argument. There are no additional\n";
692 print "modules required, unless you use the save/days/max command-line argument(s).\n";
693 print "\nSyntax:\n\n";
694 print "$0 [--help] [--debug] [--verbose] \\\n";
695 print " [--exe=/path/to/httpd] [--swappct=#] --save] [--days=#] \\\n";
696 print " [--max=realavg|running]\n\n";
697 printf ("%-15s: %s\n", "--help", "This syntax summary.");
698 printf ("%-15s: %s\n", "--debug", "Show debugging messages as the script is executing.");
699 printf ("%-15s: %s\n", "--verbose", "Display a detailed report of all values found and calculated.");
700 printf ("%-15s: %s\n", "--exe=/path", "Path to httpd binary file (if non-standard).");
701 printf ("%-15s: %s\n", "--config=/path", "Path to httpd configuration file (if non-standard).");
702 printf ("%-15s: %s\n", "--swappct=#", "% of FREE swap use allowed before WARNING condition (default 0).");
703 printf ("%-15s: %s\n", "--save", "Save average sizes to database ($dbname).");
704 printf ("%-15s: %s\n", "--days=#", "Remove database entries older than # days (default 30).");
705 printf ("%-15s: %s\n", "--max=realavg", "Use largest HttpdRealAvg size from current procs or database.");
706 printf ("%-15s: %s\n", "--max=running", "Use HttpdRealAvg size from the largest MaxRunning recorded.");
707 #------------------------------------------------------------------------------
708 print "\nThe save/days/max command-line arguments require the DBD::SQLite perl module.\n";
709 print "Use --max=running if the size and number of httpd processes increases and\n";
710 print "decreases rapidly or unpredictably. The --max=realavg setting should be more\n";
711 print "accurate for servers that have stable httpd sizes, and progressive increase /\n";
712 print "decrease in the number of httpd processes.\n";
713 print "\nExample:\n\n";
714 print "/usr/local/bin/check_httpd_limits.pl --save --days=14 --max=realavg --swappct=25\n\n";
715 exit $err;
716}