【Golang】正規表現チェックツールRegoについて

Go言語で正規表現のコードを書く際に、どういう表現ができるんだっけ?と思う時ありますよね。

Ruby(Ruby on Rails)の正規表現のコードを書く際はRubularというツールを使う方は多いと思いますが、Golangでも同じようなRegoという正規表現チェックツールがあって、よくそれを使っています。

regoio.herokuapp.com

使ってみる

f:id:moritamorie:20210119005710g:plain

分かりやすい対話的なインターフェースが便利です。

Regoを使うメリット

画面上のGo RegExp Quick Referenceに記載されている記法は Rubular とまったく同じなので一見Regoを使う必要はなさそうにみえます。

しかし、以下のRegoの実装のコードを見ると正規表現コンパイル・実行はGolangの実装で行っているので、Golangのプロジェクトの場合は事前にRegoで確認しておくと実際のコードの中で正規表現を使用した時に動かなくなる懸念が少なくて済みそうです。

https://github.com/stevedomin/rego/blob/master/rego.go#L38-L49

Regoで正規表現を決めた後は

正規表現を決めたら、コードの中で動かす前に、goreという対話シェル(REPL)を使って事前にRegexpの関数を動作確認しておくとスムーズです。(Regexpの様々な関数を使い慣れている方の場合は不要かと思います。)

というのもRegexpには挙動が分かりづらい関数も多く、例えばFindAllStringSubmatchを使って、以下の文字列からiMacという部分だけを抜き出すような場合、事前に関数の戻り値を把握しておきたいです。

アイマック(iMac)
r := regexp.MustCompile(`(\S+)\((\S+)\)`)
result := r.FindAllStringSubmatch("アイマック(iMac)", -1)
result[0][2] => "iMac"

ちなみにresultにはこんな値が入ります。

[][]string{
  []string{
    "アイマック(iMac)",
    "アイマック",
    "iMac",
  },
}

goreに関しては別の記事で紹介してみたいと思います。

Webサイトクローラー(スクレイピング)の目的・メリット、適切な設定とは

とあるWebサイトクローラーを開発していて、クローラーにとって適切な設定にしたいと考えることがありました。

クローラーの設定のように、選択肢が複数あり複雑なものは判断が難しくなります。そのようなものは、まず考えるための観点(例えば、セキュリティ面ではどう設定すると良いかなど。)から考えるとすんなり進みます。観点があると、一度に考えることが絞られるので考えやすくなります。

具体的にどのような観点かというと、クローラーの目的・得られる利益(メリット)に合った観点を洗い出すと良いです。

今回は一般的なWebサイトクローラーを元に観点を整理してみたので共有してみたいと思います。

f:id:moritamorie:20210118022541p:plain

(結論) Webサイトクローラー適切な設定とは

結論からですが、クローラーの目的から考えると、得られる情報により良い意思決定ができるような設定が適切という結論になり、一般的には以下のような観点で考えると良いと思います。

  • 情報が誤りがなく、正確であること
  • 情報に漏れやダブリがないこと
  • 情報が新しいこと
  • 情報が得られない状況を作らないこと
  • ※これらは一般的なケースであり、他の観点はケース次第。

この結論に行き着いた過程を以下に記載していきます。

クローラースクレイピング)の用途

まず、目的を明確にするには、一般的なクローラーの用途を考えると分かると思い、考えてみました。

用途は、数多くのWebサイトの中から目的の情報をもったWebサイトを巡回・解析し、自分達や世の中の人にとって意味のある情報を抽出・変換する、というものです。

"意味のある情報"という表現だと抽象的すぎるので、具体的な例を挙げると

  • あるモノの価格が上昇していることが分かれば、需要があがっているので、自社でもっと生産しよう・このモノを作る事業に参入しよう、と判断ができる。
  • 逆に、多くの競合他者が販売するあるモノの価格が下がっていることが分かれば、価格競争が始まって供給過多になっていそう。価格競争に巻き込まれると利益が出にくいので、他の事業に注力しよう・事業の撤退を検討し始めよう、と判断できる。

といった情報が、意味のある情報になるかと思います。

f:id:moritamorie:20210118023959p:plain

クローラースクレイピング)の目的

この具体例から、"意味のある情報" というのを別の言葉で言い換えると、"意思決定の判断材料になる情報"ということになるかと思います。

そうだとすると、意思決定の判断材料になる情報収集を機械的にすることがクローラーの目的だということが分かります。より良い意思決定に繋がる判断材料を得ることができれば、仕事などの物事を前に進めたり、より良い成果を得られるようになるかと思います。

より良い成果というと、「売上を上げる」や「早く仕事を終わらせられる」など仕事・ビジネス上での成果だけに聞こえますが、日常生活に関しても同じように言えます。

  • 安くモノ(洋服や家電など)が買える
  • 目的地に早く辿り着く方法を知れる
  • 自分好みのレストランを見つけられる

また成果に関しても

  • 量的(定量的)(安く買えるとか、早く着くとか)な観点
  • 質的(定性的)(美味しい食べ物、好みの洋服とか)な観点

のように観点を明確にすると考えやすくなるかと思います。

適切なクローラーの設定とは

このようにクローラーの目的まで考えると、適切なクローラーの設定とは、より良い成果を得るための意思決定ができるような情報を収集できる設定にするということが分かります。

意思決定にするための情報の精度を元にいくつか観点を洗い出してみました。

  • 情報が誤りがなく、正確であること
  • 情報が新しいこと
  • 情報に漏れやダブリがないこと
  • 情報が得られない状況を作らないこと

一般的にはこのような観点で考えると、適切なクローラーの設定ができそうです。

他の観点は、具体的になケースに依るかと思います。

  • セキュリティ面はどの程度か?
    • どの程度機密性が高い情報を扱うか等に依る
      • 例えば、公開されているサイトの情報を収集するのであれば情報が漏れることはリスクではないので、セキュリティ要件は低い。
  • パフォーマンス面はどの程度の速さを求めるのか?
    • どのくらいの量のデータを扱うのか等に依る
      • 例えば、収集する情報の量が少ないなら、さほど1件あたり早い時間で収集できなくて良い。
  • コストはどの程度までかけて良いか?
    • 情報を得ることでどの程度メリットがあるのか等に依る。
      • 例えば、直接売上には直結しない情報であれば、比較的コストが低いスペックのマシンで運用できる必要がある。

f:id:moritamorie:20210118125834p:plain

【golang】migrateでDBマイグレーションをする

go言語で実装したWebアプリケーションサイトのDBマイグレーションツールとしてgolang-migrate/migrateを使っていて、そこそこ便利につかえているので紹介してみたいと思います。

migrateを使うメリット

migrateは、Go言語で作成されたDBマイグレーションツールの1つです。

migrateを使うことで得られるメリットは

の3つかと思います。

migrateを使うことにした背景

golang のORMフレームワークgorm を使っているのですが、CLIが提供されていないためDBマイグレーションのタイミングをコントロールしづらいので、 migrate でカバーすることにしました。

migrate を採用する際の注意点としては、gormは独自のDSLスキーマ定義を記載することでDBシステム間(MySQLPostgreSQL等)の差異を吸収してくれますが、 migrateの場合は定義ファイルに書かれたDDLを実行するだけなので、DBシステムの差異を意識して定義ファイルを記載する必要があります。

環境/前提

  • Ubuntu 18.04 LTS デスクトップ
  • PostgreSQLインストール済
  • PostgreSQLに"sample"という名前のデータベース作成済

インストール

インストール方法が、README.mdにもGETTING_STARTED.md にも書いていないので、分かりづらいですが以下に記載されています(2021/1/13時点)。今後READMEに移動するかもしれないので、都度最新の情報を確認してください。

https://github.com/golang-migrate/migrate/tree/master/cmd/migrate

ビルド済のバイナリをインストールする方法やaptでもインストールできますが、今回はgo getでインストールしました。

$ go get -tags 'postgres' -u github.com/golang-migrate/migrate/cmd/migrate

使ってみる

最初は以下のPostgreSQLチュートリアルをやってみるのがてっとり早いかと思います。 migrate/TUTORIAL.md at master · golang-migrate/migrate · GitHub

マイグレーションの実行に失敗したらどうなるか等把握するために色々試してみたいので、チュートリアルにの内容にちょっと手を加えて進めてみます。

マイグレーションの定義ファイルを作る

1つ目のマイグレーションとして、ユーザーテーブルを作って、インデックスを貼ってみたいと思います。

を指定して migrate createを実行すると、000001_create_users.(up/down).sql という2つのSQLファイルができます。

$ migrate create -ext sql -dir db/migrations -seq create_users
~/golang-migrate-sample/db/migrations/000001_create_users.up.sql
~/golang-migrate-sample/db/migrations/000001_create_users.down.sql

作成されたup/down.sqlファイルにそれぞれ以下のようにCREATE TABLEDROP TABLEDDLを書いてみます。PostgreSQLDDLでもトランザクションが使えるので使ってみます。

BEGIN;
CREATE TABLE IF NOT EXISTS users(
   id serial PRIMARY KEY,
   username VARCHAR (50) UNIQUE NOT NULL,
   age INT NOTNULL
);
CREATE INDEX on users (age);
COMMIT;
DROP TABLE IF EXISTS users;

同様に、コメントテーブルのマイグレーションも書いてみます。

$ migrate create -ext sql -dir db/migrations -seq create_comments
~/golang-migrate-sample/db/migrations/000002_create_comments.up.sql
~/golang-migrate-sample/db/migrations/000002_create_comments.down.sql
CREATE TABLE IF NOT EXISTS comments(
   id INT PRIMARY KEY,
   body VARCHAR (50) NOT NULL,
   user_id INT NOT NULL
);
DROP TABLE IF EXISTS comments;

ファイル/ディレクトリ構成確認

ファイル/ディレクトリ構成としては以下のようになっている状態かと思います。

└── db
    └── migrations
        ├── 000001_create_users.down.sql
        ├── 000001_create_users.up.sql
        ├── 000002_create_comments.down.sql
        └── 000002_create_comments.up.sql

マイグレーションの実行

現状は以下の図のように、1と2の定義ファイルがあるだけでDBにはテーブルが1つもない状態です。 f:id:moritamorie:20210113022223p:plain

さっそくマイグレーションを実行してみたいと思います。

migrate up 1を実行し、ユーザーテーブルをCREATEする

PostgreSQLの接続URLを環境変数に入れて、バージョンを1つ上げてみます。

$ export POSTGRESQL_URL='postgres://postgres:password@localhost:5432/sample?sslmode=disable'
$ migrate -path db/migrations -database ${POSTGRESQL_URL} up 1

以下の図のように、000001_create_users.up.sqlだけが実行され、schema_migrationsテーブル(version: 1のレコードが1件)とusersテーブル(内容は空)ができました。

f:id:moritamorie:20210113022153p:plain

マイグレーションを実行するとschema_migrationsテーブルに

  • 現在のバージョン(version)
  • エラーが発生するとdirty: true、しなければ dirty: false

が保存されます。

migrate down 1を実行し、ユーザーテーブルをDROPする

同様にバージョンを1つ下げてみます。

$ migrate -path db/migrations -database ${POSTGRESQL_URL} down 1

すると以下の図のようにusersテーブル、とschema_migrationsテーブルのレコードがなくなり、DROPできたことが確認できます。

f:id:moritamorie:20210113022101p:plain

わざと失敗してみる

先程の000001_create_users.up.sqlを以下のように修正し、CREATE TABLEはそのままでCREATE INDEXは失敗するようにしてみます。

BEGIN;
CREATE TABLE IF NOT EXISTS users(
   id serial PRIMARY KEY,
   username VARCHAR (50) UNIQUE NOT NULL,
   age INT NOTNULL
);
CREATE INDEX on users, (age);
COMMIT;
$ migrate -path db/migrations -database ${POSTGRESQL_URL} up 1
error: migration failed: ","またはその近辺で構文エラー (column 22) in line 7: BEGIN;
CREATE TABLE IF NOT EXISTS users(
   id INT PRIMARY KEY,
   username VARCHAR (50) UNIQUE NOT NULL,
   age INT NOT NULL
);
CREATE INDEX on users, (age);
COMMIT;
 (details: pq: ","またはその近辺で構文エラー)

すると、以下のようにトランザクションが効いてユーザーテーブルが作られずに、 schema_migrationsdirtytrue になることが確認できます。

f:id:moritamorie:20210113020549p:plain

dirty: trueの状態を修正する方法をドキュメントから探していると forceを指定することで、dirtyfalse の状態(1つ前のバージョン)に強制的に変更できるという記載があったのでそれをやろうとしましたが、こちらのissueにバージョン1で失敗した場合はバージョン0にはできないと書いてあったので、仕方なく schema_migrationsファイルは手動で削除しました。

バージョン2以降で失敗した場合には、以下のように force バージョンを指定することで、 schema_migrationsvarsion: 1, dirty: falseの状態にできます。

$ migrate -path db/migrations -database ${POSTGRESQL_URL} force 1
migrate upを実行し、ユーザーテーブルとコメントテーブルを作成

最後にmigrate up 実行することで、未実行のマイグレーションが全て実行されることを確認しました。000001_create_users.up.sqlは修正は元に戻して実行しています。

$ migrate -path db/migrations -database ${POSTGRESQL_URL} up
1/u create_users (25.104164ms)
2/u create_comments (53.025987ms)

f:id:moritamorie:20210113021917p:plain

使ってみた所感

ちょっとしたハマりどころもあるので、そんなによく作り込まれているという感じではなく万人受けはしなさそうだけど、インタフェースがシンプル(up、down、forceくらい)なので使い勝手がよく

  • 小規模のシステム
  • 個人開発

といった使用用途には良い気がしました。少なくとも個人開発で使っている分には困るケースには当たってないです。

サンプル

作ったサンプルのURLを載せておきますので、よかったら参考にしてみてください! https://github.com/moritamori/golang-migrate-sample

参考資料

Goのインタフェースを使って、共通のメソッド・属性を持つ構造体を作る

最近、Goで開発をしている時に、インタフェースと構造体の理解が曖昧でハマることがあったので、理解を深めるためのサンプルコードを書いてみました。

要件

サンプルコードは、掃除機の共通のインタフェース(操作)

  • ①電源を入れる(TrunOn)
  • ②電源を切る(TrunOff)
  • ③掃除する(Clean)

を持つダイソン掃除機、マキタ掃除機の構造体を作り、構造体毎に異なるインタフェース(操作)の実装を持たせ、main関数から呼び出すというものです。

構造体のデータに関しては同じだと面白くないので、ダイソン掃除機だけワイヤレス掃除機という想定で、バッテリー残量という属性を持っているとします。

要件を簡単に以下の図にまとめてみました。

f:id:moritamorie:20210104205952p:plain

実装

以下のように実装してみました。

ポイントは最後のmain()関数の中でダイソン掃除機、マキタ掃除機の構造体ポインタのレシーバの型が共通のインタフェース(IVacuum)になっている点です。

package main

import "fmt"

// インタフェース
type IVacuum interface {
    TurnOn()
    TurnOff()
    Clean()
}

// ダイソン掃除機・マキタ掃除機共通の構造体
type Vacuum struct {
    Maker   string
    Model   string
    Serial  string
    Running bool
}

// ダイソン掃除機の構造体
type Dyson struct {
    Battery int
    Vacuum
}

// マキタ掃除機の構造体
type Makita struct {
    Vacuum
}

// ダイソン掃除機構造体のメソッド定義
func NewDyson(Maker string, Model string, Serial string) *Dyson {
    dyson := &Dyson{Battery: 100}
    dyson.Maker = Maker
    dyson.Model = Model
    dyson.Serial = Serial
    return dyson
}

func (d *Dyson) TurnOn() {
    d.Running = true
    d.Battery = 1
    fmt.Println("ダイソンの掃除機:電源ONになりました。")
}

func (d *Dyson) TurnOff() {
    d.Running = false
    d.Battery = 0
    fmt.Println("ダイソンの掃除機:電源OFFになりました。")
}

func (d *Dyson) Clean() {
    if d.Running && d.Battery > 0 {
        fmt.Println("ダイソンの掃除機で掃除する!")
        d.Battery -= 1
    } else if d.Battery <= 0 {
        fmt.Println("ダイソンの掃除機:バッテリーがありません充電してください。")
    } else {
        fmt.Println("ダイソンの掃除機:電源を入れてください。")
    }
}

// マキタ掃除機構造体のメソッド定義
func NewMakita(Maker string, Model string, Serial string) *Makita {
    makita := new(Makita)
    makita.Maker = Maker
    makita.Model = Model
    makita.Serial = Serial
    return makita
}

func (d *Makita) TurnOn() {
    d.Running = true
    fmt.Println("マキタの掃除機:電源ONになりました。")
}

func (m *Makita) TurnOff() {
    m.Running = false
    fmt.Println("マキタの掃除機:電源OFFになりました。")
}

func (m *Makita) Clean() {
    if m.Running {
        fmt.Println("マキタの掃除機で掃除する!")
    } else {
        fmt.Println("マキタの掃除機:電源を入れてください。")
    }
}

func main() {
    // ダイソン掃除機、マキタ掃除機を作り、同じ操作をする
    vacuums := []IVacuum{
        NewDyson("Dyshon", "V7 Fluffy Origin", "839YQBX"),
        NewMakita("Makita", "AC100V", "192IWID"),
    }
    for _, vacuum := range vacuums {
        vacuum.Clean()
        vacuum.TurnOn()
        vacuum.Clean()
        vacuum.Clean()
        vacuum.TurnOff()
        fmt.Println("")
    }
}

このコードを実行すると以下のように出力され、main関数から呼び出した際に同じ操作をしているにも関わらず、異なる振る舞いをしていることがわかります。

$ go run main.go
ダイソンの掃除機:電源を入れてください。
ダイソンの掃除機:電源ONになりました。
ダイソンの掃除機で掃除する!
ダイソンの掃除機:バッテリーがありません充電してください。
ダイソンの掃除機:電源OFFになりました。

マキタの掃除機:電源を入れてください。
マキタの掃除機:電源ONになりました。
マキタの掃除機で掃除する!
マキタの掃除機で掃除する!
マキタの掃除機:電源OFFになりました。

値渡しと参照渡し

ハマっていたとき、値渡しと参照渡しを意識せずに以下のように書いていて、あれ、Runningがfalseになるのはなぜだ、、と思い、調べ始めたのでした。

type Dyson struct {
    Running bool
}

func NewDyson() Dyson {
    return Dyson{}
}

func (d Dyson) TurnOn() {
    d.Running = true
}

func (d Dyson) IsRugging() bool {
    return d.Running == true
}

func main() {
    vacuum := NewDyson()
    vacuum.TurnOn() // ここでRunningがtrueになる想定だった
    fmt.Println(vacuum.IsRugging()) // が、falseが返る
}

$ go run main.go
false

TurnOn, IsRunningメソッドのレシーバが値渡しになっているので、コンストラクで初期化したまま(Running: false)になっていて、メソッドが呼ばれた時にはコピーが渡されているので、変更が反映されないという現象が発生していました。

近年、RubyJavaのWeb開発案件が多かったせいか、値渡しなのか参照渡しなのかをあまり意識できなくなっているなと実感しました。。

f:id:moritamorie:20210106133659p:plain

これが何の役にたつのか?

最近goで作ったアプリで、とある商品のWebサイトをクロールして、実行結果を自分にメール通知するというアプリを作った際に似たような実装をしました。

クローラー自体は

  • インタフェース(操作)は同じ
  • 持つデータ構造もほぼ同じ
  • Webサイト毎にHTMLの構造が異なり、クロールの仕方が異なるのでクロールの仕方(Crawlメソッドの実装)は異なる

という特徴があり、ほぼサンプルと同じような実装にするとクローラを管理する機能をシンプルに実装できました。

f:id:moritamorie:20210105010235p:plain

PostgreSQL10 から PostgreSQL12 へのアップグレード手順(pg_upgrade)

古いPostgreSQLのバージョンがインストールされている開発マシンがあったので、pg_upgradeの公式ドキュメントを参考に、PostgreSQL10.15 から PostgreSQL12.5 に移行してみました。

ドキュメントに よると

pg_upgradeは8.4.X以降から現時点のPostgreSQLのメジャーリリース(スナップショット版やβリリースを含む)へのアップグレードをサポートします。

ということで、古めのバージョンであってもpg_upgradeで一気にバージョンを上げられます。

前提

  • 環境: Ubuntu 18.04 LTS デスクトップ
  • 移行前バージョン: PostgreSQL10.15
  • 移行後バージョン PostgreSQL12.5

バックアップを取得

念の為全DatabaseのDumpバックアップを取っておきます。

$ pg_dumpall > backup.sql

postgresql12をインストール

UbuntuへのPostgresqlのインストールは、公式の手順に従いました。

GPG keyを追加して、aptのsource.listにリポジトリを追加します。

$ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
$ echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |sudo tee  /etc/apt/sources.list.d/pgdg.list

このリポジトリには

といった一般的なパッケージやサードパーティアドオンのような様々なパッケージが含まれているようです。

apt updateして、postgresql12をインストールします。

$ sudo apt update
$ sudo apt -y install postgresql-12 postgresql-client-12

インストールが終わったら、念の為がpostgresql12のクラスタサービスが起動しているか確認します。

$  systemctl status postgresql@12-main.service 
● postgresql@12-main.service - PostgreSQL Cluster 12-main
   Loaded: loaded (/lib/systemd/system/postgresql@.service; indirect; vendor pre
   Active: active (running) since Sat 2021-01-02 12:13:16 JST; 14min ago
  Process: 19223 ExecStop=/usr/bin/pg_ctlcluster --skip-systemctl-redirect -m fa
  Process: 22868 ExecStart=/usr/bin/pg_ctlcluster --skip-systemctl-redirect 12-m
 Main PID: 22877 (postgres)
    Tasks: 7 (limit: 4915)
   CGroup: /system.slice/system-postgresql.slice/postgresql@12-main.service
           ├─22877 /usr/lib/postgresql/12/bin/postgres -D /var/lib/postgresql/12
           ├─22882 postgres: 12/main: checkpointer   
           ├─22883 postgres: 12/main: background writer   
           ├─22884 postgres: 12/main: walwriter   
           ├─22885 postgres: 12/main: autovacuum launcher   
           ├─22886 postgres: 12/main: stats collector   
           └─22887 postgres: 12/main: logical replication launcher

バージョンを戻すことを考慮に入れて一応postgresqlサービスの状態を確認したうえで、停止しておきます。

$ systemctl is-enabled postgresql
enabled

$ sudo systemctl stop postgresql.service

アップグレード

postgresユーザでpg_upgradeを実行します。

$ sudo su postgres

 /usr/lib/postgresql/12/bin/pg_upgrade \
     --old-datadir=/var/lib/postgresql/10/main \
     --new-datadir=/var/lib/postgresql/12/main \
     --old-bindir=/usr/lib/postgresql/10/bin \
     --new-bindir=/usr/lib/postgresql/12/bin \
     --old-options '-c config_file=/etc/postgresql/10/main/postgresql.conf' \
     --new-options '-c config_file=/etc/postgresql/12/main/postgresql.conf'

整合性チェックを実行しています。
-----------------------------
Checking cluster versions                                   ok
Checking database user is the install user                  ok
Checking database connection settings                       ok
Checking for prepared transactions                          ok
Checking for reg* data types in user tables                 ok
Checking for contrib/isn with bigint-passing mismatch       ok
Checking for tables WITH OIDS                               ok
Checking for invalid "sql_identifier" user columns          ok
Creating dump of global objects                             ok
Creating dump of database schemas
                                                            ok
Checking for presence of required libraries                 ok
Checking database user is the install user                  ok
Checking for prepared transactions                          ok
Checking for new cluster tablespace directories             ok

この後pg_upgradeが失敗した場合は、続ける前に新しいクラスタを
initdbで再作成する必要があります。

アップグレードを実行しています。
------------------
Analyzing all rows in the new cluster                       ok
Freezing all rows in the new cluster                        ok
Deleting files from new pg_xact                             ok
Copying old pg_xact to new server                           ok
Setting next transaction ID and epoch for new cluster       ok
Deleting files from new pg_multixact/offsets                ok
Copying old pg_multixact/offsets to new server              ok
Deleting files from new pg_multixact/members                ok
Copying old pg_multixact/members to new server              ok
Setting next multixact ID and offset for new cluster        ok
Resetting WAL archives                                      ok
Setting frozenxid and minmxid counters in new cluster       ok
Restoring global objects in the new cluster                 ok
Restoring database schemas in the new cluster
                                                            ok
ユーザリレーションのファイルをコピーしています
                                                            ok
Setting next OID for new cluster                            ok
Sync data directory to disk                                 ok
Creating script to analyze new cluster                      ok
Creating script to delete old cluster                       ok

アップグレードが完了しました
----------------
オプティマイザーの統計は、pg_upgrade では転送されません。そのため
新サーバーを起動した後、./analyze_new_cluster.sh を動かすことを検討してください。


このスクリプトを実行すると、旧クラスタのデータファイル ./delete_old_cluster.shが削除されます:

アップグレードが終わったので、postgresユーザから一般ユーザに戻します。

  exit

新しくPostgreSQLバージョンのクラスタのポートがデフォルトポートではないため、postgresql.confのport番号を変更します。

$ sudo vim /etc/postgresql/12/main/postgresql.conf
port = 5433                             # (change requires restart)
=> port = 5432                             # (change requires restart)

同様にpostgresql10のポートも変更します。

$ sudo vim /etc/postgresql/10/main/postgresql.conf
port = 5432                             # (change requires restart)
=> port = 5433                             # (change requires restart)

もし、他にもpostgresql.confを変更している箇所があったり、rootユーザパスワードの認証なしでログインできる等の設定をpg_hba.confで行っている場合は、PostgreSQL12の方でも手動変更しておきます。

再度postgresサービスを起動し、接続を確認してみます。

sudo systemctl start postgresql.service

再度、postgresユーザに戻りpsqlコマンドがpostgresql12の方に接続しに行っているか確認します。

$ sudo su postgres
$  psql -c "SELECT version();"
PostgreSQL 12.5 (Ubuntu 12.5-1.pgdg18.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, 64-bit

続いて、pg_upgrade実行時に生成されたanalyze_new_cluster.shを実行し、オプティマイザの統計情報を収集しておきます。

$ ./analyze_new_cluster.sh
vacuumdb: データベース"calendar_development"の処理中です: 最適化のための情報を最小限生成します(1対象)
vacuumdb: データベース"calendar_development"の処理中です: 最適化のための情報を最小限生成します(1対象)
〜〜〜
vacuumdb: データベース"world_myroom_development"の処理中です: 最適化のための情報をデフォルト数(全て)生成します

Done

postgresql10の削除

接続が確認できたらpostgresql10は不要なので削除します。

$ sudo apt-get remove postgresql-10 postgresql-server-dev-10
$ sudo rm -rf /etc/postgresql/10/

再度、postgresユーザに戻って、pg_upgrade時に生成されたdelete_old_cluster.shを実行しておきます。

$ sudo su postgres
./delete_old_cluster.sh

参考資料

【React hooks】"Warning: Can't perform a React state update on an unmounted component."の止め方

React hooksで実装したSPAアプリをChromeで動かした際に、以下のWarningが表示されていたので対処してみました。

Warning: Can't perform a React state update on an unmounted 
component. This is a no-op, but it indicates a memory leak in 
your application. To fix, cancel all subscriptions and asynchronous 
tasks in %s.%s","a useEffect cleanup function","\n in UserPage 

例えば、以下のようなコードで useEffect の中でAPIにアクセスして、返ってきたら state にセットするようなアプリケーションでよく発生するWarningかと思います。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [user, setUser] = 
    useState({ 
      id: 1, 
      first_name: "安倍", 
      last_name: "晋三"
    });

  useEffect(() => {
    axios.get('https://reqres.in/api/users/2')
      .then((response) => {
        if (!!response.data) {
          setUser(response.data.data)
        }
      })
  }, []);

  return (
    <div className="App">
      <header className="App-body">
        ID:{user.id}、名前:{user.first_name} {user.last_name}
      </header>
    </div>
  );
}

export default App;

前提

  • React: 17.0.1
  • Axios: 0.21.1

Warningの発生原因

Warningを出しているReactのコードを確認したところ以下のコメントが書いてありました。

Updating state from within an effect cleanup function is sometimes a necessary pattern, e.g.: 1. Updating an ancestor that a component had registered itself with on mount. 2. Resetting state when a component is hidden after going offscreen.

例えば、APIからレスポンスが返ってくるまでに時間がかかっていて、レスポンスを受け取る前に別ページに遷移した等、コンポーネントがアンマウントされている場合に、stateを保存する(上記の例だとsetUser)タイミングで保存できずにWarningが表示されるようです。

Warningの表示を管理している変数のコメントを確認したところ、開発環境でしか表示されないWarningのようなので、本番環境におけるメモリリークを回避するためにも開発時に見かけたタイミングで対処しておきたいWarningです。

対応パターン①:アンマウント状態を変数で管理する

以下のようにアンマウント時にisMounted変数を更新するクリーンナップ関数を追加したうえで、既にアンマウントされている状態の場合に保存しないようにしたらWarningがなくなりました。

useEffect の戻り値に関数が指定されている場合、それはクリーンナップ関数になり、コンポーネントのアンマウント時に1度だけ実行されます。

useEffect(() => {
  let isMounted = true
  axios.get('https://reqres.in/api/users/2')
    .then((response) => {
      if (!!response.data) {
        if (isMounted) {
          setUser(response.data.data)
        }
      }
    })

  return () => { isMounted = false };
}, []);

クリーンナップ関数の詳細は、公式ドキュメントに記載されています。 reactjs.org

メリット

この対応のメリットとしては

  • 様々なケースで対処できる
    • ケース①:APIからレスポンスが遅い場合
    • ケース②:APIからレスポンスを取得した後の処理が長い場合

デメリット

この対応のデメリットとしては

  • 管理する変数が1つ増える

ため汎用的ではありますが、コードが煩雑になり、保守しずらくなることかと思います。

対応パターン②Ajaxをキャンセルする

APIからレスポンスが返ってくるのが遅いことが主な原因であれば、以下のように axios のキャンセルトークン等を利用してAPIリクエストをキャンセルすると良さそうです。

useEffect(() => {
  const source = axios.CancelToken.source()
  axios.get('https://reqres.in/api/users/2')
    .then((response) => {
      if (!!response.data) {
        setUser(response.data.data)
      }
    })
  return () => { 
    source.cancel("APIはキャンセルされました");
  };
}, []);

メリット・デメリット

この対応は、変数( source )が増えてはいますが、成功時の処理の中(thenの中)では使われないので、対応パターン①とは反対に保守がしやすいというメリットはあるものの、汎用的ではないためAjaxをキャンセルしてWarningがなくなるケース以外ではWarningが残るというデメリットがありそうです。

参考資料

DockerのRootless modeで、rootユーザ以外でデーモンを実行してみた

Docker 19.03(2019年7月リリース)で入った機能の1つであるRootless modeを触ってみました。

Rootless モード(Rootless mode)は Docker デーモンとコンテナを root 以外のユーザが実行できるようにするもので、デーモンやコンテナ・ランタイムにおける潜在的脆弱性を回避してくれるものです。

CVE-2019–5736のようなコンテナの中からホスト上の権限を奪取することが出来る脆弱性が見つかっているので、特に実運用環境ではRootless modeで実行されるようにした方が良さそうです。

NTT須田さんの記事に詳しく書いてあるのでリンクを貼っておきます。 medium.com

Rootlessモードの場合とそうでない場合で、Dockerdのプロセスとコンテナ内で動くプロセスのユーザがどうなっているか確認してみました。

Rootlessモードではない場合

Dockerをインストール済のホストマシンで、起動中のDockerデーモンの実行ユーザを確認してみました。

psをコマンドでdockerdの実行ユーザを確認すると

f:id:moritamorie:20201222015545p:plain

たしかにrootで実行されていそうです。

次にUSERを指定しないでプロセスを起動しているコンテナに入って実行中のプロセス情報を確認してみます。

f:id:moritamorie:20201222024151p:plain

コンテナ内のプロセスの実行ユーザもrootになっていました。

Rootlessモードの場合

手元のマシンでは、既にrootlessモードではないdockerdが起動しているので、Amazon EC2(ubuntu 20.04 LTS, t2.nano )環境にDockerをインストールしrootlessモードで起動してみます。

前提条件は環境によって異なります。Ubuntuでは事前準備は不要ですが、他の環境で試される方はドキュメントを参考にセットアップしてみてください。

$ curl -fsSL https://get.docker.com/rootless | sh
# Installing stable version 20.10.1
# Missing system requirements. Please run following commands to
# install the requirements and run this installer again.
# Alternatively iptables checks can be disabled with SKIP_IPTABLES=1

cat <<EOF | sudo sh -x
apt-get install -y uidmap
EOF

uidmapのインストールが必要のようなので、インストールしてみました。

$ sudo apt update
$ cat <<EOF | sudo sh -x
apt-get install -y uidmap
EOF 

再度、dockerをインストールしてみます。

$ curl -fsSL https://get.docker.com/rootless | sh
# Installing stable version 20.10.1

〜〜〜

[INFO] Creating /home/ubuntu/.config/systemd/user/docker.service
[INFO] starting systemd service docker.service
+ systemctl --user start docker.service
+ sleep 3
+ systemctl --user --no-pager --full status docker.service
● docker.service - Docker Application Container Engine (Rootless)
     Loaded: loaded (/home/ubuntu/.config/systemd/user/docker.service; disabled; vendor preset: enabled)

〜〜〜

[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger ubuntu`

[INFO] Make sure the following environment variables are set (or add them to ~/.bashrc):

export PATH=/home/ubuntu/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock

無事にdockerのインストールができました。インストールプロセスの中でsystemctlコマンドを使ってdocker.serviceを起動しているので、既にDockerデーモンは起動している状態です。

dockerコマンドにパスを通すために~/.bashrc環境変数を追加して、sourceコマンドで反映してみます。

export PATH=/home/ubuntu/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock
$ source ~/.bashrc

Dockerデーモンの実行ユーザを確認してみました。psをコマンドでdockerdの実行ユーザを確認すると f:id:moritamorie:20201223000227p:plain

ubuntuユーザで実行されていて、rootではなくなっていることが確認できました。

次にUSERを指定しないでプロセスを起動しているコンテナに入って実行中のプロセス情報を確認してみます。

f:id:moritamorie:20201223003935p:plain

おや。rootユーザで実行されていて、Rootless modeではない場合とユーザと同じになっていそうです。

以下の Docker公式のブログ記事を読んでみると

ルートレスモードは、最初にユーザ名前空間を作成し、リマップされた名前空間で既にデーモンを起動することを除いて、同様の方法で動作します。デーモンとコンテナは、ホストとは異なる同じユーザー名空間を使用します。

https://i2.wp.com/cdn-images-1.medium.com/max/2000/1*SfAokC2YQ-f04Wc2WhSRCw.png?ssl=1

という内容が記載されているので、コンテナ内ではrootユーザではあるものの、ホストマシンのubuntuユーザにマッピングされて実行されているようなので、万が一コンテナが乗っ取られてもホスト上の権限を奪取できない状態になっていそうです。

参考記事