გადასვლა შიგთავსზე
როგორ დავზოგეთ 100 ტერაბაიტი მეხსიერება 1.1.1.1-ის DNS ქეშის ოპტიმიზაციით
CloudflareNetworkingDevelopment12 წთ

როგორ დავზოგეთ 100 ტერაბაიტი მეხსიერება 1.1.1.1-ის DNS ქეშის ოპტიმიზაციით

Big Pineapple , პლატფორმა 1.1.1.1 -ის, Gateway DNS -ის, DNS Firewall -ის, AS112 -ისა და Cloudflare-ის რამდენიმე სხვა DNS სერვისის უკან, ნებისმიერ მომენტში ინახავს 250 მილიარდზე მეტ DNS ქეშ-ჩანაწერს. ამ მასშტაბზე, თითო ჩანაწერზე თუნდაც ერთი ბაიტის ფუჭად კარგვა ჩვენს მთელ ინფრასტრუქტურაში 250…

გაზიარება

Big Pineapple, პლატფორმა 1.1.1.1-ის, Gateway DNS-ის, DNS Firewall-ის, AS112-ისა და Cloudflare-ის რამდენიმე სხვა DNS სერვისის უკან, ნებისმიერ მომენტში ინახავს 250 მილიარდზე მეტ DNS ქეშ-ჩანაწერს. ამ მასშტაბზე, თითო ჩანაწერზე თუნდაც ერთი ბაიტის ფუჭად კარგვა ჩვენს მთელ ინფრასტრუქტურაში 250 გიგაბაიტზე მეტ მეხსიერებას უდრის.

მეხსიერებაში ქეშ-ჩანაწერების შენახვის ხუთმა თანმიმდევრულმა ცვლილებამ თითო ჩანაწერის მოცულობა 50%-ზე მეტით შეამცირა. მთელ ჩვენს სერვერულ პარკში ამ ცვლილებებმა დაახლოებით 100 ტერაბაიტი მეხსიერება გამოათავისუფლა, რაც ჩვენი 130 Gen 13 სერვერის ოპერატიული მეხსიერების (RAM) ჯამურ მოცულობას უტოლდება. ქეში ასევე უფრო სწრაფი გახდა. ჩაწერის გამტარუნარიანობა გაიზარდა 43%-ით, ხოლო ძიების დაყოვნება შემცირდა 19%-ით, რადგან ნაკლებმა გამოყოფამ (allocation) და მეხსიერების უკეთესმა ლოკალურობამ მოგვცა საშუალება, რომ სივრცე სისწრაფის ხარჯზე არ დაგვეზოგა.

რას ვინახავთ ქეშში

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

ქეშის ზუსტი ზომა მონაცემთა ცენტრების მიხედვით განსხვავდება. როდესაც გამოიყენება EDNS Client Subnet (ECS), ავტორიტეტული სერვერები კლიენტის ქსელის მიხედვით სხვადასხვა პასუხს აბრუნებენ, ამიტომ ჩვენ ერთი და იმავე მოთხოვნის რამდენიმე ვერსიას ვაქეშირებთ. ეს ზრდის როგორც ჩანაწერების რაოდენობას, ასევე თითოეულის მიერ მოხმარებულ მეხსიერებას, რის გამოც ამ პოსტში აღწერილი ოპტიმიზაციები განსაკუთრებით ეფექტურია ECS-ით დატვირთული ლოკაციებისთვის.

ქეშში თითოეული ელემენტი წარმოადგენს გასაღები-მნიშვნელობის (key-value) წყვილს. გასაღები განსაზღვრავს იმას, თუ რაზე გაიგზავნა მოთხოვნა:

bash
pub struct CacheKey {
    qname: Name,
    qtype: Rtype,
    authenticated: bool,
    tag: Vec<u8>,
}

მნიშვნელობა ინახავს თავად DNS პასუხს: პასუხის (answer), ავტორიტეტის (authority) და დამატებითი ჩანაწერების (additional) სექციებს, ისეთ მეტამონაცემებთან ერთად, როგორიცაა შექმნის დრო, მოთხოვნათა მთვლელი (hit counter) და სიცოცხლის ხანგრძლივობა (TTL).

bash
pub struct CacheEntry {
    timestamp: UnixTimeStamp,
    pub inception: Instant,
    pub ttl: Ttl,
    pub hits: u32,
    pub answers: Vec<Record>,
    pub authority: Vec<Record>,
    pub additional: Vec<Record>,
    pub errors: Vec<ExtendedError>,
    ...
}

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

მეხსიერების გამოყენების ბენჩმარკინგი

თითოეული ცვლილების ეფექტის გასაზომად, ჩვენ ვახორციელებთ ბენჩმარკინგს ქეშის შემთხვევით გენერირებული ჩანაწერებით შევსებით, რომლებიც მიახლოებით ემთხვევა რეალურ პროდაქშენ ტრაფიკს: 56% A ჩანაწერები, 25% AAAA და 19% TXT. თითოეული ჩანაწერი შეიცავს ერთიდან ოთხამდე რეკორდს.

TXT ჩანაწერები ბენჩმარკში გამოიყენება ყველა არა-A/AAAA ჩანაწერის ტიპის შემცვლელად. მათი ზომა შემთხვევითად მერყეობს 64-დან 224 ბაიტამდე, რაც ახლოსაა ცვლადი სიგრძის ჩანაწერების ტიპებისთვის დაფიქსირებულ საშუალო პასუხის ზომასთან.

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

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

ტევადობის (Capacity) ფასი

Vec<T> ინახავს სამ ველს: მაჩვენებელს (pointer) ჰიპზე (heap) გამოყოფილ მონაცემზე, მიმდინარე სიგრძეს და მთლიან ტევადობას (capacity). ელემენტის დამატებისას (push), Vec ამოწმებს, აჭარბებს თუ არა სიგრძე ტევადობას და საჭიროების შემთხვევაში ახდენს მეხსიერების ხელახალ გამოყოფას (reallocate). თუ ადგილი არის, ის უბრალოდ ამატებს ელემენტს და ზრდის სიგრძეს.

თუმცა, როგორც კი DNS პასუხს ქეშში შევინახავთ, მას აღარასოდეს ვცვლით. ტევადობის ველი არანაირ მიზანს აღარ ემსახურება, მაგრამ მაინც იკავებს 8 ბაიტს თითოეულ Vec-ზე. ჰიპზე ზედმეტად გამოყოფილი სივრცეც ფუჭად იკარგება — მაგალითად, Vec რვა ელემენტის ტევადობით, რომელშიც მხოლოდ ხუთი ელემენტია შენახული, ჰიპზე სამ გამოუყენებელ სლოტს ტოვებს.

Box<[T]>-ის გამოყენება ორივე პრობლემას აგვარებს. შექმნის შემდეგ მისი გაზრდა შეუძლებელია, ამიტომ მას არ სჭირდება ტევადობის ველი და არც მომავალი ელემენტებისთვის იტოვებს ადგილს. იგივე ეხება String-საც, რომელსაც ასევე აქვს ტევადობის ველი; Box<str> მას აშორებს.

თითოეული ქეშ-ჩანაწერი ინახავს 8 Vec და String ველს. მათი Box<[T]> და Box<str> ტიპებით ჩანაცვლება ზოგავს 8 ბაიტს თითო ველზე — სულ 64 ბაიტს თითო ჩანაწერზე. ეს ასევე აქრობს ზედმეტ მეხსიერებას ჰიპზე, რომელსაც Vec სამომავლო ზრდისთვის რეზერვად ინახავს. 250 მილიარდზე მეტი ქეშ-ჩანაწერის პირობებში, ჯამურმა დანაზოგმა 15 ტერაბაიტზე მეტი შეადგინა.

ნაკლები სია, ნაკლები მაჩვენებელი

პასუხის, ავტორიტეტისა და დამატებითი სექციების ცალ-ცალკე სიებში შენახვის ნაცვლად, შეგვიძლია შევინახოთ ერთი ერთიანი სია თითოეული სექციის დასაწყისის წანაცვლებით (offsets). ვინაიდან სექციაში DNS ჩანაწერების რაოდენობა ეტევა u16-ში, თითოეული წანაცვლებისთვის შეგვიძლია გამოვიყენოთ u16 (2 ბაიტი), ნაცვლად 8-ბაიტიანი მაჩვენებლისა და 8-ბაიტიანი სიგრძისა, რასაც თითოეული ცალკეული Box<[T]> მოითხოვს.

ამით ამოვიღეთ ორი სია (თითოეული 8-ბაიტიანი მაჩვენებლით და 8-ბაიტიანი სიგრძით) და ჩავანაცვლეთ ორი 2-ბაიტიანი წანაცვლებით, რამაც თითო ჩანაწერზე 28 ბაიტი დაზოგა.

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

მფლობელის (Owner) ამოღება

თითოეულ DNS ჩანაწერს ჰყავს მფლობელი (owner) — დომენი, რომელსაც ეს ჩანაწერი ეკუთვნის. ხშირ შემთხვევაში, ეს მფლობელი იდენტურია მოთხოვნილი დომენისა. მაგალითად, მოთხოვნა example.com A აბრუნებს ორ ჩანაწერს ერთი და იმავე მფლობელით:

bash
$ dig example.com A

;; ANSWER SECTION:
example.com.        300    IN    A        198.51.100.1
example.com.        300    IN    A        198.51.100.2

მაგრამ როდესაც საქმე ეხება, მაგალითად, CNAME-ს, ჩანაწერის მფლობელი შესაძლოა განსხვავდებოდეს მოთხოვნილი დომენისგან:

bash
$ dig example.com A

;; ANSWER SECTION:
example.com.        300    IN    CNAME    cdn.example.com.
cdn.example.com.    300    IN    A        198.51.100.1
cdn.example.com.    300    IN    A        198.51.100.2

DNS ქსელური ფორმატი (wire format) განმეორებად მფლობელებს სახელის კომპრესიით ამუშავებს, როგორც ეს განსაზღვრულია RFC 1035-ში. ერთი და იმავე დომენის ორჯერ კოდირების ნაცვლად, შემდგომი შემთხვევები ინახავს 2-ბაიტიან მაჩვენებელს პირველ შემთხვევაზე. დომენს, როგორიცაა www.example.com, შეუძლია დააკოდიროს მხოლოდ www, რასაც მოჰყვება მაჩვენებელი იმ ადგილზე, სადაც example.com უკვე გამოჩნდა შეტყობინებაში.

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

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

bash
pub struct Record {
    owner: Option<Box<Name>>,
    class: Class,
    ttl: Ttl,
    rtype: Rtype,
    data: RecordData,
}

როდესაც owner არის None, პასუხის აგებისას მოთხოვნილი დომენი აღდგება ქეშის გასაღებიდან, რაც თავიდან გვარიდებს ჰიპზე მეხსიერების გამოყოფას. ეს ნიშნავს, რომ ჩანაწერი აღარ არის დამოუკიდებელი, მაგრამ ქეშის გასაღები ისედაც ხელმისაწვდომია ყოველი ძიებისას. როდესაც მფლობელი განსხვავდება, Some ინახავს მაჩვენებელს ჰიპზე არსებულ სრულ სახელზე.

პრაქტიკაში, დაქეშირებული ჩანაწერების უმეტესობას მოთხოვნილი დომენის იდენტური მფლობელი აქვს, ამიტომ უმრავლესობას არ სჭირდება ჰიპზე მეხსიერების გამოყოფა owner ველისთვის.

Enum-ის ზომები

Rust-ის enum-ები არის ჯამის ტიპები (sum types): თითოეულ ვარიანტს შეუძლია განსხვავებული მონაცემის შენახვა, მაგრამ enum-ის ზომა ყოველთვის მისი ყველაზე დიდი ვარიანტის ზომის ტოლია.

bash
pub enum Option<T> {
    Some(T),
    None,
}

Option არის ან Some და შეიცავს მნიშვნელობას, ან None და არაფერს შეიცავს. ორივე ვარიანტი მეხსიერების ერთსა და იმავე რაოდენობას იკავებს. Enum-ი ინახავს ტეგს, რომელიც მიუთითებს აქტიურ ვარიანტზე, რასაც მოსდევს სივრცე, რომელიც საკმარისად დიდია ყველაზე დიდი ვარიანტის მონაცემისთვის. როდესაც ვარიანტი არის None, ეს სივრცე გამოუყენებელი რჩება.

ჩანაწერის მონაცემებისთვის (record data) ბუნებრივი ჩანს თითოეული DNS ჩანაწერის ტიპის შენახვა enum-ის ვარიანტად:

bash
pub enum RecordData {
    A(Ipv4Addr),
    Aaaa(Ipv6Addr),
    Txt(Txt),
    Naptr(Naptr),
    Svcb(Svcb),
    // ...
}

მაგრამ enum ყოველთვის ისეთივე დიდია, როგორც მისი ყველაზე დიდი ვარიანტი. ჩვენს შემთხვევაში ეს არის NAPTR, რომლის ზომაც 136 ბაიტია. ის ინახავს სამ ცვლადი სიგრძის ტექსტურ ველს, დომენის სახელს და ორ მთელ რიცხვს. შედეგად, მთლიანი enum-ი, ვარიანტის ტეგისა და padding-ის ჩათვლით, ხდება 144 ბაიტი.

A ჩანაწერს მხოლოდ 4 ბაიტი სჭირდება, ხოლო AAAA ჩანაწერს — 16 ბაიტი. A და AAAA ჩვენი ტრაფიკის 80%-ზე მეტს შეადგენს, ამიტომ ჩანაწერების უმეტესობა 120 ბაიტზე მეტს ფუჭად ხარჯავს padding-ზე. რადგან ერთ ქეშ-ჩანაწერს შეუძლია მრავალი რეკორდის შენახვა, ეს დანაკარგი ძალიან სწრაფად იზრდება.

ვარიანტების მოთავსება Box-ში

ამ პრობლემის გადასაჭრელად შეგვიძლია enum-ის უფრო დიდი ვარიანტები მოვათავსოთ Box-ში (Box<T>), რითაც ისინი ჰიპზე ცალკე გამოიყოფა. ამის შემდეგ enum-ი ინახავს მხოლოდ 8-ბაიტიან მაჩვენებელს ჰიპზე, სადაც მონაცემი იკავებს ზუსტად იმ ზომას, რაც რეალურად სჭირდება.

bash
pub enum RecordData {
    // მცირე და ხშირი ვარიანტები ინახება პირდაპირ (inline)
    A(Ipv4Addr),
    Aaaa(Ipv6Addr),
    // დიდი ვარიანტები ინახება ჰიპზე
    Txt(Box<Txt>),
    Naptr(Box<Naptr>),
    Svcb(Box<Svcb>),
    // ...
}

A და AAAA ჩანაწერებისთვის ეს ზოგავს 120 ბაიტს თითო ჩანაწერზე. უფრო მცირე ვარიანტის ტიპებიც, როგორიცაა TXT და CNAME, ასევე იღებენ სარგებელს. ისინი კვლავ იკავებენ 24-ბაიტიან enum-ს, მაგრამ მათი გამოყოფა ჰიპზე ზუსტად ერგება მათ რეალურ მონაცემებს და არ ივსება 144 ბაიტამდე. NAPTR, ყველაზე დიდი ვარიანტი, რეალურად ოდნავ მეტს იხდის — მას ახლა ემატება ჰიპის მაჩვენებლისა და მეხსიერების გამოყოფის დანახარჯი. მაგრამ NAPTR ჩანაწერები პრაქტიკაში იშვიათია, ამიტომ ეს კომპრომისი გამართლებულია.

თუმცა, ჩანაწერების უფრო დიდი ვარიანტების Box-ში მოთავსებას საკუთარი დანახარჯები ახლავს თან.

Boxing-ის ხარჯები

Boxing-ს ორი სახის დანახარჯი აქვს. პირველი არის ალოკატორის ზედნადები ხარჯი (allocator overhead). თითოეული დაბოქსირებული ვარიანტი ხდება ცალკე გამოყოფა ჰიპზე, ალოკატორები კი ზომას უახლოეს ზომის კლასამდე ამრგვალებენ. Big Pineapple იყენებს jemalloc-ს — ალოკატორს, რომელიც შექმნილია მრავალნაკადიანი და მეხსიერების ხშირი გამოყოფის მქონე დატვირთვებისთვის. jemalloc მსგავსი ზომის გამოყოფებს აჯგუფებს ფიქსირებული ზომის კალათებში (bins). TXT ჩანაწერი ითხოვს 32 ბაიტს და ზუსტად ჯდება 32-ბაიტიან ბინში (არაფერი იკარგება), მაგრამ MX ჩანაწერი ითხოვს 40 ბაიტს და მრგვალდება 48-მდე, რაც 8 ბაიტს ფუჭად კარგავს.

მეორე დანახარჯია მეხსიერების ცუდი ლოკალურობა (poor memory locality). Boxing-ის გარეშე, ქეშ-ჩანაწერის enum-ის მნიშვნელობები განთავსებულია ერთ უწყვეტ მეხსიერების ბლოკში. Boxing-ის შემთხვევაში კი, თითოეული დაბოქსირებული ვარიანტის მონაცემები ჰიპის ცალკეულ რეგიონში ცხოვრობს. მის წასაკითხად საჭიროა მაჩვენებლის მიყოლა, და როდესაც ეს მაჩვენებელი ჩანაწერის დანარჩენი ნაწილისგან შორს ხვდება, პროცესორს უწევს ახალი ქეშ-ხაზის (cache line) წამოღება. მილიონობით ქეშ-ჩანაწერის პირობებში, დაბოქსირებული მონაცემები ჰიპზე მიმოფანტული აღმოჩნდება, ნაცვლად იმისა, რომ კომპაქტურად იყოს ერთად თავმოყრილი.

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

ჩანაწერების შენახვა ქსელურ ფორმატში (Wire Format)

აშკარა შემდეგი ნაბიჯი იქნებოდა მთლიანი DNS პასუხის შენახვა ქსელურ ფორმატში (wire format), სადაც ყოველი ძიებისას მხოლოდ კლიენტზე მორგებული ველები (მაგალითად, message ID) ჩასწორდებოდა. მაგრამ ამას თავისი უარყოფითი მხარეები აქვს. DNSSEC ჩანაწერები იგზავნება მხოლოდ მაშინ, როდესაც კლიენტი რთავს DO (DNSSEC OK) ალამს. სრული wire format შეტყობინების შენახვა ნიშნავს ან ორი ვარიანტის დაქეშირებას (ერთი DNSSEC-ით და მეორე მის გარეშე), ან უკვე აწყობილი შეტყობინებიდან მათ გაფილტვრას. ასევე არსებობს ყოველ ძიებაზე სრული შეტყობინების პარსინგის დანახარჯი, რასაც ზემოთ აღწერილი enum-მიდგომა თავიდან იცილებდა უკვე დაპარსული ჩანაწერების შენახვით.

შუალედური გადაწყვეტის სახით, ჩვენ ვინახავთ მხოლოდ ჩანაწერის მონაცემებს ნედლი ბაიტების (raw bytes) სახით, ხოლო ქეშ-ჩანაწერის დანარჩენ ნაწილს სტრუქტურირებულ ველებად ვტოვებთ. დაპარსული enum-ვარიანტების სიის ნაცვლად, ჩანაწერებს ვინახავთ ერთიან Box<[u8]>-ში, რომელიც შეიცავს თითოეულ ჩანაწერს 2-ბაიტიანი სიგრძის პრეფიქსითა და მასზე მიყოლებული ნედლი ბაიტებით.

ეს სრულად აქრობს თითო ვარიანტზე არსებულ enum-ის ზედნადებ ხარჯსა და წინა ოპტიმიზაციის შედეგად შექმნილ ჰიპის გამოყოფებს. მონაცემები ასევე კომპაქტურად და უწყვეტად ლაგდება, რაც აუმჯობესებს CPU ქეშის ლოკალურობას. კომპრომისი ის არის, რომ ჩანაწერებზე შემთხვევითი წვდომა (random index) აღარ შეგვიძლია — ბუფერის თანმიმდევრული იტერაცია გვიწევს. ეს გარკვეულ სირთულეს მატებს ისეთ ფუნქციებს, როგორიცაა A/AAAA ჩანაწერების round-robin როტაცია, მაგრამ რადგან ჩანაწერების რაოდენობა თითო ელემენტზე მცირეა, დანახარჯი უმნიშვნელოა.

დაქეშირებული ჩანაწერებიდან DNS პასუხის აგებისას, ჩანაწერების უმეტესი ტიპის კოპირება შესაძლებელია პირდაპირ ბუფერიდან გამავალ შეტყობინებაში. ადრე თითოეული დაპარსული ჩანაწერი ველ-ველ უნდა სერიალიზებულიყო უკან DNS wire format-ში. ახალი სტრუქტურა ამ სამუშაოს თავიდან გვარიდებს A, AAAA, TXT და ყველა DNSSEC ჩანაწერის ტიპისთვის მათი დაკოდირებული ბაიტების პირდაპირი კოპირებით. მხოლოდ დომენის სახელების შემცველი ჩანაწერები, როგორიცაა CNAME, NS, MX და SOA, საჭიროებს კვლავ პარსინგს, რათა შევძლოთ DNS სახელების კომპრესიის გამოყენება. რადგან პირდაპირი კოპირების მხარდამჭერი ჩანაწერები ჩვენი ტრაფიკის უდიდეს ნაწილს შეადგენს, ამ ცვლილებამ შეამცირა სამუშაო ძიების პროცესში. მეხსიერების გაუმჯობესებულ ლოკალურობასთან ერთად, ამან ჩვენს ბენჩმარკებში ქეშში ძიების დაყოვნება 5%-ით შეამცირა.

ჩანაწერების მონაცემთა ბუფერის ასაგებად, ჩვენ ვწერთ მრავალჯერად დროებით ბუფერში (scratchspace buffer), რომელიც ინარჩუნებს არსებობას ქეშში ჩაწერებს შორის. რადგან წინა ჩაწერებმა ის უკვე გაზარდა, ბუფერის ხელახალი გამოყოფა იშვიათად ხდება საჭირო. ჩანაწერები ზომაში განსხვავდება, ამიტომ ბუფერის ზუსტი ზომა მათ სერიალიზაციამდე არ ვიცით. მას შემდეგ, რაც ჩანაწერები აღმოჩნდება scratchspace ბუფერში, ჩვენ გამოვყოფთ Box<[u8]>-ს და მონაცემებს მასში memcpy-ით ვაკოპირებთ. ეს თითოეული დაბოქსირებული ჩანაწერისთვის ცალკე გამოყოფას ანაცვლებს ერთი გამოყოფით ყველა ჩანაწერის მონაცემისთვის. ეს ასევე თავიდან გვარიდებს დანაკარგს Vec<u8>-ის ზომის შემცირებისას (shrink), სადაც ალოკატორმა შესაძლოა ვერ შეძლოს საწყისი გამოყოფის გამოუყენებელი ბოლო ნაწილის დაბრუნება. ჩვენს ბენჩმარკში მხოლოდ ამ ცვლილებამ ქეშში ჩაწერის გამტარუნარიანობა 13%-ით გაზარდა.

შედეგები

პროდაქშენის გაზომვები აჩვენებს, თუ როგორ აისახა ბენჩმარკში მიღებული თითო ჩანაწერის დანაზოგი მთლიანი პროცესის რეზიდენტულ მეხსიერებაზე. ქვემოთ მოცემული გრაფიკი აჩვენებს p90, p98 და p99 მეხსიერების გამოყენებას Big Pineapple-ის ინსტანსებში. პირველი წყვეტილი ხაზი აღნიშნავს განლაგების (rollout) დაწყებას 2026 წლის 18 მაისს, ხოლო მეორე — მის დასრულებას ყველა სერვისში 2026 წლის 6 ივლისს. თითოეულ რელიზს შემოჰქონდა ზემოთ აღწერილი ერთი ან რამდენიმე ოპტიმიზაცია, ამიტომ მეხსიერების მოხმარება ეტაპობრივად მცირდებოდა და არა ერთბაშად.

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

თითო ინსტანსზე მეხსიერების მოხმარება შემცირდა ყველა პროცენტილში. p99-ზე მეხსიერება 9.3 გბ-დან 5.3 გბ-მდე შემცირდა, რაც რეზიდენტული მეხსიერების 43%-იან კლებას წარმოადგენს. p90-ზე მეხსიერება 6.5 გბ-დან 3.8 გბ-მდე შემცირდა (42%-იანი კლება). ყველაზე დიდი აბსოლუტური დანაზოგი უფრო სავსე ქეშის მქონე ინსტანსებმა აჩვენეს.

ჩვენს ბენჩმარკებში ამ ხუთმა ოპტიმიზაციამ თითო ჩანაწერის მეხსიერების მოცულობა 953 ბაიტიდან 420 ბაიტამდე შეამცირა (56%-იანი კლება). თითო ჩანაწერზე გამოყოფები შემცირდა 1.1 კბ-დან 461 ბაიტამდე. პროდაქშენში გაზომილი შემცირება შედარებით ნაკლებია, რადგან რეზიდენტული მეხსიერება ქეშთან ერთად პროცესის ყველა სხვა მონაცემსაც მოიცავს. მას შემდეგ, რაც განლაგების პროცესი დასრულდა და დასტაბილურდა, მთლიან ინფრასტრუქტურაში სამუშაო მეხსიერების ჯამური მოცულობა (working-set memory) დაახლოებით 100 ტერაბაიტით შემცირდა.

წარმადობაც გაუმჯობესდა. ქეშში ჩაწერის გამტარუნარიანობა გაიზარდა 43%-ით, ხოლო ძიების დაყოვნება შემცირდა 19%-ით.

მეტრიკა

მანამდე

შემდეგ

ცვლილება

თითო ჩანაწერის წმინდა მოცულობა

953 ბაიტი

420 ბაიტი

\-56%

თითო ჩანაწერზე გამოყოფები

1.1 კბ

461 ბაიტი

\-58%

ქეშში ჩაწერის გამტარუნარიანობა

625,000 ჩანაწერი/წმ

893,000 ჩანაწერი/წმ

+43%

ქეშში ძიების დაყოვნება

828 ნს

670 ნს

\-19%

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

Big Pineapple-ის შესახებ მეტის გასაგებად, იხილეთ როგორ მართავს Rust და Wasm Cloudflare-ის 1.1.1.1-ს. თუ მუშაობთ DNS-ზე ან სხვა მასშტაბურ სისტემებზე, გაგვიზიარეთ ოპტიმიზაციები, რომლებმაც თქვენთან გაამართლა Cloudflare Community-ში ან Cloudflare Developers Discord-ზე.