بخش تخصصی (۸): RGW — مسیر درخواست، SigV4، سهمیه، محدودیت نرخ، gc و lifecycle
حوصله ندارم این فصل در چند خط
- frontend از نوع Beast هر اتصال را در یک coroutine روی thread pool اجرا میکند؛ درخواست به یک op با گامهای ثابت تبدیل میشود.
- احراز هویت SigV4 کلید امضا را از secret، تاریخ، region و سرویس میسازد؛ فصل یک مثال را گامبهگام محاسبه میکند.
- quotaها نرماند (در هر RGW cache میشوند) و rate limitها token bucket به ازای کاربر یا bucket هستند.
- tailهای حذفشده به صف gc میروند و بعداً آزاد میشوند؛ lifecycle در پنجرهی کاری روزانه اجرا میشود و
rgw_lc_debug_interval آن را برای آزمون سریع میکند.
۴۱-۱ پیش از شروع#
فصل ۴۰ نشان داد دادهی RGW روی RADOS چگونه چیده میشود. این فصل همان داده را در حرکت دنبال میکند: یک درخواست HTTP از Beast تا اجرای op چه مراحلی را طی میکند، امضای SigV4 دقیقاً چگونه بازسازی و بررسی میشود، PUT و GET چگونه داده را در پنجرههای موازی جابهجا میکنند، سهمیه و محدودیت نرخ در کجای مسیر و با چه حسابی اعمال میشوند، و دو کارگر پسزمینه یعنی gc و lifecycle کار خود را چگونه تقسیم و زمانبندی میکنند. پیشفرضها از src/common/options/rgw.yaml.in در شاخهی main (نوامبر ۲۰۲۶) خوانده شدهاند.
۴۱-۱-۱ این فصل به چه سؤالهایی جواب میدهد؟#
- چرا یک درخواست با امضای درست ممکن است با
RequestTimeTooSkewed رد شود؟ - کلید امضای SigV4 از کلید محرمانه با چند HMAC به دست میآید؟
- سهمیهی حجم با اندازهی واقعی شیء مقایسه میشود یا با مقدار گردشده؟
- محدودیت نرخ در هر RGW جداگانه است یا در کل خوشه؟
- چرا پس از حذف یک bucket بزرگ، فضای pool تا چند ساعت آزاد نمیشود؟
- lifecycle کدام bucket را چه زمانی پردازش میکند؟
۴۱-۱-۲ پیشنیاز#
فصل ۴ (RGW)، فصل ۴۰ (مدل داده) و فصل ۲۱ (تنظیم کارایی).
۴۱-۲ از سوکت تا op#
frontend پیشفرض Beast است (rgw_frontends = "beast port=7480") که بر پایهی Boost.Asio هر اتصال را در یک coroutine روی مجموعهای از نخها (rgw_thread_pool_size ۱۲۸) اجرا میکند. وقتی rgw_beast_enable_async روشن است (پیشفرض)، انتظار برای RADOS نخ را مسدود نمیکند و یک نخ میتواند هزاران درخواست همزمان را جلو ببرد. rgw_max_concurrent_requests (۱۰۲۴) سقف درخواستهای در جریان است.
منبع در کد: src/rgw/rgw_process.cc :: process_request، rgw_process_authenticated؛ src/rgw/rgw_asio_frontend.cc
پیچیدگی: هزینهی ثابت هر درخواست: احراز هویت، خواندن ACL و policy از cache، سپس op->execute
pseudocode
process_request(req):
s = req_state ; rest handler by dialect (S3, Swift, admin, STS ...) from URL and headers
op = handler.get_op() # e.g. RGWPutObj_ObjStore_S3
handler.authorize() # SigV4/V2, STS, anonymous ...
normalize buckets and tenants
rgw_process_authenticated(handler, op):
init permissions # bucket and user policy, ACL (cached)
reading permissions ; init op ; verifying op mask
verifying CORS origin matches ; verifying op permissions (IAM + ACL)
verifying op params ; pre-executing
check rate limiting → 503 SlowDown + Retry-After when delayed
executing: op->execute() # RADOS I/O, quota check inside
completing: op->complete() → response, ops log, usage log, bucket notificationهر مرحله با debug_rgw = 2 در لاگ دیده میشود (مثلاً req 123 0.000s s3:put_obj verifying op permissions) و زمان نسبی کنار هر خط آمده است؛ همین سادهترین راه برای یافتن مرحلهی کند است.

۴۱-۳ امضای SigV4#
منبع در کد: src/rgw/rgw_auth_s3.cc :: get_v4_canonical_headers، get_v4_string_to_sign، get_v4_signing_key، get_v4_signature؛ src/rgw/rgw_auth_s3.h :: RGW_AUTH_GRACE
پیچیدگی: پنج HMAC-SHA256 و یک SHA256 روی canonical request؛ کلید امضا قابل cache است
pseudocode
parse Authorization: AWS4-HMAC-SHA256 Credential=AK/YYYYMMDD/region/s3/aws4_request,
SignedHeaders=h1;h2;..., Signature=hex
reject if |now - x-amz-date| > RGW_AUTH_GRACE (15 min) # RequestTimeTooSkewed
canonical = METHOD \n URI \n sorted query \n "name:value\n" for each signed header \n
SignedHeaders \n payload hash (or UNSIGNED-PAYLOAD / STREAMING-...)
sts = "AWS4-HMAC-SHA256" \n x-amz-date \n "YYYYMMDD/region/s3/aws4_request" \n hex(sha256(canonical))
kDate = HMAC("AWS4" + secret, YYYYMMDD) ; kRegion = HMAC(kDate, region)
kService = HMAC(kRegion, "s3") ; kSigning = HMAC(kService, "aws4_request")
ok if hex(HMAC(kSigning, sts)) == Signature (constant-time compare)
streaming upload: every chunk carries its own signature chained from the previous oneمثال عددی. با یک کلید محرمانهی ساختگی (EXAMPLE/secret/NOT+A+REAL+KEY0000000000)، region default، تاریخ 20261115T071500Z و درخواست GET /photos/cat.jpg با سه سرآیند امضاشده و UNSIGNED-PAYLOAD:
مثال:
sha256(canonical request) = 36f24a8179a5bc758bde32b4a6932f313216b6c9f7d84e04e33bd7a2086b87f5
kSigning = 38793786cc844d69350de848f15fe23b8f7887e3bcbb48d552b2e0a2c1d9746d
Signature = 5e63a875b9d9e0ec540db8784929f6d4c5799c453bd5442804a9a7bc7f57aff8تغییر یک بایت در هر کدام از ورودیها امضا را کاملاً عوض میکند. دو خطای رایج: region امضا با api_name zonegroup یکی نیست (SignatureDoesNotMatch)، و یک proxy سرآیند Host را بازنویسی کرده است. با debug_rgw = 20 RGW متن canonical request و string to sign را چاپ میکند و میشود آن را با خروجی کلاینت مقایسه کرد.

هیچگاه لاگ debug_rgw = 20 یک محیط واقعی را بدون پاک کردن سرآیندها به اشتراک نگذارید؛ مقدار Authorization، توکنهای STS و گاهی بدنهی درخواست در آن است.
۴۱-۴ PUT و GET در حرکت#
منبع در کد: src/rgw/driver/rados/rgw_putobj_processor.cc؛ src/rgw/rgw_op.cc :: RGWPutObj::execute، RGWGetObj::execute؛ src/rgw/driver/rados/rgw_rados.cc :: get_obj_iterate
پیچیدگی: PUT: O(size) با حداکثر rgw_put_obj_min_window_size بایت در پرواز؛ GET: پنجرهی rgw_get_obj_window_size
pseudocode
PUT:
check quota (estimated size = Content-Length) # before any data write
processor = Atomic (or Multipart part / Append)
loop body chunks: filter chain (compression, encryption, etag md5) → StripeProcessor → ChunkProcessor
aio write chunk ; keep up to rgw_put_obj_min_window_size (16 MiB) in flight
complete: wait aio ; index prepare → write head attrs + manifest → index complete (alg-40-1)
old object's tail (if overwritten) → gc chain with tail_tag
GET:
read head (attrs + up to first chunk of data) ; check conditions (If-Match, Range ...)
walk manifest from range start: each RADOS read ≤ rgw_get_obj_max_req_size (4 MiB)
keep up to rgw_get_obj_window_size (16 MiB) outstanding ; stream to client in order
filters in reverse (decrypt, decompress)بنابراین توان یک جریان واحد PUT یا GET تقریباً «پنجره تقسیم بر تأخیر یک نوشتن یا خواندن RADOS» است. برای شیء بزرگ روی یک اتصال پرتأخیر، بزرگ کردن پنجرهها کمک میکند ولی مصرف حافظه را بهازای هر درخواست بالا میبرد. کلاینتهای S3 معمولاً با multipart موازی همین محدودیت را دور میزنند.
۴۱-۵ سهمیه#
منبع در کد: src/rgw/rgw_quota.cc :: RGWQuotaHandlerImpl::check_quota، RGWQuotaInfoDefApplier، RGWQuotaInfoRawApplier؛ src/rgw/rgw_common.h :: rgw_rounded_objsize
پیچیدگی: O(۱) با cache؛ بازخوانی آمار هر rgw_bucket_quota_ttl
pseudocode
check_quota(bucket, owner, num_objs = 1, size):
for scope in (bucket quota, user quota) if enabled:
stats = quota cache (refreshed after rgw_bucket_quota_ttl = 10 min, or on async update)
if max_objects >= 0 and stats.num_objects + num_objs > max_objects → QuotaExceeded
if max_size >= 0:
if check_on_raw: exceeded = stats.size + size > max_size
else: exceeded = stats.size_rounded + round_up(size, 4096) > max_size
→ 403 QuotaExceededدو پیامد: سهمیه بهازای هر RGW از cache خوانده میشود، پس چند RGW همزمان میتوانند کمی از سقف عبور کنند (سهمیه «نرم» است)؛ و در حالت پیشفرض هر شیء دستکم ۴ KiB حساب میشود، پس میلیونها شیء ۱۰۰ بایتی سهمیه را بسیار زودتر از حجم واقعی پر میکنند. آمار سهمیهی کاربر با radosgw-admin user stats --uid U --sync-stats بازسازی میشود.
۴۱-۶ محدودیت نرخ#
منبع در کد: src/rgw/rgw_ratelimit.h :: RateLimiterEntry؛ src/rgw/rgw_process.cc
پیچیدگی: O(۱) بهازای هر درخواست؛ یک ورودی برای هر account، کاربر و bucket در حافظهی هر RGW
pseudocode
tokens kept ×1000 (fixed point) ; interval = rgw_ratelimit_interval (60 s)
on request, levels checked in order: account, user, bucket (each with its own bucket of tokens):
elapsed = now - ts ; refill = limit × elapsed / interval (capped at limit)
op type ∈ {read, write, list, delete} # list/delete classes: main branch
if ops tokens < 1 or bytes tokens < 0:
delay = ceil(needed × interval / limit) seconds → 503 SlowDown, Retry-After: delay
else take 1 op token
after response: subtract transferred bytes from read or write bytes bucket (may go negative)
if a lower level blocks, op tokens already taken at higher levels are given backمثال. با max-read-ops = 600 در بازهی ۶۰ ثانیه، سطل هر ثانیه ۱۰ توکن پر میشود و حداکثر ۶۰۰ توکن جمع میکند. کاربری که پس از یک دقیقه سکوت ۷۰۰ GET پشتسرهم بفرستد، ۶۰۰ درخواست اول پذیرفته و بقیه با ۵۰۳ رد میشوند تا سطل پر شود. چون سطل در حافظهی هر RGW است، پشت یک load balancer با چهار RGW سقف مؤثر تا چهار برابر میشود؛ مقدار را بر تعداد RGWها تقسیم کنید.
# مثال: تنظیم محدودیت برای یک کاربر
radosgw-admin ratelimit set --ratelimit-scope=user --uid=alice --max-read-ops=600 --max-write-bytes=104857600
radosgw-admin ratelimit enable --ratelimit-scope=user --uid=alice
radosgw-admin ratelimit get --ratelimit-scope=user --uid=aliceمثال (خروجی):
{ "user_ratelimit": { "max_read_ops": 600, "max_write_ops": 0, "max_list_ops": 0, "max_delete_ops": 0,
"max_read_bytes": 0, "max_write_bytes": 104857600, "enabled": true } }مقدار ۰ یعنی نامحدود. فیلدهای list و delete فقط در نسخههایی هستند که این دستهها را دارند.
۴۱-۷ garbage collection#
حذف یا بازنویسی یک شیء فقط head و ایندکس را فوراً عوض میکند. tailها در یک زنجیره (cls_rgw_obj_chain) با برچسب tail_tag به صف gc سپرده میشوند تا درخواستهای GET در جریان که هنوز manifest قدیمی را دارند، tail را از دست ندهند.
منبع در کد: src/rgw/driver/rados/rgw_gc.cc :: RGWGC::tag_index، send_chain، process؛ src/cls/rgw_gc
پیچیدگی: enqueue O(۱)؛ process O(تعداد اشیای منقضی) با rgw_gc_max_concurrent_io حذف موازی
pseudocode
enqueue(chain, tag):
shard = XXH64(tag, seed 8675309) mod rgw_gc_max_objs (32) → object gc.<shard> in <zone>.rgw.log:gc
cls rgw_gc.queue_enqueue(entry{tag, chain, time = now + rgw_gc_obj_min_wait (2 h)})
(split into several entries if chain too large for one queue entry)
gc thread every rgw_gc_processor_period (1 h), for up to rgw_gc_processor_max_time (1 h):
for each shard in random start order:
try lock "gc_process" on gc.<shard> ; skip if held by another RGW
list expired entries ; delete each tail object (≤ rgw_gc_max_concurrent_io = 10 in flight)
remove processed entries from queue (trim in chunks of rgw_gc_max_trim_chunk)مثال. حذف bucketای با یک میلیون شیء ۶۴ MiB تقریباً ۱۵ میلیون tail ۴ MiB به صف میفرستد. با پیشفرضها هیچکدام زودتر از ۲ ساعت حذف نمیشوند و هر RGW در هر چرخهی ساعتی با ۱۰ حذف همزمان کار میکند؛ اگر هر حذف ۵ میلیثانیه طول بکشد، یک RGW در ساعت حدود ۷ میلیون tail پاک میکند. افزایش rgw_gc_max_concurrent_io یا اجرای دستی radosgw-admin gc process --include-all سرعت را بالا میبرد ولی بار OSD را هم.

۴۱-۸ lifecycle#
منبع در کد: src/rgw/rgw_lc.cc :: get_lc_index، RGWLC::process، bucket_lc_process، LCOpAction_*؛ src/rgw/rgw_lc.h
پیچیدگی: هر bucket روزی یک بار؛ O(تعداد اشیای منطبق با prefixها) فهرستگیری
pseudocode
PutBucketLifecycle: store rules in bucket attr ; add entry "<tenant>:<bucket>:<marker>" to shard
lc.<ceph_str_hash_linux(key) mod 7877 mod rgw_lc_max_objs (32)> in <zone>.rgw.log:lc
rgw_lc_max_worker (3) worker threads, active only inside rgw_lifecycle_work_time (00:00-06:00):
pick shard ; lock "lc_process" (rgw_lc_lock_max_time 90 s, renewed) ; take next entry not done today
bucket_lc_process(entry):
skip if bucket marker != entry marker (bucket was recreated)
group rules by prefix ; list objects (and versions) per prefix
for each object, run matching actions with up to rgw_lc_max_wp_worker (128) parallel ops:
CurrentExpiration, NonCurrentExpiration, DMExpiration (expired delete markers),
CurrentTransition, NonCurrentTransition (copy tail to class pool, new manifest)
abort incomplete multipart uploads older than N days
mark entry complete with start timeنکتههای عملیاتی: تاریخ انقضا بر پایهی روز است و اجرا در پنجرهی کاری انجام میشود، پس «Expiration: 1 day» یعنی «در نخستین پنجره پس از گذشت یک روز». برای آزمایش، rgw_lc_debug_interval (پیشفرض -۱، خاموش) یک «روز» را به چند ثانیه کوتاه میکند و هرگز نباید در تولید روشن بماند. radosgw-admin lc list وضعیت هر bucket را نشان میدهد:
مثال (خروجی lc list):
[ { "bucket": ":logs:a1b2c3d4-...4123.9", "shard": "lc.17", "started": "Sun, 15 Nov 2026 00:00:04 GMT", "status": "COMPLETE" },
{ "bucket": ":photos:a1b2c3d4-...4123.7", "shard": "lc.4", "started": "Sun, 15 Nov 2026 00:00:06 GMT", "status": "PROCESSING" } ]۴۱-۹ دستورها با کلاینتهای S3#
| کار | aws cli | mc |
|---|---|---|
| تعریف endpoint | aws --endpoint-url https://s3.example.test s3 ls | mc alias set c1 https://s3.example.test AK SK |
| آپلود چندبخشی موازی | aws configure set s3.multipart_chunksize 64MB سپس aws s3 cp f s3://b/ | mc cp f c1/b/ |
| نسخهبندی | aws s3api put-bucket-versioning --bucket b --versioning-configuration Status=Enabled | mc version enable c1/b |
| فهرست نسخهها | aws s3api list-object-versions --bucket b | mc ls --versions c1/b |
| lifecycle | aws s3api put-bucket-lifecycle-configuration --bucket b --lifecycle-configuration file://lc.json | mc ilm rule add --expire-days 30 c1/b |
| storage class | aws s3 cp f s3://b/ --storage-class COLD | mc cp --storage-class COLD f c1/b/ |
| URL امضاشده | aws s3 presign s3://b/k --expires-in 3600 | mc share download --expire 1h c1/b/k |
# مثال: یک lifecycle ساده با انتقال و انقضا
cat > lc.json <<'J'
{ "Rules": [ { "ID": "logs", "Status": "Enabled", "Filter": { "Prefix": "logs/" },
"Transitions": [ { "Days": 30, "StorageClass": "COLD" } ],
"Expiration": { "Days": 365 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 } } ] }
J
aws --endpoint-url https://s3.example.test s3api put-bucket-lifecycle-configuration --bucket logs --lifecycle-configuration file://lc.json۴۱-۱۰ تنظیم و عیبیابی#
| نشانه | علت محتمل | اقدام |
|---|---|---|
RequestTimeTooSkewed | ساعت کلاینت یا RGW بیش از ۱۵ دقیقه اختلاف دارد | NTP/chrony |
SignatureDoesNotMatch فقط پشت proxy | بازنویسی Host یا مسیر | ارسال Host اصلی، بررسی rgw_dns_name |
| ۵۰۳ SlowDown با Retry-After | محدودیت نرخ | ratelimit get، تقسیم سقف بر تعداد RGW |
| ۵۰۳ بدون Retry-After | rgw_max_concurrent_requests یا صف پر | افزایش RGW یا سقف، بررسی تأخیر OSD |
| QuotaExceeded زودتر از انتظار | گردکردن ۴ KiB یا آمار کهنه | user stats --sync-stats، بررسی check_on_raw |
| فضای pool پس از حذف آزاد نمیشود | صف gc | شمردن ورودیهای gc list --include-all، سپس gc process |
| lifecycle اجرا نمیشود | خارج از پنجره، قفل گیرکرده، marker کهنه | lc list، lc process --bucket B |
۴۱-۱۱ تکمیلی و بهروز#
- دستههای list و delete در محدودیت نرخ و سرآیند
Retry-After در شاخهی main دیده میشوند؛ در نسخههای قدیمیتر فقط read و write وجود داشت. - صف gc بر پایهی cls
rgw_gc (صف دو مرحلهای روی یک شیء) است؛ نسخههای قدیمی از omap استفاده میکردند و مسیر مهاجرت در RGWGC::list هنوز هر دو را میخواند. -
rgw_lc_max_rules (۱۰۰۰) سقف قواعد یک bucket است.
۴۱-۱۲ تمرین عملی#
- ۱. یک درخواست GET را با یک اسکریپت پایتون (فقط hashlib و hmac) امضا کنید و با
curl بفرستید. سپس یک سرآیند را عوض کنید و SignatureDoesNotMatch را ببینید. - ۲. برای یک کاربر سقف ۶۰ read op در دقیقه بگذارید و با یک حلقهی curl زمان اولین ۵۰۳ و مقدار Retry-After را بیابید.
- ۳. یک bucket با ۱۰۰ شیء ۱۰ MiB را حذف کنید و تعداد ورودیهای
gc list --include-all را پیش و پس از gc process --include-all بشمارید. - ۴. با
rgw_lc_debug_interval = 10 روی یک خوشهی آزمایشی، یک قاعدهی Expiration یکروزه را ظرف چند ثانیه ببینید.
۴۱-۱۳ پرسشهای مرور#
- ۱. مراحل اصلی
rgw_process_authenticated به ترتیب کداماند؟ - ۲. RGW اختلاف زمان مجاز درخواست SigV4 را چقدر میگیرد و چه خطایی میدهد؟
- ۳. کلید امضای SigV4 چگونه از کلید محرمانه مشتق میشود؟
- ۴. چه چیزی توان یک جریان PUT را محدود میکند و کدام گزینه آن را تنظیم میکند؟
- ۵. در حالت پیشفرض، سهمیهی حجم شیء ۱۰۰ بایتی را چقدر حساب میکند؟
- ۶. چرا سهمیه در RGW «نرم» است؟
- ۷. محدودیت نرخ ۶۰۰ read op در دقیقه پشت سه RGW در عمل چقدر است؟
- ۸. shard gc یک زنجیره چگونه انتخاب میشود و چرا tail فوراً حذف نمیشود؟
- ۹. lifecycle چگونه bucket را به shard میبرد و در چه بازهای کار میکند؟
- ۱۰. چرا bucket_lc_process مقدار marker را بررسی میکند؟
پاسخ این پرسشها در پیوست ه: پاسخ پرسشها آمده است. پاسخ پرسشهای این فصل →
۴۱-۱۴ منابع این فصل#
- rgw_process.cc
https://github.com/ceph/ceph/blob/main/src/rgw/rgw_process.cc - rgw_auth_s3.cc
https://github.com/ceph/ceph/blob/main/src/rgw/rgw_auth_s3.cc - rgw_ratelimit.h
https://github.com/ceph/ceph/blob/main/src/rgw/rgw_ratelimit.h - rgw_quota.cc
https://github.com/ceph/ceph/blob/main/src/rgw/rgw_quota.cc - rgw_gc.cc
https://github.com/ceph/ceph/blob/main/src/rgw/driver/rados/rgw_gc.cc - rgw_lc.cc
https://github.com/ceph/ceph/blob/main/src/rgw/rgw_lc.cc - AWS Signature Version 4
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html - مستند gc و lifecycle در Ceph
https://docs.ceph.com/en/latest/radosgw/config-ref/