Читать в телеге. Когда-то там были посты не только от меня.
Большие стрелки и маленькие квадратики
Когда готовил доклад про скучное программирование, то не нашел эту статью, поэтому заменил затравку на цитату Хоара (“Inside every large program is a small program struggling to get out.”).
А ключевая идея статьи заключается в том, что больше всего времени разработчика тратится не на бизнес-логику приложения, а на интеграции и передачу данных между различными слоями и компонентами. И картинки с тонкими стрелочками и крупными блоками искажают картину. С точки зрения трудозатрат они должны выглядеть примерно так:
Хотя с другой стороны, толстые стрелки можно обозвать деталями реализации, ведь многие говорят о том, что данные важнее алгоритмов: Брукс, Пайк, Торвальдс. Возможно, это применимо и к архитектуре приложения.
Автоматическая репликация на все узлы Elasticsearch
Как-то совсем мимо меня прошла полезная настройка, которая была даже в первой версии эластика. index.auto_expand_replicas со значением 1-all
позволит иметь по реплике индекса на всех узлах кластера, что очень удобно для небольших вспомогательных индексов (настройки, пользователи и т.п.).
Самоорганизующаяся команда
Неплохая статья про самоорганизующиеся команды и уровни зрелости команды.
TLDR:
- Директивное управление работает на энергии руководителя, кончится она — кончится команда.
- У команды должна быть четкая и понятная цель, общее видение и ответственность.
- Члены команды должны знать контекст друг друга, работа всех должна быть прозрачна.
- Если переборщить с самостоятельностью, то можно начать выходить за зоны своей ответственности или указывать бизнесу на его недочеты (и это может быть как плюс, так и минус).
- Чем больше нужно согласований для действий, тем меньше инициативы в команде.
И другая статья, в которой изложены похожие мысли, но отмечается, что работать это будет не всегда: не все люди готовы брать инициативу и принимать решения.
Коммит в соавторстве
Иногда на GitHub можно увидеть коммит от нескольких авторов (например). Сделать совместный коммит довольно просто: нужно просто добавить строку в коммит с текстом
Co-authored-by: name <name@example.com>
Разумеется, туда можно вписать что угодно. Отображение соавторства поддерживается GitHub и GitLab, но это не стандартная фича git. При этом есть и другие “прицепы”.
Индексирование нового поля в ElasticSearch
Чтобы добавить новое поле в существующий индекс, новый индекс создавать не нужно, достаточно обновления маппинга. Но старые данные не будут переиндексированы, даже если это поле было в документах раньше. Чтобы индексировать их, можно выполнить update by query с пустым скриптом. В запросе имеет смысл указать новое поле как отсутствующее и, возможно, какое-нибудь старое поле как маркер необходимости обновления документа в индексе (чтобы не индексировать все подряд).
POST your-index/_update_by_query?conflicts=proceed&wait_for_completion=false
{
"query": {
"bool": {
"must_not": [{
"exists": {
"field": "new.field"
}
}],
"must": [{
"exists": {
"field": "exisiting.field.as.flag"
}
}]
}
}
}
Пулл-реквест в Kotlin
Пара недавних постов были подводкой к этому (и еще один грядет). Захотел сделать пулл-реквест в Kotlin, выбрал один из “простых” тикетов. Выкачал репозиторий с GitHub, импортировал в Idea и начались проблемы.
Для регресса нужна установка Java 6. В Readme рекомендуют пакетные менеджеры, аж 3 штуки, но все они устанавливаются только через curl | sh
, что опять меня триггернуло. Заленился искать адекватный вариант, в итоге просто отключил флагом gradle.
Попробовал запустить тесты. Один тест не запустился — оказалось, что надо было сначала сгенерировать все тесты, а потом уже запускать compilerTest
. Кстати, хрен найдешь этот таск в списке грейдловских. Сгенерированные тесты не прошли, работало больше 2 часов, завершения тестов javascript я так и не дождался. В завалившихся, кроме javascript, были еще тесты с Java 9-15, но даже если запуститься с Java 15, ситуация не изменилась. Причем в репозитории лежит .idea
-проект, т.е. подразумевается, что все уже настроено, а по факту — даже просто прогнать тесты уже не так просто, причем проблема не только у меня, а может это и вовсе норма.
Сам проект выглядит не очень ухоженным. Открытых тикетов больше 4 тысяч, веток 3,5 тысячи, TODO в коде 9 тысяч, некоторые пулл-реквесты висят с 2017 года (бывают и просто грустные). В коде встречаются непонятные сокращения, периодически выскакивают java-файлы, форматирование и нейминг неконсистентные, есть даже файлы, названные по идентификатору тикетов в YouTrack. Создается впечатление, что раньше Kotlin был прибит гвоздями к Idea, потому что плагин был в основном коде языка и добавление инспекции могло привести к изменению языка (судя по истории git). Структура проекта вроде понятна, но найти место, где нужно поменять код, не очень тривиально (хотя это скорее я просто затупил). У меня возникло подозрение, что есть код, вообще не покрытый тестами, ну или я их просто не смог найти. У некоторых тестов интересный формат входных данных: котлиновский файл с кучей комментариев для управления проверками (но документации нет).
Выбранный тикет так и не получилось сделать: сначала я долго не понимал, что именно там нужно сделать и где, а потом пришел к выводу, что не факт, что эти изменения вообще нужны. Даже совсем плюнуть хотел на эту затею, но потом вспомнил о баге с манифестом. В нем хотя бы было понятно, что делать и откуда начать. Корневая причина оказалась не там, где я думал, но в итоге со всем разобрался (хотя тесты чужеродно выглядят). Мой пулл-реквест в итоге приняли (achievement unlocked!), но ревьюер проделал больше работы, чем я: я упустил довольно серьезный баг, поэтому не очень собой доволен. А еще TeamCity шлет письма по валящимся билдам, в которые попал мой коммит (хорошо хоть проблемы вызваны не моими изменениями).
Переключение на форк
Если возитесь в скачанных исходниках и появилась потребность внести исправления, то не нужно качать заново качать код из своего форка.
Можно просто переименовать origin
в upstream
, добавить форк в качестве origin
и подтянуть информацию из форка.
git remote rename origin upstream
git remote add origin git@FORK
git fetch origin
Уровни компиляции JIT
В продолжение темы про работу JVM.
Обычно исходный код компилируется в байткод компилятором языка. Потом проходит его верификация и линковка, после которых он попадает в интерпретатор. И уже интерпретатор переводит его в исполняемый (машинный) код. Хотя можно вообще все скомпилировать заранее, как в C: например, с некоторым ограничениями это позволяет сделать GraalVM.
Есть 4 уровня компиляции:
- Интерпретатор
- C1 без профилирования
- C1 с минимальным профилированием
- C1 с максимальным профилированием
- C2 со всеми оптимизациями
Интерпретатор выполняет байт-код максимально тупо, “построчно”. В процессе исполнения интерпретатор приблизительно считает, как часто вызывался каждый метод, и после достижения порога он переходит на следующий уровень компиляции — третий. Метод компилируется, в него добавляются счетчики, чтобы получить более точную картину нагрузки. Он исполняется быстрее, чем в интерпретаторе, но все равно медленно. Потом метод попадает на уровень C2. Однако компилятор C2 довольно медленный и не всегда успевает все сделать, поэтому перед попаданием на третий уровень, метод иногда тусит на втором, “в очереди” — исполнение побыстрее третьего, но тут меньше информации. Бывает, что компиляция в C2 невозможна — тогда профилирование выкидывается, и метод работает на первом уровне без профилирования. А иногда JVM решает, что метод очень простой, сразу закидывает его на первый уровень и больше не трогает.
Можно отключить C1 (уровни 1-3) совсем, тогда метод будет попадать из интерпретатора сразу на C2 — и так было до JVM 8: у “серверных” приложений был С2 (медленный старт, но быстрая работа), а у клиентских — C1 (быстрый старт, но медленнее работа).
Возможна и деоптимизация, когда уже скопилированный код выкидывается и метод выполняется на интерпретаторе. Происходит это обычно из-за того, что спекулятивные предположения компилятора не оправдались (причин масса), и код не может работать корректно. В этом случае инлайнинг может навредить производительности: вместо маленького кусочка в интерпретатор может быть выкинут большой кусок с кучей заинлайненного кода.
Как настроить память для JVM
Некоторые утверждают, что настройки можно определить автоматически, но звучит это сомнительно, потому что JVM — довольно сложная штука.
Память JVM разбита на несколько областей:
- Куча (heap). В ней хранятся основные данные и работает сборщик мусора (GC), накладные расходы на который бывают до 10%. Кучу можно зарезервировать сразу всю флагом (
-XX:+AlwaysPreTouch
), а можно динамически уменьшать и отдавать системе (-XX:UseAdaptiveSizePolicy
), поигравшись с настройками зарезервированного запаса, но это работает не с любым GC. Источник проблем в этой области — большие объемы данных. - Метаданные классов. Всякие структуры классов, символы, константы, аннотации и т.п. В поздних версиях разделена на непрерывную область для классов (чтобы можно было сжимать указатели) и область для остальных метаданных. Гадостей в этой области может наделать загрузчик классов, генерирующий кучу классов на каждый чих.
- Область JIT-компилятора. Включает в себя скомпилированные куски кода и “арены” компилятора (где хранятся необходимые данные для компиляции: промежуточные представления, статистика и т.п.). Если включена многоуровневая компиляция (по умолчанию это так), то размер области под скомпилированный код увеличивается. Опасность плохих настроек тут в том, что область забьется, и код будет компилироваться и вытесняться по кругу, сжирая процессор. В стабильной системе JIT прогревается на старте и потом почти не вызывается.
- Потоки со стеками вызовов. По утверждению докладчика, тут обычно волноваться не о чем.
- Таблица символов и таблица строк. Тут тоже редко бывают проблемы.
- Внутренняя память. Содержит память, выделенную вне области с GC: ByteBuffer’ы (есть настройка для ограничения) и замапленные в память файлы (нельзя ограничить по памяти). Причем у буферов могут быть кэши в куче, которые еще локальны для потока и никогда не чистятся (но можно ограничить их размер). При аллокации буферов могут возникнуть проблемы с фрагментацией, поэтому стандартный аллокатор имеет смысл заменить, например на jemalloc.
Найти проблемы с утечками памяти можно с помощью async-profiler, который может перехватывать вызовы malloc
и определять стектрейс к ним.
В итоге, чтобы определить проблемные места, нужно мониторить все метрики (в том числе включить логи GC и смотреть на метрики операционной системы) и подключать Native Memory Tracking для исследования тяжелых случаев. Посчитать итоговую формулу для контейнера нельзя. Хотя по опыту коллег примерно половина памяти контейнера уходит на память вне кучи.
Разновидности event-driven архитектуры
Согласно Фаулеру (видео), существует 3 типа Event-Driven архитектуры:
- event notification — уведомление о событии. У меня что-то поменялось, дерни меня синхронно, чтобы узнать что.
- event-carried state transfer — передача состояния в событиях. У меня поменялось поле, новое значение такое-то. Или мое состояние поменялось, вот текущее.
- event sourcing — нет никаких состояний, есть только набор событий, они являются мастер-данными. Хочешь состояние — либо сам храни, либо вычисляй. Иногда, так и быть, дадим тебе слепок, чтобы попроще было вычислять.