銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

.NET アプリから BigQuery に Streaming Insert する方法

このブログは Google Cloud Platform Advent Calendar 2014 の7日目のエントリです。

Google Cloud Platform Advent Calendar 2014 - Qiita

さて、以前テーブルをAPIから生成する方法は紹介したのですが、データをStreaming Insertする方法について書いていなかったので、そこを説明したいと思います。また、すでに本番運用して、WebサーバーからデータをBigQueryに送信しているのですが、どのような点に注意しているかも紹介したいと思います。

C# から Streaming Insertする方法

まずはサンプルコードをのせます。

StreamingInsert.cs

CreateTable メソッドは前回の記事を参考にしてください。LoadRowsメソッドは実際に送信するデータですが、でたらめな値を入れるのであればこんな感じになります。特徴としては、テーブル定義に対応した列名とその値を Dictionary<string,object> で指定することです。

private IList<TableDataInsertAllRequest.RowsData> LoadRows()
{
    return Enumerable.Range(0, 450)
        .Select(_ => new TableDataInsertAllRequest.RowsData
        {
            InsertId = Guid.NewGuid().ToString(), //リクエストの一意なIdなのでGuidで代用
            Json = new Dictionary<string, object>
            {
                { "date", DateTime.Now },
                { "source", "hoge" }
            }
        }).ToList();
}

長くなっていますが、基本的には

var req = new TableDataInsertAllRequest
{
    Rows = LoadRows()
};

var response = await bigquery.Tabledata.InsertAll(req,
                    "testprj", "testdataset", "testtbl").ExecuteAsync();

でInsertできます。面倒なことをしているのは、

  • Insert先のテーブルがないときにテーブルを生成する処理
  • 失敗時にリトライする処理

を入れているからです。

テーブルの自動生成処理を入れるかどうかはInsertの要件次第でしょう。たとえば期間ごとにテーブルを分けて格納する場合*1に、新しいテーブルを作るたタイミングで別のツールから生成するのではなく、このコードの中で生成することを想定しています*2

また、後者の失敗時のリトライですが、これは割と必須の処理です。BigQuery のStreaming InsertはバックエンドのエラーでInsertが失敗することがあり、その場合にはリトライすることが推奨されています。リトライも単純に一定時間おきに繰り返すのではなく、指数関数的な伸び+ランダムに追加した時間だけ待機する Google.Apis.Util.ExponentialBackOff を利用しています。なお、所定の最高試行回数(ExponentialBackOff の初期値だと10)を超えると、Insertをあきらめデータは破棄することになります。

本番運用で考慮していること

というわけで上のような Streaming Insert なコードを本番環境で動かして BigQuery にデータを送信しています*3

さて、そもそもどうやってこのコードを動かしているかですが、 上のコードを SLABのSink として実装し、 Out-of-Process Service として動かしています。送信するイベントを送信するのはIIS上のASP.NETアプリになります。

テーブルの自動生成と失敗時のリトライは上のコードのように考慮していますが、ほかに次のようなことを考慮して動かしています。

  • Insert先のDataSet名、Table名、テーブル定義などは EventSource のメタ情報として定義しています。 DataSet名 は EventKeyword、Table名は EventTask、テーブル定義は Paylowd 経由で取得、という感じです。
  • 1回のリクエストあたり500行というハードリミットがあるので、450行ずつバッファリングして送信
  • SLABのSinkでは BufferedEventPublisher<EventEntry> でイベントをバッファリングしてまとめて送信できるが、バッファが溜まった時に発火するeventPublisher*4の中でリトライを繰り返すと、後続のログもたまり続けるため、1回失敗した場合は、別の BufferedEventPublisher にためていくようにしている
  • Insertしている Out-of-Process Service の健全性は、Windows EventLogおよびパフォーマンスカウンタを通して確認している。*5

といった点です。

というわけで、意外な組み合わせかもしれませんが、.NET アプリからもBigQueryにデータを送信して活用しています、というお話でした。

*1:test_20141201, test_20141202 ... みたいなテーブル名を想定

*2:その場合、テーブル名は日時に応じて動的に判断することになり、テーブル構造を指定する必要もある

*3:動いているコードそのものをお見せしていないのは、テーブル名やテーブル定義の生成部分に弊社固有のロジックが入ってコードの見通しが悪いためです...

*4:CreateAndStartメソッドの第2引数で指定

*5:実際はそれらを AWS CloudWatch で集約してアラートをしかけている

PowerShell Cmdlet の中でRxを使いたい

PowerShell Advent Calendarの4日目の記事です。

PowerShell Advent Calendar 2014 : ATND

去年もPowerShell Advent Calendarなのに C# な話だったんですが、今年も性懲りもなくC# コード満載のCmdletネタになります。

PowerShell を C# から実行する - 銀の光と碧い空

最近、マルチプラットフォーム対応のポータブルクラスライブラリが話題ですが、PowerShell Cmdlet として実装するC# プロジェクトでも中身のロジックをポータブルにしておくといろいろ便利です。といっても、AndroidとかiOSに対応するところまでいかなくとも、ロジックのクラスをクラスライブラリとして分離して Cmdlet のクラスから参照してPowerShell から呼び出すほかに、たとえばそのクラスライブラリをWPFツールから参照してGUIから実行する、といったことができます。

さて、そんな状況で、わりと長い(数分くらい)の処理を実行する Cmdlet とその中身のロジックを作ることになりました。また、この処理はWPFのツールからも実行したいので、上に書いたようにクラスライブラリとして分離させることにしました。当然数分かかるため、進捗状況を知りたいので、処理の途中経過でメッセージで表示することにしました。単純に処理の結果だけ取得するなら、Task な返り値でいいのですが、進捗状況を知りたいとなると。。。と悩んだ結果、Rxを使って IObservable で進捗状況を返すことにしました(Tは独自定義の進捗状況を表すクラス)。で、IObservable なAPIをCmdletクラスで呼びだそうとして、問題はここで起きたのです。

問題を再現するためのコードを紹介します。まず、IObservable なクラスライブラリのメソッドはこんなものです*1

で、これを Cmdlet で呼ぶために、まずこんな書き方をしてみました。

Select メソッド内で飛んできた進捗を Write-Object しつつ、処理の完了を待つために、 FirstAsync したものを Wait しています。 これを実行するとこんなことになります。

Invoke-BadHelloRx : WriteObject メソッドと WriteError メソッドは、BeginProcessing メソッド、ProcessRecord メソッド、
および EndProcessing メソッドの上書きの外側から呼び出すことはできず、同じスレッド内からだけ呼び出すことができます。
コマンドレットで呼び出しが正しく作成されていることを確認するか、または Microsoft カスタマー サポート サービスにお問い合わせください。
発生場所 行:3 文字:1
+ Invoke-BadHelloRx
+ ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Invoke-BadHelloRx]、PSInvalidOperationException
    + FullyQualifiedErrorId : InvalidOperation,ObservableCmdletSample.BadRxCmdlet

ぐぬぬ... Write-Object は呼びだされたスレッドの中から実行しないといけないのですね...((ちなみに、async void にして、Waitメソッドの代わりにawaitすると、PowerShellが強制終了し、ほとんど情報のないイベントログがイベントID:1000 として記録され、さらに訳が分からなくなります)) では、ということで、SynchronizationContext.Current を取得して、 ObserveOn してみましょう。

実行すると...

Invoke-BadHelloRx : 値を Null にすることはできません。
パラメーター名:context
発生場所 行:3 文字:1
+ Invoke-BadHelloRx
+ ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Invoke-BadHelloRx], ArgumentNullException
    + FullyQualifiedErrorId : System.ArgumentNullException,ObservableCmdletSample.BadRxCmdlet

おお、ぬるぽ!!!111 Cmdletの中は SynchronizationContext.Current が null でした。

対処法を検索したところ*2*3、このCmdletが呼ばれる環境(コマンドプロンプトの中なのか、WPFの中なのか...など)がわかれば、それに対応した SynchronizationContext を作ればいいようなんですが、そもそもいろんな環境で再利用したいので、そういうこともしたくない...

と悩んでいたところ、CTO に教えてもらったのがこの方法です。

BlockingCollection<Action> を使って、RxのSubscribeの中でQueueに処理を追加し、後から逐次実行していくイメージです。

実行してみると...

f:id:tanaka733:20141201115100p:plain

無事に実行できました。この書き方がベストかどうかはわからないのですが、PowerShell Cmdlet から RxなAPIを叩くときに困ったときの助けになれば幸いです。

*1:この程度なら、Subject使わずに Observable クラスのメソッドチェーンで書けそうですが、実際は様々な非同期処理をawaitしている処理になっています。

*2:c# - Powershell custom cmdlet Calling WriteVerbose outside of implementation of standard methods - Stack Overflow

*3:powershell-for-developers/WPFJob.cs at 437aab5821e8de698168f245413d3ad725b40f7f · dfinke/powershell-for-developers · GitHub

CloudWach (Windows版)に.NET アプリから独自のイベントログとパフォーマンスカウンタを送信する方法

このエントリは、AWS Advent Calendar 3日目の記事です

AWS Advent Calendar 2014 - Qiita

先日出た CloudWatch にWindows Serverからイベントログやパフォーマンスカウンタの値を送信できる機能ですが、これを使うことによって、アプリケーションから独自のイベントログやパフォーマンスカウンタを定義し送信することができます。ただ、若干面倒なところがあるのでブログにまとめてみました。

事前準備

Ec2Configは念の為最新の2.2.11 を使っています*1。Windows Serverは最新の Windows Server 2012 R2。アプリケーションはC# で書かれた.NET アプリを想定していますが、対応するWindowsのAPIを利用すれば他言語/Frameworkでも問題ないと思います。

イベントログを送る

独自のイベントログを定義する(理由編)

AWSのドキュメントの手順のところにあるのですが、Ec2Configが送信するイベントログは、イベントビューアー上で確認できる状態になっていないといけません。一方、Windows においてイベントビューアーに表示されるようなイベントログを定義するのは割と面倒です。というのも、最近のWindowsでは、イベントログとは別のETW(Event Tracing for Windows)という仕組みがが使われるようになっています。このETWは、.NET標準のAPIが用意されているため、C#/VBといった managed なコードから利用することが簡単になっている反面、標準の機能ではイベントビューアーに表示されません*2

ではどうするかというと、ETWについているChannel Supportという機能を使います。ChannelをサポートしたETWのログはイベントビューアーに表示されます。が、このChannelの機能、.NET標準のAPIではサポートされていません。その代わり、BCLチームが作成した Microsoft.Diagnostics.Tracing.EventSource ライブラリを使うと、事前準備が必要ですが、ManagedコードからChannelサポートしたETWのログを書きこめるようになります。

独自のイベントログを定義する(手順編)

1: ログを送信したいアプリ(コンソールなりWPFなりASP.NETなり)に、Microsoftほげもげライブラリの参照を追加します。Nugetからどうぞ。ダウンロードすると、docx なファイルも一緒に追加されるのですが、使い方はこれが一番詳しいです。

Install-Package Microsoft.Diagnostics.Tracing.EventSource -Version 1.0.26

2: ログを送信するクラス・メソッドを定義します。もともと構造化ログをサポートする意味合いもあったためログのメソッドに意味合いをもたせるのですが、単純なサンプルとしてはこうなります。

using Microsoft.Diagnostics.Tracing;

namespace MyEventSource
{
    [EventSource(Name = "Sample-EventLog")]
    public sealed class MyEventSource : EventSource
    {
        public static MyEventSource Log = new MyEventSource();

        [Event(1, Message = "{0}", Channel = EventChannel.Admin)]
        public void Debug(string message)
        {
            WriteEvent(1, message);
        }
    }
}

3: ビルドします。すると *.etwManifest.dll とか *.etwManifest.man とかいう見慣れないファイルが生成されているはずです。これが Microsoft.Diagnostics.Tracing.EventSource ライブラリの便利なところで、ChannelをサポートしたETWはOSに登録しないといけないのですが、そのために必要なファイルを生成してくれます。

f:id:tanaka733:20141201232051p:plain

4: OSに登録します。実際にログを送信するEC2インスタンス上に3で生成された2つのファイルを持ってきて、以下のコマンドを管理者権限で実行します。なお、rf と mfの後にしているdllはフルパスが必要です。

> wevtutil im "D:\Documents\visual studio 2013\Projects\MyEventSource\MyEventSource\bin\Debug\MyEventSource.Sample-EventLog.etwManifest.man"  rf:"D:\Documents\visual studio 2013\Projects\MyEventSource\MyEventSource\bin\Debug\MyEventSource.Sample-EventLog.etwManifest.dll" /mf:"D:\Documents\visual studio 2013\Projects\MyEventSource\MyEventSource\bin\Debug\MyEventSource.Sample-EventLog.etwManifest.dll"

5: 念のためイベントビューアーで確認してみましょう

f:id:tanaka733:20141201232401p:plain

6: Ec2Configの設定ファイルに追加してみます。PrametersのLogName は下記のように EventSourceの名前/Channelの名前 で指定します。

{
    "Id": "SampleEventLog",
    "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
    "Parameters": {
        "LogName": "Sample-EventLog/Admin",
        "Levels": "7"
    }
},

7: Ec2Config サービスを再起動後、ログのAPIを呼びだすメソッドを実行すると、CloudWatchLogs に送信されているはずです。

パフォーマンスカウンタの値を送る

1: カスタムパフォーマンスカウンタを定義する。

MSDNに説明が載っています。ただ、複数サーバーに展開するときなどを考えるとC#で書いてビルドしたexeを実行する、というのは若干面倒なこともあるので、PowerShell版のサンプルコードを載せておきます*3

# Perfmon Name and Help
$categoryName = "MyAppPerformance"
$categoryHelp = "MyApp"
$categoryType = [System.Diagnostics.PerformanceCounterCategoryType]::MultiInstance

# Fix it by Index
$counterName = "Count", "Time"
$counterType = "NumberOfItems32", "NumberOfItems32"
$counterHelp = "Count", "Time"

# Delete Existing Category
$categoryCount = ([System.Diagnostics.PerformanceCounterCategory]::GetCategories() | where CategoryName -eq $categoryName).Count
If ($categoryCount -ne 0){ [System.Diagnostics.PerformanceCounterCategory]::Delete($categoryName) }

# Create Collection
$objCCDC = New-Object System.Diagnostics.CounterCreationDataCollection
0..($counterName.count -1) `
| %{
    $objCCD = New-Object System.Diagnostics.CounterCreationData
    $objCCD.CounterName = $counterName[$_]
    $objCCD.CounterType = $counterType[$_]
    $objCCD.CounterHelp = $counterHelp[$_]
    $objCCDC.Add($objCCD) > $null
}
$objCCDC | Format-Table -AutoSize | Out-String | %{[Console]::WriteLine($_)}

# Perfmon Execute
[System.Diagnostics.PerformanceCounterCategory]::Create($categoryName, $categoryHelp, $categoryType, $objCCDC) | Out-String | %{[Console]::WriteLine($_)}

2: アプリでカスタムパフォーマンスカウンタに送信する処理を書く

こちらも最低限のサンプルを載せておきます。

private static readonly string categoryName = "MyAppPerformance";
private static readonly string countCounterName = "Count";
private static readonly string timeCounterName = "Time";


public void Write(PerformanceData data)
{
    var retryCounter = new PerformanceCounter(categoryName, countCounterName, data.EventName, false);
    var elaspedCounter = new PerformanceCounter(categoryName, timeCounterName, data.EventName, false);
    retryCounter.RawValue = data.Count;
    elaspedCounter.RawValue = data.Time;
}

3: 設定用のjsonに追記する。

{
    "Id": "PerformanceCounterMyAppCount",
    "FullName": "AWS.EC2.Windows.CloudWatch.PerformanceCounterComponent.PerformanceCounterInputComponent,AWS.EC2.Windows.CloudWatch",
    "Parameters": {
        "CategoryName": "MyAppPerformance",
        "CounterName": "Count",
        "InstanceName": "instance1",
        "MetricName": "MyAppPerformanceCount",
        "Unit": "Count",
        "DimensionName": "Host",
        "DimensionValue": "{hostname}"
    }
},
{
    "Id": "PerformanceCounterMyAppTime",
    "FullName": "AWS.EC2.Windows.CloudWatch.PerformanceCounterComponent.PerformanceCounterInputComponent,AWS.EC2.Windows.CloudWatch",
    "Parameters": {
        "CategoryName": "MyAppPerformance",
        "CounterName": "Time",
        "InstanceName": "instance1",
        "MetricName": "MyAppPerformanceTime",
        "Unit": "Count",
        "DimensionName": "Host",
        "DimensionValue": "{hostname}"
    }
},

MetricName はCloudWatch から見てわかりやすい名前にしましょう。またUnitもCloudWatchで使える値の中から適したものを選んでおきます。 また、Dimensionが1組だけ指定できます*4。1つだけなので厳選する必要がありますが、複数台スケーリングさせているWebサーバー上のパフォーマンスカウンタであれば、ホスト名でDimensionを作っておくのがいいかと思います*5

4: アプリを実行して、CloudWatchに送信されていることを確認する。

まとめ

というわけで、AWS といいつつ、ほぼC# とPowerShellな内容になってしまいました。が、内容はC# と PowerShell でも、これを使うことによってシステムから出力されるイベントログやパフォーマンスカウンタだけでなく、自前のアプリのログやパフォーマンスカウンタをCloudWatchとCloudWatchLogsで管理できるようになりました。

この機能の一番のメリットは、CloudWatchとCloudWatchLogsというAWSのパワフルなツールを活用していますが、ログやパフォーマンスカウンタを送信するアプリ側はAWSのことを一切知らない点です。アプリだけであれば、オンプレミスでもAzuer上でも動きます。そして、ETWもしくはEventLogおよびパフォーマンスカウンタという標準的なツールでそのログやカウンタの監視もできます。Ec2Configサービスがその間の橋渡しをしてくれているのです。

なお、弊社ではアプリは別の監視サービスで監視していて、このCloudWatchとCloudWatchLogsはそのサービスでは監視しづらい、Webサーバーの常駐させる自前のWindowsサービスの監視に利用しています。

と持ち上げたところでちょっと下げておくと、まだこなれていない部分もあります。パフォーマンスカウンタのデータを取る頻度をもっと多くしてほしかったり、 設定用のjsonで {ip_address} という置換変数だけ置換されなかったり、Dimensionが1つしか指定できなかったりします。このあたりは、今後のアップデートに期待しています。

PS

Windows パフォーマンスカウンタをCloudWatch に送信しようとすると、設定用の json ファイルがかなり肥大化してきて、手書きではなかなかつらいです。

というわけで、弊社ではExcel VSTO を使って、Excel上で必要なパフォーマンスカウンタ一覧や、イベントログなどを指定すると、設定用のjsonを吐き出すツールを私が作っています。

この辺はきっとAWS側でのなにかしらの対応してくれるとは思っていたりします...

*1:バージョンの確認とサイレントインストールがめんどいです

*2:なので、ETWのログを見るためには別のツールを利用する

*3:PowerShellという名のC#コードという噂もある

*4:本来のPutCustomMetrics なら10組までいけるはずですが...

*5:ip_address は現バージョンでは動きません