그러면 왜 CPU가 100%가 되었는지 궁금하여 문제를 찾아봤습니다.
해당 EC2는 Micro로 설정하고 mysql, redis, mongodb를 설치를 하였습니다. 그런데 일반적으로 cpu는 평균 30~40을 유지를 하여 문제가 없다고 판단을 하였지만 갑자기 100%가 되었습니다.
해당 문제의 원인은 Burstable performance instances
의 문제라고 생각을 하였습니다. 버스트 가능 성능 인스턴스에 대한 이해가 부족으로 인하여 서버의 오류가 발생을 하였습니다.
이번에는 문제의 원인, 해결을 하기 위한 노력, CloudWatch을 이용한 모니터링에 대하여 설명하겠습니다.
서버를 운영하면 CPU가 갑자기 올라가는 버스트 현상을 볼 수 있다. 여기서 문제는 버스트 존만큼 CPU 사양을 높이게 되면 서버의 비용이 증가하고 평균 CPU 사용 범위로 지정을 하면 버스트 현상이 발생을 하였을 때 서버가 죽을 수 있는 문제가 있다.
이걸 해결하기 위해 버스트 가능 인스턴스의 개념을 나왔다.
EC2의 버스트 가능 인스턴스는 기본 CPU 사용율을 유지한다. 만약 많은 사용이 필요할 경우에 크레딧을 소모하여 문제를 해결하게 만들었습니다.
이때 인스턴스 유형에 따라 크레딧이 다르다.
예시로 현재 t3.small 타입의 인스턴스를 사용하게되면 AWS에서 시간당 24크레딧을 발급해준다. 그렇게 계속 누적되다가 일정 시간에 20%가 넘는 CPU 사용율이 발생할 경우 20% 넘었던 구간의 시간(ms)을 기준으로 크레딧을 일정량 소모하게 된다. 그리고 발급되고 있는 크레딧을 계속 누적되는 것이 아니라 특정 양만큼만 누적이 되고 더이상 누적이 안되기 때문에 크레딧 사용을 주의해야한다. 예시로 t3.small은 576의 크레딧만 누적할 수 있다. 그 이상 발급이 된 크레딧은 버려지게 된다.
여기서 조심해야 되는 부분은 공급보다 수요가 많아서 크레딧을 전부 사용하면 성능적으로 제약이 걸려서 서버의 장애를 일으킬 수 있다.
메모리 스왑 (Memory Swap): 메모리 스왑은 시스템이 현재 사용하지 않는 메모리 페이지를 디스크의 스왑 공간에 저장하고, 필요한 경우에 다시 읽어와서 사용하는 메커니즘입니다. 이는 물리적인 RAM이 부족할 때 시스템의 성능을 유지하기 위해 사용됩니다.
메모리 스왑을 하는 이유
물리적 메모리 부족 : 현재 실행 중인 프로세스들이 사용하는 메모리 양이 물리적인 RAM의 용량을 초과하는 경우, 스왑을 사용하여 디스크의 공간을 활용해 추가 메모리를 확보할 수 있다.
프로세스 지속성 : 스왑은 메모리 부족 시 프로세스가 강제 종료되는 것을 방지하고, 시스템이 더 많은 메모리를 확보할 수 있도록 합니다.
유연성 : 스왑을 통해 더 많은 프로세스나 데이터를 메모리에 유지할 수 있어 시스템의 유연성이 증가합니다.
sudo dd if=/dev/zero of=/swapfile bs=128M count=32
# /dev/zero에서 128MB 크기의 블록을 32개 생성하여 /swapfile에 쓰기
sudo chmod 600 /swapfile
# /swapfile의 파일 권한을 소유자만 읽기 및 쓰기 가능하도록 변경
sudo mkswap /swapfile
# /swapfile을 스왑 파티션으로 초기화
sudo swapon /swapfile
# /swapfile을 활성화하여 스왑으로 사용
sudo swapon -s
# 현재 활성화된 스왑 파티션 목록 확인
sudo vi /etc/fstab
# /etc/fstab 파일을 편집
[ vi에 하단에 추가 ]
/swapfile swap swap defaults 0 0
# 부팅 시 자동으로 스왑을 마운트하기 위한 설정 추가
## 용량 확인
free
# 시스템의 메모리 및 스왑 사용량 확인
서버의 오류가 발생하는 이유는 버스트의 수요와 공급이 맞지 않다고 판단하여 인스턴스를 micro -> small로 유형을 변경하여 크레딧의 수를 변경을 하였습니다.
기존의 유형보다 성능이 높은 small을 선택하여 cpu의 성능이 더 좋아졌습니다.
정의 : AWS에서 CPU Credit은 1분동안 CPU Boost를 해줄 수 있는 갯수를 의미합니다.
크래딧은 1개의 CPU의 사용률이 100%가 되면, CPU는 BOOST 상태가 되며 1분에 1개의 크래딧을 소모를 합니다.
이때 크래딧이 없으면 성능 저하로 이어집니다.
이 부분은 마나와 비슷합니다. 적이 없을 때 적은 마나를 사용하기 때문에 마나가 충전이 되고, 적이 많으면 많은 마법을 사용하기 때문에 마나를 사용합니다. 크래딧도 똑같습니다. 트래픽을 적으로 생각하면 이해가 쉬울 거 같습니다.
간단하게 인스턴스를 정지 & 시작을 의미합니다. (재실행 X) 이러한 방식으로 처리하면 크래딧이 재충전이 되기 때문에 문제를 해결할 수 있습니다.
간단하게 컴퓨터를 재부팅으로 생각하면 될거 같습니다.
현재 문제점에서 Stop and Start 방식을 사용을 하였지만 이 방식은 개발자, 관리자가 수동으로 해야되는 문제가 발생을 합니다.
또한 CPU가 증가를 하여 서버가 터지기 이전에 해야되기 때문에 지속적으로 모니터링을 해야됩니다.
그래서 CloudWatch를 통해 일정 CPU가 되었을 때 SLACK에 알림을 발송하는 방식으로 모니터링을 대체를 하
겠습니다.
URL은 람다에서 변수로 사용을 하기 때문에 다른 파일에 저장
// 구성 -> 환경변수로 webhook을 받도록 합니다.
const ENV = process.env
if (!ENV.webhook) throw new Error('Missing environment variable: webhook')
const webhook = ENV.webhook;
const https = require('https')
const statusColorsAndMessage = {
ALARM: {"color": "danger", "message":"위험"},
INSUFFICIENT_DATA: {"color": "warning", "message":"데이터 부족"},
OK: {"color": "good", "message":"정상"}
}
const comparisonOperator = {
"GreaterThanOrEqualToThreshold": ">=",
"GreaterThanThreshold": ">",
"LowerThanOrEqualToThreshold": "<=",
"LessThanThreshold": "<",
}
exports.handler = async (event) => {
await exports.processEvent(event);
}
exports.processEvent = async (event) => {
const snsMessage = event.Records[0].Sns.Message;
const postData = exports.buildSlackMessage(JSON.parse(snsMessage))
await exports.postSlack(postData, webhook);
}
exports.buildSlackMessage = (data) => {
const newState = statusColorsAndMessage[data.NewStateValue];
const oldState = statusColorsAndMessage[data.OldStateValue];
const executeTime = exports.toYyyymmddhhmmss(data.StateChangeTime);
const description = data.AlarmDescription;
const cause = exports.getCause(data);
return {
attachments: [
{
title: `[${data.AlarmName}]`,
color: newState.color,
fields: [
{
title: '언제',
value: executeTime
},
{
title: '설명',
value: description
},
{
title: '원인',
value: cause
},
{
title: '이전 상태',
value: oldState.message,
short: true
},
{
title: '현재 상태',
value: `*${newState.message}*`,
short: true
},
{
title: '바로가기',
value: exports.createLink(data)
}
]
}
]
}
}
// CloudWatch 알람 바로 가기 링크
exports.createLink = (data) => {
return `https://console.aws.amazon.com/cloudwatch/home?region=${exports.exportRegionCode(data.AlarmArn)}#alarm:alarmFilter=ANY;name=${encodeURIComponent(data.AlarmName)}`;
}
exports.exportRegionCode = (arn) => {
return arn.replace("arn:aws:cloudwatch:", "").split(":")[0];
}
exports.getCause = (data) => {
const trigger = data.Trigger;
const evaluationPeriods = trigger.EvaluationPeriods;
const minutes = Math.floor(trigger.Period / 60);
if(data.Trigger.Metrics) {
return exports.buildAnomalyDetectionBand(data, evaluationPeriods, minutes);
}
return exports.buildThresholdMessage(data, evaluationPeriods, minutes);
}
// 이상 지표 중 Band를 벗어나는 경우
exports.buildAnomalyDetectionBand = (data, evaluationPeriods, minutes) => {
const metrics = data.Trigger.Metrics;
const metric = metrics.find(metric => metric.Id === 'm1').MetricStat.Metric.MetricName;
const expression = metrics.find(metric => metric.Id === 'ad1').Expression;
const width = expression.split(',')[1].replace(')', '').trim();
return `${evaluationPeriods * minutes} 분 동안 ${evaluationPeriods} 회 ${metric} 지표가 범위(약 ${width}배)를 벗어났습니다.`;
}
// 이상 지표 중 Threshold 벗어나는 경우
exports.buildThresholdMessage = (data, evaluationPeriods, minutes) => {
const trigger = data.Trigger;
const threshold = trigger.Threshold;
const metric = trigger.MetricName;
const operator = comparisonOperator[trigger.ComparisonOperator];
return `${evaluationPeriods * minutes} 분 동안 ${evaluationPeriods} 회 ${metric} ${operator} ${threshold}`;
}
// 타임존 UTC -> KST
exports.toYyyymmddhhmmss = (timeString) => {
if(!timeString){
return '';
}
const kstDate = new Date(new Date(timeString).getTime() + 32400000);
function pad2(n) { return n < 10 ? '0' + n : n }
return kstDate.getFullYear().toString()
+ '-'+ pad2(kstDate.getMonth() + 1)
+ '-'+ pad2(kstDate.getDate())
+ ' '+ pad2(kstDate.getHours())
+ ':'+ pad2(kstDate.getMinutes())
+ ':'+ pad2(kstDate.getSeconds());
}
exports.postSlack = async (message, slackUrl) => {
return await request(exports.options(slackUrl), message);
}
exports.options = (slackUrl) => {
const {host, pathname} = new URL(slackUrl);
return {
hostname: host,
path: pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
};
}
function request(options, data) {
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
res.setEncoding('utf8');
let responseBody = '';
res.on('data', (chunk) => {
responseBody += chunk;
});
res.on('end', () => {
resolve(responseBody);
});
});
req.on('error', (err) => {
reject(err);
});
req.write(JSON.stringify(data));
req.end();
});
}
sudo apt-get install stress
stress --cpu 1 --timeout 600
[ 개선을 생각하는 부분 ]