പുലർച്ചെ മൂന്ന് മണിക്ക് നിങ്ങളുടെ ഡാറ്റാബേസ് CPU 100% ഉപയോഗത്തിലേക്ക് ഉയരുന്നു. ട്രാഫിക് സാധാരണ നിലയിലാണെന്ന് തോന്നുന്നു. റിക്വസ്റ്റുകളുടെ എണ്ണത്തിൽ അസാധാരണമായ വർദ്ധനവുമില്ല. എന്നിട്ടും നിങ്ങളുടെ പ്രൈമറി സ്റ്റോർ തകരാറിലാകുന്നു, കാരണം ആ റിക്വസ്റ്റുകളിലൊന്നിനും മറ്റൊന്നിനും ഒരേ സമയം ഒരേ കാര്യം തന്നെ ആവശ്യപ്പെടുന്നു.

ഇതാണ് 'തണ്ടറിംഗ് ഹെർഡ്' (thundering herd) പ്രശ്നം.

ഒരു സംഗീത പരിപാടിക്ക് ശേഷമുള്ള സ്റ്റേഡിയം സങ്കൽപ്പിക്കുക. ഒരു മണിക്കൂർ സമയത്തിനുള്ളിൽ, ഒരേ എക്സിറ്റ് ഡോർ ഉപയോഗിച്ച് എല്ലാവർക്കും സുഗമമായി പുറത്തുപോകാം. എന്നാൽ ആൾക്കൂട്ടം മുഴുവൻ പത്തു സെക്കൻഡ് സമയത്തിനുള്ളിൽ ആ ഒറ്റ വാതിലിലൂടെ പുറത്തുപോകാൻ തീരുമാനിച്ചാൽ, ആ വാതിൽ തകരില്ല. പക്ഷേ, ആ ഒരേസമയം ഉണ്ടാകുന്ന തിരക്കിനെ (concurrency) കൈകാര്യം ചെയ്യാൻ അതിന് കഴിയില്ല. അവിടെ പ്രശ്നം വാതിലല്ല, മറിച്ച് ഉണ്ടാകുന്ന തിരക്കാണ്.

എവിടെയാണ് ഈ പ്രശ്നം രൂപപ്പെടുന്നത്?

ഒരേസമയം നിരവധി പ്രോസസ്സുകൾ ഒരു പ്രത്യേക സംഭവത്തിൽ (event) ഒത്തുചേരുമ്പോൾ ഈ രീതി കാണപ്പെടുന്നു. ഇതിന് വിനാശകരമായ ട്രാഫിക് ആവശ്യമില്ല. സാധാരണ സിസ്റ്റങ്ങൾ തന്നെ തനിയെ ഇത് സംഭവിക്കുന്നു.

Cache expiration. ഒരു പ്രധാനപ്പെട്ട (hot) cache key കാലാവധി കഴിയുന്നു. അത് ഒരു പ്രൊഡക്റ്റ് കാറ്റലോഗോ, ഫീച്ചർ ഫ്ലാഗോ, അല്ലെങ്കിൽ ഒരു ഉപയോക്താവിന്റെ പെർമിഷൻ ലിസ്റ്റോ ആകാം. ആയിരക്കണക്കിന് ആപ്ലിക്കേഷൻ സെർവറുകൾ ഒരേസമയം ആ ശൂന്യത ശ്രദ്ധിക്കുന്നു. ഓരോ സെർവറും ശരിയായ രീതിയിൽ പ്രവർത്തിക്കാൻ ശ്രമിക്കുന്നതിന്റെ ഭാഗമായി, സ്വന്തമായി ഒരു ഡാറ്റാബേസ് കണക്ഷൻ തുറക്കുകയും ആ മൂല്യം വീണ്ടെടുക്കാൻ ഒരേ കനത്ത ക്വറി (heavy query) തന്നെ പ്രവർത്തിപ്പിക്കുകയും ചെയ്യുന്നു. ഒരേ സമയം രണ്ടായിരം തവണ ഒരേ കനത്ത ചോദ്യത്തിന് ഉത്തരം നൽകാൻ ഡാറ്റാബേസ് ഒരിക്കലും ക്രമീകരിച്ചിട്ടില്ല. ഒരു expired key കാരണം മുഴുവൻ ക്ലസ്റ്ററും തകരാറിലാകുന്നു.

Connection pool wakeups. ചില ആർക്കിടെക്ചറുകളിൽ, നിരവധി വർക്കർ പ്രോസസ്സുകൾ ജോലി വരുന്നത് കാത്ത് ഒരു നിശ്ചിത അവസ്ഥയിൽ (condition) തടഞ്ഞുനിൽക്കുന്നു. ആ അവസ്ഥ സംഭവിക്കുമ്പോൾ, ഓപ്പറേറ്റിംഗ് സിസ്റ്റം ഉറങ്ങിക്കിടക്കുന്ന എല്ലാ വർക്കർമാരെയും ഉണർത്തുന്നു. എന്നാൽ ഒരു വർക്കർ മാത്രമേ യഥാർത്ഥത്തിൽ കണക്ഷനോ ജോലിയോ ഏറ്റെടുക്കുന്നുള്ളൂ. ബാക്കിയുള്ളവർ ഉണരുകയും തങ്ങൾക്ക് ആ ജോലി ലഭിച്ചില്ലെന്ന് മനസ്സിലാക്കി വീണ്ടും ഉറങ്ങാൻ പോകുകയും ചെയ്യുന്നു. ഈ ചക്രം context switches വഴി CPU ഉപയോഗം വർദ്ധിപ്പിക്കുകയും യഥാർത്ഥ ജോലികൾ തടസ്സപ്പെടുത്തുകയും ചെയ്യുന്നു.

Retry storms. ഒരു ഡൗൺസ്ട്രീം സർവീസ് തകരാറിലാകുന്നു. ഓരോ ക്ലയന്റും ടൈമൗട്ട് സംഭവിക്കുന്നത് ശ്രദ്ധിക്കുകയും വീണ്ടും ശ്രമിക്കുന്നതിന് മുമ്പ് കൃത്യം ഒരു സെക്കൻഡ് പോലുള്ള ഒരു നിശ്ചിത ഇടവേള കാത്തിരിക്കുകയും ചെയ്യുന്നു. സർവീസ് വീണ്ടെടുക്കുകയും ട്രാഫിക് സ്വീകരിക്കാൻ തുടങ്ങുകയും ചെയ്യുമ്പോൾ, എല്ലാ ക്ലയന്റുകളും ഒരേ നിമിഷം തന്നെ അതിനെ സമീപിക്കുന്നു. സർവീസ് വീണ്ടെടുക്കൽ പൂർത്തിയാകാത്തതിനാൽ അത് വീണ്ടും തകരാറിലാകുന്നു. ഈ ചക്രം ആവർത്തിച്ചുകൊണ്ടേയിരിക്കുന്നു.

Cron collisions. ഇൻസ്റ്റൻസുകളുടെ ഒരു കൂട്ടത്തിൽ മെയിന്റനൻസ്, റിപ്പോർട്ട് ജനറേഷൻ അല്ലെങ്കിൽ cache warming എന്നിവ കൃത്യം 00:00 UTC-യിൽ നടക്കാൻ നിങ്ങൾ ഷെഡ്യൂൾ ചെയ്യുകയാണെങ്കിൽ, കൃത്യമായ സമയക്രമത്തിൽ ഒരു ട്രാഫിക് സ്പൈക്ക് (traffic spike) നിങ്ങൾ തന്നെ സൃഷ്ടിക്കുന്നു. ഈ ലോഡ് മുൻകൂട്ടി അറിയാൻ കഴിയുന്നതാണെങ്കിലും, അതിന്റെ തീവ്രത വളരെ വലുതാണ്.

Request Coalescing

Cache stampedes പരിഹരിക്കാനുള്ള ഏറ്റവും ഫലപ്രദമായ മാർഗ്ഗം ഒരേ ചോദ്യത്തിന് ഒന്നിലധികം തവണ ഉത്തരം നൽകുന്നത് ഒഴിവാക്കുക എന്നതാണ്. ഒരു cache miss സംഭവിക്കുമ്പോൾ, 2,500 ത്രെഡുകളും (threads) ഓരോന്നായി ഡാറ്റാബേസ് ക്വറി പ്രവർത്തിപ്പിക്കുന്നത് നിങ്ങൾ ആഗ്രഹിക്കില്ല. പകരം, ഒരു ത്രെഡ് ക്വറി പ്രവർത്തിപ്പിക്കുകയും ബാക്കിയുള്ള 2,499 ത്രെഡുകളും ആ ഫലത്തിനായി കാത്തിരിക്കുകയും വേണം.

ഈ രീതിയെ request coalescing എന്ന് വിളിക്കുന്നു. Go-യിൽ, golang.org/x/sync-ലെ singleflight പാക്കേജ് ഇതിനായി ഒരു മികച്ച സംവിധാനം നൽകുന്നു. ഒരു പ്രത്യേക കീക്കായി Do വിളിക്കുന്ന ആദ്യത്തെ goroutine ജോലി ആരംഭിക്കുന്നു. അതേ കീ ഉപയോഗിച്ച് പിന്നീട് വരുന്നവ ആ Do കോളിനായി കാത്തിരിക്കുന്നു. ജോലി പൂർത്തിയാകുമ്പോൾ, കാത്തിരുന്ന എല്ലാവർക്കും ഒരേസമയം ഫലം ലഭിക്കുന്നു. നിങ്ങൾ ഒരു ക്വറി മാത്രം പ്രവർത്തിപ്പിച്ചുകൊണ്ട് 2,500 റിക്വസ്റ്റുകൾക്ക് മറുപടി നൽകുന്നു.

Promises, futures അല്ലെങ്കിൽ channels എന്നിവ ഉപയോഗിച്ചുള്ള ഒരു in-memory concurrent map വഴി നിങ്ങൾക്ക് സമാനമായ രീതി നിർമ്മിക്കാം. നിലവിൽ നടന്നുകൊണ്ടിരിക്കുന്ന ഒരു റിക്വസ്റ്റ് (in-flight request) ആണോ എന്ന് പരിശോധിക്കുന്നതും അത് രേഖപ്പെടുത്തുന്നതും (register) അറ്റോമിക് (atomically) ആയിരിക്കണം എന്നതാണ് ഇതിലെ പ്രധാന കാര്യം. റിക്വസ്റ്റ് പരാജയപ്പെട്ടാൽ എല്ലാവർക്കും ആ എറർ ലഭിക്കും, ഇത് സാധാരണയായി ശരിയായ രീതിയാണ്. റിക്വസ്റ്റ് വിജയിച്ചാൽ എല്ലാവർക്കും കാഷെ ചെയ്ത മൂല്യം ലഭിക്കും, അതിനുശേഷമുള്ള റിക്വസ്റ്റുകൾ നേരിട്ട് കാഷെ ഉപയോഗിക്കും. ഡാറ്റാബേസ് ലോഡ് ഒരു വലിയ സ്പൈക്കിൽ നിന്ന് നേർരേഖയായി മാറുന്നു.

Probabilistic Early Expiration

ചിലപ്പോൾ ഇത്തരം തിരക്കുകളെ നിയന്ത്രിക്കുന്നതിനേക്കാൾ അവ പൂർണ്ണമായും ഒഴിവാക്കാൻ നിങ്ങൾ ആഗ്രഹിച്ചേക്കാം. അത്തരം സാഹചര്യത്തിലാണ് XFetch പോലുള്ള അൽഗോരിതങ്ങൾ ഉപയോഗിച്ചുള്ള probabilistic early expiration പ്രസക്തമാകുന്നത്.

ഒരു നിശ്ചിത expiration സമയത്തിന് പകരം, ഓരോ കാഷെ ചെയ്ത മൂല്യത്തിനും ഒരു 'soft expiration window' ഉണ്ടായിരിക്കും. ഒരു റിക്വസ്റ്റ് വരുമ്പോൾ, ആപ്ലിക്കേഷൻ ഒരു റാൻഡം നമ്പർ നിർമ്മിക്കുകയും ഒരു ലളിതമായ പ്രോബബിലിറ്റി ചെക്ക് നടത്തുകയും ചെയ്യുന്നു. ഡൈസ് ഉരുട്ടുന്നതുപോലെ ആ പരിശോധനയിൽ 'അതെ' എന്ന് വന്നാൽ, ആ ഒറ്റ റിക്വസ്റ്റ് കാഷെ നേരത്തെ തന്നെ പുതുക്കുന്നു (refresh). ഇല്ലെങ്കിൽ, ആ റിക്വസ്റ്റിന് അല്പം പഴയ മൂല്യം തന്നെ നൽകുന്നു.

ഓരോ റിക്വസ്റ്റും സ്വന്തമായി ഡൈസ് ഉരുട്ടുന്നതിനാൽ, കാഷെ പുതുക്കുന്ന പ്രക്രിയ ആ സോഫ്റ്റ് വിൻഡോയിലുടനീളം വ്യാപിച്ചു കിടക്കുന്നു. ഒരു ക്ലയന്റ് കാലാവധി തീരുന്നതിന് നാൽപ്പത് സെക്കൻഡ് മുമ്പ് കാഷെ പുതുക്കിയേക്കാം, മറ്റൊരാൾ പന്ത്രണ്ട് സെക്കൻഡ് മുമ്പ്, ഭൂരിഭാഗം പേരും പുതുക്കണമെന്നില്ല. ജോലി എന്നത് ഒരേസമയം ഉണ്ടാകുന്ന ഒരു വലിയ സ്പൈക്കിന് പകരം ഒരു മൃദുവായ...