অধ্যায় 8 · ডিপ্লয়মেন্ট, স্ট্রিমিং API ও স্কেলিং
স্কেলিং, রেজিলিয়েন্স ও রোলব্যাক
- পৃষ্ঠা 16 / 22
- 27 মিনিট পড়া
পুনের প্রিয়া গত পাতার স্ট্রিমিং EvidenceDesk সার্ভিস চালু করে। একটা ক্যাম্পেইনে ট্রাফিক তিনগুণ হয়। মডেল প্রোভাইডার ধীর হয়ে যায়, ক্লায়েন্ট টাইমআউট পেয়ে রিট্রাই করে, আর রিট্রাই ধীরগতিকে আরও গভীর করে। সেই রাতেই একটা নতুন প্রম্পট ভার্সন সবার কাছে একসঙ্গে যায়; উত্তর কাটা অবস্থায় ফেরে, আর কত ব্যবহারকারী তা দেখেছে বা কীভাবে আগের অবস্থায় ফেরা যায়, কেউ জানে না। (এটা একটা বোঝানোর গল্প।) প্রতিটা ব্যর্থতাই সাধারণ; কষ্ট দেয় পরিকল্পনা না থাকা। এই পাতা সেই পরিকল্পনা, একটা রিকোয়েস্ট যে ক্রমে এসব মুখোমুখি হয় সেই ক্রমে।
সৎ পরিধি: এই পাতা যেখানে লেখা হয়েছে সেখানে কোনো Docker ডেমন বা অর্কেস্ট্রেটর নেই, আর আমি কোনো ক্লাউড-প্রোভাইডারের তথ্য দিচ্ছি না। এখানকার সবকিছু নকল প্রোভাইডারসহ ভার্চুয়াল সময়ে চলা একটা সিমুলেশন। সিমুলেশন কৌশল দেখায়, প্রোডাকশনের সংখ্যা নয়। সব দাম, হার ও সক্ষমতা বানানো।
যা শিখবেন
- I/O-নির্ভর মডেল কলের জন্য কেন প্যারালেলিজম নয়, কনকারেন্সি লাগে; ওয়ার্কাররা কী ভাগ করে; আর লিটলের সূত্র কীভাবে লোড টেস্টের ফল আগেই বলে দেয়।
- ওভারলোড, টাইমআউট ও রিট্রাই কীভাবে সীমিত করবেন, যাতে একটা ধীর নির্ভরতা পুরো সার্ভিস নামিয়ে না আনে।
- অপরীক্ষিত মডেল ছাড়া, ব্রেকার ও বাল্কহেডসহ, প্রোভাইডারের মধ্যে কীভাবে ফেইলওভার করবেন।
- কীভাবে কানারি দিয়ে মেট্রিক গেটসহ রিলিজ করবেন, ফ্ল্যাগ বা পরিকল্পনা দিয়ে রোলব্যাক করবেন, আর গেম ডেতে মহড়া দেবেন।
কী নতুন, আর কী পুনর্ব্যবহার
নতুন, evidencedesk/serve/-এ: দশটা মডিউল, data/serve/-এ তিনটা ডেটা ফাইল এবং সাতটা test_scale_*.py ফাইল। ব্যবহার হচ্ছে: AdmissionGate ও ServeSettings (কন্টেইনারাইজেশন ও স্ট্রিমিং ডিপ্লয়মেন্ট), UsageLimiter (বাজেট, ক্যাশিং ও ব্যবহারের সীমা), RetryPolicy (অ্যাডাপ্টার, কনফিগারেশন ও এরর হ্যান্ডলিং), storm, Breaker, Flags ও অ্যালার্টের Engine (মনিটরিং, বাজেট ও ইনসিডেন্ট) এবং রিলিজ গেট (মজবুত মেট্রিক ও রিলিজ গেট)।
কনকারেন্সি, প্যারালেলিজম ও ওয়ার্কার মডেল
একটা মডেল কল প্রায় সব সময় অপেক্ষা করে। কনকারেন্সি মানে অনেক রিকোয়েস্ট একসঙ্গে চলমান, বেশিরভাগই অপেক্ষায়; প্যারালেলিজম মানে একই মুহূর্তে কয়েকটা কোরে কয়েকটা কাজ চলা। I/O-নির্ভর সার্ভিসের লাগে প্রথমটা; প্যারালেলিজম শুধু হিসাবের ছোট অংশে কাজে লাগে (রিট্রিভাল, স্ক্রিনিং, JSON)। এখানে একটা রিকোয়েস্ট ১৫ মিলিসেকেন্ড একটা কোর ধরে রাখে আর মডেলের জন্য ৮০০ মিলিসেকেন্ড অপেক্ষা করে, আর একটা ওয়ার্কারের থাকে ইন-ফ্লাইট স্লট (AdmissionGate.max_in_flight)। সর্বোচ্চ সীমা হলো slots / (cpu + io) আর cores / cpu-এর ছোটটা।
from evidencedesk.serve.loadsim import Profile, Service, capacity, simulate, sweep
p = Profile(rate=1, cpu_s=0.015, io_s=0.8) # one request: 15 ms on a core, 800 ms waiting for the model
for name, s in {"1 worker, 8 slots": Service(1, 8), "1 worker, 64 slots": Service(1, 64), "4 workers, 64 slots": Service(4, 64)}.items():
print(f"{name:<20} ceiling {capacity(p, s):6.1f} req/s")1 worker, 8 slots ceiling 9.8 req/s
1 worker, 64 slots ceiling 66.7 req/s
4 workers, 64 slots ceiling 266.7 req/sআটটা স্লট একটা ওয়ার্কারকে সেকেন্ডে ৯.৮ রিকোয়েস্টে আটকে রাখে, অথচ তার কোর প্রায় অলস। চৌষট্টিটা স্লটে সেই সীমা সরে যায় আর কোর হয় সীমা; সেটা সরাতে পারে শুধু আরও প্রসেস (প্যারালেলিজম)। আগে কনকারেন্সি বাড়ান, CPU সীমা হলে ওয়ার্কার যোগ করুন। ওয়ার্কাররা কিছুই ভাগ করে না, যতক্ষণ না আপনি ব্যবস্থা করেন: প্রত্যেকের নিজের AdmissionGate (max_in_flight=8-এ চারটা ওয়ার্কার ৩২টা ঢোকায়), ক্যাশ, ব্যবহারের কাউন্টার, ব্রেকার ও ফ্ল্যাগ আছে।
টুল নোট (২০২৬-১০-১২ অনুযায়ী): uvicorn-এর
--workers-এর ডিফল্ট$WEB_CONCURRENCYসেট থাকলে সেটা, নইলে ১, এবং--reload-এর সঙ্গে বৈধ নয়। এর ওয়ার্কার প্রি-ফর্ক নয়, স্পন করা হয়, আর মরে গেলে আবার চালু হয়;SIGHUPএকটা একটা করে রিস্টার্ট করে।uvicorn.workersমডিউল ডিপ্রিকেটেড:uvicorn --workers Nবাuvicorn-workerপ্যাকেজ ব্যবহার করুন। Gunicorn-এর--graceful-timeout-এর ডিফল্ট ৩০ সেকেন্ড, তারপর ওয়ার্কার জোর করে মারা হয়। uvicorn SIGTERM-এ কী করে, তা পড়া পাতাগুলোতে বলা নেই: টেস্ট করে দেখুন। বর্তমান ডকুমেন্টেশন দেখে নিন।
স্টেটলেস ওয়ার্কার আলাদা করা, স্টেট টেকসই জায়গায়
একটা ওয়ার্কার স্টেটলেস যখন তাকে হারালে, একটা যোগ করলে বা পরের রিকোয়েস্ট অন্য কোথাও গেলে কোনো উত্তর বদলায় না; তখন তাকে স্বাধীনভাবে স্কেল ও রিস্টার্ট করা যায়। স্টেট উধাও হয় না, টেকসই কোথাও সরে যায়।
- ইতিহাস ও এজেন্ট চেকপয়েন্ট: ব্যবহারকারী ও সেশন ধরে একটা ডেটাবেস; মেমরিতে থাকলে রিস্টার্টে বা অন্য ওয়ার্কারে হারিয়ে যায়।
- ব্যবহারের সীমা: একটাই ভাগ করা কাউন্টার; প্রতি ওয়ার্কারে হলে সীমা ওয়ার্কার সংখ্যা দিয়ে গুণ হয়ে যায়।
- রেসপন্স ক্যাশ: ভাগ করা ক্যাশ, নয়তো ক্ষতি মেনে নেওয়া; প্রতি ওয়ার্কারে হলে হিট রেট কমে, মডেল কল বাড়ে।
- অ্যাডমিশন গেট ও ব্রেকার: ইচ্ছা করেই প্রতি প্রসেসে, কারণ তারা এই প্রসেসকে রক্ষা করে; ওয়ার্কার প্রতি মাপ ঠিক করুন।
Cluster রিকোয়েস্টগুলো রাউন্ড-রবিনে চারটা ওয়ার্কারে পাঠায়: shared=False-এ প্রত্যেকে নিজের স্টেট রাখে; shared=True-এ সবাই একটাই অবজেক্ট ব্যবহার করে, যা একটা স্থায়ী সার্ভিসের প্রতিনিধি (তার অ্যাটমিসিটি ও ব্যর্থতা মডেল করা হয়নি)।
from evidencedesk.limits import Quota
from evidencedesk.serve.placement import Cluster
from evidencedesk.usage import ManualClock
clock = ManualClock(0.0)
for shared in (False, True):
c = Cluster(4, shared, clock) # four workers; each user may make 10 calls a day
c.ask("aiko", "refund window")
seen = c.ask("aiko", "and sale items?")["history_seen"]
allowed = sum(c.ask("omar", f"q{i}")["status"] == 200 for i in range(40))
big = Cluster(4, shared, clock, per_user=Quota(1000, 10**6))
hits = sum(big.ask("mateo", q)["cache"] == "hit" for q in ["refund window", "shipping time"] * 10)
print(f"shared={shared!s:<5} follow-up saw {seen} earlier question(s) | limit 10 let {allowed}/40 through | cache hits {hits}/20, model calls {big.model_calls}")shared=False follow-up saw 0 earlier question(s) | limit 10 let 40/40 through | cache hits 16/20, model calls 4
shared=True follow-up saw 1 earlier question(s) | limit 10 let 10/40 through | cache hits 18/20, model calls 2আলাদা কাউন্টারে একজন ব্যবহারকারী ১০ সীমায় ৪০টা কল করতে পারে; ফলো-আপ প্রশ্ন এমন ওয়ার্কারে পড়ে যার কোনো ইতিহাস নেই; ক্যাশ দ্বিগুণ মডেল কল করে। ভাগ করলে ক্রিটিকাল পথে একটা নির্ভরতা যোগ হয়, যার নিজের টাইমআউট ও ফলব্যাক লাগে।
নকল ক্লায়েন্ট দিয়ে লোড টেস্ট
লোড টেস্ট জানতে চায় "R রেটে সার্ভিস কী করে?" টোকেন খরচ ছাড়া, তাই জেনারেটর ও সার্ভিস দুটোই নকল এবং ভার্চুয়াল সময়ে চলে: simulate(Profile, Service) সিড করা Poisson আগমন আঁকে, একটা ডিসক্রিট-ইভেন্ট কিউ চালায় এবং একটা LoadReport ফেরায়, ঘুম বা সকেট ছাড়া। আগমন ওপেন-লুপ: সার্ভিস যত ধীরই হোক ক্লায়েন্ট আসতেই থাকে, ক্যাম্পেইনে যেমন হয় (ক্লোজড-লুপ জেনারেটর সার্ভিসের সঙ্গে ধীর হয়ে ওভারলোড লুকিয়ে ফেলে)। একটা ওয়ার্কার, ৮টা স্লট, অপেক্ষার জন্য ৮টা জায়গা:
print("rate shed% p95_s done/s slots_used L=in system lambda*W")
for rate, r in sweep([4, 8, 10, 14], Service(1, 8, max_waiting=8), p):
print(f"{rate:>4} {100 * r.shed / r.offered:5.1f} {r.p95_s:5.2f} {r.throughput:6.2f} {r.util:10.2f} {r.in_system:11.2f} {r.throughput * r.mean_s:8.2f}")rate shed% p95_s done/s slots_used L=in system lambda*W
4 0.0 1.19 4.22 0.43 3.46 3.46
8 0.9 1.44 8.06 0.82 7.51 7.51
10 6.3 1.72 9.49 0.96 11.09 11.09
14 30.4 1.91 9.88 1.00 14.35 14.35এটা একটা স্যাচুরেশন কার্ভ। ৯.৮ সীমার পর p95 বাড়ে, রিকোয়েস্ট শেড হয়, আর থ্রুপুট সমতল হয়ে যায়। শেষ দুটো কলাম লিটলের সূত্র, L = λ × W: সিস্টেমে গড় রিকোয়েস্টের সংখ্যা সমান আগমনের হার গুণ প্রতিটার সেখানে কাটানো গড় সময়। L ইভেন্ট স্ট্রিম থেকে ইন্টিগ্রেট করে পাওয়া, λ × W সমাপ্তি থেকে হিসাব করা, তাই তাদের মিল সিমুলেটরকে যাচাই করে। এটা সক্ষমতার পরিকল্পনাও করে: সেকেন্ডে ৪০ রিকোয়েস্ট, প্রতিটা ০.৮১৫ সেকেন্ডের, গড়ে প্রায় ৩৩টা ইন-ফ্লাইট রাখে।
সিমুলেটরে আছে একটা নিখুঁত কিউ আর নেটওয়ার্ক নেই: এটা স্যাচুরেশনের আকার দেখায়। আসল সার্ভিস মাপুন আসল লোড টুল ও নকল মডেল দিয়ে, যাতে টেস্টে কোনো খরচ না হয়।
ব্যাকপ্রেশার ও ওভারলোড সামলানো
ব্যাকপ্রেশার হলো কাজ নীরবে শুষে না নিয়ে সার্ভিসের "আস্তে" বলা। এটা ছাড়া কিউ সীমাহীন বাড়ে, লেটেন্সি চড়ে, আর প্রতিটা উত্তর পৌঁছায় ক্লায়েন্ট হাল ছেড়ে দেওয়ার পরে। একই ওভারলোডের (সেকেন্ডে ১৪, সীমা ৯.৮) বিরুদ্ধে তিনটা নকশা:
shapes = {"no waiting room": Service(1, 8), "8 may wait": Service(1, 8, max_waiting=8),
"unbounded, 3 s deadline": Service(1, 8, max_waiting=10_000, deadline_s=3.0)}
for name, s in shapes.items(): # the same 14 requests per second against a ceiling of 9.8
r = simulate(Profile(14), s)
print(f"{name:<24} shed {r.shed:>4} expired {r.expired:>4} late {r.late:>4} on time {r.ok:>4} p95 {r.p95_s}s")no waiting room shed 696 expired 0 late 0 on time 1028 p95 1.172s
8 may wait shed 524 expired 0 late 0 on time 1200 p95 1.912s
unbounded, 3 s deadline shed 0 expired 503 late 1158 on time 63 p95 4.03sঅপেক্ষার জায়গা না থাকলে ৬৯৬টা রিকোয়েস্ট সঙ্গে সঙ্গে প্রত্যাখ্যাত হয় আর বাকিগুলো দ্রুত। আটটা অপেক্ষার জায়গা কম প্রত্যাখ্যান করে (৫২৪), বিনিময়ে p95 বেশি। ৩ সেকেন্ডের ডেডলাইনসহ সীমাহীন কিউ কাউকে প্রত্যাখ্যান করে না আর প্রায় কাউকেই সেবা দেয় না: ৫০৩টা অপেক্ষা করতে করতেই ডেডলাইন পার করেছে, ১,১৫৮টা ডেডলাইনের পরে প্রসেস হয়েছে (কারও জন্য নয় এমন কাজ, আবার বিলও হয়েছে), আর সময়মতো হয়েছে ৬৩টা। কিউ সীমিত করুন, মেয়াদ-উত্তীর্ণ কাজ সামনে এলেই ফেলে দিন, আর ওভারলোডে সঙ্গে সঙ্গে রিট্রাই ইঙ্গিতসহ উত্তর দিন। গত পাতার AdmissionGate এই নকশাই; সিমুলেটর পরীক্ষা করে তার সংখ্যা বেছে নিতে দেয়।
টুল নোট (২০২৬-১০-১২ অনুযায়ী): HTTP 503 মানে "currently unable to handle the request due to a temporary overload or scheduled maintenance" এবং এর সঙ্গে
Retry-Afterথাকতে পারে; কিছু সার্ভার শুধু সংযোগই প্রত্যাখ্যান করে। HTTP 429 (RFC 6585) মানে "too many requests in a given amount of time"। শেডিং-পরামর্শ আমাদের নিজের নকশা। বর্তমান ডকুমেন্টেশন দেখে নিন।
অনুরোধ শৃঙ্খলজুড়ে টাইমআউট ঠিক করা
প্রতিটা হপের টাইমআউট আছে, আর সেগুলো একটার ভেতর আরেকটা বসতে হবে: প্রতিটা স্তর উপরের স্তরের আগে হাল ছাড়বে, নইলে উপরের স্তর এমন কাজ ছেড়ে দেয় যা অলক্ষ্যে চলতে থাকে (পরিত্যক্ত, তবু বিল হওয়া মডেল কল)। chain_problems এটা ServeSettings-এর বিরুদ্ধে যাচাই করে। একটা Deadline বাজেটটা রিকোয়েস্টের সঙ্গে বয়ে নেয়: প্রতিটা হপ পায় যা বাকি আছে, আর কলারের হেডার সীমা ছোট করতে পারে, কখনো বাড়াতে পারে না। call_with_deadline ডেডলাইনের পরে কোনো কল শুরু করে না, প্রতিটা চেষ্টাকে বাকি সময়ে সীমিত করে, যে ব্যর্থতা রিট্রাইয়ে সারে না তা রিট্রাই করে না, আর অপেক্ষা ডেডলাইন পার করে দিলে থেমে যায়।
import random
from evidencedesk.errors import LLMError, RetryPolicy
from evidencedesk.serve.chain import Deadline, call_with_deadline, chain_problems, layers_from
from evidencedesk.serve.settings import ServeSettings
for name, args in {"as configured": (30, 12, 3, 10, 2), "repaired": (30, 20, 2, 5, 1)}.items(): # client, gateway, tries, seconds per try, backoff
chain = layers_from(ServeSettings(), *args) # the stream limit is 15 s
print(name, [(layer.name, layer.timeout_s) for layer in chain])
for problem in chain_problems(chain):
print(" -", problem)
policy = RetryPolicy(max_attempts=4, base_delay=0.5, factor=2.0, max_delay=8.0, jitter=1.0) # jitter=1.0: wait anywhere in 0..backoff
for label, plan, seconds in (("two timeouts, then ok", ["timeout", "timeout"], 12), ("429, Retry-After 4 s, 3 s left", ["rate_limited"], 3), ("invalid request", ["invalid_request"], 12)):
clock, tries, kinds = ManualClock(0.0), [], iter(plan)
def model(timeout): # a fake model call that fails as the plan says
tries.append(round(timeout, 1))
if kind := next(kinds, None):
clock.advance(timeout if kind == "timeout" else 0.1)
raise LLMError(kind, "simulated", retry_after=4.0 if kind == "rate_limited" else None)
return "answer"
try:
outcome = call_with_deadline(model, Deadline.after(clock, seconds), policy, attempt_s=5.0, sleep=clock.advance, rng=random.Random(1))
except LLMError as error:
outcome = f"gave up: {error.kind}"
print(f"{label:<36} timeouts given {tries} -> {outcome}")as configured [('client', 30), ('gateway', 12), ('stream', 15.0), ('model client', 32)]
- stream waits 15.0s but gateway waits only 12s: gateway gives up first
- model client waits 32s but stream waits only 15.0s: stream gives up first
repaired [('client', 30), ('gateway', 20), ('stream', 15.0), ('model client', 11)]
two timeouts, then ok timeouts given [5.0, 5.0, 1.4] -> answer
429, Retry-After 4 s, 3 s left timeouts given [3.0] -> gave up: rate_limited
invalid request timeouts given [5.0] -> gave up: invalid_requestকনফিগার করা শৃঙ্খল দুবার ব্যর্থ: গেটওয়ে ১২ সেকেন্ডে হাল ছাড়ে অথচ স্ট্রিম ১৫ সেকেন্ড চলতে পারে, আর মডেল ক্লায়েন্ট ১৫ সেকেন্ডের স্ট্রিমের ভেতর ৩২ সেকেন্ড রিট্রাই করতে পারে। রানগুলোতে তৃতীয় চেষ্টা পায় মাত্র বাকি ১.৪ সেকেন্ড; যে 429-এর Retry-After (৪ সেকেন্ড) বাকি ৩ সেকেন্ডের বেশি, সেটা সঙ্গে সঙ্গে ফেরানো হয়; কলারের ভুল একবারই চেষ্টা হয়।
রিট্রাই ও আইডেমপোটেন্সি আচরণ নকশা করা
রিট্রাই ছোট ব্যর্থতাকে সেরে ওঠায় আর দীর্ঘ ব্যর্থতাকে বিভ্রাটে পরিণত করে। তিনটা নিয়ন্ত্রণ তাকে কাজের রাখে: জিটারসহ ব্যাকঅফ (প্রতিবার বেশি অপেক্ষা, এলোমেলো পরিমাণে, যাতে ক্লায়েন্টরা একসঙ্গে না ফেরে), রিট্রাই বাজেট (রিট্রাই প্রথম চেষ্টার বড়জোর ১০% যোগ করে) এবং Retry-After মেনে চলা।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): রিট্রাই নিয়ে একটা AWS Architecture Blog পোস্ট "Full Jitter", "Equal Jitter" ও "Decorrelated Jitter" নাম দেয়, Full Jitter-কে
random_between(0, min(cap, base * 2 ** attempt))হিসেবে দেয়, এবং বলে জিটার ছাড়া রিট্রাই জট পাকায়। এখানকারRetryPolicy(jitter=1.0)হলো Full Jitter।Retry-Afterএকটা HTTP-তারিখ বা সেকেন্ড; MDN বলে সমর্থন "still inconsistent"। "থাকলে মানুন, নইলে full jitter, চেষ্টা সীমিত করুন, শুধু নিরাপদ কাজ রিট্রাই করুন" আমাদের নিজের পরামর্শ। বর্তমান ডকুমেন্টেশন দেখে নিন।
মনিটরিং, বাজেট ও ইনসিডেন্ট পাতার storm সিমুলেশন (সেকেন্ডে ১২টা নতুন রিকোয়েস্ট, সক্ষমতা ২০, ৩০ সেকেন্ডের বিভ্রাট) প্রতিটা নিয়ন্ত্রণ দেখায়:
from evidencedesk.observe.resilience import BACKOFF, NAIVE, storm
for name, policy, budget in (("retry at once x4", NAIVE, None), ("backoff + full jitter", BACKOFF, None), ("same, plus a 10% retry budget", BACKOFF, 0.1)):
s = storm(policy, retry_budget=budget) # 12 new requests/s, capacity 20/s, a 30 s outage
print(f"{name:<30} calls {s.attempts:>4} peak {s.peak:>2}/s served {s.served} gave up {s.gave_up} seconds over capacity {s.overloaded_seconds}")retry at once x4 calls 2188 peak 48/s served 756 gave up 324 seconds over capacity 33
backoff + full jitter calls 1814 peak 38/s served 746 gave up 334 seconds over capacity 32
same, plus a 10% retry budget calls 1130 peak 24/s served 722 gave up 358 seconds over capacity 1সঙ্গে সঙ্গে রিট্রাই লোডকে চারগুণ করে (সেকেন্ডে ১২টা নতুন রিকোয়েস্ট ও সক্ষমতা ২০-এর বিপরীতে সর্বোচ্চ ৪৮ কল)। জিটার সর্বোচ্চ কমায়। ১০% বাজেট প্রোভাইডারকে মাত্র এক সেকেন্ড বাদে সক্ষমতার ভেতর রাখে, বিনিময়ে আরও কিছু রিকোয়েস্ট ছেড়ে দিতে হয়। তিন রানেই উত্তর পাওয়া রিকোয়েস্ট প্রায় ৭৫০: বিভ্রাটই তাকে সীমিত করে, আর রিট্রাই সক্ষমতা তৈরি করতে পারে না।
আইডেমপোটেন্সি। ক্লায়েন্ট টাইমআউট পায় কিন্তু সার্ভার কাজ শেষ করেছে; রিট্রাই কাজটা আবার করে: পড়ার ক্ষেত্রে টোকেন খরচ, "টিকেট তৈরি করুন"-এর ক্ষেত্রে দ্বিতীয় টিকেট। একটা আইডেমপোটেন্সি কি, প্রতিটা অভিপ্রেত কাজের জন্য একটা, রিকোয়েস্টের ফিঙ্গারপ্রিন্ট এবং কাজ শেষে রেসপন্সসহ রাখা হয়। পুনরাবৃত্তি সংরক্ষিত রেসপন্স ফেরায়; একই কি অন্য রিকোয়েস্টের সঙ্গে হলে সেটা ত্রুটি। IdempotencyStore.begin একটা অ্যাটমিক INSERT OR IGNORE দিয়ে ঠিক করে দৌড়ে থাকা দুটো রিকোয়েস্টের কোনটা চলবে; ব্যর্থতায় কি abandon হয় যাতে রিট্রাই চলতে পারে; কি-র মেয়াদ ফুরায়।
import anyio
from evidencedesk.serve.dedupe import IdempotencyStore, SingleFlight, handle_once
tickets = []
def create(order):
def work():
tickets.append(order)
return f"ticket {len(tickets)} for {order}"
return work
store = IdempotencyStore(clock) # sqlite in memory stands in for a shared database
for label, key, order in (("first call", "k-1", "order 1042"), ("retry after a timeout", "k-1", "order 1042"), ("same key, other order", "k-1", "order 7"), ("new key", "k-2", "order 7")):
print(f"{label:<22}", handle_once(store, key, order, create(order)), "| tickets:", len(tickets))
async def burst():
flight, calls = SingleFlight(), []
async def model():
calls.append(1)
await anyio.sleep(0.01)
return "answer"
async def one():
await flight.do("refund window", model)
async with anyio.create_task_group() as tg:
for _ in range(10):
tg.start_soon(one)
print("10 identical requests ->", len(calls), "model call,", flight.followers, "followers")
anyio.run(burst)first call (200, 'ticket 1 for order 1042') | tickets: 1
retry after a timeout (200, 'ticket 1 for order 1042') | tickets: 1
same key, other order (422, 'idempotency_key_reused') | tickets: 1
new key (200, 'ticket 2 for order 7') | tickets: 2
10 identical requests -> 1 model call, 9 followersরিট্রাই দ্বিতীয় টিকেট না বানিয়ে ১ নম্বর টিকেট ফেরায়; একই কি অন্য অর্ডারের জন্য হলে 422-এ প্রত্যাখ্যাত হয়। শেষ লাইনটা ভাইবোন নিয়ন্ত্রণ, সিঙ্গল-ফ্লাইট: দশটা অভিন্ন চলমান প্রশ্ন একটাই মডেল কল ভাগ করে নেয়, যা সবচেয়ে কাজে লাগে লোডের মধ্যে ক্যাশ করা উত্তরের মেয়াদ ফুরালে।
রেট লিমিট ও ব্যবহারকারীপ্রতি বাজেট যোগ করা
দুটো সীমা দুটো জিনিস রক্ষা করে। রেট লিমিট (টোকেন বাকেট: রিফিলের হার ও বার্স্ট) কয়েক সেকেন্ডে সক্ষমতা রক্ষা করে। বাজেট (UsageLimiter, বাজেট, ক্যাশিং ও ব্যবহারের সীমা থেকে) এক দিনে টাকা রক্ষা করে; মনিটরিং, বাজেট ও ইনসিডেন্ট পাতার BudgetKeeper ব্লক করার আগে ডিগ্রেড যোগ করে। প্রত্যাখ্যান বলে কখন ফিরতে হবে: বাকেট Retry-After হিসাব করে। ইনজেক্ট করা ঘড়ি টেস্টকে নিখুঁত করে।
from evidencedesk.limits import UsageLimiter
from evidencedesk.serve.dedupe import PerUserRate
clock = ManualClock(0.0)
rate, daily = PerUserRate(2.0, 3, clock), UsageLimiter(clock, Quota(5, 10_000), Quota(100, 10**6)) # 2 calls/s, burst 3; 5 calls/day
seen = []
for t in (0, 0, 0, 0, 0.5, 1.0, 1.0, 1.0):
clock.now = t
ok, wait = rate.take("nadia")
seen.append("ok" if ok else f"429 (retry in {wait} s)")
print(seen)
print("omar has his own bucket:", rate.take("omar"))
day = []
for _ in range(7):
day.append(daily.check("omar", 100).code) # the five-call daily budget
daily.record("omar", 100)
print(day)['ok', 'ok', 'ok', '429 (retry in 1 s)', 'ok', 'ok', '429 (retry in 1 s)', '429 (retry in 1 s)']
omar has his own bucket: (True, 0)
['ok', 'ok', 'ok', 'ok', 'ok', 'budget_exceeded', 'budget_exceeded']তিনটা কল বার্স্ট ব্যবহার করে, চতুর্থটা ১ সেকেন্ডের ইঙ্গিতসহ প্রত্যাখ্যাত, আধ সেকেন্ডে একটা টোকেন রিফিল হয়, আর অন্য ব্যবহারকারীর নিজের বাকেট আছে। দৈনিক বাজেট পাঁচটা কল দেয়, তারপর budget_exceeded বলে। দুটো কাউন্টারকেই ওয়ার্কারদের মধ্যে ভাগ করা লাগে।
প্রোভাইডার বিভ্রাটের পরিকল্পনা
একটা সার্কিট ব্রেকার পরপর ব্যর্থতার পর খুলে যায়, যাতে সার্ভিস মরা প্রোভাইডারকে আঘাত না করে দ্রুত ব্যর্থ হয়, তারপর রিসেট সময়ের পর একটা ট্রায়াল কল ছাড়ে (হাফ-ওপেন)। একটা বাল্কহেড প্রতিটা টেন্যান্ট, রুট বা প্রোভাইডারকে নিজের কুঠুরি দেয়। ফেইলওভার ব্যর্থ কলকে অন্য প্রোভাইডারে পাঠায়। স্পষ্ট ডিগ্রেডেশন সোজাসুজি বলে কিছুই কাজ না করলে ব্যবহারকারী কী পায়।
ফেইলওভার বিপজ্জনক: দ্বিতীয় প্রোভাইডার একটা ভিন্ন সিস্টেম। Router.complete-এ তিনটা নিয়ম: ফলব্যাককে প্রাইমারির মতো একই রিলিজ গেটের ভেতর দিয়ে গিয়ে ব্লক না হতে হবে, ফলে অপরীক্ষিত মডেল কখনো স্বয়ংক্রিয় ফলব্যাক হয় না; রিকোয়েস্টের ডেটা পাওয়ার জন্য সে অনুমোদিত হতে হবে; আর রেসপন্স বলবে কে সেবা দিয়েছে (served_by, tried)। কলারের ভুল তোলা হয়, অন্য কোথাও রিট্রাই হয় না। কোর্সে একটাই নকল মডেল, তাই প্রতিটা প্রোভাইডারের প্রতিনিধি একটা রিট্রিভাল কনফিগারেশন, যাকে আসল গেট স্কোর করে: কৌশলটাই আসল কথা, স্কোর নয়।
from evidencedesk.evalkit.suite import Config
from evidencedesk.llm import LLMRequest, Message
from evidencedesk.serve.failover import FakeProvider, Router
from evidencedesk.serve.rollout import preflight
stand_ins = {"main": Config("sentence", 0.35, 0.05), "backup-a": Config("paragraph", 0.35, 0.05), "backup-b": Config("sentence", 0.2, 0.05)}
verdicts = {name: preflight(cfg).verdict for name, cfg in stand_ins.items()} # the release gate of the evaluation pages, per provider
print(verdicts)
approved = frozenset(n for n, v in verdicts.items() if v != "block")
request = LLMRequest((Message("user", "How long do refunds take?"),))
def providers(main_up=True, a_up=True): # an outcome script: None is success, a string is the error kind
return [FakeProvider("main", outcomes=lambda n: None if main_up else "server"),
FakeProvider("backup-a", allows=frozenset({"public", "internal"}), outcomes=lambda n: None if a_up else "timeout"),
FakeProvider("backup-b")]
for label, up, data in (("main down", {"main_up": False}, "internal"), ("main down, personal data", {"main_up": False}, "personal"),
("main and backup-a down", {"main_up": False, "a_up": False}, "internal")):
out = Router(providers(**up), approved, ManualClock(0.0)).complete(request, data)
print(f"{label:<26} served_by={out.served_by or '-':<9} degraded={out.degraded!s:<5} {[f'{n}:{o}' for n, o in out.tried]}" + (f" -> {out.payload['code']}" if out.payload else "")){'main': 'review', 'backup-a': 'review', 'backup-b': 'block'}
main down served_by=backup-a degraded=True ['main:server', 'backup-a:ok']
main down, personal data served_by=- degraded=False ['main:server', 'backup-a:skipped:data_class', 'backup-b:skipped:not_approved'] -> temporarily_unavailable
main and backup-a down served_by=- degraded=False ['main:server', 'backup-a:timeout', 'backup-b:skipped:not_approved'] -> temporarily_unavailablebackup-b-কে গেট আটকে দেয় আর বাকি সবকিছু বন্ধ থাকলেও তাকে কখনো ডাকা হয় না: অমাপা উত্তরের চেয়ে উত্তর না থাকা ভালো। ব্যক্তিগত ডেটার ক্ষেত্রে অনুমোদিত ফলব্যাকও তার ডেটা শ্রেণির কারণে বাদ পড়ে, ফলে ব্যবহারকারী একটা স্পষ্ট সাময়িক ব্যর্থতা পায়।
| পছন্দ | মান | খরচ | গোপনীয়তা |
|---|---|---|---|
| ফলব্যাক নেই, স্পষ্ট ব্যর্থতা | উত্তর নেই, ভুল উত্তরও নয় | নেই | অপরিবর্তিত |
| সস্তা অনুমোদিত মডেল | মাপা, সাধারণত কম | কম | নিজস্ব অনুমোদন লাগে |
| সমান মানের দ্বিতীয় প্রোভাইডার | মাপা, কাছাকাছি | প্রায়ই বেশি, দ্বিতীয় চুক্তি | আপনার ডেটা দ্বিতীয় জায়গায় যায় |
from evidencedesk.serve.admission import Overloaded
from evidencedesk.serve.failover import Bulkheads
router = Router(providers(main_up=False), approved, clock)
trail = []
for _ in range(5):
trail.append((router.breaker("main").state, router.complete(request).tried[0][1]))
print(trail)
clock.advance(31)
print("31 s later:", router.breaker("main").state)
async def flood(gate, n):
admitted = 0
for _ in range(n):
try:
await gate.enter() # the calls never finish: a tenant that floods
admitted += 1
except Overloaded:
pass
return admitted
async def compare():
for name, key in (("one shared gate", lambda tenant: "all"), ("a gate per tenant", lambda tenant: tenant)):
bulk = Bulkheads(4)
print(f"{name:<18} tenant-a got {await flood(bulk.gate(key('a')), 20)} of 20, tenant-b got {await flood(bulk.gate(key('b')), 2)} of 2")
anyio.run(compare)[('closed', 'server'), ('closed', 'server'), ('closed', 'server'), ('open', 'skipped:breaker_open'), ('open', 'skipped:breaker_open')]
31 s later: half_open
one shared gate tenant-a got 4 of 20, tenant-b got 0 of 2
a gate per tenant tenant-a got 4 of 20, tenant-b got 2 of 2তিনটা ব্যর্থতার পর ব্রেকার দ্রুত ব্যর্থ হয় আর রিসেট সময়ের পর হাফ-ওপেন। বাল্কহেডের রানে আসল AdmissionGate ব্যবহার হয়েছে: একটাই ভাগ করা গেটে tenant-a-র বন্যা tenant-b-কে না খাইয়ে রাখে; টেন্যান্ট প্রতি গেটে tenant-b অক্ষত।
ডেভেলপমেন্ট, স্টেজিং ও প্রোডাকশন সেটিংস আলাদা করা
একই ইমেজ সর্বত্র চলে; এনভায়রনমেন্ট আলাদা। প্রতিটা এনভায়রনমেন্টের সেটিংস ডেটা হিসেবে রাখুন আর রুল দিয়ে যাচাই করুন। envs.check প্রতিটা সেট আসল load_serve_settings দিয়ে লোড করে (ভুল বানান ব্যর্থ হয়) এবং যোগ করে: টোকেন ও কি আসে ফাইল থেকে, মান থেকে নয় (E01, E02); প্রতিটা এনভায়রনমেন্টের সিক্রেট ফাইল তার নিজের ফোল্ডারে থাকে, ফলে একটা ফাঁস হওয়া ফাইল একটাই এনভায়রনমেন্ট খোলে (E03); অফলাইন নকল ইচ্ছা করে বাছা হয় (E04); প্রোডাকশন নকল, নামহীন মডেল বা ডেমো কলার চালাতে পারে না (E05 থেকে E07)। drift দুই এনভায়রনমেন্টের পার্থক্য দেখায়।
import copy
from evidencedesk.serve import envs
all_envs = envs.load_envs()
print({name: [f.rule for f in envs.check(name, all_envs)] for name in all_envs})
copied = copy.deepcopy(all_envs)
copied["production"] = dict(all_envs["dev"]) # production started from the dev file
for f in envs.check("production", copied):
print(f.rule, f.text[:84])
print("staging vs production differ in:", list(envs.drift(all_envs["staging"], all_envs["production"]))){'dev': [], 'staging': [], 'production': []}
E01 callers' tokens must come from a file (API_TOKENS_FILE), not from the environment
E05 production must not run the offline fake
E06 production must name its model: a default can change under you
E07 a test or demo caller name is configured
staging vs production differ in: ['EVIDENCEDESK_API_TOKENS_FILE']ডেভ ফাইল থেকে বানানো প্রোডাকশন চারটা রুলে ব্যর্থ (স্টেজিংয়ের সঙ্গে ভাগ করা সিক্রেট ফাইল E03-তে ব্যর্থ হতো)। ফাইলে সিক্রেটের পথ থাকে, সিক্রেট কখনো নয়। ডিবাগ সুইচ আর টেস্ট ক্রেডেনশিয়াল একই সমস্যা, তাই অজানা সেটিং নাম ত্রুটি।
রিলিজ কৌশল, মেট্রিক গেট ও রোলব্যাক
রিলিজ হলো ব্যবহারকারীদের উপর একটা পরীক্ষা। রোলিং ধাপে ধাপে ইনস্ট্যান্স বদলায়; কানারি নতুন ভার্সনে ছোট একটা ভাগ পাঠায় আর সংখ্যা ঠিক থাকলে তা বাড়ায়; ব্লু-গ্রিন দ্বিতীয় একটা পূর্ণ ফ্লিট তৈরি রাখে এবং সব ট্রাফিক একসঙ্গে ফ্লিপ করে, তাই ফিরিয়ে ফ্লিপ করাই রোলব্যাক। প্রতিটা ধাপে গেট দিন: ট্রাফিকের আগে অফলাইন গেট (preflight: go, review বা block); রোলআউটের সময় নতুন ভার্সনের রিকোয়েস্টকে একই জানালার পুরোনো ভার্সনের সঙ্গে তুলনা।
decide(canary, baseline) কারণসহ proceed, hold বা rollback ফেরায়। খুব কম রিকোয়েস্ট মানে hold, কখনো proceed নয়। এরর রুল মজবুত মেট্রিক ও রিলিজ গেট পাতার উইলসন ইন্টারভাল ব্যবহার করে: রোলব্যাক তখনই, যখন নতুন এরর রেটের নিচের প্রান্তও পুরোনো রেটের চেয়ে একটা মার্জিন বেশি, ফলে ছোট নমুনার ভাগ্য ভালো রিলিজ থামায় না, খারাপটাও পাস করায় না। ভ্যালিডেশন ব্যর্থতায় একই রুল; p95 ও খরচে অনুপাত। কোহর্ট স্টিকি: in_cohort ব্যবহারকারীকে হ্যাশ করে, তাই ভাগ বাড়লেও ব্যবহারকারী নতুন ভার্সনে থাকে।
বানানো পরিস্থিতি: ভার্সন ২ এমন একটা লুকআপ যোগ করে যা বেশি ট্রাফিক ব্যবহার করলে বেশি ব্যর্থ হয়। প্রতিটা কৌশল গেট বন্ধ রেখে (মনিটরিং, বাজেট ও ইনসিডেন্ট পাতার অ্যালার্ট সেটা ধরে; দশ মিনিট পর একজন মানুষ রোলব্যাক করে) ও চালু রেখে চলে:
from evidencedesk.serve.rollout import STRATEGIES, load_scenario, run_rollout
sc = load_scenario() # v2 adds a lookup that fails more as more traffic uses it (data/serve/gameday.json)
print(f"{'strategy':<11} {'gate':<5} {'outcome':<12} {'v2 requests':>11} {'bad':>5} {'back at':>8}")
for strategy in STRATEGIES:
for gated in (False, True):
r = run_rollout(sc, strategy, gated)
print(f"{strategy:<11} {'on' if gated else 'off':<5} {r.outcome:<12} {r.exposed:>11} {r.failed_exposed:>5} {r.rollback_at_s / 60:>5.0f} min")strategy gate outcome v2 requests bad back at
rolling off rolled_back 4714 1441 125 min
rolling on rolled_back 293 32 30 min
canary off rolled_back 2551 848 65 min
canary on rolled_back 451 69 30 min
blue_green off rolled_back 2100 789 35 min
blue_green on rolled_back 600 217 10 minগেট বন্ধ থাকলে খারাপ রিলিজ রোলিংয়ে ৪,৭১৪ আর কানারিতে ২,৫৫১ রিকোয়েস্টে পৌঁছায়, এবং একটা অ্যালার্ট সেটা ধরে প্রায় দুই ঘণ্টা পরে। গেট চালু থাকলে এক্সপোজার নেমে ২৯৩ ও ৪৫১। ব্লু-গ্রিন সবাইকে একসঙ্গে এক্সপোজ করে (দশ মিনিটে ৬০০ রিকোয়েস্ট) কিন্তু ফ্লিপ করে সবচেয়ে দ্রুত রোলব্যাক করে। তিনটার কোনোটাই এমন ত্রুটি খুঁজে পায় না যা শুধু স্কেলে দেখা দেয়, যতক্ষণ না গেট প্রতিটা ধাপে নজর রাখে।
রোলব্যাক ট্রিগার, ফ্ল্যাগ ও কিল সুইচ
from evidencedesk.observe.resilience import Flags, choose_mode
flags = Flags(ManualClock(0.0), values={"answer.enabled": True, "answer.mode": "auto", "prompt.version": "v1", "rollout.share": 0.0})
r = run_rollout(sc, "canary", True, flags)
for s in r.steps:
print(f"t={s.t_s / 60:>3.0f} min v2 share {s.share:>4.0%} v2 errors {s.canary_errors:>2}/{s.canary_n:<3} {s.action:<8} {'; '.join(s.reasons)}")
print([(h["from"], h["to"]) for h in flags.history], "by", flags.history[-1]["who"])
print(choose_mode(flags), end=" -> ")
flags.set("answer.enabled", False, "oncall", "kill switch drill")
print(choose_mode(flags))t= 10 min v2 share 5% v2 errors 2/32 proceed
t= 20 min v2 share 25% v2 errors 9/132 proceed
t= 30 min v2 share 50% v2 errors 50/287 rollback error rate 50/287 (low end 0.135) vs baseline 6/313
[(0.0, 0.05), (0.05, 0.25), (0.25, 0.5), (0.5, 0.0)] by rollout-bot
full -> unavailable৫%-এ নতুন ভার্সনে ৩২টা রিকোয়েস্টে ২টা এরর ছিল আর ২৫%-এ ১৩২টায় ৯টা (৬.৮%, পুরোনোটার ১.৩%-এর বিপরীতে), কিন্তু ইন্টারভালের নিচের প্রান্ত মার্জিনের ভেতরে থাকল, তাই গেট এগিয়ে গেল; ৫০%-এ ২৮৭টায় ৫০টা এরর, আর গেট থামাল। লোড-নির্ভর ত্রুটি শুরুর ধাপগুলো পার হয়ে যেতে পারে, তাই পরের ধাপেও গেট লাগে; কড়া মার্জিন আগে ধরে এবং বেশি ভালো রিলিজও থামায়। শেয়ারের প্রতিটা বদল কে ও কেন সহ লগ হয়। Flags কিল সুইচও রাখে: answer.enabled বন্ধ হলে choose_mode ফেরায় unavailable, ডিপ্লয় ছাড়া একটা স্পষ্ট সাময়িক ব্যর্থতা। প্রম্পট বা কনফিগ রোলব্যাকে ফ্ল্যাগ ব্যবহার করুন; মডেল, ডিপেন্ডেন্সি বা অ্যাপ্লিকেশনে রিডিপ্লয় লাগে।
ডিপ্লয়মেন্ট রোলব্যাক পরিকল্পনা লেখা
রোলব্যাক পরিকল্পনা এমন ডেটা যা একটা মেশিন যাচাই করতে পারে। প্রতিটা ইউনিটের (অ্যাপ্লিকেশন, ডিপেন্ডেন্সি, মডেল, প্রম্পট, কনফিগ) জন্য সে বর্তমান ও আগের সুনির্দিষ্ট ভার্সন, ধাপ, একজন মালিক, সময়ের বাজেট এবং পরে পাস করা উচিত এমন একটা টেস্টের নাম দেয়। চেকার টার্গেট হিসেবে latest, বিল্ড লাগে এমন প্রম্পট রোলব্যাক, অনুপস্থিত ইউনিট আর অস্তিত্বহীন যাচাই-টেস্ট প্রত্যাখ্যান করে। একই রিলিজে ডিপেন্ডেন্সি আপগ্রেড আর রোলব্যাক করবেন না, এবং আগের ইমেজ ও মডেল আইডি যতদিন লাগতে পারে ততদিন রাখুন।
import dataclasses
from evidencedesk.serve import rollback
plan = rollback.load_plan()
print([u.kind for u in plan], rollback.problems(plan))
broken = [dataclasses.replace(u, previous="latest") if u.kind == "model" else dataclasses.replace(u, needs_deploy=True) if u.kind == "prompt" else u for u in plan if u.kind != "config"]
for problem in rollback.problems(broken):
print("-", problem)['application', 'dependency', 'model', 'prompt', 'config'] []
- no rollback step for config
- model: previous version 'latest' is not an exact, different version
- prompt: should be a flag change, not a deploy (a rollback must not wait for a build)সক্ষমতা ও খরচের পরিকল্পনা
workers_needed লিটলের সূত্র প্রতি ওয়ার্কারে প্রয়োগ করে, প্রতিটাকে তার দুই সীমার ছোটটার ৬০%-এ চালায়, একটা অতিরিক্ত যোগ করে এবং একটা ওয়ার্কার হারালে লোড জানায়; monthly_cost কম্পিউট (ব্যস্ত বা অলস, দাম একই) আর টোকেন (ট্রাফিক অনুযায়ী) আলাদা করে।
from evidencedesk.serve.capacity import monthly_cost, workers_needed
need = workers_needed(40, 0.015, 0.8, slots=8) # 40 requests/s at peak, 60% target use, one spare worker
print(need)
print(monthly_cost(need, 20_000_000, 1200)) # 20 million requests a month, 1,200 tokens eachPlan(peak_rps=40, workers=8, slot_limit_rps=78.5, cpu_limit_rps=533.3, utilisation_at_peak=0.509, utilisation_one_down=0.582)
{'compute': 292.0, 'tokens': 48000.0, 'total': 48292.0, 'per_request': 0.002415}সেকেন্ডে ৪০ রিকোয়েস্টের জন্য আটটা ওয়ার্কার, প্রতিটায় ৮ স্লট, পিকে ৫১% ব্যবহার আর একটা হারালে ৫৮%। ৬৪ স্লট হলে একটা কোর হতো সীমা আর ২টা ওয়ার্কারই যথেষ্ট। এই বানানো সংখ্যায় টোকেনের খরচ কম্পিউটের প্রায় ১৬০ গুণ, তাই একটা ক্যাশ হিট বা ছোট প্রম্পট ওয়ার্কার টিউনিংয়ের চেয়ে বেশি বাঁচায়।
মিনি-ল্যাব: ডিপ্লয়মেন্ট রেডিনেস চেক ও একটা গেম ডে
রেডিনেস চেক এই পাতার ও আগের পাতার অফলাইন যাচাইগুলো চালায়, তারপর টেস্ট।
python -m evidencedesk.serve.readinessREADINESS (offline)
[ok ] acceptance list 20 criteria, each with a check
[ok ] container files no failing rule
[ok ] smoke test 7 steps pass, no key involved
[ok ] environments dev, staging, production pass their rules
[ok ] timeout chain each layer gives up before the one above
[ok ] load at planned peak 4 req/s: shed 3.3%, p95 1.18s
[ok ] release gate offline evaluation gate says review
[ok ] game-day replay gated canary exposed 451, ungated rolling 4714
[ok ] rollback plan 5 release units, each with a previous version and a check
RESULT: READYpython -m pytest kit/tests/test_scale_*.py | tail -n 1 | sed 's/ in [0-9.]*s//'55 passedগেম ডে দলে চালান। একজন অন্যদের না জানিয়ে data/serve/gameday.json বদলায় (নতুন ত্রুটি, অন্য ঢাল, খরচের রিগ্রেশন); বাকিরা গেট চালু রেখে সেটা রিপ্লে করে, রোলব্যাকের কারণ পড়ে, আর রোলব্যাক পরিকল্পনা দেখে ঠিক করে প্রথমে কী ফ্লিপ করবে। জমা দিন রেডিনেস আউটপুট, রিপ্লে টেবিল, সিক্রেট চেকলিস্ট (E01 থেকে E03) এবং যাচাই করা রোলব্যাক পরিকল্পনা। এটা দেখাতে পারে না যে ইমেজ বিল্ড হয়, বা আসল ট্রাফিক ও আসল অর্কেস্ট্রেটর সিমুলেশনের মতো আচরণ করে।
কাজের জায়গায় এটা কোথায় দেখা যায়
ওসাকার আইকো ৪০০ সাপোর্ট এজেন্টের ব্যবহার করা একটা অ্যাসিস্ট্যান্টের অন-কলে। প্রোভাইডার বিভ্রাটের পর সে যোগ করে রিট্রাই বাজেট, ডেডলাইন হেডার ও একটা অনুমোদিত ফলব্যাক; একটা খারাপ প্রম্পট রিলিজের পর যোগ করে গেটসহ কানারি ও কিল সুইচ। পরের বিভ্রাট হয় পনেরো মিনিটের রোলব্যাক, ৩% ব্যবহারকারী ছুঁয়ে। (এটা একটা বোঝানোর গল্প।)
সাধারণ ভুল
- সীমাহীন কিউ। এটা ওভারলোডকে ধীর, দেরি করে আসা, বিল হওয়া উত্তরে পরিণত করে। সীমিত করুন, মেয়াদ-উত্তীর্ণ কাজ ফেলুন, ইঙ্গিতসহ 503 দিন।
- যে টাইমআউট একটার ভেতর আরেকটা বসে না। বাইরের স্তর হাল ছাড়ে আর কাজ চলতে থাকে। শৃঙ্খল যাচাই করুন; বাকি সময় নিচে পাঠান।
- ৫% কানারিতে ভরসা। লোড-নির্ভর ত্রুটি তা পার হয়ে যায়। প্রতিটা ধাপে গেট দিন; রোলব্যাক এক ধাপ দূরে রাখুন।
- অপরীক্ষিত মডেলে ফেইলওভার। সে উত্তর দেয়, তাই মান বা গোপনীয়তার বদল কেউ টের পায় না। গেট দিন, ডেটা শ্রেণি যাচাই করুন, কে সেবা দিল লিখে রাখুন।
নিজে চেষ্টা করুন
- সহজ। বাজেট, ক্যাশিং ও ব্যবহারের সীমা পাতা আবার দেখুন। একজন ব্যবহারকারী দিনে ৫টা কল করতে পারে; তিনটা ওয়ার্কারের প্রত্যেকে নিজের
UsageLimiterরাখে। অনুমান করুন ৩০টা কলের কতগুলো অনুমোদিত হয়, তারপর ভাগ করা লিমিটারের সঙ্গে মেলান। - মাঝারি। মনিটরিং, বাজেট ও ইনসিডেন্ট পাতা আবার দেখুন (খরচের অ্যালার্ট)। গেম-ডের প্রার্থীকে এমন বানান যার কোনো নতুন এরর নেই কিন্তু খরচ ১.৮ গুণ, আর কানারি রিপ্লে করুন। অনুমান করুন কোথায় ও কেন থামে।
- কঠিন। কন্টেইনারাইজেশন ও স্ট্রিমিং ডিপ্লয়মেন্ট পাতা আবার দেখুন (অপেক্ষার জায়গা নেই)। ওয়ার্কারপ্রতি ৮ স্লটে সিমুলেটর দিয়ে বের করুন সেকেন্ডে ৪০ রিকোয়েস্টে, তারপর ৬০-এর স্পাইকে, ১%-এর কম শেড করে এমন সবচেয়ে ছোট ফ্লিট। পরিকল্পনাকারীর ৮-এর সঙ্গে তুলনা করুন।
উত্তর
সহজ। তিনটা আলাদা লিমিটার দেয় ৩ × ৫ = ১৫; ভাগ করা একটা দেয় ৫।
for shared in (False, True):
c = Cluster(3, shared, ManualClock(0.0), per_user=Quota(5, 10**6)) # a limit of 5 calls a day, three workers
print("shared" if shared else "per worker", "->", sum(c.ask("omar", f"q{i}")["status"] == 200 for i in range(30)), "of 30 allowed")per worker -> 15 of 30 allowed
shared -> 5 of 30 allowedমাঝারি। খরচের রুল ৩০ রিকোয়েস্টওয়ালা প্রথম ধাপেই চালু হয়, তাই রিলিজ ৩১টা রিকোয়েস্টের পর ৫%-এ থামে।
from dataclasses import replace
costly = replace(sc, candidate=replace(sc.candidate, error_slope=0.0, cost=0.0018)) # v2: no new errors, but 1.8x the cost per request
r = run_rollout(costly, "canary")
for s in r.steps:
print(f"v2 share {s.share:.0%}, {s.canary_n} v2 requests -> {s.action}: {'; '.join(s.reasons)}")v2 share 5%, 31 v2 requests -> rollback: cost 0.0018 vs 0.001কঠিন। ছয়টা ওয়ার্কার পিক সামলায় (০.১% শেড) কিন্তু স্পাইক নয় (১২%); পরিকল্পনাকারীর আটটা দুটোই পার হয় (৬০-এ ০.৭%)। ৬০% লক্ষ্য আর অতিরিক্ত ওয়ার্কার স্পাইকের জায়গা কিনে দেয়। আসল ফ্লিটের লাগে এই এক-কিউ মডেলের চেয়ে বেশি।
for rate in (40, 60): # planned peak, then a spike of 1.5x
print(rate, "req/s:", {n: f"{100 * (r := simulate(Profile(rate), Service(n, 8))).shed / r.offered:.1f}% shed" for n in (4, 5, 6, 8)})40 req/s: {4: '14.4% shed', 5: '3.2% shed', 6: '0.1% shed', 8: '0.0% shed'}
60 req/s: {4: '38.2% shed', 5: '24.1% shed', 6: '12.2% shed', 8: '0.7% shed'}পাস চেকলিস্ট:
- সহজ: ১৫ ও ৫, কারণসহ (প্রতি প্রসেসে একটা কাউন্টার)।
- মাঝারি: খরচে ৫%-এ থামে, কোনো এরর রুল চালু হয়নি।
- কঠিন: পিকের জন্য ৬, স্পাইকের জন্য ৮, আর সিমুলেশন যা বাদ দেয় তা।
সারসংক্ষেপ
- I/O-নির্ভর মডেল কলে আগে কনকারেন্সি (স্লট), তারপর প্রসেস লাগে; ওয়ার্কাররা কিছু ভাগ করে না, তাই ভাগ করা সীমা ও ক্যাশের জন্য ভাগ করা স্টেট লাগে।
- ভার্চুয়াল সময়ে নকল ক্লায়েন্ট দিয়ে লোড টেস্ট করুন: স্যাচুরেশন কার্ভ ও লিটলের সূত্র আসল ট্রাফিকের আগেই সীমা দেখায়।
- কিউ সীমিত করুন, রিট্রাই ইঙ্গিতসহ শেড করুন, টাইমআউট একটার ভেতর আরেকটা বসান, বাকি ডেডলাইন নিচে পাঠান।
- ব্যাকঅফ, জিটার ও বাজেটসহ রিট্রাই করুন,
Retry-Afterমানুন, পুনরাবৃত্ত কাজ আইডেমপোটেন্ট করুন। - শুধু গেট-পাস, ডেটা-অনুমোদিত প্রোভাইডারে ফেইলওভার করুন আর কে সেবা দিল লিখুন; এনভায়রনমেন্ট সেটিংস রুল দিয়ে যাচাই করুন।
- মেট্রিক গেটসহ কানারিতে রিলিজ করুন, ফ্ল্যাগ বা পরিকল্পনায় রোলব্যাক করুন, গেম ডেতে মহড়া দিন; সিমুলেশন শুধু কৌশল দেখায়।
এরপর: ইনজেশন ও অনুমতি-সচেতন রিট্রিভাল প্রোডাকশনের ডেটার দিকে যায়: ইনজেশন চুক্তি, আইডেমপোটেন্ট পুনঃইনজেশন, ভার্সন ও উৎস-প্রমাণ, মুছে ফেলা, এবং পরিবর্তনের পরও টিকে থাকা অনুমতি যাচাই।