にゃみかんてっくろぐ

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

デプロイ頻度を上げるための「下準備」

デプロイ頻度を上げた話ではなく、デプロイ頻度を上げるために必要なことを洗い出して実施したという話。

月 1 回のビッグバンリリース

所属チームは月 1 回のいわゆるビッグバンリリースで、まあ大変な状況だった。

  • デプロイ作業は 2 時間超
  • 500 ファイル以上の変更
    • 不具合発生時の原因特定が困難
    • 「原因以外の変更を取り消したくない」ため、ロールバックは原則行わず MTTR が長い
  • 「今月のリリースに間に合わせたい」ための行動が起きがち

Four Keys の話もあるし、列挙したチームの状況を鑑みても、デプロイ頻度を上げる価値はあると考えた。

いきなり毎日デプロイできるか? いいえ。

じゃあ月 1 回だったデプロイをいきなり毎日できるかというと、現実的ではない。今よりも大変になってしまうし、そもそも毎日デプロイするものもない。そんな状態で毎日デプロイしても価値は出ない。

まずは、デプロイ頻度を上げるための土台づくりが必要になってくる。 Four Keys はよくできているな… と感じる。

土台づくり

では、土台づくりで何をするか。ここの掘り下げは yigarashi さんの Four Keysがなぜ重要なのか - 開発チームのパフォーマンスを改善する方法について - yigarashiのブログ30分でわかるFour Keysの基礎と重要性 - Speaker Deck をめちゃくちゃ参考にした。その上で、チームの状況を見ながら以下の取り組みを行った。

  • プルリクエストを小さくする
  • すばやくレビューする
  • テストコードがない箇所は書く
  • カナリアデプロイを可能にする
  • 素早いロールバックを可能にする
  • デプロイ作業を軽くする

プルリクエストを小さくする

デプロイ頻度を高めるには、デプロイする「モノ」を高頻度に生み出す必要がある。そのためには、プルリクエストを小さく分割し、高頻度にマージできるサイズになっていると良いはずだ。

チームでは、 Google Engineering Practices のコードレビューガイドラインを取り入れつつ、変更は最大でも 300 行以内という基準を設けた。

元々はコードレビューの遅さが嫌になって始めたものだが、巡り巡ってデプロイ頻度改善の下地にもなっている。

すばやくレビューする

チームではレビューを必須としているため、デプロイの高頻度化には素早いレビューも欠かせない。

これも Google Engineering Practices のガイドラインに従って 1 営業日以内のレビュー(またはレビュアー代打の提案)を行うようにした。また、別チームが開発したレビューリマインダー Bot を活用して、 1 日数回リマインドするようにした。

テストコードがない箇所は書く

例えばリファクタリングなら、 API の入出力が変化していないことを検証しておくと良い。シンプルに書くだけである。

一見、デプロイ頻度と無関係に思える。しかし、「高頻度にデプロイして大丈夫なんですか?」という不安に対する答えを用意しておくことは、意外と重要だと感じる。

カナリアデプロイを可能にする

万が一問題が発生したときの影響範囲を、可能な限り小さくしておく。

これもデプロイ頻度に直接作用するものではなく、不安を和らげる策の 1 つである。

素早いロールバックを可能にする

万が一問題が発生したときに、速やかに元に戻せるようにしておく。

これもやはり、高頻度なデプロイに対する不安を緩和するためのものである。

デプロイ作業を軽くする

これはデプロイ頻度に作用する。単純作業は自動化してしまえば良い。

ただ、チームには自動化までの道のりが長いプロセスもあった。具体的には、 QA (品質保証)と、受託開発でいう「受け入れテスト」相当のプロセス。

これについては、リファクタリングに限り、プロセスを思い切って省略することにした。

本来省略すべきものではないが、「事前に議論しても、リスクを取る選択はしづらいのでは」「一度やってみたデータがあるほうが、議論しやすいのでは」と考えた結果である。

試せる土台は整った

ここまでで、試しにデプロイ頻度を上げても良いと感じる程度には、土台は整ったと思う。

ひとり毎日デプロイ

単独で行っているリファクタリングのプロジェクトだけ、短期間毎日デプロイを試した。

思った以上に何も起こらなかった。

一度だけ不具合を起こしたが、原因を深掘りすると、従来のビッグバンリリースでも起きうる内容。カナリアリリースや素早いロールバックがプラスに働きそうだ。

あとはチーム次第

今後チームとしてデプロイ頻度が上がるかは、チーム次第。諸々の事情で遠巻きから見守ることになったので、本当にチーム次第である。

仮にデプロイ頻度が変わらなくても、それぞれの土台づくりが全く無駄になるわけではない。より高品質で、より安全なデプロイの一助になっているはずだ。

参考資料

毎日 1000 万リクエストを捌く 1 台の API サーバー

P2P地震情報API サーバー (api.p2pquake.net) は、毎日 1000 万リクエスト以上を捌いている。ピーク時は毎秒 300 リクエストを超える。

VPS 1 台でここまで到達するのにそこそこ試行錯誤した。結果として意外性はなくやることやっただけという感じではあるのだが、アーキテクチャーや設定値を整理して記事にしておく。

前提となる構成、及び状況

  • Linode Dedicated 4GB プラン x 1 台
    • CPU: AMD EPYC 7601 32-Core Processor 2 コア (専有)
  • 公開 API は GET (読み取り) のみ
    • 書き込みは地震情報・津波予報の発表時などに限定

Nginx, Go + Gin, MongoDB の 3 層構造

DB: MongoDB Capped Collections

DB はドキュメント指向データベースである MongoDB を使用していて、 API のレスポンスほぼそのままの形でデータを格納している。 2015 年頃から運用中(それ以前は CSV ファイルベース)。

> db.whole.find({ code: 551 }).sort({ $natural: -1 }).limit(1)
{ "_id" : ObjectId("63244a1551d38bb17b2e7c91"), "issue" : { "time" : "2022/09/16 19:04:04", "type" : "DetailScale", "correct" : "None", "source" : "気象庁" }, "timestamp" : { "convert" : "2022/09/16 19:04:05.177", "register" : "2022/09/16 19:04:05.204" }, "user_agent" : "jmaxml-seis-parser-go, relay, register-api", "ver" : "20220813", "earthquake" : { "foreignTsunami" : "Unknown", "time" : "2022/09/16 19:00:00", "hypocenter" : { "name" : "東京湾", "latitude" : 35.5, "longitude" : 140, "depth" : 20, "magnitude" : 2.5 }, "maxScale" : 10, "domesticTsunami" : "None" }, "points" : [ { "pref" : "千葉県", "addr" : "市原市姉崎", "scale" : 10, "isArea" : false } ], "code" : 551, "time" : "2022/09/16 19:04:05.204" }

最大の特徴は Capped Collections を使用していること。これはリングバッファー相当のもので、コレクションを一定サイズに保つことができ、古いドキュメントは自動的に消えていく(上書きされる)。直近の情報を提供できればよいP2P地震情報にぴったり合う。

ストレージエンジン WiredTiger のキャッシュは最小の 256 MB 。

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
systemd+    1750 11.9  9.1 2210568 366816 ?      Ssl  Jul09 10836:08 mongod --wiredTigerCacheSizeGB=0.25 --auth --bind_ip_all

速度

速い。

P2P地震情報では 32 MB の小さな Capped Collection で運用していて、直近 10 件の地震情報を探す時間は 1 ms くらい。

> db.whole.stats()
{
        "ns" : "p2pquake.whole",
        "size" : 33553274,
        "count" : 12057,
        "avgObjSize" : 2782,
        "storageSize" : 8462336,
        "freeStorageSize" : 2072576,
        "capped" : true
        (略)
}
> db.whole.find({ code: 551 }).sort({ $natural: -1 }).limit(10).explain("executionStats")["executionStats"]["executionTimeMillis"]
1
> db.whole.find({ code: 551 }).sort({ $natural: -1 }).limit(10).explain("executionStats")["executionStats"]["executionTimeMillis"]
0

Tailable Cursors

Capped Collections を使うメリットはもう 1 つあって、それが Tailable Cursors 。これは tail -f 相当で、簡易的な Pub/Sub として使える。

プッシュ通知、 WebSocket API 配信などで活用している。厳密には計測していないが、レイテンシは 1 ms あるかないか程度。

アプリケーションサーバー: Go

もともと Ruby + Sinatra だったが、新しい API については Go + Gin で作っている。古い API も徐々に Go + Gin に移行したい。

[GIN] 2022/09/10 - 08:42:13 | 200 | 2.064529ms | xx.xxx.xxx.xxx | GET "/v2/history?codes=551"
[GIN] 2022/09/10 - 08:42:14 | 200 | 19.144558ms | xx.xxx.xxx.xxx | GET "/v2/history?codes=551&limit=40"

Web サーバー: Nginx

アプリケーションサーバーへのリクエストをとにかく減らす ため、キャッシュ設定を諸々行っている。

proxy_cache_lock イメージ. アプリケーションサーバへのリクエストは 1 つだけ

server ブロック抜粋:

proxy_cache_use_stale timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_timeout 30s;
proxy_cache_valid 200 1s;
proxy_cache_valid 302 1h;
proxy_connect_timeout 5s;

proxy_next_upstream error timeout http_504 http_502;

location /v2/ {
    limit_req zone=apiv2 burst=10;
    proxy_cache cache;
    proxy_pass http://127.0.0.1:10001;
}
location /v2/jma {
    limit_req zone=apiv2jma burst=10;
    proxy_cache cache;
    proxy_cache_valid 200 10s;
    proxy_pass http://127.0.0.1:10001;
}

Nginx に 142 requests/s 来ているが、 アプリケーションサーバーに到達しているのはわずか 6% の 8.13 requests/s である。

あとはファイルディスクリプタ数の上限worker_connections を引き上げたり、 net.ipv4.tcp_tw_reuse を 1 にしたり、くらい。

課題

TLS の負荷(たぶん)

諸々の結果、いま最も CPU リソースを消費しているのは Nginx になっている。

HTTPS ではなく HTTP でベンチマークを流すとこんな負荷は掛からないので、 TLS の負荷だと推測している。

top - 11:40:50 up 63 days, 15:47,  1 user,  load average: 0.42, 0.84, 1.05
Tasks: 247 total,   2 running, 245 sleeping,   0 stopped,   0 zombie
%Cpu(s): 15.2 us, 14.3 sy,  0.0 ni, 68.0 id,  0.0 wa,  0.0 hi,  2.5 si,  0.0 st
MiB Mem :   3931.3 total,    358.7 free,   2375.9 used,   1196.6 buff/cache
MiB Swap:   4608.0 total,   3882.1 free,    725.9 used.   1208.3 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
1676684 systemd+  20   0  163092  79328  32748 S  12.5   2.0 496:19.37 nginx: worker process
   1750 systemd+  20   0 2210568 379080  10656 S   8.2   9.4  10913:42 mongod --wiredTigerCacheSizeGB=0.25 --auth --bind_ip_all
2802253 root      20   0  749316  67716   9608 S   5.2   1.7   3185:14 /usr/bin/cadvisor -logtostderr --global_housekeeping_interval=5m0s --housekeeping_in+
    524 root      20   0 2640424 151960  18512 S   3.8   3.8   3667:06 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
2243005 root      20   0  721888  26240  14152 S   2.7   0.7  25:08.70 ./web-api-v2
2242527 root      20   0  721056  28592  14348 S   1.9   0.7  25:02.20 ./web-api-v2

今後負荷が問題になってきたら、 ECDSA 証明書を試すか、 Load Balancer に TLS を終端してもらうかで考えている。

数百万件残っていたHTTPのはてなブログを4年越しにすべてHTTPS化させた話 - Hatena Developer Blog

最後に

まだまだ IPv4 アクセスが多い。

CPU, Disk I/O, Network I/O の状況. CPU は Max 200% であることに注意

www.p2pquake.net のサービスをぜんぶ Docker でコンテナ化した(計 35 コンテナ)

※個人開発の備忘録です。

Debian 9 (stretch) が EOL を迎えたので、ちょうど良い機会だと思って色々と手を加えた。

  • Debian 11 (bullseye) に
  • すべてを Docker でコンテナ化
  • 監視ツールを Munin + Xymon から Prometheus + Grafana に
  • Vultr から Linode

すべてを Docker でコンテナ化

www.p2pquake.net は VPS 上で動いていて、小さなサービスの組み合わせで P2P 地震情報が成り立っている。

P2P 地震情報のサービス構成図

が、「まぁ動いてるからヨシ!」状態で、以下のようなサービスもある。

  • インスタンス再起動後に手で起動している
  • ずっと安定稼働して、セットアップ方法がわからなくなっている

サービスの起動し忘れで不具合が起きたりもしていた。

そこで、その辺の問題を解消すべく、重い腰を上げてすべてコンテナ化した。

Dockerfile が 500 行ちょっと、 compose.yml が 800 行ちょっと。ちゃんと (?) Docker Compose V2 を使っている。

~/production (master) $ find epsp-server-apps/ epsp-web-services/ -name Dockerfile | xargs -I{} wc -l {} | cut -d" " -f1 | awk '{s += $1} END {print s}'
530
~/production (master) $ find epsp-server-apps/ epsp-web-services/ -name compose.yml | xargs -I{} wc -l {} | cut -d" " -f1 | awk '{s += $1} END {print s}'
807

ただの MongoDB や Blue/Green デプロイ用に多重起動しているものもあるけれど、計 35 コンテナが動作している。

NAMES                                         STATUS
external-fluent_push-1                        Up 4 hours
external-fluent_twitter-1                     Up 4 hours
external-api-1                                Up 4 hours
server-areapeer-deliver-1                     Up 23 hours
epsp-web-services-eew-gateway-1               Up 23 hours
epsp-web-services-mobile_bridge-1             Up 23 hours
epsp-web-services-client-1                    Up 23 hours
server-client-1                               Up 23 hours
server-server-1                               Up 23 hours
jma_collector-broadcaster-1                   Up 24 hours
jma_collector-client-1                        Up 24 hours
epsp-web-services-https-portal-1              Up 2 days
epsp-web-services-static-1                    Up 2 days
epsp-web-services-realtime-api-green-1        Up 2 days
epsp-web-services-realtime-api-blue-1         Up 2 days
epsp-web-services-web-api-green_www-1         Up 2 days
epsp-web-services-web-api-blue_www-1          Up 2 days
epsp-web-services-web-api-v2-green-1          Up 2 days
epsp-web-services-web-api-v2-blue-1           Up 2 days
epsp-web-services-web-api-blue_backend-1      Up 2 days
epsp-web-services-web-api-green_backend-1     Up 2 days
server-metrics-1                              Up 2 days
jma_collector-dmdata_connector-1              Up 2 days
jma_collector-register-1                      Up 2 days
jma_collector-jma_watcher-1                   Up 2 days
epsp-web-services-traefik-1                   Up 2 days
epsp-web-services-prometheus_exporter-1       Up 2 days
userquake_aggregator-userquake-aggregator-1   Up 2 days
replicator-replicator-1                       Up 2 days
logger-logger-1                               Up 29 hours
logger-register-1                             Up 2 days
instance-exporter-cadvisor-1                  Up 2 days (healthy)
instance-exporter-node-exporter-1             Up 2 days
mongodb-mongodb_exporter-1                    Up 2 days
mongodb-mongodb-1                             Up 2 days

監視ツールを Prometheus + Grafana に

これまで Munin と Xymon を組み合わせてやってきたが、さすがに厳しい気がして Prometheus + Grafana に移行した。

Munin: MongoDB のメトリクス

Grafana: MongoDB のメトリクス

Munin は有志の手によって様々な plugin が作られてメトリクスが収集できていたけれど、 Prometheus も同じように様々な exporter でメトリクスが収集できた。

Traefik は標準で Prometheus サポートが入っているし、比較的モダンなソフトウェアとモダンな監視ツールの相性はやはり良いのかなという印象(個人の感想です)。

Linode

さくらの VPS から Vultr を経て、 Linode の Dedicated CPU インスタンスに移行した。 CPU 専有。

というのも、 CPU リソースを割と消費していて、「これ高負荷を理由に止められてもおかしくないのでは…」という心配が常にあったからである。

CPU 専有ならその心配はない(はず)。

RAM 4 GB, 2 vCPU で月 30 ドル。 Shared CPU だと月 20 ドルで、月 10 ドルの上乗せで済んでいるなら良いかなという気持ち。

Grafana: CPU 使用率は 40% 弱

Amazon Lightsail も考えたが、この CPU 使用率だと月 80 ドルのインスタンスタイプが必要になるため断念した。


そんなこんなで色々とモダンになり、省力化ができましたとさ。めでたしめでたし。

2021 年を振り返る

以前、はじめて転職活動をした際に「過去をしっかり振り返っておくことが大事」と言われたことがある。これに従って 2020 年を振り返っていたので、 2021 年も少し遅いけれど振り返っておく。

技術的なもの

個人開発: フロントエンドから OS 自作まで

主なものを整理するとこうなる。量は多くないが、普段あまり触らない領域に手を出せたのは良かった。

種類 名前 言語・フレームワーク
Web サービス メリうた🐝 TypeScript, React, Ruby
P2P 地震情報 スマホ版リニューアル
Windows 版リニューアル
Dart, Flutter
C#, WPF
OS ゼロからのOS自作入門 C++

やっとフロントエンドの感覚がつかめた

メリうた🐝 の開発当時のツイート。 jQuery でずっと止まっていたフロントエンドの知識をかなりアップデートできた。

これまで、いわゆる SPA にはユーザ体験向上のイメージしかなかった。いざ触ってみると、設計・実装が宣言的 UI やコンポーネント化で整理されるのがとても良いと感じた。

はじめての Flutter

P2P 地震情報のスマートフォンアプリ。 iOS 版は機能が貧弱、 Android 版は UI が古い… ということで Dart, Flutter でリニューアルを実施した。

「Flutter のために Dart を…?」と思っていたが、そんなに癖はなく書きやすかった。それよりも、プラットフォーム間の差異がしっかり吸収されていること、ドキュメントが大変充実していることに驚くばかりだった。

リニューアル後はかなりエゴサをしていたが、好評で嬉しかった。私自身もずっと使っている。

10 年越しのソフトウェアリニューアル

P2P 地震情報のスマホアプリを 2 年ぶりにリニューアルした後、 Windows 版を 10 年ぶりにリニューアルした。 C# (.NET 5), WPF で、コアとなる P2P 通信部分はクロスプラットフォームGUIWindows のみで動作する。詳細は以下に記している。

Linux, macOS にも対応させたい気持ちはあるが、各プラットフォーム毎に作ってメンテナンスできる自信がない。 .NET MAUI に期待している。

OS 自作

ゼロからのOS自作入門」(みかん本)。 Twitter で見かけて絶好のタイミングだと思って始めた。

コンテキストスイッチの実装はめちゃくちゃテンションが上がった。はるか昔、パソコン用語事典かなにかでプリエンプティブマルチタスクを知り、大学生あたりで切替方法を知り、とうとう実装することになり… という具合。

本に抜粋されているコード以外は推測するスタイルで、理解が足りず頻繁にバグっている。 30 日でできるはずが半年経ってまだ 17 日分。でも楽しい。

技術的であるがお気持ちが強いもの

仕事

主にテックリードっぽいことをしていたが、正直かなり悩みの多い 1 年だった。

誤解を恐れずに言えば、私が苦手とする「技術のわからないリーダー」に私自身がなってしまうのでは、という焦燥感と恐怖があった。

また、振る舞いや判断が良かったのか悪かったのか、よく分からなかった。定量的な数値を追うべきだったかもしれないが、数値が変化したとてテックリードの成果と言えるのだろうか。

はっきり良かったと思っているのは、 Google Engineering Practices によるコードレビューの高速化、リーダブルコード輪読会の 2 点(テックリード 1 年半、試行錯誤の断片 - にゃみかんてっくろぐ)。ただし、チームメンバーに恵まれていてタイミングが良かっただけで、私自身の成果とは言い難い。

技術的でないもの

京都(紅葉)

流行り病のケース数が落ち着いていた頃、 2 年ぶりに京都へ行った。最高だった。

いつも見頃前に訪れることが多かったが、今回は見頃やや過ぎ。しかし、散った紅葉も大変に美しかった。

創作意欲、クリエイターへの憧れ

何かを生み出すのは尊いし、憧れる。個人開発もある種の創作活動だと思っている。このときのツイートは、メリッサ・キンレンカさんや OSTER project さんの配信を見てのものだと思う。

おわりに

2020 年と同様、計画的偶発性理論に従って、良い方向に進むよう日々積み重ねていく。めでたしめでたし。

P2P 地震情報 Windows 版を「半分くらいクロスプラットフォームで」リニューアルしました

地震情報アプリ界隈 Advent Calendar 2021 14 日目の記事です。

P2P 地震情報 Windows 版を 10 年ぶりに更新しました。デザインは一新しましたが、目立った新機能はなく、未実装の箇所も残っています。

しかし、実は半分くらいがクロスプラットフォームになり、そのコードは Linux 上で動いていたりします。今回は、そのアーキテクチャをご紹介したいと思います。

P2P 地震情報とは

P2P 地震情報は、気象庁地震情報・津波予報と、ユーザ同士の「揺れた!」という情報を P2P ネットワークで共有する無償のサービスです。

f:id:no_clock:20211213235521p:plain
P2P地震情報 Windows 版 動作の様子

Windows 版のほか、 iOS 版Android 版Twitter @p2pquake 、さらに開発者向けに JSON / WebSocket API も提供しています。

Windows 版の問題点

Windows 版 (Beta3) は Visual Basic 6.0 で作っていました。運用を続けるうちに徐々に問題が生じてきました。

  • 古すぎる。開発環境は 2008 年にサポート終了。実行は Windows 10 でも可能。
  • マルチスレッドではない。 P2P 通信にはそもそも不向きである。
  • API 提供にあたり、地震感知情報の収集のためだけに Windows 環境を常時運用する必要がある。

そこでリニューアルすることにしたわけです。

Windows 版のアーキテクチャ

Windows 版は、 Microsoft のソフトウェアフレームワーク .NET 5 で開発しています。もともと .NET Core と呼ばれていたもので、クロスプラットフォーム対応が特徴です。

開発・保守がしやすいよう 6 つのコンポーネントに分割していて、画面(ユーザインタフェース)を除く 5 つのコンポーネントクロスプラットフォームに対応しています。

f:id:no_clock:20211213235712p:plain
Windows 版のコンポーネント構成

Web 版と地図は同じ

クロスプラットフォームであることの一例を見てみます。以下の画像は、 Linux サーバで動いている Web 版の地震情報地図と、新 Windows 版の地震情報地図を並べたものです。ほとんど同じです。 Linux でも Windows でも同じ地図生成のソースコードで動作しているためです。

f:id:no_clock:20211214000731p:plain
左: Web 版の地震情報地図、 右: 新 Windows 版の地震情報地図

アーキテクチャ詳解

ここからは少しだけアーキテクチャを詳しく見ていきます。

Client: サーバ通信、 P2P 通信、それらのコントローラ

コア部分にあたる Client は、 P2P 地震情報のピアとして動作するために、サーバ・ピア通信を担います。

デザインパターンの Mediator パターンと State パターンを組み合わせたようなクラス設計にしています。接続済の状態から切断するときは、ピアとの接続をすべて切断し、サーバに参加終了の通信をする… といった具合です。

f:id:no_clock:20211214000001p:plain
ざっくりしたクラス構成

f:id:no_clock:20211213235843p:plain
ざっくりした状態遷移図

地震情報などのデータを受信すると、下位クラスでイベントが発生し、上位へ運ばれ、最終的に最上位である MediatorContext クラスのイベントが発生します。画面にあたる WpfClient は、このイベントを処理して表示や通知をしています。

ソケットプログラミングは不安定で面倒なものです。接続済みかチェックしてデータを送信しても、送信する瞬間には切断されていることも珍しくありません。悲しいことに、ソースコードには try-catch が溢れています。

f:id:no_clock:20211214000036p:plain
悲しい try-catch (GitHub: epsp-peer-cs/Client/Common/Net/CRLFSocket.cs)

Map: ImageSharp によるクロスプラットフォーム描画

Map は地図生成を担います。といっても、地図生成には画像処理以外の要素も絡んでいます。

なお、クロスプラットフォームの描画ライブラリ SixLabors.ImageSharp を用いています。

WpfClient: WPF (Windows Presentation Foundation) による GUI

ユーザインタフェースを提供するのは WpfClient です。 ModernWpfUI を用いてモダンな見た目になっています。

モダンなのは見た目だけで、 WPF 自体はフレームワークとして「枯れた」部類に入っている気がします。 WinUI 3.NET MAUI がはるかにモダンです。ただ、ここで下手に躓いて開発が滞るのだけは避けたいという思惑がありました。

「 UI だけなら作り直しは簡単だから、まずは完成を」という気持ちで作りきったので、設計と呼べるものはあまりありません。 Program.cs は 400 行を超えて恥ずかしい感じになっています。ただ、クラスの依存方向が反転することが極力ないよう実装しています。

地震感知情報の地図生成については、 1 件ずつ生成していると受信ペースに追いつかないため、適宜生成を省略するように工夫しています。

選ばなかったアーキテクチャ

開発にあたっては、次のアーキテクチャも検討しましたが、最終的には選びませんでした。

  • Electron: モダンなクロスプラットフォームとして最有力候補でしたが、既に安定稼働していた P2P 通信部分をきっちり移植できる自信がありませんでした。
  • Electron.NET + Blazor (WebAssembly): P2P 通信部分がそのまま使えるものの、 ASP.NET Core が未知だったため躊躇しました。
  • Client を gRPC サーバ化 + Flutter: Flutter デスクトップ対応があったので検討したものの、さすがに上記 2 案と比べても奇抜すぎて止めました。

クロスプラットフォーム対応の使いどころ

そんなわけで、半分ほどクロスプラットフォーム対応になっています。実は、 GUI 以外は既に Linux 環境で実際に運用しています。

  • Map: Windows 版以外の地図画像を生成するために、 Linux サーバ上で実行
  • Asn1PKCS: P2P 地震情報 サーバ (Linux 版) で利用
  • Client, PKCSPeerCrypto: API 等で提供するための地震感知情報の収集に、 Linux サーバ上で実行

コンポーネント分割せずに失敗した過去

余談ですが、 Windows 版リニューアルは過去 2~3 度ほど試みていて、いずれも失敗していました。

野心的に新機能を盛り込みすぎたり、「一気に」作り直すやり方で進めていたりして、今振り返ると「そりゃ失敗しますよ」という感じがします。

今回は機能的にはかなり控えめにして、小さく分割して、「使えるところからどんどん動かしていく」という方法で進めました。少しずつでも「ちゃんと動いている」という安心感は、開発を前進させる力になりました。

YouTube コーディング配信

またまた余談ですが、開発の模様は一部 YouTube で配信していました。他人の開発風景ってなかなかまじまじと見る機会がないもので、ならば自分から、という感じでした。

1 ヶ月くらいで止めちゃいましたが、録画は続けていて 50 時間以上になっています。数分くらいに短くまとめて動画にしたいなと思っています(いつ完成するかはわからない)。

今後の課題

アーキテクチャや実装自体はかなり良くなりました。一方、機能としてはまだまだで、メモリ消費もかなり激しいです。

私も一利用者として使いつつ、完成度を高めていきたいと思います。

ソースコード

MIT ライセンスです。なんだかんだでそこそこのボリュームになりました(一部未コミットのコードを含みます)。

$ cloc . --include-ext=xaml,cs
github.com/AlDanial/cloc v 1.82  T=68.71 s (5.7 files/s, 609.7 lines/s)
----------------------------------------------------------------
Language      files          blank        comment           code
----------------------------------------------------------------
C#              377           4986           4124          31773
XAML             18             41             16            952
----------------------------------------------------------------
SUM:            395           5027           4140          32725
----------------------------------------------------------------

テックリード 1 年半、試行錯誤の断片

1 年半くらいテックリードっぽいことをしている。具体的には、 higepon さんの以下記事にあるような役割を概ねやっているつもり。

振り返ってみて、試行錯誤の連続だったと思う。「こんな成果が出ました!(バン!)」って派手なものはなくて、様々なところで「こっちがいいですね」と走る方向を示し続けてきただけ、という気がする。

まだ浅い経験しかないけれど、自分なりの考えをいくつか整理してみた。月並みな内容ばかりだけど、将来見知らぬテックリードにちょっとでも役に立ったりすると嬉しい。

品質ではなくスコープを削る

アジャイル開発において、リリースを急ぐならスコープを削るべきで、品質を削るべきではないと常々思う。

品質を削ると痛い目に遭う。これは経験からくる感覚もあるし、t_wada さんの「質とスピード」やそこで引用されている書籍でもはっきり記されている。カジュアルなものだと、 Message Passing のトピック「締切のはなし」も近い。

スコープを削るために、「できない」と早く言う

スコープを削るのは早いほうがいい。途中で放棄するのはもったいないし精神的にもよろしくないからだ。

そのためには、プロダクトマネージャーに早く伝える必要がある。小さなタスクに分解してスプリントプランニングに組み込むよりずっと前に。ここでは 2 つのことを意識している。

  1. いつでもざっくり見積もりできる状態を保っておくこと。システムアーキテクチャは脳内にキャッシュし、次にやりそうな機能は予め把握しておく。「 3 ヶ月でどうですか?」と聞かれてパッと答えられないと、「じゃあ一旦 3 ヶ月で」となりがち。
  2. 直感で良いので数値で出すこと。「情報少ないので精度低いですが、(できる確率は 2 割くらい|その規模は 6 ヶ月くらい)だと思います」みたいに言う。これで概ねなんとかなっている(プロダクトマネージャーとの信頼関係にもよりそうだ)。

品質を削ったら、戻すのは極めて難しい

ここでいう品質は保守性などの「内部品質」である。これをうっかり削った後、ちゃんと戻った経験は今のところ 1 度もない。

2 つの側面があると思っている。

  1. 後からリファクタリング単体で行うのは難しいということ。 動作するきれいなコード: SeleniumConf Tokyo 2019 基調講演文字起こし+α - t-wadaのブログ にもあるけれど、『価値を生まないように見える』活動を理解してもらうのは難しかった。また、品質を削るほどの開発は大抵リリースでヘトヘトになっていて、リファクタリングに消極的だったりした(気持ちはよくわかる)。
  2. 割れ窓理論やコードの一貫性。「元の実装がこうだったので(今回だけ綺麗にしても…|一貫性を重視しました)」みたいな話である。ボーイスカウト・ルールを持ち出して「一緒にリファクタリングしちゃいませんか」と言っても、コードベースが大きすぎるとリファクタリングした本人へのメリットはかなり薄まってしまう。

つまり、リリースまでにできるだけ品質を上げておく

内部品質を削ると戻りにくいのであれば、削らないようにするしかない。

スコープを削るのはもちろんだけど、最近は「内部品質にこだわる雰囲気」を作るように努めている。

  1. コードレビューの改善。 Google Engineering Practices で 一石六鳥のコードレビュー - Speaker Deck に書いたが Google Engineering Practices を取り入れた。まだ 1 ヶ月半なので成功と言い切るには早いが、ざっくばらんなディスカッションや考えがコメントされている頻度がかなり増えたと感じる。いい傾向だと思う。
  2. リーダブルコードの輪読会兼実践会。リーダブルコードは、「わかっちゃいるけど実践が難しい」類の本だと思う。実際のプロダクトのコードをリファクタリングすることで、実践の仕方をみんなで学んでいる。

この 2 つを並行で進めていて、この相乗効果はかなり良い。コードレビューで「リーダブルコードの…」とか、輪読会で「この間のコードレビューを例にすると…」と相互に話題が挙がっている。

「危険な匂い」を伝える術が見つかっていない

これは悩み。 Code smell みたいな話。コードに限らず、将来障害が起きそうだなといった「危険な匂い」を感じ取ったときに、それを適切に伝える術が見つかっていない。

ハイコンテクスト文化だと「これ危ないですよね?」「あー危ないですね」で片付くもので、今まではそれに甘えてなんとかなっていた。でもそうでない文化圏が入ってくると、「これ危ないですよね?」「…?(具体的なリスクや取るべきアクションを共有してもらえませんか…?)」といったすれ違いになっている感じがする。

難しいと思っているのは、この「危険な匂い」の根拠が乏しい点。これまでの知識や経験を総合しているとして、それを紐解いて丁寧に説明できるほどには記憶が残っていない。そのため、筋道を立てて説明ができず、「例えばこういう(事例|アクション)があるかもしれません」という断片的な説明しか出来なかったり、「伝わってくれ…!」という願望しか表現できなかったりする。

書籍を読んでみている

この状況を緩和するために、まさに「紐解いて丁寧に説明」しているであろう書籍を読み直している。改めて読んでみると、 SRE 本とか Code Complete とか、「なぜそれが大事なのか」が結構書かれていたりする。やや遠い道のりではあるが、知識や経験の総合なのだとしたら仕方がない気もする。

ハイコンテクスト文化圏に引っ越すほうが早いかもしれない、と頭をよぎる瞬間は何度もある。どこまで試すかはまだわからない。

また断片が集まってきたら書くつもり。

.NET 5: System.Drawing.Common と ImageSharp 、 Windows と Linux でテキストレンダリングの差をみてみる

クロスプラットフォーム。プラットフォーム間の差異に悩まされる地獄だ。

ということで、 .NET 5 でのテキストレンダリングの差を調べてみた。フォントは等幅な Roboto Mono とプロポーショナルな Roboto を用いた。

System.Drawing.Common 5.0.2

Windows の GDI+ API を使うライブラリ。ただし、 Unix 系でも Mono のオープンソース実装 libgdiplus を入れると動作する。ソースコードを拝見すると、 GdiplusNative.Unix.cs といったファイルがあって大変そうだ…

TextRenderingHint プロパティレンダリングモードが指定できるので、すべて試した。

Roboto Mono

f:id:no_clock:20210524013156p:plain
Windows 10 20H2 (200 % 拡大)

f:id:no_clock:20210524013210p:plain
Ubuntu 20.04.1 LTS / libgdiplus 6.0.4+dfsg-2 (200 % 拡大)

f:id:no_clock:20210524012841p:plain
不透明度 50 % で重ねた (200 % 拡大)

全く同じ結果は得られなかったが、 AntiAliasGridFit と ClearTypeGridFit はかなり近い。

Roboto

f:id:no_clock:20210524221129p:plain
Windows 10 20H2 (200 % 拡大)

f:id:no_clock:20210524221205p:plain
Ubuntu 20.04.1 LTS / libgdiplus 6.0.4+dfsg-2 (200 % 拡大)

f:id:no_clock:20210524221221p:plain
不透明度 50 % で重ねた (200 % 拡大)

結構な差が出てしまった。

SixLabors.ImageSharp 1.0.3 / SixLabors.ImageSharp.Drawing 1.0.0-beta11

クロスプラットフォームライブラリ。 System.Drawing 名前空間のドキュメントにも「代替手段」として例示されている。

図形や文字を描画するための SixLabors.ImageSharp.Drawing はまだベータ版で、 Getting StartedAPI 変えるかもよと注意書きが入っている。

カーニング (TextOptions.ApplyKerning) とアンチエイリアス (GraphicsOptions.Antialias) の有無をそれぞれ変更して試した。

Roboto Mono

f:id:no_clock:20210524031007p:plain
Windows 10 20H2 (200 % 拡大)

f:id:no_clock:20210524031028p:plain
Ubuntu 20.04.1 LTS (200 % 拡大)

f:id:no_clock:20210524031040p:plain
不透明度 50 % で重ねた (200 % 拡大)

同じ結果が得られた。

Roboto

f:id:no_clock:20210524221146p:plain
Windows 10 20H2 (200 % 拡大)

f:id:no_clock:20210524221241p:plain
Ubuntu 20.04.1 LTS (200 % 拡大)

f:id:no_clock:20210524221302p:plain
不透明度 50 % で重ねた (200 % 拡大)

プロポーショナルフォントでも同じ結果が得られた。

処理時間

こうなると ImageSharp を使わない理由はないように見えるが、処理時間に大きな差がある (以下 10 回の平均値)。

プラットフォーム System.Drawing.Common SixLabors.ImageSharp
Windows 10 20H2 27 ms 419 ms
Ubuntu 20.04.1 LTS 59 ms 282 ms

詳しく見てみると、 4 行あるテキストの 1 行目のレンダリングだけが遅い。フォントの読み込みだろうか。

タイミング 経過時間
(Windows 10 20H2)
経過時間
(Ubuntu 20.04.1 LTS)
新規画像生成
(640x480, 白塗りつぶし)
38 ms 43 ms
FontCollection, Font 作成 60 ms 72 ms
1 行目レンダリング 365 ms 228 ms
2 行目レンダリング 368 ms 231 ms
3 行目レンダリング 372 ms 234 ms
4 行目レンダリング 375 ms 238 ms
PNG 画像出力 419 ms 282 ms