ユユユユユ

webエンジニアです

信頼性、保守性、スケーラビリティ: Designing Data-Intensive Applications 第1章より

 Designing Data-Intensive Applications の第1章の読書ノートを公開する。主題はシステムの信頼性、保守性、スケーラビリティについてである。

 個別の手法や技術というよりは、システム設計や技術選定の際に大事にすべきマインドセットに重点をおいた論文である。おおいに同意、納得しながら読めたし、これからも長く大切にしたい視座だとしみじみ味わっている。いつもよりもよく咀嚼しながら読み進めたのに加えて、ダメ押しで記憶に焼き付けるために、サマリとして記録しておく。




 現代のシステム開発パラダイムは、計算能力よりもデータの量、データの複雑度、データの変化の速さを問題にする。このようなシステムをデータシステムと呼ぶことにする。

 データシステムに必要な構成要素はおおかた世の中に出揃っている。データベース、キャッシュ、検索インデックス、メッセージキュー、バッチ処理、などの諸要素である。とはいえ、現実の技術選定のあり方は、プロジェクトの特性によるとしか言いようがない。実際、 Redis はデータベースだがメッセージキューとしても使えるし、Apache Kafka はメッセージキューだがデータベースにもなる。そして往々にして、「どのツールを採用するか?」というよりも「どのツールをどう組み合わせるか?」ということが問題になる。

 そうしてデータシステムを設計するとき、主要な関心ごとは例えば次のような項目となる:

  • どうやってデータの一貫性を保証する?
  • どうやってデータシステムの一部が壊れても主要機能が動き続けられるようにする?
  • どうやって負荷に耐えてスケールさせる?
  • どんな API を用意すれば使いやすいサービスになる?

 もちろん糸口は様々にあるが、多くの場合で有効な、3つの基本的な考え方がある。それが表題に掲げた「信頼性、保守性、スケーラビリティ」であり、これらの要素はいずれも非機能要件に分類されるものである。それぞれについて簡単に要約するとこうなる。

  • 信頼性: ハードやソフトの障害、あるいはユーザーエラーへの耐用性
  • 保守性: 新しいメンバーが既存の振る舞いを守りながら開発に参入していけるか?
  • スケーラビリティ: データ量やトラフィックの増大に耐えられる仕組み

以下に見出しを立ててより詳しく検討してみよう。

信頼性

 システムの信頼性とは、なにかおかしなことが起きても機能し続けることである。部分的な欠陥が存在する可能性をあらかじめ織り込むことで、「部分で障害が発生しても全体は機能し、要件を満たし続けることができる」という状態を作り出すメカニズムが要請される。たとえばカオスエンジニアリングは有効なアプローチといえるだろう。

ハードウェア障害

 耐用年数など、確実な対応が求められる問題は存在する。しかしクラウドコンピューティングの時代であるから、個々のシステム設計者がこのレイヤの問題に精通している必要は必ずしもない。

ソフトウェアエラ

 ハードウェア障害によって全てのマシンが連鎖的に落ちることは稀である。しかしソフトウェアエラーにはそれがありうる。テストや監視といったプラクティスの小さな積み重ねで予防できるから、手を抜かずに基盤を整備しよう。

人的エラー

 操作ミスや想定外のユースケースによってシステムが破綻することはありうる。そうしたエラーを起こさせないための制約を、ユーザーが不自由を感じない範囲で考えて設計しよう。たとえば自動テストはとりわけコーナーケースの振る舞いを調べるのに有効であるし、本番環境相当のサンドボックスがあればなおよい。明快な監視メトリクスを定義して、問題を予兆することも可能である。

 以上が信頼性の議論である。これらは開発コストとのトレードオフで犠牲にされることも少なくない。絶対悪だとはいわないが、その判断は決して軽い意思決定ではないことはよく認識しておこう。

保守性

 保守性を高めるにはどうすればよいか? 逆を考えてみよう。保守性が低くならざるを得ないシステムとはどんなものだろうか? それは、運用が難しく、新入社員には扱いづらく、変更のするのが困難なシステムのことだ。

 ここでは「保守性が高い」という状態を運用が容易で、シンプルで、進化の余地が用意されたシステムと定義しよう。

運用が容易であること

 悪いソフトの欠陥を運用でカバーすることはできる。しかし悪い運用はどれほどよいソフトも安全に扱うことはできない

 できることは、ここでも小さなプラクティスの積み重ねしかない。たとえば、個別のマシンに依存せず、システムを停止せずにメンテナンスできるようにすること。あるいは、規定の振る舞いには制約を与えつつ、管理者権限で柔軟に操作できるようにすること。それから、予想可能な振る舞いを定義し、驚きを最小にすること。人に優しいシステムを設計しよう。

シンプルであること

 小さくシンプルなシステムが、やがて大きくなるほどに複雑性を高めていく。これは当然の摂理である。しかし、大きなシステムでも複雑性を下げることは十分に可能である。

 複雑性には「本質的な複雑性」と「偶発的な複雑性」がある。後者は「事故的な複雑性」といってもよいかもしれない。これを削減することを考えよう。

 要するに、「単純な要件を複雑に実装してしまっている」ような箇所を見つけ出し、そこにアプローチしよう。単にテストとリファクタリングが有効なこともある。あるいは適切な抽象化が要請されることもあるだろう。これらの営みによって、この種の複雑性は排除できるはずである。

 小さなプログラムならともかく、分散システムにおいてどう抽象化を実装するかを考えるのは難しい。これはより大きな問題であるから、のちの章に話題を譲ろう。

進化の余地が用意されていること

 システムの要件は常に変わりうるものである。まずはこの真理に向き合おう。変わり続ける要件に対応できないシステムは本質的に脆弱である。

 アジャイルとは組織論だが、方法論としてこの問題に対して有効である。大規模データシステムにおいてどのようにアジャイルを実践するか? これもまたより大きなトピックである。組織のシンプルさ、システムの抽象化にも密接に関係する問題提起として、これも詳しい議論はのちの章に議論を譲ろう。

スケーラビリティ

 システムをミクロに観察して「これはスケールする/しない」と評価することは本質的にはできないはず。より適切な問いは「システムが成長したときに対応できるか?」「さらに計算資源を投じるときはどうするか?」といった事柄である。これらの問いに答えるには、まず「負荷」を定義しなければならない。

「負荷」の定義はプロダクトの性質によってさまざまである。まず次のような単純な命題から考え始めてみよう。

  • 読み込み負荷が大きい? or 書き込み負荷が大きい?
  • データ量が大きい? or データの複雑性が高い?
  • 多量の小さなリクエストが与えられる? or 少量の大きなリクエストが与えられる?

「負荷」を定義し、それに影響を与えるパラメータを定義できて、はじめてパフォーマンスを語ることができる。2012年のTwitterにとって、それは「ユーザーあたりのフォロワー数の分散」であった。リソース量を変えずに負荷を増やすとどうなる? あるいは、負荷が増えてもパフォーマンスが変わらないためにはどれだけリソースが必要になる?

 パフォーマンスの代表的な指標であるレスポンスタイムを例にとってみよう。最大値や最小値をターゲットにすることにはあまり意味はない。結果が分散していることを前提に、パーセンタイルで評価が行われることが多い。95%, 99%, 99.9%あたりをメトリクスとすることが多い。具体例として、 Amazon では 99.9% の厳しいパーセンタイルをレスポンスタイムの重要指標としている。レスポンスに多くのデータが含まれるのはしばしば大のお得意様であるから、もっとも読み込み負荷の高い上客をこそ満足させることが重要なのである。そのうえでこれより厳しい99.99%のパーセンタイルに投資することは効果に見合わないとも結論づけられている

「負荷」の定義を誤ったままパフォーマンス改善に着手してしまうと、大きな機会損失につながってしまう可能性が高い。ゆえにスタートアップなどではしばしば、負荷増大を心配するよりも機能の柔軟性を高めるほうが有効である。

Ruby の基数変換において基数の上限は 36 である

 SOMPO HD プログラミングコンテスト2021(AtCoder Beginner Contest 192) に参加した。D - Base n を解きながら、いままで知らなかった Ruby の振る舞いを知りえたので書いておく。

 0-9 からなる文字列を、整数 d で基数変換し、整数 n 以下かどうか調べる。これがやりたいことである。整数オーバーフローに気をつけながら変換アルゴリズムを書くのが面倒に思えて、 RubyString#to_i ならサクッと安全に変換できるかな? と考えた。つまりこんな感じである。

def f(s, d, m)
  s.to_i(d) <= m
end

require 'minitest/autorun'

class T < Minitest::Test
  def test_f
    assert_equal(true,  f('22', 3, 10)) # 3進数表記: '22'.to_i(3) == 8 < 10
    assert_equal(true,  f('22', 4, 10)) # 4進数表記: '22'.to_i(4) == 10 < 10
    assert_equal(false, f('22', 5, 10)) # 5進数表記: '22'.to_i(5) == 12 > 10
  end
end

で、このメソッドを繋ぎ込んで、適当な入力のテストケースを足して実行してみると、こんなエラーメッセージを出して落ちてしまう。

ArgumentError (invalid radix 37)

どうやら基数が 36 以下であるうちはよいのだが、 37 はダメらしい。まったく予期していなかった結果で、どうしてだ? 素数だからか? と無意味な当てずっぽうをはじめてしまいかねないほどにはパニックを起こしていた。

 一呼吸おいて、インターフェースを調べてみるとちゃんと書いてあった。

Returns the result of interpreting leading characters in str as an integer base base (between 2 and 36). 1

「へええ!」と思いつつ、とはいえコンテスト中に深く考える余裕はなく、さっさと Ruby で実装する方針は諦めて自前の変換アルゴリズムを書いたのだった。

 一晩あけてちゃんと考えてみると、当然の振る舞いというよりない。要するに、 [0-9a-z] の 36 要素を超えて全順序を定義することはもとよりできないというだけの話である。もちろん Integer#to_s でも同じことである。今回のケースでいえば、レシーバの文字列は 0-9 のみを含む、というのは問題文の制約にすぎない。そうでない自由な文字列を任意の値で基数変換するのは不可能であるから、結局のところ基数変換のロジックはユーザーが自前で用意するよりない。

 ところで問題そのものの解法については、制限時間を10分残したところで二分探索する方針にようやく辿りついたが、実装にダラダラ手間取ってしまい、15分超過してようやく正解できた。単純に二分探索が手に馴染んでいないことが白日のもとにさらされたわけである。今回のコンテストで手痛い思いをしたので、次にあらわれたときには倒してやるからな、という気持ちでいる。

 提出 #20343302 - SOMPO HD プログラミングコンテスト2021(AtCoder Beginner Contest 192) 

新入社員になった

 大学を出て以来はじめて会社に所属することになる。

 内定が決まったときから興奮ばかりしてきた。当日を迎えて、いっそう興奮しているかというと、そういうわけでもない。むしろ身の引き締まる思いを新たにしている。不思議なものだ。

 同期入社のひとたちはいろんなバックグラウンドを持っている。先輩社員のみなさんもそう。みんなとてもすごいひとにおもえる。自分はほんとうに井の中の蛙だったなあとおもう。要するに、いまになって不安になってきている。

 初日から手を動かすことを求められないことに落ち着かなさを感じているのかもしれない。いままでいくつかの案件に携わったが、おおむね参画当日の午後にはすでに手を動かしはじめていた。初日にプルリクを開けるようにと、自分でも気合いをいれて臨んだものだった。

 それが今日はなかった。

 研修を受けた。経験のないスケールで成長するプロダクトのほんのわずかな概観を与えられた。圧倒される間に一日が終わった。しかもこれが一週間つづく。

 はっきりいって、こんなに丁重に扱われた経験はない。一日でも早く活躍させてもらえるために、あえて最初の一週間を座学に費やすという逆説。あまりに懐が深い。

 果たしてこんなに贅沢なことがあっていいのだろうか? まだなんの貢献もしていないのに、こんな待遇を受ける価値があると思ってくれる根拠はなんだろう? そんな倒錯した不安がいまになって訪れている。果たして自分は適格なのか?

 しかしこれは僕の悩むことではないだろう。もし僕が力不足であったとして、それは僕の責任ではない。こちらの愚鈍のためにこちらが責められても困る。そして逆から見ると、これだけのオンボーディングプロセスを有する会社において、その手前の採用プロセスがガバガバということは考えづらい。だからきっと大丈夫だ。

 そう思って寝る。

macOS で GUI アプリのインストールをスクリプト化する

Homebrew/homebrew-cask で homebrew 経由で GUI アプリをインストールできる。

もともと aws-vault や corretto などは cask でいれていたけれど、インストール可能な cask の一覧をみるとよりどりみどりだ。1password に evernote, workflowy といった生活必需品相当のアプリを環境構築スクリプトに入れ込むことができる。

list casks to fetch in homebrew.sh by sato11 · Pull Request #2 · sato11/dotfiles

ちょうど新しい PC を支給されたところなので使ってみたところ、実に楽チンにセットアップが終わって満足している。

C++ のコンパイル環境を変更した

 vscode の devcontainer で debian コンテナを立てて、その上でコンパイルしていた。こうしていたのは単に <bits/stdc++.h> を楽に使いたかったからにすぎない。いろいろカスタマイズを頑張るより先に、手っ取り早くコンパイルできる環境を用意したかったのである。こうして作った環境を特に不自由なく利用していた。

 ではなぜいまになって見直したか? ac-library の環境構築をするためである。かつてと同じように、さっさと動作する環境を手に入れたかったので、深く考えずに実施した。

 ローカルにライブラリをおいて、ローカルでコンパイルするようにした。 homebrew で gcc をインストールして、これを使うように設定しただけである。 docker イメージにライブラリを含めるとか、面倒かつ本質的でない作業に逸脱せずにさっさと済ませられて満足している。コンテナ定義は不要になったので捨てた。

Enable ac-library by sato11 · Pull Request #1 · sato11/atcoder

 一周回って、実直な環境構築に回帰したわけである。単にコンパイルするためだけのお膳立てなので、ただちに不便になるようなことはないだろう。

gcc のインクルードパスを確認する

 C++ のライブラリをローカルの開発環境でインクルードできるようにしたい。また #include <aws/dynamodb> のように、相対パスではなく <> でインクルードしたい。

 正しいインクルードパスにヘッダファイルを配置してあげればいいはずだが、そのインクルードパスを忘れてしまった。過去に同じ作業をしたことはあるはずなので、マシン上のどこかにあるのは確かなのだが、どこにあるだろうか…。

 次のワンライナーが教えてくれる。

gcc -x c++ -v -E /dev/null

 出力はこうなる。 /usr/local/include が目的のパスであった。

Apple clang version 11.0.3 (clang-1103.0.32.62)
Target: x86_64-apple-darwin19.6.0
...
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1
 /Library/Developer/CommandLineTools/usr/lib/clang/11.0.3/include
 /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include
 /Library/Developer/CommandLineTools/usr/include
 /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks (framework directory)
End of search list.
...

 蛇足。あとになって気づいたが、このパスは自分で環境変数として定義していたのだった。

export CPLUS_INCLUDE_PATH=/usr/local/include

2021年の生活規範

 元日にあわてて立てた目標は決して根付かないだろうと信じるので、お試し期間の一週間に吟味し、決意しているところを掲示しておく。「○○をする」という形式はおのずと努力目標にならざるをえないだろう。強い制約として日常生活をしつけるために、「○○をしない」という禁止の形式で4箇条を定義することにした。

  1. 副業をしない
  2. 新刊本を買わない
  3. 新しい技術に踊らされない
  4. 食事中にビデオをみない

副業をしない

 副業を依頼されて嫌な気分はしない。「自分は名指しで依頼されるだけの実力の持ち主なのだ」といい気分にさせられる。しかしこれは単なる虚栄心である。

 仮に生活に困窮してしまったとして、そのときに副業というアディショナルな選択肢を持てるのは幸福なことである。ただしいまの僕はそれを求めない。自分なりに納得のいく金額で評価をもらえている現状、それを超えて金を求めるモチベーションはわかない。

 モチベーションとしては、稼働を増やす方向付けよりも、単価を上げる方が望ましい。少ない稼働で同じ収入を得られるのは魅力的に思う。とはいえこれは今年単年の目標にはなり得ないので、いまは考えないでおく。

新刊本を買わない

 スマホブームの揺り戻しなのだろうが、本を読むことすなわち教養というやや短絡的な逆張りが目立つ。「積読」という言葉を肯定的に捉え直してみたり、「巣篭もり需要」とかにかこつけて本を売ろうとする隠然たるマーケティングも多く感じた。

 出版は掛け替えのない産業と思うし、本を売りたい気持ちもわかりたいと思う。しかし怪しいブロガーや起業家に書かせた安い本からヘイト本まで、売れそうとみるや恥も外聞もなく売りにかかる姿勢は堕落そのものである。

 話題先行であったり、明らかに広告が優位な出版物とは距離をおきたい。あらゆる新刊はプロモーションを伴うとして、新刊は一切買わない、と割り切ってしまうことにする。

 時間の審判に耐えた書籍だけを読む。そして読むと決めたものについては精読する。1年で10冊も読めればそれでいいと思う。その体験をどれだけ豊かにできるかは読み手の僕自身にかかっているというだけの話である。

新しい技術に目を奪われない

 トレンドは次々にやってくる。しかしそれらの宣伝をみても、正しく意義を理解できないことの方が多い。どんなパラダイムに属していて、どんな課題を解消する技術なのかがうまく飲み込めない。

 よくわからないまま踊り始めることを悪くは言わない。現に僕自身、そうして学んできたところは大きい。しかしいくらか成長してみて、踊っているつもりが実は踊らされているだけだったと知るのはあまり嬉しくない。

 まずは枯れたパラダイムを明晰に理解することが必要だ。 teachyourselfcs.com がよいコンパスとなる。その上で、乗るべきハイプを見極めて踊ることも必要だ。踊る人々を見て、単に冷笑して見せるだけというのは醜い。ある技術にベットするのは、張り間違えたとしてもそれが掛け替えのない学びとなるのであれば、そう悪くない。ギャンブルに見立てるならば、早いフェーズで乗るかそるかを決めてしまい、引くに引けなくなるのが最悪だということだけ注意を払っておけばよい。

食事中にビデオをみない

 一人で食事をするときに、 Netflix なり YouTube なりを再生しながら食事するのをやめること。これは単純な悪癖であるが、あまりに常態化してしまっている。「ながら食べ」は行儀が悪いと自分自身をしつけ直す。実際、そこまで慌てて消費なければならないコンテンツなどない。

 いっそ、これら動画プラットフォームと縁を切られれば人生の充実度は確実に高められるだろう。「YouTube は今後一切みないことにして、余暇には読書しかしない!」という目標を立てるのもよい。ただこうした抑圧は揺り戻しを伴うから、あえてそうまではしない。まずは最低限、食事中のマナーを正しくする。それができたら、段階的に引き締めていく。