海外チームでは英語圏だけでなく、スペイン語圏やアラビア語圏や、その他いろいろな地域・ユーザーの環境に合わせてサービスをローカライズしながら展開しています。東南アジアや南米では日本に比べるとネットワークは不安定で遅く、現地に行って自分たちのサービスを使うと読み込みの遅さに愕然とすることがあります。レシピサービスにとって画像の読み込みの速度は重要なので、これまでもレイテンシ、フォーマット、圧縮率、キャッシュ、画像サイズ、リクエストの優先度、プリロードなどさまざまな最適化を試みてきました。今回はスレッドプールのサイズについて考察しました。非同期処理とスレッドプールAndroidには、UIを操作することができる唯一のメインスレッドと、APIや画像のリクエスト、DBの読み書きなどの時間のかかる処理でメインスレッドをブロックしないためのワーカースレッド(ユーザーから見えない裏側で実行されることからバッググラウンドスレッドとも呼ばれる)の、2種類のスレッドがあります。Androidではメインスレッドとワーカースレッドの処理のやり取りする方法はいろいろありますが、その一つにExecutorフレームワークがあります。Executorは内部にキューを持ち、Runnableを実装したタスクをexecuteメソッドで実行します。図にすると下のような感じです。メインスレッドでリクエストを生成して、ワーカースレッドでタスクを処理するようなデザインパターンを、プロデューサー・コンシューマーパターンと呼びます。このコンシューマーすなわちワーカースレッドはタスクが要求されるたびに生成することもできますし、スレッドプールを使って再利用することもできます。待機させておくスレッド数や、最大のスレッド数などは実装に依存します。Picassoは内部にFixedThreadPoolを持っていますが、ネットワークの接続状況に応じて自動的にプールサイズを調整するようになっており、以下のように変動します。3しかし、いくつかの理由からこの値は最適でないのではないかと思っていました。TechCrunch通信規格というのは実際の通信速度を示すものではありません。日本ではLTEよりWiFiの方が速いですが、ブラジルでは逆にWiFiの方が遅かったりするので、通信規格に依存した設定は適切ではなさそうです。画像のリクエストをタスクにしてキューに送るワーカーがキューからタスクを取り出して画像のリクエストをサーバー送るストリームをデコードしてBitmapに変換するメモリとファイルにキャッシュとして書き込むワーカーからメインスレッドにBitmapを渡して描画するこのように多くの時間がI/O待ちで占められています。ネットワークが遅いほどスレッドの数を増やした方が良さそうです。今回は東南アジアや南米を想定して下り500kbps前後の環境に最適化します。ユーザーの環境に近いのでエミュレーターのネットワークスピードとレイテンシをEDGEに設定して検証しています。画像のフォーマットはWEBPで品質は70、実際の表示サイズをパラメーターに与えてリクエストしていて、大きくても100KB以下になっています。サービス率を調べることでワーカーの利用率、システムの内のタスクの数、システムの平均待ち時間を算出することができます。平均到着率はアプリの使い方によって変わるので正確に求めることは難しいのですが、ワーカースレッド数を1に固定した状態で、何も考えずに適当に検索したりしながら測定したら、以下のようになりました。1.3330.24秒ごとにリクエストが来ていたので、逆数をとると1秒間に4.16回のリクエストが来ていることになります。タスクの処理時間は0.75秒だったので、1秒間に1.3個のタスクをさばくことができることが分かります。到着率とサービス率から、ワーカーの利用率は以下の式で求めることができます。ユーザーが画像の表示を待つために操作が中断される状態になるということが分かりました。ワーカー数を増やした場合に、ワーカーの利用率やキューの長さやキューの滞在時間がどうなるかを表にしました。ワーカー数が3以下だと利用率が1を超えてしまうためキューの長さや待ち時間を計算することができません。この表からスレッド数を7より増やしてもほとんど効果がないと言うことが分かります。待ち行列理論は、コンピューターに限らず一般的な事象に対しての式なので、今回のケースでそのまま適用可能かというのを少し考えみましょう。待ち行列とスレッドプールワーカーの利用率ρを計算しましたが、これはあくまでもワーカーがタスクを持っているというだけで、CPUの利用率とはまた別になります。コンピューターはI/Oなどでコンテキストスイッチが発生するため、ワーカーがタスクを持っていてもCPUを使っているとは限りません。待ち行列理論はよくレジとレジに並ぶ客に例えられますが、今回のケースではレストランのウェイターと客の関係に近く、注文を取ったあとウェイターは料理が出てくるまでの間に厨房の前でぼーっとしているのではなく他の客の注文が取れそう、ということです。調べてみたところ、スレッドプールサイズの最適化にはいくつかの式が提案されていることが分かりました。よると、I/Oバウンドな処理は以下の式に当てはめることができます。タスクに対するCPUが遊んでいる時間と計算している時間の比率になります。ExecutorとDownloaderにフックして測定したところ、平均待ち時間は756msで平均応答時間は1082msなりました。3.32上記の式から係数は3.32となり、CPUコア数にI/O待ちが長くなるほど大きくなる係数がかかっているので感覚的には良さそうに見えます。私たちのサービスのユーザーに多い2コアの端末では6.64スレッド、4コアの端末では13.28スレッドという値になりました。待ち行列理論の式と照らし合わせてみても係数3は妥当な値に見えます。いうライブラリがあり、人気を二分していますが、GlideのスレッドプールサイズはCPUのコア数に等しくなるように設定されています。いうことでPicassoもGlideもだいたい2〜4スレッドになっていることが大半ということで、上記の計算結果の13スレッドはもしかしたら多いのかもしれません。スレッドプールサイズが大きすぎる場合、同期のためのロックでのリソース消費であったり、他のスレッドが頻繁にプリエンプティブされるようになったり、スレッドはいるだけで数K〜Mのメモリを消費したりしてしまうので、あまり大きな値を設定するのは避けたいです。アプリでのスレッドプールサイズの最適化はリソースを使い切ることが目的ではなく、ユーザーが快適にアプリを使えるようにするのが目的なので、画像の読み込みだけにリソースを使ってしまうと、その他の動作に影響を及ぼす可能性があります。そのため、もう少し詳しくこの問題を見ることにしました。ユースケースを考えてみる話は変わりますが、出張の際には現地のユーザーさんに声を掛けてユーザーテストをさせてもらうことがあります。ユーザーテストは「今日のメニューを決めてください」のようなシナリオをやってもらいますが、ユーザーは画像の読み込みを待ってから次のページを見る、というような使い方をしていたことを思い出して、実際自分もそうだなと思いました。こんな感じに計測してたけど実際のユーザーは検索していきなり全力で下までスクロールしたりしない!つまり、画面内に表示される画像の数よりワーカースレッド数を多くしても無駄になってしまう可能性が高いのではないかと思いました。私たちのアプリだと一画面に表示される画像の数は6〜8個(解像度に依存)くらいなので、ワーカー数の最大値は8が適当でしょう。小さい方を取ると最適になりそうという結果になりました。MAX_NUMBER_OF_IMAGES_IN_SCREEN)たとえば、2コアの端末で3G回線のユーザーのデフォルトのワーカースレッド数は3だったのが、この式では6スレッドになるので、厳密にどれくらい改善するかは数字にしづらい(スレッド数によってユーザーの行動が変わり、到着率が変わり、計算結果が変わる)のですが、もしキッチリ画像の表示を待った場合に比べると、画像の待ち時間が半分になり、レシピが2倍早く決まる!(可能性がある)ということになります。今回の検証ではこのような結果になりましたが、どういうサービスでどこに最適化するかによって結果は変わってくると思います。つぶやきサービスで「ラーメンの様子です」というつぶやきと共に貼られたラーメンの画像は重要度はそこまで高くないので、貴重なリソースは画像読み込みのワーカースレッドより他に回した方がいいと思いますし、レストラン検索サービスでリソースをケチって1スレッドにすると、デートの店選びに使いたいのに、忘年会の幹事で早く店を決めないといけないのに画像の表示が遅くてイライラするなど、そういうこともありそうですね。(@hokaccha)です。〜舞台裏を支...2017-05-10最近のサービス間のデータとイベントの連携についてこんにちは。使ったセキュリティログ検索基盤の構築サービス特性にあった検索システムの設計戦略クックパッド採用説明会「クックパッドはサービスの作り手を採用したいんです。/*}*/スターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。