ElastiCacheのスケールアップは本当にダウンタイムなしでできるのか検証してみた! #reinvent

2020.01.22

こんにちは(U・ω・U)
着々とElastiCacheおじさんへの道を歩んでいる深澤です。

さて、僕はそんなラスベガスで開催されたre:Invent 2019にて「Whatʼs new with Amazon ElastiCache」というセッションに参加してきました。

このセッションでElastiCacheクラスタは 「Redisはオンラインでのスケールアップが行えるようになった」 と聞きました。ですがこれはどの程度のレベルなのか気になりますよね。もしミリ秒単位でアクセスを捌いているようなシビアな環境だったらこの問題は非常に重要です。そこで今回は簡単なスクリプトを開発して実験してみました。結論というよりもどうやって検証したの?というところを楽しんでいただければと思います!

今回のRedis構成

  • 非クラスターモード
    • プライマリ 1台
    • レプリカ2台

使用したソースコード

検証には以下のpythonコードを用いました。バージョンは3.8.1です。

import os
import time
import logging
import redis
from retrying import retry


class RepeatConnectionToRedis:
    def __init__(self, host: str, interval: int, port=6379, db=0):
        self.redis_client = redis.StrictRedis(
            connection_pool=redis.ConnectionPool(host=host, port=port, db=db)
        )
        self.interval = time.time() + interval
        logging.basicConfig(level=logging.INFO, format='%(asctime)s %(funcName)s %(levelname)s :%(message)s')
        self.logger = logging.getLogger(__name__)
        self.logger.setLevel(logging.DEBUG)

    def _chk_error(e):
        logger = logging.getLogger(__name__)
        if isinstance(e, redis.exceptions.ConnectionError):
            logger.error('Connection refused. Try to reconnect.')
            return True
        elif isinstance(e, redis.exceptions.ReadOnlyError):
            logger.error('Can not write against a read only replica. Try to reconnect.')
            return True
        else:
            logger.error('Unexpected error type: {}'.format(type(e)))
            logger.error('Error message: {}'.format(e))
            return False

    @retry(stop_max_delay=5000, retry_on_exception=_chk_error)
    def _r_set(self, key, value):
        self.logger.info('set start')
        self.logger.debug(self.redis_client.set(key, value))
        self.logger.info('set end')

    @retry(stop_max_delay=5000, retry_on_exception=_chk_error)
    def _r_get(self, key):
        self.logger.info('get start')
        self.logger.debug(self.redis_client.get(key))
        self.logger.info('get end')

    @retry(stop_max_delay=5000, retry_on_exception=_chk_error)
    def _r_delete(self, key):
        self.logger.info('delete start')
        self.logger.debug(self.redis_client.delete(key))
        self.logger.info('delete end')

    def repeat_connection(self, key):
        while True:
            try:
                self._r_set(key=key, value='value1')
                self._r_get(key=key)
                self._r_delete(key=key)
                if self.interval < time.time():
                    self.logger.info('time up')
                    break
            except Exception as e:
                self.logger.error('Catch exception')
                self.logger.error(e)
                break


if __name__ == '__main__':
    r = RepeatConnectionToRedis(
        host=os.environ.get('REDIS_HOST', '127.0.0.1'),
        interval=int(os.environ.get('INTERVAL', '30'))
    )
    r.repeat_connection(key='key1')

これをDockerに入れて回します。主な仕様は以下の通りです。

  • 指定した時間分(秒単位)で対象ホストにアクセスし続ける
  • プライマリエンドポイントへのアクセス
  • 同じキーを保存、参照、削除を繰り返す
  • どこかでエラー(ConnectionErrorかReadOnlyError)が発生すると5秒間ほどリトライ処理を行う

通信のレイテンシもあるので一概には言えませんがログの間隔を確認する限り大凡2〜3msに1回はアクセスしに行っているようです。なお、本検証ではコンテナの数は1つで行い、15分間ほどアクセスし続ける設定にしました。

準備

以下のコマンドでECRのレポジトリを作成します。

$ aws ecr create-repository --repository-name repeat_connection_to_redis

コンテナを作成します。

$ docker build -t 123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/repeat_connection_to_redis:latest .

なお、Dockerfileは以下のように定義しました。

FROM python:3.8.1-alpine
ENV REDIS_HOST='redis-scale-test.m6rlwr.ng.0001.apne1.cache.amazonaws.com'
ENV INTERVAL='900'
COPY repeat_connection_to_redis.py /opt/
WORKDIR /opt
RUN pip install redis retrying

今回は実行環境にECSのFargateを採用することにしました。まずはクラスターを作成します。

$ aws ecs create-cluster --cluster-name sample-fargate-cluster

タスクを登録します。

$ aws ecs register-task-definition --cli-input-json file://task_definition.json

タスク定義は以下のように定義しました。

{
    "family": "repeat-connection-to-redis", 
    "networkMode": "awsvpc", 
    "containerDefinitions": [
        {
            "name": "repeat-connection-to-redis", 
            "image": "123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/repeat_connection_to_redis:latest", 
            "memory": 1024,
        "cpu": 512,
        "workingDirectory": "/opt",
        "entryPoint": [
            "python",
            "repeat_connection_to_redis.py"
        ],
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-group": "fargate-runtask-app-logs",
                "awslogs-region": "ap-northeast-1",
                "awslogs-stream-prefix": "repeat-connection-to-redis"
            }
        }
        }
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ], 
    "cpu": "1024",
    "memory": "2048",
    "executionRoleArn": "arn:aws:iam::123456789123:role/ecsTaskExecutionRole"
}

ネットワーク構成をnetwork_configuration.jsonというファイルに定義します。コンテナはPublic Subnet上で実行します。

{
  "awsvpcConfiguration": {
    "subnets": ["subnet ID", "subnet ID"],
    "securityGroups": ["Security Group ID"],
    "assignPublicIp": "ENABLED"
  }
}

これで以下のコマンドを実行することでいつでもタスクが実行できるようになりました。

aws ecs run-task \
    --task-definition repeat-connection-to-redis:1 \
    --cluster sample-fargate-cluster \
    --network-configuration file://network_configuration.json \
    --launch-type FARGATE

ElastiCacheのスケールアップ

まずは以下のコマンドで対象のクラスターがどのノードタイプにスケールアップ可能か調べます。現状はcache.t3.microで構築してあります。

$ aws elasticache list-allowed-node-type-modifications \
    --replication-group-id redis-scale-test
{
    "ScaleUpModifications": [
        "cache.m5.2xlarge",
        "cache.m5.4xlarge",
        "cache.m5.large",
        "cache.m5.xlarge",
        "cache.r5.12xlarge",
        "cache.r5.24xlarge",
        "cache.r5.2xlarge",
        "cache.r5.4xlarge",
        "cache.r5.large",
        "cache.r5.xlarge",
        "cache.t3.medium",
        "cache.t3.small"
    ]
}

今回はcache.t3.smallへのスケールアップを行ってみます。スケールアップには以下のコマンドを使用します。

$ aws elasticache modify-replication-group  \
        --replication-group-id redis-scale-test \
        --cache-node-type cache.t3.small \      
        --apply-immediately

--apply-immediatelyオプションを付与することで即時アップデート適用となります。ちなみに次のメンテナンス期間に延期するには、--no-apply-immediatelyを使用します。

実行結果

最後にログを確認すると1件だけエラーがありました。

$ aws logs filter-log-events \
  --log-group-name "fargate-runtask-app-logs" \
  --log-stream-names "repeat-connection-to-redis/repeat-connection-to-redis/streamid" \
  --filter-pattern "ERROR"
{
    "events": [
        {
〜〜〜
            "message": "2020-01-22 05:30:13,637 _chk_error ERROR :Can not write against a read only replica. Try to reconnect.",
〜〜〜
        }
    ],
    "searchedLogStreams": [
〜〜〜
    ]
}

このエラーはリーダエンドポイントに対して書き込み処理を行おうとした際に発生します。実は今回検証したオンラインでのスケールアップは変更命令を受け付けたノードタイプクラスターを作成してエンドポイントを入れ替えてスケールアップを行っています。

そのためアクセス数が多い状態だとやはり多少の影響は起きやすいです。上記の公式ドキュメントにも以下のように記載があります。

データトラフィックが最小になると予想される時間帯にスケールアップ/ダウンを開始することをお勧めします。
可能であれば、ステージング環境でのスケーリング中にアプリケーションの動作をテストします。

さらにエラーが発生した前後のログを調査するとレイテンシが発生していることが分かりました。

$ aws logs filter-log-events \
  --log-group-name "fargate-runtask-app-logs" \
  --log-stream-names "repeat-connection-to-redis/repeat-connection-to-redis/streamid" \
  --start-time 1579671013000 \
  --end-time 1579671013770
{
    "events": [
        {
〜〜〜
            "message": "2020-01-22 05:30:13,089 _r_delete INFO :delete end",
〜〜〜
        },
        {
〜〜〜
            "message": "2020-01-22 05:30:13,089 _r_set INFO :set start",
〜〜〜
        },
        {
〜〜〜
            "message": "2020-01-22 05:30:13,637 _chk_error ERROR :Can not write against a read only replica. Try to reconnect.",
〜〜〜
        },
        {
〜〜〜
            "message": "2020-01-22 05:30:13,637 _r_set INFO :set start",
〜〜〜
        },
        {
〜〜〜
            "message": "2020-01-22 05:30:13,696 _r_set DEBUG :True",
〜〜〜
        },
        {
〜〜〜
            "message": "2020-01-22 05:30:13,696 _r_set INFO :set end",
〜〜〜
        },
    ],
    "searchedLogStreams": [
〜〜〜
    ]
}

流石に1秒もかかりませんでしたが、2〜3ミリ秒でアクセスできていたのに対してエラーが発生した箇所で550ミリ秒ほどのレイテンシ、さらにエラーが出た後も60ミリ秒ほどのレイテンシが発生しました。ミリ秒単位で観測するとわずかにリクエストに影響があるようです。

最後に

いかがでしたでしょうか!確かに一瞬エラーが発生したりレイテンシが発生したりという事象を今回観測しましたが、逆にこれだけのアクセス数の中、非クラスター構成で垂直スケール(スケールアップ)を行ってこれしか影響が出ないことに僕は驚いています。先日同じく非クラスター構成でリーダエンドポイントを検証したのですが、非常に高性能でした。これからは積極的に非クラスター構成の検討を進めて良いかと思います!!

もしオンラインスケールをご検討されている方がいらっしゃいましたら、本ブログを参考にしつつ、システム全体のサービスで考えた時このダウンタイムがどれほどの影響になるかを併せてご検討されることを推奨します。公式ドキュメントに記載があるように、可能であれば極力本番と同じような構成で負荷試験等のスループット系の試験をしてからオンラインスケールを実行されると良いかと思います!

以上、深澤(@shun_quartet)でした!