Upgrade to Pro — share decks privately, control downloads, hide ads and more …

挑戦!ISUCON de Server-side Swift 〜タイムゾーンには気をつけろ〜

freddi(Yuki Aki)
September 11, 2022
900

挑戦!ISUCON de Server-side Swift 〜タイムゾーンには気をつけろ〜

iOSDC Japan 2022
Special Thanks: http://www.ankokukoubou.com/font/onryou.htm

freddi(Yuki Aki)

September 11, 2022
Tweet

Transcript

  1. !"#$%&
    「ISUCON」は、LINE株式会社の商標または登録商標です。


    View Slide

  2. ISUCONとは、かの有名なイスカンダル(アレクサンドロス3世)が

    紀元前320年頃に開いた異国の椅子品評会にルーツが遡る。その際、

    かの王がこの品評会の名前を考得ていたところ、家臣の一人が「

    椅子だしイスコンテストでいいんじゃないんすか?w」と放ち、

    イスカンダル王が絶賛。以降その品評会はイスコンテストと呼ば


    れることになる。日本には安土桃山時代にシルクロードから伝わり、

    楽市楽座で活発に行われるようになる。かの織田信長が打たれた本


    能寺の変では、このイスコンテスト落選に腹を立てた明智光秀が

    腹いせに矢を寺にはなったところから始まるというただのとばっ

    ちりの説がある。その後、名前の呼び方が訛り、明治時代には

    ついにイスコンという名前に。第二次大戦などで中止になるものの、

    現在までに毎年コンテストが開かれて、現在に至る

    View Slide


  3. View Slide

  4. View Slide

  5. Webサービスの高速化を競う

    View Slide


  6. View Slide


  7. View Slide

  8. めっちゃ遅いWebサービスの高速化を競う

    View Slide

  9. 高速化の手段

    View Slide

  10. 高速化の手段
    DBのチューニング
    アルゴリズムの改善
    サーバーの構造の工夫
    → 専用のベンチマークツールでスコアを出し、そのスコアを競う
    etc

    View Slide

  11. 賞金

    View Slide

  12. Webサービスの高速化を競う
    ※ 既に各言語で実装されたサービス(参考実装)を高速化

    View Slide

  13. 参考実装としての公式の言語

    View Slide

  14. 参考実装としての公式の言語
    Ruby
    Rust
    Python
    Go
    Node.js
    PHP
    etc (isucon 12)

    View Slide

  15. '()*+,"-./-.0123-,"4256
    ※ ごめんなさい ISUCON運営の皆さん 悪気はないんです

    View Slide

  16. —— 2021年 7月

    View Slide

  17. ここに3人の勇者現る

    View Slide














































































  18. View Slide

  19. ことの発端

    View Slide

  20. やったこと(やりたかったこと)
    Swiftへのフルスクラッチ移植 + 高速化

    View Slide

  21. Server-side Swift ライブラリ

    View Slide

  22. Swift Concurrency
    2021年、当時 Vapor に電流走る

    View Slide

  23. コールバック地獄が
    routes.get("firstUser") { req -> EventLoopFuture in


    User.query(on: req.db).first().unwrap(or: Abort(.notFound)).flatMap { user in


    user.lastAccessed = Date()


    return user.update(on: req.db).map {


    return user.name


    }


    }


    }

    View Slide

  24. こんな感じに!
    routes.get("firstUser") { req async throws -> String in


    guard let user = try await User.query(on: req.db).first() else {


    throw Abort(.notFound)


    }


    user.lastAccessed = Date()


    try await user.update(on: req.db)


    return user.name


    }


    View Slide

  25. Async/awaitで Vapor による開発が

    もっと快適に!

    View Slide

  26. Q. あれ?でも Swift Concurrency ってその時安定してたの?

    View Slide

  27. Q. あれ?でも Swift Concurrency ってその時安定してたの?
    A. いろいろと使うのには手間があった
    Swift Toolchain のインストール
    サーバーがLinuxなのでインストール方法の工夫
    デバッグが効かない(LLDB)

    View Slide

  28. Q. あれ?でも Swift Concurrency ってその時安定してたの?
    A. いろいろと使うのには手間があった
    Swift Toolchain のインストール
    サーバーがLinuxなのでインストール方法の工夫
    デバッグが効かない(LLDB)
    < だいたいやっといたでw

    View Slide

  29. SPMで環境構築できるので楽

    View Slide

  30. // swift-tools-version:5.3


    import PackageDescription


    let package = Package(


    name: "isucon11",


    platforms: [


    .macOS(.v10_15)


    ],


    dependencies: [


    .package(url: "https://github.com/vapor/vapor.git", .branch("async-await")),


    .package(url: "https://github.com/vapor/mysql-kit.git", from: "4.0.0"),


    .package(url: "https://github.com/Flight-School/AnyCodable", from: "0.6.0"),


    .package(url: "https://github.com/vapor/jwt-kit.git", from: "4.0.0"),


    ],


    targets: [


    .target(


    name: "App",


    dependencies: [


    .product(name: "Vapor", package: "vapor"),


    .product(name: "MySQLKit", package: "mysql-kit"),


    .product(name: "JWTKit", package: "jwt-kit"),


    .product(name: "AnyCodable", package: "AnyCodable")


    ],


    swiftSettings: [


    .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)),


    // Disable availability checking to use concurrency API on macOS for development purpose


    // SwiftNIO exposes concurrency API with availability for deployment environment,


    // but in our use case, the deployment target is Linux, and we only use macOS while development,


    // so it's always safe to disable the checking in this situation.


    .unsafeFlags(["-Xfrontend", "-disable-availability-checking"])


    ]


    ),


    .target(name: "Run", dependencies: [.target(name: "App")])


    ]


    )


    Package.swift (+ k8s などの設定ファイル) 作れば開発可能!

    View Slide

  31. 企業の皆さん Server Side Swift どうすか?!

    View Slide

  32. Am○zonは独自のServer-side Swift Frameworkつかっている

    (らしい)

    View Slide

  33. 企業の皆さん Server Side Swift どうすか?!


    (圧)

    View Slide

  34. 一応本番の前に壁打ち2年分くらいやった

    View Slide

  35. 一応本番の前に壁打ち2年分くらいやった
    他の言語の初期スコアまでは行った
    結果
    だいたい時間はオーバーしている(13時間、本番は8時間)

    View Slide

  36. そして本番

    View Slide

  37. 出題

    View Slide

  38. 出題

    View Slide

  39. ISUCON Swift 移植RTAはーじまーるよー

    View Slide

  40. やること

    View Slide

  41. やること
    10個くらいあるAPIのエンドポイントの実装の移植
    そっからチューニング(できれば)

    View Slide

  42. 最初4時間くらい

    View Slide

  43. 最初4時間くらい
    実装移植がだいたい終わる!いけるか?!

    View Slide

  44. ベンチマークが通らない!!!!!

    View Slide

  45. ハマりポイント その1

    View Slide

  46. 謎のPayload 上限
    public func collect(max: Int? = 1 << 14) -> EventLoopFuture {


    switch self.request.bodyStorage {


    case .stream(let stream):


    return stream.consume(max: max, on: self.request.eventLoop).map { buffer in


    self.request.bodyStorage = .collected(buffer)


    return buffer


    }


    case .collected(let buffer):


    return self.request.eventLoop.makeSucceededFuture(buffer)


    case .none:


    return self.request.eventLoop.makeSucceededFuture(nil)


    }


    }


    max: Int? = 1 << 14
    https://github.com/vapor/vapor/blob/45bc9ab1e3c4ba309606762664fc18358b7c92e5/Sources/Vapor/Request/Request%2BBody.swift#L38

    View Slide

  47. 謎のPayload 上限
    max: Int? = 1 << 14

    View Slide

  48. 謎のPayload 上限
    max: Int? = 1 << 14
    1 << 14 = 0b100000000000000

    View Slide

  49. 謎のPayload 上限
    max: Int? = 1 << 14
    1 << 14 = 0b100000000000000
    0b100000000000000 = 16(KB)

    View Slide

  50. 謎のPayload 上限
    デフォルトは 16KB しかリクエストを送れない
    気づかず時間をかなり消費

    View Slide

































































  51. 💧
    💧

    View Slide

  52. ハマりポイント その2

    View Slide

  53. @discardableResult


    @_disfavoredOverload


    public func values(_ values: Encodable...) -> Self {


    let row: [SQLExpression] = values.map(SQLBind.init)


    self.insert.values.append(row)


    return self


    }


    @discardableResult


    public func values(_ values: SQLExpression...) -> Self {


    self.insert.values.append(values)


    return self


    }
    SQLクエリを組み立てる関数
    https://github.com/vapor/sql-kit/blob/3c5413a229bc2abc962dab17ea66d25e448ad344/Sources/SQLKit/Builders/SQLInsertBuilder.swift#L94
    謎のオーバーロード

    View Slide

  54. @discardableResult


    @_disfavoredOverload


    public func values(_ values: Encodable...) -> Self {


    let row: [SQLExpression] = values.map(SQLBind.init)


    self.insert.values.append(row)


    return self


    }


    @discardableResult


    public func values(_ values: SQLExpression...) -> Self {


    self.insert.values.append(values)


    return self


    }
    SQLクエリを組み立てる関数
    https://github.com/vapor/sql-kit/blob/3c5413a229bc2abc962dab17ea66d25e448ad344/Sources/SQLKit/Builders/SQLInsertBuilder.swift#L94
    こっち呼んでるつもり
    謎のオーバーロード

    View Slide

  55. SQLクエリを組み立てる関数
    https://github.com/vapor/sql-kit/blob/3c5413a229bc2abc962dab17ea66d25e448ad344/Sources/SQLKit/Builders/SQLInsertBuilder.swift#L94
    こっち呼ばれる
    ちゃんとクエリ

    つくってくれない
    @discardableResult


    @_disfavoredOverload


    public func values(_ values: Encodable...) -> Self {


    let row: [SQLExpression] = values.map(SQLBind.init)


    self.insert.values.append(row)


    return self


    }


    @discardableResult


    public func values(_ values: SQLExpression...) -> Self {


    self.insert.values.append(values)


    return self


    }
    こっち呼んでるつもり
    謎のオーバーロード

    View Slide

  56. Why?

    View Slide

  57. SQLクエリを組み立てる関数
    https://github.com/vapor/sql-kit/blob/3c5413a229bc2abc962dab17ea66d25e448ad344/Sources/SQLKit/Builders/SQLInsertBuilder.swift#L94
    こっち呼ばれる
    ちゃんとクエリ

    つくってくれない
    @discardableResult


    @_disfavoredOverload


    public func values(_ values: Encodable...) -> Self {


    let row: [SQLExpression] = values.map(SQLBind.init)


    self.insert.values.append(row)


    return self


    }


    @discardableResult


    public func values(_ values: SQLExpression...) -> Self {


    self.insert.values.append(values)


    return self


    }
    こっち呼んでるつもり
    @_disfavoredOverload
    謎のオーバーロード

    View Slide

  58. @_disfavoredOverload
    謎のオーバーロード

    View Slide

  59. 謎のオーバーロード
    @_disfavoredOverload
    これでオーバーロードの呼び出しの優先順位を低くする
    → 結果想定してない方が呼ばれてクエリが正しくない

    View Slide

  60. 🥺

    View Slide

  61. 主なハマりポイント
    バックエンド(MySQLNIO)はタイムゾーンの設定必須
    今回は MySQLKit でDBとの接続を行った
    ベンチマークが期待しているデータの時間が9時間狂ってた
    など

    View Slide

  62. 結果

    View Slide

  63. 0点

    View Slide

  64. 😢

    View Slide

  65. 0点なので統計に乗らず
    ※ 少ないだけの可能性もある

    View Slide

  66. 😢

    View Slide

  67. 反省
    タイムゾーンには気をつけろ
    あと段階的に実装しても良かったかも?
    いやでもちゃんとたのしめたのでよかった

    View Slide

  68. まとめ

    View Slide

  69. たのしかったです!

    View Slide

  70. 皆さんも是非!

    View Slide