default eye-catch image.

ACID特性 (ACID Property)

経験的にトランザクションの性質を知っている気になっているけれど、 ではACID特性のそれぞれを言葉で説明してみて, と言われると難しい. おそらくAtomicityだけをACID特性と言ってきた気がする. Wikipediaから. トランザクション分離レベルもこの際まとめておく. [arst_toc tag=\"h4\"] 不可分性(Atomicity) トランザクションに含まれるタスクが複数ある場合、全てのタスクが完全に完了するか、または全く実行されないか、いずれかであることを保証すること。 口座Aから口座Bに対して1万円送金する. 口座Aから1万円を引くタスクと口座Bに1万円を足すタスクの片方だけが実行されるとおかしなことになる. 両方のタスクが成功して取引引きが完了するか、両方のタスクが失敗して取引が失敗するかいずれか 一貫性 (Consistency) トランザクションの開始から終了までの間、操作対象のデータが正常範囲内に収まることを保証すること. 口座Aから口座Bに送金するケースで、口座Aに1万円しかないのに2万円送金しようとして一時的に口座Aが-1万円になることは一貫性に反する. 一貫性に反するイベントが発生したときにトランザクションを終了する. 独立性 (Isolation) トランザクション内の複数の操作は外部からは隠蔽されることを表す. 外部からはトランザクションの入りと出だけを知ることができる. 口座Aから口座Bに送金するケースで、口座Aから口座Bに1万円を送金する際に、中間状態として口座Aから1万円を減らしただけの状態が発生するものとする. 外部からは中間状態は見ることができず、口座Aから1万円が減り口座Bに1万円が足された状態のみを知り得る. 永続性 (Durability) DBMSの管理上の話. トランザクションが完了した場合,障害を受けたとしても完了後の状態を保持できることを表す. 通常、トランザクション操作はトランザクションログとしてストレージに記録される. トランザクションログはトランザクションの履歴で巻き戻したりできる. システムに異常が発生した場合、トランザクションんログを使って異常発生前の状態まで復旧できる. ACIDの現実 ACID特性を厳密に実装しようとすると、より広範囲のデータにアクセスする必要が発生する. 広範囲のデータにロックを掛けたり更新したりなどでパフォーマンスが落ちる. 実際はある程度妥協して実装される. ACID特性を実現する処理自体が失敗する可能性もある. ファイルシステムやバックアップ方式の工夫により冗長化する. 全ての処理を一度に実行することが求められるが、それは現実的には難しい. ログ先行書き込みとシャドウページング. トランザクション分離レベルを設定することで、トランザクションの並列実行時の厳密性とパフォーマンスのトレードオフを制御できる トランザクション分離レベル Dirty read. トランザクションAとトランザクションBが並列実行. AはBの途中の状態を見ることができる. Non-repeatable read (Fuzzy read). トランザクションAとトランザクションBが並列実行. Aが同じデータを2度読む. 1度目はBが書いていない. 2度目はBが書いている. Aから見て1度目と2度目のデータが異なるか消えているように見える. Phantom read. Non-repeatable readと似ているが、特にAの繰り返し読み込みの間にBがデータを挿入し、Aから見て突然新しいデータが出現したように見えること. 微妙な違いだが、過去から現在に渡って存在しているものの過去の状態が見えることと、過去存在していないが現在見えることは異なり、それぞれ名前がついている. ACID特性の厳密な実装にはパフォーマンス劣化とのトレードオフがあるため、 概念的に、使う側がトレードオフをコントロールできるようになっている. それがトランザクション分離レベル. あくまで概念のためDBMSによってその扱いが異なる. 分離レベル Dirty read Non-repeatable read Phantom read Read Uncomitted発生する発生する発生する Read Comitted発生しない発生する発生する Repeatable Comitted発生しない発生しない発生する Serializable発生しない発生しない発生しない

default eye-catch image.

Auroraの機能など

知識がないのに経験だけ積んだって力にならないんだよね。という話を聞いて腑に落ちた。 資格を取るために学んだことは、日々悩み考える色々な出来事を説明するための武器になる。 今自分は何をやろうとしていているのか、経験して後から回想するのでは余りに効率が悪い。 今回はAurora。やはり高いので個人では手が出ないのだけれど、 それなりの仕事であれば第1選択になり得る。 RDSと比較して圧倒的に高機能で運用時に困りそうなユースケースが通常の機能として既に備わっている. 参考書を1周したので、(著作権侵害にならないように)要約して自分の言葉でまとめていく。 [arst_toc tag=\"h4\"] クォーラムモデル コンピューティングリソースとストレージが分離している. コンピューティングとストレージを独立して管理する. コンピューティングリソース、ストレージ共に3AZに分散してレプリケートする. 1AZにコンピューティングリソース1台、ストレージ2台. 6台のストレージのうち2台が故障しても読み書き可. 3台が故障すると書き込みが不可となるが読み込み可. RDSはスタンバイレプリカとリードレプリカが別扱いだが Auroraはスタンバイ、リード共に共通. プライマリ、レプリカ、ストレージ(ボリューム)をセットでクラスタと呼ぶ. 可用性 読み書き可能なクラスターエンドポイント. 読み取り専用エンドポイント、任意のインスタンスにつなぐエンドポイントなどなど. クラスタ内の1台が読み書き用, 他は読み取り専用なので、読み書き用が落ちたときに読み取り専用が読み書き用に昇格する. これがフェイルオーバーの概念. クラスターエンドポイントに繋いでおくと、エンドポイント先で障害時に勝手にフェイルオーバーが発生する レプリカにはフェイルオーバー優先度をつけられる. 優先度が高い方が優先的にフェイルオーバー先になる. 同じだとインスタンスの大小で決まる. 多くの場合、フェイルオーバーの時間はRDSよりも短い. 通常、プライマリにのみキャッシュが効く. フェイルオーバーでキャッシュヒットしなくなる. クラスターキャッシュ管理をONにするとフェイルオーバー時に引き継がれる. 複数のリージョンに跨ってクォーラムモデルを配置するAuroraグローバルデータベース. DR対策. リージョン間のデータコピーは1秒未満. 複数のリージョンに跨ってクラスタを配置するクロスリージョンレプリケーション. DR対策. レプリカ間のデータコピーに時間がかかる. 通常クラスタ内の1台が読み書き可能で他は読み取り専用だが、全てを読み書き可能にできる. パフォーマンス 書き込み性能を上げるにはインスタンスサイズを上げる. Auroraレプリカはスタンバイレプリカ、リードレプリカを兼ねる. リードレプリカとして使うと読み込み性能が上がる. 読み込みエンドポイントは全ての読み込み用レプリカを代表する. アプリ側からは1個だが中は数台. Aurora AutoScaling. 読み込みクラスタのCPUまたは接続数が閾値以下になったときに自動スケールする. Aurora Serverless. インスタンス数,インスタンスサイズを自動スケールする. 未使用時に勝手に落ち,高負荷時に勝手に上がる. Aurora Serverlessは, 前提として利用頻度が少なくほとんど未使用だが、変化するときは大きく変化する、というアプリに適している. スケールアップは限界がある. つまり重量級のクエリの高速化には限界がある. スケールアウトはより柔軟なので多数のクエリの同時実行はより簡単に対応できる. セキュリティ 基本的にRDSと同様. VPC内に設置する. NACL、SGを使ってアクセス制御する. データ格納時・転送時に暗号化する. IAMロールを使ったクレデンシャルレス化. Auroraの監査機能は Advanced Auditing. 記録するクエリ種別を選択できる. CloudWatchLogsに転送可. コスト Auroraレプリカ1台ごとの稼働時間で課金. Aurora Serverlessはキャパシティユニット単位で課金. (cf.DynamoDB) RDSはインスタンスとストレージが密結合しているためストレージ容量はインスタンスに紐づく. インスタンス作成時に確保した量に課金. Auroraはストレージが分離しているためAuroraレプリカとは関係なく使った容量だけ重量課金. データを削除して未使用領域が出ると自動的に課金対象が減る. 通信はVPC外へのアウトバウンドにのみ課金. メンテナンス メンテナンスウインドウまたは即時で実行. Auroraではクラスター単位でパラメータを管理する.設定はクラスタ内のレプリカ全てに適用される. インスタンス単位のパラメータ管理もできる. ZDP(Zero Day Patch). ベストエフォートでダウンタイムなしのパッチ適用をおこなう. パッチ適用中も接続が維持される. バックアップ システムバックアップはメンテナンスウインドウで日時で行われる. データバックアップは継続的かつ増分的な自動バックアップ. 保持期間中の任意の時点へ復元できる. (PITR) データバックアップの保持期間は1日-35日. 0日(無効)には出来ない. S3に保存される. 手動でスナップショットを取得可能. システムバックアップ、データアップバック共に復元先は新しいAuroraクラスター. 保持期間の任意の時点を指定する. Auroraクローン. ストレージではなくコンピューティング部分のみをコピーする. リードレプリカ複製による読み取り性能向上. 1回でも書き込みしようとするとストレージ部分がコピーされる. データエクスポート、分析等読み込み専用のタスクに使う. Aurora MySQLでのみバックトラックを使用可能. 最大24時間前までSQL操作を遡れる. S3エクスポート. RDSはスナップショット作成操作だが, AuroraはSQLクエリ操作. モニタリング CloudWatchによるメトリクス監視. 拡張モニタリングによる詳細メトリクス監視. コンピューティングに関わる(CPU,メモリ等)をインスタンス単位のメトリクスとしてCloudWatchLogsで監視. ストレージに関わるクラスタ単位のメトリクスとしてCloudWatchLogsで監視. 監視が上手くいっているかを確認するため、障害を自力でシミュレートできる. 障害挿入クエリ. DBインスタンス,Auroraクラスタ,ディスクの障害. CloudWatchLogsよりもリアルタイム性があるデータベースアクティティストリーム. Amazon Kinesisにリアルタイムで入る. Kinesisに入ったデータストリームをElasticSearch等で可視化する. その他 Aurora MySQL. SQLからLambda関数を呼べる. Aurora MySQL. SQLからSageMakerエンドポイントを呼べる.

default eye-catch image.

RDSの機能など

参考書を1周した. 普段RDSを道具として使っているだけでは経験しない知識を得ることができた. インフラ系の仕事をしないと使わない可能性がある知識もあるが、アプリケーションエンジニアとしては、 RDSがここまでやってくれると知っていることで無駄な機能を作り込んだり、余計な心配をしなくて済む. [arst_toc tag=\"h3\"] 可用性 スケールアウトすることで何が冗長化されるのか. いざフェイルオーバーが発生したときどういう挙動になるのか. まとめ プライマリインスタンスとスタンバイレプリカを別AZに配置することで可用性を得る プライマリとスタンバイの間で常にデータ同期がおこなわれる プライマリに障害が発生した場合スタンバイにフェイルオーバーすることでDB接続を継続する スタンバイはトラフィック処理しない. 読み取り性能を上げるためにはリードレプリカを追加する スタンバイがある場合、スタンバイを対象にRDSのスナップショット取得がおこなわれ、プライマリのトラフィックに影響を与えない マルチAZの場合、スタンバイとのデータ同期によりシングル構成よりも書き込み・コミットでわずかにレイテンシが上がる AZの変更 プライマリのスナップショットを作成後、セカンダリとして復元し同期 AZ変更時はプライマリのパフォーマンスに影響する フェイルオーバー RDSの外からはエンドポイントでつなぐ プライマリに障害が発生した場合、エンドポイントの先が自動的にスタンバイにつなぎ変わる 切り替えにかかる時間は60秒-120秒. DNSキャッシュのTTLを60秒以内にしておくことが推奨されている AWSコンソールから手動でフェイルオーバー時の挙動を確認できる パフォーマンス スケールアップで何が良くなるのか。スケールアウトではどうか。 スケールアップさせることを前提にできるのか。 まとめ データベースのパフォーマンスは主にデータの読み書きのパフォーマンス 汎用SSD.3IOPS/GB. バースト(一時的に)100-10,000IOPS. プロビジョンドIOPS. 常に1,000-30,000IOPS. ストレージ容量の残量が10%以下の状態が5分以上続いた場合、5GBまたは割り当て容量の12%のどちらか大きい方が自動的に追加される 容量を頻繁に拡張できるわけではない.1度変更すると6時間変更できない. Storage Auto Scalingに頼るべきではない リードレプリカ 読み込み性能は、プライマリを複製したリードレプリカを増やすことで対応. トラフィックがリードレプリカに分散される 書き込み性能は、スケールアップにより対応 プライマリとリードレプリカの同期は非同期. 微妙に異なる. プライマリのスナップショットからリードレプリカが作成され複製される.従って作成直後は異なる リードレプリカは最大5個 プライマリとリードレプリカのインスタンスサイズは異なっていても良い 手動でリードレプリカをプライマリに昇格可能 マルチAZ可能. DR対応で別リージョンにリードレプリカを作成可能. リードレプリカのエンドポイントはそれぞれ異なる.負荷分散する場合、Route53等で1つのDNSレコード先を分散させる RDBMSごとの制約 SQLServerの場合、特定エディション以上でリードレプリカを使用可能 SQLServerの場合、マルチリージョン、マルチAZリードレプリカを作成不可 Oracleの場合、特定エディション以上でリードレプリカを使用可能 Oracleの場合、OracleのActiveDataGuargeにより同期がおこなわれる RDS Proxy アプリケーションがDBにアクセスする際、一度作成したコネクションをプーリングして使い回す機能 昔、LambdaからRDSにつなぐ際、コネクションがプールされずすぐに最大接続数を超過していたがこれで解決した RDS Proxyはプライマリインスタンスのみ対応 セキュリティ アプリケーションが個人情報の暗号化を意識する必要があるのか。RDSが透過的に面倒を見てくれるのか。 まとめ RDSを設置するVPCには少なくとも2つのサブネットが必要 VPCのACL、SGでアクセス制御する SGの送信元にはSGを指定できる.SGとSGの接続を定義できる 暗号化 データ格納時の暗号化と通信時の暗号化の2つ KMSのキーを使用して格納するデータを暗号化. KMSキーを別管理することでRDS内のデータが漏れても保護できる KMS暗号化は透過的におこなわれる. アプリケーションは特に意識しなくても良い 暗号化の対象は以下の通り DBインスタンスに格納するデータ 自動バックアップ リードレプリカ スナップショット ログファイル DBインスタンス作成時にのみ暗号化可能. 未暗号化インスタンスのスナップショットを作成して復元時に暗号化 プライマリだけ、リードレプリカだけ、のように非対称に暗号化することはできない KMSはリージョンを跨げないためリージョン間スナップショットを取る場合はコピー先のリージョンでコピー元とは異なるKMSキーを指定する必要がある SSL/TLSにより伝送中データの暗号化 AWSからルート証明書をDLしアプリケーション側でSSL/TLS通信時に取得したルート証明書を使う ルート証明書は定期的に失効する. 都度ダウンロードして更新すること IAMによるDBアクセス認証 MySQLとPostgreSQLに限り、IAMを使用したDBアクセス認証を利用できる. RDSへアクセス可能なIAMロールを作成. アプリケーション側は作成されたIAMロールを使ってRDSにアクセス アプリケーション側で接続情報を管理しなくてもよい 監査ログ DBエンジンがもつ監査ログ機能を利用できる. 監査ログはCloudWatchに転送され、管理・監視できる コスト アプリケーション側をチューニングする人的コストと、インスタンスに使うコスト。 何に料金がかかるということを把握して、アプリケーション側でやるべきこと/AWS側に振ることを意識する. まとめ RDSで発生するコストはインスタンス料金、ストレージ料金、データ通信料金 インスタンス料金 コストは1秒単位.ただし1時間未満は最低10分から. 2AZに配置した場合、リードレプリカを設置した場合、インスタンス数が2倍になるのでインスタンス料金も2倍になる DBエンジンの種類によって若干インスタンス料金が異なる.MySQL<postgreSQL<oracle 1年または3年の前払い制(リザーブドインスタンス)により割安になる.損益分岐点あり インスタンスを停止するとインスタンス料金の課金は止まる.ただし1週間止めておくと自動的に起動してしまう. ストレージ料金 インスタンスを止めていてもストレージ料金の課金は止まらない 利用中のストレージサイズと同サイズまでのバックアップには課金されない.それを超えたところから課金される.ただし超えた分は安い データ転送料金 RDSへのINは無料 RDSからVPC外部、またはインターネットへの通信は課金される. 通常VPC内部でEC2とやりとりする場合は無料だが、VPC外部とやりとりする場合注意 メンテナンス 作ったアプリが保守フェーズに移行した後、アプリケーション側は何を意識しなければならないか. まとめ AWSが実施するメンテナンスの実行時間を指定できる.(メンテナンスウインドウ) 22:00-06:00の間の30分. 大きなメンテナンスの場合1時間かかる場合がある.余裕をみて1時間設定する メンテナンスウインドウ期間中、いくつかのメンテナンスによりインスタンスが一時的にオフラインになる メンテナンス種別は「必須」と「利用可能」. 「必須」は無期限延期できない. 「利用可能」はできる. アプリケーションの動作に影響がありそうなものは開発環境で事前に検証すること マルチAZのメンテナンス まずスタンバイについてメンテナンスを実行 スタンバイをプライマリに昇格. 降格した元プライマリにメンテナンスを実行.そのままスタンバイになる 全体としてインスタンスがオフラインになることがない. ストレージ追加、インスタンスタイプの変更は任意またはメンテナンスウインドウ DBエンジンのアップグレード メジャーバージョンアップはユーザ自身が実施 マイナーバージョンアップは設定次第で自動でやってくれる. 手動でも可. パラメータグループ 設定値(パラメータ)のグループ. 例えばMySQLのconfに書くような設定値が集まったもの. DBエンジンごとに様々なパラメータが存在する デフォルトパラメータグループ ユーザは変更できない. ユーザが独自のパラメータグループを作成しデフォルトパラメータをオーバーライド すぐに適用される「動的パラメータグループ」.再起動が必要な「静的パラメータグループ」 追加設定はオプショングループ.デフォルトのパラメータは変更できず,ユーザが作成してオーバーライド バックアップ これも, 保守フェーズに移行した後アプリケーション側で何を意識しないといけないか. 自動バックアップと手動バックアップ 自動バックアップ 自動的にスナップショットを保存. 保存日数はデフォルト7日.0(無効)-35日. スナップショットは不可視のS3に保存される. 初回のスナップショットはフル. 2回目以降は差分. バックアップはメンテナンスウインドウで作成される. シングルAZの場合一時的にオフラインになる. マルチAZの場合オフラインにならない 手動バックアップ 任意のタイミングでバックアップできる. 手動バックアップは自動的に削除されない. DR目的で別リージョンへのスナップショットコピー 別リージョンに手動でスナップショットをコピーできる 暗号化用KMSキー、オプショングループは自動でコピーされないので自力でコピー先に作る 別アカウントとスナップショット共有 手動バックアップしたスナップショットを別アカウントと共有できる 暗号化済みの場合、KMSキーを共有先にアクセス許可する 暗号化していない場合、格納された個人情報にアクセス可能となる スナップショットの復元 既存のRDSインスタンスに復元できない.新しいRDSインスタンスを復元する エンドポイントが変わるのでアプリケーション側の再設定が必要 パラメータグループはインスタンスに紐づくため復元時に復元元のパラメータグループを使用する PITR(ポイントインタイムリカバリ) スナップショットとは別にトランザクションログがS3に5分単位で保存される スナップショット復元と合わせて最短で5分前までの状態に復元が可能. S3へのエクスポート スナップショットからS3にエクスポートできる 不可視のS3ではなく、Amazon Parquet形式でS3バケットにデータをエクスポートできる Athena、Redshift等別サービスからS3上のファイルを検索、分析できる モニタリング 作ったアプリがショボすぎて速度が出ない! ピンチを救うAWSの機能. 保守フェーズ移行, 劣化やユーザ数増加により受けた影響の調査. 他. インスタンスが効率的に使われているかを調べるためにリソース使用状況を監視できる CloudWatchにメトリクスが展開される. CloudWatchAlarmによりメトリクスの変化に伴ってSNS通知などアクションを実行できる DBエンジンが出力するログはCloudWatchLogsに転送できる ログに含まれる特定のエラー文字列を見つけてSNS通知するなどのユースケース 拡張モニタリングにより詳細なリソースデータを監視できる. パフォーマンスインサイト. パフォーマンスに関するデータを可視化する. ユーザ自身が可視化ツールを用意しなくてもある程度は確認できる スロークエリ、実行計画の確認などができる. パフォーマンスチューニングの初手に使える フェイルオーバーや再起動などをトリガーとしてSNS通知できる

default eye-catch image.

PostgreSQL ダブルクォートで括られたDB名は大文字と小文字が区別される. FATAL: database “” does not exist .

pg_dumpコマンドを使って作られたdumpファイルをpg_restoreでリストアしようとしたところ、 DB名が無いと怒られた. dbnameという名前のDBは確かにあるはずなのに. psql: FATAL: database \"DBNAME\" does not exist 理由は、dumpファイル内で、ダブルクォートで括られた\"DB名\"に対して操作をしようとしていたから. \"DBNAME\"に対する操作は全て大文字の\"DBNAME\"に対する操作を表していて、 \"dbname\"と\"DBNAME\"は区別される。 CREATE DATABASE \"DBNAME\" OWNER = postgres TEMPLATE = template0 ENCODING = \'UTF8\' LC_COLLATE = \'C\' LC_CTYPE = \'C\'; 識別子を小文字で統一できるならそうした方が良い. 既に何かがあり出来ない状況で出くわしたなら、大文字と小文字を意識する必要がある. 以下PostgreSQLの公式ドキュメント. PostgreSQLにおいて、識別子をダブルクォートで括ると、大文字小文字が区別されるようになる. 例えば、以下のようにCREATE DATABASEを叩くと、全て大文字の DBNAME というDBが作られる. PostgreSQL 12.4文書/ 4.1.1. 識別子とキーワード

default eye-catch image.

Terraformを使ってAWSにWebアプリケーションの実行環境を立てる (EC2立てるまで)

Webアプリケーション実行環境をIaCで管理したい. Terraformでクラウド構成を作ってAnsibleでミドルウェアをインストールしたい. BeanstalkやLightsailのようなPaaSではなくTerraformを使ってVPCから自前で作ってみる. この記事はEC2を立てるまでが範囲. 次の記事でAnsibleを使って立てたEC2にミドルウェアをインストールする. [arst_toc tag=\"h4\"] この記事で紹介する範囲 この記事ではTerraformを使ってAWS上に以下の構成を作るまでを書いてみる. とはいえTerraformの習得が8割くらいのモチベなので実用性はあまり重視しない. サブネットをプライベートとパブリックに分けてみたい. プライベートにDB(MySQL), パブリックにWebサーバ(nginx). ひとまずALBは配置しない. Terraformの導入 Ansibleもそうだけれども, アプリを保守している期間って割と長いもので、 その間, 構成管理ツール側のバージョンが上がってしまう傾向がある. そうすぐに古い書き方が使えなくなることはないが, 警告が出まくって気分がよくない. 構成管理ツールの古いバージョンを残しておきたい, どのバージョンを使うか選びたい, という期待がある. rbenvやpyenvのようにTerraform自体のバージョンを管理するtfenvをインストールしておき, この記事を書いた日の最新である 1.0.3 をインストールすることにする. $ brew install tfenv $ tfenv --version tfenv 2.2.2 $ tfenv list-remote .1.0-alpha20210714 1.1.0-alpha20210630 1.1.0-alpha20210616 1.0.3 1.0.2 1 ... $ tfenv install 1.0.3 ... $ tfenv list 1.0.3 $ tfenv use 1.0.3 Switching default version to v1.0.3 Switching completed $ terraform version Terraform v1.0.3 on darwin_amd64 git secretsの導入 AWSのcredentialsなどを誤ってcommitしてしまう事故を防ぐためにgit secretsを導入する. commit時に内容を検証してくれて, もしそれらしきファイルがあればリジェクトしてくれる. どこまで見てくれるのか未検証だけれども入れておく. Laravelの.env_staging等に書いたcredentialsがどう扱われるか後で検証する. $ brew install git-secrets $ git secrets --install ✓ Installed commit-msg hook to .git/hooks/commit-msg ✓ Installed pre-commit hook to .git/hooks/pre-commit ✓ Installed prepare-commit-msg hook to .git/hooks/prepare-commit-msg $ git secrets --register-aws OK ディレクトリ構成 勉強用の小さな環境を作るのだけれども, 今後の拡張性については考慮しておきたい. 割と規定されている傾向があるAnsibleと比較して,Terraformは自由な印象. 以下の記事を参考にさせて頂きました. Terraformなにもわからないけどディレクトリ構成の実例を晒して人類に貢献したい iac ├── dev │ ├── backend.tf │ ├── main.tf -> ../shared/main.tf │ ├── provider.tf -> ../shared/provider.tf │ ├── versions.tf -> ../shared/versions.tf │ ├── terraform.tfvars │ └── variables.tf -> ../shared/variables.tf └── shared ├── main.tf ├── provider.tf ├── variables.tf └── modules ├── vpc │ ├── eip.tf │ ├── internet_gateway.tf │ ├── nat_gateway.tf │ ├── routetables.tf │ ├── subnet.tf │ ├── vpc.tf │ ├── outputs.tf │ └── variables.tf └── ec2 ├── ec2.tf ├── keypair.tf ├── network_interface.tf ├── security_group.tf ├── outputs.tf └── variables.tf tfstateの保存先の定義 tfstate は Terraformが管理しているリソースの現在の状態を表すファイル. terraformは「リソースを記述したファイル」と「現在の状態」の差分を埋めるように処理を行うが, いちいち「現在の状態」を調べにいくとパフォーマンスが悪化するため, ファイルに保存される. (確かにAnsibleは毎回「現在の状態」を調べにいっているっぽく,これが結構遅くて毎回イライラする) デフォルトだとローカルに作られるが, それだとチーム開発で共有できないので, S3等に作るのが良くあるパターン. Terraformでは\"バックエンド\"という概念で扱われる. \"バックエンド\"を以下のように記述する. バックエンドの定義はterraformの前段にあり, S3 bucketとDynamoDB tableを手動で作っておく必要がある. 変数を使うことができないのでハードコードしないといけない. 議論があるらしい. key,secretを書く代わりにprofileを書くことで, 構成管理可能になる. (同じprofile名をチームで共有しないといけない...) backendをS3にする際にS3のbucketをどう作るか問題はいろいろ議論があるようで, いずれ以下の記事を参考にしてよしなにbucketを作れるようにしたい. dynamodb_tableを設定すると、そこにロックファイルを作ってくれるようになる. 多人数で同じ構成管理を触るときに便利. Backend の S3 や DynamoDB 自体を terraform で管理するセットアップ方法 terraform { backend \"s3\" { region = \"ap-northeast-1\" profile = \"ikuty\" bucket = \"terraform-state-dev\" key = \"terraform-state-dev.tfstate\" dynamodb_table = \"terraform-state-lock-dev\" } } credentialsの書き方 ルートにある terraform.tfvarsというファイルを置いておくと、 そこに記述した内容を変数に注入することができる. \"注入\"という言葉で良いのか不明だが、定義した変数の初期値を設定してくれる. credentialsを構成管理に登録するのはご法度. terraform.tfvarsを構成管理外として何らかの方法で環境にコピーする. 多くのツールで採用されている「よくあるパターン」. 他に,applyコマンドに直接渡したり, 環境変数で指定したりできるが, Terraform公式は.tfvarsを推奨している. aws_access_key_id = \"AKI*****************\" aws_secret_access_key = \"9wc*************************************\" aws_region = \"ap-northeast-1\" providerの定義 プロバイダとは, 要は\"AWS\",\"Azure\",\"GCP\".. のような粒度の何か. Terraformは結構な種類のプロバイダに対応していて「どのプロバイダを使うか」を定義する. 今回はAWSを使う. dev.tfvarsに記述しておいたCredentialsを変数で受けて設定する. 以下,変数の定義方法, デフォルト値の設定方法を示している. .tfvarsに記述した同名の変数について,terraformが値を設定してくれる. variable \"aws_access_key_id\" {} variable \"aws_secret_access_key\" {} variable \"aws_region\" { default = \"ap-northeast-1\" } provider \"aws\" { access_key = \"${var.aws_access_key_id}\" secret_key = \"${var.aws_secret_access_key}\" region = \"${var.aws_region}\" } エントリポイント Terraformのエントリポイントはルートに置いた\"main.tf\". ディレクトリ構成を凝らないのであれば、main.tf に全てをベタ書きすることもできる. 今回、devやstg, prod のような環境ごとにルートを分ける構成を作りたいのだが、 main.tf 自体は環境ごとに差異が無いことを前提にしている. ./shared/main.tf というファイルを作成し、 各環境ごとの main.tf を ./shared/main.tf の Symbolic Link とする. main.tf でリソースの定義はおこなわない. 同階層の./modules にモジュール定義があるが, main.tf は ./modules以下の各モジュールに変数を渡すだけ. VPCの作成とEC2の作成を各モジュールに分割した. 各モジュールのOutputのスコープはモジュールまでなので、 例えばEC2モジュールからVPCモジュールのVPC IDを直接受け取れない. main.tf はモジュールの上に位置するため、このようにモジュール間で変数を共有できる. module \"vpc\" { source = \"../shared/modules/vpc\" } module \"ec2\" { source = \"../shared/modules/ec2\" vpc_id = module.vpc.myVPC.id private_subnet_id = module.vpc.private_subnet.id public_subnet_id = module.vpc.public_subnet.id } VPCモジュール ./shared/modules/vpc以下にVPCモジュールを構成するファイルを配置する. スコープがVPCモジュールに閉じたローカル変数を定義する. 以下のようにしておくと、モジュール内から local.vpc_cidr.dev のように値を取得できる. locals { vpc_cidr = { dev = \"10.1.0.0/16\" } subnet_cidr = { private = \"10.1.2.0/24\" public = \"10.1.1.0/24\" } } VPCを1個作る. VPCのCIDRは10.1.0.0/16. resource \"aws_vpc\" \"myVPC\" { cidr_block = local.vpc_cidr.dev instance_tenancy = \"default\" enable_dns_support = \"true\" enable_dns_hostnames = \"false\" tags = { Name = \"myVPC\" } } 作ったVPC内にサブネットを2個作る. 1つはPrivate用. もう1つはPublic用. PrivateサブネットのCIDRは10.1.2.0/24. PublicサブネットのCIDRは10.1.1.0/24. AZは両方同じで \"ap-northeast-1a\". map_public_ip_on_launchをtrueとしておくと, そこで立ち上げたEC2に自動的にpublic ipが振られる. resource \"aws_subnet\" \"public_1a\" { vpc_id = aws_vpc.myVPC.id depends_on = [aws_vpc.myVPC] availability_zone = \"ap-northeast-1a\" cidr_block = local.subnet_cidr.public map_public_ip_on_launch = true tags = { Name = \"public-1a\" } } resource \"aws_subnet\" \"private_1a\" { vpc_id = aws_vpc.myVPC.id depends_on = [aws_vpc.myVPC] availability_zone = \"ap-northeast-1a\" cidr_block = local.subnet_cidr.private tags = { Name = \"private-1a\" } } VPCに紐づくInternet Gatewayを作る. resource \"aws_internet_gateway\" \"myGW\" { vpc_id = \"${aws_vpc.myVPC.id}\" depends_on = [aws_vpc.myVPC] tags = { Name = \"my Internet Gateway\" } } Privateサブネットからインターネットに繋ぐために、 PublicサブネットにNAT Gatewayを作りたい. NAT Gateway用のEIPを作る. resource \"aws_eip\" \"nat_gateway\" { vpc = true depends_on = [aws_internet_gateway.myGW] tags = { Name = \"Eip for Nat gateway\" } } PublicサブネットにNAT Gatewayを作る. EIPは上で作成したものを使う. resource \"aws_nat_gateway\" \"myNatGW\" { allocation_id = aws_eip.nat_gateway.id subnet_id = aws_subnet.public_1a.id depends_on = [aws_internet_gateway.myGW] tags = { Name = \"my Nat Gateway\" } } ルートテーブル. いろいろなところで書かれていた内容を試してようやく動くものができた. VPCにはデフォルトで「メインルートテーブル」が作られる. メインルートテーブルはいじっていない. 以下、Private, Publicサブネットそれぞれのためのルートテーブルを定義している. PublicサブネットからInternet Gatewayに繋ぐ. PrivateサブネットからNAT Gatewayに繋ぐ. # Route table for public # public resource \"aws_route_table\" \"public\" { vpc_id = aws_vpc.myVPC.id depends_on = [aws_internet_gateway.myGW] tags = { Name = \"my Route Table for public\" } } # private resource \"aws_route_table\" \"private\" { vpc_id = aws_vpc.myVPC.id depends_on = [aws_internet_gateway.myGW] tags = { Name = \"my Route Table for private\" } } # Route table association # public resource \"aws_route_table_association\" \"public\" { subnet_id = aws_subnet.public_1a.id route_table_id = aws_route_table.public.id } # private resource \"aws_route_table_association\" \"private\" { subnet_id = aws_subnet.private_1a.id route_table_id = aws_route_table.private.id } # Routing for public resource \"aws_route\" \"public\" { route_table_id = aws_route_table.public.id gateway_id = aws_internet_gateway.myGW.id destination_cidr_block = \"0.0.0.0/0\" } # Routing for private resource \"aws_route\" \"private\" { route_table_id = aws_route_table.private.id gateway_id = aws_nat_gateway.myNatGW.id destination_cidr_block = \"0.0.0.0/0\" } EC2モジュール ./shared/modules/ec2以下にEC2モジュールを構成するファイルを配置する. スコープがEC2モジュールに閉じたローカル変数を定義する. main.tfからVPCモジュールのOutputをEC2モジュールに渡す必要があるが、 渡すデータを受けるためにEC2モジュール側で変数を定義しておく必要がある. locals { private = { ip = \"10.1.2.5\" ami = \"ami-0df99b3a8349462c6\" instance_type = \"t2.micro\" } public = { ip = \"10.1.1.5\" ami = \"ami-0df99b3a8349462c6\" instance_type = \"t2.micro\" } } variable \"vpc_id\" { type = string } variable \"private_subnet_id\" { type = string } variable \"public_subnet_id\" { type = string } EC2にアクセスするための鍵ペア. 既に鍵ペアを持っているものとし、その公開鍵を渡す. 以下のようにすると、HostからSSHの-iオプションで秘密鍵を指定して接続できるようになる. resource \"aws_key_pair\" \"deployer\" { key_name = \"deployer\" public_key = \"{公開鍵}\" } EC2に設定するセキュリティグループを作る. この記事では, Private, Publicともに、インバウンドをSSHのみとした. 次の記事でPublicにHTTPを通す. アウトバウンドとして全て通すようにしないとインスタンスから外にアクセスできなくなる(ハマった). # Security group resource \"aws_security_group\" \"web_server_sg\" { name = \"web_server\" description = \"Allow http and https traffic.\" vpc_id = var.vpc_id } # Security group rule SSH(22) resource \"aws_security_group_rule\" \"web_inbound_ssh\" { type = \"ingress\" from_port = 22 to_port = 22 protocol = \"tcp\" cidr_blocks = [\"0.0.0.0/0\"] security_group_id = aws_security_group.web_server_sg.id } resource \"aws_security_group_rule\" \"web_outbound\" { type = \"egress\" from_port = 0 to_port = 0 protocol = \"-1\" cidr_blocks = [\"0.0.0.0/0\"] ipv6_cidr_blocks = [\"::/0\"] security_group_id = aws_security_group.web_server_sg.id } # Security group resource \"aws_security_group\" \"db_server_sg\" { name = \"db_server\" description = \"Allow MySQL traffic.\" vpc_id = var.vpc_id } # Security group rule SSH(22) resource \"aws_security_group_rule\" \"db_inbound_ssh\" { type = \"ingress\" from_port = 22 to_port = 22 protocol = \"tcp\" cidr_blocks = [\"0.0.0.0/0\"] security_group_id = aws_security_group.db_server_sg.id } resource \"aws_security_group_rule\" \"db_outbound\" { type = \"egress\" from_port = 0 to_port = 0 protocol = \"-1\" cidr_blocks = [\"0.0.0.0/0\"] ipv6_cidr_blocks = [\"::/0\"] security_group_id = aws_security_group.db_server_sg.id } ネットワークインターフェース. セキュリティグループはEC2インスタンスではなくネットワークインターフェースに紐づく. EC2(aws_instance)のsecurity_groupsに書けなくてハマった. # public resource \"aws_network_interface\" \"public_1a\" { subnet_id = var.public_subnet_id private_ips = [local.public.ip] security_groups = [ aws_security_group.web_server_sg.id ] tags = { Name = \"public_subnet_network_interface\" } } # private resource \"aws_network_interface\" \"private_1a\" { subnet_id = var.private_subnet_id private_ips = [local.private.ip] security_groups = [ aws_security_group.db_server_sg.id ] tags = { Name = \"private_subnet_network_interface\" } } 最後にEC2. # Web Server resource \"aws_instance\" \"public\" { ami = local.public.ami instance_type = local.public.instance_type key_name = aws_key_pair.deployer.id network_interface { network_interface_id = aws_network_interface.public_1a.id device_index = 0 } credit_specification { cpu_credits = \"unlimited\" } root_block_device { volume_size = 20 volume_type = \"gp2\" delete_on_termination = true tags = { Name = \"web-ebs\" } } tags = { Name = \"Web\" } } # DB Server resource \"aws_instance\" \"private\" { ami = local.private.ami instance_type = local.private.instance_type key_name = aws_key_pair.deployer.id network_interface { network_interface_id = aws_network_interface.private_1a.id device_index = 0 } credit_specification { cpu_credits = \"unlimited\" } root_block_device { volume_size = 20 volume_type = \"gp2\" delete_on_termination = true tags = { Name = \"db-ebs\" } } tags = { Name = \"DB\" } } 実行 作った.tfファイルを再生して環境を構築する. validateでデバッグして、大体できたらplan(DryRun)で変更が正しそうか確認してみた. が、評価しなければわからないものについてはDryRunではわからず、 結局applyが途中で止まって解決しないといけない. ansibleと異なり冪等性が言われていなくて、applyで間違った構成を作ってしまうと、 その先、その構成を修正したとしても上手くいかないことがある. $ cd \"/path/to/dev\" $ terraform validate Success! The configuration is valid. $ terraform plan ... $ terraform apply ... 出来たとして、Publicに立ったEC2のパブリックIPv4をメモる. 疎通確認 Host ->(SSH)-> Web ->(SSH)-> DB を試す. DBから外に繋がるか試す. SSH Agent Forwardを使うと、Web EC2に秘密鍵を置かないで済む. Web側のssh configにForwardAgent yesを指定しておく. Host db HostName 10.1.2.5 User ubuntu ForwardAgent yes いざ. $ ssh-add \"{秘密鍵のパス}\" $ ssh -A ubuntu@{WebのパブリックIPv4} ubuntu@ip-10-1-1-5$ ssh db ubuntu@ip-10-1-2-5$ ping yahoo.co.jp 64 bytes from f1.top.vip.kks.yahoo.co.jp (183.79.135.206): icmp_seq=1 ttl=33 time=14.9 ms 64 bytes from f1.top.vip.kks.yahoo.co.jp (183.79.135.206): icmp_seq=2 ttl=33 time=14.6 ms .. できた..

default eye-catch image.

Grafanaプラグインを読んでいく – Clock plugin

最も単純そうなプラグインを読んでいくシリーズ。 プラグインは Clock plugin。Panelプラグイン。配布はここ。 最も単純そうなDataSourceプラグインを読む記事は以下。 [clink url=\"https://ikuty.com/2020/11/14/grafana-code-read/\"] ダッシュボードに時計を表示できる。 ダッシュボードから設定をおこない表示に反映する機能を備えていて、 PanelプラグインのHelloWorldには良い感じ。 The Clock Panel can show the current time or a countdown and updates every second. Show the time in another office or show a countdown to an important event. 肝心のデータプロットに関する機能は無いので別途違うコンポーネントを読む。 インストール、ビルド 公式からインストールすると src が含まれない。 ソースコードをclone、buildすることにする。 初回だけgrafana-serverのrestartが必要。 # clone repository $ cd ~/ $ git clone https://github.com/grafana/clock-panel.git $ mv clock-panek /var/lib/grafana/plugins # install plugin $ yarn install $ yarn build # restart grafana-server $ sudo service grafana-server restart ディレクトリ・ファイル構成 ディレクトリ・ファイル構成は以下の通り。 clock-panel/ src/ ClockPanel.tsx ... プラグイン本体 module.ts ... プラグインのエントリポイント options.tsx plugin.json ... プラグインの設定ファイル types.ts ... TypeScript型定義 img/ ... 画像リソース clock.svg countdown1.png screenshot-clock-options.png screenshot-clocks.png screenshot-showcase.png external/ ... 外部ライブラリ moment-duration-formant.js エントリポイント ./module.ts の内容は以下の通り。 ClockPanel.tsxで定義済みのClockPanelクラスをexportしている。 options.tsxに記述したオプション画面関連のクラスを.setPanelOptions()を介して設定する。 import { PanelPlugin } from \'@grafana/data\'; import { ClockPanel } from \'./ClockPanel\'; import { ClockOptions } from \'./types\'; import { optionsBuilder } from \'./options\'; export const plugin = new PanelPlugin(ClockPanel).setNoPadding().setPanelOptions(optionsBuilder); 本体 (ClockPanel.tsx) ./ClockPanel.tsxを読んでいく。React+TypeScript。 import React, { PureComponent } from \'react\'; import { PanelProps } from \'@grafana/data\'; import { ClockOptions, ClockType, ZoneFormat, ClockMode } from \'./types\'; import { css } from \'emotion\'; // eslint-disable-next-line import moment, { Moment } from \'moment\'; import \'./external/moment-duration-format\'; interface Props extends PanelProps {} interface State { // eslint-disable-next-line now: Moment; } export function getTimeZoneNames(): string[] { return (moment as any).tz.names(); } // PureComponentクラスを派生させることでプラグイン用のパネルクラスを定義できる。 // パネルのプロパティは PanelProps型だが 当プラグイン用にProps型に拡張している。 export class ClockPanel extends PureComponent { timerID?: any; state = { now: this.getTZ(), timezone: \'\' }; //Componentのインスタンスが生成されDOMに挿入されるときに呼ばれる //DOM挿入後,1秒間隔で this.tick()の実行を開始する。 componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 // 1 second ); } //[非推奨] DOMから削除されるときに呼ばれる。古いのかな。 //this.tick()の実行を停止する。 componentWillUnmount() { clearInterval(this.timerID); } //DOM挿入後1秒間隔で呼ばれる。 //stateを更新する。 tick() { const { timezone } = this.props.options; this.setState({ now: this.getTZ(timezone) }); } //時刻フォーマットを取得する。 //時刻フォーマットはオプション設定画面で設定され props.optionsに渡される。 //渡される変数は clockType と timeSettings である。 clockTypeは 12時間/24時間のいずれか。 //12時間なら h:mm:ss A, 24時間なら HH:mm:ss。 //カスタムの場合,clockTypeがClockType.Customになる。 getTimeFormat() { const { clockType, timeSettings } = this.props.options; if (clockType === ClockType.Custom && timeSettings.customFormat) { return timeSettings.customFormat; } if (clockType === ClockType.H12) { return \'h:mm:ss A\'; } return \'HH:mm:ss\'; } // Return a new moment instnce in the selected timezone // eslint-disable-next-line getTZ(tz?: string): Moment { if (!tz) { tz = (moment as any).tz.guess(); } return (moment() as any).tz(tz); } //カウントダウン文字列を得る //設定値countdownSettings, timezone は props.options から得られる。 // getCountdownText(): string { const { now } = this.state; const { countdownSettings, timezone } = this.props.options; //カウントダウン終了時 設定された文字列 countdownSettings.endText を返す if (!countdownSettings.endCountdownTime) { return countdownSettings.endText; } //残り時間を計算。 const timeLeft = moment.duration( moment(countdownSettings.endCountdownTime) .utcOffset(this.getTZ(timezone).format(\'Z\'), true) .diff(now) ); let formattedTimeLeft = \'\'; //計算した残り時間が0以下であれば、設定された文字列 countdownSettings.endText を返す。 if (timeLeft.asSeconds() 0) { formattedTimeLeft = timeLeft.years() === 1 ? \'1 year, \' : timeLeft.years() + \' years, \'; previous = \'years\'; } // Y months (or Y month) if (timeLeft.months() > 0 || previous === \'years\') { formattedTimeLeft += timeLeft.months() === 1 ? \'1 month, \' : timeLeft.months() + \' months, \'; previous = \'months\'; } // Z days (or Z day) if (timeLeft.days() > 0 || previous === \'months\') { formattedTimeLeft += timeLeft.days() === 1 ? \'1 day, \' : timeLeft.days() + \' days, \'; previous = \'days\'; } // A hours (or A hour) if (timeLeft.hours() > 0 || previous === \'days\') { formattedTimeLeft += timeLeft.hours() === 1 ? \'1 hour, \' : timeLeft.hours() + \' hours, \'; previous = \'hours\'; } // B minutes (or B minute) if (timeLeft.minutes() > 0 || previous === \'hours\') { formattedTimeLeft += timeLeft.minutes() === 1 ? \'1 minute, \' : timeLeft.minutes() + \' minutes, \'; } // C minutes (or C minute) formattedTimeLeft += timeLeft.seconds() === 1 ? \'1 second \' : timeLeft.seconds() + \' seconds\'; return formattedTimeLeft; } //Zoneを表示するh4タグを作成して返す。Reactっぽい。 renderZone() { const { now } = this.state; const { timezoneSettings } = this.props.options; const { zoneFormat } = timezoneSettings; // ReactでCSSを書く作法。ヒアドキュメント // ロジックコードの中にHTML生成コードが混在して非常に見辛い。 const clazz = css` font-size: ${timezoneSettings.fontSize}; font-weight: ${timezoneSettings.fontWeight}; line-height: 1.4; `; let zone = this.props.options.timezone || \'\'; switch (zoneFormat) { case ZoneFormat.offsetAbbv: zone = now.format(\'Z z\'); break; case ZoneFormat.offset: zone = now.format(\'Z\'); break; case ZoneFormat.abbv: zone = now.format(\'z\'); break; default: try { zone = (this.getTZ(zone) as any)._z.name; } catch (e) { console.log(\'Error getting timezone\', e); } } return ( {zone} {zoneFormat === ZoneFormat.nameOffset && ( ({now.format(\'Z z\')}) ); } //Dateを表示するh3タグを返す。 renderDate() { const { now } = this.state; const { dateSettings } = this.props.options; const clazz = css` font-size: ${dateSettings.fontSize}; font-weight: ${dateSettings.fontWeight}; `; const disp = now.locale(dateSettings.locale || \'\').format(dateSettings.dateFormat); return ( {disp} ); } //Timeを返すh2タグを返す。 renderTime() { const { now } = this.state; const { timeSettings, mode } = this.props.options; const clazz = css` font-size: ${timeSettings.fontSize}; font-weight: ${timeSettings.fontWeight}; `; const disp = mode === ClockMode.countdown ? this.getCountdownText() : now.format(this.getTimeFormat()); return {disp}; } //React componentとしてrender()メソッドを実装する必要がある。 //CSSを整形してZone,Date,Timeを設定して返す。 render() { const { options, width, height } = this.props; const { bgColor, dateSettings, timezoneSettings } = options; const clazz = css` display: flex; align-items: center; justify-content: center; flex-direction: column; background-color: ${bgColor ?? \'\'}; text-align: center; `; return ( {dateSettings.showDate && this.renderDate()} {this.renderTime()} {timezoneSettings.showTimezone && this.renderZone()} ); } } 設定 (options.tsx) 設定画面側を読んでいく。 PanelOptionsEditorBuilder型の引数を取り、builderに対して機能実装していく。 機能実装というのは、つまり、ラジオボタンを追加したり、カスタムエディタを追加したり、など。 この実装で以下のような設定画面が表示される。(README.mdは古いので注意)。 Modeとして、時間をそのまま表示するTimeモードか、カウンドダウンモードかを二者択一で設定する。 背景色(BackgroundColor)をカラーピッカーで設定する。(ちなみにGrafanaV7では機能しない様子)。 addTimeFormat()メソッドにより、24h表示/12h表示/カスタム表示,FontSize,FontWeightの設定機能を追加する。 addTimeZone()メソッドにより,TimeZoneと表示有無の設定機能を追加する。 カウントダウンモードに設定すると、カウントダウン設定をおこなえるが, addCountdown()メソッドにより,カウントダウン設定を追加する。 決められた構文にしたがって欲しい機能を追加していくだけなので、 設定画面の実装が必要になったら必要な構文を調べて追加していくことになりそう。 import React from \'react\'; import { PanelOptionsEditorBuilder, GrafanaTheme, dateTime } from \'@grafana/data\'; import { ColorPicker, Input, Icon, stylesFactory } from \'@grafana/ui\'; import { css } from \'emotion\'; import { config } from \'@grafana/runtime\'; import { ClockOptions, ClockMode, ClockType, FontWeight, ZoneFormat } from \'./types\'; import { getTimeZoneNames } from \'./ClockPanel\'; export const optionsBuilder = (builder: PanelOptionsEditorBuilder) => { // Global options builder //ClockModeの二者択一。TimeかCountdownを選ばせる。 .addRadio({ path: \'mode\', name: \'Mode\', settings: { options: [ { value: ClockMode.time, label: \'Time\' }, { value: ClockMode.countdown, label: \'Countdown\' }, ], }, defaultValue: ClockMode.time, }) //背景色のカスタムエディタ。カラーピッカーから色を選ばせる。 .addCustomEditor({ id: \'bgColor\', path: \'bgColor\', name: \'Background Color\', editor: props => { const styles = getStyles(config.theme); let prefix: React.ReactNode = null; let suffix: React.ReactNode = null; if (props.value) { suffix = props.onChange(undefined)} />; } prefix = ( ); return ( { console.log(\'CLICK\'); }} prefix={prefix} suffix={suffix} /> ); }, defaultValue: \'\', }); // TODO: refreshSettings.syncWithDashboard addCountdown(builder); addTimeFormat(builder); addTimeZone(builder); addDateFormat(builder); }; //--------------------------------------------------------------------- // COUNTDOWN //--------------------------------------------------------------------- function addCountdown(builder: PanelOptionsEditorBuilder) { const category = [\'Countdown\']; builder .addTextInput({ category, path: \'countdownSettings.endCountdownTime\', name: \'End Time\', settings: { placeholder: \'ISO 8601 or RFC 2822 Date time\', }, defaultValue: dateTime(Date.now()) .add(6, \'h\') .format(), showIf: o => o.mode === ClockMode.countdown, }) .addTextInput({ category, path: \'countdownSettings.endText\', name: \'End Text\', defaultValue: \'00:00:00\', showIf: o => o.mode === ClockMode.countdown, }) .addTextInput({ category, path: \'countdownSettings.customFormat\', name: \'Custom format\', settings: { placeholder: \'optional\', }, defaultValue: undefined, showIf: o => o.mode === ClockMode.countdown, }); } //--------------------------------------------------------------------- // TIME FORMAT //--------------------------------------------------------------------- function addTimeFormat(builder: PanelOptionsEditorBuilder) { const category = [\'Time Format\']; builder .addRadio({ category, path: \'clockType\', name: \'Clock Type\', settings: { options: [ { value: ClockType.H24, label: \'24 Hour\' }, { value: ClockType.H12, label: \'12 Hour\' }, { value: ClockType.Custom, label: \'Custom\' }, ], }, defaultValue: ClockType.H24, }) .addTextInput({ category, path: \'timeSettings.customFormat\', name: \'Time Format\', description: \'the date formatting pattern\', settings: { placeholder: \'date format\', }, defaultValue: undefined, showIf: opts => opts.clockType === ClockType.Custom, }) .addTextInput({ category, path: \'timeSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'date format\', }, defaultValue: \'12px\', }) .addRadio({ category, path: \'timeSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, }); } //--------------------------------------------------------------------- // TIMEZONE //--------------------------------------------------------------------- function addTimeZone(builder: PanelOptionsEditorBuilder) { const category = [\'Timezone\']; const timezones = getTimeZoneNames().map(n => { return { label: n, value: n }; }); timezones.unshift({ label: \'Default\', value: \'\' }); builder .addSelect({ category, path: \'timezone\', name: \'Timezone\', settings: { options: timezones, }, defaultValue: \'\', }) .addBooleanSwitch({ category, path: \'timezoneSettings.showTimezone\', name: \'Show Timezone\', defaultValue: false, }) .addSelect({ category, path: \'timezoneSettings.zoneFormat\', name: \'Display Format\', settings: { options: [ { value: ZoneFormat.name, label: \'Normal\' }, { value: ZoneFormat.nameOffset, label: \'Name + Offset\' }, { value: ZoneFormat.offsetAbbv, label: \'Offset + Abbreviation\' }, { value: ZoneFormat.offset, label: \'Offset\' }, { value: ZoneFormat.abbv, label: \'Abbriviation\' }, ], }, defaultValue: ZoneFormat.offsetAbbv, showIf: s => s.timezoneSettings?.showTimezone, }) .addTextInput({ category, path: \'timezoneSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'font size\', }, defaultValue: \'12px\', showIf: s => s.timezoneSettings?.showTimezone, }) .addRadio({ category, path: \'timezoneSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, showIf: s => s.timezoneSettings?.showTimezone, }); } //--------------------------------------------------------------------- // DATE FORMAT //--------------------------------------------------------------------- function addDateFormat(builder: PanelOptionsEditorBuilder) { const category = [\'Date Options\']; builder .addBooleanSwitch({ category, path: \'dateSettings.showDate\', name: \'Show Date\', defaultValue: false, }) .addTextInput({ category, path: \'dateSettings.dateFormat\', name: \'Date Format\', settings: { placeholder: \'Enter date format\', }, defaultValue: \'YYYY-MM-DD\', showIf: s => s.dateSettings?.showDate, }) .addTextInput({ category, path: \'dateSettings.locale\', name: \'Locale\', settings: { placeholder: \'Enter locale: de, fr, es, ... (default: en)\', }, defaultValue: \'\', showIf: s => s.dateSettings?.showDate, }) .addTextInput({ category, path: \'dateSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'date format\', }, defaultValue: \'20px\', showIf: s => s.dateSettings?.showDate, }) .addRadio({ category, path: \'dateSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, showIf: s => s.dateSettings?.showDate, }); } const getStyles = stylesFactory((theme: GrafanaTheme) => { return { colorPicker: css` padding: 0 ${theme.spacing.sm}; `, inputPrefix: css` display: flex; align-items: center; `, trashIcon: css` color: ${theme.colors.textWeak}; cursor: pointer; &:hover { color: ${theme.colors.text}; } `, }; });

default eye-catch image.

Grafanaプラグインを読んでいく – simpod-json-datasource plugin

最も単純そうなDataSourceプラグインを読んでいく。 プラグインは simpod-json-datasource plugin。 配布はここ。 JSONを返す任意のURLにリクエストを投げて結果を利用できるようにする。 TypeScriptで書かれている。 The JSON Datasource executes JSON requests against arbitrary backends. JSON Datasource is built on top of the Simple JSON Datasource. It has refactored code, additional features and active development. install インストール方法は以下の通り。 初回だけgrafana-serverのrestartが必要。 # install plugin $ grafana-cli plugins install simpod-json-datasource # restart grafana-server $ sudo service grafana-server restart build ビルド方法は以下の通り。 yarn一発。 # build $ cd /var/lib/grafana/plugins/simpod-json-datasource $ yarn install $ yarn run build 設定 データソースの追加で、今回インストールした DataSource/JSON (simpod-json-datasource)を 選択すると、プラグインのコンフィグを変更できる。 要求するURL一覧 URLはBaseURL。 このプラグインはURLの下にいくつかのURLが存在することを想定している。 つまり、それぞれのURLに必要な機能を実装することでプラグインが機能する。 用語については順に解説する。 必須 Method Path Memo GET / ConfigページでStatusCheckを行うためのURL。200が返ればConfigページで「正常」となる。 POST /search 呼び出し時に「利用可能なメトリクス」を返す。 POST /query 「メトリクスに基づくデータポイント」を返す。 POST /annotations 「アノテーション」を返す。 オプショナル Method Path Memo POST /tag-keys 「ad-hocフィルタ」用のタグのキーを返す。 POST /tag-values 「ad-hocフィルタ」用のタグの値を返す。 メトリクス Grafanaにおいて、整理されていないデータの中から\"ある観点\"に従ったデータを取得したいという ユースケースをモデル化している. 例えば、サーバ群の負荷状況を監視したいというケースでは「サーバ名」毎にデータを取得したいし、 センサ群から得られるデータを監視したいというケースでは「センサID」毎にデータを取得したい. これらの観点は、Grafanaにおいて「メトリクス」または「タグ」として扱われる。 センサAからセンサZのうちセンサPとセンサQのデータのみ表示したい、という感じで使う。 ここで現れるセンサAからセンサZが「メトリクス」である。 クエリ変数のサポート (metricFindQuery) 「クエリ変数」は、「メトリクス」を格納する変数である。 データソースプラグインがクエリ変数をサポートするためには、 DataSourceApi クラスの metricFindQuery を オーバーライド する. string型のqueryパラメタを受け取り、MetricFindValue型変数の配列を返す. 要は以下の問答を定義するものである。 - 質問 : 「どんなメトリクスがありますか?」 - 回答 : 「存在するメトリクスはxxx,yyy,zzzです」 ちなみに、metricFindQueryが受け取るqueryの型は、 metricFindQueryを呼び出すUIがstring型で呼び出すからstring型なのであって、 UIを変更することで別の型に変更することができる。 以下、simpod-json-datasource の metricFindQuery の実装。 // MetricFindValue interfaceはtext,valueプロパティを持つ // { \"label\":\"upper_25\", \"value\": 1}, { \"label\":\"upper_50\", \"value\": 2} のような配列を返す. metricFindQuery(query: string, options?: any, type?: string): Promise { // Grafanaが用意する interpolation(補間)関数 // query として \"query field value\" が渡されたとする // interpolated は { \"target\": \"query field value\" }のようになる. const interpolated = { type, target: getTemplateSrv().replace(query, undefined, \'regex\'), }; return this.doRequest({ url: `${this.url}/search`, data: interpolated, method: \'POST\', }).then(this.mapToTextValue); } // /searchから返ったJSONは2パターンある // 配列 ([\"upper_25\",\"upper_50\",\"upper_75\",\"upper_90\",\"upper_95\"])か、 // map ([ { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} ]) // これらを MetricFindValue型に変換する mapToTextValue(result: any) { return result.data.map((d: any, i: any) => { // mapの場合 if (d && d.text && d.value) { return { text: d.text, value: d.value }; } // 配列の場合 if (isObject(d)) { return { text: d, value: i }; } return { text: d, value: d }; }); } 以下、simpod-json-datasource が メトリクスを取得する部分のUI。 FormatAs, Metric, Additional JSON Dataの各項目が選択可能で、 各々の変数がViewModelとバインドされている. コードは以下。React。 FormatAsのセレクトボックスのOnChangeでsetFormatAs()が呼ばれる。 MetricのセレクトボックスのOnChagneでsetMetric()が呼ばれる。 Additional JSON DataのonBlurでsetData()が呼ばれる。 import { QueryEditorProps, SelectableValue } from \'@grafana/data\'; import { AsyncSelect, CodeEditor, Label, Select } from \'@grafana/ui\'; import { find } from \'lodash\'; import React, { ComponentType } from \'react\'; import { DataSource } from \'./DataSource\'; import { Format } from \'./format\'; import { GenericOptions, GrafanaQuery } from \'./types\'; type Props = QueryEditorProps; const formatAsOptions = [ { label: \'Time series\', value: Format.Timeseries }, { label: \'Table\', value: Format.Table }, ]; export const QueryEditor: ComponentType = ({ datasource, onChange, onRunQuery, query }) => { const [formatAs, setFormatAs] = React.useState<selectableValue>( find(formatAsOptions, option => option.value === query.type) ?? formatAsOptions[0] ); const [metric, setMetric] = React.useState<selectableValue>(); const [data, setData] = React.useState(query.data ?? \'\'); // 第2引数は依存する値の配列 (data, formatAs, metric). // 第2引数の値が変わるたびに第1引数の関数が実行される. React.useEffect(() => { // formatAs.value が 空なら何もしない if (formatAs.value === undefined) { return; } // metric.value が 空なら何もしない if (metric?.value === undefined) { return; } // onChange(..)を実行する onChange({ ...query, data: data, target: metric.value, type: formatAs.value }); onRunQuery(); }, [data, formatAs, metric]); // Metricを表示するセレクトボックスの値に関数がバインドされている. // 関数はstring型のパラメタを1個とる. const loadMetrics = (searchQuery: string) => { // datasourceオブジェクトの metricFindQuery()を呼び出す. // 引数はsearchQuery. 戻り値はMetricFindValue型の配列(key/valueが入っている). // 例えば { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} return datasource.metricFindQuery(searchQuery).then( result => { // { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} から // { \"label\": \"upper_25\", \"value\": 1}, { \"label\": \"upper_75\", \"value\": 2} へ const metrics = result.map(value => ({ label: value.text, value: value.value })); // セレクトボックスで選択中のMetricを取得し setMetric に渡す setMetric(find(metrics, metric => metric.value === query.target)); return metrics; }, response => { throw new Error(response.statusText); } ); }; return ( <> <div className=\"gf-form-inline\"> <div className=\"gf-form\"> <select prefix=\"Format As: \" options={formatAsOptions} defaultValue={formatAs} onChange={v => { setFormatAs(v); }} /> </div> <div className=\"gf-form\"> <asyncSelect prefix=\"Metric: \" loadOptions={loadMetrics} defaultOptions placeholder=\"Select metric\" allowCustomValue value={metric} onChange={v => { setMetric(v); }} /> </div> </div> <div className=\"gf-form gf-form--alt\"> <div className=\"gf-form-label\"> <label>Additional JSON Data </div> <div className=\"gf-form\"> <codeEditor width=\"500px\" height=\"100px\" language=\"json\" showLineNumbers={true} showMiniMap={data.length > 100} value={data} onBlur={value => setData(value)} /> </div> </div> </> ); // } }; クエリ変数から値を取得 (queryの実装) 続いて、選択したメトリクスのデータ列を得るための仕組み. query()インターフェースを実装する. QueryRequest型の引数を受け取る. 引数には、どのメトリクスを使う、だとか、取得範囲だとか、様々な情報が入っている。 QueryRequest型の引数を加工した値を、/query URLにPOSTで渡す. /query URLから戻ってきた値は DataQueryResponse型と互換性があり、query()の戻り値として返る. // UIから送られてきたQueryRequest型の変数optionsを処理して /query に投げるJSONを作る。 // QueryRequest型は当プラグインがGrafanaQuery型interfaceを派生させて作った型。 query(options: QueryRequest): Promise { const request = this.processTargets(options); // 処理した結果、targets配列が空なら空を返す。 if (request.targets.length === 0) { return Promise.resolve({ data: [] }); } // JSONにadhocFiltersを追加する // @ts-ignore request.adhocFilters = getTemplateSrv().getAdhocFilters(this.name); // JSONにscopedVarsを追加する options.scopedVars = { ...this.getVariables(), ...options.scopedVars }; // /queryにJSONをPOSTで投げて応答をDataQueryResponse型で返す。 return this.doRequest({ url: `${this.url}/query`, data: request, method: \'POST\', }); } // UIから送られてきたQueryRequest型変数optionsのtargetsプロパティを加工して返す // processTargets(options: QueryRequest) { options.targets = options.targets .filter(target => { // remove placeholder targets return target.target !== undefined; }) .map(target => { if (target.data.trim() !== \'\') { // JSON様の文字列target.data をJSONオブジェクト(key-value)に変換する // reviverとして関数が指定されている // valueが文字列であった場合にvalue内に含まれる変数名を置換する // 置換ルールは cleanMatch メソッド target.data = JSON.parse(target.data, (key, value) => { if (typeof value === \'string\') { return value.replace((getTemplateSrv() as any).regex, match => this.cleanMatch(match, options)); } return value; }); } // target.targetには、変数のプレースホルダ($..)が存在する. // grafanaユーザが入力した変数がoptions.scopedVarsに届くので、 // target.target内のプレースホルダをoptions.scopedVarsで置換する。 // 置換後の書式は正規表現(regex) if (typeof target.target === \'string\') { target.target = getTemplateSrv().replace(target.target.toString(), options.scopedVars, \'regex\'); } return target; }); return options; } // cleanMatch cleanMatch(match: string, options: any) { // const replacedMatch = getTemplateSrv().replace(match, options.scopedVars, \'json\'); if ( typeof replacedMatch === \'string\' && replacedMatch[0] === \'\"\' && replacedMatch[replacedMatch.length - 1] === \'\"\' ) { return JSON.parse(replacedMatch); } return replacedMatch; } TypeScriptに明るくない場合、以下を参照。 - Typescript-array-filter - 【JavaScript】map関数を用いたおしゃれな配列処理 - TypeScript - String replace() JavaScriptのreplaceとGrafanaのreplaceが混在していて、 IDEが無いとかなり厳しい感じ。 - Interpolate variables in data source plugins - Advanced variable format options query()のパラメタについて、 Grafanaのデフォだとstring型で来るのだが、プラグインが必要に応じて好きに変更できる。 つまりquery()に値を投げる部分をプラグインが変更できるため、変更に応じて受け側も変更する。 当プラグインはDataQueryRequestインターフェースを派生させた型を用意している。 - DataQueryRequest interface getTemplateSrv()はGrafanaのユーティリティ関数。 - getTemplateSrv variable - Github getTemplateSrv - Variable syntax 現在アクティブなダッシュボード内の変数が全て得られる。 * Via the TemplateSrv consumers get access to all the available template variables * that can be used within the current active dashboard. import { getTemplateSrv } from ‘@grafana/runtime’; const templateSrv = getTemplateSrv(); const variablesProtected = templateSrv.getVariables(); const variablesStringfied = JSON.stringify( variablesProtected ); const variables = JSON.parse( variablesStringfied ); URLに投げるJSONは例えば以下の通り。 { \"app\": \"dashboard\", \"requestId\": \"Q171\", \"timezone\": \"browser\", \"panelId\": 23763571993, \"dashboardId\": 1, \"range\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\", \"raw\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\" } }, \"timeInfo\": \"\", \"interval\": \"50ms\", \"intervalMs\": 50, \"targets\": [ { \"refId\": \"A\", \"data\": \"\", \"target\": \"upper_50\", \"type\": \"timeseries\", \"datasource\": \"JSON-ikuty\" } ], \"maxDataPoints\": 1058, \"scopedVars\": { \"variable1\": { \"text\": [ \"upper_50\" ], \"value\": [ \"upper_50\" ] }, \"__interval\": { \"text\": \"50ms\", \"value\": \"50ms\" }, \"__interval_ms\": { \"text\": \"50\", \"value\": 50 } }, \"startTime\": 1605108668062, \"rangeRaw\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\" }, \"adhocFilters\": [] } URLから返る値は例えば以下の通り. [ { \"target\": \"pps in\", \"datapoints\": [ [ 622, 1450754160000 ], [ 365, 1450754220000 ] ] }, { \"target\": \"pps out\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] }, { \"target\": \"errors out\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] }, { \"target\": \"errors in\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] } ] アノテーションのサポート (annotationQueryの実装) Grafanaには、ユーザがグラフの中に注意を喚起するラベル(またはラベルの範囲)を 設定する機能がある。 ユーザが自力で書くだけでなく、プラグインがアノテーションを提供することもできる。 simpod-json-datasourceプラグインは、アノテーションを提供する機能を有する。 公式の説明はこちら。 例えば下図で薄く赤くなっている部分が プラグインによるアノテーション。 実装方法は以下。 - アノテーションサポートを有効にする - annotationQueryインターフェースを実装する - アノテーションイベントを作成する アノテーションサポートを有効にするためには、 plugin.json に以下を追加する。 { \"annotations\": true } simpod-json-datasourceプラグインのannotationQueryの実装は以下。 annotationQuery( options: AnnotationQueryRequest ): Promise { const query = getTemplateSrv().replace(options.annotation.query, {}, \'glob\'); const annotationQuery = { annotation: { query, name: options.annotation.name, datasource: options.annotation.datasource, enable: options.annotation.enable, iconColor: options.annotation.iconColor, }, range: options.range, rangeRaw: options.rangeRaw, variables: this.getVariables(), }; return this.doRequest({ url: `${this.url}/annotations`, method: \'POST\', data: annotationQuery, }).then((result: any) => { return result.data; }); } プラグインからURLにPOSTのBODYで渡ってくるデータは以下。 { \"annotation\": { \"query\": \"hogehoge\", \"name\": \"hoge\", \"datasource\": \"JSON-ikuty\", \"enable\": true, \"iconColor\": \"rgba(255, 96, 96, 1)\" }, \"range\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\", \"raw\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\" } }, \"rangeRaw\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\" }, \"variables\": { \"variable1\": { \"text\": [ \"All\" ], \"value\": [ \"upper_25\", \"upper_50\", \"upper_75\", \"upper_90\", \"upper_105\" ] } } } これに対して以下の応答を返すとイメージのようになる。 以下はAnnotationEvent型と型が一致している。 isRegionがtrueの場合、timeEndを付与することでアノテーションが領域になる。 isRegionがfalseの場合、timeEndは不要。 tagsにタグ一覧を付与できる。 [ { \"text\": \"text shown in body\", \"title\": \"Annotation Title\", \"isRegion\": true, \"time\": \"1450754170000\", \"timeEnd\": \"1450754180000\", \"tags\": [ \"tag1\" ] } ]

default eye-catch image.

PostgreSQL スキーマをコピーする

スキーマをコピーする方法はない。 代わりに以下の方法で同じ効果を得る。 スキーマ名Aをスキーマ名Bに変更する スキーマ名Bの状態でpg_dumpする スキーマ名Bをスキーマ名Aに変更する スキーマ名Bを作成する pg_dumpしたファイルをリストアする Statementは以下の通り。 $ psql -U user -d dbname -c \'ALTER SCHEMA old_schema RENAME TO new_schema\' $ pg_dump -U user -n new_schema -f new_schema.sql dbname $ psql -U user -d dbname -c \'ALTER SCHEMA new_schema RENAME TO old_schema\' $ psql -U user -d dbname -c \'CREATE SCHEMA new_schema\' $ psql -U user -q -d dbname -f new_schema.sql $ rm new_schema.sql [arst_adsense slotnumber=\"1\"]

default eye-catch image.

Redshift テーブル設計のベストプラクティス

どのようにテーブル設計するとパフォーマンスを得られるか. 公式がベストプラクティスを用意している. Redshiftのベストプラクティスが先にあってER図が後なのか、 ER図に対してベストプラクティスを適用するのか、 実際は行ったり来たりするようなイメージ. ER図とは別に何を考慮すべきなのか読み進めていく. [arst_toc tag=\"h4\"] ソートキー テーブル作成時に1つ以上の列をソートキーとして設定できる. 設定するとソートキーに準じたソート順でディスクに格納される. ソートキーに関するベストプラクティスは以下の通り. 最新のデータを得たい場合はタイムスタンプ列をソートキーにする. 1つの列に対してwhere句による範囲指定or等価指定をおこなう場合はその列をソートキーにする. ディメンションテーブルを頻繁に結合する場合は結合キーをソートキーにする. ファクトテーブルを中心にディメンションテーブルが4つある構造があるとする. ファクトテーブルにはディメンションテーブルのPKが入り関連している. また、ファクトテーブルに日付カラムがあり、常に最新のレコードが欲しいとする. ベストプラクティスによると、 各テーブルの各カラムに以下のようにソートキーを設定する. 分散スタイル クエリの実行を複数のクラスタ(コンピューティングノード、スライス)で実行するために、 それらに1)データを配信して 2)計算させて 3)結合、集計する というステップが必要になる。 最後のステップ3を達成するために、データの再び配ることが必要となる。 全体として最適となるように、1),2),3)の効率を高める必要があるが、 あらゆるデータ、条件について同じ戦略で最高の効率を得ることはできず、 設計者が戦略を指定するパラメタとなっている。 この戦略を分散スタイルと呼んでいる. 分散スタイルとして以下の3通りが用意されている. 各々だけ読むとさっぱり意味がわからないが、結局のところ再分散のコストをいかに減らすか、 というところに着目すると合点がいく. EVEN 分散 特定の列に含まれている値と関係なくラウンドロビンで複数のスライス間で行を分散させる. テーブルが結合に関与していない場合や、キー分散、ALL分散のどちらが良いかわからない場合に指定する. キー分散 キー分散のキーとは結合キーのこと. 特定の列に含まれている値に従って複数のスライスに行を分散させる.キーが同じということは「同じデータ」であり「同じデータ」達を同じスライスに分散させる意味がある. 共通の列からの一致する値が同じスライスにまとめられるよう努力する. ALL 分散 テーブル全体のコピーが全てのノードに分散される. EVEN分散、キー分散によってテーブルの一部が各ノードに配置されているときにALL分散を行うことでテーブルが関与しているあらゆる結合で全ての行が確実にコロケーションされる. 何が嬉しいのかわかりづらいが、EVEN分散やキー分散では、クエリ実行に伴って、再び必要なデータをコピーする(再分散する)必要が発生する可能性が生じる.ALL分散であればその可能性がなくなる. AUTO 分散 (デフォルト) テーブルデータのサイズに基づいて最適な分散スタイルを割り当てる. まず小さなテーブルにALL分散を設定し,テーブルが大きくなるとEVEN分散に切り替える. 分散スタイルを明示的に設定しないとAUTO分散になる. まず、ファクトテーブル関連する1つのテーブルの共通の列に基づいて分散させる. 関連するテーブルの選び方の観点は大きさで最もレコード数が大きいテーブルを選択する. 以下の構造では、ファクトテーブルとディメンションテーブル1が dim1_keyというキーを使って結合している. そこで, ファクトテーブルのdim1_key、ディメンションテーブル1のdim1_keyを分散キーとして採用する.(緑) ここまでで、dim1_keyの値が一致するレコードが同じスライスにコロケーションされる. キー分散に使うキーは1組のみ. 残りのテーブルについてはEVEN分散かALL分散を用いる. 選び方は上記の通り. テーブルのサイズが小さいのであれば、ALL分散により再分配の可能性がなくなり選びやすい. 圧縮エンコーディング 通常のRDBのように行方向の固まりを記録する場合、各列の値は型や値の傾向がまちまちであるため、 一様に圧縮しようとしても高い圧縮率を得られない. 対して、列方向の固まりを記録する場合、各列の型は同じだし値の傾向が似ていることが多いため、 高い圧縮率を得られる可能性がある. ただし、値の傾向により圧縮アルゴリズムを選択する必要がある. 公式で挙げられているアルゴリズム. 結局試してみないとわからない、というのはある. (Zstandard強すぎないか?) raw エンコード 圧縮をおこなわない. ソートキーが設定されているときはrawエンコードが設定される BOOLEAN、REAL,DOUBLE PRECISION型もraw. AZ64 エンコード Amazon 独自の圧縮エンコードアルゴリズム より小さなデータ値のグループを圧縮し、並列処理に SIMD (Single Instruction Multiple Data) 命令を使用する 数値、日付、および時刻データ型のストレージを大幅に節約する バイトディクショナリエンコード バイトディクショナリエンコード ディスク上の列値のブロックごとに、一意の値の個別のディクショナリを作成する 列に含まれる一意の値の数が制限されている場合に非常に効果的 列のデータドメインが一意の値 256 個未満である場合に最適 CHAR 列に長い文字列が含まれる場合に特に空間効率が高まる VARCHAR 列に対しては、LZO などの BYTEDICT 以外のエンコードを使用する デルタエンコード 列内の連続する値間の差を記録することにより、データを圧縮 日時列にとって非常に有用 差が小さいときに特に有用 LZO エンコード 非常に長い文字列を格納する CHAR および VARCHAR 列、特に製品説明、ユーザーコメント、JSON 文字列などの自由形式テキストに適している Mostly エンコード 列のデータ型が、格納された大部分の値で必要なサイズより大きい場合に有用. たとえば、INT2 列などの 16 ビット列を 8 ビットストレージに圧縮できる ランレングスエンコード 連続して繰り返される値を、値と連続発生数 (実行の長さ) から成るトークンに置き換える データ値が連続して繰り返されることが多いテーブルに最適 たとえば、テーブルがこれらの値でソートされている場合など Text255 および Text32k エンコード 同じ単語が頻繁に出現する VARCHAR 列を圧縮する場合に有用 Zstandard エンコード 多様なデータセット間で非常にパフォーマンスのいい高圧縮比率を提供 製品説明、ユーザーのコメント、ログ、JSON 文字列など、長さがさまざまな文字列を保存する CHAR および VARCHAR 列に対して有用 圧縮エンコーディングをテストするためには、 各アルゴリズムで差が出るように大量のデータを用意する必要がある. 公式には、テストするために大量のデータを用意することは難しいので デカルト積ででっち上げる手法が案内されている. 例えば、こんな感じにデータをでっちあげる. create table cartesian_venue( venueid smallint not null distkey sortkey, venuename varchar(100), venuecity varchar(30), venuestate char(2), venueseats integer ); insert into cartesian_venue select venueid, venuename, venuecity, venuestate, venueseats from venue, listing; このうち、venunameに対して各エンコーディングアルゴリズムを適用して格納するデータを作る. create table encodingvenue ( venueraw varchar(100) encode raw, venuebytedict varchar(100) encode bytedict, venuelzo varchar(100) encode lzo, venuerunlength varchar(100) encode runlength, venuetext255 varchar(100) encode text255, venuetext32k varchar(100) encode text32k, venuezstd varchar(100) encode zstd); insert into encodingvenue select venuename as venueraw, venuename as venuebytedict, venuename as venuelzo, venuename as venuerunlength, venuename as venuetext32k, venuename as venuetext255, venuename as venuezstd from cartesian_venue; 知りたいことは、encodingvenueの各列で実際に使われているディスク容量. 以下のようにして各列で使用される1 MBのディスク ブロック数を比較するらしい. rawが203に対してBYTEDICTが10. つまりBYTEDICTにより20:1の圧縮率を得られた例. select col, max(blocknum) from stv_blocklist b, stv_tbl_perm p where (b.tbl=p.id) and name =\'encodingvenue\' and col < 7 group by name, col order by col; col | max -----+----- 0 | 203 1 | 10 2 | 22 3 | 204 4 | 56 5 | 72 6 | 20 (7 rows) まとめ 公式のベストプラクティスを追ってみた. 面倒だけれども結構力技で出来ているなという印象. 与えられたスタースキーマからある程度決まったやり方でパラメタを選択できそう. 実データでやったら迷うことは必至w ソートキーと分散スタイルの選択は分散コストに影響する. 圧縮エンコーディングの選択はディスクストレージに影響する. 理解したら実際に試行錯誤していくしかないイメージ.

default eye-catch image.

Amazon Redshift概要 パフォーマンス総論

概要 各論に入る前に総論。 MPPでクエリ実行するために必要な制限事項を設計/実装するために、 とりあえず、何故その制限事項が必要なのかを理解しておく必要がありそう。 [arst_toc tag=\"h4\"] 並列処理 MPP(Massively Prallel Processing). シンプルで安価なプロセッサを多数集積して一台のコンピュータとする手法. 各ノードの各コアは、同じコンパイル済みクエリセグメントをデータ全体の一部分に対して実行する。 テーブルの行をコンピューティングノードに分配し分散処理する。 列指向 通常のアプリケーションとデータウェアハウスではクエリで取得したいデータが異なる. 通常のアプリケーションは行の大方の列が欲しい一方で、データウェアハウスは行の中の一部の列が欲しい。 データウェアハウスが1行の全ての列を取得する方式を使用すると、ほとんどの列は無駄になってしまう. ある行の1列にだけ関心がある状況で15行分欲しい場合、行指向であれば100回、列指向であれば5回のディスクI/O。 データ圧縮 列指向で同じ列のデータを取る場合、同じ列に入るデータの乱れ方には傾向があるはずなので、データ圧縮が効きやすい。 データ圧縮によりディスクI/Oがさらに減少する。 クエリオプティマイザ MPP対応のクエリオプティマイザ。複数のコンピューティングノードで並列処理するための最適化が走る。 結果のキャッシュ リーダーノートでキャッシュする必要性があるクエリと結果をキャッシュする。 サイズの大きなクエリ結果セットはキャッシュしない。 キャッシュするか否かは、キャッシュ内のエントリ数とAmazon Redshiftクラスターのインスタンスタイプが含まれる。