skywalking源码的搭建

zh

 

 上面有两种编译方式

第一种是通过git clone源码

 

 出现上面的提示是网络原因导致下载失败,可以参考下面的解决办法解决

哔哩哔哩上面直击痛点:一招搞定GitHub开源项目下载加速! - 1.开源项目下载优化(Av94251133,P1).mp4

 

 接下来我们要切换到tag 为v 8.2.0的代码

执行下面的两个命令

 

 接下来我们进入到skywalking的目录执行下面的两个命令

 千万要注意上面的两个操作不能出现错误

 

 

 完成上面的动作之后我们就可以开始skywalking的编译了

执行编译的命令如下

第二在编译的时候一定要注意maven地址的下载,不能仅仅只配置阿里云的仓库的地址,还需要配置官方仓库的下载地址,因为有的插件在阿里云上面没有必须到官网上面去下载,这里maven的配置仓库地址如下

         <!-- 阿里云仓库 -->
        <mirror>
            <id>alimaven</id>
            <mirrorOf>central</mirrorOf>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
        </mirror>
    
        <!-- 中央仓库1 -->
        <mirror>
            <id>repo1</id>
            <mirrorOf>central</mirrorOf>
            <name>Human Readable Name for this Mirror.</name>
            <url>http://repo1.maven.org/maven2/</url>
        </mirror>
    
        <!-- 中央仓库2 -->
        <mirror>
            <id>repo2</id>
            <mirrorOf>central</mirrorOf>
            <name>Human Readable Name for this Mirror.</name>
            <url>http://repo2.maven.org/maven2/</url>
        </mirror>

在编译的过程中报如下的错误

[ERROR] Failed to execute goal org.xolstice.maven.plugins:protobuf-maven-plugin:
0.6.1:compile (grpc-build) on project apm-network: protoc did not exit cleanly.
Review output for more information. -> [Help 1]

第一种网上的说法是升级maven的版本到3.6.3,我们升级了之后还是存在问题

最终的解决办法是重启电脑之后,在执行mvn命令居然成功了

 

在编译skywalking的web模块的时候,npm的仓库我们需要修改下,不能使用官网的,使用官网的整个下载会非常的缓慢

我们进入到apm-webapp模块,修改pom.xml文件,我们使用淘宝的地址

 

 

 

 这里即使弄成了淘宝的地址也会报错,那么如何解决了

参考这篇博客:https://blog.csdn.net/smooth00/article/details/106921934

我们手动编译前端工程

首先我们要先安装成功npm工具

我们进入G:\skywalking-8.2.0-code-1206\skywalking-ui目录下

我们执行下面的命令,我们将npm仓库的地址设置设为

npm config set registry "http://registry.npmjs.org/"

我们不使用淘宝的镜像仓库的地址

接下来我们执行npm install命令

执行的过程中报下面的错误

PS D:\workspace\podcast-oms> npm install --registry=https://registry.npm.taobao.org
npm WARN tarball tarball data for postcss@5.2.18 (sha1-ut+hSX1GJE9jkPWLMZgw2RB4U8U=) seems to be corrupted. Trying one more time.
npm ERR! path D:\workspace\podcast-oms\node_modules\.staging\postcss-8e12407f\lib\parser.js
npm ERR! code EPERM
npm ERR! errno -4048
npm ERR! syscall unlink
npm ERR! Error: EPERM: operation not permitted, unlink 'D:\workspace\podcast-oms\node_modules\.staging\postcss-8e12407f\lib\parser.js'npm ERR!  { [Error: EPERM: operation not permitted, unlink 'D:\workspace\podcast-oms\node_modules\.staging\postcss-8e12407f\lib\parser.js']
npm ERR!   cause:
npm ERR!    { Error: EPERM: operation not permitted, unlink 'D:\workspace\podcast-oms\node_modules\.staging\postcss-8e12407f\lib\parser.js'
npm ERR!      errno: -4048,
npm ERR!      code: 'EPERM',
npm ERR!      syscall: 'unlink',
npm ERR!      path:
npm ERR!       'D:\\workspace\\podcast-oms\\node_modules\\.staging\\postcss-8e12407f\\lib\\parser.js' },
npm ERR!   stack:
npm ERR!    'Error: EPERM: operation not permitted, unlink \'D:\\workspace\\podcast-oms\\node_modules\\.staging\\postcss-8e12407f\\lib\\parser.js\'',
npm ERR!   errno: -4048,
npm ERR!   code: 'EPERM',
npm ERR!   syscall: 'unlink',
npm ERR!   path:
npm ERR!    'D:\\workspace\\podcast-oms\\node_modules\\.staging\\postcss-8e12407f\\lib\\parser.js',
npm ERR!   parent: 'postcss-minify-gradients' }
npm ERR!
npm ERR! The operation was rejected by your operating system.
npm ERR! It's possible that the file was already in use (by a text editor or antivirus),
npm ERR! or that you lack permissions to access it.
npm ERR!
npm ERR! If you believe this might be a permissions issue, please double-check the
npm ERR! permissions of the file and its containing directories, or try running
npm ERR! the command again as root/Administrator (though this is not recommended).
It's possible that the file was already in use (by a text editor or antivirus)我们把
It's possible that the file was already in use (by a text editor or antivirus)

 

 使用了上面的办法还是没有解决问题,我们使用阿里的cnpm来解决

https://developer.aliyun.com/mirror/NPM?from=tnpm

我们进入到skywalking-ui

执行下面的命令

npm install -g cnpm --registry=https://registry.npm.taobao.org
看到下面的就表示执行成功了

 接下来要在skywalking-ui模块下执行cnpm-install命令

 

 

 

 

 接下来我们执行打包命令执行npm run build命令,执行成功之后打包就成功了

 

 

 

 会在skywalking-ui下面生成一个dist目录

 

 独立编译成的UI dist文件,也是可以放到apm-webapp中打包的,可以将上图dist中的文件拷贝到apm-webapp\target\classes\public下,然后修改apm-webapp\pom.xml,将npm install和build过程都注释了:

 接下来进入到skywalking源码目录执行下面的编译命令进行编译

 

mvn  clean package install -Denforcer.skip=true  -Dmaven.test.skip=true -Dcheckstyle.skip=true
就可以进行打包了,但是在打包的过程中会报错

 

 会报下面的错误

[WARNING] The assembly descriptor contains a *nix-specific root-relative-referen

因为当前是在windows环境,在binary,xml中写的确认是linux环境的路径,解决的办法就是

手动的apache-skywalking-apm-8.2.0的源码下面创建dist目录,在dist目录下面创建下面的目录,将上面编译完成的文件按照binary,xml定义的路径拷贝到下面的文件夹中

 

 接下来我们要在idea中启动skywalking集群的源码

 接下来我们使用idea打开项目

 

 https://my.oschina.net/mingshashan/blog/3167233?from=groupmessage

然后查看设置生成的源代码(主要是看potobuf文件编译生成的源代码)

  • apm-protocol/apm-network/target/generated-sources/protobuf 选中这个目录下面的grpc-javajava,然后右键选择Mark Directory As-->Generated Sources Root如下图所示

  • oap-server/server-core/target/generated-sources/protobuf目录的grpc-java 和 java 文件夹Mark Directory As-->Generated Sources Root

  • oap-server/server-receiver-plugin/receiver-proto/target/generated-sources/protobuf 目录的grpc-java 和 java 文件夹Mark Directory As-->Generated Sources Root

  • oap-server/exporter/target/generated-sources/protobuf目录的grpc-java 和 java 文件夹Mark Directory As-->Generated Sources Root

  • oap-server/server-configuration/grpc-configuration-sync/target/generated-sources/protobuf目录的grpc-java 和 java 文件夹Mark Directory As-->Generated Sources Root

  • oap-server/oal-grammar/target/generated-sources目录的grpc-java 和 java 文件夹Mark Directory As-->Generated Sources Root

 这里只需要导入上面的几个目录作为源码目录,如果多导入就会代码报错,下面我就是多导入了下面的目录结果在编译的时候代码就发生了异常,antlr4这个目录是不能被导入成源码的

 

 第二个代码的源码中包下面的错误

 

 我用的idea版本是2018的版本,我安装了lombook的插件,但是本地使用的maven版本是3.6.3版本,我把maven版本更换成3.3.9版本就好了

 

 

 

第二种是直接从apache skywalking下载的源码,直接编译

 这里我采用的是第二种直接从从apache skywalking下载的源码,直接编译

进入到skywalking源码目录执行下面的编译命令进行编译

mvn  clean package install -Denforcer.skip=true  -Dmaven.test.skip=true -Dcheckstyle.skip=true

在编译的过程中报probuffer协议编译失败,无论如何都搞不定,结果重启电脑之后,再次编译就好了、

 

 

 

 

 

 首先skywalking中存在前端

一定要保证npm 和node已经安装成功

 

 

 

 

 

 上面保存的原因是node-8.17.0-win-x64.zip文件下载失败,我们需要手动下载node-8.17.0-win-x64.zip放在对应的maven库中,接下来在编译的时候报错

 

 

 

 我们需要将npm仓库的地址换成国内的仓库地址

并且需要把 apm-webapp工程的pom文件中npm的下载地址改成国内的,不然访问在编译过程中会因为访问不了国外的仓库而报错

 

 

上面的编译成功之后,我们需要将代码 导入到eclipse中

 

 

 我们在eclipse中需要将编译之后的文件夹设置为源码

eclipse设置源码的方式如所示

 

 

 

  • 设置 gRPC 的自动生成的代码目录,为源码目录 :
  • 将 apm-protocol/apm-network/target/generated-sources/protobuf 目录下面grpc-java 和 java 目录右键设置为 Generated Rources Root 。
  • 将 oap-server/server-core/target/generated-sources/protobuf 目录下面grpc-java 和 java目录右键设置为 Generated Rources Root 。
  • 将 oap-server/server-receiver-plugin/skywalking-istio-telemetry-receiver-plugin/target/generated-sources/protobuf目录下面grpc-java 和 java 目录右键设置为 Generated Rources Root 。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

IDEA 运行

skywalking的源码中存在lombok组件,我们需要安装lombok的插件

  1. 首先我们需要安装IntelliJ IDEA中的lombok插件,打开IntelliJ IDEA后点击菜单栏中的File-->Settings,或者使用快捷键Ctrl+Alt+S进入到设置页面。

    IntelliJ IDEA lombok插件的安装和使用
  2. 我们点击设置中的Plugins进行插件的安装,在右侧选择Browse repositories...,然后在搜索页面输入lombok变可以查询到下方的Lombok Plugin,鼠标点击Lombok Plugin可在右侧看到Install按钮,点击该按钮便可安装。

    IntelliJ IDEA lombok插件的安装和使用
    IntelliJ IDEA lombok插件的安装和使用
    IntelliJ IDEA lombok插件的安装和使用
    IntelliJ IDEA lombok插件的安装和使用
  3. 我们在安装页面可以看到lombok具体支持的所有注解,在安装过程中有Downloading Plugins的提示,安装过程中进度条会变化。需要提醒的是,在安装过程中一定要保证网络连接可用且良好,否则可能会安装失败。安装成功后我们可以看到右侧的Restart按钮,此时可先不操作,因为我们还有后续的配置工作。安装完成后我们再回到Plugins,此时在右侧可以搜索到lombok,而安装前是不行的。

    IntelliJ IDEA lombok插件的安装和使用
    IntelliJ IDEA lombok插件的安装和使用
    IntelliJ IDEA lombok插件的安装和使用
    IntelliJ IDEA lombok插件的安装和使用
    END

配置注解处理器

 
  1. 同样我们在Settings设置页面,我们点击Build,Execution,Deployment-->选择Compiler-->选中Annotation Processors,然后在右侧勾选Enable annotation processing即可。

    END

lombok插件的使用

 
  1. 使用前我们需要说明的是安装的插件只是一个调用,就像我们使用maven插件一样,本机需要安装maven才行。我们在使用lombok前也需要添加lombok的依赖。lombok的版本一直在更新,大家可以在百度搜索框输入lombok maven找到最新的依赖版本。

    <dependency>    <groupId>org.projectlombok</groupId>    <artifactId>lombok</artifactId>    <version>1.16.10</version></dependency>

  2.  

     

     

     

     

     

    2

    接下来我们编辑一个实体类Student,添加三个属性,最后在类上添加@Data属性,这个注解可以帮我们在.class文件中生成类中所有属性的get/set方法、equals、canEqual、hashCode、toString方法等。

  3. 第二

apm-agent-core是skywalking的核心类,打包会生产skywalking-agent.jar这个包

微内核架构
SkyWalking Agent 采用了微内核架构(Microkernel Architecture),那什么是微内核架构呢?微内核架构也被称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构。在基于产品的应用中通常会使用微内核架构,例如,IDEA、Eclipse 这类 IDE 开发工具,内核都是非常精简的,对 Maven、Gradle 等新功能的支持都是以插件的形式增加的。

如下图所示,微内核架构分为核心系统和插件模块两大部分。

 

 

 

 

idea在编译skywalking的时候,发现项目的java源码没有编译的情况

第一要保证idea的jdk设置正确

 

 

项目无法编译运行的问题要设置上图保证maven正确要编译项目

 

 

 

 

 

 

 

在上图展示的微内核架构中,内核功能是比较稳定的,只负责管理插件的生命周期,不会因为系统功能的扩展而不断进行修改。功能上的扩展全部封装到插件之中,插件模块是独立存在的模块,包含特定的功能,能拓展核心系统的功能。通常,不同的插件模块互相之间独立,当然,你可以设计成一个插件依赖于另外一个插件,但应尽量让插件之间的相互依赖关系降低到最小,避免繁杂的依赖带来扩展性问题。

最终所有插件会由内核系统统一接入和管理:

首先,内核系统必须知道要加载哪些插件,一般会通过配置文件或是扫描 ClassPath 的方式(例如前文介绍的 SPI 技术)确定待加载的插件;
之后,内核系统还需要了解如何使用这些插件,微内核架构中需要定义一套插件的规范,内核系统会按照统一的方式初始化、启动这些插件;
最后,虽然插件之间完全解耦,但实际开发中总会有一些意想不到的需求会导致插件之间产生依赖或是某些底层插件被复用,此时内核需要提供一套规则,识别插件消息并能正确的在插件之间转发消息,成为插件消息的中转站。
由此可见微内核架构的好处:

测试成本下降。从软件工程的角度看,微内核架构将变化的部分和不变的部分拆分,降低了测试的成本,符合设计模式中的开放封闭原则。
稳定性。由于每个插件模块相对独立,即使其中一个插件有问题,也可以保证内核系统以及其他插件的稳定性。
可扩展性。在增加新功能或接入新业务的时候,只需要新增相应插件模块即可;在进行历史功能下线时,也只需删除相应插件模块即可。
SkyWalking Agent 就是微内核架构的一种落地方式。在前面的课时中我已经介绍了 SkyWalking 中各个模块的功能,其中 apm-agent-core 模块对应微内核架构中的内核系统,apm-sdk-plugin 模块中的各个子模块都是微内核架构中的插件模块。

SkyWalking Agent 启动流程概述
此前,在搭建 SkyWalking 源码环境的最后,我们尝试 Debug 了一下 SkyWalking Agent 的源码,其入口是 apm-agent 模块中 SkyWalkingAgent 类的 premain() 方法,其中完成了 Agent 启动的流程:

初始化配置信息。该步骤中会加载 agent.config 配置文件,其中会检测 Java Agent 参数以及环境变量是否覆盖了相应配置项。
查找并解析 skywalking-plugin.def 插件文件。
AgentClassLoader 加载插件。
PluginFinder 对插件进行分类管理。
使用 Byte Buddy 库创建 AgentBuilder。这里会根据已加载的插件动态增强目标类,插入埋点逻辑。
使用 JDK SPI 加载并启动 BootService 服务。BootService 接口的实现会在后面的课时中展开详细介绍。
添加一个 JVM 钩子,在 JVM 退出时关闭所有 BootService 服务。
SkywalkingAgent.premain() 方法的具体实现如下,其中省略了 try/catch 代码块以及异常处理逻辑:

复制代码
public static void premain(String agentArgs,
      Instrumentation instrumentation) throws PluginException {
   // 步骤1、初始化配置信息
   SnifferConfigInitializer.initialize(agentArgs);
   // 步骤2~4、查找并解析skywalking-plugin.def插件文件;
   // AgentClassLoader加载插件类并进行实例化;PluginFinder提供插件匹配的功能
   final PluginFinder pluginFinder = new PluginFinder(
      new PluginBootstrap().loadPlugins());
   // 步骤5、使用 Byte Buddy 库创建 AgentBuilder
   final ByteBuddy byteBuddy = new ByteBuddy()
      .with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
   new AgentBuilder.Default(byteBuddy)...installOn(instrumentation);
   // 这里省略创建 AgentBuilder的具体代码,后面展开详细说
   // 步骤6、使用 JDK SPI加载的方式并启动 BootService 服务。
   ServiceManager.INSTANCE.boot();
   // 步骤7、添加一个JVM钩子
   Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
     public void run() { ServiceManager.INSTANCE.shutdown(); }
   }, "skywalking service shutdown thread"));
}
了解了 SkyWalking Agent 启动的核心步骤之后,本课时剩余部分将对每个步骤进行深入分析。

初始化配置
在启动 demo-webapp 和 demo-provider 两个 demo 应用的时候,需要在 VM options 中指定 agent.confg 配置文件(skywalking_config 参数),agent.config 配置文件中的配置项如下:

复制代码
# 当前应用的服务名称,通过Skywalking Agent上报的Metrics、Trace数据都会
# 携带该信息进行标识
agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
在  SnifferConfigInitializer.initialize() 方法中会将最终的配置信息填充到 Config 的静态字段中,填充过程如下:

将 agent.config 文件中全部配置信息填充到 Config 中相应的静态字段中。
解析系统环境变量值,覆盖 Config 中相应的静态字段。
解析 Java Agent 的参数,覆盖 Config 中相应的静态字段。
SnifferConfigInitializer.initialize() 方法的具体实现如下:

复制代码
public static void initialize(String agentOptions) {
   // 步骤1、加载 agent.config配置文件
   InputStreamReader configFileStream = loadConfig();
   Properties properties = new Properties();
   properties.load(configFileStream);
   for (String key : properties.stringPropertyNames()) {
       String value = (String)properties.get(key);
       // 按照${配置项名称:默认值}的格式解析各个配置项
       properties.put(key, PropertyPlaceholderHelper.INSTANCE
           .replacePlaceholders(value, properties));
   }
   // 填充 Config中的静态字段
   ConfigInitializer.initialize(properties, Config.class);
   // 步骤2、解析环境变量,并覆盖 Config中相应的静态字段
   overrideConfigBySystemProp();
   // 步骤3、解析 Java Agent参数,并覆盖 Config中相应的静态字段
   overrideConfigByAgentOptions(agentOptions);
   // 检测SERVICE_NAME和BACKEND_SERVICE两个配置项,若为空则抛异常(略)
   IS_INIT_COMPLETED = true; // 更新初始化标记
}
步骤 1 中的 loadConfig() 方法会优先根据环境变量(skywalking_config)指定的 agent.config 文件路径加载。若环境变量未指定 skywalking_ config 配置,则到 skywalking-agent.jar 同级的 config 目录下查找 agent.confg 配置文件。

将 agent.config 文件中的配置信息加载到 Properties 对象之后,将使用 PropertyPlaceholderHelper 对配置信息进行解析,将当前的“${配置项名称:默认值}”格式的配置值,替换成其中的默认值,demo-provider 解析结果如下图所示:

 

完成解析之后,会通过 ConfigInitializer 工具类,将配置信息填充到 Config 中的静态字段中,具体填充规则如下:

 

在接下来的 overrideConfigBySystemProp() 方法中会遍历环境变量(即 System.getProperties() 集合),如果环境变 是以 "skywalking." 开头的,则认为是 SkyWalking 的配置,同样会填充到 Config 类中,以覆盖 agent.config 中的默认值。

最后的 overrideConfigByAgentOptions() 方法解析的是 Java Agent 的参数,填充 Config 类的规则与前面两步相同,不再重复。

到此为止,SkyWalking Agent 启动所需的全部配置都已经填充到 Config 中,后续使用配置信息时直接访问 Config 中的相应静态字段即可。

 

dubbo 插件

 

 

 

 

 

 这里要注意这里拦截点是连接类的实例方法所以继承的类是ClassInstanceMethodEnhancePluginDefine

如果是拦截类的静态方法,就要集成另外的类,如下

 

 下面的具体的拦截的实现

 

 

 

 结论

 

 

 

 

skywalking中witnessClass机制

 

 

witness机制的原理,列如dubbo有1.0  2.0两个版本

1.0中原理的拦截的MonitorFilter类对应的方法是invoke方法

2.0中原理的拦截的MonitorFilter类对应的方法变成了name方法

现在我们为dubbo1.0 编写了skywalking一个插件dubbo1.0 -jar

我们为dubbo2.0 编写了skywalking一个插件dubbo2.0 -jar

 我们如何实现dubbo1.0 -jar只拦截invoke方法

dubbo2.0 -jar只拦截name方法

这就通过witnessClass机制来实现,dubbo1.0 -jar的插件中项目组需要实现witnessClass方法,在改方法中返回一直方法,改方法只有dubbo1.0这个版本有,列如方法为aa

项目启动的时候,classload会对dububbo 1.0这个项目组运行的版本进行字节码解析看是否存在aa方法,如果aa方法与skywalking agent中witnessClass方法中定义的aa方法一致,就使用dubbo1.0 -jar只拦截invoke方法

列如sping存在多个版本

 

 agent后后端通信的设置

一种是是明文通信

一种是ssl通信

 

 

 

 

 

 agent后后端的token认证

 

 grpc和后端的oap重连的时候是随机选择一个后端的oap server 实例来进行连接

 

 

 应用通过grpc向opa注册注册的时候会携带当前的应用名称,注册成功之后会将当前应用的ID返回给agent,agent会存在在缓存中

 

 当前服务实例也需要进行注册,注册成功之后,oap会给当前的实例返回一个id,然后将这个ID缓存起来

 注册成功之后,agengt需要通过定时任务不断向后端发送心跳包,说明当前的实例是存在的

 

 

 

 

 Tracesegement的核心概念

 

 skywalking将一个进程中的所有span封装到一个Tracesegement中

 

 

 

 

 

 

 

 

 

 一个Tracesement中包含多个span

span的定义如下

 

 span中提供了下面的方法

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 概念3:TraceSegment中存在一个字段TraceSegmentref字段,记录了TraceSegment与TraceSegment的相互关联关系

 

 

 

 概念4:上面的概念介绍完成之后接下来介绍context对象来实现对TraceSegment的管理和实现对span的管理

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 在上面创建entryspan之后,会将当前的TraceSegment上传到oap中,entryspan一般是分为两种,一个中进程传播,一种是线程传播,接下来讲讲跨进程传递,需要将Trace的信息上下文进行跨进程传播

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 跨线程传播如下

 11、接下来在发送TracesSegment上报给oap的时候用到了DateCarrier组件,DateCarrier组件中会存储要发送的TracesSegment

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

接下来我们重点讲解下几个重要的概念

 

 

 

 

 

 我们来查看下一个案例contextManger如何进行管理的

 

 

 

讲讲TraceSegment中的采样,看是否需要将当前的审判添加到TraceSegment中

 

 

 

 

 

 

 当TraceContext调用stopSpan关闭最后一个span的时候,会调用finish方法关闭改span所在的TraceSegment,于此同时会调用TracContext的listen通过改segment被关闭了的通知,这个时候就会将TraceSegment发送给oap的后端

 

 

 

 

 

 TraceSegmentServiceClient主要负责将TraceSegment通过DataCarrier将TraceSegment序列化发送给后端的oap

整个序列化profuffer协议格式如下所示

 

 

 

 

 

 

 

 

 

 

 

 

 

 这里序列化之后将segement发送给后端oap,发送的过程是阻塞性的

 

lombok

posted on 2020-11-24 19:01  luzhouxiaoshuai  阅读(2454)  评论(0编辑  收藏  举报

导航