NiFi文档

Apache NiFi是一款始于美国国家安全局并开源的数据流处理工具,设计目标是实现高度可管理的FlowBasedProgramming。核心概念包括FlowFile、处理器、连接和流控制器。NiFi提供保证交付、背压机制、队列优先级和数据流的QoS配置等关键特性。系统具有强大的可视化界面,支持数据流模板和数据溯源,确保数据安全性并具备灵活的扩展性和集群部署能力。NiFi的架构包括Web服务器、Flow Controller、处理器和扩展组件,采用定制类加载器实现扩展性,通过Java SPI加载自定义处理器和服务。开发人员可以利用Processor、ControllerService和ReportingTask等接口扩展NiFi功能,确保并发安全并遵循特定的类加载规则。
摘要由CSDN通过智能技术生成

NiFi文档

  1. 初识NiFi
    1. 概述

NiFi 最早是美国国家安全局内部使用的工具,用来投递海量的传感器数据.后来由 apache 基金会开源。NiFi基本设计理念:Flow Based Programming ,

 

    1. 核心概念
  • FlowFile
    FlowFile
    表示在系统中移动的每个对象,FlowFile由两部分组成:
    • content 内容,即数据本身
    • attributes 属性,每条数据带上的属性信息.以键值对的形式.
  • FlowFile Processor
    • FlowFile处理器,由它完成对数据的实际处理工作.包括但不限于对数据内容和属性的加载,路由,转换,输出等.
    • 处理器最灵活之处在于处理器可以读写FlowFile的属性信息,并且用自带的领域特定语言(DSL)对属性进行编程.
  • Connection
    • Connections把各个处理器链接起来,从而形成数据处理流程的有向无环图(DAG).也称数据流, NiFi 中的 Flow.
    • Connection 同时充当处理器间的队列,并且队列的属性高度可配置.
    • 这些队列可以配置优先级,可以在设置阈值,可以实现反压。
  • Flow Controller
    • 流控制器对用户不可见的.它充当维护处理器如何连接和管理所有处理器所使用的线程及其分配的重要角色。
    • Flow Controller充当促进处理器之间FlowFiles交换的代理。
  • Process Group
    • 为了方便管理,把一组特定的处理器及其连接组成的 Flow 放到一个处理组中去,可以通过输入端口接收数据并通过输出端口发送数据。
    • 以这种方式,处理组可以通过组合其他组来创建全新组,形成更加复杂的DAG( Flow )
    1. 关键特性
  • Flow 流高度可管理
  • 保证交付
    NiFi的一个核心理念是即使在非常高的规模下,保证交付也是必须的。这是通过有效使用专门的持久化的预写日志(WAL)和内容存储库来实现的。它们的设计可以实现非常高的事务处理速率,有效的负载分散,写入时复制以及发挥传统磁盘读/写的优势。
  • 背压和数据缓冲机制
    NiFi支持缓冲所有队列数据,以及在这些队列达到指定限制时提供背压的能力,或者在数据达到指定时间时使数据过期失效。
  • 可配置优先级的队列
    NiFi允许设置一个或多个优先级策略,用于如何从队列中检索数据。默认是先进先出,但有时候应该先拉取最新的数据,最大的数据或其他一些自定义方案。
  • Flow 流可配置特定的QoS(延迟v吞吐量,容量损失等)
    在 Flow 流中有一些点是很关键的,且不能容忍丢失.或者有时候必须在几秒钟内处理和交付它。NiFi 可以对这些问题进行细粒度的特定配置。

 

  • 易于使用
  • 可视化的控制和命令
    得益于强大的 web 操作界面.无论多么复杂的数据流都能在 web 界面上直观的呈现.整个数据处理流程,包括设计,控制,反馈和监控都可在web界面完成,一步到位.任何更改都能在界面上立马生效,完全不要部署的过程.对于整个数据流,更可以对中间某个处理器进行单独变更,实时生效.
  • 数据流模板
    对于设计好的数据流处理流,可以保存为模板来进行复用.模板可以导出成xml文件,导入到其他 NiFi 中进行多处使用.
  • 数据溯源
    flowfile 流过Flow 流时,NiFi会自动记录,索引并提供可用的起源数据,包括导入,导出,转换等。这些信息对于故障排除,优化等很有用处.
  • 对历史数据进行细粒度的恢复
    NiFi的内容存储库旨在充当历史记录的滚动缓冲区。数据仅在内容存储库过期时或存储空间不足时才会被删除。这与数据起源能力相结合,提供了非常精细的操作功能.包括对数据历史中的某一个点的点击查看内容,下载内容,处理回放等功能.所有数据都可以回溯到它生命周期中很早的某一点.

 

  • 安全机制
  • 系统内部安全
    Flow 流中的流动的数据都可以进行加密传输
  • 用户使用安全
    支持用户认证和不同级别的用户授权(可读,管理数据流,系统管理)
  • 多租户授权
  • 可扩展的架构设计
  • 可扩展组件
    NiFi 的核心设计就是扩展. 它的 processors, Controller Services, Reporting Tasks, Prioritizers, and Customer User Interfaces 都是 可扩展的.
  • 隔离的类加载器
    自定义的类加载器保证了扩展的组件简单的依赖关系.
  • 点到点的通信协议
    NiFi实例之间的通信协议是NiFi 点到点(S2S)协议。S2S可以轻松,高效,安全地将数据从一个NiFi实例传输到另一个实例。NiFi 客户端 的 库也可以轻松在其他应用程序使用,以通过S2S来与NiFi 实例进行通信。S2S中支持基于套接字的协议和HTTP(S)协议作为底层传输协议,使得可以将代理服务器嵌入到S2S通信中。
  • 灵活的扩容模型
  • 更多的NiFi 实例
    可以搭建 NiFi 集群,也可以不组成集群,多台机器使用 点到点 协议来协作.
  • 更大的并发数量
    直接修改处理器的并发数
    1. 架构

 

  • Web Server

web服务器的提供基于http的命令和控制API

  • Flow Controller
  • 流量控制器是操作的大脑。它为扩展程序提供运行所需的线程,并管理扩展程序何时接收执行资源的时间表。
  • Processor

处理组件

 

  • Extensions

扩展组件

 

  • FlowFile Repository

通过FlowFile Respository可跟踪Flow中处于活动状态的FlowFile的状态。存储库的实现是插件式的,默认是位于指定磁盘分区上的持久性预写日志。

  • Content Repository

Content Repository 作为FlowFile的存储库,实现是插件式的,默认是一种相当简单的机制,该机制将数据块存储在文件系统中。可以指定多个文件系统存储位置,以便使用不同的物理分区以减少任何单个卷上的争用。

 

  • Provenance Repository

Provenance Repository是存储所有来源事件数据的地方。存储库实现是插件式的,默认实现是使用一个或多个物理磁盘卷。在每个位置内,事件数据都被索引并可以搜索。

 

集群

 

NiFi 1.0版本开始,采用了零主群集的范例。NiFi群集中的每个节点都对数据执行相同的任务,但是每个节点都对不同的数据集进行操作。通过ZooKeeper选择一个节点作为集群协调器,并且故障转移由ZooKeeper自动处理。所有群集节点均向群集协调器报告心跳和状态信息。群集协调器负责断开和连接节点。此外,每个群集都有一个主节点,该节点也由ZooKeeper选择。作为DataFlow管理者,您可以通过任何节点的用户界面(UI)与NiFi群集进行交互。您所做的任何更改都将复制到群集中的所有节点,从而允许多个入口点。

 

  1. 源代码浅析
      1. 总体结构

   

  • nifi-api

就是 nifi 的应用程序接口,里面就是定义了整个工程用到的接口,注解,抽象类和枚举等基本的接口和信息.

  • nifi-assembl

负责 nifi 的成品装配, 工程打包最后形成的可供部署的压缩包就在这个工程里的 target 目录内.

  • nifi-bootstarp

负责 nifi 这个 jvm 应用程序的启动相关事宜

  • nifi-commons

nifi 诸多特性,比如data-provenance,expression-language,s2s 传输 的实现就在这里,同时也是 nifi 的工具类集合

  • nifi-docker

nifi docker 应用相关

  • nifi-docs

nifi 的文档 实现相关

  • nifi-external

nifi 内部元信息和外部交换,主要用于集群模式下

  • nifi-framework-api

这就是nifi 核心框架层的api,也就是架构图中的 Flow Controller 那一层,注意这里只是各种接口信息定义,不是实现.

  • nifi-maven-archetypes

这里只是为了生成两个 maven archetype,一个是 nifi 自定义处理器的脚手架,一个是 nifi 自定义服务的脚手架.这些脚手架在 maven 的中央仓库都有提供.

  • nifi-mock

用于 nifi mock 测试

  • nifi-nar-bundles

nifi java 工具箱就是这里了.整个 nifi 里面大部分的 maven 工程都是这个工程的子工程.在这个工程里面,一个 bundle 就是一个工具,也对应着上面架构图里的 Extension

  • nifi-toolkit

这里面是 nifi 的命令行工具的实现.nifi 也提供了比较丰富的命令行指令.

      1. Nifi程序入口

nifi-bpootstrap模块内有一个org.apache.nifi.bootstrap.RunNifi的类,该类的main()方法即为Nifi的启动入口方法。

 

接着看 start 方法,里面做了很多前期的准备性工作,主要是加载 bootstrap.conf 里配置的属性,以及在里面构建另外一个 java cmd 命令:

final ProcessBuilder builder = new ProcessBuilder();
……

builder.command(cmd)

……

Process process = builder.start();

……

 

所以这个 start 方法是启动了另外一个 java 进程,这个进程才是真正的 NiFi runtime
通过代码跟踪或查看日志,可见,这个cmd命令类似如下:

/opt/jdk1.8.0_131/bin/java
-classpath /opt/nifi-1.7.1/./conf:/opt/nifi-1.7.1/./lib/logback-core-1.2.3.jar:/opt/nifi-1.7.1/./lib/jetty-schemas-3.1.jar:/opt/nifi-1.7.1/./lib/logback-classic-1.2.3.jar:/opt/nifi-1.7.1/./lib/jul-to-slf4j-1.7.25.jar:/opt/nifi-1.7.1/./lib/jcl-over-slf4j-1.7.25.jar:/opt/nifi-1.7.1/./lib/nifi-properties-1.7.1.jar:/opt/nifi-1.7.1/./lib/nifi-runtime-1.7.1.jar:/opt/nifi-1.7.1/./lib/nifi-framework-api-1.7.1.jar:/opt/nifi-1.7.1/./lib/nifi-nar-utils-1.7.1.jar:/opt/nifi-1.7.1/./lib/javax.servlet-api-3.1.0.jar:/opt/nifi-1.7.1/./lib/log4j-over-slf4j-1.7.25.jar:/opt/nifi-1.7.1/./lib/slf4j-api-1.7.25.jar:/opt/nifi-1.7.1/./lib/nifi-api-1.7.1.jar
-Dorg.apache.jasper.compiler.disablejsr199=true
-Xmx3g -Xms3g
-Djavax.security.auth.useSubjectCredsOnly=true
-Djava.security.egd=file:/dev/urandom
-Dsun.net.http.allowRestrictedHeaders=true
-Djava.net.preferIPv4Stack=true
-Djava.awt.headless=true -XX:+UseG1GC
-Djava.protocol.handler.pkgs=sun.net.www.protocol
-Duser.timezone=Asia/Shanghai
-Dnifi.properties.file.path=/opt/nifi-1.7.1/./conf/nifi.properties
-Dnifi.bootstrap.listen.port=56653
-Dapp=NiFi
-Dorg.apache.nifi.bootstrap.config.log.dir=/opt/nifi-1.7.1/logs
org.apache.nifi.NiFi

 

可以清晰的看到,命令中实际执行的是javaorg.apache.nifi.NiFimain方法。

 

      1. Nifi启动初始化

这个org.apache.nifi.NiFi类在以下模块中:

nifi-nar-bundles

       +-- nifi-framework-bundle

                +--- nifi-framework

                            +--- nifi-runtime

 

Nifi-framework模块就是nifi框架的核心代码

Org.apache.nifi.NiFi.main()方法如下:

 

/**
 * Main entry point of the application.

 *

 * @param args things which are ignored

 */

public static void main(String[] args) {

    LOGGER.info("Launching NiFi...");

    try {

        NiFiProperties properties = convertArgumentsToValidatedNiFiProperties(args);

        new NiFi(properties);

    } catch (final Throwable t) {

        LOGGER.error("Failure to launch NiFi due to " + t, t);

    }

}

 

Main()方法调用了NiFi的构造方法:

public NiFi(final NiFiProperties properties)
        throws ClassNotFoundException, IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
  
    this(properties, ClassLoader.getSystemClassLoader());



}
 
public NiFi(final NiFiProperties properties, ClassLoader rootClassLoader)

        throws ClassNotFoundException, IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
  
 

 

第二个构造方法是实际上的构造方法,里面进行了大量初始化操作,以下是非常关键的部分:

// expand the nars
final ExtensionMapping extensionMapping = NarUnpacker.unpackNars(properties, systemBundle);



// load the extensions classloaders

NarClassLoaders narClassLoaders = NarClassLoadersHolder.getInstance();



narClassLoaders.init(rootClassLoader,

        properties.getFrameworkWorkingDirectory(), properties.getExtensionsWorkingDirectory());

这部分初始化了NiFiJava扩展工具箱,这些工具从Nifi的用户来说就是在NiFi安装目录的lib目录下的各个*.nar包,这个些nar包实际就是NiFi增加了特定额外信息的jar包集合的压缩,本质上还是jar包。以下是我们解压开的一个nar包,结构如下:

 

那么回到NiFi的构造方法内,首先是解压这些nar包,并在代码内用ExtensionMapping对象描述,代码如下:

 
// expand the nars

final ExtensionMapping extensionMapping = NarUnpacker.unpackNars(properties, systemBundle);

 

然后加载并初始化这些类加载器:

 
// load the extensions classloaders

NarClassLoaders narClassLoaders = NarClassLoadersHolder.getInstance();



narClassLoaders.init(rootClassLoader,

        properties.getFrameworkWorkingDirectory(), properties.getExtensionsWorkingDirectory());



// load the framework classloader

final ClassLoader frameworkClassLoader = narClassLoaders.getFrameworkBundle().getClassLoader();

if (frameworkClassLoader == null) {

    throw new IllegalStateException("Unable to find the framework NAR ClassLoader.");

}



final Set<Bundle> narBundles = narClassLoaders.getBundles();

NiFi 的官方介绍中,有两处它的特性介绍是扩展和类加载隔离,这里我们可以对它这两个特性的实现一探究竟了.它为每一个 nar 包构造了一个独立的自定义的类加载器: NarClassLoader

 
public class NarClassLoader extends URLClassLoader {



    private static final Logger LOGGER = LoggerFactory.getLogger(NarClassLoader.class);



    private static final FileFilter JAR_FILTER = new FileFilter() {

        @Override

        public boolean accept(File pathname) {

            final String nameToTest = pathname.getName().toLowerCase();

            return nameToTest.endsWith(".jar") && pathname.isFile();

        }

    };

 

目前基本清晰, NiFi 扩展性是由自定义的压缩文件 nar 自定义的类加载器来提供的. 接着往下看:

 
// load the server from the framework classloader

Thread.currentThread().setContextClassLoader(frameworkClassLoader);

Class<?> jettyServer = Class.forName("org.apache.nifi.web.server.JettyServer", true, frameworkClassLoader);

Constructor<?> jettyConstructor = jettyServer.getConstructor(NiFiProperties.class, Set.class);



final long startTime = System.nanoTime();

nifiServer = (NiFiServer) jettyConstructor.newInstance(properties, narBundles);

nifiServer.setExtensionMapping(extensionMapping);

nifiServer.setBundles(systemBundle, narBundles);

回想架构图VM 的最上层是 web server , 这个 web server 就是在这里被加载了,这是一个 jetty server ,继续往下看:

 
if (shutdown) {

    LOGGER.info("NiFi has been shutdown via NiFi Bootstrap. Will not start Controller");

} else {

    nifiServer.start();



    if (bootstrapListener != null) {

        bootstrapListener.sendStartedStatus(true);

    }



    final long duration = System.nanoTime() - startTime;

    LOGGER.info("Controller initialization took " + duration + " nanoseconds "

            + "(" + (int) TimeUnit.SECONDS.convert(duration, TimeUnit.NANOSECONDS) + " seconds).");

}

start 这个 nifiServer ,这个 NiFi 对象的构造方法这里就全部走完了.

      1. NiFi-Web

NiFiServer.start()的方法内,代码跳转到nifi-framework下的一个子模块nifi-web内了。

 

      1. nifi-jetty

与Web相关的代码都在这个模块了,包括Server和界面相关的代码,上面提到的NiFiServer的实现类JettyServer就在子模块nifi-jetty内了。

 

接着看 JettyServer 这个类,上面的 NiFi 构造方法里面最后是先实例化了这个 JettyServer ,然后调用了 start 方法.先看它的构造方法,只看注释,找到了核心方法:

 
// load wars from the bundle

final Handler warHandlers = loadInitialWars(bundles);

可以看到,其实就是把 war 包加载进来了,这些 war 包就是 nifi-web 下面的子工程,有几个子工程的 pom 文件中配置的就是<packaging>war</packaging>

接着看这个 start 方法:
第一句就是 ExtensionManager.discoverExtensions(systemBundle, bundles); 就是这里把所有的扩展类加载进 JVM , 看到看到 ExtensionManager 的注释,这个注释就说明了一切

Scans through the classpath to load all FlowFileProcessors, FlowFileComparators, and ReportingTasks using the service provider API and running through all classloaders (root, NARs).

这个 ExtensionManager 在加载类的时候,用到了java 的一种比较高级的机制, java SPI(service provider interface),这种机制在很多框架中比如 spring 中大量使用

final ServiceLoader<?> serviceLoader = ServiceLoader.load(entry.getKey(), bundle.getClassLoader());

这个机制解释了为什么写自定义的处理器的时候要在 /resources /META-INF/services 目录下面写上配置文件.在自定义处理开发的时候,一定要注意写这个配置文件,否则类是加载不进来的

接着 start 这个 jetty server,接着往下看,只看注释,可以看到,大致就是做了 server context 以及 filter 的注入工作了:

// ensure the appropriate wars deployed successfully before injecting the NiFi context and security filters // this must be done after starting the server (and ensuring there were no start up failures)

 

      1. nifi-web-api

基本到这里, NiFi 的实例化和初始化流程基本就有个大致了解了.我们可以接着再进一步,看到 nifi-web-api 这个工程,这个工程其实就是 nifi restful 接口工程,nifi 的所有 restful 接口都是这里实现的,包括处理器的新增,处理器的连接以及处理器的 start

在里面随便打开一个以 resource 结尾的类:

 

这里我们可看到rest接口注解,接着打开 resources 文件夹,看到了 nifi-web-api-context.xml 文件 :

 

原来这是一个 spring web 工程.然后找到一个关键的 configuration :

 

NiFi 实例内所有的对象都是通过 spring ioc 注入的.

      1. 小结

现在为止,从开发角度对 NiFi 就有了一个基本的认识了,它是一个 JVM 应用,它通过独立的类加载器加载类,使用 spring ioc 注入和管理对象.从以上的分析,我们了解到了 NiFi 的扩展性特性的大致实现,也了解了架构图最上面的一部分源码.至于它其他诸多特性的源码和实现,则需要花更多的时间研究 nifi-framework-core 工程了.

 

  1. 开发指南(译)

开发指南主要是介绍NiFi的扩展开发方式。

    1. NiFi组件

NiFi提供了几个扩展点,以便开发人员能够扩展NiFi功能来满足特定需求。以下列表对最常见的扩展点进行了概要描述:

      1. Processor(处理器)

Processor接口是NiFiFlowFile,以及其属性和内容的访问机制 ProcesorNiFi DataFlow的基本组成部分。此接口用于完成以下所有任务:

  • Create FlowFiles/创建FlowFiles
  • Read FlowFile content/读取FlowFile内容
  • Write FlowFile content/写入FlowFile内容
  • Read FlowFile attributes/读取FlowFile属性
  • Update FlowFile attributes/更新FlowFile属性
  • Ingest data/提取(采集)数据
  • Egress data/输出数据
  • Route data/路由数据
  • Extract data/抽取数据
  • Modify data/修改数据
      1. ReportingTask

ReportingTask接口允许将度量标准,监视信息和内部NiFi状态发布到外部端点(例如日志文件,电子邮件和远程Web服务)

      1. ControllerService

ControllerService在单个JVM中跨Processor,提供其他ControllerServiceReportingTasks等的共享状态和功能。例如,我们可通过在ControllerService中一次性加载非常大的数据集,并将其共享给其它需要的processor,而不需要各个Processor自己加载数据集。

      1. FlowFilePrioritizer

FlowFilePrioritizer可以对队列中的FlowFile进行优先级排序,以便可以按对特定期望顺序处理FlowFiles

      1. AuthorityProvider

AuthorityProvider负责确定应授予给定用户哪些特权和角色(如果有)。

 

    1. Processor API

ProcessorNiFi内是使用最广泛的组件,是唯一可以创建,删除,修改或检查FlowFiles(数据和属性)的组件。

所有Processor都使用JavaServiceLoader机制(JSL)加载和实例化。所以所有处理器都必须遵守以下规则:

  1. 处理器必须具有默认构造函数。
  2. 处理器的JAR文件必须在META-INF / services目录中包含org.apache.nifi.processor.Processor文本文件,其中每一行都包含处理器的完全限定的类名。

尽管我们可以直接实现Processor接口来实现一个Processor,但通常我们不会这么做,因为NiFi为我们提供一个org.apache.nifi.processor.AbstractProcessor的抽象基础类,这使的开发一个Processor更容易。

 

并发

NiFi是一个高度并发的框架。这意味着所有扩展都必须是线程安全的。如果不熟悉用Java编写并发软件,强烈建议您熟悉Java并发原理。

 

      1. Supporting API

为了理解Processor API,我们必须首先理解几个支持的类和接口,下面将对其进行讨论。

        1. FlowFile

FlowFile是一种逻辑概念,它包含了数据以及与数据关联的的一组属性,这些属性包括FlowFile的唯一标识符,名称,大小或者其他属性。虽然FlowFile的内容和属性可以更改,但FlowFile对象是不可变的。通过ProcessSession可以对FlowFile进行修改。

 

FlowFiles的核心属性在org.apache.nifi.flowfile.attributes.CoreAttributes枚举中定义。最常见的属性是文件名,路径和uuid。用引号引起来的字符串是CoreAttributes枚举中属性的值。

 

  • 文件名(“filename”):FlowFile的文件名。文件名不应包含任何目录结构。
  • UUID“ uuid”):分配给此FlowFile的通用唯一标识符,用于将FlowFile与系统中的其他FlowFile区分开。
  • 路径(“path”):FlowFile的路径指示FlowFile所属的相对目录,并且不包含文件名。
  • 绝对路径(“ absolute.path”):FlowFile的绝对路径指示FlowFile所属的绝对目录,并且不包含文件名。
  • 优先级(“priority”):指示FlowFile优先级的数字值。
  • MIME类型(“ mime.type”):此FlowFileMIME类型。
  • 丢弃原因(“ discard.reason”):指定丢弃FlowFile的原因。
  • 备用标识符(“ alternate.identifier”):表示除FlowFileUUID之外的已知标识符,该标识符引用该FlowFile
        1. ProcessSession(处理会话)

ProcessSession,通常简称为会话,通过它可以创建,销毁,检查,克隆FlowFiles并将其转移到其他处理器。同一时间一个ProcessSession只能绑定到单一Processor上,并确同一个FlowFile不会被超过一个Processor访问。

ProcessSession提供了一种机制,用于通过添加或删除属性或修改FlowFile的内容来创建修改版本的FlowFilesProcessSession还公开了一种发出源事件的机制,该机制提供了跟踪FlowFile的血统和历史的功能。在一个或多个FlowFiles上执行操作后,可以提交或回滚ProcessSession

 

        1. ProcessContext(处理上下文)

ProcessContextProcessorNiFi FrameWork之间的桥梁。它提供Processor的配置信息,并允许Processor执行特定于框架的任务,例如生成其资源,以便框架将调度其他处理器运行,而不会消耗不必要的资源。

 

        1. PropertyDescriptor(属性描述)

PropertyDescriptor定义将由ProcessorReportingTaskControllerService使用的属性。属性的定义包括其名称,属性说明,可选的默认值,验证逻辑以及关于是否需要该属性才能使Processor有效的指示符。通过实例化该类的实例Proper

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值