229 skaitymai

Nėra įrankių, galinčių tvarkyti mūsų žaidimų apkrovos testavimo sistemą – taigi mes sukūrėme „Swarm“

pateikė Andrew Rakhubov19m2025/05/20
Read on Terminal Reader

Per ilgai; Skaityti

Mums reikėjo specializuoto apkrovos testavimo įrankio, galinčio tvarkyti individualizuotus „WebSocket+Protobuf“ protokolus, tarpusavio sinchronizavimą ir didelio masto simuliacijas. Esami įrankiai pritrūko, todėl jie sukūrė „Swarm“: vidinę „C#“ sistemą, kurioje „bots“ (aktyvai) paleidžia scenarijus pagal koordinuotus tvarkaraščius, palaiko „bot-to-bot“ ryšius per bilietus ir atrankus, o „Swarm“ dizainas pirmenybę teikia paprastam reaguojant į būsenos atnaujinimus, konkurencijos kontrolei per „ConcurrentExclusiveSchedulerPair“, kodo generavimui per šaltinio generatorius ir visapusiškam stebėjimui
featured image - Nėra įrankių, galinčių tvarkyti mūsų žaidimų apkrovos testavimo sistemą – taigi mes sukūrėme „Swarm“
Andrew Rakhubov HackerNoon profile picture
0-item
1-item

Kaip mes sukūrėme „Swarm“, mūsų vidinę apkrovos testavimo sistemą, kad galėtume spręsti iššūkius, susijusius su „end-to-end“ testavimu, individualizuotais protokolais ir didelio masto botų modeliavimu „gamedev“.

Kaip mes sukūrėme „Swarm“, mūsų vidinę apkrovos testavimo sistemą, kad galėtume spręsti iššūkius, susijusius su „end-to-end“ testavimu, individualizuotais protokolais ir didelio masto botų modeliavimu „gamedev“.


Šiais laikais tikrai sunku įsivaizduoti programinės įrangos produktą be bentKai kurieBandymų lygis. Vieneto bandymai yra standartinis būdas sugauti klaidas mažose kodo dalyse, o galiniai bandymai apima visą programų darbo eigą. „GameDev“ nėra išimtis, tačiau ji turi savo unikalius iššūkius, kurie ne visada atitinka tai, kaip mes paprastai bandome žiniatinklio programas.


Sveiki, aš esu Andrey Rakhubov, pagrindinis programinės įrangos inžinierius MY.GAMES! Šiame pranešime aš pasidalinsiu detalėmis apie mūsų studijoje naudojamą "backend" ir mūsų požiūrį į karo robotų: sienų meta-žaidimo testavimą.

Pastaba apie mūsų architektūrą

Nesikreipiant į jokias nereikalingas detales, mūsų "backend" susideda iš klasikinių paslaugų ir tarpusavyje susijusių mazgų, kuriuose gyvena veikėjai, grupės.

backend architecture overview

Nepaisant kai kurių kitų trūkumų, kurie gali egzistuoti, šis metodas pasirodė esąs gana geras sprendžiant vieną iš labiausiai erzinančių žaidimų kūrimo problemų: iteracijos greitį.TaiNa, mes turime tą patį pasakymą apie GDD (žaidimo dizaino dokumentą).


Aktoriai padeda tokiu būdu, kad jie yra labai pigūs įgyvendinti ir modifikuoti ankstyvose funkcijų dizaino stadijose, ir jie yra gana paprasti refaktoriui kaip atskirai paslaugai, jei vėliau atsiranda poreikis.


Akivaizdu, kad tai apsunkina apkrovos ir integracijos bandymus, nes nebėra aiškaus paslaugų atskyrimo su mažais, gerai apibrėžtais API.

Išbandymas

Kaip galite atspėti, vieneto testavimas veikėjams ir paslaugoms (ir apkrovos / integracijos testavimas paslaugoms) nesiskiria nuo to, ką rasite bet kokio kito tipo programinėje įrangoje.

  • Apkrovos ir integracijos bandymų veikėjai
  • End-to-end ir load backend testavimas


Mes nežiūrėsime išsamiai išbandyti aktorių čia, nes tai nėra specifinė GameDev, bet susijusi su pačiu aktorių modeliu. Įprastas požiūris yra turėti specialų vieno mazgo klasterį, kuris yra tinkamas, kad būtų prijungtas prie atminties viename teste ir kuris taip pat gali išstumti visus išeinančius prašymus ir kreiptis į aktorių API.


Tai sakė, kad dalykai pradeda tapti įdomesni, kai mes ateiname į apkrovą ar testavimą nuo galo iki galo - ir čia prasideda mūsų istorija.


Taigi, mūsų atveju kliento programa yra pats žaidimas.Žaidimas naudoja Unreal Engine, todėl kodas parašytas C++, o serveryje mes naudojame C#.Žaidėjai sąveikauja su žaidimo UI elementais, pateikdami užklausas į galinį galinį (arba užklausos pateikiamos netiesiogiai).


Šiuo metu didžioji dauguma sistemų tiesiog nustoja dirbti mums, ir bet kokie seleno tipo rinkiniai, kurie laiko kliento programas naršykle, yra ne taikymo srityje.


Kitas klausimas yra tai, kad mes naudojame individualizuotą komunikacijos protokolą tarp kliento ir atgalinės dalies.Ši dalis tikrai nusipelno atskiro straipsnio, bet aš išryškinsiu pagrindines sąvokas:

  • Bendravimas vyksta per WebSocket ryšį
  • Tai schema-pirmas; mes naudojame Protobuf apibrėžti pranešimų ir paslaugų struktūrą
  • "WebSocket" pranešimai yra "Protobuf" pranešimai, supakuoti į konteinerį su metaduomenimis, kurie imituoja tam tikrą būtiną gRPC susijusią informaciją, pvz., URL ir antraštes.

Taigi, bet koks įrankis, kuris neleidžia apibrėžti pritaikytų protokolų, netinka užduočiai.

Įkrovimo įrankis išbandyti juos visus

Tuo pačiu metu mes norėjome turėti vieną įrankį, kuris galėtų parašyti tiek REST / gRPC apkrovos testus, tiek galinius testus su mūsų individualizuotu protokolu.


K6lokalizuotaNBOMBER

Kiekvienas iš jų turėjo savo privalumus ir trūkumus, tačiau buvo keletas dalykų, kurių nė vienas iš jų negalėjo išspręsti be didžiulių (kartais vidinių) pokyčių:


  • Pirma, buvo poreikis, susijęs su bendravimu tarp robotų sinchronizavimo tikslais, kaip aptarta anksčiau.
  • Antra, klientai yra gana reaktyvūs ir jų būklė gali pasikeisti be aiškių veiksmų iš bandymo scenarijaus; tai lemia papildomą sinchronizavimą ir poreikį peršokti per daugelį savo kodo.
  • Paskutinis, bet ne mažiau svarbus, tie įrankiai buvo pernelyg siaurai orientuoti į našumo testavimą, siūlydami daugybę funkcijų palaipsniui didinti apkrovą ar nurodyti laiko intervalus, tačiau trūko galimybės sukurti sudėtingus šakotus scenarijus paprastu būdu.


Atėjo laikas priimti faktą, kad mums tikrai reikia specializuoto įrankio.SwarmBuvo gimęs

Swarm

Aukšto lygio, "Swarm" užduotis yra pradėti daug veikėjų, kurie sukurs apkrovą serveryje. Šie veikėjai vadinami robotais, ir jie gali imituoti kliento elgesį arba skirto serverio elgesį. "Swarm" susideda iš vieno ar kelių agentų, kurie priima šiuos robotus ir bendrauja tarpusavyje.


Formaliau, čia yra įrankio reikalavimų sąrašas:


  1. Reaguoti į valstybės atnaujinimus turėtų būti lengva
  2. Konkurencija neturėtų būti problema
  3. Kodas turėtų būti automatiškai instrumentuojamas
  4. Bot-to-bot komunikacija turėtų būti lengva
  5. Keletas egzempliorių turėtų būti sujungti, kad būtų sukurta pakankamai apkrovos
  6. Įrankis turėtų būti lengvas ir sugebėti sukurti tinkamą stresą pačiai galinei daliai


Kaip premiją, aš taip pat pridėjau keletą papildomų taškų:

  1. Tiek našumo, tiek galutinio bandymo scenarijai turėtų būti įmanomi
  2. Įrankis turėtų būti transportavimo agnostika; prireikus turėtume galėti prijungti šį įrankį prie bet kurio kito galimo transporto.
  3. Įrankis pasižymi imperatyviu kodo stiliumi, nes aš asmeniškai turiu tvirtą nuomonę, kad deklaratyvus stilius netinka sudėtingiems scenarijams su sąlyginiais sprendimais
  4. Robotai turėtų egzistuoti atskirai nuo bandymo įrankio, o tai reiškia, kad neturėtų būti sunkių priklausomybių.


Mūsų tikslas

Įsivaizduokime kodą, kurį norėtume parašyti; mes laikysime botą kaip lėlę, ir jis negali daryti dalykų savarankiškai, jis gali išlaikyti tik invariantus, o scenarijus yra lėlės, traukiančios virves.


public class ExampleScenario : ScenarioBase
{
    /* ... */
    public override async Task Run(ISwarmAgent swarm)
    {
        // spawn bots and connect to backend
        var leader = SpawnClient();
        var follower = SpawnClient();
        await leader.Login();
        await follower.Login();


        // expect incoming InviteAddedEvent
        var followerWaitingForInvite = follower.Group.Subscription
            .ListenOnceUntil(GroupInviteAdded)
            .ThrowIfTimeout();


        // leader sends invite and followers waits for it
        await leader.Group.SendGroupInvite(follower.PlayerId);
        await followerWaitingForInvite;
        Assert.That(follower.Group.State.IncomingInvites.Count, Is.EqualTo(1));
        var invite = follower.Group.State.IncomingInvites[0];


        // now vice versa, the leader waits for an event...
        var leaderWaitingForAccept = leader.Group.Subscription
            .ListenOnceUntil(InviteAcceptedBy(follower.PlayerId))
            .ThrowIfTimeout();


        // ... and follower accept invite, thus producing the event
        await follower.Group.AcceptGroupInvite(invite.Id);
        await leaderWaitingForAccept;
        Assert.That(follower.Group.State.GroupId, Is.EqualTo(leader.Group.State.GroupId));


        PlayerId[] expectedPlayers = [leader.PlayerId, follower.PlayerId];
        Assert.That(leader.Group.State.Players, Is.EquivalentTo(expectedPlayers));
        Assert.That(follower.Group.State.Players, Is.EquivalentTo(expectedPlayers));
    }
}


Steaminė

"Backend" siunčia klientui daug atnaujinimų, kurie gali įvykti retkarčiais; aukščiau pateiktame pavyzdyje vienas toks įvykis yraGroupInviteAddedEventBotas turėtų sugebėti reaguoti į šiuos įvykius viduje ir suteikti galimybę stebėti juos iš išorinio kodo.


public Task SubscribeToGroupState()
{
   Subscription.Listen(OnGroupStateUpdate);
   Subscription.Start(api.GroupServiceClient.SubscribeToGroupStateUpdates, new SubscribeToGroupStateUpdatesRequest());
   return Task.CompletedTask;
}


Nors kodas yra gana paprastas (ir kaip jūs galite atspėtiOnGroupStateUpdateViskas, kas vyksta, yra tik ilgas perjungimo atvejis), čia vyksta daug dalykų.


StreamSubscriptionJis pats aIObservable<ObservedEvent<TStreamMessage>>

ir jis suteikia naudingų pratęsimų, turi priklausomą gyvavimo ciklą ir yra padengtas metrika.

Kitas privalumas: nereikia aiškaus sinchronizavimo, kad būtų galima perskaityti ar modifikuoti būseną, nepriklausomai nuo to, kur įgyvendinamas tvarkytuvas.


case GroupUpdateEventType.GroupInviteAddedEvent:
   State.IncomingInvites.Add(ev.GroupInviteAddedEvent.GroupId, ev.GroupInviteAddedEvent.Invite);
   break;


Konkurencija neturėtų būti problema

Idėja yra paprasta: niekada nevykdyti kodo, kuris vienu metu priklauso scenarijui ar botui, gimusiam tame scenarijuje, ir toliau, vidinis kodas botui / moduliui neturėtų būti vykdomas vienu metu su kitomis boto dalimis.


Tai panašu į skaitymo / rašymo užraktą, kuriame skaitomas scenarijaus kodas (pasidalinta prieiga) ir bot kodas yra rašomas (išskirtinė prieiga).

Užduočių tvarkaraščiai ir sinchronizavimo kontekstas

Šioje antraštėje minėti du mechanizmai dabar yra labai galingos asynchrono kodo dalys C #. Jei kada nors sukūrėte WPF programą, tikriausiai žinote, kad tai yra asynchrono kodas.DispatcherSynchronizationContexttai yra atsakinga už elegantišką jūsų async skambučių grąžinimą į UI siūlą.


Mūsų scenarijuje mums nerūpi siūlo afinitetas, o vietoj to labiau rūpinamės užduočių vykdymo tvarka scenarijuje.


Prieš skubėdami rašyti žemo lygio kodą, pažvelkime į vieną ne plačiai žinomą klasę, vadinamąConcurrentExclusiveSchedulerPairIš Thedocs:

Dokių


Teikia užduočių tvarkaraščius, kurie koordinuoja užduočių vykdymą, tuo pačiu užtikrinant, kad vienu metu vykdomos užduotys gali būti vykdomos vienu metu, o išskirtinės užduotys niekada neveikia.

Teikia užduočių tvarkaraščius, kurie koordinuoja užduočių vykdymą, tuo pačiu užtikrinant, kad vienu metu vykdomos užduotys gali būti vykdomos vienu metu, o išskirtinės užduotys niekada neveikia.


Tai atrodo kaip būtent tai, ko mes norime!Dabar turime užtikrinti, kad visas scenarijaus kodas būtų vykdomasConcurrentSchedulerkai roboto kodas vykdomasExclusiveScheduler.


Kaip nustatyti užduoties tvarkaraštį? Viena galimybė yra aiškiai perduoti jį kaip parametrą, kuris atliekamas, kai paleidžiamas scenarijus:


public Task LaunchScenarioAsyncThread(SwarmAgent swarm, Launch launch)
{
   return Task.Factory.StartNew(
       () =>
       {
           /* <some observability and context preparations> */
           return RunScenarioInstance(Scenario, swarm, LaunchOptions, ScenarioActivity, launch);
       },
       CancellationToken.None,
       TaskCreationOptions.DenyChildAttach,
       Scenario.BotScenarioScheduler.ScenarioScheduler).Unwrap();
}


RunScenarioInstanceMetodas, savo ruožtu, reikalaujaSetUpirRunmetodus scenarijuje, todėl jie vykdomi vienu metu tvarkaraštyje (ScenarioSchedulerTai konkuruojanti dalisConcurrentExclusiveSchedulerPair) ir

Dabar, kai mes esame scenarijaus kodo viduje ir mes tai darome...


public Task LaunchScenarioAsyncThread(SwarmAgent swarm, Launch launch)
{
   return Task.Factory.StartNew(
       () =>
       {
           /* <some observability and context preparations> */
           return RunScenarioInstance(Scenario, swarm, LaunchOptions, ScenarioActivity, launch);
       },
       CancellationToken.None,
       TaskCreationOptions.DenyChildAttach,
       Scenario.BotScenarioScheduler.ScenarioScheduler).Unwrap();
}

Asynchrono būsenos mašina atlieka savo darbą už mus, išlaikydama mūsų užduočių tvarkaraštį.

DabarSendGroupInvitetaip pat veikia kartu su tvarkaraščiu, todėl mes taip pat darome tą patį triuką:


public Task<SendGroupInviteResponse> SendGroupInvite(PlayerId inviteePlayerId)
{
   return Runtime.Do(async () =>
   {
       var result = await api.GroupServiceClient.SendGroupInviteAsync(/* ... */);
       if (!result.HasError)
       {
           State.OutgoingInvites.Add(inviteePlayerId);
       }


       return result;
   });
}


"Runtime" abstrakcija supa tvarkaraštį taip pat, kaip ir anksčiau, skambindamaTask.Factory.StartNewSu tinkamu tvarkaraščiu.

Kodų kartos

Gerai, dabar mes turime rankiniu būdu apvynioti viską vidujeDoir nors tai išsprendžia problemą, ji yra linkusi į klaidą, lengva pamiršti, ir apskritai kalbant, gerai, atrodo keista.

Pažvelkime į šią kodo dalį dar kartą:


await leader.Group.SendGroupInvite(follower.PlayerId);


Štai mūsų botas turiGroupNekilnojamojo turto .Groupyra modulis, kuris egzistuoja tik norint suskirstyti kodą į atskiras klases ir išvengtiBotIš klasės.


public class BotClient : BotClientBase
{
    public GroupBotModule Group { get; }
    /* ... */
}


Tarkime, kad modulis turėtų turėti sąsają:


public class BotClient : BotClientBase
{
    public IGroupBotModule Group { get; }
    /* ... */
}


public interface IGroupBotModule : IBotModule, IAsyncDisposable
{
    GroupBotState State { get; }


    Task<SendGroupInviteResponse> SendGroupInvite(PlayerId toPlayerId);
    /* ... */
}




public class GroupBotModule :
   BotClientModuleBase<GroupBotModule>,
   IGroupBotModule
{
    /* ... */
    public async Task<SendGroupInviteResponse> SendGroupInvite(PlayerId inviteePlayerId)
    {
        // no wrapping with `Runtime.Do` here
        var result = await api.GroupServiceClient.SendGroupInviteAsync(new() /* ... */);
        if (!result.HasError)
        {
            State.OutgoingInvites.Add(new GroupBotState.Invite(inviteePlayerId, /* ... */));
        }


        return result;
   }
|


Nėra daugiau apvyniojimo ir bjauraus kodo, tik paprastas reikalavimas (kuris yra gana dažnas kūrėjams) sukurti sąsają kiekvienam moduliui.


Bet kur yra tvarkaraštis? Magija vyksta šaltinio generatoriuje, kuris sukuria kiekvienos IBotModule sąsajos proxy klasę ir supa kiekvieną funkciją įRuntime.DoSkambinkite :


   public const string MethodProxy =
@"
   public async $return_type$ $method_name$($method_params$)
   {
       $return$await runtime.Do(() => implementation.$method_name$($method_args$));
   }
";


Instrumentacijos

Kitas žingsnis yra įrankių kodas, siekiant surinkti rodiklius ir pėdsakus. Pirma, kiekvienas API skambutis turėtų būti stebimas. Ši dalis yra labai paprasta, nes mūsų transportas veiksmingai apsimeta gRPC kanalu, todėl mes naudojame tik tinkamai parašytą interceptorių...


callInvoker = channel
   .Intercept(new PerformanceInterceptor());


... kurCallInvokeryra kliento pusės RPC invokacijos gRPC abstrakcija.


Tada būtų puiku apimti kai kurias kodo dalis, kad būtų galima išmatuoti našumą.IInstrumentationFactoryNaudojant šią sąsają:


public interface IInstrumentationFactory

{

   public IInstrumentationScope CreateScope(

       string? name = null,

       IInstrumentationContext? actor = null,

       [CallerMemberName] string memberName = "",

       [CallerFilePath] string sourceFilePath = "",

       [CallerLineNumber] int sourceLineNumber = 0);

}


Dabar galite supakuoti dalis, kurios jus domina:


public Task AcceptGroupInvite(GroupInvideId inviteId)
{
    using (instrumentationFactory.CreateScope())
    {
        var result = await api.GroupServiceClient.AcceptGroupInviteAsync(request);
    }
}


Nors vis dar galite naudoti šią funkciją, kad sukurtumėte pogrupius, kiekvienas modulio proxy metodas automatiškai apibrėžia taikymo sritį:


   public const string MethodProxy =
@"
   public async $return_type$ $method_name$($method_params$)
   {
       using(instrumentationFactory.CreateScope(/* related args> */))
       {
           $return$await runtime.Do(() => implementation.$method_name$($method_args$));
       }
   }
";


Prietaisų apimtis registruoja vykdymo laiką, išimtis, sukuria paskirstytas pėdsakus, gali rašyti debug logus ir daugelį kitų konfigūruojamų dalykų.


Bot-to-bot ryšys

Pavyzdys skyriuje „Mūsų tikslas“ iš tikrųjų nerodo jokio bendravimo.PlayerIdNors šis scenarijų rašymo stilius dažnai yra paprastas ir paprastas, yra sudėtingesnių atvejų, kai šis metodas tiesiog neveikia.


Iš pradžių planavau įgyvendinti tam tikrą juodosios plokštės modelį su bendra raktinių reikšmių saugykla komunikacijai (pavyzdžiui, „Redis“), tačiau atlikus kai kuriuos koncepcijos scenarijų bandymus, paaiškėjo, kad darbo apimtis gali būti labai sumažinta iki dviejų paprastesnių koncepcijų: eilės ir atrankos.

Bilietai – bilietai

Realiame pasaulyje žaidėjai kažkaip bendrauja tarpusavyje – jie rašo tiesiogines žinutes arba naudoja balso pokalbį. Su robotais mums nereikia imituoti to granuliarumo, ir mums tiesiog reikia pranešti ketinimus.Taigi, vietoj „Hey Killer2000, pakviesk mane į savo grupę prašau“ mums reikia paprasto „Hey,KažkasPrašau, pakviesk mane į grupę.“ Štai kur bilietai ateina į žaidimą:


public interface ISwarmTickets
{
   Task PlaceTicket(SwarmTicketBase ticket);


   Task<SwarmTicketBase?> TryGetTicket(Type ticketType, TimeSpan timeout);
}


Botas gali įdėti bilietą, o tada kitas botas gali gauti tą bilietą.ISwarmTicketsyra ne kas kita, kaip pranešimų brokeris su eilės perticketType(Iš tikrųjų tai yraŠiek tiekdaugiau nei tai, nes yra papildomų galimybių užkirsti kelią robotams gauti savo bilietus, taip pat kitus nedidelius pakeitimus).


Su šia sąsaja galiausiai galime suskirstyti pavyzdinį scenarijų į du nepriklausomus scenarijus. (čia visas papildomas kodas pašalinamas iliustruojant pagrindinę idėją):


private async Task RunLeaderRole(ISwarmAgent swarm)
{
   var ticket = await swarm.Blackboard.Tickets
       .TryGetTicket<BotWantsGroupTicket>(TimeSpan.FromSeconds(5))
       .ThrowIfTicketIsNull();


   await bot.Group.SendGroupInvite(ticket.Owner);
   await bot.Group.Subscription.ListenOnceUntil(
           GotInviteAcceptedEvent,
           TimeSpan.FromSeconds(5))
       .ThrowIfTimeout();
}


private async Task RunFollowerRole(ISwarmAgent swarm)
{
   var waitingForInvite = bot.Group.Subscription.ListenOnceUntil(
           GotInviteAddedEvent,
           TimeSpan.FromSeconds(5))
       .ThrowIfTimeout();


   await swarm.Blackboard.Tickets.PlaceTicket(new BotWantsGroupTicket(bot.PlayerId));
   await waitingForInvite;
   await bot.Group.AcceptGroupInvite(bot.Group.State.IncomingInvites[0].Id);
}


Selektorius

Žinoma, jie gali būti suskirstyti į du skirtingus scenarijus ir paleisti lygiagrečiai. Kartais tai yra geriausias būdas tai padaryti, kitais atvejais jums gali prireikti dinamiškos grupės dydžio konfigūracijos (keletas pasekėjų vienam lyderiui) arba bet kokios kitos situacijos, kad paskirstytumėte / pasirinktumėte skirtingus duomenis / vaidmenis.


public override async Task Run(ISwarmAgent swarm)
{
   var roleAction = await swarm.Blackboard
       .RoundRobinRole(
           "leader or follower",
           Enumerable.Repeat(RunFollowerRole, config.GroupSize - 1).Union([RunLeaderRole]));
   await roleAction(swarm);
}


Štai čia,RoundRobinRoleyra tik išgalvotas apvyniojimas aplink bendrą skaitiklį ir modulo operaciją, kad pasirinktumėte tinkamą elementą iš sąrašo.


Swarms grupė

Dabar, kai visi ryšiai yra paslėpti už eilių ir bendrų skaitiklių, tampa trivialu įvesti orchestratoriaus mazgą arba naudoti kai kurias esamas MQ ir KV saugyklas.


Įdomus faktas: mes niekada nesibaigė įgyvendinti šią funkciją. Kai QA gavo savo rankas ant vieno mazgo įgyvendinimo SwarmAgent, jie iš karto pradėjo tiesiog dislokuoti kelis nepriklausomų agentų atvejus, kad padidintų apkrovą.

Vienkartinės veiklos rezultatai

Ką apie našumą? Kiek prarandama visose tose pakuotėse ir netiesioginėse sinchronizacijose? aš neperkrausiu jus su visais atliktais įvairiais testais, tik du svarbiausi, kurie įrodė, kad sistema gali sukurti pakankamai gerą apkrovą.

Benchmark nustatymas:

„BenchmarkDotNet v0.14.0“, „Windows 10“ (10.0.19045.4651/22H2/2022Update)
„Intel Core i7-10875H“ 2.30 GHz procesorius, 1 procesorius, 16 loginių ir 8 fizinių branduolių
.NET SDK 9.0.203 išmanusis ryšys
[Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2
.NET 9.0 : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2


private async Task SimpleWorker()
{
   var tasks = new List<Task>();
   for (var i = 0; i < Parallelism; ++i)
   {
       var index = i;
       tasks.Add(Task.Run(async () =>
       {
           for (var j = 0; j < Iterations; ++j)
           {
               await LoadMethod(index);
           }
       }));
   }


   await Task.WhenAll(tasks);
}


private async Task SchedulerWorker()
{
    // scenarios are prepared in GlobalSetup
    await Task.WhenAll(scenarios.Select(LaunchScenarioAsyncThread));
}


public class TestScenario : ScenarioBase
{
    public override async Task Run()
    {
        for (var iteration = 0; iteration < iterations; ++iteration)
        {
            await botClient.Do(BotLoadMethod);
        }
    }
}


Pirmiausia pažvelkime į beveik gryną tvarkaraščio viršūnę prieš paprastą užduočių gimdymą.

In this case, both load methods are as follows:


private ValueTask CounterLoadMethod(int i)

{

   Interlocked.Increment(ref StaticControl.Counter);

   return ValueTask.CompletedTask;

}


Iterijų rezultatai = 10

WorkerType

Parallelism

Mean

Error

StdDev

Gen0

Gen1

Gen2

Allocated

Simple

10

3.565us

0.0450us

0.0421us

0.3433

-

-

2.83KB

Simple

100

30.281us

0.2720us

0.2544us

3.1128

0.061

-

25.67KB

Simple

1000

250.693us

2.1626us

2.4037us

30.2734

5.8594

-

250.67KB

Scheduler

10

40.629us

0.7842us

0.8054us

8.1787

0.1221

-

66.15KB

Scheduler

100

325.386us

2.3414us

2.1901us

81.0547

14.6484

-

662.09KB

Scheduler

1000

4,685.812us

24.7917us

21.9772us

812.5

375

-

6617.59KB

Paprastas

10

3 565Mėnulis

0.0450us

0 0 0 421

0.3433

-

-

2.83 kB

Paprastas

100

30281Mėnulis

0,2720 Lt

0,2544 Lt

3.1128

0.061

-

25,67 kB

Paprastas

1000

250 693

1626 m.

2.4037Mėnulis

30.2734

5.8594

-

250.67 kB

tvarkaraštis

10

40629Mėnulis

07842Mėnulis

0 805

8.1787

0.1221

-

615 kB

tvarkaraštis

100

325.386Tėvynė

2.3414Mėnulis

1901 m.

81.0547

14.6484

-

6299 kB

tvarkaraštis

1000

4 685 812 Lt

247917Užsakymas

219772Užsakymas

812.5

375

-

6617.59 kB



Atrodo blogai? Ne tiksliai. Prieš iš tikrųjų atlikdamas testą, aš tikėjausi daug blogesnių rezultatų, tačiau rezultatai mane tikrai nustebino. Tik pagalvokite apie tai, kiek įvyko po dangteliu ir naudą, kurią gavome tik už ~4us vienam lygiagrečiam atvejui.


Koks galėtų būti realus blogiausio atvejo bandymo scenarijus? Na, o apie funkciją, kuri daro nieko daugiau nei API skambutį? Viena vertus, beveik kiekvienas botas tai daro, bet kita vertus, jei nėra nieko daugiau nei tik API skambučiai, tada visos sinchronizavimo pastangos yra švaistomos, tiesa?


Įkrovimo metodas būtų tik skambintiPingAsyncDėl paprastumo RPC klientai saugomi už robotų ribų.


private async ValueTask PingLoadMethod(int i)
{
   await clients[i].PingAsync(new PingRequest());
}


Štai rezultatai, dar 10 iteracijų (grpc serveris yra vietiniame tinkle):


WorkerType

Parallelism

Mean

Error

StdDev

Gen0

Gen1

Allocated

Simple

100

94.45 ms

1.804 ms

2.148 ms

600

200

6.14 MB

Simple

1000

596.69 ms

15.592 ms

45.730 ms

9000

7000

76.77 MB

Scheduler

100

95.48 ms

1.547 ms

1.292 ms

833.3333

333.3333

6.85 MB

Scheduler

1000

625.52 ms

14.697 ms

42.405 ms

8000

7000

68.57 MB

Paprastas

100

44,45 ms

1 804 psl

2 148 psl.

600

200

6.4 MB

Paprastas

1000

6956 m2

15 592 psl.

45 730 ms

9000

7000

67,77 MB

tvarkaraštis

100

95.48 ms peržiūra

1547 Mėnulis

1 292 psl.

833.3333

333.3333

685 MB

tvarkaraštis

1000

625,52 psl

14 697 psl.

42 405 ms

8000

7000

68,57 MB


Kaip ir tikėtasi, veiklos poveikis realiems darbo scenarijams yra nereikšmingas.


Analizė

Žinoma, yra "backend" žurnalų, metrų ir pėdsakų, kurie suteikia gerą vaizdą apie tai, kas vyksta įkėlimo metu. bet "Swarm" eina žingsnį toliau, rašydamas savo duomenis - kartais susijusius su "backend" - papildydamas jį.


Testas nepavyko dėl daugelio SLA pažeidimų (žr. API skambučių laiko tarpsnius), ir mes turime visą reikalingą įrangą, kad stebėtume, kas vyksta iš kliento perspektyvos.


Grafana dashboard panels showing failed test


Paprastai mes visiškai sustabdome testą po tam tikro skaičiaus klaidų, todėl API skambučių linijos staiga baigiasi.


Žinoma, pėdsakai yra būtini geram stebėjimui. „Swarm“ bandymų metu kliento pėdsakai yra susieti su atsarginėmis pėdsakomis viename medyje.connectionIdD/botIdTai padeda atlikti debugging.


Distributed tracing connecting Swarm scenario with backend


Pastaba: "DevServer" yra patogus monolitinis "viskas viename" atsarginių paslaugų kūrimas, kuris bus pradėtas kūrėjų kompiuteriuose, ir tai yra specialiai sukurtas pavyzdys, siekiant sumažinti detalių kiekį ekrane.


Swarm robotai rašo kitokį pėdsaką: jie imituoja pėdsakus, naudojamus našumo analizėje.Su esamų įrankių pagalba, tuos pėdsakus galima peržiūrėti ir analizuoti naudojant įmontuotas galimybes, tokias kaip PerfettoSQL.


Adapting perf logs to show bot execution flow

Ką galų gale padarėme

Dabar turime SDK, kuriame yra pastatyti tiek GameClient, tiek Dedicated Server robotai.Šie robotai dabar palaiko daugumą žaidimo logikos – jie apima draugų posistemius, palaikymo grupes ir matchmaking.Dedicated robotai gali imituoti rungtynes, kuriose žaidėjai uždirba apdovanojimus, pažangos užduotis ir dar daugiau.


Sugebėjimas rašyti paprastą kodą leido QA komandai sukurti naujus modulius ir paprastus bandymų scenarijus.


Veiksmingumo testai yra beveik nuolatiniai - sakau beveik todėl, kad vis dar reikia juos paleisti rankiniu būdu.


Swarm padeda ne tik su testais, bet paprastai naudojamas atkurti ir išspręsti klaidas žaidimo logikoje, ypač kai tai sunku padaryti rankiniu būdu, pavyzdžiui, kai jums reikia kelių žaidimo klientų, atliekančių specialią veiksmų seką.


Apibendrinant, mes esame labai patenkinti mūsų rezultatu ir tikrai nesigailiu dėl pastangų, padarytų kuriant savo bandymų sistemą.



Tikiuosi, kad jums patiko šis straipsnis, buvo sudėtinga jums papasakoti apie visus Swarm vystymosi niuansus, o ne padaryti šį tekstą pernelyg išsipūsti.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks