cocone共通プラットフォーム障害対応記録

こんにちは。エンジニアの L です。
今日はアプリクライアント側とサーバーやインフラエンジニア数人が協力し、障害対応をした話を紹介したいと思います。

2020年6月頃、社内で使っているココネのアプリを紹介する機能を持っている共通プラットフォームで問題が発生し、障害になりました。
リリースされてから2年間、正常にサービスが提供されていましたが、潜在的なバグが存在し、2020年6月の平日午前11時頃、突然、お客様が共通プラットフォームの機能を使えない症状が発生しました。

影響範囲が全アプリで及んでいたため、直ちに運営側と連絡を行い、お客様へお知らせしました。

お客様にご不便をかけることになっていたため、担当エンジニアが協力し合い、障害が起きたその日に正常にサービスを再開できるよう対応に向かいました。

障害の内容

共通プラットフォームは、iOS / Android 側のクライアントSDKと、サーバーサイドとして適用しています。

今回の問題の原因は、iOSのクライアントSDKからサーバーに送る際の https 通信で、SDKのバージョンなどが分かるように User-Agent を定義していたところにありました。
その SDK で User-Agent のヘッダーを生成する際、C/C++で書かれていた部分で char型の配列を使っていたところをNULL文字で初期化したものから、文字列として使われていない部分のバッファー全サイズ(256bytesくらい)を渡して送るところでした。

User-Agent 情報はHTTPの仕様上、ヘッダーにキーと値の形として渡します。また、それは文字列であることが前提です。

そのため、nginx や Apache などのWebサーバーでは文字列としてパーサーが正常に動かず、400 Bad Request を返すようになっていました。今回のようなリクエストは基本的には nginx, Apache などのウェブサーバーでは正常に処理できません。

なぜリリース後2年間は問題がなかったのか?

パブリッククラウドを使った際に Load Balancer を経由し、nginxから共通プラットフォームのアプリケーション・サーバーに通信していました。ここでは問題なく通信できていたことになっていました。

そのため、おそらく Load Balancer 内でイレギュラーパターンのヘッダーリクエストを(不具合で)うまく処理していたと推定しています。

それが正しく修正されたバージョンが Load Balancer に適用された際に障害になったのではないかと思います。

  • リクエストの流れ

具体的には以下 の問題がサービス障害の原因でした。

① Mobile Client Application からhttps通信でUser-Agentの値で文字列以外のNULL文字の256bytesのバッファー初期化バグ
② Public Cloud L7 Load Balancer について、従来、①から複数のNULL文字が埋まるようなイレギュラーなヘッダーリクエストで通信できてしまっていたところ、修正により正しくNGとして400 Bad Requestを返して通信が失敗

障害の調査

2年間問題なくサービスができていたため、問題が報告されたときにクライアント・サーバーのログやクライアントデバッグを行って調査し、クライアントSDK側に問題があることを突き止めました。

クライアント側のパッチのほうまで作成したものの、ココネのほぼ全サービスでの共通プラットフォームであるため、問題を 100% 解決するためには、すべてのココネのアプリを更新してストアに出し、お客様に更新していただかないといけませんでした。

2つの流れを考えながらエンジニア同士で議論をし始めました。

  1. サーバー対応を検討しながらクライアントSDKのバグ修正を行った上、いち早くSDKを配布して全サービスに適用する。
    問題解決完了まで7週間から3週間を目処
  2.  並行して、最優先で機能を復旧できるように問題が起きているクライアントSDKをそのままでもサーバーやインフラ側での対応方法を見つけて適用する。
    対応のやり方次第で24時間以内に対応を目処

障害の対応:サーバーやインフラ側対応

障害が起きたその日にクライアントSDK側の原因特定ができたため、すぐにパッチを準備して配布まで完了しました。

一番早く問題を収める方法は、サーバーサイドの対応であることは間違いありません。クライアントSDKの対応はクライアントエンジニアに対応してもらいつつ、サーバーサイドの対応をいくつか試して解決しました。

最初のアプローチは以下の2つでした。

① パブリッククラウド側の L7 Load Balancer で問題を回避できるか。
② L7 Load Balancer はそのまま使いながら nginx 側のヘッダーのパースオプション変更で回避できるか。

nginx.conf 周りからの回避設定として、以下のような設定を試したりしました。

ignore_invalid_headers on;

検証してみたところ、残念ながら設定を変えても回避ができませんでした。

Load Balancer 周りはパブリッククラウド側のもので変更ができないため、次のアプローチとして、nginx そのものをソースコードをビルドさせながらデバッグを行ってみて、nginx 内部で回避できないかを検証しました。

nginx にデバッグログを吐き出しの設定の上、問題があるリクエストの内容を curl で再生してみたところ、基本的にヘッダーの値の部分そのものにNULL文字の配列のパースが失敗して 400 Bad Request を返すところを集中してデバッグを行いないました。

  • nginx 側のログ
    ※パケットキャプチャを用意にするため http 通信に臨時的に変更
2020/06/05 19:02:04 [debug] 80664#80664: *838 http process request line
2020/06/05 19:02:04 [debug] 80664#80664: *838 http request line: "POST /api/check HTTP/1.1"
2020/06/05 19:02:04 [debug] 80664#80664: *838 http uri: "/api/check"
2020/06/05 19:02:04 [debug] 80664#80664: *838 http args: ""
2020/06/05 19:02:04 [debug] 80664#80664: *838 http exten: ""
2020/06/05 19:02:04 [debug] 80664#80664: *838 posix_memalign: 000056066A0035D0:4096 @16
2020/06/05 19:02:04 [debug] 80664#80664: *838 http process request header line
2020/06/05 19:02:04 [debug] 80664#80664: *838 http header: "Host: 10.254.254.1"
2020/06/05 19:02:04 [debug] 80664#80664: *838 http header: "Content-Type: application/x-www-form-urlencoded"
2020/06/05 19:02:04 [debug] 80664#80664: *838 http header: "Connection: keep-alive"
2020/06/05 19:02:04 [debug] 80664#80664: *838 http header: "Accept: application/json"
2020/06/05 19:02:04 [info] 80664#80664: *838 client sent invalid header line while reading client request headers, client: 10.0.0.1, server: api.host.domain, request: "POST /api/check HTTP/1.1", host: "10.254.254.1"
2020/06/05 19:02:04 [debug] 80664#80664: *838 http finalize request: 400, "/api/check?" a:1, c:1
2020/06/05 19:02:04 [debug] 80664#80664: *838 event timer del: 19: 67825888034
2020/06/05 19:02:04 [debug] 80664#80664: *838 http special response: 400, "/api/check?"
2020/06/05 19:02:04 [debug] 80664#80664: *838 http set discard body
2020/06/05 19:02:04 [debug] 80664#80664: *838 HTTP/1.1 400 Bad Request^M
Server: nginx^M
Date: Fri, 05 Jun 2020 10:02:04 GMT^M
Content-Type: text/html^M
Content-Length: 166^M
Connection: close^M
2020/06/05 19:02:04 [debug] 80664#80664: *838 write new buf t:1 f:0 000056066A010CE0, pos 000056066A010CE0, size: 145 file: 0, size: 0
2020/06/05 19:02:04 [debug] 80664#80664: *838 http write filter: l:0 f:0 s:145


ログ分析とソースコードを動かしながらのデバッグを行い、nginx サーバーのソースを修正することで基本的にリクエストに対して 400 Bad Request エラーになる部分を回避できる部分があることまでは見つけました。

今回、回避させた箇所は ngx_http_parse_header_line 関数のところです。ヘッダーの値の後ろにスペースが入っている場合はスルーするところがあって、NULL文字の場合にスペースと同じく処理させることでの修正を行いました。

  • src/http/ngx_http_parse.c 周りの修正箇所

インフラのメンバーと、Linux Virtual Server / HAProxy のような Load Balancer を別途構築するか、DNS ラウンドロビンでクライアントからのリクエストをパッチ対応された nginx に到達させるかなど議論しました。

ところが、パブリッククラウドでは L7 の他に L4 の Load Balancer も作成できることに気づきました。そこで、既存のL7から新規に作成した L4 へ切り替えることでリクエストを通すことができるようになりました。

まとめ

今回は問題解決に向けて、準備を行い、緊急対応に向き合ったときのココネのエンジニアの1つの事例として記事を書かせていただきました。オンラインで多くのお客様に対してのサービスを行っている以上、自分達のミスから発生する障害だけではなく、外部的な要因でも障害は起こりうるものだと認識しています。

障害の原因も様々な状況に置かれている場合もあるため、一番大事な姿勢は問題が発生したときにどれだけ早く対応ができるのか、その対応の方法は正しい判断であるのか、時間との戦いですが、1つ1つ慎重にかつ丁寧に試す必要があります。

普段、安定したサービスができていることは裏で我々のエンジニアが頑張っていることであることで、障害が起きた時にすばやく対応ができるように、日々、様々な分野に興味を持ってスキルを磨いて行く必要があります。

ココネのエンジニアは、日々、協力し合いながら問題解決をしながらサービスを作っています。よいエンジニアを常に募集中です。

参考