京東JDK在大數(shù)據(jù)平臺(tái)的探索與研究
本文旨在概述京東在JDK方向上的嘗試與探索,以及京東JDK項(xiàng)目背景,基本特性以及未來(lái)的工作方向。對(duì)于JDK特性的技術(shù)討論,實(shí)現(xiàn)細(xì)節(jié)及效果,將在后續(xù)系列文章中深入討論。
一、HDFS簡(jiǎn)介
HDFS是作為底層的分布式存儲(chǔ)服務(wù)而存在的,是Hadoop的分布式文件系統(tǒng)組件。HDFS是高容錯(cuò)的,被設(shè)計(jì)成在低成本硬件上部署。HDFS為應(yīng)用數(shù)據(jù)提供高吞吐量的訪問(wèn),適用于具有大規(guī)模數(shù)據(jù)集的應(yīng)用程序。HDFS采用了基于Master/Slave主從架構(gòu)的分布式文件系統(tǒng), 一個(gè)HDFS集群包含的Master節(jié)點(diǎn)(NameNode)和多個(gè)Slave節(jié)點(diǎn)(DataNode)服務(wù)器,文件以block的形式存儲(chǔ)在DataNode節(jié)點(diǎn)。NameNode主要負(fù)責(zé)響應(yīng)客戶端請(qǐng)求,進(jìn)行文件的打開(kāi)、關(guān)閉、重命名文件和目錄,同時(shí)決定block到具體Datanode節(jié)點(diǎn)的映射。Datanode在Namenode的指揮下進(jìn)行block的創(chuàng)建、刪除和復(fù)制。
二、JVM對(duì)HDFS的作用
由于HDFS采用Java開(kāi)發(fā),并運(yùn)行于JVM上,因此如何從JVM角度提升HDFS的能力是主要研究的方向之一。 從JVM角度看,NameNode節(jié)點(diǎn)的特點(diǎn)是進(jìn)程生命周期長(zhǎng),對(duì)象創(chuàng)建頻繁,資源利用率高,對(duì)于內(nèi)存的資源要求較高,NameNode的性能是HDFS性能的關(guān)鍵。DataNode節(jié)點(diǎn)的特點(diǎn)是進(jìn)程生命周期短,多數(shù)進(jìn)程創(chuàng)建后進(jìn)行對(duì)文件塊的操作后即退出。如何對(duì)JVM進(jìn)行優(yōu)化,才能使其更加適用于HDFS NameNode和DataNode的工作特點(diǎn)是京東JDK研發(fā)的主要方向。
三、京東對(duì)通用JDK的嘗試
1. 使用Oracle JDK 1.8的經(jīng)驗(yàn)
在京東,曾經(jīng)嘗試使用Oracle的JDK1.8做為HDFS的JDK解決方案。經(jīng)過(guò)不斷的工作與參數(shù)調(diào)優(yōu),已經(jīng)使HDFS穩(wěn)定的運(yùn)行在OracleJDK1.8環(huán)境中。但是,隨著京東業(yè)務(wù)的不斷增長(zhǎng),對(duì)于HDFS的要求也在不斷提高,OracleJDK1.8在以下問(wèn)題上并不能提供更多的幫助:
- 性能優(yōu)化:雖然OracleJDK1.8 的JVM中具備很多先進(jìn)的優(yōu)化功能,比如tiered compiler, 高效的CMS垃圾收集器等,但其主要針對(duì)通用Java程序的性能進(jìn)行優(yōu)化,缺少針對(duì)分布式工作環(huán)境的特定優(yōu)化。由于無(wú)法對(duì)oracle JDK1.8的源代碼進(jìn)行修改,通過(guò)參數(shù)調(diào)整并不能從根本上解決問(wèn)題。
- 不可控的GC: 雖然OracleJDK1.8提供的相當(dāng)優(yōu)秀的CMS垃圾收集器,可以有效的提高GC暫停時(shí)間帶來(lái)的性能損失,但在實(shí)際使用過(guò)程中,發(fā)現(xiàn)GC停頓時(shí)間仍然不能滿足要求,比如YoungGC的時(shí)間仍在1秒左右,而OldGC消耗在60秒左右,如果一旦發(fā)生FullGC,則經(jīng)常會(huì)導(dǎo)致NameNode暫停時(shí)間過(guò)長(zhǎng)從而導(dǎo)致系統(tǒng)假死,結(jié)果往往是災(zāi)難性的。
- 內(nèi)存利用率低:對(duì)于NameNode節(jié)點(diǎn),能夠使用的物理內(nèi)存在512GB,而為了避免JVM中老年代GC和Full GC時(shí)間過(guò)長(zhǎng)而導(dǎo)致的災(zāi)難性后果,NameNode節(jié)點(diǎn)只能配置Java堆在200GB左右。通常NameNode節(jié)點(diǎn)的機(jī)器上只運(yùn)行NameNode進(jìn)程和一個(gè)輕量級(jí)的ZKFC進(jìn)程,所以物理內(nèi)存不能得到有效利用。另一方面,NameNode的承載能力受到Java堆大小的制約,導(dǎo)致HDFS的總體承載能力受限。
- JDK版本更新:隨著以上問(wèn)題的不斷顯現(xiàn),同時(shí)JDK1.8將在2019年停止更新,同時(shí)需要嘗試新的JDK以及OpenJDK能否幫助解決問(wèn)題。
2. 嘗試openJDK11
隨著openJDK的不斷演進(jìn),為了緩解上面提到的問(wèn)題,也嘗試了OpenJDK11, 相對(duì)于openJDK1.8,發(fā)現(xiàn)openJDK11在以下方面可能具備優(yōu)勢(shì):
G1GC: open JDK11采用G1作為默認(rèn)的GC算法,相對(duì)于CMS,G1具備以下優(yōu)點(diǎn):
- 更小的內(nèi)存碎片:由于CMS老年代采用Mark-sweep算法,并不是每次做OldGC都進(jìn)行Compact,所以CMS老年代空間常常會(huì)引入碎片問(wèn)題。而G1采用分塊Copy算法,使得內(nèi)存碎片問(wèn)題僅僅在G1的分塊中存在,相對(duì)于CMS,其內(nèi)存的利用率更高,發(fā)生FullGC和OOM的可能性更低。
- 可控的GC暫停時(shí)間:G1算法典型的特點(diǎn)就是它可以讓用戶提供期望的GC暫停時(shí)間,在其內(nèi)部通過(guò)統(tǒng)計(jì)預(yù)測(cè)的方法對(duì)下一次即將發(fā)生的GC算法進(jìn)行有效的暫停時(shí)間的控制,從而優(yōu)化GC對(duì)于性能的損耗。
- 更豐富的性能分析工具:OpenJDK11引入了Java Frame Recorder(JFS),這是原來(lái)oracle JDK1.8商業(yè)版才具備的特性,JFR可以在不損耗,或輕微損耗性能的情況下,對(duì)Java程序進(jìn)行sampling,從而幫助分析性能、功能瓶頸和指導(dǎo)優(yōu)化。
- HDFS更高的負(fù)載能力:OpenJDK11由于采用G1作為默認(rèn)的GC算法,其可以更高效的利用堆內(nèi)存,同時(shí)由于G1算法的設(shè)計(jì)及優(yōu)化,其發(fā)生FullGC的幾率非常低,并且FullGC的暫停時(shí)間也得到了優(yōu)化,所以相對(duì)于oracle JDK1.8的CMS,對(duì)于HDFS NameNode來(lái)說(shuō),其負(fù)載能力受到堆大小的限制更加寬松。
雖然OpenJDK11能夠幫助緩解一系列問(wèn)題,但對(duì)于京東大數(shù)據(jù)來(lái)說(shuō),僅使用原生的OpenJDK11仍然缺少針對(duì)性的優(yōu)化,目前主要存在以下問(wèn)題:
- 針對(duì)大堆的優(yōu)化:由于openJDK上G1內(nèi)部的一些限制,其針對(duì)大堆,如360GB的堆的性能并沒(méi)有達(dá)到理想狀態(tài)。
- 針對(duì)大堆的工具開(kāi)發(fā):以JMap為例,當(dāng)堆內(nèi)存很大的時(shí)候,一次JMap操作便利整個(gè)堆內(nèi)存耗時(shí)巨大,我們經(jīng)常遇到JMap導(dǎo)致假死的情況。
- 針對(duì)HDFS的定制化工作:另外,目前仍然希望JDK具備一些可利用的特性幫助我們對(duì)HDFS在問(wèn)題分析,危機(jī)處理以及線上分析方面的能力進(jìn)行增強(qiáng)。
四、京東定制化JDK
經(jīng)過(guò)以上嘗試,結(jié)合HDFS業(yè)務(wù)特點(diǎn)及優(yōu)化需求。最終決定在OpenJDK11的基礎(chǔ)上,對(duì)openjdk進(jìn)行有針對(duì)性的開(kāi)發(fā)和優(yōu)化,打造京東的定制化JDK。
1. 京東JDK特性介紹
除openJDK11具備的特性外,目前京東JDK主要具備以下能力:
(1) JDK8 兼容性支持 javah:
由于JDK8具備Javah工具能供根據(jù)Java的類(lèi)定義文件生成相應(yīng)JNI實(shí)現(xiàn)所需的C/C++頭文件。在大型項(xiàng)目中,如Hadoop,Yarn都會(huì)利用Javah進(jìn)行JNI頭文件的生成。從JDK10開(kāi)始,javah工具在JDK中被移除,取而代之的是javac –h功能,但由于javac –h在使用上不同于javah,并且在復(fù)雜的項(xiàng)目中,要想用javac –h 代替javah, 必須要修改編譯系統(tǒng),工作量和難度都比較大。為了在京東內(nèi)部流暢的進(jìn)行JDK升級(jí),重寫(xiě)了javah,使其能成功的利用javac –h進(jìn)行JNI頭文件的生成。
(2) 擴(kuò)大G1 region size:
由于openJDK的限制,針對(duì)G1GC的region大小只能達(dá)到32MB, 并且JVM內(nèi)部推薦的region個(gè)數(shù)為2048, 即G1GC最為適用的堆大小在64GB (2048*32MB),而業(yè)務(wù)量要求NameNode堆至少要在180GB,因此JDJDK確定了優(yōu)化G1GC對(duì)于大堆的支持的目標(biāo),以期望提高管理結(jié)點(diǎn)的性能。
經(jīng)過(guò)調(diào)查研究,針對(duì)G1GC的region調(diào)整,實(shí)際上有兩種方向,一種是保持region大小不變,增大region的個(gè)數(shù)以適應(yīng)大堆,比如針對(duì)180GB的堆,region大小保持在32MB不變,那么就需要?jiǎng)?chuàng)建5760個(gè)region。此方案的好處是保持region大小不變,可以將分配的影響降到最小,但同時(shí)由于G1算法需要對(duì)每個(gè)region之間的引用關(guān)系做同步,如果堆數(shù)量過(guò)多,則同步的開(kāi)銷(xiāo)增大,從而影響GC的效率。
另一種方案是增加region大小,以保持region個(gè)數(shù)保持在2048或少量增長(zhǎng),其特點(diǎn)是增大region可能會(huì)導(dǎo)致應(yīng)用程序?qū)ο蠓峙涞男袨楦淖?,但?duì)于region間引用關(guān)系的同步影響比較小。
為了能夠達(dá)到優(yōu)化性能的目標(biāo),對(duì)NameNode做了如下分析:通過(guò)采集GCdebug的日志信息,可以看到NameNode的對(duì)象分配速率非常頻繁,old space allocation rate 達(dá)到1MB/s,即有大量的object被頻繁提升到老年代,同時(shí)存在大量的TLAB refile以及出現(xiàn)TLAB fill的頻率在每分鐘3萬(wàn)次左右,TLAB fill 即allocation進(jìn)入slow path,需要進(jìn)行TLAB的替換或者在非TLAB中分配。因此對(duì)象的分配性能是NameNode 性能的關(guān)鍵點(diǎn)之一。
結(jié)合以上分析,對(duì)JDK的region大小上限進(jìn)行了優(yōu)化,同時(shí)針對(duì)region大小,對(duì)G1進(jìn)行了相應(yīng)的修改。以下為優(yōu)化后的實(shí)驗(yàn)得到的數(shù)據(jù)。
可以看到,TLAB fill次數(shù)從每分鐘30000降到了20000,即對(duì)象分配在slow path的機(jī)率減少了33%。
(3) 針對(duì)多線程下鎖的性能優(yōu)化:
在JDJDK版本升級(jí)后, 運(yùn)維與研發(fā)人員在大數(shù)據(jù)平臺(tái)運(yùn)行過(guò)程中,發(fā)現(xiàn)G1在運(yùn)行過(guò)程中會(huì)出現(xiàn)2s左右的超長(zhǎng)YoungGC,而相同規(guī)模的YGC大部分只有200ms左右. 如下圖中綠線所示。
經(jīng)過(guò)分析, G1出現(xiàn)2s GC的主要原因在于偏向鎖功能的revoke過(guò)于頻繁。利用JFR可以看到如下現(xiàn)象。
綜合以上分析, 在管理節(jié)點(diǎn)采用-XX:-UseBiasedLocking后, 2s的GC 消失, 上圖藍(lán)色線條所示。
(4) Java堆的動(dòng)態(tài)拓展:
Java程序在啟動(dòng)時(shí)要求程序員為JVM預(yù)設(shè)堆內(nèi)存上限,即指定-Xmx的大小(或采用默認(rèn)JVM參數(shù))。但在實(shí)際使用過(guò)程中,很難清晰的計(jì)算出究竟應(yīng)該采用多大的Java堆上限,尤其是對(duì)于線上系統(tǒng)中的管理進(jìn)程,很有可能在發(fā)生大量的業(yè)務(wù)請(qǐng)求時(shí)出現(xiàn)OOM(Out-Of-Memory)異常而導(dǎo)致管理進(jìn)程退出,出現(xiàn)災(zāi)難性后果。另一方面,考慮到系統(tǒng)資源占用,Java程序往往要求JVM不要占用大量的系統(tǒng)內(nèi)存,即使-Xmx的值小于RAM的大小,所以在程序運(yùn)行時(shí),經(jīng)常會(huì)出現(xiàn)Java進(jìn)程因?yàn)镺OM退出,而系統(tǒng)RAM卻還有很多剩余可以利用。
為了緩解OOM的問(wèn)題,京東JDK研發(fā)了基于G1GC的動(dòng)態(tài)拓展堆大小的功能。 該功能在JVM堆內(nèi)存使用率正常的情況下,維持java堆在-Xmx之下,而當(dāng)JVM發(fā)現(xiàn)當(dāng)前進(jìn)程Java堆被大量占用時(shí),將發(fā)出警報(bào),從而運(yùn)維人員可以根據(jù)當(dāng)前業(yè)務(wù)情況即系統(tǒng)RAM使用情況,動(dòng)態(tài)的打開(kāi)Java堆拓展功能,JVM將Java堆進(jìn)行一定比例的拓展,以保證JVM順利度過(guò)業(yè)務(wù)繁忙的時(shí)段。 當(dāng)業(yè)務(wù)量降低,并且heap使用率低于一定閾值時(shí),JVM將利用G1GC回收拓展的堆區(qū)域,從而保證在正常情況下JVM進(jìn)程不會(huì)給系統(tǒng)內(nèi)存造成額外的壓力。
(5) 定期、定時(shí)觸發(fā)GC:
經(jīng)過(guò)調(diào)研,發(fā)現(xiàn)京東的業(yè)務(wù)呈現(xiàn)明顯的時(shí)間周期性,比如某個(gè)集群在某一時(shí)段基本處于空閑狀態(tài)。而在繁忙狀態(tài)時(shí),堆內(nèi)存以及CPU資源都集中于業(yè)務(wù)的處理,如果此時(shí)發(fā)生OldGC或者FullGC,或者YoungGC發(fā)生過(guò)于頻繁,都會(huì)導(dǎo)致系統(tǒng)的業(yè)務(wù)處理能力下降。
為了降低GC對(duì)于業(yè)務(wù)處理能力的影響,京東JDK基于G1GC開(kāi)發(fā)了周期性GC的功能。運(yùn)維人員可以在每天系統(tǒng)不繁忙的時(shí)間段定時(shí)觸發(fā)多次YoungGC以及必要的MixedGC/FullGC來(lái)清里Java堆中的垃圾,從而降低高峰時(shí)段GC觸發(fā)的頻率及時(shí)間。
(6) JVM及時(shí)歸還未使用的內(nèi)存(Uncommitted Memory)給系統(tǒng):
JDK12特性,京東JDK目前已經(jīng)支持。此功能主要為節(jié)省物理內(nèi)存空間。JDK11版本中的G1并不會(huì)及時(shí)的將空的region交還給OS,只有在FullGC或Old GC的concurrent 階段才會(huì)交還已經(jīng)回收的region給OS。但由于G1的設(shè)計(jì)目標(biāo)就是避免FullGC以及盡量少的觸發(fā)OldGC,所以實(shí)際運(yùn)行過(guò)程中,G1 堆占用的物理內(nèi)存會(huì)遲遲不能釋放給系統(tǒng),導(dǎo)致JVM進(jìn)程占用內(nèi)存遠(yuǎn)高于實(shí)際使用量。在多進(jìn)程多任務(wù)環(huán)境中,會(huì)整體導(dǎo)致系統(tǒng)內(nèi)存資源不能有效分配及使用,同時(shí)提高內(nèi)存硬件的需求量,增加企業(yè)的成本投入。
京東JDK在JDK11的基礎(chǔ)上,從JDK12引入了JEP346特性 --“及時(shí)回收未使用的Uncommitted Memory給系統(tǒng)“這個(gè)特性,其在JVM內(nèi)部引入了監(jiān)測(cè)機(jī)制,當(dāng)發(fā)現(xiàn)系統(tǒng)空閑以及JVMGC觸發(fā)不頻繁時(shí),JVM會(huì)自動(dòng)觸發(fā)concurrentGC 或FullGC來(lái)回收uncommitted region給系統(tǒng)。
(7) 可撤銷(xiāo)的G1 Mixed GC以保證GC停頓時(shí)間:
JDK12特性,有效減少及控制G1停頓時(shí)間。G1GC的主要設(shè)計(jì)目標(biāo)是保證G1的停頓時(shí)間在可控的范圍內(nèi),用戶可以通過(guò)-XX:MaxGCPauseMills參數(shù)來(lái)指定G1的停頓時(shí)間上限,G1GC會(huì)盡量嘗試保證每次GC的時(shí)間不會(huì)超過(guò)-XX:MaxGCPauseMills。在JVM內(nèi)部,G1GC在Concurrent 階段會(huì)根據(jù)停頓時(shí)間上限來(lái)選擇需要回收的集合(Collect Set),然后在暫停階段回收這些集合中的對(duì)象。
在JDK11版本中,Collection Set一旦確定就無(wú)法改變,但由于Collection Set是JVM根據(jù)歷史GC信息推斷出的,因此如果推斷與真實(shí)情況的誤差過(guò)大,會(huì)導(dǎo)致MixGC(oldGC)的暫停時(shí)間過(guò)長(zhǎng),遠(yuǎn)超過(guò)-XX:MaxGCPauseMills設(shè)定的目標(biāo)。
京東JDK從JDK12引入了JEP344特性—Abortable Mixed Collections for G1,該特性可以將Collection Set分解為“必須回收”和“可選擇回收”的兩部分,在發(fā)生MixedGC時(shí),GC在回收完“必須回收”的部分后,會(huì)根據(jù)目標(biāo)暫停時(shí)間的剩余量循環(huán)的從“可選擇回收”部分中選取回收集合進(jìn)行回收,以保證GC整體暫停時(shí)間可控。
(8) 默認(rèn)的類(lèi)型信息共享文件(Class Data Sharing - CDS Archive):
Class Data Sharing (CDS)有助于加快Java程序啟動(dòng)時(shí)間,同時(shí)允許多JVM實(shí)例復(fù)用SharedArchive以減少memory footprint.
JDK10對(duì)CDS進(jìn)一步拓展,SharedArchive中保存應(yīng)用程序數(shù)據(jù):Application Class-data sharing (參見(jiàn)JEP 310)
對(duì)于CDS,JEP中的介紹如下:
- We can save about 340MB of RAM for a Java EE app server that includes 6 JVM processes consuming a total of 13GB of RAM (~2GB of that is for class meta data).
- We can improve the startup time of the JEdit benchmark by 20-30%.
- We can reduce the RAM usage of the embedded Felix benchmark by 18% across 4 JVM processes.
京東JDK引入了新的JDK12中關(guān)于CDS的新特性 - Default CDS Archives。該功能在編譯階段生成默認(rèn)的Archive,并且無(wú)需用戶指定JVM選項(xiàng)-Xshare:auto即可享受到CDS帶來(lái)的優(yōu)點(diǎn)。
(9) 并行的高效JMap Java堆分析工具:
JMap作為Java開(kāi)發(fā)人員常用工具,一般在調(diào)查OOM,查看堆對(duì)象分布時(shí)都能發(fā)揮重要作用。但是在日常工作中,發(fā)現(xiàn)對(duì)于大堆,例如堆內(nèi)存配置為-Xmx200g時(shí),在線上系統(tǒng)運(yùn)行JMap histo時(shí)間非常長(zhǎng),并且影響整個(gè)JVM進(jìn)程的響應(yīng)速度,一旦JVM進(jìn)程被KILL,運(yùn)行中JMap histo也無(wú)法提供有效信息。 經(jīng)過(guò)調(diào)研,JMap 工具在掃描Java堆時(shí)是單線程工作,并且只有在整個(gè)堆掃描完成時(shí)才會(huì)統(tǒng)計(jì)信息并輸出。
針對(duì)JMap的問(wèn)題,京東JDK團(tuán)隊(duì)對(duì)JMap進(jìn)行了拓展,實(shí)現(xiàn)了其并行,增量式對(duì)掃描方案。對(duì)JMap histo在大堆上的掃描并行化,同時(shí)在運(yùn)行中統(tǒng)計(jì)中間結(jié)果。使得JMap在200GB堆掃描性能提升2倍,同時(shí)能夠使JMap在運(yùn)行過(guò)程中不斷輸出中間結(jié)果,這樣即使JVM進(jìn)程退出,JMap仍能提供有效的信息用于分析內(nèi)存使用情況。
2. 京東JDK優(yōu)化效果
經(jīng)過(guò)一系列的工作,目前京東JDK已經(jīng)順利應(yīng)用于京東大數(shù)據(jù)平臺(tái)HDFS的NameNode節(jié)點(diǎn)上,其對(duì)于管理結(jié)點(diǎn)優(yōu)化達(dá)到50%, 見(jiàn)下圖:
另一方面,JDJDK對(duì)于管理結(jié)點(diǎn)文件數(shù)承載能力從4億上升到10億,承載能力提升1.5倍。緩解了業(yè)務(wù)方的需求,節(jié)省了人力。
針對(duì)G1GC 也做了相關(guān)優(yōu)化, 優(yōu)化后的G1GC 對(duì)比之前JDK8的CMS的YoungGC暫停時(shí)間如下圖:
GC發(fā)生的次數(shù)對(duì)于如下:
在加/解鎖及線程同步方面,京東JDK團(tuán)隊(duì)也進(jìn)行了深入的研究及優(yōu)化,除了上文提到的偏向鎖以外,還利用JVM 的instrumentation等工具,對(duì)鎖相關(guān)的bytecode進(jìn)行線上優(yōu)化,針對(duì)不同的HDFS訪問(wèn),優(yōu)化效果如下:
Mkdir:
Delete:
Getfileinfo:
Rename:
五、京東JDK的發(fā)展方向
在未來(lái),京東JDK團(tuán)隊(duì)將更加注重于降本增效方面的工作,我們計(jì)劃進(jìn)行更多的嘗試及創(chuàng)新,例如:
- 用于特定使用場(chǎng)景的,獨(dú)立的heap區(qū)域
- 半自動(dòng)式GC
- 基于大數(shù)據(jù)應(yīng)用場(chǎng)景的GC算法開(kāi)發(fā)及優(yōu)化
- 基于Graal的AOT功能的開(kāi)發(fā)及優(yōu)化
同時(shí),京東JDK團(tuán)隊(duì)也將積極參與openJDK社區(qū)的開(kāi)發(fā)及研究工作,盡可能將京東JDK的特性貢獻(xiàn)到社區(qū),讓更多人能夠使用到。
作者簡(jiǎn)介:臧琳,京東JVM專(zhuān)家,主要負(fù)責(zé)京東JDK針對(duì)京東大數(shù)據(jù)業(yè)務(wù)的定制化開(kāi)發(fā)及優(yōu)化工作。專(zhuān)注于JVM中內(nèi)存管理,runtime運(yùn)行時(shí)以及JIT編譯器的性能分析及優(yōu)化等領(lǐng)域。
【本文來(lái)自51CTO專(zhuān)欄作者張開(kāi)濤的微信公眾號(hào)(開(kāi)濤的博客),公眾號(hào)id: kaitao-1234567】