Created
November 20, 2013 16:11
-
-
Save itxx00/7565843 to your computer and use it in GitHub Desktop.
check_httpd_limits
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
# Copyright 2012 - Jean-Sebastien Morisset - http://surniaulula.com/ | |
# | |
# This script is free software; you can redistribute it and/or modify it under | |
# the terms of the GNU General Public License as published by the Free Software | |
# Foundation; either version 3 of the License, or (at your option) any later | |
# version. | |
# | |
# This script is distributed in the hope that it will be useful, but WITHOUT | |
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more | |
# details at http://www.gnu.org/licenses/. | |
# Perl script to compare the size of running Apache httpd processes, the | |
# configured prefork/worker limits, and the available server memory. Exits with | |
# a warning or error message if the configured limits exceed the server's | |
# memory. | |
# | |
# Syntax: check_httpd_limits.pl --help | |
# The script performs the following tasks: | |
# | |
# - Reads the /proc/meminfo file for server memory values. | |
# - Reads the /proc/*/exe symbolic links to find the matching httpd binaries. | |
# - Reads the /proc/*/stat files for pid, process name, ppid, and rss. | |
# - Reads the /proc/*/statm files for the shared memory size. | |
# - Executes HTTP binary with "-V" to get the config file path and MPM info. | |
# - Reads the HTTP config file to get MPM (prefork or worker) settings. | |
# - Calculates the average and total HTTP process sizes, taking into account | |
# the shared memory used. | |
# - Calculates possible changes to MPM settings based on available memory and | |
# process sizes. | |
# - Displays all the values found and settings calculated if the --verbose | |
# parameter is used. | |
# - Exits with OK (0), WARNING (1), or ERROR (2) based on projected memory use | |
# with all (allowed) HTTP processes running. | |
# OK: Maximum number of HTTP processes fit within available RAM. | |
# WARNING: Maximum number of HTTP processes exceeds available RAM, but still | |
# fits within the free swap. | |
# ERROR: Maximum number of HTTP processes exceeds available RAM and swap. | |
# Changes: | |
# | |
# v2.4: | |
# - Added config for Apache Httpd v2.5 and 2.6 (identical to 2.4). | |
# - Added config for 'eventopt' MPM (identical to 'event' MPM). | |
# | |
# v2.5: | |
# - Added 'config' command-line argument. | |
# - Re-arranged search path for httpd binary. | |
use strict; | |
use warnings; | |
use POSIX; | |
use Getopt::Long; | |
no warnings 'once'; # no warning for $DBI::err | |
my $VERSION = '2.5'; | |
my $pagesize = POSIX::sysconf(POSIX::_SC_PAGESIZE); | |
my @stathrefs; | |
my $err = 0; | |
my %mem = ( | |
'MemTotal' => '', | |
'MemFree' => '', | |
'Cached' => '', | |
'SwapTotal' => '', | |
'SwapFree' => '', | |
); | |
my %httpd = ( | |
'EXE' => '', | |
'ROOT' => '', | |
'CONFIG' => '', | |
'MPM' => '', | |
'VERSION' => '', | |
); | |
my $cf_IfModule = ''; | |
my $cf_MaxName = ''; # defined based on httpd version (MaxClients or MaxRequestWorkers) | |
my $cf_LimitName = ''; # defined once MPM is determined (MaxClients/MaxRequestWorkers or ServerLimit) | |
my $cf_ver = ''; | |
my $cf_min = '2.2'; | |
my $cf_mpm = ''; | |
my %cf_read = (); | |
my %cf_changed = (); | |
my %cf_defaults = ( | |
'2.2' => { | |
'prefork' => { | |
'StartServers' => 5, | |
'MinSpareServers' => 5, | |
'MaxSpareServers' => 10, | |
'ServerLimit' => 256, | |
'MaxClients' => 256, | |
'MaxRequestsPerChild' => 10000, | |
}, | |
'worker' => { | |
'StartServers' => 3, | |
'MinSpareThreads' => 75, | |
'MaxSpareThreads' => 250, | |
'ThreadsPerChild' => 25, | |
'ServerLimit' => 16, | |
'MaxClients' => 400, | |
'MaxRequestsPerChild' => 10000, | |
}, | |
}, | |
'2.4' => { | |
'prefork' => { | |
'StartServers' => 5, | |
'MinSpareServers' => 5, | |
'MaxSpareServers' => 10, | |
'ServerLimit' => 256, | |
'MaxRequestWorkers' => 256, # aka MaxClients | |
'MaxConnectionsPerChild' => 0, # aka MaxRequestsPerChild | |
}, | |
'worker' => { | |
'StartServers' => 3, | |
'MinSpareThreads' => 75, | |
'MaxSpareThreads' => 250, | |
'ThreadsPerChild' => 25, | |
'ServerLimit' => 16, | |
'MaxRequestWorkers' => 400, # aka MaxClients | |
'MaxConnectionsPerChild' => 0, # aka MaxRequestsPerChild | |
}, | |
}, | |
); | |
$cf_defaults{'2.5'} = $cf_defaults{'2.4'}; | |
$cf_defaults{'2.6'} = $cf_defaults{'2.5'}; | |
# The event MPM config is identical to the worker MPM config | |
# Uses a hashref instead of copying the hash elements | |
for my $ver ( keys %cf_defaults ) { | |
$cf_defaults{$ver}{'event'} = $cf_defaults{$ver}{'worker'}; | |
$cf_defaults{$ver}{'eventopt'} = $cf_defaults{$ver}{'event'}; | |
} | |
# easiest way to copy the three-dimensional hash without using a module | |
for my $ver ( keys %cf_defaults ) { | |
for my $mpm ( keys %{$cf_defaults{$ver}} ) { | |
for my $el ( keys %{$cf_defaults{$ver}{$mpm}} ) { | |
$cf_read{$ver}{$mpm}{$el} = $cf_defaults{$ver}{$mpm}{$el}; | |
$cf_changed{$ver}{$mpm}{$el} = $cf_defaults{$ver}{$mpm}{$el}; | |
} | |
} | |
} | |
my %cf_comments = ( | |
'2.2' => { | |
'prefork' => { | |
'ServerLimit' => 'MaxClients', | |
'MaxClients' => '(MemFree + Cached + HttpdRealTot + HttpdSharedAvg) / HttpdRealAvg', | |
}, | |
'worker' => { | |
'ServerLimit' => '(MemFree + Cached + HttpdRealTot + HttpdSharedAvg) / HttpdRealAvg', | |
'MaxClients' => 'ServerLimit * ThreadsPerChild', | |
}, | |
}, | |
'2.4' => { | |
'prefork' => { | |
'ServerLimit' => 'MaxRequestWorkers', | |
'MaxRequestWorkers' => '(MemFree + Cached + HttpdRealTot + HttpdSharedAvg) / HttpdRealAvg', | |
}, | |
'worker' => { | |
'ServerLimit' => '(MemFree + Cached + HttpdRealTot + HttpdSharedAvg) / HttpdRealAvg', | |
'MaxRequestWorkers' => 'ServerLimit * ThreadsPerChild', | |
}, | |
}, | |
); | |
$cf_comments{'2.5'} = $cf_comments{'2.4'}; | |
$cf_comments{'2.6'} = $cf_comments{'2.5'}; | |
# the event MPM config is identical to the worker MPM config | |
# uses a hashref instead of copying the hash elements | |
for my $ver ( keys %cf_comments ) { | |
$cf_comments{$ver}{'event'} = $cf_comments{$ver}{'worker'}; | |
$cf_comments{$ver}{'eventopt'} = $cf_comments{$ver}{'event'}; | |
} | |
my %calcs = ( | |
'HttpdRealAvg' => 0, | |
'HttpdSharedAvg' => 0, | |
'HttpdRealTot' => 0, | |
'HttpdRunning' => 0, | |
'OtherProcsMem' => '', | |
'FreeMemNoHttpd' => '', | |
'MaxLimitHttpdMem' => '', | |
'AllProcsTotalMem' => '', | |
); | |
# comment string when MaxLimitHttpdMem is calculated from DB values | |
my $mcs_from_db = ''; | |
# common location for httpd binaries if not sepcified on command-line | |
my @httpd_paths = ( | |
'/usr/sbin/httpd', | |
'/usr/sbin/apache2', | |
'/usr/local/sbin/httpd', | |
'/usr/local/sbin/apache2', | |
'/opt/apache/bin/httpd', | |
'/opt/apache/sbin/httpd', | |
'/usr/lib/apache2/mpm-prefork/apache2', | |
'/usr/lib/apache2/mpm-worker/apache2', | |
); | |
my $dbname = '/var/tmp/check_httpd_limits.sqlite'; | |
my $dbuser = ''; | |
my $dbpass = ''; | |
my $dbtable = 'HttpdProcInfo'; | |
my $dsn = "DBI:SQLite:dbname=$dbname"; | |
my $dbh; | |
my %dbrow = ( | |
'DateTimeAdded' => 0, | |
'HttpdRealAvg' => 0, | |
'HttpdSharedAvg' => 0, | |
'HttpdRealTot' => 0, | |
'HttpdRunning' => 0, | |
); | |
my %opt = (); | |
GetOptions(\%opt, | |
'help', | |
'debug', | |
'verbose', | |
'exe=s', | |
'config=s', | |
'swappct=i', | |
'save', | |
'days=i', | |
'max=s', | |
); | |
$opt{'swappct'} = 0 unless ( $opt{'swappct'} ); | |
$opt{'max'} = $opt{'max'} ? lc($opt{'max'}) : ""; | |
&ShowUsage() if ( $opt{'help'} ); | |
if ( $opt{'verbose'} ) { | |
print "\nCheck Apache Httpd MPM Config Limits (Version $VERSION)\n"; | |
print "by Jean-Sebastien Morisset - http://surniaulula.com/\n\n"; | |
} | |
# | |
# READ MAXIMUM FROM DATABASE | |
# | |
if ( $opt{'save'} || $opt{'days'} || $opt{'max'} ) { | |
$opt{'days'} = 30 unless ( defined $opt{'days'} ); | |
print "Saving Httpd Averages to $dsn\n\n" | |
if ( $opt{'save'} && $opt{'verbose'} ); | |
require DBD::SQLite; | |
print "DEBUG: Connecting to database $dsn.\n" if ( $opt{'debug'} ); | |
$dbh = DBI->connect($dsn, $dbuser, $dbpass); | |
die "ERROR: $DBI::errstr\n" if ($DBI::err); | |
$dbh->do("PRAGMA foreign_keys = ON;"); | |
$dbh->do("CREATE TABLE IF NOT EXISTS $dbtable ( | |
DateTimeAdded DATE PRIMARY KEY, | |
HttpdRealAvg INTEGER NOT NULL, | |
HttpdSharedAvg INTEGER NOT NULL, | |
HttpdRealTot INTEGER NOT NULL, | |
HttpdRunning INTEGER NOT NULL);"); | |
# Use an array instead of a hash to keep the column order. If you're | |
# using MySQL, you may want to add an 'AFTER ColumnName' to the | |
# definiton string. 'AFTER' is not supported by SQLite, so always add | |
# new columns to the end of the array. | |
my @dbcol = ( | |
{ 'name' => 'DateTimeAdded', 'definition' => 'DATE', }, | |
{ 'name' => 'HttpdRealAvg', 'definition' => 'INTEGER', }, | |
{ 'name' => 'HttpdSharedAvg', 'definition' => 'INTEGER', }, | |
{ 'name' => 'HttpdRealTot', 'definition' => 'INTEGER', }, | |
{ 'name' => 'HttpdRunning', 'definition' => 'INTEGER', }, | |
); | |
my @dbidx = ( | |
{ 'name' => 'HttpdRealAvgIdx', 'table' => 'HttpdRealAvg', }, | |
{ 'name' => 'HttpdRunningIdx', 'table' => 'HttpdRunning', }, | |
); | |
# Use hashes to quickly define (and lookup) which tables/indexes already exist. | |
my %dbcol_exists = (); | |
my %dbidx_exists = (); | |
for ( @{ $dbh->selectall_arrayref( "PRAGMA TABLE_INFO($dbtable)") } ) { $dbcol_exists{$_->[1]} = 1; }; | |
for ( @{ $dbh->selectall_arrayref( "PRAGMA INDEX_LIST($dbtable)") } ) { $dbidx_exists{$_->[1]} = 1; }; | |
# Create any missing columns. | |
for my $col ( @dbcol ) { | |
unless ( $dbcol_exists{$col->{'name'}} ) { | |
print "DEBUG: Adding missing column $col->{'name'} as $col->{'definition'}.\n" if ( $opt{'debug'} ); | |
$dbh->do("ALTER TABLE $dbtable ADD COLUMN $col->{'name'} $col->{'definition'};"); | |
$dbh->do("UPDATE $dbtable SET $col->{'name'} = 0 WHERE $col->{'name'} = NULL;"); | |
} | |
} | |
# Create any missing indexes. | |
for my $idx ( @dbidx ) { | |
unless ( $dbidx_exists{$idx->{'name'}} ) { | |
print "DEBUG: Adding missing index $idx->{'name'} for $idx->{'table'}.\n" if ( $opt{'debug'} ); | |
$dbh->do("CREATE INDEX $idx->{'name'} ON $dbtable ($idx->{'table'});"); | |
} | |
} | |
print "DEBUG: Removing DB rows older than $opt{'days'} days.\n" if ( $opt{'debug'} ); | |
$dbh->do("DELETE FROM $dbtable WHERE DateTimeAdded < DATETIME('NOW', '-$opt{'days'} DAYS');"); | |
if ( $opt{'max'} eq 'realavg' ) { | |
print "DEBUG: Selecting largest HttpdRealAvg value in past $opt{'days'} days.\n" if ( $opt{'debug'} ); | |
( $dbrow{'DateTimeAdded'}, $dbrow{'HttpdRealAvg'}, $dbrow{'HttpdSharedAvg'}, $dbrow{'HttpdRealTot'}, $dbrow{'HttpdRunning'} ) = | |
$dbh->selectrow_array("SELECT DateTimeAdded, HttpdRealAvg, HttpdSharedAvg, HttpdRealTot, HttpdRunning | |
FROM $dbtable ORDER BY HttpdRealAvg DESC, DateTimeAdded DESC LIMIT 1;"); | |
} elsif ( $opt{'max'} eq 'running' ) { | |
print "DEBUG: Selecting largest HttpdRunning value in past $opt{'days'} days.\n" if ( $opt{'debug'} ); | |
( $dbrow{'DateTimeAdded'}, $dbrow{'HttpdRealAvg'}, $dbrow{'HttpdSharedAvg'}, $dbrow{'HttpdRealTot'}, $dbrow{'HttpdRunning'} ) = | |
$dbh->selectrow_array("SELECT DateTimeAdded, HttpdRealAvg, HttpdSharedAvg, HttpdRealTot, HttpdRunning | |
FROM $dbtable ORDER BY HttpdRunning DESC, HttpdRealAvg DESC, DateTimeAdded DESC LIMIT 1;"); | |
} | |
if ( $opt{'max'} && %dbrow ) { | |
# make sure HttpdRunning (a column added later) has a value | |
$dbrow{'HttpdRunning'} = 0 unless( $dbrow{'HttpdRunning'} ); | |
if ( $opt{'debug'} ) { | |
print "DEBUG: DateTimeAdded=$dbrow{'DateTimeAdded'}\n"; | |
print "DEBUG: HttpdRealAvg=$dbrow{'HttpdRealAvg'}\n"; | |
print "DEBUG: HttpdSharedAvg=$dbrow{'HttpdSharedAvg'}\n"; | |
print "DEBUG: HttpdRealTot=$dbrow{'HttpdRealTot'}\n"; | |
print "DEBUG: HttpdRunning=$dbrow{'HttpdRunning'}\n"; | |
} | |
} | |
} | |
# --------------------------- | |
# READ THE SERVER MEMORY INFO | |
# --------------------------- | |
# | |
print "DEBUG: Open /proc/meminfo\n" if ( $opt{'debug'} ); | |
open ( my $mem_fh, "<", "/proc/meminfo" ) or die "ERROR: /proc/meminfo - $!\n"; | |
while (<$mem_fh>) { | |
if ( /^[[:space:]]*([a-zA-Z]+):[[:space:]]+([0-9]+)/) { | |
if ( defined $mem{$1} ) { | |
$mem{$1} = sprintf ( "%0.2f", $2 / 1024 ); | |
print "DEBUG: Found $1 = $mem{$1}.\n" if ( $opt{'debug'} ); | |
} | |
} | |
} | |
close ( $mem_fh ); | |
# ----------------------- | |
# LOCATE THE HTTPD BINARY | |
# ----------------------- | |
# | |
if ( defined $opt{'exe'} ) { | |
$httpd{'EXE'} = $opt{'exe'}; | |
print "DEBUG: Command-Line Exe \"$httpd{'EXE'}\".\n" | |
if ( $opt{'debug'} ); | |
} else { | |
for ( @httpd_paths ) { | |
if ( $_ && -x $_ ) { | |
$httpd{'EXE'} = $_; | |
print "DEBUG: Found Httpd Exe \"$httpd{'EXE'}\".\n" | |
if ( $opt{'debug'} ); | |
last; | |
} | |
} | |
} | |
die "ERROR: No executable Apache HTTP binary found!\n" | |
unless ( defined $httpd{'EXE'} && -x $httpd{'EXE'} ); | |
# ----------------------------------------- | |
# READ PROCESS INFORMATION FOR HTTPD BINARY | |
# ----------------------------------------- | |
# | |
print "DEBUG: Opendir /proc\n" if ( $opt{'debug'} ); | |
opendir ( my $proc_fh, "/proc" ) or die "ERROR: /proc - $!\n"; | |
while ( my $pid = readdir( $proc_fh ) ) { | |
my $exe = readlink( "/proc/$pid/exe" ); | |
next unless ( defined $exe ); | |
print "DEBUG: Readlink /proc/$pid/exe ($exe)" if ( $opt{'debug'} ); | |
if ( $exe eq $httpd{'EXE'} ) { | |
print " - matched ($httpd{'EXE'})\n" if ( $opt{'debug'} ); | |
print "DEBUG: Open /proc/$pid/stat\n" if ( $opt{'debug'} ); | |
open ( my $stat_fh, "<", "/proc/$pid/stat" ) or die "ERROR: /proc/$pid/stat - $!\n"; | |
my @pid_stat = split (/ /, readline( $stat_fh )); close ( $stat_fh ); | |
print "DEBUG: Open /proc/$pid/statm\n" if ( $opt{'debug'} ); | |
open ( my $statm_fh, "<", "/proc/$pid/statm" ) or die "ERROR: /proc/$pid/statm - $!\n"; | |
my @pid_statm = split (/ /, readline( $statm_fh )); close ( $statm_fh ); | |
my %all_stats = ( | |
'pid' => $pid_stat[0], | |
'name' => $pid_stat[1], | |
'ppid' => $pid_stat[3], | |
'rss' => $pid_stat[23] * $pagesize / 1024 / 1024, | |
'share' => $pid_statm[2] * $pagesize / 1024 / 1024, | |
); | |
if ( $opt{'debug'} ) { | |
print "DEBUG:"; | |
for (sort keys %all_stats) { print " $_:$all_stats{$_}"; } | |
print "\n"; | |
} | |
push ( @stathrefs, \%all_stats ); | |
} else { print "\n" if ( $opt{'debug'} ); } | |
} | |
close ( $proc_fh ); | |
die "ERROR: No $httpd{'EXE'} processes found in /proc/*/exe! Are you root?\n" | |
unless ( @stathrefs ); | |
# ------------------------------------- | |
# READ THE HTTPD BINARY COMPILED VALUES | |
# ------------------------------------- | |
# | |
print "DEBUG: Open $httpd{'EXE'} -V\n" if ( $opt{'debug'} ); | |
open ( my $set_fh, "-|", "$httpd{'EXE'} -V" ) or die "ERROR: $httpd{'EXE'} - $!\n"; | |
while ( <$set_fh> ) { | |
$httpd{'ROOT'} = $1 if (/^.*HTTPD_ROOT="(.*)"$/); | |
$httpd{'CONFIG'} = $1 if (/^.*SERVER_CONFIG_FILE="(.*)"$/); | |
$httpd{'VERSION'} = $1 if (/^Server version:[[:space:]]+Apache\/([0-9]\.[0-9]).*$/); | |
$httpd{'MPM'} = lc($1) if (/^Server MPM:[[:space:]]+(.*)$/); | |
$httpd{'MPM'} = lc($1) if (/APACHE_MPM_DIR="server\/mpm\/([^"]*)"$/); | |
} | |
close ( $set_fh ); | |
if ( $opt{'debug'} ) { | |
print "DEBUG: HTTPD ROOT = $httpd{'ROOT'}\n"; | |
print "DEBUG: HTTPD CONFIG = $httpd{'CONFIG'}\n"; | |
print "DEBUG: HTTPD VERSION = $httpd{'VERSION'}\n"; | |
print "DEBUG: HTTPD MPM = $httpd{'MPM'}\n"; | |
} | |
if ( $opt{'config'} ) { | |
$httpd{'CONFIG'} = $opt{'config'}; | |
print "DEBUG: Command-Line Config \"$httpd{'CONFIG'}\".\n" | |
if ( $opt{'debug'} ); | |
} | |
# check for relative path | |
if ( $httpd{'CONFIG'} !~ /^\// ) { | |
$httpd{'CONFIG'} = "$httpd{'ROOT'}/$httpd{'CONFIG'}"; | |
print "DEBUG: Relative Path Adjusted = $httpd{'CONFIG'}\n" | |
if ( $opt{'debug'} ); | |
} | |
die "ERROR: Cannot determine httpd version number.\n" | |
unless ( $httpd{'VERSION'} && $httpd{'VERSION'} > 0 ); | |
die "ERROR: Cannot determine httpd server MPM type.\n" | |
unless ( $httpd{'MPM'} ); | |
# determine the config version number to use | |
if ( $cf_defaults{$httpd{'VERSION'}} ) { | |
$cf_ver = $httpd{'VERSION'}; | |
} elsif ( $httpd{'VERSION'} < $cf_min ) { | |
$cf_ver = $cf_min; | |
print "INFO: Httpd version $httpd{'VERSION'} not configured - using $cf_ver values instead.\n"; | |
} else { | |
die "ERROR: Httpd version $httpd{'VERSION'} configuration values not defined.\n"; | |
} | |
if ( $cf_defaults{$cf_ver}{$httpd{'MPM'}} ) { $cf_mpm = $httpd{'MPM'}; } | |
else { die "ERROR: Httpd server MPM \"$httpd{'MPM'}\" is unknown.\n"; } | |
# -------------------------- | |
# READ THE HTTPD CONFIG FILE | |
# -------------------------- | |
# | |
print "DEBUG: Open $httpd{'CONFIG'}\n" if ( $opt{'debug'} ); | |
open ( my $conf_fh, "<", $httpd{'CONFIG'} ) or die "ERROR: $httpd{'CONFIG'} - $!\n"; | |
my $conf = do { local $/; <$conf_fh> }; | |
close ( $conf_fh ); | |
# Read the MPM config values | |
if ( $conf =~ /^[[:space:]]*<IfModule ($cf_mpm\.c|mpm_$cf_mpm\_module)>([^<]*)/im ) { | |
$cf_IfModule = $1; my $cf_Content = $2; | |
print "DEBUG: IfModule $cf_IfModule\n$cf_Content\n" if ( $opt{'debug'} ); | |
for ( split (/\n/, $cf_Content) ) { | |
if ( /^[[:space:]]*([a-zA-Z]+)[[:space:]]+([0-9]+)/) { | |
print "DEBUG: $1 = $2\n" if ( $opt{'debug'} ); | |
$cf_read{$cf_ver}{$cf_mpm}{$1} = $2; | |
$cf_changed{$cf_ver}{$cf_mpm}{$1} = $2; | |
} | |
} | |
} | |
if ( $cf_ver <= $cf_min ) { | |
$cf_MaxName = 'MaxClients'; | |
} else { | |
$cf_MaxName = 'MaxRequestWorkers'; | |
my %dep = ( | |
'MaxClients' => 'MaxRequestWorkers', | |
'MaxRequestsPerChild' => 'MaxConnectionsPerChild', | |
); | |
for ( sort keys %dep ) { | |
if ( defined $cf_read{$cf_ver}{$cf_mpm}{$_} ) { | |
print "INFO: $_($cf_read{$cf_ver}{$cf_mpm}{$_}) is deprecated - renaming to $dep{$_}.\n"; | |
$cf_read{$cf_ver}{$cf_mpm}{$dep{$_}} = $cf_read{$cf_ver}{$cf_mpm}{$_}; | |
$cf_changed{$cf_ver}{$cf_mpm}{$dep{$_}} = $cf_changed{$cf_ver}{$cf_mpm}{$_}; | |
delete $cf_read{$cf_ver}{$cf_mpm}{$_}; | |
delete $cf_changed{$cf_ver}{$cf_mpm}{$_}; | |
} | |
} | |
} | |
# If using prefork MPM, base the caculation on MaxClients/MaxRequestWorkers instead of ServerLimit | |
# When using prefork, MaxClients/MaxRequestWorkers determines how many processes can be started | |
$cf_LimitName = $cf_mpm eq 'prefork' ? $cf_MaxName : 'ServerLimit'; | |
# Exit with an error if any value is not > 0 | |
for my $set ( sort keys %{$cf_changed{$cf_ver}{$cf_mpm}} ) { | |
die "ERROR: $set value is 0 in $httpd{'CONFIG'}!\n" | |
unless ( $cf_changed{$cf_ver}{$cf_mpm}{$set} > 0 || | |
$set =~ /^(MaxRequestsPerChild|MaxConnectionsPerChild)$/ ); | |
} | |
# ----------------------- | |
# CALCULATE SIZE AVERAGES | |
# ----------------------- | |
# | |
my @procs; | |
for my $stref ( @stathrefs ) { | |
my $real = ${$stref}{'rss'} - ${$stref}{'share'}; | |
my $share = ${$stref}{'share'}; | |
my $proc_msg = sprintf ( " - %-22s: %7.2f MB / %6.2f MB shared", | |
"PID ${$stref}{'pid'} ${$stref}{'name'}", ${$stref}{'rss'}, $share ); | |
if ( ${$stref}{'ppid'} > 1 ) { | |
$calcs{'HttpdRealAvg'} = $real if ( $calcs{'HttpdRealAvg'} == 0 ); | |
$calcs{'HttpdSharedAvg'} = $share if ( $calcs{'HttpdSharedAvg'} == 0 ); | |
$calcs{'HttpdRealAvg'} = ( $calcs{'HttpdRealAvg'} + $real ) / 2; | |
$calcs{'HttpdSharedAvg'} = ( $calcs{'HttpdSharedAvg'} + $share ) / 2; | |
} else { | |
$proc_msg .= " [excluded from averages]"; | |
} | |
$calcs{'HttpdRealTot'} += $real; | |
print "DEBUG: $proc_msg\n" if ( $opt{'debug'} ); | |
print "DEBUG: Avg $calcs{'HttpdRealAvg'}, Shr $calcs{'HttpdSharedAvg'}, Tot $calcs{'HttpdRealTot'}\n" if ( $opt{'debug'} ); | |
push ( @procs, $proc_msg); | |
} | |
# round off the calcs | |
$calcs{'HttpdRealAvg'} = sprintf ( "%0.2f", $calcs{'HttpdRealAvg'} ); | |
$calcs{'HttpdSharedAvg'} = sprintf ( "%0.2f", $calcs{'HttpdSharedAvg'} ); | |
$calcs{'HttpdRealTot'} = sprintf ( "%0.2f", $calcs{'HttpdRealTot'} ); | |
$calcs{'HttpdRunning'} = $#procs + 1; | |
# save the new averages to the database | |
if ( $opt{'save'} ) { | |
if ( $opt{'debug'} ) { | |
print "DEBUG: Adding to database: HttpdRealAvg($calcs{'HttpdRealAvg'}), "; | |
print "HttpdSharedAvg($calcs{'HttpdSharedAvg'}), HttpdRealTot($calcs{'HttpdRealTot'}), "; | |
print "HttpdRunning($calcs{'HttpdRunning'}).\n" | |
} | |
my $sth = $dbh->prepare( "INSERT INTO $dbtable VALUES ( DATETIME('NOW'), ?, ?, ?, ? )" ); | |
$sth->execute( $calcs{'HttpdRealAvg'}, $calcs{'HttpdSharedAvg'}, $calcs{'HttpdRealTot'}, $calcs{'HttpdRunning'} ); | |
$sth->finish; | |
} | |
if ( $opt{'save'} || $opt{'days'} || $opt{'max'} ) { | |
print "DEBUG: Disconnecting from database." if ( $opt{'debug'} ); | |
$dbh->disconnect; | |
} | |
# use max averages from database if --max used (and the database average is larger than current) | |
if ( $opt{'max'} eq 'realavg' && $dbrow{'HttpdRealAvg'} && $dbrow{'HttpdSharedAvg'} && $dbrow{'HttpdRealAvg'} > $calcs{'HttpdRealAvg'} ) { | |
$mcs_from_db = " [Avg from $dbrow{'DateTimeAdded'}]"; | |
$calcs{'MaxLimitHttpdMem'} = $dbrow{'HttpdRealAvg'} * $cf_changed{$cf_ver}{$cf_mpm}{$cf_LimitName} + $dbrow{'HttpdSharedAvg'}; | |
print "DEBUG: DB HttpdRealAvg: $dbrow{'HttpdRealAvg'} > Current HttpdRealAvg: $calcs{'HttpdRealAvg'}.\n" if ( $opt{'debug'} ); | |
} else { | |
$calcs{'MaxLimitHttpdMem'} = $calcs{'HttpdRealAvg'} * $cf_changed{$cf_ver}{$cf_mpm}{$cf_LimitName} + $calcs{'HttpdSharedAvg'}; | |
} | |
$calcs{'OtherProcsMem'} = $mem{'MemTotal'} - $mem{'Cached'} - $mem{'MemFree'} - $calcs{'HttpdRealTot'} - $calcs{'HttpdSharedAvg'}; | |
$calcs{'FreeMemNoHttpd'} = $mem{'MemFree'} + $mem{'Cached'} + $calcs{'HttpdRealTot'} + $calcs{'HttpdSharedAvg'}; | |
$calcs{'AllProcsTotalMem'} = $calcs{'OtherProcsMem'} + $calcs{'MaxLimitHttpdMem'}; | |
# --------------------------------- | |
# CALCULATE NEW HTTPD CONFIG VALUES | |
# --------------------------------- | |
# | |
$cf_changed{$cf_ver}{$cf_mpm}{'ServerLimit'} = sprintf ( "%0.2f", | |
( $mem{'MemFree'} + $mem{'Cached'} + $calcs{'HttpdRealTot'} + $calcs{'HttpdSharedAvg'} ) / $calcs{'HttpdRealAvg'} ); | |
if ( $cf_mpm eq 'prefork' ) { | |
$cf_changed{$cf_ver}{$cf_mpm}{$cf_MaxName} = $cf_changed{$cf_ver}{$cf_mpm}{'ServerLimit'}; | |
} else { | |
$cf_changed{$cf_ver}{$cf_mpm}{$cf_MaxName} = sprintf ( "%0.2f", | |
$cf_changed{$cf_ver}{$cf_mpm}{'ServerLimit'} * $cf_changed{$cf_ver}{$cf_mpm}{'ThreadsPerChild'} ); | |
} | |
# ---------------------- | |
# DISPLAY VERBOSE REPORT | |
# ---------------------- | |
# | |
if ( $opt{'verbose'} ) { | |
print "Httpd Binary\n\n"; | |
for ( sort keys %httpd ) { printf ( " - %-22s: %s\n", $_, $httpd{$_} ); } | |
print "\nHttpd Processes\n\n"; | |
for ( @procs ) { print $_, "\n"; } | |
print "\n"; | |
printf ( " - %-22s: %7.2f MB [excludes shared]\n", "HttpdRealAvg", $calcs{'HttpdRealAvg'} ); | |
printf ( " - %-22s: %7.2f MB\n", "HttpdSharedAvg", $calcs{'HttpdSharedAvg'} ); | |
printf ( " - %-22s: %7.2f MB [excludes shared]\n", "HttpdRealTot", $calcs{'HttpdRealTot'} ); | |
printf ( " - %-22s: %7.0f\n", "HttpdRunning", $calcs{'HttpdRunning'} ); | |
if ( $opt{'max'} && %dbrow ) { | |
print "\nDatabase Values\n\n"; | |
printf ( " - DB %-19s: %s\n", "DateTimeAdded", $dbrow{'DateTimeAdded'} ); | |
printf ( " - DB %-19s: %7.2f MB [excludes shared]\n", "HttpdRealAvg", $dbrow{'HttpdRealAvg'} ); | |
printf ( " - DB %-19s: %7.2f MB\n", "HttpdSharedAvg", $dbrow{'HttpdSharedAvg'} ); | |
printf ( " - DB %-19s: %7.2f MB [excludes shared]\n", "HttpdRealTot", $dbrow{'HttpdRealTot'} ); | |
printf ( " - DB %-19s: %7.0f\n", "HttpdRunning", $dbrow{'HttpdRunning'} ); | |
} | |
print "\nHttpd Config\n\n"; | |
# sort in reverse to make sure ServerLimit is before MaxClients | |
for my $set ( reverse sort keys %{$cf_read{$cf_ver}{$cf_mpm}} ) { | |
printf ( " - %-22s: %d\n", $set, $cf_read{$cf_ver}{$cf_mpm}{$set} ); | |
} | |
print "\nServer Memory\n\n"; | |
for ( sort keys %mem ) { printf ( " - %-22s: %8.2f MB\n", $_, $mem{$_} ); } | |
print "\nCalculations Summary\n\n"; | |
printf ( " - %-22s: %8.2f MB (MemTotal - Cached - MemFree - HttpdRealTot - HttpdSharedAvg)\n", "OtherProcsMem", $calcs{'OtherProcsMem'} ); | |
printf ( " - %-22s: %8.2f MB (MemFree + Cached + HttpdRealTot + HttpdSharedAvg)\n", "FreeMemNoHttpd", $calcs{'FreeMemNoHttpd'} ); | |
printf ( " - %-22s: %8.2f MB (HttpdRealAvg * $cf_LimitName + HttpdSharedAvg)%s\n", "MaxLimitHttpdMem", $calcs{'MaxLimitHttpdMem'}, $mcs_from_db ); | |
printf ( " - %-22s: %8.2f MB (OtherProcsMem + MaxLimitHttpdMem)\n", "AllProcsTotalMem", $calcs{'AllProcsTotalMem'} ); | |
print "\nMaximum Values for MemTotal ($mem{'MemTotal'} MB)\n\n"; | |
print " <IfModule $cf_IfModule>\n"; | |
# sort in reverse to make sure ServerLimit is before MaxClients | |
for my $set ( reverse sort keys %{$cf_changed{$cf_ver}{$cf_mpm}} ) { | |
printf ( "\t%-22s %5.0f\t# ", $set, $cf_changed{$cf_ver}{$cf_mpm}{$set} ); | |
if ( $cf_read{$cf_ver}{$cf_mpm}{$set} != $cf_changed{$cf_ver}{$cf_mpm}{$set} ) { | |
printf ( "(%0.0f -> %0.0f)", $cf_read{$cf_ver}{$cf_mpm}{$set}, $cf_changed{$cf_ver}{$cf_mpm}{$set} ); | |
} else { print "(no change)"; } | |
if ( $cf_comments{$cf_ver}{$cf_mpm}{$set} ) { | |
print " $cf_comments{$cf_ver}{$cf_mpm}{$set}" | |
} elsif ( $cf_defaults{$cf_ver}{$cf_mpm}{$set} ne '' ) { | |
print " Default is $cf_defaults{$cf_ver}{$cf_mpm}{$set}" | |
} | |
print "\n"; | |
} | |
print " </IfModule>\n"; | |
print "\nResult\n\n"; | |
} | |
# ------------------------ | |
# EXIT WITH RESULT MESSAGE | |
# ------------------------ | |
# | |
my $result_prefix = sprintf ( "AllProcsTotalMem (%0.2f MB)$mcs_from_db", $calcs{'AllProcsTotalMem'} ); | |
my $result_availram = "MemTotal ($mem{'MemTotal'} MB)"; | |
if ( $calcs{'AllProcsTotalMem'} <= $mem{'MemTotal'} ) { | |
print "OK: $result_prefix fits within $result_availram.\n"; | |
$err = 0; | |
} elsif ( $calcs{'AllProcsTotalMem'} <= ( $mem{'MemTotal'} + ( $mem{'SwapFree'} * $opt{'swappct'} / 100 ) ) ) { | |
print "OK: $result_prefix exceeds $result_availram, but fits within $opt{'swappct'}% of free swap "; | |
printf ( "(uses %0.2f MB of %0.0f MB).\n", $calcs{'AllProcsTotalMem'} - $mem{'MemTotal'}, $mem{'SwapFree'} ); | |
$err = 1; | |
} elsif ( $calcs{'AllProcsTotalMem'} <= ( $mem{'MemTotal'} + $mem{'SwapFree'} ) ) { | |
print "WARNING: $result_prefix exceeds $result_availram, but still fits within free swap "; | |
printf ( "(uses %0.2f MB of %0.0f MB).\n", $calcs{'AllProcsTotalMem'} - $mem{'MemTotal'}, $mem{'SwapFree'} ); | |
$err = 1; | |
} else { | |
print "ERROR: $result_prefix exceeds $result_availram and free swap ($mem{'SwapFree'} MB) "; | |
printf ( "by %0.2f MB.\n", $calcs{'AllProcsTotalMem'} - ( $mem{'MemTotal'} + $mem{'SwapFree'} ) ); | |
$err = 2; | |
} | |
print "\n" if ( $opt{'verbose'} ); | |
if ( $opt{'debug'} ) { | |
print "DEBUG: OtherProcsMem($calcs{'OtherProcsMem'}) + MaxLimitHttpdMem($calcs{'MaxLimitHttpdMem'})"; | |
print " = AllProcsTotalMem($calcs{'AllProcsTotalMem'}) vs MemTotal($mem{'MemTotal'}) + SwapFree($mem{'SwapFree'})\n"; | |
} | |
exit $err; | |
# --------------- | |
# BEGIN FUNCTIONS | |
# --------------- | |
# | |
sub ShowUsage { | |
#------------------------------------------------------------------------------ | |
print "\nPurpose:\n\n"; | |
print "This script will attempt to predict the memory used by Apache Httpd processes\n"; | |
print "when the maximum configured limits are reached. The prediction is based on the\n"; | |
print "(calculated) HttpdRealAvg value -- an average of the memory used by each\n"; | |
print "running Httpd process. To see the HttpdRealAvg value, and all other calculated\n"; | |
print "variables, use the \"verbose\" command-line argument. There are no additional\n"; | |
print "modules required, unless you use the save/days/max command-line argument(s).\n"; | |
print "\nSyntax:\n\n"; | |
print "$0 [--help] [--debug] [--verbose] \\\n"; | |
print " [--exe=/path/to/httpd] [--swappct=#] --save] [--days=#] \\\n"; | |
print " [--max=realavg|running]\n\n"; | |
printf ("%-15s: %s\n", "--help", "This syntax summary."); | |
printf ("%-15s: %s\n", "--debug", "Show debugging messages as the script is executing."); | |
printf ("%-15s: %s\n", "--verbose", "Display a detailed report of all values found and calculated."); | |
printf ("%-15s: %s\n", "--exe=/path", "Path to httpd binary file (if non-standard)."); | |
printf ("%-15s: %s\n", "--config=/path", "Path to httpd configuration file (if non-standard)."); | |
printf ("%-15s: %s\n", "--swappct=#", "% of FREE swap use allowed before WARNING condition (default 0)."); | |
printf ("%-15s: %s\n", "--save", "Save average sizes to database ($dbname)."); | |
printf ("%-15s: %s\n", "--days=#", "Remove database entries older than # days (default 30)."); | |
printf ("%-15s: %s\n", "--max=realavg", "Use largest HttpdRealAvg size from current procs or database."); | |
printf ("%-15s: %s\n", "--max=running", "Use HttpdRealAvg size from the largest MaxRunning recorded."); | |
#------------------------------------------------------------------------------ | |
print "\nThe save/days/max command-line arguments require the DBD::SQLite perl module.\n"; | |
print "Use --max=running if the size and number of httpd processes increases and\n"; | |
print "decreases rapidly or unpredictably. The --max=realavg setting should be more\n"; | |
print "accurate for servers that have stable httpd sizes, and progressive increase /\n"; | |
print "decrease in the number of httpd processes.\n"; | |
print "\nExample:\n\n"; | |
print "/usr/local/bin/check_httpd_limits.pl --save --days=14 --max=realavg --swappct=25\n\n"; | |
exit $err; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment