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つの要素があります。

  1. Service
  2. Route
  3. Configuration
  4. 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へリクエストできます。

ポートの32380knative-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だと自動で最新のものになるようなので、 ConfigurationRoute を追加していきます。 今ある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"

curlコマンドでリクエストできるようになります。

次にイメージに変更を加えたバージョンを指定し、今までのバージョンには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を実際に試してみました。

github.com 

試したこと

  • リポジトリ内にあるexampleをもとに、実際にprotoファイルからgRPC-webでコードを生成し実行
  • gRPC-webでTypeScriptを生成して同様に実行
  • バンドルしたファイルの大きさの確認 

hello world

公式のリポジトリ内にexampleとして用意されている helloworldから試していきます。

helloworldの構成としては次のような形になっています。

webブラウザ(grpc-web) --- envoy --- grpcサーバ(nodejs)

README.mdにあるように実行までは次の流れです。

  1.  makeコマンドからprotoc-gen-grpc-webをインストールして、protoファイルからjsのコードを生成
  2. node server.js でnodeのgRPCサーバを立ち上げる.
  3. dockerを使って、envoyを立ち上げる
  4. webpackで、クライアントサイドjsをバンドルし、1つにまとめる
  5. 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で確認することができました。

f:id:terachanple:20181031213556p:plain

main.js以下の3つのリクエストがgrpc-webでのリクエストです。

またコンソール上に出力された結果としてはこのようになりました。

f:id:terachanple:20181031213511p:plain

 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.tsHelloworldServiceClientPb.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を作ってみました。

  • idのスコアを返すapi
  • 全体のランキングを返すapi
  • 週間ランキングを生成する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 を使用しました。

github.com

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で動作確認できるようにしてあるので試してみてください。

github.com

golang.tokyo#6に参加しました

2017年6月1日(木)に行われた、golang.tokyo#6にブログ枠で参加してきました。
golang.tokyo #3以来の2回目の参加になります。

golangtokyo.connpass.com

 

会場は株式会社ディー・エヌ・エーさん

 

今回のはソーシャルゲームプラットフォームと現地時間の5月15日にアメリカで開催されたGopher Fest 2017がメインコンテンツとなる会でした。

 

メインセッション

Gopher Fest 2017 参加レポート

tenntenn / 株式会社ソウゾウ

www.slideshare.net

概要

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を無視
    • テスト関数一覧を表示可能に
  • go doc フィールドにリンクが貼れる


感想

スレッドセーフなマップなど便利そうだし、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) / 株式会社カヤック

speakerdeck.com

概要

CSVビューアを作成した時のお話でした。

GitHub - Konboi/csviewer: csv viewer command

背景

  • マスターデータがCSV, Excel, SpreadSheet
  • カラムとデータの関連性が見辛い、空欄が辛いなどなど、CSVで困ることがあったので作成

使用したライブラリの紹介

感想

便利なライブラリの紹介だったのでcliツールを作る際には役立てたいですね 

マスタデータの管理がcsv等なのはどこにでもあるのだと改めて感じることができる回でした...

 

Go Review Commentを翻訳した話

鎌田健史 (@knsh14) / KLab株式会社

speakerdeck.com

概要

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 / 株式会社エウレカ

www.slideshare.net

概要

Scalaの人がGoに移ってきた時のお話です。

  • 関数型開発はGoできますか? => No
  • 関数型開発のコンセプトはGOで使えますか => Yes
  • 関数型開発の考え方
    • 関数型開発は副作用がない開発
    • 関数の副作用があるとテストしにくしバグの原因になる
    • コードとしてだと例として部分適用
  • どちらが好きか?
    • 個人的にはscala => scalaの長期成長が楽しい 
    • 会社としてはGoがいいかもしれない => 初心者でも綺麗なコードを書いやすい

感想

副作用があるのかないのかはっきりさせるということは、Goでも関数型でも同じだと思うので、普段のコードから考えて書いてみようと思えました。今までは正直意識できていなかったので。。。

 

Crypto in Go

Kengo Suzuki / マネーフォワード

paper.dropbox.com

概要

暗号化のお話でした

  • Goにおける暗号アルゴリズムを利用
  • AES
    • 固定長しか扱えない
    • 任意の長さの平文に暗号化するためにECBを始め、様々なモードがある
  • AES + PKCS7パティング + HMAC
    • 実装するのに行数が多く面倒
  • AES + GCM
    • アメリカ国立標準技術研究所(NIST)にも標準として認められている
    • とてもシンプルに実現

 

最後に

最新のgo1.9の話や実プロダクトでの知見だけでなく、他言語からの視点,暗号の話など様々な有意義な内容に触れることができ、楽しい会でした。

運営の皆さんありがとうございました。またぜひ参加したいです

golang.tokyo#3に参加しました

先日golang.tokyo#3にブログ枠で参加してきました。
ブログ枠で参加したのは、自分のアウトプットを向上させるためです。

アウトプット初心者の拙い文章ですがご容赦ください。

ちなみにこの golang.tokyoは今回で3回目の開催ですが、自分自身は初参加です

golangtokyo.connpass.com

 

メインセッション

久保達彦(@cubicdaiya) / 株式会社メルカリ

Accelerating real applications in Go

使用されているchoconとgaurnを題材にしたパフォーマンスでの工夫点のお話です。

chocon - Persistent connector between multi datacenters

  • メルカリの日本とアメリカとヨーロッパという3つの離れたネットワークのレイテンシーを抑えるためのプロキシサーバ
  • プロキシサーバはサーバでありクライアントなので、両方の最適が必要.
  • 特にのクライアントの方が必要で net/http/transport.go の3つの値が大切になる
  1. MaxIdleConns (デフォルト値: 100)
  2. MaxIdleConnsPerHost (デフォルト値: 2)
  3. 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 / 楽天株式会社

slides.com

goでどうこうといった話ではなく、開発現場に当てはまるお話でした。

概要

  • 価値を共有し、最適にはきちんと計測を行う
  • 開発者 > サーバコスト
  • The Twelve-Factor App 

  • 何事も失敗することを前提に冗長性を確保する
  • リカバリはプレイブックではなく、自動化する。だけどすべてを自動化しようとは言わない
  • コードを読む時間 > コードを書く時間。読むほうが10倍時間を使う。だからこそシンプルであるべき
  • 読みづらい速いコード < 読みやすい遅いコード。開発者 > リソースコストなので、読みやすいコードを優先するべき
  • KISS (Keep It Siimple, Stupid)
  • 目的を1つにして、分離/可視化する
  • シンプルに保ち、すべてを計測し、問題を最適化する

感想

シンプルにすることや目的を1つにするなど、今一度気を引き締めて実践していたいとと思います。twelve-factorは聞いたことはあったもののきちんと読んでいないので、この機会に読んでみます。日本語に翻訳されているものもあるようです。The Twelve-Factor App (日本語訳)

そして、このセッションは自分の英語力の足りなさを痛感する回でした...もっと聞き取れるともっと吸収できることがあったんだろうにと、英語ができないともったいないと改めて実感しました 。

 

LT

辻 純平 (@jun06t) / AWA株式会社

slides.com

Streamを使うことでのメリットとその実装例を示したTipsの話です。

概要

  • ioutil.ReadAll()をつける癖をなくし、Streamを使う
  • 多くの標準パッケージがstreamをサポートしているので、bytesの変換しないくても良い
  • ベンチ結果からもstreamを使う方がbyte変換する場合に比べ、メモリの消費量やアロケーション回数が少なくなっている

実装例

  • json.Unmarshal => json.NewDecoder
  • io.Copy, io.CopyBuf
  • bytes.Buffer =>  io.Pipe
  • io.Reader 

感想 

自分が書いたものを見返すとjson.NewDecoderで記述していた部分があったのですが、正直先輩たちが書いているのを参考にして書いており、恥ずかしいながら自分自身パフォーマンスの差のことを理解せずに使っていました... 

streamと使う場合と[]byteでの場合の2つ方法の違いがわかったので、自分も意識して活かしていこうと思います。「推測するな、計測せよ」自分も実践していかないと

 

Ryosuke Yabuki (@Konboi) / 株式会社カヤック

speakerdeck.com

git-schemalexを用いたマイグレーションとその開発の流れのお話です。

概要 

Rails方式のマイグレーション (都度SQL等を記述していく形式)

  • アプリケーションと同じ言語で記述できる
  • DSLを覚えるコストがかかる
  • コンフリクトが起きやすい

git-schemalexを用いたマイグレーション(スキーマの差分からSQLを生成する)

  • sqlのみ書ければ良い (DSLを覚えなく良い)
  • コンフリクトが起きづらい

schemaファイルをgoのstructから自動生成するddl-makerを作成

開発の流れ

  1. struct定義
  2. ddl-makerでschemaファイルを生成
  3. schemalexでDBに反映

感想

schemalexのマイグレーションとstruct定義からの自動生成はスキーマの管理に便利そうで興味深かったです。自分が関わるプロジェクトではgooseを使い、都度変更点のsqlを記述し実行する形を取っていますが、実行時などで他のメンバとの前後関係でうまく実行できないということも実際に体験したことがあります。

自分自身Rails方式以外のマイグレーション方式は試したことないので、今回のschemalexから試してみようと思います。

 

高橋 明 (@Talos208) /  株式会社スプラウト

www.slideshare.net

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シャツをもらえなかったのは、残念でした...