#!/usr/bin/perl -w use strict; use warnings; use diagnostics; use Getopt::Long; # Set defaults my %opt = ( "nobad" => 0, "nogood" => 0, "noinfo" => 0, "nocolor" => 0, ); # Gather the options from the command line GetOptions(\%opt, 'nobad', 'nogood', 'noinfo', 'nocolor', 'help', ); if (defined $opt{'help'} && $opt{'help'} == 1) { usage(); } sub usage { print "\n". " MySQL High Performance Tuning Script\n". " Bug reports, feature requests, and downloads at http://mysqltuner.com/\n". " Maintained by Major Hayden (major.hayden\@rackspace.com)\n\n". " Important Usage Guidelines:\n". " To run the script with the default options, run the script without arguments\n". " Allow MySQL server to run for at least 24-48 hours before trusting suggestions\n". " Some routines may require root level privileges (script will provide warnings)\n\n". " Output Options:\n". " --nogood Remove OK responses\n". " --nobad Remove negative/suggestion responses\n". " --noinfo Remove informational responses\n". " --nocolor Don't print output in color\n". "\n"; exit; } # CONFIGURATION ITEMS my ($good,$bad,$info); if ($opt{nocolor} == 0) { $good = "[\e[00;32mOK\e[00m]"; $bad = "[\e[00;31m!!\e[00m]"; $info = "[\e[00;34m--\e[00m]"; } else { $good = "[OK]"; $bad = "[!!]"; $info = "[--]"; } sub goodprint { if ($opt{nogood} == 1) { return 0; } my $text = shift; print $good." ".$text; } sub infoprint { if ($opt{noinfo} == 1) { return 0; } my $text = shift; print $info." ".$text; } sub badprint { if ($opt{nobad} == 1) { return 0; } my $text = shift; print $bad." ".$text; } my ($physical_memory,$swap_memory,$duflags); sub os_setup { my $os = `uname`; $duflags = ''; if ($os =~ /Linux/) { $physical_memory = `free -b | grep Mem | awk '{print \$2}'`; $swap_memory = `free -b | grep Swap | awk '{print \$2}'`; $duflags = '-b'; } elsif ($os =~ /Darwin/) { $physical_memory = `sysctl -n hw.memsize`; $swap_memory = `sysctl -n vm.swapusage | awk '{print \$3}' | sed 's/\..*\$//'`; } elsif ($os =~ /BSD/) { $physical_memory = `sysctl -n hw.realmem`; $swap_memory = `swapinfo | grep '^/' | awk '{ s+= \$2 } END { print s }'`; } chomp($physical_memory); } my $mysqllogin; sub mysql_setup { my $command = `which mysqladmin`; chomp($command); if (! -e $command) { badprint "Unable to find mysqladmin in your \$PATH. Is MySQL installed?\n"; exit; } if ( -r "/etc/psa/.psa.shadow" ) { # It's a Plesk box, use the available credentials $mysqllogin = "-u admin -p`cat /etc/psa/.psa.shadow`"; my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`; if ($loginstatus =~ /mysqld is alive/) { # Login was successful, but we won't say anything to save space return 1; } else { badprint "Attempted to use login credentials from Plesk, but they failed.\n"; exit 0; } } else { # It's not Plesk, we should try a login my $loginstatus = `mysqladmin ping 2>&1`; if ($loginstatus =~ /mysqld is alive/) { # Login went just fine $mysqllogin = ""; # Did this go well because of a .my.cnf file or is there no password set? my $userpath = `ls -d ~`; chomp($userpath); if ( -e "$userpath/.my.cnf" ) { # Login was successful, but we won't say anything to save space } else { badprint "Successfully authenticated with no password - SECURITY RISK!\n"; } return 1; } else { print "Please enter your MySQL login: "; my $name = <>; print "Please enter your MySQL password: "; system("stty -echo"); #don't show the password my $password = <>; system("stty echo"); #plz give echo back chomp($password); chomp($name); $mysqllogin = "-u $name -p'$password'"; my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`; if ($loginstatus =~ /mysqld is alive/) { # Login was successful, but we won't say anything to save space print "\n"; return 1; } else { print "\n".$bad." Attempted to use login credentials, but they were invalid.\n"; exit 0; } exit 0; } } } my (%mystat,%myvar); sub get_all_vars { my @mysqlvarlist = `mysql $mysqllogin -Bse "SHOW /*!50000 GLOBAL */ VARIABLES;"`; foreach my $line (@mysqlvarlist) { $line =~ /([a-zA-Z_]*)\s*(.*)/; $myvar{$1} = $2; } my @mysqlstatlist = `mysql $mysqllogin -Bse "SHOW /*!50000 GLOBAL */ STATUS;"`; foreach my $line (@mysqlstatlist) { $line =~ /([a-zA-Z_]*)\s*(.*)/; $mystat{$1} = $2; } } my ($mysqlvermajor,$mysqlverminor); sub validate_mysql_version { ($mysqlvermajor,$mysqlverminor) = $myvar{'version'} =~ /(\d)\.(\d)/; if ($mysqlvermajor < 5) { badprint "Your MySQL version ".$myvar{'version'}." is EOL software! Upgrade soon!\n"; } elsif ($mysqlvermajor == 5 && $mysqlverminor == 1) { badprint "Currently running supported MySQL version ".$myvar{'version'}." (BETA - USE CAUTION)\n"; } else { goodprint "Currently running supported MySQL version ".$myvar{'version'}."\n"; } } sub pretty_uptime { my $uptime = shift; my $seconds = $uptime % 60; my $minutes = int(($uptime % 3600) / 60); my $hours = int(($uptime % 86400) / (3600)); my $days = int($uptime / (86400)); my $uptimestring; if ($days > 0) { $uptimestring = "${days}d ${hours}h ${minutes}m ${seconds}s"; } elsif ($hours > 0) { $uptimestring = "${hours}h ${minutes}m ${seconds}s"; } elsif ($minutes > 0) { $uptimestring = "${minutes}m ${seconds}s"; } else { $uptimestring = "${seconds}s"; } return $uptimestring; } sub hr_bytes_rnd { my $num = shift; if ($num >= (1024**3)) { #GB return int(($num/(1024**3)))."G"; } elsif ($num >= (1024**2)) { #MB return int(($num/(1024**2)))."M"; } elsif ($num >= 1024) { #KB return int(($num/1024))."K"; } else { return $num; } } sub hr_bytes { my $num = shift; if ($num >= (1024**3)) { #GB return sprintf("%.1f",($num/(1024**3)))."G"; } elsif ($num >= (1024**2)) { #MB return sprintf("%.1f",($num/(1024**2)))."M"; } elsif ($num >= 1024) { #KB return sprintf("%.1f",($num/1024))."K"; } else { return $num; } } sub hr_num { my $num = shift; if ($num >= (1000**3)) { #GB return int(($num/(1000**3)))."G"; } elsif ($num >= (1000**2)) { #MB return int(($num/(1000**2)))."M"; } elsif ($num >= 1000) { #KB return int(($num/1000))."K"; } else { return $num; } } my %mycalc; sub calculations { if ($mystat{'Questions'} < 1) { badprint "Your server has not answered any queries - cannot continue..."; exit 0; } # Per-thread memory if ($mysqlvermajor > 3) { $mycalc{'per_thread_buffers'} = $myvar{'read_buffer_size'} + $myvar{'read_rnd_buffer_size'} + $myvar{'sort_buffer_size'} + $myvar{'thread_stack'} + $myvar{'join_buffer_size'}; } else { $mycalc{'per_thread_buffers'} = $myvar{'record_buffer'} + $myvar{'record_rnd_buffer'} + $myvar{'sort_buffer'} + $myvar{'thread_stack'} + $myvar{'join_buffer_size'}; } $mycalc{'total_per_thread_buffers'} = $mycalc{'per_thread_buffers'} * $myvar{'max_connections'}; $mycalc{'max_total_per_thread_buffers'} = $mycalc{'per_thread_buffers'} * $mystat{'Max_used_connections'}; # Server-wide memory $mycalc{'max_tmp_table_size'} = ($myvar{'tmp_table_size'} > $myvar{'max_heap_table_size'}) ? $myvar{'max_heap_table_size'} : $myvar{'tmp_table_size'} ; $mycalc{'server_buffers'} = $myvar{'key_buffer_size'} + $mycalc{'max_tmp_table_size'}; $mycalc{'server_buffers'} += (defined $myvar{'innodb_buffer_pool_size'}) ? $myvar{'innodb_buffer_pool_size'} : 0 ; $mycalc{'server_buffers'} += (defined $myvar{'innodb_additional_mem_pool_size'}) ? $myvar{'innodb_additional_mem_pool_size'} : 0 ; $mycalc{'server_buffers'} += (defined $myvar{'innodb_log_buffer_size'}) ? $myvar{'innodb_log_buffer_size'} : 0 ; $mycalc{'server_buffers'} += (defined $myvar{'query_cache_size'}) ? $myvar{'query_cache_size'} : 0 ; # Global memory $mycalc{'max_used_memory'} = $mycalc{'server_buffers'} + $mycalc{"max_total_per_thread_buffers"}; $mycalc{'total_possible_used_memory'} = $mycalc{'server_buffers'} + $mycalc{'total_per_thread_buffers'}; $mycalc{'pct_physical_memory'} = int(($mycalc{'total_possible_used_memory'} * 100) / $physical_memory); # Slow queries $mycalc{'pct_slow_queries'} = int(($mystat{'Slow_queries'}/$mystat{'Questions'}) * 100); # Connections $mycalc{'pct_connections_used'} = int(($mystat{'Max_used_connections'}/$myvar{'max_connections'}) * 100); $mycalc{'pct_connections_used'} = ($mycalc{'pct_connections_used'} > 100) ? 100 : $mycalc{'pct_connections_used'} ; # Key buffers if ($mysqlvermajor > 3) { $mycalc{'pct_key_buffer_used'} = sprintf("%.1f",(1 - (($mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'}) / $myvar{'key_buffer_size'})) * 100); } if ($mystat{'Key_read_requests'} > 0) { $mycalc{'pct_keys_from_mem'} = sprintf("%.1f",(100 - (($mystat{'Key_reads'} / $mystat{'Key_read_requests'}) * 100))); } if ($mysqlvermajor < 5) { $mycalc{'total_myisam_indexes'} = `find $myvar{'datadir'} -name '*.MYI' 2>&1 | xargs du $duflags '{}' 2>&1 | awk '{ s += \$1 } END { print s }'`; if ($mycalc{'total_myisam_indexes'} =~ /^0\n$/) { $mycalc{'total_myisam_indexes'} = "fail"; } } else { $mycalc{'total_myisam_indexes'} = `mysql $mysqllogin -Bse "/*!50000 SELECT SUM(INDEX_LENGTH) from information_schema.TABLES where ENGINE='MyISAM' */"`; } chomp($mycalc{'total_myisam_indexes'}); # Query cache if ($mysqlvermajor > 3) { $mycalc{'query_cache_efficiency'} = sprintf("%.1f",($mystat{'Qcache_hits'} / ($mystat{'Com_select'} + $mystat{'Qcache_hits'})) * 100); if ($myvar{'query_cache_size'}) { $mycalc{'pct_query_cache_used'} = sprintf("%.1f",100 - ($mystat{'Qcache_free_memory'} / $myvar{'query_cache_size'}) * 100); } if ($mystat{'Qcache_lowmem_prunes'} == 0) { $mycalc{'query_cache_prunes_per_day'} = 0; } else { $mycalc{'query_cache_prunes_per_day'} = int($mystat{'Qcache_lowmem_prunes'} / ($mystat{'Uptime'}/86400)); } } # Sorting $mycalc{'total_sorts'} = $mystat{'Sort_scan'} + $mystat{'Sort_range'}; if ($mycalc{'total_sorts'} > 0) { $mycalc{'pct_temp_sort_table'} = int(($mystat{'Sort_merge_passes'} / $mycalc{'total_sorts'}) * 100); } # Joins $mycalc{'joins_without_indexes'} = $mystat{'Select_range_check'} + $mystat{'Select_full_join'}; # Temporary tables if ($mystat{'Created_tmp_tables'} > 0) { if ($mystat{'Created_tmp_disk_tables'} > 0) { $mycalc{'pct_temp_disk'} = int(($mystat{'Created_tmp_disk_tables'} / $mystat{'Created_tmp_tables'}) * 100); } else { $mycalc{'pct_temp_disk'} = 0; } } # Table cache if ($mystat{'Opened_tables'} > 0) { $mycalc{'table_cache_hit_rate'} = int($mystat{'Open_tables'}*100/$mystat{'Opened_tables'}); } else { $mycalc{'table_cache_hit_rate'} = 100; } # Open files if ($mystat{'Open_files'} > 0 && $myvar{'open_files_limit'} > 0) { $mycalc{'pct_files_open'} = int($mystat{'Open_files'}*100/$myvar{'open_files_limit'}); } # Table locks if ($mystat{'Table_locks_immediate'} > 0) { if ($mystat{'Table_locks_waited'} == 0) { $mycalc{'pct_table_locks_immediate'} = 100; } else { $mycalc{'pct_table_locks_immediate'} = int($mystat{'Table_locks_immediate'}*100/($mystat{'Table_locks_waited'} + $mystat{'Table_locks_immediate'})); } } # Thread cache $mycalc{'thread_cache_hit_rate'} = int(100 - (($mystat{'Threads_created'} / $mystat{'Connections'}) * 100)); #foreach my $key (sort keys %mycalc) { print "$key\t\t-> \t".$mycalc{$key}."\n"; } } my (@decvars, @incvars, @generalrec); sub mysql_stats { print "-------- General Statistics --------------------------------------------------\n"; # Show uptime, queries per second, connections, traffic stats my $qps; if ($mystat{'Uptime'} > 0) { $qps = sprintf("%.3f",$mystat{'Questions'}/$mystat{'Uptime'}); } if ($mystat{'Uptime'} < 86400) { push(@generalrec,"MySQL started within last 24 hours - recommendations may be inaccurate"); } infoprint "Up for: ".pretty_uptime($mystat{'Uptime'})." (".hr_num($mystat{'Questions'}). " q [".hr_num($qps)." qps], ".hr_num($mystat{'Connections'})." conn,". " TX: ".hr_num($mystat{'Bytes_sent'}).", RX: ".hr_num($mystat{'Bytes_received'}).")\n"; # Memory usage if ($mycalc{'pct_physical_memory'} > 85) { badprint "Maximum possible memory usage: ".hr_bytes($mycalc{'total_possible_used_memory'})." ($mycalc{'pct_physical_memory'}% of installed RAM)\n"; push(@generalrec,"Reduce your overall MySQL memory footprint for system stability"); } else { goodprint "Maximum possible memory usage: ".hr_bytes($mycalc{'total_possible_used_memory'})." ($mycalc{'pct_physical_memory'}% of installed RAM)\n"; } # Slow queries if ($mycalc{'pct_slow_queries'} > 5) { badprint "Slow queries: $mycalc{'pct_slow_queries'}%\n"; } else { goodprint "Slow queries: $mycalc{'pct_slow_queries'}%\n"; } if ($myvar{'long_query_time'} > 5) { push(@decvars,"long_query_time (<= 5)"); } # Connections if ($mycalc{'pct_connections_used'} > 85) { badprint "Highest connection usage: $mycalc{'pct_connections_used'}%\n"; push(@incvars,"max_connections (> ".$myvar{'max_connections'}.")"); push(@decvars,"wait_timeout (< ".$myvar{'wait_timeout'}.")","interactive_timeout (< ".$myvar{'interactive_timeout'}.")"); push(@generalrec,"Reduce or eliminate persistent connections to reduce connection usage") } else { goodprint "Highest usage of available connections: $mycalc{'pct_connections_used'}%\n"; } # Key buffer if ($mycalc{'total_myisam_indexes'} =~ /^fail$/) { badprint "Cannot calculate MyISAM index size - re-run script as root user\n"; } elsif ($mycalc{'total_myisam_indexes'} == "0") { badprint "None of your MyISAM tables are indexed - add indexes immediately\n"; } else { if ($myvar{'key_buffer_size'} < $mycalc{'total_myisam_indexes'}) { badprint "Key buffer size / total MyISAM indexes: ".hr_bytes($myvar{'key_buffer_size'})."/".hr_bytes($mycalc{'total_myisam_indexes'})."\n"; push(@incvars,"key_buffer_size (> ".hr_bytes($mycalc{'total_myisam_indexes'}).")"); } else { goodprint "Key buffer size / total MyISAM indexes: ".hr_bytes($myvar{'key_buffer_size'})."/".hr_bytes($mycalc{'total_myisam_indexes'})."\n"; } if ($mystat{'Key_read_requests'} > 0) { if ($mycalc{'pct_keys_from_mem'} < 95) { badprint "Key buffer hit rate: $mycalc{'pct_keys_from_mem'}%\n"; } else { goodprint "Key buffer hit rate: $mycalc{'pct_keys_from_mem'}%\n"; } } else { # For the sake of space, we will be quiet here # No queries have run that would use keys } } if ($mysqlvermajor > 3 && $myvar{'max_seeks_for_key'} > 100) { push(@decvars,"max_seeks_for_key (<= 100)"); } # Query cache if ($mysqlvermajor < 4) { # For the sake of space, we will be quiet here # MySQL versions < 4.01 don't support query caching push(@generalrec,"Upgrade MySQL to version 4+ to utilize query caching"); } elsif ($myvar{'query_cache_size'} < 1) { badprint "Query cache is disabled\n"; push(@incvars,"query_cache_size (>= 8M)"); } elsif ($mystat{'Com_select'} == 0) { badprint "Query cache cannot be analyzed - no SELECT statements executed\n"; } else { if ($mycalc{'query_cache_efficiency'} < 20) { badprint "Query cache efficiency: $mycalc{'query_cache_efficiency'}%\n"; push(@incvars,"query_cache_limit (> 1M, or use smaller result sets)"); } else { goodprint "Query cache efficiency: $mycalc{'query_cache_efficiency'}%\n"; } if ($mycalc{'query_cache_prunes_per_day'} > 98) { badprint "Query cache prunes per day: $mycalc{'query_cache_prunes_per_day'}\n"; push(@incvars,"query_cache_size (> ".hr_bytes_rnd($myvar{'query_cache_size'}).")") } else { goodprint "Query cache prunes per day: $mycalc{'query_cache_prunes_per_day'}\n"; } } # Sorting if ($mycalc{'total_sorts'} == 0) { # For the sake of space, we will be quiet here # No sorts have run yet } elsif ($mycalc{'pct_temp_sort_table'} > 10) { badprint "Sorts requiring temporary tables: $mycalc{'pct_temp_sort_table'}%\n"; push(@incvars,"sort_buffer_size (> ".hr_bytes_rnd($myvar{'sort_buffer_size'}).")"); push(@incvars,"read_rnd_buffer_size (> ".hr_bytes_rnd($myvar{'read_rnd_buffer_size'}).")"); } else { goodprint "Sorts requiring temporary tables: $mycalc{'pct_temp_sort_table'}%\n"; } # Joins if ($mycalc{'joins_without_indexes'} > 0) { badprint "Joins performed without indexes: $mycalc{'joins_without_indexes'}\n"; push(@incvars,"join_buffer_size (> ".hr_bytes($myvar{'join_buffer_size'}).", or always use indexes with joins)"); push(@generalrec,"Adjust your join queries to always utilize indexes"); } else { # For the sake of space, we will be quiet here # No joins have run without indexes } # Temporary tables if ($mystat{'Created_tmp_tables'} > 0) { if ($mycalc{'pct_temp_disk'} > 25) { badprint "Temporary tables created on disk: $mycalc{'pct_temp_disk'}%\n"; push(@incvars,"tmp_table_size (> ".hr_bytes_rnd($myvar{'tmp_table_size'}).")"); push(@incvars,"max_heap_table_size (> ".hr_bytes_rnd($myvar{'max_heap_table_size'}).")"); push(@generalrec,"Be sure that tmp_table_size/max_heap_table_size are equal"); } else { goodprint "Temporary tables created on disk: $mycalc{'pct_temp_disk'}%\n"; } } else { # For the sake of space, we will be quiet here # No temporary tables have been created } # Thread cache if ($mycalc{'thread_cache_hit_rate'} <= 50) { badprint "Thread cache hit rate: $mycalc{'thread_cache_hit_rate'}%\n"; } else { goodprint "Thread cache hit rate: $mycalc{'thread_cache_hit_rate'}%\n"; } # Table cache if ($mystat{'Open_tables'} > 0) { if ($mycalc{'table_cache_hit_rate'} < 20) { badprint "Table cache hit rate: $mycalc{'table_cache_hit_rate'}%\n"; push(@incvars,"table_cache (> ".$myvar{'table_cache'}.")"); push(@generalrec,"Increase table_cache gradually to avoid file descriptor limits"); } else { goodprint "Table cache hit rate: $mycalc{'table_cache_hit_rate'}%\n"; } } # Open files if ($myvar{'open_files_limit'} > 0) { if ($mycalc{'pct_files_open'} > 85) { badprint "Open file limit used: $mycalc{'pct_files_open'}%\n"; push(@incvars,"open_files_limit (> ".$myvar{'open_files_limit'}.")"); } else { goodprint "Open file limit used: $mycalc{'pct_files_open'}%\n"; } } # Table locks if (defined $mycalc{'pct_table_locks_immediate'}) { if ($mycalc{'pct_table_locks_immediate'} < 95) { badprint "Table locks acquired immediately: $mycalc{'pct_table_locks_immediate'}%\n"; push(@generalrec,"Optimize queries and/or use InnoDB to reduce lock wait"); } else { goodprint "Table locks acquired immediately: $mycalc{'pct_table_locks_immediate'}%\n"; } } # Performance options if ($mysqlvermajor == 3 || ($mysqlvermajor == 4 && $mysqlverminor == 0)) { push(@generalrec,"Upgrade to MySQL 4.1+ to use concurrent MyISAM inserts"); } elsif ($myvar{'concurrent_insert'} eq "OFF" || $myvar{'concurrent_insert'} < 1) { if ($mysqlvermajor == 4) { push(@generalrec,"Enable concurrent_insert by setting it to 'ON'"); } else { push(@generalrec,"Enable concurrent_insert by setting it to 1"); } } } sub make_recommendations { print "-------- Recommendations -----------------------------------------------------\n"; if (@generalrec > 0) { print "General recommendations:\n"; foreach (@generalrec) { print " ".$_."\n"; } } if (@incvars > 0) { print "Variables to increase:\n"; if ($mycalc{'pct_physical_memory'} > 85) { print " *** MySQL's maximum memory usage exceeds your installed memory ***\n". " *** Add more RAM before increasing any MySQL buffer variables ***\n"; } foreach (@incvars) { print " ".$_."\n"; } } if (@decvars > 0) { print "Variables to decrease:\n"; foreach (@decvars) { print " ".$_."\n"; } } } # --------------------------------------------------------------------------- # BEGIN 'MAIN' # --------------------------------------------------------------------------- print " MySQL High-Performance Tuner - Major Hayden \n". " Bug reports, feature requests, and downloads at mysqltuner.com\n". " Run with '--help' for additional options and output filtering\n"; os_setup; # Set up some OS variables mysql_setup; # Gotta login first get_all_vars; # Toss variables/status into hashes validate_mysql_version; # Check current MySQL version calculations; # Calculate everything we need mysql_stats; # Print the server stats make_recommendations; # Make recommendations based on stats # --------------------------------------------------------------------------- # END 'MAIN' # ---------------------------------------------------------------------------