🦀

aws-sdk-rustを高速化する at Mac

2025/01/30に公開

ことのはじまり

GoとRustでaws sdkを使ってS3にアクセスしてみたところ、

main.go
package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/smithy-go"
)

func main() {
	start := time.Now()

	profile := "hakusai"
	bucket := "hakusai-test-bucket"

	ctx := context.Background()
	sdkConfig, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(profile))
	if err != nil {
		fmt.Println("Couldn't load default configuration. Have you set up your AWS account?")
		fmt.Println(err)
		return
	}
	s3Client := s3.NewFromConfig(sdkConfig)
	_, err = s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: &bucket})
	if err != nil {
		var ae smithy.APIError
		if errors.As(err, &ae) && ae.ErrorCode() == "AccessDenied" {
			fmt.Println("You don't have permission to list buckets for this account.")
		} else {
			fmt.Printf("Couldn't list buckets for your account. Here's why: %v\n", err)
		}
		return
	}
	requestSent := time.Since(start)
	fmt.Printf("request sent %d\n", requestSent)
}
main.rs
use std::{error::Error, time::Instant};

use aws_config::BehaviorVersion;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let start = Instant::now();

    let profile = "hakusai";
    let bucket = "hakusai-test-bucket";

    let config = aws_config::ConfigLoader::default()
        .behavior_version(BehaviorVersion::latest())
        .profile_name(profile)
        .load()
        .await;

    let client = aws_sdk_s3::Client::new(&config);
    let _result = client.list_objects_v2().bucket(bucket).send().await?;
    let request_sent = start.elapsed();
    println!("{:?}", request_sent);
    Ok(())
}
Cargo.toml
[package]
name = "aws_sdk_rust_bench"
version = "0.1.0"
edition = "2021"

[dependencies]
aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1.71.0"
tokio = { version = "1", features = ["full"] }

[profile.release]
debug = true
  • Go 89ms
  • Rust 207ms

もちろんRustが常に最速とは限りませんが、さすがに差が大きすぎないか?と思ったので調べてみました。

ボトルネックを特定する

cargo flamegraphを使ってどこに時間が掛かっているかを見てみます。
https://github.com/flamegraph-rs/flamegraph

一番時間がかかっているのがrustls_native_certs::macos::load_native_certsから呼ばれているSecurity SecTrustSettingsCopyTrustSettings

https://developer.apple.com/documentation/security/sectrustsettingscopytrustsettings(::_:)

これはMacOSの標準APIで、どうやらrustls経由でOSが信用している証明書を取ってくる処理に時間がかかっているようです。(他のOSの場合はここまで時間はかからないかもしれません。)

解決策

aws-sdk-rustが使うrustlsの設定を変更し、OS搭載の証明書の代わりに他の証明書で検証するようにしてみます。hyper_rustlsのfeaturesを指定すれば代わりにwebpki_rootsを使うことができます。webpki_rootsはソースコードにCA証明書のバイト列がベタ書きされているという力技です。

https://github.com/rustls/webpki-roots/blob/main/webpki-root-certs/src/lib.rs

以下のように書き換えます。

Cargo.toml
[package]
name = "aws_sdk_rust_bench"
version = "0.1.0"
edition = "2021"

[dependencies]
aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1.71.0"
aws-smithy-runtime = "1.7.7"
hyper-rustls = { version = "0.25.0", features = ["webpki-roots"] }
tokio = { version = "1", features = ["full"] }

[profile.release]
debug = true
main.rs
use std::{error::Error, time::Instant};

use aws_config::BehaviorVersion;
use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let start = Instant::now();

    let profile = "hakusai";
    let bucket = "hakusai-test-bucket";

    let rustls_connector = hyper_rustls::HttpsConnectorBuilder::new()
        .with_webpki_roots()
        .https_only()
        .enable_http1()
        .build();

    let http_client = HyperClientBuilder::new().build(rustls_connector);

    let config = aws_config::ConfigLoader::default()
        .behavior_version(BehaviorVersion::latest())
        .profile_name(profile)
        .http_client(http_client)
        .load()
        .await;

    let config_loaded = start.elapsed();
    println!("{:?}", config_loaded);
    let client = aws_sdk_s3::Client::new(&config);
    let _result = client.list_objects_v2().bucket(bucket).send().await?;
    let request_sent = start.elapsed();
    println!("{:?}", request_sent - config_loaded);
    Ok(())
}

注意点としては、aws-sdk-rustは内部的にはhttpクレートの0.2系に依存しているので、1系に依存している最新のhyper-rustlsを使うと型が合わずにコンパイルエラーになります。以下のエラーが出たらバージョンを見直してください。

the trait bound `HttpsConnector<hyper_util::client::legacy::connect::http::HttpConnector>: tower_service::Service<http::uri::Uri>` is not satisfied
the trait `tower_service::Service<http::uri::Uri>` is implemented for `HttpsConnector<hyper_util::client::legacy::connect::http::HttpConnector>`
for that trait implementation, expected `http::uri::Uri`, found `http::uri::Uri`
  • 変更前 207ms
  • 変更後 110ms

十分でしょう。

参考

https://github.com/1hakusai1/aws_sdk_rust_bench

Discussion