|
#!/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; |
|
} |