Knative Servingを試してみた
この記事はCyberAgent Developers Advent Calendar 2018 8日目の記事です
この記事では、最近興味があるknativeの中のServingについて紹介していきます。
knative とは
サーバレスワークロードのビルド、デプロイ、管理するためのKubernetesをベースにしたプラットフォームを提供するフレームワークです cloud.google.com
knativeは、Serving, Build, Eventingの3つのコンポーネントがあります。概要としてはそれぞれこのような認識です。
- Serving: リクエスト駆動でポッドの起動やポッド数の管理
- Build: ソースからコンテナを生成するビルドに関するオーケストレーション
- Eventing: イベント管理と配信
今回は、このServing部分で実際に動作させながらスケールリングやリクエストの振り分けなどをまとまめていきます。 環境としては、docker desktop for mac上のk8s環境で行いました。
インストール
knative servingのセットアップには、kubernetes環境にIstioとKnative Servingの2つをインストールすることでできます。
今回は、docker desktop for macでのkubernetesクラスタを使っていますが、他環境の場合も記載されているので、手持ちのkubernetesで試すことができると思います。
docs/README.md at master · knative/docs · GitHub
istioのインストール
まずistioのインストールから行います。 minikubeの場合を参考にNodePortにしてapplyします。
curl -L https://github.com/knative/serving/releases/download/v0.2.2/istio.yaml \ | sed 's/LoadBalancer/NodePort/' \ | kubectl apply --filename - kubectl label namespace default istio-injection=enabled
関連するのpodがRunningやCompletedになるとインストールは完了です。apply後にはistio-injection=enabled
ラベルを設定も行います。
この istio-injection
の設定ができていないと動作させられないです。後続のkubectl applyなどはエラーも出ず、この設定漏れにも気づかずになぜ動作しないのかと時間を使ったことがあるので忘れずやりましょう。
それぞれ行うと自分の環境だと次のようになりました。
$ kubectl get pods --namespace istio-system NAME READY STATUS RESTARTS AGE istio-citadel-84fb7985bf-tvjf2 1/1 Running 0 1m istio-cleanup-secrets-4lk9z 0/1 Completed 0 1m istio-egressgateway-bd9fb967d-g57cw 1/1 Running 0 1m istio-galley-655c4f9ccd-vr2dz 1/1 Running 0 1m istio-ingressgateway-688865c5f7-ppqwl 1/1 Running 0 1m istio-pilot-6cd69dc444-9mqgp 2/2 Running 0 1m istio-policy-6b9f4697d-b8n4r 2/2 Running 0 1m istio-sidecar-injector-8975849b4-2sf9h 1/1 Running 0 1m istio-statsd-prom-bridge-7f44bb5ddb-wm2pw 1/1 Running 0 1m istio-telemetry-6b5579595f-vmlfj 2/2 Running 0 1m $ kubectl get namespace -L istio-injection NAME STATUS AGE ISTIO-INJECTION default Active 3d enabled docker Active 3d istio-system Active 6m disabled kube-public Active 3d kube-system Active 3d
Knative Servingのインストール
Knative Servingのインストール時にも、先程と同様にNodePortに変更してapplyします。
curl -L https://github.com/knative/serving/releases/download/v0.2.2/release-lite.yaml \ | sed 's/LoadBalancer/NodePort/' \ | kubectl apply --filename -
完了時には次のようになりました。
$ kubectl get pods --namespace knative-serving NAME READY STATUS RESTARTS AGE activator-df78cb6f9-cdjqz 2/2 Running 0 1m activator-df78cb6f9-gbn7f 2/2 Running 0 1m activator-df78cb6f9-whhbk 2/2 Running 0 1m autoscaler-6fccb66768-cnwzk 2/2 Running 0 1m controller-56cf5965f5-dnfr6 1/1 Running 0 1m webhook-5dcbf967cd-8bjzs 1/1 Running 0 1m
これで一連のインストールは完了です。
Knative Serving
今回扱うKnative Servingは、次の4つの要素があります。
- Service
- Route
- Configuration
- Revision
参考: https://github.com/knative/docs/tree/master/serving
Service
他の3つの要素を含んだ全体を自動的に管理するもの。
更新に対する新しいRevionを作成し、最新のRevisionや特定のRevisionに常にルーティングされるように管理している。
kubectl get kservice
で定義されているものを取得できる
Route
どのRevionに何%トラフィックを流すかなどをマッピング。
kubectl get route
で定義されているものを取得できる
Configuration
実行する構成の定義、変更されると新しいRevisionが作成される
他のものと同様に kubectl get configuration
定義されているものを取得できる
Revision
Configurationの履歴にあたるポイントインタイムスナップショット。
kubectl get revision
で取得できる
デプロイ
環境も整ったので、実際にデプロイしていきます。 今回は、単純なjsonを返すサーバアプリで、このアプリのdockerイメージはDocker Hubにある状態です。
これをknative上で動かすためにknative servingのServiceリソースを追加します。
# echo-server-service.yaml apiVersion: serving.knative.dev/v1alpha1 kind: Service metadata: name: echo-server namespace: default spec: runLatest: configuration: revisionTemplate: spec: container: image: terachanple/echo-server-go:latest
kubectl apply -f echo-server-service.yaml
このapplyが成功すると、
curl -H "Host: echo-server.default.example.com" "http://localhost:32380"
上記のようなcurlコマンドでデプロイしたecho-serverへリクエストできます。
ポートの32380
は knative-ingressgateway
へリクエストするためのもので次のようにして確認できます。
# knative-ingressgateway のNodePort kubectl get svc knative-ingressgateway --namespace istio-system --output 'jsonpath={.spec.ports[?(@.port==80)].nodePort}' # HostヘッダのURL kubectl get services.serving.knative.dev echo-server -o jsonpath='{.status.domain}'
podの動きとしては次のようになっていて、初めてリクエストしたときにpodの初期化生成が行われています。そしてしばらくリクエストをしないとそのpodは削除されています。なのでリクエストがない状態であればpod数が0になります。
$ kubectl get pod --watch NAME READY STATUS RESTARTS AGE echo-server-00001-deployment-dbb4b64b9-xsnjt 0/3 Pending 0 0s echo-server-00001-deployment-dbb4b64b9-xsnjt 0/3 Pending 0 0s echo-server-00001-deployment-dbb4b64b9-xsnjt 0/3 Init:0/1 0 0s echo-server-00001-deployment-dbb4b64b9-xsnjt 0/3 PodInitializing 0 3s echo-server-00001-deployment-dbb4b64b9-xsnjt 2/3 Running 0 5s echo-server-00001-deployment-dbb4b64b9-xsnjt 3/3 Running 0 6s echo-server-00001-deployment-dbb4b64b9-xsnjt 3/3 Terminating 0 5m echo-server-00001-deployment-dbb4b64b9-xsnjt 0/3 Terminating 0 5m echo-server-00001-deployment-dbb4b64b9-xsnjt 0/3 Terminating 0 6m echo-server-00001-deployment-dbb4b64b9-xsnjt 0/3 Terminating 0 6m
上のように1podだけでなく、リクエストが増えれば、podの数も増加させてくれます。
https://github.com/knative/docs/blob/master/serving/samples/autoscale-go/README.md#algorithm ここで記載されているように、スケールはpodごとのリクエスト中でまだ完了していない数の平均をもとに算出しており、例としては次のような計算になるようです。
条件 - リクエスト数: 350req/s - 1リクエストの処理時間: 0.5s/req 式 350[req/s] * 0.5[s/req] = 175 175 / 100 (デフォルト:100) = 1.75 ceil(1.75) = 2 [pods]
ちなみに、podがない状態からの最初のアクセスではpodの作成処理から行われているので時間はかかってしまいます。
# echo-server podが0個の場合 $ curl -H "Host: echo-server.default.example.com" -s -o /dev/null -w "%{time_starttransfer}\n" "http://localhost:32380" 9.562675 # echo-server podが1個の場合 $ curl -H "Host: echo-server.default.example.com" -s -o /dev/null -w "%{time_starttransfer}\n" "http://localhost:32380" 0.023103
一連のserviceを削除する場合は次のようにkubectlから削除できます。
kubectl delete -f echo-server-service.yaml
リクエストをrevisionに振り分ける
knative servingではRouteを設定することでバージョンAに何%のトラフィック、バージョンBに何%のトラフィックを流すといったような制御ができます。 なので機能追加などで変更した新しいものへのリクエストは数パーセントから初めて徐々に増やしていくような形をとることができます。
先程で使ったecho-serverを使って試していきます。
Serviceだと自動で最新のものになるようなので、 Configuration
と Route
を追加していきます。
今あるecho-serverに100%リクエストが行く形です。
# echo-server-split.yaml (latestのみ) apiVersion: serving.knative.dev/v1alpha1 kind: Configuration metadata: name: echo-server namespace: default spec: revisionTemplate: metadata: labels: knative.dev/type: container spec: container: image: terachanple/echo-server-go:latest imagePullPolicy: Always --- apiVersion: serving.knative.dev/v1alpha1 kind: Route metadata: name: echo-server namespace: default spec: traffic: - revisionName: echo-server-00001 percent: 100 name: "v1"
上記のyamlをapplyすると先程と同じように
curl -H "Host: echo-server.default.example.com" "http://localhost:32380"
次にイメージに変更を加えたバージョンを指定し、今までのバージョンには90%、新しいバージョンには10%で振り分けるように設定します。(今回のアプリの変更はレスポンスjsonに含まれるmeta:"v1"
を meta:"v2"
へ変更しただけです)
# echo-server-split.yaml (新しくfeature_aを追加) apiVersion: serving.knative.dev/v1alpha1 kind: Configuration metadata: name: echo-server namespace: default spec: revisionTemplate: metadata: labels: knative.dev/type: container spec: # container: # image: terachanple/echo-server-go:latest # imagePullPolicy: Always container: image: terachanple/echo-server-go:feature_a imagePullPolicy: Always --- apiVersion: serving.knative.dev/v1alpha1 kind: Route metadata: name: echo-server namespace: default spec: traffic: - revisionName: echo-server-00001 percent: 90 name: "v1" - revisionName: echo-server-00002 percent: 10 name: "v2"
再度applyし、 先程までと同様にリクエストすると、 "meta":"v2"
となっているレスポンスがたまに返ってくるようになります。
リクエストとレスポンスの例としては、次のようなものです。
# リクエストとレスポンスの例 $ curl -H "Host: echo-server.default.example.com" "http://localhost:32380" {"meta":"v2","msg":""}% $ curl -H "Host: echo-server.default.example.com" "http://localhost:32380" {"meta":"v1","msg":""}% $ curl -H "Host: echo-server.default.example.com" "http://localhost:32380" {"meta":"v1","msg":""}% $ curl -H "Host: echo-server.default.example.com" "http://localhost:32380" {"meta":"v1","msg":""}%
このように同じリクエストでもリクエストごとに2つのものに振り分けられます。この振り分ける割合を traffic.percent
の値を調整することで簡単に制御できます。
特定のものにリクエストする場合はtraffic.name
を設定し、その設定したname
をリクエスト時のhostの先頭につけることで指定したrevisionへアクセスできます。
今回であれば、今までの方にv1
、新しく追加した方に v2
とnameを設定しているので、次のようにして特定のものへアクセスできます。
# name v1へのアクセス curl -H "Host: v1.echo-server.default.example.com" "http://localhost:32380" # name v2へのアクセス curl -H "Host: v2.echo-server.default.example.com" "http://localhost:32380" # 指定なし (v1, v2は設定したpercentで振り分けられる) curl -H "Host: echo-server.default.example.com" "http://localhost:32380"
リクエストを振り分けて簡単に適用できるのは、カナリアリリースなどもできるようになるのですごく嬉しいですね。
まとめ
- knative servingをローカルのKubernetes環境でデプロイやリクエストの振り分けの動作までの方法をまとめました
- リクエストに応じて自動でスケールし、リクエストが無いとpod数が0にまでなるので、必要分だけが起き上がるサーバレスな感じは体感でよかった
- pod数が0の状態からのアクセス時には時間がかかってしまうので、apiサーバとして使うときは注意が必要そうに感じました。batchサーバだとすぐに開始しないといけない用途以外にはあまり気にならないようには感じました。
- リクエストの振り分けは特定の%を設定するだけで簡単に行えるので便利
- servingだけだとリクエストに応じてだけになるってしまうので今回できなかったですが、BuildとEventingを連携して、特定のイベントでソースがビルドされり、特定のサービスが動くというのも試していきたいと思います
今回使用したyamlやecho-serverのコードは次のリポジトリに置いてあります github.com
grpc-webを触ってみた
webブラウザのJavaScriptからgRPCができるgRPC-webが先日リリースされました。実際に実行するまでの流れや自動生成されたコードをどのように使うのかなど知りたかったので、そのgRPC-webを実際に試してみました。
試したこと
- リポジトリ内にあるexampleをもとに、実際にprotoファイルからgRPC-webでコードを生成し実行
- gRPC-webでTypeScriptを生成して同様に実行
- バンドルしたファイルの大きさの確認
hello world
公式のリポジトリ内にexampleとして用意されている helloworldから試していきます。
helloworldの構成としては次のような形になっています。
webブラウザ(grpc-web) --- envoy --- grpcサーバ(nodejs)
README.mdにあるように実行までは次の流れです。
- makeコマンドからprotoc-gen-grpc-webをインストールして、protoファイルからjsのコードを生成
- node server.js でnodeのgRPCサーバを立ち上げる.
- dockerを使って、envoyを立ち上げる
- webpackで、クライアントサイドjsをバンドルし、1つにまとめる
- webサーバを立ち上げて、index.htmlとmain.jsを読み込む
ただこの流れで試したみたのですが、自分の環境だとエラーが発生し動作を確認できませんでした。
エラーの原因は、envoyが意図通りに実行できていないというものでした。
envoyののdocker起動は↓のようになっていました。
$ docker run -d -p 8080:8080 --network=host helloworld/envoy
ただ、自分の環境で--network=host
の設定をつけようとすると、
WARNING: Published ports are discarded when using host network mode
のようにwarningが発生しポートフォワーディングが設定できなかったというものです。--network=host
を指定しなければ、正しくポート8080で受け付けられる状態で起動はすることはできました。
これで大丈夫かと思ったのですが、まだ動きませんでした。
このサンプルではenvoyだけがdockerで他はそうではない状態です。そしてenvoyの設定ファイルのenvoy.yamlには、gRPCのnodeサーバへのプロキシ部分がlocalhost:9090のように記述されていたため、ただenvoyのコンテナが立ち上がっても、envoyからみるとgRPCサーバはlocalhost:9090ではないため正しく実行できなかったようです。--network=host
の状態で動作させることができれば、このままで大丈夫だとは思います。
なので対応として、gRPCのnodeサーバもdockerで起動してenvoyとnodeサーバ間で通信できるようにしてみました。
webブラウザ(grpc-web) -- envoy(docker) -- gRPC nodeサーバ(docker)
これでようやくブラウザから通信が成功し、一連の動作を確認できるようになりました。エラーになっていなかったクライアント部分はdocker化せずにそのままで試しています
実行すると次のようなリクエストをdeveloper consoleで確認することができました。
main.js以下の3つのリクエストがgrpc-webでのリクエストです。
またコンソール上に出力された結果としてはこのようになりました。
TypeScript化
次にgrpc-webではTypeScriptにも出力できるようなので、そちらも試してみました。ただTypeScriptのサポートはまだ実験段階のようです。
TypeScriptへの出力は、次のようなコマンドで生成されます。
protoc -I=. helloworld.proto \ --js_out=import_style=commonjs:. \ --grpc-web_out=import_style=typescript,mode=grpcwebtext:.
このときに出力されるファイルは次の3つです
- helloworld_pb.js
- helloworld_pb.d.ts
- HelloworldServiceClientPb.ts
全てがTypeScriptというわけではなく、一部jsファイルも含まれている状態です。示したコマンドにあるように --js_out=import_style= にtypescriptを指定できませんでした。 (指定した場合でもエラーにはならずファイルは出力されましたが、そTypeScriptではなく HelloRequestのようなリクエストやレスポンスの実体部分が、hellorequest.jsのように別々のjsファイルのとして出力されました)
typescriptを指定できなかったですが、--js_out=のオプションはつけないとhelloworld_pb.d.ts とHelloworldServiceClientPb.tsだけが生成されることになり、リクエストやレスポンスの定義ファイルだけで実体がない状態になってしまい、実行できません。なのでtypescriptだけではなく一部jsを含む形になりました。
全てがtypescriptというわけではないですが型定義はあるのと、jsファイルも自動生成されたもので編集はしないものなので、コードを書くときには気にしなくとも大丈夫な部分ものだとは思います。
protoファイルからtypescriptでの生成もできたので、clientのjsをtypescriptへ書き換え、jsのときと同様の動作も確認できたのでtypescriptへの書き換えもうまくいきました。このときtypescriptをwebpackで扱えるようにするため、ts-loader, typescript をnpm installして追加しています。
typescriptで書いたクライアントのコードは次の通りです。
import * as grpcWeb from 'grpc-web'; import { GreeterClient } from './HelloworldServiceClientPb'; import { HelloRequest, RepeatHelloRequest, HelloReply} from './helloworld_pb'; class HelloworldApp { greeterService: GreeterClient; constructor(greeterService: GreeterClient) { this.greeterService = greeterService; } sayHelloworld(name: string) { const request = new HelloRequest(); request.setName(name); this.greeterService.sayHello(request, {}, (err, response) => { console.log(response.getMessage()); }); } streamRequest(name: string, count: number) { const streamRequest = new RepeatHelloRequest(); streamRequest.setName(name); streamRequest.setCount(count); const stream = this.greeterService.sayRepeatHello(streamRequest, {}); stream.on('data', (response: HelloReply) => { console.log(response.getMessage()); }); } sayHelloAfterDelay(name: string, delay: number) { const request = new HelloRequest(); request.setName(name); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + delay); this.greeterService.sayHelloAfterDelay(request, {deadline: deadline.getTime().toString()}, (err, response) => { console.log('Got error, code = ' + err.code + ', message = ' + err.message); }); } } const greeterService = new GreeterClient('http://localhost:8080', null, null); const app = new HelloworldApp(greeterService); app.sayHelloworld('world'); app.streamRequest('world', 5); app.sayHelloAfterDelay('world', 1);
バンドルされたファイルサイズを確認
最後webpackで生成されたmain.jsのファイルサイズがどれくらいになっているのかも確認してみました。 (確認にはtypescirptに書き換えたものを使っています)
特に最適化などは何も行っていない状態で、生成されたファイルは確認する403KBと、400KBを超えていました。
何が大きいのか確認していきます。どのファイルが大きいのかを調べるときにはwebpack-bundle-size-analyzer を使いました。
結果は↓のような内訳になっていました。
$ npx webpack -p client.ts --json | webpack-bundle-size-analyzer grpc-web: 217.97 KB (48.8%) google-protobuf: 156.66 KB (35.1%) buffer: 47.47 KB (10.6%) base64-js: 3.85 KB (0.862%) ieee754: 2.02 KB (0.452%) webpack: 489 B (0.107%) isarray: 132 B (0.0289%) <self>: 17.85 KB (4.00%)
grpc-webとgoogle-protobufで8割以上占めている状態です。書き方によってはもっと小さくできるのかもしれないですが、今回の書き方だとこのような結果になりました。
また参考までに webpackのoptimization.splitChunks を設定してnode_moduleだけをvendor.jsとしてまとめ、それ以外をmain.jsとした場合それぞれのファイルサイズは次のようになりました。
8.3K main.js 395K vendors.js
gRPC-webを使ってみて
先日リリースされたgrpc-webを使ってjsとtypescriptでwebブラウザからgRPCを試してみましたが、protoというサーバとクライアントで共通のものから自動生成できるのはやはり便利ですし、実装も楽でした。
今回は簡単な実装のみだったのでReactやVueなども使ったアプリケーションを作って見ようかと思ってます。
ファイルサイズの規模感については正直まだわかっていないところですが、生成されたファイルは大きそうな印象ではあるので、ファイルサイズについてもまたどこかで調べてみようと思います。
redisとGoでランキングを作ってみた
「アクセスの多かったページ」のようなランキングを実装したかったので、redisを使って実装してみました。 redis自体がキャッシュとして使うことはあったのですが、そのほかで使ったことがなく、redisを使えば簡単に実現できるということだったのでredisを使用しました。
今回ランキングのサンプルとして、次の3つのapiを作ってみました。
apiはGoで実現して、redis環境についてはdockerを使いました。
ランキングのやり方
ランキングを生成には redisの sorted set
というデータ型を用いて実現します。
この sorted set
はキー単位で集合を持ち、各メンバにはスコアを持っており、そのメンバがスコア順に並ぶというものです。
次のようなイメージです。
key | member | score |
---|---|---|
20170701 | 1 | 10 |
2 | 15 | |
3 | 20 |
アクセスの多いページの日別ランキングを生成するのであれば、キーが日付でメンバーがそのページのid、スコアにアクセス数となればその日のランキングを生成できるということです。
実装にはGoのredisライブラリ go-redis/redis
を使用しました。
idのsocreを返すapi
[GET] /rankings/:id
アクセスされたidの現在のスコアを+1したスコアを返すapiです。
レスポンス例
{ "id": 1, "score": 10 }
Goで日別のアクセス数を+1する部分の実装は次のようになります.
key := fmt.Sprintf("daily:%s", time.Now().Format("20060102")) score, err := redisClient.ZIncrBy(key, 1, id).Result() if err != nil { return nil, err } if _, err := redisClient.Expire("key", 7*24*time.Hour).Result(); err != nil { return nil, err }
+1加算するだけでキーがずっと残ってしまうので、不要にたまらないようにするためにもexpireを設定しています。 (今回の場合は7日)
なので、実際の+1する部分は、redisClient.ZIncrBy(key, 1, id).Result()
の部分のみです。
ちなみにZIncrByした時に、memberが存在しないものは初期値が0として扱われるため、初めてのidのものは0に+1されスコアが1のメンバが生成されます。
またここで実行しているのはredisコマンドの ZINCRBY key 1, member
に相当しているものです。
全体のランキングを返すapi
[GET] /rankings
その日の日別ランキングをスコア順の高いもの順に返すapiです。
レスポンス例
[ { "id": 2, "score": 15 }, { "id": 1, "score": 10 } ]
日別ランキングをアクセス数の降順で取得する部分の実装は嗣のようになります。
key := fmt.Sprintf("daily:%s", time.Now().Format("20060102")) result, err := redisClient.ZRevRangeWithScores(key, 0, -1).Result() if err != nil { return nil, err }
ZRevRangeWithScoresでスコアを降順にしたメンバをscoreとともに取得できます。 これだけでランキングをアクセス数の降順取得できます。ソートをする必要がなく取得も簡単ですね。
今回はZRevRangeWithScores(key, 0, -1)で指定しているので、そのキーの全てのメンバを取得しています。取得部分を絞る時には, ZRevRangeWithScores(key, 2, 5) にすると 上位の 2,3,4,5
番のメンバを取得できます(0始まり)
ちなみにここではredisコマンドの ZRANGE key 0 -1 WITHSCORES
に相当しています。
週間ランキング生成のapi
[POST] /rankings
その日の週間ランキングを生成するapiです。
週間ランキングの生成では、日別のランキングを合成することで生成します。
Goでの実装部分は次の通りです。
day := 7 now := time.Now() dateFormat := "20060102" distKey := fmt.Sprintf("weekly:%s", now.Format(dateFormat)) weights := make([]float64, 0, day) targetKeys := make([]string, 0, day) for i := 0; i < day; i++ { t := now.Add(time.Duration(i) * -24 * time.Hour) targetKeys = append(targetKeys, fmt.Sprintf("daily:%s", t.Format(dateFormat))) weights = append(weights, 1) } if _, err := redisClient.ZUnionStore(distKey, redis.ZStore{Weights: weights}, targetKeys...).Result(); err != nil { return err } if _, err := redisClient.Expire("key", 7*24*time.Hour).Result(); err != nil { return err }
その日から7日分の日別ランキングをマージして週間ランキングを生成しています。
マージ部分には redisClient.ZUnionStore(distKey, redis.ZStore{Weights: weights}
を行うとそれぞれのsorted setの和集合を取ることができるので、7日分が1つになった週間ランキングを作ることができます
今回使用した ZUnionStore(distKey, redis.ZStore{Weights: weights}, targetKeys...)
はredisコマンドの
ZUNIONSTORE distKey 7 targetKey1, targetKey2 ... WEIGHTS 1 1 ....
に相当しています。
ZUNIONSTOREは各キーに対して重みをつけることができますが、今回はアクセス数ランキングなので、それぞれの日別のランキングをそのまま使用するために重みを1にしてます。
また月間ランキングもこの週間ランキングと同様にすることで実現することができます。
今回は簡単に実現するためと他と合わせるためという理由でapiとして実現しましたが、特定の時間で動くバッチで実行するのも良いと思います。バッチ処理に分けることで、特定のタイミングでDBに永続化させる処理なども入れることができるので、実用を考えるとバッチ処理は必要になるとおもうのでそちらでやった方が良さそうです。もちろん週間ランキングのユースケースにもよりますが
まとめ
redisの sored set
とGoでランキングapi作成しました。redisを使うことで集計とソートを意識することなく、ランキングを簡単に実現することができました。
今回は ZUNIONSTORE
で全ての和集合を取るやり方を行いましたが、使用するsorted setを増えることでパフォーマンスがどうなるかなど調べられていない部分があるので、また別の機会に確認してみようと思います。
コード
ランキング部分を断片的に載せましたが、githubに今回作成したコードはあります。docker-composeで動作確認できるようにしてあるので試してみてください。
golang.tokyo#6に参加しました
2017年6月1日(木)に行われた、golang.tokyo#6にブログ枠で参加してきました。
golang.tokyo #3以来の2回目の参加になります。
会場は株式会社ディー・エヌ・エーさん
今回のはソーシャルゲームプラットフォームと現地時間の5月15日にアメリカで開催されたGopher Fest 2017がメインコンテンツとなる会でした。
メインセッション
Gopher Fest 2017 参加レポート
tenntenn / 株式会社ソウゾウ
概要
Gopher Fest 2017の中の The State of Go 内容についてお話でした。
またGopher Fest2017のセッションの動画はアップロードされているみたいです。
golangの開発状況
- go1.9は5/1コードフリーズしており、8月上旬にリリース予定
言語仕様の変更が1点. 型のエイリアスを定義できるようになる
- 完全に同じ型として扱える
- キャスト不要
- エイリアスの方にはメソッド定義はできない
- 例: type Applicant = http.Client でApllicantはhttp.Clientとして同様に扱える
標準ライブラリの変更
- math/bits => ビット演算に便利なライブラリ
- sync.Map => スレッドセーフなマップ
- html/template => スクリプトのインジェクトがエラーになるようになり、より安全に
- os.Exec => 重複する環境変数を削除するように一番後ろのものを優先する.直感的になった
-
https://tip.golang.org/pkg/ のようにtipをつけてアクセスすると、最新のmasterのドキュメントを見ることができる
ランタイムの改善
- 多くのものがベンチマーク結果が良くなった
ツール類の改善
感想
スレッドセーフなマップなど便利そうだし、go testの改善でvendor配下を無視してくれるのも良いですね。ベンチの結果もよくなっているということなのでgo1.9のリリースが楽しみになりました。
初めてGolangで大規模Microservicesを作り得た教訓
村田雄一 / 株式会社ディー・エヌ・エー
スライドが公開されたら追記します
概要
実際のプロダクションにおいて、Golangで大規模なMicroservices構成のAndAppを作ったときに得た教訓の紹介でした。
1. フレームワークにこだわらない
- 洗練されたnet/httpがあり、その足らずはライブラリで補完すれば大丈夫
- 言語仕様がシンプルかつgofmtがあるので、コーティングスタイルは似てくる
2. interfaceを尊重する
- 独自エラー型を利用する場合にNilには型があるという罠が踏んだ経験から、素直に他の関数のようにerrorインターフェースに統一したほうがよかったという
- interfaceの定義されてたものは積極的に使用したほうが標準ライブラリとの連携がシンプルになる
- Interfaceで定義されてるものはInterfaceを維持する
3. regex compile / reflection は遅い
- gojsonschemaを使って、バリデーションのたびに毎回regexコンパイルしていたことやrelectionの多用をしていたためバリデーションをかけるとかなり遅くなった
- チューニングした結果2倍以上の改善
- regexマッチのキャッシュ
- reflection多用部のロジックチューニング(無くすことはできないので、ロジックの無駄を省く)
- gojsonschemaをチューニングするより、go-jsval段違いに速かった
4. 非対称暗号は遅い
- 非対称鍵の署名が重い
- opensslと比べてGoのcrypt/rsaが貧弱
- 対象鍵の署名: 0.37msec
- 非対称鍵の署名(Goのcrypt/rsa): 486msec程度
- 非対称鍵の署名(openssl): 50msec程度
- cgoを使ったopensslのバインディングも存在.(ただApp Engineのサンドボックス環境の都合で利用できない)
- goの最新版(1.7)でもあまり改善されていない様子
まとめ
- 困った時にはGoの哲学に帰りシンプルなアプローチを取る
- Goを過信せずにパフォーマンスに気を配る
感想
非対称鍵の署名処理部分を(PHP + openssl)で外出してhttpリクエストしたほうが早いということがすごく印象に残りました。他にも独自のエラー型のことやパフォーマンスのことも参考になりました
LT
ゲーム開発には欠かせない?!あれをシュッと見る
Ryosuke Yabuki (@Konboi) / 株式会社カヤック
概要
CSVビューアを作成した時のお話でした。
GitHub - Konboi/csviewer: csv viewer command
背景
使用したライブラリの紹介
-
GitHub - soh335/sliceflag: slice flag for flag package of go
- 同一のオプションで複数の値を受け取る
- 普段のflagの使い方と大きく変わらない
- 簡単かつ綺麗にテーブル形式で表示
- ヘッダとフッタが便利
感想
便利なライブラリの紹介だったのでcliツールを作る際には役立てたいですね
マスタデータの管理がcsv等なのはどこにでもあるのだと改めて感じることができる回でした...
Go Review Commentを翻訳した話
鎌田健史 (@knsh14) / KLab株式会社
概要
Go Review Commentsを翻訳することで得た学びを紹介でした。
Code Review Comments (CodeReviewComments · golang/go Wiki · GitHub) とは?
- コードレビューする些細に見るべき箇所をいい感じにまとめたもの
- effective GOをさらに簡単にしたもの
その日本語訳を書いた (#golang CodeReviewComments 日本語翻訳 - Qiita)
内容は大きく4つの種類がある
- コードの見た目を改善 => 大体はツールで解決
- コメント、文章の体裁 => コード内の文章の書き方のアドバイス
- tips系 => より良い方法を紹介
- 設計の指針 => レシーバタイプの考え方など
翻訳を書くきっかけ
- Goコードをレビューしてもらった時に大量の指摘を受けた
- その時にCode Review Commentsを渡せれる
- 読んだ証拠に自分で翻訳
翻訳してよかった点
- 正解パターンを勉強できるのが効率が良かった
- 英語の勉強にもなり、他の英語書籍への挑戦するハードルも下がった
まとめ
- Code Review Commentは目を通してほしい
- 良いドキュメントの翻訳は勉強になる
感想
Code Review Commentは把握していなかったので、これを気に読んで翻訳された日本語訳のものから読み、自分のコードやレビュー時をよりよくしていきたいです。
ScalaからGo
James / 株式会社エウレカ
概要
Scalaの人がGoに移ってきた時のお話です。
- 関数型開発はGoできますか? => No
- 関数型開発のコンセプトはGOで使えますか => Yes
- 関数型開発の考え方
- 関数型開発は副作用がない開発
- 関数の副作用があるとテストしにくしバグの原因になる
- コードとしてだと例として部分適用
- どちらが好きか?
感想
副作用があるのかないのかはっきりさせるということは、Goでも関数型でも同じだと思うので、普段のコードから考えて書いてみようと思えました。今までは正直意識できていなかったので。。。
Crypto in Go
Kengo Suzuki / マネーフォワード
概要
暗号化のお話でした
- Goにおける暗号アルゴリズムを利用
- AES
- 固定長しか扱えない
- 任意の長さの平文に暗号化するためにECBを始め、様々なモードがある
- AES + PKCS7パティング + HMAC
- 実装するのに行数が多く面倒
- AES + GCM
- アメリカ国立標準技術研究所(NIST)にも標準として認められている
- とてもシンプルに実現
最後に
最新のgo1.9の話や実プロダクトでの知見だけでなく、他言語からの視点,暗号の話など様々な有意義な内容に触れることができ、楽しい会でした。
運営の皆さんありがとうございました。またぜひ参加したいです
golang.tokyo#3に参加しました
先日golang.tokyo#3にブログ枠で参加してきました。
ブログ枠で参加したのは、自分のアウトプットを向上させるためです。
アウトプット初心者の拙い文章ですがご容赦ください。
ちなみにこの golang.tokyoは今回で3回目の開催ですが、自分自身は初参加です
メインセッション
久保達彦(@cubicdaiya) / 株式会社メルカリ
Accelerating real applications in Go
使用されているchoconとgaurnを題材にしたパフォーマンスでの工夫点のお話です。
chocon - Persistent connector between multi datacenters
- メルカリの日本とアメリカとヨーロッパという3つの離れたネットワークのレイテンシーを抑えるためのプロキシサーバ
- プロキシサーバはサーバでありクライアントなので、両方の最適が必要.
- 特にのクライアントの方が必要で net/http/transport.go の3つの値が大切になる
- MaxIdleConns (デフォルト値: 100)
- MaxIdleConnsPerHost (デフォルト値: 2)
- IdleConnTimeout (デフォルト値: 90)
gaurun - Push notification provider
- プッシュ通知用サーバでHTTP/2でGCMとAPNsへのリクエストをプロキシする
- 30分以内に全ユーザへのプッシュを行うなどのために高い並列実行性が必要
- キューで受けてworkerがそれぞれプッシュ通知を行う。このworker数分のgoroutineを起動
- workerがそれぞれプールを持つことで worker × pool のプッシュ通知を送信することが可能になった
- ブロッキングしないために capとlenで channelの数とキュー数をモニタリング
- 全体の状態をモニタリングするためにメトリクスのエンドポイントをgo用とapp用にそれぞれ用意
感想
遠く離れたネットワークではないにしろ、マイクロサービスアーキテクチャなどで各サービス間での通信クライアントを作成することがあるので、興味深かったです。通信クライアント作成時にレイテンシを少しでも抑えるためにも自分でも標準パッケージについて理解を深めようと思います。
金子慎太郎( @kaneshin ) / 株式会社エウレカ
登壇予定だったのですが、体調不良で欠席とのことでした。
残念でしたが、また後日ブログで書くとの事だったので、楽しみにして待ってようと思います。
Carlo Alberto Ferraris / 楽天株式会社
goでどうこうといった話ではなく、開発現場に当てはまるお話でした。
概要
- 価値を共有し、最適にはきちんと計測を行う
- 開発者 > サーバコスト
- 何事も失敗することを前提に冗長性を確保する
- リカバリはプレイブックではなく、自動化する。だけどすべてを自動化しようとは言わない
- コードを読む時間 > コードを書く時間。読むほうが10倍時間を使う。だからこそシンプルであるべき
- 読みづらい速いコード < 読みやすい遅いコード。開発者 > リソースコストなので、読みやすいコードを優先するべき
- KISS (Keep It Siimple, Stupid)
- 目的を1つにして、分離/可視化する
- シンプルに保ち、すべてを計測し、問題を最適化する
感想
シンプルにすることや目的を1つにするなど、今一度気を引き締めて実践していたいとと思います。twelve-factorは聞いたことはあったもののきちんと読んでいないので、この機会に読んでみます。日本語に翻訳されているものもあるようです。The Twelve-Factor App (日本語訳)
そして、このセッションは自分の英語力の足りなさを痛感する回でした...もっと聞き取れるともっと吸収できることがあったんだろうにと、英語ができないともったいないと改めて実感しました 。
LT
辻 純平 (@jun06t) / AWA株式会社
Streamを使うことでのメリットとその実装例を示したTipsの話です。
概要
- ioutil.ReadAll()をつける癖をなくし、Streamを使う
- 多くの標準パッケージがstreamをサポートしているので、bytesの変換しないくても良い
- ベンチ結果からもstreamを使う方がbyte変換する場合に比べ、メモリの消費量やアロケーション回数が少なくなっている
実装例
感想
自分が書いたものを見返すとjson.NewDecoderで記述していた部分があったのですが、正直先輩たちが書いているのを参考にして書いており、恥ずかしいながら自分自身パフォーマンスの差のことを理解せずに使っていました...
streamと使う場合と[]byteでの場合の2つ方法の違いがわかったので、自分も意識して活かしていこうと思います。「推測するな、計測せよ」自分も実践していかないと
Ryosuke Yabuki (@Konboi) / 株式会社カヤック
git-schemalexを用いたマイグレーションとその開発の流れのお話です。
概要
Rails方式のマイグレーション (都度SQL等を記述していく形式)
- アプリケーションと同じ言語で記述できる
- DSLを覚えるコストがかかる
- コンフリクトが起きやすい
git-schemalexを用いたマイグレーション(スキーマの差分からSQLを生成する)
schemaファイルをgoのstructから自動生成するddl-makerを作成
開発の流れ
- struct定義
- ddl-makerでschemaファイルを生成
- schemalexでDBに反映
感想
schemalexのマイグレーションとstruct定義からの自動生成はスキーマの管理に便利そうで興味深かったです。自分が関わるプロジェクトではgooseを使い、都度変更点のsqlを記述し実行する形を取っていますが、実行時などで他のメンバとの前後関係でうまく実行できないということも実際に体験したことがあります。
自分自身Rails方式以外のマイグレーション方式は試したことないので、今回のschemalexから試してみようと思います。
高橋 明 (@Talos208) / 株式会社スプラウト
database/sqlの使用方法と設定値のお話です。
概要
- db.Closeを書きたくなるが、ドキュメントを読むと複数のgoroutine間で共有することを意図しているから、DBをcloseすることはほとんどないと記述されている
- ソースを読むと、都度open/closeすると接続/切断のコストがかかることがわかるので、sql.Openは起動時にDB.Closeは最後にそれぞれ1回呼ぶ
現場で起こった事例
- sql.OpenとDB.Pingは成功するが、DB.Queryの実行時にコネクションエラー
- go -> maxscale -> MariaDBという構成
- go -> maxscaleはコネクションが成功している
- maxscale -> MariaDB ここのコネクションがタイムアウト... サーバにはタイムアウトの通知がされない
- go1.6で導入されたsetConnMaxLifetimeで解決 (一定時間以上経過したコネクションは再利用しない)
感想
ここでも感じたのは、やはりググるだけではなく、標準パッケージのソースコードなどを読むということの重要性です。
自分の関わるプロジェクトでもdatabese/sqlのMaxIdleConnsの設定値を変えて改善したというものがありましたが、そもそも自分がコネクション周りの知識が足りていないのできちんと理解できていないと思うので、これを機にコネクション周りの勉強もしてみようと思います。
最後に
標準パッケージのことをもっとよく知ろうと思えた会でした。また、ググるだけでなく、golangのソースコードを読むことが重要だと感じることができました。
休憩時間も色々お話できて楽しかったです。次回もぜひ参加したいですね。
じゃんけんに負けてTシャツをもらえなかったのは、残念でした...