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