Redash の複数台構成化その2(Redis を別インスタンスにする)
以下の記事の続きです。
前置き
前の記事で Redash と PostgreSQL を別インスタンスで動作させる環境ができていることを前提とします。
Redis を別インスタンスにする
PostgreSQL に比べ、Redis を別インスタンスにすることは性能に大きく影響を与えるものではないと思いますが、Redash に全部入り状態になっているミドルウェアから別インスタンスに切り出しやすいこともあり、この記事では Redis を別インスタンスにする方法を紹介します。
Redis のインスタンスを Redash インスタンスから参照できるネットワーク内に配置し、Redash の設定を変えていきます。
Redis インスタンスの設定は割愛しますが、Redash インスタンスからアクセスできるようになっていることを前提とします。
- プライベート IP 192.168.33.101
Redis のバージョンは デフォルトの Ubuntu 16.04 でインストールできる 3.0.6
としています。
設定を書き換える
/opt/redash/.env
で定義されている REDASH_REDIS_URL
を、以下のように書き換えます。
export REDASH_REDIS_URL=redis://192.168.33.101:6379/0
これで Redis が準備できました。
Redash を再起動して、Redash にアクセスし、何かクエリを実行してみましょう。
クエリが実行できたら、Redash インスタンス上の Redis は停止しても問題ありません。
既存環境を使用する場合の注意点
既存の Redash の Redis を別インスタンスにする場合は、実行中またはキューに入っているクエリが実行されない、正常終了しないなどの問題が起きる可能性が考えられます。
作業前にキューに何も入っていない、または再実行などで問題を回避できることを確認したうえで対応するのがよいでしょう。
まとめ
簡単でしたが、Redash で使用する Redis を別インスタンスにする手順を紹介しました。
次回は Redash の worker プロセスを別インスタンスで実行する方法を紹介する予定です。
Redash の複数台構成化その1(PostgreSQL を別インスタンスにする)
Redash はある程度スペックに余裕を持たせておけば、気を使わなくても1インスタンスで十分運用できるのが良いところではありますが、大量のデータを扱ったり、大量のクエリ実行を受け付けたりするような場合は、複数台での運用を考えたくなる時もあるでしょう。
この記事では、Redash の複数台構成について考えてみます。
前置き
この記事で紹介する方法は私個人の意見や考えに基づいて書いています。
業務上実績があったり、Redash が公式にこのような構成を推奨しているというものではないので、その点を踏まえてお読みください。
また、Redash を構成するミドルウェアなどについての説明は割愛します。
Redash の構成については、先日 Redash Meetup #2 で発表したスライドにも一部記載がありますので、必要に応じで参照してください。
環境
この記事では Vagrant で構築した VM で動作検証しています。
- Ubuntu 16.04
- Redash 4.0.1
- プライベート IP 192.168.33.10
また、この VM は新規に Redash をインストールしているため、既存環境からの移行については考慮していません(既存環境からの移行の場合に影響がありそうなところは適宜コメントをいれています)
PostgreSQL を別インスタンスにする
Redash を1台で運用する場合、クエリ結果を読み書きするため PostgreSQL の負荷が高くなりがちなので、PostgreSQL を別のインスタンスにすることで負荷分散が期待できるでしょう。
PostgreSQL のインスタンスを Redash インスタンスから参照できるネットワーク内に配置し、Redash の設定を変えていきます。
PostgreSQL インスタンスの設定は割愛しますが、Redash インスタンスからアクセスできるようになっていることを前提とします。
- プライベート IP 192.168.33.100
- ユーザ名:
redash
- パスワード:
redash
- データベース名:
redash
PostgreSQL のバージョンは デフォルトの Ubuntu 16.04 でインストールできる 9.5.13
としています。
設定を書き換える
/opt/redash/.env
で定義されている REDASH_DATABASE_URL
を、以下のように書き換えます。
export REDASH_DATABASE_URL="postgresql://redash:redash@192.168.33.100/redash"
テーブルを生成する
以下のコマンドで、 PostgreSQL インスタンス上にテーブルを作成します。
ubuntu@ubuntu-xenial:~/$ cd /opt/redash/current ubuntu@ubuntu-xenial:/opt/redash/current$ ./bin/run ./manage.py database create_tables
これで Redash に必要なデータベースが準備できました。
Redash を再起動して、Redash にブラウザからアクセスすると、セットアップ画面が表示されるはずです。
セットアップが完了し、Redash にログインできるようになったら、Redash インスタンス上の PostgreSQL は停止しても問題ありません。
既存環境を使用する場合の注意点
既存の Redash の PostgreSQL を別インスタンスにする場合は、上記の手順で作業してしまうと、クエリなどをイチから作り直しになってしまいます。
この記事では紹介しませんが、既存環境を使用する場合はテーブル生成の代わりに、既存環境からのダンプ、PostgreSQL インスタンスでのリストアが必要になるため注意してください。
まとめ
Redash で使用する PostgreSQL を別インスタンスにする手順を紹介しました。
次回は Redis を別インスタンスにする手順を紹介します。
Redash のクエリ定期実行はどのように実現されているのか
内容は本記事を書いた2018/06/03時点の理解なので、間違っていたら直す。
対象のバージョン
Redash 4.0.1 c86423a で確認した。
ざっくり言うと
30秒ごとにクエリの最終取得日時と定期実行の設定を比較し、再実行が必要なものを scheuled_queries
キューで実行する。
クエリ定期実行の流れ
- 30秒に1度、redash.tasks.queries.refresh_queries が実行される
- celery beat を使って定期実行されている
- デフォルトでは定期実行の最小値が毎時1分になっているのは、
celery beat
が30秒に1度動くためかもしれない- 「30秒」というところは今のところベタ書きされている
- 各クエリの
retrieved_at
や、そのクエリのスケジュール実行設定をみて、再実行が必要かどうかを判断するFEATURE_DISABLE_REFRESH_QUERIES
が有効になっているとスケジュール実行はすべて無視される
- クエリパラメータを使用している場合は、
queries
テーブルに保存されているデフォルトのパラメータをクエリにバインドする。クエリパラメータを使用していない場合クエリ文字列を取得する - キューにタスクを追加する
- Redash では画面からのクエリ実行と定期実行のキューが別れていて、定期実行は
scheuled_queries
キューを使用する
- Redash では画面からのクエリ実行と定期実行のキューが別れていて、定期実行は
- 全クエリに対して上記の処理をする
- 再実行対象数と、再実行されたクエリIDのリスト、実行日時を Redis の
redash:status
にハッシュとして書き込む- Redash のステータスページでみられる項目の一部はこの情報を使っているはず(そこまでは追っていない)
まとめ
Redis を FLUSHALL
しても、定期実行が動き続ける理由が知れてよかった。
Celery を試してみる
Redash を介してお世話になることはあれど、単体で使ったことがなかったので改めて Celery を試してみる。
Homepage | Celery: Distributed Task Queue
Python のバージョンは 3.6.5。Celery のバージョンは 4.1.1 を使用した。
Celery とは
Celery: Distributed Task Queue
分散タスクキュー。とのこと。
Celery で使えるメッセージブローカ
Brokers — Celery 4.1.0 documentation
RabbitMQ, Redis, Amazon SQS あたりが使えるとのこと。今回は Redash の理解を深めることも目的のひとつなので、Redis を使うことにする。
結果を取得するための Result Backend
First Steps with Celery — Celery 4.1.0 documentation
実行されたタスクの結果を利用したい場合、結果の保存には Result Backend という仕組みをつかうらしい。
保存先は SQLAlchemy, Memcached, Redis などが使用できるようだが、今回は Result Backend も Redash にあわせて Redis を使う。
試してみる
worker 上で動作するタスクの実装
非同期で動くことを確認するため、与えられたメッセージ文字列を0-2秒待ったあとに、そのまま結果として返す簡単なタスクを用意した。
from celery import Celery from time import sleep from random import randint app = Celery('tasks', backend='redis://localhost', broker='redis://localhost') @app.task def hello(m): sleep(randint(0, 2)) return m
非同期タスクを呼び出すアプリケーションの実装
FizzBuzz ループの中で 3 Fizz
のような文字列を先程のタスクで実行する。
すべて非同期実行をしたあとに、処理結果を確認、表示する。
from collections import deque import tasks results = deque([]) for i in range(1, 42): if i % 15 == 0: results.append(tasks.hello.delay('{} FizzBuzz'.format(i))) elif i % 3 == 0: results.append(tasks.hello.delay('{} Fizz'.format(i))) elif i % 5 == 0: results.append(tasks.hello.delay('{} Buzz'.format(i))) else: results.append(tasks.hello.delay('{}'.format(i))) print('すべてのタスクがキューに入りました') while len(results) > 0: result = results.popleft() if result.ready(): print(result.get()) continue results.append(result)
実行してみる
まずは worker を起動する。
$ celery --app=tasks worker --loglevel=info -c 2
--app
で非同期実行されるタスクが書かれたスクリプト名を渡し、 --loglevel
は文字通りログレベル、 -c
で並列実行のための子プロセス数を指定する。
次に、アプリケーションを実行する。
これはただの Python スクリプトなので、 python
コマンドで実行するだけ。
$ python main.py すべてのタスクがキューに入りました 1 2 4 3 Fizz 5 Buzz ...省略... 39 Fizz 40 Buzz 41
実行すると、「すべてのタスクがキューに入りました」というメッセージのあとに、1から41までの数字と、条件にあった数値は「Fizz」などの文字列を付加した結果が表示される。
上の例では、 2
の次の結果が 4
になっているが、これはタスク側でランダムに待ちをいれているためなので問題なし。
一方、アプリケーション実行時の worker 側の出力はこんな感じ。
[2018-05-28 20:08:15,640: INFO/MainProcess] Received task: tasks.hello[cbf77878-c2b4-48ec-8214-491155479188] [2018-05-28 20:08:15,643: INFO/MainProcess] Received task: tasks.hello[f1808ca7-f663-4c9e-8343-be5581b464da] [2018-05-28 20:08:15,647: INFO/MainProcess] Received task: tasks.hello[74ade9ad-8136-4ed9-8ee1-029cb9a003a9] [2018-05-28 20:08:15,652: INFO/MainProcess] Received task: tasks.hello[02925313-3346-41a8-bab8-9b4506151fc4] [2018-05-28 20:08:15,657: INFO/MainProcess] Received task: tasks.hello[e0558aef-3206-4feb-a09e-97bc48e9496d] [2018-05-28 20:08:15,661: INFO/MainProcess] Received task: tasks.hello[3dae0719-daa8-4c42-b703-1a2bf7293bae] [2018-05-28 20:08:15,667: INFO/MainProcess] Received task: tasks.hello[94a9a252-2794-413f-8aac-fd781a893cab] [2018-05-28 20:08:15,674: INFO/MainProcess] Received task: tasks.hello[84d9a82a-571a-477b-8911-4010083cc0b6] ...省略... [2018-05-28 20:08:35,937: INFO/ForkPoolWorker-2] Task tasks.hello[7c86fa11-a1c4-4261-900a-bcd6f261d76d] succeeded in 0.0011464759991213214s: '36 Fizz' [2018-05-28 20:08:36,940: INFO/ForkPoolWorker-2] Task tasks.hello[8f6658a9-2fcc-47a7-98e8-cae9a8096546] succeeded in 1.0012103780027246s: '37' [2018-05-28 20:08:37,813: INFO/ForkPoolWorker-1] Task tasks.hello[af3513f5-c6af-4e64-b324-7c4b1f8634bc] succeeded in 2.0011870139969687s: '35 Buzz' [2018-05-28 20:08:37,945: INFO/ForkPoolWorker-2] Task tasks.hello[38ecd7d3-2897-43ba-8e0c-1f36f24d7acb] succeeded in 1.0030830889991194s: '38' [2018-05-28 20:08:38,836: INFO/ForkPoolWorker-1] Task tasks.hello[f58921ba-5322-419a-b3d9-5d6f6d27e2ba] succeeded in 1.0212826329989184s: '39 Fizz' [2018-05-28 20:08:38,947: INFO/ForkPoolWorker-2] Task tasks.hello[1fff8e54-ded8-4a85-86f5-9f68fd9f2aa5] succeeded in 1.0010614519997034s: '40 Buzz' [2018-05-28 20:08:40,861: INFO/ForkPoolWorker-1] Task tasks.hello[2ac6cfeb-e204-427b-9ea6-7ddbd6fcfc6f] succeeded in 2.0224582139999256s: '41'
キューにタスクが積まれて、その後徐々に処理されていく様子がわからなくもない。
タスクには一意の ID が振られていて、ぱっと見で UUID っぽく見える。
worker を2つにしてみる
コードは変えず、 Celery の worker プロセスをもう一つ起動してみる。
$ celery --app=tasks worker --loglevel=info -c 2
この状態でアプリケーションを実行すると。
$ python main.py すべてのタスクがキューに入りました 3 Fizz 1 2 4 5 Buzz ...省略... 31 36 Fizz 38 34 40 Buzz
worker が増えたため、先程の実行結果よりさらに処理順序にばらつきが出ている。
worker 側の出力は割愛するが、 どちらの worker でも実行ログが出ているので、おそらく均一に処理されているものと思われる。
まとめ
タスクキューってだいたいこんな感じだよね。という使い心地だった。
Redash のコードを追う際の基礎知識として、触ってみてよかったと思う。
今度は Redash のクエリワーカを複数にしても問題なく動作するのか?という検証をしてみるつもり。
余談: Celery のタスクはどのような形で メッセージブローカ(Redis)に保存されているのか?
気になったので redis-cli
で見てみる。
$ redis-cli 127.0.0.1:6379> keys * ...省略... 231) "celery-task-meta-6dae9f06-bfb5-497a-8926-2b03dfb8a1a3" 232) "celery" 233) "celery-task-meta-68ab5751-0fa7-45d6-9c97-9aee871b410b" ...省略...
celery
というキーがあったので型を確認したら list
型だった。
127.0.0.1:6379> type celery list
1件取り出してみる。
127.0.0.1:6379> LRANGE celery 0 0 1) "{\"body\": \"W1siNDEiXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"tasks.hello\", \"id\": \"b69ad207-308f-4c73-988a-8321a640dbdc\", \"eta\": null, \"expires\": null, \"group\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"b69ad207-308f-4c73-988a-8321a640dbdc\", \"parent_id\": null, \"argsrepr\": \"('41',)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen14561@ariarijp.local\"}, \"properties\": {\"correlation_id\": \"b69ad207-308f-4c73-988a-8321a640dbdc\", \"reply_to\": \"e3c01706-26cb-3330-98aa-b5551bd6e56b\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"celery\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"0c5d61f8-ae4a-492e-904f-cbd4b551c3ca\"}}"
JSON。ちょっと見にくいので整形するとこんな感じ。
{ "body": "W1siNDEiXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d", "content-encoding": "utf-8", "content-type": "application/json", "headers": { "lang": "py", "task": "tasks.hello", "id": "b69ad207-308f-4c73-988a-8321a640dbdc", "eta": null, "expires": null, "group": null, "retries": 0, "timelimit": [ null, null ], "root_id": "b69ad207-308f-4c73-988a-8321a640dbdc", "parent_id": null, "argsrepr": "('41',)", "kwargsrepr": "{}", "origin": "gen14561@ariarijp.local" }, "properties": { "correlation_id": "b69ad207-308f-4c73-988a-8321a640dbdc", "reply_to": "e3c01706-26cb-3330-98aa-b5551bd6e56b", "delivery_mode": 2, "delivery_info": { "exchange": "", "routing_key": "celery" }, "priority": 0, "body_encoding": "base64", "delivery_tag": "0c5d61f8-ae4a-492e-904f-cbd4b551c3ca" } }
body
の中身は Base64 エンコードしてるように見えるのでデコードしてみた。
[["41"], {}, {"callbacks": null, "errbacks": null, "chain": null, "chord": null}]
celery-task-meta- *
みたいなのも気になるので見てみる。
127.0.0.1:6379> type celery-task-meta-e6f5f414-1d8f-42c2-b614-75f9615ec61f string
文字列型。JSON かな。
127.0.0.1:6379> get celery-task-meta-e6f5f414-1d8f-42c2-b614-75f9615ec61f "{\"status\": \"SUCCESS\", \"result\": \"41\", \"traceback\": null, \"children\": [], \"task_id\": \"e6f5f414-1d8f-42c2-b614-75f9615ec61f\"}"
JSON だった。Result Backend を使用すると、きっと celery-task-meta-*
に結果を書き出すのだろう。
例外を起こしたらどうなるのか?
気になったので、例外が起きるように書き換え、その結果を見てみた。
127.0.0.1:6379> get celery-task-meta-d5024461-52b3-429b-ab71-57c8e76522b3 "{\"status\": \"FAILURE\", \"result\": {\"exc_type\": \"Exception\", \"exc_message\": \"\"}, \"traceback\": \"Traceback (most recent call last):\\n File \\\"/Users/ariarijp/.local/share/virtualenvs/celerytutorial-nzo1Q02k/lib/python3.6/site-packages/celery/app/trace.py\\\", line 375, in trace_task\\n R = retval = fun(*args, **kwargs)\\n File \\\"/Users/ariarijp/.local/share/virtualenvs/celerytutorial-nzo1Q02k/lib/python3.6/site-packages/celery/app/trace.py\\\", line 632, in __protected_call__\\n return self.run(*args, **kwargs)\\n File \\\"/Users/ariarijp/workspace/python/celerytutorial/tasks.py\\\", line 12, in hello\\n raise Exception()\\nException\\n\", \"children\": [], \"task_id\": \"d5024461-52b3-429b-ab71-57c8e76522b3\"}"
ちょっと長いので整形。
{ "status": "FAILURE", "result": { "exc_type": "Exception", "exc_message": "" }, "traceback": "Traceback (most recent call last):\\n File \\"/Users/ariarijp/.local/share/virtualenvs/celerytutorial-nzo1Q02k/lib/python3.6/site-packages/celery/app/trace.py\\", line 375, in trace_task\\n R = retval = fun(*args, **kwargs)\\n File \\"/Users/ariarijp/.local/share/virtualenvs/celerytutorial-nzo1Q02k/lib/python3.6/site-packages/celery/app/trace.py\\", line 632, in __protected_call__\\n return self.run(*args, **kwargs)\\n File \\"/Users/ariarijp/workspace/python/celerytutorial/tasks.py\\", line 12, in hello\\n raise Exception()\\nException\\n", "children": [], "task_id": "d5024461-52b3-429b-ab71-57c8e76522b3" }
ステータスが FAILURE
になり、結果に例外の種類やトレースバックが含まれるようになった。
この値を使って結果を受け取るときに例外を発生させている様子。
ここのコードを追っていったら、Python で Promise を実現するための vine というモジュールがあることを知った。Celery プロジェクトの一部っぽい。
Redash のリリースビルドの場所を探す
ここ見ればわかる。
簡単。 jq
はお好みでどうぞ。
$ curl https://version.redash.io/api/releases | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 3424 100 3424 0 0 4626 0 --:--:-- --:--:-- --:--:-- 4620 [ { "id": 28, "version": "4.0.1", "channel": "stable", "download_url": "https://s3.amazonaws.com/redash-releases/redash.4.0.1.b4038.tar.gz", "backward_compatible": false, "released_at": "2018-05-02T00:00:00.000Z", "description": "* Before doing an upgrade, please make sure you have a backup.\n* If you have any issues, please refer to the troubleshooting section in the upgrade guide:\n https://redash.io/help/open-source/admin-guide/how-to-upgrade\n* If the upgrade guide doesn't help, you can ask for help on the forum (https://discuss.redash.io). \n\nFull CHANGELOG for this release: https://github.com/getredash/redash/blob/master/CHANGELOG.md", "docker_image": "redash/redash:4.0.1.b4038" }, { "id": 27, "version": "4.0.0", "channel": "stable", "download_url": "https://s3.amazonaws.com/redash-releases/redash.4.0.0.b3948.tar.gz", "backward_compatible": false, "released_at": "2018-04-16T00:00:00.000Z", "description": "* Before doing an upgrade, please make sure you have a backup.\n* If you have any issues, please refer to the troubleshooting section in the upgrade guide:\n https://redash.io/help/open-source/admin-guide/how-to-upgrade\n* If the upgrade guide doesn't help, you can ask for help on the forum (https://discuss.redash.io). \n\nFull CHANGELOG for this release: https://github.com/getredash/redash/blob/master/CHANGELOG.md", "docker_image": "redash/redash:4.0.0.b3948" }, { "id": 23, "version": "3.0.0", "channel": "stable", "download_url": "https://s3.amazonaws.com/redash-releases/redash.3.0.0.b3134.tar.gz", "backward_compatible": true, "released_at": "2017-11-13T00:00:00.000Z", "description": "* Before doing an upgrade, please make sure you have a backup.\n* If you have any issues, please refer to the troubleshooting section in the upgrade guide:\n https://redash.io/help-onpremise/maintenance/how-to-upgrade-redash.html\n* If the upgrade guide doesn't help, you can ask for help on the forum (https://discuss.redash.io). \n\nFull CHANGELOG for this release: https://github.com/getredash/redash/blob/master/CHANGELOG.md", "docker_image": "redash/redash:3.0.0.b3134" }, { "id": 22, "version": "2.0.1", "channel": "stable", "download_url": "https://s3.amazonaws.com/redash-releases/redash.2.0.1.b3080.tar.gz", "backward_compatible": true, "released_at": "2017-10-22T00:00:00.000Z", "description": "* Before doing an upgrade, please make sure you have a backup.\n* If you have any issues, please refer to the troubleshooting section in the upgrade guide:\n https://redash.io/help-onpremise/maintenance/how-to-upgrade-redash.html\n* If the upgrade guide doesn't help, you can ask for help on the forum (https://discuss.redash.io). \n\nFull CHANGELOG for this release:\nhttps://github.com/getredash/redash/blob/master/CHANGELOG.md#v201---2017-10-22", "docker_image": null }, { "id": 21, "version": "2.0.0", "channel": "stable", "download_url": "https://s3.amazonaws.com/redash-releases/redash.2.0.0.b2990.tar.gz", "backward_compatible": true, "released_at": "2017-08-08T00:00:00.000Z", "description": "* Before doing an upgrade, please make sure you have a backup.\n* If you have any issues, please refer to the troubleshooting section in the upgrade guide:\n https://redash.io/help-onpremise/maintenance/how-to-upgrade-redash.html\n* If the upgrade guide doesn't help, you can ask for help on the forum (https://discuss.redash.io). \n\nFull CHANGELOG for this release: https://github.com/getredash/redash/blob/master/CHANGELOG.md#v200---2017-08-08", "docker_image": null } ]
これがどういう時に欲しくなるかというと、VM で動いてる Ubuntu に特定のバージョンをインストールする時にバージョンとビルド番号が欲しくなる。
2.0.1 から 4.0.1 へのアップグレードを考えているので、検証を進めていこうと思う。