その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

いい感じにPHP Runtimeのdebパッケージを作る

PHPのエッジなバージョン (例えば今の時点だと 8.0.0alpha3) を使いたい場合,多くの場合はソースコードからビルドする必要があります.その他にもランタイムのオプションをいじりたいだとか,そういった様々な理由からPHPソースコードからビルドしなければならないことがあります.あるでしょう.

ソースコードからビルドするのはまあ良いんですが,そのビルド成果物 (つまりPHPランタイム) のデプロイ対象が多い場合に都度都度その環境でビルドをするのは時間がかかってしまいますし,計算機リソースの無駄遣いです.
というわけでDebianUbuntuを利用している場合だとdebパッケージを一度こさえて,それをばら撒くようにすると経済的です.

以下はミニマムな例:

$ curl -LO https://downloads.php.net/~carusogabriel/php-8.0.0alpha3.tar.gz
$ tar zxf php-8.0.0alpha3.tar.gz
$ cd php-8.0.0alpha3
$ ./configure
$ make
$ INSTALL_ROOT="/path/to/php-dist" make install

というふうにしておき, /path/to/php-dist/DEBIAN/control

Package: php8
Maintainer: moznion <moznion@gmail.com>
Architecture: amd64
Version: 8.0.0alpha3
Description: php 8 binary

というようなcontrolファイルを置いて,

fakeroot dpkg-deb --build /path/to/php-dist/ .

というふうにしておくと良い感じでdebパッケージが作成されます.


特記事項としては

  • ./configure の際に --prefix を付与してはならない
  • --prefix の代わりに make install の際に INSTALL_ROOT 環境変数を付加する

というあたりです.

というのも ./configure--prefix を付けてしまうと,build / install された成果物がそのprefixの内容に依存してしまうため,ビルドした環境の構成 (特にpath) とdebパッケージの展開先の環境構成が異なるときにうまく動作しなくなる場合があります.
例えば,./configure --prefix=/tmp/dist として makemake install すると,成果物は /tmp/dist 以下に展開されます.その状態でdebパッケージを作成し,そのパッケージを利用して別の環境にインストールすると,モノ自体は /usr/local 以下にインストールされますが,インストールされたモノたちは /tmp/dist に依存しています (例えば php.ini の場所は /tmp/dist/usr/local/lib/php.ini にあることが期待されてしまう).びみょう.

というわけで --prefix を渡さないようにしておきつつ (つまりデフォルトの /usr/local が利用される),しかしそのまま make install するとそのまま /usr/local 以下にモノがインストールされてしまいdebパッケージを作成するには具合が悪いので,INSTALL_ROOT 環境変数によって「インストール先だけを変える」ことにより,そのディレクトリに対してdebパッケージを作るといい感じに作成できます (つまりこの場合だと php.ini の場所は /usr/local/lib/php.ini になる).

良かったですね.

The Perl Foundationに寄付した

perl,やはり心のふるさとという感じがある……(流石に全盛期と比較して手のスピードは落ちたけど
大きな問題は他の人にとってふるさとではないということです

とりたてて特別な何かがあったわけではなく,久しくPerlを書いていなかったなか,たまたまPerlXML::XPathを使ってXMLをどやこやするスクリプトを書いたところ上記のような気持ちになったので「ふるさと納税」と称してPerlの総本山ことThe Perl Foundationに寄付したという経緯です.

www.perlfoundation.org

寄付はこちらからできます.PayPalを選ぶと日本からの寄付は未対応である旨が表示されるので,ここは一丁勇気を出して "Online credit card payment" すると良いでしょう.

心のふるさとに寄付をするというのもなかなかオツなものですね.

Javaの非同期アプリケーションのflame graph profileを取る

netty を使うような非同期 Java のアプリケーション (例えば Play2 Web アプリ) の flame graph profile を取るという話題です.色々な方法が考えられますが,jvm-profiling-tools/async-profiler を利用するのが最も手っ取り早そうな感じがしたので,その方法を示します.

github.com

使い方はいたって簡単で,Releases から任意のバージョンのアーカイブを取ってきて,

./profiler.sh -d 60 -f /path/to/flame_graph.svg $JAVA_APP_PID

として実行するだけで 60 秒間プロファイリングし,その結果の flame graph を取得することが可能です.

f:id:moznion:20200620234509p:plain

便利ですね.

tinygo 向けの JSON marshaler: go-json-ice を書いた

English article is here: Released go-json-ice: a code generator of JSON marshaler for tinygo - moznion's tech blog


tinygo では encoding/json を import するとコンパイルできなくなるという問題があり *1,なんらかの struct を JSON に marshal したい時に使える de facto な方法が無いように見えました.これに関しては例えば以下のような issue が立っています:

github.com

github.com

つまり tinygo 上で任意の struct を JSON にしたい時は「手で気を付けてシリアル化する」しか方法がなかったわけですが,まあそれだと何かと不便だったので表題の通り json-ice という encoding/json に依存しない JSON marshaler のコードジェネレータを作りました.

github.com

挙動としては,事前に marshaling 対象となる struct (の json カスタム struct タグ) を解釈して JSON に marshal するコードを吐き出す,という至ってシンプルなものとなります.似たような挙動をする先行実装に mailru/easyjson などがありますが,これらは内部的に encoding/json に依存しているようで,今回の用途にはマッチしませんでした.


例えば以下のような struct を marshal したい時には go:generate と一緒にコードを書いておくと

//go:generate json-ice --type=AwesomeStruct
type AwesomeStruct struct {
	Foo string `json:"foo"`
	Bar string `json:"bar,omitempty"`
}

MarshalAwesomeStructAsJSON(s *AwesomeStruct) ([]byte, error) というコードが生成されるので,それを利用して struct を JSON に marshal することが可能です:

marshaled, err := MarshalAwesomeStructAsJSON(&AwesomeStruct{
	Foo: "buz",
	Bar: "",
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%s\n", marshaled) // => {"foo":"buz"}

これにより実行時に reflection を使って動的に marshaling する必要がなくなるので,tinygo でも JSON marshaling が簡単に行えるようになります.また,動的な reflection の代わりに事前計算するので当然の結果ですがパフォーマンスも少し良くなります *2
もちろんその副作用として interface{} な値を持つ struct については動的な型の解決ができないため marshaling ができません.Marshaling するためには静的に型の解決ができる必要があります.

また tinygo は wasm を吐き出す機能も有しており,この wasm が実行時に import するモジュールは「元のコードが何に依存しているか」によって変化してきます.この実行時の依存をできる限りミニマムにしたい (例えばブラウザランタイム以外の強力な sandbox 環境で wasm を動かすというユースケースが考えられる) という動機があったので,生成コードが依存するパッケージは可能な限り最小限にとどめました.結果的に現時点では strconv にのみ依存するようになっています.ミニマル!


そんな感じのライブラリです.どうぞご利用ください!
もちろん tinygo ではなく通常の go の処理系でも利用できますが,それについてはもっと良い先行実装 (それこそ easyjson とか) があると思うので,そちらの利用の検討をおすすめします.


なお,この実装は JSON の marshaling のみをサポートするものですが,逆に tinygo で JSON unmarshaling するにはどうすればよいかと言うと,buger/jsonparser を利用すれば良いように思いました.

> It does not rely on encoding/json, reflection or interface{}, the only real package dependency is bytes.

github.com




余談ですが

//go:generate json-ice --type=DeepStruct
type DeepStruct struct {
	Deep []map[string]map[string]map[string]map[string]string `json:"deep"`
}

のような深く,再帰的(?)な型についてもちゃんとしたコード生成が可能です:

given := &DeepStruct{
	Deep: []map[string]map[string]map[string]map[string]string{
		{
			"foo": {
				"bar": {
					"buz": {
						"qux": "foobar",
					},
				},
			},
		},
		{
			"foofoo": {
				"barbar": {
					"buzbuz": {
						"quxqux": "foobarfoobar",
					},
				},
			},
		},
	},
}

marshaled, err := MarshalDeepStructAsJSON(given)
if err != nil {
	log.Fatal(err)
}

log.Printf("[debug] %s", marshaled) // => {"deep":[{"foo":{"bar":{"buz":{"qux":"foobar"}}}},{"foofoo":{"barbar":{"buzbuz":{"quxqux":"foobarfoobar"}}}}]}

生成コードはこんな感じ

import "github.com/moznion/go-json-ice/serializer"

func MarshalDeepStructAsJSON(s *DeepStruct) ([]byte, error) {
	buff := make([]byte, 1, 54)
	buff[0] = '{'
	if s.Deep == nil {
		buff = append(buff, "\"deep\":null,"...)
	} else {
		buff = append(buff, "\"deep\":"...)
		buff = append(buff, '[')
		for _, v := range s.Deep {
			if v == nil {
				buff = append(buff, "null"...)
			} else {
				buff = append(buff, '{')
				for mapKey, mapValue := range v {
					buff = serializer.AppendSerializedString(buff, mapKey)
					buff = append(buff, ':')
					if mapValue == nil {
						buff = append(buff, "null"...)
					} else {
						buff = append(buff, '{')
						for mapKey, mapValue := range mapValue {
							buff = serializer.AppendSerializedString(buff, mapKey)
							buff = append(buff, ':')
							if mapValue == nil {
								buff = append(buff, "null"...)
							} else {
								buff = append(buff, '{')
								for mapKey, mapValue := range mapValue {
									buff = serializer.AppendSerializedString(buff, mapKey)
									buff = append(buff, ':')
									if mapValue == nil {
										buff = append(buff, "null"...)
									} else {
										buff = append(buff, '{')
										for mapKey, mapValue := range mapValue {
											buff = serializer.AppendSerializedString(buff, mapKey)
											buff = append(buff, ':')
											buff = serializer.AppendSerializedString(buff, mapValue)
											buff = append(buff, ',')
										}
										if buff[len(buff)-1] == ',' {
											buff[len(buff)-1] = '}'
										} else {
											buff = append(buff, '}')
										}

									}
									buff = append(buff, ',')
								}
								if buff[len(buff)-1] == ',' {
									buff[len(buff)-1] = '}'
								} else {
									buff = append(buff, '}')
								}

							}
							buff = append(buff, ',')
						}
						if buff[len(buff)-1] == ',' {
							buff[len(buff)-1] = '}'
						} else {
							buff = append(buff, '}')
						}

					}
					buff = append(buff, ',')
				}
				if buff[len(buff)-1] == ',' {
					buff[len(buff)-1] = '}'
				} else {
					buff = append(buff, '}')
				}

			}
			buff = append(buff, ',')
		}
		if buff[len(buff)-1] == ',' {
			buff[len(buff)-1] = ']'
		} else {
			buff = append(buff, ']')
		}

		buff = append(buff, ',')
	}
	if buff[len(buff)-1] == ',' {
		buff[len(buff)-1] = '}'
	} else {
		buff = append(buff, '}')
	}
	return buff, nil
}

td-agent-gem で `google-protobuf requires Ruby version < 2.8.dev, >= 2.5.` みたいなエラーが出るって時

td-agent-gem を利用している時に google-protobuf requires Ruby version < 2.8.dev, >= 2.5. というエラーがでて困るという事がありました.

support.treasuredata.com

td-agent の ChangeLog を見た感じ,この記事を書いている時点での td-agent の最新バージョン v3.7.1 にバンドルされている ruby のバージョンは 2.4.10 のようなので,このエラーメッセージの内容は正しそうです.


そもそもなぜこのようなことが起きたかというと,awslabs/aws-fluent-plugin-kinesis を td-agent-gem でインストールしようとした際に

ERROR:  Error installing fluent-plugin-kinesis:
        google-protobuf requires Ruby version < 2.8.dev, >= 2.5.

というような内容が出てしまったのでした.

ちょっと調べてみると

github.com

という pull-request がすでに作られており,このdiff を見た感じ google-protobuf のバージョン指定が甘かったため,google-protobuf のマイナーバージョンが 3.12.0 に上がったタイミングで非互換が出てしまっていたということがわかりました *1.以下の protocolbuffers/protobuf に対する pull-request に拠るもののようです:

github.com


aws-fluent-plugin-kinesis に出ている pull-request がマージされないとどうしようもない感じがするのですが,そう待ってはおれんので以下のように「先に google-protobuf:3.11.4 をインストールしてしまう」ワークアラウンドを書いて現状は乗り切っています.

$ td-agent-gem install google-protobuf:3.11.4 --no-ri --no-rdoc
$ td-agent-gem install fluent-plugin-kinesis --no-ri --no-rdoc


以上です.はやく pull-request がマージされる (あるいは td-agent にバンドルされている ruby のバージョンが上がる)と良いですね.


[追記]

github.com

マージされたようです.3.2.2 以降のバージョンの aws-fluent-plugin-kinesis を利用すればこの問題は解決できそうです.

*1:他にも atlassian/fluent-plugin-kinesis-aggregation でも同じような問題が起きていた様子

Email::MIME::ContentType が build_content_type と build_content_disposition を提供するようになっていた

Perl の話です.

metacpan.org

Email::MIME::ContentType 1.023 (なお本バージョンは TRIAL Release となっています) 以降から build_content_typebuild_content_disposition という関数が追加されています.それぞれ名前の通り Content-TypeContent-Disposition を構築する責務を担っています.

テストコードから一部拝借すると,

use Email::MIME::ContentType;

my $content_type = build_content_type({
    type => 'text',
    subtype => 'plain',
    attributes => {
        charset => 'us-ascii'
    }
}); # => 'text/plain; charset=us-ascii'

my $content_disposition = build_content_disposition({
    type => 'attachment',
    attributes => {
        filename => 'genome.jpeg',
        'modification-date' => 'Wed, 12 Feb 1997 16:29:51 -0500'
    }
}); # => 'attachment; filename=genome.jpeg; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"'

というふうに使えるようになっています,ウオー便利ですね!


github.com
github.com

実に7年越しの feature request が結実した形になります *1.ありがとうございます!

*1:今となってはなぜこれが必要だったかを思い出せませんが……

Redis の Slave (Replica) の Expire は 4.0 RC3 以降信用して良くなっている

maedama.hatenablog.com
trapezoid.hatenablog.com

上記のブログには今から6年ほど前の当時の情報が記されていますが,Redis 4.0 RC3 以降の Slave (replica) の Expire は信用して良くなっているようです.
Redis の公式ドキュメント (Replication – Redis) を参照すると,

However note that writable replicas before version 4.0 were incapable of expiring keys with a time to live set. This means that if you use EXPIRE or other commands that set a maximum TTL for a key, the key will leak, and while you may no longer see it while accessing it with read commands, you will see it in the count of keys and it will still use memory. So in general mixing writable replicas (previous version 4.0) and keys with TTL is going to create issues.

Redis 4.0 RC3 and greater versions totally solve this problem and now writable replicas are able to evict keys with TTL as masters do, with the exceptions of keys written in DB numbers greater than 63 (but by default Redis instances only have 16 databases).

とあり,どうやらExpire が信用できない挙動は Redis 4.0 RC3 から修正されているようです.
どういうことかと言うと,「Slave 側ではデータは Purge しない。Master で データの Purge の Replication をまつ」という挙動が解消され,slave (replica) 側でも能動的に TTL の判断ができるようになっています.
つまり,slave (replica) に対して TTL 切れのアイテムに問い合わせたとしても,正しく expire することとなります.


というわけで検証してみます.Dockerを使ってやってみましょう.なお検証に用いたバージョンは 4.0.14 および 5.0.7 です (記載上は 4.0.14 を使用します).

まずは master (primary) 側の Redis server の Docker container を用意します.

FROM redis:4.0.14
COPY primary.redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

この時の primary.redis.conf は以下の通り:

bind 0.0.0.0

コンテナ間通信をさせる必要があるので network を作っておきます.

$ docker network create -d bridge redis-test

作った master (primary) の Redis server を起動しましょう.

$ docker run --net=redis-test -p 6379:6379 primary-redis:latest

コンテナの IP アドレスを確認しておきます:

$ docker network inspect redis-test
[
    {
        "Name": "redis-test",
        "Id": "379ae24b35454bc119d80f35d3dd40e3db6e1ec72579ddbba32ca322cec4293a",
        "Created": "2020-05-08T02:53:19.955015821Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "e75cbce6bce2e1a797780da5c7500aaebbc1b4cf0e28865854e6ba5c46e78b80": {
                "Name": "amazing_greider",
                "EndpointID": "a3ddb49a5406447b3f52757b7b9c6be91baa2d5d3a221ec3ef27af697860f595",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

172.18.0.2 了解.

それでは slave (replica) 側の Redis server の Docker container も作ってしまいましょう.

FROM redis:4.0.14
COPY replica.redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

この時の replica.redis.conf は以下の通り:

bind 0.0.0.0
slaveof 172.18.0.2 6379

slaveof に先ほど取得した master (primary) の Redis server の IP アドレスを入れておきます.

そしてこのコンテナも起動.

$ docker run --net=redis-test -p 16379:6379 replica-redis:latest
$ docker ps
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                     NAMES
f3efc8e79267        replica-redis:latest   "docker-entrypoint.s…"   3 seconds ago       Up 2 seconds        0.0.0.0:16379->6379/tcp   interesting_wright
52e66b665fe2        primary-redis:latest   "docker-entrypoint.s…"   4 seconds ago       Up 3 seconds        0.0.0.0:6379->6379/tcp    wonderful_tereshkova

上記のようにホストマシンのポート 6379 は master (primary) に,そして 16379 は slave (replica) の Redis server に繋がっていることとなります.


それでは Expire を利用するコードをホストマシンから流し込んで検証してみましょう.

$ redis-cli -p 6379 SET foo 1 EX 5 && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo && \
  sleep 6 && \
  redis-cli -p 16379 GET foo && \
  redis-cli -p 6379 GET foo
  • master (primary) にセット
  • master (primary) と slave (replica) でゲット
  • expire duration が経過するまで sleep
  • slave (replica) 側から再度ゲット (master (primary) が先ではないのが重要)
  • master (primary) 側から再度ゲット

というシナリオです.

結果としては

OK
"1"
"1"
# 6 秒経過
(nil)
(nil)

というふうになり,slave (replica) 側から GET を試みたとしてもちゃんとデータが purge されています.したがって「Slave 側ではデータは Purge しない。Master で データの Purge の Replication をまつ」という挙動は解消していることがわかります.

よかったですね!


おまけ

さて,前述のブログで id:maedama さんが指摘している,replication 遅延が生じた時の挙動についても見てみましょう.

tc を使って擬似的なパケット遅延を表現するので,master (primary) 側の Docker file に手を加えて iproute2 をインストールしておきます.

FROM redis:4.0.14
COPY primary.redis.conf /usr/local/etc/redis/redis.conf
RUN apt-get -y update && apt-get install -y iproute2
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

そしてコンテナを立ち上げてみます.tc で制御するためには --cap-add NET_ADMIN で権限を与える必要あり.

$ docker run --net=redis-test -p 6379:6379 --cap-add NET_ADMIN primary-redis:latest

次に tc で擬似的に master (primary) から slave (replica) 宛て (この場合 172.18.0.3/32) のパケットを 2000ms 遅延させる設定を加えてみます (678a54eb5d91 は primary-redis の container ID).

docker exec 678a54eb5d91 tc qdisc add dev eth0 root handle 1: prio
docker exec 678a54eb5d91 tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip dst 172.18.0.3/32 flowid 2:1
docker exec 678a54eb5d91 tc qdisc add dev eth0 parent 1:1 handle 2: netem delay 2000ms

ホストマシンから redis-cli で検証コマンドを流し込んでみましょう.

redis-cli -p 6379 SET foo 1 EX 3 && \
  sleep 2 && \
  redis-cli -p 6379 EXPIRE foo 3 && \
  sleep 2 && \
  redis-cli -p 6379 INCR foo && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo

このときの出力は以下のようになります:

OK
# 2秒後
(integer) 1
# 2秒後
(integer) 2
"2"
"1"

slave (replica) 側では EXPIRE foo 3 までは適用されている一方,2000ms の遅延によって INCR foo が反映されていないことがわかります.

さらに2秒待ってみましょう

redis-cli -p 6379 SET foo 1 EX 3 && \
  sleep 2 && \
  redis-cli -p 6379 EXPIRE foo 3 && \
  sleep 2 && \
  redis-cli -p 6379 INCR foo && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo && \
  sleep 2 && \
  redis-cli -p 6379 GET foo && \
  redis-cli -p 16379 GET foo

出力はこのような感じ

OK
# 2秒後
(integer) 1
# 2秒後
(integer) 2
"2"
"1"
# 2秒後
(nil)
"2"

INCR foo まで slave (replica) に反映されたことが見て取れます.slave (replica) 側は自前で TTL を持っているような感じに見えますね.この場合は素朴なレプリケーション遅延が起きるような感じがします.