ആദ്യകാല വെബ് പ്ലെയിൻ ടെക്സ്റ്റ് (plain text) അടിസ്ഥാനമാക്കിയുള്ളതായിരുന്നു. നിങ്ങൾ ഒരു ഫോമിൽ യൂസർനെയിമോ കമന്റോ ടൈപ്പ് ചെയ്ത് സബ്മിറ്റ് ചെയ്യുമ്പോൾ, ഒരു ചെറിയ സ്ട്രിംഗ് (string) HTTP വഴി സെർവറിലേക്ക് പോകും. ആ ലളിതമായ റിക്വസ്റ്റ്-റെസ്പോൺസ് സൈക്കിൾ (request-response cycle) ആയിരുന്നു ഇൻഫ്രാസ്ട്രക്ചറിന്റെ അടിസ്ഥാനം. ആളുകൾ ഫോട്ടോകളും ഡോക്യുമെന്റുകളും വീഡിയോകളും പങ്കിടാൻ ആഗ്രഹിച്ചു തുടങ്ങിയപ്പോൾ, വായിക്കാവുന്ന ടെക്സ്റ്റിനായി മാത്രം നിർമ്മിച്ച ഒരു സിസ്റ്റത്തിലൂടെ എങ്ങനെ ബൈനറി ഡാറ്റ (binary data) കൈമാറാം എന്ന് എഞ്ചിനീയർമാർക്ക് കണ്ടെത്തേണ്ടി വന്നു.

ഒരു JPEG ഫയൽ ഒരു JSON ഒബ്‌ജക്റ്റിലേക്ക് ഉൾപ്പെടുത്താൻ ശ്രമിച്ചാൽ, നിങ്ങൾ ഒരു വലിയ തടസ്സത്തിൽ ഉടക്കും. JSON ഒരു ടെക്സ്റ്റ് പ്രോട്ടോക്കോൾ ആണ്. ഇതിന് സാധുവായ യൂണിക്കോഡ് ക്യാരക്ടറുകൾ, കോട്ടുകൾ (quotes), ബ്രേസുകൾ (braces), ശരിയായി എസ്കേപ്പ് ചെയ്ത സ്ട്രിംഗുകൾ എന്നിവ ആവശ്യമാണ്. ഒരു ബൈനറി ഫയൽ എന്നത് പ്രിന്റബിൾ അല്ലാത്ത നിരവധി ബൈറ്റുകളുടെ ഒരു നീണ്ട ശ്രേണി മാത്രമാണ്. ഈ ബൈറ്റുകൾ ഒരു JSON സ്ട്രിംഗിലേക്ക് തിരുകിക്കയറ്റിയാൽ പാഴ്സർ (parser) തകരാറിലാകുകയും, എസ്കേപ്പ് സീക്വൻസുകൾ ഡാറ്റയെ നശിപ്പിക്കുകയും ചെയ്യുന്നു, അങ്ങനെ സന്ദേശം മറ്റേ വശത്ത് വായിക്കാൻ കഴിയാത്തതാകുന്നു.

Base64 എൻകോഡിംഗ് ഇതിനുള്ള വ്യക്തമായ ഒരു പരിഹാരമായി ഉയർന്നു വന്നു. ഇത് ബൈനറി ഡാറ്റയെ അറുപത്തിനാല് പ്രിന്റബിൾ ASCII ക്യാരക്ടറുകളിലേക്ക് മാറ്റി ക്രമീകരിക്കുന്നു. ഓരോ മൂന്ന് ബൈനറി ബൈറ്റുകളും നാല് ടെക്സ്റ്റ് ക്യാരക്ടറുകളായി മാറുന്നു. ഇപ്പോൾ പേലോഡ് (payload) നിയമപരമായ ഒരു JSON ആയി മാറുന്നു, അതായത് ഏതൊരു സ്റ്റാൻഡേർഡ് API വഴിയും ഇത് സുരക്ഷിതമായി കടന്നുപോകും. എന്നാൽ ഇതിന് ഒരു വില നൽകേണ്ടതുണ്ട്. ഈ റീ-എൻകോഡിംഗ് ഫയൽ സൈസ് ഏകദേശം മുപ്പത്തിമൂന്ന് ശതമാനം വർദ്ധിപ്പിക്കുന്നു. മൂന്ന് മെഗാബൈറ്റ് ഉള്ള ഒരു ചിത്രം കൈമാറ്റം ചെയ്യുമ്പോൾ നാല് മെഗാബൈറ്റായി മാറുന്നു. ഡാറ്റ തിരിച്ചാറ്റുന്നതിനായി ക്ലയന്റും സെർവറും അധികമായി CPU സൈക്കിളുകൾ ഉപയോഗിക്കുന്നു. ഇതിനേക്കാൾ പ്രധാനമായി, പല JSON സെർവർ ഫ്രെയിംവർക്കുകളും പാഴ്സ് ചെയ്യുന്നതിന് മുമ്പ് മുഴുവൻ ബോഡിയും മെമ്മറിയിലേക്ക് വായിക്കുന്നു. ഒരേസമയം വലിയ ഫയലുകൾ അപ്‌ലോഡ് ചെയ്യപ്പെട്ടാൽ ഒരു സാധാരണ സെർവർ പോലും തകരാറിലാകാം, കാരണം ഓരോ ഫയലും ഡിസ്കിലേക്ക് സേവ് ചെയ്യുന്നതിന് മുമ്പ് തന്നെ മെമ്മറിയിൽ വലിയൊരു ടെക്സ്റ്റ് സ്ട്രിംഗായി നിലനിൽക്കുന്നു. അത്യാവശ്യ ഘട്ടങ്ങളിൽ Base64 ഉപയോഗിക്കാം, പക്ഷേ വലിയ തോതിലുള്ള പ്രൊഡക്ഷൻ ആവശ്യങ്ങൾക്കായി ഇത് രൂപകൽപ്പന ചെയ്തിട്ടില്ല.

ഇതിനുള്ള മികച്ച പരിഹാരം multipart/form-data ആണ്. ഈ ഫോർമാറ്റ് ഒരു HTTP റിക്വസ്റ്റിനെ വ്യത്യസ്ത ഭാഗങ്ങളുടെ ഒരു ശേഖരമായി കാണുന്നു, ഓരോ ഭാഗവും ഒരു പ്രത്യേക ബൗണ്ടറി സ്ട്രിംഗ് (boundary string) ഉപയോഗിച്ച് വേർതിരിച്ചിരിക്കുന്നു. ഒരു ഭാഗം പ്ലെയിൻ ടെക്സ്റ്റ് ഫീൽഡ് ആകാം. അടുത്ത ഭാഗം അതിന്റെ സ്വന്തം Content-Type, Content-Disposition ഹെഡറുകൾ ഉള്ള ഒരു ബൈനറി ഇമേജ് ആകാം. സെർവർ വരുന്ന സ്ട്രീമിനെ ക്രമമായി വായിക്കുകയും, ബൗണ്ടറി മാർക്കറുകൾ ശ്രദ്ധിക്കുകയും ചെയ്യുന്നു, അങ്ങനെ ഓരോ ഭാഗവും അനുയോജ്യമായ ഹാൻഡ്‌ലറിലേക്ക് കൈമാറുന്നു. ഇതിനായി മുഴുവൻ പേലോഡിനെയും ഒരു ടെക്സ്റ്റ് ബ്ലോക്കായി കാണേണ്ട ആവശ്യമില്ല.

Node.js-ൽ ഈ വ്യത്യാസം വളരെ പ്രധാനമാണ്. express.json() മിഡിൽവെയറിന് JSON ബോഡികൾ പാഴ്സ് ചെയ്യാൻ അറിയാം, പക്ഷേ ഫയൽ സ്ട്രീമുകൾ കൈകാര്യം ചെയ്യാൻ കഴിയില്ല. മൾട്ടിപാർട്ട് അപ്‌ലോഡുകൾ പ്രോസസ്സ് ചെയ്യാൻ Multer അല്ലെങ്കിൽ Busboy പോലുള്ള ഒരു സ്ട്രീമിംഗ് പാഴ്സർ ആവശ്യമാണ്. ഈ ടൂളുകൾ റോ (raw) റിക്വസ്റ്റ് സ്ട്രീമുമായി ബന്ധപ്പെട്ട് ഡാറ്റയെ കഷണങ്ങളായി (chunk by chunk) വായിക്കുന്നു. ഉദാഹരണത്തിന്, Multer ഉപയോഗിച്ച് വരുന്ന ഫയലുകൾ ഡിസ്കിലെ ഒരു താൽക്കാലിക ഫോൾഡറിലേക്ക് എഴുതണോ അതോ ചെറിയ ഫയലുകൾ മെമ്മറിയിൽ സൂക്ഷിക്കണോ എന്ന് നിങ്ങൾക്ക് തീരുമാനിക്കാം. ഈ തീരുമാനം വളരെ പ്രധാനമാണ്. എല്ലാം മെമ്മറിയിൽ തന്നെ സൂക്ഷിക്കുകയാണെങ്കിൽ, പെട്ടെന്ന് വലിയ ഫയലുകൾ അപ്‌ലോഡ് ചെയ്യപ്പെടുമ്പോൾ നിങ്ങളുടെ ആപ്പിന്റെ ഹീപ്പ് സ്പേസ് (heap space) തീരുകയും പ്രോസസ്സ് ക്രാഷ് ആകുകയും ചെയ്തേക്കാം. ഡിസ്കിലേക്ക് എഴുതുന്നത് സ്റ്റെബിലിറ്റിക്കായി I/O ഉപയോഗിക്കുന്നു, എന്നാൽ ഇത് ക്ലീനപ്പും പാത്ത് സെക്യൂരിറ്റിയും സംബന്ധിച്ച ചോദ്യങ്ങൾ ഉയർത്തുന്നു.

ചെറിയ ആപ്ലിക്കേഷനുകൾക്ക് ./uploads പോലുള്ള ഒരു ലോക്കൽ ഫോൾഡറിൽ ഫയലുകൾ സേവ് ചെയ്യുന്നത് എളുപ്പവും വേഗതയുമുള്ള കാര്യമാണ്. നിങ്ങളുടെ കോഡ് പ്രവർത്തിക്കുന്ന അതേ മെഷീനിൽ തന്നെ ഫയൽ സേവ് ചെയ്യപ്പെടുന്നു, അത് തിരികെ നൽകുന്നത് ശരിയായ പാത്തിലേക്ക് പോയിന്റ് ചെയ്താൽ മാത്രം മതി. എന്നാൽ ഇത് എപ്പോഴും ഫലപ്രദമാകണമെന്നില്ല.

ഒരു ലോഡ് ബാലൻസർ (load balancer) ഉപയോഗിക്കാൻ തുടങ്ങുന്ന നിമിഷം, ലോക്കൽ സ്റ്റോറേജ് ഒരു പ്രശ്നമായി മാറുന്നു. ഒരു ഉപയോക്താവ് പ്രൊഫൈൽ ചിത്രം അപ്‌ലോഡ് ചെയ്യുന്നു. ലോഡ് ബാലൻസർ ആ റിക്വസ്റ്റ് സെർവർ A-യിലേക്ക് അയക്കുന്നു, ഫയൽ സെർവർ A-യുടെ ഡിസ്കിൽ സേവ് ചെയ്യപ്പെടുന്നു. പിന്നീട് ആ ഉപയോക്താവ് ചിത്രം കാണാൻ ശ്രമിക്കുമ്പോൾ, ലോഡ് ബാലൻസർ ആ റിക്വസ്റ്റ് സെർവർ B-യിലേക്ക് അയക്കുന്നു. സെർവർ B അതിന്റെ ഫയൽസിസ്റ്റം പരിശോധിക്കുമ്പോൾ അവിടെ ഒന്നും കാണുന്നില്ല. ഫയൽ നഷ്ടപ്പെട്ടതായി തോന്നും. ഉപയോക്താവിനെ ഒരേ മെഷീനിൽ തന്നെ നിലനിർത്താൻ 'സ്റ്റിക്കി സെഷനുകൾ' (sticky sessions) ഉപയോഗിക്കാം, പക്ഷേ അത് ഒരു താൽക്കാലിക പരിഹാരം മാത്രമാണ്. സെർവർ A റീസ്റ്റാർട്ട് ചെയ്യുകയോ അല്ലെങ്കിൽ പുതിയൊരു ഇൻസ്റ്റൻസ് വന്നാൽ പഴയത് മാറ്റിസ്ഥാപിക്കപ്പെടുകയോ ചെയ്താൽ ഡാറ്റ നഷ്ടപ്പെടും. കണ്ടെയ്‌നറൈസ്ഡ് എൻവയോൺമെന്റുകളിൽ (containerized environments), ലോക്കൽ ഡിസ്കുകൾ വളരെ താൽക്കാലികമാണ്. ഒരു ഡോക്കർ കണ്ടെയ്നറിന്റെ ഫയൽസിസ്റ്റം എപ്പോൾ വേണമെങ്കിലും ഒഴിവാക്കാവുന്നതാണ്. അതിനെ സ്ഥിരമായ സ്റ്റോറേജായി കാണുന്നത് ഡാറ്റ നഷ്ടപ്പെടാൻ കാരണമാകും.

ഇതിനുള്ള സ്റ്റാൻഡേർഡ് പരിഹാരം കമ്പ്യൂട്ട് (compute) സ്റ്റോറേജിൽ (storage) നിന്ന് വേർതിരിക്കുക എന്നതാണ്. നിങ്ങളുടെ ആപ്ലിക്കേഷൻ സെർവറുകൾ സ്റ്റേറ്റ്‌ലെസ്സ് (stateless) ആയി നിലനിർത്തുകയും അപ്‌ലോഡ് ചെയ്യുന്ന ഫയലുകൾ AWS S3 അല്ലെങ്കിൽ Google Cloud Storage പോലുള്ള ഡെഡിക്കേറ്റഡ് ഒബ്‌ജക്റ്റ് സ്റ്റോറേജുകളിലേക്ക് അയക്കുകയും ചെയ്യുക. ഈ സേവനങ്ങൾ ഡ്യൂറബിലിറ്റി (durability), ജിയോഗ്രാഫിക് ഡിസ്ട്രിബ്യൂഷൻ, വലിയ തോതിലുള്ള കൺകറൻസി എന്നിവയ്ക്കായി രൂപകൽപ്പന ചെയ്തവയാണ്. ആപ്ലിക്കേഷൻ സെർവർ റിക്വസ്റ്റ് കൈകാര്യം ചെയ്യുകയും മെറ്റാഡാറ്റ പരിശോധിക്കുകയും ചെയ്ത ശേഷം, ഡാറ്റ സൂക്ഷിക്കാനായി പ്രത്യേകം രൂപകൽപ്പന ചെയ്ത ഇൻഫ്രാസ്ട്രക്ചറിലേക്ക് ബൈറ്റുകൾ കൈമാറുകയും ചെയ്യുന്നു.

എങ്കിലും, നിങ്ങൾ ഇത് ശ്രദ്ധയില്ലാതെ നടപ്പിലാക്കിയാൽ ഈ രീതി പോലും ഒരു തടസ്സം (bottleneck) സൃഷ്ടിക്കും. പല ടീമുകളും ബ്രൗസർ ഫയൽ backend-ലേക്ക് അപ്‌ലോഡ് ചെയ്യാനും, തുടർന്ന് backend ഓരോ ബൈറ്റും object storage-ലേക്ക് ഫോർവേഡ് ചെയ്യാനുമാണ് ആദ്യം ശ്രമിക്കുന്നത്. ഒരു ഉപയോക്താവ് അഞ്ഞൂറ് മെഗാബൈറ്റ് വീഡിയോ അപ്‌ലോഡ് ചെയ്യുകയാണെങ്കിൽ, നിങ്ങളുടെ സെർവർ ഒരു ഇടനിലക്കാരനായി മാറുന്നു. ഫയൽ ഉള്ളിലേക്ക് വലിച്ചെടുക്കാൻ ഇത് ബാൻഡ്‌വിഡ്ത്ത് ഉപയോഗിക്കുന്നു, തുടർന്ന് അത് S3-ലേക്ക് എത്തിക്കാൻ കൂടുതൽ ബാൻഡ്‌വിഡ്ത്ത് ഉപയോഗിക്കുന്നു. ട്രാൻസ്ഫർ പൂർത്തിയാകുന്നത് വരെ കണക്ഷൻ തുറന്നുതന്നെ ഇരിക്കും. മോശം നെറ്റ്‌വർക്ക് സാഹചര്യങ്ങളുള്ള ഉപയോക്താക്കളിൽ നിന്നുള്ള സാവധാനത്തിലുള്ള അപ്‌ലോഡുകൾ മിനിറ്റുകളോളം സെർവർ കണക്ഷനുകൾ തടഞ്ഞുവെച്ചേക്കാം. സെർവർ സ്ട്രീം ബഫർ ചെയ്യുന്നുണ്ടെങ്കിൽ മെമ്മറി ഉപയോഗം ഉയർന്ന നിലയിൽ തന്നെ തുടരും, കൂടാതെ നിങ്ങൾ metered hosting ആണ് ഉപയോഗിക്കുന്നതെങ്കിൽ, ഒരേ ഡാറ്റാ ട്രാൻസ്ഫറിന് നിങ്ങൾ രണ്ടുതവണ പണം നൽകേണ്ടി വരും. Horizontal scaling ഇതിന് പരിഹാരമാവില്ല, കാരണം നിങ്ങൾ ചേർക്കുന്ന ഓരോ അധിക സെർവറും കാണേണ്ടതില്ലാത്ത ബൈറ്റുകൾ കൈമാറുന്ന തിരക്കിലായിരിക്കും.

ഡാറ്റാ പാത്തിൽ (data path) നിന്ന് backend-നെ പൂർണ്ണമായും ഒഴിവാക്കിയാണ് ആധുനിക സിസ്റ്റങ്ങൾ ഈ പ്രശ്നം പരിഹരിക്കുന്നത്. ഫയൽ സ്വീകരിക്കുന്നതിന് പകരം, അപ്‌ലോഡ് ചെയ്യാനുള്ള അനുമതിക്കായി backend ഒരു റിക്വസ്റ്റ് മാത്രം സ്വീകരിക്കുന്നു. ഇതിന്റെ പ്രവർത്തനരീതി ഇപ്രകാരമാണ്:

  • ബ്രൗസർ അപ്‌ലോഡ് ആരംഭിക്കാൻ backend-നോട് ആവശ്യപ്പെടുന്നു, സാധാരണയായി ഫയൽ നാമം (filename), ഫയൽ ടൈപ്പ്, ഉദ്ദേശിച്ച ലക്ഷ്യം എന്നിവ മാത്രമാണ് ഇതിനായി അയക്കുന്നത്.
  • backend ഉപയോക്താവിനെ ഓതന്റിക്കേറ്റ് ചെയ്യുന്നു, ബിസിനസ് റൂൾസ് അനുസരിച്ച് റിക്വസ്റ്റ് പരിശോധിക്കുന്നു, തുടർന്ന് ഒരു SDK ഉപയോഗിച്ച് object storage പ്രൊവൈഡറിൽ നിന്ന് താൽക്കാലികമായ ഒരു presigned URL നിർമ്മിക്കുന്നു.
  • backend ആ URL ബ്രൗസറിന് തിരികെ നൽകുന്നു. ഈ URL ഒരു പ്രത്യേക bucket-നും key-നും മാത്രമായി പരിമിതപ്പെടുത്തിയിരിക്കുന്നു, അഞ്ച് അല്ലെങ്കിൽ പതിനഞ്ച് മിനിറ്റ് പോലുള്ള കുറഞ്ഞ സമയത്തേക്ക് മാത്രമേ ഇത് സാധുതയുള്ളൂ, കൂടാതെ ആവശ്യമായ കൃത്യമായ അനുമതികൾ മാത്രം നൽകുന്ന ഒരു ടോക്കൺ ഉപയോഗിച്ച് ഇത് സൈൻ ചെയ്തിരിക്കുന്നു.
  • ബ്രൗസർ ഒരു സ്റ്റാൻഡേർഡ് PUT അല്ലെങ്കിൽ POST ഉപയോഗിച്ച് ഫയൽ നേരിട്ട് S3 അല്ലെങ്കിൽ GCS-ലേക്ക് അപ്‌ലോഡ് ചെയ്യുന്നു. ബൈറ്റുകൾ ഉപയോക്താവിന്റെ ഉപകരണത്തിൽ നിന്ന് നേരിട്ട്