Goでgraceful shutdownをミニマムに書く

graceful shutdownの仕組みがわかってなかったので自分でミニマムに実装してみる。

コード量が小さく読みやすかったので以下のパッケージを参考にした。

github.com

github.com

実装

自分でミニマムに実装したコードは以下の通り。アクティブな接続が返ってこなかった場合とかも考えなくてはいけないが、ここでは理解のため手元で動かして簡単に試す程度の実装をしている。

package main

import (
    "log"
    "net"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(10 * time.Second)
    w.Write([]byte("Hello, middleware!\n"))
}

func main() {
    http.HandleFunc("/", indexHandler)

    srv := &http.Server{Addr: ":8888", Handler: http.DefaultServeMux}

    var wg sync.WaitGroup
    srv.ConnState = func(conn net.Conn, state http.ConnState) {
        switch state {
        case http.StateActive:
            log.Println("StateActive!!!")
            wg.Add(1)
        case http.StateIdle:
            log.Println("StateIdle!!!")
            wg.Done()
        }
    }
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        log.Fatal(err)
    }
    waitSignal(l)
    err = srv.Serve(l)
    wg.Wait()
    if err != nil {
        log.Fatal(err)
    }
}

func waitSignal(l net.Listener) {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT)
    go func() {
        sig := <-c
        switch sig {
        case syscall.SIGINT:
            signal.Stop(c)
            l.Close()
        }
    }()
}

動作確認すると、

// server
$ go run test.go // サーバ起動

// client
$ curl localhost:8888 // curl実行

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!! // 特に何もしない

// client
$ curl localhost:8888
Hello, middleware! // 正常にレスポンスが返る

// client
$ curl localhost:8888
Hello, middleware!
$ curl localhost:8888 // 再度curl実行

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!!
2017/12/20 11:11:59 StateActive!!!
^C // Ctrl+CでSIGINTを送る

// client
$ curl localhost:8888
Hello, middleware!
$ curl localhost:8888
Hello, middleware! // 正常にレスポンスが返る

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!!
2017/12/20 11:11:59 StateActive!!!
^C2017/12/20 11:12:09 StateIdle!!! // 既に確立されたコネクションは正常にレスポンスを返している
2017/12/20 11:12:09 accept tcp [::]:8888: use of closed network connection
exit status 1 // 確立されたコネクションが全てレスポンスを返してからサーバシャットダウンしている

感想

ポイントは

  • プロセスを終了するシグナルをsignal.Notifyで捕まえて処理を変更すること
  • http.StateActiveな接続が存在する場合はプロセスがすぐに終了しないようにすること

だろうか。最初はリスナーをクローズすると確立されたConnも終了してしまうのかと思っていたが、ここに関してはTCPListener.Closeがうまい感じにやってくれている。このためリスナーをクローズしたタイミングで新規の接続は受け付けないといったことも実現できている。

https://golang.org/pkg/net/#TCPListener.Close

Close stops listening on the TCP address. Already Accepted connections are not closed.

Goアプリケーションをsupervisorでdaemonとして起動する

起動したGoのアプリケーションをdaemonプロセスとして動かす。なお、Centos6.7を使用している。

Supervisor: A Process Control System — Supervisor 3.3.3 documentation

Install

$ wget https://bootstrap.pypa.io/ez_setup.py -O - | python
$ easy_install supervisor
$ supervisord -v
3.3.3
  • yumでインストールすると古いバージョン(2.x.x系)がインストールされる。古いバージョンだとsupervisorctlコマンドでstartstopを実行した時にコマンドが返ってこないので注意

Configuration

設定ファイル(/etc/supervisord.conf)を作成する

設定は以下を参考にした。設定例は補足に記載。

SupervisorでGolangで書かれたアプリケーションのデーモン化をした話 - こんちゃんブログ

詳細は以下を参照

http://supervisord.org/configuration.html

How to use

各コマンドの詳細は以下を参照

http://supervisord.org/running.html

日本語だとこちらが詳しい

supervisorctlについて調べてみた - Masteries

supervisord

supervisorを起動するためのコマンド。以下のコマンドでsupervisorを起動する。なお、設定ファイルでautostart=trueと設定しているので、sueprvisordを起動するとGoアプリケーションも自動で立ち上がっている。

$ supervisord -c /etc/supervisord.conf

supervisorctl

supervisorでdaemonとして動かしたいアプリケーションの起動・停止するための主なコマンド

  • supervisorctl start <name>
  • supervisorctl stop <name>
  • supervisorctl restart <name>
    • supervisorの設定ファイルを再読込せずにアプリを再起動
  • supervisorctl update
    • supervisorの設定ファイルを再読込してアプリを再起動
  • supervisorctl status
$ supervisorctl start goapp
goapp: started
$ supervisorctl status
goapp           RUNNING   pid 2675, uptime 0:00:02

$ supervisorctl stop goapp
goapp: stopped
$ supervisorctl status
goapp           STOPPED   Dec 15 08:12 AM

$ supervisorctl restart goapp
goapp: stopped
goapp: started

$ ps aux | grep goapp
root      2681  0.0  0.2  46592  5416 ?        Sl   08:14   0:00 /go/bin/goapp

個人の感想

公式のドキュメントがしっかりしてて助かる

補足

設定ファイルの例

[unix_http_server]
file = /var/tmp/supervisor.sock
chmod = 0777
chown= root:root

[supervisord]
http_port=/var/tmp/supervisor.sock ; (default is to run a UNIX domain socket server)
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB       ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10          ; (num of main logfile rotation backups;default 10)
loglevel=info               ; (logging level;default info; others: debug,warn)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false              ; (start in foreground if true;default false)
minfds=1024                 ; (min. avail startup file descriptors;default 1024)
minprocs=200                ; (min. avail process descriptors;default 200)

[supervisorctl]
serverurl=unix:///var/tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[program:goapp]
command=/go/bin/goapp ; the program (relative uses PATH, can take args)
autostart=true              ; start at supervisord start (default: true)
autorestart=true            ; retstart at unexpected quit (default: true)

Goで始めるMiddleware その2

前回記事を書いたがもう少し詳しく書く。

takayukinakata.hatenablog.com

なぜGoでMiddlewareを書きたいのか

前回書き忘れていたところ。

おそらくだがフレームワークを使用している場合には、フレームワークがよろしくやってくれるので自分でMiddlewareをどうこうしたいというモチベーションはあまりない気がする。しかし、あくまで個人の主観だが、標準パッケージが利用しやすいGoでは、シンプルなAPIなどであればフレームワークを使用せずに標準のnet/httpを使用して書きたいケースなどは十分あり得るはずである。そして、標準のnet/httpを使用して書く場合には自分でMiddlewareまわりを実装する必要が発生するはずである(パッケージを使用して実装するのも含め)。このため、GoにおいてはMiddlewareを書きたくなるモチベーションが生まれることは十分あり得る。

前回のおさらい

GoでシンプルにMiddlewareを実装すると以下のようになる。

Middlewares in Go: Best practices and examples – Nicolas Merouze

package main

import (
    "fmt"
    "net/http"
)

// indexHandler ...
func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, middleware!")
}

// aboutHandler ...
func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("This is midlleware test!!")
}

// middleware1 ...
func middleware1(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("[START] middleware1")
        next.ServeHTTP(w, r)
        fmt.Println("[END] middleware1")
    }
}

// middleware2 ...
// middleware3 ...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", middleware1(middleware2(middleware3(indexHandler))))
    mux.HandleFunc("/about", middleware1(middleware2(middleware3(aboutHandler))))

    http.ListenAndServe(":8888", mux)
}

このようにMiddlewareを入れ子にして実装することで、例えば複数のMiddlewareを追加したい場合も容易に実装できる。

もっといい感じにMiddlewareを書くには

GoでMiddlewareを始めることができた。Middlewareの追加もできた。

しかし、追加するMiddlewareがさらに増えるとめんどくさそうなのは容易にわかると思う。

ではこの場合はどう実装したらいいだろうか。解の一つとしてMiddlewareスタックのようなものを作るというのがある。

package main

import (
    "fmt"
    "net/http"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("[START] middleware1")
        next.ServeHTTP(w, r)
        fmt.Println("[END] middleware1")
    }
}

// middleware2 ...
// middleware3 ...

func main() {
    middlewares := newMws(middleware1, middleware2, middleware3)

    mux := http.NewServeMux()
    mux.HandleFunc("/", middlewares.then(indexHandler))
    mux.HandleFunc("/about", middlewares.then(aboutHandler))

    http.ListenAndServe(":8888", mux)
}

type middleware func(http.HandlerFunc) http.HandlerFunc

type mwStack struct {
    middlewares []middleware
}

func newMws(mws ...middleware) mwStack {
    return mwStack{append([]middleware(nil), mws...)}
}

func (m mwStack) then(h http.HandlerFunc) http.HandlerFunc {
    for i := range m.middlewares {
        h = m.middlewares[len(m.middlewares)-1-i](h)
    }
    return h
}

比較するとこうなる。Middlewareの入れ子を何回も書く必要がなくなる。

// 前
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", middleware1(middleware2(middleware3(indexHandler))))
    mux.HandleFunc("/about", middleware1(middleware2(middleware3(aboutHandler))))

    http.ListenAndServe(":8888", mux)
}

// 後
func main() {
    middlewares := newMws(middleware1, middleware2, middleware3)

    mux := http.NewServeMux()
    mux.HandleFunc("/", middlewares.then(indexHandler))
    mux.HandleFunc("/about", middlewares.then(aboutHandler))

    http.ListenAndServe(":8888", mux)
}

パッケージを使うと

ではパッケージを使うとどうだろうか。ここではAliceとnegroniを紹介する。

Alice

github.com

Aliceを使用すると前述したMiddlewareスタックのようなものを容易に実装できる(というか元ネタ)。以下、Aliceの説明を引用する。

Alice provides a convenient way to chain your HTTP middleware functions and the app handler.

In short, it transforms

Middleware1(Middleware2(Middleware3(App)))

to

alice.New(Middleware1, Middleware2, Middleware3).Then(App)

コードを見ればわかるが考え方的には前述したMiddlewareスタックのようなものをalice.New(Middleware1, Middleware2, Middleware3)で作ってから.Then(App)でハンドラを返すようになっている。Aliceを使用すると実装は以下のようになる。

package main

import (
    "fmt"
    "net/http"

    "github.com/justinas/alice"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("[START] middleware1")
        next.ServeHTTP(w, r)
        fmt.Println("[END] middleware1")
    })
}

// middleware2 ...
// middleware3 ...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler)
    mux.HandleFunc("/about", aboutHandler)

    chain := alice.New(middleware1, middleware2, middleware3)

    http.ListenAndServe(":8888", chain.Then(mux))
}

negroni

github.com

negroniも同様にMiddlewareスタックのようなものを扱うが、これまで紹介したものと違い実装する側がそのスタックを意識しなくてよい。negroni独自にHandlerインターフェースを用意しており、これに沿って実装すれば容易にMiddlewareの追加もできる。また、デフォルトでロガーやリカバリー処理(negroni.Classic())を用意してくれており、比較的リッチなパッケージとなっている。

https://github.com/urfave/negroni#negroniclassic

negroniを使用すると実装は以下のようになる。

package main

import (
    "fmt"
    "net/http"

    "github.com/urfave/negroni"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    fmt.Println("[START] middleware1")
    next(w, r)
    fmt.Println("[END] middleware1")
}

// middleware2 ...
// middleware3 ...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler)
    mux.HandleFunc("/about", aboutHandler)

    n := negroni.New()
    n.Use(negroni.HandlerFunc(middleware1))
    n.Use(negroni.HandlerFunc(middleware2))
    n.Use(negroni.HandlerFunc(middleware3))
    n.UseHandler(mux)
    http.ListenAndServe(":8888", n)
}

感想

Goを書き始めてから初めてMiddlewareを意識した。最初はよくわからなかったが実際に書いてみるとそんなに怖くなかった。とはいえGoを書き始めて間もないので他によさげなパッケージがあれば教えてほしいです。

書いたコードはここに置きました。

github.com

Goの標準logでやってるintをasciiにする変換処理を見てみる

Goのlogパッケージでやってるintをasciiにする変換処理が何やってるか最初わけわかんなかったけど、多少理解できたからメモしておく。対象のコードは以下の通り。標準で年月日をログの先頭に付与できる機能(2009/01/23 01:23:23 log-messageみたいな感じ)があってそこで使用されている。

src/log/log.go - The Go Programming Language

// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
func itoa(buf *[]byte, i int, wid int) {
    // Assemble decimal in reverse order.
    var b [20]byte
    bp := len(b) - 1
    for i >= 10 || wid > 1 {
        wid--
        q := i / 10
        b[bp] = byte('0' + i - q*10)
        bp--
        i = q
    }
    // i < 10
    b[bp] = byte('0' + i)
    *buf = append(*buf, b[bp:]...)
}

例えば、

itoa(buf, 2017, 4)

とすると、bufには'2''0''1''7'が設定される。つまり、2017とログに出力される。

まず、単純にintをasciiにする処理を見てみる。上記のコードではb[bp] = byte('0' + i - q*10)b[bp] = byte('0' + i)に当たるところ。 そもそもasciiでは10進数の数字と文字の数字は以下のように対応している。

10進数 : 文字
48 : 0
49 : 1
50 : 2
51 : 3
52 : 4
53 : 5
54 : 6
55 : 7
56 : 8
57 : 9

このため、例えばintの7をasciiの7にしたい場合にはintの55を持つ必要がある。このintからasciiへの変換処理をどのように実装するかというと、byte('0' + i)に当たる。つまり、byte('0' + 7) = byte(48 + 7) = byte(55) = '7'という具合である。'0' = 48を基準にしてここにintを足すことでintからasciiへの変換処理を実現している。

では、b[bp] = byte('0' + i - q*10)の処理はどうだろうか。なお、widに関してはここでは説明しない。例えばi = 2017とすると1回目のループで各変数は以下のようになる。

// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
func itoa(buf *[]byte, i int, wid int) {
    // Assemble decimal in reverse order.
    var b [20]byte
    bp := len(b) - 1     // bp = 19
    for i >= 10 || wid > 1 {
        wid--
        q := i / 10     // q = 2017 / 10 = 201
        b[bp] = byte('0' + i - q*10)     // b[19] = byte('0' + 2017 - 201*10) = byte('0' + 2017 - 2010) = byte(48 + 7) = byte(55) =  '7'
        bp--     // bp = 18
        i = q     // i = 201
    }
    // i < 10
    b[bp] = byte('0' + i)
    *buf = append(*buf, b[bp:]...)
}

これを見るとわかるように2017の7(int)を'7'(ascii)と変換していることがわかる。また、ループを繰り返すと一の位から順にintからasciiに変換していることもわかる。

上記のようにしてintからasciiへの変換を実現している。

まとめ

標準パッケージは全て「イカした」実装していて自分じゃ思いつかないようなやり方ばっかりだろうから読んでもわからなさそう、とか勝手に思い込んでた。だけど、読んでみるとプレーンな実装箇所は案外泥臭いやり方(上記q := i / 10のところとか!)だし、理解できるところも多くて、今まで自分が勝手に持ってた心理的障壁みたいなものがちょっと取り除かれた気がする。

Goで始めるMiddleware

そもそもMiddlewareとは何かと言うと、私の認識ではアプリケーションの処理の前後で何らかの処理を行ったりするもの。例えば、ログを追加したりリクエストのAPIキーを認証したりする。以下の図がわかりやすい。

f:id:takayukinakata:20171124184420p:plain

Laravel 5.0 - Middleware (Filter-style) | MattStauffer.com より引用

とはいえ、上記だけでは具体的にどのように実装したらイメージがつかなかったんだけど、Goの公式のwikiでいろいろ紹介していてわかりやすかったのでこれを元に簡単なサンプルを実装した。

LearnServerProgramming · golang/go Wiki · GitHub

Middlewares in Go: Best practices and examples – Nicolas Merouze

package main

import (
    "fmt"
    "net/http"
    "time"
)

// indexHandler ...
func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, middleware!")
}

// aboutHandler ...
func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("This is midlleware test!!")
}

// loggingHandler ...
func loggingHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        t1 := time.Now()
        next.ServeHTTP(w, r)
        t2 := time.Now()
        fmt.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
    }
}

func main() {
    http.HandleFunc("/", loggingHandler(indexHandler))
    http.HandleFunc("/about", loggingHandler(aboutHandler))
    http.ListenAndServe(":8888", nil)
}

ポイントは、

  • http.HandlerFuncを返すラッパーを作ること
  • ラッパーの中で引数のhttp.HandlerFuncを呼び出すこと

だと個人的に思った。前述の図との対応としては、処理のイメージとしてはこんな感じだろうか。Middlewareで前処理を行って、アプリケーションの処理( next.ServeHTTP(w, r) )を行って、Middlewareで後処理を行う。

// sampleHandler ...
func sampleHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 前処理
        next.ServeHTTP(w, r)
        // 後処理
    }
}

まとめ

Middlewareは怖くない

CentOSでユーザ追加

DockerでGoの環境構築する際にCentOSでユーザ追加したのでメモ。

$ useradd -m -d /home/docker -s /bin/bash docker
$ echo "docker:docker" | chpasswd
$ echo "docker ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
  • useradd:新規ユーザの追加
  • chpasswdでパスワードの更新

それぞれ詳細はmanコマンドで確認できる。 追加されたユーザは/etc/passwdファイルで確認できる。なお、passwdファイルの詳細はman 5 passwdで確認できる。

/etc/sudoersファイルでユーザのsudo権限を管理している。dockerで環境構築した際には上記のコマンドで設定できたが、通常ではrootでさえ編集不可のファイルとなっている。こちらもmanコマンドで詳細を確認できる。

sudoersを変更する、よく使う設定例 - それマグで!

感想

普段の作業では設定しないところだったので忘れないようにメモした。そしてmanコマンドってこんなに便利なんだってことがわかった。

Real World HTTP を読んだ

Real World HTTPを読んだ。前に読んだやつ。

www.oreilly.co.jp

HTTPについてそれが作成されてきた歴史と併せて説明されておりわかりやすかった。RFCのリファレンスにもなっており、これを通して初めてRFCの内容を読んだ。ハイパフォーマンスブラウザネットワーキングではパフォーマンスの観点からの説明だけであったのに対して、Real World HTTPでは一般的に使用される機能に関して説明してあり、HTTPの入門書としてもいいんじゃないかと思った。

  • HTTP HEADERの説明が一通りあって、今までスルーしてた部分をフォローできた。
  • Cache-Controlについてもう少し詳しく調べた

リクエスト時の Cache-Control、max-age=0 と no-cache の違い | tech - 氾濫原

RFC 7234 - Hypertext Transfer Protocol (HTTP/1.1): Caching

ちなみにGOによる実装の部分はスルーしたが、最近になってGoを書くようになったので改めて読み直したい。

ハイパフォーマンスブラウザネットワーキングを読んだ

www.oreilly.co.jp

TCP, TLS, HTTPといったアプリケーションよりも低いレイヤーについて基本的な構造を説明し、パフォーマンスの観点からエンジニアはどう関わるべきかが書かれている。文章は読みやすかった。読み飛ばした部分もあるが10日間ほどで読み終わった。380ページ。

以下、自分用メモ。

1章 レイテンシ・帯域幅入門

  • ネットワークのパフォーマンスを決める2つの要素がある
  • レイテンシは要するにパケットを送信してから受信するまでの時間。光ファイバーを通る時間やルータの処理時間などの計。
  • 帯域幅は要するに速度(bps)。
  • 光ファイバーを通る時間(propagation delay)は光の速さが29km/sであり、限界が決まっている。
  • 感覚的に数百ミリ秒遅れると人間は遅れを認識する。
  • ラストワンマイルのレイテンシ。要するにパケットが太平洋を渡っても、最後に複数のプロバイダを経由することでレイテンシが発生してしまうこと。
  • ほとんどの場合、帯域幅ではなくレイテンシがパフォーマンスのボトルネック

2章 TCPの構成要素

  • TCP接続で3ウェイハンドシェイクから始まる。ここでもクライアントがデータを送信し始めるまでに1往復分のレイテンシが発生している。
  • フロー制御。データの送信サイズがウィンドウ値で制限されてしまう。データ受信側が処理しきれないデータ量を送信側が送信してしまうのを防ぐための仕組み。
  • スロースタート。データの送信サイズは小さい値から始まって、1往復ごとに送信サイズが大きくなっていく。ネットワーク自体の許容量を超えないようにするための仕組み。
  • 上記のようにデータサイズが制限されることにより発生するレイテンシ(データを余計に送信するためデータの往復が発生するなど)がボトルネックであり、帯域幅はそんなに関係ない。
  • ウィンドウ値を大きくしたり、スロースタートリスタートを無効にしたりするとパフォーマンスにはいいらしい。
  • そもそもとして、CDNを利用してデータを近くでやり取りするようにするといい

3章 UDPの構成要素

  • 受信側に確実に届けるための仕組みがない。このため信頼性は低い。
  • その分、余計なデータの往復などが発生しないため、レイテンシの発生が少ない。
  • NATの話が出てきた。ここでも処理時間(レイテンシ)が発生する
  • 疲れてたので読み飛ばしながら進んだ。

4章 TLS

  • HTTPSのSのやつ。暗号化、認証、データ整合性を提供する。要するに、認証局を利用して、クライアントから送信されたデータが本当にクライアントから送られてきたものかどうかを保証する。逆も同じ。
  • TLSハンドシェイクから通信を始める。最大2往復。
  • 1往復目で使用するTLSのバージョンや多分だけど利用できる認証局をクライアントが送信して、サーバがバージョンや認証局はこれを使います、っていうレスポンスを返す。
  • 2往復目で暗号化のために共通鍵のやり取りを行う。
  • TCPハンドシェイクと合わせると、最大で3往復分のレイテンシが発生する。暗号化の計算によるレイテンシは大したことないらしいので、ここがボトルネックとなる。
  • パケットはすでに光速で近い速度なためここは大きなレイテンシ改善は見込めないが、CDNを利用することで距離を近くすることができる。つまり、CDNTCPTLSハンドシェイクを行わせることでレイテンシ改善が見込める。
  • これが動的コンテンツのやり取りにおいて、キャッシュ機構は使用しないのにCDNを利用する理由。

5章 ワイヤレスネットワーク入門

  • パフォーマンスは周波数帯域幅とシグナル強度に依存する

6章 WiFi

7章 モバイルネットワーク

  • 3G,4G,LTEの規格について
  • RRCは物理層での接続を管理する。TCPハンドシェイクが行われる前段階。ここでもレイテンシが発生する。
  • 読み飛ばした

8章 モバイルネットワークの最適化

  • 読み飛ばした

9章 HTTPの歴史

  • 0.9 => 1.0 => 1.1 => 2.0
  • 0.9のリクエストはGETメソッドとパスの1行で構成される。めちゃシンプル
  • 1.0でキャッシュ(Expires)がレスポンスで返るようになった
  • 1.1でKeep-Alive接続が可能になった
  • 2.0はパフォーマンス向上が主なテーマ

10章 Webパフォーマンス入門

  • JavaScriptの実行はCSSにブロックされる可能性がある。
  • パフォーマンスを計測できるツールを使う、そして計測する。
  • ウォーターフォールチャートがパフォーマンスを計測する上で見やすい
  • ブラウザがやっているパフォーマンス最適化

11章 HTTP 1.x

  • キープアライブ接続がとにかく重要!
    • TCPTLSハンドシェイクを省略できるのでレイテンシ削減に繋がる
  • クライアントは一度に一つのリクエストしか送れない、サーバは一度に一つのリクエストしか処理できない、といったように並列処理ができないという課題がある。HTTPのHoLブロッキング(TCPのHoLブロッキングとは別)。
  • ここを解決できればパフォーマンス改善に繋がる。これをやりたいのがHTTP2.0。

12章 HTTP 2.0

  • パフォーマンス向上に主眼が置かれている
  • バイナリフレーミングレイヤー(?)。これでクライアントから複数のリクエストを一度に送信できるようになるらしい
  • サーバからプッシュで画像などを配信できるようになるらしい

13章 アプリケーション配信最適化

  • 定番のパフォーマンスベストプラクティス
    • DNSルックアップを減らす(キープアライブ接続)
    • TCP接続の再利用(キープアライブ接続)
    • HTTPリダイレクトを減らす(新たにDNSルックアップやTCP接続が必要になるため)
    • 不要なリソースはそもそも配信しない
    • リソースのキャッシュ

14章 ブラウザネットワーク入門

  • 印象に残っていない

15章 XMLHttpRequest

  • ajaxで使うやつ
  • CORSを気にしている

16章 Sever-Sent Events

  • 読み飛ばした
  • サーバ => ブラウザ

17章 WebSocket

  • 読み飛ばした
  • サーバ <=> ブラウザ

18章 WebRTC

  • 読み飛ばした
  • リアルタイム通信で使用する(Web会議など)

Webエンジニアに転職して半年が経った

Webエンジニアに転職して半年が経った。

仕事にも慣れてきたのでこれまでとこれからについて整理しようと思う。

転職前

企業向けのパッケージ開発をしていた。Web経由で使用してもらうものではなく、お客さんのとこに作ったサーバにパッケージをインストールして使ってもらう形態だった。

いわゆるオレオレフレームワークJavaを使って、ビジネスロジックをひたすら書いてた。ちなみにDBはOracle

人間関係とか仕事の出来とかは問題なかったけど、やはりオレオレフレームワークJavaでは何か物足りなかった。オレオレフレームワークの上だと、クラス設計すら何も考えなくていいのでただif文書いてメソッド呼び出して、みたいなことしか書かなくてよかった。

そんな中、インターネットを覗くと、Webエンジニアたちが自分の知らない言葉でしゃべってた。gitとは、デザインパターンとは、HTTPとは、他にもたくさん。まるで話についていけない。自分自身とインターネット上のWebエンジニアを比較して、技術的なことが身についていないことが不安で仕方がなかった。もちろん、そもそも非WebとWebを単純に比較してどうなのとは今は思うが。

というわけで、将来的にエンジニアとしての技術力が足りなくて会社の外では食っていけないと思った。一番大事なのは社内ではなく社外(市場)での評価だと考えていたので、転職することを決意した。

ちなみに、技術的には物足りなかったが、技術以前に問題解決に対する考え方に関しては徹底的に教えてもらっていて、これは本当によかったと今感じている。直属だった先輩、上司にはかなりよくしてもらっていたのでありがたかった。

転職後〜今

新しい会社ではRubyRailsを使ってサイトを作っている。同じフレームワークでもRailsだとググったら情報がたくさん出てくるし、ソースも読めるし、勉強しがいがあって楽しく過ごせている。

自分たちで運用しているので、アプリケーションを動かすサーバや下のレイヤーであるHTTP, TCP/IPなどについても知っておく必要があり、正直いくら勉強してもなかなか追いつけないでいる。それでもやはり勉強しがいがあるというのは楽しいものであると思った。

入社したての時はWebど素人であり、HTMLも若干怪しいくらいだったが、やっていけば案外なんとかなった。前職で学んだ仕事のやり方のおかげ。頭の中で考えているリスクはほとんどが必要以上にリスクとして認識されている、って話はよく聞くが本当にそうだった。そういえば、前職の先輩に「具体的にここがヤバい、って言えなければそれは大体ヤバくない。必要以上に頭がリスクとして認識しているだけ。」と言われたことがあったのを思い出した。

スキルセット

今現在、自分がわかる範囲はこんな感じだろうか。どれも雰囲気使えるレベルではあるが。

こう書くとなかなか不安になるからたまに列挙するのはいいかも。

今後

次の半年だが、大きく2つのテーマで勉強する。キーワードはRubyと低レイヤー。

一つはRubyRubyでできることを増やす。

基本的な文法はわかったので手を動かしてとにかく書く。正直、仕事で使っているからRubyなのだが、一つの言語で大体のことできるようになると他の言語の習得も容易になるという話をよく聞くので、最初はあえてこだわってRubyを書いていく。

もう一つは低レイヤー。HTTP、TCP/IPなどなどネットワークまわりを中心に。

流行り廃りのない知識がないと将来的に死ぬ気がする。今後死にたくないので勉強する。

md2keyを使った

社内LTでスライド作るのにmd2keyを使った。便利。

github.com

k0kubun.hatenablog.com

太字や取り消し線はまだ使えないらしい

Rich texts · Issue #11 · k0kubun/md2key · GitHub

とのこと。

調べてみた

実装できないかなと思って、applescriptで太字や取り消し線を実現できないか調べてみた。keynote見る限り、文字のスタイルにボールドはあるから案外すぐ見つかるんじゃないかと思った。

結果としては見つけられなかった。リッチテキストのプロパティがcolor, font, sizeしかなかったのでそもそもできないんじゃないか(styleみたいなのがあると期待してたがなかった)。ちなみに色やフォントはうまく指定できた。

rich text n, pl rich text : This provides the base rich text class for all iWork applications.
elements
contains characters, paragraphs, words.
properties
color (color) : The color of the font. Expressed as an RGB value consisting of a list of three color values from 0 to 65535. ex: Blue = {0, 0, 65535}.
font (text) : The name of the font. Can be the PostScript name, such as: “TimesNewRomanPS-ItalicMT”, or display name: “Times New Roman Italic”. TIP: Use the Font Book application get the information about a typeface.
size (integer) : The size of the font.

applescriptをあんまり深追いしたくないのでこの辺でやめる。