国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐

這篇具有很好參考價值的文章主要介紹了JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

作者:vivo 互聯(lián)網(wǎng)服務器團隊 - Liu Zhen、Ye Wenhao

服務器內(nèi)存問題是影響應用程序性能和穩(wěn)定性的重要因素之一,需要及時排查和優(yōu)化。本文介紹了某核心服務內(nèi)存問題排查與解決過程。首先在JVM與大對象優(yōu)化上進行了有效的實踐,其次在故障轉移與大對象監(jiān)控上提出了可靠的落地方案。最后,總結了內(nèi)存優(yōu)化需要考慮的其他問題。

一、問題描述

音樂業(yè)務中,core服務主要提供歌曲、歌手等元數(shù)據(jù)與用戶資產(chǎn)查詢。隨著元數(shù)據(jù)與用戶資產(chǎn)查詢量的增長,一些JVM內(nèi)存問題也逐漸顯露,例如GC頻繁、耗時長,在高峰期RPC調(diào)用超時等問題,導致業(yè)務核心功能受損。

JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐,內(nèi)存,大對象,故障轉移,監(jiān)控,優(yōu)化

圖1 業(yè)務異常數(shù)量變化

二、分析與解決

通過對日志,機器CPU、內(nèi)存等監(jiān)控數(shù)據(jù)分析發(fā)現(xiàn):

YGC平均每分鐘次數(shù)12次,峰值為24次,平均每次的耗時在327毫秒。FGC平均每10分鐘0.08次,峰值1次,平均耗時30秒??梢钥吹紾C問題較為突出。

在問題期間,機器的CPU并沒有明顯的變化,但是堆內(nèi)存出現(xiàn)較大異常。圖2,黃色圓圈處,內(nèi)存使用急速上升,F(xiàn)GC變的頻繁,釋放的內(nèi)存越來越少。

JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐,內(nèi)存,大對象,故障轉移,監(jiān)控,優(yōu)化

圖2 老年代內(nèi)存使用異常

因此,我們認為業(yè)務功能異常是機器的內(nèi)存問題導致的,需要對服務的內(nèi)存做一次專項優(yōu)化。

  • 步驟1 JVM優(yōu)化

以下是默認的JVM參數(shù):

-Xms4096M -Xmx4096M -Xmn1024M -XX:MetaspaceSize=256M -Djava.security.egd=file:/dev/./urandom -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/{runuser}/logs/other

如果不指定垃圾收集器,那么JDK 8默認采用的是Parallel Scavenge(新生代) +Parallel Old(老年代),這種組合在多核CPU上充分利用多線程并行的優(yōu)勢,提高垃圾回收的效率和吞吐量。但是,由于采用多線程并行方式,會造成一定的停頓時間,不適合對響應時間要求較高的應用程序。然而,core這類的服務特點是對象數(shù)量多,生命周期短。在系統(tǒng)特點上,吞吐量較低,要求時延低。因此,默認的JVM參數(shù)并不適合core服務。

根據(jù)業(yè)務的特點和多次對照實驗,選擇了如下參數(shù)進行JVM優(yōu)化(4核8G的機器)。該參數(shù)將young區(qū)設為原來的1.5倍,減少了進入老年代的對象數(shù)量。將垃圾回收器換成ParNew+CMS,可以減少YGC的次數(shù),降低停頓時間。此外還開啟了CMSScavengeBeforeRemark,在CMS的重新標記階段進行一次YGC,以減少重新標記的時間。

-Xms4096M -Xmx4096M -Xmn1536M -XX:MetaspaceSize=256M -XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark -Djava.security.egd=file:/dev/./urandom -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/{runuser}/logs/other

JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐,內(nèi)存,大對象,故障轉移,監(jiān)控,優(yōu)化

圖3 JVM優(yōu)化前后的堆內(nèi)存對比

優(yōu)化后效果如圖3,堆內(nèi)存的使用明顯降低,但是Dubbo超時仍然存在。

我們推斷,在業(yè)務高峰期,該節(jié)點出現(xiàn)了大對象晉升到了老年代,導致內(nèi)存使用迅速上升,并且大對象沒有被及時回收。那如何找到這個大對象及其產(chǎn)生的原因呢?為了降低問題排查期間業(yè)務的損失,提出了臨時的故障轉移策略,盡量降低異常數(shù)量。

  • 步驟2 故障轉移策略

在api服務調(diào)用core服務出現(xiàn)異常時,將出現(xiàn)異常的機器ip上報給監(jiān)控平臺。然后利用監(jiān)控平臺的統(tǒng)計與告警能力,配置相應的告警規(guī)則與回調(diào)函數(shù)。當異常觸發(fā)告警,通過配置的回調(diào)函數(shù)將告警ip傳遞給api服務,此時api服務可以將core服務下的該ip對應的機器視為“故障”,進而通過自定義的故障轉移策略(實現(xiàn)Dubbo的AbstractLoadBalance抽象類,并且配置在項目),自動將該ip從提供者集群中剔除,從而達到不去調(diào)用問題機器。圖 4 是整個措施的流程。在該措施上線前,每當有機器內(nèi)存告警時,將會人工重啟該機器。

JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐,內(nèi)存,大對象,故障轉移,監(jiān)控,優(yōu)化

圖4 故障轉移策略
  • 步驟3 大對象優(yōu)化

大對象占用了較多的內(nèi)存,導致內(nèi)存空間無法被有效利用,甚至造成OOM(Out Of Memory)異常。在優(yōu)化過程中,先是查看了異常期間的線程信息,然后對堆內(nèi)存進行了分析,最終確定了大對象身份以及產(chǎn)生的接口。

(1) Dump Stack 查看線程

從監(jiān)控平臺上Dump Stack文件,發(fā)現(xiàn)一定數(shù)量的如下線程調(diào)用。

Thread 5612: (state = IN_JAVA)
 - org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.encodeResponse(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, org.apache.dubbo.remoting.exchange.Response) @bci=11, line=282 (Compiled frame; information may be imprecise)
 - org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.encode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, java.lang.Object) @bci=34, line=73 (Compiled frame)
 - org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec.encode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, java.lang.Object) @bci=7, line=40 (Compiled frame)
 - org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter$InternalEncoder.encode(io.netty.channel.ChannelHandlerContext, java.lang.Object, io.netty.buffer.ByteBuf) @bci=51, line=69 (Compiled frame)
 - io.netty.handler.codec.MessageToByteEncoder.write(io.netty.channel.ChannelHandlerContext, java.lang.Object, io.netty.channel.ChannelPromise) @bci=33, line=107 (Compiled frame)
 - io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(java.lang.Object, io.netty.channel.ChannelPromise) @bci=10, line=717 (Compiled frame)
 - io.netty.channel.AbstractChannelHandlerContext.invokeWrite(java.lang.Object, io.netty.channel.ChannelPromise) @bci=10, line=709 (Compiled frame)
...

state = IN_JAVA 表示Java虛擬機正在執(zhí)行Java程序。從線程調(diào)用信息可以看到,Dubbo正在調(diào)用Netty,將輸出寫入到緩沖區(qū)。此時的響應可能是一個大對象,因而在對響應進行編碼、寫緩沖區(qū)時,需要耗費較長的時間,導致抓取到的此類線程較多。另外耗時長,也即是大對象存活時間長,導致full gc 釋放的內(nèi)存越來越小,空閑的堆內(nèi)存變小,這又會加劇full gc 次數(shù)。

這一系列的連鎖反應與圖2相吻合,那么接下來的任務就是找到這個大對象。

(2)Dump Heap 查看內(nèi)存

對core服務的堆內(nèi)存進行了多次查看,其中比較有代表性的一次快照的大對象列表如下,

JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐,內(nèi)存,大對象,故障轉移,監(jiān)控,優(yōu)化

圖5 core服務的堆內(nèi)存快照
整個Netty的taskQueue有258MB。并且從圖中綠色方框處可以發(fā)現(xiàn),單個的Response竟達到了9M,紅色方框處,顯示了調(diào)用方的服務名以及URI。

進一步排查,發(fā)現(xiàn)該接口會通過core服務查詢大量信息,至此基本排查清楚了大對象的身份以及產(chǎn)生原因。

(3)優(yōu)化結果

在對接口進行優(yōu)化后,整個core服務也出現(xiàn)了非常明顯的改進。YGC全天總次數(shù)降低了76.5%,高峰期累計耗時降低了75.5%。FGC三天才會發(fā)生一次,并且高峰期累計耗時降低了90.1%。

JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐,內(nèi)存,大對象,故障轉移,監(jiān)控,優(yōu)化

圖6 大對象優(yōu)化后的core服務GC情況
盡管優(yōu)化后,因內(nèi)部異常導致獲取核心業(yè)務失敗的異常請求數(shù)顯著減少,但是依然存在。為了找到最后這一點異常產(chǎn)生的原因,我們打算對core服務內(nèi)存中的對象大小進行監(jiān)控。

JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐,內(nèi)存,大對象,故障轉移,監(jiān)控,優(yōu)化

圖7 系統(tǒng)內(nèi)部異常導致核心業(yè)務失敗的異常請求數(shù)
  • 步驟4 無侵入式內(nèi)存對象監(jiān)控

Debug Dubbo 源碼的過程中,發(fā)現(xiàn)在網(wǎng)絡層,Dubbo通過encodeResponse方法對響應進行編碼并寫入緩沖區(qū),通過checkPayload方法去檢查響應的大小,當超過payload時,會拋出ExceedPayloadLimitException異常。在外層對異常進行了捕獲,重置buffer位置,而且如果是ExceedPayloadLimitException異常,重新發(fā)送一個空響應,這里需要注意,空響應沒有原始的響應結果信息,源碼如下。

//org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#encodeResponse
protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
    //...省略部分代碼
    try {
 
        //1、檢查響應大小是否超過 payload,如果超過,則拋出ExceedPayloadLimitException異常
        checkPayload(channel, len);
 
 
    } catch (Throwable t) {
         
        //2、重置buffer
        buffer.writerIndex(savedWriteIndex);
 
        //3、捕獲異常后,生成一個新的空響應
        Response r = new Response(res.getId(), res.getVersion());
        r.setStatus(Response.BAD_RESPONSE);
         
        //4、ExceedPayloadLimitException異常,將生成的空響應重新發(fā)送一遍
        if (t instanceof ExceedPayloadLimitException) {
            r.setErrorMessage(t.getMessage());
            channel.send(r);
            return;
        }
         
    }
}
 
//org.apache.dubbo.remoting.transport.AbstractCodec#checkPayload
protected static void checkPayload(Channel channel, long size) throws IOException {
    int payload = getPayload(channel);
    boolean overPayload = isOverPayload(payload, size);
    if (overPayload) {
        ExceedPayloadLimitException e = new ExceedPayloadLimitException("Data length too large: " + size + ", max payload: " + payload + ", channel: " + channel);
        logger.error(e);
        throw e;
    }
}

受此啟發(fā),自定義了編解碼類(實現(xiàn)org.apache.dubbo.remoting.Codec2接口,并且配置在項目),去監(jiān)控超出閾值的對象,并打印請求的詳細信息,方便排查問題。在具體實現(xiàn)中,如果特意去計算每個對象的大小,那么勢必是對服務性能造成影響。經(jīng)過分析,采取了和checkPayload一樣的方式,根據(jù)編碼前后buffer的writerIndex位置去判斷有沒有超過設定的閾值。代碼如下。

/**
 * 自定義dubbo編碼類
 **/
public class MusicDubboCountCodec implements Codec2 {
 
    /**
     * 異常響應池:緩存超過payload大小的responseId
     */
    private static Cache<Long, String> EXCEED_PAYLOAD_LIMIT_CACHE = Caffeine.newBuilder()
        // 緩存總條數(shù)
        .maximumSize(100)
        // 過期時間
        .expireAfterWrite(300, TimeUnit.SECONDS)
        // 將value設置為軟引用,在OOM前直接淘汰
        .softValues()
        .build();
 
 
    @Override
    public void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException {
        //1、記錄數(shù)據(jù)編碼前的buffer位置
        int writeBefore = null == buffer ? 0 : buffer.writerIndex();
 
        //2、調(diào)用原始的編碼方法
        dubboCountCodec.encode(channel, buffer, message);
 
        //3、檢查&記錄超過payload的信息
        checkOverPayload(message);
 
        //4、計算對象長度
        int writeAfter = null == buffer ? 0 : buffer.writerIndex();    
        int length = writeAfter - writeBefore;
 
        //5、超過告警閾值,進行日志打印處理
        warningLengthTooLong(length, message);
    }
 
    //校驗response是否超過payload,超過了,緩存id
    private void checkOverPayload(Object message){
        if(!(message instanceof Response)){
            return;
        }
        Response response = (Response) message;
 
        //3.1、新的發(fā)送過程:通過狀態(tài)碼BAD_RESPONSE與錯誤信息識別出空響應,并記錄響應id
        if(Response.BAD_RESPONSE == response.getStatus() && StrUtil.contains(response.getErrorMessage(), OVER_PAYLOAD_ERROR_MESSAGE)){          
            EXCEED_PAYLOAD_LIMIT_CACHE.put(response.getId(), response.getErrorMessage());
            return;
        }
 
        //3.2、原先的發(fā)送過程:通過異常池識別出超過payload的響應,打印有用的信息
        if(Response.OK == response.getStatus() &&  EXCEED_PAYLOAD_LIMIT_CACHE.getIfPresent(response.getId()) != null){      
            String responseMessage = getResponseMessage(response);
            log.warn("dubbo序列化對象大小超過payload,errorMsg is {},response is {}", EXCEED_PAYLOAD_LIMIT_CACHE.getIfPresent(response.getId()),responseMessage);
        }
    }
     
}

在上文中提到,當捕獲到超過payload的異常時,會重新生成空響應,導致失去了原始的響應結果,此時再去打印日志,是無法獲取到調(diào)用方法和入?yún)⒌?,但是encodeResponse方法步驟4中,重新發(fā)送這個Response,給了我們機會去獲取到想要的信息,因為重新發(fā)送意味著會再去走一遍自定義的編碼類。

假設有一個超出payload的請求,執(zhí)行到自定編碼類encode方法的步驟2(Dubbo源碼中的編碼方法),在這里會調(diào)用encodeResponse方法重置buffer,發(fā)送新的空響應。

(1)當這個新的空響應再次進入自定義encode方法,執(zhí)行 checkOverPayload方法的步驟3.1時,就會記錄異常響應的id到本地緩存。由于在encodeResponse中buffer被重置,無法計算對象的大小,所以步驟4、5不會起到實際作用,就此結束新的發(fā)送過程。

(2)原先的發(fā)送過程回到步驟2 繼續(xù)執(zhí)行,到了步驟3.2 時,發(fā)現(xiàn)本地緩存的異常池中有當前的響應id,這時就可以打印調(diào)用信息了。

綜上,對于大小在告警閾值和payload之間的對象,由于響應信息成功寫入了buffer,可以直接進行大小判斷,并且打印響應中的關鍵信息;對于超過payload的對象,在重新發(fā)送中記錄異常響應id到本地,在原始發(fā)送過程中訪問異常id池識別是否是異常響應,進行關鍵信息打印。

在監(jiān)控措施上線后,通過日志很快速的發(fā)現(xiàn)了一部分產(chǎn)生大對象的接口,當前也正在根據(jù)接口特點做針對性優(yōu)化。

三、總結

在對服務JVM內(nèi)存進行調(diào)優(yōu)時,要充分利用日志、監(jiān)控工具、堆棧信息等,分析與定位問題。盡量降低問題排查期間的業(yè)務損失,引入對象監(jiān)控手段也不能影響現(xiàn)有業(yè)務。除此之外,還可以在定時任務、代碼重構、緩存等方面進行優(yōu)化。優(yōu)化服務內(nèi)存不僅僅是JVM調(diào)參,而是一個全方面的持續(xù)過程。文章來源地址http://www.zghlxwxcb.cn/news/detail-683486.html

到了這里,關于JVM 內(nèi)存大對象監(jiān)控和優(yōu)化實踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉載,請注明出處: 如若內(nèi)容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 【Flume】高級組件之Sink Processors及項目實踐(Sink負載均衡和故障轉移)

    【Flume】高級組件之Sink Processors及項目實踐(Sink負載均衡和故障轉移)

    ???????Sink Processors類型包括這三種:Default Sink Processor、Load balancing Sink Processor和Failover Sink Processor。 Default Sink Processor是默認的,不用配置Sink group,就是咱們現(xiàn)在使用的這種最普通的形式,一個Channel后面接一個Sink的形式; Load balancing Sink Processor是負載均衡處理器,一個

    2024年02月10日
    瀏覽(15)
  • JVM 性能監(jiān)控與故障處理工具

    JVM 性能監(jiān)控與故障處理工具

    基礎工具 jps:虛擬機進程狀態(tài)工具 jps 命令格式: jps [options] [hostid] 命令可選項解釋: 選項 解釋 -q 只輸出 LVMID,省略主類的名稱 -m 輸出傳給 main 函數(shù)的參數(shù) -l 輸出主類的全名,如果進程運行的 JAR 包,則輸出 JAR 包的路徑 -v 輸出虛擬機進程啟動時的 JVM 參數(shù) jstat:虛擬機統(tǒng)

    2024年02月07日
    瀏覽(24)
  • Jvm創(chuàng)建對象之內(nèi)存分配-JVM(七)

    Jvm創(chuàng)建對象之內(nèi)存分配-JVM(七)

    上篇文章介紹了jvm創(chuàng)建,會校驗是否已加載類,沒有則加載,通過之前學的源碼,classLoader加載完之后,虛擬機開始給類分配內(nèi)存,指針移動分配和free鏈表分配,解決并發(fā)分配情況用cap和TLAB方法。之后設置對象頭部信息,有mark word線程鎖,分代年齡等,klass pointer。還有指針

    2024年02月13日
    瀏覽(25)
  • 四、JVM-對象內(nèi)存模型

    四、JVM-對象內(nèi)存模型

    一個Java對象在內(nèi)存中包括3個部分:對象頭、實例數(shù)據(jù)和對齊填充 數(shù)據(jù) 內(nèi)存 – CPU 寄存器 -127 補碼 10000001 - 11111111 32位的處理器 一次能夠去處理32個二進制位 4字節(jié)的數(shù)據(jù) 64位操作系統(tǒng) 8字節(jié) 2的64次方的尋址空間 指針壓縮技術 JDK1.6出現(xiàn)的 開啟了指針壓縮 什么時候指針壓縮會

    2024年02月14日
    瀏覽(19)
  • jvm對象內(nèi)存劃分

    jvm內(nèi)存空間是邏輯上連續(xù)的虛擬地址空間(虛擬內(nèi)存中的概念)映射到物理內(nèi)存(不一定連續(xù)),物理內(nèi)存不足時還會將物理內(nèi)存中的數(shù)據(jù)交換到swap(磁盤的一塊區(qū)域)。 這塊水有點深,查閱資料做了個個人的總結。 內(nèi)存劃分,有 指針碰撞 和 空閑列表 這兩種劃分方式: 1、

    2024年02月07日
    瀏覽(18)
  • JVM 給對象分配內(nèi)存空間

    指針碰撞 空閑列表 TLAB 為對象分配空間的任務實際上便等同于把一塊確定大小的內(nèi)存塊從Java堆中劃分出來。 指針碰撞:(Bump The Pointer) 堆的內(nèi)存是絕對規(guī)整的,內(nèi)存主要分為兩部分,所有使用過的內(nèi)存被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個指針作為分界點

    2024年02月11日
    瀏覽(20)
  • JVM面試題-JVM對象的創(chuàng)建過程、內(nèi)存分配、內(nèi)存布局、訪問定位等問題詳解

    JVM面試題-JVM對象的創(chuàng)建過程、內(nèi)存分配、內(nèi)存布局、訪問定位等問題詳解

    內(nèi)存分配的兩種方式 指針碰撞 適用場合:堆內(nèi)存 規(guī)整 (即沒有內(nèi)存碎片)的情況下。 原理:用過的內(nèi)存全部整合到一邊,沒有用過的內(nèi)存放在另一邊,中間有一個分界指針,只需要向著沒用過的內(nèi)存方向?qū)⒃撝羔樢苿訉ο髢?nèi)存大小位置即可。 使用該分配方式的GC收集器:

    2024年02月08日
    瀏覽(19)
  • 貨拉拉智能監(jiān)控實踐:如何解決多云架構下的故障應急問題?

    貨拉拉智能監(jiān)控實踐:如何解決多云架構下的故障應急問題?

    在月活超千萬的大規(guī)模業(yè)務背景下,貨拉拉遭遇了多云環(huán)境下的監(jiān)控碎片化、規(guī)劃無序等問題。為了應對這些挑戰(zhàn),貨拉拉開發(fā)了一站式監(jiān)控平臺——Monitor。該平臺的部署有效地實現(xiàn)了對核心應用的監(jiān)控和報警全覆蓋,顯著提高了應急響應的效率:超過 72%的云應急事件能在

    2024年02月01日
    瀏覽(15)
  • JVM—對象的創(chuàng)建流程與內(nèi)存分配

    JVM—對象的創(chuàng)建流程與內(nèi)存分配

    對象創(chuàng)建的流程圖如下: 內(nèi)存分配的方式有兩種: 指針碰撞(Bump the Pointer) 空閑列表(Free List) 分配方式 說明 收集器 指針碰撞(Bump the Pointer) 內(nèi)存地址是連續(xù)的(新生代) Serial和ParNew收集器 空閑列表(Free List) 內(nèi)存地址不連續(xù)(老年代) CMS收集器和Mark-Sweep收集器

    2024年04月10日
    瀏覽(30)
  • 06-JVM對象內(nèi)存回收機制深度剖析

    06-JVM對象內(nèi)存回收機制深度剖析

    上一篇:05-JVM內(nèi)存分配機制深度剖析 堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷哪些對象已經(jīng)死亡( 即不能再被任何途徑使用的對象 )。 給對象中添加一個引用計數(shù)器,每當有一個地方引用它,計數(shù)器就加1;當引用失效,計數(shù)器就減1;任何時候計

    2024年02月09日
    瀏覽(22)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包