システム開発でよくある「ごん、お前だったのか」現象と依存関係、そして汎用性の罠の話
こんにちは、羽山です。
昔話には生きる上での数多くの教訓が込められています。今回は ごんぎつね からシステム設計・開発について考えてみましょう。
ごんぎつねの話はみなさんもご存じの通り、いたずらを悔いたごんぎつねが人知れず兵十という青年に贈り物を届けるも最後まで気づかれないまま火縄銃で撃たれてしまい、最後に「ごん、お前だったのか」となる話です。
さて、 達人プログラマー という書籍には 契約による設計(Design by Contract) という考え方が解説されています。
メソッドを契約として、 要求された以上のことも以下のことも行わない という考え方です。
システムの実装において要求未満の動作が許されないことは当たり前ですが、ここで重要なのは 要求以上の動作もするべきではない という点です。
この2つの話の共通項は人知れず役割を果たすことは美徳ではあるものの、時として大きなトラブルの原因になる可能性があるということです。
日常生活や職場でよかれと思って始めたことが、いつの間にか他の人にとっての当たり前になってしまった経験はありませんか?
もしくは当たり前だと思って利用していたものが、実は他の人の善意や偶然で成り立っていたことなど。
その類いの親切や気遣いは大切なものですが、それを受け取った相手が正しい認識を持たないと危険な場合があります。
- 自分宛の郵送物のついでに部門メンバー宛に届いた郵送物を毎日各デスクに配っていた。ある日長期休暇をとって海外旅行へ、一部メンバーは郵送物がデスクに届かなくなったことに気づかず期日のある郵便物を見過ごした。
- 可燃ゴミの回収はいつも11時くらいだった。ある朝いつものように10時半に出したらすでに回収済みで出せなかった。よくよくルールを確認したら朝8時までに出すことになっていた。
- 親切で片手間に作ったExcelマクロがいつの間にか各所で使われるようになった。あるとき外的要因でそのマクロが利用できなくなったが運用がマクロありきとなっていたので外部のユーザーに影響する大混乱が発生した。
- よかれと思って制約が少なく柔軟に運用可能なバックオフィス用のシステムを導入、顧客の要望に答えられると好評だった。しかしサービスの成長で利用者が増加した反面その柔軟さ故にオペレーションミスも目立つように。そこでシステムの刷新を考えたが多岐にわたる運用パターンの影響で見直しと改善に多大な労力と犠牲が伴った。
これらの例はすべて 担保する責務を明確にしないまま担保していた ことから発生しています。
双方が担保すべき責務を共有できていたらこういった問題の発生は最小限にすることができます。
世のあらゆる場面には縁の下の力持ちのように目立たず支えてくれているものがあります。
それらがある日突然なくなった後に、
「ごん、お前だったのか。いつも○○してくれていたのは」(ごんおま現象)
と、ならないための工夫が必要です。
では、ごんぎつねの居場所を探ってみましょう
(本稿にはプログラムコードが登場しますが読み飛ばしても理解できる内容になっています)
ごんぎつねを探す
まずはある架空のシステムで発生した問題を例にごんおま現象とその対処方法を考えてみましょう。
プログラムにおいてメソッドとその呼び出し元、及びその後続処理はごんぎつねが住み着きやすい環境です。
ここに配列要素を一意にして返すuniq
メソッドがあります。そのアルゴリズムは配列全体をソートしてから一意を判定するものでした。
def uniq(items):
"""配列を一意にして返す"""
uniq_items = []
for item in sorted(items):
if len(uniq_items) == 0 or uniq_items[-1] != item:
uniq_items.append(item)
return uniq_items
fruits = [
'lemon', 'banana', 'durian', 'banana', 'coconut', 'mango', 'banana', 'apple', 'mango',
'mango', 'banana', 'apple', 'mango', 'durian', 'mango', 'coconut', 'apple', 'mango',
'banana', 'apple', 'coconut', 'durian', 'banana', 'durian', 'mango', 'coconut',
]
uniq(fruits)
['apple', 'banana', 'coconut', 'durian', 'lemon', 'mango']
しかしデータ量の増加で全体をソートしてから重複を排除する方法ではパフォーマンスに問題がでてきてしまいました。
テストデータを5000万件に増やした状態でuniq
関数を実行すると実行時間がかかることが分かります。
import itertools
import random
# 5000万件のランダム順なサンプルデータを用意
fruits = random.sample(list(
itertools.chain.from_iterable(
[[fruit]*10000000 for fruit in ['apple','banana','coconut','durian','lemon','mango']]
)), 50000000)
# 実行に時間がかかる
uniq(fruits)
そこでパフォーマンス改善を依頼された別の担当者はハッシュを利用した効率的な処理にリファクタリングしました。
巨大な配列のソートが不要になるので、元の処理よりも高いパフォーマンスを得られます。
def uniq(items):
"""配列を一意にして返す"""
return list(set(items))
uniq(fruits)
['banana', 'durian', 'lemon', 'mango', 'apple', 'coconut']
要素を一意にして返すという仕様から考えると問題ないリファクタリングですが、 この場面でのごんぎつねはソート処理 です。
両者の出力を比べるとハッシュを利用して一意にした後者のメソッドは並び順がランダムになっています。
もしも後続処理がソートされた状態に依存していたら、そのシステムには問題が発生してしまいます。
例えば考えられる影響は以下です。
- 並列でDBレコードを更新する処理だった場合はデッドロックが発生する
(※ A, Bの順番で更新するトランザクションとB, Aの順で更新するトランザクションが同時に動くとデッドロックとなる) - 画面での表示順にそのまま影響していることもある、利用者がランダム順の表で混乱をきたす
問題が早期に発覚すればまだいいのですが、顕在化するまで時間がかかることもあります。
ごんぎつねの恩恵を受けていた兵十が四半期決算に利用されるシステムだったとします。プライマリキー順に取得したら都合の良い並び順となっていたので、そのまま利用していました。
四半期決算の資料が突然ランダム順になったことに気づいた担当者は原因究明に苦労します。
そして 一意にして返すメソッドのリファクタリング に問題があったと気づくまでにはそれなりに時間を要するでしょう。
では今回のケースにおける3つの問題について考えてみます
- 四半期決算システムの開発者が偶然の並び順を信じてしまった
- uniqメソッド開発者が要件以上の隠れた仕様を実装してしまった
- uniqメソッドが実装した仕様を明記しなかった
四半期決算システムの開発者が偶然の並び順を信じてしまった
最初の問題は担保されていない事象・偶然を四半期決算システムが前提条件に組み込んでしまったことです。
ソートされて採番されているかに見えるプライマリキーはどこかで仕様として担保されているでしょうか?
一般的にプライマリキーが保証するのは一意かつNOT NULLだけなので、それ以上の事柄はアプリケーションが意図的に担保する必要があります。
既存データが望む順番になっていたとしても、それを前提条件とせずに四半期決算システム側で望む順にソートしていれば問題は発生しませんでした。
uniqメソッド開発者が要件以上の隠れた仕様を実装してしまった
次の問題点は明文化されない隠れ仕様を実装したuniq
メソッドです。
あらゆる処理には大小様々な副作用が伴いゼロにすることはできませんが、意識して減らす努力は必要です。
uniq
メソッドの開発者は一意にする上手なアルゴリズムが思い浮かばずにソートに頼ったのかもしれません。しかし処理するデータのソートは副作用として大きめなのでその影響を考えるべきでした。
後続の四半期決算システムも既存データがランダム順で入っていたら自前でソートして利用したはずです。
システム開発では不必要に多機能な実装を行うとその多機能さに依存して動作する周辺システムが必ず生まれます。
一方で多機能な実装は多機能ゆえに業が深くシステムとしての寿命が短くなりがちです。今回のケースではソート処理の結果パフォーマンスに問題が起きたように何か得るものがあれば何かを失うトレードオフ関係であることが多いのです。
そしていざ問題が起きて修正しようと思っても、その多機能さを維持したまま改修するか、もしくはその多機能さに依存するシステムをすべて修正する必要があって難易度が高くなります。
uniqメソッドが実装した仕様を明記しなかった
次の問題点はせっかく良い行いをしているのに黙って行動したごんぎつねです。
uniq
メソッドを含むシステムがソートした結果を挿入することを仕様として明記していれば、リファクタリング担当者はその仕様を踏襲したリファクタリングをすることができました。
ただしシステムの仕様書をどのように残すのかは常に悩みが伴います。処理内容をアルゴリズムまで含めて網羅的に記載するとプログラムのソースコードそのものとほぼ同一のものになってしまいますし、アルゴリズムまで含めた処理すべてが担保する責務となるので後からのリファクタリングが難しくなります。
同じ理由でソースコードが仕様だ!とするのも悪手です。担保する責務が不明だとソースコードで行われている事象を副作用も含め、どこまでを再現したリファクタリングが求められるのかの影響範囲調査に労力がかかります。
そこで、ほどよく必要な人が必要なタイミングでほぼ気づくことができることが重要です。
今回の場合は以下のような方法が考えられます。
- メソッド名を
sort_uniq
に変更する、名は体を表す - メソッドのコメントに ソートした結果を返す と記載する
- ユニットテストのテストケースにソートを保証する内容を入れる
リファクタリング担当者はメソッドが担保する責務を壊さないように改善するので、上記のいずれかがあれば担保する責務を正しく把握した上で壊さないように改善可能だったでしょう。
例えばJavaで実装するならハッシュを利用して効率的に一意にしつつ任意の並び順を持てるTreeSetを利用できます。
public static String[] uniq(String[] original) {
return Stream.of(original)
.collect(Collectors.toCollection(TreeSet::new))
.toArray(new String[0]);
}
public static void main(String... args) {
String[] fruits = {
"lemon", "banana", "durian", "banana", "coconut", "mango", "banana", "apple", "mango",
"mango", "banana", "apple", "mango", "durian", "mango", "coconut", "apple", "mango",
"banana", "apple", "coconut", "durian", "banana", "durian", "mango", "coconut"
};
System.out.println(String.join(", ", uniq(fruits)));
}
apple, banana, coconut, durian, lemon, mango
具体例でごんおま現象のイメージがなんとなくつかめたと思います。
では次はごんおま現象をもっと広い視点で考えてみましょう。
依存関係ツリー
ごんおま現象は一般的な存在であらゆる粒度、あらゆる場面、あらゆるレイヤーに存在します。
そしてごんおま現象を理解するために欠かせない概念が依存関係ツリーです。
まずは本稿で登場する依存関係ツリーの概念について、ここで意味を定義します。
何らか事象や責務が維持されると時間経過と共にできあがる依存関係。
親・子・孫・ひ孫と上から下に対して依存するツリー状のもので上位の事象や責務に変化が起きるとその下位が強い影響を受ける。
管理しないと徐々にツリーの依存関係が肥大化・複雑化していく傾向があり本来不可視のもの。
「部門に届いた郵便物を各デスクに毎日配る」という行動が継続すると、時間経過と共に部門のメンバー達は「自動的に郵便物がデスクに届く」状態が当たり前だと認識します。
すると「デスクに届いた請求書を経理部門に持って行く」という業務が依存することが考えられます。
このように多くの事象には依存する・依存される関係、目に見えない依存関係ツリーが生じます。
誰の目にも明らかになっている事象・責務もありますが、一方でごんぎつねのように縁の下の力持ちよろしく見えづらいものもあります。
そして依存関係ツリーの上流で、はしごが外されたら下流が崩壊します。新しい仕組みやシステム・サービスなど導入は比較的容易な一方で、導入してから時間が経過したものからの撤退には痛みを伴うのはこのためです。
例えばそれをレガシーシステムと考えればエンジニアにとってなじみ深い技術的負債となります。技術的負債の返済が難しい理由もこのあたりから察していただけるかと思います。
システム開発における狭義の依存関係はnpm
, yarn
, bundler
, Composer
, yum
, apt
などが自動的に解決してくれますが、ごんおま現象に由来する依存関係ツリーは自動的には解決できません。
こんな経験はありませんか?
- 一人暮らしの時は不要になったものをサクサクと捨てられたのに共同生活が始まると自分以外がそれを必要としているかもしれないから捨てられない
- 職場内のサブスク契約が増える一方、それぞれ少しずつ何かが依存しているので廃止しにくい
導入した仕組みが社内を越えてユーザーへ提供する価値と直接的・間接的に紐づいていることもあり、そうなるとなおさら廃止が難しくなります。
ではなにも責務を担保せずに現状維持をするのが正しいのかというとそうではありません。前進は必要です。
システム開発においてアーキテクト担当は利用するフレームワークのバージョン4.0がいずれリリースされることが必然だと知っていながら、将来的に技術的負債となるバージョン3.0を今は導入する必要があります。
創業間もないスタートアップは最小の工数で最大の価値を生み出すために不適切な依存関係ツリーを許容することがあります。
エンジニアではない人物がAccessやExcelやSaaSを使ってつぎはぎだらけの社内システムを構築したとします。
不適切に設計されたシステムはごんぎつねの住処となるので将来的に苦労することは目に見えています。しかし短期的に顧客に価値を提供するためには必要なことです。
管理できない依存関係ツリーができあがることを恐れるあまり、価値を提供できなくなるなら本末転倒です。
依存関係ツリーができることは必然と考えて、その時点で可能な範囲で 管理する ことと 複雑にしない ことを意識しましょう。
汎用性の罠
汎用性とは限られた用途ではなく幅広い利用方法があることの意でポジティブにとらえることが多い言葉です。
システム設計においても汎用性はしばしば登場します。よく練られたシステムで様々なユースケースに対して汎用性があって柔軟な使い方ができるなどです。
しかしシステムに汎用性を持たせることは本当にプラスでしょうか?
実は最も汎用的な状態はそもそもシステムを導入しないで人間がすべてをアナログに対応することだったりします。
人は元々汎用的な動きが可能で、補助ツールとしてExcelあたりを利用すれば大抵の要望には柔軟にその場その場で対応可能です。
スタートアップが最短工数で価値を届ける、そして頻繁な運用変更にも耐えるためには外部向けに最低限のシステムを用意するに留めて、社内にはシステムを導入しすぎない状態が正解に近いと言えます。
一方で 汎用性は担保する責務を曖昧にする行為 です。責務が曖昧になると不適切な依存関係ツリーができあがります。
これらは一時的には必要悪として許容したとしても、いずれ環境やサービスの成熟に合わせて価値観をアップデートする必要があります。
以前までは最優先であった 最短の工数, 要望への柔軟な対応, 運用の変化に対する汎用性 が徐々に、 運用フローの効率性, ミスの発生を防ぐ仕組み, 膨大なデータにおけるパフォーマンス, システムの保守性 などに変わっていきます。
そういったステージの変化に合わせてシステムからは徐々に汎用性の殻を剥がして、1つ1つ明確な責務を担保するものに作り替えていく必要があります。
元とするシステムや運用がやんちゃに作られたものならこの段階では少々苦労することにはなります。しかしそのやんちゃなシステム・運用がなければ今その会社は存続していないと考えたらステージの変化に伴う成長痛のようなものだと考えるしかありません。
依存関係ツリーの例
言葉だけではイメージがわきづらいので具体的な依存関係ツリーの例を見てみます。会社のステージは簡易的ながらシステムを導入して手作業から脱却しつつある段階を想定しています。
しかしイレギュラーの運用が所々に残っていて、システムで対応できないことはデータベース内のデータをSQLで直接修正しています。
(A1) データベースのデータの直接修正を許容する方針
├ (B1) 顧客からの要望へ柔軟に対応
│ ├ (C1) 顧客満足度が高い
│ │ ├ (D1) 顧客継続率の増加
│ │ │ └ (E1) 売上の増加
│ │ │ └ (F1) 従業員の給与増
│ │ └ (D2) 客単価の増加
│ │ └ (E1) 売上の増加
│ │ └ (F1) 従業員の給与増
│ └ (C2) 同一の要望に対して異なる対応になりがち
│ └ (D3) 顧客からのクレーム増加
│ └ (E2) 現場部門の疲弊
└ (B2) SQLの実行ミスで障害発生の可能性
(A1)で顧客からの要望のうちシステム上で実現できないことはSQLで直接修正しています。
顧客対応においてルールが厳密ではないので要望に対して「今回だけ特別ですよ」など柔軟に対応(B1)することができ、それ故に顧客満足度は高め(C1)です。
顧客満足度が高いので顧客の継続率も高く(D1)、客単価も増加傾向(D2)、そのため売上は順調に増加(E1)、そして従業員の給与も順調に上がっています(F1)
一方で柔軟な対応をしている負の側面として対応内容の不安定(C2)さなどはあるようです。
依存関係ツリーでは上位の事象に下位の事象が依存します。
平和に運用していたある日、内部統制の対応で (A1) データベースのデータの直接修正を許容する方針 が撤回されることになったらどうなるでしょうか?
(A1)に依存している (B1) 顧客からの要望へ柔軟に対応 は直接影響を受け、それ以下も間接的に影響を受けます。なにもしなければ顧客への要望へ柔軟に対応できなくなり、顧客満足度が低下して、継続率、客単価が減少、売上も減少して従業員の給与も減ってしまいます。
しかし依存関係ツリーを管理できているならば、はしごを外された形の (B1) 顧客からの要望へ柔軟に対応 に対して、なんらか別の方法で担保できればよいと分かるので対策を立てやすくなります。
今回のケースでは以下のような方法を考えることができます。
- 【対策1】過去の要望を分析、対応すべきものとそうでないものに分けて、対応すべきものをシステム化する
- 【対策2】システム開発の担当者を増員して要望に対して最短でシステム対応できるよう努める
- 【対策3】(B1)顧客からの要望へ柔軟に対応 の下位、 (C1) 顧客満足度が高い を担保する方法を別に検討する
依存関係ツリーによると (B1)顧客からの要望へ柔軟に対応 には負の側面(C2)もあるので、実は (C1) 顧客満足度が高い を直接満たした方が効果的であることが分かります。これは依存関係ツリーを管理できているからこそ分かったことです。
システムにおける依存関係ツリーの管理
ごんおま現象や依存関係ツリーが一般的な課題であることは理解いただけたと思いますが、風呂敷を広げすぎたのでここからはシステム開発に話題を戻して以下の3点から依存関係ツリーを管理する方法を考えます。
- 必要のない責務は担保しない
- 不適切な依存関係ツリーを断つカオスエンジニアリング
- 外部プロダクトの責任範囲を明確にする
必要のない責務は担保しない
システムやコードが不必要な責務を担保していると不必要な依存関係ツリーが生まれます。
そこで必要のない責務は担保しない努力が必要です。
今開発しているシステムはリリースされたらその後長期的に運用していくことになります。そして実装された要件・仕様はそのサービスが存続する限りは形を変えて長期的に維持されます。
利用基盤の陳腐化などシステムの耐用年数を超えた場合は要件定義し直してリプレースされますが、既存システムの仕様は大部分を踏襲することになります。しかし複雑な仕様や不透明な責務を担保しているコードからは なにを維持してなにを捨てればいいかをくみ取ることができない のです。
今回リプレースして機能追加するシステムの中に以下のJavaコードがあるとします。
private ConcurrentHashMap<Long, Long> counter = new ConcurrentHashMap<>();
public void countUp(Long id) {
counter.put(id, counter.getOrDefault(id, 0L) + 1L);
}
しかしこのコードには矛盾があって責務が不明瞭なのでなにを担保しているのかがよく分かりません。
ConcurrentHashMap
を利用しているのでマルチスレッドでcountUp()
が呼ばれる想定だと受け取れるcounter.getOrDefault()
がマルチスレッドから同タイミングでコールされたらcounter.put()
があと勝ちになり1回しかインクリメントされない
ConcurrentHashMap
の代わりにHashMap
が使われていればスレッドセーフではないと受け取れるので担保している責務を誤解なく理解することができます。
もしくは以下のようにsynchronized (counter) { ... }
で囲まれていたら、マルチスレッド対応のcountUp()
メソッドだと理解した上でリプレースできます。
public void countUp(Long id) {
synchronized (counter) {
counter.put(id, counter.getOrDefault(id, 0L) + 1L);
}
}
今回は長く稼働しているシステムなので表面上はエラーが出ていないはずです。そこで以下3パターンのいずれなのかを調べる必要があります。
- マルチスレッド非対応の設計、なんとなく
ConcurrentHashMap
を利用しているだけで実際にはシングルスレッドでコールされている - マルチスレッド非対応の設計、しかし呼び出し側のミスで同時に呼ばれて結果として正しくない数値が記録されている
- マルチスレッド対応の設計、しかしプログラミングミスで同時にコールされると1回しかインクリメントされない
影響範囲を調査した結果1.だった場合は今度こそHashMap
を利用するコードに置き換えることになります。影響調査の工数は無駄になりますが比較的穏便に済みました。
2.の場合はHashMap
を利用する形に置き換えつつ呼び出し側が意図せず並列稼働している問題への対処が必要です。不適切に同時コールされている原因を調べて過去のデータに問題がないかも確認する必要があります。
3.の場合はスレッドセーフなメソッドとして正しく実装し直します。そして正しくインクリメントされなかった過去の数値についてデータの復旧や対処を検討することとなります。
ConcurrentHashMap
はHashMap
の上位互換なので毎回マルチスレッド対応かを判断する手間を惜しんで、常に高機能なConcurrentHashMap
を利用している方もいるかもしれません。
しかし不必要に高機能な仕組みを利用すると暗黙で担保される事象が増加します。フェイルファストの考え方のように早期にエラーが発生していれば被害が少なかったのですが、マルチスレッドでもギリギリ動く所までが意図せず担保されてしまったことで発見が遅れるのです。結果として誤った数値を元にした依存関係ツリーは徐々に広がり、誤りに気づいた段階でその全ての依存関係について対処が必要になります。countUp()
が記録した数値が顧客への支払い額にまで影響していたらと考えるとゾッとします。
他の例としてあるテーブルのデータをプライマリキー順で処理するシステムがあったとします。
データ量の増加で並列処理の導入を検討しますが、そうなると今までは暗黙で保障されていた処理順番が保証されなくなります。
結果として処理の並列化に対応する他にも後続システムが処理順番に依存していないか確認する必要がでてきます。
この場合ははじめから適当な順番で処理するようになっていれば不必要な依存関係を事前に防止できたのです。
不適切な依存関係ツリーを断つカオスエンジニアリング
カオスエンジニアリングは意図的な障害をあえて注入する手法ですが、これは不適切な依存関係ツリーの発生を防ぐ効果があります。
ハードウェアやOSなどは100%動作し続けることを保証できません。常に一定確率で故障や停止の可能性があります。
一方で今現在はハードウェアもOSも優秀で故障率は低いので稼働が続くと徐々に 100%正常動作することがあたりまえ な状態になり、単一障害点となりうるアプリやミドルウェアをつい動かしてしまいます。
そしてひとたび障害が発生すると依存関係ツリーの子孫達は未曾有の事態に追い込まれます。
そこで、カオスエンジニアリングはあえて障害を発生させることで100%動作することを担保していない状態を強制的に作ります。
これはフェイルファストの考え方とも共通項があって、複雑な依存関係ツリーに組み込まれる前にダメージが少ない早期に発見させることができます。
事前に「100日に1回ほど自動的にランダムで再起動します」と言われていたら、そこで動かすアプリは多重化を考えるはずです。これによって100%動作することが保証されていないことが明確化されます。
例えばスレッドセーフではないメソッドが誤ってマルチスレッドで利用されている状況で、そのメソッドの実装はどちらがより望ましいでしょうか?
- マルチスレッドで利用してもほぼ問題なく動作する、まれにしか問題は起こさない
- マルチスレッドで利用したら途端に問題が発生する
これは言うまでもなく後者の方が望ましいです。
前者のメソッドは潜在的な問題の発覚を遅らせる上にいざ問題が起きた段階で原因究明を困難にします。
外部プロダクトの責任範囲を明確にする
外部プロダクトを導入する場合そこには把握していない機能や必要ないけど提供されている機能などが含まれていることがあります。
また社内で作り込むシステムと比較すると様々なユースケースが想定されているので 汎用性が高く、なんにでも使える ことも多いです。
そういったシステムを無計画に導入するとそれに依存した複雑な運用フローができあがったり、依存関係ツリーが管理できなくなることがあります。
導入自体はブーストの為に不可避だとしたら以下のような点を注意しましょう。
- 導入するプロダクトのもつ機能を念入りに把握する
- 不必要な機能を無効化できるなら無効にする
- 権限管理が可能なツールならば必要最低限の権限を付与する
- 自社の基幹システムとのデータ連係など、インターフェースを最小限とする
基幹システムを構築できるSaaSを利用して簡単に柔軟なシステムを構築するのはいいのですが、あまりに自由にやり過ぎると複雑怪奇な依存関係ツリーと年々上昇するSaaSのコストで前にも後ろにも動けない状況になりかねません。
世の中のごんおま現象と依存関係ツリー問題
世の中には様々なごんおま現象や依存関係ツリーにまつわる問題が発生しています。
もちろん当社でも例外ではなく、ここではいくつかの例を紹介します。
ラクーングループで発生したごんおま現象の例
まずは当社で実際に発生した問題を紹介します。
ある日サービスの運営部門からPDFファイル内のとあるセル値が急に表示されなくなったという連絡がありました。
確認してみるとセルは表示されているものの値が表示されていません。まずは生成しているアプリを疑ってここ最近の変更履歴を確認するも、しばらく修正された形跡はありません。
該当のPDFで利用しているデータの生成元システムにも範囲を広げて確認してみるものの問題になりそうな点は見つかりませんでした。
さらに範囲をインフラにも確認範囲を広げてみたところ、システムのデプロイのための社内ツールに修正が入っていることが分かりました。
種明かしをするとそのデプロイツールの修正が入るまでは該当アプリはservice
コマンドを利用せずログインシェルの環境変数を引き継いだ状態で起動していたのです。
デプロイツールの修正でservice
コマンド経由の起動となって環境変数が引き継がれなくなり、さらにPDFの生成処理がLANG
, LC_ALL
に依存していたことで利用されるフォントが切り替わっていました。そしてセル内に収まらなくなった値が表示されなくなったようです。
ログインシェルの環境変数という縁の下の力持ちが突如いなくなったことで発生した問題で、この例では環境変数に対する依存を最小化するために、はじめからservice
コマンドを利用しておくべきではありました。
しかしごんおま現象でありがちな後味の悪さ、原因が分かったとしても「ああ、それは気づかないよね」という雰囲気で 誰もそれほど悪くない ことが多く、それ故に次回以降の同様の問題への事前の対処が難しい傾向があります。
オープンソースプロダクトのメジャーバージョンアップ問題
長く存続しているオープンソースのプロダクトはほとんど例外なく依存関係ツリーの問題に直面しています。
そしてそれが最も色濃く表れるのがメジャーバージョンアップのタイミングです。
PHPの歯ブラシ問題
PHPが一部で歯ブラシと呼ばれることがあるのはご存じでしょうか?PHPの発案者であるラスマス氏がPHPのことを歯ブラシと例えたことがあるのです。
精密機器ではなく日用品、そして登場した当時の時代背景もあってか確かにPHPには曖昧で首をかしげる言語仕様が一部にあります。
現在のPHPはもはや歯ブラシと呼ぶのは不適切ですが、初期の言語仕様を未だに色濃く引き継いでいる部分もあります。
例えば"1000" == "10E+2"
がtrue
になるという挙動があります。これは文字列同士の比較にもかかわらず非厳密な比較演算子が数値に暗黙の変換をしてから比較するという誰得な仕様です。
PHP8で非厳密な比較演算子の挙動が変わるというので、細かい情報を調べずにこの部分を確認してみたのですが結果は残念ながら変わっておらず。
root@5c40c4de0ab1:/# php -version
PHP 8.0.6 (cli) (built: May 12 2021 12:47:05) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.6, Copyright (c) Zend Technologies
root@5c40c4de0ab1:/# php -r 'echo ("1000" == "1000" ? "true" : "false") . "\n";'
true
root@5c40c4de0ab1:/# php -r 'echo ("1000" == "10E+2" ? "true" : "false") . "\n";'
true
root@5c40c4de0ab1:/# php -r 'echo ("1000" == "10e+2" ? "true" : "false") . "\n";'
true
root@5c40c4de0ab1:/# php -r 'echo ("1000" == "10e+3" ? "true" : "false") . "\n";'
false
言語仕様として担保された事象はかくも重く後のバージョンにのしかかり、一度担保した仕様を覆すことが難しくなるという例でしょう。この仕様を進んで維持したいと思っている人がいるとは到底思えません。
しかし一方で言語のメジャーバージョンアップのタイミングでは現実としてはそのままのプログラムでは動かないことが大半です。「これくらいなら大丈夫だろう」くらいの事象はバンバン変更されているのです。
メジャーバージョンアップの陰にはごんぎつね達の尊い犠牲があります。
MySQLのsql_mode問題
依存関係ツリーに苦しむ問題で思い出されるのがMySQLのsql_mode問題です。
言語仕様と同様にデータベースの挙動も変わってしまっては利用者としては困ります。そこでMySQLは過去の依存関係ツリーを維持したまま新たな機能を導入するためにsql_mode
という設定パラメータを用意しています。
sql_modeの一覧を確認すればMySQLが依存関係ツリーに組み込まれた過去の仕様に苦労している様子が分かります。
例えば0000-00-00 00:00:00
という日付のゼロ値は特殊な値でORMが対応していなかったりIS NULL
, IS NOT NULL
両方にヒットしたりなど不適切な挙動があります。
今からMySQLを再設計するならば亡き者としたい仕様だと思われますが、以前のバージョンから長らく存続しているものなので数多くのシステムがこのゼロ値に依存していて安易に該当機能を削除するわけにもいきません。
そこで、MySQLはNO_ZERO_IN_DATE
, NO_ZERO_DATE
などの設定でON/OFFを可能として、既存の依存関係ツリーの下流に影響を与えないまま徐々に対応を縮小しているのです。
まとめ
今回はごんぎつねの話から見えざる依存関係ツリーの存在に光を当てています。
よくよく考えてみたらとても身近なものであるにもかかわらず、この概念を直接的に解説した記事をあまり目にしたことがなく、どう表現したらいいのか悩んだ末に今回は 依存関係ツリー というベタな呼び方をしてみました。
この記事でとりあげなくても誰しも依存関係ツリーがあることを元々知っていたはずですが、依存関係ツリーそのものよりも周囲の話題にフォーカスされることが多いので見過ごされがちです。
本稿はきっかけです。世の中のあらゆるものが依存関係ツリーでできあがっていることを認識して考える習慣をつけていただければ幸いです。
外部に向けてなんらかの責務を提供している組織は社会に対して依存関係ツリーの一端になっていますし、その組織内部にブレイクダウンしたらそこにも依存関係ツリーがあります。
今後何らかの責務を果たすタイミングがあれば、 その責務が依存している事象 と、 将来その責務に依存する事象が生まれること を考慮する習慣を付けるといざというときの立ち回りがしやすくなります。
それでは世の中の「ごん、お前だったのか」が少なくなることを願っています。