كيف قمنا بتحسين خادم DNS الخاص بنا باستخدام أدوات GO

تحسبًا لبدء تيار جديد في دورة "Developer Golang" ، أعدت ترجمة لمواد مثيرة للاهتمام.




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

dnsflood هي أداة صغيرة يمكنها إنشاء عدد كبير من طلبات UDP.

# timeout 20s ./dnsflood example.com 127.0.0.1 -p 2053

أظهرت مراقبة النظام أن استخدام الذاكرة لخدمتنا نما بسرعة كبيرة لدرجة أننا اضطررنا لإيقافه ، وإلا فإننا قد نواجه أخطاء OOM. كان مثل مشكلة تسرب للذاكرة. هناك العديد من الأسباب وراء تسرب الذاكرة "المتشابه" و "الحقيقي":

  • معلقة الغوروتينات
  • إساءة استخدام المؤجل والمُنهي
  • السلاسل والقطع الفرعية
  • المتغيرات العالمية

هذه الوظيفة تقدم شرحا وافيا لمختلف التسريبات.

قبل إجراء أي استنتاجات ، دعنا أولاً إجراء ملف تعريف.

Godebug


يمكن تمكين أدوات تصحيح مختلفة باستخدام متغير بيئة GODEBUG، بتمرير قائمة مفصولة بفواصل من الأزواج name=value.

مخطط التتبع


يمكن أن يوفر تتبع المجدول معلومات حول سلوك الغوروتين في وقت التشغيل. لتمكين تتبع المجدول ، قم بتشغيل البرنامج باستخدام GODEBUG=schedtrace=100، تحدد القيمة فترة الإخراج في مللي ثانية.

$ GODEBUG=schedtrace=100 ./redins -c config.json
SCHED 2952ms: ... runqueue=3 [26 11 7 18 13 30 6 3 24 25 11 0]
SCHED 3053ms: ... runqueue=3 [0 0 0 0 0 0 0 0 4 0 21 0]
SCHED 3154ms: ... runqueue=0 [0 6 2 4 0 30 0 5 0 11 2 5]
SCHED 3255ms: ... runqueue=1 [0 0 0 0 0 0 0 0 0 0 0 0]
SCHED 3355ms: ... runqueue=0 [1 0 0 0 0 0 0 0 0 0 0 0]
SCHED 3456ms: ... runqueue=0 [0 0 0 0 0 0 0 0 0 0 0 0]
SCHED 3557ms: ... runqueue=0 [13 0 3 0 3 33 2 0 10 8 10 14]
SCHED 3657ms: ...runqueue=3 [14 1 0 5 19 54 9 1 0 1 29 0]
SCHED 3758ms: ... runqueue=0 [67 1 5 0 0 1 0 0 87 4 0 0]
SCHED 3859ms: ... runqueue=6 [0 0 3 6 0 0 0 0 3 2 2 19]
SCHED 3960ms: ... runqueue=0 [0 0 1 0 1 0 0 1 0 1 0 0]
SCHED 4060ms: ... runqueue=5 [4 0 5 0 1 0 0 0 0 0 0 0]
SCHED 4161ms: ... runqueue=0 [0 0 0 0 0 0 0 1 0 0 0 0]
SCHED 4262ms: ... runqueue=4 [0 128 21 172 1 19 8 2 43 5 139 37]
SCHED 4362ms: ... runqueue=0 [0 0 0 0 0 0 0 0 0 0 0 0]
SCHED 4463ms: ... runqueue=6 [0 28 23 39 4 11 4 11 25 0 25 0]
SCHED 4564ms: ... runqueue=354 [51 45 33 32 15 20 8 7 5 42 6 0]

Runqueue هو طول الطابور العالمي من goroutines لتشغيل. الأرقام الموجودة بين قوسين هي طول قائمة انتظار العملية.

الوضع المثالي هو عندما تكون جميع العمليات مشغولة في تنفيذ goroutines ، ويتم توزيع الطول المعتدل لقائمة انتظار التنفيذ بالتساوي بين جميع العمليات:

SCHED 2449ms: gomaxprocs=12 idleprocs=0 threads=40 spinningthreads=1 idlethreads=1 runqueue=20 [20 20 20 20 20 20 20 20 20 20 20]

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

أثر جامع القمامة


لتمكين تتبع جمع البيانات المهملة (GC) ، قم بتشغيل البرنامج باستخدام متغير بيئة GODEBUG=gctrace=1:

GODEBUG=gctrace=1 ./redins -c config1.json
.
.
.
gc 30 @3.727s 1%: 0.066+21+0.093 ms clock, 0.79+128/59/0+1.1 ms cpu, 67->71->45 MB, 76 MB goal, 12 P
gc 31 @3.784s 2%: 0.030+27+0.053 ms clock, 0.36+177/81/7.8+0.63 ms cpu, 79->84->55 MB, 90 MB goal, 12 P
gc 32 @3.858s 3%: 0.026+34+0.024 ms clock, 0.32+234/104/0+0.29 ms cpu, 96->100->65 MB, 110 MB goal, 12 P
gc 33 @3.954s 3%: 0.026+44+0.13 ms clock, 0.32+191/131/57+1.6 ms cpu, 117->123->79 MB, 131 MB goal, 12 P
gc 34 @4.077s 4%: 0.010+53+0.024 ms clock, 0.12+241/159/69+0.29 ms cpu, 142->147->91 MB, 158 MB goal, 12 P
gc 35 @4.228s 5%: 0.017+61+0.12 ms clock, 0.20+296/179/94+1.5 ms cpu, 166->174->105 MB, 182 MB goal, 12 P
gc 36 @4.391s 6%: 0.017+73+0.086 ms clock, 0.21+492/216/4.0+1.0 ms cpu, 191->198->122 MB, 210 MB goal, 12 P
gc 37 @4.590s 7%: 0.049+85+0.095 ms clock, 0.59+618/253/0+1.1 ms cpu, 222->230->140 MB, 244 MB goal, 12 P
.
.
.

كما نرى هنا ، يزداد مقدار الذاكرة المستخدمة ، ويزداد أيضًا مقدار الوقت الذي يستغرقه gc لإكمال عمله. هذا يعني أننا نستهلك ذاكرة أكثر مما يمكنها التعامل معها.

يمكنك GODEBUGمعرفة المزيد عن بعض متغيرات بيئة golang الأخرى هنا .

تمكين ملف التعريف


go tool pprof- أداة لتحليل وتنميط البيانات. هناك طريقتان للتهيئة pprof: إما استدعاء وظائف مباشرة runtime/pprofفي التعليمات البرمجية الخاصة بك ، على سبيل المثال pprof.StartCPUProfile()، أو تثبيت net/http/pprofمستمع http والحصول على البيانات من هناك ، وهو ما فعلناه. لديها pprofاستهلاك قليل جدًا من الموارد ، لذلك يمكن استخدامها بأمان في التطوير ، ولكن يجب ألا تكون نقطة نهاية الملف الشخصي متاحة للجمهور ، حيث يمكن الكشف عن البيانات الحساسة.

كل ما نحتاجه للخيار الثاني هو استيراد حزمة "net / http / pprof" :

import (
  _ "net/http/pprof"
)

ثم أضف مستمع http:

go func() {
  log.Println(http.ListenAndServe("localhost:6060", nil))
}()

يحتوي Pprof على العديد من الملفات الشخصية الافتراضية:

  • allocs : جلب جميع عمليات تخصيص الذاكرة السابقة
  • block : تتبع المكدس الذي أدى إلى حجب بدائل المزامنة
  • الغوروتين : تتبع مكدس لجميع الغوروتينات الحالية
  • الكومة : إحضار تخصيصات ذاكرة الكائنات الحية.
  • كائن المزامنة (mutex) : تتبع تكديس أصحاب عناصر المزامنة المتضاربة
  • الملف الشخصي : ملف تعريف المعالج.
  • threadcreate : تتبع مكدس أدى إلى إنشاء سلاسل رسائل جديدة في نظام التشغيل
  • trace : تتبع تنفيذ البرنامج الحالي.

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

محلل المعالج


‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10


يعمل ملف تعريف المعالج بشكل افتراضي لمدة 30 ثانية (يمكننا تغيير ذلك عن طريق تعيين المعلمة seconds) ويجمع عينات كل 100 مللي ثانية ، وبعد ذلك ينتقل إلى الوضع التفاعلي. هي معظم الأوامر المشتركة المتاحة top، list، web.

يُستخدم top nلعرض أهم الإدخالات بتنسيق النص ، وهناك أيضًا خياران لفرز الإخراج ، -cumللترتيب التراكمي و -flat.

(pprof) top 10 -cum
Showing nodes accounting for 1.50s, 6.19% of 24.23s total
Dropped 347 nodes (cum <= 0.12s)
Showing top 10 nodes out of 186
flat  flat%   sum%  cum   cum%
0.03s 0.12% 0.12% 16.7s 69.13% (*Server).serveUDPPacket
0.05s 0.21% 0.33% 15.6s 64.51% (*Server).serveDNS
0     0%    0.33% 14.3s 59.10% (*ServeMux).ServeDNS
0     0%    0.33% 14.2s 58.73% HandlerFunc.ServeDNS
0.01s 0.04% 0.37% 14.2s 58.73% main.handleRequest
0.07s 0.29% 0.66% 13.5s 56.00% (*DnsRequestHandler).HandleRequest
0.99s 4.09% 4.75% 7.56s 31.20% runtime.gentraceback
0.02s 0.08% 4.83% 7.02s 28.97% runtime.systemstack
0.31s 1.28% 6.11% 6.62s 27.32% runtime.mallocgc
0.02s 0.08% 6.19% 6.35s 26.21% (*DnsRequestHandler).FindANAME
(pprof)

تستخدم listلاستكشاف الوظيفة.

(pprof) list handleRequest
Total: 24.23s
ROUTINE ======================== main.handleRequest in /home/arash/go/src/arvancloud/redins/redins.go
     10ms     14.23s (flat, cum) 58.73% of Total
        .          .     35: l          *handler.RateLimiter
        .          .     36: configFile string
        .          .     37:)
        .          .     38:
        .          .     39:func handleRequest(w dns.ResponseWriter, r *dns.Msg) {
     10ms      610ms     40: context := handler.NewRequestContext(w, r)
        .       50ms     41: logger.Default.Debugf("handle request: [%d] %s %s", r.Id, context.RawName(), context.Type())
        .          .     42:
        .          .     43: if l.CanHandle(context.IP()) {
        .     13.57s     44:  h.HandleRequest(context)
        .          .     45: } else {
        .          .     46:  context.Response(dns.RcodeRefused)
        .          .     47: }
        .          .     48:}
        .          .     49:
(pprof)

webينشئ الفريق رسمًا بيانيًا SVG للمناطق المهمة ويفتحه في المتصفح.

(pprof)web handleReques



يؤدي قضاء وقت كبير في وظائف GC ، مثل runtime.mallocgc، في كثير من الأحيان إلى تخصيص ذاكرة كبيرة ، والتي يمكن أن تضيف الحمل إلى جامع القمامة وزيادة الكمون.

قد يكون مقدار كبير من الوقت المستغرق في آليات المزامنة ، مثل runtime.chansendأو runtime.lock، علامة على وجود تعارض. يعني

قضاء وقت كبير على syscall.Read/Writeالاستخدام المفرط لعمليات الإدخال / الإخراج.

محلل الذاكرة


$ go tool pprof http://localhost:6060/debug/pprof/allocs


بشكل افتراضي ، يعرض عمر الذاكرة المخصصة. ويمكننا أن نرى عدد من الكائنات المحددة باستخدام -alloc_objectsخيارات أخرى مفيدة: -iuse_objectsو -inuse_spaceلفحص الحية الذاكرة.

عادة ، إذا كنت تريد تقليل استهلاك الذاكرة ، فأنت بحاجة إلى النظر -inuse_space، ولكن إذا كنت تريد تقليل وقت الاستجابة ، فابحث عن وقت -alloc_objectsتشغيل / تحميل كافٍ.

تحديد عنق الزجاجة


من المهم أولاً تحديد نوع عنق الزجاجة (المعالج ، I / O ، الذاكرة) الذي نتعامل معه. بالإضافة إلى المحللون ، هناك نوع آخر من الأدوات.

go tool traceيمكن أن تظهر ما تفعله الغوروتين بالتفصيل. لجمع مثال التتبع ، نحتاج إلى إرسال طلب http إلى نقطة نهاية التتبع:

$ curl http://localhost:6060/debug/pprof/trace?seconds=5 --output trace.out

يمكن عرض الملف الذي تم إنشاؤه باستخدام أداة التتبع:

$ go tool trace trace.out
2019/12/25 15:30:50 Parsing trace...
2019/12/25 15:30:59 Splitting trace...
2019/12/25 15:31:10 Opening browser. Trace viewer is listening on http://127.0.0.1:42703

Go tool trace هو تطبيق ويب يستخدم بروتوكول Chrome DevTools وهو متوافق فقط مع متصفحات Chrome. تبدو الصفحة الرئيسية شيئًا مثل هذا:

عرض التتبع (0s-409.575266ms)
عرض التتبع (411.075559ms-747.252311ms)
عرض التتبع (747.252311ms-1.234968945s)
عرض التتبع (1.234968945s-1.774245108s)
عرض التتبع (1.774245484s-2.111339514)
مشاهدة أثر (2.111339514s-2.674030898s)
عرض أثر (2.674031362s-3.044145798s)
عرض أثر (3.044145798s-3.458795252s)
عرض أثر (3.43953778s-4.075080634s)
عرض أثر (4.075081098s-4.439271287s)
View35 4،439 -4.814869651s)
عرض التتبع (4.814869651s-5.253597835s)

تحليل الغوروتين
ملف تعريف الشبكة ()
ملف تعريف حظر المزامنة ()
ملف تعريف حظر Syscall ()
ملف تعريف زمن الوصول للجدولة ()
المهام المحددة من قبل
المستخدم المناطق المعرفة من قبل المستخدم
الحد الأدنى من استخدام الطفرة

التعقب يقسم وقت التتبع حتى يتمكن المتصفح من التعامل معه.



هناك كمية كبيرة من البيانات هنا ، مما يجعلها غير قابلة للقراءة تقريبًا إذا لا نعرف ما الذي نبحث عنه. دعنا نتركها الآن.

الرابط التالي في الصفحة الرئيسية هو "تحليل الغوروتين" ، والذي يوضح أنواع مختلفة من الغوروتينات العاملة في البرنامج خلال فترة التتبع:

Goroutines:
github.com/miekg/dns.(*Server).serveUDPPacket N=441703
runtime.gcBgMarkWorker N=12
github.com/karlseguin/ccache.(*Cache).worker N=2
main.Start.func1 N=1
runtime.bgsweep N=1
arvancloud/redins/handler.NewHandler.func2 N=1
runtime/trace.Start.func1 N=1
net/http.(*conn).serve N=1
runtime.timerproc N=3
net/http.(*connReader).backgroundRead N=1
N=40

انقر على العنصر الأول مع N = 441703 ، هذا ما نحصل عليه:


تحليل goroutin

هذا مثير للاهتمام للغاية. لا تستغرق معظم العمليات وقتًا تقريبًا لإكمالها ، ويتم قضاء معظم الوقت في كتلة المزامنة. دعونا نلقي نظرة فاحصة على أحدها:


نمط تتبع goroutine

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

runtime.SetBlockProfileRate(1)

الآن يمكننا الحصول على مجموعة مختارة من الأقفال:

$ go tool pprof http://localhost:6060/debug/pprof/block
(pprof) top
Showing nodes accounting for 16.03wks, 99.75% of 16.07wks total
Dropped 87 nodes (cum <= 0.08wks)
Showing top 10 nodes out of 27
     flat  flat%   sum%        cum   cum%
 10.78wks 67.08% 67.08%   10.78wks 67.08%  internal/poll.(*fdMutex).rwlock
  5.25wks 32.67% 99.75%    5.25wks 32.67%  sync.(*Mutex).Lock
        0     0% 99.75%    5.25wks 32.67%  arvancloud/redins/handler.(*DnsRequestHandler).Filter
        0     0% 99.75%    5.25wks 32.68%  arvancloud/redins/handler.(*DnsRequestHandler).FindANAME
        0     0% 99.75%   16.04wks 99.81%  arvancloud/redins/handler.(*DnsRequestHandler).HandleRequest
        0     0% 99.75%   10.78wks 67.08%  arvancloud/redins/handler.(*DnsRequestHandler).Response
        0     0% 99.75%   10.78wks 67.08%  arvancloud/redins/handler.(*RequestContext).Response
        0     0% 99.75%    5.25wks 32.67%  arvancloud/redins/handler.ChooseIp
        0     0% 99.75%   16.04wks 99.81%  github.com/miekg/dns.(*ServeMux).ServeDNS
        0     0% 99.75%   16.04wks 99.81%  github.com/miekg/dns.(*Server).serveDNS
(pprof)

هنا لدينا قفلان مختلفان ( poll.fdMutex و sync.Mutex ) ، مسؤولان عن 100٪ من الأقفال. هذا يؤكد افتراضنا حول تعارض قفل ، والآن نحن بحاجة فقط إلى معرفة مكان حدوث ذلك:

(pprof) svg lock

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


svg-graph لنقطة نهاية الحظر

يمكننا الحصول على نفس النتيجة من نقطة نهاية goroutine:

$ go tool pprof http://localhost:6060/debug/pprof/goroutine

وثم:

(pprof) top
Showing nodes accounting for 294412, 100% of 294424 total
Dropped 84 nodes (cum <= 1472)
Showing top 10 nodes out of 32
     flat  flat%   sum%        cum   cum%
   294404   100%   100%     294404   100%  runtime.gopark
        8 0.0027%   100%     294405   100%  github.com/miekg/dns.(*Server).serveUDPPacket
        0     0%   100%      58257 19.79%  arvancloud/redins/handler.(*DnsRequestHandler).Filter
        0     0%   100%      58259 19.79%  arvancloud/redins/handler.(*DnsRequestHandler).FindANAME
        0     0%   100%     293852 99.81%  arvancloud/redins/handler.(*DnsRequestHandler).HandleRequest
        0     0%   100%     235406 79.95%  arvancloud/redins/handler.(*DnsRequestHandler).Response
        0     0%   100%     235406 79.95%  arvancloud/redins/handler.(*RequestContext).Response
        0     0%   100%      58140 19.75%  arvancloud/redins/handler.ChooseIp
        0     0%   100%     293852 99.81%  github.com/miekg/dns.(*ServeMux).ServeDNS
        0     0%   100%     293900 99.82%  github.com/miekg/dns.(*Server).serveDNS
(pprof)

تقع جميع برامجنا تقريبًا في runtime.gopark ، وهي أداة جدولة الذهاب التي تنام goroutines. سبب شائع جدا لهذا الصراع القفل

(pprof) svg gopark


svg-رسم بياني لـ goroutin لنقطة النهاية

هنا نرى مصدرين للتضارب:

UDPConn.WriteMsg ()


يبدو أن جميع الإجابات تنتهي في نهاية المطاف بالكتابة إلى نفس FD (ومن ثم القفل) ، فهذا أمر منطقي ، نظرًا لأنهم جميعًا لديهم نفس عنوان المصدر.

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

راند ()


يبدو أن هناك قفلًا في وظائف الرياضيات / الراند العادية (المزيد عن هذا هنا ). يمكن إصلاح ذلك بسهولة بمساعدة Rand.New()مولد رقم عشوائي يقوم بإنشاء غلاف غير قابل للحظر.

rg := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))

إنه أفضل قليلاً ، ولكن إنشاء مصدر جديد أمر مكلف في كل مرة. هل من الممكن القيام بعمل أفضل؟ في حالتنا ، نحن حقا لا نحتاج إلى رقم عشوائي. نحن بحاجة فقط إلى توزيع موحد لموازنة الحمل ، وتبين أنه Time.Nanoseconds()قد يناسبنا.

الآن بعد أن أزلنا جميع الأقفال غير الضرورية ، دعنا نلقي نظرة على النتائج:


تحليل goroutin

يبدو أفضل ، ولكن لا يزال ، في معظم الوقت يستغرق قفل المزامنة. دعونا نلقي نظرة على ملف تعريف قفل المزامنة من الصفحة الرئيسية لواجهة مستخدم التتبع:


المخطط الزمني لقفل المزامنة

دعونا نلقي نظرة على وظيفة التعزيز ccacheمن نقطة نهاية القفل pprof:

(pprof) list promote
ROUTINE ======================== github.com/karlseguin/ccache.(*Cache).promote in ...
        0   9.66days (flat, cum) 99.70% of Total
        .          .    155: h.Write([]byte(key))
        .          .    156: return c.buckets[h.Sum32()&c.bucketMask]
        .          .    157:}
        .          .    158:
        .          .    159:func (c *Cache) promote(item *Item) {
        .   9.66days    160: c.promotables <- item
        .          .    161:}
        .          .    162:
        .          .    163:func (c *Cache) worker() {
        .          .    164: defer close(c.donec)
        .          .    165:

ccache.Get()تنتهي جميع المكالمات في قناة واحدة c.promotables. يعد التخزين المؤقت جزءًا مهمًا من خدمتنا ، يجب أن نفكر في خيارات أخرى ؛ في Dgraph لديه مقالة ممتازة عن حالة ذاكرة التخزين المؤقت! اذهب ، لديهم أيضًا وحدة تخزين مؤقت ممتازة تسمى ريستريتو . لسوء الحظ ، لا يدعم ريستريتو حتى الآن الإصدار المستند إلى Ttl ، يمكننا التغلب على هذه المشكلة باستخدام إصدار كبير جدًا MaxCostوالحفاظ على قيمة المهلة في هيكل ذاكرة التخزين المؤقت لدينا (نريد الاحتفاظ بالبيانات القديمة في ذاكرة التخزين المؤقت). دعونا نرى النتيجة عند استخدام ristretto:


تحليل goroutin

عظيم!

تمكنا من تقليل الحد الأقصى لوقت تشغيل gorutin من 5000 مللي ثانية إلى 22 مللي ثانية. ومع ذلك ، يتم تقسيم معظم وقت التنفيذ بين "قفل المزامنة" و "في انتظار المجدول". دعنا نرى ما إذا كان بإمكاننا القيام بشيء حيال هذا:


الجدول الزمني لقفل المزامنة

يمكننا فعل القليل به fdMutex.rwlock، والآن دعنا نركز على آخر: gcMarkDone ، الذي يمثل 53 ٪ من وقت الحظر. هذه الميزة هي جزء من عملية جمع القمامة. غالبًا ما يكون وجودهم في القسم الحاسم علامة على أننا نفرط في تحميل gc.

تحسين التخصيص


عند هذه النقطة ، قد يكون من المفيد أن نرى كيف يعمل جمع القمامة. يستخدم Go منتقي علامات ومسح. يتتبع كل شيء يتم تحديده ، وبمجرد وصوله إلى ضعف الحجم (أو أي قيمة أخرى تم تعيينها لـ GOGC) على حجم الحجم السابق ، يبدأ تنظيف GC. يتم التقييم على ثلاث مراحل:

  • إعداد العلامة (STW)
  • وسم (بالتوازي)
  • نهاية وضع العلامات (STW)

توقف مراحل Stop The World (STW) العملية بأكملها ، على الرغم من أنها عادة ما تكون قصيرة جدًا ، إلا أن الدورات المستمرة يمكن أن تزيد من المدة. هذا لأنه في الوقت الحاضر (go v1.13) يتم استبدال الغوروتينات فقط عند نقاط استدعاء الوظيفة ، وبالتالي ، بالنسبة للدورة المستمرة ، من الممكن أن يكون هناك فترة توقف طويلة بشكل تعسفي ، حيث يتوقع GC إيقاف جميع الغوروتينات.

أثناء وضع العلامات ، يستخدم gc حوالي 25٪ GOMAXPROCS، ولكن يمكن إضافة وظائف المساعد الإضافية بالقوة لتحديد المساعدة ، يحدث هذا عندما يتخطى goroutine سريع الحركة علامة الخلفية لتقليل التأخير الناجم عن gc ، نحتاج إلى تقليل استخدام كومة الذاكرة المؤقتة.

شيئين لملاحظة:

  • عدد عمليات التخصيص أكثر أهمية من الحجم (على سبيل المثال ، تخصيصات ذاكرة 1000 من بنية 20 بايت إنشاء تحميل أكثر على كومة الذاكرة المؤقتة من تخصيص واحد من 20000 بايت) ؛
  • على عكس لغات مثل C / C ++ ، لا تنتهي جميع عمليات تخصيص الذاكرة في كومة الذاكرة المؤقتة. يقرر المترجم go ما إذا كان المتغير سينتقل إلى الكومة أم أنه يمكن وضعه داخل إطار المكدس. على عكس المتغيرات المخصصة في كومة الذاكرة المؤقتة ، لا يتم تحميل المتغيرات المخصصة على المكدس gc.

لمزيد من المعلومات حول نموذج ذاكرة Go وبنية GC ، انظر هذا العرض التقديمي .

لتحسين تخصيص الذاكرة ، نستخدم مجموعة أدوات go:

  • ملف تعريف المعالج للعثور على توزيعات ساخنة ؛
  • ملف تعريف الذاكرة لتتبع عمليات التخصيص ؛
  • التتبع للأنماط ؛ ج
  • تحليل الهروب لمعرفة سبب حدوث التخصيص.

لنبدأ بملف تعريف المعالج:

$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=20
(pprof) top 20 -cum
Showing nodes accounting for 7.27s, 29.10% of 24.98s total
Dropped 315 nodes (cum <= 0.12s)
Showing top 20 nodes out of 201
     flat  flat%   sum%        cum   cum%
        0     0%     0%     16.42s 65.73%  github.com/miekg/dns.(*Server).serveUDPPacket
    0.02s  0.08%  0.08%     16.02s 64.13%  github.com/miekg/dns.(*Server).serveDNS
    0.02s  0.08%  0.16%     13.69s 54.80%  github.com/miekg/dns.(*ServeMux).ServeDNS
    0.01s  0.04%   0.2%     13.48s 53.96%  github.com/miekg/dns.HandlerFunc.ServeDNS
    0.02s  0.08%  0.28%     13.47s 53.92%  main.handleRequest
    0.24s  0.96%  1.24%     12.50s 50.04%  arvancloud/redins/handler.(*DnsRequestHandler).HandleRequest
    0.81s  3.24%  4.48%      6.91s 27.66%  runtime.gentraceback
    3.82s 15.29% 19.78%      5.48s 21.94%  syscall.Syscall
    0.02s  0.08% 19.86%      5.44s 21.78%  arvancloud/redins/handler.(*DnsRequestHandler).Response
    0.06s  0.24% 20.10%      5.25s 21.02%  arvancloud/redins/handler.(*RequestContext).Response
    0.03s  0.12% 20.22%      4.97s 19.90%  arvancloud/redins/handler.(*DnsRequestHandler).FindANAME
    0.56s  2.24% 22.46%      4.92s 19.70%  runtime.mallocgc
    0.07s  0.28% 22.74%      4.90s 19.62%  github.com/miekg/dns.(*response).WriteMsg
    0.04s  0.16% 22.90%      4.40s 17.61%  github.com/miekg/dns.(*response).Write
    0.01s  0.04% 22.94%      4.36s 17.45%  github.com/miekg/dns.WriteToSessionUDP
    1.43s  5.72% 28.66%      4.30s 17.21%  runtime.pcvalue
    0.01s  0.04% 28.70%      4.15s 16.61%  runtime.newstack
    0.06s  0.24% 28.94%      4.09s 16.37%  runtime.copystack
    0.01s  0.04% 28.98%      4.05s 16.21%  net.(*UDPConn).WriteMsgUDP
    0.03s  0.12% 29.10%      4.04s 16.17%  net.(*UDPConn).writeMsg

نحن مهتمون بشكل خاص بالوظائف ذات الصلة ، mallocgcوهذا هو المكان الذي تساعد فيه العلامات

(pprof) svg mallocgc



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

$ go tool pprof -alloc_objects http://localhost:6060/debug/pprof/allocs
(pprof) top -cum
Active filters:
  show=handler
Showing nodes accounting for 58464353, 59.78% of 97803168 total
Dropped 1 node (cum <= 489015)
Showing top 10 nodes out of 19
     flat  flat%   sum%        cum   cum%
 15401215 15.75% 15.75%   70279955 71.86%  arvancloud/redins/handler.(*DnsRequestHandler).HandleRequest
  2392100  2.45% 18.19%   27198697 27.81%  arvancloud/redins/handler.(*DnsRequestHandler).FindANAME
   711174  0.73% 18.92%   14936976 15.27%  arvancloud/redins/handler.(*DnsRequestHandler).Filter
        0     0% 18.92%   14161410 14.48%  arvancloud/redins/handler.(*DnsRequestHandler).Response
 14161410 14.48% 33.40%   14161410 14.48%  arvancloud/redins/handler.(*RequestContext).Response
  7284487  7.45% 40.85%   11118401 11.37%  arvancloud/redins/handler.NewRequestContext
 10439697 10.67% 51.52%   10439697 10.67%  arvancloud/redins/handler.reverseZone
        0     0% 51.52%   10371430 10.60%  arvancloud/redins/handler.(*DnsRequestHandler).FindZone
  2680723  2.74% 54.26%    8022046  8.20%  arvancloud/redins/handler.(*GeoIp).GetSameCountry
  5393547  5.51% 59.78%    5393547  5.51%  arvancloud/redins/handler.(*DnsRequestHandler).LoadLocation

من الآن فصاعدًا ، يمكننا استخدام قائمة لكل وظيفة ومعرفة ما إذا كان بإمكاننا تقليل تخصيص الذاكرة. دعونا نتحقق:

وظائف شبيهة printf

(pprof) list handleRequest
Total: 97803168
ROUTINE ======================== main.handleRequest in /home/arash/go/src/arvancloud/redins/redins.go
  2555943   83954299 (flat, cum) 85.84% of Total
        .          .     35: l          *handler.RateLimiter
        .          .     36: configFile string
        .          .     37:)
        .          .     38:
        .          .     39:func handleRequest(w dns.ResponseWriter, r *dns.Msg) {
        .   11118401     40: context := handler.NewRequestContext(w, r)
  2555943    2555943     41: logger.Default.Debugf("handle request: [%d] %s %s", r.Id, context.RawName(), context.Type())
        .          .     42:
        .          .     43: if l.CanHandle(context.IP()) {
        .   70279955     44:  h.HandleRequest(context)
        .          .     45: } else {
        .          .     46:  context.Response(dns.RcodeRefused)
        .          .     47: }
        .          .     48:}
        .          .     49:

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

أداة Go Escape Analysis Tool هي في الواقع علامة مترجم

$ go build -gcflags '-m'

يمكنك إضافة -m أخرى لمزيد من المعلومات.

$ go build -gcflags '-m '

للحصول على واجهة أكثر ملاءمة ، استخدم ملفًا توضيحيًا للعرض .

$ go build -gcflags '-m'
.
.
.
../redins.go:39:20: leaking param: w
./redins.go:39:42: leaking param: r
./redins.go:41:55: r.MsgHdr.Id escapes to heap
./redins.go:41:75: context.RawName() escapes to heap
./redins.go:41:91: context.Request.Type() escapes to heap
./redins.go:41:23: handleRequest []interface {} literal does not escape
./redins.go:219:17: leaking param: path
.
.
.

هنا Debugfتذهب جميع المعلمات إلى كومة الذاكرة المؤقتة. هذا يرجع إلى طريقة تحديد Debugf:

func (l *EventLogger) Debugf(format string, args ...interface{})

يتم argsتحويل جميع المعلمات إلى نوع interface{}يتم تفريغه دائمًا في كومة الذاكرة المؤقتة. يمكننا إما حذف سجلات التصحيح ، أو استخدام مكتبة من السجلات بتوزيع صفري ، على سبيل المثال ، zerolog .

لمزيد من المعلومات حول تحليل الهروب ، راجع "كفاءة التخصيص في golang" .

تعامل مع الأوتار

(pprof) list reverseZone
Total: 100817064
ROUTINE ======================== arvancloud/redins/handler.reverseZone in /home/arash/go/src/arvancloud/redins/handler/handler.go
  6127746   10379086 (flat, cum) 10.29% of Total
        .          .    396:  logger.Default.Warning("log queue is full")
        .          .    397: }
        .          .    398:}
        .          .    399:
        .          .    400:func reverseZone(zone string) string {
        .    4251340    401: x := strings.Split(zone, ".")
        .          .    402: var y string
        .          .    403: for i := len(x) - 1; i >= 0; i-- {
  6127746    6127746    404:  y += x[i] + "."
        .          .    405: }
        .          .    406: return y
        .          .    407:}
        .          .    408:
        .          .    409:func (h *DnsRequestHandler) LoadZones() {
(pprof)

نظرًا لأن الخط في Go غير قابل للتغيير ، فإن إنشاء خط مؤقت يسبب تخصيص الذاكرة. بدءًا من Go 1.10 ، يمكنك استخدامه strings.Builderلإنشاء سلسلة.

(pprof) list reverseZone
Total: 93437002
ROUTINE ======================== arvancloud/redins/handler.reverseZone in /home/arash/go/src/arvancloud/redins/handler/handler.go
        0    7580611 (flat, cum)  8.11% of Total
        .          .    396:  logger.Default.Warning("log queue is full")
        .          .    397: }
        .          .    398:}
        .          .    399:
        .          .    400:func reverseZone(zone string) string {
        .    3681140    401: x := strings.Split(zone, ".")
        .          .    402: var sb strings.Builder
        .    3899471    403: sb.Grow(len(zone)+1)
        .          .    404: for i := len(x) - 1; i >= 0; i-- {
        .          .    405:  sb.WriteString(x[i])
        .          .    406:  sb.WriteByte('.')
        .          .    407: }
        .          .    408: return sb.String()

نظرًا لأننا لا نهتم بقيمة الخط المقلوب ، يمكننا إزالته Split()ببساطة عن طريق قلب الخط بأكمله.

(pprof) list reverseZone
Total: 89094296
ROUTINE ======================== arvancloud/redins/handler.reverseZone in /home/arash/go/src/arvancloud/redins/handler/handler.go
  3801168    3801168 (flat, cum)  4.27% of Total
        .          .    400:func reverseZone(zone string) []byte {
        .          .    401: runes := []rune("." + zone)
        .          .    402: for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        .          .    403:  runes[i], runes[j] = runes[j], runes[i]
        .          .    404: }
  3801168    3801168    405: return []byte(string(runes))
        .          .    406:}
        .          .    407:
        .          .    408:func (h *DnsRequestHandler) LoadZones() {
        .          .    409: h.LastZoneUpdate = time.Now()
        .          .    410: zones, err := h.Redis.SMembers("redins:zones")

اقرأ المزيد عن العمل مع السلاسل هنا .

المزامنة


(pprof) list GetASN
Total: 69005282
ROUTINE ======================== arvancloud/redins/handler.(*GeoIp).GetASN in /home/arash/go/src/arvancloud/redins/handler/geoip.go
  1146897    1146897 (flat, cum)  1.66% of Total
        .          .    231:func (g *GeoIp) GetASN(ip net.IP) (uint, error) {
  1146897    1146897    232: var record struct {
        .          .    233:  AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
        .          .    234: }
        .          .    235: err := g.ASNDB.Lookup(ip, &record)
        .          .    236: if err != nil {
        .          .    237:  logger.Default.Errorf("lookup failed : %s", err)
(pprof) list GetGeoLocation
Total: 69005282
ROUTINE ======================== arvancloud/redins/handler.(*GeoIp).GetGeoLocation in /home/arash/go/src/arvancloud/redins/handler/geoip.go
  1376298    3604572 (flat, cum)  5.22% of Total
        .          .    207:
        .          .    208:func (g *GeoIp) GetGeoLocation(ip net.IP) (latitude float64, longitude float64, country string, err error) {
        .          .    209: if !g.Enable || g.CountryDB == nil {
        .          .    210:  return
        .          .    211: }
  1376298    1376298    212: var record struct {
        .          .    213:  Location struct {
        .          .    214:   Latitude        float64 `maxminddb:"latitude"`
        .          .    215:   LongitudeOffset uintptr `maxminddb:"longitude"`
        .          .    216:  } `maxminddb:"location"`
        .          .    217:  Country struct {
        .          .    218:   ISOCode string `maxminddb:"iso_code"`
        .          .    219:  } `maxminddb:"country"`
        .          .    220: }
        .          .    221: // logger.Default.Debugf("ip : %s", ip)
        .          .    222: if err := g.CountryDB.Lookup(ip, &record); err != nil {
        .          .    223:  logger.Default.Errorf("lookup failed : %s", err)
        .          .    224:  return 0, 0, "", err
        .          .    225: }
        .    2228274    226: _ = g.CountryDB.Decode(record.Location.LongitudeOffset, &longitude)
        .          .    227: // logger.Default.Debug("lat = ", record.Location.Latitude, " lang = ", longitude, " country = ", record.Country.ISOCode)
        .          .    228: return record.Location.Latitude, longitude, record.Country.ISOCode, nil
        .          .    229:}
        .          .    230:
        .          .    231:func (g *GeoIp) GetASN(ip net.IP) (uint, error) {

نستخدم وظائف maxmiddb للحصول على بيانات تحديد الموقع الجغرافي. يتم أخذ هذه الوظائف interface{}كمعلمات ، والتي ، كما رأينا سابقًا ، يمكن أن تسبب براعم كومة الذاكرة المؤقتة.

يمكننا استخدام sync.Poolالتخزين المؤقت للعناصر المحددة ولكن غير المستخدمة لإعادة استخدامها لاحقًا.

type MMDBGeoLocation struct {
  Coordinations struct {
     Latitude        float64 `maxminddb:"latitude"`
     Longitude       float64
     LongitudeOffset uintptr `maxminddb:"longitude"`
  } `maxminddb:"location"`
  Country struct {
     ISOCode string `maxminddb:"iso_code"`
  } `maxminddb:"country"`
}

type MMDBASN struct {
  AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
}
func (g *GeoIp) GetGeoLocation(ip net.IP) (*MMDBGeoLocation, error) {
  if !g.Enable || g.CountryDB == nil {
     return nil, EMMDBNotAvailable
  }
  var record *MMDBGeoLocation = g.LocationPool.Get().(*MMDBGeoLocation)
  logger.Default.Debugf("ip : %s", ip)
  if err := g.CountryDB.Lookup(ip, &record); err != nil {
     logger.Default.Errorf("lookup failed : %s", err)
     return nil, err
  }
  _ = g.CountryDB.Decode(record.Coordinations.LongitudeOffset, &record.Coordinations.Longitude)
  logger.Default.Debug("lat = ", record.Coordinations.Latitude, " lang = ", record.Coordinations.Longitude, " country = ", record.Country.ISOCode)
  return record, nil
}

func (g *GeoIp) GetASN(ip net.IP) (uint, error) {
  var record *MMDBASN = g.AsnPool.Get().(*MMDBASN)
  err := g.ASNDB.Lookup(ip, record)
  if err != nil {
     logger.Default.Errorf("lookup failed : %s", err)
     return 0, err
  }
  logger.Default.Debug("asn = ", record.AutonomousSystemNumber)
  return record.AutonomousSystemNumber, nil
}

المزيد عن sync.Pool هنا .

هناك العديد من التحسينات الممكنة الأخرى ، ولكن في الوقت الحالي ، يبدو أننا قمنا بما يكفي بالفعل. لمزيد من المعلومات حول تقنيات تحسين الذاكرة ، يمكنك قراءة كفاءة التخصيص في خدمات Go عالية الأداء .

النتائج


من أجل تصور نتائج تحسين الذاكرة ، نستخدم رسمًا توضيحيًا go tool traceبعنوان "الحد الأدنى من استخدام Mutator" ، هنا يعني Mutator لا gc.

قبل التحسين:



هنا لدينا نافذة حوالي 500 مللي ثانية بدون استخدام فعال (تستهلك gc جميع الموارد) ، ولن نحصل أبدًا على استخدام فعال بنسبة 80٪ على المدى الطويل. نريد أن تكون نافذة الاستخدام الفعال الصفري صغيرة بقدر الإمكان ، وبأسرع ما يمكن لتحقيق الاستخدام بنسبة 100٪ ، شيء من هذا القبيل:

بعد التحسين:



استنتاج


باستخدام أدوات go ، تمكنا من تحسين خدمتنا للتعامل مع عدد كبير من الطلبات والاستخدام الأفضل لموارد النظام.

يمكنك رؤية شفرة المصدر الخاصة بنا على GitHub . هنا نسخة غير محسنة وهنا نسخة محسنة.

سيكون مفيدًا أيضًا



هذا كل شئ. نراكم في الدورة !

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


All Articles