229 საკითხავი

არ არსებობს ინსტრუმენტი, რომელიც შეგვიძლია გააკეთოთ ჩვენი load-testing Framework თამაშები – ასე რომ ჩვენ შეიქმნა Swarm

მიერ Andrew Rakhubov19m2025/05/20
Read on Terminal Reader

Ძალიან გრძელი; Წაკითხვა

ჩვენ საჭიროა სპეციალურ დატვირთვის ტესტირების ინსტრუმენტი, რომელიც შეუძლია მორგებული WebSocket+Protobuf პროტოლოკები, inter-bot სინქრონიზაცია და ფართო მოცულობის ტესტირება. ხელმისაწვდომი ინსტრუმენტები შეუზღუდა, ასე რომ მათ შეიქმნა Swarm: ინტეგრირებული C# Framework, სადაც "ბოტები" (სატორები) იყენებენ სინამატორები კონტროლირებული გრაფიკორები, მხარს უჭერს bot-to-bot კომუნიკაცია ბილეთების და აირჩიორები, და ავტომატური ინსტრუმენტები ყველა კოდი მეტრიკები და ტანსაცმელი. Swarm- ის დიზაინი უპირატესობთ ადვილად
featured image - არ არსებობს ინსტრუმენტი, რომელიც შეგვიძლია გააკეთოთ ჩვენი load-testing Framework თამაშები – ასე რომ ჩვენ შეიქმნა Swarm
Andrew Rakhubov HackerNoon profile picture
0-item
1-item

How We Design Swarm, ჩვენი ინტეგრირებული load-testing framework, რათა შეესაბამება მოთხოვნებს end-to-end ტესტირების, საბაჟო პროტოკები, და დიდი ზომის bot Simulations in gamedev.

How We Design Swarm, ჩვენი ინტეგრირებული load-testing framework, რათა შეესაბამება მოთხოვნებს end-to-end ტესტირების, საბაჟო პროტოკები, და დიდი ზომის bot Simulations in gamedev.


დღეს, ეს არის ნამდვილად რთული ვფიქრობ, პროგრამული პროდუქტი გარეშე მინიმუმზოგიერთიდონის ტესტირება. ერთეული ტესტირება არის სტანდარტული გზა შეკუმშვის bugs მცირე კოდი, ხოლო end-to-end ტესტირება მოიცავს მთელი განაცხადის სამუშაო ხაზები. GameDev არ არის შეზღუდვა, მაგრამ იგი მოიცავს საკუთარი უნიკალური პრობლემები, რომლებიც არ ყოველთვის შეესაბამება, თუ როგორ ჩვენ ჩვეულებრივ ტესტირება ვებ პროგრამები. ეს სტატიის შესახებ გზა ჩვენ მიიღო, რათა შეესაბამება, რომ უნიკალური საჭიროებებს ჩვენი საკუთარი შიდა ტესტირების ინსტრუმენტი.


Hello, I'm Andrey Rakhubov, Lead Software Engineer at MY.GAMES! ამ პოსტში, მე გაუზიაროს დეტალები backend გამოიყენება ჩვენი სტუდია, და ჩვენი მიმოხილვა დატვირთვა ტესტირება meta-game War Robots: Frontiers.

One Response to ჩვენი არქიტექტურა

არასამთავრობო დეტალურად, ჩვენი backend შედგება კლასიკური მომსახურების კომპლექტი და კლასიკური კონტაქტები, სადაც მოთამაშეები ცხოვრობენ.

backend architecture overview

მიუხედავად იმისა, რომ ზოგიერთი სხვა ნაკლებები, რომ შეიძლება არსებობს, ეს მიმოხილვა უკვე შესანიშნავი გადაწყვეტა ერთ-ერთი ყველაზე შეუზღუდავი პრობლემები თამაშის განვითარება: iteration სიჩქარე. თქვენ შეიძლება აღიაროთ, რომ დროს პირველი დღის Rust-lang განვითარება იყო შერჩევა, რომ დოკუმენტაცია შეიცვალა, როგორც თქვენ წაიკითხეესრა თქმა უნდა, ეს არის GDD (Game Design Document) დოკუმენტი.


მოთამაშეები ეხმარება გზა, რომ ისინი ძალიან ხელმისაწვდომია განახლება და განახლება ადრე ფუნქციონალური დიზაინი და ისინი ძალიან მარტივი refactor როგორც განსხვავებული მომსახურება, თუ საჭიროა შემდეგ.


რა თქმა უნდა, ეს შეუზღუდავი დატვირთვის და ინტეგრირების ტესტირება, რადგან თქვენ არ გაქვთ უფრო ნათელი მომსახურების განსხვავება მცირე, კარგად შეზღუდავი API-ები. გარდა ამისა, არსებობს ქსოვილის heterogeneous ღონისძიებები, სადაც არსებობს ძალიან ნაკლებად გამოხატული ან კონტროლირებული კავშირები მოთამაშეების შორის.

ტესტირება

როგორც თქვენ შეგვიძლია მიუთითოთ, ერთეული ტესტირება მოთამაშეები და მომსახურება (და load/integration ტესტირება მომსახურება) არ განსხვავდება ის, რაც თქვენ ნახავთ ნებისმიერი სხვა ტიპის პროგრამული უზრუნველყოფა. განსხვავებები ჩანს:

  • Load და Integration ტესტირება მოთამაშეები
  • End-to-end და load backend ტესტირება


ჩვენ არ ვხედავ ტესტირება სტატისტიკები დეტალურად აქ, რადგან ეს არ არის სპეციფიკური GameDev, მაგრამ დაკავშირებულია სტატისტიკა მოდელი თავს. ჩვეულებრივი მიმოხილვა არის, რომ აქვს სპეციალური ერთი ნომერი კლასტერია, რომელიც განკუთვნილია, რომ შეუწყოს მეხსიერება ერთი ტესტიში და ეს ასევე შეუძლია შეუწყოს ყველა გამოცემული მოთხოვნები და მოვუწოდოს სტატისტიკა API. ჩვენ გამოიყენებთ იგივე მიმოხილვა.


ეს თქმა უნდა, ყველაფერი იწყება უფრო საინტერესო, როდესაც ჩვენ ვართ დატვირთვა ან end-to-end ტესტირება - და ეს არის, სადაც ჩვენი ისტორია დაიწყება.


ასე რომ, ჩვენი შემთხვევაში, კლიენტული განაცხადის არის თამაში. თამაში გამოიყენებს Unreal Engine, ასე რომ კოდი წერილი C++, და სერვერზე ჩვენ გამოიყენებთ C#. მოთამაშეები ინტეგრირება UI ელემენტები თამაშში, წარმოების მოთხოვნები backend (ან მოთხოვნები გაკეთება უარყოფითი).


ამ ეტაპზე, უამრავი უმრავლესობა Frameworks უბრალოდ შეწყვიტოს მუშაობა ჩვენთვის, და ნებისმიერი ტიპის selenium-მაგული კომპლექტი, რომელიც განიხილავს კლიენტული პროგრამები ბრაუზერი არ არის გარკვეული.


შემდეგი საკითხი არის, რომ ჩვენ გამოიყენებთ საბაჟო კომუნიკაციის პროტოკოლი კლიენტების და backend შორის. ეს ნაწილი ნამდვილად ღირს განსხვავებული სტატიაში, მაგრამ მე აჩვენებთ ძირითადი კონცეფციები:

  • კომუნიკაცია მოხდება WebSocket კავშირი მეშვეობით
  • ეს არის schema-first; ჩვენ გამოიყენებთ Protobuf შეტყობინება და მომსახურების სტრუქტურა
  • WebSocket- ის შეტყობინებები Protobuf- ის შეტყობინებები, რომლებიც შეფუთულია კონტეინერში metadata- ს ერთად, რომელიც შეკუმშავს ზოგიერთი საჭირო gRPC- ს დაკავშირებული ინფორმაცია, როგორიცაა URL და headers.

ასე რომ, ნებისმიერი ინსტრუმენტი, რომელიც არ საშუალებას გაძლევთ აირჩიოთ საბაჟო პროტოკები, არ არის განკუთვნილია სამუშაოთვის.

Load ინსტრუმენტი, რათა ტესტირება მათ ყველა

ამავე დროს, ჩვენ გსურთ ერთ-ერთი ინსტრუმენტი, რომელიც შეუძლია დააყენოთ REST / gRPC ტესტები და end-to-end ტესტები ჩვენი საბაჟო პროტოკონის გამოყენებით. მას შემდეგ, რაც ყველა მოთხოვნებს, რომლებიც ტესტირება და ზოგიერთი წინასწარ შეტყობინებები შეამოწმოთ, ჩვენ გაქვთ ამ კლიენტებს:


K6LocustNBOMB

თითოეული მათთან აქვს მათი უპირატესობები და უპირატესობები, მაგრამ არსებობს რამდენიმე რამ, რომლებიც მათ შორის არ შეუძლიათ გადაიხადოს დიდი (სამდვილეში შიდა) ცვლილებები:


  • პირველი, არსებობს საჭიროება, რომელიც დაკავშირებულია inter-bot კომუნიკაციის სინქრონიზაციის მიზნით, როგორც ადრე შეხვდა.
  • მეორე, კლიენტები საკმაოდ რეაქტიული და მათი სტატისტიკა შეიძლება განსხვავდეს გარეშე ექსკლუზიური საქმიანობა ტესტი სცენარეთა; ეს იწვევს დამატებითი სინქრონიზაცია და საჭიროება გადაიხადოს ბევრი ცვლილებები თქვენი კოდი.
  • Last but not least, ეს ინსტრუმენტები ძალიან შეუზღუდავი ფოკუსირება შესრულების ტესტირება, გთავაზობთ მრავალფეროვანი ფუნქციები ნაცვლად გაზრდის დატვირთვა ან განკუთვნილია დროები, მაგრამ არ აქვს შესაძლებლობა შექმნას კომპლექსური გაფართოებული სტრატეები მარტივი გზა.


დროა მიიღოს ფაქტი, რომ ჩვენ ნამდვილად საჭიროა სპეციალურ ინსტრუმენტი. ეს არის, თუ როგორ ჩვენი ინსტრუმენტი -Swarmდა დასაწყისი

Swarm

მაღალი დონეზე, Swarm- ის სამუშაოა, რათა დაწყოს ბევრი მოთამაშეებს, რომლებიც ქმნის სერვერზე. ეს მოთამაშეებს აცხადებენ ბოტებს, და მათ შეუძლიათ შეიმუშავოთ კლიენტების ქცევა ან სპეციფიკაციური სერვერზე ქცევა. Swarm შედგება ერთი ან რამდენიმე მოთამაშეებს, რომლებიც ამ ბოტებს და კომუნიკაცია ერთად.


More formally, აქ არის სია მოთხოვნები ინსტრუმენტი:


  1. Reaction to state updates უნდა იყოს მარტივი
  2. კონკურენტი არ უნდა იყოს პრობლემა
  3. კოდი უნდა იყოს ავტომატურად instrumentable
  4. Bot-to-bot კომუნიკაცია უნდა იყოს მარტივი
  5. Multiple instances უნდა იყოს joinable, რათა შექმნათ საკმარისი load
  6. ინსტრუმენტი უნდა იყოს მსუბუქი და შეუძლია შექმნას საკმაოდ წნევა backend itself


როგორც ბონუს, მე ასევე დაამატა ზოგიერთი დამატებითი ნაბიჯები:

  1. ორივე ეფექტურობის და End-to-End ტესტი სცენარეთა უნდა იყოს შესაძლებელი
  2. ინსტრუმენტი უნდა იყოს სატრანსპორტო agnostic; ჩვენ უნდა იყოს შეუძლიათ დააყენოთ ეს ნებისმიერი სხვა შესაძლებელი სატრანსპორტო, თუ საჭიროა
  3. ინსტრუმენტი მოიცავს imperative კოდი სტილი, რადგან მე პირდაპირი ვფიქრობ, რომ declarative სტილი არ არის განკუთვნილია კომპლექსური სტრატეები მოდული გადაწყვეტილებები
  4. ბოტებს უნდა იყოს შეუძლიათ არსებობს განსხვავებით ტესტირების ინსტრუმენტი, რაც იმას ნიშნავს, რომ არ უნდა იყოს მძიმე დამოკიდებულებები


ჩვენი მიზანი

ვფიქრობთ, რომ კოდი, რომ ჩვენ გსურთ წაიკითხოთ; ჩვენ ვფიქრობთ, რომ ბოტი, როგორც ფანჯარა, და ის არ შეუძლია გააკეთოს რამ თავს, მას შეუძლია მხოლოდ შენარჩუნება უბრავი, ხოლო Scenario არის ფანჯარა, რომელიც წაიკითხა string.


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));
    }
}


Steaming

Backend დატოვებს ბევრი განახლებები კლიენტს, რომელიც შეიძლება მოხდეს მოდული; ზედაპირზე მაგალითად, ერთ-ერთი ასეთი მოვლენები არისGroupInviteAddedEventRobot უნდა იყოს შეუძლიათ ორივე რეაგირება ამ მოვლენებს ინტენსიურად და გაძლევთ შესაძლებლობა, რომ იხილოთ მათ გარე კოდი.


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


მიუხედავად იმისა, რომ კოდი არის საკმაოდ მარტივი (და როგორც თქვენ შეიძლება მივიღოთOnGroupStateUpdateეს არის მხოლოდ ხანგრძლივი გადახდის შემთხვევაში), ბევრი ხდება აქ.


StreamSubscriptionეს არის საკუთარი aIObservable<ObservedEvent<TStreamMessage>>

და ეს უზრუნველყოფს სასარგებლო გაფართოება, აქვს დამოკიდებული სიცოცხლის ციკლი, და მოიცავს მეტრიკები.

კიდევ ერთი უპირატესობა: ეს არ მოითხოვს სპეციფიკური სინქრონიზაცია, რათა წაიკითხოთ ან შეცვალოს სტატისტიკა, მიუხედავად იმისა, რომ მენეჯერი განახლებულია.


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


კონკურენტი არ უნდა იყოს პრობლემა

ზემოთ, კოდი დარეგისტრირებულია როგორც ხაზიური, ერთი ხაზის კოდი. იდეა არის მარტივი: არასოდეს აწარმოეთ კოდი, რომელიც ერთდროულად მოიცავს სცენარეთა ან ამ სცენარში მოპოვებული ბოტი, და გარდა ამისა, ბოტი / მოდულების შიდა კოდი არ უნდა აწარმოოს ერთდროულად ბოტის სხვა ნაწილებთან.


ეს არის მსგავსი წაიკითხეთ / წაიკითხეთ კურსი, სადაც კურსი კურსი წაიკითხა (დაზიდული ხელმისაწვდომობა) და ბოტ კურსი წაიკითხა (სხვეწილი ხელმისაწვდომობა). და მიუხედავად იმისა, რომ ჩვენი მიზანია შეიძლება გააკეთა ამ ტიპის კურსი, არსებობს უკეთესი გზა.

Task Scheduler და სინქრონიზაციის კონტაქტი

ორი მექანიზმები, რომლებიც ამ სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიო სატელევიზიოDispatcherSynchronizationContextეს არის პასუხისმგებლობა ელეგანტურად გადაცემის თქვენი async calls უკან UI thread.


ჩვენი სტრატეგია, ჩვენ არ დაინტერესებთ thread affinity, და ვიდრე უფრო დაინტერესებთ შესრულების ნაბიჯები სტრატეგია.


წინასწარ დასაწყისში დაბალი დონეზე კოდი წაიკითხეთ ერთ-ერთი არ ცნობილი კლასის სახელწოდებით.ConcurrentExclusiveSchedulerPair• საწყისიდოკუმენტი:

დოკუმენტი


გთავაზობთ საქმიანობის გრაფიკებს, რომლებიც აკონტროლებს საქმიანობის გაკეთება, ხოლო უზრუნველყოფს, რომ ერთობლივი საქმიანობა შეიძლება გაკეთდეს ერთდროულად და ექსკლუზიური საქმიანობა არ გაკეთება.

გთავაზობთ საქმიანობის გრაფიკებს, რომლებიც აკონტროლებს საქმიანობის გაკეთება, ხოლო უზრუნველყოფს, რომ ერთობლივი საქმიანობა შეიძლება გაკეთდეს ერთდროულად და ექსკლუზიური საქმიანობა არ გაკეთება.


ახლა ჩვენ უნდა უზრუნველყოს, რომ ყველა კოდი სინამდვილეში გააქტიურებაConcurrentSchedulerროდესაც ბოსტის კოდი აწარმოებსExclusiveScheduler.


ერთი ვარიანტი არის, რომ გამოქვეყნოს იგი როგორც პარამეტრი, რომელიც გაკეთდება, როდესაც სცენარი დაიწყება:


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();
}


ესRunScenarioInstanceMethod, ამავე დროს, აცხადებს, რომSetUpდაRunკონფიდენციალურობის გადაწყვეტილებები, მათ შორის კონფიდენციალურობის გადაწყვეტილებები (ScenarioSchedulerეს არის კონკურენტული ნაწილიConcurrentExclusiveSchedulerPair) და

Now, when we are inside the scenario code and we do this...


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();
}

...async სტატისტიკა მანქანა გააკეთებს მისი მუშაობა ჩვენთვის, შენარჩუნების გრაფიკული ჩვენი სამუშაოები.

ახლაSendGroupInviteიგი ასევე აწარმოებს concurrent გრაფიკორზე, ასე რომ ჩვენ იგივე ტრიკს აწარმოებთ, ასევე:


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 abstraction შეფუთვა გრაფიკაცია იგივე გზა, როგორც დასაწყისში, callingTask.Factory.StartNewსწორი გრაფიკით

კოდი Generation

OK, ახლა ჩვენ უნდა manually შეფუთვა ყველაფერი შიდაDoდა მიუხედავად იმისა, რომ ეს გადაიხადოს პრობლემა, ეს არის შეცდომა, ადვილად დაგავიწყება, და ზოგადად, კარგად, ეს ხდის ცუდი.

ვხედავ ამ ნაწილი კოდი კიდევ ერთხელ:


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


აქ, ჩვენი bot აქვს aGroupქონებაGroupარის მოდული, რომელიც არსებობს მხოლოდ კოდის გაზიანება სხვადასხვა კლასებში და თავიდან ავიცილოთ გაფართოებაBotკლასის


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


ვფიქრობ, რომ მოდულზე უნდა იყოს ინტერფეისი:


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;
   }
|


და ახლა ეს უბრალოდ მუშაობს! არ არის მეტი შეფუთვა და ცუდი კოდი, უბრალოდ მარტივი მოთხოვნები (არ არის საკმაოდ პოპულარული განვითარებლები) შექმნა ინტერფეისი თითოეული მოდულზე.


რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, რა თქმა უნდა, ეს არის.Runtime.Doტელეფონი :


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


ინსტრუმენტები

შემდეგი ნაბიჯი არის კოდის ინსტრუმენტაცია, რათა შეინახოთ მეტრიკები და ტანსაცმელი. პირველი, თითოეული API call უნდა შეამოწმოთ. ეს ნაწილი არის ძალიან მარტივი, რადგან ჩვენი გადაზიდვა ეფექტურად აჩვენებს, რომ არის gRPC კანელი, ასე რომ ჩვენ უბრალოდ გამოიყენებთ სწორი წერილი interceptor...


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


→ სადაცCallInvokerარის gRPC abstraction of client-side RPC invocation.


შემდეგი, ეს იქნება დიდი, რომ ზოგიერთი ნაწილები კოდი, რათა შეამციროს შესრულება. ამ მიზეზით, თითოეული მოდული ინექციებიIInstrumentationFactoryშემდეგი ინტერფეისი:


public interface IInstrumentationFactory

{

   public IInstrumentationScope CreateScope(

       string? name = null,

       IInstrumentationContext? actor = null,

       [CallerMemberName] string memberName = "",

       [CallerFilePath] string sourceFilePath = "",

       [CallerLineNumber] int sourceLineNumber = 0);

}


ახლა თქვენ შეგიძლიათ შეფუთვა ნაწილები, რომ თქვენ დაინტერესებული:


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


Looks familiar, right? While you can still use this feature to create sub-scopes, every module proxy method defines a scope automatically:


   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$));
       }
   }
";


ინსტრუმენტების მოცულობა დარეგისტრირებს შესრულების დრო, შეზღუდვები, შექმნათ გაფართოებული ტანსაცმელი, შეუძლია დარეგისტრირებას და აწარმოებს მრავალი სხვა კონფიგურაცია შესაძლებელი რამ.


Bot-to-bot კომუნიკაცია

მაგალითად, “ ჩვენი მიზანი” ნაწილში არ არის რეალური კომუნიკაცია. ჩვენ უბრალოდ იცით, რომ შემდეგი ბოტიPlayerIdიმიტომ, რომ ეს არის სხვა ადგილობრივი ვარიანტი. მიუხედავად იმისა, რომ ეს სტრატეგიული წერილის სტრატეგიები ხშირად მარტივი და პირდაპირი, არსებობს უფრო რთული შემთხვევაში, სადაც ეს მიმოხილვა უბრალოდ არ მუშაობს.


დასაწყისში მე გსურთ გააქტიუროთ ზოგიერთი შავი პლატფორმა, რომელიც შეიცავს კომუნიკაციისთვის ერთობლივი Key-value შენახვა (გალითად, Redis), მაგრამ კონცეფცია სინამაზის ტესტირების გამოცდილების შემდეგ, გამოჩნდა, რომ მუშაობის რაოდენობა შეიძლება ძალიან შეზღუდული იყოს ორი მარტივი კონცეფცია: ხაზები და შერჩევა.

ბილეთები - Tickets

რეალური მსოფლიოში, მოთამაშეები კომუნიკაციის ერთად ერთ-ერთი გზა – ისინი წაიკითხე პირდაპირი შეტყობინებები ან გამოიყენოთ საუბუქი ჩატი. ბოტზე, ჩვენ არ უნდა შედუღებოდეს, რომ granularity, და ჩვენ უბრალოდ უნდა კომუნიკაციის მიზნები. ასე რომ, ვიდრე “Hey Killer2000, გთხოვთ, გთხოვთ, რომ მე თქვენი ჯგუფი” ჩვენ უნდა მარტივი “Hey,ვინმესგთხოვთ, მოვუწოდეთ მე ჯგუფიში”. აქ არის, სადაც ბილეთები ითამაშებენ:


public interface ISwarmTickets
{
   Task PlaceTicket(SwarmTicketBase ticket);


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


ბოტი შეუძლია დააყენოთ ბილეთი, და შემდეგ სხვა ბოტი შეუძლია მიიღოს ის ბილეთი.ISwarmTicketsარ არის არაფერი სხვა, ვიდრე შეტყობინება ბროკერზე, რომელიც შეუწყობსticketType(სასრულად ეს არისმცირეუფრო მეტია, რადგან არსებობს დამატებითი ვარიანტი, რათა თავიდან აცილოთ ბოტებს მათი საკუთარი ბილეთების მიღება, ისევე როგორც სხვა მცირე ტრიპები).


ამ ინტერფეისი, ჩვენ შეგვიძლია საბოლოოდ გაზიაროთ მაგალითად სცენარეთა ორი დამოუკიდებელი სცენარეთა. (თე, ყველა დამატებითი კოდი იღებს ძირითადი იდეა):


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);
}


შერჩევა

ჩვენ გვაქვს ორი განსხვავებული ქცევა, ერთ-ერთი ლიდერი, ერთ-ერთი მუდმივი. რა თქმა უნდა, ისინი შეიძლება გათავისუფლდეს ორი განსხვავებული სტრატეგიებში და ჩვეულებრივ იწყება. ზოგჯერ, ეს არის საუკეთესო გზა, სხვა დროს თქვენ შეიძლება უნდა დინამიური კონფიგურაცია ჯგუფი ზომის (მაკონტალური მუდმივი თითოეული ლიდერი) ან ნებისმიერი სხვა სიტუაცია გათავისუფლებული / აირჩიოთ სხვადასხვა მონაცემები / როლები.


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);
}


აქRoundRobinRoleეს არის უბრალოდ ფანტასტიკური შეფუთვა საერთო კონტაქტი და modulo ოპერაცია, რათა აირჩიოთ სწორი ელემენტი სიაში.


Cluster of Swarms-ის მოვლენები

ახლა, მას შემდეგ, რაც ყველა კომუნიკაცია შეშფოთებულია დახვეწილი კონტაქტორები, ის იღებს მინიმალურია, რომ დააყენოთ ან ანკასტრატორი ნომერი, ან გამოიყენოთ ზოგიერთი ხელმისაწვდომი MQ და KV შენახვა.


ფსიქიკური ფაქტი: ჩვენ არასდროს არ დასრულდა ამ ფუნქციონირება. როდესაც QA მიიღო მათი ხელები ერთი ნომერი განახლება SwarmAgent, ისინი დაუყოვნებლივ დაიწყო უბრალოდ გააყენოთ მრავალჯერადი ინტენსიები დამოუკიდებელი მენეჯერი, რათა გაზრდის დატვირთვა. ჩანს, ეს იყო მეტი, ვიდრე საკმარისი.

Single Instance შესრულება

რა არის ეფექტურობის შესახებ? რა არის დაკარგული ყველა ამ შეფუთვა და შედუღებადი სინკრონიზაცია? მე არ გადატვირთვა თქვენ ყველა სხვადასხვა ტესტიების მიერ, მხოლოდ ყველაზე მნიშვნელოვანი ორი, რომელიც აჩვენა, რომ სისტემა შეუძლია შექმნას საკმარისი კარგი დატვირთვა.

Benchmark სტრუქტურა:

BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.4651/22H2/2022Update)
Intel Core i7-10875H CPU 2.30GHz, 1 CPU, 16 ლოგიკური და 8 ფიზიკური კედლები
.NET SDK 9.0.203 გაფართოება
[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);
        }
    }
}


პირველ რიგში, ვხედავ თითქმის სუფთა overhead გარიგების შედარებით მარტივი Task spawning.

ამ შემთხვევაში, ორივე სატვირთო მეთოდები არის შემდეგი:


private ValueTask CounterLoadMethod(int i)

{

   Interlocked.Increment(ref StaticControl.Counter);

   return ValueTask.CompletedTask;

}


შედეგები Iterations = 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

მარტივი

10

5653 კმ

0 0 0 0 0 0 0

0 0 0 0 0 0 0

0.3433

-

-

2.8 კგ

მარტივი

100

30281 წლამდე

0.2720 წლამდე

0.2544 ის

3.1128

0.061

-

5.7 კგ

მარტივი

1000

250693ს

2.162 სათაური

2.4037 იაფი

30.2734

5.8594

-

250.67 კბ

გრაფიკი

10

40 629 წლამდე

0784 კუნძული

0 0 0 0 0 0

8.1787

0.1221

-

615 კგ

გრაფიკი

100

35386 წლამდე

2.3414ს

1901 წელს

81.0547

14.6484

-

62.09 კბ

გრაფიკი

1000

4.685.812 სუნი

24.7917 იაფი

21972 იაფი

812.5

375

-

6617.59 კბ



არ არის საკმარისი. სანამ ნამდვილად გააკეთა ტესტი, მე მოითხოვს ბევრი უკეთესი შესრულება, მაგრამ შედეგები ნამდვილად შეუზღუდავი. უბრალოდ ფიქრობთ, თუ რამდენად ხდება ქვეშ კაბა და უპირატესობები ჩვენ მიიღო მხოლოდ ~4us თითო პარამეტრი instance. ნებისმიერ შემთხვევაში, ეს არის მხოლოდ იმიტომ, რომ overhead; ჩვენ სასარგებლო ვართ ბევრი უფრო პრაქტიკული ნიმუში.


რა შეიძლება იყოს რეალური ცუდი შემთხვევაში ტესტი სცენარი? რა არის ფუნქცია, რომელიც არაფერი, ვიდრე API call? ერთ-ერთი მხრივ, თითქმის ყველა მეთოდი bot გააკეთებს, მაგრამ მეორე მხრივ, თუ არაფერი, ვიდრე მხოლოდ API calls, მაშინ ყველა სინქრონიზაციის ეფექტები ცუდი, არაფერი?


Load Method უბრალოდ მოვუწოდებსPingAsyncადვილად, RPC კლიენტებს სინათლისგან შეინახება.


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


აქ არის შედეგები, კიდევ 10 iterations (grpc სერვერზე არის ადგილობრივი ქსელში):


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

მარტივი

100

94.45 მმ

1.804 მმ

2.148 მმ

600

200

6.14 მმ

მარტივი

1000

69.99 მმ

15.592 მმ

45 730 მმ

9000

7000

77.7 კმ

გრაფიკი

100

95.48 მმ

1547 მმ

1.292 მმ

833.3333

333.3333

5.85 კმ

გრაფიკი

1000

625.52 მმ

14,697 მმ

24405 მმ

8000

7000

68.57 კმ


როგორც მოითხოვს, ეფექტურობის ეფექტი რეალურ სამუშაო სინამდვილეში არ არის შეუზღუდავი.


ანალიზი

Naturally, there are backend logs, metrics, and traces that provide a good view of what’s happening during loading. მაგრამ Swarm აპირებს ერთი ნაბიჯზე მეტი, წაიკითხოს საკუთარი მონაცემები – ზოგჯერ დაკავშირებული backend – შეამციროს იგი.


აქ არის მაგალითია რამდენიმე მეტრიკების ცუდი ტესტიში, რომელიც იზრდება მოვლენების რაოდენობით. ტესტი არ გააკეთა მრავალჯერადი SLA-ის შეზღუდვის გამო (იხილეთ API-ის მოვლენების დროები), და ჩვენ გვაქვს ყველა ინსტრუმენტაცია, რომელიც საჭიროა, რათა შეამოწმოთ, რა ხდება კლიენტების პრაქტიკაში.


Grafana dashboard panels showing failed test


ჩვეულებრივ, ჩვენ შეჩერებთ ტესტი მთლიანად გარკვეული რაოდენობის შეცდომების შემდეგ, ამიტომ API call lines დასრულდება ნაცვლად.


რა თქმა უნდა, რკინიგზები საჭიროა კარგი მოვლენებისთვის. Swarm- ის ტესტირებისთვის კლიენტების რკინიგზები დაკავშირებულია backend- ის რკინიგზებით ერთ-ერთი ხეზე. სპეციალური რკინიგზები შეიძლება იპოვდესconnectionIdD/botIdრომელიც დაგეხმარებათ debugging.


Distributed tracing connecting Swarm scenario with backend


შენიშვნა: DevServer არის მოსახერხებელი monolith build all-in-one backend მომსახურება, რომელიც დაიწყება Developer PC, და ეს არის სპეციალურად შექმნილია მაგალითია, რათა შეამციროს რაოდენობის დეტალები ეკრანზე.


Swarm bots წაიკითხე სხვა სახის წაიკითხე: ისინი იმიტომ, რომ წაიკითხე გამოიყენება ეფექტურობის ანალიზი. დახმარებით ხელმისაწვდომი ინსტრუმენტები, ეს წაიკითხე შეიძლება ნახოთ და ანალიზი გამოყენებით ინტეგრირებული შესაძლებლობები, როგორიცაა PerfettoSQL.


Adapting perf logs to show bot execution flow

რა დასაწყისში მივიღე

ჩვენ ახლა გვაქვს SDK, რომელიც ორივე GameClient და Dedicated Server ბოტები მზადდება. ეს ბოტები ახლა მხარს უჭერს ყველაზე თამაშის ლოგიკას – მათ შორის მეგობრები დისზისტენტები, მხარდაჭერა ჯგუფიები და matchmaking. Dedicated ბოტები შეუძლია იმიტომ, რომ მოთამაშეებს იღებს საფასური, პროგრესის მიზნები და ბევრი სხვა.


მას შემდეგ, რაც შესაძლებელია მარტივი კოდი შექმნა, შესაძლებელია ახალი მოდულების შექმნა და მარტივი ტესტი სცენარეთა მიერ QA გუნდი. არსებობს იდეა FlowGraph სცენარეთა (ვუზიური პროგრამირება), მაგრამ ეს ჯერ კიდევ მხოლოდ იდეა.


ეფექტურობის ტესტიები თითქმის მუდმივად არიან - მე ვფიქრობ, თითქმის, რადგან ჯერ კიდევ უნდა დაიწყოს მათ manually.


Swarm არ მხოლოდ დაგეხმარება ტესტირების, მაგრამ ხშირად გამოიყენება reproducing და შეცვალოს bugs თამაშის ლოგიკაში, განსაკუთრებით როდესაც ეს არის რთული გააკეთოთ manually, როგორიცაა, როდესაც თქვენ უნდა რამდენიმე თამაშის კლიენტები შესრულების სპეციალური სერვისი ოპერაციები.


შეტყობინება, ჩვენ ძალიან კმაყოფილებული ვართ ჩვენი შედეგად და დარწმუნებული ვართ, რომ არ გვაქვს შეტყობინებები ჩვენი საკუთარი ტესტირების სისტემის განვითარება.



მე ვფიქრობ, რომ თქვენ გაქვთ ამ სტატიაში, ეს იყო განიცდიან, რომ განიცდიან ყველა შანსი Swarm განვითარების გარეშე, რომ ეს ტექსტი ძალიან გაფართოებული. მე დარწმუნებული ვარ, რომ მე შეიძლება შეამციროს ზოგიერთი მნიშვნელოვანი დეტალები პროცესში შეესაბამება ტექსტის ზომა და ინფორმაცია, მაგრამ მე ბედნიერი გთავაზობთ მეტი კონტექსტი, თუ თქვენ გაქვთ რაიმე კითხვები!

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks