SpringCloud微服务如何实现链路追踪?

声明:内容来自AI,未经验证,仅供参考!

1、链路追踪与日志的可追踪性概念

链路追踪:

        是在遵守openTraceing的情况下,把一次接口调用的各个逻辑分支以trace和span的形式记录下来,并在UI里展示出来,以供网络故障排查、性能监控、安全分析等。

日志的可追踪性:

        日志的可追踪性(Log Traceability)指的是通过日志记录来跟踪和监控软件系统或应用程序在运行时的行为和状态,以及用户的活动,从而使得这些信息能够被回溯和分析。可追踪性强的日志系统可以帮助开发人员和运维工程师快速定位问题源头,了解系统的运行历史,以及在出现问题时进行效率高的故障排除。一般的做法,就是把日志用traceId串起来。

为了实现日志的可追踪性,日志应当包含以下几个关键点:

  1. 准确性:日志记录需要准确无误地反映出系统的真实行为和状态。

  2. 详细性:日志应该提供足够的细节,以便于理解事件的上下文。这包括时间戳、事件类型、涉及的数据和资源、用户操作、错误消息等。

  3. 一致性:日志格式应当保持一致,以便于分析和自动处理。这可能包括使用标准化的日志格式、一致的时间格式和时区、一致的关键字和术语等。

  4. 关联性:在复杂的分布式系统中,一个业务流程可能会跨越多个服务和组件。因此,日志应该包含能够将相互独立的日志记录关联起来的信息,如唯一的请求ID或某种形式的关联ID。

  5. 安全性:日志中可能包含敏感信息,因此需要确保日志数据的安全,避免泄露敏感信息,并确保日志的完整性和不可篡改性。

  6. 易用性:日志应当易于读取和理解,即使对于没有深入了解系统内部原理的人员也是如此。这通常意味着日志中应当有清晰的描述和足够的背景信息。

        使日志具有良好的可追踪性,往往需要在系统设计和开发阶段就考虑到日志策略,而不是事后附加。此外,随着微服务和云原生应用的兴起,日志的可追踪性往往与分布式追踪(Distributed Tracing)等技术结合使用,以实现更为精细和全面的系统监控和故障排查。

2、实现日志的可追踪性

        实现链路追踪可以顺便实现日志的可追踪性,当然也可以只实现日志的可追踪性不要链路追踪,这是两个不同的概念。当然我也见过别人实现了一个融合日志和链路追踪的系统,不知道怎么实现的,也不知道性能如何。

2.1 Jaeger实现日志可追踪性

        目的是在日志中记录 Jaeger 的 traceId,但是没有完整的 OpenTracing 实现,并且不需要发送 trace 数据到任何后端(例如 Jaeger agent 或 collector),可以使用一个较为简单的方法来传递 traceId。

首先,确保应用程序依赖于 Jaeger 客户端库,即使不将 trace 数据发送到任何地方。Jaeger 客户端库会生成和管理 traceId。然后,你可以创建一个 Span,并将 traceId 放入 Mapped Diagnostic Context (MDC) 中,用于日志记录。以下是一个基本的示例:

  1. 引入 Jaeger 客户端库依赖:
    <!-- Maven 依赖示例 -->
    <dependency>
        <groupId>io.jaegertracing</groupId>
        <artifactId>jaeger-client</artifactId>
        <version>你的Jaeger客户端版本</version>
    </dependency>
    
  2. 配置 Jaeger Tracer,你可以采用 NoopTracerFactory 创建一个不发送数据的 Tracer,因为你不需要将 trace 数据发送到 Jaeger:
    import io.jaegertracing.Configuration;
    import io.opentracing.Tracer;
    import io.opentracing.util.GlobalTracer;
    import io.opentracing.noop.NoopTracerFactory;
    
    // ...
    
    public void configureTracer() {
        Tracer tracer = NoopTracerFactory.create();
        GlobalTracer.registerIfAbsent(tracer);
    }
    
  3. 在你的代码中,使用 Tracer 来创建一个新的 Span,并将 traceId 放入 MDC 中:
    import io.opentracing.Span;
    import org.slf4j.MDC;
    import io.opentracing.util.GlobalTracer;
    
    // ...
    
    public void doSomething() {
        Tracer tracer = GlobalTracer.get();
    
        // 创建或获取 Span 上下文
        Span span = tracer.buildSpan("operationName").start();
        
        // 将 traceId 放入 MDC
        String traceId = span.context().toTraceId();
        MDC.put("traceId", traceId);
    
        try {
            // ... 你的业务逻辑 ...
    
        } finally {
            // 清理 MDC
            MDC.remove("traceId");
            span.finish(); // 完成 Span,因为我们使用 NoopTracer,这个 Span 并不会被发送到任何地方
        }
    }
    

    在上面的代码中,我们首先获取一个 GlobalTracer 的实例,并通过它来创建一个新的 Span。随后,我们从 Span 的上下文(span.context())中获取 traceId 并将其放入 MDC。这样,traceId 就可以在你的日志中被记录下来。

    记得在你的日志配置中,将 traceId 添加到日志格式中。例如,在 logback 中,你可以这样配置:

    <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n</pattern>
    
    在上面的 <pattern> 配置中,%X{traceId} 会被替换为 MDC 中的 traceId。
    

2.2 Sleuth实现日志可追踪性

        如果你只想在日志中记录 traceId 并且不需要将 trace 数据发送到后端(如 Zipkin),你可以使用 Spring Cloud Sleuth 来自动为你的日志添加 traceId 和 spanId。这个过程不需要与 Zipkin 或其他追踪系统集成。Spring Cloud Sleuth 本身就可以在本地为你生成 trace 和 span 信息,并且这些信息可以集成到日志中。

下面是在不集成任何追踪后端系统的情况下使用 Spring Cloud Sleuth 的基本步骤:

1、引入 Sleuth 依赖

在你的 pom.xml 中添加 Spring Cloud Sleuth 的依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

2、配置 Sleuth

默认情况下,Sleuth 会以一定的采样率生成 trace 和 span 信息。如果你想要记录所有请求的 trace,可以在你的 application.propertiesapplication.yml 中进行如下配置:

# application.yml
spring:
  sleuth:
    sampler:
      probability: 1.0  # 记录所有请求

3、配置日志

确保你的日志配置包含了 Sleuth 提供的 traceId 和 spanId。例如,在 logback 中的配置如下:

<encoder>
    <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} [%X{traceId},%X{spanId}] - %msg%n</pattern>
</encoder>

4、写业务代码

编写你的业务代码,Sleuth 会自动在每个请求中创建 trace 和 span,并且 traceId 和 spanId 会自动记录在日志中。(无需手动往mdc里放值)

3、实现链路追踪

3.1 Jaeger实现链路追踪

1、引入依赖

引入 opentracing-spring-cloud-starter 依赖到你的 Spring Boot 应用程序中。还需要引入 Jaeger 的依赖作为 OpenTracing 的实现。

<!-- Maven 依赖示例 -->
<dependency>
    <groupId>io.opentracing.contrib</groupId>
    <artifactId>opentracing-spring-cloud-starter</artifactId>
    <version>你的版本号</version>
</dependency>
<dependency>
    <groupId>io.jaegertracing</groupId>
    <artifactId>jaeger-client</artifactId>
    <version>你的Jaeger客户端版本</version>
</dependency>

2、配置 Jaeger Tracer

配置 Jaeger Tracer,可以通过环境变量、系统属性或编程方式进行。

import io.jaegertracing.Configuration;
import io.opentracing.Tracer;

// ...

@Bean
public Tracer jaegerTracer() {
    return new Configuration("your-service-name")
            .withSampler(new Configuration.SamplerConfiguration()
                    .withType("const")
                    .withParam(1))
            .withReporter(new Configuration.ReporterConfiguration()
                    .withLogSpans(true))
            .getTracer();
}

Jaeger 客户端默认会尝试将数据发送到本地的 Jaeger Agent,一般在 localhost:6831。这就是为什么在简单的配置示例中没有显式设置 endpoint 的原因。

如果你需要配置特定的 endpoint,比如说直接向 Jaeger Collector 发送追踪数据,你可以通过以下方式配置 reporter:

@Bean
public Tracer jaegerTracer() {
    return new Configuration("your-service-name")
            .withSampler(new Configuration.SamplerConfiguration()
                    .withType("const")
                    .withParam(1))
            .withReporter(new Configuration.ReporterConfiguration()
                    .withLogSpans(true)
                    .withSender(new Configuration.SenderConfiguration()
                            .withEndpoint("http://jaeger-collector:14268/api/traces"))) // 设置 Jaeger Collector 的 endpoint
            .getTracer();
}

3、编写你的服务代码

使用 @Autowired 注入 Tracer 对象,并在需要创建新追踪或子追踪的时候使用它。

import io.opentracing.Tracer;

// ...

@Autowired
private Tracer tracer;

public void yourServiceMethod() {
    Span span = tracer.buildSpan("yourOperationName").start();

    try {
        // 业务逻辑
    } finally {
        span.finish();
    }
}

4、Feign 客户端的链路追踪:(调用的例子)

使用 Feign 客户端调用其他服务时,opentracing-spring-cloud-starter 库会自动将追踪信息注入到请求头中,因此不需要进行额外的配置。

@FeignClient(name = "other-service")
public interface OtherServiceClient {
    @RequestMapping(method = RequestMethod.GET, value = "/endpoint")
    String getSomething();
}

5、日志集成

确保在你的日志配置中添加了 traceId 和 spanId,以便追踪信息能够显示在日志中。比如我们可以在过滤器里往MDC里放traceId。

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId},%X{spanId}] - %msg%n</pattern>

6、启动和测试你的应用程序

 启动你的应用程序,在两个服务之间进行一些调用,并检查链路追踪系统是否捕获到了追踪信息。

总的来说,opentracing-spring-cloud-starter 库是一个好选择,因为它为 Spring Cloud 应用程序提供了自动和无痛的 OpenTracing 集成。它简化了追踪信息的传播,特别是在多个服务之间进行远程调用时。记得检查你使用的库版本是否与你的 Spring Boot 和 Spring Cloud 版本兼容。 

7、补充一个复杂的span嵌套的例子

在 OpenTracing 模型中,创建多个 span 来表示不同的操作,以及在 span 内部创建子 span 来表示嵌套操作是相对简单的。这些 span 和子 span 将构成一个追踪树,反映了事务的整个执行路径。

下面是一个例子,我们假设一个 HTTP 请求到达被调用方的服务,这个服务将执行几个步骤,并且对每个步骤都创建一个 span,有些步骤还将创建子 span。

import io.opentracing.Span;
import io.opentracing.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
public class YourController {

    @Autowired
    private Tracer tracer;

    @GetMapping("/complex-operation")
    public String handleComplexRequest() {
        // 最外层的 Span 表示整个 HTTP 请求的处理
        Span httpSpan = tracer.buildSpan("http-request").start();
        
        try {
            // 执行第一个操作,并创建对应的 Span
            Span operation1Span = tracer.buildSpan("operation1")
                    .asChildOf(httpSpan) // 设置为 httpSpan 的子 span
                    .start();
            try {
                // 执行操作1的相关业务逻辑...
            } finally {
                operation1Span.finish();
            }

            // 执行第二个操作,这个操作有多个子步骤
            Span operation2Span = tracer.buildSpan("operation2")
                    .asChildOf(httpSpan)
                    .start();
            try {
                // 子步骤 1
                Span subOperation1Span = tracer.buildSpan("subOperation1")
                        .asChildOf(operation2Span)
                        .start();
                try {
                    // 执行子操作1的相关业务逻辑...
                } finally {
                    subOperation1Span.finish();
                }

                // 子步骤 2
                Span subOperation2Span = tracer.buildSpan("subOperation2")
                        .asChildOf(operation2Span)
                        .start();
                try {
                    // 执行子操作2的相关业务逻辑...
                } finally {
                    subOperation2Span.finish();
                }

                // 其他子操作...

            } finally {
                operation2Span.finish();
            }

            // 执行其他操作...
            // ...

            return "Complex operation completed";
        } finally {
            httpSpan.finish();
        }
    }
}

在上面的代码中,我们首先创建了一个表示整个 HTTP 请求的 span (httpSpan)。然后我们为两个主要的操作创建了单独的 span (operation1Spanoperation2Span),这些 span 都是 httpSpan 的子 span。对于第二个操作,我们继续创建了更多的子 span 来代表更细粒度的步骤。

注意,每个 span 的开始 (start()) 和结束 (finish()) 被正确地包含在 try-finally 语句中,以确保 span 即使在抛出异常的情况下也能得到正确的关闭。

使用这种方式,你可以在服务的不同层次和不同的执行路径中创建多个 span 和子 span,并通过这些 span 的父子关系来映射整个请求的处理流程。这样,当你查看 Jaeger UI 中的跟踪时,你将看到一个层次结构的 span 图,它反映了请求在你的服务中的具体执行路径。

3.2 Spring Cloud Sleuth实现链路追踪

1、引入依赖

将 Spring Cloud Sleuth 和 Zipkin 的依赖添加到你的 pom.xml 中:

<!-- Add Spring Cloud Sleuth Starter -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

<!-- Add Zipkin Starter (包含了spring-cloud-sleuth-zipkin) -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

2、配置应用

application.propertiesapplication.yml 中配置 Zipkin 服务器的地址:

# application.yml
spring:
  zipkin:
    base-url: http://localhost:9411/
  sleuth:
    sampler:
      probability: 1.0  # 采样率设置为 1.0 表示记录所有请求

上面的例子中,我们假设 Zipkin 服务器正在本地的 9411 端口运行。如果你的 Zipkin 服务器部署在其他位置,请修改 base-url 为相应的 URL。

3、写业务代码

在 Spring Boot 应用中编写你的控制器和服务,Spring Cloud Sleuth 在背后会自动为你处理 span 的创建和传递:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class YourController {

    @GetMapping("/endpoint")
    public String handleRequest() {
        // Spring Cloud Sleuth 自动创建了一个新的 span
        // 执行你的业务逻辑...
        return "Hello from Zipkin!";
    }
}

在这个例子中,你不需要显式地操作 Span。Spring Cloud Sleuth 将自动为你的每个请求创建一个新的 Span,并将追踪信息传递到 Zipkin。

4、启动和测试

启动你的应用程序并进行一些请求。请求将自动被 Sleuth 追踪,并将追踪数据发送到 Zipkin 服务器。

使用 Sleuth 和 Zipkin 时,你不需要手动处理 Span 的创建和传递,这样使得在 Spring Boot 应用程序中集成 Zipkin 变得非常简单和直接。Sleuth 会自动为每个入站请求创建一个 span,并为像 Feign 这样的出站请求自动传输 span 上下文。

在 Zipkin UI 中,你可以查看完整的追踪和 span 详情,了解请求是如何在你的微服务架构中流转的。

5、补充一个手动创建复杂span的例子

import brave.Span;
import brave.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class YourController {

    @Autowired
    private Tracer tracer;

    @GetMapping("/complex")
    public String handleComplexOperation() {
        // 开始一个新的 span 来表示这个复杂的操作
        Span complexOperationSpan = tracer.nextSpan().name("complex-operation").start();

        try (Tracer.SpanInScope complexScope = tracer.withSpanInScope(complexOperationSpan)) {
            // 业务逻辑 A
            doBusinessLogicA();

            // 业务逻辑 B
            doBusinessLogicB();

            // 业务逻辑 C
            doBusinessLogicC();

            return "Complex Operation Completed";
        } finally {
            complexOperationSpan.finish();
        }
    }

    private void doBusinessLogicA() {
        // 在 doBusinessLogicA 中创建一个子 span
        Span span = tracer.nextSpan().name("business-logic-a").start();
        try (Tracer.SpanInScope scope = tracer.withSpanInScope(span)) {
            // 执行逻辑 A
            // ...
        } finally {
            span.finish();
        }
    }

    private void doBusinessLogicB() {
        // 在 doBusinessLogicB 中创建一个子 span
        Span span = tracer.nextSpan().name("business-logic-b").start();
        try (Tracer.SpanInScope scope = tracer.withSpanInScope(span)) {
            // 执行逻辑 B
            // 创建另一个子 span
            Span childSpan = tracer.nextSpan().name("business-logic-b-child").start();
            try (Tracer.SpanInScope childScope = tracer.withSpanInScope(childSpan)) {
                // 执行逻辑 B 的子操作
                // ...
            } finally {
                childSpan.finish();
            }
        } finally {
            span.finish();
        }
    }

    private void doBusinessLogicC() {
        // 在 doBusinessLogicC 中创建一个子 span
        Span span = tracer.nextSpan().name("business-logic-c").start();
        try (Tracer.SpanInScope scope = tracer.withSpanInScope(span)) {
            // 执行逻辑 C
            // ...
        } finally {
            span.finish();
        }
    }
}

在上面的例子中,handleComplexOperation 方法代表了一个复杂的业务操作,我们为它创建了一个顶层的 span。随后,我们调用了三个私有方法,每个方法都代表了业务逻辑的一个部分,并且每个部分都有自己的 span。在 doBusinessLogicB 方法中,我们甚至还创建了一个额外的子 span 来表示更细粒度的操作。

请注意,我们使用了 try (Tracer.SpanInScope scope = tracer.withSpanInScope(span)) {...} 语法,这是因为在 Java 7 及以上版本中,try-with-resources 语句可以确保资源(在这个例子中是 Tracer.SpanInScope 对象)在 try 代码块执行完毕后自动关闭。这种语法能够帮助我们确保 span 在使用完成后正确地被结束(通过 span.finish() 调用),即使在抛出异常的情况下也是如此。

一旦你完成了针对每个业务逻辑的 span 创建,Spring Cloud Sleuth 会自动将它们作为追踪信息发送到 Zipkin,你可以在 Zipkin UI 中查看完整的调用链以及每个 span 的详细信息。这样,你就能够对整个操作流程有一个清晰的视图,包括每个独立操作的持续时间和顺序。

在 Spring Cloud Sleuth 中,创建子 span 的过程是自动的,因为 Sleuth 会自动将当前线程上下文中的 span 作为新创建的 span 的父 span。所以在示例中,business-logic-b-child span 将自动成为 business-logic-b span 的子 span,因为当 business-logic-b-child span 被创建时,business-logic-b span 是活跃的并存在于当前线程的上下文中。

在示例代码中,business-logic-b-child span 是在 business-logic-b span 的作用域内创建的,这意味着 Sleuth 会自动将 business-logic-b-child span 与其父 span 进行关联

4、TraceId通过头返回

import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class TraceIdInterceptor implements HandlerInterceptor {

    private static final String TRACE_ID_HEADER = "X-Trace-Id";

    @Autowired
    private Tracer tracer;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 取得当前 Span 的 Trace ID 并放入 MDC
        String traceId = tracer.currentSpan().context().traceId();
        MDC.put(Tracer.TRACE_ID_NAME, traceId);
        return true; // 继续执行其他拦截器链
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 从 MDC 中取得 Trace ID 并添加到响应头
        String traceId = MDC.get(Tracer.TRACE_ID_NAME);
        if (traceId != null) {
            response.addHeader(TRACE_ID_HEADER, traceId);
        }
        // 清理 MDC
        MDC.clear();
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private TraceIdInterceptor traceIdInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(traceIdInterceptor);
    }
}

拦截器不配置路径默认是全局的。

Spring MVC 中的拦截器遵循责任链(Chain of Responsibility)模式。当一个请求到达 Spring MVC 应用时,它会通过一系列的拦截器,这些拦截器按照它们注册的顺序依次执行。这个模式允许多个拦截器独立地处理请求和响应,而不会互相干扰。

拦截器责任链的工作机制如下:

  1. 前置处理 (preHandle): 当请求到达时,每个拦截器的 preHandle 方法将按注册的顺序被调用。如果任何一个 preHandle 方法返回 false,那么责任链将停止执行,后续的拦截器(包括控制器)都不会被执行。如果返回 true,处理过程则会继续传递到下一个拦截器。

  2. 控制器处理: 如果所有的 preHandle 方法都返回 true,请求将会被传递给相应的控制器,控制器的方法将会被调用以处理请求。

  3. 后置处理 (postHandle): 控制器方法返回后,每个拦截器的 postHandle 方法将会以相反的顺序被调用(如果 preHandle 返回了 true)。这个方法可以用来对模型和视图进行额外的处理。

  4. 完成处理 (afterCompletion): 无论请求处理是否成功,或者是否遇到异常,每个拦截器的 afterCompletion 方法都会被调用,允许释放资源、执行清理等操作。

5、日志的保存

结合Docker技术,logback里配置所有的日志打印到控制台。未完待续。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Cloud Alibaba是一个基于Spring Cloud的开源框架,提供了丰富的微服务开发工具包,包括服务注册与发现、配置管理、消息总线、负载均衡、熔断降级、分布式事务、流量控制、安全控制、分布式追踪等功能。 Spring Cloud Alibaba与Nacos、Dubbo、Sentinel等组件协同工作,Nacos作为注册中心和配置中心,Dubbo作为RPC框架,Sentinel作为流量控制和熔断降级组件,共同构建起完整的微服务生态体系。 使用Spring Cloud Alibaba进行服务注册和发现,可以通过注解@EnableDiscoveryClient开启服务注册与发现功能,并通过@FeignClient或@LoadBalanced注解实现服务调用和负载均衡。 使用Spring Cloud Alibaba进行服务治理,可以通过注解@EnableCircuitBreaker开启熔断降级功能,并通过@SentinelResource注解配置熔断降级规则。 使用Spring Cloud Alibaba进行熔断降级,可以通过Sentinel Dashboard进行实时监控和管理,同时通过注解@SentinelRestTemplate实现对RestTemplate的熔断降级。 使用Spring Cloud Alibaba进行服务配置管理,可以通过注解@EnableConfigNacosConfig和@Value注解实现动态配置管理。 使用Spring Cloud Alibaba进行分布式事务管理,可以通过注解@EnableDistributedTransaction开启分布式事务管理功能,并通过@GlobalTransactional注解实现分布式事务的统一管理和控制。 使用Spring Cloud Alibaba进行链路追踪,可以通过注解@EnableZipkinServer和@EnableZipkinClient开启链路追踪功能,并通过Sleuth和Zipkin实现微服务调用链的追踪和分析。 使用Spring Cloud Alibaba进行服务限流,可以通过Sentinel进行实时流控和熔断降级,同时通过注解@SentinelResource实现对服务的限流控制。 Spring Cloud Alibaba支持微服务的服务网关管理,可以通过注解@EnableZuulProxy或@EnableGateway开启服务网关功能,并通过Zuul或Gateway实现微服务的API网关管理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

济南大飞哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值