1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

Goの並行処理解説ブログを参考に少しソースを書き換えてみて理解を深める

Goの並行処理について書かれたブログ

medium.com

このブログがイラスト付きで本当に可愛くGoの平行処理の基礎を教えてくれて良いんです。

このブログを参考に整理してみる

このブログはある一つの例を取って、並行処理に記述しています。それは鉱物の発見・掘り出し・加工です。 元のブログでは、それぞれの動作をするGopher君が描かれています(めちゃめちゃ愛らしいので是非見てください)。

シングルスレッド処理とマルチスレッド処理の違い

シングルスレッド

Gopher族Gary君が1人で鉱物の加工までをこなします。なんてマルチな才能なんだ。 ブログではシングルスレッドのソースコードは載ってなかったので愚直に書きました。

出力

From Finder:  [Ore Ore Ore]
From Miner:  [minedOre minedOre minedOre]
From Smelter:  [smeltedOre smeltedOre smeltedOre]
package main

import (
    "fmt"
    "strings"
)

func main() {
   // すべてGary君のお仕事
    theMine := [5]string{"Rock", "Ore", "Ore", "Rock", "Ore"}
    foundOre := finder(theMine)
    minedOre := miner(foundOre)
    smelter(minedOre)
}

func finder(mine [5]string) []string {
    foundOre := make([]string, 0)
    for _, m := range mine {
        if m == "Ore" {
            foundOre = append(foundOre, m)
        }
    }
    fmt.Println("From Finder: ", foundOre)
    return foundOre
}

func miner(foundOre []string) []string {
    minedOre := make([]string, 0)
    for _, fo := range foundOre {
        minedOre = append(minedOre, fmt.Sprintf("mined%s", fo))
    }
    fmt.Println("From Miner: ", minedOre)
    return minedOre
}

func smelter(minedOre []string) {
    smeltedOre := make([]string, 0)
    for _, mo := range minedOre {
        smeltedOre = append(smeltedOre, strings.Replace(mo, "mined", "smelted", -1))
    }
    fmt.Println("From Smelter: ", smeltedOre)
}

マルチスレッド

Gary君は鉱物の発見、Janeは掘り出し、Peterは加工を行うようにしました。 ブログのほうではそれぞれをgoroutineにして処理を行っていました。そのまま写経するのも面白くないのでselectcontextを使って書いてみました。

出力

From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted
From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted
From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted
func main() {
    theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
    oreChannel := make(chan string, 5)
    minedOreChan := make(chan string, 5)

    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()

    go func(ctx context.Context) {
        for {
            select {
            // Janeの仕事
            case foundOre := <-oreChannel:
                fmt.Println("From Finder: ", foundOre)
                minedOreChan <- "minedOre"
            // Peterの仕事
            case minedOre := <-minedOreChan:
                fmt.Println("From Miner: ", minedOre)
                fmt.Println("From Smelter: Ore is smelted")
            }
        }
    }(ctx)

    // Gary君の仕事
    go func(mine [5]string) {
        for _, item := range mine {
            if item == "ore" {
                oreChannel <- item
                time.Sleep(10 * time.Millisecond)
            }
        }
    }(theMine)

    <-ctx.Done()
}

channel

channelはgoroutineが相互にやり取りするときに使用されます。上の例だと、finderとminerがやり取りするときとminerがsmelterとやり取りするときに使用しています。goroutineはchannelに対して送信と受信ができます。<-を使用して表現します。

oreChannel <- item  // 送信
foundOre := <-oreChannel  // 受信

このchannelという機構のおかげでJaneはGary君がmineをすべて見つけるのを待たずして、見つけた分から処理をしていくことができます。

channelはいろんな状況でgoroutineをブロックします。これによって同期的な処理を行うことも可能です。 バッファ付きチャネルとそうでないチャネルの2種類があります。チャネルに送信するときバッファがない場合は、ブロックされます。oreChannel := make(chan string, 5)宣言のときにcapを指定しないとbufferなしのchannelになるはずです。

context

goの1.6まではcontextパッケージが無く自前で実装していたそうな。contextパッケージは、キャンセルを伝搬できるのも強みとのこと。 やはりdeeeetさんは神

Go1.7のcontextパッケージ | SOTA

まとめ

selectchannelの組み合わせで楽に並行処理を実装できる。場合によっては、selectじゃなくてfor~rangeのほうが良いかもしれない。 タイムアウト系はcontextパッケージを利用する。

CurlでGCSにファイルをアップロードする

gsutilがどうしても使えないけどGCS使いたい

例えば、KubernetesのPostStartとかPrestopでGCSからダウンロード/アップロードするとか。
Dockerfile内でGCS使わなくてはいけなくなった。でもそれだけのためにRUNして後でお片づけするのも時間ちょっと増えるし嫌みたいな時とか。

curlでの処理

アクセストークンを取得

jqが使えれば、もっとちゃんとしたのが書けるのですが今回は無い想定で書いてます。もっと良い方法があれば是非教えていただきたく。 以下を参照。

Creating and Enabling Service Accounts for Instances  |  Compute Engine Documentation  |  Google Cloud

TOKEN=$(curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token?alt=text" -H "Metadata-Flavor: Google" | grep access_token |awk '{ print $2 }')

POSTリクエストでファイルアップロードする

TEXT="文字列"
curl -X POST --data "${TEXT}" \
                    -H "Authorization: Bearer ${TOKEN}" \
                    -H "Content-Type: text/plain" \
                    "https://www.googleapis.com/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${OBJECT_NAME}"

k8sのdeploymentのpostStartに記述してみる

アップロードするパスは後で見やすくなるように適宜スラッシュ入れて区切るべしってやつですね。アップロードが多くなるようなら日にちで一旦スラッシュ入れたりとかになるんでしょうか。 postStart / preStopで複数行の記述をする方法に関しては以下を参考にしました。

kubernetes - multiple command in postStart hook of a container - Stack Overflow

          preStop:
            exec:
              command:
              - "sh"
              - "-c"
              - >
                ACCESS_TOKEN=$(curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token?alt=text" -H "Metadata-Flavor: Google" | grep access_token |awk '{ print $2 }');
                TEXT="文字列";
                OBJECT_NAME=Bucket/$(date +"%Y%m%d%H%M%S")_$(hostname);
                curl -X POST --data "${TEXT}" \
                    -H "Authorization: Bearer ${ACCESS_TOKEN}" \
                    -H "Content-Type: text/plain" \
                    "https://www.googleapis.com/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${OBJECT_NAME}";

docker参照コマンドチートシート

よく使うDockerコマンド

調査系のみ

コンテナの基本情報を絞って表示

docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
NAMES               IMAGE               STATUS
r1                  redis               Up 25 minutes

コンテナのIPアドレス一覧

docker ps -q | xargs docker inspect --format '{{ .Name }} - {{ .NetworkSettings.IPAddress }}'
/r1 - 172.18.0.2

コンテナがマウントしているボリュームを表示

docker run  -v /docker/redis-data:/data --name r1 -d redis
# マウント一覧を表示
docker ps -q   | xargs docker inspect --format '{{ .Name }} - {{range $mount :=.Mounts}}{{ $mount.Source }} <-> {{ $mount.Destination }}{{end}}'
/r1 - /docker/redis-data <-> /data

コンテナの統計情報

docker ps -q | xargs docker stats
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %
     NET I/O             BLOCK I/O           PIDS
3c1719011282        r1                  0.08%               6.918MiB / 737.6MiB   0.94%     26.3kB / 90B        6.37MB / 0B         4

docker内のネットワーク

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
cc9ab9e4598e        bridge              bridge              local
fa054a9af353        host                host                local
f50397115ef2        none                null                local
$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "cc9ab9e4598e563b04c3c58f93ccdc15d9c1c682abbaf05e44034c63254cde4f",
        "Created": "2018-08-06T16:05:15.115331313Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.18.0.1/24",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "0e4a62e4d4a90d38f9f3d1be868f278807b390cdaba92951448ef64bdedd492e": {
                "Name": "tutorial_web_1",
                "EndpointID": "b396b59a93be30084d902e882eb21e961eac9d8599edbf32e832e1c05d3dc305",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/24",
                "IPv6Address": ""
            },
            "94158270f37a09f5ee28412dc569b048319fec166e1b0cf952fd8efa24c798a7": {
                "Name": "tutorial_redis_1",
                "EndpointID": "a14d820f0659721ebe1293e56871e1e68f94d6e07c5067f85c892e37a86bcfe4",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

まとめ

コンテナの起動や削除は開発環境(ローカル)はcompose、検証・本番環境はECS/GKEだから使用頻度が高いのは調査系のみだった。

pod(Kubernetes)のlifecycle.prestopの挙動

コンテナ削除時すぐにコンテナをSTOPされると困ることありません?

 

例えば….?

ApacheやNginxなどのWebサーバはSIGTERMが送られても、処理中のコネクションがCloseされないまま終了してしまう。 理想としては、残っている接続済みのコネクションを終了してからコンテナがSTOPして欲しいですよね。

Prestopを設定する

SIGTERMでの挙動は以下のようなもの(https://httpd.apache.org/docs/2.2/ja/stopping.htmlより引用)

TERM あるいは stop シグナルを親プロセスに送ると、即座に子プロセス全てを kill しようとします。 子プロセスを完全に kill し終わるまでに数秒かかるかもしれません。 その後、親プロセス自身が終了します。 処理中のリクエストは全て停止され、もはやリクエストに対する 応答はされません。

これでは、まずいのでPrestopフックを入れます。PrestopフックはPodのシャットダウンプロセスの最初に実行されるものです。Deploymentの.spec.template.spec.containers.lifecycle.preStopに処理を記載していきます。httpGet等でhttpリクエストを引っ掛けることもできますが、基本的にはexecで記述することが多いように思えます。execは記述したコマンドの同一のコンテナで実行します。

Apacheのwebサーバの場合だと、httpd -k graceful-stopをDeploymentの.spec.template.spec.containers.lifecycle.preStop.exec.commandに書いていくような形となるかと思います。Pod preStop フックの実行と Service エンドポイントからの Pod IP アドレス削除は並列して実行されるため、preStop フックの実行が先に実行されてしまいアクセスがまだ来る状態でコネクション作成をやめてしまうことになります。 なので、httpd -k graceful-stopの前に数秒置く必要があります。また、指定したコマンドが非同期の場合には、勝手にpreStopフックが完了されたとみなし、SIGTERMがPodのコンテナに送られてしまいます。そのため、非同期のコマンドのあとにはそのコマンドでかかる時間sleepしておくなどするべきです。

もしapacheのリクエストのタイムアウト時間を30秒としているならば、以下のような設定となるかと思います。 terminationGracePeriodSecondsはpodのシャットダウンプロセスが始まってからSIGKILLが送られるまでの時間です。preStopの設定を入れている場合は、preStop処理が終了後にSIGTERMが送られ、シャットダウンプロセスが始まってからterminationGracePeriodSecondsで指定した秒数が経過してもプロセスが完了していない場合にはSIGLKILLが送られます。デフォルトは30秒となっているはずです。

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: httpd
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 2; httpd -k graceful-stop; sleep 30"]
  terminationGracePeriodSeconds: 40

kubernetesソースコード上では以下のように、preStopが実装されてます。 preStopの定義がDeploymentにない場合は、すぐにStopContainerが送られてしまうのがわかると思います。

        // Run the pre-stop lifecycle hooks if applicable and if there is enough time to run it
    if containerSpec.Lifecycle != nil && containerSpec.Lifecycle.PreStop != nil && gracePeriod > 0 {
        gracePeriod = gracePeriod - m.executePreStopHook(pod, containerID, containerSpec, gracePeriod)
    }
    // always give containers a minimal shutdown window to avoid unnecessary SIGKILLs
    if gracePeriod < minimumGracePeriodInSeconds {
        gracePeriod = minimumGracePeriodInSeconds
    }
    if gracePeriodOverride != nil {
        gracePeriod = *gracePeriodOverride
        glog.V(3).Infof("Killing container %q, but using %d second grace period override", containerID, gracePeriod)
    }

    err := m.runtimeService.StopContainer(containerID.ID, gracePeriod)
    if err != nil {
        glog.Errorf("Container %q termination failed with gracePeriod %d: %v", containerID.String(), gracePeriod, err)
    } else {
        glog.V(3).Infof("Container %q exited normally", containerID.String())
    }

Prestopフックですが、標準出力などに吐き出してもStackdriverなどでは拾えないので注意です。なお失敗した場合には、kubectl describe <pod>のイベントのところに表示されます。

インフラ初心者がAWS(Amazon ECS)についてまとめてみた

前提となる知識の理解

以下の資料を読んで理解を深めた。
アマゾン ウェブ サービスの概要
20180411 AWS Black Belt Online Seminar Amazon EC2
リージョンとアベイラビリティーゾーン - Amazon Elastic Compute Cloud
AWSにおけるマルチアカウント管理の手法とベストプラクティス
AWS アカウント ID とその別名 - AWS Identity and Access Management
AWS Black Belt Online Seminar AWS Identity and Access Management (AWS…

RegionとAvailability Zone

  • AWS クラウドインフラストラクチャはリージョンとアベイラビリティーゾーン (AZ) を中心として構築される。
  • リージョンは2つ以上のAZ から構成されている(ローカルリージョンを除く)。各リージョンは、他のリージョンと完全に分離されるように設計されている。
  • 各AZは互いに影響にを受けないように、地理的・電源的・ネットワーク的に独立している。AZ間は低遅延の高速専用線で繋がれている。
  • AZ は 1 つ以上の独立したデータセンターで構成される。各データセンターは、冗長性のある電源、ネットワーキング、および接続性を備えており、別々の設備に収容されている。このような AZ によって、お客様は単一のデータセンターでは実現できない高い可用性、耐障害性、および拡張性を備えた本番用のアプリケーションとデータベースを運用できる。
  • AZはAWS上ではリージョンコードとそれに続く文字識別子によって表されます (us-east-1a など)
  • 東京リージョンのリージョンコードはap-northeast-1で大阪ローカルリージョンのリージョンコードはap-northeast-3となっている。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/images/aws_regions.png

Account

  • AWSアカウントはAWSアカウントIDによって一意に識別される。全てのAWS製品の中で一意であれば、AWS アカウントの別名を作成することもできる(ただし一つまで)
  • AWSアカウントとは、”リソース管理の単位”かつ”セキュリティ上の境界”であり、”AWS課金単位”でもある。
  • 単一でなく、複数のAWSアカウントの使用がAWSゴールドパートナーであるNRIさんの推奨である。
  • 複数のAWSアカウントを用いる理由(メリット)
    • ガバナンスの観点
      • 本番環境の管理コンソール(後述)を分離できる
      • 本番環境と非本番環境を管理するメンバーとで明確な権限の分離
      • 環境間におけるセキュリティ対策の分離が可能
    • 課金の観点
      • 分離したアカウント毎に明確な課金管理を行うことができる。
      • 複数のコストセンターやLOB等に対し、シンプルな課金やチャージバックの運用を行うことができる。
    • 組織の観点
      • 異なるLOBを異なる課金管理と共にサポートすることができ、LOB単位での管理を容易に行うことができる
      • 統制や課金が異なる専用アカウントを用いる個々の顧客に対してサービスプロバイダー型のサービスを提供しやすい
    • 運用の観点
      • 変更による影響範囲を縮小し、リスクアセスメントをシンプルにできる。例)開発環境の変更が本番環境に及ばない
      • AWSアカウントのリソース上限にかかる可能性を緩和できる

サービスにアクセスする方法

AWS IAM (Identity and Access Management)

  • AWS操作をよりセキュアに⾏うための認証・認可の仕組み
  • AWS利⽤者の認証と、アクセスポリシーを管理
    • AWS操作のためのグループ・ユーザー・ロールの作成が可能
    • グループ、ユーザーごとに、実⾏出来る操作を規定できる
    • ユーザーごとに認証情報の設定が可能 f:id:jrywm121:20180505205648p:plain

AWSアカウント(root)ユーザー

  • AWSアカウント作成時のID
  • アカウントの全ての AWS サービスとリソースへの完全なアクセス権限を持つ
  • アカウントの作成に使⽤したメールアドレスとパスワードでサインイン
  • ⽇常的なタスクには、それが管理者タスクであっても、root ユーザーを使⽤しないことを強く推奨
  • AWSのroot権限が必要な操作
    • AWS ルートアカウントのメールアドレスやパスワードの変更
    • IAMユーザーの課⾦情報へのアクセスに関するactivate/deactivate 等々…
  • AWSルートアカウントはIAMで設定するアクセスポリシーが適⽤されない強⼒なアカウントであるため、⼗分に強度の強いパスワードを設定した上、通常は極⼒利⽤しないような運⽤をする。Security CredentialのページからAccess Keyの削除を行う。

IAMユーザー

  • AWS操作⽤のユーザー(1AWSアカウント毎に5000ユーザーまで作成可能)
  • ユーザーごとにユーザー名/パス(オプション)/所属グループ(上限は10)/パーミッション(Json形式で記述)

IAMグループ

  • IAMユーザーをまとめるグループ(1AWSアカウント毎に100グループまで作成可能)
  • グループ毎にグループ名/パス(オプション)/パーミッション

IAMで使⽤する認証情報

  • アクセスキーID/シークレットアクセスキー(2つまで) = REST,Query形式のAPI利⽤時の認証に使⽤
  • X.509 Certificate = SOAP形式のAPIリクエスト⽤
  • AWSマネジメントコンソールへのログインパスワード
  • 多要素認証(MFA)

アクセス条件の記述

f:id:jrywm121:20180505214653p:plain

AWS CloudTrail

  • AWSアカウントで利⽤されたAPI Callを記録し、S3上にログを保存するサービス。
  • AWSのリソースにどのような操作が加えられたか記録に残す機能であり全リージョンでの有効化を推奨。
  • 適切なユーザーが与えられた権限で環境を操作しているかの確認と記録に使⽤。
  • 記録される情報
    • APIを呼び出した⾝元
    • APIを呼び出した時間
    • API呼び出し元のSource IP
    • 呼び出されたAPI 等々…

IAMロール

  • AWSサービスやアプリケーション等、エンティティに対してAWS操作権限を付与するための仕組み
  • IAMユーザーやグループには紐付かない
  • 設定項⽬は、ロール名とIAMポリシー

AWS STS(Security Token Service)

  • ⼀時的に利⽤するトークンを発⾏するサービス
  • ユーザーに対して、以下の3つのキーを発⾏
    • アクセスキー:(ASIAJTNDEWXXXXXXX)
    • シークレットアクセスキー:(HQUdrMFbMpOHJ3d+Y49SOXXXXXXX)
    • セッショントークン(AQoDYXdzEHQ……1FVkXXXXXXXXXXXXXXXXXXXXXXXX)
  • トークンのタイプにより有効期限は様々

AWSを運用する上での常識を身につける

以下の資料を読んで理解を深めた。 (英語のホワイトペーパーは読めてない)
20180403 AWS White Belt Online Seminar AWS利用開始時に最低限おさえておきたい10のこと
AWS Well-Architected Framework
AWS Black Belt Online Seminar 2016 AWS CloudTrail & AWS Config
Amazon RDS 入門

セキュリティ

  • AWSルートアカウントは極力使用しない
    • 十分に強度の強いパスワードを設定した上で多要素認証(MFA)で保護し、通常は極力使用しない運用を行う。
    • Security CredentialのページからAccess Keyの削除を行う。
  • ユーザには最小限の権限を付与する
    • IAMユーザとIAMグループを利用する
    • 最小限のアクセス権を設定し、必要に応じて追加のアクセス権限を付与する。
    • 特権のあるIAMユーザ(機密性の高いリソースやまたはAPIにアクセスが許されているユーザ)に対してはMFAを設定する
    • 認証情報を定期的にローテーションする
  • 認証情報を埋め込まない
    • 認証情報をOSやアプリケーション側に持たせることなく、IAMロールを使用してAWSサービス操作権限を付与する
    • 認証情報はSTS(Security Token Service)で生成し、自動的に認証情報のローテーションが行われる
  • 証跡取得(ログ取得)
    • AWS CloudTrailによって操作ログを取得する。取得したログをCloudWatch Logsに転送し監視することも可能
    • Amazon GuardDutyを利用して疑わしいアクティビティをログから検知する
  • 各レイヤでのセキュリティ対策
    • ネットワークレイヤ(ネットワークACL
      • VPCのサブネット単位で設定するステートレスなファイアーウォール
      • ベースラインとなるポリシーを設定するのに用いる
    • Amazon RDS / EC2などのリソース(セキュリティグループ)
      • インスタンス(グループ)単位に設定するステートフルなファイアーウォール
      • サーバの機能や用途に応じたルール設定に適している
    • AWS WAF
    • AWS Shield

コスト最適化

  • 適切なサイジング
    • AWS CloudWatchでリソース利用状況を把握する
      • AWS上で稼働するシステム監視サービス
      • システム全体のリソース使用率、アプリケーションパフォーマンスを把握
      • 予め設定した閾値を超えたら、メール通知、AutoScalingなどのアクションを起こすこともできる
      • またCloudWatch LogsでOS上やアプリケーションのログも取得可能

データのバックアップ

  • EC2のAMI(Amazon Machine Image)を作成。
  • EBSのスナップショットを取得。作成時はデータ整合性を保つために静止点を設けることを推奨
  • RDSの自動バックアップ機能

障害や不具合への対策

f:id:jrywm121:20180506011824p:plain

  • ELB(Elastic Load Balancing)の活用
  • AutoScalingの活用
  • EC2 AutoRecoveryの活用

Amazon VPC(Virtual Private Cloud)

以下の資料を読んで理解を深めた。
20180418 AWS Black Belt Online Seminar Amazon VPC
AWS Black Belt Online Seminar 2016 AWS CloudFormation

概要

  • AWS クラウドの論理的に分離されたセクションをプロビジョニング(※1)し定義した仮想ネットワーク内の AWS リソースを起動することができる
  • 独自の IP アドレス範囲の選択、サブネットの作成、ルートテーブル、ネットワークゲートウェイの設定など、仮想ネットワーク環境を完全にコントロールできる
  • VPC のネットワーク設定は容易にカスタマイズすることができる。たとえば、インターネットとのアクセスが可能なウェブサーバーのパブリックサブネットを作成し、データベースやアプリケーションサーバーなどのバックエンドシステムをインターネットとのアクセスを許可していないプライベートサブネットに配置できる。
  • 既存のデータセンターと自分の VPC 間にハードウェア仮想プライベートネットワーク (VPN) 接続を作成することができるので、AWS クラウドを既存のデータセンターを拡張するかのように活用することができる。

※1 プロビジョニング:
必要に応じてネットワークやコンピューターの設備などのリソースを提供できるよう予測し、準備しておくこと

VPCをカスタマイズするコンポーネントたちと構成例

f:id:jrywm121:20180506014018p:plain

f:id:jrywm121:20180506014126p:plain

↓あとでそれぞれの項目を補足する
インターネット接続VPCのステップ

  • アドレスレンジの選択
    • 推奨: /16 65,534アドレス
  • AZに於けるサブネットの選択
    • VPCあたりのサブネット作成上限数はデフォルト200個
    • /24に設定すれば、200個作成する上で効率が良い(サブネットあたりのIPアドレス数 251)
    • サブネットで利用できないIPアドレス(/24の例)

      ホストアドレス 用途
      .0 ネットワークアドレス
      .1 VPCルータ
      .2 Amazonが提供するDNSサービス
      .3 AWSで予約
      .255 ブロードキャストアドレス
  • インターネットへの経路を設定
    • ルートテーブルはパケットがどこに向かえば良いかを示すもの
    • PublicサブネットのIPがIGW(Internet GateWay)経由でインターネットまたはAWSクラウドと通信するときにパブリックIPを利用
    • EC2のOSで確認できるのはプライベートIPのみ
  • VPCへのIN/OUTトラフィックを許可 f:id:jrywm121:20180506120927p:plain

    |ネットワークACL |セキュリティグループ| |:--:|:--:| |サブネットレベルで効果|サーバレベルで効果| |Allow/DenyをIN・OUTで指定可能(ブラックリスト型)|AllowのみをIN・OUTで指定可能(ホワイトリスト型)| |ステートレスなので、戻りのトラフィックも明示的に許可設定する|ステートフルなので、戻りのトラフィックを考慮しなくて よい| |番号の順序通りに適用|全てのルールを適用| |サブネット内のすべてのインスタンスACLの管理下に入る|インスタンス管理者がセキュリティグループを適用すればその管理下になる|

VPCとのプライベート接続

  • VPN接続 : バーチャルプライベートゲートウェイを利用したサイト間VPN
    • 1つのVPN接続は2つのIPsecトンネルで冗長化
    • ルーティングは静的(スタティック) / 動的(ダイナミック:BGP)が選択可能
  • 専用線接続 : AWS Direct Connectを利用し、一貫性のあるネットワーク接続を実現する。本番サービス向け。
    • AWSとお客様設備を専用線でネットワーク接続

VPC設計のポイント

  • CIDR(IPアドレス)は既存のVPC、社内のDCやオフィスと被らないアドレス帯をアサイ
  • 複数のアベイラビリティゾーンを利用し、可用性の高いシステムを構築
  • パブリック/プライベートサブネットへのリソースの配置を慎重に検討

VPCと他サービスとのやり取り

  • サービスによってVPC内と外のどちらにリソースやエンドポイントが存在するかが異なる
  • VPC Endpointは、グローバルIPをもつAWSのサービス(例えば、DynamoDB / Lambda / S3 / SQS 等…)に対して、VPC内部から直接アクセスするための出口です。
  • VPC EndpointはGateway型の動作とPrivateLink (Interface型)の動作に分かれる
    • Gateway
    • PrivateLink (Interface型)
      • サブネットにエンドポイント用のプライベートIPアドレスが生成される。
      • VPC内部のDNSがエンドポイント向けの名前解決に対してしてプライベートIPアドレスで回答する。
      • エンドポイント用プライベートIPアドレス向け通信が内部的にサービスに届けられる。

f:id:jrywm121:20180506101243p:plain

NATゲートウェイ

Amazon VPCの実装方法

  • マネジメントコンソール
  • AWS CLI / AWS SDK
  • Ansibleなどのサードパーティツール
  • AWS CloudFormation
    • EC2やELBといったAWSリソースの環境構築を、設定ファイル(テンプレート)を元に自動化できるサービス
    • テンプレートには起動すべきリソースの情報をJSONYAMLフォーマットのテキスト形式で記述する
    • 構築済みの環境からテンプレートを作成するツール(CloudFormer)も有益

VPC Flow Logs

  • ネットワークトラフィックをキャプチャし、CloudWatch LogsへPublishする機能
  • ネットワークインタフェースを送信元/送信先とするトラフィックが対象
  • セキュリティグループとネットワークACLのルールでaccepted/rejectされたトラフィックログを取得
  • 運用例:Elasticsearch Service + kibanaによる可視化

f:id:jrywm121:20180506104749p:plain

Amazon ECS(Elastic Container Service)

以下の資料を読んで理解を深めた。 20180220 AWS Black Belt Online Seminar - Amazon Container Services
20180313 Amazon Container Services アップデート
What is Amazon Elastic Container Service? - Amazon Elastic Container Service
Task Definition Parameters - Amazon Elastic Container Service Amazon EC2 Container Service(ECS)の概念整理 - Qiita

概要

  • Amazon EC2 Container Service (ECS) は、Docker コンテナをサポートするスケーラビリティに優れた高性能なコンテナ管理サービス
  • Amazon EC2 インスタンスのマネージド型クラスターで簡単にアプリケーションを実行できる
  • 簡単な API 呼び出しで、Docker が有効なアプリケーションを起動および停止したり、クラスターの完全な状態を問い合わせたり、セキュリティグループ、Elastic Load Balancing、Amazon Elastic Block Store (Amazon EBS) ボリューム、AWSIdentity and Access Management (IAM) ロールなどの機能にアクセスすることができる
  • Amazon EC2 Container Registry (ECR)
    • 開発者が Docker コンテナイメージを簡単に保存、管理、デプロイできる完全マネージド型の Docker コンテナレジストリ

コンテナを利用した開発に必要な技術要素

  • アプリのステートレス化
    • ステートが必要なものはコンテナの外に置く
  • レジストリ
    • コンテナの起動元となるイメージの置き場所
      • アプリ+実行環境をpush / 実行時にpull→build→run
    • 高い可用性、スケーラビリティが求められる
      • 落ちたらデプロイ不能、同時に大量にpullされることもある
      • 自前で持つとその管理コストがかかるので、Amazon ECRを利用しましょう
  • コントロールプレーン / データプレーン
    • コントロールプレーン = コンテナの管理をする場所
      • どこでコンテナを動すか / 生死の監視 / いつ停止するか / デプロイ時の配置 → Amazon ECS / Amazon EKS
    • データプレーン = 実際にコンテナが稼働する場所

f:id:jrywm121:20180506123718p:plain

Task: コンテナ(群)の実行単位

  • Task Definitionの情報から起動される
    • Task Definitionとは、jsonフォーマットのテキストファイルで、これは1つ以上(最大10)のコンテナーを記述できる
    • 記述するパラメータは以下のようなものがある
      • family : タスク定義の名前で、この名前をベースにシーケンシャルにリビジョンナンバーが付与される
      • taskRoleArn : タスク定義を登録するときに、IAM タスクロールを割り当てて、タスクのコンテナに、関連するポリシーに指定された AWS API を呼び出すためのアクセス権限を付与できる。
      • executionRoleArn : タスク定義を登録するとき、タスクのコンテナがユーザーに代わってコンテナイメージを取得して、CloudWatch にコンテナログを発行するためのタスク実行ロールを提供することができる
      • networkMode : タスクのコンテナで使用する Docker ネットワーキングモード。有効な値は、none、bridge、awsvpc、および host です。Fargate 起動タイプを使用している場合、awsvpc ネットワークモードが必要。
      • containerDefinitions
        • name : コンテナの名前。Docker Remote API の コンテナを作成する セクションの name と docker run の --name オプションにマッピングされる。
        • image : コンテナの開始に使用するイメージ。新しいタスクが開始されると、Amazon ECS コンテナエージェントは、指定されたイメージおよびタグの最新バージョンをプルしてコンテナで使用する。
        • memory : コンテナに適用されるメモリのハード制限    などなど…
  • Task内のコンテナ群は同じホスト上で実行
  • CPUとメモリの上限を指定し、それをもとにスケジュールする。

Service : ロングランニングアプリ用スケジューラ

  • Taskの数を希望数に保つ
  • Task Definitionを新しくするとBlue/Greenデプロイ
  • ELBと連携することも可能
  • メトリクスに応じてTask数のAuto Scalingも可能

IAM Role: Task毎に異なる権限のAWS認証が可能

※ タスクの単位について
例えば、裏にいるAPIをCallしてWEB UIを提供するもので、expressのアプリケーション のコンテナとnginx(expressにプロキシする) のコンテナで構成されるFront Serviceがある場合には、expressコンテナとnginxコンテナはTaskという単位でくくられる。

  • IAM RoleをTask毎に設定可能
  • AWS SDKを利用していれば自動的に認証情報が得られるため、アクセス鍵等の埋め込み不要

コンテナの数をAuto Scalingさせる

  • 何らかのメトリクス(例えば、コンテナのCPU・メモリ使用率 / リクエスト数)に応じて、コンテナの数を自動スケールさせたい
  • コントロールプレーンの課題 : メトリクスの変化に対して、コンテナ数をどの程度変化させるのか
  • データプレーンの課題 : コンテナのスケールに応じて、インスタンス数もスケールが必要

ECSのTarget TrackingとFargateの組合せがオススメ

  • Target Trackingとの連携

    • メトリクスに対してターゲットの値を設定するだけ(例: CPU使用率 50%)
    • その値に近づく様に、Application AutoScalingが自動的にServiceのDesiredCountを調整
  • Fargateを利用したコンテナAuto Scalingの優位性

    • Serviceのスケールに応じて自然にコンテナが起動・終了する
    • コンテナの起動時間に対してのみ課金

dokkuのソースコードでシェルスクリプトを勉強する

5月からの仕事がアプリケーション側でなくインフラ側になったので、急遽シェルスクリプトの勉強。powershellはちょいちょい扱ってはいたけども、Invoke-RestMethod(Linuxでのcurlコマンド)でjson取ってきて加工するみたいなことしかやってこなかったため知識不足甚だしい。
dokkuというbash200行ちょいでdockerベースのPaaS環境を構築できるプロジェクトがあり、せっかくなのでシェルスクリプトを学びつつPaaS構築のことも勉強できたら良いなと思い、dokkuのbootstrap.shのコードを見つつ勉強していきます。

bashスクリプトの先頭

#!/usr/bin/env bash
set -eo pipefail; [[ $TRACE ]] && set -x

シェルスクリプトの一行目に必ず記述する#!で始まる行はshebangと言われる。
ここで#!/bin/bashと指定すれば、絶対パスでのbash指定となります。

上記のように、#!/usr/bin/env bashと指定すれば、$PATH 上の bash が使われます。$PATHはコマンド検索パスを格納している環境変数printenv PATHで確認することができる。
メリットは、例えば $HOME/.opt 配下に最新の bash をインストールするなどした場合、$PATH にさえ入っていればそっちが使われるというのがあります。ちなみに$PATHに指定するときには、export PATH="$PATH:/opt/local/bin"とする。

set -eを宣言しておくとエラーが起きた行で中断するので、予想外のエラーを無視してスクリプトが処理を続行するのを防げます。ただ、パイプの中で起こったエラーは検知できず、右端のコマンドがエラーとなった時のみ有効です。set -o pipefailを指定することで、パイプ内のコマンドのエラーでも中断します。その2つを組み合わせてエラーを無視せず処理を中断するようにしたのが、set -eo pipefail

&&の前にあるコマンドを実行し、もし正常に終了した場合(戻り値が0)に&&の後ろにあるコマンドを実行する。set -xによってシェルのtraceが有効になる。実行したコマンドとその引数がトレース情報として標準エラー出力へ出力される。
[[ $TRACE ]]は調べたが分からなかった。

変数宣言と関数の書き方

main() {
  export DOKKU_DISTRO DOKKU_DISTRO_VERSION
  # shellcheck disable=SC1091
  DOKKU_DISTRO=$(. /etc/os-release && echo "$ID")
  # shellcheck disable=SC1091
  DOKKU_DISTRO_VERSION=$(. /etc/os-release && echo "$VERSION_ID")

  export DEBIAN_FRONTEND=noninteractive
  export DOKKU_REPO=${DOKKU_REPO:-"https://github.com/dokku/dokku.git"}

  ensure-environment
  install-requirements
  install-dokku
}

main "$@"

関数の書き方はfunction main() { ... }だが、functionは省略可能であり、通常省略されるため上記のソースのようになる。関数を呼び出すときはmain "$@"のように関数名を宣言すれば良い。引数として値を渡す場合には、関数名と並べて書いていく。今回の場合は、このスクリプトが呼び出された時の全ての引数をそのまま渡している($@は全ての引数を意味する)。
export 変数名環境変数の宣言となる。定数に関しては、Java命名規則と同じで定数+アンダースコアになる。 export DEBIAN_FRONTEND=noninteractiveのように環境変数の宣言と代入を同時に行うことができる。

変数の代入は変数=値の形となる。注意点としては、DOKKU_DISTRO = $(. /etc/os-release && echo "$ID")のように=の前後に空白を入れてしまうとうまく機能しません。

${変数値:-デフォルト値という書き方で変数を初期化することができる。export DOKKU_REPO=${DOKKU_REPO:-"https://github.com/dokku/dokku.git"}$DOKKU_REPOが存在していなければ、"https://github.com/dokku/dokku.git"で初期化する。

変数スコープとリダイレクト、終了ステータス

ensure-environment() {
  local FREE_MEMORY
  echo "Preparing to install $DOKKU_TAG from $DOKKU_REPO..."

  hostname -f > /dev/null 2>&1 || {
    echo "This installation script requires that you have a hostname set for the instance. Please set a hostname for 127.0.0.1 in your /etc/hosts"
    exit 1
  }

# 後半の処理は次のセクション

}

local FREE_MEMORYのようにlocalキーワードを変数宣言の前につけることによって、変数のスコープを関数内に狭めることができる。

echo は改行つきで値を表示するための命令。""(ダブルクォーテーション)の中では$変数または${変数}が展開されるが、''(シングルクォーテーション)の中では展開されない。

hostname -fでは、ホスト名とドメイン名からなるFQDNを表示する。
>command > /dev/null 2>&1これは標準エラー出力の結果を標準出力にマージして、/dev/nullに捨てることを意味する。つまり、標準出力された物も標準エラー出力された物も捨てるということ。詳しくは右記を参照。( 5分で一通り理解できる!Linuxのリダイレクト 使い方と種類まとめ

&&は左辺のコマンドが成功していたら右辺のコマンドも実行するものだが、||に関しては、左辺のコマンドが失敗していたら右辺のコマンドが実行される。このスクリプトの場合は、hostnameが設定されておらず、hostname -fのコマンドが評価され失敗したときに、「127.0.0.1ループバックアドレス)にhostnameを設定してください」とメッセージを出力し終了ステータス1をセットしてスクリプトを終了している。

コマンド終了時には「終了ステータス (exit-status)」と呼ばれるコマンドの成否を表す数値が特殊変数 $? に自動で設定される。一般的には、コマンド成功時には「0」、失敗時には「1」が設定される。exit コマンドに指定したパラメータ (0 もしくは 1~255 の正の整数値のみ可) が、そのシェルの終了ステータスとなる( bashの&&と||: みズとおかズ )。

パイプとawk、if文

ensure-environment() {
  
# 前半の処理は上のセクション

  FREE_MEMORY=$(grep MemTotal /proc/meminfo | awk '{print $2}')
  if [[ "$FREE_MEMORY" -lt 1003600 ]]; then
    echo "For dokku to build containers, it is strongly suggested that you have 1024 megabytes or more of free memory"
    echo "If necessary, please consult this document to setup swap: http://dokku.viewdocs.io/dokku/advanced-installation/#vms-with-less-than-1gb-of-memory"
  fi
}

grep 正規表現 ファイル名でファイル内の文字を検索し該当する行を抽出できる。/proc/meminfoは、カーネルが内部的に管理している枠組みでのメモリ情報を持っている。MemTotalカーネルが認識している全物理メモリを表す。
|(パイプ)はコマンドの入出力をコマンドへ引き渡す処理で、今回の場合はgrepで抽出した行MemTotal: 8069288 kBawkコマンドへ引き渡している。
awk 'パターン {アクション}' ファイル名で、テキストファイルを1行ずつ読み、パターンに合致した行に対して、アクションで指定された内容を実行する。パターンが指定されていない場合には、全ての行に対して処理を行う。テキストの各行を空白文字で区切って“フィールド”として処理するので、今回は1番目のフィールドがMemTotal:となり$1で表現できる。2番目のフィールドが8069288となり、これをprint(出力)している。

if文は、testを使用するもの、[(シングルブラケット)を使用するもの、[[(ダブルブラケット)を使用するものに分けられる。testを使用するのは直感的でないし他言語からくると若干困惑する。[(シングルブラケット)は、変数展開するときにダブルクォーテーションが必要であっったり、数値比較を行う場合には丸括弧で囲まなくてはいけないなどハマりポイントが多いため、[[(ダブルブラケット)を使用するべき。 test と [ と [[ コマンドの違い - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

-lt比較演算子はless thanを意味しており、今回ではメモリが1003600kBより小さかった場合にメッセージを出力している。
ifの終了はfiキーワードを使用する。またthenキーワードは1行消費してしまい見にくいので条件文に;(セミコロン)をつけて続けて書いている。

case文

install-requirements() {
  echo "--> Ensuring we have the proper dependencies"

  case "$DOKKU_DISTRO" in
    debian|ubuntu)
      apt-get update -qq > /dev/null
      ;;
  esac
}

case文はcase 値 in 値1) 処理 ;; 値2) 処理 ;; esacと記述していく。この場合は、$DOKKU_DISTROdebianもしくはubuntuだった場合に、apt-get updateでパッケージ情報を更新している。-qqオプションを使用することによってエラー以外は表示しない。
パッケージをinstallする際に参照するインデックスファイルが存在しない可能性があるから?単純にパッケージリストを更新したいから?

dokkuのソースコードを追ってみる

install-dokku() {
  if [[ -n $DOKKU_BRANCH ]]; then
    install-dokku-from-source "origin/$DOKKU_BRANCH"
  elif [[ -n $DOKKU_TAG ]]; then
    local DOKKU_SEMVER="${DOKKU_TAG//v}"
    major=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[1]}')
    minor=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[2]}')
    patch=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[3]}')

    use_plugin=false
    # 0.4.0 implemented a `plugin` plugin
    if [[ "$major" -eq "0" ]] && [[ "$minor" -ge "4" ]] && [[ "$patch" -ge "0" ]]; then
      use_plugin=true
    elif [[ "$major" -ge "1" ]]; then
      use_plugin=true
    fi

-n 文字列で文字列の長さが0より大きければ真となる。
sourceからdokkuのupgradeをする際に、DOKKU_BRANCHをmasterに設定することになっている( Dokku - The smallest PaaS implementation you've ever seen )。 設定されていた場合には、install-dokku-from-source関数が実行される。

通常インストールされる際には、以下のようなコマンドでインストールされる。
sudo DOKKU_TAG=v0.12.5 bash bootstrap.sh
この際にDOKKU_TAGに"v0.12.5"という値が入り条件が真となる。
${変数名//置換前文字列/置換後文字列}で置換前文字列に一致した全ての文字列が、置換される。今回の場合は"v"が排除される形となり、DOKKU_SEMVERには"0.12.5"という値が入る。
awkのsplit関数はsplit(分割する文字列,格納先の配列,分割文字列の指定)というような使い方をする。配列は(0, 12, 5)となりmajorには"0"、minorには"12"、patchには"5"の値が入る。

# install-dokku()の続き

    # 0.3.13 was the first version with a debian package
    if [[ "$major" -eq "0" ]] && [[ "$minor" -eq "3" ]] && [[ "$patch" -ge "13" ]]; then
      install-dokku-from-package "$DOKKU_SEMVER"
      echo "--> Running post-install dependency installation"
      dokku plugins-install-dependencies
    elif [[ "$use_plugin" == "true" ]]; then
      install-dokku-from-package "$DOKKU_SEMVER"
      echo "--> Running post-install dependency installation"
      sudo -E dokku plugin:install-dependencies --core
    else
      install-dokku-from-source "$DOKKU_TAG"
    fi
  else
    install-dokku-from-package
    echo "--> Running post-install dependency installation"
    sudo -E dokku plugin:install-dependencies --core
  fi
}

Effective Java 読書メモ 2章

英語版のEffective Java 第3版が出ていたので、日本語版が出るまでの繋ぎに。 もし読み終わっても出る気配が無かったら英語版買ってなんとか解読するしか。

コンストラクタの代わりにstaticファクトリーメソッドを活用する

コンストラクタが複数で、渡す引数によって動きが異なる場合には実装すべきと思う。メソッドの戻り値の型の任意のサブタイプで返せるが、そのサブタイプのコンストラクタはpublicもしくはprotectedでなくてはならない。

pros

  • コンストラクタと違い名前を持てること
  • メソッドが呼び出される毎に新たなオブジェクトを生成する必要なし
  • メソッドの戻り値の型の任意のサブタイプで返せる
  • パラメータ化された方のインスタンス生成の面倒さを低減できる

cons

  • public/protedctedのコンストラクタを持たないサブタイプは作れない
  • 他のstaticメソッドと区別しにくい
// 利用する側
char[] array = new char[] { 'H', 'A', 'T', 'E', 'N', 'A' };
String str1 = new String(array)  // コンストラクタの場合
String str2 = String.valueOf(array)  // staticメソッドの場合

// 利用される側(jdk8)
public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
}
public static String valueOf(char data[]) {
        return new String(data);
}

数多くのコンストラクタパラメータに直面した時にはビルダーを検討する

個人的には利用側がスマートになって素敵だと思った。パラメータが多くなる場合は、まずクラス設計を見直すべきだと思うが、仕方なくパラメータを増やさざるを得ない場合には良いのでは。

// 利用側のコード
public class Main
{
  // arguments are passed using the text field below this editor
  public static void main(String[] args)
  {
    Contact contact = new Contact.Builder("Satoshi", "Matsuda", LocalDateTime.now()).address("東京都千代田区××町3−24").age(27).build();
  }
}

// 利用される側のコード
class Contact
{
  private final String firstName;
  private final String lastName;
  private final String address;
  private final int age;
  private final LocalDateTime contactDate;
  
  static class Builder {
    
    // 必須パラメータ
    private final String firstName;
    private final String lastName;
    private final LocalDateTime contactDate;
    
    // オプションパラメータ
    private String address = ""; // String.emptyってJavaには無いんだね・・・
    private int age = 0;  
    
    public Builder(String firstName,  String lastName, LocalDateTime contactDate){
      this.firstName = firstName;
      this.lastName = lastName;
      this.contactDate = contactDate;
    }
    
    public Builder address(String val){
        address = val;
        return this;
    }
    
    public Builder age(int val){
        age = val;
        return this;
    }
    
    public Contact build(){
        return new Contact(this); // クラス内部ならprivateでも呼べる
    }
  }
  
  private Contact(Builder builder){
      firstName = builder.firstName;
      lastName = builder.lastName;
      address = builder.address;
      age = builder.age;
      contactDate = builder.contactDate;
  }
}

privateのコンストラクタかenum 型でシングルトン特性を強制する

コンストラクタのアクセス修飾子をprivateにすると初期化の時に1回だけ呼ばれる

singleton実装①
public finalによるシングルトン。単純で分かりやすい。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton obj1 = Singleton.INSTANCE;
    }
}

class Singleton {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton(){ /* process */ }
}

singleton実装②
staticファクトリーメソッドによるシングルトン。結城先生のデザインパターン本にも載っている方法がこれ。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton obj1 = Singleton.getInstance(); // この時点でSingletonクラスの初期化が行われ、staticフィールドも初期化される
        Singleton obj2 = Singleton.getInstance();
        // Singleton obj3 = Singleton(); エラーが発生する
        
        System.out.println(obj1 == obj2); // output : true
    }
}

class Singleton {
    private static final Singleton SINGLETON = new Singleton();
    private Singleton(){ /* process */ }
    
    public static Singleton getInstance(){
        return SINGLETON;
    }
}

singleton実装③
上記の2つの方法だと、シングルトンのクラスをシリアライズするときには、"implements Serializable"を追加するだけではダメとのこと。全てのインスタンスフィールドをtransientと宣言し、readResolveメソッドを実装する必要がある。そうしない場合に、シリアライズされたインスタンスをディシリアライズするたびに、新たなインスタンスが生成されてしまうため、シングルトンを保証できない。transient修飾子は、シリアライズの対象から外すもの。
また、AccessibleObject.setAccesibleメソッドを利用してリフレクションにより、privateのコンストラクタを呼び出すことができてしまうため、シングルトンを保証できない。
Enumを使うとシリアライズするためにアレコレしなくてよくてスマート。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton.INSTANCE.saySingleton();  // singleton!!
    }
}

enum Singleton {
    INSTANCE;
    public void saySingleton(){
        System.out.println("singleton!!");
    }
}

privateのコンストラクタでインスタンス化不可能を強制する

コンパイラはデフォルトでpublicで引数なしのコンストラクタを提供しているが、明示的にprivateのコンストラクタを用意することでインスタンス化できないようにすることが可能。

不必要なオブジェクトの生成を避ける

staticファクトリーメソッドとコンストラクタの両方を提供している不変クラスの場合は、staticファクトリーメソッドを使用した方が、不必要なオブジェクトの生成を防げる。可変オブジェクトに関しても、メンバのインスタンス生成が一度でいい場合には、static初期化子を使用することを検討すること。
オートボクシング(auto boxing: プリミティブ型→ラッパー型)はパフォーマンスを悪くするので、注意すること。意図した上で変換するのであれば、明示的に行うこと。

廃れたオブジェクト参照を取り除く

クラスが独自にメモリを管理している場合には、例えば下記のようなEffecive JavaにあるようなStackクラスなどに関しては、メモリリークに注意する必要がある。

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack () {        
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push (Object e) {
        ensureCapacity();
        elements[size++] = e; // 参照先をセット
    }

    public Object pop () {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return elements[size--]; 
  // 参照値を渡してスタックには残って無いように見えるが、実際はまだ参照値を持っている
  // 参照値を持っているが、もう参照されることは無い(=廃れた参照)
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

以下のように明示的にnull参照を突っ込むことによって、Garbage Collectionが動いてくれる。

public Object pop () {
    if (size == 0) {
        throw new EmptyStackException();
    }

    Object result = elements[size--];
    elements[size] = null; // ここ!!
    return result;
}

クラスがメモリを独自管理している場合以外にも、キャッシュやリスナー・コールバックでメモリリークが起きる可能性は高くなる。ヒープの使用量とかデバッグしているときに意識できるといいよね的な感じだろうか。
HashMapを使用している場合には、WeakHashMap(WeakHashMap (Java Platform SE 8))で代替すると、使われることがなくなったキーが自動的に削除されてガベージコレクションの対象となります。

ファイナライザを避ける

C++プログラマJavaに移ってきたときに誤解しがちなデストラクタ=ファイナライザに対して「そんなことないぞ!!」と。try-finallyを使用して(DBとかファイルはtry-resourcesでしたっけ?)、明示的に終了メソッドを組み込めとのこと。安定してファイナライザは動作をしてくれない。