2018年12月16日日曜日

QueryChangedDiskAreas と VixDiskLib_Read/Write を組み合わせて増分バックアップをしてみた

概要

CBT を有効にした VM に対して QueryChangedDiskAreas を実行することでディスクの変更箇所を確認することができます (参考)
今回は govmomi で変更箇所を取得し VDDK API の VixDiskLib_ReadVixDiskLib_Write を使ってディスクの変更箇所 (増分) だけをディスクに追記してみました

環境

  • CentOS 7
  • VCSA 6.5.0 9451637
  • VDDK API 6.7.1
  • golang 1.11.2

事前作業

全く同じ状態のサーバを 2 台用意しましょう
そして 1 台 (src) を変更してその変更をもう 1 台 (dest) に反映する処理をします
同じサーバは VixDiskLib_QueryAllocatedBlocks を使ってコピーを作成しても OK ですしクローンでも OK です

src での作業

CBT を有効にする

src (vm-10) の VM に対して CBT を有効にしましょう
こちらの記事を参考に mob を使ってやるのが簡単だと思います

CBT を有効化したらスナップショットを作成し ChangeID を取得しておきます (52 54 eb 64 13 82 bc 71-41 42 a2 b3 ba f4 96 2e/102)
ここで取得した ChangeID は src の大元の ChangeID になります
この ChangeID とこの後新たに作成するスナップショットを使って QueryChangedDiskAreas を実行し増分情報を取得します

ChangeID を取得したらスナップショットは削除して OK です

増分を取得する

サーバは起動するだけでもまだ増分が出るので起動するだけでも良いですがそれだとつまらないので適当にファイルでも作成しましょう
一旦 src サーバを起動して以下のファイルを作成しましょう

  • date > src_file.txt

ファイルを作成したら VM を一旦停止しスナップショットを新たに作成しましょう (snapshot-20)
そしてそのスナップショットと先ほどメモしておいた ChangeID を使って増分情報を取得します

package main

import (
    "context"
    "flag"
    "fmt"
    "net/url"
    "os"

    "github.com/vmware/govmomi"
    "github.com/vmware/govmomi/find"
    "github.com/vmware/govmomi/vim25/methods"
    "github.com/vmware/govmomi/vim25/types"
)

var envURL = "https://192.168.100.20/sdk"
var user = "administrator@vsphere.local"
var pass = "xxxxxxxxxxxx"
var vmname = "src"
var diskNum int32 = 2000
var changeID = "52 54 eb 64 13 82 bc 71-41 42 a2 b3 ba f4 96 2e/102"
var urlDescription = fmt.Sprintf("ESX or vCenter URL [%s]", envURL)
var urlFlag = flag.String("url", envURL, urlDescription)

var envInsecure = true
var insecureDescription = fmt.Sprintf("Don't verify the server's certificate chain [%s]", envInsecure)
var insecureFlag = flag.Bool("insecure", envInsecure, insecureDescription)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    flag.Parse()
    u, err := url.Parse(*urlFlag)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    u.User = url.UserPassword(user, pass)
    c, err := govmomi.NewClient(ctx, u, *insecureFlag)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    f := find.NewFinder(c.Client, true)
    dc, err := f.DefaultDatacenter(ctx)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    f.SetDatacenter(dc)
    ss := &types.ManagedObjectReference{Type: "VirtualMachineSnapshot", Value: "snapshot-20"}
    // change disk areas
    query := new(types.QueryChangedDiskAreas)
    query.ChangeId = changeID
    query.DeviceKey = diskNum
    query.Snapshot = ss
    query.StartOffset = 0
    query.This.Type = "VirtualMachine"
    query.This.Value = "vm-10"
    res, err := methods.QueryChangedDiskAreas(ctx, c.RoundTripper, query)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    for _, area := range res.Returnval.ChangedArea {
        fmt.Printf("%d,%d\n", area.Start, area.Length)
    }
}

QueryChangedDiskAreas は vSphere API なので golang からコールします
しかし VDDK API は c++ から呼ぶので連携するために CSV ファイルを作成します

  • go build github.com/hawksnowlog/j
  • go install github.com/hawksnowlog/j
  • $GOPATH/bin/j > changed_areas.csv

この CSV 増分の offset と length が羅列されています

1048576,65536
135266304,131072
143654912,65536
155189248,65536
155713536,65536
511705088,1703936
...

dest での作業

dest VM は念の為停止しておきましょう

増分を書き込む

先程の CSV を使って VDDK API で増分のセクタ情報を書き込みます
まずコードは以下の通りです

#include <iostream>
#include <cstring>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
#include "vixDiskLib.h"
using std::cout;
using std::endl;
using std::vector;
using std::string;
using std::ifstream;
using std::istringstream;

#define VIXDISKLIB_VERSION_MAJOR 6
#define VIXDISKLIB_VERSION_MINOR 7

static struct {
  VixDiskLibConnection connection;
  char *libdir;
  char *cfgFile;
} params, wparams;

static void LogFunc(const char *fmt, va_list args) {
  printf("Log: ");
  vprintf(fmt, args);
}

static void WarnFunc(const char *fmt, va_list args) {
  printf("Warning: ");
  vprintf(fmt, args);
}

static void PanicFunc(const char *fmt, va_list args) {
  printf("Panic: ");
  vprintf(fmt, args);
  exit(10);
}

static vector<string> split(string& input, char delimiter) {
  istringstream stream(input);
  string field;
  vector<string> result;
  while (getline(stream, field, delimiter)) {
    result.push_back(field);
  }
  return result;
}

int main(int argc, char* argv[]) {
  VixDiskLibHandle _handle = NULL;
  VixDiskLibHandle _whandle = NULL;
  try {
    VixError err;

    err = VixDiskLib_InitEx(VIXDISKLIB_VERSION_MAJOR, VIXDISKLIB_VERSION_MINOR, &LogFunc, &WarnFunc, &PanicFunc, params.libdir, "init.cfg");
    printf("%lu\n", err);
    err = VixDiskLib_InitEx(VIXDISKLIB_VERSION_MAJOR, VIXDISKLIB_VERSION_MINOR, &LogFunc, &WarnFunc, &PanicFunc, wparams.libdir, "init.cfg");
    printf("%lu\n", err);

    VixDiskLibConnectParams cnxParams = {0};
    cnxParams.vmxSpec = {(char*)"moref=vm-10"};
    cnxParams.specType = VIXDISKLIB_SPEC_VMX;
    cnxParams.serverName = {(char*)"192.168.100.20"};
    cnxParams.credType = VIXDISKLIB_CRED_UID;
    cnxParams.creds.uid.userName = {(char*)"administrator@vsphere.local"};
    cnxParams.creds.uid.password = {(char*)"xxxxxxxxxxxx"};
    cnxParams.thumbPrint = {(char*)"96:09:d6:5b:e0:83:58:1b:ba:2b:cc:78:22:88:33:36:64:50:32:eb"};
    err = VixDiskLib_ConnectEx(&cnxParams, 1, NULL, NULL, &params.connection);
    printf("%lu\n", err);
    VixDiskLibConnectParams wcnxParams = {0};
    wcnxParams.vmxSpec = {(char*)"moref=vm-20"};
    wcnxParams.specType = VIXDISKLIB_SPEC_VMX;
    wcnxParams.serverName = {(char*)"192.168.100.20"};
    wcnxParams.credType = VIXDISKLIB_CRED_UID;
    wcnxParams.creds.uid.userName = {(char*)"administrator@vsphere.local"};
    wcnxParams.creds.uid.password = {(char*)"xxxxxxxxxxxx"};
    wcnxParams.thumbPrint = {(char*)"96:09:d6:5b:e0:83:58:1b:ba:2b:cc:78:22:88:33:36:64:50:32:eb"};
    err = VixDiskLib_ConnectEx(&wcnxParams, 1, NULL, NULL, &wparams.connection);
    printf("%lu\n", err);

    err = VixDiskLib_Open(params.connection, "[datastore2] src/src.vmdk", VIXDISKLIB_FLAG_OPEN_SINGLE_LINK, &_handle);
    printf("%lu\n", err);
    err = VixDiskLib_Open(wparams.connection, "[datastore2] dest/dest.vmdk", VIXDISKLIB_FLAG_OPEN_SINGLE_LINK, &_whandle);
    printf("%lu\n", err);

    ifstream ifs("changed_areas.csv");
    string line;
    while (getline(ifs, line)) {
      vector<string> strvec = split(line, ',');
      VixDiskLibSectorType offset = stoul(strvec.at(0)) / VIXDISKLIB_SECTOR_SIZE;
      VixDiskLibSectorType length = stoul(strvec.at(1)) / VIXDISKLIB_SECTOR_SIZE;
      printf("offset: %lu\n", offset);
      printf("length: %lu\n", length);
      VixDiskLibSectorType start = offset;
      VixDiskLibSectorType bufSize = 128;
      VixDiskLibSectorType count = length / bufSize;
      for (VixDiskLibSectorType j = 0; j < count; j++) {
        uint8 *buf = new uint8[bufSize * VIXDISKLIB_SECTOR_SIZE];
        // read
        err = VixDiskLib_Read(_handle, start + (j * bufSize), bufSize, buf);
        if (err != VIX_OK) {
          throw std::exception();
        }
        // write
        err = VixDiskLib_Write(_whandle, start + (j * bufSize), bufSize, buf);
        if (err != VIX_OK) {
          throw std::exception();
        }
        delete[] buf;
      }
    }
    VixDiskLib_Close(_whandle);
    VixDiskLib_Close(_handle);
    VixDiskLib_Disconnect(params.connection);
  } catch (...) {
    VixDiskLib_Close(_whandle);
    VixDiskLib_Close(_handle);
    VixDiskLib_Disconnect(params.connection);
  }
  return 0;
}

解説

CSV の読み込みは ifstream を使います
必要になるヘッダファイルがあるので冒頭で追加しています
読み込んだ CSV の行数分ループさせます

実は CSV の offset と length の値はそのままでは使えません
VDDK API の世界で使える単位にしなければいけないので VIXDISKLIB_SECTOR_SIZE (512) で割る必要があります
(どこにもこの情報がなく初めはそのまま使っていたのですがうまく行かずいろいろ試してたどり着きました、、)

VixDiskLibSectorType offset = stoul(strvec.at(0)) / VIXDISKLIB_SECTOR_SIZE;
VixDiskLibSectorType length = stoul(strvec.at(1)) / VIXDISKLIB_SECTOR_SIZE;

あとは基本的には[フルバックアップ時[()に行ったように書き込めば OK です
bufSize を上げれば Read/Write の速度が上昇します
増分はそこまで多くないのであれば 128 で十分だと思います

動作確認

実際に VDDK API 側のコードを動かして dest VM を起動すると src VM 側に作成した src_file.txt があるのが確認できると思います

この方法だと確かに増分のファイルだけ送ることができるようになります
ですが src 側の kernel ログなども書き込んでしまいます
dest があくまでも src 側のバックアップサーバなのであれば問題ないですが、そうでない場合は src と dest のログが混在するケースが発生するので注意が必要です

最後に

QueryChangedDiskAreas を使って増分バックアップを実装してみました

後で調べてみたのですが QueryChangedDiskAreas にバグのような挙動があるらしく vmdk のサイズを後から拡大するとおかしな offset と length を返すようです (参考1, 参考2)
今回使用した vSphere 6.5 環境でその現象になるかまでは試していません
最新版は 6.7 なので最新版では解決している可能性もあります
そもそもこのバグらしく挙動をどうやって再現するのかから考えないとダメそうですが、、

0 件のコメント:

コメントを投稿