パフォーマンスチューニング虎の巻
パフォーマンスチューニング虎の巻
Section titled “パフォーマンスチューニング虎の巻”Spring Bootアプリケーションのパフォーマンスを向上させるための実践的なテクニックを、問題の特定から解決まで体系的に解説します。
パフォーマンスチューニングの基本フロー
Section titled “パフォーマンスチューニングの基本フロー”1. 問題の特定(プロファイリング) ↓2. ボトルネックの分析 ↓3. 最適化の実施 ↓4. 効果の測定 ↓5. 繰り返し改善1. データベースクエリの最適化
Section titled “1. データベースクエリの最適化”// パフォーマンスログを有効化spring.jpa.show-sql=truespring.jpa.properties.hibernate.format_sql=truelogging.level.org.hibernate.SQL=DEBUGlogging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACEクエリの最適化テクニック
Section titled “クエリの最適化テクニック”1. インデックスの活用
@Entity@Table(name = "users", indexes = { @Index(name = "idx_email", columnList = "email"), @Index(name = "idx_name_email", columnList = "name,email")})public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(name = "email", nullable = false) private String email; // インデックスが設定される
@Column(name = "name", nullable = false) private String name;}
// 複合インデックスを使用したクエリ@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { // インデックスを活用した検索 List<User> findByEmail(String email); // idx_emailを使用 List<User> findByNameAndEmail(String name, String email); // idx_name_emailを使用}2. 必要なカラムのみ取得(プロジェクション)
// 悪い例: すべてのカラムを取得List<User> users = userRepository.findAll();for (User user : users) { System.out.println(user.getName()); // nameだけ使用しているのに、全カラムを取得}
// 良い例: 必要なカラムのみ取得public interface UserNameOnly { String getName();}
@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { List<UserNameOnly> findAllProjectedBy();
// または、@Queryを使用 @Query("SELECT u.name FROM User u") List<String> findAllNames();}3. バッチ処理の最適化
@Service@Transactionalpublic class UserService { @Autowired private UserRepository userRepository;
public void saveUsersInBatch(List<User> users) { int batchSize = 50; for (int i = 0; i < users.size(); i += batchSize) { List<User> batch = users.subList(i, Math.min(i + batchSize, users.size())); userRepository.saveAll(batch); userRepository.flush(); // バッチごとにフラッシュ userRepository.clear(); // エンティティマネージャーをクリア } }}
// application.propertiesspring.jpa.properties.hibernate.jdbc.batch_size=50spring.jpa.properties.hibernate.order_inserts=truespring.jpa.properties.hibernate.order_updates=true4. ページングの活用
@Servicepublic class UserService { @Autowired private UserRepository userRepository;
// 悪い例: すべてのデータを取得 public List<User> getAllUsers() { return userRepository.findAll(); // 10万件のデータを一度に取得 }
// 良い例: ページングを使用 public Page<User> getUsers(int page, int size) { Pageable pageable = PageRequest.of(page, size); return userRepository.findAll(pageable); // 必要な分だけ取得 }}2. N+1問題の解決
Section titled “2. N+1問題の解決”// ログでN+1問題を確認// 以下のようなログが大量に出力される場合、N+1問題が発生している可能性が高い// SELECT * FROM users// SELECT * FROM orders WHERE user_id = 1// SELECT * FROM orders WHERE user_id = 2// SELECT * FROM orders WHERE user_id = 3// ...1. JOIN FETCHの使用
@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { // JOIN FETCHで一度に取得 @Query("SELECT DISTINCT u FROM User u JOIN FETCH u.orders") List<User> findAllWithOrders();
// 条件付きJOIN FETCH @Query("SELECT u FROM User u JOIN FETCH u.orders o WHERE o.status = :status") List<User> findUsersWithOrdersByStatus(@Param("status") OrderStatus status);}2. @EntityGraphの使用
@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { @EntityGraph(attributePaths = {"orders"}) List<User> findAll();
@EntityGraph(attributePaths = {"orders", "orders.orderItems"}) Optional<User> findById(Long id);}3. バッチサイズの設定
@Entitypublic class User { @OneToMany(fetch = FetchType.LAZY) @BatchSize(size = 50) private List<Order> orders;}3. キャッシングの活用
Section titled “3. キャッシングの活用”Spring Cacheの設定
Section titled “Spring Cacheの設定”@Configuration@EnableCachingpublic class CacheConfig {
@Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); }}キャッシュの使用
Section titled “キャッシュの使用”@Servicepublic class UserService { @Autowired private UserRepository userRepository;
// キャッシュを使用 @Cacheable(value = "users", key = "#id") public User findById(Long id) { return userRepository.findById(id).orElseThrow(); }
// キャッシュを更新 @CachePut(value = "users", key = "#user.id") public User updateUser(User user) { return userRepository.save(user); }
// キャッシュを削除 @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { userRepository.deleteById(id); }
// すべてのキャッシュを削除 @CacheEvict(value = "users", allEntries = true) public void clearAllCache() { // キャッシュをクリア }}4. コネクションプールの最適化
Section titled “4. コネクションプールの最適化”HikariCPの設定
Section titled “HikariCPの設定”spring.datasource.hikari.maximum-pool-size=20spring.datasource.hikari.minimum-idle=5spring.datasource.hikari.connection-timeout=30000spring.datasource.hikari.idle-timeout=600000spring.datasource.hikari.max-lifetime=1800000spring.datasource.hikari.leak-detection-threshold=60000プールサイズの計算
Section titled “プールサイズの計算”// 推奨されるプールサイズの計算式// connections = ((core_count * 2) + effective_spindle_count)
// 例: 4コアCPU、1つのディスクの場合// connections = (4 * 2) + 1 = 9// ただし、実際の負荷に応じて調整が必要5. 非同期処理の活用
Section titled “5. 非同期処理の活用”@Asyncの設定
Section titled “@Asyncの設定”@Configuration@EnableAsyncpublic class AsyncConfig {
@Bean(name = "taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }}非同期処理の実装
Section titled “非同期処理の実装”@Servicepublic class EmailService {
@Async("taskExecutor") public CompletableFuture<Void> sendEmail(String to, String subject, String body) { // メール送信処理(時間がかかる) try { Thread.sleep(1000); // メール送信のシミュレーション System.out.println("Email sent to: " + to); return CompletableFuture.completedFuture(null); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return CompletableFuture.failedFuture(e); } }}
@Servicepublic class UserService { @Autowired private EmailService emailService;
@Transactional public User createUser(UserCreateRequest request) { User user = userRepository.save(convertToEntity(request));
// 非同期でメール送信(ブロックしない) emailService.sendEmail(user.getEmail(), "Welcome", "Welcome message");
return user; // メール送信を待たずに返す }}6. メモリの最適化
Section titled “6. メモリの最適化”不要なデータの削除
Section titled “不要なデータの削除”@Servicepublic class UserService { @Autowired private UserRepository userRepository;
// 悪い例: すべてのデータをメモリに読み込む public void processAllUsers() { List<User> users = userRepository.findAll(); // 10万件をメモリに読み込む for (User user : users) { processUser(user); } }
// 良い例: ストリーム処理でメモリ効率を向上 public void processAllUsers() { int pageSize = 100; int page = 0; Page<User> userPage;
do { Pageable pageable = PageRequest.of(page, pageSize); userPage = userRepository.findAll(pageable);
for (User user : userPage.getContent()) { processUser(user); }
page++; } while (userPage.hasNext()); }}ガベージコレクションの最適化
Section titled “ガベージコレクションの最適化”# JVMオプションの設定java -Xms2g -Xmx4g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:+PrintGCDetails \ -XX:+PrintGCTimeStamps \ -jar myapp.jar7. ロギングの最適化
Section titled “7. ロギングの最適化”ログレベルの調整
Section titled “ログレベルの調整”# 本番環境では、不要なログを無効化logging.level.root=INFOlogging.level.com.example.myapp=INFOlogging.level.org.springframework.web=WARNlogging.level.org.hibernate.SQL=WARN # SQLログは本番では無効化非同期ロギング
Section titled “非同期ロギング”<configuration> <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>512</queueSize> <discardingThreshold>0</discardingThreshold> <appender-ref ref="FILE"/> </appender>
<root level="INFO"> <appender-ref ref="ASYNC_FILE"/> </root></configuration>8. プロファイリングツールの使用
Section titled “8. プロファイリングツールの使用”JProfilerの使用
Section titled “JProfilerの使用”// プロファイリングポイントの設定@Servicepublic class UserService { public User findById(Long id) { // プロファイリング開始 long startTime = System.currentTimeMillis();
User user = userRepository.findById(id).orElseThrow();
// プロファイリング終了 long duration = System.currentTimeMillis() - startTime; if (duration > 100) { log.warn("Slow query detected: findById took {}ms", duration); }
return user; }}Spring Boot Actuatorの使用
Section titled “Spring Boot Actuatorの使用”<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency>management.endpoints.web.exposure.include=health,metrics,prometheusmanagement.metrics.export.prometheus.enabled=true9. パフォーマンステスト
Section titled “9. パフォーマンステスト”負荷テストの実施
Section titled “負荷テストの実施”@Testpublic void testUserServicePerformance() { // 1000件のユーザーを作成 List<User> users = createTestUsers(1000);
long startTime = System.currentTimeMillis();
for (User user : users) { userService.findById(user.getId()); }
long duration = System.currentTimeMillis() - startTime;
// 1000件の取得が1秒以内に完了すること assertThat(duration).isLessThan(1000);
// 1件あたりの平均時間 double averageTime = (double) duration / users.size(); System.out.println("Average time per user: " + averageTime + "ms");}10. チェックリスト
Section titled “10. チェックリスト”パフォーマンスチューニングのチェックリスト:
データベース関連
Section titled “データベース関連”- 適切なインデックスが設定されているか
- N+1問題が発生していないか
- 必要なカラムのみ取得しているか
- バッチ処理が最適化されているか
- ページングが適切に使用されているか
- コネクションプールサイズが適切か
キャッシング関連
Section titled “キャッシング関連”- 頻繁にアクセスされるデータがキャッシュされているか
- キャッシュの有効期限が適切に設定されているか
- キャッシュの更新戦略が適切か
非同期処理関連
Section titled “非同期処理関連”- 時間のかかる処理が非同期化されているか
- スレッドプールサイズが適切か
- メモリリークが発生していないか
- 不要なデータがメモリに残っていないか
- ガベージコレクションが適切に動作しているか
パフォーマンスチューニングのポイント:
- 問題の特定: プロファイリングツールを使用してボトルネックを特定
- データベース最適化: インデックス、JOIN FETCH、バッチ処理
- キャッシング: 頻繁にアクセスされるデータをキャッシュ
- 非同期処理: 時間のかかる処理を非同期化
- メモリ最適化: 不要なデータの削除、ストリーム処理
- 継続的な改善: パフォーマンステストを実施し、継続的に改善
これらのテクニックを適切に組み合わせることで、アプリケーションのパフォーマンスを大幅に向上させることができます。