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

В марте 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.
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:
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.
Компоненты сервиса
После всех изменений сервис изнутри стал выглядеть вот так:

- 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.
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, но только внутри одной ноды.
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 одного компонента):
Разворачивание кластера k8s
Для начала я развернул кластер локально с помощью Minikube.
Конечно, не обошлось без ошибок, поэтому мне очень помогли команды
kubectl logs -f pod-name kubectl describe pod pod-name
После того, как я научился разворачивать кластер в Minikube, для меня не составило труда развернуть его в DigitalOcean.
В заключение могу сказать, что сервис стабильно работает уже 2 месяца.
Полную конфигурацию можно посмотреть тут https://github.com/mrsuh/rent-k8s.
ссылка на оригинал статьи https://habr.com/ru/post/484528/
Добавить комментарий