NEXTSTEP - 인프라공방 후기로 3주차 분량이 많아 1, 2부로 나눠서 작성한다.
인프라공방은 직접 인프라를 경험해보는 실습 강의로
AWS 사용, 성능 테스트 및 개선, 모니터링, 스케일 아웃, 쿼리 최적화, MySQL Replication 등을 진행해볼 수 있다.
미션 진행과정을 요약하기 때문에 자세한 내용이 궁금하면 수강을 추천한다.
⏱️안정적인 인프라 만들기 - 학습 목표
🎯 HTTP 개선에 따른 차이를 이해하고 Reverse Proxy 성능 개선을 해봅니다.
🎯 HTTP Cache 전략을 이해하여 적절한 정책을 설정해봅니다.🎯 쿼리를 최적화하여 조회 성능을 개선해봅니다. (2부에서)🎯 인덱스를 설정하여 조회 성능을 개선해봅니다. (2부에서)
🚀1단계 - 화면 응답 개선하기
요구사항
- 부하테스트 각 시나리오의 요청시간을 목푯값 이하로 개선
지난 주 웹 성능 테스트 결과를 바탕으로 화면 응답을 개선하는 미션이다.
개선할 부분은 크게 분류하면 다음과 같다
- 불필요한 다운로드 제거
- 다양한 압축 기술을 통해 각 리소스의 전송 인코딩을 최적화
- 스크립트 병합하여 요청수 최소화
- 웹 프로토콜 최적화
- 기타 등등
결과표를 참고하여 하나씩 개선해나가 보자.
Reverse Proxy 개선
가장 먼저 Reverse Proxy 개선하는 방식으로 gzip, cache, HTTP/2 설정을 추가한다.
## gzip.conf
gzip on; ## http 블록 수준에서 gzip 압축 활성화
gzip_comp_level 9;
gzip_vary on;
gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/rss+xml text/javascript image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype;
## cache.conf
## Proxy 캐시 파일 경로, 메모리상 점유할 크기, 캐시 유지기간, 전체 캐시의 최대 크기 등 설정
proxy_cache_path /tmp/nginx levels=1:2 keys_zone=mycache:10m inactive=10m max_size=200M;
## 캐시를 구분하기 위한 Key 규칙
proxy_cache_key "$scheme$host$request_uri $cookie_user";
server {
location ~* \.(?:css|js|gif|png|jpg|jpeg)$ {
proxy_pass http://app;
## 캐시 설정 적용 및 헤더에 추가
# 캐시 존을 설정 (캐시 이름)
proxy_cache mycache;
# X-Proxy-Cache 헤더에 HIT, MISS, BYPASS와 같은 캐시 적중 상태정보가 설정
add_header X-Proxy-Cache $upstream_cache_status;
# 200 302 코드는 20분간 캐싱
proxy_cache_valid 200 302 10m;
# 만료기간을 1 달로 설정
expires 1M;
# access log 를 찍지 않는다.
access_log off;
}
}
events {}
http {
upstream app {
server 172.17.0.1:8080;
}
server {
listen 80;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/loopstudy.p-e.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/loopstudy.p-e.kr/privkey.pem;
# Disable SSL
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# 통신과정에서 사용할 암호화 알고리즘
ssl_prefer_server_ciphers on;
ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
# Enable HSTS
# client의 browser에게 http로 어떠한 것도 load 하지 말라고 규제합니다.
# 이를 통해 http에서 https로 redirect 되는 request를 minimize 할 수 있습니다.
add_header Strict-Transport-Security "max-age=31536000" always;
# SSL sessions
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://app;
}
}
# gzip
include /etc/nginx/gzip.conf;
# cache
include /etc/nginx/cache.conf;
}
WAS 성능 개선
- Spring Data Cache 적용
Redis를 활용한 Cache 적용으로 docker 이용해서 진행한다.
$ docker pull redis
$ docker run -d -p 6379:6379 redis
application.properties에 설정값을 추가한다.
# redis
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
build.gradle에 redis 의존성을 추가한다.
implementation('org.springframework.boot:spring-boot-starter-data-redis')
Cache를 Redis로 사용해주는 클래스를 만들어준다.
@EnableCaching
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
@Autowired
RedisConnectionFactory connectionFactory;
@Bean
public CacheManager redisCacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder.
fromConnectionFactory(connectionFactory).cacheDefaults(redisCacheConfiguration).build();
return redisCacheManager;
}
}
캐쉬를 적용할 대상을 정한다.
@Cacheable(value = "stations")
public List<Station> findStations() {
return stationRepository.findAll();
}
@CacheEvict(value = "station", key = "#id")
public void deleteStationById(Long id) {
stationRepository.deleteById(id);
}
@Cacheable(value = "station", key = "#id")
public Station findStationById(Long id) {
return stationRepository.findById(id).orElseThrow(RuntimeException::new);
}
PageSpeed에서 테스트를 다시 진행해보면 이전보다 많이 좋아진 결과를 볼 수 있다.
| 사이트 | FCP | TTI | SI | LCP | TBT | CLS |
|-------|--------|--------|--------|--------|----------|---------|
| 개선 전 | 3.0초 | 3.0초 | 3.0초 | 3.0초 | 10 밀리초 | 0.000 |
| 개선 후 | 1.2초 | 1.3초 | 1.8초 | 1.3초 | 70 밀리초 | 0.004 |
| ----- | ------ | ------ | ------ | ------ | -------- | ------- |
| 비교 | -1.8초 | -1.7초 | -1.2초 | -1.7초 | +60 밀리초 | +0.004 |
그 외에도 여러가지 추가 사항이 있지만 생략하고 넘어간다.
🚀2단계 - 스케일 아웃(with ASG)
요구사항
- springboot에 HTTP Cache, gzip 설정하기
- Launch Template 작성하기
- Auto Scaling Group 생성하기
- Smoke, Load, Stress 테스트 후 결과를 기록
1단계 Reverse Proxy에서 설정한 gzip, HTTP cache를 Spring Boot에 적용하면서 스케일 아웃을 진행하는 미션이다.
Spring Boot 개선
스프링 부트는 압축 기능을 제공하는데 gzip을 설정하는 방법은 application.properties에 설정값을 추가하면 된다.
# gzip
server.compression.enabled: true
server.compression.mime-types: text/html,text/plain,text/css,application/javascript,application/json
server.compression.min-response-size: 500
HTTP cache 적용은 간략히 addResourceHandlers만 언급한다
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
public static final String PREFIX_STATIC_RESOURCES = "/resources";
@Autowired
private BlogVersion version;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// css
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/css/**")
.addResourceLocations("classpath:/static/css/")
.setCachePeriod(60 * 60 * 24 * 365);
// js
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/js/**")
.addResourceLocations("classpath:/static/js/")
.setCacheControl(CacheControl.noStore())
.setCacheControl(CacheControl.noCache().cachePrivate());
}
...
}
배포 스크립트 업로드
외부망에 deploy.sh 코드를 Amason S3 버킷에 업로드하면 되는 단순한 작업이다
Launch Template (시작 템플릿) 작성하기
Auto Scaling Group에서 자동으로 생성할 EC2 템플릿을 생성한다.
이전에 진행한 EC2 설정과 비슷하다.
인스턴스 유형, Key pair, 서브넷, 보안 그룹 등 WAS에 적용할 정책을 선택한다.
IAM role은 강사가 생성해둔 ec2-s3-api로 설정한다.
EC2 정상적으로 실행된 후 동작해야 할 명령어를 입력한다. 스크립트 구성에 따라 다양하게 동작하게 된다.
시작 템플릿은 수정이 가능하지만 버전 단위로 생성되어 관리된다.
Auto Scaling Group 생성
생성된 시작 템플릿을 설정한다.
네트워크는 이전에 생성한 VPC, 외부망 서브넷으로 선택한다.
로드 밸런서를 설정한다.
Auto Scaling Group 크기와 임계점을 설정하고 계속 진행하면 생성된다.
종료 정책을 설정한다. 이 정책은 생성 이후에 가능하다.
종료 정책은 오래된 시작 템플릿 버전, 오래된 인스턴스, 기본 순서대로 구성한다.
DNS 설정
Auto Scaling Group 설정 중에 선택한 새로운 로드 밸런서도 생성이 되어있다.
해당 로드 밸런서의 값으로 DNS CNAME을 변경해준다.
기존 IP연결(A)는 사용 안 한다. ALB가 알아서 IP를 연결해준다.
TLS 설정
letsencrypt 인증서를 AWS로 옮기면 된다.
순서대로 cert.pem, privkey.pem, chain.pem 내용을 복붙 하면 된다.
ALB 인증서 적용
가져온 인증서를 ALB에 적용한다. 프로토콜을 HTTPS로 설정해야 TLS 인증서 선택이 가능하다.
부하 테스트 진행
ALB 설정 이후에 이전에 했던 부하 테스트(stress_paths.js)를 실행해본다.
이전과 다르게 에러없이 완료되어서 다른 테스트(stress_stations.js)로 진행한다.
이번에는 시작부터 VUser 800명으로 설정하고 1분마다 100씩 증가시켜 1300명 늘린다.
import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';
export let options = {
stages: [
{ duration: '1m', target: 800 },
{ duration: '1m', target: 900 },
{ duration: '1m', target: 1000 },
{ duration: '1m', target: 1100 },
{ duration: '1m', target: 1200 },
{ duration: '1m', target: 1300 },
{ duration: '10s', target: 0 },
],
thresholds: {
http_req_duration: ['p(99)<150'], // 99% of requests must complete below 0.15s
},
};
const BASE_URL = 'https://loopstudy.kro.kr';
export default function () {
var payload = JSON.stringify({
name: Math.random().toString(36).substring(2, 10)
});
var params = {
headers: {
'Content-Type': 'application/json',
},
};
let stations = http.post(`${BASE_URL}/stations`, payload, params);
check(stations, {
'stations in successfully': (resp) => resp.json('id') !== '',
});
sleep(1);
};
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: stress_paths.js
output: -
scenarios: (100.00%) 1 scenario, 1300 max VUs, 6m40s max duration (incl. graceful stop):
* default: Up to 1300 looping VUs for 6m10s over 7 stages (gracefulRampDown: 30s, gracefulStop: 30s)
WARN[0192] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0192] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0193] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0193] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0193] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0194] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0194] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0194] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0194] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0194] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0195] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0195] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0195] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0195] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0195] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0195] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0195] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0196] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0196] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0196] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0196] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0196] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0196] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0196] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0196] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0197] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0197] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0197] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0197] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0197] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0197] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
WARN[0197] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 52.78.14.53:443: socket: too many open files"
WARN[0197] Request Failed error="Get \"https://loopstudy.kro.kr/paths?source=1&target=3\": dial tcp 3.35.59.64:443: socket: too many open files"
^C
running (3m16.4s), 0000/1300 VUs, 147461 complete and 1027 interrupted iterations
default ✗ [==================>-----------------] 0179/1300 VUs 3m16.4s/6m10.0s
✗ paths in successfully
↳ 99% — ✓ 148449 / ✗ 33
checks.........................: 99.97% ✓ 148449 ✗ 33
data_received..................: 458 MB 2.3 MB/s
data_sent......................: 7.2 MB 36 kB/s
http_req_blocked...............: avg=37.77µs min=0s med=323ns max=31.26ms p(90)=458ns p(95)=529ns
http_req_connecting............: avg=6.69µs min=0s med=0s max=16.96ms p(90)=0s p(95)=0s
✓ http_req_duration..............: avg=3.17ms min=0s med=2.84ms max=55.02ms p(90)=4.32ms p(95)=5.91ms
{ expected_response:true }...: avg=3.17ms min=1.06ms med=2.84ms max=55.02ms p(90)=4.32ms p(95)=5.91ms
http_req_failed................: 0.02% ✓ 33 ✗ 148449
http_req_receiving.............: avg=137.85µs min=0s med=56.51µs max=19.16ms p(90)=208.82µs p(95)=309.98µs
http_req_sending...............: avg=50.74µs min=0s med=32.13µs max=17.33ms p(90)=67.53µs p(95)=119.71µs
http_req_tls_handshaking.......: avg=29.59µs min=0s med=0s max=21.44ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=2.98ms min=0s med=2.69ms max=43.17ms p(90)=4.09ms p(95)=5.41ms
http_reqs......................: 148482 756.093082/s
iteration_duration.............: avg=1s min=1s med=1s max=1.05s p(90)=1s p(95)=1s
iterations.....................: 147461 750.893994/s
vus............................: 1026 min=14 max=1026
vus_max........................: 1300 min=1300 max=1300
1026명에서 에러가 발생했다.
지난 주 테스트에 비교하면 많은 부하를 버티는 걸 알 수 있다.
2단계 미션은 여기서 끝난다.
📃 3주차 1단계 & 2단계 후기
3주차는 전반부 미션은 지난 주 성능 & 부하테스트로 알게 된 문제를 해결하면서 성능 개선하는게 목적이다.
시작은 압축과 캐쉬 사용이다. 가이드에 따라 프록시에 간단히 gzip, cache를 적용하는 모습은 Spring이 아니여도 다른 곳에서 문제해결이 가능하다는 걸 깨닫게 되었다.
그리고 지난 1주차에 망을 구성하면서 도메인을 도입해도 하나의 IP만 할당이 가능해서 외부망이 독립적으로 실행되어 어떻게 여러 서버를 하나로 묶는지 궁금했는데
여러 인스턴스 타겟을 잡아 로드밸런서로 고르게 분배하는 모습을 통해 궁금증이 해소되었다.
진행하면서 임계값 설정을 가이드에 따라 50에 설정하긴 했지만
강사님이 다니고 있는 회사에서는 어떻게 하는지 궁금해서 질문했고 다음과 같은 답변을 받았다.
다른 회사도 궁금해서 톡방 사람들에게 물어봤는데 회사마다 임계점이 달랐다.
그 중 한분은 서버가 수백대지만 만약에 90% 달성하면 올 패스백 걸어서 요청을 무시하게 세팅되어있다고 한다.
이번 3주차는 그 어느 때보다 가장 많은 삽질을 진행했다.
인프라공방에서 제공하는 가이드는 말그대로 힌트이기 때문에 가이드에 없는 요소는 직접 알아보면서 진행한다.
초반에 진행했던 삽질 중에서 시간을 꽤 잡아먹은건 Redis cache 적용이 있다.
무지성으로 조회오는 곳마다 @Cacheable을 적용했더니 순환참조가 발생했다.
처음엔 JPA 때문인줄 알고 구글링했지만 해결이 안되길래 멍~때렸는데
Redis도 순환참조 문제가 발생한다고 하길래 필요하다는 부분만 적용하는걸로 문제 해결이 되었다.
중간에 도메인 loopstudy.p-e.kr가 loopstudy.kro.kr 로 바뀐 이유도 삽질 때문이다.
DNS CNAME 설정하는 것도 CNAME만 하면 되는 줄 알고
TXT(SPF) 내용을 지워버리고 수정했다가 지워버린 인증서 값을 찾아봤지만 백업해두지 않으면 방법이 없다고 한다.
새로운 도메인을 생성하면서 인증서도 처음부터 진행했다.
오토 스케일 그룹 설정에서 로드 밸런서 설정을 잘못 선택해서 날린 시간이 많다.
이미지 하단을 보면 로드 밸런서 체계가 Internal과 Internet-facing 2가지로 분류되는데 Internal을 Internet 인줄 착각해서 계속 잘못 생성했던 것...
결국엔 타겟 그룹을 직접 생성된 인스턴스로 설정하면서 성공하긴했다.
하지만 미션 힌트로 제공해주는 가이드는 ubuntu lts로 내가 구현한 방식과는 다르게 동작하게 되어있었다고 한다.
가이드와 다르지만 미션의 목표를 달성해서 머지해주셨다. 🙈
그 외에도 여러 삽질이 있었다.
* 3주차에서 정리할 내역은 3주차 후기 2부에 적어야겠다.
'교육 및 인강 > 인프라 공방' 카테고리의 다른 글
인프라 공방 4주차 - ♾️ 확장하는 인프라 만들기 후기 (0) | 2022.05.23 |
---|---|
인프라 공방 3주차 - ⏱️안정적인 인프라 만들기 후기 2부 (0) | 2022.05.19 |
인프라 공방 2주차 - 🕵🏻♂️ 성능 진단하기 후기 (0) | 2022.05.16 |
인프라 공방 1주차 - 👨🏻💻그럴듯한 인프라 만들기 후기 (0) | 2022.05.16 |
인프라공방 시작글... (0) | 2022.05.13 |