RoomClip 開発者ブログ

JPEG 変換をすると画像はどう劣化するのか?

JPEG は非可逆圧縮の画像フォーマットとして有名です。今回は JPEG 圧縮するとどう劣化するのか、そしてそのメカニズムを紹介します。

圧縮率を上げる事により発生する劣化

JPEG で圧縮率をあげると以下のような現象が見られます。

モスキートノイズ

元画像(png)q=100q=80q=40q=20

クオリティ1を下げると文字やその周りに汚れのようなノイズが発生しています。これはモスキートノイズやリンギングと呼ばれます。大きく色が異なる箇所付近に目立って発生します。

ブロックノイズ

元画像(png) q=100 q=60 q=40 q=20

なだらかだったグラデーションが、クオリティを下げると四角い形での歪みが見られるようになります。これはブロックノイズやブロック歪みと呼ばれています。

ポスター化

元画像(png) q=20 q=10 q=5 q=3

さらにクオリティを下げると、グラデーションが荒くなり、色数が減っていきます。

JPEGの劣化を引き起こすメカニズム

ブロック分割

JPEG は 8x8 のブロック2に分割してそれぞれに対して圧縮処理を行います。そのため、圧縮による劣化はブロック単位で変わる事になり、ブロックの境界で劣化が目立つ事になります。これがブロックノイズとなって現れます。

JPEG(2倍拡大) 4倍に拡大 8x8で線を追加

上の例でいうと、細かい劣化の目立つブロック(薄い赤い部分)と目立たないブロック(青い部分)の境界(赤い部分)で劣化が目立ちます。

離散コサイン変換と量子化

JPEG は各ブロックに離散コサイン変換という変換を施した形式で保存しています。

離散コサイン変換の詳細の説明は省きますが、JPEG の 8x8 のブロックに対して離散コサイン変換を施すと下図のような 64 種類のパターンの組み合わせに分解されて表現されるようになります。

例として「A」という文字の32x32(つまり4x4ブロック)の画像を離散コサイン変換します。

変換前 変換後の64パターン 左上からブレンドするとこうなる

ポイントとしては、以下のようになります。

  • 変換すると64種類のパターンに分解される
  • 64種類のパターンをブレンド3するとちゃんと元の画像になる

左上のパターンは低周波成分と呼ばれ、ブロック全体の色を決めるのに使用します。一方で、右下のパターンは高周波成分と呼ばれ、ブロック内のより細かい色の変化を表現するのに使用します。

ところで、離散コサイン変換を行うだけでは画像の劣化は起きません4。しかし、JPEG ではさらに各パラメーターの値を近似値に丸めるため、それによって品質が劣化します。これを「量子化」と呼びます。

圧縮品質パラメーター

グラフィックソフトなどで JPEG を書き出す際に 0〜100 指定する品質を設定した経験があるかもしれません。100 の場合はほぼ正確な値を残すと思われますが、数値を下げると思い切った近似をするようになります。

実はこの数字によってどのような近似を行うかはソフトウェア毎に異なります5。なので、同じ品質の数値でもソフトウェアによっては劣化の仕方が変わります。

また、JPEG ではパターン(周波数)毎に近似の強さを変える事ができます。例えば、上の図で言う左上の低周波成分を強く近似すると、ブロックの色がまんべんなく変わります。一方、右や下のストライプや、右下のチェックのような高周波成分を近似すると、縞模様やチェックが浮き上がって来る事になります。これがモスキートノイズの原因になります。

高周波のパターンが人間には判別しづらい事がわかっているので、多くのソフトウェアでは左上よりも右下を優先的に近似をします。

JPEG の色空間とダウンサンプリングによる劣化

量子化によって情報を落としている事が JPEG の画質劣化の主な原因ですが、JPEG の画質に関しては YCbCr という色の表現方法を採用している事も関係しています。

YCbCr とモスキートノイズ

先ほどのモスキートノイズの画像を拡大してみます。

画像は白と赤しか使ってないにもかかわらず、うっすらと青いノイズが見えるのではないでしょうか。これは一般的な RGB ではなく YCbCr で表現している事から発生するものです。

YCbCr では色を輝度(Y)と二軸の色差で表現します。

元画像 輝度と色差

JPEG では Y/Cb/Cr それぞれの成分に対してブロック分割し、圧縮を行います。そのため、圧縮に伴う歪みは「輝度の歪み」と「色差の歪み」という形で発生します

例えば、輝度成分に対してモスキートノイズが発生すると、縞模様やチェック状の「輝度の歪み」が発生します。一方で色差成分に対してモスキートノイズが発生すると、縞模様やチェック状の「色差の歪み」が発生します。先ほどの画像で青いノイズが発生したのは、色差成分のモスキートノイズが原因です。

クロマサブサンプリング

JPEG では上で説明した量子化による情報の切り落としの他に、色差の情報を落とす事があります。

例を挙げてみます。以下は品質を100の状態でPNGからJPEGに変換したものです。

元画像(png) q=100

最高品質にしたのにもかかわらず、緑と紫のチェックがグレー6になってしまいました。ストライプの部分もぼんやりと色がついているもののほぼグレーです。

この劣化はクロマサブサンプリングと呼ばれる操作が行われているのが原因です。クロマサブサンプリングは YCbCr の色差成分を縮小する事でデータ量を減らす方法で、人間が色の変化よりも明度の変化に敏感であることを活用しています。サブサンプリングの方式は 4:4:4 (間引かない)、4:2:0 (縦横方向で半分間引く)、4:2:2 (横方向だけ間引く)などがあります。上の例では 4:2:0 の形式でサブサンプリングを行っています7

4:4:4(間引かない) 4:2:0 4:2:2

クロマサブサンプリングとブロックノイズ

JPEGのブロック化や離散コサイン変換の処理は、クロマサブサンプリングされたものに対して行われます。そのため、色差成分に関してはより広い範囲(4:2:2だと16x8、4:2:0だと16x16)でブロックノイズが発生します。

元画像(png) q=20(4:4:4) q=20(4:2:2) q=20(4:2:0)

  1. JPEG への変換は ImageMagick 7.0.9-8 を使用しています。 ↩︎

  2. 8x8 以外のサイズに指定する事も可能です。 ↩︎

  3. グレーより明るい色でブレンドするとより明るくなり、暗い色でブレンドするとより暗くなる、といったブレンドになっています。 ↩︎

  4. 実際には丸め誤差で小さな劣化が発生します。 ↩︎

  5. Independent JPEG Group という団体により作られた方法が一般的ですが、例えば Adobe 製品はこれとは別の独自の量子化ロジックを使っているようです。 ↩︎

  6. なおこの画像は色差の平均をとるとグレーになるように調整しています。エンコーダー(ImageMagickが使っているlibjpegだと思われます)が平均値をとってサブサンプリングを行っているため、このようになっています。写真の場合はこのように極端な色差の劣化が発生する事は少ないと思われます。 ↩︎

  7. ImageMagick のデフォルト ↩︎


この記事を書いた人:鷲田

ルームクリップのアプリケーションエンジニア。


TV番組「今夜くらべてみました」でRoomClipを紹介されて得られたたくさんのこと

みなさんこんにちわ。 ルームクリップ株式会社CTOの平山です。 残暑が厳しい日々ですね。

昨日の9日、表題の通り日本テレビ「今夜くらべてみました」でルームクリップが紹介されました。 ありがたい話です。

いつだってユーザさんに触れてもらうのは大変に喜ばしいことですが、 大きな衆目を集めるシーンでの言及となると、ちょっぴり特別な興奮がありますね。

これが「エンジニアとして」、となるとなおさらです。

なんてったって巨大トラフィックが襲撃してくるかも知れないわけです。 この手の興奮は上手にコントロールして、常に冷静に対処し、万全の対策で迎え撃たなければなりません。 ご多分に漏れず今回も我々は相応の準備をもって立ち向かいました。

というつもりでしたが、、、

結果として、今回はもっと興奮しておくべきでした。

TVは、

「今夜くらべてみました」という番組は、

私の想像のはるか上空を飛び、ずいぶん時間がたちこのブログを書いている今ですら、おおきな影響を及ぼし続けております。

本ブログでは、「こんくら」メンバーの影響力を甘く見ていた一人のエンジニアの猛省と、 これから同じ轍を踏むかもしれないすべてのエンジニアに向けての転ばぬ先の杖を記していこうと思います。

中には「え、こんなの当たり前でしょ、むしろなんでやってなかったの?」ってものもあるかと思います。

わかっております。不安にならずとも皆様の手厳しいご意見は、これ以上ないくらい私の脳幹をえぐっています。

ただ、時には包み隠さないことも大事です。

わたしは後ろ暗さを抱えて危険なシステムを運用しているすべてのエンジニアの味方です。 「正しい運用しようぜ」の流れを全力で社内に引き込むための道具として、ぜひ本ブログをお使いください。

その前に…

改めまして、今回突発的に発生しましたアクセスに対応しきれず、長きに渡ってサービスに障害を起こしてしまいましたことをこの場を借りて謝罪いたします。 期待をもってアクセスしていただいたすべてのユーザの皆様、また、すべての関係各位の皆様に対し大変にご迷惑をおかけいたしました。重ね重ね申し訳ありません。

以下、専門的な話になりますが公開できる限りにおいて、障害の経緯を共有させていただきます。

概要

簡単に言ってしまうと下記のようなことが起きました。

  • TV局から連絡を受けて流入トラフィックを推計した
    • 余裕で外れた
  • 単一障害点はスケールアップで大丈夫だろうと高をくくった
    • 余裕で外れてそいつのせいで全部落ちた
  • 分散しまくればなんだって耐えうるだろうと甘く構えた
    • ネットワークリソースについて忘れてて分散できず落ちた
  • 対策範囲なんてアプリケーションサーバとDBくらいだろ、って思った
    • バックエンド色々マイクロ化したため対策ポイントが増えててんやわんやになって気分が落ちた

一つでも「あ、これは対岸の火事じゃないぞ」と思ったエンジニアは今すぐ点検しましょう。 単一障害点は本当に危険です。当たり前ですが大事なことです。 「当たり前のことができていない」というケースは「当たり前に多い」と思ってます。 心当たりあるエンジニアは、今日だけは責めないので、そっと確認しましょう。

それでは、一つ一つ見ていきましょう。

TV番組のトラフィック見積もり

一般的には視聴率と世帯数から視聴者数を概算して母数を求め、 その後に「サービス言及中に見ている人の割合」とか「その時間帯に検索する人の割合」とかを適当に見積もって積算して当たりをつけるものだと思います。

大事になるのは瞬間最大風速req/secなので、秒単位で平坦にならすのではなく、適当にピーク分布を積算して出すのがまぁ妥当かと思われます。

今回わたしが見誤ったのは、「その時間帯に検索する人の割合」でした。 番組の性質上、検索やサービス起動を促す作りになっていて(これは本当にすばらしい!)、思った以上のリクエストが発生しました。数値でいうと2倍程度見誤りました。

でも、2倍です。

皆様、「危険だな」と思ったら妥当な計算のあと、「2倍」しましょう。

ちなみに私は、放送直前に「チュートリアル徳井さんがサービス名を発話するかも」という情報を追加で得ました。(このあたりの連絡網を強化することも必須ですね!)

この時点で実はかなり迷いました。今の対策が不十分である可能性がよぎっていたのです。

・・・皆さん、虫の知らせがやってきたら迷わず「最大値の2倍」にしましょう。

単一障害点への対策

これが本当に悔やまれるポイントです。 どの本にだって書いてあることです、こういうボトルネックは障害時にも影響を波及させないように対策しておけと。 それができないなら、どんなアクセスにも耐えられるような設計にしておけと。

ところで、RoomClipはiOS、Android、ブラウザ向けにサービスを展開しており、そのバックエンドとして概ね下記のようなクラウドサービスを利用しています。

  • アプリケーションサーバ/ストレージサーバとしてAWS
    • EC2、ECSを併用
    • DBはAurora
  • 画像配信/変換サーバとしてさくらインターネット
  • 検索サーバとしてElastic Cloud

共有リソースに対するAPIアクセス、が多発するような状態と言えるでしょう。

かつてのルームクリップにおいて負荷障害発生時の犯人の9割はDBでした。 よって我々はDBに対してどんな奇行種も入ってこれない防壁で守っておりました。 実際今回のアクセスに対しても唯一と言っていいくらい割と大丈夫だったのはDBです。 皆さんもそこは大丈夫だと思います。

問題は、かねてよりバックエンドで進めていたマイクロ化にあります。 特に検索サーバ・画像配信サーバ、このあたりはサービスの隅々で使われている重要な機能なので特別に本体設計から切り離し、大事に保守メンテしていました。それはそれでいいことだと思います。

だが、そのAPIの呼び出し部分が悪かった。

一部のページにおいて、APIのレスポンスをタイムアウトせず待つ、帰ってこなければExceptionを投げエラー終了する、というコードになっていました。

運命共同体コードです。

ぞっとしますね。

本当に反省しています。

本来であればサービスメッシュやenvoyレイヤのように、API接地面の安全な設計は暗黙的に処理する予定でした。 しかし、そのレイヤについてちゃんと決めきることを少し後回しにて、とりあえず(後からどうとでもなるような形で)切り離すということを優先していました。 これはこれで限られたリソースや検証という意味ではメリットのあることだと思います。 ただ外部環境は待ってくれません。 TV番組の話は突然来ますよ。 「まぁ一旦これでいって、ある程度わかったらちゃんと固めてこうや」 これは悪くない選択肢です。 ただ「リスクを受容した」ということはもっと強烈に認識すべきでした。

皆さん、モノリシックなシステムをDDD的思想でもって妥当な範囲でバラしたく気持ちはわかります。

しかし、いつでも想定しておいてください。

来週、あなたのサービスについてチュートリアル徳井さんが言及するかも知れないことを。

想定外のネットワークリソース枯渇

さて、異常を察知して直ちにリソースの水平スケールをアップした我々ですが、そこで地団駄を踏むことになります。

インスタンス・コンテナが全く増えません。

ルームクリップのAPIサーバ、Webサーバは基本的にECS(一部EC2)という状況だったので、オートスケーリンググループをいじれば続々と援軍追加されるはずでした。 が、いくらたってもエラーで立ち上がりません。AWSリソース制限かとも思ったのですが、つい最近あげたばかりです。

いろいろ調査してわかったことは、サブネットのPrivateIP枯渇でした。事情あってサブネットの許可IPのCIDRを厳しくチューンしていたため、ここの上限にぶつかってインスタンスの立ち上げができなくなっていました。

サブネットの設定なんてそうそう変更しないので、すっかり忘れておりました。 皆さんもそらで言えますか?あなたのサブネットは第何オクテットまでマスクしていますか?

あ、ごめんなさい、そらで言える必要は全くありません。確認しに行けばいいだけです。 ネットワークに限らず「リソース枯渇」系の話は結構緊急時に人をイラつかせるものです。

例えばnlimit、inode、エフェメラルポート、帯域制限、もちろんHDD容量。そして今回忘れてならないIP。

これは激痛で、リカバリの速度がだいぶ遅れた原因の一つとなってしまいます。

対策点の増加

先に触れたマイクロ化の流れもそうですが、BFFなども積極的に導入した結果、スケール対象となるインスタンスの塊は結構増えておりました。

端的にALBの数が多いんですね。

これは本質的にはそこまで問題ではないことなのですが、少ない人数でこれらのスケールアップを管理するというのは中々ストレスがたまります。 事前準備はまぁいいんですが、障害中「やべぇ!」となったときのあっちゃこっちゃいじる千手観音っぷりは悲惨です。 いろいろな対策がありえますが、Lambdaなりでサーバレスに切り替えていくのは非常に良い選択かもしれません。

とにかくウォームアップなんていらない!というのは大正義です。

まとめ

非常に長くなってしまいましたが、簡単にまとめますと、今回の障害の大きな原因はやはり単一障害点の対策不備になります。

RDB周りは特に手厚く対策されると思いますが、(当たり前ですが)転置インデックスを扱うサーバや、DynamoやRedisといった領域についても、「手厚くするか、緊急時に被害を軽微にするような設計にする」など、最低でも波及を抑える設計にすべきでした。

また、外部APIを使うときの接地面の設計(リトライ制御やブレーカーなどを含む、一般にenvoyレイヤなどで処理される諸問題)も非常に繊細になされるべきだということを改めて実感しました。

これ以外にも気づけなかった問題は多々ありますが、本ブログではひとまず猛省しつつ「速報」という形での共有とさせていただきたいと思います。

また、これからバックエンドをマイクロ化していくプレーヤーは多数存在していると思いますが、ぜひ改めて「管理すべきドメイン」が増えていくことにより、それぞれの障害点への対策やスケーラビリティの保証などが手薄にならないよう、気をつけていただけるとよいかと思います。

色々と課題はたくさんありますが、日々精進し、これからも皆様の日常にある「創造性」を応援して参ります。 何卒末永くルームクリップをよろしくお願い申し上げます。 改めまして、この度の大規模な障害、大変申し訳ありませんでした。

そして、本ブログを読んで、 「しかたない、、わたしがなんとかしてあげよう」と思うエンジニアの方、ぜひ遊びに来てください!

フロントもサーバサイドも!生産性の高いサービス開発基盤を作りませんか?



この記事を書いた人:平山知宏

ルームクリップ株式会社のCTO。 インフラからサーバサイド全般担当。 インダストリアル系の家に住みたいと思うけど、子供もできちゃったし危ないから工具とかほっぽっとくのやめようと思っている1985年生まれ。