覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Lombok - @Builderでデフォルト値を指定する

Lombok@Builderアノテーションは何かと便利なのですが、プロパティへ値をセットするためのBuilderのメソッドを呼ばないと対応するフィールドが初期値(数値なら0, booleanならfalse, オブジェクトならnull)になってしまいます。

このあたりの動きをv1.16.16で増えた@Builder.Defaultで改善できるようになっています。


Builderで値をセットしなかった時の動きの確認

動作確認用にPerson1というクラスを用意して@Builderを付与します。
Person1をBuilderを通じて2回生成します。1回目は全プロパティをセットしますが2回目はnameプロパティに値をセットしません。

import lombok.Builder;
import lombok.Value;

@Builder
@Value
public class Person1 {

    private final long id;

    private final String name;

    private final String description;
}


■呼び出し側のコード

public final class Person1Client {

    public static void main(String[] args) {
        Person1 p1a = Person1.builder()
                .id(10L)
                .name("Taro")
                .description("name set person.")
                .build();

        System.out.println("p1a#toString: " + p1a);

        // nameメソッドを呼ばない
        Person1 p1b = Person1.builder()
                .id(20L)
                .description("name not set person.")
                .build();

        System.out.println("p1b#toString: " + p1b);
    }
}


■実行結果

結果としてPerson1生成の2回目の呼び出しではnameの値がnullになっています。

p1a#toString: Person1(id=10, name=Taro, description=name set person.)
p1b#toString: Person1(id=20, name=null, description=name not set person.)

対象オブジェクトの生成側が全プロパティをセットするのが必須ならいいですが、そうでないケースもあると思います。
そういう場合にはセットしなかったプロパティにはデフォルト値を入れておきたいところです。


デフォルト値をセットしたい時は

デフォルト値を入れておきたいという要望に応えるための方法として若干裏技的に以下リンク先のような方法が存在しています。

@Builderアノテーションによって生み出されるコードを逆手に取ったような手法になっています。


@Builder.Default によるデフォルト値セット

Lombokのv1.16.16から@Builderでのデフォルト値指定のための@Builder.Defaultが追加されました

デフォルト値をセットしたいフィールドに@Builder.Defaultアノテーションを付与して、フィールドにデフォルト値をセットします。

本サンプルではnameフィールドに “<UNKNOWN>"という値をデフォルト値としてセットします

import lombok.Builder;
import lombok.Value;

@Builder
@Value
public class Person2 {

    private final long id;

    @Builder.Default
    private final String name = "<UNKNOWN>";

    private final String description;
}


■呼び出し側のコード

public final class Person2Client {

    public static void main(String[] args) {
        Person2 p2a = Person2.builder()
                .id(10L)
                .name("Taro")
                .description("name set person.")
                .build();

        System.out.println("p1a#toString: " + p2a);


        // nameメソッドを呼ばない
        Person2 p2b = Person2.builder()
                .id(20L)
                .description("name not set person.")
                .build();

        System.out.println("p2b#toString: " + p2b);
    }
}


■実行結果

結果を見ると、Builderのnameメソッドを呼ばない場合はデフォルト値(<UNKNOWN>)がセットされていることが分かります

p1a#toString: Person2(id=10, name=Taro, description=name set person.)
p2b#toString: Person2(id=20, name=<UNKNOWN>, description=name not set person.)


補足

実は私がこのエントリ書いてる段階では、IntelliJのIDE上では、@Builder.Defaultを付与したフィールドへBuilderで値をセットするメソッドがコンパイルエラーになってしまいました。
ただし、ビルドや実行はできるので、Lombokのpluginが対応できていないだけなのかもしれません。

EclipseであればIDE上も何もエラー出ませんでした。



関連エントリ

Go言語 - XMLを読んで特定の要素を削って出力

Golangではencoding/xmlパッケージでXMLを簡単に扱えそうなので、Hello World的に試してみました。

今回のエントリでは、XMLファイルを読んで特定のエレメントを削ったXMLを再出力するということをやってみました。
(といっても、Unmarshal(XML⇒構造体への変換)する構造体に対象要素に紐づくメンバを用意しないだけで実現できます)


前提

今回は以下のようなXMLが書かれたファイルがあるとして、その中からdescriptionというエレメントを除外したいとします

<?xml version="1.0" encoding="UTF-8"?>
<servers>
  <server>
    <id>s0001</id>
    <name>サーバA-001</name>
    <port>443</port>
    <description>試験用のサーバです</description>
    <subscriber_id>sub000002</subscriber_id>
    <contract_period>
      <start_date>2017-10-01</start_date>
      <end_date>2021-09-30</end_date>
    </contract_period>
  </server>
  <server>
    <id>s0002</id>
    <name>サーバA-002</name>
    <port>8080</port>
    <description>ECサイトで利用するための</description>
    <subscriber_id>sub000001</subscriber_id>
    <contract_period>
      <start_date>2012-04-01</start_date>
      <end_date>2024-03-31</end_date>
    </contract_period>
  </server>
  <server>
    <id>s0003</id>
    <name>サーバA-002</name>
    <port>8080</port>
    <description>利用用途未定</description>
    <subscriber_id>sub000003</subscriber_id>
    <contract_period>
      <start_date>2015-04-01</start_date>
      <end_date>2019-03-31</end_date>
    </contract_period>
  </server>
</servers>


Golang側のコードでは、上記XMLをUnmarshalして紐づける構造体を以下のように用意しました。
(server構造体に、descriptionのエレメントを紐づける変数がありません)

除外する要素以外は構造体のタグで紐づけを行っています

type ServerList struct {
    XMLName xml.Name `xml:"servers"`
    Svs     []server `xml:"server"`
}

type server struct {
    XMLName                   xml.Name `xml:"server"`
    ID                        string   `xml:"id"`
    ServerName                string   `xml:"name"`
    Port                      string   `xml:"port"`
    SubscriberID              string   `xml:"subscriber_id"`
    ContractStartDate         string   `xml:"contract_period>start_date"`
    ContractEndDate           string   `xml:"contract_period>end_date"`
}


実行サンプル

以下XMLファイルを読んで、エレメント削って出力するGoのプログラムです。
読み込むXMLファイルをコマンドラインで -f パラメータに与えて、エレメントを除外した結果を標準出力に出力するようにしています。
(そのため、結果をリダイレクトれば結果をファイルに出力できます)

package main

import (
    "encoding/xml"
    "flag"
    "fmt"
    "io/ioutil"
    "os"
)

type ServerList struct {
    XMLName xml.Name `xml:"servers"`
    Svs     []server `xml:"server"`
}

type server struct {
    XMLName                   xml.Name `xml:"server"`
    ID                        string   `xml:"id"`
    ServerName                string   `xml:"name"`
    Port                      string   `xml:"port"`
    SubscriberID              string   `xml:"subscriber_id"`
    ContractStartDate         string   `xml:"contract_period>start_date"`
    ContractEndDate           string   `xml:"contract_period>end_date"`
}

func main() {
    var (
        srcXML string
    )

    flag.StringVar(&srcXML, "f", "servers.xml.", "src xml path.")

    flag.Parse()

    file, err := os.Open(srcXML)
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }
    defer file.Close()
    data, err := ioutil.ReadAll(file)
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }
    serverList := ServerList{}
    err = xml.Unmarshal(data, &serverList)
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }

    output, err := xml.MarshalIndent(serverList, "  ", "    ")
    if err != nil {
        fmt.Printf("error: %v\n", err)
    }
    os.Stdout.Write([]byte(xml.Header))
    os.Stdout.Write(output)
}


ビルド(go build)後に以下のようにコマンドを実行(original.xmlが元のXMLファイル)

xmlconv.exe -f original.xml > dest.xml


結果、dest.xmlには以下のような内容が出力されています(descriptionというエレメントが除外されています)

<?xml version="1.0" encoding="UTF-8"?>
<servers>
    <server>
        <id>s0001</id>
        <name>サーバA-001</name>
        <port>443</port>
        <subscriber_id>sub000002</subscriber_id>
        <contract_period>
            <start_date>2017-10-01</start_date>
            <end_date>2021-09-30</end_date>
        </contract_period>
    </server>
    <server>
        <id>s0002</id>
        <name>サーバA-002</name>
        <port>8080</port>
        <subscriber_id>sub000001</subscriber_id>
        <contract_period>
            <start_date>2012-04-01</start_date>
            <end_date>2024-03-31</end_date>
        </contract_period>
    </server>
    <server>
        <id>s0003</id>
        <name>サーバA-002</name>
        <port>8080</port>
        <subscriber_id>sub000003</subscriber_id>
        <contract_period>
            <start_date>2015-04-01</start_date>
            <end_date>2019-03-31</end_date>
        </contract_period>
    </server>
</servers>


というわけで簡単にXMLファイルを扱うことができました

普通にコンストラクタ呼び出すのとリフレクション使うのとでどれだけ速度差あるの?

Javaでインスタンス生成する場合は、コンストラクタを実行するのが普通ですが、リフレクションでもやれます。

ただ、一般的にリフレクション使うと遅いというのが懸念点の一つとして上がってきます。

というわけで、普通にコンストラクタ実行するとのリフレクション使うのでどんな差があるのかをJMH使って測定してみます


測定条件

測定するインスタンス生成方法は以下の通りです

  • 普通のコンストラクタ実行
  • Class#newInstance(リフレクションを利用)
  • Constructor#newInstance(リフレクションを利用)


インスタンス化するクラスは以下です(デフォルトコンストラクタのみ持ちます)

final class Person {
    private long id;
    private String name;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}


実行環境のJavaは、Java7です


JMHでの測定用のコードは以下の通りとなります

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;


public class NewInstanceBenchmark {

    @Benchmark
    public void normal() {
        Person p = new Person();
    }

    @Benchmark
    public void clazzNewInstance() throws Exception {
        Person p = Person.class.newInstance();
    }

    @Benchmark
    public void constructorNewInstance() throws Exception {
        Person p = Person.class.getDeclaredConstructor().newInstance();
    }

    static final class Person {
        private long id;
        private String name;

        public long getId() {
            return id;
        }

        public void setId(long id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(NewInstanceBenchmark.class.getSimpleName())
                .warmupIterations(20)
                .forks(1)
                .mode(Mode.Throughput)
                .build();
        new Runner(opt).run();
    }
}


測定結果

JMHの実行結果は以下の通りです

# Run complete. Total time: 00:02:02

Benchmark                                     Mode  Cnt           Score          Error  Units
NewInstanceBenchmark.clazzNewInstance        thrpt   20     7833794.167 ±    96565.569  ops/s
NewInstanceBenchmark.constructorNewInstance  thrpt   20     2182452.929 ±    70285.111  ops/s
NewInstanceBenchmark.normal                  thrpt   20  3600508098.995 ± 18204543.613  ops/s

上記結果は以下のスループットを表しています

  • NewInstanceBenchmark.clazzNewInstance ⇒ Class#newInstance で インスタンス生成
  • NewInstanceBenchmark.constructorNewInstance ⇒ Constructor#newInstance で インスタンス生成
  • NewInstanceBenchmark.normal ⇒ 普通のコンストラクタ(デフォルトコンストラクタ)実行 で インスタンス生成


Java7環境で測定したというのも関係しているとは思いますが、普通にコンストラクタ実行するのとリフレクション使うのでは雲泥の差です。
本測定においては、普通のコンストラクタ呼び出しとClass#newInstanceでは450倍以上の違いが出ています。
やはり、リフレクションは必要最低限の場面で使うべきかなと感じました。



関連エントリ

Javaでユニットテストを書く時に気を付けたいこと

Javaプログラムに対するユニットテスト(単体テスト)を書く際に気にしておきたいことを書いてみました。

以下、個人的な経験則と本で読んだ内容が混ざっています


ユニットテスト作成時の原則

  • 「このクラスはXXXの理由でユニットテストが難しいんです!」と言いたくなったら
    • ユニットテストし易いように対象のクラスを作る(作り直す)
      一般的にこのような設計の変更をした方が実運用にも適していたらい柔軟になったりする場合が多いです
  • privateメソッドを強引にユニットテストしない
    • リフレクションを使って出来なくはないが、基本的にやるべきではない。ユニットテストが内部の実装に依存しすぎる
    • 呼び出し元のpublicメソッドのユニットテスト経由で処理を網羅すればよい
    • ‘’‘どうしてもprivateメソッドのテストをしたいなら、そもそもprivateメソッドではなく別クラスのpublicメソッドとして公開する必要性が高い(元の設計が間違っている)’‘’
  • テストが書けない(書きにくい)箇所もある
    • 画面が絡む部分はJUnitでのユニットテストは書きにくい。無理やり書く必要は無いと思います。
  • 振る舞いをテストする
    • テストを作成する際には、個々のメソッドをテストするとは思わず、対象のクラスのふるまいに着目する必要があります。
    • ユニットテストを作成する際には、まず全体的な観点を持つべきです。
      個々のメソッドをテストするのではなく、それぞれの組み合わせからなるクラスとしてのふるまいをテストするということを念頭に置く必要があります


AAA

アプリケーションのテストは一般的に、Arrange(セットアップ) と Act(操作) と Assert(アサーション) という3つの部分に分かれています。
この構成を頭文字をとってAAAと呼ぶことがあります

  • Arrange(セットアップ)
    • テスト実施の準備をし、テストが実行される環境が適切な状態である事を保証します
  • Act(操作)
    • テストのコードを実行します。
  • Assert(アサーション)
    • テスト対象のコードが正しくふるまったかどうかを確認します。コードからの戻り値やテストに関わったオブジェクトの状態などがチェックされます。


FIRST

良いテストはFIRST

  • Fast(迅速)
  • Isolated(隔離)
  • Repetable(繰り返し可能)
  • Self-validation(自律的検証)
  • Timely(タイムリー)

逆に以下に該当すると良くないテストと言えるかもしれません

  • 読んだ人が理解できないテスト
  • 成功することも失敗することもあるテスト
  • 意味のある検証を行っていないテスト
  • テスト対象のコードを十分に網羅していないテスト
  • 実行に長い時間がかかるテスト
  • テスト対象のコードが少し変更されただけでも、多数の失敗が発生してしまうような依存性の強いテスト
  • セットアップに多くの時間を必要とする複雑なテスト


カバレッジ

  • ソフトウェア全体として'‘'カバレッジが70%未満の場合はかなり低い’‘'(と思います)
  • 本質的ではないコードに対してテストを書いてカバレッジを無理やりあげる必要は無いです(以下のようなテストは書くのは本質的ではない)
    • JavaBeansの単純なgetter/setterに対するユニットテスト
    • privateコンストラクタのユニットテスト
  • カバレッジの感覚的な把握ではなく、できるだけツールで測定する(EclEmma等)。


バグが発生しやすい場所

バグが発生しやすい場所(状況)として以下のようなものがあります。これらは重点的にテストすべき箇所となります。

  • 境界値近傍の引数
  • ifやforループなどがネストしている箇所
  • if文の条件が複雑になっている箇所
  • メソッド内で早期returnができる状況で早期returnをしていない場合
  • nullを取り扱っている箇所
  • ローカル変数を使い回している箇所
  • 他プロダクト(他システムと)とのインターフェース境界部分
  • 不変ではない(可変の)変数やオブジェクトをマルチスレッドで扱っている場合


関連ツール・ライブラリ

  • Mockito
    • モックライブラリ
  • JMockit
    • モックライブラリ
  • EclEmma
    • テストのカバレッジを計測・可視化するためのツール
  • AssertJ
    • 流れるようなインターフェースで書けるので、HamcrestのMatchersよりテストケースのコードが書きやすい
  • mocker
    • Fluent Mockito Builder



関連書籍・関連エントリ

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

実践 JUnit ―達人プログラマーのユニットテスト技法

実践 JUnit ―達人プログラマーのユニットテスト技法

blog.y-yuki.net

運用から見たシステムに関してのあれやこれや

ITシステム(サービス, アプリケーション)の開発を行っていると、設計してコード書いてテストしてリリースするという流れになり、 このリリースというのがゴールという感覚に陥る場合があります。

実際は、システム(サービス)は運用が開始されてからがスタートだと言っていいと思います。

が、私含めた開発者というのは往々にして運用フェーズに対して意識がいきにくい気がします。
DevOpsが広まった世の中ではそんなことないんですかね?私の感覚が古すぎ?)

これまでの経験から、運用観点でシステムがどうあるべきか、開発者が何を意識しておくべきなのか を書きました。
(思いっきり個人的な見解なので、システムやサービスやビジネスモデルによって当てはまったり当てはまらなかったりだと思います)


実行環境

  • 実環境とQA環境は一致していない
    • 開発環境やQA(品質保証)環境が実際のサービスが動作する環境と(スペックやリクエスト量が)同等になっている状況はほとんどありません。
      自分の開発環境やQAチームの環境で問題が出なくても、実環境では問題が出ます。
  • ハードウェア
    • ITシステムは当然ですが、ソフトウェアだけではなくハードウェアがあることで成り立っています。
      クラウドになろうが仮想化されようがその下支えとしてハードウェアが存在する事実は変わりありません、ハードウェアは経年劣化したり破損したりが当然のようにあります。
      ファンが故障したり、ディスクが読めなくなったり、マシンの電源が入らなくなったり。
      ハードウェアの影響を受けて性能が上手く出せない、予期せぬシャットダウンを余儀なくされる等の試練をソフトウェアは受けることになります。
  • 実環境とステージング環境を区別できるように
    • コンソールでステージング環境(テスト環境)に接続して作業しているつもりが、実際には実環境(本番環境)につないでました。というケースがあります。
      そうならないためにも、実環境とステージング環境では接続までのステップを変える。接続した際のターミナルの背景を異なる色で表示する等、明確にどこにつないでいるかが分かるようにした方が良いです。
  • ネットワーク機器
    • 実環境では、開発環境やQA環境には存在していなかった負荷散装置やファイアウォール等のネットワーク機器が介在していることが多々あります。


ユーザ

  • 大事なお客様は攻撃者
    • システム(サービス)は利用ユーザが存在して(お金を払ってくれるから)こそ存続することができます。
      しかし、ユーザは意図せずシステムへの攻撃者ともなります。大量のリクエストを投げてきたり、思いもよらない巨大なデータのダウンロードをしたり…
      とにかく、この意図しない負荷にシステムは耐える必要があります。
  • やらかしているユーザを特定できるようにする
    • 上に書いた通り、ユーザは意図しないにせよ意図したものであるにせよシステムに対して負荷をかけてくる場合があります。
      そういったユーザに対しては何らかの対処が必要になり、対象のユーザを把握する必要があります。
      そのためにも対象のユーザのユーザIDや送信元のIP等がログに記録されている必要があります。
  • ユーザIDを共有させない
    • いくら対象ユーザを特定しようとしても複数ユーザ間で1つのユーザIDが共有されていると特定が困難になります。BtoBのシステムでこの状況に陥る場合があります。
      必ず個人単位に割り当てられたユーザIDのみが利用されるシステムである必要があります。
  • 優先すべきユーザ
    • 多くの代金を支払っていたり紐づく実利用者が多い等のビッグユーザも存在すれば、そうではないユーザもいます。やはり、ビッグユーザの方がサービス運営には大きなインパクトがあります。
      規模の小さなユーザを軽んじるという意味ではないですが、サービスを運用・開発する側としてはユーザ対応に関して戦略的な優先度付けは必要になります。
  • ITリテラシーは人それぞれ
    • サービスの利用者はITリテラシーが高い人だけとは限りません。色々な人が使うことを理解したうえで、ユーザのサポートをする必要があります
  • ユーザの無効化
    • システムを利用しなくなったユーザを削除または無効化するための仕組みは必須です


現場での障害・エラー

現場(実環境)では、実装やユニットテスト時には想定しにくいエラーパターン(障害パターン)が存在しています。
アプリケーションレベルでは対応できないものも含まれますが、システム全体としては考慮しておいても良いと思います。

  • ディスクフルになって書き込みができない
  • ディスクがunmountされている
  • inodeが枯渇
  • ファイルシステムが破損
  • ファイルシステムがreadonlyになった
  • スレッドが枯渇
  • メモリの枯渇(OutOfMemory)
  • 他システムから仕様に違反した電文が送られてきた
  • リクエスト先が無応答
  • 無限や非数を表す数値データが送られてきた
  • レプリケーションしているDB間に差分がある
  • データベースに仕様と異なるデータが混入している
  • 証明書の期限が切れた
  • JavaVMがクラッシュ
  • 設定ファイルに書かれた設定内容の整合性が取れていない
  • (機器の不具合で)稼働後に一定日数経つと機器が勝手に再起動する
  • bindしたいポートが重複
  • システム日時がずれている
  • シーケンス番号がオーバーフロー
  • DBのMAXコネクション数を超えた接続要求があった
  • 通信量が多すぎてインターネットプロバイダに通信速度を制限される
  • メールの宛先や文面に対するチェックでメールが到達しない
    • メールを監視するシステムが別に存在していて、メールの宛先が不適切だったりメールの文面に入れてはいけない単語が入っていたりするとそのシステムがメールを弾く場合があります


ログ

  • ログが頼り
    • ログを出力しないアプリケーションも存在しますが、基本的にシステムの状況を知るための入口となるのはログです。
      エラーが発生した状況などではとにかくログが手掛かりですので詳細な状況を記録する必要があります。
  • 整形
    • 基本的に同一システム内では統一されたフォーマットでログを出力したほうが良いです。フォーマットが異なるとログを解析する労力が大きくなりやすいです
  • コンテキスト(文脈)を出力
    • エラーをログに記録する場合は、発生したエラー内容だけではなく、その現象を取り巻くコンテキストの情報についても出力するべきです。(いつ、誰の操作で、どんなイベントやデータが関連しているかetc)
      そうした情報がある方が、原因を追いやすくなります。
  • 単位も出力
    • 何らかの数値をログなどに出す際は単位も出力するべきです。単位がbyteなのかMBなのかで話は大きく変わってきます。
  • 個人情報
    • システムの特性にもよりますが基本的にログに個人情報は出力するべきではありません。出力するにしても暗号化するかマスク処理は必須です。
  • Diskを消費する
    • ファイルにログを出力しているとすれば、出力すればするほどDisk容量を食うことになります。ログもリソースを消費することは意識しておいたが方が良いです。
  • パフォーマンスの劣化
    • 非同期型ではなく同期型のロギングを行っている場合は特に、ログを出力すればするほとアプリケーションの性能劣化につながる場合があります。ログの出力し過ぎが害になる場合があります。
  • エラーは記録した、でも誰も気づかない
    • アプリケーションでエラーが発生して、ログファイルに記録した。が、誰もこのエラーに気づいていない。
      この状態は最悪で、何の意味もありません。
      エラーが書かれたことを監視する仕組みや通知する仕組みなど必須です。
      誰の目にも触れないものは存在していないのとほとんど同じです。


画面

  • ユーザが一番触る場所
    • システムが提供する画面は、ユーザが最も頻繁に触れる場所となります。ということは、ここを通じて何らかのエラーが発生することになります。
      画面そのものがおかしな動作をする場合もあれば、バックエンドの仕組みに悪影響を与えることがあります。
  • 画面はシステムの顔
    • システムは画面だけではなくグラフィカルなUIを持たないバックエンドのサービスや色んな物の組み合わせで成り立っています。画面はシステムの一部でしかありません。
      が、ユーザが直接見るのは基本的に画面です。そのためユーザは画面の操作性やカッコよさ(or ダサさ)を通じてシステムの良し悪しを判断することがあります。


分散

  • 相関ID
    • 複数のサブシステムやサービスに分かれてシステムが成り立っている場合、特定のリクエストに対する各サービスの一連の処理を串刺しで確認したい場合があります。そのために、リクエストID(correlationId)等を発行してそのIDを各サービスに引き渡した方が良いです。
      各サービスはそのIDをログに出力します。そうすることで繋がりを把握することができます。
  • オフラインでも動作してほしい
    • 分散したサービスが協調している場合、リクエスト先のサービスが落ちていることも十分にあります。相手先が落ちている状態でも自分自身のサービスは正常に動作し続けるべきです。
  • 処理のブロッキング
    • 意識してプログラムを書かない限りほとんどの場合は処理をブロックするコードを書くことになります。
      分散して非同期で動作して協調するシステムの場合ブロッキングされている箇所は、ボトルネックになり問題を引き越す箇所になりやすいです。
  • 物理的な距離
    • データセンターを複数個所においてレプリケーションしている場合に、データセンターの物理的な距離に応じてデータが書かれてから冗長性が本当に確保されるまでに時間がかかることがあります。
  • 障害の発生個所と原因箇所
    • システムが複数のサービスに分割している状況では、障害が発生する(目に見える形でエラーが発生する)箇所と原因箇所は離れていることが往々にしてあります。
      障害発生個所周辺だけを追っても原因にたどり着かないことがあります。


バグのないシステムは無い

アプリケーションにバグがあるのは当然ですが、OS(カーネル)やミドルウェアやその他ファームウェアにも当然のようにバグはあります。
それらに対してどう付き合っていくかは当初から計画しておくべきです。


ネットワーク

  • ボトルネックになる
    • システム内で最もボトルネックになりやすい場所かもしれません。他の場所で性能が出ていてもネットワーク起因で上手く性能が出せないことは多々あります。
  • 信頼してはいけない
    • ネットワークはシステム内で最も信頼してはいけない場所です。運用中に何らかの通信が失敗する状況は必ずやってきます。
  • リモート呼び出しとローカル呼び出しは別物
    • リモート呼び出しでは、ローカル呼び出しになかった障害パターンが現れます。同じ機能を呼び出す場合もローカルで行うのかネットワーク越しなのかで考慮することは全く変わってきます。


データベース

  • ボトルネックになる
    • データベースは基本的にボトルネックになりやすい場所です。
  • トリガー
    • 開発者が意図しないトリガーを運用チームなどがDBにしかけてしまうと予期せぬ動きをすることがあります。把握していないトリガーが存在していると相当な混乱を生みます。
  • データ量
    • テーブルのレコード数が極端に増えた場合にSQLの実行計画が全く意図しないものになる場合があり、突然性能劣化する場合があります。
  • 遅いクエリを監視する
    • 遅いSQLがあると、システム全体がその遅さに引っ張られる場合があります。遅くなかったSQLがデータ量の増加などによって遅くなることがあります。遅いSQLが実行されていないか監視すべきです。
  • インデックスが破損する
    • インデックスが壊れる場合があります。インデックスの再構築が必要となります。


できるだけ自動化

  • 可能な限り変更操作のSQLを直接実行させない
    • データのメンテナンスが必要で、運用チームが手動でSQLを実行するケースがあります。
      ただし、人間が介在すれば当然操作を誤る場合もあります。アプリケーションとしてのデータの整合性がとれない状況も発生します。
      可能な限りアプリケーション側でデータメンテナンスの仕組みやツールを提供するべきです。
  • 手動の作業はスケールしない
    • 人の手が必要な作業はスケールしません。システムが大規模化していくと人手がかかる作業は漏れていく場合があります。
  • 一度限りと思っていた作業も
    • “こういうデータを抽出したい"という運用観点での要望が良くあります。一度限りならその場限りのSQLを組み立てたり、手動でデータを整形したりでも良いでしょう。
      同じ依頼が再度来たらシステム化またはツール化することを検討した方が良いです。3度目4度目の依頼が来るのは確定です。
      最低限でも手順化して、他の人にも任せられるようにした方が良いです。


エラーコード・メッセージID

エラーや障害があった場合に、そのエラーを一意に特定するエラーコードやメッセージIDをログや画面に表示するべきです。
詳細なエラー内容やメッセージの文面しかないと担当者間で誤った伝言ゲームが発生して状況が正しく伝わらない場合があります。
状況を一意に示すID等があった方が伝達はしやすいです。また、国際化対応しているシステムの場合にもIDやコードだけ分かれば状況を把握しやすくなります。


データ

  • 稼働当初が一番データが少ない
    • ほとんどのシステムがシステム内に蓄えられるデータが増加していきます。稼働の最初が最もデータが少なかったという状態が普通です。
      当初のデータ量なら動いていても、1年後5年後のデータ量では性能が出なくなることもあります。
  • データの増加傾向を監視する
    • 定期的に日単位、月単位、年単位でデータの増加量を監視するべきです。ディスクの残容量との兼ね合いもありますし、意図しない怪しい増加が無いかを確認しておくことも重要です。
  • 削除する仕組み
    • データを保管するというのもタダではありません。扱っているデータのサイズが巨大化した場合に必要のないデータを削除して、取り扱う量を減らす仕組みを検討すべきです
      仮に削除しないにしても、アクセス頻度の低いデータは安価な保管手段に変更するなどの検討をする必要があります。
  • 去っていくユーザのデータの取扱い
    • 提供しているサービスに不満がありユーザが契約を切る(契約を更新しない)場合もあります。そうしたケースにおいてシステム内に蓄積された対象ユーザのデータをどう扱うのかは当初から取り決めておく必要があります。
  • テストデータ
    • 運用時は利用しないが、運用前の試験時に負荷をかける目的などでテストデータを用意する場合があります。
      このデータが誤って運用中にシステム内に送られてしまうケースがあります。できればテストデータのID等からフィルタリングできる仕組みなどを設けておいた方が安全です。


データ移行

完全に新規のシステムやサービスで無ければ基本的に何らかのデータ移行が発生します。

移行のパターンとして以下のようなものが考えられます。

  • 他システムからの移行
  • 他システムへの移行
  • 既存システムから新システムへの移行

データ移行に関しては、移行元のデータをどのように受け取るのかとどう出力するのかのインターフェースが重要です。
また、インターフェース以外にも以下の点についても考慮が必要です。

  • データ移行にかかる費用。その費用を誰が負担するのか
  • データ移行にかかる期間
  • データ移行がシステムへどのような負荷をかけるか
  • データ移行時にシステム停止が伴うか?
  • データ移行が正常に完了したこと(移行元と移行先で不整合になっていないか)をどのようにチェックするのか?


結合・接続部分

他社サービス、他システムや他プロダクトとの結合点・接続部分は色々と考慮してもしきれません

  • 絶対に問題が発生する場所
    • 他システム・他サービスなどと連携している箇所は運用後に絶対にエラーが発生します。結合部分でのエラー処理は必須です
  • 意図しないエラー
    • 他システムとは取り決められたインターフェースでやり取りします。決められたエラーコードなどに基づいたエラーハンドリングも組み込むでしょう。
      が、運用後には取り決めに無かったエラーが突然返ってきます。それにもアプリケーションは対処できる必要があります
  • 耐えられない量のリクエスト
    • 誤って大量のリクエストを投げる場合もあります。リクエストを受ける側は何らかの流量制御して自分自身がダウンしないようにする必要があります。
  • タイムアウト
    • リクエスト先からエラーが即時に返ってくれば良いですが、場合によってはいつまでたっても応答が無い場合もあります。必ずタイムアウトできる仕組みを入れるべきです。
      そうしなければスレッド全部を使い切ってサービスがダウンすることにもつながります。
  • リクエストしすぎて攻撃とみなされる
    • 他社サービスに短時間に大量のリクエストを投げたことによって、相手側に攻撃とみなされ、リクエストの受け付けが拒否されるようになる場合があります。


リトライ

  • 短い間隔で無限リトライすると攻撃と同じ
    • リクエストを受ける側のサービスがダウンしている場合に、クライアント側はリトライすることでしょう。
      しかし、短い間隔で無限にリトライを繰り返すとシステム全体への負荷になり、場合によってはシステムをダウンさせる原因にもなります。
  • べき等性
    • 何らかの理由でクライアント側が全く同じリクエストを複数回送信してくることがあります。リクエストを受けた側はそれでも正常に動作して同じ結果を生み出すべきです。


監視・復旧

  • システムの負荷の周期性
    • 基本的にはシステムにかかる負荷(CPU負荷、ネットワークトラフィック、メモリの使用量・・etc)には、一定の周期があります。(平日のこの時間帯はCPU負荷が高い、夜間の特定時間帯はネットワークトラフィックが多い 等)
      その周期性から外れた予想外の負荷状況が発生していないかを日単位、週単位、月単位で確認した方が良いです。
  • 自動復旧
    • よほど単純なサービスやアプリケーションでなければ、長期間稼働する中でダウンする時はダウンします。
      重要なサービスであれば、対象のサービスのプロセスを監視して、ダウンしていれば自動復旧させるなどの仕組みを検討した方が良いです。
  • ユーザより先に気づく方が良い
    • 監視の仕組みでシステムの障害予兆を把握できた方が、ユーザから連絡により障害を気づいた場合よりも対処のコストは圧倒的に下がります。
  • 監視サービス自体がダウンしないように
    • システムを監視する側のサービスがダウンしてしまうと、システムが本当に正常に稼働しているのかどうか分からなくなります。
      監視サービス自体がダウンしない(ダウンしても自動復旧する)ような仕組みが必要です。


アプリケーションの実行状況把握

  • ヘルスチェック
    • アプリケーションが正常に稼働しているかどうかをチェックするための口を設けた方が良いです。監視サービスから定期的にそのヘルスチェックを実行するのも一つの監視となります。
  • 処理の進捗状況
    • 時間のかかる処理や、大量のリクエストを扱う処理などは、その処理の進捗状況(処理が詰まっていないか)が分かる仕組みがあると良いです。
      キューの残数、ダウンロード状況など。
  • アプリケーションのバージョン
    • 実際にシステムにデプロイ(インストール)されているアプリケーションのバージョンがいくつなのかを把握できるようにすべきです。
      アプリケーション(サービス)に問いかけると自身のバージョンを応答してくれるようになっていると扱いやすいです


システムは安定稼働している?

システムが安定稼働しているように見えるが本当にそうなのか?
システムの現在の状況は以下のいずれの状態なのか?

  • システムは安定稼働しており、負荷についても余裕がある。ユーザも何の問題も無く利用できている。
  • 現状は正常に稼働している、しかし実は後わずかで負荷が閾値を超えてシステムが正常ではない状態に陥ろうとしている。
  • システムに遅延が発生しているが、運用側で設定した閾値は超えていない。しかし実は、ユーザ(利用者)はシステムの遅さに苦痛を感じている。
  • 自システムは正常に稼働している、が、連携する他システムが障害を起こしている。ユーザは、私たちのシステムを通じてエラーを体験している。
  • 異常が発生しており、監視サービスにより通知もされている。が、運用側の人間がエラーを無視している。
  • 監視の仕組みの漏れにより、異常が起こっていることに気付けていない(エラーが埋もれている)。


バックアップ

  • リストアできないと意味が無い
    • 定期的なバックアップはシステムにとって必須です。バックアップはできている(はず)だが、そのバックアップしたデータでリストアはできますか?
      もしくはリストアするのにどれだけの時間を要しますか?リストア中に発生した整合性の取れていないデータを整合が取れる状態にできますか?
  • バックアップ処理による負荷
    • システムによってはバックアップするデータのサイズが巨大化する場合があります。バックアップ処理そのものがシステムに負荷をかける場合もあります。


単一障害点

その一箇所が動かなくなるとシステム全体がダウンするような単一障害点(Single Point of Failure)は当然ない方が良いです。冗長化するなど何らかの方策が必要です。
この点はシステム上の話だけではなく、人にもいえます。対象のサブシステムについて詳しい人が一人しかいない場合、その人が長期休暇を取るのが危険という場合があります
日ごろから人間側の冗長性も確保すべきです。


スケール・国際化

  • 最初からスケールする仕組みを組み込む
    • システムのスケールについては当初から計画しておく必要があります。後付けでのスケールはかなり難しくなります
      1000リクエストに対応できていたサービスにおいて、(後付けで)サービスが稼働するサーバの台数を単純に増やしても1000万リクエストには耐えられない場合があります。
  • グローバルにやる予定なら
    • 対象のサービスをグローバルに展開する予定があるのなら最初から国際化の対応をしておくべきです。後付けで国際化しようとするのは結構な労力になります
  • 展開する地域が増えたときのリクエスト量の増加
    • サービスを展開する国や地域が追加された時に、リクエストの増加がそれまでの伸びとは関係なく激増する場合があります。
      例えば中国などの人口の多い地域に展開した際にはそのような可能性があります。
  • 小規模ユーザーに対して提供できるか
    • (主にBtoBのサービスにおいて、)サービスの初期ターゲットが大規模ユーザであった場合でも、サービス拡大とともに小規模ユーザへのサービス提供も必要になってくるケースがあります。
      この際に、サービスのビジネス的な設計がうまくいっていないと、小規模ユーザへのサービス提供が金額的な面などで折り合わないことがあります。
      小⇒大のスケールだけではなく、大⇒小のスケールのさせ方も考慮しておくべきです。


法的要求事項・ガイドライン/監査

  • 要求事項
    • システムによっては法的な要求事項や国が定めたガイドラインに準拠しなければならない場合があります。
      これらの内容も年々変わっていく場合があり、追従していく必要があります。
  • IaaS系クラウドサービスの利用
    • クラウドサービスによっては、物理的なサーバが隠蔽されているだけではなくそのサーバが存在するデータセンターの所在が非公開になっている場合があります。
      通常は特に問題になりませんが、データセンターの監査を受ける必要が出てきた時にこれではNGとなる場合があります(所在不明のため監査ができない)
  • 法によるデータの存在場所のしばり
    • システムにかかわる法律やガイドラインによって、データを管理してよい場所(国)が限られる場合があります(例えば、日本にしかデータを置いてはいけない等)
      このような状況では例えば、クラウドでアメリカなどのリージョンを指定できないことになります。
  • 監査ログ
    • ユーザ操作を監査ログとして永続化して通常のアプリケーションログとは別に管理する場合があります。監査ログを追うことでユーザが何をしていたのかが明らかになります。
  • 法定停電
    • ビルなどは点検のために定期的に停電する日が必ず来ます。ユーザー環境にシステムに関する機器があるなら、電源が落ちる日が定期的に来ます。


改善したら悪くなった

システム(サービス)を良かれと思って改善したら結果的にユーザにとっては改悪になってしまったという場合もあります

  • バグによる挙動を改修したが、ユーザにとってはそれが既に仕様となっていてやりたいことができなくなってしまった。
  • デフォルトの挙動をより良いと思うものに変えた、ユーザにとっては動きが勝手に変わってしまって業務効率が落ちた。
  • 速度に問題の合った箇所を改善したら別の場所が改善後の速度に耐えられずハングした。


知識の共有

  • リリースした機能を共有する
    • 開発チームと運用チームが分かれている場合に、新しくリリースされたアプリケーションの変化を運用チームが把握できない場合があります。
      定期的にチームを超えて情報を共有する場を設けるべきです。
  • 暗黙知を無くす
    • サービス稼働後に走りながら障害対応やデータメンテナンス等を行っていくと暗黙知が溜まっていきます。暗黙知が当然の状態になってしまうと、チームに新しい人が加わっても上手くパフォーマンスが発揮できません。
      得られた知見はドキュメト化してチーム内に共有する必要があります。
  • 現場の対応をドキュメント化
    • 運用中に緊急でその場限りの対応(設定を変更した、再起動のcronを仕掛けたetc)をする場合がありますが、そのような対応が結果的に恒久策になっている場合があります。
      ただ、これらの対応がドキュメント化されていないと後の運用・保守において混乱を招くことになります。


競合

  • 参入障壁が低いと
    • 構築したサービスの参入障壁が低いとすぐに競合サービスが出現します。他社サービスの状況を観察する必要がでてきます
  • 競合サービスがダウンした際の自サービスへの影響
    • BtoCのサービスにおいてシェアが同規模の競合サービスが存在する場合に、何らかの理由で競合サービスのシステムがダウンすると、一気に自サービスにユーザが流入してくる場合があります。
      その際には予定しない負荷がかかることになります


コスト

  • 運用にかかったコスト
    • クラウドのサービスに払った費用だけではなく、パッチを当てる対応や障害時の対応等にどれだけの人的リソースが投入されたかも把握しておくべきです。
  • サービスによってコストが違う
    • IaaS系クラウドサービス等は、サービスごとに発生する費用のモデルが違います。CPUの使用時間、データの蓄積量、通信量など何に対して課金されるのかが異なってきます。
      提供するビジネスモデルに応じてコスト計算しなければ、とんでもない費用を払わなければならない事態になる場合もあり得ます。


システムへ影響を与えるシステム外のイベント

アプリケーションやシステムそのものではどうにも対処しきれないが、場合によってはシステムに悪影響を及ぼすものがあります。

  • 落雷
  • 停電
  • 地震
  • 洪水
  • 火災
  • 空調の故障による温度異常
  • ネットワークケーブルが切断
  • 電圧降下

必ずしも悪影響を与えるわけではないが、念頭に置いておいた方が良いものとして以外

  • うるう年
  • うるう秒
    • うるう秒挿入時にOSが暴走するケースなどもあります
  • 年号が変わる
  • サマータイム
    • 日本ではほぼ無縁ですが、これが絡んでバグを発生させるライブラリが存在したりします。


まとめ

ユーザの要求を満たす機能を提供するプログラムを書くだけでは、システムを作り上げて運用に耐えられるということにはなりません。
もう少し高い位置からの視点が必要になりますね。

色々雑多な感じで書きましたが、ものによってはシステム稼働後に追加で対応できるものもあります。しかし、稼働前から考慮しておかないと厳しいものもあります。



関連エントリ