如何将虚拟机迁移至新的存储账户
Contributors: Blair Chen, Sun Wei, Liu Qing
我们在使用Azure的过程中,有时会需要把Azure虚拟机的相关VHD文件从现有的存储账户迁移到其它存储账户。比如说,当现有的存储账户下面已经存在超过了40个活跃使用的VHD文件,而由于每个VHD文件作为一个Azure Blob对象都可以产生上至每秒500个请求,导致单个存储账户的每秒20000个请求的上限可能会被超出,从而触发限制的机制。在这种情况下,建议需要将这个存储账户下的某些虚机尽快迁移至其它的存储账户,以免触发限制机制。
在讨论如何把虚拟机的VHD从一个存储账户迁移到另外一个存储账户之前,我们先来回顾一下Azure虚拟机的一些基本概念。首先,对于Azure IaaS的虚拟机在设计上是把计算和存储是分离开的。在创建虚拟机的时候,所有持久VHD(系统盘,数据盘VHD文件)都是创建在Azure存储里,而不是直接创建在虚机所处于的物理节点上。虚拟机启动的时候,会直接从存储账户里的VHD文件启动引导操作系统,存储账户中VHD文件本质上是一个blob文件。其次,创建VM的时候,Azure只允许把VHD创建在和虚拟机在同一个区域(比如北京或者上海的数据中心)的存储里,这主要是为了保证计算节点和存储之间的网络延迟尽可能小,从而保证虚拟机的IO性能。
迁移之前的准备计划
如果现有环境已经是生产环境,那么在迁移之前需要有周全的准备和计划,以确保最小化业务系统的中断时间。下面列出了在迁移之前需要检查以及准备的一些事项。
1. 梳理虚机迁移的源存储账户和目标存储账户的对应列表。
首先需要基于上述限制指标梳理一个详细的迁移列表,包括哪些虚机需要迁移,每台虚机迁移的源存储账户以及目标存储账户。如果目标存储账户还不存在,那么可以把目标存储账户先创建好备用。
2. 确认现有的部署架构是否需要虚机的内部IP地址需要保持不变。
由于在虚机的迁移过程中会删除现有的虚机,然后基于迁移至目标存储账户的VHD文件重新创建虚机,在这个过程中虚机的内部IP地址有可能会改变。所以需要确认现有的部署架构中是否对现有虚机的IP地址有任何依赖关系,比如是否使用了内部IP地址进行一些应用层的配置,IP地址的改变是否会影响应用的某些连接等等。
如果需要内部IP地址保持不变,可以在基于迁移后的VHD文件创建虚机时使用静态IP地址来确保虚机能够沿用之前的IP地址。
3. 确认现有的部署架构是否需要虚机的公有虚拟IP (VIP )地址保持不变。
如果需要迁移的虚机是其所属的云服务下面的唯一虚机,那么迁移的过程可能会导致这台虚机及其所属的云服务的公有虚拟IP地址变化。主要因为在这台虚机被删除掉后并且还未被重新创建之前这个时间段内,其所属云服务下面没有挂载任何虚机,这个状态下云服务的VIP资源将会被释放。而当新的虚机在这个云服务下重新被创建后,新的VIP则会分配给云服务。
如果确认需要保持现有的VIP不变,比如说某些客户端不是用DNS域名而是使用IP地址来指向Azure中的云服务,那么可以在迁移中增加一个步骤,比如在迁移这类虚机之前先在其所属的云服务下面创建一个A0型号的虚机。这样来确保在整个迁移过程中这个云服务下面始终至少有一台虚机,这样VIP资源就不会被释放,公有IP地址也就不会改变。在迁移完成后即可删除这个临时的A0虚机。
4. 检查并导出现有虚机的所有属性及配置信息。
通常虚机的所有属性及配置在迁移前后都是需要保持不变的,比如虚机名称、大小型号、开放的端口、所属的云服务、地缘组、虚拟网络等。所以建议导出虚机、虚拟网络的所有属性及配置信息至XML文件以保存记录。
5. 准备回滚计划及脚本。
建议要准备回滚计划,以备万一迁移失败或者时间过长无法在运维窗口时间内完成时,能够快速回滚至原有状态。迁移之前应该准备好回滚的脚本,确保虚机能够随时恢复至原有状态,避免出现预期外的系统中断。
6. 测试存储账户之间的VHD拷贝速度。
在存储账户之间拷贝文件是一个异步的操作,没有速度的SLA。但通常情况如果源和目标存储账户属于同一个物理集群的话,速度一般还是比较快的。根据经验,通常可以在10秒到3分钟之间完成一个100GB的VHD文件拷贝。
但是,如果源和目标存储账户不在同一个物理集群中,那个拷贝文件就可能会比较慢,这就需要运维窗口有足够的时间来完成迁移。同时,也可以用以下方法来控制迁移的时间长度。
a) 如果源存储账户属于一个地缘组,那么可以把目标存储账户也创建在同一个地缘组当中。这样可以确保源和目标存储账户属于同一个集群,从而避免跨集群拷贝文件可能会慢的问题。
b) 也可以进行应用层面的迁移,比如将应用以及相关的数据从现有的虚机中迁移出来,重新部署到新的虚机中。
7. 在生产环境执行迁移之前,先用一台测试虚机测试迁移脚本。
在迁移任何生产环境的虚机之前,建议使用测试虚机对测试脚本进行完整的测试。
8. 准备一台Azure虚机用来执行迁移脚本,避免执行脚本环境的网络问题。
有时用户自己的本地网络可能会有各种限制以及不稳定的问题,这可能会导致迁移脚本在执行过程中由于网络的临时不稳定或丢包造成迁移的中断,造成不必要的麻烦。所以,建议准备一台Azure虚机配置好相关的powershell环境,用来执行迁移脚本。
9. 检查确保所有虚机中的应用、数据库以及用户数据都已经做好备份。
虚机迁移步骤
如果需要迁移的虚机是生产环境,建议按照顺序逐一迁移虚机,确保一台虚机迁移成功没有问题后再迁移下一台。如果出现问题或失败,可以立即执行回滚脚本将虚机恢复至其原有状态,最小化系统中断时间。
下面是一个单台虚机的迁移步骤说明供参考。注意由于每个用户的环境和需求不同,建议可以根据具体情况参考下述步骤进行补充和调整。
1. 导出虚拟机机配置文件
迁移的第一个准备工作是把虚拟机的配置文件导出,这主要是为了后面能用同样的配置信息把虚拟机重新创建出来。下面是导出虚拟机配置文件的powershell代码:
$sourceVM = Get-AzureVM –ServiceName $cloudServiceName –Name $vmName
$sourceVM | Export-AzureVM -Path $vmConfigurationPath
2. 远程登陆进入虚机OS, 在OS中关闭虚机
3. 删除虚机并保留VHD文件
需要把所有的磁盘和vhd文件的映射关系通过Remove-AzureDisk来解除。
4. 复制VHD文件到目标存储账户
Azure存储提供了从一个存储账户复制文件到另外一个存储账户的API,这里有一些需要注意的地方:首先,跨存储账户的blob复制的PowerShell命令Start-AzureStorageBlobCopy是一个异步命令,也就是说会立刻返回,调用者需要通过Get-AzureStorageBlobCopyState轮询来获得复制的最新进度。其次,blob的复制是没有SLA的,所以在跨物理集群复制blob的时候,可能会遇到花费时间比较长的情况。下面便是把虚拟机所有的VHD文件从源存储账户复制到目标存储账户的代码示例。
foreach($disk in $allDisks)
{
$elapsed = [System.Diagnostics.Stopwatch]::StartNew()
$blobName = $disk.MediaLink.Segments[2]
Write-Host "$blobName copy started at $(get-date)"
Write-Host "Source =" $sourceContext.BlobEndpoint
Write-Host "Dest =" $destContext.BlobEndpoint
$targetBlob = Start-AzureStorageBlobCopy -SrcContainer vhds -SrcBlob $blobName -DestContainer vhds -DestBlob $blobName -Context $sourceContext -DestContext $destContext -Force
do
{
if ($copyState.TotalBytes -gt 0 )
{
$percent = ($copyState.BytesCopied / $copyState.TotalBytes) * 100
Write-Host "$blobName copy completed $('{0:N2}' -f $percent)%"
}
Start-Sleep -Seconds 10
$copyState = $targetBlob | Get-AzureStorageBlobCopyState
} while ($copyState.Status -ne "Success")
Write-Host "$blobName copy ended at $(get-date)"
Write-Host "$blobName copy total elapsed time: $($elapsed.Elapsed.ToString())"
If ($disk -eq $sourceOSDisk)
{
$destOSDisk = $targetBlob
}
Else
{
$destDataDisks += $targetBlob
}
}
5. 创建Azure磁盘
在完成把VHD复制到目标存储账户后,下一步便是把所有的磁盘通过Add-Disk创建出来,这里需要确保每个磁盘的名字和顺序和原来一样。
Add-AzureDisk -OS $sourceOSDisk.OS -DiskName $sourceOSDisk.DiskName -MediaLocation $destOSDisk.ICloudBlob.Uri
foreach($currenDataDisk in $destDataDisks)
{
$diskName = ($sourceDataDisks | ? {$_.MediaLink.Segments[2] -eq $currenDataDisk.Name}).DiskName
Add-AzureDisk -DiskName $diskName -MediaLocation $currenDataDisk.ICloudBlob.Uri
}
6. 创建虚机
最后,用Import-AzureVM和New-AzureVM这2个powershell cmdlet就可以将虚拟机按原有的配置重新建出来。如果需要,可以设置静态IP以确保虚机沿用之前的IP地址不变,如下面样例脚本所示。
Import-AzureVM -Path $vmConfigurationPath | Set-AzureStaticVNetIP -IPAddress $sourceVM.IpAddress | New-AzureVM -ServiceName $cloudServiceName -VNetName $vnetName -WaitForBoot
7. 检查虚机是否开启以及运行正常。
远程登陆进入虚机,检查确保其中的应用及数据库等均正常运行。
下面是一个完整的powershell迁移示例脚本,包含了上述步骤和逻辑供参考。
<#
.EXAMPLE
.\Move-AzureVM.ps1 -cloudServiceName "vhdmove1" -vmName "vhdmove1" -destStorageAccountName "teststorage" -vnetName "RVNET-SH01"
.\Move-AzureVM.ps1 -cloudServiceName "vhdmove2" -vmName "vhdmove2" -destStorageAccountName "teststorage " -vnetName "RVNET-SH01"
.\Move-AzureVM.ps1 -cloudServiceName "vhdmove3" -vmName "vhdmove3" -destStorageAccountName "teststorage " -vnetName "RVNET-SH01"
#>
param
(
[string] $cloudServiceName, #Cloud service name of VM
[string] $vmName, # VM Name
[string] $destStorageAccountName, #Target storage account name
[string] $vnetName # Virtual network name of VM
)
$ErrorActionPreference = "Stop"
try{ stop-transcript|out-null }
catch [System.InvalidOperationException] { }
$workingDir = (Get-Location).Path
$log = $workingDir + "\VM-" + $vmName + ".log"
Start-Transcript -Path $log -Append -Force
Write-Host "==========Migration Setting ==========="
Write-Host "Cloud Service = $cloudServiceName "
Write-Host " VM = $vmName"
Write-Host " Dest Storage = $destStorageAccountName "
Write-Host " VNET = $vnetName"
Write-Host "========================================"
$currentSubscription = Get-AzureSubscription -Current
$cloudEnv = $currentSubscription.Environment
$vmConfigurationPath = $workingDir + "\VM-" + $vmName + ".xml"
$sourceVM = Get-AzureVM – ServiceName $cloudServiceName –Name $vmName
# 首先检查虚机的状态是否为stopped. 如果不是,那么提示用户先关掉虚机。
if ( $sourceVM.Status -ne "StoppedVM" )
{
Write-Error "ERROR: please make sure $vmName is shut down"
Exit
}
Write-Host "Exporting VM configuration to $vmConfigurationPath"
$sourceVM | Export-AzureVM -Path $vmConfigurationPath
$sourceOSDisk = $sourceVM.VM.OSVirtualHardDisk
$sourceDataDisks = $sourceVM.VM.DataVirtualHardDisks
# Get source storage account information
$sourceStorageAccountName = $sourceOSDisk.MediaLink.Host -split "\." | select -First 1
$sourceStorageAccount = Get-AzureStorageAccount –StorageAccountName $sourceStorageAccountName
$sourceStorageKey = (Get-AzureStorageKey -StorageAccountName $sourceStorageAccountName).Primary
$sourceContext = New-AzureStorageContext –StorageAccountName $sourceStorageAccountName -StorageAccountKey $sourceStorageKey -Environment $cloudEnv
# Get destination storage account information
$destStorageAccount = Get-AzureStorageAccount | Where {$_.StorageAccountName -eq $destStorageAccountName }
$destStorageAccountName = $destStorageAccount.StorageAccountName
if ($destStorageAccount -eq $null)
{
if ($sourceStorageAccount.Location -ne $null)
{
New-AzureStorageAccount -StorageAccountName $destStorageAccountName -Location $sourceStorageAccount.Location
}
else {
New-AzureStorageAccount -StorageAccountName $destStorageAccountName -AffinityGroup $sourceStorageAccount.AffinityGroup
}
$destStorageAccount = Get-AzureStorageAccount -StorageAccountName $destStorageAccountName
}
$destStorageKey = (Get-AzureStorageKey -StorageAccountName $destStorageAccountName).Primary
$destContext = New-AzureStorageContext –StorageAccountName $destStorageAccountName -StorageAccountKey $destStorageKey -Environment $cloudEnv
if ((Get-AzureStorageContainer -Context $destContext -Name vhds -ErrorAction SilentlyContinue) -eq $null)
{
New-AzureStorageContainer -Context $destContext -Name vhds
}
Write-Host "Remove VM " $vmName
Remove-AzureVM –§CServiceName $cloudServiceName –Name $vmName
# Copy all vhd disk blobs, including both OS and data disks
$allDisks = @($sourceOSDisk) +$sourceDataDisks
$destDataDisks = @()
Write-Host "Waiting utill all vhd disk locks are released"
do
{
Start-Sleep -Seconds 10
$disksInUse = Get-AzureDisk | Where-Object { ($_.AttachedTo.RoleName -eq $vmName) -and ($_.AttachedTo.HostedServiceName -eq $cloudServiceName) }
Write-Host "Disk in use: " $disksInUse.Count
} while (($disksInUse -ne $null) -or ($disksInUse.Count -gt 0))
foreach($disk in $allDisks)
{
Write-Host "Remove VM Disk " $disk.DiskName
Remove-AzureDisk -DiskName $disk.DiskName
}
foreach($disk in $allDisks)
{
$elapsed = [System.Diagnostics.Stopwatch]::StartNew()
$blobName = $disk.MediaLink.Segments[2]
Write-Host "$blobName copy started at $(get-date)"
Write-Host "Source =" $sourceContext.BlobEndpoint
Write-Host "Dest =" $destContext.BlobEndpoint
$targetBlob = Start-AzureStorageBlobCopy -SrcContainer vhds -SrcBlob $blobName -DestContainer vhds -DestBlob $blobName -Context $sourceContext -DestContext $destContext -Force
do
{
if ($copyState.TotalBytes -gt 0 )
{
$percent = ($copyState.BytesCopied / $copyState.TotalBytes) * 100
Write-Host "$blobName copy completed $('{0:N2}' -f $percent)%"
}
Start-Sleep -Seconds 10
$copyState = $targetBlob | Get-AzureStorageBlobCopyState
} while ($copyState.Status -ne "Success")
Write-Host "$blobName copy ended at $(get-date)"
Write-Host "$blobName copy total elapsed time: $($elapsed.Elapsed.ToString())"
If ($disk -eq $sourceOSDisk)
{
$destOSDisk = $targetBlob
}
Else
{
$destDataDisks += $targetBlob
}
}
# Create OS and data disks
Write-Host "Add VM OS Disk " $destOSDisk.MediaLink
Add-AzureDisk -OS $sourceOSDisk.OS -DiskName $sourceOSDisk.DiskName -MediaLocation $destOSDisk.ICloudBlob.Uri
foreach($currenDataDisk in $destDataDisks)
{
$diskName = ($sourceDataDisks | ? {$_.MediaLink.Segments[2] -eq $currenDataDisk.Name}).DiskName
Write-Host "Add VM Data Disk " $currenDataDisk.ICloudBlob.Uri
Add-AzureDisk -DiskName $diskName -MediaLocation $currenDataDisk.ICloudBlob.Uri
}
Write-Host "Import VM from " $vmConfigurationPath
Set-AzureSubscription -SubscriptionName $currentSubscription.SubscriptionName -CurrentStorageAccountName $destStorageAccountName
do
{
Write-Host $sourceVM.IpAddress " Waiting till ip becomes available"
Sleep -Seconds 10
$ip = Test-AzureStaticVNetIP –VNetName $vnetName –IPAddress $sourceVM.IpAddress
} while ( $ip.IsAvailable -ne $true )
# Import VM with previous exported configuration plus vnet info
Import-AzureVM -Path $vmConfigurationPath | Set-AzureStaticVNetIP -IPAddress $sourceVM.IpAddress | New-AzureVM -ServiceName $cloudServiceName -VNetName $vnetName -WaitForBoot
Stop-Transcript -ErrorAction SilentlyContinue