---
title: "سحر التواصل بين عمال NGINX: واحدة من أهم هياكل البيانات `shared dict` - API7.ai"
description: "shared dict هي بنية بيانات مهمة في NGINX وOpenResty."
url: "https://api7.ai/ar/learning-center/openresty/communication-magic-between-nginx-workers-shared-dict"
---

# سحر التواصل بين عمال NGINX: واحدة من أهم هياكل البيانات \`shared dict\`

API7.ai

October 27, 2022

[OpenResty (NGINX + Lua)](https://api7.ai/learning-center/openresty)

كما قلنا في المقال السابق، فإن `table` هي بنية البيانات الوحيدة في Lua. وهذا يتوافق مع `shared dict`، وهي بنية البيانات الأكثر أهمية التي يمكنك استخدامها في برمجة OpenResty. فهي تدعم تخزين البيانات، القراءة، العد الذري، وعمليات الطابور.

بناءً على `shared dict`، يمكنك تنفيذ التخزين المؤقت والاتصال بين عدة `Worker`s، والحد من المعدل، وإحصاءات حركة المرور، وغيرها من الوظائف. يمكنك استخدام `shared dict` كبديل بسيط لـ Redis، مع ملاحظة أن البيانات في `shared dict` ليست دائمة، لذا يجب عليك مراعاة فقدان البيانات المخزنة.

## عدة طرق لمشاركة البيانات

عند كتابة كود Lua في OpenResty، ستواجه حتمًا مشاركة البيانات بين `Worker`s مختلفة في مراحل مختلفة من الطلب. قد تحتاج أيضًا إلى مشاركة البيانات بين كود Lua وC.

لذا، قبل أن نقدم بشكل رسمي واجهات برمجة تطبيقات `shared dict`، دعنا أولاً نفهم طرق مشاركة البيانات الشائعة في OpenResty ونعرف كيفية اختيار طريقة مشاركة بيانات أكثر ملاءمة وفقًا للوضع الحالي.

**الأول هو المتغيرات في NGINX.** يمكنها مشاركة البيانات بين وحدات C في NGINX. وبطبيعة الحال، يمكنها أيضًا مشاركة البيانات بين وحدات C ووحدة `lua-nginx-module` التي يوفرها OpenResty، كما في الكود التالي.

```
location /foo {
     set $my_var ''; # هذا السطر مطلوب لإنشاء $my_var في وقت التكوين
     content_by_lua_block {
         ngx.var.my_var = 123;
         ...
     }
 }
```

ومع ذلك، فإن استخدام متغيرات NGINX لمشاركة البيانات بطيء لأنه يتضمن بحثًا في الهاش وتخصيصًا للذاكرة. أيضًا، هذه الطريقة لها قيد أنها يمكن استخدامها فقط لتخزين السلاسل ولا تدعم أنواع Lua المعقدة.

**الثاني هو `ngx.ctx`، الذي يمكنه مشاركة البيانات بين مراحل مختلفة من نفس الطلب.** إنه `table` عادي في Lua، لذا فهو سريع ويمكنه تخزين كائنات Lua المختلفة. دورة حياته على مستوى الطلب؛ عندما ينتهي الطلب، يتم تدمير `ngx.ctx`.

فيما يلي سيناريو استخدام نموذجي حيث نستخدم `ngx.ctx` لتخزين مكالمات مكلفة مثل متغيرات NGINX واستخدامها في مراحل مختلفة.

```
location /test {
     rewrite_by_lua_block {
         ngx.ctx.host = ngx.var.host
     }
     access_by_lua_block {
        if (ngx.ctx.host == 'api7.ai') then
            ngx.ctx.host = 'test.com'
        end
     }
     content_by_lua_block {
         ngx.say(ngx.ctx.host)
     }
 }
```

في هذه الحالة، إذا كنت تستخدم `curl` للوصول إليه.

```
curl -i 127.0.0.1:8080/test -H 'host:api7.ai'
```

ثم ستطبع `test.com`، مما يظهر أن `ngx.ctx` يشارك البيانات في مراحل مختلفة. بالطبع، يمكنك أيضًا تعديل المثال أعلاه بحفظ كائنات أكثر تعقيدًا مثل `table`s بدلاً من السلاسل البسيطة لترى إذا كانت تلبي توقعاتك.

ومع ذلك، ملاحظة خاصة هنا هي أن دورة حياة `ngx.ctx` على مستوى الطلب، لذا فهو لا يقوم بالتخزين المؤقت على مستوى الوحدة. على سبيل المثال، لقد ارتكبت خطأ باستخدام هذا في ملف `foo.lua` الخاص بي.

```
local ngx_ctx = ngx.ctx

local function bar()
    ngx_ctx.host =  'test.com'
end
```

يجب أن نستدعي ونخزن على مستوى الوظيفة.

```
local ngx = ngx

local function bar()
    ngx_ctx.host =  'test.com'
end
```

هناك الكثير من التفاصيل حول `ngx.ctx`، والتي سنستمر في استكشافها لاحقًا في قسم تحسين الأداء.

**الطريقة الثالثة تستخدم متغيرات على مستوى الوحدة لمشاركة البيانات بين جميع الطلبات داخل نفس `Worker`.** على عكس متغيرات NGINX السابقة و`ngx.ctx`، هذه الطريقة أقل قابلية للفهم قليلاً. لكن لا تقلق، المفهوم مجرد، والكود يأتي أولاً، لذا دعنا ننظر إلى مثال لفهم متغير على مستوى الوحدة.

```
-- mydata.lua
local _M = {}

local data = {
    dog = 3,
    cat = 4,
    pig = 5,
}

function _M.get_age(name)
    return data[name]
end

return _M
```

التكوين في `nginx.conf` هو كما يلي.

```
location /lua {
     content_by_lua_block {
         local mydata = require "mydata"
         ngx.say(mydata.get_age("dog"))
     }
 }
```

في هذا المثال، `mydata` هي وحدة يتم تحميلها مرة واحدة فقط بواسطة عملية `Worker`، وجميع الطلبات التي تتم معالجتها بواسطة `Worker` بعد ذلك تشارك الكود والبيانات الخاصة بوحدة `mydata`.

وبطبيعة الحال، فإن المتغير `data` في وحدة `mydata` هو متغير على مستوى الوحدة يقع في الجزء العلوي من الوحدة، أي في بداية الوحدة، وهو قابل للوصول من قبل جميع الوظائف.

لذا، يمكنك وضع البيانات التي تحتاج إلى مشاركتها بين الطلبات في المتغير العلوي للوحدة. ومع ذلك، من الضروري ملاحظة أننا نستخدم هذه الطريقة بشكل عام فقط لتخزين البيانات للقراءة فقط. إذا كانت هناك عمليات كتابة، يجب أن تكون حذرًا جدًا لأنه قد يكون هناك `race condition`، وهو خطأ صعب تحديده.

يمكننا تجربة ذلك مع المثال المبسط التالي.

```
-- mydata.lua
local _M = {}

local data = {
    dog = 3,
    cat = 4,
    pig = 5,
}

function _M.incr_age(name)
    data[name]  = data[name] + 1
    return data[name]
end

return _M
```

في الوحدة، نضيف وظيفة `incr_age`، التي تعدل البيانات في جدول `data`.

ثم، في الكود الذي يستدعي، نضيف السطر الأكثر أهمية `ngx.sleep(5)`، حيث `sleep` هي عملية `yield`.

```
location /lua {
     content_by_lua_block {
         local mydata = require "mydata"
         ngx.say(mydata. incr_age("dog"))
         ngx.sleep(5) -- yield API
         ngx.say(mydata. incr_age("dog"))
     }
 }
```

بدون هذا السطر من كود `sleep` (أو عمليات IO غير المحظورة الأخرى، مثل الوصول إلى Redis، إلخ)، لن تكون هناك عملية yield، ولن يكون هناك تنافس، وسيكون الإخراج النهائي متسلسلًا.

ولكن عندما نضيف هذا السطر من الكود، حتى لو كان فقط خلال 5 ثوانٍ من النوم، فمن المحتمل أن يقوم طلب آخر باستدعاء وظيفة `mydata.incr_age` وتعديل قيمة المتغير، مما يتسبب في أن تكون الأرقام النهائية غير متتالية. المنطق ليس بهذه البساطة في الكود الفعلي، والخطأ أكثر صعوبة في تحديده.

لذا، إلا إذا كنت متأكدًا من عدم وجود عملية `yield` في المنتصف التي ستسلم التحكم إلى حلقة أحداث NGINX، أنصحك بالحفاظ على متغيرات مستوى الوحدة للقراءة فقط.

الطريقة الرابعة والأخيرة تستخدم `shared dict` لمشاركة البيانات التي يمكن مشاركتها بين عدة عمال.

هذه الطريقة تعتمد على تنفيذ شجرة حمراء-سوداء، والتي تعمل بشكل جيد. ومع ذلك، لها قيودها: يجب عليك الإعلان عن حجم الذاكرة المشتركة في ملف تكوين NGINX مسبقًا، ولا يمكن تغيير ذلك أثناء التشغيل:

```
lua_shared_dict dogs 10m;
```

`shared dict` أيضًا يقوم فقط بتخزين بيانات `string` ولا يدعم أنواع بيانات Lua المعقدة. هذا يعني أنه عندما أحتاج إلى تخزين أنواع بيانات معقدة مثل `table`s، سأضطر إلى استخدام JSON أو طرق أخرى لتسلسل وفك تسلسل البيانات، مما سيسبب خسارة كبيرة في الأداء.

على أي حال، لا يوجد حل سحري هنا، ولا توجد طريقة مثالية لمشاركة البيانات. يجب عليك الجمع بين عدة طرق وفقًا لاحتياجاتك وسيناريوهاتك.

## Shared dict

لقد قضينا الكثير من الوقت في تعلم جزء مشاركة البيانات أعلاه، وقد يتساءل البعض: يبدو أنها ليست مرتبطة مباشرة بـ shared dict. أليس هذا خارج الموضوع؟

في الواقع، لا. فكر في الأمر: لماذا يوجد `shared dict` في OpenResty؟ تذكر أن الطرق الثلاث الأولى لمشاركة البيانات كلها على مستوى الطلب أو مستوى `Worker` الفردي. لذلك، في التطبيق الحالي لـ OpenResty، فقط `shared dict` يمكنه تحقيق مشاركة البيانات بين `Worker`s، مما يتيح الاتصال بين `Worker`s، وهذا هو قيمة وجوده.

في رأيي، فهم سبب وجود التكنولوجيا ومعرفة الفروق والمزايا مقارنة بالتكنولوجيات المشابهة الأخرى هو أكثر أهمية بكثير من مجرد إتقان استدعاء واجهات برمجة التطبيقات التي توفرها. هذه الرؤية التقنية تمنحك درجة من البصيرة والاستبصار، ويمكن القول إنها فرق مهم بين المهندسين والمهندسين المعماريين.

بالعودة إلى shared dict، الذي يوفر أكثر من 20 واجهة برمجة تطبيقات Lua للعامة، وكلها ذرية، لذا لا داعي للقلق بشأن المنافسة في حالة وجود عدة `Workers` وارتفاع التزامن.

هذه الواجهات لديها [وثائق رسمية](https://github.com/openresty/lua-nginx-module#ngxshareddict) مفصلة، لذا لن أتطرق إليها جميعًا. أريد التأكيد مرة أخرى على أن لا دورة تقنية يمكن أن تحل محل القراءة الدقيقة للوثائق الرسمية. لا يمكن لأحد تخطي هذه الإجراءات المملة والغبية.

التالي، دعنا نستمر في النظر إلى واجهات برمجة تطبيقات `shared dict`، والتي يمكن تقسيمها إلى ثلاث فئات: قراءة/كتابة القاموس، عمليات الطابور، والإدارة.

### قراءة/كتابة القاموس

دعنا أولاً ننظر إلى فئة قراءة وكتابة القاموس. في النسخة الأصلية، كانت هناك فقط واجهات برمجة تطبيقات لفئة قراءة وكتابة القاموس، وهي الميزات الأكثر شيوعًا في القواميس المشتركة. هنا أبسط مثال.

```
$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs
                               dict:set("Tom", 56)
                               print(dict:get("Tom"))'
```

بالإضافة إلى `set`، يوفر OpenResty أيضًا أربع طرق كتابة: `safe_set`، `add`، `safe_add`، و`replace`. معنى البادئة `safe` هنا هو أنه إذا كانت الذاكرة ممتلئة، بدلاً من إزالة البيانات القديمة وفقًا لـ LRU، فإن الكتابة ستفشل وستعيد خطأ `no memory`.

بالإضافة إلى `get`، يوفر OpenResty أيضًا طريقة `get_stale` لقراءة البيانات، والتي لديها قيمة إضافية للبيانات المنتهية الصلاحية مقارنة بطريقة `get`.

```
value, flags, stale = ngx.shared.DICT:get_stale(key)
```

يمكنك أيضًا استدعاء طريقة `delete` لحذف المفتاح المحدد، وهو ما يعادل `set(key, nil)`.

### عمليات الطابور

بالانتقال إلى عمليات الطابور، تمت إضافتها لاحقًا إلى OpenResty وتوفر واجهة مشابهة لـ Redis. يتم وصف كل عنصر في الطابور بواسطة `ngx_http_lua_shdict_list_node_t`.

```
typedef struct {
    ngx_queue_t queue;
    uint32_t value_len;
    uint8_t value_type;
    u_char data[1];
} ngx_http_lua_shdict_list_node_t;
```

لقد نشرت [PR](https://github.com/openresty/lua-nginx-module/pull/586/files) لواجهات برمجة تطبيقات الطابور هذه في المقال. إذا كنت مهتمًا بهذا، يمكنك متابعة الوثائق، حالات الاختبار، والكود المصدري لتحليل التنفيذ المحدد.

ومع ذلك، لا توجد أمثلة كود مقابلة لواجهات برمجة تطبيقات الطابور الخمسة التالية في الوثائق، لذا سأقدمها باختصار هنا.

- `lpush``/``rpush` يعني إضافة عناصر في كلا طرفي الطابور.
- `lpop``/``rpop`، الذي يخرج العناصر من كلا طرفي الطابور.
- `llen`، الذي يشير إلى عدد العناصر المرجعة للطابور.

دعونا لا ننسى أداة أخرى مفيدة ناقشناها في المقال السابق: حالات الاختبار. يمكننا عادةً العثور على الكود المقابل في حالة اختبار إذا لم يكن في الوثائق. الاختبارات المتعلقة بالطابور موجودة بالضبط في ملف `145-shdict-list.t`.

```
=== TEST 1: lpush & lpop
--- http_config
    lua_shared_dict dogs 1m;
--- config
    location = /test {
        content_by_lua_block {
            local dogs = ngx.shared.dogs

            local len, err = dogs:lpush("foo", "bar")
            if len then
                ngx.say("push success")
            else
                ngx.say("push err: ", err)
            end

            local val, err = dogs:llen("foo")
            ngx.say(val, " ", err)

            local val, err = dogs:lpop("foo")
            ngx.say(val, " ", err)

            local val, err = dogs:llen("foo")
            ngx.say(val, " ", err)

            local val, err = dogs:lpop("foo")
            ngx.say(val, " ", err)
        }
    }
--- request
GET /test
--- response_body
push success
1 nil
bar nil
0 nil
nil nil
--- no_error_log
[error]
```

### الإدارة

واجهة برمجة التطبيقات النهائية للإدارة هي أيضًا إضافة لاحقة وهي مطلب شائع في المجتمع. أحد الأمثلة الأكثر شيوعًا هو استخدام الذاكرة المشتركة. على سبيل المثال، إذا طلب المستخدم مساحة `100M` كـ `shared dict`، هل هذه `100M` كافية؟ كم عدد المفاتيح المخزنة فيها، وما هي هذه المفاتيح؟ هذه كلها أسئلة حقيقية.

لهذا النوع من المشاكل، يأمل OpenResty الرسمي أن يستخدم المستخدمون الرسوم البيانية للهب لحلها، أي بطريقة غير تدخلية، مع الحفاظ على قاعدة الكود فعالة ونظيفة، بدلاً من توفير واجهة برمجة تطبيقات تدخلية لإرجاع النتائج مباشرة.

ولكن من منظور ودي للمستخدم، هذه واجهات برمجة التطبيقات للإدارة لا تزال ضرورية. بعد كل شيء، المشاريع مفتوحة المصدر مصممة لحل متطلبات المنتج، وليس لعرض التكنولوجيا نفسها. لذا، دعنا ننظر إلى واجهات برمجة التطبيقات للإدارة التالية التي سيتم إضافتها لاحقًا.

أولاً `get_keys(max_count?)`، التي تعيد بشكل افتراضي أول `1024` مفتاحًا فقط؛ إذا قمت بتعيين `max_count` إلى `0`، فستعيد جميع المفاتيح. ثم تأتي `capacity` و`free_space`، وكلاهما جزء من مستودع `lua-resty-core`، لذا تحتاج إلى `require` قبل استخدامها.

```
require "resty.core.shdict"

local cats = ngx.shared.cats
local capacity_bytes = cats:capacity()
local free_page_bytes = cats:free_space()
```

تعيد حجم الذاكرة المشتركة (الحجم المحدد في `lua_shared_dict`) وعدد البايتات من الصفحات الحرة. نظرًا لأن `shared dict` يتم تخصيصه بالصفحات، حتى إذا أعاد `free_space` `0`، فقد يكون هناك مساحة في الصفحات المخصصة. وبالتالي، فإن قيمته المرجعة لا تمثل مقدار الذاكرة المشتركة المستخدمة.

## الخلاصة

في الممارسة العملية، نستخدم غالبًا التخزين المؤقت متعدد المستويات، ومشروع OpenResty الرسمي لديه أيضًا حزمة تخزين مؤقت. هل يمكنك معرفة أي المشاريع هي؟ أو هل تعرف بعض مكتبات `lua-resty` الأخرى التي تقوم بتغليف التخزين المؤقت؟

نرحب بمشاركة هذه المقالة مع زملائك وأصدقائك حتى نتمكن من التواصل والتحسين معًا.

Share article link

Share

Related Posts

[![أسئلة وأجوبة حول OpenResty | كيفية استخدام OpenResty في الممارسة العملية](https://static.api7.ai/uploads/2023/02/12/jbv9M0zi_Hello%20World%20in%20OpenResty%20%283%29.png)](https://api7.ai/learning-center/openresty/faq-5)

## [أسئلة وأجوبة حول OpenResty | كيفية استخدام OpenResty في الممارسة العملية](https://api7.ai/learning-center/openresty/faq-5)

[![الجزء الثالث: كيفية بناء بوابة API للخدمات المصغرة باستخدام OpenResty](https://static.api7.ai/uploads/2023/02/07/3XJXY9Jy_45.png)](https://api7.ai/learning-center/openresty/how-to-build-a-microservices-api-gateway-part-3)

## [الجزء الثالث: كيفية بناء بوابة API للخدمات المصغرة باستخدام OpenResty](https://api7.ai/learning-center/openresty/how-to-build-a-microservices-api-gateway-part-3)

Table of Contents

- [عدة طرق لمشاركة البيانات](#%D8%B9%D8%AF%D8%A9-%D8%B7%D8%B1%D9%82-%D9%84%D9%85%D8%B4%D8%A7%D8%B1%D9%83%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA)

- [Shared dict](#shared-dict)

  - [قراءة/كتابة القاموس](#%D9%82%D8%B1%D8%A7%D8%A1%D8%A9%D9%83%D8%AA%D8%A7%D8%A8%D8%A9-%D8%A7%D9%84%D9%82%D8%A7%D9%85%D9%88%D8%B3)
  - [عمليات الطابور](#%D8%B9%D9%85%D9%84%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D8%B7%D8%A7%D8%A8%D9%88%D8%B1)
  - [الإدارة](#%D8%A7%D9%84%D8%A5%D8%AF%D8%A7%D8%B1%D8%A9)

- [الخلاصة](#%D8%A7%D9%84%D8%AE%D9%84%D8%A7%D8%B5%D8%A9)
