MySQL 一定秒以上 Sleep しているプロセスを一括 kill

メモ

300秒以上 Sleep しているプロセスIDをまとめて表示

1
2
3
4
5
6
7
$ mysql -h <host> -u <user> -p -e'SELECT GROUP_CONCAT(ID) FROM information_schema.PROCESSLIST WHERE TIME > 300;'

+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| GROUP_CONCAT(ID) |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 2147,2143,2138,2113,2111,2104,2102,2098,2087,2085,2082,2081,2079,2078,2069,2068,2045,2037,2029,2025,2023,2016,2015,2006,2005,2003,2002,2001,1999,1998,1997,1995,1987,1986,1984,1982,1981,1974,1973,1968,1966,1963,1961,1959,1957,1955,1954,1949,1937,1936,1928,1925,1923,1920,1916,1914,1912,1908,1906,1898,1892,1869,1847,1842,1651,1650,1572,1570,1568,1566,1539,1522,1517,1516,1514,1511,1506,1483 |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

プロセスIDをまとめて一括 kill

1
$ mysqladmin kill <pid,pid,pid...> -h <host> -u <user> -p
Vagrant + docker-compose で Rails 5.1.0 (Puma) + Nginx + MySQL 環境構築

Vagrant + docker-compose で Rails 5.1.0 (Puma) + Nginx + MySQL 環境構築

概要

簡易的に Rails 環境を構築・開発できる様にすべく構築しました。

こんな時に利用してます。

  • 新規プロジェクト開発
  • 新規 gem, その他ミドルウェアの試験
  • 簡単なモックを作ってディレクターに見せたい時とか

構築手順をまとめました。

環境

  • macOS Sierra 10.12.5
  • VirtualBox 5.1.18r114002
  • Vagrant 1.9.3
  • VagrantBox Ubuntu 14.04.5
  • Docker version 17.06.0-ce, build 02c1d87

Git Clone

1
2
3
4
5
macOS%$ git clone https://github.com/kenzo0107/vagrant-docker
macOS%$ cd vagrant-docker
macOS%$ vagrant up
macOS%$ vagrant ssh
vagrant%$ cd /vagrant/rails-puma-nginx-mysql/

Rails プロジェクト作成

1
2
// database = mysql
vagrant%$ docker-compose run --rm web rails new . --force --database=mysql --skip-bundle

puma.rb 設定

1
2
3
// backup
vagrant%$ cp ./rails/config/puma.rb ./rails/config/puma.rb.bk
vagrant%$ cp puma.rb ./rails/config/
  • ./rails/config/puma.rb
1
2
3
4
5
6
7
8
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
threads threads_count, threads_count
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
plugin :tmp_restart

app_root = File.expand_path("../..", __FILE__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"

データベース接続情報設定

1
2
3
// backup
vagrant%$ cp ./rails/config/database.yml ./rails/config/database.yml.bk
vagrant%$ cp database.yml ./rails/config/
  • ./rails/config/database.yml
1
2
3
4
5
6
7
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password: <%= ENV['MYSQL_ROOT_PASSWORD'] %> # <--- MYSQL_ROOT_PASSWORD
host: db # <--- service name

DB 作成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vagrant%$ docker-compose run --rm web rails db:create
Created database 'app_development'
Created database 'app_test'

vagrant%$ docker-compose exec db mysql -u root -p -e'show databases;'
Enter password: (password)
+--------------------+
| Database |
+--------------------+
| information_schema |
| app_development | <--- add !
| app_test | <--- add !
| mysql |
| performance_schema |
| sys |
+--------------------+

Rails 実行

1
vagrant%$ docker-compose up -d

http://192.168.35.101 にアクセスすると Rails のウェルカムページが表示されます。

rails g

rails g 実行時は基本 one-off container で実行するのが良いです。

例えば以下は articles テーブルを作成、また、関連する controller, view, model を作成します。

1
vagrant%$ docker-compose run --rm web rails g scaffold article title:string body:text

Gemfile 更新

Gemfile 更新した際はビルドし再起動します。

1
2
3
vagrant%$ docker-compose stop web
vagrant%$ docker-compose build web
vagrant%$ docker-compose up -d web

あとがき

Rack server との接続は一癖ありましたが、そこさえ乗り越えたら
すっと行きました ♪

DB は 3306 でオープンしてるので
Mac のローカルから Sequel Pro で接続して確認できます。

これをベースに EFK でログ確認できる様にしたり、
mailcatcher でメール機能を試験できる様にしたりと
何かと便利です。

Docker 有難や ♪

Go+Revelフレームワーク 非同期でS3へ画像リサイズ/アップロード

備忘録です。

概要

AWS 向けの go ライブラリが乱立していて
どれ使ったらいい?という感じだったので
本家の launchpad.net/goamz/aws を利用して
実装することにしました。

Controller

  • app/controllers/img.go

Component

画像アップロード部分を component 化しました。

  • app/utility/aws.go

Views

  • Views/Img/Index.html
  • public/js/ajax.js
  • public/js/jquery.uploadThumbs.js

GKEチュートリアルでサイト構築・運用

概要

以前さくらVPS上で tocoちゃんバスアプリを作成しました。

さくらVPSは個人プロジェクトを幾つか載せていましたが
一部終了した為、tocoちゃんバスアプリを GCP にお引越ししました。

その時の話を GKE チュートリアルを兼ねて改めてまとめました。

何故 GCP ?

toco ちゃんバスアプリはDBも持たない軽量なサイトです。
その為、GCPの無料枠が利用できると思い、移行に至りました。

構成

GCPでは Container Cluster を利用し
この様な構成を取っております。

以下、GCP のチュートリアルに倣い構築手順まとめました。

gcloud デフォルト設定

以前無料枠を利用して構築した際の記事を参照ください。
Pod 単体の寂しい構成ではありますが汗

コンテナクラスタ作成

こちらも以前の記事同様、無料枠を利用すべく
初めに3ノードで作成し完了後、1ノードにします。

クラスターバージョンは 1.7.2 を指定しました。((2017年8月2日時点で cluster version 最新は 1.7.2))

1
$ gcloud container get-server-config

Fetching server config for us-west1-b
defaultClusterVersion: 1.6.7
defaultImageType: COS
validImageTypes:

  • CONTAINER_VM
  • COS
  • UBUNTU
    validMasterVersions:
  • 1.7.2
  • 1.6.7
    validNodeVersions:
  • 1.7.2
  • 1.7.1
  • 1.7.0
  • 1.6.7
  • 1.6.6
  • 1.6.4
  • 1.5.7
  • 1.4.9
  • コンテナクラスタ作成
1
2
3
4
5
$ gcloud container clusters create tocochan-cluster-free \
--cluster-version=1.7.2 \
--machine-type f1-micro \
--disk-size=30 \
--num-nodes=3
  • Node 数を 1 に設定
1
$ gcloud container clusters resize tocochan-cluster-free --size=1
  • 確認
1
$ gcloud container clusters describe tocochan-cluster-free | grep currentNodeCount

currentNodeCount: 1

現在のノード数が 1 であることが確認できました。
これで無料枠!

クラスタ作成後、コンテナクラスタの認証情報を取得し
kubectl でクラスタ接続し操作できる様にします。

1
$ gcloud container clusters get-credentials tocochan-cluster-free

Container Registory 登録

ローカルで起動したコンテナからイメージ作成し
GCP 上の Private な Docker リポジトリである Container Registory に登録します。

以下リポジトリを利用します。

https://github.com/kenzo0107/toda-tocochan-bus

  • Docker コンテナ起動
1
2
3
$ git clone https://github.com/kenzo0107/toda-tocochan-bus
$ cd toda-tocochan-bus
$ docker-compose up --build -d
  • 起動した Docker コンテナからイメージ作成・GCR へ push ((プロジェクトIDは 「mametsubuservice-175801」 ))
1
2
3
$ container_id=$(docker ps | grep [f]lask | awk '{print $1}')
$ docker commit $container_id gcr.io/mametsubuservice-175801/tocochan:latest
$ gcloud docker -- push gcr.io/mametsubuservice-175801/tocochan:latest

Pod 単体デプロイ as チュートリアル①

基本単体でデプロイすることは稀です。
単純に Pod 内のコンテナが異常停止した場合などを管理できない為です。
今回は無料運用の為と内容理解の為のチュートリアルとしての作業です。

  • pod.yaml
1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Pod
metadata:
name: tocochan
spec:
containers:
- image: gcr.io/mametsubuservice-175801/tocochan:latest
imagePullPolicy: Always
name: tocochan

Pod 単体デプロイ実行

1
$ kubectl create -f pod.yaml

Pod 状態確認

1
$ kubectl get pods
  • アクセス設定

flask は 5000 ポートで起動します。

1
2
3
4
$ kubectl port-forward tocochan 5000

Forwarding from 127.0.0.1:5000 -> 5000
Forwarding from [::1]:5000 -> 5000
  • ブラウザから http://localhost:5000 にアクセス

トップページが取得できることが確認できます。

Pod 名指定し削除

Pod 単体デプロイが確認できましたので削除しましょう。

1
$ kubectl delete pods tocochan

ReplicaSet デプロイ as チュートリアル②

Pod 単体作成した場合、 Pod に異常停止したとしても特に何もリカバーされません。
ReplicaSet では常に正常に動作するコンテナ数を管理しており
異常停止があった場合は新たに Pod を追加します。

こちらもチュートリアルとして記載してます。こちらは終わったら削除します。

  • replicaset.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
name: tocochan
spec:
replicas: 1 # 常に動作するコンテナ数
template:
metadata:
labels:
name: tocochan
spec:
containers:
- image: gcr.io/mametsubuservice-175801/tocochan:latest
imagePullPolicy: Always
name: tocochan

ReplicaSet デプロイ実行

1
$ kubectl create -f replicaset.yaml

ReplicaSet 確認

1
$ kubectl get rs -l name=tocochan

NAME DESIRED CURRENT READY AGE
tocochan-4006188167 1 1 1 10m

仮に Pods を削除しようとすると?

1
$ kubectl delete pods -l name=tocochan

起動コンテナが 0 になることなく、新たに作成されていることがわかります。

NAME READY STATUS RESTARTS AGE
tocochan-14s3b 1/1 Running 0 4s
tocochan-tsvfn 1/1 Terminating 0 5m

ReplicaSet 削除

1
$ kubectl delete rs tocochan

Deployment デプロイ

ReplicaSet のデプロイは k8s 上に履歴が残りません。
Deployment デプロイでは履歴が残り、現行バージョンに異常があった場合は
バージョンを簡単に戻せます。

これまでの問題を解決しているのが Deployment デプロイです。

  • deployment.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: tocochan
spec:
replicas: 1
template:
metadata:
labels:
name: tocochan
spec:
containers:
- image: gcr.io/mametsubuservice-175801/tocochan:latest
imagePullPolicy: Always
name: tocochan

Deployment デプロイ実行

--record を付けることで操作履歴を残すことができます。
履歴に残すことで問題がある場合に kubectl の操作で過去のバージョンに戻すことができます。

1
$ kubectl create -f deployment.yaml --record

Deployment 確認

1
$ kubectl get deployments -l name=tocochan

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
tocochan 1 1 1 1 10m

ReplicaSet 確認

1
$ kubectl get rs -l name=tocochan

NAME DESIRED CURRENT READY AGE
tocochan-2006588533 1 1 1 10m

Pod 確認

1
$ kubectl get pods -l name=tocochan

NAME READY STATUS RESTARTS AGE
tocochan-4006188167-3zrn9 1/1 Running 0 10m

デプロイ結果確認

1
$ kubectl rollout status deployment/tocochan

deployment “tocochan” successfully rolled out

正しく Rollout 公開されたことがわかりました。

履歴確認

1
$ kubectl rollout history deployment tocochan

deployments “tocochan”
REVISION CHANGE-CAUSE
1 kubectl create –filename=deployment.yaml –record=true

編集

1
$ kubectl edit deployment tocochan

vim が起動し deployment の編集が可能です。

1
2
- image: gcr.io/mametsubuservice-175801/tocochan:latest
+ image: gcr.io/mametsubuservice-175801/tocochan:v0.0.1

上記の様に編集し保存して終了すると

NAME READY STATUS RESTARTS AGE
tocochan-1297744065-2qb87 1/1 Terminating 0 15m
tocochan-4006188167-3zrn9 1/1 Running 0 10s

既存コンテナが停止中となコンテナが新たに立ち上がったことがわかります。

  • 履歴確認
1
$ kubectl rollout history deployment tocochan

REVISION CHANGE-CAUSE
1 kubectl create –filename=all.yaml –record=true
2 kubectl edit deployment tocochan

Rollout 履歴を確認すると 編集内容が追加されていることがわかります。

バージョンを戻す

REVISION 1 に戻します。

1
$ kubectl rollout undo deployment tocochan --to-revision=1
1
$ kubectl get pods -l name=tocochan

NAME READY STATUS RESTARTS AGE
tocochan-1297744065-2qb87 1/1 Terminating 0 6m
tocochan-4006188167-zswcj 1/1 Running 0 7s

先ほどと同様に既存コンテナが停止しコンテナが新たに起動している様子がわかります。

外部から接続できる?

ここまでの Pod の状態で以下コマンドを実行します。

1
$ kubectl get svc

NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes 10.3.240.1 443/TCP 3h

kubernetes の cluster-ip が割り振られている以外は特に IP が割り振られておらず
外部からアクセスできない状態です。

外部からアクセス出来る様、設定する必要があります。

Service 作成

外部向けの IP を設定し、外部から Pod にアクセス出来る様にルーティングします。

  • service.yaml
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
name: tocochan
spec:
type: LoadBalancer
selector:
name: tocochan
ports:
- port: 5000

Service 作成

1
$ kubectl create -f service.yaml

Service 確認

1
$ kubectl get svc

数分経過すると から IPになります。

NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes 10.3.240.1 443/TCP 10m
tocochan 10.3.240.70 xx.xxx.xxx.xxx 5000:32429/TCP 10m

以下コマンドで Web ページにアクセス出来ることが確認できます。

1
$ curl -v http://$EXTERNAL-IP:5000

ロードバランサー作成

ロードバランサーを立てることが可能です。
80 port で受け、5000 port をバックエンドに流します。

  • ingress.yaml
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: hello-world
spec:
rules:
- http:
paths:
- path: /*
backend:
serviceName: hello-world
servicePort: 5000

Ingress 作成

1
$ kubectl create -f ingress.yaml

Ingress 確認

1
$ kubectl get ingress tocochan

NAME HOSTS ADDRESS PORTS AGE
tocochan * yy.yyy.yy.yy 80 10m

以下アクセスで先ほど実施した curl -v http://$EXTERNAL-IP:5000 と同様の結果が取得できることがわかります。

1
$ curl http://$INGRESS_IP/

設定ファイルをまとめる

1
2
3
4
5
6
7
8
9
$ echo '---' > hyphen.txt; \
cat \
deployment.yaml \
hyphen.txt \
service.yaml \
hyphen.txt \
ingress.yaml \
> all.yaml; \
rm hyphen.txt
  • all.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: tocochan
spec:
replicas: 1
template:
metadata:
labels:
name: tocochan
spec:
containers:
- image: gcr.io/mametsubuservice-175801/tocochan:v0.0.1
imagePullPolicy: Always
name: tocochan
---
apiVersion: v1
kind: Service
metadata:
name: tocochan
spec:
type: LoadBalancer
selector:
name: tocochan
ports:
- port: 5000
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: tocochan
spec:
rules:
- http:
paths:
- path: /*
backend:
serviceName: tocochan
servicePort: 5000

以降、以下コマンドで OK !

1
$ kubectl create -f all.yaml --record

ドメイン取得

toda-tocochan-bus.tkfreenom で無料ドメイン取得し
Ingress の IP を設定し公開しています。

総評

ローカルで開発して〜デプロイ、という流れが本当に簡単になりました。
コンテナの理念遂行に kubernetes は大きく寄与しているなぁと実感しました。

以上です。
参考になれば幸いです。

I used Phinx, DB migration Tool on Docker!

I used Phinx, DB migration Tool on Docker!

Overview

This is Sandbox for DB Migration Tool Phinx.

Preparation

1
2
$ git clone https://github.com/kenzo0107/phinx-mysql
$ cd phinx-mysql

Create and Run Containers of Phinx, DB (MySQL).

1
$ make build
1
2
3
4
5
6
$ docker-compose ps

Name Command State Ports
-------------------------------------------------------------------------
phinxmysql_db-migrate_1 phinx --help Exit 0
phinxmysql_db_1 docker-entrypoint.sh mysqld Up 3306/tcp

The Container db-migrate is used as for one-off container, so its state is Exit 0.

Initialize Phinx Project

Phinx creates a default file called phinx.yml.

1
$ make init

In default setting, phinx select development environment.

1. Create Table

Create phinx definition file

1
2
3
4
5
6
$ make create DB=hogehoge CLASS=CreateTableUsers
$ make create DB=mogemoge CLASS=CreateTableMembers
...
...
created db/migrations/hogehoge/20170724065658_create_table_users.php
created db/migrations/mogemoge/20170724065738_create_table_members.php

Edit phinx definition file

  • db/migrations/hogehoge/20170724065658_create_table_users.php

Writing Migrations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

use Phinx\Migration\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;

class CreateTableUsers extends AbstractMigration
{
public function up()
{
// Automatically generated id is excluded, and primary key is set as user_id
$t = $this->table('users', ['id' => 'user_id']);

$t->addColumn('last_name', 'string', ['limit' => 10, 'comment' => '姓'])
->addColumn('first_name', 'string', ['limit' => 10, 'comment' => '名'])
->addColumn('last_kana_name', 'string', ['null' => true, 'limit' => 10, 'comment' => '姓(カナ)'])
->addColumn('first_kana_name', 'string', ['null' => true, 'limit' => 10, 'comment' => '名(カナ)'])
->addColumn('username', 'string', ['limit' => 20, 'comment' => 'ユーザ名'])
->addColumn('password', 'string', ['limit' => 40, 'comment' => 'パスワード'])
->addColumn('email', 'string', ['limit' => 100, 'comment' => 'Email'])
->addColumn('postcode', 'string', ['limit' => 10, 'comment' => '郵便番号'])
->addColumn('birthday', 'date', ['comment' => '誕生日'])
->addColumn('gender', 'integer', ['limit' => MysqlAdapter::INT_TINY, 'comment' => '性別(1:男 2:女 3:その他)'])
->addColumn('card_number', 'string', ['null' => true, 'limit' => 20, 'comment' =>'クレジットカードNo'])
->addColumn('description', 'text', ['null' => true, 'limit' => MysqlAdapter::TEXT_LONG, 'comment' =>'説明'])
->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated', 'datetime', ['null' => true])
->addIndex(['username', 'email'], ['unique' => true])
->create();
}

public function down()
{
$this->dropTable('users');
}
}
  • db/migrations/mogemoge/20170724065738_create_table_members.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

use Phinx\Migration\AbstractMigration;

class CreateTableMembers extends AbstractMigration
{
public function up()
{
$t = $this->table('members');
$t->addColumn('member_code', 'string', ['limit' => 20, 'comment' => '会員コード'])
->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated', 'datetime', ['null' => true])
->addIndex(['member_code'], ['unique' => true])
->create();
}

public function down()
{
$this->dropTable('members');
}
}

2. Add Column

Create phinx definition file

1
2
3
4
$ make create CLASS=AddTableUsersColumnsCity
...
...
created db/migrations/hogehoge/20170724065838_add_table_users_columns_city.php

Edit phinx definition file

  • db/migrations/hogehoge/20170724065838_add_table_users_columns_city.php

Add the column city after the column email.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

use Phinx\Migration\AbstractMigration;

class AddTableUsersColumnsCity extends AbstractMigration
{
public function up()
{
$t = $this->table('users');
$t->addColumn('city', 'string', ['limit' => 10, 'comment' => '都市', 'after' => 'postcode'])
->update();
}

public function down()
{
$t = $this->table('users');
$t->removeColumn('city')
->save();
}
}

Migration

1
$ make migrate
  • Result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
mysql> use hogehoge
mysql> show full columns from users;
+-----------------+--------------+-----------------+------+-----+-------------------+----------------+---------------------------------+---------------------------------+
| Field | Type | Collation | Null | Key | Default | Extra| Privileges | Comment |
+-----------------+--------------+-----------------+------+-----+-------------------+----------------+---------------------------------+---------------------------------+
| user_id | int(11) | NULL | NO | PRI | NULL | auto_increment| select,insert,update,references | |
| last_name | varchar(10) | utf8_general_ci | NO | | NULL || select,insert,update,references | 姓 |
| first_name | varchar(10) | utf8_general_ci | NO | | NULL || select,insert,update,references | 名 |
| last_kana_name | varchar(10) | utf8_general_ci | YES | | NULL || select,insert,update,references | 姓(カナ) |
| first_kana_name | varchar(10) | utf8_general_ci | YES | | NULL || select,insert,update,references | 名(カナ) |
| username | varchar(20) | utf8_general_ci | NO | MUL | NULL || select,insert,update,references | ユーザ名 |
| password | varchar(40) | utf8_general_ci | NO | | NULL || select,insert,update,references | パスワード |
| email | varchar(100) | utf8_general_ci | NO | | NULL || select,insert,update,references | Email |
| city | varchar(255) | utf8_general_ci | NO | | NULL || select,insert,update,references | |
| postcode | varchar(10) | utf8_general_ci | NO | | NULL || select,insert,update,references | 郵便番号 |
| birthday | date | NULL | NO | | NULL || select,insert,update,references | 誕生日 |
| gender | tinyint(4) | NULL | NO | | NULL || select,insert,update,references | 性別(1:男 2:女 3:その他) |
| card_number | varchar(20) | utf8_general_ci | YES | | NULL || select,insert,update,references | クレジットカードNo |
| description | longtext | utf8_general_ci | YES | | NULL || select,insert,update,references | 説明 |
| created | timestamp | NULL | NO | | CURRENT_TIMESTAMP || select,insert,update,references | |
| updated | datetime | NULL | YES | | NULL || select,insert,update,references | |
+-----------------+--------------+-----------------+------+-----+-------------------+----------------+---------------------------------+---------------------------------+


mysql> use mogemoge
mysql> show full columns from members;
+-------------+-------------+-----------------+------+-----+-------------------+----------------+---------------------------------+-----------------+
| Field | Type | Collation | Null | Key | Default | Extra | Privileges | Comment |
+-------------+-------------+-----------------+------+-----+-------------------+----------------+---------------------------------+-----------------+
| id | int(11) | NULL | NO | PRI | NULL | auto_increment | select,insert,update,references | |
| member_code | varchar(20) | utf8_general_ci | NO | UNI | NULL | | select,insert,update,references | 会員コード |
| created | timestamp | NULL | NO | | CURRENT_TIMESTAMP | | select,insert,update,references | |
| updated | datetime | NULL | YES | | NULL | | select,insert,update,references | |
+-------------+-------------+-----------------+------+-----+-------------------+----------------+---------------------------------+-----------------+

Rollback

1
$ make rollback

3. Create sample seeds for Multi Databases;

Create phinx definition file

1
2
3
4
5
6
$ make seed_create DB=hogehoge CLASS=UserSeeder
$ make seed_create DB=mogemoge CLASS=MembersSeeder
...
...
created ./db/seeds/hogehoge/UsersSeeder.php
created ./db/seeds/mogemoge/MembersSeeder.php

Edit phinx definition file

  • ./db/seeds/hogehoge/UsersSeeder.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

use Phinx\Seed\AbstractSeed;

class UsersSeeder extends AbstractSeed
{
public function run()
{
$t = $this->table('users');
$t->truncate();

$genders = [1,2,3];

$faker = Faker\Factory::create('ja_JP');
$d = [];
for ($i = 0; $i < 10; $i++) {
$d[] = [
'last_name' => $faker->lastName(10),
'first_name' => $faker->firstName(10),
'last_kana_name' => $faker->lastKanaName(10),
'first_kana_name' => $faker->firstKanaName(10),
'username' => $faker->userName(20),
'password' => sha1($faker->password),
'email' => $faker->email,
'postcode' => $faker->postcode,
'city' => $faker->city,
'birthday' => $faker->date($format='Y-m-d',$max='now'),
'gender' => $faker->randomElement($genders),
'card_number' => $faker->creditCardNumber,
'description' => $faker->text(200),
'created' => date('Y-m-d H:i:s'),
'updated' => date('Y-m-d H:i:s'),
];
}

$this->insert('users', $d);
}
}
  • ./db/seeds/hogehoge/MembersSeeder.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

use Phinx\Seed\AbstractSeed;

class MembersSeeder extends AbstractSeed
{
public function run()
{
$t = $this->table('members');
$t->truncate();

$faker = Faker\Factory::create('ja_JP');
$d = [];
for ($i = 0; $i < 10; $i++) {
$d[] = [
'member_code' => $faker->regexify('[0-9]{20}'),
'created' => date('Y-m-d H:i:s'),
'updated' => date('Y-m-d H:i:s'),
];
}

$this->insert('members', $d);
}
}

Run seed

1
$ make seed
  • Result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
mysql> use hogehoge;
mysql> select * from users;
+---------+-----------+------------+-----------------+-----------------+-------------+------------------------------------------+------------------------------+----------+--------------+------------+--------+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+
| user_id | last_name | first_name | last_kana_name | first_kana_name | username | password | email | postcode | city | birthday | gender | card_number | description | created | updated |
+---------+-----------+------------+-----------------+-----------------+-------------+------------------------------------------+------------------------------+----------+--------------+------------+--------+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+
| 1 | 佐々木 | 零 | ヤマダ | カナ | akira97 | e270038c94f231da7bca25dead3e386ba3984491 | hirokawa.rika@hotmail.co.jp | 1867251 | 佐々木市 | 1987-09-25 | 1 | 4024007116991463 | Dolor reiciendis fuga fugiat id molestiae eos. Dolores sint rem repudiandae perspiciatis. Ducimus aut mollitia aut asperiores laboriosam. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 2 | 宮沢 | 千代 | ノムラ | ヨウイチ | nagisa.taro | 695a90d1b84cf004357aad3eb37697b51afbf5cc | tanabe.hiroshi@kudo.org | 8639535 | 江古田市 | 1977-06-01 | 3 | 344103919563863 | Doloribus et recusandae quam accusantium pariatur nobis reiciendis quo. Eos quae et commodi quos accusamus ex. Ullam repellendus maiores vero sit sit et. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 3 | 斉藤 | 充 | ミヤケ | オサム | kana.suzuki | f309f34d08b4d0d686863fa38ed3d3af5e0b2104 | kana.kudo@mail.goo.ne.jp | 2763622 | 青田市 | 1997-01-30 | 1 | 4716886227252 | Veritatis voluptatem pariatur libero aut quia. Facere nemo quos enim amet ut ipsum sequi. Nobis natus et aspernatur aut. Natus pariatur deserunt voluptatum deserunt. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 4 | 吉田 | 太郎 | ウノ | ツバサ | naoko.uno | 45d04bda7ac79244c90a33ff68798b979138054a | taro.nagisa@hirokawa.com | 6099661 | 江古田市 | 2006-03-19 | 2 | 5372535333698250 | Nostrum velit nostrum eos magni. Reiciendis quos enim adipisci quisquam sed voluptas. Necessitatibus sint qui dolorem animi impedit consectetur commodi. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 5 | 野村 | 亮介 | サトウ | ミノル | rika.tanabe | dd3d50714c0775bfee453f7d9a9815ce26ba57db | wkudo@hotmail.co.jp | 6966314 | 渡辺市 | 1985-12-21 | 1 | 4929108488987091 | Id atque molestiae expedita omnis libero natus et. Repellendus ut tenetur molestias voluptas. Perspiciatis nisi et illum aut aut vel repudiandae. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 6 | 木村 | 裕美子 | タナカ | ヒロキ | hiroshi53 | 033bfd0493b72efd0ff60bc15c7eeb3b2e054501 | ztanabe@tanabe.biz | 3155238 | 山田市 | 1996-01-02 | 3 | 5476616628100007 | Assumenda consectetur ea sed et omnis alias fugiat quo. Porro nihil similique sint laudantium asperiores blanditiis. Error dolores vitae quia explicabo facilis deleniti distinctio. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 7 | 吉本 | 陽一 | キムラ | ヒデキ | akira27 | 51de6afc65f535ae58f927d698f07e60e04c7746 | rika59@suzuki.com | 6457702 | 田辺市 | 2010-04-12 | 2 | 5388155063289311 | Nesciunt qui beatae ut officia qui error autem. Temporibus alias earum ullam incidunt quo recusandae enim qui. Sed atque veritatis sed ad ullam qui. Repellendus est nostrum et pariatur. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 8 | 渡辺 | 翔太 | ササダ | クミコ | uno.momoko | fa2d16d5f2acffd5aeeaab6791fe64c9f70a9b2f | stanabe@uno.com | 5849600 | 伊藤市 | 2012-06-09 | 1 | 5274550197820022 | Odio quasi sunt tempora. Molestias aut qui sed quos beatae eum accusantium. Non dolores quam veniam et ab quidem nostrum repellendus. Qui ducimus et optio et. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 9 | 坂本 | 翔太 | ナカツガワ | ナオキ | akira.kudo | 4af41e536bf19fa3cb0527304adad0de76260e82 | suzuki.momoko@mail.goo.ne.jp | 8609563 | 宮沢市 | 2005-10-23 | 3 | 5231530310398512 | Qui id neque molestiae facere aut et consequatur. Delectus ea voluptatibus provident atque assumenda maxime eum. At quidem sint accusamus. Eaque sed voluptate quo sint non non. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
| 10 | 野村 | 翼 | ヒロカワ | ナオコ | taro.kudo | f8a63d0010c99d6403e0c1f458005b934ec03f8c | kana.tanabe@mail.goo.ne.jp | 5804069 | 桐山市 | 1988-12-25 | 2 | 5140671281503530 | Dolorem consequatur nulla alias perspiciatis ut. Tenetur modi cumque incidunt dolor. | 2017-07-25 12:22:50 | 2017-07-25 12:22:50 |
+---------+-----------+------------+-----------------+-----------------+-------------+------------------------------------------+------------------------------+----------+--------------+------------+--------+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+



mysql> use mogemoge;
mysql> select * from members;

+----+----------------------+---------------------+---------------------+
| id | member_code | created | updated |
+----+----------------------+---------------------+---------------------+
| 1 | 86190539096622228312 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 2 | 77322186584623078448 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 3 | 17169562241415794809 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 4 | 86738824931379981947 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 5 | 23125815173540252188 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 6 | 81839177491562485300 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 7 | 82938165381845652192 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 8 | 87208503292784158954 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 9 | 80172779107984112104 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
| 10 | 22825755425594828330 | 2017-07-25 12:22:51 | 2017-07-25 12:22:51 |
+----+----------------------+---------------------+---------------------+

Reference

5分でできる♪ AWS Lambda で EC2 Event を Slack 通知

以前 AWS EC2 メンテ通知のイベントチェックスクリプトを作成しました。
合わせて、対象インスタンスを停止・起動する様にしました。

これを AWS Lamda で Slack 通知させる様にし
毎朝メンテの必要なイベントがわかる様にしました。

事前準備

1
2
macOS%$ pip install lambda-uploader awscli
macOS%$ aws configure --profile <profile>

プロジェクト clone

1
2
3
4
5
6
7
8
macOS%$ git clone https://github.com/kenzo0107/AWSEC2Events2Slack
macOS%$ tree AWSEC2Events2Slack
.
├── README.md
├── event.json
├── lambda.json
├── lambda_function.py
└── requirements.txt

各種環境に合わせて情報編集

  • lambda.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "AWSEvent2Slack",
"description": "Notificate AWS events to Slack",
"region": "ap-northeast-1",
"handler": "lambda_function.lambda_handler",
"role": "arn:aws:iam::xxxxxxxxxxxx:role/lambda-check-events-to-slack",
"timeout": 60,
"memory": 128,
"variables":
{
"SLACK_INCOMING_WEBHOOK":"https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX",
"SLACK_CHANNEL":"#channel",
"SLACK_USERNAME":"AWSEvent2Slack",
"SLACK_ICON_URL":"http://i.imgur.com/6RCTdfi.png"
}
}
Item Explain
role EC2リソースをdescribeする権限を所持したポリシーをアタッチ
variables 通知先Slack情報

AWS Lambda へソースアップロード

1
2
3
4
5
macOS%$ lambda-uploader --profile <profile>

λ Building Package
λ Uploading Package
λ Fin

AWSコンソールより Lambda 確認

登録されていることがわかります。

テスト実行

イベントを取得しSlackに通知させる様にすることができました。

トリガー設定

CloudWatch スケジュール式で cron 設定し 毎朝届ける様に指定しました。

総評

lambda-uploader でのアップロードにより
ローカルで開発→テスト→デプロイ
とバージョン管理が明確になって良いです。

但し、一点気になる点はアップロード後、ソースがコンソール上で見えません。

具体的には

Lambda 関数 「AWSEvent2Slack」のデプロイパッケージが大きすぎて、インラインコード編集を有効にできません。ただし、関数を今すぐ呼び出すことはできます。

とコンソール上に表示されます。

前まで zip にまとめてアップロードするシェルを書いていたけど
その時はソースは見ることができました。

ローカルで挙動確認しておりコンソール上では見えなくても今のところ支障なしです。

以上
参考になれば何よりです。

Mackerel で Docker の起動状態確認

Mackerel で Docker の起動状態確認

概要

Docker コンテナがいつの間にか Exit していた!
なんてことを防ぐ為の Mackerel Agent の設定です。

mackerel-plugin-docker-state インストール

1
2
3
4
5
6
7
$ sudo mkdir -p /etc/mackerel-agent/conf.d
$ sudo curl https://raw.githubusercontent.com/ABCanG/mackerel-plugin-docker-state/master/mackerel-plugin-docker-state -o /etc/mackerel-agent/conf.d/mackerel-plugin-docker-state
$ sudo chmod +x /etc/mackerel-agent/conf.d/mackerel-plugin-docker-state
$ sudo cat <<'EOF'>/etc/mackerel-agent/conf.d/docker-state.conf
[plugin.metrics.docker-state]
command = "/etc/mackerel-agent/conf.d/mackerel-plugin-docker-state"
EOF

mackerel-agent.conf に include 設定追加

  • /etc/mackerel-agent/mackerel-agent.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
pidfile = "/var/run/mackerel-agent.pid"
root = "/var/lib/mackerel-agent"
verbose = false
apikey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
diagnostic = true

roles = ["xxxxxxxx:xxx"]

# include conf.d/*.conf
include = "/etc/mackerel-agent/conf.d/*.conf"

...
...

Mackrel Agent 再起動

1
$ sudo service mackerel-agent restart

グラフ確認

しばらくするとグラフが表示されます。
※0 or 1 のみのグラフなので積み重ねグラフの方が見やすかったです

※上記グラフではコンテナ 2 つが起動しています。

新規監視ルールを作成

running で検索すると出てきます。

3 分間の平均が 1 より低くなったら
コンテナが停止(Exit)と見なし通知する様にしました。

総評

今回たまたま Mackerel の入ったサービスを触る機会を頂きました。

Mackerel の様なマネージドサービスを利用するメリットは
監視サーバを監視しないで良い、
という省運用コストだなぁと改めて実感。

Install latest Nginx on Ubuntu

Install latest Nginx on Ubuntu

Just a memo.

Install Nginx

1
2
3
4
5
6
ubuntu%$ sudo su
ubuntu%$ curl http://nginx.org/keys/nginx_signing.key | sudo apt-key add -
ubuntu%$ sh -c "echo 'deb http://nginx.org/packages/ubuntu/ trusty nginx' >> /etc/apt/sources.list"
ubuntu%$ sh -c "echo 'deb-src http://nginx.org/packages/ubuntu/ trusty nginx' >> /etc/apt/sources.list"
ubuntu%$ apt-get update
ubuntu%$ apt-get install -y nginx

Install sysv-rc-conf

SysV is a runlevel configuration tool.

1
ubuntu%$ apt-get install -y sysv-rc-conf

Configure runlevel of nginx.

The command chkconfig is no longer available in Ubuntu. The equivalent command to chkconfig is update-rc.d. This command nearly supports all the new versions of ubuntu.

  • chkconfig —> sysv-rc-conf
1
ubuntu%$ sysv-rc-conf nginx on

Show runlevel of nginx

1
2
ubuntu%$ sysv-rc-conf --list nginx
nginx 0:off 1:off 2:on 3:on 4:on 5:on 6:off
Flask Python3 で 戸田市 tocoちゃんバスあと何分? Webアプリ作成♪

Flask Python3 で 戸田市 tocoちゃんバスあと何分? Webアプリ作成♪

Flask(フラスク) とは

Flask Official Site を参照すると冒頭に以下の文章があります。

1
Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions

Flask は Python の小規模なフレームワークで、 Werkzug や Jinja 2 をベースとしています。

何がいいの?

最小限の構成で簡単な Web アプリケーションが作成できることです。
django, Rails でも簡単に出来なくもないですが、さらに手順は短く容易です。

チュートリアル

1
2
3
4
5
6
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

Flask を利用する判断基準

  • 大規模なプロジェクトでない。(ファイル構成は自分 or チームで決定する必要がある為)
  • DB を使わない。使うにしても複雑な DB 設計でない。
  • 利用するライブラリが Python 製でその gateway として利用したい。

手始めに!

初心者向け記事は多数あるので割愛し
学習がてら Web アプリケーション作ったのでそちらをどうぞ ♪

実際動くものとそのソースを見た方がイメージ湧くと思います。

toco ちゃんバス あと何分?

戸田市のローカルバス toco ちゃんバスの停留所を指定しあと何分で来るかをカウントダウンするアプリです。
自分が使うので作ったら割と広まってきて嬉しい限り ♪

1
2
※ちなみに戸田市役所に確認し非公式ではありますが公開許可をいただいております。
※戸田市役所ご担当者様より「可能であれば最新の迂回情報なども載せていただけたら〜」という要望も頂きました♪

ソースはこちら ♪

ソースを参照頂けるとやってみたことがわかりやすいと思います。

やってみたこと

  • docker で flask ローカル開発環境作成
  • config ファイルからデータ呼び出し (config.py)
  • session 機能
  • Bootstrap 適用
  • superagent.js で非同期通信
  • flickity.js でフリッカブルに

本番動作環境

  • さくら VPS CentOS 6
  • httpd2.4 + wsgi + virtualenv + python3

さくら VPS には CakePHP や SpringBoot のプロジェクトが乗っかっていたりとやりたい放題の環境として所持しています。

開発期間

ほぼほぼ 1 週間。仕事の昼休みと日曜大工で 10 時間足らずで公開出来ました。
どちらかというと js 側の学習コストが掛かった感じ。

総評

今回作成したユーザ情報を管理しない、
DB を持たないアプリにはうってつけでした。
構成が複雑になりすぎず丁度良かったです。

やはり大きな規模のプロジェクトには django が適しています。

個人的に戸田市役所に電話して Web アプリ公開の許可を頂く、
という承認申請が出来、地域貢献できる喜びが非常に大きかったです。

今後、バージョンアップしたいと思います。

Flask 利用に際して参考になれば何よりです。

以上です。

Apache 2.2.15 → 2.4.25   PHP 5.6 → 7 へアップデート on CentOS 6.9

Apache 2.2.15 → 2.4.25 PHP 5.6 → 7 へアップデート on CentOS 6.9

概要

PHP5 利用していますか?

PHP5.6 のセキュリティサポート期限は 31 Dec 2018
Supported Versions 参考

Apache/PHP アップデート、腰が重かったのですが
個人契約サーバなら誰にも迷惑かけないしいいか ♪

ということで
放置気味にされた Apache2.2.15/PHP5.6 の個人のサーバを
アップデートすべく実施した内容をまとめました。

三行まとめ

  • SoftwareCollection を利用し現存 Apache/PHP を残したまま、アップデート版を共存させ切り替え。のち古い Apache/PHP 削除
  • 必要モジュール (MySQLi, PHPRedis)インストール
  • PHP 7 で廃止された PHP5.6 機能やシンタックスを修正

SoftwareCollection とは?

公式サイト によると
以下の様に説明されています。

  • 英語

    Software Collections give you the power to build, install, and use multiple versions of software on the same system, without affecting system-wide installed packages.

  • 日本語

    ソフトウェアコレクションは、システム全体でインストールされたパッケージに影響を与えることなく、同じシステム上に複数のバージョンのソフトウェアを構築、インストール、使用する能力を提供します。

同じシステム上に複数バージョンのソフトウェアをインストールできる様になる、
ということです。

SoftwareCollection インストール

1
$ sudo yum install centos-release-scl

httpd24 関連のモジュールインストール

1
2
3
4
$ sudo yum-config-manager --enable rhel-server-rhscl-6-rpms
$ sudo yum install httpd24-httpd httpd24-httpd-devel httpd24-mod_proxy_html httpd24-mod_session httpd24-mod_ssl
$ sudo scl enable httpd24 bash
$ sudo service httpd graceful
1
2
3
$ httpd -v
Server version: Apache/2.4.25 (Red Hat)
Server built: Apr 12 2017 06:35:50

RHSCL リポジトリ利用可設定

1
$ sudo yum-config-manager --enable rhel-server-rhscl-7-rpms

php7 関連モジュールをインストール

1
2
3
4
5
6
7
8
# yum install -y scl-utils
# yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
# yum install -y http://rpms.remirepo.net/enterprise/remi-release-7.rpm
# yum install -y php70
# yum install -y php70-php-mysqlnd
# yum install -y php70-php-curl
# yum install -y php70-php-simplexml
# yum install -y php70-php-devel php70-php-gd php70-php-json php70-php-mcrypt php70-php-mbstring php70-php-opcache php70-php-pear php70-php-pecl-apcu php70-php-pecl-geoip php70-php-pecl-imagick php70-php-pecl-json-post php70-php-pecl-memcache php70-php-pecl-xmldiff php70-php-pecl-zip php70-php-pspell php70-php-soap php70-php-tidy php70-php-xml php70-php-xmlrpc

mysqli インストール

1
# yum --enablerepo=remi-php70 install php-mysqli

PHP7 用 phpredis インストール

1
2
3
4
5
6
7
8
9
# cd /usr/local/src
# git clone https://github.com/phpredis/phpredis.git
# cd phpredis
# git checkout php7
# phpize
# ./configure
# make
# make install
# echo 'extension=redis.so' > /etc/opt/rh/rh-php70/php.d/redis.ini

php-fpm 再起動

1
# /etc/init.d/php70-php-fpm restart

httpd 再起動

1
# /etc/init.d/httpd24-httpd restart

ここまでで PHP7 で動作する環境が整っているかと思います。
エラーログを見ながら修正に当たってください。

PHP 7 で廃止された構文を修正

PHP Parse error: syntax error, unexpected ‘new’ (T_NEW)

  • &= new <クラス名> の指定が不可となり、 = new <クラス名> にする必要があります。
1
2
3
&= new Class

= new Class

PHP Fatal error: Cannot use ‘String’ as class name as it is reserved

  • PHP7 では class String, Int と型名の Class を作成できなくなりました。

自分は以下の様に修正しました。
※ 適宜プロジェクトのコーディングルールに則ってご変更ください。

1
2
3
class String {

class Stringer {
1
2
3
class Int {

class Intger {

プロジェクトによってはもっと色々出てくると思いますので
適宜修正ください。

総評

放置されがちになるミドルウェアのアップデートは小まめにやっておきたいですね。
脆弱性の定期的な棚卸しせねば

業務でアップデートするのであれば
アップデートする環境を別途用意してアップデートする、
そこでミドルウェア、アプリケーションのコードレベルでのアップデート手順をまとめ
本番環境で実施。

機能(url)毎に正しく動いたものだけプロキシで PHP7 へ流す
というのもアリかなと思います。

以上です。

参考