9月に株式会社メルカリを退職していました

表題の通りですが、2022年9月6日付で株式会社メルカリを退職していました。最終出社日は5月末でした。

2014年に入社し、約7年半という長い時間お世話になりました。初期のメルカリの開発、3年間のアメリカ赴任、US Microservice Platform Team の立ち上げ等とても貴重な経験をさせて頂きました。ありがとうございました。

詳しくは別のブログに書いたので興味あればご覧ください。

hackernoon.com

現在はフリーランスとして活動しています。もし何かお手伝いできそうなことがあれば twitter のDMか以下のメールアドレスにご連絡ください。大体何でもやりますが、特に go や kubernetesphp 周りが得意です。

istio-proxyが自分以外のコンテナ終了を待たずに終了するのをなんとかする

TL;DR

pod 削除時、istio-proxy と実アプリケーションコンテナの終了は同期されません。そして大抵 istio-proxy のほうが早く終了します。

pod は istio-proxy のプロセス終了後、リクエストを受けると即座に503を返すようになります。

つまり、例えば http を serve するようなアプリケーションだと、graceful shutdown が実装されていてもそれが無効になってしまいます。デプロイ時などには非常に困る挙動です。

何もしなければこの問題は現在最新の istio 1.1.1 でも起こります。詳細は以下の issue などが参考になります。

https://github.com/istio/istio/issues/7136

対処方法としては、実アプリケーションの pod 終了を待つように istio-proxy に preStop を入れるか、VirtualService の retry を使うと良いです。

実験準備

httpbin を使い、リクエスト処理中に pod を delete するとどうなるか実験してみます。 準備として、docker.io/citizenstig/httpbin を使った Deployment を2つ作り、片方にだけ istio-proxy を注入します。 その2つの Deployment に Ingress と istio の Gateway を紐づけて挙動を見てみます。ここではGKE(ingress-gce)を使っています。

cat <<EOF | kubectl create -f -
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: httpbin-1
  namespace: httpbin
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: httpbin-1
    spec:
      containers:
      - image: docker.io/citizenstig/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        ports:
        - containerPort: 8000
EOF

はい

kubectl expose deployment httpbin-1 --type LoadBalancer --name httpbin-svc-1 -n httpbin

同じ感じで istio-proxy を持ったものを作ります

cat <<EOF | istioctl kube-inject -f - | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: httpbin-2
  namespace: httpbin
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: httpbin-2
    spec:
      containers:
      - image: docker.io/citizenstig/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        ports:
        - containerPort: 8000
EOF

httpbin-2 に istio の gateway を割り当てたものを作ります

kubectl expose deployment httpbin-2 --type ClusterIP --name httpbin-svc-2-ci -n httpbin
cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
  namespace: httpbin
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin-vs
  namespace: httpbin
spec:
  hosts:
  - "*"
  gateways:
  - httpbin-gateway
  http:
  - match:
    route:
    - destination:
        host: httpbin-svc-2-ci
        port:
          number: 8000
EOF

一応整理しておくとこんな感じです

名前 種類 接続先
httpbin-svc-1 Ingress httpbin-1 (istio-proxyなし)
istio-ingressgateway Gateway httpbin-2 (istio-proxyあり)

実験

httpbin には /delay エンドポイントがあります。 /delay/10 のようにすれば10秒間サーバー側で sleep してからレスポンスを返してくれます。

ちなみに、sleep の値はデフォルトで2秒、最大10秒までしか待てないので注意です。

/delay を使って、ここまでに作った pod に対して、curl でリクエストしつつ裏で pod を delete するという操作をしてみます。

httpbin には graceful shutdown が実装されているため、delay 中でサーバー側で処理が続いている場合に pod を delete してもきちんとレスポンスは帰ってくるはず、というのが期待する振る舞いです。

$ export ING1=$(kubectl get svc httpbin-svc-1 -n httpbin -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ export GATE=$(kubectl get svc istio-ingressgateway -n istio-system -o jsonpath="{.status.loadBalancer.ingress[0].ip}")

ING1(Ingress&istio-proxyなし)の場合、しっかり10秒待ってからレスポンスが得られます。

$ time curl -so /dev/null -w '%{http_code} ' $ING1:8000/delay/10
$ kubectl delete po (kubectl get po -n httpbin | grep httpbin-1 | cut -d ' ' -f 1) -n httpbin # この操作は別の terminal でやる
pod "httpbin-1-545cbd998-989d8" deleted
200 curl -so /dev/null -w '%{http_code} ' $ING1:8000/delay/10  0.01s user 0.01s system 0% cpu 10.414 total

GATE(Gateway&istio-proxyあり)の場合も上記と同様に、しっかり10秒まってからレスポンスを返してほしいのですが・・・。

$ time curl -so /dev/null -w '%{http_code} ' $GATE/delay/10
$ kubectl delete po $(kubectl get po -n httpbin | grep httpbin-2 | cut -d ' ' -f 1) -n httpbin # この操作は別の terminal でやる
pod "httpbin-2-67cd76c979-dhl2m" deleted
503 curl -so /dev/null -w '%{http_code} ' $GATE/delay/10  0.01s user 0.01s system 0% cpu 2.666 total

503が返ってきてしまいました。

対処法1:preStopを挟む

こちらの issue で言及されている通り、istio-proxy を編集しもう片方の container の終了を待つように preStop を作ります。

containers:
- name: istio-proxy
   lifecycle:
     preStop:
       exec:
         command: ["/bin/sh", "-c", "while [ $(netstat -plunt | grep tcp | grep -v envoy | wc -l | xargs) -ne 0 ]; do sleep 1; done"]

ぱっと見一番シンプルかつ楽な方法に見えます。この方法で上記の問題を解決できるのか試してみます。

試すには httpbin-2 の Deployment を直接編集して preStop を追加すればよいのですが、それではクラスタ全体にこの変更を適用できないため、ここでは istio-sidecar-injector を差し替える方法を紹介したいと思います。

とはいえ難しいことは特になく、istio-system に istio-sidecar-injector という configMap と pod があるので、それを編集するだけです。前者の configMap の値が実際に適用される yaml となります。

# configMapの値は jsonpath を使うときれいに取れる
kubectl get cm istio-sidecar-injector -n istio-system -o jsonpath='{.data.config}' > xxx/config
kubectl delete cm istio-sidecar-injector
# 先程とった config を編集して preStop を差し込む
kubectl create cm istio-sidecar-injector -n istio-system --from-file xxx/config

新しい configMap を istio-sidecar-injector を読み込みませます

kubectl delete po istio-sidecar-injector-c8c4c568b-k9g5f -n istio-system

istio-proxy を更新するためには最終的に Deployment を更新する必要があります。kube-inject をもう一度行います。

cat <<EOF | istioctl kube-inject -f - | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: httpbin-2
  namespace: httpbin
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: httpbin-2
    spec:
      containers:
      - image: docker.io/citizenstig/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        ports:
        - containerPort: 8000
EOF

kubectl get deploy httpbin-2 -n httpbin -o yaml とかして istio-proxy の preStop が定義されていることを確認してください。

その状態で先程の実験ように time curl -so /dev/null -w '%{http_code} ' $GATE/delay/10 の最中に pod を delete してみると、しっかり200が返ってくることが確認できると思います。

以下はおまけの話ですが、この方法でやる場合 istio の Gateway を使わず Ingress でもきちんとことが確認できます。

httpbin-1 と同じように type: LoadBalancer のものを作ります。

$ kubectl expose deployment httpbin-2 --type LoadBalancer --name httpbin-svc-2 -n httpbin
$ export ING=$(kubectl get svc httpbin-svc-2 -n httpbin -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ time curl -so /dev/null -w '%{http_code} ' $ING2:8000/delay/10
$ kubectl delete po $(kubectl get po -n httpbin | grep httpbin-2 | cut -d ' ' -f 1) -n httpbin # この操作は別の terminal でやる
200 curl -so /dev/null -w '%{http_code} ' $ING2:8000/delay/10  0.01s user 0.01s system 0% cpu 10.281 total

Gateway を使っていなければこちらの対処方法のほうが気楽かもしれないです。

対処法2:VirtualServiceでretryする

別の対処法として、VirtualService で retry してしまうという手があります。こちらも手軽な方法かと思います。

$ cat <<EOF | kubectl create -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin-vs
  namespace: httpbin
spec:
  hosts:
  - "*"
  gateways:
  - httpbin-gateway
  http:
  - match:
    route:
    - destination:
        host: httpbin-svc-2-ci
        port:
          number: 8000
      retries:
        attempts: 2
        perTryTimeout: 1s
EOF

なお、この retry は http status が502-503の場合に行われるようです(参考:https://github.com/istio/istio/issues/8846

アプリケーションの実装が500以外も頻繁に使われるようなものだと、予期しない挙動になる可能性もあるため注意が必要です。

また、言うまでもないですが外部からのリクエストに VirtualService を適用するためには Gateway が必要です。つまり上記のING2ではこちらの方法は通用しません。

tcpdumpでistioをデバッグして遊ぶ

kubernetes の周辺で service mesh の話が聞かれるようになってそこそこ経ちました。istio に関して言えば、今年の半ばに1.0もリリースされています。今後 kubernetes と istio という組み合わせはどんどん一般的なものになっていきそうです。

例えば istio には、特定条件下のリクエストの振り分け、リトライ、gRPC の Client loadbalancing をしてくれる機能があります。それぞれ至って簡単なアイディアですが、それらに機能を実現するために istio 内部では複数のコンポーネントが複雑に連携しています。そういった内部的なアーキテクチャを知りたければ、例えば traffic-management のページにいくと詳しい解説を見ることができます。ただ、やはりドキュメントにない細かい挙動を知りたい、実際に動かしながら挙動を見てみたほうがわかりやすい、ドキュメントにはそう書いてあるがどう考えてもそうやって動いているようなようにみえない、といったこともあると思います。

そういった際にはデバック的な方法を取ることになります。そのうちの一つとして tcpdump が挙げられています。

ですがこのページには以下のような簡単な記述しかありません。

With Tcpdump

Tcpdump doesn’t work in the sidecar pod - the container doesn’t run as root. However any other container in the same pod will see all the packets, since the network namespace is shared. iptables will also see the pod-wide configuration.

Communication between Envoy and the app happens on 127.0.0.1, and is not encrypted.

最初見たとき、自分は具体的にどういった手順を踏めばいいのかよくわかりませんでした。落ち着いて考えてみればすごく難しいわけではないのですが、同じように感じる人がいるかもしれないのでここに具体的な手順を書いていきたいと思います。また、取ってきたパケットを覗いて少しだけ考察もしてみたいと思います。

環境はGKEを使った kubernetes cluster を想定しますが、一部読み替えれば応用はできるはずです。

bookinfoを作る

kubernetes cluster の作成と istio のインストールは既に済んでいるものとします。

今回はサンプルとして bookinfo を使います。基本的には以下のページの通り設定していけば良いはずですが、ある程度 node のリソースが必要な点は気をつけましょう。

以下に自分の方で実行したコマンドだけ列挙しますが、少し公式のものに手を加えているので注意してください。具体的には default namespace を使わず istio-injection=enabled になっている新しい namespace を作っています。また、上記リンクの内容だけでは istio の機能を大して使えず面白くなさそうだったので、 virtual-service-all-v1.yaml 及び virtual-service-reviews-test-v2.yaml を追加で適用しています。

$ git clone -b release-1.0 https://github.com/istio/istio /hoge/istio-1.0.0 && cd /hoge/istio-1.0.0
$ kubectl create namespace bookinfo
$ kubectl label namespace bookinfo istio-injection=enabled
$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml -n bookinfo
$ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml -n bookinfo
$ kubectl apply -f samples/bookinfo/networking/destination-rule-all.yaml -n bookinfo
$ kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml -n bookinfo
$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-test-v2.yaml -n bookinfo

上記実行後、こちら に従ってingress ip と port を調べます。

ブラウザで http://$GATEWAY_URL/productpage に行けば bookinfo のページが見れると思います。また、jason というユーザーを作ってログインすると通常のユーザーとは少し違うUIになっているはずです。具体的には Book Reviews の欄に rating の星が見えていると思います。

tcpdumpの設置と実行をする

bookinfo の作成ができたら、次に tcpdump の設置を行います。今回は bookinfo の gateway 的な立ち位置になっている productpage に corfr/tcpdump を横付けします。

pod-overview のページに解説がありますが、Pod には container を複数指定することができ、その場合でもネットワーク等は container 間で共有されます。つまり、任意の Pod に対して tcpdump の container を追加し実行すれば、一方の container で起きたトラフィックでも tcpdump の container から見えるというわけです。

bookinfo を作成する際に使った yaml ファイルを以下のように修正し、もう一度 kubectl apply します。面倒であれば kubectl edit して変えても良いと思います。

diff --git a/samples/bookinfo/platform/kube/bookinfo.yaml b/samples/bookinfo/platform/kube/bookinfo.yaml
index c0470c400..0b2281e9c 100644
--- a/samples/bookinfo/platform/kube/bookinfo.yaml
+++ b/samples/bookinfo/platform/kube/bookinfo.yaml
@@ -189,4 +189,7 @@ spec:
         imagePullPolicy: IfNotPresent
         ports:
         - containerPort: 9080
+      - name: tcpdump
+        image: corfr/tcpdump
+        command: ["sleep", "10000"]
 ---

ここでは ["sleep", "10000"] としていますが、例えばここは以下のようにし、この /var/productpage.pcap の内容を Persistent Volume に書き出したりする方法もあるかもしれません。

- name: tcpdump
  image: corfr/tcpdump
  args: ["-i", "eth0", "-A", "-w", "/var/productpage.pcap"]

ただ、Persistent volume の準備が若干手間なのと、デバッグ用途であることから、今回はお手軽にコンテナに接続して直接 tcpdump を実行する方法をとることにしました。

実行は以下のように行います。以下を実行中、ブラウザから bookinfo にアクセスします。いくつかリクエストを送ったら tcpdump を終了します。

$ kubectl exec -it productpage-v1-5869b65c77-59j8r -n bookinfo -c tcpdump -- tcpdump -i eth0 -A -w /var/productpage.pcap
$ kubectl exec -it istio-ingressgateway-58f5f7644d-blmbs -n istio-system -c tcpdump -- tcpdump -i eth0 -A -w /var/gateway.pcap

tcpdumpの実行結果を手元に持ってくる

キャプチャした内容を手元に持ってくる方法ですが、今回はシンプルにssh(!)します。まずは先程の pod がどの node にあるのか確認します。

$ kubectl get pod productpage-v1-5869b65c77-59j8r -n bookinfo -o wide
NAME                              READY     STATUS    RESTARTS   AGE       IP            NODE
productpage-v1-5869b65c77-59j8r   3/3       Running   0          1m        10.48.40.84   gke-test-cluster-default-pool-d2e6f5a4-5nkn

今回はGKE環境を使っているので、以下のように ssh できます。

$ gcloud compute --project $PROJECT_ID ssh --zone $ZONE gke-test-cluster-default-pool-d2e6f5a4-5nkn

インスタンス上で docker ps をすればどれが tcpdump のプロセスなのかわかります。その後 docker cp でコンテナからGCEインスタンスに移します。

$ xxxx@gke-test-cluster-default-pool-d2e6f5a4-5nkn ~ $ docker ps | grep tcpdump
4d3feb7416e7   corfr/tcpdump@sha256:3006b3bd9f041bf73f21e626b97cca5e78fd6ce271549ca95b8e6a508165512b   "sleep 10000"    11 minutes ago    Up 11 minutes    k8s_tcpdump_productpage-v1-5869b65c77-z5szc_bookinfo_5e30a482-d698-11e8-9222-42010a9201a5_0

$ xxxx@gke-test-cluster-default-pool-d2e6f5a4-5nkn ~ $ docker cp 4d3feb7416e7:/var/productpage.pcap .

最後に gcloud scp すればキャプチャした内容を手元に持ってくることができます。

$ gcloud compute --project $PROJECT_ID scp --zone $ZONE gke-test-cluster-default-pool-d2e6f5a4-5nkn:/home/xxxx/productpage.pcap .

wiresharkの設定をする

tcpdump の内容は wireshark で見ることができます。 charles も使えそうですが、tcpdump の結果は wireshark の方が見やすいと思います。

キャプチャした内容を見るには、単純に上記の手順で取得したファイルを wireshark で開けばOKです。しかし、開いてみるとわかると思うのですが、そのままでは特に目立った情報は見られないと思います。これは Pod と istio のやりとりに gRPC が使われており、wireshark でHTTP2のみるには追加でデコード方法を指定する必要があるためです。

デコードの設定は、もしくは wireshark のメニューから Menu > Analyze > Decode As... と辿っていけば見つかるはずです(適当なパケットを右クリックし Decode As を選んでもよい)。

以下のスクリーンショットのように、二番目の列にポート番号、最後の行にHTTP2を指定します。

f:id:gong023:20181216105843p:plain

ポート番号ですが、istioctl を使えば productpage から見たときにどれがどのポートに見えるのか確認できます。このなかから istio に関するものをピックアップして指定するのが良いと思います。

$ istioctl proxy-config clusters productpage-v1-5869b65c77-z5njr -n bookinfo | grep istio

キャプチャした内容を少し考察してみる

ここまでの段階で、以下のような内容を得られています。

f:id:gong023:20181216105911p:plain

どのIPがどの pod なのかは、 kubectl get pods --all-namespaces -o wide とかすればわかるはずです。

681行目から /productpage のリクエストが始まります。687行目で productpage から istio-policy (mixerとかを司るところ)へ gRPC のリクエストが送られています。

gRPC はすべてPOSTメソッドで、パスが protocol buffer に対応します。従ってこの呼出しは以下の proto に対応しているようです。

https://github.com/istio/istio/tree/master/mixer に解説がありますが、このAPIDestination Rule に従った送信先見つけているのかなと思いました(コードをちゃんと読んだわけではないので間違っているかもしれません。あしからずお願いします)。

Precondition Checking.

Enables callers to verify a number of preconditions before responding to an incoming request from a service consumer. Preconditions can include whether the service consumer is properly authenticated, is on the service's whitelist, passes ACL checks, and more.

今回試した限り、他にも Report への呼び出しもみられました。色々みていて、意外と mixer のAPIの種類は少ないんだなあとか発見があり面白かったです。

他にやってみたいこと

今回の実験ですが、最初に述べたように istio のドキュメントに言及があったからというのがきっかけなのですが、How Istio manages microservice applications – A traffic flow analysis with TCP Capture というビデオをみて面白そうだなと思ったのもありました。

このビデオは自分がキャプチャした内容よりとれているAPI呼び出しが多かったです。一年以上前の内容なので istio の内部も色々と変わっていると思うのですが、もしかしたらどっかで何か見落としてるところがあるかもしれないと思っています。この記事で詳細は書きませんでしたが、実は自分はこのビデオ内容のように productpage と同時に istio-gateway のキャプチャもしていました。しかしあまり面白いものは見られずなんだろうなあと思っています。tcpdump の引数とかちゃんと見直してみてもいいかもしれません。また、今回は単純に /productpage をリロードしていただけですが、別の方法にしたら何かしら変化が見られたかもしれません。

また、今回 envoy からと思われるパケットがみられなかったのも気になっています。bookinfo は一般的なREST APIなので、gRPC のアプリケーションだったら何か変わるかなあとかも考えています。gRPC なら呼び出し元で loadbalancing がされるはずだし何かしら動きがありそうな気がします。pod の死活状態は streaming で送られるみたいな話をどっかで聞いたような気がするので今回の方法でできるかわかりませんが・・・。

というわけで、tcpdump を使う方法はなんとなくわかったものの、まだまだ istio についてはわからない部分も多いので気が向いたらまた何か試してみようと思います。

また、勘のいい人ならわかると思いますが、今回の方法は別に istio に関係なく使えます。単純に k8s 上にあるアプリケーションのデバッグ方法として覚えておくと役に立つこともあるかもしれません。

雰囲気でgRPC,GKE+kubernetes使ってマイクロサービス作る

マイクロサービス構成を作る上で、gRPC でアプリケーションを繋ぎ、それらを GKE+kubernetes で動かすというのは有力な選択肢の一つだと思います。ここでは実際にこの構成で動くAPIを作る手順を書いてみます。作ったコードと kubernetes の設定ファイルは以下のリポジトリに置いてあります。必要に応じて参照して下さい。

なお、gRPC, GKE といったものの概要説明・メリット説明などはなるべく省きます。これらは公式ドキュメントで説明されているため、そちらを見るのが良いです。このエントリの末尾にリンクをまとめて貼っておきます。

作る構成

インフラ部分は GKE+kubernetes で、その上に gRPC でやり取りするアプリケーション群が動くという形でAPIを作っていきます。

アプリケーション部分は frontend と backend に分け、各々マイクロサービスのように振る舞います。frontend は外部のネットワークからの http リクエストを受けて裏側のサービスに gRPC でリクエストする役割を持ちます。backend はこの gRPC に応答を返し、外部のネットワークには公開しません。言語は go を使います。

   internet
    ↓  ↑ (http)
┏---↓--↑---------┐
|   ↓  ↑         |
|  frontend      |
|   ↓ ↑ (gRPC)   |
|  backend       |
|                |
└-GKE+kubernetes-┘

gRPCの説明

元も子もない説明ですが、gRPC というのは google が作った RPC です。つまりコミュニケーションの決め事(とその実装)です。REST や GraphQL といったものと比較して語られることもありますが、それらより強くマイクロサービス間でのやり取りに使われることを意識している印象を受けます。

gRPC は基本的には http2 + protocol buffers で動きます。なので簡単に言って早いです。一つの接続を保持しつつ複数のリクエストのデータを並列で処理することができ、ブロッキングが少ないです(http2 の機能)。またリクエスト・レスポンスのデータは全てバイナリでやり取りでき、 json string をパースするような手間が要らないので CPU に優しいです(protocol buffers の機能)。

速度面以外のメリットもあります。リクエスト・レスポンスに型付けを行えるという点です。通常 gRPC でやり取りされるリクエスト・レスポンスは .proto ファイルに書いた定義に従う事になります。この .proto 定義は様々な言語に翻訳でき、リクエスト元とリクエスト先で共有できるので双方でどのような型をやり取りするのか明確にしておくことができます。

proto定義を作る

実際に gRPC を使ったアプリケーションを作っていきます。とりあえず .proto ファイルを作って今回作るアプリケーションの定義をしましょう

// calc.proto

syntax = "proto3";

service Calc {
    rpc Increment(NumRequest) returns (NumResponse) {}
}

message NumRequest {
    int64 val = 1;
}

message NumResponse {
    int64 val = 1;
}

service で定義されているのが今回作るアプリケーションです。Calc という名前で、受け取った int を+1して返す Increment という機能を持つものとします。

message で定義されているのはそれぞれリクエストとレスポンスで使う型です。今回は int の値が一つあれば十分なので、フィールドには int64 val のみを宣言しておきます。

これらの定義に従ってリクエストとレスポンスの型、そして service の interface になるコードを生成します。

生成には protoc というコマンドを使うため、必要であれば ここ からインストールして下さい。また今回は go のコードを生成するため、それ用のプラグインも入れておきます。

go get -u github.com/golang/protobuf/protoc-gen-go

calc.proto を置いたところに gen というディレクトリを作り、以下のコマンドを実行すると calc.pb.go というファイルが得られるはずです。

protoc --go_out=plugins=grpc:gen calc.proto

この calc.pb.go はこれから作る frontend, service のアプリケーションで使うので GOPATH で見える位置に置いて下さい。

なお calc.pb.go の中身を見てみると、type Calc interface, type NumRequest struct, type NumResponse struct といったコードが見つけられると思います。これらが protoc で自動生成されたコードです。今回は go しか使わないのでありがたみが薄いかもしれませんが、protoc は一つの定義から様々な言語でコードを生成できるため、リクエスト元とリクエスト先の言語が違っても問題ありません。対応言語は2017年9月現在 C++, Java, Python, Go, Ruby, C#, Node.js, Android Java, Objective-C, PHP のようです。

frontendを作る

proto を作ったので実際に frontend アプリケーションの実装をしていきます。最初に書いたとおり、今回 frontend というのは「外部のネットワークからの http リクエストを受けて裏側のサービスに gRPC でリクエストする役割」を持たせます。

今回はこんな感じで書きました。(色々雑なのは許してください)

ピックアップすると以下の辺りが重要になります。

    conn, err := grpc.Dial(servName+":8000", grpc.WithInsecure(), grpc.WithUnaryInterceptor(
        grpc_zap.UnaryClientInterceptor(logger),
    ))

    // ...

    client := pb.NewCalcClient(conn)
    ctx := context.Background()
    res, err := client.Increment(ctx, &pb.NumRequest{Val: int64(val)})

    // ...
}

grpc.Dial で backend との接続を確立します。それを pb.NewCalcClient に渡してあげると Increment が定義されている Calc interface を得ることができます。Increment は pb.NumRequest を引数として取り、pb.NumResponse を返します。実際にやってみるとよりわかりやすいですが、楽に proto 定義に従った実装ができると思います。

なお今回 grpc.WithUnaryInterceptor を使っていますが、これは必須ではないです。UnaryInterceptor というのは一般的な gRPC コールの middleware です。それに grpc_zap を渡してロギングしやすくしています。

backendを作る

まだ backend がないので、先の frontend のコードを実行して curl "http://localhost:8080/increment?val=1" のようにしてもエラーになるだけだと思います。frontend からの接続を受け、応答を返す backend を実装してあげます。

こんな感じで実装しました

重要なのは main 内に書かれている以下と

        lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))

        // ...

        server := grpc.NewServer(grpc.UnaryInterceptor(
                grpc_zap.UnaryServerInterceptor(logger),
        ))

        pb.RegisterCalcServer(server, &CalcService{})
        server.Serve(lis)

CalcService struct です。

type CalcService struct{}

func (s *CalcService) Increment(ctx context.Context, req *pb.NumRequest) (*pb.NumResponse, error) {
        req.Val++
        return &pb.NumResponse{Val: req.Val}, nil
}

CalcService には proto で生成された interface を実装させます。NumRequest を受けて NumResponse を返さなければなりません。

main 内のコードで実際に listen を始めますが、RegisterCalcServer は引数に proto の interface を実装した値が必要になります。ここでリクエスト元とリクエスト先で proto に定義された約束が守られているのがわかると思います。UnaryInterceptor は今回も必須ではありません。

これで frontend と backend が出揃ったので、各々実行し curl "http://localhost:8080/increment?val=1" 等するとうまく結果が得られるはずです。サンプルだと json を返すようにしています。

余談ですが、ここまでのコードで gRPC は別のマシンに実装された関数を呼ぶような感覚で使えることがわかると思います。REST でインターフェースを決めていこうとするとパスの位置や http メソッドの種類で揉めることがありますが、gRPC のこの形式だとそういった事が起こりづらいのを利点に挙げる人もいます。いいインターフェースを作ることは大切ですが、どうしても REST でやろうとすると複数の正解があったりして、そういったものを延々議論するのは生産性が高いとも思えないので個人的にもこれは賛成です。

GKE+kubernetesの説明

まず kubernetes というのは docker を始めとした container 技術をオーケストレーションするためのツールです。ここで言うオーケストレーションというのは、例えば container が停止したら復活させるとか、リクエストが増えたら container 数を増やすとかいった操作です。kubernetes はアプリケーションは全て container で扱うことを前提としているため、そういった操作を柔軟かつ簡単に行うことができるようになっています。

GKE は Google Cloud Platform のいち機能で、主に kubernetes の機能をユーザーに提供します。それだけなら kubernetes を直接使えばいいように見えますが、実のところ kubernetes という仕組み自体がマイクロサービスのようになっており、GKE はその構成の立ち上げを簡単にやってくれます。

kubernetes は master node と node という二つの部分からなります。node が実際にアプリケーションの container がホスティングされるところで、master node がその node 内の操作を行うAPIを提供します。ユーザーは master node が公開したAPIを使ってCLI,GUI問わず container の操作を行うことができます。

docker imageの準備

先ほど作った frontend, backend を kubernetes で扱えるようにするために dockerize していくのですが、ここからはGCPプロジェクトが必要になるのでなければ作って下さい。また gcloud, kubectl といったコマンドも使うのでなければ以下を参考にインストールとセットアップをして下さい。

GCPの準備が整ったら docker image を作成します。今回は こちらこちら のような Dockerfile を作りました。

image の作成は以下のように普通に docker build を使えば良いですが、名前規則は gcr.io/$PROJECT_ID/$NAME としましょう。$NAME は任意ですが $PROJECT_ID は自分のGCPプロジェクトの id です。この名前規則でないと後述の Container Registry に push できません(確か)。

docker build -t gcr.io/$PROJECT_ID/micro-sample-frontend:v0.1 .
docker build -t gcr.io/$PROJECT_ID/micro-sample-backend:v0.1 .

docker image ができたら Google Container Registry に push します。Google Container Registry というのはデフォルトで非公開になっている Docker Hub みたいなやつです。

gcloud docker -- push gcr.io/$PROJECT_ID/micro-sample-frontend:v0.1
gcloud docker -- push gcr.io/$PROJECT_IDt/micro-sample-backend:v0.1

push できたら GCP console の左上メニュー > Container Registry から確認できるはずです。ここに push された docker image を kubernetes で使っていきます。

container clusterの作成

先述したとおり kubernetes という仕組み自体一つのマイクロサービスのようなものなので、まずそれを立ち上げます。立ち上げると言っても簡単で以下のコマンドを実行するだけです。

gcloud container clusters create micro-sample --num-nodes=2

実行には数分かかると思われます。以下のようなコマンドで cluster の状態を確認できます。

gcloud container clusters list
gcloud container clusters describe $CLUSTER_NAME

自分の cluster 上の kubernetes のバージョンは master node, node ともに1.7.5でした。ちなみにですが GCP console > Container Engine > Container Cluster から kubernetes のバージョンアップなどもできたりするので覚えておくと便利です。

kubernetesの設定について

ここからは実際に kubernetes で使う設定ファイルを yaml で書いて実行していきます。設定についてはこちらに色々まとまっているのですが、バラエティが豊富でとっつきづらい部分があると思います。

正直言って自分もまだちゃんと全部読めていないのですが、取っ掛かりとしては Pod, Deployment, Service を押さえれば十分ではないかなと思っています。これらを基本として派生した概念が多い印象を受けます。

まず Pod は container の別名みたいなものです。Pod の制御は常に kubernetes が行い、ユーザーが何か直接変更を加えたりすることはありませんが、基本単位として把握しておく必要があります。

Deployment は Pod の集合体の定義です。配布はどの image を使うのか、また RollingUpdate か Blue/Green かといった文字通りデプロイの設定をするような項目もあるのですが、それ以外にもいくつの Pod が常に立ち上がっていればよいのか、もしくはオートスケールにして最低いくつ最高いくつの Pod を持てるのか、Pod を増やす閾値は何かといったことまで定義できます。個人的には色々できすぎて割とカオスな印象を持っていますが、今後の流れとしては Deployment から分離して別の概念として取り扱っていこうみたいなものを感じます。今回はごく簡単な Deployment を作ります。

Service は Deployment で定義した Pod 群がネットワーク的にどういう見え方をするかを定義します。cluster 内の Pod にはそれぞれIPアドレスが振られますが、何らかの原因で停止・復活した際には別のIPアドレスが与えられます。またオートスケールして新しい Pod が立ち上がったりもします。そういった Pod に対して正しくアクセスできなければならないので、ここでどういう名前を持つかなどを定義します。

frontendのService作成

まず最初に frontend 側の Service を作ってみます。今回は以下のような yaml を書きました。

kind: Service
apiVersion: v1
metadata:
  name: micro-sample-service-frontend
spec:
  type: LoadBalancer
  selector:
    app: micro-sample
    tier: frontend
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080

大事なのは type: LoadBalancer です。雑にいうと、こうすると ingress という仕組みが使われインターネットから見た時どう見えるのかといったことを定義できます。また selector でリクエストを受けて探しに行く Deployment を定義しています。

この定義を先ほど作った cluster に適用します。

kubectl apply -f frontend-service.yml

kubectl get svc とすると作られた Service が確認できます。EXTERNAL-IP のところは反映されるのに少し時間がかかります。

なお、 kubectl describe svc micro-sample-service-frontend などすると Service の詳細を確認できます。

frontendのDeployment作成

次に frontend 側の Deployment を作ります。

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: micro-sample-frontend-deployment
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: micro-sample
        tier: frontend
        track: stable
    spec:
      containers:
      - name: micro-sample-frontend
        image: gcr.io/$PROJECT_ID/$NAME:v0.1
        ports:
          - containerPort: 8080
            name: http

とりあえず replicas: 2 として Pod が2つできるようにしました。image のところには先ほど Google Container Registry に push した docker image を指定します。また labels の項目は Service の selector と対応させるようにしておきます。

これを Service と同じように cluster に適用します。

kubectl apply -f frontend-deployment.yml

確認方法も Service のときのように

kubectl get deployment
kubectl describe deployment micro-sample-frontend-deployment

で行えます。

backendのService作成

ここまでの操作で curl "http://{EXTERNAL-IP}/" をするときちんと200のレスポンスが返ってくるはずです(EXTERNAL-IP は kubectl get svc で得られるやつです)。ですがまだ backend は何もしていないため /increment は動きません。frontend と同じようにこちらも作っていきます。

まず Service ですが、こちら のような定義を作りました。frontend とあまり変わりません。外部に公開しないので type: LoadBalancer は除いてあります。

これを適用します。

kubectl apply -f backend-service.yml

kubectl get svc すると Service が追加されているのを確認できるはずです。EXTERNAL-IP はありません。

backendのDeployment作成

最後に backend 側の Deployment を作ります。ここ にありますが、こちらもあまり frontend 側の Deployment と大差ありません。

これまでのように適用していきます。

kubectl apply -f backend-deployment.yml

Pod の状態は kubectl get pods で見れるのですが、これを実行すると先ほどの frontend 側と合わせて合計4つの Pod が立ち上がっているのが確認できると思います。

さて、ここまでで必要な設定は全て終わりです。

curl "http://{EXTERNAL-IP}/increment?val=1" とかするときちんと {"val":2} というレスポンスが得られると思います。

クリーンアップ

最後にここまで作ったもののクリーンアップをします。今回 GKE で container cluster を作りましたが、これは Google Compute Engine という VM 上で動きます。あまり安くなかった覚えがあるので特に理由が無ければ消しておくことをお勧めします。

kubectl delete svc micro-sample-service-frontend
kubectl delete svc micro-sample-service-backend
gcloud container clusters delete micro-sample

参考

annictでreact-nativeの練習した

react-native の練習のために annict クライアント作りました。

コードここにあります

GitHub - gong023/annictApp: annict client by react-native

一応デモもあります。動くといいですね

デモ見てもらったらわかるんですが間違ってもストアに乗っけられる代物ではないです。annict のクライアントつくって小銭稼ごうみたいな意図はなく、単純に練習のために作りました。それも一段落したので今後機能追加等を行うこともないと思います。

何を目的にしていたかというと、一つは単純に react-native を動かしてみること、もう一つは redux に始まるフロントエンド周りアーキテクチャを咀嚼するすることです。興味が js 部分に寄っていたので、アプリとしては ios しかつくってません。今回書いたコードは android でも動かせるはずですが、deeplink の設定とか .env の読み込み設定とか結構面倒くさいのでパスしています。

中の作りに関しては、package.json を少し間引いてみるとなんとなく見えると思います

  "dependencies": {
    "axios": "^0.16.1",
    "lodash": "^4.17.4",
    "react": "16.0.0-alpha.6",
    "react-native": "^0.44.0",
    "react-native-config": "^0.4.1",
    "react-native-router-flux": "^3.38.1",
    "react-redux": "^5.0.4",
    "redux": "^3.6.0",
    "redux-logger": "^3.0.1",
    "redux-observable": "^0.14.1",
    "rxjs": "^5.3.0"
  },
  "devDependencies": {
    "eslint": "^3.13.1",
    "eslint-config-airbnb": "^13.0.0",
    "eslint-plugin-import": "^2.2.0",
    "eslint-plugin-jest": "^1.0.2",
    "eslint-plugin-jsx-a11y": "^2.2.3",
    "eslint-plugin-react": "^6.8.0",
 }

redux, redux-observable, react-native-router-flux 辺りがキモになるかと思いますが、js のシーンを真面目に追っている人間ではないので、だいたいこんな感じ?っていうのは探り探りで作りました。

とはいえ最終的に、見た目の雑さに反して裏側はそこそこ真面目に作れたような気がしてます。まあこれくらいやっとけば割と機能追加やブラッシュアップはいけるかなと。あげたコードが自分のような初学者の助けになればいいなと思います。一応 oauth 認証 -> ログイン -> いくつかの API を叩くという一連のフローは書いています。特に oauth 周りは面倒くさいので、そのサンプルとしてはいいかもしれません。

全体的にやってみた感想としては、react-native、割とドキュメントどおりにやれば動くのですごいなと思いました。この手の技術は正しくやってるはずなのになんか動かないっていうケースが多い印象だったんですが、そういうのでハマった記憶が特にありません。まあ、簡単なものしか作ってないからだと思いますが。しかし全体的に web の js よりコンフィグが100倍楽な印象があり、初学者がフロントエンドの js やるなら web より先に react-native やった方が楽なんじゃないかと思うレベルでした。

とりあえず今回の一件はロジックの組み方に焦点を当てられたのでとても体験良かったです。js 色々言われているし色々ありますが、パラダイムシフトとしてこの辺かなあというのを実際に手を動かしながら見れたのは楽しかったです。コード見てみたけどお前ここダメだよとかあったら教えてもらえるとうれしいです。

時間かかったなあと思うのは xcode へのライブラリ追加とか deeplink の設定とか appetize.io にアップロードする際の release ビルドとか、ios 側の設定です。自分のようにサーバーサイドに寄っている人はこの辺不慣れですし避けることもできないので変なストレスがあると思います。とはいえ本来フルネイティブでやるとしたら全部そんな感じになるはずなので、js の知識を使いまわしてアプリを書く部分だけでも楽できるっていうのはすごいことだなと思いました。

phpの名前空間をお手軽に変えるライブラリ書いてた

表題の通り php名前空間IDEみたいに置き換えられるコマンド書いてました。

実をいうと割と前の話で、社内の勉強会とかでは喋ってたんですが、ちゃんとパブリックで書いてなかったので書きます。あれからちょこちょこアップデートもしたし

これなに

主に php名前空間を置き換えるためのコマンドです。IntelliJ とか使ってる人にはおなじみの機能だと思いますが、それを php スクリプトで実現した感じです。

基本的に必要なオプションは以下の3つです。

  1. composer.jsonのパス
  2. 変更前の名前
  3. 変更後の名前

変更前後の名前を入力するのは当然だと思いますが、composer.json のパスを読み込ませるのは解析対象にするファイルを知るためです。

つまり composer.json

    "autoload": {
        "psr-4": {
            "YourName\\": "src"
        },
        "files": ["src/functions.php"]
    },
    "autoload-dev": {
        "psr-4": {
            "YourName\\": "tests"
        }
    },

と書いてあればその内容に従って src,tests,functions.php を解析対象にします。

なお、解析は nikic/PHP-Parser で行います。このライブラリについては qiita に書いたので興味あれば御覧ください。

正規表現とかではなくまともな方法で静的解析するので安定性はそれなりにある、と思います。

また、各ファイルの解析はマルチプロセスで並列実行されます。namae-space を作るにあたって、100%プレーンな php だけで動くように気をつけていたので速度面でちょっと不安がありましたが、並列実行にしたことによりまあまあ大丈夫なパフォーマンスになったような気がします。遅かったらすいません。php-cs-fixer よりはマシ、かも・・・。

ついでに言うとコードの内容を書き換えるだけではなくその名前のクラスを find する機能とかも乗っけています。そのあたりはリポジトリの readme を読んでみて頂ければ幸いです。

なんでつくったの

会社のリポジトリにグローバルな名前空間に作られたクラスが結構あってちょっとアレだったので、それを一息で殺すために作りました。もう2017年だし namespace ナシでクラス作る人はさすがにいないんですが、昔のコードにそういうのが残ってて殲滅したいなと思ってました。

先述の通り IntelliJ とかには名前空間を置き換える refactoring 機能がついていて、日頃から大変お世話になっているのですが、名前がグローバルだと置き換えられないんですよね。そういうわけで自分でスクリプト書きました。

便利オプション補足

一応 readme にも書いたんですが replace コマンドののオプションを一応ここにも載せます。以下は全て optional なので必要になったら参照して下さい。

-D:–dry_run

ドライランです。コードの置き換えは行わず diff だけ出力します。

-M:–max_process

静的解析の並列実行数を指定します。デフォルト10になっててちょっと強めかも。

-A:–additional_paths, -E:exclude_paths

先述の通り解析対象は composer.json を読み込んでよしなに出すんですが、お作法が違うディレクトリがあったりしたらそれらを強制的に加えたり外したりできます。グローバルな名前を置き換えたいとかいうニーズの場合このオプションが必要になるのではないでしょうか。

-R:–replace_dir

composer.json の内容だけでは新しい名前空間のファイルの置き場所が一意に決まらない場合があります。例えば先述した composer.json の内容で、YourName\Klass.php を作りたい場合、Klass.php は src 以下におけば良いのか tests 以下におけばいいのか厳密には判断できません。そうなった場合 namae-space はインタラクティブモードで「src と tests どっち使う?」と聞いてくるのですが、それをスキップしたい場合このオプションを使って直にファイルの置き場所を指定できます。

苦労話

gong023/namae-space はとにかく手軽に使えるものにしたくて、インストールgithub から phar を落とせば終わり、という形にしようかと思ってました。

しかし、phar だと proc_open が動かないんじゃないかという疑惑があり、このため phar による提供は一旦止めてます。

phar だと proc_open が動かないんじゃないかという疑惑

再現コードはこんな感じです。

gist.github.com

php をポチポチ gdb デバッグしてみると、なんかどうも この辺php phar://myphar… みたいな文字列が渡されていてそれを実行しようとするのでそりゃ動くわけないなって感じなんですが、これこういうもんなんでしょうか。

これだれか分かる人がいたら教えていただきたいです。

php-blt2でLTした

PHP BLT #2 - connpass でLTしてきたので資料貼っておきます

speakerdeck.com

普段ほとんどエンジニアの勉強会行かないしLTするのとか多分2年ぶりとかだったと思う。

自分の場合、ついこの前まで卓球ハウスにいたので技術の話する相手や機会には困らなかったけれど、卓球ハウスは解散したので今後もしかしたらこういう機会増えるのかなあとか思った。

内容について特に補足することはないけど、PHP拡張のマクロ情報少なくて面倒だなあという感じです。References に載せてあるリンクからの抜粋だけど、一応ここわかって助かったみたいなの書いときます。

  • PHP_CHECK_LIBRARY で必要な引数について
  1. The name of the library. In our case score will be transformed into -lscore when compiling. Example: cc -o conftest -g -O0 -Wl,-rpath,/usr/local/lib -L/usr/local/lib -lscore conftest.c
  2. The name of the function to try and find within our score library.
  3. The set of actions to take if the function is found. In our case, we are adding to the Makefile code to compile against our library and defining HAVE_SCORE which is used by the during compilation.
  4. The set of actions to take if the function is not found. In our case, we are throwing an error with a human readable error message.
  5. The set of extra library definitions. In our case, we are making sure the compiler knows where to find our shared object.
  • PHP_ADD_LIBRARY_WTH_PATH で必要な引数について
  1. The name of the library.
  2. The path to the library.
  3. The name of a variable to store library information. We will use this with PHP_SUBST.

詳しくはこちらで