부하 테스트
게시글 CRUD 기능을 제공하는 게시판 서버가 어느 정도의 트래픽을 처리할 수 있는지 확인하기 위해 부하 테스트 툴인 nGrinder와 APM 툴인 scouter를 이용해 로컬 환경에서 게시글 조회 기능의 성능을 테스트했습니다.
부하 테스트는 미리 20000개의 게시글을 DB에 저장한 뒤, 로컬 PC의 CPU 사용량을 고려하여 vuser를 50으로 설정하고 1분 동안 게시글 id 조회와 게시글 페이지 조회 API를 각각 1~100의 id, 0~9의 페이지 번호로 호출하는 방식으로 수행하였습니다.
1. 게시글 id 조회
1) nGrinder groovy script 수정
@Test
public void test() {
// 랜덤한 페이지 값 생성 (1 이상, 100 이하)
def randomNum = new Random().nextInt(100) + 1
// API 호출 URL 조합
def apiUrl = "http://127.0.0.1/posts/${randomNum}"
HTTPResponse response = request.GET(apiUrl, params)
if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
} else {
assertThat(response.statusCode, is(200))
}
}
2) 부하 테스트 결과
DB에 20000개의 적지 않은 데이터가 저장되었음에도 약 2800 TPS라는 빠른 조회 성능을 확인할 수 있었습니다.
2. 게시글 페이지 조회
1) nGrinder groovy script 수정
@Test
public void test() {
// 랜덤한 페이지 값 생성 (0 이상, 10 미만)
def randomPage = new Random().nextInt(10)
// API 호출 URL 조합
def apiUrl = "http://127.0.0.1/posts?page=${randomPage}"
HTTPResponse response = request.GET(apiUrl, params)
if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
} else {
assertThat(response.statusCode, is(200))
}
}
2) 부하 테스트 결과
평균 TPS가 약 370으로 게시글 id 조회의 부하 테스트 결과에 비해 상대적으로 매우 낮은 성능을 가지고 있음을 알 수 있었습니다.
이는 페이지 조회 시 최신순으로 게시글을 정렬하여 20개의 게시글을 가져오는 과정에서 게시글 작성 날짜 column에 인덱스가 생성되지 않아서 20000개의 데이터를 모두 확인한 뒤 최신순으로 20개의 데이터를 가져오는 과정이 매우 비효율적이기 때문으로 생각됩니다.
따라서 게시글 작성 날짜 column에 인덱스를 생성하고, 페이지 조회시 인덱스를 이용한다면 조회 성능을 크게 개선할 수 있을 것이라 생각할 수 있었습니다.
조회 성능 개선
게시판의 특성상 주로 조회되는 게시글과 페이지가 존재하고, 게시글의 변경사항이 꼭 실시간으로 처리될 필요는 없기 때문에 캐싱을 이용한다면 조회 성능을 크게 개선할 수 있을 것이라 생각했습니다.
따라서 조회 기능에 공통적으로 Ehcache를 이용한 로컬 캐싱을 적용하고 페이지 조회 시 정렬 기준으로 사용되는 게시글 작성 날짜 column에 인덱스를 생성한 뒤, 성능이 얼마나 개선되었는가 확인해보았습니다.
1. 공통
1) Ehcache 적용
로컬 캐시 라이브러리인 Ehcache를 사용하기 위해 build.gradle 파일에 dependency를 추가한 후, 게시글 조회 결과가 10초 동안 캐시되도록 설정하였습니다. 페이지의 경우 주로 조회될 것이라 예상되는 0~9page만 캐시되도록 설정하였습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.ehcache:ehcache:3.10.0'
implementation 'javax.cache:cache-api:1.1.1'
}
@Configuration
@EnableCaching
public class EhcacheConfiguration {
@Bean
public CacheManager getCacheManager() {
CachingProvider provider = Caching.getCachingProvider();
CacheManager cacheManager = provider.getCacheManager();
// 게시글 id 캐시 설정
CacheConfiguration<Long, ResponseDto> cacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, ResponseDto.class,
ResourcePoolsBuilder.heap(1000))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(10)))
.build();
Cache<Long, ResponseDto> postCache = cacheManager.createCache("postCache",
Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfiguration));
// 게시글 페이지 캐시 설정
CacheConfiguration<String, ResponseDto> cacheConfiguration2 = CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, ResponseDto.class,
ResourcePoolsBuilder.heap(1000))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(10)))
.build();
Cache<String, ResponseDto> postsCache = cacheManager.createCache("postsCache",
Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfiguration2));
return cacheManager;
}
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {
private final PostRepository postRepository;
private final PostQueryRespository postQueryRespository;
@Cacheable(value="postCache")
public ResponseDto<? super PostDto> get(Long postId) {
if (!postRepository.existsById(postId)) {
return GetPostResponseDto.notExistPost();
}
Post findPost = postRepository.findById(postId).orElseThrow();
return GetPostResponseDto.success(new PostDto(findPost));
}
@Cacheable(value = "postsCache", key = "#cond.hashCode() + \" \" + #pageable.hashCode()", condition = "#pageable.getPageNumber() < 10")
public ResponseDto<? super List<PostDto>> findPosts(PostSearchCond cond, Pageable pageable) {
List<Post> posts = postQueryRespository.findAll(cond, pageable);
if (posts.isEmpty()) {
return GetPostsResponseDto.notExistPost();
}
Long count = postQueryRespository.countAll(cond);
List<PostDto> postDtos = posts.stream()
.map(PostDto::new)
.collect(Collectors.toList());
return GetPostsResponseDto.success(postDtos, count);
}
}
2. 게시글 id 조회
1) Ehcache 적용 후 부하 테스트 결과
평균 TPS가 2800에서 5200으로 약 85% 증가했음을 확인할 수 있었습니다.
3. 게시글 페이지 조회
1) Ehcache 적용 후 부하 테스트 결과
평균 TPS가 370에서 3900으로 10배 이상 증가했음을 확인할 수 있었습니다.
2) 인덱스 활용 후 부하 테스트 결과
// 게시글 작성 날짜 column인 post_date에 인덱스 생성
create index idx_post_date on post ( post_date );
// post db에 생성된 인덱스 확인
show index from post;
평균 TPS가 3900에서 4600으로 약 18% 증가했음을 확인할 수 있었습니다.
MySQL profiling 기능을 통해 인덱스 활용 전과 후를 비교한 결과, 페이지 조회에 걸린 시간이 0.03720s에서 0.00042s로 크게 감소했음을 확인할 수 있었습니다.
결론
오픈소스 툴을 이용해 부하 테스트와 성능 모니터링을 실습해보면서, 예상치 못했던 문제를 찾고 해결하여 조회 성능을 크게 향상시키는 의미있는 경험을 할 수 있었습니다. 개발을 완료한 후에도 꾸준한 성능 모니터링을 통해 개선점을 찾고 유지보수 하는 것이 중요하다는 것을 체감할 수 있었고, 로컬 캐싱과 인덱싱을 활용해보는 좋은 기회가 되었습니다.
다음으로는 MySQL TEXT 타입인 게시글 제목순으로 정렬하여 페이지를 조회하는 경우 인덱스와 동적 쿼리를 어떻게 생성하고 활용할 수 있을지, DB 커넥션 풀을 어떻게 최적화할 수 있는지에 대해서 공부해야겠다고 느끼게 되었습니다.
'Cloud engineering' 카테고리의 다른 글
ArgoCD 설치 및 설정 (1) | 2024.09.01 |
---|---|
Terraform 이용해 EKS Cluster 구축하기 (0) | 2024.09.01 |
올리브영 온라인 쇼핑몰 public cloud(AWS) 인프라 구축 프로젝트 (3) | 2024.08.31 |
[ Jenkins ] Jenkins를 이용해 빌드/배포 자동화하기 (0) | 2024.02.15 |
[ AWS ] Spring Boot 서버 AWS EC2에 배포하기 (0) | 2024.02.12 |