TGViewer
Channel Public Channel
dev notes

dev notes

@junsenior

Пишу про Go, Vim, и про то, как я медленно ползу в сторону FAANG.

С предложениями: @junsenpub
Subscribers
1.39K
Photos
33
Videos
6
Links
189

Showing posts older than #216 · Back to latest

Older Posts 20 shown
Post #215 1.07K
Post #214 1.15K

Forwarded from PHP Digest

Вышел PHP 8.1 🎉

https://www.php.net/releases/8.1/ru.php

Основные новые возможности:

🔹 Enums они же перечисления;
🔹 Readonly свойства;
🔹 First-class callable — получение ссылки на любую функцию;
🔹 Оператор new в инициализаторах (и вложенные атрибуты);
🔹 Файберы;
🔹 final константы в классах;
🔹 Новый тип never для (не)возвращаемых значений;
🔹 Запись восьмеричных чисел с префиксом 0o;
🔹 Оператор ... поддерживает массивы со строковыми ключами;
🔹 Много улучшений по производительности
(+23% к скорости для на демо приложении Symfony)

В релизе также много депрекейшнов, улучшений консистентности и обработки ошибок.

Основные депрекейшны:

🔺 Объявлено устаревшим неявное преобразование float в int, где теряется дробная часть;
🔺 Интерфейс Serializable объявлен устаревшим;
🔺 Ограничено использование $GLOBALS;
🔺 Объявлено устаревшим передача значения null в параметры встроенных функций, которые не nullable;
🔺 Добавлены типы для возвращаемых значений встроенных классов (и новый атрибут #[ReturnTypeWillChange]);
🔺 Продолжено удаление типа resource. Ресурсы file_info, imap FTP Connection, LDAP, PostgreSQL теперь будут объектами, соответственно finfo, IMAP\Connection, FTP\Connection, PgSql\Connection, PgSql\Result.

Еще почитать-посмотреть:

• Подробно: php.watch/versions/8.1
• Коротко в видео: What's New in PHP 8.1
• Валентин Удальцов: Лайв-кодинг-обзор PHP 8.1
• Максимально полный список изменений: php-8.1.0/UPGRADING
www.php.net PHP 8.1 Released PHP 8.1 — большое обновление языка PHP: перечисления, readonly-свойства, callback-функции как объекты первого класса, файберы, пересечение типов, улучшения производительности и многое другое.
Post #213 915
Я тут изначально про PHP говорил, так что не могу не поделиться важным для сообщества релизом!
Post #212 1.11K
В go мне очень не хватало архитектурных примеров построения приложений. Когда кто-то взял какой-либо тип архитектуры, и показал, как возможностями этого языка её реализовать и как это было бы правильно. Конечно, дефолтный "контроллер-сервис-репозиторий" реализовать не сложно, но чистую архитектуру по Роберту Мартину - хотелось бы подглядеть.

На днях вышел ман с отличной реализацией clean architecture дядя Боба - https://www.youtube.com/watch?v=eVhIlhLl4e4 (https://github.com/theartofdevel/golang-clean-architecture).
Крайне качественный контент по гошке (да ещё и на русском!) - делюсь. Мне понравился стиль изложения, понравилась теория и понравился абстрактный пример, где раскрываются основные принципы. И что самое приятное - этого примера хватило, чтобы на практике заюзать подобный формат реализации этой архитектуры.
Post #210 1.14K
Конспекты микросервисов отложились до лучших времен, и на то у меня есть причина: уже с месяц на работе я пилю проект на go. Проект небольшой, но интересный: много асинхронщины, много новых паттернов, и повсеместное страдание :D Страдание - это набитие шишек, а нибитие шишек - это лучший опыт.
Сервис я почти допилил, и за время работы над ним вынес несколько, вероятно очевидных, но интересных для меня вещей, которыми поделюсь ниже.

1. Если к обработке ошибок я уже давно привык, к тому, что каждую привычную конструкцию нужно писать с нуля - тоже, то вот к тому, что в go не принято делать подпакеты - не могу привыкнуть. Мне бы очень хотелось иметь 2 дирректории: internal/repository/mysql/ и internal/repository/clickhouse/, но так не принято :) Так что имеем internal/repository, где имеем кучу файлов, и стараемся к этому привыкнуть.

2. Для контроля количества одновременно запущенных горутин, в рамках какой-то задачи отлично подходит буферизированный канал: перед go func().. кидаем в канал значение, а в defer, внутри функции, запускаемой в горутине, убираем.

3. Чем лучше у тебя теоретическая база по асинхронщине, параллельному программированию и прочим страшным вещам - тем проще. В процессе всплывали ещё те знания, которые я получил в универе на курсе параллелки, и частенько помогали найти верное решение.

4. ORM в go (я юзал gorm) - плохое решение, не теряй время и бахай нативные запросы через sqlx. Лично у меня было несколько кейсов, которые ORM решить не смогла.

5. Все найденные мной библиотеки, реализующие работу с clickhouse - умеют в ограниченное число типов. Клик поддерживает, например, мапы, а либы, которая умела бы в такое - я не нашёл. В следующий раз буду это учитывать :)

6. Если тебе нужно проверять, запущена ли сейчас какая-либо задача в горутине, лучшее решение, которое я нашёл - go.uber.org/atomic + мапа.

7. gin - наиболее удобный веб-фреймворк, из тех, с которыми мне довелось работать. Прям максимально всё для людей.

8. Архитектурные паттерны (https://github.com/golang-standards/project-layout) - мастхэв. Просто подведя свой проект под такую структуру ты уже в процессе понимаешь, насколько она удобная и продуманная.

9. Gitlab ci/cd из коробки отлично справляется с деплоем go-приложения, за что ему честь и почет. На днях попробую github actions в пет-проекте и дам фидбэк, что удобней и проще.

Список будет дополняться по мере открытия мной чего-то интересного :)
pkg.go.dev atomic package - go.uber.org/atomic - Go Packages Package atomic provides simple wrappers around numerics to enforce atomic access.
Post #209 1.24K
Продолжаю конспектировать Микросервисы Ричардсона, сегодняшняя глава - "Транзакционный обмен сообщениями", или когда нам нужно атомарно выполнить операции через брокер.

Сервису часто нужно публиковать сообщения в рамках транзакции, обновляющей базу данных.
Тут мы возвращаемся к паттерну, о котором я писал пару лет назад - https://t.me/junsenior/70
Подробно и на примере - по ссылке, тут лишь повторю краткий алгоритм:
* Сервис, отправляющий сообщения, записывает их в таблицу outbox, в реляционную субд или nosql (но с поддержкой ACID) субд
* Таблица outbox - гарантирует атомарность записи (спасибо ACID), и играет роль временной очереди сообщений
* Ретранслятор - компонент, читающий таблицу outbox и передающий сообщения брокеру
* Шаблон "Опрашивающий издатель" - шаблон для ретранслятора, подходящий для случая, если outbox реляционная: ретранслятору достаточно просто вычитать все новые сообщения, отправить их брокеру и удалить из базы
* Шаблон "Отслеживание транзакционного журнала" - более интересный и сложный способ, имеющий значительный плюс - он не даёт нагрузки на базу данных, анализируя только журнал фиксации. Если ты не знаешь, что такое журнал фиксации или транзакционный журнал - я упоминал про него тут - https://t.me/junsenior/184, когда разбирался с "Высоконагруженными приложениями" Клеппмана.

Есть несколько готовых инструментов, реализующих анализ журнала транзакций и умеющий отправлять новые сообщения в брокеры:
* debezium.io - публикует изменения базы данных для брокера Apache Kafka
* github.com/linkedin/databus - анализирует журнал Oracle и публикует изменения в виде событий
* DynaboDB streams (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html) - создает поток упорядоченных по времени изменений, приложения может читать эти изменения из потока и публиковать их в брокер.
* Eventuate Tram (github.com/eventuate-tram/eventuate-tram-core) - либа от самого Ричардсона, использует протокол двоичного журнала MySQL, Postgres или просто проверяет изменения, внесенные в таблицу outbox, и публикует их в Apache Kafka.
Telegram 1000 дней программирования Вы просили паттерны? Их есть у меня. Transactional outbox - паттерн, о котором на русском очень мало информации. Хотя понимание его структуры и схемы работы критически важно в случае создания системы логирования для твоего приложения. Не хочешь влететь…
Post #207 1.02K
Дублирование сообщений: клиент может отказать после получения сообщения, его обработки, но перед подтверждением сообщения. Тогда брокер доставит сообщения повторно, и мы получим дубль. В идеале брокер должен сохранять порядок следования сообщений (отправлять сообщение тому же потребителю), тогда потребитель может проигнорировать сообщение, которое уже обработано и отправить подтверждение обработки. Существупет несколько методов борьбы с повторяющимися сообщениями:
* Добавление идемпотентных дескрипторов: логика обработки считается идемпотентной, если её многократное выполнение с одинаковыми входными параметрами не несёт рассогласования данных. Например, отмена уже отменённого заказа - идемпотентная операция. К сожалению, логику обработки не всегда можно сделать идемпотентной, и брокер не всегда может отправить сообщение тому же получателю (например, при выходе последнего из строя). В таких случаях обработчики должны отслеживать сообщения и отклонять результаты.
* Отслеживание и отклонение дубликатов: например, обработчик сообщений авторизует банковскую карту. Для каждого заказа он должен выполнить ровно одну авторизацию. Идемпотенция обработчика в этом случае достигается отслеживанием и отклонением дублей. В качестве простого решения можно сделать так, чтобы потребитель отслеживал обработанные сообщения с помощью идентификаторов (например, записанных в БД), и отклонял те, что уже были обработаны. Ещё один вариант состоит в том, чтобы обработчик записывал сообщения не в отдельную таблицу, а в таблицу приложения. Этот подход полезен при использовании NoSQL, которые имеют ограниченную транзакционную модель и не поддерживают обновление двух таблиц в рамках одной транзакции (Про NoSQL я писал выше, и писал много - смотри конспекты "Высоконагруженных приложений" от Клеппмана).
Post #206 911
Сегодня будет конспект главы "Взаимодействие с помощью асинхронного обмена сообщениями".
В контексте асинхронного обмена мы будем говорить про брокеры сообщений.

Сообщения могут быть нескольких видов:
* Документ - только данные
* Команда - сообщение, эквивалентное RPC-запросу
* Событие - сообщение о том, что с отправителем что-то произошло
В рамках книги автор рассматривает использование команд и событий.

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

Каналов есть 2 вида:
* Точка - точка: доставка сообщений от одного издателя к одному потребителю
* Издатель - подписчик: доставка сообщений всем подключенным потребителям

Есть множество брокеров, на основе которых можно реализовать обмен сообщениями, и автор сразу выделяет наиболее популярные, и что не менее важно, с открытым исходным кодом:
* RabbitMQ
* ActiveMQ
* Apache Kafka

Проблемы при использовании брокеров.

Нарушение порядка следования сообщений: например, у нас есть много обработчиков, и есть 3 сообщения, которые должны быть выполнены в четком порядке: Order Created, Order Updated, Order Cancelled.
Распространённое решение, которое используют, например, Apache Kafka и AWS Kinesis - сегментированные каналы:
* Сегментированный канал - это два или более сегмента, где каждый сегмент - так же является каналом.
* Отправитель указывает в заголовке ключ сегмента, который обычно представляет собой произвользую строку или байты. Брокер использует этот ключ, чтобы привязать сообщение к определённому сегменту/разделу. Например, он может выбрать сегмент взятием остатка от целочисленного деления хеша сегментного ключа на количество сегментов.
От себя добавлю, что такой подход шардинга был реализован в видосах SovietReliable - https://www.youtube.com/watch?v=t3FdULDRfRM&list=PLWwSgbaBp9XqeuIuTWqpNtvf_EL0I4TJ2 (да, тут чувак с потрясающим русским акцентом пилит распределённый брокер на Go, кто не смотрел - всем рекомендасьён)
* Брокер группирует экземпляры получателя и обращается с ними как с одним логическим получаетелем. В kafka применяется термин "группа получателей". Брокей назначает каждый сегмент отдельному получаетлю, при запуске и остановке получателей процедура повторяется.
YouTube Design phase / Chukcha, communist answer to Kafka / Golang In this stream we will be developing the distributed event bus a-la Kafka from scratch in Go. https://github.com/YuriyNasretdinov/chukcha
Post #205 1.09K
Сегодня рассмотрим ещё один простой паттерн - шаблон обнаружения сервисов.

Проблема: раньше сервис, как правило, был статически развернут на сервере, имел статический адрес и порт. Приложению достаточно было прочитать адрес из конфига и работать с сервисом.
Сейчас же большинство сервисов имеют динамические адреса, развернуты в облаках и могут масштабироваться, добавляя или удаляя уже существующие адреса.

Решение: реестр сервисов - некая база данных, с которой сервисы либо работают напрямую, либо за обнаружение сервисов и запись отвечает инфраструктура развертывания.
Рассмотрим первый и наиболее простой способ реализации обнаружения - работу с реестром напрямую. Для вызова сервиса клиент сначала обращается к реестру, получает его экземпляры, и затем начинает с ними работать. Тут подключается второй шаблон - шаблон саморегистрации (microservices.io/patterns/self-registration.html) сервиса. Экземпляр сервиса обращается к API реестра, чтобы зарегистрировать своё сетевое местоположение. Он так же может предоставить URL для проверки работоспособности - конечную точку, которую реестр периодически запрашивает, чтобы убедиться, что сервис работает в нормальном режиме. Затем отрабатывает второй шаблон - "Обнаружение на клиентской стороне": клиент извлекает из реестра список доступных экземпляров сервиса и выбирает один из них с учетом балансирования нагрузки (https://microservices.io/patterns/client-side-discovery.html).

Второй способ - обнаружение на стороне развертывания. Docker и Kubernetes имеют встроенный механизм обнаружения и регистрации. Платформа выдаёт каждому сервису DNS-имя, адрес и привязанный домен. Клиент делает запрос к DNS, а платформа сама решает, к какому из доступных экземпляров его направить. Этот механизм более предпочтительный, т.к. нам не нужно искать или реализовывать реестр, и не нужно писать имплементацию работы с ним на стороне каждого сервиса.
В процессе такой маршрутизации участвуют два шаблона: "Сторонняя регистрация" - экземпляры сервиса автоматически регистрируются внешним компонентом (https://microservices.io/patterns/3rd-party-registration.html) и "Обнаружение на стороне сервера" - клиент делает запрос к маршрутизатору, который отвечает за обнаружение (https://microservices.io/patterns/server-side-discovery.html).
Post #204 1.23K
Я вернулся из отпуска, отдохнул и с новыми силами планирую дочитать "Микросервисы" Ричардсона. Параллельно (до отпуска) уже пару недель у меня был в работе небольшой пет-проект на go, в рамках которого удалось заюзать много интересных вещей, которые в ближайшее время начну описывать. Но главное - проект будет на микросервисах, где мы на практике попробуем концепции этой книги. А сейчас небольшой конспект главы 3.2.3 - "Работа в условиях частичного отказа с применением шаблона 'Предохранитель'".

Каждый раз, когда сервис в распределённой системе делает синхронный запрос к другому сервису - возникает риск частичного отказа. Поскольку сервис является отдельным процессом, он может не ответить вовремя на запрос клиента (сбой, тех. обслуживание, перегрузка).
Клиент блокируется в ожидании ответа, и появляется опасность блокировки всей системы по цепочке. Тут нам помогает шаблон "Предохранитель": RPI-прокси, который в случае достижения определённого лимита последовательных отказов начинает отклонять все вызовы, пока не истечет определённое время.
Разберём на примере: сервис Order перестаёт отвечать (этот сервис я выделял и описывал в постах выше): мобильный клиент делает REST-запрос к API-шлюзу, тот проксирует запрос к недоступному сервису Order. OrderServiceProxy (сервис, проксирующий запросы к микросервису Order) будет блокироваться до бесконечности в ожидании ответа, что плохо скажется на удобстве использования и, что ещё хуже, на потреблении ресурсов. Рано или поздно ресурсы закончатся, и весь API станет недоступен.
Чтобы частичный отказ не распространился по всему приложению, при проектировании сервисов нам нужно:
* Использовать RPI-прокси, наподобие OrderServiceProxy, чтобы справляться с недоступными сервисами
* Решить, как восстанавливаться после отказа удалённого сервиса.
Для начала рассмотрим, как написать надежный RPT-прокси.

Каждый раз, когда сервис вызывает другой сервис, он должен защитить себя следующими механизмами:
* Сетевое время ожидания (корректно выставить таймаут)
* Ограничение количества неудачных запросов от клиента к сервису
* Шаблон "Предохранитель": отслеживание количества успешных и битых запросов. Если часоста ошибок привысит некоторый порог, предохранитель размыкается, и все дальнейшие попытки на некоторое время сразу завершаются с некоторым ошибочным кодом. Если через некоторое время сервис успешно отвечает, предохранитель смыкается, и появляется возможность обработать все битые запросы, которые можно хранить в какой-либо очереди, и корректно обрабатывать новые.

Восстановление после отказа
Самый простой способ - вернуть ошибку клиенту, если такой подход имеет место быть и данные, не полученные от сервиса, не критичны.
В ином случае можно вернуть резервное значение (например, значение по умолчанию), либо закэшированный ранее ответ.
  • 👍 1
Post #203 1.31K
Ричардсон подробно описывает REST, его дальнейшую модель развития и то, почему часто в микросервисной архитектуре не получится сделать RESTful, даже если очень захочется. Если коротко:
выделили мы сервис Order, и решили подвести его под RESTful. Что используется для обновления данных? Правильно, PUT. За что отвечает сервис Order? Что мы можем обновить? Например, статус заказа, и, например, мы можем отредактировать заказ. Можно сделать одну ручку, которая будет обновлять заказ, в рамках который мы сможем обновить любые поля, например, только статус. Но помимо самих заказов у сервиса Order может быть ещё несколько сущностей, связанных с заказами, которые тоже нужно будет обновлять. Таким образом PUT потеряет идемпотентность, и придётся делать несколько ручек на обновление, что уже не будет накладываться на RESTful.

Резюмируя тему REST API, приводятся основные преимущества и недостатки REST:
* Он простой и привычный
* API на основе HTTP достаточно просто тестировать
* Он имеет встроенную поддержку взаимодействия вида запрос-ответ
* Протокол HTTP дружественнен к брандмауэрам
* Он не нуждается в промежуточном брокере, что упрощает систему
И недостатки:
* Он поддерживает только 1 стиль - запрос-ответ
* Степень доступности снижена. Поскольку клиент и сервис взаимодействует между собой напрямую, без промежуточного звена для буферизации сообщений, они оба должны работать на протяжении всего обмена данными
* Клиенты должны знать местонахождение (URL) сервиса
* Извлечение нескольких ресурсов за один запрос связано с определёнными трудностями
* Иногда непросто привязать несколько операций обновления к HTTP-командам

Несмотря на недостатки, REST считается де-факто стандартом для построения API. Но, есть и множество альтернатив, например - gRPC, которую мы дальше и рассмотрм.


gRPC - двоичный протокол на основе сообщений, для написания многоязычных клиентов и серверов. Проектирование сервиса должно начинаться с его API. В gRPC API описывается с помощью языка IDL на основе Protocol Buffers - многоязычного механизма сериализации структурированных данных от Google. Компилятор Protocol Buffer генерирует клиентские заглушки и серверные каркасы, и поддерживает разные языки. Клиенты и серверы обмениваются сообщениями в формате Protocol Buffers используя HTTP/2.

gRPC API состоит из определений сервисов и сообщений вида "запрос/ответ". Определение сервиса - что-то вроде интерфейса в ООП языках: набор строго типизированных методов. Помимо стандартного флоу вида запрос-ответ, gRPC поддерживает поточный вызов процедур: сервер может вернуть клиенту поток сообщений, и клиент может отправить на сервер поток сообщений.
Ниже приведу пример gRPC API для сервиса Order, описывающего несколько методов, включая createOrder():

service OrderService {
rpc createOrder(CreateOrderRequest) returns (CreateOrderReply) {}
rpc cancelOrder(CancelOrderRequest) returns (CancelOrderReply) {}
rpc reviseOrder(ReviseOrderRequest) returns (ReviseOrderReply) {}
}

message CreateOrderRequest {
int64 restaurantId = 1;
int64 consumerId = 2;
repeated LineItem lineItem = 3;
}

message LineItem {
string menuItemId = 1;
int32 quantity = 2;
}

message CreateOrderReply {
int64 orderId = 1;
}

Преимущества протокола gRPC:
* Он позволяет легко спроектировать API с богатым набором операций
* Он имеет эффективный компактный механизм IPC, что особенно явно проявляется при обмене крупными сообщениями
* Поддержка двунаправленных потоков
Недостатки:
* Для JS, например, процесс описания API на gRPC более трудоёмок, нежели для REST из-за особенностей типизации
* Старые брандмауэры не поддерживают HTTP/2
Post #202 1.13K
Продолжаю конспекты Ричардсона и его микросервисов. В прошлый раз я остановился на второй главе, где рассказывалось, как определить сервисы и что вообще такое микросервисная архитектура. Сегодня открываю третью главу - "Межпроцессорное взаимодействие в микросервисной архитектуре".

Сервисы могут использовать множество технологий для связи между друг другом: rest или grpc поверх http, или же механизмы коммуникации сообщений, такие как amqp или stopm. Форматы сообщений тоже различаются: от json до двоичных avro или protocol buffers.

Существуют множество стилей взаимодействия между клиентом и сервисом. Их можно разделить на два уровня. Первый опередляет отношения "один к одному" и "один ко многим".
"Один к одному" - каждый запрос обрабатывается ровно одним сервисом.
"Один ко многим" - каждый запрос обрабатывается несколькими сервисами.
Второй уровень определяет выбор между синхронным и асинхронным взаимодействием.
Синхронное - клиент ждёт ответ сразу после запроса и может даже заблокироваться на время ожидания.
Асинхронное - клиент не блокируется, а ответ, если придёт, может быть отправлен не сразу.

Общение "один к одному" может быть нескольких видов:
1. Запрос - синхронный ответ;
2. Запрос - асинхронный ответ;
3. Однонаправленные уведомления - клиент не ждёт ответа от сервера.

Общение "один ко многим" так же может быть нескольких видов:
1. Издатель/подписчик - клинет публикует сообщение с уведомлением, которое потребляется любым количеством заинтересованных сервисов;
2. Издатель/асинхронные ответы - клиент публикует сообщение с запросом и ждёт определённое время ответа от заинтересованных сервисов.

API приложения важно проектировать таким образом, чтобы его можно было развивать и поддерживать без урона для клиентов. Если минорные правки или новые API-методы, не ломающие логику, мы можем ввести в API без проблем, то мажорные изменения, такие как изменение формата данных уже существующих методов, нужно делать с помощью версионирования. Хороший подход - семантическая нумерация версий (semver.org). Это набор правил, регламентирующий, как использовать и увеличивать номера версий. Каждая версия API состоит из 3-х частей: major.minor.patch, где:
major - изменяется при внесении в API несовместимых изменений;
minor - изменяется при внесении в API изменений с обратной совместимостью;
patch - изменяется при исправлении ошибок с сохранением обратной совместимости.

Например, в случае REST API мажорную версию можно указать в качестве первого элемента URL-адреса (https://.../v1/...). Если сервис задействует обмен сообщениями, мажорную версию можно включать в публикуемое сообщение.
Post #201 1.22K
Как обычно мы биндим аргументы для сервиса в Symfony?
Правильно, через services.yaml. Например, мы создали интерфейс, у коготорого есть 3 реализации. Массив реализаций нам нужно передать куда-то в конструктор. У нас есть autowiring, и мы хотим его использовать. Переходим в services.yaml:

App\Service\SightingScorer:
arguments:
$scoringFactors:
- '@App\Scoring\TitleFactor'
- '@App\Scoring\DescriptionFactor'
- '@App\Scoring\CoordinatesFactor'

Где каждый из перечисленных классов реализует интерфейс ScoringFactorInterface.

Со временем реализаций становится всё больше, и всё больше вероятность, что при очередной деплое мы забудем обновить конфигурацию и прибиндить очередной класс. Так вот, ребята с symfonycast описали очень крутой способ, как биндить такие реализации автоматом. Идём в Kernel.php, там переопределяем метод build, и в нём добавляем одну строчку:
protected function build(ContainerBuilder $container)
{
parent::build($container);

$container->registerForAutoconfiguration(ScoringFactorInterface::class)
->addTag('scoring.factor');
}

Всё, после этого все реализации будут помечены тегом "scoring.factor", и в services.yaml нам достаточно один раз прописать:
App\Service\SightingScorer:
arguments:
$scoringFactors: !tagged_iterator scoring.factor

Такой подход называется тегированный итератор, и о нём можно почитать более подробно: https://symfony.com/doc/current/service_container/tags.html

Главное не забыть о том, что для того, чтобы автовайринг сработал нам нужно передавать аргументом итератор, а не массив:

private iterable $scoringFactors;

public function __construct(iterable $scoringFactors)
{
$this->scoringFactors = $scoringFactors;
}

Очень крутая штука, как по мне. И, в теории, что-то подобное можно реализовать и для других языков и фреймворков.
Symfony Service Tags (Symfony Docs) In Symfony applications, it's common that some services must be processed in a special way by the framework or by third-party bundles. For example, you may create a class that adds some custom filters…
Post #200 983
1. Латентность сети - определённый вид декомпозиции вынуждает сервисы часто обмениваться данными. Есть много способов минимизировать эту проблему, начиная от перепроектирования, и заканчивая реализацией API для извлечения нескольких объектов за один вызов.
2. Синхронное межпроцессорное взаимодействие: если один сервис оказался заблокированным, мы не сможем синхронно к нему обратиться, что ухудшит доступность.
3. Обеспечивание согласованности: если один вызов подразумевает обновление информации в нескольких сервисах - нужно что-то вроде логической транзакции, чтобы данные остались согласованными.
4. Получение согласованного представления данных: разные БД могут выдавать не согласованные данные в контексте одного процесса.
5. Божественные классы: раздутые классы, используемые в разных частях приложения. Проблема тут очевидна - слишком сильная связанность. Одно из решений - упаковка такого класса в библиотеку и создание центральной базы. Проблема тут в том, что такой подход нарушает принципы микросервисной архитектуры и, опять же, приводит к связыванию. Ещё одно решение - сделать для разных сервисов свою модель класса, привязав его только к тем возможностям, которые требует данный сервис. Например, в сервисе Delivery класс Order будет работать с адресом получения, временем получения, адресом и временем доставки.
Тот же класс Delivery в сервисе ресторанов будет работать с только с тем, что связывает заказ и рестораны.
Post #199 932
Следующие главы более подробно описывают то, как правильно выделить микросервисы. Информации много, и чтобы корректно её понять в полном контексте, крайне желательно прочитать и посмотреть на все таблицы своими глазами. Я лишь приведу очень сокращенный конспект.
Как я уже писал выше, автор предлагает делать это в 3 этапа:
* Определить системные операции: определяем пользовательские истории и связанные с ними сценарии использования.
Сначала определяется доменная модель: перечисление основных сущностей приложения, завязанных на требования к приложению. Например, для приложения-доставки это могут быть: Order, Restaurant, Delivery, Courier и другие.
Затем определяются операции, связанные с этими сервисами. Например, клиент может создать заказ - операция createOrder. Ресторан может взять заказ на обработку - операция acceptOrder. Курьер может доставить заказ - операция deliveryDelivered. Аналогично с операциями опередляются запросы, которые приложение будет обрабатывать: запрос на доступные рестораны - findAvaliableRestaurants, и так далее.
На этом этапе, вероятно, не получиться учесть все операции и запросы, но чем больше операций и запросов получиться выделить и спроектировать - тем проще будет их сгруппировать и тем больше будет вероятность того, что сервисы будут выделены верно.
* Второй этап - разбиение на сервисы, исходя из запросов, описанных ранее. Вероятно, доменная модель сможет показать сервисы без каких-либо дополнительных действий. Но, автор приводит и несколько стратегий, которые можно применить, если явно выделить сервисы не получается.
Первая - разбиение на сервисы по бизнес-возможностям. Бизнес-возможности определяют то, чем занимается организация и что она предлагает своим клиентам. Например, что предлагает клиентам приложение-доставка?
1. Управление поставщиками - курьерами и информацией о ресторанах.
2. Управление клиентами
3. Прием и выполнение заказов: создание заказов, управление заказами, логистика, управление доступностью курьеров, управление доставкой
4. Бухучёт: отчётность по клиентам, по курьерам, по доставкам и так далее.
5. ...

Иногда сервисы создаются для бизнес-возможностей верхнего уровня, а иногда отдельный сервис может быть создан для подвозможности. Например:
Управление курьерами - сервис Courier (сервис для подвозможности)
Управление информацией о ресторанах - сервис Restaurant (подвозможность)
Управление клиентами - сервис Consumer
Управление заказами для клиента, создающего заказ - сервис Order (подвозможность)
Управление заказами в ресторане - сервис Kitchen (подвозможность)
Управление доступностью курьеров и доставка - сервис Delivery (подвозможности)
Бухучёт - сервис Accounting
И так далее.

Решение, для каких возможностей создавать отдельный сервис - субъективно.

Вторая стратегия - разбиение по проблемным областям. Тут уже включается DDD, с его необычайно большим набором терминов, и этот момент я пропущу, так как автор затрагивает его очень поверхностно, а для полного понимания нужно читать дополнительную литературу. Но, в общем и целом, разбиение по проблемным областям часто дают схожий результат с разбиением по бизнес-возможностям.

Смотря на примеры сервисов, приведённые автором, явно вспоминается принцип единственной ответственности, и он тут на самом деле фигурирует. Мы выделяем команды и запросы, а потом группируем их и формируем на их основе отдельный сервис, который будет иметь, очень абстрактно, одну причину для изменений.
Ещё один принцип, применяемый при декомпозиции - принцип согласованного изменения: изменение пакета должно затрагивать все его классы (Роберт Мартин). Или, другими словами: если два класса изменяются вместе по одной и той же причине - они должны входить в один пакет.

Какие трудности могут возникнуть при разбиении приложения на сервисы? Помимо пары сотен других, автор приводит следующие:
Post #197 1.07K
Микросервисная архитектура - тоже архитектурный стиль. Реализация, как правило, представима в виде набора компонентов. Компоненты предствлены сервисами, а в качестве коннекторов - коммуникационные протоколы (например, AMQP вместе с rabbitmq).
Каждый сервис имеет собственную архитектуру, как правило - шестигранную (читай выше).
Каждый сервис можно выделить из какой-либо бизнес-возможности. Например для приложения-доставки, работающего с ресторанами, курьерами и пользователями, можно выделить следующие сервисы (на самом деле потенциально сервисов может быть гораздо больше):
* Сервис ресторанов - светит наружу API, с помощью которого клиент может посмотреть меню и сделать заказ;
* Сервис доставок - светит наружу API, с помощью которого курьер видит, какой заказ и куда ему доставлять.
* Сервис заказов - сервис, обрабатывающий заказы, получая их от ресторана и передающий их в сервис доставки.
* Сервис уведомлений - сервис, с помощью которого другие сервисы могут отправлять клиентам и курьерам информацию о готовности заказов.

Связь между сервисами реализуется с помощью механизма межпроцессорного взаимодействия, например REST API или асинхронного обмена сообщениями.
API состоит из команд (например, обновить заказ), запросов (получить данные по заказу) и событий (заказ создан), которые могут потреблять внешние клиенты.
API сервиса инкапсулирует его внутреннюю реализацию. В отличие от монолита, этот подход не позволяет разработчику писать код, минующий API. Благодаря этому обеспечивается модульность приложения.
Ключевое здесь - слабая связанность сервисов, т.е. каждый сервис ограничен в том, как он может общаться с другими сервисами.
Каждый микросервис обладает собственной архитектурой и иногда разным стеком технологий. Но, как правило, все сервисы имеют шестигранную архитектуру: бизнес-логика вызывается адаптером операций, а события, которые она генерирует, публикуется адаптером событий наружу.

Слабо-связанные сервисы обязательно должны взаимодействовать только через API, и исключать коммуникацию через базу данных.


Теперь коротко об интересном и холиварном: как выделить микросервисы?
Зачем нам вообще приложение и какой его основной функционал? Обрабатывать запросы. Поэтому первый шаг - формируем ключевые запросы, описывая, что мы хотим от приложения и что оно должно сделать в ответ. Например, мы хотим, чтобы клиент смог разместить заказ. А ещё хотим, чтобы ресторан мог этот заказ принять. Наши хотелки - это требования, выраженные в виде пользовательских историй. Когда мы сформировали все требования, из них мы можем выделить внешние запросы, которые будут отправляться приложению. В нашем случае это createOrder, для создания заказа от пользователя и acceptOrder, для приёма заказа рестораном.
Когда все запросы описаны, переходим ко второму шагу - разбиваем на сервисы исходя из запросов. Каждый запрос можно отнести к тому или иному домену: например, запрос createOrder можно отнести к абстрактному домену "Order", запрос acceptOrder можно отнести к доменам "Order" и "Restaurant".
Разбив все запросы на домены, мы сможем выделить, например, сервисы Order, Restaurant, Kitchen, Delivery и так далее.
Третим шагом мы назначаем всем сервисам те операции, которые были выделены на первом шаге. Тут уже будет приблизительно понятно, как сервисы будут взаимодействовать между собой и какой примерный API у них будет.
Post #196 1.1K
Продолжаю обзор "Микросервисов" Ричардсона. Сегодня вторая глава, дающая уже более интересный материал, чем просто перечисление паттернов.

Чтобы разобраться с тем, как проектировать и строить микросервисную архитектуру, нужно сначала дать определение архитектуре программного обеспечения как таковой. Ричардсон приводит определение Лена Басса:
"Программная архитектура вычислительной системы - это набор структур, необходимых для её обсуждения и состоящих из программных элементов, связей между ними и свойств, присущих этим элементам и связям".

Модель представлений архитектуры вида 4+1
Архитектура 4+1 - это универсальный способ описания архитектуры любого приложения, предложенный Филиппом Кратченом, который состоит из четырёх разных представлений архитектуры ПО, где каждое описывает определённый аспект и состоит из определённого набора элементов и связей между ними:
* Логическое представление - модули, создаваемые разработчиками. Например, в ООП языках это классы и пакеты. Связи между ними - отношения между классами, включая наследование и зависимости.
* Представление реализации - результат работы системы сборки. Для компилируемых языков, например, это исполняемый файл, с соответствующими зависимостями, представленными в компилируемом виде.
* Представление процесса - компоненты на этапе выполнения. Каждый элемент является процессом, а отношения между ними - межпроцессорное взаимодействие.
* Развертывание - то, как процессы распределяются по устройствам. Элементами тут выступают сервера и процессы. Связи между ними - сеть.
+1 - это сценарии, определяющие связи и то, как представление обрабатывает запрос.

Архитектура выходит на первый план тогда, когда речь заходит не о функционале, который нам нужно реализовать (даже в самой плохой архитектуре можно реализовать новую фичу, вопрос лишь в том, насколько это будет больно), а о качестве обслуживания (простота, тестирование, скорость доставки изменений).

Есть разные стили архитектуры.

Классический - многоуровневый стиль, в котором можно выделить наиболее частый - трёхуровневый:
* уровень представления - код, реализующий пользовательский интерфейс или внешние API
* уровень бизнес-логики
* уровень хранения данных - реализация взаимодействия с базой данных
Недостатки многоуровневой архитектуры:
* Подразумевается единый уровень представления и не учитывается, что клиентов может быть несколько
* Единый уровень хранения данных не подразумевает, что будет работа с более чем одной базой
* Уровень бизнес-логики зависит от уровня хранения данных - в теории эта зависимость не позволяет тестировать бизнес-логику отдельно от БД

Шестигранный стиль - аналог многоуровневой архитектуры, ставящий бизнес-логику в центр. Вместо уровня представления у приложения есть адаптеры, которые обрабатывают внешние запросы, вызывая бизнес-логику. Вместо уровня хранения данных используются адаптеры, вызываемые бизнес-логикой и обращающиеся к внешним приложениям, например через брокер сообщений. В чём профит? Бизнес-логика не зависит от адаптеров, наоборот - адаптеры зависят от бизнес-логики. Инверсия зависимостей в чистом виде.
Post #195 1.22K
Начал читать "Микросервисы" Ричардсона, а поэтому начинаю вести краткие конспекты.

В микросервисной архитектуре есть несколько типов шаблонов, из которых строится сама архитектура, инфраструктура и процесс разработки.

1 тип: Шаблоны для разбиения приложения на микросервисы
* Разбиение по бизнес-возможностям
* Разбиение по проблемным областям

2 тип: Шаблоны взаимодействия
* Транзакционный обмен сообщениями
* Стиль взаимодействия
* Надежность
* Обнаружение
* Внешний API

3 тип: Шаблоны согласованности данных для реализации управления транзакциями
Для обеспечения слабой связанности каждый сервис должен иметь собственную базу данных. Такой подход чреват проблемами, для решения которых следует использовать шаблон "Повествование".

4 тип: Шаблоны запрашивания данных в микросервисной архитектуре
Использование отдельной базы для каждого сервиса имеет ещё один недостаток: некоторые запросы должны объединять информацию с нескольких сервисов, а значит, мы хотим иметь возможность использовать транзакционный подход к этим запросам. Тут помогут такие шаблоны запросов, как:
* Объединение API (обращение к API одного или нескольких сервисов и агрегирование результатов)
* CQRS (командные запросы с разделением ответственности, которые хранят одну или несколько копий данных и позволяют легко к ним обращаться).

5 тип: Шаблоны развертывания сервисов
* Традиционно - развертывание сервисов в формате упаковки определённого языка (нельзя масштабировать для поддержки микросервисной архитектуры)
* Аналог - развертывание в виде виртуальных машин или контейнеров
* Бессерверные технологии

6 тип: Шаблоны наблюдаемости, позволяющие понять, как ведёт себя приложение
* API проверки работоспособности - роут, возвращающий набор метрик, показывающих, как работает приложение
* Аггрегация журналов - логи, желательно с поддержкой поиска
* Распределённая трассировка - назначение уникальных идентификаторов для каждого запроса для отслеживания его перемещений между сервисами
* Отслеживание исключений - автоматическая реакций на ошибки, часто это отдельный сервис, умеющий определять по логу какую команду оповещать
* Показатели приложения
* Введения журнала аудита - журнал действий пользователя

7 тип: Шаблоны автоматического тестирования сервисов
* Тестирование с растчетом на потребителя - проверка того, что сервис отвечает ожиданиям клиентов
* Тестирование на стороне потребителя - проверка того, что клиент может взаимодействовать с сервисом
* Тестирование компонентов сервиса в изоляции

8 тип: Шаблоны для решения сквозных проблем
* Шаблон шасси микросервисов - построение архитектуры с выбором ниболее подходящего инструмента под конкретную задачу

9 тип: Шаблоны безопасности:
Шаблоны безопасности выбираются в зависимости от метода коммуникации сервисов и клиентов. Например для API наиболее часто применяется такой шаблон как JWT-токен.

Мне показался крайне интересным закон Конвея, который приводит Ричардсон:
Организация, проектирующая системы, обречена воспроизводить архитектуру, имитирующую структуру собственных коммуникаций.

Это говорит о том, что множество небольших продуктовых команд гораздо более эффективны, чем одна большая команда на несколько десятков человек.
Post #193 1.87K
Сегодня, продолжая 3-ю главу, опять поговорим о том, какие бывают индексы и как их правильно использовать.

Индексы могут быть кластеризованными (по ключу хранятся все данные) или некластеризованными (по ключу хранится ссылка на данные). Например, InnoDB в MySQL - использует кластеризованные индексы, т.е. когда мы делаем запрос с индексом, сначала идёт поиск в memory-структуре, в зависимости от типа индекса, и затем, в случае успеха, мы сразу можем получить запрашиваемые данные. У некластеризованных индексов в случае успешно найденного значения, представляющего ссылку, нам потребуется выполнить переход по ней, считать значение и только потом его вернуть.
Бывает так же и компромисс между кластеризованным и некластеризованным - охватывающий индекс. Этот тип индекса хранит в себе не всю строку, а часть столбцов. Как правило, эта та часть, которая чаще всего запрашивается запросами.

Если мы запрашиваем сразу несколько столбцов строки, то нам могут потребоваться составные индексы. Самый частый тип составных индексов - сцепленные индексы: объединение нескольких полей в один ключ, например: имя-фамилия.
Второй по популярности тип составных индексов - многомерные индексы, особенно часто они используются при работе с пространственными данными, например, при работе с PostGIS от Postgres. Для многомерных пространственных индексов используются R-деревья, позволяющие производить поиск по многомерным данным.
Многомерные индексы применяются не только для работы с адресами. Например, можно использовать двумерный индекс (год, температура), чтобы одним запросом найти данные за 2013 год, когда температура была -20 градусов. Иначе, без многомерных индексов, нам придётся сначала найти все данные за 2013 год, а затем фильтровать их по температуре.

Интересная, в плане использования индексов, тема - полнотекстовый поиск. Например, известные индексы Lucene, лежащие в основе эластики, хранят словарь термов в SS-структуре (которую я описывал в прошлый раз), а рядом с этой структурой они хранят небольшой индекс, который описывает, как разбивается (на буквы, или на триграммы, или как-то иначе) каждый терм. На всё это накладываются различные алгоритмы, например конечный автомат в связке с расстоянием Левенштейна, что позволяет очень быстро искать слова по заданному соответствию.

Все описанные выше структуры так или иначе подразумевают запись на диск. Хотя основная работа происходит в оперативной памяти, в случае с LSM-таблицами на диске у нас хранятся уплотнённые сегменты, а в случае с B-деревьями на диске у нас хранится информация, куда ссылается B-дерево. И, само собой, на диске хранятся данные самих таблиц.
По мере удешевления RAM у дискового хранения остался 1 аргумент над хранением в оперативной памяти: надёжность. Хотя в последние годы, благодаря различным инструментам и подходам (репликации на разные сетевые копии; память, питаемая от отдельного аккумулятора; ротация журнала состояния на диск) появилась возможность хранить данные только в оперативной памяти. Уже существуют несколько РСУБД, которые работают в RAM: VoltDB, MemSQL, Oracle TimesTen.
Telegram 1000 дней программирования ​Поиск по триграммам или как найти соответствие по запросу с ошибками Предлагаю тебе отвлечься от новостей вокруг и сгладить своё заточение дома новым материалом, которым я открываю серию статей о полнотекстовых поисках - Поиск по триграммам Какой кейс:…
Post #192 1.62K
Индексы на основе журнала - не самый популярный тип индексов. Чаще используются индексы на основе B-дерева.
В отличии от журналированных индексов, которые индексируют базу данных сегментами по несколько мегабайт и всегда записывают эти сегменты на диск последовательно, B-деревья разбивают базу на сегменты по несколько килобайт (часто - по 4Kb) и читают/пишут по 1 странице за раз. Такое разбитие и такой размер сегментов отлично накладываются на нижележащие аппаратные слои.
Все страницы имеют свой адрес, и на основе таких ссылок строится дерево. Одна из страниц - корень, с него начинается любой поиск ключа. Все ключи во всех сегментах - отсортированы, и любой узел дерева содержит некоторый диапазон сегментов и несколько ссылок на поддиапазоны.
Если на странице заканчивается место, то создаётся ещё одна ссылка, и страница делится на две полу-пустых страницы.
Такой алгоритм гарантирует, что дерево будет сбалансированным, т.е. любой поиск будет занимать O(log n), что весьма быстро.
Чтобы обезопасить данные от потери во время сбоя, на диске так же хранится журнал упреждающей записи, в который все добавляемые данные записываются перед тем, как попасть в B-дерево.

А зачем тогда нужны LSM-деревья, если есть B-деревья? LSM-деревья реализуют как одну из подсистем во многих СУБД из-за того, что они быстрее при записи. Нам не нужно идти по дереву и искать место, куда добавить ключ. Нам достаточно записать данные в in-memory-структуру и сдублировать их в журнал, и всё. И для таких задач LSM-деревья до сих пор используются.
В свою очередь, B-деревья быстрее при чтении.
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →