- 投稿日:2022-01-31T22:04:12+09:00
[MySQL]よく見かけるSQLチューニング方法が本当に効果があるのか検証してみるよ(SELECT文編)
気がついたら年明けて1ヶ月経ってびっくり 今日は、MySQLでSQLチューニングについて学ぶよ お仕事でMySQLを使っているので、この書き方が良いらしい・・みたいなことは知っているのですが、 本当に有効なのかを検証していきたいと思います。 前書き 検証テーマ 1.SELECT文の書き方で速度はどれくらい変わるのか?←今回の記事 2.INDEXの貼り方で速度はどのくらい変わるのか←準備中 検証内容 下記に記載されているSQLチューニング方法のうち、気になったものを抜粋し、速度比較を実施。 参考ページ:https://style.potepan.com/articles/26070.html 参考ページ:https://qiita.com/ichi_zamurai/items/fdbe3872a505c22ee431 参考図書:達人に学ぶ SQL徹底指南書 (CodeZine BOOKS) この記事ではなぜ速くなるのかなどの理由には触れていないので、気になった方は上記より! 検証環境 ・Docker(20.10.7) ・MySQL(Server version: 8.0.27) 今回はSQLチューニングが目的なので、MySQLの環境設定は文字コード等の最低限設定しか行っていません。 速度の定義 SQLの実行にかかった時間=events_stages_history_long.TIMER_WAITの値(秒)として計測。 参考ページ:MySQL公式 検証用TBL 名前、年齢、血液型や誕生日といった個人情報が入ったTBLを、カラム数とデータ量に差をつけて準備。 1.users_13_s: カラム数多×データ量小(13×100行) 2.users_13_m: カラム数多×データ量中(13×5000行) 3.users_13_l: カラム数多×データ量多(13×1万行) 4.users_6_s: カラム数小×データ量小(6×100行) 5.users_6_m: カラム数小×データ量中(6×5000行) 6.users_6_l: カラム数小×データ量多(6×1万行) 【カラム数 13(users_13_*)テーブル構成】 【カラム数6(users_6_*)テーブル構成】 カラム数12と6で試していたつもりが、今記事を書いている時に13だったことに気がついた 実際のTBL名と違うけれど、記事上はusers_13~と書く 【各カラムの値】 id : 整数型 連番 主キー age : 整数型 0~69のランダム gender: femaleかmale の文字列型 大体半々 blood: A ,B,O,AB いずれかの文字列型 A40%,B20%,O30%,AB10%となるように準備 birth:YYYY/MM/DDのDATE型 married: 1か0 の整数型 大体半々 lastName: 文字列型 漢字 firstName: 文字列型 漢字 prefectureCode: 整数型 1~47のランダム tel:文字列型 mail:文字列型 createdAt: Datetime型 del: 1か0の整数型 *今回はprimaryKeyのみでINDEXは貼っていません。 はてさて前置き長くなったけれど、ここから検証結果を書いていくよー 検証と結果 *結果は2種類の表を用いて説明しています。 【スコア】 各条件毎の結果 【比較】 同じSQLに対する速度が速い方のカラムに色付け&速度差を算出。 (3つで比較の場合は最大値と最小値の引き算) 疑問1 : 大文字SELECT vs 小文字select だと、SELECTのほうが早いってほんと? テスト内容: 下記4種類のSQLを10回実行し、各平均値を算出。大文字、小文字での違いをテーブルサイズごとの比較を実施。 SQL 1.SELECT * FROM users_XXX WHERE 1 = 1 2.SELECT * FROM users_XXX WHERE gender = 'male' 3.SELECT * FROM users_12_s WHERE married = 1 4.SELECT * FROM users_XXX WHERE age > 40 --小文字の例 1. select * from users_XXX where 1 = 1 【スコア】 【比較】 あれ・・・?selectのほうが早い・・? よくよく記事を参照すると、「大文字で書くことでキャッシュを使う確率が上がる」との記載があり、 mysql8.0のようにクエリキャッシュが廃止されている場合は、効果が得られないのかなと推測。 そもそも、クエリキャッシュは場合によっては遅くなる可能性もあり、 5.7でも非推奨となっているくらいなので、わざわざこの速度のためにキャッシュをONにするのは正しくないのかな。 mysql公式 結論 : クエリキャッシュが廃止されたmysql8.0で試したところ、selectのほうが速いかも。 疑問2 : TBL名.カラム名 vs カラム名 だと、TBL名.カラム名のほうが早いってほんと? テスト内容:SELECT句をすべてのカラム、半分のカラム、1つのカラムの3パターンで実施。下記4種類のSQLを10回実行した平均値で、テーブルサイズ×カラム数ごとの比較を実施。 SQL(大文字バージョンのみ抜粋。小文字もWHERE以下同じ) 1.SELECT X-テスト毎-X FROM users_XXX WHERE 1 = 1 2.SELECT X-テスト毎-X FROM users_XXX WHERE gender = 'male' 3.SELECT X-テスト毎-X FROM users_XXX WHERE married = 1 4.SELECT X-テスト毎-X FROM users_XXX WHERE age > 40 【スコア】 【比較】 結論 : 全項目取得の場合は*よりも圧倒的にカラム名指定の方が早い!取得するカラム数やテーブルサイズが大きければ大きいほど、TBL名.カラム名のほうが有効かも。 疑問3 :データ型が正しいWHERE文 vs データ型が正しくないWHERE文だと、正しいほうが早いってほんと? テスト内容: 整数型のカラムに対し、WHERE句を整数型と文字列型の2パターン実施。下記3種類のSQLをそれぞれ10回実行した平均値で比較。 SQL 1.SELECT * FROM users_XXX WHERE married = 1 / '1' 2.SELECT * FROM users_XXX WHERE age = 73 / '73' 3.SELECT * FROM users_XXX WHERE id = 9 /'9' 【スコア】 【比較】 結論 :users_13_sだけ速度がなぜかかなり遅いが、それ以外はデータ型が正しいWHERE文の方が早いことが多い。 疑問4 :WHERE XXX IN(多い順) vs WHERE XXX IN (適当)だと、多い順のほうが早いってほんと? テスト内容: A40%,O30%,B20%,AB10%で設定されたblood項目に対し、下記4種類のSQLをそれぞれ10回実行した平均で速度を比較。 SQL 1.SELECT * FROM users_XXX WHERE blood IN ('A','AB') / ('AB','A') 2.SELECT * FROM users_XXX WHERE blood IN ('O','B') / ('B','O') 3.SELECT * FROM users_XXX WHERE blood IN ('O','B','AB') / ('AB','O','B') 3.SELECT * FROM users_XXX WHERE blood IN ('A','O','AB') / ('O','AB','A') 【スコア】 【比較】 結論 データ量とカラム数が小さいとあまり違いがないが、カラム数13×データ量大だと順序が正しい方が全体的に早くなった。 疑問5 :BETWEEN vs <> だとBETWEENの方が早いってほんと? SQL 1.SELECT * FROM users_XXX WHERE age BETWEEN 10 AND 35 / age >= 10 AND age <= 35 2.SELECT * FROM users_XXX WHERE id BETWEEN 340 AND 350 / id >= 340 AND id <= 350 3.SELECT * FROM users_XXX WHERE age BETWEEN 50 AND 100 / age >= 50 AND age <= 100 4.SELECT * FROM users_XXX WHERE id BETWEEN 50 AND 450 / id >= 50 AND id <= 450 【スコア】 【比較】 おお、これは綺麗な結果が出た・・ 結論5 :ほぼ全てのパターンで効果がありそう。 疑問6 :EXISTS vs IN vs INNER JOIN だとEXISTSが早いってほんと? テスト内容: 顧客のオーダー情報的なTBLを連結用に用意。下記4種類のSQLを10回実行した平均で、それぞれの記載方法の速度を比較。 連結用TBL:orders / orders.userId = users_XXX.id SQL --- EXISTS 1.SELECT * FROM users_XXX AS main WHERE EXISTS (SELECT 1 FROM orders WHERE orders.userId = main.id AND paymentType = 1) 2.SELECT * FROM users_XXX AS main WHERE EXISTS (SELECT 1 FROM orders WHERE orders.userId = main.id AND orderDate BETWEEN '2000-01-01' AND '2020-01-01') 3.SELECT * FROM users_XXX AS main WHERE EXISTS (SELECT 1 FROM orders WHERE orders.userId = main.id AND id > 40) 4.SELECT * FROM users_XXX AS main WHERE EXISTS (SELECT 1 FROM orders WHERE orders.userId = main.id AND paymentType IN ('0','1')) -- INの場合は下記のような形式 SELECT * FROM users_XXX AS main WHERE id IN (SELECT userId FROM orders WHERE paymentType = 1) -- INNER JOINの場合は下記のような形式 SELECT * FROM users_XXX AS main INNER JOIN (SELECT DISTINCT userId FROM orders WHERE paymentType = 1) AS tmp ON tmp.userId = main.id 【スコア】 【比較】 結論6 : 平均を見るとEXISTSが良さそう。INNER JOINの方が速度が出るパターンもあるので、使い方によるのかも。 疑問7 : ORDER BY カラム名 vs ORDER BY カラム番号 だと、カラム名指定のほうが早いってほんと? SQL 1.SELECT * FROM users_XXX WHERE age > 50 ORDER BY id / ORDER BY 1 2.SELECT * FROM users_XXX WHERE married = 0 ORDER BY age / ORDER BY 2 3.SELECT * FROM users_XXX WHERE blood = 'A' ORDER BY id,age / ORDER BY 1,2 4.SELECT * FROM users_XXX WHERE gender = 'female' ORDER BY id,age /ORDER BY 1,2 【スコア】 【比較】 こ、これは・・・。 列名への読み替えが発生しているから遅くなるはずなのに・・違いが出なかった・・。 ORDER BY で指定する数を増やしたらもっと変わるのかしら・・ 結論7: relaxed: :違いを捕捉できなかった まとめ 今回はよく聞くけれど実際どのくらい効果あるかわからないものと、ちょっと疑わしい・・と勝手に思っていたものを抜粋して検証しました。次回はINDEX編と題して、同じように試していきます。 今回の検証も、INDEX有無で変わるものもあるのかもしれないので、その辺りも絡めて見ていきたいと思います。 計測方法や、内容に誤りやアドバイスがあれば、ご連絡いただけると嬉しいです! 参考 https://style.potepan.com/articles/26070.html https://qiita.com/ichi_zamurai/items/fdbe3872a505c22ee431 【テストデータ】 こちらのツールで作成したデータに必要な加工を加えて準備しました↓ 参考:テストデータ生成
- 投稿日:2022-01-31T21:41:25+09:00
MySQL系レプリケーション構成3種とインストール方法
この記事はリンク情報システムの「TechConnect!2022年2月」のリレー記事です。 engineer.hanzomon のグループメンバによってリレーされます。 (リンク情報システムのFacebookはこちらから) TechConnect!2022年2月のインデックスはこちら アドカレ4回目にして、トリっぽいですが、通常運営でインフラの話です ここ最近、mysqlを色々検証したのでまとめました! 余談ですが限定共有記事を消すと、下書きまで消えるんですね。。泣きながらキャッシュを漁って復活した記事ですw 目次 1. Group Replication + Proxy SQL構成 2. MySQL Replication(GTID) + MHA + Proxy SQL構成 3. MySQL Cluster構成 1. Group Replication + Proxy SQL構成 MySQL Server へのプラグインの1つ。単一プライマリモードでも、マルチプライマリモードでも動作可能。グループメンバーシップ制でグループから自由に退出、参加ができる。メンバーが予期せずグループから離脱した場合は障害検出メカニズムが検知し、他メンバーに離脱を自動通知する。 ■ 要件 項目 要件/仕様 最小構成 3台 最大構成 9台 ストレージ InnoDB 要求帯域 中~高 稼働中の拡張 可能 自動復旧 可能 ☆ memo ネットワークが高~中水準で安定していれば、トランザクションが多いシステムでも運用が可能な印象。SQL Routerと組み合わせることで、マスターはinsertのみ、スレーブはselectのみに処理を分散させることが可能。 ネットワークの状態によってはメンバーが離脱しやすい傾向があったが、帯域が確保できる環境では安定稼働している。(パラメータ調整で何とかなるが、限界もある。) そのため、遠隔地レプリケーションには向いてないと思われる。 1ノード(マスター or スレーブ)が離脱しても、フェイルオーバーし、疎通が取れるようになった時点で自動的にメンバーを追加してくれるので、メンテでスレーブだけを再起動する運用も可能。また、全台停止後のGroup Replicationの起動もコマンド1つ打てばよいので、頻繁に停止する環境でもそれなりに組みやすいかも? ただ、ディスクフル等のどうしようもない障害が起きた時は手動リカバリになり、それなりに手順が複雑になる。 インストール方法 環境:CentOS7 Group Replication:3台 Proxy SQL:1台 全台共通 (1) 事前準備 selinux無効 # vi /etc/selinux/config > SELINUX=disabled fierewalld無効 # systemctl disable firewalld timezone変更 # timedatectl set-timezone Asia/Tokyo パッケージアップデート ♯ yum update 再起動 # reboot (2) hostnameでお互いに疎通できるか確認する 手順9.で利用する。 # hostname # ping [自分以外のhost名称] 解決できなければ/etc/hostsに設定を追加し、その名称を利用する。 # vi /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 172.31.40.172 DB1 172.31.46.221 DB2 172.31.34.91 DB3 Group Replication用の3台の構築 (3) mysqlをインストールする mysqlリポジトリの追加 # rpm -Uvh https://dev.mysql.com/get/mysql80-community-release-el7-4.noarch.rpm MySQL 8.0 Community Serverが有効になっているか確認 # yum repolist all | grep mysql mysqlをインストールする # yum install mysql-community-server mysql-shell log用ディレクトリを作成する # mkdir /var/log/mysql # touch /var/log/mysql/slow.log # chown mysql. -R /var/log/mysql 自動起動を有効化 # systemctl enable mysqld (4) my.cnfの設定をする server_idはDB毎に違う値を設定する。また、server_idに0は設定できない。 # vi /etc/my.cnf # For advice on how to change settings please see # http://dev.mysql.com/doc/refman/8.0/en/server-configuration-defaults.html [mysqld] # # Remove leading # and set to the amount of RAM for the most important data # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. # innodb_buffer_pool_size = 128M # # Remove the leading "# " to disable binary logging # Binary logging captures changes between backups and is enabled by # default. It's default setting is log_bin=binlog # disable_log_bin # # Remove leading # to set options mainly useful for reporting servers. # The server defaults are faster for transactions and fast SELECTs. # Adjust sizes as needed, experiment to find the optimal values. # join_buffer_size = 128M # sort_buffer_size = 2M # read_rnd_buffer_size = 2M # # Remove leading # to revert to previous value for default_authentication_plugin, # this will increase compatibility with older clients. For background, see: # https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_default_authentication_plugin # default-authentication-plugin=mysql_native_password datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock log-error=/var/log/mysql/mysqld.log pid-file=/var/run/mysqld/mysqld.pid # slow query slow_query_log=1 slow_query_log_file=/var/log/mysql/slow.log long_query_time=5 # DB毎に変更する。ex DB1:1, DB2:2, DB:3 server_id=1 default_authentication_plugin=mysql_native_password # super_read_only=on innodb_flush_log_at_trx_commit=2 log_timestamps=SYSTEM # バイナリログの設定 sync_binlog=500 binlog_expire_logs_seconds=259200 # memory(全テーブルのサイズが望ましい) #innodb_buffer_pool_size=2048M # connextion # MySQLがクライアントからの接続パケットを待機する時間(秒) connect_timeout=10 # アプリケーションなどから接続された非対話型の接続に対してのアイドルタイムアウト時間(秒) wait_timeout=300 # mysqlクライアントでログインした時などの対話型の接続に対してのアイドルタイムアウト時間(秒) interactive_timeout=28800 # MySQLの同時接続数 max_connections=2000 (5) mysqldを起動する # systemctl start mysqld # systemctl status mysqld (6) mysqlのパスワード変更する defaultのパスワードを抽出 # grep 'temporary password' /var/log/mysql/mysqld.log | cut -d' ' -f13 抽出したパスワードでログインし、パスワードを設定する # mysql -uroot -p'抽出したパスワード' --connect-expired-password -e "ALTER USER root@localhost IDENTIFIED WITH mysql_native_password BY 'P@ssw0rd'" (7) 読み取り専用で再起動する コメントアウト解除 # vi /etc/my.cnf super_read_only=on # mysqld再起動 # systemctl restart mysqld (8) クラスター用のユーザを作成する # mysqlsh -e "dba.configureLocalInstance('root@localhost:3306',{clusterAdmin:'icroot@\'%\'', clusterAdminPassword:'P@ssw0rd'})" Please provide the password for 'root@localhost:3306': P@ssw0rd Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No): Y Do you want to perform the required configuration changes? [y/n]: y Do you want to restart the instance after configuring it? [y/n]: y Do you want to disable super_read_only and continue? [y/N]:y (9) クラスター用のパスワードを保存する Masterにする1台のみ実行する # mysqlsh -e "shell.connect('icroot@DB1:3306')" Please provide the password for 'icroot@DB1:3306': P@ssw0rd Save password for 'icroot@DB1:3306'? [Y]es/[N]o/Ne[v]er (default No): Y # mysqlsh -e "shell.connect('icroot@DB2:3306')" Please provide the password for 'icroot@DB2:3306': ******** Save password for 'icroot@DB2:3306'? [Y]es/[N]o/Ne[v]er (default No): Y # mysqlsh -e "shell.connect('icroot@DB3:3306')" Please provide the password for 'icroot@DB3:3306': ******** Save password for 'icroot@DB3:3306'? [Y]es/[N]o/Ne[v]er (default No): Y (10) クラスターの作成を行う Masterにする1台のみ実行する The instance 'XXXX' was successfully added to the cluster.と出力されたら問題無し # mysqlsh -e "shell.connect('icroot@DB1:3306');cluster=dba.createCluster('cluster');cluster.addInstance('icroot@DB2:3306',{'recoveryMethod':'clone'});cluster.addInstance('icroot@DB3:3306',{'recoveryMethod':'clone'})" クラスターの状態を確認する。MEMBER_STATEがONLINEになっていればOK。 PRIMARY: Masterノード SECONDARY: Slaveノード # mysql -uroot -p'P@ssw0rd' -e 'select * from performance_schema.replication_group_members;' mysql: [Warning] Using a password on the command line interface can be insecure. +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ | CHANNEL_NAME | MEMBER_ID | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE | MEMBER_VERSION | MEMBER_COMMUNICATION_STACK | +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ | group_replication_applier | 0ee34962-6873-11ec-897b-065f00176f79 | ip-172-31-40-172.ap-northeast-1.compute.internal | 3306 | ONLINE | PRIMARY | 8.0.27 | XCom | | group_replication_applier | 2bb61bf7-6873-11ec-899b-06da60501adb | ip-172-31-46-221.ap-northeast-1.compute.internal | 3306 | ONLINE | SECONDARY | 8.0.27 | XCom | | group_replication_applier | 3317cb98-6873-11ec-898c-06c5c712ef6f | ip-172-31-34-91.ap-northeast-1.compute.internal | 3306 | ONLINE | SECONDARY | 8.0.27 | XCom | +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ (11) DBの作成・ユーザーの設定を行う Masterにする1台のみ実行する DBを作成 # mysql -uroot -p'P@ssw0rd' -e "create database if not exists world" 管理ユーザ作成 # mysql -uroot -p'P@ssw0rd' -e "create user admin@'%' identified by 'P@ssw0rd'" # mysql -uroot -p'P@ssw0rd' -e "grant all on world.* to admin@'%'" # mysql -uroot -p'P@ssw0rd' -e "grant super on *.* to admin@'%'" 参照ユーザ作成 # mysql -uroot -p'P@assw0rd' -e "create user operator@'%' identified by 'P@assw0rd'" # mysql -uroot -p'P@assw0rd' -e "grant select on world.* to operator@'%'" (12) パラメータ設定 全台 デフォルトだと「あるノードに障害が発生し、グループレプリケーションから離脱した時の挙動は更新不可。参照可能」になる。そのため、更新されなくなった当該DBがProxySQLから参照できてしまい、 古いデータがselectされる事態になる。 これを避けるため、以下の設定をする。 # mysql -uroot -p mysql> select @@group_replication_exit_state_action; +---------------------------------------+ | @@group_replication_exit_state_action | +---------------------------------------+ | READ_ONLY | +---------------------------------------+ 1 row in set (0.00 sec) mysql> SET persist group_replication_exit_state_action = ABORT_SERVER; Query OK, 0 rows affected (0.00 sec) mysql> select @@group_replication_exit_state_action; +---------------------------------------+ | @@group_replication_exit_state_action | +---------------------------------------+ | ABORT_SERVER | +---------------------------------------+ 1 row in set (0.00 sec) mysql> また、古いバージョンのmysqlだと、以下の値が0に設定されるため頻繁に離脱するようになる。 ※頻繁に離脱する際はこの値を変更する。 mysql> select @@group_replication_member_expel_timeout; +------------------------------------------+ | @@group_replication_member_expel_timeout | +------------------------------------------+ | 5 | +------------------------------------------+ 1 row in set (0.00 sec) mysql> select @@group_replication_autorejoin_tries; +--------------------------------------+ | @@group_replication_autorejoin_tries | +--------------------------------------+ | 3 | +--------------------------------------+ 1 row in set (0.00 sec) mysql> ■設定の意味 パラメータ 設定値 設定の意味 設定方法 group_replication_exit_state_action ABORT_SERVER(サーバ停止) サーバーインスタンスが意図せずにグループを離れたとき、グループを追放した場合のグループレプリケーションの動作を構成する。 SET persist group_replication_exit_state_action = ABORT_SERVER; group_replication_member_expel_timeout 5 グループレプリケーショングループメンバーが離脱疑惑を作成した後、失敗した疑いのあるメンバーをグループから追放するまで待機する時間を秒単位で指定する。8.0.22以降デフォルト値が5に変更。 SET persist group_replication_member_expel_timeout = 5; group_replication_autorejoin_tries 1 メンバーが追放された場合、またはgroup_replication_unreachable_majority_timeout 設定に達する前にグループの過半数に連絡できない場合に、メンバーがグループに自動的に再参加するために行う試行回数。 SET group_replication_autorejoin_tries = 1; ここまででグループレプリケーションの構築は完了。 ProxySQL用の1台の構築 (1) mysql clientをインストールする mysqlリポジトリの追加 # rpm -Uvh https://dev.mysql.com/get/mysql80-community-release-el7-4.noarch.rpm MySQL 8.0 Community Serverが有効になっているか確認 # yum repolist all | grep mysql mysql clientをインストールする # yum install mysql-community-client (2) proxySQLをインストールする proxysqlリポジトリの追加 # cat <<EOF | tee /etc/yum.repos.d/proxysql.repo [proxysql_repo] name= ProxySQL YUM repository baseurl=https://repo.proxysql.com/ProxySQL/proxysql-2.2.x/centos/7 gpgcheck=1 gpgkey=https://repo.proxysql.com/ProxySQL/repo_pub_key EOF (3) proxysqlをインストールする # yum install proxysql proxysql用のログディレクトリを作成する # mkdir /var/log/proxysql # touch /var/log/proxysql/proxysql.log # chown -R proxysql. /var/log/proxysql 自動起動を有効化 # systemctl enable proxysql (4) ProxySQLを起動する # systemctl start proxysql (5) proxysql.cnfの設定をする adminユーザは更新専用(Masterに行く)、operatorユーザは参照専用(Slaveに行く)に設定をしています。 ※この設定の場合Slaveが全台落ちた場合に参照できなくなります。 # vi /etc/proxysql.cnf #file proxysql.cfg ######################################################################################## # This config file is parsed using libconfig , and its grammar is described in: # http://www.hyperrealm.com/libconfig/libconfig_manual.html#Configuration-File-Grammar # Grammar is also copied at the end of this file ######################################################################################## ######################################################################################## # IMPORTANT INFORMATION REGARDING THIS CONFIGURATION FILE: ######################################################################################## # On startup, ProxySQL reads its config file (if present) to determine its datadir. # What happens next depends on if the database file (disk) is present in the defined # datadir (i.e. "/var/lib/proxysql/proxysql.db"). # # If the database file is found, ProxySQL initializes its in-memory configuration from # the persisted on-disk database. So, disk configuration gets loaded into memory and # then propagated towards the runtime configuration. # # If the database file is not found and a config file exists, the config file is parsed # and its content is loaded into the in-memory database, to then be both saved on-disk # database and loaded at runtime. # # IMPORTANT: If a database file is found, the config file is NOT parsed. In this case # ProxySQL initializes its in-memory configuration from the persisted on-disk # database ONLY. In other words, the configuration found in the proxysql.cnf # file is only used to initial the on-disk database read on the first startup. # # In order to FORCE a re-initialise of the on-disk database from the configuration file # the ProxySQL service should be started with "systemctl start proxysql-initial". # ######################################################################################## datadir="/var/lib/proxysql" errorlog="/var/lib/proxysql/proxysql.log" admin_variables= { admin_credentials="admin:P@ssw0rd" mysql_ifaces="127.0.0.1:6032;/var/lib/proxysql/admin.sock" # mysql_ifaces="0.0.0.0:6032" # refresh_interval=2000 # debug=true } mysql_variables= { threads=4 max_connections=2000 default_query_delay=0 default_query_timeout=36000000 have_compress=true poll_timeout=2000 interfaces="0.0.0.0:3306;/var/lib/proxysql/proxysql.sock" # interfaces="0.0.0.0:6033" default_schema="information_schema" stacksize=1048576 server_version="8.0.27" connect_timeout_server=3000 # make sure to configure monitor username and password # https://github.com/sysown/proxysql/wiki/Global-variables#mysql-monitor_username-mysql-monitor_password monitor_username="icroot" monitor_password="P@ssw0rd" monitor_history=600000 monitor_connect_interval=60000 monitor_ping_interval=10000 monitor_read_only_interval=1500 monitor_read_only_timeout=500 monitor_writer_is_also_reader="false" ping_interval_server_msec=120000 ping_timeout_server=500 commands_stats=true sessions_sort=true connect_retries_on_failure=10 } # defines all the MySQL servers mysql_servers = ( # { # address = "127.0.0.1" # no default, required . If port is 0 , address is interpred as a Unix Socket Domain # port = 3306 # no default, required . If port is 0 , address is interpred as a Unix Socket Domain # hostgroup = 0 # no default, required # status = "ONLINE" # default: ONLINE # weight = 1 # default: 1 # compression = 0 # default: 0 # max_replication_lag = 10 # default 0 . If greater than 0 and replication lag passes such threshold, the server is shunned # }, # { # address = "/var/lib/mysql/mysql.sock" # port = 0 # hostgroup = 0 # }, # { # address="127.0.0.1" # port=21891 # hostgroup=0 # max_connections=200 # }, { address="DB1" , port=3306 , hostgroup=1 }, { address="DB2" , port=3306 , hostgroup=1 }, { address="DB3" , port=3306 , hostgroup=1 } ) # defines all the MySQL users mysql_users: ( { username = "icroot" password = "P@ssw0rd" default_hostgroup = 1 active = 1 max_connections = 100 default_schema = "mysql" }, { username = "admin" password = "P@ssw0rd" default_hostgroup = 1 active = 1 max_connections = 1000 default_schema = "mysql" }, { username = "operator" password = "P@ssw0rd" default_hostgroup = 1 active = 1 max_connections = 1000 default_schema = "mysql" } ) #defines MySQL Query Rules mysql_query_rules: ( { rule_id=1 active=1 username="admin" destination_hostgroup=0 apply=1 }, { rule_id=2 active=1 username="operator" destination_hostgroup=1 apply=1 } ) scheduler= ( # { # id=1 # active=0 # interval_ms=10000 # filename="/var/lib/proxysql/proxysql_galera_checker.sh" # arg1="0" # arg2="0" # arg3="0" # arg4="1" # arg5="/var/lib/proxysql/proxysql_galera_checker.log" # } ) mysql_replication_hostgroups= ( { writer_hostgroup=0 reader_hostgroup=1 comment="replication" } ) (6) proxysql.dbを削除する 設定はここで持っているため、一旦初期化する。 ※ 稼働中にproxysqlの設定を変えたい場合はUPDATE global_variables SET variable_value='XXXX' WHERE variable_name = 'XXXX'で設定値を更新した後、LOAD MYSQL VARIABLES TO RUNTIME;でproxySQLに反映して、SAVE MYSQL VARIABLES TO DISK;を叩けば、proxysql.dbに保存され、再起動後も設定が維持されます。 # rm -i /var/lib/proxysql/proxysql.db (7) ProxySQLを再起動する # systemctl restart proxysql (8) ProxySQL側からMySQL GroupReplicationが参照できていることを確認する。 ※ちなみに、ここのステータスはProxySQLからSQLが発行された際に検証されます。DBが落ちていてもSQL発行していなければSQL Router側からはONLINEで見えることもあるので注意。 # mysql -u icroot -p'P@ssw0rd' --socket=/var/lib/proxysql/admin.sock -e 'select * from stats_mysql_connection_pool;' mysql: [Warning] Using a password on the command line interface can be insecure. +-----------+----------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+ | hostgroup | srv_host | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | MaxConnUsed | Queries | Queries_GTID_sync | Bytes_data_sent | Bytes_data_recv | Latency_us | +-----------+----------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+ | 1 | DB3 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 541 | | 1 | DB2 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 566 | | 0 | DB1 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 495 | +-----------+----------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+ ProxySQL側からGroup Replicationへの接続確認 Master更新ユーザ # mysql -u admin -p'P@ssw0rd' --socket=/var/lib/proxysql/proxysql.sock Slave参照ユーザ # mysql -u operator -p'P@ssw0rd' --socket=/var/lib/proxysql/proxysql.sock 以上。 再起動方法 ★サーバを個別に再起動したい場合 Master/Slaveでもsystemctlから再起動すれば、自動的にグループレプリケーションに組み込んでくれる。 # systemctl restart mysqld # mysql -uroot -p'P@ssw0rd' -e 'select * from performance_schema.replication_group_members;' mysql: [Warning] Using a password on the command line interface can be insecure. +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ | CHANNEL_NAME | MEMBER_ID | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE | MEMBER_VERSION | MEMBER_COMMUNICATION_STACK | +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ | group_replication_applier | 0ee34962-6873-11ec-897b-065f00176f79 | ip-172-31-40-172.ap-northeast-1.compute.internal | 3306 | ONLINE | PRIMARY | 8.0.27 | XCom | | group_replication_applier | 2bb61bf7-6873-11ec-899b-06da60501adb | ip-172-31-46-221.ap-northeast-1.compute.internal | 3306 | RECOVERING | SECONDARY | 8.0.27 | XCom | | group_replication_applier | 3317cb98-6873-11ec-898c-06c5c712ef6f | ip-172-31-34-91.ap-northeast-1.compute.internal | 3306 | ONLINE | SECONDARY | 8.0.27 | XCom | +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ ★グループレプリケーション全体を再起動したい場合 例えば、DBサーバを全台停止→起動後、グループレプリケーションを起動したいケース 再起動後の状態 # mysql -uroot -p'P@ssw0rd' -e 'select * from performance_schema.replication_group_members;' mysql: [Warning] Using a password on the command line interface can be insecure. +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ | CHANNEL_NAME | MEMBER_ID | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE | MEMBER_VERSION | MEMBER_COMMUNICATION_STACK | +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ | group_replication_applier | 0ee34962-6873-11ec-897b-065f00176f79 | ip-172-31-40-172.ap-northeast-1.compute.internal | 3306 | OFFLINE | | | XCom | +---------------------------+--------------------------------------+--------------------------------------------------+-------------+--------------+-------------+----------------+----------------------------+ # mysqlsh masterだったノードに接続する MySQL JS > shell.connect('icroot@db1:3306') 再起動する MySQL db1:3306 ssl JS > dba.rebootClusterFromCompleteOutage() Would you like to rejoin it to the cluster? [y/N]: y 以下が出力されればOK。 The cluster was successfully rebooted. <Cluster:cluster> MySQL db1:3306 ssl JS >\q 全台ONLINEになっていることを確認する # mysql -uroot -p'P@ssw0rd' -e 'select * from performance_schema.replication_group_members;' ★手動リカバリ方法 Group Replicationは自動リカバリしてくれるが、バイナリログ保持期間が過ぎた、もしくはDBに不整合が起きた場合は手動でリカバリする必要がある。 ★1ノード離脱で、バイナリログの保持期間も過ぎた場合 Plugin group_replication reported: 'Member with address db3:3306 has become unreachable.やMember was expelled from the group due to network failures, changing member status to ERROR.がログに出て、離脱したケース。他のPRIMARY、およびSLAVEはONLINEの状態。 オンラインの方のスレーブノードでダンプを取得し、離脱サーバに配布する。 # mysqldump --all-databases -uroot --triggers --routines --events -p > /tmp/backup_db.sql # scp /tmp/backup_db.sql db3:/tmp 離脱ノードにログインして、リストアする # mysql -u root; mysql> reset master; mysql> reset slave; mysql> set global super_read_only=off; mysql> source /tmp/backup_db.sql; リストア後 mysql> set global super_read_only=on; 離脱ノードを再参加させる # mysqlsh マスターノードにログインする MySQL JS > shell.connect('icroot@db1:3306') MySQL JS > cluster = dba.getCluster() 離脱ノードを再参加させる MySQL JS > cluster.addInstance('icroot@db3:3306') ノードが再参加したか確認する MySQL JS > cluster.status() 再参加時にCluster.addInstance: The instance 'db3:3306' is already part of another InnoDB cluster (RuntimeError)と出る場合は、cluster.removeInstance('icroot@db3:3306', {force:true})で離脱ノードを一旦グループレプリケーションから消して再参加させる。 ★全ノード離脱で、DBの整合が取れなくなった場合 各サーバにログインしてgtidが最新のものを確認する。 全サーバで確認 # mysql -uroot -p mysql> show global variables like "gtid%"; +----------------------------------+---------------------------------------------------------------------------------------------+ | Variable_name | Value | +----------------------------------+---------------------------------------------------------------------------------------------+ | gtid_executed | 08099b3b-f646-11ea-b99d-005056a3841b:1-300406342, ★ここの数値が一番進んでいるものが新しい ・・・ gtidが最新のDBでダンプを取得し、離脱サーバに配布する。 # mysqldump --all-databases -uroot --triggers --routines --events -p > /tmp/backup_db.sql # scp /tmp/backup_db.sql db2:/tmp # scp /tmp/backup_db.sql db3:/tmp 各サーバでリストア # mysql -u root; mysql> reset master; mysql> reset slave; mysql> set global super_read_only=off; mysql> source /tmp/backup_db.sql; リストア後 mysql> set global super_read_only=on; クラスター全体を再起動する # mysqlsh gtidが最新だったノードにログインする MySQL JS > shell.connect('icroot@db1:3306') MySQL JS > cluster = dba.getCluster() クラスター全体を再起動する MySQL JS > dba.rebootClusterFromCompleteOutage() ノードが再参加したか確認する MySQL JS > cluster.status() ※MISSINGのノードが存在する場合は当該ノードのmysqldを再起動すると改善する場合がある。 ※全ノードがONLINEの状態だが、1DBだけERRORステータスで、addによる再参加もできないケースが発生した場合、以下のようなエラーがでることがある。 Cluster.addInstance: Operation not allowed. No cluster change operations can be executed because the installed metadata version 1.0.1 is lower than the version required by Shell which is version 2.0.0. Upgrade the metadata to remove this restriction. See \? dba.upgradeMetadata for additional details. (RuntimeError) dba.upgradeMetadataを実行しても上記と同じようなエラーで実行できない。おそらく、グループ再構成時にMASTERではないノードから起動した場合に、二つグループができてしまったことが原因? この場合は、STOP GROUP_REPLICATION;を実行し、MASTERのダンプファイルをERRORになっているノードでリストア後、いったん全ノードのグループレプリケーションを停止し、(サーバ再起動、もしくはSTOP GROUP_REPLICATION;)。マスターノードからdba.rebootClusterFromCompleteOutage()を実行する。 ※リストアで断片化して、SQLが遅延するケースが発生することもあるので、リストア→グループレプリケーション再構成後は各DB毎にoptimize table [テーブル名]を打つのが安全。 ★バイナリログ削除 保持期間の関係でバイナリログが増大してしまった時の対処。通常、バイナリログを削除するコマンドを打つが、この構成の場合は 各DBサーバ毎に実行する必要がある。 ■DB1~DB3 # mysql -uroot -p バイナリログ一覧を出力する mysql> show master logs; mysql> purge master logs to 'binlog.000044'; 2. MySQL Replication(GTID) + MHA + Proxy SQL構成 MySQLのデータを複数の MySQL データベースサーバー (レプリカと呼ばれる) にコピーできる機能を使った冗長化構成。MHA + ProxySQLと合わせて冗長化・分散アクセスが可能。 ソースから更新を受信するためにレプリカを永続的に接続する必要はないという特性のため、group replicationの様な永続的アクセスが不要になり、遠距離のデータ分散にも向いている。 また、GTIDでレプリケーションを組むことで本構成 + マルチソースレプリケーション構成にもしやすい。 Masterが落ちた場合はMHAでフェイルオーバー ■ 要件 項目 要件/仕様 備考 最小構成 2台 フェイルオーバーソフト次第 最大構成 2台 フェイルオーバーソフト次第 ストレージエンジン InnoDB 要求帯域 小 フェイルオーバーソフト次第 稼働中の拡張 不可 フェイルオーバーソフト次第 ☆ memo サーバ台数が2台とお手頃なため、小~中規模のシステムに採用される印象。台数は少ないが、サーバ単体のスペックは高めにして、ある程度の負荷にも耐えられるように設計していることが多い。 MHAはLinux8以降の対応パッケージが今のところ無いので、将来的には代替フェイルオーバーのミドルが必要かもしれない。※一応、特定の設定をすれば8系でも動作はしますが、自己責任でお願い致します。 1度フェイルオーバーするとMHA側の再設定が必要なため、運用上はZabbixなどでフェイルオーバーを検知後、手動で再設定が必要になる。 インストール方法 環境:Rocky Linux8 Replication:2台 Proxy SQL:1台 全台共通 (1) 事前準備 # vi /etc/selinux/config > SELINUX=disabled fierewalld無効 # systemctl disable firewalld timezone変更 # timedatectl set-timezone Asia/Tokyo パッケージアップデート ♯ yum update 再起動 # reboot (2) hostnameでお互いに疎通できるか確認する # hostname # ping [自分以外のhost名称] 解決できなければ/etc/hostsに設定を追加し、その名称を利用する。 # vi /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 172.31.42.85 DB1 172.31.40.245 DB2 Replication2台の構築 (1) mysqlをインストールする mysqlモジュールの無効化 # dnf module disable mysql mysqlリポジトリの追加 # rpm -Uvh https://dev.mysql.com/get/mysql80-community-release-el8-2.noarch.rpm MySQL 8.0 Community Serverが有効になっているか確認 # dnf repolist all | grep mysql mysqlをインストールする # dnf install mysql-community-server log用ディレクトリを作成する # mkdir /var/log/mysql # touch /var/log/mysql/slow.log # chown mysql. -R /var/log/mysql 自動起動を有効化 # systemctl enable mysqld (2) my.cnfを設定する GTIDを有効化する。 ■MasterにするDB # vi /etc/my.cnf # For advice on how to change settings please see # http://dev.mysql.com/doc/refman/8.0/en/server-configuration-defaults.html [mysqld] # # Remove leading # and set to the amount of RAM for the most important data # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. # innodb_buffer_pool_size = 128M # # Remove the leading "# " to disable binary logging # Binary logging captures changes between backups and is enabled by # default. It's default setting is log_bin=binlog # disable_log_bin # # Remove leading # to set options mainly useful for reporting servers. # The server defaults are faster for transactions and fast SELECTs. # Adjust sizes as needed, experiment to find the optimal values. # join_buffer_size = 128M # sort_buffer_size = 2M # read_rnd_buffer_size = 2M # # Remove leading # to revert to previous value for default_authentication_plugin, # this will increase compatibility with older clients. For background, see: # https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_default_authentication_plugin # default-authentication-plugin=mysql_native_password datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock log-error=/var/log/mysql/mysqld.log pid-file=/var/run/mysqld/mysqld.pid # slow query slow_query_log=1 slow_query_log_file=/var/log/mysql/slow.log long_query_time=5 # DB毎に変更する。ex DB1:1, DB2:2 server_id=1 default_authentication_plugin=mysql_native_password innodb_flush_log_at_trx_commit=2 log_timestamps=SYSTEM # GTIDの有効化 gtid_mode=ON enforce_gtid_consistency # バイナリログの設定 log-bin = mysql-bin sync_binlog=500 binlog_expire_logs_seconds=259200 # memory(全テーブルのサイズが望ましい) #innodb_buffer_pool_size=2048M # connextion # MySQLがクライアントからの接続パケットを待機する時間(秒) connect_timeout=10 # アプリケーションなどから接続された非対話型の接続に対してのアイドルタイムアウト時間(秒) wait_timeout=300 # mysqlクライアントでログインした時などの対話型の接続に対してのアイドルタイムアウト時間(秒) interactive_timeout=28800 # MySQLの同時接続数 max_connections=2000 ■slaveにするDB # vi /etc/my.cnf # For advice on how to change settings please see # http://dev.mysql.com/doc/refman/8.0/en/server-configuration-defaults.html [mysqld] # # Remove leading # and set to the amount of RAM for the most important data # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. # innodb_buffer_pool_size = 128M # # Remove the leading "# " to disable binary logging # Binary logging captures changes between backups and is enabled by # default. It's default setting is log_bin=binlog # disable_log_bin # # Remove leading # to set options mainly useful for reporting servers. # The server defaults are faster for transactions and fast SELECTs. # Adjust sizes as needed, experiment to find the optimal values. # join_buffer_size = 128M # sort_buffer_size = 2M # read_rnd_buffer_size = 2M # # Remove leading # to revert to previous value for default_authentication_plugin, # this will increase compatibility with older clients. For background, see: # https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_default_authentication_plugin # default-authentication-plugin=mysql_native_password datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock log-error=/var/log/mysql/mysqld.log pid-file=/var/run/mysqld/mysqld.pid # slow query slow_query_log=1 slow_query_log_file=/var/log/mysql/slow.log long_query_time=5 # DB毎に変更する。ex DB1:1, DB2:2 server_id=2 default_authentication_plugin=mysql_native_password innodb_flush_log_at_trx_commit=2 log_timestamps=SYSTEM # スレーブSQLスレッドによって実行される更新をバイナリログに書き込む log_slave_updates # リレーログファイルの名前 relay_log=mysql-relay-bin # クラッシュセーフなレプリケーション設定 relay_log_info_repository=TABLE relay_log_recovery=ON relay_log_purge=ON # GTIDの有効化 gtid_mode=ON enforce_gtid_consistency # バイナリログの設定 log-bin = mysql-bin sync_binlog=500 binlog_expire_logs_seconds=259200 # memory(全テーブルのサイズが望ましい) #innodb_buffer_pool_size=2048M # connextion # MySQLがクライアントからの接続パケットを待機する時間(秒) connect_timeout=10 # アプリケーションなどから接続された非対話型の接続に対してのアイドルタイムアウト時間(秒) wait_timeout=300 # mysqlクライアントでログインした時などの対話型の接続に対してのアイドルタイムアウト時間(秒) interactive_timeout=28800 # MySQLの同時接続数 max_connections=2000 (3) mysqlを起動する # systemctl start mysqld 6.mysqlのパスワード変更する defaultのパスワードを抽出 # grep 'temporary password' /var/log/mysql/mysqld.log | cut -d' ' -f13 抽出したパスワードでログインし、パスワードを変更する # mysql -uroot -p'抽出したパスワード' --connect-expired-password -e "ALTER USER root@localhost IDENTIFIED WITH mysql_native_password BY 'P@ssw0rd'" (4) DBの作成・ユーザの設定を行う。 Masterにする1台のみ実行する DBを作成 # mysql -uroot -p'P@ssw0rd' -e "create database if not exists world" レプリケーション用のユーザ作成 # mysql -uroot -p'P@ssw0rd' -e "create user repl@'%' identified by 'P@ssw0rd'" # mysql -uroot -p'P@ssw0rd' -e "grant replication slave on *.* to 'repl'@'%';" (5) DBをDumpし、slaveサーバに送信する。 Masterにする1台のみ実行する # mysqldump -u root -p \ --single-transaction \ --flush-logs \ --master-data=2 \ --all-databases \ --hex-blob \ --triggers \ --routines \ --events > /tmp/master_db.sql スレーブにするサーバに送信 # scp /tmp/master_db.sql root@172.31.40.245:/tmp (6) ダンプファイルをリストアする Slaveにする1台のみ実行する # mysql -u root -p mysql> reset master; mysql> source /tmp/master_db.sql (7) レプリケーションを設定する Slaveにする1台のみ実行する # mysql -u root -p CHANGE MASTER TO MASTER_HOST='172.31.42.85', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='P@ssw0rd', MASTER_AUTO_POSITION=1; (8) Replicationを起動する Slaveにする1台のみ実行する # mysql -u root -p mysql> start slave; mysql> show slave status\G; Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.エラーが出るときは、 SHA2側にキャッシュされていないことが問題なので一度スレーブサーバ側からログインする。 # mysql -u repl -h 172.31.42.85 -p GTIDによるレプリケーションここまで (9) MHAをダウンロード・インストールする # dnf install wget EPEL/PowerToolsを有効化 # dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm # vi /etc/yum.repos.d/Rocky-PowerTools.repo ※cntosの場合は/etc/yum.repos.d/CentOS-Linux-PowerTools.repo 以下のよう書き換える。 enabled=1 # wget https://github.com/KyojiOsada/mha4mysql/raw/master/mha4mysql-node-0.57-0.el7.noarch.rpm # wget https://github.com/KyojiOsada/mha4mysql/raw/master/mha4mysql-manager-0.57-0.el7.noarch.rpm # dnf install perl-DBD-MySQL perl-Time-HiRes perl-Config-Tiny perl-Specio # yum localinstall mha4mysql-node-0.57-0.el7.noarch.rpm mha4mysql-manager-0.57-0.el7.noarch.rpm (10) mha.confを設定する $ vi /etc/mha.conf [server default] user=mha password=P@ssw0rd manager_workdir=/var/lib/mha manager_log=/var/log/mha.log remote_workdir=/var/lib/mha repl_user=repl repl_password=P@assw0rd ssh_user=mha [server1] hostname=172.31.42.85 [server2] hostname=172.31.40.245 (11) MHA用のユーザを作成する # useradd -m mha ssh準備 # su - mha $ mkdir .ssh $ chmod 700 .ssh ディレクトリ作成 # mkdir /var/lib/mha # chown mha. /var/lib/mha ログ作成 # touch /var/log/mha.log # chown mha. /var/log/mha.log (12) ssh認証鍵を生成する Masterにする1台のみ # su - mha $ cd .ssh $ ssh-keygen -t rsa Enter3回 $ mv id_rsa.pub authorized_keys 鍵を送信する。 $ scp id_rsa.pub mha@172.31.40.245:/home/mha/.ssh/. $ scp authorized_keys mha@172.31.40.245:/home/mha/.ssh/. $ vi authorized_keys 末尾を相手のホスト名に変更する。 ssh-rsa ・・・・ mha@ip-172-31-40-245.ap-northeast-1.compute.internal 疎通確認 $ ssh 172.31.40.245 (13) mhaユーザを作成する Masterにする1台のみ # mysql -uroot -p'P@ssw0rd' -e "create user mha@'%' identified by 'P@ssw0rd'" # mysql -uroot -p'P@ssw0rd' -e "grant all on *.* to 'mha'@'%';" (14) mha疎通 Slaveにする1台のみ実行する sshチェック $ su - mha $ /usr/bin/masterha_check_ssh --conf=/etc/mha.cnf レプリケーションチェック MySQL8系の場合は以下エラーになる。 $ /usr/bin/masterha_check_repl --conf=/etc/mha.cnf Fri Dec 31 21:33:37 2021 - [warning] Global configuration file /etc/masterha_default.cnf not found. Skipping. Fri Dec 31 21:33:37 2021 - [info] Reading application default configuration from /etc/mha.cnf.. Fri Dec 31 21:33:37 2021 - [info] Reading server configuration from /etc/mha.cnf.. Fri Dec 31 21:33:37 2021 - [info] MHA::MasterMonitor version 0.57. (in cleanup) cannot chdir to /root from /tmp/yj731UNUpw: Permission denied, aborting. at /usr/share/perl5/vendor_perl/Parallel/ForkManager.pm line 327. Fri Dec 31 21:33:38 2021 - [error][/usr/share/perl5/vendor_perl/MHA/MasterMonitor.pm, ln427] Error happened on checking configurations. Redundant argument in sprintf at /usr/share/perl5/vendor_perl/MHA/NodeUtil.pm line 190. Fri Dec 31 21:33:38 2021 - [error][/usr/share/perl5/vendor_perl/MHA/MasterMonitor.pm, ln525] Error happened on monitoring servers. Fri Dec 31 21:33:38 2021 - [info] Got exit code 1 (Not master dead). MySQL Replication Health is NOT OK! バージョンが取得できないのが原因なので、MHAのソースを下記の様に修正する。 〇/usr/share/perl5/vendor_perl/MHA/NodeUtil.pm sub parse_mysql_major_version($) { my $str = shift; $str =~ /(\d+)\.(\d+)/; my $strmajor = "$1.$2"; my $result = sprintf( '%03d%03d', $strmajor =~ m/(\d+)/g ); return $result; } 通る $ /usr/bin/masterha_check_repl --conf=/etc/mha.cnf ・・・ MySQL Replication Health is OK. (15) MHAを起動する $ nohup /usr/bin/masterha_manager --conf=/etc/mha.cnf & (16) ProxySQLインストール 前述の[13proxy-sql用の1台]mysql-clientをインストールするを参照。 以上 動作確認 DB1号機のmysqldを止めると、2号機にフェイルオーバーする # tail -f /var/log/mha.log ----- Failover Report ----- mha: MySQL Master failover 172.31.42.85(172.31.42.85:3306) to 172.31.40.245(172.31.40.245:3306) succeeded Master 172.31.42.85(172.31.42.85:3306) is down! Check MHA Manager logs at ip-172-31-40-245.ap-northeast-1.compute.internal:/var/log/mha.log for details. Started automated(non-interactive) failover. Selected 172.31.40.245(172.31.40.245:3306) as a new master. 172.31.40.245(172.31.40.245:3306): OK: Applying all logs succeeded. 172.31.40.245(172.31.40.245:3306): Resetting slave info succeeded. Master failover to 172.31.40.245(172.31.40.245:3306) completed successfully. ただし、mhaプロセスは終了するので、再度nohup /usr/bin/masterha_manager --conf=/etc/mha.cnf &でプロセスを上げる必要がある。 3. MySQL Cluster構成 遠隔地レプリケーションに実績があるミドルウェア。単一障害点が存在しないのが特徴。 データを保管するデータノード、SQLの調停をする管理ノード、SQL実行を受け付けるSQLノードの3つに分かれる。1サーバに「SQL + データノード」や、「管理 + データノード」 と同居させることも可能なので、割と柔軟に構成を変えられる。また、余談だがNDBエンジンでMySQL CLusterを組み、InoDBエンジンでMySQLレプリケーションを組む構成も可能。 ■ 要件 項目 要件/仕様 最大データノード 145 最大管理ノード 255 最大SQLノード 記載なし 合計最大ノード数 255 ストレージエンジン NDB 要求帯域 中~高 稼働中の拡張 可 インストール方法 環境:Rocky Linux8 Management:1台 DataNode: 2台 SQL Node:1台 全台共通 (1) 事前準備 selinux無効 # vi /etc/selinux/config > SELINUX=disabled fierewalld無効 # systemctl disable firewalld timezone変更 # timedatectl set-timezone Asia/Tokyo パッケージアップデート ♯ dnf update 再起動 # reboot (2) mysqlユーザの追加 # grep mysql /etc/passwd # groupadd mysql -g 27 # useradd -g mysql -s /bin/false mysql -u 27 # id mysql uid=27(mysql) gid=27(mysql) groups=27(mysql) (3) rpmファイルのダウンロード&解凍 # cd /tmp # dnf install wget # mkdir work # wget https://dev.mysql.com/get/Downloads/MySQL-Cluster-8.0/mysql-cluster-community-8.0.28-1.el8.x86_64.rpm-bundle.tar # tar xf mysql-cluster-community-8.0.28-1.el8.x86_64.rpm-bundle.tar -C work/ 不要ファイル削除 # rm -f mysql-cluster-community-8.0.28-1.el8.x86_64.rpm-bundle.tar SQLノード2台の構築 MySQL ClusterでSQLノードにするサーバで実行する。 (1) SQLノードをインストールする 依存パッケージのインストール # dnf install perl libaio libaio-devel net-tools # cd /tmp/work/ # rpm -Uhv mysql-cluster-community-debuginfo-8.0.28-1.el8.x86_64.rpm # rpm -Uhv mysql-cluster-community-client-* mysql-cluster-community-common-8.0.28-1.el8.x86_64.rpm mysql-cluster-community-libs-* mysql-cluster-community-icu-data-files-8.0.28-1.el8.x86_64.rpm # rpm -Uhv mysql-cluster-community-server-8.0.28-1.el8.x86_64.rpm 不要ファイル削除 # cd /tmp # rm -rf work (2) mysqlログ出力用のディレクトリを作成する ※デフォルト値(/var/log/mysqld.log)から/var/log/mysql/mysqld.logに変えた場合 # cd /var/log # mkdir mysql # chown mysql. mysql (3) my.cnfを設定する # vi /etc/my.cnf # For advice on how to change settings please see # http://dev.mysql.com/doc/refman/8.0/en/server-configuration-defaults.html [mysqld] # # Remove leading # and set to the amount of RAM for the most important data # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. # innodb_buffer_pool_size = 128M # # Remove the leading "# " to disable binary logging # Binary logging captures changes between backups and is enabled by # default. It's default setting is log_bin=binlog # disable_log_bin # # Remove leading # to set options mainly useful for reporting servers. # The server defaults are faster for transactions and fast SELECTs. # Adjust sizes as needed, experiment to find the optimal values. # join_buffer_size = 128M # sort_buffer_size = 2M # read_rnd_buffer_size = 2M # # Remove leading # to revert to previous value for default_authentication_plugin, # this will increase compatibility with older clients. For background, see: # https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_default_authentication_plugin # default-authentication-plugin=mysql_native_password datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock log-error=/var/log/mysql/mysqld.log pid-file=/var/run/mysqld/mysqld.pidi slow_query_log=ON long_query_time=5 slow_query_log_file=/var/log/mysql/slow.log server-id=1 # サーバ毎に変更する gtid_mode=ON enforce-gtid-consistency innodb_flush_log_at_trx_commit=2 sync_binlog=500 log_bin=/var/lib/mysql/mysql-bin.log # innodb_buffer_pool_size=2048M default_authentication_plugin=mysql_native_password binlog_expire_logs_seconds=259200 log_timestamps=SYSTEM connect_timeout=10 wait_timeout=300 interactive_timeout=28800 max_connections=1000 [mysqld] ndbcluster [mysql_cluster] ndb-connectstring="172.31.38.31:1186,172.31.47.41:1186" データノードのインストール MySQL Clusterでデータノードにするサーバで実行する。 (1) データノードをインストールする # cd /tmp/work データノードインストール # rpm -Uhv mysql-cluster-community-data-node-8.0.28-1.el8.x86_64.rpm mysqlclient、commonのインストール # dnf install perl # rpm -Uhv mysql-cluster-community-debuginfo-8.0.28-1.el8.x86_64.rpm # rpm -Uhv mysql-cluster-community-client-* mysql-cluster-community-common-8.0.28-1.el8.x86_64.rpm mysql-cluster-community-libs-* 不要ファイル削除 # cd /tmp # rm -rf work (2) データ用のディレクトリを削除する # cd /var/lib # mkdir mysql # chown mysql. mysql 監理ノードのインストール MySQL Clusterで管理ノードにするサーバで実行する。 (1) 監理ノードをインストールする # cd /tmp/work 監理ノードインストール # rpm -Uhv mysql-cluster-community-management-server-8.0.28-1.el8.x86_64.rpm mysqlclient、commonのインストール # dnf install perl # rpm -Uhv mysql-cluster-community-debuginfo-8.0.28-1.el8.x86_64.rpm # rpm -Uhv mysql-cluster-community-client-* mysql-cluster-community-common-8.0.28-1.el8.x86_64.rpm mysql-cluster-community-libs-* 不要ファイル削除 # cd /tmp # rm -rf work (2) 監理用のディレクトリを削除する # mkdir /var/lib/mysql-cluster # chown mysql. /var/lib/mysql-cluster (3) 管理用の設定ファイルの作成 # cd /var/lib/mysql-cluster # vi config.ini # chown mysql. config.ini ■設定 [tcp default] SendBufferMemory=2M ReceiveBufferMemory=2M [ndb_mgmd default] DataDir=/var/lib/mysql-cluster [ndb_mgmd] # 管理ノードを設定 # Management process options: HostName=172.31.41.195 NodeId=1 [ndbd default] # Options affecting ndbd processes on all data nodes: NoOfReplicas=2 LockPagesInMainMemory=1 DataMemory=90M #IndexMemory=384M #NoOfFragmentLogFiles=300 #MaxNoOfConcurrentOperations=100000 MaxNoOfTables=1024 MaxNoOfOrderedIndexes=256 [ndbd] # データノードを設定 # Options for data node "A": # (one [ndbd] section per data node) HostName=172.31.44.196 # Hostname or IP address NodeId=3 # Node ID for this data node DataDir=/var/lib/mysql # Directory for this data node's data files [ndbd] # Options for data node "B": HostName=172.31.32.52 NodeId=4 DataDir=/var/lib/mysql [mysqld] # SQLノードを設定 # SQL node options: HostName=172.31.38.31 # Hostname or IP address # (additional mysqld connections can be # specified for this node for various # purposes such as running ndb_restore) [mysqld] HostName=172.31.47.41 初回起動 (1) MySQLクラスターの初回起動 管理ノードから実行 # ndb_mgmd -f /var/lib/mysql-cluster/config.ini --reload ログイン # ndb_mgm ndb_mgm> show Connected to Management Server at: localhost:1186 Cluster Configuration --------------------- [ndbd(NDB)] 2 node(s) id=3 (not connected, accepting connect from 172.31.44.196) id=4 (not connected, accepting connect from 172.31.32.52) [ndb_mgmd(MGM)] 1 node(s) id=1 @172.31.41.195 (mysql-8.0.28 ndb-8.0.28) [mysqld(API)] 2 node(s) id=5 (not connected, accepting connect from 172.31.38.31) id=6 (not connected, accepting connect from 172.31.47.41) ndb_mgm> (2) NDBデーモンの起動 データノードでNDBデーモン起動。マネージャーを指定しないとERROR: Unable to connect with connect string: nodeid=0,localhost:1186になる。 # ndbd --ndb-mgmd-host=172.31.41.195 (3) SQLノードの初回起動 SQLノードで実行 # systemctl start mysqld (4) rootのパスワードを変更 パスワードを確認 # grep 'temporary password' /var/log/mysql/mysqld.log # mysql -uroot -p mysql> ALTER USER root@localhost IDENTIFIED WITH mysql_native_password BY 'P@ssw0rd'; (5)ノードが全て起動していることを確認する 監理ノードで # ndb_mgm ndb_mgm> show 以上。 あとがき 今年のアドカレは如何だったでしょうか。筆者記事はテキトーになりつつあるのに(もはやメモ)、他記事の皆様は年々クオリティが上がってきているような。。。 いいな!来年こそ!面白い記事を書きたい! 読んでくださりありがとうございました~? 参考 以下サイトを参考にさせて頂きました! https://blog.apar.jp/linux/6725/
- 投稿日:2022-01-31T13:59:41+09:00
SQLSTATE[HY000] [2002] Connection refused が出た時の対処方法
エラー内容 このような内容でした SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = laravel and table_name = migrations and table_type = 'BASE TABLE') at vendor/laravel/framework/src/Illuminate/Database/Connection.php:712 708▕ // If an exception occurs when attempting to run a query, we'll format the error 709▕ // message to include the bindings with SQL, which will make this exception a 710▕ // lot more helpful to the developer instead of just the database's errors. 711▕ catch (Exception $e) { ➜ 712▕ throw new QueryException( 713▕ $query, $this->prepareBindings($bindings), $e 714▕ ); 715▕ } 716▕ } +36 vendor frames 37 artisan:37 Illuminate\Foundation\Console\Kernel::handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) 対処法 php artisan tinkerで.envファイルの内容がちゃんと反映されてるか確かて見たところ、反映されていなかったためこれが原因だったようです。 root@8dae87ded68a:/var/www/app# php artisan tinker Psy Shell v0.11.1 (PHP 8.0.15 — cli) by Justin Hileman >>> config('database') => [ ../ "mysql" => [ "driver" => "mysql", "url" => null, "host" => "127.0.0.1", #要注意 "port" => "3306", "database" => "laravel",#要注意 "username" => "root",#要注意 "password" => "",#要注意 "unix_socket" => "", "charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci", "prefix" => "", "prefix_indexes" => true, "strict" => true, "engine" => null, "options" => [], ], ../ なので.envファイルを適用させればいいのですが、php artisan config:clearコマンドで内容を適用させればいらしいんですが、私の場合これではうまく適用されずrm app/.envでファイルを削除させもう一度復元させたら成功しました。 まとめ ファイルを消さないと適用されないところを治したい
- 投稿日:2022-01-31T08:28:14+09:00
Dockerでnginx + PHP(Laravel)+ MySQLのLEMP環境を構築する
概要 Docker, Docker Compose を使って、nginx + PHP(Laravel)+ MySQLのLEMP環境を構築する記事です。 検索するとDockerfileやdocker-compose.ymlの書き方については色んな方が記事を書いてくださっていて、動く環境を作ること自体は難しくありませんでした。 ですが、筆者はひとつコンテナを作っては動作確認していくという過程を経てすごく理解が深まったなと思うので、ファイルの書き方だけでなく動作確認したことやその結果を含めて記事に残しておきたいと思います。 全体像 最終的なディレクトリ構成は以下の通りです。 全コンテナを管理するdocker-compose.ymlがトップレベルにあり、同じくトップレベルにあるdockerディレクトリ配下に各コンテナのDockerfileや設定ファイルを置いています。 tree docker_sample/ ├── src // Laravelプロジェクトのソースコード ├── docker │ ├── app // PHPコンテナ │ │ ├── Dockerfile │ │ └── php.ini │ ├── db // MySQLコンテナ │ │ ├── Dockerfile │ │ └── my.cnf │ └── web // nginxコンテナ │ ├── Dockerfile │ └── default.conf └── docker-compose.yml // 全コンテナの管理 早速一つずつコンテナを作っていきます。 PHPのコンテナ(Laravelの開発環境) まずはLaravelの開発環境を構築するための、PHPのコンテナを作成します。 docker-compose.ymlへの記述 PHPコンテナ用のDockerfile PHPコンテナ用の設定ファイル(php.ini) について順に説明します。 docker-compose.yml(PHP) docker-compose.ymlのうち、PHPコンテナについての記述は以下の通りです。 ※docker-compose.ymlはインデントが意味を持つので注意。 docker-compose.yml version: "3.9" services: app: // サービス名 build: context: . dockerfile: ./docker/app/Dockerfile volumes: - ./src/:/app version: これはPHPコンテナについてではなく docker-compose.yml の先頭に書く設定ですが、Composeファイルのバージョンを表しています。 今回は最新の 3.9 を使います。(最新のバージョンは下記の公式サイトを参照) Compose file build: ビルドコンテキストを指定します。 context:ビルドコンテキスト(buildを実行する場所)の設定 dockerfile:buildするDockerfileまでのパス(docker-compose.ymlから見た相対パス) 参考記事: docker-compose.ymlのbuild設定はとりあえずcontextもdockerfileも埋めとけって話 - Qiita volumes: ホスト側の ./src/をコンテナ側の /app にマウントするという意味になります。 ※ここで services: の中に書いているこの volumes: は バインドマウント を行っています。 参考記事:Dockerのマウント3種類についてわかったことをまとめる - Qiita 何が起きているかは実際の挙動を見てみた方が分かりやすいと思うので、後ほどまた動作確認します。 Dockerfile(PHP) Dockerfileはテキストファイルであり、Dockerイメージを作り上げるために実行する命令をこのファイルに書きます。 まず作成するDockerfileの全文がこちらです。 /docker/app/Dockerfile FROM php:8.0-fpm ENV TZ Asia/Tokyo RUN apt-get update && \ apt-get install -y git unzip libzip-dev libicu-dev libonig-dev && \ docker-php-ext-install intl pdo_mysql zip bcmath COPY ./docker/app/php.ini /usr/local/etc/php/php.ini COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer WORKDIR /app 一つずつ説明します。 FROM php:8.0-fpm FROMではイメージをビルドするためのベースイメージを設定します。 書き方は FROM イメージ名:タグ名 です。 ここではDocker HubからPHP公式のイメージをベースとして指定しています。 Php - Official Image | Docker Hub ENV TZ Asia/Tokyo ENVはコンテナ内のサーバー環境変数を設定します。 RUN apt-get update && \ apt-get install -y git unzip libzip-dev libicu-dev libonig-dev && \ docker-php-ext-install intl pdo_mysql zip bcmath RUN にはコンテナビルド時に実行するコマンドを書きます。 && で複数のコマンドをつなぎ、 \ で改行します。 ※ && と \ を使うことで複数コマンドを1レイヤーにまとめることができ、公式でもベストプラクティスのTipsとして挙げられています。 Intro Guide to Dockerfile Best Practices - Docker Blog apt-get update :インストール可能なパッケージの「一覧」を更新 apt-get -y install:パッケージをインストール docker-php-ext-install:PHPの拡張ライブラリをインストール ※Laravelのインストールや開発に必要な(もしくは便利な)パッケージや拡張ライブラリをインストールしています。 ※試しにこれらを全くインストールせず手順を進めてみると、コンテナを起動することはできますが、その後のLaravelプロジェクトを作成する時に大量のエラーに遭遇しました(笑) COPY ./docker/app/php.ini /usr/local/etc/php/php.ini ローカルで(後ほど)作成する php.ini (PHPの設定ファイル)をDockerコンテナ内にコピーします。 書き方は COPY [ローカル側のパス] [コンテナ側のパス]です。 ※ローカル側のパスは、Dockerfileから見てではなくbuildコマンドを実行するディレクトリから見た相対パスです。今回はDocker composeを使ってイメージビルドを行うので、docker-compose.ymlから見た相対パスになっています。 COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer Laravelを使うためComposerをインストールします。 この書き方によりマルチステージビルドという方法でインストールされます。 ※マルチステージビルドという方法はイメージの軽量化に役立つようです。 公式ドキュメント:Use multi-stage builds WORKDIR /app コンテナを起動している時に $ docker-compose exec コンテナ名 bash というコマンドを実行すると、コンテナの中でbashを実行することができるのですが、 WORKDIRはその時のカレントディレクトリを指定しています。 設定ファイル(php.ini) COPY ./docker/app/php.ini /usr/local/etc/php/php.ini で出てきたPHPの設定ファイル php.iniを作成します。 作成したファイルの中身がこちらです。 /docker//app/php.ini zend.exception_ignore_args = off expose_php = on max_execution_time = 30 max_input_vars = 1000 upload_max_filesize = 64M post_max_size = 128M memory_limit = 256M error_reporting = E_ALL display_errors = on display_startup_errors = on log_errors = on error_log = /var/log/php/php-error.log default_charset = UTF-8 [Date] date.timezone = Asia/Tokyo [mysqlnd] mysqlnd.collect_memory_statistics = on [Assertion] zend.assertions = 1 [mbstring] mbstring.language = Japanese こちらの記事の開発用php.iniをまるっとお借りしました。 開発用 / 本番用の設定例と、項目ごとの説明も載っていて面白かったので、ぜひご覧ください。 参考記事:PHP7.4 ぼくのかんがえたさいきょうのphp.ini - Qiita PHPコンテナを起動する ここまででPHPのコンテナの準備が出来たので、実際に起動してみます。 docker-sompose.ymlのあるディレクトリで以下のコマンドを実行します。 $ docker-compose up -d --build docker compose up は docker-compose.yml に定義したサービスを起動します。 -d 「デタッチド」モードでコンテナを起動します。 (デフォルトは「アタッチド」モードで全てのコンテナログを画面上に表示する。「デタッチド」モードではバックグラウンドで動作する。) -build コンテナの開始前にイメージをビルドします。 (特に変更がない場合はキャッシュが使用される。) ※ $ docker-compose build → $ docker-compose up -d を順に行うのと同じです。 起動したコンテナを確認する 以下のコマンドで起動中のコンテナを一覧で確認することができます。 $ docker-compose ps NAME COMMAND SERVICE STATUS PORTS docker_sample-app-1 "docker-php-entrypoi…" app running 9000/tcp STATUSがrunningになっていれば正常に起動しています。 今はPHPのコンテナしか作っていないので1つだけ表示されていますが、この後nginx, MySQLのコンテナを作り、最終的に3つのコンテナが表示されるようにします。 ここで今作業しているディレクトリの構成を確認してみます。 $ tree . ├── src // 作成された! ├── docker │ ├── app │ │ ├── Dockerfile │ │ └── php.ini └── docker-compose.yml するとdocker-compose.ymlに書いたバインドマウントの以下の部分を受けて、コンテナを起動したときに自動でsrcディレクトリが作成されたことが確認できます。(同様にコンテナ内にもappディレクトリが作成されています。) volumes: - ./src/:/app コンテナの中に入ってみる コンテナを起動中に以下のコマンドを実行すると、コンテナの中に入ってbashを実行することができます。 $ docker-compose exec app bash // appの部分はサービス名を指定する これで今コンテナの中に入れたので、Dockerfileの記述通りにコンテナが作られているか、またPHP・Composer・インストールした拡張機能が使えるか確認していきます。 // Dockerfileの「WORKDIR /app」で指定したカレントディレクトリ通りか確認 [app]:/app$ pwd /app // PHPのバージョン確認 [app]:/app$ php -v PHP 8.0.15 (cli) (built: Jan 26 2022 17:38:36) ( NTS ) Copyright (c) The PHP Group Zend Engine v4.0.15, Copyright (c) Zend Technologies // Composerのバージョン確認 [app]:/app$ composer -v ______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ Composer version 2.0.14 2021-05-21 17:03:37 // gitのバージョン確認 [app]:/app$ git --version git version 2.30.2 // インストール済の拡張機能の一覧 [app]:/app$ php -m [PHP Modules] bcmath intl pdo_mysql zip // たくさん出てくるので他は省略 // php.iniがコピー出来ているか確認 [app]:/app$ cat /usr/local/etc/php/php.ini zend.exception_ignore_args = off expose_php = on max_execution_time = 30 max_input_vars = 1000 upload_max_filesize = 64M post_max_size = 128M memory_limit = 256M error_reporting = E_ALL display_errors = on display_startup_errors = on log_errors = on error_log = /var/log/php/php-error.log default_charset = UTF-8 [Date] date.timezone = Asia/Tokyo [mysqlnd] mysqlnd.collect_memory_statistics = on [Assertion] zend.assertions = 1 [mbstring] mbstring.language = Japaneseroot@0e3ba825df88 確認できたのでコンテナを抜けます。 $ exit // もしくは ctrl + d コンテナの外からコマンドを実行する 上記のコンテナの中で実行したコマンドは、 $ docker-compose exec サービス名 実行したいコマンド でコンテナの外から実行することもできます。(コンテナが起動中に限る) バインドマウントの挙動を確認する コンテナの外からコマンドが実行できるという確認も兼ねて、バインドマウントの動きを見る為、試しに以下のコマンドを実行してみます。 // コンテナ内の/app配下にファイルを作ってみる $ docker-compose exec app touch sample.php $ docker-compose exec app pwd /app $ docker-compose exec app ls sample.php コンテナ内の/app配下にファイルを作成しました。 この/appという場所は、docker-compose.ymlに書いた以下の記述の通りにバインドマウントされています。 volumes: - ./src/:/app これにより、プロジェクトディレクトリ配下の/srcにもsample.phpが作成されています。 $ ls src sample.php 以上より、コンテナ内の /app に対して行ったことが、プロジェクトの /src に反映していることが分かりました。 では次に /src への変更がコンテナ内の /app に対しても反映するか試してみます。 /src/sample.php をエディタで開いてファイルに hello と書きこみ、以下のコマンドを実行します。 $ docker-compose exec app cat sample.php hello // この時permission errorが出たら権限を変更する $ sudo chmod -R 777 ./src するとコンテナ内の ./app/sample.php も編集されていることがわかります。 このようにバインドマウントにより、ホスト側のディレクトリがコンテナ内へマウント出来ていることも確認できました。 試しに作成したファイルは不要なので消しておきます。 $ rm src/sample.php nginxのコンテナ(webサーバー) webサーバーとなるnginxのコンテナを作成します。 PHPコンテナのときと同じく、 docker-compose.ymlへの記述 nginxコンテナ用のDockerfile nginxコンテナ用の設定ファイル(default.conf) の流れで説明します。 docker-compose.yml(nginx) 既に作成しているdocker-compose.ymlに、nginxのコンテナについての部分を追記します。 docker-compose.yml services: api: // 中略 web: // サービス名 build: context: . dockerfile: ./docker/web/Dockerfile ports: - 8081:80 depends_on: - app volumes: - ./src/:/app 解説していきます。 build: ・volumes: ここはPHPコンテナと同様なので説明は省きます。 ports: ホスト側とコンテナ間のポート番号の対応付けを設定します。 書き方は ホスト側のポート番号 : コンテナのポート番号です。 ※今回ホスト側(自分のPC)は既に他の開発で使っているポートとの兼ね合いで 8081 を使いました。コンテナ側はnginxのデフォルトのポート番号である 80 にしています。 depends_on: サービスの起動順序を制御します。 web の depends_on に app と書いているので、 app → web の順に起動するように指定しています。 ※但しこの記述なしで $ docker-compose up -d --build をしてみても私の環境では全く問題なく動きました。が、サービス同士の依存関係を明示的に記すという意味でも書いておくに越したことはないという判断で書いています。 ※ nginxとphp間でTCPによるfpm接続についてや、depends_on オプションについては以下の記事がすごく勉強になったので是非読んでみてください。 参考記事:【docker-compose】depends_onとサービス名解決にまつわるエトセトラ - Qiita Dockerfile(nginx) 作成するDockerfileの全文はこちらです。 /docker/web/Dockerfile FROM nginx:1.20-alpine ENV TZ Asia/Tokyo COPY ./docker/web/default.conf /etc/nginx/conf.d/default.conf FROM , ENV , COPY の意味はPHPコンテナのDockerfileと同様なので詳しい説明は省略し、このファイルで設定していることを簡単にまとめます。 nginx公式のイメージ(Alpineベース)をベースイメージに使用 Nginx - Official Image | Docker Hub 参考記事:Dockerでよく利用されているAlpineは他のLinuxディストリビューションと比べて、どれだけ軽量なのか - プログラミングは芸術だ! 環境変数のタイムゾーンを設定する nginxの設定ファイル(default.conf)をコンテナ内にバインドマウント ※ちなみにnginxは1.18, 1.20などの偶数バージョンが安定バージョンであり、安定バージョンの使用を推奨されています。 公式ドキュメント:Installing NGINX Open Source 設定ファイル(default.conf) ./docker/web/default.confを作成します。 Laravel公式に用意されているnginxの設定例をべースに使います。 Laravel 8.x デプロイ root とfastcgi_passの設定のみ、このプロジェクトに合わせて書き換えています。 /docker/web/default.conf server { listen 80; server_name example.com; root /app/public; // 書き換え add_header X-Frame-Options "SAMEORIGIN"; add_header X-XSS-Protection "1; mode=block"; add_header X-Content-Type-Options "nosniff"; index index.php; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } error_page 404 /index.php; location ~ \.php$ { fastcgi_pass app:9000; // 書き換え fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.(?!well-known).* { deny all; } } root リクエストのルートディレクトリです。 root /app/public; と書いたので、 localhost:8081(docker-compose.ymlで設定したポート番号)にアクセスすると /app/publicを見に行きます。 fastcgi_pass FastCGIサーバーのアドレスです。 ※FastCGI:Webサーバ上で動くプログラムを一度起動したらしばらく待機させることによって、プログラムの開始と終了にかかる手間を減らし、動きを速くしたりWebサーバの負荷を軽減することができる仕組み。 app:9000; と書いたので、appコンテナの9000番ポートを指定しています。 nginxコンテナを起動する nginxのコンテナの準備が出来たので、また実際に起動してみます。 docker-sompose.ymlのあるディレクトリで以下のコマンドを実行します。 $ docker-compose up -d --build $ docker-compose ps NAME COMMAND SERVICE STATUS PORTS docker_sample-app-1 "docker-php-entrypoi…" app running 9000/tcp docker_sample-web-1 "/docker-entrypoint.…" web running 0.0.0.0:8081->80/tcp api(PHPのコンテナ)、web(nginxのコンテナ)の2つが起動できました。 またdocker_sample-web-1の PORTS が 0.0.0.0:8081->80/tcp となっており、ホスト上の8081番ポートをコンテナの80番ポートへ割り当てられていることも確認できます。 コンテナの動作確認 nginxのバージョンを確認します。 $ docker-compose exec web nginx -v nginx version: nginx/1.20.2 コンテナの中に入ってみます。 $ docker-compose exec web bash OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "bash": executable file not found in $PATH: unknown するとエラーになりました。 Alpineをベースとすると bash は使えず、 ash や sh は使えるようです。 $ docker-compose exec web ash [web]:/ $ pwd / 無事コンテナの中に入れました。 webサーバーとしての動作確認 リクエストに対してファイルを返しブラウザで表示できる nginxのコンテナからPHPのコンテナへphpを実行させることができる 上記2点を確認します。 $ mkdir src/public $ touch src/public/test.php test.phpを以下のように編集します。 /src/public/test.php <?php echo 'test.phpです'; phpinfo(); http://localhost:8081/test.php にアクセスすると以下のように表示され、webサーバーが正しく動作していることが確認できます。 試しに作成したファイルは不要なので消しておきます。 $ rm -rf src/* MySQLのコンテナ(データベース) データベースのMySQLコンテナを作成します。 以下の流れで説明します。 docker-compose.ymlへの記述 MySQLコンテナ用のDockerfile MySQLコンテナ用の設定ファイル(my.conf) docker-compose.yml(MySQL) 既に作成しているdocker-compose.ymlに、MySQLのコンテナについての部分を追記します。 docker-compose.yml services: // 中略 db: // サービス名 build: context: . dockerfile: ./docker/db/Dockerfile ports: - 3306:3306 environment: MYSQL_DATABASE: database MYSQL_USER: user MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: password TZ: 'Asia/Tokyo' volumes: - mysql-volume:/var/lib/mysql volumes: mysql-volume: build: ・ ports: 既出の通りです。 enviroment: 環境変数の設定です。 ※名前やDB名・ユーザー名・パスワードは好きなものを設定します。 ※実際のプロジェクト管理では、環境変数は .env に書いて .gitignore にするなどして、重要な情報が公開されないようにします。 MYSQL_DATABASE:DB名 MYSQL_USER:ユーザー名 MYSQL_PASSWORD:パスワード MYSQL_ROOT_PASSWORD:ルート権限のパスワード TZ:時間設定(Time Zone) volumes: 考え方はPHPやnginxのコンテナ同様ですが、先ほどまではホスト側のディレクトリを書いていたところに mysql-volume と書いています。 これにより mysql-colume という名前で作成した名前付きボリュームとコンテナ内を紐づけています。 Docker Volume Volumeとは、コンテナを破棄してもデータを永続的に保存できるように、コンテナ外に提供されているデータの保存領域です。 Dockerの管理下でホスト上にストレージ領域を確保しており、Linux なら /var/lib/docker/volumes/以下にあります。 参考記事:Docker、ボリューム(Volume)について真面目に調べた - Qiita なぜVolumeが必要なのか? コンテナが起動している間はDBのレコードは保存された状態が続きますが、例えば以下のようなコマンドでコンテナを新しく作り直したとするとデータベースの情報はゼロに戻ってしまいます。 // コンテナ削除 $ docker-compose down // コンテナ起動 $ docker-compose up -d これでは困るのでコンテナを破棄してもデータを残したい、、、というときにVolumesを使ってデータの永続化を行います。 ※ボリュームには名前付きボリュームと匿名ボリュームがありますが、通常は管理しやすい名前付きボリュームを使うと良いかと思います。 名前付きボリュームを作成する場合は、データの永続化対象のコンテナに対して volumes: オプションでバインドマウントを行うだけでなく、以下のようにdocker-compose.ymlのトップレベルでボリューム名を定義します。 docker-compose.yml volumes: - mysql-volume: Dockerfile(MySQL) 作成するDockerfileの全文はこちらです。 /docker/db/Dockerfile FROM mysql:8.0 COPY ./docker/db/my.cnf /etc/my.cnf 内容を簡単にまとめると、 公式のMySQLイメージをベースイメージに使用 Mysql - Official Image | Docker Hub MySQLの設定ファイル(my.cnf)をコンテナ内にバインドマウント ※ここで使用しているイメージではM1 Macでは動作しないという情報が見られました。どうやらOracleのMySQLチームがメンテしている mysql/mysql-server のイメージだと動作するようです。 mysql-server | Docker Hub 設定ファイル(my.conf) ./dicker/db/my.cnfを作成します。 色んな記事で作成されているmy.cnfを参考にさせていただきながら書きました。 参考記事:【Docker】docker-composeでmysqlのコンテナを立てる 文字コード タイムゾーン ログ の設定を行っています。 /docker/db/my.conf [mysqld] # character character_set_server = utf8mb4 collation_server = utf8mb4_0900_ai_ci # timezone default-time-zone = SYSTEM log_timestamps = SYSTEM # Error Log log-error = mysql-error.log # Slow Query Log slow_query_log = 1 slow_query_log_file = mysql-slow.log long_query_time = 1.0 log_queries_not_using_indexes = 0 # General Log general_log = 1 general_log_file = mysql-general.log [mysql] default-character-set = utf8mb4 [client] default-character-set = utf8mb4 MySQLコンテナを起動する MySQLのコンテナの準備が出来たので、また実際に起動してみます。 docker-sompose.ymlのあるディレクトリで以下のコマンドを実行します。 $ docker-compose up -d --build $ docker-compose ps NAME COMMAND SERVICE STATUS PORTS docker_sample-app-1 "docker-php-entrypoi…" app running 9000/tcp docker_sample-db-1 "docker-entrypoint.s…" db running 0.0.0.0:3306->3306/tcp docker_sample-web-1 "/docker-entrypoint.…" web running 0.0.0.0:8081->80/tcp api(PHPのコンテナ)、web(nginxのコンテナ)、db(MySQLのコンテナ)の3つが起動できました。 コンテナの動作確認 MySQLのバージョンを確認します。 $ docker compose exec db mysql -V mysql Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL) ※この後Laravelのプロジェクトを作成したら、このDBとLaravelを接続します。 そしてマイグレーションを行ってDBにテーブルを作成してから、MySQLにログインしてDBを使う動作確認をしたいと思います。 Laravelのインストール Laravelプロジェクトの作成 LEMP環境が構築できたので、Laravelのアプリケーションを作っていきます。 appコンテナに入り、Laravelをインストールします。 $ docker compose exec app bash [app]:/app$ composer create-project --prefer-dist "laravel/laravel=8.*" . [app]:/app$ php artisan -v Laravel Framework 8.81.0 コンテナ内の/app配下にLaravelのプロジェクトが新規作成され、ホスト側の/src配下にも同じくLaravelのプロジェクトが出来ました。 localhost:8081にアクセスして、ブラウザでもLaravelのウェルカムページが表示できることを確認します。 DB接続 appコンテナ(Laravel)からdbコンテナ(MySQL)へ接続する設定を行います。 Laravelではデータベースへの接続設定を .env ファイルに定義しているので、 /src/.env のDBの部分を以下のように修正します。 /src/.env DB_CONNECTION=mysql DB_HOST=db // MySQLコンテナのサービス名 DB_PORT=3306 DB_DATABASE=database DB_USERNAME=root DB_PASSWORD=passwor DB_HOST にはMySQLコンテナのサービス名を指定します。 その他の項目もMySQLコンテナで設定した値(今回はdocker-compose.ymlのenviromentで定義)と同じ値を指定します。 DBに接続出来ているか確認する為、以下のコマンドを実行してマイグレーションを行います。 $ docker compose exec app bash [app]:/app$ php artisan migrate Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (55.42ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (51.11ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (45.12ms) Migrating: 2019_12_14_000001_create_personal_access_tokens_table Migrated: 2019_12_14_000001_create_personal_access_tokens_table (75.74ms) MySQLを使ってみる MySQLのコンテナに入ってDBを確認してみます。 $ docker-compose exec db bash [db]:/$ mysql -u root -p // パスワードを求められるので入力 [db] mysql> use database; [db] mysql> show tables; +------------------------+ | Tables_in_database | +------------------------+ | failed_jobs | | migrations | | password_resets | | personal_access_tokens | | users | +------------------------+ 5 rows in set (0.00 sec) 先ほどマイグレーションを実行したので、Laravelのデフォルトで用意されているマイグレーションファイル通りにテーブルが作成されていることが確認できました。 最後に これでDocker(Docker Compose)を使ったLEMP環境の構築が完了です。 今回作成した環境は非常にシンプルなものなので、業務で使うとなるともっと設定を細やかに行ったり、開発環境・ステージング環境・本番環境それぞれの設定ファイルを用意して設定を切り替えたりといった作業が必要になるかと思いますが、仕組みが分かればあとは全てこの延長にあるのかなと思います。 またいずれ環境ごとの設定ファイル切り替えだったり、あとはNodeのコンテナを使ってNext.jsの環境構築もやってみたいです。 参考記事
- 投稿日:2022-01-31T08:28:14+09:00
DockerでPHP(Laravel)+ nginx + MySQLのLEMP環境を構築する
概要 Docker, Docker Compose を使って、PHP(Laravel) + nginx + MySQLのLEMP環境を構築する記事です。 検索するとDockerfileやdocker-compose.ymlの書き方については色んな方が記事を書いてくださっていて、動く環境を作ること自体は難しくありませんでした。 ですが、筆者はひとつコンテナを作っては動作確認していくという過程を経てすごく理解が深まったなと思うので、ファイルの書き方だけでなく動作確認したことやその結果を含めて記事に残しておきたいと思います。 ↓ 完成後のリポジトリはこちらです。 全体像 最終的なディレクトリ構成は以下の通りです。 全コンテナを管理するdocker-compose.ymlがトップレベルにあり、同じくトップレベルにあるdockerディレクトリ配下に各コンテナのDockerfileや設定ファイルを置いています。 tree docker_sample/ ├── src // Laravelプロジェクトのソースコード ├── docker │ ├── app // PHPコンテナ │ │ ├── Dockerfile │ │ └── php.ini │ ├── db // MySQLコンテナ │ │ ├── Dockerfile │ │ └── my.cnf │ └── web // nginxコンテナ │ ├── Dockerfile │ └── default.conf └── docker-compose.yml // 全コンテナの管理 早速一つずつコンテナを作っていきます。 PHPのコンテナ(Laravelの開発環境) まずはLaravelの開発環境を構築するための、PHPのコンテナを作成します。 docker-compose.ymlへの記述 PHPコンテナ用のDockerfile PHPコンテナ用の設定ファイル(php.ini) について順に説明します。 docker-compose.yml(PHP) docker-compose.ymlのうち、PHPコンテナについての記述は以下の通りです。 ※docker-compose.ymlはインデントが意味を持つので注意。 docker-compose.yml version: "3.9" services: app: // サービス名 build: context: . dockerfile: ./docker/app/Dockerfile volumes: - ./src/:/app version: これはPHPコンテナについてではなく docker-compose.yml の先頭に書く設定ですが、Composeファイルのバージョンを表しています。 今回は最新の 3.9 を使います。(最新のバージョンは下記の公式サイトを参照) Compose file build: ビルドコンテキストを指定します。 context:ビルドコンテキスト(buildを実行する場所)の設定 dockerfile:buildするDockerfileまでのパス(docker-compose.ymlから見た相対パス) 参考記事: docker-compose.ymlのbuild設定はとりあえずcontextもdockerfileも埋めとけって話 - Qiita volumes: ホスト側の ./src/をコンテナ側の /app にマウントするという意味になります。 ※ここで services: の中に書いているこの volumes: は バインドマウント を行っています。 参考記事:Dockerのマウント3種類についてわかったことをまとめる - Qiita 何が起きているかは実際の挙動を見てみた方が分かりやすいと思うので、後ほどまた動作確認します。 Dockerfile(PHP) Dockerfileはテキストファイルであり、Dockerイメージを作り上げるために実行する命令をこのファイルに書きます。 まず作成するDockerfileの全文がこちらです。 /docker/app/Dockerfile FROM php:8.0-fpm ENV TZ Asia/Tokyo RUN apt-get update && \ apt-get install -y git unzip libzip-dev libicu-dev libonig-dev && \ docker-php-ext-install intl pdo_mysql zip bcmath COPY ./docker/app/php.ini /usr/local/etc/php/php.ini COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer WORKDIR /app 一つずつ説明します。 FROM php:8.0-fpm FROMではイメージをビルドするためのベースイメージを設定します。 書き方は FROM イメージ名:タグ名 です。 ここではDocker HubからPHP公式のイメージをベースとして指定しています。 Php - Official Image | Docker Hub ENV TZ Asia/Tokyo ENVはコンテナ内のサーバー環境変数を設定します。 RUN apt-get update && \ apt-get install -y git unzip libzip-dev libicu-dev libonig-dev && \ docker-php-ext-install intl pdo_mysql zip bcmath RUN にはコンテナビルド時に実行するコマンドを書きます。 && で複数のコマンドをつなぎ、 \ で改行します。 ※ && と \ を使うことで複数コマンドを1レイヤーにまとめることができ、公式でもベストプラクティスのTipsとして挙げられています。 Intro Guide to Dockerfile Best Practices - Docker Blog apt-get update :インストール可能なパッケージの「一覧」を更新 apt-get -y install:パッケージをインストール docker-php-ext-install:PHPの拡張ライブラリをインストール ※Laravelのインストールや開発に必要な(もしくは便利な)パッケージや拡張ライブラリをインストールしています。 ※試しにこれらを全くインストールせず手順を進めてみると、コンテナを起動することはできますが、その後のLaravelプロジェクトを作成する時に大量のエラーに遭遇しました(笑) COPY ./docker/app/php.ini /usr/local/etc/php/php.ini ローカルで(後ほど)作成する php.ini (PHPの設定ファイル)をDockerコンテナ内にコピーします。 書き方は COPY [ローカル側のパス] [コンテナ側のパス]です。 ※ローカル側のパスは、Dockerfileから見てではなくbuildコマンドを実行するディレクトリから見た相対パスです。今回はDocker composeを使ってイメージビルドを行うので、docker-compose.ymlから見た相対パスになっています。 COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer Laravelを使うためComposerをインストールします。 この書き方によりマルチステージビルドという方法でインストールされます。 ※マルチステージビルドという方法はイメージの軽量化に役立つようです。 公式ドキュメント:Use multi-stage builds WORKDIR /app コンテナを起動している時に $ docker-compose exec コンテナ名 bash というコマンドを実行すると、コンテナの中でbashを実行することができるのですが、 WORKDIRはその時のカレントディレクトリを指定しています。 設定ファイル(php.ini) COPY ./docker/app/php.ini /usr/local/etc/php/php.ini で出てきたPHPの設定ファイル php.iniを作成します。 作成したファイルの中身がこちらです。 /docker//app/php.ini zend.exception_ignore_args = off expose_php = on max_execution_time = 30 max_input_vars = 1000 upload_max_filesize = 64M post_max_size = 128M memory_limit = 256M error_reporting = E_ALL display_errors = on display_startup_errors = on log_errors = on error_log = /var/log/php/php-error.log default_charset = UTF-8 [Date] date.timezone = Asia/Tokyo [mysqlnd] mysqlnd.collect_memory_statistics = on [Assertion] zend.assertions = 1 [mbstring] mbstring.language = Japanese こちらの記事の開発用php.iniをまるっとお借りしました。 開発用 / 本番用の設定例と、項目ごとの説明も載っていて面白かったので、ぜひご覧ください。 参考記事:PHP7.4 ぼくのかんがえたさいきょうのphp.ini - Qiita PHPコンテナを起動する ここまででPHPのコンテナの準備が出来たので、実際に起動してみます。 docker-sompose.ymlのあるディレクトリで以下のコマンドを実行します。 $ docker-compose up -d --build docker compose up は docker-compose.yml に定義したサービスを起動します。 -d 「デタッチド」モードでコンテナを起動します。 (デフォルトは「アタッチド」モードで全てのコンテナログを画面上に表示する。「デタッチド」モードではバックグラウンドで動作する。) --build コンテナの開始前にイメージをビルドします。 (特に変更がない場合はキャッシュが使用される。) ※ $ docker-compose build → $ docker-compose up -d を順に行うのと同じです。 起動したコンテナを確認する 以下のコマンドで起動中のコンテナを一覧で確認することができます。 $ docker-compose ps NAME COMMAND SERVICE STATUS PORTS docker_sample-app-1 "docker-php-entrypoi…" app running 9000/tcp STATUSがrunningになっていれば正常に起動しています。 今はPHPのコンテナしか作っていないので1つだけ表示されていますが、この後nginx, MySQLのコンテナを作り、最終的に3つのコンテナが表示されるようにします。 ここで今作業しているディレクトリの構成を確認してみます。 $ tree . ├── src // 作成された! ├── docker │ ├── app │ │ ├── Dockerfile │ │ └── php.ini └── docker-compose.yml するとdocker-compose.ymlに書いたバインドマウントの以下の部分を受けて、コンテナを起動したときに自動でsrcディレクトリが作成されたことが確認できます。(同様にコンテナ内にもappディレクトリが作成されています。) volumes: - ./src/:/app コンテナの中に入ってみる コンテナを起動中に以下のコマンドを実行すると、コンテナの中に入ってbashを実行することができます。 $ docker-compose exec app bash // appの部分はサービス名を指定する これで今コンテナの中に入れたので、Dockerfileの記述通りにコンテナが作られているか、またPHP・Composer・インストールした拡張機能が使えるか確認していきます。 // Dockerfileの「WORKDIR /app」で指定したカレントディレクトリ通りか確認 [app]:/app$ pwd /app // PHPのバージョン確認 [app]:/app$ php -v PHP 8.0.15 (cli) (built: Jan 26 2022 17:38:36) ( NTS ) Copyright (c) The PHP Group Zend Engine v4.0.15, Copyright (c) Zend Technologies // Composerのバージョン確認 [app]:/app$ composer -v ______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ Composer version 2.0.14 2021-05-21 17:03:37 // gitのバージョン確認 [app]:/app$ git --version git version 2.30.2 // インストール済の拡張機能の一覧 [app]:/app$ php -m [PHP Modules] bcmath intl pdo_mysql zip // たくさん出てくるので他は省略 // php.iniがコピー出来ているか確認 [app]:/app$ cat /usr/local/etc/php/php.ini zend.exception_ignore_args = off expose_php = on max_execution_time = 30 max_input_vars = 1000 upload_max_filesize = 64M post_max_size = 128M memory_limit = 256M error_reporting = E_ALL display_errors = on display_startup_errors = on log_errors = on error_log = /var/log/php/php-error.log default_charset = UTF-8 [Date] date.timezone = Asia/Tokyo [mysqlnd] mysqlnd.collect_memory_statistics = on [Assertion] zend.assertions = 1 [mbstring] mbstring.language = Japaneseroot@0e3ba825df88 確認できたのでコンテナを抜けます。 $ exit // もしくは ctrl + d コンテナの外からコマンドを実行する 上記のコンテナの中で実行したコマンドは、 $ docker-compose exec サービス名 実行したいコマンド でコンテナの外から実行することもできます。(コンテナが起動中に限る) バインドマウントの挙動を確認する コンテナの外からコマンドが実行できるという確認も兼ねて、バインドマウントの動きを見る為、試しに以下のコマンドを実行してみます。 // コンテナ内の/app配下にファイルを作ってみる $ docker-compose exec app touch sample.php $ docker-compose exec app pwd /app $ docker-compose exec app ls sample.php コンテナ内の/app配下にファイルを作成しました。 この/appという場所は、docker-compose.ymlに書いた以下の記述の通りにバインドマウントされています。 volumes: - ./src/:/app これにより、プロジェクトディレクトリ配下の/srcにもsample.phpが作成されています。 $ ls src sample.php 以上より、コンテナ内の /app に対して行ったことが、プロジェクトの /src に反映していることが分かりました。 では次に /src への変更がコンテナ内の /app に対しても反映するか試してみます。 /src/sample.php をエディタで開いてファイルに hello と書きこみ、以下のコマンドを実行します。 $ docker-compose exec app cat sample.php hello // この時permission errorが出たら権限を変更する $ sudo chmod -R 777 ./src するとコンテナ内の ./app/sample.php も編集されていることがわかります。 このようにバインドマウントにより、ホスト側のディレクトリがコンテナ内へマウント出来ていることも確認できました。 試しに作成したファイルは不要なので消しておきます。 $ rm src/sample.php nginxのコンテナ(webサーバー) webサーバーとなるnginxのコンテナを作成します。 PHPコンテナのときと同じく、 docker-compose.ymlへの記述 nginxコンテナ用のDockerfile nginxコンテナ用の設定ファイル(default.conf) の流れで説明します。 docker-compose.yml(nginx) 既に作成しているdocker-compose.ymlに、nginxのコンテナについての部分を追記します。 docker-compose.yml services: api: // 中略 web: // サービス名 build: context: . dockerfile: ./docker/web/Dockerfile ports: - 8081:80 depends_on: - app volumes: - ./src/:/app 解説していきます。 build: ・volumes: ここはPHPコンテナと同様なので説明は省きます。 ports: ホスト側とコンテナ間のポート番号の対応付けを設定します。 書き方は ホスト側のポート番号 : コンテナのポート番号です。 ※今回ホスト側(自分のPC)は既に他の開発で使っているポートとの兼ね合いで 8081 を使いました。コンテナ側はnginxのデフォルトのポート番号である 80 にしています。 depends_on: サービスの起動順序を制御します。 web の depends_on に app と書いているので、 app → web の順に起動するように指定しています。 ※但しこの記述なしで $ docker-compose up -d --build をしてみても私の環境では全く問題なく動きました。が、サービス同士の依存関係を明示的に記すという意味でも書いておくに越したことはないという判断で書いています。 ※ nginxとphp間でTCPによるfpm接続についてや、depends_on オプションについては以下の記事がすごく勉強になったので是非読んでみてください。 参考記事:【docker-compose】depends_onとサービス名解決にまつわるエトセトラ - Qiita Dockerfile(nginx) 作成するDockerfileの全文はこちらです。 /docker/web/Dockerfile FROM nginx:1.20-alpine ENV TZ Asia/Tokyo COPY ./docker/web/default.conf /etc/nginx/conf.d/default.conf FROM , ENV , COPY の意味はPHPコンテナのDockerfileと同様なので詳しい説明は省略し、このファイルで設定していることを簡単にまとめます。 nginx公式のイメージ(Alpineベース)をベースイメージに使用 Nginx - Official Image | Docker Hub 参考記事:Dockerでよく利用されているAlpineは他のLinuxディストリビューションと比べて、どれだけ軽量なのか - プログラミングは芸術だ! 環境変数のタイムゾーンを設定する nginxの設定ファイル(default.conf)をコンテナ内にバインドマウント ※ちなみにnginxは1.18, 1.20などの偶数バージョンが安定バージョンであり、安定バージョンの使用を推奨されています。 公式ドキュメント:Installing NGINX Open Source 設定ファイル(default.conf) ./docker/web/default.confを作成します。 Laravel公式に用意されているnginxの設定例をべースに使います。 Laravel 8.x デプロイ root とfastcgi_passの設定のみ、このプロジェクトに合わせて書き換えています。 /docker/web/default.conf server { listen 80; server_name example.com; root /app/public; // 書き換え add_header X-Frame-Options "SAMEORIGIN"; add_header X-XSS-Protection "1; mode=block"; add_header X-Content-Type-Options "nosniff"; index index.php; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } error_page 404 /index.php; location ~ \.php$ { fastcgi_pass app:9000; // 書き換え fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.(?!well-known).* { deny all; } } root リクエストのルートディレクトリです。 root /app/public; と書いたので、 localhost:8081(docker-compose.ymlで設定したポート番号)にアクセスすると /app/publicを見に行きます。 fastcgi_pass FastCGIサーバーのアドレスです。 ※FastCGI:Webサーバ上で動くプログラムを一度起動したらしばらく待機させることによって、プログラムの開始と終了にかかる手間を減らし、動きを速くしたりWebサーバの負荷を軽減することができる仕組み。 app:9000; と書いたので、appコンテナの9000番ポートを指定しています。 nginxコンテナを起動する nginxのコンテナの準備が出来たので、また実際に起動してみます。 docker-sompose.ymlのあるディレクトリで以下のコマンドを実行します。 $ docker-compose up -d --build $ docker-compose ps NAME COMMAND SERVICE STATUS PORTS docker_sample-app-1 "docker-php-entrypoi…" app running 9000/tcp docker_sample-web-1 "/docker-entrypoint.…" web running 0.0.0.0:8081->80/tcp api(PHPのコンテナ)、web(nginxのコンテナ)の2つが起動できました。 またdocker_sample-web-1の PORTS が 0.0.0.0:8081->80/tcp となっており、ホスト上の8081番ポートをコンテナの80番ポートへ割り当てられていることも確認できます。 コンテナの動作確認 nginxのバージョンを確認します。 $ docker-compose exec web nginx -v nginx version: nginx/1.20.2 コンテナの中に入ってみます。 $ docker-compose exec web bash OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "bash": executable file not found in $PATH: unknown するとエラーになりました。 Alpineをベースとすると bash は使えず、 ash や sh は使えるようです。 $ docker-compose exec web ash [web]:/ $ pwd / 無事コンテナの中に入れました。 webサーバーとしての動作確認 リクエストに対してファイルを返しブラウザで表示できる nginxのコンテナからPHPのコンテナへphpを実行させることができる 上記2点を確認します。 $ mkdir src/public $ touch src/public/test.php test.phpを以下のように編集します。 /src/public/test.php <?php echo 'test.phpです'; phpinfo(); http://localhost:8081/test.php にアクセスすると以下のように表示され、webサーバーが正しく動作していることが確認できます。 試しに作成したファイルは不要なので消しておきます。 $ rm -rf src/* MySQLのコンテナ(データベース) データベースのMySQLコンテナを作成します。 以下の流れで説明します。 docker-compose.ymlへの記述 MySQLコンテナ用のDockerfile MySQLコンテナ用の設定ファイル(my.conf) docker-compose.yml(MySQL) 既に作成しているdocker-compose.ymlに、MySQLのコンテナについての部分を追記します。 docker-compose.yml services: // 中略 db: // サービス名 build: context: . dockerfile: ./docker/db/Dockerfile ports: - 3306:3306 environment: MYSQL_DATABASE: database MYSQL_USER: user MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: password TZ: 'Asia/Tokyo' volumes: - mysql-volume:/var/lib/mysql volumes: mysql-volume: build: ・ ports: 既出の通りです。 enviroment: 環境変数の設定です。 ※名前やDB名・ユーザー名・パスワードは好きなものを設定します。 ※実際のプロジェクト管理では、環境変数は .env に書いて .gitignore にするなどして、重要な情報が公開されないようにします。 MYSQL_DATABASE:DB名 MYSQL_USER:ユーザー名 MYSQL_PASSWORD:パスワード MYSQL_ROOT_PASSWORD:ルート権限のパスワード TZ:時間設定(Time Zone) volumes: 考え方はPHPやnginxのコンテナ同様ですが、先ほどまではホスト側のディレクトリを書いていたところに mysql-volume と書いています。 これにより mysql-colume という名前で作成した名前付きボリュームとコンテナ内を紐づけています。 Docker Volume Volumeとは、コンテナを破棄してもデータを永続的に保存できるように、コンテナ外に提供されているデータの保存領域です。 Dockerの管理下でホスト上にストレージ領域を確保しており、Linux なら /var/lib/docker/volumes/以下にあります。 参考記事:Docker、ボリューム(Volume)について真面目に調べた - Qiita なぜVolumeが必要なのか? コンテナが起動している間はDBのレコードは保存された状態が続きますが、例えば以下のようなコマンドでコンテナを新しく作り直したとするとデータベースの情報はゼロに戻ってしまいます。 // コンテナ削除 $ docker-compose down // コンテナ起動 $ docker-compose up -d これでは困るのでコンテナを破棄してもデータを残したい、、、というときにVolumesを使ってデータの永続化を行います。 ※ボリュームには名前付きボリュームと匿名ボリュームがありますが、通常は管理しやすい名前付きボリュームを使うと良いかと思います。 名前付きボリュームを作成する場合は、データの永続化対象のコンテナに対して volumes: オプションでバインドマウントを行うだけでなく、以下のようにdocker-compose.ymlのトップレベルでボリューム名を定義します。 docker-compose.yml volumes: - mysql-volume: Dockerfile(MySQL) 作成するDockerfileの全文はこちらです。 /docker/db/Dockerfile FROM mysql:8.0 COPY ./docker/db/my.cnf /etc/my.cnf 内容を簡単にまとめると、 公式のMySQLイメージをベースイメージに使用 Mysql - Official Image | Docker Hub MySQLの設定ファイル(my.cnf)をコンテナ内にバインドマウント ※ここで使用しているイメージではM1 Macでは動作しないという情報が見られました。どうやらOracleのMySQLチームがメンテしている mysql/mysql-server のイメージだと動作するようです。 mysql-server | Docker Hub 設定ファイル(my.conf) ./docker/db/my.cnfを作成します。 色んな記事で作成されているmy.cnfを参考にさせていただきながら書きました。 参考記事:【Docker】docker-composeでmysqlのコンテナを立てる 文字コード タイムゾーン ログ の設定を行っています。 /docker/db/my.conf [mysqld] # character character_set_server = utf8mb4 collation_server = utf8mb4_0900_ai_ci # timezone default-time-zone = SYSTEM log_timestamps = SYSTEM # Error Log log-error = mysql-error.log # Slow Query Log slow_query_log = 1 slow_query_log_file = mysql-slow.log long_query_time = 1.0 log_queries_not_using_indexes = 0 # General Log general_log = 1 general_log_file = mysql-general.log [mysql] default-character-set = utf8mb4 [client] default-character-set = utf8mb4 MySQLコンテナを起動する MySQLのコンテナの準備が出来たので、また実際に起動してみます。 docker-sompose.ymlのあるディレクトリで以下のコマンドを実行します。 $ docker-compose up -d --build $ docker-compose ps NAME COMMAND SERVICE STATUS PORTS docker_sample-app-1 "docker-php-entrypoi…" app running 9000/tcp docker_sample-db-1 "docker-entrypoint.s…" db running 0.0.0.0:3306->3306/tcp docker_sample-web-1 "/docker-entrypoint.…" web running 0.0.0.0:8081->80/tcp api(PHPのコンテナ)、web(nginxのコンテナ)、db(MySQLのコンテナ)の3つが起動できました。 コンテナの動作確認 MySQLのバージョンを確認します。 $ docker compose exec db mysql -V mysql Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL) ※この後Laravelのプロジェクトを作成したら、このDBとLaravelを接続します。 そしてマイグレーションを行ってDBにテーブルを作成してから、MySQLにログインしてDBを使う動作確認をしたいと思います。 Laravelのインストール Laravelプロジェクトの作成 LEMP環境が構築できたので、Laravelのアプリケーションを作っていきます。 appコンテナに入り、Laravelをインストールします。 $ docker compose exec app bash [app]:/app$ composer create-project --prefer-dist "laravel/laravel=8.*" . [app]:/app$ php artisan -v Laravel Framework 8.81.0 コンテナ内の/app配下にLaravelのプロジェクトが新規作成され、ホスト側の/src配下にも同じくLaravelのプロジェクトが出来ました。 localhost:8081にアクセスして、ブラウザでもLaravelのウェルカムページが表示できることを確認します。 DB接続 appコンテナ(Laravel)からdbコンテナ(MySQL)へ接続する設定を行います。 Laravelではデータベースへの接続設定を .env ファイルに定義しているので、 /src/.env のDBの部分を以下のように修正します。 /src/.env DB_CONNECTION=mysql DB_HOST=db // MySQLコンテナのサービス名 DB_PORT=3306 DB_DATABASE=database DB_USERNAME=root DB_PASSWORD=passwor DB_HOST にはMySQLコンテナのサービス名を指定します。 その他の項目もMySQLコンテナで設定した値(今回はdocker-compose.ymlのenviromentで定義)と同じ値を指定します。 DBに接続出来ているか確認する為、以下のコマンドを実行してマイグレーションを行います。 $ docker compose exec app bash [app]:/app$ php artisan migrate Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (55.42ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (51.11ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (45.12ms) Migrating: 2019_12_14_000001_create_personal_access_tokens_table Migrated: 2019_12_14_000001_create_personal_access_tokens_table (75.74ms) MySQLを使ってみる MySQLのコンテナに入ってDBを確認してみます。 $ docker-compose exec db bash [db]:/$ mysql -u root -p // パスワードを求められるので入力 [db] mysql> use database; [db] mysql> show tables; +------------------------+ | Tables_in_database | +------------------------+ | failed_jobs | | migrations | | password_resets | | personal_access_tokens | | users | +------------------------+ 5 rows in set (0.00 sec) 先ほどマイグレーションを実行したので、Laravelのデフォルトで用意されているマイグレーションファイル通りにテーブルが作成されていることが確認できました。 最後に これでDocker(Docker Compose)を使ったLEMP環境の構築が完了です。 今回作成した環境は非常にシンプルなものなので、業務で使うとなるともっと設定を細やかに行ったり、開発環境・ステージング環境・本番環境それぞれの設定ファイルを用意して設定を切り替えたりといった作業が必要になるかと思いますが、仕組みが分かればあとは全てこの延長にあるのかなと思います。 またいずれ環境ごとの設定ファイル切り替えだったり、あとはNodeのコンテナを使ってNext.jsの環境構築もやってみたいです。 参考記事