tjun月1日記

なんでもいいので毎月書きたい

独自imageのdocker-machineをGCEで利用する

独自のdocker imageを作ってGoogleCloudPlatform(以下GCP)上のContainer Registryに登録して、GCE(Google Compute Engine)で動かすやり方です。

Dockerfileを元にimageを作ってContainer Registryにpushするまで

以下の例ではcontainer registryのサーバはアジアにしてます。

NAME=myapp
VERSION=1
APPID=<gcpのprojectID>

docker build -t ${NAME}:${VERSION} .
docker tag ${NAME}:${VERSION} asia.gcr.io/$(APPID)/${NAME}:${VERSION}
docker tag asia.gcr.io/$(APPID)/${NAME}:${VERSION} asia.gcr.io/$(APPID)/${NAME}:latest

gcloud --project=$(APPID) docker --server=asia.gcr.io -- push asia.gcr.io/$(APPID)/${NAME}

GCEをdocker-machineとして起動する

以下のように、docker-machineを作る際にdriverとしてgoogleを指定します。 その他のoptionは Google Compute Engine | Docker Documentation を参考に設定します。

docker-machine create \
    --driver google \
    --google-project $(APPID) \
    --google-preemptible \
    --google-zone asia-northeast1-a \
    --google-machine-type n1-highcpu-8 \
    --google-disk-size 300 \
    --google-disk-type pd-ssd \
    --google-machine-image https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-1604-xenial-v20170815a \
    --google-scopes https://www.googleapis.com/auth/devstorage.read_write \
    ${NAME} 

作ったdocker-machineを確認する

$ docker-machine ls
NAME             ACTIVE   DRIVER   STATE     URL   SWARM   DOCKER    ERRORS
myapp                 -        google   Stopped                 Unknown

activeなdocker-machineを切り替える

eval $$(docker-machine env ${NAME}) 

docker-machine上で、登録したimageを実行する

gcloud docker -- run -it -e PROJECT_ID=$(APPID) --name ${NAME} asia.gcr.io/$(APPID)/${NAME}:latest /bin/bash

goのtemplateのrangeで複数の配列を扱う

最初ちょっとやり方が分からなかったのでメモ。

やりたいことは、例えばgoで以下のような配列があったとき

type User struct {
 ID int
 Name string
}

type UserInfo struct {
 ID int
 Age int
}

users := []User{
    User{ID: 1, Name: "taro"},
    User{ID: 2, Name: "jiro"},
    User{ID: 3, Name: "hanako"},
}

infos := []UserInfo{
    UserInfo{ID: 1, Age: 10},
    UserInfo{ID: 2, Age: 20},
    UserInfo{ID: 3, Age: 30},
}

ちょっと例が微妙ですが、usersinfosを同じindexでループを回したいようなことがあるかと思います。

goのtemplateでは、単独のループであれば

{{range $index, $user := Users}}
    {{$user.Name}}
{{end}}

のように書けるのですが、もう一つの配列も同じようにループを回す場合には、以下のように書く必要があります。

{{range $index, $user := Users}}
    {{$user.Name}}
    {{(index $.Infos $index).Age}}
{{end}}

のindexを使って、ループ外部の変数の配列のindexを指定する感じです。

GoogleAppEngine/Goでのディレクトリ構成

GAE/Goで、結構悩むのがディレクトリ構成です。 pkgをどう分けて、どこのディレクトリに何を置くか、自分はどうやってるか紹介します。

関連する記事はこのあたりです。

はじめに

  • 基本的にはGOPATH以下にコードを置くことを想定しています。 以下の例では$GOPATH/src/github.com/tjun/gaesample とします。
  • AppEngineは Go1.6 を利用している場合になります。1.8になると変わる部分があるかもしれません。
  • echoでサーバを書いていますが、構成にはあまり影響がないと思います。
  • vendoringはglideを使ってやってます
  • goappコマンドはあまり使っていません

方針

  • 依存するpkgは、バージョンを固定したいので vendor以下におきます。
  • デプロイスクリプトなどは、_deployなどアンダースコアから始まるディレクトリに入れます

ディレクトリ構成

以下のような感じでやってます。

$GOPATH/src/github.com/tjun/gaesample
|-- main/
|       |-- app.yaml
|       |-- index.yaml
|       |-- main.go  # AppEngineのエントリポイント
|       `-- public/   # img, cssなど静的なファイル
|
|-- controllers/   # コントローラ
|-- models/        # モデル
|-- views/         # view
|-- server/        # echoのサーバのエントリポイント
|-- router/router.go   # ルーティング
...                       # その他のpkgも直下に置く
|-- vendor/  
|
|--Makefile 
|-- _deploys/   # デプロイスクリプト置き場
|
...
  • クライアントのコードは省略していますが、ビルド時に main/public以下に持っていきます。

サーバをローカルで動かすときは

./dev_appserer.py main

で動かせます。 デプロイのときは、

./appcfg.py update main

になります。

この構成のいいところは、内部のpkgのimportが

import (
 "github.com/tjun/gaesample/models"
)

みたいな感じでわりと自然に書けることです。

GOPATH以下にコードを置いてると、CircleCIからのデプロイがそのままじゃできなかったりしますが、そのやり方はまた今度書きます。

tmuxのwindow nameをgitのレポジトリ名にする

window nameっていうのは、tmux で下のstatusに出てくるタブみたいなやつの名前。 どのタブがどのレポジトリで作業しているのか分からなくなるので、レポジトリ名を表示するようにした。 git管理下じゃない場合はカレントディレクトリを表示する。

.zshrcに以下を追加する。

autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git svn
zstyle ':vcs_info:*' formats '%r'

precmd () {
  LANG=en_US.UTF-8 vcs_info
  if [[ -n ${vcs_info_msg_0_} ]]; then
          tmux rename-window $vcs_info_msg_0_
  else
          tmux rename-window `basename $(pwd)`
  fi
}

f:id:taka-jun:20170301201202g:plain

GoogleAppEngine/GoでCloudStorageのファイルを配信する方法とハマりどころ

半年前くらいからGAE/Goを結構触っているので、ちょっと書いておく。 今回は、CloudStorage上のファイルをGAE/Goで配信する方法。

概要

GoogleCloudStorageにおいてるファイル(例えば画像)をWebサービス上でユーザに配信したいということがある。 画像ならブラウザ上で表示させるだけでいいかもしれないし、その他のファイルならダウンロードさせたいし、複数ファイルならzipで固めて配信したい。

イメージ f:id:taka-jun:20170301160414p:plain

cloud.google.com/go/storageのライブラリを利用する。 以下の例では、エラー処理などは省略する。また、普段はEchoを使ってるので以下のコードは試してないし正しく動かない部分があるかも。

コード例

画像を表示させる

画像をブラウザで表示させるケース。 特に難しい部分はなく、シンプルに書ける。 検索して出て来るサンプルだとioutil.ReadAllを使ってることもあるけど、io.Copyで 直接http.ResponseWriterに書き出した方がパフォーマンス的にいいと思う。

func serveImage(res http.ResponseWriter, req *http.Request) error {
  ctx := appengine.NewContext(req)
  cli, _ := storage.NewClient(ctx)
  reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
  defer reader.Close()

  res.Header().Set("Content-Type", reader.ContentType())
  res.WriteHeader(http.StatusOK)

  siz, _ := io.Copy(res, reader)
  return nil
}

ファイルをダウンロードさせる

基本的には上と同じだけど、Content-Dispositionヘッダを指定して画像でもダウンロードさせられるようになる。

func serveImage(res http.ResponseWriter, req *http.Request) error {
  ctx := appengine.NewContext(req)
  cli, _ := storage.NewClient(ctx)
  reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
  defer reader.Close()

  res.Header().Set("Content-Type", reader.ContentType())
  disposition := "attachment; filename=" + fileName
  res.Header().Set("Content-Disposition", disposition)
  res.WriteHeader(http.StatusOK)

  siz, _ := io.Copy(res, reader)
  return nil
}

zipで固めてダウンロードさせる

複数のファイルをダウンロードさせたい場合、ディレクトリごとダウンロードさせたい場合などに使える。 storageのReaderと httpのResponseWriterの間にzipWriterを挟む感じ。

func serveImage(res http.ResponseWriter, req *http.Request) error {
  ctx := appengine.NewContext(req)
  cli, _ := storage.NewClient(ctx)

  res.Header().Set("Content-Type", "application/octet-stream")
  disposition := "attachment; filename=" + fileName
  res.Header().Set("Content-Disposition", disposition)
  res.WriteHeader(http.StatusOK)

  zipWriter := zip.NewWriter(res)
  defer zipWriter.Close()

  for _, fname := range fileNames {
 path := Path(path.Join(dir), fname))
    reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
    defer reader.Close()

    zipFile, err := zipWriter.Create(fname)

    _, err = io.Copy(zipFile, reader)
  }
  return nil
}

エラー処理がないとすごくシンプルに見えるけど、実際は if err != nil {... というコードがあちこちに入ります。

ハマりどころ

クラウドストレージ上のファイルのサイズが 32MBを越えると、storage.NewReaderAPI error 3 (urlfetch: UNSPECIFIED_ERROR)というエラーになります。 また、複数のファイルをZIPする場合でも、合計のサイズが32MB超えるとResponse返すところでエラーになります。 CloudStorage -> AppEngine -> Client の2箇所とも32MB制限があります。 参考: https://cloud.google.com/appengine/quotas#Requests

エラーが分かりにくいし、32MBはちょっと少ない気がする。 しかもGAE/Goのドキュメントを読むと、Streaming Responseに対応してないので、ファイルを少しずつ読んでStreaming Responseで返すってこともできません。

参考: https://cloud.google.com/appengine/docs/standard/go/how-requests-are-handled#streaming_responses

32MB以上のファイルを扱う際には、BlobStoreを使います。 BlobStoreは以下のように使えます。

func serveImage(res http.ResponseWriter, req *http.Request) error {
  ctx := appengine.NewContext(req)

  bpath := path.Join("/gs", fpath)
  bkey, _ := blobstore.BlobKeyForFile(ctx, bpath)
  
  disposition := "attachment; filename=" + fileName
  res.Header().Set("Content-Disposition", disposition)

  blobstore.Send(res, bkey)

  return nil
}

参考: The blobstore package  |  App Engine standard environment for Go

recvmmsgについて

(過去の記事の転載)

recvmmsg というのは、recvmsg(2)を拡張して複数のメッセージをsocketから受け取れるシステムコールで、Linux の 2.6.33 から利用できます。 glibcの version 2.12 からサポートされてます。 これを使うと、要するに1回のシステムコールで複数回recvmsgをすることができるので、messageをどかどか受けるようなアプリケーションでパフォーマンスが上がることがあります。

実際に使ってみて効果が見られたのと、man読んだだけじゃ使い方分かりにくかったので、これを書いておく。

manをみれば使い方はなんとなく分かると思う。manにあるサンプルコードを一番下に貼りました。 複数個受けるだけではなくtimeoutを取れるのがrecvmsgと違うところで、manを読むとまあ普通のタイムアウトのように見えるんだけど、カーネルのソースを読むと厳密なタイムアウトにはなってないようです。(ということを教えてもらいました)

また、すぐに抜けてきて欲しいときに timeoutに0 (tvsec=0, tvnsec=0) をセットすると、1つrecvしただけで返ってきてしまうのも注意。 すぐに抜けて来て欲しいけど、そのときに取れるmessageは全て取りたい、という時には、flagに MSG_DONTWAIT をセットして、timeoutはNULLにして呼び出すと期待している動作になります。

ハマりやすいポイントみたいで、manが分かりにくいという意見もあるようです。

また、sendでも似たようなことをやるためのsendmmsgというのもあります。

以下に、manにあったサンプルコードを載せておく。

#define _GNU_SOURCE
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>

int  
main(void)  
{
#define VLEN 10
#define BUFSIZE 200
#define TIMEOUT 1
  int sockfd, retval, i;
  struct sockaddr_in sa;
  struct mmsghdr msgs[VLEN];
  struct iovec iovecs[VLEN];
  char bufs[VLEN][BUFSIZE+1];
  struct timespec timeout;

  sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sockfd == -1) {
    perror("socket()");
    exit(EXIT_FAILURE);
  }

  sa.sin_family = AF_INET;
  sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
  sa.sin_port = htons(1234);
  if (bind(sockfd, (struct sockaddr *) &sa, sizeof(sa)) == -1) {
    perror("bind()");
    exit(EXIT_FAILURE);
  }

  memset(msgs, 0, sizeof(msgs));
  for (i = 0; i < VLEN; i++) {
    iovecs[i].iov_base = bufs[i];
    iovecs[i].iov_len = BUFSIZE;
    msgs[i].msg_hdr.msg_iov = &iovecs[i];
    msgs[i].msg_hdr.msg_iovlen = 1;
  }

  timeout.tv_sec = TIMEOUT;
  timeout.tv_nsec = 0;

  retval = recvmmsg(sockfd, msgs, VLEN, 0, &timeout);
  if (retval == -1) {
    perror("recvmmsg()");
    exit(EXIT_FAILURE);
  }

  printf("%d messages received\n", retval);
  for (i = 0; i < retval; i++) {
    bufs[i][msgs[i].msg_len] = 0;
    printf("%d %s", i+1, bufs[i]);
  }
  exit(EXIT_SUCCESS);
}