検索

クリアフィルター
記事
Toshihiko Minamoto · 2021年11月30日

Covid-19に感染した胸部X線画像分類とCT検出デモを実行する

**キーワード**: COVID-19、医用画像、ディープラーニング、PACSビューア、HealthShare。   ## **目的** 私たちは皆、この前例のないCovid-19パンデミックに悩まされています。 現場のお客様をあらゆる手段でサポートする一方で、今日のAI技術を活用して、Covid-19に立ち向かうさまざまな前線も見てきました。  昨年、私は[ディープラーニングのデモ環境](https://jp.community.intersystems.com/node/506626)について少し触れたことがあります。 この長いイースターの週末中に、実際の画像を扱ってみてはどうでしょうか。Covid-19に感染した胸部X線画像データセットに対して簡単な分類を行うディープラーニングモデルをテスト実行し、迅速な「AIトリアージ」や「放射線科医の支援」の目的で、X線画像やCT用のツールがdockerなどを介してクラウドにどれほど素早くデプロイされるのかを確認してみましょう。      これは、10分程度の簡易メモです。学習過程において、最も単純なアプローチでハンズオン経験を得られることを願っています。          ## **範囲** このデモ環境では、次のコンポーネントが使用されます。 これまで見てきた中で最も単純な形式です。 * 3タイプの小さな**匿名加工オープンデータセット**: Covid-19感染胸部、細菌性肺炎胸部、正常な胸部。 * 1セットの**ディープラーニングモデル**(X線胸部画像分類用のInception V3モデル) * Jupyterノートブックによる**TensorFlow 1.13.2**コンテナ * **GPU用のNvidia-Docker2**コンテナ * Nvidia T4 GPU搭載AWS **Ubuntu 16.04 VM**(事前トレーニング済みモデルの再トレーニングを行わない場合はノートパソコンのGPUで十分です) および * 「AI支援CT検出」のデモコンテナ * サードパーティのOpen PACS Viewerのデモコンテナ * HealthShare Clinical Viewerのデモインスタンス 以下は、このデモの範囲では説明されていません。 * PyTorchの人気が高まっています(次の機会に利用します) * デモ環境では、TensorFlow 2.0の実行速度がはるかに低下します(そのためバージョン1.13に戻します)  * AutoMLなどのマルチモデルアンサンブル(実際に人気が高まっていますが、この小さなデータセットでは昔ながらのシングルモデルで十分です) * 実際のサイトから得るX線およびCTデータ。    ## **免責事項** このデモは、この特定の分野における臨床試験についてではなく、技術的なアプローチに関するものです。 CTおよびX線などの証拠に基づくCovid-19診断は現在ではオンラインで広く入手可能であり、肯定的なレビューもあれば、否定的なレビューもあります。また、このパンデミックにおいて、国や文化における役割が実際に異なります。 また、この記事のコンテンツとレイアウトは、必要に応じて変更される場合があります。   このコンテンツは「開発者」としての純粋な個人的見解です。   ## **データ** このテストの元の画像は、Joseph Paul Cohenによって一般公開されている[Covid-19 Lung X-Ray set](https://github.com/ieee8023/covid-chestxray-dataset)に含まれるものと、オープン形式の[Kaggle Chest X-Ray sets](https://www.kaggle.com/paultimothymooney/chest-xray-pneumonia)からAdrian Yuが[GradientCrescentリポジトリ](http://github.com/EXJUSTICE/GradientCrescent)の小さなテストセットに集めたの正常な胸部画像です。 また、皆さんが使用できるように、[簡単なテスト用のテストデータをこちらにアップロード](https://github.com/zhongli1990/Covid19-X-Rays)しました。 これまでのところ、以下の小さなトレーニングセットが含まれます。  * Covid-19に感染した胸部画像 60点  * 正常な胸部画像 70点 * 細菌性肺炎に感染した胸部画像 70点 ## **テスト** 以下のテストでは、独自のフレーバーに合わせてわずかに変更したものを実行しました。 * ベースのInception V3モデルとその上の2つのCNNレイヤー * 再トレーニング用に基盤のInceptionレイヤーの非凍結重みを使った転移学習(ノートパソコンのGPUでは、事前トレーニング済みのInceptionレイヤーを凍結するだけです) * これまでに収集した小さなデータセットを補うためのわずかなデータ拡張 * バイナリの代わりに3つのカテゴリを使用: Covid-19、正常、細菌性(またはウイルス性)肺炎(これらの3つのクラスを使用した理由は後で説明します) * 後のステップで使えるテンプレートとして、基本的な3クラス混同行列を計算   **注意**: ほかの一般的なCNNベースのモデル(VGG16やResNet50など)ではなく、Inception V3を選択した理由は特にありません。 他にもモデルがありますが、最近、骨折データセットのデモ実行に使用したため、ここではそれを再利用しているだけです。  以下のJupyterノートブックスクリプトを実行し直す際には、[お好きなモデル](https://towardsdatascience.com/illustrated-10-cnn-architectures-95d78ace614d)を使用してください。 この記事には、Jupyterノートブックも添付しました。 以下に簡単な説明も示しています。 ### 1. 必要なライブラリをインポートする # import the necessary packages from tensorflow.keras.layers import AveragePooling2D, Dropout, Flatten, Dense, Input from tensorflow.keras.models import Model from tensorflow.keras.optimizers import Adam from tensorflow.keras.utils import to_categorical from tensorflow.keras import optimizers, models, layers from tensorflow.keras.applications.inception_v3 import InceptionV3 from tensorflow.keras.applications.resnet50 import ResNet50 from tensorflow.keras.preprocessing.image import ImageDataGenerator from sklearn.preprocessing import LabelEncoder, OneHotEncoder from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix from imutils import paths import matplotlib.pyplot as plt import numpy as np import cv2 import os ### 2. [ここに提供されているサンプルの画像ファイル](https://github.com/zhongli1990/Covid19-X-Rays)を読み込む # set learning rate, epochs and batch size INIT_LR = 1e-5 # この値は選択したモデルに固有の値です: Inception、VGG、ResNetなど。 EPOCHS = 50 BS = 8 print("Loading images...") imagePath = "./Covid_M/all/train" # 同じ画像のローカルパスに変更してください imagePaths = list(paths.list_images(imagePath)) data = [] labels = [] # 指定されたパスにあるすべてのX線画像を読み取り、サイズを256x256に変更します for imagePath in imagePaths: label = imagePath.split(os.path.sep)[-2] image = cv2.imread(imagePath) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image = cv2.resize(image, (256, 256)) data.append(image) labels.append(label) #ピクセル値を0.0~1.0の実数に正規化します data = np.array(data) / 255.0 labels = np.array(labels) # マルチクラスのラベル付けを行うためにワンホットエンコーディングを実行します label_encoder = LabelEncoder() integer_encoded = label_encoder.fit_transform(labels) labels = to_categorical(integer_encoded) print("... ... ", len(data), "images loaded in multiple classes:") print(label_encoder.classes_) Loading images... ... ... 200 images loaded in 3x classes: ['covid' 'normal' 'pneumonia_bac'] ### 3. 基本的なデータ拡張を追加し、モデルを再構成してトレーニングする  # データとトレーニング用と検証用に分割します。 (trainX, testX, trainY, testY) = train_test_split(data, labels, test_size=0.20, stratify=labels, random_state=42) # 単純な拡張を追加します。 注意: このケースでは、拡張が多すぎるとうまく行きません。テストの過程でわかりました。 trainAug = ImageDataGenerator(rotation_range=15, fill_mode="nearest") #事前トレーニング済み「ImageNet」の重みの転移学習でInceptionV3モデルを使用します。 #注意: VGG16またはResNetを選択した場合は、最上部で初期の学習率をリセットする必要がある場合があります。 baseModel = InceptionV3(weights="imagenet", include_top=False, input_tensor=Input(shape=(256, 256, 3))) #baseModel = VGG16(weights="imagenet", include_top=False, input_tensor=Input(shape=(256, 256, 3))) #baseModel = ResNet50(weights="imagenet", include_top=False, input_tensor=Input(shape=(256, 256, 3))) #Inception V3モデルの上にカスタムCNNレイヤーを2つ追加します。 headModel = baseModel.output headModel = AveragePooling2D(pool_size=(4, 4))(headModel) headModel = Flatten(name="flatten")(headModel) headModel = Dense(64, activation="relu")(headModel) headModel = Dropout(0.5)(headModel) headModel = Dense(3, activation="softmax")(headModel) # 最終モデルを構成します model = Model(inputs=baseModel.input, outputs=headModel) # Navidia T4 GPUを使用しているので、再トレーニングを行うために、事前トレーニング済みのInception「ImageNet」重みの凍結を解除します #baseModel.layersのレイヤーの場合: # layer.trainable = False print("Compiling model...") opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS) model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"]) # 上記で事前トレーニング済みの重みの凍結を解除したため、全モデルをトレーニングします。 print("Training the full stack model...") H = model.fit_generator( trainAug.flow(trainX, trainY, batch_size=BS), steps_per_epoch=len(trainX) // BS, validation_data=(testX, testY), validation_steps=len(testX) // BS, epochs=EPOCHS) ... ... Compiling model... Training the full stack model... ... ... Use tf.cast instead. Epoch 1/50 40/40 [==============================] - 1s 33ms/sample - loss: 1.1898 - acc: 0.3000 20/20 [==============================] - 16s 800ms/step - loss: 1.1971 - acc: 0.3812 - val_loss: 1.1898 - val_acc: 0.3000 Epoch 2/50 40/40 [==============================] - 0s 6ms/sample - loss: 1.1483 - acc: 0.3750 20/20 [==============================] - 3s 143ms/step - loss: 1.0693 - acc: 0.4688 - val_loss: 1.1483 - val_acc: 0.3750 Epoch 3/50 ... ... ... ... Epoch 49/50 40/40 [==============================] - 0s 5ms/sample - loss: 0.1020 - acc: 0.9500 20/20 [==============================] - 3s 148ms/step - loss: 0.0680 - acc: 0.9875 - val_loss: 0.1020 - val_acc: 0.9500 Epoch 50/50 40/40 [==============================] - 0s 6ms/sample - loss: 0.0892 - acc: 0.9750 20/20 [==============================] - 3s 148ms/step - loss: 0.0751 - acc: 0.9812 - val_loss: 0.0892 - val_acc: 0.9750   ### 4. 検証結果用に混同行列をプロットする print("Evaluating the trained model ...") predIdxs = model.predict(testX, batch_size=BS) predIdxs = np.argmax(predIdxs, axis=1) print(classification_report(testY.argmax(axis=1), predIdxs, target_names=label_encoder.classes_)) # 基本的な混同行列を計算します cm = confusion_matrix(testY.argmax(axis=1), predIdxs) total = sum(sum(cm)) acc = (cm[0, 0] + cm[1, 1] + cm[2, 2]) / total sensitivity = cm[0, 0] / (cm[0, 0] + cm[0, 1] + cm[0, 2]) specificity = (cm[1, 1] + cm[1, 2] + cm[2, 1] + cm[2, 2]) / (cm[1, 0] + cm[1, 1] + cm[1, 2] + cm[2, 0] + cm[2, 1] + cm[2, 2]) # 混同行列、精度、感度、特異度を示します print(cm) print("acc: {:.4f}".format(acc)) print("sensitivity: {:.4f}".format(sensitivity)) print("specificity: {:.4f}".format(specificity)) # トレーニング損失と精度をプロットします N = EPOCHS plt.style.use("ggplot") plt.figure() plt.plot(np.arange(0, N), H.history["loss"], label="train_loss") plt.plot(np.arange(0, N), H.history["val_loss"], label="val_loss") plt.plot(np.arange(0, N), H.history["acc"], label="train_acc") plt.plot(np.arange(0, N), H.history["val_acc"], label="val_acc") plt.title("Training Loss and Accuracy on COVID-19 Dataset") plt.xlabel("Epoch #") plt.ylabel("Loss/Accuracy") plt.legend(loc="lower left") plt.savefig("./Covid19/s-class-plot.png") 上の図から、小さなデータセットと5分未満の簡易トレーニングでも「転移学習」のメリットにより、結果はそれほど悪くないことがわかります。12点すべてのCovid-19に感染した胸部画像は正しく分類されており、誤って「細菌性肺炎」の胸部画像として分類された正常な胸部画像は、合計40点のうち1点のみです。  ###   ### 5. 実際のX線画像をテストするための混同行列をプロットする では、さらにもう一歩飛躍してみましょう。簡単にトレーニングされたこの分類器がどれほど効果的であるかをテストするために、実際のX線画像を送信してみます。  上記のトレーニングや検証セットで使用していない27点のX線画像をモデルにアップロードしました。 Covid-19に感染した胸部画像 9点と正常な胸部画像 9点と細菌性肺炎に感染した胸部画像 9点です。  (これらの画像は、この記事にも添付されています。) ステップ2のコードを1行だけ変更し、テスト画像が確実に異なるパスから読み込まれるようにしました。 ... imagePathTest = "./Covid_M/all/test" ... 次に上記のトレーニング済みのモデルを使用して予測します。  predTest = model.predict(dataTest, batch_size=BS) print(predTest) predClasses = predTest.argmax(axis=-1) print(predClasses) ... 最後に、ステップ5のようにして、混同行列を再計算します。 testX = dataTest testY = labelsTest ... ... 実際のテスト結果が得られました。  またしても、トレーニング済みモデルは、すべてのCovid-19に感染した胸部画像を正しく分類できるようです。 こんなに小さなデータセットにしては、それほど悪くない出来です。  ### 6. さらなる監察結果 このイースターの週末に、さまざまなデータセットを試し、Covid-19に感染した胸部画像には明確な特徴があるように見えました。AI分類器の観点では、通常の細菌性またはウイルス性(インフルエンザ)感染症の影響を受けた胸部画像と区別することは比較的簡単です。 また、いくつかの簡単なテストから、細菌性とウイルス性(一般的なインフルエンザ)の胸部画像を区別するのは非常に困難であることに気づきました。  時間があれば、ほかのKaggle挑戦者がおそらくあの状況で行うように、クラスタアンサンブルを使用して、その違いを追跡しなければならないでしょう。   では、臨床の観点では、上記の結果は本当に正しいのでしょうか? Covid-19に感染した胸部にはX線で本当に明確な特徴が見られるのでしょうか? あまり確信が持てません。 実際の胸部放射線科医に意見を聞かなくてはいけないでしょう。 今のところは、現在のデータセットは結論を出すには小さすぎると思います。   **次**: 実際のサイトのX線画像をもっと集めて、xgsboot、AutoML、または新しいIRIS IntegratedMLワークベンチを使って、深く調べたいと思います。  さらに、医師やA&Eトリアージ向けに、Covid-19に感染した胸部画像を、その深刻度に応じてレベル1、レベル2、およびレベル3のようにさらに分類できるはずです。 とにかく話を戻すと、[データセットと上記のJupyterノートブックを添付しました](https://github.com/zhongli1990/Covid19-X-Rays)。   ## **デプロイメント** 上記では、この「医用画像」分野における簡単なセットアップについて、わりと単純な出発点に触れました。 このCovid-19フロントは、実際には、過去1年の週末や長期休暇中に調べたもののうち、3つ目の試行となります。 他には、「AI支援骨折検出」システムや「AI支援眼科網膜診断」システムなどを調べました。  上記のモデルは、今のところは単純すぎて試すほどでもないかもしれませんが、一般的な疑問はすぐにでも避けて通れないでしょう。[ある種の「AIサービス」はどのようにデプロイすればよいのでしょうか? ](https://community.intersystems.com/post/deploy-mldl-models-consolidated-ai-demo-service-stack) テクノロジースタックとサービスのライフサイクルに関わることですが、どのような問題を解決しようとしているのか、どういった実際の価値が得られるのかといった実際の「ユースケース」によっても異なります。  答えはテクノロジーほど明確ではないこともあります。 イギリスの[RCR(王立放射線学者会)の原案](https://www.rcr.ac.uk/sites/default/files/integrating-ai-with-radiology-reportin%20g-workflow-guidance-covid19.pdf)では、「放射線技師のAI支援」と「A&EまたはプライマリケアにおけるAIトリアージ」という2つの単純なユースケースが提案されました。 正直なところ現時点では2つ目の「AIトリアージ」に同意しており、個人的には、この方がより高い価値をもたらすのではないかと思っています。  また、幸いにも、今日の開発者はクラウド、Docker、AI、そしてもちろんInterSystemsのHealthShareにより、これまで以上の力を使ってこの種のケースを解決することができます。  たとえば、以下のスクリーンキャプチャは実際にAWSにホストされたエンタープライズ級の「AI支援によるCovid-19感染胸部のCT検出」サービスを示しており、デモの目的で、いかにしてこのサービスを直接HealthShare Clinical Viewerに組み込むかを説明しています。 X線撮影と同様に、DICOMに設定されたCTも迅速な「AIトリアージ」のユースケース用に毎日24時間稼働しながら、このオープンPACS Viewerに直接アップロードまたは送信し、「AI診断」をクリックするだけで、トレーニング済みモデルに基づいて定量化されたCovid-19感染確率の判定を提供することができます。 X線画像分類などのモデルは、最前線に立つ医師を支援できるよう、同一患者のコンテキストで既存のPACSビューアの上にデプロイし、同じ方法で呼び出すことができます。   ![](/sites/default/files/inline/images/images/healthshare_cv_covid19_pacs_ct_ai_embeded.png)   ## **謝辞**   繰り返しますが、このテスト画像は、Joseph Paul Cohenによる[ Covid-19 Lung X-Ray set](https://github.com/ieee8023/covid-chestxray-dataset)から得たもので、正常な胸部画像はAndrian Yuが[GradientCrescentリポジトリ](http://github.com/EXJUSTICE/GradientCrescent)に収集しているオープンの[Kaggle Chest X-Ray sets](https://www.kaggle.com/paultimothymooney/chest-xray-pneumonia)に含まれるものです。 また、「テスト」セクションにリストされたとおり、独自に改善したトレーニングを使って[PyImageSearchのAdrian](https://github.com/AleGiovanardi/covidhelper)の構造を再利用しています。 また、テストデータベースを確認するために、X線画像とCT画像用のAIモジュール付き[AWSクラウドベースOpen PACS Viewer](http://ec2-3-8-183-64.eu-west-2.compute.amazonaws.com/doctor/index.html#!/login)を提供していただいた[HYM](http://en.huiyihuiying.com/)にも感謝しています。   **次の内容** 今日、AIは、人間の健康と日常生活のほぼすべての側面を「浸食」しています。 単純化し過ぎた私の見解では、ヘルスケア分野におけるAI技術の応用には、以下のようないくつかの方向性があると考えられます。 * **医用画像**: 胸部、心臓、目、脳などのX線、CT、またはMRIなどの診断画像。 * **NLPによる読解**: 膨大な量のテキスト資産とナレッジベースのマイニング、理解、学習。 * **公衆衛生**: 疫学などの傾向予測、分析、モデリングなど。  * パーソナル化AI: 個人専用の健康アシスタントとして、ともに成長して老化するように特別にトレーニングされたAI/ML/DLモデル。 * その他のAI: AlphaGoや、Covid-19への対抗に活用できる3次元タンパク質構造予測向けのAlphaFoldなど。このような最先端の画期的な技術に非常に感激しています。  学習の過程で、焦点を当てられるものを検討していきたいと思います。 いずれにしても、在宅が長く続かない限りは、取り上げたいリストにしかすぎないかもしれません。    付録 - ファイルアップロードはこちらです。 上記で使用した画像と上記のJupyterノートブックファイルが含まれています。 週末に新規でセットアップして実行するには、数時間かかるかもしれません。  
記事
Minoru Horita · 2020年6月2日

グローバルはデータを保存するための魔法の剣ですパート2 - ツリー

最初の記事については、パート1を参照してください。   3. グローバルを使用する場合のさまざまな構造   順序付きツリーなどの構造には、さまざまな特殊ケースがあります。 グローバルを使用する上で実用的な価値があるものを見てみましょう。         3.1 特殊ケース1 - 枝のない1つのノード   グローバルは配列のようにも、通常の変数のようにも使用できます。 例えば、カウンターを作成する場合を考えてみましょう。   Set ^counter = 0 ; カウンターの設定 Set id=$Increment(^counter) ; アトミックなインクリメント操作   また、グローバルには値に加えて枝を持たせることができます。 一方が他方を除外することはありません。   3.2 特殊ケース2 - 1つのノードと複数の枝   実際、これは典型的なキー・バリューベースのデータ構造です。 また、値の代わりに値のタプルを保存すると、主キーを持つ通常のテーブルが得られます。   グローバルに基づくテーブルを実装するには、カラムの値から文字列を作成し、主キー別にそれをグローバルに保存する必要があります。 読み取りの時に文字列をカラムに分割できるようにするため、以下のいずれかを使用することができます。 区切り文字 Set ^t(id1) = "col11/col21/col31" Set ^t(id2) = "col12/col22/col32" 各フィールドが特定のバイト数を占める、固定のスキーマ。 これは、リレーショナルデータベースで通常行われる方法です。 値から文字列を作る専用の $LB 関数(Cachéで導入されます)。   Set ^t(id1) = $LB("col11", "col21", "col31") Set ^t(id2) = $LB("col12", "col22", "col32")   興味深いことに、グローバルを使用すればリレーショナルDBの外部キーと同様のことを行うのは難しくありません。 このような構造をインデックスグローバルと呼びましょう。 インデックスグローバルは、メイングローバルの主キーを構成しないフィールドですばやく検索するための補助的なツリーです。 インデックスグローバルにデータを入れて使用するには、追加のコードを記述する必要があります。 最初のカラムに基づいてグローバルインデックスを作成しましょう。 Set ^i("col11", id1) = 1 Set ^i("col12", id2) = 1   最初のカラムですばやく検索するには、^i グローバルを調べ、最初のカラムで必要な値に対応する主キー(id)を見つける必要があります。 値を挿入する際には、必要なフィールドの値とインデックスグローバルの両方を作成できます。 信頼性を確保するため、トランザクションで囲みましょう。   TSTART Set ^t(id1) = $LB("col11", "col21", "col31") Set ^i("col11", id1) = 1 TCOMMIT   詳細については、グローバルとセカンダリキーのエミュレーションを使用してMでテーブルを作成する方法をご覧ください。   挿入/更新/削除関数がCOS/Mで記述されてコンパイルされている場合、これらのテーブルは従来のDBと同じくらい高速に(またはさらに高速に)機能します。 単一の2カラム構成のテーブルにINSERTおよびSELECT操作の大部分を適用し、TSTARTおよびTCOMMITコマンド(トランザクション)も併用することにより、このことをを検証しました。 同時アクセスと並列トランザクションが発生する、より複雑なシナリオはテストしていません。 トランザクションを使用しない場合、100万件の値を挿入したときの速度は毎秒778,361レコードでした。 3億件の場合、挿入速度は毎秒422,141レコードでした。トランザクションが使用された場合、挿入速度は5,000万件の値で毎秒572,082レコードでした。 すべての操作はコンパイル済みのMコードから実行されました。 SSDではなく、通常のハードドライブを使用しました。 ライトバックキャッシュ付きのRAID5を構成していました。 すべての処理は、Phenom II 1100T CPUで実行されました。 SQLデータベースに対して同じテストを実行するには、ループで挿入を行うストアドプロシージャを作成する必要があります。同じ方法を使用してMySQL 5.5(InnoDBストレージ)をテストしたときは、1秒あたりの挿入件数が11,000を超えることはありませんでした。   そうです。グローバルを使ったテーブルの実装は、リレーショナルデータベースで同じ実装をするよりも複雑です。 そのため、グローバルに基づく実務用DBはSQLデータにアクセスし、表形式データでの作業を簡略化しているのです。 一般的に、データスキーマが頻繁に変更されない場合、挿入速度は重要ではなく、データベース全体を正規化されたテーブルで簡単に表現することができます。これにより、より高度な抽象化が行われるため、SQLでの作業が容易になります。 このケースでは、グローバルを他の種類のDBを作成するための構成要素として使用できることを示したいと思いました。 これは、他の言語の作成に使用できるアセンブリ言語と同じです。 また、グローバルを使用してキー/値、リスト、セット、表形式、ドキュメント指向のDBに相当するものを作成する例がいくつか存在します。 最小限の労力で標準的ではないDBを作成する必要がある場合は、グローバルの使用を検討すると良いでしょう。   3.3 特殊ケース3 - 第2階層に枝の数が固定されたノードを持つ2階層のツリー     ご想像のとおり、これはグローバルを使用したテーブルの代わりに実装したものです。 以前のものと比較してみましょう。 2階層ツリー内のテーブルと 1階層ツリー内のテーブルの比較。 短所 長所 ノード数とカラム数が等しくなるように設定する必要があるため、挿入が遅くなります。 カラム名を含むグローバルインデックス(配列インデックスなど)がハードドライブの容量を占有し、各レコードで複製されるため、ハードドライブの容量の消費が多くなります。   文字列を解析する必要がないため、特定カラムの値に高速にアクセスできます。 私がテストした結果によれば、2カラムの場合は11.5%高速で、カラム数が増えるとさらに高速になりました。 データスキーマの変更が簡単 コードが読みやすい   結論: 特筆すべきことはありません。 パフォーマンスはグローバルの主なメリットの1つであるため、このアプローチを使用しても実質的には意味がありません。リレーショナルデータベースの通常のテーブルよりも高速に動作することはほとんどないからです。   3.4 一般的なケース - ツリーと順序キー   ツリーとして表現できるすべてのデータ構造は、完全にグローバルに適合します。   3.4.1 サブオブジェクトを持つオブジェクト     これは、グローバルが昔から使用されている分野です。 医療分野には数多くの病気、薬剤、症状、治療法があります。 これらのフィールドの99%は空になるため、患者ごとに100万フィールドのテーブルを作成するのは不合理です。 「患者」〜100,000フィールド、「投薬」100,000フィールド、「治療」100,000フィールド、「合併症」100,000フィールドなどのテーブルで構成されたSQL DBを想像してください。 別の方法として、特定の患者タイプ(これも重複する可能性があります!)、治療、投薬ごとの数千テーブルと、これらのテーブル間のリレーション用の数千テーブルを含むDBを作成できます。 グローバルは手袋のようにヘルスケアに適合します。これは、各患者に対して完全な症例記録、治療法のリスト、投与薬とその効果をすべて、空のカラムに無駄なディスク領域を浪費することなくツリーの形で持たせることができるためです。リレーショナルデータベースではそうはいきません。 グローバルは、個人の詳細情報を含むデータベースに適しています。取引先に関するさまざまな個人データを最大限に蓄積し、システム化することが課題である場合が想定されます。 これは、医療、銀行、マーケティング、アーカイブなどの分野で特に重要です。 当然ながら、SQLを使用すればいくつかのテーブルのみを使用してツリーをエミュレートすることもできますが(EAV、1、2、3、4、5、6、7、8)、その場合はかなり複雑になり、処理が遅くなります。 つまりテーブルに基づいてグローバルを記述し、すべてのテーブル関連ルーチンを抽象化レイヤーの下に隠す必要があるでしょう。 上位の技術(SQL)を利用して下位の技術(グローバル)をエミュレートすることは正しくありません。 それは単純に言って不当です。 巨大なテーブルのデータスキーマを変更する処理(ALTER TABLE)には、かなりの時間がかかる場合があるのは明らかです。 例えば、MySQLの場合はすべてのデータを古いテーブルから新しいテーブルにコピーすることにより、ALTER TABLE ADD|DROP COLUMN操作を実行します(MyISAMとInnoDBでテスト済みです)。 このような処理は、数十億件のレコードを含む本番データベースを数週間とは言わないまでも、数日間ハングアップさせる可能性があります。 グローバルを使用している場合、データ構造の変更にコストはかかりません。いつでも任意の階層の任意のオブジェクトにいくらでも新しいプロパティを追加できます。 枝の名前変更が必要な場合は、DBがバックグラウンドモードで稼働している状態で適用できます。 そのため、グローバルは省略可能なプロパティを多数含むオブジェクトを格納する場合に最適です。   グローバルではすべてのパスがBツリーであるため、プロパティへのアクセスが瞬時に行われることを思い出してください。 一般的なケースでは、グローバルに基づくデータベースは階層情報の格納に対応した一種のドキュメント指向データベースであると言えます。 したがって、ドキュメント指向のデータベースはカルテ保存の分野でグローバルと効果的に競合できます。 しかし、それではまだ不十分です。 MongoDBを例にとってみましょう。この分野では 、以下の理由でグローバルに劣っています。 1. ドキュメントサイズ。保存単位はJSON形式(正確にはBSON形式)のテキストで、最大サイズは約16 MBです。 この制限は、解析中に巨大なJSONドキュメントが保存され、特定フィールド値のアドレスが指定された場合にJSONデータベースが遅くなりすぎないようにするために導入されました。 このドキュメントには、患者に関する完全な情報が含まれていなければなりません。 私たちは皆、カルテがどれほど厚いかを知っています。 カードの最大サイズが16 MBに制限されているのであれば、カードにMRIスキャン画像、X線スキャン画像などの資料が含まれている患者はすぐに除外されてしまうでしょう。 グローバルの単一ブランチには、ギガバイトや数千テラバイトのデータを持たせることができます。 それがすべてを物語っていますが、もう少し話を続けましょう。 2. 患者カードから新しいプロパティを作成/変更/削除するのに要する時間。このようなデータベースでは、カルテ全体(大量のデータ!)をメモリにコピーし、BSONデータを解析し、新しいノードを追加/変更/削除し、インデックスを更新し、すべての情報をBSONに圧縮してディスクに保存する必要があります。 グローバルの場合は必要なプロパティのアドレスを指定し、必要な操作を実行するだけで済みます。 3. 特定プロパティへのアクセス速度。ドキュメントに多くのプロパティと複数階層構造がある場合、各パスがBツリーになっているグローバルのほうが特定プロパティへのアクセスが高速になります。 BSONでは、必要なプロパティを見つけるためにドキュメントを順番に解析する必要があります。   3.3.2 連想配列 連想配列(入れ子になった配列でさえも)は、グローバルで完璧に使用できます。 例えば、次のPHP配列は3.3.1の最初の図のようになります。   $a = array( "name" => "Vince Medvedev", "city" => "Moscow", "threatments" => array( "surgeries" => array("apedicectomy", "biopsy"), "radiation" => array("gamma", "x-rays"), "physiotherapy" => array("knee", "shoulder") ) );     3.3.3 階層ドキュメント:XML、JSON   これらも簡単にグローバルに格納し、さまざまな方法で分解できます。 XML XMLをグローバルに分解するには、ノードにタグ属性を格納するのが最も簡単です。 タグの属性に素早くアクセスする必要がある場合は、それらを別々の枝に配置できます。 <note id=5> <to>Alex</to> <from>Sveta</from> <heading>Reminder</heading> <body>Call me tomorrow!</body> </note>   COSでは、次のようなコードになります。     Set ^xml("note")="id=5" Set ^xml("note","to")="Alex" Set ^xml("note","from")="Sveta" Set ^xml("note","heading")="Reminder" Set ^xml("note","body")="Call me tomorrow!"   注意:XML、JSON、および連想配列の場合、それらをグローバルに表示する方法はいくつか見つけることができます。 この特定のケースでは、「note」タグ内で入れ子になったタグの順序は反映されていません。 ^xml グローバルでは、入れ子になったタグはアルファベット順に表示されます。 順序を正確に表すには、次のようなモデルを使用できます。   JSON   このJSONドキュメントの内容は、セクション3.3.1の最初の図に示されています。 var document = { "name": "Vince Medvedev", "city": "Moscow", "threatments": { "surgeries": ["apedicectomy", "biopsy"], "radiation": ["gamma", "x-rays"], "physiotherapy": ["knee", "shoulder"] }, };     3.3.4 階層関係に制限された同一の構造   例:営業所の構造、ネットワークビジネス構造における人々のポジション。 デビューのデータベース。グローバルのノードインデックスの値として、手の強さを評価に使用できます。 この場合、最高の手を決定するには、重みが最も高い枝を選択する必要があります。 グローバルでは、すべての階層のすべての枝が手の強さでソートされます。 営業所、ネットワークビジネス企業の人々の構造。ノードには、サブツリー全体の特性を反映するいくつかのキャッシュ値を保存できます。 例えば、この特定のサブツリーの売上高です。 いつでもすべての支店の業績について正確な情報を得ることができます。 4. グローバルの使用が役立つ場面 最初の列にはグローバルを使用した場合にパフォーマンス面でかなりのメリットが得られるケースを、2番目の列には開発またはデータモデルを単純化できる状況を一覧で掲載しています。 スピード データ処理/表現の利便性 挿入(各階層での自動ソートを伴うもの)、(主キーごとにインデックスを作成するもの) サブツリーの削除 個別にアクセスが必要な入れ子になったプロパティを多数持つオブジェクト 存在しないものも含め、任意の枝から子の枝を探索する可能性のある階層構造 ツリーの深さ方向の探索 不要なプロパティ/インスタンス(入れ子になったものを含む)を大量に含むオブジェクト/インスタンス スキーマのないデータ。新しいプロパティが頻繁に追加され、古いプロパティが削除される可能性がある場合。 標準的ではないDBを作成する必要がある場合。 パスデータベースとソリューションツリー。 パスをツリーとして適切に表現できる場合。 再帰を使用しない階層構造の削除     続きは「グローバルはデータを保存するための魔法の剣です パート3 - 疎な配列」を読み進めてください。   免責事項:この記事と記事に対する筆者(英語原文はSergey Kamenev氏によるものです)のコメントは、筆者の意見を反映しているにすぎず、InterSystems Corporationの公式見解とは関係ありません。 次のパート「グローバルはデータを保存するための魔法の剣です パート3 - 疎な配列 」を読み進めてください。 また、前のパート「グローバルはデータを管理するための魔法の剣です パート1」も確認してください。
記事
Toshihiko Minamoto · 2021年9月30日

Caché 2016.2のドキュメントデータモデルの紹介

## はじめに Caché 2016.2のフィールドテストはかなり前から利用可能ですので、このバージョンで新しく追加されたドキュメントデータモデルという重要な機能に焦点を当てたいと思います。 このモデルは、オブジェクト、テーブル、および多次元配列など、データ処理をサポートするさまざまな方法として自然に追加されました。 プラットフォームがより柔軟になるため、さらに多くのユースケースに適したものになります。 いくつかのコンテキストから始めましょう。 NoSQLムーブメントの傘下にあるデータベースシステムを少なくとも1つは知っているかもしれません。 これにはかなりたくさんのデータベースがあり、いくつかのカテゴリにグループ化することができます。 Key/Valueは非常に単純なデータモデルです。 値をデータベースに格納し、それにキーを関連付けることができます。 値を取得する場合は、キーを介してそれにアクセスする必要があります。 適切なキーを選択によってソートが決まり、キーの一部であるものでグループ化する場合に単純な集計に使用できるようになるため、キーの選択が重要な鍵となります。 ただし、値は値にすぎません。 値内の特定のサブ要素にアクセスしたり、それらにインデックスを作成したりすることはできません。 値をさらに活用するには、アプリケーションロジックを書く必要があります。 Key/Valueは、大規模なデータセットと非常に単純な値を操作する必要がある場合に最適ですが、より複雑なレコードを扱う場合には価値が劣ります。 ドキュメントデータモデルはKey/Valueにとてもよく似ていますが、値はより複雑です。 値はキーに関連付けられたままになりますが、さらに、値のサブ要素にアクセスして特定の要素にインデックスを作成することができます。 つまり、いくつかのサブ要素が制限を満たす特定のドキュメントを検索することもできるということです。 明らかに、NoSQLの世界にはGraphのような他のモデルもさらに存在しますが、ここでは、ドキュメントに焦点を置くことにします。 ###   ## そもそもドキュメントとは? 一部の読者を混乱させる傾向があるため、まず最初に、1つ明確にしておきましょう。この記事で「ドキュメント」と言った場合、PDFファイルやWordドキュメントといった物理的なドキュメントを指してはいません。 この文脈でのドキュメントとは、サブ値を特定のパスと関連付けることのできる構造を指しています。 ドキュメントを記述できるシリアル化形式には、JSONやXMLなどのよく知られたものが様々あります。 通常こういった形式には、共通して次のような構造とデータ型があります。 1. 順序付けされていないKey/Valueペアの構造 2. 順序付けされた値のリスト 3. スカラー値 1つ目は、XMLの属性要素とJSONのオブジェクトにマッピングされます。 2つ目の構造は、XMLのサブ要素とJSONの配列を使ったリストによって導入されています。 3つ目は、単に、文字列、数値、ブール値といったネイティブのデータ型を利用できるようしています。 JSONのようなシリアル化された形式でドキュメントを視覚化することは一般的ですが、これは、ドキュメントを表現できる一方法にすぎないことに注意してください。 この記事では、JSONを主なシリアル化形式として使用することにします。JSONサポートの改善機能をうまく利用できるでしょう。この改善についてまだ読んでいない方は[こちら](https://community.intersystems.com/post/introducing-new-json-capabilities-cach%C3%A9-20161)をご覧ください。 ドキュメントはコレクションにグループ化されます。 セマンティックと潜在的に共通の構造を持つドキュメントは同じコレクションに保存する必要があります。 コレクションはその場で作成できるため、事前にスキーマ情報を用意しておく必要はありません。 コレクションにアクセスするには、データベースハンドルを最初に取得しておく必要があります。 データベースハンドルはサーバーへの接続として機能し、コレクションへの単純なアクセスを提供しますが、分散環境の場合にはさらに複雑なシナリオを処理することもできます。 ###   ## 基本 まず、Caché Object Scriptで単純なドキュメントを挿入する方法を見てみましょう。 USER>set db = ##class(%DataModel.Document.Database).$getDatabase() USER>set superheroes = db.$getCollection("superheroes") USER>set hero1 = {"name":"Superman","specialPower":"laser eyes"} USER>set hero2 = {"name":"Hulk","specialPower":"super strong"} USER>do superheroes.$insert(hero1) USER>do superheroes.$insert(hero2) USER>write superheroes.$size() 2 上記のコードサンプルでは、まずデータベースハンドルが取得されて、「superheroes」というコレクションが取得されます。 コレクションは明示的に作成されるため、事前に設定する必要はありません。 新しいコレクションにアクセスできるようになったら、ヒーローのSupermanとHulkを表す非常に単純なドキュメントを2つ作成します。 これらは$insert(<document>)への呼び出しでコレクションに保存され、コレクションサイズの最終チェックで、2つのドキュメントを報告します。これは、以前にコレクションが存在していなかったためです。 $insert()呼び出しは、成功すると、挿入されたドキュメントを返します。 このため、ドキュメントの操作を続行する場合に、自動的に割り当てられたIDを取得することができます。 また、チェーンメソッドも可能になります。 USER>set hero3 = {"name":"AntMan","specialPower":"can shrink and become super strong"} USER>write superheroes.$insert(hero3).$getDocumentID() 3 このコードスニペットは、別のヒーローオブジェクトを作成し、superheroesコレクションに永続させます。 今回は、メソッド呼び出しの$getDocumentID()を$insert()呼び出しに連鎖させ、システムがこのドキュメントに割り当てたIDを取得します。 $insert()は必ず自動的にIDを割り当てます。 独自のIDを割り当てる必要がある場合は、$insertAt(<User-ID>,<document>)呼び出しを利用できます。 特定のIDでドキュメントを取得する場合は、コレクションに対して$get(<ID>)メソッドを呼び出すことができます。 USER>set antMan = superHeroes.$get(3) USER>write antMan.$toJSON() {"name":"AntMan","specialPower":"can shrink and become super strong"} Supermanとそのhometownを表すドキュメントを更新するとしましょう。 この場合、$upsert(<ID>,<document>)呼び出しを使用して、既存のドキュメントを簡単に更新することができます。 USER>set hero1.hometown = "Metropolis" USER>do superheroes.$upsert(1,hero1) USER>write superheroes.$get(1).$toJSON() {"name":"Superman","specialPower":"laser eyes","hometown":"Metropolis"} $upsert()は、IDがまだほかに使用されていない場合にドキュメントを挿入するか、そうでない場合に既存のドキュメントを更新します。 もちろん、$toJSON()を呼び出すだけで、コレクションの全コンテンツをJSONにシリアル化することも可能です。 USER>write superheroes.$toJSON() [ {"documentID":1,"documentVersion":4,"content":{"name":"Superman","specialPower":"laser eyes","hometown":"Metropolis"}}, {"documentID":2,"documentVersion":2,"content":{"name":"Hulk","specialPower":"super strong"}}, {"documentID":3,"documentVersion":3,"content":{"name":"AntMan","specialPower":"can shrink and become super strong"}} ] コレクションがドキュメントの配列として表されていることがわかります。 各ドキュメントは、ドキュメントIDとドキュメントバージョンでラップされており、同時実行を適切に処理するために使用されます。 実際のドキュメントコンテンツは、ラッパーのプロパティコンテンツに格納されます。 これは、コレクションの完全なスナップショットを取得して移動できるようにするために必要な表現です。 また、これはモデルの非常に重要な側面をカバーしており、特殊プロパティを予約してドキュメントデータに挿入することはありません。 ドキュメントを適切に処理するためのエンジンが必要とする情報は、ドキュメントの外部に保存されます。  さらに、ドキュメントはオブジェクトまたは配列のいずれかです。 その他の多くのドキュメントストアは、オブジェクトを最上位の要素としてのみ許可しています。 コレクションでドキュメントを変更するためのAPI呼び出しには他にもたくさんあり、基本的な演算をいくつか見てきました。 ドキュメントの挿入と変更は楽しい作業ですが、実際にデータセットを分析したり、特定の制限を満たすドキュメントを取得したりすると、さらに興味深くなります。  ##   ## クエリ すべてのデータモデルが有用とみなされるには、何らかのクエリ機能が必要です。  動的スキーマを使用してドキュメントをクエリできるようにするには、2つの潜在的な方法があります。 1. 動的なドキュメントの性質に対処できる独自のクエリ言語を設計して実装する 2. 定着している構造化されたクエリ言語にクエリを統合する この記事の後の方で説明するいくつかの理由により、コレクションをSQLエンジンに公開することにしました。 SQLの知識を引き続き活用できるというメリットがあります。また、クエリ方言の別のフレーバーを作成しているところでもあります。 実際、SQL ANSI委員会は、JSONの標準拡張機能を提案しており、それに準拠しています。 まとめれば、これらの拡張機能には、JSON関数の2つのカテゴリが含まれています。 1. リレーションコンテンツからJSONコンテンツに公開するための関数セット 2. 動的JSONコンテンツをクエリするための関数セット  この記事の範囲では、2つ目のカテゴリである動的JSONコンテンツのクエリのみを取り上げ、結果をSQLで処理できるようにテーブルとして利用できるようにします。 動的コンテンツ(関連するスキーマのないコンテンツ)を公開し、事前に定義されたスキーマを使用してデータを操作するSQLで利用できるようにする魔法の関数は、JSON_TABLEです。 一般に、この関数は2つの引数を取ります。 1. JSONデータソース 2. 名前と型で列へのJSONパスのマッピングを指定する定義 骨に肉付けした例を見てみましょう。 SELECT name, power FROM JSON_TABLE( 'superheroes', '$' COLUMNS( name VARCHAR(100) PATH '$.name', power VARCHAR(300) PATH '$.specialPower' ) ) name power --------- ------------------------------- Superman laser eyes Hulk super strong AntMan can shrink and become super strong JSON_TABLE関数の最初の引数は、それが作成する仮想テーブルのソースを定義します。 この場合、コレクション「superheroes」をクエリします。 このコレクションのドキュメントごとに行が作られます。 2つ目の引数は、ドキュメントの特定の値をテーブルの列として公開することを忘れないでください。 この引数は2つ部分で構成されています。最初のステップとして、次の式のコンテキストを設定します。 ドル記号'$'には特別な意味があり、ドキュメントのルートを指しています。 それ以降のすべての式はこのコンテキストを基準としています。 後に続くのは、COLUMNS句で、カンマ区切りのCOLUMN式のリストです。 COLUMN式ごとに、仮想テーブルの列が作成されます。 「name」と「power」という2つの列をクエリで公開しています。 列「name」はVARCHAR(100)型で定義されていますが、列「power」は300文字に制限されています。 PATH式は、JPL(JSONパス言語)式を使用してドキュメントの特定の値を列に関連付けています。 キー「name」の値は列「name」に公開されますが、キー「specialPower」の値は列「power」にマッピングされます。 JPL式は非常に表現力が高く強力ですが、これについては別のトピックで説明することにします。 このサンプルで使用した式は非常に基本的な式です。 この構文が初めてであれば、理解するのに少し時間が掛かるかもしれませんが、 JSON_TABLE関数を自然に読み取ると理解しやすいでしょう。 例として、上記のクエリを使います。 ここで表現しているのは、基本的に次のことです。 コレクション「superheroes」をクエリし、各ドキュメントのルートに式のコンテキストを設定します。 次の2つの列を公開します。 1. 列「name」を型VARCHAR(100)で公開し、キー「name」の値を挿入します。 2. 列「power」を型VARCHAR(300)で公開し、キー「specialPower」の値を挿入します。 前に述べた通り、JPL式は複雑になりがちであるか、たくさんの列を公開したいだけの場合もあります。 そのため、型定義を参照できる標準への拡張を組み込みました。これは基本的に、事前定義済みのCOLUMNS句です。 このようにして、上記のCOLUMNS句を登録することができます。 do db.$createType("heropower",{"columns":[{"column":"name","type":"VARCHAR(100)","path":"$.name"},{"column":"power","type":"VARCHAR(300)","path":"$.specialPower"}]}) 型情報を登録したら、%TYPE式を使って、JSON_TABLE関数でそれを参照することができます。 SELECT name, power FROM JSON_TABLE( 'superheroes', '$' %TYPE 'heropower' ) これは明らかに、SQLクエリにドキュメントの一貫したビューを提供し、クエリそのものを大幅に簡略化する上で役立ちます。 ## 高度な内容 ここまで説明したことのほぼすべてについて補足することはたくさんありますが、ここでは最も重要なことに焦点を当てたいと思います。 最後のセクションを読みながら、JSON_TABLE関数を非常に強力なポイントとしている手掛かりに気づいたかもしれません。  1. 仮想テーブルを作成する 2. JSONのようなデータをソースデータとして消費できる 最初の項目はそれだけで重要なポイントです。コレクションを簡単にクエリして、別のJSON_TABLE呼び出しまたは正にテーブルと結合することができるからです。 コレクションをテーブルと結合できることは大きなメリットです。要件に応じて、データに完璧なデータモデルを選択できるのです。  型安全、整合性チェックが必要であるのに、モデルがあまり進化していませんか? リレーショナルを使いましょう。 他のソースのデータを処理してそのデータを消費する必要があり、モデルが必ず急速に変化するか、アプリケーションユーザーの影響を受ける可能性のあるモデルを保存することを検討していますか? ドキュメントデータモデルを選択しましょう。 モデルはSQLで一緒にまとめることができるので安心です。 JSON_TABLE関数の2つ目のメリットは、実のところ基盤のデータモデルとは無関係です。 これまでに、JSON_TABLEでコレクションのクエリを説明してきました。 最初の引数は任意の有効なJSON入力にすることもできます。 次の例を考察しましょう。 SELECT name, power FROM JSON_TABLE( '[ {"name":"Thor","specialPower":"smashing hammer"}, {"name":"Aquaman","specialPower":"can breathe underwater"} ]', '$' %TYPE 'heropowers' ) name power --------- ------------------------------- Thor smashing hammer Aquaman can breathe underwater 入力は通常のJSON文字列で、オブジェクトの配列を表します。 構造がsuperheroesコレクションと一致するため、保存された型識別子「heropowers」を再利用することができます。  これにより、強力なユースケースが可能になります。 実際にメモリ内のJSONデータをディスクに永続させずにクエリすることができます。 REST呼び出しでJSONデータをリクエストし、クエリを実行してコレクションまたはテーブルを結合できます。 この機能を使用すると、Twitterタイムライン、GitHubリポジトリの統計、株式情報、または単に天気予報のフィードをクエリできます。 この機能は非常に便利であるため、これについては、後日、専用の記事で取り上げたいと思います。 ##   ## REST対応 ドキュメントデータモデルでは、初期状態でREST対応のインターフェースが備わっています。 すべてのCRUD(作成、読み取り、更新、削除)とクエリ機能はHTTP経由で利用できます。 完全なAPIについては説明しませんが、ネームスペース「USER」内の「superheroes」コレクションのすべてのドキュメントを取得するサンプルcURLを以下に示します。 curl -X GET -H "Accept: application/json" -H "Cache-Control: no-cache" http://localhost:57774/api/document/v1/user/superheroes ###   ## 私のユースケースで使用できますか? ドキュメントデータモデルは、InterSystemsのプラットフォームへの重要な追加機能です。 これは、オブジェクトとテーブルに続く、まったく新しいモデルです。 SQLとうまく統合できるため、既存のアプリケーションで簡単に利用することができます。 Caché 2016.1で導入された新しいJSON機能によって、CachéでのJSONの処理が楽しく簡単になります。 そうは言っても、これは新しいモデルです。 いつ、そしてなぜそれを使用するのかを理解する必要があります。 常に言っていることですが、特定のタスクにはそれに適したツールを選択してください。 このデータモデルは、動的データを処理する必要がある場合に優れています。 以下に、主な技術的メリットをまとめます。 * 柔軟性と使いやすさ スキーマを予め定義する必要がないため、データの作業環境を素早くセットアップし、データ構造の変更に簡単に適応させることができます。 * スパース性 テーブルに300列があっても、各行が入力できるのはその内の15列であることを覚えていますか? これはスパースなデータセットであり、リレーショナルシステムではそれらを最適に処理にできません。 ドキュメントは設計上スパースであり、効率的に保存と処理を行えるようになっています。 * 階層 配列やオブジェクトなどの構造化型は、任意の深さでネストすることができます。 つまり、ドキュメント内で関連するデータを保存することができるため、そのレコードにアクセスする必要がある場合の読み取りのI/Oを潜在的に縮小することができます。 データは非正規化して保存できますが、リレーショナルモデルではデータは正規化して保存されます。 * 動的な型 特定のキーには、列のように固定されたデータ型がありません。 名前は、あるドキュメントでは文字列であっても、別のドキュメントでは複雑なオブジェクトである可能性があります。 単純なものは単純にしましょう。 複雑になることはありますが、そうなった場合はもう一度単純化しましょう。 上記の項目はそれぞれ重要であり、適切なユースケースには、少なくとも1つが必要ではありますが、すべての項目が一致することは珍しいことではありません。 モバイルアプリケーションのバックエンドを構築しているとしましょう。 クライアント(エンドユーザー)は自由に更新できるため、同時に複数のバージョンのインターフェースをサポートする必要があります。 WebServicesを使用するなど、契約によって開発すると、データインターフェースを素早く適応させる能力が低下する可能性があります(ただし、安定性が増す可能性はあります)。 ドキュメントデータモデルには柔軟性が備わっているため、スキーマを素早く進化させ、特定のレコード型の複数のバージョンを処理し、それでもクエリで相関させることができます。 ###   ## その他のリソース この魅力的な新機能についてさらに詳しく知りたい方は、利用可能なフィールドテストバージョン2016.2または2016.3を入手してください。 『ドキュメントデータモデル学習パス』を必ず確認してください。 今年のグローバルサミットのセッションをお見逃しなく。 「データモデリングリソースガイド」には、関連するすべてのセッションが集められています。 最後に、開発者コミュニティに参加し、質問を投稿しましょう。また、フィードバックもお待ちしています。
記事
Tomohiro Iwamoto · 2020年5月7日

データプラットフォームとパフォーマンス-パート7 パフォーマンス、スケーラビリティ、可用性のためのECP

Cachéの優れた可用性とスケーリング機能の1つは、エンタープライズキャッシュプロトコル(ECP)です。 アプリケーション開発中に考慮することにより、ECPを使用した分散処理は、Cachéアプリケーションのスケールアウトアーキテクチャを可能にします。 アプリケーション処理は、アプリケーションを変更することなく、単一のアプリケーションサーバーから最大255台といった非常に高いレートにまで、アプリケーションサーバー処理能力を拡張できます。 ECPは、私が関与していたTrakCareのデプロイメントで長年広く使用されていました。 10年前は、主要ベンダーの1つが提供する「大きな」x86サーバーは、合計で8つのコアしか備えていなかったかもしれません。 大規模なデプロイメントの場合、ECPは、高価な大型コンピュータを使う単一のエンタープライズサーバーではなく、コモディティサーバーでの処理をスケールアウトする方法でした。 コア数の多いエンタープライズサーバーでさえ制限があったため、ECPはそれらのサーバーへのデプロイメントのスケーリングにも使用されました。 現在、ほとんどの新しいTrakCareのデプロイメントや主流ハードウェアへのアップグレードは、ECPでのスケーリングを必要としません。 現行の2ソケットx86プロダクションサーバーは、数十のコアと巨大なメモリを持つことができます。 最近のCachéバージョンでは、TrakCare(および他の多くのCachéアプリケーション)は、単一サーバーにおいて、CPUコア数とメモリの増設により、ユーザーとトランザクションの増加をサポートしうる、予測可能な線形スケーリングを備えています。 現場では、ほとんどの新しいデプロイメントが仮想化されています。その場合でも、VMは、必要に応じてホストサーバーのサイズまで拡張できます。 単一の物理ホストが提供できる以上のリソース要件である場合、ECPを使用してスケールアウトします。 ヒント: 管理とデプロイメントを簡略化するため、ECPをデプロイする前に単一サーバー内でスケーリングを実行します。 この投稿では、アーキテクチャの例とECPの基本的な仕組みを示し、ストレージに重点を置いてパフォーマンスの考慮事項を説明します。 ECPとアプリケーション開発の構成に関する特定の情報は、オンラインの「Caché分散データ管理ガイド」で得ることができ、このコミュニティには ECP学習トラックがあります。 ECPの他の主要な機能の1つは、アプリケーションの可用性の向上です。詳細については、 「Caché高可用性ガイド」のECPセクションを参照してください。 このシリーズの他の記事のリストはこちら ECPアーキテクチャの基本 ECPのアーキテクチャと働きは、概念としては単純です。ECPは、複数のサーバーシステム間でデータ、ロック、実行可能コードを効率的に共有する方法を提供します。 アプリケーションサーバーから見たデータとコードは、リモートにあるデータサーバー に保存されますが、アプリケーションサーバーのローカルメモリにキャッシュされ、最小限のネットワークトラフィックでアクティブなデータへの効率的なアクセスを提供します。 データサーバーはディスク上の永続ストレージへのデータベースの読み取りと書き込みを管理しますが、複数のアプリケーションサーバーはアプリケーション処理のほとんどを実行するソリューションの主力です。 多層アーキテクチャ ECPは多層アーキテクチャです。 処理層とそれらの役割を説明するにはさまざまな方法があります。以下は、WebブラウザベースのCachéアプリケーションを説明するときに役立つものであり、私の投稿で使用するモデルと用語です。 各層を分解するためのさまざまな方法があるかもしれませんが、ここでは私の方法を使ってみましょう。:) たとえば、CachéServer Pages(CSP)を使用するブラウザベースのアプリケーションは、プレゼンテーション、アプリケーション処理、およびデータ管理機能が論理的に分離された多層アーキテクチャを使用します。さまざまな役割を持つ「 論理サーバー」が層を構成します。 論理サーバーを個別の物理ホストまたは仮想サーバーに配置する必要はありません。コスト効率と管理性のために、一部またはすべての論理サーバーを単一のホストまたはオペレーティングシステムインスタンスに配置することもできます。 デプロイメントがスケールアップすると、ECPを使用してサーバーを複数の物理ホストまたは仮想ホストに分割できるため、アプリケーションに変更を加えることなく、必要に応じて処理ワークロードを分散できます。 ホストシステムは、容量と可用性の要件に応じて、物理的なもの、または仮想化されたものである場合があります。 以下の層と論理サーバーがデプロイメントを構成します。 プレゼンテーション層: ブラウザベースのクライアントとアプリケーション層の間のゲートウェイとして機能するWebサーバーが含まれます。 アプリケーション層: これは、ECPアプリケーションサーバーが置かれる層です。 上記のように、これは、アプリケーションサーバーがデータサーバーから分離する必要がない論理モデルであり、通常、非常に大きなサイト以外で必要なものではありません。 この層には、レポートサーバーなど、特殊な処理のための他のサーバーも含まれます。 データ層: これは、データサーバーが配置されている層です。 データサーバーはトランザクション処理を実行し、Cachéデータベースに格納されているアプリケーションコードとデータのリポジトリです。 データサーバーは、永続ディスクストレージの読み取りと書き込みを行います。 論理アーキテクチャ 次の図は、3層アーキテクチャーとしてデプロイされたブラウザーベースのアプリケーションの論理図です。 アーキテクチャは一見複雑に見えるかもしれませんが、単一のサーバーにインストールされたCachéシステムと同じコンポーネントで構成されています。ただし、論理コンポーネントは、複数の物理サーバーまたは仮想サーバーにインストールされています。 サーバー間のすべての通信はTCP/IPを介して行われます。 論理ビューでのECPの働き 上の図は、上から順に、複数の負荷分散されたWebサーバーに安全に接続しているユーザーを示しています。 Webサーバーは、クライアントと任意の処理を実行するアプリケーション層(アプリケーションサーバー)の間でCSP Webページ要求を渡し、コンテンツを動的に作成し、完成したページをWebサーバー経由でクライアントに返します。 この3層モデルでは、アプリケーション処理はECPによって複数のアプリケーションサーバーに分散されています。 アプリケーションは、データ(アプリケーションデータベース)をアプリケーションサーバーに対してローカルであるかのようにシンプルに扱います。 アプリケーションサーバーがデータを要求すると、ローカルキャッシュからの要求を満たそうとしますが、それができない場合は、ECPが自身のキャッシュからの要求を満たすことができるデータサーバーに必要なデータを要求します。また、それができない場合は、ディスクからデータをフェッチします。 データサーバーからアプリケーションサーバーへの応答には、そのデータが格納されたデータベースブロックが含まれます。 これらのブロックが使用され、次にアプリケーションサーバーにキャッシュされます。 ECPは、ネットワーク全体のキャッシュの一貫性を自動的に管理し、変更をデータサーバーに反映します。 クライアントはローカルにキャッシュされたデータを頻繁に使用するため、迅速な応答を得ることができます。 データが既にローカルキャッシュにある可能性があるため、デフォルトでは、Webサーバーは優先アプリケーションサーバーと通信し、同じアプリケーションサーバーが関連データに対する後続の要求にサービスを提供することを保証します。 ヒント: Cachéのドキュメントで詳述されているように、アプリケーションサーバーでのキャッシュの利点に影響を与えるラウンドロビンや負荷分散スキームでユーザーをアプリケーションサーバーに接続することは避けてください。 同じユーザーまたはユーザーグループが同じアプリケーションサーバーに接続したままになることが理想的です。 このソリューションは、ユーザーのダウンタイムなしで、Webサーバーを追加することによってプレゼンテーション層を、また、アプリケーションサーバーを追加することによってアプリケーション層を拡張できます。 データ層は、データサーバーのCPUとメモリを増やすことによって拡張されます。 物理アーキテクチャ 次の図は、3層論理アーキテクチャの例と同じ3層デプロイメントで使用される物理ホストの例を示しています。 物理ホストまたは仮想化ホストは、ホスト障害が発生した場合や定期メンテナンスに備えて、100%容量のn + 1またはn + 2モデルを使用して各層にデプロイされることに注意してください。 ユーザーは複数のWebサーバーとアプリケーションサーバーに分散しているため、単一のサーバーの障害は少数のユーザーに影響を与え、ユーザーは残りのサーバーの1つに自動的に再接続します。 データ管理層は、たとえば1つ以上のストレージアレイに接続されたフェイルオーバークラスター(たとえば、 仮想化HA、InterSystemsデータベースミラーリング、または従来のフェイルオーバークラスタリング)によって高い可用性を持ちます。 ハードウェアまたはサービスに障害が発生した場合、クラスタリングは、クラスター内の残りのノードの1つでサービスを再起動します。 その他の利点として、ECPには復旧力が組み込まれており、データベースノードクラスターのフェールオーバーが発生した場合でもトランザクションの整合性が維持されるので、アプリケーションユーザーは、フェールオーバーと自動復旧が完了するまで処理の一時停止を観察しますが、ユーザーは切断されずにシームレスにトランザクションを再開することができます。 同じアーキテクチャを仮想化サーバーにマッピングすることもできます。たとえば、VMware vSphereを使用してアプリケーションサーバーを仮想化できます。 ECPキャパシティプランニング 上述のように、データサーバーはディスク上の永続ストレージへのデータベースの読み取りと書き込みを管理しますが、複数のアプリケーションサーバーはアプリケーション処理のほとんどを実行するソリューションの主力となるものです。 システムリソースのキャパシティプランニングを実施する際の重要な概念の要約は次のとおりです。 データサーバー (データベースサーバーと呼ばれることもあります)は通常、アプリケーション処理をほとんど実行しないため CPU要件は低くなります。ところが、このサーバーはストレージIOの大部分を実行するため、非常に高いストレージIOPSとなる可能性があります。これらは、 データベースの読み取りと書き込み、およびジャーナルの書き込み(ジャーナルIOについては後で詳しく説明します)です。 アプリケーションサーバーはほとんどのアプリケーション処理を実行するため高いCPU要件がありますが、ストレージIOはほとんどありません。 一般的に、ECPサーバーのCPU、メモリ、およびIOの要件は、高可用性に合わせてN + 1またはN + 2サーバーを考慮しながら、非常に大規模な単一サーバーソリューションのサイズを決定する場合と同じルールを使用して決定します。 基本的なCPUおよびストレージのサイジング: My_Applicationがアプリケーション処理にピーク72 CPUコアを必要とし(ヘッドルームも考慮に入れてください)、書き込みデーモンサイクル中に2万回の書き込みが必要であり、持続的ピーク1万回のランダムデータベース読み取りが必要であると想像してください。 仮想サーバーまたは物理サーバーの、大まかなサイジングは次のとおりです。 4 x 32 CPUアプリケーションサーバー(3サーバー+ HA用に1)。 低いIOPS要件。 2 x 10 CPUデータサーバー(HA用にミラーリングまたはクラスター化)。 低レイテンシIOPS要件は、 2万回の書き込み、1万回の読み取り、およびWIJとジャーナルです。 データサーバーはほとんど処理を実行していませんが、システムプロセスとCachéプロセスを考慮して、8〜10 CPUのサイズに設定されています。 アプリケーションサーバーのサイズは、物理ホストごとのベストの価格性能比に基づいて、および/または可用性を考慮して設定することができます。 スケールアウトすると効率が多少低下しますが、通常はサーバーブロックに処理を追加して、スループットの線形に近い増加を期待できます。 制限は、ストレージIOで最初に生じる可能性が高くなります。 ヒント: HAで通常するように、ホスト、シャーシ、またはラックの障害の影響を考慮します。 VMWareでアプリケーションサーバーとデータサーバーを仮想化する場合、必ずvSphere DRSとアフィニティルールを適用して処理負荷を分散し、可用性を確保します。 ジャーナル同期IO要件 ECPデプロイメントのキャパシティプランニングに関するその他の考慮事項は、より高いIOが必要であり、ジャーナルの同期(別名 ジャーナル同期)のためより高いIOが必要であり、データサーバー上のジャーナリングのスケーラビリティを維持するため、非常に厳格なストレージ応答時間要件を課することです。 同期要求の発行により、ジャーナルの最後のブロックへの書き込みがトリガーされ、データの耐久性が確保されます。 アプリケーション次第ですが、高いトランザクションレートの一般的な顧客サイトでは、ECP以外の構成でのジャーナル書き込みIOPSが毎秒2桁台で見られることがよくあります。 ビジー状態のシステムでECPを使用すると、ECPによってジャーナル同期が強制されるため、ジャーナルディスク上で100〜1,000の書き込みIOPSを確認できます。 ヒント: mgstatを表示するか、 pButtons でmgstatを確認すると、ストレージIOリソース計画で考慮されるJrnwrts(ジャーナル書き込み)が表示されます。 ECPデータサーバーには、mgstatに表示されないジャーナルディスクへのジャーナル同期書き込みもあります。これらを確認するには、iostatなどを使用して、ジャーナルディスクのオペレーティングシステムメトリックを確認する必要があります。 ジャーナル同期とは何ですか? ジャーナルの同期は次の場合に必要です。 データサーバーで障害が発生した場合のデータの耐久性と回復性を保証します。 また、アプリケーションサーバー間のキャッシュの一貫性を確保するためのトリガーでもあります。 非ECP構成では、Cachéデータベースへの変更はジャーナルバッファ(128 x 64Kバッファ)に書き込まれ、ジャーナルデーモンによって、バッファが一杯になったら、あるいは2秒ごとに、ディスク上のジャーナルファイルに書き込まれます。 Cachéはバッファ全体に64kを割り当て、これらは破棄されたり再作成される事なく常に再利用され、またCachéは単に終了オフセットを追跡します。 ほとんどの場合(大量の更新が一度に行われない限り)、ジャーナルの書き込みは非常に小さなものです。 ECPシステムでは、ジャーナルの同期も行われます。 ジャーナル同期は、現在のジャーナルバッファの関連部分をディスクに再書き込みして、ジャーナルが常にディスク上で常に最新になるようにすることとして定義できます。 そのため、ジャーナル同期リクエストにより、同じジャーナルブロックの一部(サイズが2kから64kの間)が、複数回、再書き込みされる事があります。 ジャーナル同期要求をトリガーできるECPクライアント上のイベントは、更新(SETまたはKILL)またはLOCKです。 たとえば、SETまたはKILLごとに、現在のジャーナルバッファがディスクに書き込まれます(または書き換えられます)。 非常にビジーなシステムでは、ジャーナル同期を1回の同期操作で複数の同期リクエストにバンドルまたは遅延できます。 ジャーナル同期のキャパシティプランニング スループット維持のために、ジャーナル同期の平均書き込み応答時間は次のようにする必要があります。 <= 0.5 ms、最大<= 1 ms 詳細については、この投稿のIO要件の表を参照してください。パート6-CachéストレージIOプロファイル。 ヒント: CachéデータベースミラーリングをECPで使用する場合、ジャーナル同期は、プライマリミラーノードとバックアップミラーノード・ジャーナルディスクの両方に適用されます。 ミラー構成のルールは、両方のノードのストレージIOを同等に構成することなので、これは問題ではないでしょう。 お使いのシステムの特定のIOメトリックを検証する必要があります。このセクションの目的は、非常に厳密な応答時間の要件があるということを理解し、メトリックを見つける場所を把握することです。 ヒント: Cachéシステムでは、Red Hat LinuxとSuSE XFSファイルシステムが推奨されます。 ただし、テストでは、ext4はジャーナル同期などの小さな書き込みに対してより高いIOPS機能を提供することが明らかになっています。 ジャーナルディスクにはext4の使用を検討してください。 概要 この投稿はECPと、キャパシティプランニング中に考慮するその他のメトリックに関するオリエンテーションです。 近い将来、非常に大規模なシステムで実施されたCachéとECPに関する最新のベンチマークの結果を皆さんと共有できることを願っています。 いつものように、ご質問やご意見がある場合はどうぞお気軽にご連絡ください。 ツイッター @murray_oldfield