20210113のGoに関する記事は5件です。

glibc, musl libc, go の resolver の違い

先日、resolv.conf で timeout を調整したいなと思うことがありました、しかし、Docker だの Kubernetes だのといった時代です。Linux しか使っていなかったとしても resolver が glibc のそれだとは限らないわけですね。そこで glibc, musl libc (alpine のやつです), go の resolver の違いを調べてみました。

環境

それぞれ docker container を使い、問い合わせの状況は tcpdump で確認しました。

macOS Catalina (10.15.7)
docker desktop 3.0.3 (51017)

container image は

alpine:3.12.3 / musl-1.1.24-r10
debian:buster-20201209 (10.7) / glibc 2.28-10

Go は mac 上で 1.15.6 を使い、クロスコンパイルして上記の alpine 上で実行しました。
クロスコンパイルなのでデフォルトで CGO_ENABLED=0 のはずですが、明示してビルドしました。

resolv.conf の options などの対応状況

resolv.confの例
nameserver 8.8.8.8
nameserver 8.8.4.4
search default.svc.cluster.local svc.cluster.local cluster.local asia-northeast1-b.c.project-id.internal c.project-id.internal google.internal
options ndots:5 timeout:5 attempts:2

resolv.conf の options にはいろいろありますが、今回の 3 つの resolver 全てで対応されているのは

  • ndots
  • timeout
  • attempts

の3つでした。

nameserver は勿論ですが search にも対応しています。
昔の musl libc は search (domain 補完) には対応していませんでしたが 1.1.13 から対応していました。
nameserver は複数回指定可能ですが、使われるのは3つまでです。4つ以上指定しても3つまでしか使われません。

それでは機能の違いを見ていきましょう

ndots

まずは ndots です、Kubernetes を使うようになってはじめて知りました。

指定するのは問い合わせるドメインに含まれる dot (.) の数です、Kubernetes のデフォルト設定では 5 (ndots:5) となっています。

例えば ping www.google.com とした場合、dot の数は 2 です。

dot の数が ndots 以上の場合

dot の数が ndots で指定した以上であればそれは FQDN であろうと判断し、search で指定したドメインを補完せずに DNS サーバーに問い合わせます。ここで、NXDOMAIN (そんなドメイン存在しないよ) が返ってきたら glibc と go は search で指定したドメインを補完して問い合わせます。しかし、musl libc は補完した問い合わせを行わないでそんなドメインは存在しないよということで終了してしまいます。

その昔、musl はそもそもドメイン補完に対応していなかったので当時から使っている人は常に FQDN で指定しているでしょうから問題ないでしょうが、知らないで ndots を小さくすると解決できるはずのドメインが解決できないかもしれません。

dot の数が ndots 未満の場合

dot の数が ndots 未満であった場合はまず search で指定した複数のドメインを順番に補完して問い合わせます、見つかるまで繰り返すため(これの上限を確認するの忘れた)、順序は重要ですかね。ただ、順序以上に ndots で指定する数が重要です、5 というのは結構多いです。Kubernetes でデフォルト設定だと service.namespace.svc.cluster.local という FQDN を指定したとしても 4 なわけです。search に並んでいる全てのドメインの補完を試した後に service.namespace.svc.cluster.local をそのまま問い合わせてやっと欲しい結果が得られるわけです、DNS サーバー側にネガティブキャッシュがあるとはいえ、DNS サーバーとの通信はその回数行われるわけですから、かなりの無駄です。GKE のデフォルト設定では6つもドメインが列挙されています。EKS は4つ。ndots を小さくして名前解決ができないドメインが発生してしまうよりも、無駄な問い合わせが増えたとしても名前解決できる値がデフォルトなっているわけですね。ndots はチューニングの余地がありそうです。

ちなみに、末尾に . をつけた場合はそれは FQDN だぞということを意味するので ping www.google.com. などとした場合はどの resolver でもドメインの補完は行いません。

nameserver

複数サーバーを指定した場合の扱いが glibc と musl で大きく異なるため、まず説明しておきます。
glibc と Go は一つ目のサーバーに問い合わせて、timeout 秒待っても応答がない場合に、次のサーバーに問い合わせます。
musl libc は複数の nameserver に同時にリクエストを送って最初に返ってきた応答を使います。3つのネームサーバーが指定されていたら3倍の問い合わせが発生してしまうわけですね。

timeout と attempts

timeoutattempts は関連しているため一緒に扱います。また、nameserver の数にもよるためそれごとにまとめます。

nameserver が1つの場合

glibc と Go は問い合わせを投げては timeout 秒待ち、応答がなければ再度問い合わせて、合計 attempts 回問い合わせます。

musl libc は timeout 秒を attempts で割った秒数をそれぞれの問い合わせで待ちます。つまり、リトライを含めた合計で最大 timeout 秒待つということです。 timeout:5 attempts:2 というデフォルト設定では 2.5 秒ずつ待ちます。

nameserver が2つの場合

glibc と Go は1つ目の nameserver に問い合わせて timeout 秒待ち、次は2つ目の nameserver に問い合わせます。これが attempts の1回分です。timeout:5 attempts:2 というデフォルト設定ではそれぞれの nameserver に対して2回ずつ、合計4回問い合わせます。その都度5秒待ちます。

musl libc は nameserver の項で書いた通り、複数の nameserver に対して同時に問い合わせるため、nameserver が1つの場合と同じです。

nameserver が3つの場合

nameserver が3つになると glibc の timeout の振る舞いが少し変わります。1つ目の nameserver に対しては timeout で指定した秒数待ちますが、2つ目は timeout で指定したのよりも短くなり、3つ目は timeout で指定したものより長くなります。
timeout:5 の場合は5秒、3秒、6秒となります。このセットを attempts 回繰り返します。

Go は常に timeout 秒待ちます。

musl libc はこれまでと同じです。

timeout 時のドメイン補完

何度問い合わせても timeout するような場合はもういろいろダメなのであまり重要ではないですが、動作に違いがあったので書いておきます。

timeout が続く状態で、2つ目以降の search ドメインを補完するのかどうか、glibc は2つ目以降のドメイン補完は行いませんが、補完なしの問い合わせを行います。Go は律儀に全てのドメイン補完を試しますし、補完なしでの問い合わせもします。musl libc は1つ目の補完の問い合わせは行いますが、それで終わりです。補完なしの問い合わせもありません。

Go の動作確認で使ったコード

package main

import (
    "context"
    "net"
    "os"
    "log"
)

func main() {
    ctx := context.Background()
    resolver := &net.Resolver{}
    for _, v := range os.Args[1:] {
        log.Printf("Resolving %s\n", v)
        names, err := resolver.LookupHost(ctx, v)
        if err != nil {
            log.Fatal(err)
        }
        for _, name := range names {
            log.Printf("%s\n", name)
        }
    }
}

ndots:5 の凶悪さを可視化する

文章でつらつら書いても分かりづらいので、 GKE 環境で storage.googleapis.com にアクセスしようとした場合にどんな問い合わせが発生するかを見てみましょう。default namespace に立てた Pod で ping storage.googleapis.com を実行した際の tcpdump の出力を加工しました。(横幅を抑えるために)

Cloud Storage の API にアクセスしようとする度にこの数の問い合わせが発生すると思うとゾッとしますね。マイクロサービスで Keep-Alive などをしていない場合は大量の内部通信でもこれが発生しているかもしれません。JVM などのように DNS のキャッシュをしてくれれば良いですけどね。

ndots:2 として常に FQDN で指定するということが徹底できると良いのかな。

14:53:34.706909 ▶︎ A? storage.googleapis.com.default.svc.cluster.local. (66)
14:53:34.707130 ▶︎ AAAA? storage.googleapis.com.default.svc.cluster.local. (66)
14:53:34.708881 ◁ NXDomain 0/1/0 (159)
14:53:34.708940 ◁ NXDomain 0/1/0 (159)
14:53:34.709051 ▶︎ A? storage.googleapis.com.svc.cluster.local. (58)
14:53:34.709133 ▶︎ AAAA? storage.googleapis.com.svc.cluster.local. (58)
14:53:34.709615 ◁ NXDomain 0/1/0 (151)
14:53:34.709689 ◁ NXDomain 0/1/0 (151)
14:53:34.709771 ▶︎ A? storage.googleapis.com.cluster.local. (54)
14:53:34.709816 ▶︎ AAAA? storage.googleapis.com.cluster.local. (54)
14:53:34.712211 ◁ NXDomain 0/1/0 (147)
14:53:34.712280 ◁ NXDomain 0/1/0 (147)
14:53:34.712387 ▶︎ A? storage.googleapis.com.asia-northeast1-b.c.my-project-id.internal. (83)
14:53:34.712479 ▶︎ AAAA? storage.googleapis.com.asia-northeast1-b.c.my-project-id.internal. (83)
14:53:34.716561 ◁ NXDomain 0/1/0 (189)
14:53:34.716623 ◁ NXDomain 0/1/0 (189)
14:53:34.716718 ▶︎ A? storage.googleapis.com.c.my-project-id.internal. (65)
14:53:34.716760 ▶︎ AAAA? storage.googleapis.com.c.my-project-id.internal. (65)
14:53:34.719891 ◁ NXDomain 0/1/0 (162)
14:53:34.720191 ◁ NXDomain 0/1/0 (162)
14:53:34.720304 ▶︎ A? storage.googleapis.com.google.internal. (56)
14:53:34.720390 ▶︎ AAAA? storage.googleapis.com.google.internal. (56)
14:53:34.724145 ◁ NXDomain 0/1/0 (145)
14:53:34.724352 ◁ NXDomain 0/1/0 (145)
14:53:34.724458 ▶︎ A? storage.googleapis.com. (40)
14:53:34.724500 ▶︎ AAAA? storage.googleapis.com. (40)
14:53:34.726930 ◁ 4/0/0 AAAA 2404:6800:4004:813::2010, AAAA 2404:6800:4004:81c::2010, AAAA 2404:6800:4004:81d::2010, AAAA 2404:6800:4004:81e::2010 (152)
14:53:34.726957 ◁ 16/0/0 A 172.217.161.80, A 172.217.175.16, A 172.217.175.48, A 172.217.175.80, A 172.217.175.112, A 216.58.197.144, A 172.217.25.208, A 172.217.25.240, A 172.217.26.48, A 172.217.31.176, A 172.217.161.48, A 172.217.174.112, A 172.217.175.240, A 216.58.220.112, A 216.58.197.208, A 216.58.197.240 (296)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang製PHPアプリケーションサーバRoadRunnerでLaravel-adminを動かす

アプリケーションサーバーの新潮流

昨今のPHP界隈では、Swooleに代表されるように、よくあるApache+mod_phpやPHP-FPMによる従来の構成と異なる実行方法を持つPHPのためのHTTPサーバまたはアプリケーションサーバが少しずつ注目を集めるようになっています。

RoadRunner

RoadRunnerもそんな新興のアプリケーションサーバの1つで、Go言語で書かれています。 HTTPのリクエストを前段のGoのHTTPハンドラがさばき、ロードバランサ/プロセスマネージャがPHPのWorkerにリクエストを割り振るという構成になっています。

通常のHTTPサーバだけでなく、RoadRunnerによるgRPCサーバの実装も公開されており、PHPの新しい可能性を見せてくれています。

RoadRunnerを試す

RoadRunnerはLaravelもサーポートしており、Laravel用のパッケージも開発されているので

今回はこれをベースにRoadRunner実装していきたいと思います。

RoadRunnerのインストール

まずは下記コマンドでパッケージのインストールを行います。

# roadrunner
composer require spiral/roadrunner "^1.8"
# roadrunner-laravel
composer require spiral/roadrunner-laravel "^3.4"

次に、下記コマンドを実行してパッケージ構成ファイル(./config/roadrunner.php)を作ります。

php artisan vendor:publish --provider='Spiral\RoadRunnerLaravel\ServiceProvider' --tag=config

.rr.ymlの作成

roadrunnerの設定ymlファイルを書きます。

80番ポートで起動するように設定します。

env:
  APP_REFRESH: true
http:
  address: 0.0.0.0:80
  http2:
    enabled: true
    h2c: true
    maxConcurrentStreams: 128
  workers:
    command: 'php ./vendor/bin/rr-worker'
static:
  dir: public
reload:
  interval: 1s
  patterns: [ '.php' ]
  services:
    http:
      dirs: [ '' ]
      recursive: true

諸々の微修正

logging.php

'stdout' => [
    'driver' => 'monolog',
    'handler' => StreamHandler::class,
    'with' => [
        //php://stdoutにするとエラーになってコケるのでphp://stderrに変える
        'stream' => 'php://stderr',
     ],
     'level' => 'debug',
],

composer.jsonにscriptを追加する。

"scripts": {
        "roadrunner:dev": [
            "composer dump-autoload",
            "./rr serve -v -d"
        ],
        "roadrunner:prod": [
            "./rr serve"
        ],
        "roadrunner:setup": [
            "rr get-binary"
        ],
    }

startup.shに起動コマンドを乗せる

# roadrunner
composer roadrunner:setup
chmod +x rr
composer roadrunner:dev

これで大体の設定は完了しました。

起動してみる

以下の手順で起動させます。

cp docker-compose.example.yml docker-compose.yml

docker-compose up -d

無事に起動できました。

スクリーンショット 2020-11-27 12.59.30.png (168.6 kB)

まとめ

今回はGolangで作られたPHPのweb・アプリケーションサーバーを使った。サンプルを作ってみました。
おもしろいですが、不安定なのでプロダクション環境ではあまり使うのはおすすめできないですね。

参考文献

https://nextat.co.jp/staff/archives/235

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang製PHPアプリケーションサーバRoadRunnerを試す

アプリケーションサーバーの新潮流

昨今のPHP界隈では、Swooleに代表されるように、よくあるApache+mod_phpやPHP-FPMによる従来の構成と異なる実行方法を持つPHPのためのHTTPサーバまたはアプリケーションサーバが少しずつ注目を集めるようになっています。

RoadRunner

RoadRunnerもそんな新興のアプリケーションサーバの1つで、Go言語で書かれています。 HTTPのリクエストを前段のGoのHTTPハンドラがさばき、ロードバランサ/プロセスマネージャがPHPのWorkerにリクエストを割り振るという構成になっています。

通常のHTTPサーバだけでなく、RoadRunnerによるgRPCサーバの実装も公開されており、PHPの新しい可能性を見せてくれています。

RoadRunnerを試す

RoadRunnerはLaravelもサーポートしており、Laravel用のパッケージも開発されているので

今回はこれをベースにRoadRunner実装していきたいと思います。

RoadRunnerのインストール

まずは下記コマンドでパッケージのインストールを行います。

# roadrunner
composer require spiral/roadrunner "^1.8"
# roadrunner-laravel
composer require spiral/roadrunner-laravel "^3.4"

次に、下記コマンドを実行してパッケージ構成ファイル(./config/roadrunner.php)を作ります。

php artisan vendor:publish --provider='Spiral\RoadRunnerLaravel\ServiceProvider' --tag=config

.rr.ymlの作成

roadrunnerの設定ymlファイルを書きます。

80番ポートで起動するように設定します。

env:
  APP_REFRESH: true
http:
  address: 0.0.0.0:80
  http2:
    enabled: true
    h2c: true
    maxConcurrentStreams: 128
  workers:
    command: 'php ./vendor/bin/rr-worker'
static:
  dir: public
reload:
  interval: 1s
  patterns: [ '.php' ]
  services:
    http:
      dirs: [ '' ]
      recursive: true

諸々の微修正

logging.php

'stdout' => [
    'driver' => 'monolog',
    'handler' => StreamHandler::class,
    'with' => [
        //php://stdoutにするとエラーになってコケるのでphp://stderrに変える
        'stream' => 'php://stderr',
     ],
     'level' => 'debug',
],

composer.jsonにscriptを追加する。

"scripts": {
        "roadrunner:dev": [
            "composer dump-autoload",
            "./rr serve -v -d"
        ],
        "roadrunner:prod": [
            "./rr serve"
        ],
        "roadrunner:setup": [
            "rr get-binary"
        ],
    }

startup.shに起動コマンドを乗せる

# roadrunner
composer roadrunner:setup
chmod +x rr
composer roadrunner:dev

これで大体の設定は完了しました。

起動してみる

以下の手順で起動させます。

cp docker-compose.example.yml docker-compose.yml

docker-compose up -d

無事に起動できました。

スクリーンショット 2020-11-27 12.59.30.png (168.6 kB)

まとめ

今回はGolangで作られたPHPのweb・アプリケーションサーバーを使った。サンプルを作ってみました。
おもしろいですが、不安定なのでプロダクション環境ではあまり使うのはおすすめできないですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【gvm】Goのバージョン管理

はじめに

集団開発においてバージョンを一致させる必要があり、バージョン管理はマストの作業である.
本記事はGoにおけるバージョン管理について記録を残しておく.

Goのバージョン管理ツール 【gvm】

Goのバージョン管理ツール go version manager (以降 gvm )を扱う
https://github.com/moovweb/gvm

gvmの利用手順

gvm の利用手順は大きく以下の3つ

  1. gvmのインストール
  2. goのバージョンをインストール
  3. goの特定のバージョンを利用

1. gvmインストール

gvmのインストールはターミナル上で以下を実行する

$ bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
$ source /Users/nancy/.gvm/scripts/gvm

インストールが正しく実行されているかを確認

$ gvm version
Go Version Manager v1.0.22 installed at hoge

2. goのバージョンをインストール

gvm のインストールを確認したら開発で扱う go のバージョンを取り込む.

gvm install go1.15.6

この際、下記のようなエラーが出る場合がある.

gvm install go1.15.6
Installing go1.15.6...
 * Compiling...
ERROR: Failed to compile. Check the logs at /Users/ko-wa/.gvm/logs/go-go1.15.6-compile.log
ERROR: Failed to use installed version

このエラーが出た際の対処法としてはバイナリをインストールする.

$ gvm install go1.15.6 -B
Installing go1.15.6 from binary source

参考:https://akataworks.hatenadiary.jp/entry/2016/10/18/201025

3. goの特定のバージョンを利用

インストールしたバージョンを再度 gvm コマンドを使って切り替える.

$ gvm use go1.15.6
Now using version go1.15.6

goのバージョンを確認する.

$ go version
go version go1.15.6 darwin/amd64

既定のバージョンになっていればオッケー!

gvm を常に利用可能にする

gvm はターミナルを開き直すと利用できなくなるので、bash_profile(ターミナルが開かれる際に最初に実行される) に設定が必要

$ cat $HOME/.gvm/scripts/gvm >> ~/.bash_profile

書き込まれていれば、gvm が常に利用できる.

Go のバージョンデフォルトを変更する

最後に Go のバージョンの固定したい場合にデフォルトを変更する方法を書きます.

$ gvm use go1.15.6 --default
Now using version go1.15.6

以上で Go バージョン管理、gvm の使い方になります.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang入門4

Golangでgorilla/muxやgin, echo等フレームワークを使わずに一覧/詳細画面を作る。

本文はこちら: https://bizzare.me/post/golang4/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む