開発時にdockerを使ってmavenをいい感じに動かす

dockerを使ってJavaの開発しているとき、時折docker buildの遅さが気になる。 遅さというのは、ほぼ mvn package 実行時の依存解決にかかる時間に対して言っています。 というのも、例えば以下のようなDockerfileがあったとき

# Dockerfile
FROM maven:3.6.2-jdk-13 as builder

COPY . /usr/src/

WORKDIR /usr/src/
RUN mvn -B package

FROM openjdk:13-alpine

COPY --from=builder /usr/src/target/example-1.0.0.jar /
ENV CLASSPATH /example-1.0.0.jar

ENTRYPOINT []
CMD ["java", "-jar", "Example"]

依存量にもよりますが、 mvn -B package のところで2分半~3分半ほどかかります。 docker buildを叩く度、毎回数分以上かかるのは、さすがに開発効率が悪すぎる。 ~/.m2/ を永続化しようとしても、docker buildではvolume mountできませんし。 ちょっと、解決策を模索してみました。

docker buildのビルドキャッシュを活用してみる

docker build時には、ビルドキャッシュが効くはずなので、効くようにしてみました。

# Dockerfile
FROM maven:3.6.2-jdk-13 as builder

WORKDIR /usr/src/
# pom.xmlだけCOPYし、先に依存解決
COPY ./pom.xml /usr/src/
RUN mvn -B dependency:resolve dependency:resolve-plugins

# ビルドで必要となるファイルのみCOPY
COPY ./src/ /usr/src/src/
RUN mvn -B package

FROM openjdk:13-alpine

COPY --from=builder /usr/src/target/example-1.0.0.jar /
ENV CLASSPATH /example-1.0.0.jar

ENTRYPOINT []
CMD ["java", "-jar", "Example"]

結果、初回のdocker buildに4分半ほど、2回目以降のdocker buildでは1分~1分半ほどまで高速化しました。 mvn -B dependency:resolve dependency:resolve-plugins では、 mvn -B package に必要な依存が含まれていないため、 mvn -B package 時にまだ依存解決をする必要があるため、分単位で時間がかかっているようです。 ということは、依存解決のために mvn -B package を先に実行してやれば良いはず。

# Dockerfile
FROM maven:3.6.2-jdk-13 as builder

WORKDIR /usr/src/
# pom.xmlだけCOPYし、先に依存解決
COPY ./pom.xml /usr/src/
RUN mvn -B package; echo ""

# ビルドで必要となるファイルのみCOPY
COPY ./src/ /usr/src/src/
RUN mvn -B package

FROM openjdk:13-alpine

COPY --from=builder /usr/src/target/example-1.0.0.jar /
ENV CLASSPATH /example-1.0.0.jar

ENTRYPOINT []
CMD ["java", "-jar", "Example"]

mvn -B package; echo "" としている理由は、依存をキャッシュすることが目的のため、ビルドエラーなんかでキャッシュできなかったら悲しいからです。 これで、初回のdocker buildeに2分半~3分半ほど、2回目以降のdocker buildは5秒ほどになりました。 ローカルで実行したのと変わりない時間で実行できるようになりました。

ビルド用のコンテナを立ててみる

ビルドキャッシュを効かせれば目的は達成できますが、別解としてビルド用のコンテナを立ててみる方法を試しました。 今回はMakefileと組み合わせてみます。

# Makefile
CONTAINER_NAME=builder

.PHONY: package
package: up-builder
    docker exec "${CONTAINER_NAME}" mvn -B package

.PHONY: up-builder
up-builder:
    if [ -z "$$(docker ps --filter "name=${CONTAINER_NAME}" --all --quiet)" ]; then \
        docker run --rm --detach \
            --name "${CONTAINER_NAME}" \
            --volume '${CURDIR}:/workspace' \
            --workdir '/workspace' \
            maven:3.6.2-jdk-13 \
            tail -f /dev/null; fi

.PHONY: down-builder
down-builder:
    docker stop "${CONTAINER_NAME}"
# Dockerfile
FROM openjdk:13-alpine

COPY ./target/example-1.0.0.jar /
ENV CLASSPATH /example-1.0.0.jar

ENTRYPOINT []
CMD ["java", "-jar", "Example"]

これで make package を実行すれば、自動的にビルド用のコンテナがバックグラウンドで起動し、コンテナは止めない限り起動し続けるため、あまり難しいことを考えずとも2回目以降のキャッシュが効きます。 また実質的に、 maven:3.6.2-jdk-13 を使ってビルドした成果物を openjdk:13-alpine にCOPYしているため、docker buildの結果生成されるdocker imageは同じものだと期待できます。 このようにすると、ビルドキャッシュを効くようにしたときと同様、初回のdocker buildeに2分半~3分半ほど、2回目以降のdocker buildは5秒ほどになります。

とはいえ、例えばwindowsで開発しているときに、誤ってwindows上で mvn package を実行した成果物をCOPYし、誤ったdocker imageを生成してしまうリスクがあります。 個人的には、Dockerfile内でなんとかできるなら、そっちのほうが好みですね。 ケースバイケースですけれど。

goen v0.0.7について

以下の不具合を修正しました:

  • Update/Delete操作時に、RowKeyがnilのとき、パッチからクエリが生成されていなかった
  • Many to Oneのforeign keyを指定したフィールドに対して、参照先エンティティが存在しないとき、IncludeLoaderでpanicすることがある

いずれもテストを追加しているため、詳細はテストコードを参照してください。

https://github.com/kamichidu/goen/commit/7b0161e86d671529fe32281824da5c706ce7e6bb#diff-37dcb2da9fb3728a0bca1148008ad9d9R146 https://github.com/kamichidu/goen/commit/7b0161e86d671529fe32281824da5c706ce7e6bb#diff-37dcb2da9fb3728a0bca1148008ad9d9R337 https://github.com/kamichidu/goen/commit/947d1218b143c0536f175e593fbb5e614dd62553#diff-0317820e89b935f54fc3e2037d5bd0cdR439

goen v0.0.6について

BulkCompilerに対して、MaxChunksが指定できるようになりました。 これまでは大量のプレースホルダを持つクエリを生成したとき、クエリ実行時にエラーになっていました。

package main

import (
    "database/sql"
    "fmt"

    "github.com/kamichidu/goen"
    _ "github.com/kamichidu/goen/dialect/sqlite3"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    _, err = db.Exec(`create table users (user_id, name)`)
    if err != nil {
        panic(err)
    }

    dbc := goen.NewDBContext("sqlite3", db)
    dbc.Compiler = goen.BulkCompiler
    for i := 0; i < 1000; i++ {
        dbc.Patch(goen.InsertPatch("users", []string{"user_id", "name"}, []interface{}{i, "testing"}))
    }

    // 実行時に以下エラーが発生する
    // panic: too many SQL variables
    if err := dbc.SaveChanges(); err != nil {
        panic(err)
    }

    type Res struct {
        Count int
    }
    var res []Res
    rows, err := dbc.Query(`select count(*) as count from users`)
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    if err := dbc.Scan(rows, &res); err != nil {
        panic(err)
    }
    fmt.Printf("number of rows %d\n", res[0].Count)
}

BulkComoilerに対してMaxChunksを指定することで、回避が可能になりました。

package main

import (
    "database/sql"
    "fmt"

    "github.com/kamichidu/goen"
    _ "github.com/kamichidu/goen/dialect/sqlite3"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    _, err = db.Exec(`create table users (user_id, name)`)
    if err != nil {
        panic(err)
    }

    dbc := goen.NewDBContext("sqlite3", db)
    dbc.Compiler = &goen.BulkCompilerOptions{
        // 最大で100パッチを1つのクエリにまとめる
        MaxPatches: 100,
    }
    for i := 0; i < 1000; i++ {
        dbc.Patch(goen.InsertPatch("users", []string{"user_id", "name"}, []interface{}{i, "testing"}))
    }

    if err := dbc.SaveChanges(); err != nil {
        panic(err)
    }

    type Res struct {
        Count int
    }
    var res []Res
    rows, err := dbc.Query(`select count(*) as count from users`)
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    if err := dbc.Scan(rows, &res); err != nil {
        panic(err)
    }
    fmt.Printf("number of rows %d\n", res[0].Count)
}

1つのクエリに含めることができるプレースホルダ数をパラメータにしなかった理由は、ドライバごとに取得可否が分かれるためです。 最大プレースホルダ数の指定で確実に問題が回避できるようなら、将来的に別のオプションを追加するかもしれません。 現状では、1つのクエリにマージするパッチ数を調整することで、実行時エラーを回避する方針としています。

PowerShell + msys2でのMakefileによるdocker volume mountについて

Windowsでmsys2のmake等を使いながら、Makefileでdockerを使ったコマンドを定義するときの、volume mountがうまいこといかなかったのでメモ。 PowerShellと書いているけれど、msys2のminttyからmakeを叩いたときにも通用する。

hoge:
    docker run --rm --volume '${CURDIR}:/workspace' alpine:3.9 ls /workspace

これだとmsys2がパスをうまいことやろうとして、 /workspaceC: 始まりの文字列に変換したりするので、うまいこといかない。 この挙動は、コンテナ内のパス指定を、 // で開始するようにすればなんとかなる。

ただし、dockerのvolume mountが絡んだとき、docker for windowsでは C: 始まりのwindowsの形式によるパスを期待するため、 ${CURDIR} ではうまいことvolume mountできない。 ${CURDIR} はmakeの変数で、例えば /c/Users/kamichidu みたいなパスになる。 PowerShellから直接dockerを叩くときなんかは、 docker run -v ${PWD}:/workspace みたいにすれば良いのだけど、makeで簡単なコマンドをまとめたいときには使えない。

結論、以下のようにする。

BASEDIR=$(subst /,\\,$(subst /c,C:,${CURDIR}))

hoge:
    docker run --rm --volume '${BASEDIR}://workspace' alpine:3.9 ls //workspace

私の場合、windowsだけでなく、linuxmacなんかでも共通のMakefileを使いたいので、最終的に以下のようにした。

ifeq (${OS},Windows_NT)
    BASEDIR=$(subst /,\\,$(subst /c,C:,${CURDIR}))
else
    BASEDIR=${CURDIR}
endif

hoge:
    docker run --rm --volume '${BASEDIR}://workspace' alpine:3.9 ls //workspace

EdgeRouterXへのL2TP/IPSec設定

自宅ではEdgeRouterXを使ってPPPoEでインターネットに接続しています。 今回EdgeRouterXにL2TP/IPSecでのVPN設定を実施したので、その備忘録。

参考にしたのは以下4サイトです:

基本的には、公式サイトに出ている手順通りにやればすんなりと通ります。 この記事は、すんなりと通らなかった私のための備忘録です。

記事の内容を1行にまとめると、 EdgeRouterXのファームバージョンがv1.7.1だったせいで、L2TP/IPSecの設定が正しくてもVPNできない。v2.0.6に上げたらいけた。

さて、公式サイトの記載に従ってL2TP/IPSecの設定を入れてはみたものの、

と、VPNしたい端末からは悉くVPNできませんでした。 設定を何度も見直して、パケットをキャプチャして調査してみて、IKEのレスポンスがEdgeRouterXから返却されてないっぽいことがわかりました。 また参考にしたサイトでは、EdgeRouterXのファームはv1.9.1という記載があったため、私の使っているv1.7.1から結構上がっていることがわかりました。 それでなんとなくファームかなぁと思ったため、文鎮化するリスクを取ってファームを現在の最新であるv2.0.6に上げました。 (ついでなので、boot imageも上げました。)

ファームを上げた後に試してみたところ、

でした。 ファーム上げた時点でVPNがうまく通ったので、ちょっと嬉しかった。

WindowsからのVPNでは、VPNアダプタのセキュリティ設定から、

  • 暗号化を必須
  • CHAPv2を有効化

する必要がありました。

以上、備忘録でした。

よくあるCompareという関数の理解の方法について

何個かのプログラミング言語触ってると、よくcompareという名前の比較関数を目にする。 この記事内では、goの文法で書くが、大抵の言語で当てはまる考え方だと思う。

// if a == b then 0
// if a < b then negative integer
// if a > b then positve integer
func Compare(a, b int) int {
    return a - b
}

このような実装で、int同士の大小比較をする比較関数が書ける。 この記事で書きたいのは、このような比較関数を使うとき、比較関数から返される数値を扱うためのわかりやすい理解の仕方について。

実際に上で書いた比較関数を使うときには、このようにする。

if Compare(a, b) == 0 {
    // aとbの大小関係が等しいときの処理
}
if Compare(a, b) < 0 {
    // aがbより小さいときの処理
}
if Compare(a, b) > 0 {
    // aがbより大きいときの処理
}

個人的ポイントは、比較関数を扱うときは常に0との比較をすること。 なぜかというと、形式的に上記if文を展開することができるから。 注釈を加えて再度if文を記述してみると、以下のようになる。

if Compare(a, b) == 0 { // a == 0
    // aとbの大小関係が等しいときの処理
}
if Compare(a, b) < 0 { // a < b
    // aがbより小さいときの処理
}
if Compare(a, b) > 0 { // a > b
    // aがbより大きいときの処理
}

非常にわかりやすく、覚えやすくなりませんかね。 なぜこうなるか、という理屈の話をすると、数式で見ると明らかです。

a - b = 0
a - b < 0
a - b > 0

単にbを右辺に移動させるだけっていうね。

ここまで書いた話は、数値比較だろうと文字列比較だろうと変わりなく使えます。 比較関数の返り値と0以外を比較したときには、当てはまらない場合があるため、基本戦略として比較関数の返り値は0と比較しておくと安心です。

以下余談。

C言語の比較関数などは、ここまで書いた話と違っている場合があり、具体的には比較関数の返り値の意味が正負反転することがあります。 こういうものは、大抵が言語の文化に合わせてあることが多いため、対象とする言語が正論理なのか負論理なのか、と合わせて覚えるとわかりやすいと思います。