nemo64/dbal-rds-data

doctrine dbal 的 rds-data 驱动程序

v1.3.2 2021-05-20 11:08 UTC

This package is auto-updated.

Last update: 2024-09-29 05:36:06 UTC


README

Packagist Version GitHub Workflow Status Packagist License Packagist Downloads

Aurora Serverless rds 数据 API 的 doctrine 驱动程序

这是一个驱动程序,用于在项目中使用 dbal 进行数据库访问时,使用 aws rds-data API。

它模拟了 MySQL 连接,包括事务。然而:该驱动程序永远不会建立持久连接。

这是一个实验性的项目。我在一个 symfony 项目中实现了它,与 doctrine orm 一起使用,并且驱动程序工作正常。我测试了模式工具、迁移和事务。

目前,我不会推荐使用 rds-data API,因为它在 2020 年年底秘密添加了每秒 1000 个请求的限制。它列在您的服务配额列表中的“每秒数据 API 请求”下,但在文档中并未提及。

你为什么想使用它?

  • 数据 API 使您能够在 AWS 托管环境中使用数据库,而无需 VPC,这增加了复杂性和成本,特别是如果您需要通过 NAT 网关进行互联网访问时。
  • 您的应用程序不需要明文中的数据库密码。您只需要访问 AWS API,这可以更好地管理。(还有其他方法可以实现相同的功能,但使用数据 API 真的很简单)
  • 由于不需要建立直接的数据库连接和自动池管理,可能会有性能优势。

你为什么不使用它?

  • 这个实现还没有经过 20 年的实战测试。请查看实现细节部分,看看您是否感到舒适。
  • 运行大量小查询时的性能可能是最大的问题。rds data API(截至撰写本文时)有一个不太文档化的每秒每个账户 1000 个查询的限制。如果您曾经使用过 doctrine orm,那么您知道那并不多,特别是如果您运行未经优化的后台作业。AWS SDK 会重试查询,这意味着一切都会变慢。
  • rds-data API 在ExecuteStatement调用中有大小限制,当您的应用程序增长时可能会成为问题,尽管它们目前似乎并未启用。
  • rds-data API尚未在所有地区可用。尽管如此,这种限制正在逐渐放宽。
  • rds-data API 由于其主要是无状态的,因此存在一些固有的限制。最大的问题是您不能设置(会话)变量。这也意味着您不能设置事务隔离级别,尽管在 dbal 中这仍然是一个可选功能。尽管如此,您仍然可以在事务中使用正常的锁定。
  • 仅与rds-data API配合Aurora Serverless使用,并且这个库也限制您只能使用MySQL模式。如果您打算使用其他数据库,那么您现在无法使用rds-data API和这个库。以下是一些您可能想要考虑的替代方案:
    • Aurora Serverless在Postgres模式下(虽然这很可能很容易添加到此处,我愿意接受pull请求)
    • 使用Aurora Classic来获取SLA或从可预测的工作负载中受益于预留实例定价
    • Aurora Global提供更好的可用性和Aurora Classic的所有好处
    • 或者甚至正常RDS来节省金钱或使用Aurora未模拟的引擎

如何使用它

首先,您必须将数据库凭据作为一个秘密存储,包括用户名。然后请确保正确配置数据库访问以使用秘密和数据库。如果您创建了一个iam用户,则有一个“AmazonRDSDataFullAccess”策略可以直接使用。

如果您直接使用dbal,那么这是方法

<?php
$connectionParams = array(
    'driverClass' => \Nemo64\DbalRdsData\RdsDataDriver::class,
    'host' => 'eu-west-1', // the aws region
    'user' => '[aws-api-key]', // optional if it is defined in the environment 
    'password' => '[aws-api-secret]', // optional if it is defined in the environment
    'dbname' => 'mydb',
    'driverOptions' => [
        'resourceArn' => 'arn:aws:rds:eu-west-1:012345678912:cluster:database-1np9t9hdbf4mk',
        'secretArn' => 'arn:aws:secretsmanager:eu-west-1:012345678912:secret:db-password-tSo334',
    ]
);
$conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);

或者使用简短的url语法,这使用环境变量更容易更改

<?php
$connectionParams = array(
    'driverClass' => \Nemo64\DbalRdsData\RdsDataDriver::class,
    'url' => '//eu-west-1/mydb'
        . '?driverOptions[resourceArn]=arn:aws:rds:eu-west-1:012345678912:cluster:database-1np9t9hdbf4mk'
        . '&driverOptions[secretArn]=arn:aws:secretsmanager:eu-west-1:012345678912:secret:db-password-tSo334'
);
$conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);

鉴于我在symfony项目中开发,我也可以添加如何在symfony中定义驱动程序的方法

# doctrine.yaml
doctrine:
    dbal:
        # the url can override the driver class
        # but I can't define this driver in the url which is why i made it the default
        # Doctrine\DBAL\DriverManager::parseDatabaseUrlScheme
        driver_class: Nemo64\DbalRdsData\RdsDataDriver
        url: '%env(resolve:DATABASE_URL)%'
# .env

# you must not include a driver in the database url
# in this case I also didn't include the aws tokens in the url 
DATABASE_URL=//eu-west-1/mydb?driverOptions[resourceArn]=arn&driverOptions[secretArn]=arn

# the aws-sdk will pick those up
# they are automatically configured in lambda and ec2 environments 
#AWS_ACCESS_KEY_ID=...
#AWS_SECRET_ACCESS_KEY=...
#AWS_SESSION_TOKEN=...

除了配置之外,它应该像任何其他dbal连接一样工作。

驱动程序选项

  • resourceArn(字符串;必需)这是数据库集群的ARN。进入您的RDS-Management > 您的数据库 > 配置并从那里复制。
  • secretArn(字符串;必需)这是存储数据库密码的秘密的ARN。进入[SecretManager] > 您的秘密并使用Secret ARN。
  • timeout(整数;默认值=45)guzzle的超时设置。设置为0为无限期,但这可能没有用。rds-data API将最多阻塞45秒(见rds-data 文档)。带有continueAfterTimeout选项的架构更新查询将自动执行。如果您需要运行较长的更新查询,则可能需要直接使用rds数据客户端。使用$dbalConnection->getWrappedConnection()->getClient()获取aws-sdk客户端。
  • pauseRetries(整数;默认值=0)数据库暂停时的重试次数。如果您设置了此值,也请考虑设置pauseRetryDelay以确保大致正确的重试时间。
  • pauseRetryDelay(整数;默认值=10)如果在最后一次尝试失败且由于数据库暂停而失败的情况下,等待再次尝试的秒数。截至撰写本文时,Aurora从30秒到2分钟暂停。这在大多数情况下等待时间太长。Lambda会自动重试事件,因此您通常最好让事件失败。用户通常也不会等一分钟来加载页面,因此您应该向他们展示适当的错误。请参阅暂停数据库以了解如何执行此操作。

CloudFormation

当然,这里有一个CloudFormation模板来配置Aurora Serverless和一个秘密,将它们组合在一起并设置一个包含所需信息的环境变量。

这可能具有serverless风格,但您应该能够掌握它。

# [...]

  iamRoleStatements:
    # allow using the rds-data api
    # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html#data-api.access
    - Effect: Allow
      Resource: '*' # it isn't supported to limit this
      Action:
        # https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazonrdsdataapi.html
        - rds-data:ExecuteStatement
        - rds-data:BeginTransaction
        - rds-data:CommitTransaction
        - rds-data:RollbackTransaction
    # this rds-data endpoint will use the same identity to get the secret 
    # so you need to be able to read the password secret
    - Effect: Allow
      Resource: !Ref DatabaseSecret
      Action:
        # https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awssecretsmanager.html
        - secretsmanager:GetSecretValue

# [...]

  environment:
    DATABASE_URL: !Join
      - ''
      - - '//' # rds-data is set to default because custom drivers can't be named in a way that they can be used here
        - !Ref AWS::Region # the hostname is the region
        - '/mydb'
        - '?driverOptions[resourceArn]='
        - !Join [':', ['arn:aws:rds', !Ref AWS::Region, !Ref AWS::AccountId, 'cluster', !Ref Database]]
        - '&driverOptions[secretArn]='
        - !Ref DatabaseSecret

# [...]

  # Make sure that there is a default VPC in your account.
  # https://console.aws.amazon.com/vpc/home#vpcs:isDefault=true
  # If not, click "Actions" > "Create Default VPC"
  # While your applications doesn't need it, the database must still be provisioned into a VPC so use the default. 
  Database:
    Type: AWS::RDS::DBCluster # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html
    Properties:
      Engine: aurora
      EngineMode: serverless
      EnableHttpEndpoint: true # https://stackoverflow.com/a/58759313 (not fully documented in every language yet)
      DatabaseName: 'mydb'
      MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:username}}']]
      MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:password}}']]
      BackupRetentionPeriod: 1 # day
      # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-scalingconfiguration.html
      ScalingConfiguration: {MinCapacity: 1, MaxCapactiy: 2, AutoPause: true}
  DatabaseSecret:
    Type: AWS::SecretsManager::Secret # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html
    Properties:
      GenerateSecretString:
        SecretStringTemplate: '{"username": "admin"}'
        GenerateStringKey: "password"
        PasswordLength: 41 # max length of a mysql password
        ExcludeCharacters: '"@/\'
  DatabaseSecretAttachment:
    Type: AWS::SecretsManager::SecretTargetAttachment # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html
    Properties:
      SecretId: !Ref DatabaseSecret
      TargetId: !Ref Database
      TargetType: AWS::RDS::DBCluster

我还写了一篇更详细的文章,介绍了如何在多个堆栈之间设置和共享Aurora Serverless:www.marco.zone/shared-aurora-serverless-using-cloudformation

实现细节

错误处理

RDS数据API只提供错误消息,不提供错误代码。为了正确地将这些错误映射到dbal异常,我使用了一个巨大的正则表达式。参见:Nemo64\DbalRdsData\RdsDataException::EXPRESSION。由于大部分内容都是使用mysql错误文档生成的,应该没有问题,但它可能不是100%可靠的。我在亚马逊开发者论坛上提出了这个问题,但还没有得到回复。

暂停的数据库

如果Aurora Serverless被暂停,您将收到以下错误消息

通信链路故障

上次成功发送到服务器的数据包是在0毫秒之前。驱动程序没有从服务器接收到任何数据包。

我将此错误消息映射到错误代码6000(服务器错误是1xxx,客户端错误是2xxx)。它也将转换为dbal的Doctrine\DBAL\Exception\ConnectionException,现有应用程序可能已经优雅地处理。但最重要的是,您可以专门捕获并处理代码6000,以便更好地向用户说明数据库已暂停,并可能很快可用。

预编译语句中的参数

虽然ExecuteStatement支持参数,但它只支持命名参数。问号占位符需要模拟(这很有趣,因为mysqli驱动程序只支持问号占位符,不支持命名参数)。这是通过将?替换为:1:2等来实现的。替换算法将避免替换字符串文字中的?,但请注意,出于安全考虑,您不应该混合重字符串文字和问号占位符。

字符串文字

每个驱动程序都有某种形式的连接感知的字符串文字转义功能。但由于rds-data API是无连接的,它没有这样的方法(当然,除了参数)。为了模拟转义功能,执行对非ASCII字符的检查。如果字符串是纯ASCII,它将直接通过addslashes并得到引号。如果它包含更奇怪的字符,它将被base64编码以防止任何多字节SQL注入攻击。这应该在大多数情况下透明工作,但您绝对应该避免使用literal函数,而应在可能的情况下使用参数绑定。