LoginSignup
0
1

More than 3 years have passed since last update.

『失敗から学ぶRDBの正しい歩き方』を読んでいく(後編)

Last updated at Posted at 2020-08-03

前編は こちら

概要

11. 見られないエラーログ

アンチパターン

  • エラーログの軽視と無知。
    • 大きな問題を未然に防ぐチャンスを自ら潰している。
  • エラーログの運用設計をシステムの設計時に行っていない。
  • エラーログの吐き出し過ぎ。
    • 担当者が見なくなる大きな要因の一つ。

エラーログ出力の設計

  • 運用ログとして欲しい情報。

    • いつ(タイムスタンプ)
    • だれが(ユーザー)
    • どこに(データベース)
  • 監査ログとして欲しい情報。

    • どこから(クライアントのIPなど)
    • 何をしたか(実行されたSQLコマンド)
    • どうなったか(エラーコード、エラーメッセージ)

エラーログの監視

PostgreSQLの場合

  • 重要な通知を知るべきすべての人に素早く・正しく・漏れなく通知できること
    • Slackへの通知にはサーバ監視サービスのMackerelやサーバ監視ツールのZabbixなどを使い、「PANIC」などの文字列が出力された場合にアラートとして扱う仕組みが一般的。
深刻度 本番環境でのロギング 通知
DEBUG1~DEBUG5 場合による(デバッグで必要などの理由がなければ本番では行わず、ステージングのみで行う) しない
INFO 場合による(統計情報で取得できない場合など他で代替できないときのみ) しない
NOTICE する しない
WARNING する しない
ERROR する Slackの、エラー対応のためのチャンネル(errorチャンネルなど)に通知
FATAL する Slackの主要チャンネル(generalなど)に通知(@channel付き)
PANIC する Slackの主要チャンネル(generalなど)に通知(@channel付き)

まとめ

  • エラーログの種類について知り、適切な設計・運用を行う。

ログを見やすくする工夫

Elastic Stackを活用する
  • ログの集計結果をHTMLやCLI上でわかりやすく表示してくれるため、私達を大いに助けてくれるツール。
    • Logstashでログを集約、Elasticsearchで集計、Kibanaで可視化。
PostgreSQLの場合
  • PANICになってからでは、障害が発生しているため対応しとしては出遅れているので、ログレベルLOGのエラーログを集計して事前に問題を検知し、より早い初動で対応する。
MySQLの場合
  • Error, Waring, Note のうち、 Note 以外を監視する。
    • Elastic Stackの活用やホワイトリスト形式の文字列監視も良いプラクティス。

12. 監視されないデータベース

アンチパターン

  • RDBMS、ミドルウェアの監視(モニタリング)をする文化が存在しない。

ミドルウェアの監視の種類

1. サービス(プロセス)の死活監視

  • 「いち早く障害発生を確認できるようにする」ために必要な監視。

2. 特定条件のチェック監視

  • 「特定の条件を設けて、障害を未然に防いだり、復旧にいち早く取り掛かれたりするようにする」ために必要な監視。
    • 例)ディスク容量が90%になるとアラートが飛ぶ。

3. 時系列データをもとにしたメトリックス監視

  • 「状況の変化を時系列で管理することで、キャパシティプランニングや障害の予兆の把握に役立てる」ために必要な監視。

モニタリングの対象と実践

  • メトリックを見て、シナリオを考えることが大切。

OS側

  • ディスクI/O
  • ネットワークトラフィック
  • CPU利用率
  • メモリ利用率

RDBMS側

  • SELECT/INSERT/UPDATE/DELETEなどのSQLの実行量
  • 実際に読み込まれているレコードの量
  • インデックスヒット率
  • デッドロックの可否
  • テンポラリファイルの作成の有無
  • ロックの量と時間

MySQLのモニタリングツール

PMP
  • ZabbixやCactiのプラグインとして公開されており、手軽に使うことができる。
    • MySQLの項目をかなり細かく取ることができる。

PostgreSQLのモニタリングツール

pg_monz
  • Zabbixのプラグインとして使うことができる。

まとめ

  • モニタリングを続け、育てることが大切。
    • モニタリングはインフラの見える化。
      • プログラミングに対するテストコードと同じ。
    • パフォーマンスのモニタリングを、インフラエンジニアとアプリケーションエンジニアが一緒に、1~2週間に1回は行うようにする。
  • モニタリング後のチューニングが大事
    • プログラミングに対するリファクタリングと同じ。

12-2. ミドルウェアの監視の種類

コラム 可視化と改善は両輪

13. 知らないロック

アンチパターン

  • 意図せずロックがかかり問題が発生する。
    • ロックの粒度や性質を知らない。
    • DBの負荷同様にロックをモニタリングの対象にしていない。

ロックの基本

デッドロック

  • 「複数のトランザクションが、もう一方の処理が終わるのをお互い待って身動きが取れなくなっている状態」
    • デッドロックが発生しないように、ロックでは取得の順番が大切。
MySQLのギャップロック
  • 対象が存在しなくてもロックを取る
    • 「INDEX値を持つ行」と「INDEX値を持つ行」の間にあるギャップ
    • 先頭の「INDEX値を持つ行」の前に存在するギャップ
    • 末尾の「INDEX値を持つ行」の後に存在するギャップ
MySQLのネクストキーロック
  • 対象よりも1つ先の行までロックを取る。

明示的ロック

  • 自ら宣言して取得するロック。
    • 例)外部キー制約を利用した状態で子テーブルに対する更新を行った場合は親テーブルがわに共有ロックを取得する。

まとめ

  • RDBMSごとのロックの振る舞いを知っておく。
    • 時にはクリティカルなバグをうみ、最悪サービスが停止することもある。
      • ロックの振る舞いについてはドキュメントを読み、手を実際に動かし、正しい知識を身につける。

14. ロックの功罪

アンチパターン

  • トランザクション分離レベルを、理解せずに変更してしまう。
    • データの一貫性が失われてしまう。

ACID

  • RDBMSの保証する要件。
説明
Atomicity(原子性) ・ トランザクション内の操作がすべて実行されるか、されないかを保証する
・ 操作できる最小の単位であり、実際の処理としては、COMMITですべて実行され、ROLLBACKで全て取り消される
Consistency(一貫性、整合性) ・ 指定された状態に対して整合性がある、一貫性がある(金額は負の値を取らないなど)ことを保証する
Isolation(分離性、独立性) ・ 実行中のトランザクションが他のトランザクションに影響を与えないことを保証する
・ 実行中のトランザクションの状態は参照・変更できない
Durability(永続性) ・ 一度コミットされたトランザクションは、何があっても残されることを保証する
・ システム障害が発生しても、コミットされたトランザクションの結果は残り、なおかつ復元できる

Isolation

  • Isolationを完全に担保しようとすると、直列処理で1件ずつ処理する必要がある。しかし、実際にRDBMSは並列に処理できる。
    • これはIsolationの制限を、 トランザクション分離レベル によって緩めているから。

トランザクション分離レベル

  • 下に行くほど並列度が低い。
    1. read uncommitted = Isolationを犠牲にする代わりに並列度を高めている。
    2. read committed
    3. repeatable read
    4. serializable = Isolationを担保している状態

トランザクション分離レベルと関連する現象

1. ダーティーリード

  • 他のトランザクションから自分のコミットしている変更内容が見えてしまう現象。
    • 並列処理の際、データの不整合が発生する場合がある。

2. ファジーリード

  • 他のトランザクションのコミットしていないデータは見えない。
  • トランザクションの途中に他のトランザクションがコミットした変更が見えてしまう。

  • PostgresQLのデフォルトのトランザクション分離レベルは read committed のため、ファジーリードは発生する。

  • MySQLのデフォルトは repeatable read のため、トランザクション分離レベルを変更しない限り発生しない。

3. ファントムリード

  • 他のトランザクションがコミットした追加・削除が見えてしまう現象。

4. ロストアップデート

  • 複数のトランザクションで更新が並列に行われた場合、後に実行されたトランザクションで結果が上書きされる現象。
ダーティーリード ファジーリード ファントムリード ロストアップリード
read uncommitted 発生 発生 発生 発生
read committed 起きない 発生 発生 発生
repeatable read 起きない 起きない 発生 発生
serializable 起きない 起きない 起きない 起きない

まとめ

  • 並列度を維持しながらデータの一貫性を担保するために、適切にロックを取得する。
    • トランザクション同士のデータの一貫性を保つには、 serializable を設定して直列処理で順番に処理するほかない。
    • 並列度を維持しながらデータの一貫性を担保するためには、ファジーリードなどを防ぐために、排他ロックを取得すれば、COMMITするまで参照を待たせることでできる。

15. 簡単過ぎる不整合

アンチパターン

1. テーブルを作って正規化するのが面倒なとき

  • テーブルを分けた方が良いことを理解しておきながらも、実装の都合を優先して非正規化してしまう。
    • 非正規化を行っていると、アプリケーション側で簡単にデータが壊れてしまう。

2. 外部キー制約によってデッドロックなどが発生しているとき

  • 外部キー制約を外すことに加え、非正規化することによって対処している現場が多い。
    • 親テーブルに対して正しく共有ロックを取っていれば防げる問題。

3. 正規化によってJOINのコストが高くなり、パフォーマンスに問題が出ているとき

  • ケースバイケースと言える難しい問題。
    • 正規化を行えば重複はなくなり、データの全体量は少なくなる。結果メモリに乗りやすくなり、RDBMSとしては高速に処理しやすくなるはず。

まとめ

  • 非正規化は基本的には行わず、必要になったタイミングではデータベースの設計に問題がないか、他の代替手段はないかをよく検討する。
    • 非正規化に見えても非正規化でないデータもあり、これを勘違いすると「失われた事実」と同じ話になってしまう。

代替手段

  1. CHECK制約
  2. ENUM型

16. キャッシュ中毒

アンチパターン

  • しっかりと検討を行わずキャッシュの利用を決めてしまう。

キャッシュのデメリット

  • キャッシュしたデータの状態を意識することが難しく、参照時にどの状態なのかコード側からは直感的に把握しづらい。
  • キャッシュしたデータのデバッグが難しく、どのデータがキャッシュされているかを把握しづらい。
  • キャッシュを保存しているストレージが稼働しなくなったためにキャッシュが消失し、サービスが停止してしまう。
  • キャッシュが意図しないデータを参照してしまい、個人情報流出等、見えてはいけないデータが見えてしまう。

キャッシュの種類

クエリキャッシュ

  • 基本的に利用されていない。
    • 頻繁に更新されるテーブルではむしろパフォーマンスが落ちる。
    • 全く更新がされないテーブルの場合、クエリキャッシュよりもアプリケーションや他のデータストアなどに持たせた方が効率が良い。

マテリアライズド・ビューとサマリーテーブル

  • 実行されたSQLの結果を、テーブルとして保存している状態。

    • INDEXを貼ったり、マテリアライズド・ビューに対して新たなクエリを実行できたりする。
  • クエリキャッシュと同様に、頻繁にマテリアライズド・ビューを更新する必要があるようなケースでは、逆にパフォーマンスが落ちることがある。

アプリケーションキャッシュ

  • サーバーサイドキャッシュともいう。アプリケーションフレームワークがサポートしていることが多い。
    • 一度RDBMSから取得したデータや作成したデータをアプリケーションのキャッシュとして利用できる。
    • 場合によっては作成したHTMLを丸ごとキャッシュし、再度リクエストが来た場合にそれを返すことができる。
  • RDBMSの処理だけでなく、アプリケーションの処理も省略するため高速に処理できる。
  • 保存先として、アプリケーションのプロセスない、ファイル、memcached、Redisなどがある。

そのほかのキャッシュ

CDN(Content Delivery Network)
  • HTTPレイヤのキャッシュ。

PWA(Progressive Web Apps)

  • ブラウザや端末のローカルストレージを利用したキャッシュ。

まとめ

  • キャッシュヒット率や更新頻度を推測、計測する
    • キャッシュを利用しても、キャッシュヒットしなければ高速化しない。
  • キャッシュの対象と範囲を見極める
    • キャッシュする範囲が広い分取り回しが難しくなるため、キャッシュの対象と範囲は小さくすることが定石。
  • キャッシュのキーを決める
    • キャッシュを取り出す場面をしっかりと想定したキー名にする。
  • キャッシュの生存期間と更新方法を決める
    • 永続するキャッシュは運用時に消していいかの判断が難しくなるため、生存期間を決める。

17. 複雑なクエリ

アンチパターン

  • 無知ゆえの豪腕
    • スキル不足に起因した、力技による解決としての複雑なクエリ
  • 腐ったテーブルの腐ったクエリ
    • テーブル設計に問題を抱えており、目的を達成するため結果的に複雑になったクエリ

まとめ

  • パーツで読み解く
    • SQLの構文評価順にパーツに分けて読み進めると良い。
      • パーツに分けることで不要なパーツが見えてくる。
  1. FROM
  2. ON
  3. JOIN
  4. WHERE
  5. GROUP BY
  6. HAVING
  7. SELECT
  8. DISTINCT
  9. ORDER BY
  10. LIMIT
  • JOINの場合はベン図を書くのも良い。

  • 意図と背景を読み解く

    • テーブル設計が問題の場合、分けたパーツの中で特に複雑どの高いパーツがあるはず。
      • そのような問題のあるパーツは、別のクエリとして分け、アプリケーション側で実行結果を加工した方が取り扱いやすいケースが大半。
        • クエリの責務を分け、複数回実行する。
  • コードレビューを通したスキルアップによって防げる。

18. ノーチェンジ・コンフィグ

アンチパターン

  • コンフィグの設定、管理が行われていない。
    • コンフィグの設定がデフォルトのまま、minimum。
      • メンテナンスは属人化しやすく、一度設定されるとそのまま放置されることが多い。

コンフィグを知る

  • ドキュメントを見て、設定できるパラメータを知る。

コンフィグの役割

1. パフォーマンスに関わる部分。
  • どのくらいのメモリを使い、どのくらいのコネクション数を用意するかを決める。
    • RDBMSがサーバーの能力を活かせるかどうかに関わる。
2. セキュリティ
  • アクセスできる範囲やSSLの利用を設定することでデータを守る。
3. RDBMSの振る舞い
  • トランザクション分離レベルの設定。
    • MySQLでは「SQLモード」と言われる、SQLに対するRDBMSの振る舞いを設定できる。

コンフィグを管理する

  • 汎用的で使い回すことができる。
    • ミドルウェアのコンフィグのバックアップをちゃんと取る。

まとめ

  • コンフィグの役割や意味を知り、適切に管理する。

  • Infrastructure as Code

    • コンフィグをバックアップしてファイルとしてローカル管理するのではなく、gitのリポジトリで管理する方法などがある。
    • Ansibleのようなプロビジョニングツールでインフラ全体を管理する方法がある。
      • コンフィグの作成、配置以外にもミドルウェアのインストールやアカウントの作成もできるため、構築手順書をまさにコードで管理するような形になる。
        • コードレビューやCIの仕組みも利用できることから多くのメリットを得られる。
  • バージョンごとのコンフィグの違い

    • メジャーバージョンが変わると動作しないことも多い。
      • リリースノートをはじめとする情報を揃えた上で動作確認をする。
  • コンフィグのチェックツール

    • コンフィグのチューニングをするためのサポートツール
      • PostgreSQLではPgTune
      • MySQLではMySQL Tunner
  • Database as a Service

    • Amazon RDSやGoogle Cloud SQLなど。
      • フルマネージドサービスであるため、サーバに合わせたパラメータチューニングが行われた状態で利用できる。
      • バックアップの仕組みや参照ようのレプリケーションを作成する仕組みなども用意されている。
      • バージョンごとのコンフィグの差異も吸収してくれる。

19. 塩漬けのバージョン

アンチパターン

  • バージョンアップを恐れ、最新バージョンに追従することを怠った運用の放棄。

なぜバージョンアップは重要なのか

マイナーバージョンアップ

  • バグ対応やセキュリティアップデートが中心。

メジャーバージョンアップ

  • 機能追加や機能改善が行われる。

バージョンアップをする理由

  • 現在利用しているバージョンのサポートが切れるため(旧バージョンを使い続けることによるセキュリティリスク)
  • 現在利用しているバージョンを使用することによる管理コストの増加
    • gccのバージョンが違ってbuildできない、rpmパッケージがないなどインストールできる環境がなくなる
    • AUTO VACUUMなどの新機能によって、相対的に運用コストが下がる
    • 複数バージョンを固定で管理している場合、それぞれ異なるバージョンの管理が必要
  • 新機能を利用したい
  • パフォーマンスを向上させたい

バージョンアップをしない理由

  • 停止時間とメンテナンス時間
    • マイナーバージョンアップであればデータベースを再起動するだけの数秒で済むことも多いが、メジャーバージョンアップは、データベースの入れ替えやアップデートするための専用ツールを利用する必要がある。
      • ローリングアップデートなど、技術力次第では十分に短い時間でバージョンアップ可能。
  • アプリケーションへの影響
    • 開発費の制約や人員不足を問題として、アプリケーションの振る舞いが変わるかもしれない場合のメンテナンスコストが払えない

バージョンアップの方法

メジャーバージョンアップの方法

  • それぞれの手法にメリット・デメリットがあるので、自分たちに合った手法を選ぶ。
項目 停止時間 難易度 細く
ダンプ・リストア 長い(データ量に比例) 簡単 全てのバージョンに対応
専用ツール 一定 簡単 RDBMSによってやり方が違う
レプリケーション 切り替え時間のみ 中程度 RDBMS、バージョンによってやり方が違う
アプリケーションからの二重書き込み 切り替え時間のみ アンプリケーションの設計次第 工数はかかるが部分的な切り替えも可能

1. コンフィグの確認

  • メジャーバージョンアップではコンフィグでパラメータの差異が発生する。
    • 新しいものや廃止されたものを、リリースノートや公式ドキュメントを見て確認する。

2. リハーサル

  • サービスの停止作業、バージョンアップの作業、ロールバックの手順を本番データと同様の環境で数回行う。

3. バージョンアップ作業

  • 旧データベースに戻して復旧できるように、ロールバックの手順と判断基準を事前に明確にしておく。

4. バージョンアップする文化を作る

  • バージョンアップの実績を作り、小さなシステム停止はサービスに問題がないことや、バージョンアップはシステムに好影響を与えることをビジネスサイドに理解してもらうことから始める。
  • RDBMSが難しい場合は、プログラミング言語や他のミドルウェアなどから初めていくのが良い。

まとめ

  • バージョンアップの文化を作り、最新版に追従していく。
    • メジャーバージョンを一気に飛ばして上げるのは大変なので、小さくバージョンアップしていくことが大切。

20. フレームワーク依存症

アンチパターン

  • フレームワークに合わせた設計を行うことで、RDBMS側の機能やデータの整合性が失われてしまう。
    • バグとヒューマンエラーからデータを守れない。
      • 加えて、スロークエリの発行元を追うことの難しさもある。

フレームワークのデメリット

マジックビーンズ

  • 以下のような問題がある。

  • CRUD機能を公開することでビジネスロジックのModelとは別の場所から呼ばれることがある。

  • Controller層やService層にビジネスロジックが記載された「ドメインモデル貧血症」をもたらす。

  • データベースにアクセスする場所が散財し、ビジネスロジックがデータに依存するため、ユニットテストが難しくなる。

テーブル設計がViewに依存する

  • ModelがViewに依存し過ぎると、「1フォーム1列」のケースになってしまう。
    • シンプルな実装になる反面、不要な列を作ってしまったり、正規化に失敗したりする原因になる。

テーブル設計がライブラリに依存する

  • STI(Single Table Inheritance)を採用すると、子クラスが増えると同時にカラムも増え、NOT NULL制約も付けられない。
    • RDBMSの効率的なクエリとデータを守る仕組みを犠牲にする。

独自型への制約

  • ORMに合わせた結果、RDBMSの便利な独自型を利用できない。

漏れのある抽象化

  • ORMが発行するSQLが見えない。
    • 発行されているSQLを意識する。
      • N+1問題やINDEXを利用しないJOINなど。

まとめ

  • ORMを利用する上で重要なことは、DOA(Data Oriented Approach)に基づき、Modelの中でデータを取り出す層と取り出したデータ型を加工する層を分けておくこと。
  • ビジネスロジックをコードに落とし込む設計力と、全体を見渡して設計できるような視野の広さが必要。
    • データベースにフレームワーク都合の問題を押しつけず、トレードオフを常に意識する。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1