はじめに
遅くれるとよくないもの、それは納期とAPIのレスポンスです。
納期はお客さんや仲間からの信頼を失い、APIのレスポンス遅延はエンドユーザを失います。
webAPI構築にLambdaを利用するシーンについてパフォーマンスを改善していくのかをお話できればと思います。
Lambdaエコノミクスと謳っているとおり、Lambdaの前段にApiGatewayを配置したときのよくある構成をベースに、ボトルネックとなりなりうる箇所を紹介していきます。
対象読者層
・DynamoDB使ってみたいけど何に気をつければいいかわからない人
・RDS Proxy使ってみたいけど何に気をつければいいかわらかない人
・同じ構成でパフォーマンスがでなくて困ってる人
負荷試験をはじめるまえに
まず、プロダクトのパフォーマンスは負荷試験のときに初めて考えるものではありません。技術選定の段階から徐々に実施していくものです。そうすれば、フレームワークの選定ミスなど根っこの部分からの改善してがより早くできるのです。なので、なんとなくこのページに来た人は負荷試験からいきなりチャレンジではなく、もっと前の開発段階から負荷試験を実施し、ちょっとずつボトルネックを攻略していくものだと肝に命じてください。
非機能要件が決まっているのに機能が正常に動くの待ってからテストをするなんて、時間がもったいないのです。
以下にエッセンスが詰まっています。時間がある方はぜひご一読を。
参考図書:Amazon Web Services負荷試験入門―クラウドの性能の引き出し方がわかる
参考記事:Webアプリケーション負荷試験実践入門
パフォーマンス改善
1. LambdaとRDS、そしてRDS Proxyの関係
LambdaとRDSは兼ねてより相性が悪いことで知られていました。Lambdaは1リクエスト毎にスケールするため、あっという間にdbのコネクションが貼られ、メモリを食い潰します。2000の同時アクセスがきたら2000コネクションを貼るようなイメージです。
従来のwebサーバやwebアプリケーションだと、2000リクエストきたとしてもdbとは100だけコネクションを貼っておき、そのコネクションを使い回すことによってdbのコネクション溢れを防ぐ、コネクションプールという機能があります。
Lambdaは呼出毎に作られるためコネクションプール機構をもてないため、コネクションプール機構をサービス化しようという理由で作られたのがRDS Proxyなのです。Lambdaのインスタンスを都度起動するという性質上、接続コストを以下に抑えいくかがポイントになってきます。
さて、RDS Proxyをいれて終わりではありません。さらに2つのことを意識する必要があります。
1-1. LambdaとRDS Proxyのコネクション数を最小限にする
Lambdaは1リクエスト毎にインスタンスが立ち上がるという説明をしましたが、若干嘘があり、実際は一度立ち上がったインスタンスは一定時間アクセスが途絶えるまで再利用され続けるという性質です。
※公式の発表はないですが、5分ぐらいかも。
起動してすぐ死ぬわけではなく、できる限り再利用されるのを待ってくれているわけですね。Lambdaは起動に時間がかかってしまうため、スパイクに弱いという弱点がありますが、これを補うために予め予測されるスパイク数分を起動させておくことができます。これがProvisioned Concurrencyを有効にするとできるウォームスタート状態を維持させておくテクニックですね。
ここで大事なのは再利用された時に、新たにDB接続を作らないようにすることです。Lambdaも破棄されるまではDB接続を保持しているため、再利用されるたびに新規接続を増やし続けるとLambdaとRDS Proxy間の接続はいたずらに増えていきます。RDS ProxyがLambdaから受けたコネクションを無駄に保持してしまうので、性能劣化が起きると可能性があります。
本来であれば、Lambdaインスタンスは1リクエストしか処理できないため、コネクションプールも1つで十分です。不用意にDB接続をプールしないように注意しましょう。
1-2.RDS ProxyとRDSのコネクション数を減らす
RDS ProxyとRDS間のコネクションは、RDSのmax_connectionsのnパーセントを上限に設定できます。この上限のなかでRDS Proxyは接続をやりくりするのです。なので、RDSからレスポンスが返ってこないとその間他のdbコネクションが待たされることになります。ピン留めと呼ばれるコネクションがゾンビとして残ってしまう現象への注意や、時間がかかりすぎるクエリをタイムアウトで殺し次に繋げる設定はとても大事です。
tips
mysqlとjava構成だと、MySQL Connector/Jの設定はとても重要です。
用法容量守って正しくお使いください。
tips
insertやupdateなどの副作用のあるコマンドは大規模アクセスが予想されるapiではdynamodbへの移行を検討しましょう。dynamodb streamsなど非同期処理でrdsにいれれば性能劣化を防ぐことができます。
2. DynamoDB
DynamoDBの設計では、パーティションのスロットリングにも注意しましょう。同じキーに大量アクセスがあるとDynamoDBがスロットリングエラーを返します。DynamoDBだからといって、大量アクセスに強いと脳死設計していると陥りがちです。あえて分散させるというテクニックが必要です。
DynamoDBへの大量の読み込みリクエストによるスロットリングエラーの解消という記事で実際に検証結果が載せられています。気になる方はご一読ください。
3. コールドスタートを避ける
Lambdaにはコールドスタートがあるため、起動時間に時間がかかります。ただでさえ起動に時間がかかるため、そもそも起動に時間のかかるアプリケーションとは相性が悪いです。代表的なwebアプリケーションの言語のJavaとはとても相性が悪く、TypeScriptやPythonという言語を使われることが多いです。ですが、大規模アプリケーションの構築となるとどうしても堅牢性やリソース収集コストの容易性からJavaを採用したいこともあるでしょう。
そんな場合は、GraalVMでJavaをJVM上ではなく、直接実行可能な形式にしたりすることもできます。詳細は、業務でGraalVMとKotlinを使ってサーバレス開発やってみたのでちょっとハンズオンしてみるを確認してみてください。ハンズオン形式となっており、起動イメージが掴めると思います。
またAWS re:Invent 2022では、AwsSnapStartと呼ばれるサービスが登場しました。
4. AWSサポートを有効活用する
パフォーマンス改善にて行き詰まれば、遠慮なくAWSサポートに問い合わせしましょう。
自分が行き詰まっている部分は大抵ほかでも行き詰まっているはずです。調査と並行して活用することを強くおすすめします。
最後に
負荷試験とは、実験と心得てください。
仮説を立て、計測し、事実(ボトルネック)を見つけるのです。
仮説、計測、事実。のサイクルを繰り返すことで、ボトルネックを浮き彫りにし、パフォーマンスを改善していくのです。ときには、今まで事実と思っていることが間違ってることもあり、心が折れることもあるかもしれません。作った人もテストをしている自分も、みんな悪くありません。悪いのはプロダクトを遅くさせるボトルネックです。諦めないで根気良く取り組みましょう。
プロダクトの顔はデザインや機能ではありますが、それらが良ければ良いほど、パフォーマンスがユーザ体験に影響します。納得いくまで頑張りましょう!
私の理解で各種サービスについて記載しています。
誤った記載などしていれば、ご指摘いただけると幸いです。
参考Qiita