كتابة موازن بسيط على الذهاب


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

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

كيف يعمل موازننا


يتم استخدام خوارزميات مختلفة لتوزيع الحمل بين الأسطح الخلفية. على سبيل المثال:

  • Round Robin - يتم توزيع الحمل بالتساوي ، مع مراعاة نفس القوة الحاسوبية للخوادم.
  • مرجح جولة روبن - اعتمادا على قوة المعالجة ، يمكن تعيين خوادم أوزان مختلفة.
  • الاتصالات الأقل - يتم توزيع الحمل عبر الخوادم التي تحتوي على أقل عدد من الاتصالات النشطة.

في موازننا ، ننفذ أبسط الخوارزميات - Round Robin.



اختيار في جولة روبن


خوارزمية Round Robin بسيطة. إنه يعطي جميع فناني الأداء نفس الفرصة لإكمال المهام.


حدد الخوادم في Round Robin لمعالجة الطلبات الواردة.

كما هو موضح في الرسم التوضيحي ، تحدد الخوارزمية الخوادم في دائرة بشكل دوري. لكن لا يمكننا اختيارهم مباشرة ، أليس كذلك؟

وإذا كان الخادم يكذب؟ ربما لا نحتاج إلى إرسال حركة مرور إليه. وهذا يعني أنه لا يمكن استخدام الخادم مباشرة حتى نصل إلى الحالة المطلوبة. من الضروري توجيه حركة المرور فقط إلى الخوادم التي يتم تشغيلها وتشغيلها.

تحديد الهيكل


نحن بحاجة إلى تتبع جميع التفاصيل المتعلقة الخلفية. تحتاج إلى معرفة ما إذا كان على قيد الحياة ، وتتبع عنوان URL. للقيام بذلك ، يمكننا تحديد الهيكل التالي:

type Backend struct { URL *url.URL Alive bool mux sync.RWMutex ReverseProxy *httputil.ReverseProxy } 

لا تقلق ، سأشرح معنى الحقول في الخلفية.

الآن في الموازن ، تحتاج إلى تتبع كل المؤثرات الخلفية بطريقة أو بأخرى. للقيام بذلك ، يمكنك استخدام Slice وعداد متغير. حددها في ServerPool:

 type ServerPool struct { backends []*Backend current uint64 } 

باستخدام ReverseProxy


كما حددنا بالفعل ، فإن جوهر الموازن هو توزيع حركة المرور على خوادم مختلفة وإعادة النتائج إلى العميل. كما تقول وثائق Go:

ReverseProxy هو معالج HTTP يأخذ الطلبات الواردة ويرسلها إلى خادم آخر ، ويعيد الإجابات إلى العميل.

بالضبط ما نحتاجه. لا حاجة لإعادة اختراع العجلة. يمكنك ببساطة دفق طلباتنا من خلال ReverseProxy .

 u, _ := url.Parse("http://localhost:8080") rp := httputil.NewSingleHostReverseProxy(u) // initialize your server and add this as handler http.HandlerFunc(rp.ServeHTTP) 

باستخدام httputil.NewSingleHostReverseProxy(url) يمكنك تهيئة ReverseProxy ، والذي سيبث الطلبات إلى url تم تمريره. في المثال أعلاه ، تم إرسال جميع الطلبات إلى المضيف المحلي: 8080 ، وتم إرسال النتائج إلى العميل.

إذا نظرت إلى توقيع أسلوب ServeHTTP ، فيمكنك العثور على توقيع معالج HTTP فيه. لذلك ، يمكنك تمريره إلى HandlerFunc في http .

أمثلة أخرى في الوثائق .

بالنسبة إلى ReverseProxy ، يمكنك بدء ReverseProxy URL المرتبط في Backend بحيث يقوم ReverseProxy بتوجيه الطلبات إلى URL .

عملية اختيار الخادم


أثناء اختيار الخادم التالي ، نحتاج إلى تخطي الخوادم الأساسية. ولكن تحتاج إلى تنظيم العد.

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

أفضل حل لتلبية هذه المتطلبات هو الزيادة الذرية. Go يدعمها مع الحزمة atomic .

 func (s *ServerPool) NextIndex() int { return int(atomic.AddUint64(&s.current, uint64(1)) % uint64(len(s.backends))) } 

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

اختيار خادم الحية


نحن نعلم بالفعل أن طلباتنا يتم تدويرها دوريًا عبر جميع الخوادم. ونحن بحاجة فقط لتخطي الخمول.

إرجاع GetNext() دائمًا قيمة تتراوح من 0 إلى طول الصفيف. في أي وقت ، يمكننا الحصول على العقدة التالية ، وإذا كانت غير نشطة ، فنحن بحاجة إلى مزيد من البحث من خلال المصفوفة كجزء من الحلقة.


نحن حلقة من خلال مجموعة.

كما هو موضح في الرسم التوضيحي ، نريد الانتقال من العقدة التالية إلى نهاية القائمة. يمكن القيام بذلك باستخدام next + length . ولكن لتحديد فهرس ، تحتاج إلى قصره على طول الصفيف. يمكن القيام بذلك بسهولة باستخدام عملية التعديل.

بعد أن وجدنا خادمًا عاملاً أثناء البحث ، يجب وضع علامة عليه كخادم:

 // GetNextPeer returns next active peer to take a connection func (s *ServerPool) GetNextPeer() *Backend { // loop entire backends to find out an Alive backend next := s.NextIndex() l := len(s.backends) + next // start from next and move a full cycle for i := next; i < l; i++ { idx := i % len(s.backends) // take an index by modding with length // if we have an alive backend, use it and store if its not the original one if s.backends[idx].IsAlive() { if i != next { atomic.StoreUint64(&s.current, uint64(idx)) // mark the current one } return s.backends[idx] } } return nil } 

تجنب حالة السباق في بنية Backend


هنا تحتاج إلى تذكر قضية مهمة. تحتوي بنية Backend على متغير يمكن للعديد من goroutines تعديله أو الاستعلام عنه في نفس الوقت.

نحن نعلم أن goroutines سيقرأ المتغير أكثر من الكتابة إليه. لذلك ، لتسلسل الوصول إلى Alive اخترنا RWMutex .

 // SetAlive for this backend func (b *Backend) SetAlive(alive bool) { b.mux.Lock() b.Alive = alive b.mux.Unlock() } // IsAlive returns true when backend is alive func (b *Backend) IsAlive() (alive bool) { b.mux.RLock() alive = b.Alive b.mux.RUnlock() return } 

تحقيق التوازن بين الطلبات


الآن يمكننا صياغة طريقة بسيطة لموازنة طلباتنا. سوف تفشل فقط إذا سقطت جميع الخوادم.

 // lb load balances the incoming request func lb(w http.ResponseWriter, r *http.Request) { peer := serverPool.GetNextPeer() if peer != nil { peer.ReverseProxy.ServeHTTP(w, r) return } http.Error(w, "Service not available", http.StatusServiceUnavailable) } 

يمكن تمرير هذه الطريقة إلى خادم HTTP ببساطة مثل HandlerFunc .

 server := http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: http.HandlerFunc(lb), } 

نحن نوجه حركة المرور فقط إلى الخوادم التي تعمل


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

  • نشط: تنفيذ الطلب الحالي ، نجد أن الخادم المحدد لا يستجيب ، ووضع علامة عليه في وضع الخمول.
  • المبني للمجهول: يمكنك اختبار اتصال خوادم في فترة زمنية معينة والتحقق من الحالة.

التحقق بنشاط الخوادم قيد التشغيل


في حالة ReverseProxy أي خطأ ReverseProxy يبدأ ReverseProxy وظيفة رد الاتصال ErrorHandler . هذا يمكن استخدامه للكشف عن الفشل:

 proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, e error) { log.Printf("[%s] %s\n", serverUrl.Host, e.Error()) retries := GetRetryFromContext(request) if retries < 3 { select { case <-time.After(10 * time.Millisecond): ctx := context.WithValue(request.Context(), Retry, retries+1) proxy.ServeHTTP(writer, request.WithContext(ctx)) } return } // after 3 retries, mark this backend as down serverPool.MarkBackendStatus(serverUrl, false) // if the same request routing for few attempts with different backends, increase the count attempts := GetAttemptsFromContext(request) log.Printf("%s(%s) Attempting retry %d\n", request.RemoteAddr, request.URL.Path, attempts) ctx := context.WithValue(request.Context(), Attempts, attempts+1) lb(writer, request.WithContext(ctx)) } 

في تطوير معالج الأخطاء هذا ، استخدمنا إمكانيات عمليات الإغلاق. هذا يتيح لنا التقاط متغيرات خارجية مثل عناوين URL للخوادم في طريقتنا. يتحقق المعالج من عداد إعادة المحاولة ، وإذا كان أقل من 3 ، فإننا نرسل الطلب نفسه مرة أخرى إلى نفس الخادم. هذا لأنه بسبب الأخطاء المؤقتة ، قد يتخلى الخادم عن طلباتنا ، لكنه سيصبح متاحًا قريبًا (قد لا يكون لدى الخادم مآخذ توصيل مجانية للعملاء الجدد). لذلك تحتاج إلى ضبط مؤقت التأخير لمحاولة جديدة بعد حوالي 10 مللي ثانية. مع كل طلب نقوم بزيادة عداد المحاولات.

بعد فشل كل محاولة ، نحتفل على الخادم باعتباره خاملاً.

أنت الآن بحاجة إلى تعيين خادم جديد لنفس الطلب. سنفعل ذلك باستخدام عداد العداد باستخدام حزمة context . بعد زيادة عداد المحاولات ، نقوم بتمريرها إلى lb لتحديد خادم جديد لمعالجة الطلب.

لا يمكننا القيام بذلك إلى أجل غير مسمى ، لذلك سنقوم بالتحقق مما إذا كان قد تم الوصول إلى الحد الأقصى لعدد المحاولات قبل متابعة معالجة الطلب.

يمكنك ببساطة الحصول على عداد المحاولات من الطلب ، إذا وصل إلى الحد الأقصى ، فسنقطع الطلب.

 // lb load balances the incoming request func lb(w http.ResponseWriter, r *http.Request) { attempts := GetAttemptsFromContext(r) if attempts > 3 { log.Printf("%s(%s) Max attempts reached, terminating\n", r.RemoteAddr, r.URL.Path) http.Error(w, "Service not available", http.StatusServiceUnavailable) return } peer := serverPool.GetNextPeer() if peer != nil { peer.ReverseProxy.ServeHTTP(w, r) return } http.Error(w, "Service not available", http.StatusServiceUnavailable) } 

هذا هو تنفيذ العودية.

باستخدام حزمة السياق


تسمح لك حزمة context بتخزين البيانات المفيدة في طلبات HTTP. سوف نستخدم هذا بنشاط لتتبع البيانات المتعلقة بالطلبات - عدادات Retry وإعادة Retry .

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

 const ( Attempts int = iota Retry ) 

يمكنك بعد ذلك استخراج القيمة ، كما نفعل عادةً مع HashMap . قد تعتمد القيمة الافتراضية على الموقف الحالي.

 // GetAttemptsFromContext returns the attempts for request func GetRetryFromContext(r *http.Request) int { if retry, ok := r.Context().Value(Retry).(int); ok { return retry } return 0 } 

خادم السلبي التحقق من الصحة


الشيكات السلبية تحديد واستعادة الخوادم الساقطة. نحن نقوم بتجربتها في فترة زمنية معينة لتحديد حالتها.

لإجراء اختبار ping ، حاول تأسيس اتصال TCP. إذا كان الخادم يستجيب ، فإننا نحتفل به. يمكن تكييف هذه الطريقة لاستدعاء نقاط نهاية معينة مثل /status . تأكد من إغلاق الاتصال بعد إنشائه لتقليل الحمل الإضافي على الخادم. وإلا ، فسيحاول الحفاظ على هذا الاتصال وسيستنفد موارده في النهاية.

 // isAlive checks whether a backend is Alive by establishing a TCP connection func isBackendAlive(u *url.URL) bool { timeout := 2 * time.Second conn, err := net.DialTimeout("tcp", u.Host, timeout) if err != nil { log.Println("Site unreachable, error: ", err) return false } _ = conn.Close() // close it, we dont need to maintain this connection return true } 

يمكنك الآن تكرار الخوادم وتحديد حالاتها:

 // HealthCheck pings the backends and update the status func (s *ServerPool) HealthCheck() { for _, b := range s.backends { status := "up" alive := isBackendAlive(b.URL) b.SetAlive(alive) if !alive { status = "down" } log.Printf("%s [%s]\n", b.URL, status) } } 

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

 // healthCheck runs a routine for check status of the backends every 2 mins func healthCheck() { t := time.NewTicker(time.Second * 20) for { select { case <-tC: log.Println("Starting health check...") serverPool.HealthCheck() log.Println("Health check completed") } } } 

في هذا الرمز ، تقوم قناة <-tC بإرجاع قيمة كل 20 ثانية. select يسمح لك لتحديد هذا الحدث. في حالة عدم وجود موقف default ، ينتظر حتى يتم تنفيذ حالة واحدة على الأقل.

الآن قم بتشغيل الكود في مجموعة منفصلة:

 go healthCheck() 

استنتاج


في هذه المقالة ، درسنا العديد من الأسئلة:

  • جولة روبن خوارزمية
  • ReverseProxy من المكتبة القياسية
  • كائنات المزامنة
  • العمليات الذرية
  • دوائر قصيرة
  • الاسترجاعات
  • عملية الاختيار

هناك العديد من الطرق لتحسين الموازن لدينا. على سبيل المثال:

  • استخدم الكومة لفرز الخوادم الحية لتقليل نطاق البحث.
  • جمع الإحصاءات.
  • قم بتطبيق خوارزمية الدارة المستديرة الموزونة مع أقل عدد من الاتصالات.
  • إضافة دعم لملفات التكوين.

و هكذا.

شفرة المصدر هنا .

Source: https://habr.com/ru/post/ar476276/


All Articles