にゃみかんてっくろぐ

猫か百合を見守る壁になりたい

Traefik でお手軽に Docker コンテナの無停止デプロイを実現した

3 行でまとめると

  • Traefik を使って Docker Compose だけで無停止デプロイを実現
  • docker-compose.yml に定義するだけ、他の設定ファイルは不要のお手軽さ
  • コンテナは graceful shutdown するようにしておく

docker-compose.yml

ざっくり作り上げた docker-compose.yml がこちらです。

gist.github.com

これだけで動きます。リクエストを投げながら nginx-blue, nginx-green のどちらか一方を落としてもエラーになることはありません。

f:id:no_clock:20200603225248p:plain
nginx-blue を停止してもエラーなくレスポンスし続けている様子

Traefik とは

Traefik はある種のリバースプロキシです。ただし、 HAProxy や nginx と違ってサービス検出機能が備わっているほか、環境変数メタデータ (Docker の場合は labels) で設定ができるのも特徴的です。

サービス検出機能と 仲良く なる必要はあるものの、設定の記述量が少なくて済みます。 Docker の場合は、 Traefik 自身が Docker デーモンにアクセス出来るようにします。

    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

ちょっと気をつけたいところ

  1. 公式ドキュメントのみを頼ったほうが良いです。Google 検索で調べても情報は出てきますが、 frontendbackend といった v1.x 系列の設定を取り上げた記事がヒットして非常に混乱します。
  2. EntryPoints, Routers, Services と複数段定義する必要があり、とっつきにくい印象があるかもしれません。めちゃくちゃ端折って Nginx と対比すると下記の通りです、難しくないと思い込みましょう。
Traefik Nginx
EntryPoints listen ディレクティブ
Routers server ディレクティブ(ホスト名) と location ディレクティブ
Services upstream ディレクティブ

無停止デプロイする

さて、無停止デプロイをしましょう。ポイントは 2 点です。

  • コンテナが graceful shutdown *1 するか、リクエストが冪等であること。
    • traefik はクラウドロードバランサーサービスにあるような接続ドレインの機能を持ちません。コンテナ自身が受け付けたリクエストを正しく処理しきることが望ましく、それが出来ない場合はリクエストの処理を冪等にしておく必要があります。
  • Retry middleware を使う

冒頭の docker-compose.yml では、 nginx に SIGQUIT を投げることで graceful shutdown を実現しています。

    image: "nginx:alpine"
    stop_signal: SIGQUIT
    labels:
(略)
      - "traefik.http.middlewares.nginx-retry.retry.attempts=4"

healthcheck はなくても大丈夫

traefik には healthcheck 機能があり、これを使ったコンテナの切り離しも可能です。

    labels:
(略)
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.path=/"
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.interval=1s"
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.timeout=1s"

ただし、 Retry middleware を使うことで下記の動きとなるため、 healthcheck を設定しなくても問題はありません。

  1. Listen をやめた停止中のコンテナにルーティング
  2. コネクションが確立出来ないため、即座にリトライ
  3. 実行中のコンテナにルーティング

メモリ使用量

かなり雑ですが、 1MB のファイルを 100 同時接続で計 10,000 リクエスト、 10GB 分の転送をさせました。

$ ab -n 10000 -c 100 -H "Host: localhost" http://localhost/
# 起動直後
$ docker stats
CONTAINER ID        NAME                    CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
903a420aa79c        traefik_nginx-blue_1    0.00%               1.898MiB / 592.5MiB   0.32%               800B / 42B          6.21MB / 0B         2
bb10478ecb4b        traefik_traefik_1       0.03%               10.18MiB / 592.5MiB   1.72%               800B / 0B           46.3MB / 0B         6
7f123e90926c        traefik_nginx-green_1   0.00%               1.777MiB / 592.5MiB   0.30%               578B / 0B           348kB / 0B          2

# リクエスト後
$ docker stats
CONTAINER ID        NAME                    CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
903a420aa79c        traefik_nginx-blue_1    0.00%               2.191MiB / 592.5MiB   0.37%               4.28MB / 5.21GB     7.27MB / 0B         2
bb10478ecb4b        traefik_traefik_1       0.00%               25.42MiB / 592.5MiB   4.29%               10.5GB / 10.5GB     46.8MB / 0B         19
7f123e90926c        traefik_nginx-green_1   0.00%               2.293MiB / 592.5MiB   0.39%               4.22MB / 5.22GB     1.4MB / 0B          2

nginx のメモリ使用量の少なさが際立ちますが、 traefik も 25MB に収まっています。実行中も 30MB を超えることはありませんでした。

参考

*1:明確な定義はないですが、ここでは、 Listen をやめ新たなリクエストを受け付けないようにした後、既存のリクエストを処理しきってから終了する振る舞いを指します。参考: Apache HTTP Server の停止と再起動 - Apache HTTP サーバ バージョン 2.4

個人開発サービスのログをぜんぶ CloudWatch Logs にまとめた

3 行でまとめると

  • ログを CloudWatch Logs に集約した
  • Metric filter でエラー数をメトリクス化した
  • CloudWatch Alarms ではなく Lambda でエラーを Slack 通知させるようにした

CloudWatch Logs にログを集約した

zenrei.nyamikan.net をすべて Docker 化した のに続いて、 P2P 地震情報にゃみかんの個人開発サービスのログを CloudWatch Logs に集約しました。

むかし

これまでは MonitXymon (旧 Hobbit ) を使ったログ監視を行っていました。

f:id:no_clock:20200531163107p:plain
旧構成

ログファイルとアラート対象のキーワードを指定するシンプルな仕組みでしたが、 長期運用するには少し面倒 でした。あと使っているものが古い 。

  • ディスクフル。気がつくとログがいっぱいになりがち。ログは残しておきたいので logrotate で消すわけにもいかず、定期的に退避する仕組みを用意するのも面倒で、手で退避させる作業を時々やっていた。
  • 再設定の手間Debian GNU/Linux の LTS リリースの度にサーバを構築し直しているが、 2 年に 1 回なのでいつも設定方法を忘れて四苦八苦していた。

いま

いまは CloudWatch へ集約した形にしました(その他、大部分を Docker コンテナ化してポータビリティを高めています)。

f:id:no_clock:20200531151932p:plain
新構成

ログ収集

CloudWatch エージェント もしくは "awslogs" Docker ログドライバ を用いて CloudWatch Logs にログを集約します。この際、サーバ起動時に logs.*.amazonaws.com が解決できない問題にハマりました。 Docker サービス起動前に 30 秒のスリープを入れる雑な方法で対処しています。

エラーの検出

エラー検出には、 CloudWatch のメトリクスフィルタを使用しています。特定のキーワードがあると、メトリクスがカウントされます。

f:id:no_clock:20200531171226p:plain
エラーがメトリクスでカウントされている様子

20 弱あるロググループを手で設定するのは大変なので、 AWS CDK (C#) でコード化しています(参考: AWS CDK for .NET: C# で CloudWatch Metric Filter を設定する - にゃみかんてっくろぐ)。

エラーの通知

ふつうはメトリクスを CloudWatch Alarms で監視させますが、今回は使いませんでした。 CloudWatch Events で Lambda をキックして Slack に通知させています。

  • アラームメトリクスあたり $0.1/month で、今後メトリクスを増やすとそこそこのお値段になってしまう。
  • 個人開発で Lambda を有効活用してみたい。

合理的というよりは 興味本位 の選択です。

コードは Gist に置いていますが、不慣れな JavaScript && 動けばいいやの精神なので可読性はお察しください → Notify CloudWatch Metrics to Slack (typewriter/metrics-to-slack.js)

f:id:no_clock:20200531172812p:plain
ゆかりさんが Slack にエラー発生を通知してくれている図

おねだん

気になるお値段はざっとこんな感じです。

サービス 月額料金 備考
CloudWatch Logs ログ収集 $0.76 無料枠 5GB/ 月 を超え、毎月 6GB ほどのログを収集している
CloudWatch Logs ログ保管 $0.00 無料枠 5GB/ 月 以下(まだ運用して 1 ヶ月ほどで蓄積がないため)
CloudWatch Metrics $0.00 無料枠 10 メトリクス / 月以下。メトリクスフィルタはデータポイントが発生しない部分は課金されない様子
CloudWatch Events $0.04 ? 無料枠なし。まだ請求に上がって来ていないが、カスタムイベント扱い・毎分実行の場合で計算
Lambda $0.00 実行回数 43,200 回(無料枠 1,000,000 回)、実行時間 3,240 GB 秒(無料枠 400,000 GB 秒)

感想

総合すると、そこそこ楽になりました。

  • よいところ
    • ディスクフルを気にしなくていい
    • 1 箇所に集約されている安心感
    • 「とりあえず CloudWatch Logs に流しちゃお」というデフォルトの選択肢が出来た
  • わるいところ
    • 雑に作った通知がエラー数だけで、エラー内容は見に行かないと行けない
  • どちらでもないところ
    • 毎月 6GB もログが発生していることに初めて気づいた

AWS CDK for .NET: C# で CloudWatch Metric Filter を設定する

.NET サポートあるじゃん」と思ったのでちょっと触ってみました。

手順とコード

  1. Getting Started With the AWS CDK - AWS Cloud Development Kit (AWS CDK) を参照して npm で AWS CDK をインストールする。
  2. Working with the AWS CDK in C# - AWS Cloud Development Kit (AWS CDK) を参照してプロジェクトを新規作成し、 NuGet から Amazon.CDK と Amazon.CDK.AWS.Logs パッケージを追加する。
  3. 書く。
  4. 先程のドキュメントや新規作成したプロジェクトの README.md に記載されている通り、 cdk deploy で適用する。
using Amazon.CDK;
using Amazon.CDK.AWS.Logs;

namespace P2PQuakeCDK
{
    public class P2PQuakeCDKStack : Stack
    {
        internal P2PQuakeCDKStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
        {
            new CfnMetricFilter(this, "www.p2pquake.net/p2pquake-server/error", new CfnMetricFilterProps()
            {
                FilterPattern = "ERROR",
                LogGroupName = "/vps/p2pquake.net/p2pquake-server",
                MetricTransformations = new CfnMetricFilter.IMetricTransformationProperty[]
                {
                    new CfnMetricFilter.MetricTransformationProperty()
                    {
                        MetricName = "www.p2pquake.net/p2pquake-server/error",
                        MetricNamespace = "app",
                        MetricValue = "1"
                    }
                }
            });
        }
    }
}

触ってみて

Node.js からは逃れられない

いきなり残念なお知らせですが、 .NET バインディングを使う場合でも Node.js からは逃れられません。

All CDK developers need to install Node.js (>= 10.3.0), even those working in languages other than TypeScript or JavaScript. The AWS CDK Toolkit (cdk command-line tool) and the AWS Construct Library are developed in TypeScript and run on Node.js.

Getting Started With the AWS CDK - AWS Cloud Development Kit (AWS CDK)

公式ドキュメントが若干怪しい

例えば CfnMetricFilter クラスのコンストラクタに投げ込む ICfnMetricFilterProps 型を見てみると、 MetricTransformations がなんと object 型になっています。

object MetricTransformations { get; }

Interface ICfnMetricFilterProps

コンソールでは、メトリクス名やメトリクス値を指定するはずの部分です。

f:id:no_clock:20200402000017p:plain
コンソールでの表示

困り果ててしまってアセンブリ ブラウザーで覗いてみると、 CfnMetricFilter.MetricTransformationProperty 型を投げ込めそうに見えます。実際、これで解決しました。

f:id:no_clock:20200402000613p:plain
アセンブリブラウザー ICfnMetricFilterProps

言語共通の API Reference に記載されている Type name も間違っているようです(ネストされていることが表現されていない)。

.NET Amazon.CDK.AWS.Logs.MetricTransformationProperty

interface MetricTransformationProperty · AWS CDK

コントリビューションチャンスか? ちょっと様子を伺ってみようと思います。

参考

ぜんぶ Docker コンテナにする (HTTPS+IPv6 対応 )

事例が何かの参考になればと思ったので記事にしました。

3行でまとめると

すべて Docker 化

個人開発のサービスを省力運用にするべく、 Zenrei (zenrei.nyamikan.net) をすべて Docker コンテナ化しました。

f:id:no_clock:20200308221840p:plain
構成図

docker-compose.yml とそれぞれの Dockerfile はリポジトリに公開しています。

nginx (host network)

https-portal の節で紹介します。

nginx (bridge network)

フロントエンドの静的ファイル配信と、サーバサイドアプリケーションへのリバースプロキシを担います。マルチステージビルドを使用し、 Vue.js の生成物のみを nginx に持ってきてイメージサイズを 23MB と小さくしています。

view/Dockerfile

FROM node:slim as builder
WORKDIR /app
ADD package*.json ./
RUN npm install
ADD . /app
RUN npm run build

FROM nginx:alpine
ADD nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist /app

api

サーバサイドアプリケーションです。 nginx と同様にマルチステージビルドとしていますが、日本語 WordNet の辞書を用いている関係でイメージサイズは 290MB あります。

aaaton/golem 辞書 (Git LFS) を取っている箇所は妥協しています( go build で Git LFS のデータがどうしても取得できなかった)。個人開発は動けばよかろうの精神であまり深追いしていません。

api/Dockerfile

FROM golang:latest as builder
WORKDIR /go/src
RUN apt-get update && apt-get install -y git-lfs && git lfs install --skip-repo
RUN curl http://compling.hss.ntu.edu.sg/wnja/data/1.1/wnjpn.db.gz > wnjpn.db.gz && gunzip wnjpn.db.gz
RUN mkdir -p /go/src && git clone https://github.com/aaaton/golem.git /go/src/golem
ADD . /go/src
RUN go build . && ls -l /go/src

FROM debian:buster-slim
WORKDIR /go
COPY --from=builder /go/src/api /go/src/wnjpn.db ./
CMD ["./api"]

mongo

ソースコード内の変数名の使用実績などを永続化している MongoDB です。こちらは公式イメージをそのまま使用しています。

https-portal

上記コンテナ群だけでは、 HTTPS で提供することはできません。そこで登場するのが https-portal です。 https-portal は、下記の機能を持った nginx のリバースプロキシ Docker コンテナです。

  • 環境変数による複数ドメインのシンプルなリバースプロキシ
  • Let's Encrypt を用いた https 化(証明書は自動取得・自動更新)

通常は nginx や certbot や cron をゴニョゴニョしなければなりませんが、これを使うと docker-compose.yml に十数行書くだけで済みます (Quick Start 参照 ) 。他のオーケストレーションツールを追加せずに Docker / Docker Compose で完結するのも嬉しいポイントです。

version: "3"

services:
  https-portal:
    image: steveltn/https-portal:latest
    network_mode: host
    ports:
      - "80:80"
      - "443:443"
    environment:
      DOMAINS: "zenrei.nyamikan.net -> http://127.0.0.1:8080"
      STAGE: "production"
      LISTEN_IPV6: "true"

IPv6 対応

https-portal の nginx が IPv4 でしかリッスンしていなかったため、 IPv6 でリッスンするようプルリクエストを出しました(マージ済み🎉)。

github.com

IPv6 と docker-proxy

…ただし、 IPv6 でリッスンしなくても、 IPv6 で通信を受け付けること自体は可能です。ブリッジネットワークでコンテナを起動してポートを開放すると、 IPv4 は NAT で、 IPv6 は docker-proxy による IPv4 への変換で、それぞれ通信がされるようです。

$ sudo iptables -L -n
Chain DOCKER (3 references)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            172.18.0.2           tcp dpt:3001

$ sudo netstat -anp
稼働中のインターネット接続 (サーバと確立)
Proto 受信-Q 送信-Q 内部アドレス            外部アドレス            状態        PID/Program name
tcp6       0      0 :::3001                 :::*                    LISTEN      9181/docker-proxy

この状態で IPv6 通信を行うと、ブリッジアダプタの IPv4 アドレスが通信元になってしまい、それがアクセスログに記録されてしまいます。

172.18.0.1 - - [08/Mar/2020:04:56:33 +0000] "GET /favicon.ico HTTP/2.0" 200 6183

そこで、ホストネットワークでコンテナを起動しつつ、 IPv6 で nginx をリッスンさせることで、これを解消しました。

240f:<MASKED>:7194 - - [08/Mar/2020:07:01:14 +0000] "GET /favicon.ico HTTP/2.0" 200 6183

もう一度まとめると

参考

CRCチェックつきでAM2320の温湿度を読み取る (Raspberry Pi + Python 3)

温湿度センサモジュール AM2320の温湿度を取得するサンプルコードは数あれど、CRCチェックまでやっているコードが見当たりません。

ということで、データシートを見つつ書きました。

import smbus
import time

i2c = smbus.SMBus(1)
address = 0x5c

# Step one: Wake Sensor
try:
    i2c.write_i2c_block_data(address, 0x00, [])
except:
    pass
time.sleep(0.003)

# Step two: Send the read command
i2c.write_i2c_block_data(address, 0x03, [0x00, 0x04])

# Step three: To return the data read
time.sleep(0.015)
block = i2c.read_i2c_block_data(address, 0, 8)

# Check CRC
crc = 0xFFFF
for i in range(6):
  crc ^= block[i]
  for j in range(8):
    if (crc & 0x0001 == 1):
      crc >>= 1
      crc ^= 0xA001
    else:
      crc >>= 1

if block[7] << 8 | block[6] != crc:
  raise RuntimeError("CRC error")

# Print values
humidity = float(block[2] << 8 | block[3]) / 10
temperature = float(block[4] << 8 | block[5]) / 10

print(humidity, "%")
print(temperature, "°C")
$ python3 read_am2320.py
33.9 %
21.3 °C

参考

Linux コマンドの最長しりとりを求める

Linux コマンドでしりとりをすると、最長でいくつ繋がるのか?」

この疑問を解消すべく、最長しりとりを求めるプログラムを実装しました。

結論

最長しりとり問題

論文「最長しりとり問題の解法」

全パターン網羅して最長を決める… といきたいところですが、単語数が増えてくると数時間掛けても解けません。

調べてみると、なんと論文になっています。整数計画問題として定式化した上で、分枝限定法により解を得るものです。さらには、既に実装済みのWebサービスソースコードまであります。

しかし、 Crystal を触りたい、という別の目的から車輪の再発明をすることにしました。

Crystal 実装

ソースコード

論文を読み解きつつ再発明した実装は、 GitHub に公開しています(ソルバーに GLPK を使用しています)。やみくもに木で探索するものも入っています。

Crystal 言語

Crystal は、 Ruby に非常に似た文法を持つ、型推論の付いた静的型付け言語です。

private def self.find_closed_path(v : Char, x : Hash(String, Int32), k : Int32)
  edges = x.select { |k, v| v > 0 }.map { |e| e[0] }
  find_closed_path_recursively(v.to_s, edges, k)
end

型の指定方法、キャスト、プロパティの指定方法などは公式ドキュメントを参照しましたが、それ以外は Ruby のつもりで書けました。それくらい似ています。

ただし、 gem は使えません( C バインディングは使えます)。「文法が似ている全く別の言語」であることを思い出させてくれます。

結果

各種 Docker イメージの PATH に含まれているコマンドを抽出し、最長しりとりを求めました(いずれも数秒程度で解き終わりました)。

Docker image:tag コマンド数 しりとり長 使用率
alpine:3.10.3 317 173 54.6%
amazonlinux:2.0.20191016.0 261 146 55.9%
centos:8 573 326 56.9%
debian:10.2 409 231 56.5%

なんと CentOS 8 では 300 以上のコマンドが繋がります。 Amazon Linux が Alpine Linux よりもコマンド数が少ないなど、しりとりとは無関係に興味深い点もあります。

以下に、しりとりの内容を記載します。 w または z で始まり、数字で終わっている点はどのディストリビューションも共通です。

Alpine Linux 3.10.3

Length: 173
Shiritori: whoami => ipcs => strings => setlogcons => setkeycodes => swapoff => fgrep => powertop => pmap => pgrep => pwd => depmod => dd => dc => crontab => brctl => lzma => add-shell => login => nologin => nsenter => rmdir => rm => microcom => md5sum => mkdir => reset => timeout => test => ttysize => ether-wake => env => volname => eject => touch => halt => tr => realpath => hdparm => modinfo => openvt => truncate => expr => raidautorun => nl => logger => rfkill => lzopcat => tunctl => lzcat => tail => ln => nmeter => remove-shell => lsusb => bc => chown => nproc => comm => mpstat => tac => crond => dnsdomainname => expand => dirname => ed => deluser => rmmod => date => echo => od => deallocvt => tar => readahead => dumpkmap => passwd => delgroup => ping => gzip => patch => hexdump => pkill => lzop => printenv => vconfig => gunzip => pwdx => xzcat => top => pscan => nc => cksum => mkpasswd => du => unlzma => arping => getopt => true => egrep => printf => fsync => cal => lsof => fdflush => hostid => diff => fbsplash => head => df => fstrim => mesg => getconf => fsck => killall => losetup => poweroff => flock => klogd => dmesg => grep => pidof => findfs => sysctl => ls => setserial => less => sum => mkmntdirs => sha512sum => mkdosfs => su => unix2dos => sync => clear => run-parts => slattach => hd => dumpleases => sendmail => ldconfig => groups => sha3sum => mktemp => ps => sha256sum => mkswap => pipe_progress => sha1sum => mknod => dos2unix => xargs => shuf => fuser => rev => vi => ifconfig => getent => tty => yes => scanelf => fatattr => readlink => kill => lspci => ipcalc => cryptpw => whois => swapon => nameif => factor => rdev => vlock => killall5

Amazon Linux 2.0.20191016.0

Length: 146
Shiritori: zic => curl => ln => nohup => pwd => dd => db_load => diff => fg => gpg => gpg-error => rmdir => rpm => md5sum => mkdir => rpmkeys => sotruss => stat => tzselect => tsort => tset => trust => tput => timeout => test => tabs => split => tty => yes => signver => reset => tr => rpmdb => bg => gsettings => sum => makedb => bashbug => groups => stdbuf => false => ex => xmlcatalog => glib-compile-schemas => sprof => factor => rview => whoami => iconvconfig => getpcaps => shuf => fold => df => find => db_printlog => gdbm_load => dir => read => db_tuner => realpath => hostid => db_recover => rm => mknod => db_stat => truncate => expand => db_checkpoint => touch => head => dircolors => sha512sum => mkfifo => oldfind => db_hotbackup => pldd => db_dump => paste => egrep => printf => fgrep => pinentry-curses => ssltap => pr => rvi => infotocap => printenv => vi => infocmp => pinky => yum => mktemp => ptx => xargs => sleep => python => nice => expr => runcon => numfmt => true => echo => od => du => users => sha384sum => mv => vdir => rpcgen => nl => ls => sha256sum => modutil => lua => applygnupgdefaults => setcap => pk12util => ldconfig => getopts => sdiff => fmt => tail => luac => chown => nproc => cp => pydoc => cut => tic => chkconfig => gdbus => sync => crlutil => localedef => fc => cmp => p11-kit => tac => cmsutil => logname => env => view => wc => certutil => ldd => diff3

CentOS 8

Length: 326
Shiritori: zic => cracklib-unpacker => runuser => rtpr => rmdir => rmmod => dmfilemapd => depmod => dd => dbus-send => db_load => dnf => fg => gpg => groupdel => loginctl => localectl => lastb => busctl => logname => egrep => pmap => pgrep => ps => systemd-tmpfiles => systemd-sysusers => systemd-cgls => strings => ss => sotruss => systemd-delta => as => sum => md5sum => mksquashfs => sha512sum => mkfs.cramfs => sha384sum => mkfs => systemd-run => nologin => newusers => su => users => systemd-machine-id-setup => prlimit => tzselect => tsort => trust => timeout => test => telinit => taskset => tracepath => hash => halt => top => pinky => yum => mktemp => pwunconv => vi => ipcs => swapon => nohup => pwconv => vipw => w => wipefs => sulogin => newuidmap => pwscore => elfedit => type => eject => truncate => echo => objcopy => ypdomainname => evmctl => lesspipe.sh => hostnamectl => login => nl => losetup => pkill => lsns => systemctl => lslogins => sysctl => lslocks => swaplabel => ls => skill => less => setfacl => lnstat => timedatectl => last => tail => ldattach => hostname => ethtool => ldconfig => gpgparsemail => lastlog => gsettings => sg => groups => systemd-hwdb => bg => gapplication => namei => ifcfg => groupmems => systemd-cgtop => pkg-config => glib-compile-schemas => strip => ping => getpcaps => setpriv => vmcore-dmesg => getfacl => lsmem => makedb => bashbug => getopts => systemd-path => hexdump => pwdx => xmlcatalog => gprof => fips-finish-install => localedef => fips-mode-setup => printf => fgrep => poweroff => fsck.cramfs => swapoff => findfs => stdbuf => fstrim => mesg => gpgconf => fsck.minix => xzless => sprof => fold => df => find => dbus-test-tool => lsmod => dwp => pwd => dmsetup => pldd => dracut => tload => dmesg => genl => lsinitrd => db_printlog => gdbmtool => ldd => dbus-uuidgen => nisdomainname => expand => dbus-run-session => nm => mknod => dirmngr-client => touch => hostid => db_hotbackup => pkgconf => fsck => kmod => du => update-crypto-policies => sha256sum => mkinitrd => db_dump => pidof => flock => kill => ld.gold => dhclient-script => tty => yes => sha224sum => mkdumprd => dhclient => true => ex => x86_64-redhat-linux-gnu-pkg-config => gdbus => sha1sum => modinfo => od => dirmngr => read => dir => routef => fix-info-dir => resolvconf => factor => runlevel => logger => rpm => mkhomedir_helper => rm => mkdir => runcon => nsenter => rpmdb => blkid => dbus-monitor => ranlib => blkdiscard => db_tuner => realpath => head => db_recover => rtmon => nice => expr => rview => watchgnupg => gtar => rpmkeys => sleep => pwhistory_helper => routel => ld.bfd => delpart => tr => rfkill => ld => dbus-update-activation-environment => tar => raw => whoami => iconvconfig => gpg-error => rdma => applygnupgdefaults => shutdown => newgrp => printenv => vigr => readelf => fdisk => kernel-install => ln => newgidmap => pivot_root => tee => env => vdir => rdisc => coreutils => sync => command => dbus-daemon => nproc => chpasswd => dmstats => sha512hmac => chmod => dircolors => sha384hmac => chgpasswd => dbus-cleanup-sockets => sha256hmac => cd => db_stat => tipc => chkconfig => getconf => fc => capsh => hwclock => kexec => comm => mountpoint => tac => colrm => mkfs.minix => xzdec => cracklib-packer => rvi => ipcalc => chcpu => update-alternatives => sha224hmac => cksum => mv => view => wc => cp => pr => resolvectl => lsipc => chgrp => ptx => xargs => sha1hmac => chmem => mkswap => partx => xz => zless => shuf => faillock => kdumpctl => lscpu => unxz => znew => whereis => setterm => mkfifo => objdump => p11-kit => tracepath6

Debian 10.2

Length: 231
Shiritori: znew => wc => ctrlaltdel => ldconfig => groupmod => dd => diff => fgrep => paste => e2image => expr => runuser => rmt-tar => rmdir => rm => md5sum => mkfs.cramfs => ss => swapon => nologin => newusers => switch_root => tzselect => tsort => tset => tput => timeout => test => taskset => tarcat => tune2fs => stat => tabs => su => users => split => tty => yes => sum => mkfs.bfs => sha512sum => mkfs => sha384sum => mke2fs => sha256sum => md5sum.textutils => sha224sum => mkhomedir_helper => runcon => nsenter => rtstat => tr => run-parts => sha1sum => mkdir => resize2fs => sort => tar => rtcwake => e4crypt => truncate => expiry => ypdomainname => e2mmpstatus => script => true => env => vigr => renice => egrep => pr => rgrep => pager => rtmon => nohup => ptx => xargs => sulogin => newgrp => printf => factor => routef => fsck.cramfs => swapoff => findfs => stdbuf => fold => df => find => dash => hostid => delgroup => pwd => dumpe2fs => shred => dircolors => setsid => debugfs => sed => du => usermod => dpkg-deb => b2sum => mknod => debconf => fstrim => mklost+found => dpkg-trigger => readprofile => expand => dpkg-maintscript-helper => realpath => head => debconf-set-selections => sleep => policy-rc.d => debconf-apt-progress => setcap => pldd => dir => rbash => hostname => echo => od => dpkg => groupadd => dmesg => getconf => filefrag => gzip => ping => groups => shadowconfig => groupmems => sg => getpcaps => savelog => getopt => tzconfig => gpasswd => debconf-copydb => bashbug => getent => toe => e4defrag => gunzip => pidof => faillog => groupdel => lastlog => genl => login => nl => losetup => perl => lsattr => routel => logger => rename.ul => lsns => swaplabel => lscpu => userdel => ldd => deluser => remove-shell => localedef => fmt => tail => ln => namei => installkernel => lslogins => shuf => findmnt => tempfile => e2label => lslocks => setterm => mesg => grep => passwd => deb-systemd-helper => rdma => add-shell => lsipc => chown => nproc => comm => mountpoint => tipc => cksum => mount => tic => choom => mktemp => prlimit => tc => chmem => mkswap => pivot_root => tac => cppw => wdctl => lsmem => mv => vipw => wipefs => sync => chcpu => useradd => debconf-show => wall => ls => sdiff => fdformat => tee => e2freefrag => getcap => pwunconv => vdir => raw => whoami => install => lastb => badblocks => start-stop-daemon => numfmt => touch => hwclock => killall5

ふたたび結論

紅葉スポットと紅葉状況をツイートから推定する

ひとり開発 Advent Calendar 2019 10日目の記事です。

成果物: ソーシャル紅葉見頃情報

Webサービスソーシャル紅葉見頃情報」を作りました。

f:id:no_clock:20191209230023p:plain

動機: 紅葉情報サイトの「見頃」表示が信用できない

以下の写真は、複数の紅葉情報サイトで「紅葉状況:見頃」だった時の御岳昇仙峡(山梨県)の様子です。

f:id:no_clock:20191209230525j:plain

正直、見頃はまだ先だと感じました。Twitterを調べても同様の感想を持った方が多いようで、甲府市観光課も「全体的に3分〜4分付き」と表現されている状況でした。

せっかく紅葉を見に行くなら、見頃のピークに行きたいものです。

しかし、紅葉情報サイトの「見頃」は判定が緩いようで使えません。Twitterで流れているツイートのほうが、はるかに信用できそうです……… ということは、ツイートを活用すれば、より厳しく「見頃」を判断出来る可能性がありそうです。

作ることにしました。

解説: サービス構成

サービスの全体像は以下の通り。ぜんぶ書き慣れている Ruby で楽しく書きました。

f:id:no_clock:20191208204338p:plain

それぞれ順を追って説明していきます。

ツイート収集

「紅葉」「見頃」「色づき」などのキーワードで、紅葉状況と思わしきツイートをひたすらに収集します。

紅葉スポットの自動抽出

紅葉スポットは、それこそ ウォーカープラスの紅葉情報ウェザーニュースの紅葉Ch. から集める方法もありそうですが、依存したくないので ツイートから紅葉スポット名も抽出 します。

形態素解析: MeCab + mecab-ipadic-NEologd

ツイートを形態素解析エンジン MeCab に掛けて 固有名詞 を拾います。辞書には、最新の固有表現が多数採録されている mecab-ipadic-NEologd を用います。例えば、福島県会津地方にある景勝地塔のへつり」も正しく抽出できます。

# 標準の辞書 (mecab-ipadic)
$ echo "福島県南会津郡下郷町、塔のへつり。" | mecab -d /usr/local/lib/mecab/dic/ipadic/
福島  名詞,固有名詞,地域,一般,*,*,福島,フクシマ,フクシマ
県  名詞,接尾,地域,*,*,*,県,ケン,ケン
南会津  名詞,固有名詞,地域,一般,*,*,南会津,ミナミアイヅ,ミナミアイズ
郡  名詞,接尾,地域,*,*,*,郡,グン,グン
下郷  名詞,固有名詞,地域,一般,*,*,下郷,シモゴウ,シモゴー
町  名詞,接尾,地域,*,*,*,町,マチ,マチ
、  記号,読点,*,*,*,*,、,、,、
塔の  名詞,固有名詞,地域,一般,*,*,塔の,トウノ,トーノ
へ  助詞,格助詞,一般,*,*,*,へ,ヘ,エ
つり  名詞,一般,*,*,*,*,つり,ツリ,ツリ
。  記号,句点,*,*,*,*,。,。,。
EOS
# 今回使用する辞書 (mecab-ipadic-NEologd)
$ echo "福島県南会津郡下郷町、塔のへつり。" | mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/
福島県  名詞,固有名詞,地域,一般,*,*,福島県,フクシマケン,フクシマケン
南会津  名詞,固有名詞,地域,一般,*,*,南会津,ミナミアイヅ,ミナミアイズ
郡  名詞,接尾,地域,*,*,*,郡,グン,グン
下郷町  名詞,固有名詞,地域,一般,*,*,下郷町,シモゴウマチ,シモゴーマチ
、  記号,読点,*,*,*,*,、,、,、
塔のへつり  名詞,固有名詞,一般,*,*,*,塔のへつり,トウノヘツリ,トーノエツリ
。  記号,句点,*,*,*,*,。,。,。
EOS

ここから固有名詞を拾って、「福島県」「南会津」「下郷町」「塔のへつり」が紅葉スポットの候補となります。

都道府県推定

紅葉スポットの候補に対し、都道府県を推定します。

方法はシンプルで、『京都の南禅寺』のように都道府県名が一緒になっているものを拾い上げ、その最頻値を推定結果としているだけです。ただし、「高尾山口」が「山口」になったり「東京都」が「京都」になったりしないよう、一部正規表現で避けています。

ノイズ除去

紅葉スポットの候補から、ノイズと思われる名称を除外します。

  • 都道府県の推定が分散しすぎている場合
    • 例えば「植物園」は全国にあり、特定のスポットを指していないので除外したい
  • 半角文字を含むか、漢字が1文字も含まれない場合
    • 「1日」「エモい」なども固有名詞として候補に挙がっているため除外したい

紅葉状況の推定

ここまでで紅葉スポットの候補が定まったため、紅葉状況を推定します。

スコア算出

ツイートに含まれる「紅葉状況っぽいキーワード」を抽出し、 0(青葉)〜1(見頃)〜2(落葉) の間で数値化します。

正直ここはかなりゴリ押しです。たとえば 「早かった」は0.4、「もう少し」は0.7、「ちょうど」は1、 のようにで数値を設定しています。複数あれば平均を取ります。

Webサービス

これで紅葉スポット・紅葉状況の情報が揃ったため、Webサービスとして提供します。息を吸うようにフロントエンドを書ける人間ではないので、古き良き非SPAでの実装です。

非紅葉スポットフィルタ(雑)

ノイズ除去をしても紅葉スポット「でない」固有名詞が大量に残るため、ここでフィルタします。ひたすらキーワードでフィルタを掛ける一時しのぎで、改善の余地が残る部分です。

place !~ /^(.+観光客|外国人.+|最盛期|今日この頃|.+気温|何度|四季桜|可能性|寒い朝|世界遺産|誕生日|今シーズン|黄緑|[0-9]+|皇室献上|.+観光|お久しぶり|開催中|全国的|敷地内|温暖化|五平餅|お勧め|青紅葉|.+|.+|.+流星群|.+|.+旅行|暑さ|.+|桜の木|桜を見る会|ロケ地|.+公開|女子力|仙山線|お題|お姉ちゃん|外国人|私たち|朝活|好きだ|平野部|数年|異常気象|トロッコ列車|知らんけど|山手線|予防接種|午前中|募集中|.週間|観光.+|冬桜|落葉高木|今月末|.+市内|目的地|質問箱|.+|.+年前|リア充|.|分からん|.+希望|オフ会|聖地巡礼|子どもたち|大嘗宮|代表者|落羽松|つけ麺|定点観測|入場料|競馬場|.|.|.?週末|冷たい雨||お客さん|時間帯|)$/

検証: 紅葉状況の推定結果

約3週間動かしたので、推定結果を見ておきます。と言っても、Twitterのツイート群からの推定結果とツイートを比べるので出来レースです。

高尾山

f:id:no_clock:20191208212934p:plain

東京 高尾山の推定結果ですが、11/27頃から見頃に近く、12/7頃に見頃を過ぎようとしているようです。高尾ビジターセンターのTwitterアカウントと比べてみます。

11/16 見頃間近

11/24 見頃

12/1 見頃

12/6 落葉進む

推定結果「11/27頃から見頃に近く、12/7頃に見頃を過ぎ」は実際の様子とかなり近そうです。

東福寺

f:id:no_clock:20191208212922p:plain

京都 東福寺は、11/23〜11/30頃で見頃間近〜見頃を行ったり来たりしています。12/1以降は見頃過ぎのようです。公式Twitterアカウントがないため、ユーザのツイートと比べてみます。

11/16 見頃間近

11/23 見頃

11/27 見頃

12/1 落葉始まり

12/7 ほぼ終わり

推定結果「11/23〜11/30頃で見頃間近〜見頃」と期間的には合致しました。スコアがもう少し安定していれば、良い結果と言えそうです。

課題

概ね出来上がりましたが、まだまだ課題は多いです。

  • サンプル数の不足。マイナーな紅葉スポットではほとんど推定出来ない
  • 紅葉スポットのノイズ。紅葉スポット以外が大量に含まれている
  • 紅葉以外への対応。春までに桜の開花状況に対応したい

まとめ

  • 紅葉スポットと紅葉状況を推定するWebサービスを開発した
  • 有名所の推定精度はそれなりにありそうだった
  • 精度向上や桜への対応なんかは今後の課題

ソースコードGitHubに公開しています。