Assembla home | Assembla project page
 

root/mysqltuner.pl

Revision 39, 35.9 kB (checked in by major, 1 month ago)

Version bump to 0.9.8
Added new options to help output
Made version checking off by default, enabled by --checkversion option

Line 
1 #!/usr/bin/perl -w
2 # mysqltuner.pl - Version 0.9.8
3 # High Performance MySQL Tuning Script
4 # Copyright (C) 2006-2008 Major Hayden - major@mhtx.net
5 #
6 # For the latest updates, please visit http://mysqltuner.com/
7 # Subversion repository available at http://tools.assembla.com/svn/mysqltuner/
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 #
22 # This project would not be possible without help from:
23 #   Matthew Montgomery
24 #   Paul Kehrer
25 #   Dave Burgess
26 #   Jonathan Hinds
27 #   Mike Jackson
28 #   Nils Breunese
29 #   Shawn Ashlee
30 #   Luuk Vosslamber
31 #   Ville Skytta
32 #   Trent Hornibrook
33 #   Jason Gill
34 #
35 # Inspired by Matthew Montgomery's tuning-primer.sh script:
36 # http://forge.mysql.com/projects/view.php?id=44
37 #
38 use strict;
39 use warnings;
40 use diagnostics;
41 use Getopt::Long;
42
43 # Set up a few variables for use in the script
44 my $tunerversion = "0.9.8";
45 my (@adjvars, @generalrec);
46
47 # Set defaults
48 my %opt = (
49                 "nobad" => 0,
50                 "nogood" => 0,
51                 "noinfo" => 0,
52                 "nocolor" => 0,
53                 "forcemem" => 0,
54                 "forceswap" => 0,
55                 "host" => 0,
56                 "port" => 0,
57                 "user" => 0,
58                 "pass" => 0,
59                 "skipsize" => 0,
60                 "checkversion" => 0,
61         );
62        
63 # Gather the options from the command line
64 GetOptions(\%opt,
65                 'nobad',
66                 'nogood',
67                 'noinfo',
68                 'nocolor',
69                 'forcemem=i',
70                 'forceswap=i',
71                 'host=s',
72                 'port=i',
73                 'user=s',
74                 'pass=s',
75                 'skipsize',
76                 'checkversion',
77                 'help',
78         );
79
80 if (defined $opt{'help'} && $opt{'help'} == 1) { usage(); }
81
82 sub usage {
83         # Shown with --help option passed
84         print "\n".
85                 "   MySQLTuner $tunerversion - MySQL High Performance Tuning Script\n".
86                 "   Bug reports, feature requests, and downloads at http://mysqltuner.com/\n".
87                 "   Maintained by Major Hayden (major\@mhtx.net) - Licensed under GPL\n".
88                 "\n".
89                 "   Important Usage Guidelines:\n".
90                 "      To run the script with the default options, run the script without arguments\n".
91                 "      Allow MySQL server to run for at least 24-48 hours before trusting suggestions\n".
92                 "      Some routines may require root level privileges (script will provide warnings)\n".
93                 "      You must provide the remote server's total memory when connecting to other servers\n".
94                 "\n".
95                 "   Connection and Authentication\n".
96                 "      --host <hostname>    Connect to a remote host to perform tests (default: localhost)\n".
97                 "      --port <port>        Port to use for connection (default: 3306)\n".
98                 "      --user <username>    Username to use for authentication\n".
99                 "      --pass <password>    Password to use for authentication\n".
100                 "\n".
101                 "   Performance and Reporting Options\n".
102                 "      --skipsize           Don't enumerate tables and their types/sizes (default: on)\n".
103                 "                             (Recommended for servers with many tables)\n".
104                 "      --checkversion       Check for updates to MySQLTuner (default: don't check)\n".
105                 "      --forcemem <size>    Amount of RAM installed in megabytes\n".
106                 "      --forceswap <size>   Amount of swap memory configured in megabytes\n".
107                 "\n".
108                 "   Output Options:\n".
109                 "      --nogood             Remove OK responses\n".
110                 "      --nobad              Remove negative/suggestion responses\n".
111                 "      --noinfo             Remove informational responses\n".
112                 "      --nocolor            Don't print output in color\n".
113                 "\n";
114         exit;
115 }
116
117 # Setting up the colors for the print styles
118 my $good = ($opt{nocolor} == 0)? "[\e[00;32mOK\e[00m]" : "[OK]" ;
119 my $bad = ($opt{nocolor} == 0)? "[\e[00;31m!!\e[00m]" : "[!!]" ;
120 my $info = ($opt{nocolor} == 0)? "[\e[00;34m--\e[00m]" : "[--]" ;
121
122 # Functions that handle the print styles
123 sub goodprint { print $good." ".$_[0] unless ($opt{nogood} == 1); }
124 sub infoprint { print $info." ".$_[0] unless ($opt{noinfo} == 1); }
125 sub badprint { print $bad." ".$_[0] unless ($opt{nobad} == 1); }
126 sub redwrap { return ($opt{nocolor} == 0)? "\e[00;31m".$_[0]."\e[00m" : $_[0] ; }
127 sub greenwrap { return ($opt{nocolor} == 0)? "\e[00;32m".$_[0]."\e[00m" : $_[0] ; }
128
129 # Calculates the parameter passed in bytes, and then rounds it to one decimal place
130 sub hr_bytes {
131         my $num = shift;
132         if ($num >= (1024**3)) { #GB
133                 return sprintf("%.1f",($num/(1024**3)))."G";
134         } elsif ($num >= (1024**2)) { #MB
135                 return sprintf("%.1f",($num/(1024**2)))."M";
136         } elsif ($num >= 1024) { #KB
137                 return sprintf("%.1f",($num/1024))."K";
138         } else {
139                 return $num."B";
140         }
141 }
142
143 # Calculates the parameter passed in bytes, and then rounds it to the nearest integer
144 sub hr_bytes_rnd {
145         my $num = shift;
146         if ($num >= (1024**3)) { #GB
147                 return int(($num/(1024**3)))."G";
148         } elsif ($num >= (1024**2)) { #MB
149                 return int(($num/(1024**2)))."M";
150         } elsif ($num >= 1024) { #KB
151                 return int(($num/1024))."K";
152         } else {
153                 return $num."B";
154         }
155 }
156
157 # Calculates the parameter passed to the nearest power of 1000, then rounds it to the nearest integer
158 sub hr_num {
159         my $num = shift;
160         if ($num >= (1000**3)) { # Billions
161                 return int(($num/(1000**3)))."B";
162         } elsif ($num >= (1000**2)) { # Millions
163                 return int(($num/(1000**2)))."M";
164         } elsif ($num >= 1000) { # Thousands
165                 return int(($num/1000))."K";
166         } else {
167                 return $num;
168         }
169 }
170
171 # Calculates uptime to display in a more attractive form
172 sub pretty_uptime {
173         my $uptime = shift;
174         my $seconds = $uptime % 60;
175         my $minutes = int(($uptime % 3600) / 60);
176         my $hours = int(($uptime % 86400) / (3600));
177         my $days = int($uptime / (86400));
178         my $uptimestring;
179         if ($days > 0) {
180                 $uptimestring = "${days}d ${hours}h ${minutes}m ${seconds}s";
181         } elsif ($hours > 0) {
182                 $uptimestring = "${hours}h ${minutes}m ${seconds}s";
183         } elsif ($minutes > 0) {
184                 $uptimestring = "${minutes}m ${seconds}s";
185         } else {
186                 $uptimestring = "${seconds}s";
187         }
188         return $uptimestring;
189 }
190
191 # Retrieves the memory installed on this machine
192 my ($physical_memory,$swap_memory,$duflags);
193 sub os_setup {
194         sub memerror {
195                 badprint "Unable to determine total memory/swap; use '--forcemem' and '--forceswap'\n";
196                 exit;
197         }
198         my $os = `uname`;
199         $duflags = ($os =~ /Linux/) ? '-b' : '';
200         if ($opt{'forcemem'} > 0) {
201                 $physical_memory = $opt{'forcemem'} * 1048576;
202                 infoprint "Assuming $opt{'forcemem'} MB of physical memory\n";
203                 if ($opt{'forceswap'} > 0) {
204                         $swap_memory = $opt{'forceswap'} * 1048576;
205                         infoprint "Assuming $opt{'forceswap'} MB of swap space\n";
206                 } else {
207                         $swap_memory = 0;
208                         badprint "Assuming 0 MB of swap space (use --forceswap to specify)\n";
209                 }
210         } else {
211                 if ($os =~ /Linux/) {
212                         $physical_memory = `free -b | grep Mem | awk '{print \$2}'` or memerror;
213                         $swap_memory = `free -b | grep Swap | awk '{print \$2}'` or memerror;
214                 } elsif ($os =~ /Darwin/) {
215                         $physical_memory = `sysctl -n hw.memsize` or memerror;
216                         $swap_memory = `sysctl -n vm.swapusage | awk '{print \$3}' | sed 's/\..*\$//'` or memerror;
217                 } elsif ($os =~ /NetBSD/) {
218                         $physical_memory = `sysctl -n hw.physmem` or memerror;
219                         $swap_memory = `swapctl -l | grep '^/' | awk '{ s+= \$2 } END { print s }'` or memerror;
220                 } elsif ($os =~ /BSD/) {
221                         $physical_memory = `sysctl -n hw.realmem`;
222                         $swap_memory = `swapinfo | grep '^/' | awk '{ s+= \$2 } END { print s }'`;
223                 }
224         }
225         chomp($physical_memory);
226 }
227
228 # Checks to see if a MySQL login is possible
229 my ($mysqllogin,$doremote,$remotestring);
230 sub mysql_setup {
231         $doremote = 0;
232         $remotestring = '';
233         my $command = `which mysqladmin`;
234         chomp($command);
235         if (! -e $command) {
236                 badprint "Unable to find mysqladmin in your \$PATH.  Is MySQL installed?\n";
237                 exit;
238         }
239         # Are we being asked to connect to a remote server?
240         if ($opt{host} ne 0) {
241                 chomp($opt{host});
242                 $opt{port} = ($opt{port} eq 0)? 3306 : $opt{port} ;
243                 # If we're doing a remote connection, but forcemem wasn't specified, we need to exit
244                 if ($opt{'forcemem'} eq 0) {
245                         badprint "The --forcemem option is required for remote connections\n";
246                         exit;
247                 }
248                 infoprint "Performing tests on $opt{host}:$opt{port}\n";
249                 $remotestring = " -h $opt{host} -P $opt{port}";
250                 $doremote = 1;
251         }
252         # Did we already get a username and password passed on the command line?
253         if ($opt{user} ne 0 and $opt{pass} ne 0) {
254                 $mysqllogin = "-u $opt{user} -p'$opt{pass}'".$remotestring;
255                 my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`;
256                 if ($loginstatus =~ /mysqld is alive/) {
257                         goodprint "Logged in using credentials passed on the command line\n";
258                         return 1;
259                 } else {
260                         badprint "Attempted to use login credentials, but they were invalid\n";
261                         exit 0;
262                 }
263         }
264         if ( -r "/etc/psa/.psa.shadow" and $doremote == 0 ) {
265                 # It's a Plesk box, use the available credentials
266                 $mysqllogin = "-u admin -p`cat /etc/psa/.psa.shadow`";
267                 my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`;
268                 unless ($loginstatus =~ /mysqld is alive/) {
269                         badprint "Attempted to use login credentials from Plesk, but they failed.\n";
270                         exit 0;
271                 }
272         } else {
273                 # It's not Plesk, we should try a login
274                 my $loginstatus = `mysqladmin $remotestring ping 2>&1`;
275                 if ($loginstatus =~ /mysqld is alive/) {
276                         # Login went just fine
277                         $mysqllogin = "";
278                         # Did this go well because of a .my.cnf file or is there no password set?
279                         my $userpath = `ls -d ~`;
280                         chomp($userpath);
281                         unless ( -e "$userpath/.my.cnf" ) {
282                                 badprint "Successfully authenticated with no password - SECURITY RISK!\n";
283                         }
284                         return 1;
285                 } else {
286                         print STDERR "Please enter your MySQL administrative login: ";
287                         my $name = <>;
288                         print STDERR "Please enter your MySQL administrative password: ";
289                         system("stty -echo");
290                         my $password = <>;
291                         system("stty echo");
292                         chomp($password);
293                         chomp($name);
294                         $mysqllogin = "-u $name";
295                         if (length($password) > 0) {
296                                 $mysqllogin .= " -p'$password'";
297                         }
298                         $mysqllogin .= $remotestring;
299                         my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`;
300                         if ($loginstatus =~ /mysqld is alive/) {
301                                 print STDERR "\n";
302                                 if (! length($password)) {
303                                         # Did this go well because of a .my.cnf file or is there no password set?
304                                         my $userpath = `ls -d ~`;
305                                         chomp($userpath);
306                                         unless ( -e "$userpath/.my.cnf" ) {
307                                                 badprint "Successfully authenticated with no password - SECURITY RISK!\n";
308                                         }
309                                 }
310                                 return 1;
311                         } else {
312                                 print "\n".$bad." Attempted to use login credentials, but they were invalid.\n";
313                                 exit 0;
314                         }
315                         exit 0;
316                 }
317         }
318 }
319
320 # Populates all of the variable and status hashes
321 my (%mystat,%myvar,$dummyselect);
322 sub get_all_vars {
323         # We need to initiate at least one query so that our data is useable
324         $dummyselect = `mysql $mysqllogin -Bse "SELECT VERSION();"`;
325         my @mysqlvarlist = `mysql $mysqllogin -Bse "SHOW /*!50000 GLOBAL */ VARIABLES;"`;
326         foreach my $line (@mysqlvarlist) {
327                 $line =~ /([a-zA-Z_]*)\s*(.*)/;
328                 $myvar{$1} = $2;
329         }
330         my @mysqlstatlist = `mysql $mysqllogin -Bse "SHOW /*!50000 GLOBAL */ STATUS;"`;
331         foreach my $line (@mysqlstatlist) {
332                 $line =~ /([a-zA-Z_]*)\s*(.*)/;
333                 $mystat{$1} = $2;
334         }
335 }
336
337 # Checks for updates to MySQLTuner
338 sub validate_tuner_version {
339         print "\n-------- General Statistics --------------------------------------------------\n";
340         if ($opt{checkversion} eq 0) {
341                 infoprint "Skipped version check for MySQLTuner script\n";
342                 return;
343         }
344         my $update;
345         my $url = "http://mysqltuner.com/versioncheck.php?v=$tunerversion";
346         if (-e "/usr/bin/curl") {
347                 $update = `/usr/bin/curl --connect-timeout 5 '$url' 2>/dev/null`;
348                 chomp($update);
349         } elsif (-e "/usr/bin/wget") {
350                 $update = `/usr/bin/wget -e timestamping=off -T 5 -O - '$url' 2>/dev/null`;
351                 chomp($update);
352         }
353         if ($update eq 1) {
354                 badprint "There is a new version of MySQLTuner available\n";
355         } elsif ($update eq 0) {
356                 goodprint "You have the latest version of MySQLTuner\n";
357         } else {
358                 infoprint "Unable to check for the latest MySQLTuner version\n";
359         }
360 }
361
362 # Checks for supported or EOL'ed MySQL versions
363 my ($mysqlvermajor,$mysqlverminor);
364 sub validate_mysql_version {
365         ($mysqlvermajor,$mysqlverminor) = $myvar{'version'} =~ /(\d)\.(\d)/;
366         if ($mysqlvermajor < 5) {
367                 badprint "Your MySQL version ".$myvar{'version'}." is EOL software!  Upgrade soon!\n";
368         } elsif ($mysqlvermajor == 5) {
369                 goodprint "Currently running supported MySQL version ".$myvar{'version'}."\n";
370         } else {
371                 badprint "Currently running unsupported MySQL version ".$myvar{'version'}."\n";
372         }
373 }
374
375 # Checks for 32-bit boxes with more than 2GB of RAM
376 my ($arch);
377 sub check_architecture {
378         if ($doremote eq 1) { return; }
379         if (`uname -m` =~ /64/) {
380                 $arch = 64;
381                 goodprint "Operating on 64-bit architecture\n";
382         } else {
383                 $arch = 32;
384                 if ($physical_memory > 2147483648) {
385                         badprint "Switch to 64-bit OS - MySQL cannot currenty use all of your RAM\n";
386                 } else {
387                         goodprint "Operating on 32-bit architecture with less than 2GB RAM\n";
388                 }
389         }
390 }
391
392 # Start up a ton of storage engine counts/statistics
393 my (%enginestats,%enginecount,$fragtables);
394 sub check_storage_engines {
395         if ($opt{skipsize} eq 1) {
396                 print "\n-------- Storage Engine Statistics -------------------------------------------\n";
397                 infoprint "Skipped due to --skipsize option\n";
398                 return;
399         }
400         print "\n-------- Storage Engine Statistics -------------------------------------------\n";
401         infoprint "Status: ";
402         my $engines;
403         $engines .= (defined $myvar{'have_archive'} && $myvar{'have_archive'} eq "YES")? greenwrap "+Archive " : redwrap "-Archive " ;
404         $engines .= (defined $myvar{'have_bdb'} && $myvar{'have_bdb'} eq "YES")? greenwrap "+BDB " : redwrap "-BDB " ;
405         $engines .= (defined $myvar{'have_federated'} && $myvar{'have_federated'} eq "YES")? greenwrap "+Federated " : redwrap "-Federated " ;
406         $engines .= (defined $myvar{'have_innodb'} && $myvar{'have_innodb'} eq "YES")? greenwrap "+InnoDB " : redwrap "-InnoDB " ;
407         $engines .= (defined $myvar{'have_isam'} && $myvar{'have_isam'} eq "YES")? greenwrap "+ISAM " : redwrap "-ISAM " ;
408         $engines .= (defined $myvar{'have_ndbcluster'} && $myvar{'have_ndbcluster'} eq "YES")? greenwrap "+NDBCluster " : redwrap "-NDBCluster " ;     
409         print "$engines\n";
410         if ($mysqlvermajor >= 5) {
411                 # MySQL 5 servers can have table sizes calculated quickly from information schema
412                 my @templist = `mysql $mysqllogin -Bse "SELECT ENGINE,SUM(DATA_LENGTH),COUNT(ENGINE) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','mysql') GROUP BY ENGINE HAVING SUM(DATA_LENGTH) > 0 ORDER BY ENGINE ASC;"`;
413                 foreach my $line (@templist) {
414                         my ($engine,$size,$count);
415                         ($engine,$size,$count) = $line =~ /([a-zA-Z_]*)\s+(\d+)\s+(\d+)/;
416                         $enginestats{$engine} = $size;
417                         $enginecount{$engine} = $count;
418                 }
419                 $fragtables = `mysql $mysqllogin -Bse "SELECT COUNT(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','mysql') AND Data_free > 0"`;
420                 chomp($fragtables);
421         } else {
422                 # MySQL < 5 servers take a lot of work to get table sizes
423                 my @tblist;
424                 # Now we build a database list, and loop through it to get storage engine stats for tables
425                 my @dblist = `mysql $mysqllogin -Bse "SHOW DATABASES"`;
426                 foreach my $db (@dblist) {
427                         chomp($db);
428                         if ($db eq "information_schema") { next; }
429                         if ($mysqlvermajor == 3 || ($mysqlvermajor == 4 && $mysqlverminor == 0)) {
430                                 # MySQL 3.23/4.0 keeps Data_Length in the 6th column
431                                 push (@tblist,`mysql $mysqllogin -Bse "SHOW TABLE STATUS FROM \\\`$db\\\`" | awk '{print \$2,\$6,\$9}'`);
432                         } else {
433                                 # MySQL 4.1+ keeps Data_Length in the 7th column
434                                 push (@tblist,`mysql $mysqllogin -Bse "SHOW TABLE STATUS FROM \\\`$db\\\`" | awk '{print \$2,\$7,\$10}'`);
435                         }
436                 }
437                 # Parse through the table list to generate storage engine counts/statistics
438                 $fragtables = 0;
439                 foreach my $line (@tblist) {
440                         chomp($line);
441                         $line =~ /([a-zA-Z_]*)\s+(\d+)\s+(\d+)/;
442                         my $engine = $1;
443                         my $size = $2;
444                         my $datafree = $3;
445                         if ($size !~ /^\d+$/) { $size = 0; }
446                         if (defined $enginestats