Redis-ൽ ഒരു Sliding Window Rate Limiter ഞാൻ എങ്ങനെ നിർമ്മിച്ചു
ഞങ്ങളുടെ വീഡിയോ API എല്ലാ ദിവസവും വൈകുന്നേരം 8pm UTC-ക്ക് തകരാറിലാകാറുണ്ടായിരുന്നു.
അത് യഥാർത്ഥ ട്രാഫിക് ആയിരുന്നില്ല. ഏതാനും സ്ക്രാപ്പർമാർ (scrapers) ഞങ്ങളുടെ ട്രെൻഡിംഗ് എൻഡ്പോയിന്റ് (trending endpoint) കണ്ടെത്തി അതിനെ നിരന്തരം ആക്രമിച്ചു. ഞങ്ങളുടെ ഡാറ്റാബേസ് ക്വറികൾ (database queries) കുമിഞ്ഞുകൂടി. യഥാർത്ഥ ഉപയോക്താക്കൾക്ക് വീഡിയോകൾക്ക് പകരം ലോഡിംഗ് സ്പിന്നറുകൾ മാത്രമാണ് കാണാൻ കഴിഞ്ഞത്.
ഞങ്ങൾക്ക് റേറ്റ് ലിമിറ്റിംഗ് (rate limiting) ആവശ്യമായിരുന്നു. മിക്ക ലളിതമായ രീതികളും പ്രശ്നം കൂടുതൽ വഷളാക്കുകയാണ് ചെയ്തത്.
Fixed Window കൗണ്ടറുകളിലെ പ്രശ്നം:
മിക്ക ആളുകളും ഫിക്സഡ് വിൻഡോകളിലൂടെയാണ് (fixed windows) തുടങ്ങുന്നത്. ഒരു നിശ്ചിത സമയപരിധിക്കുള്ളിലെ റിക്വസ്റ്റുകൾ നിങ്ങൾ എണ്ണുന്നു. നിങ്ങളുടെ പരിധി മിനിറ്റിൽ 100 ആണെങ്കിൽ, ഒരു ഉപയോക്താവിന് 11:59:59-ൽ 100 റിക്വസ്റ്റുകളും 12:00:00-ൽ മറ്റൊരു 100 റിക്വസ്റ്റുകളും അയക്കാൻ കഴിയും. അതായത് ഒരു സെക്കൻഡിൽ 200 റിക്വസ്റ്റുകൾ. നിങ്ങളുടെ പരിധികൾ മറികടക്കാൻ സ്ക്രാപ്പർമാർ ഈ അതിർവരമ്പുകളെ ഉപയോഗപ്പെടുത്തുന്നു.
പരിഹാരം: Sliding Window.
ഒരു സ്ലൈഡിംഗ് വിൻഡോ നിലവിലെ സമയവുമായി ബന്ധപ്പെട്ട കഴിഞ്ഞ N സെക്കൻഡുകളിലെ റിക്വസ്റ്റുകൾ എണ്ണുന്നു. ഇതിൽ ചൂഷണം ചെയ്യാൻ പാകത്തിലുള്ള അതിർവരമ്പുകൾ ഇല്ല.
ഞങ്ങളുടെ ആവശ്യങ്ങൾക്കനുസരിച്ച് ഞങ്ങൾ രണ്ട് വ്യത്യസ്ത രീതികൾ ഉപയോഗിക്കുന്നു:
1. Sliding Window Log (കൃത്യതയ്ക്കായി)
ഓതന്റിക്കേറ്റഡ് (authenticated) ഉപയോക്താക്കൾക്കായി ഞങ്ങൾ Redis Sorted Sets (ZSET) ഉപയോഗിക്കുന്നു. ഓരോ റിക്വസ്റ്റിനും ഞങ്ങൾ ഒരു ടൈംസ്റ്റാമ്പ് (timestamp) സംഭരിക്കുന്നു.
- പഴയ എൻട്രികൾ ഒഴിവാക്കാൻ ഞങ്ങൾ
ZREMRANGEBYSCOREഉപയോഗിക്കുന്നു. - നിലവിലെ റിക്വസ്റ്റുകൾ എണ്ണാൻ ഞങ്ങൾ
ZCARDഉപയോഗിക്കുന്നു. - പുതിയവ രേഖപ്പെടുത്താൻ ഞങ്ങൾ
ZADDഉപയോഗിക്കുന്നു.
റേസ് കണ്ടീഷനുകൾ (race conditions) ഒഴിവാക്കാൻ, ഞങ്ങൾ ഈ ലോജിക് ഒരു സിംഗിൾ Lua സ്ക്രിപ്റ്റിൽ പ്രവർത്തിപ്പിക്കുന്നു. ഇത് ഓപ്പറേഷൻ അറ്റോമിക് (atomic) ആക്കുന്നു. ഒരു സ്ക്രിപ്റ്റ് Redis-ലേക്ക് നടത്തുന്ന ഒരു യാത്രയിൽ തന്നെ എല്ലാം കൈകാര്യം ചെയ്യുന്നു. ഒരേ സമയം രണ്ട് റിക്വസ്റ്റുകളും പരിധിക്കുള്ളിലാണെന്ന് കരുതുന്നത് ഇത് തടയുന്നു.
2. Sliding Window Counter (സ്കെയിലബിലിറ്റിക്കായി)
അനോണിമസ് (anonymous) ട്രാഫിക്കിനായി ഞങ്ങൾക്ക് ദശലക്ഷക്കണക്കിന് കീകൾ ഉണ്ട്. ഓരോ ടൈംസ്റ്റാമ്പും സംഭരിക്കാൻ ഞങ്ങൾക്ക് കഴിയില്ല. പകരം, നിലവിലെ വിൻഡോയ്ക്കും മുൻപത്തെ വിൻഡോയ്ക്കും പ്രാധാന്യം നൽകുന്ന ഒരു കൗണ്ടർ ഞങ്ങൾ ഉപയോഗിക്കുന്നു. ഇത് വളരെ കുറഞ്ഞ മെമ്മറി മാത്രമേ ഉപയോഗിക്കുന്നുള്ളൂ കൂടാതെ അതീവ കാര്യക്ഷമവുമാണ്.
പ്രൊഡക്ഷനിൽ നിന്നുള്ള പ്രധാന പാഠങ്ങൾ:
- Lua സ്ക്രിപ്റ്റുകൾ ഉപയോഗിക്കുക. നിങ്ങളുടെ ചെക്കിംഗും റൈറ്റിംഗും ഒരൊറ്റ ആക്ഷനായി നടക്കുന്നുണ്ടെന്ന് ഇത് ഉറപ്പാക്കുന്നു.
- സ്റ്റാൻഡേർഡ് ഹെഡറുകൾ നൽകുക. എപ്പോഴും
X-RateLimit-Remaining,Retry-Afterഎന്നിവ അയക്കുക. ഇത് കൃത്യമായി പ്രവർത്തിക്കുന്ന ക്ലയന്റുകൾക്ക് സ്വയം നിയന്ത്രിക്കാൻ സഹായിക്കുന്നു. - Fail open രീതി സ്വീകരിക്കുക. Redis പ്രവർത്തനരഹിതമായാൽ, റിക്വസ്റ്റ് അനുവദിക്കുക. പൂർണ്ണമായും നിലച്ച ഒരു API-യേക്കാൾ നല്ലത് അമിതഭാരം നേരിടുന്ന ഒരു API ആണ്.
- ക്ലയന്റുകളെ ശരിയായി തിരിച്ചറിയുക. ഉപയോക്താക്കൾക്കായി API കീകൾ ഉപയോഗിക്കുക, ഗസ്റ്റുകൾക്കായി ഹാഷ് ചെയ്ത (hashed) IP-കൾ ഉപയോഗിക്കുക.
- എൻഡ്പോയിന്റുകൾക്ക് അനുസൃതമായ പരിധികൾ നിശ്ചയിക്കുക. ഒരു ലളിതമായ ലിസ്റ്റ് എൻഡ്പോയിന്റിനേക്കാൾ കഠിനമായ സെർച്ച് എൻഡ്പോയിന്റിന് കൂടുതൽ കർശനമായ പരിധികൾ ആവശ്യമാണ്.
ഫലം ഉടനടി ലഭിച്ചു. വൈകുന്നേരങ്ങളിലെ ട്രാഫിക് സ്പൈക്കുകൾ (spikes) ഇല്ലാതായി. ഞങ്ങളുടെ ഡാറ്റാബേസ് പെർഫോമൻസ് സാധാരണ നിലയിലായി.
