契約している会社のバグ報告会(こういう取り組みは良いですね)で、登壇する事になったので、
今までにやってしまったり、遭遇したバグやオペミス・障害についてまとめてみました。

場数(バカず?)を踏むのは大切だけど、出来ればヒヤリ・ハットで済ませたいですね。
失敗した事がない人はいないと思いますが、失敗のまま終わらせず、そこから学びを得るのが大切。
新しい事に挑戦すると失敗の確率は上がりますが、失敗を受け入れられる人や組織は成長します。

「Failure is success if we learn from it.」(マルコム・フォーブス)
→ 失敗から学ぶことができれば、失敗は成功である。

バグの原因は確認不足や誤解以外にも想定外(経験や知識不足、想像を超える)ものもあります。
最初から経験や知識がある訳でもないし、完璧にできる訳でもないので、
段々と感度を上げて行けると良いですね。

「In the race for success, speed is less important than stamina.」(マルコム・フォーブス)
→ 成功を目指すレースでは、速さよりも持久力の方が重要である。

1. Apacheを再起動したらページが真っ白に

引き継ぎ案件で、サーバーが本番のみで1台しかない。GitやSVNもない。
PHPのアプリ(OpenPNEをカスタマイズしたもの)が正しく動かなくなったので、
Apacheを再起動する事に。

CentOS 5か6

# service httpd status
httpd (pid xxxx) is running...

# service httpd restart
Stopping httpd:                                            [  OK  ]
Starting httpd:                                            [  OK  ]

ブラウザでアクセスするとページが真っ白に。

原因

yumでインストールされたApacheではなく、ソースでインストールされたApacheが使われていた。

statusでは、httpdのプロセスが起動していると言っているけど、
yumでインストールしたのが起動している訳ではなかった。

httpd (pid xxxx) is running...

起動しているプロセスを確認した方が良かった。

# ps aux | grep httpd
daemon  ・・・  /usr/local/apache2/bin/httpd

この頃から、インフラの学習に力を入れるようになり、結果的に次の会社で役立ちました。

2. UPDATE文にWHERE付け忘れて全件更新

業務では、SELECTで対象を確認してから、UPDATEやDELETE文に直して実行していましたが、
個人サーバーだったので、まあいっかと実行してしまった。

リカバリー

日次でDBダンプを取るようにcronを入れていたので、リストアして、差分を手動で再登録して復旧。
やってしまうと、余計に時間が取られてしまいます。

変更を実行する前に、指差し確認をより意識するようになりました。

3. サーバーのフェールオーバーでデータが先祖返り

複数の案件とPM・メンバーをマネージメントしていた時の話。
フェールオーバー(2台目に切り替わる事)が発生し、日中の変更が元に戻る現象に遭遇しました。

原因

フェールオーバーはkeepalivedでされていたが、ファイルは深夜にrsyncされたのが使われていた。
切り替わったらデータが失われるのは仕方ないと言っていたのが衝撃だった。

当時はVM入れた物理サーバーだったので、DRBDでの作り直しを障害報告書に記載して実施。
サーバー構成やフェールオーバー時の挙動について、レビューしておけば良かったと反省。

4. deadlock多過ぎで連携遅延

利用が増え始めた頃、そのエラーは増え始めた。
頻繁に複数のバッチを起動し、外部連携を行うシステムなので、deadlockが発生すると、
そのタイミングでは更新されず、次回のバッチ起動時に処理する事になる。
ゆえに想定よりも遅れて連携される事になるし、連続で発生すると何回も待たされる事になる。

原因

同時に実行される複数のバッチで同じテーブルを更新しようとしていたのが原因だったので、
テーブルを処理単位(≒バッチ単位)に分割するように再設計して、ある程度解消。

複雑になっていたキューテーブルを廃止して、各データにステータスを持たせるように変更したが、
処理単位を意識して、1対1のテーブルに分割すべきだった。

5. クレカの支払回数を指定しても1回払いに

引き継ぎ案件で、新たに導入した顧客でのその問題は発生しました。
モールで分割払いを指定したにも関わらず、請求は1回払いになってしまいました。

原因

請求のAPIでは、支払回数を指定する必要がありましたが、ベタ書きで1回になっていた。
受注時に支払い回数も連携されるが、この値は使われていなかった。

理由は、最初に導入した顧客が1回のみしか使わなかったから。
(後日、膨大な設計書に記載されていた事に気付く)
ファーストユーザー向けに作成したシステムを横展開した為、その仕様が継承されてしまった。

テストしても実際に請求される訳ではないので気付きにくい。
引き継ぎ期間も無かったしな〜

6. CORSエラーで一部ユーザーが表示できず

管轄していたチームで、Lmabda@Edgeでアクセス制限を実現した時の話。
アクセス制限の実装自体は、事前のテストで問題なく表示できる事が確認されていましたが、
表示されないユーザーが出てしまいました。

原因

CloudFrontのビヘイビアを追加した際に、許可されたメソッドにOPTIONSが含まれていなかった。

顧客サイトに埋め込むタイプのASPサービスでは、ドメインが異なる為、OPTIONSリクエスト(プリフライトリクエスト)が発生する。

ビヘイビアを追加する前にリクエストした関係者は、OPTIONSリクエストが既に通った状態(ブラウザが判断)だったので、表示できたと推測される。
また、関係者以外にも見れた人もいたので、CDNでキャッシュされたサーバーに、たまたま当たっただけと推測される。

有効期限やキャッシュの挙動も意識して、端末やブラウザを変えてテストすべきだった。
が、CORSやCDNの知識をすべてのエンジニアに求めるのも酷だと思う。

7. バッチの実行タイミングが同じで連携先が高負荷に

プロダクトをベースに、カスタマイズして納品していた案件で、定期的にステータスを連携先に確認する処理があり、ある時から連携先(自社プロダクト)が高負荷になるようになった。

原因

ステータスを確認するバッチの実行タイミングがまったく同じ時間で、案件数も増え続ける。
かつ、未完了も永遠にリクエストし続ける仕様になっていたので、対象数も増え続ける。

sleepをランダムで入れて、秒数をズラすのと、
未完了で、一定期間経過したものは対象から外すように改修を行い、
各案件に通知して対応を促しました。

8. クラウドの制限に引っ掛かりサービス停止

他部署の話でしたが、一部の案件で連携していた為、自部署の案件も影響を受けました。

原因

非常にアクセスの多いサービスで、GCPを使っていて、
クオータ(制限)の1日あたりの上限に達して、停止されてしまいました。
起点の時間(時差があるので24時ではない)になれば解除されますが、
急ぎ緩和申請も行なったそうです。

AWSでも同様に制限があり、どんな制限があるかを把握しておく事と、使用量の監視
イベント等で増えそうかの確認や予測をして、事前に緩和申請しておく事が大事です。

制限があるのは、一部ユーザーの使用量で全体が落ちないようにする為と、
使用量を予測して、ハードを調達する為で、物理サーバーを意識させられますね。

9. クラウドみたいに制限がなく一部顧客のアクセス過多で障害に

今まで携わってきたASPサービスの全てですが、顧客毎の制限がある所はありませんでした。
サービスを立ち上げる段階で制限に開発コストを掛ける事は難しく、成長しても費用を沢山払ってくれている所に制限を掛けるのは、解約リスクが高まるので難しいという事情もあります。
上位数十社が売上の大部分を占めているサービスだとこの傾向が強く、上位顧客に振り回される残念なサービスになってしまいます。

対策

該当サービスでは、数時間停止してしまった事を重く受け止め、
複数の対策を実施して信頼を取り戻せたようです。
・ロードバランサーをBIG-IP(高額ですが)に変更して、アクセス制限を導入
・負荷の高い処理やDBの見直し
・24時間有人監視(安定するまで)

10. 決済代行会社の障害で月額契約がすべて解約に

月初の0時台は、サブスクの決済リクエストが大量に行われ障害になりやすい時間帯だそうです。

「決済代行会社」(ここと契約)→「中間カード会社」→「対象のカード会社」
大雑把にこんな感じで連携していて、その時は中間カード会社がエラーを返却した事が原因でした。

もう1つの原因

事が大きくなったのは、実装でエラーコードの定義が不足していた為、解約扱いになったから。
本当は、カード番号を変更させるフローがあった方が良いんだけど。。。

そもそもエラーコードは連携先で発番されたものを決済代行会社でまとめているだけだったり、
オーソリエラー(利用限度額超過)でも直後に実行すると成功したりする。
ゆえに、エラーコードに関係ないなく、リトライ処理を入れる事が推奨されている。

通常の連携では、エラー内容により、リトライするか決めるのが設計としては正しけど、
連携相手によって正解は変わる

幸い全ての案件が0時台に実行していた訳ではなかったので、影響は限られました。

11. 自前でキャッシュを実装したら紐付け先が違ってしまった

Railsでは、SQLキャッシュが機能するので、基本的には自前で実装する必要はないのですが、
taskでは効かなかったのと、自前の方が若干早い(らしい)ので、実装しました。

殆どの箇所では、1つのコードに対して、IDが一意なので問題なかったのですが、
親子コードで一意で、子コードに同じ値が使われるケースで紐付け先が違ってしまった。

原因

検索は親と子のコードで行っていましたが、キャッシュのキーが子コードのみだった。

[DB]ID,親コード,子コード,・・・
1,P001,C001,・・・
2,P002,C001,・・・

[CSV]親コード,子コード,・・・
P001,C001,・・・ → ID: 1、キャッシュ保存: C001 => 1
P002,C001,・・・ → ID: 2の筈が、キャッシュ取得: C001 => 1

1つのコードに対して実装した処理をそのまま横展開して漏れてしまった。
RSpecも同様に、このケースが漏れてしまった。

十分テストしたつもりでも、バグがない保証はないので、リカバリーを意識した設計にした方が良かった。元の値がDBにあれば、切り分けやリカバリーも容易にできる。

まとめ

今回、思い出しながら改めて考えてみましたが、言語化すると状況の整理や改善策が明確にできるので良いですね。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です