முக்கிய உள்ளடக்கத்திற்கு செல்க
OpenAI

கோர் டம்ப் தொற்றியல்: 18 ஆண்டு பிழையைச் சரிசெய்தல்

எங்கள் தரவு உள்கட்டமைப்பில் சிக்கலான செயலிழப்புகளைச் சரிசெய்ய, தொகுதி அளவிலான பகுப்பாய்வைப் பயன்படுத்துதல்.

ஏற்றுகிறது…

OpenAI-இன் மாடல்கள் மற்றும் ஏஜென்ட்கள், இன்ஃபரன்ஸ் நேரத்தில்—அவை உங்கள் கேள்வியைப் பற்றி சிந்திக்கும்போது—தொடர்புடைய தரவைத் தேட, அளவிடக்கூடிய தரவு உள்கட்டமைப்பை அதிகமாக நம்புகின்றன. இந்தச் சேவைகளில் சில C++-இல் எழுதப்பட்டவை; செயல்திறனை உயர்த்தவும் நினைவகப் பயன்பாட்டைக் குறைக்கவும் சிஸ்டத்தின் குறைந்த-நிலை கட்டுப்பாடு உதவுகிறது. நாம் விரிவுபடுத்தும்போது இந்த திறன் நன்மைகள் முக்கியம்; ஆனால் C++-இல் நினைவக பாதுகாப்பு இல்லாததால், பிழைகள் தவறான அல்லது இல்லாத நினைவக முகவரிகளில் எழுதுவதன் மூலம் செயலிழப்பை ஏற்படுத்தலாம்.

சில மாதங்களுக்கு முன், ChatGPT தரவு உள்கட்டமைப்பில் தனிப்பயன் பகுதியாக இருக்கும் Rockset சேவைக்குள் சில செயலிழப்புகளைக் கண்டோம்; அது பல தரவு செருகுநிரல்களுக்கும் உரையாடல் தேடலுக்கும் முக்கியம். ஒவ்வொரு செயலிழப்பிலும், ஒரு சாதாரண C++ செயல்பாடு முடிந்து போலி முகவரியை ரிட்டர்ன் செய்வது போல இருந்தது; கட்டளைச் சுட்டி இனி கோடிங்கைச் சுட்டிக்காட்டாததால் கெர்னெல் நிரலை நிறுத்தியது. சில நேரங்களில் ஸ்டாக் ஃபிரேமில் உள்ள ரிட்டர்ன் முகவரி ஸ்லாட் NULL-ஆக இருந்தது. சில நேரங்களில் ஸ்டாக் சுட்டியின் CPU பதிவேடு தானே 8 பைட்டுகள் விலகியிருந்தது; சாதாரண இயக்கத்தின் நடுவில் %rsp எப்படியோ குறைக்கப்பட்டதுபோல். இரண்டு சூழலிலும் ரிட்டர்ன் செய்யும் நேரத்தில்தான் செயலிழப்பு ஏற்பட்டது.

இவை பயன்பாட்டுக் கோடிங்கிற்கான சாதாரண தோல்வி முறைகள் அல்ல. சேமிக்கப்பட்ட ரிட்டர்ன் முகவரியை மட்டும் தாக்கும் தவறான எழுதுதல் சாத்தியம் தான், ஆனால் மிக அரிது. இன்லைன் அசெம்பிளி, setcontext அல்லது longjmp இல்லாமல் %rsp-ஐ 8-ஆல் தவறாகச் சீரமைக்கும் பிழை இன்னும் விசித்திரம்; ஏனெனில் தொகுக்கப்பட்ட கோடிங் அந்தப் பதிவேட்டைச் செயல்பாட்டின் முன்னுரை மற்றும் முடிவுரையில் மட்டுமே நேரடியாக மாற்றும். நாங்கள் (அல்லது ChatGPT) நினைத்த ஒவ்வொரு கருதுகோளுக்கும் எதிராக வலுவான ஆதாரம் இருந்ததால், பிழை சாத்தியமற்றது போலத் தோன்றியது.

ஒரே பிரச்சினை என்று நினைத்தது, இறுதியில் ஒரே நேரத்தில் தற்செயலாக கண்டறியப்பட்ட தொடர்பில்லாத இரண்டு பிழைகளாக தெரிய வந்தது. முதலில், ஓர் Azure ஹோஸ்ட்டில் அமைதியான வன்பொருள் சிதைவு; CPU-ஆனது கணக்கை சரியாக செய்யவில்லை. இரண்டாவது, GNU libunwind-இல் 18 ஆண்டுகள் பழைய ரேஸ் கண்டிஷன்; பரவலாக பயன்படுத்தப்படும் திறந்த மூல நூலகத்தில் கவனிக்கப்படாத பிழை.

ஒரு தொற்றியல் நிபுணரைப் போல சிந்தித்து, செயலிழப்புகளின் முழு தொகுதிக்கான உயர்தர தரவுத் தொகுப்பை உருவாக்கி, புரியாத செயலிழப்புகளை எவ்வாறு கண்டறிந்து சரிசெய்தோம் என்பதுதான் இந்தப் பதிவு.

முதல் பிழை திருத்த முயற்சி: சில கோர் டம்ப்களைக் கவனமாக ஆய்வு செய்தல்

முதலில், Rockset பற்றி மேலும் பார்ப்போம். இது தேடல் மற்றும் நிகழ்நேரப் பகுப்பாய்வுக்கு கிளவுட் இயல்புத் தரவு முறைமை; ஒத்திசைவு இணைப்பான்கள் போன்ற OpenAI-இன் பல உள்புறப் பயன்பாட்டு வழக்குகளுக்கு இதைப் பயன்படுத்துகிறோம் (Rockset 2024-இல் OpenAI-ஆல் வாங்கப்பட்டது). வர்க்ஸ்பேஸின் அறிவுத் தளத்திற்கு புதுப்பித்த இண்டெக்ஸை வைத்திருக்க ஸ்ட்ரீமிங் அறிவிப்புகள் பயன்படுகின்றன; அதனால் ChatGPT கேள்விகளுக்கு பதிலளிக்கும்போது அல்லது செயல்கள் செய்யும்போது தொடர்புடைய தகவலைத் தேட முடியும்.

Rockset-இன் செயலாக்க அடுக்கு, C++-இல் எழுதப்பட்டுள்ளது. C++ மொழியானது CPU-க்கு குறைந்த-நிலை அணுகலை வழங்குகிறது; இது செயல்திறன் மற்றும் திறனுக்கு நல்லது, ஆனால் பயன்பாட்டுப் பிழைகள், செல்லாத நினைவக அணுகல்கள் மற்றும் செக்ஃபால்ட்களுக்கு வழிவகுக்கலாம். இவற்றைத் தேட உதவ, செயலிழப்பு ஏற்படும் போது ஸ்டாக் டிரேஸைப் பதிவு செய்ய ஃபாலியின் ஃபேட்டல் சிக்னல் ஹேண்ட்லரைப் பயன்படுத்துகிறோம்; தொடர்புடைய கோர் டம்ப்களைப் பின்னர் ஆய்வுக்காக Azure blob storage-க்கு பதிவேற்றுகிறோம். Rockset-இன் அனைத்து வினவல் செயலாக்க லீஃப்களும் நகலெடுக்கப்பட்டவை; இது செயலிழப்பின் கிளையண்ட் தாக்கத்தை குறைக்கிறது. ஆனால் ஒவ்வொரு செக்ஃபால்ட்டும், எங்கள் நம்பகத்தன்மை மற்றும் தர இலக்குகளை அடைய சரிசெய்ய வேண்டிய பிழையைக் குறிக்கும்.

எங்கள் தொடக்க அணுகுமுறை, இவற்றை வழக்கமான பிழை திருத்த பிரச்சினையாகக் காண்பது: சில கோர் டம்ப்களை நெருக்கமாகப் பார்ப்பது, கருதுகோள்களை அமைப்பது, ஒவ்வொன்றாக நிராகரிப்பது.

DocumentTree::updateDocument என்ற முறையில் பெரும்பாலான செயலிழப்புகள் நடந்தன. இந்தச் செயலிழப்புகளில் updateDocument தெரியாத செயல்பாடு X-ஐ அழைத்தது, X இயங்கும்போது ஸ்டாக் சிதைவடைந்தது, பின்னர் X இயக்கக்கூடிய கோடிங் அல்லாத முகவரிக்கு ரிட்டர்ன் செய்தது போலத் தோன்றியது. சில சமயங்களில் X-இன் அப்போதுதான்-அகற்றப்பட்ட ஃபிரேம் செல்லுபடியாகத் தோன்றியது; ஆனால் அதன் சேமித்த ரிட்டர்ன் முகவரி மட்டும் NULL-ஆக இருந்தது. மற்ற சமயங்களில் ஸ்டாக் சுட்டி தானே தவறாகத் தோன்றியது; ஆனால் அடுத்த செல்லுபடியான ஃபிரேம் இன்னும் updateDocument போலவே இருந்தது.

ஸ்டாக் எப்போது சிதைகிறது என்பதை நாங்கள் அறியவில்லை; அதனால் தேட வேண்டிய பரப்பு மிகப் பெரியதாக இருந்தது. updateDocument மிகப் பெரிய முறை; அது அதிக இன்லைனிங்கிற்கு உட்படும். ஆகவே X-க்கான சாத்தியக்கூறுகளின் எண்ணிக்கை மலைப்பூட்டியது.

இது எங்கள் C++ கோடிங்கில் பிழை ஆக இருந்ததா? கம்பைலர் அல்லது இணைப்புச் சிக்கலா? எங்கள் இயக்க நேர நூலகங்களில் ஒன்றில் பிரச்சினையா? சிக்னல் டெலிவரி அல்லது சூழல் மாறுதல் பற்றிய Linux கெர்னெல் பிழையாக இருந்ததா? இன்னும் அரிதான ஏதாவது விஷயமா? இது தற்செயலான எழுத்துப்பிழை என்றால், எங்கள் ASAN ஸ்டேஜிங் சூழலுக்கு அதை ஏன் கண்டுபிடிக்கவில்லை?

எங்கள் பயன்பாட்டு நிலை பதிவுகளைப் பயன்படுத்தி இந்த பிரச்சினையின் எல்லா நிகழ்வுகளையும் கண்டறிய முயன்றோம்; ஆனால் பதிவுகளை மட்டும் கொண்டு ஸ்டாக் சிதைவுப் பிழைகளை வகைப்படுத்துவது கடினம், ஏனெனில் பதிவு செய்யப்பட்ட ஸ்டாக் டிரேஸ்கள் தாமே சிதைவடைந்தவை அல்லது விடுபட்டவையாக இருக்கும். தவறான நேர்மறைகள், தவறான எதிர்மறைகள் இரண்டும் இல்லாத பதிவு வினவலை உருவாக்க முடியவில்லை. மேலும் கோர்களைக் கைமுறையாக ஆய்ந்து சில கூடுதல் எடுத்துக்காட்டுகளைக் கண்டோம்; ஆனால் அது நம்பகமான தரவுத் தொகுப்பை அளிக்க அளவுக்கு அதிக உழைப்பு தேவையாக இருந்தது.

விசாரணையின் இந்த கட்டத்தில், பல பிராந்தியங்கள் மற்றும் பல வன்பொருள் வகைகளில் செயலிழப்புகளைப் பார்த்ததால் வன்பொருள் பிழையைத் தவறாக நிராகரித்தோம்; ஆகவே மென்பொருள் சார்ந்த காரணங்களை மட்டுமே தேடிக் கொண்டிருந்தோம். சில நாட்கள், ஸ்டாக் மற்றும் பதிவேடு உள்ளடக்கங்களைப் பயன்படுத்தி செயலிழப்பிற்கு முந்தைய வரலாற்றை மீட்டமைத்து, தவறாகச் சீரமைக்கப்பட்ட-%rsp செயலிழப்பு ஒன்றில் ஆழமாக மூழ்கினோம். இதில் சில சாத்தியமான தடயங்கள் கிடைத்தன; ஆனால் எல்லா பிழைகளுக்கும் ஒரே காரணம் என்ற தொடக்க முடிவை விடாததால், நாம் முன்னேற முடியவில்லை.

ஸ்டாக்கிலிருந்து கிடைத்த தடயங்கள்

எங்கள் விசாரணையின் திருப்புமுனைக்கு செல்லும் முன், கோர் கோப்புகளிலிருந்து எவ்வகை தகவலை எடுத்தோம் என்பதை விளக்குவது முக்கியம்.

-fno-omit-frame-pointer உடன் Rockset தொகுக்கப்படுகிறது; ஆகவே செயல்படும் ஸ்டாக் ஃபிரேம் எப்போதும் %rbp வழியாக கிடைக்கும், ஃபிரேம் சுட்டிகளின் இணைக்கப்பட்ட பட்டியலாக அழைப்பாளர்கள் அமைகின்றனர்.

Linux x86_64-இல், AMD64 முறைமை V ABI-ஆனது %rsp-க்கு கீழே 128 பைட்டுகளை சிவப்பு மண்டலமாக ஒதுக்குகிறது. அந்தப் பகுதியானது பயனர்வெளி கோடிங்கிற்கு கிடைக்கிறது; முக்கியமாக, சிக்னல் அனுப்பும்போது கெர்னெல் அதைச் சிதைக்க மாட்டேன் என்று ABI ஒப்பந்தத்தின் பகுதியாக உறுதி அளிக்கிறது.

ரிட்டர்ன் செய்வதற்குப் பிந்தைய செயலிழப்பைச் சரிசெய்ய சிவப்பு மண்டலம் மையமாக இருந்தது; ஏனெனில் அது ரிட்டர்ன் செய்வதற்கு முன் இருந்த சில தகவலைத் தக்கவைக்கிறது. SIGSEGV தூண்டப்படும் போது, ஃபாலியின் ஃபேட்டல் சிக்னல் ஹேண்ட்லரான த்ரெட்டின் ஸ்டாக்கில் இயங்குகிறது. இனி செயலில் அல்லாத ஸ்டாக் ஃபிரேம்கள் (அவற்றின் செயல்பாடு ரிட்டர்ன் செய்யப்பட்டதால்) கடைசி 128 பைட்டுகள் தவிர சிக்னல் ஹேண்ட்லரால் சிதைக்கப்படும். அதனால்தான் “X-இன் அப்போதுதான்-அகற்றப்பட்ட ஸ்டாக் ஃபிரேம் செல்லுபடியாக இருந்தது, NULL ரிட்டர்ன் முகவரி மட்டும் விதிவிலக்கு” என்று சொல்ல முடிகிறது. சிவப்பு மண்டலம் சில செயலில் இல்லாத ஃபிரேம்களை அல்லது சில நேரங்களில் ஒன்றின் கடைசி பகுதியை மட்டும், தக்கவைக்கிறது.

ரிட்டர்ன் முகவரிகளை மேலெழுதி செயலிழப்புகளை ஏற்படுத்தக்கூடிய சிதைந்த ஸ்டாக் ஃபிரேம்களைக் காட்டும் ஸ்டாக் வரைபடம்.

சம்பந்தப்பட்ட எல்லா செயல்பாடுகளும் மிகச் சிறியதாக இருந்த ஒரு தவறாகச் சீரமைக்கப்பட்ட ஸ்டாக் செயலிழப்பைக் கண்டோம். இதனால், ஒப்பீட்டளவில் எளிய செயல்பாடு இயங்கும்போது %rsp தவறாகச் சீரமைக்கப்பட்டதும், அதன் பிறகும் பல அழைப்புகள் வெற்றியடைந்ததும் தெரிந்தது. செயலில் உள்ள செயல்பாடு இறுதியில் ரிட்டர்ன் செய்ய முயன்றபோதுதான் நிரல் செயலிழப்பு ஏற்பட்டது. அந்த கோடிங் பாதைகள் எதுவும் விதிவிலக்குகள், இன்லைன் அசெம்பிளி, setcontext அல்லது longjmp பயன்படுத்தவில்லை; ஆகவே கோர் கூறியபடி ஸ்டாக் சுட்டி உண்மையில் மாறியிருந்தால், பயனர்வெளி கோடிங்கில் நியாயமான பிழை எதுவும் இதை விளக்கவில்லை.

அது எங்களை கெர்னெல் நோக்கித் தள்ளியது.

பெரும்பாலான நிரல்களை விட Rockset சிக்னல்களை அதிக தீவிரமாகப் பயன்படுத்துகிறது. வினவல் செயலாக்கமானது தரவை பரிமாறும் பல இலகுவான பணிகளாகப் பிரிக்கப்படுகிறது. உயர்-QPS பணிச்சுமைகளைத் திறம்பட கையாள இது முக்கியம்; ஆனால் பல வினவல்களின் வேலை ஒரே த்ரெட் தொகுப்பில் மல்ட்டிபிளெக்ஸ் செய்யப்படுவதால் ஒவ்வொரு வினவலுக்கான CPU கணக்கீட்டைச் சிரமமாக்குகிறது.

எங்கள் தீர்வு coarse_thread_cputime_clock என்று அழைக்கப்படுவது; இது ஒவ்வொரு பணி எல்லையிலும் மாதிரியாக்கச் செய்யும் அளவுக்கு மலிவாக clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...)-ஐ அணுகுமுறைப்படுத்துகிறது. CPU நேரம் சேர்வது உட்பட கால ஓட்டத்தின் பல கருத்துகளின் அடிப்படையில் குறிப்பிட்ட கால இடைவெளியில் சிக்னலை அனுப்புவதற்கு timer_create API பயன்படலாம். சில மில்லி வினாடி CPU நேரத்திற்கு ஒருமுறை சிக்னல் (SIGUSR2) அனுப்புமாறு திட்டமிடுகிறோம்; அப்போது சிக்னல் ஹேண்ட்லர் ஒரு த்ரெட் லோக்கல் மதிப்பைப் புதுப்பிக்கிறது. பல பணிகள் இயங்கும்போது கோர்ஸ் கிளாக் முன்னேறுவதைக் காணாவிட்டாலும், எல்லா கால மாற்றங்களையும் கூட்டினால் வினவலுக்கான உண்மையான CPU நேரத்தின் சார்பற்ற மதிப்பீடு கிடைக்கும்.

நாங்கள் சிக்னல்களை இவ்வளவு அடிக்கடி அனுப்புவதால், சூழல் மாறுதல் அல்லது சிக்னல் அனுப்புதல் பற்றிய அரிதான கெர்னெல் பிழை சாத்தியமாகத் தோன்றியது. பிழை அறிக்கைகள், கெர்னெல் மூல கோடிங் மற்றும் Azure-சார்ந்த கெர்னெல் பேட்ச்கள் ஆகியவற்றைப் படிக்க நேரம் செலவிட்டோம். அழுத்தச் சோதனைகளை முயன்றோம். தொடர்புடையதாகத் தோன்றிய எதையும் கண்டுபிடிக்க முடியவில்லை.

அந்த நேரத்தில் பின்னடங்கி வேறு அணுகுமுறையை முயல முடிவு செய்தோம்.

மருத்துவரா, தொற்றியல் நிபுணரா?

இத்தகைய பிரச்சினையைச் சரிசெய்ய இரண்டு பரந்த வழிகள் உள்ளன.

ஒன்று, ஒரு வகை மருத்துவரைப் போல நடப்பது: ஒரே நோயாளியில் கவனம் செலுத்தி, பல சோதனைகள் செய்து, விரிவான ஆதாரத்தில் இருந்து ஒரு நோயைக் கண்டறிய முயல்வது.

மற்றொன்று, தொற்றியல் நிபுணரைப் போல நடப்பது: முழு தொகுதியையும் பார்த்து, ஒரு தனி நிகழ்வு காட்ட முடியாத வடிவங்கள் உள்ளனவா என்று கேட்பது. பிழை ஒரு குறிப்பிட்ட வெளியீட்டில் தொடங்கியதா? அது ஒரே வன்பொருள் SKU (குறிப்பிட்ட CPU மற்றும் சேவையக மாடல்), ஒரு பகுதி அல்லது ஒரு கெர்னெல் பதிப்பு உடன் தொடர்புடையதா? ஒரே நோய்க்குறி போலத் தோன்றுவதற்குள் பல தனி குழுக்கள் மறைந்துள்ளனவா?

நாம் பெரும்பாலும் மருத்துவர் நிலையில் இருந்தோம். உயர்தர தொகுதி தரவைச் சேகரிக்க வேண்டும் என்று முடிவு செய்ததே முக்கிய மாற்றம்.

தரவைச் சுத்தப்படுத்துதல்

பிரச்சினையின் எல்லா நிகழ்வுகளையும் தானாக கண்டுபிடிக்க எங்கள் முந்தைய முயற்சிகள் தோல்வியடைந்தன; ஏனெனில் பதிவுகள் மீது உரைத் தேடல்களைப் பயன்படுத்த முயன்றோம். கோர் டம்ப்களிலேயே அதிக தகவல் இருந்தது; ஆனால் அவற்றை கைமுறையாகப் பார்ப்பது பெரிய அளவில் ஆகவில்லை. கோர் டம்ப்களைத் தானாகப் பகுப்பாய்வு செய்யக்கூடிய செயல்முறை ஒன்றை உருவாக்க முயற்சியை முதலீடு செய்ய முடிவு செய்தோம்.

ஒவ்வொரு கோர் கோப்பின் முன்னொட்டைப் பதிவிறக்கம் செய்து, பதிவேடுகளை எடுத்து, பதிவுகள் மூலம் அறியப்பட்ட தவறான நேர்மறைகளை வடிகட்டி, செயலிழப்பை பூஜ்ஜியத்திற்கு ரிட்டர்ன் செய்தல், தவறாகச் சீரமைக்கப்பட்ட-ஸ்டாக் அல்லது பிற என்று தானாக வகைப்படுத்தும் ஸ்கிரிப்ட்டை ChatGPT‑இடம் எழுத வைத்தோம். பின்னர் அந்த ஸ்கிரிப்ட்டை முந்தைய ஆண்டின் ஒவ்வொரு தயாரிப்பு Rockset கோர் டம்ப் மீதும் இணையாக இயக்கினோம்.

இதுவே திருப்புமுனையாக இருந்தது.

சுத்தமான தரவுத் தொகுப்பு கிடைத்தவுடன், தொடர்புகள் உடனே தென்பட்டன. ஒரே விசித்திரமான பிழை என நினைத்தது உண்மையில் இரண்டு தனி செயலிழப்புத் தொகுதிகளாக இருந்தன.

பூஜ்ஜியத்திற்கு ரிட்டர்ன் செய்தல் கோர்கள், பல கிளஸ்டர்கள் மற்றும் புவியியல் பகுதிகளில் பரவியிருந்தன. அவற்றின் நிகழ்வெண் சமீபத்தில் அதிகரித்திருந்தது; ஆனால் தெளிவான தொடக்கத் தேதி அல்லது சுத்தமான உள்கட்டமைப்பு எல்லை இல்லை.

தவறாகச் சீரமைக்கப்பட்ட-ஸ்டாக் செயலிழப்புகள் முற்றிலும் வேறுபட்டதாகத் தோன்றின. அவை அனைத்தும் ஒரே பகுதியிலிருந்து வந்தவை, தெளிவான தொடக்கத் தேதி இருந்தது, நீண்ட நேரம் ஓடியிருந்த நோட்களில் ஒருபோதும் நடக்கவில்லை. பல Azure VMகள் (கிளவுடில் ஹோஸ்ட் செய்யப்பட்ட மெய்நிகர் இயந்திரங்கள்) சம்பந்தப்பட்டிருந்தாலும், எந்த VM அதில் வந்து சேருமானாலும் பிரச்சினை தரும் மோசமான வன்பொருள் கொண்ட ஒரு இயல்நிலை இயந்திரம் போல வடிவம் இருந்தது.

காலப்போக்கில் கிளஸ்டர் வாரியாக செயலிழப்பு விகிதங்களைக் காட்டும் புள்ளி வரைபடம்; பெரும்பாலான செயலிழப்புகள் கிளஸ்டர்கள் 2, 3, 6-இல் குவிந்துள்ளன, காலத்தின் இறுதியில் கிளஸ்டர் 1-இல் ஒரு அதிகரிப்பு உள்ளது.

அந்த தருணத்தில்தான் இரண்டு பிழைகளை மனதில் ஒன்றாகக் கலக்கியிருந்தோம் என்பதை உணர்ந்தோம். இரண்டு பிழைகளிலிருந்தும் முரண்பட்ட எடுத்துக்காட்டுகளைக் கலந்ததால், ஒரே ஒழுங்கான விளக்கத்தை கண்டுபிடிக்க முடியவில்லை.

பிழை #1: மோசமான ஹோஸ்ட்

குபெர்னெட்டஸ் நோட்கள் மற்றும் நேர முத்திரைகளின் சுத்தமான பட்டியலுடன், தவறாகச் சீரமைக்கப்பட்ட ஸ்டாக் செயலிழப்புகளை ஒரே இயல்நிலை ஹோஸ்ட் வரை கண்டறிய முடிந்தது; அதை மறுப்புப் பட்டியல் செய்வது எளிதாக இருந்தது.

பல வாரங்கள் தீவிர சோதனை செய்த பிறகும், அந்த ஹோஸ்ட்டில் பதிவேடு சிதைவைக் கட்டுப்படுத்தப்பட்ட சூழலில் மீண்டும் உருவாக்க முடியவில்லை. ஆனால் சேவையிலிருந்து பிரச்சினை கொண்ட ஹோஸ்ட் நீக்கப்பட்டதும், தவறாகச் சீரமைக்கப்பட்ட ஸ்டாக் செயலிழப்புகள் மறைந்தன.

மோசமான ஹோஸ்ட்டை நீக்குவது நிரந்தர தீர்வு அல்ல; அதே பிரச்சினை மீண்டும் வருவதை அது தடுக்காது. ஆனால் இதே போன்ற சிக்கல் மீண்டும் வந்தால், அதை எளிதில் கண்டறிந்து கையாளும் வகையில் மென்பொருளை மாற்றலாம். பதிவுகள் மட்டும் வைத்து சிக்கல் மீண்டும் ஏற்படுவதைக் கண்டறிய, பதிவேட்டின் நிலையைச் சேர்க்கும் வகையில் எங்கள் ஃபேட்டல் சிக்னல் ஹேண்ட்லரை மேம்படுத்தினோம் (கோர் டம்ப் தேவையில்லை). VMகள் வழக்கமாக மறுசுழற்சி செய்யப்படாமல் மீண்டும் பயன்படுத்தப்படுமாறு கட்டுப்பாட்டுத் தளத்தை மாற்றினோம்; இது உள்கட்டமைப்பு ஸ்டாக்கின் எங்கள் நிலையில் மோசமான நோடைக் கண்டறிவதை மிக எளிதாக்குகிறது. இந்த சாத்தியத்தையும் சேர்க்க எங்கள் ரன்புக்குகளையும் அணியின் மன ரீதியான மாடல்களையும் புதுப்பித்தோம்.

மோசமான ஹோஸ்ட் செயலிழப்புகள் தனியாகப் பிரிக்கப்பட்டதும், மீதமிருந்த பூஜ்ஜியத்திற்கு ரிட்டர்ன் செய்தல் கோர்கள் பற்றி ஆராய்வது மிகவும் எளிதானது. முன்பு விதிவிலக்கு அன்வைண்டிங்கை நிராகரித்தோம்; ஏனெனில் விதிவிலக்குகள் நிச்சயமாக பயன்படுத்தப்படாத கோடிங் பாதைகளில் செயலிழப்புகள் என்ற முரண்பட்ட எடுத்துக்காட்டுகள் இருந்ததாக நினைத்தோம். ஆனால் அந்த முரண்பட்ட எடுத்துக்காட்டுகள் அனைத்தும் வன்பொருள் சிதைவுக் கிளஸ்டரிலிருந்தவை.

அதை மனதில் வைத்து மீதமுள்ள கோர்களை மீண்டும் பார்த்தபோது, அந்த முடிவு முற்றிலும் தலைகீழ் என்பதைக் கண்டோம்: செயலிழப்புகள் அனைத்தும் விதிவிலக்கு அன்வைண்டிங் நேரத்திலேயே நடந்தன.

விதிவிலக்கு கையாளுதல் என்பது மாறும் கட்டுப்பாட்டு மாற்றம் ஆகும்

C++-ஆனது விதிவிலக்கை உருவாக்கும்போது, எந்தப் பிடிப்புத் தொகுதி அதை பெற வேண்டும், வழியில் எந்த அழிப்பான்கள் அல்லது தூய்மைப்படுத்தும் ஹேண்ட்லர்கள் இயங்க வேண்டும் என்பதை இயக்கநேரம் கண்டறிய வேண்டும். கம்பைலர் இந்த மெட்டாடேட்டாவை வெளியிடுகிறது; ஆனால் உண்மையான பொருத்தமானது இயக்கநேரத்தில் மாறும் வகையில் நடக்கிறது.

விதிவிலக்கு அன்வைண்டிங் உண்மையில் த்ரோவைத் தூண்டும் சார்பினால் செய்யப்படுவதில்லை; விளையும் தொகுக்கப்பட்ட கோடிங் அழைக்கும் துணைச் சார்புகளால் செய்யப்படுகிறது. அந்த இயக்கநேர நடைமுறைகளானவை ஸ்டாக்கை ஆய்ந்து, ஸ்டாக்கில் உள்ள சார்புகள் பற்றிய மெட்டாடேட்டாவைப் பெற்று, தூய்மைப்படுத்தும் ஹேண்ட்லர்கள் மற்றும் பிடிப்புத் தொகுதிகளை மாறும் தன்மையுடன் தேடி, பின்னர் அவற்றில் ஒன்றுக்கு கட்டுப்பாட்டை மாற்றுகின்றன. கட்டுப்பாட்டை மாற்றுவதற்கு இடையில் உள்ள எல்லா ஸ்டாக் ஃபிரேம்களையும் அன்வைண்டிங் செய்வது அடங்கும் (துணைச் சார்புகளின் ஃபிரேம்கள் உட்பட).

செயல்பாட்டு ரீதியாக, இது சாதாரண அழைப்பு மற்றும் ரிட்டர்ன் செய்தலை விட longjmp அல்லது ஃபைபர் ஸ்விட்சுக்கு மிகவும் அருகில் உள்ளது. அழைக்கப்பட்ட சேமிப்புப் பதிவேடுகள் மட்டுமல்ல, ஸ்டாக் ஃபிரேம் பதிவேடுகள் %rbp மற்றும் %rsp என்பவையும் மீட்டமைக்கப்பட வேண்டும்.

C++ விதிவிலக்கு அன்வைண்டிங் செய்யும் செயல்பாடுகளின் செயலாக்கங்களைக் கொண்ட இரண்டு நூலகங்களுக்கு எங்கள் பைனரி இணைக்கப்படுகிறது: libgcc மற்றும் GNU libunwind. இயக்கநேர இணைப்பி தேர்ந்தெடுத்தவை GNU libunwind-இன் வரையறைகள். அது எங்களை ஆச்சரியப்படுத்தியது; குறியீட்டுப் பதிப்பு விதிகள் காரணமாக libgcc செயலாக்கம் வெல்லும் என எதிர்பார்த்தோம்; ஆனால் இயங்கிக்கொண்டிருக்கும் பைனரிகளைப் பார்த்ததில் அப்படி இல்லை.

கடைசி ஒரு அனுமானத்தை விடுதல்

இந்த கட்டத்தில், ஒரே பிழை மட்டுமே உள்ளது என்று நினைத்தபோது செய்த இன்னொரு அனுமானத்தைத் தளர்த்தியதால், எங்கள் செயல்பாட்டு அனுமானம் மாறியது.

ஒரு சாதாரண செயல்பாடு NULL-க்கு ரிட்டர்ன் செய்வதை நாங்கள் பார்த்திருக்காமல் இருக்கலாம். அதற்கு பதிலாக, இலக்கு கட்டளைச் சுட்டியானது கட்டுப்பாட்டை மாற்றுவதற்கு முன் NULL ஆகிவிட்ட அன்வைண்டு மாற்றம்—அதாவது setcontext-பாணியிலான பதிவேடு— மீட்டமைப்பைப் பார்த்திருக்கலாம். வேறு வார்த்தைகளில், ஸ்டாக்கில் தவறான ரிட்டர்ன் முகவரி ஸ்லாட் அல்ல; அன்வைண்டு நூலகத்திலிருந்து வந்த தவறான தரவு.

இதனால் பிரச்சினை மிகவும் குறுகியது. GNU libunwind தவறான இலக்கு நிலையைக் கணக்கிட்டிருக்கலாம்; அல்லது சரியான நிலையைக் கணக்கிட்டு, அது செயல்படுத்துவதற்கு முன் ஏதோ அதை கெடுத்திருக்கலாம்.

GNU libunwind மூலத்தை வாசித்தோம்; அது ஸ்டாக்கில் ucontext_t ஒன்றை உருவாக்கி, தூய்மைப்படுத்தல் ஹேண்ட்லரின் ஃபிரேமுக்கான வேண்டிய பதிவேடு நிலையை நிரப்பி, அந்தக் கட்டமைப்பிற்கான சுட்டியை உள்புற அசெம்பிளி செயல்முறைக்கு கொடுக்கிறது: _Ux86_64_setcontext.

இந்த கட்டத்தில் எங்களிடம் எல்லா பாகங்களும் இருந்தன.

உருவாக்கப்பட்ட ucontext_t, _Ux86_64_setcontext செயல்பாட்டின்போது அதனால் அன்வவுண்டு செய்யப்படும் ஸ்டாக் ஃபிரேம்களில் ஒன்றில் உள்ளது. _Ux86_64_setcontext-ஆனது %rsp-ஐ மாற்றிய பிறகும் கட்டமைப்பிலிருந்து படித்ததா? அந்த நேரத்தில் கட்டமைப்பு ஆனது செயலில் உள்ள ஸ்டாக்கின் பகுதியாக இல்லை. அப்படியானால் எங்கள் அடிக்கடி வரும் SIGUSR2 போன்ற சிக்னல் அனுப்புவதால் அது சிதைக்கப்படும் பாதிப்புக்கு உள்ளாகும்.

பிழை #2: libunwind பிழை

பதில் ஆம் என்பதாகும்.

நாங்கள் பயன்படுத்திய GNU libunwind பதிப்பில் _Ux86_64_setcontext-இன் கடைசி ஆறு கட்டளைகள் இவை; இவை பெரும்பாலும் நினைவகத்திலிருந்து இலக்கு பதிவேடுக்கு தரவை ஏற்றும் mov கட்டளைகளைக் கொண்டவை:

எளிய உரை

1
74: mov UC_MCONTEXT_GREGS_RSP(%rdi),%rsp
2
75:
3
76: /* push the return address on the stack */
4
77: mov UC_MCONTEXT_GREGS_RIP(%rdi),%rcx
5
78: push %rcx
6
79:
7
80: mov UC_MCONTEXT_GREGS_RCX(%rdi),%rcx
8
81: mov UC_MCONTEXT_GREGS_RDI(%rdi),%rdi
9
82: retq

(%rdi-ஆனது ஸ்டாக்கில் ஒதுக்கப்பட்ட ucontext_t-ஐச் சுட்டுகிறது; UC_MCONTEXT_* மேக்ரோக்கள் குறிப்பிட்ட பதிவேடு சேமிக்கப்படும் நிலையான ஆஃப்செட்டாக மட்டுமே விரிவடைகின்றன.)

முதல் கட்டளை என்பது ரேஸ் கால இடைவெளியின் தொடக்கம். அது செயலில் உள்ள ஸ்டாக்கின் புதிய அடியைச் சுட்டுமாறு %rsp-ஐப் புதுப்பிக்கிறது. இது நடந்தவுடன், %rdi சுட்டும் கட்டமைப்பு ஆனது செயலில் உள்ள ஸ்டாக்கின் (அல்லது சிவப்பு மண்டலத்தின்) பகுதியாக இல்லை; அதனால் கெர்னெல்லுக்கு இனி அணுகக்கூடியதும் அல்ல.

பொதுவாக இது பிரச்சினை தராது; ஆனால் சரியான (தவறான?) தருணத்தில் சிக்னல் வந்தால், கெர்னெல் சிக்னல் ஃபிரேமை %rsp-128-இல் உருவாக்கும். அது %rdi சுட்டும் நினைவகத்தை மேலெழுதச் செய்யலாம்.

அடுத்த கட்டளையானது UC_MCONTEXT_GREGS_RIP(%rdi)-ஐப் படிக்கும் முன் அது நடந்தால், மீட்டமைக்கப்பட்ட கட்டளைச் சுட்டி சிதைவடையலாம். எங்கள் செயலிழப்புகளில் அது NULL ஆனது.

அதுவே பிழை.

கோர்கள் ஏன் சாதாரண மோசமான ரிட்டர்ன்கள் போல மறைந்தன

இந்த அசெம்பிளி நம்மை குழப்பிய ஒரு கவனிப்பையும் விளக்குகிறது: முந்தைய ஸ்டாக் ஃபிரேமின் ரிட்டர்ன் முகவரி ஸ்லாட்டில் செயல்பாடு X-க்கு ஏன் NULL இருந்தது.

%rdi உட்பட எல்லா பதிவேடுகளையும் மீட்டமைக்கும் வகையில் setcontext எழுதப்பட்டது; ஆகவே கட்டுப்பாட்டை மாற்றும் கடைசி தருணத்தில் UC_MCONTEXT_GREGS_RIP(%rdi)-ஐப் படிக்க அந்தப் பதிவேட்டைப் பயன்படுத்த முடியாது. அதற்குப் பதிலாக, மதிப்பை முன்பே படித்து ஸ்டாக்கில் சேமித்து, இன்னும் சில பதிவேடுகளை மீட்டமைத்து, பின்னர் retq மூலம் சேமித்த மதிப்பைப் படித்து கட்டுப்பாட்டை மாற்றுகிறது.

கோர்களில் “ஒரு செயல்பாடு NULL-க்கு ரிட்டர்ன் செய்யப்பட்டது” போலத் தோன்றியது உண்மையில் “அன்வைண்டரானது ஸ்டாக்கில் இலக்கு ரிட்டர்ன் முகவரி ஒன்றை உருவாக்கியது; ஆனால் மாற்றம் முடிவதற்கு முன் அந்த இலக்கு சிதைக்கப்பட்டிருந்தது” என்பதே. ரிட்டர்ன் முகவரி ஸ்லாட்டில் ஏற்படும் சிதைவு இடத்திலேயே நடக்க வேண்டும் என்று நாங்கள் கருதினோம்; ஏனெனில் சிதைவுக்குள்ளாகக்கூடிய தரவை ரிட்டர்ன் முகவரி ஸ்லாட்டில் நோக்கமாக எழுதும் இடங்கள் எங்களுக்குத் தெரியவில்லை.

ஒரே கட்டளை அளவிலான ரேஸ் கால இடைவெளி

இந்தப் பிழை அபத்தமாகத் தோன்றுவதற்கு காரணம், இந்த ரேஸ் கால இடைவெளி மிகவும் குறுகியது. இவ்வகை ரேஸ் கண்டிஷனில், வெளிப்புற நிகழ்வு (சிக்னல்) மற்றொரு த்ரெட் எடுக்கும் இரண்டு படிகளுக்கு நடுவில் நடக்க வேண்டும். அந்த படிகள் ஒன்றுக்கொன்று நெருக்கமாக இருந்தால், ரேஸ் கண்டிஷன் நடக்கும் வாய்ப்பு குறையும்.

இந்தச் சூழலில் பாதிப்புள்ளாகும் கால இடைவெளி உண்மையிலேயே ஒரே கட்டளையின் அகலம்தான்! %rsp மாறிய பின், ஆனால் அடுத்த கட்டளையானது %rip-ஐ ஏற்றுவதற்கு முன் சிக்னல் அனுப்ப வேண்டும். இத்தகைய பல எளிய கட்டளைகளை நவீன பல கட்டளைகளை ஒரே நேரத்தில் வரிசை சாராமல் இயக்கும் CPU-இல் ஒவ்வொரு சுழற்சிக்கும் இயங்க முடியும்; ஆகவே ரேஸ் கால இடைவெளி சுமார் நூறு பிகோவினாடிகள் ஆகும்.

இந்த ரேஸைக் கண்டபோது, கவனித்த செயலிழப்பு விகிதத்தை விளக்க இதுவே மிகவும் அரிதாக இருக்கும் என்பதே எங்கள் முதல் எதிர்வினை. கணினித்தொகுப்பு முழுவதும் தினமும் ஒரு டஜனுக்கு மேல் பூஜ்ஜியத்திற்கு ரிட்டர்ன் செய்தல் செயலிழப்புகளைப் பார்த்தோம். விதிவிலக்கு சுத்திகரிப்பு நேரத்தில் ஒரே-கட்டளை ரேஸ் உண்மையிலேயே அதற்கு காரணமாக இருக்க முடியுமா?

நாங்கள் ஃபெர்மாட் மதிப்பீட்டை நாடினோம். பாதிப்புள்ளாகும் கால இடைவெளி 101010^{-10} வினாடி அளவில் இருந்தும் SIGUSR2 ஒவ்வொரு 10210^{-2} வினாடி CPU நேரத்துக்கும் வந்தாலும், ஒவ்வொரு விதிவிலக்கு சுத்திகரிப்பு ஹேண்ட்லர் அல்லது பிடிப்புத் தொகுதிக்கும் ரேஸை இழக்கும் சாத்தியம் சுமார் 10810^{-8}.

Rockset தனது உள்புற உள்ளீட்டு பின்அழுத்தப் பொறிமுறையின் பகுதியாக விதிவிலக்குகளைப் பயன்படுத்துகிறது. ஒரே அதிக சுமையுள்ள ஹோஸ்ட் ஒரு வினாடிக்கு 10410^{4} அளவில் விதிவிலக்குகளை உருவாக்கலாம். அதனால் பின் அழுத்தத்தைப் பயன்படுத்தும் ஹோஸ்ட்டின் தோல்விகளுக்கு இடையிலான சராசரி நேரம் 10410^{4} வினாடிகள், அதாவது சில மணிநேரத்திற்கு ஒரு செயலிழப்பு என பொருள். கணினித்தொகுப்பு அளவில், கவனித்த செயலிழப்பு அதிர்வெண்ணை விளக்க இது போதுமானதை விட அதிகம்.

இப்போது libunwind பிழை ஏன் தோன்றியது?

GNU libunwind பிழை பழையது—18 ஆண்டுகளுக்கும் மேல் பழையது; C++ விதிவிலக்கு அன்வைண்டிங்கை ஆதரித்த முதல் x86_64 பதிப்பிலேயே இருந்தது.

அப்படியிருக்க இப்போது ஏன் வெளிப்பட்டது?

செயலிழப்பு விகிதம், எத்தனை விதிவிலக்குகள் உருவாக்கப்படுகின்றன மற்றும் எத்தனை சிக்னல்கள் அனுப்பப்படுகின்றன என்பதற்கு சுமார் நேர் விகிதத்தில் அமைகிறது. சிக்னல் ஹேண்ட்லர் எவ்வளவு ஸ்டாக்கைப் பயன்படுத்துகிறது என்பதையும் அது சார்ந்துள்ளது.

Rockset இந்த மூன்று அச்சுகளிலும் வழக்கத்திற்கு மாறானது. சாதாரண அதிக சுமையுள்ள கட்டுப்பாட்டின் பகுதியாக உயர்ந்த விகிதத்தில் விதிவிலக்குகளை உருவாக்குகிறோம்; coarse_thread_cputime_clock காரணமாக SIGUSR2-ஐ வழக்கத்திற்கு மாறாக அடிக்கடி அனுப்புகிறோம்; மேலும் ஒன்றிணைக்கப்பட்ட சிக்னல்களைக் கணக்கில் கொள்ளும் வகையில் timer_getoverrun அழைப்பைச் சேர்த்ததால், இந்த ஆண்டின் தொடக்கத்தில் SIGUSR2 ஹேண்ட்லர் அதிக ஸ்டாக்கைப் பயன்படுத்தியது.

அந்த கடைசி மாற்றம் முக்கியமானதாகத் தெரிகிறது. ஹேண்ட்லர் போதுமான அளவு குறைந்த ஸ்டாக்கைப் பயன்படுத்தினால், அது பழைய ucontext_t நினைவகத்தை எட்டியும் மேலெழுத முடியாமல் போகலாம். அந்த மாற்றத்திற்கு முன், இந்தச் செயலிழப்புகள் எதையும் நாங்கள் காணவில்லை. மாற்றத்திற்கு பின், பின் அழுத்தப் பொறிமுறையை அழுத்திய சில பயன்பாட்டுச் சூழல்களுக்கு சுமையை உயர்த்தும் வரை விகிதம் குறைவாகவே இருந்தது.

வேறு வார்த்தைகளில், libunwind பிழை எப்போதும் இருந்தது; ஆனால் எங்கள் விதிவிலக்கு விகிதம், சிக்னல் விகிதம் மற்றும் ஹேண்ட்லர் ஸ்டாக் பயன்பாடு ஆகியவற்றின் பெருக்கற்பலன் சமீபத்தில்தான் செயல்பாட்டு ரீதியாகத் தெரியும் அளவின் வரம்பைக் கடந்தது.

வன்பொருள் பிழை மற்றும் libunwind பிழை இரண்டும் பெரும்பாலும் DocumentTree::updateDocument-க்குள் செயலிழப்பானது ஏன் என்ற தற்செயலையும் இந்தப் பொறிமுறை விளக்குகிறது. libunwind-இலிருந்து வந்த செயலிழப்புகள் இந்த முறை நோக்கி பெரிதும் நிகழ்ந்தவை; ஏனெனில் உள்ளீட்டு பின் அழுத்தத்தைப் பயன்படுத்த விதிவிலக்கை உருவாக்கும் நேரத்தில் அது எப்போதும் செயலில் இருக்கும். %rsp-தவறாகச் சீரமைக்கப்பட்ட செயலிழப்புகளுக்கும் இது பெரிதும் தேர்ந்தெடுக்கப்பட்டது; ஏனெனில் மோசமான வன்பொருள் நோடானது பெருமளவிலான தரவு உள்ளீட்டிற்குப் பயன்படுத்தும் SKU-ஐச் சேர்ந்தது, அது தனது CPU நேரத்தின் பெரும்பகுதியை அந்த முறையில் செலவிடுகிறது.

எங்கள் உடனடித் தணிப்பு, GNU libunwind-இலிருந்து libgcc-இன் அன்வைண்டருக்கு மாறுவது. அது தனியாகவே நல்ல நடவடிக்கையாக இருந்தது: libgcc செயலாக்கமானது பூட்டுப் போட்டியைக் குறைக்க பல பணிகளால் மேம்பட்டுள்ளது; பெரிய VMகளுக்கு விரிவுபடுத்தும்போது இது முக்கியம்.

மேலும் தனித்து இயங்கக்கூடிய மறுஉருவாக்கி ஒன்றையும் GNU libunwind-க்கு ஒரு சரிசெய்தலையும்(புதிய சாளரத்தில் திறக்கும்) சமர்ப்பித்தோம்; மற்ற அன்வைண்டர்களில் இதே போன்ற சிக்கல் இல்லை என்பதையும் சரிபார்த்தோம்.

தொகுதி அளவிலான நோயறிதலின் சக்தி

இந்தப் பிழை திருத்தப் பயணம் ஆனது இயக்கநேர இணைப்பு , DWARF அன்வைண்ட் மெட்டாடேட்டா, Linux சிக்னல் அனுப்புதல், முறைமை V ABI, மற்றும் C++ விதிவிலக்கு கையாளுதல் கட்டமைப்பு ஆகியவற்றின் குறிப்பான விவரங்களை எங்களுக்கு பலமாக கற்றுக் கொடுத்தது. ஆனால் முதன்மை பாடம் இவை அனைத்தையும் விட எளியது.

மிக முக்கியமான படி கூர்மையான அசெம்பிளி வாசிப்போ விவரங்களில் ஆழ்ந்த அறிவோ அல்ல. அது உயர்தர தரவுத் தொகுப்பை உருவாக்குவதுதான். இந்த தரவுத் தொகுப்பு இல்லாதபோது, இரண்டு தனி நிகழ்வுகளை ஒரே கதையாகக் கலந்து, குழப்பத்திலிருந்து காரணத்தைக் கூறி வெளியேற முயன்றோம். துல்லியமும் முழுமையும் உள்ள தொகுதி தரவு கிடைத்தவுடன், பிரச்சினையின் அமைப்பு தெளிவானது: ஒரு சிதைவுத் தரவுத் தொகுதி மோசமான ஹோஸ்ட்டைச் சேர்ந்தது; மற்றொன்று libunwind-இல் உள்ள ரேஸைச் சேர்ந்தது. தரவு மேம்பட்டதும் பிழை திருத்தம் செய்வது எளிதானது.

Rockset போன்ற உள்கட்டமைப்பு முறைமைகளுக்கு அது மிகவும் முக்கியம். ஆழமான கண்காணிப்பு வசதிகள், தானியங்கி ஆய்வுகள் மற்றும் எங்கள் செயல்பாட்டு கருவிகளில் தொடர்ச்சியான மேம்பாடுகள் மீது உள்ள உறுதிப்பாட்டை இந்த விசாரணை வலுப்படுத்தியது. நம்பகத்தன்மை என்பது பிழைகள் நடந்த பிறகு அவற்றை சரிசெய்வது மட்டுமல்ல; சாத்தியமற்றது போலத் தோன்றும் பிரச்சினைகளைக் கண்டறிந்து தீர்க்கக்கூடியவையாக மாற்றும் தரவு, பணிப்பாய்வுகள் மற்றும் திறன்களை உருவாக்குவதும்தான்.

ஆசிரியர்கள்

By Nathan Bronson மற்றும் Member of Technical Staff