読者です 読者をやめる 読者になる 読者になる

めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Cloud DatastoreのEventual Consistencyに関するメモ(google.cloudクライアント編)

何の話かというと

Cloud Datastoreに対するQueryは、"Ancestor Query" を使用する事でStrong Consistencyが保証されます。逆に Ancestor Query を使用しなかった場合にどのような現象が発生するのかを雑多にメモしておきます。ここでは、GCEのVMからDatastoreにアクセスする前提で、google.cloudクライアントを使用します。(現在、ndbクライアントはGAE限定なので。)

Ancestor Queryとは?

Cloud Datastoreに格納するEntityは、それぞれに「親」Entityを指定することで、ツリー状のグループを構成します。1つのツリーを「Entity Group」と呼びます。Ancestor Queryは、Queryの検索条件として「Ancestor Key」を指定して、検索範囲をその下にぶら下がったEntityに限定することを言います。(親Entityは、自分と同じKindである必要はありませんので、Entity Gorupには、さまざまなKindのEntityが入り交じる点に注意してください。)

一方、Ancestor Keyを指定しない場合は、Datastoreに格納されたすべてのEntityが検索対象となります。これを「Global Query」と呼びます。

プロパティによるGlobal Query

まず次のスクリプトで1秒ごとにEntityを生成します。EntityのID(Key name string)の他に、'myid' というプロパティに同じIDの文字列を格納します。プロパティ 'timestamp' にはEntityを生成したタイムスタンプを入れておきます。

writer.py

#!/usr/bin/python

from google.cloud import datastore
import time, datetime, os, uuid

project_id = os.environ.get('PROJECT_ID')
ds = datastore.Client(project_id)

os.remove('/tmp/tmp0')
for i in range(1000):
    myid = str(uuid.uuid4())
    key = ds.key('Kind01', myid)
    ent = datastore.Entity(key)
    ts = datetime.datetime.now()
    ent.update({'timestamp': ts, 'myid': myid})
    ds.put(ent)
    with open('/tmp/tmp0', 'a') as file:
        file.write('%s\n' % myid)
    print ts, myid 
    time.sleep(1)

実行するとこんな感じ・・・

$ ./writer.py 
2016-10-26 00:50:27.589180 36a1fca7-a4f8-4d74-b2e0-5a8521f68cff
2016-10-26 00:50:28.749832 e1cb00a0-9ccc-4558-b7d6-2bb41b10be56
2016-10-26 00:50:29.866750 8245581d-2b0c-4888-90e4-710f683837e9
2016-10-26 00:50:30.950333 b1058f07-3c58-4ad1-8ea1-c4191be2bcbe
2016-10-26 00:50:32.061670 9874503a-7318-4d96-a0ca-78668f736205
2016-10-26 00:50:33.229060 be9fd3f0-d2e6-4e53-a748-4d26b766e7d8
...

この時、ファイル /tmp/tmp0 にEntityのIDが追記されていきます。

同時に次のスクリプトを実行して、/tmp/tmp0に書き込まれたIDを用いて、'myid' = ID という条件でQuery(Global Query)を実行します。

reader.py

#!/usr/bin/python

from google.cloud import datastore
import time, datetime, os
import subprocess

project_id = os.environ.get('PROJECT_ID')
ds = datastore.Client(project_id)

f = subprocess.Popen(['tail','-F','/tmp/tmp0'],
                     stdout=subprocess.PIPE,stderr=subprocess.PIPE)

while True:
    myid = f.stdout.readline().strip()
    query = ds.query(kind='Kind01')
    query.add_filter('myid', '=', myid)
    ts = datetime.datetime.now()
    print ts, 'Trying to find ', myid
    while True:
        iter = query.fetch()
        ent, _, _ = iter.next_page()
        if len(ent) == 0:
          print 'Failed and retry...'
          continue
        ts = datetime.datetime.now()
        print ts, 'Succeeded.'
        print ent[0]['timestamp'], myid
        break

実行するとこんな感じ・・・

2016-10-26 00:50:27.748414 Trying to find  36a1fca7-a4f8-4d74-b2e0-5a8521f68cff
Failed and retry...
Failed and retry...
Failed and retry...
Failed and retry...
2016-10-26 00:50:27.948764 Succeeded.
2016-10-26 00:50:27.589180+00:00 36a1fca7-a4f8-4d74-b2e0-5a8521f68cff
2016-10-26 00:50:28.865377 Trying to find  e1cb00a0-9ccc-4558-b7d6-2bb41b10be56
Failed and retry...
Failed and retry...
Failed and retry...
Failed and retry...
2016-10-26 00:50:29.066650 Succeeded.
2016-10-26 00:50:28.749832+00:00 e1cb00a0-9ccc-4558-b7d6-2bb41b10be56
2016-10-26 00:50:29.948932 Trying to find  8245581d-2b0c-4888-90e4-710f683837e9
Failed and retry...
2016-10-26 00:50:30.004495 Succeeded.
2016-10-26 00:50:29.866750+00:00 8245581d-2b0c-4888-90e4-710f683837e9
2016-10-26 00:50:31.060321 Trying to find  b1058f07-3c58-4ad1-8ea1-c4191be2bcbe
2016-10-26 00:50:31.177282 Succeeded.
2016-10-26 00:50:30.950333+00:00 b1058f07-3c58-4ad1-8ea1-c4191be2bcbe

タイミングによって、Queryに失敗していることがわかります。これは、Global Queryでは、作成直後のEntityが発見されない可能性があることを示しています。(この例では、1秒以内には発見されています。)

(ちなみに、Ancestor Queryだとなぜこのような事が起こらないかというと、Datastoreの実装として、Ancestor Queryが発行された場合は、該当のEntity Groupに対して内部的にデータ同期状態のチェックが入るようになっているからです。Global Queryの場合、検索対象がすべてのEntityになるため、データ同期状態のチェックはコストが高すぎて実行できません。そのため、Eventual Consistencyな検索となります。)

ID指定によるEntityの取得

次のスクリプトは、プロパティではなく、明示的に ID を指定してEntityを取得します。

reader2.py

#!/usr/bin/python

from google.cloud import datastore
import time, datetime, os
import subprocess

project_id = os.environ.get('PROJECT_ID')
ds = datastore.Client(project_id)

f = subprocess.Popen(['tail','-F','/tmp/tmp0'],
                     stdout=subprocess.PIPE,stderr=subprocess.PIPE)

while True:
    myid = f.stdout.readline().strip()
    key = ds.key('Kind01', myid)
    ts = datetime.datetime.now()
    print ts, 'Trying to find ', myid
    while True:
        ent = ds.get(key)
        if not ent:
            print 'Failed and retry...'
            continue
        ts = datetime.datetime.now()
        print ts, 'Succeeded.'
        print ent['timestamp'], myid
        break

こちらを実行すると、次のようになります。

$ ./reader2.py
2016-10-26 00:59:28.266182 Trying to find  2d4ad50f-488c-466d-8273-3533c95de6ed
2016-10-26 00:59:28.378003 Succeeded.
2016-10-26 00:59:28.065682+00:00 2d4ad50f-488c-466d-8273-3533c95de6ed
2016-10-26 00:59:29.422306 Trying to find  1e1b6e94-0697-4a8f-9b2c-96696085e429
2016-10-26 00:59:29.560694 Succeeded.
2016-10-26 00:59:29.267432+00:00 1e1b6e94-0697-4a8f-9b2c-96696085e429
2016-10-26 00:59:30.570010 Trying to find  a159836f-cdf5-4148-a305-9b2f01e8f7d0
2016-10-26 00:59:30.679568 Succeeded.
2016-10-26 00:59:30.423487+00:00 a159836f-cdf5-4148-a305-9b2f01e8f7d0
2016-10-26 00:59:31.701340 Trying to find  add815ed-7cb9-4e61-8377-38eca8dac14d
2016-10-26 00:59:31.817533 Succeeded.
2016-10-26 00:59:31.571264+00:00 add815ed-7cb9-4e61-8377-38eca8dac14d
2016-10-26 00:59:32.844253 Trying to find  9ce71c69-d5e0-48c1-a065-ec41aad386d3
2016-10-26 00:59:32.919830 Succeeded.
2016-10-26 00:59:32.702581+00:00 9ce71c69-d5e0-48c1-a065-ec41aad386d3
2016-10-26 00:59:33.908359 Trying to find  5c250b18-4b03-45c7-bbc4-c8a703737148
2016-10-26 00:59:33.926057 Succeeded.
2016-10-26 00:59:33.845533+00:00 5c250b18-4b03-45c7-bbc4-c8a703737148

この場合は、Strong Consistencyとなり、かならず、Entityの最新の内容が取得されます。

Ancestor Queryの場合

Entityグループを構成して、親Entityを指定したAncestor Queryを実施する場合は、作成直後のEntityも必ず取得されるはずですが、念のために確認しておきましょう。

ここでは、次のようなEntityグループを構成します。

Kind00: 'grandpa'
    |
    |
Kind00: 'father'
    |
    ---------------------
    |                |
Kind01: uuid     Kind01: uuid ・・・

Entityを作成するスクリプトは、こんな感じ。(Kind00: 'grandpa' と Kind00: 'father' に対応するEntityは実際には作成していませんが、これは問題ありません。「Parent Key」は実際のところ、Entityグループのローカルインデックスの検索Keyとして使われるだけですので・・・)

writer3.py

#!/usr/bin/python

from google.cloud import datastore
import time, datetime, os, uuid

project_id = os.environ.get('PROJECT_ID')
ds = datastore.Client(project_id)

os.remove('/tmp/tmp0')

for i in range(1000):
    myid = str(uuid.uuid4())
    parent_key = ds.key('Kind00', 'grandpa', 'Kind00', 'father')
    key = ds.key('Kind01', myid, parent=parent_key)
    ent = datastore.Entity(key)
    ts = datetime.datetime.now()
    ent.update({'timestamp': ts, 'myid': myid})
    ds.put(ent)
    with open('/tmp/tmp0', 'a') as file:
        file.write('%s\n' % myid)
    print ts, myid 
    time.sleep(1)

Entityを検索する方はこんな感じ。ここでは、直近の親 ds.key('Kind00', 'grandpa', 'Kind00', 'father') を検索の起点としていますが、もちろんその上の ds.key('Kind00', 'grandpa') を起点にしても構いません。

#!/usr/bin/python

from google.cloud import datastore
import time, datetime, os
import subprocess

project_id = os.environ.get('PROJECT_ID')
ds = datastore.Client(project_id)

f = subprocess.Popen(['tail','-F','/tmp/tmp0'],
                     stdout=subprocess.PIPE,stderr=subprocess.PIPE)

while True:
    myid = f.stdout.readline().strip()
    ancestor_key = ds.key('Kind00', 'grandpa', 'Kind00', 'father')
    query = ds.query(kind='Kind01', ancestor=ancestor_key)
    query.add_filter('myid', '=', myid)
    ts = datetime.datetime.now()
    print ts, 'Trying to find ', myid
    while True:
        iter = query.fetch()
        ent, _, _ = iter.next_page()
        if len(ent) == 0:
          print 'Faild and retry...'
          continue
        ts = datetime.datetime.now()
        print ts, 'Succeeded.'
        print ent[0]['timestamp'], myid
        break

実行結果は次の通りで、予想通り、取りこぼしはありません。

$ ./reader3.py 
2016-11-02 07:21:02.236359 Trying to find  6b77f8da-0775-4f77-980e-14c5c59a451f
2016-11-02 07:21:02.392515 Succeeded.
2016-11-02 07:21:01.482085+00:00 6b77f8da-0775-4f77-980e-14c5c59a451f
2016-11-02 07:21:02.634073 Trying to find  3681841d-72e1-43ac-9d91-0dfcbf137713
2016-11-02 07:21:02.662415 Succeeded.
2016-11-02 07:21:02.582529+00:00 3681841d-72e1-43ac-9d91-0dfcbf137713
2016-11-02 07:21:03.682064 Trying to find  164ff921-bf65-4835-9a3b-6c0a5e7948d4
2016-11-02 07:21:03.719449 Succeeded.
2016-11-02 07:21:03.635356+00:00 164ff921-bf65-4835-9a3b-6c0a5e7948d4
2016-11-02 07:21:04.777847 Trying to find  eeb6702b-2f4b-4254-8957-1db2ae52a23e
2016-11-02 07:21:04.805840 Succeeded.
2016-11-02 07:21:04.683344+00:00 eeb6702b-2f4b-4254-8957-1db2ae52a23e
2016-11-02 07:21:05.852476 Trying to find  a514fe27-37df-4639-b269-5192b926624b
2016-11-02 07:21:05.946797 Succeeded.