Перейти к содержимому

Миграция данных между кластерами Managed Kafka

Вы можете перенести данные из исходного кластера Managed Kafka в целевой с помощью инструмента Apache Kafka MirrorMaker 2 (MM2). Исходный кластер остается доступен для записи во время миграции.

Чтобы мигрировать данные:

Если вам больше не нужны созданные ресурсы, удалите их.

  1. Создайте пользователя в исходном кластере для чтения данных, а в целевом — для записи.

  2. Создайте сервисный аккаунт sa-kafka-mm2 с ролью mkafka.editor. Аккаунт понадобится для создания топиков в целевом кластере.

  3. Создайте виртуальную машину в той же сети, где расположены оба кластера. При создании ВМ привяжите к ней сервисный аккаунт sa-kafka-mm2. В зависимости от количества переносимых разделов и интенсивности потока данных выберите конфигурацию ВМ:

    ПараметрМинимальная конфигурацияРекомендуемая конфигурация
    vCPU2от 4
    RAM4 ГБот 8 ГБ
    Диск10 ГБот 20 ГБ, с запасом под логи
  4. Подготовьте данные для подключений:

    • адреса брокеров исходного и целевого кластеров;
    • имена и пароли пользователей исходного и целевого кластеров;
    • IP-адрес ВМ.
  1. Установите необходимые пакеты:

    bash
    sudo apt update && \
    sudo apt install -y \
    curl \
    wget \
    tar \
    gzip \
    ca-certificates \
    netcat-openbsd \
    dnsutils \
    jq
  2. Установите Java 17:

    bash
    sudo apt install -y openjdk-17-jre-headless

    Проверьте успешность установки:

    bash
    java -version
  3. Создайте системного пользователя kafka-mm2 и каталоги для него:

    bash
    sudo useradd \
    --system \
    --home /opt/kafka \
    --shell /usr/sbin/nologin \
    kafka-mm2 && \
    sudo mkdir -p /opt/kafka && \
    sudo mkdir -p /etc/kafka-mm2 && \
    sudo mkdir -p /opt/kafka-mm2/scripts
  4. Установите Apache Kafka версии не ниже, чем в кластере:

    bash
    cd /tmp && \
    wget https://downloads.apache.org/kafka/4.1.2/kafka_2.13-4.1.2.tgz && \
    tar -xzf kafka_2.13-4.1.2.tgz && \
    sudo mv kafka_2.13-4.1.2 /opt/kafka/ && \
    sudo ln -s /opt/kafka/kafka_2.13-4.1.2 /opt/kafka/current && \
    sudo chown -R kafka-mm2:kafka-mm2 /opt/kafka

    Проверьте успешность установки:

    bash
    ls -l /opt/kafka/current/bin/connect-mirror-maker.sh && \
    ls -l /opt/kafka/current/bin/kafka-topics.sh
    Примечание

    Запускать Kafka Broker на ВМ не нужно: Apache Kafka устанавливается для получения CLI-скриптов и библиотек Kafka Connect.

  5. Установите корневой сертификат MWS — он понадобится для TLS-подключения к кластерам:

    bash
    cd /tmp && \
    wget https://pki.mws.ru/certs/mws-root-ca.crt && \
    sudo mv /tmp/mws-root-ca.crt /etc/kafka-mm2/mws-root-ca.crt && \
    sudo chown kafka-mm2:kafka-mm2 /etc/kafka-mm2/mws-root-ca.crt && \
    sudo chmod 644 /etc/kafka-mm2/mws-root-ca.crt
  6. Проверьте доступность кластеров с ВМ. Порт подключения из внутренней сети — 9095:

    bash
    nc -vz <адрес брокера исходного кластера> 9095 && \
    nc -vz <адрес брокера целевого кластера> 9095

    Ожидаемый результат: обе команды завершаются сообщением succeeded.

Примечание

Все последующие шаги руководства выполняйте на виртуальной машине.

Файлы конфигурации содержат параметры подключения к кластеру: аутентификацию SASL_SSL с механизмом SCRAM-SHA-256 и корневой сертификат MWS.

  1. Создайте файл /etc/kafka-mm2/source-client.properties со следующим содержимым:

    properties
    security.protocol=SASL_SSL
    sasl.mechanism=SCRAM-SHA-256
    ssl.truststore.type=PEM
    ssl.truststore.location=/etc/kafka-mm2/mws-root-ca.crt
    sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="<имя пользователя исходного кластера>" password="<пароль>";
    ssl.endpoint.identification.algorithm=
  2. Создайте файл /etc/kafka-mm2/target-client.properties со следующим содержимым:

    properties
    security.protocol=SASL_SSL
    sasl.mechanism=SCRAM-SHA-256
    ssl.truststore.type=PEM
    ssl.truststore.location=/etc/kafka-mm2/mws-root-ca.crt
    sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="<имя пользователя целевого кластера>" password="<пароль>";
    ssl.endpoint.identification.algorithm=
  3. Задайте права на файлы конфигурации:

    bash
    sudo chown kafka-mm2:kafka-mm2 \
    /etc/kafka-mm2/source-client.properties \
    /etc/kafka-mm2/target-client.properties && \
    sudo chmod 600 \
    /etc/kafka-mm2/source-client.properties \
    /etc/kafka-mm2/target-client.properties
  4. Проверьте подключение к исходному кластеру:

    bash
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-topics.sh \
    --bootstrap-server <адрес брокера исходного кластера> \
    --command-config /etc/kafka-mm2/source-client.properties \
    --list

    Ожидаемый результат: список топиков исходного кластера.

  5. Проверьте подключение к целевому кластеру:

    bash
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-topics.sh \
    --bootstrap-server <адрес брокера целевого кластера> \
    --command-config /etc/kafka-mm2/target-client.properties \
    --list

    Ожидаемый результат: список топиков целевого кластера.

    Примечание

    Если подключение завершается ошибкой PKIX path building failed, проверьте, что существует файл сертификата MWS /etc/kafka-mm2/mws-root-ca.crt, а в файлах конфигурации указаны параметры ssl.truststore.type=PEM и ssl.truststore.location=/etc/kafka-mm2/mws-root-ca.crt.

Топики целевого кластера создаются через API Managed Kafka. Чтобы выполнить запросы, получите IAM-токен сервисного аккаунта sa-kafka-mm2 и проверьте доступ к API с помощью этого токена:

  1. Убедитесь, что привязка сервисного аккаунта к ВМ завершена. Привязка может занять несколько минут:

    bash
    curl -s \
    -H "Metadata-Flavor: Google" \
    http://169.254.169.254/computeMetadata/v1/instance/service-accounts/?recursive=true \
    | jq

    Ожидаемый вывод:

    json
    {
    "<имя проекта>/serviceAccounts/sa-kafka-mm2": {
    "aliases": [
    "default"
    ]
    }
    }
  2. Получите IAM-токен сервисного аккаунта:

    bash
    TOKEN="$(
    curl -s \
    -H "Metadata-Flavor: Google" \
    http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token \
    | jq -r '.access_token'
    )"

    MWS автоматически обновляет токен в метаданных ВМ.

  3. Задайте имя проекта и получите список кластеров:

    bash
    export MWS_PROJECT='<имя проекта>' && \
    curl -sS \
    -H "Authorization: Bearer ${TOKEN}" \
    "https://mkafka.mwsapis.ru/mkafka/v1/projects/${MWS_PROJECT}/clusters" \
    | jq -r '
    .items[] |
    [
    (.metadata.id | split("/") | last),
    .metadata.displayName
    ] | @tsv
    '

    Пример вывода:

    text
    cluster-source-b Source Kafka
    cluster-target-b Target Kafka
  4. Задайте переменную с техническим именем целевого кластера:

    bash
    export MWS_TARGET_CLUSTER='<техническое имя целевого кластера>'

    В примере техническое имя кластера — cluster-target-b, а не Target Kafka (отображаемое имя).

    API Managed Kafka использует техническое имя кластера — последний сегмент из metadata.id. Если в запросе передать отображаемое имя, API вернет ошибку INVALID_ARGUMENT.

  5. Проверьте доступ к API Managed Kafka:

    bash
    curl -sS \
    -H "Authorization: Bearer ${TOKEN}" \
    "https://mkafka.mwsapis.ru/mkafka/v1/projects/${MWS_PROJECT}/clusters/${MWS_TARGET_CLUSTER}/topics" \
    | jq

    Ожидаемый результат: JSON-описание топиков целевого кластера.

MirrorMaker 2 в процессе работы попытается создать недостающие топики через Kafka Admin API. Пользователь кластера не имеет прав на создание топиков, поэтому такая операция завершится ошибкой TopicAuthorizationException.

Чтобы избежать этой ошибки, подготовьте топики целевого кластера одним из способов:

  • Скриптом
  • Через API

Скрипт sync-topics.sh создает в целевом кластере пользовательские топики исходного кластера, а также внутренние топики MM2, необходимые для миграции.

Используйте этот способ, чтобы перенести большое количество топиков.

  1. Создайте на ВМ файл /opt/kafka-mm2/scripts/sync-topics.sh и вставьте в него содержимое скрипта:

    bash
    #!/usr/bin/env bash
    set -euo pipefail
    API_BASE="https://mkafka.mwsapis.ru/mkafka/v1"
    METADATA_TOKEN_URL="http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token"
    METADATA_SA_URL="http://169.254.169.254/computeMetadata/v1/instance/service-accounts/?recursive=true"
    : "${MWS_PROJECT:?Set MWS_PROJECT}"
    : "${MWS_TARGET_CLUSTER:?Set MWS_TARGET_CLUSTER}"
    : "${SOURCE_BOOTSTRAP_SERVERS:?Set SOURCE_BOOTSTRAP_SERVERS}"
    SOURCE_CLIENT_CONFIG="${SOURCE_CLIENT_CONFIG:-/etc/kafka-mm2/source-client.properties}"
    KAFKA_TOPICS_BIN="${KAFKA_TOPICS_BIN:-/opt/kafka/current/bin/kafka-topics.sh}"
    SOURCE_TOPICS_INCLUDE_REGEX="${SOURCE_TOPICS_INCLUDE_REGEX:-.*}"
    SOURCE_TOPICS_EXCLUDE_REGEX="${SOURCE_TOPICS_EXCLUDE_REGEX:-^(__.*|mm2-.*[.]internal|.*[.]checkpoints[.]internal|heartbeats)$}"
    SOURCE_ALIAS="${SOURCE_ALIAS:-source}"
    REPLICATION_FACTOR="${REPLICATION_FACTOR:-1}"
    CONNECT_OFFSET_PARTITIONS="${CONNECT_OFFSET_PARTITIONS:-1}"
    CONNECT_STATUS_PARTITIONS="${CONNECT_STATUS_PARTITIONS:-1}"
    WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
    WATCH_INTERVAL_SECONDS="${WATCH_INTERVAL_SECONDS:-0}"
    for cmd in curl jq grep sed sort; do
    command -v "$cmd" >/dev/null 2>&1 || {
    echo "ERROR: required command '$cmd' is not installed." >&2
    exit 1
    }
    done
    [[ -x "$KAFKA_TOPICS_BIN" ]] || {
    echo "ERROR: kafka-topics.sh not found: $KAFKA_TOPICS_BIN" >&2
    exit 1
    }
    [[ -r "$SOURCE_CLIENT_CONFIG" ]] || {
    echo "ERROR: Source client config is not readable: $SOURCE_CLIENT_CONFIG" >&2
    echo "Run as kafka-mm2 or use SOURCE_CLIENT_CONFIG." >&2
    exit 1
    }
    TMP_RESPONSE="$(mktemp)"
    TMP_METADATA="$(mktemp)"
    trap 'rm -f "$TMP_RESPONSE" "$TMP_METADATA"' EXIT
    check_metadata_service() {
    if ! curl -fsS --connect-timeout 2 --max-time 5 -H "Metadata-Flavor: Google" "$METADATA_SA_URL" -o "$TMP_METADATA"; then
    echo "ERROR: cannot access MWS VM metadata service." >&2
    echo "Attach a service account to this MWS VM and grant it access to manage Managed Kafka topics." >&2
    return 1
    fi
    if ! jq -e 'type == "object" and length > 0' "$TMP_METADATA" >/dev/null 2>&1; then
    echo "ERROR: no service account is visible in VM metadata." >&2
    cat "$TMP_METADATA" >&2
    return 1
    fi
    }
    get_mws_token() {
    local response token
    response="$(
    curl -fsS --connect-timeout 2 --max-time 10 -H "Metadata-Flavor: Google" "$METADATA_TOKEN_URL"
    )" || {
    echo "ERROR: failed to obtain IAM token from MWS metadata service." >&2
    return 1
    }
    token="$(jq -er '.access_token | select(type=="string" and length>0)' <<<"$response")" || {
    echo "ERROR: metadata response does not contain access_token." >&2
    echo "$response" >&2
    return 1
    }
    printf '%s' "$token"
    }
    urlencode() {
    jq -rn --arg v "$1" '$v | @uri'
    }
    topic_url() {
    printf '%s/projects/%s/clusters/%s/topics/%s' "$API_BASE" "$(urlencode "$MWS_PROJECT")" "$(urlencode "$MWS_TARGET_CLUSTER")" "$(urlencode "$1")"
    }
    api_request() {
    local method="$1"
    local url="$2"
    local data="${3:-}"
    local token
    token="$(get_mws_token)"
    if [[ -n "$data" ]]; then
    curl -sS -o "$TMP_RESPONSE" -w "%{http_code}" -X "$method" "$url" -H "Authorization: Bearer ${token}" -H "Content-Type: application/json" -d "$data"
    else
    curl -sS -o "$TMP_RESPONSE" -w "%{http_code}" -X "$method" "$url" -H "Authorization: Bearer ${token}"
    fi
    }
    wait_topic_ready() {
    local topic="$1"
    local started="$SECONDS"
    while true; do
    local code state
    code="$(api_request GET "$(topic_url "$topic")")"
    if [[ "$code" != "200" ]]; then
    echo "ERROR: cannot read '$topic' while waiting (HTTP $code)." >&2
    cat "$TMP_RESPONSE" >&2
    return 1
    fi
    state="$(jq -r '.status.ready.state // empty' "$TMP_RESPONSE")"
    if [[ "$state" == "OK" ]]; then
    echo " ready: $topic"
    return 0
    fi
    if (( SECONDS - started >= WAIT_TIMEOUT_SECONDS )); then
    echo "ERROR: timeout waiting for '$topic'; state='${state:-unknown}'." >&2
    cat "$TMP_RESPONSE" >&2
    return 1
    fi
    sleep 2
    done
    }
    create_or_validate_topic() {
    local topic="$1"
    local partitions="$2"
    local config="${3:-}"
    local code
    code="$(api_request GET "$(topic_url "$topic")")"
    if [[ "$code" == "200" ]]; then
    local actual_partitions actual_rf
    actual_partitions="$(jq -r '.spec.partitions // empty' "$TMP_RESPONSE")"
    actual_rf="$(jq -r '.spec.replicationFactor // empty' "$TMP_RESPONSE")"
    if [[ "$actual_partitions" != "$partitions" ]]; then
    echo "ERROR: '$topic' partition count differs." >&2
    echo " expected/source: $partitions" >&2
    echo " target: ${actual_partitions:-unknown}" >&2
    return 1
    fi
    if [[ "$actual_rf" != "$REPLICATION_FACTOR" ]]; then
    echo "ERROR: '$topic' replication factor differs." >&2
    echo " expected: $REPLICATION_FACTOR" >&2
    echo " target: ${actual_rf:-unknown}" >&2
    return 1
    fi
    echo "exists: $topic (partitions=$actual_partitions rf=$actual_rf)"
    wait_topic_ready "$topic"
    return 0
    fi
    if [[ "$code" != "404" ]]; then
    echo "ERROR: failed to check '$topic' (HTTP $code)." >&2
    cat "$TMP_RESPONSE" >&2
    return 1
    fi
    local payload
    payload="$(
    jq -n --argjson partitions "$partitions" --argjson rf "$REPLICATION_FACTOR" --arg config "$config" '{
    spec: {
    partitions: $partitions,
    replicationFactor: $rf,
    config: $config
    }
    } | if $config == "" then del(.spec.config) else . end'
    )"
    echo "create: $topic (partitions=$partitions rf=$REPLICATION_FACTOR${config:+ config='$config'})"
    code="$(api_request POST "$(topic_url "$topic")" "$payload")"
    if [[ "$code" != "200" ]]; then
    echo "ERROR: failed to create '$topic' (HTTP $code)." >&2
    cat "$TMP_RESPONSE" >&2
    return 1
    fi
    wait_topic_ready "$topic"
    }
    source_topics() {
    "$KAFKA_TOPICS_BIN" --bootstrap-server "$SOURCE_BOOTSTRAP_SERVERS" --command-config "$SOURCE_CLIENT_CONFIG" --list | grep -E "$SOURCE_TOPICS_INCLUDE_REGEX" | grep -Ev "$SOURCE_TOPICS_EXCLUDE_REGEX" | sort -u || true
    }
    source_partition_count() {
    local topic="$1"
    local description count
    description="$(
    "$KAFKA_TOPICS_BIN" --bootstrap-server "$SOURCE_BOOTSTRAP_SERVERS" --command-config "$SOURCE_CLIENT_CONFIG" --describe --topic "$topic"
    )"
    count="$(
    printf '%s\n' "$description" | sed -n '1{s/.*PartitionCount:[[:space:]]*\([0-9][0-9]*\).*/\1/p;}'
    )"
    [[ -n "$count" ]] || {
    echo "ERROR: cannot determine PartitionCount for '$topic'." >&2
    echo "$description" >&2
    return 1
    }
    printf '%s' "$count"
    }
    create_internal_topics() {
    echo "== Kafka Connect internal topics =="
    create_or_validate_topic "mm2-configs.${SOURCE_ALIAS}.internal" 1 "cleanup.policy = compact"
    create_or_validate_topic "mm2-offsets.${SOURCE_ALIAS}.internal" "$CONNECT_OFFSET_PARTITIONS" "cleanup.policy = compact"
    create_or_validate_topic "mm2-status.${SOURCE_ALIAS}.internal" "$CONNECT_STATUS_PARTITIONS" "cleanup.policy = compact"
    echo
    echo "== MirrorMaker 2 internal topics =="
    create_or_validate_topic "mm2-offset-syncs.${SOURCE_ALIAS}.internal" 1 "cleanup.policy = compact"
    create_or_validate_topic "${SOURCE_ALIAS}.checkpoints.internal" 1 "cleanup.policy = compact"
    create_or_validate_topic "heartbeats" 1 "cleanup.policy = compact"
    }
    sync_user_topics() {
    echo
    echo "== Discover Source user topics =="
    echo "Source: $SOURCE_BOOTSTRAP_SERVERS"
    echo "Include: $SOURCE_TOPICS_INCLUDE_REGEX"
    echo "Exclude: $SOURCE_TOPICS_EXCLUDE_REGEX"
    mapfile -t topics < <(source_topics)
    if (( ${#topics[@]} == 0 )); then
    echo "No Source user topics matched."
    return 0
    fi
    echo "Matched ${#topics[@]} topic(s):"
    printf ' %s\n' "${topics[@]}"
    echo
    echo "== Synchronize destination topics =="
    local topic partitions
    for topic in "${topics[@]}"; do
    partitions="$(source_partition_count "$topic")"
    echo "source: $topic partitions=$partitions"
    create_or_validate_topic "$topic" "$partitions"
    done
    }
    echo "Checking MWS metadata service..."
    check_metadata_service
    get_mws_token >/dev/null
    echo "MWS service-account authentication: OK"
    echo
    echo "MWS project: $MWS_PROJECT"
    echo "Target cluster: $MWS_TARGET_CLUSTER"
    echo "MM2 source alias: $SOURCE_ALIAS"
    echo "Target RF: $REPLICATION_FACTOR"
    echo
    create_internal_topics
    while true; do
    sync_user_topics
    if (( WATCH_INTERVAL_SECONDS <= 0 )); then
    break
    fi
    echo
    echo "Next Source scan in ${WATCH_INTERVAL_SECONDS}s..."
    sleep "$WATCH_INTERVAL_SECONDS"
    done
    echo
    echo "Topic synchronization completed."
  2. Назначьте права на скрипт:

    bash
    sudo chown kafka-mm2:kafka-mm2 \
    /opt/kafka-mm2/scripts/sync-topics.sh && \
    sudo chmod 750 \
    /opt/kafka-mm2/scripts/sync-topics.sh
  3. Выполните предварительную синхронизацию топиков:

    bash
    sudo -u kafka-mm2 env \
    MWS_PROJECT='<имя проекта>' \
    MWS_TARGET_CLUSTER='<техническое имя целевого кластера>' \
    SOURCE_BOOTSTRAP_SERVERS='<адрес брокера исходного кластера>' \
    /opt/kafka-mm2/scripts/sync-topics.sh

    Ожидаемый результат: скрипт создает топики и завершается сообщением Topic synchronization completed.

  4. Проверьте топики целевого кластера:

    bash
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-topics.sh \
    --bootstrap-server <адрес брокера целевого кластера> \
    --command-config /etc/kafka-mm2/target-client.properties \
    --list

    Ожидаемый результат: список содержит все пользовательские топики исходного кластера.

  5. (опционально) Если во время миграции в исходном кластере появляются новые топики, запустите скрипт в режиме наблюдения. Скрипт будет периодически проверять исходный кластер и создавать новые топики в целевом:

    bash
    sudo -u kafka-mm2 env \
    MWS_PROJECT='<имя проекта>' \
    MWS_TARGET_CLUSTER='<техническое имя целевого кластера>' \
    SOURCE_BOOTSTRAP_SERVERS='<адрес брокера исходного кластера>' \
    WATCH_INTERVAL_SECONDS=30 \
    /opt/kafka-mm2/scripts/sync-topics.sh
  1. Создайте файл /etc/kafka-mm2/connect-mirror-maker.properties со следующим содержимым:

    properties
    ########################################
    # Кластеры
    ########################################
    clusters = source, target
    source.bootstrap.servers = <адрес брокера исходного кластера>
    target.bootstrap.servers = <адрес брокера целевого кластера>
    ########################################
    # Аутентификация в исходном кластере
    ########################################
    source.security.protocol = SASL_SSL
    source.sasl.mechanism = SCRAM-SHA-256
    source.sasl.jaas.config = org.apache.kafka.common.security.scram.ScramLoginModule required username="<имя пользователя исходного кластера>" password="<пароль>";
    source.ssl.truststore.type = PEM
    source.ssl.truststore.location = /etc/kafka-mm2/mws-root-ca.crt
    source.ssl.endpoint.identification.algorithm =
    ########################################
    # Аутентификация в целевом кластере
    ########################################
    target.security.protocol = SASL_SSL
    target.sasl.mechanism = SCRAM-SHA-256
    target.sasl.jaas.config = org.apache.kafka.common.security.scram.ScramLoginModule required username="<имя пользователя целевого кластера>" password="<пароль>";
    target.ssl.truststore.type = PEM
    target.ssl.truststore.location = /etc/kafka-mm2/mws-root-ca.crt
    target.ssl.endpoint.identification.algorithm =
    ########################################
    # Направление репликации
    ########################################
    source->target.enabled = true
    target->source.enabled = false
    ########################################
    # Топики
    ########################################
    source->target.topics = .*
    source->target.topics.exclude = __.*,mm2-.*[.]internal,.*[.]checkpoints[.]internal,heartbeats
    source->target.refresh.topics.enabled = true
    source->target.refresh.topics.interval.seconds = 30
    ########################################
    # Сохранение имен топиков
    ########################################
    replication.policy.class = org.apache.kafka.connect.mirror.IdentityReplicationPolicy
    ########################################
    # Настройки топиков
    ########################################
    source->target.sync.topic.configs.enabled = false
    source->target.sync.topic.acls.enabled = false
    replication.factor = 1
    ########################################
    # Синхронизация смещений
    ########################################
    source->target.emit.offset-syncs.enabled = true
    source->target.offset-syncs.topic.location = target
    offset-syncs.topic.replication.factor = 1
    ########################################
    # Контрольные точки
    ########################################
    source->target.emit.checkpoints.enabled = true
    checkpoints.topic.replication.factor = 1
    ########################################
    # Heartbeats
    ########################################
    source->target.emit.heartbeats.enabled = false
    target->source.emit.heartbeats.enabled = false
    source->target.heartbeats.replication.enabled = false
    heartbeats.topic.replication.factor = 1
    ########################################
    # Внутренние топики Kafka Connect
    ########################################
    offset.storage.partitions = 1
    status.storage.partitions = 1
    offset.storage.replication.factor = 1
    status.storage.replication.factor = 1
    config.storage.replication.factor = 1
    ########################################
    # Параллельность
    ########################################
    source->target.tasks.max = 4

    Значение .* для параметра source->target.topics включает все топики исходного кластера, а source->target.topics.exclude задает исключения и имеет приоритет над include-фильтром.

  2. Задайте права на файл конфигурации:

    bash
    sudo chown kafka-mm2:kafka-mm2 \
    /etc/kafka-mm2/connect-mirror-maker.properties && \
    sudo chmod 600 \
    /etc/kafka-mm2/connect-mirror-maker.properties

В этой конфигурации заданы следующие ключевые решения:

ПараметрЗначениеПричина
replication.policy.classIdentityReplicationPolicyИмена топиков переносятся без префикса исходного кластера
source->target.emit.heartbeats.enabledfalseДля миграции heartbeat-топики не нужны
source->target.sync.topic.configs.enabledfalseТопики целевого кластера создаются через API Managed Kafka
replication.factor1В примере кластер состоит из одного брокера
source->target.tasks.max4Количество параллельных задач репликации

Почему используется IdentityReplicationPolicy

По умолчанию MirrorMaker 2 добавляет к имени топика префикс исходного кластера: например, топик orders на целевом кластере изменит имя на source.orders. При миграции должны быть сохранены исходные имена топиков, поэтому применяется IdentityReplicationPolicy.

Почему отключены heartbeats

MirrorMaker 2 создает heartbeat-топики для отслеживания состояния репликации. В сочетании с IdentityReplicationPolicy он попытается создать топик source.heartbeats на целевом кластере и получит ошибку TopicAuthorizationException, т.к. MWS Cloud Platform не разрешает создание топиков через Kafka Admin API.

Для миграции данных heartbeat-механизм не обязателен, поэтому репликация heartbeat-топиков отключена параметрами source->target.emit.heartbeats.enabled, target->source.emit.heartbeats.enabled и source->target.heartbeats.replication.enabled.

Параметр heartbeats.replication.enabled по умолчанию равен true и заставляет MirrorSourceConnector реплицировать heartbeat-топик независимо от фильтра топиков.

Почему отключена синхронизация конфигураций и ACL

Топики целевого кластера создаются через API Managed Kafka, поэтому дополнительные административные операции через Kafka Admin API не нужны.

После проверки переноса данных сравните конфигурации топиков и перенесите необходимые настройки отдельно.

Почему replication.factor равен 1

В примере исходный и целевой кластеры состоят из одного брокера. Для кластера с несколькими брокерами задайте фактор репликации в соответствии с требованиями к отказоустойчивости.

Тот же принцип применяется к offset-syncs.topic.replication.factor, checkpoints.topic.replication.factor и параметрам хранилища Kafka Connect.

Перед первым запуском репликации убедитесь, что адреса брокеров не перепутаны: данные должны идти из исходного кластера в целевой, а обратная репликация отключена.

  1. Выполните команду:

    bash
    grep -nE \
    '^(clusters|source\.bootstrap\.servers|target\.bootstrap\.servers|source->target\.enabled|target->source\.enabled)' \
    /etc/kafka-mm2/connect-mirror-maker.properties

    Ожидаемый результат:

    text
    clusters = source, target
    source.bootstrap.servers = <адрес брокера исходного кластера>
    target.bootstrap.servers = <адрес брокера целевого кластера>
    source->target.enabled = true
    target->source.enabled = false
  1. Запустите MirrorMaker 2:

    bash
    sudo -u kafka-mm2 \
    env KAFKA_HEAP_OPTS="-Xms512m -Xmx1g" \
    /opt/kafka/current/bin/connect-mirror-maker.sh \
    /etc/kafka-mm2/connect-mirror-maker.properties \
    2>&1 | tee /tmp/mm2.log
  2. В отдельном терминале проверьте работу процесса:

    bash
    grep -E \
    'MirrorSourceTask|replicating|ERROR|WARN' \
    /tmp/mm2.log

    Ожидаемый результат:

    text
    MirrorSourceTask
    replicating 6 topic-partitions source->target:
    [orders-0, orders-1, orders-2, orders-3, orders-4, orders-5]
  1. Проверьте права записи в целевой кластер. Для этого отправьте тестовое сообщение:

    bash
    echo '{"probe":"direct-target-write"}' | \
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-console-producer.sh \
    --bootstrap-server <адрес брокера целевого кластера> \
    --producer.config /etc/kafka-mm2/target-client.properties \
    --topic <имя топика>

    Прочитайте его:

    bash
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-console-consumer.sh \
    --bootstrap-server <адрес брокера целевого кластера> \
    --consumer.config /etc/kafka-mm2/target-client.properties \
    --topic <имя топика> \
    --from-beginning \
    --max-messages 1

    Ожидаемый результат: {"probe":"direct-target-write"}.

  2. Проверьте перенос сообщений из исходного кластера:

    bash
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-console-consumer.sh \
    --bootstrap-server <адрес брокера целевого кластера> \
    --consumer.config /etc/kafka-mm2/target-client.properties \
    --topic <имя топика> \
    --from-beginning \
    --max-messages 5

    Ожидаемый результат: потребитель выводит сообщения, отправленные в исходный кластер.

  3. Проверьте прогресс миграции. Количество сообщений определяется разницей между последним и первым смещением: сообщения старше срока хранения удаляются из топика, поэтому одного последнего смещения недостаточно.

    bash
    TOPIC='<имя топика>'
    SOURCE_BOOTSTRAP='<адрес брокера исходного кластера>'
    TARGET_BOOTSTRAP='<адрес брокера целевого кластера>'
    SOURCE_CONFIG='/etc/kafka-mm2/source-client.properties'
    TARGET_CONFIG='/etc/kafka-mm2/target-client.properties'
    SOURCE_EARLIEST=$(
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-get-offsets.sh \
    --bootstrap-server "$SOURCE_BOOTSTRAP" \
    --command-config "$SOURCE_CONFIG" \
    --topic "$TOPIC" \
    --time -2 \
    | awk -F: '{sum += $3} END {print sum+0}'
    )
    SOURCE_LATEST=$(
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-get-offsets.sh \
    --bootstrap-server "$SOURCE_BOOTSTRAP" \
    --command-config "$SOURCE_CONFIG" \
    --topic "$TOPIC" \
    --time -1 \
    | awk -F: '{sum += $3} END {print sum+0}'
    )
    TARGET_EARLIEST=$(
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-get-offsets.sh \
    --bootstrap-server "$TARGET_BOOTSTRAP" \
    --command-config "$TARGET_CONFIG" \
    --topic "$TOPIC" \
    --time -2 \
    | awk -F: '{sum += $3} END {print sum+0}'
    )
    TARGET_LATEST=$(
    sudo -u kafka-mm2 \
    /opt/kafka/current/bin/kafka-get-offsets.sh \
    --bootstrap-server "$TARGET_BOOTSTRAP" \
    --command-config "$TARGET_CONFIG" \
    --topic "$TOPIC" \
    --time -1 \
    | awk -F: '{sum += $3} END {print sum+0}'
    )
    SOURCE_COUNT=$((SOURCE_LATEST - SOURCE_EARLIEST))
    TARGET_COUNT=$((TARGET_LATEST - TARGET_EARLIEST))
    LAG=$((SOURCE_COUNT - TARGET_COUNT))
    if [ "$SOURCE_COUNT" -gt 0 ]; then
    PROGRESS=$(
    awk \
    -v target="$TARGET_COUNT" \
    -v source="$SOURCE_COUNT" \
    'BEGIN { printf "%.2f", (target / source) * 100 }'
    )
    else
    PROGRESS="100.00"
    fi
    echo "Topic: $TOPIC"
    echo "Source: $SOURCE_COUNT"
    echo "Target: $TARGET_COUNT"
    echo "Lag: $LAG"
    echo "Progress: ${PROGRESS}%"

    Пример вывода скрипта подсчета:

    text
    Topic: mm2.test.orders
    Source: 1000000
    Target: 734521
    Lag: 265479
    Progress: 73.45%
  4. Остановите производителей в исходном кластере и дождитесь значения Progress: 100.00% — это будет означать, что миграция успешно завершена.

    Пока производители отправляют сообщения, значение прогресса будет постоянно изменяться.

text
SSL handshake failed
PKIX path building failed
unable to find valid certification path

Проверьте, что существует файл сертификата MWS /etc/kafka-mm2/mws-root-ca.crt, а в файлах конфигурации MM2 указаны параметры ssl.truststore.type=PEM и ssl.truststore.location=/etc/kafka-mm2/mws-root-ca.crt.

Проверьте параметры аутентификации в файле конфигурации MM2: security.protocol=SASL_SSL, sasl.mechanism=SCRAM-SHA-256 и учетные данные в sasl.jaas.config.

text
ConnectException:
Could not look up partition metadata for topic
'mm2-offsets.source.internal'

MM2 попытался создать внутренний топик через Kafka Admin API. Чтобы устранить ошибку, создайте топики в целевом кластере.

text
Could not create topic ...
TopicAuthorizationException:
Authorization failed

Создание топиков в Managed Kafka выполняется через API сервиса, а не через Kafka Admin API. Роль CLUSTER_ADMIN этого не меняет. Предварительно создайте топики в целевом кластере.

text
Could not create topic source.heartbeats

Отключите репликацию heartbeat-топиков. Для этого используйте корректную конфигурацию.

json
{
"code": "INVALID_ARGUMENT",
"description": "listKafkaTopics.cluster has invalid value"
}

В запрос передано отображаемое имя кластера вместо технического. Используйте техническое имя кластера.

Сначала проверьте имя топика. При IdentityReplicationPolicy имя топика не меняется: если в исходном кластере топик называется mm2.test.orders, то и в целевом он должен называться mm2.test.orders (но не orders).

Затем проверьте лог MirrorMaker 2:

bash
grep -E \
'MirrorSourceTask|replicating|ERROR|WARN' \
/tmp/mm2.log

Ресурсы, созданные в руководстве, тарифицируются. Если вы больше не планируете использовать их:

  1. Удалите целевой кластер Managed Kafka, если вы проходили руководство на тестовых данных.
Внимание

Не удаляйте кластер, в который перенесены реальные данные: восстановить данные после удаления невозможно.