Data Eng (なりたい)

平日の学びを週末にアウトプットしたい

Firebase hosting + Cloud Run の機能を試していくよ!!

Firebase hosting + Cloud Run で Microservices のL7ロードバランシングができるみたいだったので試してみました!!

勢いでMicroserviceのサンプルAPIを作る

goa とかでやるとパッとできるよ(趣味)

goa.design

本編

ツールインストール

$ npm install -g firebase-tools
$ firebase login

Hosting

Cloud Run を使用した動的コンテンツの配信とマイクロサービスのホスティング  |  Firebase

以下を参考にリライトする Hosting 動作を構成する  |  Firebase

firebase.json”rewrites” を以下のように記述するとロードバランシングできます。

最低限のファイル

{
  "hosting": {
    "public": "",
    "rewrites": [
      {
        "source": "/get-hello**",
        "run": {
          "serviceId": "goa-microservice-sample",
          "region": "us-central1"
        }
      }
    ]
  }
}
$ firebase deploy

# hosting のみ選択

以下のように /get-hello へアクセスすると goa-microservice-sample/get-hello パスが呼び出される。

curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" "https://<FIREBASE_DOMAIN>/get-hello?name=hoge"

ちなみに httpstat で計測してみると…

$ httpstat "https://<FIREBASE_DOMAIN>/get-hello?name=hoge"  -H "Authorization: Bearer $(gcloud auth print-identity-token)"

...

 DNS Lookup   TCP Connection   TLS Handshake   Server Processing   Content Transfer
[     5ms    |       5ms      |     20ms      |        6ms        |        1ms       ]
             |                |               |                   |                  |
    namelookup:5ms            |               |                   |                  |
                        connect:10ms          |                   |                  |
                                    pretransfer:30ms              |                  |
                                                      starttransfer:36ms             |
                                                                                 total:37ms

キャッシュにヒットした際のレスポンスのサンプルなのでパフォーマンスの参考にはしないでください。

キャッシュの動作は確認していないので以下のドキュメンとを読む。

キャッシュ動作の管理  |  Firebase HTTP キャッシュ  |  Web Fundamentals  |  Google Developers

まとめ

Firebase Hosting + Cloud Run で動的なコンテンツをホスティングできることが確認できた。 Firebase Hosting を利用することで 自動的に CDN が用いられる。(デフォルトでは動的コンテンツには適用されていないとドキュメントには記載されていたが、キャッシュされていた…) キャッシュや認証の仕様を確認していないので引き続き調査していきたい。

goaを使ってAPIサーバをサクッと作る

概要

目的

JSONを返すREST APIサーバをサクッと作りたいという要求がちょくちょくあります。

そのたびにサーバの骨組みを作ってリクエストのバリデーションをして…みたいな作業をするのに嫌気がさしたのでJSON APIのクッキーカッターを作ろうと思い立ちました。

目的は上記のとおりなので「こっちのほうが楽なんだがw」といったものがあれば是非教えて下さい!!

なぜgoaを選んだか

まずgoaとはなにかなんですが以下に公式Docを引用します ref. https://github.com/goadesign/goa

Goa takes a different approach to building services by making it possible to describe thedesignof the service API using a simple Go DSL. Goa uses the description to generate specialized service helper code, client code and documentation.

以下が主な選定理由です。

  • golangベースのDSLで書きやすい(※個人の感想です)
  • DSLによってサーバの枠組みが生成される
    • リクエストのvalidationもしてくれる(少し癖があるが)
    • リクエストの構造体を受け取って処理するロジックのみを書くだけ
  • DSLからSwaggerが生成される
  • クライアント用のコードも生成される(後述)

Swaggerが生成されるのが結構いいなと思っています。(普通は逆かもしれませんが) Swaggerを起点にしてJMeterなどの負荷試験ツールのコードも生成したいという野望があります。

Prerequisites

Install goa

goaコマンドをインストールします。 グローバルを汚したくないという方は適宜moduleを使ってください

$ go get -u goa.design/goa/v3/cmd/goa

作っていく

Design file を書く

プロジェクトを作り、moduleを初期化します。

$ mkdir -p my-project/design
$ cd my-project/
$ export GO111MODULE=on
$ go mod init

# edit design/design.go

後半でDockerも使うのでDocker環境も適宜用意してください。

Design file というインタフェースを定義するファイルを記述します。 今回はリクエストをそのまま返すechoサーバを実装します。 パラメータは

  • name: URLのパスとして受け取る (required)
  • age: クエリストリングで受け取る (required)

です。 POSTでリクエストボディを受け取ったり、デフォルト値を設定したり、フォーマットを指定したりなど便利な機能はたくさんありますが今回は全体の流れを説明するために割愛します。(今後書いていきたい)

DSLの文法自体も詳しくは解説しませんがなんとなく雰囲気で読めると思います。(押し付け)

詳しくは公式のリファレンスを参照してください。v2v3でかなり書き方が違うのでどちらのバージョンかをしっかり確認してください。 dsl - GoDoc

意外とサンプルが転がっていないので(特に日本語サイトはない)公式のExampleも参照するといいと思います。 GitHub - goadesign/examples: Examples for goa showing specific capabilities

design/desing.go

package design

import . "goa.design/goa/v3/dsl"

// API describes the global properties of the API server.
var _ = API("echo", func() {
    Title("Echo Service")
    Description("This is HTTP echo service")
    Server("echo-server", func() {
        Host("localhost", func() { URI("http://0.0.0.0:8088") })
    })
})

// Service describes a service
var _ = Service("echo-service", func() {
    Description("Echo your request")
    // Method describes a service method (endpoint)
    Method("echo-get", func() {
        // define request payload
        Payload(func() {
            // Attribute describes an object field
            Attribute("name", String, "Your name")
            Attribute("age", Int, "Your age")
            // Both attributes must be provided when invoking "add"
            Required("name", "age")
        })
        // define response data type
        Result(String)
        // HTTP describes the HTTP transport mapping
        HTTP(func() {
            // Requests to the service consist of HTTP GET requests
            GET("/name/{name}")
            Param("age")
            // Responses use a "200 OK" HTTP status
            // The result is encoded in the response body
            Response(StatusOK)
        })
    })
})

コードを生成する

Design file をもとにサーバの枠となるファイルを生成します。

# goa gen <MODULE_NAME>
$ goa gen github.com/kaito2/my-project/design
gen/echo_service/client.go
gen/echo_service/endpoints.go
gen/echo_service/service.go
gen/http/cli/echo_server/cli.go
gen/http/echo_service/client/cli.go
gen/http/echo_service/client/client.go
gen/http/echo_service/client/encode_decode.go
gen/http/echo_service/client/paths.go
gen/http/echo_service/client/types.go
gen/http/echo_service/server/encode_decode.go
gen/http/echo_service/server/paths.go
gen/http/echo_service/server/server.go
gen/http/echo_service/server/types.go
gen/http/openapi.json
gen/http/openapi.yaml

# goa example <MODULE_NAME>
$ goa example github.com/kaito2/my-project/design
cmd/echo_server-cli/http.go
cmd/echo_server-cli/main.go
cmd/echo_server/http.go
cmd/echo_server/main.go
echo_service.go

$ tree .
.
├── cmd
│   ├── echo_server
│   │   ├── http.go
│   │   └── main.go
│   └── echo_server-cli
│       ├── http.go
│       └── main.go
├── design
│   └── design.go
├── echo_service.go
├── gen
│   ├── echo_service
│   │   ├── client.go
│   │   ├── endpoints.go
│   │   └── service.go
│   └── http
│       ├── cli
│       │   └── echo_server
│       │       └── cli.go
│       ├── echo_service
│       │   ├── client
│       │   │   ├── cli.go
│       │   │   ├── client.go
│       │   │   ├── encode_decode.go
│       │   │   ├── paths.go
│       │   │   └── types.go
│       │   └── server
│       │       ├── encode_decode.go
│       │       ├── paths.go
│       │       ├── server.go
│       │       └── types.go
│       ├── openapi.json
│       └── openapi.yaml
├── go.mod
└── go.sum

ロジックを書く

リクエストを処理するために編集するファイルはプロジェクトルートディレクトリにある echo_server.go のみです。

echo_server.go

package echo

import (
    "context"
    "log"

    echoservice "github.com/kaito2/my-project/gen/echo_service"
)

// echo-service service example implementation.
// The example methods log the requests and return zero values.
type echoServicesrvc struct {
    logger *log.Logger
}

// NewEchoService returns the echo-service service implementation.
func NewEchoService(logger *log.Logger) echoservice.Service {
    return &echoServicesrvc{logger}
}

// EchoGet implements echo-get.
func (s *echoServicesrvc) EchoGet(ctx context.Context, p *echoservice.EchoGetPayload) (res string, err error) {
    s.logger.Print("echoService.echo-get")
    return
}

gRPCでスタブを生成した事がある人は見慣れたコードかもしれません。 このEchoGet()関数にロジックを記述します。 以下のように記述することでリクエストのパラメータを取得できます。

...
// EchoGet implements echo-get.
func (s *echoServicesrvc) EchoGet(ctx context.Context, p *echoservice.EchoGetPayload) (res string, err error) {
    s.logger.Println("echoService.echo-get")
    s.logger.Printf("Request name: %s", p.Name)
    s.logger.Printf("Request age : %d", p.Age)
    return fmt.Sprintf("Your name: %s, Your age: %d", p.Name, p.Age), nil
}
...

実行する

実際にサーバを起動してみます。

$ go build ./cmd/echo_server
$ ./echo_server
[echo] 21:19:42 HTTP "EchoGet" mounted on GET /add/{name}
[echo] 21:19:42 HTTP server listening on "localhost:8088"

別のターミナルからアクセスすると…

$ curl "http://localhost:8088/name/kaito2?age=13"
"Your name: kaito2, Your age: 13"

無事動作しました。

ちなみに Requiredに指定したパラメータを記述しないと400が返ります。

$ curl "http://localhost:8088/add/kaito2"
{"name":"missing_field","id":"XXXXXXX","message":"\"age\" is missing from query string; invalid value \"\" for \"age\", must be a integer","temporary":false,"timeout":false,"fault":false}

上で書いたクライアントも生成されているので利用します。

$ go build ./cmd/echo_server-cli/
$ ./echo_server-cli --help
./echo_server-cli is a command line client for the echo API.

Usage:
    ./echo_server-cli [-host HOST][-url URL][-timeout SECONDS][-verbose|-v] SERVICE ENDPOINT [flags]

    -host HOST:  server host (localhost). valid values: localhost
    -url URL:    specify service URL overriding host URL (http://localhost:8080)
    -timeout:    maximum number of seconds to wait for response (30)
    -verbose|-v: print request and response details (false)

Commands:
    echo-service echo-get

Additional help:
    ./echo_server-cli SERVICE [ENDPOINT] --help

Example:
    ./echo_server-cli echo-service echo-get --name "Quam dolores." --age 8576636692946696796

$ ./echo_server-cli echo-service echo-get --name "Quam dolores." --age 8576636692946696796
"Your name: Quam dolores., Your age: 8576636692946696796"

デフォルトでsampleの値が用意されているので変な値になっていますが(笑) クライントで動作確認もできます(curlでいい気もするが)

Dockerizeする

せっかく(?)なのでDockerizeしましょう。

Dockerfile

# Dockerfile References: https://docs.docker.com/engine/reference/builder/
FROM golang:1.11.12 as builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/echo_server

FROM alpine:3.10.1
WORKDIR /app
EXPOSE 8080
COPY --from=builder /app/main .
CMD ["./main"]

プロダクションで alpine ってどうなんだろう…結構使われてるのかな…

実行してみます。

$ docker login
$ docker build . -t my-project-image
$ docker run --rm -p 8088:8088 my-project-image
[echo] 12:48:25 HTTP "EchoGet" mounted on GET /name/{name}
[echo] 12:48:25 HTTP server listening on "0.0.0.0:8088"

いい感じに実行できました!!

まとめ

  • goaでAPIサーバをつくる
    • Design file を記述
    • Design file をもとにコード生成
    • リクエスト処理のロジックを記述
  • Dockerで実行

という流れで簡単にサーバができました!! これをCIでビルドしてCloud Runにデプロイできるようにして…と夢が広がります!!

DSLに関しては少し癖があり、色々動作検証をしてみたのでまた記事にしたいと思います。

dockerコマンド無しでコンテナを動かしてみる

以下のブログを参考にコンテナ周りの技術についての学習も兼ねてDockerコマンドなしでコンテナを動かす。

ref. Container Runtimes Part 2: Anatomy of a Low-Level Container Runtime - Ian Lewis

Dockerコマンドを使わずにコンテナを走らせる。(ファイルシステムの用意では使う)

(後始末が面倒なのでVirtualboxクラウドサービスのVMなどを推奨)

0. 環境

  • Google Compute Engine
    • Machine Type: f1-micro
    • OS: Ubuntu 18.04 LTS
  • Cloud Shellからアクセス

1. ファイルシステムのセットアップ

まずはファイルシステムをセットアップする。

(早速Dockerコマンドという感じですが、ファイルの雛形を用意するためでコンテナの実行には使わないのでご容赦ください。)

今回はbusyboxのDockerイメージからファイルとディレクトリを抽出する。

$ sudo su

# (1)
$ CID=$(docker create busybox)
# (2)
$ ROOTFS=$(mktemp -d)
# (3)
$ docker export $CID | tar -xf - -C $ROOTFS

(1) 補足

ref. http://docs.docker.jp/engine/reference/commandline/create.html create — Docker-docs-ja 17.06.Beta ドキュメント

  • docker createは書込み可能なコンテナレイヤを作成する。
    • つまり「コンテナはは作成するが runはしない」状態
  • 通常のdocker container lsでは作成したコンテナは作成されないが、-aオプションで出力できる。
  • コンテナIDを標準出力に表示する。

Example

$ docker create busybox
64aad11d274cec50b6e03f4422d50e01bfec2d894692934cd6965afd693a7c49
$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
64aad11d274c        busybox             "sh"                8 seconds ago       Created                                 silly_swartz

(2) 補足

ref. mktemp(1): create temporary file/directory - Linux man page

  • 一時ファイルまたはディレクトリを作成するコマンド
  • -dオプションなのでディレクトリが作成される。
  • 引数でXを3個以上渡すとその文字数分ランダム文字列のファイルまたはディレクトリが作成される。
  • 指定しない場合は/tmp/tmp.XXXXXXXXXXになる。

Example

$ mktemp
/tmp/tmp.ZTZHxN7V7B
$ mktemp -d
/tmp/tmp.Svu9ewej2o
$ mktemp XXXX
t5rk

(3) 補足

ref. Dockerイメージのexportとimport( save, load ) - Qiita

  • docker export <CONTAINER_ID>はコンテナをtarファイルにする。
  • tar -xf - -C $ROOTFSにリダイレクトしてtarファイルを展開
    • -x: eXtract(展開)
    • -f: fileから展開(デフォルトはテープデバイスらしい)
    • -C  <DIR>: <DIR>に移動してから実行

2. コントロールグループを作成

$ apt install cgroup-tools

# (4)
$ UUID=$(uuidgen)
# (5)
$ cgcreate -g cpu,memory:$UUID
# (6)
$ cgset -r memory.limit_in_bytes=100000000 $UUID
$ cgset -r cpu.shares=512 $UUID
# (7)
$ cgset -r cpu.cfs_period_us=1000000 $UUID
$ cgset -r cpu.cfs_quota_us=2000000 $UUID

(4) 補足

ref. uuidgen(1) - Linux manual page

(5) 補足

refs.

そもそも cgroup とは

ref. 第1章 コントロールグループについて (cgroup) - Red Hat Customer Portal

cgroup(control group)を用いることでCPU時間、システムメモリ、ネットワーク帯域などのリソースをシステムで実行中のユーザ定義タスクグループ(プロセス)に割り当てることができる。

コマンドについて

cgcreateでコントロールグループ作成後、以下のコマンドで確認できる。

$ lscgroup | grep $UUID
cpu,cpuacct:/*86aec293-c8c7-47f4-8158-817d978570ea*
memory:/*86aec293-c8c7-47f4-8158-817d978570ea*

(6) 補足

ref. cgroupを使ってCPUとメモリの割り当てを制限する - 偏った言語信者の垂れ流し

(5)で作成したコントロールグループの制限を設定する。

以下のコマンドで確認できる。

# cgset で設定した値になっている
$ cat /sys/fs/cgroup/cpu/$UUID/cpu.shares 
512
$ cat /sys/fs/cgroup/memory/$UUID/memory.limit_in_bytes 
99999744
  • Ubuntuでは/sys/fs/cgroup以下にcgroupの情報が格納される
  • cpu.sharesはスケジューリングの優先度の値
  • limit_in_bytesはそのままメモリのバイト単位の制限

(7) 補足

ref. リソース管理ガイド - Red Hat Customer Portal

  • cpu.cfs_quota_us
    • cgroupによるCPUリソースの再割り当てが行われる間隔(μs だが、ここではusと表記されている)
    • 1,000 < cpu.cfs_quota_us < 1,000,000
  • cpu.cfs_period_us
    • cgroup内のすべてのタスクがcpu.cfs_quota_us間に利用できるCPU時間(μs)
    • 特定のタスクが使い切った場合はcgroup内の他のタスクはスロットリングされ、次の期間まで実行ができない
    • Ex) cgroupが1秒あたり0.2秒間単一ののCPUにアクセスするためには以下のように設定する
      • cpu.cfs_period_us = 1,000,000
      • cpu.cfs_period_us = 200,000

CFS (Completely Fair Scheduler)とは

ref. Linux カーネル 2.6 Completely Fair Scheduler の内側

CFS の背後にある主な概念は、タスクに与えるプロセッサー時間のバランス (公平性) を維持するためのものです。つまり、それぞれのプロセスには公平にプロセッサー時間が与えられるようにしなければなりません。


3. コンテナ内でコマンドを実行

1. 2.で作成したファイルシステムとコントロールグループでコンテナを作成し、その中でコマンドを実行する。

# (8)
$ cgexec -g cpu,memory:$UUID \
    unshare -uinpUrf --mount-proc \
    sh -c "/bin/hostname $UUID && chroot $ROOTFS /bin/sh"
# (9)
/ # echo "Hello from in a container"
Hello from in a container
/ # touch hoge.txt
/ # exit

(8) 補足

実行後にマウントしていた$ROOTFS以下を確認するとhoge.txtがある。

$ ls $ROOTFS
bin  dev  etc  hoge.txt  home  proc  root  sys  tmp  usr  var

(一応)クリーンアップ

作成したファイルシステムとコントロールグループを削除する

$ cgdelete -r -g cpu,memory:$UUID
$ rm -r $ROOTFS

DNS再入門

ScrapBoxのメモを整形して貼ります。 なので僕が理解が薄い部分を調べたりメモしたり引用したりしているだけなので体系的な知識は得られないかと思います。

本編

Google Cloud DNS を使って独自ドメインを取得ししつつDNSについて学んでいきます。

Google Cloud DNSでIPアドレスとドメイン名を紐付ける - Qiita まずはコレにしたがってやっていく

Cloud DNS 登録

事前にしておく

お名前.com 等でドメインを取得
UIとかいろいろ変わっていくと思うので公式サイトを参照されたし
(お名前ドットコム)

DNSゾーン

DNSゾーンとは

あんまり詳しく知らなかった。

Image

ref. DNSサーバとゾーン
一つのネームサーバが管理する範囲をDNSゾーンっていうみたい
サブドメインを移譲したらそのサブドメインは別のDNSゾーンになる みたいな?

Image
ref. @IT > Master of IP Network > DNS Tips > ゾーンとは

ゾーンとは、ネームサーバがドメインを管理する範囲です

DNSゾーン作成

ネットワークサービス > Cloud DNS に移動
DNSゾーンの作成」> 入力 > 作成

Image

NSレコードとSOAレコードがデフォルトで作成されています

ぼく:「NSレコード? SOAレコード??」

NSレコード
ざっくりいうと

管理を委託しているDNSサーバさんの名前が書いてあるんだな~
らしい

ref. NSレコードとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

それっぽく書いてあるスライド: https://jprs.jp/tech/material/iw2012-lunch-L3-01.pdf

SOAレコード
Start Of Authority recordの略らしい
権威の開始を示す
ref. SOAレコードとは何ですか。|よくあるご質問|法人向けクラウドサービスのbit-drive

ゾーン(管理する範囲)に関する情報が書いてあるんだな~
ref. SOAレコードとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

SOAレコードの1番目に定義されるns1.example.com.は、そのドメインのプライマリDNSサーバを指定します。
通常、そのゾーンのNSレコードで定義されたホスト名のいずれか1台と一致するかと思います。
ref. DNS - SOAレコードの最初の部分の意味がわかりません。|teratail

Aレコードの追加

AレコードはIPアドレスドメイン名を対応させるレコードです。
これはわかりやすい
www.example.com => 123.123.123.123 みたいな。

レジストラの登録

Cloud DNS で登録したドメインDNSサーバを参照されるように上のレジストラのネームサーバに登録する必要がある

ぼく: 「レジストラ(リ)って何??」
レジストリ

レジストリとは、世界中で使われている「.com」や「.net」「.jp」などのトップレベルドメイン毎に1つのみ存在する一番上位の機関をいいます。
レジストラ
レジストラドメインを登録する事業者をいいます。
ref. レジストリとレジストラ|ドメインの基礎知識|名づけてねっと

  • data
  • ns-cloud-d1.googledomains.com.
  • ns-cloud-d2.googledomains.com.
  • ns-cloud-d3.googledomains.com.
    • ns-cloud-d4.googledomains.com.

これらのns サーバーをお名前.com等のレジストラに登録する。
ネームサーバーの変更>他のネームサーバーを利用で更新可能

移譲完了

GAEのレイテンシがつらいという話

TL;DR

GAEは…

  • (Google App Engine)のレイテンシが思っていたより大きい
  • Keepaliveヘッダーを無視する
  • API サーバーとして利用するにはあまり向いていなさそう(感想)

経緯

既存のシステムの一部をAPIとして切り出そうという作業をしていました。 そこで目をつけたのがGCP(Google Cloud Platform)のGAE(Google App Engine)です。(楽だし) 他のシステムから呼ばれることもあってレスポンスタイムの要求がかなりシビアなので計測しておくことにしました。 計測対象のアプリ自体はただの echo server です。

環境

region: asia-northeast1
environment: standard
language: go

計測

abコマンドを用いて計測しました。

$ ab -c 1 -n 100 <URL>
...
Concurrency Level:      1
Time taken for tests:   25.657 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      31412 bytes
HTML transferred:       4100 bytes
Requests per second:    3.90 [#/sec] (mean)
Time per request:       256.571 [ms] (mean)
Time per request:       256.571 [ms] (mean, across all concurrent requests)
Transfer rate:          1.20 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      163  198  16.5    198     250
Processing:    49   58  11.5     54     150
Waiting:       47   58  11.6     53     150
Total:        219  256  21.0    257     360

Percentage of the requests served within a certain time (ms)
  50%    257
  66%    262
  75%    270
  80%    271
  90%    281
  95%    289
  98%    311
  99%    360
 100%    360 (longest request)

300ms 程度のレスポンスタイムとなりました。

さらに httpstatコマンドを使って内訳を見てみると。

$ httpstat <URL>
...
  DNS Lookup   TCP Connection   TLS Handshake   Server Processing   Content Transfer
[     4ms    |      61ms      |     148ms     |       49ms        |        0ms       ]
             |                |               |                   |                  |
    namelookup:4ms            |               |                   |                  |
                        connect:65ms          |                   |                  |
                                    pretransfer:213ms             |                  |
                                                      starttransfer:262ms            |
                                                                                 total:262ms

TLS Handshakeがかなりの時間を占めていました。

他のシステムからの利用であることもありぱっと思いつくのは、Keep-Alive なので試してみます。

$ ab -c 1 -n 100 -k <URL>
...
Concurrency Level:      1
Time taken for tests:   6.330 seconds
Complete requests:      100
Failed requests:        0
Keep-Alive requests:    100
Total transferred:      33804 bytes
HTML transferred:       4100 bytes
Requests per second:    15.80 [#/sec] (mean)
Time per request:       63.302 [ms] (mean)
Time per request:       63.302 [ms] (mean, across all concurrent requests)
Transfer rate:          5.21 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2  20.0      0     200
Processing:    51   61   5.2     60      81
Waiting:       51   61   5.2     60      81
Total:         51   63  20.5     60     260

Percentage of the requests served within a certain time (ms)
  50%     60
  66%     61
  75%     63
  80%     64
  90%     68
  95%     76
  98%     81
  99%    260
 100%    260 (longest request)

いい感じ? httpstat で確認してみると

$ httpstat -k <URL>
...
  DNS Lookup   TCP Connection   Server Processing   Content Transfer
[     4ms    |      57ms      |       60ms        |        0ms       ]
             |                |                   |                  |
    namelookup:4ms            |                   |                  |
                        connect:61ms              |                  |
                                      starttransfer:276ms            |
                                                                 total:276ms

なんか数値が壊れている…

本当に Keep-Alive が効いているのか確認してみます。

$ curl -s -I -k <URL>
HTTP/2 400
content-type: text/plain; charset=utf-8
x-cloud-trace-context: XXXXXXXXXXXXXX
content-length: 30
date: Sun, 19 May 2019 13:54:14 GMT
server: Google Frontend
alt-svc: quic=":443"; ma=2592000; v="46,44,43,39"

Connection: keep-aliveがない…

ぐぐってみると…

The following headers are removed from the request: ... Keep-Alive ...

ref. Request Headers and Responses

そういう話か… 検証はしてないですが、ab , httpstatコマンドは-kオプションを付けると 「Keep-Alive」が効いている前提で計測しているだけで実際にKeep-Aliveが効いているかの確認はしていないみたいです。 ちなみに Apache JmeterではしっかりコネクションタイムがKeep-Aliveを有効にしても計測されていました。

コンテナの思想的にもKeep-Aliveを無視するというのはあっている気がするが、これに気づくのにかなり時間を使ってしまったので頭の片隅においておきたいです。

レイテンシが99%ileで360になるのはつらいと思うんだが、みんなどうやって解決しているのか気になる…

おまけ

計測の中でGCEを立てて httpstat で計測をした結果があったので載せます。

  DNS Lookup   TCP Connection   TLS Handshake   Server Processing   Content Transfer
[     4ms    |       1ms      |     10ms      |        5ms        |        0ms       ]
             |                |               |                   |                  |
    namelookup:4ms            |               |                   |                  |
                        connect:5ms           |                   |                  |
                                    pretransfer:15ms              |                  |
                                                      starttransfer:20ms             |
                                                                                 total:20ms

当たり前のことなんですが内部ネットワークが爆速で一気にすべてGCPに移行したい気持ちになりました。(そうはいかない。)

このブログは

2019年4月に東京のWeb企業にデータエンジニアとして新卒入社しました。

複雑な経緯は特にありませんが情報系の修士1年が終了したタイミングで大学院を中退しての入社です。(自戒のためにもこれも言語化するか…)

 

業務で用いている技術スタックは 言語ならGo, PythonがメインでGCP贔屓です。

特に技術選定の思想は強くないので(エディタはPycharm)食べず嫌いせずいろいろ触れていきたいです。

 

平日の業務の中で疑問、検証したいこと、試したい技術、知見などはスタックしているので言語化のためにブログにまとめようと思います。日記みたいなものも書きます。