ちょっと話題の記事

その設計、本当に正しいですか?

2016.12.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こむろです。今年も残すところあと数日。札幌は本日(2016/12/29)は雪と風が吹いていて外出しようとする心を折りにかかってます。

IMG_20161227_135536

2016年は、設計・実装・膨大なデータ移行、そして現在サービス運用からインフラ保守のお手伝いと、開発と構築と運用の3つの役割を行き来していた一年でした。今までは開発一辺倒でしたので、本格的にサービスの運用を経験したところ色々と学ぶところが多かったので、1年の最後としてちまちまと書き下してみます。

自分の所属するプリサーでは、普段AWSのサービスを積極的に利用しながらモバイルを中心としたシステムの設計・開発から運用まで行っています。最近では開発だけではなくその後の運用も含めてサポートさせて貰う機会もあり、稼働した後も安定したシステムの運用ができるよう日々奮闘しています。そんな中でAWSならではの現象や制限が当然あるため、なかなか今までの知識や想定ではうまくいかない部分も出てきます。クラウドを前提としたシステム設計をする上で大事なこと、運用する上で考慮しておいた方が良い点など、今までぼんやりと曖昧模糊としか認識できていなかった事項でした。

いずれも皆今までの設計でも当然のように気をつけて設計してきた部分ですが、どうも人はお金を払って提供を受けるサービスについては、100%稼働することを期待してしまうようで、AWSが提供するサービスだから大丈夫、と盲目的に信じてしまったりするようです(というか自分がそう)

今一度、きちんと安定したシステムを設計するためにどのようなポイントを確認すれば良いかを確認します。(と言ってもこのポイントも完全ではないですが)

AWS上で稼働するシステムを設計するということ

私達は普段の業務ではAWSの提供するサービスを基礎として、お客様の望むシステムを設計・構成・実装から運用することで価値を提供しています。特に運用面においては、AWSサービスの障害や停止の影響を最小限にしながら、利用者の利便性を損なうことなく、日々安定的なサービスを提供できるように設計しています。ただ単にAWSのサービスを繋げるだけでシステム構成が完了するならば、僕らのようなエンジニアは必要ありません。AWSが提供する多種多様なサービスやツール、それぞれの特性や弱点を理解し、それらを組み合わせ、さらに足りない部分は自分たちで実装することで、一つの大きな安定したシステムを構築しています。それまでのオンプレ環境での設計とはまた少し異なる視点を持ちながらも、旧来のオンプレ環境での設計の良いところも取り入れ、よりAWSに適した設計を行うことが求められています。

マネージドサービスの障害、停止、能力低下は必ず起こる

AWSが管理してくれているため、恐らく自分たちが管理するよりかはずっと低いですが、それでもどのサービスでも障害は発生します。複数のAvailability Zoneにレプリケーションを分散させるなど、完全に停止することを避けるなど、様々な対策も講じられていますが、やはりどうしても0にすることはできません。

しかし、「AWSのサービスが止まってしまったからシステムが止まってしまうのは仕方がない」で良いのでしょうか。サービス障害の影響を最小限に抑えつつ、可能であれば自動的に復旧できるシステムを設計することがベストだと思います。

異常に気づくことができるのか

稼働後のシステムにおいて大切なのは、障害や異常が発生した際にいち早く気づくことができるかです。気づかない障害は時が経つうちに傷口を広げ、深刻な状況をもたらします。そのため、異常が発生したことに気づくことはとても大切です。当然ながら設計の段階から異常に関しての考慮は欠かさず行います。前段でも述べましたが、フルマネージドなサービスとは言え停止(または能力低下)しないものはありません。これらを検知し適切なコントロール配下に置くことは安定したシステムを提供する上ではとても大切なことです。

AWSのマネージドサービスには様々なメトリクスが提供されており、これらを適切に設定することでCloudwatchにてきめ細かな異常を検知することが可能です。

利用者のデータは守られているか。正常な状態に復旧できるか

稼働したシステムは、稼働して初めて価値があります。実装したら終わりではありません。AWSのサービスが一部障害があろうとシステムの利用者には関係ありません。利用者にとっては必要なものは稼働しているサービスそのものです。そのため、何らかの障害が発生した場合でも、データの不整合などは極力発生しないように設計するべきです。AWSには異常を検知した際に自動的にリトライされる機能や、必ず一度以上は実行することが保証されている機能など、そのサービスによって復旧やリトライを行ってくれるものもあります。それぞれのサービスの特性を知った上で

  • 多重に登録される可能性がないか
  • 処理の途中で失敗して中途半端なデータが登録されないか
  • 不整合を起こしたデータが発生したとしてそれを検知することは出来るのか、そしてその復旧方法は

マネージドサービスの組み合わせを行った際、ある一部のサービスのみ障害が発生しているというのはよくあることです。これらを考慮した上でサービスの取捨選択を行っています。システムが正常に稼働している状態に戻すことも大切ですが、利用者の処理やデータに不整合がでないようにすることも大切です。

とまあ

皆あまり明文化したりすることはありませんが、こんな心構えで仕事してます、という話しでした。

本題はここからでDynamoDB Streams, Lambda, S3を使った簡単な実例からAWSのマネージドサービスを使って設計する際に、上記のようなポイントに注意して簡単なサンプルを作成してみます。ここでは、サービスに対しての正しい理解がなければ安定した設計は難しいというのを示せれば良いかなと思っています。

DynamoDB StreamsのEvent Trigger

DynamoDB StreamsのEvent Triggerは皆様よくご存知かと思います。DynamoDBのテーブルのItemが 作成更新削除 されたイベントをトリガに、いくつかのActionを実行出来る機能です。よくLambdaなどと組み合わせて利用される事が多いのではないでしょうか。 以下の要件を満たすちょっとしたサンプルを作成してみます。

要件

  • 登録されるデータは、ユーザーのハッシュID、バージョン番号のみ
  • 登録されているユーザーの更新されたバージョン全てのダンプデータを全てS3へ記録しておきたい

スクリーンショット 2016-12-26 1.08.50

こんな構成です。なんらかのクライアントからDynamoDBのテーブルのItemが作成されたり、既存のデータを更新したりします。各Itemは、version番号を持っており、この値はAtomicカウンタとして更新されます。Itemの情報が変化したら一意なhashIdとversionを連結した名前のJSONファイルを作成し、それをS3のBucketへアップロードします。とても単純ですが、Event Triggerを使いながらversionの履歴がファイルとして残っていく構成です。

問題点?

DynamoDBへ登録、更新を行う簡単なスクリプトを書いてみると分かりますが、意図通りの動作を行うことが分かります。

DynamoDBへ新たなユーザーを登録するケース

  1. 新たなユーザーID(A000001)を発行してversionをデフォルト値0で登録
  2. Lambdaが起動される
  3. S3の指定Bucket以下に「A000001-0.log」が作成される

スクリーンショット 2016-12-30 0.57.51

スクリーンショット 2016-12-30 0.58.55

DynamoDBの既存のデータを更新するケース

  1. 既存のユーザーID(A000001)を指定し、versionをインクリメント
  2. Lambdaが起動される
  3. S3の指定Bucket以下に「A000001-1.log」が作成される

スクリーンショット 2016-12-30 0.59.29

何か問題があるでしょうか?

前段でも書いたように、AWSの提供するサービスは常に100%稼働し続けるものではありません。そのため、今回利用しているDynamoDB Streams, Lambda, S3が何らかの障害やメンテナンス等で停止した場合は適切に復旧が可能することができるかが不透明です。確認してみましょう。

AWSのサービスが提供するログ

稼働するシステムにおいてログはとても大事です。ログがないことは運用時に提供するサービスの死を意味します。処理のトレーサビリティに優れたログの適切な設計はサービス設計を行う際の死活問題です。AWSのマネージドサービスは、利用者があまり手出しを出来ない代わりに色々なログを出力しています。今回作成したちょっとしたアプリケーションではどのあたりでログが出力されているか確認してみましょう。

スクリーンショット 2016-12-26 1.09.21

しかし、これらはマネージドサービスはAWSが管理しているリソースであるため、僕ら利用者は自由にログを編集出来るわけではありません。そのため、マネージドサービスに依存した設計を行っている場合、想定される障害に対して適切なログが提供されているかは大変重要です。

この場合、DynamoDBへ書き込むクライアントプログラムは適切にログが出力されているとして、DynamoDB Streamsの箇所のログが出力されていないことが分かります。これは大きな問題です。LambdaはEvent Triggerによって起動されるため、Event Triggerが正常に起動したかどうかを確認する術がありません。ログから現象を確認しようとしても データに変更がないのか, Event Triggerが障害によって起動できないのか を判断することができません。

解決は可能?

現時点(2016/12/28時点)では不可能と思われます。DynamoDB StreamsはKinesis Streamと同様にシャードを利用しているようですが、そのシャードがどのコンテナで起動されているかなどは我々には知るよしもありません。さらにシャードではどのような処理が行われているかもこちらが介入できるものではないため、ログを出力する処理を入れることも出来ないと思われます。

サービスの監視

サービスに何らかの障害やエラーが発生した場合には、適切な監視を施すことで検知することができます。AWSサービスの場合はCloudWatchに集約されていますね。マネージドサービスにはいくつかのメトリクスがあり、これらを設定することでマネージドサービスに対して適切な監視を行うことができます。 またサービスはエラーが発生した場合に自動的にリトライする機能を有しているものもあります。今回の構成ではどこにどのような監視が入っているかを見てみます。

スクリーンショット 2016-12-30 0.11.18

さて、こちらでもDynamoDB Streamsに監視する項目が存在しないことが大変気になります。ストリームデータは24時間前後を保存を保証してくれているようですが、正常に動作しているのかは分かりません。

この設計における一番の問題点

現時点(2016/12/28)でDynamoDB Streamsに適切な監視を入れることが出来ないため、シャードに問題が発生した場合に障害を検知することも出来ず、24時間以内に処理が再実行されて成功するのを結果のBucketの中のファイルを見て判断するしかないという状況です。

さらに悪い事におよそ24時間後に起動されたシャードのコンテナは破棄されると思われます。そのため、シャードの中に残っていたはずのversionの履歴はロストします。Lambda側には多くのメトリクスが提供されており、Invocation(実行回数)で監視する方法も考えられますが、起動されないのが データに本当に変化がないのか, Event Triggerが障害によって発火できないのか を判断することはできません。

文章だとわかりづらいかもしれないので図にしてみました。

スクリーンショット 2016-12-29 23.57.54

こちらの例の場合、DynamoDBのレコードはすでにVersionは 5 まで進んでいます。1, 2 はすでにS3へ正常にダンプされています。シャードに 3, 4 のデータが停滞しています。

最終的に異常が検知できるのは、S3のBucketのファイルのVersion情報が一部飛んでいるという結果から判断する他ありません。そしてこの場合、ロストしてしまったデータの復旧は困難です。

こんな現象起きるの?

起きます。というか実際に起きました。

常日頃大量のコンテナやリソースが立ち上がり、破棄されているため、極稀にシャードが正常に動作しないものをひくことがあるようです。シャードが正常に動作せず、Event Triggerが発火しなければ、当然Lambdaは起動せず更新されたデータのフローは闇の藻屑と消える運命です。

DynamoDB Streamsには適さない設計のパターンが有ることを知りましょう

DynamoDB Streamsは起動されない可能性があることを知り、履歴情報や確実にデータの変更を時系列で記録しておかねばならない箇所には向きません。最終的な結果のみが整合されていれば良い要件の場合は今回の設計でも問題ないかもしれません。

DynamoDB Streamsの機能は大変強力で適切に利用すれば、Key-Valueのデータストアの表現力をさらに高めてくれる機能です。しかし、対象のデータの性質を見極めずに単に「できそうだから」という理由で導入することは適切な技術選定とは言えません。障害が発生した場合など適切に復旧が可能なのか、検知が可能なのかを今一度見直したほうが良いかと思います。

  • ユーザーのポイント獲得・利用履歴
  • データの編集履歴
  • 入院患者の心拍数データ
  • 買い物履歴

上記は一部の例ですが、時系列で並びかつロストがあってはいけないようなものがDynamoDB Streamsに依存している形の設計にはなってないでしょうか?

解決策?

現状DynamoDB Streamsを監視するメトリクスがないため、異常を検知することは出来ません。24時間以内であればシャード内のデータから復旧は可能です。しかし、あまり現実的な回答ではない気がします。

DynamoDBの更新の変化を、最終的にS3のBucketにダンプしたファイルを配置できれば要件を達成できます。そのため、変化したデータダンプをSQSに入れてS3のBucketに結果が格納されるかどうかをチェックする、なども考えられるでしょう。

また、DynamoDBのデータを登録のみ許可するというのも一つ手かもしれません。更新がなくなるため、全ての更新レコードがDynamoDBのテーブルに存在することになります。膨大なレコード数になるのは想像ができますが、どんなに件数が多くてもパーティションキーの設計次第では、検索時間が一定のままデータを抜き出すことも可能になると思われます。

検証したサンプルについて

一応おまけ程度にサンプルで用意したものを掲載しておきます。

DynamoDB

スクリーンショット 2016-12-30 0.26.57

S3

スクリーンショット 2016-12-30 0.27.17

Lambda

from __future__ import print_function
import boto3
import json

print('Loading function')

def lambda_handler(event, context):
    #print("Received event: " + json.dumps(event, indent=2))
    for record in event['Records']:
        dynamodb_record = record['dynamodb']
        print("DynamoDB Record: " + json.dumps(dynamodb_record, indent=2))

        # get partition key
        keys = dynamodb_record.get("Keys", {})
        for key in keys:
            print("Key: %s" % key)

        # get NewItem
        new_items = dynamodb_record.get("NewImage", {})
        if (new_items):
            hashid = new_items.get("hashid", {}).get("S", "")
            version = new_items.get("_version", {}).get("N", -99)
            print("hashid: [%s], version: [%d]" % (hashid, int(version)))
            putS3file(hashid, version)

    return 'Successfully processed {} records.'.format(len(event['Records']))

def putS3file(hashid, version):

    path = "logs/%s-%s.log" % (hashid, str(version))
    print("put s3 : %s" % path)
    log_object = {
        "hashid" : hashid,
        "version" : version
    }

    s3client = boto3.client("s3")
    res = s3client.put_object(
        Bucket='blog-lambda-log',
        Key=path,
        Body=json.dumps(log_object)
    )
    print(res)

まとめ

「とりあえずデータストアはDynamoDB」という思考停止で技術を選定するのではなく、要件の周囲の状況から正しく選定されるべきです。恐らく実現が不可能なことはほとんどありません。しかし、そのサービスの組み合わせが提供する信頼性と、必要とされるデータの種類や属性(最終的に結果が合ってればOKなのか、それとも厳密に管理されるべきものなのか等)がマッチしているのかを熟慮した上で、選択されるべきかと思います。今更ではありますが、こういったことを考えずに何となく組み合わせて、何となく実装し、何となく動かしていると、予期せぬ事態に後々対応のコストが高くなる可能性があるな、ということを実経験をもって学んだ1年でした。 *1

2016年のre:Inventでも様々な新たなサービスが発表され、なかなか追うのが大変ですが、既存のサービスのきめ細やかなアップデートに今後も期待しつつ今年最後のエントリーとします。来年もがんばるぞ。

良いお年を。 2016/12/29 体感温度-11℃の札幌にて。

IMG_20161229_180244

脚注

  1. というか、AWSのマネージドサービスで意図的にFailureを起こして試験できるような環境は揃えておいてほしいなぁ・・・。レアリティの高い障害の場合、どうしても検証が難しいもので。。。