【IRIS/Cache】続・他DBのグローバルが見たい!書きたい!(ECP編)

本記事は、ECP接続について紹介します。

※この記事は下記の方向けになります。
  • 他のDBにあるグローバルを操作(保存・参照)したい方
  • 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」があるのでクリックし、サービス編集画面を起動させます。

画面が起動すると、下記操作を行います。

  1. ECP接続を行いたいので、「サービス有効」にチェックを入れます
  2. 設定は任意ですが、アクセス制限を行う際は、アプリケーションサーバのIPを記述します
  3. 設定が完了したら「保存」ボタンをクリックします

正常に登録されると「有効=はい」と表示されます。
 ※許可済みの接続元に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. 既存ネームスペースに対し、グローバルマッピングを行う
  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接続を重複作成しない
  • データサーバのデータベース・キャッシュは通常よりも多めにする
  • 同一データを複数のアプリケーションサーバで参照している時は、更新はあまり行わない方が良い
  • トランザクション処理を行うときは、なるべくデータサーバ側で実行した方が良い。
  • 一時グローバルは、各アプリケーションサーバに配置する
  • 未定義のグローバルを繰り返し参照しない
  • ストリームフィールドがあるとロックが発生、データサーバへ接続し遅くなる
  • $Increment, $Sequenceは、ロック処理を行わないので活用すべし

詳細が知りたい場合は、ドキュメントを参照して下さい。

速度検証

次は、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.9116.161.19
書き込み(Lock付)8.3710831.14(3時間)8.69
読み込み($ORDER)1.083.181.73
読み込み(未定義NGパターン)0.3610980.86(3時間)0.287
読み込み(未定義OKパターン)0.360.850.39
オブジェクト登録(%Save)215.2434875.79(9.7時間)172.39
オブジェクト取得(%OpenId)65.6071.4065.62
SQL(select文)39.1740.2533.97
SQL(insert文)469.2140228.96(11.2時間)328.86
ストリームフィールドあり
SQL(select文)
482.1711719.23(3.3時間)944.57
ストリームフィールドあり
オブジェクト取得(%OpenId)
189.3311188.94(3.1時間)160.91
$Increment0.3413381.09(3.7時間)0.60
$Sequence0.2413.850.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ってちょっと難しそう」と感じていた方も、ぜひこの記事をきっかけに、自分のプロジェクトに導入可能か検討してみて下さい。
慣れてくると、意外と頼もしい機能ですよ。

・・・導入するとしても、環境は結構限られてくるかもしれませんね。