Все специализации

Разработчик встраиваемых систем: вопросы на собеседовании

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

Для выполнения параллельных HTTP-запросов к двум URL можно использовать многопоточность или асинхронные вызовы. Ниже пример на Python с использованием модуля concurrent.futures и библиотеки requests:

Этот код создаёт два параллельных запроса и выводит коды ответов.

Программа выполнит 10 000 итераций, в каждой из которых вызывается функция networkRequest, которая делает паузу в 1 миллисекунду и увеличивает глобальную переменную count.

Вывод программы будет:

Время выполнения примерно:

Каждая итерация занимает около 1 миллисекунды (из-за time.Sleep(time.Millisecond))

Всего 10 000 итераций

Значит, общее время около 10 000 миллисекунд, или примерно 10 секунд.

Программа выполняется последовательно, без параллелизма, поэтому время суммируется.

Код выведет:

Объяснение:

В main создаётся указатель person на структуру Person с именем "Bob".

При первом fmt.Println(person.Name) выводится "Bob".

Функция changeName принимает указатель на Person и присваивает по этому указателю новую структуру с именем "Alice".

Это изменяет содержимое по адресу, на который указывает person.

Поэтому после вызова changeName(person) значение person.Name становится "Alice".

В вашем примере в комментариях указано, что второй вывод будет "Bob", но это неверно. Поскольку функция меняет значение по указателю, имя изменится на "Alice".

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

Docker Image — это шаблон или «снимок» контейнера, содержащий все необходимые файлы, библиотеки и настройки для запуска приложения. Из образа создаётся контейнер.

Docker Compose — инструмент для определения и запуска многоконтейнерных Docker-приложений. С помощью файла docker-compose.yml можно описать несколько сервисов, их связи, сети и тома, а затем запустить всё одной командой.

Пример docker-compose.yml:

python

import asyncio

import aiohttp

async def fetch_status(session, url):

async with session.get(url) as response:

print(f"URL: {url} - Status code: {response.status}")

async def main():

urls = [

'https://www.google.com',

'https://shtrafovnet.ru'

]

async with aiohttp.ClientSession() as session:

tasks = [fetch_status(session, url) for url in urls]

await asyncio.gather(*tasks)

if name == 'main':

asyncio.run(main())

В модели GMP (GNU Multi-Processing) количество процессоров соответствует числу физических или логических ядер, доступных системе. Если в системе 24 ядра, то в GMP модели будет 24 процессора.

Это означает, что GMP создаст 24 рабочих потока (процесса), которые могут выполняться параллельно, используя все доступные ядра для максимальной производительности.

Код выведет:

Объяснение:

В функции main создаётся указатель на структуру Person с именем "Bob".

При первом выводе печатается person.Name — "Bob".

Функция changeName принимает указатель на Person и изменяет поле Name на "Alice".

После вызова changeName значение поля изменяется, поэтому второй вывод показывает "Alice".

В Go при передаче указателя в функцию изменения структуры отражаются на оригинальном объекте.

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

Вывод программы будет:

По времени выполнение примерно составит 10_000 миллисекунд, то есть около 10 секунд, так как вызовы networkRequest() идут последовательно, и каждый задерживается на 1 мс.

Если бы запросы выполнялись параллельно (например, с использованием горутин), время могло бы значительно сократиться.

В Java публичные поля не рекомендуются, потому что они нарушают принцип инкапсуляции — одну из основ объектно-ориентированного программирования. При использовании public полей:

Нет контроля над тем, как и когда изменяются данные.

Невозможно добавить логику при чтении или записи (например, валидацию).

Трудно менять внутреннюю реализацию класса без влияния на внешний код.

Переход к private полям с getter/setter позволяет:

Контролировать доступ к полям (например, сделать поле только для чтения).

Добавлять дополнительную логику при получении или установке значения (валидация, преобразование).

Сохранять совместимость при изменении внутренней структуры класса.

Пример:

Такой подход делает код более надежным и поддерживаемым.

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

Индексы:

Создавал индексы на часто используемых в WHERE и JOIN полях, чтобы ускорить поиск.

Использовал составные индексы для запросов с несколькими фильтрами.

Применял частичные индексы, чтобы индексировать только релевантные подмножества данных.

Анализировал планы выполнения запросов через EXPLAIN, чтобы выявить необходимость новых индексов или их перестройку.

Регулярно выполнял VACUUM и ANALYZE для обновления статистики и очистки.

Транзакции:

Использовал транзакции для обеспечения атомарности операций, особенно при сложных изменениях данных.

Контролировал уровень изоляции транзакций (например, READ COMMITTED или SERIALIZABLE) в зависимости от требований к консистентности и производительности.

Минимизировал длительность транзакций, чтобы снизить блокировки и повысить параллелизм.

В случаях массовой загрузки данных применял батчи внутри транзакций для оптимизации.

Пример создания индекса:

Пример использования транзакции в SQL:

Когда бины объявлены через @Configuration и @Bean, Spring создает их с именами, соответствующими имени метода, который возвращает бин. Например:

В этом случае имя бина — myService.

Аннотация @Qualifier используется для уточнения, какой именно бин нужно внедрить, если в контексте есть несколько бинов одного типа. Например:

Здесь @Qualifier указывает Spring, что нужно внедрить бин с именем myService2.

Таким образом, @Qualifier работает по имени бина, которое по умолчанию совпадает с именем метода, объявляющего бин, если явно не указано иное через параметр name в @Bean.

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

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

Основные отличия:

Kafka хранит сообщения дольше и позволяет читать их несколько раз, RabbitMQ удаляет сообщения после доставки.

Kafka лучше подходит для обработки больших потоков данных и аналитики, RabbitMQ — для классической очереди задач и распределения работы.

Пример: если нужно обрабатывать логи в реальном времени и хранить их для анализа, выбирают Kafka. Если нужно распределять задачи между воркерами, удобнее RabbitMQ.

В Java при переопределении метода clone(), который объявлен в классе Object с сигнатурой:

вы не можете расширить список проверяемых исключений (checked exceptions). То есть, если вы переопределяете этот метод, вы можете:

Не объявлять throws вовсе,

Или объявить throws CloneNotSupportedException или его подкласс,

Но не можете добавить throws Exception, так как это более общее исключение.

Если попытаться подписать throws Exception, компилятор выдаст ошибку, так как метод с аннотацией @Override должен строго соответствовать или сужать сигнатуру базового метода.

Пример правильного переопределения:

Таким образом, подписывать throws Exception нельзя.

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

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

Юнит-тесты пишу регулярно, особенно для критичных функций и драйверов, чтобы обеспечить стабильность и облегчить поддержку кода. Например, использую фреймворки типа Unity или Ceedling для тестирования C-кода.

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

Желание профессионального роста и новых вызовов.

Поиск проектов с более интересной или современной технологией.

Изменение личных обстоятельств (переезд, семейные причины).

Несовпадение ожиданий по развитию карьеры или условиям работы.

Важно избегать негативных высказываний о предыдущем работодателе и сосредоточиться на позитивных мотивах.

Время выполнения после оптимизации зависит от многих факторов: от характера задачи, эффективности алгоритма, а также от аппаратных ресурсов.

Если говорить про параметр GOMAXPROCS в Go, он задаёт максимальное количество потоков ОС, которые могут одновременно выполнять Go-рутины. Увеличение GOMAXPROCS может ускорить выполнение, если задача хорошо распараллеливается и есть несколько ядер процессора.

Однако если задача не параллельна или ограничена по ресурсам (например, ввод-вывод, синхронизация), увеличение GOMAXPROCS не даст значительного прироста.

Таким образом, время выполнения после оптимизации может существенно сократиться, если оптимизация включает распараллеливание и правильное использование GOMAXPROCS, но точное время зависит от конкретной реализации и нагрузки.

Планировщик Go основан на модели MPG (M — Machine, P — Processor, G — Goroutine):

G (Goroutine) — легковесная нить исполнения.

P (Processor) — логический процессор, который связывает G и M.

M (Machine) — системный поток ОС, который выполняет код.

В Go есть фиксированное число P, которое ограничивает количество одновременно выполняющихся горутин на уровне ОС потоков.

Work Stealing — механизм балансировки нагрузки, когда P с меньшей нагрузкой «ворует» горутины у других P, чтобы равномерно распределить задачи.

Handoff — передача горутины от одного P к другому без помещения в глобальную очередь, что снижает задержки.

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

SKIP LOCKED — это опция в PostgreSQL для оператора SELECT ... FOR UPDATE, которая позволяет пропускать заблокированные строки при выборке. Это полезно для реализации очередей задач, где несколько воркеров одновременно берут задачи из таблицы.

Когда один процесс блокирует строку (задачу), другие процессы с SKIP LOCKED просто пропускают её и берут следующую доступную задачу, не ожидая освобождения блокировки.

Пример использования для очереди задач:

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

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

Например, пользовался такими инструментами как:

gprof для анализа времени выполнения функций в Linux-среде.

Valgrind (Callgrind) для анализа использования памяти и профилирования вызовов.

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

Код выведет:

Объяснение:

В функции changeName параметр person — это локальная копия указателя на структуру Person. Когда внутри функции person переназначается на новый адрес &Person{Name: "Alice"}, это изменение касается только локальной копии указателя, а не оригинального указателя, переданного в main.

Таким образом, в main переменная person продолжает указывать на исходный объект с именем "Bob".

Чтобы изменить имя оригинального объекта, нужно изменить поле через указатель, например:

Тогда вывод будет:

Задача для оптимизации обычно формулируется с указанием конкретной проблемы и цели, например:

"Уменьшить энергопотребление устройства при сохранении производительности"

"Сократить время отклика сенсора до 100 мс"

"Оптимизировать использование памяти для работы с ограниченными ресурсами"

Важно, чтобы задача была измеримой и имела критерии успеха. Например, "снизить время обработки данных на 20%" или "увеличить время работы от батареи на 30 минут".

Такое формулирование помогает сфокусироваться на конкретных метриках и оценить результат оптимизации.

Да, в нашей команде сейчас 6 Go-разработчиков, 2 фронтенд-разработчика, 2 QA-инженера и проектный менеджер (PM).

Такой состав позволяет эффективно распределять задачи: разработчики backend занимаются бизнес-логикой и сервисами, фронтенд — пользовательским интерфейсом, QA — тестированием и обеспечением качества, а PM — координацией работы и коммуникацией с заказчиком.

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

Объемы данных, попадающих в ClickHouse, зависят от конкретного проекта, но в типичных IoT-сценариях это могут быть миллионы записей в день. Источниками данных обычно являются сенсоры и устройства, передающие телеметрию, логи или метрики.

Например, если устройство отправляет данные каждую секунду, и таких устройств 1000, то в секунду поступает 1000 записей, что примерно 86,4 млн записей в сутки.

RPS (запросов в секунду) на запись может варьироваться от нескольких сотен до нескольких тысяч, часто данные пишутся батчами для оптимизации — например, по 1000 записей за один запрос. На чтение RPS зависит от нагрузки аналитических запросов, может быть от десятков до сотен запросов в секунду.

Пример батчевой записи в ClickHouse через HTTP API:

Где data.csv содержит множество строк с данными для вставки.

Transactional Outbox — это паттерн для обеспечения атомарности операций записи в базу данных и отправки сообщений в систему обмена сообщениями (например, очередь). Идея в том, что при изменении данных в БД вместе с этими изменениями в той же транзакции сохраняется специальная запись (outbox) с информацией о событии, которое нужно отправить.

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

Пример:

Затем отдельный воркер читает из outbox и отправляет сообщения.

Процесс code review в проекте обычно включает несколько этапов: разработчик создает pull request с изменениями, после чего коллеги просматривают код, проверяют его на соответствие стандартам, читаемость, корректность и потенциальные баги. Обсуждения и правки вносятся до тех пор, пока код не будет одобрен и слит в основную ветку. Такой процесс помогает повысить качество кода и обменяться знаниями внутри команды.

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

Чтобы выполнить одинаковый код после инициализации для всех бинов, реализующих определённый интерфейс, без дублирования аннотации @PostConstruct в каждом классе, можно использовать один из следующих подходов:

Создать базовый класс с методом, помеченным @PostConstruct

Сделайте абстрактный класс, реализующий интерфейс, и в нём реализуйте метод с @PostConstruct. Все ваши бины будут наследоваться от этого класса и унаследуют поведение.

public interface MyInterface {

void doSomething();

}

public abstract class BaseBean implements MyInterface {

@PostConstruct

public void init() {

// общий код инициализации

System.out.println("Общая инициализация");

}

}

@Component

public class MyBean extends BaseBean {

@Override

public void doSomething() {

// реализация

}

}

Использовать BeanPostProcessor

Создайте класс, реализующий BeanPostProcessor, в котором в методе postProcessAfterInitialization проверяйте, реализует ли бин нужный интерфейс, и выполняйте общий код.

@Component

public class MyInterfacePostProcessor implements BeanPostProcessor {

@Override

public Object postProcessAfterInitialization(Object bean, String beanName) {

if (bean instanceof MyInterface) {

// общий код после инициализации

System.out.println("Общая инициализация для " + beanName);

}

return bean;

}

}

Этот способ позволяет централизованно выполнять код для всех бинов с интерфейсом без изменения самих бинов.

Использовать аспектно-ориентированное программирование (AOP)

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

Выбор подхода зависит от конкретных требований и архитектуры проекта, но BeanPostProcessor — наиболее гибкий и не требует изменения классов бинов.

Для реализации highload RPC ручки /weather с нагрузкой 10k RPS и функцией aiWeatherForecast(), которая работает около 1 секунды, нужно обеспечить масштабируемость и асинхронность, чтобы не блокировать обработку запросов.

Основные подходы:

Асинхронная обработка запросов — использовать async/await или event loop, чтобы не блокировать поток во время ожидания результата.

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

Масштабирование — запускать несколько инстансов сервиса за балансировщиком нагрузки.

Очереди и пул воркеров — если aiWeatherForecast() тяжёлая, можно обрабатывать запросы через очередь задач.

Пример на Python с использованием asyncio и aiohttp (упрощённо):

Для 10k RPS потребуется запускать несколько инстансов сервера, использовать балансировщик нагрузки (например, Nginx), и, возможно, оптимизировать aiWeatherForecast() или кэшировать результаты, чтобы не запускать тяжёлую функцию на каждый запрос.

Также стоит рассмотреть использование более производительных языков или сервисов, если Python не справляется с нагрузкой.

Если Kafka упала, то отправка событий в неё невозможна, и сообщения не будут доставлены в брокер. В зависимости от реализации клиента и настроек, события могут:

Буферизоваться локально до восстановления соединения.

Быть потеряны, если нет механизма повторной отправки.

В embedded/IoT системах обычно реализуют локальный буфер или очередь, чтобы временно хранить события и отправлять их, когда Kafka снова доступна. Это помогает избежать потери данных при кратковременных сбоях сети или брокера.

В Java существует несколько видов сборщиков мусора (Garbage Collectors), каждый из которых оптимизирован под разные сценарии и требования к производительности:

Serial GC — однопоточный сборщик, подходит для небольших приложений с ограниченными ресурсами.

Parallel GC (Throughput Collector) — многопоточный сборщик, ориентирован на максимальную пропускную способность, подходит для серверных приложений.

CMS (Concurrent Mark-Sweep) — ориентирован на минимизацию пауз, выполняет большую часть работы параллельно с приложением.

G1 (Garbage First) — современный сборщик, который делит кучу на регионы и старается минимизировать паузы, хорошо подходит для больших куч.

ZGC (Z Garbage Collector) — низколатентный сборщик, работает параллельно и масштабируется на большие объемы памяти.

Shenandoah GC — похож на ZGC, ориентирован на минимальные паузы и параллельную работу.

Выбор сборщика зависит от требований к задержкам, пропускной способности и размера кучи. Например, для приложений с критичными задержками лучше использовать ZGC или Shenandoah, а для максимальной пропускной способности — Parallel GC.

Spring Boot Starter — это набор зависимостей (обычно в виде Maven/Gradle артефакта), который упрощает подключение функционала в проект. Стартеры собирают в себе нужные библиотеки и конфигурации для определённой задачи (например, spring-boot-starter-web).

Auto-configuration — это механизм автоматической настройки Spring контекста на основе наличия определённых классов, свойств и условий. Auto-configuration реализуется в виде классов с аннотацией @Configuration и условными аннотациями (@ConditionalOnClass, @ConditionalOnProperty и т.д.).

Разница:

Стартеры — это способ собрать зависимости и включить auto-configuration.

Auto-configuration — это код, который автоматически настраивает бины и параметры при старте приложения.

При написании кастомного стартерa вы обычно создаёте:

Maven/Gradle модуль с зависимостями.

Класс auto-configuration, который настраивает бины.

Файл spring.factories, который регистрирует auto-configuration.

Таким образом, стартер — это упаковка, а auto-configuration — логика настройки.

java.lang.Error — это класс в Java, который представляет серьёзные ошибки, возникающие в виртуальной машине (JVM), например, ошибки памяти (OutOfMemoryError) или ошибки виртуальной машины (VirtualMachineError). Такие ошибки обычно не обрабатываются приложением, так как они указывают на проблемы, которые невозможно или нецелесообразно исправлять программно.

Exception — это класс, представляющий исключения, которые могут быть обработаны в коде. Исключения делятся на проверяемые (checked) и непроверяемые (unchecked). Проверяемые исключения требуют обязательной обработки или объявления в сигнатуре метода.

Основное отличие:

Error сигнализирует о серьёзных проблемах JVM, которые обычно не обрабатываются.

Exception — это исключения, которые приложение может и должно обрабатывать для корректной работы.

Пример:

Размер батча выбирается исходя из нескольких факторов:

Ограничения памяти устройства. В embedded-системах часто ограниченный объем оперативной памяти, поэтому размер батча должен помещаться в доступный буфер.

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

Характеристики источника данных. Частота и объем поступающих данных влияют на оптимальный размер.

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

Часто размер батча определяется экспериментально: начинают с небольшого значения, затем увеличивают, оценивая баланс между производительностью, задержкой и ресурсами. Например, если устройство собирает сенсорные данные, можно накапливать 10-50 измерений перед отправкой, чтобы снизить количество коммуникаций и сэкономить энергию.

В Spring внедрение зависимостей (Dependency Injection, DI) можно реализовать тремя основными способами:

Через конструктор — зависимости передаются через параметры конструктора класса. Это позволяет создавать неизменяемые объекты и облегчает тестирование.

Через сеттеры (setters) — зависимости устанавливаются через методы установки после создания объекта.

Через поля (field injection) — зависимости внедряются напрямую в поля класса с помощью аннотаций.

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

Явное объявление всех зависимостей

Иммутабельность объекта после создания

Легкость тестирования и поддержки кода

Пример внедрения через конструктор:

Если consumer lag вырос до 10 миллионов сообщений, это означает, что потребитель не успевает обрабатывать входящий поток данных. План действий:

Анализ причин:

Проверить, не упала ли производительность потребителя (CPU, память, I/O).

Оценить, не увеличился ли объем входящих сообщений внезапно.

Проверить ошибки или блокировки в коде потребителя.

Анализ причин:

Масштабирование:

Добавить больше экземпляров потребителей (партиционирование топика, если поддерживается).

Увеличить ресурсы для существующих потребителей.

Масштабирование:

Оптимизация обработки:

Улучшить алгоритмы обработки сообщений.

Использовать батчи для обработки нескольких сообщений за раз.

Внедрить асинхронную обработку, если возможно.

Мониторинг и алерты:

Настроить мониторинг lag и ресурсов.

Внедрить алерты для раннего предупреждения.

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

Проверить настройки брокера:

Убедиться, что retention policy и настройки хранения сообщений не мешают обработке.

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

Я инженер по встраиваемым системам с опытом работы более 5 лет. Работал в компаниях, занимающихся разработкой IoT-устройств и промышленной автоматизации. Мои обязанности включали разработку и отладку прошивок для микроконтроллеров, интеграцию сенсоров и периферии, а также оптимизацию энергопотребления устройств.

В одном из проектов я занимался созданием системы мониторинга температуры и влажности с передачей данных по беспроводной сети. Использовал языки C и C++, работал с RTOS и протоколами связи, такими как MQTT.

Также имел опыт взаимодействия с аппаратной частью, включая схемотехнику и отладку на уровне железа.

Dependency Injection (внедрение зависимостей) и Dependency Inversion (инверсия зависимостей) — это связанные, но разные концепции в области проектирования ПО.

Dependency Inversion Principle (DIP) — один из пяти принципов SOLID. Он говорит, что:

Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.

Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Dependency Injection (DI) — это паттерн (способ) реализации инверсии зависимостей. DI означает, что зависимости объекта передаются ему извне (например, через конструктор, сеттер или интерфейс), а не создаются внутри объекта.

Пример DI на C++:

Итого: DIP — это принцип проектирования, а DI — способ его реализации.

В одном из проектов для IoT-устройства возникла задача параллельного сбора данных с нескольких сенсоров и одновременной обработки этих данных без блокировки основного потока.

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

Решение:

Использовал многопоточность с помощью RTOS или потоков ОС.

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

Для обмена данными между потоками использовал очереди сообщений (message queues), что позволило буферизовать данные и обрабатывать их асинхронно.

Такой подход обеспечил стабильный сбор и обработку данных без взаимных блокировок и потерь.

Reverse proxy — это сервер, который принимает запросы от клиентов и перенаправляет их на один или несколько внутренних серверов. Клиент взаимодействует только с reverse proxy, не зная о реальных серверах, которые обрабатывают запросы. Это позволяет балансировать нагрузку, кэшировать ответы, обеспечивать безопасность и скрывать внутреннюю структуру сети.

Реализовать reverse proxy можно с помощью:

Nginx — популярный веб-сервер с возможностями обратного проксирования.

Apache HTTP Server с модулем mod_proxy.

HAProxy — специализированный балансировщик нагрузки.

Traefik — современный прокси с поддержкой микросервисов.

Пример конфигурации Nginx для reverse proxy:

TTL (Time To Live) и LRU (Least Recently Used) — это разные стратегии управления кэшем.

TTL-кэш хранит данные с ограничением по времени жизни. Каждый элемент в кэше имеет метку времени, и после истечения заданного времени (TTL) элемент считается устаревшим и удаляется или обновляется.

LRU-кэш удаляет из кэша самый давно неиспользуемый элемент, когда достигается максимальный размер кэша. Это стратегия, ориентированная на частоту и недавность использования данных.

Пример применения:

TTL подходит, когда данные устаревают со временем (например, кэширование ответов API, которые обновляются каждые 5 минут).

LRU подходит, когда важно сохранять наиболее часто или недавно используемые данные, а ограничение — по объёму памяти.

Таким образом, TTL управляет временем жизни данных, а LRU — порядком удаления на основе использования.

В данном классе Person реализованы интерфейсы Serializable и Externalizable, что влияет на сериализацию объекта.

Особенности:

Используется @XmlAccessorType(XmlAccessType.PROPERTY), значит JAXB будет работать через геттеры/сеттеры.

Класс реализует Externalizable, поэтому методы writeExternal и readExternal полностью контролируют процесс сериализации.

В writeExternal записываются поля name, surname, phone, address.

В readExternal читаются поля в том же порядке.

Поля name и surname приватные с геттерами, address и phone — публичные.

hashCode всегда возвращает 1 — это плохая практика, может привести к проблемам в коллекциях.

equals сравнивает только name и surname.

Пример использования сериализации с Externalizable:

Важно, что при Externalizable порядок записи и чтения полей должен строго совпадать. Также стоит улучшить hashCode для корректной работы в хэш-структурах.

В моём последнем проекте я работал над разработкой системы мониторинга для умных устройств на базе микроконтроллеров. Мои обязанности включали написание прошивки на C для сбора данных с датчиков, настройку коммуникации по протоколу MQTT и интеграцию с облачной платформой. Стек технологий: C, FreeRTOS, MQTT, облачная платформа AWS IoT. Я занимался разработкой драйверов для сенсоров, реализацией логики обработки данных и обеспечением стабильной связи с сервером.

Постраничная выгрузка данных (пагинация) реализуется для ограничения объёма данных, передаваемых за один запрос, что важно для устройств с ограниченными ресурсами.

Обычно используется один из подходов:

Смещение и лимит (offset & limit):

Клиент передаёт номер страницы и размер страницы.

Сервер возвращает данные с учётом смещения: offset = (page - 1) * page_size.

Курсоры (cursor-based pagination):

Вместо номера страницы используется уникальный идентификатор последнего полученного элемента.

Позволяет более эффективно работать с динамическими данными.

Пример реализации на embedded-устройстве:

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

Команда EXPLAIN в PostgreSQL показывает план выполнения SQL-запроса, то есть как СУБД собирается его выполнить, без фактического запуска запроса. Она выводит информацию о выбранных индексах, последовательности сканирования таблиц, соединениях и т.д.

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

Пример:

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

Пример на псевдокоде:

Такой подход прост и надёжен, особенно если нет необходимости в сложных системах событий или очередях. В более сложных системах можно использовать триггеры базы данных или механизмы уведомлений, но для embedded/IoT часто достаточно простого поллинга.

Если сервис начал отвечать за 3 секунды вместо 100 миллисекунд, я бы последовательно выполнил следующие шаги:

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

Проверка инфраструктуры: Убедился, что серверы, сеть и базы данных работают корректно, нет ли проблем с ресурсами (CPU, память, диск, сеть).

Профилирование кода: Запустил профилирование сервиса, чтобы выявить узкие места — возможно, появились блокирующие операции или неэффективные запросы.

Анализ изменений: Проверил последние изменения в коде, конфигурациях или окружении, которые могли повлиять на производительность.

Тестирование: Попробовал воспроизвести проблему в тестовом окружении для более глубокого анализа.

Оптимизация: В зависимости от выявленных причин — оптимизировал код, исправил запросы к базе, добавил кэширование или масштабировал ресурсы.

Мониторинг: После исправлений настроил более детальный мониторинг, чтобы быстро реагировать на подобные ситуации в будущем.

Данный SQL-запрос выбирает имена из таблицы PERSON и соответствующие номера телефонов из таблицы PHONE, используя левое соединение (LEFT JOIN). Это означает, что будут показаны все люди из PERSON, даже если у них нет номера телефона (в этом случае поле NUMBER будет NULL).

Пример результата:

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

При возникновении ошибки OutOfMemoryError встраиваемых систем или IoT-устройств необходимо:

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

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

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

При необходимости увеличить доступный объем памяти, если это возможно аппаратно.

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

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

В PostgreSQL доступны различные типы индексов, которые позволяют оптимизировать поиск и сортировку данных:

B-tree (по умолчанию) — подходит для большинства операций сравнения (=, <, <=, >, >=).

Hash — эффективен для операций равенства, но менее универсален.

GIN (Generalized Inverted Index) — используется для индексации массивов, JSON, полнотекстового поиска.

GiST (Generalized Search Tree) — поддерживает сложные структуры данных, например, геометрические объекты.

SP-GiST — специализированный GiST для определённых типов данных.

BRIN (Block Range Index) — эффективен для очень больших таблиц с упорядоченными данными.

Пример создания B-tree индекса:

Выбор типа индекса зависит от типа данных и характера запросов.

В Go объект попадает в кучу (heap), если его время жизни выходит за пределы функции, в которой он создан, и он должен быть доступен после выхода из этой функции. Компилятор Go анализирует, где используется объект, и если он видит, что объект нужен вне текущего стека вызовов, выделяет его в куче.

Например, если функция возвращает указатель на локальную переменную, эта переменная будет размещена в куче, чтобы не потерять данные после выхода из функции:

Если же объект используется только внутри функции и не выходит за её пределы, он будет размещён в стеке для более быстрой работы.

Да, речь идёт о списании с лицевого счёта абонента.

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

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

Ошибка "connections not available" в HikariCP при пуле из 10 коннектов означает, что все доступные соединения заняты и новые запросы не могут получить соединение из пула.

Что делать:

Увеличить размер пула (например, увеличить maxPoolSize), если нагрузка действительно требует больше соединений.

Проверить, что соединения корректно закрываются (вызывается метод close() на Connection), иначе они остаются занятыми.

Проанализировать время жизни и время ожидания соединений (connectionTimeout), возможно стоит увеличить таймаут.

Оптимизировать запросы и транзакции, чтобы они занимали соединения как можно меньше времени.

Включить логирование HikariCP для выявления утечек соединений (leakDetectionThreshold).

Пример настройки увеличения пула:

Важно убедиться, что база данных и инфраструктура могут выдержать увеличенное количество соединений.

Connection pool в пакете database/sql — это механизм управления набором открытых соединений с базой данных, которые переиспользуются для выполнения запросов. Вместо открытия и закрытия соединения при каждом запросе, пул поддерживает несколько активных соединений, что значительно повышает производительность и снижает нагрузку на базу.

Как это работает:

При первом запросе создаётся новое соединение и помещается в пул.

Следующие запросы берут свободное соединение из пула.

Если все соединения заняты, запрос ждёт освобождения или создаётся новое соединение, если не достигнут лимит.

После использования соединение возвращается в пул для повторного использования.

Пример настройки пула в Go:

Идемпотентность и Exactly-once delivery — это разные концепции, связанные с надежностью и повторяемостью операций.

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

Пример: HTTP метод PUT для обновления ресурса — если отправить один и тот же запрос несколько раз, состояние ресурса останется одинаковым.

Exactly-once delivery — гарантия доставки сообщения или выполнения операции ровно один раз, без потерь и дубликатов.

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

Отличия:

Идемпотентность — свойство операции, позволяющее безопасно повторять её без изменения результата.

Exactly-once delivery — гарантия системы доставки, что операция будет выполнена ровно один раз.

В системах с ненадежной доставкой часто комбинируют эти подходы: делают операции идемпотентными, чтобы при повторной доставке (at-least-once) не было нежелательных эффектов, или реализуют сложные механизмы для exactly-once.

В embedded/IoT системах, где связь может быть ненадежной, идемпотентность упрощает обработку повторных сообщений, а exactly-once delivery требует дополнительных протоколов и подтверждений.

Оптимизация PostgreSQL начинается с анализа медленного SELECT-запроса:

Использовать EXPLAIN ANALYZE для понимания плана выполнения и выявления узких мест.

Проверить наличие и корректность индексов по полям, участвующим в фильтрах и соединениях.

Избегать SELECT *, выбирать только нужные колонки.

Переписать запрос, если он слишком сложный, разбить на несколько или использовать CTE.

Проверить статистику таблиц и при необходимости выполнить ANALYZE.

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

Пример:

Если запрос медленный, возможно, нет индекса по email. Тогда стоит создать индекс:

После этого запрос должен выполняться значительно быстрее.

Жёсткие (hard) и мягкие (soft) ссылки — это разные типы ссылок на объекты в файловых системах и встраиваемых системах.

Жёсткая ссылка (hard link):

Это дополнительное имя для существующего файла.

Указывает напрямую на индексный дескриптор (inode) файла.

Несёт равные права с оригинальным именем файла.

Удаление одного из имён не удаляет сам файл, пока есть хотя бы одна жёсткая ссылка.

Нельзя создавать жёсткие ссылки на каталоги (в большинстве систем) и на файлы на других файловых системах.

Мягкая ссылка (soft link или символическая ссылка):

Это отдельный файл, который содержит путь к другому файлу или каталогу.

Работает как ярлык или указатель.

Если оригинальный файл удалён, мягкая ссылка становится «битой» (dangling).

Можно создавать на каталоги и на файлы в других файловых системах.

В контексте Embedded / IoT систем мягкие ссылки могут использоваться для удобства навигации или обновления путей без изменения оригинальных файлов, а жёсткие ссылки — для экономии места и обеспечения целостности данных.

Пример в Linux:

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

Опишите конкретные пользовательские функции, которые вы реализовали, например, новые интерфейсы, улучшения UX, интеграции.

Оцените процент задач, выполненных на PHP, исходя из общего объема работы.

Укажите комфортное соотношение PHP/GO, например, 70/30 или 50/50, в зависимости от ваших предпочтений и опыта.

Объясните причины смены работы честно и конструктивно.

Опишите опыт коммерческой разработки на Go, проекты, задачи.

Расскажите о работе с REST, RPC, gRPC, API, приведите примеры.

Опишите опыт разработки и развертывания сервисов в Kubernetes, проекты.

Укажите опыт установки и настройки Unix-систем, уровень владения Linux.

Расскажите о проектах с использованием Terraform.

Опишите опыт работы с PostgreSQL, MongoDB.

Укажите желаемый уровень дохода.

Сколько лет в коммерческой разработке и с Go.

Опишите опыт разработки игр или мобильных приложений с геймификацией, если есть.

Кратко опишите архитектуру последнего Go-сервиса, ключевые компоненты, что бы улучшили.

Расскажите о долгом рефакторинге или миграции, вызовах и результатах.

Опишите использование AI-инструментов при кодировании и изменения в 2025 году.

Приведите пример, когда AI существенно помог.

Опишите опыт с WebSocket или real-time решениями, масштабирование.

Расскажите об опыте с очередями (BullMQ, Kafka, RabbitMQ), задачи.

Приведите пример, когда тесты поймали баг или их отсутствие привело к проблеме.

Опишите самый сложный баг на Go, процесс отладки.

Укажите максимальный RPS или MAU продукта, p99 latency.

Прикрепите пример кода или ссылку на репозиторий.

Прикрепите резюме, если не откликались через HH.

Свободное поле для комментариев.

Для каждого пункта лучше подготовить конкретные примеры из вашего опыта.

Чтобы предотвратить овербукинг (бронирование одного и того же последнего места несколькими пользователями одновременно) в PostgreSQL, можно использовать несколько подходов:

Транзакции с уровнем изоляции SERIALIZABLE — гарантируют, что транзакции выполняются последовательно, предотвращая конфликтные изменения.

Блокировки строк (SELECT ... FOR UPDATE) — при попытке забронировать место, сначала блокируем соответствующую запись, чтобы другие транзакции ждали завершения текущей.

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

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

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

Также важно обрабатывать ошибки и повторять попытки бронирования при конфликте транзакций.

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

Например, в языке Go лямбда-выражение выглядит так:

Её можно сразу вызвать или присвоить переменной для дальнейшего использования.

В Java исключения делятся на Checked (проверяемые) и Unchecked (непроверяемые):

Checked исключения — это исключения, которые проверяются компилятором во время компиляции. Методы, которые могут их выбросить, должны объявлять это через throws, и вызывающий код обязан либо обработать эти исключения (try-catch), либо пробросить дальше. Пример: IOException, SQLException.

Unchecked исключения — это наследники RuntimeException и Error. Компилятор не требует их обработки или объявления. Обычно они связаны с программными ошибками, например, NullPointerException, IllegalArgumentException.

Как работать:

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

Unchecked исключения обычно сигнализируют о логических ошибках, которые лучше исправлять, а не ловить.

Пример:

Таким образом, Checked исключения требуют явного контроля, а Unchecked — сигнализируют о критических ошибках, которые лучше предотвращать.

А если вопрос прозвучит не так, как вы готовились?

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

Посмотреть ИзиСобес