本当にメモリが原因なのか?
ずっと前からとある個人開発のサービスを見ているのだが、このサービスが1週間に1回くらいクラッシュするようで、その都度作者による再起動が必要となっている。同氏によればメモリ不足とのことで、これを解決すべく最近になってサーバープランを見直してメモリを増やしたらしい。「これでサクサク動くようになりました」といった1週間後にまたクラッシュしていたので根本解決はまだ遠い未来のようにも思える。
個人開発であればなるべく安く、できれば無料でサービスが運用できることが望ましいが、運用形態によっては常時起動のサーバーが必要なこともある。1段階スペックを上げると費用が約2倍になるのは業界標準のようで、会社でやっているならまだしも個人の趣味の範囲でやるには倍々スケールはかなり痛い出費になり得る。今の時代、満足にアプリケーションを動作させるには最低でも1,024MBのメモリが欲しいし、スクリプト言語を動かすのであればその倍は欲しい。結果的に期待するメモリを持つ環境を用意するにはそれなりの持ち出しが必要となる。
しかし本当にメモリが足りないことが問題なのだろうか?サーバーを強化すれば問題は解決するのだろうか?このサービスの状況を見ていると平常時は動いているわけだし、連続稼働かアクセス集中が発生するとクラッシュするようだから、メモリの管理かアーキテクチャになんらかの不具合を抱えていると推察している。
メモリをいつ解放するか?
動的メモリはおそろしい
私が最初に触れたプログラミング言語はCだったが、この言語の大きな話題としてはポインタと動的メモリがある。スタック領域のメモリ確保はシンプルかつ理解しやすいもので、再帰呼出しでも起きない限りはほとんどの状況で安全だが、一方の動的メモリはスコープを超えて利用できるメモリを確保できるものの、確保自体に失敗するかもしれないという不確定さと、解放漏れによるメモリリークが私を大いに悩ませた。動的メモリの確保はCではおおよそこんなコードを書く。
FooBar *foobar = (char *)malloc(sizeof(FooBar));
if (!foobar) {
// メモリの確保に失敗した。死ぬしかない
exit(-1);
}
// 解放は *適切* なタイミングで行う。適切っていつ?とはいえメモリの確保に失敗したら死ぬくらいしかできない上に、適切なメモリ解放はC言語の表現力ではかなり難しい。後発のプログラミング言語は自動的にメモリを解放するためのガーベジコレクタ(GC)を備えるものもあったし、Rustのようにライフタイムをガチガチに管理してそもそも解放漏れが発生しないように強制しているものもある。
ガーベジコレクタは天才的な発明のひとつではあるが、循環参照になるとメモリリークするので、この問題を解決するための手法や弱参照(Weak Reference)を導入する等の努力が行われてきていて、通常我々が書くようなプログラムではほぼ問題が起きないといっていいくらいにはなっている。しかし、こうした抽象化が進んだ結果、プログラマが期待するタイミングでのメモリ解放ができないこともあり、それが却って煩わしいこともある。
メモリを解放できず死ぬサービス
これはもうかなり昔の話で時効なので書くのだが、私にもスクリプトキディのクソガキだったころがあった。当時はチャットサイト全盛期で私も大手が運営するチャットサイトに入り浸り、おかげで学業が疎かになり両親にも心配かけた。しかしそのチャットサイトも人口減少とコスト増を理由に閉鎖されてしまい、虚しさを感じていたところ、かつての仲間のひとりに別のチャットサイトを紹介された。そこはすでに過疎ってはいたが、休日になれば数人から十数人が集まり、それなりに賑わってもいた。
しかし、多感なお年頃のキッズが集まるチャットサイトなので、些細なことで衝突が発生するのは日常茶飯事だった。最初に入室した参加者(または最後に残った参加者)にホスト権限が与えられるというゆるい管理体制が敷かれていたので、横暴な振る舞いをする参加者がホストになると、往々にしてBAN祭りが発生するのだった。このBANも仕組み的には甘いもので、最長で数時間同名で再入室できなくなるようなものであった。いろいろ検証している中でそれほどBANのデメリットがないと気づいた私は、当時のCORSの概念なんてないJavaScriptでコントローラ(コン)1を作り、BANされた瞬間に連番を付けて再入室できるようにした。
さて、このコンの出番はすぐにやってきた。私が所用で席を外していたときにいつの間にか蹴られていたのだ。これは納得いかない。この横暴なホストに一泡吹かせてやるべきだ。こんなザコとは違って私には負けるビジョンが見えない。戦いが幕を開けた。次々と影分身する私と片っ端からそれらをハンマーで殴り続けるホスト。果てのない戦いに見えたが、私の分身が200体目を少し超えたあたりでサーバーの調子が鈍くなり、220体目あたりで見慣れない真っ白な画面にJavaServletのエラー文が表示された。アプリケーションがクラッシュしたのだ。後日私は運営元からIPアドレスでBANされた。
確かに私はサーバーに負荷をかけた結果、アプリケーションをクラッシュさせて本来得られるはずの広告収入の機会損失を引き起こした罪はある。しかし、適切にメモリを解放できず、スロットリングもせず、たかだか200人程度のユーザー情報を管理できない設計に課題があるし23、そんなアーキテクチャを採用していた運営側にも落ち度がある。メモリは無限ではないし、ガーベジコレクタを使っているならプログラマが期待したタイミングで解放なんてしてはくれない。このサービスも時代の流れで消えていったが、メモリ管理の話になるといつもこの経験を思い出すのだ。
キューイング(約束)をする
じゃぶじゃぶリソースが使える状況であれば、金の力で殴ればいいが、そうもいかない場合に取れる手段としてひとつ考えられるのはスロットリングやキューイングをしてしまうことだ。サーバーひとつだけで捌き切ろうとしてはいけない。昨今のECサイトでも競争率の高い商品に対しては待合室で待たされることが増えてきた。リソースが限られているのであれば、入口から流入数を抑えて正常に稼働させつつ、その範囲内で次から次へと捌いていけばよいという考えだ。待つほどの熱量がなければそこで離脱もするので一石二鳥である。
また、人が殺到した結果サービスが停止してそもそも受け付けてもらえないということよりも、受け付けた上で待たされることのほうが納得感がある。5年待ちの冷凍餃子のように、あまりにも待たされて注文したことすら忘れてしまうとなると話は変わってくるが、自分の要求が受け入れられたという事実が大切で、時間がかかってもそれを果たすことで、やがて信頼に繋がっていくようにも感じられる。
キューイング用のアーキテクチャやサービスも使ってみればそれほど難しくないものだが、独特の概念ではあるので、慣れるまでには少し時間がかかるかもしれない。私はフルマネージドのほうが管理が楽なので主にAWSのSQSを利用しているが、数年運用してみてはじめてその仕組みとありがたさが分かってきた。悩ましいことに、ある程度の規模になるとキューイングなしにはサービスが成り立たない部分も出てくるのだ。