论Apache Spark中大数据处理的某些方面——有用的设计模式

78 阅读7分钟

在这篇文章中,我展示了如何利用这些知识,以可维护和可升级的方式构建Spark应用程序,同时避免出现 "任务不可序列化 "的异常。

当我参加一个大数据项目时,我需要对Spark应用程序进行编程,以便将数据从关系型数据库和分布式数据库(如Apache Hive)中移动和转换。我发现这样的应用有很多隐患,所以所有 "代码难读"、"方法太大,无法装入一个屏幕 "等问题都需要避免,以便我们关注更深层次的问题。另外,Spark的工作也是类似的:数据从单个或多个数据库加载,得到转换,然后保存到单个或多个数据库。因此,尝试使用GoF模式为Spark应用程序编程似乎是合理的。

这篇文章的组织结构如下。

  • 第1节描述了我的系统。
  • 第2节讨论了哪些GoF模式在这种情况下可能是有用的。
  • 在第3节中,我提出了一个解决所述问题的方案。

让我们开始吧。

1.问题

一个典型的Spark应用如下(图1)。有多个数据源(数据库、文件等)。DBi,i=1:N,和一个Java Spark服务器。我们需要从数据源加载数据,转换数据,并将转换后的数据保存到不同或相同的数据源。每个应用程序可能有不同的输入和输出数据源的组合。 image.png 图1:一个典型的Spark应用程序

为了使我的代码简洁并能开箱即用,我简化了这个问题:让有2个JSON文件作为数据源(图2)。然后,数据进行转换:转换是一个微不足道的单元转换或两个数据集的联合。最后,有两个保存器。一个返回数据集模式,另一个显示数据集内容。我把Imtiaz Ahmad的 "Master Apache Spark - Hands On!"课程中的代码作为入门代码。尽管它很简单,但该代码足以说明如何在Spark应用程序中使用基本的设计模式。 image.png 图2:我们的简化系统

让我们研究一下2个Spark应用程序(图3)。第一个是一个简单的加载-转换-保存作业(A)。第二个是一个更复杂的案例,两个数据集被合并成一个,然后被保存(B)。我们如何以最小的代码重复对这些Spark应用程序进行编程? image.png 图3:一个基本的Spark应用(A);一个有两个源、一个保存器和一个联合转换的Spark应用(B)

2.模式

为了解决这个问题,让我们从《设计模式》中选择设计模式。可重用的面向对象软件的要素 "中的设计模式。另外,我们需要我们的解决方案是可序列化的。这里可能会想到以下GoF模式。

  1. 建造者:该模式由一个产品、一个生成器和一个主管组成。主管指导建设者制作产品。该模式对于构建不同的产品是有效的;这就是为什么产品分两步构建。第一步是配置生成器。在第二步中,主任指导生成器构建一个产品。然而,这种模式在我们的案例中不是特别有用,因为Spark应用程序有一个共同的结构。
  2. 模板方法:该模式包括一个AbstractClass(具有原始操作,加上一个骨架方法来连锁这些操作)和一个ConcreteClass(具有原始操作的实现)。在我们的案例中,这对于定义一个加载-转换-保存的应用程序的骨架当然很有用。
  3. 工厂:该模式由一个产品(接口)、具体产品、一个创造者(接口)和具体创造者组成。一个ConcreteCreator有条件地创建一个ConcreteProduct。这看起来对我们很有用。
  4. 抽象工厂:该模式由一个AbstractFactory(接口或超类)、ConcreteFactories、一个AbstractProduct(接口或超类)、ConcreteProducts和一个Client组成。客户端配置ConcreteFactories(共享同一个AbstractFactory父类),以建立ConcreteProducts(共享同一个AbstractProduct父类)。根据D.Banas的演讲 "抽象工厂设计模式",这种模式如果与策略模式相结合是最有用的。这正是我们所需要的!我们可以同时利用如何运行作业的相似性和如何构建这些作业的相似性。
  5. 策略:该模式由一个策略接口和实现该接口的具体策略组成。这使得用户可以轻松地切换某个功能的具体实现。在我们的案例中,对不同的加载器、转换器和保存器进行编程是非常有用的。
  6. 装饰器:该模式由一个组件(接口),ConcreteComponents,一个装饰器(抽象类),ConcreteDecorators组成。ConcreteDecorators向ConcreteComponents添加功能。在我们的案例中,它对于为我们的方法添加额外的功能也很有用。例如,我们可以在将数据写入关系型数据库表之前截断它。让我们结合这些模式来得到一个解决方案。

3.解决方案

我对这个问题提出了以下解决方案(图4)。这个解决方案是基于抽象工厂模式的。SimpleJobFactoryBasicJobPlan 分别是一个ConcreteFactory和一个ConcreteProduct。IFactoryIJobPlan 分别是一个AbstractFactory和一个AbstractProduct接口。 image.png 图4:用抽象工厂、模板方法和策略模式为我们的Spark作业编程

这些类的实现细节如图5所示。请注意,我们将一个可序列化的SparkSession 对象保存在一个单独的SparkSessionContainer 的实例中。其他所有使用SparkSession的类都扩展了SparkSessionContainer。 image.png 图5: 如何在我们的案例中实现具体的工作和具体的工厂

还注意到SimpleJobFactory和BasicJobPlan已经包含了构建作业(SimpleJobFactory.make() )和运行作业(BasicJobPlan.run() )的骨架方法。这些类可以作为模板方法模式的抽象类。更详细的作业工厂和作业计划扩展了这些类。

事实上,为了创建一个作业(B)计划,我们需要在作业(A)计划中添加第二个加载器、第二个保存器和另一个转化器(union)。同时,为了创建一个工作(B)的具体工厂,我们需要 "装饰 "SimpleJobFactory.make() 方法,以添加第二个装载器,添加联合转化器,并用一个空的保存器替换基本的保存器,以通过新的(第二个)保存器保存数据。我们可以在每一步上改变作业计划的保存器、转换器和装载器,因为我们可以在每一步上访问ConcreteFactory.make() 方法中的作业。如何做到这一点,如图5所示。

注意,我们的SimpleJobFactory依赖于一个I extends IJobPlan 通用接口,以利用SimpleJobFactory的方法和字段来实现更复杂的作业计划。这些更复杂的作业计划接口依赖于加载器、转换器和保存器接口。装载器、转换器和保存器实现了一个策略模式,所以我们更容易切换不同的实现。

为了避免出现 "任务不可序列化 "的异常,我们应该只把可序列化的对象作为类字段。不可序列化的对象应该被创建为方法中的局部变量。详见我之前的文章。

为了运行这些作业,我们需要创建一个Spark驱动(见第一部分,回顾Spark术语),在这里我们创建一个SparkSession实例,设置数据源的配置,创建Concrete Factories,制作Concrete Jobs,并运行作业。

代码

public class JsonDriver {
	public static void main(String[] args) {
		  final SparkSession spark = SparkSession.builder()
			        .appName("JSON Lines to Dataframe")
			        .master("local")
			        .getOrCreate();
		  final String path = "src/main/resources/multiline.json";
		  final String path2 = "src/main/resources/multiline2.json";
		  Map<String, String> options = new HashMap<>();
		  options.put("path", path);
		  options.put("path2", path2);
		  options.put("jobName", "Job_1");
		  
		  IFactory factory = new TwoJobFactory(spark, options);
		  factory.make();
		  factory.getJob().run();	  

	}

在这里,Spark驱动作为一个客户端来配置抽象工厂模式中的具体工厂。整个应用有一个单一的SparkSession。该会话被提供给SparkSessionContainer,以便于容器的子节点访问。该应用在本地模式下运行,并打印出两个数据集的联合。

总结

在这篇文章中,我演示了如何使用GoF抽象工厂、模板方法和策略模式来使Spark应用程序的代码模块化、可读性、可升级性,以及可序列化。我希望这对你的Spark项目会有帮助。