인프라 공방 2주차 - 🕵🏻♂️ 성능 진단하기 후기
NEXTSTEP - 인프라공방 후기글이다.
인프라공방은 직접 인프라를 경험해보는 실습 강의로
AWS 사용, 성능 테스트 및 개선, 모니터링, 스케일 아웃, 쿼리 최적화, MySQL Replication 등을 진행해볼 수 있다.
미션 진행과정을 요약하기 때문에 자세한 내용이 궁금하면 수강을 추천한다.
인프라 공방
edu.nextstep.camp
🕵🏻♂️ 성능 진단하기- 학습 목표
🎯 USE 방법론을 활용하여 서버를 진단할 수 있고 쓰레드 덤프를 확인해봅니다.
🎯 webpagetest, pagespeed를 활용하여 웹 성능 예산을 고민해봅니다.
🎯 목표치를 정하고 부하테스트를 직접 수행해봅니다.
🚀1단계 - 웹 성능 테스트
[1단계] 웹 성능 테스트 by loop-study · Pull Request #262 · next-step/infra-subway-monitoring
안녕하세요! 박현철입니다. 1단계 진행하여 제출합니다. 확인 부탁드립니다. 감사합니다!
github.com
요구사항
- 웹 성능 예산 작성하기
- 개선이 필요한 부분을 파악하기
2주차 1단계 미션은 WebPageTest, PageSpeed에서 배포한 웹 성능을 테스트하고 개선 사항을 알아본다.
간략히 PageSpeed 테스트 결과 기준으로 설명한다.
성능 테스트를 진행하면 휴대전화와 데스크탑 2가지 결과를 확인할 수 있다.
평가는 전체적인 점수와 각 측정 항목별 반응시간을 나타낸다.
- First Contentful Patin : 첫번째 텍스트 혹은 이미지가 표시되는 시간
- Speed Index : 컨텐츠가 얼마나 빨리 표시되는지 보여준다.
- Largest Contentfull Paint : 최대 텍스트 혹은 이미지가 표시되는 시간
- Time to Interactive : 페이지와 상호작용할 수 있는 시간을 표시
- Total Blocking Time : FCP와 상호작용 시간 사이의 모든 시간의 합
- Cumulative Layout Shift : 표시 영역 안에 보이는 요소의 이동을 측정
페이지 성능은 우선 기본적으로 가장 많이 방문하는 페이지나 트래픽이 많이 발생할 것으로 예상되는 페이지 등
중요하다는 페이지로 웹 성능 테스트를 진행하면 된다.
또한 경쟁사 사이트도 테스트하여 성능 차이를 비교해보면 좋다.
| 사이트 | FCP | TTI | SI | LCP | TBT | CLS |
|----------------|------|------|------|------|--------|-------|
| Infra-Subway | 3.0초 | 3.0초 | 3.0초 | 3.0초 | 10 밀리초 | 0.000 |
| 서울교통공사 | 1.6초 | 2.2초 | 4.1초 | 3.5초 | 250밀리초 | 0.013 |
| 네이버맵 | 0.5초 | 0.5초 | 2.1초 | 1.6초 | 0밀리초 | 0.006 |
| 카카오맵 | 1.2초 | 1.4초 | 2.9초 | 1.2초 | 10밀리초 | 0.046 |
각 항목별로 평가가 나오고 나서 페이지를 내려보면 개선해야 할 점도 알려준다.
이를 참고하여 무엇을 개선해야할지 목록을 작성하면 미션은 끝난다.
🚀2단계 - 부하테스트
[2주차] 2단계 & 3단계 제출 by loop-study · Pull Request #272 · next-step/infra-subway-monitoring
안녕하세요! 박현철입니다. 삽질을 하다보니 이제야 2단계 & 3단계 동시 제출합니다. 부하테스트 스크립트와 결과는 스크립트 폴더 확인 부탁드립니다. 진행하면서 궁금사항이 있습니다. 2단계
github.com
요구사항
- 테스트 전체조건 정리
- 대상 시스템 범위
- 목푯값 설정(latency, throughput, 부하 유지기간)
- 부하 테스트 시 저장될 데이터 건수 및 크기
- 각 시나리오에 맞춰 스크립트 작성
- 접속 빈도가 높은 페이지
- 데이터를 갱신하는 페이지
- 데이터를 조회하는데 여러 데이터를 참조하는 페이지
- Smoke, Load, Stress 테스트 후 결과를 기록
이번 단계는 앱에 대한 부하테스트를 진행한다.
테스트는 Smoke, Load, Stress 3가지 형태로 진행한다.
간략히 설명하자면
- Smoke : 최소 부하 상태에서 시스템에 오류가 발생하는지 확인하며, VUser는 1~2로 구성
- Load : 서비스의 평소 트래픽과 최대 트래픽 상황에서 기능이 정상 동작하는지 확인
- Stress : 서비스가 극한의 상황에서 어떻게 작동되는지 확인한다. 에러가 발생할 때까지 VUser를 계속 늘린다.
k6 설치
부하테스트를 진행하기 위해선 k6를 설치한다
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
$ echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
$ sudo apt-get update
$ sudo apt-get install k6
그리고 테스트할 스크립트를 작성한다.
# smoke.js
import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';
export let options = {
vus: 1, // 1 user looping for 1 minute
duration: '10s',
thresholds: {
http_req_duration: ['p(99)<150'], // 99% of requests must complete below 0.15s
},
};
const BASE_URL = 'https://loopstudy.p-e.kr';
const USERNAME = 'test id';
const PASSWORD = 'test password';
export default function () {
var payload = JSON.stringify({
email: USERNAME,
password: PASSWORD,
});
var params = {
headers: {
'Content-Type': 'application/json',
},
};
let loginRes = http.post(`${BASE_URL}/login/token`, payload, params);
check(loginRes, {
'logged in successfully': (resp) => resp.json('accessToken') !== '',
});
let authHeaders = {
headers: {
Authorization: `Bearer ${loginRes.json('accessToken')}`,
},
};
let myObjects = http.get(`${BASE_URL}/members/me`, authHeaders).json();
check(myObjects, { 'retrieved member': (obj) => obj.id != 0 });
sleep(1);
};
스크립트는 js 기반으로 동작한다.
vus는 가상유저를 설정하고 duration에 적힌 시간 동안 가상유저가 반복적으로 시스템을 실행한다.
마지막에 보면 sleep(1);를 선언했는데 스크립트를 실행하는 곳이 관리망이기 때문에
외부와 응답 시간을 고려해서 1초간 sleep 시켰다.
k6 실행 방법은 'k6 run [파일명]'을 입력하면 된다.
$ k6 run smoke.js
작성된 스크립트에 따라 테스트가 진행된다.
결과에서 봐야 할 점은 우선 http_req_duration로 평균, 최소, 최대 시간을 알 수 있고
http_reqs는 앱에 요청한 횟수로 10초동안 20번을 요청했다는 걸 알 수 있다.
iterations은 반복된 횟수로 10번 반복되었다.
스크립트를 다시보면 첫번째로 로그인 요청을 하고 두번째로 로그인한 멤버 정보를 요청한다.
그렇게 iterater 1번에 http_req를 2번 요청하고 10번 반복되어서 20번 요청이 된걸 알 수 있다.
이런 식으로 Smoke, Load, Stress 테스트를 할 페이지를 정해서 실행하고 결과를 첨부하면 이번 미션은 끝난다.
import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';
export let options = {
stages: [
{ duration: '1m', target: 100 }, // simulate ramp-up of traffic from 1 to 100 users over 5 minutes.
{ duration: '1m', target: 200 }, // stay at 100 users for 10 minutes
{ duration: '10s', target: 0 }, // ramp-down to 0 users
],
thresholds: {
http_req_duration: ['p(99)<150'] // 99% of requests must complete below 0.15s
},
};
const BASE_URL = 'https://loopstudy.p-e.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);
};
추가 미션으로 부하테스트 모니터링을 할 수 있게 influx db와 grafana 진행하기도 한다. (테스트 대상인 외부망에 설치한다)
influx db는 8086포트를 grafana는 3000포트를 점유하니 보안그룹에 해당 포트를 연다.
🚀3단계 - 로깅, 모니터링
[2주차] 2단계 & 3단계 제출 by loop-study · Pull Request #272 · next-step/infra-subway-monitoring
안녕하세요! 박현철입니다. 삽질을 하다보니 이제야 2단계 & 3단계 동시 제출합니다. 부하테스트 스크립트와 결과는 스크립트 폴더 확인 부탁드립니다. 진행하면서 궁금사항이 있습니다. 2단계
github.com
요구사항
- 애플리케이션 진단하기 실습을 진행하고 문제되는 코드 수정
- 로그 설정하기
- Application Log 파일로 저장
- Nginx Access Log 설정
- Cloudwatch로 모니터링
- Cloudwatch로 로그 수집
- Cloudwatch로 메트릭 수집
- USE 방법론을 활용하기 용이하도록 대시보드 구성
이번 미션은 다양한 로깅과 이를 수집하여 모니터링 대시보드를 구성한다.
Application Log
애플리케이션 로그는 logback.xml를 활용한다.
<!-- logback.xml -->
<configuration debug="false">
<!--spring boot의 기본 logback base.xml은 그대로 가져간다.-->
<include resource="org/springframework/boot/logging/logback/base.xml" />
<include resource="file-appender.xml" />
<!-- logger name이 file일때 적용할 appender를 등록한다.-->
<logger name="file" level="INFO" >
<appender-ref ref="file" />
</logger>
</configuration>
<!-- file-appender.xml -->
<!-- appender 이름이 file인 consoleAppender를 선언 -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--로깅이 기록될 위치-->
<file>${home}file.log</file>
<!--로깅 파일이 특정 조건을 넘어가면 다른 파일로 만들어 준다.-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${home}file-%d{yyyyMMdd}-%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>15MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 해당 로깅의 패턴을 설정 -->
<encoder>
<charset>utf8</charset>
<Pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n
</Pattern>
</encoder>
</appender>
logback.xml 세팅이 끝났으면 log가 필요한 곳에 적용한다.
@RestController
public class MemberController {
private static final Logger fileLogger = LoggerFactory.getLogger("file");
private MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@PostMapping("/members")
public ResponseEntity createMember(@RequestBody MemberRequest request) {
fileLogger.info("[회원가입 요청] 이메일 : {}, 나이 : {}", request.getEmail(), request.getAge());
MemberResponse member = memberService.createMember(request);
fileLogger.info("[회원가입 성공] 회원 ID : {}, 이메일 : {}", member.getId(), member.getEmail());
return ResponseEntity.created(URI.create("/members/" + member.getId())).build();
}
...
}
JSON 형태의 로그를 적용하는 방법도 있다.
우선 gradle에 디펜던시를 하나 추가하고 json-appender.xml를 생성한다.
implementation("net.logstash.logback:logstash-logback-encoder:6.1")
<!-- appender 이름이 json인 consoleAppender를 선언 -->
<appender name="json" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--로깅이 기록될 위치-->
<file>${home}json.log</file>
<!--로깅 파일이 특정 조건을 넘어가면 다른 파일로 만들어 준다.-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${home}json-%d{yyyyMMdd}-%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>15MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" >
<includeContext>true</includeContext>
<includeCallerData>true</includeCallerData>
<timestampPattern>yyyy-MM-dd HH:mm:ss.SSS</timestampPattern>
<fieldNames>
<timestamp>timestamp</timestamp>
<thread>thread</thread>
<message>message</message>
<stackTrace>exception</stackTrace>
<mdc>context</mdc>
</fieldNames>
</encoder>
</appender>
logback.xml에 json-append를 추가해준다.
<!-- logger name이 json일때 적용할 appender를 등록한다.-->
<logger name="json" level="INFO" >
<appender-ref ref="json" />
</logger>
원하는 곳에 json 로그를 적용한다.
@Service
@Transactional
public class MapService {
private static final Logger json = LoggerFactory.getLogger("json");
...
public PathResponse findPath(Long source, Long target) {
List<Line> lines = lineService.findLines();
Station sourceStation = stationService.findById(source);
Station targetStation = stationService.findById(target);
SubwayPath subwayPath = pathService.findPath(lines, sourceStation, targetStation);
json.info("{}, {}",
kv("출발지", sourceStation.getName()),
kv("도착지", targetStation.getName())
);
return PathResponseAssembler.assemble(subwayPath);
}
}
애플리케이션을 실행해보고 파일에 로그가 찍히는지 확인한다.
Nginx Log
docker를 실행할 때 volumne 옵션을 이용하면 간단히 로그를 적용할 수 있다.
$ docker run -d -p 80:80 -v /var/log/nginx:/var/log/nginx nextstep/reverse-proxy
Cloudwatch
로그를 수집할 인스턴스의 IAM role 설정 변경이 필요하다.
IAM 역할은 인프라공방에서 제공해주는 ec2-cloudwatch-api를 사용한다.
IAM role이 변경된 인스턴스에 접속해서 cloudwatch logs agent 설치를 진행한다.
$ curl https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py -O
$ sudo python ./awslogs-agent-setup.py --region ap-northeast-2
진행하다 보면 step 3 단계에서 Access Key, Secret Key 등 입력이 나오는데 IAM Role 설정으로 충분하니 스킵하고 진행하면 된다.
설치가 되었으면 다음 명령어로 로그가 쌓일 log_group_name 값을 변경해야 한다.
$ sudo vi /var/awslogs/etc/awslogs.conf
[/var/log/syslog]
datetime_format = %b %d %H:%M:%S
file = /var/log/syslog
buffer_duration = 5000
log_stream_name = {instance_id}
initial_position = start_of_file
log_group_name = loop-study
[/var/log/nginx/access.log]
datetime_format = %d/%b/%Y:%H:%M:%S %z
file = /var/log/nginx/access.log
buffer_duration = 5000
log_stream_name = access.log
initial_position = end_of_file
log_group_name = loop-study
[/var/log/nginx/error.log]
datetime_format = %Y/%m/%d %H:%M:%S
file = /var/log/nginx/error.log
buffer_duration = 5000
log_stream_name = error.log
initial_position = end_of_file
log_group_name = loop-study
syslog, access, error 3가지 모두 변경해주고 awslogs를 재시작한다.
$ sudo service awslogs restart
EC2 Metric 수집
인스턴스의 Metric 수집을 하기 위해 다음 명령어를 실행한다.
$ wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
$ sudo dpkg -i -E ./amazon-cloudwatch-agent.deb
이제 생성된 폴더에 config.json 파일을 생성해준다.
# /opt/aws/amazon-cloudwatch-agent/bin/config.json
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "root"
},
"metrics": {
"metrics_collected": {
"disk": {
"measurement": [
"used_percent",
"used",
"total"
],
"metrics_collection_interval": 60,
"resources": [
"*"
]
},
"mem": {
"measurement": [
"mem_used_percent",
"mem_total",
"mem_used"
],
"metrics_collection_interval": 60
}
}
}
}
생성한 config.json를 적용시킨다.
# 생성한 config로 적용시킨다.
$ sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json
# 상태를 확인할 수 있다.
$ sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -m ec2 -a status
Spring Actuator Metric
애플리케이션으로 Metric을 수집할 수 있다.
application.properties와 build.gralde에 다음과 같은 세팅을 추가한다.
# application.properties
cloud.aws.stack.auto=false # 실행시 AWS stack autoconfiguration 수행과정에서 발생하는 에러 방지
cloud.aws.region.static=ap-northeast-2
management.metrics.export.cloudwatch.namespace=loopstudy # 해당 namespace로 Cloudwatch 메트릭을 조회 가능
management.metrics.export.cloudwatch.batch-size=20
management.endpoints.web.exposure.include=*
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.1.RELEASE")
implementation("io.micrometer:micrometer-registry-cloudwatch")
}
AWS Cloudwatch 대시보드 설정
Metric 수집이 끝났으면 수집된 데이터를 바탕으로 Cloudwatch 대시보드를 구성하면 된다.
처음엔 아무것도 없는 빈 화면이다. 이 빈 화면에 위젯을 하나씩 추가해보자.
추가할 위젯 유형은 행이며, 지표를 선택한다
그럼 지표 그래프가 나타나고 Custom namespaces 같이 수집된 로그들이 나타난다.
loopstudy는 Spring에서 수집된 Metric이다. 검색을 통해 원하는 지표를 찾을 수도 있다.
CPU Utilization, Network In / Out, mem_used_percent, disk_used_percent 등을 찾아 하나씩 위젯을 추가한다.
위젯이 추가되었으면 대시보드 저장을 꼭 눌러야 한다. 안 그러면 대시보드 위젯을 처음부터 추가해야 하는 불상사가 발생한다.
대시보드가 완성되었으면 2주차 미션은 여기서 마무리된다.
2주차 후기
2주차는 백엔드 개발자에게 중요하다고 느껴지는 주차였다.
테스트라고 한다면 단순히 JUnit를 활용한 테스트 위주밖에 몰랐는데
k6, grafana, cloudwatch 같은 다양한 도구를 경험해보고 현재 서비스가 얼마나 버틸 수 있는지, 각 페이지마다 성능은 어떻게 되는지 등등
다양한 방법으로 성능을 테스트하고 여러 지표를 통해 대시보드를 구성하여 모니터링을 완성하고 매우 보람찬 시간이였다.
지난 1주차처럼 미션이 단순해 보일 수 있는데 2주차는 1주차보다 더 많은 삽질을 했다.
우선 부하테스트를 통해 서버가 다운된적이 있는데 다운되고나서 docker나 gradle 배포가 실행이 아예 안되거나
외부망A에 접속하는데도 말도 안되게 느려지고 시간이 걸리다보니 외부망A를 버리고 외부망B에서 다시 세팅하고 미션을 진행한 적도 있었다.
원인은 단순했다. 부하테스트를 통해서 log 파일 용량이 서버 한계까지 도달해버린것...
문제가 된 log 파일을 찾아서 삭제해주고 인스턴스 재부팅으로 문제는 해결이 되었다.
Cloudwatch 대시보드 저장을 안해서 다시 위젯을 세팅한적도 있다.
그 외에도 다양한 삽질을 했지만 생략한다.
2주차에서 정리할 내역
2주차는 개념도 있지만 주로 도구에 관련된 정리할 내용이 많다.
- 성능 기준, 성능 예산 등
- 부하테스트 관련된 개념(가용성, TPS 등)
- 부하테스트로 인한 서버 상태 진단하는 방법(top, vmstat, top 등)과 다양한 지표 설명
- k6, influx db, grafana
- Logback, cloudwatch, Metric 등
각 도구는 자세히 알아보는 것보단 간단히 진행할 거 같다.