Как я свой хобби-проект переносил на k8s

image

В этой статье я хотел бы рассказать о своем хобби-проекте поиска и классификации объявлений о сдаче квартир из социальной сети ВКонтакте и опыте его переезда на k8s.


Оглавление


  • Немного о проекте
  • Знакомство с k8s
  • Подготовка к переезду
  • Разработка конфигурации k8s
  • Разворачивание кластера k8s


Немного о проекте

img

В марте 2017 года я запустил сервис по парсингу и классификации объявлений о сдаче квартир из социальной сети ВКонтакте.
Вот тут можно подробнее прочитать о том, как я пытался классифицировать объявления разными способами и в итоге остановился на лексическом парсере Yandex Tomita Parser.
Вот тут можно почитать об архитектуре проекта на старте его существования и о том, какие технологии при этом использовались и почему.
Разработка первой версии сервиса заняла примерно год.
Для разворачивания каждого компонента сервиса я написал скрипты на Ansible.
Периодически сервис не работал из-за ошибок в переусложнённом коде или неверной настройки компонентов.
Примерно в июне 2019 года в коде парсера обнаружилась ошибка, из-за которой не собирались новые объявления. Вместо очередного исправления было принято решение временно его отключить.
Поводом для восстановления сервиса стало изучение k8s.


Знакомство с k8s

k8s — открытое программное обеспечение для автоматизации развёртывания, масштабирования контейнеризированных приложений и управления ими.
Вся инфраструктура сервиса описывается конфигурационными файлами в формате yaml (чаще всего).
Я не буду рассказывать о внутреннем устройстве k8s, а только дам немного информации о некоторых его компонентах.


Компоненты k8s


  • Pod — минимальная единица. Может содержать в себе несколько контейнеров, которые будут запущены на одной ноде.
    Контейнеры внутри Pod:
    • имеют общую сеть и могут обращаться друг к другу через 127.0.0.1:$containerPort;
    • не имеют общей файловой системы, поэтому нельзя напрямую писать файлы из одного контейнера в другой.
  • Deployment — следит за работой Pod. Может поднимать необходимое количество инстансов Pod, перезапускать их в случае, если они упали, выполнять деплой новых Pod.
  • PersistentVolumeClaim — хранилище данных. По умолчанию работает с локальной файловой системой ноды. Поэтому, если вы хотите, чтобы два разных Pods на разных нодах могли иметь общую файловую систему, то вам придётся использовать сетевую файловую систему вроде Ceph.
  • Service — проксирует запросы к Pod и от них.
    Типы Service:
    • LoadBalancer — для взаимодействия с внешней сетью с балансировкой нагрузки между несколькими Pods;
    • NodePort (только 30000–32767 порты) — для взаимодействия с внешней сетью без балансировки нагрузки;
    • ClusterIp — для взаимодействия в локальной сети кластера;
    • ExternalName — для взаимодействия Pod с внешними сервисами.
  • ConfigMap — хранилище конфигов.
    Чтобы k8s рестартовал Pod с новыми конфигами при изменении ConfigMap, следует в имени своего ConfigMap указать версию и менять её каждый раз, когда меняется сам ConfigMap.
    То же самое касается и Secret.


Пример конфига с ConfigMap
containers:
    -   name: collect-consumer
        image: mrsuh/rent-collector:1.3.1
        envFrom:
            -   configMapRef:
                    name: collector-configmap-1.1.0
            -   secretRef:
                    name: collector-secrets-1.0.0


  • Secret — хранилище секретных конфигов (пароли, ключи, токены).
  • Label — пары ключ/значение, которые закрепляются за компонентами k8s, например, Pod.
    В начале знакомства с k8s может быть не совсем понятно, как пользоваться Labels. Вот конфиг, в котором поясняются основные принципы работы с Labels:


Пример конфига c Labels
apiVersion: apps/v1
kind: Deployment # тип Deployment
metadata:
    name: deployment-name # имя Deployment
    labels:
        app: deployment-label-app # Label Deployment
spec:
    selector:
        matchLabels:
            app: pod-label-app # Label, по которому Deployment понимает за какими Pods нужно следить
    template:
        metadata:
            name: pod-name
            labels:
                app: pod-label-app # Label Pod
        spec:
            containers:
                -   name: container-name
                    image: mrsuh/rent-parser:1.0.0
                    ports:
                        -   containerPort: 9080

---

apiVersion: v1
kind: Service # тип Service
metadata:
    name: service-name # имя Service
    labels:
        app: service-label-app # Label Service
spec:
    selector:
        # Тип Service не подерживает matchLabels, как Deployment, но фильтрует все равно по Labels
        app: pod-label-app # Label, по которому Service понимает, на какой Pod нужно отправлять трафик
    ports:
        -   protocol: TCP
            port: 9080
    type: NodePort


Подготовка к переезду


Урезание функциональности

Чтобы сервис стал вести себя более стабильно и предсказуемо, пришлось убрать все дополнительные компоненты, которые плохо работали, и немного переписать основные.
Так, я принял решение отказаться от:


  • кода парсинга других сайтов, кроме ВКонтакте;
  • компонента проксирования запросов;
  • компонента уведомлений о новых объявлениях в ВКонтакте и Telegram.


Компоненты сервиса

После всех изменений сервис изнутри стал выглядеть вот так:
Scheme


  • view — поиск и отображение объявлений на сайте (NodeJS);
  • parser — классификатор объявлений (Go);
  • collector — сбор, обработка и удаление объявлений (PHP):
    • cron-explore — консольная команда, которая ищет группы во ВКонтакте о сдаче жилья;
    • cron-collect — консольная команда, которая ходит в группы, собранные cron-explore, и собирает сами объявления;
    • cron-delete — консольная команда, которая удаляет просроченные объявления;
    • consumer-parse — обработчик очереди, в который попадают задания от cron-collect. Он классифицирует объявления с помощью компонента parser;
    • consumer-collect — обработчик очереди, в который попадают задания от consumer-parse. Он фильтрует плохие и дублирующиеся объявления.


Сборка Docker образов

Для того, чтобы управлять компонентами и мониторить их в едином стиле, я решил:


  • вынести конфигурацию компонентов в переменные env,
  • писать логи в stdout.

В самих образах нет ничего специфичного.


Разработка конфигурации k8s

Таким образом, у меня появились компоненты в образах Docker, и я приступил к разработке конфигурации k8s.
Все компоненты, которые работают как демоны, я выделил в Deployment.
Каждый демон должен быть доступен внутри кластера, поэтому у всех есть Service.
Все таски, которые должны исполняться периодически, работают в CronJob.

Вся статика (картинки, js, css) хранится в контейнере view, а раздавать её должен контейнер Nginx.
Оба контейнера находятся в одном Pod.
Файловая система в Pod не шарится, но можно при старте Pod скопировать всю статику в общую для обоих контейнеров папку emptyDir.
Такая папка будет шариться для разных контейнеров, но только внутри одного Pod.


Пример конфига с emptyDir
apiVersion: apps/v1
kind: Deployment
metadata:
    name: view
spec:
    selector:
        matchLabels:
            app: view
    replicas: 1
    template:
        metadata:
            labels:
                app: view
        spec:
            volumes:
                -   name: view-static
                    emptyDir: {}
            containers:
                -   name: nginx
                    image: mrsuh/rent-nginx:1.0.0
                -   name: view
                    image: mrsuh/rent-view:1.1.0
                    volumeMounts:
                        -   name: view-static
                            mountPath: /var/www/html
                    lifecycle:
                        postStart:
                            exec:
                                command: ["/bin/sh", "-c", "cp -r /app/web/. /var/www/html"]

Компонент collector используется в Deployment и CronJob.
Все эти компоненты обращаются к API ВКонтакте и должны где-то хранить общий токен доступа.
Для этого я использовал PersistentVolumeClaim, который подключил к каждому Pod.
Такая папка будет шариться для разных Pod, но только внутри одной ноды.


Пример конфига с PersistentVolumeClaim
apiVersion: apps/v1
kind: Deployment
metadata:
    name: collector
spec:
    selector:
        matchLabels:
            app: collector
    replicas: 1
    template:
        metadata:
            labels:
                app: collector
        spec:
            volumes:
                -   name: collector-persistent-storage
                    persistentVolumeClaim:
                        claimName: collector-pv-claim
            containers:
                -   name: collect-consumer
                    image: mrsuh/rent-collector:1.3.1
                    volumeMounts:
                        -   name: collector-persistent-storage
                            mountPath: /tokenStorage
                    command: ["php"]
                    args: ["bin/console", "app:consume", "--channel=collect"]

                -   name: parse-consumer
                    image: mrsuh/rent-collector:1.3.1
                    volumeMounts:
                        -   name: collector-persistent-storage
                            mountPath: /tokenStorage
                    command: ["php"]
                    args: ["bin/console", "app:consume", "--channel=parse"]

Для хранения данных БД также используется PersistentVolumeClaim.

В итоге получилась вот такая схема (в блоках собраны Pods одного компонента):

Scheme


Разворачивание кластера k8s

Для начала я развернул кластер локально с помощью Minikube.
Конечно, не обошлось без ошибок, поэтому мне очень помогли команды

kubectl logs -f pod-name
kubectl describe pod pod-name 

После того, как я научился разворачивать кластер в Minikube, для меня не составило труда развернуть его в DigitalOcean.

В заключение могу сказать, что сервис стабильно работает уже 2 месяца.
Полную конфигурацию можно посмотреть тут https://github.com/mrsuh/rent-k8s.

© Habrahabr.ru