kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Material-UI の List コンポーネントを実装する

先週「React + TypeScript + Material-UI」を使ってプロトタイプ開発をする機会があった.今まで Material-UI は使ったことがなかったけど,ドキュメントを読むとコンポーネントも多く用意されていて,非常によくできていた.React で Material Design を簡単に実装できる.

material-ui.com

今回は Material-UI の List コンポーネントをサンプルコードを参考に実装しながら理解を深めていく.

material-ui.com

なお,実装したサンプルコードは GitHub に公開してある.TypeScript で create-react-app を実行してから実装を進めた.記事に載せるコードはポイントを限定し抜粋するため,実際にコード全体を見る場合は GitHub を参照して頂ければと!

$ create-react-app sandbox-material-ui-list --typescript

github.com

List / ListItem / ListItemText コンポーネント

まず,リストを実装する場合は List コンポーネントを使う.List コンポーネントに component プロパティを指定すると,例えば navdiv など,リストを表現する要素を決められる.

List コンポーネントに含める項目1個1個は ListItem コンポーネントを使う.代表的なプロパティとして以下を指定できる.

  • button : ボタンとして押せるようになる / デフォルト false
  • disabled : 非活性にして選択できないようにする
  • selected : 最初から選択状態にする
  • divider : リストに下線を引く
  • component : 要素を決める

詳細は以下に載っている.

リストに文字列を表示する場合は ListItemText コンポーネントを使う.primary プロパティに文章を指定する.

List / ListItem / ListItemText コンポーネントを使ってリストを実装した.紹介したプロパティも一部使っている.キャプチャに載せた「List 6」ListItem コンポーネントに a 要素を指定した関数を実装し ListItemLink として使えるようにしている(GitHub 参照).クリックすると kakakakakku blog に遷移する.

return (
  <div className={classes.root}>
    <List component="nav">
      <ListItem>
        <ListItemText primary="List 1" />
      </ListItem>
      <ListItem button>
        <ListItemText primary="List 2 (button)" />
      </ListItem>
      <ListItem disabled>
        <ListItemText primary="List 3 (disabled)" />
      </ListItem>
      <ListItem selected>
        <ListItemText primary="List 4 (selected)" />
      </ListItem>
      <ListItem divider>
        <ListItemText primary="List 5 (divider)" />
      </ListItem>
      <ListItemLink href="https://kakakakakku.hatenablog.com/">
        <ListItemText primary="List 6 (link)" />
      </ListItemLink>
    </List>
  </div>
);

f:id:kakku22:20190915143303p:plain

ListItemIcon コンポーネント

ListItemIcon コンポーネントを使うとリストにアイコンを追加できる.

Material-UI は「Material Icons」としてアイコン自体も配布しているため,コンポーネントとして使える.よって,以下のようにアイコンを import し,ListItemIcon コンポーネントと組み合わせることができる.

import CloudUploadIcon from '@material-ui/icons/CloudUpload';
import CloudDownloadIcon from '@material-ui/icons/CloudDownload';

material-ui.com

ListItemIcon コンポーネントを使ってアイコン付きリストを実装した.「List 7」「List 8」はより直感的になった.

return (
  <div className={classes.root}>
    (中略)
    <List component="nav">
      <ListItem button>
        <ListItemIcon>
          <CloudUploadIcon />
        </ListItemIcon>
        <ListItemText primary="List 7 - Upload" />
      </ListItem>
      <ListItem button>
        <ListItemIcon>
          <CloudDownloadIcon />
        </ListItemIcon>
        <ListItemText primary="List 8 - Download" />
      </ListItem>
    </List>
  </div>
);

f:id:kakku22:20190915143320p:plain

ListItemAvatar / Avatar コンポーネント

ListItemAvatar コンポーネントと Avatar コンポーネントを使うと写真をプロフィール画像のように表現できるようになる.Avatar コンポーネントには alt / src / className プロパティなどを指定できる.さらに ListItemText コンポーネントに secondary プロパティを指定するとリスト2行目に文章を追加できる.

ListItemAvatar / Avatar コンポーネントを使ってプロフィール画像とイニシャル画像(個人的に Gmail Inbox でよく見た)を実装した.イニシャル画像の場合は backgroundColor を指定した makeStyles() を定義している(GitHub 参照).

return (
  <div className={classes.root}>
    (中略)
    <List component="nav">
      <ListItem button>
        <ListItemAvatar>
          <Avatar alt="kakakakakku" src="https://pbs.twimg.com/profile_images/604918632460656640/FdOmiWZW_200x200.png" />
        </ListItemAvatar>
        <ListItemText primary="List 9 - @kakakakakku" secondary="https://kakakakakku.hatenablog.com/" />
      </ListItem>
      <ListItem button>
        <ListItemAvatar>
          <Avatar alt="kakakakakku" className={classes.orangeAvatar}>K</Avatar>
        </ListItemAvatar>
        <ListItemText primary="List 10 - @kakakakakku" secondary="https://github.com/kakakakakku" />
      </ListItem>
    </List
  </div>
);

f:id:kakku22:20190915143334p:plain

ListItemSecondaryAction コンポーネント

ListItemSecondaryAction コンポーネントを使うとリストをカスタマイズできる.

今回は ListItemSecondaryAction コンポーネントと Checkbox コンポーネントを組み合わせて,選択可能なリストを実装した.実際にアクションを含める場合,ドキュメントに載っているように handleToggle() などを実装し onClick() から呼び出すようにする.

return (
  <div className={classes.root}>
    (中略)
    <List component="nav">
      {[1, 2, 3].map(value => {
        return (
          <ListItem button>
            <ListItemAvatar>
              <Avatar className={classes.purpleAvatar}>{value}</Avatar>
            </ListItemAvatar>
            <ListItemText primary={`List ${value + 10} - User ${value}`} />
            <ListItemSecondaryAction>
              <Checkbox />
            </ListItemSecondaryAction>
          </ListItem>
        );
      })}
    </List>
  </div>
);

f:id:kakku22:20190915143347p:plain

まとめ

  • Material-UI を使うと React で Material Design を簡単に実装できる
  • 今回はドキュメントを読みながら List コンポーネントを実装しながら理解を深めた
  • 他にも多くのコンポーネントがあり,プロトタイプ開発のためにも調べたいと思う

github.com

アプリケーションとインフラのパフォーマンスをモニタリングしよう /「入門 Prometheus」を読んだ

今年5月に出版された「入門 Prometheus」を読んだ.本書は Prometheus の仕組みから,実際に本番環境で運用するときに必要になるデプロイの観点まで解説されているため「Prometheus に興味のある幅広い読者層」にオススメできる1冊だった.僕自身も Prometheus を本番環境で運用した経験はなく,Kubernetes や Microservices のハンズオンを試しながら,合わせて Prometheus と Grafana を使う場面が多く,本書の読者層に合っていた.

本書は O'Reilly Japan 様より献本を頂き,ありがとうございます!そして,本書の監訳者である @superbrothers さん,出版おめでとうございます!本書を読んでいたら,多くのページに「監訳注」が入っていて本当に素晴らしかった!原著出版後に追加された Prometheus API も紹介されていて,とても読みやすくなっていた.予想以上に本書のボリュームが大きく,さらに実際に Prometheus 環境を構築し,試しながら読み進めたり,個人的に忙殺されていた時期もあり,書評記事の公開が予定よりも遅れてしまったという点は反省点と言える.

目次

  • 第Ⅰ部 : イントロダクション
    • 1章 : Prometheusとは何か
    • 2章 : 初めてのPrometheus
  • 第Ⅱ部 : アプリケーションのモニタリング
    • 3章 : インストルメンテーション
    • 4章 : 開示
    • 5章 : ラベル
    • 6章 : Grafanaによるダッシュボードの作成
  • 第Ⅲ部 : インフラストラクチャのモニタリング
    • 7章 : Node exporter
    • 8章 : サービスディスカバリ
    • 9章 : コンテナとKubernetes
    • 10章 : よく使われるexporter
    • 11章 : ほかのモニタリングシステムとの連携
    • 12章 : exporterの書き方
  • 第Ⅳ部 : PromQL
    • 13章 : PromQL入門
    • 14章 : 集計演算子
    • 15章 : 二項演算子
    • 16章 : 関数
    • 17章 : レコーディングルール
  • 第V部 : アラート
    • 18章 : アラート
    • 19章 : Alertmanager
  • 第Ⅵ部 : デプロイ
    • 20章 : 本番システムへのデプロイ

Prometheus とは?

まず「1章 : Prometheusとは何か」を読むと Prometheus の基本を学べる.SoundCloud 社によって開発された話 / CNCF (Cloud Native Computing Foundation) / モニタリングの必要性もまとまっている.そして Prometheus のアーキテクチャを整理し,本書に出てくる様々な Prometheus 用語の概要も学べる.具体的には以下など.

  • exporter
  • サービスディスカバリ
  • スクレイプ(プル型)
  • インストルメンテーション
  • Alertmanager
  • PromQL
  • Grafana

そして「2章 : 初めてのPrometheus」では,さっそく Prometheus を実行し,Prometheus 自体のメトリクスを取得する.次に Prometheus コンソール http://xx.xx.xx.xx:9090 にアクセスし,PromQL を使って upprocess_resident_memory_bytesrate(prometheus_tsdb_head_samples_appended_total[1m]) などのクエリを体験する.さらに Node exporter を実行し,Linux メトリクスを取得する.もし Prometheus を試したことがなければ,最初に検証用の Prometheus 環境を構築し,雰囲気を掴んでから本書を読み進めると効率的に学べると思う.

f:id:kakku22:20190911104232p:plain

なお,今回は「Prometheus 2.12.0」を検証環境にした.

prometheus.io

Prometheus クライアントライブラリ

「3章 : インストルメンテーション」では,アプリケーションのメトリクスを Prometheus でスクレイプする.本書では Python のクライアントライブラリである prometheus_client を使って,アプリケーションを実装する.以下のサンプルコードはスクレイプする HTTP Server を実行している.なお,本書に載っているサンプルコードは GitHub に公開されているため,写経しても良いし,GitHub からコピーもできる.

github.com

import http.server
from prometheus_client import start_http_server

class MyHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"Hello World")

if __name__ == "__main__":
    start_http_server(8000)
    server = http.server.HTTPServer(('localhost', 8001), MyHandler)
    server.serve_forever()

Python コードを実行したら,prometheus.yml に以下の定義をする.Prometheus を再起動をしておく.

scrape_configs:
  - job_name: example
    static_configs:
      - targets:
        - localhost:8000

すると,HTTP Server をスクレイプし,PromQL を使って python_info のメトリクスを取得できるようになる.実際にアプリケーションに組み込む場合は「カウンタ」「ゲージ」「サマリ」「ヒストグラム」などを実装し,より価値のあるメトリクスを取得する.さらに「メトリクス名のサフィックス」に慣習があるという話もコラムにあって参考になった.

python_info{implementation="CPython",instance="localhost:8000",job="example",major="3",minor="7",patchlevel="4",version="3.7.4"}

f:id:kakku22:20190911104401p:plain

Pushgateway とは?

バッジジョブなど,スクレイプできないターゲットからメトリクスを取得する場合に「Pushgateway」を使う.注意点として Pushgateway は Prometheus をプル型からプッシュ型に変えるものではなく,Prometheus は Pushgateway をスクレイプする.よって,Pushgateway も exporter と言える.今まで Pushgateway を使ったことがなく,本書を参考に試した.

まず,Pushgateway を実行する.

$ wget https://github.com/prometheus/pushgateway/releases/download/v0.9.1/pushgateway-0.9.1.linux-amd64.tar.gz
$ tar -xzf pushgateway-0.9.1.linux-amd64.tar.gz
$ cd pushgateway-0.9.1.linux-amd64
$ ./pushgateway

次に prometheus.yml に以下の定義をする.Prometheus を再起動をしておく.

scrape_configs:
  - job_name: pushgateway
    honor_labels: true
    static_configs:
      - targets:
        - localhost:9091

今度は prometheus_clientpush_to_gateway() を実装し,Python コードを実行すると,メトリクスを Pushgateway に送信できる.

from prometheus_client import CollectorRegistry, Gauge, push_to_gateway

registry = CollectorRegistry()
g = Gauge('job_last_success_unixtime', 'Last time a batch job successfully finished', registry=registry)
g.set_to_current_time()
push_to_gateway('localhost:9091', job='batchA', registry=registry)

最後は Pushgateway コンソール http://xx.xx.xx.xx:9091 にアクセスすると,スクレイプされたメトリクスを確認できるようになる.

f:id:kakku22:20190911113232p:plain

サービスディスカバリ

Auto Scaling を前提にすると,毎回 prometheus.ymlstatic_configs を定義するのは運用的に難しく「サービスディスカバリ」を検討する必要がある.「8章 : サービスディスカバリ」では,Prometheus における「サービスディスカバリ」の仕組みを学べる.サービスディスカバリも本書を参考に試した.

まず,最初に「ファイルサービスディスカバリ」を試す.下のように prometheus.ymlfile_sd_configs を定義し,JSON もしくは YAML を指定すると,Prometheus の再起動をせず,自動的に読み込まれるようになる.Dynamic な環境だと運用的に難しいけど,prometheus.yml との依存度を減らせるメリットがある.

scrape_configs:
  - job_name: file
    file_sd_configs:
      - files:
        - '*.yml'

そして,任意の YAML ファイルを作成し,targets の中にターゲットを指定すると,Prometheus 側で確認できるようになる.今回は検証のために 10.10.10.10 は意図的に存在しないターゲットにしている.

- targets:
    - 10.0.1.84:9100
    - 10.0.2.104:9100
    - 10.10.10.10:9100
  labels:
    service: web

f:id:kakku22:20190911104444p:plain

より Dynamic に Amazon EC2 を対象にする場合,prometheus.ymlec2_sd_configs を定義する.さらに relabel_configsTag を定義し,タグによる絞り込みを定義できる.Prometheus で非常に重要な機能である「ラベル」に関しても,本書に多く解説があり,参考になった.実際に Dynamic な環境で Prometheus を運用するときに,もう1度読み直そうと思う.

scrape_configs:
  - job_name: ec2
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: xxx
        secret_key: xxx
        port: 9100
    relabel_configs:
      - source_labels: [__meta_ec2_tag_environment]
        regex: prd
        action: keep
      - source_labels: [__meta_ec2_tag_role]
        regex: web
        action: keep

デプロイ

最後の章「20章 : 本番システムへのデプロイ」では,Prometheus を本番環境で運用することを前提に,理解しておくべき知識を整理できる.特に Prometheus を階層化する「フェデレーション」や,時系列データを長期保存するために Prometheus API から「スナップショット」を取得する機能は今まで使ったことがなく,参考になった.個人的に「ダウンサンプリング」など,Prometheus のメトリクスを意図的に中長期に残していく話にも興味があったけど,本書にはメトリクス数を減らす設定と scrape_intervalevaluation_interval を変更する設定などが載っていた.Thanos の事例など,別途調べてみようと思う.

prometheus.io

誤植

O'Reilly のサイトにまだ「正誤表」がなく,気付いた誤植を載せておこうと思う.今回読んだのは「初版第1刷」となる.

  • P.201「この章では、の書き方を学ぶ。」
    • exporter の記述が抜けている

www.oreilly.co.jp

まとめ

  • 「入門 Prometheus」を読んだ(献本ありがとうございます!)
  • 「Prometheus に興味のある幅広い読者層」にオススメできる1冊だった
  • 実際に Prometheus 環境を構築し,試しながら読み進めると効率的に学べると思う

Python + unittest を使ってテストパターンをパラメータ化するときに subTest() メソッドを使う

Python + unittest を使って TDD (Test Driven Development) の練習(ペアプログラミング)をしていたときに,リファクタリングのサイクルでテストパターンをパラメータ化することになった.Parameterized Test にリファクタリングをするために,期待値をパラメータ化し,for の中に assert を実装した.以下はサンプルコードとなり,インプット文字列を大文字に変換する挙動を確認している.さらに「意図的に」ce を誤った期待値に設定し,失敗するようにした.なお,今回は Python 3.7 を検証環境にした.

import unittest


class MyTestCase(unittest.TestCase):
    def test_upper(self):
        patterns = [
            ('a', 'A'),
            ('b', 'B'),
            ('c', 'c'),
            ('d', 'D'),
            ('e', 'e'),
        ]

        for lower, upper in patterns:
            self.assertEqual(lower.upper(), upper)

テストコードを実行すると,以下の結果となる.期待した通りにテストは失敗しているけど,c の失敗で止まってしまう.

AssertionError: 'C' != 'c'
- C
+ c

FAILED (failures=1)

subTest() メソッドを使う

Python 3.4 で追加された subTest() メソッドを使うと,テストパターンをサブテスト化して区別できるようになる.unittest のドキュメントにも「サブテストを利用して繰り返しテストの区別を付ける」として,サンプルコードが載っている.

docs.python.org

実際に subTest() メソッドを使ってリファクタリングをすると,以下のような実装になる.subTest() メソッドの定義は subTest(msg=None, **params) となり,msgparams にサブテスト失敗時のメタデータを設定できる.失敗時の判断を正確にするために重要なオプションと言える.今回はサンプルとして仕様を書いた.

import unittest


class MyTestCase(unittest.TestCase):
    def test_upper(self):
        patterns = [
            ('a', 'A'),
            ('b', 'B'),
            ('c', 'c'),
            ('d', 'D'),
            ('e', 'e'),
        ]

        for lower, upper in patterns:
            with self.subTest('%s is upper case of %s' % (upper, lower)):
                self.assertEqual(lower.upper(), upper)

テストコードを実行すると,以下の結果となる.

  • サブテストを全て実行してから結果を表示できた(e の失敗まで実行できた)
  • [e is upper case of e] のように失敗時のメッセージを表示できた
  • failures にサブテスト数を表示できた
AssertionError: 'C' != 'c'
- C
+ c

AssertionError: 'E' != 'e'
- E
+ e

One or more subtests failed
Failed subtests list: [c is upper case of c], [e is upper case of e]

FAILED (failures=2)

まとめ

  • Python + unittest を使って Parameterized Test を実装するときは subTest() メソッドを使う
  • ドキュメントを読むと「サブテストをネストできる」と書いてあり,試してみようと思う
  • Python 用に Parameterized というライブラリもあり,1度試してみようと思う

github.com

Mac で「なめらかに」デモを見せるために必須なアプリと機能

技術講師としてプレゼンテーションをしながらデモをする場面が多く,無駄な操作をせず,Mac を「なめらかに」操作することを意識している.「なめらかな」操作をするために個人的に必須なアプリと機能があり,最近紹介する機会も増えているため,記事にまとめることにした.

ウィンドウ操作「Magnet」

Mac でウィンドウ操作を「なめらかに」するために「Magnet」を購入している.現時点だと250円で購入できる.ウィンドウの「移動」「リサイズ」なら関連するアプリとして有名な「Spectacle」も便利だけど,Magnet なら Windows でよく使う「スクリーンの端にウィンドウ当ててリサイズするジェスチャー」にも対応していて,適材適所に使える.正直 Magnet に慣れすぎてしまって離れられなくなっている.僕は以下のショートカットをよく使う.

  • 最大 : ^ ⌘ ↑
  • 左 : ^ ⌘ ←
  • ^ ⌘ →

Magnet

Magnet

  • CrowdCafé
  • Productivity
  • $1.99
apps.apple.com

magnet.crowdcafe.com

ズーム「2本指でピンチ」

Chrome を使ってウェブサイトを「なめらかに」ズームする場合,Mac のトラックパッドで「2本指でピンチ」を使う.ブラウザ側の機能(⌘ +)を使うと,ウェブサイトのスタイルが崩れることが多く,基本的に使わなくて良いと思う.

設定は「システム環境設定 → トラックパッド → スクロールとズーム」を開き「拡大/縮小」を有効化する.1日使っていると動かなくなることもあり,その場合は「設定 OFF → 数秒待機 → 設定 ON」で直る.

f:id:kakku22:20190828221052p:plain

ズーム「スクロールジェスチャ」

基本的に「2本指でピンチ」を使うけど,例えば Chrome の DevTools など「2本指でピンチ」に未対応の場所もあり,その場合は Mac の「スクロールジェスチャ」を使う.

設定は「システム環境設定 → アクセシビリティ → ズーム機能」を開き「スクロールジェスチャと修飾キーを使ってズーム」を有効化する.ショートカットはデフォルトのまま ^ Control にしているため ^ を押しながらトラックパッドを上下にスクロールするとズームできるようになる.紹介した2種類のズームを覚えておくと良いと思う.

f:id:kakku22:20190828221108p:plain

ドラッグ「3本指のドラッグ」

「3本指のドラッグ」は去年に記事を書いているけど,まだ未設定の人が多いように思う.記事にも書いているけど,例えば「文字をドラッグしてコピーするとき」「ウィンドウをスクリーン側に移動するとき」など,「3本指のドラッグ」を使うとスムーズに操作できるようになる.PowerPoint や Keynote でスライドを作るときにも使えて,正直「3本指のドラッグ」を使わないと作業の効率さが大きく変わってしまう.

設定は「システム環境設定 → アクセシビリティ → マウスとトラックパッド」を開き「トラックパッドオプション → ドラッグを有効にする → 3本指のドラッグ」を有効化する.本当にオススメ!

f:id:kakku22:20190828223446p:plain

kakakakakku.hatenablog.com

ランチャー「Alfred」

Mac でアプリを「なめらかに」開くために,Dock から選ぶのではなくランチャーから起動する.Spotlight もあるけど,個人的には「Alfred」に慣れてしまっていて,ずっと使っている.起動するときのショートカットは ⌥ Space にしている.

「Alfred」から iTerm2 や Visual Studio Code などのアプリを開くこともできるし,接頭辞に ' を付けるとファイル検索ができるし,Google で検索をするときにも Alfred を使う.ランチャーをうまく使いこなせるとデモに限らず日常的な操作が効率的になる.

www.alfredapp.com

まとめ

Mac を「なめらかに」操作しよう!練習あるのみ!

nginx でリクエストを制限できるモジュール「ngx_http_limit_req_module」

nginx でリクエストを制限できるモジュール「ngx_http_limit_req_module」を使うと,Throttling や DoS 対策など,リクエストの過剰な増加に nginx で対応できるようになる.挙動を確認するため,Docker Compose を使って検証環境を構築した.

nginx.org

検証環境

今回 Docker Compose を使って,nginx と Sinatra を起動する検証環境を構築した.コンテナは計4種類で,以下の構成図にまとめた.今回は default.conf の異なる3種類の nginx (Frontend1-3) の挙動を確認する.

  • Frontend1 (nginx) : limit_req
  • Frontend2 (nginx) : limit_req + burst
  • Frontend3 (nginx) : limit_req + burst + limit_req_status
  • Backend (Sinatra)

f:id:kakku22:20190825131113p:plain

リクエストを投げるために今回は Vegeta を使う.

github.com

特に設定はなく,すぐに docker-compose up で起動できるようにした.なお,Docker Compose の設定などは GitHub に公開した.

$ docker-compose up

$ curl http://localhost:8080
Hello, backend!

$ curl http://localhost:8081
Hello, backend!

$ curl http://localhost:8082
Hello, backend!

github.com

検証 1 : limit_req

まず,Frontend1 の設定(一部)を以下に載せる.ngx_http_limit_req_module の基本設定となる limit_req_zone$binary_remote_addr を指定し,IP アドレスごとに制限をしている.他にも $server_name を指定し,サーバごとに制限をすることもできる.そして今回は rate=1r/s を指定し,1秒間に1リクエストを許容する.

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    server {
        (中略)
        location / {
            limit_req zone=one;
            proxy_pass http://backend;
        }
    }
}

さっそく Vegeta でリクエストを投げてみる.今回は -rate=10-duration=5s を指定し「1秒間に10リクエストを5秒間」とする.結果レポートの中で注目するポイントは「200:5 503:45」で,1秒間に1リクエストを許容しているため,残った45リクエストは「503 Service Temporarily Unavailable」になっている.

$ echo 'GET http://localhost:8080' | vegeta attack -rate=10 -duration=5s > result.bin

$ vegeta report -type=text result.bin
Requests      [total, rate, throughput]  50, 10.21, 1.02
Duration      [total, attack, wait]      4.898020366s, 4.896749649s, 1.270717ms
Latencies     [mean, 50, 95, 99, max]    2.134954ms, 1.814726ms, 3.90771ms, 6.088868ms, 6.088868ms
Bytes In      [total, mean]              22305, 446.10
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    10.00%
Status Codes  [code:count]               200:5  503:45
Error Set:
503 Service Temporarily Unavailable

nginx のアクセスログは以下のように出力されていた.

frontend1_1  | 2019/08/25 00:00:00 [error] 6#6: *3 limiting requests, excess: 0.100 by zone "one", client: 172.21.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
frontend1_1  | 172.21.0.1 - - [25/Aug/2019:00:00:00 +0000] "GET / HTTP/1.1" 503 494 "-" "Go-http-client/1.1" "-"

検証 2 : limit_req + burst

次に,Frontend2 の設定(一部)を以下に載せる.Frontend1 との差は burst=10nodelay で,今回は burst を指定することにより,制限を超えたリクエストをキューに溜められるようになる.さらに nodelay を合わせて指定し,キューに溜まったリクエストを遅延処理しないようにできる.リクエストを制限しながら緩和できる設定となり,よく使うことになりそう.

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    server {
        (中略)
        location / {
            limit_req zone=one burst=10 nodelay;
            proxy_pass http://backend;
        }
    }
}

Frontend2 にも Vegeta でリクエストを投げてみる.結果レポートを確認すると「200:15 503:35」となり,burst に設定したリクエストも処理できた.残った35リクエストは「503 Service Temporarily Unavailable」になっている.

$ echo 'GET http://localhost:8081' | vegeta attack -rate=10 -duration=5s > result.bin

$ vegeta report -type=text result.bin
Requests      [total, rate, throughput]  50, 10.20, 3.06
Duration      [total, attack, wait]      4.901103127s, 4.899598364s, 1.504763ms
Latencies     [mean, 50, 95, 99, max]    3.344031ms, 1.78755ms, 13.669119ms, 17.032244ms, 17.032244ms
Bytes In      [total, mean]              17515, 350.30
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    30.00%
Status Codes  [code:count]               200:15  503:35
Error Set:
503 Service Temporarily Unavailable

検証 3 : limit_req + burst + limit_req_status

最後は,Frontend3 の設定(一部)を以下に載せる.Frontend2 との差は limit_req_status で,特に API リクエストを制限する場合など,レスポンスコードとして「503 Service Temporarily Unavailable」ではなく「429 Too Many Requests」を返すべき場面もあると思う.今回は limit_req_status を指定することにより,レスポンスコードを指定している.

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    server {
        (中略)
        location / {
            limit_req zone=one burst=10 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

Frontend3 にも Vegeta でリクエストを投げてみる.結果レポートを確認すると「200:15 429:35」となり,残った35リクエストは「429 Too Many Requests」になっている.

$ echo 'GET http://localhost:8082' | vegeta attack -rate=10 -duration=5s > result.bin

$ vegeta report -type=text result.bin
Requests      [total, rate, throughput]  50, 10.20, 3.06
Duration      [total, attack, wait]      4.905475679s, 4.903894373s, 1.581306ms
Latencies     [mean, 50, 95, 99, max]    3.614673ms, 2.121613ms, 8.324474ms, 34.016092ms, 34.016092ms
Bytes In      [total, mean]              6140, 122.80
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    30.00%
Status Codes  [code:count]               200:15  429:35
Error Set:
429 Too Many Requests

nginx のアクセスログは以下のように出力されていた.

frontend3_1  | 2019/08/25 00:00:00 [error] 6#6: *1 limiting requests, excess: 10.210 by zone "one", client: 172.21.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8082"
frontend3_1  | 172.21.0.1 - - [25/Aug/2019:00:00:00 +0000] "GET / HTTP/1.1" 429 169 "-" "Go-http-client/1.1" "-"

まとめ

nginx でリクエストを制限できるモジュール「ngx_http_limit_req_module」を試した.IP アドレスごとなど,設定した単位ごとにリクエストを制限でき,burst を設定すれば緩和もできる.モジュールに実装されたレベルの制限なら nginx で実現できそう.実際に ngx_http_limit_req_module.c など,実装を紐解くと,正確には r/s は1秒間に許容するリクエストではないらしく,本番環境などトラフィックのある環境に導入するときには様々なシナリオを考えて負荷テストをしておく必要がありそう.

関連記事

kakakakakku.hatenablog.com