仕事中の問題と解決メモ。

最近はPythonとGoogle Cloud Platformがメイン。株式会社ビズオーシャンで企画と開発運用、データ活用とか。https://github.com/uyamazak/

Google Container Engineでdeploymentとserviceをコピーするために設定を出力する

Google Container Engineで運用しているアプリケーションをそろそろ東京リージョンに移そうと思った。

これまで、

kubectl get service -o yaml
kubectl get deployment -o yaml

で出力されるyamlは手作業で必要な項目消したり、書き換えていたりしてから、kubectl -f {ファイル名}で新しいコンテナクラスタに読み込んでいた。


でも、それぞれ数が増えてきて、やってられないので自動化しようと思った。

JSONで出力して、Pythonなり、何らかのプログラムでパースするしかないかなと思ってたけど、Kubernetesのドキュメントを漁っていたらtempleteというのを見つけた。

公式で探したけど、過渡期なのかリンク切れがひどくて見つけられない。

ともかく使い方としては下記のようなコマンド。

※kcは.zshrcで設定しているkubectlのエイリアス

% kc get service -o=go-template-file=service-backup-template.txt
% kc get deploy -o=go-template-file=deploy-backup-template.txt

go-template-file=には、Go言語の表示パッケージであるtemplate形式でファイルを作る。

template - The Go Programming Language

何度もエラーと戦いながら2時間以上書けて書いた。環境によっては足りない項目等で、そのままでは使えない場合がある。

deploy-backup-template.txt

apiVersion: {{.apiVersion}}
items:{{with .items}}{{range . }}
- apiVersion: {{.apiVersion}}
  kind: {{.kind}}
  metadata:
    name: {{.metadata.name}}
  spec:
    replicas: {{.spec.replicas}}
    template:
      metadata:
        labels:
          run: {{.spec.template.metadata.labels.run}}
      spec:{{with .spec.template.spec.containers}}{{range .}}
        containers:
        - name: {{.name}}
          {{with .env}}env:{{range .}}
          - name: {{.name}}
            value: {{.value}}{{- end}}{{end}}
          {{with .command}}command:{{range .}}
          - {{.}}{{end}}{{end}}
          image: {{.image}}
          {{with .ports}}{{range .}}
          ports:
            - containerPort: {{.containerPort}}
              protocol: {{.protocol}}
          {{end}}{{end}}
          {{with .volumeMounts}}{{range .}}
          volumeMounts:
            - mountPath: {{.mountPath}}
              name: {{.name}}
        {{end}}{{end}}
      {{end}}{{end}}
      {{with .spec.template.spec.volumes}}{{range .}}
        volumes:
        - gcePersistentDisk:
            fsType: {{.gcePersistentDisk.fsType}}
            pdName: {{.gcePersistentDisk.pdName}}
          name: {{.name}}{{end}}{{end}}
{{end}}{{end}}
kind: List
metadata: {}
resourceVersion: ""
selfLink: ""

この可読性保持のために出力されたファイルには不要な空白行が含まれるけど、読み込みには問題ない。

出力後、いくつか手作業が必要。

  • envのvalueに数字のみの値がある場合、出力した後に手作業で""で囲む
  • Volumeに使う永続ディスクを変更する。


service-backup-template.txt

apiVersion: {{.apiVersion}}
items:{{with .items}}{{range .}}{{ if ne .metadata.name "kubernetes"}}
- apiVersion: {{.apiVersion}}
  kind: {{.kind}}
  metadata:
    name: {{.metadata.name}}
  spec:
    ports:{{with .spec.ports}}{{range .}}
    - port: {{.port}}
      {{if .nodePort}}nodePort: {{.nodePort}}{{end}}
      protocol: {{.protocol}}
      targetPort: {{.targetPort}}{{end}}{{end}}
    selector:
      run: {{.spec.selector.run}}
    sessionAffinity: {{.spec.sessionAffinity}}
    type: {{.spec.type}}
{{end}}{{end}}{{end}}
kind: List

こちらはデフォルトで存在するkubernetesのサービスを{{ if ne .metadata.name "kubernetes"}}で飛ばすようにしてある。
HTTPSロードバランサーを使っているので、KubernetesのロードバランサーのServiceは使っている場合は、変更が必要だと思う。

これを作ったことで、今後のバックアップと複製から大幅に手作業をカットできた。

GKEでPODが立ち上がらなくなったときの調査方法

Google CONTAINER ENGINE(以下GKE)ではreplicasの値で簡単にPODを増やせる。


https://cloud.google.com/container-engine/cloud.google.com


でも、なぜかStatusがPendingのままのPODが出るときがある。

※kcはkubectlのエイリアス

% kc get po
NAME READY STATUS RESTARTS AGE
arms-2765276764-o4c8l 1/1 Running 0 21d
arms-2765276764-owzgw 1/1 Running 0 21d
arms-2765276764-tkvdw 1/1 Running 0 21d
gopub-3058232056-emfwd 0/1 Pending 0 9m
gopub-3058232056-h6aan 1/1 Running 0 9m
gopub-3058232056-qiz9u 1/1 Running 0 11m
r2bq-95534634-4ptzf 1/1 Running 0 19d
rabbitmq-3083250092-8jj79 1/1 Running 0 75d
redis-pd-1996650620-p1o6f 1/1 Running 0 160d
revelation-749222209-qqguw 1/1 Running 0 16m
revelation-worker-3204269062-t1vyn 1/1 Running 0 15m
table-manager-1264182560-txkwf 1/1 Running 1 21d

今回、gopub-3058232056-emfwdがいつまで経ってもPendingだった。

おそらくリソース不足なんだけど、詳細が欲しい。

そんなときは、kubectlでイベントを取得する。

kubectl get events
また省略形の
kubectl get ev
で表示できる

% kc get ev (git)-[master]
LASTSEEN FIRSTSEEN COUNT NAME KIND SUBOBJECT TYPE REASON SOURCE MESSAGE15m 15m 1 gopub-2970282743-agna1 Pod spec.containers{gopub} Normal Killing {kubelet gke-oceanus-asia-b-default-pool-0eeece87-38ht} Killing
container with docker id f23ac3bcf97a: Need to kill pod.
15m 15m 1 gopub-2970282743 ReplicaSet Normal SuccessfulDelete {replicaset-controller } Deleted
pod: gopub-2970282743-agna1
44s 13m 39 gopub-3058232056-emfwd Pod Warning FailedScheduling {default-scheduler } pod (go
pub-3058232056-emfwd) failed to fit in any node
fit failure on node (gke-oceanus-asia-b-default-pool-0eeece87-38ht): Insufficient cpu
fit failure on node (gke-oceanus-asia-b-default-pool-0eeece87-m6i1): Insufficient cpu

20s 13m 10 gopub-3058232056-emfwd Pod Warning FailedScheduling {default-scheduler } pod (gopub-3058232056-emfwd) failed to fit in any node
fit failure on node (gke-oceanus-asia-b-default-pool-0eeece87-m6i1): Insufficient cpu
fit failure on node (gke-oceanus-asia-b-default-pool-0eeece87-38ht): Insufficient cpu

13m 13m 1 gopub-3058232056-h6aan Pod Normal Scheduled {default-scheduler } Successfully assigned gopub-3058232056-h6aan to gke-oceanus-asia-b-default-pool-0eeece87-38ht
13m 13m 1 gopub-3058232056-h6aan Pod spec.containers{gopub} Normal Pulled {kubelet gke-oceanus-asia-b-default-pool-0eeece87-38ht} Container image "asia.gcr.io/oceanus-dev/gopub:v20170328-02" already present on machine
13m 13m 1 gopub-3058232056-h6aan Pod spec.containers{gopub} Normal Created {kubelet gke-oceanus-asia-b-default-pool-0e


今回は下記の内容から、Insufficient cpu→CPUが不足という原因がわかった。

44s 13m 39 gopub-3058232056-emfwd Pod Warning FailedScheduling {default-scheduler } pod (go
pub-3058232056-emfwd) failed to fit in any node
fit failure on node (gke-oceanus-asia-b-default-pool-0eeece87-38ht): Insufficient cpu
fit failure on node (gke-oceanus-asia-b-default-pool-0eeece87-m6i1): Insufficient cpu

原因が分かれば、クラスタの数を上げるかノードのマシンタイプをアップグレードするか、またはPODを増やすのを諦めるかすれば良い。


クラスタの数は、コマンドなり、クラウドコンソールなりで簡単に数字をいじるだけで変更できるけど、マシンタイプは新しく作り直す必要があるので大変。

またノードの数が5つを超えると料金がかかったりした記憶があるので注意。

今回のgopubサーバーはGo言語で初めて作ったサーバーで、一つ消費メモリ4~7M程度で動いてるので、たくさん動かせるかと思ったけど、n1-standard-1×2ではそろそろ限界が来てしまったよう。

追記

kubectl describe pod {POD ID}

でも似たような結果を取得できる

Go言語でgRPCのエラーと戦って負ける

以前の記事からGo言語でサーバーアプリを自分で書き始めて、エラーハンドリングが必要になった。


エラーについては下記記事が参考になった。
qiita.com


この記事のように自分で作ったエラーならいいんだけど、大抵は外部のライブラリのエラーではまる。

Cloud PUB/SUBのライブラリ("cloud.google.com/go/pubsub")は、内部ではgRPCでリクエストをしており、エラーもこのライブラリのが返ってくる。


godoc.org


godoc.org

import "google.golang.org/grpc"

下記のようなコードで、起動時にはトピックがあったけど、その後に消されてしまったとき、ps.publishHandlerが返すエラーを特定したかった。

ソースコード全体はこちら

とりあえず、一番汚いやり方、err.Error()で帰ってきた文字列をstrings.Indexで単純に比較。これでも「今は」動く。

go func() {
    err = ps.publishHandler(buf[:n])
    if err != nil {
        log.Printf("publishHandler err: %v", err)
        log.Printf("err type:%v", reflect.TypeOf(err))
        log.Printf("err.Error():%v", err.Error())
        log.Println(strings.Index(err.Error(), "rpc error: code = NotFound desc = Resource not found"))

    }
}()

実行すると

2017/03/28 01:51:19 publishHandler err: rpc error: code = NotFound desc = Resource not found (resource=ml30gen9-bizocean).
2017/03/28 01:51:19 err type:*grpc.rpcError
2017/03/28 01:51:19 err.Error():rpc error: code = NotFound desc = Resource not found (resource=ml30gen9-bizocean).
2017/03/28 01:51:19 0

と0が帰ってきて判別はできる。含まれない場合は-1。


エラーの型には*grpc.rpcErrorが返ってくる。

単純にこいつと型比較しようとするとrpcErrorという名前から分かるように、最初が小文字なので外部から呼び出せなくてむかつく。

なぜ外から触れないようになってるのか、私のGo力(ごーぢから)ではソースコードから読みとれなかった。

grpc - GoDoc

grpcには下記のような関数があり、エラーを渡せば、コードが返ってくる。

func Code(err error) codes.Code

が、他のエラーも返ってくることもあり、その場合エラーになるのでまず型判定が必要。

app/main.go:212: cannot convert err (type error) to type codes.Code


reflect.TypeOfを使って

src/reflect/type.go - The Go Programming Language

返ってくるTypeをstringにして比較してみる。

if reflect.TypeOf(err).String() == "*grpc.rpcError" {
    //process
}

これは成功。

でも文字列比較はダサい。


だがさっき書いた用に呼び出して型を調べようとするとエラーになる。文字列化もできない。

if reflect.TypeOf(err) == grpc.rpcError {
  //process
}
if reflect.TypeOf(err) == *grpc.rpcError {
  //process
}

log.Printf("TypeOf(*grpc.rpcError):v", reflect.TypeOf(grpc.rpcError).String())

3つともこういうエラーが出る。

app/main.go:209: cannot refer to unexported name grpc.rpcError

今のところ下記のダサい方法しかない

if reflect.TypeOf(err).String() == "*grpc.rpcError" {
  //process
}||<


誰か助けてー

Docker環境でGoogle Cloud APIへの認証を行う

よく忘れるのでメモ。

ググって出てくるページだと

gcloud auth application-default login

しろとか、コマンドが出てくるけどDockerだといろいろ面倒なので、鍵ファイルを用いた認証がしたい。



GCPのコンソールから「IAMと管理」→「サービスアカウントを作成」。

URL的には下記。

https://console.cloud.google.com/iam-admin/serviceaccounts/project?project={プロジェクトID}

当たり前だけど権限は必要なものだけにして、複数のアプリがあれば、それぞれに作ったほうが良い。



もし間違って鍵ファイルをGitHubに公開してしまった時などは危ないので、すぐこの画面から削除して、新しい鍵に差し替えればおk(以前やった)。

アプリごとに鍵を変えていれば、その作業も最低限で済む。


作成すると、鍵ファイルがJSON形式でダウンロードされるので、dockerのホストマシンにコピーする。名前は変更してしまっても良い。


ファイル名がkey.jsonだったらDockerfileで下記のようにコピ-。

そのファイル名を環境変数GOOGLE_APPLICATION_CREDENTIALS」にファイル名を入れる。

COPY key.json key.json
ENV GOOGLE_APPLICATION_CREDENTIALS key.json


これでビルドすれば、各言語のライブラリから特に難しいことなく認証が通る。

Google Application Default Credentials  |  Google Identity Platform  |  Google Developers


Go言語でGoogle Cloud PUB/SUBへの高速HTTPS中継サーバーを作る

以前から本を買ったり気になっていたGo言語だけど、書き慣れてないし変更が多いと大変なので、使い所がなくPythonで全部済ましてしまっていた。


でも以前、Google Cloud PUB/SUBを使おうとした時、通信がHTTPSなのでレスポンスが遅く、WEBアプリ側から直接はとても使えないので、ローカルにRedisを立てて使っているのを思い出した。


cloud.google.com


どのくらい遅いかというと、ローカルだと10ms以下で返せていたAPI的なものが、Cloud PUB/SUBへのpublishを入れると、そこの処理だけで片道20ms~50ms、遅いときは全体で100ms以上かかってしまっていた。

サーバー台数の節約と、ユーザー側のストレス軽減も考えて大量のアクセスを瞬時に捌きたいのに。


ローカルネットワーク内に立てたTCP接続のRedisのPubSubであれば遅くても数msなので、話にならない。


で、今回はその中継に、静的言語なので処理速度も早く、簡単に非同期の並行処理を書けるGo言語で自前サーバー「GOPUBサーバー(仮)」を作ろうと思った。

フロント側はローカル内のGOPUBサーバーに渡すので、HTTPSや外部のネットワーク等、通信のオーバーヘッドが少なく、GOPUBサーバーは起動しっぱなしで、HTTPS接続を使いまわし、さらにgoroutineによる並行処理を使い、非同期で早く返せるだろうという考え。

図にするとこんな感じ。

f:id:uyamazak:20170321112530p:plain


Go自体が書き慣れていないので、いろんなところを切り貼りしたり、試行錯誤したのでコードは汚いけど、go文によるgoroutineの力は想像以上だった。

1万リクエストを直列で一つずつやった場合は、一つ100msぐらいかかり、時間を測る気にならず途中止めたけど、go文を使うと1万リクエストも1, 2秒程度でレスポンスできた。

goroutineの数を出してみると、4コア8スレッドの開発サーバーでは、最大で500ほど生成され、並列&並行処理がされていた。ver1.5以降は、デフォでCPUに合わせて並列処理も行ってくれる。

go文による非同期の並列&並行処理はHTTPS通信のような処理は軽いけど待ち時間が長い処理には最適だと思う。

今のところGo言語で読んでる本は下記だけ。変に思えるGoの設計も、C言語との関わりなど説明があり納得できた。

スターティングGo言語

スターティングGo言語


コードもエラー処理、リトライ、バッファサイズの調整など、まだいろいろ手をいれる必要があるけど、TCPのlistenと、Googleの認証、Cloud PUB/SUBへのpublishを含めても1ファイルで100行程度で書けた。

package main

import (
	"log"
	"net"
	"os"
	"runtime"
	"strconv"

	"cloud.google.com/go/pubsub"
	"golang.org/x/net/context"
)

func main() {
	ctx := context.Background()
	projectID := os.Getenv("PROJECT_ID")
	listenHost := os.Getenv("LISTEN_HOST")
	listenPort := os.Getenv("LISTEN_PORT")
	topicName := os.Getenv("GOPUB_TOPIC_NAME")
	log.Printf("runtime.GOMAXPROCS: %v\nprojectID: %v\nlistenHost: %v\nlistenPort:%v\ntopicName:%v",
		runtime.GOMAXPROCS(0), projectID, listenHost, listenPort, topicName)
	// Creates a client.
	p_client, err := pubsub.NewClient(ctx, projectID)
	if err != nil {
		log.Printf("Failed to create client: %v", err)
		return
	}
	topic := createTopicIfNotExists(ctx, p_client)
	var listen net.Listener

	listen, err = net.Listen("tcp", listenHost+":"+listenPort)
	if err != nil {
		log.Printf("Failed to listen tcp %v", err)
		return
	}
	defer listen.Close()

	for {
		conn, e := listen.Accept()
		if e != nil {
			log.Printf("Failed to accept %v", e)
			return
		}
		connection_handler(conn, ctx, topic)
	}
}

func connection_handler(conn net.Conn, ctx context.Context, topic *pubsub.Topic) {
	defer conn.Close()

	bufferSize, _ := strconv.ParseUint(os.Getenv("GOPUB_BUFFER_SIZE"), 10, 64)
	buf := make([]byte, bufferSize)

	for {
		n, err := conn.Read(buf)
		if err != nil {
			return
		}
		go publish_handler(ctx, topic, buf[:n])
		conn.Write(buf[:n])
	}
}

func publish_handler(ctx context.Context, topic *pubsub.Topic, buf []byte) {
	buf_copy := make([]byte, len(buf))
	copy(buf_copy, buf)

	result := topic.Publish(ctx, &pubsub.Message{Data: buf_copy})
	_, err := result.Get(ctx)
	if err != nil {
		return
	}
}

func createTopicIfNotExists(ctx context.Context, c *pubsub.Client) *pubsub.Topic {
	// Create a topic to subscribe to.
	topicName := os.Getenv("GOPUB_TOPIC_NAME")
	t := c.Topic(topicName)
	ok, err := t.Exists(ctx)
	if err != nil {
		log.Print(err)
	}
	if ok {
		return t
	}

	t, err = c.CreateTopic(ctx, topicName)
	if err != nil {
		log.Printf("Failed to create the topic: %v", err)
	}
	return t
}

これもoceanusプロジェクト内に含める予定で、速度的に問題なければWEBアプリ部分から直接このGOPUBサーバーにデータを送る予定。

もちろん開発環境や、本番環境はすべてDockerを使用。

Googleの認証については別記事に書いた。
uyamazak.hatenablog.com


https://github.com/uyamazak/oceanus

Google Cloud PUB/SUBにデータを送ることができれば、データ処理基板であるCloud Dataflowや、まだアルファ版のCloud Functions からもリアルタイムでデータを受け取れるので、夢が広がります。

Googleのslack競合「Hangouts Chat」に期待

弊社ではG Suiteを使っているので、Googleがslackっぽいの出してくれればと思ってましたが、やってくれるようです。


www.itmedia.co.jp


slackは今も使ってますが、たくさんメッセージを使うのには有料プランが必要なので、月額がかかってしまいます。G Suiteに含めてくれれば助かります。

G Suiteで会社のユーザー管理をしているので、Google側でまとまれば助かります。


あとはSlack並にbotが簡単に作れるようになっていれば文句ありません。

kubectlでimageの変更をコマンドラインで行う

Google Container Engineでのデプロイは、今までは

kubectl edit deploy {name}

でエディタを開き、手でimageの値を書き換えていたけど、面倒になったので自動化を考えた。

マスターのバージョンは1.4.8。

editではなくpatchというコマンドがあった。

kubectl patch - Kubernetes


不要な部分を削除して下記の様な形で成功した。
nameを入れないとエラーになるのでハマった。

既存のnameにしないと、別名で複数起動してしまう。

kc patch deployment redis-pd -p '{"spec": { "template": { "spec": {"containers": [{"name":"appname", "image": "asia.gcr.io/project-id/appname:tag"}]}}}}'

これを更新用シェルスクリプトに含めて、アプリ名、タグ名を入れればpushとデプロイまでコマンド一発でできるようになった。