背景
今年上半年接手了一位離職同事負(fù)責(zé)的推薦項(xiàng)目赡译,主要是圍繞智能推薦服務(wù)相關(guān)的內(nèi)容湃密,包括了離線蒋失、實(shí)時(shí)數(shù)據(jù)處理和線上的預(yù)測(cè)服務(wù)领舰。
這里的堆外內(nèi)存泄漏也是發(fā)生在預(yù)測(cè)服務(wù)這一塊夫嗓,大概的表現(xiàn)情況就是線上服務(wù)運(yùn)行一段時(shí)間后,客戶端會(huì)發(fā)生大面積connect reset異常冲秽,并且在較短時(shí)間內(nèi)會(huì)發(fā)生雪崩的情況舍咖,下面就簡(jiǎn)單回顧一下整個(gè)問題的排查過程。
環(huán)境
該服務(wù)運(yùn)行在公司內(nèi)部的微服務(wù)框架上锉桑,這套框架比較久遠(yuǎn)排霉,依舊是基于HTTP1.X協(xié)議進(jìn)行通信的,傳輸?shù)男蛄谢玫氖莋oogle的Gson民轴,Server端基于Netty自研了一套郑诺,Client端則是基于okhttp3開發(fā)的。
我們服務(wù)端目前部署了67臺(tái)節(jié)點(diǎn)杉武,下游調(diào)用端超過100臺(tái)節(jié)點(diǎn)辙诞。
公司近期做了雙中心推廣,很多服務(wù)都進(jìn)行了雙中心部署轻抱。
Server節(jié)點(diǎn)配置信息:
8核 13G飞涂,系統(tǒng)版本Centos Linux realease 7.8.2003
服務(wù)性能指標(biāo):
TP99:50ms
TP95:30ms
QPS:2000+(其實(shí)不高)
JVM啟動(dòng)參數(shù):
這里就截取部分參數(shù)了
-Xms10240m -Xmx10240m -XX:NewSize=3072m -XX:MaxNewSize=3072m -XX:MaxDirectMemorySize=1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/skynet-xxx/xxx_heapDump.hprof -XX:+UseParNewGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1024M -XX:+ExplicitGCInvokesConcurrent -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=65 -XX:CMSFullGCsBeforeCompaction=2 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/data/logs/skynet-xxx/xxx_gc.log
請(qǐng)求入?yún)⑸希蛻舳苏?qǐng)求參數(shù)大概在mb級(jí)別祈搜,小點(diǎn)的基本也在數(shù)十kb左右较店,這里入?yún)⑤^大的原因也是業(yè)務(wù)決定的,單次請(qǐng)求下容燕,用戶需要組裝的Item集數(shù)量很大梁呈,并且推薦模型所用到的特征中,實(shí)時(shí)特征部分也大量依賴了實(shí)時(shí)的Item業(yè)務(wù)屬性蘸秘,例如官卡,兩程方案按照用戶請(qǐng)求動(dòng)態(tài)組合所計(jì)算出的特征會(huì)依賴Item的具體多個(gè)業(yè)務(wù)屬性蝗茁,因此入?yún)?bào)文大也就可以理解了。
故障
先來看下當(dāng)天的故障監(jiān)控
8點(diǎn)半開始寻咒,下游調(diào)用方開始大面積訪問異常哮翘,并且出現(xiàn)雪崩的現(xiàn)象,當(dāng)時(shí)9點(diǎn)半收到通知報(bào)警(當(dāng)天正好是周五毛秘,美好的周五準(zhǔn)備晚點(diǎn)到公司饭寺,恰逢高架無限堵車,均速5碼叫挟,不知道大家能體會(huì)筆者當(dāng)時(shí)的心境不艰匙,唉,其實(shí)當(dāng)時(shí)不光滿心都是淚抹恳,三急也是呼之欲出的狀態(tài)旬薯,then,N minutes later......)适秩。
到公司那一刻起,開始定位問題硕舆,調(diào)用方發(fā)現(xiàn)大面積拋connect reset
分析
1 查看日志
首先定位服務(wù)端日志異常秽荞,看下來,并沒有發(fā)現(xiàn)存在可疑異常抚官,采樣了幾臺(tái)節(jié)點(diǎn)扬跋,通過jstack打印了線程棧,看了下凌节,也并沒有看到blocked住的情況钦听。
無奈去查看公共微服務(wù)日志文件夾下的日志,結(jié)果發(fā)現(xiàn)了端倪倍奢,Server端大量拋OOM朴上。
2 問題分析
就OOM來看,是源于Netty堆外內(nèi)存溢出引起的卒煞,再看used:1056964615, max:1073741824痪宰,已用堆外內(nèi)存1056964615=1056964615/1024/1024>1008M,而max=1073741824/1024/1024=1024M畔裕,Netty再向堆外申請(qǐng)16777216=16777216/1024/1024=16M內(nèi)存時(shí)衣撬,明顯就不夠了,因此拋OOM扮饶。
這里回看本文開頭提到的JVM啟動(dòng)參數(shù)也可以對(duì)應(yīng)起來具练,-XX:MaxDirectMemorySize=1024m,剛好max也是1024M甜无。
緊接著扛点,我們就去查看了下Netty具體的溢出判定邏輯哥遮。
先看PlatformDependent類中的incrementMemoryCounter方法:
Netty內(nèi)部通過全局的DIRECT_MEMORY_COUNTER變量來統(tǒng)計(jì)應(yīng)用端已經(jīng)使用的堆外內(nèi)存空間,并且DIRECT_MEMORY_COUNTER也被申明為全局靜態(tài)變量占键,在allocateDirectNoCleaner和reallocateDirectNoCleaner會(huì)做compareAndSet(usedMemory, newUsedMemory)增加動(dòng)作昔善,如exception則執(zhí)行decrementMemoryCounter,相當(dāng)于回滾畔乙;在freeDirectNoCleaner也會(huì)進(jìn)行decrementMemoryCounter君仆,歸還已經(jīng)申請(qǐng)的空間。
其實(shí)到這里牲距,業(yè)務(wù)訂單流失的壓力已經(jīng)很大了返咱,為了優(yōu)先保障業(yè)務(wù),這里保留了一臺(tái)線上故障節(jié)點(diǎn)的故障現(xiàn)場(chǎng)牍鞠,先將所有故障節(jié)點(diǎn)進(jìn)行重啟咖摹,重啟前留了個(gè)心,將DIRECT_MEMORY_COUNTER變量進(jìn)行了監(jiān)控难述,監(jiān)控部分代碼也同時(shí)上線重啟萤晴,代碼如下:
@Component
public class DirectMemoryReporterImpl {
private AtomicLong directMemory;
@PostConstruct
public void init() {
Field field = ReflectionUtils.findField(PlatformDependent.class, "DIRECT_MEMORY_COUNTER");
field.setAccessible(true);
try {
directMemory = (AtomicLong) field.get(PlatformDependent.class);
} catch (IllegalAccessException ignored) {
}
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleAtFixedRate(this::doReport, 0, 1, TimeUnit.SECONDS);
}
private void doReport() {
try {
long memory = directMemory.get();
SkyLogHelper.traceInfo(LogModule.Monitor, "DirectMemoryReporterImpl", "doReport", "DIRECT_MEMORY_COUNTER -> " + memory + "b", "");
} catch (Exception e) {
SkyLogHelper.traceError(LogModule.Monitor, "DirectMemoryReporterImpl", "doReport", "e", "", e);
}
}
}
重新部署期間,除了觀察線上調(diào)用側(cè)的異常指標(biāo)監(jiān)控外胁后,又想了下店读,為什么會(huì)有16M的內(nèi)存申請(qǐng)呢,16M又是哪來的呢攀芯,然后繼續(xù)按照異常棧一步步跟源碼屯断,到了PoolArena類的newChunk方法,并且這里的chunkSize是由PoolArena類的allocateNormal傳入侣诺,這里能看到DirectArena是PoolArena的實(shí)現(xiàn)殖演,而調(diào)用其構(gòu)造方法的地方則是PooledByteBufAllocator的初始化方法。
PoolArena
PooledByteBufAllocator
validateAndCalculateChunkSize就是計(jì)算Chunk大小的方法年鸳,通過pageSize頁(yè)大小和maxOrder深度來計(jì)算的趴久,在PooledByteBufAllocator內(nèi)部也有兩個(gè)地方有說明,如下:
具體Netty是如何管理PoolChunk的大家可以參考下這篇文章 [支撐百萬(wàn)級(jí)并發(fā)搔确,Netty如何實(shí)現(xiàn)高性能內(nèi)存管理]朋鞍,講的還是比較不錯(cuò)的,這里就不額外展開了妥箕。
我們回到服務(wù)滥酥,接著聊,生產(chǎn)部署完成后畦幢,找了下具體新部署的節(jié)點(diǎn)坎吻,觀察下剛剛上線的DIRECT_MEMORY_COUNTER監(jiān)控,N hours later......
果然宇葱,存在持續(xù)緩慢增長(zhǎng)的內(nèi)存泄漏問題瘦真,由于項(xiàng)目?jī)?nèi)部并沒有存在持續(xù)的基于Netty的IO操作刊头,因此將懷疑點(diǎn)下沉到底層組件。
復(fù)現(xiàn)
這里筆者將所有的業(yè)務(wù)邏輯代碼全部注掉诸尽,只保留微服務(wù)協(xié)議接口原杂,方法內(nèi)部只做了一個(gè)Thread.sleep(50);然后返回結(jié)果,并將代碼在線下本地部署您机;sdk端使用公司client進(jìn)行了兩輪壓測(cè)穿肄,壓測(cè)邏輯分別為串行1000次超時(shí)200ms的調(diào)用和串行1000次1ms超時(shí)的調(diào)用(這里僅僅只是為了模擬成功和失敗的兩種場(chǎng)景);請(qǐng)求入?yún)⑹褂昧艘粋€(gè)800kb的線上業(yè)務(wù)實(shí)體际看。
這里為了更便于復(fù)現(xiàn)咸产,服務(wù)啟動(dòng)時(shí)啟用了
-Dio.netty.allocator.type=unpooled 使用非Pool池管理
-Dio.netty.leakDetectionLevel=paranoid 啟用Netty堆外內(nèi)存泄漏檢測(cè)工具,級(jí)別=paranoid
有意思的一幕發(fā)生了
1000次200ms超時(shí)調(diào)用:
1000次1ms超時(shí)調(diào)用:
實(shí)驗(yàn)2直接Netty檢測(cè)內(nèi)存泄漏了仲闽,問題到此復(fù)現(xiàn)成功脑溢。
定位
那么,為什么超時(shí)會(huì)造成堆外內(nèi)存泄漏呢赖欣,這里我們沒有別的辦法屑彻,只能閱讀公司組件源碼,找找原因了顶吮。
首先看下公司服務(wù)端組件Netty相關(guān)的初始化動(dòng)作社牲,它繼承了ChannelInitializer饱亮,并在初始化階段追加了自定義的HttpHandler:
HttpHandler繼承了ChannelInboundHandlerAdapter,并且使用了CompositeByteBuf鸯绿,這里由于涉及到公司內(nèi)部核心組件规个,因此,只能用偽代碼進(jìn)行展示了钢拧,這里我們只截取一些Netty相關(guān)通用代碼邏輯塊:
public class HttpHandler extends ChannelInboundHandlerAdapter {
private HttpRequest req;
private final CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(32);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpRequest) {
try {
req = (HttpRequest) msg;
context = initReq(ctx, msg, req);
if (!req.decoderResult().isSuccess()) {
WriteUtils.write("請(qǐng)求無效, 解碼失敗", HttpResponseStatus.BAD_REQUEST, context);
return;
}
。巡揍。。
} catch (Exception e) {
WriteUtils.write(e.getMessage(), HttpResponseStatus.INTERNAL_SERVER_ERROR, context);
return;
}
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
ByteBuf bytebuf = content.content();
compositeByteBuf.addComponent(true, bytebuf);
if (msg instanceof LastHttpContent) {
// 校驗(yàn)請(qǐng)求
if (!validRequest(ctx, context)) return;
context.getInvocation().setRequestBody(compositeByteBuf.toString(CharsetUtil.UTF_8));
compositeByteBuf.clear();
compositeByteBuf.removeComponents(0, compositeByteBuf.numComponents());
// 具體業(yè)務(wù)行為傳遞
buildChainHandler().handle(context);
}
}
}
菌瘪。腮敌。。
}
看到這里俏扩,我們就必須了解下Netty對(duì)于Http協(xié)議的抽象定義了糜工,推薦這篇文章netty對(duì)http協(xié)議解析原理解析,這里就簡(jiǎn)單提下對(duì)于Http的幾種內(nèi)容的包裝:
HttpMethod:主要是對(duì)method的封裝录淡,包含method序列化的操作
HttpVersion: 對(duì)version的封裝捌木,netty包含1.0和1.1的版本
QueryStringDecoder: 主要是對(duì)url進(jìn)行封裝,解析path和url上面的參數(shù)嫉戚。(Tips:在tomcat中如果提交的post請(qǐng)求是application/x-www-form-urlencoded刨裆,則getParameter獲取的是包含url后面和body里面所有的參數(shù)澈圈,而在netty中,獲取的僅僅是url上面的參數(shù))
HttpHeaders:包含對(duì)header的內(nèi)容進(jìn)行封裝及操作
HttpContent:是對(duì)body進(jìn)行封裝帆啃,本質(zhì)上就是一個(gè)ByteBuf瞬女。如果ByteBuf的長(zhǎng)度是固定的,則請(qǐng)求的body過大努潘,可能包含多個(gè)HttpContent诽偷,其中最后一個(gè)為L(zhǎng)astHttpContent(空的HttpContent),用來說明body的結(jié)束。
HttpRequest:主要包含對(duì)Request Line和Header的組合
FullHttpRequest: 主要包含對(duì)HttpRequest和httpContent的組合
從生命周期上來講慈俯,HttpRequest -> HttpContent ...... -> LastHttpContent渤刃,一個(gè)完整的流程。
公司組件的代碼中贴膘,是將每個(gè)HttpContent類型的msg都放入CompositeByteBuf卖子,當(dāng)?shù)阶詈笠粋€(gè)LastHttpContent到達(dá)時(shí),組裝CompositeByteBuf中已經(jīng)寫入的所有HttpContent刑峡,然后清理CompositeByteBuf中所有的ByteBuf引用洋闽,并進(jìn)行清理(這里的清理也并不是立即執(zhí)行,而是會(huì)等到AbstractReferenceCountedByteBuf中的refCnt下一次變?yōu)?時(shí)觸發(fā)deallocate())突梦,再調(diào)用業(yè)務(wù)方法诫舅,直到ChannelHandler結(jié)束被回收,完成整個(gè)生命周期宫患。
那么刊懈,如果LastHttpContent沒有到來會(huì)怎么樣呢?
其實(shí)這也是timeout=1的那一輪測(cè)試所對(duì)應(yīng)的問題了娃闲,消息體發(fā)送不完整虚汛,這里就會(huì)存在LastHttpContent邏輯塊無法觸達(dá)的情況,也就是CompositeByteBuf所緩存的ByteBuf引用一直被持有皇帮,并且未被執(zhí)行手動(dòng)釋放操作卷哩,那么一直到ChannelHandler生命周期結(jié)束,堆外所開辟的空間都將一直被占用属拾,內(nèi)存泄漏将谊。
到此,我們似乎找到了一個(gè)可疑的內(nèi)存泄漏點(diǎn)渐白,那么如何證明就是它引起的呢尊浓?做法也很簡(jiǎn)單。
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("*** channelInactive");
super.channelInactive(ctx);
compositeByteBuf.clear();
compositeByteBuf.removeComponents(0, compositeByteBuf.numComponents());
}
在Inative階段纯衍,我們?cè)偃ナ謩?dòng)釋放一次CompositeByteBuf眠砾,下面來測(cè)試一下。
堆外內(nèi)存泄漏問題順利解決。
到此褒颈,其實(shí)我們已經(jīng)將問題順利定位柒巫,并提交公共組件負(fù)責(zé)團(tuán)隊(duì)進(jìn)行確認(rèn),并著手修復(fù)了谷丸。
那我們?cè)倩叵胂卤ぬ停@個(gè)場(chǎng)景時(shí)線下可以復(fù)現(xiàn)的情況下,我們嘗試通過測(cè)試手段進(jìn)行場(chǎng)景模擬刨疼,然后定位的泉唁,那么如果生產(chǎn)環(huán)境下,我們會(huì)怎么去定位這個(gè)問題呢揩慕,下面再聊聊當(dāng)時(shí)生產(chǎn)環(huán)境戰(zhàn)斗的過程亭畜,也是異常兇險(xiǎn)啊~~
開始下半場(chǎng)正文
由于是Netty堆外溢出,重新部署服務(wù)時(shí)迎卤,筆者保留了一臺(tái)線上故障節(jié)點(diǎn)拴鸵,可供回溯,就從這臺(tái)節(jié)點(diǎn)入手蜗搔。
首先期望可以定位到既然堆外內(nèi)存溢出劲藐,那么當(dāng)時(shí)堆外內(nèi)存泄漏的部分到底是什么內(nèi)容呢?
這里我們查看了服務(wù)進(jìn)行的內(nèi)存映射:
jps -m
pmap -pid
163: java -Duser.timezone=GMT+08 -server -Xms8192m -Xmx10240m -XX:NewSize=3072m -XX:MaxNewSize=3072m -XX:MaxDirectMemorySize=1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m-XX:+UseContainerSupport -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/xxx/xxx_heapDump.hprof -XX:+UseParNewGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1024M -XX:+ExplicitGCInv
0000000000400000 4K r-x-- java
0000000000600000 4K r---- java
0000000000601000 4K rw--- java
00000000016c0000 132K rw--- [ anon ]
0000000560800000 8388608K rw--- [ anon ]
0000000760800000 2097152K ----- [ anon ]
00000007e0800000 6528K rw--- [ anon ]
00000007e0e60000 1042048K ----- [ anon ]
00007f91b8000000 892K rw--- [ anon ]
00007f91b80df000 64644K ----- [ anon ]
00007f91bc000000 132K rw--- [ anon ]
00007f91bc021000 65404K ----- [ anon ]
00007f91c0000000 800K rw--- [ anon ]
00007f91c00c8000 64736K ----- [ anon ]
00007f91c4000000 980K rw--- [ anon ]
00007f91c40f5000 64556K ----- [ anon ]
00007f91c8000000 916K rw--- [ anon ]
00007f91c80e5000 64620K ----- [ anon ]
00007f91cc000000 608K rw--- [ anon ]
00007f91cc098000 64928K ----- [ anon ]
00007f91d0000000 932K rw--- [ anon ]
00007f91d00e9000 64604K ----- [ anon ]
00007f91d4000000 584K rw--- [ anon ]
00007f91d4092000 64952K ----- [ anon ]
00007f91d8000000 756K rw--- [ anon ]
00007f91d80bd000 64780K ----- [ anon ]
00007f91dc000000 1452K rw--- [ anon ]
00007f91dc16b000 64084K ----- [ anon ]
00007f91e0000000 612K rw--- [ anon ]
00007f91e0099000 64924K ----- [ anon ]
能看到不少anon的64M左右的連續(xù)空間樟凄,每一組例如892K + 64644K = 65536K 正好是64M聘芜,筆者線下也對(duì)比了堆外內(nèi)存泄漏前后相關(guān)內(nèi)存塊的變化:
發(fā)現(xiàn)所分配的rw內(nèi)存會(huì)持續(xù)增長(zhǎng),那么這里就準(zhǔn)備查看下這一部分變化的內(nèi)存中到底是哪些內(nèi)容缝龄。
這里會(huì)用到gdb調(diào)試工具汰现,如果有c或c++相關(guān)基礎(chǔ)的同學(xué)可以跳過這一部分:
這里附帶一份安裝指令集:
wget http://mirrors.ustc.edu.cn/gnu/gdb/gdb-7.9.1.tar.xz
tar -xf gdb-7.9.1.tar.xz
cd gdb-7.9.1
yum install texinfo
./configure
// 這里可能會(huì)拋異常no termcap library found
// 下載termcap -> https://ftp.gnu.org/gnu/termcap/
mkdir ../termcap
cd ../termcap
wget https://ftp.gnu.org/gnu/termcap/termcap-1.3.1.tar.gz
tar -zxvf termcap-1.3.1.tar.gz
cd termcap-1.3.1
./configure
make
make install
cd ../gdb-7.9.1
make install
gdb -v // 確認(rèn)安裝成功
// 這里如果遇到configure: error: no acceptable C compiler found in $PATH
// 則需要安裝gcc
yum install gcc
N minutes later......
我們終于可以開始使用gdb了,let's do it叔壤。
// 我們就上面分析的那一塊連續(xù)的64M內(nèi)存進(jìn)行dump快照瞎饲,查看具體的內(nèi)容
gdb -p 160
(gdb)dump memory 0x7f91b8000000_0x7f91b80df000.bin 0x7f91b8000000 0x7f91b80df000
(gdb)dump memory 0x7f91b80df000_0x7f91bc000000.bin 0x7f91b80df000 0x7f91bc000000
(gdb)quit
strings 0x7f91b8000000_0x7f91b80df000.bin > 0x7f91b8000000_0x7f91b80df000.log
strings 0x7f91b80df000_0x7f91bc000000.bin > 0x7f91b80df000_0x7f91bc000000.log
less 0x7f91b8000000_0x7f91b80df000.log
終于我們尋根之地,看到了最終的光明圣地百新,
這不正是我們的請(qǐng)求入?yún)⒄拿雌笕恚俏覀冊(cè)?G到最后一行看看庐扫,
果然饭望,非正常中斷,再看第二個(gè)內(nèi)存塊strings后的文件形庭,發(fā)現(xiàn)內(nèi)容為空铅辞,文件大小為0,筆者也在線下模擬內(nèi)存泄漏后進(jìn)行對(duì)比萨醒,發(fā)現(xiàn)持續(xù)的內(nèi)容也是非正常中斷斟珊,對(duì)應(yīng)了我們驗(yàn)證的LastHttpContent未到達(dá),且堆外內(nèi)存未進(jìn)行回收的結(jié)論富纸。
至此囤踩,整個(gè)Netty堆外內(nèi)存泄漏的排查定位過程結(jié)束旨椒。
總結(jié)
其實(shí)這個(gè)問題存在了很長(zhǎng)時(shí)間,至于到近期爆發(fā)堵漱,其實(shí)也是源于公司雙中心機(jī)房升級(jí)引起的综慎,部分下游服務(wù)異地部署,異地服務(wù)需要走專線勤庐,造成異地服務(wù)訪問耗時(shí)加長(zhǎng)示惊,堆外內(nèi)存泄漏的問題才會(huì)被放大。
總的來說愉镰,這次故障定位的過程很艱辛米罚,當(dāng)然,最終的結(jié)果也是很棒的丈探。
筆者在此也算沉淀出一些方法論录择,面對(duì)像OOM這樣的問題時(shí),首先要先明確类嗤,是哪一種OOM糊肠,有堆內(nèi)、堆外遗锣、方法區(qū)等等货裹,也有啟動(dòng)時(shí)階段或者運(yùn)行時(shí)階段,首先要明確當(dāng)下自己的場(chǎng)景精偿,因?yàn)椴粫?huì)有人比你更了解你的代碼弧圆,當(dāng)你堅(jiān)定問題方向后,也要義無反顧的堅(jiān)持下去笔咽,總會(huì)有自己不在行的領(lǐng)域搔预,只要肯多付出時(shí)間、精力叶组,總會(huì)有提高和回報(bào)拯田。
參考文獻(xiàn)
生活不易、各自努力
前路漫漫甩十,互勉同行