이전 글 '특전기 카오스 엔지니어링 실전'에서는 특전기(特来电)의 카오스 엔지니어링 설계와 계획을 상세히 소개했습니다. 현재 저희는 애플리케이션 계층에서의 카오스 실험을 시작했습니다.
애플리케이션 계층 카오스 실험에서는 HSF 서비스 컨테이너의 쓰레드 고갈, CPU 사용률 25%/50%/75%, 포트 고갈, 메모리 누수, 서비스 타임아웃, 서비스 예외 등 다양한 시나리오를 자주 시뮬레이션해야 합니다.
초기에는 특정 HSF 서비스를 선정해 위와 같은 카오스 이벤트 시나리오를 수동으로 주입했습니다. 하지만 주입할 때마다 준비 시간이 길고, 제어가 복잡하며, 많은 시간이 소요되는 문제에 직면했습니다.
이후 알리바바의 중팅(中亭) 선생님과의 논의를 통해 영감을 얻어, 카오스 이벤트 주입 도구를 개발하기로 결정했습니다. 이를 통해 카오스 실험 시나리오에 따라 유연하게 이벤트를 주입할 수 있게 되었습니다.
구체적인 설계 방향은 다음과 같습니다:
- 카오스 이벤트 주입 인터페이스 통일
- 다양한 카오스 이벤트 주입을 지원하고, 핫 업데이트 및 취소가 가능한 통합 주입기 설계
- HSF, API 게이트웨이, 미들웨어 SDK 계층에 카오스 이벤트 주입기 의존성 주입
1. 카오스 이벤트 주입 인터페이스 통일 및 구현
먼저 IChaosEvent 인터페이스를 정의합니다. 이 인터페이스는 Inject와 Stop 두 가지 메서드를 포함합니다.
interface IChaosEvent
{
void Inject(Dictionary<string, string> context);
void Stop();
}
동시에 ChaosEventType 열거형을 추가합니다:
public enum ChaosEventType
{
CPU25, CPU50, CPU75,
ServiceTimeout, ServiceException,
Memory, Threads, Ports
}
CPU 25% 사용률 시뮬레이션
class Chaos_HighCPU25 : IChaosEvent
{
CancellationTokenSource cts;
public Chaos_HighCPU25()
{
cts = new CancellationTokenSource();
}
public void Inject(Dictionary<string, string> context)
{
var count = (25 / 100.0) * Environment.ProcessorCount;
for (int i = 0; i < count; i++)
{
var cpuTask = new Task(() =>
{
while (true && cts.IsCancellationRequested == false) { }
}, cts.Token, TaskCreationOptions.LongRunning);
cpuTask.Start();
}
}
public void Stop()
{
cts.Cancel();
}
}
CPU 50% 사용률 시뮬레이션
class Chaos_HighCPU50 : IChaosEvent
{
CancellationTokenSource cts;
public Chaos_HighCPU50()
{
cts = new CancellationTokenSource();
}
public void Inject(Dictionary<string, string> context)
{
var count = (50 / 100.0) * Environment.ProcessorCount;
for (int i = 0; i < count; i++)
{
var cpuTask = new Task(() =>
{
while (true && cts.IsCancellationRequested == false) { }
}, cts.Token, TaskCreationOptions.LongRunning);
cpuTask.Start();
}
}
public void Stop()
{
cts.Cancel();
}
}
CPU 75% 사용률 시뮬레이션
class Chaos_HighCPU75 : IChaosEvent
{
CancellationTokenSource cts;
public Chaos_HighCPU75()
{
cts = new CancellationTokenSource();
}
public void Inject(Dictionary<string, string> context)
{
var count = (75 / 100.0) * Environment.ProcessorCount;
for (int i = 0; i < count; i++)
{
var cpuTask = new Task(() =>
{
while (true && cts.IsCancellationRequested == false) { }
}, cts.Token, TaskCreationOptions.LongRunning);
cpuTask.Start();
}
}
public void Stop()
{
cts.Cancel();
}
}
메모리 누수(2GB) 시뮬레이션
class Chaos_Memory : IChaosEvent
{
CancellationTokenSource cts;
static string OneKB = new string('1', 1024);
static List<string> list = new List<string>();
public Chaos_Memory()
{
cts = new CancellationTokenSource();
}
public void Inject(Dictionary<string, string> context)
{
var count = context.ContainsKey("MemoryMB") ? context["MemoryMB"] : "2000";
if (int.TryParse(count, out int c))
{
Task task = new Task(() =>
{
for (int k = 0; k < c / 2; k++)
{
var builder = new StringBuilder();
for (int i = 0; i < 1024; i++)
builder.Append(OneKB);
list.Add(builder.ToString());
}
}, cts.Token, TaskCreationOptions.LongRunning);
task.Start();
}
}
public void Stop()
{
cts.Cancel();
list.Clear();
list = new List<string>();
}
}
포트 고갈 시뮬레이션
class Chaos_Ports : IChaosEvent
{
CancellationTokenSource cts;
static List<Socket> sockets;
public Chaos_Ports()
{
cts = new CancellationTokenSource();
sockets = new List<Socket>();
}
public void Inject(Dictionary<string, string> context)
{
var count = Convert.ToInt32(context["Count"]);
var server = context["Server"];
var parts = server.Split(':');
var task = Task.Factory.StartNew(() =>
{
for (int i = 0; i < count; i++)
{
try
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
socket.Connect(parts[0], Convert.ToInt32(parts[1]));
SetKeepAlive(socket, true, 36000000, 1000);
sockets.Add(socket);
}
catch { }
}
while (cts.IsCancellationRequested == false)
Thread.Sleep(20 * 60000);
}, TaskCreationOptions.LongRunning);
}
public void Stop()
{
cts.Cancel();
if (sockets != null)
{
foreach (var socket in sockets)
try { socket.Close(); } catch { }
sockets.Clear();
sockets = null;
}
}
int SetKeepAlive(Socket socket, bool onOff, uint keepAliveTime, uint keepAliveInterval)
{
var keepAlive = new TcpKeepAlive
{
OnOff = Convert.ToUInt32(onOff),
KeepaLiveTime = keepAliveTime,
KeepaLiveInterval = keepAliveInterval
};
byte[] inValue = new byte[12];
for (int i = 0; i < 12; i++) inValue[i] = keepAlive.Bytes[i];
return socket.IOControl(IOControlCode.KeepAliveValues, inValue, null);
}
}
[StructLayout(LayoutKind.Explicit)]
unsafe struct TcpKeepAlive
{
[FieldOffset(0)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
public fixed byte Bytes[12];
[FieldOffset(0)] public uint OnOff;
[FieldOffset(4)] public uint KeepaLiveTime;
[FieldOffset(8)] public uint KeepaLiveInterval;
}
쓰레드 고갈 시뮬레이션
class Chaos_Threads : IChaosEvent
{
CancellationTokenSource cts;
List<Task> tasks = new List<Task>();
public Chaos_Threads()
{
cts = new CancellationTokenSource();
}
public void Inject(Dictionary<string, string> context)
{
if (int.TryParse(context["Threads"], out int count))
{
for (int i = 0; i < count; i++)
{
var task = new Task(() =>
{
for (int j = 0; j < 120; j++)
{
if (cts.IsCancellationRequested) return;
Thread.Sleep(10000);
}
}, cts.Token);
task.Start();
tasks.Add(task);
}
}
}
public void Stop()
{
cts.Cancel();
if (tasks != null)
foreach (var task in tasks)
try { task.Dispose(); } catch { }
}
}
서비스 호출 예외 시뮬레이션
class Chaos_ServiceException : IChaosEvent
{
bool _stopped = false;
public void Inject(Dictionary<string, string> context)
{
if (!_stopped) throw new Exception("Simulated service exception");
}
public void Stop()
{
_stopped = true;
}
}
서비스 호출 타임아웃 시뮬레이션
class Chaos_ServiceTimeout : IChaosEvent
{
CancellationTokenSource cts = new CancellationTokenSource();
bool _stopped = false;
public void Inject(Dictionary<string, string> context)
{
if (!_stopped) Task.Delay(10000, cts.Token).Wait();
}
public void Stop()
{
cts.Cancel();
_stopped = true;
}
}
2. 통합 카오스 이벤트 주입기 설계
ChaosEventInjecter 클래스는 이벤트 객체 생성, 전역/서비스별 주입, 중지를 담당합니다.
이벤트 객체 팩토리 메서드
IChaosEvent CreateEvent(ChaosEventType type)
{
return type switch
{
ChaosEventType.CPU25 => new Chaos_HighCPU25(),
ChaosEventType.CPU50 => new Chaos_HighCPU50(),
ChaosEventType.CPU75 => new Chaos_HighCPU75(),
ChaosEventType.Memory => new Chaos_Memory(),
ChaosEventType.Threads => new Chaos_Threads(),
ChaosEventType.ServiceException => new Chaos_ServiceException(),
ChaosEventType.ServiceTimeout => new Chaos_ServiceTimeout(),
ChaosEventType.Ports => new Chaos_Ports(),
_ => null
};
}
주입기 클래스 (핵심 부분)
public class ChaosEventInjecter
{
static readonly object _lock = new object();
static readonly object _eventLock = new object();
static ChaosEventInjecter _instance;
ConcurrentDictionary<ChaosEventType, IChaosEvent> _eventCache = new();
ConcurrentDictionary<ChaosEventType, ChaosEventType> _triggeredEvents = new();
ChaosEventInjecter() { }
public static ChaosEventInjecter Instance
{
get
{
if (_instance == null)
lock (_lock)
_instance ??= new ChaosEventInjecter();
return _instance;
}
}
// 전역 1회 주입
public void InjectGlobal(ChaosEventType type, Dictionary<string, string> ctx = null)
{
if (!_triggeredEvents.ContainsKey(type))
lock (_eventLock)
if (!_triggeredEvents.ContainsKey(type))
{
var evt = GetOrCreate(type);
evt?.Inject(ctx);
_triggeredEvents.TryAdd(type, type);
}
}
// 서비스별 주입 (매 호출마다)
public void InjectPerService(List<string> serviceIds)
{
var mgr = ChaosEventManager.Instance;
if (mgr.IsEmpty)
{
StopAll();
return;
}
foreach (var sid in serviceIds)
{
var cfg = mgr.GetConfig(sid);
if (cfg == null) continue;
switch (cfg.Type)
{
case ChaosEventType.ServiceException:
case ChaosEventType.ServiceTimeout:
InjectSingle(cfg.Type, cfg.Parameters);
break;
default:
InjectGlobal(cfg.Type, cfg.Parameters);
break;
}
}
}
// 모든 이벤트 중지
public void StopAll()
{
if (_triggeredEvents.IsEmpty) return;
foreach (var kv in _triggeredEvents)
GetOrCreate(kv.Key)?.Stop();
_triggeredEvents = new();
}
// 단일 주입
void InjectSingle(ChaosEventType type, Dictionary<string, string> ctx = null)
{
GetOrCreate(type)?.Inject(ctx);
}
IChaosEvent GetOrCreate(ChaosEventType type)
{
if (!_eventCache.ContainsKey(type))
lock (_lock)
{
var evt = CreateEvent(type);
if (evt != null) _eventCache.TryAdd(type, evt);
}
return _eventCache.GetValueOrDefault(type);
}
}
ChaosEventManager: Redis 기반 설정 관리
public class ChaosEventManager
{
static readonly object _lock = new();
static ChaosEventManager _instance;
ConcurrentDictionary<string, ChaosEventConfig> _configs = new();
readonly CacheService _cache = CacheService.GetInstance("DefaultPool");
ChaosEventManager()
{
ReloadConfigs();
StartAutoRefresh();
}
public static ChaosEventManager Instance =>
_instance ??= new();
void StartAutoRefresh()
{
Task.Run(async () =>
{
while (true)
{
await Task.Delay(10000);
try { ReloadConfigs(); }
catch { /* retry on next cycle */ }
}
});
}
void ReloadConfigs()
{
using var client = _cache.GetClient();
var keys = client.GetHashKeys("ChaosEvents");
var newConfigs = new ConcurrentDictionary<string, ChaosEventConfig>();
keys?.ForEach(k =>
newConfigs.TryAdd(k, client.GetValueFromHash<ChaosEventConfig>("ChaosEvents", k)));
foreach (var kv in newConfigs)
_configs[kv.Key] = kv.Value;
if (newConfigs.IsEmpty)
ChaosEventInjecter.Instance.StopAll();
}
public ChaosEventConfig GetConfig(string serviceId) =>
_configs.GetValueOrDefault(serviceId);
public bool IsEmpty => _configs.IsEmpty;
}
설정 데이터 클래스
public class ChaosEventConfig
{
public ChaosEventType Type { get; set; }
public Dictionary<string, string> Parameters { get; set; }
}
3. HSF, API 게이트웨이, 미들웨어 SDK 계층 통합
HSF 서비스 호출 시 AOP를 통해 카오스 이벤트를 주입합니다. API 게이트웨이와 미들웨어 SDK에서도 동일한 방식으로 적용됩니다.
주입 시점은 서비스 호출 전/후로 나누어 구성할 수 있으며, 각 계층에서 ChaosEventInjecter.Instance.InjectPerService 또는 InjectGlobal을 호출합니다.
부록: 카오스 이벤트 주입 도구
실시간 이벤트 주입, 취소, 시뮬레이션 실행을 지원하는 도구를 함께 개발했습니다. 예를 들어 CPU 25% 사용률 시뮬레이션 화면은 다음과 같습니다:
위 도구와 설계 아이디어가 여러분께 도움이 되길 바랍니다.