1. Skyalking介绍
1.1 Skywalking概述
SkyWalking 是一个开源可观测性平台,用于收集、分析、聚合和可视化来自服务和云原生的数据 基础 设施。SkyWalking 提供了一种简单的方法来保持分布式系统的清晰视图,甚至可以跨云。 它是一个现代 APM,专为云原生、基于容器的分布式系统而设计。
1.2 Skywalking整体架构
SkyWalking在逻辑上分为四个部分:探针,平台后端,存储和UI。
- 探测器收集遥测数据,包括各种格式(SkyWalking,Zipkin,OpenTelemetry,Prometheus,Zabbix等)的指标,跟踪,日志和事件。 2. 平台后端支持数据聚合、分析和流流程,涵盖跟踪、指标、日志和事件。充当聚合器角色和/或接收者角色。 3. 存储通过开放/可插拔接口存储SkyWalking数据。您可以选择现有实现,例如 ElasticSearch,H2,MySQL,TiDB,BanyanDB,或者实现你自己的。 4. UI是一个高度可定制的基于Web的界面,允许SkyWalking最终用户可视化和管理SkyWalking数据。
2.Skyalking部署
安装包下载
https://skywalking.apache.org/downloads/
注意:目前最高版本为OAP 9.4.0版本【注意9.4.0 未测试jdk1.8 可能无法使用,因此建议使用的9.3.0版本 】,Agent版本为8.14
2.1 服务端OAP部署
2.1.1修改配置文件
进去config目录,找到oap配置文件 application.yml
里面有很多模块配置项,具体配置可以看官方文档,此处我们只修改oap数据源,其余保持默认,找到配置文件中的storage 模块,可以看到里面很多数据源配置,elasticserch,h2,mysql等,此处我们选择elasticsearch作为数据存储,修改selector 值即可。
2.1.2 启动OAP组件
进入bin目录
可以看到bin目录下有bat脚本和sh脚本,其中oapService是oap的启动脚本,其中分为两种类型,init和非init,如果用init的脚本启动,则会初始化oap的数据库相关表或者es 索引,非init则直接启动oap服务。所以第一次启动我们选择oapServiceInit.bat 启动,初始化es 相关索引,下次启动用oapService.bat启动即可。
2.2 服务端UI部署
2.2.1 修改配置文件
进入webapp目录,webapp.yml为ui服务的配置文件,里面有端口号以及spring cloud gateway的相关配置
2.2.2 启动ui服务
进入bin目录,使用webappService.sh启动即可
2.3 客户端Agent部署
2.3.1 javaagent 目录介绍
Agent目录结构如下:
2.3.2 集成javaAgent插件
1、配置文件
Javaagent配置文件在 agent config目录下,具体参数请参考官网介绍,其中
agent.service_name 为服务名称,其他默认,我们修改此项即可。
部分配置说明:
- agent.namespace:命名空间,可通过此参数实现隔离 agent.service_name:在链路追踪 UI 中展示的应用名,如果是微服务架构,可以和注册中心中的应用名一致 agent.is_cache_enhanced_class:如果为true,则SkyWalking代理会将所有检测到的类文件缓存到内存或磁盘文件中(由类缓存模式决定) collector.backend_service:后端Collector收集器的地址 logging.file_name:日志文件名 logging.level:日志等级 logging.max_file_size:日志文件大小控制 logging.max_history_files:历史日志文件个数控制 plugin.mount:挂载插件的文件夹名 plugin.mysql.trace_sql_parameters:收集sql的参数 agent.ignore_suffix,表示对请求追踪进行忽略,多个路径用逗号分隔,在实际的生产环境中某些请求是不需要被追踪的,例如心跳检查/health,监控指标/metrics等等,我们需要将对应的插件jar包apm-trace-ignore-plugin-8.4.0.jar拷贝到plugins目录下并进行对应的配置,配置文件内容部分如下
# If the operation name of the first span is included in this set, this segment should be ignored. Multiple values should be separated by `,`.
#agent.ignore_suffix=${SW_AGENT_IGNORE_SUFFIX:.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg}
2.3.3 javaagent集成
常用配置:
-javaagent:/opt/skwalking/skywalking-agent/skywalking-agent.jar
-Dskywalking.agent.service_name=服务名
-Dskywalking.collector.backed_service=127.0.0.1:11800 --OAP通讯地址
3.Skywalking自定义链路追踪
3.1 概念
Span
- EntrySpan代表服务提供商。它也是服务器端的端点。作为一个APM系统,我们的目标是 应用程序服务器。因此,几乎所有的服务和MQ消费者都是EntrySpan。
- LocalSpan 表示一种不涉及远程服务的普通 Java 方法。它既不是 MQ 生产者/消费者 也不是服务(例如 HTTP 服务)提供者/消费者。
- ExitSpan 表示服务的客户端或 MQ 生产者。它在SkyWalking的早期版本中被命名。 例如,通过JDBC访问数据库和读取Redis/Memcached被归类为ExitSpan。LeafSpan
ContextCarrier
为了实现分布式跟踪,必须绑定跨进程跟踪,并且上下文必须传播 在整个过程中。这就是ContextCarrier的用武之地。
以下是有关如何在分布式呼叫中使用上下文载体的步骤。A->B
1.在客户端创建一个新的空。ContextCarrier
2.创建退出跨度 或 用于启动 .ContextManager#createExitSpanContextManager#injectContextCarrier
3.将所有项目放入heads(例如HTTP HEAD),附件(例如Dubbo RPC框架)或消息(例如Kafka)中。ContextCarrier
4.通过服务调用传播到服务器端。ContextCarrier
5.在服务器端,从标题、附件或消息中获取所有项目。
6.创建 EntrySpan 或用于绑定客户端和服务器端。ContextManager#createEntrySpanContextManager#extract
服务器端使用 Tomcat 7 服务器插件
ContextCarrier contextCarrier = new ContextCarrier();
CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
next.setHeadValue(request.getHeader(next.getHeadKey()));
}
ContextSnapshot
除了跨进程跟踪之外,还必须支持跨线程跟踪。例如,两个异步进程(内存中 MQ) 和批处理在 Java 中很常见。跨进程和跨线程跟踪非常相似,因为它们都需要传播 上下文,但跨线程跟踪不需要序列化。
以下是跨线程传播的三个步骤:
1.用于获取 ContextSnapshot 对象。ContextManager#capture
2.让子线程通过方法参数或由现有参数携带访问 ContextSnapshot
3.在子线程中使用。ContextManager#continued
3.2 Tracing API 接口跟踪
依赖工具包
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>${skywalking.version}</version>
</dependency>
使用 API 获取traceID。TraceContext.traceId()
使用 API 获取segmentID。TraceContext.segmentId()
使用 API 获取 spanId。TraceContext.spanId()
@Trace注释的方法将尝试使用给定的键 () 和 () 标记当前活动范围 如果根本没有活动跨度,则此注释无效。 可以重复使用,并且可以与 一起使用,请参阅下面的示例。 的与自定义增强跟踪中支持的内容相同。@TagTag#key()Tag#value()@Tag@TracevalueTag
在跟踪方法的上下文中添加自定义标记。ActiveSpan.tag(“key”, “val”)
ActiveSpan.error()将当前范围标记为错误状态。
ActiveSpan.error(String errorMsg)使用消息将当前范围标记为错误状态。
ActiveSpan.error(Throwable throwable)使用可抛出对象将当前范围标记为错误状态。
ActiveSpan.debug(String debugMsg)在当前范围中添加调试级别日志消息。
ActiveSpan.info(String infoMsg)在当前范围中添加信息级别日志消息。
ActiveSpan.setOperationName(String operationName)自定义操作名称。
3.2.1 侵入式追踪
如果要用@tag或@tags注解,前提是必须要使用@Trace注解,不然仅仅给业务方法加@Tag注解的话,SkyWalking也不会显示
@Tag注解中key我们可以自定义,而value的写法就固定了,如果要查看返回值就只能写returnedObj,如果要查看请求参数就只能用arg,下标代表第几个请求参数
返回值的对象,注意要重写toString()方法,不然在SkyWalking的界面中显示的只是一个对象的内存地址
3.2.2 非侵入式追踪
- 激活插件,将插件从
optional-plugins/apm-customize-enhance-plugin-x.x.x.jar
移动到plugin/apm-customize-enhance-plugin-x.x.x.jar
。 - 在agent.config中配置
plugin.customize.enhance_file
,指明增强规则文件,比如/absolute/path/to/customize_enhance.xml
。 - 在
customize_enhance.xml
中配置增强规则
<?xml version="1.0" encoding="UTF-8"?>
<enhanced>
<class class_name="test.apache.skywalking.testcase.customize.service.TestService1">
<method method="staticMethod()" operation_name="/is_static_method" static="true"/>
<method method="staticMethod(java.lang.String,int.class,java.util.Map,java.util.List,[Ljava.lang.Object;)" operation_name="/is_static_method_args" static="true">
<operation_name_suffix>arg[0]</operation_name_suffix>
<tag key="tag_1">arg[2].['k1']</tag>
<tag key="tag_2">arg[4].[1]</tag>
</method>
<method method="method()" static="false"/>
</class>
</enhanced>
文件中的配置说明
配置说明class_name要被增强的类method类的拦截器方法operation_name如果进行了配置,将用它替代默认的operation_nameoperation_name_suffix表示在operation_name后添加动态数据static方法是否为静态方法tag将在local span中添加一个tag。key的值需要在XML节点上表示。log将在local span中添加一个log。key的值需要在XML节点上表示。arg[x]表示输入的参数值。比如args[0]表示第一个参数。.[x]当正在被解析的对象是Array或List,你可以用这个表达式得到对应index上的对象。.[‘key’]当正在被解析的对象是Map, 你可以用这个表达式得到map的key。
3.3 openTracing API 接口跟踪
依赖工具包,例如使用 maven 或 gradle
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-opentracing</artifactId>
<version>{project.release.version}</version>
</dependency>
代码示例:
Tracer tracer = new SkywalkingTracer();
Tracer.SpanBuilder spanBuilder = tracer.buildSpan("/yourApplication/yourService");
3.4 跨线程追踪
SkyWalking提供了跨线程构建Trace的能力,通过对 Callable、Runnable、Supplier 这3种接口的实现者进行增强拦截,将 Trace 的上下文信息传递到子线程中,实现了异步链路追踪。有非常多的方式来实现Callable,Runnable,Supplier 这3种接口。
SkyWalking提供了一种既通用又快捷的方式来规范这一现象:
1.只拦截增强带有@TraceCrossThread 注解的类;
2.通过装饰的方式包装任务,避免大刀阔斧的修改
原始类提供的包装类拦截方法使用技巧CallableCallableWrappercallCallableWrapper.of(xxxCallable)RunnableRunnableWrapperrunRunnableWrapper.of(xxxRunable)SupplierSupplierWrappergetSupplierWrapper.of(xxxSupplier)
包装类 都有注解 @TraceCrossThread ,skywalking内部的拦截匹配逻辑是,标注了@TraceCrossThread的类,拦截 其名称为call 或run或 get ,且没有入参的方法;对使用者来说大致分为2种方式:
1.自定义类,实现接口 Callable、Runnable、Supplier,加@TraceCrossThread注解。当需要有更多的自定义属性时,考虑这种方式;参考 CallableWrapper、RunnableWrapper 、SupplierWrapper 的实现方式。
通过xxxWrapper.of 装饰的方式,即CallableWrapper.of(xxxCallable)、RunnableWrapper.of(xxxRunable)、SupplierWrapper.of(xxxSupplier)。大多情况下,通过这种包装模式即可。
依赖工具包,例如使用 maven 或 gradle
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>${skywalking.version}</version>
</dependency>
用法 1.
@TraceCrossThread
public static class MyCallable<String> implements Callable<String> {
@Override
public String call() throws Exception {
return null;
}
}...
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(new MyCallable());
用法 2.
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(CallableWrapper.of(new Callable<String>() {
@Override public String call() throws Exception {
return null;
}
}));
或
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.execute(RunnableWrapper.of(new Runnable() {
@Override public void run() {
//your code }
}));
用法 3.
@TraceCrossThread
public class MySupplier<String> implements Supplier<String> {
@Override
public String get() {
return null;
}
}...
CompletableFuture.supplyAsync(new MySupplier<String>());
或
CompletableFuture.supplyAsync(SupplierWrapper.of(()->{
return "SupplierWrapper";
})).thenAccept(System.out::println);
用法 4.
CompletableFuture.supplyAsync(SupplierWrapper.of(() -> {
return "SupplierWrapper";
})).thenAcceptAsync(ConsumerWrapper.of(c -> {
// your code visit(url) System.out.println("ConsumerWrapper");
}));
或
CompletableFuture.supplyAsync(SupplierWrapper.of(() -> {
return "SupplierWrapper";
})).thenApplyAsync(FunctionWrapper.of(f -> {
// your code visit(url) return "FunctionWrapper";
}));
3.5自定义插件
3.5.1 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itmuch.skywalking</groupId>
<artifactId>apm-string-plugin</artifactId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<properties>
<shade.package>org.apache.skywalking.apm.dependencies</shade.package>
<shade.net.bytebuddy.source>net.bytebuddy</shade.net.bytebuddy.source>
<shade.net.bytebuddy.target>${shade.package}.${shade.net.bytebuddy.source}</shade.net.bytebuddy.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-agent-core</artifactId>
<version>8.12.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-util</artifactId>
<version>8.8.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>false</shadedArtifactAttached>
<createDependencyReducedPom>true</createDependencyReducedPom>
<createSourcesJar>true</createSourcesJar>
<shadeSourcesContent>true</shadeSourcesContent>
<relocations>
<relocation>
<pattern>${shade.net.bytebuddy.source}</pattern>
<shadedPattern>${shade.net.bytebuddy.target}</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.5.2 继承ClassInstanceMethodsEnhancePluginDefine
非静态
package com.itmuch.skywalking.plugin.define;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.DeclaredInstanceMethodsInterceptPoint;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
import org.apache.skywalking.apm.agent.core.plugin.match.NameMatch;
public class DemoMethodInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
public static final String INTERCEPT_CLASS="com.springboot.master.service.impl.AsyncServiceImpl";
@Override
protected ClassMatch enhanceClass() {
return NameMatch.byName(INTERCEPT_CLASS);
}
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return new ConstructorInterceptPoint[0];
}
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[]{
new DeclaredInstanceMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
System.out.println("-----------------------------");
return ElementMatchers.named("getCityName");
}
@Override
public String getMethodsInterceptor() {
return "com.itmuch.skywalking.plugin.DemoMethodInterceptor";
}
@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
}
静态
package com.itmuch.skywalking.plugin.define;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.StaticMethodsInterceptPoint;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
import org.apache.skywalking.apm.agent.core.plugin.match.NameMatch;
public class StringReplaceInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
@Override
protected ClassMatch enhanceClass() {
// 指定想要监控的类
return NameMatch.byName("org.apache.commons.lang3.StringUtils");
}
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return new ConstructorInterceptPoint[0];
}
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
// 指定想要监控的实例方法,每个实例方法对应一个InstanceMethodsInterceptPoint
return new InstanceMethodsInterceptPoint[0];
}
@Override
public StaticMethodsInterceptPoint[] getStaticMethodsInterceptPoints() {
// 指定想要监控的静态方法,每一个方法对应一个StaticMethodsInterceptPoint
return new StaticMethodsInterceptPoint[]{
new StaticMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
// 静态方法名称
return ElementMatchers.named("replace");
}
@Override
public String getMethodsInterceptor() {
// 该静态方法的监控拦截器类名全路径
return "com.itmuch.skywalking.plugin.StringReplaceInterceptor";
}
@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
}
3.5.3 实现MethodsAroundInterceptor
静态方法实现StaticMethodsAroundInterceptor
package com.itmuch.skywalking.plugin;
import org.apache.skywalking.apm.agent.core.context.ContextManager;
import org.apache.skywalking.apm.agent.core.context.tag.StringTag;
import org.apache.skywalking.apm.agent.core.context.tag.Tags;
import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.StaticMethodsAroundInterceptor;
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
import java.lang.reflect.Method;
public class StringReplaceInterceptor implements StaticMethodsAroundInterceptor {
@Override
public void beforeMethod(Class aClass, Method method, Object[] argumentsTypes, Class[] classes, MethodInterceptResult methodInterceptResult) {
// 创建span(监控的开始),本质上是往ThreadLocal对象里面设值
AbstractSpan span = ContextManager.createLocalSpan("replace");
/*
* 可用ComponentsDefine工具类指定Skywalking官方支持的组件
* 也可自己new OfficialComponent或者Component
* 不过在Skywalking的控制台上不会被识别,只会显示N/A
*/
span.setComponent(ComponentsDefine.TOMCAT);
span.tag(new StringTag(1000, "params"), argumentsTypes[0].toString());
// 指定该调用的layer,layer是个枚举
span.setLayer(SpanLayer.CACHE);
}
@Override
public Object afterMethod(Class aClass, Method method, Object[] objects, Class[] classes, Object o) {
String retString = (String) o;
// 激活span,本质上是读取ThreadLocal对象
AbstractSpan span = ContextManager.activeSpan();
// 停止span(监控的结束),本质上是清理ThreadLocal对象
ContextManager.stopSpan();
return retString;
}
@Override
public void handleMethodException(Class aClass, Method method, Object[] objects, Class[] classes, Throwable throwable) {
AbstractSpan activeSpan = ContextManager.activeSpan();
// 记录日志
activeSpan.log(throwable);
activeSpan.errorOccurred();
}
}
非静态方法实现InstanceMethodsAroundInterceptor
package com.itmuch.skywalking.plugin;
import org.apache.skywalking.apm.agent.core.context.ContextManager;
import org.apache.skywalking.apm.agent.core.context.tag.StringTag;
import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
import java.lang.reflect.Method;
public class DemoMethodInterceptor implements InstanceMethodsAroundInterceptor {
@Override
public void beforeMethod(EnhancedInstance enhancedInstance, Method method, Object[] objects, Class<?>[] classes, MethodInterceptResult methodInterceptResult) throws Throwable {
// 创建span(监控的开始),本质上是往ThreadLocal对象里面设值
AbstractSpan span = ContextManager.createLocalSpan("demo");
/*
* 可用ComponentsDefine工具类指定Skywalking官方支持的组件
* 也可自己new OfficialComponent或者Component
* 不过在Skywalking的控制台上不会被识别,只会显示N/A
*/
span.setComponent(ComponentsDefine.TOMCAT);
span.tag(new StringTag(1000, "params"), "demo");
// 指定该调用的layer,layer是个枚举
span.setLayer(SpanLayer.CACHE); }
@Override
public Object afterMethod(EnhancedInstance enhancedInstance, Method method, Object[] objects, Class<?>[] classes, Object o) throws Throwable {
String retString = (String) o;
// 激活span,本质上是读取ThreadLocal对象
AbstractSpan span = ContextManager.activeSpan();
// 停止span(监控的结束),本质上是清理ThreadLocal对象
ContextManager.stopSpan();
return retString;
}
@Override
public void handleMethodException(EnhancedInstance enhancedInstance, Method method, Object[] objects, Class<?>[] classes, Throwable throwable) {
}
}
3.5.3 在resourse目录下编写skywalking-plugin.def文件
Plugin=com.itmuch.skywalking.plugin.define.StringReplaceInstrumentation
Plugin=com.itmuch.skywalking.plugin.define.DemoMethodInstrumentation
Key随意写 ,value为继承ClassInstanceMethodsEnhancePluginDefine类的全路径,注意多个key最好保持一致
文件名固定为skywalking-plugin.def
版权归原作者 水落大海 所有, 如有侵权,请联系我们删除。