mourningdove/bin/dbcheck.pl

373 lines
11 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
#
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.
use strict;
BEGIN { require "$ENV{LJHOME}/cgi-bin/LJ/Directories.pm"; }
use LJ::DB;
use DBI;
use Getopt::Long;
use Time::HiRes ();
my ( $help, $opt_err, $opt_all ) = ( 0, 0, 0 );
my ( $opt_checkreport, $opt_verbose, $opt_rates ) = ( 0, undef, undef );
exit 1
unless GetOptions(
'help' => \$help,
'checkreport' => \$opt_checkreport,
'rates' => \$opt_rates,
'onlyerrors' => \$opt_err,
'all' => \$opt_all,
'verbose' => \$opt_verbose,
);
if ($help) {
die( "Usage: dbcheck.pl [opts] [[cmd] args...]\n"
. " --all Check all hosts, even those with no weight assigned.\n"
. " --help Get this help\n"
. " --checkreport Show tables that haven't been checked in a while.\n"
. " --onlyerrors Will be silent unless there are errors.\n" . "\n"
. "Commands\n"
. " (none) Shows replication status.\n"
. " queries <host> Shows active queries on host, sorted by running time.\n" );
}
debug("Connecting to master...");
my $dbh = LJ::DB::dbh_by_role("master");
die "Can't get master db handle\n" unless $dbh;
my %dbinfo; # dbid -> hashref
my %name2id; # name -> dbid
my $sth;
my $masterid = 0;
my %subclust; # id -> name of parent (pork-85 -> "pork")
$sth = $dbh->prepare("SELECT dbid, name, masterid, rootfdsn FROM dbinfo");
$sth->execute;
while ( $_ = $sth->fetchrow_hashref ) {
if ( $_->{name} =~ /(.+)\-\d\d$/ ) {
$subclust{ $_->{dbid} } = $1;
next;
}
next unless $_->{'dbid'};
$dbinfo{ $_->{'dbid'} } = $_;
$name2id{ $_->{'name'} } = $_->{'dbid'};
}
my %role; # rolename -> dbid -> [ norm, curr ]
my %rolebyid; # dbid -> rolename -> [ norm, curr ]
$sth = $dbh->prepare("SELECT dbid, role, norm, curr FROM dbweights");
$sth->execute;
while ( $_ = $sth->fetchrow_hashref ) {
my $id = $_->{dbid};
if ( $subclust{$id} ) {
$id = $name2id{ $subclust{$id} };
}
next unless defined $dbinfo{$id};
$dbinfo{$id}->{'totalweight'} += $_->{'curr'};
$role{ $_->{role} }->{$id} = [ $_->{norm}, $_->{curr} ];
$rolebyid{$id}->{ $_->{role} } = [ $_->{norm}, $_->{curr} ];
}
my %root_handle; # name -> $db
my $get_root_handle = sub {
my $name = shift;
return $root_handle{$name} if exists $root_handle{$name};
debug("Connecting to '$name' ...");
$LJ::DB_TIMEOUT = 1;
my $db = LJ::DB::root_dbh_by_name($name);
debug(" ($name: failed to connect)") unless $db;
return $root_handle{$name} = $db;
};
my @errors;
my %master_status; # dbid -> [ $file, $pos ]
my $check_master_status = sub {
my $dbid = shift;
my $d = $dbinfo{$dbid};
die "Bogus DB: $dbid" unless $d;
my $db = $get_root_handle->( $d->{name} );
next unless $db;
my ( $masterfile, $masterpos ) = $db->selectrow_array("SHOW MASTER STATUS");
$master_status{$dbid} = [ $masterfile, $masterpos ];
};
my $check = sub {
my $dbid = shift;
my $d = $dbinfo{$dbid};
die "Bogus DB: $dbid" unless $d;
# calculate roles to show
my $roles;
{
my %drole; # display role -> 1
foreach my $role ( grep { $role{$_}{$dbid}[1] } keys %{ $rolebyid{$dbid} } ) {
my $drole = $role;
$drole{$drole} = 1;
}
$roles = join( ", ", sort keys %drole );
}
my $db = $get_root_handle->( $d->{name} );
unless ($db) {
printf(
"%4d %-18s %4s %16s %14s ($roles)\n",
$dbid, $d->{name}, $d->{masterid} ? $d->{masterid} : "",
) unless $opt_err;
push @errors, "Can't connect to $d->{'name'}";
return 0;
}
my $tzone;
( undef, $tzone ) = $db->selectrow_array("show variables like 'system_time_zone'");
$tzone ||= "???";
$sth = $db->prepare("SHOW PROCESSLIST");
$sth->execute;
my $pcount_total = 0;
my $pcount_busy = 0;
while ( my $r = $sth->fetchrow_hashref ) {
next if $r->{'State'} =~ /waiting for/i;
next if $r->{'State'} eq "Reading master update";
next if $r->{'State'} =~ /^(Has (sent|read) all)|(Sending binlog)/;
$pcount_total++;
$pcount_busy++ if $r->{'State'};
}
my $log_count = 0;
if ( $master_status{$dbid} && $master_status{$dbid}->[1] ) {
$sth = $db->prepare("SHOW MASTER LOGS");
$sth->execute;
while ( my ($log) = $sth->fetchrow_array ) {
$log_count++;
}
}
my $ss = $db->selectrow_hashref("show slave status");
if ($ss) {
foreach my $k ( sort keys %$ss ) {
$ss->{ lc $k } = $ss->{$k};
}
}
my $diff;
if ($ss) {
if ( $ss->{'slave_io_running'} eq "Yes" && $ss->{'slave_sql_running'} eq "Yes" ) {
if ( $ss->{'master_log_file'} eq $ss->{'relay_master_log_file'} ) {
$diff = $ss->{'read_master_log_pos'} - $ss->{'exec_master_log_pos'};
}
else {
$diff = "XXXXXXX";
push @errors, "Wrong log file: $d->{name}";
}
}
else {
$diff = "XXXXXXX";
$ss->{last_error} =~ s/[^\n\r\t\x20-\x7e]//g;
push @errors, "Slave not running: $d->{name}: $ss->{last_error}";
}
my $ms = $master_status{ $d->{masterid} } || [];
#print " master: [@$ms], slave at: [$ss->{master_log_file}, $ss->{read_master_log_pos}]\n";
if ( $ss->{master_log_file} ne $ms->[0] || $ss->{read_master_log_pos} < $ms->[1] - 20_000 )
{
push @errors,
"$d->{name}: Relay log behind: master=[@$ms], $d->{name}=[$ss->{master_log_file}, $ss->{read_master_log_pos}]";
}
}
else {
$diff = "-"; # not applicable
}
my $extra_version = "";
my $ver = $db->selectrow_array('SELECT VERSION()');
if ($ver) {
$ver =~ s/^(\d\.\d+\.\d+).*$/$1/;
$extra_version = $ver;
}
else {
$extra_version = "unknown";
}
#print "$dbid of $d->{masterid}: $d->{name} ($roles)\n";
printf(
"%4d %-18s %4s repl:%7s %4s conn:%4d/%4d $tzone \%s ($roles)\n",
$dbid, $d->{name}, $d->{masterid} ? $d->{masterid} : "",
$diff, $log_count ? sprintf( "<%2s>", $log_count ) : "",
$pcount_busy, $pcount_total, $extra_version
) unless $opt_err;
};
check_report() if $opt_checkreport;
rate_report() if $opt_rates;
$check_master_status->($_) foreach ( sorted_dbids() );
$check->($_) foreach ( sorted_dbids() );
if (@errors) {
if ($opt_err) {
my %ignore;
open( EX, "$ENV{'HOME'}/.dbcheck.ignore" );
while (<EX>) {
s/\s+$//;
$ignore{$_} = 1;
}
close EX;
@errors = grep { !$ignore{$_} } @errors;
}
print STDERR "\nERRORS:\n" if @errors;
foreach (@errors) {
print STDERR " * $_\n";
}
}
my $sorted_cache;
sub sorted_dbids {
return @$sorted_cache if $sorted_cache;
$sorted_cache = [ _sorted_dbids() ];
return @$sorted_cache;
}
sub _sorted_dbids {
my @ids;
my %added; # dbid -> 1
my $add = sub {
my $dbid = shift;
$added{$dbid} = 1;
push @ids, $dbid;
};
my $masterid = ( keys %{ $role{'master'} } )[0];
$add->($masterid);
# then slaves
foreach my $id (
sort { $dbinfo{$a}->{name} cmp $dbinfo{$b}->{name} }
grep { !$added{$_} && $rolebyid{$_}->{slave} } keys %dbinfo
)
{
$add->($id);
}
# now, figure out which remaining are associated with cluster roles (user clusters)
my %minclust; # dbid -> minimum cluster number associated
my %is_master; # dbid -> bool (is cluster master)
foreach my $dbid ( grep { !$added{$_} } keys %dbinfo ) {
foreach my $role ( keys %{ $rolebyid{$dbid} || {} } ) {
next unless $role =~ /^cluster(\d+)(.*)/;
$minclust{$dbid} = $1 if !$minclust{$dbid} || $1 < $minclust{$dbid};
$is_master{$dbid} ||= $2 eq "" || $2 eq "a" || $2 eq "b";
}
}
# then misc
foreach my $id (
sort { $dbinfo{$a}->{name} cmp $dbinfo{$b}->{name} }
grep { !$added{$_} && !$minclust{$_} } keys %dbinfo
)
{
$add->($id);
}
# then clusters, in order
foreach my $id (
sort {
$minclust{$a} <=> $minclust{$b}
|| $is_master{$b} <=> $is_master{$a}
|| $dbinfo{$a}->{name} cmp $dbinfo{$b}->{name}
}
grep { !$added{$_} && $minclust{$_} } keys %dbinfo
)
{
$add->($id);
}
return @ids;
}
sub check_report {
foreach my $dbid (
sort { $dbinfo{$a}->{name} cmp $dbinfo{$b}->{name} }
keys %dbinfo
)
{
my $d = $dbinfo{$dbid};
die "Bogus DB: $dbid" unless $d;
my $db = $get_root_handle->( $d->{name} );
unless ($db) {
print "$d->{name}\t?\t?\t?\n";
next;
}
my $dbs = $db->selectcol_arrayref("SHOW DATABASES");
foreach my $dbname (@$dbs) {
$db->do("USE $dbname");
my $ts = $db->selectall_hashref( "SHOW TABLE STATUS", "Name" );
foreach my $tn ( sort keys %$ts ) {
my $v = $ts->{$tn};
my $ut = $v->{Check_time} || "0000-00-00 00:00:00";
$ut =~ s/ /,/;
print "$d->{name}\t$dbname\t$tn\t$ut\t$v->{Type}-$v->{Row_format}\t$v->{Rows}\n";
}
}
}
exit 0;
}
sub rate_report {
my %prev; # dbid -> [ time, questions ]
while (1) {
print "\n";
my $sum = 0;
foreach my $dbid ( sorted_dbids() ) {
my $d = $dbinfo{$dbid};
die "Bogus DB: $dbid" unless $d;
my $db = $get_root_handle->( $d->{name} );
next unless $db;
my ( undef, $qs ) = $db->selectrow_array("SHOW STATUS LIKE 'Questions'");
my $now = Time::HiRes::time();
my $cur = [ $now, $qs ];
if ( my $old = $prev{$dbid} ) {
my $dt = $now - $old->[0];
my $qnew = $qs - $old->[1];
my $rate = ( $qnew / $dt );
$sum += $rate;
printf "%20s: %7.01f q/s\n", $d->{name}, $rate;
}
$prev{$dbid} ||= $cur;
}
printf "%20s: %7.01f q/s\n", "SUM", $sum;
sleep 1;
}
}
sub debug {
return unless $opt_verbose;
warn $_[0], "\n";
}