Kubernetesに負荷をかけオートスケールを観察する

本番環境でKubernetesを使うためには、Kubernetesがどのように動作するか把握していないと安心して運用することができません。
まずはサービスを運用する上で重要になる、高負荷時の動きを確認してみたいと思います。

KubernetesにはHPAとCluster Autoscalerの二種類がありますが、両方の動きを見ていきます。

詳細な解説は他に譲りますが、
HPAは稼働中のPodを増加させることで高負荷に耐え、
Cluster AutoscalerはPodを増加させることができないときにKubernetesが扱えるリソースを追加するものです。

つまり、負荷が高まるとHPAがおこりPodが追加され、それでも負荷が高くHPAが続きKubernetesが管理しているリソースに空きがなくなったときにCluster AutoscalerによってVMが追加され、そこにPodが追加されます。

環境

  • GKE 1.9.7-gke.0

実行するアプリケーション

できるだけ手軽に実験したいので、goでリクエストを受け付けて、少し負荷がかかる処理をした後にレスポンスを返すアプリケーションをKubernetesで実行してみます。

Kubernetesの構成は
Ingress -> Service -> Deployment
となっています。

実行するgoのAPIのソースはこのようにしました。

package main

import (
    "fmt"
    "net/http"
    "strconv"
)

func handler(w http.ResponseWriter, r *http.Request) {
    n := 0
    for i := 0; i < 1*1000*1000*1000; i++ {
        n += i
    }
    fmt.Fprintf(w, strconv.Itoa(n)+"\n")
    fmt.Fprintf(w, "my-api")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":80", nil)
}

とても単純なものです。
これをDocker Imageにし、DockerのrunコマンドでAPIサーバーが起動するようにDockerfileを書きます。

FROM golang:1.9.2-alpine3.6

ADD ./server.go ./
RUN go build -o server
CMD ["/go/server"]

Imageをアップロードしますが、せっかくGKEを使用するので同じGCP上にあるGoogle Container Registryにアップロードします。

$ docker build -t asia.gcr.io/{your-project-id}/my-api:v1 .
$ gcloud docker -- push asia.gcr.io/{your-project-id}/my-api:v1

Kubernetesの設定ファイル

Kubernetesについては省略します。
今回は以下の設定ファイルを使用します。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "test-ip"
spec:
  backend:
    serviceName: service-my-api
    servicePort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: service-my-api
spec:
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: http
  selector:
    app: my-api
    version: v1
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-api
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: my-api
        version: v1
    spec:
      containers:
      - name: myapi
        image: gcr.io/{your-project-id}/myapi:v1
        resources:
          requests:
            cpu: "500m"
          limits:
            cpu: "1500m"
        ports:
        - containerPort: 80
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: my-api
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-api
  minReplicas: 2
  maxReplicas: 6
  targetCPUUtilizationPercentage: 70

オートスケールに関係のあるところだけ解説します。
今回負荷をかけるAPIがあるPodに対してはCPUを1.5個まで使用を許可しています。
このPodが初期値として2台稼働しており、閾値を70としています。

つまり、CPUの使用率が70%を超えたときにPodの追加が行われることになります。
また、公式ドキュメントのautoscaling-algorithmによると、10%の許容値があったり瞬間的に超えただけだとオートスケールしなかったりするようです。

負荷をかける

負荷をかけるツールはいろいろありますが、手軽に使えるheyを使用します。

github.com

このツールで負荷をかけてみた結果、このようになりました。

PodのCPU使用率 f:id:akaimo3:20180515231231p:plain

ノードのCPU使用率 f:id:akaimo3:20180515231244p:plain

ピーク時はPodは6個に、ノードは3台にまで増えていますね。
HPAは追加にかかる時間が数秒なので、EC2とかのオートスケールと比べるとだいぶ早いですね。
これは便利そうです。

Webpackでビルドするときにflowtypeで型をチェックする

flowを導入して型のチェックがされるようになっても、開発者がエディタで設定していなかったり、 エディタによっては開いていないソースはチェックの対象になっていなかったりします。

これではせっかく導入したflowが活かせません。

そこでWebpackでビルドと同時にflowの型チェックを走らせてみます。

プラグインの導入

flow-status-webpack-pluginを使用してビルド時にチェックします。

インストールします。

yarn add flow-status-webpack-plugin --dev

あとはwebpack.config.jsに設定を追加すれば完了です。
このファイルは環境によっては異なる場合があるので適宜読み替えてください。

var FlowStatusWebpackPlugin = require('flow-status-webpack-plugin');

module.exports = {
  ...,
  plugins: [
    ...,
    new FlowStatusWebpackPlugin({
      onSuccess: function (stdout) {
        console.log('\u001b[32m' + 'Succeeded in type check by flow' + '\u001b[0m');
      },
      onError: function (stdout) {
        console.log('\u001b[;41m' + ' Failed in type check by flow ' + '\u001b[0m');
        console.error(stdout);
      }
    })
  ]
}

補足

\u001b[32mなどはコンソールの出力に色をつけています。
成功したときは緑、失敗したときは赤になっています。

開発者に認識してもらうことが目的なので、このようなちょっとしたことも大切ですね。

QNAPにTwonky Serverをインストールする

QNAPが公式に提供しているDLNAサーバーはいろいろと貧弱なので、評判の高いTwonky Serverを使えるようにします。

環境

  • QNAP: TS-431P
  • OS: QTS 4.3.4

Twonky Serverのダウンロード

まずは以下のリンクからダウンロードページに移動します。

http://download.twonky.com/

次にインストールするバージョンを決めます。
今回は最新バージョンである8.5を選びました。

次に自分の環境にあったビルドを選びます。
ここで正しいものを選ばないと動かないので気をつけてください。
QNAP用には次のビルドが用意されています。

Qnap arm-x09 package
Qnap arm-x19 package
Qnap arm-x31 package
Qnap arm-x41 package
Qnap x86 package
Qnap x86-64 package

自分の環境を調べます。
別のソフトウェアですが、次のサイトが参考になります。

Kazoo Server ソフト QNAP NAS対応モデル | OLIOSPEC BLOG

私のQNAPはTS-431PですのでQnap arm-x41をダウンロードします。

Twonky Serverのインストール

QNAPにログインしてAppCenterを起動します。
AppCenterの右上にあるアイコンをクリックし、手動インストールの画面で先程ダウンロードしたqpkgを選択し、ダイアログに従っていけばインストールが完了します。

JUnit5をmainから実行する

こんにちは。akaimoです。
今回はJUnit5をmain関数内で使用する方法です。

環境

  • Java9
  • JUnit5.1.0-M1
  • Kotlin1.2

ソース

これでmainからJUnitを動かせます。詳しい解説は後ほど。

import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder
import org.junit.platform.launcher.core.LauncherFactory
import org.junit.platform.launcher.listeners.SummaryGeneratingListener
import org.junit.platform.engine.discovery.ClassNameFilter.*
import org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage
import java.io.PrintWriter


fun main(args: Array<String>) {
    val launcher = LauncherFactory.create()
    val summary = SummaryGeneratingListener()
    launcher.registerTestExecutionListeners(summary)
    val request = LauncherDiscoveryRequestBuilder
            .request()
            .selectors(selectPackage("jp.hoge.piyo.test"))
            .filters(includeClassNamePatterns(".*"))
            .build()
    launcher.execute(request)
    summary.summary.printFailuresTo(PrintWriter(System.out))
    summary.summary.printTo(PrintWriter(System.out))
}

これを実行すると次のようなログが出力されます。

Test run finished after 13255 ms
[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         4 tests found           ]
[         3 tests skipped         ]
[         1 tests started         ]
[         0 tests aborted         ]
[         1 tests successful      ]
[         0 tests failed          ]

もしテストが失敗していたら、このログの前に失敗したテストと失敗した内容が表示されます。

詳細

testディレクトリ配下に入れIDEなどでテストを実行する場合はテストコードだけを書けば良いですが、main内で実行させる場合は全て自分で実行させなければいけません。

Launcher

JUnit5には、テストを検出したりフィルタリング、実行するためにLauncherというものを使用します。
まずはそのLauncherを作成します。

val launcher = LauncherFactory.create()

Summary

次はSummaryです。
これは名前の通り、テストの内容であったり結果を保持しています。
Summaryを作成して、Launcherに登録します。

val summary = SummaryGeneratingListener()
launcher.registerTestExecutionListeners(summary)

Request

Requestでは実行するテストの選択やフィルタリングをします。
selectorefiltersを適宜変更して実行したいテストが実行できるようにします。

val request = LauncherDiscoveryRequestBuilder
            .request()
            .selectors(
                selectPackage("jp.hoge.piyo.test"),
                selectClass(MyTestClass::class.java)
            )
            .filters(includeClassNamePatterns(".*Tests"))
            .build()

作成したrequestをLauncherに渡せばテストを実行できます。

launcher.execute(request)

結果の表示

最後にテスト結果を表示して終了です。

summary.summary.printFailuresTo(PrintWriter(System.out))
summary.summary.printTo(PrintWriter(System.out))

なお、summary.summaryよりデフォルトで出力される項目に個別にアクセスできるので、任意のフォーマットで出力することもできます。

react-reduxにflowtypeを導入しPropsに型を付ける

flowtypeを導入したとき、reactとreduxをつなぐ部分の情報が少なかったのでまとめます。

flowtypeの導入前

比較として導入前のソースを載せておきます。

import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux'
import * as actions from '../actions

class SamplePage extends Component {
  render() {
    const {title, description, myAction} = this.props

    return (
      <div>
        <button onClick={myAction}>{title}</button>
        <p>{description}</p>
      </div>
    )
  }
}

SamplePage.propTypes = {
  title: PropTypes.string,
  description: PropTypes.string,
  myAction: PropTypes.func
}

const mapStateToProps = state => {
  return {
    title: state.title,
    description: state.description
  }
}

const mapDispatchToProps = dispatch => {
  return {
    myAction: () => dispatch(actions.myAction())
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(SamplePage)

導入後

これが導入後のソースです。propTypesが消えましたが、typeを記述しているので行数はあまり変わっていません。

import React, {Component} from 'react'
import {connect} from 'react-redux'
import * as actions from '../actions
import type {MapStateToProps, MapDispatchToProps} from 'react-redux'

type StateToProps = {
  title: string,
  description: string
}

type DispatchToProps = {
  myAction: () => void
}

type Props = StateToProps & DispatchToProps

class SamplePage extends Component<void, Props, void> {
  render() {
    const {title, description, myAction} = this.props

    return (
      <div>
        <button onClick={myAction}>{title}</button>
        <p>{description}</p>
      </div>
    )
  }
}

const mapStateToProps: MapStateToProps<*, *, StateToProps> = state => {
  return {
    title: state.title,
    description: state.description
  }
}

const mapDispatchToProps: MapDispatchToProps<*, *, DispatchToProps> = dispatch => {
  return {
    myAction: () => dispatch(actions.myAction())
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(SamplePage)

ポイント

propTypesが3つのtypeに置き換えられています。
その中で特に大事なところが以下で、

type Props = StateToProps & DispatchToProps

mapStateToPropsmapDispatchToPropsで生成された2つのオブジェクトのtypeが合成されたものが、

class SamplePage extends Component<void, Props, void> { ...

となりcomponentに渡されていることを明示的に示すことができます。

補足1

MapStateToPropsは以下のように定義されています。

declare type MapStateToProps<S, OP: Object, SP: Object> = (state: S, ownProps: OP) => SP;

ジェネリクスで、SとOPを受け取ってSPを返す関数という型になってますね。
導入後のソースではMapStateToProps<*, *, StateToProps>のように宣言しています。 Stateのtypeを宣言していれば、MapStateToProps<State, *, StateToProps>となり、MapStateToProps内のstateに対して型が適用されるのでSやOPに対しても型の宣言をすることをオススメします。

補足2

2018/02/01現在で最新のflowtypeではComponentに渡すPropsの型を以下のように記述します。

class SamplePage extends Component<Props> { ...

Travis CIでfastlaneを使用する

こんにちは。akaimoです。
Travis CIを利用しているiOSプロジェクトにfastlaneを導入して、xcodebuildを直接実行する環境から開放されたので、
備忘録としてやったことを残しておきます。

github.com

前提

  • CI上でfastlaneを使用
  • 複数人で開発

fastlaneのインストール

まずはfastlaneをインストールします。

fastlaneはgemで公開されているのでrubyの環境のバージョンを固定しておきましょう。
CIにインストールされているrubyのバージョンと同じにしておくことをオススメします。
違うとCI上でrubyのインストールが始まってしまうかもしれません。無駄な時間なので素直に同じバージョンを使いましょう。

$ rbenv local 2.4.2

こんどはfastlaneのバージョンを固定するためにbundlerをインストールします。

$ gem install bundler

Gemfileに以下を記述します。

source 'https://rubygems.org'

gem 'fastlane'

インストールします。

$ bundle install --path=vendor/bundle

ポイントとしては --path を指定することでローカルの環境を汚すことなくfastlaneをインストールできます。
ここでインストールされたディレクトリはgitで管理する必要はないので.gitignoreに入れておきましょう。

セットアップ

xcodeprojがあるディレクトリでinitコマンドを実行して雛形を作成します。

$ bundle exec fastlane init

実行するとlane(1つのコマンドで実行されるアクションをまとめたもの)を作成するか聞かれます。2か3を選ぶと質問に答えるだけでitunes connectの設定がされるのでオススメです。

完了するとfastlaneというディレクトリが作成されています。
ファイルの中は以下のようになっています。

  • Fastfile
    • レーンを記述します。initコマンドで選択したlaneが作成されています。
  • Appfile
    • fastlane全体で使用する情報を記載します。initで2か3を選んでいるとapple idやteam idなどが記載されています。

iTunes Connect(TestFlight)へのアップロード

TestFligntへアップロードします。
プロジェクトの設定によって変わりますが、基本は以下のような形です。

  lane :beta do
    build_app(
      workspace: "Hoge.xcworkspace",
      scheme: "Hoge",
      output_directory: "./tmp"
    )
    upload_to_testflight(ipa: "./tmp/Hoge.ipa")
  end

lanebetaを実行します。

$ bundle exec fastlane beta

initのときに2か3を選択した場合は、initで入力した情報がキーチェーンに保存されているのでローカル環境ではアップロードできますが、
CI上だとapple idのパスワードが存在せず失敗してしまいます。

このような時のためにfastlaneはFASTLANE_PASSWORDという環境変数からパスワードを取得してログインするようになっています。
.travis.yml環境変数にパスワードをセットしましょう。

この記事が参考になります。

qiita.com

これでCI上からアップロードできるようになります。

HockeyAppへのアップロード

次はHockeyAppへアップロードします。

  HOCKEY_API_TOKEN = ENV['HOCKEY_APP_TOKEN']
  PLIST_PATH = "path/to/Info.plist"
  build = get_info_plist_value(path: PLIST_PATH, key: "CFBundleVersion")
  version = get_info_plist_value(path: PLIST_PATH, key: "CFBundleShortVersionString")

  lane :alpha do
    build_app(
      workspace: "Hoge.xcworkspace",
      scheme: "Hoge",
      configuration: "Adhoc",
      export_method: "enterprise",
      output_directory: "./tmp"
    )
    hockey(
      api_token: HOCKEY_API_TOKEN,
      bundle_short_version: version,
      bundle_version: build,
      ipa: "./tmp/Hoge.ipa",
      dsym: "./tmp/Hoge.dSYM.zip",
    )
  end

先程と同じようにapiのtokenを環境変数にセットし、それを読み込みます。
TestFlightへのアップロードができていれば、同じようにできるはずです。

Unit Testの実行

せっかくCIの環境があるので、テストも実行しておきましょう。

  lane :test do
    run_tests(
      workspace: "Hoge.xcworkspace",
      scheme: "Hoge",
      configuration: "Debug",
      device: "iPhone 8"
    )
  end

Slackへの通知

fastlaneにはslackに通知を送る機能があり、自由にカスタマイズでき、好きなタイミングで送ることができます。
今回はアップロードが完了したら通知するようにします。

まずは通知用のlaneを作成します。

  lane :hockey_notification do
    link = lane_context[SharedValues::HOCKEY_DOWNLOAD_LINK].to_s
    name = sh("echo $USER")

    slack(
      slack_url: "https://hooks.slack.com/services/",
      channel: "#general",
      success: true,
      default_payloads: [],
      attachment_properties: {
        title: "Uploaded to HockeyApp",
        text: "Beta version is now available for <" + link +  "|download>.",
        fields: [
          {
            title: "Build by",
            value: name,
            short: true
          },
          {
            title: "Build Date",
            value: Time.new.to_s,
            short: true
          }
        ]
      }
    )
  end

これを実行すると、このような通知がきます。

f:id:akaimo3:20180120121916p:plain

slackのattachment apiをカバーしているので、公式のドキュメント通りにカスタマイズできます。

api.slack.com

これを先程のHockeyAppへアップロードするlaneに追加します。

  HOCKEY_API_TOKEN = ENV['HOCKEY_APP_TOKEN']
  PLIST_PATH = "path/to/Info.plist"
  build = get_info_plist_value(path: PLIST_PATH, key: "CFBundleVersion")
  version = get_info_plist_value(path: PLIST_PATH, key: "CFBundleShortVersionString")

  lane :alpha do
    build_app(
      workspace: "Hoge.xcworkspace",
      scheme: "Hoge",
      configuration: "Adhoc",
      export_method: "enterprise",
      output_directory: "./tmp"
    )
    hockey(
      api_token: HOCKEY_API_TOKEN,
      bundle_short_version: version,
      bundle_version: build,
      ipa: "./tmp/Hoge.ipa",
      dsym: "./tmp/Hoge.dSYM.zip",
    )
    hockey_notification
  end

alphalaneを実行するとアップロード完了後にslackへ通知がきます。

おわりに

fastlaneを使用してCI上の処理を簡略化しました。
xcodebuildを直接実行していたときと比較すると、

  • CIのコードが大幅に減った
  • 出力に色が付き整形されるので見やすい

ことがすぐに感じられたメリットです。

Xcodeのバージョンアップによるxcodebuildの変更への追随が、fastlaneの更新だけで済むようになればいいな
と期待しながらCIを拡充していきたいと思います。

ScrollViewとStackViewを組み合わせる

akaimoです。
そろそろiOS8のサポートが終わってきたと思うので、TableViewの変わりにStackViewを使う方法を書きます。

使いにくいScrollView

スクロールが必要な複雑な画面の場合、同じ要素が1つも存在しないのにTableViewを使うことがありました。
ScrollViewではなく、TableViewを使う理由はいくつかありますが、
一番大きな理由はScrollViewを使うためのハマりポイントがたくさんあったからだと思います。

そんなScrollViewですが、StackViewと組み合わせて使うことで大幅に使いやすくなります。

ScrollViewの設定

storyboardとxibを使用して実装します。

storyboard

シンプルにいくために、UIViewControllerを継承したcontrollerを用意します。
このcontrollerをstoryboardで設定していきます。

  • このcontrollerにScrollViewを設置し、alignを0で四カ所に制約を付けます。
  • ScrollViewの上にStackViewを設置し、こちらもalignを0で四カ所に制約を付けます。
  • ScrollViewとStackViewを選択し、Equal Widthの制約を付けます。

これで基本的な設定は完了です。

StackView

StackViewの設定をします。

  • AxisをVertical
  • AlignmentをFil
  • DistributionをEqual Centering
  • Spacingを0

view

複数の要素から構成されるので、カスタムviewを使うことをオススメします。

xibを使用してviewを構成します。
viewはいつも通りに作成します。注意するところとしては、このview全体のサイズが決まるように制約を付けることです。
これを守ればあとはStackViewがそのサイズを確保してくれるので、TableViewのように様々なviewを組み合わせてScrollViewを使用することができると思います。