ソフトウェア設計について
文章練習中ですので連日投下します。
導入
Twitterで「設計ってどうやって勉強するの?」っていうリプを見ました。
確かに座学や個人レベルでの開発では設計を学ぶのは難しいと思います。
設計の書籍はとても抽象的で実感が湧かないですし、ライブラリやフレームワークを組み合わせるだけの開発では「設計」はほとんどしなくていいからです。
実践で学んできてしまった僕にはどうやって勉強するのかはわかりませんが、僕が何を考えて設計しているのかはわかります。
設計には物理設計(今名付けた)と論理設計(今名付けた)があります。
僕は用語を覚えるのが不得意なため勝手に名付けましたが正式な名前がきっとあります。
物理設計は、決められた動作をする仕組み(=システム)を考えることです。
基本的には「どう実現するか」を考えます。
論理設計は、人間がわかりやすいか、改修しやすいか、テストがしやすいか、などを考えます。
基本的には「どう分割するか」を考えます。
双方はどちらも必要なものですが、システムが実現できてから分割方法が決まると思いますので、最初は物理設計から行います。
物理設計の概要が決まればその後は並行して論理設計も決めていけると思います。
(この辺りたぶん異論反論がたくさんある人がいると思います)
今回は物理設計についてです。
論理設計については近いうちに書きたいですが、こちらは書物やブログも豊富ですので気になる方は読んでみてください。
オブジェクト指向設計やDDDやClean Architectureなんかはこれです。
たぶん一般的に「ソフトウェア設計」と呼ばれているものは論理設計です。
さて、僕は抽象的な文章を書くのが得意ではありませんので具体例で説明します。
最初に言っておかなければならないのは、 使っているプログラミング言語やライブラリ・フレームワーク標準の機能で実現できたり、 社内標準の方法があるのであれば、 それを使うのが「一番無難」だと言うことです。
あえてオレオレ設計を作ったりトリッキーなことをして技術的負債を増やすことはありません。
(Ruby on RailsでJavaのようなサービス層を儲けようとして爆死なんかしてませんよ?)
設計するもの
あるシステム(もしくはプロセス)で、他のシステム(あるいはプロセス)から受け取ったデータを別の他のシステム(あるいはプロセス)に送るシステム(もしくはプロセス)を考えます。
なんでこんな書き方になっているかと言うと、
僕の持論では「システム全体の設計」と「プロセス内部の設計」は粒度が違うだけ
で考え方は同じだと思っているからです。
あえて違いを言えば、前者は必要とされる知識が広く、後者は深い、ということでしょうか。
これ以降はだるいので「プロセス」で統一します。
さて、要は「他プロセスから受信したデータを別の他プロセスに送信するプロセス」を設計します。
インターフェース
まずはどんなプロトコルで受信・送信するのかを決めます。
これはわりかし誰でも決められるというか、
相手方が既存プロセスならもう決まってますし、
即時性が求められないし大量だからファイルでいいねとか、
リアルタイムだからTCP/IPねとか、
配る範囲が広範囲だからIPマルチキャストにしようねとか、
であっさり決まります。
ここでは(今後の話の展開上)TCP/IPでリアルタイムで送受信することに決まったとします。
動く仕組み(1)
次に「受信したデータを送信する仕組み」について考えます。
ここでは安直に「受信したらそのまま送信する」としましょう。
受け取ったら送信、受け取ったら送信、シンプルですね。
動く仕組み(2)
ところが、送信する側のプロセスは秒間最大10000件(最大であって常に10000件ではない)送ってくるのに対し、受信する側のプロセスは秒間1件しか受け取れない、とわかりました。
さぁ大変です。
今のままだと、秒間最大10000件送りつけられてしまう受信側のプロセスが受けきれません。
このプロセスの送信が延々と待たされるか、受信側のプロセスで何かトラブルが起きるかです。
さらにこちらの送信が待たされればこちらに送信してきている側のプロセスにも影響が及びます。
これでは全然ダメですね。
別の案を考えましょう。
素直に考えると「受信側のペースで送信すればいいのでは?」と考えますよね。
「受信側のペースで送信する」にはどうしたらいいでしょうか?
ひとつ考えられるのは「送信側プロセスから秒間10000件送られてきても(ウェイトを入れて)秒間1件しか受信しない(=受信側のプロセスに秒間1件しか送信しない)」です。
でもこれはすぐにダメなことがわかります。
先ほどと同様に秒間10000件送る側のプロセスに影響(待たされる)が出てしまいます。
それが他ベンダだったりすると怒られが発生します。
動く仕組み(3)
もうひとつ考えられるのは「データを貯めておく」です。
秒間10000件受信したものは一度貯めておき、ゆっくりと秒間1件送信すればいいのです。
貯めておく方法には色々ありますが、一番簡単なメモリ内のキューを使ってみます。
プログラミング言語のライブラリにそのものが用意されているかも知れませんし、リストなどのコレクションを用いて自分で作らないといけないかも知れません。
送信側プロセスから受信したデータをぽんぽんキューに投げ込んで、受信側プロセスに送信する側はそれを1秒間に一回取り出して送ります。
これは別スレッドなり非同期IOを用いないと実現できませんがここでは触れません。
(スレッドやトランザクションについては個人的に大好きなので改めて何か書きたいですね)
これで基本的な動きは大丈夫そうです。
完成?
本当に?
パフォーマンスは?
メモリ使用量は?
CPU使用率は?
送信側は「最大」とは言え、延々と数時間秒間10000件送りつけてくるとするならばメモリ使用量が心配ですね。
実際の動作環境でどのくらいメモリを使えるかを調べて秒間10000件を最大どのくらい受け続けても大丈夫かを計算しましょう。
同時に送信側プロセスの作成者(もしくは社)に秒間10000件の状態が最大どのくらいの時間続くのか問い合わせましょう。
それでメモリ使用量がヤバそうなら、メモリ内のキューじゃくてミドルウェアのMQを使用しましょうか。
MQを使うのが面倒ならば泥臭くデータベースを使いますか(使いたくないけど)。
それらを使うにしても今度はストレージの容量を見積もる必要があります。
この仕組みで本当に動くのかパイロット(僕の場合、エラー処理とかUIを端折って最低限動くようにしたもの)を作ってみましょう。
パフォーマンスや各種リソースの利用量は大丈夫ですか?
再確認
でも、ちょっと待ってください。
受信側プロセスは本当に全てのデータが必要なのでしょうか?
送信側プロセスから秒間10000件送られてきたとしても、受信側プロセスは1秒おきの(間引かれた)最新データのみが欲しいのではないでしょうか?
これは受信側プロセスの作成者(もしくは社)に聞いてみないとわかりません。
動く仕組み(4)
結果その解釈(間引かれたデータ)で良かったとします。
では「秒間最大10000件受信したデータの1秒おきの最新データを送信する」にはどうしたらいいでしょうか?
送信する側プロセスが送ってきたデータを前回の受信から1秒間経つまでは捨てて、1秒以上経ってから受信したデータだけを受信する側プロセスに送信するのはどうでしょう?
送信する側プロセスが常に秒間10000件送ってきてくれればうまくいきそうです。
でも、秒間10000件はあくまで「最大」ですので5秒おきにしか送ってこない時間帯もあります。
そして1秒経つ前(0.5秒とか)に送信が止まってしまったら?
最新のデータを送信することができなくなってしまいますね。
ここで1秒過ぎてもデータを受信しなかったら、直前に受信したデータを送るという仕組みも考えられますが、やりたいことに対して複雑すぎます。
動く仕組み(5)
改めて考え直します。
秒間10000件受信しようが1秒経つまでのデータは捨ててしまって良いのだから、キューなんていらなかったのです。
単純に送信側プロセスから受信したデータを変数に上書きしてしまいましょう。
1秒おきにその変数のデータを見に行って受信側プロセスに送信すればできあがりです。
実際にはこのままだと受信してない時には最後のデータを1秒おきに送り続けてしまうので、受信したかのフラグを持つなど工夫が必要です。
また、別スレッドで同じ変数を読み書きすることになるのでスレッド間の同期処理も必要になります。
これでなんとなく動く仕組みにはなったと思います。
耐障害性
ただこれだけではダメです。
このプロセスが落ちた時にシステム全体が止まるようではダメです。
2台を常時稼働させるようにしましょう。
送信側プロセスには1台で2台に送信してもらうか、2台稼働してもらってそれぞれに送信してもらいましょう。
受信側プロセスは2台から同じデータを重複して受信することになるので、
データに一意な通番を用意して同じデータは弾いてもらうようにしましょう。
受信側プロセスが落ちても同様です。
システム全体を止めるわけにはいきません。
2台同時稼働してもらってもいいですが、今回は1台死んだらもう1台に切り替わる、としましょう。
そうすると、受信側プロセスと一度切断してももう一度接続しにいく仕組みが必要になります。
また、永遠に再接続ループしたら大変なのでその上限値を設けたり、受信側プロセスがメンテナンスなどで止まる時間帯が週に1度あるならばその時間帯は再接続しないなどの仕組みも必要になります。
切断前にどこまで送ったかの管理をして再送できるような仕組みも必要かも知れません。
パフォーマンス
このプロセスは実際の稼働に耐えられるでしょうか?
どのくらいまでなら耐えられるのかパイロットを作ってパフォーマンスを様々な条件で測っておきましょう。
このプロセスはユーザ数やデータ数の増加によって負荷が上がるでしょうか?
もしそうならば同時に何台もデータを分散して稼働できるような仕組みも作る必要があります。
運用を考える
エラー時のログはどんな時にどんなものを出しましょうか?
些細なことでエラーログを吐くと監視している人が24時間不眠になります。
監視システムがパンクするかも知れません。
エラーコードやメッセージはシステム全体で統一した方がいいですよね。
このプロセスには定期的なメンテナンスジョブ(初期化やデータのクリアなど)は必要ないでしょうか?
起動ジョブ・停止ジョブは何曜日の何時に仕込みましょうか?
その時間に止まっても受信側プロセス・送信側プロセスはエラーになりませんか?
その日が休日だった場合の動きはどうなりますか?
終わり
だいたいこんなことを考えながら設計を行っています。
これをシステム全体に渡って個々のプロセスに至るまで延々と行います。
何をどのように考えて何をしてるのかが伝わったでしょうか?
まぁ、書いているうちに僕の捉える「設計」が一般的な「設計」の意味とずれている気がしてきたんですがどうなんでしょ。
ぶっちゃけ、各種設計とアーキテクチャの違いもわかってないですしね。
※ 絵を入れてみようと思っていたのですが面倒でやめました…
※ 要望があればこのサイトではMermaid.jsが使えるようになっているので描きます。