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 はちゃんと設計しないとハマります。




セッションごにょごにょしたくなった

clock January 9, 2012 23:53 by author Ito

こんばんは。

今夜は Azure のセッションについて調べてみた。
調べていて、良いまとめサイトがあったので紹介します。

ブチザッキ - {2011/08/23}   Windows AzureのWeb Roleでセッションを共有する
http://buchizo.wordpress.com/2011/08/23/windows-azure%E3%81%AEweb-role%E3%81%A7%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%85%B1%E6%9C%89%E3%81%99%E3%82%8B/

では今夜はこのへんで…

 

ごめんなさい。

以上の記事に書かれていることで、だいたいのセッションの問題は解決すると思いうけど、
あるものを使うだけでは面白くないため、今回から自前でセッション管理してみようとおもいます。

まず、簡単なカスタムセッションを作ってみます。
SessionStateStoreProviderBase クラスを継承して作っていくようです。
http://msdn.microsoft.com/ja-jp/library/ms178587%28v=VS.90%29.aspx

サンプルはMSDNにもあるのでそれをみつつ…
http://msdn.microsoft.com/ja-jp/library/ms178588%28v=VS.90%29.aspx
http://msdn.microsoft.com/ja-jp/library/ms178589%28v=VS.90%29.aspx

public class TestSessionStateProvider : System.Web.SessionState.SessionStateStoreProviderBase {

        private int Timeout {
            get { return 20; }
        }

        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) {
            base.Initialize(name, config);
        }
        public override void InitializeRequest(HttpContext context) {
        }

        public override void EndRequest(HttpContext context) {
        }

        public override void Dispose() {
        }

        public override System.Web.SessionState.SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out System.Web.SessionState.SessionStateActions actions) {
            return GetSessionData(true, context, id, out locked, out lockAge, out lockId, out actions);
        }

        public override System.Web.SessionState.SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out System.Web.SessionState.SessionStateActions actions) {
           return GetSessionData(false, context, id, out locked, out lockAge, out lockId, out actions);
        }

        public override void SetAndReleaseItemExclusive(HttpContext context, string id, System.Web.SessionState.SessionStateStoreData item, object lockId, bool newItem) {
            UpdateSessionData(context, id, item, lockId, newItem);
        }

        public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) {
            // 実装は後ほど
        }

        public override void RemoveItem(HttpContext context, string id, object lockId, System.Web.SessionState.SessionStateStoreData item) {
            // 実装は後ほど
        }

        public override void CreateUninitializedItem(HttpContext context, string id, int timeout) {
            // 実装は後ほど
        }

        public override System.Web.SessionState.SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) {
            return new SessionStateStoreData(new SessionStateItemCollection(),
                                             SessionStateUtility.GetSessionStaticObjects(context),
                                             timeout);
        }

        public override bool SetItemExpireCallback(System.Web.SessionState.SessionStateItemExpireCallback expireCallback) {
            // 実装は後ほど
            return false;
        }

        public override void ResetItemTimeout(HttpContext context, string id) {
            // 実装は後ほど
        }

        private SessionStateStoreData GetSessionData(bool lockSession, HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out System.Web.SessionState.SessionStateActions actions) {
            // 今回は、HttpContext.Application にセッション情報を保存する

            SessionStateStoreData sessionState;

            Dictionary<string, byte[]> sessionData
                = (Dictionary<string, byte[]>)HttpContext.Current.Application["session"];

            // id のセッションがあるか
            if ( sessionData != null && sessionData.ContainsKey(id) ) {
                // セッションを復元

                using ( var stream = new MemoryStream(sessionData[id]) ) {
                    using ( var reader = new BinaryReader(stream) ) {

                        sessionState = new SessionStateStoreData(SessionStateItemCollection.Deserialize(reader),
                                                         SessionStateUtility.GetSessionStaticObjects(context),
                                                         this.Timeout);
                    }
                }

            } else {
                // 新規にセッションを作成
                sessionState = CreateNewStoreData(context, this.Timeout);
            }
           
            locked = false;
            lockId = null;
            lockAge = new TimeSpan(0, this.Timeout, 0);
            actions = SessionStateActions.None;

            return sessionState;
        }

        private void UpdateSessionData(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem) {
            // HttpContext.Application に保存されているセッション情報を復元する

            Dictionary<string, byte[]> sessionData
                = (Dictionary<string, byte[]>)HttpContext.Current.Application["session"];
            if ( sessionData == null ) {
                sessionData = new Dictionary<string, byte[]>();
            }

            using ( var stream = new MemoryStream() ) {
                using ( var writer = new BinaryWriter(stream) ) {
                    var sessionItem = (SessionStateItemCollection)item.Items;
                    sessionItem.Serialize(writer);
                }

                sessionData[id] = stream.GetBuffer();
            }

            HttpContext.Current.Application["session"] = sessionData;
        }

    }

ただ HttpContext.Application に保存するだけのものです。
(InProc セッションの超劣化版)

これから少しずつまともに使えるものにしていこう。



テーブルまとめ

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>
 


WCFでRESTなサービス作ってみた (Azure無視)

clock May 19, 2011 19:13 by author Ito

いままで、AzureでWCFな郵便番号検索サービスつくるぜ!と、ちまちま作ってきましたが、
今回はAzure無視で行きます!

理由?
今まで散々失敗してきたから…

まずは…普通のWCFサービス作ろう!

…かなり退化しているがキニシナイ。

 

■WCFプロジェクトの作成

VisualStudio(2010)で、「WCF サービス アプリケーション」プロジェクトを作ります。

そして、デフォルトで作られる Service1.svc たちは速攻で亡き者にします。

 

■AJAX 対応 WCF サービス作成 (GET)

前回、普通のWCFサービスを試したため、
今回はちょっと先に進むべく、『AJAX 対応 WCF サービス』 なんてものを使ってみます。
しかも、GETで取得できるものを…

サービスの内容はこんな感じにコーディング

[WcfTestService.svc]

namespace WcfTestService {

    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class WcfTestService {

        [OperationContract]
        [WebGet(ResponseFormat=WebMessageFormat.Json)]
        public string HelloWorld(string name) {
            return "こんにちは、" + name + "さん。";
        }

    }
}

WebGet(ResponseFormat=WebMessageFormat.Json) とすることで、GETで取得できるようだ(JSON形式)。

 

そしておもむろに実行

なんか出た。

「WCF テスト クライアント」 には別に用はないため、閉じ。

ブラウザ (Chrom) を開いて…
URLは、
http://localhost:49652/WcfTestService.svc/HelloWorld?name=YAMADA と入力…

 

 

幸先よさそうだ。

 

■[?name=YAMADA] がかっこ悪い

クエリ文字列は今ではかっこ悪い?ので、[HelloWorld/YAMADA] で呼べるようにしてみる。

プログラムを変更してみる

[WcfTestService.svc WcfTestService.HelloWorld メソッド]

[OperationContract]
[WebGet(ResponseFormat=WebMessageFormat.Json, UriTemplate="HelloWorld/{name}")]
public string HelloWorld(string name) {
    return "こんにちは、" + name + "さん。";
}

そして実行→ブラウザでアクセス (
http://localhost:49652/WcfTestService.svc/HelloWorld/YAMADA)

 

 

シボンヌ。

エラーメッセージ的に、エンドポイントの設定を変えてあげればいいようだ。
ということで、いろいろと調べてみて、Web.config を編集してみた。

[Web.config]

<behavior name="WcfTestService.WcfTestServiceAspNetAjaxBehavior">
  <!-- UriTemplate を使用する場合、<enableWebScript/>ではなく、<webHttp/>に変更するみたい -->
  <!-- 削除 -->
  <!-- <enableWebScript /> -->
  <!-- 追加 -->
  <webHttp />
</behavior>

そして実行…

 

 

やばい…調子よすぎる…

 

■[.svc] がかっこ悪いです。

URLに 「~.svc/~」 の .svc が非常にかっこ悪いし気持ち悪い。

これを何とかするために試行錯誤…

 

・Web.config の endpoint をいじってみる

<endpoint address="" をいじってみたが、どうやってもできなかった…

 

・サービスを起動するプログラムを作る

エンドポイントを指定して起動させるといいみたい…メンドクサイ…
なので、試しませんでした。

 

・MVCなどのルーティングを使用してみる

Global.asax で、RouteTable なんたらかんたらと記述すると、できるようだ。
(最近ASPで使ってみた)

内容はこんな感じ。

[Global.asax]

protected void Application_Start(object sender, EventArgs e) {

    RouteTable.Routes.Add(
        new ServiceRoute("WcfTestService", new WebServiceHostFactory(), typeof(WcfTestService)));

}

 

そして起動…(http://localhost:49652/WcfTestService/HelloWorld/YAMADA)

 

 

これ超楽や…

 

今回はAzureに全く関係なかったけど、まあ結果でたからOK。



WCFわけわかんねぇ

clock April 28, 2011 21:33 by author Ito

こんばんは。

前回の続きで、今回は「WCFにjQueryでRESTな方法で住所を取得しよう」に挑戦…

 

できませんでした。

 

まずWCFがよくわからない。

とりあえずごく簡単なWCFサービスを作成はできた。
(サーバー側でWCF通信はできた)

内容はこんなの。

 

■WCFサービス

<IZipCodeSvc.cs>

[ServiceContract]
public interface IZipCodeSvc {

    [OperationContract]
    string GetAddress(string zipCode);
}

<ZipCodeSvc.svc.cs>

public class ZipCodeSvc : IZipCodeSvc {
        
    /// <summary>
    /// 郵便番号から住所を取得する
    /// </summary>
    /// <param name="zipCode"></param>
    /// <returns></returns>
    public string GetAddress(string zipCode) {

        CloudStorageAccount   storageAccount = null;
        CloudTableClient      tableClient    = null;
        ZipCodeServiceContext context        = null;

        string address = string.Empty;

        try {
            storageAccount = WCFServiceTest.AzureStorageAccount.GetStorageAccount();

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

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

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

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

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

        return address;
    }
}

<WCFServiceTest.AzureStorageAccount>

public class AzureStorageAccount {

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

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

        } catch {
            return null;

        }
    }
}

 

■Web側 (MVC)

<Controller>

public class ZipCodeController : Controller {

    //
    // GET/POST: /ZipCode/
    public ActionResult Index(string zipCode) {

        // サービスクライアント作成 (まずはローカル)
        WCFTestService.ZipCodeSvcClient svc
                = new WCFTestService.ZipCodeSvcClient(new BasicHttpBinding(),
                                                          new EndpointAddress("
http://127.0.0.1:81/ZipCodeSvc.svc"));

        // 住所を取得
        string address = svc.GetAddress(zipCode);

        ViewData["Title"] = Request.HttpMethod;
        ViewData["ZipCode"] = zipCode;
        ViewData["Address"] = address;

        return View();
    }
}

<View>

    <h2><%: ViewData["Title"] %></h2>
    <p>
        〒<%: ViewData["ZipCode"] %><br />
        <%: ViewData["Address"] %><br />
    </p>
    <% using ( Html.BeginForm() ) { %>
        <%: Html.TextBox("ZipCode") %>
        <input type="submit" value="検索" />
    <% } %>

 

そして実行…   

ただのWCFできた。

次、AJAX対応のWCFサービス作ろう…
というところで詰まった。

 

明日からの連休で、いろいろ試して、次回はちゃんとしたものを書けるようにします…

 



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

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 にはめられたか・・・

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

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



サービス作ってみたくなったんです

clock March 17, 2011 09:09 by author Ito

今回は、AzureでWebサービス作ってみよう。

 

ということで、まずはasmxファイルで作ってみる。

適当なWebロールにasmxファイルを追加して、以下を記述。

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
[ScriptService]  // スクリプトから呼び出す為の属性
public class WebService1 : System.Web.Services.WebService {

    [WebMethod]
    public string HelloWorld() {
        return "Hello World";
    }

    [WebMethod]
    [ScriptMethod(ResponseFormat=ResponseFormat.Json)]  // Json形式で返す
     public DateTime NowDate() {
        return DateTime.Now;
    }
}

 

勝手に作られるHelloWorldと、ただ現在時刻をJSON形式で返すだけのサービス・・・

 

これを呼び出す側を作ってみる。(MVCで作った)

[HandleError]
public class HomeController : Controller {

    public ActionResult Index() {

        WebService1 service = new WebService1();

        ViewData["Message"] = service.HelloWorld();

        return View();
    }

}

 

勝手に作られるコードに毛が生えた!

 

そして実行・・・

 

よしできた。終了。

 

・・・

 

あまりにも内容が薄いので、もう少し毛を生やします・・・

 

WebサービスからjQueryのAjaxで現在時刻を取得・表示してみる。

先ほど作った呼び出し側のページ(MVCビュー)に時刻を表示する部分を追加。

<p>現在の時刻:<span id="now_date"></span></p>

 

そして、Ajax取得するためのスクリプト追加。

<script type="text/javascript">

    $(document).ready(function() {
        setInterval(function() {
            $.ajax({
                url: "WebService1.asmx/NowDate",
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                type: "POST",
                success: AjaxSuccess
            });
        }, 1000);
    });

    function AjaxSuccess(data, dataType) {
        $("#now_date").text(ConvertMSJsonDate(data).toString());
    }

    // ASP.net WebサービスからJSONで返されたDateTime型を
    // javascriptのDate型に変換
    function ConvertMSJsonDate(jsonDate) {
        jsonDate.d.match(/\/Date\((.*?)\)\//g);
        return new Date(parseInt(RegExp.$1));
    }

</script>

 

実行・・・

できた!

 

豆知識

jQueryで asmx Webサービス

jQuery.ajaxのurlパラメータに、asmxファイルのUrlを指定し、
/の後に、サービスのメソッド名を記述するようだ。
サービスに渡す引数は、dateパラメータで指定する。

DateTimeの罠

ASP.netのWebサービスからDateTime型をJSONで取得すると、
  { "d": "/Date(1300323898991)/"}
というオブジェクトが返ります。

こいつをjavascriptのDate型に変換するには、"/Date(1300323898991)/"の数値を切り出し、
Date型のコンストラクタに指定してあげないといけないみたい。

 

あれ?今回の内容、Azure関係ないような気がする・・・



サーバー監視の第一歩

clock March 3, 2011 21:37 by author Ito

 さて、今回はふと思った事 「azureのリソース状況(CPU使用率とか)でも取得してみようかな」 でもやってみよう。

まずは各種リソースの取得方法を調べてみる。
…… PerformanceCounter 使えばいいみたい。

で、次はおもむろに WorkerRole を作成し、Runメソッドに直で書いてみる。

public override void Run() {

    while ( true ) {
        PerformanceCounter processorTimeCounter  = new PerformanceCounter("Processor", "% Processor Time", "_Total", ".");
        PerformanceCounter availableBytesCounter = new PerformanceCounter("Memory", "Available Bytes");
        PerformanceCounter committedBytesCounter = new PerformanceCounter("Memory", "Committed Bytes");

        Trace.WriteLine("CPU : " + processorTimeCounter.NextValue() + "%");
        Trace.WriteLine("Available MBytes : " + availableBytesCounter.NextValue() / 1024.0 / 1024.0 + "MBytes");
        Trace.WriteLine("Committed Bytes  : " + committedBytesCounter.NextValue() / 1024.0 / 1024.0 + "MBytes");

        Thread.Sleep(1000);
    }
}

そして実行 (デバッグ)

CPU : 0%
Available MBytes : 780.40234375MBytes
Committed Bytes  : 4535.29296875MBytes
CPU : 0%
Available MBytes : 783.65625MBytes
Committed Bytes  : 4531.33984375MBytes

"CPU : 0%" ってなんですか?

 

Azureだからだめなのかと思い、普通のコンソールプロジェクトでやってみたが同じ結果…

仕方ないので毎度のGoogle先生に伺ってみたが、なかなか返事が返ってこない…

そしてついに見つけた

カウンタの計算される値が 2 回のカウンタ読み取りに依存する場合、最初の読み取りでは 0.0 が返されます。
          http://msdn.microsoft.com/ja-jp/library/system.diagnostics.performancecounter.nextvalue%28v=VS.80%29.aspx
                                                       MSDN : PerformanceCounter.NextValue メソッド より

CPU使用率は上記に該当する模様。

 

な、なんだってー!

 

ということで、プログラム書きなおしてみた。

public override void Run() {

    // ループの外に出しただけ
    PerformanceCounter processorTimeCounter  = new PerformanceCounter("Processor", "% Processor Time", "_Total", ".");
    PerformanceCounter availableBytesCounter = new PerformanceCounter("Memory", "Available Bytes");
    PerformanceCounter committedBytesCounter = new PerformanceCounter("Memory", "Committed Bytes");

    while ( true ) {
        Trace.WriteLine("CPU : " + processorTimeCounter.NextValue() + "%");
        Trace.WriteLine("Available MBytes : " + availableBytesCounter.NextValue() / 1024.0 / 1024.0 + "MBytes");
        Trace.WriteLine("Committed Bytes  : " + committedBytesCounter.NextValue() / 1024.0 / 1024.0 + "MBytes");

        Thread.Sleep(1000);
    }
}

実行

CPU : 10.32648%
Available MBytes : 830.2109375MBytes
Committed Bytes  : 4754.65234375MBytes
CPU : 14.0092%
Available MBytes : 832.2734375MBytes
Committed Bytes  : 4751.90234375MBytes

…………OK、できた。

あとはこれらデータをストレージテーブルにでも格納すればOK。

 

 

これって、このWorkerRoleだけのリソース情報だよな…
フロント部のWebRoleの情報が知りたいんだが…

つかえねぇ…



Sign In