20190526のMySQLに関する記事は5件です。

MySQLレプリケーション遅延と不整合を体験してみよう

本稿ではDockerを使って、MySQLのレプリケーションを手軽に試すことができる環境を構築します。そしてレプリケーション遅延と不整合を実際に起こしてみて、どのような状態になるかを確認していきます。

具体的にはSHOW SLAVE STATUSクエリの下記の値を確認することで、レプリケーションの状態がどうなっているかを見ていきます。

  • Slave_IO_RunningSlave_SQL_Running
  • Last_Error
  • Seconds_Behind_Master

ここ数年でクラウドサービスのフルマネージドデータベースが主流になってきており、そこでは特にレプリケーションの知識が無くても簡単にレプリカを作成することができます。手動でMySQLレプリケーションを構築しなければいけない機会は減ってきているかもしれません。しかし裏側で動いている仕組みを知っておくと、何かの役に立つかもしれません。

たとえば AWS RDS 公式ドキュメントでも、AWS 外部のデータベースにデータを移行する手段としてレプリケーションが紹介されています。
参考: Aurora と MySQL との間、または Aurora と別の Aurora DB クラスターとの間のレプリケーション - Amazon Aurora

また一方で、Dockerを使うことによって、MySQLレプリケーションの検証環境を構築するのも非常に楽になりました。実際にレプリケーション遅延も簡単に体験することができます。

環境

  • Windows 10 Pro 1809 Windows Subsystem for Linux (Bash on windows) Ubuntu 16.04.4 LTS
    • たぶんMacでも同じです
  • Docker version 18.09.1
  • docker-compose version 1.18.0

前提知識

本稿ではDockerを利用しますが、環境構築に利用するだけのため特に知識は無くても使えると思います。

1: レプリケーション構成MySQLを構築してみる

作業用に適当なディレクトリを作成します。

$ mkdir mysql-repl-sample && cd $_

単一構成MySQLの構築

まずはレプリケーションを貼る前に、1台だけのMySQLを構築します。

Dockerfile-masterというファイルを作成します。後にSlave用のDockerfileも作成するため、名前を分けておきます。

Dockerfile-master
FROM mysql:5.6
ADD ./mysql-master.cnf /etc/mysql/my.cnf
RUN chmod 644 /etc/mysql/my.cnf

mysql-master.cnfというファイルを作成します。こちらもmy.cnfですが、Slave用のファイルと区別できる名前にしています。

mysql-master.cnf
[mysqld]
log-bin
server-id=1

設定について詳しくは下記を参照してください。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 5.2.4.2 バイナリログ形式の設定

次に、Dockerfileとmy.cnfを使ってMySQLを起動するための、docker-compose.ymlを作成します。

docker-compose.yml
version: '2'
services:
  db-master:
    build:
      context: "./"
      dockerfile: "Dockerfile-master"
    ports:
      - "13306:3306"
    volumes:
      - mysql-master-data:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
volumes:
  mysql-master-data:

ファイルを作成し終えたら、下記コマンドでMySQLを起動できます。

$ docker-compose up -d

13306ポートを開けているため、下記コマンドで接続できます。

$ mysql -u root -h 127.0.0.1 -P 13306

接続できたら成功です。

2台のMySQLを構築

先程と同様に、Slave用のDockerfileとmy.cnfを作成します。

Dockerfile-slave
FROM mysql:5.6
ADD ./mysql-slave.cnf /etc/mysql/my.cnf
RUN chmod 644 /etc/mysql/my.cnf
mysql-slave.cnf
[mysqld]
server-id=2

docker-compose.ymlは、下記のように書き換えます。

docker-compose.yml
version: '2'
services:
  db-master:
    build:
      context: "./"
      dockerfile: "Dockerfile-master"
    ports:
      - "13306:3306"
    volumes:
      - mysql-master-data:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
  db-slave:
    build:
      context: "./"
      dockerfile: "Dockerfile-slave"
    ports:
      - '23306:3306'
    volumes:
      - mysql-slave-data:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
volumes:
  mysql-master-data:
  mysql-slave-data:

Masterは13306、Slaveは23306ポートに設定しています。これは覚えておいてください。

ファイルを作成したら、下記コマンドでDockerコンテナを作り直します。

$ docker-compose down
$ docker-compose up -d

レプリケーションの開始

Master(13306ポート)に接続し、binlogの状態を確認します。

$ mysql -u root -h 127.0.0.1 -P 13306 -e "SHOW MASTER STATUS\G"

この時の「File」と「Position」の値をメモしておきます。

続いて Slave(23306ポート) に接続し、下記CHANGE MASTERSTART SLAVEというクエリを実行します。

$ mysql -u root -h 127.0.0.1 -P 23306

> CHANGE MASTER TO
    MASTER_HOST='db-master',
    MASTER_PORT=3306,
    MASTER_USER='root',
    MASTER_PASSWORD='',
    MASTER_LOG_FILE='<MasterLogFile>',
    MASTER_LOG_POS=<MasterLogPosition>;

> START SLAVE;

> SHOW SLAVE STATUS\G

<MasterLogFile><MasterLogPosition>には、先ほどSHOW MASTER STATUSでメモした値を入れてください。

Slave(23306ポート)に接続し、Slave_IO_RunningSlave_SQL_RunningがともにYesになっていることを確認できたら、レプリケーション成功です。

$ mysql -u root -h 127.0.0.1 -P 23306 -e "SHOW SLAVE STATUS\G" | grep Running: 
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes

レプリケーションを使ってデータを2台のMySQLに反映させる

Master(13306ポート)に接続し、適当なデータを入れてみます。

$ mysql -u root -h 127.0.0.1 -P 13306
> CREATE DATABASE sushi_ya;
> USE sushi_ya;

> CREATE TABLE sushi(
    id   INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    INDEX(id)
);

> INSERT into sushi_ya.sushi (name) VALUES ('maguro');
> INSERT into sushi_ya.sushi (name) VALUES ('tamago');

つづいて Slave(23306ポート)に接続 し、データを確認してみましょう。

$ mysql -u root -h 127.0.0.1 -P 23306 -e "SELECT * FROM sushi_ya.sushi;" 
+----+--------+ 
| id | name   |
+----+--------+
|  1 | maguro | 
|  2 | tamago |
+----+--------+

SlaveにはINSERTクエリを実行していません。しかし、MasterにINSERTしたデータがSlaveにも入っていることを確認できました。これがレプリケーションです。

2: レプリケーション不整合を起こして、修復してみる

レプリケーション構成においてはデータの不整合が起きないようにするために、Masterに更新系クエリが実行される事が想定されています。Slaveにはロックをかけておいて更新を防ぐ事もあります。しかし何らかの理由でMasterとSlaveのデータに不整合が起きてしまった場合、レプリケーションが反映できず止まってしまう事があります。

実際にやってみましょう。

MasterとSlaveに同じIDで異なるデータを入れて、レプリケーションを止める

Slave(23306ポート)に直接 、id=100のデータを追記してみます。

$ mysql -u root -h 127.0.0.1 -P 23306 -e "INSERT into sushi_ya.sushi (id, name) VALUES (100, 'ika');"

当然Slaveには追加されますが、Masterには反映されていません。

$ mysql -u root -h 127.0.0.1 -P 13306 -e "SELECT * FROM sushi_ya.sushi WHERE id = 100;" # Master
$ mysql -u root -h 127.0.0.1 -P 23306 -e "SELECT * FROM sushi_ya.sushi WHERE id = 100;" # Slave 
+-----+------+ 
| id  | name |
+-----+------+
| 100 | ika  |
+-----+------+

ここで Master(13306ポート)にもid=100を追加する と、どうなるでしょうか?

$ mysql -u root -h 127.0.0.1 -P 13306 -e "INSERT into sushi_ya.sushi (id, name) VALUES (100, 'tako');"

MasterとSlaveの様子を見てみます。

$ mysql -u root -h 127.0.0.1 -P 13306 -e "SELECT * FROM sushi_ya.sushi WHERE id = 100;" # Master 
+-----+------+ 
| id  | name |
+-----+------+
| 100 | tako |
+-----+------+
$ mysql -u root -h 127.0.0.1 -P 23306 -e "SELECT * FROM sushi_ya.sushi WHERE id = 100;" # Slave 
+-----+------+ 
| id  | name |
+-----+------+
| 100 | ika  |
+-----+------+

id=100なのに、異なるデータが入ってしまっており、データの不整合が起きています。

このときレプリケーションは止まってしまっています。
Master(13306ポート)にid=101のデータを追加しても、Slaveには反映されません。

$ mysql -u root -h 127.0.0.1 -P 13306 -e "INSERT into sushi_ya.sushi (id, name) VALUES (101, 'ebi');" # Master
$ mysql -u root -h 127.0.0.1 -P 13306 -e "SELECT * FROM sushi_ya.sushi WHERE id = 101;" # Master
$ mysql -u root -h 127.0.0.1 -P 23306 -e "SELECT * FROM sushi_ya.sushi WHERE id = 101;" # Slave

SHOW SLAVE STATUSを確認します。

$ mysql -u root -h 127.0.0.1 -P 23306 -e "SHOW SLAVE STATUS\G" | grep Last_Error 
                   Last_Error: Could not execute Write_rows event on table sushi_ya.sushi; Duplicate entry '100' for key 'PRIMARY', Error_code: 1062; handler error HA_ERR_FOUND_DUPP_KEY; the event's master log f4e6386f510a-bin.000001, end_log_pos 1351

id=100のデータを反映させようとした時のDuplicate entryのエラーでレプリケーションが止まっています。

$ mysql -u root -h 127.0.0.1 -P 23306 -e "SHOW SLAVE STATUS\G" | grep Running: 
             Slave_IO_Running: Yes
            Slave_SQL_Running: No

Slave_SQL_Running: Noになっているのが確認できます。

レプリケーションの再開

止まってしまったレプリケーションを再開するには、下記の方法があります

  • エラーになっているクエリをスキップする (Slaveを採択)
  • エラーを直してbinlogを通す (Masterを採択)

今回は、エラーを修正する方法を紹介します。

Slave(23306ポート)に入ってしまったid=100の不正なデータを削除し、Masterにあるid=100を反映できるようにします。そしtSTART SLAVEでレプリケーションを再開します。

$ mysql -u root -h 127.0.0.1 -P 23306 -e "DELETE FROM sushi_ya.sushi WHERE id = 100;"
$ mysql -u root -h 127.0.0.1 -P 23306 -e "START SLAVE;"

エラーが解消され、レプリケーションが再開され、SHOW SLAVE STATUSSlave_SQL_Running: Yesになります。

$ mysql -u root -h 127.0.0.1 -P 23306 -e "SHOW SLAVE STATUS\G" | grep Last_Error 
                   Last_Error:
$ mysql -u root -h 127.0.0.1 -P 23306 -e "SHOW SLAVE STATUS\G" | grep Running: 
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes

レプリケーションが止まっていた後のid=101のデータも、全て入っているのが確認できます。

$ mysql -u root -h 127.0.0.1 -P 23306 -e "SELECT * FROM sushi_ya.sushi;" 
+-----+--------+ 
| id  | name   |
+-----+--------+
|   1 | maguro |
|   2 | tamago |
| 100 | tako   |
| 101 | ebi    | 
+-----+--------+

今回とは逆にSlaveを採択する場合は、SET GLOBAL sql_slave_skip_counter = 1というクエリを実行しMasterのbinlogをスキップします。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.4.2.4 SET GLOBAL sql_slave_skip_counter 構文

3: レプリケーション遅延を体験する

Masterに対して非常に重いクエリが実行されたりすると、レプリケーションに遅延が発生し「Masterのクエリが完了してからSlaveに反映されるまでの間、MasterにはレコードがあるのにSlaveには無い状態」になってしまいます。Slaveに参照系のクエリを投げて負荷分散している場合、更新したはずのデータが取得できず不具合を起こしてしまうことがあります。

実際にレプリケーション遅延を起こしてみましょう。

しかし今回の検証用のたった数件のデータベースでは、どんなクエリも一瞬で返ってきてしまうと思います。そこでここではSLEEP関数を使って「30秒かかる超遅いINSERTクエリ」を擬似的に発生させます。

まず準備として、Masterのバイナリログの形式をステートメントベースに変更しておきます。

$ mysql -u root -h 127.0.0.1 -P 13306 -e "SET GLOBAL binlog_format = 'STATEMENT';"

SHOW VARIABLESで確認できます。

$ mysql -u root -h 127.0.0.1 -P 13306 -e "SHOW VARIABLES LIKE 'binlog_format'\G"; 
*************************** 1. row *************************** 
Variable_name: binlog_format
        Value: STATEMENT

これは本稿においてはSLEEP関数を使ってレプリケーション遅延を発生させるためだけの設定であり、本質とは関係ありません。詳細は下記を確認してください。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 5.2.4.2 バイナリログ形式の設定

これから1分間で、下記の順番でオペレーションをします。

  1. Masterに「30秒かかるINSERT文」を実行する
  2. 30秒待つ
  3. Slaveに反映されるまでの30秒間に、下記を確認する
    1. Masterにデータが入っていること
    2. Slaveにデータが入っていないこと
    3. SHOW SLAVE STATUSSeconds_Behind_Masterの値が1以上になること

時間が限られているので、何が起きるか事前に頭に入れて準備した上で実行してみてください。

まずMaster(13306ポート)に「30秒かかるクエリ」を実行します。

$ mysql -u root -h 127.0.0.1 -P 13306 -e "INSERT into sushi_ya.sushi (name) VALUES (CONCAT('wasabi', SLEEP(30)));" # 30秒待つ

MasterへのINSERTに30秒かかるのを待ったら、Slaveに反映される30秒の間に、下記クエリを実行してみましょう。

$ mysql -u root -h 127.0.0.1 -P 13306 -e "SELECT * FROM sushi_ya.sushi;" # Master
$ mysql -u root -h 127.0.0.1 -P 23306 -e "SELECT * FROM sushi_ya.sushi;" # Slave
$ mysql -u root -h 127.0.0.1 -P 23306 -e "SHOW SLAVE STATUS\G" | grep Seconds_Behind_Master

Masterに登録されたはずのデータが、Slaveに登録されていないのが分かります。

最後のSHOW SLAVE STATUSを何度も繰り返し実行していると、Seconds_Behind_Masterの数値がどんどん増えていって、30になると一気にゼロになると思います。30秒経過すると、Slaveにもデータが入っている事が確認できると思います。

これがレプリケーション遅延です。レプリケーションが非同期のため起きる現象です。

このようにSeconds_Behind_Masterの値が大きくなっていると、MasterとSlaveとの間でデータに差異が生まれ、アプリケーションに不具合を引き起こす可能性があります。

まとめ

MySQLレプリケーションの状態は、SHOW SLAVE STATUSクエリで確認することができます。その中でも不整合や遅延は、以下の値で確認できます。

  • Slave_IO_RunningSlave_SQL_Running がYesになっているかどうか
  • Last_Error があるかどうか
  • Seconds_Behind_Master が1以上の値になっていないかどうか

なお今回使ったDockerでの検証環境構築プログラムは、下記リポジトリにもあります。
https://github.com/s2terminal/mysql-repl

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nodejs製のwebアプリにnextjsを組み込むにはどのようにすればいいか試してみた

はじめに

以下の疑問点を解決するために試したことをまとめた。

  1. nodejs製のwebアプリにnextjsを組み込むにはどのようにすればいいか?
  2. DBに保存した値をSSRするためにはどのようにすればいいのか?
  3. ページの遷移はどのようにすればいいのか?

アプローチ

ユーザ別に登録できるタスクリストを作った。
repository
Kapture 2019-05-26 at 23.01.00.gif

koajsでwebアプリの基盤を作り、typeormでDB操作を行う。
DBはdocker上にmysqlを立てた。
ormはtypeormを使用した。
その他細かいライブラリについてはpackage.jsonを参照。

構成

全ユーザの一覧が見れるindexページと、タスクの一覧が見えるtaskページがある。
indexページではユーザとタスクのcrudが可能。
taskページではタスク一覧の読み取りのみ可能とした。

データの構造は次の通り。
- server/entities/User.ts
- server/entities/Task.ts

ユーザを削除すると紐付いたタスクが自動的に削除されるように Task.useronDelete: 'CASCADE' オプションを付与した。

nextに関係する処理があるファイルには★をつけた。

.
├── README.md
├── docker
│   └── db
│       └── my.conf
├── docker-compose.yml
├── models <- tableとapiの項目が一致するので同一のinterfaceを使用することにした
│   ├── Task.ts
│   └── User.ts
├── next.config.js
├── nodemon.json
├── ormconfig.json
├── package-lock.json
├── package.json
├── pages <- フロント側の処理。ちょっとnextが入ってるがほとんど普通のreact
│   ├── index.tsx ★
│   └── tasks.tsx ★
├── repositories <- api接続処理
│   ├── helpers.ts
│   ├── tasks.ts
│   └── users.ts
├── server
│   ├── api <- controllerに当たる部分。apiリクエストに応答する処理。
│   │   ├── tasks.ts
│   │   └── users.ts
│   ├── commands <- サーバ処理とは一切関係ない。
│   │   ├── generateData.ts <- データを作る。 npm run generate:data で起動する。
│   │   └── route.ts <- routingを確認する。 npm run route で起動する。
│   ├── createRouter.ts ★ <- routerを作るentrypoint
│   ├── entities <- tableの項目とマッピングするentity
│   │   ├── Task.ts
│   │   └── User.ts
│   ├── helpers.ts <- DB接続処理がある。commandsでも使用するので分けた。
│   ├── index.ts ★ <- entrypoint。npm start で起動する。
│   ├── next.ts ★ <- nextを直接触る処理はここに閉じ込めた。
│   ├── repositories <- repositoriesと名前をつけたがuseCase層と合体させた。
│   │   ├── TaskRepository.ts
│   │   └── UserRepository.ts
│   └── views ★ <- controllerに当たる部分。
│       ├── index.ts
│       └── tasks.ts
├── tsconfig.json
└── tsconfig.server.json

結論

1. nodejs製のwebアプリにnextjsを組み込むにはどのようにすればいいか?

1. 初期化処理でprepare()

まずserverのentrypointである server/index.tsprepare() を行う。
それなりに時間がかかる処理なので、DB接続処理と同時に行うと時間節約できる。

await Promise.all([
  connectDatabase(),
  prepare()
])

2. routingを設定する

すべてのリクエストにnextのhandleを設定する。

server/next.ts
const handler = app.getRequestHandler()
// omit...
export async function handle(ctx: BaseContext) {
  await handler(ctx.req, ctx.res)
  ctx.respond = false
}
server/createRouter.ts
import { handle } from './next'
// omit...
export default function () {
  const router = new Router()
  const assign = pipe(
    views,
    apiUsers,
    apiTasks,
    next
  )
  return assign(router)
}

// omit...

function next(router: Router) {
  return router
    .get('*', handle) // <- これが必要
}

router.get('*', handle) で設定しないと以下のエラーが発生する。

  <-- GET /_next/on-demand-entries-ping?page=/
  --> GET /_next/on-demand-entries-ping?page=/ 404 1ms -
  <-- GET /_next/static/development/pages/index.js?ts=1558873127678
  --> GET /_next/static/development/pages/index.js?ts=1558873127678 404 0ms -
  <-- GET /_next/static/development/pages/_app.js?ts=1558873127678
  --> GET /_next/static/development/pages/_app.js?ts=1558873127678 404 1ms -
  <-- GET /_next/static/runtime/webpack.js?ts=1558873127678
  --> GET /_next/static/runtime/webpack.js?ts=1558873127678 404 1ms -
  <-- GET /_next/static/runtime/main.js?ts=1558873127678
  --> GET /_next/static/runtime/main.js?ts=1558873127678 404 0ms -
  <-- GET /_next/static/development/dll/dll_599a58a60c43245180de.js?ts=1558873127678
  --> GET /_next/static/development/dll/dll_599a58a60c43245180de.js?ts=1558873127678 404 0ms -
  <-- GET /_next/static/development/pages/index.js?ts=1558873127678
  --> GET /_next/static/development/pages/index.js?ts=1558873127678 404 1ms -
  <-- GET /_next/static/development/pages/_app.js?ts=1558873127678
  --> GET /_next/static/development/pages/_app.js?ts=1558873127678 404 1ms -
  <-- GET /_next/static/runtime/webpack.js?ts=1558873127678
  --> GET /_next/static/runtime/webpack.js?ts=1558873127678 404 1ms -
  <-- GET /_next/static/runtime/main.js?ts=1558873127678
  --> GET /_next/static/runtime/main.js?ts=1558873127678 404 0ms -

一見viewへのリクエストに書けばいいように見える。
そこで server/views/index.ts に書いてみたが、 await render(ctx.req, ctx.res, '/', query) の後に await handler(ctx.req, ctx.res) を差し込めば上のエラーが、 await render(ctx.req, ctx.res, '/', query) より前に差し込めば下のエラーが発生する。

fetch is not defined
ReferenceError: fetch is not defined
    at _callee$ (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/.next/server/static/development/pages/index.js:886:13)
    at tryCatch (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/node_modules/regenerator-runtime/runtime.js:62:40)
    at Generator.invoke [as _invoke] (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/node_modules/regenerator-runtime/runtime.js:288:22)
    at Generator.prototype.(anonymous function) [as next] (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/node_modules/regenerator-runtime/runtime.js:114:21)
    at asyncGeneratorStep (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/.next/server/static/development/pages/index.js:186:24)
    at _next (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/.next/server/static/development/pages/index.js:208:9)
    at /Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/.next/server/static/development/pages/index.js:215:7
    at new Promise (<anonymous>)
    at new F (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/node_modules/core-js/library/modules/_export.js:36:28)
    at Module.<anonymous> (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/.next/server/static/development/pages/index.js:204:12)
    at Module.all (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/.next/server/static/development/pages/index.js:873:15)
    at _callee8$ (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/.next/server/static/development/pages/index.js:696:75)
    at tryCatch (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/node_modules/regenerator-runtime/runtime.js:62:40)
    at Generator.invoke [as _invoke] (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/node_modules/regenerator-runtime/runtime.js:288:22)
    at Generator.prototype.(anonymous function) [as next] (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/node_modules/regenerator-runtime/runtime.js:114:21)
    at asyncGeneratorStep (/Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/.next/server/static/development/pages/index.js:186:24)

1. レンダリング処理を行う

次項(2. DBに保存した値をSSRするためにはどのようにすればいいのか?)で説明する。

2. DBに保存した値をSSRするためにはどのようにすればいいのか?

views/index.tsviews/tasks.ts にあるようにrender関数に 文字列化した データを設定する。

export default async function (ctx: BaseContext) {
  const users = await getRepository().all()
  const stringified = JSON.stringify(users)
  const query = { users: stringified }
  await render(ctx.req, ctx.res, '/', query) // <- ここで設定
  ctx.respond = false
}

pages/index.tsxpages/tasks.tsx にある通り、 getInitialProps でデータを取得する。

Page.getInitialProps = async (context: NextContext) => {
  const renderingOnServer = context.query.users !== undefined
  if (renderingOnServer) {
    // server処理の場合はqueryに文字列が設定されているので
    // JSON.parseして使用する。
    const raw = context.query.users as string
    const users = JSON.parse(raw)
    return { users }
  }
  // ブラウザ側ならapiからデータを取得する。
  const users = await UsersRepository.all()
  return { users }
}

生成されたhtmlを見てみると、以下のようにjsonが書き込まれていることがわかる。

<script id="__NEXT_DATA__" type="application/json">{"dataManager":"[]","props":{"pageProps":{"users":[{"id":1,"name":"山田","tasks":[{"id":1,"userId":1,"text":"皿洗い"},{"id":2,"userId":1,"text":"買い物"}]},{"id":2,"name":"佐藤","tasks":[{"id":3,"userId":2,"text":"草むしり"},{"id":4,"userId":2,"text":"押入れの片付け"}]},{"id":3,"name":"平田","tasks":[{"id":5,"userId":3,"text":"野菜の皮むき"}]},{"id":4,"name":"山本","tasks":[{"id":6,"userId":4,"text":"カレー作り"}]},{"id":5,"name":"柴田","tasks":[{"id":7,"userId":5,"text":"食卓の準備"},{"id":8,"userId":5,"text":"押入れの片付け"}]}]}},"page":"/","query":{"users":"[{\"id\":1,\"name\":\"山田\",\"tasks\":[{\"id\":1,\"userId\":1,\"text\":\"皿洗い\"},{\"id\":2,\"userId\":1,\"text\":\"買い物\"}]},{\"id\":2,\"name\":\"佐藤\",\"tasks\":[{\"id\":3,\"userId\":2,\"text\":\"草むしり\"},{\"id\":4,\"userId\":2,\"text\":\"押入れの片付け\"}]},{\"id\":3,\"name\":\"平田\",\"tasks\":[{\"id\":5,\"userId\":3,\"text\":\"野菜の皮むき\"}]},{\"id\":4,\"name\":\"山本\",\"tasks\":[{\"id\":6,\"userId\":4,\"text\":\"カレー作り\"}]},{\"id\":5,\"name\":\"柴田\",\"tasks\":[{\"id\":7,\"userId\":5,\"text\":\"食卓の準備\"},{\"id\":8,\"userId\":5,\"text\":\"押入れの片付け\"}]}]"},"buildId":"development","dynamicBuildId":false}</script>

server側の処理のときならDBに接続できるのでは?と思い以下のようにしたがだめだった。

Page.getInitialProps = async (context: NextContext) => {
  const renderingOnServer = context.query.users !== undefined
  if (renderingOnServer) {
    const UserRepository = require('../server/repositories/UserRepository')
    const Container = require('typedi')
    const repository = Container.get(UserRepository)
    const users = await repository.all()
    return  { users }
    // const raw = context.query.users as string
    // const users = JSON.parse(raw)
    // return { users }
  }
  const users = await UsersRepository.all()
  return { users }
}
./server/repositories/UserRepository.ts
SyntaxError: /Users/h-h/h_h/2019-05-25-nextjs-koa-mysql/server/repositories/UserRepository.ts: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (6:1):

  4 | import { User } from '../entities/User'
  5 |
> 6 | @Service()
    | ^
  7 | @EntityRepository(User)
  8 | export class UserRepository {
  9 |

SyntaxErrorなのでbabelの設定次第では行けそうな気もするが、できたとしてもviewにDB操作するような処理を書くのは好きじゃないので考えるのをやめた。

3. ページの遷移はどのようにすればいいのか?

pages/index.tsxpages/tasks.tsxにある通り次のようにする。

import Link from 'next/Link'

// omit...

<Link href="/tasks">
  <a>> tasks</a>
</Link>

参考資料

nextの組み方を調べたときに確認した。
- custom-server-koa

nextでtypescriptを使えるようにするために確認した。
- custom-server-typescript
- with-typescript

nodemonの設定方法を確認した。
- custom-server-nodemon

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SSHのポートフォワーディングを学習して、SSH経由でMySQLサーバーに接続する

前置き

WorkbenchからSSHのみ接続許可されているMySQLサーバーに接続する事情がありましたので、SSHポートフォワーディングを利用しました。SSHポートフォワーディングは既に色々なところでご紹介されていますが、少しハマった箇所がありましたので記事にしたいと思います。なお、ポートフォワーディングの学習も兼ねてSSHコマンドとWorkbenchを分けましたが、Workbench単体で全てを設定させるより簡単な方法もあります。

構成

  • クライアント

    名前 バージョン
    macOS Mojave 10.14.5
    MySQL Workbench 8.0.15
  • RDBサーバー

    名前 バージョン
    AWS EC2 Amazon Linux release 2 (Karoo)
    MySQL Ver 8.0.16 for Linux on x86_64 (MySQL Community Server - GPL)

今回はサンプル環境になるのでEC2にGlobal IP(Public IP)を割当てて、外部にSSHのポート22を解放しています。
MySQLのポートはデフォルトの3306を使用しており外部には解放してません。
実務で利用する環境では、既定のセキュリティポリシーと構成があると思います。

登場するRDBサーバーの指定方法

以下、全て同じRDBサーバーを表します。後述の注意点で説明することになりますので、予め記載します。

名前 IP or 名前 備考
Global IP 18.179.xxx.xxx 既にインスタンスを破棄しました。
今後別の利用者に割り当てられるかもしれないので一部伏せておきます。
Private IP 10.0.0.176
ループバック 127.0.0.1 もちろん、RDBサーバーから見た場合です。
ホスト localhost

今回接続に使用したコマンド

最終的に目的を実現したコマンドは以下のとおりです。

sudo ssh -fNL 33060:localhost:3306 -i "hoge.pem" ec2-user@18.179.xxx.xxx

image.png
上記のイメージでSSHポートフォワーディングのコマンドを組み立てます。

  1. クライアント側の転送対象のポートを選ぶ
    特に拘りがありませんでしたので、競合していない33060としました。
  2. RDBサーバーから見た転送先を指定
    SSHで接続したRDBサーバー(18.179.xxx.xxx)にMySQLが稼働しています。RDBサーバーから見ると転送先は自分自身になりますので、ホストがlocalhostで、ポートは3306になります。Global IPやPrivate IPで指定しない理由は後ほど解説します。

その他、接続SSHへ接続するためのプライベートキーやユーザー、接続先の指定は通常のSSH接続と変わりありません。

Workbenchの設定

クライアント側のWorkbench以下のように指定します。
image.png

項目 IP or 名前 備考
Hostname 127.0.0.1 / localhost
Port 33060
Username 任意のユーザー RDBサーバー(自分自身)から接続できるユーザーを指定します。
例えば特定IPからのみログインを許可しているユーザーはログインできません。
Password 上記ユーザーのパスワード

無事にテスト接続ができました:thumbsup:

image.png

ハマったところ

「ハマった」と言っても少しですが、躓いたところを記載します。

  1. 疎通ができない
    当初、転送先にRDBサーバーのGlobal IPを指定していました。

    ssh -fNL 33060:18.179.xxx.xxx:3306 -i "web_a.pem" ec2-user@18.179.xxx.xxx
    

    ネットワークの疎通については設定次第になりますが、RDBサーバーにログインして、以下のようにncコマンドを実行してみます。自身のGlobal IPとポート3306を指定して通信が疎通しない場合は、SSHポートフォワーディングもできません。

    RDBサーバー側で疎通しない例
    $ nc -w 1 -vz 18.179.xxx.xxx 3306
    Ncat: Version 7.50 ( https://nmap.org/ncat )
    Ncat: Connection timed out.
    

    Private IPやループバック、localhostを指定することで、疎通ができるようになります。

    RDBサーバー側で疎通する例
    $ nc -w 1 -vz 10.0.0.176 3306
    Ncat: Version 7.50 ( https://nmap.org/ncat )
    Ncat: Connected to 10.0.0.176:3306.
    Ncat: 0 bytes sent, 0 bytes received in 0.01 seconds.
    
  2. 正しいパスワードを入力しているのにログインできない
    疎通ができてもログインできないケースがあります。転送先にRDBサーバーのPrivate IPを指定しているとします。

    ssh -fNL 33060:10.0.0.176:3306 -i "web_a.pem" ec2-user@18.179.xxx.xxx
    

    ログイン時のエラーメッセージは以下のとおりです。
    image.png

ログインユーザーが'root'@'ip-10-0-0-176.ap-northeast-1.compute.internal'になっています。筆者の環境では、他のサーバーからのrootユーザーのログインを禁止していますが、その時の設定は以下のとおりです。

SELECT user, host FROM mysql.user WHERE user = 'root';
+------+-----------+
| user | host      |
+------+-----------+
| root | localhost |
+------+-----------+
1 row in set (0.00 sec)

そのため、'root'@'ip-10-0-0-176.ap-northeast-1.compute.internal'ではログインできないことになります。SSHポートフォワーディングのためにログインユーザーを作成しない場合は、転送先にRDBサーバーに127.0.0.1localhostを指定します。

Workbenchのみで設定する

「いままでの説明は何だったんだ:question:」と思われるかもしれませんが、前述のとおりSSHコマンドを使わずにWorkbenchのみで設定する方法があります。今までSSHコマンドで実行した内容を再現すると以下のようになります。
image.png

参考

終わりに

Workbenchにポートフォワーディングの機能が含まれているので、SSHコマンドで別途実行する需要は無いかもしれません。しかし、SSHのポートフォワーディング単体を理解することで、どこかで応用ができるかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【MySQL】起動しない時の対処方法

何が起きたか

  • MySQLを起動しようとしたところ、エラーで起動しなくなった
  • 入力したコマンドは、「sudo mysql.server start」
  • ⇨ ERROR! The server quit without updating PID file

エラーメッセージ

sudo mysql.server start
Password:
Starting MySQL
.. ERROR! The server quit without updating PID file (/usr/local/var/mysql/xxxxxxxxx.local.pid).

どのように解決したか

$sudo chown -R _mysql:_mysql /usr/local/var/mysql
  • 起動に成功

結果

$ sudo mysql.server start
Starting MySQL
.. SUCCESS! 

まとめ

  • 解決しました
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GCPの無料枠でWordPressを起動して運用するまでの備忘録 2

背景

前回GCPに Ubuntu 18.04 LTS Minimal のインスタンスを作成したので、そこに Docker をインストールして、WordPress を立ち上げます。

今回は、こちら ↓ を参考にさせて頂きました。

repositoryをアップデートする

apt パッケージのアップデート。
$ sudo apt update
HTTPS経由でrepositoryをやりとり出来るようにするためのパッケージをインストール。
$ sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
Dockerの公式GPG keyを追加する。
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
repository( stable ) を追加する。
$sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
再度 apt パッケージのアップデート
$ sudo apt update

docker-ce をインストールする

インストール。
sudo apt install -y docker-ce
起動確認。
$ sudo systemctl status docker
プロセスの確認(sudo有)
$ sudo docker ps

指定した一般ユーザでも sudo 無しでdockerを使えるようにする

初期設定では出来ない

$ docker ps
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.37/containers/json: dial unix /var/run/docker.sock: connect: permission denied

一般ユーザをDockerグループに追加する

### 確認
$ cat /etc/group | grep docker
docker:x:999:

### 追加
sudo gpasswd -a xxxx docker

### 再確認
$ cat /etc/group | grep docker
docker:x:999:xxxx

dockerが使用するソケットを一般ユーザでも読み込み出来るようにする
$ sudo chmod 666 /var/run/docker.sock
プロセスの確認(sudo無)
$ docker ps

docker-composeをインストール

最新のバージョンを確認

$ export compose='1.24.0'
/usr/local/bin/ 配下にダウンロード

$ sudo curl -L https://github.com/docker/compose/releases/download/${compose}/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

実行権限の付与
$ sudo chmod 0755 /usr/local/bin/docker-compose

確認(sodo無)

$ docker-compose -v
docker-compose version 1.24.0, build 0aa59064

WordPress と Mysql インスタンスを個別に起動

プロジェクト用の空のディレクトリを作成
$ mkdir ghidorah_wordpress
$ cd ghidorah_wordpress
docker-compose.yml ファイルを作成

version: '3'

services:
  mysql:
    image: mysql:5.7
    container_name: ghidorah-mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: xxxx
    volumes:
      - "./.data/db:/var/lib/mysql"

  wordpress:
    image: wordpress:latest
    depends_on:
      - mysql
    container_name: ghidorah-wordpress
    restart: always
    ports:
      - 80:80
    environment:
      WORDPRESS_DB_PASSWORD: xxxx
    volumes:
      - "./html:/var/www/html"

docker-compose をバックグランドで起動
$ docker-compose up -d
WordPress が起動。確認のため、ブラウザでアクセスしてみる。
WordPress インストール画面が表示されていれば成功。
WP01.JPG
手順通りインストールを進める。
WP02.JPG
WP04.JPG
WP05.JPG
WP06.JPG

まとめ

WordPress が起ち上がったので、次回は独自ドメインの設定をします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む