🚀 1단계 - 로깅과 모니터링
로그 설정하기
- Application Log 파일로 저장하기
- 회원가입, 로그인, 최단거리 조회 등의 이벤트에 로깅을 설정
- Nginx Access Log 설정하기
Cloudwatch로 모니터링
- Cloudwatch로 로그 수집하기
- Cloudwatch로 메트릭 수집하기
* 주의점
- Avoid side effects
- logging으로 인해 애플리케이션 기능의 동작에 영향을 미치지 않아야 합니다.
- 예를 들어 logging하는 시점에 NullPointerException이 발생해 프로그램이 정상적으로 동작하지 않는 상황이 발생하면 안됩니다.
- Be concise and descriptive
- 각 Logging에는 데이터와 설명이 모두 포함되어야 합니다.
- Log method arguments and return values
- 메소드의 input과 output을 로그로 남기면 debugger를 사용해 디버깅하지 않아도 됩니다. 특히 debugger를 사용할 수 없는 상황에서는 상당히 유용하게 사용할 수 있습니다.
- 이를 구현하려면 메소드 앞 부분과 뒷 부분에 지저분한 중복 코드가 계속해서 발생하는 상황이 발생하는데 이는 AOP를 통해 해결할 수 있습니다.
- Delete personal information
- 로그에 사용자의 전화번호, 계좌번호, 패스워드, 주소, 전화번호와 같은 개인정보를 남기지 않습니다.
* logging level
Logging Level을 적절하게 나눠 구현하는 것이 신경쓰면서 개발해야 합니다.
- ERROR : 예상하지 못한 심각한 문제가 발생하여 즉시 조사해야 함
- WARN : 로직상 유효성 확인, 예상 가능한 문제로 인한 예외처리 등을 남김, 서비스는 운영될 수 있지만, 주의해야 함
- INFO : 운영에 참고할만한 사항으로, 중요한 비즈니스 프로세스가 완료됨
- DEBUG / TRACE : 개발 단계에서만 사용하고 운영 단계에서는 사용하지 않음
- 즉, DEBUG 레벨로 설정하면 DEBUG 레벨보다 높은 로그 레벨의 메시지가 모두(DEBUG, INFO, WARN, ERROR) 출력됩니다. ERROR 레벨로 설정하면 ERROR 레벨의 로그만 출력되는 방식으로 동작합니다.
B. Application Log
console-appender.xml
<included>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
</included>
file-appender.xml
<included>
<property name="home" value="log/" />
<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>
</included>
json-appender.xml
<included>
<property name="home" value="log/" />
<!-- appender이름이 file인 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>
</included>
logback.xml을 작성합니다.
- logback의 기본 설정 파일은 logback.xml 입니다. logback 라이브러리는 classpath 아래에 위치하는 logback.xml을 기본으로 찾아봅니다.
logback.xml
<configuration debug="false">
<include resource="org/springframework/boot/logging/logback/base.xml" />
<include resource="console-appender.xml" />
<include resource="file-appender.xml" />
<include resource="json-appender.xml" />
<logger name="console" level="TRACE">
<appender-ref ref="console"/>
</logger>
<logger name="file" level="INFO" >
<appender-ref ref="file" />
</logger>
<logger name="json" level="INFO" >
<appender-ref ref="json" />
</logger>
</configuration>
- logger: 실제 로그 기능을 수행하는 객체로 각 Logger마다 Name을 부여하여 사용합니다.
b. logback을 이용하여 logging을 찍어봅니다.
private static final Logger log = LoggerFactory.getLogger(Controller.class);
private static final Logger fileLogger = LoggerFactory.getLogger("file");
...
log.error("An ERROR Message");
fileLogger.info("파일 로깅 입니다.");
C. Nginx Log
- volume 옵션을 활용하여 호스트의 경로와 도커의 경로를 마운트합니다.
$ docker run -d -p 80:80 -v /var/log/nginx:/var/log/nginx nextstep/reverse-proxy
D. 도커 상태 확인하기(cAdvisor 설치하기)
docker run \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:ro \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--volume=/dev/disk/:/dev/disk:ro \
--publish=8080:8080 \
--detach=true \
--name=cadvisor \
google/cadvisor:latest
적용된 내용
Docker로 운영하는 경우 cAdvisor를 활용하여 간단한 모니터링이 가능합니다.
- 호스트 리소스 모니터링에 필요한 디렉토리를 볼륨으로 지정
- 보안을 위해 읽기 전용으로 볼륨 지정
- 포트는 8080으로 오픈
E. Cloudwatch로 수집하기
a. EC2에 IAM role 설정
b. 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
c. 로그 수집
$ 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 = [로그그룹 이름]
[/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 = [로그그룹 이름]
[/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 = [로그그룹 이름]
설정 후 재시작 (was서버와 nginx서버 각각 맞게 설정)
$ sudo service awslogs restart
d. Metric 수집
* EC2 Metric 수집
$ wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
$ sudo dpkg -i -E ./amazon-cloudwatch-agent.deb
위에 파이썬 2.x 가 설치가 되어있지 않으면 파이썬을 설치하고 위에 두번째 라인 다시 입력 후 아래와 같이 진행한다.
계속 물어보는 문구가 나오면 enter를 치면서 가볍게 넘겨주고, Y, N여부에서 N으로 마무리 한다.
/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
}
}
}
}
$ 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
{
"status": "running",
"starttime": "2021-03-20T15:12:07+00:00",
"configstatus": "configured",
"cwoc_status": "stopped",
"cwoc_starttime": "",
"cwoc_configstatus": "not configured",
"version": "1.247347.5b250583"
}
적용된 부분 (결과와 함께)
- 위젯 추가 > 유형으로 행 선택 > 원본데이터로 지표 선택 > CPU Utilization, Network In / Out, mem_used_percent, disk_used_percent 등을 추가
위에는 예시 아래는 셋팅한 화면 맨첫번째 전체 등록보다는 두번째부터 따로 관리해주는것을 권장한다.
* Spring Actuator Metric 수집
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")
}
cloud.aws.stack.auto=false # 로컬에서 실행시 AWS stack autoconfiguration 수행과정에서 발생하는 에러 방지
cloud.aws.region.static=ap-northeast-2
management.metrics.export.cloudwatch.namespace= # 해당 namespace로 Cloudwatch 메트릭을 조회 가능
management.metrics.export.cloudwatch.batch-size=20
management.endpoints.web.exposure.include=*
최종 화면
🚀 2단계 - 성능 테스트
요구사항
- 웹 성능 테스트
- 웹 성능 예산을 작성
- WebPageTest, PageSpeed 등 테스트해보고 개선이 필요한 부분을 파악
- 부하 테스트
- 테스트 전제조건 정리
- 대상 시스템 범위
- 목푯값 설정 (latency, throughput, 부하 유지기간)
- 부하 테스트 시 저장될 데이터 건수 및 크기
- 각 시나리오에 맞춰 스크립트 작성
- 접속 빈도가 높은 페이지
- 데이터를 갱신하는 페이지
- 데이터를 조회하는데 여러 데이터를 참조하는 페이지
- Smoke, Load, Stress 테스트 후 결과를 기록
- 테스트 전제조건 정리
진행 순서
* 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 Test
$ k6 run smoke.js
# 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)<1500'], // 99% of requests must complete below 1.5s
},
};
const BASE_URL = '[Target URL]';
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);
};
export let options = {
stages: [
{ duration: '1m', target: 500 }, // simulate ramp-up of traffic from 1 to 100 users over 5 minutes.
{ duration: '2m', target: 500 }, // stay at 100 users for 10 minutes
{ duration: '10s', target: 0 }, // ramp-down to 0 users
],
thresholds: {
http_req_duration: ['p(99)<1500'], // 99% of requests must complete below 1.5s
'logged in successfully': ['p(99)<1500'], // 99% of requests must complete below 1.5s
},
};
2단계를 통해 느낀점
- 경쟁사 분석(similarweb)을 통해 평균 방문 수를 파악 할 수 있었다.
- k6를 통해 smoke, load, stress 별로 최대 몇 명까지 부하를 견딜 수 있는지 산정 할 수 있었다.
'오늘의 일상 > 회고록' 카테고리의 다른 글
[우아한테크캠프 pro 2기] 8주차 안정적인 서비스 만들기 (0) | 2021.07.12 |
---|---|
[우아한테크캠프 pro 2기] 4주차 미션 그럴듯한 서비스 후기, 코드리뷰 (0) | 2021.06.26 |
[우아한테크캠프 pro 2기] 3주차 미션 ATDD 후기, 코드리뷰 (0) | 2021.06.22 |
[우아한테크캠프 pro 2기] 2주차 미션 JPA 후기, 코드리뷰 (0) | 2021.06.15 |
[우아한테크캠프 pro 2기] 1주차 미션 로또(Lotto) 후기, 코드리뷰 (0) | 2021.05.30 |