关于rocketmq:RocketMqBroker-启动脚本分析

53次阅读

共计 11959 个字符,预计需要花费 30 分钟才能阅读完成。

引言

继 [[【RocketMq】NameServ 启动脚本剖析(Ver4.9.4)]] 之后又来看看 Broker 的脚本。总体上来看大差不差,以浏览外围的配置局部调优为主。

mqbroker

#!/bin/sh  

if [-z "$ROCKETMQ_HOME"] ; then  
  ## resolve links - $0 may be a link to maven's home  
  PRG="$0"  
  
  # need this for relative symlinks  
  while [-h "$PRG"] ; do  
    ls=`ls -ld "$PRG"`  
    link=`expr "$ls" : '.*-> \(.*\)$'`  
    if expr "$link" : '/.*' > /dev/null; then  
      PRG="$link"  
    else  
      PRG="`dirname"$PRG"`/$link"  
    fi  
  done  
  saveddir=`pwd`  
  
  ROCKETMQ_HOME=`dirname "$PRG"`/..  
  
  # make it fully qualified  
  ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd`  
  
  cd "$saveddir"  
fi  
  
export ROCKETMQ_HOME  
  
sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.broker.BrokerStartup $@

后面的一大段脚本的最终目标就是获取 ROCKETMQ_HOME 的变量。

咱们关注最初一个脚本,这里调用了 runbroker.sh 的脚本:

sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.broker.BrokerStartup $@

runbroker.sh

runbroker.sh的脚本尽管内容很多,然而大部分和之前剖析 NameServ 的启动内容是重合的,这里间接跳过其余函数判断,只关注 JVM 的参数设置局部。

#!/bin/sh  

#===========================================================================================  
# Java Environment Setting  
#===========================================================================================  
error_exit ()  
{echo "ERROR: $1 !!"    exit 1}  
  
[! -e "$JAVA_HOME/bin/java"] && JAVA_HOME=$HOME/jdk/java  
[! -e "$JAVA_HOME/bin/java"] && JAVA_HOME=/usr/java  
[! -e "$JAVA_HOME/bin/java"] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"  
  
export JAVA_HOME  
export JAVA="$JAVA_HOME/bin/java"  
export BASE_DIR=$(dirname $0)/..  
export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH}  
  
#===========================================================================================  
# JVM Configuration  
#===========================================================================================  
# The RAMDisk initializing size in MB on Darwin OS for gc-log  
DIR_SIZE_IN_MB=600  
  
choose_gc_log_directory()  
{  
    case "`uname`" in  
        Darwin)  
            if [! -d "/Volumes/RAMDisk"]; then  
                # create ram disk on Darwin systems as gc-log directory  
                DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null  
                diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null  
                echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS."  
            fi  
            GC_LOG_DIR="/Volumes/RAMDisk"  
        ;;  
        *)  
            # check if /dev/shm exists on other systems  
            if [-d "/dev/shm"]; then  
                GC_LOG_DIR="/dev/shm"  
            else  
                GC_LOG_DIR=${BASE_DIR}  
            fi  
        ;;    esac}  
  
choose_gc_options()  
{JAVA_MAJOR_VERSION=$("$JAVA" -version 2>&1 | head -1 | cut -d'"'-f2 | sed's/^1\.//'| cut -d'.' -f1)  
    if [-z "$JAVA_MAJOR_VERSION"] || ["$JAVA_MAJOR_VERSION" -lt "8"] ; then  
      JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"    else  
      JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"    fi  
  
    if [-z "$JAVA_MAJOR_VERSION"] || ["$JAVA_MAJOR_VERSION" -lt "9"] ; then  
      JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"  
      JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"    else  
      JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"      JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"  
    fi  
}  
  
choose_gc_log_directory  
  
JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g"choose_gc_options  
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"JAVA_OPT="${JAVA_OPT} -XX:+AlwaysPreTouch"JAVA_OPT="${JAVA_OPT} -XX:MaxDirectMemorySize=15g"JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages -XX:-UseBiasedLocking"#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"  
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"  
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"  
  
numactl --interleave=all pwd > /dev/null 2>&1  
if [$? -eq 0]  
then  
 if [-z "$RMQ_NUMA_NODE"] ; then  
  numactl --interleave=all $JAVA ${JAVA_OPT} $@  
 else  
  numactl --cpunodebind=$RMQ_NUMA_NODE --membind=$RMQ_NUMA_NODE $JAVA ${JAVA_OPT} $@  
 fi  
else  
 $JAVA ${JAVA_OPT} $@  
fi

choose_gc_options() 剖析

先来看如何抉择 GC 参数局部。

choose_gc_options()  
{JAVA_MAJOR_VERSION=$("$JAVA" -version 2>&1 | head -1 | cut -d'"'-f2 | sed's/^1\.//'| cut -d'.' -f1)  
    if [-z "$JAVA_MAJOR_VERSION"] || ["$JAVA_MAJOR_VERSION" -lt "8"] ; 
    then  
      JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"       else  
      JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"    
    fi  
  
    if [-z "$JAVA_MAJOR_VERSION"] || ["$JAVA_MAJOR_VERSION" -lt "9"] ; then  
      JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"  
      JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"    
    else  
      JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"      
      JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"  
    fi  
}

粗略看去和 NameServ 的参数基本上没啥差异(难堪 ….),这里只好列举一些这些参数的作用了:

留神 Broker 对于小于 JDK8 的版本和小于 JDK9 的版本做了两种策略,这里的脚本其实是有点奇怪的,因为RocketMq 最低不是只反对 JDK8 么? 当然这样的脚本设置也不是不能够,只是没啥作用罢了。

因为怎么看怎么顺当,为了不便了解,集体调整了一下这个脚本的“实在用意”,针对 JDK8 和 JDK8 以下和 JDK8 以上三个分支判断:

  1. JDK8 以下用 CMS+ParNew 垃圾收集器经典组合
  2. G1 是在 JDK9 才成为默认垃圾收集器的,JDK8 须要手动设置应用 G1。须要留神这个版本 G1 是残血版本,Full Gc 是单线程的,JDK11 才被 Oracle 官网加上去。(顺带一提满血的 G1 从配置能够猜到是大量复用 CMS 的代码实现的)
  3. JDK8 之前的日志打印参数应用了 xloggc,JDK9 以及之后的版本用一个对立的打印参数 xlog 替换与之配合的附加参数,脚本洁净了很多。
  4. JDK 版本晋升能够看出官网在尽可能各方面简化垃圾收集器的参数管制,比方日志接口对立和简化。
choose_gc_options()
{JAVA_MAJOR_VERSION=$("$JAVA" -version 2>&1 | head -1 | cut -d'"'-f2 | sed's/^1\.//'| cut -d'.' -f1)
    # 如果以后版本小于 JDK1.8,应用 CMS+ParNew 垃圾收集器组合和相干参数
    if [-z "$JAVA_MAJOR_VERSION"] || ["$JAVA_MAJOR_VERSION" -lt "8"] ; then
      # CMS + ParNew 垃圾收集器
      # CMSInitiatingOccupancyFraction=70 示意当老年代达到 70% 时,触发 CMS 垃圾回收。#  CMSParallelRemarkEnabled 老年代收集器指定为 CMS 的时候无效,在进行了 Full GC 时对老年代进行压缩整顿,解决掉内存碎片。# UseConcMarkSweepGC 应用 CMS 老年代收集器
      # SoftRefLRUPolicyMSPerMB 软援用不给任何的存活工夫,对于序列化或者反射的对象在垃圾回收的时候踊跃清理
      # CMSClassUnloadingEnabled 启用对 Perm 区启用类回收,避免 Perm 区内存垃圾对象堆满
      # -XX:SurvivorRatio=8 Eden 区域在新生代占比,Eden 占新生代的 8 /10,From 幸存区和 To 幸存区各占新生代的 1 /10
      # -XX:-UseParNewGC ParNew 新生代垃圾收集器
       JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"

       # 垃圾收集器日志存储配置
       # PrintGCApplicationStoppedTime 打印利用因为 GC 而产生的进展工夫
       # 这个参数的次要作用是能够在 JVM 运行的时候动静调整新生代的 Eden、From、To 三个区域的区域调配,计算根据是 GC 过程中统计的 **GC 工夫、吞吐量、内存占用量 **。# -verbose:gc 和 -XX:+PrintGCDetails 垃圾收集时的信息打印 打印开启,大部分时候会一起配置
       # PrintGCDateStamps 打印 GC 产生时的工夫戳,搭配 -XX:+PrintGCDetails 应用,不能够独立应用
       # PrintAdaptiveSizePolicy 动静调整 Eden From To 三个区域的大小,判断根据为 GC 工夫、吞吐量、内存占用量
       JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"

       # 给予 5 个 GC 日志文件,每个文件 30M,如果 5 个文件写满,则从第一个文件笼罩。JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"

    # 如果以后版本等于 JDK 1.8
    else if ["$JAVA_MAJOR_VERSION" -eq "8"]; then
      # PrintGCApplicationStoppedTime 打印利用因为 GC 而产生的进展工夫
       # 这个参数的次要作用是能够在 JVM 运行的时候动静调整新生代的 Eden、From、To 三个区域的区域调配,计算根据是 GC 过程中统计的 **GC 工夫、吞吐量、内存占用量 **。# -verbose:gc 和 -XX:+PrintGCDetails 垃圾收集时的信息打印 打印开启,大部分时候会一起配置
       # PrintGCDateStamps 打印 GC 产生时的工夫戳,搭配 -XX:+PrintGCDetails 应用,不能够独立应用
       # PrintAdaptiveSizePolicy 动静调整 Eden From To 三个区域的大小,判断根据为 GC 工夫、吞吐量、内存占用量
      JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"
     # 给予 5 个 GC 日志文件,每个文件 30M,如果 5 个文件写满,则从第一个文件笼罩。JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
      # 触发全局并发标记的老年代应用占比,默认值 45%。# UseG1GC G1 垃圾收集器
      # SoftRefLRUPolicyMSPerMB 软援用不给任何的存活工夫,对于序列化或者反射的对象在垃圾回收的时候踊跃清理。# G1HeapRegionSize 16M 一个 Region 的大小能够通过参数 `-XX:G1HeapRegionSize` 设定,取值范畴从 1M 到 32M,且是 2 的指数。如果不设定,那么 G1 会依据 Heap 大小主动决定。JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
    fi
    
    
    # 如果是 JDK 9 以及 JDK9 之后的版本
    else
      JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
      # -Xlog 是 JDK9 对立日志参数,对于之前版本凌乱的 GC LOG 日志治理进行一波优化
      JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"
    fi
}

如果一个参数调研那么文章会没完没了,咱们间接拆分三局部进行浏览。

JDK8 以下的版本(无用)

实践上来说没屁用的 GC 参数,因为 RocketMq 规定了最低反对的 JDK 版本为 JDK1.8。

if [-z "$JAVA_MAJOR_VERSION"] || ["$JAVA_MAJOR_VERSION" -lt "8"] ; then
    
  # CMS + ParNew 垃圾收集器
  # CMSInitiatingOccupancyFraction=70 示意当老年代达到 70% 时,触发 CMS 垃圾回收。#  CMSParallelRemarkEnabled 老年代收集器指定为 CMS 的时候无效,在进行了 Full GC 时对老年代进行压缩整顿,解决掉内存碎片。# UseConcMarkSweepGC 应用 CMS 老年代收集器
  # SoftRefLRUPolicyMSPerMB 软援用不给任何的存活工夫,对于序列化或者反射的对象在垃圾回收的时候踊跃清理
  # CMSClassUnloadingEnabled 启用对 Perm 区启用类回收,避免 Perm 区内存垃圾对象堆满
  # -XX:SurvivorRatio=8 Eden 区域在新生代占比,Eden 占新生代的 8 /10,From 幸存区和 To 幸存区各占新生代的 1 /10
  # -XX:-UseParNewGC ParNew 新生代垃圾收集器
   JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"

   # 垃圾收集器日志存储配置
   # PrintGCApplicationStoppedTime 打印利用因为 GC 而产生的进展工夫
   # 这个参数的次要作用是能够在 JVM 运行的时候动静调整新生代的 Eden、From、To 三个区域的区域调配,计算根据是 GC 过程中统计的 **GC 工夫、吞吐量、内存占用量 **。# -verbose:gc 和 -XX:+PrintGCDetails 垃圾收集时的信息打印 打印开启,大部分时候会一起配置
   # PrintGCDateStamps 打印 GC 产生时的工夫戳,搭配 -XX:+PrintGCDetails 应用,不能够独立应用
   # PrintAdaptiveSizePolicy 动静调整 Eden From To 三个区域的大小,判断根据为 GC 工夫、吞吐量、内存占用量
   JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"

   # 给予 5 个 GC 日志文件,每个文件 30M,如果 5 个文件写满,则从第一个文件笼罩。JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"

JDK8 版本

从 JDK8 的版本开始,RocketMq 的垃圾收集器变更为 G1,对应的参数配置也是 G1 的配置。然而须要留神 -Xloggc 的配置文件利用了工夫戳进行格式化避免轮循反复笼罩的问题。其余参数曾经在 NameServ 的笔记中进行过剖析,集体把参数写入到命令上方不便查看

    # 如果以后版本等于 JDK 1.8
    else if ["$JAVA_MAJOR_VERSION" -eq "8"]; then
        
      # PrintGCApplicationStoppedTime 打印利用因为 GC 而产生的进展工夫
       # 这个参数的次要作用是能够在 JVM 运行的时候动静调整新生代的 Eden、From、To 三个区域的区域调配,计算根据是 GC 过程中统计的 **GC 工夫、吞吐量、内存占用量 **。# -verbose:gc 和 -XX:+PrintGCDetails 垃圾收集时的信息打印 打印开启,大部分时候会一起配置
       # PrintGCDateStamps 打印 GC 产生时的工夫戳,搭配 -XX:+PrintGCDetails 应用,不能够独立应用
       # PrintAdaptiveSizePolicy 动静调整 Eden From To 三个区域的大小,判断根据为 GC 工夫、吞吐量、内存占用量
      JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"
           
     # 给予 5 个 GC 日志文件,每个文件 30M,如果 5 个文件写满,则从第一个文件笼罩。JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
           
      # 触发全局并发标记的老年代应用占比,默认值 45%。# UseG1GC G1 垃圾收集器
      # SoftRefLRUPolicyMSPerMB 软援用不给任何的存活工夫,对于序列化或者反射的对象在垃圾回收的时候踊跃清理。JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
    fi

JDK9 及之后的版本

如果是之后的版本,则基本上的 GC 垃圾收集器和参数不变,然而须要留神 JDK9 之后因为 Xloggc 的参数被废除,用了 -xlog 的参数作为代替,这个起名的确比拟坑,因为和之前长的特地像。

# 如果是 JDK 9 以及 JDK9 之后的版本
else
  JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
  # -Xlog 是 JDK9 对立日志参数,对于之前版本凌乱的 GC LOG 日志治理进行一波优化
  JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"
fi

Oracle 官网文档也解释了这几个 JDK8 以及之前的日志打印参数被废除。

https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/logging.html#GUID-33074D03-B4F3-4D16-B9B6-8B0076661AAF

须要留神

因为日志参数打印属于 JVM 的领域,本节不做过多探讨。

总结

  1. JDK8 之前应用 Cms+ParNew,JDK8 以及之后的版本全副采纳 G1 垃圾收集器。
  2. NameServ 的启动脚本和 Broker 的相似,看懂任意一个就可以看懂另一个。
  3. Xlog 和 Xloggc 是比拟容易混同的中央,也是集体认为 Broker 启动脚本在不同版本判断启动参数理论最大的区别。
  4. 按照脚本的判断逻辑,上面的 JVM 参数在 JDK 9 及之后会呈现两次。
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"

写在最初

从脚本格调能够看出和改写 NameServ.sh 的人是同一个编写,所以有很多大量重复性的内容都给省略了,具体的介绍都放到了 nameserv.sh 的脚本剖析当中。

通篇看下来集体不太了解为什么要针对 JDK8 以前的版本做 JVM 参数调优,或者这就是工程师编写的谨严之处吧,思考全面,值得学习。

正文完
 0