-
서버 진단하기 ( + 로깅, 모니터링)네트워크 & 인프라 2022. 4. 14. 12:41
이번엔 서버를 진단하는 방법을 공부하였다.
서비스의 상태 진단
서버는 크게 CPU, Network Interface Card(NIC), RAM, Disk Drives 4가지로 구성되어있는데 각 자원들은 여유 또는 포화 상태를 가지게 된다. 획득할 수 있는 상태정보는 다음과 같다.
- 요약 : 단위시간 정보의 합계나 평균을 보여줌 (대략정인 상태만 파악) -> sar, vmstat
- 이벤트 기록 : 순차적으로 이벤트 기록 -> 패킷, 시스템콜
- 스냅샷 : 순간의 상태를 기록 (문제 발생 시 원인 조사에 사용됨) -> top
이벤트 기록은 장애상황에서 사용하기에 데이터가 방대하기때문에 문제상황 재현시에 사용된다. 즉 문제 발생시 로그, 요약 정보의 일정 수치등에 알람을 설정해두고 스냅샷을 통해 원인을 파악 하도록 한다.
USE 방법론
넷플릭스의 퍼포먼스 엔지니어가 만든 분석기법으로 각 하드웨어 리소스에서 에러가 있는지(Error), 사용률이 높은지(Utilization), 리소스가 포화되어 더이상 못쓰는 상태인지(Saturation) 등을 보고 각 리소스의 문제를 배제해나가는 방식이다. 이를 통해 모니터링을 보다 체계화 할 수 있다.
USE 방법론 ① 로그
에러 로그를 남긴다면 관리자가 지정한 문제 발생시 로그만 확인하여도 원인과 해결방안을 파악하기가 용이해진다. 에러가 발생할 경우 다음의 사항들을 파악하도록 한다.
- 직전 배포가 연관이 있는지
- 특정 사용자, 특정시간, 특정조건에 따른 문제인지
- 어떤 맥락에서 발생하는 에러인지
로그는 시스템 로그(/var/log/syslog, cron, dmesg 등)와 어플리케이션 로그가 있다. 로깅을 할 때 주의해야 할 점은 다음과 같다.
- side effects 주의 : 로깅으로 인해 앱의 기능에 영향을 미치면 안됨
- 설명 구체화 : 로깅에 데이터와 설명이 모두 포함되어 충분한 설명의도를 가지도록 해야함
- log method : 메서드의 input, output 로그를 남길 수 있는데 이때 중복부분은 AOP를 통해 해결
- 개인정보 노출 주의 : 로그에 사용자의 개인정보는 남기지 않음
로깅 레벨은 다음과 같이 설정한다. 해당 레벨을 설정하면 해당레벨 부터 최상위 레벨까지의 로그를 확인할 수 있다.
- ERROR : 예상하지 못한 심각한 문제 발생, 즉시 조치 필요
- WARN : 로직상 유효성 확인, 예상 가능한 문제로 인한 예외처리 등을 남김, 서비스는 운영될 수 있지만 주의
- INFO : 운영에 참고할만한 사항으로 중요한 비즈니스 프로세스가 완료됨
- DEBUG / TRACE : 개발 단계에서만 사용하고 운영 단계에서는 사용되지 않음
1) 로깅
이제 로그를 남기도록 해보자. 먼저 다음의 dependency를 추가해준다.
// log implementation("net.logstash.logback:logstash-logback-encoder:6.1")
로그설정의 기본이 되는 logback.xml 파일을 작성해준다.
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!--spring boot의 기본 logback base.xml은 그대로 가져간다.--> <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일때 적용할 appender를 등록한다.--> <logger name="console" level="TRACE" > <appender-ref ref="console" /> </logger> <!-- logger name이 file일때 적용할 appender를 등록한다.--> <logger name="file" level="INFO" > <appender-ref ref="file" /> </logger> <!-- logger name이 json일때 적용할 appender를 등록한다.--> <logger name="json" level="INFO" > <appender-ref ref="json" /> </logger> </configuration>
여기서 각각 logger name이 console, file, json 타입일 경우 사용할 xml 설정을 추가해준다.
// console-appender.xml <?xml version="1.0" encoding="UTF-8"?> <included> <!-- appender이름이 console인 consoleAppender를 선언 --> <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 <?xml version="1.0" encoding="UTF-8"?> <included> <property name="home" value="log/" /> <!-- 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> </included> // json-appender.xml <?xml version="1.0" encoding="UTF-8"?> <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을 이용하여 로깅해보자. 여기서 주의할 점은 유효하지 않은 토큰으로 인한 로그인 실패는 예상가능한 예외이기 때문에 warn 레벨을 사용한다는 것이다.
import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Service @Transactional public class AuthService { private static final Logger logger = LoggerFactory.getLogger(Service.class); // ... public TokenResponse login(TokenRequest request) { logger.info("로그인 요청, email:{}", request.getEmail(), request.getPassword()); Member member = memberRepository.findByEmail(request.getEmail()).orElseThrow(AuthorizationException::new); member.checkPassword(request.getPassword()); logger.info("로그인 성공, email:{}", request.getEmail()); String token = jwtTokenProvider.createToken(request.getEmail()); return new TokenResponse(token); } public LoginMember findMemberByToken(String credentials) { if (!jwtTokenProvider.validateToken(credentials)) { logger.warn("토큰 인증 실패 : 유효하지 않은 토큰입니다."); throw new AuthorizationException(); } String email = jwtTokenProvider.getPayload(credentials); Member member = memberRepository.findByEmail(email).orElseThrow(RuntimeException::new); return new LoginMember(member.getId(), member.getEmail(), member.getAge()); } }
json을 이용한 로그를 남겨보자. 여기서 StructuredArguments를 이용하여 키-값의 관계로 값을 넣어줄 수 있다.
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.logstash.logback.argument.StructuredArguments; @Service @Transactional public class MapService { private static final Logger jsonLogger = 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); jsonLogger.info("경로찾기 요청 {} -> {}", StructuredArguments.kv("출발역", sourceStation.getName()), StructuredArguments.kv("도착역", targetStation.getName())); SubwayPath subwayPath = pathService.findPath(lines, sourceStation, targetStation); jsonLogger.info("경로찾기 성공 {} -> {}", StructuredArguments.kv("출발역", sourceStation.getName()), StructuredArguments.kv("도착역", targetStation.getName())); return PathResponseAssembler.assemble(subwayPath); } }
각각 작성된 로그 파일은 프로젝트의 log 폴더안에 json.log로 작성되는 것을 확인할 수 있다.
2) cAdvisor
서버가 도커로 운영되는 경우에 cAdvisor를 통해서 상태를 확인할 수 있다.
## volume 옵션을 통해 호스트 경로와 도커를 마운트 ## $ docker run -d -p 80:80 -v /var/log/nginx:/var/log/nginx nextstep/reverse-proxy $ docker run -d -it -p 80:80 -p 443:443 -v /var/log/nginx:/var/log/nginx --name proxy nextstep/reverse-proxy:0.0.2 ## 도커 상태 확인 (포트는 8080, 읽기전용으로 호스트 리소스 모니터링에 필요한 디렉토리 볼륨 지정) $ 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
3) Cloudwatch 모니터링
이번엔 AWS의 cloudwatch를 이용하여 모니터링을 진행해보자.
먼저 EC2의 IAM role에 cloudwatch 를 부여해주어야 한다. (작업 -> 보안 -> IAM 역할 수정 -> ec2-cloudwatch-api선택)
그 다음 서버에 cloudwatch logs agent를 설치해준다. 여기서 설치진행 시 다른 것을 입력하여 IAM 역할을 변경하지 말고 그대로 엔터를 쳐준 후 첫번째 설정만 똑같이 따라해준다.
## 서버에 cloudwatch logs agent를 설치 $ curl https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py -O ## Python 없으면 다음 실행 $ sudo apt-get update $ sudo apt-get upgrade $ sudo apt-get install python ## 설치 진행 $ sudo python ./awslogs-agent-setup.py --region ap-northeast-2 [/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/syslog [로그그룹 이름] ## 설정파일 추가 $ 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/syslog [로그그룹 이름] [/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/syslog [로그그룹 이름] [/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 = /var/log/syslog [로그그룹 이름] ## json 로그 수집 [/home/ubuntu/infra-subway-deploy/log/json.log] datetime_format = %Y/%m/%d %H:%M:%S file = /home/ubuntu/infra-subway-deploy/log/json.log buffer_duration = 5000 log_stream_name = json.log initial_position = end_of_file log_group_name = /home/ubuntu/infra-subway-deploy/log/json.log ## 변경내용 적용 $ sudo service awslogs restart
이제 ec2 metric을 수집하자.
$ wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb $ sudo dpkg -i -E ./amazon-cloudwatch-agent.deb $ vi /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 } } } } ## Running 상태 확인 $ 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" }
이제 cloudwatch 대시보드를 생성하자.
위젯을 추가하여 유형 행 -> 원본데이터 지표 선택 -> 원하는 지표 선택 하여 대시보드를 구성한다.
여기서 우리는 위의 로그 설정부분에서 json 로그를 추가해주었기 때문에 다음과 같이 로그 이벤트에서 해당 내용을 확인할 수 있다.
4) Spring Actuator Metric
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") } // application-properties에 설정 추가 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=*
USE 방법론 ② 사용률
사용률을 통해서도 문제상황을 인지할 수 있는데 서버의 경우 주로 CPU Utilization, Available memory, RX/TX 패킷량, Dist 사용률, IOPS등을 확인한다.
다음의 스크립트를 통해서 서버의 리소스를 확인할 수 있다
https://github.com/woowacourse/script-practice/blob/master/script/server_resource.sh
#!/bin/bash # color variables txtrst='\e[1;37m' # White txtred='\e[1;31m' # Red txtylw='\e[1;33m' # Yellow txtpur='\e[1;35m' # Purple txtgrn='\e[1;32m' # Green HOSTNAME=`hostname` CPU_MODEL=`cat /proc/cpuinfo | grep "model name" | uniq -c | awk '{print $5 $6, $7,$8, $9, $10 $11}'` SERIAL=`dmidecode -t 1 | grep 'Serial Number' | awk '{print $3}'` KERNEL_INFO=`uname -a | awk '{print $3,$4,$5}'` CORE_COUNT_PER_CPU=`cat /proc/cpuinfo | grep "cpu cores" | tail -1 | awk '{print $4}'` MEMORY_SIZE=`cat /proc/meminfo | grep MemTotal | awk '{print $2}'` DISK_SIZE=`fdisk -l | grep 'Disk /dev' | awk '{gsub(/:/," =");gsub(/,/,"");print substr($2,6),$3,$4,$5}'` GATEWAY_INFO=`netstat -rn | egrep '^0.0.0.0' | awk '{printf $2 " "}'` function get_cpu_info() { echo -e "CPU Model is $CPU_MODEL" echo -e "Serial Number is $SERIAL" echo -e "Kernel is $KERNEL_INFO" echo -e "Core count per CPU is $CORE_COUNT_PER_CPU" } function get_mem_info() { echo -e "Memory size is $MEMORY_SIZE" } function get_disk_info() { echo -e "Disk size is $DISK_SIZE" } function get_net_info() { echo -e "Gateway is $GATEWAY_INFO" } ####### COMMAND ############# if [[ $# -ne 1 ]]; then echo -e "${txtylw}=======================================${txtrst}" echo -e "${txtgrn} << ${txtpur}${HOSTNAME}${txtgrn} Server Resources >>${txtrst}" echo -e "${txtgrn} $0 ${txtred}[all|cpu|mem|disk|net]" echo -e "${txtylw}=======================================${txtrst}" exit fi case "$1" in "all" ) get_cpu_info; get_mem_info; get_disk_info; get_net_info ;; * ) get_$1_info ;; esac
먼저 top, uptime등의 툴을 이용하여 Load average가 core 수보다 높은지 확인 한다. oom-killer 등 시스템 메세지가 발생한다면 dmesg 또는 syslog 를 통해 확인한다. 부하가 클 경우 sar, vmstat등으로 시간경과에 따른 CPU 사용률이나 I/O 대기율 추이를 확인한다.
- CPU 부하가 높은 경우
- User / Kernel 프로그램 중 원인 확인
- I/O 부하가 높은 경우
- ps로 특정 프로세스가 극단적으로 메모리를 소비하고 있는지 확인
$ vmstat 5 5 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st
r : CPU에서 동작중인 프로레스 수 (r 값이 CPU보다 클 경우 처리하지 못해 대기하는 프로세스 발생한다는 의미로 포화 상태 파악 가능)
1) CPU 사용률 (cpu)
CPU사용률이 높다는 것은 장애라고 보기 보다 CPU를 잘 사용하고 있다는 의미이지만 CPU 사용률이 100%일때는 포화상태로 대기가 발생할 수 있는 상황이라고 할 수 있다. us = user time, 커널에서 사용되는 sy = system time, id = idle, wa = wait I/O(process의 block 상태), st = stolen time(hypervisor가 가상 CPU를 서비스 하는 동안 실제 CPU를 차지한 시간)을 뜻한다. 여기서 I/O대기와 관련해서는 wa가 아닌 b 열을 참고하도록 한다.
- CPU 병목 현상
- 100%라고 나쁜 것이 아니라 효율적인 상태를 나타내는것
- CPU를 이용하는 처리가 많아서 큐 대기행렬이 발생하고 있을 때(vmstat의 Run-Queue값이 증가한 상태)
- CPU 코어 수를 늘리거나 수평분할에 따른 서브를 추가하여 병렬처리하도록 처리량 늘리기 (scale out)
- CPU 응답이 느릴 때 (응답의 병목현상)
- CPU 클럭수 (초당 명령 처리 수)를 늘려처리능력을 향상시키기 (scale -up)
- 클럭수를 늘리는 데에는 한계가 있고 public cloud를 사용할 때 cpu 클럭 수를 제어할 수 없는 단점이 있어 극적인 개선 효과는 기대하기 어려움
- 처리를 분할하여 다수의 CPU 코어에게 병렬로 동시 처리 시키기
- 처리를 병렬화 할 수 있는 가가 중요한 문제
- CPU 클럭수 (초당 명령 처리 수)를 늘려처리능력을 향상시키기 (scale -up)
- CPU 사용률이 오르지 않을 때 (네트워크 I/O 나 디스크 I/O 에서 막히는 경우 -> CPU의 문제보다 어플리케이션이 CPU, 메모리, I/O등의 하드웨어 리소스를 제대로 활용하지 못함)
- 처리 다중화
- 쓰레드를 여러개 가동하여 동기 I/O 명령을 쓰레드 단위로 병행해서 실행 -> I/O 부하와 CPU 사용률 증가
- I/O 비동기화
- 프로세스가 I/O 처리 완료를 기다리지 않고 다음으로 넘어갈 수 있도록 설정 -> CPU 처리와 I/O 처리를 동시에 진행하여 사용상태 개선
- 처리 다중화
2) 메모리 사용률(swap)
Memory Swap이 발생하는지 확인한다.
swap in, out 발생시 현재 시스템의 메모리가 부족함을 나타내는 것으로 RES가 큰 프로세스가 없는지 확인해야한다. free 명령어로도 메모리 사용량을 확인할 수 있다 . 여기서 available은 swapping 없이 새로운 프로세스에서 할당이 가능한 메모리 예상 크기를 나타낸다.
$ free -wh total used free shared buffers cache available Mem: 31Gi 1.3Gi 19Gi 0.0Ki 122Mi 10Gi 29Gi Swap: 0B 0B 0B
- buffers : Block I/O의 buffer 캐시 사용량
- cache : 파일 시스템에서 사용되는 page cache ( 0 일 경우 disk I/O가 높다는 의미)
여기서 top 명령어를 사용해서도 서버 리소스 사용률을 대부분 알 수 있다.
- VIRT : 프로세스가 확보한 가상 메모리 영역 크기
- RES : 실제 물리 메모리 영역 크기
- 메모리 병목 현상
- 영역 부족
- 페이징(paging) 또는 스와핑(swapping)처리를 이용해서 빈 메모리 확보
- 동일 데이터에 대한 병목현상
- 메모리는 빠르지만 특정 영역을 복수의 프로세스가 공유하는 경우 빠른 쪽이 독점해서 관리하기 때문에 각각의 프로세스나 스레드가 경합하면서 CPU 리소스를 불필요하게 소비함 (비효율)
- 복수의 프로세스나 스레드가 같은 메모리 영역을 참조하지 않도록 설정
- 대기 행렬로 하면 해당 영역이 배열로 관리되어 관리영역이 데이터보다 커지기 때문에 이는 사용하지 않음
- 영역 부족
3) 디스크 사용률 (memory)
$ iostat -xt Linux 5.4.0-1038-aws (ip-192-168-0-146.ap-northeast-2.compute.internal) 03/19/21 _x86_64_ (8 CPU) 03/19/21 14:59:35 avg-cpu: %user %nice %system %iowait %steal %idle 0.15 0.00 0.08 0.00 0.04 99.73 Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz aqu-sz %util loop0 0.01 0.01 0.00 0.00 0.30 1.04 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 loop1 0.00 0.00 0.00 0.00 2.27 1.25 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
iostat을 통해 OS 커널에서 취득한 디스크 사용률을 알 수 있다. 부족하면 증설하고 mount 하거나 불필요한 파일을 지워야 한다. (r/s, w/s 는 read 요청, write 요청을 의미한다)
- await : I/O 처리 평균 시간 (어플리케이션 대기 시간)
- 디스크 I/O 병목현상
- 디스크는 I/O는 느리다, I/O 효율을 높이거나 I/O를 줄여야 한다
- 외부저장소 사용
- 많은 디스크를 배치하고 캐시 전용 메모리 영역을 갖추고 있어 디스크 수에 따라 처리량이 증가하므로 유리)
- 랜덤I/O
- 디스크는 한 곳에 랜덤 액세스 하여 느림
- 작은 파일에 액세스 하는 경우
- 단일 디스크에 랜덤 I/O 하여 느림
- 강한 기억 영역을 이용하여 디스크의 내용을 캐시하는 구조를 적용해서 효율화를 도모
- 순차 I/O
- 복수의 디스크에 순차적으로 액세스 병렬화 및 디스크 자체의 순차 특성을 이용, 빠름)
- 큰 파일에 대한 일괄 읽기 처리가 발생시
4) 네트워크 상태
$ sar -n DEV,TCP,ETCP 5 Linux 5.3.0-1032-aws (ip-192-168-0-207) 08/13/20 _x86_64_ (2 CPU) 23:17:08 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil 23:17:13 eth0 0.60 0.20 0.03 0.01 0.00 0.00 0.00 0.00 23:17:08 active/s passive/s iseg/s oseg/s 23:17:13 0.00 0.00 0.40 0.00 23:17:08 atmptf/s estres/s retrans/s isegerr/s orsts/s
sar 명령어를 통해서 네트워크 I/O 정보를 확인할 수 있다. Network 재전송 비율이 0.5%보다 높은지 확인한다.
- active/s : 서버에서 다른 외부 장비로 연결한 횟수 (TCP 연결)
- passive/s : 서버에 새로 접근한 클라이언트 수 (새로운 요청)
- retains/s : 초당 재전송 된 총 세그먼트 수 (품질을 판단할 수 있는 기준으로 재전송 비율이 0.01% 이하가 정상이고 원거리의 경우에도 0.5%로 가 일반적)
https://tech.kakao.com/2016/04/21/closewait-timewait/
1) TIME_WAIT 상태 (Client (연결 요청자))
Active close 측에 TIME_WAIT 이 필요한 이유는 다음과 같다.
- TIME_WAIT이 없이 즉시 연결이 종료될 경우 이미 다른 연결이 진행되었다면 passive close 의 ACK 보다 먼저 도착할 경우 잘못된 데이터를 처리하게 되어 데이터 무결성 문제가 발생
- 마지막 ACK 패킷이 유실되면 상대가 LAST_ACK 상태에 빠지고 새로운 SYN 패킷 전달시 RST 리턴을 하게 된다. 새로운 연결을 오류를 내고 실패하며 이미 연결을 시도한 상태이기 때문에 상대방에게 접속 오류 메시지가 출력된다.
따라서 TIME_WAIT 을 이용하여 패킷의 오동작을 막아야 하므로 상태 자체는 연결을 해제하는 과정에서 나타나는 자연스러운 현상이다. 하지만 TIME_WAIT 소켓이 많아져 로컬의 포트 고갈에 따른 어플리케이션 타임아웃이 발생할 수 있고, 잦은 tcp 연결/해제로 인해 응답속도가 낮아질 수 있다. 따라서 웹 성능 개선을 위해 keepalive(server 측), connection pool(client 측)을 통해 연결을 재사용 하도록 한다.
2) CLOSE_WAIT 상태 (Server (연결응답자))
CLOSE_WAIT은 다음의 순서로 이루어진다.
- Active Close쪽이 먼저 close() 수행하고 FIN 보낸 후 FIN_WAIT1 상태로 대기
- 서버는 CLOSE_WATI으로 바꾸고 응답 ACK전달함과 동시에 해당 포트에 연결되어 있는 어플리케이션에게 close() 요청
- ACK 받은 클라이언트는 상태를 FIN_WAIT2로 변경
- close() 요청을 받은 서버 어플리 케이션은 종료 프로세스를 진행하고 FIN을 클라이언트에게 보내고 LAST_ACK 상태로 바꿈
- FIN 받은 클라이언트는 서버에 ACK를 다시 전송하고 TIME_WAIT 상태로 변경
- 일정 시간이 지나면 CLOSED로 닫음
- ACK 받은 서버도 포트를 CLOSED로 닫음
병목, 서버 멈춤 현상등으로 인해 정상적으로 close 하지 못할 경우 CLOSE_WAIT 상태로 대기하게 된다. 즉, CLOSE_WAIT이 많을 수록 부하가 몰려 서버측에서 능동적으로 연결을 끊지 못한다는 말이다. 커널 옵션으로 타임아웃 조절이 가능한 FIN_WAIT이나 재사용이 가능한 TIME_OUT과 달리 CLOSE_WAIT은 포트를 잡고 있는 프로세스의 종료 또는 네트워크 재시작 외에는 제거할 방법이 없다. 따라서 평소에 서비스의 부하를 낮은 상태로 유지하여야 한다.
- 네트워크 I/O 병목현상
- 응답시간 오버헤드가 큼
- 처리량을 개선하는 접근법이나 네트워크 I/O 자체가 발생하지 않도록 해야함
- 통신 프로세스의 병목
- 처리를 다중화 하여 병렬화 (통신대역 모두 사용)
- 네트워크 경로 병목현상
- IP 주소수가 부족한지, 경로와 트래픽 증감에 대해서도 검토 (게이트웨이)
USE 방법론 ③ 부하
부하란 1분, 5분, 15분 마다 상태가 R, D인 프로세스 개수의 평균값을 나타내며 처리를 실행하려고 해도 리소스들이 처리하지 못하여 대기하고 있는 프로세스 수를 뜻한다.
- TASK_RUNNING : CPU 사용하려는데 다른 프로세스가 CPU를 사용하고 있어서 기다리고 있는 프로세스
- TASK_UNINTERRUPTIBLE : 계속 처리하려고 해도 디스크 입출력이 끝날 때까지 기다려야 하는 프로세스
키보드 입력대기나 sleep에 의한 대기는 프로그램이 명시적으로 기다리는 것이고 원격 호스트로부터의 데이터 착신 대기도 언제 데이터가 올지 불확실하므로 Load Aerage에 포함되지 않는다.
부하를 확인하기 위해서는 다음의 항목을 확인한다.
- Load Average가 core 수보다 높은지
- CPU 동작 프로세스가 core 수보다 많은지
- I/O 처리 대기시간이 급격히 높아지는지
- Memory Swap 발생 여부
- Network 재전송 비율이 0.5%보다 높은지
부하가 높은 상태에 있다면 프로세스 상태가 R, D이며서 CPU사용률이 높은 프로세스를 찾아 원인을 파악한다.
(참고한 사이트)
NextStep 프로젝트 공방
https://tech.kakao.com/2016/04/21/closewait-timewait/
'네트워크 & 인프라' 카테고리의 다른 글
AWS Auto Scaling 적용하기 (Load balancer를 이용한 부하분산) (0) 2022.04.14 화면 응답 개선하기 (0) 2022.04.14 부하 테스트 ( + k6, grafana + influxdb, ngrinder) (0) 2022.04.14 웹 성능 진단하기 (0) 2022.04.14 AWS 망 구성하고 서비스 배포하기 (0) 2022.03.28