Docker + SolrCloud

概要

以下の書籍の内容をベースにDocker + Solr8でSolrCloud環境を構築した。

構成

  • ZooKeeper
  • Solr
    • シャード 1、レプリカ 2の構成で2つのSolrは同じデータを保持する前提
    • スキーマAPIで管理するmanaged-schemaではなく自前で管理するschema.xmlを使用
  • Nginx
    • 2つのSolrへリクエストを振り分けるLB
    • 片方が死んでいる場合は生きている方に流す

f:id:sota2502:20200907225046p:plain

コード

https://github.com/sota2502/japan_address

使用するデータ

郵便番号データダウンロード - 日本郵便 を使用した。 全都道府県で約12万件。 Shift-JIS → UTF-8 への変更とスキーマに合わせたヘッダーを追加している。

ken_all.csv

ZooKeeper

いきなりSolrCloudの構成を作る前にZooKeeperに慣れておくとよい。

docker-compose-zk.yml

version: '3.7'

x-zoo-service: &zoo-service
  image: zookeeper:3.5
  restart: always
  networks:
    - solr

services:
  zoo1:
    <<: *zoo-service
    container_name: zoo1
    hostname: zoo1
    ports:
      - 2181:2181
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 1
    volumes:
      - ./data/zoo1/data:/data
      - ./data/zoo1/datalog:/datalog
  zoo2:
    <<: *zoo-service
    container_name: zoo2
    hostname: zoo2
    ports:
      - 2182:2181
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 2
    volumes:
      - ./data/zoo2/data:/data
      - ./data/zoo2/datalog:/datalog
  zoo3:
    <<: *zoo-service
    container_name: zoo3
    hostname: zoo3
    ports:
      - 2183:2181
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 3
    volumes:
      - ./data/zoo3/data:/data
      - ./data/zoo3/datalog:/datalog

networks:
  solr:

各々のZooKeeperのデータを永続化するため、/data,/datalog をVolumeにマウントする。これによりrestartなどをしてもデータは失われない。

ZooKeeperのクラスタを起動。

$ docker-compose -f docker-compose-zk.yml up -d

ZooKeeperのクライアントを用意する。Macであればhomebrewでインストールするのが簡単。

$ brew install zookeeper

zoo1 に接続してznodeを作成。

$ zkCli -server localhost:2181 create /hoge fuga
Connecting to localhost:2181

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
Created /hoge

先ほど作ったznodeのデータを zoo2 から読み出す。

$ zkCli -server localhost:2182 get /hoge
Connecting to localhost:2182

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
fuga

SolrのCollection用Configsetの作成

_default のConfigsetをベースに郵便番号を管理するためのConfigsetを作成する。これはCollectionを作るときに使う。

以下のコマンドを実行して、テンポラリなコンテナを作って、その中から _default Configsetsを取り出す。

$ docker create --name mysolr solr:8.5.2
$ docker cp mysolr:/opt/solr/server/solr/configsets/_default ./configsets
$ docker rm mysolr

この中で solrconfig.xmlschema.xml を編集していく。

特別なポイントはUUIDの設定をしているところ。 Solrのドキュメントを更新するためにはUniqueKeyが必要になる。しかし、郵便番号データにはUniqueKeyに適したものが存在しない。(同一の郵便番号の市町村が複数存在するケースがある) この制約はCSVを編集してIDを採番することでも満たすことは可能。 しかし、UUIDを採番するようにしておくほうが手間は省ける。

solrconfig.xml

差分は以下の通り。

38a39
>   <schemaFactory class="ClassicIndexSchemaFactory" />
1111c1112
<   <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:true}"
---
>   <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:false}"
1117a1119,1124
>   <updateRequestProcessorChain name="uuid" default="true">
>     <processor class="solr.UUIDUpdateProcessorFactory">
>       <str name="fieldName">uuid</str>
>     </processor>
>     <processor class="solr.RunUpdateProcessorFactory" />
>   </updateRequestProcessorChain>

<schemaFactory class="ClassicIndexSchemaFactory" />managed-schema の代わりに schema.xml を使用する場合に記述する。

add-unknown-fields-to-the-schemaUpdateRequestProcessorChain のdefault値をfalseにしておく。これはschemalessモードを使わないための設定。

uuidUpdateRequestProcessorChain はUUIDを採番するための設定。

schema.xml

_default Configsetsには schema.xml は存在しない。 これは managed-schemaschema.xml にリネームして手動編集する。 差分は以下のようになる。これは郵便番号データに対応したfieldの定義の追加と、不要なfieldの定義の削除、UniqueKeyの変更をしている。

111a112,127
>     <field name="uuid" type="uuid" multiValued="false" indexed="true" required="true" stored="true"/>
>     <field name="jis" type="string" indexed="true" required="true" stored="true"/>
>     <field name="postcode" type="string" indexed="true" required="true" stored="true"/>
>     <field name="postcode5" type="string" indexed="true" required="true" stored="true"/>
>     <field name="prefecture_kana" type="text_ja" required="true"/>
>     <field name="city_kana" type="text_ja" required="true"/>
>     <field name="town_area_kana" type="text_ja" required="true"/>
>     <field name="prefecture" type="text_ja" indexed="true" required="true" stored="true"/>
>     <field name="city" type="text_ja" indexed="true" required="true" stored="true"/>
>     <field name="town_area" type="text_ja" indexed="true" required="true" stored="true"/>
>     <field name="is_one_town_by_multi_postcode" type="boolean" required="true"/>
>     <field name="is_need_small_area_address" type="boolean" required="true"/>
>     <field name="is_chome" type="boolean" required="true"/>
>     <field name="is_multi_town_by_one_postcode" type="boolean" required="true"/>
>     <field name="updated" type="boolean" required="true"/>
>     <field name="update_reason" type="boolean" required="true"/>
113d128
<     <field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
117,122d131
<     <!-- If you don't use child/nested documents, then you should remove the next two fields:  -->
<     <!-- for nested documents (minimal; points to root document) -->
<     <field name="_root_" type="string" indexed="true" stored="false" docValues="false" />
<     <!-- for nested documents (relationship tracking) -->
<     <field name="_nest_path_" type="_nest_path_" /><fieldType name="_nest_path_" class="solr.NestPathField" />
< 
169c178
<     <uniqueKey>id</uniqueKey>
---
>     <uniqueKey>uuid</uniqueKey>
1012a1022,1023
>     <fieldType name="uuid" class="solr.UUIDField" indexed="true" />
> 
1023d1033
< 

SolrCloud環境の構築

docker-composeZooKeeperSolrNginx のSolrCloud環境を構築する。

docker-compose

docker-compose.yml やその他に必要なファイルは以下のようになる。

docker-compose.yml

version: '3.7'

x-zoo-service: &zoo-service
  image: zookeeper:3.5
  restart: always
  networks:
    - solr

x-solr-service: &solr-service
  image: solr:8.5.2
  environment:
    ZK_HOST: zoo1:2181,zoo2:2181,zoo3:2181
    SOLR_JAVA_MEM: -Xms1g -Xmx1g -Xmn512m
  networks:
    - solr
  depends_on:
    - zoo1
    - zoo2
    - zoo3

services:
  zoo1:
    <<: *zoo-service
    container_name: zoo1
    hostname: zoo1
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 1
    volumes:
      - ./data/zoo1/data:/data
      - ./data/zoo1/datalog:/datalog
  zoo2:
    <<: *zoo-service
    container_name: zoo2
    hostname: zoo2
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 2
    volumes:
      - ./data/zoo2/data:/data
      - ./data/zoo2/datalog:/datalog
  zoo3:
    <<: *zoo-service
    container_name: zoo3
    hostname: zoo3
    env_file: ./zoo.env
    environment:
      ZOO_MY_ID: 3
    volumes:
      - ./data/zoo3/data:/data
      - ./data/zoo3/datalog:/datalog
  solr1:
    <<: *solr-service
    container_name: solr1
    volumes:
      - ./data/solr1:/var/solr
  solr2:
    <<: *solr-service
    container_name: solr2
    volumes:
      - ./data/solr2:/var/solr
  nginx:
    image: nginx:1.19.2
    container_name: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - 8080:80
    networks:
      - solr
    depends_on:
      - solr1
      - solr2

networks:
  solr:

Solrのデータを永続化するために /var/solr をVolumeにマウントする。

zoo.env

ZOO_SERVERS=server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
ZOO_4LW_COMMANDS_WHITELIST=mntr,conf,ruok

nginx.conf

user nginx;
worker_processes  auto;

error_log /var/log/nginx/error.log warn;
pid       /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {
  upstream solr {
    server solr1:8983;
    server solr2:8983;
  }

  server {
    listen 80;
    client_max_body_size 20M;

    location / {
      proxy_pass http://solr;
    }
  }
}

コンテナを起動。

$ docker-compose up -d

起動が完了したら http://localhost:8080/solr/ にブラウザでアクセスする。SolrのWeb管理画面が表示される。 Cloud → Graph でSolrノードの繋がりが見える。

f:id:sota2502:20200908011613p:plain

ConfigsetsのUpload

[SolrのCollection用Configsetの作成] で作ったConfigsetsをアップロードする。 これは一度zipで一つにしたものをAPIで送る。

$ cd configsets/conf
$ zip -r japan_address.zip *
$ curl -X POST --header "Content-Type:application/octet-stream" \
  --data-binary @japan_address.zip \
  "http://localhost:8080/solr/admin/configs?action=UPLOAD&name=japan_address"

ZooKeeperコンテナに対して2181ポートをマッピングしておいて、 zkClils コマンドを実行するとConfigsetsに対するznodeができているのが確認できる。 (上記のdocker-compose.ymlではポートをマッピングしていないので、これを行う場合には ports の記述を追加する必要がある)

$ zkCli -server localhost:2181 ls /configsets

Collectionの作成

http://localhost:8080/solr/#/~collections:Solr管理コンソール からCollectionを作成する。 今回はシャード1、レプリカ2なので以下のように入力する。

項目
name japan_address
configset japan_address
numShards 1
replicationFactor 2

Add Collection をクリックして画面をリロードするとCollectionが作成されたのが確認できる。

CSVデータのインポート

以下のコマンドを実行してCSVデータをインポートする。これは結構時間がかかる。

$ curl -H 'Content-type: text/csv; charset=utf-8' \
  --data-binary @ken_all.csv \
  'http://localhost:8080/solr/japan_address/update?commit=true' 

検索の実行

Curlコマンドか Solr管理画面 でクエリを実行すると結果が返ってくるのが見える。

$ curl 'http://localhost:8080/solr/japan_address/select?q=*:*'

フェールオーバーの確認

現在は2つのSolrにNginxがリクエストを振り分けている状態。 ここから片方のSolrが落ちた状態でもリクエストを処理していることを確認する。

以下のコマンドで solr2 のコンテナを停止させる。

$ docker-compose stop solr2

Solr管理画面のCloud → Tree で片方が停止しているのが把握できる。

Curlで何回かクエリを実行して、いずれもレスポンスが返ってきているのが確認できる。