- 投稿日:2020-02-27T18:47:30+09:00
自分なりのGoアプリケーションDockerfileのベストプラクティス
最終的に作成されるDocker Imageが軽量になること
Docker Buildにかかる時間を短縮するを目標としている
フォルダ構成
$ tree . . ├── Dockerfile ├── Makefile ├── pkg_a │ ├── pkg_a.go │ └── pkg_a_test.go ├── pkg_b │ ├── pkg_b.go │ └── pkg_b_test.go ├── go.mod ├── go.sum ├── main.go標準的なGoアプリケーションの構成
ライブラリ管理はGo Modulesを使います。Dockerfile
Dockerfileは以下のようになっています。
工夫点はコメントで番号つけてます。詳細は後述します# ① FROM golang:1.13 as builder # ② ENV CGO_ENABLED=0 ENV GOOS=linux ENV GOARCH=amd64 WORKDIR /go/<アプリのリポジトリ名> # ③ COPY go.mod go.sum ./ RUN go mod download COPY . . # ④ RUN make # ⑤ FROM alpine RUN apk add --no-cache ca-certificates # ⑥ COPY --from=builder /go/<アプリのリポジトリ名>/app /app CMD ["/app"]説明
① multi-stage buildsを使いDocker Imageの軽量化を行う
Dockerにはmulti-stage buildsという機能があり、これを使うと1つのDockerfile内で複数の
FROM
を指定し、ビルドのベースとなるImageを分けることができます。これによりアプリケーションのビルド時のみ必要となるパッケージなどを、最終的なImageから省き、Imageサイズを削減するというのが簡単に行えます。
Goの場合、Goランタイムはビルド時のみ必要でバイナリにビルドして実行する場合は不要なので、最初のbuild用のImageベースはgolang, 最終的なImageのベースはalpineとしています。
Imageのサイズとしてはgolang:1.13が5.5GB、alpine:latestが2.68MBなので、そのままgolangを最終的なイメージベースとするより5GB以上のサイズ削減になります。
② Goのビルド設定
ENV CGO_ENABLED=0 ENV GOOS=linux ENV GOARCH=amd64でビルドの設定を行なっています
特に重要なのが
CGO_ENABLED=0
で静的バイナリの生成を指定してます。
これ指定しないと、alpineで実行するときにエラーになります。③ 依存ライブラリのダウンロードはなるべくキャッシュをきかせる
なるべくDocker build時にキャッシュをきかせて、ビルド速度を上げるために、リポジトリ内の全てのファイルをコピーする前に、Go Modules系のファイルだけコピーして依存ライブラリのダウンロードを行なっています。
Dockerのビルドは1行を1レイヤーとして考え、そのレイヤーで変更がなければキャッシュを利用し、変更があれば、それ以降のレイヤー全てがキャッシュ使われなくなります。
そのため実際は以下でも動くのですが、アプリのソースコードを修正しただけでも毎回依存ライブラリのダウンロードが発生してしまいます。
- COPY go.mod go.sum ./ - RUN go mod download - COPY . . - RUN make + COPY . . + RUN makeそのため、事前に
go.mod
,go.sum
だけをコピーしています。
この場合、依存ライブラリを新たに追加しない限りは、毎回キャッシュが使われてDocker buildにかかる時間を大幅に短縮できます。④ Makefile使う
RUN go build -o app
とかでもいいんですが、Makefileを用意しておくと、Dockerfileがスッキリして見栄えがよくなるかと思います。ちなみにMakefileは以下のようになっています。
all: test build setup: go get test: go test ./... build: go build -o app⑤ 最終的なImageのベースはalpineを利用する
multi-stage buildについては前述の通りです。
mulit-stage buildで最終的なImageのベースとしては、サイズを意識するならalpine一択かなと思ってます。
(運用時にコンテナ内にexecで入ってデバッグ作業とかしたいなら別のImageとかの方が良いかもですが)⑥ 別のステージで作成したアプリのバイナリファイルをコピーする
FROM golang:1.13 as builder COPY --from=builder /go/<アプリのリポジトリ名>/app /app
FROM golang:1.13 as builder
で最初のgolang:1.13を使ったbuildの間にbuilder
というaliasをつけてます。これを使って
COPY --from=builder /go/<アプリのリポジトリ名>/app /app
とすることで、golang:1.13でビルドしたアプリのバイナリファイルをalpineの中に持ってくることができます。
- 投稿日:2020-02-27T17:11:29+09:00
Go言語製 抽選Botの精度が怪しいので検証してみた話
いきさつ
ACALL株式会社では、slackで抽選をするbotがあって、会議のファシリテーターとか
誰か担当を決めるときにそのbotを使って決めています。Go + AWS LambdaでSlackの社員抽選botを作った話
https://blog.acall.jp/2019/11/develop-slack-lottery-bot/このbotの精度がどうも怪しい。
私と弊社CTOがやたら当選する気がする!最初は「まあプログラムのランダム性ってそんなもんだよね。ハハハ」なんて言ってたもんですが、
なかなかの頻度で当選するので、これはもしやバグが寝てるのでは・・・・と思い、精度を検証してみました。前提
- goのバージョンは1.14rc1を使用
検証内容
抽出処理の確認
まず、抽選botのランダム処理は下記のようになっています。
func lotteryOneUserFromUsers(userIDs []string) string { rand.Seed(time.Now().UnixNano()) userID := userIDs[rand.Intn(len(userIDs))] return userID }userIDsには抽選対象のユーザーが入っているわけですが、この処理自体はシンプルで怪しいところはない。
というわけで、この処理を複数回実行して確率を算出します。確認用コード
下記のようなコードを用意しました。
probability.gopackage main import ( "math/rand" "time" "fmt" "strconv" ) func main() { userIDs := []string{"CTO","EM","山田","鈴木","藤田","田中","中島","北川","西山","川口","遠藤"} var lottery_cnt map[string]int = make(map[string]int) const LOOP_CNT int = 1000 //初期化 for _, s := range userIDs { lottery_cnt[s] = 0 } var userID string for i := 0; i < LOOP_CNT; i++ { userID = lotteryOneUserFromUsers(userIDs) lottery_cnt[userID] += 1 } fmt.Println("抽選実施回数は" + strconv.Itoa(LOOP_CNT) + "回です") fmt.Println("----------------------------") sum := 0 for k, v := range lottery_cnt { fmt.Println(k + " : " + strconv.FormatFloat((float64(v)/float64(LOOP_CNT)*100.0), 'f', 1, 64) + "% (" + strconv.Itoa(v) + "回)") sum += v } } // 抽選処理実体 func lotteryOneUserFromUsers(userIDs []string) string { rand.Seed(time.Now().UnixNano()) userID := userIDs[rand.Intn(len(userIDs))] return userID }LOOP_CNTで抽選する回数を設定しています。
いざ検証
さて、上記のコードを使ってまずは1,000回の抽選で検証してみます。
~/d/s/lottery ❯❯❯ go run probability.go 抽選実施回数は1000回です ---------------------------- 山田 : 7.5% (75回) 鈴木 : 10.8% (108回) 西山 : 7.3% (73回) 遠藤 : 8.8% (88回) 中島 : 10.0% (100回) 北川 : 9.9% (99回) 川口 : 9.9% (99回) CTO : 7.8% (78回) EM : 10.1% (101回) 藤田 : 9.8% (98回) 田中 : 8.1% (81回)
ほうほう・・・それなりのゆらぎがあります。
ちなみにEMが私です。
理論上は回数を重ねれば重なるほど確率は均一になるはずなので、回数を増やします。
次は1万回でやってみます。~/d/s/lottery ❯❯❯ go run probability.go 抽選実施回数は10000回です ---------------------------- 遠藤 : 8.6% (856回) EM : 9.4% (936回) 山田 : 9.2% (921回) 鈴木 : 9.1% (914回) 藤田 : 9.1% (908回) 中島 : 9.6% (960回) 川口 : 9.3% (927回) CTO : 8.9% (889回) 田中 : 9.0% (900回) 北川 : 9.2% (921回) 西山 : 8.7% (868回)
前回よりも均一化されてきました。ほぼ9%前後です。
では一気に、50万件まで増やしてみます。~/d/s/lottery ❯❯❯ go run probability.go 抽選実施回数は500000回です ---------------------------- 北川 : 9.1% (45357回) 西山 : 9.1% (45277回) 川口 : 9.1% (45562回) CTO : 9.0% (45103回) 山田 : 9.2% (45825回) 藤田 : 9.1% (45574回) 田中 : 9.1% (45283回) EM : 9.2% (45765回) 鈴木 : 9.1% (45417回) 中島 : 9.1% (45630回) 遠藤 : 9.0% (45207回)
ほぼゆらぎはなくなりましたね!
50万回やれば問題ないと言えそうです。が!
会社の抽選BOTで50万回もやるわけはない。
実際に弊社Slackの抽選bot利用回数を調べると163回の抽選が行われていました。
というわけで163回で検証してみます。~/d/s/lottery ❯❯❯ go run probability.go 抽選実施回数は163回です ---------------------------- 西山 : 9.2% (15回) 川口 : 12.9% (21回) 藤田 : 7.4% (12回) 中島 : 10.4% (17回) 北川 : 10.4% (17回) 鈴木 : 7.4% (12回) 田中 : 9.2% (15回) 遠藤 : 6.1% (10回) CTO : 7.4% (12回) EM : 10.4% (17回) 山田 : 9.2% (15回)
ふーむ・・・・多い人で12%。前後2〜3%程度のゆらぎですね。
母数が少ないとけっこうバラけます。まとめ
slack上でのbot実行回数の総回数もすくないし、当選確率が均一化されないのは仕方ない。
よく当選する気がするのは気のせいかもしれない。気のせいでしょうきっと。
これからもこのbotは運用されるので、確率が均一化されていくのを期待します!
- 投稿日:2020-02-27T11:28:04+09:00
Golang製のSlackAPI(nlopes氏の)がレポジトリ引っ越したらしい(´・ω・`)
ちょうどSlackBotの開発をしていたところ気がついたのでメモ。
旧レポジトリ( 2020年2月27日時点 )
レポジトリURL: https://github.com/nlopes/slack新レポジトリ( 2020年2月27日時点 )
レポジトリURL: https://github.com/slack-go/slackREADME( nlopes/slack )
上記は2020年2月27日時点の
nlopes/slack
のREADMEですが以下のbut no guarantees are made on how up to date it will be
からとりあえずこれから入るアップデートが必要な機能とかは保証されないうんぬんでちゃんと新レポジトリから取った方が良さそうですね!
また、旧に
nlopes/slack
使えなくなったらとりあえず新レポジトリから引っ張ってくれば良さそうですね :)
- 投稿日:2020-02-27T01:47:15+09:00
leet code 83 Remove Duplicates from Sorted List の覚え書き
83 Remove Duplicates from Sorted List
https://leetcode.com/problems/remove-duplicates-from-sorted-list/この問題が曲者すぎた。
ぱっと見で簡単やろうと思った自分を殴りたい。激ムズでっせ。
ListNodeの値は[1,1,3]func deleteDuplicates(head *ListNode) *ListNode { cur := head for cur != nil && cur.Next != nil { if cur.Val == cur.Next.Val { cur.Next = cur.Next.Next //これでheadも更新済み fmt.Println(head, "headそのもの") fmt.Println(head.Next, "head.Next") fmt.Println(cur.Next, "cur.Next") } else { cur = cur.Next //ここでcur自身を新しく作る(上書き)ので,curはheadとは別物になる //curという入れ物自体は変化する //しかし、入れた値のNextは番地を与えているので、中身を変えるとheadも変わる } } return head }ここで、考えなければならないことが大きく3つある。
ListNodeの扱い方
アルゴリズムを考える
ポインタ(番地とか)その中でも、Listnodeとポインタが時間を取られたところ。
1 ListNode
// * Definition for singly-linked list. type ListNode struct { Val int Next *ListNode }これはstruct(構造体)である。すでに定義済みのものを扱うとのこと
でもその中身とかどうやって見るのかわからん。調べてみた。
https://play.golang.org/p/StDSHvDEvb
&ListNode{Val: 2, Next: &ListNode{Val: 4, Next: &ListNode{Val: 3, Next: &ListNode{}}}}
中身はこうなっていて、2を取得するには、ListNode.Val、4を取得するにはListNode.Next.Valと書く。
ちなみに最後のNextは{0,nil}になるみたいです。⬅︎重要!2 ポインタ
if分のprintを実行すると以下のようになる。&{1 0xc000086050} headそのもの &{3 0xc000086060} head.Next &{3 0xc000086060} cur.Next &{1 0xc000086050} headそのもの &{3 0xc000086070} head.Next &{0 <nil>} cur.Nextいかなる時でもheadの番地は変化していないのがわかる。
headの入れ物自体は変化はしないが、実際にはheadの中身(Next)は変化しているのもわかる。
head = [Val:1,Next{Val:1,Next:{Val:3,Next{}}}]
headの変数の入れ物自体は変化はしていない。これ→[]
headの中身head.Nextは変化しているので番地は変更される[Val:1,Next:{Val:3,Next{}}]elseの処理の cur = cur.Nextはcurを上書きしている(つまり新しく作成しているので番地は変更になる)
けれども、その中身のNextの番地はheadのものなのでcur.Nextをいじるとhead.Nextも変更される。という、大きな仮説を立てました。
よくわかる解説が欲しい。