雰囲気で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.

詳しくはこちらで

rust-carbonっていうライブラリ作った

先月ようやく rust 1.0 が出たということで、いじってみるがてらライブラリをひとつ作った。

rust-carbon という名前で、rust で時間の扱いをやりやすくするものだ。

インターフェイスphpcarbon という有名なライブラリを真似たので、それが名前の由来となっている。以下使い方とか書いていく。

インストール

Cargo.toml にこれ書いて

[dependencies]
carbon = "0.1.*"

こう

extern crate carbon;

使い方

こんな感じ。現在時刻が 2015-01-15 01:30:30.500000000 だとして考えて欲しい。

// インポート
use carbon::*;

// 現在時刻は 2015-01-15 01:30:30.500000000

DateTime::now().start_of().second();
// => carbon::DateTime に 2015-01-15 01:30:30.000000000 が詰まって返る
DateTime::now().start_of().minute();
// => carbon::DateTime に 2015-01-15 01:30:00.000000000 が詰まって返る
DateTime::now().start_of().hour();
// => carbon::DateTime に 2015-01-15 01:00:00.000000000 が詰まって返る
DateTime::now().start_of().day();
// => carbon::DateTime に 2015-01-15 00:00:00.000000000 が詰まって返る
DateTime::now().start_of().month();
// => carbon::DateTime に 2015-01-01 00:00:00.000000000 が詰まって返る

DateTime::now().end_of().second();
// => carbon::DateTime に 2015-01-15 01:30:30.999999999 が詰まって返る
DateTime::now().end_of().minute();
// => carbon::DateTime に 2015-01-15 01:30:59.999999999 が詰まって返る
DateTime::now().end_of().hour();
// => carbon::DateTime に 2015-01-15 01:59:59.999999999 が詰まって返る
DateTime::now().end_of().day();
// => carbon::DateTime に 2015-01-15 23:59:59.999999999 が詰まって返る
DateTime::now().end_of().month();
// => carbon::DateTime に 2015-01-31 23:59:59.999999999 が詰まって返る

当然メソッドチェーンできる

DateTime::now().start_of().day().end_of().hour();
// => carbon::DateTime に 2015-01-15 00:59:59.999999999 が詰まって返る

単体テストなどで now の返り値を指定したい場合があると思う。rust-carbon では以下のように指定できる。

// 例えば現在時刻を 2015-01-01 00:00:00 にしたいとする
let tm = time::Tm {
  tm_sec: 0, tm_min: 0, tm_hour: 0, tm_mday: 1, tm_mon: 0, tm_year: 115, tm_wday: 4, tm_yday: 0, tm_isdst: 0, tm_utcoff: 0, tm_nsec: 0 };
};

let test_now = DateTime::create_from_tm(tm);
DateTime::set_test_now(test_now);
DateTime::now();
// => carbon::DateTime に 2015-01-01 00:00:00 が詰まって返る

また、rust-carbon は rust の準標準ライブラリである rust-time の薄いラッパーである。

そのため、rust-time の構造体及びそこに生えているメソッドに簡単にアクセスできる。to_string() とか細かい時間の調節とかはそっちのメソッドを使ってもらうことになる。

// 現在時刻は 2015-01-15 01:30:30.500000000

DateTime::now().tm.strftime("%Y-%m-%d %H:%M:%S").ok().unwrap().to_string();
// => "2015-01-15 01:30:30" という string が返る

use std::ops::{Add, Sub};

let hour = time::Duration::hours(1);
DateTime::now().tm.add(hour)
// rust-time の time::Tm 構造体に 2015-01-15 02:30:30.500000000 が詰まって返る

他詳細なことについては README を見てほしい。

補足

rust-carbon は習作で、冒頭にも書いたとおり、とりあえず rust 1.0 を触ってみたかったという動機から作られている。実用を目的としたわけではないので、実際に使おうとすると困る点がいくつかある。具体的には、まず UTC 以外の時間が扱えない(日本人だと絶望的)。あと time::Tm.add()/sub() を使った時に time::Tm が返ってしまうので、細かい時間調節をしようとすると小回りが効かない。

この辺りは気が向いた時とか実際に使い始めたとかされたら直していきたい。

また、rust で時間を扱うライブラリとしては、rust-chrono もある。rust-carbon はシンプルなのに対して、rust-chrono は複雑なことができる。 まぁ個人的には rust-chrono はインターフェイスがゴチャゴチャしていて気にくわないし、元々 rust-time にかなり任せられそうなので、あんな大仰で面倒くさそうなラッパーいるか?というのが正直な気持ちだが、もし rust で便利な時間操作系のライブラリを探しているとかいうのであれば、検討すると良いと思う。

rust 1.0 の雑感

rust は地味に nightly の頃にいじってみていたのだが 当時作ったライブラリは何にもいじってなくてもデイリーでビルドがぶっ壊れていてなかなか愉快だった。 公式ドキュメントの説明も難しかったし、まともな rust のコード読もうとすると rust 本体のコードしかないみたいな感じだった。

正直 1.0 も見送って 1.1 になるまで触るのやめようかなと思っていたのだが、今回の 1.0 でも触っていて先に挙げていたようなストレスはほとんどなくなっていた。ビルドもぶっ壊れなくなったし、今回の公式ドキュメント もだいぶ読みやすくなったと思う。

rust はモダンな言語仕様をふんだんに取り入れていて、本来持っているポテンシャルは高いと思う。細かいところはいちいち説明しきれないのでぜひ自分で見てくれという感じだが、結構堅牢でカッコイイ実装ができる。その上 ownership,borrow,lifetime の概念により省メモリに抑えることができる。結果として堅牢さと軽さが両立するのでかなりいい言語なのではという気がする。触っていてなかなか楽しかった。

ただ、go みたいな手軽さはほとんどない。サッと書いて動かして感動を得るというよりは、堅牢に設計した時の俺スゲー感とか、メモリチューニングを楽しんでいく言語だと思う。(go との比較ついでに書くと、並列処理書くのがまだ面倒なのが痛いが)

ちなみに、rust を今現在の仕事の現場で使いたいかというとそれはNOになる。あんまりぶっ壊れなくなったとはいえヘビーに触ったわけではないのでまだ怖いし、そもそも書く人が多いとはいえない言語なので現場で導入というのは考えられない。

言語本来のポテンシャルとしては他人と一緒に開発するのに向いているし、これから伸びていったら面白そう。

ctrl-aでDJした

一応備忘録として書いておこうと思う。割と前の話だけど、去る5/28に、秋葉原mograでctrl-aっていうイベントがあって、そこでDJした。

出るきっかけは本当にたまたまで、プライベートで仲良いkazzoneに飲み屋で出る?って言われたからだった気がする。酔っぱらってて出る出るって返事したら本当に出してもらえた。自分のような人見知りでもこういうことがあるのだから、人の縁というのは恐ろしい。

DJ自体は本当に初心者で、去年会社辞めて有給消化してるときにいじりはじめてそれからなんとなく練習してた程度だった。mograといえばアキバ文化の一端を担う聖地だし、自分じゃなくたってあそこにイベントで立ちたい人はいっぱいいると思う。出演前は嬉しいよりも畏れ多い気持ちで一杯だった。

いざイベントが始まると恐ろしいぐらいに人がたくさん来て盛り上がった。正直何が起きてるのかよくわからなかった。実際平日であれだけ人が集まったのは異常だと複数の人から聞いた。

自分は出番がオープニングだったので人は多くなかったのだけど、kazzoneがDJしてる時にVJとして前に立っていたので、場の盛り上がりを肌で感じることができた。好きな曲が流れた時に起こる歓声というか怒声みたいなものと、限界までボリュームが引き上げられたキック音とが混ざって起きる異常な振動は、日常生活ではなかなか感じることはできない。お客さんはちゃんと訓練されたクズで、色々なところで助けられた。本当にありがたかった。

ちなみにVJは本番の2日前にやってくれって言われて、一夜漬けで準備したとかそういう思い出もある。しかも準備してる時に仕事のほうでデカい障害出してしまって頭おかしくなりそうなぐらい脳汁出てた。キツかった。

DJの方は身内には良かったみたいなことは言ってもらえたけど、まぁ身内だし、初めてにしてはという枕詞が省略されていたので、客観的にみてお世辞にも上手くいってはいないと思う。一応ネットのどこかには当日のプレイを再現して上げたものもあるけど、とてもソーシャルアカウントとかで宣伝する気にはならない。選曲は本当に難しい。イベントの文脈、客層、自分の順番、他のDJの趣向、BPM、キー、その他諸々考えなければならないことがたくさんある。みんな軽々と回してるけどその難しさが身にしみた。

ctrl-aはおかげさまで次回もあるし、また出させてもらえそうなので次はもっとうまくやれるように頑張りたい。

goでテスト並列実行させたら楽な気がする

チームにある程度テストがかさばってくると、テストの実行時間が問題になる。せっかくテストを頑張って作っていても、実行時間が10分とか20分になってくるとどうしてもテストを動かすのが億劫になる。 せっかっく開発がノッてきたのに、テストのフィードバックがすぐ得られないと集中が途切れてしまう。手元なら実行範囲を狭めればいいだけだが、 CI 環境ではそうもいかないし、 そこでこけている原因を探るのに一回のトライエラーが10分間隔とかになると辛い。

まぁ、そういう建前は色々あるけど、テストのフィードバックが早く得られると困る人はいないと思う。

そういうわけでテストを並列実行しようという話が出てきて、php 界隈で並列実行を試みようとすると例えば以下のライブラリに行き当たる。

paratest は一番メジャーっぽいんだけど、php で書かれていて心配。php でマルチプロセスを行うのはかなりの黒魔術が必要なはずで、正直地雷踏みそうで近寄りたくない。 あとで見てみるとそんなことなさそう。昔読んだ php でマルチプロセスしてるコードが並列関係なく単純に出来が悪くて悪い印象持っていただけっぽい。

一方で parallel-phpunit は shell でなんか安心感がある。ただやっぱりそこは shell なのでテストの実行単位を分けていったりするのにもう少し自由度があったほうがいいのではという気がする。また少し離れたところでいうと、RRRSpec とかもある。これは確かにすごいのだが、こういう大艦巨砲が必要なわけではない。とにかく楽に学習コストとか下げてやりたい。

そんな感じの文脈で、ある程度費用対効果が良さそうな線を考えると、go で phpunit の並列実行するスクリプト書くのがいいんじゃないかなあという結論に至ってえいやで書いてみた。

package main

import (
    "fmt"
    "os/exec"
    "path/filepath"
    "runtime"
    "sync"
)

var (
    runner  = "./vendor/bin/phpunit"
    target  = "./tests/*"
    exclude = []string{"tests/fixture"}
    outputs = []byte{}
)

func main() {
    files, err := filepath.Glob(target)
    if err != nil {
        panic(err)
    }

    var wg sync.WaitGroup
    runtime.GOMAXPROCS(runtime.NumCPU())
    for _, file := range files {
        wg.Add(1)
        if contains(file, exclude) {
            wg.Done()
            continue
        }
        go func(file string) {
            out, err := exec.Command(runner, file).Output()
            if err != nil {
                wg.Done()
                fmt.Println("failed to execute: " + runner + " " + file)
                panic(err)
            }
            outputs = append(out)
            wg.Done()
        }(file)
    }
    wg.Wait()

    for _, output := range outputs {
        fmt.Printf("%c", output)
    }
}

func contains(value string, slice []string) bool {
    for _, s := range slice {
        if value == s {
            return true
        }
    }

    return false
}

このスクリプトでは、tests 以下のディレクトリごとに phpunit を並列実行している。今回はテストランナーを phpunit にしているけれど、var 以下を書き換えれば rspec でも karma でも testem でも何でも動かせる。

本当にとりあえずで書いたのでテストの実行単位が雑すぎるというのはあるけど、まぁそれに関しては拡張は難しくないのであまり気にしていない。 それよりも一番弱点になるのは CI 環境で go が必要になることだろうけど、別に致命的ではないと思う。go の書き方がなってない、とかそういう話は素直にすいませんといいたい。

とにかく楽に、費用対効果が高い形でテストを並列実行して高速化したいという話であれば go でスクリプト書くのはよい気がしてる。