woshidan's blog

あいとゆうきとITと、とっておきの話。

いまさらRSpecに少し慣れてテストをすっきり書く方法について少し覚えた話

いまさらRSpecをさわってきて慣れて覚えた話もメモしておきます。

TL;DR

  • letで宣言した変数の初期化用パラメータで置き換えたくなったパラメータもletで宣言しておくとよさそう
  • shared_context や shared_examplesでまとめるのがちょうどいいくらいの事例としてちょうどいいのは、外部公開するAPIの異常系のレスポンスのテストくらいでは

慣れてきて覚えた話

letで宣言した変数の初期化用パラメータで置き換えたくなったパラメータもletで宣言しておく

これは同僚の方から教えていただいていいな、と思ったんですが、

describe "post comment" do
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:params) { { user_name: "", comment: "...", "book_id": 3 } }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid comment" do
    let(:params) { { user_name: "HOGE", comment: "INVALID_FORMAT", "book_id": 3 } }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid book" do
    let(:params) { { user_name: "HOGE", comment: "...", "book_id": nil } }

    it "cannot be posted" do
      ...
    end
  end
end

のような場合、 params の値の一部がそれぞれの context で異なるので、それぞれの context ブロックでパラメータを宣言しなおしていますが、

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
    end
  end
end

のように、example内で触る変数はparamsだけかもしれないんですが、それぞれのcontextで注目しているパラメータだけあとで置き換えられるように、宣言の時に工夫しておくとすっきりするなと思いました。

shared_context や shared_examplesでまとめるのがちょうどいいくらいの事例としての外部公開するAPIの異常系のレスポンスのテスト

この話がしたくてこの記事を書きはじめたのに随分長くなってしまったのですが、RSpecでは、shared_contextを使ってspecの中で繰り返し登場するテストケースを

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request" # このケース自体がいいかは微妙ですが...
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end
end

のような場合、

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  shared_examples "invalid request" do
    it "returns 400 response" do 
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end
end

のように、同じテストコードを実行している部分を shared_examples に定義して it_behaves_like "SHARED_EXAMPLE_NAME" で呼び出すことができます。

こうするとテストケースはDRYになりますが、

  • 一方でテストが読みにくくなったり
  • バリデーションエラーなどは案外全く同じコードにならなかったり
    • たとえば、上記のサンプルコードは実際はパラメータごとに invalid user などのレスポンスを返したほうが親切でしょう

して、使いどころが難しいんですが、外部公開しているAPIの場合、

  • エラーの内容からAPI内部の処理を推測できないようにわざと詳細なレスポンスを返さないケースがある
  • 外部公開しているAPIが複数あるとしても、機能ごとにエラー時のレスポンスを変えない場合が多い
    • たとえば、一覧取得用API投稿用APIで認証エラー時のレスポンス内容は変えない方が親切でしょう

という感じで使ってみるとよいかもしれないな、と思ったのでメモです。

参考

https://qiita.com/jnchito/items/42193d066bd61c740612 https://qiita.com/etet-etet/items/7babe4856a1cd62b9ecb

ペアワイズ法について簡単にまとめて手を動かしてテストケースが削れるのを眺めてみた

この記事はソフトウェアテストの小ネタアドベントカレンダーの2日目の記事です。

この記事では、大量にあるテストケースをある程度不具合検出率を担保しつつ効率よく削るための手法であるペアワイズ法についてその概要をまとめ、組み合わせが小さい場合について手で実際に行ってみて、なぜテストケースが減らせるのか確認します。

TL;DR

  • テスト対象のパラメータの値を単純に組み合わせていくと、テストケースがあっという間に増えすぎてしまう
  • 実際に影響があるのは2つくらいのパラメータの値の組み合わせであることが多いので、パラメータでペアを作って、そのペアごとにすべての値をテストできるようにテストケースを作成するようにすると効率がいいのでは、というのが ペアワイズ法 の基本の考え方
  • ペアごとにすべての値をテストすればよい、としたことで1つのペアの条件をテストしながら他のペアの条件もテストしていくことでテストケースが削減される様子を眺めてみた

単純に条件の組み合わせを増やしていくとテストケースがあっという間に増えてしまう

テストを作成する際、テスト対象に入力されるパラメータの組み合わせからテストケースを作成することが多いと思います。

たとえば、以下のような入力欄があるログインフォームがあるとして

  • 会員IDの欄
  • パスワードの欄

がありそうですが、これらに

  • 未入力か
  • 正常に入力されているか
  • 異常な値が入力されているか

などの状態があると思うんですが、律儀にテストしていると

  • 3 * 3 = 9

パターンになります。これだけのログインフォームならまだいいですが、例えば会員登録時のフォームで、さらに

  • 性別
  • 都道府県
  • メールアドレス

などの項目がある場合、律儀に同様の組み合わせを試験に追加しようとすると

  • 9 * 27 = 243

とちょっと真面目に組み合わせの試験をする気がなくなってきますね。

ペアワイズ法とは

ところで、上の例では、すべてのパラメータの組み合わせが同等に結果に影響をあたえる、つまり、

  • 会員IDに関する結果を見たい時、他の4つのパラメータ(パスワード/性別/都道府県/メールアドレス)のすべての組み合わせを考える必要がある

としているので、

  • 会員IDが未入力
  • 会員IDが正常に入力されている
  • 会員IDに異常な値が入力されている

の3つのケースそれぞれに対して、他の4つのパラメータの組み合わせ(81通り)をテストするということで243通りになっているのですが、実際、会員IDに利用したい値が正常かどうかについて都道府県のフォームの入力値はあまり関係なさそうですよね。

つまり、

  • 複数のパラメータの組み合わせがテスト結果に影響をあたえるとしても、すべてのパラメータの組み合わせを使う必要はない
  • 実際テスト結果に影響をあたえるパラメータの組み合わせは2つくらいのパラメータからなることが多い

ので、すべてのパラメータから2個ずつ*1のペアを作って、そのペアについてはすべての結果を試験するようにテストケースを作成していこう、というのがペアワイズ法の考え方みたいです。

組み合わせの数が小さい場合について、実際に手作業でやってみる

ペアワイズ法を使ってみるとどういう風になるか、具体例を使って考えてみます。今回は、

  • 会員ID
  • パスワード
  • 性別

のパラメータについて、パラメータごとの値を、正常に入力されている(o)/未入力()/異常な値が入力されている(-)として、3つのパラメータのすべての値の組み合わせで表を作ると

会員ID パスワード 性別
o o o
o o -
o o
o - o
o - -
o -
o o
o -
o
- o o
- o -
- o
- - o
- - -
- -
- o
- -
-
o o
o -
o
- o
- -
-
o
-

と27パターンありますが、特に禁則などを指定せず、組み合わせの強さ2でペアワイズ法を利用した場合、

会員ID パスワード 性別
o o o
o - o
o o
- o -
- - -
- -
o
-
o - x
o -
- o o
- - x
o o
- x

の15パターンに減ります。

なぜ減るかというと、ペアワイズ法でテストケースを作成していく時、個々のテストケースにおいてペアにしていないパラメータの値の影響は気にしないことにしているので、複数のペアについて1つのテストケースである程度同時にテストしていくことが可能になるからです*2

たとえば、上の例だと、最初の9行で

会員ID パスワード
o o
o -
o
- o
- -
-
o
-

のように、会員IDとパスワードのペアについてすべての組み合わせ(9通り)をテストしていますが、この時、

会員ID パスワード 性別
o o o
o - o
o o
- o -
- - -
- -
o
-

のように、性別の値も入れ替えておくことで、

  • 会員IDとパスワードの組み合わせのテストをしながら
  • パスワードと性別の値のペア(9通り)のテストが同時に実行でき
  • 会員IDと性別のペアのテストの一部(3通り)も進められる

ので、ペアワイズ法を用いた場合のテストケースは、もとの3つのパラメータのすべての値の組み合わせをテストしていた場合と比べて 27 パターンから 27 - 9 - 3 = 15 パターンに減ります。

また、これまでパラメータと書いてきた入力条件にあたるものを 因子、因子ごとに取りうる値のことを 水準 と呼びます。

参考

ペアワイズ法を利用する際に使うPICTのツールや、ペアワイズ法そのものについてもっと詳しい解説記事はたくさんありますが、この記事を書く際に参考にしたものをリンクして、この記事はおしまいにします。

http://www.atmarkit.co.jp/ait/articles/1503/16/news149.html https://qiita.com/mitch0807/items/2d8af40fc58d359232ba

*1:これは実際のテストケースの状態によって調整する。ペアを作る時のパラメータの個数を「組み合わせの強さ」といって、2個ずつのパラメータでペアを作る時の組み合わせの強さは2.

*2:実際にその組み合わせはありえない、など、結果に影響があるパラメータについては指定することも可能です http://www.atmarkit.co.jp/ait/articles/1503/16/news149.html

いまさらRSpecを触ってみて最初のうち戸惑っていた部分の話

この記事はRuby on Rails Advent Calendarの1日目の記事です。

実は、この夏からはじめて仕事で本格的に RSpec を触ることになり、少し慣れてきたところで最初に感じたことをメモしておきます。

RSpecの構文は慣れない人にはこんな風に映ることもあるんだーと笑っていただけますと幸いです。

TL;DR

  • subjectlet でテストで利用する変数の宣言と初期化をする記法に慣れるまで時間がかかった
  • it .. do ... end のブロックを example ということ、 example の中にテストコードを書いていくことがわからなかった
  • describecontext に文法上の違いがあるかと思って身構えていた

慣れないうちに戸惑った話

テストコードやテスト対象をどこにどうやって記述したらいいかわからない

なかなか斬新な話なんですが、どういうことかというと、たとえば、自分がこれまで一番テストを書いてきたのはAndroidなので、Java用のテストフレームワーク JUnit5を例に挙げると

// https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested より
@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @BeforeEach
      void createNewStack() {
      stack = new Stack<>();
    }

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        // ...

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            // ...
        }
    }
}

のような感じなんですが、

  • 基本的にテストはクラスの単位でまとめられていて
  • インスタンス変数などの定義は通常のクラスと同様に行ってよく
  • テストの各ケースも通常のメソッドと補足情報をアノテーションで指定したりするが、同様に書いていけばよい
  • 各テストケース実行前の変数の初期化は BeforeEach などのアノテーションがついた箇所で宣言とは別の箇所で行う

んですね。一方同じ内容をRSpecで書くと以下のようになります。

describe "A stack" do
  let(:stack) { Stack.new }

  describe "when new" do
    it "is empty" do
      expect(stack.empty?).to eq true
    end

    # ...

    describe "after pushing an element" do
      let(:an_element) { "an element" }

      it "is no longer empty" do
        expect(stack.empty?).to eq false
      end

      # ...
    end
  end
end
  • まず、テストのためにクラスを定義しない
    • 内部実装をよくわかってないので違うのかもしれませんが、少なくともそういう書き方をしない
  • let で変数の宣言と初期化の方法を一緒に記載してしまう。変数の初期化だけのために前処理のブロックを用意していない
  • テストの内容を書いていく箇所もメソッドではなくてブロックの中

という具合で、かなり戸惑いが大きかったです。

特に3番目について、RSpecでは it ... do ... end のブロックで囲まれた部分にテストのコードを書いていき、その単位を example といって、その中にexampleなどを書いてくのですが、最初は

describe ブロックや context ブロックの中にどうして処理を書いていけないのか( example のそれと同じブロックやんけ)」

みたいな感じであわあわしてました。

また、テスト対象が1つに決まっている場合は subject という機能を使うと

  • テスト対象が1つに決まっていることを明確化され
  • expect(Hogehoge).to みたいにテスト対象の指定をしない is_expected.to という書き方が可能となる

のですが、これも最初「テスト対象どこに行った??」という感じでした。

# subject を使わない場合
describe "A stack" do
  let(:stack) { Stack.new }

  describe "when new" do
    it "is empty" do
      expect(stack.empty?).to eq true
    end
# subject を使う場合
describe "A stack" do
  let(:stack) { Stack.new }
  subject { stack.empty? }
  # 特にテスト対象がインスタンスそのものでなく、
  # インスタンスのメソッドの結果だったりする場合は
  # 直感的に理解できるようになるまで時間がかかった

  describe "when new" do
    it "is empty" do
      is_expected.to eq true
    end

エイリアスがたくさんある

また、テストコードを読んでいると

describe "..." do
  describe "..." do
    it "..." do
      ...
    end
  end

  context "..." do
    it "..." do
    end
  end
end

みたいな同じ文法の位置に違う単語があって、最初は文法的に何か意味があるんだろうか、と思ったんですが、

  • it / specify/ example は同じ意味で1つのテストケースのコードをグループ化するために用いられる
  • describe / context は同じ意味で example をグループ化するために用いられる
  • これらのエイリアスは自然な英語でテストが記述できるように用意されている

そうです。

まとめ

黙って一回チュートリアル。謙虚な気持ち、大事。

現場からは以上です。

慣れてきて覚えたことの話も書こうかと思ったのですが、長くなったので別記事にします。

参考

JUnit 5 User Guide

qiita.com

OPTIONSメソッドが気になったのでCORSとプリフライトリクエストについて少し調べました

Amazon API GatewayでCORSを有効にするとOPTIONSメソッドが追加されるのが気になったので、CORSについて少し調べました。

CORSとは

オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジン (ドメイン) で動作しているウェブアプリケーションに、異なるオリジンのサーバーにある選択されたリソースへのアクセスを許可することができる仕組みです。ウェブアプリケーションは、自分のオリジンとは異なるオリジン (ドメインプロトコル、ポート番号) からリソースをリクエストするとき、オリジン間 HTTP リクエストを発行します。*1

クライアントサイドの実装は、最近ではIEのごく一部のブラウザ以外で実装されていて、JavaScriptで異なるドメインのサーバへアクセスしようとした時によくエラーになっているあれです。

CORSが必要な理由

CORSのことは一旦置いておいて、Webサーバにリクエストを送った時のことを考えましょう。基本的に、Webサーバはリクエストを送ってきたクライアントのIPにレスポンスを返します。

f:id:woshidan:20181126210159p:plain

たとえば自社でAPIとWebページを開発しているときは以下のような感じになりますね。

f:id:woshidan:20181126210321p:plain

ここでもし、自社API(図中の api.example.com)に他者のホームページからリクエストが

f:id:woshidan:20181126210754p:plain

のように送信されていたらどうなるでしょうか。不正なリクエストを送ってサーバに不具合を起こされたり、顧客に本物のWebサイトと偽って表示して顧客に表示するためのコンテンツを盗まれたり、といったことは起こるかもしれません。

CORSはこのような場合に役に立つもので、CORSを用いると、Webサイトのものとは異なるドメインにリクエストを送る場合、そのリクエストが許可されていることをサーバに一度確認してからでないとPOSTなどのHTTPリクエストが送ることができなくなります*2

f:id:woshidan:20181126211356p:plain

f:id:woshidan:20181126211528p:plain

CORSの仕組み

CORSを用いると、Webサイトのものとは異なるドメインにリクエストを送る場合、そのリクエストが許可されていることをサーバに一度確認してからでないとPOSTなどのHTTPリクエストが送ることができなくなります と書きましたが、具体的にどのようにやっているのか、基本的なパターンについてさらっておきます*3

プリフライトリクエス

Content-Typetext/plain でそれ以外にめぼしいリクエストヘッダを含まないGETリクエストなど一部の単純なもの以外*4、 Webページ上のJavaScriptが外部ドメインにリクエストを送ろうとした際、Webブラウザは元のHTTPリクエストを送るのではなく、 プリフライトリクエスト というものを送信します。

この プリフライトリクエスト には

  • パスは元のリクエストのまま
  • メソッドは OPTIONS
  • リクエストヘッダには
    • リクエスト元のドメインを示す Origin
    • 元々送ろうとしていたリクエストが入る Access-Control-Request-Method
    • 元々送ろうとしていたリクエストのリクエストヘッダが入る Access-Control-Request-Headers

が含まれ、この プリフライトリクエスト を受け取ったサーバは、

  • サーバがリクエストを許可する外部サーバのドメイン一覧である Access-Control-Allow-Origin
  • サーバがリクエストを許可するリクエストメソッドの一覧である Access-Control-Allow-Methods
  • サーバがリクエストを許可するリクエストヘッダの一覧である Access-Control-Allow-Headers
  • プリフライトリクエストをしばらく有効にしておく期限を表す Access-Control-Max-Age

が含まれるレスポンスを返します。Webブラウザはこのレスポンスを見て、自分が送ろうとしていたリクエストが外部サーバから許可されているかを確認し、許可されているのを確認できたら元々送信したかったリクエストを送信します。

サーバから通信を許可されていないドメインであったり、許可されていないメソッドでリクエストしようとしている場合は、Webブラウザはリクエストの送信を断念します。

参考

*1:https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

*2:GETのみなど一部確認用のリクエスト(プリフライトリクエスト)なしに送ることも可能です(単純なリクエスト)。

*3:リクエストそのものだけでなくクッキーを扱えるかどうかなどもCORSを用いて制御することができます https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34#cookie%E3%82%82%E8%A8%B1%E5%8F%AF%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88

*4:詳しくは https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Simple_requests

AWSによるサーバーレスアーキテクチャを読みました

3連休はサーバレスをやるぞ! と決めていたので、2ヶ月くらい積ん読してた気がする本をようやく読みました。

AWSによるサーバーレスアーキテクチャ

AWSによるサーバーレスアーキテクチャ

最初の方、思ったよりLambda関数を作成してはロールを付与して確認して、の流れが慣れなくて思ったより時間がかかりました。。

また感想がてらいくつかの話題についてまとめておきます。

サンプルのLambda関数を動かしてみて

2年以上前の本だったのでAuth0などのライブラリが古くて動かないところもあったのですが、そもそもLambda関数を作ってイベントから起動させたり、API Gatewayから呼び出したり、というのに不慣れだったため、7章くらいまでは一部妥協しながらも手を動かしながら読み進めていました。

Lambdaで実装しようとするときの一連の流れは

  • Lambda関数用のロールとロール用ポリシー作る
  • 上を関数のロールに指定してLambda関数を作る
  • Nodeなど関数を書く
  • 書いた関数をzipでまとめてデプロイ
  • その後、S3などイベントを発行するサービスから連携の設定をする

という感じです。

ポリシーの作り方について、Lambda関数の実行に必要最低限のポリシーを用意するのに管理ポリシーの AWSLambdaBasicExecutionRole (CloudWatchへのログ書き込みに必要な権限のみ)というのがあり、これに対し関数で行う内容によってさらに必要な権限を付与していきました。

特に参考になったのが、テスト手法の紹介で、lamdbaと同様の処理*1がローカルで実行できる run-local-lambda の紹介や、Lambda関数のコンソールにあるテストの設定を作る際に利用出来るイベントテンプレートがすごくよかったです。

イベントテンプレートはAWSの各種サービスから受け取るイベントの内容を下のgifのように1クリックで確認できて、すごい楽になりました。

f:id:woshidan:20181125135514g:plain

あと、地味にLambda関数のデフォルト実行時間の3秒が短くて、サンプルの動画をダウンロードしてメタデータを読み取る関数がエラーになりまくるなどしました。

バックエンドで実行すべき処理の例

サーバレスアプリケーションでは、基本的にフロントエンド*2から直接リクエストできる処理はなるべく間にバックエンドのサーバーなどを挟まない方がよいとされているのですが、それでもバックエンドから呼び出した方がよい処理はあります。

たとえば、

  • フロントエンドのコードというのはユーザーのブラウザにロードされる情報なので、ユーザーのブラウザに読み込まれてはいけないような 機密情報 を利用する処理であったり。
  • ブラウザというのはいつ閉じられてもおかしくないのに、中断されたら困るような処理をブラウザ上で実行したり。

というのはバックエンドで行った方がよい処理です。

フロントエンドから直接サービスを呼ぶために、HTTPリクエストをAWSのサービスに合わせたクエリに変換する

上の節にもサーバレスアプリケーションでは、基本的にフロントエンドから直接リクエストできる処理はなるべく間にバックエンドのサーバーなどを挟まない方がよい、と書いたのですが、たとえば、

  • Firebaseをフロントエンドから利用する
  • Auth0などの認証・認可のサービスをフロントエンドから利用する

といったサーバレスを意識して作られているだろうサービスを利用するのは、これらのサービスのSDKやサンプルコードも公開されているしイメージがつきやすいです。

しかし、それ以外のサービス、たとえばAWSに用意したRDSへ接続して SELECT 文を発行するような場合、単純なHTTPリクエストでもHTTPリクエストの内容をクエリに変換するようなコードを動かすバックエンド(LambdaやEC2上で動くWebアプリケーション)を用意する必要があるのではないでしょうか?

たとえば GET /users/2 というHTTPリクエストを受け取ったとき、それを SELECT * FROM users where id = 2; のクエリとその発行に変換するためにLambda関数を用意するといった具合に。

そういう疑問があったのですが、単純なクエリであればAmazon API GatewayAWSサービス統合を利用すれば、バックエンドを用意せずに実装することが可能です。

リクエストのパラメータとクエリに使う値、レスポンスの形式をテンプレートを書いて指定するのですが、DynamoDBやKinesis Firehoseの例は以下の記事が参考になります。

dev.classmethod.jp

dev.classmethod.jp

Lambda関数を書いていく上で重要な要素について

Lambdaの関数を書いてく上で重要な要素

  • 関数ハンドラ
  • イベントオブジェクト
  • コンテキストオブジェクト
  • コールバック関数
  • ログ

についてメモします。

まず、関数ハンドラは、Lambdaランタイムが書いた関数を実行するために呼び出しているもの(=エントリポイント)です。関数ハンドラの外に書いたコードはウォームスタート時は実行されないので、変数の初期化などは関数ハンドラの外で行うとよさそうです。

関数ハンドラの構文は

exports.handler = fucntion(event, context, callback) { // コード }

となっていますが、それぞれの引数は

  • 第1引数 ... イベントデータ
  • 第2引数 ... コンテキストオブジェクト
  • 第3引数 ... コールバックオブジェクト

と呼びます。コールバックオブジェクトは、関数の呼び出し元に情報を返したい時やエラーのログを残したい時に使います。

ちなみに、LambdaではS3へのファイルアップロードなどのイベント駆動で動作する関数は呼び出し元に情報を返す必要はなく、これらの実装(情報を返したりログを残す)はLambda関数を作成する上ではオプションです(やった方がいいです)。

イベントオブジェクトは、Lambda関数を呼び出した側がLambda関数に渡す情報が入ったオブジェクトで、S3のファイルアップロードによるイベントで起動される関数なら、ファイル名やバケット名が入ったイベントオブジェクトを受け取ります。

単純にリクエストパラメータとイベントオブジェクトの値をJSONとして対応させたいならAPI GatewayでLambda統合を利用すればよく、認証情報などがリクエストヘッダに含まれるのをイベントオブジェクトから取り出せるようにしたい、などカスタマイズする場合はマッピング*3を行います。

コンテキストオブジェクトはLambdaランタイムについての情報がわかる便利なプロパティやメソッドが生えています。たとえば、getRemaininTimeInMillis() メソッドで実行中のLambda関数のタイムアウトまでの残り時間が分かります*4

コールバック関数は、ハンドラ関数のオプションの第3引数で、Amazon API Gateway経由で呼び出される関数のように、関数の呼び出し元に情報を返すためのものです。

callback(Error error, Object result) のように第1引数がエラーを表すオブジェクトとなっていて、処理に成功した場合の第一引数はnullとします。

ログを出力したい場合は consle.log("message") でCloudWatchにログを書き込めます。 console.warn()console.error() なども利用できますが、CloudWatch側としては実質的な処理に違いはありません*5

もう一冊読みたいな、と思ってたんだけど一冊目だけで思ったよりわたわたしてしまいました。

現場からは以上です。

*1:イベントのJSONが渡されてハンドラ関数が実行されるという意味で

*2:具体的にはWebブラウザで動作するJavaScriptですね

*3:https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html など

*4:詳しくは http://amzn.to/1UK9eib

*5:ただ、運用としてはアラートレベルやログオブジェクトはしっかり管理した方がよいでしょう。

試して理解 Linuxのしくみを読みました

有名な本を読みました。感想がてら特に面白かったいくつかのトピックについてまとめてしめます。

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

ファイルシステムの不整合について

本で出てきた例では

  • ファイルシステム上ではファイルはディレクトリに紐づいていて、階層構造になっている
  • そういった階層構造をmvなどで移動させる際、リンクの書き換えと元のリンクの削除の二つの操作が存在する
  • その2つ以上のアトミックに行われていてほしい処理が、途中で電源が切れるなどが原因で一部行われていないがために、ディレクトリの階層構造が壊れてファイルシステムをマウントしたりできなくなる

というような状態をファイルシステム不整合、という。ファイルシステムごとの対応策のあとの最終手段として、 fsck コマンドが存在するが、ファイルシステムの正しい状態がわからずにとりあえず整合性が取れた状態にしようとしたり、失敗に終わることが多かったりするのでファイルシステムが期待しているように利用できる状態になるというものではない。

ファイルシステムの違いとは何を指すのか、特定の目的のためのファイルシステム

ファイルシステム上のファイルやディレクトリをファイルやディレクトリとして扱うことができるのは、ファイルシステムごとに所定の方式でメタデータと本文のデータがストレージ上に配置されているため。

パーティションというのはハードディスク上で論理的に1つのデバイスのように扱える単位で*1、このパーティションという単位に対してファイルシステムを作成、マウントする*2

ファイルシステムでは、データへのアクセス方法やファイルとして扱うデータの形式によってさまざまなファイルシステムがあり、本の中では

他が紹介されていた。

コピーオンライトのライトにどうやって気づくか

fork() システムコールで新しいプロセスを生成する際、親プロセスのメモリを全て子プロセスにコピーするのではなく、ページテーブルだけをコピーする。

この際、親と子の両プロセスのページテーブルの各エントリに書き込み禁止フラグをつける。そうすると、書き込みが発生した時には、CPU上でページフォールトが発生するので、ページフォールトが発生したページだけ書き込み禁止を解除して子用に新しい物理メモリを割り当てるようにする。

コピーオンライトの「ライト」がなされたことを発見する方法が権限違反のエラーというのが面白かった。

HDDのアクセス特性と I/O スケジューラ、SSDの特性について

HDDは磁気ディスク(円盤)で、データ読み取りの際は

  • 半径方向へはアームを機械的に動かして
  • 円周方向へはディスクの回転でデータが書いてある位置へと

ディスクを動かす。そのため、HDDからデータを読み出す際は、複数のデータ読み出しがある場合は同じ半径の位置のデータはまとめて読み出すなどの工夫をすると機械的操作(時間がかかる)を減らせて高速化できる。アームの移動や回転が効率的に行えるように、複数のI/O要求の内容を並べ替えたりまとめたりするのがI/Oスケジューラ。

アームをどう動かすか、といったことはHDDによるが、HDDヘのI/O要求の内容自体はHDDごとに差はないのでデバイスドライバに実装するのではなく、ブロックデバイス層というファイルシステムやデバイスファイルより下のレイヤーにて実装されている。

HDDはシーケンシャルアクセスでは一定の値(HDDの性能限界)までは1回あたりのI/Oサイズが大きくなるほど性能が上がる。また、先読みやI/OスケジューラといったI/O支援機能によって1回あたりのI/Oサイズが小さい場合も性能を上げることができる。

一方、SSD機械的操作がないのでランダムでもシーケンシャルアクセスでもHDDほどは性能の差がなく、I/O支援機能を利用するとI/O要求を貯める待ち時間が無視できず性能が上がらない場合も。

現場からは以上です。

*1:https://users.miraclelinux.com/technet/document/linux/training/1_3_2.html

*2:ファイルシステムの作成、あるいはフォーマットで特定のファイルシステムで扱えるようにメタデータと本文の配置方法などを設定し、マウントでアクセスできるようにする? http://www.atmarkit.co.jp/ait/articles/1802/15/news035.html など

AWS DevDay Tokyo 2018 LT大会に「AWSを用いたWebサービスの負荷試験のTips」というテーマで参加してきました

9月にLTのCFPの公募があり、これはいい勉強になるだろうということで応募したら通ったのでわたわたしながら準備していました。

speakerdeck.com

以下、とっちらかっていますが、自分用のメモ兼補足です。

  • JMeterが同時に1000スレッドを立ち上げたらフリーズした件
    • JMeterJava製の負荷試験用ツールなんですが、2~3GHzの1つのクライアントだと同時に300~600くらいまででWebサーバの場合は例外*1とのことですが、約3GHzの仕事用MBPだとこの辺で止まりました。メモリじゃなくてクロック数書いた方がよかったかもです
    • 実際のテストケースでは、一度に立ち上げるスレッド数を制限して、かわりに短いテストシナリオを短時間でたくさんループして1分あたりのスレッド数*2を増やしています
  • 負荷用クラスタの構成あたりの話について
    • 3/10の量のリクエストが送れるかどうかのテストをしていた際に心配していたこと
      • これで3/10のリクエスト数が送れなかったらAWSアカウントで利用できるEC2のインスタンスタイプごとの制限台数*3にひっかかるからインスタンスタイプあげなきゃ...
      • マスターとスレーブの間の通信でマスターが詰まらないか
        • JMeterのスレーブはテストの最中、制御に必要な情報(グラフリスナーなどが必要とする情報も含むっぽい)をマスターに送っているので、それによってマスター/スレーブ間が詰まらないか*4
        • 結論からいうとCUIモードならだいたい大丈夫ではと思います
      • スレーブから対象クラスタへの通信でネットワーク帯域が詰まらないか
    • なぜEC2を利用しているのか
      • 並列数が欲しいだけならLambdaでもいいのでは、という感じなのですが、負荷試験で調べたいことにElastiCache, RDSとEC2のクラスタとの間のネットワーク転送量があり、ある程度キャッシュが効いた状態の様子を見たかったので
        • キャッシュが効くまでに5分くらい暖機数分をとりたかったことと
        • 上記の暖機をした上で本番に近い形でキャッシュが効くようなデータセットを続けて利用することを考えると、5分の稼働時間のLamdbaで設定やテスト用データの用意をするのはややこしそうです。EC2なら動かしっぱなしでいいので
        • ランダムだったり固定データだったらLambdaでもよさそう
  • 負荷試験用環境のセットアップについて*5
    • ストレージ容量が4桁GiBになってくるとバックトラック(弊社の試験時にうまくいかず。。)がうまくいかないこともあるみたいですね
    • 書いていない工夫としてはバッチの結果が入っているRedisがあって、そのデータを用意するため、バッチの実行時間のAuroraのスナップショットとバッチ実行後のRedisのスナップショットを用意していました
      • 試験当時はAuroraが停止できなかったのでクローンでインスタンス立ち上げた後停止させるわけにはいかなかったので。。

現場からは以上です。

あわせて読みたい

dev.classmethod.jp

*1:https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.pdf

*2:JMeter(に限らないかもですが)での同時に動かすスレッド数はだいたい同時にアクセスしているユーザ数みたいな感じ

*3:https://aws.amazon.com/jp/ec2/faqs/#How_many_instances_can_I_run_in_Amazon_EC2

*4:https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.pdf

*5:質問されたのですが、試験環境のセットアップも自分でやってました。だいなみっく!