تحسبًا لبدء تيار جديد في دورة "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:
يعمل ملف تعريف المعالج بشكل افتراضي لمدة 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:
بشكل افتراضي ، يعرض عمر الذاكرة المخصصة. ويمكننا أن نرى عدد من الكائنات المحددة باستخدام -alloc_objects
خيارات أخرى مفيدة: -iuse_objects
و -inuse_space
لفحص الحية الذاكرة.عادة ، إذا كنت تريد تقليل استهلاك الذاكرة ، فأنت بحاجة إلى النظر -inuse_space
، ولكن إذا كنت تريد تقليل وقت الاستجابة ، فابحث عن وقت -alloc_objects
تشغيل / تحميل كافٍ.تحديد عنق الزجاجة
من المهم أولاً تحديد نوع عنق الزجاجة (المعالج ، I / O ، الذاكرة) الذي نتعامل معه. بالإضافة إلى المحللون ، هناك نوع آخر من الأدوات.go tool trace
يمكن أن تظهر ما تفعله الغوروتين بالتفصيل. لجمع مثال التتبع ، نحتاج إلى إرسال طلب http إلى نقطة نهاية التتبع:$ curl http:
يمكن عرض الملف الذي تم إنشاؤه باستخدام أداة التتبع:$ 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:
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:
(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:
وثم:(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:
(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:
(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:
. . 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:
. . 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 . هنا نسخة غير محسنة وهنا نسخة محسنة.سيكون مفيدًا أيضًا
هذا كل شئ. نراكم في الدورة !