
本記事は、ECP接続について紹介します。
はじめに
本記事は、Enterprise Cache Protocol(以降ECP)について解説します。
ECPは、複数のIRIS/Cacheインスタンス間でデータを共有し、システムのスケーラビリティとパフォーマンスを向上させる技術です。
つまり「サーバのスペックを上げて単一サーバ運用を行うよりも、サーバを分散する事により1つ1つのサーバスペックを抑えて、全体的な経費を抑えよう!」
って事です。
では、ECPの構成、設定方法等を解説していきます。
ECPの構成
ECPの構成は、「データサーバ」と「アプリケーションサーバ」になります。
お互いの関係性は下記になります。
- データベースサーバ → データベース(DB)の本体がある
- アプリケーションサーバ → データベースサーバにあるDBを参照する(リモートデータベース)
下記図であれば、各「アプリケーションサーバ」は、自前のDB(A)と「データサーバ」にあるDB(B)のグローバルを操作する事ができます。
また、DBキャッシュは、各アプリケーションサーバが受け持つ事になります。
→ データサーバ側もDBキャッシュは必用になります(多めに)。
ジャーナルファイルに関しては、各データベースが配置している端末に出力される事になります。
→ データベース(B)に関しては、アプリケーションサーバ側の操作もデータサーバに出力されます。

このように、データベースサーバのDB(B)を各アプリケーションサーバが参照する事で、データサーバの負荷を下げる事ができます。
ハイスペックサーバを用意しなくても良いのは、メリットですよね。
ECP設定方法
データサーバ側の設定
サービスを有効にする
管理ポータルを起動し、[システム管理] > [セキュリティ] > [サービス]をクリックし、サービス画面を起動します。

一覧の中に「%Service_ECP」があるのでクリックし、サービス編集画面を起動させます。
画面が起動すると、下記操作を行います。

正常に登録されると「有効=はい」と表示されます。
※許可済みの接続元にIPを追加すると「許可された接続」に設定したIPが表示されます。

ECP用のDBを作成する
今回は、「DB名=ECPSAMPLE」として、新規DBを「D:\IRISDB\ECPSAMPLE」に作成しました。
ネームスペースは特に作成していません。
データ参照用として、下記グローバルを設定します。
zn "^^D:\IRISDB\ECPSAMPLE"
s ^ECPData = "ECP用のデータです"
s ^ECPData(1) = "ECP用のデータです-1"
他DBへのアクセス方法は下記記事を参照してください。
データサーバとしての設定
データサーバの設定を行います。
管理ポータルの[システム管理] > [構成] > [接続] > [ECP設定]をクリックし、ECP設定画面を起動します。
データサーバ側の設定なので、赤枠の中を設定します。
この際、「ECPサービスは有効になっています」と表記されている事を確認してください。
複数のアプリケーションサーバの接続を行う場合は、「アプリケーションサーバの最大数」を変更して、「保存」ボタンをクリックしてください。
※変更時はインスタンスの再起動が必要です。

接続の状態を確認する
時系列的には、アプリケーションサーバ側の設定が完了した後になります。
アプリケーションサーバ側からの接続状況を確認します。
ECP設定画面の「アプリケーションサーバ」をクリックします。

「ステータス」が正常と表示されていれば、問題ありません。
アプリケーションサーバ側の設定
アプリケーションサーバ側の設定としては、下記4点があります。
- アプリケーションサーバとしての設定
- データサーバとのECP接続設定
- リモート・データベースの設定
- リモート・データベースの運用
では、設定方法を解説していきます。
アプリケーションサーバとしての設定
アプリケーションサーバ側の管理ポータルを起動し、[システム管理] > [構成] > [接続] > [ECP設定]をクリックし、ECP設定画面を表示します。
アプリケーションサーバとして設定するため、赤枠の中の項目を必用に応じて変更し、「保存」ボタンをクリックします。

データサーバへの接続設定
データサーバへの接続を行うため、ECP設定画面で「データサーバ」ボタンをクリックします。
① ECPデータ・サーバ画面で「サーバを追加」ボタンをクリックし、ECPデータサーバ設定画面を起動させます。
② 「サーバ名」「ホストDNS名またはIPアドレス」「IPポート」は必須入力になります。
その他は任意の設定になります。
③ 設定が完了したら「保存」ボタンをクリックします。

設定した内容が一覧に表示されます。

リモート・データベースとしての設定
ECPの接続設定が完了したら、リモート・データベースの設定を行います。
管理ポータルの[システム管理] > [構成] > [システム構成] > [リモートデータベース]をクリックし、リモートデータベース画面を起動させます。
① リモートデータベース画面で「リモートデータベースを作成」ボタンをクリックし、リモートデータベースを作成画面を起動させます。
② 「リモート・サーバ」「ディレクトリ」「データベース名(名称は任意)」は必須入力になります。
その他は任意の設定になります。
③ 設定が完了したら「保存」ボタンをクリックします。

設定した内容が一覧に表示されます。

リモート・データベースの運用
アプリケーションサーバ側から、データサーバ側のデータベースが参照できるようになりましたが、このままでは使いにくいため、下記2点の運用を想定します。
- 既存ネームスペースに対し、グローバルマッピングを行う
- 新規ネームスペースのグローバル・デフォルトデータベースに設定する
今回は、1.のグローバルマッピングで運用したいと思います。

ECP接続は以上で完了になります。
次は、動作確認を行いたいと思います。
動作確認
グローバルの制御
グローバルの取得
先ずは、データサーバ側のグローバルを参照してみます。
ターミナルを起動し、下記コマンドを実行してグローバルの値を確認します。
zn "sample"
zw ^ECPData
実行結果が下記になります。
データサーバ側のグローバルを参照できることが確認できます。

グローバルの入力
グローバルの書き込みを試します。
下記コマンドをターミナルで入力します。
s ^ECPData(3) = "追加のグローバル設定"
これをデータサーバ側の管理ポータルで確認してみます。

先ほどのコマンドが追加されている事が確認できます。
ジャーナルの確認
先ほど入力したコマンドのジャーナルを確認します。
アプリケーション側で実行したグローバル入力も、リモートデータベースの配置しているデータサーバ側に出力されます。
データサーバ側の管理ポータルを開き、ジャーナルを確認します。

データサーバ側にジャーナルレコードが出力されている事が確認できました。
グローバル・キャッシュの確認
ECPの目的の1つにスケーラビリティを謳っています。
そこで、グローバルの操作を行った際、グローバル・バッファ(データベース・キャッシュ)がアプリケーションサーバ側にもある事を確認したいと思います。
下記操作を行い、グローバル・バッファに「^ECPData」が使用されているか確認します。
f pos=4:1:10000 s ^ECPData(pos)="グローバル・バッファの確認"
f pos=1:1:10000 s data = $g(^ECPData(pos))
zn "%SYS"
d ^GLOBUFF
上記コマンドでは、約2.8%しか使用していませんが、確かにアプリケーションサーバ側でキャッシュされている事を確認しました。

データサーバ側も、制御用(恐らく)としてキャッシュされます。

データサーバ側の方が多い
コーディング上の注意点
下記は、ECP接続を行う際のコーディング上の注意点になります。
※公式ドキュメントより抜粋
詳細が知りたい場合は、ドキュメントを参照して下さい。
速度検証
次は、ECPの速度を検証してみたいと思います。
とは言え、処理速度に関しては、ECPの複雑なロック管理や回線速度等々の影響が大きいので、参考程度にして下さい。
グローバル書き込み
一端、グローバル「^ECPData」を削除し、IRISを再起動してグローバル・バッファをクリアします。
書き込み速度を検証するため、下記をターミナルに張り付けて実行します。
Check(cnt) ;
s start = $zh
f pos=1:1:cnt {
s ^ECPData(pos) = "速度検証"
}
w !,"ECPタイム:",$zh-start
s start = $zh
f pos=1:1:cnt {
s ^DefData(pos) = "速度検証"
}
w !,"デフォルトタイム:",$zh-start
d Check(1000000)
結果を確認してみます。

あー、うん。
分かっていましたが、やっぱりちょっと遅いですね。
100万件のデータ登録になるので、1つ1つのSET文に関してはそこまで差があるわけではありません。
ただ、塵も積もになるのも事実です。
速度が命のシステムでは、ECP接続の運用は向いていないようです。
グローバル読み込み
再度IRISを再起動し、グローバル・バッファを削除した後、下記コマンドを実行します。
Check ;
s start = $zh
s cnt = ""
f { s cnt = $o(^ECPData(cnt),1,data) q:cnt="" }
w !,"ECPタイム:",$zh-start
s start = $zh
s cnt = ""
f { s cnt = $o(^DefData(cnt),1,data) q:cnt="" }
w !,"デフォルトタイム:",$zh-start
d Check
結果を確認してみます。

以外に差がありません。
データの取得だけであれば、割と優秀な感じがします。
オブジェクト登録
最後にオブジェクトの登録を検証します。
オブジェクトは、グローバルとインデックスの登録が行われます。
また、登録時はレコードのロックも行われるため、1回の%Save()で複数の処理が実行される事になります。
オブジェクトの登録がECP環境下で正常に動作するか、また速度はどの程度か確認したいと思います。
検証用として、下記データクラスを用意しまいした。
プロパティ数:16、インデックス数:11 になります。
Class developer.data.Patient Extends %Persistent
{
Index unique On (patientId, updateCount) [ PrimaryKey ];
Index i1 On patientId [ Data = (漢字氏名, カナ氏名, 生年月日) ];
Index i2 On (漢字氏名, カナ氏名, 生年月日);
Index a1 On (dele, patientId) [ Data = 未使用区分 ];
Index p1 On 漢字氏名;
Index p2 On カナ氏名;
Index p3 On 生年月日;
Index p4 On dele;
Index p5 On updateCount;
Index p6 On ABO血液型;
Index p7 On RH血液型;
/// 更新通番。
Property updateCount As %Integer [ InitialExpression = 1, Required ];
/// 削除フラグ
Property dele As %Boolean [ InitialExpression = 0, Required ];
Property patientId As %String [ Required ];
// 基本情報 -----------------------------
/// 姓_全角スペース_名
Property 漢字氏名 As %String;
/// 姓_半角スペース_名
Property カナ氏名 As %String;
/// 姓_半角スペース_名
Property ローマ字氏名 As %String;
Property 漢字旧姓 As %String;
Property カナ旧姓 As %String;
Property 性別 As %String;
/// YYYYMMDD形式
Property 生年月日 As %Date;
/// YYYYMMDDhhmmss形式
Property 死亡日時 As %DateTime;
Property コメント As %String;
/// YYYYMMDD形式
Property 新患登録日 As %Date;
// 付加情報 -----------------------------
Property ABO血液型 As %String;
Property RH血液型 As %String;
Property 未使用区分 As %Boolean;
Storage Default
{
<Data name="PatientDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>updateCount</Value>
</Value>
<Value name="3">
<Value>dele</Value>
</Value>
<Value name="4">
<Value>patientId</Value>
</Value>
<Value name="5">
<Value>漢字氏名</Value>
</Value>
<Value name="6">
<Value>カナ氏名</Value>
</Value>
<Value name="7">
<Value>ローマ字氏名</Value>
</Value>
<Value name="8">
<Value>漢字旧姓</Value>
</Value>
<Value name="9">
<Value>カナ旧姓</Value>
</Value>
<Value name="10">
<Value>性別</Value>
</Value>
<Value name="11">
<Value>生年月日</Value>
</Value>
<Value name="12">
<Value>死亡日時</Value>
</Value>
<Value name="13">
<Value>コメント</Value>
</Value>
<Value name="14">
<Value>新患登録日</Value>
</Value>
<Value name="15">
<Value>ABO血液型</Value>
</Value>
<Value name="16">
<Value>RH血液型</Value>
</Value>
<Value name="17">
<Value>未使用区分</Value>
</Value>
</Data>
<DataLocation>^developer.data.PatientD</DataLocation>
<DefaultData>PatientDefaultData</DefaultData>
<ExtentSize>1000449</ExtentSize>
<IdLocation>^developer.data.PatientD</IdLocation>
<IndexLocation>^developer.data.PatientI</IndexLocation>
<StreamLocation>^developer.data.PatientS</StreamLocation>
<Type>%Storage.Persistent</Type>
}
}
登録する関数は下記に用意しました。
ClassMethod saveObject(cnt As %Integer)
{
try {
s start = $zh
f pos=1:1:cnt {
s obj = ##class(developer.data.Patient).%New()
s obj.patientId = $tr($j(pos, 10)," ", "0")
s obj.updateCount = 1
s obj.dele = 0
s obj.漢字氏名 = "日本 太郎"_pos
s obj.カナ氏名 = "ニホン タロウ"_pos
s obj.ローマ字氏名 = "taroh nihon"_pos
s obj.漢字旧姓 = ""
s obj.カナ旧姓 = ""
s obj.性別 = "男"
s obj.生年月日 = $zdh("2000-01-01",3)
s obj.死亡日時 = ""
s obj.コメント = "コメント"
s obj.新患登録日 = $zdh("2025-01-01",3)
s obj.ABO血液型 = "A"
s obj.RH血液型 = "+"
s obj.未使用区分 = ""
$$$ThrowOnError( obj.%Save() )
}
w !, "処理時間:",$zh-start
} catch e {
w !,$system.Status.DisplayError(e.AsStatus())
}
}
では実行してみます。

9.7時間・・・・!?
おっっっっっっっっそ!!
グローバルマッピングを解除して、ローカルデータベースで処理速度を確認してみます。

あ、うん。
ですよね。
この結果から見ると、ECP接続はオブジェクトの登録に全く向いていないですね。
むしろシステムの性質によっては致命傷になりかねないです。
オブジェクトの読み込み
先ほどオブジェクトの登録を行ったので、今度はオブジェクトの読み込みを検証したいと思います。
読み込み用の関数は下記になります。
ClassMethod loadObject(cnt As %Integer)
{
try {
s start = $zh
f pos=1:1:cnt {
s obj = ##class(developer.data.Patient).%OpenId(pos)
s name = obj.漢字氏名
, ptnId = obj.patientId
}
w !, "処理時間:",$zh-start
} catch e {
w !,$system.Status.DisplayError(e.AsStatus())
}
}
では実行してみます。

グローバルマッピングを外してローカルデータベースで試します。

良かった・・・
両者にそこまでの差は無いようです。
各検証項目のサマリ
各処理時間を下記にまとめました。
■注意事項
・各項目毎にサンプリング回数が1回なので、参考値程度で捉えてください。
・各項目100万件のデータを対象としています。
・データサーバ側のスペック検証も兼ねて、同じ処理を実行しています。
検証項目 | APサーバ ローカルDB | APサーバ リモートDB | データサーバ ローカルDB |
---|---|---|---|
書き込み(SET) | 0.91 | 16.16 | 1.19 |
書き込み(Lock付) | 8.37 | 10831.14(3時間) | 8.69 |
読み込み($ORDER) | 1.08 | 3.18 | 1.73 |
読み込み(未定義NGパターン) | 0.36 | 10980.86(3時間) | 0.287 |
読み込み(未定義OKパターン) | 0.36 | 0.85 | 0.39 |
オブジェクト登録(%Save) | 215.24 | 34875.79(9.7時間) | 172.39 |
オブジェクト取得(%OpenId) | 65.60 | 71.40 | 65.62 |
SQL(select文) | 39.17 | 40.25 | 33.97 |
SQL(insert文) | 469.21 | 40228.96(11.2時間) | 328.86 |
ストリームフィールドあり SQL(select文) | 482.17 | 11719.23(3.3時間) | 944.57 |
ストリームフィールドあり オブジェクト取得(%OpenId) | 189.33 | 11188.94(3.1時間) | 160.91 |
$Increment | 0.34 | 13381.09(3.7時間) | 0.60 |
$Sequence | 0.24 | 13.85 | 0.23 |
※APサーバ:アプリケーションサーバ
※単位=秒
この比較を見ると、ロックを行った処理(オブジェクト登録含む)や未定義グローバルの参照に関しては、極端に遅い事が分ります。
※やはりデータサーバへ接続が発生すると遅くなります。
ただし、極端に遅いと言っても、1レコード辺りの処理速度は0.03~0.04秒なので、ガンガン登録を行うようなシステムでなければ、そこまで気にしなくても良いかもしれません。
システム構築時は、よくご検討下さい。
サンプル処理(参考程度)
SET文(ロック付き)
Check(cnt) ;
s start = $zh
f pos=1:1:cnt {
l +^ECPData
s ^ECPData(pos) = "速度検証"
l -^ECPData
}
w !,"ECPタイム:",$zh-start
s start = $zh
f pos=1:1:cnt {
l +^DefData
s ^DefData(pos) = "速度検証"
l -^DefData
}
w !,"デフォルトタイム:",$zh-start
d Check(1000000)
サンプル(グローバル取得 未定義のグローバルNGパターン)
親グローバルが無く、子ノードを参照するパターン
→グローバル・バッファが作成されないため、データサーバへのアクセスが毎回発生する
// 事前にk ^ECPData, ^DefDataを実施してIRIS再起動を行う
Check(cnt) ;
s start = $zh
f pos=1:1:cnt s txt = $g(^ECPData(pos))
w !,"ECPタイム:",$zh-start
s start = $zh
f pos=1:1:cnt s txt = $g(^DefData(pos))
w !,"デフォルトタイム:",$zh-start
d Check(1000000)
サンプル(グローバル取得 未定義のグローバルOKパターン)
親グローバルがあり、子ノードを参照するパターン
→グローバル・バッファが作成されるため、データサーバへのアクセスは発生しない
// 事前にs (^ECPData, ^DefData)=1を実施してIRIS再起動を行う
Check(cnt) ;
s start = $zh
f pos=1:1:cnt s txt = $g(^ECPData(pos))
w !,"ECPタイム:",$zh-start
s start = $zh
f pos=1:1:cnt s txt = $g(^DefData(pos))
w !,"デフォルトタイム:",$zh-start
d Check(1000000)
サンプル(SQL select文)
ClassMethod readSQL()
{
try {
s start = $zh
s rset = ##class(%SQL.Statement).%ExecDirect(,"select * from developer_data.Patient")
while( rset.%Next() ){
s name = rset.漢字氏名
, ptnId = rset.patientId
}
w !, "処理時間:",$zh-start
} catch e {
w !,$system.Status.DisplayError(e.AsStatus())
}
}
サンプル(SQL insert文)
ClassMethod insertSQL(cnt As %Integer)
{
try {
s start = $zh
s stmt = ##class(%SQL.Statement).%New()
f pos=1:1:cnt {
s query = 4
s query(1) = "INSERT INTO developer_data.Patient SET"
s query(2) = "patientId=?,updateCount=1,dele=0,漢字氏名=?,カナ氏名=?,ローマ字氏名=?,漢字旧姓='',"
s query(3) = "カナ旧姓='',性別='男',生年月日=?,死亡日時=null,コメント='コメント',"
s query(4) = "新患登録日=?,ABO血液型='A',RH血液型='+',未使用区分=0"
s sts = stmt.%Prepare(.query)
s rset = stmt.%Execute($tr($j(pos, 10)," ", "0"),"日本 太郎"_pos,"ニホン タロウ"_pos,"taroh nihon"_pos,$zdh("2000-01-01",3),$zdh("2025-01-01",3))
}
w !, "処理時間:",$zh-start
} catch e {
w !,$system.Status.DisplayError(e.AsStatus())
}
}
ストリームフィールドあり
コメントフィールドのデータ型を「%Stream.GlobalCharacter」に変更しました。
Property コメント As %Stream.GlobalCharacter;
SQL(select文)
ClassMethod readSQL2(cnt As %Integer)
{
try {
s start = $zh
f id=1:1:cnt {
s rset = ##class(%SQL.Statement).%ExecDirect(,"select * from developer_data.Patient where id=?", id)
while( rset.%Next() ){
s name = rset.漢字氏名
, ptnId = rset.patientId
s cmdObj = ##class(%Stream.GlobalCharacter).%Open(rset.コメント)
s cmt = cmdObj.Read()
}
}
w !, "処理時間:",$zh-start
} catch e {
w !,$system.Status.DisplayError(e.AsStatus())
}
}
オブジェクト(%OpenId)
ClassMethod loadObject(cnt As %Integer)
{
try {
s start = $zh
f pos=1:1:cnt {
s obj = ##class(developer.data.Patient).%OpenId(pos)
s name = obj.漢字氏名
, ptnId = obj.patientId
, cmntObj = obj.コメント
s cmnt = cmntObj.Read()
}
w !, "処理時間:",$zh-start
} catch e {
w !,$system.Status.DisplayError(e.AsStatus())
}
}
$Increment
Check(cnt) ;
s start = $zh
f pos=1:1:cnt d $i(^ECPData)
w !,"ECPタイム:",$zh-start
s start = $zh
f pos=1:1:cnt d $i(^DefData)
w !,"デフォルトタイム:",$zh-start
d Check(1000000)
$Sequence
Check(cnt) ;
s start = $zh
f pos=1:1:cnt s num=$seq(^ECPData)
w !,"ECPタイム:",$zh-start
s start = $zh
f pos=1:1:cnt s num=$seq(^DefData)
w !,"デフォルトタイム:",$zh-start
d Check(1000000)
おわりに
いかがだったでしょうか。
ECPは、分散構成を実現するための便利な仕組みです。
アプリケーションサーバーがデータサーバーと通信しながら、キャッシュやロックの整合性をしっかり保ってくれるので、負荷分散やスケールアウトを考える上で、かなり強力な選択肢になります。
ただし、ロック処理がある場合、データサーバへの接続が発生し処理が遅くなる傾向があります。
リアルタイム性が求められるシステムや、大量のデータ登録を行う処理には向いていないと考えます。
導入を検討する際は、システムの特性と使用方法をよく検討する事が大切でしょう。
一方で、データの参照が中心となるような機能やシステムであれば、ECPは十分にその力を発揮すると思います。
「ECPってちょっと難しそう」と感じていた方も、ぜひこの記事をきっかけに、自分のプロジェクトに導入可能か検討してみて下さい。
慣れてくると、意外と頼もしい機能ですよ。
・・・導入するとしても、環境は結構限られてくるかもしれませんね。