2018年12月19日水曜日

AMP 超入門

概要

AMP は Google が推奨するモバイル用の Web ページの描画を高速化するための仕組みです
簡単に言えば使える JavaScript やタグを制限しキャッシュすることでページの高速化を実現しています
今回は AMP 対応したページの作成を公式のチュートリアルを元に試してみました

環境

  • Chrome 70.0.3538.110

AMP 対応のサンプルページ

まずは AMP 対応したページを実際に作って動かしてみましょう

  • mkdir -p /path/to/workspace
  • cd /path/to/workspace
  • touch index.html
<!doctype html>
<html amp lang="en">
  <head>
    <meta charset="utf-8">
    <script async src="https://cdn.ampproject.org/v0.js"></script>
    <title>Hello, AMPs</title>
    <link rel="canonical" href="/index.html">
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
    <script type="application/ld+json">
      {
        "@context" : "http://schema.org",
        "@type" : "NewsArticle",
        "headline" : "AMP のテストページです",
        "image" : {
          "@type" : "ImageObject",
          "url" : "/welcome.jpg",
          "height" : 400,
          "width" : 400
        },
        "publisher" : {
          "@type" : "Organization",
          "name" : "@hawksnowlog",
          "logo" : {
            "@type" : "ImageObject",
            "url" : "/welcome.jpg",
            "height" : 400,
            "width" : 400
          }
        },
        "author" : {
          "@type" : "Person",
          "name" : "hawksnowlog"
        },
        "datePublished" : "2018-12-17T00:00:00Z"
      }
    </script>
    <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
    <style amp-custom>
      /* any custom style goes here */
      body {
        background-color: white;
      }
      amp-img {
        background-color: gray;
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <h1>Welcome to the mobile web</h1>
    <amp-img src="/welcome.jpg" alt="Welcome" height="400" width="400"></amp-img>
  </body>
</html>

welcome.jpg は適当にダウンロードして index.html と同じパスに配置します

  • wget 'https://pbs.twimg.com/profile_images/712848447569661952/ayfI9-77_400x400.jpg' -O welcome.jpg

動かす

  • cd /path/to/workspace
  • python -m SimpleHTTPServer

で localhost:8080/index.html にアクセスするとページが表示されます

本当に AMP 対応しているか確認する

ページを見るだけではただの Web ページです
このページが AMP 対応しているかどうか確認するには

  1. デベロッパーツールのコンソールを使う
  2. amp-validator という Chrome 拡張を使う

の 2 通りがあります
1 の場合コンソールを開いた上で http://localhost:8000/index.html#development=1 という URL にアクセスします
するとコンソールに以下のように表示されると思います
try_amp1.png

ここにエラーが表示される場合は AMP 対応できていません
例えば使えないタグを使っていたり必要なメタタグなどが不足している場合などです

2 の場合はツールバーにアイコンが表示されます
ページにアクセスして緑のアイコンが表示されれば AMP 対応が完了しているページになります

解説

では動かせたところで AMP 対応に必要なそれぞれのタグのについて紹介します

html タグ

まず上部にある HTML タグに AMP 対応であることの記載が必要です
amp ではなく雷マークでもいいようです

<html amp lang="en">

JavaScript

次に amp の JavaScript ライブラリを参照します
これも必須です

<script async src="https://cdn.ampproject.org/v0.js"></script>

AMP は基本的に上記以外の JavaScript は使えません
動的なコンテンツを表示したい場合それ専用の AMP タグがあるのでそれで代用します
将来的には好きな JavaScript が使えるようになるという話も聞きますが現状では厳しいようです (Twitter などを埋め込む場合はそれ専用のタグが用意されているようです)

rel=”canonical” の設定

簡単に言えば AMP 対応していない同一ページへのリンクを設定します
AMP 対応しているページだけであればその自身へのページのリンクを設定します

<link rel="canonical" href="/index.html">

AMP 対応するとよく言われるのが AMP 対応していないページ (モバイル用ではなく通常の Web 用のページ) を残すというケースです
その場合にはこの rel="canonical" を使って参照して上げることで Google に別のページがあることも教えてあげることで、そちらのページもインデックスさせることができます
またモバイル用のページが見づらい人がいる場合には Web 版に誘導することもできるようになります

viewport

必須のタグと属性です
基本はコピペで OK です

<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">

viewport はレスポンシブ対応しているページでよく見かける属性です

JSON-LD

JSON-LD はページの構造やメタ情報を定義することができる機能です
AMP 用の JSON-LD スキームが決められており定義することが推奨されています
JSON-LD を定義することで Google の検索結果 (特にニュース記事など) でよく見る「カルーセル」に該当ページを表示されることができるようになります

<script type="application/ld+json">
  {
    "@context" : "http://schema.org",
    "@type" : "NewsArticle",
    "headline" : "AMP のテストページです",
    "image" : {
      "@type" : "ImageObject",
      "url" : "/welcome.jpg",
      "height" : 400,
      "width" : 400
    },
    "publisher" : {
      "@type" : "Organization",
      "name" : "@hawksnowlog",
      "logo" : {
        "@type" : "ImageObject",
        "url" : "/welcome.jpg",
        "height" : 400,
        "width" : 400
      }
    },
    "author" : {
      "@type" : "Person",
      "name" : "hawksnowlog"
    },
    "datePublished" : "2018-12-17T00:00:00Z"
  }
</script>

上記は @type NewsArticle の定義になります
他にもタイプの定義があるので公開するページのタイプに合わせて設定してあげると良いと思います (参考)

amp-boilerplate

AMP Boilerplate Code と呼ばれています
これも必須の要素です
viewport と同じで基本はコピペで OK (なはず) です

<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>

余分なスペースなどは削除しておく必要があるようです

amp-custom

カスタム用のスタイルシートをここで定義します
むしろここ以外でスタイルシートを定義することはできません

<style amp-custom>
  /* any custom style goes here */
  body {
    background-color: white;
  }
  amp-img {
    background-color: gray;
    border: 1px solid black;
  }
</style>

amp-img

画像を表示する場合には img タグではなく amp-img タグを使います
amp-img のように AMP 対応したページでは使えるタグが限られています
既存の HTML タグにも使えるものがありますが基本的には限られたタグしか使えません

AMP で使える基本コンポーネントは以下の通りです
https://www.ampproject.org/ja/docs/reference/components

逆に使えないタグや属性は以下の通りです
https://www.ampproject.org/ja/docs/fundamentals/spec

一旦作成してみて validator やコンソールで確認してみて、エラーが出なくなるまでタグを置き換えたり削除したりするしかないかなと思います

実際に AMP 対応するには

ケースとしては 2 つが考えられると思います

  1. 既存のページを AMP 対応する
  2. 新規で AMP 用のページを作成する

1 は動的なコンテンツに依存していれば依存しているほど大変だと思います
また大規模なページになればなるほど大変なイメージもあります
やり方はいろいろありますがやはり前に紹介した通り validator を使って 1 つ 1 つエラーを解決していく感じだと思います

2 のほうが簡単だとは思います
が、2 を採用すると同一ページの管理が 2 つになってしまうため管理コストがあがります
うまく吸収してくれるようなツールもありそうですが単純に考えれば 2 倍になります
rel="canonical" 属性もあるようにそういったケース自体 AMP は想定しているので間違いではないです

個人的にはできれば 1 で進めて無理そうであれば 2 にするというのが良いかなーとは思います

当然ですがどちらにするにしてもモバイル向けのページデザインにする必要はあります
1 であればレスポンシブデザイン対応は必須になるので考えることが増えます
2 であればレスポンシブにする必要はなくなり完全にモバイル用の別ページを作れば良いだけになるので考えることは減ります

自分は実際に自分のページに適用したなどの経験はないのであくまでも推測にはなります

最後に

AMP 対応したサンプルページを作成して AMP に入門してみました
制限を設けることで最適化しているのでシンプルなページほど AMP に向いているなと思いました
AMP 用の JSON-LD の @type を見てもわかるようにニュース記事やブログ記事、レビュー記事など静的コンテンツを含んでいるページに向いているのだと思います

また JSON-LD を使うことで Google 検索のカルーセルに該当記事を表示させることもできるので SEO 的な効果もあるのかもしれません

同じではないですが PWA やレスポンシブデザインも意識したほうが良いかもしれません
最近は PWA + AMP (PWAMP (ぷわんぷ)) という取り組みをしているページもあるようです
PWA 自体は Service Worker を使った仕組みになるので仕組み自体は全然違いますがシナジーはあるので興味があれば調べてみると良いかなと思います

参考サイト

2018年12月18日火曜日

VDDK API 学習コンテンツ

これまで VDDK API 絡みでいろいろと記事を投稿してきました
一通り基本から応用まで学べるコンテンツになったので記事をまとめておきます

基本概念

VDDK API を使ったバックアップ処理を実装する際の基本概念や簡単な仕組みを紹介しています
図も載せているので導入としては理解しやすいと思います

VDDK のインストールおよびサンプルのビルドと実行方法を紹介しています

VDDK API 入門

VDDK API を C++ から操作する基本的な方法を紹介しています
Init -> Connect -> Open までの基本的な操作を学べます

vmdk から実際にセクタ情報を取得/書き込みするサンプルです
とりあえず Read/Write の動かし方を学ぶことができます

セクタの書き込み、読み込みとは別の API も試しました
PrepareForAccess と EndAccess は対象の VM の vMotion を禁止することができます

VDDK API 側のログを出力する方法を紹介しています
Init する際に設定ファイルを指定する方法も紹介しています

VixDiskLib_QueryAllocatedBlocks は 6.7 を使った際にフルバックアップを実装するために使います
応用では他の API と組み合わせて実際にフルバックアップを実装しています

CBT 入門

コーディングの前に ManagedObjectBrowser を使ってディスクの増分領域を取得する方法を紹介しています

mob を使って実行した操作を govmomi を使ってコードに落とし込んでいます
govmomi を使った初期化の部分から学べます

応用

フルバックアップを実装してみました
対象の VM と全く同じ VM を作ることができます
VDDK API の基本操作と vSphere API の QueryChangedDiskAreas の使い方および CBT の使い方を理解している必要があります

こちらは増分バックアップのサンプル実装になります
vSphere API と組み合わせた実装になっているのでより難しくなっています

2018年12月17日月曜日

govmomi で Task の Wait を行う方法

概要

govmomi の methods で API を呼び出した場合、レスポンスが object.Task でないためいきなり Wait が呼べません
レスポンスから object.Task を作成して Wait する必要があります

環境

  • CentOS 7.5.1804
  • golang 1.11.2
  • govmomi 6712f991d8852a25ae4304a720463301c1ac4c64

サンプルコード

今回はスナップショットの作成 (methods.CreateSnapshot_Task) を Wait する方法を紹介します

  • mkdir go/src/github.com/hawksnowlog/h
  • vim go/src/github.com/hawksnowlog/h/main.go
package main

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

    "github.com/vmware/govmomi"
    "github.com/vmware/govmomi/find"
    "github.com/vmware/govmomi/object"
    "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 = "xxxxxxxxxx"
var vmname = "vm01"
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)
    // vm list
    vss, err := f.VirtualMachineList(ctx, "*")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    ref := types.ManagedObjectReference{}
    for _, vm := range vss {
        name := vm.Name()
        if name == vmname {
            ref = vm.Common.Reference()
            break
        }
    }
    // snapshot from vm
    fmt.Println(ref.Type)
    fmt.Println(ref.Value)
    ssreq := new(types.CreateSnapshot_Task)
    ssreq.Description = "Snapshot created by govmomi"
    ssreq.Name = vmname + "ss"
    ssreq.Memory = false
    ssreq.This.Type = ref.Type
    ssreq.This.Value = ref.Value
    res, err := methods.CreateSnapshot_Task(ctx, c.RoundTripper, ssreq)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    task := object.NewTask(c.Client, res.Returnval)
    err = task.Wait(ctx)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("created snapshot")
}
  • go build github.com/hawksnowlog/h
  • go install github.com/hawksnowlog/h
  • $GOPATH/bin/h

解説

res, err := methods.CreateSnapshot_Task(ctx, c.RoundTripper, ssreq) で取得できる res は types.CreateSnapshot_TaskResponse になっています
これだと .Wait(ctx) 関数を呼び出せません

なので res を元に task := object.NewTask(c.Client, res.Returnval)object.Task を作成してから .Wait(ctx) を呼び出してあげます

ssreq.This.Type = ref.Typessreq.This.Value = ref.Value はただの文字列になっています
例えばそれぞれ「VirtualMachine」「vm-20」という文字列です
今回のサンプルでは VM を取得してそこから moref を参照していますが決め打ちで決まっている場合は VM を検索するロジックは削除できます

最後に

govmomi で Task を Wait する方法を紹介しました
govmomi は基本は非同期なので、この Wait のやり方を覚えておかないとまだリソースが作成されていない状態で次に行ってしまったりするので注意が必要です
簡単な方法なので覚えておくと良いと思います

参考サイト

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 なので最新版では解決している可能性もあります
そもそもこのバグらしく挙動をどうやって再現するのかから考えないとダメそうですが、、

2018年12月15日土曜日

VixDiskLib_QueryAllocatedBlocks と VixDiskLib_Read/Write を組み合わせてサーバのコピー的なことをしてみた

概要

過去にこんな記事を紹介しました
初回のフルバックアップ時には VixDiskLib_QueryAllocatedBlocks を使って使用済みのセクタ情報を取得します
そして Read で実際にディスクの情報を取得して Write で書き込みます
フルバックアップすることでそっくりそのまま同じ vmdk を作ることができるので実質サーバコピー的なことができます
今回は各 API を使ったサンプルコードを紹介します

環境

  • CentOS 7
  • VCSA 6.5.0 9451637
  • VDDK API 6.7.1

サンプルコード

まずはサンプルコード全体です
後で詳しく説明します

#include <iostream>
#include <cstring>
#include "vixDiskLib.h"
using std::cout;
using std::endl;

#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);
}

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*)"xxxxxxxxxx"};
    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*)"xxxxxxxxxx"};
    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] src3/src3.vmdk", VIXDISKLIB_FLAG_OPEN_SINGLE_LINK, &_handle);
    printf("%lu\n", err);
    err = VixDiskLib_Open(wparams.connection, "[datastore2] dest3/dest3.vmdk", VIXDISKLIB_FLAG_OPEN_SINGLE_LINK, &_whandle);
    printf("%lu\n", err);

    VixDiskLibBlockList *blocks = new VixDiskLibBlockList;
    VixDiskLibSectorType startSector = 0;
    VixDiskLibSectorType sectorCount = 62914560;
    VixDiskLibSectorType chunk = VIXDISKLIB_SECTOR_SIZE;
    err = VixDiskLib_QueryAllocatedBlocks(_handle, startSector, sectorCount, chunk, &blocks);
    printf("%lu\n", err);
    for (int i = 0; i < blocks->numBlocks; i++) {
      printf("i: %d\n", i);
      VixDiskLibBlock vb = blocks->blocks[i];
      VixDiskLibSectorType offset = vb.offset;
      VixDiskLibSectorType length = vb.length;
      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;
      }
    }
    delete[] blocks;

    VixDiskLib_Close(_whandle);
    VixDiskLib_Close(_handle);
    VixDiskLib_Disconnect(params.connection);
  } catch (...) {
    VixDiskLib_Close(_whandle);
    VixDiskLib_Close(_handle);
    VixDiskLib_Disconnect(params.connection);
  }
  return 0;
}

説明

概要図を作成しました
vddk_server_copy3.jpeg

コピー元のサーバ (src (vm-10)) は 30GB のハードディスクが接続されています
ディスクのタイプはシンプロビジョニングで使用量は 6,900,736 KB です
vddk_server_copy2.png
src に対して VixDiskLib_QueryAllocatedBlocks を実行します
src サーバのディスクに対する操作は _handle を使います
またコピー先のサーバ (dest (vm-20)) に対しては別のハンドラ _whandle を使います
なので初期化の段階で VixDiskLib_ConnectExVixDiskLib_Open をそれぞれのハンドラ分コールしています
sectorCount = 62914560 は 30GB / 512 (VIXDISKLIB_SECTOR_SIZE) になります
この結果 98 ブロックの allocated な領域を取得することができます

次にこのブロック分ループさせます
各ブロックは offset と length を持ちます
この 2 つの値を元に実際にディスクからセクタ情報を取得します
Read/Write するスピードは bufSize 変数で調整できます
今回は 128 を指定しています
1 回の Read/Write で bufSize 分のセクタ情報を操作するので次の Read/Write のスタート位置 (offset) は start + (j * bufSize) 分ずらして行う必要があります
また Read/Write するループ回数も bufSize の大きさによって変わるので length / bufSize で割っておきます
bufSize は単純に係数みたいな感じで掛け合わせているだけなのでなくても動きます
ただその場合は bufSize=1 として書き込むためループ回数も length 分ループするので時間がものすごくかかります
ちなみに今回の 7GB 弱のシンプロビジョニングのディスクを bufSize=1 で行うと 1 日以上普通にかかります
なので bufSize は必ず設定したほうが良いです (ただし大きくしすぎると OOM の可能性も上がるのでそこは自身の環境に合わせて調整してください)

すべての処理が終了したらハンドラの Close と vCenter からの Disconnect を必ず行いましょう

実行

コピー先に関しては新規 VM の作成からただ VM を作成しているだけです
ISO などを使って OS をインストールしないでください
vmdk のサイズが 0 bytes の状態の空 VM の状態で作成してください
今回はコピー元に関しては今回は停止した状態で行っています
(起動した状態でもできると思いますが起動中は常にディスクの状態が変わっているので完全に同じディスクの状態のコピーにはならないと思います)

だいたいですが bufSize=128 で 7GB ほどのディスクで 30 分程度で終了しました
bufSize を調整すればもう少し早く終了させることもできると思います
また終了後に dest 側のディスクのサイズを見たのですが src と少し違っていました
vddk_server_copy1.png

すべてのセクタ情報をこの記事で紹介した DumpBytes という関数を使って確認すれば差分がわかるかもしれませんがさすがにそこまではできませんでした

コピー完了後に dest 側のマシンを起動してみたところ src 側のマシンと全く同じ内容で起動しました

その他

Read/Write 時に大量のセクタ情報を読み込もうとすると NBD_ERR_INSUFFICIENT_RESOURCES というエラーが出ることがありました
ESXi の nfc サーバ (?) のメモリ量を増やすみたいな対策方法が紹介されている記事があったのですが試しても解決せず諦めてセクタ情報を小さく書き込むことで対応しました

最後に

VixDiskLib_QueryAllocatedBlocks と Read/Write の関数を使ってサーバコピーを自作してみました
Read/Write のセクタに書き込む処理は並列化できるような気もします
が、ストレージの負荷も上がると思うのでその辺りも要調整かなと思います

今回の処理はいわゆるフルバックアップ的な操作になります
次回は QueryChangedDiskAreas を使って増分バックアップを実現してみたいと思います
QueryChangedDiskAreas の場合 vSphere API になるので実装も少し面倒になると予想しています