Open6

「スケール」に対する、主にシステム観点からの雑多なこと

さざんかぬふさざんかぬふ

スケールするシステムを作る、上で私が感じたこと...

私は、とにかくミニマル/ミニマムなサービス(=目的を文字通り最低限達成するサービス)というものを作るのが得意で、与えられた課題に対して、とにかくある一つの側面から見た時に最低限実現できている、みたいな答えを出すことが得意だった。
私がそのようにして組み上げたサービスをリリースした時に、しばしば生じたパターンは、「今展開しているデータをコピー的に沢山作って、沢山の場所で同じことをしたい」だった。
当たり前といえば当たり前なのだけど、システムというのはコピー・繰り返し的な事が簡単にできる事が利点で、私がミニマムで作ったものを"沢山"使いたいという場合がよくあった。

例えば...

  • 似たデータセットを作りたい
  • スケジュール的に繰り返したい
  • 複数店舗で展開したい

このパターンの課題が発生すると、既存処理をループ的に実行したり、ループ的に展開したりする事によって、システム化の恩恵を得ることができる。"広がる方向"は時によって変わるのだが、要は人間の目から見て「一つの処理として概念的に固まったもの」があるとき、それを繰り返したくなるという自然な欲求があり、それをどこまで叶えられるか?という事がある意味でシステムの柔軟性であり、一つのスケール限界であると言える。
というような事を、足掛け5年、ゼロから作ったサービスを商業展開していて感じていた。

私はミニマムで作りすぎていて、しばしばこのパターンの課題が出てきた時に判断を間違える事があった。
例えば、A-<Bという親子構造(1-<nを表すとする、つまりAが親でBが子)があったとき、これを2階層ではなくて3階層で管理したいという要望がよくあった。
これに無理やりA-<X-<Bという親子構造を追加するのだが、既存のデータやデータ作成手順との関係で、Xはあっても無くても良いようにしたい、という場合があった。そのような"いびつな"親子構造、あるいは後からBをグルーピングするような概念を入れるという戦略は、しばしば失敗して、Xの有無での分岐が生じたり、管理画面でどう見せるのかみたいな問題が多数発生した。

途中から、このようなパターンで間に概念を追加する場合は、なるべく「あってもなくても良い」状態を回避するようにして、BかAのデータをXにコピーして親子構造としては必須にする、という戦略を取るようになった。これは、それなりにワークしている。

さざんかぬふさざんかぬふ

他に判断を間違えたパターンとしては、システムを最初に作った時の概念のうち「手軽であること」に悪い意味で固執してしまったというパターンがあった。
親子を増やす
A-<X-<B
と似たパターンで、A-<Bという構造に対して、本来Bが持っていた役割ではない役割をBと同じ階層に追加したい時に、Bにカラムとして追加して持たせてしまうという事をよくやった。
これは、DB取得のパフォーマンスや"マスタデータ登録のある種の手軽さ"という言い訳によってついつい選択をしていたパターンだが、これによってデータ構造がいびつになる場合がよくあった。

このよくないカラム追加と複合して致命的な問題を生じたのは、「Bの中で親子構造が発生する場合」だった。

A-<Bにおいて、Bを繰り返し量産したい場合に、量産するBの雛形になるデータbを用意して、bのコピー1、bのコピー2、bのコピー3、...というBのデータを作るという方法を取りたい場合がある。
このとき、単純にデータとして複製するだけではなくて、「bがbのコピー1の親である」という情報を持っておいたほうがよい場合がある。これをBに持たせてしまうと、Bを扱う様々な場面において、Bの中における親/子を区別して考える必要が出てきてしまい、例えばデータ取得時に「それがBにおける親なのか子なのか」を常に考慮しなければならない、みたいな状況になってしまう事がある。
おまけに、技術的に「自分自身に外部キーを貼ると一度NULLに更新しないと削除ができない場合がある」みたいな削除時にめんどくさい事情もあったりして、とにかくめんどくさい状況を発生させがちだった。

この判断は、【宣言的な定義量を減らす】という事を重視していたから発生した部分があった。
つまり、Bとほぼ同じレイアウトの「Bの複製元」みたいなテーブルを作ると、宣言的な定義量は明らかに増える。しかし、その他のデータ取得等において考慮することが減り、コードの総量や把握すべき定義の量は増えても、実装の際に実際に考えなければいけない関係性・複雑さが減る。

この「把握すべき定義の量が増えても、実際に考えなければいけない関係性・複雑さが減る」というのが、私の苦手なパターンで、私はどちらかというと「把握すべき定義の量を減らして、複雑な関係性を表現する」ということに走りがちで、それによって苦しむことが結構あった。

さざんかぬふさざんかぬふ

「コードの再利用性」ということは、昔から様々な本において言われることであったが、私が業務Webアプリ開発をしていたとき、その重要性を実感することはほとんどなかった。
Aシステムで組んだ汎用関数をBシステムで使う、みたいなことは当然あったし、様々なライブラリを利用してはいたが、例えばその業務システムにおけるコテコテの業務ロジックを再利用するというような事はほとんどなかった。
雑に設計した"再利用できるはずの共通関数"は、実際には微妙に利用方法が違って使えない。
特定の業務の為のロジックは、それ自体を別の関数で呼ぶという事はまずない。
自分が中途半端にそのような設計を検討したとしても、他の人が多くの場合愚直に実装してしまうため、全く意味がない。

というような事が、実に多く発生した。
そもそも、私が作った業務システムは、今みたいな速さでスケールさせるようなものではなくて、当初の設計を全うできればそれでよい、という考え方のものだった。当初想定した利用者や受益対象が突然倍になったり、10倍になったり、というような増加は考えられないものだった。

しかし、実際にスケールさせる事が前提のシステムを作る立場になると、はじめに述べたようなロジックの繰り返しという事がとても重要になった。

特定の操作でミニマムなデータを作成してシステムを利用することによって、ミニマムなパターンでのシステムの有効性が認められると、次はそれを何らかの観点で大量に増やしていくことになる。
それは、同じ設計のままで単純にデータが増える場合も多くあって、普通の2Cのサービスなどはそうなのだと思うが、B2B2Cみたいなシステムになると、間のBがある種の繰り返し処理を行うという事が重要になってくる。
そのような場面で、コードの再利用可能性というのは非常に重要な問題になった。
つまり、従来は一度でよい処理を繰り返したり、複数の処理を結合して別の一つの処理を定義したり、という事が自然に発生する。

開発者の目線で言えば、例えばインスタンスを一つ作るという作業を初期は手作業でやっているかもしれないが、スケールが必要になるとそれを自動化する必要が出てくる。
また、「インスタンスを作る」と一言で表現したものも、実際には単純にインスタンスを作る作業やそこにアプリケーションをインストールしたり設定をしたりといった作業があって、それらを結合して一つの処理として定義したり、繰り返したり、という事が必要になってくるということ。

そのような場合に、コードの再利用性が大きく作用する。

さざんかぬふさざんかぬふ

これは必ずしもスケールというよりは仕様変更的な概念だけれど、A-<Bの時に以下のような仕様変更が発生する場合があった。

  • A単位で設定していた条件をB単位で設定したい(例えば、商品が返品可能であるかどうかをA=商品分類の単位で指定していたのが、B=商品の単位で設定したくなった、とか)
  • B単位で設定していた条件をA単位で設定したい(上記の逆)
  • A単位でもB単位でも設定したい

これは、イメージ的にはエクセルの書式設定(特に条件付き書式)みたいなのと同じで、範囲指定して定義したルールと個別のセルで定義したルールが混在するみたいな感じの場合。

具体的な実装方法としては、

  1. AまたはB(または両方)に条件を直接もたせる
  2. 新しいテーブルXに条件を格納し、XにはAのidまたはBのid(または両方のid)をもたせる
  3. 新しいテーブルXに条件を格納し、XにはBのidを範囲指定やカンマ区切り等で複数指定できるようにする
  4. 新しいテーブルXに条件を格納し、AまたはBにXのidをもたせる

どれも若干手がかかるので、なんというか、このパターンを極力簡潔にやりたい、みたいなことはよくあった。

これの逆みたいなパターンが情報分析システムを構築する場合によく課題になった。
つまり、元データの種類によって、商品の大分類までしか持っていないとか、商品のIDまで持っているとか、そういった事が発生する。こういう情報粒度の差みたいなことにどう向き合うか、というのがよく課題になった。これも必ずしもスケールだけの話ではないが、スケールにおいて課題になること。

さざんかぬふさざんかぬふ

マニュアル関連の話。
ユースケース図というか、ユースケースの一覧というか、そういうものを利用者に共有可能な形で定義しておくのが良いんだろうな...
プログラムにユースケース一覧を組み込みたい

さざんかぬふさざんかぬふ

テストを書いて、テストから自動生成でユースケースを作成する、みたいなのが理想的なのかもしれない?