Flink 的学习笔记

Flink 的学习笔记

1. 概述

​ Flink诞生于欧洲的一个大数据研究项目 StratoSphere。该项目是柏林工业大学的一个研究性项目。早期,Flink是做 Batch 计算的,但是在 2014 年,StratoSphere 里面的核心成员孵化出Flink,同年将 Flink 捐赠 Apache ,并在后来成为 Apache 的顶级大数据项目,同时 Flink 计算的主流方向被定位为 Streaming,即用六十计算来做所有大数据的计算,这就是 Flink 技术 诞生的背景。

官网: https://flink.apache.org/

核心理念:Apache Flink 是为分布式、高性能、岁时可用以及准确的刘处理应用程序打造的开源流处理框架。

在这里插入图片描述

​ **Apache Flink是一个面向分布式数据流处理和批量数据处理的开源计算平台,它能够基于同一个 Flink 运行时(Flink Runtime),提供支持流处理和批处理两种类型应用的功能。**现有的开源计算方案,会把流处理和批处理作为两种不同的应用类型,因为他们所提供的SLA是完全不相同的: 流处理一般需要支持低延迟、Exactly-once 保证,而批处理需要支持高吞吐、高效处理,所以在实现的时候通常是分别给出两套实现方法,或者通过一个独立的开源框架来实现其中每一种处理方案。例如,实现批处理的开源方案有 MapReduce、Tez、Crunch、Spark,实现流处理的开源方案有Samza、Storm。

Flink 在实现流处理和批处理时,与传统的一些方案完全不同,它从另一个视角看待流处理和批处理,将二者统一起来:**Flink是完全支持流处理,也就是说作为流处理看待时输入数据是无界的;批处理被作为一种特殊的流处理,只是它的输入数据流被定义为有界的。** 基于同一个Flink 运行时(Flink Runtime),分别提供了流处理和批处理 API,而这两种API 也是实现上层面向流处理、批处理类型应用框架的基础。

在这里插入图片描述

总的来说,现有的开源计算方案,会把流处理和批处理作为两种不同的应用类型,因为它们所提供的SLA(Service-Level-Aggreement,服务等级协议)是完全不相同的:

  • 流处理一般需要支持低延迟、Exactly-once保证;
  • 批处理需要支持高吞吐、高效处理。

Flink 从另一个视角看待流处理和批处理,将二者统一起来:

  • FlinK 是完全支持流处理,也就是说作为流处理看待时输入数据流是无界的;
  • 批处理被作为一种特殊的流处理,只是它的输入数据流杯定义为有界的。

2. Flink 特性

2.1 核心特性

  • 支持高吞吐、低延迟、高性能的流处理
  • 支持带有事件时间的窗口(Window)操作
  • 支持有状态计算的 Exactly-once 语义
  • 支持高度灵活的窗口(time/count/session)Window 操作,以及 data-driven 驱动
  • 支持具有 BackPressure 功能的持续流模型
  • 支持基于轻量级分布式快照(Snapshot)实现的容错
  • 一个运行时同时支持 Batch on Streaming 处理和Streaming 处理
  • Flink 在 JVM 内部实现了自己的内存管理
  • 支持迭代计算
  • 支持程序自动优化:避免特定情况下 Shuffle、排序等昂贵操作,中间结果进行缓存

2.2 特点

  • Streaming-first:流处理引擎
  • Fault-tolerant:容错,可靠性,checkpoint
  • Scalable:可扩展性,1000节点以上
  • Performance:性能,高吞吐量,低延迟

2.3 Flink 关键特性

  • 低延迟:提供 ms 级时延的处理能力
  • Exactly-once:提供异步快照机制,保增所有数据真正处理一次
  • HA:JobManager 支持主备模式,保证无单点故障
  • 水平扩展能力:TaskManager 支持手动水平扩展

2.4 Hadoop兼容性

Flink 能够支持 YARN,能够从HDFS 和 HBase 中获取数据

能够使用所有的 Hadoop 的格式化输入和输出

能够使用 Hadoop 原有的 Mappers 和 Reducers,并且能与 Flink 的操作混合使用

能够更快的运行 Hadoop 作业

3. Flink 优势

在这里插入图片描述

流场景使用案例正确性保证API 分层体系
1、数据驱动的应用 2、批/流数据分析 3、数据通道和ELA1、Exactly-once 状态一致性保证 2、事件时间处理 3、复杂的late date处理1、统一SQL支持 Stream 和 Batch 数据处理 2、DataStream API & DataSet API 3、ProcessFunction(Time & State)
操作重点适用于各种应用场景高性能
1、部署灵活 2、高可用配置 3、Savepoints1、架构可扩展 2、超大 state 支持 3、增量 checkpointing1、低延迟 2、高吞吐 3、内存计算

4. Flink核心四大基石

在这里插入图片描述

  1. Checkpoint:基于 Chandy-Lamport 算法,实现了分布式一致性快照,提供了一致性的语义。
  2. State:丰富的 State API。ValueState,ListState,MapState BroadcastState
  3. Time:实现了Watermark 机制。乱序数据处理,迟到数据容忍。
  4. Window:开箱即用的滚动、滑动、会话窗口。以及灵活的自定义窗口

5. Flink应用场景

  • Event-driven Applications:时间驱动应用
  • Data Analytics Applications :数据分析应用
  • Data Pipeline Applications :数据挖掘应用

Flink 最适合的应用场景是低延迟的数据处理场景:高并发处理数据,时延毫秒级,且兼具可靠性。

典型应用场景有互联网金融有任务、点击流日志处理、舆情监控。

  1. 优化电子商务的实时搜索结果:阿里巴巴的所有基础设施团队使用 Flink 实时更新产品细节和库存信息,为用户提供更高的关联性。
  2. 针对数据分析团队提供实时流处理服务:king 通过flink-power 数据分析平台提供实时数据分析,从游戏数据中大幅缩短了观察世间
  3. 网络/传感器检测和错误检测:Bouygues 电信公司,是法国最大的电信供应商之一,使用 Flink 监控其有线和无线网络,实现快速故障响应。
  4. 商业智能分析 ETL:Zalando 使用 flink 转换数据以便于加载到数据仓库,将复杂的转换操作转换为相对简单的并确保分析终端用户可以更快的访问数据。

Flink 的典型功能:

  1. Flink 是一个纯流式系统,吞吐量世纪测试可达 100K EPS。而不像某些框架是用 mini batch的模式来达到所谓的流式处理的;
  2. 面对不同的用户数据格式,我们必须支持多种数据源,这一点上 Flink 内置的对各种数据源的支持 (CSV,Kafka,Hbase,Text,Socket数据等)也为用户数据的接入提供了遍历;
  3. Flink 强大的窗口机制(包括翻转窗口,滑动窗口,session窗口,全窗口以及允许用户自定义窗口)可以满足复杂的业务逻辑,使得用户可以编写复杂的业务规则;
  4. Flink 内置的RocksDB 数据存储格式使其数据处理速度快且资源消耗少,在Checkpoint 上起到了至关重要的作用;
  5. Flink 对 算子(operator)的高可控性,使得用户可以灵活添加删除或更改算子行为。这一点对于动态部署有着至关重要的意义。

Flink 非常适合于:

  1. 多种数据源(有时不可靠):当数据是由数以百万计的不同用户或设备产生的,它是安全的假设数据会按照时间产生的顺序到达,和在上游数据失败的情况下,一些时间可能会比他们晚几个小时,迟到的数据也需要计算,这样的结果是准确的。
  2. 应用程序状态管理:当程序变得更加的复杂,比简单的过滤或者增强的数据结构,这个时候管理这些应用的状态将会变得比较难(例如:计算器,过去数据的窗口,状态机,内置数据库)。Flink 提供了工具,浙西状态是有效的,容错的,和可控的,所以你不需要自己构建这些功能。
  3. 数据的快速处理:有一个焦点在实时或近实时用例场景中,从数据生成的那个时刻,数据就应该是可达的。在必要的时候,Flink 完全有能力满足这些延迟
  4. 海量数据处理:这些程序需要分布在很多节点运行来支持所需的规模。Flink 可以在大型的集群中无缝运行,就像是在一个小集群一样。

6. Flink 执行引擎解析/架构

6.1 Flink集群架构

在这里插入图片描述

从上图中可以了解到 Flink 几个最基础的概念,Client、JobManager 和 TaskManager 。Client用来提交任务给 JobManager,JobManager 分发任务给TaskManager 去执行,然后 Taskmanager会心跳的汇报任务状态。对Flink而言,可能是很多级,并且在 Taskmanager 内部和 TaskManager 之间都会有数据传递。

6.2 JobManagers,TaskManagers,Clients

Flink 是一个分布式的主从架构,即 集群运行时是由主节点和从节点组成。Flink 的分布式执行包括两个重要的进程:Master 和 Worker。执行 Flink 程序时,多个进程参与执行,即作业管理器(JobManager),任务管理器(TaskManager)和作业客户端(JobClient)。

在这里插入图片描述

Flink 程序需要提交给 Job Client。然后,JobClient 将作业提交给 Job Manager。JobManager 负责协调资源分配和作业执行。它首先要做的是分配所需的资源。资源分配完成后,任务将提交给相应的TaskManager。在接受任务时,TaskManager 启动一个线程以开始执行。执行到位时,TaskManager 会继续向 JobManager 报告状态更新。可以有各种状态,例如开始执行,正在进行或以完成。作业执行完成后,结果将发送回客户端(JobClient)。

Flink 集群任务启动后架构图:
在这里插入图片描述

  • Program Code:编写的 Flink 应用程序代码
  • Job Client :JobClient 不是 Flink 程序执行的内部部分,但它是任务执行的起点。JobClient 负责接收用户的程序代码,然后创建数据流,将数据流提交给 JobManager 以便进一步执行。执行完成后,Job Client 将结果返回给用户
  • JobManager:主进程(也成为作业管理器)协调和管理程序的执行。它的主要职责包括安排任务,管理checkpoint,故障恢复等。机器集群中至少要有一个 master,master 负责调度 task,协调 checkpoints 和容灾,高可用设置的话可以有多 master,但要保证一个是 leader,其他是standby;JobManager 包含 ActorSystem、Scheduler、CheckPoint三个重要的组件
  • TaskManager:从 JobManager 处接受需要部署的 Task。TaskManager 是在JVM 中的一个或多个线程中执行任务的工作节点。任务执行的并行性由每个 TaskManager 上可用的任务槽决定。每个人物代表分配给任务槽的一组资源。例如,如果 TaskManager 有四个插槽,那么 它将为每个插槽分配25%的内存。可以在任务槽中运行一个或多个线程。同一插槽中的线程共享相同的JVM。同一 JVM 中的任务共享 TCP 连接和心跳信息。TaskManager 的一个 Slot 代表一个可用线程,该线程具有固定的内存,注意 Slot 只对内存隔离,没有对CPU 隔离。默认情况下,Flink 允许子任务共享 Slot,即使它们是不同task 的subtask,只要它们来自相同的 job。 这种共享可以有更好的资源利用率。
  • TaskSlot:任务槽,类似于 YARN 当中的 Container,用于资源的封装。但是在 Flink 中,taskSlot 只负责封装内存的资源,不包含 CPU 的资源。每一个 TaskManager 中会包含3个TaskSlot,所以每一个 TaskManager 中最多能并发执行的任务是可控的,最多 3 个。TaskSlot 有独欠的内存资源,在一个 TaskManager 中可以运行不同的任务。
  • Task:TaskSlot 当中的 Task 就是任务执行的具体单元。

6.3 Task Slots and Resources

每个 worker(TaskManager)都是一个 JVM 进程,可以在单独的线程中执行一个或多个子任务 task。为了控制 worker 接受多少task,worker 具有所谓的 task slot(至少一个)。

每个 task slot 表示 Task Manager 资源的一个固定自己。例如,一个有三个slots 的 Taskmanager 会将其 1/3 的托管内存分配给每个插槽。对资源进行插槽管理意味着子任务不会与来自其他作业的子任务争夺托管内存,而是拥有一定数量的预留托管内存。注意,这里没有发生 CPU 隔离;当前插槽只分割任务的托管内存。

通过调整任务槽的数量,用户可以定义子任务如何彼此隔离。每个 TaskManager 有一个插槽意味着每个人物组运行在单独的 JVM中(例如,可以在单独的容器中启动JVM)。拥有多个插槽意味着更多的子任务共享同一个JVM。相同 JVM中的任务共享 TCP连接(通过多路复用)和心跳消息。 它们还可以共享数据集和数据结构,从而减少每个任务的开销。

在这里插入图片描述

默认情况下,Flink 允许子任务共享插槽,即使它们是不同任务的子任务,只要它们来自相同的作业。结果是一个可以容纳作业的整个管道。允许这个插槽共享有两个主要好处:

  1. Flink 集群需要的任务插槽与作业中使用的最高并行度一样多。不需要计算一个程序总共包含多少任务(具有不同的并行度)。
  2. 更容易得到更好的资源利用。如果没有插槽共享,非密集型 source/map() 子任务将阻塞与资源密集型窗口子任务一样多的资源。使用插槽共享,将我们实例中的基本并行度从 2 提高到 6,可以充分利用插槽资源,同时确保繁重的子任务在任务管理器中得到公平分配。

在这里插入图片描述

6.4 Tasks and Operator Chains

对于分布式执行,将操作符子任务一起链接到任务中。每个任务由一个线程执行。将操作符链接到任务中是一种有用的优化:它减少了线程到线程切换和缓冲的开销,增加了总体吞吐量,同时降低了延迟,可以对链接行为进行配置。下图中的实例数据流使用 5 个子任务执行,因此使用5个并行线程。

在这里插入图片描述

6.5 Flink 组织架构

在这里插入图片描述

Flink 具有分层架构,其中每个组件都是特定层的一部分。每个层都建立在其他层之上,以实现清晰的抽象。Flink 旨在本地,YARN 集群或云上运行。Runtime 是 Flink 的核心数据处理引擎,它通过 JobGraph 形式的 API 接受程序,在执行 JobGraph 时,Flink 提供了多种候选部署方案(如 local,remote,YARN 等)。JobGraph 即为一个一般化的并行数据流图(data flow),它拥有任意数量的Task 来接收和产生data stream。

Datastream 和 DataSet API 是程序员可用于定义 Job 的接口。编译程序时,这些API 会生成 JobGraphs。编译后,DataSet API 允许优化器生成最佳执行计划,而 DataStream API 使用流构建来实现高效的执行计划。DataStream API 和 DataSet API 都会使用单独编译的处理方式生成 JobGraph。DataSet API 使用 optimizer 来决定针对程序的优化方法,而DataStream API 则使用stream builder 来完成该任务。

然后根据部署模型将优化的 JobGraph 提交给执行程序。可以选择本地,远程或 YARN 部署模式。如果已经运行了 Hadoop 集群,那么最好使用 YARN 部署模式。

Flink 附随了一些产生DataSet或DataStream API 程序的类库和API :处理逻辑表查询的Table,机器学习的FlinkML ,图像处理的 Gelly,复杂事件处理的CEP。

在这里插入图片描述

7. Flink容错 State 和 Checkpoints

7.1 状态解释

在批处理过程中,数据是划分为块分片去完成的,然后每一个Task 去处理一个分片。当分片执行完成后,把输出聚合起来就是最终的结果。在这个过程当中,对于 state 的需求还是比较小的。在流计算过程中,对 State 有非常高的要求,因为在流系统中输入是一个无限制的流,会持续运行从不间断。在这个过程当中,就需要将状态数据很好的管理起来。Flink 的失败恢复依赖于“检查点机制+可部分重发的数据源”。检查点机制:检查点定期触发,产生快照,快照中记录了:(1)当前检查点开始是数据源(例如Kafka)中消息的offset (2)记录了所有状态的 operator 当前的状态信息(例如 sum 中的数值)。可部分重发的数据源:Flink 选择最近完成的检查点 K。然后系统重放整个分布式的数据流,然后给予每个 operatpr 他们在检查点 k 快照中的状态。数据源被设置为从位置 Sk 开始重新读取流。例如在 Apache Kafka 中,那意味着告诉消费者从偏移量 Sk 开始重新消费。Flink 中有两种基本类型的State,即 Keyed State 和 Operator State。State 可以被记录,在失败的情况下数据还可以恢复。state 一般指一个具体的 task / operator 的状态(state 数据默认保存在 JVM 的堆内存中)

7.2 State 详解

我们写的 wordcount 的程序没有包含状态管理。如果一个 task 在运行中挂掉了,那么它在内存当中的状态数据都会丢失。所有的数据都需要重新计算。从容错和消息处理的语义上,Flink 引入了 State 和 CheckPoint

首先区分两个概念:

  1. State 一般是指一个具体的 task/operator 的状态(state 数据默认保存在Java的堆内存中)
  2. checkpoint 则表示了一个 FlinK Job 在一个特定时刻的一份全局状态快照,即包含了所有的task/operator 的状态。可以理解成 checkpoint 是把所有 state 数据持久化存储了。

State 可以被记录,这样可以在失败的情况下进行恢复

Flink 中有两种State

  1. KeyedState
  2. operatorState

KeyedState 和 OperatorState 都有两种形式存在

  • 原始状态 RawState

    原始状态,是由用户自行管理状态具体的数据结构,框架在做 checkpoint 的时候,使用 byte[] 来读写内容,对其内部数据结构一无所知

  • 托管状态 managedState

    托管状态是由 Flink 框架管理的状态

通常,在 DataStream 上的状态,推荐使用托管的状态,当实现一个用户自定义的 Operator 的时候,会使用到原始状态

在这里插入图片描述

7.3 CheckPoint

checkpoint (可以理解为 checkpoint 是把 state 数据持久化存储了),则表示了一个Flink Job 在一个特定时刻的一份全局状态快照,即包含了所有task/operator 的状态

CheckPoint 是 Flink 容错的主要机制。它不断为分布式数据流和 executor 状态拍摄快照。它的思想来自 Chandy-Lamport 算法,但已根据 Flink 的定制要求进行了修改。

Flink 基于 Chandy-Lamport 算法实现了一个分布式的一致性快照,从而提供了一致性的语义。

Chandy-Lamport 算法实际上在 1985 年的时候已经被提出来,但并没有被很广泛的应用,而 Flink 则把这个算法发扬光大了。Spark 最近在实现 Continue streaming,Continue streaming 的目的是为了降低它处理的掩饰,其也需要提供这种一致性的予以。最终采用 Chandy-Lamport 这个算法,说明 Chandy-Lamport 算法在业界得到了一定的肯定。

每个快照状态都会报告给 Flink 作业管理器(JobManager)的检查点协调器。在制作快照时,Flink 处理记录对齐,以避免因任何故障而重新处理相同的记录。这种对齐通常需要几毫秒。但是对于某些要求高的应用程序,即使毫秒级的延迟也是不可接受的,我们可以选择在单个记录处理中选择低延迟。默认情况下,Flink 只处理每个记录一次。如果任何应用程序需要低延迟并且至少在一次交付就可以,我们可以关闭该触发器。这将跳过对齐并将改善延迟。

容错:checkpoint 是很重要的机制,因为 Flink 的检查点是通过分布式快照实现的,所以这里对快照和检查点不进行区分。

分布式数据流的轻量级异步快照

分布式有状态流处理支持在云中部署和执行大规模连续计算,同时针对低延迟和高吞吐量。这种模式最基本的挑战之一是在潜在的失败下提供处理保证。现有方法依赖于可用与故障恢复的周期性全局状态快照。这个方法有两个主要缺点。首先,它们经常会停止影响摄取的整体计算。其次,他们急切地坚持传输中的所有记录以及操作状态,这导致比所需更快的快照。**在这项工作中,提出了异步屏障快照(ABS),这是一种适用于现代数据流执行引擎的轻量级算法,可最大限度地减少空间需求。ABS 仅保留非循环执行拓扑上的运算符状态,同时保持循环数据流额最小记录日志。**我们在 Apache Flink 上实现了 ABS,这是一个支持有状态流处理的分布式分析引擎。我们的评估表明,我们的算法对执行没有太大的影响,保持线性可扩展性并且在频繁的快照中表现良好。

7.4 Barriers Flink

分布式快照的核心概念之一是 barriers。这些 barriers 被注入数据流并与记录一起作为 数据流的一部分向下流动。 barriers 永宣不会超过记录,数据流严格有序。barriers 将数据流中的记录分为进入当前快照的记录和进入下一个快照的记录。每个barriers 都带有快照的ID,并且barriers 之前的记录都进入了该快照。barriers 不会中断流的流动,非常轻量级。来自不同快照的多个 barriers 可以同时在流中出现,这意味着可以同时发生各种快照。

**流 barriers 是 Flink 快照的核心要素。它们被摄取到数据流中而不会影响流量。barriers 永远不会超过记录。他们将记录集合分为快照。每个 barriers 都带有唯一的 ID。**下图显示了如何 将 barriers 注入到快照的数据流中:

在这里插入图片描述

基于上图:

  1. 出现一个 Barrier,在该 Barrier 之前出现的记录都属于该 Barrier对应的 Snapshot,在该 Barrier 之后出现的记录属于下一个 Snapshpt
  2. 来自不同 Snapshot 多个 Barrier可能同时出现在数据流中,也就是说同一时刻可能并发生成多个Snapshot
  3. 当一个中间(Intermediate)Operator 接收到一个 Barrier后,它会发送 Barrier 到属于该 Barrier 的 Snapshot 的数据流中,等到 Sink Operator 接收到该 Barrier 后会向 Checkpoint Coordinator 确认该 Snapshot,直到所有的 Sink Operator 都确认了该 Snapshot,才被认为完成了 该 Snapshot

在这里插入图片描述

  1. 一旦操作算子从一个输入流接收到快照 barriers n,它就不能处理来自该流的任何记录,直到它从其他输入接收到 barriers n 为止。否则,它会搞混属于快照 n 的记录和属于快照 n +1 的记录。
  2. barriers n 所属的流暂时会被搁置。从这些流接受的记录不会被处理,而是放入输入缓冲区。
  3. 一旦从最后一个流接收到 barriers n ,操作算子就会发出所有挂起的向后传送的记录,然后自己发出快照 n 的 barriers。
  4. 之后,它恢复处理来自所有输入流的记录,在处理来自流的记录之前优先处理来自输入缓冲区的记录。

Barrier 分为两类:

BarrierBuffer 通过阻塞已接收到barrier 的 input channel 并缓存被阻塞的channel 中后续流入的数据流,直到所有的 barrier 都接收到或者不满足特定的检查点的条件后,才会释放这些被阻塞的channel ,这个机制被称之为-aligning(对齐)。正是这种机制来实现 EXACTLY_ONCE的一致性(它将检查点中的数据精准的隔离开)。

而 Barrier Track 的实现就要简单地多,它仅仅是对数据流中的 barrier 进行跟踪,但是数据流中的元素 buffer 是直接放行的。这种情况会导致同一个检查点中可能会预先混入后续检查点的元素,从而只能提供 AT_LEAST_ONCE 的一致性。

Snapshot 并不仅仅是对数据流做了一个状态的 Checkpoint,它也包含了一个 Operator 内部所持有的状态,这样才能够在保证在流处理系统失败时能够正常地恢复数据流处理。

barrier 作用:它会作为数据流的记录被同等看待,被插入到数据流中,将数据流中记录的进行分组,并沿着数据流的方向向前推进

具体排列过程如下:

  1. Operator 从一个 incoming Stream 接受到 Snapshot Barriern,然后暂停处理,直到其它的 incoming Stream 的 Barrier n(否则属于 2个 Snapshot 的记录就混在一起了)到达该 Operator 接受 Barrier n 的 Stream 被临时搁置,来自这些 Stream 的记录不会被处理,而是被放在一个 Buffer 中。
  2. 一旦最后一个 Stream 接收到 Barrier n ,Operator 会 emit 所有暂存在 Buffer 中的记录,然后向 Checkpoint Coordinator 发送 Snapshot n,继续处理来自多个 Stream 的记录
  3. 基于 Stream Aligning 操作能够实现 Exactly Once 语义,但是也会给流处理应用带来延迟,因为为了排列对齐 Barrier ,会暂时缓存一部分 Stream 的记录到Buffer 中,尤其是在数据流并行度很高的场景下可能更加明显,通常以最迟对齐 Barrier 的一个 Stream 为处理 Buffer 中缓存记录的时刻点。在 Flink 中,提供了一个开关,选择是否使用 Stream Aligning,如果关掉则 Exactly Once 会变成 At least once。
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

白居不易.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值