Save 37% off PRO during our Black Friday Sale! »

グラブル流運用術〜1700万人を満足させるためのシステム構成、PHPプログラムの考え方〜

510ec964f5d26c2724c883fd7b671e3d?s=47 Cygames
October 10, 2017

 グラブル流運用術〜1700万人を満足させるためのシステム構成、PHPプログラムの考え方〜

2017/10/08 PHPカンファレンス2017

510ec964f5d26c2724c883fd7b671e3d?s=128

Cygames

October 10, 2017
Tweet

Transcript

  1. © Cygames, Inc. © Cygames, Inc. 1 / 120

  2. 2 / 120

  3. 3 / 120 自己紹介 【自分の目指すところ】 最速のシステムを 作りたい

  4. 4 / 120 Cygamesについて

  5. 5 / 120 Cygamesについて 【Cygamesのビジョン】 最高のコンテンツを作る会社

  6. 6 / 120 グランブルーファンタジーとは • 1700万ユーザーを超えたRPG • ブラウザゲーム • 30人が同時に戦うマルチバトル

    ※ ※2017年10月8日現在
  7. 7 / 120 グラブルの技術的特徴 大量のアクセスがある!

  8. 8 / 120 グラブルの技術的特徴 サーバ 攻撃ボタンやアビリティボタンを 押下するたびにリクエスト (約3秒に1回)

  9. 9 / 120 グラブルの技術的特徴 大量のアクセス 1日の総PV数10億PV強

  10. 10 / 120 グラブルの技術的特徴 騎空団(ギルド)同士のバトルイベント中 PV数 :20億PV/日 バトル数:1100万回/日 ※最大値

  11. 11 / 120 大量アクセスをさばくシステム構成 Web Server 1 twemproxy memcached 1

    … Application Servers Cache Servers Database Servers Master MySQL MHA Cluster 1 Apache + mod_php Node Server 1 Layer4 Load Balancer(BIG-IP) Client N … Web(HTTP) static data access Memcached Servers Client (Apps or Web Browser) 1 Akamai CDN … Nginx 1 … Application Load Balancers WebSocket(WS) Nginx N Redis 1 … Redis Servers Redis N 10G Ethernet 10G Ethernet Node Server N Web Server N … WebSocket (WS) Web(HTTP) Access Load Balancer / Proxy Sharding (水平分割) … N Node.js twemproxy memcached N Slave 1 Slave 2 MySQL MHA Cluster 2 MySQL MHA Cluster 3 Master Slave 1 Slave 2 Master Slave 1 Slave 2 mysqli Web Server 1 twemproxy Apache + mod_php mysqli local memcached local redis
  12. 12 / 120 大量アクセスをさばくシステム構成 データベースがたくさんある! アクセスが多いテーブルは水平分割してる 大量のアクセスをさばくためにDBを細かく分ける ※DBサーバーは100セット以上

  13. 13 / 120 グラブルは多くのユーザーがプレイ トータル1日10億オーバーのPV

  14. 14 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • 実際に改修してみた例

  15. 15 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • 実際に改修してみた例

  16. 16 / 120 今回の改修の目標 グラブルのシステムを もっと早くする!!

  17. 17 / 120 今回の改修の目標 • グラブルはたくさんの人が遊んでいて、大量のアクセスがある 「最高のコンテンツを作る会社」なので 「サーバーサイドエンジニア」として 「レスポンス改善」を目標にした

  18. 18 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • 実際に改修してみた例

  19. 19 / 120 改修するところを探す 具体的にどうするのか検討するために 下記の観点で調査した ・APIの特定 ・処理の見直し ・具体的な改修箇所

  20. 20 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • APIの特定

    • 処理の見直し • 具体的な改修箇所 • 実際に改修してみた例
  21. 21 / 120 APIの特定 • 重いAPIはどれだろう APIの重さが相対的にわかるツールが必要 New Relicで調査しました

  22. 22 / 120 APIの特定 【New Relicからわかったこと】 バトル処理全般に対して下記の現象が判明しました ・レスポンスタイムが高い ・throughputが高い ・most

    time consumingが上位
  23. 23 / 120 New Relicとは サーバでの処理時間がリアルタイムでわかるツール

  24. 24 / 120 レスポンスタイムが高い Transaction項目をみると 各APIごとの平均レスポンスタイムの ランキングが表示されている 上位にバトルのAPIが入っていた

  25. 25 / 120 throughputが高い throughput項目を見ると ほかの処理に比べて、バトルAPIはthroughputが高いの で高頻度にたたかれる

  26. 26 / 120 most time consumingが上位 Most time consumingの観点で バトルのAPIが上位2位であった

    (全体の3割強) 相対的にバトルソースが Webサーバに負荷を与えている
  27. 27 / 120 APIの特定 ・バトルの処理回数が多い ・バトルの処理時間が長い 以上のことがわかったので、 バトル処理全般を改修することにした

  28. 28 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • APIの特定

    • 処理の見直し • 具体的な改修箇所 • 実際に改修してみた例
  29. 29 / 120 処理の見直し • バトルの中の処理はどうなっているんだろう 実際に自分で処理を見直した

  30. 30 / 120 処理の見直し 特にバトル処理中のスキル計算処理の中で ・多くのスキルを判定している ・計算数が多い箇所がある

  31. 31 / 120 処理の見直し 特にバトル処理中のスキル計算処理の中で ・多くのスキルを判定している ・計算数が多い箇所がある 具体的に数値で回数を見たい プロファイラを使って処理を分析

  32. 32 / 120 処理の見直し 今回はXHProfを使いました XHProfとは、PHPのプロファイラ 処理時間や関数のcall数、メモリ使用など詳細に数値で把握できる

  33. 33 / 120 処理の見直し 下記のことがわかりました ・スキル計算処理がメモリを消費し、処理時間も長い ・CSV取得処理のcall数が多く処理時間が長い

  34. 34 / 120 スキル計算処理の負荷が高い スキルの評価部分が処理時間が長く、 メモリ消費が激しかった

  35. 35 / 120 CSV取得処理のcall数が多く処理時間が長い CSVを取得している処理の call回数が多い 処理に時間がかかっている

  36. 36 / 120 処理の見直し プロファイラからわかったこと ・スキル計算処理がメモリを消費し処理時間も長い ・CSV取得処理のcall数が多く処理時間が長い スキル計算処理とCSV取得処理がネック

  37. 37 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • APIの特定

    • 処理の見直し • 具体的な改修箇所 • 実際に改修してみた例
  38. 38 / 120 具体的な改修箇所 • スキル計算処理がメモリを消費し処理時間も長い スキル計算処理のメモリ使用量を減らし、処理時間を短くする • CSV取得処理のcall数が多く処理時間が長い CSV取得処理のcall数を減らし、処理時間を短くする

  39. 39 / 120 具体的な改修箇所 「スキル計算の高速化」 「CSVの取得処理の効率化」 上記を改修箇所とした。

  40. 40 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • 実際に改修してみた例

  41. 41 / 120 実際に改修してみた例 全項目より下記2点を改修箇所として定めた • スキル計算の高速化 • CSVの取得処理を効率化

  42. 42 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • 実際に改修してみた例

    • スキル計算の高速化 • CSVの取得処理を効率化
  43. 43 / 120 スキル計算の高速化 スキルの高速化として下記の3項目を説明します • グラブルのスキル • 現状の把握 •

    改善方法
  44. 44 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • グラブルのスキル

    • 現状の把握 • 改善方法 • CSVの取得処理を効率化
  45. 45 / 120 グラブルのスキル グラブルのスキルは種類が多く、 バトル内でも大量に判定されます バトルの編成の中でスキルに関連するもの ・武器 ・召喚石 ・キャラ

    etc...
  46. 46 / 120 武器 1編成に付き10本 1本あたり2種類のスキルを持つ

  47. 47 / 120 武器 この武器には以下のスキルが付いている 1. 風属性キャラの攻撃力上昇(中) 2. 風属性キャラのHPが少ないほど攻撃力が上昇(小)

  48. 48 / 120 召喚石 編成全体の能力に影響を与える (バトル中は自分とサポーターの2つ)

  49. 49 / 120 召喚石 この召喚石には以下のスキルが付いている スキル「嵐竜方陣」の効果を100%UP

  50. 50 / 120 キャラクター 1ターンに4人攻撃する 1人3つのスキルを持っている

  51. 51 / 120 キャラクター このキャラクターには以下のスキルが付いている 1.弱体耐性UP 2.バトル開始時に自動復活効果 3.被ダメージに稀にHP全回復

  52. 52 / 120 グラブルのスキル 多彩なスキルを組み合わせて、 高いダメージを得たり、 戦闘中に有利になる効果を得たりする

  53. 53 / 120 グラブルのスキル • 判定するスキルの数は 武器(10 × 2)+ 召喚石(2

    × 1) + キャラクター(4 × 3) = 34 バトル中に34回判定される。
  54. 54 / 120 グラブルのスキル ・武器スキル数は800以上 ・召喚石のスキルは250以上 ・キャラクター数は400キャラ以上 組み合わせの数が膨大になる ※10/8現在

  55. 55 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • グラブルのスキル

    • 現状の把握 • 改善方法 • CSVの取得処理を効率化
  56. 56 / 120 現状の把握 • グラブルのスキルは数が多く、組み合わせ数も膨大 実際に確認したところスキルの計算処理が膨大にあった。 プロファイラで詳細分析

  57. 57 / 120 現状の把握 ・スキル計算処理が処理時間も長い、メモリ使用量が高い

  58. 58 / 120 現状の把握 ・実際の処理例 foreach($character_party as $character) { foreach($weapon_skill_ids

    as $skill_id) { $skill_master = Master¥Skill::find($skill_id); Skill::calcurate_skill($character, $skill_master) } foreach($summon_skill_ids as $skill_id) { $skill_master = Master¥Skill::find($skill_id); Skill::calcurate_skill($character, $skill_master) } … } 様々なスキルをループ 回している たくさん計算している →処理が長い
  59. 59 / 120 現状の把握 スキル評価ロジックの メモリ使用量が高い

  60. 60 / 120 現状の把握 • スキルの組み合わせが多いので、膨大な計算処理がある 計算処理を減らせれば軽くなる

  61. 61 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • グラブルのスキル

    • 現状の把握 • 改善方法 • CSVの取得処理を効率化
  62. 62 / 120 改善方法 • スキルの組み合わせが多いので、膨大な計算処理がある 計算処理を減らせれば軽くなる 普段どのような負荷対策をしているか考えてみた

  63. 63 / 120 改善方法 PHP DB サーバーからDBに大量にアクセスがある

  64. 64 / 120 改善方法 KVS 一度DBから取得したら、KVSに乗せる PHP DB

  65. 65 / 120 改善方法 一度計算した結果をKVSにのせると早くなるはず! PHP KVS

  66. 66 / 120 改善方法 キャラクター スキルの 計算結果 計算結果 武器スキルの 計算結果

    召喚石スキルの 計算結果 計算結果をKVSに載せると 計算処理が省ける
  67. 67 / 120 改善方法 • 実際の処理例 foreach($character_party as $character) {

    foreach($weapon_skill_ids as $skill_id) { $skill_master = Master¥Skill::find($skill_id); Skill::calcurate_skill($character, $skill_master) } foreach($summon_skill_ids as $skill_id) { $skill_master = Master¥Skill::find($skill_id); Skill::calcurate_skill($character, $skill_master) } … } 処理する スキルが たくさんある スキルの ロジックに アクセス
  68. 68 / 120 【改修後処理】 $identifier = $raid_id . $user_id; $skill

    = Memcache::instance()->get(raid_id); If (empty($skill)) { foreach($character_party as $character) { $array[キャラクター番号] = a foreach($weapon_skill_ids as $skill_id) { $skill_master = Master¥Skill::find($skill_id); $skill[‘weapon_skill’][]=Skill::calcurate_skill($character,$skill_master); } foreach($summon_skill_ids as $skill_id) { $skill_master = Master¥Skill::find($skill_id); $skill[‘summon_skill’][] = Skill::instance()->calcurate_skill($character, $skill_master); } foreach($support_skill_ids as $skill_id) … Memcache ::instance()->set_key($ identifier )->set($skill); } 計算してあるスキル値を memcachedから取得 計算結果が入っている配列を memcachedに詰める 計算した値を 配列に詰めておく
  69. 69 / 120 実装上で気をつけたこと ・キーの持ち方 ・データが取れないの挙動

  70. 70 / 120 キーの持ち方 正しい一意のキーで取得しないと 別のデータがとれてしまい、障害原因となる。 「バトルのID」と「ユーザーのID」を結合して 一意のキーを保証した。

  71. 71 / 120 データが取れないの挙動 データが取れないときは、再度計算しmemcachedに載せる 有効期限が切れた場合や、memcachedに異常が発生した場合でも、 データが取れないので正しく計算される よって安全性が担保される

  72. 72 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • 実際に改修してみた例

    • スキル計算の高速化 • CSVの取得処理を効率化
  73. 73 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • CSVの取得処理を効率化

    • グラブルのマスタデータ • 改善ポイント • 改善方法
  74. 74 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • CSVの取得処理を効率化

    • グラブルのマスタデータ • マスタデータとは • 現状のマスタデータの扱い方 • グラブルのマスタデータの特徴 • 改善ポイント • 改善方法
  75. 75 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • CSVの取得処理を効率化

    • グラブルのマスタデータ • マスタデータとは • 現状のマスタデータの扱い方 • グラブルのマスタデータの特徴 • 改善ポイント • 改善方法
  76. 76 / 120 マスタデータとは キャラクターや武器などのパラメータ情報 マスタデータはCSVファイル

  77. 77 / 120 マスタデータとは • 例 【キャラの情報】 性別 身長 体重

    【パラメータ】 攻撃力 HP 防御力 【アビリティ】 スキルのデータ スキル文言 追加効果の情報 【図鑑】 キャラ紹介の文言
  78. 78 / 120 マスタデータとは パラメータの情報を CSVで保持 【キャラの情報】 性別 身長 体重

    【キャラの情報】 性別,身長,体重 男,178,50
  79. 79 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • CSVの取得処理を効率化

    • グラブルのマスタデータ • マスタデータとは • 現状のマスタデータの扱い方 • グラブルのマスタデータの特徴 • 改善ポイント • 改善方法
  80. 80 / 120 現状のマスタデータの扱い方 現状のマスタデータの保存方法はWebサーバ内のlocal redisを使用 サーバ local redis

  81. 81 / 120 現状のマスタデータの扱い方 実際に運用しながら試してみた順に紹介 ・KVSの場所について ・local memcachedに保存 ・local redisに保存

  82. 82 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • CSVの取得処理を効率化 • グラブルのマスタデータ

    • マスタデータとは • 現状のマスタデータの扱い方 ・KVSの場所について ・local memcachedに保存 ・local redisに保存 • グラブルのマスタデータの特徴
  83. 83 / 120 KVSの場所について ・マスタデータは全てのユーザに対して同じデータ globalサーバにKVSを立ててデータを置くと 全ユーザーがデータを取得するため、トラフィックが多い PHP PHP KVS

  84. 84 / 120 KVSの場所について • 各Webサーバ内にKVSを立てて、アクセスするようにするとWebサーバの数だ けアクセスが分散できる サーバ local KVS

    サーバ local KVS サーバ local KVS
  85. 85 / 120 システム構成図から見るglobal KVSとlocal KVS Web Server 1 twemproxy

    memcached 1 … Application Servers Cache Servers Database Servers Master MySQL MHA Cluster 1 Apache + mod_php Node Server 1 Layer4 Load Balancer(BIG-IP) Client N … Web(HTTP) static data access Memcached Servers Client (Apps or Web Browser) 1 Akamai CDN … Nginx 1 … Application Load Balancers WebSocket(WS) Nginx N Redis 1 … Redis Servers Redis N 10G Ethernet 10G Ethernet Node Server N Web Server N … WebSocket (WS) Web(HTTP) Access Load Balancer / Proxy Sharding (水平分割) … N Node.js twemproxy memcached N Slave 1 Slave 2 MySQL MHA Cluster 2 MySQL MHA Cluster 3 Master Slave 1 Slave 2 Master Slave 1 Slave 2 mysqli local memcached local redis Webサーバにある local redis local memcached globalにある redis memcahced
  86. 86 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • CSVの取得処理を効率化 • グラブルのマスタデータ

    • マスタデータとは • 現状のマスタデータの扱い方 ・KVSの場所について ・local memcachedに保存 ・local redisに保存 • グラブルのマスタデータの特徴
  87. 87 / 120 local memcachedに保存 PHPからCSVファイルにアクセスしたとき、 一番最初のアクセスで全行をlocal memcachedに保存 local memcached

    PHP CSVファイル
  88. 88 / 120 local memcachedの問題点 CSVファイルのレコード数が増えたため、 CSVファイルのデータをlocal memcachedに保存する際に Webサーバに負荷がかかるようになった。

  89. 89 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • CSVの取得処理を効率化 • グラブルのマスタデータ

    • マスタデータとは • 現状のマスタデータの扱い方 ・KVSの場所について ・local memcachedに保存 ・local redisに保存 • グラブルのマスタデータの特徴
  90. 90 / 120 local redisに保存 local redisだとlocal memcachedで起きていた問題点が起きないため Webサーバにlocal redisをたてて、マスタデータをlocal

    redisに移行した PHP local redis
  91. 91 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • CSVの取得処理を効率化

    • グラブルのマスタデータ • マスタデータとは • 現状のマスタデータの扱い方 • マスタデータの特徴 • 改善ポイント • 改善方法
  92. 92 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • CSVの取得処理を効率化

    • グラブルのマスタデータ • マスタデータとは • 現状のマスタデータの扱い方 • マスタデータの特徴 ・マスタデータの構成について ・更新頻度について ・問題点
  93. 93 / 120 マスタデータの構成について • キャラクターの例 【キャラの情報】 性別 身長 体重

    【パラメータ】 攻撃力 HP 防御力 【アビリティ】 スキルのデータ スキル文言 追加効果の情報 【図鑑】 キャラ紹介の文言
  94. 94 / 120 マスタデータの構成について 基本情報 バトル パラメータ アビリティ 図鑑 キャラクターのデータ

  95. 95 / 120 マスタデータの構成について ID スキルのID 演出データID 奥義ID etc 1

    2 21 33 Etc… ID 効果値 スキル演出ID ダメージ補正ID Etc.. 2 2 9 43 Etc… ダメージ補正 スキル 基本情報 ID 基礎値 ダメージの伸 び方ID 固定値 Etc… 43 23 22 2000 Etc…
  96. 96 / 120 マスタデータの構成について マスタデータは多くのCSVファイルから構成されている 多くのCSVファイルとリレーション関係がある

  97. 97 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • CSVの取得処理を効率化

    • グラブルのマスタデータ • マスタデータとは • 現状のマスタデータの扱い方 • マスタデータの特徴 ・マスタデータの構成について ・更新頻度について ・問題点
  98. 98 / 120 更新頻度について マスタデータがリリースされたら基本変わらない マスタデータが変わる例: 1.不具合修正 2.バランス調整

  99. 99 / 120 1700万人を満足させるためのシステム改修 • 実際に改修してみた例 • スキル計算の高速化 • CSVの取得処理を効率化

    • グラブルのマスタデータ • マスタデータとは • 現状のマスタデータの扱い方 • マスタデータの特徴 ・マスタデータの構成について ・更新頻度について ・問題点
  100. 100 / 120 問題点 マスタデータの構成上CSVファイルが大量に関連しているため、 検索コスト、データを保持するコストがかかる

  101. 101 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • 実際に改修してみた例

    • スキル計算の高速化 • CSVの取得処理を効率化 • グラブルのマスタデータ • 改善ポイント • 改善方法
  102. 102 / 120 マスタデータの構成について 基本情報 バトル パラメータ アビリティ 図鑑 キャラクターのデータ

  103. 103 / 120 改善ポイント • 通信ごとにキャラクターのデータを作り直している

  104. 104 / 120 改善ポイント • キャラクターのデータを作るときに多くのマスタデータを取得している • 通信ごとにキャラクターのデータを作り直している CSVをとる頻度が高い

  105. 105 / 120 改善ポイント CSVを取得する頻度を低くすれば効率が良い

  106. 106 / 120 1700万人を満足させるためのシステム改修 • 今回の改修の目標 • 改修するところを探す • 実際に改修してみた例

    • スキル計算の高速化 • CSVの取得処理を効率化 • グラブルのマスタデータ • 改善ポイント • 改善方法
  107. 107 / 120 改善方法 必要な個所に対して 中間データをキャッシュしたら効率が良い!

  108. 108 / 120 改善方法 基本情報 バトル パラメータ アビリティ 図鑑 キャラクターのデータ

    • 改修前のイメージ
  109. 109 / 120 改善方法 基本情報 バトル パラメータ アビリティ キャラクターのデータ •

    改修後のイメージ memcached 必要なデータに絞る 一度取得したら memcachedに保存
  110. 110 / 120 改善方法 【実装時に気を付けたこと】 ・中間データのキャッシュの置き場 共通のデータを参照するため local memcachedに格納 ・マスタデータが更新された時について

  111. 111 / 120 マスタデータが更新された時について マスタデータが更新されたときに、同時に中間データも更新されなければなら ない そのためリリースバッチでマスタデータが保存されているredisと 中間データが保存されているmemcachedを削除している Webサーバ local

    memcached 中間データ local redis マスターデータ サーバ内で flush_all
  112. 112 / 120 改善結果 中間データをキャッシュ化したことによって 全体のCSV取得回数が減った CSVファイルを検索する機会が減った マスタデータをサーバメモリに展開する必要がなくなったので メモリの節 約になった

  113. 113 / 120 まとめ • スキル計算の高速化 • CSV取得処理の効率化 上記の処理を行いました それによる効果をグラフ化します

  114. 114 / 120 まとめ バトルAPIの平均速度は42.5%減 0 50 100 150 200

    250 300 350 400 450 改修前 改修後 APIの速度 400ms 230ms
  115. 115 / 120 まとめ バトルAPIのメモリ使用量は40%減 0 20 40 60 80

    100 120 改修前 改修後 メモリ使用量 100MB 60MB
  116. 116 / 120 まとめ • CALL関数の数は50%減 0 50000 100000 150000

    200000 250000 300000 350000 400000 450000 改修前 改修後 CALL関数の数 400,000 関数 200,000 関数
  117. 117 / 120 改修を通して感じたこと • 常に改善の意識をもつ必要性 • プログラムを書く上での当たり前の大切さ データは最小限にまとめて再利用 同じデータを何度も取得しないで再利用

  118. 118 / 120 ▪まとめ キャッシュは神 使用用途は正しく守ってください

  119. 119 / 120

  120. © Cygames, Inc. 120 / 120