Swiftがオープンソース化されLinuxに対応して久しいですが、このアドカレの投稿数を見ても分かる通りServer side swift、いまいち盛り上がってませんね。。。😅
理由は色々あると思いますが、原因の1つとして本番プロダクトに投入できるだけのナレッジが溜まっていないというのがあると思います。
今回はこの悪循環を解決すべく、手始めにいま最も人気のあるSwift製WebFramework「Vapor」をServer::Starterに対応させてホットデプロイ可能にしてやりましょう!!!
GitHub - vapor/vapor: 💧 A server-side Swift web framework.
え? SO_REUSEPORT
? ローリングデプロイ?そんな贅沢なものは知りません😉
Hot deployとは?
ダウンタイム無しで新しいバージョンのアプリケーションをデプロイすることです。
デプロイのたびにアプリケーションが止まってしまうのはもちろん良くないので、本番運用するにあたって必要な要件の一つだと考えています。
Server::Starterとは?
Server::Starter - search.cpan.org
サーバーをホットデプロイ可能にするためのCPANモジュールです。
こちらの記事が詳しいです。
PerlだけでなくRuby, Java, Go, Scalaなどの言語でも対応事例を見たことがあり、
自分が所属している会社含め、(特に日本のWeb界隈では)いまでも現役で使われていると思います。
Server::Starterに対応したサーバーを作るには?
READMEを読みながら要件を確認します。
By using Server::Starter it is much easier to write a hot-deployable server. Following are the only requirements a server program to be run under Server::Starter should conform to:
・receive file descriptors to listen to through an environment variable
・perform a graceful shutdown when receiving SIGTERM
-
start_server
のプロセスが環境変数経由でソケットのファイルディスクリプタを渡してくるので、それに対してaccept(2)
を呼ぶ作りにする -
start_server
のプロセスに対して(再起動のために)SIGHUPが打たれると、子プロセスにSIGTERMを投げてくるので、処理中のものを処理し終えてから死ぬようにする。つまりgraceful shutdownするようにする。
この2つさえ実装できればServer::Starterに対応したと言えそうです!
参考:
- Server::Starterに対応するとはどういうことか - limitusus’s diary
- 「Server::Starterに対応するとはどういうことか」の補足 - sonots:blog
- Server::Starter を Java で利用する方法。または、System.inheritedChannel() の挙動について - tokuhirom’s blog
Vaporの仕組み
swift-server/httpで「Swift server API」の策定が進んではいますが、まだリリースされておらず、各フレームワークが独自にサーバーのインターフェースを決めているような現状です。
Vaporにおいては ServerProtocol
というprotocolがサーバーが実装すべきprotocolになります。
/// Represents an HTTP server.
public protocol ServerProtocol {
/// creates a new server
init(
hostname: String,
port: Port,
_ securityLayer: SecurityLayer
) throws
/// starts the server, using the responder
/// to respond to accepted requests
func start(
_ responder: Responder,
errors: @escaping ServerErrorHandler
) throws
}
これをServerFactoryProtocol
を実装したファクトリ経由で作ることができ、このファクトリはアプリケーションの設定として渡すことができます。
/// types conforming to this protocol can be
/// set as the Droplet's `.server`
public protocol ServerFactoryProtocol {
func makeServer(
hostname: String,
port: Port,
_ securityLayer: SecurityLayer
) throws -> ServerProtocol
}
// main.swift
// 設定
let drop = try Droplet(config: config, server: myServerFactory)
環境変数からファイルディスクリプタとポート番号を取り出す
SERVER_STARTER_PORT
という環境変数にポート番号=ファイルディスクリプタ
のような形で入っているのでこれを取り出してあげます。
let env = ProcessInfo().environment
guard let value = env["SERVER_STARTER_PORT"] else {
fatalError("Server::Starte経由の起動でない")
}
guard let port = value.components(separatedBy: "=")[0]) else {
fatalError("portが入っていない")
}
guard let fd = value.components(separatedBy: "=")[1] else {
fatalError("ファイルディスクリプタが入ってない")
}
取り出したファイルディスクリプタ、ポート番号を元にソケットを作る
VaporにはTCPInternetSocket
というソケットを表すクラスがあるので、それを作ります。
let config = Sockets.Config.TCP()
// 上で取り出したものをもとにVaporのDescriptorオブジェクトを作る
let descriptor = Descriptor(integerLiteral: fd)
// 下記を参考にResolvedInternetAddressオブジェクトを作る
let resolved: ResolvedInternetAddress = ...
// サーバーソケットGET!!
let socket = try! TCPInternetSocket(descriptor, config, resolved)
ResolvedInternetAddress
を作るのがちょっと難しいですが、このあたりの実装を参考 にやってみてください。
あとはこのソケットに対してaccept(2)
していく作りにしておけばおkです。
start_server
プロセスからのSIGTERMを拾う
あとはSIGTERMを拾っていい感じに終了してやれば良さそうです!
signal
関数を使って直接ハンドルしても良いですが、良さそうなライブラリがあったのでそちらをつかました。
Signals.trap(signal: .term) { signal in
// 終了フラグを立てるなどの処理
// あとは良い感じにgraceful shutdown
}
ちなみにSIGPIPE
はVapor側で拾ってくれているみたいなので特に設定しなくても大丈夫そうです。
start_server
コマンド経由でVaporを実行
Vaporにはvapor
コマンドが用意されていて、これ経由でビルドや実行を行うことができます。(実際にはswift package
のラッパーです。)
リリース用にビルドしてあげます。
$ vapor build --release
あとはstart_server
経由でvapor run
を実行してあげます。
アプリケーションの起動に数秒かかるので適当に--kill-old-delay
を設定してあげて新しいプロセスの立ち上がりを待ってあげます。
$ start_server --port=80 --kill-old-delay=10 vapor run --release
アプリケーションを更新してビルドしたら、start_server
のプロセスにSIGHUP
を打ってあげればrestartします。
$ kill -HUP プロセス番号
まとめ
社内のハッカソンでこのネタをやったのでコードはめっちゃ汚いですが、一応レポジトリを貼っておきます。
テスト用に作った(Vaporとは関係ない)サーバー単体だとうまくいったのですが、Vapor側ではgraceful shutdownがうまく実装しきれずハッカソンでのデモは失敗しました。。が、方向性は示せたかなと思います😅
まだまだ本番運用するにはいろんな課題をクリアする必要がありますが、1つ1つ前に進めてServer side swiftを盛り上げていきましょう!