mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-21 00:24:07 +08:00
337 lines
10 KiB
Go
337 lines
10 KiB
Go
// Copyright 2019 Yunion
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package azure
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/storage"
|
|
"github.com/Microsoft/azure-vhd-utils/vhdcore/block/bitmap"
|
|
"github.com/Microsoft/azure-vhd-utils/vhdcore/common"
|
|
"github.com/Microsoft/azure-vhd-utils/vhdcore/diskstream"
|
|
"github.com/Microsoft/azure-vhd-utils/vhdcore/footer"
|
|
"github.com/Microsoft/azure-vhd-utils/vhdcore/validator"
|
|
|
|
"yunion.io/x/onecloud/pkg/multicloud/azure/concurrent"
|
|
"yunion.io/x/onecloud/pkg/multicloud/azure/progress"
|
|
)
|
|
|
|
// DiskUploadContext type describes VHD upload context, this includes the disk stream to read from, the ranges of
|
|
// the stream to read, the destination blob and it's container, the client to communicate with Azure storage and
|
|
// the number of parallel go-routines to use for upload.
|
|
//
|
|
|
|
type DataWithRange struct {
|
|
Range *common.IndexRange
|
|
Data []byte
|
|
}
|
|
|
|
type DiskUploadContext struct {
|
|
VhdStream *diskstream.DiskStream // The stream whose ranges needs to be uploaded
|
|
AlreadyProcessedBytes int64 // The size in bytes already uploaded
|
|
UploadableRanges []*common.IndexRange // The subset of stream ranges to be uploaded
|
|
BlobServiceClient storage.BlobStorageClient // The client to make Azure blob service API calls
|
|
ContainerName string // The container in which page blob resides
|
|
BlobName string // The destination page blob name
|
|
Parallelism int // The number of concurrent goroutines to be used for upload
|
|
Resume bool // Indicate whether this is a new or resuming upload
|
|
MD5Hash []byte // MD5Hash to be set in the page blob properties once upload finishes
|
|
}
|
|
|
|
// oneMB is one MegaByte
|
|
//
|
|
const oneMB = float64(1048576)
|
|
|
|
// Upload uploads the disk ranges described by the parameter cxt, this parameter describes the disk stream to
|
|
// read from, the ranges of the stream to read, the destination blob and it's container, the client to communicate
|
|
// with Azure storage and the number of parallel go-routines to use for upload.
|
|
//
|
|
func Upload(cxt *DiskUploadContext) error {
|
|
// Get the channel that contains stream of disk data to upload
|
|
dataWithRangeChan, streamReadErrChan := GetDataWithRanges(cxt.VhdStream, cxt.UploadableRanges)
|
|
|
|
// The channel to send upload request to load-balancer
|
|
requtestChan := make(chan *concurrent.Request, 0)
|
|
|
|
// Prepare and start the load-balancer that load request across 'cxt.Parallelism' workers
|
|
loadBalancer := concurrent.NewBalancer(cxt.Parallelism)
|
|
loadBalancer.Init()
|
|
workerErrorChan, allWorkersFinishedChan := loadBalancer.Run(requtestChan)
|
|
|
|
// Calculate the actual size of the data to upload
|
|
uploadSizeInBytes := int64(0)
|
|
for _, r := range cxt.UploadableRanges {
|
|
uploadSizeInBytes += r.Length()
|
|
}
|
|
fmt.Printf("\nEffective upload size: %.2f MB (from %.2f MB originally)", float64(uploadSizeInBytes)/oneMB, float64(cxt.VhdStream.GetSize())/oneMB)
|
|
|
|
// Prepare and start the upload progress tracker
|
|
uploadProgress := progress.NewStatus(cxt.Parallelism, cxt.AlreadyProcessedBytes, uploadSizeInBytes, progress.NewComputestateDefaultSize())
|
|
progressChan := uploadProgress.Run()
|
|
|
|
// read progress status from progress tracker and print it
|
|
go readAndPrintProgress(progressChan, cxt.Resume)
|
|
|
|
// listen for errors reported by workers and print it
|
|
var allWorkSucceeded = true
|
|
go func() {
|
|
for {
|
|
fmt.Println(<-workerErrorChan)
|
|
allWorkSucceeded = false
|
|
}
|
|
}()
|
|
|
|
var err error
|
|
L:
|
|
for {
|
|
select {
|
|
case dataWithRange, ok := <-dataWithRangeChan:
|
|
if !ok {
|
|
close(requtestChan)
|
|
break L
|
|
}
|
|
|
|
// Create work request
|
|
//
|
|
containerClinet := cxt.BlobServiceClient.GetContainerReference(cxt.ContainerName)
|
|
blobClient := containerClinet.GetBlobReference(cxt.BlobName)
|
|
req := &concurrent.Request{
|
|
Work: func() error {
|
|
err := blobClient.WriteRange(
|
|
storage.BlobRange{Start: uint64(dataWithRange.Range.Start), End: uint64(dataWithRange.Range.End)},
|
|
bytes.NewReader(dataWithRange.Data),
|
|
&storage.PutPageOptions{},
|
|
)
|
|
if err == nil {
|
|
uploadProgress.ReportBytesProcessedCount(dataWithRange.Range.Length())
|
|
}
|
|
return err
|
|
},
|
|
ShouldRetry: func(e error) bool {
|
|
return true
|
|
},
|
|
ID: dataWithRange.Range.String(),
|
|
}
|
|
|
|
// Send work request to load balancer for processing
|
|
//
|
|
requtestChan <- req
|
|
case err = <-streamReadErrChan:
|
|
close(requtestChan)
|
|
loadBalancer.TearDownWorkers()
|
|
break L
|
|
}
|
|
}
|
|
|
|
<-allWorkersFinishedChan
|
|
uploadProgress.Close()
|
|
|
|
if !allWorkSucceeded {
|
|
err = errors.New("\nUpload Incomplete: Some blocks of the VHD failed to upload, rerun the command to upload those blocks")
|
|
}
|
|
|
|
if err == nil {
|
|
fmt.Printf("\r Completed: %3d%% [%10.2f MB] RemainingTime: %02dh:%02dm:%02ds Throughput: %d Mb/sec %2c ",
|
|
100,
|
|
float64(uploadSizeInBytes)/oneMB,
|
|
0, 0, 0,
|
|
0, ' ')
|
|
|
|
}
|
|
return err
|
|
}
|
|
|
|
// GetDataWithRanges with start reading and streaming the ranges from the disk identified by the parameter ranges.
|
|
// It returns two channels, a data channel to stream the disk ranges and a channel to send any error while reading
|
|
// the disk. On successful completion the data channel will be closed. the caller must not expect any more value in
|
|
// the data channel if the error channel is signaled.
|
|
//
|
|
func GetDataWithRanges(stream *diskstream.DiskStream, ranges []*common.IndexRange) (<-chan *DataWithRange, <-chan error) {
|
|
dataWithRangeChan := make(chan *DataWithRange, 0)
|
|
errorChan := make(chan error, 0)
|
|
go func() {
|
|
for _, r := range ranges {
|
|
dataWithRange := &DataWithRange{
|
|
Range: r,
|
|
Data: make([]byte, r.Length()),
|
|
}
|
|
_, err := stream.Seek(r.Start, 0)
|
|
if err != nil {
|
|
errorChan <- err
|
|
return
|
|
}
|
|
_, err = io.ReadFull(stream, dataWithRange.Data)
|
|
if err != nil {
|
|
errorChan <- err
|
|
return
|
|
}
|
|
dataWithRangeChan <- dataWithRange
|
|
}
|
|
close(dataWithRangeChan)
|
|
}()
|
|
return dataWithRangeChan, errorChan
|
|
}
|
|
|
|
// readAndPrintProgress reads the progress records from the given progress channel and output it. It reads the
|
|
// progress record until the channel is closed.
|
|
//
|
|
func readAndPrintProgress(progressChan <-chan *progress.Record, resume bool) {
|
|
var spinChars = [4]rune{'\\', '|', '/', '-'}
|
|
s := time.Time{}
|
|
if resume {
|
|
fmt.Println("\nResuming VHD upload..")
|
|
} else {
|
|
fmt.Println("\nUploading the VHD..")
|
|
}
|
|
|
|
i := 0
|
|
for progressRecord := range progressChan {
|
|
if i == 4 {
|
|
i = 0
|
|
}
|
|
t := s.Add(progressRecord.RemainingDuration)
|
|
fmt.Printf("\r Completed: %3d%% [%10.2f MB] RemainingTime: %02dh:%02dm:%02ds Throughput: %d Mb/sec %2c ",
|
|
int(progressRecord.PercentComplete),
|
|
float64(progressRecord.BytesProcessed)/oneMB,
|
|
t.Hour(), t.Minute(), t.Second(),
|
|
int(progressRecord.AverageThroughputMbPerSecond),
|
|
spinChars[i],
|
|
)
|
|
i++
|
|
}
|
|
}
|
|
|
|
func ensureVHDSanity(localVHDPath string) error {
|
|
if err := validator.ValidateVhd(localVHDPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validator.ValidateVhdSize(localVHDPath); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func LocateUploadableRanges(stream *diskstream.DiskStream, rangesToSkip []*common.IndexRange, pageSizeInBytes int64) ([]*common.IndexRange, error) {
|
|
var err error
|
|
var diskRanges = make([]*common.IndexRange, 0)
|
|
stream.EnumerateExtents(func(ext *diskstream.StreamExtent, extErr error) bool {
|
|
if extErr != nil {
|
|
err = extErr
|
|
return false
|
|
}
|
|
|
|
diskRanges = append(diskRanges, ext.Range)
|
|
return true
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
diskRanges = common.SubtractRanges(diskRanges, rangesToSkip)
|
|
diskRanges = common.ChunkRangesBySize(diskRanges, pageSizeInBytes)
|
|
return diskRanges, nil
|
|
}
|
|
|
|
func DetectEmptyRanges(diskStream *diskstream.DiskStream, uploadableRanges []*common.IndexRange) ([]*common.IndexRange, error) {
|
|
if diskStream.GetDiskType() != footer.DiskTypeFixed {
|
|
return uploadableRanges, nil
|
|
}
|
|
|
|
fmt.Println("\nDetecting empty ranges..")
|
|
totalRangesCount := len(uploadableRanges)
|
|
lastIndex := int32(-1)
|
|
emptyRangesCount := int32(0)
|
|
bits := make([]byte, int32(math.Ceil(float64(totalRangesCount)/float64(8))))
|
|
bmap := bitmap.NewBitMapFromByteSliceCopy(bits)
|
|
indexChan, errChan := LocateNonEmptyRangeIndices(diskStream, uploadableRanges)
|
|
L:
|
|
for {
|
|
select {
|
|
case index, ok := <-indexChan:
|
|
if !ok {
|
|
break L
|
|
}
|
|
bmap.Set(index, true)
|
|
emptyRangesCount += index - lastIndex - 1
|
|
lastIndex = index
|
|
fmt.Printf("\r Empty ranges : %d/%d", emptyRangesCount, totalRangesCount)
|
|
case err := <-errChan:
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Remove empty ranges from the uploadable ranges slice.
|
|
i := int32(0)
|
|
for j := 0; j < totalRangesCount; j++ {
|
|
if set, _ := bmap.Get(int32(j)); set {
|
|
uploadableRanges[i] = uploadableRanges[j]
|
|
i++
|
|
}
|
|
}
|
|
uploadableRanges = uploadableRanges[:i]
|
|
return uploadableRanges, nil
|
|
}
|
|
|
|
func LocateNonEmptyRangeIndices(stream *diskstream.DiskStream, ranges []*common.IndexRange) (<-chan int32, <-chan error) {
|
|
indexChan := make(chan int32, 0)
|
|
errorChan := make(chan error, 0)
|
|
go func() {
|
|
count := int64(-1)
|
|
var buf []byte
|
|
for index, r := range ranges {
|
|
if count != r.Length() {
|
|
count = r.Length()
|
|
buf = make([]byte, count)
|
|
}
|
|
|
|
_, err := stream.Seek(r.Start, 0)
|
|
if err != nil {
|
|
errorChan <- err
|
|
return
|
|
}
|
|
_, err = io.ReadFull(stream, buf)
|
|
if err != nil {
|
|
errorChan <- err
|
|
return
|
|
}
|
|
if !isAllZero(buf) {
|
|
indexChan <- int32(index)
|
|
}
|
|
}
|
|
close(indexChan)
|
|
}()
|
|
return indexChan, errorChan
|
|
}
|
|
|
|
// isAllZero returns true if the given byte slice contain all zeros
|
|
//
|
|
func isAllZero(buf []byte) bool {
|
|
l := len(buf)
|
|
j := 0
|
|
for ; j < l; j++ {
|
|
if buf[j] != byte(0) {
|
|
break
|
|
}
|
|
}
|
|
return j == l
|
|
}
|