ओपनरेस्टी में 10x प्रदर्शन सुधार के लिए टिप्स: `Table` डेटा स्ट्रक्चर

API7.ai

December 9, 2022

OpenResty (NGINX + Lua)

OpenResty में, string ऑपरेशन्स के साथ-साथ table ऑपरेशन्स भी प्रदर्शन समस्याओं का एक बड़ा कारण हैं। पिछले लेखों में, हमने table से संबंधित फ़ंक्शन्स को कभी-कभी कवर किया है, लेकिन विशेष रूप से प्रदर्शन सुधार के संदर्भ में नहीं। आज, मैं आपको table ऑपरेशन्स के प्रदर्शन प्रभाव के बारे में बताऊंगा।

डेवलपर्स table से संबंधित प्रदर्शन अनुकूलन के बारे में string ऑपरेशन्स की तुलना में कम जानते हैं, इसके दो मुख्य कारण हैं:

  1. OpenResty में उपयोग किया जाने वाला Lua, LuaJIT का एक स्व-प्रबंधित शाखा है, न कि मानक LuaJIT या मानक Lua। अधिकांश डेवलपर्स इस अंतर से अनजान होते हैं और OpenResty कोड लिखने के लिए मानक Lua table लाइब्रेरी का उपयोग करते हैं।
  2. चाहे मानक LuaJIT में हो या OpenResty द्वारा प्रबंधित LuaJIT शाखा में, table ऑपरेशन्स से संबंधित दस्तावेज़ीकरण गहराई में छिपा होता है और डेवलपर्स के लिए इसे ढूंढना मुश्किल होता है। और दस्तावेज़ीकरण में कोई नमूना कोड नहीं होता है, इसलिए डेवलपर्स को उदाहरणों के लिए ओपन-सोर्स प्रोजेक्ट्स में खोजना पड़ता है।

ये दो बिंदु OpenResty सीखने को एक अपेक्षाकृत उच्च संज्ञानात्मक बाधा बनाते हैं, जिसके परिणामस्वरूप ध्रुवीकृत परिणाम होते हैं - अनुभवी OpenResty डेवलपर्स बहुत ही उच्च प्रदर्शन वाला कोड लिख सकते हैं, जबकि नए शुरुआत करने वाले यह सोचेंगे कि क्या OpenResty का उच्च प्रदर्शन वास्तविक है। लेकिन इस पाठ को सीखने के बाद, आप आसानी से इस धारणा बाधा को पार कर सकते हैं और 10 गुना प्रदर्शन सुधार प्राप्त कर सकते हैं।

table अनुकूलन के बारे में विस्तार से जाने से पहले, मैं table से संबंधित अनुकूलन का एक सरल सिद्धांत पर जोर देना चाहूंगा।

टेबल्स को पुन: उपयोग करने का प्रयास करें और अनावश्यक टेबल निर्माण से बचें।

हम table निर्माण, तत्व सम्मिलन, खाली करने और लूप उपयोग के संदर्भ में अनुकूलन का परिचय देंगे।

पूर्व-निर्मित सरणी

पहला कदम एक सरणी बनाना है। Lua में, हम एक सरणी बनाने का तरीका सरल है।

local t = {}

उपरोक्त कोड की पंक्ति एक खाली सरणी बनाती है। आप निर्माण के समय आरंभिक डेटा भी जोड़ सकते हैं:

local color = {first = "red", "blue", third = "green", "yellow"}

हालांकि, दूसरा लेखन तरीका प्रदर्शन के मामले में अधिक महंगा है क्योंकि इसमें हर बार एक सरणी तत्व जोड़ने और हटाने पर स्थान आवंटन, resize और rehash शामिल होता है।

तो इसे कैसे अनुकूलित किया जाए? समय के लिए स्थान एक सामान्य अनुकूलन विचार है। चूंकि यहां प्रदर्शन बाधा सरणी स्थान का गतिशील आवंटन है, तो हम एक निर्दिष्ट आकार की सरणी को पूर्व-निर्मित कर सकते हैं। हालांकि यह कुछ मेमोरी स्थान बर्बाद कर सकता है, लेकिन कई स्थान आवंटन, resize, और rehash को एक में जोड़ा जा सकता है, जो कि अधिक कुशल है।

LuaJIT में table.new(narray, nhash) फ़ंक्शन जोड़ा गया है।

यह फ़ंक्शन निर्दिष्ट सरणी और हैश स्थान आकार को पूर्व-आवंटित करता है, न कि तत्व सम्मिलित करते समय स्वयं को बढ़ाता है। यह इसके दो पैरामीटर्स, narray और nhash का अर्थ है।

यहां एक सरल उदाहरण है कि इसे कैसे उपयोग किया जाए। चूंकि यह फ़ंक्शन LuaJIT एक्सटेंशन है, इसलिए हमें इसका उपयोग करने से पहले निम्नलिखित की आवश्यकता है।

local new_tab = require "table.new" local t = new_tab(100, 0) for i = 1, 100 do t[i] = i end

इसके अलावा, क्योंकि पिछले OpenResty ने LuaJIT को पूरी तरह से बाइंड नहीं किया था और अभी भी मानक Lua का समर्थन करता है, कुछ पुराने कोड संगतता के लिए ऐसा करेंगे। यदि फ़ंक्शन table.new नहीं मिलता है, तो एक खाली फ़ंक्शन का अनुकरण किया जाएगा ताकि कॉलर की एकरूपता सुनिश्चित की जा सके।

local ok, new_tab = pcall(require, "table.new") if not ok then new_tab = function (narr, nrec) return {} end end

मैन्युअल रूप से table सबस्क्रिप्ट की गणना

एक बार जब आपके पास एक table ऑब्जेक्ट होता है, तो अगला कदम इसमें तत्व जोड़ना होता है। एक तत्व सम्मिलित करने का सबसे सीधा तरीका table.insert फ़ंक्शन को कॉल करना है:

local new_tab = require "table.new" local t = new_tab(100, 0) for i = 1, 100 do table.insert(t, i) end

वैकल्पिक रूप से, पहले वर्तमान सरणी की लंबाई प्राप्त करें और इंडेक्स का उपयोग करके तत्व सम्मिलित करें:

local new_tab = require "table.new" local t = new_tab(100, 0) for i = 1, 100 do t[#t + 1] = i end

हालांकि, दोनों को पहले सरणी की लंबाई की गणना करने की आवश्यकता होती है और फिर नए तत्व जोड़ने होते हैं। इस ऑपरेशन का समय जटिलता O(n) है। उपरोक्त कोड उदाहरण में, for लूप सरणी की लंबाई की गणना 100 बार करेगा, इसलिए प्रदर्शन अच्छा नहीं है, और सरणी जितनी बड़ी होगी, प्रदर्शन उतना ही कम होगा।

आइए देखें कि आधिकारिक lua-resty-redis लाइब्रेरी इस समस्या को कैसे हल करती है।

local function _gen_req(args) local nargs = #args local req = new_tab(nargs * 5 + 1, 0) req[1] = "*" .. nargs .. "\r\n" local nbits = 2 for i = 1, nargs do local arg = args[i] req[nbits] = "$" req[nbits + 1] = #arg req[nbits + 2] = "\r\n" req[nbits + 3] = arg req[nbits + 4] = "\r\n" nbits = nbits + 5 end return req end

यह फ़ंक्शन सरणी req को पूर्व-निर्मित करता है, जिसका आकार फ़ंक्शन के इनपुट पैरामीटर्स द्वारा निर्धारित किया जाता है, ताकि यह सुनिश्चित किया जा सके कि जितना संभव हो उतना कम स्थान बर्बाद हो।

यह req के सबस्क्रिप्ट को मैन्युअल रूप से बनाए रखने के लिए nbits वेरिएबल का उपयोग करता है, न कि Lua के अंतर्निहित table.insert फ़ंक्शन और # ऑपरेटर का उपयोग करके लंबाई प्राप्त करने के लिए।

आप देख सकते हैं कि for लूप में, कुछ ऑपरेशन जैसे nbits + 1 सीधे सबस्क्रिप्ट के रूप में तत्व सम्मिलित कर रहे हैं और अंत में सबस्क्रिप्ट को सही मान पर रखने के लिए nbits = nbits + 5 का उपयोग कर रहे हैं।

इसका लाभ स्पष्ट है, यह सरणी के आकार को प्राप्त करने के O(n) ऑपरेशन को छोड़ देता है और इसके बजाय इंडेक्स के साथ सीधे एक्सेस करता है, और समय जटिलता O(1) हो जाती है। नुकसान भी स्पष्ट है, यह कोड की पठनीयता को कम करता है, और त्रुटि की संभावना बहुत बढ़ जाती है, इसलिए यह एक दोधारी तलवार है।

एकल table का पुन: उपयोग

चूंकि table बनाने की लागत इतनी अधिक है, हम स्वाभाविक रूप से इसे जितना संभव हो उतना पुन: उपयोग करना चाहते हैं। हालांकि, पुन: उपयोग के लिए शर्तें हैं। हमें पहले table में मौजूद मूल डेटा को साफ करने की आवश्यकता है ताकि अगले उपयोगकर्ता पर प्रभाव न पड़े।

यहीं पर table.clear फ़ंक्शन काम आता है। इसके नाम से आप समझ सकते हैं कि यह क्या करता है, यह सरणी में सभी डेटा को साफ कर देता है, लेकिन सरणी की लंबाई नहीं बदलती है। यानी यदि आप table.new(narray, nhash) का उपयोग करके 100 लंबाई की सरणी बनाते हैं, तो इसे साफ करने के बाद, लंबाई अभी भी 100 होगी।

इसके कार्यान्वयन को बेहतर ढंग से समझने के लिए, मैंने नीचे एक कोड उदाहरण दिया है जो मानक Lua के साथ संगत है:

local ok, clear_tab = pcall(require, "table.clear") if not ok then clear_tab = function (tab) for k, _ in pairs(tab) do tab[k] = nil end end end

जैसा कि आप देख सकते हैं, clear फ़ंक्शन प्रत्येक तत्व को nil पर सेट करता है।

आमतौर पर, हम इस प्रकार के चक्रीय table को एक मॉड्यूल के शीर्ष स्तर पर रखते हैं। इस तरह, जब आप मॉड्यूल में फ़ंक्शन्स का उपयोग करते हैं, तो आप अपनी स्थिति के आधार पर यह तय कर सकते हैं कि उन्हें सीधे उपयोग करना है या साफ करने के बाद।

आइए एक व्यावहारिक अनुप्रयोग उदाहरण देखें। निम्नलिखित स्यूडो-कोड ओपन-सोर्स माइक्रोसर्विसेस API गेटवे Apache APISIX से लिया गया है, और यह प्लगइन लोड करते समय इसका तर्क है।

local local_plugins = core.table.new(32, 0) local function load(plugin_names, wasm_plugin_names) local processed = {} for _, name in ipairs(plugin_names) do if processed[name] == nil then processed[name] = true end end for _, attrs in ipairs(wasm_plugin_names) do if processed[attrs.name] == nil then processed[attrs.name] = attrs end end core.log.warn("new plugins: ", core.json.delay_encode(processed)) for name, plugin in pairs(local_plugins_hash) do local ty = PLUGIN_TYPE_HTTP if plugin.type == "wasm" then ty = PLUGIN_TYPE_HTTP_WASM end unload_plugin(name, ty) end core.table.clear(local_plugins) core.table.clear(local_plugins_hash) for name, value in pairs(processed) do local ty = PLUGIN_TYPE_HTTP if type(value) == "table" then ty = PLUGIN_TYPE_HTTP_WASM name = value end load_plugin(name, local_plugins, ty) end -- sort by plugin's priority if #local_plugins > 1 then sort_tab(local_plugins, sort_plugin) end for i, plugin in ipairs(local_plugins) do local_plugins_hash[plugin.name] = plugin if enable_debug() then core.log.warn("loaded plugin and sort by priority:", " ", plugin.priority, " name: ", plugin.name) end end _M.load_times = _M.load_times + 1 core.log.info("load plugin times: ", _M.load_times) return true end

आप देख सकते हैं कि सरणी local_plugins plugin मॉड्यूल के लिए शीर्ष-स्तरीय वेरिएबल है। लोड फ़ंक्शन की शुरुआत में, table को साफ किया जाता है, और वर्तमान स्थिति के आधार पर एक नई प्लगइन सूची उत्पन्न की जाती है।

tablepool

अब तक, आपने एकल टेबल के चक्रीय उपयोग का अनुकूलन सीख लिया है। फिर आप एक कदम आगे बढ़ सकते हैं और कई टेबल्स को किसी भी समय उपलब्ध कराने के लिए एक कैश पूल का उपयोग कर सकते हैं, जो कि आधिकारिक lua-tablepool का कार्य है।

निम्नलिखित कोड एक टेबल पूल के बुनियादी उपयोग को दर्शाता है। हम एक निर्दिष्ट पूल से एक टेबल प्राप्त कर सकते हैं और इसे उपयोग करने के बाद वापस रिलीज़ कर सकते हैं:

local tablepool = require "tablepool" local tablepool_fetch = tablepool.fetch local tablepool_release = tablepool.release local pool_name = "some_tag" local function do_sth() local t = tablepool_fetch(pool_name, 10, 0) -- -- using t for some purposes tablepool_release(pool_name, t) end

tablepool हमारे द्वारा पहले परिचय किए गए कई तरीकों का उपयोग करता है, और इसमें सौ से कम लाइन्स का कोड है, इसलिए मैं इसे स्वयं खोजने और अध्ययन करने की अत्यधिक अनुशंसा करता हूं। यहां, मैं मुख्य रूप से इसके दो APIs का परिचय दूंगा।

पहला fetch मेथड है, जो table.new के समान तर्क लेता है, लेकिन एक अतिरिक्त pool_name के साथ। यदि पूल में कोई मुक्त सरणी नहीं है, तो fetch एक नई सरणी बनाने के लिए table.new को कॉल करेगा।

tablepool.fetch(pool_name, narr, nrec)

दूसरा release है, एक फ़ंक्शन जो टेबल को पूल में वापस डालता है। इसके तर्कों में, अंतिम एक, no_clear, यह कॉन्फ़िगर करने के लिए उपयोग किया जाता है कि क्या सरणी को साफ करने के लिए table.clear को कॉल करना है।

tablepool.release(pool_name, tb, [no_clear])

देखें कि हमारे द्वारा पहले परिचय किए गए सभी तरीके अब कैसे संबंधित हैं? हालांकि, इस कारण से tablepool का दुरुपयोग न करने का ध्यान रखें। वास्तविक प्रोजेक्ट्स में tablepool का उपयोग बहुत कम होता है। उदाहरण के लिए, इसे Kong द्वारा उपयोग नहीं किया जाता है, और APISIX में इससे संबंधित केवल कुछ कॉल हैं। अधिकांश मामलों में, इस परत tablepool के एनकैप्सुलेशन के बिना भी हमारे लिए पर्याप्त है।

सारांश

प्रदर्शन अनुकूलन, OpenResty में एक कठिन क्षेत्र, एक हॉट स्पॉट है। आज मैंने table से संबंधित प्रदर्शन अनुकूलन टिप्स का परिचय दिया। मुझे आशा है कि ये आपके वास्तविक प्रोजेक्ट में मदद कर सकते हैं।

अंत में, एक प्रश्न सोचें: क्या आप स्वयं एक प्रदर्शन परीक्षण कर सकते हैं और table से संबंधित अनुकूलन तकनीकों का उपयोग करने से पहले और बाद के प्रदर्शन अंतर की तुलना कर सकते हैं? फिर से, टिप्पणी करने और मेरे साथ संवाद करने के लिए आपका स्वागत है क्योंकि मैं आपके अभ्यास और विचारों को जानना चाहूंगा। इस लेख को साझा करने के लिए आपका स्वागत है ताकि अधिक लोग इसमें भाग ले सकें।