そごうソフトウェア研究所

SOA、開発プロセス、ITアーキテクチャなどについて書いています。Twitterやってます@rsogo

Mule ESBのフロー定義中にでGroovyスクリプトでカスタムのロジックを埋め込む

そういえば、Salesforce.comによるMulesoftの買収が発表されましたね。今後、どのようになっていくのか注目したいと思います。

www.mulesoft.com

さて、今回はMuleのフロー定義にGroovyのスクリプトを埋め込んでみたいと思います。 これができるとMuleで予め用意されたメッセージの変換ルール関数以外の独自処理をスクリプトで行うことができます。 Groovy以外にもJavaScriptなどのスクリプトを呼び出せます。

フロー定義

全体像

HTTPのGETリクエストを受け取り、URLのPATHのlxに置き換えるという処理をしています。 処理は<scripting:transformer>の中でやっています。名前空間http://www.mulesoft.org/schema/mule/scriptingを使うためにhttp://www.mulesoft.org/schema/mule/scripting http://www.mulesoft.org/schema/mule/scripting/current/mule-scripting.xsd"を宣言に追加しています。

<mule
  xmlns="http://www.mulesoft.org/schema/mule/core"
  xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"
  xmlns:http="http://www.mulesoft.org/schema/mule/http"
  xmlns:scripting="http://www.mulesoft.org/schema/mule/scripting"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd
http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd
http://www.mulesoft.org/schema/mule/scripting http://www.mulesoft.org/schema/mule/scripting/current/mule-scripting.xsd">

  <http:listener-config name="HTTP_Listener_Configuration" host="0.0.0.0" port="8086" basePath="groovy" doc:name="HTTP Listener Configuration"/>

  <!-- GET -->
  <flow name="GetFlow">
    <http:listener config-ref="HTTP_Listener_Configuration" path="/*" doc:name="Recieve HTTP request" allowedMethods="GET">
      <http:response-builder reasonPhrase="#[flowVars['reason']]" statusCode="#[flowVars['statusCode']]"/>
    </http:listener>
    <logger doc:name="Log the payload" level="INFO" message="GET is called."/>

    <set-variable variableName="VarTableName"
                  value="#[message.inboundProperties['http.request.path']]"
                  doc:name="Variable"/>
                  
    <scripting:transformer name="stringReplaceWithParams">
      <scripting:script engine="groovy">
        <property key="oldStr" value="l" />
        <property key="newStr" value="x" />
        <scripting:text>
          return VarTableName.toString().replaceAll("$oldStr", "$newStr")
        </scripting:text>
      </scripting:script>
    </scripting:transformer>
  </flow>
</mule>

ステップバイステップで見ていきます。

HTTPリクエストのリッスン

8086ポートで、URLのベースのパスとして/groovyに来たリクエストを受け取る設定になっています。

<http:listener-config name="HTTP_Listener_Configuration" host="0.0.0.0" port="8086" basePath="groovy" doc:name="HTTP Listener Configuration"/>

GETメソッドで呼び出されたときに、下記のフローが動作します。

  <flow name="GetFlow">
    <http:listener config-ref="HTTP_Listener_Configuration" path="/*" doc:name="Recieve HTTP request" allowedMethods="GET">

URLパスの取り出し

HTTPリクエストのパス(ホスト名、ポート名は含まず、それ以降)を取り出し、VarTableNameという変数に入れています。

    <set-variable variableName="VarTableName"
                  value="#[message.inboundProperties['http.request.path']]"
                  doc:name="Variable"/>

Groovyのスクリプトで文字列操作

    <scripting:transformer name="stringReplaceWithParams">
      <!-- scriptのエンジンとして、groovyを指定 -->
      <scripting:script engine="groovy">
        <!-- 変数に値を設定 -->
        <property key="oldStr" value="l" />
        <property key="newStr" value="x" />
        <!-- 実際のGroovyのコード。replaceAllでURLパスの文字列を変換して、応答として返す -->
        <scripting:text>
          return VarTableName.toString().replaceAll("$oldStr", "$newStr")
        </scripting:text>
      </scripting:script>
    </scripting:transformer>

テストしてみましょう

リクエス

次のURLに対して、GETメソッドのリクエストを出します

http://localhost:8086/groovy/helloworld

レスポンス

URLのパス(/groovy/helloworld)のうち、lxに置換した結果が返ってきています。

/groovy/hexxoworxd

curlコマンドで確認したら、こんな感じ。

$ curl http://localhost:8086/groovy/helloworld
/groovy/hexxoworxd

うまくいきましたね。

AWS Lambdaの何が嬉しかったか。Excel生成WebサービスのLambda関数化

AWS Lambdaを使った開発をやってみて何が嬉しかったのかを書いておこうと思います。

Lambda関数化したのは以下のようなWebサービスでした

  • SPAなWebアプリからJSONでリクエストを受け取る
  • JavaExcelを扱うライブラリであるPOIでExcelのテンプレートファイルにJSONで受け取ったデータを埋め込む
  • Excelをクライアントに返す(ブラウザでダウンロードが始まる)

Excelを触る時点で、Web系の技術だけで完結するのは難しいので、 この部分だけJavaでやるというのは今後もあるんじゃないかなと思います。

で、次のような課題がありました

  • 普段は大丈夫だけど、たまにでかいデータが来て、Java側でOutOfMemoryが発生する
  • そもそもExcel作成リクエストはたまにしか実行されない(業務のひとまとまりの作業が終わって帳票を出したいときだけ)
  • なのに、でかいデータのためにそれなりのサイズのEC2を用意しておかないといけない

Lambda関数化したことで

  • 使うときのみリソースを割り当てるので、思い切ったリソースを割り当ててもコスト的には下がる。これは処理頻度が低いサービスでは大きいメリットだと思います
  • 副次的な効果として、これまで同じJavaアプリケーションサーバー上で実行していた他のアプリケーションがExcel作成WebサービスのOutOfMemoryの巻き添えを食っていたのですが、単独機能のサービスとして切り出されたことで、お互いに影響を受けることがなくなりました。マイクロサービス化ができたわけです。

他の用途ですと、Javaの立ち上がり時の遅さが気になったかもしれませんが、 今回のケースだと、そもそもExcelを生成するのに10秒くらいかかるような Webサービスだったので性能は気になりませんでした。

まとめ

AWS Batchも使ってみたいなーと思っているサービスで、 バッチという処理頻度が低い処理に対して、 実行されるときだけ大きいリソースを割り当てて(必要だったら並行でいっぱい立ち上げて)、 終わったら終了というのは非常にクラウドらしく、魅力的です。

もちろんサーバレスになることで、運用が楽になるか、スケールするとかいい事はあると思うのですが、 処理面での直接的な効果として頻度が低い処理に、思い切りリソースを割り当てられるというメリットがありました。

Lambda関数をAPI Gatewayで公開する(シンプル版)

先日、JavaでLambdaを作るエントリを書きましたが、今回、API Gatewayを使って、Lambdaで実装した処理をWebサービスとして公開したいと思います。

API GatewayはRESTfulなAPIを簡単に作れます。 RESTfulなので、まずはリソースを定義して、そのリソースに対するGET、POST、PUT、DELETEなどのメソッドを提供します。 後はどのバックエンドに接続するのかが、基本的な手順です。

手順はこちらをベースに進めます。

docs.aws.amazon.com

Lambda関数の作成

バックエンドはLambda関数を使います。 こちらで公開されているNode.jsのサンプルを、そのまま使って作成しておきました。

docs.aws.amazon.com

APIの作成

f:id:begirama:20180513002008p:plain

リソースの定義

最初にリソースを定義します。

f:id:begirama:20180513002455p:plain

  • リソース名: 任意の名前です。ここではproxyとしています
  • リソースパス: /proxy/{itemId}など個別のパターンを設定できますが、ここではワイルドカードになる{proxy+}を使います。これで、ルートのURL以下はなんでも受け取ります。

メソッドの定義

f:id:begirama:20180513002542p:plain アクションで、メソッドの作成を選びます。

f:id:begirama:20180513002702p:plain Lambda関数プロキシを選択します。 リージョンを選択して、Lambda関数名に何文字か入力すると、候補が表示されます。

f:id:begirama:20180515072400p:plain ここまでの状態はこんな感じ。

APIのデプロイ

f:id:begirama:20180513003023p:plain このままでは呼び出せないので、APIのデプロイを行います。

f:id:begirama:20180513003052p:plain ステージというのが設定できて、ステージごとに認証やスロットリングの設定を変更することができます。 例えば、社内用のAPIと、取引先用のAPIなんかで分けることもできそうです。

f:id:begirama:20180513003200p:plain

APIの呼び出し

デプロイが完了すると、こんな感じのURLが割り当てられます。

f:id:begirama:20180515070649p:plain

このまま呼び出すと、

{"message":"Missing Authentication Token"}

エラーが返ってきます。 エラーメッセージの内容がわかりにくいのですが、上のURLはルートのもので、子リソース(ここではproxy)が定義されている場合には、そのまま呼び出すとこのエラーが返ってきます。

そこで、https://dummy.execute-api.ap-northeast-1.amazonaws.com/dev/proxyのような子リソースを含むURLで呼び出すと、 欲しかった結果を得られます。

Install Node.js v8 and npm 5.6 for macOS High Sierra

以前、Node.js v5をMacにインストールする手順を書きましたが、MacBook Proを新調したので、新しくセットアップした手順をメモします。

begirama.hatenablog.com

公式サイト Node.js からMac用のpkgファイルをダウンロードします。2018/5/14時点で安定版の最新はv8.11.1でした。

pkgを実行して、Node.jsをインストールします。

セットアップ後のバージョンの確認は以下です。

$ node -v
v8.11.1

ついでにnpmのバージョンも確認します。

$ npm -v
5.6.0

npmを使って、gulpをインストールしてみます。

$ sudo npm install --global gulp-cli
Password:
/usr/local/bin/gulp -> /usr/local/lib/node_modules/gulp-cli/bin/gulp.js
+ gulp-cli@2.0.1
added 238 packages in 5.825s

bowerも入れてみます。

$ sudo npm install -g bower
Password:
/usr/local/bin/bower -> /usr/local/lib/node_modules/bower/bin/bower
+ bower@1.8.4
added 1 package in 2.983s


   ╭─────────────────────────────────────╮
   │                                     │
   │   Update available 5.6.0 → 6.0.1    │
   │     Run npm i -g npm to update      │
   │                                     │
   ╰─────────────────────────────────────╯

正常に動作していそうですね。

JavaでAWS Lambdaの実装をやってみる

各種情報

  • 開発者ガイド docs.aws.amazon.com

  • AWS Lambdaには設計書と呼ばれるテンプレートが用意されていて、テンプレートをカスタマイズすることでいろいろな機能を試すことができます f:id:begirama:20180415104106p:plain

チュートリアル

チュートリアルへのリンクが公開されています。 aws.amazon.com

開発者ドキュメントの中に含まれるチュートリアルdocs.aws.amazon.com

テストと実行結果の確認

Jarのデプロイ

Jarファイルをアップロードすることでデプロイができます。 デプロイ時に必要なのは次の項目の設定です。 - Zip/Jarファイルの直接アップロードか、S3からのアップロードか - ランタイム - Java8を指定します - ハンドラー - エントリポイントになるメソッドの{パッケージ名}.{クラス名}::{メソッド名}の形式で指定します(例: example.Hello::handleRequest

デプロイして実行時にエラーになった場合には、コンソールにエラーが出力されます。 これはLambdaはJavaのランタイムとして、Java8がサポートされているが、ビルド時にJava9でコンパイルしてしまったためエラーが発生しています。 f:id:begirama:20180415084134p:plain

テストと実行結果の確認

テストの設定を行って、テストを実行します。入力イベントを設定する前に試してみることができます。

  • テストの設定 f:id:begirama:20180415103536p:plain

  • テスト結果の確認 f:id:begirama:20180415103452p:plain

エラー時

f:id:begirama:20180415091308p:plain

CouldWatchにもエラーのログが出力されています。

f:id:begirama:20180415093108p:plain

Apache ActiveMQ ArtemisのLibrary artemis-native-64 not foundメッセージは問題あるのか調査

AppPot - 企業向けスマートデバイスアプリ開発のためのプラットフォーム」の内部では、Apache ActiveMQ Artemisを使用しています。 下記のようなメッセージが出るので調査をしてみました。

DEBUG [org.apache.activemq.artemis.jlibaio] (MSC service thread 1-2) Library artemis-native-64 not found!
DEBUG [org.apache.activemq.artemis.jlibaio] (MSC service thread 1-2) artemis-native-32 -> error loading the native library: java.lang.UnsatisfiedLinkError: no artemis-native-32 in java.library.path
        at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
        at java.lang.Runtime.loadLibrary0(Runtime.java:870)
        at java.lang.System.loadLibrary(System.java:1122)
        at org.apache.activemq.artemis.jlibaio.LibaioContext.loadLibrary(LibaioContext.java:68)
        at org.apache.activemq.artemis.jlibaio.LibaioContext.<clinit>(LibaioContext.java:88)
        at org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory.isSupported(AIOSequentialFileFactory.java:107)
        at org.wildfly.extension.messaging.activemq.ActiveMQServerService.start(ActiveMQServerService.java:155)
        at org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1963)
        at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1896)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

DEBUG [org.apache.activemq.artemis.jlibaio] (MSC service thread 1-2) Library artemis-native-32 not found!
DEBUG [org.apache.activemq.artemis.jlibaio] (MSC service thread 1-2) Couldn't locate LibAIO Wrapper
DEBUG [org.jgroups.protocols.FD_SOCK] (FD_SOCK pinger,swarm-jgroups,amapawsmbw02) amapawsmbw02: ping_dest is ip-10-236-2-198, pingable_mbrs=[ip-10-236-2-198, amapawsmbw02]
INFO  [org.wildfly.extension.messaging-activemq] (MSC service thread 1-2) WFLYMSGAMQ0075: AIO wasn't located on this platform, it will fall back to using pure Java NIO. Your platform is Linux, install LibAIO to enable the AIO journal and achieve optimal performance.

Apache ActiveMQ ArtemisのPersistenceのドキュメントに依ると、Java NIOとLinuxのネイティブの非同期IOライブラリ ( libaio ) を使う2つの実装が用意されているようです。

Persistence | ActiveMQ Artemis Documentation

libaioについては、更にこちらのドキュメントで、詳細に記述されています。

Libaio Native Libraries | ActiveMQ Artemis Documentation

libActiveMQAIO32.so または libActiveMQAIO64.so でlibaioを使うのですが、これがライブラリのパスに見つからないというメッセージがDEBUGレベルで出ています。 そのため、Java NIOを使いますよ、LibAIOを使ったほうが性能が良くなりますよ、というログがでているみたいですね。

動作させる上では問題なさそうですが、性能を向上させるためには対応したほうが良さそうです。

普通のWildFly10にはlibartemis-native-64.soが含まれているのを確認しましたが、 今このログがでているのはWildfly Swarmで実装しているアプリケーションででているので、 Wildfly Swarmのパッケージングを調査しようと思います。

EXPLAINによるMySQLの実行計画の確認

担当しているソフトウェアで、運用しているとMySQL内のレコードが増えて性能が悪化するというお問い合わせをいただきました。 そこで、Indexを改善しようということになったのですが、どこにIndexを付けるべきか確認するためにMySQLの実行計画を確認しました。

自分のバックグラウンドとしては、データベースは特別詳しいわけではないので、 Indexは論理的に付けてはいるものの、それがあっているのかどうか確証はありませんでした。

で、調べたところMySQLでも実行計画が確認できるみたいなので、今回、それを使って確認を行ってみました。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 8.8 クエリー実行プランの理解

対象のテーブル

こんなテーブルです。ユーザーのセッション情報を格納するためのテーブルで、ログアウトされないと、レコードが蓄積されていきます。

MySQL [apppot]> desc UserSession;
+-----------------+--------------+------+-----+---------+-------+
| Field           | Type         | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+-------+
| userToken       | varchar(255) | NO   | PRI | NULL    |       |
| appTableId      | bigint(20)   | YES  |     | NULL    |       |
| companyId       | bigint(20)   | YES  |     | NULL    |       |
| deviceUDID      | varchar(255) | YES  |     | NULL    |       |
| loginDate       | datetime     | YES  |     | NULL    |       |
| tokenExpireDate | datetime     | YES  |     | NULL    |       |
| userId          | bigint(20)   | YES  |     | NULL    |       |
+-----------------+--------------+------+-----+---------+-------+
7 rows in set (0.05 sec)

こんな感じのSQLが実行されます。

select * from UserSession 
where userId=205 
and appTableId=4 
and deviceUDID='apppotsdkjs'
 and tokenExpireDate>='2018-01-12 22:16:13';

MySQLの実行計画の確認は手軽で、EXPLAINを頭に付けるだけでOKです。

Index追加前

rowsに122381という値が入っていますが、いわゆるフルスキャンが発生して、全部のレコードをチェックしていることがわかりました。そりゃ遅いですね。

MySQL [apppot]> EXPLAIN select * from UserSession where userId=205 and appTableId=4 and deviceUDID='apppotsdkjs' and tokenExpireDate>='2018-01-12 22:16:13';
+----+-------------+-------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table       | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-------------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | UserSession | ALL  | NULL          | NULL | NULL    | NULL | 122381 | Using where |
+----+-------------+-------------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.01 sec)

userIdにIndexを追加してみる

試しにuserIdにIndexを追加してみます。 ログインするユーザーが十分バラけていれば、これでも早くなりそうな感じがします。

ALTER TABLE UserSession 
ADD INDEX index_userId(userId);

実行計画はこんな感じ。チェックする対象のrowsの値が1/4になってますね。

MySQL [apppot]> EXPLAIN select * from UserSession where userId=205 and appTableId=4 and deviceUDID='apppotsdkjs' and tokenExpireDate>='2018-01-12 22:16:13';
+----+-------------+-------------+------+---------------+--------------+---------+-------+-------+-------------+
| id | select_type | table       | type | possible_keys | key          | key_len | ref   | rows  | Extra       |
+----+-------------+-------------+------+---------------+--------------+---------+-------+-------+-------------+
|  1 | SIMPLE      | UserSession | ref  | index_userId  | index_userId | 9       | const | 33436 | Using where |
+----+-------------+-------------+------+---------------+--------------+---------+-------+-------+-------------+
1 row in set (0.00 sec)

複数カラムのIndex

MySQLは複数のIndexあっても、どれか1つしか使ってくれません。 そこで、複数のカラムで1つのIndexを作ることができます。遅いSQLの条件で使っているカラムの組み合わせてIndexを作ります。

ALTER TABLE UserSession 
ADD INDEX index_get_session(userId, appTableId, deviceUDID, tokenExpireDate);

実行計画を見てみましょう。rowsの値が3桁減ってますね。 possible_keysが使用可能なIndex、keyが実際に使われたIndexということのようです。 新しく付けたindex_get_sessionが使われてますね。

MySQL [apppot]> EXPLAIN select * from UserSession where userId=205 and appTableId=4 and deviceUDID='apppotsdkjs' and tokenExpireDate>='2018-01-12 22:16:13';
+----+-------------+-------------+-------+--------------------------------+-------------------+---------+------+------+-----------------------+
| id | select_type | table       | type  | possible_keys                  | key               | key_len | ref  | rows | Extra                 |
+----+-------------+-------------+-------+--------------------------------+-------------------+---------+------+------+-----------------------+
|  1 | SIMPLE      | UserSession | range | index_userId,index_get_session | index_get_session | 792     | NULL |  258 | Using index condition |
+----+-------------+-------------+-------+--------------------------------+-------------------+---------+------+------+-----------------------+
1 row in set (0.08 sec)

以上です。