Probe:Android線上OOM問題定位組件

體現App穩定性的一個重要數據就是Crash率,而在眾多Crash中最棘手最難定位的就是OOM問題。本文主要分享美團的Probe組件是如何對線上OOM問題進行快速定位的。

背景

配送騎手端App是騎手用於完成配送履約的應用,幫助騎手完成接單、到店、取貨及送達,提供各種不同的運力服務,也是整個外賣閉環中的重要節點。由於配送業務的特性,騎手App對於應用穩定性的要求非常高,體現App穩定性的一個重要數據就是Crash率,而在眾多Crash中最棘手最難定位的就是OOM問題。對於騎手端App而言,每天騎手都會長時間的使用App進行配送,而在長時間的使用過程中,App中所有的內存洩漏都會慢慢累積在內存中,最後就容易導致OOM,從而影響騎手的配送效率,進而影響整個外賣業務。

於是我們構建了用於快速定位線上OOM問題的組件——Probe,下圖是Probe組件架構,本文主要分享Probe組件是如何對線上OOM問題進行快速定位的。

Probe:Android線上OOM問題定位組件

OOM原因分析

要定位OOM問題,首先需要弄明白Android中有哪些原因會導致OOM,Android中導致OOM的原因主要可以劃分為以下幾個類型:

Probe:Android線上OOM問題定位組件

Android 虛擬機最終拋出OutOfMemoryError的代碼位於/art/runtime/thread.cc。

void Thread::ThrowOutOfMemoryError(const char* msg)
參數 msg 攜帶了 OOM 時的錯誤信息

下面兩個地方都會調用上面方法拋出OutOfMemoryError錯誤,這也是Android中發生OOM的主要原因。

堆內存分配失敗

系統源碼文件:/art/runtime/gc/heap.cc

void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type)
拋出時的錯誤信息:
oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";

這是在進行堆內存分配時拋出的OOM錯誤,這裡也可以細分成兩種不同的類型:

  1. 為對象分配內存時達到進程的內存上限。由Runtime.getRuntime.MaxMemory()可以得到Android中每個進程被系統分配的內存上限,當進程佔用內存達到這個上限時就會發生OOM,這也是Android中最常見的OOM類型。
  2. 沒有足夠大小的連續地址空間。這種情況一般是進程中存在大量的內存碎片導致的,其堆棧信息會比第一種OOM堆棧多出一段信息:failed due to fragmentation (required continguous free "<< required_bytes << " bytes for a new buffer where largest contiguous free " << largest_continuous_free_pages << " bytes)"; 其詳細代碼在art/runtime/gc/allocator/rosalloc.cc中,這裡不作詳述。

創建線程失敗

系統源碼文件:/art/runtime/thread.cc

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon)
拋出時的錯誤信息:
"Could not allocate JNI Env"
或者
StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));

這是創建線程時拋出的OOM錯誤,且有多種錯誤信息。源碼這裡不展開詳述了,下面是根據源碼整理的Android中創建線程的步驟,其中兩個關鍵節點是創建JNIEnv結構體和創建線程,而這兩步均有可能拋出OOM。

Probe:Android線上OOM問題定位組件

創建JNI失敗

創建JNIEnv可以歸為兩個步驟:

  • 通過Andorid的匿名共享內存(Anonymous Shared Memory)分配 4KB(一個page)內核態內存。
  • 再通過Linux的mmap調用映射到用戶態虛擬內存地址空間。

第一步創建匿名共享內存時,需要打開/dev/ashmem文件,所以需要一個FD(文件描述符)。此時,如果創建的FD數已經達到上限,則會導致創建JNIEnv失敗,拋出錯誤信息如下:

E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
java.lang.OutOfMemoryError: Could not allocate JNI Env
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:730)

第二步調用mmap時,如果進程虛擬內存地址空間耗盡,也會導致創建JNIEnv失敗,拋出錯誤信息如下:

E/art: Failed anonymous mmap(0x0, 8192, 0x3, 0x2, 116, 0): Operation not permitted. See process maps in the log.
java.lang.OutOfMemoryError: Could not allocate JNI Env
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1063)

創建線程失敗

創建線程也可以歸納為兩個步驟:

  1. 調用mmap分配棧內存。這裡mmap flag中指定了MAP_ANONYMOUS,即匿名內存映射。這是在Linux中分配大塊內存的常用方式。其分配的是虛擬內存,對應頁的物理內存並不會立即分配,而是在用到的時候觸發內核的缺頁中斷,然後中斷處理函數再分配物理內存。
  2. 調用clone方法進行線程創建。

第一步分配棧內存失敗是由於進程的虛擬內存不足,拋出錯誤信息如下:

W/libc: pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory
W/tch.crowdsourc: Throwing OutOfMemoryError with VmSize 4191668 kB "pthread_create (1040KB stack) failed: Try again"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:753)

第二步clone方法失敗是因為線程數超出了限制,拋出錯誤信息如下:

W/libc: pthread_create failed: clone failed: Out of memory
W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1078)

OOM問題定位

在分析清楚OOM問題的原因之後,我們對於線上的OOM問題就可以做到對症下藥。而針對OOM問題,我們可以根據堆棧信息的特徵來確定這是哪一個類型的OOM,下面分別介紹使用Probe組件是如何去定位線上發生的每一種類型的OOM問題的。

堆內存不足

Android中最常見的OOM就是Java堆內存不足,對於堆內存不足導致的OOM問題,發生Crash時的堆棧信息往往只是“壓死駱駝的最後一根稻草”,它並不能有效幫助我們準確地定位到問題。

堆內存分配失敗,通常說明進程中大部分的內存已經被佔用了,且不能被垃圾回收器回收,一般來說此時內存佔用都存在一些問題,例如內存洩漏等。要想定位到問題所在,就需要知道進程中的內存都被哪些對象佔用,以及這些對象的引用鏈路。而這些信息都可以在Java內存快照文件中得到,調用Debug.dumpHprofData(String fileName)函數就可以得到當前進程的Java內存快照文件(即HPROF文件)。所以,關鍵在於要獲得進程的內存快照,由於dump函數比較耗時,在發生OOM之後再去執行dump操作,很可能無法得到完整的內存快照文件。

於是Probe對於線上場景做了內存監控,在一個後臺線程中每隔1S去獲取當前進程的內存佔用(通過Runtime.getRuntime.totalMemory()-Runtime.getRuntime.freeMemory()計算得到),當內存佔用達到設定的閾值時(閾值根據當前系統分配給應用的最大內存計算),就去執行dump函數,得到內存快照文件。

在得到內存快照文件之後,我們有兩種思路,一種想法是直接將HPROF文件回傳到服務器,我們拿到文件後就可以使用分析工具進行分析。另一種想法是在用戶手機上直接分析HPROF文件,將分析完得到的分析結果回傳給服務器。但這兩種方案都存在著一些問題,下面分別介紹我們在這兩種思路的實踐過程中遇到的挑戰和對應的解決方案。

線上分析

首先,我們介紹幾個基本概念:

Probe:Android線上OOM問題定位組件

  • Dominator:從GC Roots到達某一個對象時,必須經過的對象,稱為該對象的Dominator。例如在上圖中,B就是E的Dominator,而B卻不是F的Dominator。
  • ShallowSize
    :對象自身佔用的內存大小,不包括它引用的對象。
  • RetainSize:對象自身的ShallowSize和對象所支配的(可直接或間接引用到的)對象的ShallowSize總和,就是該對象GC之後能回收的內存總和。例如上圖中,D的RetainSize就是D、H、I三者的ShallowSize之和。

JVM在進行GC的時候會進行可達性分析,當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是可回收的。

Github上有一個開源項目HAHA庫,用於自動解析和分析Java內存快照文件(即HPROF文件)。下面是HAHA庫的分析步驟:

Probe:Android線上OOM問題定位組件

於是我們嘗試在App中去新開一個進程使用HAHA庫分析HPROF文件,在線下測試過程中遇到了幾個問題,下面逐一進行敘述。

分析進程自身OOM

測試時遇到的最大問題就是分析進程自身經常會發生OOM,導致分析失敗。為了弄清楚分析進程為什麼會佔用這麼大內存,我們做了兩個對比實驗:

  • 在一個最大可用內存256MB的手機上,讓一個成員變量申請特別大的一塊內存200多MB,人造OOM,Dump內存,分析,內存快照文件達到250多MB,分析進程佔用內存並不大,為70MB左右。
  • 在一個最大可用內存256MB的手機上,添加200萬個小對象(72字節),人造OOM,Dump內存,分析,內存快照文件達到250多MB,分析進程佔用內存增長很快,在解析時就發生OOM了。

實驗說明,分析進程佔用內存與HPROF文件中的Instance數量是正相關的,在將HPROF文件映射到內存中解析時,如果Instance的數量太大,就會導致OOM。

HPROF文件映射到內存中會被解析成Snapshot對象(如下圖所示),它構建了一顆對象引用關係樹,我們可以在這顆樹中查詢各個Object的信息,包括Class信息、內存地址、持有的引用以及被持有引用的關係。

Probe:Android線上OOM問題定位組件

HPROF文件映射到內存的過程:

// 1.構建內存映射的 HprofBuffer 針對大文件的一種快速的讀取方式,其原理是將文件流的通道與 ByteBuffer 建立起關聯,並只在真正發生讀取時才從磁盤讀取內容出來。
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
// 2.構造 Hprof 解析器
HprofParser parser = new HprofParser(buffer);
// 3.獲取快照
Snapshot snapshot = parser.parse();
// 4.去重 gcRoots

deduplicateGcRoots(snapshot);

為了解決分析進程OOM的問題,我們在HprofParser的解析邏輯中加入了計數壓縮邏輯(如下圖所示),目的是在文件映射過程去控制Instance的數量。在解析過程中對於ClassInstance和ArrayInstance,以類型為key進行計數,當同一類型的Instance數量超過閾值時,則不再向Snapshot中添加該類型的Instance,只是記錄Intsance被丟棄的數量和Instance大小。這樣就可以控制住每一種類型的Instance數量,減少了分析進程的內存佔用,在很大程度上避免了分析進程自身的OOM問題。既然我們在解析時丟棄了一部分Instance,後面就得把丟棄的這部分找補回來,所以在計算RetainSize時我們會進行計數桶補償,即把之前丟棄的相同類型的Instance數量和大小都補償到這個對象上,累積去計算RetainSize。

Probe:Android線上OOM問題定位組件

鏈路分析時間過長

在線下測試過程中還遇到了一個問題,就是在手機上進行鏈路分析的耗時太長。

使用HAHA算法在PC上可以快速地對所有對象都進行鏈路分析,但是在手機上由於性能的侷限性,如果對所有對象都進行鏈路分析會導致分析耗時非常長。

考慮到RetainSize越大的對象對內存的影響也越大,即RetainSize比較大的那部分Instance是最有可能造成OOM的“元兇”。

Probe:Android線上OOM問題定位組件

我們在生成Reference之後,做了一步鏈路歸併(如上圖所示),即對於同一個對象的不同Instance,如果其底下的引用鏈路中的對象類型也相同,則進行歸併,並記錄Instance的個數和每個Instance的RetainSize。

然後對歸併後的Instance按RetainSize進行排序,取出TOP N的Instance,其中在排序過程中我們會對N的值進行動態調整,保證RetainSize達到一定閾值的Instance都能被發現。對於這些Instance才進行最後的鏈路分析,這樣就能大大縮短分析時長。

排序過程:創建一個初始容量為5的集合,往裡添加Instance後進行排序,然後遍歷後面的Instance,當Instance的RetainSize大於總共消耗內存大小的5%時,進行擴容,並重新排序。當Instance的RetainSize大於現有集合中的最小值時,進行替換,並重新排序。

Probe:Android線上OOM問題定位組件

基礎類型檢測不到

Probe:Android線上OOM問題定位組件

為了解決HAHA算法中檢測不到基礎類型洩漏的問題,我們在遍歷堆中的Instance時,如果發現是ArrayInstance,且是byte類型時,將它自身捨棄掉,並將它的RetainSize加在它的父Instance上,然後用父Instance進行後面的排序。

至此,我們對HAHA的原始算法做了諸多優化(如下圖所示),很大程度解決了分析進程自身OOM問題、分析時間過長問題以及基礎類型檢測不到的問題。

Probe:Android線上OOM問題定位組件

針對線上堆內存不足問題,Probe最後會自動分析出RetainSize大小Top N對象到GC Roots的鏈路,上報給服務器,進行報警。下面是一個線上案例,這裡截取了上報的鏈路分析結果中的一部分,完整的分析結果就是多個這樣的組合。在第一段鏈路分析可以看到,有個Bitmap對象佔用了2MB左右的內存,根據鏈路定位到代碼,修復了Bitmap洩漏問題。第二段鏈路分析反映的是一個Timer洩漏問題,可以看出內存中存在4個這樣的Instance,每個Instance的Retain Size是595634,所以這個問題會洩漏的內存大小是4*595634=2.27MB。

*GC ROOT static com.meituan.z.q
*reference com.meituan.z.p
*reference com.meituan.a.bitmapBuffer
*leaks android graphics.Bitmap instance
retainSize:2621191


instance num:4
*GC ROOT java.util.Timer$TimerImpl.<java>
*reference com.meituan.q.b(anonymous subclass of java.util.TimerTask)
*reference com.meituan.q.a
*leaks android.widget.ListView instance
retainSize:595634/<java>

裁剪回撈HPROF文件

在Probe上線分析方案之後,發現儘管我們做了很多優化,但是受到手機自身性能的約束,線上分析的成功率也只有65%。

於是,我們對另一種思路即回撈HPROF文件後本地分析進行了探索,這種方案最大的問題就是線上流量問題,因為HPROF文件動輒幾百MB,如果直接進行上傳,勢必會對用戶的流量消耗帶來巨大影響。

使用這種方案的關鍵點就在於減少上傳的HPROF文件大小,減少文件大小首先想到的就是壓縮,不過只是做壓縮的話,文件還是太大。接下來,我們就考慮幾百MB的文件內容是否都是我們需要的,是否可以對文件進行裁剪。我們希望對HPROF無用的信息進行裁剪,只保留我們關心的數據,就需要先了解HPROF文件的格式:

Debug.dumpHprofData()其內部調用的是VMDebug的同名函數,層層深入最終可以找到/art/runtime/hprof/hprof.cc,HPROF的生成操作基本都是在這裡執行的,結合HAHA庫代碼閱讀hrpof.cc的源碼。

HPROF文件的大體格式如下:

Probe:Android線上OOM問題定位組件

一個HPROF文件主要分為這四部分:

  • 文件頭。
  • 字符串信息:保存著所有的字符串,在解析的時候通過索引id被引用。
  • 類的結構信息:是所有Class的結構信息,包括內部的變量佈局,父類的信息等等。
  • 堆信息:即我們關心的內存佔用與對象引用的詳細信息。

其中我們最關心的堆信息是由若干個相同格式的元素組成,這些元素的大體格式如下圖:

Probe:Android線上OOM問題定位組件

每個元素都有個TAG用來標識自己的身份,而後續字節數則表示元素的內容長度。元素攜帶的內容則是若干個子元素組合而成,通過子TAG來標識身份。

具體的TAG和身份的對應關係可以在hrpof.cc源碼中找到,這裡不進行展開。

Probe:Android線上OOM問題定位組件

弄清楚了文件格式,接下來需要確定裁剪內容。經過思考,我們決定裁減掉全部基本類型數組的值,原因是我們的使用場景一般是排查內存洩漏以及OOM,只關心對象間的引用關係以及對象大小即可,很多時候對於值並不是很在意,所以裁減掉這部分的內容不會對後續的分析造成影響。

最後需要確定裁剪方案。先是嘗試了dump後在Java層進行裁剪,發現效率很低,很多時候這一套操作下來需要20s。然後又嘗試了dump後在Native層進行裁剪,這樣做效率是高了點,但依然達不到預期。

經過思考,如果能夠在dump的過程中篩選出哪些內容是需要保留的,哪些內容是需要裁剪的,需要裁剪的內容直接不寫入文件,這樣整個流程的性能和效率絕對是最高的。

為了實現這個想法,我們使用了GOT表Hook技術(這裡不展開介紹)。有了Hook手段,但是還沒有找到合適的Hook點。通過閱讀hrpof.cc的源碼,發現最適合的點就是在寫入文件時,拿到字節流進行裁剪操作,然後把有用的信息寫入文件。於是項目最終的結構如下圖:

Probe:Android線上OOM問題定位組件

我們對IO的關鍵函數open和write進行Hook。Hook方案使用的是愛奇藝開源的xHook庫。

在執行dump的準備階段,我們會調用Native層的open函數獲得一個文件句柄,但實際執行時會進入到Hook層中,然後將返回的FD保存下來,用作write時匹配。

在dump開始時,系統會不斷的調用write函數將內容寫入到文件中。由於我們的Hook是以so為目標的,系統運行時也會有許多寫文件的操作,所以我們需要對前面保存的FD進行匹配。若FD匹配成功則進行裁剪,否則直接調用origin-write進行寫入操作。

流程結束後,就會得到裁剪後的mini-file,裁剪後的文件大小隻有原始文件大小的十分之一左右,用於線上可以節省大部分的流量消耗。拿到mini-file後,我們將裁剪部分的位置填上字節0來進行恢復,這樣就可以使用傳統工具打開進行分析了。

原始HPROF文件和裁剪後再恢復的HPROF文件分別在Android Studio中打開,發現裁剪再恢復的HPROF文件打開後,只是看不到對象中的基礎數據類型值,而整個的結構、對象的分佈以及引用鏈路等與原始HPROF文件是完全一致的。事實證明裁剪方案不會影響後續對堆內存的鏈路分析。

方案融合

由於目前裁剪方案在部分機型上(主要是Android 7.X系統)不起作用,所以在Probe中同時使用了這兩種方案,對兩種方案進行了融合。即通過一次dump操作得到兩份HPROF文件,一份原始文件用於下次啟動時分析,一份裁剪後的文件用於上傳服務器。

Probe的最終方案實現如下圖,主要是在調用dump函數之前先將兩個文件路徑(希望生成的原始文件路徑和裁剪文件路徑)傳到Native層,Native層記錄下兩個文件路徑,並對open和write函數進行Hook。hookopen函數主要是通過open函數傳入的path和之前記錄的path比對,如果相同,我們就會同時調用之前記錄的兩個path的open,並記錄下兩個FD,如果不相同則直接調原生open函數。hookwrite函數主要是通過傳入的FD與之前hookopen中記錄的FD比對,如果相同會先對原始文件對應的FD執行原生write,然後對裁剪文件對應的FD執行我們自定義的write,進行裁剪壓縮。這樣再傳入原始文件路徑調用系統的dump函數,就能夠同時得到一份完整的HPROF文件和一份裁剪後的HPROF文件。

Probe:Android線上OOM問題定位組件

線程數超出限制

對於創建線程失敗導致的OOM,Probe會獲取當前進程所佔用的虛擬內存、進程中的線程數量、每個線程的信息(線程名、所屬線程組、堆棧信息)以及系統的線程數限制,並將這些信息上傳用於分析問題。

/proc/sys/kernel/threads-max規定了每個進程創建線程數目的上限。在華為的部分機型上,這個上限被修改的很低(大約500),比較容易出現線程數溢出的問題,而大部分手機這個限制都很大(一般為1W多)。在這些手機上創建線程失敗大多都是因為虛擬內存空間耗盡導致的,進程所使用的虛擬內存可以查看/proc/pid/status的VmPeak/VmSize記錄。

然後,通過Thread.getAllStackTraces()可以得到進程中的所有線程以及對應的堆棧信息。

一般來說,當進程中線程數異常增多時,都是某一類線程被大量的重複創建。所以我們只需要定位到這類線程的創建時機,就能知道問題所在。如果線程是有自定義名稱的,那麼直接就可以在代碼中搜索到創建線程的位置,從而定位問題,如果線程創建時沒有指定名稱,那麼就需要通過該線程的堆棧信息來輔助定位。下面這個例子,就是一個“crowdSource msg”的線程被大量重複創建,在代碼中搜索名稱很快就查出了問題。針對這類線程問題推薦的做法就是在項目中統一使用線程池,可以很大程度上避免線程數的溢出問題。

線程信息:

thread name: Thread[nio_tunnel_handler,5,main] count: 1
thread name: Thread[OkHttp Dispatcher,5,main] count: 30
thread name: Thread[process_read_thread,5,main] count: 4
thread name: Thread[Jit thread pool worker thread 0,5,main] count: 1
thread name: Thread[crowdSource msg,5,main] count: 202
thread name: Thread[Timer-4,5,main] count: 1
thread name: Thread[mqt_js,5,main] count: 1

threadnames:Thread[Thread-5,5,main] count:1
trace:
java.lang.Object.wait(NativeMethod)
com.dianping.networklog.d.run(UnknownSource:28)

FD數超出限制

前面介紹了,當進程中的FD數量達到最大限制時,再去新建線程,在創建JNIEnv時會拋出OOM錯誤。但是FD數量超出限制除了會導致創建線程拋出OOM以外,還會導致很多其它的異常,為了能夠統一處理這類FD數量溢出的問題,Probe中對進程中的FD數量做了監控。在後臺啟動一個線程,每隔1s讀取一次當前進程創建的FD數量,當檢測到FD數量達到閾值時(FD最大限制的95%),讀取當前進程的所有FD信息歸併後上報。

在/proc/pid/limits描述著Linux系統對對應進程的限制,其中Max open files就代表可創建FD的最大數目。

進程中創建的FD記錄在/proc/pid/fd中,通過遍歷/proc/pid/fd,可以得到FD的信息。

獲取FD信息:

File fdFile=new File("/proc/" + Process.myPid() + "/fd");
File[] files = fdFile.listFiles();
int length = files.length; //即進程中的fd數量
for (int i = 0; i < length ; i++) {
if (Build.VERSION.SDK_INT >= 21) {
Os.readlink(files[i].getAbsolutePath()); //得到軟鏈接實際指向的文件
} else {
//6.0以下系統可以通過執行readlink命令去得到軟連接實際指向文件,但是耗時較久
}
}

得到進程中所有的FD信息後,我們會先按照FD的類型進行一個歸併,FD的用途主要有打開文件、創建socket連接、創建handlerThread等。

Probe:Android線上OOM問題定位組件

比如像下面這個例子中,就是anon_inode:[eventpoll]和anon_inode:[eventfd]的數量異常的多,說明進程中很可能是啟動了大量的handlerThread,再結合回傳上來的線程信息就能快速定位到問題代碼的具體位置。

FD溢出案例:

FD信息:
anon_inode:[eventpoll] count: 381
anon_inode:[eventfd] count: 381
pipe count 26
socket count 32
/system/framework/framework-res.apk count: 1
..........
Thread信息:
thread name: Thread[Jit thread pool worker thread 0,5,main] count: 1
thread name: Thread[mtqq handler,5,main] count: 302
thread name: Thread[Timer-4,5,main] count: 1
thread name: Thread[mqt_js,5,main] count: 1

總結

Probe目前能夠有效定位線上Java堆內存不足、FD洩漏以及線程溢出的OOM問題。騎手Android端使用Probe組件解決了很多線上的OOM問題,將線上OOM Crash率從最高峰的2‰降低到了現在的0.02‰左右。我們後續也會繼續完善Probe組件,例如HPROF文件裁剪方案對7.X系統的兼容以及Native層的內存問題定位。

逢搏,美團配送App團隊研發工程師。


毅然,美團配送App團隊高級技術專家。
永剛,美團平臺監控團隊研發工程師。

歡迎加入美團Android技術交流群,跟作者零距離交流。如想進群,請加美美同學的微信(微信號:MTDPtech03),回覆:Probe,美美會自動拉你進群。

招聘信息

美團配送App團隊,負責美團騎手、美團眾包、美團跑腿等配送相關App的研發,涉及技術領域包括但不限於App的穩定性建設、App性能監控和優化、大前端跨平臺動態化、App安全。對上述領域感興趣的請聯繫[email protected](郵件標題註明:美團配送App團隊)。


分享到:


相關文章: