diff --git a/mysqltuner.pl b/mysqltuner.pl index f4134affe..686c889c1 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -2546,7 +2546,7 @@ sub log_file_recommendations { } # Try to find logs from docker/podman if file doesn't exist locally - elsif (!-f "$myvar{'log_error'}" + elsif ( !-f "$myvar{'log_error'}" && $myvar{'log_error'} !~ /^(docker|podman|kubectl|systemd):/ && !is_docker() ) { @@ -3158,18 +3158,22 @@ sub get_system_info { ? 'Windows' : ( $prefix eq '' ? $sysname : execute_system_command('uname -o') ); infoprint "Operating System Type : " - . ( $is_win + . ( + $is_win ? 'Windows' - : ( $prefix eq '' ? $sysname : execute_system_command('uname -o') ) ); + : ( $prefix eq '' ? $sysname : execute_system_command('uname -o') ) + ); $result{'OS'}{'Kernel'} = $is_win ? execute_system_command('ver') : ( $prefix eq '' ? $release : execute_system_command('uname -r') ); infoprint "Kernel Release : " - . ( $is_win + . ( + $is_win ? execute_system_command('ver') - : ( $prefix eq '' ? $release : execute_system_command('uname -r') ) ); + : ( $prefix eq '' ? $release : execute_system_command('uname -r') ) + ); $result{'OS'}{'Hostname'} = ( !$is_win && $prefix eq '' ) ? $nodename : Sys::Hostname::hostname(); @@ -3192,9 +3196,11 @@ sub get_system_info { infocmd_tab "ifconfig| grep -A1 mtu"; } infoprint "Internal IP : " - . ( ( !$is_win && $prefix eq '' ) + . ( + ( !$is_win && $prefix eq '' ) ? execute_system_command('hostname -I') - : infocmd_one "hostname -I" ); + : infocmd_one "hostname -I" + ); if ( which( "ip", $ENV{'PATH'} ) ) { $result{'Network'}{'Internal Ip'} = execute_system_command('ip addr | grep -A1 mtu'); @@ -7269,7 +7275,7 @@ sub mariadb_xtradb { infoprint "XtraDB is enabled."; infoprint "Note that MariaDB 10.2 makes use of InnoDB, not XtraDB." - # Not implemented + # Not implemented } # Recommendations for RocksDB @@ -8585,7 +8591,7 @@ sub mysql_innodb { infoprint "innodb_buffer_pool_chunk_size is set to 'autosize' (0) in MariaDB >= 10.8. Skipping chunk size checks."; } - elsif (!defined( $myvar{'innodb_buffer_pool_chunk_size'} ) + elsif ( !defined( $myvar{'innodb_buffer_pool_chunk_size'} ) || $myvar{'innodb_buffer_pool_chunk_size'} == 0 || !defined( $myvar{'innodb_buffer_pool_size'} ) || $myvar{'innodb_buffer_pool_size'} == 0 @@ -9657,7 +9663,8 @@ sub dump_result { } my $json = JSON->new->allow_nonref; - print $json->utf8(1)->pretty( ( $opt{'prettyjson'} ? 1 : 0 ) ) + print $json->utf8(1) + ->pretty( ( $opt{'prettyjson'} ? 1 : 0 ) ) ->encode( \%result ); if ( $opt{'outputfile'} ne 0 ) { @@ -9665,7 +9672,8 @@ sub dump_result { open my $fh, q(>), $opt{'outputfile'} or die "Unable to open $opt{'outputfile'} in write mode. please check permissions for this file or directory"; - print $fh $json->utf8(1)->pretty( ( $opt{'prettyjson'} ? 1 : 0 ) ) + print $fh $json->utf8(1) + ->pretty( ( $opt{'prettyjson'} ? 1 : 0 ) ) ->encode( \%result ); close $fh; } diff --git a/mysqltuner.pl.tdy b/mysqltuner.pl.tdy deleted file mode 100755 index c3614118e..000000000 --- a/mysqltuner.pl.tdy +++ /dev/null @@ -1,10079 +0,0 @@ -#!/usr/bin/env perl -# mysqltuner.pl - Version 2.8.37 -# High Performance MySQL Tuning Script -# Copyright (C) 2015-2026 Jean-Marie Renouard - jmrenouard@gmail.com -# Copyright (C) 2006-2026 Major Hayden - major@mhtx.net - -# For the latest updates, please visit http://mysqltuner.pl/ -# Git repository available at https://github.com/jmrenouard/MySQLTuner-perl/ -# -# This program 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 program 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. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# This project would not be possible without help from: -# Matthew Montgomery Paul Kehrer Dave Burgess -# Jonathan Hinds Mike Jackson Nils Breunese -# Shawn Ashlee Luuk Vosslamber Ville Skytta -# Trent Hornibrook Jason Gill Mark Imbriaco -# Greg Eden Aubin Galinotti Giovanni Bechis -# Bill Bradford Ryan Novosielski Michael Scheidell -# Blair Christensen Hans du Plooy Victor Trac -# Everett Barnes Tom Krouper Gary Barrueto -# Simon Greenaway Adam Stein Isart Montane -# Baptiste M. Cole Turner Major Hayden -# Joe Ashcraft Jean-Marie Renouard Christian Loos -# Julien Francoz Daniel Black Long Radix -# -# Inspired by Matthew Montgomery's tuning-primer.sh script: -# http://www.day32.com/MySQL/ -# -package main; - -use 5.005; -use strict; -use warnings; - -use diagnostics; -use POSIX; -use File::Spec; -use Getopt::Long; -use Pod::Usage; -use Sys::Hostname; -use File::Basename; -use Cwd 'abs_path'; - -# Subroutine declarations -sub show_help; -sub subheaderprint; -sub execute_system_command; - -#use Data::Dumper; -#$Data::Dumper::Pair = " : "; - -# for which() -#use Env; - -our $is_win = $^O eq 'MSWin32'; - -# Set up a few variables for use in the script -our $tunerversion = "2.8.37"; -our ( @adjvars, @generalrec, @modeling, @sysrec, @secrec ); - -# Set defaults -# Central metadata for CLI options -# Categories: CONNECTION, PERFORMANCE, OUTPUT, CLOUD, MISC -our %CLI_METADATA = ( - - # Connection and Authentication - 'host' => { - type => '=s', - default => '0', - desc => 'Connect to a remote host to perform tests', - placeholder => '', - cat => 'CONNECTION' - }, - 'socket' => { - type => '=s', - default => '0', - desc => 'Use a different socket for a local connection', - placeholder => '', - cat => 'CONNECTION' - }, - 'pipe' => { - type => '!', - default => 0, - desc => 'Connect to a local Windows database using named pipes', - cat => 'CONNECTION' - }, - 'pipe_name' => { - type => '=s', - default => '0', - desc => 'Use a different pipe name for a local connection', - placeholder => '', - cat => 'CONNECTION' - }, - 'port' => { - type => '=i', - default => 3306, - desc => 'Port to use for connection', - placeholder => '', - cat => 'CONNECTION', - validate => qr/^\d+$/ - }, - 'user|u' => { - type => '=s', - default => '0', - desc => 'Username to use for authentication', - placeholder => '', - cat => 'CONNECTION' - }, - 'pass|p|password' => { - type => '=s', - default => '0', - desc => 'Password to use for authentication', - placeholder => '', - cat => 'CONNECTION' - }, - 'userenv' => { - type => '=s', - default => '0', - desc => 'Env variable name for username', - placeholder => '', - cat => 'CONNECTION' - }, - 'passenv' => { - type => '=s', - default => '0', - desc => 'Env variable name for password', - placeholder => '', - cat => 'CONNECTION' - }, - 'ssl-ca' => { - type => '=s', - default => '0', - desc => 'Path to public key (SSL CA)', - placeholder => '', - cat => 'CONNECTION' - }, - 'mysqladmin' => { - type => '=s', - default => '0', - desc => 'Path to a custom mysqladmin executable', - placeholder => '', - cat => 'CONNECTION' - }, - 'mysqlcmd' => { - type => '=s', - default => '0', - desc => 'Path to a custom mysql executable', - placeholder => '', - cat => 'CONNECTION' - }, - 'defaults-file' => { - type => '=s', - default => '0', - desc => 'Path to a custom .my.cnf', - placeholder => '', - cat => 'CONNECTION' - }, - 'defaults-extra-file' => { - type => '=s', - default => '0', - desc => 'Path to an extra custom config file', - placeholder => '', - cat => 'CONNECTION' - }, - 'protocol' => { - type => '=s', - default => '0', - desc => 'Force TCP connection instead of socket', - placeholder => 'tcp', - cat => 'CONNECTION' - }, - 'server-log' => { - type => '=s', - default => '0', - desc => 'Path to explicit log file (error_log)', - placeholder => '', - cat => 'CONNECTION' - }, - - # Performance and Reporting - 'skipsize' => { - type => '!', - default => 0, - desc => "Don't enumerate tables and their sizes", - cat => 'PERFORMANCE' - }, - 'checkversion' => { - type => '!', - default => 1, - desc => 'Check for updates to MySQLTuner', - cat => 'PERFORMANCE' - }, - 'updateversion' => { - type => '!', - default => 0, - desc => 'Update MySQLTuner if newer version is available', - cat => 'PERFORMANCE' - }, - 'forcemem' => { - type => '=i', - default => 0, - desc => 'Amount of RAM installed in megabytes', - placeholder => '', - cat => 'PERFORMANCE', - validate => qr/^\d+$/ - }, - 'forceswap' => { - type => '=i', - default => 0, - desc => 'Amount of swap memory configured in megabytes', - placeholder => '', - cat => 'PERFORMANCE', - validate => qr/^\d+$/ - }, - 'buffers' => { - type => '!', - default => 0, - desc => 'Print global and per-thread buffer values', - cat => 'PERFORMANCE' - }, - 'passwordfile' => { - type => '=s', - default => '0', - desc => 'Path to a password file list', - placeholder => '', - cat => 'PERFORMANCE' - }, - 'cvefile' => { - type => '=s', - default => '0', - desc => 'CVE File for vulnerability checks', - placeholder => '', - cat => 'PERFORMANCE' - }, - 'outputfile' => { - type => '=s', - default => '0', - desc => 'Path to a output txt file', - placeholder => '', - cat => 'PERFORMANCE' - }, - 'reportfile' => { - type => '=s', - default => '0', - desc => 'Path to a report txt file', - placeholder => '', - cat => 'PERFORMANCE' - }, - 'template' => { - type => '=s', - default => '0', - desc => 'Path to a template file', - placeholder => '', - cat => 'PERFORMANCE' - }, - 'json' => { - type => '!', - default => 0, - desc => 'Print result as JSON string', - cat => 'PERFORMANCE' - }, - 'prettyjson' => { - type => '!', - default => 0, - desc => 'Print result as JSON formatted string', - cat => 'PERFORMANCE' - }, - 'dumpdir' => { - type => '=s', - default => '0', - desc => 'Path to a directory where to dump information files', - placeholder => '', - cat => 'PERFORMANCE' - }, - 'schemadir' => { - type => '=s', - default => '0', - desc => - 'Path to a directory where to dump one markdown file per schema', - placeholder => '', - cat => 'PERFORMANCE' - }, - 'feature' => { - type => '=s', - default => '0', - desc => 'Run a specific feature', - placeholder => '', - cat => 'PERFORMANCE', - implies => { verbose => 1 } - }, - 'skippassword' => { - type => '!', - default => 0, - desc => "Don't perform checks on user passwords", - cat => 'PERFORMANCE' - }, - - # Output Options - 'silent' => { - type => '!', - default => 0, - desc => "Don't output anything on screen", - cat => 'OUTPUT' - }, - 'verbose|v' => { - type => '!', - default => 0, - desc => 'Print out all options', - cat => 'OUTPUT', - implies => { - dbstat => 1, - tbstat => 1, - idxstat => 1, - sysstat => 1, - buffers => 1, - pfstat => 1, - structstat => 1, - myisamstat => 1, - plugininfo => 1 - } - }, - 'color!' => { - type => '!', - default => ( -t STDOUT ? 1 : 0 ), - desc => 'Print output in color', - cat => 'OUTPUT' - }, - 'nobad' => { - type => '!', - default => 0, - desc => 'Remove negative/suggestion responses', - cat => 'OUTPUT' - }, - 'nogood' => { - type => '!', - default => 0, - desc => 'Remove OK responses', - cat => 'OUTPUT' - }, - 'noinfo' => { - type => '!', - default => 0, - desc => 'Remove informational responses', - cat => 'OUTPUT' - }, - 'debug' => { - type => '!', - default => 0, - desc => 'Print debug information', - cat => 'OUTPUT' - }, - 'dbgpattern' => { - type => '=s', - default => '', - desc => 'Debug pattern (regex)', - placeholder => '', - cat => 'OUTPUT' - }, - 'noprettyicon' => { - type => '!', - default => 0, - desc => 'Print output with legacy tags', - cat => 'OUTPUT' - }, - 'experimental' => { - type => '!', - default => 0, - desc => 'Print experimental analysis', - cat => 'OUTPUT' - }, - 'nondedicated' => { - type => '!', - default => 0, - desc => 'Consider server is not dedicated to DB', - cat => 'OUTPUT' - }, - 'noprocess' => { - type => '!', - default => 0, - desc => 'Consider no other process is running', - cat => 'OUTPUT' - }, - - # Stats / Reporting flags - 'dbstat!' => { - type => '!', - default => 0, - desc => 'Print database information', - cat => 'OUTPUT' - }, - 'tbstat!' => { - type => '!', - default => 0, - desc => 'Print table information', - cat => 'OUTPUT' - }, - 'colstat!' => { - type => '!', - default => 0, - desc => 'Print column information', - cat => 'OUTPUT' - }, - 'idxstat!' => { - type => '!', - default => 0, - desc => 'Print index information', - cat => 'OUTPUT' - }, - 'sysstat!' => { - type => '!', - default => 0, - desc => 'Print system stats', - cat => 'OUTPUT' - }, - 'pfstat!' => { - type => '!', - default => 0, - desc => 'Print Performance schema info', - cat => 'OUTPUT' - }, - 'plugininfo!' => { - type => '!', - default => 0, - desc => 'Print plugin information', - cat => 'OUTPUT' - }, - 'myisamstat!' => { - type => '!', - default => 0, - desc => 'Print MyISAM stats', - cat => 'OUTPUT' - }, - 'structstat!' => { - type => '!', - default => 0, - desc => 'Print table structures', - cat => 'OUTPUT' - }, - - # Cloud and Containers - 'cloud' => { - type => '!', - default => 0, - desc => 'Enable cloud mode', - cat => 'CLOUD' - }, - 'azure' => { - type => '!', - default => 0, - desc => 'Enable Azure-specific support', - cat => 'CLOUD' - }, - 'ssh-host' => { - type => '=s', - default => '0', - desc => 'The SSH host for cloud connections', - placeholder => '', - cat => 'CLOUD' - }, - 'ssh-user' => { - type => '=s', - default => '0', - desc => 'The SSH user for cloud connections', - placeholder => '', - cat => 'CLOUD' - }, - 'ssh-password' => { - type => '=s', - default => '0', - desc => 'The SSH password for cloud connections', - placeholder => '', - cat => 'CLOUD' - }, - 'ssh-identity-file' => { - type => '=s', - default => '0', - desc => 'The path to the SSH identity file', - placeholder => '', - cat => 'CLOUD' - }, - 'container' => { - type => '=s', - default => '0', - desc => 'Enable container mode with ID or name', - placeholder => '', - cat => 'CLOUD' - }, - 'server-log' => { - type => '=s', - default => '0', - desc => 'Path to explicit log file (error_log)', - placeholder => '', - cat => 'PERFORMANCE' - }, - - # Misc - 'max-password-checks' => { - type => '=i', - default => 100, - desc => 'Max password checks from dictionary', - placeholder => '', - cat => 'MISC' - }, - 'ignore-tables' => { - type => '=s', - default => '0', - desc => 'Tables to ignore (comma separated)', - placeholder => '', - cat => 'MISC' - }, - 'bannedports' => { - type => '=s', - default => '0', - desc => 'Ports banned separated by comma', - placeholder => '

', - cat => 'MISC' - }, - 'maxportallowed' => { - type => '=i', - default => 0, - desc => 'Number of open ports allowable', - placeholder => '', - cat => 'MISC' - }, - 'defaultarch' => { - type => '=i', - default => 64, - desc => 'Default architecture (32 or 64)', - placeholder => '<32|64>', - cat => 'MISC', - validate => sub { $_[0] == 32 || $_[0] == 64 } - }, - 'noask' => { - type => '!', - default => 0, - desc => "Don't ask for confirmation", - cat => 'MISC' - }, - 'help|?' => { - type => '', - default => 0, - desc => 'Show this help message', - cat => 'MISC' - }, -); - -# Initialize %opt from metadata -our %opt = map { - my ($primary) = split /\|/, $_; - $primary =~ s/[!+=:].*$//; # Strip modifiers (Getopt::Long compatibility) - $primary => $CLI_METADATA{$_}->{default} -} keys %CLI_METADATA; - -# Declare shared variables at top level -our ( - $devnull, $basic_password_files, $outputfile, - $fh, $me, $good, - $bad, $info, $deb, - $cmd, $end, $maxlines, - $mysqlvermajor, $mysqlverminor, $mysqlvermicro, - @banned_ports, @dblist, %result -); - -# Gather the options from the command line -sub parse_cli_args { - - # Build GetOptions arguments dynamically - my @getopt_args; - Getopt::Long::Configure( "no_auto_abbrev", "no_ignore_case" ); - foreach my $opt_spec ( sort keys %CLI_METADATA ) { - my $type = $CLI_METADATA{$opt_spec}->{type} // ''; - my ($primary) = split /\|/, $opt_spec; - $primary =~ s/[!+=:].*$//; # Strip modifiers - my $final_spec = $opt_spec; - - # Only append type if it's not already part of the specification key - if ( $type && $opt_spec !~ /\Q$type\E$/ ) { - $final_spec .= $type; - } - push @getopt_args, $final_spec => \$opt{$primary}; - } - - GetOptions(@getopt_args) - or pod2usage( - -exitval => 1, - -verbose => 99, - -sections => - [ "NAME", "IMPORTANT USAGE GUIDELINES", "OPTIONS", "VERSION" ] - ); - - # Apply metadata-driven rules (Implications and Validation) - foreach my $opt_spec ( keys %CLI_METADATA ) { - my ($primary) = split /\|/, $opt_spec; - my $meta = $CLI_METADATA{$opt_spec}; - - # Implications (e.g., --feature implies --verbose) - if ( ( $opt{$primary} // '0' ) ne '0' && $meta->{implies} ) { - while ( my ( $target, $value ) = each %{ $meta->{implies} } ) { - $opt{$target} = $value; - } - } - - # Validation (regex or coderef) - if ( defined $opt{$primary} - && ( $opt{$primary} // '0' ) ne '0' - && $meta->{validate} ) - { - my $val = $opt{$primary}; - my $is_valid = 1; - if ( ref $meta->{validate} eq 'Regexp' ) { - $is_valid = ( $val =~ $meta->{validate} ); - } - elsif ( ref $meta->{validate} eq 'CODE' ) { - $is_valid = $meta->{validate}->($val); - } - - unless ($is_valid) { - print "Error: Invalid value for --$primary: $val\n"; - exit 1; - } - } - } -} - -sub setup_environment { - if ( defined $opt{'help'} && $opt{'help'} == 1 ) { - show_help(); - } - - if ( defined $opt{'version'} && $opt{'version'} == 1 ) { - subheaderprint("MySQLTuner $tunerversion"); - exit(0); - } - - $devnull = File::Spec->devnull(); - $basic_password_files = - ( $opt{passwordfile} eq "0" ) - ? abs_path( dirname(__FILE__) ) . "/basic_passwords.txt" - : abs_path( $opt{passwordfile} ); - - # Username from envvar - if ( exists $opt{userenv} && exists $ENV{ $opt{userenv} // '' } ) { - $opt{user} = $ENV{ $opt{userenv} // '' }; - } - - # Related to password option - if ( exists $opt{passenv} && exists $ENV{ $opt{userenv} // '' } ) { - $opt{pass} = $ENV{ $opt{userenv} // '' }; - } - $opt{pass} = $opt{password} - if ( ( $opt{pass} // '0' ) eq '0' and ( $opt{password} // '0' ) ne '0' ); - - # for RPM distributions - $basic_password_files = "/usr/share/mysqltuner/basic_passwords.txt" - unless -f "$basic_password_files"; - - $opt{dbgpattern} = '.*' if ( ( $opt{dbgpattern} // '' ) eq '' ); - - # check if we need to enable verbose mode - $opt{noprettyicon} = 0 if ( $opt{noprettyicon} // 0 ) != 1; - $opt{nocolor} = 1 if defined( $opt{outputfile} ); - $opt{noprocess} = 0 - if ( ( $opt{noprocess} // 0 ) == 1 ); # Don't print process information - $opt{structstat} = 0 - if ( ( $opt{nostructstat} // 0 ) == 1 ) - ; # Don't print table struct information - $opt{myisamstat} = 1 - if ( not defined( $opt{myisamstat} ) ); - $opt{myisamstat} = 0 - if ( ( $opt{nomyisamstat} // 0 ) == 1 ) - ; # Don't print MyISAM table information - - # Handle cvefile if it was not passed but exists locally - $opt{cvefile} = './vulnerabilities.csv' - if ( ( !defined( $opt{cvefile} ) || $opt{cvefile} eq '' ) - && -f './vulnerabilities.csv' ); - - $opt{'bannedports'} = '' unless defined( $opt{'bannedports'} ); - @banned_ports = split ',', $opt{'bannedports'}; - - $outputfile = undef; - $outputfile = abs_path( $opt{outputfile} ) unless $opt{outputfile} eq "0"; - - $fh = undef; - open( $fh, '>', $outputfile ) - or die("Fail opening $outputfile") - if defined($outputfile); - $opt{nocolor} = 1 if defined($outputfile); - $opt{nocolor} = 1 unless ( -t STDOUT ); - - $opt{nocolor} = 0 if ( ( $opt{color} // 0 ) == 1 ); - - # Setting up the colors for the print styles - $me = ( getpwuid($<) )[0] // $ENV{USER} // $ENV{USERNAME} // 'unknown'; - - if ($is_win) { $opt{nocolor} = 1; } - $good = ( $opt{nocolor} == 0 ) ? "[\e[0;32mOK\e[0m]" : "[OK]"; - $bad = ( $opt{nocolor} == 0 ) ? "[\e[0;31m!!\e[0m]" : "[!!]"; - $info = ( $opt{nocolor} == 0 ) ? "[\e[0;34m--\e[0m]" : "[--]"; - $deb = ( $opt{nocolor} == 0 ) ? "[\e[0;31mDG\e[0m]" : "[DG]"; - $cmd = ( $opt{nocolor} == 0 ) ? "\e[1;32m[CMD]($me)" : "[CMD]($me)"; - $end = ( $opt{nocolor} == 0 ) ? "\e[0m" : ""; - - if ( ( not $is_win ) and ( $opt{noprettyicon} == 0 ) ) { - $good = ( $opt{nocolor} == 0 ) ? "\e[0;32m✔\e[0m " : "✔ "; - $bad = ( $opt{nocolor} == 0 ) ? "\e[0;31m✘\e[0m " : "✘ "; - $info = ( $opt{nocolor} == 0 ) ? "\e[0;34mℹ\e[0m " : "ℹ "; - $deb = ( $opt{nocolor} == 0 ) ? "\e[0;31m⚙\e[0m " : "⚙ "; - $cmd = ( $opt{nocolor} == 0 ) ? "\e[1;32m⌨️($me)" : "⌨️($me)"; - $end = ( $opt{nocolor} == 0 ) ? "\e[0m " : " "; - } - - # Maximum lines of log output to read from end - $maxlines = 30000; - - # Super structure containing all information - %result = (); - $result{'MySQLTuner'}{'version'} = $tunerversion; - $result{'MySQLTuner'}{'datetime'} = scalar localtime; - $result{'MySQLTuner'}{'options'} = \%opt; -} - -# Functions that handle the print styles -sub show_help { - my %categories = ( - 'CONNECTION' => 'CONNECTION AND AUTHENTICATION', - 'CLOUD' => 'CLOUD SUPPORT', - 'PERFORMANCE' => 'PERFORMANCE AND REPORTING OPTIONS', - 'OUTPUT' => 'OUTPUT OPTIONS', - 'MISC' => 'MISCELLANEOUS OPTIONS', - ); - - print "MySQLTuner $tunerversion - MySQL High Performance Tuning Script\n\n"; - print "Usage: ./mysqltuner.pl [options]\n\n"; - - foreach my $cat (qw(CONNECTION CLOUD PERFORMANCE OUTPUT MISC)) { - print "$categories{$cat}:\n"; - foreach my $opt_spec ( sort keys %CLI_METADATA ) { - next unless $CLI_METADATA{$opt_spec}->{cat} eq $cat; - my $meta = $CLI_METADATA{$opt_spec}; - my ($primary) = split /\|/, $opt_spec; - my $display = "--$primary"; - - # Handle negatable options (trailing !) - my $is_negatable = - ( $opt_spec =~ /!$/ || ( $meta->{type} // '' ) eq '!' ); - if ($is_negatable) { - - # Remove ! from display if present - $display =~ s/!$//; - } - - $display .= " " . $meta->{placeholder} if $meta->{placeholder}; - - # Handle aliases - my @parts = split /\|/, $opt_spec; - shift @parts; # remove primary - - my @display_parts; - if ($is_negatable) { - push @display_parts, "no-$primary"; - } - push @display_parts, @parts; - - if (@display_parts) { - - # Format aliases: length 1 -> -x, length > 1 -> --xx - $display .= " (" . join( - ", ", - map { - my $p = $_; - $p =~ s/!$//; - length($p) == 1 ? "-$p" : "--$p" - } @display_parts - ) . ")"; - } - - printf " %-32s %s", $display, $meta->{desc}; - - # Special case for defaults: Don't print if 0 or empty string unless meaningful - # For booleans (type !), default 0 is expected and silent. - if ( defined $meta->{default} - && $meta->{default} ne '0' - && $meta->{default} ne '' - && $meta->{default} ne "0" ) - { -# For string/numeric defaults, use ne and != appropriately or just string compare since it's for display - print " (default: $meta->{default})"; - } - print "\n"; - } - print "\n"; - } - exit 0; -} - -sub prettyprint { - print $_[0] . "\n" unless ( $opt{'silent'} or $opt{'json'} ); - print $fh $_[0] . "\n" if defined($fh); -} - -sub goodprint { - prettyprint $good. " " . $_[0] unless ( $opt{nogood} == 1 ); -} - -sub infoprint { - prettyprint $info. " " . $_[0] unless ( $opt{noinfo} == 1 ); -} - -sub badprint { - prettyprint $bad. " " . $_[0] unless ( $opt{nobad} == 1 ); -} - -sub debugprint { - prettyprint $deb. " " . $_[0] unless ( $opt{debug} == 0 ); -} - -sub redwrap { - return ( $opt{nocolor} == 0 ) ? "\e[0;31m" . $_[0] . "\e[0m" : $_[0]; -} - -sub greenwrap { - return ( $opt{nocolor} == 0 ) ? "\e[0;32m" . $_[0] . "\e[0m" : $_[0]; -} - -sub cmdprint { - prettyprint $cmd. " " . $_[0] . $end; -} - -sub push_recommendation { - my ( $cat, $msg ) = @_; - push @generalrec, $msg; - push @sysrec, $msg if $cat =~ /sys/i; - push @secrec, $msg if $cat =~ /sec/i; - push @modeling, $msg if $cat =~ /mod/i; -} - -sub infoprintml { - for my $ln (@_) { $ln =~ s/\n//g; infoprint "\t$ln"; } -} - -sub infoprintcmd { - cmdprint "@_"; - infoprintml grep { $_ ne '' and $_ !~ /^\s*$/ } `@_ 2>&1`; -} - -sub subheaderprint { - my $tln = 100; - my $sln = 8; - my $ln = length("@_") + 2; - - prettyprint " "; - prettyprint "-" x $sln . " @_ " . "-" x ( $tln - $ln - $sln ); -} - -sub infoprinthcmd { - subheaderprint "$_[0]"; - infoprintcmd "$_[1]"; -} - -sub is_remote() { - my $host = $opt{'host'}; - return 1 if ( $opt{'cloud'} && ( $opt{'ssh-host'} // '0' ) ne '0' ); - return 0 if ( ( $host // '0' ) eq '0' ); - return 0 if ( $host eq 'localhost' ); - return 0 if ( $host eq '127.0.0.1' ); - return 1; -} - -sub is_docker() { - return 1 if -f '/.dockerenv'; - if ( -f '/proc/self/cgroup' ) { - if ( open( my $fh, '<', '/proc/self/cgroup' ) ) { - while ( my $line = <$fh> ) { - if ( $line =~ /docker|kubepods|containerd|podman/ ) { - close $fh; - return 1; - } - } - close $fh; - } - } - return 1 - if ( - ( - defined $ENV{'container'} - && $ENV{'container'} =~ /^(docker|podman|lxc)$/ - ) - || ( defined $opt{'container'} && ( $opt{'container'} // '0' ) ne '0' ) - ); - return 0; -} - -sub is_int { - return 0 unless defined $_[0]; - my $str = $_[0]; - - #trim whitespace both sides - $str =~ s/^\s+|\s+$//g; - - #Alternatively, to match any float-like numeric, use: - # m/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ - - #flatten to string and match dash or plus and one or more digits - if ( $str =~ /^(\-|\+)?\d+?$/ ) { - return 1; - } - return 0; -} - -# Calculates the number of physical cores -sub cpu_cores { - if ( $^O eq 'linux' ) { - if ( get_transport_prefix() eq '' ) { - my %cpus; - my %cores; - if ( open( my $proc_cpuinfo, '<', '/proc/cpuinfo' ) ) { - while (<$proc_cpuinfo>) { - if ( /^physical id\s*:\s*(.*)/ ) { $cpus{$1} = 1; } - if ( /^core id\s*:\s*(.*)/ ) { $cores{$1} = 1; } - } - close $proc_cpuinfo; - my $cntCPU = ( scalar keys %cpus ) * ( scalar keys %cores ); - return $cntCPU if $cntCPU > 0; - } - } - my $cntCPU = - execute_system_command( -"awk -F: '/^core id/ && !P[\$2] { CORES++; P[\$2]=1 }; /^physical id/ && !N[\$2] { CPUs++; N[\$2]=1 }; END { print CPUs*CORES }' /proc/cpuinfo" - ); - chomp $cntCPU; - return ( $cntCPU == 0 ? execute_system_command("nproc") : $cntCPU ) + 0; - } - - if ( $^O eq 'freebsd' ) { - my $cntCPU = execute_system_command("sysctl -n kern.smp.cores"); - chomp $cntCPU; - return $cntCPU + 0; - } - if ($is_win) { - my $cntCPU = - execute_system_command( - 'wmic cpu get NumberOfCores| perl -ne "s/[^0-9]//g; print if /[0-9]+/;"' - ); - chomp $cntCPU; - return $cntCPU + 0; - } - return 0; -} - -# Calculates the number of logical cores (including HT) -sub logical_cpu_cores { - if ( $^O eq 'linux' ) { - if ( get_transport_prefix() eq '' ) { - my $cntCPU = 0; - if ( open( my $proc_cpuinfo, '<', '/proc/cpuinfo' ) ) { - while (<$proc_cpuinfo>) { - $cntCPU++ if /^processor\s*:\s*\d+/; - } - close $proc_cpuinfo; - return $cntCPU if $cntCPU > 0; - } - } - my $cntCPU = execute_system_command("grep -c ^processor /proc/cpuinfo"); - chomp $cntCPU; - if ( $cntCPU == 0 ) { - $cntCPU = execute_system_command("nproc"); - chomp $cntCPU; - } - return $cntCPU + 0; - } - - if ( $^O eq 'freebsd' ) { - my $cntCPU = execute_system_command("sysctl -n kern.smp.cpus"); - chomp $cntCPU; - return $cntCPU + 0; - } - if ($is_win) { - my $cntCPU = - execute_system_command( - 'wmic cpu get NumberOfLogicalProcessors| perl -ne "s/[^0-9]//g; print if /[0-9]+/;"' - ); - chomp $cntCPU; - return $cntCPU + 0; - } - return cpu_cores(); -} - -# Calculates the parameter passed in bytes, then rounds it to one decimal place -sub hr_bytes { - my $num = shift; - return "0B" unless defined($num); - return "0B" if $num eq "NULL"; - return "0B" if $num eq ""; - - 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 . "B"; - } -} - -# Calculates the parameter passed in bytes, then rounds it to a practical power-of-2 value in GB. -sub hr_bytes_practical_rnd { - my $num = shift; - return "0B" unless defined($num) and $num > 0; - - my $gbs = $num / ( 1024**3 ); # convert to GB - my $power_of_2_gb = 1; - while ( $power_of_2_gb < $gbs ) { - $power_of_2_gb *= 2; - } - - return $power_of_2_gb . "G"; -} - -sub hr_raw { - my $num = shift; - return "0" unless defined($num); - return "0" if $num eq "NULL"; - if ( $num =~ /^(\d+)G$/ ) { - return $1 * 1024 * 1024 * 1024; - } - if ( $num =~ /^(\d+)M$/ ) { - return $1 * 1024 * 1024; - } - if ( $num =~ /^(\d+)K$/ ) { - return $1 * 1024; - } - if ( $num =~ /^(\d+)$/ ) { - return $1; - } - return $num; -} - -# Calculates the parameter passed in bytes, then rounds it to the nearest integer -sub hr_bytes_rnd { - my $num = shift; - return "0B" unless defined($num); - return "0B" if $num eq "NULL"; - - 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 . "B"; - } -} - -# Calculates the parameter passed to the nearest power of 1000, then rounds it to the nearest integer -sub hr_num { - my $num = shift; - if ( $num >= ( 1000**3 ) ) { # Billions - return int( ( $num / ( 1000**3 ) ) ) . "B"; - } - elsif ( $num >= ( 1000**2 ) ) { # Millions - return int( ( $num / ( 1000**2 ) ) ) . "M"; - } - elsif ( $num >= 1000 ) { # Thousands - return int( ( $num / 1000 ) ) . "K"; - } - else { - return $num; - } -} - -# Calculate Percentage -sub percentage { - my $value = shift; - my $total = shift; - $total = 0 unless defined $total; - $total = 0 if $total eq "NULL"; - return "100.00" if $total == 0; - return sprintf( "%.2f", ( $value * 100 / $total ) ); -} - -# Calculates uptime to display in a human-readable form -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; -} - -# Retrieves the memory installed on this machine -my ( $physical_memory, $swap_memory, $duflags, $xargsflags ); - -sub memerror { - badprint -"Unable to determine total memory/swap; use '--forcemem' and '--forceswap'"; - exit 1; -} - -sub os_setup { - my $prefix = get_transport_prefix(); - my $os; - if ($is_win) { - $os = 'windows'; - } - elsif ( $prefix eq '' ) { - $os = ( POSIX::uname() )[0]; - } - else { - $os = execute_system_command('uname'); - } - - $duflags = ( $os =~ /Linux/ ) ? '-b' : ''; - $xargsflags = ( $os =~ /Darwin|SunOS/ ) ? '' : '-r'; - if ( $opt{'forcemem'} > 0 ) { - $physical_memory = $opt{'forcemem'} * 1048576; - infoprint "Assuming $opt{'forcemem'} MB of physical memory"; - if ( $opt{'forceswap'} > 0 ) { - $swap_memory = $opt{'forceswap'} * 1048576; - infoprint "Assuming $opt{'forceswap'} MB of swap space"; - } - else { - $swap_memory = 0; - badprint "Assuming 0 MB of swap space (use --forceswap to specify)"; - } - } - else { - if ( $os =~ /Linux|CYGWIN/ ) { - if ( $prefix eq '' && open( my $meminfo, '<', '/proc/meminfo' ) ) { - while (<$meminfo>) { - if (/^MemTotal:\s+(\d+)/i) { $physical_memory = $1 * 1024; } - if (/^SwapTotal:\s+(\d+)/i) { $swap_memory = $1 * 1024; } - } - close $meminfo; - } - - if ( !defined $physical_memory || $physical_memory == 0 ) { - $physical_memory = - execute_system_command( - "grep -i memtotal: /proc/meminfo | awk '{print \$2}'") - or memerror; - $physical_memory *= 1024; - } - - if ( !defined $swap_memory ) { - $swap_memory = - execute_system_command( - "grep -i swaptotal: /proc/meminfo | awk '{print \$2}'") - or memerror; - $swap_memory *= 1024; - } - } - elsif ( $os =~ /Darwin/ ) { - $physical_memory = execute_system_command('sysctl -n hw.memsize') - or memerror; - $swap_memory = - execute_system_command( - "sysctl -n vm.swapusage | awk '{print \$3}' | sed 's/\..*\$//'") - or memerror; - } - elsif ( $os =~ /NetBSD|OpenBSD|FreeBSD/ ) { - $physical_memory = execute_system_command('sysctl -n hw.physmem') - or memerror; - if ( $physical_memory < 0 ) { - $physical_memory = - execute_system_command('sysctl -n hw.physmem64') - or memerror; - } - $swap_memory = - execute_system_command( - "swapctl -l | grep '^/' | awk '{ s+= \$2 } END { print s }'") - or memerror; - } - elsif ( $os =~ /BSD/ ) { - $physical_memory = execute_system_command('sysctl -n hw.realmem') - or memerror; - $swap_memory = - execute_system_command( - "swapinfo | grep '^/' | awk '{ s+= \$2 } END { print s }'"); - } - elsif ( $os =~ /SunOS/ ) { - $physical_memory = - execute_system_command( - "/usr/sbin/prtconf | grep Memory | cut -f 3 -d ' '") - or memerror; - chomp($physical_memory); - $physical_memory = $physical_memory * 1024 * 1024; - } - elsif ( $os =~ /AIX/ ) { - $physical_memory = - execute_system_command( - "lsattr -El sys0 | grep realmem | awk '{print \$2}'") - or memerror; - chomp($physical_memory); - $physical_memory = $physical_memory * 1024; - $swap_memory = execute_system_command( - "lsps -as | awk -F'(MB| +)' '/MB /{print \$2}'") - or memerror; - chomp($swap_memory); - $swap_memory = $swap_memory * 1024 * 1024; - } - elsif ( $os =~ /windows/i ) { - $physical_memory = - execute_system_command( -'wmic ComputerSystem get TotalPhysicalMemory | perl -ne "s/[^0-9]//g; print if /[0-9]+/;' - ) or memerror; - $swap_memory = - execute_system_command( -'wmic OS get FreeVirtualMemory | perl -ne "s/[^0-9]//g; print if /[0-9]+/;' - ) or memerror; - } - } - debugprint "Physical Memory: $physical_memory"; - debugprint "Swap Memory: $swap_memory"; - chomp($physical_memory); - chomp($swap_memory); - chomp($os); - $result{'OS'}{'OS Type'} = $os; - $result{'OS'}{'Physical Memory'}{'bytes'} = $physical_memory; - $result{'OS'}{'Physical Memory'}{'pretty'} = hr_bytes($physical_memory); - $result{'OS'}{'Swap Memory'}{'bytes'} = $swap_memory; - $result{'OS'}{'Swap Memory'}{'pretty'} = hr_bytes($swap_memory); - $result{'OS'}{'Other Processes'}{'bytes'} = get_other_process_memory(); - $result{'OS'}{'Other Processes'}{'pretty'} = - hr_bytes( get_other_process_memory() ); -} - -sub get_http_cli { - my $httpcli = which( "curl", $ENV{'PATH'} ); - chomp($httpcli); - if ($httpcli) { - return $httpcli; - } - - $httpcli = which( "wget", $ENV{'PATH'} ); - chomp($httpcli); - if ($httpcli) { - return $httpcli; - } - return ""; -} - -# Checks for updates to MySQLTuner -sub validate_tuner_version { - if ( $opt{'checkversion'} eq 0 ) { - print "\n" unless ( $opt{'silent'} or $opt{'json'} ); - infoprint "Skipped version check for MySQLTuner script"; - return; - } - - my $url = -'https://raw.githubusercontent.com/jmrenouard/MySQLTuner-perl/master/mysqltuner.pl'; - my $content; - - # Try HTTP::Tiny if available (Core since 5.13.9) - if ( eval { require HTTP::Tiny; 1 } ) { - debugprint "Using HTTP::Tiny for version check"; - my $http = HTTP::Tiny->new( timeout => 3 ); - my $response = $http->get($url); - if ( $response->{success} ) { - $content = $response->{content}; - } - else { - debugprint - "HTTP::Tiny failed: $response->{status} $response->{reason}"; - } - } - - # Fallback to curl/wget if content is still empty or HTTP::Tiny not available - if ( !$content ) { - debugprint "Falling back to curl/wget for version check"; - my $httpcli = get_http_cli(); - if ($httpcli) { - my $cmd_line = - ( $httpcli =~ /curl$/ ) - ? "$httpcli -sL $url" - : "$httpcli -q -O - $url"; - $content = execute_system_command($cmd_line); - } - } - - if ($content) { - -# Robust regex for version extraction (handles my/our/local, spacing, and quotes) - if ( $content =~ - /^\s*(?:our|my|local)\s+\$tunerversion\s*=\s*["']([\d.]+)["']\s*;/m - ) - { - my $update = $1; - infoprint "VERSION: $update"; - compare_tuner_version($update); - } - else { - badprint - "Cannot determine latest tuner version from fetched content"; - } - } - else { - badprint "Failed to fetch tuner version information from $url"; - } - return; -} - -# Checks for updates to MySQLTuner -sub update_tuner_version { - if ( $opt{'updateversion'} eq 0 ) { - badprint "Skipped version update for MySQLTuner script"; - print "\n" unless ( $opt{'silent'} or $opt{'json'} ); - return; - } - - my $update; - my $fullpath = ""; - my $url = - "https://raw.githubusercontent.com/jmrenouard/MySQLTuner-perl/master/"; - my @scripts = - ( "mysqltuner.pl", "basic_passwords.txt", "vulnerabilities.csv" ); - my $totalScripts = scalar(@scripts); - my $receivedScripts = 0; - my $httpcli = get_http_cli(); - - foreach my $script (@scripts) { - - if ( $httpcli =~ /curl$/ ) { - debugprint "$httpcli is available."; - - $fullpath = dirname(__FILE__) . "/" . $script; - debugprint "FullPath: $fullpath"; - debugprint -"$httpcli -s --connect-timeout 3 '$url$script' 2>$devnull > $fullpath"; - $update = - execute_system_command( -"$httpcli -s --connect-timeout 3 '$url$script' 2>$devnull > $fullpath" - ); - chomp($update); - debugprint "$script updated: $update"; - - if ( -s $script eq 0 ) { - badprint "Couldn't update $script"; - } - else { - ++$receivedScripts; - debugprint "$script updated: $update"; - } - } - elsif ( $httpcli =~ /wget$/ ) { - - debugprint "$httpcli is available."; - - debugprint -"$httpcli -qe timestamping=off -t 1 -T 3 -O $script '$url$script'"; - $update = - execute_system_command( -"$httpcli -qe timestamping=off -t 1 -T 3 -O $script '$url$script'" - ); - chomp($update); - - if ( -s $script eq 0 ) { - badprint "Couldn't update $script"; - } - else { - ++$receivedScripts; - debugprint "$script updated: $update"; - } - } - else { - debugprint "curl and wget are not available."; - infoprint "Unable to check for the latest MySQLTuner version"; - } - - } - - if ( $receivedScripts eq $totalScripts ) { - goodprint "Successfully updated MySQLTuner script"; - } - else { - badprint "Couldn't update MySQLTuner script"; - } - infoprint "Stopping program: MySQLTuner script must be updated first."; - exit 0; -} - -sub compare_tuner_version { - my $remoteversion = shift; - debugprint "Remote data: $remoteversion"; - - #exit 0; - if ( $remoteversion ne $tunerversion ) { - badprint - "There is a new version of MySQLTuner available ($remoteversion)"; - update_tuner_version(); - return; - } - goodprint "You have the latest version of MySQLTuner ($tunerversion)"; - return; -} - -# Checks to see if a MySQL login is possible -our ( $mysqllogin, $doremote, $remotestring, $mysqlcmd, $mysqladmincmd ); - -sub cloud_setup { - if ( $opt{'cloud'} || $opt{'azure'} ) { - $opt{'cloud'} = 1; # Ensure cloud is enabled if azure is - infoprint "Cloud mode activated."; - if ( $opt{'azure'} ) { - infoprint - "Azure-specific checks enabled (currently generic cloud checks)."; - } - if ( $opt{'ssh-host'} ) { - infoprint "Cloud SSH mode."; - my @os_info = execute_system_command('uname -a'); - infoprint "Remote OS Info:"; - infoprintml @os_info; - my @mem_info = - execute_system_command('grep MemTotal /proc/meminfo'); - if ( scalar @mem_info > 0 && $mem_info[0] =~ /(\d+)/ ) { - my $remote_mem_bytes = $1 * 1024; - $opt{'forcemem'} = $remote_mem_bytes / 1048576; - infoprint "Remote memory detected: " - . hr_bytes($remote_mem_bytes); - } - else { - badprint -"Could not determine remote memory. Using --forcemem if provided, or default."; - if ( $opt{'forcemem'} == 0 ) { - $opt{'forcemem'} = 1024; # Default to 1GB - } - } - my @swap_info = - execute_system_command('grep SwapTotal /proc/meminfo'); - if ( scalar @swap_info > 0 && $swap_info[0] =~ /(\d+)/ ) { - my $remote_swap_bytes = $1 * 1024; - $opt{'forceswap'} = $remote_swap_bytes / 1048576; - infoprint "Remote swap detected: " - . hr_bytes($remote_swap_bytes); - } - else { - infoprint "Could not determine remote swap. Assuming 0."; - if ( $opt{'forceswap'} == 0 ) { - $opt{'forceswap'} = 0; - } - } - } - else { - infoprint "Direct DB Connection mode."; - $opt{'nosysstat'} = 1; - if ( $opt{'forcemem'} == 0 ) { - badprint - "Direct cloud connection requires --forcemem. Assuming 1GB."; - $opt{'forcemem'} = 1024; - } - } - } -} - -sub get_ssh_prefix { - return "" if not( $opt{'cloud'} and $opt{'ssh-host'} ); - - my $ssh_base_cmd = 'ssh'; - if ( $opt{'ssh-identity-file'} ) { - $ssh_base_cmd .= " -i '" . $opt{'ssh-identity-file'} . "'"; - } - $ssh_base_cmd .= - " -o 'StrictHostKeyChecking=no' -o 'UserKnownHostsFile=/dev/null'"; - my $ssh_target = ''; - if ( $opt{'ssh-user'} ) { - $ssh_target = $opt{'ssh-user'} . '@'; - } - $ssh_target .= $opt{'ssh-host'}; - - my $prefix; - if ( $opt{'ssh-password'} ) { - my $sshpass_path = which( "sshpass", $ENV{'PATH'} ); - if ($sshpass_path) { - $prefix = - "sshpass -p '" - . $opt{'ssh-password'} . "' " - . $ssh_base_cmd . " " - . $ssh_target; - } - else { - badprint -"sshpass is not installed. Password authentication for SSH will not work."; - $prefix = $ssh_base_cmd . " " . $ssh_target; - } - } - else { - $prefix = $ssh_base_cmd . " " . $ssh_target; - } - return $prefix . " "; -} - -sub get_container_prefix { - return "" if ( $opt{'container'} // '0' ) eq '0' or $opt{'container'} eq ''; - my ( $engine, $name ) = - $opt{'container'} =~ /^(docker|podman|kubectl):(.*)/ - ? ( $1, $2 ) - : ( "docker", $opt{'container'} ); - if ( $engine eq "docker" || $engine eq "podman" ) { - return "$engine exec $name sh -c "; - } - elsif ( $engine eq "kubectl" ) { - return "kubectl exec $name -- sh -c "; - } - return ""; -} - -sub get_transport_prefix { - my $prefix = get_ssh_prefix(); - return $prefix if $prefix ne ''; - return get_container_prefix(); -} - -sub execute_system_command { - my ($command) = @_; - my $ssh_prefix = get_ssh_prefix(); - my $container_prefix = get_container_prefix(); - - # Avoid double transport if the command is already prefixed - my $full_cmd = $command; - if ( $ssh_prefix ne '' && index( $command, $ssh_prefix ) != 0 ) { - $full_cmd = "$ssh_prefix '$command'"; - } - elsif ( $container_prefix ne '' - && index( $command, $container_prefix ) != 0 ) - { - $command =~ s/'/'\\''/g; - $full_cmd = "$container_prefix '$command'"; - } - - debugprint "Executing system command: $full_cmd"; - my @output = `$full_cmd 2>&1`; - - if ( $? != 0 ) { - - # Be less verbose for commands that are expected to fail on some systems - if ( $command !~ -/(?:^|\/)(dmesg|lspci|dmidecode|ipconfig|isainfo|bootinfo|ver|wmic|lsattr|prtconf|swapctl|swapinfo|svcprop|ps|ping|ifconfig|ip|hostname|who|free|top|uptime|netstat|sysctl|mysql|mariadb|curl|wget)/ - ) - { - badprint "System command failed: $command"; - infoprintml @output; - } - } - - # Return based on calling context - return wantarray ? @output : join( "", @output ); -} - -if ($is_win) { - eval { require Win32; } or last; - my $osname = Win32::GetOSName(); - infoprint "* Windows OS ($osname) is not fully tested.\n"; - - #exit 1; -} - -sub mysql_setup { - $doremote = 0; - $remotestring = ''; - my $transport_prefix = get_transport_prefix(); - - if ( $opt{mysqladmin} ) { - $mysqladmincmd = $opt{mysqladmin}; - } - else { - if ( $transport_prefix ne '' ) { - my $check = execute_system_command("which mariadb-admin"); - $mysqladmincmd = - ( $check =~ /mariadb-admin/ ) ? "mariadb-admin" : "mysqladmin"; - } - else { - $mysqladmincmd = - ( which( "mariadb-admin", $ENV{'PATH'} ) - || which( "mysqladmin", $ENV{'PATH'} ) ); - } - } - chomp($mysqladmincmd); - if ( !$mysqladmincmd - || ( $transport_prefix eq '' && !-x $mysqladmincmd ) ) - { - badprint - "Couldn't find an executable mysqladmin/mariadb-admin command."; - exit 1; - } - - if ( $opt{mysqlcmd} ) { - $mysqlcmd = $opt{mysqlcmd}; - } - else { - if ( $transport_prefix ne '' ) { - my $check = execute_system_command("which mariadb"); - $mysqlcmd = ( $check =~ /mariadb/ ) ? "mariadb" : "mysql"; - } - else { - $mysqlcmd = - ( which( "mariadb", $ENV{'PATH'} ) - || which( "mysql", $ENV{'PATH'} ) ); - } - } - chomp($mysqlcmd); - if ( !$mysqlcmd || ( $transport_prefix eq '' && !-x $mysqlcmd ) ) { - badprint "Couldn't find an executable mysql/mariadb command."; - exit 1; - } - - # MySQL Client defaults - $mysqlcmd =~ s/\n$//g; - my $mysqlclidefaults = execute_system_command("$mysqlcmd --print-defaults"); - debugprint "MySQL Client: $mysqlclidefaults"; - if ( $mysqlclidefaults =~ /auto-vertical-output/ ) { - badprint - "Avoid auto-vertical-output in configuration file(s) for MySQL like"; - exit 1; - } - - debugprint "MySQL Client: $mysqlcmd"; - - # Are we being asked to connect via a socket? - if ( $opt{socket} ne 0 ) { - if ( $opt{port} ne 0 ) { - $remotestring = " -S $opt{socket} -P $opt{port}"; - } - else { - $remotestring = " -S $opt{socket}"; - } - } - - # Are we being asked to connect via a named pipe? - if ( $opt{pipe} ne 0 ) { - if ( $opt{pipe_name} ne 0 ) { - $remotestring = " -W -S $opt{pipe_name}"; - } - else { - $remotestring = " -W"; - } - } - - if ( $opt{protocol} ) { - $remotestring = " --protocol=$opt{protocol}"; - } - - # Are we being asked to connect to a remote server? - if ( $opt{host} ne 0 ) { - chomp( $opt{host} ); - $opt{port} = ( $opt{port} eq 0 ) ? 3306 : $opt{port}; - -# If we're doing a remote connection, but forcemem wasn't specified, we need to exit - if ( $opt{'forcemem'} eq 0 && is_remote eq 1 ) { - badprint "The --forcemem option is required for remote connections"; - badprint - "Assuming RAM memory is 1Gb for simplify remote connection usage"; - $opt{'forcemem'} = 1024; - - #exit 1; - } - if ( $opt{'forceswap'} eq 0 && is_remote eq 1 ) { - badprint - "The --forceswap option is required for remote connections"; - badprint - "Assuming Swap size is 1Gb for simplify remote connection usage"; - $opt{'forceswap'} = 1024; - - #exit 1; - } - infoprint "Performing tests on $opt{host}:$opt{port}"; - $remotestring = " -h $opt{host} -P $opt{port}"; - $doremote = is_remote(); - - } - else { - $opt{host} = '127.0.0.1'; - } - - if ( $opt{'ssl-ca'} ne 0 ) { - if ( -e -r -f $opt{'ssl-ca'} ) { - $remotestring .= " --ssl-ca=$opt{'ssl-ca'}"; - infoprint - "Will connect using ssl public key passed on the command line"; - } - else { - badprint -"Attempted to use passed ssl public key, but it was not found or could not be read"; - exit 1; - } - } - - if ( $transport_prefix ne '' && ( $opt{pass} // '0' ) eq '0' ) { - if ( ( $ENV{MARIADB_ROOT_PASSWORD} // '' ) ne '' - || ( $ENV{MYSQL_ROOT_PASSWORD} // '' ) ne '' ) - { - $opt{pass} = $ENV{MARIADB_ROOT_PASSWORD} || $ENV{MYSQL_ROOT_PASSWORD}; - debugprint "Detected password from container environment"; - } - } - - # Did we already get a username with or without password on the command line? - if ( $opt{user} ne 0 || $opt{container} ) { - my $username = $opt{user} ne 0 ? $opt{user} : "root"; - $mysqllogin = - "-u $username " - . ( ( $opt{pass} ne 0 ) ? "-p'$opt{pass}' " : " " ) - . $remotestring; - my $loginstatus = - execute_system_command( - "$mysqlcmd -Nrs -e 'select \"mysqld is alive\";' $mysqllogin"); - if ( $loginstatus =~ /mysqld is alive/ ) { - goodprint "Logged in using credentials passed on the command line"; - return 1; - } - } - - my $svcprop = which( "svcprop", $ENV{'PATH'} ); - if ( substr( $svcprop, 0, 1 ) =~ "/" ) { - - # We are on solaris - ( - my $mysql_login = execute_system_command( -"svcprop -p quickbackup/username svc:/network/mysql-quickbackup:default" - ) - ) =~ s/\s+$//; - ( - my $mysql_pass = execute_system_command( -"svcprop -p quickbackup/password svc:/network/mysql-quickbackup:default" - ) - ) =~ s/\s+$//; - if ( substr( $mysql_login, 0, 7 ) ne "svcprop" ) { - - # mysql-quickbackup is installed - $mysqllogin = "-u $mysql_login -p$mysql_pass"; - my $loginstatus = - execute_system_command("mysqladmin $mysqllogin ping"); - if ( $loginstatus =~ /mysqld is alive/ ) { - goodprint "Logged in using credentials from mysql-quickbackup."; - return 1; - } - else { - badprint -"Attempted to use login credentials from mysql-quickbackup, but they failed."; - exit 1; - } - } - } - elsif ( -r "/etc/psa/.psa.shadow" and $doremote == 0 ) { - - # It's a Plesk box, use the available credentials - $mysqllogin = - "-u admin -p" . execute_system_command("cat /etc/psa/.psa.shadow"); - my $loginstatus = - execute_system_command("$mysqladmincmd ping $mysqllogin"); - unless ( $loginstatus =~ /mysqld is alive/ ) { - - # Plesk 10+ - $mysqllogin = - "-u admin -p" - . execute_system_command( - "/usr/local/psa/bin/admin --show-password"); - $loginstatus = - execute_system_command("$mysqladmincmd ping $mysqllogin"); - unless ( $loginstatus =~ /mysqld is alive/ ) { - badprint -"Attempted to use login credentials from Plesk and Plesk 10+, but they failed."; - exit 1; - } - } - } - elsif ( -r "/usr/local/directadmin/conf/mysql.conf" and $doremote == 0 ) { - - # It's a DirectAdmin box, use the available credentials - my $mysqluser = - execute_system_command( - "cat /usr/local/directadmin/conf/mysql.conf | egrep '^user=.*'"); - my $mysqlpass = - execute_system_command( - "cat /usr/local/directadmin/conf/mysql.conf | egrep '^passwd=.*'"); - - $mysqluser =~ s/user=//; - $mysqluser =~ s/[\r\n]//; - $mysqlpass =~ s/passwd=//; - $mysqlpass =~ s/[\r\n]//; - - $mysqllogin = "-u $mysqluser -p$mysqlpass"; - - my $loginstatus = execute_system_command("mysqladmin ping $mysqllogin"); - unless ( $loginstatus =~ /mysqld is alive/ ) { - badprint -"Attempted to use login credentials from DirectAdmin, but they failed."; - exit 1; - } - } - elsif ( -r "/etc/mysql/debian.cnf" - and $doremote == 0 - and $opt{'defaults-file'} eq '' ) - { - - # We have a Debian maintenance account, use it - $mysqllogin = "--defaults-file=/etc/mysql/debian.cnf"; - my $loginstatus = - execute_system_command("$mysqladmincmd $mysqllogin ping"); - if ( $loginstatus =~ /mysqld is alive/ ) { - goodprint - "Logged in using credentials from Debian maintenance account."; - return 1; - } - else { - badprint -"Attempted to use login credentials from Debian maintenance account, but they failed."; - exit 1; - } - } - elsif ( $opt{'defaults-file'} and -r "$opt{'defaults-file'}" ) { - - # defaults-file - debugprint "defaults file detected: $opt{'defaults-file'}"; - my $mysqlclidefaults = - execute_system_command("$mysqlcmd --print-defaults"); - debugprint "MySQL Client Default File: $opt{'defaults-file'}"; - - $mysqllogin = "--defaults-file=" . $opt{'defaults-file'}; - my $loginstatus = - execute_system_command("$mysqladmincmd $mysqllogin ping"); - if ( $loginstatus =~ /mysqld is alive/ ) { - goodprint "Logged in using credentials from defaults file account."; - return 1; - } - } - elsif ( $opt{'defaults-extra-file'} - and -r "$opt{'defaults-extra-file'}" ) - { - - # defaults-extra-file - debugprint "defaults extra file detected: $opt{'defaults-extra-file'}"; - my $mysqlclidefaults = - execute_system_command("$mysqlcmd --print-defaults"); - debugprint - "MySQL Client Extra Default File: $opt{'defaults-extra-file'}"; - - $mysqllogin = "--defaults-extra-file=" . $opt{'defaults-extra-file'}; - my $loginstatus = - execute_system_command("$mysqladmincmd $mysqllogin ping"); - if ( $loginstatus =~ /mysqld is alive/ ) { - goodprint - "Logged in using credentials from extra defaults file account."; - return 1; - } - } - else { - # It's not Plesk or Debian, we should try a login - debugprint "$mysqladmincmd $remotestring ping 2>&1"; - - #my $loginstatus = ""; - debugprint "Using mysqlcmd: $mysqlcmd"; - - #if (defined($mysqladmincmd)) { - # infoprint "Using mysqladmin to check login"; - # $loginstatus=`$mysqladmincmd $remotestring ping 2>&1`; - #} else { - infoprint "Using mysql to check login"; - my $loginstatus = - execute_system_command( -"$mysqlcmd $remotestring -Nrs -e 'select \"mysqld is alive\"' --connect-timeout=3" - ); - - #} - - if ( $loginstatus =~ /mysqld is alive/ ) { - - # Login went just fine - $mysqllogin = " $remotestring "; - - # Did this go well because of a .my.cnf file or is there no password set? - my $userpath = - $is_win - ? ( $ENV{MARIADB_HOME} || $ENV{MYSQL_HOME} || $ENV{USERPROFILE} ) - : ( $ENV{HOME} // '' ); - if ( length($userpath) > 0 ) { - chomp($userpath); - } - unless ( -e "${userpath}/.my.cnf" or -e "${userpath}/.mylogin.cnf" ) - { - badprint - "SECURITY RISK: Successfully authenticated without password"; - } - return 1; - } - else { - if ( $opt{'noask'} == 1 ) { - badprint - "Attempted to use login credentials, but they were invalid"; - exit 1; - } - my ( $name, $password ); - - # If --user is defined no need to ask for username - if ( $opt{user} ne 0 ) { - $name = $opt{user}; - } - else { - print STDERR "Please enter your MySQL administrative login: "; - $name = ; - } - - # If --pass is defined no need to ask for password - if ( $opt{pass} ne 0 ) { - $password = $opt{pass}; - } - else { - print STDERR - "Please enter your MySQL administrative password: "; - system("stty -echo >$devnull 2>&1"); - $password = ; - system("stty echo >$devnull 2>&1"); - } - chomp($password); - chomp($name); - $mysqllogin = "-u $name"; - - if ( length($password) > 0 ) { - if ($is_win) { - $mysqllogin .= " -p\"$password\""; - } - else { - $mysqllogin .= " -p'$password'"; - } - } - $mysqllogin .= $remotestring; - my $loginstatus = - execute_system_command("$mysqladmincmd ping $mysqllogin"); - if ( $loginstatus =~ /mysqld is alive/ ) { - - #print STDERR ""; - if ( !length($password) ) { - - # Did this go well because of a .my.cnf file or is there no password set? - my $userpath = - $is_win - ? ( $ENV{MARIADB_HOME} - || $ENV{MYSQL_HOME} - || $ENV{USERPROFILE} ) - : ( $ENV{HOME} // '' ); - chomp($userpath); - unless ( -e "$userpath/.my.cnf" ) { - print STDERR ""; - badprint -"SECURITY RISK: Successfully authenticated without password"; - } - } - return 1; - } - else { - #print STDERR ""; - badprint - "Attempted to use login credentials, but they were invalid."; - exit 1; - } - exit 1; - } - } -} - -sub build_mysql_connection_command { - return "$mysqlcmd $mysqllogin"; -} - -# MySQL Request Array -sub select_array { - my $req = shift; - debugprint "PERFORM: $req "; - my $req_escaped = $req; - $req_escaped =~ s/"/\\"/g; - my @result = - execute_system_command( - "$mysqlcmd $mysqllogin -Bse \"$req_escaped\" 2>>$devnull"); - if ( $? != 0 ) { - badprint "Failed to execute: $req"; - badprint "FAIL Execute SQL / return code: $?"; - if ( $opt{debug} ) { - debugprint execute_system_command( - "$mysqlcmd $mysqllogin -Bse \"$req_escaped\" 2>&1"); - } - - #exit $?; - return (); - } - debugprint "select_array: return code : $?"; - chomp(@result); - return @result; -} - -# MySQL Request Array -sub select_array_with_headers { - my $req = shift; - debugprint "PERFORM: $req "; - my $req_escaped = $req; - $req_escaped =~ s/"/\\"/g; - my @result = - execute_system_command( - "$mysqlcmd $mysqllogin -Bre \"$req_escaped\" 2>>$devnull"); - if ( $? != 0 ) { - badprint "Failed to execute: $req"; - badprint "FAIL Execute SQL / return code: $?"; - if ( $opt{debug} ) { - debugprint execute_system_command( - "$mysqlcmd $mysqllogin -Bse \"$req_escaped\" 2>&1"); - } - - #exit $?; - } - debugprint "select_array_with_headers: return code : $?"; - chomp(@result); - return @result; -} - -# MySQL Request Array -sub select_csv_file { - my $tfile = shift; - my $req = shift; - debugprint "PERFORM: $req CSV into $tfile"; - - #return; - my @result = select_array_with_headers($req); - open( my $fh, '>', $tfile ) or die "Could not open file '$tfile' $!"; - for my $l (@result) { - $l =~ s/\t/","/g; - $l =~ s/^/"/; - $l =~ s/$/"\n/; - print $fh $l; - print $l if $opt{debug}; - } - close $fh; - infoprint "CSV file $tfile created"; -} - -sub human_size { - my ( $size, $n ) = ( shift, 0 ); - ++$n and $size /= 1024 until $size < 1024; - return sprintf "%.2f %s", $size, (qw[ bytes KB MB GB TB ])[$n]; -} - -# MySQL Request one -sub select_one { - my $req = shift; - debugprint "PERFORM: $req "; - my $result = - execute_system_command("$mysqlcmd $mysqllogin -Bse \"$req\" 2>>$devnull"); - if ( $? != 0 ) { - badprint "Failed to execute: $req"; - badprint "FAIL Execute SQL / return code: $?"; - if ( $opt{debug} ) { - debugprint execute_system_command( - "$mysqlcmd $mysqllogin -Bse \"$req\" 2>&1"); - } - - #exit $?; - return ""; - } - debugprint "select_array: return code : $?"; - chomp($result); - return $result; -} - -# MySQL Request one -sub select_one_g { - my $pattern = shift; - - my $req = shift; - debugprint "PERFORM: $req "; - my @result = execute_system_command( - "$mysqlcmd $mysqllogin -re \"$req\\G\" 2>>$devnull"); - if ( $? != 0 ) { - badprint "Failed to execute: $req"; - badprint "FAIL Execute SQL / return code: $?"; - if ( $opt{debug} ) { - debugprint execute_system_command( - "$mysqlcmd $mysqllogin -Bse \"$req\" 2>&1"); - } - - #exit $?; - } - debugprint "select_array: return code : $?"; - chomp(@result); - return ( grep { /$pattern/ } @result )[0]; -} - -sub select_str_g { - my $pattern = shift; - - my $req = shift; - my $str = select_one_g $pattern, $req; - return () unless defined $str; - my @val = split /:/, $str; - shift @val; - return trim(@val); -} - -sub select_user_dbs { - return select_array( -"SELECT DISTINCT TABLE_SCHEMA FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'percona', 'sys')" - ); -} - -sub select_tables_db { - my $schema = shift; - return select_array( -"SELECT DISTINCT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='$schema'" - ); -} - -sub select_indexes_db { - my $schema = shift; - return select_array( -"SELECT DISTINCT INDEX_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='$schema'" - ); -} - -sub select_views_db { - my $schema = shift; - return select_array( -"SELECT DISTINCT TABLE_NAME FROM information_schema.VIEWS WHERE TABLE_SCHEMA='$schema'" - ); -} - -sub select_triggers_db { - my $schema = shift; - return select_array( -"SELECT DISTINCT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA='$schema'" - ); -} - -sub select_routines_db { - my $schema = shift; - return select_array( -"SELECT DISTINCT ROUTINE_NAME FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA='$schema'" - ); -} - -sub select_table_indexes_db { - my $schema = shift; - my $tbname = shift; - return select_array( -"SELECT INDEX_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='$schema' AND TABLE_NAME='$tbname'" - ); -} - -sub select_table_columns_db { - my $schema = shift; - my $table = shift; - return select_array( -"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='$schema' AND TABLE_NAME='$table'" - ); -} - -sub get_password_column_name { - my @mysql_user_columns = select_table_columns_db( 'mysql', 'user' ); - my $pass_column = ''; - my $auth_column = ''; - - if ( grep { /^authentication_string$/msx } @mysql_user_columns ) { - $auth_column = 'authentication_string'; - } - - # Case-insensitive match for Password/password - my @pass_matches = grep { lc($_) eq 'password' } @mysql_user_columns; - if (@pass_matches) { - $pass_column = $pass_matches[0]; - } - - if ( $auth_column && $pass_column ) { - return "IF(plugin='mysql_native_password', $auth_column, $pass_column)"; - } - elsif ($auth_column) { - return $auth_column; - } - elsif ($pass_column) { - return $pass_column; - } - - return ''; -} - -sub get_tuning_info { - my @infoconn = select_array "\\s"; - my ( $tkey, $tval ); - @infoconn = - grep { !/Threads:/ and !/Connection id:/ and !/pager:/ and !/Using/ } - @infoconn; - foreach my $line (@infoconn) { - if ( $line =~ /\s*(.*):\s*(.*)/ ) { - debugprint "$1 => $2"; - $tkey = $1; - $tval = $2; - chomp($tkey); - chomp($tval); - $result{'MySQL Client'}{$tkey} = $tval; - } - } - $result{'MySQL Client'}{'Client Path'} = $mysqlcmd; - $result{'MySQL Client'}{'Admin Path'} = $mysqladmincmd; - $result{'MySQL Client'}{'Authentication Info'} = $mysqllogin; - -} - -# Populates all of the variable and status hashes -our ( %mystat, %myvar, $dummyselect, %myrepl, %myslaves, %mycalc ); - -sub arr2hash { - my $href = shift; - my $harr = shift; - my $sep = shift; - my $key = ''; - my $val = ''; - - $sep = '\s' unless defined($sep); - foreach my $line (@$harr) { - next if ( $line =~ m/^\*\*\*\*\*\*\*/ ); - $line =~ /([a-zA-Z0-9_\/]*)\s*$sep\s*(.*)/; - $key = $1; - $val = $2; - $$href{$key} = $val; - - debugprint " * $key = $val" if $key =~ /$opt{dbgpattern}/i; - } -} - -sub check_privileges { - debugprint "Checking database privileges..."; - my @grants = select_array("SHOW GRANTS FOR CURRENT_USER()"); - my $all_grants = join( " ", @grants ); - - # If the user has ALL PRIVILEGES or SUPER, we assume they have enough - if ( $all_grants =~ /ALL PRIVILEGES/i || $all_grants =~ /SUPER/i ) { - debugprint "Current user has high-level privileges (ALL or SUPER)."; - return; - } - - my @required_privs = - ( 'SELECT', 'PROCESS', 'EXECUTE', 'SHOW DATABASES', 'SHOW VIEW' ); - - # Version-specific privileges - if ( mysql_version_ge( 8, 0 ) && $myvar{'version'} !~ /mariadb/i ) { - push( @required_privs, 'REPLICATION SLAVE', 'REPLICATION CLIENT' ); - } - elsif ( $myvar{'version'} =~ /mariadb/i && mysql_version_ge( 10, 5 ) ) { - push( @required_privs, - 'BINLOG MONITOR', - 'REPLICATION MASTER ADMIN', - 'SLAVE MONITOR' ); - -# MariaDB 11+ might use REPLICA MONITOR instead of SLAVE MONITOR, but SLAVE MONITOR is usually still there as an alias - } - else { - push( @required_privs, 'REPLICATION CLIENT' ); - } - - my @missing_privs = (); - foreach my $priv (@required_privs) { - - # Use word boundaries and case-insensitive matching - if ( $all_grants !~ /\b$priv\b/i ) { - push( @missing_privs, $priv ); - } - } - - if (@missing_privs) { - badprint "Current user is missing the following privileges: " - . join( ", ", @missing_privs ); - badprint "Some checks may be skipped or provide incomplete results."; - infoprint "Refer to README.md for the minimum required privileges."; - } - else { - debugprint "Current user has all required privileges."; - } -} - -sub get_all_vars { - - # We need to initiate at least one query so that our data is useable - $dummyselect = select_one "SELECT VERSION()"; - if ( not defined($dummyselect) or $dummyselect eq "" ) { - badprint - "You probably do not have enough privileges to run MySQLTuner ..."; - exit(256); - } - $dummyselect =~ s/(.*?)\-.*/$1/; - debugprint "VERSION: " . $dummyselect . ""; - $result{'MySQL Client'}{'Version'} = $dummyselect; - - my @mysqlvarlist = select_array("SHOW VARIABLES"); - push( @mysqlvarlist, select_array("SHOW GLOBAL VARIABLES") ); - arr2hash( \%myvar, \@mysqlvarlist ); - $result{'Variables'} = \%myvar; - - # Check privileges after we have version and variable information - check_privileges(); - - my @mysqlstatlist = select_array("SHOW STATUS"); - push( @mysqlstatlist, select_array("SHOW GLOBAL STATUS") ); - arr2hash( \%mystat, \@mysqlstatlist ); - $result{'Status'} = \%mystat; - unless ( defined( $myvar{'innodb_support_xa'} ) ) { - $myvar{'innodb_support_xa'} = 'ON'; - } - $mystat{'Uptime'} = 1 - unless defined( $mystat{'Uptime'} ) - and $mystat{'Uptime'} > 0; - $myvar{'have_galera'} = "NO"; - if ( defined( $myvar{'wsrep_provider_options'} ) - && $myvar{'wsrep_provider_options'} ne "" - && $myvar{'wsrep_on'} ne "OFF" ) - { - $myvar{'have_galera'} = "YES"; - debugprint "Galera options: " . $myvar{'wsrep_provider_options'}; - } - - # Workaround for MySQL bug #59393 wrt. ignore-builtin-innodb - if ( ( $myvar{'ignore_builtin_innodb'} || "" ) eq "ON" ) { - $myvar{'have_innodb'} = "NO"; - } - - # Support GTID MODE FOR MARIADB - # Issue MariaDB GTID mode #513 - $myvar{'gtid_mode'} = 'ON' - if ( defined( $myvar{'gtid_current_pos'} ) - and $myvar{'gtid_current_pos'} ne '' ); - - # Whether the server uses a thread pool to handle client connections - # MariaDB: thread_handling = pool-of-threads - # MySQL: thread_handling = loaded-dynamically - $myvar{'have_threadpool'} = "NO"; - if ( - defined( $myvar{'thread_handling'} ) - and ( $myvar{'thread_handling'} eq 'pool-of-threads' - || $myvar{'thread_handling'} eq 'loaded-dynamically' ) - ) - { - $myvar{'have_threadpool'} = "YES"; - } - - # have_* for engines is deprecated and will be removed in MySQL 5.6; - # check SHOW ENGINES and set corresponding old style variables. - # Also works around MySQL bug #59393 wrt. skip-innodb - my @mysqlenginelist = select_array "SHOW ENGINES"; - foreach my $line (@mysqlenginelist) { - if ( $line =~ /^([a-zA-Z_]+)\s+(\S+)/ ) { - my $engine = lc($1); - - if ( $engine eq "federated" || $engine eq "blackhole" ) { - $engine .= "_engine"; - } - elsif ( $engine eq "berkeleydb" ) { - $engine = "bdb"; - } - my $val = ( $2 eq "DEFAULT" ) ? "YES" : $2; - $myvar{"have_$engine"} = $val; - $result{'Storage Engines'}{$engine} = $2; - } - } - - #debugprint Dumper(@mysqlenginelist); - - my @mysqlslave; - - # Issue #553: Fix replication command compatibility - # MySQL 8.0+: SHOW REPLICA STATUS (deprecated: SHOW SLAVE STATUS) - # MariaDB 10.5+: SHOW REPLICA STATUS (deprecated: SHOW SLAVE STATUS) - # Older versions: SHOW SLAVE STATUS - my $is_mysql8 = - ( $myvar{'version'} =~ /^8\./ && $myvar{'version'} !~ /mariadb/i ); - my $is_mariadb105 = - ( $myvar{'version'} =~ /mariadb/i && mysql_version_ge( 10, 5 ) ); - - if ( $is_mysql8 or $is_mariadb105 ) { - @mysqlslave = select_array("SHOW REPLICA STATUS\\G"); - } - else { - @mysqlslave = select_array("SHOW SLAVE STATUS\\G"); - } - arr2hash( \%myrepl, \@mysqlslave, ':' ); - $result{'Replication'}{'Status'} = \%myrepl; - - # Issue #553: Fix slave/replica host listing commands - # MySQL 8.0+: SHOW REPLICAS (deprecated: SHOW SLAVE HOSTS) - # MariaDB 10.5+: SHOW REPLICA HOSTS (deprecated: SHOW SLAVE HOSTS) - # Older versions: SHOW SLAVE HOSTS - my @mysqlslaves; - if ($is_mysql8) { - @mysqlslaves = select_array("SHOW REPLICAS"); - } - elsif ($is_mariadb105) { - @mysqlslaves = select_array("SHOW REPLICA HOSTS\\G"); - } - else { - @mysqlslaves = select_array("SHOW SLAVE HOSTS\\G"); - } - - my @lineitems = (); - foreach my $line (@mysqlslaves) { - debugprint "L: $line "; - @lineitems = split /\s+/, $line; - $myslaves{ $lineitems[0] } = $line; - $result{'Replication'}{'Slaves'}{ $lineitems[0] } = $lineitems[4]; - } - - # InnoDB Transaction Info - if ( $myvar{'have_innodb'} eq "YES" ) { - if ( mysql_version_ge(5) ) { - $mycalc{'innodb_active_transactions'} = - select_one("SELECT COUNT(*) FROM information_schema.INNODB_TRX"); - $mycalc{'innodb_longest_transaction_duration'} = select_one( -"SELECT IFNULL(MAX(TIMESTAMPDIFF(SECOND, trx_started, NOW())),0) FROM information_schema.INNODB_TRX" - ); - } - } -} - -sub remove_cr { - return map { - my $line = $_; - $line =~ s/\n$//g; - $line =~ s/^\s+$//g; - $line; - } @_; -} - -sub remove_empty { - grep { $_ ne '' } @_; -} - -sub grep_file_contents { - my $file = shift; - my $patt; -} - -sub get_file_contents { - my $file = shift; - open( my $fh, "<", $file ) or die "Can't open $file for read: $!"; - my @lines = <$fh>; - close $fh or die "Cannot close $file: $!"; - @lines = remove_cr(@lines); - return @lines; -} - -sub get_basic_passwords { - return get_file_contents(shift); -} - -sub get_log_file_real_path { - my $file = shift; - my $hostname = shift; - my $datadir = shift; - if ( -f "$file" ) { - return $file; - } - elsif ( -f "$hostname.log" ) { - return "$hostname.log"; - } - elsif ( -f "$hostname.err" ) { - return "$hostname.err"; - } - elsif ( -f "$datadir$hostname.err" ) { - return "$datadir$hostname.err"; - } - elsif ( -f "$datadir$hostname.log" ) { - return "$datadir$hostname.log"; - } - elsif ( -f "$datadir" . "mysql_error.log" ) { - return "$datadir" . "mysql_error.log"; - } - elsif ( -f "/var/log/mysql.log" ) { - return "/var/log/mysql.log"; - } - elsif ( -f "/var/log/mysqld.log" ) { - return "/var/log/mysqld.log"; - } - elsif ( -f "/var/log/mysql/$hostname.err" ) { - return "/var/log/mysql/$hostname.err"; - } - elsif ( -f "/var/log/mysql/$hostname.log" ) { - return "/var/log/mysql/$hostname.log"; - } - elsif ( -f "/var/log/mysql/" . "mysql_error.log" ) { - return "/var/log/mysql/" . "mysql_error.log"; - } - else { - return $file; - } -} - -sub log_file_recommendations { - my $has_pfs_error_log = 0; - if ( $opt{'dbstat'} ) { - my $pfs_result = select_one( -"SELECT 1 FROM information_schema.tables WHERE table_schema='performance_schema' AND table_name='error_log' LIMIT 1" - ); - $has_pfs_error_log = 1 if $pfs_result; - } - - if ( is_remote eq 1 && !$has_pfs_error_log ) { - infoprint -"Skipping error log files checks on remote host (No Performance Schema error_log)"; - return; - } - my $fh; - $myvar{'log_error'} = $opt{'server-log'} - || get_log_file_real_path( $myvar{'log_error'}, $myvar{'hostname'}, - $myvar{'datadir'} ); - - # Use explicit container if provided - if ( $opt{'container'} ) { - my $container_cmd = "docker"; - if ( $opt{'container'} =~ /^(docker|podman|kubectl):(.*)/ ) { - $myvar{'log_error'} = $opt{'container'}; - } - else { - if ( which( "podman", $ENV{'PATH'} ) - && !which( "docker", $ENV{'PATH'} ) ) - { - $container_cmd = "podman"; - } - $myvar{'log_error'} = "$container_cmd:$opt{'container'}"; - } - debugprint "Using explicit container: $myvar{'log_error'}"; - } - - # Try to find logs from docker/podman if file doesn't exist locally - elsif (!-f "$myvar{'log_error'}" - && $myvar{'log_error'} !~ /^(docker|podman|kubectl|systemd):/ - && !is_docker() ) - { - my $container_cmd = ""; - if ( which( "docker", $ENV{'PATH'} ) ) { - $container_cmd = "docker"; - } - elsif ( which( "podman", $ENV{'PATH'} ) ) { - $container_cmd = "podman"; - } - - if ( $container_cmd ne "" ) { - my $port = $opt{'port'} || 3306; - my $container = - execute_system_command( -"$container_cmd ps --filter \"publish=$port\" --format \"{{.Names}}\" | grep -vEi \"traefik|haproxy|maxscale|maxsale|proxy\" | head -n 1" - ); - chomp $container; - if ( $container eq "" ) { - $container = - execute_system_command( -"$container_cmd ps --format \"{{.Names}} {{.Image}}\" | grep -Ei \"mysql|mariadb|percona|db|database\" | grep -vEi \"traefik|haproxy|maxscale|maxsale|proxy\" | head -n 1 | awk '{print \$1}'" - ); - chomp $container; - } - if ( $container ne "" ) { - $myvar{'log_error'} = "$container_cmd:$container"; - debugprint "Detected $container_cmd container: $container"; - } - } - } - - subheaderprint "Log file Recommendations"; - if ( $has_pfs_error_log && !$opt{'server-log'} ) { - goodprint "Performance Schema error_log table detected"; - my $pfs_count = - select_one("SELECT COUNT(*) FROM performance_schema.error_log"); - infoprint "Performance Schema error_log: $pfs_count entries detected"; - - # Build mysql command for streaming output - my $mysql_conn = build_mysql_connection_command(); - open( $fh, '-|', -"$mysql_conn -N -s -e \"SELECT DATA FROM performance_schema.error_log ORDER BY LOGGED DESC LIMIT $maxlines\"" - ) || debugprint "Failed to open PFS error_log stream"; - $myvar{'log_error'} = "performance_schema.error_log"; - } - elsif ( "$myvar{'log_error'}" eq "stderr" ) { - badprint -"log_error is set to $myvar{'log_error'}, but this script can't read stderr"; - return; - } - elsif ( $myvar{'log_error'} =~ /^(docker|podman|kubectl):(.*)/ ) { - open( $fh, '-|', "$1 logs --tail=$maxlines '$2'" ) - // die "Can't start $1 $!"; - goodprint "Log from cloud` $myvar{'log_error'} exists"; - } - elsif ( $myvar{'log_error'} =~ /^systemd:(.*)/ ) { - open( $fh, '-|', "journalctl -n $maxlines -b -u '$1'" ) - // die "Can't start journalctl $!"; - goodprint "Log journal` $myvar{'log_error'} exists"; - } - elsif ( -f "$myvar{'log_error'}" ) { - goodprint "Log file $myvar{'log_error'} exists"; - my $size = ( stat $myvar{'log_error'} )[7]; - infoprint "Log file: " - . $myvar{'log_error'} . " (" - . hr_bytes_rnd($size) . ")"; - - if ( $size > 0 ) { - goodprint "Log file $myvar{'log_error'} is not empty"; - if ( $size < 32 * 1024 * 1024 ) { - goodprint "Log file $myvar{'log_error'} is smaller than 32 MB"; - } - else { - badprint "Log file $myvar{'log_error'} is bigger than 32 MB"; - push @generalrec, - $myvar{'log_error'} - . " is > 32MB, you should analyze why or implement a rotation log strategy such as logrotate!"; - } - } - else { - infoprint -"Log file $myvar{'log_error'} is empty. Assuming log-rotation. Use --server-log={file} for explicit file"; - return; - } - if ( !open( $fh, '<', $myvar{'log_error'} ) ) { - badprint "Log file $myvar{'log_error'} isn't readable."; - return; - } - goodprint "Log file $myvar{'log_error'} is readable."; - - if ( $maxlines * 80 < $size ) { - seek( $fh, -$maxlines * 80, 2 ); - <$fh>; # discard line fragment - } - } - else { - badprint "Log file $myvar{'log_error'} doesn't exist"; - return; - } - - my $numLi = 0; - my $nbWarnLog = 0; - my $nbErrLog = 0; - my @lastShutdowns; - my @lastStarts; - - while ( my $logLi = <$fh> ) { - chomp $logLi; - $numLi++; - debugprint "$numLi: $logLi" if $logLi =~ /\[(warning|error)\]/i; - $nbErrLog++ if $logLi =~ /\[error\]/i; - $nbWarnLog++ if $logLi =~ /\[warning\]/i; - push @lastShutdowns, $logLi - if $logLi =~ /Shutdown complete/ and $logLi !~ /Innodb/i; - push @lastStarts, $logLi if $logLi =~ /ready for connections/; - } - close $fh; - - if ( $nbWarnLog > 0 ) { - badprint "$myvar{'log_error'} contains $nbWarnLog warning(s)."; - push @generalrec, "Check warning line(s) in $myvar{'log_error'} file"; - } - else { - goodprint "$myvar{'log_error'} doesn't contain any warning."; - } - if ( $nbErrLog > 0 ) { - badprint "$myvar{'log_error'} contains $nbErrLog error(s)."; - push @generalrec, "Check error line(s) in $myvar{'log_error'} file"; - } - else { - goodprint "$myvar{'log_error'} doesn't contain any error."; - } - - infoprint scalar @lastStarts . " start(s) detected in $myvar{'log_error'}"; - my $nStart = 0; - my $nEnd = 10; - if ( scalar @lastStarts < $nEnd ) { - $nEnd = scalar @lastStarts; - } - for my $startd ( reverse @lastStarts[ -$nEnd .. -1 ] ) { - $nStart++; - infoprint "$nStart) $startd"; - } - infoprint scalar @lastShutdowns - . " shutdown(s) detected in $myvar{'log_error'}"; - $nStart = 0; - $nEnd = 10; - if ( scalar @lastShutdowns < $nEnd ) { - $nEnd = scalar @lastShutdowns; - } - for my $shutd ( reverse @lastShutdowns[ -$nEnd .. -1 ] ) { - $nStart++; - infoprint "$nStart) $shutd"; - } - - #exit 0; -} - -sub cve_recommendations { - subheaderprint "CVE Security Recommendations"; - unless ( defined( $opt{cvefile} ) && $opt{cvefile} ) { - infoprint "Skipped: --cvefile option not specified"; - return; - } - unless ( -f "$opt{cvefile}" ) { - infoprint "Skipped: CVE file not found ($opt{cvefile})"; - return; - } - -#$mysqlvermajor=10; -#$mysqlverminor=1; -#$mysqlvermicro=17; -#prettyprint "Look for related CVE for $myvar{'version'} or lower in $opt{cvefile}"; - my $cvefound = 0; - open( my $fh, "<", $opt{cvefile} ) - or die "Can't open $opt{cvefile} for read: $!"; - while ( my $cveline = <$fh> ) { - my @cve = split( ';', $cveline ); - debugprint -"Comparing $mysqlvermajor\.$mysqlverminor\.$mysqlvermicro with $cve[1]\.$cve[2]\.$cve[3] : " - . ( mysql_version_le( $cve[1], $cve[2], $cve[3] ) ? '<=' : '>' ); - - # Avoid not major/minor version corresponding CVEs - next - unless ( int( $cve[1] ) == $mysqlvermajor - && int( $cve[2] ) == $mysqlverminor ); - if ( int( $cve[3] ) >= $mysqlvermicro ) { - badprint "$cve[4](<= $cve[1]\.$cve[2]\.$cve[3]) : $cve[6]"; - $result{'CVE'}{'List'}{$cvefound} = - "$cve[4](<= $cve[1]\.$cve[2]\.$cve[3]) : $cve[6]"; - $cvefound++; - } - } - close $fh or die "Cannot close $opt{cvefile}: $!"; - $result{'CVE'}{'nb'} = $cvefound; - - my $cve_warning_notes = ""; - if ( $cvefound == 0 ) { - goodprint "NO SECURITY CVE FOUND FOR YOUR VERSION"; - return; - } - if ( $mysqlvermajor eq 5 and $mysqlverminor eq 5 ) { - infoprint - "False positive CVE(s) for MySQL and MariaDB 5.5.x can be found."; - infoprint "Check carefully each CVE for those particular versions"; - } - badprint $cvefound . " CVE(s) found for your MySQL release."; - push( @generalrec, - $cvefound - . " CVE(s) found for your MySQL release. Consider upgrading your version !" - ); -} - -sub get_opened_ports { - my @opened_ports = execute_system_command('netstat -ltn'); - if ($is_win) { - @opened_ports = grep { /LISTEN/ } execute_system_command('netstat -n'); - } - @opened_ports = map { - my $v = $_; - $v =~ s/^.*:(\d+)\s.*$/$1/; - $v =~ s/\D//g; - $v; - } @opened_ports; - @opened_ports = sort { $a <=> $b } grep { !/^$/ } @opened_ports; - - #debugprint Dumper \@opened_ports; - $result{'Network'}{'TCP Opened'} = \@opened_ports; - return @opened_ports; -} - -sub is_open_port { - my $port = shift; - if ( grep { /^$port$/ } get_opened_ports ) { - return 1; - } - return 0; -} - -sub get_process_memory { - return 0 if $is_win; #Windows cmd cannot provide this - my $pid = shift; - - # Linux /proc fallback - if ( $^O eq 'linux' && -f "/proc/$pid/statm" ) { - if ( open( my $fh, '<', "/proc/$pid/statm" ) ) { - my $line = <$fh>; - close($fh); - if ( $line =~ /^\d+\s+(\d+)/ ) { - my $rss_pages = $1; - - # Get page size (default to 4096 if uncertain, but usually 4096 on Linux) - my $pagesize = POSIX::sysconf(POSIX::_SC_PAGESIZE) || 4096; - debugprint "Memory for PID $pid from /proc: " - . ( $rss_pages * $pagesize ); - return $rss_pages * $pagesize; - } - } - } - - my @mem = execute_system_command("ps -p $pid -o rss"); - return 0 if scalar @mem != 2; - return $mem[1] * 1024; -} - -sub get_other_process_memory { - return 0 if ( $opt{tbstat} == 0 ); - return 0 if $is_win; #Windows cmd cannot provide this - my @procs = execute_system_command('ps eaxo pid,command'); - @procs = map { - my $v = $_; - $v =~ s/.*PID.*//; - $v =~ s/.*mysqld.*//; - $v =~ s/.*\[.*\].*//; - $v =~ s/^\s+$//g; - $v =~ s/.*PID.*CMD.*//; - $v =~ s/.*systemd.*//; - $v =~ s/\s*?(\d+)\s*.*/$1/g; - $v; - } @procs; - @procs = remove_cr @procs; - @procs = remove_empty @procs; - my $totalMemOther = 0; - if (@procs) { - map { $totalMemOther += get_process_memory($_); } @procs; - } - return $totalMemOther; -} - -sub get_os_release { - if ( -f "/etc/lsb-release" ) { - my @info_release = get_file_contents "/etc/lsb-release"; - my $os_release = $info_release[3]; - $os_release =~ s/.*="//; - $os_release =~ s/"$//; - return $os_release; - } - - if ( -f "/etc/system-release" ) { - my @info_release = get_file_contents "/etc/system-release"; - return $info_release[0]; - } - - if ( -f "/etc/os-release" ) { - my @info_release = get_file_contents "/etc/os-release"; - my $os_release = $info_release[0]; - $os_release =~ s/.*="//; - $os_release =~ s/"$//; - return $os_release; - } - - if ( -f "/etc/issue" ) { - my @info_release = get_file_contents "/etc/issue"; - my $os_release = $info_release[0]; - $os_release =~ s/\s+\\n.*//; - return $os_release; - } - return "Unknown OS release"; -} - -sub get_fs_info { - my @sinfo = execute_system_command("df -P | grep '%'"); - my @iinfo = execute_system_command("df -Pi| grep '%'"); - shift @sinfo; - shift @iinfo; - - foreach my $info (@sinfo) { - - #exit(0); - if ( $info =~ /.*?(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.*)$/ ) { - next if $5 =~ m{(run|dev|sys|proc|snap|init)}; - if ( $4 > 85 ) { - badprint "mount point $5 is using $4 % total space (" - . human_size( $2 * 1024 ) . " / " - . human_size( $1 * 1024 ) . ")"; - push( @generalrec, "Add some space to $4 mountpoint." ); - } - else { - infoprint "mount point $5 is using $4 % total space (" - . human_size( $2 * 1024 ) . " / " - . human_size( $1 * 1024 ) . ")"; - } - $result{'Filesystem'}{'Space Pct'}{$5} = $4; - $result{'Filesystem'}{'Used Space'}{$5} = $2; - $result{'Filesystem'}{'Free Space'}{$5} = $3; - $result{'Filesystem'}{'Total Space'}{$5} = $1; - } - } - - @iinfo = map { - my $v = $_; - $v =~ s/.*\s(\d+)%\s+(.*)/$1\t$2/g; - $v; - } @iinfo; - foreach my $info (@iinfo) { - next if $info =~ m{(\d+)\t/(run|dev|sys|proc|snap)($|/)}; - if ( $info =~ /(\d+)\t(.*)/ ) { - if ( $1 > 85 ) { - badprint "mount point $2 is using $1 % of max allowed inodes"; - push( @generalrec, -"Cleanup files from $2 mountpoint or reformat your filesystem." - ); - } - else { - infoprint "mount point $2 is using $1 % of max allowed inodes"; - } - $result{'Filesystem'}{'Inode Pct'}{$2} = $1; - } - } -} - -sub get_fs_info_win { - my @sinfo = - execute_system_command('wmic logicaldisk get Name,Size,FreeSpace'); - - foreach my $info (@sinfo) { - if ( $info =~ /^\s*(\d+)\s+(.*?)\s+(\d+)\s*$/ ) { - my ( $free, $name, $size ) = ( $1, $2, $3 ); - my $used = $size - $free; - my $free_pct = int( ( $free / $size ) * 100 ); - my $used_pct = int( ( $used / $size ) * 100 ); - if ( $used_pct > 85 ) { - badprint "Disk $name is using $used_pct % total space (" - . human_size($used) . " / " - . human_size($size) . ")"; - push( @generalrec, "Add some space to DIsk $name." ); - } - else { - infoprint "Disk $name is using $used_pct % total space (" - . human_size($used) . " / " - . human_size($size) . ")"; - } - $result{'Filesystem'}{'Space Pct'}{$name} = $used_pct; - $result{'Filesystem'}{'Used Space'}{$name} = $used; - $result{'Filesystem'}{'Free Space'}{$name} = $free; - $result{'Filesystem'}{'Total Space'}{$name} = $size; - } - } -} - -sub merge_hash { - my $h1 = shift; - my $h2 = shift; - my %result = {}; - foreach my $substanceref ( $h1, $h2 ) { - while ( my ( $k, $v ) = each %$substanceref ) { - next if ( exists $result{$k} ); - $result{$k} = $v; - } - } - return \%result; -} - -sub is_virtual_machine { - my $prefix = get_transport_prefix(); - if ( $^O eq 'linux' ) { - if ( $prefix eq '' && open( my $cpuinfo, '<', '/proc/cpuinfo' ) ) { - my $isVm = 0; - while (<$cpuinfo>) { - if ( /^flags.*\ hypervisor / ) { $isVm = 1; last; } - } - close $cpuinfo; - return $isVm; - } - my $isVm = execute_system_command( - "grep -Ec '^flags.*\ hypervisor\ ' /proc/cpuinfo"); - return ( $isVm == 0 ? 0 : 1 ); - } - - if ( $^O eq 'freebsd' ) { - my $isVm = execute_system_command('sysctl -n kern.vm_guest'); - chomp $isVm; - print "FARK DEBUG isVm=[$isVm]"; - return ( $isVm eq 'none' ? 0 : 1 ); - } - - if ($is_win) { - my $isVM = execute_system_command('systeminfo'); - return ( $isVM =~ /System Model:\s*(Virtual Machine|VMware)/i ? 1 : 0 ); - } - return 0; -} - -sub infocmd { - my $cmd = "@_"; - debugprint "CMD: $cmd"; - my @result = execute_system_command($cmd); - @result = remove_cr @result; - for my $l (@result) { - infoprint "$l"; - } -} - -sub infocmd_tab { - my $cmd = "@_"; - debugprint "CMD: $cmd"; - my @result = execute_system_command($cmd); - @result = remove_cr @result; - for my $l (@result) { - infoprint "\t$l"; - } -} - -sub infocmd_one { - my $cmd = "@_"; - my @result = execute_system_command("$cmd 2>&1"); - @result = remove_cr @result; - return join ', ', @result; -} - -sub get_kernel_info { - my $prefix = get_transport_prefix(); - my @params = ( - 'fs.aio-max-nr', 'fs.aio-nr', - 'fs.nr_open', 'fs.file-max', - 'sunrpc.tcp_fin_timeout', 'sunrpc.tcp_max_slot_table_entries', - 'sunrpc.tcp_slot_table_entries', 'vm.swappiness' - ); - infoprint "Information about kernel tuning:"; - foreach my $param (@params) { - if ( $param =~ /^sunrpc/ ) { - next unless -d "/proc/sys/sunrpc"; - } - my @res = execute_system_command("sysctl $param 2>/dev/null"); - if ( $? == 0 ) { - foreach my $l (@res) { - chomp $l; - infoprint "\t$l"; - } - my $val = execute_system_command("sysctl -n $param 2>/dev/null"); - chomp $val; - $result{'OS'}{'Config'}{$param} = $val; - } - } - my $prefix = get_transport_prefix(); - if ( $prefix eq '' && -f "/proc/sys/vm/swappiness" ) { - if ( open( my $fh, '<', "/proc/sys/vm/swappiness" ) ) { - $swappiness = <$fh>; - close $fh; - chomp $swappiness; - } - } - if ( !defined $swappiness || $swappiness eq '' ) { - $swappiness = execute_system_command('sysctl -n vm.swappiness'); - chomp $swappiness; - } - - if ( $swappiness > 10 ) { - badprint - "Swappiness is > 10, please consider having a value lower than 10"; - push @generalrec, "setup swappiness lower or equal to 10"; - push @adjvars, -'vm.swappiness <= 10 (echo 10 > /proc/sys/vm/swappiness) or vm.swappiness=10 in /etc/sysctl.conf'; - } - else { - infoprint "Swappiness is < 10."; - } - - # only if /proc/sys/sunrpc exists - if ( -d "/proc/sys/sunrpc" ) { - my $tcp_slot_entries = execute_system_command( - "sysctl -n sunrpc.tcp_slot_table_entries 2>$devnull"); - chomp $tcp_slot_entries; - if ( $tcp_slot_entries eq '' or $tcp_slot_entries < 100 ) { - badprint -"Initial TCP slot entries is < 1M, please consider having a value greater than 100"; - push @generalrec, "setup Initial TCP slot entries greater than 100"; - push @adjvars, -'sunrpc.tcp_slot_table_entries > 100 (echo 128 > /proc/sys/sunrpc/tcp_slot_table_entries) or sunrpc.tcp_slot_table_entries=128 in /etc/sysctl.conf'; - } - else { - infoprint "TCP slot entries is > 100."; - } - } - - if ( -f "/proc/sys/fs/aio-max-nr" ) { - if ( execute_system_command('sysctl -n fs.aio-max-nr') < 1000000 ) { - badprint -"Max running total of the number of max. events is < 1M, please consider having a value greater than 1M"; - push @generalrec, "setup Max running number events greater than 1M"; - push @adjvars, -'fs.aio-max-nr > 1M (echo 1048576 > /proc/sys/fs/aio-max-nr) or fs.aio-max-nr=1048576 in /etc/sysctl.conf'; - } - else { - infoprint "Max Number of AIO events is > 1M."; - } - } - if ( -f "/proc/sys/fs/nr_open" ) { - if ( execute_system_command('sysctl -n fs.nr_open') < 1000000 ) { - badprint -"Max running total of the number of file open request is < 1M, please consider having a value greater than 1M"; - push @generalrec, - "setup running number of open request greater than 1M"; - push @adjvars, -'fs.aio-nr > 1M (echo 1048576 > /proc/sys/fs/nr_open) or fs.nr_open=1048576 in /etc/sysctl.conf'; - } - else { - infoprint "Max Number of open file requests is > 1M."; - } - } -} - -sub get_system_info { - my $prefix = get_transport_prefix(); - $result{'OS'}{'Release'} = get_os_release(); - infoprint get_os_release; - if ( is_docker() || $opt{'container'} ) { - infoprint "Machine type : Container"; - $result{'OS'}{'Virtual Machine'} = 'YES'; - } - elsif (is_virtual_machine) { - infoprint "Machine type : Virtual machine"; - $result{'OS'}{'Virtual Machine'} = 'YES'; - } - else { - infoprint "Machine type : Physical machine"; - $result{'OS'}{'Virtual Machine'} = 'NO'; - } - - $result{'Network'}{'Connected'} = 'NO'; - if ($is_win) { - execute_system_command("ping -n 1 ipecho.net > $devnull 2>&1") - if which( "ping", $ENV{'PATH'} ); - } - else { - execute_system_command("ping -c 1 ipecho.net > $devnull 2>&1") - if which( "ping", $ENV{'PATH'} ); - } - my $isConnected = $?; - if ( $isConnected == 0 ) { - infoprint "Internet : Connected"; - $result{'Network'}{'Connected'} = 'YES'; - } - else { - badprint "Internet : Disconnected"; - } - $result{'OS'}{'NbCore'} = cpu_cores; - infoprint "Number of Core CPU : " . cpu_cores; - - my ( $sysname, $nodename, $release, $version, $machine ); - if ( !$is_win && $prefix eq '' ) { - ( $sysname, $nodename, $release, $version, $machine ) = POSIX::uname(); - } - - $result{'OS'}{'Type'} = - $is_win ? 'Windows' : ( $prefix eq '' ? $sysname : execute_system_command('uname -o') ); - infoprint "Operating System Type : " - . ( $is_win ? 'Windows' : ( $prefix eq '' ? $sysname : execute_system_command('uname -o') ) ); - - $result{'OS'}{'Kernel'} = - $is_win - ? execute_system_command('ver') - : ( $prefix eq '' ? $release : execute_system_command('uname -r') ); - infoprint "Kernel Release : " - . ( $is_win ? execute_system_command('ver') : ( $prefix eq '' ? $release : execute_system_command('uname -r') ) ); - - $result{'OS'}{'Hostname'} = - ( !$is_win && $prefix eq '' ) ? $nodename : Sys::Hostname::hostname(); - - $result{'Network'}{'Internal Ip'} = - $is_win - ? execute_system_command( -'ipconfig |perl -ne "if (/IPv. Address/) {print s/^.*?([\\d\\.]*)\\s*$/$1/r; exit; }"' - ) - : execute_system_command('hostname -I'); - infoprint "Hostname : " . ( ( !$is_win && $prefix eq '' ) ? $nodename : Sys::Hostname::hostname() ); - infoprint "Network Cards : "; - - if ( which( "ip", $ENV{'PATH'} ) ) { - infocmd_tab "ip addr | grep -A1 mtu"; - } - elsif ( which( "ifconfig", $ENV{'PATH'} ) ) { - infocmd_tab "ifconfig| grep -A1 mtu"; - } - infoprint "Internal IP : " . ( ( !$is_win && $prefix eq '' ) ? execute_system_command('hostname -I') : infocmd_one "hostname -I" ); - if ( which( "ip", $ENV{'PATH'} ) ) { - $result{'Network'}{'Internal Ip'} = - execute_system_command('ip addr | grep -A1 mtu'); - } - elsif ( which( "ifconfig", $ENV{'PATH'} ) ) { - $result{'Network'}{'Internal Ip'} = - execute_system_command('ifconfig| grep -A1 mtu'); - } - my $httpcli = get_http_cli(); - infoprint "HTTP client found: $httpcli" if defined $httpcli; - - my $ext_ip = ""; - if ( defined $httpcli && $httpcli ne '' ) { - if ( $httpcli =~ /curl$/ ) { - $ext_ip = infocmd_one "$httpcli -s -m 3 ipecho.net/plain"; - } - elsif ( $httpcli =~ /wget$/ ) { - $ext_ip = infocmd_one "$httpcli -q -t 1 -T 3 -q -O - ipecho.net/plain"; - } - } - infoprint "External IP : " . $ext_ip; - $result{'Network'}{'External Ip'} = $ext_ip; - badprint "External IP : Can't check, no Internet connectivity" - unless defined($httpcli); - - my $ns_str = ""; - if ( $prefix eq '' && open( my $ns_file, '<', '/etc/resolv.conf' ) ) { - my @ns_list; - while (<$ns_file>) { - push @ns_list, $1 if /^\s*nameserver\s+([^\s]+)/; - } - close $ns_file; - $ns_str = join( ', ', @ns_list ); - } - else { - $ns_str = infocmd_one "grep 'nameserver' /etc/resolv.conf \| awk '{print \$2}'"; - } - infoprint "Name Servers : " . $ns_str; - - infoprint "Logged In users : "; - infocmd_tab "who"; - $result{'OS'}{'Logged users'} = execute_system_command('who'); - infoprint "Ram Usages in MB : "; - infocmd_tab "free -m | grep -v +"; - $result{'OS'}{'Free Memory RAM'} = - execute_system_command('free -m | grep -v +'); - infoprint "Load Average : "; - infocmd_tab "top -n 1 -b | grep 'load average:'"; - $result{'OS'}{'Load Average'} = - execute_system_command("top -n 1 -b | grep 'load average:'"); - - infoprint "System Uptime : "; - infocmd_tab "uptime"; - $result{'OS'}{'Uptime'} = execute_system_command('uptime'); -} - -sub system_recommendations { - if ( is_remote eq 1 ) { - infoprint "Skipping system checks on remote host"; - return; - } - return if ( $opt{sysstat} == 0 ); - subheaderprint "System Linux Recommendations"; - my $os = $is_win ? 'windows' : execute_system_command('uname'); - unless ( $os =~ /Linux/i ) { - infoprint "Skipped due to non Linux server"; - return; - } - prettyprint "Look for related Linux system recommendations"; - - #prettyprint '-'x78; - get_system_info(); - - my $nb_cpus = cpu_cores; - if ( $nb_cpus > 1 ) { - goodprint "There is at least one CPU dedicated to database server."; - } - else { - badprint -"There is only one CPU, consider dedicated one CPU for your database server"; - push_recommendation( 'System', - "Consider increasing number of CPU for your database server" ); - } - - if ( $physical_memory >= 1.5 * 1024 * 1024 * 1024 ) { - goodprint "There is at least 1.5 Gb of RAM dedicated to Linux server."; - } - else { - badprint -"There is less than 1,5 Gb of RAM, consider dedicated 1 Gb for your Linux server"; - push_recommendation( 'System', - "Consider increasing 1,5 / 2 Gb of RAM for your Linux server" ); - } - - my $omem = get_other_process_memory; - infoprint "User process except mysqld used " - . hr_bytes_rnd($omem) . " RAM."; - if ( ( 0.15 * $physical_memory ) < $omem ) { - if ( $opt{nondedicated} ) { - infoprint "No warning with --nondedicated option"; - infoprint -"Other user process except mysqld used more than 15% of total physical memory " - . percentage( $omem, $physical_memory ) . "% (" - . hr_bytes_rnd($omem) . " / " - . hr_bytes_rnd($physical_memory) . ")"; - } - else { - - badprint -"Other user process except mysqld used more than 15% of total physical memory " - . percentage( $omem, $physical_memory ) . "% (" - . hr_bytes_rnd($omem) . " / " - . hr_bytes_rnd($physical_memory) . ")"; - push( @generalrec, -"Consider stopping or dedicate server for additional process other than mysqld." - ); - push( @adjvars, -"DON'T APPLY SETTINGS BECAUSE THERE ARE TOO MANY PROCESSES RUNNING ON THIS SERVER. OOM KILL CAN OCCUR!" - ); - } - } - else { - infoprint -"Other user process except mysqld used less than 15% of total physical memory " - . percentage( $omem, $physical_memory ) . "% (" - . hr_bytes_rnd($omem) . " / " - . hr_bytes_rnd($physical_memory) . ")"; - } - - if ( $opt{'maxportallowed'} > 0 ) { - my @opened_ports = get_opened_ports; - infoprint "There is " - . scalar @opened_ports - . " listening port(s) on this server."; - if ( scalar(@opened_ports) > $opt{'maxportallowed'} ) { - badprint "There are too many listening ports: " - . scalar(@opened_ports) - . " opened > " - . $opt{'maxportallowed'} - . "allowed."; - push( @generalrec, -"Consider dedicating a server for your database installation with fewer services running on it!" - ); - } - else { - goodprint "There are less than " - . $opt{'maxportallowed'} - . " opened ports on this server."; - } - } - - foreach my $banport (@banned_ports) { - if ( is_open_port($banport) ) { - badprint "Banned port: $banport is opened.."; - push( @generalrec, -"Port $banport is opened. Consider stopping the program over this port." - ); - } - else { - goodprint "$banport is not opened."; - } - } - - subheaderprint "Filesystem Linux Recommendations"; - if ($is_win) { - get_fs_info_win; - } - else { - get_fs_info; - if ( !is_docker() && $opt{'container'} eq '' ) { - subheaderprint "Kernel Information Recommendations"; - get_kernel_info; - } - } -} - -# --------------------------------------------------------------------------- -# SSL/TLS Security Recommendations -# --------------------------------------------------------------------------- -sub ssl_tls_recommendations { - subheaderprint "SSL/TLS Security Recommendations"; - -# Check current session encryption -# Ssl_cipher session status variable tells us if the current connection is encrypted. - my $session_ssl = select_one("SHOW SESSION STATUS LIKE 'Ssl_cipher'"); - if ( $session_ssl =~ /Ssl_cipher\s+(.*)/ ) { - my $cipher = $1; - $cipher =~ s/^\s+|\s+$//g; - if ( $cipher eq "" || $cipher eq "NULL" || $cipher eq "0" ) { - badprint "Current connection is NOT encrypted!"; - push_recommendation( 'Security', -"Current connection is NOT encrypted! Consider using SSL for all connections." - ); - } - else { - goodprint "Current connection is encrypted ($cipher)"; - } - } - - # Global SSL check - if ( defined( $myvar{'have_ssl'} ) ) { - if ( $myvar{'have_ssl'} eq 'DISABLED' ) { - badprint "SSL is DISABLED on the server."; - push_recommendation( 'Security', - "Enable SSL support on the server (check have_ssl variable)." ); - } - elsif ( $myvar{'have_ssl'} eq 'YES' || $myvar{'have_ssl'} eq 'ON' ) { - goodprint "SSL support is enabled"; - } - } - - # require_secure_transport (MySQL 5.7+, MariaDB 10.5+) - if ( defined( $myvar{'require_secure_transport'} ) ) { - if ( $myvar{'require_secure_transport'} eq 'OFF' ) { - badprint "require_secure_transport is OFF"; - push_recommendation( 'Security', -"Enable require_secure_transport to force all connections to use SSL." - ); - } - else { - goodprint "require_secure_transport is ON"; - } - } - - # TLS Versions (MySQL 8.0+, MariaDB 10.4.6+) - if ( defined( $myvar{'tls_version'} ) ) { - my $tls_versions = $myvar{'tls_version'}; - if ( $tls_versions =~ /TLSv1\.0/i || $tls_versions =~ /TLSv1\.1/i ) { - badprint "Insecure TLS versions enabled: $tls_versions"; - push_recommendation( 'Security', - "Disable TLSv1.0 and TLSv1.1. Use only TLSv1.2 or TLSv1.3." ); - } - else { - goodprint "Only secure TLS versions enabled: $tls_versions"; - } - } - - # missing certificates - if ( ( $myvar{'ssl_cert'} || "" ) eq "" - && ( $myvar{'ssl_key'} || "" ) eq "" ) - { - badprint "No SSL certificates configured (ssl_cert/ssl_key are empty)"; - push_recommendation( 'Security', -"Configure SSL certificates (ssl_cert, ssl_key, ssl_ca) to enable encrypted connections." - ); - } -} - -sub security_recommendations { - subheaderprint "Security Recommendations"; - - infoprint "$myvar{'version_comment'} - $myvar{'version'}"; - - my $PASS_COLUMN_NAME = get_password_column_name(); - - if ( $PASS_COLUMN_NAME eq '' ) { - infoprint "Skipped due to none of known auth columns exists"; - return; - } - debugprint "Password column = $PASS_COLUMN_NAME"; - - # IS THERE A ROLE COLUMN - my $is_role_column = select_one -"select count(*) from information_schema.columns where TABLE_NAME='user' AND TABLE_SCHEMA='mysql' and COLUMN_NAME='IS_ROLE'"; - - my $extra_user_condition = ""; - $extra_user_condition = "IS_ROLE = 'N' AND" if $is_role_column > 0; - my @mysqlstatlist; - if ( $is_role_column > 0 ) { - @mysqlstatlist = select_array -"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE IS_ROLE='Y'"; - foreach my $line ( sort @mysqlstatlist ) { - chomp($line); - infoprint "User $line is User Role"; - } - } - else { - debugprint "No Role user detected"; - goodprint "No Role user detected"; - } - - # Looking for Anonymous users - @mysqlstatlist = select_array -"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE $extra_user_condition (TRIM(USER) = '' OR USER IS NULL)"; - - #debugprint Dumper \@mysqlstatlist; - - #exit 0; - if (@mysqlstatlist) { - push_recommendation( 'Security', - "Remove Anonymous User accounts: there are " - . scalar(@mysqlstatlist) - . " anonymous accounts." ); - foreach my $line ( sort @mysqlstatlist ) { - chomp($line); - badprint "User " - . $line - . " is an anonymous account. Remove with DROP USER " - . $line . ";"; - } - } - else { - goodprint "There are no anonymous accounts for any database users"; - } - - if ( $opt{skippassword} eq 1 ) { - infoprint "Skipped password checks due to --skippassword option"; - return; - } - - if ( mysql_version_le( 5, 1 ) ) { - badprint "No more password checks for MySQL version <=5.1"; - badprint "MySQL version <=5.1 is deprecated and end of support."; - return; - } - - # Looking for Empty Password - if ( mysql_version_ge( 10, 4 ) ) { - @mysqlstatlist = select_array -q{SELECT CONCAT(QUOTE(user), '@', QUOTE(host)) FROM mysql.global_priv WHERE - ( user != '' - AND JSON_CONTAINS(Priv, '"mysql_native_password"', '$.plugin') AND JSON_CONTAINS(Priv, '""', '$.authentication_string') - AND NOT JSON_CONTAINS(Priv, 'true', '$.account_locked') - )}; - } - else { - @mysqlstatlist = select_array -"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE ($PASS_COLUMN_NAME = '' OR $PASS_COLUMN_NAME IS NULL) - AND user != '' - /*!50501 AND plugin NOT IN ('auth_socket', 'unix_socket', 'win_socket', 'auth_pam_compat') */ - /*!80000 AND account_locked = 'N' AND password_expired = 'N' */"; - } - if (@mysqlstatlist) { - foreach my $line ( sort @mysqlstatlist ) { - chomp($line); - badprint "User '" . $line . "' has no password set."; - push_recommendation( 'Security', -"Set up a Secure Password for $line user: SET PASSWORD FOR $line = PASSWORD('secure_password');" - ); - } - } - else { - goodprint "All database users have passwords assigned"; - } - - if ( mysql_version_ge( 5, 7 ) ) { - my $valPlugin = select_one( -"select count(*) from information_schema.plugins where PLUGIN_NAME='validate_password' AND PLUGIN_STATUS='ACTIVE'" - ); - if ( $valPlugin >= 1 ) { - infoprint -"Bug #80860 MySQL 5.7: Avoid testing password when validate_password is activated"; - return; - } - } - - # Looking for User with user/ uppercase /capitalise user as password - if ( !mysql_version_ge(8) ) { - @mysqlstatlist = select_array -"SELECT CONCAT(QUOTE(user), '\@', QUOTE(host)) FROM mysql.user WHERE user != '' AND (CAST($PASS_COLUMN_NAME as Binary) = PASSWORD(user) OR CAST($PASS_COLUMN_NAME as Binary) = PASSWORD(UPPER(user)) OR CAST($PASS_COLUMN_NAME as Binary) = PASSWORD(CONCAT(UPPER(LEFT(User, 1)), SUBSTRING(User, 2, LENGTH(User)))))"; - if (@mysqlstatlist) { - foreach my $line ( sort @mysqlstatlist ) { - chomp($line); - badprint "User " . $line . " has user name as password."; - push( @generalrec, -"Set up a Secure Password for $line user: SET PASSWORD FOR $line = PASSWORD('secure_password');" - ); - } - } - } - - @mysqlstatlist = select_array - "SELECT CONCAT(QUOTE(user), '\@', host) FROM mysql.user WHERE HOST='%'"; - if ( scalar(@mysqlstatlist) > 0 ) { - if ( $opt{dumpdir} ne '' && $opt{dumpdir} ne '0' ) { - select_csv_file( - "$opt{dumpdir}/user_with_general_wildcard.csv", - "SELECT user, host FROM mysql.user WHERE HOST='%'" - ); - } - my $luser = 'user_name'; - if ( scalar(@mysqlstatlist) == 1 ) { - $luser = ( split /@/, $mysqlstatlist[0] )[0]; - } - foreach my $line ( sort @mysqlstatlist ) { - chomp($line); - badprint "User " . $line - . " does not specify hostname restrictions."; - } - push( @generalrec, - "Restrict Host for $luser\@'%' to $luser\@LimitedIPRangeOrLocalhost" - ); - push( @generalrec, - "RENAME USER $luser\@'%' TO " - . $luser - . "\@LimitedIPRangeOrLocalhost;" ); - } - - unless ( -f $basic_password_files ) { - badprint "There is no basic password file list!"; - return; - } - - my @passwords = get_basic_passwords $basic_password_files; - infoprint "There are " - . scalar(@passwords) - . " basic passwords in the list."; - my $nbins = 0; - my $passreq; - if (@passwords) { - my $nbInterPass = 0; - my $skip_dict_check = 0; - - # Behavioral check for socket authentication or password bypass (issue #875) - # Testing if any of these passwords work (including random tokens) - my $target_user = $opt{user} || 'root'; - foreach my $p ( "true", "false", - "RA-ND-OM-P-ASS-W-ORD-" . int( rand(100000) ) ) - { - my $check_cmd = -"$mysqlcmd $mysqllogin -u $target_user -p'$p' -Nrs -e 'select \"mysqld is alive\";' 2>$devnull"; - my $alive_res = execute_system_command($check_cmd); - if ( $alive_res =~ /mysqld is alive/ ) { - infoprint -"Authentication plugin allows access without a valid password for user '$target_user'. Skipping dictionary check."; - $skip_dict_check = 1; - last; - } - } - - unless ($skip_dict_check) { - foreach my $pass (@passwords) { - $nbInterPass++; - last if $nbInterPass > $opt{'max-password-checks'}; - if ( $nbInterPass % 100 == 0 ) { - select_one("FLUSH HOSTS;"); - } - - $pass =~ s/\s//g; - $pass =~ s/\'/\\\'/g; - chomp($pass); - - if ( !mysql_version_ge(8) ) { - - # Looking for User with user/ uppercase /capitalise weak password - @mysqlstatlist = - select_array -"SELECT CONCAT(user, '\@', host) FROM mysql.user WHERE $PASS_COLUMN_NAME = PASSWORD('" - . $pass - . "') OR $PASS_COLUMN_NAME = PASSWORD(UPPER('" - . $pass - . "')) OR $PASS_COLUMN_NAME = PASSWORD(CONCAT(UPPER(LEFT('" - . $pass - . "', 1)), SUBSTRING('" - . $pass - . "', 2, LENGTH('" - . $pass . "'))))"; - debugprint "There are " - . scalar(@mysqlstatlist) - . " items."; - if (@mysqlstatlist) { - foreach my $line (@mysqlstatlist) { - chomp($line); - badprint "User '" . $line - . "' is using weak password: $pass in a lower, upper or capitalize derivative version."; - - push( @generalrec, -"Set up a Secure Password for $line user: SET PASSWORD FOR '" - . ( split /@/, $line )[0] . "'\@'" - . ( split /@/, $line )[1] - . "' = PASSWORD('secure_password');" ); - $nbins++; - } - } - } - else { - # New way to check basic password for MySQL 8.0+ - my $target_user = $opt{user} || 'root'; - my @variants = ( $pass, uc($pass), ucfirst($pass) ); - foreach my $v (@variants) { - my $check_login = "$mysqllogin -u $target_user -p'$v'"; - my $alive_res = execute_system_command( -"$mysqlcmd -Nrs -e 'select \"mysqld is alive\";' $check_login 2>$devnull" - ); - if ( $alive_res =~ /mysqld is alive/ ) { - badprint - "User '$target_user' is using weak password: $v"; - push( @generalrec, -"Set up a Secure Password for $target_user user." - ); - $nbins++; - last; - } - } - } - debugprint "$nbInterPass / " . scalar(@passwords) - if ( $nbInterPass % 1000 == 0 ); - } - } - } - if ( $nbins > 0 ) { - push( @generalrec, - $nbins - . " user(s) used basic or weak password from basic dictionary." ); - } -} - -sub get_replication_status { - subheaderprint "Replication Metrics"; - infoprint "Galera Synchronous replication: " . $myvar{'have_galera'}; - if ( scalar( keys %myslaves ) == 0 ) { - infoprint "No replication slave(s) for this server."; - } - else { - infoprint "This server is acting as master for " - . scalar( keys %myslaves ) - . " server(s)."; - } - infoprint "Binlog format: " . $myvar{'binlog_format'}; - infoprint "XA support enabled: " . $myvar{'innodb_support_xa'}; - - infoprint "Semi synchronous replication Master: " - . ( - ( - defined( $myvar{'rpl_semi_sync_master_enabled'} ) - or defined( $myvar{'rpl_semi_sync_source_enabled'} ) - ) - ? ( $myvar{'rpl_semi_sync_master_enabled'} - // $myvar{'rpl_semi_sync_source_enabled'} ) - : 'Not Activated' - ); - infoprint "Semi synchronous replication Slave: " - . ( - ( - defined( $myvar{'rpl_semi_sync_slave_enabled'} ) - or defined( $myvar{'rpl_semi_sync_replica_enabled'} ) - ) - ? ( $myvar{'rpl_semi_sync_slave_enabled'} - // $myvar{'rpl_semi_sync_replica_enabled'} ) - : 'Not Activated' - ); - if ( scalar( keys %myrepl ) == 0 and scalar( keys %myslaves ) == 0 ) { - infoprint "This is a standalone server"; - return; - } - if ( scalar( keys %myrepl ) == 0 ) { - infoprint - "No replication setup for this server or replication not started."; - return; - } - - $result{'Replication'}{'status'} = \%myrepl; - my ($io_running) = $myrepl{'Slave_IO_Running'} - // $myrepl{'Replica_IO_Running'}; - debugprint "IO RUNNING: $io_running "; - my ($sql_running) = $myrepl{'Slave_SQL_Running'} - // $myrepl{'Replica_SQL_Running'}; - debugprint "SQL RUNNING: $sql_running "; - - my ($seconds_behind_master) = $myrepl{'Seconds_Behind_Master'} - // $myrepl{'Seconds_Behind_Source'}; - $seconds_behind_master = 1000000 unless defined($seconds_behind_master); - debugprint "SECONDS : $seconds_behind_master "; - - if ( defined($io_running) - and ( $io_running !~ /yes/i or $sql_running !~ /yes/i ) ) - { - badprint - "This replication slave is not running but seems to be configured."; - } - if ( defined($io_running) - && $io_running =~ /yes/i - && $sql_running =~ /yes/i ) - { - if ( $myvar{'read_only'} eq 'OFF' ) { - badprint -"This replication slave is running with the read_only option disabled."; - } - else { - goodprint -"This replication slave is running with the read_only option enabled."; - } - if ( $seconds_behind_master > 0 ) { - badprint -"This replication slave is lagging and slave has $seconds_behind_master second(s) behind master host."; - } - else { - goodprint "This replication slave is up to date with master."; - } - } - - # Parallel replication checks (MariaDB specific) - if ( ( $myvar{'version'} =~ /MariaDB/i ) - or ( $myvar{'version_comment'} =~ /MariaDB/i ) ) - { - my $parallel_threads = $myvar{'slave_parallel_threads'} - // $myvar{'replica_parallel_threads'} // 0; - if ( $parallel_threads > 1 ) { - goodprint - "Parallel replication is enabled with $parallel_threads threads."; - - # Check parallel mode for MariaDB 10.5+ - if ( mysql_version_ge( 10, 5 ) ) { - my $parallel_mode = $myvar{'slave_parallel_mode'} - // $myvar{'replica_parallel_mode'} // ''; - if ( $parallel_mode eq 'optimistic' ) { - goodprint - "Parallel replication mode is set to 'optimistic'."; - } - else { - badprint -"Parallel replication mode is not 'optimistic' (recommended for MariaDB 10.5+)."; - push( @adjvars, "replica_parallel_mode=optimistic" ); - } - } - infoprint -"Ensure binlog_format=ROW is set on the master for parallel replication to work effectively."; - } - else { - badprint "Parallel replication is disabled."; - push( @adjvars, - "replica_parallel_threads (set to number of vCPUs)" ); - } - } -} - -# https://endoflife.date/mysql -# https://endoflife.date/mariadb -sub validate_mysql_version { - ( $mysqlvermajor, $mysqlverminor, $mysqlvermicro ) = - $myvar{'version'} =~ /^(\d+)(?:\.(\d+)|)(?:\.(\d+)|)/; - $mysqlverminor ||= 0; - $mysqlvermicro ||= 0; - - prettyprint " "; - - if ( mysql_version_eq( 8, 0 ) - or mysql_version_eq( 8, 4 ) - or mysql_version_eq( 9, 5 ) - or mysql_version_eq( 10, 6 ) - or mysql_version_eq( 10, 11 ) - or mysql_version_eq( 11, 4 ) - or mysql_version_eq( 11, 8 ) ) - { - goodprint "Currently running supported MySQL/MariaDB version " - . $myvar{'version'} . "(LTS)"; - return; - } - else { - badprint "Your MySQL version " - . $myvar{'version'} - . " is EOL software. Upgrade soon!"; - push( @generalrec, - "You are using an unsupported version for production environments" - ); - push( @generalrec, - "Upgrade as soon as possible to a supported version !" ); - - } -} - -# Checks if MySQL version is equal to (major, minor, micro) -sub mysql_version_eq { - my ( $maj, $min, $mic ) = @_; - my ( $mysqlvermajor, $mysqlverminor, $mysqlvermicro ) = - $myvar{'version'} =~ /^(\d+)(?:\.(\d+)|)(?:\.(\d+)|)/; - - return int($mysqlvermajor) == int($maj) - if ( !defined($min) && !defined($mic) ); - return int($mysqlvermajor) == int($maj) && int($mysqlverminor) == int($min) - if ( !defined($mic) ); - return ( int($mysqlvermajor) == int($maj) - && int($mysqlverminor) == int($min) - && int($mysqlvermicro) == int($mic) ); -} - -# Checks if MySQL version is greater than equal to (major, minor, micro) -sub mysql_version_ge { - my ( $maj, $min, $mic ) = @_; - $min ||= 0; - $mic ||= 0; - my ( $mysqlvermajor, $mysqlverminor, $mysqlvermicro ) = - $myvar{'version'} =~ /^(\d+)(?:\.(\d+)|)(?:\.(\d+)|)/; - - return - int($mysqlvermajor) > int($maj) - || ( int($mysqlvermajor) == int($maj) && int($mysqlverminor) > int($min) ) - || ( int($mysqlvermajor) == int($maj) - && int($mysqlverminor) == int($min) - && int($mysqlvermicro) >= int($mic) ); -} - -# Checks if MySQL version is lower than equal to (major, minor, micro) -sub mysql_version_le { - my ( $maj, $min, $mic ) = @_; - $min ||= 0; - $mic ||= 0; - my ( $mysqlvermajor, $mysqlverminor, $mysqlvermicro ) = - $myvar{'version'} =~ /^(\d+)(?:\.(\d+)|)(?:\.(\d+)|)/; - - #infoprint "MySQL version: $mysqlvermajor.$mysqlverminor.$mysqlvermicro"; - - return - int($mysqlvermajor) < int($maj) - || ( int($mysqlvermajor) == int($maj) && int($mysqlverminor) < int($min) ) - || ( int($mysqlvermajor) == int($maj) - && int($mysqlverminor) == int($min) - && int($mysqlvermicro) <= int($mic) ); -} - -# Checks for 32-bit boxes with more than 2GB of RAM -my ($arch); - -sub check_architecture { - my $prefix = get_transport_prefix(); - if ( is_remote eq 1 || $prefix ne '' ) { - infoprint "Skipping architecture check on remote host"; - infoprint "Using default $opt{defaultarch} bits as target architecture"; - $arch = $opt{defaultarch}; - return; - } - elsif ( $is_win ) { - if ( execute_system_command('wmic os get osarchitecture') =~ /64/ ) { - goodprint "Operating on 64-bit architecture"; - $arch = 64; - } - } - else { - my ( $sysname, $nodename, $release, $version, $machine ); - if ( $prefix eq '' ) { - ( $sysname, $nodename, $release, $version, $machine ) = POSIX::uname(); - } - else { - $sysname = execute_system_command('uname'); - $machine = execute_system_command('uname -m'); - } - - if ( $sysname =~ /SunOS/ ) { - if ( execute_system_command('isainfo -b') =~ /64/ ) { - $arch = 64; - goodprint "Operating on 64-bit architecture"; - } - } - elsif ( $sysname =~ /AIX/ ) { - if ( execute_system_command('bootinfo -K') =~ /64/ ) { - $arch = 64; - goodprint "Operating on 64-bit architecture"; - } - } - elsif ( $sysname =~ /NetBSD|OpenBSD/ ) { - if ( execute_system_command('sysctl -b hw.machine') =~ /64/ ) { - $arch = 64; - goodprint "Operating on 64-bit architecture"; - } - } - elsif ( $sysname =~ /FreeBSD/ ) { - if ( execute_system_command('sysctl -b hw.machine_arch') =~ /64/ ) { - $arch = 64; - goodprint "Operating on 64-bit architecture"; - } - } - elsif ( $sysname =~ /Darwin/ && $machine =~ /Power Macintosh/ ) { - # Darwin box.local 9.8.0 Darwin Kernel Version 9.8.0: Wed Jul 15 16:57:01 PDT 2009; root:xnu1228.15.4~1/RELEASE_PPC Power Macintosh - $arch = 64; - goodprint "Operating on 64-bit architecture"; - } - elsif ( $sysname =~ /Darwin/ && $machine =~ /x86_64/ ) { - # Darwin gibas.local 12.6.0 Darwin Kernel Version 12.3.0: Sun Jan 6 22:37:10 PST 2013; root:xnu-2050.22.13~1/RELEASE_X86_64 x86_64 - $arch = 64; - goodprint "Operating on 64-bit architecture"; - } - elsif ( $machine =~ /(64|s390x|x86_64|amd64)/ ) { - $arch = 64; - goodprint "Operating on 64-bit architecture"; - } - } -} - else { - $arch = 32; - if ( $physical_memory > 2147483648 ) { - badprint -"Switch to 64-bit OS - MySQL cannot currently use all of your RAM"; - } - else { - goodprint "Operating on 32-bit architecture with less than 2GB RAM"; - } - } - $result{'OS'}{'Architecture'} = "$arch bits"; - -} - -# Start up a ton of storage engine counts/statistics -my ( %enginestats, %enginecount, $fragtables ); - -sub check_storage_engines { - subheaderprint "Storage Engine Statistics"; - if ( $opt{skipsize} eq 1 ) { - infoprint "Skipped due to --skipsize option"; - return; - } - - my $engines; - if ( mysql_version_ge( 5, 5 ) ) { - my @engineresults = select_array -"SELECT ENGINE,SUPPORT FROM information_schema.ENGINES ORDER BY ENGINE ASC"; - foreach my $line (@engineresults) { - my ( $engine, $engineenabled ); - ( $engine, $engineenabled ) = $line =~ /([a-zA-Z_]*)\s+([a-zA-Z]+)/; - $result{'Engine'}{$engine}{'Enabled'} = $engineenabled; - $engines .= - ( $engineenabled eq "YES" || $engineenabled eq "DEFAULT" ) - ? greenwrap "+" . $engine . " " - : redwrap "-" . $engine . " "; - } - } - elsif ( mysql_version_ge( 5, 1, 5 ) ) { - my @engineresults = select_array -"SELECT ENGINE, SUPPORT FROM information_schema.ENGINES WHERE ENGINE NOT IN ('MyISAM', 'MERGE', 'MEMORY') ORDER BY ENGINE"; - foreach my $line (@engineresults) { - my ( $engine, $engineenabled ); - ( $engine, $engineenabled ) = $line =~ /([a-zA-Z_]*)\s+([a-zA-Z]+)/; - $result{'Engine'}{$engine}{'Enabled'} = $engineenabled; - $engines .= - ( $engineenabled eq "YES" || $engineenabled eq "DEFAULT" ) - ? greenwrap "+" . $engine . " " - : redwrap "-" . $engine . " "; - } - } - else { - $engines .= - ( defined $myvar{'have_archive'} && $myvar{'have_archive'} eq "YES" ) - ? greenwrap "+Archive " - : redwrap "-Archive "; - $engines .= - ( defined $myvar{'have_bdb'} && $myvar{'have_bdb'} eq "YES" ) - ? greenwrap "+BDB " - : redwrap "-BDB "; - $engines .= - ( defined $myvar{'have_federated_engine'} - && $myvar{'have_federated_engine'} eq "YES" ) - ? greenwrap "+Federated " - : redwrap "-Federated "; - $engines .= - ( defined $myvar{'have_innodb'} && $myvar{'have_innodb'} eq "YES" ) - ? greenwrap "+InnoDB " - : redwrap "-InnoDB "; - $engines .= - ( defined $myvar{'have_isam'} && $myvar{'have_isam'} eq "YES" ) - ? greenwrap "+ISAM " - : redwrap "-ISAM "; - $engines .= - ( defined $myvar{'have_ndbcluster'} - && $myvar{'have_ndbcluster'} eq "YES" ) - ? greenwrap "+NDBCluster " - : redwrap "-NDBCluster "; - } - - my @dblist = grep { $_ ne 'lost+found' } select_array "SHOW DATABASES"; - - $result{'Databases'}{'List'} = [@dblist]; - infoprint "Status: $engines"; - if ( mysql_version_ge( 5, 1, 5 ) ) { - -# MySQL 5+ servers can have table sizes calculated quickly from information schema - my @templist = select_array -"SELECT ENGINE, SUM(DATA_LENGTH+INDEX_LENGTH), COUNT(ENGINE), SUM(DATA_LENGTH), SUM(INDEX_LENGTH) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql') AND ENGINE IS NOT NULL GROUP BY ENGINE ORDER BY ENGINE ASC;"; - - my ( $engine, $size, $count, $dsize, $isize ); - foreach my $line (@templist) { - ( $engine, $size, $count, $dsize, $isize ) = - $line =~ /([a-zA-Z_]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/; - debugprint "Engine Found: $engine"; - next unless ( defined($engine) or trim($engine) eq '' ); - $size = 0 unless ( defined($size) or trim($engine) eq '' ); - $isize = 0 unless ( defined($isize) or trim($engine) eq '' ); - $dsize = 0 unless ( defined($dsize) or trim($engine) eq '' ); - $count = 0 unless ( defined($count) or trim($engine) eq '' ); - $enginestats{$engine} = $size; - $enginecount{$engine} = $count; - $result{'Engine'}{$engine}{'Table Number'} = $count; - $result{'Engine'}{$engine}{'Total Size'} = $size; - $result{'Engine'}{$engine}{'Data Size'} = $dsize; - $result{'Engine'}{$engine}{'Index Size'} = $isize; - } - - #print Dumper( \%enginestats ) if $opt{debug}; - my $not_innodb = ''; - if ( not defined $result{'Variables'}{'innodb_file_per_table'} ) { - $not_innodb = "AND NOT ENGINE='InnoDB'"; - } - elsif ( $result{'Variables'}{'innodb_file_per_table'} eq 'OFF' ) { - $not_innodb = "AND NOT ENGINE='InnoDB'"; - } - $result{'Tables'}{'Fragmented tables'} = - [ select_array -"SELECT TABLE_SCHEMA, TABLE_NAME, ENGINE, CAST(DATA_FREE AS SIGNED) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql') AND DATA_LENGTH/1024/1024>100 AND cast(DATA_FREE as signed)*100/(DATA_LENGTH+INDEX_LENGTH+cast(DATA_FREE as signed)) > 10 AND NOT ENGINE='MEMORY' $not_innodb" - ]; - $fragtables = scalar @{ $result{'Tables'}{'Fragmented tables'} }; - if ( $opt{dumpdir} ne '' && $opt{dumpdir} ne '0' ) { - select_csv_file( "$opt{dumpdir}/fragmented_tables.csv", -"SELECT TABLE_SCHEMA, TABLE_NAME, ENGINE, CAST(DATA_FREE AS SIGNED) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql') AND DATA_LENGTH/1024/1024>100 AND cast(DATA_FREE as signed)*100/(DATA_LENGTH+INDEX_LENGTH+cast(DATA_FREE as signed)) > 10 AND NOT ENGINE='MEMORY' $not_innodb" - ); - } - - } - else { - - # MySQL < 5 servers take a lot of work to get table sizes - my @tblist; - -# Now we build a database list, and loop through it to get storage engine stats for tables - foreach my $db (@dblist) { - chomp($db); - if ( $db eq "information_schema" - or $db eq "performance_schema" - or $db eq "mysql" - or $db eq "lost+found" ) - { - next; - } - my @ixs = ( 1, 6, 9 ); - if ( !mysql_version_ge( 4, 1 ) ) { - - # MySQL 3.23/4.0 keeps Data_Length in the 5th (0-based) column - @ixs = ( 1, 5, 8 ); - } - my $cmd = "SHOW TABLE STATUS FROM \\\`$db\\\`"; - if ($is_win) { - $cmd = "SHOW TABLE STATUS FROM \`$db\`"; - } - push( @tblist, map { [ (split)[@ixs] ] } select_array $cmd ); - } - - # Parse through the table list to generate storage engine counts/statistics - $fragtables = 0; - foreach my $tbl (@tblist) { - - #debugprint "Data dump " . Dumper(@$tbl) if $opt{debug}; - my ( $engine, $size, $datafree ) = @$tbl; - next if $engine eq 'NULL' or not defined($engine); - $size = 0 if $size eq 'NULL' or not defined($size); - $datafree = 0 if $datafree eq 'NULL' or not defined($datafree); - if ( defined $enginestats{$engine} ) { - $enginestats{$engine} += $size; - $enginecount{$engine} += 1; - } - else { - $enginestats{$engine} = $size; - $enginecount{$engine} = 1; - } - if ( $datafree > 0 ) { - $fragtables++; - } - } - } - foreach my $engine ( sort keys %enginestats ) { - my $size = $enginestats{$engine}; - infoprint "Data in $engine tables: " - . hr_bytes($size) - . " (Tables: " - . $enginecount{$engine} . ")" . ""; - } - - # If the storage engine isn't being used, recommend it to be disabled - if ( !defined $enginestats{'InnoDB'} - && defined $myvar{'have_innodb'} - && $myvar{'have_innodb'} eq "YES" ) - { - badprint "InnoDB is enabled, but isn't being used"; - push( @generalrec, - "Add skip-innodb to MySQL configuration to disable InnoDB" ); - } - if ( !defined $enginestats{'BerkeleyDB'} - && defined $myvar{'have_bdb'} - && $myvar{'have_bdb'} eq "YES" ) - { - badprint "BDB is enabled, but isn't being used"; - push( @generalrec, - "Add skip-bdb to MySQL configuration to disable BDB" ); - } - if ( !defined $enginestats{'ISAM'} - && defined $myvar{'have_isam'} - && $myvar{'have_isam'} eq "YES" ) - { - badprint "MyISAM is enabled, but isn't being used"; - push( @generalrec, -"Add skip-isam to MySQL configuration to disable MyISAM (MySQL > 4.1.0)" - ); - } - - # Fragmented tables - if ( $fragtables > 0 ) { - badprint "Total fragmented tables: $fragtables"; - push @generalrec, -'Run ALTER TABLE ... FORCE or OPTIMIZE TABLE to defragment tables for better performance'; - my $total_free = 0; - my $fragmented_tables_csv = "schema,table,free_space_mb,sql\n"; - foreach my $table_line ( @{ $result{'Tables'}{'Fragmented tables'} } ) { - my ( $table_schema, $table_name, $engine, $data_free ) = - split /\t/msx, $table_line; - $data_free = $data_free / 1024 / 1024; - $total_free += $data_free; - my $generalrec; - my $fragmented_tables_sql; - if ( $engine eq 'InnoDB' ) { - $fragmented_tables_sql = - "ALTER TABLE `$table_schema`.`$table_name` FORCE;"; - $generalrec = " $fragmented_tables_sql"; - } - else { - $fragmented_tables_sql = - "OPTIMIZE TABLE `$table_schema`.`$table_name`;"; - $generalrec = " $fragmented_tables_sql"; - } - $fragmented_tables_csv .= -"$table_schema,$table_name,$data_free,\"$fragmented_tables_sql\"\n"; - $generalrec .= " -- can free $data_free MiB"; - push @generalrec, $generalrec; - } - dump_into_file( 'fragmented_tables.csv', $fragmented_tables_csv ); - push @generalrec, -"Consider defragmenting $fragtables tables to free up $total_free MiB"; - } - else { - goodprint "Total fragmented tables: $fragtables"; - } - - # Auto increments - my %tblist; - - # Find the maximum integer - my $maxint = select_one "SELECT ~0"; - $result{'MaxInt'} = $maxint; - -# Now we use a database list, and loop through it to get storage engine stats for tables - foreach my $db (@dblist) { - chomp($db); - - if ( !$tblist{$db} ) { - $tblist{$db} = (); - } - - if ( $db eq "information_schema" ) { next; } - my @ia = ( 0, 10 ); - if ( !mysql_version_ge( 4, 1 ) ) { - - # MySQL 3.23/4.0 keeps Data_Length in the 5th (0-based) column - @ia = ( 0, 9 ); - } - my $cmd = "SHOW TABLE STATUS FROM \\\`$db\\\`"; - if ($is_win) { - $cmd = "SHOW TABLE STATUS FROM \`$db\`"; - } - push( @{ $tblist{$db} }, map { [ (split)[@ia] ] } select_array $cmd ); - } - - my @dbnames = keys %tblist; - - foreach my $db (@dbnames) { - foreach my $tbl ( @{ $tblist{$db} } ) { - my ( $name, $autoincrement ) = @$tbl; - - if ( $autoincrement =~ /^\d+?$/ ) { - my $percent = percentage( $autoincrement, $maxint ); - $result{'PctAutoIncrement'}{"$db.$name"} = $percent; - if ( $percent >= 75 ) { - badprint -"Table '$db.$name' has an autoincrement value near max capacity ($percent%)"; - } - } - } - } -} - -sub dump_into_file { - my $file = shift; - my $content = shift; - if ( -d "$opt{dumpdir}" && $opt{dumpdir} ne '0' ) { - $file = "$opt{dumpdir}/$file"; - open( FILE, ">$file" ) or die "Can't open $file: $!"; - print FILE $content; - close FILE; - infoprint "Data saved to $file"; - } -} - -sub calculations { - if ( $mystat{'Questions'} < 1 ) { - badprint "Your server has not answered any queries: cannot continue..."; - exit 2; - } - - #infoprint "====>>>> MySQL version: $myvar{'version'}"; - $myvar{'version'} =~ s/(.+)-.*?$/$1/; - - #infoprint "====>>>> MySQL version updated: $myvar{'version'}"; - # Per-thread memory - $mycalc{'per_thread_buffers'} = 0; - $mycalc{'per_thread_buffers'} += $myvar{'read_buffer_size'} - if is_int( $myvar{'read_buffer_size'} ); - $mycalc{'per_thread_buffers'} += $myvar{'read_rnd_buffer_size'} - if is_int( $myvar{'read_rnd_buffer_size'} ); - $mycalc{'per_thread_buffers'} += $myvar{'sort_buffer_size'} - if is_int( $myvar{'sort_buffer_size'} ); - $mycalc{'per_thread_buffers'} += $myvar{'thread_stack'} - if is_int( $myvar{'thread_stack'} ); - $mycalc{'per_thread_buffers'} += $myvar{'join_buffer_size'} - if is_int( $myvar{'join_buffer_size'} ); - $mycalc{'per_thread_buffers'} += $myvar{'binlog_cache_size'} - if is_int( $myvar{'binlog_cache_size'} ); - debugprint "per_thread_buffers: $mycalc{'per_thread_buffers'} (" - . human_size( $mycalc{'per_thread_buffers'} ) . " )"; - -# Error max_allowed_packet is not included in thread buffers size -#$mycalc{'per_thread_buffers'} += $myvar{'max_allowed_packet'} if is_int($myvar{'max_allowed_packet'}); - - # Total per-thread memory - $mycalc{'total_per_thread_buffers'} = - $mycalc{'per_thread_buffers'} * $myvar{'max_connections'}; - - # Max total per-thread memory reached - $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; - $mycalc{'server_buffers'} += - ( defined $myvar{'aria_pagecache_buffer_size'} ) - ? $myvar{'aria_pagecache_buffer_size'} - : 0; - -# Global memory -# Max used memory is memory used by MySQL based on Max_used_connections -# This is the max memory used theoretically calculated with the max concurrent connection number reached by mysql - $mycalc{'max_used_memory'} = - $mycalc{'server_buffers'} + - $mycalc{"max_total_per_thread_buffers"} + - get_pf_memory(); - - # + get_gcache_memory(); - $mycalc{'pct_max_used_memory'} = - percentage( $mycalc{'max_used_memory'}, $physical_memory ); - -# Total possible memory is memory needed by MySQL based on max_connections -# This is the max memory MySQL can theoretically used if all connections allowed has opened by mysql - $mycalc{'max_peak_memory'} = - $mycalc{'server_buffers'} + - $mycalc{'total_per_thread_buffers'} + - get_pf_memory(); - - # + get_gcache_memory(); - $mycalc{'pct_max_physical_memory'} = - percentage( $mycalc{'max_peak_memory'}, $physical_memory ); - - debugprint "Max Used Memory: " - . hr_bytes( $mycalc{'max_used_memory'} ) . ""; - debugprint "Max Used Percentage RAM: " - . $mycalc{'pct_max_used_memory'} . "%"; - - debugprint "Max Peak Memory: " - . hr_bytes( $mycalc{'max_peak_memory'} ) . ""; - debugprint "Max Peak Percentage RAM: " - . $mycalc{'pct_max_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'}; - - # Aborted Connections - $mycalc{'pct_connections_aborted'} = - percentage( $mystat{'Aborted_connects'}, $mystat{'Connections'} ); - debugprint "Aborted_connects: " . $mystat{'Aborted_connects'} . ""; - debugprint "Connections: " . $mystat{'Connections'} . ""; - debugprint "pct_connections_aborted: " - . $mycalc{'pct_connections_aborted'} . ""; - - # Key buffers - if ( mysql_version_ge( 4, 1 ) && $myvar{'key_buffer_size'} > 0 ) { - $mycalc{'pct_key_buffer_used'} = sprintf( - "%.1f", - ( - 1 - ( - ( - $mystat{'Key_blocks_unused'} * - $myvar{'key_cache_block_size'} - ) / $myvar{'key_buffer_size'} - ) - ) * 100 - ); - } - else { - $mycalc{'pct_key_buffer_used'} = 0; - } - - if ( $mystat{'Key_read_requests'} > 0 ) { - $mycalc{'pct_keys_from_mem'} = sprintf( - "%.1f", - ( - 100 - ( - ( $mystat{'Key_reads'} / $mystat{'Key_read_requests'} ) * - 100 - ) - ) - ); - } - else { - $mycalc{'pct_keys_from_mem'} = 0; - } - if ( defined $mystat{'Aria_pagecache_read_requests'} - && $mystat{'Aria_pagecache_read_requests'} > 0 ) - { - $mycalc{'pct_aria_keys_from_mem'} = sprintf( - "%.1f", - ( - 100 - ( - ( - $mystat{'Aria_pagecache_reads'} / - $mystat{'Aria_pagecache_read_requests'} - ) * 100 - ) - ) - ); - } - else { - $mycalc{'pct_aria_keys_from_mem'} = 0; - } - - if ( $mystat{'Key_write_requests'} > 0 ) { - $mycalc{'pct_wkeys_from_mem'} = sprintf( "%.1f", - ( ( $mystat{'Key_writes'} / $mystat{'Key_write_requests'} ) * 100 ) - ); - } - else { - $mycalc{'pct_wkeys_from_mem'} = 0; - } - - if ( $doremote eq 0 and !mysql_version_ge(5) ) { - if ($is_win) { - my $size = 0; - my @allfiles = - execute_system_command("dir /-c /s $myvar{'datadir'}"); - foreach ( - map { /^\s*\d+\/\S+\s+\S+\s+(A|P)M\s+(\d+)\s/i; $2 } - grep { /\.MYI$/i } @allfiles - ) - { - $size += $_; - } - $mycalc{'total_myisam_indexes'} = $size; - $size = 0; - foreach ( - map { /^\s*\d+\/\S+\s+\S+\s+(A|P)M\s+(\d+)\s/i; $2 } - grep { /\.MAI$/i } @allfiles - ) - { - $size += $_; - } - $mycalc{'total_aria_indexes'} = $size; - } - else { - my $size = 0; - $size += (split)[0] - for execute_system_command( -"find '$myvar{'datadir'}' -name '*.MYI' -print0 2>&1 | xargs $xargsflags -0 du -L $duflags 2>&1" - ); - $mycalc{'total_myisam_indexes'} = $size; - $size = 0 + (split)[0] - for execute_system_command( -"find '$myvar{'datadir'}' -name '*.MAI' -print0 2>&1 | xargs $xargsflags -0 du -L $duflags 2>&1" - ); - $mycalc{'total_aria_indexes'} = $size; - } - } - elsif ( mysql_version_ge(5) ) { - $mycalc{'total_myisam_indexes'} = select_one -"SELECT IFNULL(SUM(INDEX_LENGTH), 0) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema') AND ENGINE = 'MyISAM';"; - $mycalc{'total_aria_indexes'} = select_one -"SELECT IFNULL(SUM(INDEX_LENGTH), 0) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema') AND ENGINE = 'Aria';"; - } - if ( defined $mycalc{'total_myisam_indexes'} ) { - chomp( $mycalc{'total_myisam_indexes'} ); - } - if ( defined $mycalc{'total_aria_indexes'} ) { - chomp( $mycalc{'total_aria_indexes'} ); - } - - # Query cache - if ( mysql_version_ge(8) and mysql_version_le(10) ) { - $mycalc{'query_cache_efficiency'} = 0; - } - elsif ( mysql_version_ge(4) ) { - $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'}; - $mycalc{'joins_without_indexes_per_day'} = - int( $mycalc{'joins_without_indexes'} / ( $mystat{'Uptime'} / 86400 ) ); - - # 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 ) { - if ( not defined( $mystat{'Table_open_cache_hits'} ) ) { - $mycalc{'table_cache_hit_rate'} = - int( $mystat{'Open_tables'} * 100 / $mystat{'Opened_tables'} ); - } - else { - $mycalc{'table_cache_hit_rate'} = int( - $mystat{'Table_open_cache_hits'} * 100 / ( - $mystat{'Table_open_cache_hits'} + - $mystat{'Table_open_cache_misses'} - ) - ); - } - } - else { - $mycalc{'table_cache_hit_rate'} = 100; - } - - # Open files - if ( $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 ) ); - - # Other - if ( $mystat{'Connections'} > 0 ) { - $mycalc{'pct_aborted_connections'} = - int( ( $mystat{'Aborted_connects'} / $mystat{'Connections'} ) * 100 ); - } - if ( $mystat{'Questions'} > 0 ) { - $mycalc{'total_reads'} = $mystat{'Com_select'}; - $mycalc{'total_writes'} = - $mystat{'Com_delete'} + - $mystat{'Com_insert'} + - $mystat{'Com_update'} + - $mystat{'Com_replace'}; - if ( $mycalc{'total_reads'} == 0 ) { - $mycalc{'pct_reads'} = 0; - $mycalc{'pct_writes'} = 100; - } - else { - $mycalc{'pct_reads'} = int( - ( - $mycalc{'total_reads'} / - ( $mycalc{'total_reads'} + $mycalc{'total_writes'} ) - ) * 100 - ); - $mycalc{'pct_writes'} = 100 - $mycalc{'pct_reads'}; - } - } - - # InnoDB - $myvar{'innodb_log_files_in_group'} = 1 - unless defined( $myvar{'innodb_log_files_in_group'} ); - $myvar{'innodb_log_files_in_group'} = 1 - if $myvar{'innodb_log_files_in_group'} == 0; - - $myvar{"innodb_buffer_pool_instances"} = 1 - unless defined( $myvar{'innodb_buffer_pool_instances'} ); - if ( $myvar{'have_innodb'} eq "YES" ) { - if ( defined $myvar{'innodb_redo_log_capacity'} ) { - $mycalc{'innodb_log_size_pct'} = - ( $myvar{'innodb_redo_log_capacity'} / - $myvar{'innodb_buffer_pool_size'} ) * 100; - } - else { - $mycalc{'innodb_log_size_pct'} = 0; - if ( defined $myvar{'innodb_log_file_size'} - && $myvar{'innodb_log_file_size'} ne '' - && defined $myvar{'innodb_buffer_pool_size'} - && $myvar{'innodb_buffer_pool_size'} ne '' - && $myvar{'innodb_buffer_pool_size'} != 0 ) - { - $mycalc{'innodb_log_size_pct'} = - ( $myvar{'innodb_log_file_size'} * - $myvar{'innodb_log_files_in_group'} * 100 / - $myvar{'innodb_buffer_pool_size'} ); - } - } - } - if ( !defined $myvar{'innodb_buffer_pool_size'} ) { - $mycalc{'innodb_log_size_pct'} = 0; - $myvar{'innodb_buffer_pool_size'} = 0; - } - - # InnoDB Buffer pool read cache efficiency - ( - $mystat{'Innodb_buffer_pool_read_requests'}, - $mystat{'Innodb_buffer_pool_reads'} - ) - = ( 1, 1 ) - unless defined $mystat{'Innodb_buffer_pool_reads'}; - $mycalc{'pct_read_efficiency'} = percentage( - $mystat{'Innodb_buffer_pool_read_requests'}, - ( - $mystat{'Innodb_buffer_pool_read_requests'} + - $mystat{'Innodb_buffer_pool_reads'} - ) - ) if defined $mystat{'Innodb_buffer_pool_read_requests'}; - debugprint "pct_read_efficiency: " . $mycalc{'pct_read_efficiency'} . ""; - debugprint "Innodb_buffer_pool_reads: " - . $mystat{'Innodb_buffer_pool_reads'} . ""; - debugprint "Innodb_buffer_pool_read_requests: " - . $mystat{'Innodb_buffer_pool_read_requests'} . ""; - - # InnoDB log write cache efficiency - ( $mystat{'Innodb_log_write_requests'}, $mystat{'Innodb_log_writes'} ) = - ( 1, 1 ) - unless defined $mystat{'Innodb_log_writes'}; - $mycalc{'pct_write_efficiency'} = percentage( - ( $mystat{'Innodb_log_write_requests'} - $mystat{'Innodb_log_writes'} ), - $mystat{'Innodb_log_write_requests'} - ) if defined $mystat{'Innodb_log_write_requests'}; - debugprint "pct_write_efficiency: " . $mycalc{'pct_write_efficiency'} . ""; - debugprint "Innodb_log_writes: " . $mystat{'Innodb_log_writes'} . ""; - debugprint "Innodb_log_write_requests: " - . $mystat{'Innodb_log_write_requests'} . ""; - $mycalc{'pct_innodb_buffer_used'} = percentage( - ( - $mystat{'Innodb_buffer_pool_pages_total'} - - $mystat{'Innodb_buffer_pool_pages_free'} - ), - $mystat{'Innodb_buffer_pool_pages_total'} - ) if defined $mystat{'Innodb_buffer_pool_pages_total'}; - - my $lreq = - "select ROUND( 100* sum(allocated)/ " - . $myvar{'innodb_buffer_pool_size'} - . ',1) FROM sys.x\$innodb_buffer_stats_by_table;'; - debugprint("lreq: $lreq"); - $mycalc{'innodb_buffer_alloc_pct'} = select_one($lreq) - if ( $opt{experimental} ); - - # Binlog Cache - if ( $myvar{'log_bin'} ne 'OFF' ) { - $mycalc{'pct_binlog_cache'} = percentage( - $mystat{'Binlog_cache_use'} - $mystat{'Binlog_cache_disk_use'}, - $mystat{'Binlog_cache_use'} ); - } -} - -sub mysql_stats { - subheaderprint "Performance Metrics"; - - # Show uptime, queries per second, connections, traffic stats - my $qps; - if ( $mystat{'Uptime'} > 0 ) { - $qps = sprintf( "%.3f", $mystat{'Questions'} / $mystat{'Uptime'} ); - } - push( @generalrec, -"MySQL was started within the last 24 hours: recommendations may be inaccurate" - ) if ( $mystat{'Uptime'} < 86400 ); - infoprint "Up for: " - . pretty_uptime( $mystat{'Uptime'} ) . " (" - . hr_num( $mystat{'Questions'} ) . " q [" - . hr_num($qps) - . " qps], " - . hr_num( $mystat{'Connections'} ) - . " conn," . " TX: " - . hr_bytes_rnd( $mystat{'Bytes_sent'} ) - . ", RX: " - . hr_bytes_rnd( $mystat{'Bytes_received'} ) . ")"; - infoprint "Reads / Writes: " - . $mycalc{'pct_reads'} . "% / " - . $mycalc{'pct_writes'} . "%"; - - # Binlog Cache - if ( $myvar{'log_bin'} eq 'OFF' ) { - infoprint "Binary logging is disabled"; - } - else { - infoprint "Binary logging is enabled (GTID MODE: " - . ( defined( $myvar{'gtid_mode'} ) ? $myvar{'gtid_mode'} : "OFF" ) - . ")"; - } - - # Memory usage - infoprint "Physical Memory : " . hr_bytes($physical_memory); - infoprint "Max MySQL memory : " . hr_bytes( $mycalc{'max_peak_memory'} ); - infoprint "Other process memory: " . hr_bytes( get_other_process_memory() ); - - infoprint "Total buffers: " - . hr_bytes( $mycalc{'server_buffers'} ) - . " global + " - . hr_bytes( $mycalc{'per_thread_buffers'} ) - . " per thread ($myvar{'max_connections'} max threads)"; - infoprint "Performance_schema Max memory usage: " - . hr_bytes_rnd( get_pf_memory() ); - $result{'Performance_schema'}{'memory'} = get_pf_memory(); - $result{'Performance_schema'}{'pretty_memory'} = - hr_bytes_rnd( get_pf_memory() ); - infoprint "Galera GCache Max memory usage: " - . hr_bytes_rnd( get_gcache_memory() ); - $result{'Galera'}{'GCache'}{'memory'} = get_gcache_memory(); - $result{'Galera'}{'GCache'}{'pretty_memory'} = - hr_bytes_rnd( get_gcache_memory() ); - - if ( $opt{buffers} ne 0 ) { - infoprint "Global Buffers"; - infoprint " +-- Key Buffer: " - . hr_bytes( $myvar{'key_buffer_size'} ) . ""; - infoprint " +-- Max Tmp Table: " - . hr_bytes( $mycalc{'max_tmp_table_size'} ) . ""; - - if ( defined $myvar{'query_cache_type'} ) { - infoprint "Query Cache Buffers"; - infoprint " +-- Query Cache: " - . $myvar{'query_cache_type'} . " - " - . ( - $myvar{'query_cache_type'} eq 0 | - $myvar{'query_cache_type'} eq 'OFF' ? "DISABLED" - : ( - $myvar{'query_cache_type'} eq 1 ? "ALL REQUESTS" - : "ON DEMAND" - ) - ) . ""; - infoprint " +-- Query Cache Size: " - . hr_bytes( $myvar{'query_cache_size'} ) . ""; - } - - infoprint "Per Thread Buffers"; - infoprint " +-- Read Buffer: " - . hr_bytes( $myvar{'read_buffer_size'} ) . ""; - infoprint " +-- Read RND Buffer: " - . hr_bytes( $myvar{'read_rnd_buffer_size'} ) . ""; - infoprint " +-- Sort Buffer: " - . hr_bytes( $myvar{'sort_buffer_size'} ) . ""; - infoprint " +-- Thread stack: " - . hr_bytes( $myvar{'thread_stack'} ) . ""; - infoprint " +-- Join Buffer: " - . hr_bytes( $myvar{'join_buffer_size'} ) . ""; - if ( $myvar{'log_bin'} ne 'OFF' ) { - infoprint "Binlog Cache Buffers"; - infoprint " +-- Binlog Cache: " - . hr_bytes( $myvar{'binlog_cache_size'} ) . ""; - } - } - - if ( $arch - && $arch == 32 - && $mycalc{'max_used_memory'} > 2 * 1024 * 1024 * 1024 ) - { - badprint - "Allocating > 2GB RAM on 32-bit systems can cause system instability"; - badprint "Maximum reached memory usage: " - . hr_bytes( $mycalc{'max_used_memory'} ) - . " ($mycalc{'pct_max_used_memory'}% of installed RAM)"; - } - elsif ( $mycalc{'pct_max_used_memory'} > 85 ) { - badprint "Maximum reached memory usage: " - . hr_bytes( $mycalc{'max_used_memory'} ) - . " ($mycalc{'pct_max_used_memory'}% of installed RAM)"; - } - else { - goodprint "Maximum reached memory usage: " - . hr_bytes( $mycalc{'max_used_memory'} ) - . " ($mycalc{'pct_max_used_memory'}% of installed RAM)"; - } - - if ( $mycalc{'pct_max_physical_memory'} > 85 ) { - badprint "Maximum possible memory usage: " - . hr_bytes( $mycalc{'max_peak_memory'} ) - . " ($mycalc{'pct_max_physical_memory'}% of installed RAM)"; - push( @generalrec, - "Reduce your overall MySQL memory footprint for system stability" ); - } - else { - goodprint "Maximum possible memory usage: " - . hr_bytes( $mycalc{'max_peak_memory'} ) - . " ($mycalc{'pct_max_physical_memory'}% of installed RAM)"; - } - - if ( $physical_memory < - ( $mycalc{'max_peak_memory'} + get_other_process_memory() ) ) - { - if ( $opt{nondedicated} ) { - infoprint "No warning with --nondedicated option"; - infoprint -"Overall possible memory usage with other process exceeded memory"; - } - else { - badprint -"Overall possible memory usage with other process exceeded memory"; - push( @generalrec, - "Dedicate this server to your database for highest performance." - ); - } - } - else { - goodprint -"Overall possible memory usage with other process is compatible with memory available"; - } - - # Slow queries - if ( $mycalc{'pct_slow_queries'} > 5 ) { - badprint "Slow queries: $mycalc{'pct_slow_queries'}% (" - . hr_num( $mystat{'Slow_queries'} ) . "/" - . hr_num( $mystat{'Questions'} ) . ")"; - } - else { - goodprint "Slow queries: $mycalc{'pct_slow_queries'}% (" - . hr_num( $mystat{'Slow_queries'} ) . "/" - . hr_num( $mystat{'Questions'} ) . ")"; - } - if ( $myvar{'long_query_time'} > 10 ) { - push( @adjvars, "long_query_time (<= 10)" ); - } - if ( defined( $myvar{'log_slow_queries'} ) ) { - if ( $myvar{'log_slow_queries'} eq "OFF" ) { - push( @generalrec, - "Enable the slow query log to troubleshoot bad queries" ); - } - } - - # Connections - if ( $mycalc{'pct_connections_used'} > 85 ) { - badprint -"Highest connection usage: $mycalc{'pct_connections_used'}% ($mystat{'Max_used_connections'}/$myvar{'max_connections'})"; - push( @adjvars, - "max_connections (> " . $myvar{'max_connections'} . ")" ); - push( @adjvars, - "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'}% ($mystat{'Max_used_connections'}/$myvar{'max_connections'})"; - } - - # Aborted Connections - if ( $mycalc{'pct_connections_aborted'} > 3 ) { - badprint -"Aborted connections: $mycalc{'pct_connections_aborted'}% ($mystat{'Aborted_connects'}/$mystat{'Connections'})"; - push( @generalrec, - "Reduce or eliminate unclosed connections and network issues" ); - } - else { - goodprint -"Aborted connections: $mycalc{'pct_connections_aborted'}% ($mystat{'Aborted_connects'}/$mystat{'Connections'})"; - } - - # name resolution - debugprint "skip name resolve: $result{'Variables'}{'skip_name_resolve'}" - if ( defined( $result{'Variables'}{'skip_name_resolve'} ) ); - if ( defined( $result{'Variables'}{'skip_networking'} ) - && $result{'Variables'}{'skip_networking'} eq 'ON' ) - { - infoprint -"Skipped name resolution test due to skip_networking=ON in system variables."; - } - elsif ( not defined( $result{'Variables'}{'skip_name_resolve'} ) ) { - infoprint -"Skipped name resolution test due to missing skip_name_resolve in system variables."; - } - - # Cpanel and Skip name resolve (Issue #863) - # Ref: https://support.cpanel.net/hc/en-us/articles/21664293830423 - elsif (-r "/usr/local/cpanel/cpanel" - || -r "/var/cpanel/cpanel.config" - || -r "/etc/cpupdate.conf" ) - { - if ( $result{'Variables'}{'skip_name_resolve'} ne 'OFF' - and $result{'Variables'}{'skip_name_resolve'} ne '0' ) - { - badprint -"cPanel/Flex system detected: skip-name-resolve should be disabled (OFF)"; - push( @generalrec, -"cPanel recommends keeping skip-name-resolve disabled: https://support.cpanel.net/hc/en-us/articles/21664293830423" - ); - } - } - elsif ( $result{'Variables'}{'skip_name_resolve'} ne 'ON' - and $result{'Variables'}{'skip_name_resolve'} ne '1' ) - { - badprint -"Name resolution is active: a reverse name resolution is made for each new connection which can reduce performance"; - push( @generalrec, -"Configure your accounts with ip or subnets only, then update your configuration with skip-name-resolve=ON" - ); - push( @adjvars, "skip-name-resolve=ON" ); - } - - # Query cache - if ( !mysql_version_ge(4) ) { - - # MySQL versions < 4.01 don't support query caching - push( @generalrec, - "Upgrade MySQL to version 4+ to utilize query caching" ); - } - elsif ( mysql_version_ge(8) and mysql_version_le( 9, 9 ) ) { - infoprint "Query cache has been removed since MySQL 8.0"; - - #return; - } - elsif ($myvar{'query_cache_size'} < 1 - or $myvar{'query_cache_type'} eq "OFF" ) - { - goodprint -"Query cache is disabled by default due to mutex contention on multiprocessor machines."; - } - elsif ( $mystat{'Com_select'} == 0 ) { - badprint - "Query cache cannot be analyzed: no SELECT statements executed"; - } - else { - if ( $mycalc{'query_cache_efficiency'} < 20 ) { - badprint - "Query cache efficiency: $mycalc{'query_cache_efficiency'}% (" - . hr_num( $mystat{'Qcache_hits'} ) - . " cached / " - . hr_num( $mystat{'Qcache_hits'} + $mystat{'Com_select'} ) - . " selects)"; - badprint - "Query cache may be disabled by default due to mutex contention."; - push( @adjvars, "query_cache_size (=0)" ); - push( @adjvars, "query_cache_type (=0)" ); - } - else { - goodprint - "Query cache efficiency: $mycalc{'query_cache_efficiency'}% (" - . hr_num( $mystat{'Qcache_hits'} ) - . " cached / " - . hr_num( $mystat{'Qcache_hits'} + $mystat{'Com_select'} ) - . " selects)"; - if ( $mycalc{'query_cache_prunes_per_day'} > 98 ) { - badprint -"Query cache prunes per day: $mycalc{'query_cache_prunes_per_day'}"; - if ( $myvar{'query_cache_size'} >= 128 * 1024 * 1024 ) { - push( @generalrec, -"Increasing the query_cache size over 128M may reduce performance" - ); - push( @adjvars, - "query_cache_size (> " - . hr_bytes_rnd( $myvar{'query_cache_size'} ) - . ") [see warning above]" ); - } - else { - push( @adjvars, - "query_cache_size (> " - . hr_bytes_rnd( $myvar{'query_cache_size'} ) - . ")" ); - } - } - else { - goodprint -"Query cache prunes per day: $mycalc{'query_cache_prunes_per_day'}"; - } - } - - } - - # Sorting - if ( $mycalc{'total_sorts'} == 0 ) { - goodprint "No Sort requiring temporary tables"; - } - elsif ( $mycalc{'pct_temp_sort_table'} > 10 ) { - badprint - "Sorts requiring temporary tables: $mycalc{'pct_temp_sort_table'}% (" - . hr_num( $mystat{'Sort_merge_passes'} ) - . " temp sorts / " - . hr_num( $mycalc{'total_sorts'} ) - . " sorts)"; - push( @adjvars, - "sort_buffer_size (> " - . hr_bytes_rnd( $myvar{'sort_buffer_size'} ) - . ")" ); - push( @adjvars, - "read_rnd_buffer_size (> " - . hr_bytes_rnd( $myvar{'read_rnd_buffer_size'} ) - . ")" ); - } - else { - goodprint - "Sorts requiring temporary tables: $mycalc{'pct_temp_sort_table'}% (" - . hr_num( $mystat{'Sort_merge_passes'} ) - . " temp sorts / " - . hr_num( $mycalc{'total_sorts'} ) - . " sorts)"; - } - - # Joins - if ( $mycalc{'joins_without_indexes_per_day'} > 250 ) { - badprint - "Joins performed without indexes: $mycalc{'joins_without_indexes'}"; - if ( $myvar{'join_buffer_size'} < 4 * 1024 * 1024 ) { - push( @adjvars, - "join_buffer_size (> " - . hr_bytes( $myvar{'join_buffer_size'} ) - . ", or always use indexes with JOINs)" ); - } - else { - push( @adjvars, "always use indexes with JOINs" ); - } - push( - @generalrec, -"We will suggest raising the 'join_buffer_size' until JOINs not using indexes are found. - See https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_join_buffer_size" - ); - } - else { - goodprint "No joins without indexes"; - - # No joins have run without indexes - } - - # Temporary tables - if ( $mystat{'Created_tmp_tables'} > 0 ) { - if ( $mycalc{'pct_temp_disk'} > 25 - && $mycalc{'max_tmp_table_size'} < 256 * 1024 * 1024 ) - { - badprint - "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% (" - . hr_num( $mystat{'Created_tmp_disk_tables'} ) - . " on disk / " - . hr_num( $mystat{'Created_tmp_tables'} ) - . " total)"; - push( @adjvars, - "tmp_table_size (> " - . hr_bytes_rnd( $myvar{'tmp_table_size'} ) - . ")" ); - push( @adjvars, - "max_heap_table_size (> " - . hr_bytes_rnd( $myvar{'max_heap_table_size'} ) - . ")" ); - push( @generalrec, -"When making adjustments, make tmp_table_size/max_heap_table_size equal" - ); - push( @generalrec, - "Reduce your SELECT DISTINCT queries which have no LIMIT clause" - ); - } - elsif ($mycalc{'pct_temp_disk'} > 25 - && $mycalc{'max_tmp_table_size'} >= 256 * 1024 * 1024 ) - { - badprint - "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% (" - . hr_num( $mystat{'Created_tmp_disk_tables'} ) - . " on disk / " - . hr_num( $mystat{'Created_tmp_tables'} ) - . " total)"; - push( @generalrec, - "Temporary table size is already large: reduce result set size" - ); - push( @generalrec, - "Reduce your SELECT DISTINCT queries without LIMIT clauses" ); - } - else { - goodprint - "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% (" - . hr_num( $mystat{'Created_tmp_disk_tables'} ) - . " on disk / " - . hr_num( $mystat{'Created_tmp_tables'} ) - . " total)"; - } - } - else { - goodprint "No tmp tables created on disk"; - } - - # Thread cache - if ( defined( $myvar{'have_threadpool'} ) - and $myvar{'have_threadpool'} eq 'YES' ) - { -# https://www.percona.com/doc/percona-server/5.7/performance/threadpool.html#status-variables -# When thread pool is enabled, the value of the thread_cache_size variable -# is ignored. The Threads_cached status variable contains 0 in this case. - infoprint "Thread cache not used with thread pool enabled"; - } - else { - if ( $myvar{'thread_cache_size'} eq 0 ) { - badprint "Thread cache is disabled"; - push( @generalrec, - "Set thread_cache_size to 4 as a starting value" ); - push( @adjvars, "thread_cache_size (start at 4)" ); - } - else { - if ( $mycalc{'thread_cache_hit_rate'} <= 50 ) { - badprint - "Thread cache hit rate: $mycalc{'thread_cache_hit_rate'}% (" - . hr_num( $mystat{'Threads_created'} ) - . " created / " - . hr_num( $mystat{'Connections'} ) - . " connections)"; - push( @adjvars, - "thread_cache_size (> $myvar{'thread_cache_size'})" ); - } - else { - goodprint - "Thread cache hit rate: $mycalc{'thread_cache_hit_rate'}% (" - . hr_num( $mystat{'Threads_created'} ) - . " created / " - . hr_num( $mystat{'Connections'} ) - . " connections)"; - } - } - } - - # Table cache - my $table_cache_var = ""; - if ( $mystat{'Open_tables'} > 0 ) { - if ( $mycalc{'table_cache_hit_rate'} < 20 ) { - - unless ( defined( $mystat{'Table_open_cache_hits'} ) ) { - badprint - "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% (" - . hr_num( $mystat{'Open_tables'} ) - . " hits / " - . hr_num( $mystat{'Opened_tables'} ) - . " requests)"; - } - else { - badprint - "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% (" - . hr_num( $mystat{'Table_open_cache_hits'} ) - . " hits / " - . hr_num( $mystat{'Table_open_cache_hits'} + - $mystat{'Table_open_cache_misses'} ) - . " requests)"; - } - - if ( mysql_version_ge( 5, 1 ) ) { - $table_cache_var = "table_open_cache"; - } - else { - $table_cache_var = "table_cache"; - } - - push( @adjvars, - $table_cache_var . " (> " . $myvar{$table_cache_var} . ")" ); - push( @generalrec, - "Increase " - . $table_cache_var - . " gradually to avoid file descriptor limits" ); - push( @generalrec, - "Read this before increasing " - . $table_cache_var - . " over 64: https://bit.ly/2Fulv7r" ); - push( @generalrec, - "Read this before increasing for MariaDB" - . " https://mariadb.com/kb/en/library/optimizing-table_open_cache/" - ); - push( @generalrec, -"This is MyISAM only table_cache scalability problem, InnoDB not affected." - ); - push( @generalrec, - "For more details see: https://bugs.mysql.com/bug.php?id=49177" - ); - push( @generalrec, -"This bug already fixed in MySQL 5.7.9 and newer MySQL versions." - ); - push( @generalrec, - "Beware that open_files_limit (" - . $myvar{'open_files_limit'} - . ") variable " ); - push( @generalrec, - "should be greater than $table_cache_var (" - . $myvar{$table_cache_var} - . ")" ); - } - else { - unless ( defined( $mystat{'Table_open_cache_hits'} ) ) { - goodprint - "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% (" - . hr_num( $mystat{'Open_tables'} ) - . " hits / " - . hr_num( $mystat{'Opened_tables'} ) - . " requests)"; - } - else { - goodprint - "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% (" - . hr_num( $mystat{'Table_open_cache_hits'} ) - . " hits / " - . hr_num( $mystat{'Table_open_cache_hits'} + - $mystat{'Table_open_cache_misses'} ) - . " requests)"; - } - } - } - - # Table definition cache - my $nbtables = select_one('SELECT COUNT(*) FROM information_schema.tables'); - $mycalc{'total_tables'} = $nbtables; - if ( defined $myvar{'table_definition_cache'} ) { - if ( $myvar{'table_definition_cache'} == -1 ) { - infoprint( "table_definition_cache (" - . $myvar{'table_definition_cache'} - . ") is in autosizing mode" ); - } - elsif ( $myvar{'table_definition_cache'} < $nbtables ) { - badprint "table_definition_cache (" - . $myvar{'table_definition_cache'} - . ") is less than number of tables ($nbtables) "; - push( @adjvars, - "table_definition_cache (" - . $myvar{'table_definition_cache'} . ") > " - . $nbtables - . " or -1 (autosizing if supported)" ); - } - else { - goodprint "table_definition_cache (" - . $myvar{'table_definition_cache'} - . ") is greater than number of tables ($nbtables)"; - } - } - else { - infoprint "No table_definition_cache variable found."; - } - - # Open files - if ( defined $mycalc{'pct_files_open'} ) { - if ( $mycalc{'pct_files_open'} > 85 ) { - badprint "Open file limit used: $mycalc{'pct_files_open'}% (" - . hr_num( $mystat{'Open_files'} ) . "/" - . hr_num( $myvar{'open_files_limit'} ) . ")"; - push( @adjvars, - "open_files_limit (> " . $myvar{'open_files_limit'} . ")" ); - } - else { - goodprint "Open file limit used: $mycalc{'pct_files_open'}% (" - . hr_num( $mystat{'Open_files'} ) . "/" - . hr_num( $myvar{'open_files_limit'} ) . ")"; - } - } - - # 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'}%"; - push( @generalrec, - "Optimize queries and/or use InnoDB to reduce lock wait" ); - } - else { - goodprint -"Table locks acquired immediately: $mycalc{'pct_table_locks_immediate'}% (" - . hr_num( $mystat{'Table_locks_immediate'} ) - . " immediate / " - . hr_num( $mystat{'Table_locks_waited'} + - $mystat{'Table_locks_immediate'} ) - . " locks)"; - } - } - - # Binlog cache - if ( defined $mycalc{'pct_binlog_cache'} ) { - if ( $mycalc{'pct_binlog_cache'} < 90 - && $mystat{'Binlog_cache_use'} > 0 ) - { - badprint "Binlog cache memory access: " - . $mycalc{'pct_binlog_cache'} . "% (" - . ( - $mystat{'Binlog_cache_use'} - $mystat{'Binlog_cache_disk_use'} ) - . " Memory / " - . $mystat{'Binlog_cache_use'} - . " Total)"; - push( @generalrec, - "Increase binlog_cache_size (current value: " - . $myvar{'binlog_cache_size'} - . ")" ); - push( @adjvars, - "binlog_cache_size (" - . hr_bytes( $myvar{'binlog_cache_size'} + 16 * 1024 * 1024 ) - . ")" ); - } - else { - goodprint "Binlog cache memory access: " - . $mycalc{'pct_binlog_cache'} . "% (" - . ( - $mystat{'Binlog_cache_use'} - $mystat{'Binlog_cache_disk_use'} ) - . " Memory / " - . $mystat{'Binlog_cache_use'} - . " Total)"; - debugprint "Not enough data to validate binlog cache size\n" - if $mystat{'Binlog_cache_use'} < 10; - } - } - - # Performance options - if ( !mysql_version_ge( 5, 1 ) ) { - push( @generalrec, "Upgrade to MySQL 5.5+ to use asynchronous write" ); - } - elsif ( $myvar{'concurrent_insert'} eq "OFF" ) { - push( @generalrec, "Enable concurrent_insert by setting it to 'ON'" ); - } - elsif ( $myvar{'concurrent_insert'} eq 0 ) { - push( @generalrec, "Enable concurrent_insert by setting it to 1" ); - } -} - -# Recommendations for MyISAM -sub mysql_myisam { - return 0 unless ( $opt{'myisamstat'} > 0 ); - subheaderprint "MyISAM Metrics"; - my $nb_myisam_tables = select_one( -"SELECT COUNT(*) FROM information_schema.TABLES WHERE ENGINE='MyISAM' and TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema')" - ); - push( @generalrec, - "MyISAM engine is deprecated, consider migrating to InnoDB" ) - if $nb_myisam_tables > 0; - - if ( $nb_myisam_tables > 0 ) { - badprint - "Consider migrating $nb_myisam_tables following tables to InnoDB:"; - my $sql_mig = ""; - for my $myisam_table ( - select_array( -"SELECT CONCAT('|',TABLE_SCHEMA, '|.|', TABLE_NAME,'|') FROM information_schema.TABLES WHERE ENGINE='MyISAM' and TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema')" - ) - ) - { - my $myisam_table_escape = $myisam_table =~ s/\|/\`/gr; - $sql_mig = -"${sql_mig}-- InnoDB migration for $myisam_table_escape\nALTER TABLE $myisam_table_escape ENGINE=InnoDB;\n\n"; - infoprint -"* InnoDB migration request for $myisam_table_escape Table: ALTER TABLE $myisam_table_escape ENGINE=InnoDB;"; - } - dump_into_file( "migrate_myisam_to_innodb.sql", $sql_mig ); - } - infoprint("General MyIsam metrics:"); - infoprint " +-- Total MyISAM Tables : $nb_myisam_tables"; - infoprint " +-- Total MyISAM indexes : " - . hr_bytes( $mycalc{'total_myisam_indexes'} ) - if defined( $mycalc{'total_myisam_indexes'} ); - infoprint " +-- KB Size :" . hr_bytes( $myvar{'key_buffer_size'} ); - infoprint " +-- KB Used Size :" - . hr_bytes( $myvar{'key_buffer_size'} - - $mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'} ); - infoprint " +-- KB used :" . $mycalc{'pct_key_buffer_used'} . "%"; - infoprint " +-- Read KB hit rate: $mycalc{'pct_keys_from_mem'}% (" - . hr_num( $mystat{'Key_read_requests'} ) - . " cached / " - . hr_num( $mystat{'Key_reads'} ) - . " reads)"; - infoprint " +-- Write KB hit rate: $mycalc{'pct_wkeys_from_mem'}% (" - . hr_num( $mystat{'Key_write_requests'} ) - . " cached / " - . hr_num( $mystat{'Key_writes'} ) - . " writes)"; - - if ( $nb_myisam_tables == 0 ) { - infoprint "No MyISAM table(s) detected ...."; - return; - } - if ( mysql_version_ge(8) and mysql_version_le(10) ) { - infoprint "MyISAM Metrics are disabled since MySQL 8.0."; - if ( $myvar{'key_buffer_size'} > 0 ) { - push( @adjvars, "key_buffer_size=0" ); - push( @generalrec, - "Buffer Key MyISAM set to 0, no MyISAM table detected" ); - } - return; - } - - if ( !defined( $mycalc{'total_myisam_indexes'} ) ) { - badprint - "Unable to calculate MyISAM index size on MySQL server < 5.0.0"; - push( @generalrec, - "Unable to calculate MyISAM index size on MySQL server < 5.0.0" ); - return; - } - if ( $mycalc{'pct_key_buffer_used'} == 0 ) { - - # No queries have run that would use keys - infoprint "Key buffer used: $mycalc{'pct_key_buffer_used'}% (" - . hr_bytes( $myvar{'key_buffer_size'} - - $mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'} ) - . " used / " - . hr_bytes( $myvar{'key_buffer_size'} ) - . " cache)"; - infoprint "No SQL statement based on MyISAM table(s) detected ...."; - return; - } - - # Key buffer usage - if ( $mycalc{'pct_key_buffer_used'} < 90 ) { - badprint "Key buffer used: $mycalc{'pct_key_buffer_used'}% (" - . hr_bytes( $myvar{'key_buffer_size'} - - $mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'} ) - . " used / " - . hr_bytes( $myvar{'key_buffer_size'} ) - . " cache)"; - - push( - @adjvars, - "key_buffer_size (\~ " - . hr_num( - $myvar{'key_buffer_size'} * - $mycalc{'pct_key_buffer_used'} / 100 - ) - . ")" - ); - } - else { - goodprint "Key buffer used: $mycalc{'pct_key_buffer_used'}% (" - . hr_bytes( $myvar{'key_buffer_size'} - - $mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'} ) - . " used / " - . hr_bytes( $myvar{'key_buffer_size'} ) - . " cache)"; - } - - # Key buffer size / total MyISAM indexes - if ( $myvar{'key_buffer_size'} < $mycalc{'total_myisam_indexes'} - && $mycalc{'pct_keys_from_mem'} < 95 - && $mycalc{'pct_key_buffer_used'} >= 90 ) - { - badprint "Key buffer size / total MyISAM indexes: " - . hr_bytes( $myvar{'key_buffer_size'} ) . "/" - . hr_bytes( $mycalc{'total_myisam_indexes'} ) . ""; - push( @adjvars, - "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'} ) . ""; - } - if ( $mystat{'Key_read_requests'} > 0 ) { - if ( $mycalc{'pct_keys_from_mem'} < 95 ) { - badprint - "Read Key buffer hit rate: $mycalc{'pct_keys_from_mem'}% (" - . hr_num( $mystat{'Key_read_requests'} ) - . " cached / " - . hr_num( $mystat{'Key_reads'} ) - . " reads)"; - } - else { - goodprint - "Read Key buffer hit rate: $mycalc{'pct_keys_from_mem'}% (" - . hr_num( $mystat{'Key_read_requests'} ) - . " cached / " - . hr_num( $mystat{'Key_reads'} ) - . " reads)"; - } - } - - # No queries have run that would use keys - debugprint "Key buffer size / total MyISAM indexes: " - . hr_bytes( $myvar{'key_buffer_size'} ) . "/" - . hr_bytes( $mycalc{'total_myisam_indexes'} ) . ""; - if ( $mystat{'Key_write_requests'} > 0 ) { - if ( $mycalc{'pct_wkeys_from_mem'} < 95 ) { - badprint - "Write Key buffer hit rate: $mycalc{'pct_wkeys_from_mem'}% (" - . hr_num( $mystat{'Key_write_requests'} ) - . " cached / " - . hr_num( $mystat{'Key_writes'} ) - . " writes)"; - } - else { - goodprint - "Write Key buffer hit rate: $mycalc{'pct_wkeys_from_mem'}% (" - . hr_num( $mystat{'Key_write_requests'} ) - . " cached / " - . hr_num( $mystat{'Key_writes'} ) - . " writes)"; - } - } - else { - # No queries have run that would use keys - debugprint - "Write Key buffer hit rate: $mycalc{'pct_wkeys_from_mem'}% (" - . hr_num( $mystat{'Key_write_requests'} ) - . " cached / " - . hr_num( $mystat{'Key_writes'} ) - . " writes)"; - } -} - -# Recommendations for ThreadPool -# See issue #404: https://github.com/jmrenouard/MySQLTuner-perl/issues/404 -sub mariadb_threadpool { - my $is_mariadb = ( ( $myvar{'version'} // '' ) =~ /mariadb/i ); - my $is_percona = ( - ( $myvar{'version'} // '' ) =~ /percona/i - or ( $myvar{'version_comment'} // '' ) =~ /percona/i - ); - - # Thread Pool is only relevant for MariaDB and Percona - return unless ( $is_mariadb or $is_percona ); - - my $thread_handling = $myvar{'thread_handling'} - // 'one-thread-per-connection'; - my $is_threadpool_enabled = ( $thread_handling eq 'pool-of-threads' ); - -# Recommendation to ENABLE thread pool if connections are high -# https://www.percona.com/blog/2014/01/23/percona-server-improve-scalability-percona-thread-pool/ - if ( !$is_threadpool_enabled - && ( $mystat{'Max_used_connections'} // 0 ) >= 512 ) - { - subheaderprint "ThreadPool Metrics"; - infoprint "ThreadPool stat is disabled."; - badprint - "Max_used_connections ($mystat{'Max_used_connections'}) is >= 512."; - push( @generalrec, -"Enabling the thread pool is recommended for servers with max_connections >= 512 (currently $myvar{'max_connections'})" - ); - push( @adjvars, "thread_handling=pool-of-threads" ); - } - - # If it IS enabled, show metrics and recommendations - if ($is_threadpool_enabled) { - subheaderprint "ThreadPool Metrics"; - infoprint "ThreadPool stat is enabled."; - infoprint "Thread Pool Size: " - . $myvar{'thread_pool_size'} - . " thread(s)."; - - # Recommendation to DISABLE thread pool if connections are low - if ( ( $mystat{'Max_used_connections'} // 0 ) < 512 ) { - badprint -"ThreadPool is enabled but Max_used_connections is < 512 ($mystat{'Max_used_connections'})."; - push( @generalrec, -"Thread pool is usually only efficient for servers with max_connections >= 512" - ); - } - - my $np = logical_cpu_cores(); - if ( $np <= 0 ) { - debugprint -"Unable to detect logical CPU cores for thread_pool_size recommendation."; - return; - } - -# Percona and MariaDB recommendation: ideally one active thread per CPU -# Efficient range: [NCPU, NCPU + NCPU/2] -# Source: https://mariadb.com/kb/en/library/thread-pool-in-mariadb/ -# Source: https://www.percona.com/blog/2014/01/23/percona-server-improve-scalability-percona-thread-pool/ - my $min_tps = $np; - my $max_tps = int( $np * 1.5 ); - - if ( $myvar{'thread_pool_size'} >= $min_tps - && $myvar{'thread_pool_size'} <= $max_tps ) - { - goodprint -"thread_pool_size is optimal ($myvar{'thread_pool_size'}) for your $np CPUs (range: $min_tps - $max_tps)"; - } - else { - badprint -"thread_pool_size ($myvar{'thread_pool_size'}) is not in the recommended range [$min_tps, $max_tps] for your $np CPUs."; - push( @adjvars, "thread_pool_size between $min_tps and $max_tps" ); - } - } -} - -sub get_pf_memory { - - # Performance Schema - return 0 unless defined $myvar{'performance_schema'}; - return 0 if $myvar{'performance_schema'} eq 'OFF'; - - my @infoPFSMemory = grep { /\tperformance_schema[.]memory\t/msx } - select_array("SHOW ENGINE PERFORMANCE_SCHEMA STATUS"); - @infoPFSMemory == 1 || return 0; - $infoPFSMemory[0] =~ s/.*\s+(\d+)$/$1/g; - return $infoPFSMemory[0]; -} - -# Recommendations for Performance Schema -sub mysql_pfs { - return if ( $opt{pfstat} == 0 ); - subheaderprint "Performance schema"; - - # Performance Schema - debugprint "Performance schema is " . $myvar{'performance_schema'}; - $myvar{'performance_schema'} = 'OFF' - unless defined( $myvar{'performance_schema'} ); - if ( $myvar{'performance_schema'} eq 'OFF' ) { - badprint - "Performance_schema should be activated (observability issue)."; - push( @adjvars, "performance_schema=ON" ); - push( @generalrec, -"Performance schema should be activated for better diagnostics and observability" - ); - } - if ( $myvar{'performance_schema'} eq 'ON' ) { - infoprint "Performance_schema is activated."; - debugprint "Performance schema is " . $myvar{'performance_schema'}; - infoprint "Memory used by Performance_schema: " - . hr_bytes( get_pf_memory() ); - } - - unless ( grep /^sys$/, select_array("SHOW DATABASES") ) { - infoprint "Sys schema is not installed."; - push( @generalrec, - mysql_version_ge( 10, 0 ) - ? "Consider installing Sys schema from https://github.com/FromDual/mariadb-sys for MariaDB" - : "Consider installing Sys schema from https://github.com/mysql/mysql-sys for MySQL" - ) unless ( mysql_version_le( 5, 6 ) ); - - return; - } - infoprint "Sys schema is installed."; - return if ( $opt{pfstat} == 0 or $myvar{'performance_schema'} ne 'ON' ); - - infoprint "Sys schema Version: " - . select_one("select sys_version from sys.version"); - - # Top user per connection - subheaderprint "Performance schema: Top 5 user per connection"; - my $nbL = 1; - for my $lQuery ( - select_array( -'select user, total_connections from sys.user_summary order by total_connections desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery conn(s)"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top user per statement - subheaderprint "Performance schema: Top 5 user per statement"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, statements from sys.user_summary order by statements desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery stmt(s)"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top user per statement latency - subheaderprint "Performance schema: Top 5 user per statement latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, statement_avg_latency from sys.x\\$user_summary order by statement_avg_latency desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top user per lock latency - subheaderprint "Performance schema: Top 5 user per lock latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, lock_latency from sys.x\\$user_summary_by_statement_latency order by lock_latency desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top user per full scans - subheaderprint "Performance schema: Top 5 user per nb full scans"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, full_scans from sys.x\\$user_summary_by_statement_latency order by full_scans desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top user per row_sent - subheaderprint "Performance schema: Top 5 user per rows sent"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, rows_sent from sys.x\\$user_summary_by_statement_latency order by rows_sent desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top user per row modified - subheaderprint "Performance schema: Top 5 user per rows modified"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, rows_affected from sys.x\\$user_summary_by_statement_latency order by rows_affected desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top user per io - subheaderprint "Performance schema: Top 5 user per IO"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, file_ios from sys.x\\$user_summary order by file_ios desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top user per io latency - subheaderprint "Performance schema: Top 5 user per IO latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, file_io_latency from sys.x\\$user_summary order by file_io_latency desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per connection - subheaderprint "Performance schema: Top 5 host per connection"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, total_connections from sys.x\\$host_summary order by total_connections desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery conn(s)"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per statement - subheaderprint "Performance schema: Top 5 host per statement"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, statements from sys.x\\$host_summary order by statements desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery stmt(s)"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per statement latency - subheaderprint "Performance schema: Top 5 host per statement latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, statement_avg_latency from sys.x\\$host_summary order by statement_avg_latency desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per lock latency - subheaderprint "Performance schema: Top 5 host per lock latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, lock_latency from sys.x\\$host_summary_by_statement_latency order by lock_latency desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per full scans - subheaderprint "Performance schema: Top 5 host per nb full scans"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, full_scans from sys.x\\$host_summary_by_statement_latency order by full_scans desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per rows sent - subheaderprint "Performance schema: Top 5 host per rows sent"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, rows_sent from sys.x\\$host_summary_by_statement_latency order by rows_sent desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per rows modified - subheaderprint "Performance schema: Top 5 host per rows modified"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, rows_affected from sys.x\\$host_summary_by_statement_latency order by rows_affected desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per io - subheaderprint "Performance schema: Top 5 host per io"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, file_ios from sys.x\\$host_summary order by file_ios desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top 5 host per io latency - subheaderprint "Performance schema: Top 5 host per io latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, file_io_latency from sys.x\\$host_summary order by file_io_latency desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top IO type order by total io - subheaderprint "Performance schema: Top IO type order by total io"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select substring(event_name,14), SUM(total)AS total from sys.x\\$host_summary_by_file_io_type GROUP BY substring(event_name,14) ORDER BY total DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery i/o"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top IO type order by total latency - subheaderprint "Performance schema: Top IO type order by total latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select substring(event_name,14), ROUND(SUM(total_latency),1) AS total_latency from sys.x\\$host_summary_by_file_io_type GROUP BY substring(event_name,14) ORDER BY total_latency DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top IO type order by max latency - subheaderprint "Performance schema: Top IO type order by max latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select substring(event_name,14), MAX(max_latency) as max_latency from sys.x\\$host_summary_by_file_io_type GROUP BY substring(event_name,14) ORDER BY max_latency DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top Stages order by total io - subheaderprint "Performance schema: Top Stages order by total io"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select substring(event_name,7), SUM(total)AS total from sys.x\\$host_summary_by_stages GROUP BY substring(event_name,7) ORDER BY total DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery i/o"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top Stages order by total latency - subheaderprint "Performance schema: Top Stages order by total latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select substring(event_name,7), ROUND(SUM(total_latency),1) AS total_latency from sys.x\\$host_summary_by_stages GROUP BY substring(event_name,7) ORDER BY total_latency DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top Stages order by avg latency - subheaderprint "Performance schema: Top Stages order by avg latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select substring(event_name,7), MAX(avg_latency) as avg_latency from sys.x\\$host_summary_by_stages GROUP BY substring(event_name,7) ORDER BY avg_latency DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top host per table scans - subheaderprint "Performance schema: Top 5 host per table scans"; - $nbL = 1; - for my $lQuery ( - select_array( -'select host, table_scans from sys.x\\$host_summary order by table_scans desc LIMIT 5' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # InnoDB Buffer Pool by schema - subheaderprint "Performance schema: InnoDB Buffer Pool by schema"; - $nbL = 1; - for my $lQuery ( - select_array( -'select object_schema, allocated, data, pages from sys.x\\$innodb_buffer_stats_by_schema ORDER BY pages DESC' - ) - ) - { - infoprint " +-- $nbL: $lQuery page(s)"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # InnoDB Buffer Pool by table - subheaderprint "Performance schema: 40 InnoDB Buffer Pool by table"; - $nbL = 1; - for my $lQuery ( - select_array( -'select object_schema, object_name, allocated,data, pages from sys.x\\$innodb_buffer_stats_by_table ORDER BY pages DESC LIMIT 40' - ) - ) - { - infoprint " +-- $nbL: $lQuery page(s)"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Process per allocated memory - subheaderprint "Performance schema: Process per time"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, Command AS PROC, time from sys.x\\$processlist ORDER BY time DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # InnoDB Lock Waits - subheaderprint "Performance schema: InnoDB Lock Waits"; - $nbL = 1; - for my $lQuery ( - select_array( -'select wait_age_secs, locked_table, locked_type, waiting_query from sys.x\\$innodb_lock_waits order by wait_age_secs DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Threads IO Latency - subheaderprint "Performance schema: Thread IO Latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select user, total_latency, max_latency from sys.x\\$io_by_thread_by_latency order by total_latency DESC;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # High Cost SQL statements - subheaderprint "Performance schema: Top 15 Most latency statements"; - $nbL = 1; - for my $lQuery ( - select_array( -'select LEFT(query, 120), avg_latency from sys.x\\$statement_analysis order by avg_latency desc LIMIT 15' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top 5% slower queries - subheaderprint "Performance schema: Top 15 slower queries"; - $nbL = 1; - for my $lQuery ( - select_array( -'select LEFT(query, 120), exec_count from sys.x\\$statements_with_runtimes_in_95th_percentile order by exec_count desc LIMIT 15' - ) - ) - { - infoprint " +-- $nbL: $lQuery s"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top 10 nb statement type - subheaderprint "Performance schema: Top 15 nb statement type"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select statement, sum(total) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top statement by total latency - subheaderprint "Performance schema: Top 15 statement by total latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select statement, sum(total_latency) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top statement by lock latency - subheaderprint "Performance schema: Top 15 statement by lock latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select statement, sum(lock_latency) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top statement by full scans - subheaderprint "Performance schema: Top 15 statement by full scans"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select statement, sum(full_scans) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top statement by rows sent - subheaderprint "Performance schema: Top 15 statement by rows sent"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select statement, sum(rows_sent) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Top statement by rows modified - subheaderprint "Performance schema: Top 15 statement by rows modified"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select statement, sum(rows_affected) as total from sys.x\\$host_summary_by_statement_type group by statement order by total desc LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Use temporary tables - subheaderprint "Performance schema: 15 sample queries using temp table"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select left(query, 120) from sys.x\\$statements_with_temp_tables LIMIT 15' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Unused Indexes - subheaderprint "Performance schema: Unused indexes"; - $nbL = 1; - for my $lQuery ( - select_array( -"select CONCAT(object_schema, '.', object_name, ' (', index_name, ')') from sys.schema_unused_indexes where object_schema not in ('performance_schema', 'mysql', 'information_schema', 'sys')" - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - my ( $schema, $table, $index ) = $lQuery =~ /^(.*?)\.(.*?)\s\((.*?)\)$/; - push( - @modeling, - { - type => 'unused_index', - schema => $schema, - table => $table, - index => $index, - } - ); - $nbL++; - } - if ( $nbL > 1 ) { - my $idx_count = $nbL - 1; - badprint "Performance schema: $idx_count unused index(es) found."; - push( @generalrec, -"Unused indexes found: $idx_count index(es) should be reviewed and potentially removed." - ); - } - else { - infoprint "No information found or indicators deactivated."; - } - - # Full table scans - subheaderprint "Performance schema: Tables with full table scans"; - $nbL = 1; - for my $lQuery ( - select_array( -'select * from sys.x\\$schema_tables_with_full_table_scans order by rows_full_scanned DESC' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Latest file IO by latency - subheaderprint "Performance schema: Latest File IO by latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select thread, file, latency, operation from sys.x\\$latest_file_io ORDER BY latency LIMIT 10;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # FILE by IO read bytes - subheaderprint "Performance schema: File by IO read bytes"; - $nbL = 1; - for my $lQuery ( - select_array( -'select file, total_read from sys.x\\$io_global_by_file_by_bytes order by total_read DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # FILE by IO written bytes - subheaderprint "Performance schema: File by IO written bytes"; - $nbL = 1; - for my $lQuery ( - select_array( -'select file, total_written from sys.x\\$io_global_by_file_by_bytes order by total_written DESC LIMIT 15' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # file per IO total latency - subheaderprint "Performance schema: File per IO total latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select file, total_latency from sys.x\\$io_global_by_file_by_latency ORDER BY total_latency DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # file per IO read latency - subheaderprint "Performance schema: file per IO read latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select file, read_latency from sys.x\\$io_global_by_file_by_latency ORDER BY read_latency DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # file per IO write latency - subheaderprint "Performance schema: file per IO write latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select file, write_latency from sys.x\\$io_global_by_file_by_latency ORDER BY write_latency DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Event Wait by read bytes - subheaderprint "Performance schema: Event Wait by read bytes"; - $nbL = 1; - for my $lQuery ( - select_array( -'select event_name, total_read from sys.x\\$io_global_by_wait_by_bytes order by total_read DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Event Wait by write bytes - subheaderprint "Performance schema: Event Wait written bytes"; - $nbL = 1; - for my $lQuery ( - select_array( -'select event_name, total_written from sys.x\\$io_global_by_wait_by_bytes order by total_written DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # event per wait total latency - subheaderprint "Performance schema: event per wait total latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select event_name, total_latency from sys.x\\$io_global_by_wait_by_latency ORDER BY total_latency DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # event per wait read latency - subheaderprint "Performance schema: event per wait read latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select event_name, read_latency from sys.x\\$io_global_by_wait_by_latency ORDER BY read_latency DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # event per wait write latency - subheaderprint "Performance schema: event per wait write latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select event_name, write_latency from sys.x\\$io_global_by_wait_by_latency ORDER BY write_latency DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - #schema_index_statistics - # TOP 15 most read index - subheaderprint "Performance schema: Top 15 most read indexes"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name,index_name, rows_selected from sys.x\\$schema_index_statistics ORDER BY ROWs_selected DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 most used index - subheaderprint "Performance schema: Top 15 most modified indexes"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name,index_name, rows_inserted+rows_updated+rows_deleted AS changes from sys.x\\$schema_index_statistics ORDER BY rows_inserted+rows_updated+rows_deleted DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 high read latency index - subheaderprint "Performance schema: Top 15 high read latency index"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name,index_name, select_latency from sys.x\\$schema_index_statistics ORDER BY select_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 high insert latency index - subheaderprint "Performance schema: Top 15 most modified indexes"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name,index_name, insert_latency from sys.x\\$schema_index_statistics ORDER BY insert_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 high update latency index - subheaderprint "Performance schema: Top 15 high update latency index"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name,index_name, update_latency from sys.x\\$schema_index_statistics ORDER BY update_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 high delete latency index - subheaderprint "Performance schema: Top 15 high delete latency index"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name,index_name, delete_latency from sys.x\\$schema_index_statistics ORDER BY delete_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 most read tables - subheaderprint "Performance schema: Top 15 most read tables"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name, rows_fetched from sys.x\\$schema_table_statistics ORDER BY ROWs_fetched DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 most used tables - subheaderprint "Performance schema: Top 15 most modified tables"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name, rows_inserted+rows_updated+rows_deleted AS changes from sys.x\\$schema_table_statistics ORDER BY rows_inserted+rows_updated+rows_deleted DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 high read latency tables - subheaderprint "Performance schema: Top 15 high read latency tables"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name, fetch_latency from sys.x\\$schema_table_statistics ORDER BY fetch_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 high insert latency tables - subheaderprint "Performance schema: Top 15 high insert latency tables"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name, insert_latency from sys.x\\$schema_table_statistics ORDER BY insert_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 high update latency tables - subheaderprint "Performance schema: Top 15 high update latency tables"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name, update_latency from sys.x\\$schema_table_statistics ORDER BY update_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # TOP 15 high delete latency tables - subheaderprint "Performance schema: Top 15 high delete latency tables"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select table_schema, table_name, delete_latency from sys.x\\$schema_table_statistics ORDER BY delete_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - # Redundant indexes - subheaderprint "Performance schema: Redundant indexes"; - $nbL = 1; - for my $lQuery ( - select_array( -'select CONCAT(table_schema, ".", table_name, " (", redundant_index_name, ") redundant of ", dominant_index_name, " - SQL: ", sql_drop_index) from sys.schema_redundant_indexes;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - my ( $schema, $table, $redundant, $dominant, $sql ) = - $lQuery =~ - /^(.*?)\.(.*?)\s\((.*?)\)\sredundant\sof\s(.*?)\s-\sSQL:\s(.*)$/; - push( - @modeling, - { - type => 'redundant_index', - schema => $schema, - table => $table, - index => $redundant, - dominant_index => $dominant, - sql => $sql, - } - ); - $nbL++; - } - if ( $nbL > 1 ) { - my $idx_count = $nbL - 1; - badprint "Performance schema: $idx_count redundant index(es) found."; - push( @generalrec, -"Redundant indexes found: $idx_count index(es) should be reviewed and potentially removed." - ); - } - else { - infoprint "No information found or indicators deactivated."; - } - - subheaderprint "Performance schema: Table not using InnoDB buffer"; - $nbL = 1; - for my $lQuery ( - select_array( -' Select table_schema, table_name from sys.x\\$schema_table_statistics_with_buffer where innodb_buffer_allocated IS NULL;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 Tables using InnoDB buffer"; - $nbL = 1; - for my $lQuery ( - select_array( -'select table_schema,table_name,innodb_buffer_allocated from sys.x\\$schema_table_statistics_with_buffer where innodb_buffer_allocated IS NOT NULL ORDER BY innodb_buffer_allocated DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 Tables with InnoDB buffer free"; - $nbL = 1; - for my $lQuery ( - select_array( -'select table_schema,table_name,innodb_buffer_free from sys.x\\$schema_table_statistics_with_buffer where innodb_buffer_allocated IS NOT NULL ORDER BY innodb_buffer_free DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 Most executed queries"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), exec_count from sys.x\\$statement_analysis order by exec_count DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint - "Performance schema: Latest SQL queries in errors or warnings"; - $nbL = 1; - for my $lQuery ( - select_array( -'select LEFT(query, 120), last_seen from sys.x\\$statements_with_errors_or_warnings ORDER BY last_seen LIMIT 40;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 20 queries with full table scans"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), exec_count from sys.x\\$statements_with_full_table_scans order BY exec_count DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Last 50 queries with full table scans"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), last_seen from sys.x\\$statements_with_full_table_scans order BY last_seen DESC LIMIT 50;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 reader queries (95% percentile)"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), rows_sent from sys.x\\$statements_with_runtimes_in_95th_percentile ORDER BY ROWs_sent DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint - "Performance schema: Top 15 most row look queries (95% percentile)"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), rows_examined AS search from sys.x\\$statements_with_runtimes_in_95th_percentile ORDER BY rows_examined DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint - "Performance schema: Top 15 total latency queries (95% percentile)"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), total_latency AS search from sys.x\\$statements_with_runtimes_in_95th_percentile ORDER BY total_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint - "Performance schema: Top 15 max latency queries (95% percentile)"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), max_latency AS search from sys.x\\$statements_with_runtimes_in_95th_percentile ORDER BY max_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint - "Performance schema: Top 15 average latency queries (95% percentile)"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), avg_latency AS search from sys.x\\$statements_with_runtimes_in_95th_percentile ORDER BY avg_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 20 queries with sort"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), exec_count from sys.x\\$statements_with_sorting order BY exec_count DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Last 50 queries with sort"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), last_seen from sys.x\\$statements_with_sorting order BY last_seen DESC LIMIT 50;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 row sorting queries with sort"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), rows_sorted from sys.x\\$statements_with_sorting ORDER BY ROWs_sorted DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 total latency queries with sort"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), total_latency AS search from sys.x\\$statements_with_sorting ORDER BY total_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 merge queries with sort"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), sort_merge_passes AS search from sys.x\\$statements_with_sorting ORDER BY sort_merge_passes DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint - "Performance schema: Top 15 average sort merges queries with sort"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), avg_sort_merges AS search from sys.x\\$statements_with_sorting ORDER BY avg_sort_merges DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 scans queries with sort"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), sorts_using_scans AS search from sys.x\\$statements_with_sorting ORDER BY sorts_using_scans DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 range queries with sort"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), sort_using_range AS search from sys.x\\$statements_with_sorting ORDER BY sort_using_range DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - -################################################################################## - - #statements_with_temp_tables - -#mysql> desc statements_with_temp_tables; -#+--------------------------+---------------------+------+-----+---------------------+-------+ -#| Field | Type | Null | Key | Default | Extra | -#+--------------------------+---------------------+------+-----+---------------------+-------+ -#| query | longtext | YES | | NULL | | -#| db | varchar(64) | YES | | NULL | | -#| exec_count | bigint(20) unsigned | NO | | NULL | | -#| total_latency | text | YES | | NULL | | -#| memory_tmp_tables | bigint(20) unsigned | NO | | NULL | | -#| disk_tmp_tables | bigint(20) unsigned | NO | | NULL | | -#| avg_tmp_tables_per_query | decimal(21,0) | NO | | 0 | | -#| tmp_tables_to_disk_pct | decimal(24,0) | NO | | 0 | | -#| first_seen | timestamp | NO | | 0000-00-00 00:00:00 | | -#| last_seen | timestamp | NO | | 0000-00-00 00:00:00 | | -#| digest | varchar(32) | YES | | NULL | | -#+--------------------------+---------------------+------+-----+---------------------+-------+ -#11 rows in set (0,01 sec)# -# - subheaderprint "Performance schema: Top 20 queries with temp table"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), exec_count from sys.x\\$statements_with_temp_tables order BY exec_count DESC LIMIT 20;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Last 50 queries with temp table"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), last_seen from sys.x\\$statements_with_temp_tables order BY last_seen DESC LIMIT 50;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint - "Performance schema: Top 15 total latency queries with temp table"; - $nbL = 1; - for my $lQuery ( - select_array( -'select db, LEFT(query, 120), total_latency AS search from sys.x\\$statements_with_temp_tables ORDER BY total_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 queries with temp table to disk"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select db, LEFT(query, 120), disk_tmp_tables from sys.x\\$statements_with_temp_tables ORDER BY disk_tmp_tables DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - -################################################################################## - #wait_classes_global_by_latency - -#mysql> select * from wait_classes_global_by_latency; -#-----------------+-------+---------------+-------------+-------------+-------------+ -# event_class | total | total_latency | min_latency | avg_latency | max_latency | -#-----------------+-------+---------------+-------------+-------------+-------------+ -# wait/io/file | 15381 | 1.23 s | 0 ps | 80.12 us | 230.64 ms | -# wait/io/table | 59 | 7.57 ms | 5.45 us | 128.24 us | 3.95 ms | -# wait/lock/table | 69 | 3.22 ms | 658.84 ns | 46.64 us | 1.10 ms | -#-----------------+-------+---------------+-------------+-------------+-------------+ -# rows in set (0,00 sec) - - subheaderprint "Performance schema: Top 15 class events by number"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select event_class, total from sys.x\\$wait_classes_global_by_latency ORDER BY total DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 30 events by number"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select events, total from sys.x\\$waits_global_by_latency ORDER BY total DESC LIMIT 30;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 class events by total latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select event_class, total_latency from sys.x\\$wait_classes_global_by_latency ORDER BY total_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 30 events by total latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'use sys;select events, total_latency from sys.x\\$waits_global_by_latency ORDER BY total_latency DESC LIMIT 30;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 15 class events by max latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select event_class, max_latency from sys.x\\$wait_classes_global_by_latency ORDER BY max_latency DESC LIMIT 15;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - - subheaderprint "Performance schema: Top 30 events by max latency"; - $nbL = 1; - for my $lQuery ( - select_array( -'select events, max_latency from sys.x\\$waits_global_by_latency ORDER BY max_latency DESC LIMIT 30;' - ) - ) - { - infoprint " +-- $nbL: $lQuery"; - $nbL++; - } - infoprint "No information found or indicators deactivated." - if ( $nbL == 1 ); - -} - -# Recommendations for Aria Engine -sub mariadb_aria { - subheaderprint "Aria Metrics"; - - # Aria - if ( !defined $myvar{'have_aria'} ) { - infoprint "Aria Storage Engine not available."; - return; - } - if ( $myvar{'have_aria'} ne "YES" ) { - infoprint "Aria Storage Engine is disabled."; - return; - } - infoprint "Aria Storage Engine is enabled."; - - # Aria pagecache - if ( !defined( $mycalc{'total_aria_indexes'} ) ) { - push( @generalrec, - "Unable to calculate Aria index size on MySQL server" ); - } - else { - if ( - $myvar{'aria_pagecache_buffer_size'} < $mycalc{'total_aria_indexes'} - && $mycalc{'pct_aria_keys_from_mem'} < 95 ) - { - badprint "Aria pagecache size / total Aria indexes: " - . hr_bytes( $myvar{'aria_pagecache_buffer_size'} ) . "/" - . hr_bytes( $mycalc{'total_aria_indexes'} ) . ""; - push( @adjvars, - "aria_pagecache_buffer_size (> " - . hr_bytes( $mycalc{'total_aria_indexes'} ) - . ")" ); - } - else { - goodprint "Aria pagecache size / total Aria indexes: " - . hr_bytes( $myvar{'aria_pagecache_buffer_size'} ) . "/" - . hr_bytes( $mycalc{'total_aria_indexes'} ) . ""; - } - if ( $mystat{'Aria_pagecache_read_requests'} > 0 ) { - if ( $mycalc{'pct_aria_keys_from_mem'} < 95 ) { - badprint -"Aria pagecache hit rate: $mycalc{'pct_aria_keys_from_mem'}% (" - . hr_num( $mystat{'Aria_pagecache_read_requests'} ) - . " cached / " - . hr_num( $mystat{'Aria_pagecache_reads'} ) - . " reads)"; - } - else { - goodprint -"Aria pagecache hit rate: $mycalc{'pct_aria_keys_from_mem'}% (" - . hr_num( $mystat{'Aria_pagecache_read_requests'} ) - . " cached / " - . hr_num( $mystat{'Aria_pagecache_reads'} ) - . " reads)"; - } - } - else { - - # No queries have run that would use keys - } - } -} - -# Recommendations for TokuDB -sub mariadb_tokudb { - subheaderprint "TokuDB Metrics"; - - # AriaDB - unless ( defined $myvar{'have_tokudb'} - && $myvar{'have_tokudb'} eq "YES" ) - { - infoprint "TokuDB is disabled."; - return; - } - infoprint "TokuDB is enabled."; - - # Not implemented -} - -# Recommendations for XtraDB -sub mariadb_xtradb { - subheaderprint "XtraDB Metrics"; - - # XtraDB - unless ( defined $myvar{'have_xtradb'} - && $myvar{'have_xtradb'} eq "YES" ) - { - infoprint "XtraDB is disabled."; - return; - } - infoprint "XtraDB is enabled."; - infoprint "Note that MariaDB 10.2 makes use of InnoDB, not XtraDB." - - # Not implemented -} - -# Recommendations for RocksDB -sub mariadb_rockdb { - subheaderprint "RocksDB Metrics"; - - # RocksDB - unless ( defined $myvar{'have_rocksdb'} - && $myvar{'have_rocksdb'} eq "YES" ) - { - infoprint "RocksDB is disabled."; - return; - } - infoprint "RocksDB is enabled."; - - # Not implemented -} - -# Recommendations for Spider -sub mariadb_spider { - subheaderprint "Spider Metrics"; - - # Spider - unless ( defined $myvar{'have_spider'} - && $myvar{'have_spider'} eq "YES" ) - { - infoprint "Spider is disabled."; - return; - } - infoprint "Spider is enabled."; - - # Not implemented -} - -# Recommendations for Connect -sub mariadb_connect { - subheaderprint "Connect Metrics"; - - # Connect - unless ( defined $myvar{'have_connect'} - && $myvar{'have_connect'} eq "YES" ) - { - infoprint "Connect is disabled."; - return; - } - infoprint "Connect is enabled."; - - # Not implemented -} - -# Perl trim function to remove whitespace from the start and end of the string -sub trim { - my $string = shift; - return "" unless defined($string); - $string =~ s/^\s+//; - $string =~ s/\s+$//; - return $string; -} - -sub get_wsrep_options { - return () unless defined $myvar{'wsrep_provider_options'}; - - my @galera_options = split /;/, $myvar{'wsrep_provider_options'}; - @galera_options = remove_cr @galera_options; - @galera_options = remove_empty @galera_options; - - #debugprint Dumper( \@galera_options ) if $opt{debug}; - return @galera_options; -} - -sub get_gcache_memory { - my $gCacheMem = hr_raw( get_wsrep_option('gcache.size') ); - - return 0 unless defined $gCacheMem and $gCacheMem ne ''; - return $gCacheMem; -} - -sub get_wsrep_option { - my $key = shift; - return '' unless defined $myvar{'wsrep_provider_options'}; - my @galera_options = get_wsrep_options; - return '' unless scalar(@galera_options) > 0; - my @memValues = grep /\s*$key =/, @galera_options; - my $memValue = $memValues[0]; - return 0 unless defined $memValue; - $memValue =~ s/.*=\s*(.+)$/$1/g; - return $memValue; -} - -# REcommendations for Tables -sub mysql_table_structures { - return 0 unless ( $opt{structstat} > 0 ); - subheaderprint "Table structures analysis"; - - my @primaryKeysNbTables = select_array( - "Select CONCAT(c.table_schema, ',' , c.table_name) -from information_schema.columns c -join information_schema.tables t using (TABLE_SCHEMA, TABLE_NAME) -where c.table_schema not in ('sys', 'mysql', 'information_schema', 'performance_schema') - and t.table_type = 'BASE TABLE' -group by c.table_schema,c.table_name -having sum(if(c.column_key in ('PRI', 'UNI'), 1, 0)) = 0" - ); - - my $tmpContent = 'Schema,Table'; - if ( scalar(@primaryKeysNbTables) > 0 ) { - badprint "Following table(s) don't have primary key:"; - foreach my $badtable (@primaryKeysNbTables) { - badprint "\t$badtable"; - push @{ $result{'Tables without PK'} }, $badtable; - $tmpContent .= "\n$badtable"; - } - push @generalrec, -"Ensure that all table(s) get an explicit primary keys for performance, maintenance and also for replication"; - push @modeling, "Following table(s) don't have primary key: " - . join( ', ', @primaryKeysNbTables ); - - } - else { - goodprint "All tables get a primary key"; - } - dump_into_file( "tables_without_primary_keys.csv", $tmpContent ); - - # Advanced PK checks - my @pkInfo = select_array( -"SELECT c.TABLE_SCHEMA, c.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, c.COLUMN_TYPE -FROM information_schema.columns c -JOIN information_schema.tables t USING (TABLE_SCHEMA, TABLE_NAME) -WHERE t.TABLE_TYPE = 'BASE TABLE' - AND c.COLUMN_KEY = 'PRI' - AND c.TABLE_SCHEMA NOT IN ('sys', 'mysql', 'information_schema', 'performance_schema')" - ); - - foreach my $pk (@pkInfo) { - my ( $schema, $table, $column, $datatype, $columntype ) = split /\t/, - $pk; - $schema //= ''; - $table //= ''; - $column //= ''; - $datatype //= ''; - $columntype //= ''; - - # PK Naming Convention - if ( $column ne 'id' && $column ne "${table}_id" ) { - badprint -"Table $schema.$table: Primary key '$column' does not follow 'id' or '${table}_id' naming convention"; - push @generalrec, -"Use 'id' or '${table}_id' for Primary Key naming in $schema.$table"; - push @modeling, -"Table $schema.$table: Primary key '$column' does not follow naming convention (id or ${table}_id)"; - } - - # Surrogate Key Recommendation - if ( $datatype !~ /int/i - || $columntype !~ /unsigned/i - || $columntype !~ /auto_increment/i ) - { - # Check if it might be a UUID - if ( $column =~ /uuid/i ) { - if ( $datatype !~ /binary/i || $columntype !~ /16/ ) { - badprint -"Table $schema.$table: UUID primary key '$column' is not optimized (use BINARY(16))"; - push @generalrec, -"Use optimized BINARY(16) for UUID Primary Keys in $schema.$table"; - push @modeling, -"Table $schema.$table: UUID primary key '$column' is not optimized (use BINARY(16))"; - } - } - else { - badprint -"Table $schema.$table: Primary key '$column' is not a recommended surrogate key (BIGINT UNSIGNED AUTO_INCREMENT)"; - push @generalrec, -"Use BIGINT UNSIGNED AUTO_INCREMENT for Primary Keys in $schema.$table"; - push @modeling, -"Table $schema.$table: Primary key '$column' is not a recommended surrogate key (BIGINT UNSIGNED AUTO_INCREMENT)"; - } - } - } - - # Large Tables (>1GB) without Secondary Indexes - my @largeTablesWithoutIndexes = select_array( - "SELECT TABLE_SCHEMA, TABLE_NAME, (DATA_LENGTH + INDEX_LENGTH) -FROM information_schema.tables t -WHERE TABLE_TYPE = 'BASE TABLE' - AND (DATA_LENGTH + INDEX_LENGTH) > 1024*1024*1024 - AND (SELECT COUNT(*) FROM information_schema.statistics s WHERE s.TABLE_SCHEMA = t.TABLE_SCHEMA AND s.TABLE_NAME = t.TABLE_NAME AND s.INDEX_NAME != 'PRIMARY') = 0 - AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - if (@largeTablesWithoutIndexes) { - foreach my $lt (@largeTablesWithoutIndexes) { - my ( $schema, $table, $size ) = split /\t/, $lt; - $schema //= ''; - $table //= ''; - $size //= 0; - badprint "Table $schema.$table is large (" - . hr_bytes($size) - . ") and has no secondary indexes"; - push @generalrec, -"Add secondary indexes to large table $schema.$table to improve query performance"; - push @modeling, - "Table $schema.$table is large (" - . hr_bytes($size) - . ") and has no secondary indexes"; - } - } - - # Foreign Key Type Mismatches - my @fkMismatches = select_array( -"SELECT CONCAT(k.TABLE_SCHEMA, '.', k.TABLE_NAME, ' (', k.COLUMN_NAME, ': ', c1.COLUMN_TYPE, ') -> ', k.REFERENCED_TABLE_SCHEMA, '.', k.REFERENCED_TABLE_NAME, ' (', k.REFERENCED_COLUMN_NAME, ': ', c2.COLUMN_TYPE, ')') -FROM information_schema.KEY_COLUMN_USAGE k -JOIN information_schema.COLUMNS c1 ON k.TABLE_SCHEMA = c1.TABLE_SCHEMA AND k.TABLE_NAME = c1.TABLE_NAME AND k.COLUMN_NAME = c1.COLUMN_NAME -JOIN information_schema.COLUMNS c2 ON k.REFERENCED_TABLE_SCHEMA = c2.TABLE_SCHEMA AND k.REFERENCED_TABLE_NAME = c2.TABLE_NAME AND k.REFERENCED_COLUMN_NAME = c2.COLUMN_NAME -WHERE k.REFERENCED_TABLE_NAME IS NOT NULL - AND (c1.DATA_TYPE != c2.DATA_TYPE OR c1.COLUMN_TYPE != c2.COLUMN_TYPE) - AND k.TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - if (@fkMismatches) { - badprint "Following Foreign Key(s) have data type mismatches:"; - foreach my $fk (@fkMismatches) { - badprint "\t$fk"; - push @generalrec, "Fix data type mismatch for Foreign Key: $fk"; - push @modeling, "Foreign Key type mismatch: $fk"; - } - } - - my @nonInnoDBTables = select_array( - "select table_schema, table_name, ENGINE -FROM information_schema.tables t -WHERE ENGINE <> 'InnoDB' -and t.table_type = 'BASE TABLE' -and table_schema not in -('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - $tmpContent = 'Schema,Table,Engine'; - if ( scalar(@nonInnoDBTables) > 0 ) { - badprint "Following table(s) are not InnoDB table:"; - push @generalrec, -"Ensure that all table(s) are InnoDB tables for performance and also for replication"; - push @modeling, "Following table(s) are not InnoDB: " - . join( ', ', @nonInnoDBTables ); - foreach my $badtable (@nonInnoDBTables) { - if ( $badtable =~ /Memory/i ) { - badprint -"Table $badtable is a MEMORY table. It's suggested to use only InnoDB tables in production"; - } - else { - badprint "\t$badtable"; - } - $tmpContent .= "\n$badtable"; - } - } - else { - goodprint "All tables are InnoDB tables"; - } - dump_into_file( "tables_non_innodb.csv", $tmpContent ); - - my @nonutf8columns = select_array( -"SELECT CONCAT(table_schema, ',', table_name, ',', column_name, ',', CHARacter_set_name, ',', COLLATION_name, ',', data_type, ',', CHARACTER_MAXIMUM_LENGTH) -from information_schema.columns -WHERE table_schema not in ('sys', 'mysql', 'performance_schema', 'information_schema') -and (CHARacter_set_name NOT LIKE 'utf8%' -or COLLATION_name NOT LIKE 'utf8%');" - ); - $tmpContent = - 'Schema,Table,Column, Charset, Collation, Data Type, Max Length'; - if ( scalar(@nonutf8columns) > 0 ) { - badprint "Following character columns(s) are not utf8 compliant:"; - push @generalrec, -"Ensure that all text colums(s) are UTF-8 compliant for encoding support and performance"; - push @modeling, "Following collection(s) are not UTF-8 compliant: " - . join( ', ', @nonutf8columns ); - foreach my $badtable (@nonutf8columns) { - badprint "\t$badtable"; - $tmpContent .= "\n$badtable"; - } - } - else { - goodprint "All columns are UTF-8 compliant"; - } - dump_into_file( "columns_non_utf8.csv", $tmpContent ); - - my @utf8columns = select_array( -"SELECT CONCAT(table_schema, ',', table_name, ',', column_name, ',', CHARacter_set_name, ',', COLLATION_name, ',', data_type, ',', CHARACTER_MAXIMUM_LENGTH) -from information_schema.columns -WHERE table_schema not in ('sys', 'mysql', 'performance_schema', 'information_schema') -and (CHARacter_set_name LIKE 'utf8%' -or COLLATION_name LIKE 'utf8%');" - ); - $tmpContent = - 'Schema,Table,Column, Charset, Collation, Data Type, Max Length'; - foreach my $badtable (@utf8columns) { - $tmpContent .= "\n$badtable"; - } - dump_into_file( "columns_utf8.csv", $tmpContent ); - - my @ftcolumns = select_array( -"SELECT CONCAT(table_schema, ',', table_name, ',', column_name, ',', data_type) -from information_schema.columns -WHERE table_schema not in ('sys', 'mysql', 'performance_schema', 'information_schema') -AND data_type='FULLTEXT';" - ); - $tmpContent = 'Schema,Table,Column, Data Type'; - foreach my $ctable (@ftcolumns) { - $tmpContent .= "\n$ctable"; - } - dump_into_file( "fulltext_columns.csv", $tmpContent ); - - mysql_naming_conventions(); - mysql_foreign_key_checks(); - mysql_80_modeling_checks(); - mysql_datatype_optimization(); - mysql_schema_sanitization(); -} - -sub mysql_80_modeling_checks { - return unless mysql_version_ge( 8, 0 ); - - my $is_mariadb = ( - ( $myvar{'version'} =~ /MariaDB/i ) - or ( $myvar{'version_comment'} =~ /MariaDB/i ) - ); - my $header = - $is_mariadb - ? "MariaDB 10.x+ Specific Modeling" - : "MySQL 8.0+ Specific Modeling"; - subheaderprint $header; - - my $modeling80Count = 0; - - # JSON indexability - my @jsonColumns = select_array( -"SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME FROM information_schema.columns WHERE DATA_TYPE = 'json' AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - foreach my $jc (@jsonColumns) { - my ( $schema, $table, $column ) = split /\t/, $jc; - $schema //= ''; - $table //= ''; - $column //= ''; - - # Check if there are generated columns for this table - my @genCols = select_array( -"SELECT COLUMN_NAME FROM information_schema.columns WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND EXTRA LIKE '%VIRTUAL%'" - ); - if ( scalar(@genCols) == 0 ) { - infoprint -"Table $schema.$table: JSON column '$column' detected without Virtual Generated Columns for indexing"; - push @generalrec, -"Consider using Generated Columns to index frequently searched attributes in JSON column $schema.$table.$column"; - push @modeling, -"Table $schema.$table: JSON column '$column' detected without Virtual Generated Columns for indexing"; - $modeling80Count++; - } - } - - # Invisible Indexes (MySQL: IS_VISIBLE='NO', MariaDB: IGNORED='YES') - my $visible_col = $is_mariadb ? "IGNORED" : "IS_VISIBLE"; - my $visible_val = $is_mariadb ? "'YES'" : "'NO'"; - - my @invisibleIdx = select_array( -"SELECT TABLE_SCHEMA, TABLE_NAME, INDEX_NAME FROM information_schema.statistics WHERE $visible_col = $visible_val AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - foreach my $ii (@invisibleIdx) { - my ( $schema, $table, $index ) = split /\t/, $ii; - $schema //= ''; - $table //= ''; - $index //= ''; - infoprint "Index $schema.$table.$index is INVISIBLE"; - push @modeling, "Index $schema.$table.$index is INVISIBLE"; - $modeling80Count++; - } - - # Check Constraints - if ( mysql_version_ge( 8, 0, 16 ) ) { - my @checkConstraints = select_array( -"SELECT CONSTRAINT_SCHEMA, TABLE_NAME, CONSTRAINT_NAME FROM information_schema.table_constraints WHERE CONSTRAINT_TYPE = 'CHECK' AND CONSTRAINT_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - if ( scalar(@checkConstraints) == 0 ) { - -# Maybe too noisy to always suggest, but it's a good practice -# infoprint "No CHECK constraints detected; consider using them for data integrity"; - } - else { - # We skip counting these as we don't badprint/infoprint them for now - } - } - goodprint "No MySQL 8.0+ specific modeling issues found" - if $modeling80Count == 0; -} - -sub mysql_datatype_optimization { - subheaderprint "Data Type optimization"; - - # NULLability - my @nullableCols = select_array( -"SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME FROM information_schema.columns WHERE IS_NULLABLE = 'YES' AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - my $nullableCount = scalar(@nullableCols); - if ( $nullableCount > 20 ) { - infoprint -"There are $nullableCount columns with NULL enabled. Consider using NOT NULL where possible for better performance."; - push @modeling, -"There are $nullableCount columns with NULL enabled. Consider using NOT NULL where possible for better performance."; - } - else { - goodprint "No data type optimization recommendations"; - } - - # BIGINT vs INT - # This is a bit hard to check without looking at table rows and max values -} - -sub mysql_naming_conventions { - subheaderprint "Naming conventions analysis"; - - my $namingIssues = 0; - - # Table Naming - my @tables = select_array( -"SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.tables WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - foreach my $t (@tables) { - my ( $schema, $table ) = split /\t/, $t; - $schema //= ''; - $table //= ''; - - # Plural check (very basic: ends with 's' but not 'ss') - if ( ( $table // '' ) =~ /[^s]s$/i - && ( $table // '' ) !~ /status|address|glass|process/i ) - { - badprint - "Table $schema.$table: Plural name detected (prefer singular)"; - push @generalrec, "Use singular names for table $schema.$table"; - push @modeling, - "Table $schema.$table: Plural name detected (prefer singular)"; - $namingIssues++; - } - - # Casing check (detect CamelCase/PascalCase) - if ( ( $table // '' ) =~ /[a-z][A-Z]/ ) { - badprint "Table $schema.$table: Non-snake_case name detected"; - push @generalrec, "Use snake_case for table $schema.$table"; - push @modeling, - "Table $schema.$table: Non-snake_case name detected"; - $namingIssues++; - } - } - - # Column Naming - my @columns = select_array( -"SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.columns WHERE TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - foreach my $c (@columns) { - my ( $schema, $table, $column, $datatype ) = split /\t/, $c; - $schema //= ''; - $table //= ''; - $column //= ''; - $datatype //= ''; - - # Casing check - if ( ( $column // '' ) =~ /[a-z][A-Z]/ ) { - badprint - "Column $schema.$table.$column: Non-snake_case name detected"; - push @generalrec, - "Use snake_case for column $schema.$table.$column"; - push @modeling, - "Column $schema.$table.$column: Non-snake_case name detected"; - $namingIssues++; - } - - # Boolean naming - if ( ( $datatype // '' ) =~ /tinyint\(1\)|bool/i ) { - if ( ( $column // '' ) !~ /^(is_|has_|was_|had_)/ ) { - infoprint -"Column $schema.$table.$column: Boolean-like column missing verbal prefix (is_, has_, etc.)"; - push @modeling, -"Column $schema.$table.$column: Boolean-like column missing verbal prefix (is_, has_, etc.)"; - - # Not a badprint as it's a recommendation - $namingIssues++; - } - } - - # Date naming - if ( ( $datatype // '' ) =~ /date|time/i ) { - if ( ( $column // '' ) !~ /(_at|_date|_time)$/ ) { - infoprint -"Column $schema.$table.$column: Date/Time column missing explicit suffix (_at, _date, _time)"; - push @modeling, -"Column $schema.$table.$column: Date/Time column missing explicit suffix (_at, _date, _time)"; - $namingIssues++; - } - } - } - goodprint "No naming convention issues found" if $namingIssues == 0; -} - -sub mysql_foreign_key_checks { - subheaderprint "Foreign Key analysis"; - - my $fkIssues = 0; - - # Unconstrained _id columns - my @unconstrainedId = select_array( - "SELECT c.TABLE_SCHEMA, c.TABLE_NAME, c.COLUMN_NAME -FROM information_schema.columns c -LEFT JOIN information_schema.key_column_usage k ON c.TABLE_SCHEMA = k.TABLE_SCHEMA AND c.TABLE_NAME = k.TABLE_NAME AND c.COLUMN_NAME = k.COLUMN_NAME AND k.REFERENCED_TABLE_NAME IS NOT NULL -WHERE c.COLUMN_NAME LIKE '%_id' - AND k.COLUMN_NAME IS NULL - AND c.TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - foreach my $id (@unconstrainedId) { - my ( $schema, $table, $column ) = split /\t/, $id; - $schema //= ''; - $table //= ''; - $column //= ''; - - # Exclude PKs that are named table_id - next if $column eq "${table}_id"; - - badprint -"Column $schema.$table.$column ends in '_id' but has no FOREIGN KEY constraint"; - push @generalrec, - "Add FOREIGN KEY constraint to $schema.$table.$column"; - push @modeling, -"Column $schema.$table.$column ends in '_id' but has no FOREIGN KEY constraint"; - $fkIssues++; - } - - # FK Actions - my @fkActions = select_array( -"SELECT rc.CONSTRAINT_SCHEMA, rc.TABLE_NAME, k.COLUMN_NAME, rc.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME, rc.DELETE_RULE -FROM information_schema.referential_constraints rc -JOIN information_schema.key_column_usage k ON rc.CONSTRAINT_SCHEMA = k.CONSTRAINT_SCHEMA AND rc.CONSTRAINT_NAME = k.CONSTRAINT_NAME -WHERE rc.CONSTRAINT_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - foreach my $fk (@fkActions) { - my ( $schema, $table, $column, $ref_table, $ref_column, $delete_rule ) - = split /\t/, $fk; - $schema //= ''; - $table //= ''; - $column //= ''; - $ref_table //= ''; - $ref_column //= ''; - $delete_rule //= ''; - if ( $delete_rule eq 'CASCADE' ) { - infoprint -"Constraint on $schema.$table.$column uses ON DELETE CASCADE; ensure this is intended."; - push @modeling, -"Constraint on $schema.$table.$column uses ON DELETE CASCADE; ensure this is intended."; - $fkIssues++; - } - } - - # Foreign Key Type Mismatches - my @fkTypeMismatches = select_array( -"SELECT k.CONSTRAINT_SCHEMA, k.TABLE_NAME, k.COLUMN_NAME, c1.COLUMN_TYPE, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME, c2.COLUMN_TYPE -FROM information_schema.key_column_usage k -JOIN information_schema.columns c1 ON k.TABLE_SCHEMA = c1.TABLE_SCHEMA AND k.TABLE_NAME = c1.TABLE_NAME AND k.COLUMN_NAME = c1.COLUMN_NAME -JOIN information_schema.columns c2 ON k.REFERENCED_TABLE_SCHEMA = c2.TABLE_SCHEMA AND k.REFERENCED_TABLE_NAME = c2.TABLE_NAME AND k.REFERENCED_COLUMN_NAME = c2.COLUMN_NAME -WHERE k.REFERENCED_TABLE_NAME IS NOT NULL - AND c1.COLUMN_TYPE != c2.COLUMN_TYPE - AND k.CONSTRAINT_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema')" - ); - foreach my $mm (@fkTypeMismatches) { - my ( $schema, $table, $col, $type1, $ref_table, $ref_col, $type2 ) = - split /\t/, $mm; - $schema //= ''; - $table //= ''; - $col //= ''; - $type1 //= ''; - $ref_table //= ''; - $ref_col //= ''; - $type2 //= ''; - badprint -"FK Type Mismatch: $schema.$table.$col ($type1) -> $ref_table.$ref_col ($type2)"; - push @generalrec, -"Fix data type mismatch in Foreign Key $schema.$table.$col ($type1 vs $type2)"; - push @modeling, -"FK Type Mismatch: $schema.$table.$col ($type1) references $ref_table.$ref_col ($type2)"; - $fkIssues++; - } - goodprint "No foreign key issues found" if $fkIssues == 0; -} - -sub mysql_schema_sanitization { - subheaderprint "Schema sanitization"; - - my @emptyOrViewOnlySchemas = select_array( - "SELECT TABLE_SCHEMA, - SUM(CASE WHEN TABLE_TYPE = 'BASE TABLE' THEN 1 ELSE 0 END), - SUM(CASE WHEN TABLE_TYPE = 'VIEW' THEN 1 ELSE 0 END) -FROM information_schema.tables -WHERE TABLE_SCHEMA NOT IN ('sys', 'mysql', 'performance_schema', 'information_schema') -GROUP BY TABLE_SCHEMA -HAVING SUM(CASE WHEN TABLE_TYPE = 'BASE TABLE' THEN 1 ELSE 0 END) = 0" - ); - - if ( scalar(@emptyOrViewOnlySchemas) == 0 ) { - goodprint "No empty or view-only schemas detected"; - } - else { - foreach my $s (@emptyOrViewOnlySchemas) { - my ( $schema, $tables, $views ) = split /\t/, $s; - $schema //= ''; - $tables //= 0; - $views //= 0; - if ( $tables == 0 && $views == 0 ) { - infoprint "Schema $schema is empty (no tables or views)"; - push @modeling, "Schema $schema is empty (no tables or views)"; - } - elsif ( $tables == 0 && $views > 0 ) { - infoprint "Schema $schema contains only views ($views views)"; - push @modeling, - "Schema $schema contains only views ($views views)"; - } - } - } -} - -# Recommendations for Galera -sub mariadb_galera { - subheaderprint "Galera Metrics"; - - # Galera Cluster - unless ( defined $myvar{'have_galera'} - && $myvar{'have_galera'} eq "YES" ) - { - infoprint "Galera is disabled."; - return; - } - infoprint "Galera is enabled."; - debugprint "Galera variables:"; - foreach my $gvar ( keys %myvar ) { - next unless $gvar =~ /^wsrep.*/; - next if $gvar eq 'wsrep_provider_options'; - debugprint "\t" . trim($gvar) . " = " . $myvar{$gvar}; - $result{'Galera'}{'variables'}{$gvar} = $myvar{$gvar}; - } - if ( not defined( $myvar{'wsrep_on'} ) or $myvar{'wsrep_on'} ne "ON" ) { - infoprint "Galera is disabled."; - return; - } - debugprint "Galera wsrep provider Options:"; - my @galera_options = get_wsrep_options; - $result{'Galera'}{'wsrep options'} = get_wsrep_options(); - foreach my $gparam (@galera_options) { - debugprint "\t" . trim($gparam); - } - debugprint "Galera status:"; - foreach my $gstatus ( keys %mystat ) { - next unless $gstatus =~ /^wsrep.*/; - debugprint "\t" . trim($gstatus) . " = " . $mystat{$gstatus}; - $result{'Galera'}{'status'}{$gstatus} = $myvar{$gstatus}; - } - infoprint "GCache is using " - . hr_bytes_rnd( get_wsrep_option('gcache.mem_size') ); - - infoprint "CPU cores detected : " . (cpu_cores); - my $wsrep_threads_var_name = 'wsrep_slave_threads'; - if ( defined( $myvar{'wsrep_applier_threads'} ) ) { - $wsrep_threads_var_name = 'wsrep_applier_threads'; - } - - # Use 1 as a fallback if $myvar{$wsrep_threads_var_name} is undefined or zero, - # to ensure there is at least one thread for Galera replication. - my $wsrep_threads_value = $myvar{$wsrep_threads_var_name} || 1; - - infoprint "$wsrep_threads_var_name: " . $wsrep_threads_value; - - if ( $wsrep_threads_value > ( (cpu_cores) * 4 ) - or $wsrep_threads_value < ( (cpu_cores) * 2 ) ) - { - badprint -"$wsrep_threads_var_name is not equal to 2, 3 or 4 times the number of CPU(s)"; - push @adjvars, "$wsrep_threads_var_name = " . ( (cpu_cores) * 4 ); - } - else { - goodprint -"$wsrep_threads_var_name is equal to 2, 3 or 4 times the number of CPU(s)"; - } - - if ( $wsrep_threads_value > 1 ) { - infoprint - "wsrep parallel slave can cause frequent inconsistency crash."; - push @adjvars, -"Set $wsrep_threads_var_name to 1 in case of HA_ERR_FOUND_DUPP_KEY crash on slave"; - - # check options for parallel slave - if ( get_wsrep_option('wsrep_slave_FK_checks') eq "OFF" ) { - badprint "wsrep_slave_FK_checks is off with parallel slave"; - push @adjvars, - "wsrep_slave_FK_checks should be ON when using parallel slave"; - } - - # wsrep_slave_UK_checks seems useless in MySQL source code - if ( $myvar{'innodb_autoinc_lock_mode'} != 2 ) { - badprint - "innodb_autoinc_lock_mode is incorrect with parallel slave"; - push @adjvars, - "innodb_autoinc_lock_mode should be 2 when using parallel slave"; - } - } - - if ( get_wsrep_option('gcs.fc_limit') != $wsrep_threads_value * 5 ) { - badprint - "gcs.fc_limit should be equal to 5 * $wsrep_threads_var_name (=" - . ( $wsrep_threads_value * 5 ) . ")"; - push @adjvars, "gcs.fc_limit= $wsrep_threads_var_name * 5 (=" - . ( $wsrep_threads_value * 5 ) . ")"; - } - else { - goodprint "gcs.fc_limit is equal to 5 * $wsrep_threads_var_name ( =" - . get_wsrep_option('gcs.fc_limit') . ")"; - } - - if ( get_wsrep_option('gcs.fc_factor') != 0.8 ) { - badprint "gcs.fc_factor should be equal to 0.8 (=" - . get_wsrep_option('gcs.fc_factor') . ")"; - push @adjvars, "gcs.fc_factor=0.8"; - } - else { - goodprint "gcs.fc_factor is equal to 0.8"; - } - if ( get_wsrep_option('wsrep_flow_control_paused') > 0.02 ) { - badprint "Fraction of time node pause flow control > 0.02"; - } - else { - goodprint -"Flow control fraction seems to be OK (wsrep_flow_control_paused <= 0.02)"; - } - - if ( $myvar{'binlog_format'} ne 'ROW' ) { - badprint "Binlog format should be in ROW mode."; - push @adjvars, "binlog_format = ROW"; - } - else { - goodprint "Binlog format is in ROW mode."; - } - if ( $myvar{'innodb_flush_log_at_trx_commit'} != 0 ) { - badprint "InnoDB flush log at each commit should be disabled."; - push @adjvars, "innodb_flush_log_at_trx_commit = 0"; - } - else { - goodprint "InnoDB flush log at each commit is disabled for Galera."; - } - - if ( defined $myvar{'wsrep_causal_reads'} - and $myvar{'wsrep_causal_reads'} ne '' ) - { - infoprint "Read consistency mode :" . $myvar{'wsrep_causal_reads'}; - } - elsif ( defined $myvar{'wsrep_sync_wait'} ) { - infoprint "Sync Wait mode : " . $myvar{'wsrep_sync_wait'}; - } - - if ( defined( $myvar{'wsrep_cluster_name'} ) - and $myvar{'wsrep_on'} eq "ON" ) - { - goodprint "Galera WsREP is enabled."; - if ( defined( $myvar{'wsrep_cluster_address'} ) - and trim("$myvar{'wsrep_cluster_address'}") ne "" ) - { - goodprint "Galera Cluster address is defined: " - . $myvar{'wsrep_cluster_address'}; - my @NodesTmp = split /,/, $myvar{'wsrep_cluster_address'}; - my $nbNodes = @NodesTmp; - infoprint "There are $nbNodes nodes in wsrep_cluster_address"; - my $nbNodesSize = trim( $mystat{'wsrep_cluster_size'} ); - if ( $nbNodesSize == 3 or $nbNodesSize == 5 ) { - goodprint "There are $nbNodesSize nodes in wsrep_cluster_size."; - } - else { - badprint -"There are $nbNodesSize nodes in wsrep_cluster_size. Prefer 3 or 5 nodes architecture."; - push @generalrec, "Prefer 3 or 5 nodes architecture."; - } - - # wsrep_cluster_address doesn't include garbd nodes - if ( $nbNodes > $nbNodesSize ) { - badprint -"All cluster nodes are not detected. wsrep_cluster_size less than node count in wsrep_cluster_address"; - } - else { - goodprint "All cluster nodes detected."; - } - } - else { - badprint "Galera Cluster address is undefined"; - push @adjvars, - "set up wsrep_cluster_address variable for Galera replication"; - } - if ( defined( $myvar{'wsrep_cluster_name'} ) - and trim( $myvar{'wsrep_cluster_name'} ) ne "" ) - { - goodprint "Galera Cluster name is defined: " - . $myvar{'wsrep_cluster_name'}; - } - else { - badprint "Galera Cluster name is undefined"; - push @adjvars, - "set up wsrep_cluster_name variable for Galera replication"; - } - if ( defined( $myvar{'wsrep_node_name'} ) - and trim( $myvar{'wsrep_node_name'} ) ne "" ) - { - goodprint "Galera Node name is defined: " - . $myvar{'wsrep_node_name'}; - } - else { - badprint "Galera node name is undefined"; - push @adjvars, - "set up wsrep_node_name variable for Galera replication"; - } - if ( trim( $myvar{'wsrep_notify_cmd'} ) ne "" ) { - goodprint "Galera Notify command is defined."; - } - else { - badprint "Galera Notify command is not defined."; - push( @adjvars, - "set up parameter wsrep_notify_cmd to be notified" ); - } - if ( trim( $myvar{'wsrep_sst_method'} ) !~ "^xtrabackup.*" - and trim( $myvar{'wsrep_sst_method'} ) !~ "^mariabackup" ) - { - badprint "Galera SST method is not xtrabackup based."; - push( @adjvars, -"set up parameter wsrep_sst_method to xtrabackup based parameter" - ); - } - else { - goodprint "SST Method is based on xtrabackup."; - } - if ( - ( - defined( $myvar{'wsrep_OSU_method'} ) - && trim( $myvar{'wsrep_OSU_method'} ) eq "TOI" - ) - || ( defined( $myvar{'wsrep_osu_method'} ) - && trim( $myvar{'wsrep_osu_method'} ) eq "TOI" ) - ) - { - goodprint "TOI is default mode for upgrade."; - } - else { - badprint "Schema upgrade are not replicated automatically"; - push( @adjvars, "set up parameter wsrep_OSU_method to TOI" ); - } - infoprint "Max WsRep message : " - . hr_bytes( $myvar{'wsrep_max_ws_size'} ); - } - else { - badprint "Galera WsREP is disabled"; - } - - if ( defined( $mystat{'wsrep_connected'} ) - and $mystat{'wsrep_connected'} eq "ON" ) - { - goodprint "Node is connected"; - } - else { - badprint "Node is disconnected"; - } - if ( defined( $mystat{'wsrep_ready'} ) and $mystat{'wsrep_ready'} eq "ON" ) - { - goodprint "Node is ready"; - } - else { - badprint "Node is not ready"; - } - infoprint "Cluster status :" . $mystat{'wsrep_cluster_status'}; - if ( defined( $mystat{'wsrep_cluster_status'} ) - and $mystat{'wsrep_cluster_status'} eq "Primary" ) - { - goodprint "Galera cluster is consistent and ready for operations"; - } - else { - badprint "Cluster is not consistent and ready"; - } - if ( $mystat{'wsrep_local_state_uuid'} eq - $mystat{'wsrep_cluster_state_uuid'} ) - { - goodprint "Node and whole cluster at the same level: " - . $mystat{'wsrep_cluster_state_uuid'}; - } - else { - badprint "Node and whole cluster not the same level"; - infoprint "Node state uuid: " . $mystat{'wsrep_local_state_uuid'}; - infoprint "Cluster state uuid: " . $mystat{'wsrep_cluster_state_uuid'}; - } - if ( $mystat{'wsrep_local_state_comment'} eq 'Synced' ) { - goodprint "Node is synced with whole cluster."; - } - else { - badprint "Node is not synced"; - infoprint "Node State : " . $mystat{'wsrep_local_state_comment'}; - } - if ( $mystat{'wsrep_local_cert_failures'} == 0 ) { - goodprint "There is no certification failures detected."; - } - else { - badprint "There is " - . $mystat{'wsrep_local_cert_failures'} - . " certification failure(s)detected."; - } - - for my $key ( keys %mystat ) { - if ( $key =~ /wsrep_|galera/i ) { - debugprint "WSREP: $key = $mystat{$key}"; - } - } - - #debugprint Dumper get_wsrep_options() if $opt{debug}; -} - -# Recommendations for InnoDB -sub mysql_innodb { - subheaderprint "InnoDB Metrics"; - - # InnoDB - unless ( defined $myvar{'have_innodb'} - && $myvar{'have_innodb'} eq "YES" ) - { - infoprint "InnoDB is disabled."; - if ( mysql_version_ge( 5, 5 ) ) { - my $defengine = 'InnoDB'; - $defengine = $myvar{'default_storage_engine'} - if defined( $myvar{'default_storage_engine'} ); - badprint -"InnoDB Storage engine is disabled. $defengine is the default storage engine" - if $defengine eq 'InnoDB'; - infoprint -"InnoDB Storage engine is disabled. $defengine is the default storage engine" - if $defengine ne 'InnoDB'; - } - return; - } - infoprint "InnoDB is enabled."; - if ( !defined $enginestats{'InnoDB'} ) { - if ( $opt{skipsize} eq 1 ) { - infoprint "Skipped due to --skipsize option"; - return; - } - badprint "No tables are Innodb"; - $enginestats{'InnoDB'} = 0; - } - - if ( $opt{buffers} ne 0 ) { - infoprint "InnoDB Buffers"; - if ( defined $myvar{'innodb_buffer_pool_size'} ) { - infoprint " +-- InnoDB Buffer Pool: " - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) . ""; - } - if ( defined $myvar{'innodb_buffer_pool_instances'} ) { - infoprint " +-- InnoDB Buffer Pool Instances: " - . $myvar{'innodb_buffer_pool_instances'} . ""; - } - - if ( defined $myvar{'innodb_buffer_pool_chunk_size'} ) { - infoprint " +-- InnoDB Buffer Pool Chunk Size: " - . hr_bytes( $myvar{'innodb_buffer_pool_chunk_size'} ) . ""; - } - if ( defined $myvar{'innodb_additional_mem_pool_size'} ) { - infoprint " +-- InnoDB Additional Mem Pool: " - . hr_bytes( $myvar{'innodb_additional_mem_pool_size'} ) . ""; - } - if ( defined $myvar{'innodb_redo_log_capacity'} ) { - infoprint " +-- InnoDB Redo Log Capacity: " - . hr_bytes( $myvar{'innodb_redo_log_capacity'} ); - } - else { - if ( defined $myvar{'innodb_log_file_size'} ) { - infoprint " +-- InnoDB Log File Size: " - . hr_bytes( $myvar{'innodb_log_file_size'} ); - } - if ( defined $myvar{'innodb_log_files_in_group'} ) { - infoprint " +-- InnoDB Log File In Group: " - . $myvar{'innodb_log_files_in_group'}; - infoprint " +-- InnoDB Total Log File Size: " - . hr_bytes( $myvar{'innodb_log_files_in_group'} * - $myvar{'innodb_log_file_size'} ) - . "(" - . $mycalc{'innodb_log_size_pct'} - . " % of buffer pool)"; - } - else { - infoprint " +-- InnoDB Total Log File Size: " - . hr_bytes( $myvar{'innodb_log_file_size'} ) . "(" - . $mycalc{'innodb_log_size_pct'} - . " % of buffer pool)"; - } - } - if ( defined $myvar{'innodb_log_buffer_size'} ) { - infoprint " +-- InnoDB Log Buffer: " - . hr_bytes( $myvar{'innodb_log_buffer_size'} ); - } - if ( defined $mystat{'Innodb_buffer_pool_pages_free'} ) { - infoprint " +-- InnoDB Buffer Free: " - . hr_bytes( $mystat{'Innodb_buffer_pool_pages_free'} ) . ""; - } - if ( defined $mystat{'Innodb_buffer_pool_pages_total'} ) { - infoprint " +-- InnoDB Buffer Used: " - . hr_bytes( $mystat{'Innodb_buffer_pool_pages_total'} ) . ""; - } - } - - if ( defined $myvar{'innodb_thread_concurrency'} ) { - infoprint "InnoDB Thread Concurrency: " - . $myvar{'innodb_thread_concurrency'}; - } - - # InnoDB Buffer Pool Size - if ( $myvar{'innodb_file_per_table'} eq "ON" ) { - goodprint "InnoDB File per table is activated"; - } - else { - badprint "InnoDB File per table is not activated"; - push( @adjvars, "innodb_file_per_table=ON" ); - } - - # InnoDB Buffer Pool Size - if ( $arch == 32 && $myvar{'innodb_buffer_pool_size'} > 4294967295 ) { - badprint - "InnoDB Buffer Pool size limit reached for 32 bits architecture: (" - . hr_bytes(4294967295) . " )"; - push( @adjvars, - "limit innodb_buffer_pool_size under " - . hr_bytes(4294967295) - . " for 32 bits architecture" ); - } - if ( $arch == 32 && $myvar{'innodb_buffer_pool_size'} < 4294967295 ) { - goodprint "InnoDB Buffer Pool size ( " - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) - . " ) under limit for 32 bits architecture: (" - . hr_bytes(4294967295) . ")"; - } - if ( $arch == 64 - && $myvar{'innodb_buffer_pool_size'} > 18446744073709551615 ) - { - badprint "InnoDB Buffer Pool size limit(" - . hr_bytes(18446744073709551615) - . ") reached for 64 bits architecture"; - push( @adjvars, - "limit innodb_buffer_pool_size under " - . hr_bytes(18446744073709551615) - . " for 64 bits architecture" ); - } - - if ( $arch == 64 - && $myvar{'innodb_buffer_pool_size'} < 18446744073709551615 ) - { - goodprint "InnoDB Buffer Pool size ( " - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) - . " ) under limit for 64 bits architecture: (" - . hr_bytes(18446744073709551615) . " )"; - } - if ( $myvar{'innodb_buffer_pool_size'} > $enginestats{'InnoDB'} ) { - goodprint "InnoDB buffer pool / data size: " - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) . " / " - . hr_bytes( $enginestats{'InnoDB'} ) . ""; - } - else { - badprint "InnoDB buffer pool / data size: " - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) . " / " - . hr_bytes( $enginestats{'InnoDB'} ) . ""; - push( @adjvars, - "innodb_buffer_pool_size (>= " - . hr_bytes( $enginestats{'InnoDB'} ) - . ") if possible." ); - } - - # select round( 100* sum(allocated)/( select VARIABLE_VALUE - # FROM information_schema.global_variables - # where VARIABLE_NAME='innodb_buffer_pool_size' ) - # ,2) as "PCT ALLOC/BUFFER POOL" - #from sys.x$innodb_buffer_stats_by_table; - - if ( $opt{experimental} ) { - debugprint( 'innodb_buffer_alloc_pct: "' - . $mycalc{innodb_buffer_alloc_pct} - . '"' ); - if ( defined $mycalc{innodb_buffer_alloc_pct} - and $mycalc{innodb_buffer_alloc_pct} ne '' ) - { - if ( $mycalc{innodb_buffer_alloc_pct} < 80 ) { - badprint "Ratio Buffer Pool allocated / Buffer Pool Size: " - . $mycalc{'innodb_buffer_alloc_pct'} . '%'; - } - else { - goodprint "Ratio Buffer Pool allocated / Buffer Pool Size: " - . $mycalc{'innodb_buffer_alloc_pct'} . '%'; - } - } - } - -# InnoDB Log File Size / InnoDB Redo Log Capacity Recommendations -# For MySQL < 8.0.30, the recommendation is based on innodb_log_file_size and innodb_log_files_in_group. -# For MySQL >= 8.0.30, innodb_redo_log_capacity replaces the old system. - if ( mysql_version_ge( 8, 0, 30 ) - && defined $myvar{'innodb_redo_log_capacity'} ) - { - # Recalculate ratio if needed (ensure accuracy for modern systems) - if ( defined $myvar{'innodb_buffer_pool_size'} - && $myvar{'innodb_buffer_pool_size'} > 0 ) - { - $mycalc{'innodb_log_size_pct'} = - ( $myvar{'innodb_redo_log_capacity'} / - $myvar{'innodb_buffer_pool_size'} ) * 100; - } - - # New recommendation logic for MySQL >= 8.0.30 - infoprint "InnoDB Redo Log Capacity is set to " - . hr_bytes( $myvar{'innodb_redo_log_capacity'} ); - - my $innodb_os_log_written = $mystat{'Innodb_os_log_written'} || 0; - my $uptime = $mystat{'Uptime'} || 1; - - if ( $uptime > 3600 ) - { # Only make a recommendation if server has been up for at least an hour - my $hourly_rate = $innodb_os_log_written / ( $uptime / 3600 ); - my $suggested_redo_log_capacity_str = - hr_bytes_practical_rnd($hourly_rate); - my $suggested_redo_log_capacity_bytes = - hr_raw($suggested_redo_log_capacity_str); - - infoprint "Hourly InnoDB log write rate: " - . hr_bytes_rnd($hourly_rate) . "/hour"; - - if ( hr_raw( $myvar{'innodb_redo_log_capacity'} ) < $hourly_rate ) { - badprint -"Your innodb_redo_log_capacity is not large enough to hold at least 1 hour of writes."; - push( @adjvars, - "innodb_redo_log_capacity (>= " - . $suggested_redo_log_capacity_str - . ")" ); - } - else { - goodprint -"Your innodb_redo_log_capacity is sized to handle more than 1 hour of writes."; - } - - # Sanity check against total InnoDB data size - if ( defined $enginestats{'InnoDB'} and $enginestats{'InnoDB'} > 0 ) - { - my $total_innodb_size = $enginestats{'InnoDB'}; - if ( $suggested_redo_log_capacity_bytes > - $total_innodb_size * 0.25 ) - { - infoprint "The suggested innodb_redo_log_capacity (" - . $suggested_redo_log_capacity_str - . ") is more than 25% of your total InnoDB data size. This might be unnecessarily large."; - } - } - } - else { - infoprint -"Server uptime is less than 1 hour. Cannot make a reliable recommendation for innodb_redo_log_capacity."; - } - } - elsif ( !mysql_version_ge( 8, 0, 30 ) ) { - - # Keep existing logic for older versions - if ( $mycalc{'innodb_log_size_pct'} < 20 - or $mycalc{'innodb_log_size_pct'} > 30 ) - { - if ( defined $myvar{'innodb_redo_log_capacity'} ) { - badprint - "Ratio InnoDB redo log capacity / InnoDB Buffer pool size (" - . $mycalc{'innodb_log_size_pct'} . "%): " - . hr_bytes( $myvar{'innodb_redo_log_capacity'} ) . " / " - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) - . " should be equal to 25%"; - push( @adjvars, - "innodb_redo_log_capacity should be (=" - . hr_bytes_rnd( $myvar{'innodb_buffer_pool_size'} / 4 ) - . ") if possible, so InnoDB Redo log Capacity equals 25% of buffer pool size." - ); - push( @generalrec, -"Be careful, increasing innodb_redo_log_capacity means higher crash recovery mean time" - ); - } - else { - badprint - "Ratio InnoDB log file size / InnoDB Buffer pool size (" - . $mycalc{'innodb_log_size_pct'} . "%): " - . hr_bytes( $myvar{'innodb_log_file_size'} ) . " * " - . $myvar{'innodb_log_files_in_group'} . " / " - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) - . " should be equal to 25%"; - push( - @adjvars, - "innodb_log_file_size should be (=" - . hr_bytes( - ( - defined $myvar{'innodb_buffer_pool_size'} - && $myvar{'innodb_buffer_pool_size'} ne '' - ? $myvar{'innodb_buffer_pool_size'} - : 0 - ) / ( - defined $myvar{'innodb_log_files_in_group'} - && $myvar{'innodb_log_files_in_group'} ne '' - && $myvar{'innodb_log_files_in_group'} != 0 - ? $myvar{'innodb_log_files_in_group'} - : 1 - ) / 4 - ) - . ") if possible, so InnoDB total log file size equals 25% of buffer pool size." - ); - push( @generalrec, -"Be careful, increasing innodb_log_file_size / innodb_log_files_in_group means higher crash recovery mean time" - ); - } - if ( mysql_version_le( 5, 6, 2 ) ) { - push( @generalrec, -"For MySQL 5.6.2 and lower, total innodb_log_file_size should have a ceiling of (4096MB / log files in group) - 1MB." - ); - } - } - else { - if ( defined $myvar{'innodb_redo_log_capacity'} ) { - goodprint - "Ratio InnoDB Redo Log Capacity / InnoDB Buffer pool size: " - . hr_bytes( $myvar{'innodb_redo_log_capacity'} ) . "/" - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) - . " should be equal to 25%"; - } - else { - push( @generalrec, -"Before changing innodb_log_file_size and/or innodb_log_files_in_group read this: https://bit.ly/2TcGgtU" - ); - goodprint - "Ratio InnoDB log file size / InnoDB Buffer pool size: " - . hr_bytes( $myvar{'innodb_log_file_size'} ) . " * " - . $myvar{'innodb_log_files_in_group'} . "/" - . hr_bytes( $myvar{'innodb_buffer_pool_size'} ) - . " should be equal to 25%"; - } - } - } - - # InnoDB Buffer Pool Instances (MySQL 5.6.6+) - if ( not mysql_version_ge( 10, 4 ) - and defined( $myvar{'innodb_buffer_pool_instances'} ) ) - { - - # Bad Value if > 64 - if ( $myvar{'innodb_buffer_pool_instances'} > 64 ) { - badprint "InnoDB buffer pool instances: " - . $myvar{'innodb_buffer_pool_instances'} . ""; - push( @adjvars, "innodb_buffer_pool_instances (<= 64)" ); - } - - # InnoDB Buffer Pool Size > 1Go - if ( $myvar{'innodb_buffer_pool_size'} > 1024 * 1024 * 1024 ) { - -# InnoDB Buffer Pool Size / 1Go = InnoDB Buffer Pool Instances limited to 64 max. - - # InnoDB Buffer Pool Size > 64Go - my $max_innodb_buffer_pool_instances = - int( $myvar{'innodb_buffer_pool_size'} / ( 1024 * 1024 * 1024 ) ); - - my $nb_cpus = cpu_cores(); - if ( $nb_cpus > 0 && $max_innodb_buffer_pool_instances > $nb_cpus ) - { - infoprint -"Recommendation for innodb_buffer_pool_instances is capped by the number of CPU cores ($nb_cpus)."; - $max_innodb_buffer_pool_instances = $nb_cpus; - } - - $max_innodb_buffer_pool_instances = 64 - if ( $max_innodb_buffer_pool_instances > 64 ); - - if ( $myvar{'innodb_buffer_pool_instances'} != - $max_innodb_buffer_pool_instances ) - { - badprint "InnoDB buffer pool instances: " - . $myvar{'innodb_buffer_pool_instances'} . ""; - push( @adjvars, - "innodb_buffer_pool_instances(=" - . $max_innodb_buffer_pool_instances - . ")" ); - } - else { - goodprint "InnoDB buffer pool instances: " - . $myvar{'innodb_buffer_pool_instances'} . ""; - } - - # InnoDB Buffer Pool Size < 1Go - } - else { - if ( $myvar{'innodb_buffer_pool_instances'} != 1 ) { - badprint -"InnoDB buffer pool <= 1G and Innodb_buffer_pool_instances(!=1)."; - push( @adjvars, "innodb_buffer_pool_instances (=1)" ); - } - else { - goodprint "InnoDB buffer pool instances: " - . $myvar{'innodb_buffer_pool_instances'} . ""; - } - } - } - - # InnoDB Used Buffer Pool Size vs CHUNK size - if ( - ( - ( $myvar{'version'} =~ /MariaDB/i ) - or ( $myvar{'version_comment'} =~ /MariaDB/i ) - ) - and mysql_version_ge( 10, 8 ) - and defined( $myvar{'innodb_buffer_pool_chunk_size'} ) - and $myvar{'innodb_buffer_pool_chunk_size'} == 0 - ) - { - infoprint -"innodb_buffer_pool_chunk_size is set to 'autosize' (0) in MariaDB >= 10.8. Skipping chunk size checks."; - } - elsif (!defined( $myvar{'innodb_buffer_pool_chunk_size'} ) - || $myvar{'innodb_buffer_pool_chunk_size'} == 0 - || !defined( $myvar{'innodb_buffer_pool_size'} ) - || $myvar{'innodb_buffer_pool_size'} == 0 - || !defined( $myvar{'innodb_buffer_pool_instances'} ) - || $myvar{'innodb_buffer_pool_instances'} == 0 ) - { - - badprint -"Cannot calculate InnoDB Buffer Pool Chunk breakdown due to missing or zero values:"; - - infoprint " - innodb_buffer_pool_size: " - . ( - defined $myvar{'innodb_buffer_pool_size'} - ? $myvar{'innodb_buffer_pool_size'} - : "undefined" - ); - infoprint " - innodb_buffer_pool_chunk_size: " - . ( - defined $myvar{'innodb_buffer_pool_chunk_size'} - ? $myvar{'innodb_buffer_pool_chunk_size'} - : "undefined" - ); - infoprint " - innodb_buffer_pool_instances: " - . ( - defined $myvar{'innodb_buffer_pool_instances'} - ? $myvar{'innodb_buffer_pool_instances'} - : "undefined" - ); - - } - else { - my $num_chunks = int( $myvar{'innodb_buffer_pool_size'} / - $myvar{'innodb_buffer_pool_chunk_size'} ); - infoprint "Number of InnoDB Buffer Pool Chunk: $num_chunks for " - . $myvar{'innodb_buffer_pool_instances'} - . " Buffer Pool Instance(s)"; - - my $expected_size = int( $myvar{'innodb_buffer_pool_chunk_size'} ) * - int( $myvar{'innodb_buffer_pool_instances'} ); - - if ( int( $myvar{'innodb_buffer_pool_size'} ) % $expected_size == 0 ) { - goodprint -"Innodb_buffer_pool_size aligned with Innodb_buffer_pool_chunk_size & Innodb_buffer_pool_instances"; - } - else { - badprint -"Innodb_buffer_pool_size not aligned with Innodb_buffer_pool_chunk_size & Innodb_buffer_pool_instances"; - - push( @adjvars, -"innodb_buffer_pool_size must always be equal to or a multiple of innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances" - ); - } - } - - # InnoDB Read efficiency - if ( $mystat{'Innodb_buffer_pool_reads'} > - $mystat{'Innodb_buffer_pool_read_requests'} ) - { - infoprint -"InnoDB Read buffer efficiency: metrics are not reliable (reads > read requests)"; - } - elsif ( defined $mycalc{'pct_read_efficiency'} - && $mycalc{'pct_read_efficiency'} < 90 ) - { - badprint "InnoDB Read buffer efficiency: " - . $mycalc{'pct_read_efficiency'} . "% (" - . $mystat{'Innodb_buffer_pool_read_requests'} - . " hits / " - . ( $mystat{'Innodb_buffer_pool_reads'} + - $mystat{'Innodb_buffer_pool_read_requests'} ) - . " total)"; - } - else { - goodprint "InnoDB Read buffer efficiency: " - . $mycalc{'pct_read_efficiency'} . "% (" - . $mystat{'Innodb_buffer_pool_read_requests'} - . " hits / " - . ( $mystat{'Innodb_buffer_pool_reads'} + - $mystat{'Innodb_buffer_pool_read_requests'} ) - . " total)"; - } - - # InnoDB Write efficiency - if ( $mystat{'Innodb_log_writes'} > $mystat{'Innodb_log_write_requests'} ) { - infoprint -"InnoDB Write Log efficiency: metrics are not reliable (writes > write requests)"; - } - elsif ( defined $mycalc{'pct_write_efficiency'} - && $mycalc{'pct_write_efficiency'} < 90 ) - { - badprint "InnoDB Write Log efficiency: " - . abs( $mycalc{'pct_write_efficiency'} ) . "% (" - . abs( $mystat{'Innodb_log_write_requests'} - - $mystat{'Innodb_log_writes'} ) - . " hits / " - . $mystat{'Innodb_log_write_requests'} - . " total)"; - push( @adjvars, - "innodb_log_buffer_size (> " - . hr_bytes_rnd( $myvar{'innodb_log_buffer_size'} ) - . ")" ); - } - else { - goodprint "InnoDB Write Log efficiency: " - . $mycalc{'pct_write_efficiency'} . "% (" - . ( $mystat{'Innodb_log_write_requests'} - - $mystat{'Innodb_log_writes'} ) - . " hits / " - . $mystat{'Innodb_log_write_requests'} - . " total)"; - } - - # InnoDB Log Waits - $mystat{'Innodb_log_waits_computed'} = 0; - - if ( defined( $mystat{'Innodb_log_waits'} ) - and defined( $mystat{'Innodb_log_writes'} ) - and $mystat{'Innodb_log_writes'} > 0.000001 ) - { - $mystat{'Innodb_log_waits_computed'} = - $mystat{'Innodb_log_waits'} / $mystat{'Innodb_log_writes'}; - } - else { - undef $mystat{'Innodb_log_waits_computed'}; - } - - if ( defined $mystat{'Innodb_log_waits_computed'} - && $mystat{'Innodb_log_waits_computed'} > 0.000001 ) - { - badprint "InnoDB log waits: " - . percentage( $mystat{'Innodb_log_waits'}, - $mystat{'Innodb_log_writes'} ) - . "% (" - . $mystat{'Innodb_log_waits'} - . " waits / " - . $mystat{'Innodb_log_writes'} - . " writes)"; - push( @adjvars, - "innodb_log_buffer_size (> " - . hr_bytes_rnd( $myvar{'innodb_log_buffer_size'} ) - . ")" ); - } - else { - goodprint "InnoDB log waits: " - . percentage( $mystat{'Innodb_log_waits'}, - $mystat{'Innodb_log_writes'} ) - . "% (" - . $mystat{'Innodb_log_waits'} - . " waits / " - . $mystat{'Innodb_log_writes'} - . " writes)"; - } - - # InnoDB Transaction Isolation and Metrics - subheaderprint "InnoDB Transactions"; - my $isolation = - $myvar{'transaction_isolation'} - || $myvar{'tx_isolation'} - || $myvar{'isolation_level'}; - if ( defined $isolation ) { - infoprint("Transaction Isolation Level: $isolation"); - } - - if ( defined $myvar{'innodb_snapshot_isolation'} ) { - infoprint( "InnoDB Snapshot Isolation: " - . $myvar{'innodb_snapshot_isolation'} ); - if ( $myvar{'innodb_snapshot_isolation'} eq 'OFF' - && ( $isolation || '' ) eq 'REPEATABLE-READ' ) - { - badprint( -"innodb_snapshot_isolation is OFF with REPEATABLE-READ (Stricter snapshot isolation is disabled)" - ); - push( @adjvars, "innodb_snapshot_isolation=ON" ); - } - } - - if ( defined $mycalc{'innodb_active_transactions'} ) { - infoprint "Active InnoDB Transactions: " - . $mycalc{'innodb_active_transactions'}; - } - if ( defined $mycalc{'innodb_longest_transaction_duration'} - && $mycalc{'innodb_longest_transaction_duration'} > 0 ) - { - infoprint "Longest InnoDB Transaction Duration: " - . pretty_uptime( $mycalc{'innodb_longest_transaction_duration'} ); - if ( $mycalc{'innodb_longest_transaction_duration'} > 3600 ) { - badprint "Long running InnoDB transaction detected (" - . pretty_uptime( $mycalc{'innodb_longest_transaction_duration'} ) - . ")"; - push( @generalrec, -"Long running transactions can cause InnoDB history list length to increase and impact performance." - ); - } - } - - $result{'Calculations'} = {%mycalc}; -} - -sub mariadb_query_cache_info { - subheaderprint "Query Cache Information"; - - unless ( ( $myvar{'version'} =~ /MariaDB/i ) - or ( $myvar{'version_comment'} =~ /MariaDB/i ) ) - { - infoprint - "Not a MariaDB server. Skipping Query Cache Info plugin check."; - return; - } - - my $plugin_status = select_one( -"SELECT PLUGIN_STATUS FROM information_schema.PLUGINS WHERE PLUGIN_NAME = 'QUERY_CACHE_INFO'" - ); - - if ( defined $plugin_status and $plugin_status eq 'ACTIVE' ) { - goodprint "QUERY_CACHE_INFO plugin is installed and active."; - - my $query = -"SELECT CONCAT_WS(';;', statement_schema, LEFT(statement_text, 80), result_blocks_count, result_blocks_size) FROM information_schema.query_cache_info"; - my @query_cache_data = select_array($query); - - if (@query_cache_data) { - infoprint sprintf( - "%-20s | %-82s | %-10s | %-10s", - "Schema", "Query (truncated)", - "Blocks", "Size" - ); - infoprint "-" x 130; - foreach my $line (@query_cache_data) { - my ( $schema, $text, $blocks, $size ) = split( /;;/, $line ); - infoprint sprintf( "%-20s | %-82s | %-10s | %-10s", - $schema, $text, $blocks, hr_bytes($size) ); - } - } - else { - infoprint "No queries found in the query cache."; - } - } - else { - infoprint "QUERY_CACHE_INFO plugin is not active or not installed."; - return; - } -} - -sub check_metadata_perf { - subheaderprint "Analysis Performance Metrics"; - if ( defined $myvar{'innodb_stats_on_metadata'} ) { - infoprint "innodb_stats_on_metadata: " - . $myvar{'innodb_stats_on_metadata'}; - if ( $myvar{'innodb_stats_on_metadata'} eq 'ON' ) { - badprint "Stat are updated during querying INFORMATION_SCHEMA."; - push @adjvars, "SET innodb_stats_on_metadata = OFF"; - - #Disabling innodb_stats_on_metadata - select_one("SET GLOBAL innodb_stats_on_metadata = OFF;"); - return 1; - } - } - goodprint "No stat updates during querying INFORMATION_SCHEMA."; - return 0; -} - -sub mysql_plugins { - return if ( $opt{plugininfo} == 0 ); - subheaderprint "Plugin Information"; - - my $query = -"SELECT PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_STATUS, PLUGIN_TYPE FROM information_schema.PLUGINS WHERE PLUGIN_STATUS = 'ACTIVE' AND PLUGIN_TYPE != 'INFORMATION SCHEMA' ORDER BY PLUGIN_TYPE, PLUGIN_NAME"; - my @plugin_data = select_array($query); - - if (@plugin_data) { - infoprint sprintf( "%-30s | %-10s | %-10s | %-20s", - "Plugin", "Version", "Status", "Type" ); - infoprint "-" x 80; - foreach my $line (@plugin_data) { - my ( $name, $version, $status, $type ) = split( /\t/, $line ); - infoprint sprintf( "%-30s | %-10s | %-10s | %-20s", - $name, $version, $status, $type ); - } - } - else { - infoprint -"No ACTIVE plugins found (excluding INFORMATION SCHEMA) in the information_schema."; - } -} - -# Recommendations for Database metrics -sub mysql_databases { - return if ( $opt{dbstat} == 0 ); - - subheaderprint "Database Metrics"; - unless ( mysql_version_ge( 5, 5 ) ) { - infoprint -"Database metrics from information schema are missing in this version. Skipping..."; - return; - } - - my $ignore_tables_sql = ""; - if ( $opt{'ignore-tables'} ) { - my @ignored = split /,/, $opt{'ignore-tables'}; - $ignore_tables_sql = - " AND TABLE_NAME NOT IN ('" . join( "','", @ignored ) . "')"; - } - - @dblist = select_array( -"SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME NOT IN ( 'mysql', 'performance_schema', 'information_schema', 'sys' );" - ); - infoprint "There is " . scalar(@dblist) . " Database(s)."; - my @totaldbinfo = split /\s/, - select_one( -"SELECT SUM(TABLE_ROWS), SUM(DATA_LENGTH), SUM(INDEX_LENGTH), SUM(DATA_LENGTH+INDEX_LENGTH), COUNT(TABLE_NAME), COUNT(DISTINCT(TABLE_COLLATION)), COUNT(DISTINCT(ENGINE)) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql;" - ); - infoprint "All User Databases:"; - infoprint " +-- TABLE : " - . select_one( -"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='BASE TABLE' AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql" - ) . ""; - infoprint " +-- VIEW : " - . select_one( -"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='VIEW' AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql" - ) . ""; - infoprint " +-- INDEX : " - . select_one( -"SELECT count(distinct(concat(TABLE_NAME, TABLE_SCHEMA, INDEX_NAME))) from information_schema.STATISTICS WHERE TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql" - ) . ""; - - infoprint " +-- CHARS : " - . ( $totaldbinfo[5] eq 'NULL' ? 0 : $totaldbinfo[5] ) . " (" - . ( - join ", ", - select_array( -"select distinct(CHARACTER_SET_NAME) from information_schema.columns WHERE CHARACTER_SET_NAME IS NOT NULL AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql;" - ) - ) . ")"; - infoprint " +-- COLLA : " - . ( $totaldbinfo[5] eq 'NULL' ? 0 : $totaldbinfo[5] ) . " (" - . ( - join ", ", - select_array( -"SELECT DISTINCT(TABLE_COLLATION) FROM information_schema.TABLES WHERE TABLE_COLLATION IS NOT NULL AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql;" - ) - ) . ")"; - infoprint " +-- ROWS : " - . ( $totaldbinfo[0] eq 'NULL' ? 0 : $totaldbinfo[0] ) . ""; - infoprint " +-- DATA : " - . hr_bytes( $totaldbinfo[1] ) . "(" - . percentage( $totaldbinfo[1], $totaldbinfo[3] ) . "%)"; - infoprint " +-- INDEX : " - . hr_bytes( $totaldbinfo[2] ) . "(" - . percentage( $totaldbinfo[2], $totaldbinfo[3] ) . "%)"; - infoprint " +-- SIZE : " . hr_bytes( $totaldbinfo[3] ) . ""; - infoprint " +-- ENGINE: " - . ( $totaldbinfo[6] eq 'NULL' ? 0 : $totaldbinfo[6] ) . " (" - . ( - join ", ", - select_array( -"SELECT DISTINCT(ENGINE) FROM information_schema.TABLES WHERE ENGINE IS NOT NULL AND TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')$ignore_tables_sql;" - ) - ) . ")"; - - $result{'Databases'}{'All databases'}{'Rows'} = - ( $totaldbinfo[0] eq 'NULL' ? 0 : $totaldbinfo[0] ); - $result{'Databases'}{'All databases'}{'Data Size'} = $totaldbinfo[1]; - $result{'Databases'}{'All databases'}{'Data Pct'} = - percentage( $totaldbinfo[1], $totaldbinfo[3] ) . "%"; - $result{'Databases'}{'All databases'}{'Index Size'} = $totaldbinfo[2]; - $result{'Databases'}{'All databases'}{'Index Pct'} = - percentage( $totaldbinfo[2], $totaldbinfo[3] ) . "%"; - $result{'Databases'}{'All databases'}{'Total Size'} = $totaldbinfo[3]; - print "\n" unless ( $opt{'silent'} or $opt{'json'} ); - my $nbViews = 0; - my $nbTables = 0; - - foreach (@dblist) { - my @dbinfo = split /\s/, - select_one( -"SELECT TABLE_SCHEMA, SUM(TABLE_ROWS), SUM(DATA_LENGTH), SUM(INDEX_LENGTH), SUM(DATA_LENGTH+INDEX_LENGTH), COUNT(DISTINCT ENGINE), COUNT(TABLE_NAME), COUNT(DISTINCT(TABLE_COLLATION)), COUNT(DISTINCT(ENGINE)) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$_'$ignore_tables_sql GROUP BY TABLE_SCHEMA ORDER BY TABLE_SCHEMA" - ); - next unless defined $dbinfo[0]; - - infoprint "Database: " . $dbinfo[0] . ""; - $nbTables = select_one( -"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='BASE TABLE' AND TABLE_SCHEMA='$_'$ignore_tables_sql" - ); - infoprint " +-- TABLE : $nbTables"; - infoprint " +-- VIEW : " - . select_one( -"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='VIEW' AND TABLE_SCHEMA='$_'$ignore_tables_sql" - ) . ""; - infoprint " +-- INDEX : " - . select_one( -"SELECT count(distinct(concat(TABLE_NAME, TABLE_SCHEMA, INDEX_NAME))) from information_schema.STATISTICS WHERE TABLE_SCHEMA='$_'$ignore_tables_sql" - ) . ""; - infoprint " +-- CHARS : " - . ( $totaldbinfo[5] eq 'NULL' ? 0 : $totaldbinfo[5] ) . " (" - . ( - join ", ", - select_array( -"select distinct(CHARACTER_SET_NAME) from information_schema.columns WHERE CHARACTER_SET_NAME IS NOT NULL AND TABLE_SCHEMA='$_'$ignore_tables_sql;" - ) - ) . ")"; - infoprint " +-- COLLA : " - . ( $dbinfo[7] eq 'NULL' ? 0 : $dbinfo[7] ) . " (" - . ( - join ", ", - select_array( -"SELECT DISTINCT(TABLE_COLLATION) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$_' AND TABLE_COLLATION IS NOT NULL$ignore_tables_sql;" - ) - ) . ")"; - infoprint " +-- ROWS : " - . ( !defined( $dbinfo[1] ) or $dbinfo[1] eq 'NULL' ? 0 : $dbinfo[1] ) - . ""; - infoprint " +-- DATA : " - . hr_bytes( $dbinfo[2] ) . "(" - . percentage( $dbinfo[2], $dbinfo[4] ) . "%)"; - infoprint " +-- INDEX : " - . hr_bytes( $dbinfo[3] ) . "(" - . percentage( $dbinfo[3], $dbinfo[4] ) . "%)"; - infoprint " +-- TOTAL : " . hr_bytes( $dbinfo[4] ) . ""; - infoprint " +-- ENGINE: " - . ( $dbinfo[8] eq 'NULL' ? 0 : $dbinfo[8] ) . " (" - . ( - join ", ", - select_array( -"SELECT DISTINCT(ENGINE) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$_' AND ENGINE IS NOT NULL$ignore_tables_sql" - ) - ) . ")"; - - foreach my $eng ( - select_array( -"SELECT DISTINCT(ENGINE) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$_' AND ENGINE IS NOT NULL$ignore_tables_sql" - ) - ) - { - infoprint " +-- ENGINE $eng : " - . select_one( -"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$dbinfo[0]' AND ENGINE='$eng'$ignore_tables_sql" - ) . " TABLE(s)"; - } - - if ( $nbTables == 0 ) { - badprint " No table in $dbinfo[0] database"; - next; - } - badprint "Index size is larger than data size for $dbinfo[0] \n" - if ( $dbinfo[2] ne 'NULL' ) - and ( $dbinfo[3] ne 'NULL' ) - and ( $dbinfo[2] < $dbinfo[3] ); - if ( $dbinfo[5] > 1 and $nbTables > 0 ) { - badprint "There are " - . $dbinfo[5] - . " storage engines. Be careful. \n"; - push @generalrec, -"Select one storage engine (InnoDB is a good choice) for all tables in $dbinfo[0] database ($dbinfo[5] engines detected)"; - } - $result{'Databases'}{ $dbinfo[0] }{'Rows'} = $dbinfo[1]; - $result{'Databases'}{ $dbinfo[0] }{'Tables'} = $dbinfo[6]; - $result{'Databases'}{ $dbinfo[0] }{'Collations'} = $dbinfo[7]; - $result{'Databases'}{ $dbinfo[0] }{'Data Size'} = $dbinfo[2]; - $result{'Databases'}{ $dbinfo[0] }{'Data Pct'} = - percentage( $dbinfo[2], $dbinfo[4] ) . "%"; - $result{'Databases'}{ $dbinfo[0] }{'Index Size'} = $dbinfo[3]; - $result{'Databases'}{ $dbinfo[0] }{'Index Pct'} = - percentage( $dbinfo[3], $dbinfo[4] ) . "%"; - $result{'Databases'}{ $dbinfo[0] }{'Total Size'} = $dbinfo[4]; - - if ( $dbinfo[7] > 1 ) { - badprint $dbinfo[7] - . " different collations for database " - . $dbinfo[0]; - push( @generalrec, - "Check all table collations are identical for all tables in " - . $dbinfo[0] - . " database." ); - } - else { - goodprint $dbinfo[7] - . " collation for " - . $dbinfo[0] - . " database."; - } - if ( $dbinfo[8] > 1 ) { - badprint $dbinfo[8] - . " different engines for database " - . $dbinfo[0]; - push( @generalrec, - "Check all table engines are identical for all tables in " - . $dbinfo[0] - . " database." ); - } - else { - goodprint $dbinfo[8] . " engine for " . $dbinfo[0] . " database."; - } - - my @distinct_column_charset = select_array( -"select DISTINCT(CHARACTER_SET_NAME) from information_schema.COLUMNS where CHARACTER_SET_NAME IS NOT NULL AND TABLE_SCHEMA ='$_' AND CHARACTER_SET_NAME IS NOT NULL" - ); - infoprint "Charsets for $dbinfo[0] database table column: " - . join( ', ', @distinct_column_charset ); - if ( scalar(@distinct_column_charset) > 1 ) { - badprint $dbinfo[0] - . " table column(s) has several charsets defined for all text like column(s)."; - push( @generalrec, - "Limit charset for column to one charset if possible for " - . $dbinfo[0] - . " database." ); - } - else { - goodprint $dbinfo[0] - . " table column(s) has same charset defined for all text like column(s)."; - } - - my @distinct_column_collation = select_array( -"select DISTINCT(COLLATION_NAME) from information_schema.COLUMNS where COLLATION_NAME IS NOT NULL AND TABLE_SCHEMA ='$_' AND COLLATION_NAME IS NOT NULL" - ); - infoprint "Collations for $dbinfo[0] database table column: " - . join( ', ', @distinct_column_collation ); - if ( scalar(@distinct_column_collation) > 1 ) { - badprint $dbinfo[0] - . " table column(s) has several collations defined for all text like column(s)."; - push( @generalrec, - "Limit collations for column to one collation if possible for " - . $dbinfo[0] - . " database." ); - } - else { - goodprint $dbinfo[0] - . " table column(s) has same collation defined for all text like column(s)."; - } - } -} - -# Recommendations for database columns -sub mysql_tables { - return if ( $opt{tbstat} == 0 ); - - subheaderprint "Table Column Metrics"; - unless ( mysql_version_ge( 5, 5 ) ) { - infoprint -"Table column metrics from information schema are missing in this version. Skipping..."; - return; - } - if ( mysql_version_ge(8) and not mysql_version_eq(10) ) { - infoprint -"MySQL and Percona version 8.0 and greater have removed PROCEDURE ANALYSE feature"; - $opt{colstat} = 0; - infoprint "Disabling colstat parameter"; - - } - - my $ignore_tables_sql = ""; - if ( $opt{'ignore-tables'} ) { - my @ignored = split /,/, $opt{'ignore-tables'}; - $ignore_tables_sql = - " AND TABLE_NAME NOT IN ('" . join( "','", @ignored ) . "')"; - } - - my $schema_doc = ""; - my $mermaid_er = ""; - if ( $opt{dumpdir} or $opt{schemadir} ) { - $schema_doc = "# Database Schema Documentation\n\n"; - $schema_doc .= - "Generated by MySQLTuner on " . scalar(localtime) . "\n\n"; - $mermaid_er = "## Visual Database Schema (Mermaid)\n\n"; - $mermaid_er .= "```mermaid\nerDiagram\n"; - } - - if ( $opt{schemadir} ) { - $opt{schemadir} = abs_path( $opt{schemadir} ); - if ( !-d $opt{schemadir} ) { - mkdir $opt{schemadir} - or die "Cannot create directory $opt{schemadir}: $!"; - } - } - - foreach ( select_user_dbs() ) { - my $dbname = $_; - next unless defined $_; - - my $current_schema_doc = ""; - my $current_mermaid_er = ""; - if ( $opt{schemadir} ) { - $current_schema_doc = "# Database: $dbname\n\n"; - $current_schema_doc .= - "Generated by MySQLTuner on " . scalar(localtime) . "\n\n"; - $current_mermaid_er = "## Visual Database Schema (Mermaid)\n\n"; - $current_mermaid_er .= "```mermaid\nerDiagram\n"; - } - - infoprint "Database: " . $_ . ""; - if ( $opt{dumpdir} or $opt{schemadir} ) { - $schema_doc .= "## Database: $dbname\n\n"; - $current_schema_doc .= "### Tables\n\n" if $opt{schemadir} ne ''; - } - - my @dbtable = select_array( -"SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='$dbname' AND TABLE_TYPE='BASE TABLE'$ignore_tables_sql ORDER BY TABLE_NAME" - ); - foreach (@dbtable) { - my $tbname = $_; - infoprint " +-- TABLE: $tbname"; - my $engine = select_one( -"SELECT ENGINE FROM information_schema.tables where TABLE_schema='$dbname' AND TABLE_NAME='$tbname'" - ); - infoprint " +-- TYPE: $engine"; - if ( $opt{dumpdir} or $opt{schemadir} ) { - my $table_info = "### Table: $tbname\n"; - $table_info .= "- **Engine**: $engine\n\n"; - $table_info .= "#### Indexes\n"; - - $schema_doc .= $table_info; - $current_schema_doc .= $table_info if $opt{schemadir} ne ''; - - $mermaid_er .= " $tbname {\n"; - $current_mermaid_er .= " $tbname {\n" - if $opt{schemadir} ne ''; - } - - my $selIdxReq = <<"ENDSQL"; - SELECT index_name AS idxname, - GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols, - INDEX_TYPE as type - FROM information_schema.statistics - WHERE INDEX_SCHEMA='$dbname' - AND TABLE_NAME='$tbname' - GROUP BY idxname, type -ENDSQL - my @tbidx = select_array($selIdxReq); - my $found = 0; - foreach my $idx (@tbidx) { - my @info = split /\s/, $idx; - next if $info[0] eq 'NULL'; - infoprint - " +-- Index $info[0] - Cols: $info[1] - Type: $info[2]"; - if ( $opt{dumpdir} or $opt{schemadir} ) { - my $idx_info = "- **$info[0]**: $info[1] ($info[2])\n"; - $schema_doc .= $idx_info; - $current_schema_doc .= $idx_info if $opt{schemadir} ne ''; - } - $found++; - } - if ( $found == 0 ) { - badprint("Table $dbname.$tbname has no index defined"); - if ( $opt{dumpdir} or $opt{schemadir} ) { - $schema_doc .= "- *No indexes defined*\n"; - $current_schema_doc .= "- *No indexes defined*\n" - if $opt{schemadir} ne ''; - } - push @generalrec, - "Add at least a primary key on table $dbname.$tbname"; - } - - if ( $opt{dumpdir} or $opt{schemadir} ) { - $schema_doc .= "\n#### Columns\n"; - $current_schema_doc .= "\n#### Columns\n" - if $opt{schemadir} ne ''; - } - - my @tbcol = select_array( -"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='$dbname' AND TABLE_NAME='$tbname'" - ); - foreach (@tbcol) { - my $ctype = select_one( -"SELECT COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='$dbname' AND TABLE_NAME='$tbname' AND COLUMN_NAME='$_' " - ); - my $isnull = select_one( -"SELECT IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='$dbname' AND TABLE_NAME='$tbname' AND COLUMN_NAME='$_' " - ); - - my $current_type = - uc($ctype) . ( $isnull eq 'NO' ? " NOT NULL" : " NULL" ); - my $optimal_type = ''; - infoprint " +-- Column $tbname.$_: $current_type"; - if ( $opt{dumpdir} or $opt{schemadir} ) { - my $col_info = "- **$_**: $current_type\n"; - $schema_doc .= $col_info; - $current_schema_doc .= $col_info if $opt{schemadir} ne ''; - - my $mtype = $ctype; - $mtype =~ s/\(.*\)//g; # Strip lengths for Mermaid - $mermaid_er .= " $mtype $_\n"; - $current_mermaid_er .= " $mtype $_\n" - if $opt{schemadir} ne ''; - } - if ( $opt{colstat} == 1 ) { - $optimal_type = select_str_g( "Optimal_fieldtype", -"SELECT \\`$_\\` FROM \\`$dbname\\`.\\`$tbname\\` PROCEDURE ANALYSE(100000)" - ) - unless ( mysql_version_ge(8) - and not mysql_version_eq(10) ); - } - if ( $optimal_type eq '' ) { - - #infoprint " +-- Current Fieldtype: $current_type"; - - #infoprint " Optimal Fieldtype: Not available"; - } - elsif ( $current_type ne $optimal_type - and $current_type !~ /.*DATETIME.*/ - and $current_type !~ /.*TIMESTAMP.*/ ) - { - infoprint " +-- Current Fieldtype: $current_type"; - if ( $optimal_type =~ /.*ENUM\(.*/ ) { - $optimal_type = "ENUM( ... )"; - } - infoprint " +-- Optimal Fieldtype: $optimal_type "; - if ( $optimal_type !~ /.*ENUM\(.*/ ) { - badprint -"Consider changing type for column $_ in table $dbname.$tbname"; - push( @generalrec, -"ALTER TABLE \`$dbname\`.\`$tbname\` MODIFY \`$_\` $optimal_type;" - ); - } - } - else { - goodprint "$dbname.$tbname ($_) type: $current_type"; - } - } - if ( $opt{dumpdir} or $opt{schemadir} ) { - $schema_doc .= "\n---\n\n"; - $current_schema_doc .= "\n---\n\n" if $opt{schemadir} ne ''; - - $mermaid_er .= " }\n"; - $current_mermaid_er .= " }\n" if $opt{schemadir} ne ''; - } - } - - if ( $opt{schemadir} ) { - $current_mermaid_er .= "```\n\n"; - $current_schema_doc .= $current_mermaid_er; - my $doc_file = "$opt{schemadir}/$dbname.md"; - if ( open( my $fh, '>', $doc_file ) ) { - binmode( $fh, ":utf8" ); - print $fh $current_schema_doc; - close($fh); - infoprint - "Schema documentation for $dbname generated in $doc_file"; - } - else { - badprint -"Could not write schema documentation for $dbname to $doc_file: $!"; - } - } - } - if ( $opt{dumpdir} ne '' && $opt{dumpdir} ne '0' ) { - $mermaid_er .= "```\n\n"; - $schema_doc .= $mermaid_er; - my $doc_file = "$opt{dumpdir}/schema_documentation.md"; - if ( open( my $fh, '>', $doc_file ) ) { - binmode( $fh, ":utf8" ); - print $fh $schema_doc; - close($fh); - infoprint - "Consolidated schema documentation generated in $doc_file"; - } - else { - badprint -"Could not write consolidated schema documentation to $doc_file: $!"; - } - } -} - -# Recommendations for Indexes metrics -sub mysql_indexes { - return if ( $opt{idxstat} == 0 ); - - subheaderprint "Indexes Metrics"; - unless ( mysql_version_ge( 5, 5 ) ) { - infoprint -"Index metrics from information schema are missing in this version. Skipping..."; - return; - } - -# unless ( mysql_version_ge( 5, 6 ) ) { -# infoprint -#"Skip Index metrics from information schema due to erroneous information provided in this version"; -# return; -# } - my $ignore_tables_sql = ""; - if ( $opt{'ignore-tables'} ) { - my @ignored = split /,/, $opt{'ignore-tables'}; - $ignore_tables_sql = - " AND TABLE_NAME NOT IN ('" . join( "','", @ignored ) . "')"; - } - - my $selIdxReq = <<"ENDSQL"; -SELECT - CONCAT(t.TABLE_SCHEMA, '.', t.TABLE_NAME) AS 'table', - CONCAT(s.INDEX_NAME, '(', s.COLUMN_NAME, ')') AS 'index' - , s.SEQ_IN_INDEX AS 'seq' - , s2.max_columns AS 'maxcol' - , s.CARDINALITY AS 'card' - , t.TABLE_ROWS AS 'est_rows' - , INDEX_TYPE as type - , ROUND(((s.CARDINALITY / IFNULL(t.TABLE_ROWS, 0.01)) * 100), 2) AS 'sel' -FROM INFORMATION_SCHEMA.STATISTICS s - INNER JOIN INFORMATION_SCHEMA.TABLES t - ON s.TABLE_SCHEMA = t.TABLE_SCHEMA - AND s.TABLE_NAME = t.TABLE_NAME - INNER JOIN ( - SELECT - TABLE_SCHEMA - , TABLE_NAME - , INDEX_NAME - , MAX(SEQ_IN_INDEX) AS max_columns - FROM INFORMATION_SCHEMA.STATISTICS - WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema')$ignore_tables_sql - AND INDEX_TYPE <> 'FULLTEXT' - GROUP BY TABLE_SCHEMA, TABLE_NAME, INDEX_NAME - ) AS s2 - ON s.TABLE_SCHEMA = s2.TABLE_SCHEMA - AND s.TABLE_NAME = s2.TABLE_NAME - AND s.INDEX_NAME = s2.INDEX_NAME -WHERE t.TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema')$ignore_tables_sql -AND t.TABLE_ROWS > 10 -AND s.CARDINALITY IS NOT NULL -AND (s.CARDINALITY / IFNULL(t.TABLE_ROWS, 0.01)) < 8.00 -ORDER BY sel -LIMIT 10; -ENDSQL - my @idxinfo = select_array($selIdxReq); - infoprint "Worst selectivity indexes:"; - foreach (@idxinfo) { - debugprint "$_"; - my @info = split /\s/; - infoprint "Index: " . $info[1] . ""; - - infoprint " +-- COLUMN : " . $info[0] . ""; - infoprint " +-- NB SEQS : " . $info[2] . " sequence(s)"; - infoprint " +-- NB COLS : " . $info[3] . " column(s)"; - infoprint " +-- CARDINALITY : " . $info[4] . " distinct values"; - infoprint " +-- NB ROWS : " . $info[5] . " rows"; - infoprint " +-- TYPE : " . $info[6]; - infoprint " +-- SELECTIVITY : " . $info[7] . "%"; - - $result{'Indexes'}{ $info[1] }{'Column'} = $info[0]; - $result{'Indexes'}{ $info[1] }{'Sequence number'} = $info[2]; - $result{'Indexes'}{ $info[1] }{'Number of column'} = $info[3]; - $result{'Indexes'}{ $info[1] }{'Cardinality'} = $info[4]; - $result{'Indexes'}{ $info[1] }{'Row number'} = $info[5]; - $result{'Indexes'}{ $info[1] }{'Index Type'} = $info[6]; - $result{'Indexes'}{ $info[1] }{'Selectivity'} = $info[7]; - if ( $info[7] < 25 ) { - badprint "$info[1] has a low selectivity"; - } - } - infoprint "Indexes per database:"; - foreach my $dbname ( select_user_dbs() ) { - infoprint "Database: " . $dbname . ""; - $selIdxReq = <<"ENDSQL"; - SELECT concat(table_name, '.', index_name) AS idxname, - GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols, - SUM(CARDINALITY) as card, - INDEX_TYPE as type - FROM information_schema.statistics - WHERE INDEX_SCHEMA='$dbname' - AND index_name IS NOT NULL$ignore_tables_sql - GROUP BY table_name, idxname, type -ENDSQL - my $found = 0; - foreach my $idxinfo ( select_array($selIdxReq) ) { - my @info = split /\s/, $idxinfo; - next if $info[0] eq 'NULL'; - infoprint " +-- INDEX : " . $info[0]; - infoprint " +-- COLUMNS : " . $info[1]; - infoprint " +-- CARDINALITY: " . $info[2]; - infoprint " +-- TYPE : " . $info[4] if defined $info[4]; - infoprint " +-- COMMENT : " . $info[5] if defined $info[5]; - $found++; - } - my $nbTables = select_one( -"SELECT count(*) from information_schema.TABLES WHERE TABLE_TYPE ='BASE TABLE' AND TABLE_SCHEMA='$dbname'" - ); - badprint "No index found for $dbname database" - if $found == 0 and $nbTables > 1; - push @generalrec, "Add indexes on tables from $dbname database" - if $found == 0 and $nbTables > 1; - } - return - unless ( defined( $myvar{'performance_schema'} ) - and $myvar{'performance_schema'} eq 'ON' ); - - $selIdxReq = <<"ENDSQL"; -SELECT CONCAT(object_schema, '.', object_name) AS 'table', index_name -FROM performance_schema.table_io_waits_summary_by_index_usage -WHERE index_name IS NOT NULL -AND count_star = 0 -AND index_name <> 'PRIMARY' -AND object_schema NOT IN ('mysql', 'performance_schema', 'information_schema')$ignore_tables_sql -ORDER BY count_star, object_schema, object_name; -ENDSQL - @idxinfo = select_array($selIdxReq); - infoprint "Unused indexes:"; - push( @generalrec, "Remove unused indexes." ) if ( scalar(@idxinfo) > 0 ); - foreach (@idxinfo) { - debugprint "$_"; - my @info = split /\s/; - badprint "Index: $info[1] on $info[0] is not used."; - push @{ $result{'Indexes'}{'Unused Indexes'} }, - $info[0] . "." . $info[1]; - } -} - -sub mysql_views { - subheaderprint "Views Metrics"; - unless ( mysql_version_ge( 5, 5 ) ) { - infoprint -"Views metrics from information schema are missing in this version. Skipping..."; - return; - } -} - -sub mysql_routines { - subheaderprint "Routines Metrics"; - unless ( mysql_version_ge( 5, 5 ) ) { - infoprint -"Routines metrics from information schema are missing in this version. Skipping..."; - return; - } -} - -sub mysql_triggers { - subheaderprint "Triggers Metrics"; - unless ( mysql_version_ge( 5, 5 ) ) { - infoprint -"Trigger metrics from information schema are missing in this version. Skipping..."; - return; - } -} - -# Take the two recommendation arrays and display them at the end of the output -sub make_recommendations { - $result{'Recommendations'} = \@generalrec; - $result{'AdjustVariables'} = \@adjvars; - $result{'Modeling'} = \@modeling; - - # Modular structure for modern reporting - $result{'Modules'} = { - 'System' => \@sysrec, - 'Performance' => \@adjvars, - 'Modeling' => \@modeling, - 'Security' => \@secrec, - }; - subheaderprint "Recommendations"; - if ( @generalrec > 0 ) { - prettyprint "General recommendations:"; - foreach (@generalrec) { prettyprint " " . $_ . ""; } - } - if ( @adjvars > 0 ) { - prettyprint "Variables to adjust:"; - if ( $mycalc{'pct_max_physical_memory'} > 90 ) { - prettyprint - " *** MySQL's maximum memory usage is dangerously high ***\n" - . " *** Add RAM before increasing MySQL buffer variables ***"; - } - foreach (@adjvars) { prettyprint " " . $_ . ""; } - } - if ( @generalrec == 0 && @adjvars == 0 ) { - prettyprint "No additional performance recommendations are available."; - } -} - -sub close_outputfile { - close($fh) if defined($fh); -} - -sub headerprint { - prettyprint " >> MySQLTuner $tunerversion\n" - . "\t * Jean-Marie Renouard \n" - . "\t * Major Hayden \n" - . " >> Bug reports, feature requests, and downloads at http://mysqltuner.pl/\n" - . " >> Run with '--help' for additional options and output filtering"; - debugprint( "Debug: " . $opt{debug} ); - debugprint( "Experimental: " . $opt{experimental} ); -} - -sub string2file { - my $filename = shift; - my $content = shift; - open my $fh, q(>), $filename - or die -"Unable to open $filename in write mode. Please check permissions for this file or directory"; - print $fh $content if defined($content); - close $fh; - debugprint $content; -} - -sub file2array { - my $filename = shift; - debugprint "* reading $filename"; - my $fh; - open( $fh, q(<), "$filename" ) - or die "Couldn't open $filename for reading: $!\n"; - my @lines = <$fh>; - close($fh); - return @lines; -} - -sub file2string { - return join( '', file2array(@_) ); -} - -my $templateModel; -if ( $opt{'template'} ne 0 ) { - $templateModel = file2string( $opt{'template'} ); -} -else { - # DEFAULT REPORT TEMPLATE - $templateModel = <<'END_TEMPLATE'; - - - - MySQLTuner Report - - - - -

Result output

-
-{$data}
-
- - - -END_TEMPLATE -} - -sub dump_result { - - #debugprint Dumper( \%result ) if ( $opt{'debug'} ); - debugprint "HTML REPORT: $opt{'reportfile'}"; - - if ( $opt{'reportfile'} ne 0 ) { - eval { require Text::Template }; - eval { require JSON }; - if ($@) { - badprint "Text::Template Module is needed."; - die "Text::Template Module is needed."; - } - - my $json = JSON->new->allow_nonref; - my $json_text = $json->pretty->encode( \%result ); - my %vars = ( - 'data' => \%result, - 'debug' => $json_text, - ); - my $template; - { - no warnings 'once'; - $template = Text::Template->new( - TYPE => 'STRING', - PREPEND => q{;}, - SOURCE => $templateModel, - DELIMITERS => [ '[%', '%]' ] - ) or die "Couldn't construct template: $Text::Template::ERROR"; - } - - open my $fh, q(>), $opt{'reportfile'} - or die -"Unable to open $opt{'reportfile'} in write mode. please check permissions for this file or directory"; - $template->fill_in( HASH => \%vars, OUTPUT => $fh ); - close $fh; - } - - if ( $opt{'json'} ne 0 ) { - eval { require JSON }; - if ($@) { - print "$bad JSON Module is needed.\n"; - return 1; - } - - my $json = JSON->new->allow_nonref; - print $json->utf8(1)->pretty( ( $opt{'prettyjson'} ? 1 : 0 ) ) - ->encode( \%result ); - - if ( $opt{'outputfile'} ne 0 ) { - unlink $opt{'outputfile'} if ( -e $opt{'outputfile'} ); - open my $fh, q(>), $opt{'outputfile'} - or die -"Unable to open $opt{'outputfile'} in write mode. please check permissions for this file or directory"; - print $fh $json->utf8(1)->pretty( ( $opt{'prettyjson'} ? 1 : 0 ) ) - ->encode( \%result ); - close $fh; - } - } -} - -sub which { - my $prog_name = shift; - my $path_string = shift; - my @path_array = split /:/, $ENV{'PATH'}; - if ($is_win) { @path_array = split /;/, $ENV{'PATH'} =~ s/\\/\//gr; } - - for my $path (@path_array) { - if ($is_win) { - return "$path/$prog_name.exe" if ( -x "$path/$prog_name.exe" ); - return "$path/$prog_name.com" if ( -x "$path/$prog_name.com" ); - return "$path/$prog_name.bat" if ( -x "$path/$prog_name.bat" ); - } - else { - return "$path/$prog_name" if ( -x "$path/$prog_name" ); - } - } - - return 0; -} - -sub dump_csv_files { - return if ( ( $opt{dumpdir} // '0' ) eq '0' or $opt{dumpdir} eq '' ); - - subheaderprint "Dumping CSV files"; - - $opt{dumpdir} = abs_path( $opt{dumpdir} ); - if ( !-d $opt{dumpdir} ) { - mkdir $opt{dumpdir} or die "Cannot create directory $opt{dumpdir}: $!"; - } - - infoprint("Dumpdir: $opt{dumpdir}"); - - # Always create raw_mysqltuner.txt in dumpdir for complete analysis output - # This is independent of --outputfile option - my $raw_output_file = "$opt{dumpdir}/raw_mysqltuner.txt"; - infoprint("Auto-generating raw output file: $raw_output_file"); - - # If outputfile is not already set, use raw_mysqltuner.txt as the main output - if ( $opt{outputfile} eq 0 ) { - $opt{outputfile} = $raw_output_file; - my $outputfile_path = abs_path( $opt{outputfile} ); - open( $fh, '>', $outputfile_path ) - or die("Failed to open $outputfile_path for writing: $!"); - $opt{nocolor} = 1; # Disable colors in file output - } - - # If outputfile is already set, create a second file handle for raw output - else { - my $raw_fh; - open( $raw_fh, '>', $raw_output_file ) - or die("Failed to open $raw_output_file for writing: $!"); - - # Duplicate all output to both file handles - # We'll need to modify prettyprint to write to both $fh and $raw_fh - # For now, just create a symlink - close($raw_fh); - unlink($raw_output_file); - my $target = abs_path( $opt{outputfile} ); - symlink( $target, $raw_output_file ) - or warn("Could not create symlink $raw_output_file -> $target: $!"); - } - - # Store all sys schema in dumpdir if defined - infoprint("Dumping sys schema"); - for my $sys_view ( select_array('use sys;show tables;') ) { - if ( $sys_view =~ /innodb_buffer_stats/ ) { - infoprint("SKIPPING $sys_view"); - next; - } - infoprint "Dumping $sys_view into $opt{dumpdir}"; - my $sys_view_table = $sys_view; - $sys_view_table =~ s/\$/\\\$/g; - select_csv_file( "$opt{dumpdir}/sys_$sys_view.csv", - 'select * from sys.\`' . $sys_view_table . '\`' ); - } - - # Store all information schema in dumpdir if defined - infoprint("Dumping information schema"); - for my $info_s_table ( select_array('use information_schema;show tables;') ) - { - next if $info_s_table =~ /INNODB_BUFFER_PAGE/; - infoprint "Dumping $info_s_table into $opt{dumpdir}"; - select_csv_file( - "$opt{dumpdir}/ifs_${info_s_table}.csv", - "select * from information_schema.$info_s_table" - ); - } - - # Store all performance schema in dumpdir if defined - infoprint("Dumping performance schema"); - for - my $info_pf_table ( select_array('use performance_schema;show tables;') ) - { - next if $info_pf_table =~ /^events_/; - infoprint - "Performance Schema Dumping $info_pf_table into $opt{dumpdir}"; - select_csv_file( - "$opt{dumpdir}/ps_${info_pf_table}.csv", - "select * from performance_schema.$info_pf_table" - ); - } -} - -# --------------------------------------------------------------------------- -# BEGIN 'MAIN' -# --------------------------------------------------------------------------- -if ( !caller ) { - parse_cli_args; # Parse CLI arguments - setup_environment; # Initialize variables and handle early exits - headerprint; # Header Print - - validate_tuner_version; # Check latest version - cloud_setup; - mysql_setup; # Gotta login first - debugprint "MySQL FINAL Client : $mysqlcmd $mysqllogin"; - debugprint "MySQL Admin FINAL Client : $mysqladmincmd $mysqllogin"; - - dump_csv_files; # dump csv files - os_setup; # Set up some OS variables - get_all_vars; # Toss variables/status into hashes - get_tuning_info; # Get information about the tuning connection - calculations; # Calculate everything we need - check_architecture; # Suggest 64-bit upgrade - check_storage_engines; # Show enabled storage engines - if ( $opt{'feature'} ) { - subheaderprint "See FEATURES.md for more information"; - no strict 'refs'; - for my $feature ( split /,/, $opt{'feature'} ) { - subheaderprint "Running feature: $feature"; - $feature->(); - } - make_recommendations; - goodprint "Terminated successfully"; - exit(0); - } - validate_mysql_version; # Check current MySQL version - - system_recommendations; # Avoid too many services on the same host - log_file_recommendations; # check log file content - check_metadata_perf; # Show parameter impacting performance during analysis - mysql_databases; # Show information about databases - mysql_tables; # Show information about table column - mysql_table_structures; # Show information about table structures - - mysql_indexes; # Show information about indexes - mysql_views; # Show information about views - mysql_triggers; # Show information about triggers - mysql_routines; # Show information about routines - security_recommendations; # Display some security recommendations - ssl_tls_recommendations; # Display SSL/TLS recommendations - cve_recommendations; # Display related CVE - mysql_plugins; # Print Plugin Information - - mysql_stats; # Print the server stats - mysql_pfs; # Print Performance schema info - - mariadb_threadpool; # Print MariaDB ThreadPool stats - mysql_myisam; # Print MyISAM stats - mysql_innodb; # Print InnoDB stats - mariadb_query_cache_info; # Print Query Cache Info stats - mariadb_aria; # Print MariaDB Aria stats - mariadb_tokudb; # Print MariaDB Tokudb stats - mariadb_xtradb; # Print MariaDB XtraDB stats - - #mariadb_rockdb; # Print MariaDB RockDB stats - #mariadb_spider; # Print MariaDB Spider stats - #mariadb_connect; # Print MariaDB Connect stats - mariadb_galera; # Print MariaDB Galera Cluster stats - get_replication_status; # Print replication info - make_recommendations; # Make recommendations based on stats - dump_result; # Dump result if debug is on - goodprint "Terminated successfully"; - close_outputfile; # Close reportfile if needed - - # --------------------------------------------------------------------------- - # END 'MAIN' - # --------------------------------------------------------------------------- -} -1; - -__END__ - -=pod - -=encoding UTF-8 - -=head1 NAME - - MySQLTuner 2.8.37 - MySQL High Performance Tuning Script - -=head1 IMPORTANT USAGE GUIDELINES - -To run the script with the default options, run the script without arguments -Allow MySQL server to run for at least 24-48 hours before trusting suggestions -Some routines may require root level privileges (script will provide warnings) -You must provide the remote server's total memory when connecting to other servers - -=head1 OPTIONS - -See C for a full list of available options and their categories. - -=head1 VERSION - -Version 2.8.37 -=head1 PERLDOC - -You can find documentation for this module with the perldoc command. - - perldoc mysqltuner - -=head2 INTERNALS - -L - - Internal documentation - -=head1 AUTHORS - -Major Hayden - major@mhtx.net -Jean-Marie Renouard - jmrenouard@gmail.com - -=head1 CONTRIBUTORS - -=over 4 - -=item * - -Matthew Montgomery - -=item * - -Paul Kehrer - -=item * - -Dave Burgess - -=item * - -Jonathan Hinds - -=item * - -Mike Jackson - -=item * - -Nils Breunese - -=item * - -Shawn Ashlee - -=item * - -Luuk Vosslamber - -=item * - -Ville Skytta - -=item * - -Trent Hornibrook - -=item * - -Jason Gill - -=item * - -Mark Imbriaco - -=item * - -Greg Eden - -=item * - -Aubin Galinotti - -=item * - -Giovanni Bechis - -=item * - -Bill Bradford - -=item * - -Ryan Novosielski - -=item * - -Michael Scheidell - -=item * - -Blair Christensen - -=item * - -Hans du Plooy - -=item * - -Victor Trac - -=item * - -Everett Barnes - -=item * - -Tom Krouper - -=item * - -Gary Barrueto - -=item * - -Simon Greenaway - -=item * - -Adam Stein - -=item * - -Isart Montane - -=item * - -Baptiste M. - -=item * - -Cole Turner - -=item * - -Major Hayden - -=item * - -Joe Ashcraft - -=item * - -Jean-Marie Renouard - -=item * - -Stephan GroBberndt - -=item * - -Christian Loos - -=item * - -Long Radix - -=back - -=head1 SUPPORT - - -Bug reports, feature requests, and downloads at http://mysqltuner.pl/ - -Bug tracker can be found at https://github.com/jmrenouard/MySQLTuner-perl/issues - -Maintained by Jean-Marie Renouard (jmrenouard\@gmail.com) - Licensed under GPL - -=head1 SOURCE CODE - -L - - git clone https://github.com/jmrenouard/MySQLTuner-perl/.git - -=head1 COPYRIGHT AND LICENSE - -Copyright (C) 2006-2026 Major Hayden - major@mhtx.net -# Copyright (C) 2015-2026 Jean-Marie Renouard - jmrenouard@gmail.com - -For the latest updates, please visit http://mysqltuner.pl/ - -Git repository available at https://github.com/jmrenouard/MySQLTuner-perl/ - -This program 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 program 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. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -=cut - -# Local variables: -# indent-tabs-mode: t -# cperl-indent-level: 8 -# perl-indent-level: 8 -# End: diff --git a/mysqltuner.pl.tidy b/mysqltuner.pl.tidy deleted file mode 100644 index e69de29bb..000000000