Rustをプロダクショングレードで使うときのよくあるパターン10選
Rustをプロダクショングレードで使うときのよくあるパターン10選
プロダクション環境でRustを使用する際、単に動くコードを書くだけでなく、保守性、パフォーマンス、安全性を考慮した設計が重要です。本記事では、実際のプロダクション開発で頻繁に使用される10個のパターンを、Rustの言語仕様とともに詳しく解説します。
1. Result型とエラーハンドリングの徹底
なぜ重要か
Rustには例外機構がありません。代わりにResult<T, E>
型を使用してエラーを表現します。これにより、エラーが発生する可能性のある処理が型システムで明示され、コンパイル時にエラー処理の漏れを防げます。
実装パターン
use std::fs::File;
use std::io::{self, Read};
// カスタムエラー型の定義
#[derive(Debug)]
enum AppError {
IoError(io::Error),
ParseError(String),
ValidationError(String),
}
// From トレイトを実装して ? 演算子を使えるようにする
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError::IoError(error)
}
}
// Result型を返す関数
fn read_config(path: &str) -> Result<String, AppError> {
let mut file = File::open(path)?; // ? 演算子でエラーを早期リターン
let mut contents = String::new();
file.read_to_string(&mut contents)?;
// バリデーション
if contents.is_empty() {
return Err(AppError::ValidationError("Config file is empty".to_string()));
}
Ok(contents)
}
// エラーハンドリングの例
fn main() {
match read_config("config.toml") {
Ok(config) => println!("Config loaded: {}", config),
Err(AppError::IoError(e)) => eprintln!("IO error: {}", e),
Err(AppError::ParseError(e)) => eprintln!("Parse error: {}", e),
Err(AppError::ValidationError(e)) => eprintln!("Validation error: {}", e),
}
}
ベストプラクティス
-
thiserror
クレートを使用してエラー型の定義を簡潔にする -
anyhow
クレートを使用してアプリケーションレベルのエラーハンドリングを簡素化 - エラーメッセージには十分なコンテキスト情報を含める
2. 所有権とライフタイムの適切な管理
なぜ重要か
Rustの所有権システムは、メモリ安全性を保証する核心機能です。各値には単一の所有者があり、所有者がスコープを抜けると値は自動的に解放されます。これにより、ガベージコレクタなしでメモリリークやダングリングポインタを防げます。
実装パターン
// 所有権の移動を避けるための参照の使用
struct User {
name: String,
email: String,
}
impl User {
// &self で不変参照を受け取る
fn display_name(&self) -> &str {
&self.name
}
// &mut self で可変参照を受け取る
fn update_email(&mut self, new_email: String) {
self.email = new_email;
}
}
// ライフタイムパラメータの明示
struct Cache<'a> {
data: &'a str, // 'a はライフタイムパラメータ
}
impl<'a> Cache<'a> {
fn new(data: &'a str) -> Self {
Cache { data }
}
// 戻り値のライフタイムは入力と同じ
fn get_data(&self) -> &'a str {
self.data
}
}
// スマートポインタの使用例
use std::rc::Rc;
use std::cell::RefCell;
// 複数の所有者が必要な場合
fn shared_ownership() {
let shared_data = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared_data);
let clone2 = Rc::clone(&shared_data);
println!("Reference count: {}", Rc::strong_count(&shared_data)); // 3
}
// 内部可変性が必要な場合
struct Counter {
value: RefCell<i32>,
}
impl Counter {
fn new() -> Self {
Counter { value: RefCell::new(0) }
}
fn increment(&self) {
*self.value.borrow_mut() += 1;
}
}
ベストプラクティス
- 可能な限り参照を使用し、所有権の移動を避ける
-
Clone
は慎重に使用し、パフォーマンスへの影響を考慮する - 循環参照を避けるため、
Weak<T>
を適切に使用する
3. トレイトによる抽象化とジェネリクス
なぜ重要か
トレイトはRustにおけるインターフェースの仕組みで、異なる型に共通の振る舞いを定義できます。ジェネリクスと組み合わせることで、型安全で再利用可能なコードを書けます。
実装パターン
// トレイトの定義
trait Repository<T> {
fn find_by_id(&self, id: u64) -> Option<T>;
fn save(&mut self, entity: T) -> Result<(), String>;
fn delete(&mut self, id: u64) -> Result<(), String>;
}
// ジェネリックな実装
struct InMemoryRepository<T> {
data: std::collections::HashMap<u64, T>,
next_id: u64,
}
impl<T> InMemoryRepository<T> {
fn new() -> Self {
InMemoryRepository {
data: std::collections::HashMap::new(),
next_id: 1,
}
}
}
// トレイトの実装
impl<T: Clone> Repository<T> for InMemoryRepository<T> {
fn find_by_id(&self, id: u64) -> Option<T> {
self.data.get(&id).cloned()
}
fn save(&mut self, entity: T) -> Result<(), String> {
let id = self.next_id;
self.data.insert(id, entity);
self.next_id += 1;
Ok(())
}
fn delete(&mut self, id: u64) -> Result<(), String> {
self.data.remove(&id)
.map(|_| ())
.ok_or_else(|| format!("Entity with id {} not found", id))
}
}
// トレイト境界を使用した関数
fn process_repository<T, R>(repo: &R, id: u64)
where
R: Repository<T>,
T: std::fmt::Debug,
{
if let Some(entity) = repo.find_by_id(id) {
println!("Found entity: {:?}", entity);
}
}
// 派生マクロの活用
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Product {
id: u64,
name: String,
price: f64,
}
ベストプラクティス
- トレイトは小さく、単一責任の原則に従って設計する
- ジェネリクスには適切なトレイト境界を設定する
- 標準ライブラリのトレイト(
Debug
,Clone
,Send
,Sync
など)を積極的に実装する
4. 非同期プログラミングとasync/await
なぜ重要か
プロダクション環境では、I/Oバウンドなタスクを効率的に処理することが重要です。Rustのasync/awaitは、非同期処理を同期的なコードのように書けるため、可読性とパフォーマンスを両立できます。
実装パターン
use tokio;
use std::time::Duration;
// 非同期関数の定義
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let response = reqwest::get(url).await?;
let body = response.text().await?;
Ok(body)
}
// タイムアウト処理
async fn fetch_with_timeout(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let timeout_duration = Duration::from_secs(5);
match tokio::time::timeout(timeout_duration, fetch_data(url)).await {
Ok(Ok(data)) => Ok(data),
Ok(Err(e)) => Err(Box::new(e)),
Err(_) => Err("Request timed out".into()),
}
}
// 並行処理
async fn fetch_multiple_urls(urls: Vec<&str>) -> Vec<Result<String, Box<dyn std::error::Error>>> {
let futures: Vec<_> = urls.into_iter()
.map(|url| fetch_with_timeout(url))
.collect();
// すべてのFutureを並行に実行
futures::future::join_all(futures).await
}
// エラーハンドリングと再試行
async fn fetch_with_retry(url: &str, max_retries: u32) -> Result<String, String> {
let mut retries = 0;
loop {
match fetch_data(url).await {
Ok(data) => return Ok(data),
Err(e) => {
retries += 1;
if retries >= max_retries {
return Err(format!("Failed after {} retries: {}", max_retries, e));
}
// 指数バックオフ
let delay = Duration::from_millis(100 * 2u64.pow(retries));
tokio::time::sleep(delay).await;
}
}
}
}
// メイン関数
#[tokio::main]
async fn main() {
let urls = vec![
"https://api.example.com/data1",
"https://api.example.com/data2",
"https://api.example.com/data3",
];
let results = fetch_multiple_urls(urls).await;
for (i, result) in results.iter().enumerate() {
match result {
Ok(data) => println!("URL {} fetched successfully", i),
Err(e) => eprintln!("URL {} failed: {}", i, e),
}
}
}
ベストプラクティス
- 適切な非同期ランタイム(tokio、async-std)を選択する
- CPUバウンドなタスクは別スレッドで実行する
-
select!
マクロを使用して複数のFutureを効率的に処理する
5. 構造化ログとメトリクス
なぜ重要か
プロダクション環境では、アプリケーションの動作を監視し、問題を迅速に特定できることが重要です。構造化ログとメトリクスは、この目的を達成するための基盤となります。
実装パターン
use tracing::{info, warn, error, instrument};
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u64,
username: String,
email: String,
}
// 関数レベルのトレーシング
#[instrument(skip(password))] // パスワードはログに含めない
async fn authenticate_user(username: &str, password: &str) -> Result<User, String> {
info!(username = %username, "Attempting to authenticate user");
// 認証ロジック(仮実装)
if username == "admin" && password == "secret" {
let user = User {
id: 1,
username: username.to_string(),
email: "admin@example.com".to_string(),
};
info!(user_id = %user.id, "User authenticated successfully");
Ok(user)
} else {
warn!(username = %username, "Authentication failed");
Err("Invalid credentials".to_string())
}
}
// メトリクスの実装
use prometheus::{Counter, Histogram, Registry};
use lazy_static::lazy_static;
lazy_static! {
static ref HTTP_REQUESTS_TOTAL: Counter = Counter::new(
"http_requests_total",
"Total number of HTTP requests"
).unwrap();
static ref HTTP_REQUEST_DURATION: Histogram = Histogram::with_opts(
prometheus::HistogramOpts::new(
"http_request_duration_seconds",
"HTTP request duration in seconds"
)
).unwrap();
}
// メトリクス付きハンドラー
async fn handle_request(path: &str) -> Result<String, Box<dyn std::error::Error>> {
HTTP_REQUESTS_TOTAL.inc();
let timer = HTTP_REQUEST_DURATION.start_timer();
let result = process_request(path).await;
timer.observe_duration();
result
}
async fn process_request(path: &str) -> Result<String, Box<dyn std::error::Error>> {
// リクエスト処理ロジック
Ok(format!("Processed: {}", path))
}
// 構造化ログの設定
fn setup_logging() {
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer()
.json() // JSON形式でログ出力
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true))
.init();
}
ベストプラクティス
-
tracing
クレートを使用して構造化ログを実装 - センシティブな情報はログから除外する
- 適切なログレベルを使用する(DEBUG, INFO, WARN, ERROR)
- Prometheusなどの標準的なメトリクス形式を採用
6. 設定管理とシークレット処理
なぜ重要か
プロダクション環境では、環境ごとに異なる設定値を管理し、シークレット情報を安全に扱う必要があります。Rustでは型安全な設定管理が可能です。
実装パターン
use serde::{Deserialize, Serialize};
use config::{Config, ConfigError, Environment, File};
#[derive(Debug, Deserialize, Serialize)]
struct DatabaseConfig {
host: String,
port: u16,
username: String,
#[serde(skip_serializing)] // シリアライズ時に除外
password: String,
database_name: String,
max_connections: u32,
}
#[derive(Debug, Deserialize, Serialize)]
struct ServerConfig {
host: String,
port: u16,
workers: usize,
}
#[derive(Debug, Deserialize, Serialize)]
struct AppConfig {
database: DatabaseConfig,
server: ServerConfig,
jwt_secret: String,
log_level: String,
}
impl AppConfig {
fn new() -> Result<Self, ConfigError> {
let config = Config::builder()
// デフォルト値を設定
.set_default("server.host", "127.0.0.1")?
.set_default("server.port", 8080)?
.set_default("server.workers", num_cpus::get())?
.set_default("database.max_connections", 10)?
.set_default("log_level", "info")?
// 設定ファイルを読み込む(環境に応じて)
.add_source(File::with_name("config/default"))
.add_source(File::with_name(&format!("config/{}",
std::env::var("RUN_ENV").unwrap_or_else(|_| "development".to_string())
)).required(false))
// 環境変数でオーバーライド(APP_ プレフィックス)
.add_source(Environment::with_prefix("APP").separator("_"))
.build()?;
// 設定を構造体にデシリアライズ
let mut app_config: AppConfig = config.try_deserialize()?;
// シークレットの追加処理
app_config.database.password = Self::load_secret("DB_PASSWORD")?;
app_config.jwt_secret = Self::load_secret("JWT_SECRET")?;
Ok(app_config)
}
fn load_secret(key: &str) -> Result<String, ConfigError> {
// 環境変数から読み込む
std::env::var(key)
.map_err(|_| ConfigError::Message(format!("Secret {} not found", key)))
}
// 設定の検証
fn validate(&self) -> Result<(), String> {
if self.database.username.is_empty() {
return Err("Database username cannot be empty".to_string());
}
if self.server.port == 0 {
return Err("Server port must be greater than 0".to_string());
}
if self.jwt_secret.len() < 32 {
return Err("JWT secret must be at least 32 characters".to_string());
}
Ok(())
}
}
// 設定のホットリロード
use notify::{Watcher, RecursiveMode, watcher};
use std::sync::{Arc, RwLock};
use std::sync::mpsc::channel;
use std::time::Duration;
fn watch_config(config: Arc<RwLock<AppConfig>>) {
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
watcher.watch("config/", RecursiveMode::Recursive).unwrap();
std::thread::spawn(move || {
loop {
match rx.recv() {
Ok(_) => {
match AppConfig::new() {
Ok(new_config) => {
if let Err(e) = new_config.validate() {
error!("Invalid configuration: {}", e);
continue;
}
let mut config_guard = config.write().unwrap();
*config_guard = new_config;
info!("Configuration reloaded successfully");
}
Err(e) => error!("Failed to reload configuration: {}", e),
}
}
Err(e) => error!("Watch error: {}", e),
}
}
});
}
ベストプラクティス
- 環境変数とファイルベースの設定を組み合わせる
- シークレットは専用の管理システム(Vault、AWS Secrets Manager)から読み込む
- 設定の検証を実装し、起動時にチェックする
- センシティブな情報のログ出力を防ぐ
7. テストとモックの戦略
なぜ重要か
Rustの強力な型システムを活用しても、ビジネスロジックの正確性を保証するにはテストが不可欠です。モックを使用することで、外部依存を分離したテストが可能になります。
実装パターン
// テスト対象のコード
#[derive(Debug, PartialEq)]
struct Order {
id: u64,
user_id: u64,
total: f64,
status: OrderStatus,
}
#[derive(Debug, PartialEq)]
enum OrderStatus {
Pending,
Confirmed,
Shipped,
Delivered,
}
// 依存性注入のためのトレイト
trait PaymentGateway {
fn process_payment(&self, amount: f64) -> Result<String, String>;
}
trait NotificationService {
fn send_notification(&self, user_id: u64, message: &str) -> Result<(), String>;
}
struct OrderService<P: PaymentGateway, N: NotificationService> {
payment_gateway: P,
notification_service: N,
}
impl<P: PaymentGateway, N: NotificationService> OrderService<P, N> {
fn new(payment_gateway: P, notification_service: N) -> Self {
OrderService {
payment_gateway,
notification_service,
}
}
fn process_order(&self, order: &mut Order) -> Result<(), String> {
// 支払い処理
let transaction_id = self.payment_gateway.process_payment(order.total)?;
// ステータス更新
order.status = OrderStatus::Confirmed;
// 通知送信
self.notification_service.send_notification(
order.user_id,
&format!("Order {} confirmed. Transaction: {}", order.id, transaction_id)
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockall::*;
// モックの定義
#[automock]
trait PaymentGateway {
fn process_payment(&self, amount: f64) -> Result<String, String>;
}
#[automock]
trait NotificationService {
fn send_notification(&self, user_id: u64, message: &str) -> Result<(), String>;
}
#[test]
fn test_process_order_success() {
// モックの設定
let mut mock_payment = MockPaymentGateway::new();
mock_payment
.expect_process_payment()
.with(eq(100.0))
.times(1)
.returning(|_| Ok("TX123".to_string()));
let mut mock_notification = MockNotificationService::new();
mock_notification
.expect_send_notification()
.with(eq(1), mockall::predicate::str::contains("Order 1 confirmed"))
.times(1)
.returning(|_, _| Ok(()));
// テスト実行
let service = OrderService::new(mock_payment, mock_notification);
let mut order = Order {
id: 1,
user_id: 1,
total: 100.0,
status: OrderStatus::Pending,
};
assert!(service.process_order(&mut order).is_ok());
assert_eq!(order.status, OrderStatus::Confirmed);
}
#[test]
fn test_process_order_payment_failure() {
// 支払い失敗のモック
let mut mock_payment = MockPaymentGateway::new();
mock_payment
.expect_process_payment()
.returning(|_| Err("Payment declined".to_string()));
let mock_notification = MockNotificationService::new();
let service = OrderService::new(mock_payment, mock_notification);
let mut order = Order {
id: 2,
user_id: 1,
total: 100.0,
status: OrderStatus::Pending,
};
let result = service.process_order(&mut order);
assert!(result.is_err());
assert_eq!(order.status, OrderStatus::Pending); // ステータスは変更されない
}
// プロパティベーステスト
use proptest::prelude::*;
proptest! {
#[test]
fn test_order_total_always_positive(
id in 1..1000u64,
user_id in 1..100u64,
total in 0.01..10000.0f64
) {
let order = Order {
id,
user_id,
total,
status: OrderStatus::Pending,
};
prop_assert!(order.total > 0.0);
}
}
// 統合テスト
#[test]
fn test_integration_with_real_services() {
// テスト用の実装を使用
struct TestPaymentGateway;
impl PaymentGateway for TestPaymentGateway {
fn process_payment(&self, _amount: f64) -> Result<String, String> {
Ok("TEST_TX_123".to_string())
}
}
struct TestNotificationService;
impl NotificationService for TestNotificationService {
fn send_notification(&self, _user_id: u64, _message: &str) -> Result<(), String> {
Ok(())
}
}
let service = OrderService::new(TestPaymentGateway, TestNotificationService);
let mut order = Order {
id: 3,
user_id: 1,
total: 50.0,
status: OrderStatus::Pending,
};
assert!(service.process_order(&mut order).is_ok());
}
}
// ベンチマークテスト
#[cfg(test)]
mod benches {
use super::*;
use test::Bencher;
#[bench]
fn bench_process_order(b: &mut Bencher) {
struct DummyPaymentGateway;
impl PaymentGateway for DummyPaymentGateway {
fn process_payment(&self, _: f64) -> Result<String, String> {
Ok("dummy".to_string())
}
}
struct DummyNotificationService;
impl NotificationService for DummyNotificationService {
fn send_notification(&self, _: u64, _: &str) -> Result<(), String> {
Ok(())
}
}
let service = OrderService::new(DummyPaymentGateway, DummyNotificationService);
b.iter(|| {
let mut order = Order {
id: 1,
user_id: 1,
total: 100.0,
status: OrderStatus::Pending,
};
service.process_order(&mut order)
});
}
}
ベストプラクティス
- 単体テスト、統合テスト、E2Eテストを適切に使い分ける
-
mockall
クレートを使用してモックを簡単に作成 - プロパティベーステストで境界値をカバー
- CIパイプラインでテストを自動実行
8. 並行処理とデータ競合の防止
なぜ重要か
マルチスレッド環境でのデータ競合は、予測不可能なバグの原因となります。Rustの型システムは、コンパイル時にデータ競合を防ぐことができます。
実装パターン
use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicU64, Ordering}};
use std::thread;
use std::collections::HashMap;
use parking_lot;
use dashmap::DashMap;
// 基本的なMutexの使用
struct SharedCounter {
count: Mutex<u64>,
}
impl SharedCounter {
fn new() -> Self {
SharedCounter {
count: Mutex::new(0),
}
}
fn increment(&self) {
let mut count = self.count.lock().unwrap();
*count += 1;
}
fn get(&self) -> u64 {
*self.count.lock().unwrap()
}
}
// RwLockを使用した読み取り最適化
struct Cache {
data: RwLock<HashMap<String, String>>,
}
impl Cache {
fn new() -> Self {
Cache {
data: RwLock::new(HashMap::new()),
}
}
fn get(&self, key: &str) -> Option<String> {
self.data.read().unwrap().get(key).cloned()
}
fn insert(&self, key: String, value: String) {
self.data.write().unwrap().insert(key, value);
}
}
// Atomicを使用したロックフリーカウンター
struct AtomicCounter {
count: AtomicU64,
}
impl AtomicCounter {
fn new() -> Self {
AtomicCounter {
count: AtomicU64::new(0),
}
}
fn increment(&self) -> u64 {
self.count.fetch_add(1, Ordering::SeqCst)
}
fn get(&self) -> u64 {
self.count.load(Ordering::SeqCst)
}
}
// より高性能な並行データ構造
struct HighPerformanceCache {
// DashMapは内部でシャーディングを使用
data: DashMap<String, String>,
// parking_lotのMutexは標準ライブラリより高速
stats: parking_lot::Mutex<CacheStats>,
}
struct CacheStats {
hits: u64,
misses: u64,
}
impl HighPerformanceCache {
fn new() -> Self {
HighPerformanceCache {
data: DashMap::new(),
stats: parking_lot::Mutex::new(CacheStats { hits: 0, misses: 0 }),
}
}
fn get(&self, key: &str) -> Option<String> {
let result = self.data.get(key).map(|v| v.clone());
let mut stats = self.stats.lock();
if result.is_some() {
stats.hits += 1;
} else {
stats.misses += 1;
}
result
}
fn insert(&self, key: String, value: String) {
self.data.insert(key, value);
}
}
// チャネルを使用したアクター風パターン
use std::sync::mpsc::{channel, Sender};
enum CacheCommand {
Get { key: String, response: Sender<Option<String>> },
Insert { key: String, value: String },
Clear,
}
struct ActorCache {
sender: Sender<CacheCommand>,
}
impl ActorCache {
fn new() -> Self {
let (sender, receiver) = channel();
thread::spawn(move || {
let mut cache = HashMap::new();
for command in receiver {
match command {
CacheCommand::Get { key, response } => {
let _ = response.send(cache.get(&key).cloned());
}
CacheCommand::Insert { key, value } => {
cache.insert(key, value);
}
CacheCommand::Clear => {
cache.clear();
}
}
}
});
ActorCache { sender }
}
fn get(&self, key: String) -> Option<String> {
let (response_sender, response_receiver) = channel();
self.sender.send(CacheCommand::Get {
key,
response: response_sender,
}).unwrap();
response_receiver.recv().unwrap()
}
fn insert(&self, key: String, value: String) {
self.sender.send(CacheCommand::Insert { key, value }).unwrap();
}
}
// 使用例
fn concurrent_example() {
let cache = Arc::new(HighPerformanceCache::new());
let mut handles = vec![];
// 複数のスレッドから同時アクセス
for i in 0..10 {
let cache_clone = Arc::clone(&cache);
let handle = thread::spawn(move || {
for j in 0..1000 {
let key = format!("key_{}_{}", i, j);
let value = format!("value_{}_{}", i, j);
cache_clone.insert(key.clone(), value);
// 読み取りテスト
if let Some(v) = cache_clone.get(&key) {
assert_eq!(v, format!("value_{}_{}", i, j));
}
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
ベストプラクティス
- 適切な同期プリミティブを選択(Mutex、RwLock、Atomic)
-
parking_lot
クレートでパフォーマンスを向上 -
DashMap
などの並行データ構造を活用 - デッドロックを避けるため、ロックの順序を統一
9. ビルドとデプロイの最適化
なぜ重要か
プロダクション環境では、ビルドサイズの削減と起動時間の短縮が重要です。Rustは様々な最適化オプションを提供しています。
実装パターン
# Cargo.toml
[package]
name = "production-app"
version = "1.0.0"
edition = "2021"
# 依存関係の最適化
[dependencies]
# 必要な機能のみを有効化
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
serde = { version = "1", features = ["derive"] }
# 開発時のみ必要な依存関係
[dev-dependencies]
criterion = "0.5"
proptest = "1"
# リリースビルドの最適化
[profile.release]
opt-level = 3 # 最大最適化
lto = true # Link Time Optimization
codegen-units = 1 # 単一のコード生成ユニット
strip = true # シンボル情報を削除
panic = "abort" # パニック時のスタック巻き戻しを無効化
# カスタムプロファイル(プロファイリング用)
[profile.release-with-debug]
inherits = "release"
debug = true
# ベンチマーク用プロファイル
[profile.bench]
opt-level = 3
// build.rs - ビルドスクリプト
use std::env;
use std::process::Command;
fn main() {
// Git情報を埋め込む
let output = Command::new("git")
.args(&["rev-parse", "HEAD"])
.output()
.unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash.trim());
// ビルド時刻を埋め込む
let build_time = chrono::Utc::now().to_rfc3339();
println!("cargo:rustc-env=BUILD_TIME={}", build_time);
// 条件付きコンパイル
if env::var("CARGO_FEATURE_MONITORING").is_ok() {
println!("cargo:rustc-cfg=monitoring_enabled");
}
}
// main.rs - バージョン情報の使用
const VERSION: &str = env!("CARGO_PKG_VERSION");
const GIT_HASH: &str = env!("GIT_HASH");
const BUILD_TIME: &str = env!("BUILD_TIME");
fn print_version() {
println!("Version: {}", VERSION);
println!("Git Hash: {}", GIT_HASH);
println!("Build Time: {}", BUILD_TIME);
}
// 条件付きコンパイルの使用
#[cfg(monitoring_enabled)]
fn setup_monitoring() {
// モニタリングのセットアップ
}
#[cfg(not(monitoring_enabled))]
fn setup_monitoring() {
// 何もしない
}
# Dockerfile - マルチステージビルド
# ビルドステージ
FROM rust:1.70 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
# 依存関係のキャッシュ
RUN mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release && \
rm -rf src
# ソースコードをコピーしてビルド
COPY . .
RUN cargo build --release
# 実行ステージ
FROM debian:bookworm-slim
# 必要なライブラリのみインストール
RUN apt-get update && \
apt-get install -y ca-certificates && \
rm -rf /var/lib/apt/lists/*
# ユーザーを作成
RUN useradd -m -u 1001 appuser
WORKDIR /app
# バイナリのみコピー
COPY /app/target/release/production-app /app/
# 所有権を設定
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
CMD ["./production-app"]
# .github/workflows/release.yml - GitHub Actions
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Strip binary
run: strip target/${{ matrix.target }}/release/production-app
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: production-app-${{ matrix.target }}
path: target/${{ matrix.target }}/release/production-app
ベストプラクティス
- LTOとコード生成ユニットの最適化でバイナリサイズを削減
- マルチステージDockerビルドで最小限のイメージを作成
- 複数のターゲットアーキテクチャをサポート
- CIでのキャッシュを活用してビルド時間を短縮
10. グレースフルシャットダウンとリソース管理
なぜ重要か
プロダクション環境では、アプリケーションの停止時にデータの整合性を保ち、進行中の処理を適切に完了させる必要があります。
実装パターン
use tokio::signal;
use tokio::sync::{broadcast, mpsc};
use tokio::time::{timeout, Duration};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
// シャットダウン管理
struct ShutdownManager {
shutdown_tx: broadcast::Sender<()>,
is_shutting_down: Arc<AtomicBool>,
}
impl ShutdownManager {
fn new() -> Self {
let (shutdown_tx, _) = broadcast::channel(1);
ShutdownManager {
shutdown_tx,
is_shutting_down: Arc::new(AtomicBool::new(false)),
}
}
fn subscribe(&self) -> broadcast::Receiver<()> {
self.shutdown_tx.subscribe()
}
fn is_shutting_down(&self) -> bool {
self.is_shutting_down.load(Ordering::SeqCst)
}
async fn shutdown(&self) {
self.is_shutting_down.store(true, Ordering::SeqCst);
let _ = self.shutdown_tx.send(());
}
}
// グレースフル停止可能なワーカー
struct Worker {
id: u32,
shutdown_rx: broadcast::Receiver<()>,
task_rx: mpsc::Receiver<Task>,
}
struct Task {
id: u64,
data: String,
}
impl Worker {
async fn run(mut self) {
loop {
tokio::select! {
// シャットダウンシグナル
_ = self.shutdown_rx.recv() => {
info!("Worker {} received shutdown signal", self.id);
break;
}
// タスク処理
Some(task) = self.task_rx.recv() => {
self.process_task(task).await;
}
}
}
// 残りのタスクを処理
self.drain_tasks().await;
info!("Worker {} shutdown complete", self.id);
}
async fn process_task(&self, task: Task) {
info!("Worker {} processing task {}", self.id, task.id);
// タスク処理のシミュレーション
tokio::time::sleep(Duration::from_millis(100)).await;
}
async fn drain_tasks(&mut self) {
let drain_timeout = Duration::from_secs(30);
let start = tokio::time::Instant::now();
while let Ok(Some(task)) = timeout(
drain_timeout.saturating_sub(start.elapsed()),
self.task_rx.recv()
).await {
self.process_task(task).await;
}
}
}
// リソースのクリーンアップ
struct ResourceManager {
database_pool: Arc<DatabasePool>,
cache: Arc<Cache>,
}
struct DatabasePool {
// 実際のデータベース接続プール
}
impl DatabasePool {
async fn close(&self) {
info!("Closing database connections...");
// すべての接続を閉じる
tokio::time::sleep(Duration::from_millis(500)).await;
info!("Database connections closed");
}
}
impl ResourceManager {
async fn cleanup(&self) {
// 並行してクリーンアップ
let (db_result, cache_result) = tokio::join!(
self.database_pool.close(),
self.cache.flush()
);
info!("All resources cleaned up");
}
}
// メインアプリケーション
struct Application {
shutdown_manager: ShutdownManager,
workers: Vec<mpsc::Sender<Task>>,
resource_manager: ResourceManager,
}
impl Application {
async fn run(&self) {
// シグナルハンドラーの設定
let shutdown_manager = self.shutdown_manager.clone();
tokio::spawn(async move {
shutdown_signal().await;
info!("Shutdown signal received");
shutdown_manager.shutdown().await;
});
// アプリケーションのメインループ
let mut shutdown_rx = self.shutdown_manager.subscribe();
loop {
tokio::select! {
_ = shutdown_rx.recv() => {
info!("Application shutting down...");
break;
}
// 通常の処理
_ = self.process_requests() => {}
}
}
// グレースフルシャットダウン
self.graceful_shutdown().await;
}
async fn process_requests(&self) {
// リクエスト処理のシミュレーション
tokio::time::sleep(Duration::from_secs(1)).await;
}
async fn graceful_shutdown(&self) {
info!("Starting graceful shutdown...");
// 新規リクエストの受付を停止
// ワーカーへのタスク送信を停止
for sender in &self.workers {
drop(sender);
}
// タイムアウト付きでシャットダウンを待つ
let shutdown_timeout = Duration::from_secs(60);
match timeout(shutdown_timeout, self.wait_for_workers()).await {
Ok(_) => info!("All workers shut down successfully"),
Err(_) => error!("Worker shutdown timed out"),
}
// リソースのクリーンアップ
self.resource_manager.cleanup().await;
info!("Graceful shutdown complete");
}
async fn wait_for_workers(&self) {
// ワーカーの終了を待つロジック
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
// クロスプラットフォーム対応のシグナルハンドリング
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}
// メイン関数
#[tokio::main]
async fn main() {
// ロギングの初期化
setup_logging();
// アプリケーションの初期化
let app = Application::new().await;
// アプリケーション実行
app.run().await;
info!("Application terminated");
}
ベストプラクティス
- シグナルハンドリングで適切な停止処理を実装
- タイムアウトを設定して無限に待機することを防ぐ
- リソースの並行クリーンアップで停止時間を短縮
- 進行中の処理を適切に完了させる
まとめ
これらの10個のパターンは、Rustでプロダクショングレードなアプリケーションを構築する際の基礎となります。各パターンは独立して使用できますが、組み合わせることでより堅牢なシステムを構築できます。
重要なポイント:
- Rustの型システムを最大限活用してコンパイル時にエラーを防ぐ
- 適切な抽象化により、テスタブルで保守しやすいコードを書く
- パフォーマンスと安全性のバランスを考慮する
- プロダクション環境での運用を見据えた設計をする
これらのパターンを実践することで、安全で高性能、かつ保守しやすいRustアプリケーションを構築できるでしょう。
Discussion