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

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

ビズオーシャンから添付ファイルを簡単、安全に送るTemply公開しました

社外に書類などを送るときに、ZIPでパスワードかけて添付して送る、というのがめんどくさいので出来たサービスです。

temply.bizocean.jp

プレスリリース
prtimes.jp


サービスについては本サイトに書いてあるので、開発系の話をします。

目的

  • 新卒入社3年目となるtakumi(仮)がメインとなって開発する経験をしてもらう。
  • 私がoceanus等で得たGCP, Docker周りの知見を社内に広める
  • 開発手法としてSPRINTの実践

SPRINTはこれ。邦題が良くないけどみんなで読みました。もちろん経費で購入。

SPRINT 最速仕事術――あらゆる仕事がうまくいく最も合理的な方法

SPRINT 最速仕事術――あらゆる仕事がうまくいく最も合理的な方法

まるまる1週間は難しく全体で2週間かかったり、インタビュー対象はほかの社員に協力してもらったり、本そのままはできませんでした。
また最初のがいまいちだったので公開までに2週やりました。

言語、フレームワーク

Python3.6
Django

開発環境

社内Linux開発サーバーでDocker

バージョン管理

git
今回から初めてPull requestを使うため、すでに契約していたbacklogのgitレジストリを使いました。

本番環境

GCP

  • Google Kubernetes Engine (Django + gunicorn)
  • Cloud SQL (MySQL)
  • Cloud Storage (ファイル置き場)

その他Container RegistoryとかStackdriverで監視とかツール各種。
静的ファイルはCloud Storageから直接配信しているので、WEBサーバーはgunicornのみ。

開発について

初期公開までの開発期間は一時他の案件で中断があったものの賞味3か月程度。
これまでbizocean本体のPHPIDEを使ってましたが、今回は私の押し付けでvimでやってもらいましたが難なく使えている模様。

GCPをフル活用したため、ややこしい一部は除いて、ほぼメインのtakumi(仮)一人でプログラムから、サーバー、インフラ整備までできてしまいました。
シンプルなサービスなので、最初のプロジェクトとしてはちょうどよかったかも。

今後

今回初挑戦の複数人コードレビューでかなりゴミを消せて無駄なテーブルも減ってリソース節約にもなって、読みやすいコードにもなったので、どんどんやっていきたい。
まだmargeやpullではまることが多い。
現在開発できるメンバーは4人だけどそれぞれがメインで年1つずつぐらいは作っていけそう。

GKEのコスト節約を考える2 CPUリクエストを調整してリソースを有効活用する

いろいろ調べてたら前回に引き続くことになった。

uyamazak.hatenablog.com

これまでpodが立ち上がらないエラーをkubectl evなどで確認すると原因はほぼ「Insufficient CPU」だった。

oceanusは、BigQueryにデータを流すなどのあまりメモリを使わない処理が大半なので、ほかのプロダクトだとメモリの場合もあるかもしれない。

ではKubernetesが何をもってCPUの不足を判断しているか調べてみる。

nodeの状況を確認する

まずどのnodeにどのpodが配置されているか確認する
kubectlはシェルのaliasでkcにしてある。

% kc describe node

# 省略
Capacity:
 cpu:     2
 memory:  2054088Ki
 pods:    110

# 省略

Non-terminated Pods:         (7 in total)
  Namespace                  Name                                                              CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ---------                  ----                                                              ------------  ----------  ---------------  -------------
  default                    arms-2064021732-v0q31                                             100m (5%)     0 (0%)      0 (0%)           0 (0%)
  default                    rabbitmq-1915863861-m93bp                                         100m (5%)     0 (0%)      0 (0%)           0 (0%)
  kube-system                event-exporter-v0.1.7-958884745-9n8sd                             0 (0%)        0 (0%)      0 (0%)           0 (0%)
  kube-system                fluentd-gcp-v2.0.9-554wh                                          100m (5%)     0 (0%)      200Mi (14%)      300Mi (21%)
  kube-system                heapster-v1.4.3-1324981535-1wtmq                                  138m (7%)     138m (7%)   302056Ki (21%)   302056Ki (21%)
  kube-system                kube-proxy-gke-oceanus-asia-northeast1-pool-c2m2-dee44aa5-5jgm    100m (5%)     0 (0%)      0 (0%)           0 (0%)
  kube-system                kubernetes-dashboard-1962351010-qr3r9                             100m (5%)     100m (5%)   100Mi (7%)       300Mi (21%)
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ------------  ----------  ---------------  -------------
  638m (33%)    238m (12%)  609256Ki (42%)   916456Ki (64%)

# 省略

各nodeについていろいろ出てくるけど、CPU周りに注目してみると、上記のnodeはCPUが2、計7つのPodが稼働中であり、CPU Requestsは638m (33%) だということが分かる。

podの7つ中5つはkube-systemに使われており、自分のアプリは2つしか使ってないと思うとちょっともったいない感じがする。特にログのfluentdと監視のheapsterの消費が大きい。

node一つ一つにこれらが入ってることを考えると、やはり前回の記事のように小さいnodeをたくさんより、大きいnode少しで済ますのは、全体のkube-systemの数を減らすことができるので、リソース有効活用の意味でも正しいかもしれない。

話がそれた。

これまで100m (5%) とか 638m (33%) は目にしていたけど何を表しているのか深くは考えていなかった。

Kubernetes上のCPUの意味を理解する

調べてみると下記ページに記載があった。

Managing Compute Resources for Containers | Kubernetes

Meaning of CPU

Limits and requests for CPU resources are measured in cpu units. One cpu, in Kubernetes, is equivalent to:
1 AWS vCPU
1 GCP Core
1 Azure vCore
1 Hyperthread on a bare-metal Intel processor with Hyperthreading
Fractional requests are allowed. A Container with spec.containers[].resources.requests.cpu of 0.5 is guaranteed half as much CPU as one that asks for 1 CPU. The expression 0.1 is equivalent to the expression 100m, which can be read as “one hundred millicpu”. Some people say “one hundred millicores”, and this is understood to mean the same thing. A request with a decimal point, like 0.1, is converted to 100m by the API, and precision finer than 1m is not allowed. For this reason, the form 100m might be preferred.
CPU is always requested as an absolute quantity, never as a relative quantity; 0.1 is the same amount of CPU on a single-core, dual-core, or 48-core machine

↓Google翻訳

CPUリソースの制限と要求は、CPU単位で測定されます。 Kubernetesにある1つのCPUは、次のものと同等です。
1 AWS vCPU
1 GCPコア
1 Azure vCore
1ハイパースレッディング機能を備えたベアメタルIntelプロセッサ上のハイパースレッド
分数リクエストが許可されます。 spec.containers []。resources.requests.cpuが0.5のコンテナは、CPUの1つを要求するものの半分のCPUが保証されています。 式0.1は式100mと等価であり、これは "100ミリプ"として読み取ることができる。 一部の人々は "100ミリコア"と言っています。これは同じことを意味すると理解されています。 小数点を含む要求(0.1など)はAPIによって100mに変換され、1mより細かい精度は許可されません。 このため、フォーム100mが好ましい場合がある。
CPUは常に絶対量として要求され、相対量としては決して要求されません。 0.1は、シングルコア、デュアルコア、または48コアマシン上のCPUの量と同じです

環境ごとの1CPUの違いは置いておいて、

1000m → 1CPUを100%使う

ということらしい。1CPUのリソースを時間で1秒=1000msに分割して管理している。

つまりさっきの2CPUのノードで、638m (33%)は、2000m分の638mだから33%(638/2000を計算すると32%ぐらいだけど)と、だいたい67%は空いているという意味になる。

そして、そのデフォルトは100msなので、どんなに頑張ってCPUを食わないように低リソース消費なコンテナを作ったととしても、デフォルトで100mは要求してしまい、Kube-systemを除くと実際は1ノードに最大10個までしか起動できないことになる。もったいない。そのPodが実際にどのくらい使うかは起動時点ではわからないから仕方ないかもしれない。

本当に必要なCPUを確認する

それぞれのpodがどれくらいリソースを使っているかは、稼働中のクラスタでkubectl top podで取得できる

% kc top pod
NAME                                 CPU(cores)   MEMORY(bytes)
arms-2064021732-p2153                30m          28Mi
arms-2064021732-8t1rm                26m          29Mi
arms-2064021732-v0q31                27m          29Mi
gopub-1201051946-x5twq               5m           7Mi
gopub-1201051946-f4gb6               5m           7Mi
redis-pd-2603876801-mbhm5            5m           22Mi
r2bq-3505699855-87rvn                41m          17Mi
rabbitmq-1915863861-0zdxc            1m           53Mi
revelation-2115532661-kl99b          9m           36Mi
revelation-worker-4162157242-m6bf9   3m           83Mi
table-manager-3441943665-xtbhp       4m           16Mi

午後4時に取ったので、ちょっとピークからは外れるけど、中には5m程度のpodもあり、すべてに100mをリクエストしておくのは明らかに無駄っぽい。

CPUの設定を反映する

とりあえず、余裕をもって実使用量の2~4倍程度を設定してみる。

どこで設定したかは上でも貼ったページにパスと値をどう書けば書いてあるので、参考にしていじる。
Managing Compute Resources for Containers | Kubernetes

spec.containers[].resources.requests.cpu

まずCPU使用量が少ないgopubのdeployを編集

kc edit deploy gopub

下記ページを参考に追加する
Configure Default CPU Requests and Limits for a Namespace | Kubernetes

spec:
  replicas: 2
  selector:
    matchLabels:
      run: gopub
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: gopub
    spec:
      containers:
        image: asia.gcr.io/***
        imagePullPolicy: IfNotPresent
        name: gopub
        ports:
        - containerPort: 8765
          protocol: TCP
        resources:
          requests:
            cpu: 20m

とりあえず他にもいくつか追加して再びkc describe nodeをして変わっているか確認

% kc describe node
Non-terminated Pods:         (4 in total)
  Namespace                  Name                                                             CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ---------                  ----                                                             ------------  ----------  ---------------  -------------
  default                    arms-723529091-2bgxz                                             50m (5%)      0 (0%)      0 (0%)           0 (0%)
  default                    gopub-1287380341-nt718                                           20m (2%)      0 (0%)      0 (0%)           0 (0%)
  kube-system                fluentd-gcp-v2.0.9-kb368                                         100m (10%)    0 (0%)      200Mi (14%)      300Mi (21%)
  kube-system                kube-proxy-gke-oceanus-asia-northeast1-c-pool-1-7f0c786d-kdfs    100m (10%)    0 (0%)      0 (0%)           0 (0%)

armsとgopubが100mから50m、20mになっているのが確認できた。

おそらくこれでCPU Request分不足によるpod立ち上げ失敗はなくなるはず。
一時的に負荷が上がりそうなpodは100mのままにして、本番で動かしながら様子を見る。

Google Container EngineがGoogke Kubernetes Engineになってた

それだけ。

Kunernetesの名前が普及するのを待っていたのかな。

略称はGCEとの兼ね合いもあり最初からGKEなので既定路線だったのかも。

cloud.google.com

GKEのコスト節約を考える インスタンスをスケールアップしてノード数を5以下にする

oceanusはGKEで本番運用を行っているけど、Kubernetesのバージョンを上げるたびに、CPUの消費が上がって、ノードが必要になっている気がする。

アプリケーションは大して変更していないし、アクセスも急増しているわけではない。

そこで、なるべくコストを抑える方法を考えた。


GKEの料金は2017/11/18現在下記のようになっている

Google Kubernetes Engine の料金と割り当て  |  Kubernetes Engine ドキュメント  |  Google Cloud Platform

f:id:uyamazak:20171108164828p:plain

なるべく5ノード以下にする

デフォルトでは1時間単位になっているけど、今のサービスはそんなピークのある使用ではないので、1か月で考えた。

東京リージョンでは、無料枠である5ノードを超えると、それまで無料だったのに一気に146ドル増えてしまう。

※個人的な解釈だけど、GKEのノード ≒ GCEのVMインスタンスと考えて今のところ問題ない。


n1-standard-1を東京で1か月借りると約31ドルとなるので、4台以上追加できることとなる。

5ノード以下に抑えれば、n1-standard-1×4相当のリソースを増やせると考えると、ここは可能な限りリソースに振りたいところ。

これはCloud Console上でも下記のようにメッセージで注意してくれる

4ノードの時
f:id:uyamazak:20171108165720p:plain

6ノードにしたとき
f:id:uyamazak:20171108170147p:plain

1インスタンスのリソースを増せば、ノード数は減らせる

ここで注目したいのは、ノード数(≒VMインスタンス数)であって、vCPUやメモリではないところ。

つまりvCPUが8個必要な場合、vCPUが1つ付いているn1-standard-1(もしくは同等のカスタムインスタンス)を8つとしてしまうと、3個オーバーしてしまうが、vCPUが4付いたn1-standard-1を2つにすればvCPUは8あるけど、ノード数は2になるので、無料枠で収まる計算。

もちろんn1-standard-8を一つでもいい。

VMインスタンスを分ける必要がなく、単純にCPU数が必要であれば、ノード数を増やすスケールアウトではなく、スケールアップを行いなるべくノード数を抑える方がいいと今のところ考えている。

見積もり

下記ツールで上記の条件で計算してみた

cloud.google.com

f:id:uyamazak:20171108171816p:plain

上記の通り、同じCPU数、同じメモリでありインスタンス料金は同じ$249.37/月だけど、n1-standard-1を8つの場合 Container Engine Cost: $146.00が余計にかかってしまう。

オートスケールをうまく使う

まだノードの自動スケールはベータ版だけど、これがちゃんと使えるのであれば、普段はノード数を5以下に収まるようになるべく性能のいいインスタンスを使い、急激な負荷のときだけノード数が増えるようにしておけば、GKEは1時間$0.20なので、数時間であれば大した負担にはならないのでいいと思う。

最後に

1年以上GKEで本番運用をし、新規サービスもすべてGKEで出しているものの、まだ大きな負荷で困ったことがないので、もっとユーザーがたくさん来てくれるサービスを作りたいと思った(小並感)

公開するサービス名とプロジェクト名を分けててよかった話

現在、ビズオーシャンでは、新しいサービスの開発と公開に向けて動いています。

サービスの公開が近づき、サービス名も部内の投票を参考に責任者によって決められ、ロゴも作って、サブドメインも登録して、SSL証明書(Let's Encrypt)も作って、Djangoの設定も変えて、
いよいよ!な時、念のためサービス名を商標登録しよう、ということになり、担当の人を通して弁理士?さんに確認したところNGが来てしまいました。

そしてもう一回、ちょっと英語の綴りを変えたりして、これなら大丈夫だろうと思って同様な作業を繰り返していたところ、読みが既存のものと一緒だと厳しいと、またNG。

商標取得の難しさを痛感。


でも、よかったこととして、内部的なプロジェクト名は全然別のものにしていたので、GCPのプロジェクト名とかソースコードのフォルダ名とか、Gitのレポジトリ名とかそういう開発に関わるところにはサービス名を使っていなかったので、それは非常に良かったと思います。


特にプロジェクト名は、公開しない前提で、世界の神話から神の名前を付けたり中二病を意識して付けていたので、恥ずかしくてサービス名には使えず、そもそもサービス名の候補にすらならないため、余計な手間も減りました。

今後も新規開発時は、中二病な名前で攻めようと思いました。

GKEでKubernetes 1.8が使えるようになったのでCron Jobsを使う

cron的なバッチ処理をKubernetesで行えるCron Jobsが1.8から使えるようになって、ちょうど他のメンバーの新規プロジェクトで必要になったので使ってもらった。


kubernetes.io

Prerequisites
You need a working Kubernetes cluster at version >= 1.8 (for CronJob). 


gcloudのkubectlも11/1時点ではまだ1.7だったけど、11/2でupdateをかけたら1.8になった。

ヘッドレスChrome + Node.js + express + DockerでPDF生成サーバーを作る

URLを渡すとChromeがPDFを作って返してくれるサーバーを作るお話。


業務用ソフトではよくPDFの帳票が必要になる。今作っているサービスでも必要になった。

WEB系だとHTMLは生成しやすいので、それをPDFに変換するwkhtmltopdfなどのコマンドラインツールや、Ruby製だとThinreportsなどのソフトウェアを使う必要があった。

あと選択肢としては、Google SpreadSheetやDriveのAPIを組み合わせてもできなくはないけど、やってみるとAPIが遅いのでかなり待たされるし、細かい調整は望めない。

wkhtmltopdf

オープンソース PDF 帳票ツール for Ruby, Rails | Thinreports

でも、今年4月に公開されたChrome59以降は、コマンドラインChromeを起動し、PDFを生成することもできるHeadless Chromeという機能が追加された。

ヘッドレス Chrome ことはじめ  |  Web  |  Google Developers


これの何がうれしいかというと、普段使っているChromeの印刷→PDFと同じ出力が得られるし、js、CSSの挙動も普段のChromeと同じになるはずので、それぞれのライブラリの癖を覚える必要もなければ、細かい差異にいら立つこともない。

Chromeなので改善、メンテナンスも継続的に行われるし、Linuxでも安定するだろうと予想できる。

もし、何か表示とかに変なところがあっても、普段お世話になっているChromeがこうなら仕方ないよねと諦められる。

他のHTML→PDF変換ソリューションだとこんな条件はそろわない。



今回はやらないけど、スクリーンショットも取れるので、昔だったら大変だったWEBサイトのサムネイル生成サーバーもすぐ作れそう。


ということで、Dockerに入れてchromeコマンドを使ってみると、問題が多発。

まずDocker内での起動には--no-sandboxが必要で、下記を参考にした。

bufferings.hatenablog.com

次に日本語が表示できないので、Dockerに日本語フォントをインストールした。フォントはGoogleフォントからかき集めた。WEBフォントのままでも使えないことはないが、毎回ダウンロードしてしまうので、サーバーにダウンロードしてしまった方が表示は数百ミリ秒早い。

Chrome自体は、GoogleからDebian用のdebファイルをダウンロードしてきて依存を解決するためにgdebiを使ってインストールした。最初gdebiを使わずに深い沼にはまりかけた。デスクトップ版Ubuntuなどでは特に問題にならないが、DockerだとGUI系のいろんなライブラリが入ってないのでハマる。gdebiすごい。

www.google.co.jp


Dockefileから抜粋すると下記。後でexpressのサーバーを使うのでnodeをベースにしている。抜粋なのでこれだけだと何にもおきないはず。

FROM node:8.6

# Install Chrome
RUN apt-get update && \
    apt-get install -y gdebi

COPY google-chrome-stable_current_amd64.deb ./
RUN gdebi --non-interactive google-chrome-stable_current_amd64.deb

# Install fonts
COPY fonts /usr/share/fonts

やっと動いたと思ったら、今度は上下に日付、タイトルなどの余計なヘッダー、フッターが入ってしまう。

chromeコマンドラインフラグを見ると、そこは現時点では変えられないらしい。

使えるコマンドラインフラグは下記ソースを読めとのこと。

https://cs.chromium.org/chromium/src/headless/app/headless_shell_switches.cc

どうにかならんのかと思ったら、検索していると同じ悩みの人がすでに作ってくれていた。

Nodeを使ってプログラム側から操作すれば、そのオプションも使えるらしい。

www.npmjs.com

Motivation

google-chrome currently have option to render pdf files when used with headless option. But this option contains hardcoded adding header and footer to page rendering it unusable for pdf generation. This module allows to generate it without those elements.

まさに同じ。


これを入れたところ、Dockerには対応していないので一部書き換える。


node_modules/chrome-headless-render-pdf/index.js

async spawnChrome() {
        const chromeExec = this.options.chromeBinary || await this.detectChrome();
        this.log('Using', chromeExec);
        const commandLineOptions = [
             '--headless',
             `--remote-debugging-port=${this.port}`,
             '--disable-gpu',
             '--no-sandbox'
            ];

上記した--no-sandboxを追加

node_modules/chrome-headless-render-pdf/index.js

    generatePdfOptions() {
        const options = {};
        if (this.options.landscape !== undefined) {
            options.landscape = !!this.options.landscape;
        }

        if (this.options.noMargins) {
            options.marginTop = 0;
            options.marginBottom = 0;
            options.marginLeft = 0;
            options.marginRight = 0;
        }

        if (this.options.includeBackground !== undefined) {
            options.printBackground = !!this.options.includeBackground;
        }
        options.paperHeight = 11.70;
        options.paperWidth = 8.26772;

        return options;
    }

デフォルトの出力サイズがアメリカンなレターサイズなのでoptions.paperHeight、options.paperWidthを単位インチのA4縦サイズを指定。

他のオプションは下記ページで確認できる。

Chrome DevTools Protocol Viewer - Page

とりあえず動いたから書き換えて使ってるけど、もうちょっといい感じにしてchrome-headless-render-pdfにプルリクエストを送ってみたいもの。

あとはこいつをexpressから叩いて、一時ファイルに書き出して、それを返しているけど、chromeの初回起動が重いからどうするかとか、たまに止まるとか、まだ非同期の処理の塊であるNodeの開発にも慣れていないし、ぐちゃぐちゃなので落ち着いたらまとめるつもり。


Node.js超入門

Node.js超入門

node.jsで作るWebサーバー

node.jsで作るWebサーバー