lvm2defrag/rearrange.php

650 lines
17 KiB
PHP

<?php
class Location
{
public $pv, $offset, $size;
function __construct($p,$o,$s)
{
$this->pv = $p;
$this->offset = $o;
$this->size = $s;
}
function GetOverlapType(Location $b)
{
/* Return values: 0 = no overlap,
1 = some overlap,
2 = this includes b,
3 = b includes this,
5 = both */
if($b->pv != $this->pv) return 0;
$abeg = $this->offset;
$aend = $this->size + $abeg;
$bbeg = $b->offset;
$bend = $b->size + $bbeg;
if($abeg >= $bend || $bbeg >= $aend) return 0; // no overlap
if($abeg == $bbeg && $aend == $bend) return 5;
if($abeg <= $bbeg && $aend >= $bend) return 2; // true overlap (a includes b)
if($bbeg <= $abeg && $bend >= $aend) return 3; // true overlap (b includes a)
return 1; // some overlap
}
};
/* An obj is a stripe of a logical volume. */
class Obj /* the name of the obj is insignificant. */
{
public $size; // Number of extents
public $name;
public $cur_pv, $cur_ofs; // Where it is now
public $goal_pv, $goal_ofs; // Where it should go
public $move_pv, $move_ofs; // Where it is being moved at the call of M...()
public $move_reason;
function __construct($name, $size)
{
$this->name = $name;
$this->size = $size;
$this->cur_pv = false; $this->cur_ofs = 0;
$this->goal_pv = false; $this->goal_ofs = 0;
$this->move_pv = false; $this->move_ofs = 0;
}
function AtHome() { return $this->cur_pv == $this->goal_pv
&& $this->cur_ofs == $this->goal_ofs; }
function GetGoal() { return new Location($this->goal_pv, $this->goal_ofs, $this->size); }
function GetMove() { return new Location($this->move_pv, $this->move_ofs, $this->size); }
function GetLoc() { return new Location($this->cur_pv, $this->cur_ofs, $this->size); }
};
class Pv
{
public $name; // Name of the device
public $size; // Number of extents
public $contents; // What is where.
// Array(Obj), sorted by cur_ofs.
function __construct($name)
{
$this->name = $name;
$this->size = 0;
$this->contents = Array();
}
};
function SmallestHoleCom($a,$b)
{
return $a->size - $b->size;
}
function LargestHoleCom($a,$b)
{
return -SmallestHoleCom($a,$b);
}
function SmallestPosCom($a,$b)
{
if($a->offset == $b->offset) return $a->size - $b->size;
return $a->offset - $b->offset;
}
function LvSizeCom($a,$b)
{
return $a['size'] - $b['size'];
}
class VG
{
public $name; // Name of the volume group
public $objs;
public $pvs;
public $lvs;
public $scheduled_moves;
function __construct($name)
{
$this->name = $name;
$this->objs = Array();
$this->pvs = Array();
$this->lvs = Array();
$this->scheduled_moves = 0;
}
function ScheduleMove(&$lv, $loc, $why)
{
$lv->move_pv = $loc->pv;
$lv->move_ofs = $loc->offset;
$lv->move_reason = $why;
++$this->scheduled_moves;
}
function ScheduleAllGoalable()
{
$res = Array();
foreach($this->lvs as &$lv)
if(!$lv->AtHome())
{
$goal = $lv->GetGoal();
$occupiers = $this->GetOccupierList($goal);
if(empty($occupiers))
$this->ScheduleMove($lv, $goal, 'Final goal');
}
return $res;
}
function HasScheduled()
{
return $this->scheduled_moves;
}
function FlushSchedules()
{
$rightnow = Array();
foreach($this->lvs as $lvname => &$lv)
if($lv->move_pv !== false)
{
$rightnow[$lvname] = Array('size' => $lv->size,
'goal' => $lv->GetMove(),
'reason' => $lv->move_reason);
}
uasort($rightnow, 'LvsizeCom');
if(count($rightnow) > 1) print "## Begin group of moves that could be done in parallel\n";
if(false)
{
foreach($rightnow as $lvname => $info)
$this->MoveLv($this->lvs[$lvname], $info['goal'], $info['reason']);
}
else
{
/* Group the moves by pairs of physical devices */
$moves = Array();
foreach($rightnow as $lvname => $info)
{
$lv = &$this->lvs[$lvname];
$moves[$lv->cur_pv][$lv->move_pv][] = &$lv;
}
foreach($moves as $src_pv => $goal_pv_list)
foreach($goal_pv_list as $goal_pv => $lv_list)
$this->MoveMultipleLv($src_pv, $goal_pv, $lv_list);
}
if(count($rightnow) > 1) print "## End group\n";
foreach($this->lvs as $lvname => &$lv)
{
$lv->move_pv = false;
if($lv->AtHome()) unset($this->lvs[$lvname]);
}
$this->scheduled_moves = 0;
}
function Optimize()
{
for(;;)
{
$this->ScheduleAllGoalable();
if($this->HasScheduled()) { $this->FlushSchedules(); continue; }
$found_troubles = false;
foreach($this->lvs as &$lv)
{
if($lv->AtHome()) continue;
$found_troubles = true;
#print_r($lv);
// Because this lv is not at goal yet,
// we can assume that there is an obstacle.
// Figure out what that obstacle is.
$this->TryMakeRoomTo($lv->GetGoal(), $lv->name, null);
// Now there should be no occupiers anymore.
break;
} // end search for troubled seekers
#print_r($this->lvs);
if(!$found_troubles) break;
} // end infinite loop
}
function TryMakeRoomTo(Location $from, $whosebidding, $exclude = null)
{
if($exclude === null) $exclude = Array($from);
$desirability = 0; // Desirability of current best solution
$occupiers = $this->GetOccupierList($from);
foreach($occupiers as $lvname)
{
$occu = &$this->lvs[$lvname];
$occu_goal = $occu->GetGoal();
$occu_occupiers = $this->GetOccupierList($occu_goal);
if(empty($occu_occupiers))
{
if($desirability > 2) return; $desirability = 2;
// Note: This shouldn't happen, but it improves robustness to have it here.
$this->ScheduleMove($occu, $occu_goal, 'Occupier goal');
continue;
}
/* Find out where to move the occupier */
$hole = $this->FindSmallestHoleLargerThan($occu->size, $exclude);
if($hole !== false)
{
if($desirability > 1) return; $desirability = 1;
$this->ScheduleMove($occu, $hole, 'Moving away');
$exclude[] = $hole;
continue;
}
# $excludetmp = $exclude;
# $excludetmp[] = $occu_goal;
# $this->TryMakeRoomTo($occu_goal, $excludetmp);
if($desirability > 0) return; $desirability = 0;
$hole = $this->FindLargestHoleButNotAnyOf($exclude);
if($hole === false)
{
if($occu->name == $whosebidding)
{
/* Special case: Block is overlapping its own destined goal */
$hole = new Location('', 0, abs($occu->cur_ofs - $occu->goal_ofs));
}
else
{
print "# UNABLE TO CONTINUE, NO HOLES FOR {$occu->name}, FOR $whosebidding?\n";
$holes = $this->ListHoles(Array());
print_r($holes);
return;
}
}
$this->SplitLv($occu, $hole->size, $from);
break;
}
}
function FindSmallestHoleLargerThan($size, $excludelist)
{
$holes = $this->ListHoles($excludelist);
usort($holes, 'SmallestHoleCom');
foreach($holes as $hole)
if($hole->size >= $size)
{
/* Give a position from the end of the hole */
$hole->offset += $hole->size - $size;
return $hole;
}
return false;
}
function FindLargestHoleButNotAnyOf($excludelist)
{
$holes = $this->ListHoles($excludelist);
if(empty($holes)) return false;
usort($holes, 'LargestHoleCom');
return $holes[0];
}
function GetOccupierList(Location $loc)
{
$res = Array();
$begin = $loc->offset;
$end = $loc->offset + $loc->size;
foreach($this->pvs[$loc->pv]->contents as $lv)
{
$lv_begin = $lv->cur_ofs;
$lv_end = $lv_begin + $lv->size;
if($lv_begin < $end && $lv_end > $begin)
$res[] = $lv->name;
}
return $res;
}
function ListHoles($excludelist = Array())
{
$res = Array();
foreach($this->pvs as $pv)
{
$o = 0;
$contents = Array();
foreach($pv->contents as $lv)
$contents[] = new Location($lv->cur_pv, $lv->cur_ofs, $lv->size);
foreach($excludelist as $exclude)
if($pv->name == $exclude->pv)
$contents[] = $exclude;
usort($contents, 'SmallestPosCom');
foreach($contents as $loc)
{
if($loc->offset > $o) $res[] = new Location($pv->name, $o, $loc->offset - $o);
$o = max($o, $loc->offset + $loc->size);
}
if($pv->size > $o) $res[] = new Location($pv->name, $o, $pv->size - $o);
}
return $res;
}
function MoveLv(&$lv, $goal, $reason = '')
{
if(!isset($this->pvs[$lv->cur_pv]->contents[$lv->cur_ofs]))
{
print "# HUH, GHOST LV?\n";
}
unset($this->pvs[$lv->cur_pv]->contents[$lv->cur_ofs]);
$spv = $lv->cur_pv;
$sbeg = $lv->cur_ofs;
$send = $lv->cur_ofs + $lv->size - 1;
$lv->cur_pv = $goal->pv;
$lv->cur_ofs = $goal->offset;
$dpv = $lv->cur_pv;
$dbeg = $lv->cur_ofs;
$dend = $lv->cur_ofs + $lv->size - 1;
print "# Moving {$lv->name} ($reason)\n";
print "echo 'Moving {$lv->name} ($reason)'\n";
print "pvmove --alloc anywhere $spv:$sbeg-$send $dpv:$dbeg-$dend\n";
$this->pvs[$lv->cur_pv]->contents[$lv->cur_ofs] = &$lv;
}
function MoveMultipleLv($spv, $dpv, $lvlist)
{
$src_pv = &$this->pvs[$spv];
$dest_pv = &$this->pvs[$dpv];
#print "## Begin cluster\n";
foreach($lvlist as $l=>&$lv)
if($lv->cur_pv != $spv || $lv->move_pv != $dpv)
{
print "## ERRONEOUS MOVEMULTIPLELV ($spv != {$lv->cur_pv} OR $dpv != {$lv->move_pv})\n";
}
while(!empty($lvlist))
{
/*
TODO: Because pvmove works by creating a temporary mirror LV,
We must split the moves so that we only ever move as
large spans as there are temporary contiguous spaces
available >= size of the combined move.
*/
$move_size = 0;
$excludelist = Array();
foreach($lvlist as $l=>&$lv)
{
$move_size += $lv->size;
$excludelist[] = $lv->GetMove();
}
$largest_temp_room = $this->FindLargestHoleButNotAnyOf($excludelist);
#print_r($excludelist);
#print_r($largest_temp_room);
$largest_temp_size = $largest_temp_room ? $largest_temp_room->size : 0;
print "# Largest temp size $largest_temp_size, desire $move_size\n";
$group = $lvlist;
if($largest_temp_size < $move_size)
{
print "# Not enough contiguous temporary room for a multipart pvmove\n";
/* Not enough room to move all at once!
* Try creating a sub group and moving them.
*/
$move_size = 0;
$group = Array();
$excludelist = Array();
foreach($lvlist as $l=>&$lv)
{
$tmpe = $excludelist;
$tmpe[] = $lv->GetMove();
$tmp_room = $this->FindLargestHoleButNotAnyOf($tmpe);
$tmp_size = $tmp_room ? $tmp_room->size : 0;
if($move_size + $lv->size <= $tmp_size)
{
$group[$l] = $lv;
$move_size += $lv->size;
$excludelist = $tmpe;
}
}
/* If all the pvs were larger than we can move...
* Bummer! Then we try splitting.
*/
if(empty($group))
{
print "# Not enough room for any of the pvmoves. Splitting a task.\n";
foreach($lvlist as $l=>&$lv)
{
$partsize = $largest_temp_size;
if(!$partsize) $partsize = (int)($lv->size / 2);
$split = $this->SplitLv($lv, $partsize, new Location('',0,0));
unset($lvlist[$l]);
$lvlist[] = &$split[0];
$lvlist[] = &$split[1];
unset($split);
break;
}
continue;
}
}
$mlist = '';
$slist = '';
$dlist = '';
$amount = 0;
foreach($group as $l=>&$lv)
{
unset($src_pv->contents[$lv->cur_ofs]);
$dest_pv->contents[$lv->move_ofs] = &$lv;
$sbeg = $lv->cur_ofs;
$send = $lv->cur_ofs + $lv->size - 1;
$lv->cur_pv = $dpv;
$lv->cur_ofs = $lv->move_ofs;
$dbeg = $lv->cur_ofs;
$dend = $lv->cur_ofs + $lv->size - 1;
$amount += $dend - $dbeg + 1;
print "# Moving {$lv->name} ({$lv->size}) ({$lv->move_reason})\n";
print "echo 'Moving {$lv->name} ({$lv->size}) ({$lv->move_reason})'\n";
$slist .= ":$sbeg-$send";
$dlist .= ":$dbeg-$dend";
unset($lvlist[$l]);
}
print "pvmove --alloc anywhere {$spv}$slist {$dpv}$dlist\n";
}
#print "## End cluster\n";
}
function SplitLv(&$lv, $split_size, $prefer_region)
{
/* Check out which way is better for splitting */
$begin_type = $prefer_region->GetOverlapType(
new Location($lv->cur_pv,
$lv->cur_ofs,
$split_size));
$end_type = $prefer_region->GetOverlapType(
new Location($lv->cur_pv,
$lv->cur_ofs + $lv->size - $split_size,
$split_size));
if($end_type == 3 || $end_type == 5)
{
$part2size = $split_size;
$part1size = $lv->size - $part2size;
}
else
{
$part1size = $split_size;
$part2size = $lv->size - $part1size;
}
$newlv1 = clone $lv; $newlv1->name .= " p1"; $newlv1->size = $part1size;
$newlv2 = clone $lv; $newlv2->name .= " p2"; $newlv2->size = $part2size;
echo "# LV split {$lv->name} ($part1size + $part2size = {$lv->size})\n";
$newlv2->cur_ofs += $part1size;
$newlv2->goal_ofs += $part1size;
$newlv2->move_ofs += $part1size;
unset($this->pvs[$lv->cur_pv]->contents[$lv->cur_ofs]);
unset($this->lvs[$lv->name]);
$this->pvs[$newlv1->cur_pv]->contents[$newlv1->cur_ofs] = &$newlv1;
$this->pvs[$newlv2->cur_pv]->contents[$newlv2->cur_ofs] = &$newlv2;
$this->lvs[$newlv1->name] = &$newlv1;
$this->lvs[$newlv2->name] = &$newlv2;
return Array(&$newlv1, &$newlv2);
}
};
$vgs = Array();
unset($vg); unset($pv); unset($pv_ofs);
$vg = false; $pv = false; $pv_ofs = 0;
foreach(preg_split("/\r?\n/",file_get_contents('dump.txt')) as $line)
{
if(preg_match('/^!! /', $line))
{
$vg = &$vgs[substr($line,3)];
if(!$vg) $vg = new VG(substr($line,3));
}
elseif(preg_match('/^! /', $line))
{
$pvname = substr($line,2);
$pv = &$vg->pvs[$pvname];
if(!$pv) $pv = new PV($pvname);
$pv_ofs = 0;
}
elseif(preg_match('/^[(0-9]/', $line))
{
$tokens = preg_split("/[\t ]+/", $line);
if($tokens[0][0] == '(')
$pv_ofs += (int)substr($tokens[0], 1);
else
{
$lvname = $tokens[1];
$lv = &$vg->lvs[$lvname];
if(!$lv) $lv = new Obj($lvname, (int)$tokens[0]);
$lv->cur_pv = $pv->name;
$lv->cur_ofs = $pv_ofs;
$pv->contents[$pv_ofs] = &$lv;
$pv_ofs += (int)$tokens[0];
}
$pv->size = $pv_ofs;
}
}
unset($vg); unset($pv); unset($lv); unset($pv_ofs);
$failures = 0;
$vg = false; $pv = false; $pv_ofs = 0;
$pvsize2 = Array();
foreach(preg_split("/\r?\n/",file_get_contents('rearrange.txt')) as $line)
{
if(preg_match('/^!! /', $line))
{
$vg = &$vgs[substr($line,3)];
if(!$vg) $vg = new VG(substr($line,3));
}
elseif(preg_match('/^! /', $line))
{
$pvname = substr($line,2);
$pv = &$vg->pvs[$pvname];
if(!$pv)
{
$pv = new PV($pvname);
print "ERROR: Volume {$pvname} was not mentioned in current, is in goal\n";
$failures += 1;
}
$pv_ofs = 0;
}
elseif(preg_match('/^[(0-9]/', $line))
{
$tokens = preg_split("/[\t ]+/", $line);
if($tokens[0][0] == '(')
$pv_ofs += (int)substr($tokens[0], 1);
else
{
$lvname = $tokens[1];
$lv = &$vg->lvs[$lvname];
if(!$lv)
{
$lv = new Obj($lvname, (int)$tokens[0]);
print "ERROR: Object {$lvname} was not mentioned in current, is in goal\n";
print " I won't create volumes, you do that yourself\n";
$failures += 1;
}
$lv->goal_pv = $pv->name;
$lv->goal_ofs = $pv_ofs;
$pv_ofs += (int)$tokens[0];
}
$pvsize2[$pv->name] = $pv_ofs;
}
}
unset($vg); unset($pv); unset($lv); unset($pv_ofs);
foreach($vgs as $vg)
{
foreach($vg->pvs as $pvname => &$pv)
if($pv->size != $pvsize2[$pvname])
{
print "Mismatch in PV $pvname size: original = {$pv->size}, new = {$pvsize2[$pvname]}\n";
}
foreach($vg->lvs as $lvname => &$lv)
if(!$lv->goal_pv)
{
print "ERROR: Object {$lvname} was not mentioned in goal, was in current\n";
print " I won't destroy volumes, you do that yourself\n";
$failures += 1;
}
}
if($failures)
{
print "Errors detected. Stop.\n";
return;
}
#print_r($vgs);
foreach($vgs as &$vg)
{
$vg->Optimize();
foreach($vg->pvs as $pv)
ksort($pv->contents);
#$holes = $vg->ListHoles(Array());
#print_r($holes);
}
#print_r($vgs);