로그를 살펴 보던 중, 글을 등록, 수정, 삭제 할때 마다 Principal을 통해 인증 정보를 가져오기 위해 query가 발생하는 것을 확인할 수 있었다.
수정 빈도가 매우 적은 user 정보를 매번 db에 요청하는 것은 불필요한 부하라는 생각이 들어 user 정보를 캐싱하고 성능 개선이 어느정도 되었는지를 부하 테스트 해보기로 하였다.
캐싱 전 테스트
테스트 종류:
1) Smoke 테스트
VUser: 1 ~ 2
최소의 부하로 시나리오를 검증한다.
2) Load 테스트
평소 트래픽과 최대 트래픽일 때 VUser를 계산 후 시나리오를 검증한다.
3) Stress 테스트
최대 사용자 혹은 최대 처리량인 경우의 한계점을 확인하는 테스트이다.
점진적으로 부하를 증가시켜본다.
테스트 이후 시스템이 수동 개입 없이 자동 복구되는지 확인해본다.
테스트 종합 목표:
1. 최대 latency는 300ms 이하여야 한다.
2. 최대 동시 접속자 수는 200명이여야 한다.
시나리오 수립
게시글 목록 조회 - 게시글 작성 - 해당 게시글 조회
스크립트 작성
@RunWith(GrinderRunner)
class Test {
public static GTest test1
public static GTest test2
public static GTest test3
public static HTTPRequest request
public Object cookies = []
@BeforeProcess
public static void beforeProcess() {
HTTPPluginControl.getConnectionDefaults().timeout = 6000
test1 = new GTest(1, "127.0.0.1")
test2 = new GTest(2, "127.0.0.1")
test3 = new GTest(3, "127.0.0.1")
request = new HTTPRequest()
}
@BeforeThread
public void beforeThread() {
test1.record(this, "test1")
test2.record(this, "test2")
test3.record(this, "test3")
grinder.statistics.delayReports=true;
// reset to the all cookies
def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
cookies = CookieModule.listAllCookies(threadContext)
cookies.each {
CookieModule.removeCookie(it, threadContext)
}
// do login & save to the login info in cookies
NVPair[] params = [new NVPair("username", "swkang"), new NVPair("password", "asdf1234")];
HTTPResponse res = request.POST("http://127.0.0.1:8080/login", params);
cookies = CookieModule.listAllCookies(threadContext)
}
@Before
public void before() {
grinder.sleep(1000)
// set cookies for login state
def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
cookies.each {
CookieModule.addCookie(it ,threadContext)
grinder.logger.info("{}", it)
}
}
@Test
public void test1(){
long startTime = System.currentTimeMillis()
HTTPResponse result = request.GET("http://127.0.0.1:8080/articles")
long endTime = System.currentTimeMillis()
long responseTime = endTime-startTime
long maxResponseTime = 300
if (responseTime > maxResponseTime) {
grinder.logger.error("responseTime exceeded the threshold")
fail("responseTime exceeded the threshold")
}
}
@Test
public void test2(){
long startTime = System.currentTimeMillis()
NVPair[] params = [new NVPair("title", "title01"), new NVPair("content", "content01"), new NVPair("category", "category01")];
HTTPResponse result = request.POST("http://127.0.0.1:8080/articles/form", params);
long endTime = System.currentTimeMillis()
long responseTime = endTime-startTime
long maxResponseTime = 300
if (responseTime > maxResponseTime) {
grinder.logger.error("responseTime exceeded the threshold")
fail("responseTime exceeded the threshold")
}
}
@Test
public void test3(){
long startTime = System.currentTimeMillis()
HTTPResponse result = request.GET("http://127.0.0.1:8080/articles/1")
long endTime = System.currentTimeMillis()
long responseTime = endTime-startTime
long maxResponseTime = 300
if (responseTime > maxResponseTime) {
grinder.logger.error("responseTime exceeded the threshold")
fail("responseTime exceeded the threshold")
}
}
}
reponseTime이 300ms 초과이면 오류 발생
분석
peak TPS인 698을 찍고 latency가 급격하게 줄어드는 것을 확인할 수 있었다.
Redis를 활용한 캐싱
@RequiredArgsConstructor
@EnableRedisRepositories
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}") private String redisHost;
@Value("${spring.data.redis.port}") private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory(){
return new LettuceConnectionFactory(redisHost, redisPort);
}
// 가장 많이 조회되고, 값 수정이 적은 user를 캐싱
@Bean
public RedisTemplate<String, UserAccount> userRedisTemplate(RedisConnectionFactory redisConnectionFactory){
// RedisTemplate -> Redis 명령어를 쉽게 작성할 수 있도록 도와주는 클래스
RedisTemplate<String, UserAccount> redisTemplate =new RedisTemplate<>();
// setConnectionFactory -> 작성한 명령어를 Redis 서버에 날리기 위해 서버 정보 설정
redisTemplate.setConnectionFactory(redisConnectionFactory);
// key 직렬화
redisTemplate.setKeySerializer(new StringRedisSerializer());
// value 직렬화
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<UserAccount>(UserAccount.class));
return redisTemplate;
}
}
RedisTemplate는 Operation들을 쉽게 할 수 있는 클래스다. RedisTemplate에 opsForValue() 메소드는 통해 기본적인 GET, SET을 할 수 있는 메소드다. 이외에도 HashSet이나 ZSet 과 같은 여러가지 Command 들이 RedisTemplate 에서 제공하고 있다. 그렇기 때문에 RedisTemplate라는 코드자체는 Redis에 Command를 쉽게 코드를 작성할 수 있게 도와주는 클래스이다.
@Bean
public UserDetailsService userDetailsService(UserAccountRepository userAccountRepository, UserCacheRepository userCacheRepository) {
return username -> userCacheRepository.getUser(username).map(UserAccountDto::from)
.map(BootPrincipal::from).orElseGet(() ->
userAccountRepository
.findById(username)
.map(UserAccountDto::from)
.map(BootPrincipal::from)
.orElseThrow(() -> new UsernameNotFoundException("유저를 찾을 수 없습니다.")));
}
loadByUsername를 캐시를 통해 가져오도록 변경하였다.
캐싱 후 성능 테스트
TPS가 80퍼 가까이 상승하였고, latency도 빨라져 에러 발생률이 급격하게 줄어든 것을 확인할 수 있었다.
'부트모아' 카테고리의 다른 글
[V2] JPA는 sql injection에 무조건 안전한가? (0) | 2024.09.28 |
---|---|
[V2] 부트모아 사이트 XSS 필터링 (1) | 2024.09.28 |
[V2] Spring Security CSRF (1) | 2024.09.28 |
[V2] Vault를 사용한 보안 강화 (1) | 2024.09.28 |
[V3] 부하 테스트 세팅 (2) | 2024.09.28 |