使用 Amazon Route 53 别名记录实现与 Oracle Data Guard 环境的透明连接

客户选择 亚马逊云科技 来运行其 Oracle 数据库工作负载,以帮助提高数据库层的弹性、性能和可扩展性。数据库堆栈的高可用性 (HA) 解决方案是在 亚马逊云科技 中迁移或部署 Oracle 数据库时需要考虑的重要方面,以帮助确保架构能够满足应用程序的服务级别协议 (SLA)。在 亚马逊弹性计算云 (亚马逊 EC2)上运行 Oracle 数据库的客户通常会选择 Oracle Data Guard 物理备用数据库来帮助满足其 Oracle 数据库工作负载的高可用性和灾难恢复 (DR)。

如本 Oracle 文档 中所述 ,在连接 URL 或 tnsnames.ora 条目中包含多个侦听器端点的基于角色的服务是透明地连接到 Data Guard 配置中数据库层的首选方式。但是,某些应用程序组件和驱动程序配置不支持连接 URL 中的多个主机名。这些应用程序需要一个主机名或 IP 才能让客户端连接到 Data Guard 环境。

这篇文章讨论了在 EC2 的数据保护环境 中使用 Amazon Route 53 CNAME 记录 的概念,并列出了在基于数据库角色的 Data Guard 配置中自动路由主环境和备用环境之间连接的工件。

解决方案概述

为了避免在 Data Guard 环境中执行故障转移或切换操作后手动更新 DNS 条目或 tnsnames.ora 文件,该解决方案使用 AFTER DB_ROLE_CHANGE 触发器来自动执行 D NS 故障转移过程。 此触发器在数据库主机上运行 shell 脚本,该脚本反过来会更新 Route 53 中的 CNAME 记录,指向 CNAME 记录以反映角色转换。下图说明了解决方案架构(图 1)。

Figure 1. Solution architecture

图 1。解决方案架构

这篇文章中讨论的解决方案包括在 Data Guard 切换活动后将新的数据库连接请求路由到正确的数据库。但是,其他因素(例如应用程序/客户端 TTL 设置和连接池的行为)会使切换活动之前创建的连接句柄失效,可能会导致应用程序以不同的角色连接到数据库(例如读写工作负载在切换后连接到待机),并可能生成错误,例如 ORA-16000:数据库或可插拔数据库打开以进行只读访问。 最佳做法是先验证数据库角色,然后再使用事务的连接句柄来验证应用程序是否以预期的角色连接到数据库。

以下工作流程描述了在 Data Guard 环境中进行故障转移或切换活动期间发生的事件顺序,以实现应用程序的无缝连接:

  1. 角色转换事件发生在 Data Guard 环境中。
  2. 该事件触发 AFT ER DB_ROLE_CHANGE 触发器。
  3. 触发器使用调度器任务在 EC2 实例上运行 shell 脚本。
  4. shell 脚本更新 Route 53 以指向 CNAME 记录以反映角色转换。

先决条件

这篇文章假设以下先决条件:

  • 您应该在单个 VPC 中使用一个主数据库实例和一个备用数据库实例的现有 Data Guard 配置。请参阅 Oracle 快速入门 模板,在亚马逊 EC2 上部署数据保护环境。
  • 此处讨论的步骤适用于使用红帽 Linux AMI 在 Amazon EC2 上进行自我管理的 Data Guard 配置。
  • 文章中讨论的场景涉及 Data Guard 配置中的一个主数据库和一个备用数据库。对于任何其他配置,本示例中显示的脚本需要进行其他更改。
  • 应在存在数据库环境的 VPC 中配置私有或公有 Route 53 托管区域
  • shell 脚本使用 EC2 实例的实例配置文件来运行 亚马逊云科技 命令行接口 (亚马逊云科技 CLI) 命令。确保托管主数据库和备用数据库的 EC2 实例的实例配置文件附有允许更改托管区域中的记录集的策略,例如:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DBCnameFlipPloicy",
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/<<YourHostedZoneId>>"
}
]
}
  • 必须在所有@@ 数据库主机上安装 Nslookup jq curl 实用程序。如果未安装,则可以使用以下命令在 RHEL Linux 上安装该实用程序:
yum install -y bind-utils
yum install -y curl
yum install -y jq

环境详情

这篇文章假设 Data Guard 配置在单个 VPC 中包含两个实例,一个主实例和一个备用实例,详细信息和命名规则如下:

  • Oracle 数据库版本 — 19.10 配置为最高性能模式,带有 Active Data Guard
  • 路由 53 域名 — myd bdomain
  • 数据库名称 — orc l
  • DB_UNIQUE_NAME — orcl_a 和 orcl_b
  • 实例名称 — orc l
  • 路线 53 AZ1 中主机的记录 — orcl -a-db.mydbdomain
  • 路线 53 AZ2 中主机的记录 — orcl -b-db.mydbdomain

路线 53 配置

在 Route 53 中创建了两条指向主机和备用主机的 IP 的 A 记录。在 Route 53 中还创建了两个 CNAME 记录,这两条记录在 Data Guard 切换和故障转移场景中会自动更新。CNAME 记录 orcl-rw.mydbdomain 指向具有主角色的实例,可以接受读/写交易, orcl-ro .mydbdomain 指向处于备用角色的接受只读查询的实例。

A 记录配置如下所示:

  • AZ1 中的数据库主机 IP(本示例中为 10.0.0.5)— orcl-a-db.my dbdomain
  • AZ2 中的数据库主机 IP(本示例中为 10.0.32.5)— orcl-b-db.mydbdom ain

CNAME 记录配置如下所示:

  • orcl-a-db.mydbdomain — orcl- rw.mydbdomain
  • orcl-b-db.mydbdomain — orcl-ro.myd bdomain

以下屏幕截图显示了域 mydbd omain 的 Route 53 控制台视图。

The Route 53 console view of the domain mydbdomain

图 2。网域 myd bdomain 的 Route 53 控制台视图

TNS 配置

以下 tnsnames.ora 文件条目显示了如何在不依赖托管主数据库和备用数据库的 EC2 实例的实际 IP 地址的情况下使用 CNAME 记录与主数据库和备用数据库建立连接。 orcl_a 条目 始终指向 orcl -a-db.mydbdomain 上的实例, 而 orcl_b 始终指向 orcl-b-db.mydbdb域上的实例 , 无论其角色如何 orclr w 和 orclr o 条目分别 将连接定向到扮演主要和备用角色的数据库。

orcl_a =
(description =
(address = (protocol = tcp)(host = orcl-a-db.mydbdomain)(port = 1525))
(connect_data =
(server = dedicated)
(service_name = orcl_a)
)
)

orcl_b =
(description =
(address = (protocol = tcp)(host = orcl-b-db.mydbdomain)(port = 1525))
(connect_data =
(server = dedicated)
(service_name = orcl_b)
)
)

orclrw =
(description =
(address = (protocol = tcp)(host = orcl-rw.mydbdomain)(port = 1525))
(connect_data =
(server = dedicated)
(service_name = orcl)
)
)

orclro =
(description =
(address = (protocol = tcp)(host = orcl-ro.mydbdomain)(port = 1525))
(connect_data =
(server = dedicated)
(service_name = orcl)
)
)

要使用 orclrw 和 orclr o TNS 条目启用连接,可以在主监听器和备用侦听器中使用基于角色的服务或静态侦听器注册条目,如以下代码所示:

SID_DESC =
      (GLOBAL_DBNAME = orcl)
      (ORACLE_HOME = /opt/oracle/product/19c/dbhome_1)
      (SID_NAME = orcl)
    )

实施解决方案

为了在 Oracle 切换或故障转移期间实现自动 DNS 更新,我们使用 Oracle 数据库触发器和 shell 脚本。以下是整个工作流程的高级步骤:

  1. 在主数据库上创建 DB_ROLE_CHANGE ON DATABASE 触发器
  2. 触发器反过来会创建一个 DBMS 作业,该作业 使用 cname_switch.sh 调用 shell 脚本。
  3. shell 脚本更新 Route 53 别名记录条目。

数据库触发器

将以下代码用于数据库触发器:

CREATE OR REPLACE TRIGGER sys.cname_flip_post_role_change 
AFTER DB_ROLE_CHANGE ON DATABASE
DECLARE
  v_db_name VARCHAR2(9);
  v_db_role VARCHAR2(16);
BEGIN
  SELECT DATABASE_ROLE  INTO v_db_role FROM V$DATABASE;
  SELECT DB_UNIQUE_NAME INTO v_db_name FROM V$DATABASE;

  IF v_db_role = 'PRIMARY' THEN
    BEGIN
      dbms_scheduler.drop_job('RW_CNAME_FLIP');
    EXCEPTION
      WHEN OTHERS THEN NULL;
    END;

    dbms_scheduler.create_job(
      job_name   => 'RW_CNAME_FLIP',
      job_type   => 'EXECUTABLE',
      number_of_arguments => 1,
      job_action => '/home/oracle/admin/bin/cname_switch.sh',
      enabled    => false,
      auto_drop  => true);

    dbms_scheduler.set_job_argument_value(
      job_name          => 'RW_CNAME_FLIP',
      argument_position => 1,
      argument_value    => v_db_name);

    BEGIN
      dbms_scheduler.run_job('RW_CNAME_FLIP');
    EXCEPTION
    WHEN OTHERS THEN
      raise_application_error(-20101, 'CNAME flip failed, check script error');
    END;

  END IF;

EXCEPTION
  WHEN OTHERS THEN
    raise_application_error(-20102, 'CNAME flip failed due to error: ' || SQLERR
M);
END;
/

外壳脚本

此脚本确定当前 CNAME,识别从属 A 记录,并相应地将 CNAME 映射到正确的 A 记录。假设示例配置中使用了 db_n ame 和 db_ unique_ name 的命名惯例,则提供此 s hell 脚本仅供参考。您应该查看和修改脚本以满足您的特定要求和组织标准。

如前所示的示例,shell 脚本放置在 /home/oracle/admin/bin/cname_switch.sh 位置 。

注意:通常会看到恢复或克隆到较低环境的生产数据库。

如果脚本在这些环境中运行,它可能会意外更改 CNAME 条目。为了缓解这种情况,外壳脚本使用了 restore_safeguard 函数。此函数检查分配给 EC2 实例的 IP 实际上是否与 Route 53 中为此数据库配置的 A 记录相匹配。如果未找到匹配项,则不会执行 CNAME 故障转移。

#! /bin/bash
#set -x
​
# Variables may need to be changed to suit your environment
​
DB_NAME=$1
DB_IN=$1
echo "Orginal Input : ${DB_NAME}"
DB_NAME=`echo "${DB_NAME::-2}"`  # removing last 2 characters from DB_UNIQUE_NAME
DB_NAME=`echo "${DB_NAME}" | tr '[:upper:]' '[:lower:]'`
echo "Modified Input : ${DB_NAME}"
​
DB_DOMAIN=<<YOUR_AWS_ROUTE53_DOMAIN_NAME>>    # Update as per your AWS Route53 domian name
ZONE_ID=<<YOUR_AWS_ROUTE53_HOSTED_ZONE_ID>>   # Update as per your AWS Route53 hosted zone ID
EC2_METADATA='http://169.254.169.254/latest/dynamic/instance-identity/document'
​
# CNAME and A-Records related varables :
​
RW_CNAME=`echo "${DB_NAME}-rw.${DB_DOMAIN}"`
RO_CNAME=`echo "${DB_NAME}-ro.${DB_DOMAIN}"`
A_CNAME=`echo "${DB_NAME}-a-db.${DB_DOMAIN}"`
B_CNAME=`echo "${DB_NAME}-b-db.${DB_DOMAIN}"`
​
REGION=`curl -s ${EC2_METADATA}|grep region|awk -F\" '{print $4}'`
​
# Logfile configuration and file initilization
​
TS=`date +%Y%m%d_%H%M%S`
LOG_DIR=/tmp
CHANGE_SET_FILE=`echo "${LOG_DIR}/${DB_NAME}-CnameFlip-${TS}.json"`
LOG_FILE=`echo "${LOG_DIR}/${DB_NAME}-CnameFlip-${TS}.log"`
CONF_FILE=`echo "file://${CHANGE_SET_FILE}"`
​
# Function to check if current host IP matching with Route 53 configuration
​
IS_SAFE='Unsafe'
​
function restore_safeguard()
{
    AWS_TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
    LOCAL_IPV4=`curl -sH "X-aws-ec2-metadata-token: $AWS_TOKEN" -v http://169.254.169.254/latest/meta-data/local-ipv4`
    PUBLIC_IPV4=`curl -sH "X-aws-ec2-metadata-token: $AWS_TOKEN" -v http://169.254.169.254/latest/meta-data/public-ipv4`
    NOT_FOUND=`echo ${PUBLIC_IPV4} | grep '404 - Not Found' | wc -l`
​
    if [ ${NOT_FOUND} == 1 ]; then
       PUBLIC_IPV4='No Public IP Assigned'
    fi
​
    A_IP=$(aws route53 list-resource-record-sets --hosted-zone-id ${ZONE_ID} \
           --query 'ResourceRecordSets[?Type==`A`].{Name: Name, Value:ResourceRecords[0].Value}' | \
           jq -cr --arg DB_NAME "${DB_NAME}-a" '.[] | select( .Name | contains($DB_NAME)).Value')
​
    B_IP=$(aws route53 list-resource-record-sets --hosted-zone-id ${ZONE_ID} \
           --query 'ResourceRecordSets[?Type==`A`].{Name: Name, Value:ResourceRecords[0].Value}' | \
           jq -cr --arg DB_NAME "${DB_NAME}-b" '.[] | select( .Name | contains($DB_NAME)).Value')
​
    PREVIOUS_RW_ID=$(aws route53 list-resource-record-sets --hosted-zone-id ${ZONE_ID} \
           --query 'ResourceRecordSets[?Type==`CNAME`].{Name: Name, Value:ResourceRecords[0].Value}' | \
           jq -cr --arg DB_NAME "${DB_NAME}-rw" '.[] | select( .Name | contains($DB_NAME)).Value' | cut -d'-' -f2)
​
    if [ ${PREVIOUS_RW_ID} == 'a' ]; then
       RW_NODE_IP=${A_IP}
       RO_NODE_IP=${B_IP}
    else
       RW_NODE_IP=${B_IP}
       RO_NODE_IP=${A_IP}
    fi
​
    # Looging Input values
​
    echo "Orginal Input   : ${DB_IN}"          | tee -a ${LOG_FILE}
    echo "Modified Input  : ${DB_NAME}"        | tee -a ${LOG_FILE}
    echo "Current RW ID   : ${PREVIOUS_RW_ID}" | tee -a ${LOG_FILE}
    echo "Host Private IP : ${LOCAL_IPV4}"     | tee -a ${LOG_FILE}
    echo "Host Public IP  : ${PUBLIC_IPV4}"    | tee -a ${LOG_FILE}
    echo "A Node IP       : ${A_IP}"           | tee -a ${LOG_FILE}
    echo "A Node IP       : ${B_IP}"           | tee -a ${LOG_FILE}
    echo "RW Node IP      : ${RW_NODE_IP}"     | tee -a ${LOG_FILE}
    echo "RO Node IP      : ${RO_NODE_IP}"     | tee -a ${LOG_FILE}
​
    if [ "${LOCAL_IPV4}" == "${RO_NODE_IP}" -o "${PUBLIC_IPV4}" == "${RO_NODE_IP}" ]; then
       IS_SAFE='Safe'
    else
       IS_SAFE='Unsafe'
    fi
}
​
restore_safeguard
​
if [ ${IS_SAFE} == 'Safe' ]; then
   echo "Safe for CNAME faliover..." | tee -a ${LOG_FILE}
else
   echo "Unsafe for CNAME faliover..." | tee -a ${LOG_FILE}
   echo "Aborting..."
   exit 1
fi
​
PRI_DB_ID=`nslookup ${RW_CNAME}|grep "canonical name"|cut -d'=' -f2|cut -d'-' -f2`
​
# Looging Input values :
echo "Orginal Input      : ${DB_IN}"     | tee    ${LOG_FILE}
echo "Modified Input     : ${DB_NAME}"   | tee -a ${LOG_FILE}
echo "Current RW host ID : ${PRI_DB_ID}" | tee -a ${LOG_FILE}
​
echo -e "\nChange to be done : \n" | tee -a ${LOG_FILE}
​
if [ ${PRI_DB_ID} == 'a' ]; then
   echo "Changing ${RW_CNAME} from ${A_CNAME} to ${B_CNAME}" | tee -a ${LOG_FILE}
   echo "Changing ${RO_CNAME} from ${B_CNAME} to ${A_CNAME}" | tee -a ${LOG_FILE}
   TO_BE_RW_CNAME=${B_CNAME}
   TO_BE_RO_CNAME=${A_CNAME}
else
   echo "Changing ${RW_CNAME} from ${B_CNAME} to ${A_CNAME}" | tee -a ${LOG_FILE}
   echo "Changing ${RO_CNAME} from ${A_CNAME} to ${B_CNAME}" | tee -a ${LOG_FILE}
   TO_BE_RW_CNAME=${A_CNAME}
   TO_BE_RO_CNAME=${B_CNAME}
fi
​
R53_CHANGE=`echo -e "
{
  \"Comment\": \"Flip CNAMEs\",
  \"Changes\": [
    {
      \"Action\" : \"UPSERT\",
      \"ResourceRecordSet\" : {
        \"Name\" : \"${RW_CNAME}.\",
        \"Type\" : \"CNAME\",
        \"TTL\"  : 60,
        \"ResourceRecords\" : [{ \"Value\": \"${TO_BE_RW_CNAME}.\" }]
      }
    },
    {
      \"Action\" : \"UPSERT\",
      \"ResourceRecordSet\" : {
        \"Name\" : \"${RO_CNAME}\",
        \"Type\" : \"CNAME\",
        \"TTL\"  : 60,
        \"ResourceRecords\" : [{ \"Value\": \"${TO_BE_RO_CNAME}.\" }]
      }
    }
  ]
}
"`
​
echo -e "\nRoute53 Change Set :\n" | tee -a ${LOG_FILE}
echo ${R53_CHANGE} | tee -a ${LOG_FILE}
echo ${R53_CHANGE} > ${CHANGE_SET_FILE}
​
echo -e "\nCommand to Execute : " | tee -a ${LOG_FILE}
echo -e "\naws route53 change-resource-record-sets --hosted-zone-id ${ZONE_ID} \
         --change-batch ${CONF_FILE} \n" | tee -a ${LOG_FILE}
​
echo -e "\nExecution Result :\n"
aws route53 change-resource-record-sets --hosted-zone-id ${ZONE_ID} \
--change-batch ${CONF_FILE} | tee -a ${LOG_FILE}
​
echo -e "\nAfter Change :\n "
aws route53 list-resource-record-sets --hosted-zone-id ${ZONE_ID} | tee -a ${LOG_FILE}

测试解决方案

以下屏幕截图显示了切换 之前 域 mydbd omain 的 Route 53 控制台视图。主数据库正在 orcl-a-db.mydomain 上运行,因为 orcl-rw.mydomain 指 向那个。

Route 53 console view of the domain mydbdomain before the switchover

图 3。Route 53 切换 前 域 mydbd omain 的控制台视图

以下 SQL 显示主数据库和备用数据库的当前角色以及它们当前运行的 host_name。

[oracle@ip-10-0-0-5 sql]$ cat db_info.sql

ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD:HH24:MI';
set lines 150 pages 200
col HOST_NAME for a30 trunc

select d.NAME, d.db_unique_name, d.DATABASE_ROLE, d.OPEN_MODE, i.INSTANCE_NAME, 
i.HOST_NAME, i.STARTUP_TIME
from v$instance i, v$database d;

[oracle@ip-10-0-0-5 sql]$ sqlplus system@orclrw

SQL> @db_info

NAME  DB_UNIQUE_NAME DATABASE_ROLE OPEN_MODE INSTANCE_NAME HOST_NAME STARTUP_TIME
------ ---------------- -------------- ---------------- ------------------------------ ----------------
ORCL orcl_a PRIMARY READ WRITE orcl ip-10-0-0-5.us-west-2.compute. 2020-05-24:01:47

[oracle@ip-10-0-0-5 sql]$ sqlplus system@orclro

SQL> @db_info

NAME DB_UNIQUE_NAME DATABASE_ROLE OPEN_MODE INSTANCE_NAME HOST_NAME STARTUP_TIME
------ ---------------- -------------------- -------------- ------------------------------- ----------------
ORCL orcl_b PHYSICAL STANDBY READ ONLY WITH APPLY orcl ip-10-0-32-5.us-west-2.compute. 2020-05-24:05:50

让我们开始切换:

[oracle@ip-10-0-0-5 sql]$ dgmgrl /
DGMGRL for Linux: Release 12.2.0.1.0 - Production on Wed May 27 06:42:51 2020

Copyright (c) 1982, 2017, Oracle and/or its affiliates.  All rights reserved.

Welcome to DGMGRL, type "help" for information.
Connected to "orcl_a"
Connected as SYSDG.
DGMGRL> show configuration;

Configuration - awsguard

  Protection Mode: MaxPerformance
  Members:
  orcl_a - Primary database
    orcl_b - Physical standby database

Fast-Start Failover: DISABLED

Configuration Status:
SUCCESS   (status updated 39 seconds ago)

DGMGRL> switchover to orcl_b;
Performing switchover NOW, please wait...
Operation requires a connection to database "orcl_b"
Connecting ...
Connected to "orcl_b"
Connected as SYSDBA.
New primary database "orcl_b" is opening...
Oracle Clusterware is restarting database "orcl_a" ...
Switchover succeeded, new primary is "orcl_b"
DGMGRL>
DGMGRL> show configuration;

Configuration - awsguard

  Protection Mode: MaxPerformance
  Members:
  orcl_b - Primary database
    orcl_a - Physical standby database

Fast-Start Failover: DISABLED

Configuration Status:
SUCCESS   (status updated 67 seconds ago)

DGMGRL>

现在切换已经完成,让我们使用以下代码使用 orclrw 和 orclr o TNS 条目连接到数据库:

[oracle@ip-10-0-0-5 sql]$ sqlplus system@orclrw

SQL> @db_info

NAME DB_UNIQUE_NAME  DATABASE_ROLE  OPEN_MODE     INSTANCE_NAME  HOST_NAME                      STARTUP_TIME
----- -------------- ------------- -------------- ------------------------------ ----------------
ORCL  orcl_b PRIMARY        READ WRITE    orcl          ip-10-0-32-5.us-west-2.compute 2020-05-24:05:50


[oracle@ip-10-0-0-5 sql]$ sqlplus system@orclro

SQL> @db_info

NAME  DATABASE_ROLE     OPEN_MODE            INSTANCE_NAME  HOST_NAME            STARTUP_TIME
----- ----------------- -------------------- -------------- ------------------------------ ----------------
ORCL orcl_a PHYSICAL STANDBY  READ ONLY WITH APPLY orcl          ip-10-0-0-5.us-west-2.compute. 2020-05-27:06:43

以下屏幕截图显示了切换后域 mydbdomain 的 Route 53 控制台视图。主数据库现在正在 orcl-b-db.mydomain 上运行,因为 orcl-rw.mydomain 指 向那个。

Route 53 console view of the domain mydbdomain after the switchover

图 4。Route 53 切换 后 域 mydbd omain 的控制台视图

结论

应用程序与 Data Guard 环境的连接可能很困难,尤其是在应用程序配置不支持多个主机名或侦听器端点时。在这篇文章中,我们讨论了使用 Route 53 CNAME 记录、数据库触发器和 shell 脚本实现与 Data Guard 环境的无缝连接的分步详细信息。您可以使用这些构件将数据库连接无缝地定向到具有正确角色的数据库,而无需更改应用程序。如果您使用 Dat a Guard Observer 进行自动故障转移,另一篇博客《使用 Amazon Route 53 为 Oracle Data Guard 设置高可用性设计(快速启动故障转移) 》 讨论了实现相同结果的替代机制。