Слабая связность — минимизация зависимостей между компонентами
Высокая связность — группировка функционально связанных элементов
Абстракция — скрытие деталей реализации
Единая ответственность — каждый компонент делает одну вещь хорошо
Разделение ответственности — разделение системы на отдельные модули
Бездействие (Statelessness) — по возможности избегать хранения сессий
Идемпотентность — операции можно безопасно повторять
Событийная согласованность — временное несоответствие для доступности и производительности
Быстрое обнаружение ошибок (Fail-Fast) — быстрое выявление и сообщение об ошибках
Circuit Breaker — изоляция сбойных компонентов
Механизмы повторных попыток — автоматический повтор неудачных операций
Асинхронность — дизайн для неблокирующей коммуникации
Таймауты/Дедлайны — установка лимитов для предотвращения бесконечных ожиданий
Распределённые базы данных
- NoSQL — MongoDB, Cassandra, DynamoDB (гибкая схема, масштабируемые)
- NewSQL — CockroachDB, YugabyteDB (масштабируемые с возможностями SQL)
Распределённые файловые системы
- HDFS — для больших данных (Hadoop)
- Ceph — унифицированная распределённая файловая система
Кеширование — Redis, Memcached (в памяти для скорости)
ACID — атомарность, согласованность, изоляция, долговечность (гарантии реляционных БД)
BASE — в основном доступная, с мягкой согласованностью (характеристики NoSQL)
Шардирование данных — распределение данных по нодам (шардинг, хеширование, диапазоны)
Стратегии репликации — копирование данных для отказоустойчивости
Сеть надёжна | Задержка равна нулю | Пропускная способность бесконечна | Сеть защищена | Топология не меняется | Есть один администратор | Стоимость передачи равна нулю | Сеть однородна
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3❤1
Шпаргалка по распределённым системам
🔸 Основные концепции
🔸 Архитектуры
🔸 Ключевые технологии
🔸 Коммуникация
🔸 Координация и консенсус
Распределённая система — набор взаимосвязанных компьютеров, которые общаются и координируются для достижения общей цели
Масштабируемость — обработка возросшей нагрузки
- Горизонтальная — добавление новых машин (нод)
- Вертикальная — апгрейд существующих машин (например, CPU, RAM)
Надёжность — корректная работа несмотря на сбои
Доступность — доля времени работы системы без простоев
Согласованность — согласие по значениям данных между всеми нодами в заданный момент
Устойчивость к разделению сети (Partition Tolerance) — работа при сетевых разрывах
Теорема CAP — из трёх свойств (Согласованность, Доступность, Устойчивость к разделениям) можно выбрать только два
Управление состоянием — хранение и синхронизация данных между нодами (без состояния/со состоянием)
Шардирование данных — разделение данных для масштабируемости (горизонтальное/вертикальное, по хешу/диапазону)
Стратегии репликации — копирование данных для отказоустойчивости (синхронная/асинхронная/на основе кворума)
Модели согласованности — компромиссы между согласованием данных и производительностью (строгая/событийная/каузальная)
Порядок и время — упорядочивание событий (метки Лэмпорта/векторные часы)
Отказоустойчивость — управление сбоями (фейловер/резервирование)
Управление конкурентным доступом — одновременный доступ к ресурсам (оптимистичный/пессимистичный контроль)
Клиент-сервер — клиенты запрашивают сервисы у серверов
P2P (одноранговая сеть) — ноды одновременно клиенты и серверы
Master-Slave — один мастер-узел координирует, множество слейвов выполняют задачи
Микросервисы — система разбита на мелкие независимые сервисы
Событийно-ориентированная архитектура — компоненты реагируют на события
SOA (сервис-ориентированная архитектура) — набор сервисов, общающихся между собой
Lambda-архитектура — гибридная архитектура для пакетной и потоковой обработки
Оркестрация контейнеров — Kubernetes, Docker Swarm
Сервис-дискавери — Consul, etcd
API-шлюзы — Kong, Tyk, Apigee, AWS API Gateway
Балансировщики нагрузки — HAProxy, NGINX, Traefik, Amazon ELB
Мониторинг и наблюдаемость — Prometheus, Grafana
Распределённый трейсинг — Zipkin, Jaeger
Сервисная сетка (Service Mesh) — Istio, Linkerd
Инфраструктура как код (IaC) — Terraform, CloudFormation
Управление конфигурациями — Ansible, Chef, Puppet
Распределённые кеши — Redis, Memcached
Обработка потоков — Apache Flink, Apache Spark Streaming
Оркестрация рабочих процессов — Apache Airflow, Argo Workflows
Планировщики заданий — Quartz, Apache Oozie, Kubernetes CronJobs
Облачные провайдеры — AWS, Azure, Google Cloud Platform
Безсерверные платформы — AWS Lambda, Azure Functions, Google Cloud Functions
Удалённый вызов процедур (RPC) — вызов функции на удалённой машине
Обмен сообщениями — асинхронное взаимодействие между нодами
- Очереди сообщений (Point-to-Point) — RabbitMQ, Amazon SQS
- Брокеры сообщений (Publish-Subscribe) — Kafka, Apache Pulsar
REST — язык запросов для API с точечным доступом к данным
GraphQL — TLS/SSL, HTTPS, SSH
gRPC — высокопроизводительный RPC с использованием Protocol Buffers
Webhooks — автоматические уведомления при определённых событиях
Алгоритмы консенсуса — Paxos, Raft
Распределённые блокировки — ZooKeeper, etcd
Распределённые транзакции — двухфазный коммит (2PC)
Выбор лидера — выбор координатора
Протокол госсипа — децентрализованное распространение информации
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7❤1
Динамические прокси для логирования в стиле AOP
Используй
Это позволяет эмулировать AOP без Spring — идеально подходит для обработки сквозной логики (cross-cutting concerns).
В этом примере мы логируем до и после вызова метода.
Такой подход даёт гибкий способ динамически абстрагировать поведение.
> Динамические прокси позволяют внедрять поведение — например, логирование, тайминг или проверку безопасности — без изменений исходного кода.
Этот паттерн активно используется во фреймворках вроде Spring и Hibernate.
👉 Java Portal
Используй
java.lang.reflect.Proxy
, чтобы перехватывать вызовы методов на этапе выполнения. Это позволяет эмулировать AOP без Spring — идеально подходит для обработки сквозной логики (cross-cutting concerns).
В этом примере мы логируем до и после вызова метода.
Такой подход даёт гибкий способ динамически абстрагировать поведение.
> Динамические прокси позволяют внедрять поведение — например, логирование, тайминг или проверку безопасности — без изменений исходного кода.
Этот паттерн активно используется во фреймворках вроде Spring и Hibernate.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👀2
This media is not supported in your browser
VIEW IN TELEGRAM
Конкурентность vs параллелизм - это НЕ одно и то же
Конкурентность:
Конкурентность означает, что приложение продвигается по нескольким задачам одновременно.
Хотя один CPU-ядро может обрабатывать только одну задачу за раз, оно достигает конкурентности, быстро переключаясь между задачами.
Пример: Слушание музыки во время написания кода. CPU быстро переключается между этими задачами так эффективно, что кажется, будто обе выполняются одновременно.
Основная цель конкурентности — максимизировать использование процессора, минимизируя время простоя.
Параллелизм:
Параллелизм означает выполнение нескольких задач одновременно.
Для достижения параллелизма задачи разбиваются на более мелкие независимые подзадачи и обрабатываются одновременно на нескольких процессорах, ядрах или GPU.
Пример: Обучение модели глубокого обучения, разделив датасет на батчи и обрабатывая каждый батч одновременно на нескольких GPU.
Цель параллелизма — ускорить обработку, выполняя несколько задач одновременно.
Они не являются взаимно исключающимися:
🔸 Можно иметь конкурентность без параллелизма.
🔸 Можно иметь параллелизм без конкурентности.
🔸 Или можно использовать и то, и другое для высокопроизводительных систем.
👉 Java Portal
Конкурентность:
Конкурентность означает, что приложение продвигается по нескольким задачам одновременно.
Хотя один CPU-ядро может обрабатывать только одну задачу за раз, оно достигает конкурентности, быстро переключаясь между задачами.
Пример: Слушание музыки во время написания кода. CPU быстро переключается между этими задачами так эффективно, что кажется, будто обе выполняются одновременно.
Основная цель конкурентности — максимизировать использование процессора, минимизируя время простоя.
Параллелизм:
Параллелизм означает выполнение нескольких задач одновременно.
Для достижения параллелизма задачи разбиваются на более мелкие независимые подзадачи и обрабатываются одновременно на нескольких процессорах, ядрах или GPU.
Пример: Обучение модели глубокого обучения, разделив датасет на батчи и обрабатывая каждый батч одновременно на нескольких GPU.
Цель параллелизма — ускорить обработку, выполняя несколько задач одновременно.
Они не являются взаимно исключающимися:
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17❤1
5 советов по написанию действительно хороших юнит-тестов
1) Тестируйте поведение, а не реализацию
Пишите тесты, которые проверяют, что код должен делать, а не как именно он это делает внутри.
Потому что внутренние методы могут изменяться, но ожидаемое поведение должно оставаться стабильным.
2) Давайте тестам имена как документации, а не как коду
Пишите имена тестовых методов так, как будто они рассказывают историю - - чётко, конкретно и читаемо для человека.
Это облегчает чтение отчетов о тестах — не нужно углубляться в код.
3) Пишите тест до исправления ошибки
Когда вы обнаруживаете ошибку, сначала напишите тест, который воспроизводит её, прежде чем исправить код.
Это докажет, что ошибка действительно существует (а не только в вашей голове).
Допустим, вы обнаружили, что дни рождения в високосный год крашат вашу систему.
4) Используйте тесты с параметрами для граничных случаев
Вместо того чтобы писать много похожих тестов, пробегитесь по набору входных данных с помощью параметризованных тестов.
Это предотвращает беспорядок от копипаста и делает набор тестов более чистым.
5) Проверяйте только важные моменты, а не все поля
Сфокусируйтесь на утверждениях, касающихся важных бизнес-результатов, а не на каждом поле объекта.
Это помогает тестам оставаться стабильными, даже когда несущественные детали изменяются.
👉 Java Portal
1) Тестируйте поведение, а не реализацию
Пишите тесты, которые проверяют, что код должен делать, а не как именно он это делает внутри.
Потому что внутренние методы могут изменяться, но ожидаемое поведение должно оставаться стабильным.
2) Давайте тестам имена как документации, а не как коду
Пишите имена тестовых методов так, как будто они рассказывают историю - - чётко, конкретно и читаемо для человека.
Это облегчает чтение отчетов о тестах — не нужно углубляться в код.
3) Пишите тест до исправления ошибки
Когда вы обнаруживаете ошибку, сначала напишите тест, который воспроизводит её, прежде чем исправить код.
Это докажет, что ошибка действительно существует (а не только в вашей голове).
Допустим, вы обнаружили, что дни рождения в високосный год крашат вашу систему.
4) Используйте тесты с параметрами для граничных случаев
Вместо того чтобы писать много похожих тестов, пробегитесь по набору входных данных с помощью параметризованных тестов.
Это предотвращает беспорядок от копипаста и делает набор тестов более чистым.
5) Проверяйте только важные моменты, а не все поля
Сфокусируйтесь на утверждениях, касающихся важных бизнес-результатов, а не на каждом поле объекта.
Это помогает тестам оставаться стабильными, даже когда несущественные детали изменяются.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10❤7
7 ключевых сложностей по времени выполнения, которые нужно знать для собеседований по программированию:
1. O(1) — Константное время
- Время выполнения не зависит от размера входных данных.
-📌 Пример: доступ к элементу массива по индексу.
2. O(log n) — Логарифмическое время
- Время выполнения растёт медленно по мере увеличения входных данных. Обычно встречается в алгоритмах, делящих задачу пополам на каждом шаге.
-📌 Пример: бинарный поиск в отсортированном массиве.
3. O(n) — Линейное время
- Время выполнения растёт пропорционально объёму входных данных.
-📌 Пример: поиск элемента в массиве перебором.
4. O(n log n) — Линеаритмическое время
- Время выполнения немного быстрее, чем квадратичное, и включает логарифмическое количество операций на каждый элемент.
-📌 Пример: сортировка массива алгоритмами быстрой сортировки (quick sort) или слиянием (merge sort).
5. O(n²) — Квадратичное время
- Время выполнения растёт пропорционально квадрату входных данных.
-📌 Пример: сортировка пузырьком, где сравниваются и при необходимости меняются местами все пары элементов.
6. O(2ⁿ) — Экспоненциальное время
- Время выполнения удваивается с каждым новым элементом входа. Такие алгоритмы быстро становятся неэффективными при больших объёмах данных.
-📌 Пример: генерация всех подмножеств множества.
7. O(n!) — Факториальное время
- Время выполнения растёт как факториал от количества входных данных.
-📌 Пример: генерация всех перестановок множества.
👉 Java Portal
1. O(1) — Константное время
- Время выполнения не зависит от размера входных данных.
-
2. O(log n) — Логарифмическое время
- Время выполнения растёт медленно по мере увеличения входных данных. Обычно встречается в алгоритмах, делящих задачу пополам на каждом шаге.
-
3. O(n) — Линейное время
- Время выполнения растёт пропорционально объёму входных данных.
-
4. O(n log n) — Линеаритмическое время
- Время выполнения немного быстрее, чем квадратичное, и включает логарифмическое количество операций на каждый элемент.
-
5. O(n²) — Квадратичное время
- Время выполнения растёт пропорционально квадрату входных данных.
-
6. O(2ⁿ) — Экспоненциальное время
- Время выполнения удваивается с каждым новым элементом входа. Такие алгоритмы быстро становятся неэффективными при больших объёмах данных.
-
7. O(n!) — Факториальное время
- Время выполнения растёт как факториал от количества входных данных.
-
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16❤2🤔1
Функциональное программирование в Java с использованием Streams и лямбд:
Streams обрабатывают фильтрацию, сортировку и свёртку данных, а лямбды упрощают определение функций. В Spring они используются для построения реактивных API, а в задачах алгоритмического программирования — для оптимизации обработки данных.
Выше приведён пример REST-эндпойнта на Spring, фильтрующего пользователей, и функции в стиле олимпиадного программирования для поиска лидеров по очкам.
👉 Java Portal
Streams обрабатывают фильтрацию, сортировку и свёртку данных, а лямбды упрощают определение функций. В Spring они используются для построения реактивных API, а в задачах алгоритмического программирования — для оптимизации обработки данных.
Выше приведён пример REST-эндпойнта на Spring, фильтрующего пользователей, и функции в стиле олимпиадного программирования для поиска лидеров по очкам.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10😁4👍3
Как вернуть результат в виде Map из запроса JPA или Hibernate
⏩ Читать подробнее
👉 Java Portal | #cтатья
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7👍4🌭2
This media is not supported in your browser
VIEW IN TELEGRAM
Необходимый репозиторий с сотнями бесплатных API для практики программирования и создания проектов.
Обновляется еженедельно и разделён по темам😇
⇢ https://github.com/public-apis-dev/public-apis
👉 Java Portal
Обновляется еженедельно и разделён по темам
⇢ https://github.com/public-apis-dev/public-apis
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7👍5
This media is not supported in your browser
VIEW IN TELEGRAM
Этот репозиторий представляет собой целую энциклопедию для Java-разработчиков. Автор структурировал материал от основ языка до продвинутых тем
Он охватывает основы, ООП, многопоточность, сборку мусора, коллекции и другие важные темы
Проект имеет полезные разделы с примерами кода: от классических паттернов вроде Singleton до задач на динамическое программирование. Для тех, кто готовится к собеседованиям, есть подборка LeetCode-шаблонов и реальных interview-задач.😡
👉 Java Portal
Он охватывает основы, ООП, многопоточность, сборку мусора, коллекции и другие важные темы
Проект имеет полезные разделы с примерами кода: от классических паттернов вроде Singleton до задач на динамическое программирование. Для тех, кто готовится к собеседованиям, есть подборка LeetCode-шаблонов и реальных interview-задач.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10👍5🔥4
8 концепций проектирования систем, объясненных в одной диаграмме
👉 Java Portal
1. Availability with Load Balancers
Использование балансировщиков (Layer 7) для распределения трафика между несколькими экземплярами сервисов (например, Comment Service, Post Service).
Повышает отказоустойчивость и доступность.
2. Latency with CDNs
CDN кэширует контент ближе к пользователю, уменьшая задержки.
Первый запрос идёт на origin-сервер, второй — уже из кэша CDN.
3. Scalability with Replication
Данные реплицируются между несколькими узлами.
Увеличивает доступность и масштабируемость систем.
4. Durability with Transaction Logs
Система сохраняет изменения в журнал транзакций перед применением.
В случае сбоя состояние может быть восстановлено из лога.
5. Consistency with Eventual Consistency
Записи и чтения могут идти в разные источники.
Система приходит к согласованному состоянию со временем (eventually).
6. Modularity with Loose Coupling and High Cohesion
Модули имеют чёткие границы и хорошо организованы внутри.
Улучшает читаемость, повторное использование и сопровождение кода.
7. Configurability with Configuration-as-Code
Инфраструктура управляется через код (например, Terraform, Ansible).
Контроль версий, автоматизация и повторяемость.
8. Resiliency with Message Queues
Задания помещаются в очередь, откуда обрабатываются несколькими потребителями.
Улучшает отказоустойчивость и масштабируемость обработки.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤4
Метод
Если тебе нужно выполнить сложную выборку (например, последнюю запись на дату) — не бойся использовать
⏩ Читать подробнее
👉 Java Portal | #cтатья
createNativeQuery
из JPA EntityManager
Если тебе нужно выполнить сложную выборку (например, последнюю запись на дату) — не бойся использовать
createNativeQuery
. Нативный SQL в JPA — это мощный инструмент, особенно когда JPQL ограниченPlease open Telegram to view this post
VIEW IN TELEGRAM
👍7👀2
Виртуальные потоки с Project Loom
Традиционные потоки в Java мощный, но тяжёлый механизм, который ограничивает масштабируемость в высоконагруженных многопоточных приложениях.
С появлением Project Loom вводится концепция виртуальных потоков т.е. лёгких потоков, управляемых самой JVM, которые упрощают работу с конкурентностью и делают её более эффективной.
Виртуальные потоки позволяют запускать тысячи параллельных задач с минимальными накладными расходами, при этом сохраняя простой, блокирующий стиль написания кода без ущерба для производительности.😇
👉 Java Portal
Традиционные потоки в Java мощный, но тяжёлый механизм, который ограничивает масштабируемость в высоконагруженных многопоточных приложениях.
С появлением Project Loom вводится концепция виртуальных потоков т.е. лёгких потоков, управляемых самой JVM, которые упрощают работу с конкурентностью и делают её более эффективной.
Виртуальные потоки позволяют запускать тысячи параллельных задач с минимальными накладными расходами, при этом сохраняя простой, блокирующий стиль написания кода без ущерба для производительности.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8
Гайд как откатить последний коммит в Git (пошагово)
① Если ещё не делал
Оставить изменения в
Полностью удалить изменения:
Отредактировать последний коммит (сообщение или содержимое):
② Если уже сделал
Используй
(Если возникнут конфликты — их придётся разрулить вручную.)
Получи хеш нужного коммита с помощью:
★ Уровень «эксперт»:
Используй
Можно менять порядок, объединять, править или удалять коммиты.
После изменений —
Важно: Делай
или вся команда согласована и не против переписывания истории.
👉 Java Portal
① Если ещё не делал
push
:Оставить изменения в
staging
(индексе):git reset --soft HEAD~1
Полностью удалить изменения:
git reset --hard HEAD~1
Отредактировать последний коммит (сообщение или содержимое):
git commit --amend -m "Исправленное сообщение"
② Если уже сделал
push
:Используй
git revert <хеш>
, чтобы создать новый коммит, отменяющий изменения предыдущего.(Если возникнут конфликты — их придётся разрулить вручную.)
Получи хеш нужного коммита с помощью:
git log
★ Уровень «эксперт»:
Используй
git rebase -i
для редактирования локального коммит-истории:Можно менять порядок, объединять, править или удалять коммиты.
После изменений —
git push --force-with-lease
Важно: Делай
rebase
и push --force-with-lease
только если ты один работаешь в веткеили вся команда согласована и не против переписывания истории.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👍4
Пример проекта, демонстрирующий, как использовать GraphQL вместе с Spring Boot.
В репозитории показано:
🔸 как настроить GraphQL в Spring-приложении,
🔸 как описывать схемы, резолверы и DTO,
🔸 как работать с базой данных (MongoDB),
🔸 и как совмещать GraphQL с REST-контроллерами.
Подойдёт, если ты хочешь разобраться, как устроен GraphQL в экосистеме Spring или ищешь базу для своего pet-проекта.
👉 Java Portal
В репозитории показано:
Подойдёт, если ты хочешь разобраться, как устроен GraphQL в экосистеме Spring или ищешь базу для своего pet-проекта.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤5👍3
Как выдержать нагрузку в 10 000 одновременных пользователей
1. Поставь балансировщик нагрузки (Load Balancer) спереди
- Распределяй трафик между несколькими backend-серверами.
- Инструменты:
2. Масштабируй горизонтально
- Один мощный сервер — нет.
- Используй несколько небольших stateless-серверов за LB.
3. Пиши stateless-сервисы
- Не храни сессии или состояние в оперативке.
- Используй
4. Используй connection pooling
- Базы «захлебываются» от 10 000 одновременных подключений.
- Ограничь число подключений к БД с каждого сервера через пул (например,
5. Активно кэшируй
Кешировать можно:
- Static files → CDN
- DB-запросы → Redis
- HTML-фрагменты → Edge cache
6. Используй read-replicas
- Разделяй чтение и запись, чтобы масштабировать чтение из базы.
7. Async и очереди (Queued Workloads)
- Переноси тяжёлые задачи (почта, видео и т.д.) в background jobs.
- Используй очереди:
8. Включи авто-масштабирование
- Используй инфраструктуру, способную масштабироваться под нагрузку:
9. WebSocket / Реалтайм? Используй gateway-сервисы
- Запускай WebSocket-шлюзы (например,
- Масштабируй через sticky-сессии или pub/sub (
10. Мониторь всё
- Отслеживай
- Инструменты:
👉 Java Portal
1. Поставь балансировщик нагрузки (Load Balancer) спереди
- Распределяй трафик между несколькими backend-серверами.
- Инструменты:
NGINX
, HAProxy
, AWS ELB
.2. Масштабируй горизонтально
- Один мощный сервер — нет.
- Используй несколько небольших stateless-серверов за LB.
3. Пиши stateless-сервисы
- Не храни сессии или состояние в оперативке.
- Используй
Redis
/ Memcached
или JWT
для сессий и авторизации.4. Используй connection pooling
- Базы «захлебываются» от 10 000 одновременных подключений.
- Ограничь число подключений к БД с каждого сервера через пул (например,
HikariCP
).5. Активно кэшируй
Кешировать можно:
- Static files → CDN
- DB-запросы → Redis
- HTML-фрагменты → Edge cache
6. Используй read-replicas
- Разделяй чтение и запись, чтобы масштабировать чтение из базы.
7. Async и очереди (Queued Workloads)
- Переноси тяжёлые задачи (почта, видео и т.д.) в background jobs.
- Используй очереди:
SQS
, RabbitMQ
, Kafka
.8. Включи авто-масштабирование
- Используй инфраструктуру, способную масштабироваться под нагрузку:
AWS EC2 + ASG
, GCP GKE
и т.д.9. WebSocket / Реалтайм? Используй gateway-сервисы
- Запускай WebSocket-шлюзы (например,
Socket.io
за LB). - Масштабируй через sticky-сессии или pub/sub (
Redis
, Kafka
).10. Мониторь всё
- Отслеживай
CPU
, память
, задержки
, глубину очередей
. - Инструменты:
Prometheus + Grafana
, Datadog
, New Relic
.Please open Telegram to view this post
VIEW IN TELEGRAM
❤8🔥7👍2👀1