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 შედგება კლასიკური მომსახურების კომპლექტი და კლასიკური კონტაქტები, სადაც მოთამაშეები ცხოვრობენ.
მიუხედავად იმისა, რომ ზოგიერთი სხვა ნაკლებები, რომ შეიძლება არსებობს, ეს მიმოხილვა უკვე შესანიშნავი გადაწყვეტა ერთ-ერთი ყველაზე შეუზღუდავი პრობლემები თამაშის განვითარება: 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 ტესტები ჩვენი საბაჟო პროტოკონის გამოყენებით. მას შემდეგ, რაც ყველა მოთხოვნებს, რომლებიც ტესტირება და ზოგიერთი წინასწარ შეტყობინებები შეამოწმოთ, ჩვენ გაქვთ ამ კლიენტებს:
თითოეული მათთან აქვს მათი უპირატესობები და უპირატესობები, მაგრამ არსებობს რამდენიმე რამ, რომლებიც მათ შორის არ შეუძლიათ გადაიხადოს დიდი (სამდვილეში შიდა) ცვლილებები:
- პირველი, არსებობს საჭიროება, რომელიც დაკავშირებულია inter-bot კომუნიკაციის სინქრონიზაციის მიზნით, როგორც ადრე შეხვდა.
- მეორე, კლიენტები საკმაოდ რეაქტიული და მათი სტატისტიკა შეიძლება განსხვავდეს გარეშე ექსკლუზიური საქმიანობა ტესტი სცენარეთა; ეს იწვევს დამატებითი სინქრონიზაცია და საჭიროება გადაიხადოს ბევრი ცვლილებები თქვენი კოდი.
- Last but not least, ეს ინსტრუმენტები ძალიან შეუზღუდავი ფოკუსირება შესრულების ტესტირება, გთავაზობთ მრავალფეროვანი ფუნქციები ნაცვლად გაზრდის დატვირთვა ან განკუთვნილია დროები, მაგრამ არ აქვს შესაძლებლობა შექმნას კომპლექსური გაფართოებული სტრატეები მარტივი გზა.
დროა მიიღოს ფაქტი, რომ ჩვენ ნამდვილად საჭიროა სპეციალურ ინსტრუმენტი. ეს არის, თუ როგორ ჩვენი ინსტრუმენტი -Swarmდა დასაწყისი
Swarm
მაღალი დონეზე, Swarm- ის სამუშაოა, რათა დაწყოს ბევრი მოთამაშეებს, რომლებიც ქმნის სერვერზე. ეს მოთამაშეებს აცხადებენ ბოტებს, და მათ შეუძლიათ შეიმუშავოთ კლიენტების ქცევა ან სპეციფიკაციური სერვერზე ქცევა. Swarm შედგება ერთი ან რამდენიმე მოთამაშეებს, რომლებიც ამ ბოტებს და კომუნიკაცია ერთად.
More formally, აქ არის სია მოთხოვნები ინსტრუმენტი:
- Reaction to state updates უნდა იყოს მარტივი
- კონკურენტი არ უნდა იყოს პრობლემა
- კოდი უნდა იყოს ავტომატურად instrumentable
- Bot-to-bot კომუნიკაცია უნდა იყოს მარტივი
- Multiple instances უნდა იყოს joinable, რათა შექმნათ საკმარისი load
- ინსტრუმენტი უნდა იყოს მსუბუქი და შეუძლია შექმნას საკმაოდ წნევა backend itself
როგორც ბონუს, მე ასევე დაამატა ზოგიერთი დამატებითი ნაბიჯები:
- ორივე ეფექტურობის და End-to-End ტესტი სცენარეთა უნდა იყოს შესაძლებელი
- ინსტრუმენტი უნდა იყოს სატრანსპორტო agnostic; ჩვენ უნდა იყოს შეუძლიათ დააყენოთ ეს ნებისმიერი სხვა შესაძლებელი სატრანსპორტო, თუ საჭიროა
- ინსტრუმენტი მოიცავს imperative კოდი სტილი, რადგან მე პირდაპირი ვფიქრობ, რომ declarative სტილი არ არის განკუთვნილია კომპლექსური სტრატეები მოდული გადაწყვეტილებები
- ბოტებს უნდა იყოს შეუძლიათ არსებობს განსხვავებით ტესტირების ინსტრუმენტი, რაც იმას ნიშნავს, რომ არ უნდა იყოს მძიმე დამოკიდებულებები
ჩვენი მიზანი
ვფიქრობთ, რომ კოდი, რომ ჩვენ გსურთ წაიკითხოთ; ჩვენ ვფიქრობთ, რომ ბოტი, როგორც ფანჯარა, და ის არ შეუძლია გააკეთოს რამ თავს, მას შეუძლია მხოლოდ შენარჩუნება უბრავი, ხოლო 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 დატოვებს ბევრი განახლებები კლიენტს, რომელიც შეიძლება მოხდეს მოდული; ზედაპირზე მაგალითად, ერთ-ერთი ასეთი მოვლენები არისGroupInviteAddedEvent
Robot უნდა იყოს შეუძლიათ ორივე რეაგირება ამ მოვლენებს ინტენსიურად და გაძლევთ შესაძლებლობა, რომ იხილოთ მათ გარე კოდი.
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();
}
ესRunScenarioInstance
Method, ამავე დროს, აცხადებს, რომ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-ის მოვლენების დროები), და ჩვენ გვაქვს ყველა ინსტრუმენტაცია, რომელიც საჭიროა, რათა შეამოწმოთ, რა ხდება კლიენტების პრაქტიკაში.
ჩვეულებრივ, ჩვენ შეჩერებთ ტესტი მთლიანად გარკვეული რაოდენობის შეცდომების შემდეგ, ამიტომ API call lines დასრულდება ნაცვლად.
რა თქმა უნდა, რკინიგზები საჭიროა კარგი მოვლენებისთვის. Swarm- ის ტესტირებისთვის კლიენტების რკინიგზები დაკავშირებულია backend- ის რკინიგზებით ერთ-ერთი ხეზე. სპეციალური რკინიგზები შეიძლება იპოვდესconnectionIdD/botId
რომელიც დაგეხმარებათ debugging.
შენიშვნა: DevServer არის მოსახერხებელი monolith build all-in-one backend მომსახურება, რომელიც დაიწყება Developer PC, და ეს არის სპეციალურად შექმნილია მაგალითია, რათა შეამციროს რაოდენობის დეტალები ეკრანზე.
Swarm bots წაიკითხე სხვა სახის წაიკითხე: ისინი იმიტომ, რომ წაიკითხე გამოიყენება ეფექტურობის ანალიზი. დახმარებით ხელმისაწვდომი ინსტრუმენტები, ეს წაიკითხე შეიძლება ნახოთ და ანალიზი გამოყენებით ინტეგრირებული შესაძლებლობები, როგორიცაა PerfettoSQL.
რა დასაწყისში მივიღე
ჩვენ ახლა გვაქვს SDK, რომელიც ორივე GameClient და Dedicated Server ბოტები მზადდება. ეს ბოტები ახლა მხარს უჭერს ყველაზე თამაშის ლოგიკას – მათ შორის მეგობრები დისზისტენტები, მხარდაჭერა ჯგუფიები და matchmaking. Dedicated ბოტები შეუძლია იმიტომ, რომ მოთამაშეებს იღებს საფასური, პროგრესის მიზნები და ბევრი სხვა.
მას შემდეგ, რაც შესაძლებელია მარტივი კოდი შექმნა, შესაძლებელია ახალი მოდულების შექმნა და მარტივი ტესტი სცენარეთა მიერ QA გუნდი. არსებობს იდეა FlowGraph სცენარეთა (ვუზიური პროგრამირება), მაგრამ ეს ჯერ კიდევ მხოლოდ იდეა.
ეფექტურობის ტესტიები თითქმის მუდმივად არიან - მე ვფიქრობ, თითქმის, რადგან ჯერ კიდევ უნდა დაიწყოს მათ manually.
Swarm არ მხოლოდ დაგეხმარება ტესტირების, მაგრამ ხშირად გამოიყენება reproducing და შეცვალოს bugs თამაშის ლოგიკაში, განსაკუთრებით როდესაც ეს არის რთული გააკეთოთ manually, როგორიცაა, როდესაც თქვენ უნდა რამდენიმე თამაშის კლიენტები შესრულების სპეციალური სერვისი ოპერაციები.
შეტყობინება, ჩვენ ძალიან კმაყოფილებული ვართ ჩვენი შედეგად და დარწმუნებული ვართ, რომ არ გვაქვს შეტყობინებები ჩვენი საკუთარი ტესტირების სისტემის განვითარება.
მე ვფიქრობ, რომ თქვენ გაქვთ ამ სტატიაში, ეს იყო განიცდიან, რომ განიცდიან ყველა შანსი Swarm განვითარების გარეშე, რომ ეს ტექსტი ძალიან გაფართოებული. მე დარწმუნებული ვარ, რომ მე შეიძლება შეამციროს ზოგიერთი მნიშვნელოვანი დეტალები პროცესში შეესაბამება ტექსტის ზომა და ინფორმაცია, მაგრამ მე ბედნიერი გთავაზობთ მეტი კონტექსტი, თუ თქვენ გაქვთ რაიმე კითხვები!