닷넷 환경에서 비동기적으로 작동하는 이벤트의 Message Delivery Once 를 검증하는방법과
Nbench를 이용하여 유닛테스트환경에서 성능측정을 할수 있는방법을 소개합니다.
이 샘플에서는 액터메시지 모델이 이용되었지만 액터모델과 상관없이~
유닛테스트내에서 성능측정 기법을 이용할수 있습니다.
측정할 수 없는 것은 관리할수 없다 - 피터 드러크
테스트 탐색기
테스트 탐색기를 통해 성능유닛 테스트를 수행하고 측정할수 있습니다.
여기서 설명하는 작동코드는 깃헙을 통해서도 확인할수 있으며
추가로 다양한 액터모델을 테스트하고 학습할수 있는 코드를 지속 업데이트 예정입니다.
git :
- https://github.com/psmon/NetCoreLabs/blob/main/ActorLibTest/Intro/RoutersTest.cs - 닷넷버전
- https://github.com/psmon/java-labs/blob/master/springweb/src/test/java/com/webnori/springweb/akka/README.md -자바버전으로도 병렬 작성중입니다.
기본 유닛테스트
[Theory(DisplayName = "RoundRobinPoolTest")] [InlineData(3,1000)] public void RoundRobinPoolTest(int nodeCount, int testCount, bool isPerformTest = false) { var actorSystem = akkaService.GetActorSystem(); TestProbe testProbe = this.CreateTestProbe(actorSystem); var props = new RoundRobinPool(nodeCount) .Props(Props.Create(() => new BasicActor())); var actor = actorSystem.ActorOf(props); for (int i = 0; i < nodeCount; i++) { actor.Tell(testProbe.Ref); } int cutOff = 3000; Within(TimeSpan.FromMilliseconds(cutOff), () => { for (int i = 0; i < testCount; i++) { actor.Tell("hello" + i); } for (int i = 0; i < testCount; i++) { testProbe.ExpectMsg("world"); if (isPerformTest) { _dictionary.Add(_key++, _key); _addCounter.Increment(); } } }); }
측정을 위해서는 비동기적으로 발생하는 이벤트가 완료되었다란 정의를 어떻게 내리고 측정을 해야할까요?
위코드는 단순하게 이벤트 생성에서만 측정하는것이아닌 메시지함에서 확인함까지 완료하는 검증로직이며
Message delivery once 수준을 검증 합니다.
Akka테스트 툴킷
메시지 수신함에서 메시지가 있는지를 하나씩 꺼내 체크하는 Akka Testkit이 제공하는 큐검사 방식으로
핵심로직을 블락을 시키지 않고 검사할수 있는 방법입니다.
testProbe.ExpectMsg("world");
Nbench에서 지원하는 함수로 수신검사가 완료되면 성능카운터를 1 증가합니다.
성능 테스트모드일때만 수행할수 있으며 기본 유닛테스트에서는 성능테스트 제외하여 도메인검증에 집중할수 있습니다.
_addCounter.Increment();
유닛테스트가 완료된 로직의 성능테스트가 필요시 Nbench를 활용하여 성능 유닛테스트 검사기를 추가할수 있습니다.
[NBenchFact] [PerfBenchmark(NumberOfIterations = 3, RunMode = RunMode.Throughput, RunTimeMilliseconds = 1000, TestMode = TestMode.Test)] [CounterThroughputAssertion("TestCounter", MustBe.GreaterThan, 1000.0d)] [CounterTotalAssertion("TestCounter", MustBe.GreaterThan, 1500.0d)] [CounterMeasurement("TestCounter")] public void RoundRobinPoolTestPerformanceTest() { RoundRobinPoolTest(5, 3000, true); }
주요 설정 옵션은 다음과 같습니다.
- NumberOfIterations : 테스트 횟수
- RunTimeMilliseconds : 측정 기준초 , 1000ms 여야지 TPS(Test Per Sec) 측정입니다.
- CounterThroughputAssertion : 최소 완료 회수로 이 값을 준수를 못하면 성능 테스트 실패합니다.
- CounterTotalAssertion : 평균 완료회수로 이 값을 준수못하면 성능 테스트 실패합니다.
성능 유닛트세트가 작성되면, VS-IDE가 제공하는 테스트 탐색기를 통해서도 수행할수 있습니다.
성능측정 리포트
[PASS] Expected [Counter] TestCounter to must be greater than 1,000.00 operations; actual value was 94,160.06 operations. [PASS] Expected [Counter] TestCounter to must be greater than 1,500.00 operations; actual value was 93,000.00 operations. ---------- Measurements ---------- Metric : [Counter] TestCounter Per Second ( operations ) Average : 94160.06329051661 Max : 94937.13528317196 Min : 93266.78965179897 Std. Deviation : 841.2138544819605 Std. Error : 485.6750453312026 Per Test ( operations ) Average : 93000 Max : 93000 Min : 93000 Std. Deviation : 0 Std. Error : 0 ----------
성능측정이 통과가 되면 간단한 리포팅이 표시가 되며 성능측정이 통과유무의 정보가 표현됩니다.
- [PASS] Expected [Counter] TestCounter to must be greater than 1,000.00 operations; actual value was 94,160.06 operations.
- CounterThroughputAssertion 에서 설정된 값 이상인 최소 TPS 통과됨을 의미하며 추가로 94160 수행되었음을 표현합니다.
- [PASS] Expected [Counter] TestCounter to must be greater than 1,500.00 operations; actual value was 93,000.00 operations.
- CounterTotalAssertion 에서 설정된 값 이상인 평균 TPS가 통과됨을 의미하며 평균 수행횟수가 93000임을 표현합니다.
성능제약 테스트
다양한 이유로 호출량을 통제해야할 필요도 있습니다. 최종 소비자가 충분한 소비를 하지못하는경우 생산을 제약하는 경우이며
성능제약이 잘 작동하는지도 검증할수 있습니다.
이벤트는 N개 동시 발생시킬수 있지만, TPS제약을 할수있는 ThrottleLimitActor 가 준비되었으며
[Theory(DisplayName = "테스트 n초당 1회 호출제약")] [InlineData(5, 1, false)] public void ThrottleLimitTest(int givenTestCount, int givenLimitSeconds, bool isPerformTest) { ....... // Create ThrottleLimit Actor throttleLimitActor = actorSystem.ActorOf(Props.Create(() => new ThrottleLimitActor(1, givenLimitSeconds, 1000))); throttleLimitActor.Tell(new SetTarget(probe)) for (int i = 0; i < givenTestCount; i++) { throttleLimitActor.Tell(new EventCmd() { Message = "test", }); } //Then : Safe processing within N seconds limit for (int i = 0; i < givenTestCount; i++) { probe.ExpectMsg<EventCmd>(message => { Assert.Equal("test", message.Message); }); output.WriteLine($"[{DateTime.Now}] - GTPRequestCmd"); if (isPerformTest) { _dictionary.Add(_key++, _key); _addCounter.Increment(); } } ........... }
기본유닛테스트는 발생메시지가 모두 소비가 되었는가만을 검증합니다.
TPS 1을 유지하였는가? 는 다음과 같이 분리하여 측정될수 있습니다.
[NBenchFact] [PerfBenchmark(NumberOfIterations = 3, RunMode = RunMode.Throughput, RunTimeMilliseconds = 1000, TestMode = TestMode.Test)] [CounterThroughputAssertion("TestCounter", MustBe.LessThanOrEqualTo, 1.0d)] [CounterTotalAssertion("TestCounter", MustBe.LessThanOrEqualTo, 1)] [CounterMeasurement("TestCounter")] public void ThrottleLimitPerformanceTest() { ThrottleLimitTest(1, 1, true); }
통과옵션을을 1보다 작다로 설정을 하여 TPS1이하를 검증을 할수가 있습니다.
- [CounterThroughputAssertion("TestCounter", MustBe.LessThanOrEqualTo, 1.0d)]
- [CounterTotalAssertion("TestCounter", MustBe.LessThanOrEqualTo, 1)]
성능제약 통과 로그
[PASS] Expected [Counter] TestCounter to must be less than or equal to 1.00 operations; actual value was 0.98 operations. [PASS] Expected [Counter] TestCounter to must be less than or equal to 1.00 operations; actual value was 1.00 operations. ---------- Measurements ---------- Metric : [Counter] TestCounter Per Second ( operations ) Average : 0.9802234345891607 Max : 0.9823810933292493 Min : 0.9787775577268321 Std. Deviation : 0.0019042957466220024 Std. Error : 0.0010994456619288725 Per Test ( operations ) Average : 1 Max : 1 Min : 1 Std. Deviation : 0 Std. Error : 0
검증 옵션과 측정수가 일치하지 않으면 다음과같이 유닛테스트가 실패하게 됩니다.
(실패를 유발하기위해 GreaterThanOrEqualTo 으로 설정한 케이스)
[FAIL] Expected [Counter] TestCounter to must be greater than or equal to 1.00 operations; actual value was 0.98 operations. Expected: True Actual: False
샘플 코드 : https://github.com/psmon/NetCoreLabs/blob/main/ActorLibTest/tools/ThrottleLimitActorTest.cs
여기서 확장된 개념이 소비자의 소비능력을 측정하여 생산량을 동적 TPS조절할수 있는 조절기를 설계하는것이 BackPressure 입니다.
GC 성능 테스트기
---------- Measurements ---------- Metric : TotalCollections [Gen0] Per Second ( collections ) Average : 251.73166139361632 Max : 305.97882626522244 Min : 178.82369771642138 Std. Deviation : 47.515272144026895 Std. Error : 15.025648361787713 Per Test ( collections ) Average : 1 Max : 1 Min : 1 Std. Deviation : 0 Std. Error : 0 ---------- Metric : TotalCollections [Gen1] Per Second ( collections ) Average : 251.73166139361632 Max : 305.97882626522244 Min : 178.82369771642138 Std. Deviation : 47.515272144026895 Std. Error : 15.025648361787713 Per Test ( collections ) Average : 1 Max : 1 Min : 1 Std. Deviation : 0 Std. Error : 0
GC 성능테스트 측정도 이용할수 있는것은 보너스입니다.
이상 유닛테스트와 함께 심플한 성능테스트를 함께 할수 있는 방법을 살펴보았으며
측정할 수 없으면 성능 개선을 할수 없는것과 마찬가지로 여기서 이용된 기술과 관련기술 링크도 첨부합니다.
JUnit with Bench
자바에서는 JHM를 이용하여 마이크로 벤치마크할수 있으며 대응하는 버전도 동일하게 준비되어있습니다.
참고링크 :
- https://getakka.net/articles/actors/testing-actor-systems.html
- https://nbench.io/
- https://github.com/Pro-Coded-External/Pro.NBench.xUnit
- Introducing NBench - Automated Performance Testing and Benchmarking for .NET
- https://blog.knoldus.com/backpressure-in-akka-stream/
- https://www.baeldung.com/java-microbenchmark-harness