ISUCON11予選でSwift移植チャレンジした
TR;DR
- ISUCON11予選にSwiftで参加した
- SwiftのConcurrency機能(async/await等 )の体験最高
- 素振りが足りず地区予選敗退
チーム情報
移植の覚悟
ISUCONでは例年、いくつかの言語で対象Webアプリケーションの参考実装が提供されます。しかし、それを使わないといけない、というわけではなくレギュレーション上は他言語の使用が許可されています。
許可される事項には、例として以下のような作業が含まれる。
・ ...
・他の言語による再実装
ISUCON11 予選レギュレーション : ISUCON公式Blog
Swiftの実装は残念ながら提供されていませんが、せっかく参加するのであればチームの得意分野を活かしたい、ということで競技時間内にSwiftで再実装する事になりました。
採用技術
ツールチェイン
通常、iOSやmacOS向けのSwiftアプリを開発する場合、デプロイ先のOSバージョンに同梱されているSwiftランタイムバージョンによって使えるSwiftの言語バージョンが制限されてしまいます。
が、今回はLinuxにデプロイするSwiftアプリを開発するため、特に制約はありません。最新の言語機能も使い放題です。
というわけで、Swiftツールチェインとして正式リリース前のSwift 5.5を採用しました。Swift 5.5には並行プログラミングの言語機能が追加されており、Non-blocking IOを使ったアプリを書くのにもってこいです。
今回はswift-5.5-DEVELOPMENT-SNAPSHOT-2021-07-30-aを使いました。
Webフレームワーク
現在アクティブにメンテナンスされているSwift製Webフレームワークはかなり少なくなっており、ほぼVapor一択な状況でした。 かなり重厚な作りになっていますが、必要な機能は一通り揃っています。
Vaporが依存しているNon-blocking IOを実現するライブラリSwiftNIOは、言語側のConcurrency機能との橋渡しをサポートしており、SwiftNIO内のFuture型を簡単にasyncメソッド呼び出しとして取り出すことができます。
また、VaporはSwiftNIOのConcurrencyサポートを利用して、Vapor自体のConcurrency APIサポートを進めており、 メインメンテナの@0xTimさんのPR上で開発が進行している状況です。今回はこのPRブランチを使用しました。
SQLライブラリ
Vaporは複数のSQLバックエンドをサポートするための抽象レイヤとしてFluent というORMを提供しています。しかし過去の経験から、抽象化されたDSLで思い通りのSQLを発行するのが非常に難しいことが分かっていたため、今回は採用を見送りました。
代わりに、バックエンドをMySQLのみに絞ったMySQLKitを採用しました。(FluentでMySQLバックエンドを使う場合もこれが使われる)
MySQLKitはlibmysqlclientに依存せず、SwiftNIOベースの非同期IOで実装されたMySQLNIOをMySQLクライアントとして使っています。ちょっとうれしいですね。
MySQLKitは高度な機能は提供しませんが、行をSwiftの構造体にマップするできるので今回の用途では十分でした。
struct Response: Decodable { var character: String } let characters = try await pools.withConnection { conn in try await conn.sql() .raw("SELECT `character` FROM `isu` GROUP BY `character`") .all(decoding: Row.self).get() }
最終的なPackage.swift
SwiftNIOがConcurrency APIにOSバージョンベースのavailabilityを付けちゃってるので、macOS 12未満のmacOS上で開発するために disable-availability-checking
を付けているのがミソ
// 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")]) ] )
過去問を解く
過去問としてISUCON 10の予選問題を解きました。素振りによってVaporやMySQLKitに慣れたり、Vaporのデフォルト設定の罠が把握できたのでかなり良かったです。
ただ、もう1年分くらい解いておけば本番でハマった問題も回避できたかなぁという気持ちです。過去問をやろう。
3人で13時間くらい使って移植した結果、他言語の初期実装と同程度のスコアを出すことには成功していました。
本番結果
練習の成果やasync/awaitの開発体験の良さもあり、4時間程度でおおよその実装が完了していましたが、その後のベンチマーカ互換性チェックのデバッグに手間取り、最終的にチェックを通過することができずスコアは0でした。
主なハマりポイントはDB内でのタイムゾーンの扱いでした。 MySQLNIOはDATETIME型のマッピングの際のタイムゾーン考慮はユーザの責務であるとし、ライブラリ内では常にGMTとして扱うため、9時間のズレにかなり悩まされました。
時間はむずかしい。
感想
かなり楽しかった。
- 愛と気合で移植できる
- 癖を把握する必要はあるがSwiftでもイケる
- 他の言語実装を動かしながら、一部のリクエストだけSwift実装に流す、みたなことができていればスコア0は回避できたかも。
- 来年再挑戦したい