kikumotoのメモ帳

インフラ・ミドル周りを中心に、興味をもったことを適当な感じで。twitter : @takakiku

Consul 0.6でユーザのいるshardを探す(Prepared Queryを使う)

この記事は HashiCorp Advent Calendar 2015 - Qiita の第21日目です。(大人な事情により Hamee Advent Calendar 2015 - Qiita の第20日目でもあります。なので微妙な時間に公開)

概略

ユーザIDをベースにDBを分割(sharding)しているのですが、プログラムからユーザのいるshardを探すのにConsulを使えないかなぁと思って試している話。

Consul0.6のPrepared Queryを使って解決してみました。

目次

背景

ユーザIDをベースにDBを分割(sharding)していると、プログラムからユーザのいるshardを探す必要があります。

これをどこかのメタDBサーバにするってのも良いんだけど、Consul使ってDNSで引っ張ってこれると使う側にとって扱いやすいかなと思って調べてます。

前提

  • DB shardが複数あり、各shardは Master / Slaves な構成になっているとします。
    • db01-a(master), db01-b(db01-aのslave),db02-a(master), db02-b(db02-aのslave)があるとします。
  • あるユーザはある特定のshardにそのデータが存在します。
    • user1のデータはdb01/02に存在するとします。
  • ユーザのshardを移動させることがある。
  • Slaveが昇格することがある。
    • ただし、この昇格時にはある程度の期間Masterがいない状態を許容する。

・Node設定

各DBではconsulのServiceが下記JSONの定義のように登録されています。

{
  "Service": {
     "name": "db01",
     "tags": [ "master" ]
  }
}

なので以下のような感じでDNSでひける状態です。 ちなみに試している環境にはdnsmasqを入れています。

# dig master.db01.service.consul.

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.4 <<>> master.db01.service.consul.
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53317
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;master.db01.service.consul.    IN  A

;; ANSWER SECTION:
master.db01.service.consul. 30 IN   A   192.168.0.11

;; Query time: 1 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Dec 18 14:11:33 2015
;; MSG SIZE  rcvd: 94

方法1(今回採用予定)

Consul0.6で導入されたPrepared Query(以下PQ)を使う方法です。

PQに下記のような情報を登録します。(user1.jsonとします)

{
  "Name": "master-db-user1",
  "Service": {
    "Service": "db01",
    "Tags": ["master"]
  },
  "DNS": {
    "TTL": "30s"
  }
}

これを以下のように登録します。

$ curl -X POST http://localhost:8500/v1/query --data-binary @user1.json
{"ID":"66804426-8e5f-427b-caa7-d896c9328910"}

IDが発行されるので、これは後で必要になります。もしくは /v1/query にGETでリクエストすれば一覧が取れるので、そこから必要ものを探せばOKです。

このようにすると、以下のようにDNSでひけて、ユーザが収容されているmaster DBのIPが取得できます。

$ dig master-db-user1.query.consul.

; <<>> DiG 9.3.6-P1-RedHat-9.3.6-25.P1.el5_11.2 <<>> master-db-user1.query.consul.
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14375
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;master-db-user1.query.consul.  IN  A

;; ANSWER SECTION:
master-db-user1.query.consul.   30 IN   A   192.168.0.11

;; Query time: 1 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Dec 18 14:44:21 2015
;; MSG SIZE  rcvd: 94

・slave昇格

db01-aが死んでdb01-bがmasterになるときは、db01-bで

{
  "Service": {
     "name": "db01",
     "tags": [ "master" ]
  }
}

のようにmasterのタグをもたせてconsul reloadすればよいです。

これで、同じようにDNSをひくと、昇格したノードのIPが取得できます。

$ dig master-db-user1.query.consul. +short
192.168.0.12

なお、db01-aがmasterのタグを持って起動しないようにする必要はあります。今回は、マシンブート時にconsulを自動起動させないようにして逃げてます。

・shard移動

user1のいるshardがdb02になれば、

{
  "Name": "master-db-user1",
  "Service": {
    "Service": "db02",
    "Tags": ["master"]
  },
  "DNS": {
    "TTL": "30s"
  }
}

というJSONを用意して、このPQのIDを指定してPUTします。

$ curl -X PUT http://localhost:8500/v1/query/66804426-8e5f-427b-caa7-d896c9328910 --data-binary @user1.json

これで、同じようにDNSをひくと、今度はdb02のmasterのIPが取得できます。

$ dig master-db-user1.query.consul. +short
192.168.0.21

方法2

Consul0.6より前だとPQがないので、外部サービス(ノード)を登録することで同じようにことできると思います。

この場合、外部サービスとして以下のようなJSONを登録します。

{
  "Node": "master-db-user1",
  "Address": "master.db01.service.consul"
}
$ curl -X PUT http://localhost:8500/v1/catalog/register --data-binary @user1-node.json

こうすると、以下のようにDNSがひけます。

$ dig master-db-user1.node.consul.

; <<>> DiG 9.3.6-P1-RedHat-9.3.6-25.P1.el5_11.2 <<>> master-db-user1.node.consul.
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29792
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;master-db-user1.node.consul.   IN  A

;; ANSWER SECTION:
master-db-user1.node.consul. 30 IN  CNAME   master.db01.service.consul.
master.db01.service.consul. 30 IN   A   192.168.0.11

;; Query time: 5 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Dec 18 15:20:49 2015
;; MSG SIZE  rcvd: 166

外部サービス(ノード)なので、CNAMEで解決されて、その後Aレコードを得るような形となります。

shardの移動は、Nodeの定義を更新することで対応することとなります。

Consul0.6より前ではこの方法かなぁとは思うのですが、Consul0.6だとPQによりCNAMEを介さずにひけるので方法1を採用予定です。

その他の方法

・タグだけで

{
  "Service": {
     "name": "db01",
     "tags": [ "master", "user1", "user2", ・・・ ]
  }
}

のようにタグだけで解決することも可能は可能かと。

ただ、shard移動するときとか、userの追加・削除時には必要なタグをして指定して更新するのでちょっと大変そう。

・サービスだけで

{
  "Service": {
     "name": "user1",
     "tags": [ "master"]
  }
}

みたいなサービスをユーザ数分だけ登録することでもできそう。

この場合、slave昇格でユーザ数分だけServiceを更新・登録とかしないといけなのでこれも大変そう。

まとめ

ユーザIDをベースにDBを分割(sharding)しているような場合に、それをDNSでひけるようにするには、Consul0.6のPrepared Queryを使うとなんとかなりそうでした。

ちなみに、PQを1万ほど突っ込んでみた範囲では、DNSをひくことにパフォーマンス的な影響は特に見られませんでした。

Consulでやる場合のもっと良い方法とか、そもそもConsul使うべきじゃないとかありましたら教えてください。