Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

jjugccc2018 app review postmortem

8,681 views

Published on

収益を支える中規模アプリケーション開発奮闘記

Published in: Engineering

jjugccc2018 app review postmortem

  1. 1. 収益を支える 中規模アプリケーション開発奮闘記 2018/05/26 JJUG CCC Spring @tamtam180
  2. 2. テーマ 2 ピーク時性能 数万リクエスト/秒 データストアとのやりとり 25万OP/秒 Java8 Webアプリ レビュー内容 対策コード 障害事例
  3. 3. アジェンダ • 自己紹介 • アーキテクチャの説明 • レビューについて – どのレベルで? – どういう観点で? – 指摘が多いもの/具体的な内容 • 性能問題 – JVMの話 – JVMの外の話 • 障害例 • ボトルネックの見つけ方 • 負荷試験 • おまけ 3
  4. 4. 自己紹介 • Name: Kiyotaka Suzuki • Twitter: @tamtam180 • Main works – SquareEnix (5.5Y) • PlayOnline, FF-XIV, etc. – SmartNews (4Y〜) • Cross-functional Expert Team – Software Engineer – 広告, 公共 4
  5. 5. アーキテクチャの説明
  6. 6. 対象アプリケーション • SmartNews(ニュースアプリ)の運用型広告 配信サーバ 6 こういうの
  7. 7. 対象アプリケーション • SmartNews(ニュースアプリ)の運用型広告 配信サーバ 7 こういうの チャンネル数 * 広告数 * 数万クリエイティブ * etc の組み合わせ最適化
  8. 8. アーキテクチャ 8
  9. 9. アーキテクチャ 9
  10. 10. アーキテクチャ 10 Spring Framework Embeded Jetty RxJava http://bit.ly/sn-tech-vol5-adserver
  11. 11. アーキテクチャ 11 Transform Filtering Auction Allocation Response Mem Bound CPU Bound Bloom Filter Online Prediction Creative Optimizer Counter Provider Bid Price Calculation In Memory Cache 特徴ベクトル Master Data Counter History
  12. 12. アクセス特性 12
  13. 13. 設計方針 • リクエスト中は基本的に通信はしないようにする • 一部の処理は非同期で投機的実行 • スケールしないものと直接やりとりしない • データストア – ユーザー情報単位の情報はDDB • 特徴ベクトルなど – ユーザー共通の情報はRedis -> JVMキャッシュ – キャンペーンなどのマスタ情報は DB -> JVMキャッシュ • Pubsub経由で更新 13
  14. 14. レビューについて
  15. 15. レビューのレベル • 基本的にガチレビュー • プログラムが正しいかだけではなく、プロダクトとしてどうあるべき かのレベルで見ます • 関連ドキュメントは基本的に読みます – 背景/PRD/メモ – 論文を元にしているなら論文を読みます • 時間はめちゃくちゃかかる • 普段から雑に共有しておくのが大事 • 人によっては教育も兼ねる • Productionでバグ起因による障害は片手で数えら れる程度 15
  16. 16. レビューの観点 • 計算量/メモリ空間 – O(1), O(LogN), O(N), … – メモリはワーキングメモリ/定常使用 両方 • Objectの生成数 • オンライン処理かバックグラウンド処理か • スレッドセーフか • ブロッキング処理になっていないか • 副作用(他の機能を壊していないか) • 撤退可能かどうか • お金でスケールできる設計になっているか • 意思のあるコードか – (意味を理解していないコピペかどうか) 16
  17. 17. 指摘が多い内容
  18. 18. 指摘が多い内容 • プログラムの話 • 仕事の仕方 18
  19. 19. 指摘が多い内容 • プログラムの話 • 仕事の仕方 19 対象アプリケーションの背景によって適切なコードは異なります。 パフォーマンスに困っていないのであれば、 過度な最適化は避け、 わかりやすいプログラムを書いた⽅が良いと思います。
  20. 20. 指摘が多い内容/プログラム編 • Boxing/Unboxingを意識していない • (例) CollectionライブラリにFastutilを使ってい る場合 20
  21. 21. 指摘が多い内容/プログラム編 • PrimitiveなCollectionを使わない 21 Map<Long, String> map; Long2ObjectMap<String> map;
  22. 22. 指摘が多い内容/プログラム編 • PrimitiveCollectionなのに標準実装を使っている 22 Long2ObjectMap<Campaign> map = new Long2ObjectOpenHashMap<>() long val = map.getOrDefault(10, -1); // return Long Long2ObjectMap<Campaign> map = new Long2ObjectOpenHashMap<>() map.defaultReturnValue(-1); long val = map.get(10L);
  23. 23. 指摘が多い内容/プログラム編 • Iterator 23 Long2LongMap map1; for (Map.Entry<Long, Long> entry : map1.entrySet()) { Long k = entry.getKey(); Long v = entry.getValue(); } ObjectIterator<Long2LongMap.Entry> itr = map1.long2LongEntrySet().fastIterator(); while (itr.hasNext()) { Long2LongMap.Entry e = itr.next(); long k = e.getLongKey(); long v = e.getLongValue(); }
  24. 24. 指摘が多い内容/プログラム編 • ラッパークラスを instance で比較する 24 Boolean val = returnBooleanMethod(); Optional<Boolean> optB = hogefuga(); if (optB.orElse(null) == val) { } Boolean b1 = new Boolean(true); Boolean b_box_1 = true; Boolean b2 = Boolean.TRUE; Boolean b3 = Boolean.FALSE; System.out.println(b1 == b2); // false System.out.println(b1 == b3); // false System.out.println(b_box_1 == b2); // true
  25. 25. 指摘が多い内容/プログラム編 • 古いJavaの知識のまま止まっている 25 Charset UTF8 = Charset.forName("UTF-8"); new InputStreamReader(System.in, UTF8); new InputStreamReader(System.in, StandardCharsets.UTF_8); try-with-resource, exception-multi-cache, stream, lambda, etc, etc.. I/F default実装, final化 など
  26. 26. 指摘が多い内容/プログラム編 • OptionalクラスでNULLを返す😱 26 public Optional<Campaign> returnOptionalValue() { if (campaign == null) return null; return Optional.of(campaign); } public Optional<Campaign> returnOptionalValue() { return Optional.ofNullable(campaign); }
  27. 27. 指摘が多い内容/プログラム編 • なんでもかんでも@Lombok.Dataをつける 27 @Lombok.Data public MyRequest { private long campaignId; private boolean innerFlag; // 内部フラグ private Campaign campaign; } MyRequest request = JsonUtils.toObject(requestData, MyRequest.class); このクラスでパラメータを受け取ると 内部フラグであるinnnerFlagにはsetInnnerFlagが存在するのでパラメータの Mappingが行われてしまう。 外部のRequestから内部のパラメータを操作する事が出来てしまう。
  28. 28. 指摘が多い内容/プログラム編 • Loggerの遅延評価機能を使わない 28 Logger.info("my error: " + hugeMap); Logger.info("my error: {}", hugeMap);
  29. 29. 指摘が多い内容/プログラム編 • Stacktraceを握りつぶすマン 29 try { } catch (Exception e) { log.error("error: {}", e.getMessage()); } try { } catch (Exception e) { log.error("error", e); } 末尾に例外Objectを指定すればstacktrace出してくれる
  30. 30. 指摘が多い内容/プログラム編 • 初期サイズを指定しない 30 new StringBuilder(); new ArrayList<>(); new HasMap<>(); 事前にある程度分かっているところは内部構造の拡張処理が走らないように指定す る。 expand処理, grow, copyOf等非常に重い処理が何度も走る。 ピーク時にはこれがボディーブローのように地味に処理を占める。
  31. 31. 指摘が多い内容/プログラム編 • Enumのvalues() 31 enum Color { RED(1), YELLOW(2), BLUE(3), BLACK(4), WHITE(5); private final int value; private Color(int value) { this.value = value; } private Color lookup(int value) { for (Color c : values()) { if (c.value == value) return c; } return null; } } 逆引きMapをstatic{}で作ろう values()は毎回配列が生成される (配列はImmutableではないため)
  32. 32. 指摘が多い内容/プログラム編 • スレッド/ デッドロック – ワーカースレッドから同じワーカースレッドを使う 32 ExecutorService svc; svc.submit(() -> { // 何かの処理 svc.submit(() -> { // 何かの処理 }); }); ExecutorServiceのRejectポリシー実装によりますが、デッドロックします
  33. 33. 指摘が多い内容/プログラム編 • スレッド/ サイレント – スレッドの中で例外発生に気が付かない 33 ExecutorService svc; svc.execute(() -> { something(); raiseException(); // ここで例外発生 veryImportantProcess(); // 実行されない }); 例外が何処にも出ないので気が付かない
  34. 34. 指摘が多い内容/プログラム編 • スレッド/ スケジューラーが止まる 34 ScheduledExecutorService svc; svc.scheduleWithFixedDelay(() -> { raiseError(); // 例外が出る }, 0, 5, TimeUnit.SECOND); 例外が発生するとスケジューラーは停止します
  35. 35. 指摘が多い内容/プログラム編 • スレッド/ スレッドローカル問題 – JettyのHttpServletRequestを別スレッドから参 照する 35 protected void doPost(HttpServletRequest req, HttpServletResponse resp) { asyncWorkers.execute(() -> { log.info("real-ip={}", req.getHeader("X-FORWARDED-FOR")); }); } JettyはRequestにThreadLocalを使用しているので、 別のスレッドでは値を参照できない
  36. 36. 指摘が多い内容/プログラム編 • スレッド/ forkJoinを使って並列処理 36 ForkJoinPool forkJoinPool = new ForkJoinPool(2); forkJoinPool.execute(() -> { Arrays.stream(new int[]{ 1,2,3,4,5,6,7,8,9,10 }) .parallel() .forEach(ii -> { // process }); }); 1つのTaskが重いと他のTaskを道連れにする https://blog.orz.at/2017/10/18/forkjoin/
  37. 37. 指摘が多い内容/プログラム編 • スレッド/ 無限キューでOOM 37 ExecutorService svc1 = Executors.newFixedThreadPool(10); svc1.execute(() -> { // 重い処理 }); ピーク時に処理が追いつかなくてキューが溜まる メモリ使用量が増える OutOfMemoryError new ThreadPoolExecutor(…, new LinkedBlockingQueue<Runnable>()); // 無限キュー RejectedExecutionHandlerを実装する(CallerRunsPolicyとか) Reject時のRetry処理を入れるとか
  38. 38. 指摘が多い内容/プログラム編 • スレッド/ 何でもかんでもParallelStream 38 このアプリでは他にもスレッドが沢山動いているので ContextSwitch多発で処理が遅くなる
  39. 39. 指摘が多い内容/プログラム編 • N+1問題 39 ループの中で何度も単発処理を走らせてしまう ループの中でRPCコール ループの中でDBアクセス 事前にBulk処理をしましょう
  40. 40. 指摘が多い内容/プログラム編 • Lambda + レキシカルスコープ参照 40 Lambda Compilerの問題でもある Lambdaの中でレキシカルスコープ参照で動的内部クラス生成 (Singleton実装にならない) JITによりバイトコード変換, Metaspaceへ 使われなく案ってClass unloadedが定常的に発生する Object obj = new Object(); lambdaCaller.execute(() -> { obj.toString(); });
  41. 41. 指摘が多い内容/プログラム編 • 勝手に依存を追加する – Jackson, AwsSDK, Logbackの挙動が変わる 41 mvn dependency:tree –Doutput=dependency.txt 勝手にライブラリのバージョンを上げたり、 ライブラリを追加して他の依存バージョンを更新してしまう 依存関係をきちんとgitで管理する 気が付かないまま次の副作用が Jackson: JSONのデフォルトの変換処理が変わる AwsSDK: 初期化処理やCredentialの更新周りの変更 Logger: 設定ファイルの変更(<Encode>属性の場所)とか
  42. 42. 指摘が多い内容/プログラム編 • Spring FrameworkのProxy化 – Annotationが消える 42 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation {} @Component @MyAnnotation public void MyClass { @Transactional public void dbAccess() { … } } @Autowired private MyClass myClass; myClass.getClass().getAnnotation(MyAnnotation.class); // NULL
  43. 43. 指摘が多い内容/プログラム編 • 画像処理 – ImageIO / Nativeメモリリーク • 次のクラスはdisposeを呼ばないとnativeメモリが リークする – ImageReader – ImageWriter – Graphics2D 43
  44. 44. せっかくなので画像処理編 • 画像処理 – アルファチャンネル付き画像(ARGB)をJPEGで保存 – YCrCb(明るさ, 色相) • ではなくCMYK形式のJPEGで出力される – 赤みを帯びた画像になる – ImageIOの不具合(だと思ってる) 44
  45. 45. せっかくなので画像処理編 • 画像処理 – お行儀の悪い画像を読み込むと例外が出る – APP0-JFIFが欠落しているケース • byte[]を弄ってからImageIOを使う • APP0-JFIFの情報を無理矢理入れる 45 http://hp.vector.co.jp/authors/VA032610/ byte[0..3] == { 0xff, 0xd8, 0xff, !{0xe0 && 0xe1} } // 以下を差し込む { 0xff, 0xe0, 0x00, 0x10, 'J', 'F', 'I', 'F', 0x00, 0x01, 0x01, 0x01, 0, 72, 0, 72, 0, 0};
  46. 46. 指摘が多い内容/プログラム編 • InstanceOf / DownCast – 正しく設計していれば殆どのケースで使う事は無い – これが出てくる時点で設計ミスを疑ったほうが良い • 特にDownCast • InstanceOf <Interface>のミスマッチは特に処理 コストが高い 46
  47. 47. 指摘が多い内容/プログラム編 • String#replaceAll / String#replace – 正規表現が不要な置換処理でもreplaceAllを使っている – 単純な置換処理であればreplaceで良い 47 String s = "abracatabra"; s.replaceAll("ra", "&&"); String s = "abracatabra"; s.replace("ra", "&&");
  48. 48. 性能問題
  49. 49. 性能問題 • Dockerで動かす場合のCPUの数の認識 • HostのCPUを見てしまう • 影響 – StreamのparallelStream – GCのスレッド数 – JITコンパイラのスレッド数 – プロセッサ数から自動で制御するもの 49 最近のJDKではサポートされている JDK8にもバックポートされている (--cpuset-cpusと--cpusで対応範囲が異なるので注意) メモリも同様
  50. 50. 性能問題 • 最初の1回目が遅い – ワーカースレッド – Cipher – リフレクションキャッシュ 50 // 一気にスレッドを生成する ThreadPoolExecutor#prestartAllCoreThreads(); 起動時にWarmup処理を入れる
  51. 51. 性能問題 • JavaでSSL処理の中に出てくるGHASH • 一部のバージョンではGHASHがめちゃくちゃ遅い • AWS-SDKは基本的にHTTPS通信 – CloudSearchで巨大なResponse – DynamoDBの処理でボトルネック • HTTP通信をする 51 ClientConfiguration awsConfig = new ClientConfiguration(); awsConfig.setProtocol(Protocol.HTTP);
  52. 52. 性能問題 • 同期処理問題(synchronized等) – Propertiesクラス – SecureRandom • 意外とあちこちで使われている – ChiperのInstance化まわり – GenericObjectPool – getClass().getName – Collections.shuffle – AtomicLong – etc,etc… 52
  53. 53. 性能問題 • CPU L2キャッシュ – 要素数が少ない場合はシーケンシャルアクセスの方が速 い事が多い – Sortも同様 – Mapも同様 • Get: MapはO(1)で ArrayはO(N)だけど、要素数が少な いとArrayのが速い • アルゴリズム上の計算量だけではなく 目に見えない定数項を考慮 53
  54. 54. 性能問題 • Objectを作りすぎない 54 class Points { int[] xx; int[] yy; } これで同じ事が出来る class Point { int x; int y } ImmutableList<Point> points; // 10万要素あると10万Object生成
  55. 55. JVMの話
  56. 56. JVMの話 • Gzip処理 – Native実装ではあるが、nginxのレイヤーに任せた方が 良い – Gzip -> crc -> 恐怖のGCLocker 56
  57. 57. JVMの話 • IPv6問題 – IPv6でログ等が記録されちゃうのを回避する 57 -Djava.net.preferIPv4Stack=true
  58. 58. JVMの話 • 例外のスタックトレースが全く出力されない – 同じ様な箇所で組み込み例外が繰り返し発生するような 状態になるとJITによってスタックトレースのサイズが 0の例外が投げられるようになる。 58 -XX:-OmitStackTraceInFastThrow
  59. 59. • Dockerで動かす場合のCPUの数の認識(再掲) • HostのCPUを見てしまう • 影響 – StreamのparallelStream – GCのスレッド数 – JITコンパイラのスレッド数 – プロセッサ数から自動で制御するもの JVMの話 59
  60. 60. JVMの話 • 起動時にFULL GCが走る – Metaspaceの初期サイズが小さい – Jarが大きいとその分領域が必要 – GCの目安を指定する 60 -XX:MetaspaceSize=128m
  61. 61. JVMの話 • メモリ配分 – コピペで持ってこないでアプリの特性に合わせて設定 – キャッシュライブラリの設定 • 安定稼働に必要なメモリを確保していない事とメモリ リークは異なる • キャッシュライブラリを使う場合はきちんと見積もる 61
  62. 62. JVMの外の話
  63. 63. JVMの外の話 • Kernelの設定 • インターネットに公開されているやつは古いのが多い – Kernel2.4/2.6の産物 – (オープンソーシャルが流行った時代) • 最近のKernelは自動調整項目が多い – 設定する事で逆に悪くなるものもある 63
  64. 64. JVMの外の話 • ClockSource – EC2(KVMベースのc5,m5は除く)はxen – ClockSourceもxen – getTimeOfDayはvDSOでユーザーランドで動く • xenだとカーネルランドで動く • System.currentTimeMillis / System.nanoTime – gettimeofday, clock_gettime 64 cat /sys/devices/system/clocksource/clocksource0/current_clocksource xen echo tsc >/sys/devices/system/clocksource/clocksource0/current_clocksource 危険
  65. 65. JVMの外の話 • Jumbo frame – 経路によっては通信できない 65
  66. 66. JVMの外の話 • TCP Offload – OFFにしないとxenのネットワークドライバの不具合を 踏む事がある – (最近のでは直っていると思う。たぶん。) • RabbitMQでMirror設定を入れるとこれを踏んで Panicを起こす事があった 66 for path in /proc/sys/net/ipv4/conf/* do nic=$(basename "$path") if [[ $nic == eth* ]]; then /sbin/ifconfig $nic txqueuelen 10000 /sbin/ethtool -K $nic ¥ rx off tx off sg off tso off ufo off gso off gro off lro off fi done
  67. 67. 障害例
  68. 68. 障害例 • もぐら叩き問題 • Port番号Conflict • IOPS不足 • AWSの問題 – リージョン間通信の経路がおかしくなる – ElasticacheのARPテーブル問題(FIXED) – DDBが定期的に短時間の間不安定になる • GC多すぎ問題 • ブロッキング処理多発 • JVMクラッシュ 68
  69. 69. 障害例 • JVMクラッシュ • https://bugs.openjdk.java.net/browse/JDK-8189789 • https://bugs.openjdk.java.net/browse/JDK-8059299 • https://bugs.openjdk.java.net/browse/JDK-8054883 • https://bugs.openjdk.java.net/browse/JDK-8147585 • https://bugs.openjdk.java.net/browse/JDK-8081283 • https://bugs.openjdk.java.net/browse/JDK-8075805 69
  70. 70. 障害例 • JVMクラッシュ • デプロイをしてN分後くらいにセグフォルトで死ぬ – 再現率100% • JITの不具合 • 階層型コンパイラ – 2000回実行でインタプリタからC1へ – 15000回実行でC1からC2へ • C2コンパイラの不具合でセグフォルト 70
  71. 71. どうやってボトルネックを見つけるか
  72. 72. ボトルネック発見 • JMXで色々な情報をDatadogに送っている – 400項目以上 • System Metrics: Datadog • Application Performance: NewRelic • Frame Graph • ピークタイムにjstack連打 • GCログ • Profiler: yourkit 72
  73. 73. ボトルネック発見 • (例) ワーカースレッド(ThreadPoolExecutor) • 通常の観測では意味が無い – 値を取得するタイミングに依存する • WindowTime(1分)の瞬間最大風速を記録する • Submit/Executeした後に、 Queue/Activeスレッド数の最大値を記憶する 73
  74. 74. ボトルネック発見 • 発見したら直す 74
  75. 75. 負荷テスト
  76. 76. 負荷テスト • 負荷試験とエージングテストは違う • 単純にRequestを大量に流して性能を測るのベンチ マーク • 一瞬の負荷が高いサービスで平均値を取ることは意味 が薄い – 1秒単位のスパイクを見る必要がある 76
  77. 77. 負荷テスト • だんだんと負荷を上げ、ボトルネックになるリソース を見つける。その変化点を探るのが負荷テスト • 大事な事 – 安定して稼働する閾値を見つけ、スケールの臨界点を見 つける – 露呈したボトルネックについてどのような対策を講じる か 77
  78. 78. おまけ (レビュー/仕事の仕方編)
  79. 79. おまけ: 仕事の仕方編 • レビューの指摘の返答が、コピペしたので分かりません • 指摘に対して修正をせず、問題が発生する • 何度も同じ内容を指摘 – 他のメンバーも同じ間違いが伝播する • 無駄に複雑にしている • 変なレイヤーに勝手にキャッシュを作り不整合 • DIFFを小さくする事と影響範囲を小さくする事を混同して いる • Etc.. 79 😨
  80. 80. おまけ: 仕事の仕方編 • テストを書かない • 検証をしないでいきなりProductionへ投入する • 大きな新機能をデプロイ後、誰も反応できる関係者不在 • 既存の文化に合わせないで修正コードを書きまくる • Etc.. 80 😨
  81. 81. おまけ: 仕事の仕方編 • パラメータの変更 – 検証/レビュー無しで適用してしまう • パラメータの変更 > プログラムの更新 – レビュー対象にするべき – 動作が変わるのだから 81
  82. 82. おまけ: 仕事の仕方編 • そもそも全く違う • 実装のレビューの前にアーキテクチャ/設計/方針の相談 • 事前に相談する • いきなりHugeパッチを書かない 82
  83. 83. おしまい

×