Windows Azure Tips from Prospex

システム開発 × インフラ構築・運用 × グラフィックデザイン = プロスペックス

Expression を使って動的 OrElse

clock February 19, 2012 15:47 by author Ito

なますて。

とあるAzureプロジェクトで、パフォーマンスが超悪いものがあるので、それを何とかしている今日この頃です。 

 

さて、Azureでトレンディーなもののひとつに、Windows Azure Table があります。
こいつは、自動でスケールアウトされたり、データを大量に詰め込んでもほぼ定数時間で取得出来たり(PartitionKey, RowKey を指定したとき)と、
使いこなせれば非常に強力なものだと思います。

しかし、使い方を間違ったら…マジやばいです。

 

今回のプロジェクトでも、Azure Table を使用していますが、その使用している部分が非常に遅い…

具体的に何が遅いと言うと

・RDBMSのように、親テーブルがあり、詳細情報用に子テーブルがある。
・親テーブル項目をキーとして、子テーブルを Azure Table から一件ずつ取得している。 

実際には、親テーブルは Sql Azure にいて、詳細情報は Azure Table にいるという変則なのですが…

この、Azure Table から一件ずつ取得が超遅いのです。

速度を調べてみると、Azure Table から一件取得するのにかかる時間は大体50ms。
それを約300件分取得しようとするので…15秒!
(現在は苦肉の策で20件ずつのページング処理化してあります)

 

そんなの、一回で全部取得すればいいじゃないか

 

そこで考えたのが、取得するエンティティのPartitionKeyとRowKeyをOrElseでつなげる。

しかし、取得したいエンティティの件数は動的に変化するので、Expressionクラスを用いて動的にWhereメソッドに渡す式を作ってみます。

 

Expression exp = null;
ParameterExpression paramExp = Expression.Parameter(typeof(HogeEntity), "e");

// Expression : e.PartitionKey == pkey
string pkey = string.Format("prospex-hoge-{0:0000000000}", tenantID);
Expression pKeyExp = Expression.Property(paramExp, "PartitionKey");
Expression pkeyEqualExp = Expression.Equal(pKeyExp, Expression.Constant(pkey));

foreach ( var item in items) {
    // Expression : e.RowKey.CompareTo(rkey) >= 0
    string rkey = string.Format(
        "{0:0000000000}-{1:0000000000}-{2:0000000000}-{3:0000000000}-{4:000}-",
        item.TenantID,
        item.BoxID,
        item.ThreadID,
        item.MessageID,
        item.Language);
    Expression rKeyExp = Expression.Property(paramExp, "RowKey");
    System.Reflection.MethodInfo compareToType = typeof(string).GetMethod("CompareTo",new Type[] { typeof(string) });
    Expression rKeyCompareToExp = Expression.Call(rKeyExp, compareToType, Expression.Constant(where_RKey));
    Expression rkeyGteExp = Expression.GreaterThanOrEqual(rKeyCompareToExp, Expression.Constant(0));

    // OrElse結合
    if ( exp == null ) {
        exp = rkeyGteExp;
    } else {
        exp = Expression.OrElse(exp, rkeyGteExp);
    }
}
 
exp = Expression.AndAlso(pkeyEqualExp, exp);
 
TableServiceContext context = _tableClient.GetDataServiceContext();
IQueryable<HogeEntity> entitys = context.CreateQuery<HogeEntity>("HogeEntity");
var q = from t in entities
        select t;
q = q.Where(Expression.Lambda<Func<HogeEntity, bool>>(exp, paramExp));

 

そして実行…したら例外でました。内容は、

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">

  <code>InvalidInput</code>
  <message xml:lang="en-US">0:One of the request inputs is not valid.
RequestId:d54d290e-bc01-4fa7-9c77-30bde58fde6c
Time:2012-02-19T07:49:39.6028980Z</message>
</error> 

理由はたぶん、URLが長くなりすぎたと思われ。
(TableServiceContext? は、REST API のラッパーであるため、最終的には Azure Table のURLにリクエストを投げているだけ)

 

そこで、とあるサイトで「バッチリクエスト(エンティティグループトランザクション)」で送る必要があるようなことが書かれていたため、試してみた。

 

foreach ( QueryOperationResponse res in context.ExecuteBatch((DataServiceQuery<MessageBodyEntity>)q) ) {
    foreach ( HogeEntity entity in res) {
        data = entity;
    }
}


これで実行…だが結果は同じであった。


とりあえず調べてみて分かった Azure Table の制限。

・長いURLはつかえない。(ブチザッキさんに詳しく書かれている)
・バッチリクエスト(ExecuteBatch)では、$filter は使えない。(上の方法だと、最終的に$filterを使うURLになる)


だめじゃん…


Azure Table はちゃんと設計しないとハマります。




Storage Table の設計パターン(絞り込みを考える)

clock January 14, 2012 19:05 by author Sekine

Storage Table を利用する場合、

PartitionKeyとRowKeyをどのように決めるのか重要なポイントとなります。

あまり気にせずに作ってしまうと、テーブル内のデータ量が多くなって来た時に、

抽出時のパフォーマンスが落ち、致命的な欠陥につながります。

特にRowKeyの考え方が重要で(PartitionKeyもそうですが)

まず、Storage Tableでは、RowKeyに対して昇順のインデックス付けがされます。

Table Storage の トランザクション – EGT

それを念頭に、実際にRowKeyをどのように組み立てれば良いかという事になるのですが、

例えば、PartitionKey、RowKey 以外の項目に where をかけるとすると、

対象テーブルの全てに走査・抽出が行われ、

データ量(レコード量)が多い場合には結果的に遅くなり、

さらにタイムアウトにより後方に存在するデータは抽出されないという事になります。

 

そのような事にならないように、

抽出するキーとなるものは、RowKeyに含め、RowKeyに対して where をかける

というのが原則となります。

RowKeyは、文字列型ですが、64KBの長さではなく、260文字しか有効ではありません。
(これは、PartitionKeyでも同じです)

 

データを表すキー項目が一つであれば、そのままRowKeyを使用するだけですが、

そんな単純なデータは多くはなく、複数のキーが有るのが普通でしょう。

そのような場合は、各キーを固定長の文字にして、連結してRowKeyとします。

例えば、店舗名、従業員名のキーが在るとしましょう。その場合、

RowKey = 店舗名20文字_従業員名20文字

のようにします。

数値が必要なら、string.Format(“0:10D”, 設定値);

日付なら、Ticks をうまく使うなどして、各項目を固定長に整形して連結します。

文字列が長い場合には、MD5ハッシュなどすれば、32文字になります。
(その場合、大文字小文字でハッシュ値が違うので注意してください。常に小文字にするなど)

 

このようにRowKeyを決める事で、

上の例で言えば、ある店舗名の従業員一覧が欲しい場合、

var query = from t in {TableServiceContextの従業員データ}
            where
            t.PartitionKey == partitionKey &&
            t.RowKey.CompareTo({調べたい店舗名20char}) >= 0 &&
            t.RowKey.CompareTo({調べたい店舗名20char} + "\uffff") < 0
            select t;

とすることで、その店舗名で始まるRowKeyを抽出できます。

t.RowKey.CompareTo({other}) >= 0 &&
t.RowKey.CompareTo({other} + "\uffff") < 0

上の、この構文は、StartsWith 相当です。

t.RowKey.StartsWith({other}) と同等になりますが、

Storage Table (TableServiceContext) の場合、Queryプロバイダが

StartsWithに対応していませんが、上記の書き方によって同等となります。

さらに言えば、部分条件抽出は、上記 StartsWith 相当しか無いと思ったほうがよさそうです。

SubString、EndWith、(他、部分的比較命令)は使用できません。

また、それらに相当する書き方も実際うまくいかないのが現状です。

この辺は、開発ストレージで動いたとしても、実際に動かなかったりしますので

十分注意してください。

(個人的には、開発ストレージで開発しないほうが良いとも思っています)

 

上記のことから、RowKeyは先頭一致の抽出が可能な事と、

それを利用して、キー項目を固定長で連結することが大事です。

固定長にするのは、where の文字列を作るときに任意な長さでは曖昧になるため。

また、RowKeyの値は変更できません。

 

以前、論理削除の意で、

0_DATA
削除すると
1_DATA

という、先頭1charを削除フラグとして試したことがあります。

UpdateChangesは失敗し、更新できません。

これは、仕様ですので当然なのですが、

思いついた時の歓喜と、その後の落胆が大きかったです・・



テーブルまとめ

clock November 25, 2011 21:13 by author Ito

前回の内容は無視して…

こんにちは。
さて今回は、少しテーブルストレージについて調べていたので、
簡単なメモとしてまとめてみます。



■分散 Key-Value 型である

テーブルといえば、RDBMS のテーブルを思い出しますが、
Azure ストレージテーブルは、ただのキーに対してデータがある連想配列みたいなものです。
(.NETでいうとDictionaryか)

RDBMS のテーブルとは似ても似つかないため、いろいろな制約がでてきます。


■テーブルのプロパティは、PartitionKey, RowKey, Timestamp, 他 から成る

テーブルのプロパティは、列と思ってください。(というか同じか)
Azure ストレージテーブルには、必ず「PartitionKey」「RowKey」「Timestamp」の3つの列があり、
他にユーザーが任意に列を定義できます。

・PartitionKey
 前述のパーティションを指定するプロパティ。String型
 同じテーブルの同じパーティション値を持つエンティティが、
 ストレージノードに格納される 1単位 となります。

・RowKey
 パーティション内でエンティティを識別するためのキー。String型
 そのため、パーティション内で一意である必要がある。
 これにより、テーブル内のエンティティは、「PartitionKey-RowKey」が一意になる。

・Timestamp
 エンティティの排他制御に使われるプロパティ。
 エンティティの追加、更新時に自動的に設定される。


■テーブルはパーティション単位で分割保存される

Azure ストレージテーブルのデータは、パーティション単位(同じPertitionKeyのエンティティ)で分割され、
クラウド上のサーバー群(ストレージノード)に保存されます。

一つのストレージノードへのアクセスが偏らないよう、
自動的にストレージノードへのパーティションを配分してるようです。

そのため、適切にパーティションを設定すれば、自動的にスケールアウトされる仕組みになっています。


■インデックス化されるプロパティは、PertitionKeyとRowKeyのみ

インデックス化?というかどうかはわかりませんが、
PertitionKey-RowKey が Key-Value でいう Key の部分になります。(本当かな…)

Linqなどで取得クエリを書く場合、これらのプロパティを指定すると早いかもしれません。
それ以外のプロパティはフルスキャンされる為、対象件数が多いと遅いかもしれません…
(パフォーマンスのテストはいつかしたい…)

さらに、PartitionKeyをまたいでの取得は遅いみたいです。
そのため、PertitionKeyを指定するようなクエリ考えた方がパフォーマンスが良いと思います。

と言っても、異なるストレージノード間にあるエンティティは、同時に探索処理が走るようで…
要テストですね…


■テーブルに入れられるエンティティは、他プロパティの構造が違っていてもよい

例えば、二つのエンティティ A, B があります。
それぞれのプロパティは以下


この A, B とも、1つの同じテーブルに入れることができます。

1つのテーブルに違う内容のデータが入っているのはなんか気持ち悪いですが、
これが非常に重要になってきます。

重要な理由は…


■パーティション内のエンティティではトランザクション処理が可能

まず、RDBMS のようにトランザクションを張ることはできません!
がんばってもできません。たぶん…

そのため、テーブルAとBを更新したい場合、
Aを更新してからBを更新中にこけても、Aを元に戻すお手軽な方法はありません。

でも…パーティション単位ではトランザクション処理ができます。

つまり、同じテーブルの同じパーティション内にある、A, B エンティティを更新したい場合、
Aを更新してB更新中にこけても、Aを元に戻せます。(更新を取り消せます)

ここで「テーブルに入れられるエンティティは、他プロパティの構造が違っていてもよい」を思い出してください。

先ほどの2つのエンティティ「商品在庫」「商品注文」を同じテーブル同じパーティションにすると…

「商品注文」エンティティを追加

在庫がないため「商品在庫」エンティティの更新失敗

「商品注文」エンティティの追加を取り消し

 

スケールアウト、取得クエリのパフォーマンス、トランザクション…
これらを意識して Azure ストレージテーブル を設計する必要があります…

これは頭痛くなるな…



ストレージにRESTでアクセス

clock July 12, 2011 22:20 by author Ito

Azure のストレージは、REST API が用意されているみたい。
http://msdn.microsoft.com/en-us/library/dd179355.aspx

そこで、直接このAPI達を使ってみて、Azure ストレージをちょっとだけ詳しくなろうと試みてみた…

まずは何も考えずに、blob の List Containers API の URL をブラウザでアクセス。

 

まあ予想通りエラーです。

さきほどの MSDN を眺めてみると…下のほうにサンプルが…

 GET http://myaccount.blob.core.windows.net/?comp=list&maxresults=3 HTTP/1.1
x-ms-version: 2009-09-19
x-ms-date: Sun, 27 Sep 2009 22:08:44 GMT
Authorization: SharedKey myaccount:CY1OP3O3jGFpYFbTCBimLn0Xov0vt0khH/D5Gy0fXvg=

とりあえず、サンプルを今回アクセスするストレージに合うように書き換えて、
Authorization はよくわからんので消して、TCP 送信ソフトで送信してみた。

あれ…リクエストが返ってこない…

それから1時間格闘の末、HTTPヘッダの最後に空行を入れないといけない駄目という事がわかった…
(うち、ネットワーク弱いな…)

ということで、以下のリクエストを投げてみる。(****はストレージアカウント名)

[HTTP リクエスト]

GET http://****.blob.core.windows.net/?comp=list&maxresults=3 HTTP/1.1
x-ms-version: 2009-09-19
x-ms-date: Tue, 12 Jul 2011 13:10:38 GMT
<空行>

なんか返ってきた!

HTTP/1.1 400 Bad Request
Content-Type: text/html; charset=us-ascii
Server: Microsoft-HTTPAPI/2.0
Date: Tue, 12 Jul 2011 13:40:02 GMT
Connection: close
Content-Length: 334

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Bad Request</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Bad Request - Invalid Hostname</h2>
<hr><p>HTTP Error 400. The request hostname is invalid.</p>
</BODY></HTML>

あれ? HTTP/1.1 400 Bad Request ってなんだ…

無効なホスト名って言われてる…

さらに30分…
どうやら、HTTP/1.1 では、Host ヘッダが必要みたい。

で Host ヘッダをつけて送信。

[HTTP リクエスト]

GET http://****.blob.core.windows.net/?comp=list&maxresults=3 HTTP/1.1
Host: ****.blob.core.windows.net
x-ms-version: 2009-09-19
x-ms-date: Tue, 12 Jul 2011 13:10:38 GMT

[レスポンス]

HTTP/1.1 404 The specified resource does not exist.
Content-Length: 223
Content-Type: application/xml
Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: 8c77457c-e991-4c54-9b27-5d30eb854811
x-ms-version: 2009-09-19
Date: Tue, 12 Jul 2011 13:44:34 GMT

<?xml version="1.0" encoding="utf-8"?><Error><Code>ResourceNotFound</Code><Message>The specified resource does not exist.
RequestId:8c77457c-e991-4c54-9b27-5d30eb854811
Time:2011-07-12T13:44:34.9547433Z</Message></Error>

ブラウザでアクセスしたときと同じ内容だ!

これでブラウザに並んだぜ。

で、消してた Authorization ヘッダをつけてみるが…

Authorization: SharedKey myaccount:CY1OP3O3jGFpYFbTCBimLn0Xov0vt0khH/D5Gy0fXvg=

myaccount 部分はストレージアカウント名で良いとして…
CY1OP3O3jGFpYFbTCBimLn0Xov0vt0khH/D5Gy0fXvg= の部分はどうすればいいんだ?

そこでいろいろと調べてみたら、参考になりそうなサイトを見つけた。

■てすとぶろぐ さん
 ・Azure Storage 関係の REST API で利用する Authorization ヘッダ
  
http://blogahf.blogspot.com/2011/05/azure-storage-rest-api-authorization.html
 ・Azure Storage REST API の Authorization ヘッダ・改訂版
  
http://blogahf.blogspot.com/2011/05/azure-storage-rest-api-authorization_25.html

■REST API でBlob 操作 - Azureの小ネタ
  
http://d.hatena.ne.jp/StateMachine/20110405/1302014188

どうやら、HTTPヘッダの内容に、SHA256ハッシュとBASE64で認証キーを作り、
それを指定するようだ…

で、頑張って作成してアクセス…

[HTTPリクエスト]

GET http://****.blob.core.windows.net/?comp=list&maxresults=3 HTTP/1.1
Host: ****.blob.core.windows.net
x-ms-version: 2009-09-19
x-ms-date: Tue, 12 Jul 2011 13:10:38 GMT
Authorization: SharedKeyLite ****: 7XJc+dO+3uN+ZynDbxRTK4bC4N7UIPhXgN/wedHv50E=

[レスポンス]

HTTP/1.1 400 Authentication information is not given in the correct format. Check the value of Authorization header.
Content-Length: 297
Content-Type: application/xml
Server: Microsoft-HTTPAPI/2.0
x-ms-request-id: e2116cae-c135-497d-8113-dfa7b72c943f
Date: Tue, 12 Jul 2011 13:58:20 GMT

<?xml version="1.0" encoding="utf-8"?><Error><Code>InvalidAuthenticationInfo</Code><Message>Authentication information is not given in the correct format. Check the value of Authorization header.
RequestId:e2116cae-c135-497d-8113-dfa7b72c943f
Time:2011-07-12T13:58:20.3896662Z</Message></Error>

なんか認証キーが違う言われた…

認証キーはプログラム組んで作成させないと難しそうだ…

次回は、認証キーを作るプログラム作りで行こう。

 



合体

clock June 16, 2011 22:29 by author Ito

前々回、Azureで郵便番号検索サービス(失敗)。
前回、RESTでGETなサービスをただのWCFで作る(成功)。

今回はついに 「RESTでGETなAzure版郵便番号検索サービス」 を作る!
(前々回に目指したけど失敗したわけだが…)

ということで…

 

まずはAzureのプロジェクトを作ります。
Webロールは 「WCF サービス Webロール」 を使用します。

そして、勝手に作られた Service1 とかはいらない子なので消して。

「AJAX 対応 WCF サービス」 を追加。

まずは前回作ったコードをそのままコピーして実行…

 

おうけい。最大の難関を突破した!

 

次に、前々回で作った郵便番号検索部分のコードを移植して実行…

やったよ!かあさん!!

 

さて、次のネタ考えないと…

 

以下が最終的なコードです。

 

[ZipCodeSvc]
namespace AzureWcfService {

    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class ZipCodeSvc {
        
        [OperationContract]
        [WebGet(ResponseFormat=WebMessageFormat.Json , UriTemplate="GetAddress/{zipCode}")]
        public string GetAddress(string zipCode) {
            
            CloudStorageAccount   storageAccount = null;
            CloudTableClient      tableClient    = null;
            ZipCodeServiceContext context        = null;

            string address = string.Empty;

            try {
                storageAccount = AzureStorageAccount.GetStorageAccount();

                tableClient = storageAccount.CreateCloudTableClient();
                tableClient.CreateTableIfNotExist("ZipCode");

                context = new ZipCodeServiceContext(storageAccount.TableEndpoint.ToString(), storageAccount.Credentials);

                var query = context.CreateQuery("ZipCode").Where(r => r.Field1 == zipCode).ToArray();

                address = query.First().Field2 + query.First().Field3 + query.First().Field4;

            } catch {
                address = "取得できません";
            }

            return address;
        }
        
    }
}

 

[AzureStorageAccount]
namespace AzureWcfService {

    public class AzureStorageAccount {

        public static CloudStorageAccount GetStorageAccount() {
            
            try {
                CloudStorageAccount.SetConfigurationSettingPublisher(
                    (configName, configSetter) => {
                        configSetter(RoleEnvironment.GetConfigurationSettingValue(configName));
                        RoleEnvironment.Changed += (sender, arg) => {
                            if (arg.Changes.OfType().Any((change) => (change.ConfigurationSettingName == configName))) {
                                if (!configSetter(RoleEnvironment.GetConfigurationSettingValue(configName))) {
                                    RoleEnvironment.RequestRecycle();
                                }
                            }
                        };
                    }
                );

                return CloudStorageAccount.FromConfigurationSetting("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString");

            } catch {
                return null;

            }
        }
    }

}

 

[ZipCodeEntity]
namespace AzureWcfService {

    public class ZipCodeEntity : TableServiceEntity {

        public ZipCodeEntity() {
            PartitionKey = "";
            RowKey       = "-1";
        }

        public string Field1 { get; set; }
        public string Field2 { get; set; }
        public string Field3 { get; set; }
        public string Field4 { get; set; }

    }

}

 

[Web.config]
<system.serviceModel>
<services>
  <service name="AzureWcfService.ZipCodeSvc">
    <endpoint address="" behaviorConfiguration="AzureWcfService.ZipCodeSvcAspNetAjaxBehavior"
binding="webHttpBinding" contract="AzureWcfService.ZipCodeSvc" /> </service> </services> <behaviors> <endpointBehaviors> <behavior name="AzureWcfService.ZipCodeSvcAspNetAjaxBehavior"> <!--<enableWebScript />--> <webHttp /> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior name=""> <serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /> </behavior> </serviceBehaviors> </behaviors> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" /> </system.serviceModel>
 


郵便番号検索ウェブサービス作ってみる

clock April 12, 2011 21:44 by author Ito

こんばんは。

前回、超簡単なウェブサービスを作ったので、さらに毛を生やしたいので、郵便番号検索なAzureウェブサービスを作ってみる。
だが、その前にまずは普通のMVCアプリケーションで作ってみた。

まず、今まで何度も出てきた郵便番号をTableストレージに入れるため、 Azure Storage Explorer でUploadを試みる・・・
(愛知県の郵便番号CSVをアップロード)

案の定文字化け・・・
安東の記事にもあるが、CSVの文字コードをUnicodeにしないといけないようだ。
今回はUTF-8で試してみる・・・

アップロード成功!

しかし、列名がField1、Field2…というのがかっこ悪い。
今度はヘッダをつけてアップロード・・・

あれ?なぜエラーがでるんだ?

試行錯誤すること30分・・・あきらめました。
かっこ悪いがField1でやってやろう。

次はWebRole(MVC)でプログラミング。

■ZipCodeエンティティクラス

public class ZipCode : TableServiceEntity {

    public ZipCode() {
        PartitionKey = ""; RowKey = "-1";
    }

    public string Field1 { get; set; }
    public string Field2 { get; set; }
    public string Field3 { get; set; }
    public string Field4 { get; set; }
}

■コントローラー

public class ZipCodeController : Controller

    public ActionResult Index(string zipCode) {
   
        CloudStorageAccount storageAccount = null;
        CloudTableClient tableClient = null;
        ZipCodeServiceContext context = null;

        try {
            CloudStorageAccount.SetConfigurationSettingPublisher(
                (configName, configSetter) => {
                    configSetter(RoleEnvironment.GetConfigurationSettingValue(configName));
                    RoleEnvironment.Changed += (sender, arg) => {
                        if (arg.Changes.OfType().Any((change) => (change.ConfigurationSettingName == configName))) {
                            if (!configSetter(RoleEnvironment.GetConfigurationSettingValue(configName))) {
                                RoleEnvironment.RequestRecycle();
                            }
                        }
                    };
                }
            );

            storageAccount = CloudStorageAccount.FromConfigurationSetting("StorageConnection");

            tableClient = storageAccount.CreateCloudTableClient();
            tableClient.CreateTableIfNotExist("ZipCode");
            
            context = new ZipCodeServiceContext(storageAccount.TableEndpoint.ToString(), storageAccount.Credentials);

            if ( !String.IsNullOrEmpty(Request.Form["ZipCode"]) ) {
                zipCode = Request.Form["ZipCode"];
            }

            var query = context.CreateQuery("ZipCode").Where(r => r.Field1 == zipCode).ToArray();

            if ( query.Any() ) {
                ViewData["ZipCode"] = query.First().Field1;
                ViewData["State"] = query.First().Field2;
                ViewData["City"] = query.First().Field3;
                ViewData["Address"] = query.First().Field4;
            }

        }
        catch ( StorageException ) { }

        return View();
}

■ビュー
<p>
    〒<%: ViewData["ZipCode"] %><br />
    <%: ViewData["State"] %><br />
    <%: ViewData["City"] %><br />
    <%: ViewData["Address"] %><br />
</p>
<% using ( Html.BeginForm() ) { %>
    <%: Html.TextBox("ZipCode") %>
    <input type="submit" value="検索" />
<% } %>


そして実行・・・

自分の実家の郵便番号を入れてみる・・・

あれ?でてこないぞ?

ストレージの内容をみたら、1000件分のデータしか入ってない・・・

Azure Storage Explorer にはめられたか・・・

まあ、検索できたので今回は終了!

次回はちゃんとデータ用意して、サービス化させよう・・・



Table Storage の トランザクション - EGT

clock March 23, 2011 18:03 by author Sekine

Windows Azure Table のトランザクションについて。

Azure テーブルは、端的には自由度が高いと言える。

その反面、SQLのDBテーブルのように、実質的なスキーマは無く、

また、テーブル間のリレーション(外部制約)も無いため、逆に扱いにくく感じる面もあるでしょう。



Windows Azure Storage Table で、知っておくべき重要な点は、

・一意性を確保するためのキー項目。PartitionKey、RowKey の2つの値で一意。

・レコードのエンティティ(項目)数の上限は、255個まで(キー項目も含む)。

・1レコードのデータ上限は、1Mバイトまで。

・レコードの更新には、Timestamp値による、楽観的同時実行制御が掛かる。


テーブルを利用するにあたっては、データ型も含めて、こんなところでしょう。


複数のテーブル間(ビジネス的な)のリレーション、およびトランザクションについて考えておくべき事項は、

・文字列型は、64KBまで。PartitionKey と RowKeyは文字列型という事。

・RowKey の値により、自動的にソートされる。

・1回のバッチ処理(複数レコードの一斉書込)は、100レコード且つ4Mバイトまで。

・バッチ処理では、Entity Group Transactions(EGT)のトランザクションが利用できる。

・EGTの範囲は、同一のPartitionKey、の同じテーブルに対してのみ。


という事を、常に考えながらテーブル設計をしないといけません。



まず、簡単な説明から

ここで、→ 前回の記事
Azure Storage Tableでオートナンバーを実現する採番テーブル

で、一部利用している所がある。

10レコードを追加した後に、
> context.SaveChanges(SaveChangesOptions.Batch);

を呼び出しています。

SaveChangesメソッドには、Batchオプションをつけて呼び出している。

これは、10レコードを一気に書き込みをしていますが、途中でエラーがあった場合には、

1行も書き込まれない事を意味します。

前回の記事での書き込み例では、RowKeyの重複は無いように作成するので、

途中でエラーが起こることは通常ありえないが、

もし偶然に、他のスレッドからRowKeyが重複するレコードが書き込まれた場合には、

一意制約でエラーとなり失敗します。


業務処理をする上では、RowKeyの重複が無い前提での処理を行う場合には、

偶然的にも、あり得ないように処理をするように心がけるべきです。



次に、インデックスの観点より、

Azure Table を使ったサンプルコードを見ると、

RowKeyの生成に、

DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks

というのを良く見かけます。

例:RowKey = string.Format("{0:10}", DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks) 
実際には、この後にも文字が続く..

Storage Table は、RowKey の値は、自動的にソートされます。というのを念頭に、

このようにすることで、新しいレコードは上位に来るという事になります。


具体的に言うと、例えば、テーブルに唯一のレコードが存在するとして、

RowKeyは、仮に、{"10文字_店舗コード5桁_店舗名"} と取り決めたとすると、

こんなデータになります。
"0000000001_00001_ショップ●●"


ショップコード"00001"のデータの取得は、

{RowKey抽出式 = SubString(11, 5).CompareTo("00001") >= 0 }.Take(1)

とすることで、最新のデータが取れます。

仮に、店舗登録が同時アクセスなどで、不整合なデータが出来たと仮定すると、

"0000000005_00001_ショップ■■"
"0000000004_00001_ショップ□□"
"0000000003_00001_ショップ△△"
"0000000002_00001_ショップ××"
"0000000001_00001_ショップ●●"

※先頭10文字は分かりやすいように記述してますが、実際は大きな数字です。

こんなデータが上から並んでいることになりますが、

最後に書き込まれたレコードは、"ショップ■■"として見なすことができます。

並び順は、上からなので効率の良い並び順となります。


これは、概念的な事を言ってますので、

たとえば、最新10件が業務に必要なデータとして、常に追加されるという様なケースでも、

利用できます。

溜まった不要データは、後でWorkerなどから削除すれば良いのです。



次に、トランザクションの話をします。

・EGTの範囲は、同一のPartitionKey、の同じテーブルに対してのみ。

という制約があります。

では、実際はどういう事なのかという例として、

2つのエンティティクラスを定義します。

論理的には、JOb(親) <-> Employee(子)のリレーションを考えたテーブルになります。

using System;
using System.Data.Services.Common;
namespace PubsStorageStore.TableEntitys
{
    [DataServiceKey("PartitionKey", "RowKey")]
    public class JobsEntity
    {
        public string PartitionKey { get; set; }
        public string RowKey { get; set; }
        public DateTime Timestamp { get; set; }

        public string job_desc { get; set; }
        public int min_lvl { get; set; }
        public int max_lvl { get; set; }
    }

    [DataServiceKey("PartitionKey", "RowKey")]
    public class EmployeeEntity
    {
        public string PartitionKey { get; set; }
        public string RowKey { get; set; }
        public DateTime Timestamp { get; set; }

        public string fname { get; set; }
        public string minit { get; set; }
        public string lname { get; set; }
        public int job_id { get; set; }
        public int job_lvl { get; set; }
        public string pub_id { get; set; }
        public DateTime hire_date { get; set; }
    }
}

RowKeyの定義は、
{"JobIDとして10桁_EmployeeIDとして10桁"}
とします。

JObのレコードを作成するには、
JobsEntity.PartitionKey = "test-pubs1";
JobsEntity.RowKey = experienceDac.CreateNumber2("Job").ToString().PadLeft(10, '0');
..その他データ

Employeeのレコードを作成するには、
employeeEntity.PartitionKey = "test-pubs1";
employeeEntity.RowKey = JobsEntity.RowKey + "_" + experienceDac.CreateNumber2("Employee").ToString().PadLeft(10, '0');
..その他データ

※ID生成には、前回の採番処理を使っています。

1つのJobのAdd
context.AddObject("Employee", JobsEntity);

for 等で、複数のEmployeeのAdd
context.AddObject("Employee", employeeEntity);

書き込み
context.SaveChanges(SaveChangesOptions.Batch);

これにより、

PartitionKey RowKey job_desc min_lvl ... fname minit ...
test-pubs1 0000000001 入力値 入力値 ...      
test-pubs1 0000000001_0000000001       入力値 入力値 ...
test-pubs1 0000000001_0000000002       入力値 入力値 ...
test-pubs1 0000000001_0000000003       入力値 入力値 ...
test-pubs1 0000000001_0000000004       入力値 入力値 ...


というようなレコードが出来上がります。

具体的には、RowKeyはもっと考慮すべき事になると思いますが・・。

AddObjectでは、両方の書き込み対象として、"Employee"に書き込まれていることで、

EGTを利用することができます。



SaveChangesのバッチ動作は、中途にエラーがあれば、全て無効ということになります。

アプリケーションは、FULL REST設計でなければ、エンティティクラスを用いるでしょうから、

物理的に、Azure Table上の同一テーブルかどうかは、意識しないはずです。



関根:



データバインドって、言ってみるとクセになりそう

clock March 10, 2011 22:28 by author fujii

みんな楽しくやってるだろうか?

そんな寂しい時は、声を出して言ってみよう。

「データバインド」

ということで、今日は「データバインド」して帰ります。

先週作ったプロジェクトに「WebForm2.aspx」を追加。

image

おもむろに「GridView」を張り付ける。

image 

「WebForm2.aspx」、コードはこれだけで……

   1: <%@ Page Language="vb" AutoEventWireup="false" CodeBehind="WebForm2.aspx.vb" Inherits="WebRole1.WebForm2" %>
   2:  
   3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   4:  
   5: <html xmlns="http://www.w3.org/1999/xhtml">
   6: <head runat="server">
   7:     <title></title>
   8: </head>
   9: <body>
  10:     <form id="form1" runat="server">
  11:     <div>
  12:         <asp:GridView ID="GridView1" runat="server" />
  13:     </div>
  14:     </form>
  15: </body>
  16: </html>

 

「WebForm2.vb」はこれだけ。

   1: Imports Microsoft.WindowsAzure
   2: Imports Microsoft.WindowsAzure.StorageClient
   3: Imports Microsoft.WindowsAzure.ServiceRuntime
   4:  
   5: Public Class WebForm2
   6:     Inherits System.Web.UI.Page
   7:  
   8:     Private account As CloudStorageAccount = CloudStorageAccount.Parse( _
   9:       RoleEnvironment.GetConfigurationSettingValue("StorageConnection"))
  10:  
  11:     Private table As CloudTableClient = account.CreateCloudTableClient()
  12:     Private tsc As TableServiceContext
  13:  
  14:     Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
  15:  
  16:         tsc = table.GetDataServiceContext()
  17:         table.CreateTableIfNotExist("PostCode")
  18:  
  19:         Dim query = tsc.CreateQuery(Of Class1)("PostCode")
  20:  
  21:         GridView1.DataSource = query
  22:         GridView1.DataBind()
  23:  
  24:     End Sub
  25:  
  26: End Class

今週のポイントは、「Dim query = tsc.CreateQuery(Of Class1)("PostCode")」のみ。

ちなみに、「Class1.vb」は先週と同じ。

   1: Public Class Class1
   2:     Inherits Microsoft.WindowsAzure.StorageClient.TableServiceEntity
   3:  
   4:     Public Property Time As Date
   5:     Public Property TodofukenKana As String
   6:     Public Property KushichosonKana As String
   7:     Public Property JushoKana As String
   8:     Public Property Todofuken As String
   9:     Public Property Kushichoson As String
  10:     Public Property Jusho As String
  11:  
  12:  
  13:     Public Sub New(ByVal partitionKey As String, ByVal rowKey As String, ByVal time As Date, _
  14:                    ByVal todofukenKana As String, ByVal kushichosonKana As String, ByVal jushoKana As String, _
  15:                    ByVal todofuken As String, ByVal kushichoson As String, ByVal jusho As String)
  16:  
  17:         Me.PartitionKey = partitionKey
  18:         Me.RowKey = rowKey
  19:  
  20:         Me.Time = time
  21:         Me.TodofukenKana = todofukenKana
  22:         Me.KushichosonKana = kushichosonKana
  23:         Me.JushoKana = jushoKana
  24:         Me.Todofuken = todofuken
  25:         Me.Kushichoson = kushichoson
  26:         Me.Jusho = jusho
  27:  
  28:     End Sub
  29:  
  30:     Public Sub New()
  31:  
  32:     End Sub
  33:  
  34: End Class

デバッグで実行。

image

データを投入し……

image 

手打ちで「WebForm2.aspx」へ。

image

で、例外がスローされました。

image 

今日は「データバインド」の日なので、こんな例外は無視。

「WorkerRole」は今日の主役じゃないし……

「CreateTableIfNotExist」は、「Try」するといいかも。

で、もう一回実行すると……

image 

「データバインド」

すっきりしたので、帰ろうかと思ったが……

やはり、デプロイしないと本物の「データバインド」ではない。

いつも通り、ストレージを作成し……

image

名前は「gridview」にした。

今思うと、「databind」にすれば良かったが、どうでもいい。

いつも通り、「access key」を張り付ける。

image 

image

image

発行して……

image

「Hosted Service」を作る。

名前は「gridview」にした。

今思うと……それはいい。

image

で、作ったはいいが……

image 

「WorkerRole」の「Status」が「Ready」になったり「Busy」になったりと、落ち着かない。

image

一回止めたけど、ダメ。

image 

「WebRole」は動くが……

image

「WorkerRole」が安定しないので、ボタンを押してもキューばかりが溜まっていく。

データの投入ができなければ、「データバインド」は成立しない。

image

「gridview」……名前が悪い。

ならばと思い、もう一個「Hosted Service」を作る。

名前は念願の「databind」だ。

「region」は「Anywhere US」にしてみた。

image

できた。

なんか、先に作った「gridview」は赤字になっとる。

image

で、うまくいったかというと、これがうまくいかない。

image

今日は「データバインド」の日なので、原因はおいおい調査するとしよう。

今日のところは最後の手段……

調子の悪い「WorkerRole」はあきらめ、

「WebRole」で書き込むまでよ。

image

健闘むなしく、タイムアウトになるものの……

image

154行作ってくれました。

image

それでは一緒に……

image

「データバインド」

後味悪いけど、「データバインド」

本物の「データバインド」は厳しい……

やはり、例外処理は入れといた方が良かったのか?

「データバインド」

声に出すと、すっきりしたので、帰ります。



Azure Storage Tableでオートナンバーを実現する採番テーブル

clock March 4, 2011 22:15 by author Sekine

Azure Storage Table での採番を考えます。

SQL Server での identity列 相当も無く、シーケンス的なオブジェクトもありません。

テーブル設計上では、基本的に RowKey が重複しなければ良いのですが、

やはり何処かで、オートナンバーが必要になる場合があります。



方法として、登録データを取得しMAX値を得る。というのがありますが、ダメですね~

複数スレッドが同時に動く場合には、偶然同じ番号を取得しちゃいます。

lock など クリティカルセクションでも、複数サーバーだと意味がありません。

なので、専用のテーブルで実装します。

また、TableServiceContext.SaveChanges()の挙動が正しいかの検証も兼ねました。



採番テーブルは、キー名と、その現在数値を保持する唯一のレコードとします。

using System;
using System.Data.Services.Common;

namespace StorageStore.TableEntitys
{
    [DataServiceKey("PartitionKey", "RowKey")]
    public class ExperienceEntity
    {
        public string PartitionKey { get; set; }
        public string RowKey { get; set; }
        public DateTime Timestamp { get; set; }

        public int Experience { get; set; }
    }
}

PartitionKey、RowKey で唯一のキーとし、RowKeyを採番トークン名とします。

前回の記事 で、クラスアトリビュートに、[DataServiceEntity] を指定しましたが、

キー指定のため、DataServiceKey属性とします。


次に、コンテキストと実装側

using System;
using System.Linq;
using System.Data.Services.Client;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using StorageStore.TableEntitys;

namespace DataAccess.TableEntitys
{
    public class ExperienceDac
    {
        public TableServiceContext GetContext()
        {
            CloudStorageAccount account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
            CloudTableClient client = account.CreateCloudTableClient();
            client.CreateTableIfNotExist("ExperienceTable");
            return client.GetDataServiceContext();
        }

        private ExperienceEntity createExperienceRecord(TableServiceContext context, string keyName)
        {
            IQueryable<ExperienceEntity> entitys = context.CreateQuery<ExperienceEntity>("ExperienceTable");

            var q = from t in entitys
                    where
                    t.PartitionKey.CompareTo("ApplicationPartition") >= 0 &&
                    t.RowKey.CompareTo(keyName) >= 0
                    select t;

            ExperienceEntity result;

            result = q.AsTableServiceQuery().FirstOrDefault();

            if (result == null)
            {
                result = new ExperienceEntity()
                {
                    PartitionKey = "ApplicationPartition",
                    RowKey = keyName,
                    Experience = 0
                };

                try
                {
                    context.AddObject("ExperienceTable", result);
                    context.SaveChanges();
                }
                catch (DataServiceRequestException)
                {
                    result = q.AsTableServiceQuery().FirstOrDefault();
                }

                if (result == null)
                {
                    throw new Exception("作成エラー");
                }
            }

            return result;
        }

        public int CreateNumber(string keyName)
        {
            TableServiceContext context = GetContext();

            ExperienceEntity entity = createExperienceRecord(context, keyName);

            entity.Experience++;

            try
            {
                context.UpdateObject(entity);
                context.SaveChanges();
            }
            catch (DataServiceRequestException)
            {
                throw;
            }

            return entity.Experience;
        }
    }
}


以上

int newNum = CreateNumber("注文番号");

などで呼び出します。

複数スレッドから同時にアクセスし、タイミング的にバッティングすると、

SaveChanges() が失敗してスローされます。

重複しない事が重要なので、スローされるのはアプリケーション側で対処してね。



以下、検証のためのコード

MVC アプリケーションで、HomeコントローラのView側、適当なところに

<a href="<%= Url.Action("check", "Home") %>">check</a>

コントローラ側は、

public ActionResult check()
{
    ExperienceDac experienceDac = new ExperienceDac();

    int newNum = experienceDac.CreateNumber("bangou");

    ViewData["Message"] = string.Format("発行番号は {0} です", newNum);

    return View("index");
}

ふむ・・・、これでは芸がないので、


ちょっと検証に、その2(check2)をやってみる

<a href="<%= Url.Action("check2", "Home") %>">check2</a>

public ActionResult check2()
{
    CloudStorageAccount account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
    CloudTableClient client = account.CreateCloudTableClient();
    client.CreateTableIfNotExist("ExperienceCheck");
    TableServiceContext context = client.GetDataServiceContext();

    ExperienceDac experienceDac = new ExperienceDac();

    for (int i = 0; i < 10; i++)
    {
        // ランダム秒 Wait
        byte[] buf = new byte[10];
               
        System.Security.Cryptography.RNGCryptoServiceProvider rng =
            new System.Security.Cryptography.RNGCryptoServiceProvider();

        rng.GetBytes(buf);

        int seed = 0;
        foreach (byte b in buf)
        {
            seed += b;
        }
               
        Random rnd = new Random(seed);

        System.Threading.Thread.Sleep(rnd.Next(500, 3000)); // 0.5~ 3 秒 Wait

        // 採番
        int newNum = experienceDac.CreateNumber2("bangou");

        // 書き込み
        ExperienceCheckEntity checkEntity = new ExperienceCheckEntity()
        {
            PartitionKey = "test",
            RowKey = string.Format("{0:D10}_{1}", newNum, Guid.NewGuid().ToString())
        };

        context.AddObject("ExperienceCheck", checkEntity);
    }

    context.SaveChanges(SaveChangesOptions.Batch);

    ViewData["Message"] = "10件作成しました";

    return View("index");
}

//---- ExperienceCheckEntity クラス ----
[DataServiceKey("PartitionKey", "RowKey")]
public class ExperienceCheckEntity
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public DateTime Timestamp { get; set; }
}


説明不要かと思いますが、ランダム秒のwaitをかけて、10回書き込みます。

採番された番号を、ExperienceCheck テーブルの RowKey に含まれるように書き込みます。

RowKeyの重複を避けるため、GUIDを付けています。

別ブラウザを3-4立ち上げて、素早く!? クリック


ExperienceDac.CreateNumber での、SaveChanges() は、

高確率で、というか、ほぼ確実にスローされました(^^;;

ExperienceCheckのRowKeyを確認すると、番号の重複はありません。

やったね!



例外が出にくいように、リトライをします。

CreateNumberメソッドを改良して、

public int CreateNumber2(string keyName)
{
    ExperienceEntity entity = null;

    int retry = 0;

    while (true)
    {
        try
        {
            TableServiceContext context = GetContext();

            entity = createExperienceRecord(context, keyName);

            entity.Experience++;

            context.UpdateObject(entity);
            context.SaveChanges();

            break;
        }
        catch (DataServiceRequestException)
        {
            retry++;
            if (retry >= 10)
                throw;
        }
    }

    return entity.Experience;
}

CreateNumber2メソッドを呼ぶように改良すると。

ブラウザ、5-6枚立ち上げて実行しても、例外は出ませんでした。

データを確認すると、50レコード、重複も無く順番に並んでおります。


テーブル名のキー名など、

あるいは、Blobファイル名の重複回避に利用できるね。



リトライ処置は、例外を出にくいものにするだけなので、

実際のアプリケーションでは、例外が出るものとして、対処する必要があります。


:関根



TableServiceEntity脱却と、Entity Framework

clock February 22, 2011 21:43 by author Sekine

物事は横断的に考えていこうよ っと!

Azure ストレージテーブルでデータ設計を考える。

カテゴリは、LINQです、ちょっと脱線してますが。



そもそも、データの入れ物は、単にプロパティ定義があれば良く、

Table Storage を利用するからと言って、

Microsoft.WindowsAzure.StorageClient.TableServiceEntity なんかに依存したくない。


ハイ。以下の通り。
(サンプルデータのpubs.publishersを想定)

※ System.Data.Services.Client の参照が必要
using System;
using System.Data.Services.Common;

namespace SampleStorageStore.TableEntitys
{
    [DataServiceEntity]
    public class PublisherEntity
    {
        public string PartitionKey { get; set; }
        public string RowKey { get; set; }
        public DateTime Timestamp { get; private set; }

        public string Pub_id { get; set; }
        public string Pub_name { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Country { get; set; }
    }
}

クラスのアトリビュートに、DataServiceEntity を付ければ良いわけです。

Azure Table で使うには、PartitionKey, RowKey, Timestamp を定義する必要があります。


コンテキスト側のメソッドは、次のような感じ

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;

using SampleStorageStore.TableEntitys;

namespace SampleControlImpl
{
    public class PublishersControl
    {
        public void AddPublisher(
            string pub_id, string pub_name, string city, string state, string country)
        {
            var account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");

            CloudTableClient client = account.CreateCloudTableClient();
            client.CreateTableIfNotExist("Publishers");

            TableServiceContext context = client.GetDataServiceContext();

            PublisherEntity publisherEntity = new PublisherEntity
            {
                PartitionKey = "Sample",
                RowKey = "000000",
                Pub_id = pub_id,
                Pub_name = pub_name,
                City = city,
                State = state,
                Country = country
            };

            context.AddObject("Publishers", publisherEntity);
            context.SaveChanges();
        }
    }

}

TableServiceEntity を使った場合と全く同じです。

PublishersControl control = new PublishersControl();
control.AddPublisher("0736", "New Moon Books", "Boston", "MA", "USA");
こんな感じで挿入できます。


さて、TableServiceEntity のしがらみは無くなりました。

じゃ、どうするかと言うと、

個人的には、アスペクトの組入れに、ContextBoundObject を継承させたいのですが、

Entity Framework との融合と、ExpressionVisitorの走査手助けが欲しい・・


Entity Framework を想定すると、ObjectContext の継承と思いますが、

Azure 利用では、ExpressionVisitor が、どうしても欲しいので、

結果的には、つまり、Azure利用での継承元となるのは、ExpressionVisitorとなります。


Entity Framework の想定も外せないところなので、

ここで、POCOの登場であります。

→メモメモ POCO
http://msdn.microsoft.com/ja-jp/library/dd456853.aspx

POJO じゃなくて、POCOです、ポコ!

段々、複雑になってきた感に押されそう・・・・

リレーションには、不得意とされているAzure Tableですが、

Rowkey での自動昇順で、単的リレーションは出来る! (はず)



その前に、POCOを少し勉強しないといけないのでしょうね・・・

pubs のAzureサンプル的なものを、次回かその次回か・・にでも報告しますね。


関根:



Sign In