亚马逊AWS官方博客

EMR 任务访问 S3 出现 credential 获取失败问题的分析与解决方案

本文重点介绍 Amazon EMR(以前称为 Amazon Elastic MapReduce)访问 Amazon Simple Storage Service(Amazon S3),在 Amazon EMR 极高负载情况下,访问 S3 可能会出现的 credential 相关错误以及解决办法。本文所谓的极高负载,在客户的真实场景下,EC2 的内存和 CPU 使用率接近 100%。为了让读者更好地理解此场景,本文还介绍了 EMR 访问 S3 的常用方法以及推荐安全配置。

AWS EMR 是 AWS 的一个托管集群平台,旨在简化和加速大规模数据处理任务,例如大数据分析、数据转换、数据调度和机器学习等。它基于开源的 Apache Hadoop、Apache Spark 和 Presto 等大数据处理框架构建,并提供了一系列管理工具和服务,使用户能够轻松地处理、分析和可视化大规模数据集。它支持弹性扩展、多种大数据处理框架,提供了一系列管理工具和集成,使用户能够轻松配置、监控和管理集群,还提供了多层次的安全和可靠性措施,使用户能够快速构建和部署大数据解决方案。

我们知道大数据应用起初普遍构建在 HDFS 之上,而 Amazon S3 是近年来存储数据的一种非常流行的方式,于是 Amazon EMR 与 S3 之间方便、高效和可靠的数据交互,就成为了 AWS 大数据用户的一个迫切需求。 EMRFS 就是这样一个在 EMR 和 S3 之间提供高效数据读写的重要组件。它提供了数据一致性、性能优化、安全性和兼容性等功能,使得 EMR 集群可以方便地与 S3 进行数据交互。Hadoop 社区也提供了开源的 S3A,可以使 EMR 与 S3 之间的数据交互更加方便、高效和可靠。它提供了兼容性、弹性、可扩展性、数据一致性、性能优化和安全性等优势,使得 EMR 能够轻松地与 S3 进行大数据处理和存储。从架构层面来说,EMRFS 和 S3A 是我们在使用 EMR 和 S3 这种架构时必然会面临的两种不同的技术选型,所以本文作出简要介绍,但选型策略不是本文讨论的重点。事实上,无论选择何种方式,其都可以归纳为运行在 EC2 之上的应用程序,因此,在安全配置方面都是相通的,以下将重点探讨此部分的内容。

AWS 是一个极其重视安全的云平台,我们认为在进行任何类型的身份验证时,都最好使用临时凭证而不是长期凭证,以降低或消除诸如凭证被无意泄露、共享或被盗之类的风险。EMR 集群访问 S3 的这个场景,可以归类为应用程序访问 AWS 服务的场景,而应用程序又是运行在 AWS 的 EC2 之上,所以推荐的方式是让 EMR 通过 EC2 IAM role 访问 S3。

实现的主要过程如下:

  1. 创建 IAM 角色:首先,您需要在 AWS Identity and Access Management(IAM)控制台上创建一个 IAM 角色。该角色将定义 EMR 集群的访问权限。
  2. 关联 IAM 角色:在启动 EMR 集群时,您可以指定一个 IAM 角色来关联到集群的 EC2 实例。这可以通过在 EMR 控制台、AWS CLI 或者 AWS SDK 中进行配置。
  3. EC2 实例获取凭证:一旦集群中的 EC2 实例与 IAM 角色关联,它们将自动获取与该角色关联的访问凭证。这些凭证是通过 EC2 元数据服务 IMDS(EC2 Instance Metadata Service)提供的。
  4. 访问 S3:EMR 集群中的应用程序和服务可以使用 EMRFS 或 S3A 文件系统来访问 S3。当它们尝试访问 S3 时,它们会自动使用 EC2 实例上的凭证进行身份验证,并获得与 IAM 角色关联的权限。

工作原理如下:

  1. EC2 元数据服务:EC2 实例上运行着一个元数据服务,称为 EC2 Intance Metadata Service。该服务允许实例获取与其关联的 IAM 角色的访问凭证。
  2. 自动角色凭证获取:当 EMR 集群中的应用程序或服务需要访问 S3 时,它们会通过 EMRFS 或 S3A 文件系统发起请求。文件系统会自动检测到关联的 IAM 角色,并向 EC2 元数据服务 IMDS 请求获取与该角色关联的凭证。
  3. 动态凭证交换:EC2 元数据服务 IMDS 会验证请求的来源和权限,并返回与 IAM 角色关联的访问凭证。这些凭证将用于对 S3 进行身份验证和授权,以执行所需的操作。

通过这种方式,EMR 集群中的应用程序和服务可以安全地访问 S3,无需在集群中明确配置和管理访问凭证。EC2 IAM role 提供了一种无缝的方式,将访问权限传递给集群中的 EC2 实例,实现了安全的 S3 访问。

需要注意的是,IAM 角色在创建和配置时需要遵循最佳实践,仅授予集群所需的最小权限,以减少潜在的安全风险(参考:https://docs.aws.amazon.com/zh_cn/emr/latest/ManagementGuide/emr-iam-role.html)。

IMDSv2 在过程中扮演着非常重要的角色,也和我们后面要提到的问题有着密切关系,所以这里我们着重介绍一下 IMDSv2 的工作原理:

  1. 安全性提升:IMDSv2 是 IMDSv1 的升级版本,旨在提供更强的安全性。IMDSv1 是一个 HTTP 服务,通过 EC2 实例中的特定端口(169.254.169.254)提供元数据访问。然而,IMDSv1 存在一些安全风险,如跨站脚本攻击(XSS)和请求伪造。IMDSv2 通过引入认证和授权机制来解决这些问题,增强了实例元数据的安全性。
  2. 身份验证和授权:IMDSv2 使用 EC2 实例上的 EC2 实例角色的凭证来进行身份验证和授权。在 IMDSv2 中,每个请求都需要在请求头中包含一个有效的 X-aws-ec2-metadata-token,并且该令牌是通过 EC2 实例角色的凭证签名生成的。仅当请求中的令牌和签名验证通过时,IMDSv2 才会返回相应的元数据。
  3. 请求处理:当 EC2 实例发起 IMDSv2 请求时,请求将被发送到实例上的 169.254.169.254 IP 地址。IMDSv2 将检查请求头中的令牌和签名,并验证其有效性。如果验证通过,IMDSv2 将查找请求中指定的元数据,并将其返回给 EC2 实例。
  4. 令牌过期和续订:IMDSv2 的令牌具有有效期。如果令牌过期,EC2 实例需要使用原始的 EC2 实例角色凭证来获取新的令牌。EC2 实例可以通过将凭证发送到 IMDSv2 的令牌续订端点来续订令牌,以确保持续访问实例元数据。

通过引入身份验证、授权和令牌机制,IMDSv2 提供了更加安全和可控的 EC2 实例元数据访问。它帮助防止潜在的安全漏洞,并确保仅经过授权的实体可以访问实例元数据。

至此,我们介绍完了如何实现 EMR 与 S3 之间方便、高效和安全可靠的数据交互,接下来,我们通过一个客户案例来看看在实际的大规模业务场景下,可能会出现的 credential 相关问题以及解决办法。

客户案例

客户是一个数据量非常庞大的互联网客户,活跃数据量在 PB 级别,使用 AWS EMR 来进行离线数据处理,使用 S3A 来访问 S3 中的数据,同时也遵循 AWS 的建议,EMR 通过 EC2 IAM role 访问 S3。所有 EMR 集群的日常 CPU 利用率稳定在 75%以上,日峰值接近 100%,峰值发生在夜间跑批的时候,需要将大量的数据写入到 S3 中。所以从 EMR 向 S3 做大规模的数据写入是此客户环境下的一个典型场景,并且这些数据涉及很多关键业务信息,因此,在上线服务前,我们建议客户严格按照 AWS 的安全最佳实践来进行访问方式的设计和性能压测中。在性能压测的过程中,我们发现了小概率的访问失败问题,这类问题一般发生在任务运行后的一小时左右。

离线数据处理任务允许期间,当资源(CPU,内存,网卡)使率较高时偶尔会出现 S3A 异常信息:

org.apache.hadoop.fs.s3a.auth.NoAuthWithAWSException: No AWS Credentials provided by SimpleAWSCredentialsProvider EnvironmentVariableCredentialsProvider InstanceProfileCredentialsProvider : com.amazonaws.SdkClientException: Unable to load credentials from service endpoint
at org.apache.hadoop.fs.s3a.S3AUtils.translateException(S3AUtils.java:187)
at org.apache.hadoop.fs.s3a.Invoker.once(Invoker.java:111)
at org.apache.hadoop.fs.s3a.Invoker.lambda$retry$3(Invoker.java:265)
…….
Caused by: com.amazonaws.SdkClientException: Unable to load credentials from service endpoint
at com.amazonaws.auth.EC2CredentialsFetcher.handleError(EC2CredentialsFetcher.java:183)
at com.amazonaws.auth.EC2CredentialsFetcher.fetchCredentials(EC2CredentialsFetcher.java:162)
at com.amazonaws.auth.EC2CredentialsFetcher.getCredentials(EC2CredentialsFetcher.java:82)
at com.amazonaws.auth.InstanceProfileCredentialsProvider.getCredentials(InstanceProfileCredentialsProvider.java:172)
at org.apache.hadoop.fs.s3a.AWSCredentialProviderList.getCredentials(AWSCredentialProviderList.java:137)

从异常信息我们可以看出,发生问题的原因在于 Unable to load credentials from service endpoint,导致任务运行失败。我们前面介绍过,当应用程序通过 EC2 IAM Role 来访问其他 AWS 服务时,需要到 IMDS(Intance metadata server)去获取 token,并使用 token 来和 AWS 其他服务通信,这部分是被封装到 AWS SDK 中,应用无感知。IMDS 负责生成并提供 token,token 的有效期一般为 6 小时。通过如下方式来手工查看 IMDS,  以 IMDS V2 为例:

$ TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` && curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/iam/security-credentials/MyInstanceRole
< HTTP/1.1 200 OK
< X-Aws-Ec2-Metadata-Token-Ttl-Seconds: 21600
< Content-Type: text/plain
< Accept-Ranges: none
< Last-Modified: Fri, 26 Jan 2024 00:57:20 GMT
< Content-Length: 1166
< Date: Fri, 26 Jan 2024 01:31:51 GMT  //发起请求时间
< Server: EC2ws
< Connection: close
<
{
  "Code" : "Success",
  "LastUpdated" : "2024-01-26T00:56:48Z",  //token 有效期开始时间
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ABCDEFTLI5CPBOFAH73",
  "SecretAccessKey" : "z1iQaGLLJuqIxgEyiQ9y06Om0nOtBqRrxGX4shy4",
  "Token" : "FwoDYXdzEBEaDAegS77iK14K+QcyvSKtBCYkUYHCWbRnsy4x9j2v3UqLjPCn1gcGhG+8dD7OKZtBg5rUUJsrBq9wHy8oPqDfQ7GVv/ntRrYp+YvoWgua3avsBG4pT5oTe8ubcSENzVUgq0p4TgguaXdpNv128QiTi14LR6eImTbi9z6ao+nud7l8L03G2ZVtIX1GbZWf+z+MRCWTEx7HBuUEoZQHYVRn3ZslXLtEc4nW6xSwV+IYfioZoDHtO5olY2UOkWmGi1dpioBBEr1gDf53Ll/M/h8np0wimAik0Bg4cwkaNniyAMS3r8npG3tb9Qp568+6UB5CQaAOP3gS/z4zGTqbwyCtAcBnPzwOIoUrclKhjiI5bzV8jG9Z/oBhYtUvXm+jlPJEfBC4TZ66weI6OvgwukPoh0O5E1leiJAUPM4AkaMCzU67xItV/UE7emiA/4Di4B7d1/IPmFlia2XQ+4NgHdJQ+gq62Fy9WVFBxZcwre2Q07HIOoNf3ov2H6vU8wJCrz7lUOR5Nw0rVZ2nC7ineWVbcoGxs7Q2UhnBVQ8iFqXhvPf518T/Nx2jkufp09UBHiquFKg+47jTsqEbCuD7U4PUuWo9WWzxtQJ/f2966VzK135FEhDVWRgKcWkG5lI9+Zsx5ph5opXdda6Rpa5qRUfagiRqn9ElTF9o37LWW5splpoQ28Xpwz2rMIxz3vWa8zMqOnqMve9CbzYiSbwYHTZ0DcdYhfEnmXffSMPjKUwZruz578rLQxX+LiHmj1iCKPCGzK0GMl3cJ72X6I5EHbs2KFCQUAeW2SnlNXJKV8RbAytcgno9lnS692Zc1SEg5EIxS47BanmCLfOqbk25eozE3HDPGpZ7+oCIbX0d2Qzjzuu5XMj19ByhnfU854K7mk2GLMc=",
  "Expiration" : "2024-01-26T07:18:16Z"  //token 有效期结束时间

经过多次测试发现:

  1. IMDS 并不是每次请求都会返回新的 token,同一段时间请求返回的是同一个 token。
  2. IMDS 会定时在后台刷新 token,确保返回的 token 的剩余有效期都在 4 小时以上。

S3A 获取 Intance Role
S3A 通过 AWS SDK 来获取 Instance Credential,以客户使用的 AWS SDK 1.11.563,分析其中的获取 credential 的代码(com.amazonaws.auth.EC2CredentialsFecther):

com.amazonaws.auth.AWSCredentialsProvider
com.amazonaws.auth.InstanceProfileCredentialsProvider
InstanceProfileCredentialsProvider
  private synchronized void fetchCredentials() {
    if (!needsToLoadCredentials())
      return; 
    try {
      this.lastInstanceProfileCheck = new Date();
      String credentialsResponse = EC2CredentialsUtils.getInstance().readResource(this.credentialsEndpointProvider
          .getCredentialsEndpoint(), this.credentialsEndpointProvider
          .getRetryPolicy(), this.credentialsEndpointProvider
          .getHeaders());
      JsonNode node = (JsonNode)Jackson.fromSensitiveJsonString(credentialsResponse, JsonNode.class);
      JsonNode accessKey = node.get("AccessKeyId");
      JsonNode secretKey = node.get("SecretAccessKey");
      JsonNode token = node.get("Token");
      if (null == accessKey || null == secretKey)
        throw new SdkClientException("Unable to load credentials."); 
      if (null != token) {
        this
          .credentials = new BasicSessionCredentials(accessKey.asText(), secretKey.asText(), token.asText());
      } else {
        this
          .credentials = new BasicAWSCredentials(accessKey.asText(), secretKey.asText());
      } 
      JsonNode expirationJsonNode = node.get("Expiration");
      if (null != expirationJsonNode) {
        String expiration = expirationJsonNode.asText();
        expiration = expiration.replaceAll("\\+0000$", "Z");
        try {
          this.credentialsExpiration = DateUtils.parseISO8601Date(expiration);
        } catch (Exception ex) {
          handleError("Unable to parse credentials expiration date from Amazon EC2 instance", ex);
        } 
      } 
    } catch (JsonMappingException e) {
      handleError("Unable to parse response returned from service endpoint", (Exception)e);
    } catch (IOException e) {
      handleError("Unable to load credentials from service endpoint", e);
    } catch (URISyntaxException e) {
      handleError("Unable to load credentials from service endpoint", e);
    } 
  }

//刷新本地 token 的条件
  protected boolean needsToLoadCredentials() {
    if (this.credentials == null)  //如果本地没有 credential
      return true; 
    if (this.credentialsExpiration != null && isWithinExpirationThreshold()) //本地的 credential 已过期或者是还有 15 分钟到期
      return true; 
    if (this.lastInstanceProfileCheck != null && isPastRefreshThreshold()) //上次刷新在一小时前
      return true; 
    return false;
  }

 private boolean isWithinExpirationThreshold() {
    return (this.credentialsExpiration.getTime() - System.currentTimeMillis() < 900000L);
  }

private boolean isPastRefreshThreshold() {
    return (System.currentTimeMillis() - this.lastInstanceProfileCheck.getTime() > 3600000L);
  }
  
  private boolean expired() {
    if (this.credentialsExpiration != null && 
      this.credentialsExpiration.getTime() < System.currentTimeMillis())
      return true; 
    return false;
  }

查看 S3A 封装的获取 Credential 方法org.apache.hadoop.fs.s3a.AWSCredentialProviderList.getCredentials,并没有失败重试机制:

public AWSCredentials getCredentials() {
    checkNotEmpty();
    if (this.reuseLastProvider && this.lastProvider != null)
      return this.lastProvider.getCredentials(); 
    AmazonClientException lastException = null;
    for (AWSCredentialsProvider provider : this.providers) {
      try {
        AWSCredentials credentials = provider.getCredentials();
        if ((credentials.getAWSAccessKeyId() != null && credentials
          .getAWSSecretKey() != null) || credentials instanceof com.amazonaws.auth.AnonymousAWSCredentials) {
          this.lastProvider = provider;
          LOG.debug("Using credentials from {}", provider);
          return credentials;
        } 
      } catch (AmazonClientException e) {
        lastException = e;
        LOG.debug("No credentials provided by {}: {}", new Object[] { provider, e
              .toString(), e });
      } 
    } 
    String message = "No AWS Credentials provided by " + listProviderNames();
    if (lastException != null)
      message = message + ": " + lastException; 
    throw new AmazonClientException(message, lastException);
  }

通过上面代码可以看出,AWS SDK 强制每个小时都会重新获取新的 token,在获取新的 token 失败后,会直接抛出异常。同时上层 S3A 在封装时候并没有错误重试,而是直接抛出异常。

应用程序到 IMDS 获取 credential 失败的原因:

  1. 达到了 EC2 网卡的 DNS 配额(每个 EC2 实例每个网卡每秒向 Route53 Resolver 发送 1024 个数据包)。
  2. EC2 的内存和 CPU 使用率 100%,导致网络丢包,IMDS 服务器无法响应或者响应超时。

客户侧问题出现的时候,排查发现 EC2 的 DNS 请求并没有很多,但是 CPU 使用率较高。

uptime
 15:16:11 up 236 days, 19:15,  0 users,  load average: 195.19, 157.28, 137.45
 14:35:14 up 318 days, 23:12,  0 users,  load average: 117.48, 137.96, 103.15

所以造成失败的原因主要为 CPU 利用率太高。同时在获取 token 失败后,即使本地的 token 仍在有效期,代码仍然返回异常。这也就解释了客户的任务一般都在运行一小时之后才会出现问题。

解决方案

1. 避免 CPU 长期在负载较高状态下运行。从客户的问题来看,主要原因是 CPU 长期保持在较高利用率甚至 100%。

2. 修改应用的代码,在收到 NoAuthWithAWSException 异常后,增加任务重试(指数回退)。

3. 如果使用 S3A,可以重写 AWS SDK 的 EC2CredentialsFetcher.getCredentials,在访问 IMDS 发生异常时候判断本地 credential 是否有效,如果有效,返回 credential:

public AWSCredentials getCredentials() {
        if (needsToLoadCredentials()){
         try{  
              fetchCredentials(); 
              }catch(Exception e){
                    if ( this.credentials !=null){
                          return this.credentials
                       }else{
                         Throw exception e;
                       }
               }
          }
}

4. 如果使用 EMRFS,您可以通过实现 AWSCredentialProvider 来创建自定义凭证,在自定义凭证中对 credential 进行判断,如果异常发生时候 credential 仍然有效,那么返回有效的 credential,或者增加指数回退重试。下面代码是异常发生时,不抛出异常,返回可用 credential 的样例:

@override
 public AWSCredentials getCredentials() {
        if (needsToLoadCredentials()){
         try{  
              fetchCredentials(); 
              }catch(Exception e){
                    if ( this.credentials !=null){
                          return this.credentials
                       }else{
                         Throw exception e;
                       }
               }
          }
}

总结

本文首先介绍了 AWS EMR 如何实现与 S3 之间方便、高效和安全可靠的数据交互。EC2 IAM role 提供了一种无缝的方式,将访问权限传递给 EMR 集群中的 EC2 实例,实现了安全的 S3 访问;接着,通过一个客户案例介绍了在实际的大规模业务场景下,可能会出现的 credential 获取失败的问题以及解决办法。实际上,这个获取凭证失败的问题,不完全局限于大数据场景,其他涉及到服务访问的场景下也有可能会发生类似问题,所以这个解决方案具有众多的应用场景,也希望能够帮助读者解决更多的问题。

参考链接

Instance metadata and user data – Amazon Elastic Compute Cloud

为处理 EMRFS 对 Amazon S3 的请求配置 IAM 角色 – Amazon EMR

向 Amazon EC2 实例上运行的应用程序授予访问 AWS 资源的权限

Community collaboration: The S3A story | AWS Open Source Blog

Apache Hadoop Amazon Web Services support  Hadoop-AWS module: Integrat…

本篇作者

余路

西云数据技术客户经理(TAM),拥有超过 10 年的运维经验,曾就职于运营商行业,目前为亚马逊云科技中国客户提供企业级技术支持。

Angela Ren

亚马逊云科技解决方案架构师,负责基于亚马逊云科技云计算方案架构的咨询和设计,推广亚马逊云科技云平台技术和各种解决方案。

Vincent Lyu

西云数据大客户技术经理,曾在知名通信设备厂商和顶尖云计算提供商工作,积累了丰富而广泛的系统架构和质量保证经验。擅长于协助客户建立完善的研发 DevOps 体系,构建稳定可靠的网络和安全体系,并助力企业实现业务产品的成功落地。