栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 前沿技术 > 大数据 > 大数据系统

Apache Flink-实时计算正当时(Apache Flink流处理 英文版)

Apache Flink-实时计算正当时(Apache Flink流处理 英文版)

《基于Apache Flink的流处理》

1. 状态化流处理概述

1.1 传统数据处理架构

1.1.1 事务型处理1.1.2 分析型处理 1.2 状态化流处理1.3 历史回顾(开源流处理的演变)1.4 Flink 快览 2. 流处理基础

2.1 Dataflow编程概述

2.1.1 Dataflow图2.1.2 数据并行和任务并行2.1.3 数据交换策略 2.2 并行流处理

2.2.1 延迟和吞吐

2.2.1.1 延迟2.2.1.2 吞吐2.2.1.3 延迟与吞吐 2.2.2 数据流上的操作

2.2.2.1 数据接入和数据输出2.2.2.2 转换操作2.2.2.3 滚动聚合2.2.2.4 窗口操作

2.2.2.4.1 滚动窗口2.2.2.4.2 滑动窗口2.2.2.4.3 会话窗口 2.3 时间语义

2.3.1 流处理场景中下一分钟的含义2.3.2 处理时间2.3.3 事件时间2.3.4 水位线2.3.5 处理时间与事件时间 2.4 状态和一致性模型

2.4.1 任务故障2.4.2 结果保障

2.4.2.1 至多一次(AT-MOST-ONCE)2.4.2.2 至少一次(AT-LEAST-ONCE)2.4.2.3 精确一次(EXACTLY-ONCE)2.4.2.4 端到端精确一次(END-TO-END EXACTLY-ONCE) 3. Apache Flink架构

3.1 系统架构

3.1.1 搭建Flink所需的组件

3.1.1.1 TaskManager 3.1.2 应用部署

3.1.2.1 框架模式3.1.2.2 库模式 3.1.3 任务执行3.1.4 高可用设置 4. 设置Flink开发环境5. DataStreamAPI6. 基于时间和窗口的算子7. 有状态算子和应用8. 读写外部系统

1. 状态化流处理概述

Apache Flink 是一个分布式流处理引擎,具有直观而富于表现力的API来实现有状态流处理应用程序。

1.1 传统数据处理架构

几十年来,数据和数据处理在企业中无处不在。多年来,数据的收集和使用一直在增长,很多公司都设计和构建了管理这些数据的基础架构。

大多数企业实现的传统架构区分了两种类型的数据处理

事务型处理分析型处理 1.1.1 事务型处理

公司在日常业务活动中使用各种应用程序,如企业资源规划(ERP)系统、客户关系管理(CRM)软件和基于网络的应用程序。这些系统通常设计有独立的数据处理层(应用程序本身)和数据存储层(事务数据库系统)。
当应用程序需要更新或扩展时,这种应用程序设计可能会导致问题。
克服应用程序之间紧耦合的最新方法是微服务设计。微服务被设计成小型、独立的应用程序。他们遵循UNIX哲学:做一件事并把它做好。更复杂的应用程序是通过将几个微服务相互连接而构建的,这些微服务只通过标准接口进行通信,如RESTful HTTP连接。因为微服务彼此严格分离,并且只通过定义明确的接口进行通信,所以每个微服务都可以用不同的技术堆栈来实现,包括编程语言、库和数据存储。微服务和所有必需的软件和服务通常被打包并部署在独立的容器中。

1.1.2 分析型处理

存储在公司各种交易数据库系统中的数据可以提供关于公司业务运营的有价值的见解。然而,事务性数据通常分布在几个不相连的数据库系统中,如果可以联合分析,事务性数据更有价值。
对于分析类查询,数据通常复制到数据仓库中,而不是直接在事务数据库上运行分析查询,数据仓库是用于分析查询工作的专用数据存储。为了填充数据仓库,需要将事务数据库系统管理的数据复制到数据仓库中。将数据复制到数据仓库的过程称为提取-转换-加载(extract–transform–load,ETL)。

ETL过程

    从事务性数据库中提取数据,将其转换为通用表示形式,可能包括数据验证、数据规范化、编码、去重和表模式转换,最后将其加载到分析性数据库中。

ETL过程可能非常复杂,通常需要技术上复杂的解决方案来满足性能要求。ETL过程需要定期运行,以保持数据仓库中的数据同步。
一旦数据被导入数据仓库,就可以对其进行查询和分析。通常,数据仓库上查询分为两类

第一类是定期报告查询第二种类型是即席查询(ad-hoc query)

数据仓库以批处理方式执行这两种查询。

时至今日,Apache Hadoop生态组件已经成为许多企业的信息技术基础架构中不可或缺的一部分。

大量数据(如日志文件、社交媒体或网络点击日志)被存储在Hadoop的分布式文件系统(HDFS)、S3或其他大容量数据存储区(如Apache Hbase),而不是将所有数据存储在关系型数据库系统,Apache Hbase以较小的成本提供了巨大的存储容量。驻留在这种存储系统中的数据可以通过Hadoop上的SQL引擎进行查询和处理,例如Apache Hive、Apache Drill或Apache Impala。这就是前文描述的传统架构的一种实践 1.2 状态化流处理

任何处理事件流的应用,如果要支持跨多条记录的转换操作,都需要是有状态的,具有存储和访问中间数据的能力。

当应用程序接收到事件时,它可以依赖状态来执行任意计算,包括从状态中读取数据或向状态中写入数据。原则上,状态可以在许多不同的存储方案,包括程序变量、本地文件、嵌入式或外部数据库。

Apache Flink将应用程序状态存储在本地内存或本地嵌入式数据库中并且会定期向远程持久化存储来同步。
三类常见的有状态的流处理应用:

    事件驱动型应用:通过接收事件流出发特定应用业务逻辑。
    典型应用:实时推荐(用户浏览商家页面的同事进行产品推荐)、模式识别或复杂事件处理(根据信用卡交易记录进行欺诈识别)、异常检测(计算机网络入侵检测)。
    事件驱动的应用程序对运行它们的流处理引擎有很高要求。并非所有的流处理器都同样适合运行事件驱动的应用程序。应用编程接口的可表达性以及状态处理和事件时间支持的质量决定了可以实现和执行的业务逻辑。Apache Flink是运行这类应用程序的非常好的选择。数据管道:相同的数据存储在不同的数据存储系统。
    传统方法是定期ETL作业,但是延时性太高。另一种方法是使用事件日志来分发更新。更新由事件日志写入和分发。日志的使用者(也就是基于Flink的某个应用)将更新同步到需要同步的数据存储中,我们把这类应用称为数据管道。用Flink来实现一个数据管道应用是很方便的,它可以支持对不同种类数据存储的读写,而且可以在短时间处理大量数据。流式分析:
    传统ETL作业给流式分析带来相当大的延迟。从过去视角来看,这种延迟可以接受,但是当今的应用必须能够实时收集数据并迅速响应。
    流式分析应用不再需要等待周期性地触发。相反,它持续获取事件流,以极低的延迟整合最新事件,不断更新结果。通常,流式应用程序将结果存储在支持高效更新的外部数据存储中,如数据库或键值存储。流式分析应用程序的实时更新结果可以支撑仪表板应用程序的展示。
    流式分析应用还有个优势。传统分析流程需要许多独立组件的配合使用,如ETL进程、存储系统等。相比之下,运行有状态流处理程序的流处理引擎会全面负责事件获取、维护状态的持续计算、更新结果等。还能一定程度保障故障恢复、调节应用计算资源等。诸如Flink之类的还支持事件事件处理,生成精准确定的结果,具备短时间内处理大量数据的能力。
    流式分析应用常用于:手机网络质量监控、移动应用中的用户行为分析、消费者技术中的实时数据即席分析。
    Flink还支持对于数据流的分析型SQL查询。(此书未涉及)
1.3 历史回顾(开源流处理的演变) 1.4 Flink 快览

Apache Flink是很棒的第三代分布式流处理器。它以高吞吐量和低延迟大规模提供精确的流处理。

以下特性让Flink脱颖而出:

同时支持事件时间和处理时间语义。提供精确一次(exactly-once)的状态一致性保证。每秒处理数百万个事件时的毫秒级延迟。Flink应用程序可以扩展到在数千个内核上运行。分层设计的API可以轻松连接到最常用的存储系统,如Kafka、Cassandra、ElasticSearch、JDBC、HDFS、S3等。能够全天运行流式应用程序,宕机时间非常少能够在不会丢失应用程序的状态的前提下,更新作业的应用程序代码或者将作业迁移到不同的Flink集群。提供详细的指标支持批处理对开发者友好。API极为易用。并且嵌入式执行模式可以将应用连同整个Flink都嵌入到单个JVM进程中,方便在IDE里运行和调试基于Flink的应用 2. 流处理基础 2.1 Dataflow编程概述 2.1.1 Dataflow图

2.1.2 数据并行和任务并行

可以以不同的方式利用数据流图中的并行性。

首先,可以对某个算子的输入数据进行分区,并在数据子集上并行执行相同操作的任务。这种类型的并行称为数据并行。数据并行非常有用,因为它允许将大量计算数据分布到多个不同的物理节点上并行执行。

其次,可以让不同算子的任务 并行执行相同或不同数据的计算。这种类型的并行称为任务并行。使用任务并行,可以更好地利用集群的计算资源。

2.1.3 数据交换策略

数据交换策略定义了数据项如何被分配给物理Dataflow图中的不同任务。常见的数据交换策略:转发、广播、基于键值、随机

转发策略在发送端任务和接收端任务之间一对一地进行数据传输。如果两个任务位于同一个物理机器上(这通常由任务调度器来保证),这种交换策略避免了网络通信。广播策略将每个数据项发送给算子的所有并行任务。因为这种策略复制数据并涉及网络通信,所以成本相当高。基于键值的策略通过键属性划分数据,并保证具有相同键的数据项将由相同的任务处理。随机策略将数据项均匀地随机分配给任务,以便负载均衡 2.2 并行流处理

下面看看如何将Dataflow的概念运用到并行数据流处理中。我们先给出数据流的定义:数据流是一个长度可能无限长的事件序列

数据流的例子如下:监控器产生的监控数据、传感器产生的测量数据、信用卡交易数据、气象站观测数据、搜索引擎搜索记录等

2.2.1 延迟和吞吐

对于批处理应用程序,我们通常关心作业的总执行时间,或者我们的处理引擎读取输入、执行计算和写回结果需要多长时间。由于流应用程序连续运行,并且输入可能是无限的,因此在流处理中没有总执行时间的概念。取而代之的是,流处理必须尽可能快地为传入数据提供结果(延迟),同时还要应对很高的事件输入速率(吞吐)。我们用延迟和吞吐来表示这两方面的性能需求。

2.2.1.1 延迟

延迟表示处理一个事件所需的时间。本质上,它是接收事件到在输出中看到事件处理效果的时间间隔。

2.2.1.2 吞吐

吞吐量是对系统处理能力的一种度量——它的处理速率。也就是说,吞吐量告诉我们系统每单位时间可以处理多少个事件。

2.2.1.3 延迟与吞吐

延迟和吞吐 不是独立的指标。

如果事件需要很长时间才能在数据处理管道中传输,我们就无法轻松确保高吞吐量(延迟影响了吞吐)。同样,如果系统的处理能力过低,事件将被缓冲,必须等待才能得到处理(吞吐影响了延迟)。

降低延迟可提高吞吐量。如果一个系统可以更快地执行操作,它可以在相同的时间内执行更多的操作。而一个很好的方式就是并行处理

2.2.2 数据流上的操作

流处理引擎通常提供一组内置操作来接收、转换和输出数据流。这些操作可以用来构成Dataflow图来代表流式应用的逻辑。在本节中,我们将介绍最常见的流式操作。
操作可以是无状态的,也可以是有状态的。

无状态操作不维护任何内部状态。也就是说,一个事件的处理不依赖于任何历史事件,也不保留历史数据。无状态操作很容易并行化。有状态操作会维护他们以前接收到的事件的信息。状态会通过传入的事件来更新,并且在未来事件的处理逻辑中使用。有状态流处理应用程序在并行化和容错方面更具挑战性 2.2.2.1 数据接入和数据输出

数据接入和数据输出操作允许流处理器与外部系统通信。
数据接入是从外部系统获取原始数据并将其转换为适合处理格式的操作。实现数据接入逻辑的算子称为数据源(source)。

数据输出是以适合外部系统使用的形式产生输出的操作。实现数据输出的算子称为数据汇(sink),

2.2.2.2 转换操作

转换操作是单程操作(single-pass),每个事件都独立处理。操作一个接一个地处理事件,并对事件数据进行一些转换,产生一个新的输出流。一般来说,转换操作比较简单,不用维护内部状态

转换操作的算子可以接受多个输入并产生多个输出流。他们还可以通过将一个流分成多个流或将多个流合并成一个流来修改数据流图的结构。

2.2.2.3 滚动聚合

滚动聚合是针对每个输入事件 不断更新结果的聚合操作,比如总和、最小值和最大值。聚合操作是有状态的,并将当前状态与传入事件相结合以生成新的聚合值。

2.2.2.4 窗口操作

从一个无界事件流中创建长度有限的事件集(称为桶),并让我们对这些桶执行计算。

2.2.2.4.1 滚动窗口

滚动窗口将事件分配到长度固定的不重叠的桶中。当窗口边界通过时,所有事件都被发送到一个计算函数进行处理。

2.2.2.4.2 滑动窗口

滑动窗口将事件分配到固定大小的允许互相重叠的桶中。因此,一个事件可能属于多个桶。我们通过指定桶的长度和滑动间隔来定义滑动窗口。下图窗口长度为4,滑动间隔为3。

2.2.2.4.3 会话窗口

根据会话间隔(session gap)对事件进行分组,会话间隔定义了认为会话已关闭的非活动时间。(也就是如果用户在很长的一段时间内没有与服务器通信就认为他的会话已经关闭了)

我的图图.JPG

窗口操作与流处理中的两个主要概念密切相关:时间语义和状态管理。

流数据通常会有延迟或者乱序到达,这时如何保证窗口正确划分就很重要此外,为了避免故障,需要在生成结果之前将窗口中的数据都采取安全措施保护起来 2.3 时间语义

流式场景中时间语义和不同的时间概念。流处理引擎如何基于乱序事件产生精确结果,如何使用数据流进行历史事件处理并实现“时间旅行”。

2.3.1 流处理场景中下一分钟的含义

经典例子:爱丽丝在柏林地铁上玩手机游戏。游戏为1分钟内在线消除500个泡泡,这1分钟内通过网络不断向分析应用发送事件。爱丽丝玩了30秒后,地铁进入隧道手机断网了,爱丽丝继续玩,游戏产生的事件缓存在手机里。等地铁离开隧道,爱丽丝重新上线后,将缓存的事件发送给应用。请问上述的“1分钟”的含义是什么?需要包含爱丽丝离线的时间吗?
在线游戏这个场景展示了算子语义应该依赖事件实际发生时间 而非 应用收到事件的时间。
这个例子反映了流式应用可以使用两个不同概念的时间:处理时间(processing time)和事件时间(event time)。

2.3.2 处理时间

处理时间是机器上本地时钟的时间。处理时间窗口包括在一段时间内碰巧到达窗口的所有事件,由机器的本地时钟测量。

2.3.3 事件时间

事件时间是流中的事件实际发生的时间。事件时间通过附加到流事件的时间戳来判断。
即使事件有延迟,事件时间窗口也能准确地把事件分配到正确的窗口中,从而反映事情发生的真实情况。

无论数据流的处理速度有多快,事件到达算子的顺序是怎样的,事件时间窗口的计算将产生相同的结果。
通过依赖事件时间,即使是在无序数据的情况下,我们也可以保证结果的正确性。此外,当与可重放的流结合时,时间戳的确定性使你能够回到过去。也就是说,你可以重放一个流并分析历史数据,就像事件是实时发生的一样。

2.3.4 水位线

到目前为止,在我们关于事件时间窗口的讨论中,我们忽略了一个非常重要的方面:我们如何决定事件时间窗口的触发时机(什么时候停止收集并开始计算)?也就是说,我们要等多久才能确定我们已经收到了某个时间点之前发生的所有事件?考虑到分布式系统的不可预测性和由外部带来的各种延迟,这些问题没有绝对正确的答案。

水位线(watermark)是一种全局进度度量,它是一个时间点。它表明我们确信这个时间点之前的事件全部到达了。本质上,水位线提供了一个逻辑时钟,通知系统当前的事件时间。当操作员收到时间为T的水位线时,可以假设不会再收到时间戳小于T的事件(如果收到了,直接丢弃,不进入窗口的处理函数)。水位线对于事件时间窗口和处理无序事件的算子都是必不可少的。

水位线提供了结果可信度和延迟之间trade-off。

激进的水位线确保低延迟,但提供较低的可信度。保守的水位线带来高延迟,但同时带来较高的可信度。

流处理系统会提供某种机制来处理在水位线之后到达的事件。

2.3.5 处理时间与事件时间

既然事件时间解决了我们所有的问题,为什么我们还要去关心处理时间?
事实是,在某些情况下,处理时间确实很有用。

处理时间窗口引入了尽可能低的延迟。当你需要定期实时报告结果时,但是不太关注结果的精度时,处理时间是更合适的。最后,处理时间窗口提供了流本身的真实情况,这对于一些用例来说可能是一个理想的属性。 2.4 状态和一致性模型

状态在数据处理中无处不在。任何复杂一点的计算都需要它。为了产生结果,函数在一段时间或多个事件上累积状态(例如,计算聚集或检测模式)。有状态算子使用传入事件和内部状态来计算它们的输出并更新状态。
有状态算子实现的挑战:

状态管理:系统需要有效地管理状态,并确保它不受并发更新的影响。状态划分:并行化变得复杂,因为结果取决于状态和传入的事件。幸运的是,在许多情况下,您可以通过一个键来划分状态,并独立管理每个分区的状态。例如,比如正在处理来自一组传感器的测量流,可以用不同的分区来处理不同的传感器。状态恢复:有状态操作符带来的第三个也是最大的挑战是确保状态可以恢复,并且即使在出现故障的情况下结果也是正确的。 2.4.1 任务故障

流式作业中的算子状态非常重要,应防止出现故障。如果状态在故障期间丢失,恢复后的结果将是不正确的。流处理引擎不仅需要保证在出现任务故障时可以正常运行,还需要保证结果和算子状态的正确性。

对于输入流中的每个事件,任务执行以下步骤:

    接收事件,将其存储在本地缓冲区中;更新内部状态;产生输出记录。

在这些步骤中的任何一个都可能发生故障,系统必须清楚地定义其在每种故障场景中的如何处理。例如,一个定义完整的流式处理系统需要明确以下问题:如果任务在第一步失败,事件会丢失吗?如果在更新了内部状态后失败了,恢复后还会再更新吗?而在上面这些情况下,输出还是正确的吗?

2.4.2 结果保障

"结果保障"指的是流处理引擎内部状态的一致性,关注的是故障恢复后应用代码能够看到的状态值,与保证输出的一致性不是一回事。一旦数据从数据汇中写出,除非目标系统支持事务,否则结果的正确性将难以保证。

2.4.2.1 至多一次(AT-MOST-ONCE)

当任务发生故障时,最简单的方法就是不做任何事情来恢复丢失的状态,也不重放丢失的事件。至多一次只保证每个事件至多处理一次。换句话说,系统可以简单地丢弃事件,不做任何事情来确保结果的正确性。这种类型的保障也被称为“无保障”,因为即使是系统丢弃所有事件也可以提供这种保证。

2.4.2.2 至少一次(AT-LEAST-ONCE)

在大多数现实世界的应用程序中,人们期望事件不会丢失。这种类型的保证被称为至少一次,这意味着所有事件都将被处理,并且其中一些事件有可能被处理多次。如果应用程序的正确性仅取决于信息的完整性,重复处理可能是可以接受的。

为了确保至少一次这种结果保障,需要有一种方法来重放(replay)事件——要么从源(source),要么从某个缓冲区(buffer)。

2.4.2.3 精确一次(EXACTLY-ONCE)

精确一次是最严格的保证,也很难实现。它意味着不仅不会有事件丢失,而且每个事件只允许处理一次。从本质上来说,精确一次意味着我们的应用程序将提供完全正确的结果,就好像从未发生过失败一样。

精确一次是以至少一次为前提的,因此数据重放机制必不可少。

而且在故障恢复之后,处理引擎应该知道一个事件的更新是否已经反映在状态上。有两种实现方式:

事务性更新是实现这一结果的一种方式,但是它们会导致大量的性能开销。

相反,Flink使用轻量级检查点(快照)机制来实现一次结果保证

2.4.2.4 端到端精确一次(END-TO-END EXACTLY-ONCE)

目前为止以上的保障类型都仅限于流处理引擎自身的应用状态。实际的流处理应用中,除了流处理引擎也至少还要有一个数据来源组件和一个数据终点组件。端到端保证指的是整个数据处理流水线上的结果正确性。流水线上的每个组件都提供自己的保证,完整管道的端到端保证将由所有组件中最弱的那个组件来决定。有时候弱的保障可能会表现出强的语义,比如,你使用至少一次来求最大值或者最小值(属于幂等操作),管道的其他组件都使用精确一次,那么这个管道也是端到端精确一次的。

3. Apache Flink架构

从较高层次介绍flink的架构,描述了flink是如何解决2中的流处理的问题的。重点介绍了flink的分布式架构,在流处理应用中如何处理时间和状态,讨论容错机制。

3.1 系统架构

Flink是一个用于状态化并行数据流处理的分布式系统。Flink设置由多个进程组成,这些进程通常分布在多台机器上运行。

分布式系统需要解决的常见挑战是:

    集群中计算资源的分配和管理进程协调持久和高可用性数据存储故障恢复

Flink本身并没有实现所有这些功能。它只关注于其核心功能——分布式数据流处理,但是利用了很多现有的开源中间件和框架来实现其他非核心部分。

Flink与集群资源管理器(如Apache Mesos、YARN和Kubernetes)集成得很好,但也可以配置为作为独立集群运行。Flink不提供持久的分布式存储。相反,它利用了像HDFS这样的分布式文件系统或S3这样的对象存储。对于高可用设置中的领导选举,Flink依赖于Apache ZooKeeper。 3.1.1 搭建Flink所需的组件

Flink的搭建由四个不同的组件组成,它们一起工作来执行流应用程序。这些组件是JobManager(应用管理)、ResourceManager(资源管理)、TaskManager(工作进程)和Dispatcher(与用户交互)。由于Flink是用Java和Scala实现的,所以所有组件都运行在Java虚拟机(jvm)上。

3.1.1.1 TaskManager

TaskManager是flink的工作进程(worker process)。

通常flink搭建过程中要启动多个TaskManager。每个TaskManager有一定数量的处理槽(slot),槽的数量限制了TaskManager可以执行的任务数量。当JobManager请求槽的时候,根据ResourceManager的指示,TaskManager向JobManager提供一个或多个- 槽。在执行期间,TaskManager与运行相同应用但是不同任务的其他TaskManager 交换数据。

组件交互高层次框架图

3.1.2 应用部署

Flink应用程序可以以两种不同的模式来部署。

3.1.2.1 框架模式

在这种模式下,Flink应用程序被打包到一个JAR文件中,并由客户端提交给一个正在运行的服务。该服务可以是Flink Dispatcher、Flink JobManager或YARN的ResourceManager。

3.1.2.2 库模式

在这种模式下,Flink应用程序被绑定在一个应用程序特定的容器镜像中,比如Docker镜像。

3.1.3 任务执行

TaskManager可以同时执行多个任务。
这些任务可以

属于同一算子(数据并行)、不同算子(任务并行)的子任务甚至是来自不同应用程序的子任务(应用并行)。

TaskManager提供固定数量的处理槽来控制它能够并发执行的任务的数量。!!!一个处理槽可以执行应用程序的某个算子的一个并行任务!!!。


将多个不同算子的任务分配到同一个插槽的优点是这些任务可以在同一个进程中高效地交换数据,而不需要访问网络。
每个TaskManager是一个JVM,而每个Slot是JVM中的一个线程。TaskManager在同一个JVM进程中以多线程方式执行它的任务。线程比单独的进程更轻量,通信成本更低,但不会严格地将任务彼此隔离。因此,一个行为不正常的任务可以杀死整个TaskManager进程和运行在它上面的所有任务。

3.1.4 高可用设置 4. 设置Flink开发环境 5. DataStreamAPI 6. 基于时间和窗口的算子 7. 有状态算子和应用 8. 读写外部系统
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/771845.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号