リード主体のワークロードでRDSを使っている場合、リードレプリカはパフォーマンスに効果的な選択肢だと思います。
ただ、コネクションのエンドポイントはリードレプリカのインスタンス毎に分かれているので、実際に使う場合は、何らかのプロキシを経由して使うことが多いです。
Auroraを使っている場合はまずReader Endpointを使う方法が考えられます、そのほかにもIPアドレスでNLBに登録する方法があると思いますが、今回はpostgresqlでHAProxyとNetworkLoadBalancer(NLB)を使ったパターンを考えてみました。
HAProxyを使うメリットは、バランシングのアルゴリズムの選択や、ルールを柔軟に設定できる点です。
今回は、リードレプリカを時間によって落とした場合に、バックアップとしてマスタを参照するようにしたかったので、NLBに直接リードレプリカを登録した場合、ルールのフォールバックができないようなので、HAProxyを使ってそのように設定します。
また、HAProxyに限らずですが、リードレプリカにシングルエンドポイントで接続できるようになることで、シングルAZのインスタンスでも冗長性を確保でき、負荷にもスケールアウトで対応できるのは、大きなメリットだと思います。
構成はこのようになります。今回、HAProxyは2018年7月に東京リージョンに入ったFargateで作ってみようと思います。
リードレプリカは予め2つ作っておきます。Route53のVPCプライベートホストゾーンで、RDSインスタンスに名前を振っておきます。
- db-master.myvpc.local : マスター
- rep1.myvpc.local : リードレプリカ1
- rep2.myvpc.local : リードレプリカ2
ECR リポジトリと、HAProxyのDocker イメージの作成
まず、HAProxyのDockerイメージを保存するリポジトリを作成します。コンテナ作成時はこのリポジトリが参照されます。
aws ecr create-repositry --repository-name rds-proxy
リポジトリができたら、Dockerfileを作成します。イメージはHAProxy公式のalpine linuxのものを使います。
Dockerfileで行っているのは、HAProxyのコンフィグファイルのインストールだけです。
FROM haproxy:alpine
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
global
pidfile /var/run/haproxy.pid
maxconn 3000
user root
group root
daemon
stats socket /tmp/haproxy-cli.sock
listen rdsinstances
mode tcp
retries 3
timeout connect 10s
timeout server 10s
timeout client 10s
bind 0.0.0.0:5432
option pgsql-check user root
balance roundrobin
server rep1 db-rep1.myvpc.local port 5432 check inter 2000
server rep2 db-rep2.myvpc.local port 5432 check inter 2000
server master db-master.myvpc.local port 5432 backup
マスターを backup
にすることで、リードレプリカを落としている間は、マスターを参照するようにします。ここでは最低限の設定のみですが、柔軟に設定することができると思います。
設定が書けたらDockerイメージをビルドします。できたイメージは先ほど作ったリポジトリにプッシュします。プッシュの手順はAWSコンソールのECRのメニューから確認することもできます。
docker build -t rds-proxy .
aws ecr get-login --no-include-email --region ap-northeast-1 | /bin/bash
docker tag rds-proxy:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/rds-proxy:latest
docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/rds-proxy:latest
NLB、ターゲットグループの作成
次に、起動したHAProxyを登録するNetoworkLoadBalancerとターゲットグループを作成しておきます。schemeはVPC internalにします。
ここで指定するVPC、サブネットにはRDSにアクセスできる適切なものを指定します。
aws elbv2 create-target-group --name rds-haproxies --protocol TCP --port 5432 --vpc-id vpc-xxxxxxxx --target-type ip | \
jq -r '.TargetGroups[] | [{"Type":"forward","TargetGroupArn":.TargetGroupArn}]' > defaultaction
aws elbv2 create-load-balancer --name rds-proxy --type network --scheme internal --subnets subnet-xxxxxxxx subnet-yyyyyyyy | \
jq -r '.LoadBalancers[0].LoadBalancerArn' | \
xargs -I% aws elbv2 create-listners --default-action file://./defaultaction
ECS クラスタの作成
ECSクラスタを作成します。
ecs-cli up --c rds-proxy --region ap-northeast-1
ECS タスク定義、サービスの起動
ここからは docker-compose.yml で行えます。タスクとサービスは、AWSコンソールでも設定が面倒なので、コード化できるのは大きなメリットだと思います。ECS 固有の設定は ecs-params.yml
に書くことができます。こうすることでローカルでのテストにも支障がでません。
task_execution_role
は予め作成しておく必要があります。ネットワークモードを awsvpc
にすることで、既存のVPCに所属させることができます。サブネットはNLB作成時に指定したものと合わせる必要があります。
version: '3'
services:
haproxy:
image: "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/rds-proxy"
ports:
- "5432:5432"
logging:
driver: awslogs
options:
awslogs-group: /ecs/rds-proxy
awslogs-region: ap-northeast-1
awslogs-stream-prefix: ecs
ulimits:
nofile:
soft: 65536
hard: 65536
version: 1
task_definition:
ecs_network_mode: awsvpc
task_execution_role: ecsTaskExecutionRole
task_size:
cpu_limit: 256
mem_limit: 512
run_params:
network_configuration:
awsvpc_configuration:
subnets:
- "subnet-xxxxxxxx"
- "subnet-xxxxxxxx"
security_groups:
- "sg-xxxxxxxx"
assign_public_ip: DISABLED
設定ができたら、最後に ecs-cli
でサービスを作成してタスクを起動します。
起動タイプに FARGATE
を指定します。残念ながら、ロードバランサは ecs-params.yml
に記述することができないようなので、ここで指定します。ALB/NLBの場合は --target-group-arn
でターゲットグループを指定します。
ecs-cli compose \
--verbose \
-c rds-proxy \
--task-role-arn arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole \
service up \
--launch-type FARGATE \
--create-log-groups \
--container-name haproxy \
--container-port 5432 \
--target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:targetgroup/rds-haproxies/xxxxxxxxxxxxxxxx
以上でHAProxyのタスクが1つ起動できました。
最初、--task-role-arn
でコンテナにロールを割り当てるのを忘れていたので、タスクのステータスがPENDDINGから進まず起動できませんでした。CloudWatchLogsを使用する場合、書き込み権限のあるロールをコンテナにつける必要がありました。
docker-composeに近いI/Fで操作できるのはありがたいですが、コマンドラインオプションで指定する必要があったり、まだちょっと使いづらいと感じました。
確認
最後に、リードレプリカへのリクエストの分散が効いているか確認しておきます。
$ sh <<'SCRIPT' | sort | uniq -c
for run in `seq 100`; do
psql -At -U postgres -h rds-proxy-xxxxxxxxxxxxxxxx.elb.ap-northeast-1.amazonaws.com -c 'select inet_server_addr();'
done
SCRIPT
49 172.31.27.155
51 172.31.9.138
きちんとラウンドロビンでできているようです。