MySQL
docker
docker-compose
rancher

rancherのバックエンドを外部コンテナでreplicaitonしてみた備忘録

前回シングルでたててみてHAしたくなったのでそうした的なdocker初心者の備忘録です。

・環境、前提など

Ubuntu16 が2台
overaynetworkをetcdで組んだ状態
一応mysqlもコンテナとして立ててmaster/slave構成にした
フロントは別途。

・overraynetworkをetcdで組む

ここに書きました。
(今のところオーバレイにした意味があったのかあんまり認識できていない)

・docker-compose.ymlを置く

以下の配置でdirectoryをつくりファイルを置いてく

master側
# tree . 
.
├── nginx-proxy
│   └── ssl
└── rancher-server
    ├── docker-compose.yml
    ├── init-sql-master
    │   └── init-master.sql
    ├── mycnf
    │   └── master
    │       └── custom-master.cnf
    ├── mysql

slave側
.
└── rancher-server
    ├── docker-compose.yml
    ├── init-sql-slave
    │   └── init-slave.sql
    ├── mycnf
    │   └── slave
    │       └── custom-slave.cnf
    └── mysql
directoryを作る
mkdir -p nginx-proxy/ssl
mkdir -p rancher-server/{mycnf/{slave,master},mysql,init-sql-master,init-sql-slave}
master用docker-compose.yml
version: '2'
services:
  rancher-server:
    image: rancher/server:preview
    container_name: "rancher"
    depends_on:
      - mysql-master
    restart: unless-stopped
    command: --db-host ${DB_MASTER_HOSTNAME} --db-port 3306 --db-name ${DB_SCHEMA} --db-user root --db-pass ${MYSQL_ROOT_PASSWORD} --advertise-address ${MY_HOSTNAME}
    environment:
      VIRTUAL_PORT: 8080
      VIRTUAL_HOST: ${VIRTUAL_HOST}
      LETSENCRYPT_HOST: ${VIRTUAL_HOST}
      LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
    ports:
      - 8080:8080
      - 9345:9345
  mysql-master:
    image: mysql:latest
    container_name: "mysql-master"
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_SCHEMA}
    ports:
      - 3306:3306
    volumes:
      - '/home/docker/rancher-server/mysql:/var/lib/mysql:rw'
      - '/home/docker/rancher-server/mycnf/master:/etc/mysql/conf.d'
      - '/home/docker/rancher-server/init-sql-master:/docker-entrypoint-initdb.d'
  nginx-proxy:
    image: jwilder/nginx-proxy:latest
    container_name: "nginx-proxy"
    restart: always
    depends_on:
      - rancher-server
    ports:
     - "80:80"
     - "443:443"
    volumes:
      - '/home/docker/nginx-proxy/ssl:/etc/nginx/certs:ro'
      - '/etc/nginx/vhost.d'
      - '/usr/share/nginx/html'
      - '/var/run/docker.sock:/tmp/docker.sock:ro'
  letsencrypt-nginx-proxy-companion:
    image: jrcs/letsencrypt-nginx-proxy-companion:latest
    container_name: "letsencrypt"
    restart: always
    depends_on:
      - rancher-server
      - nginx-proxy
    volumes_from:
      - nginx-proxy
    volumes:
      - '/home/docker/nginx-proxy/ssl:/etc/nginx/certs:rw'
      - '/var/run/docker.sock:/var/run/docker.sock:ro'
networks:
  default:
    external:
      name: common_link
slave用docker-compose.yml
version: '2'
services:
  rancher-server:
    image: rancher/server:preview
    container_name: "rancher"
    depends_on:
      - mysql-slave
    restart: unless-stopped
    command: --db-host ${DB_MASTER_HOSTNAME} --db-port 3306 --db-name ${DB_SCHEMA} --db-user root --db-pass ${MYSQL_ROOT_PASSWORD} --advertise-address ${MY_HOSTNAME}
    environment:
      VIRTUAL_PORT: 8080
      VIRTUAL_HOST: ${VIRTUAL_HOST}
        ports:
      - 8080:8080
      - 9345:9345
  mysql-slave:
    image: mysql:latest
    container_name: "mysql-slave"
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_SCHEMA}
    ports:
      - 3306:3306
    volumes:
      - '/home/docker/rancher-server/mysql:/var/lib/mysql:rw'
      - '/home/docker/rancher-server/mycnf/slave:/etc/mysql/conf.d'
      - '/home/docker/rancher-server/init-sql-slave:/docker-entrypoint-initdb.d'
networks:
  default:
    external:
      name: common_link
.envファイル
DB_MASTER_HOSTNAME= #masterのホスト名を指定
MY_HOSTNAME= #master,slaveそれぞれの値を指定
VIRTUAL_HOST= #rancherのurlにつかうnginxにproxyしてほしいかつ証明書がほしいFQDN
LETSENCRYPT_EMAIL= #letsencrypt用に指定するemail
MYSQL_ROOT_PASSWORD= #mysqlのrootパスワード
DB_SCHEMA=cattle #rancher用のDBスキーマ名

・mysql用の設定ファイルと初期投入用SQLを置く

公式のコンテナ親切で解説を見た感じビルドしなくていいのかなと思いました。password部分は置換でどうぞ。

init-sql-master/init-master.sql
## create database for rancher
CREATE DATABASE IF NOT EXISTS cattle COLLATE='utf8_general_ci' CHARACTER SET='utf8';
## create user for rancher
GRANT ALL ON cattle.* TO 'cattle'@'%' IDENTIFIED BY 'password';
## create user for replication
CREATE USER IF NOT EXISTS 'repl'@'%';
SET PASSWORD FOR 'repl'@'%' = PASSWORD('password');
GRANT REPLICATION SLAVE,REPLICATION CLIENT ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
init-sql-slave/init-slave.sql
## create user for replication
CREATE USER IF NOT EXISTS 'repl'@'%';
SET PASSWORD FOR 'repl'@'%' = PASSWORD('password');
GRANT REPLICATION SLAVE,REPLICATION CLIENT ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
## change master for cattle
CHANGE MASTER TO MASTER_HOST='mymaster',MASTER_USER='repl',MASTER_PASSWORD='password',MASTER_AUTO_POSITION=1,MASTER_PORT=3306;
START SLAVE;

CREATE USERにIF NOT EXISTSつけてるのはbinlog_formatがROWでユーザがreplicationされて重複エラー起こして空コミット運用の手間がアレだからです。

my.cnf、master用
[mysqld]
wait_timeout = 600
## Memory Allocation per Connection
sort_buffer_size = 1M
join_buffer_size = 1M
max_allowed_packet = 16M
max_heap_table_size = 128M
tmp_table_size = 128M
table_definition_cache = 500
## for InnoDB param.
innodb_buffer_pool_size = 536870912
innodb_flush_method = O_DIRECT
innodb_io_capacity=400
innodb_io_capacity_max=3000
innodb_read_io_threads=6
innodb_write_io_threads=6
innodb_print_all_deadlocks=ON
## replication (master/slave)
server-id=1001 #unique
log-bin=mysql-bin
log_slave_updates=1
gtid-mode = ON
enforce_gtid_consistency=ON
master-info-repository=TABLE
relay-log-info-repository=TABLE
relay_log_recovery=ON
report_host=rancher01 #unique
report-port=3306
binlog_group_commit_sync_delay=1000
binlog_group_commit_sync_no_delay_count=128
binlog_format=ROW
## slow-log
slow_query_log=1
slow_query_log_file=/var/run/mysqld/mysql.slow
long_query_time = 1
expire_logs_days = 3
[mysqld_safe]
log_error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
[mysqldump]
quick
max_allowed_packet = 16M
[mysql]
no-auto-rehash
my.cnf、slave用
[mysqld]
wait_timeout = 600
## Memory Allocation per Connection
sort_buffer_size = 1M
join_buffer_size = 1M
max_allowed_packet = 16M
max_heap_table_size = 128M
tmp_table_size = 128M
table_definition_cache = 500
## for InnoDB param.
innodb_buffer_pool_size = 536870912
innodb_flush_method = O_DIRECT
innodb_io_capacity=400
innodb_io_capacity_max=3000
innodb_read_io_threads=6
innodb_write_io_threads=6
innodb_print_all_deadlocks=ON
## replication (master/slave)
server-id=1002 #unique
log-bin=mysql-bin
log_slave_updates=1
gtid-mode = ON
enforce_gtid_consistency=ON
master-info-repository=TABLE
relay-log-info-repository=TABLE
relay_log_recovery=ON
report_host=rancher02 #unique
report_port=3306
binlog_group_commit_sync_delay=1000
binlog_group_commit_sync_no_delay_count=128
binlog_format=ROW
## slow-log
slow_query_log=1
slow_query_log_file=/var/run/mysqld/mysql.slow
long_query_time = 1
expire_logs_days = 3
[mysqld_safe]
log_error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
[mysqldump]
quick
max_allowed_packet = 16M
[mysql]
no-auto-rehash

※server-idとreport_hostが一意になる値。my.cnfはデフォルトで使うとリソースを使い切れずにひどい目に合うから無駄にじっと見て変えることが多く結構デフォルト値見比べてみたけどgtid有効ならこんなもんかなと思いました。

項目 認識してる概要など
wait_timeout 反応無しのタイムアウト時間。デフォたしか6時間くらいなので削減
sort_buffer_size よく足りなくなるセッションバッファ
join_buffer_size よく足りなくなるセッションバッファ
max_allowed_packet 送れる最大パケットサイズ。
max_heap_table_size 足らないとディスクI/Oで劇遅になるので増加
tmp_table_size 足らないとディスクI/Oで劇遅になるので増加
table_definition_cache 400 + (table_open_cache / 2)が適してるらしい
innodb_buffer_pool_size オンメモリで運用するためにデータサイズ以上OS搭載メモリ75%未満を設定するやつ
innodb_flush_method 重複処理を省いて高速化するためにO_DIRECT指定
innodb_io_capacity ディスクのIOPS性能を指定SSDやioDriveなら増やす
innodb_io_capacity_max 5.6は控えめがいいらしいが5.7は大丈夫
innodb_read_io_threads ディスク読むスレッド数
innodb_write_io_threads ディスクに書くスレッド数
innodb_print_all_deadlocks デッドロック情報をshow innodb statusなどに出す
server-id replicationするとき用のid、一意でないとrepできない
log-bin バイナリログ出す意味とそのサフィックスを指定
log_slave_updates スレーブにもマスタにもなれる指定
gtid-mode gtid(グローバルトランザクションID)を有効化する
enforce_gtid_consistency gtidのための一貫性のないSQLを禁じるやつ(WARN指定だと警告だけ出て実行はできるが非推奨)
master-info-repository マスタ情報をtableに書くクラッシュセーフreplication用
relay-log-info-repository スレーブ情報をtableに書くクラッシュセーフreplication用
relay_log_recovery replicationの一貫性を保証するやつクラッシュセーフ用
report_host masterに認識させる自分のホスト名やIPアドレスを指定
report_port masterからshow slave hosts;で見るため用
binlog_group_commit_sync_delay 一括でコミットするとき待つマイクロ秒
binlog_group_commit_sync_no_delay_count 一括でコミットするとき待つクエリ数
binlog_format バイナリログのフォーマット。ROWとMIXEDとSTATEMENTがありGTID有効だとROW推奨ぽいが遅延の噂も
slow_query_log=1 スロークエリログを出す
slow_query_log_file スロークエリログの出力path
long_query_time この秒数を超えるとスロークエリログに出る
expire_logs_days バイナリログを何日たったものから消すか指定(ディスクfullに注意)

ログとかは好きなとこに出せばいんじゃねと思いますがメモリの食い合いに注意というかOS搭載メモリをセッションバッファ×接続数+グローバルバッファ+OS利用メモリが超えてはならないしコンテナ相乗りを考慮すべきだしみたいなのがあると思います。あとはDisk性能に合わせた設定するのとTCP/IPの同時接続数限界的にDB入ってるコンテナに色々入れすぎた挙句フロントだけが増えたりする場合があるとアレかも。

https://yakst.com/ja/posts/3726

・docker-compose up -dする

# docker-compose up -d
# docker-compose ps
        Name                 Command                 State                  Ports         
-----------------------------------------------------------------------------------------
letsencrypt            /bin/bash              Up                                          
                       /app/entrypoint. ...                                               
mysql-master           docker-entrypoint.sh   Up                     0.0.0.0:3306->3306/t 
                       mysqld                                        cp                   
nginx-proxy            /app/docker-           Up                     0.0.0.0:443->443/tcp 
                       entrypoint.sh  ...                            , 0.0.0.0:80->80/tcp 
rancher                /usr/bin/entry --db-   Up                     3306/tcp, 0.0.0.0:80 
                       host i ...                                    80->8080/tcp, 0.0.0. 
                                                                     0:9345->9345/tcp     

なんか途中で止まっててエラーっぽい時は-dとるとデーモンモードじゃなく標準出力にエラーでてわかりやすいかも。ただし抜けると止まってしまいますが。upの部分をstopにすると停止、downだと削除、restartで再起動、など。

・ハマったとこと思ったこと

→初期投入用SQL間違うと色々はまりますので実際打って確かめたほうがいいかも。調査の過程でdocker attachとdocker execの違いを知りました。
→rancherはpreviewだと2.0が入るけど2.0はpreviewなだけあってアクセスコントロールの設定が一切できない仕様でした。その状態で放置するのもどうかと思って1.6に戻しました。あと環境をk8sにするとカタログが少ない気がしました。
→跡形もなく消してやりなおす方法がちょっとよくわからなかったのですがk8s用環境つくるとdocker rm -f $(docker ps -aq)するだけだとOS再起動しないとiptablesの設定が残ってキモい感じでした。
→kubernetesアンチパターン的な勉強会の情報で、コンテナでRDBMS扱うの自体がアンチっぽいような話を人づてに聞いた気がするし分散クラスタに向いてる作りでないのでKVS的な実装に寄せてCloud Spanner使うみたいなのがお金がかかるけどバックエンドRDBMSをスケーラブルに寄せられる方法なのかなあ、グーグルレベルに大規模で困ってて予算が潤沢じゃなかったらバックエンドRDBMSコンテナ化してスケーラブルに自動制御というのは難しそう。もしくはさいきんでたAuroraServerlessとかがいいんですかね?
→mysqlfailoverはadminサーバ的なのがもう一台増えたら考えることにしようかと。bkup実装も別途。
→ディスク容量とバイナリログの管理に気を付けないと、しばらくたつとディスクfull→相乗りのetcd停止でオーバレイネットワーク消える→関連サービスもろとも全部止まる、というコンボをくらうので注意が要るなあ、と。

・参考

mysqlコンテナ周り
ライブラリ/ mysql - Docker Hub
Dockerの公式MySQLイメージの使い方を徹底的に解説するよ · DQNEO起業日記

dockerまわり
docker-compose.ymlが環境別に複数ある場合はCOMPOSE_FILEを定義しておくと幸せになれる
docker-compose.ymlの中で環境変数を展開する - Qiita
Environment variables in Compose | Docker Documentation

rancherまわり
Installing Rancher Server