პატერნი, რომელიც Valkey-სთან თითქმის ყველა აპლიკაციამ უნდა გამოიყენოს, არის cache-aside: წაიკითხე ქეშიდან, ხოლო miss-ზე წაიკითხე ბაზიდან, შეინახე შედეგი TTL-ით და დააბრუნე. ჩაწერისას განაახლე ბაზა და ქეშირებული გასაღები წაშალე, მისი განახლების ნაცვლად. ყველა გასაღებს მიეცი ვადა, ყველა გასაღების სახელში ჩადე ვერსია და ძვირი გასაღებები დაიცავი stampede-ისგან - იმ მომენტისგან, როცა პოპულარულ გასაღებს ვადა გასდის და ასი მოთხოვნა ერთდროულად იწყებს მის ხელახლა აშენებას. ეს ხუთი ჩვევა ფარავს თითქმის ყველა გზას, რომლითაც ქეში ფუჭდება: მოძველებული მონაცემები, რომლებიც არასოდეს ახლდება, მონაცემები, რომლებიც ფორმას იცვლის და მკითხველებს ამტვრევს, და ქეში, რომელიც ბაზას იცავს ზუსტად იმ მომენტამდე, როცა ყველაზე მეტად სჭირდება.
ეს პოსტი თითოეულს კოდით გადის, შემდეგ კი ხსნის, როგორ გაიგო, ამართლებს თუ არა ქეში თავის არსებობას.
Cache-aside: ნაგულისხმევი პატერნი#
Cache-aside-ში ლოგიკას აპლიკაცია ფლობს. ქეშმა ბაზის შესახებ არაფერი იცის; კოდი ერთს ამოწმებს, მეორეზე გადადის და ხარვეზს ავსებს.
async function getProduct(id) { const key = `v2:product:${id}`; const hit = await valkey.get(key); if (hit !== null) return JSON.parse(hit); const product = await db.products.findById(id); // the expensive part if (product) { await valkey.set(key, JSON.stringify(product), "EX", 600); } return product;}მისი ღირსებები არის მიზეზი, რის გამოც ის ნაგულისხმევია. თუ Valkey გათიშულია, აპლიკაცია მაინც მუშაობს, უბრალოდ უფრო ნელა, იმ პირობით, რომ ქეშიდან წაკითხვა ისეა შეფუთული, რომ შეცდომა miss-ად ითვლება. ქეშირდება მხოლოდ ის მონაცემები, რაც ვინმემ რეალურად მოითხოვა, ამიტომ მეხსიერება ცხელ ნაკრებს ხმარდება. და ქეშის flush ნებისმიერ დროს შეიძლება ისე, რომ არაფერი დაიკარგოს, რადგან ჭეშმარიტების წყარო ბაზაა.
რა დაქეშო, დაახლოებით სარგებლის მიხედვით:
- ძვირი მოთხოვნები, რომლებიც ინდექსით ვერ მოიშორება - აგრეგაციები, ანგარიშები, ძებნის შედეგები, ყველაფერი, სადაც
GROUP BYბევრ სტრიქონზეა. - სხვისი API-ების გამოძახებები, რომლებიც ნელია, rate limit-ით შეზღუდული და ზოგჯერ თითო მოთხოვნაზე ფასიანი.
- დარენდერებული ფრაგმენტები - გვერდის სექცია, სერიალიზებული API პასუხი - სადაც გამოსავლის აშენება მონაცემების მოტანაზე ძვირი ჯდება.
- ცხელი ჩანაწერები, რომლებსაც გაცილებით ხშირად კითხულობენ, ვიდრე წერენ - პროდუქტი, კონფიგურაციის სტრიქონი, მომხმარებლის უფლებები.
რა არ უნდა დაქეშო: მოთხოვნები, რომლებიც სწორი ინდექსით უკვე სწრაფია. ნელი მოთხოვნის დაქეშვა, რომელსაც ინდექსი სჭირდება, პრობლემას მალავს, სანამ ქეში არ გაცივდება. EXPLAIN ANALYZE-ის წაკითხვა ქეშის ფენაზე იაფია, ხოლო Redis: როდის გჭირდება უფრო ფართოდ ასაბუთებს, რატომ შეიძლება მის გარეშე.
სხვა პატერნები და რატომ არის ისინი უფრო იშვიათი#
| პატერნი | როგორ მუშაობს | როდის ერგება |
|---|---|---|
| Cache-aside | აპი კითხულობს ქეშს, გადადის ბაზაზე, ავსებს ქეშს | თითქმის ყოველთვის |
| Read-through | ქეშის ბიბლიოთეკა miss-ზე ბაზიდან ტვირთავს | იგივე, რაც cache-aside, ბიბლიოთეკაში შეფუთული |
| Write-through | ყოველი ჩაწერა ქეშსა და ბაზაში ერთად მიდის | მონაცემები, რომლებსაც ჩაწერისთანავე კითხულობენ |
| Write-behind | ჩაწერები ქეშში მიდის და მოგვიანებით ბაზაში იწერება | მთვლელები და მეტრიკები, სადაც დანაკარგი მისაღებია |
| Refresh-ahead | ცხელი გასაღებები ვადის გასვლამდე ხელახლა შენდება | ძალიან ცხელი, ძვირი გასაღებების მცირე ნაკრები |
Write-through მიმზიდველად ჟღერს - ქეში არასოდეს ძველდება - მაგრამ ის ქეშავს ყველაფერს, რაც იწერება, კითხულობს ვინმე თუ არა, და მაინც სჭირდება TTL იმ შემთხვევისთვის, თუ ერთ მხარეს ჩაწერა ჩავარდება. Write-behind Valkey-ს გარკვეული დროით ჩანაწერების სისტემად აქცევს, რაც კარგია გვერდის ნახვების მთვლელებისთვის, რომლებიც ყოველ წუთს იწერება, და მიუღებელია შეკვეთებისთვის. Refresh-ahead ქვემოთ კიდევ ერთხელ გამოჩნდება, როგორც stampede-ისგან დაცვა.
TTL-ების შერჩევა#
TTL არის ზედა ზღვარი იმისა, რამდენად შეიძლება იყოს ქეშირებული მნიშვნელობა მოძველებული. აირჩიე ის კითხვით, რამდენ ხანს არის ასატანი არასწორი პასუხი, და არა იმით, რამდენ ხანს დარჩება მნიშვნელობა სავარაუდოდ უცვლელი.
| მონაცემები | ტიპური TTL | არგუმენტაცია |
|---|---|---|
| დარენდერებული გვერდის ფრაგმენტი ანონიმური მომხმარებლებისთვის | 30-300 წამი | მოკლე მოძველება შეუმჩნეველია |
| პროდუქტის დეტალები, სტატიების ტექსტი | 5-60 წუთი, პლუს ჩაწერისას ინვალიდაცია | რედაქტირებას ინვალიდაცია აგვარებს; TTL სათადარიგო დაცვაა |
| ანგარიშის ან დაფის აგრეგატი | 5-15 წუთი | მომხმარებლები იღებენ "რამდენიმე წუთის წინანდელს" |
| მესამე მხარის API-ს პასუხი | იმდენ ხანს, რამდენსაც მათი პირობები და სიახლე იძლევა | ზოგავს ფულს და rate limit-ს |
| უფლებები და როლები | 1-5 წუთი, პლუს ინვალიდაცია | გაუქმებული უფლება არ უნდა შემორჩეს |
| კონფიგურაცია და საძიებო ცხრილები | 1 საათი ან მეტი | იშვიათად იცვლება; ინვალიდაცია deploy-ზე |
ორი დახვეწა TTL-ებს დატვირთვის ქვეშ უკეთ აქცევს:
- დაამატე jitter. თუ batch ამოცანა ათი ათას გასაღებს ერთბაშად ავსებს
EX 3600-ით, ყველას საათის შემდეგ ერთსა და იმავე წამში გასდის ვადა, და ბაზა ათი ათას miss-ს ერთდროულად იღებს. დაამატე შემთხვევითი რამდენიმე პროცენტი:EX 3600 + random(0, 300). - არასოდეს დაქეშო მის გარეშე. გასაღები ვადის გარეშე დაპირებაა, რომ ინვალიდაცია ყოველთვის იმუშავებს. არ იმუშავებს - გამორჩენილი კოდის გზა, ბაზაში ხელით გასწორება, ჩავარდნილი წაშლა - და გასაღები არასწორ მნიშვნელობას მოგაწვდის, სანამ ვინმე ქეშს ხელით არ გაასუფთავებს. მონაცემებსაც კი, რომლებსაც ფრთხილად აინვალიდებ, სათადარიგოდ TTL ეკუთვნის.
ერიდე ერთ მახეს: უბრალო SET EX-ის გარეშე არსებულ გასაღებზე მის ვადას შლის. ჩაწერის ყველა გზამ TTL უნდა გადასცეს ან KEEPTTL გამოიყენოს.
ინვალიდაცია: წაშალე, ნუ განაახლებ#
როცა საწყისი მონაცემები იცვლება, ქეშირებული ასლი უნდა წავიდეს. საიმედო მიდგომაა ჯერ ბაზის განახლება, შემდეგ გასაღების წაშლა, და შემდეგ წაკითხვას მისცე მისი ხელახლა აშენება.
async function updateProduct(id, changes) { await db.products.update(id, changes); await valkey.unlink(`v2:product:${id}`);}რატომ წაშლა და არა ახალი მნიშვნელობის ჩაწერა ქეშში? იმიტომ, რომ ორი პარალელური ჩამწერი ბაზასა და ქეშში საპირისპირო თანმიმდევრობით შეიძლება დასრულდეს, და ქეშში სამუდამოდ ძველი მნიშვნელობა დარჩება. წაშლას თანმიმდევრობის პრობლემა არ აქვს: რომელი წაშლაც არ უნდა მოვიდეს ბოლოს, შემდეგი წაკითხვა მიმდინარე სტრიქონს ჩატვირთავს.
მაინც რჩება ერთი ვიწრო race. მკითხველს miss აქვს, ტვირთავს ძველ სტრიქონს და იგვიანებს; ჩამწერი აახლებს სტრიქონს და შლის გასაღებს; დაგვიანებული მკითხველი შემდეგ ძველ სტრიქონს ქეშში წერს. TTL ზღუდავს, რამდენ ხანს გადარჩება ეს, რაც კიდევ ერთი მიზეზია, რომ ყველა გასაღებს ის სჭირდება. სადაც ეს ფანჯარა მიუღებელია, გასაღები მეორედ წაშალე ჩაწერიდან მცირე დაყოვნებით, ან მნიშვნელობა საერთოდ ნუ შეინახავ ქეშში.
გასაღებების ჯგუფების ინვალიდაცია ისაა, სადაც ადამიანები KEYS product:*-ს მიმართავენ და სერვერს თიშავენ. უკეთესი ვარიანტები:
- ვერსიის პრეფიქსები. ჩადე ვერსია გასაღებში (
v2:product:...). ქეშირებული ფორმის შეცვლა არის deploy, რომელიც ვერსიას ზრდის; ძველ გასაღებებს უბრალოდ ვადა გასდით. - თაობის მთვლელები. შეინახე მთვლელი თითო ჯგუფზე (
gen:catalogue) და მისი მნიშვნელობა გასაღებების სახელებში ჩართე. მთელი ჯგუფის ინვალიდაცია ერთიINCR-ია; ძველ გასაღებებს აღარასოდეს წაიკითხავენ და ისინი თავიანთი TTL-ით ქრება. - თეგების სიმრავლეები. დაამატე ყოველი ქეშის გასაღები სიმრავლეში თითო თეგზე (
SADD tag:category:7 v2:product:42). თეგის ინვალიდაციისთვის წაიკითხე სიმრავლის წევრები, გაუკეთე მათUNLINKდა წაშალე სიმრავლე. სიმრავლესაც მიეცი TTL.
ნებისმიერი დიდი რამისთვის გამოიყენე UNLINK და არა DEL: ის გასაღებს მაშინვე შლის და მის მეხსიერებას ფონურად ათავისუფლებს, ამიტომ დიდი მნიშვნელობის წაშლა სხვა კლიენტებს არ აჩერებს.
Stampede: როცა ქეში ზუსტად მაშინ ფუჭდება, როცა მნიშვნელოვანია#
Stampede, რომელსაც dogpile-ს ან thundering herd-საც უწოდებენ, ხდება, როცა პოპულარულ გასაღებს ვადა გასდის და ბევრ მოთხოვნას ერთსა და იმავე მომენტში აქვს miss. თითოეული უშვებს ძვირ მოთხოვნას, თითოეული წერს შედეგს, და ბაზა იმ ფანჯარაში ყველა მოთხოვნის სრულ დატვირთვას იღებს - ხშირად პიკური ტრაფიკისას, ზუსტად მაშინ, როცა გასაღები პოპულარული იყო. ცივი სტარტი ქეშის flush-ის ან restart-ის შემდეგ იგივეა, ოღონდ ყველა გასაღებზე ერთდროულად.
სამი დაცვა, უმარტივესიდან ყველაზე საიმედომდე.
ხელახლა აშენების საკეტი. პირველი მოთხოვნა, რომელსაც miss აქვს, იღებს მოკლე საკეტს SET ... NX PX-ით, აშენებს და ათავისუფლებს მას. დანარჩენები ცოტას ელოდება და ქეშს თავიდან კითხულობს, ან მოძველებულ ასლს აწვდის, თუ ასეთი არსებობს.
import json, time, uuidUNLOCK = valkey.register_script("""if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1])endreturn 0""")def get_report(team_id): key, lock = f"v1:report:{team_id}", f"lock:v1:report:{team_id}" for _ in range(50): hit = valkey.get(key) if hit is not None: return json.loads(hit) token = str(uuid.uuid4()) if valkey.set(lock, token, nx=True, px=10_000): try: report = build_report(team_id) # the expensive part valkey.set(key, json.dumps(report), ex=900) return report finally: UNLOCK(keys=[lock], args=[token]) time.sleep(0.1) # someone else is rebuilding return build_report(team_id) # give up waitingსაკეტს ვადა აქვს (PX 10000), რომ ჩავარდნილმა worker-მა ის სამუდამოდ ვერ დაიჭიროს, და ის თავისუფლდება სკრიპტით, რომელიც ტოკენს ამოწმებს, ამიტომ ნელი worker, რომლის საკეტსაც ვადა უკვე გაუვიდა, ვერ წაშლის საკეტს, რომელიც ახლა სხვას უჭირავს.
Stale-while-revalidate. შეინახე მნიშვნელობა გრძელი მკაცრი TTL-ით, ხოლო მნიშვნელობის შიგნით შეინახე რბილი ვადის დრო. მკითხველი, რომელიც ხედავს, რომ რბილი დრო გავიდა, მაინც მაშინვე აბრუნებს მოძველებულ მნიშვნელობას და ფონურად იწყებს ხელახლა აშენებას - იმავე საკეტით დაცულს, რომ მხოლოდ ერთი გაეშვას. მომხმარებლები არასოდეს ელოდებიან, ბაზა კი ხედავს ერთ ხელახლა აშენებას თითო გასაღებზე თითო ინტერვალში.
ალბათური ადრეული განახლება. თითოეული მკითხველი hit-ზე აგდებს კამათელს, რომლის წონაც დამოკიდებულია იმაზე, რამდენად ახლოსაა გასაღები ვადის გასვლასთან და რამდენ ხანს სჭირდება მის ხელახლა აშენებას, და ხანდახან ადრე ანახლებს. ჩვეულებრივი ფორმულა ("XFetch" სტატიიდან) აშენებს მაშინ, როცა now - rebuild_time * beta * ln(random()) >= expiry, სადაც beta დაახლოებით 1-ია. შედეგი ისაა, რომ სტატისტიკურად ერთი მოთხოვნა გასაღებს ვადის გასვლამდე ცოტა ხნით ადრე ანახლებს, ყოველგვარი საკეტის გარეშე. ეს ერგება ძალიან ცხელი გასაღებების მცირე ნაკრებს.
ნეგატიური ქეშირება და შეცდომები#
თუ ძებნა ვერაფერს პოულობს - პროდუქტის ID, რომელიც არ არსებობს, მომხმარებელი ავატარის გარეშე - ესეც დაქეშე. სხვა შემთხვევაში ყოველი მოთხოვნა არარსებულ რამეზე ბაზაში მიდის, და scraper, რომელიც ID-ებს გადაუყვება, ან თავდამსხმელი, რომელიც ამას განზრახ აკეთებს, ქეშს სრულად გვერდს უვლის. შეინახე სიგნალური მნიშვნელობა, მაგალითად ცარიელი სტრიქონი ან {"missing":true}, რეალურ მნიშვნელობებზე მოკლე TTL-ით, რომ ახლად შექმნილი ჩანაწერი სწრაფად გამოჩნდეს.
შეცდომები სხვა საქმეა. ნუ დაქეშავ მესამე მხარის API-ს ჩავარდნილ გამოძახებას ისე, თითქოს პასუხი იყოს - ან თუ აუცილებლად გჭირდება, რომ გათიშულ სერვისს შეეშვა, დაქეშე წამებით და არა წუთებით, და ნათლად განასხვავე ნამდვილი ცარიელი შედეგისგან.
მნიშვნელობები: ზომა და სერიალიზაცია#
როგორ ინახავ მნიშვნელობას, მეხსიერებასა და სისწრაფეზე იმაზე მეტად მოქმედებს, ვიდრე ადამიანები ელიან.
- JSON წაკითხვადი და უნივერსალურია, და სწორი ნაგულისხმევი არჩევანია. MessagePack ან მსგავსი ბინარული ფორმატი უფრო პატარაა და უფრო სწრაფად იპარსება,
valkey-cli-ში წაკითხვადობის ფასად. - შეკუმშე დიდი მნიშვნელობები. 200 KB JSON დოკუმენტი gzip-ით ან zstd-ით მის მცირე ნაწილამდე იკუმშება, და CPU-ს დრო ჩვეულებრივ ნაკლებია, ვიდრე დაზოგილი ქსელის დრო. რამდენიმე კილობაიტზე ნაკლებისთვის არ ღირს.
- მოერიდე უზარმაზარ მნიშვნელობებს. რამდენიმემეგაბაიტიანი მნიშვნელობის გაგზავნას მილიწამები სჭირდება, და სანამ სერვერი მას აგზავნის, სხვას არავის ემსახურება. დაყავი ის, ან დაქეშე ის ნაწილები, რომლებსაც რეალურად კითხულობენ.
- გამოიყენე hash ჩანაწერებისთვის, რომლებსაც ნაწილობრივ კითხულობენ.
HGET product:42 priceერთ ველს კითხულობს მთელი ობიექტის გადაცემის გარეშე; პატარა hash-ები ასევე კომპაქტურად ინახება.
Valkey-ის მეხსიერება და eviction პოლიტიკები ფარავს, რა ხდება, როცა ქეში ივსება, და რატომ უნდა შეესაბამებოდეს eviction პოლიტიკა იმ ფაქტს, რომ ეს მონაცემები ერთჯერადია.
როგორ გავზომოთ, მუშაობს თუ არა ქეში#
გაუზომავი ქეში ვარაუდია. ძირითად მთვლელებს Valkey თვითონ ინახავს:
$ valkey-cli -h db.example.net -p 6380 --askpass INFO stats \ | grep -E 'keyspace_hits|keyspace_misses|evicted_keys|expired_keys'keyspace_hits:1843920keyspace_misses:211347evicted_keys:0expired_keys:96112Hit ratio არის hit-ები გაყოფილი hit-ებისა და miss-ების ჯამზე - აქ დაახლოებით 90 პროცენტი. სერვერის მასშტაბის რიცხვები ყველა სახის გასაღებს ერთად ურევს, ამიტომ hit-ები და miss-ები აპლიკაციაშიც დათვალე გასაღების პრეფიქსის მიხედვით, სადაც დაინახავ, რომ პროდუქტის გვერდებს 98 პროცენტი hit აქვს, ძებნის შედეგებს კი 40 პროცენტი. დაბალი ratio ნიშნავს ძალიან მოკლე TTL-ებს, ზედმეტად კონკრეტულ გასაღებებს (timestamp ან სესიის ID გასაღებში, რომელიც გაზიარებისთვის იყო განკუთვნილი), ან მონაცემებს, რომლებიც უბრალოდ ხელახლა არ გამოიყენება. მზარდი evicted_keys ნიშნავს, რომ ქეში ძალიან პატარაა თავისი სამუშაო ნაკრებისთვის. მონიტორინგი, რომელიც რამეს გეუბნება ფარავს ამათი alert-ებად ქცევას.
ბაზაც გაზომე. ქეშის აზრი ის დატვირთვაა, რომელსაც ის აცილებს; თუ ქეშის დამატებისას მოთხოვნების სიხშირე და latency არ ეცემა, ის თავის საქმეს არ აკეთებს.
ქეშირება Valkey-ით RE:NODE-ზე#
Valkey-ის ხაზი სწორედ ამ საქმეს ერგება. უმცირეს გეგმას 256 MB მეხსიერება აქვს, საიდანაც გეგმა დაახლოებით 200 MB-ს გამოსაყენებლად აღნიშნავს - საკმარისზე მეტი ერთი აპლიკაციის წინ მდგომი ქეშისთვის - და გეგმები 4 GB-მდე ადის. თითოეული სერვერი პაროლით არის დაცული და მას პანელში ნაჩვენები ჰოსტითა და პორტით მიმართავ; proxy slot არ არის, ამიტომ აპლიკაცია პირდაპირ უკავშირდება. რადგან თითოეული სერვერი დისკზე AOF-ითა და snapshot-ებით ინახავს, restart ქეშს თბილს აბრუნებს და არა ცარიელს, რაც დაგეგმილი restart-ებიდან ცივი სტარტის stampede-ს აშორებს. ეს მოხერხებულობად მიიღე და არა გარანტიად: ქეში მაინც ერთჯერადი მონაცემებია, და კოდი უნდა მუშაობდეს, როცა ის ცარიელია.
FAQ#
მონაცემების შეცვლისას ქეში უნდა განვაახლო თუ წავშალო?
წაშალე. განახლებამ შეიძლება ქეშში ძველი მნიშვნელობა დატოვოს, როცა ორი ჩაწერა სხვადასხვა თანმიმდევრობით სრულდება, მაშინ როცა წაშლა შემდეგ წაკითხვას ყოველთვის მიმდინარე მონაცემების ჩატვირთვამდე მიიყვანს. ყოველ გასაღებზე შეინახე TTL, როგორც სათადარიგო დაცვა იმ შემთხვევებისთვის, რომლებიც ინვალიდაციას გამორჩება.
რა TTL გამოვიყენო?
ყველაზე გრძელი მოძველება, რომელსაც შენი მომხმარებლები ამ მონაცემებისთვის აიტანენ, და არა ვარაუდი იმაზე, რამდენად ხშირად იცვლება ის. წამები გვერდის ფრაგმენტებისთვის, წუთები პროდუქტის მონაცემებისა და აგრეგატებისთვის, მეტი კონფიგურაციისთვის - და ყოველთვის ჩაწერისას ინვალიდაციით ყველაფრისთვის, რასაც მომხმარებლები არედაქტირებენ. დაამატე ცოტა შემთხვევითი jitter, რომ ერთად შევსებულ გასაღებებს ვადა ერთად არ გაუვიდეთ.
როგორ გავასუფთავო ერთი ფუნქციის ყველა ქეშირებული გასაღები?
ნუ გამოიყენებ KEYS-ს. ჩადე ვერსია ან თაობის ნომერი გასაღებების სახელებში და გაზარდე ის, რომ ძველ გასაღებებს აღარასოდეს წაიკითხავდნენ და მათ ვადა თავისით გაუვიდეს. თუ მყისიერი წაშლა გჭირდება, შეინახე გასაღებების სიმრავლე თითო თეგზე და მის წევრებს UNLINK გაუკეთე.
რა არის cache stampede?
ბევრი მოთხოვნა, რომლებსაც ერთსა და იმავე მომენტში ერთი და იმავე ვადაგასული გასაღების miss აქვთ და ყველა ერთად უშვებს ძვირ ხელახლა აშენებას. თავიდან აიცილე ის მოკლე აშენების საკეტით SET NX PX-ის გამოყენებით, მოძველებული მნიშვნელობების მიწოდებით, სანამ ერთი მოთხოვნა ანახლებს, ან ცხელი გასაღებების განახლებით ვადის გასვლამდე ცოტა ადრე.
რა hit ratio უნდა ჰქონდეს ქეშს?
მონაცემებზეა დამოკიდებული, მაგრამ ზოგადი დანიშნულების ქეშისთვის დაახლოებით 80 პროცენტზე დაბალი ღირს გამოკვლევად. შეხედე hit ratio-ებს გასაღების პრეფიქსის მიხედვით აპლიკაციაში; სერვერის საშუალო მალავს გასაღების იმ ერთ ტიპს, რომელსაც არასოდეს აქვს hit.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.