Я инженер по нагрузочному тестированию и не так давно работаю над проектом, где предполагается активное использование Apache Kafka. Из-за режима удаленной работы получение доступа к тестовому окружению затянулось на недели. Чтобы не терять время я решил развернуть локальный стенд в Kubernetes.


Кто работал с Apache Kafka подтвердит, что официальная документация покрывает далеко не все тонкости инсталляции и настройки. Я надеюсь, что данная пошаговая инструкция позволит вам сократить время развертывания тестового окружения. Обращаю внимание на то, что установка stateful в контейнерах — далеко не лучшая идея, поэтому данная инструкция не предназначена для развертывания промышленного стенда.


Инструкция описывает создание виртуальной машины в VirtualBox, установку и настройку операционной системы, установку Docker, Kubernetes и системы мониторинга. В Kubernetes развертывается два кластера Apache Kafka: "production" и "backup". Для репликации сообщений из production в backup используется MirrorMaker 2.0. Взаимодействие между узлами production кластера защищено TLS. К сожалению, не могу выложить в Git скрипт для генерирования сертификатов. В качестве примера можете использовать сертификаты из архива certs/certs.tar.gz. В самом конце инструкции описывается развертывание кластера Jmeter и запуск тестового сценария.


Исходники доступны в репозитории: github.com/kildibaev/k8s-kafka


Инструкция расcчитана на новичков в Kubernetes, поэтому если вы уже имеете опыт работы с контейнерами, то можете сразу перейти в раздел "12. Разворачиваем кластер Apache Kafka".


Q&A:


  • Почему используется Ubuntu? Изначально я развернул Kubernetes в CentOS 7, но после одного из обновлений окружение перестало работать. К тому же я заметил, что в CentOS нагрузочные тесты, запущенные в Jmeter, ведут себя непредсказуемо. Если сталкивались, пожалуйста, напишите в комментариях возможное решение этой проблемы. В Ubuntu всё намного стабильнее.
  • Почему не k3s или MicroK8s? Если коротко, ни k3s, ни MicroK8s из коробки не умеют работать с локальным Docker-репозиторием.
  • Почему не оптимизированы параметры конфигурации? Я намеренно использовал параметры по умолчанию где это возможно.
  • Почему Flannel? Я новичок в kubernetes и Flannel — единственный плагин, который мне удалось завести без проблем.
  • Почему Docker, а не CRI-O? Мне интересен CRI-O и я планирую изучить его в будущем.
  • Почему MirrorMaker 2.0 развернут в Kafka Connect? Kafka Connect позволяет редактировать параметры конфигурации MirrorMaker 2.0 "на лету" через REST API.

Оглавление


1. Создание виртуальной машины


2. Установка Ubuntu Server 20.04


3. Настройка Ubuntu


4. Установка Docker


5. Настройка iptables


6. Установка kubeadm, kubelet и kubectl


7. Разворачиваем кластер Kubernetes


8. Устанавливаем Flannel


9. Разрешаем запуск pod-ов на ноде control-plane


10. Добавляем алиас для команды kubectl


11. Устанавливаем Prometheus, Grafana, Alert Manager и Node Exporter


12. Разворачиваем кластер Apache Kafka


12.1. Запускаем Apache Zookeeper


12.2. Запускаем Apache Kafka


13. Проверяем отправку и получение сообщений


13.1. Запускаем генератор сообщений


13.2. Запускаем получателя сообщений


14. Настраиваем репликацию сообщений с помощью MirrorMaker 2.0


14.1. Запуск MirrorMaker 2.0 как конектор в кластере Kafka Connect


14.2. Проверяем репликацию сообщений


15. Выполнение сценариев Jmeter


16. Удаление данных




1. Создание виртуальной машины


Виртуальной машине должно быть доступно как минимум 2 ядра ЦПУ и 6-8 ГБ оперативной памяти. Если нет возможности выделить достаточный объем оперативной памяти для виртуальной машины, то посмотрите в сторону Rancher K3S.


Показать скриншоты







2. Установка Ubuntu Server 20.04


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


  • IP адрес, который будет присвоен ноде (по умолчанию 10.0.2.15);
  • Kubernetes требует, чтобы swap был отключен, поэтому дисковые разделы необходимо создать вручную;
  • В самом конце процесса установки можно выбрать опцию "Install OpenSSH server".

Показать скриншоты














3. Настройка Ubuntu


3.1. Отключить Firewall


sudo ufw disable

3.2. Отключить Swap


sudo swapoff -a

sudo sed -i 's/^\/swap/#\/swap/' /etc/fstab

3.3. Установить OpenJDK


В поставку OpenJDK входит утилита keytool, которая понадобится для генерирования сертификатов:


sudo apt install openjdk-8-jdk-headless

3.4. Авторизация по ключу (опционально)


Подробная инструкция от DigitalOcean:
https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys-on-ubuntu-20-04-ru


4. Установка Docker [источник]


# Switch to the root user 
sudo su

# (Install Docker CE)
## Set up the repository:
### Install packages to allow apt to use a repository over HTTPS
apt-get update && apt-get install -y   apt-transport-https ca-certificates curl software-properties-common gnupg2

# Add Docker’s official GPG key:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -

# Add the Docker apt repository:
add-apt-repository   "deb [arch=amd64] https://download.docker.com/linux/ubuntu   $(lsb_release -cs)   stable"

# Install Docker CE
apt-get update && apt-get install -y   containerd.io=1.2.13-2   docker-ce=5:19.03.11~3-0~ubuntu-$(lsb_release -cs)   docker-ce-cli=5:19.03.11~3-0~ubuntu-$(lsb_release -cs)

# Set up the Docker daemon
cat > /etc/docker/daemon.json <<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF

mkdir -p /etc/systemd/system/docker.service.d

# Restart Docker
systemctl daemon-reload
systemctl restart docker

# If you want the docker service to start on boot, run the following command:
sudo systemctl enable docker

5. Настройка iptables [источник]


cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF

sudo sysctl --system

6. Установка kubeadm, kubelet и kubectl [источник]


sudo apt-get update && sudo apt-get install -y apt-transport-https curl

curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -

cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF

sudo apt-get update

sudo apt-get install -y kubelet kubeadm kubectl

sudo apt-mark hold kubelet kubeadm kubectl

7. Разворачиваем кластер Kubernetes


Запускаем инициализацию ноды control-plane: [источник]


# Pulling images required for setting up a Kubernetes cluster
# This might take a minute or two, depending on the speed of your internet connection
sudo kubeadm config images pull

# Initialize a Kubernetes control-plane node
sudo kubeadm init --pod-network-cidr=10.244.0.0/16

Следующие команды необходимо выполнить под обычным пользователем (не root): [источник]


mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

8. Устанавливаем Flannel [источник]


kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

9. Разрешаем запуск pod-ов на ноде control-plane [источник]


Так как наш кластер Kubernetes развернут в режиме standalone, то необходимо разрешить запуск pod-ов на ноде control-plane:


kubectl taint nodes --all node-role.kubernetes.io/master-

10. Добавляем алиас для команды kubectl [источник]


alias k='kubectl'

echo "alias k='kubectl'" >> ~/.bashrc

11. Устанавливаем Prometheus, Grafana, Alert Manager и Node Exporter


Устанавливаем kube-prometheus: [источник]


curl -O -L https://github.com/coreos/kube-prometheus/archive/master.zip

sudo apt install -y unzip

unzip master.zip

cd kube-prometheus-master

kubectl create -f manifests/setup

kubectl create -f manifests/

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


kubectl get pods -w -n monitoring

Для сбора метрик Kafka и Zookeeper будем использовать JMX Exporter. Чтобы Prometheus получил доступ к экспортируемым метрикам необходимо добавить ServiceMonitor:


k apply -f https://raw.githubusercontent.com/kildibaev/k8s-kafka/master/servicemonitor/jmx-exporter-servicemonitor.yaml

Создадим сервис, чтобы получить доступ к веб-интерфейсу Grafana из виртуальной машины:


kubectl apply -f https://raw.githubusercontent.com/kildibaev/k8s-kafka/master/service/grafana-svc.yaml

После запуска сервиса Grafana будет доступна из гостевой системы по адресу http://localhost:32000


Выполним проброс порта, чтобы получить доступ к Grafana на хост машине:



Теперь веб-интерфейс Grafana доступен на хост машине по адресу http://127.0.0.1:3000


Для просмотра метрик в Grafana можете воспользоваться готовым дашбордом. Для этого перейдите на страницу http://127.0.0.1:3000/dashboard/import и в поле "Import via panel json" скопируйте содержимое файла grafana-dashboard.json


12. Разворачиваем кластер Apache Kafka


# Скачиваем содержимое репозитория
git clone https://github.com/kildibaev/k8s-kafka.git $HOME/k8s-kafka
cd $HOME/k8s-kafka

12.1. Запускаем Apache Zookeeper


Чтобы получить предсказуемые имена хостов воспользуемся Statefulset. В нашем случае кластер Apache Zookeeper будет состоять из трех инстансов: zookeeper-0.zookeeper, zookeeper-1.zookeeper и zookeeper-2.zookeeper


# Собираем образ zookeeper-base
sudo docker build -t zookeeper-base:local-v1 -f dockerfile/zookeeper-base.dockerfile .

# Запускаем сервис Zookeeper
k apply -f service/zookeeper-svc.yaml

# Запускаем кластер Apache Zookeeper
k apply -f statefulset/zookeeper-statefulset.yaml

# Перед выполнением следующих шагов необходимо дождаться когда все три pod-а перейдут в статус Running. Проверить состояние pod-ов можно следующей командой:
k get pods -w

12.2. Запускаем Apache Kafka


Кластер Apache Kafka будет состоять из двух брокеров: kafka-0.kafka и kafka-1.kafka


# Собираем образ kafka-base
sudo docker build -t kafka-base:local-v1 -f dockerfile/kafka-base.dockerfile .

# Запускаем сервис Kafka
k apply -f service/kafka-svc.yaml

# Запускаем кластер Apache Kafka
k apply -f statefulset/kafka-statefulset.yaml

# Перед выполнением следующих шагов необходимо дождаться когда оба pod-а перейдут в статус Running. Проверить состояние pod-ов можно следующей командой:
k get pods -w

13. Проверяем отправку и получение сообщений


13.1. Запускаем генератор сообщений


Запустим генератор сообщений, который будет отправлять 10 сообщений в секунду. Размер каждого сообщения составляет 100 байт. В общей сложности будет отправлено 30000 сообщений.


# Запускаем новый pod - producer
k run --rm -i --tty producer --image=kafka-base:local-v1 -- bash

# Создаем топик topicname и отправляем в него сообщения
bin/kafka-producer-perf-test.sh    --topic topicname    --num-records 30000    --record-size 100    --throughput 10    --producer.config /config/client.properties    --producer-props acks=1    bootstrap.servers=kafka-0.kafka:9092,kafka-1.kafka:9092    buffer.memory=33554432    batch.size=8196

13.2. Запускаем получателя сообщений


# Запускаем новый pod - consumer
k run --rm -i --tty consumer --image=kafka-base:local-v1 -- bash 

# Получаем сообщения из топика topicname
bin/kafka-consumer-perf-test.sh --broker-list kafka-0.kafka:9092,kafka-1.kafka:9092 --consumer.config /config/client.properties --messages 30000 --topic topicname --threads 2

14. Настраиваем репликацию сообщений с помощью MirrorMaker 2.0


14.1. Запуск MirrorMaker 2.0 как конектор в кластере Kafka Connect


Кластер Apache Kafka, который мы запустили ранее, условно назовем production. Поднимем еще один pod, в котором будет запущено три контейнера: Apache Zookeeper, Apache Kafka и Kafka Connect. Новый инстанс Apache Kafka назовем backup и настроим репликацию сообщений из production в backup.


k apply -f service/mirrormaker-svc.yaml

# Поднимем pod, в котором будет запущено три контейнера: Apache Zookeeper, Apache Kafka и Kafka Connect
k apply -f statefulset/mirrormaker-statefulset.yaml

# Необходимо дождаться когда pod с именем mirrormaker-0 перейдет в статус Running
k get pods -w

# Подключимся к контейнеру connect в pod-е mirrormaker-0 и откроем командную строку
k exec -ti mirrormaker-0 -c connect -- bash

# В кластере Kafka Connect создадим коннектор MirrorMaker 2.0
curl -X POST -H "Content-Type: application/json" mirrormaker-0.mirrormaker:8083/connectors -d '{
    "name": "MirrorSourceConnector",
    "config": {
      "connector.class": "org.apache.kafka.connect.mirror.MirrorSourceConnector",
      "source.cluster.alias": "production",
      "target.cluster.alias": "backup",
      "source.cluster.bootstrap.servers": "kafka-0.kafka:9092,kafka-1.kafka:9092",
      "source.cluster.group.id": "mirror_maker_consumer",
      "source.cluster.enable.auto.commit": "true",
      "source.cluster.auto.commit.interval.ms": "1000",
      "source.cluster.session.timeout.ms": "30000",
      "source.cluster.security.protocol": "SSL",
      "source.cluster.ssl.truststore.location": "/certs/kafkaCA-trusted.jks",
      "source.cluster.ssl.truststore.password": "kafkapilot",
      "source.cluster.ssl.truststore.type": "JKS",
      "source.cluster.ssl.keystore.location": "/certs/kafka-consumer.jks",
      "source.cluster.ssl.keystore.password": "kafkapilot",
      "source.cluster.ssl.keystore.type": "JKS",
      "target.cluster.bootstrap.servers": "localhost:9092",
      "target.cluster.compression.type": "none",
      "topics": ".*",
      "rotate.interval.ms": "1000",
      "key.converter.class": "org.apache.kafka.connect.converters.ByteArrayConverter",
      "value.converter.class": "org.apache.kafka.connect.converters.ByteArrayConverter"
    }
}'

14.2. Проверяем репликацию сообщений


Если репликация прошла успешно, то в backup будет создан топик production.topicname. Префикс ".production" MirrorMaker 2.0 добавит, чтобы избежать зацикливания, например, когда репликация настроена в режиме active-active.


# Запускаем новый pod - consumer
k exec -ti mirrormaker-0 -c kafka -- bash

# Получим список топиков 
bin/kafka-topics.sh --list --bootstrap-server mirrormaker-0.mirrormaker:9092

# Получаем сообщения из топика production.topicname
bin/kafka-console-consumer.sh --bootstrap-server mirrormaker-0.mirrormaker:9092 --topic production.topicname --from-beginning

Если топик production.topicname присутствует, но сообщения из него не считываются, проверьте логи Kafka Connect:


k logs mirrormaker-0 connect

Если в логах присутствуют следующие записи


ERROR WorkerSourceTask{id=MirrorSourceConnector-0} Failed to flush, timed out while waiting for producer to flush outstanding 1 messages (org.apache.kafka.connect.runtime.WorkerSourceTask:438)

ERROR WorkerSourceTask{id=MirrorSourceConnector-0} Failed to commit offsets (org.apache.kafka.connect.runtime.SourceTaskOffsetCommitter:114)

для решения проблемы можете уменьшить значение параметра producer.buffer.memory:


k exec -ti mirrormaker-0 -c connect -- bash

curl -X PUT -H "Content-Type: application/json" mirrormaker-0.mirrormaker:8083/connectors/MirrorSourceConnector/config -d '{
  "connector.class": "org.apache.kafka.connect.mirror.MirrorSourceConnector",
  "source.cluster.alias": "production",
  "target.cluster.alias": "backup",
  "source.cluster.bootstrap.servers": "kafka-0.kafka:9092,kafka-1.kafka:9092",
  "source.cluster.group.id": "mirror_maker_consumer",
  "source.cluster.enable.auto.commit": "true",
  "source.cluster.auto.commit.interval.ms": "1000",
  "source.cluster.session.timeout.ms": "30000",
  "source.cluster.security.protocol": "SSL",
  "source.cluster.ssl.truststore.location": "/certs/kafkaCA-trusted.jks",
  "source.cluster.ssl.truststore.password": "kafkapilot",
  "source.cluster.ssl.truststore.type": "JKS",
  "source.cluster.ssl.keystore.location": "/certs/kafka-consumer.jks",
  "source.cluster.ssl.keystore.password": "kafkapilot",
  "source.cluster.ssl.keystore.type": "JKS",
  "target.cluster.bootstrap.servers": "localhost:9092",
  "target.cluster.compression.type": "none",
  "topics": ".*",
  "rotate.interval.ms": "1000",
  "producer.buffer.memory:" "1000",
  "key.converter.class": "org.apache.kafka.connect.converters.ByteArrayConverter",
  "value.converter.class": "org.apache.kafka.connect.converters.ByteArrayConverter"
}'

15. Выполнение сценариев Jmeter


# Собираем образ jmeter-base
sudo docker build -t jmeter-base:local-v1 -f dockerfile/jmeter-base.dockerfile .

# Запускаем сервис Jmeter
k apply -f service/jmeter-svc.yaml

# Запускаем четыре pod-а Jmeter, которые будут непосредственно генерировать нагрузку
k apply -f statefulset/jmeter-statefulset.yaml

Создадим pod jmeter-producer и выполним в нем сценарий producer.jmx


k run --rm -i --tty jmeter-producer --image=jmeter-base:local-v1 -- bash ./jmeter -n -t /tests/producer.jmx -r -Jremote_hosts=jmeter-0.jmeter:1099,jmeter-1.jmeter:1099

Создадим pod jmeter-consumer и выполним в нем сценарий consumer.jmx


k run --rm -i --tty jmeter-consumer --image=jmeter-base:local-v1 -- bash ./jmeter -n -t /tests/consumer.jmx -r -Jremote_hosts=jmeter-2.jmeter:1099,jmeter-3.jmeter:1099

16. Удаление данных


Удаляем statefulset


k delete statefulset jmeter zookeeper kafka mirrormaker

Удаляем контейнеры


sudo docker rmi -f zookeeper-base:local-v1 kafka-base:local-v1 jmeter-base:local-v1

Удаляем сервисы


k delete svc grafana jmeter kafka mirrormaker zookeeper
k delete servicemonitor jmxexporter

linkedin: kildibaev
Всем добра! Не болейте.