ਕੋਰ ਡੰਪ ਮਹਾਮਾਰੀ ਵਿਗਿਆਨ: 18 ਸਾਲ ਪੁਰਾਣਾ ਬੱਗ ਠੀਕ ਕਰਨਾ
ਸਾਡੇ data infrastructure ਵਿੱਚ ਔਖੇ crashes debug ਕਰਨ ਲਈ population-level analysis ਦੀ ਵਰਤੋਂ.
OpenAI ਦੇ ਮਾਡਲ ਅਤੇ ਏਜੰਟ inference ਵੇਲੇ, ਯਾਨੀ ਜਦੋਂ ਮਾਡਲ ਤੁਹਾਡੇ ਸਵਾਲ ਬਾਰੇ ਸੋਚ ਰਹੇ ਹੁੰਦੇ ਹਨ, ਸੰਬੰਧਿਤ ਡਾਟਾ ਲੱਭਣ ਲਈ ਸਕੇਲ ਹੋਣ ਯੋਗ ਡਾਟਾ ਢਾਂਚੇ ਉੱਤੇ ਵੱਧ ਨਿਰਭਰ ਕਰ ਰਹੇ ਹਨ. ਇਨ੍ਹਾਂ ਵਿੱਚੋਂ ਕੁਝ ਸੇਵਾਵਾਂ C++ ਵਿੱਚ ਲਿਖੀਆਂ ਹਨ, ਜਿਸ ਦਾ ਸਿਸਟਮ ਉੱਤੇ ਨੀਵੇਂ ਪੱਧਰ ਦਾ ਕੰਟਰੋਲ ਸਾਨੂੰ ਕਾਰਗੁਜ਼ਾਰੀ ਵਧਾਉਣ ਅਤੇ ਮੈਮੋਰੀ ਵਰਤੋਂ ਘਟਾਉਣ ਦਿੰਦਾ ਹੈ. ਸਕੇਲ ਕਰਦੇ ਸਮੇਂ ਇਹ ਕੁਸ਼ਲਤਾ ਜ਼ਰੂਰੀ ਹੈ, ਪਰ C++ ਵਿੱਚ ਮੈਮੋਰੀ ਸੁਰੱਖਿਆ ਨਾ ਹੋਣ ਕਰਕੇ ਬੱਗ ਗਲਤ ਜਾਂ ਨਾ-ਮੌਜੂਦ ਮੈਮੋਰੀ ਪਤਿਆਂ ਉੱਤੇ ਲਿਖ ਕੇ ਕਰੈਸ਼ ਕਰ ਸਕਦੇ ਹਨ.
ਕੁਝ ਮਹੀਨੇ ਪਹਿਲਾਂ ਸਾਨੂੰ Rockset ਸੇਵਾ ਅੰਦਰ ਕੁਝ ਕਰੈਸ਼ ਦਿਖੇ. ਇਹ ਸਾਡੇ ChatGPT ਡਾਟਾ ਢਾਂਚੇ ਦਾ ਖਾਸ ਹਿੱਸਾ ਹੈ, ਜੋ ਕਈ ਡਾਟਾ ਪਲੱਗਇਨਾਂ ਅਤੇ ਗੱਲਬਾਤਾਂ ਵਿੱਚ ਖੋਜ ਲਈ ਮਹੱਤਵਪੂਰਣ ਹੈ. ਹਰ ਕਰੈਸ਼ ਵਿੱਚ ਇੱਕ ਆਮ C++ ਫੰਕਸ਼ਨ ਖਤਮ ਹੋ ਕੇ ਝੂਠੇ ਪਤੇ ਉੱਤੇ ਵਾਪਸ ਜਾਂਦਾ ਲੱਗਿਆ, ਇਸ ਲਈ kernel ਨੇ ਪ੍ਰੋਗਰਾਮ ਰੋਕ ਦਿੱਤਾ ਕਿਉਂਕਿ instruction pointer ਹੁਣ ਕੋਡ ਵੱਲ ਨਹੀਂ ਸੀ. ਕਦੇ stack frame ਵਿੱਚ return address slot NULL ਸੀ. ਕਦੇ stack pointer CPU register ਖੁਦ 8 bytes ਨਾਲ ਹਟਿਆ ਲੱਗਦਾ ਸੀ, ਜਿਵੇਂ ਆਮ execution ਦੌਰਾਨ %rsp ਕਿਸੇ ਤਰ੍ਹਾਂ ਘਟ ਗਿਆ ਹੋਵੇ. ਦੋਵੇਂ ਹਾਲਤਾਂ ਵਿੱਚ ਕਰੈਸ਼ return ਉੱਤੇ ਹੋਇਆ.
application code ਲਈ ਇਹ ਆਮ failure modes ਨਹੀਂ ਹਨ. ਇੱਕ stray write ਸਿਰਫ saved return address ਉੱਤੇ ਪੈ ਸਕਦੀ ਹੈ, ਪਰ ਇਹ ਬਹੁਤ ਹੀ ਅਸੰਭਵ ਹੈ. inline assembly, setcontext ਜਾਂ longjmp ਤੋਂ ਬਿਨਾ, ਜਿਨ੍ਹਾਂ ਦੀ ਅਸੀਂ ਵਰਤੋਂ ਨਹੀਂ ਕਰਦੇ, %rsp ਨੂੰ 8 ਨਾਲ misalign ਕਰਨ ਵਾਲਾ ਬੱਗ ਹੋਰ ਵੀ ਅਜੀਬ ਹੈ, ਕਿਉਂਕਿ compiled code ਇਹ register ਸਿੱਧਾ ਸਿਰਫ function prologue ਅਤੇ epilogue ਵਿੱਚ ਬਦਲਦਾ ਹੈ. ਹਰ hypothesis ਜਿਸ ਬਾਰੇ ਅਸੀਂ ਜਾਂ ChatGPT ਸੋਚ ਸਕੇ, ਉਸ ਦੇ ਵਿਰੁੱਧ ਮਜ਼ਬੂਤ ਸਬੂਤ ਸੀ, ਇਸ ਲਈ ਬੱਗ ਅਸੰਭਵ ਜਿਹਾ ਲੱਗਿਆ.
ਜਿਸਨੂੰ ਅਸੀਂ ਇੱਕ ਸਮੱਸਿਆ ਮੰਨਿਆ ਸੀ, ਆਖਿਰਕਾਰ ਉਹ ਦੋ ਅਸੰਬੰਧਿਤ ਬੱਗ ਨਿਕਲੇ, ਜੋ ਇਤਫਾਕ ਨਾਲ ਇੱਕੋ ਸਮੇਂ ਮਿਲੇ. ਪਹਿਲਾ, ਇੱਕ Azure host ਉੱਤੇ ਚੁੱਪਚਾਪ hardware corruption, ਜਿੱਥੇ CPU ਗਣਨਾ ਠੀਕ ਨਹੀਂ ਕਰ ਰਿਹਾ ਸੀ. ਦੂਜਾ, GNU libunwind ਵਿੱਚ 18 ਸਾਲ ਪੁਰਾਣੀ race condition, ਇੱਕ widely used open source library ਵਿੱਚ ਅਣਦੇਖਿਆ ਬੱਗ.
ਇਹ ਪੋਸਟ ਦੱਸਦੀ ਹੈ ਕਿ ਅਸੀਂ epidemiologist ਵਾਂਗ ਸੋਚ ਕੇ ਅਤੇ ਕਰੈਸ਼ਾਂ ਦੀ ਪੂਰੀ population ਬਾਰੇ ਉੱਚ-ਗੁਣਵੱਤਾ data set ਬਣਾਕੇ ਸਮਝ ਤੋਂ ਪਰੇ ਕਰੈਸ਼ਾਂ ਨੂੰ ਕਿਵੇਂ ਪਛਾਣਿਆ ਅਤੇ ਠੀਕ ਕੀਤਾ.
ਪਹਿਲਾਂ, Rockset ਬਾਰੇ ਹੋਰ ਗਹਿਰਾਈ ਨਾਲ ਵੇਖੀਏ. ਇਹ search ਅਤੇ real-time analytics ਲਈ cloud-native data system ਹੈ, ਜਿਸਨੂੰ ਅਸੀਂ OpenAI ਵਿੱਚ ਕਈ internal use cases, ਜਿਵੇਂ sync connectors, ਲਈ ਵਰਤਦੇ ਹਾਂ. Rockset ਨੂੰ OpenAI ਨੇ 2024 ਵਿੱਚ acquire ਕੀਤਾ ਸੀ. Streaming updates ਨਾਲ ਵਰਕਸਪੇਸ ਦੇ knowledge base ਦਾ ਤਾਜ਼ਾ index ਰੱਖਿਆ ਜਾਂਦਾ ਹੈ, ਤਾਂ ਜੋ ChatGPT ਸਵਾਲਾਂ ਦੇ ਜਵਾਬ ਜਾਂ actions ਕਰਦਿਆਂ ਸੰਬੰਧਿਤ ਜਾਣਕਾਰੀ ਲੱਭ ਸਕੇ.
Rockset ਦੀ execution layer C++ ਵਿੱਚ ਲਿਖੀ ਹੈ. C++ ਭਾਸ਼ਾ CPU ਤੱਕ ਨੀਵੇਂ ਪੱਧਰ ਦੀ ਪਹੁੰਚ ਦਿੰਦੀ ਹੈ, ਜੋ performance ਅਤੇ efficiency ਲਈ ਚੰਗੀ ਹੈ, ਪਰ ਇਸ ਨਾਲ application bugs invalid memory accesses ਅਤੇ segfaults ਕਰ ਸਕਦੇ ਹਨ. ਇਹ ਲੱਭਣ ਲਈ ਅਸੀਂ folly ਦੇ fatal signal handler ਨਾਲ ਕਰੈਸ਼ ਵੇਲੇ stack trace log ਕਰਦੇ ਹਾਂ, ਅਤੇ ਬਾਅਦਲੀ analysis ਲਈ ਸੰਬੰਧਿਤ core dumps, ਯਾਨੀ ਕਰੈਸ਼ ਸਮੇਂ program state ਦਾ snapshot, Azure blob storage ਉੱਤੇ upload ਕਰਦੇ ਹਾਂ. Rockset ਦੇ ਸਾਰੇ query processing leaves replicated ਹਨ, ਇਸ ਨਾਲ ਕਰੈਸ਼ ਦਾ client ਉੱਤੇ ਅਸਰ ਘਟਦਾ ਹੈ. ਫਿਰ ਵੀ ਹਰ segfault ਇੱਕ ਅਜਿਹਾ ਬੱਗ ਹੈ ਜਿਸਨੂੰ ਸਾਡੀ reliability ਅਤੇ quality goals ਲਈ ਠੀਕ ਕਰਨਾ ਪੈਂਦਾ ਹੈ.
ਸਾਡੀ ਸ਼ੁਰੂਆਤੀ ਪਹੁੰਚ ਇਹ cores ਨੂੰ ਰਵਾਇਤੀ debugging ਸਮੱਸਿਆ ਵਾਂਗ ਲੈਣ ਦੀ ਸੀ: ਕੁਝ core dumps ਬਹੁਤ ਧਿਆਨ ਨਾਲ ਵੇਖੋ, hypotheses ਬਣਾਓ, ਅਤੇ ਇੱਕ-ਇੱਕ ਕਰਕੇ ਰੱਦ ਕਰੋ.
ਜ਼ਿਆਦਾਤਰ ਕਰੈਸ਼ DocumentTree::updateDocument ਨਾਂ ਦੀ method ਵਿੱਚ ਹੋਏ. ਇਨ੍ਹਾਂ ਕਰੈਸ਼ਾਂ ਵਿੱਚ ਲੱਗਿਆ ਕਿ updateDocument ਨੇ ਕੋਈ ਅਣਜਾਣ function X call ਕੀਤਾ, X ਦੇ active ਹੋਣ ਦੌਰਾਨ stack corrupt ਹੋ ਗਿਆ, ਫਿਰ X ਉਸ ਪਤੇ ਉੱਤੇ ਵਾਪਸ ਗਿਆ ਜੋ executable code ਨਹੀਂ ਸੀ. ਕੁਝ cases ਵਿੱਚ X ਦਾ ਹੁਣੇ pop ਹੋਇਆ frame valid ਲੱਗਦਾ ਸੀ, ਸਿਰਫ ਉਸ ਦਾ saved return address NULL ਸੀ. ਹੋਰ cases ਵਿੱਚ stack pointer ਖੁਦ ਗਲਤ ਲੱਗਿਆ, ਪਰ ਅਗਲਾ valid frame ਫਿਰ ਵੀ updateDocument ਹੀ ਦਿਸਦਾ ਸੀ.
ਸਾਨੂੰ ਨਹੀਂ ਪਤਾ ਸੀ stack ਕਦੋਂ corrupt ਹੋ ਰਿਹਾ ਸੀ, ਇਸ ਨਾਲ search space ਬਹੁਤ ਵੱਡੀ ਰਹਿ ਗਈ. updateDocument ਵੱਡੀ method ਹੈ ਅਤੇ ਇਸ ਵਿੱਚ ਕਾਫੀ inlining ਹੁੰਦੀ ਹੈ, ਇਸ ਲਈ X ਲਈ candidates ਬੇਹੱਦ ਜ਼ਿਆਦਾ ਸਨ.
ਕੀ ਇਹ ਸਾਡੇ C++ code ਵਿੱਚ ਬੱਗ ਸੀ? Compiler ਜਾਂ linkage issue? ਸਾਡੀਆਂ runtime libraries ਵਿੱਚੋਂ ਕਿਸੇ ਵਿੱਚ ਸਮੱਸਿਆ? Signal delivery ਜਾਂ context switching ਦੇ ਆਲੇ-ਦੁਆਲੇ Linux kernel bug? ਕੁਝ ਹੋਰ ਵੀ ਵਿਰਲਾ? ਜੇ ਇਹ stray write ਸੀ, ਤਾਂ ਸਾਡੇ ASAN staging environment ਨੇ ਇਸਨੂੰ ਕਿਉਂ ਨਹੀਂ ਫੜਿਆ?
ਅਸੀਂ problem ਦੀਆਂ ਸਾਰੀਆਂ occurrences ਪਛਾਣਨ ਲਈ application-level logs ਵਰਤਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕੀਤੀ, ਪਰ stack-corruption bugs ਸਿਰਫ logs ਤੋਂ classify ਕਰਨੇ ਔਖੇ ਹਨ ਕਿਉਂਕਿ logged stack traces ਆਪ corrupt ਜਾਂ missing ਹੁੰਦੀਆਂ ਹਨ. ਅਸੀਂ ਐਸੀ log query ਨਹੀਂ ਬਣਾ ਸਕੇ ਜਿਸ ਵਿੱਚ false positives ਅਤੇ false negatives ਦੋਵੇਂ ਨਾ ਹੋਣ. ਅਸੀਂ ਹੋਰ cores ਹੱਥੋਂ ਵੇਖੇ ਅਤੇ ਕੁਝ ਵਾਧੂ ਉਦਾਹਰਨਾਂ ਮਿਲੀਆਂ, ਪਰ ਇਹ process ਭਰੋਸੇਯੋਗ data set ਲਈ ਬਹੁਤ ਮਿਹਨਤ ਵਾਲਾ ਸੀ.
ਜਾਂਚ ਦੇ ਇਸ ਪੜਾਅ ਉੱਤੇ ਅਸੀਂ hardware bug ਨੂੰ ਗਲਤ ਤੌਰ ਉੱਤੇ ਰੱਦ ਕਰ ਦਿੱਤਾ, ਕਿਉਂਕਿ ਕਰੈਸ਼ ਕਈ regions ਅਤੇ hardware types ਵਿੱਚ ਦਿਖੇ; ਇਸ ਲਈ ਅਸੀਂ ਅਜੇ ਵੀ ਸਿਰਫ software causes ਲੱਭ ਰਹੇ ਸੀ. ਕੁਝ ਦਿਨਾਂ ਲਈ ਅਸੀਂ ਇੱਕ misaligned-%rsp crash ਵਿੱਚ ਬਹੁਤ ਡੂੰਘੇ ਗਏ, stack ਅਤੇ register contents ਨਾਲ pre-crash history ਦੁਬਾਰਾ ਬਣਾਈ. ਇਸ ਨਾਲ ਕੁਝ clues ਮਿਲੇ, ਪਰ ਕਿਉਂਕਿ ਅਸੀਂ ਆਪਣੀ ਮੁੱਢਲੀ ਧਾਰਣਾ ਨਹੀਂ ਛੱਡੀ ਕਿ ਸਾਰੇ bugs ਦਾ ਇੱਕੋ cause ਹੈ, ਇਸ ਨੇ ਰਾਹ ਨਹੀਂ ਖੋਲ੍ਹਿਆ.
ਜਾਂਚ ਦੇ turning point ਉੱਤੇ ਜਾਣ ਤੋਂ ਪਹਿਲਾਂ, ਇਹ ਸਮਝਾਉਣਾ ਜ਼ਰੂਰੀ ਹੈ ਕਿ ਅਸੀਂ core files ਤੋਂ ਕਿਸ ਕਿਸਮ ਦੀ ਜਾਣਕਾਰੀ ਕੱਢ ਰਹੇ ਸੀ.
Rockset ਨੂੰ -fno-omit-frame-pointer ਨਾਲ compile ਕੀਤਾ ਜਾਂਦਾ ਹੈ, ਇਸ ਲਈ active stack frame ਹਮੇਸ਼ਾਂ %rbp ਰਾਹੀਂ ਮਿਲਦਾ ਹੈ, ਅਤੇ callers frame pointers ਦੀ linked list ਬਣਾਉਂਦੇ ਹਨ.
Linux x86_64 ਉੱਤੇ AMD64 System V ABI, %rsp ਤੋਂ ਹੇਠਾਂ 128 bytes ਨੂੰ red zone ਵਜੋਂ reserve ਕਰਦਾ ਹੈ. ਇਹ ਖੇਤਰ userspace code ਲਈ available ਹੈ ਅਤੇ, ਮਹੱਤਵਪੂਰਣ ਤੌਰ ਉੱਤੇ, kernel ABI contract ਦੇ ਹਿੱਸੇ ਵਜੋਂ signal deliver ਕਰਦਿਆਂ ਇਸਨੂੰ clobber ਨਾ ਕਰਨ ਦਾ ਵਾਅਦਾ ਕਰਦਾ ਹੈ.
post-return crash ਦੀ debugging ਵਿੱਚ red zone ਕੇਂਦਰੀ ਸੀ, ਕਿਉਂਕਿ ਇਹ return ਤੋਂ ਪਹਿਲਾਂ ਦੀ ਕੁਝ ਜਾਣਕਾਰੀ ਸੰਭਾਲਦਾ ਹੈ. ਜਦੋਂ SIGSEGV trigger ਹੁੰਦਾ ਹੈ, folly ਦਾ fatal signal handler crashing thread ਦੇ stack ਉੱਤੇ ਚੱਲਦਾ ਹੈ. ਜੋ stack frames ਹੁਣ active ਨਹੀਂ ਰਹੇ, ਕਿਉਂਕਿ ਉਨ੍ਹਾਂ ਦਾ function return ਕਰ ਚੁੱਕਾ ਹੈ, ਉਹ signal handler ਨਾਲ clobber ਹੋ ਜਾਣਗੇ, ਆਖਰੀ 128 bytes ਤੋਂ ਇਲਾਵਾ. ਇਸੇ ਕਰਕੇ ਅਸੀਂ ਕਹਿ ਸਕਦੇ ਹਾਂ ਕਿ “X ਦਾ ਹੁਣੇ pop ਹੋਇਆ stack frame valid ਸੀ, NULL return address ਤੋਂ ਇਲਾਵਾ.” red zone inactive frames ਦਾ ਕੁਝ ਹਿੱਸਾ, ਕਦੇ ਸਿਰਫ ਇੱਕ inactive frame ਦਾ tail, ਬਚਾ ਲੈਂਦਾ ਹੈ.
ਸਾਨੂੰ ਇੱਕ misaligned-stack crash ਮਿਲਿਆ ਜਿਸ ਵਿੱਚ ਸ਼ਾਮਲ ਸਾਰੇ functions ਬਹੁਤ ਛੋਟੇ ਸਨ. ਇਸ ਨਾਲ ਅਸੀਂ ਵੇਖ ਸਕੇ ਕਿ %rsp ਇੱਕ ਕਾਫੀ ਸਧਾਰਣ function ਦੀ execution ਦੌਰਾਨ misalign ਹੋਇਆ ਸੀ, ਅਤੇ ਬਾਅਦ ਵਿੱਚ ਹੋਰ calls ਸਫਲ ਰਹੀਆਂ. Program ਸਿਰਫ ਤਦ ਕਰੈਸ਼ ਹੋਇਆ ਜਦੋਂ active function ਨੇ ਆਖਿਰ return ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕੀਤੀ. ਉਨ੍ਹਾਂ code paths ਵਿੱਚ exceptions, inline assembly, setcontext ਜਾਂ longjmp ਨਹੀਂ ਸੀ; ਇਸ ਲਈ ਜੇ stack pointer ਸੱਚਮੁੱਚ core ਦੇ ਸੁਝਾਏ ਢੰਗ ਨਾਲ ਬਦਲਿਆ, ਤਾਂ userspace code ਦਾ ਕੋਈ ਵਾਜਬ bug ਇਸ issue ਨੂੰ ਨਹੀਂ ਸਮਝਾ ਸਕਦਾ ਸੀ.
ਇਸ ਨੇ ਸਾਨੂੰ kernel ਵੱਲ ਧੱਕਿਆ.
Rockset signals ਨੂੰ ਜ਼ਿਆਦਾਤਰ programs ਨਾਲੋਂ ਜ਼ਿਆਦਾ ਤੀਬਰਤਾ ਨਾਲ ਵਰਤਦਾ ਹੈ. Query execution ਨੂੰ ਕਈ ਹਲਕੇ tasks ਵਿੱਚ ਵੰਡਿਆ ਜਾਂਦਾ ਹੈ ਜੋ data exchange ਕਰਦੇ ਹਨ. High-QPS workloads ਨੂੰ ਕੁਸ਼ਲਤਾ ਨਾਲ ਸੰਭਾਲਣ ਲਈ ਇਹ ਜ਼ਰੂਰੀ ਹੈ, ਪਰ per-query CPU accounting ਔਖੀ ਹੋ ਜਾਂਦੀ ਹੈ ਕਿਉਂਕਿ ਕਈ queries ਦਾ ਕੰਮ ਇੱਕੋ thread pool ਉੱਤੇ multiplex ਹੁੰਦਾ ਹੈ.
ਸਾਡਾ ਹੱਲ coarse_thread_cputime_clock ਹੈ, ਜੋ clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...) ਦਾ ਇੰਨਾ ਸਸਤਾ approximation ਕਰਦਾ ਹੈ ਕਿ ਹਰ task boundary ਉੱਤੇ sample ਕੀਤਾ ਜਾ ਸਕੇ. timer_create API ਨਾਲ ਸਮੇਂ ਦੇ ਗੁਜ਼ਰਨ ਦੀਆਂ ਕਈ notions, CPU time accumulation ਸਮੇਤ, ਦੇ ਅਧਾਰ ਉੱਤੇ periodic signal delivery schedule ਕੀਤੀ ਜਾ ਸਕਦੀ ਹੈ. ਅਸੀਂ CPU time ਦੇ ਹਰ ਕੁਝ milliseconds ਬਾਅਦ signal (SIGUSR2) deliver ਕਰਨ ਲਈ schedule ਕਰਦੇ ਹਾਂ, ਜਿੱਥੇ signal handler ਇੱਕ thread-local value update ਕਰਦਾ ਹੈ. ਭਾਵੇਂ ਕਈ tasks ਆਪਣੇ execution ਦੌਰਾਨ coarse clock ਅੱਗੇ ਵਧਦਾ ਨਹੀਂ ਵੇਖਦੇ, ਸਾਰੇ deltas ਜੋੜਨ ਨਾਲ query ਦੇ ਅਸਲ CPU time ਦਾ unbiased estimate ਮਿਲਦਾ ਹੈ.
ਕਿਉਂਕਿ ਅਸੀਂ signals ਬਹੁਤ ਵਾਰ deliver ਕਰਦੇ ਹਾਂ, context switching ਜਾਂ signal delivery ਦੇ ਆਲੇ-ਦੁਆਲੇ rare kernel bug ਸੰਭਵ ਲੱਗਿਆ. ਅਸੀਂ bug reports, kernel source code, ਅਤੇ Azure-specific kernel patches ਪੜ੍ਹੇ. ਅਸੀਂ stress tests ਕੀਤੇ. ਸਾਨੂੰ ਸੰਬੰਧਿਤ ਲੱਗਣ ਵਾਲੀ ਕੋਈ ਚੀਜ਼ ਨਹੀਂ ਮਿਲੀ.
ਉਸ ਮੋੜ ਉੱਤੇ ਅਸੀਂ ਪਿੱਛੇ ਹਟ ਕੇ ਵੱਖਰੀ ਪਹੁੰਚ ਅਜ਼ਮਾਉਣ ਦਾ ਫੈਸਲਾ ਕੀਤਾ.
ਇਹੋ ਜਿਹੀ ਸਮੱਸਿਆ debug ਕਰਨ ਦੇ ਦੋ ਵੱਡੇ ਤਰੀਕੇ ਹਨ.
ਇੱਕ ਹੈ doctor ਵਾਂਗ ਕੰਮ ਕਰਨਾ: ਇੱਕ patient ਉੱਤੇ ਧਿਆਨ, ਬਹੁਤ tests, ਅਤੇ detailed evidence ਤੋਂ ਇੱਕ case diagnose ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼.
ਦੂਜਾ ਹੈ epidemiologist ਵਾਂਗ ਕੰਮ ਕਰਨਾ: ਪੂਰੀ population ਵੇਖੋ ਅਤੇ ਪੁੱਛੋ ਕਿ ਕੀ ਉਹ patterns ਹਨ ਜੋ ਇੱਕ case ਨਹੀਂ ਦਿਖਾ ਸਕਦਾ. ਕੀ bug ਕਿਸੇ ਖਾਸ release ਉੱਤੇ ਸ਼ੁਰੂ ਹੋਇਆ? ਕੀ ਇਹ ਇੱਕ hardware SKU, ਯਾਨੀ ਖਾਸ CPU ਅਤੇ server model, ਇੱਕ region ਜਾਂ ਇੱਕ kernel version ਨਾਲ correlate ਕਰਦਾ ਹੈ? ਕੀ ਇੱਕ syndrome ਵਰਗਾ ਦਿਸਣ ਵਾਲੇ ਅੰਦਰ ਕਈ ਵੱਖਰੇ clusters ਲੁਕੇ ਹਨ?
ਅਸੀਂ ਜ਼ਿਆਦਾਤਰ doctor mode ਵਿੱਚ ਸੀ. ਮੁੱਖ ਬਦਲਾਅ ਇਹ ਫੈਸਲਾ ਸੀ ਕਿ ਸਾਨੂੰ high-quality population data ਇਕੱਠਾ ਕਰਨਾ ਹੈ.
ਸਮੱਸਿਆ ਦੇ ਸਾਰੇ instances ਆਪੇ ਲੱਭਣ ਦੀਆਂ ਪਿਛਲੀਆਂ ਕੋਸ਼ਿਸ਼ਾਂ ਇਸ ਲਈ ਨਾਕਾਮ ਰਹੀਆਂ ਕਿਉਂਕਿ ਅਸੀਂ logs ਉੱਤੇ text searches ਵਰਤ ਰਹੇ ਸੀ. Core dumps ਵਿੱਚ ਖੁਦ ਕਾਫੀ ਵੱਧ ਜਾਣਕਾਰੀ ਸੀ, ਪਰ ਉਨ੍ਹਾਂ ਨੂੰ manually ਵੇਖਣਾ scale ਨਹੀਂ ਕਰਦਾ ਸੀ. ਅਸੀਂ ਇੱਕ pipeline ਬਣਾਉਣ ਦੀ ਮਿਹਨਤ ਕਰਨ ਦਾ ਫੈਸਲਾ ਕੀਤਾ ਜੋ core dumps ਨੂੰ ਆਪ analyze ਕਰ ਸਕੇ.
ਅਸੀਂ ChatGPT ਤੋਂ script ਲਿਖਵਾਇਆ ਜੋ ਹਰ core file ਦਾ prefix download ਕਰਦਾ, registers ਕੱਢਦਾ, logs ਨਾਲ known false positives filter ਕਰਦਾ, ਅਤੇ crash ਨੂੰ return-to-null, misaligned-stack ਜਾਂ other ਵਜੋਂ label ਕਰਦਾ. ਫਿਰ ਅਸੀਂ ਉਹ script ਪਿਛਲੇ ਸਾਲ ਦੇ ਹਰ production Rockset core dump ਉੱਤੇ parallel ਚਲਾਇਆ.
ਇਹ turning point ਸੀ.
ਜਿਵੇਂ ਹੀ ਸਾਫ data set ਮਿਲਿਆ, correlations ਤੁਰੰਤ ਦਿਸਣ ਲੱਗੇ. ਜਿਸਨੂੰ ਅਸੀਂ ਇੱਕ ਅਜੀਬ bug ਮੰਨ ਰਹੇ ਸੀ, ਉਹ ਅਸਲ ਵਿੱਚ ਦੋ ਵੱਖਰੀਆਂ crash populations ਸਨ.
return-to-null cores ਕਈ clusters ਅਤੇ geographic regions ਵਿੱਚ ਫੈਲੇ ਹੋਏ ਸਨ. ਉਨ੍ਹਾਂ ਦੀ frequency ਹਾਲ ਹੀ ਵਿੱਚ ਵਧੀ ਸੀ, ਪਰ ਨਾ ਕੋਈ ਸਾਫ start date ਸੀ ਨਾ ਕੋਈ clean infrastructure boundary.
misaligned-stack crashes ਬਿਲਕੁਲ ਵੱਖਰੇ ਲੱਗੇ. ਉਹ ਸਾਰੇ ਇੱਕ region ਤੋਂ ਆਏ, ਉਨ੍ਹਾਂ ਦੀ clear start date ਸੀ, ਅਤੇ ਉਹ ਲੰਮੇ ਸਮੇਂ ਤੋਂ ਚੱਲ ਰਹੇ ਨੋਡਾਂ ਉੱਤੇ ਕਦੇ ਨਹੀਂ ਹੋਏ. ਭਾਵੇਂ ਉਹ ਕਈ Azure VMs, ਯਾਨੀ cloud-hosted virtual machines, ਨਾਲ ਜੁੜੇ ਸਨ, pattern ਇਉਂ ਲੱਗਿਆ ਜਿਵੇਂ bad hardware ਵਾਲੀ ਇੱਕ physical machine ਜਿਸ ਵੀ VM ਨੂੰ ਮਿਲੀ, ਉਸ ਲਈ ਸਮੱਸਿਆ ਬਣਾ ਰਹੀ ਸੀ.
ਇਹੀ ਪਲ ਸੀ ਜਦੋਂ ਸਾਨੂੰ ਅਹਿਸਾਸ ਹੋਇਆ ਕਿ ਅਸੀਂ ਮਨ ਵਿੱਚ ਦੋ bugs ਨੂੰ ਮਿਲਾ ਰਹੇ ਸੀ. ਕਿਉਂਕਿ ਅਸੀਂ ਦੋਵੇਂ bugs ਦੇ counterexamples ਮਿਲਾ ਰਹੇ ਸੀ, ਇੱਕੋ coherent explanation ਨਹੀਂ ਲੱਭ ਸਕੇ.
Kubernetes ਨੋਡਾਂ ਅਤੇ timestamps ਦੀ ਸਾਫ list ਨਾਲ ਅਸੀਂ misaligned-stack crashes ਨੂੰ ਇੱਕ physical host ਤੱਕ trace ਕਰ ਸਕੇ, ਜਿਸਨੂੰ denylist ਕਰਨਾ ਆਸਾਨ ਸੀ.
ਕਈ ਹਫ਼ਤਿਆਂ ਦੀ stress testing ਤੋਂ ਬਾਅਦ ਵੀ ਅਸੀਂ ਉਸ host ਉੱਤੇ register corruption ਨੂੰ controlled environment ਵਿੱਚ reproduce ਨਹੀਂ ਕਰ ਸਕੇ. ਪਰ ਜਦੋਂ problematic host ਨੂੰ service ਤੋਂ ਕੱਢਿਆ ਗਿਆ, misaligned-stack crashes ਗਾਇਬ ਹੋ ਗਏ.
Bad host ਹਟਾਉਣਾ permanent solution ਨਹੀਂ, ਇਸ ਅਰਥ ਵਿੱਚ ਕਿ ਇਹ same problem ਦੀ ਨਵੀਂ occurrence ਨਹੀਂ ਰੋਕਦਾ. ਪਰ ਅਸੀਂ software ਬਦਲ ਸਕਦੇ ਹਾਂ ਤਾਂ ਜੋ similar issue ਦੁਬਾਰਾ ਆਵੇ ਤਾਂ ਆਸਾਨੀ ਨਾਲ detect ਅਤੇ handle ਹੋ ਸਕੇ. ਅਸੀਂ fatal signal handler ਨੂੰ register state ਸ਼ਾਮਲ ਕਰਨ ਲਈ ਸੁਧਾਰਿਆ, ਤਾਂ ਜੋ recurrence ਸਿਰਫ logs ਤੋਂ detect ਹੋ ਸਕੇ, core dump ਦੀ ਲੋੜ ਨਾ ਹੋਵੇ. ਅਸੀਂ control plane ਬਦਲਿਆ ਤਾਂ ਜੋ VMs ਆਮ ਤੌਰ ਉੱਤੇ recycle ਦੀ ਥਾਂ reuse ਹੋਣ, ਜਿਸ ਨਾਲ infrastructure stack ਦੇ ਸਾਡੇ level ਉੱਤੇ bad-ਨੋਡ detection ਕਾਫੀ ਆਸਾਨ ਹੋ ਜਾਂਦੀ ਹੈ. ਅਸੀਂ ਆਪਣੇ runbooks ਅਤੇ team ਦੇ mental models ਨੂੰ ਵੀ ਇਸ possibility ਨਾਲ update ਕੀਤਾ.
Bad-host crashes ਵੱਖ ਕਰ ਦੇਣ ਨਾਲ remaining return-to-null cores ਨੂੰ ਸਮਝਣਾ ਕਾਫੀ ਆਸਾਨ ਹੋ ਗਿਆ. ਪਹਿਲਾਂ ਅਸੀਂ exception unwinding ਰੱਦ ਕਰ ਦਿੱਤੀ ਸੀ ਕਿਉਂਕਿ ਸਾਨੂੰ counterexamples ਲੱਗੇ: ਉਹ code paths ਜਿੱਥੇ exceptions ਯਕੀਨੀ ਤੌਰ ਉੱਤੇ ਵਰਤੇ ਨਹੀਂ ਗਏ. ਪਰ ਉਹ counterexamples ਸਾਰੇ hardware-corruption cluster ਤੋਂ ਸਨ.
ਇਹ ਧਿਆਨ ਵਿੱਚ ਰੱਖ ਕੇ ਜਦੋਂ ਅਸੀਂ remaining cores ਮੁੜ ਵੇਖੇ, ਤਾਂ ਪਤਾ ਲੱਗਾ ਕਿ ਨਤੀਜਾ ਬਿਲਕੁਲ ਉਲਟ ਸੀ: ਸਾਰੇ crashes exception unwinding ਦੌਰਾਨ ਹੋ ਰਹੇ ਸਨ.
ਜਦੋਂ C++ exception throw ਕਰਦਾ ਹੈ, runtime ਨੂੰ ਪਤਾ ਲਗਾਉਣਾ ਪੈਂਦਾ ਹੈ ਕਿ ਕਿਹੜਾ catch block ਇਸਨੂੰ ਲਵੇ ਅਤੇ ਰਾਹ ਵਿੱਚ ਕਿਹੜੇ destructors ਜਾਂ cleanup handlers ਚੱਲਣ. Compiler ਇਹ metadata emit ਕਰਦਾ ਹੈ, ਪਰ actual matching runtime ਉੱਤੇ dynamically ਹੁੰਦੀ ਹੈ.
Exception unwinding ਅਸਲ ਵਿੱਚ throw invoke ਕਰਨ ਵਾਲਾ function ਨਹੀਂ ਕਰਦਾ, ਬਲਕਿ resulting compiled code ਵੱਲੋਂ call ਕੀਤੇ helper functions ਕਰਦੇ ਹਨ. ਉਹ runtime routines stack examine ਕਰਦੀਆਂ ਹਨ, stack ਉੱਤੇ ਮਿਲੇ functions ਬਾਰੇ metadata ਲੈਂਦੀਆਂ ਹਨ, cleanup handlers ਅਤੇ catch blocks dynamically ਲੱਭਦੀਆਂ ਹਨ, ਫਿਰ control ਉਨ੍ਹਾਂ ਵਿੱਚੋਂ ਕਿਸੇ location ਨੂੰ transfer ਕਰਦੀਆਂ ਹਨ. Control transfer ਵਿੱਚ ਦਰਮਿਆਨੀ ਸਾਰੀਆਂ stack frames unwind ਕਰਨਾ ਸ਼ਾਮਲ ਹੈ, helper functions ਵਾਲੀਆਂ ਵੀ.
Operationally, ਇਹ normal call ਅਤੇ return ਨਾਲੋਂ longjmp ਜਾਂ fiber switch ਦੇ ਕਾਫੀ ਨੇੜੇ ਹੈ. Callee save registers ਦੇ ਨਾਲ stack frame registers %rbp ਅਤੇ %rsp ਵੀ restore ਹੋਣੇ ਚਾਹੀਦੇ ਹਨ.
ਸਾਡਾ binary ਦੋ libraries ਨਾਲ link ਕਰਦਾ ਹੈ ਜਿਨ੍ਹਾਂ ਵਿੱਚ C++ exception unwinding ਕਰਨ ਵਾਲੇ functions ਦੀਆਂ implementations ਹਨ: libgcc ਅਤੇ GNU libunwind. Dynamic linker ਨੇ GNU libunwind ਦੀਆਂ definitions ਚੁਣੀਆਂ. ਇਹ ਸਾਨੂੰ ਹੈਰਾਨੀਜਨਕ ਲੱਗਿਆ; ਅਸੀਂ symbol versioning rules ਕਰਕੇ libgcc implementation ਦੇ ਜਿੱਤਣ ਦੀ ਉਮੀਦ ਕੀਤੀ ਸੀ; ਪਰ running binaries ਵੇਖਣ ਨਾਲ ਪਤਾ ਲੱਗਿਆ ਕਿ ਗੱਲ ਐਸੀ ਨਹੀਂ ਸੀ.
ਇਸ ਮੋੜ ਉੱਤੇ ਸਾਡੀ working hypothesis ਬਦਲੀ, ਕਿਉਂਕਿ ਅਸੀਂ ਹੋਰ ਇੱਕ assumption ਢਿੱਲੀ ਕੀਤੀ ਜੋ ਇੱਕੋ bug ਮੰਨਦੇ ਸਮੇਂ ਬਣਾਈ ਸੀ.
ਸ਼ਾਇਦ ਅਸੀਂ ordinary function ਨੂੰ NULL ਉੱਤੇ return ਹੁੰਦਾ ਨਹੀਂ ਦੇਖ ਰਹੇ ਸੀ. ਸ਼ਾਇਦ ਅਸੀਂ unwind transfer ਦੇਖ ਰਹੇ ਸੀ, ਅਸਲ ਵਿੱਚ setcontext-style register restore, ਜਿੱਥੇ destination instruction pointer control transfer ਤੋਂ ਪਹਿਲਾਂ NULL ਹੋ ਗਿਆ ਸੀ. ਦੂਜੇ ਸ਼ਬਦਾਂ ਵਿੱਚ, stack ਉੱਤੇ incorrect return address slot ਦੀ ਥਾਂ unwind library ਤੋਂ incorrect data.
ਇਸ ਨਾਲ problem ਨਾਟਕੀ ਤੌਰ ਉੱਤੇ narrow ਹੋ ਗਈ. ਜਾਂ GNU libunwind ਗਲਤ destination state compute ਕਰ ਰਿਹਾ ਸੀ, ਜਾਂ ਸਹੀ state compute ਕਰਕੇ apply ਹੋਣ ਤੋਂ ਪਹਿਲਾਂ ਕੁਝ ਉਸਨੂੰ corrupt ਕਰ ਰਿਹਾ ਸੀ.
ਅਸੀਂ GNU libunwind source ਪੜ੍ਹਿਆ ਅਤੇ ਵੇਖਿਆ ਕਿ ਇਹ stack ਉੱਤੇ ucontext_t synthesize ਕਰਦਾ ਹੈ, cleanup handler ਦੇ frame ਲਈ desired register state ਭਰਦਾ ਹੈ, ਫਿਰ ਉਸ struct ਦਾ pointer ਇੱਕ internal assembly routine ਨੂੰ ਦਿੰਦਾ ਹੈ: _Ux86_64_setcontext.
ਇਸ ਮੋੜ ਉੱਤੇ ਸਾਡੇ ਕੋਲ ਸਾਰੇ pieces ਸਨ.
Synthesized ucontext_t ਉਹਨਾਂ stack frames ਵਿੱਚੋਂ ਇੱਕ ਵਿੱਚ ਰਹਿੰਦਾ ਹੈ ਜੋ _Ux86_64_setcontext ਉਸ function ਦੀ execution ਦੌਰਾਨ unwind ਕਰਦਾ ਹੈ. ਕੀ _Ux86_64_setcontext, %rsp ਬਦਲਣ ਤੋਂ ਬਾਅਦ struct ਤੋਂ ਪੜ੍ਹ ਰਿਹਾ ਸੀ, ਜਦੋਂ struct active stack ਦਾ ਹਿੱਸਾ ਨਹੀਂ ਰਹਿੰਦਾ? ਇਸ ਨਾਲ ਇਹ signal delivery, ਜਿਵੇਂ ਸਾਡਾ frequent SIGUSR2, ਵੱਲੋਂ clobber ਹੋਣ ਲਈ vulnerable ਬਣ ਜਾਂਦਾ.
ਜਵਾਬ ਹਾਂ ਸੀ.
GNU libunwind ਦੇ ਜਿਸ version ਦੀ ਅਸੀਂ ਵਰਤੋਂ ਕਰ ਰਹੇ ਸੀ, ਉਸ ਵਿੱਚ _Ux86_64_setcontext ਦੀਆਂ ਆਖਰੀ ਛੇ instructions ਇਹ ਹਨ, ਜਿਨ੍ਹਾਂ ਵਿੱਚ ਜ਼ਿਆਦਾਤਰ mov instructions ਹਨ ਜੋ memory ਤੋਂ destination register ਵਿੱਚ load ਕਰਦੀਆਂ ਹਨ:
(%rdi stack-allocated ucontext_t ਵੱਲ point ਕਰਦਾ ਹੈ, ਅਤੇ UC_MCONTEXT_* macros ਸਿਰਫ ਉਸ fixed offset ਵਿੱਚ expand ਹੁੰਦੇ ਹਨ ਜਿੱਥੇ particular register stored ਹੁੰਦਾ ਹੈ.)
ਪਹਿਲੀ instruction race window ਦੀ ਸ਼ੁਰੂਆਤ ਹੈ. ਇਹ %rsp ਨੂੰ active stack ਦੇ ਨਵੇਂ bottom ਵੱਲ point ਕਰਨ ਲਈ update ਕਰਦੀ ਹੈ. ਜਿਵੇਂ ਹੀ ਇਹ ਹੁੰਦਾ ਹੈ, %rdi ਵੱਲੋਂ point ਕੀਤਾ struct active stack ਜਾਂ red zone ਦਾ ਹਿੱਸਾ ਨਹੀਂ ਰਹਿੰਦਾ, ਅਤੇ kernel ਲਈ off-limits ਨਹੀਂ ਰਹਿੰਦਾ.
ਆਮ ਤੌਰ ਉੱਤੇ ਇਸ ਨਾਲ problem ਨਹੀਂ ਬਣਦੀ, ਪਰ ਜੇ signal ਬਿਲਕੁਲ ਸਹੀ, ਜਾਂ ਗਲਤ, ਪਲ ਉੱਤੇ ਆ ਜਾਵੇ, kernel %rsp-128 ਉੱਤੇ signal frame ਬਣਾਏਗਾ. ਇਹ %rdi ਵੱਲੋਂ point ਕੀਤੀ memory overwrite ਕਰ ਸਕਦਾ ਹੈ.
ਜੇ ਅਗਲੀ instruction UC_MCONTEXT_GREGS_RIP(%rdi) ਪੜ੍ਹਣ ਤੋਂ ਪਹਿਲਾਂ ਇਹ ਹੋ ਜਾਵੇ, ਤਾਂ restored instruction pointer corrupt ਹੋ ਸਕਦਾ ਹੈ. ਸਾਡੇ crashes ਵਿੱਚ, ਇਹ NULL ਹੋ ਗਿਆ.
ਬੱਗ ਇਹੀ ਹੈ.
ਇਹ assembly ਸਾਡੀ ਇੱਕ ਉਲਝਣ ਵੀ ਸਮਝਾਉਂਦੀ ਹੈ: preceding stack frame ਦੇ return address slot ਵਿੱਚ function X ਕੋਲ NULL ਕਿਉਂ ਸੀ.
setcontext ਸਭ registers, %rdi ਸਮੇਤ, restore ਕਰਨ ਲਈ ਲਿਖਿਆ ਗਿਆ ਸੀ, ਇਸ ਲਈ control transfer ਦੇ ਆਖਰੀ ਪਲ ਉੱਤੇ UC_MCONTEXT_GREGS_RIP(%rdi) ਪੜ੍ਹਨ ਲਈ ਇਹ ਉਹ register ਨਹੀਂ ਵਰਤ ਸਕਦਾ. ਇਸ ਦੀ ਥਾਂ, ਇਹ value ਪਹਿਲਾਂ ਪੜ੍ਹਦਾ ਹੈ, stack ਉੱਤੇ save ਕਰਦਾ ਹੈ, ਕੁਝ ਹੋਰ registers restore ਕਰਦਾ ਹੈ, ਫਿਰ saved value ਪੜ੍ਹ ਕੇ control transfer ਕਰਨ ਲਈ retq ਵਰਤਦਾ ਹੈ.
Cores ਵਿੱਚ ਜੋ “function NULL ਉੱਤੇ return ਹੋਇਆ” ਲੱਗਦਾ ਸੀ, ਉਹ ਅਸਲ ਵਿੱਚ “unwinder ਨੇ stack ਉੱਤੇ target return address ਬਣਾਇਆ, ਪਰ transfer ਮੁਕੰਮਲ ਹੋਣ ਤੋਂ ਪਹਿਲਾਂ target corrupt ਹੋ ਗਿਆ” ਸੀ. ਅਸੀਂ ਮੰਨਿਆ ਸੀ ਕਿ return address slot ਦੀ corruption ਓਥੇ ਹੀ ਹੋਣੀ ਚਾਹੀਦੀ ਹੈ, ਕਿਉਂਕਿ ਸਾਨੂੰ ਕੋਈ ਥਾਂ ਨਹੀਂ ਪਤਾ ਸੀ ਜਿੱਥੇ corruptible data ਜਾਨਬੁੱਝ ਕੇ return address slot ਵਿੱਚ ਲਿਖਿਆ ਜਾਂਦਾ ਹੋਵੇ.
ਇਸ bug ਨੂੰ absurd ਬਣਾਉਂਦੀ ਗੱਲ ਇਹ ਹੈ ਕਿ ਇਹ race window ਕਿੰਨੀ narrow ਹੈ. ਇਸ ਕਿਸਮ ਦੀ race condition ਵਿੱਚ external event, ਯਾਨੀ signal, ਦੂਜੇ thread ਦੇ ਦੋ steps ਦੇ ਵਿਚਕਾਰ ਹੋਣਾ ਲਾਜ਼ਮੀ ਹੈ. ਉਹ steps ਜਿੰਨੇ ਨੇੜੇ ਹੋਣ, race condition ਹੋਣ ਦੀ ਸੰਭਾਵਨਾ ਉਨੀ ਘੱਟ ਹੁੰਦੀ ਹੈ.
ਇਸ case ਵਿੱਚ vulnerable window ਸੱਚਮੁੱਚ ਸਿਰਫ ਇੱਕ instruction ਚੌੜੀ ਹੈ! Signal %rsp ਬਦਲਣ ਤੋਂ ਬਾਅਦ ਪਰ ਅਗਲੀ instruction ਵੱਲੋਂ %rip load ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ deliver ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ. ਆਧੁਨਿਕ super-scalar out-of-order CPU ਉੱਤੇ ਇਹੋ ਜਿਹੀਆਂ ਕਈ simple instructions ਪ੍ਰਤੀ cycle ਚੱਲ ਸਕਦੀਆਂ ਹਨ, ਇਸ ਲਈ race window ਲਗਭਗ ਸੌ picoseconds ਹੈ.
ਜਦੋਂ ਸਾਨੂੰ ਇਹ race ਮਿਲੀ, ਸਾਡੀ ਪਹਿਲੀ ਪ੍ਰਤੀਕਿਰਿਆ ਸੀ ਕਿ ਇਹ observed crash rate ਸਮਝਾਉਣ ਲਈ ਬਹੁਤ rare ਹੋਵੇਗੀ. Fleet ਭਰ ਵਿੱਚ ਸਾਨੂੰ ਰੋਜ਼ ਦਰਜਨ ਤੋਂ ਵੱਧ return-to-null crashes ਦਿਖ ਰਹੇ ਸਨ. ਕੀ exception cleanup ਦੌਰਾਨ ਇੱਕ-instruction race ਸੱਚਮੁੱਚ ਇਹ ਸਭ ਸਮਝਾ ਸਕਦੀ ਸੀ?
ਅਸੀਂ Fermat estimation ਵੱਲ ਮੁੜੇ. ਜੇ vulnerable window seconds ਦੇ order ਦੀ ਹੈ ਅਤੇ SIGUSR2 ਹਰ seconds CPU time ਉੱਤੇ ਆਉਂਦਾ ਹੈ, ਤਾਂ ਹਰ exception cleanup handler ਜਾਂ catch block ਲਈ race ਹਾਰਨ ਦੀ probability ਲਗਭਗ ਹੈ.
Rockset exceptions ਨੂੰ ਆਪਣੇ internal ingest backpressure mechanism ਦੇ ਹਿੱਸੇ ਵਜੋਂ ਵਰਤਦਾ ਹੈ. ਇੱਕ overloaded host ਪ੍ਰਤੀ second ਲਗਭਗ exceptions throw ਕਰ ਸਕਦਾ ਹੈ. ਇਸ ਤੋਂ ਨਿਕਲਦਾ ਹੈ ਕਿ backpressure ਵਰਤਦੇ host ਦਾ mean time between failures seconds ਹੈ, ਯਾਨੀ ਹਰ ਕੁਝ ਘੰਟਿਆਂ ਵਿੱਚ ਇੱਕ crash. Fleet scale ਉੱਤੇ ਇਹ observed crash frequency ਸਮਝਾਉਣ ਲਈ ਕਾਫੀ ਤੋਂ ਵੱਧ ਹੈ.
GNU libunwind bug ਪੁਰਾਣਾ ਹੈ, 18 ਸਾਲ ਤੋਂ ਵੀ ਵੱਧ, ਅਤੇ ਪਹਿਲੇ x86_64 version ਵਿੱਚ ਮੌਜੂਦ ਸੀ ਜਿਸ ਨੇ C++ exception unwinding support ਕੀਤਾ.
ਤਾਂ ਇਹ ਹੁਣ ਕਿਉਂ ਸਾਹਮਣੇ ਆਇਆ?
Crash rate ਲਗਭਗ thrown exceptions ਅਤੇ delivered signals ਦੀ ਗਿਣਤੀ ਦੇ ਅਨੁਪਾਤ ਵਿੱਚ ਹੈ. ਇਹ ਇਸ ਉੱਤੇ ਵੀ ਨਿਰਭਰ ਹੈ ਕਿ signal handler ਕਿੰਨਾ stack ਵਰਤਦਾ ਹੈ.
Rockset ਤਿੰਨਾਂ axes ਉੱਤੇ unusual ਹੈ. ਅਸੀਂ normal overload control ਦੇ ਹਿੱਸੇ ਵਜੋਂ high rates ਉੱਤੇ exceptions throw ਕਰਦੇ ਹਾਂ; coarse_thread_cputime_clock ਕਰਕੇ SIGUSR2 ਅਸਧਾਰਣ ਤੌਰ ਉੱਤੇ ਵਾਰ-ਵਾਰ deliver ਕਰਦੇ ਹਾਂ; ਅਤੇ ਇਸ ਸਾਲ ਪਹਿਲਾਂ merged signals account ਕਰਨ ਲਈ timer_getoverrun call ਜੋੜ ਕੇ SIGUSR2 handler ਨੂੰ ਹੋਰ stack ਵਰਤਣ ਲਾਇਆ.
ਉਹ ਆਖਰੀ change ਮਹੱਤਵਪੂਰਣ ਲੱਗਦੀ ਹੈ. ਜੇ handler ਬਹੁਤ ਘੱਟ stack ਵਰਤੇ, ਤਾਂ ਇਹ stale ucontext_t memory ਤੱਕ ਪਹੁੰਚ ਕੇ overwrite ਨਹੀਂ ਕਰ ਸਕਦਾ. ਉਸ change ਤੋਂ ਪਹਿਲਾਂ ਸਾਨੂੰ ਇਹ crashes ਬਿਲਕੁਲ ਨਹੀਂ ਦਿਖੇ. Change ਤੋਂ ਬਾਅਦ rate ਘੱਟ ਰਹੀ, ਜਦ ਤੱਕ ਅਸੀਂ ਕੁਝ use cases ਲਈ load ਨਹੀਂ ਵਧਾਇਆ ਜਿਨ੍ਹਾਂ ਨੇ backpressure mechanism ਨੂੰ stress ਕੀਤਾ.
ਦੂਜੇ ਸ਼ਬਦਾਂ ਵਿੱਚ, libunwind bug ਹਮੇਸ਼ਾਂ ਸੀ, ਪਰ exception rate, signal rate ਅਤੇ handler stack usage ਦਾ ਗੁਣਨਫਲ ਹਾਲ ਹੀ ਵਿੱਚ ਉਸ threshold ਤੋਂ ਪਾਰ ਹੋਇਆ ਜਿੱਥੇ ਇਹ operations ਵਿੱਚ ਦਿਸਣ ਲੱਗਾ.
ਇਹ mechanism ਇਹ coincidence ਵੀ ਸਮਝਾਉਂਦਾ ਹੈ ਕਿ hardware bug ਅਤੇ libunwind bug ਦੋਵੇਂ mostly DocumentTree::updateDocument ਅੰਦਰ crash ਹੋਏ. libunwind ਵਾਲੇ crashes ਇਸ method ਵੱਲ ਜ਼ੋਰ ਨਾਲ biased ਸਨ, ਕਿਉਂਕਿ ingest backpressure ਲਾਗੂ ਕਰਨ ਲਈ exception throw ਕਰਨ ਵੇਲੇ ਇਹ ਹਮੇਸ਼ਾਂ active ਹੁੰਦੀ ਹੈ. %rsp-misalignment crashes ਲਈ ਵੀ ਇਹੀ ਬਹੁਤ select ਹੋਈ, ਕਿਉਂਕਿ bad hardware ਨੋਡ ਉਸ SKU ਦਾ ਸੀ ਜੋ ਅਸੀਂ bulk ingest ਲਈ ਵਰਤਦੇ ਹਾਂ, ਅਤੇ ਜੋ ਆਪਣਾ ਜ਼ਿਆਦਾਤਰ CPU time ਇਸ method ਵਿੱਚ ਲਗਾਉਂਦਾ ਹੈ.
ਸਾਡੀ ਤੁਰੰਤ mitigation GNU libunwind ਤੋਂ libgcc ਦੇ unwinder ਉੱਤੇ switch ਕਰਨਾ ਸੀ. ਇਹ ਆਪਣੇ ਆਪ ਵਿੱਚ ਵਧੀਆ trade ਸੀ: libgcc ਦੀ implementation ਨੇ lock contention ਘਟਾਉਣ ਲਈ ਕਾਫੀ ਕੰਮ ਤੋਂ ਲਾਭ ਲਿਆ ਹੈ, ਜੋ large VMs ਤੱਕ scale ਕਰਦਿਆਂ ਮਹੱਤਵਪੂਰਣ ਹੈ.
ਅਸੀਂ GNU libunwind ਵਿੱਚ self-contained reproducer ਅਤੇ fix(ਨਵੀਂ ਵਿੰਡੋ ਵਿੱਚ ਖੁੱਲ੍ਹਦਾ ਹੈ) ਵੀ upstream ਕੀਤਾ, ਅਤੇ verify ਕੀਤਾ ਕਿ ਹੋਰ unwinders ਵਿੱਚ similar issue ਨਹੀਂ ਹੈ.
ਇਸ debugging ਯਾਤਰਾ ਨੇ ਸਾਨੂੰ dynamic linking, DWARF unwind metadata, Linux signal delivery, System V ABI, ਅਤੇ C++ exception machinery ਦੀਆਂ ਖਾਸ details ਬਾਰੇ ਕਾਫੀ ਕੁਝ ਸਿਖਾਇਆ. ਪਰ ਮੁੱਖ ਸਬਕ ਇਹ ਸਭ ਤੋਂ ਸੌਖਾ ਸੀ.
ਸਭ ਤੋਂ ਮਹੱਤਵਪੂਰਣ ਕਦਮ clever assembly reading ਜਾਂ details ਦਾ ਡੂੰਘਾ ਗਿਆਨ ਨਹੀਂ ਸੀ. ਉਹ ਸੀ high-quality data set ਬਣਾਉਣਾ. ਇਸ data set ਤੋਂ ਬਿਨਾ, ਅਸੀਂ ਦੋ ਵੱਖਰੀਆਂ phenomena ਨੂੰ ਇੱਕ ਕਹਾਣੀ ਵਿੱਚ ਮਿਲਾ ਰਹੇ ਸੀ ਅਤੇ confusion ਤੋਂ reasoning ਨਾਲ ਨਿਕਲਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਸੀ. ਜਦੋਂ ਸਾਡੇ ਕੋਲ accurate ਅਤੇ complete population data ਆ ਗਿਆ, ਸਮੱਸਿਆ ਦੀ structure ਸਾਫ ਹੋ ਗਈ: ਇੱਕ crash population bad host ਦੀ ਸੀ, ਅਤੇ ਦੂਜੀ libunwind ਦੀ race ਦੀ. ਜਿਵੇਂ data ਬਿਹਤਰ ਹੋਇਆ, debugging ਆਸਾਨ ਹੋ ਗਈ.
Rockset ਵਰਗੇ infrastructure systems ਲਈ ਇਹ ਬਹੁਤ ਮਾਇਨੇ ਰੱਖਦਾ ਹੈ. ਇਸ investigation ਨੇ deep instrumentation, automated investigations, ਅਤੇ operational tooling ਵਿੱਚ ਲਗਾਤਾਰ ਸੁਧਾਰਾਂ ਲਈ ਸਾਡੀ commitment ਮਜ਼ਬੂਤ ਕੀਤੀ. Reliability ਸਿਰਫ bugs ਹੋਣ ਤੋਂ ਬਾਅਦ ਉਨ੍ਹਾਂ ਨੂੰ fix ਕਰਨ ਦਾ ਨਾਮ ਨਹੀਂ. ਇਹ ਉਹ data, workflows ਅਤੇ skills ਬਣਾਉਣ ਬਾਰੇ ਹੈ ਜੋ ਅਸੰਭਵ problems ਨੂੰ diagnosable ਅਤੇ solvable ਬਣਾਉਂਦੇ ਹਨ.
ਲੇਖਕ
By Nathan Bronson, Member of Technical Staff


