OTUS-Social-Network

Запустить проект можно, используя команду docker compose up в корне проекта.

Для тестов можно использовать postman коллекцию Otus Social Network из корня проекта.

ДЗ: Индексы и оптимизация запросов

В данной домашней работе требовалось проверить и улучшить производительность эндпоинта поиска пользователя по имени и фамилии.

Тестовые данные генерировались с использованием библиотеки mimesis (скрипт).

Нагрузочное тестирование производилось при помощи JMeter (конфигурация) в 1/10/100 потоков в течение 60 секунд для каждого числа потоков.

Далее представлены графики latency и throughput до и после добавления индекса (кликабельные).

Метрика До индексов После индексов
Active Threads Over Time Active Threads Over Time Active Threads Over Time
Response Times Over Time Response Times Over Time Response Times Over Time
Transactions per Second Transactions per Second Transactions per Second

Из графиков видно, что удалось улучшить latency и throughput на порядок.

Запрос на создание индексов:

CREATE INDEX users_lower_first_name_collate_c_idx ON users(lower(first_name) COLLATE "C");
CREATE INDEX users_lower_second_name_collate_c_idx ON users(lower(second_name) COLLATE "C");

Explain запроса на поиск после создания индексов:

Limit  (cost=241.30..241.36 rows=25 width=1279) (actual time=1.481..1.484 rows=22 loops=1)
  ->  Sort  (cost=241.30..241.36 rows=25 width=1279) (actual time=1.481..1.482 rows=22 loops=1)
        Sort Key: id
        Sort Method: quicksort  Memory: 64kB
        ->  Bitmap Heap Scan on users  (cost=141.11..240.72 rows=25 width=1279) (actual time=1.321..1.461 rows=22 loops=1)
              Filter: ((lower(first_name) ~~ 'ана%'::text) AND (lower(second_name) ~~ 'бор%'::text))
              Heap Blocks: exact=22
              ->  BitmapAnd  (cost=141.11..141.11 rows=25 width=0) (actual time=1.205..1.205 rows=0 loops=1)
                    ->  Bitmap Index Scan on users_lower_second_name_collate_c_idx  (cost=0.00..70.42 rows=5000 width=0) (actual time=0.553..0.553 rows=4268 loops=1)
                          Index Cond: ((lower(second_name) >= 'бор'::text) AND (lower(second_name) < 'бос'::text))
                    ->  Bitmap Index Scan on users_lower_first_name_collate_c_idx  (cost=0.00..70.42 rows=5000 width=0) (actual time=0.508..0.508 rows=5598 loops=1)
                          Index Cond: ((lower(first_name) >= 'ана'::text) AND (lower(first_name) < 'анб'::text))
Planning Time: 0.533 ms
Execution Time: 1.540 ms

Мною были выбраны именно BTree индексы, так как они позволяют быстро искать по префиксу строки.

ДЗ: Репликация: практическое применение

  1. Асинхронную репликацию настроил в docker compose следующим образом:
version: '3'

x-postgres-common: &postgres-common
  image: postgres:16
  restart: always
  user: postgres
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U otus-sn"]
    interval: 5s
    timeout: 5s
    retries: 5

services:
  # ... other services

  otus-sn-database-primary:
    container_name: otus-sn-database-primary
    <<: *postgres-common
    ports: 
      - 5432:5432
    command: |
      postgres
      -c max_connections=200
      -c wal_level=replica 
      -c hot_standby=on 
      -c max_wal_senders=10 
      -c max_replication_slots=10 
      -c hot_standby_feedback=on
    environment: 
      POSTGRES_DB: otus-sn
      POSTGRES_USER: otus-sn
      POSTGRES_PASSWORD: otus-sn
      POSTGRES_HOST_AUTH_METHOD: "scram-sha-256\nhost replication all 0.0.0.0/0 md5"
      POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
    volumes: 
      - otus-sn-database-primary-data:/var/lib/postgresql/data

  otus-sn-database-replica:
    container_name: otus-sn-database-replica
    <<: *postgres-common
    ports: 
      - 5433:5432
    environment:
      PGUSER: replicator
      PGPASSWORD: replicator_password
    command: |
      bash -c '
      if [ -z "$(ls -A /var/lib/postgresql/data)" ]
      then
        until pg_basebackup --pgdata=/var/lib/postgresql/data -v -R --slot=replication_slot --host=otus-sn-database-primary --port=5432
        do
          echo "Waiting for primary to connect..."
          sleep 1s
        done
        echo "Backup done, starting replica..."
        chmod 0700 /var/lib/postgresql/data
      fi
      postgres -c max_connections=200
      '
    depends_on:
      otus-sn-database-primary:
        condition: service_healthy
    volumes: 
      - otus-sn-database-replica-data:/var/lib/postgresql/data


volumes:
  otus-sn-database-primary-data:
  otus-sn-database-replica-data:
  1. Перенес запросы /user/get и /user/search на чтение из реплики.

  2. Произвел нагрузочное тестирование этих запросов при помощи JMeter (скрипт).

    • Метрики мастера во время теста: alt text
    • Метрики реплики во время теста: alt text Видно, что всю нагрузку приняла на себя реплика.
  3. Добавил еще одну реплику:

# ... other services
  otus-sn-database-another-replica:
    container_name: otus-sn-database-another-replica
    <<: *postgres-common
    ports: 
      - 5434:5432
    environment:
      PGUSER: replicator
      PGPASSWORD: replicator_password
    command: |
      bash -c '
      if [ -z "$(ls -A /var/lib/postgresql/data)" ]
      then
        until pg_basebackup --pgdata=/var/lib/postgresql/data -v -R --slot=another_replication_slot --host=otus-sn-database-primary --port=5432
        do
          echo "Waiting for primary to connect..."
          sleep 1s
        done
        echo "Backup done, starting replica..."
        chmod 0700 /var/lib/postgresql/data
      fi
      postgres -c max_connections=200\
        -c primary_conninfo="host=otus-sn-database-primary port=5432 user=replicator password=replicator_password application_name=anotherpgslave"
      '
    depends_on:
      otus-sn-database-primary:
        condition: service_healthy
    volumes: 
      - otus-sn-database-another-replica-data:/var/lib/postgresql/data
  1. Настроил синхронную репликацию на мастере:
--  -c synchronous_commit=on
--  -c synchronous_standby_names='ANY 1 (pgslave, anotherpgslave)'

select application_name, state, sync_state from pg_stat_replication;

|application_name|state    |sync_state|
|----------------|---------|----------|
|pgslave         |streaming|quorum    |
|anotherpgslave  |streaming|quorum    |
  1. Создал постоянную нагрузку на запись при помощи скрипта и стопнул реплику.

  2. Имеем следующую картину на мастере и оставшейся реплике:

-- otus-sn-database-primary
select count(*) from users;
 2924

-- otus-sn-database-another-replica
select count(*) from users;
 2924

-- Ни одна транзакция не "потерялась"

ДЗ: Лента постов от друзей

Мною были реализованы следующие новые эндпоинты:

  • /friend/set - добавляет пользователю нового друга и инвалидирует его ленту постов
  • /friend/delete - удаляет у пользователя друга и инвалидирует его ленту постов
  • /post/create - добавляет новый пост в БД и очередь постов
  • /post/get - возвращает пост по его id
  • /post/feed - возвращает закэшированную ленту для пользователя
  • /debug/cache - возвращает внутреннее состояние кэша

Помимо инвалидации в ручках изменения, кэш еще прогревается при старте приложения.

В качестве кэша использовался обычный питоновский словарь. Кажется, что этого достаточно для учебных целей. В дальнейшем его можно будет легко заменить например на Redis.

ДЗ: Онлайн обновление ленты новостей

Мною были изменен эндпоинт /post/create. Теперь в нем, помимо сохранения поста в БД, происходит его отправка в очередь постов для дальнейшей обработки.

Также был добавлен вэбсокет-эндпоинт /post/feed/posted. В нем в реальном времени вычитываются посты друзей пользователя из подходящей очереди.

Для корректной маршрутизации сообщений между очередями была реализована фоновая таска feeds_update_background_task. Она берет пост из очереди постов, определяет всех пользователей, которые добавили автора поста в друзья, обновляет их ленты постов и пересылает посты в их персональные очереди.

Схема передачи сообщений выглядит следующим образом:

flowchart LR
  pce([post/create]) --> pq[[posts]]
  pq --> fut([feeds update task])
  fut --> fe{feeds}
  fe --> fu1q[[user/user_1]]
  fe --> fu2q[[user/user_2]]
  fu1q --> pfp1([post/feed/posted])
  fu2q --> pfp2([post/feed/posted])
Loading