解决方案:如何在 Amazon EMR Serverless 上执行纯 SQL 文件?

长久已来,SQL以其简单易用、开发效率高等优势一直是ETL的首选编程语言,在构建数据仓库和数据湖的过程中发挥着不可替代的作用。Hive和Spark SQL也正是立足于这一点,才在今天的大数据生态中牢牢占据着主力位置。在常规的Spark环境中,开发者可以使用spark-sql命令直接执行SQL文件,这是一项看似平平无奇实则非常重要的功能:一方面,这一方式极大地降低了Spark的使用门槛,用户只要会写SQL就可以使用Spark;另一方面,通过命令行驱动SQL文件的执行可以极大简化SQL作业的提交工作,使得作业提交本身被“代码化”,为大规模工程开发和自动化部署提供了便利。

但遗憾的是,Amazon EMR Serverless 未能针对执行SQL文件提供原生支持,用户只能在Scala/Python代码中嵌入SQL语句,这对于倚重纯SQL开发数仓或数据湖的用户来说并不友好。为此,我们专门开发了一组用于读取、解析和执行SQL文件的工具类,借助这组工具类,用户可以在 Amazon EMR Serverless 上直接执行SQL文件,本文将详细介绍一下这一方案。

1. 方案设计

鉴于在Spark编程环境中执行SQL语句的方法是:spark.sql("..."),我们可以设计一个通用的作业类,该类在启动时会根据传入的参数读取指定位置上的SQL文件,然后拆分成单条SQL并调用spark.sql("...")执行。为了让作业类更加灵活和通用,还可以引入通配符一次加载并执行多个SQL文件。此外,ETL作业经常需要根据作业调度工具生成的时间参数去执行相应的批次,这些参数同样会作用到SQL中,所以,作业类还应允许用户在SQL文件中嵌入自定义变量,并在提交作业时以参数形式为自定义变量赋值。基于这种设计思路,我们开发了一个项目,实现了上述功能,项目地址为:

项目名称项目地址
Amazon EMR Serverless Utilitieshttps://github.com/bluishglc/emr-serverless-utils

项目中的com.github.emr.serverless.SparkSqlJob类即为通用的SQL作业类,该类接受两个可选参数,分别是:

参数说明取值示例
–sql-files指定要执行的SQL文件路径,支持Java文件系统通配符,可指定多个文件一起执行s3://my-spark-sql-job/sqls/insert-into-*.sql
–sql-paramsK1=V1,K2=V2,...形式为SQL文件中定义的${K1},${K2},…形式的变量设值CUST_CITY=NEW YORK,ORD_DATE=2008-07-15

该方案具备如下特性:

① 允许单一SQL文件包含多条SQL语句
② 允许在SQL文件中使用${K1},${K2},…的形式定义变量,并在执行作业时使用K1=V1,K2=V2,...形式的参数进行变量赋值
③ 支持Java文件系统通配符,可一次执行多个SQL文件

下面,我们将分别在AWS控制台和命令行两种环境下介绍并演示如何使用该项目的工具类提交纯SQL作业。

2. 实操演示

2.1. 环境准备

在EMR Serverless上提交作业时需要准备一个“EMR Serverless Application”和一个“EMR Serverless Job Execution Role”,其中后者应具有S3和Glue Data Catalog的读写权限。Application可以在EMR Serverless控制台(EMR Studio)上通过向导轻松创建(全默认配置即可),Execution Role可以使用 《CDC一键入湖:当 Apache Hudi DeltaStreamer 遇见 Serverless Spark》 一文第5节提供的脚本快速创建。

接下来要准备提交作业所需的Jar包和SQL文件。首先在S3上创建一个存储桶,本文使用的桶取名:my-spark-sql-job(当您在自己的环境中操作时请注意替换桶名),然后从 [ 此处 ] 下载编译好的 emr-serverless-utils.jar包并上传至s3://my-spark-sql-job/jars/目录下:

请添加图片描述

在演示过程中还将使用到5个SQL示例文件,从 [ 此处 ] 下载解压后上传至s3://my-spark-sql-job/sqls/目录下:

请添加图片描述

2.2. 在控制台上提交纯SQL文件作业

2.2.1. 执行单一SQL文件

打开EMR Serverless的控制台(EMR Studio),在选定的EMR Serverless Application下提交一个如下的Job:

请添加图片描述

① Script location:设定为此前上传的Jar包路径 s3://my-spark-sql-job/jars/emr-serverless-utils-1.0.jar
② Main class:设定为 com.github.emr.serverless.SparkSqlJob
③ Script arguments:设定为 ["--sql-files","s3://my-spark-sql-job/sqls/drop-tables.sql"]

至于其他选项,无需特别设定,保持默认配置即可,对于在生产环境中部署的作业,您可以结合自身作业的需要灵活配置,例如Spark Driver/Executor的资源分配等。需要提醒的是:通过控制台创建的作业默认会启用Glue Data Catalog(即:Additional settings -> Metastore configuration -> Use AWS Glue Data Catalog 默认是勾选的),为了方便在Glue和Athena中检查SQL脚本的执行结果,建议您不要修改此项默认配置。

上述配置描述了这样一项工作:以s3://my-spark-sql-job/jars/emr-serverless-utils-1.0.jar中的com.github.emr.serverless.SparkSqlJob作为主类,提起一个Spark作业。其中["--sql-files","s3://my-spark-sql-job/sqls/drop-tables.sql"]是传递给SparkSqlJob的参数,用于告知作业所要执行的SQL文件位置。本次作业执行的SQL文件只有三条简单的DROP TABLE语句,是一个基础示例,用以展示工具类执行单一文件内多条SQL语句的能力。

2.2.2. 执行带自定义参数的SQL文件

接下来要演示的是工具类的第二项功能:执行带自定义参数的SQL文件。新建或直接复制上一个作业(在控制台上选定上一个作业,依次点击 Actions -> Clone job),然后将“Script arguments”的值设定为:

["--sql-files","s3://my-spark-sql-job/sqls/create-tables.sql","--sql-params","APP_S3_HOME=s3://my-spark-sql-job"]

如下图所示:

请添加图片描述

这次的作业设定除了使用--sql-files参数指定了SQL文件外,还通过--sql-params参数为SQL中出现的用户自定义变量进行了赋值。根据此前的介绍,APP_S3_HOME=s3://my-spark-sql-job是一个“Key=Value”字符串,其含义是将值s3://my-spark-sql-job赋予了变量APP_S3_HOME,SQL中所有出现${APP_S3_HOME}的地方都将被s3://my-spark-sql-job所替代。查看create-tables.sql文件,在建表语句的LOCATION部分可以发现自定义变量${APP_S3_HOME}

CREATE EXTERNAL TABLE IF NOT EXISTS ORDERS (... ...
)
... ...
LOCATION '${APP_S3_HOME}/data/orders/';

SparkSqlJob读取该SQL文件时,会根据键值对字符串APP_S3_HOME=s3://my-spark-sql-job将SQL文件中所有的${APP_S3_HOME}替换为s3://my-spark-sql-job,实际执行的SQL将变为:

CREATE EXTERNAL TABLE IF NOT EXISTS ORDERS (... ...
)
... ...
LOCATION 's3://my-spark-sql-job/data/orders/';

提交作业并执行完毕后,可登录Athena控制台,查看数据表是否创建成功。

2.2.3. 使用通配符执行多个文件

有时候,我们需要批量执行一个文件夹下的所有SQL文件,或者使用通配符选择性的执行部分SQL文件,SparkSqlJob使用了Java文件系统通配符来支持这类需求。下面的作业就演示了通配符的使用方法,同样是新建或直接复制上一个作业,然后将“Script arguments”的值设定为:

["--sql-files","s3://my-spark-sql-job/sqls/insert-into-*.sql"]

如下图所示:

请添加图片描述

这次作业的--sql-files参数使用了路径通配符,insert-into-*.sql将同时匹配insert-into-orders.sqlinsert-into-customers.sql两个SQL文件,它们将分别向ORDERSCUSTOMERS两张表插入多条记录。执行完毕后,可以可登录Athena控制台,查看数据表中是否有数据产生。

2.2.4. 一个复合示例

最后,我们来提交一个更有代表性的复合示例:文件通配符 + 用户自定义参数。再次新建或直接复制上一个作业,然后将“Script arguments”的值设定为:

["--sql-files","s3://my-spark-sql-job/sqls/select-*.sql","--sql-params","APP_S3_HOME=s3://my-spark-sql-job,CUST_CITY=NEW YORK,ORD_DATE=2008-07-15"]

如下图所示:

![emr-serverless-snapshot-4.jpg-150.8kB][6]

本次作业的--sql-files参数使用路径通配符select-*.sql匹配select-tables.sql文件,该文件中存在三个用户自定义变量,分别是${APP_S3_HOME}${CUST_CITY}${ORD_DATE}

CREATE EXTERNAL TABLE ORDERS_CUSTOMERS... ...LOCATION '${APP_S3_HOME}/data/orders_customers/'
AS SELECT... ...
WHEREC.CUST_CITY = '${CUST_CITY}' ANDO.ORD_DATE = CAST('${ORD_DATE}' AS DATE);

--sql-params参数为这三个自定义变量设置了取值,分别是:APP_S3_HOME=s3://my-spark-sql-jobCUST_CITY=NEW YORKORD_DATE=2008-07-15,于是上述SQL将被转化为如下内容去执行:

CREATE EXTERNAL TABLE ORDERS_CUSTOMERS... ...LOCATION 's3://my-spark-sql-job/data/orders_customers/'
AS SELECT... ...
WHEREC.CUST_CITY = 'NEW YORK' ANDO.ORD_DATE = CAST('2008-07-15' AS DATE);

至此,通过控制台提交纯SQL文件作业的所有功能演示完毕。

2.3. 通过命令行提交纯SQL文件作业

实际上,很多EMR Serverless用户并不在控制台上提交自己的作业,而是通过AWS CLI提交,这种方式方式多见于工程代码或作业调度中。所以,我们再来介绍一下如何通过命令行提交纯SQL文件作业。

本文使用命令行提交EMR Serverless作业的方式遵循了《最佳实践:如何优雅地提交一个 Amazon EMR Serverless 作业?》一文给出的最佳实践。首先,登录一个安装了AWS CLI并配置有用户凭证的Linux环境(建议使用Amazon Linux2),先使用命令sudo yum -y install jq安装操作json文件的命令行工具:jq(后续脚本会使用到它),然后完成如下前期准备工作:

① 创建或选择一个作业专属工作目录和S3存储桶
② 创建或选择一个EMR Serverless Execution Role
③ 创建或选择一个EMR Serverless Application

接下来将所有环境相关变量悉数导出(请根据您的AWS账号和本地环境替换命令行中的相应值):

export APP_NAME='change-to-your-app-name'
export APP_S3_HOME='change-to-your-app-s3-home'
export APP_LOCAL_HOME='change-to-your-app-local-home'
export EMR_SERVERLESS_APP_ID='change-to-your-application-id'
export EMR_SERVERLESS_EXECUTION_ROLE_ARN='change-to-your-execution-role-arn'

以下是一份示例:

export APP_NAME='my-spark-sql-job'
export APP_S3_HOME='s3://my-spark-sql-job'
export APP_LOCAL_HOME='/home/ec2-user/my-spark-sql-job'
export EMR_SERVERLESS_APP_ID='00fbfel40ee59k09'
export EMR_SERVERLESS_EXECUTION_ROLE_ARN='arn:aws:iam::123456789000:role/EMR_SERVERLESS_ADMIN'

《最佳实践:如何优雅地提交一个 Amazon EMR Serverless 作业?》一文提供了多个操作Job的通用脚本,都非常实用,本文也会直接复用这些脚本,但是由于我们需要多次提交且每次的参数又有所不同,为了便于使用和简化行文,我们将原文中的部分脚本封装为一个Shell函数,取名为submit-spark-sql-job

submit-spark-sql-job() {sqlFiles="$1"sqlParams="$2"cat << EOF > $APP_LOCAL_HOME/start-job-run.json
{"name":"my-spark-sql-job","applicationId":"$EMR_SERVERLESS_APP_ID","executionRoleArn":"$EMR_SERVERLESS_EXECUTION_ROLE_ARN","jobDriver":{"sparkSubmit":{"entryPoint":"$APP_S3_HOME/jars/emr-serverless-utils-1.0.jar","entryPointArguments":[$([[ -n "$sqlFiles" ]] && echo "\"--sql-files\", \"$sqlFiles\"")$([[ -n "$sqlParams" ]] && echo ",\"--sql-params\", \"$sqlParams\"")],"sparkSubmitParameters":"--class com.github.emr.serverless.SparkSqlJob --conf spark.hadoop.hive.metastore.client.factory.class=com.amazonaws.glue.catalog.metastore.AWSGlueDataCatalogHiveClientFactory"}},"configurationOverrides":{"monitoringConfiguration":{"s3MonitoringConfiguration":{"logUri":"$APP_S3_HOME/logs"}}}
}
EOFjq . $APP_LOCAL_HOME/start-job-run.jsonexport EMR_SERVERLESS_JOB_RUN_ID=$(aws emr-serverless start-job-run \--no-paginate --no-cli-pager --output text \--name my-spark-sql-job \--application-id $EMR_SERVERLESS_APP_ID \--execution-role-arn $EMR_SERVERLESS_EXECUTION_ROLE_ARN \--execution-timeout-minutes 0 \--cli-input-json file://$APP_LOCAL_HOME/start-job-run.json \--query jobRunId)now=$(date +%s)secwhile true; dojobStatus=$(aws emr-serverless get-job-run \--no-paginate --no-cli-pager --output text \--application-id $EMR_SERVERLESS_APP_ID \--job-run-id $EMR_SERVERLESS_JOB_RUN_ID \--query jobRun.state)if [ "$jobStatus" = "PENDING" ] || [ "$jobStatus" = "SCHEDULED" ] || [ "$jobStatus" = "RUNNING" ]; thenfor i in {0..5}; doecho -ne "\E[33;5m>>> The job [ $EMR_SERVERLESS_JOB_RUN_ID ] state is [ $jobStatus ], duration [ $(date -u --date now-$now +%H:%M:%S) ] ....\r\E[0m"sleep 1doneelseprintf "The job [ $EMR_SERVERLESS_JOB_RUN_ID ] is [ $jobStatus ]%50s\n\n"breakfidone
}

该函数接受两个位置参数:

① 第一位置上的参数用于指定SQL文件路径,其值会传递给SparkSqlJob--sql-files
② 第二位置上的参数用于指定SQL文件中的用户自定义变量,其值会传递给SparkSqlJob--sql-params

函数中使用的Jar包和SQL文件与《2.1. 环境准备》一节准备的Jar包和SQL文件一致,所以使用脚本提交作业前同样需要完成2.1节的环境准备工作。接下来,我们就使用该函数完成与2.2节一样的操作。

2.3.1. 执行单一SQL文件

本节操作与2.2.1节完全一致,只是改用了命令行方式实现,命令如下:

submit-spark-sql-job "$APP_S3_HOME/sqls/drop-tables.sql"

2.3.2. 执行带自定义参数的SQL文件

本节操作与2.2.2节完全一致,只是改用了命令行方式实现,命令如下:

submit-spark-sql-job "$APP_S3_HOME/sqls/create-tables.sql" "APP_S3_HOME=$APP_S3_HOME"

2.3.3. 使用通配符执行多个文件

本节操作与2.2.3节完全一致,只是改用了命令行方式实现,命令如下:

submit-spark-sql-job "$APP_S3_HOME/sqls/insert-into-*.sql"

2.3.4. 一个复合示例

本节操作与2.2.4节完全一致,只是改用了命令行方式实现,命令如下:

submit-spark-sql-job "$APP_S3_HOME/sqls/select-tables.sql" "APP_S3_HOME=$APP_S3_HOME,CUST_CITY=NEW YORK,ORD_DATE=2008-07-15"

3. 在源代码中调用工具类

尽管在Spark编程环境中可以使用spark.sql(...)形式直接执行SQL语句,但是,从前文示例中可以看出 emr-serverless-utils 提供的SQL文件执行能力更便捷也更强大一些,所以,最后我们简单介绍一下如何在源代码中调用相关的工具类获得上述SQL文件的处理能力。具体做法非常简单,你只需要:

① 将emr-serverless-utils-1.0.jar加载到你的类路径中
② 声明隐式类型转换
③ 在spark上直接调用execSqlFile()


# 初始化SparkSession及其他操作
...# 声明隐式类型转换
import com.github.emr.serverless.SparkSqlSupport._# 在spark上直接调用execSqlFile()
spark.execSqlFile("s3://YOUR/XXX.sql")# 在spark上直接调用execSqlFile()
spark.execSqlFile("s3://YOUR/XXX.sql", "K1=V1,K2=V2,...")# 其他操作
...

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/40632.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

国企的大数据岗位方向的分析

现如今大数据已无所不在&#xff0c;并且正被越来越广泛的被应用到历史、政治、科学、经济、商业甚至渗透到我们生活的方方面面中&#xff0c;获取的渠道也越来越便利。 今天我们就来聊一聊“大屏应用”&#xff0c;说到大屏就一定要聊到数据可视化&#xff0c;现如今&#xf…

【Git】(三)回退版本

1、git reset命令 1.1 回退至上一个版本 git reset --hard HEAD^ 1.2 将本地的状态回退到和远程的一样 git reset --hard origin/master 注意&#xff1a;谨慎使用 –-hard 参数&#xff0c;它会删除回退点之前的所有信息。HEAD 说明&#xff1a;HEAD 表示当前版本HEAD^ 上…

服务链路追踪

一、服务链路追踪导论 1.背景 对于一个大型的几十个、几百个微服务构成的微服务架构系统&#xff0c;通常会遇到下面一些问题&#xff0c;比如&#xff1a; 如何串联整个调用链路&#xff0c;快速定位问题&#xff1f;如何理清各个微服务之间的依赖关系&#xff1f;如何进行…

pycorrector一键式文本纠错工具,整合了BERT、MacBERT、ELECTRA、ERNIE等多种模型,让您立即享受纠错的便利和效果

pycorrector&#xff1a;一键式文本纠错工具&#xff0c;整合了Kenlm、ConvSeq2Seq、BERT、MacBERT、ELECTRA、ERNIE、Transformer、T5等多种模型&#xff0c;让您立即享受纠错的便利和效果 pycorrector: 中文文本纠错工具。支持中文音似、形似、语法错误纠正&#xff0c;pytho…

Python OpenGL环境配置

1.Python的安装请参照 Anconda安装_安装anconda_lwb-nju的博客-CSDN博客anconda安装教程_安装ancondahttps://blog.csdn.net/lwbCUMT/article/details/125322193?spm1001.2014.3001.5501 Anconda换源虚拟环境创建及使用&#xff08;界面操作&#xff09;_anconda huanyuan_l…

彻底卸载Android Studio

永恒的爱是永远恪守最初的诺言。 在安装Android Studio会有很多问题导致无法正常运行&#xff0c;多次下载AS多次错误后了解到&#xff0c;删除以下四个文件才能彻底卸载Android Studio。 第一个文件&#xff1a;.gradle 路径&#xff1a;C:\Users\yao&#xff08;这里yao是本…

解密人工智能:线性回归 | 逻辑回归 | SVM

文章目录 1、机器学习算法简介1.1 机器学习算法包含的两个步骤1.2 机器学习算法的分类 2、线性回归算法2.1 线性回归的假设是什么&#xff1f;2.2 如何确定线性回归模型的拟合优度&#xff1f;2.3 如何处理线性回归中的异常值&#xff1f; 3、逻辑回归算法3.1 什么是逻辑函数?…

火山引擎联合Forrester发布《中国云原生安全市场现状及趋势白皮书》,赋能企业构建云原生安全体系

国际权威研究咨询公司Forrester 预测&#xff0c;2023年全球超过40%的企业将会采用云原生优先战略。然而&#xff0c;云原生在改变企业上云及构建新一代基础设施的同时&#xff0c;也带来了一系列的新问题&#xff0c;针对涵盖云原生应用、容器、镜像、编排系统平台以及基础设施…

用栈解决有效的括号匹配问题

//用数组实现栈 typedef char DataType; typedef struct stack {DataType* a;//动态数组int top;//栈顶int capacity; //容量 }ST;void STInit(ST*pst);//初始化void STDestroy(ST* pst);//销毁所有空间void STPush(ST* pst, DataType x);//插入数据到栈中void STPop(ST* pst);…

【业务功能篇65】maven加速 配置settings.xml文件 镜像

maven加速 添加阿里镜像仓 <?xml version"1.0" encoding"UTF-8"?><!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additi…

Kubernetes 调度约束(亲和性、污点、容忍)

目录 一、Pod启动典型创建过程 二、调度流程 三、指定调度节点 1.使用nodeName字段指定调度节点 2.使用nodeSelector指定调度节点 2.1给对应的node节点添加标签 2.2修改为nodeSelector调度方式 3.通过亲和性来指定调度节点 3.1节点亲和性 3.2Pod亲和性与反亲和性 3.2…

Vue CLI创建Vue项目详细步骤

&#x1f680; 一、安装Node环境&#xff08;建议使用LTS版本&#xff09; 在开始之前&#xff0c;请确保您已经安装了Node.js环境。您可以从Node.js官方网站下载LTS版本&#xff0c;以确保稳定性和兼容性。 中文官网下载 确认已安装 Node.js。可以在终端中运行 node -v 命令…

基于STM32设计的中药分装系统

一、设计需求 基于STM32设计的中药分装系统 【1】项目背景 中药文化是我国文化瑰宝之一,它具有疗效好、副作用小的优点,而且相对于西药,全天然的中药还具有标本兼治的特点,不仅可以用来治病,更可以对患者身体进行调理,所以格外受到当今一直追求生活质量的人们的追捧&quo…

在Spring Boot和Vue中实现请求过滤器以验证请求头中的Token

在Spring Boot应用程序中创建一个过滤器类&#xff0c;用于处理请求&#xff1a; Component public class AuthenticationFilter implements Filter {Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException,…

FastAPI入门

目录 FastAPI FastAPI 是什么 为什么要用 FastAPI FastAPI 入门 安装 用 FastAPI 写个接口 调试接口 创建快捷请求 保存为快捷请求 发送请求 总结 FastAPI FastAPI 是什么 什么是 FastAPI 呢&#xff1f; FastAPI 是 Python 的一个框架&#xff0c;如果要类比的话…

ros编译catkin_make/catkin build 报错“string no output variable specified”

文章目录 问题描述问题分析问题描述 CMake Error at /opt/ros/melodic/share/catkin/cmake/platform/lsb.cmake:8 (string):string no output variable specified Call Stack (most recent call first):/opt/ros/melodic/share/catkin/cmake/all.cmake:164 (include)/opt/ros/…

Django的简介安装与配置及两大设计模式

一.Djang的介绍 1.Django是什么 Django 是使用 Python 语言开发的一款免费而且开源的 Web 应用框架。 由于 Python 语言的跨平台性&#xff0c;所以 Django 同样支持 Windows、Linux 和 Mac 系统。 在 Python 语言炽手可热的当下&#xff0c;Django 也迅速的崛起&#xff0c;在…

javaScript:一分钟看懂数组排序以及冒泡排序(重点)

目录 一.前言 二.数组排序 sort&#xff08;&#xff09; 字符串大小的比较原则 字符编码排序规则 如果使用sort从小到大排列 &#xff08;重点&#xff09; 函数的返回值 规则&#xff1a; 代码 案例 数组打乱 sort相关代码 三.冒泡排序&#xff08;重点&#xff…

【RocketMQ】NameServer总结

NameServer是一个注册中心&#xff0c;提供服务注册和服务发现的功能。NameServer可以集群部署&#xff0c;集群中每个节点都是对等的关系&#xff08;没有像ZooKeeper那样在集群中选举出一个Master节点&#xff09;&#xff0c;节点之间互不通信。 服务注册 Broker启动的时候会…

【通俗易懂】如何使用GitHub上传文件,如何用git在github上传文件

目录 创建 GitHub 仓库 使用 Git 进行操作 步骤 1&#xff1a;初始化本地仓库 步骤 2&#xff1a;切换默认分支 步骤 3&#xff1a;连接到远程仓库 步骤 4&#xff1a;获取远程更改 步骤 5&#xff1a;添加文件到暂存区 步骤 6&#xff1a;提交更改 步骤 7&#xff1a…