СМИ о нас
Диагностируем проблемы
в микросервисной архитектуре
на Node.js с помощью
OpenTracing и Jaeger
#IT
Время чтения: 24 мин 1 сек
22 Февраля 2021
#IT
Поделиться статьей
Подпишитесь на рассылку
Статья программиста IT-компании Lad Сергея Кучина.
Всем привет! В современном мире крайне важна возможность масштабировать приложение по щелчку пальцев, ведь нагрузка на приложение может сильно отличаться в разное время. Наплыв клиентов, которые решили воспользоваться вашим сервисом, может принести как большую прибыль так и убытки. Разбиение приложения на отдельные сервисы решает проблемы с масштабированием, всегда можно добавить инстансов нагруженных сервисов. Это несомненно поможет справиться с нагрузкой и сервис не упадет от нахлынувших на него клиентов. Но микросервисы вместе с неоспоримой пользой, вносят и более сложную структуру приложения, а так же запутанность в их взаимосвязях. Что если даже успешно масштабировав свой сервис, проблемы продолжаются? Время ответа растет и ошибок становится все больше? Как понять, где именно проблема? Ведь каждый запрос к API может порождать за собой цепочку вызовов разных микросервисов, получение данных из нескольких БД и сторонних API. Может это проблема с сетью, или API вашего партнера не справляется с нагрузкой, а может это кеш виноват? В этой статье я постараюсь рассказать, как ответить на эти вопросы и быстро найти точку отказа. Добро пожаловать под кат.

Чтобы быстро определить точку отказа и решить проблему, необходимо собрать метрики прохождения каждого этапа запроса. Для решения этой задачи можно воспользоваться спецификацией OpenTracing. Этот инструмент описывает основные принципы и модели данных для работы с трассировками в распределенных системах, но не предоставляет реализации. В данной статье мы воспользуемся имплементацией для JavaScript, и будем писать примеры на TypeScript. Но для того, чтобы перейти к практике, необходимо разобраться с теорией.
Теория
Основными понятиями в спецификации OpenTracing являются Trace, Span, SpanContext, Carrier, Tracer.

Trace. Это временной интервал, в течение которого выполнялся один или несколько Span'ов, связанных между собой одним идентификатором traceId. Span'ы так же могут быть связаны между собой ссылками двух оcновных типов. ChildOf это обычная связь родитель — потомок. Она говорит о том, что для завершения родительского span'a требуется завершение дочернего. Связь FollowsFrom говорит лишь о том, что родительский span запустил другой span, но на завершение текущего он не влияет.

Span. Это основная и минимальная единица информации в спецификации OpenTracing. Span описывает интервал во времени, в котором происходила работа. Например, вызов функции, которая делает запрос в БД за данными, можно описать как span, сохранив в нем необходимую информацию. Span создается с помощью конкретной реализации OpenTracing, которая называется Tracer (об этом чуть позже). При создании интервала обязательным полем является имя (например название функции), также неявно в Span записывается timestamp создания интервала и идентификатор spanId. Каждый интервал содержит traceId, если span является дочерним, то в него записывается traceId родительского интервала, если родительского spana'а нет, генерируется новый. Когда функция завершила свою работы, у объекта span мы должны вызвать метод finish. Этот метод запишет в Span timestamp завершения работы, а так же отправит получившийся span в Трассировщик (если это предусмотренно конкретной реализацией). В span можно добавлять Теги или Логи, которые являются объектами типа key:value. Ключи, этих объектов, могут обладать семантическими свойствами, которые описаны в соглашении OpenTracing. Например, если во время выполнения функции возникла ошибка, то к Span'у, который описывает эту функцию, можно добавить тег error: true. Обработка таких тегов, описанных в спецификации, может быть реализована в трейсере, который вы используете. Отличием логов, добавленных к интервалу, является то, что вместе с логом добавляется и timestamp лога. То есть это конкретная временная точка между стартом span'a и его завершением. У обычного тега нет этой метки.

SpanContext. Это объект, описанный в спецификации OpenTracing, который содержит информацию, необходимую для связывания span'ов между собой при межсервисном взаимодействии. Контекст содержит идентификаторы traceId, spanId, а также любую информацию вида key:value, которую мы хотим передавать между микросервисами. В терминологии OpenTracing эта информация называется baggage. Если мы создаём новый интервал, но этот интервал дочерний по отношению к другому. Создав SpanContext, мы можем передать его новому span'у, указав его как родителя. За счет этого новый интервал получит ссылку на свой родительский span.

Carrier. Этот простой объект типа key:value содержит информацию, с помощью которой можно создать SpanContext. Carrier можно создать с помощью реализации tracer. В спецификации OpenTracing описаны два типа этого объекта. Первый — это FORMAT_TEXT_MAP, простой объект типа key:value. Полученный объект можно передавать вместе с запросом к другому сервису. Второй FORMAT_BINARY трансформирует контекст в бинарный вид. Это минимальный набор, который должен реализовать tracer. Есть и другие форматы преобразования контекста, например FORMAT_HTTP_HEADERS, который сериализует контекст в объект заголовков для передачи по http.

Tracer. Это конкретная имплементация спецификации OpenTracing, которая непосредственно предоставляет методы по созданию span'ов, генерации идентификаторов, создания контекстов и отправку завершенных интервалов на хранение в трассировщик (distributed tracing system) например Jaeger или Elastic APM. Tracer реализует два метода, с помощью которых мы можем преобразовывать объект контекста в carrier для передачи между сервисами. Это inject и extract
Extract принимает первым аргументом тип carrier'a, вторым сам carrier и возвращает объект контекста. Inject получает первым аргументом SpanContext, вторым тип желаемого объекта carrier и третьим пустой объект, в который будет добавлена вся необходимая информация из контекста, для дальнейшей передачи между сервисами. За счет работы этих функций мы можем связывать наши сервисы в единый трейс, состоящий из связанных между собой span'ов.
Используемые технологии
NATS
Это быстрый, легкий и производительный брокер, написанный на golang. Через этот брокер можно реализовать две основные схемы взаимодействия микросервисов это Publish-Subscribe для асинхронных операций и Request-Reply для синхронных. NATS работает по простому текстовому протоколу, что упрощает разработку, а также предоставляет много разных полезных функций, таких как балансировку нагрузки и мониторинг подключенных сервисов. В тестовом проекте я буду использовать NATS как транспорт между сервисами, но в конечном счете для сбора трассировок транспорт не будет играть особой роли, это будет видно из примера. Для запуска проекта на локальной машине потребуется запустить NATS, что можно сделать с помощью Docker.
docker run -d --name nats -p 4222:4222 -p 6222:6222 -p 8222:8222 nats
Jaeger
Это система хранения и анализа трассировок, созданная и выпущенная в opensource компанией Uber. Jaeger предоставляет удобный интерфейс для анализа трассировок, а так же возможность отображать трассировки в виде графа зависимостей как отдельного метода, так и системы в целом, что можно использовать для самодокументирования системы в целом. В качестве хранилища трассировок Jaeger может использовать Cassandra, Elasticsearch а также просто хранить трейсы в памяти, что удобно для тестов. При большом количестве трассировок можно использовать Kafka, как буфер между коллектором, в который прилетают span'ы, и хранилищем. Также в библиотеках, реализующих трейсер Jaeger, можно настроить сэмплирование трейсов. Поддерживается несколько видов сэмплирования:

Const. Эта стратегия подойдет, если нужно хранить каждый трейс (если передать значение 1) или не сохранять ни один (значение 0)

Probabilistic. Значение этой стратегии говорит о том, какой процент трейсов Jaeger будет сохранять. Трейсы выбираются случайным образом. Например, при значении 0.1 будет сохранен только 1 трейс из 10.

Rate Limiting. Эта стратегия позволяет сохранять определенное значение полученных трейсов за секунду.

Remote. Стратегия говорит о том, что решение о сохранении трейса будет приниматься на стороне бэкенда Jaeger'a. Что позволяет гибко настраивать сэмплирование, не меняя настроек трейсера в коде приложения.

Для запуска тестового примера локально, также потребуется запустить Jaeger, например, с помощью Docker
docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.8
Реализуем сбор трассировок
Напишем небольшой тестовый проект, что бы посмотреть как работает теория на практике. Приложение будет обрабатывать один пользовательский запрос по http. В ответ на get запрос по руту /devices/:regionId будет возвращаться json с данными, которые будут собраны из нескольких сервисов. Общая архитектура приложения такая.
Все три микросервиса подключены к NATS. Единственный endpoint приложения отвечает за получение массива подключенных устройств, доступных в регионе, с информацией о пользователе этого устройства. После того, как запрос за устройствами пришел в http шлюз, микросервис api через NATS вызывает метод получения данныx из микросервиса devices. Микросервис devices для получения данных делает запрос в БД (например, mongodb), за объектами подключенных устройств, затем по количеству полученных устройств делает параллельнные запросы в другую БД (например redis) за получением геолокации каждого устройства. Далее микросервис devices передаёт массив id устройств в микросервис users для получения подробной информации о владельцах устройств. После получения массива пользователей данные агрегируются и возвращаются в api. Это выдуманный тестовый пример, но он хорошо продемонстрирует полезность сбора трассировок из подобных распределенных систем.

Для начала опишем основные интерфейсы данных, с которыми будем работать
// Координаты устройства
export interface Location {
  lat: number;
  lng: number;
}
 
export interface Device {
  id: string;
  regionId: string;
  userId: string;
  connected: boolean;
}
 
export interface User {
  id: string;
  name: string;
  address: string;
}
 
// Результат работы приложения
export interface ConnectedDevice extends Device {
  user: User;
  connected: true;
  location: Location;
}
У микросервиса devices и users будет по одному методу

export const UsersMethods = {
  getByIds: 'users.getByIds',
};
 
export const DevicesMethods = {
  getByRegion: 'devices.getByRegion',
};
Теперь напишем класс транспорта, который будет реализовывать подключение к NATS, а также два метода publish и subscribe. С помощью этих методов можно отправить данные подписчику по определенной теме и подписаться на них соответственно.

import * as Nats from 'nats';
import * as uuid from 'uuid';
 
export class Transport {
  private _client: Nats.Client;
  public async connect() {
    return new Promise(resolve => {
      this._client = Nats.connect({
        url: process.env.NATS_URL || 'nats://localhost:4222',
        json: true,
      });
 
      this._client.on('error', error => {
        console.error(error);
        process.exit(1);
      });
 
      this._client.on('connect', () => {
        console.info('Connected to NATS');
        resolve();
      });
    });
  }
  public async disconnect() {
    this._client.close();
  }
  public async publish<Request = any, Response = any>(subject: string, data: Request): Promise<Response> {
    const replyId = uuid.v4();
    return new Promise(resolve => {
      this._client.publish(subject, data, replyId);
      const sid = this._client.subscribe(replyId, (response: Response) => {
        resolve(response);
        this._client.unsubscribe(sid);
      });
    });
  }
  public async subscribe<Request = any, Response = any>(subject: string, handler: (msg: Request) => Promise<Response>) {
    this._client.subscribe(subject, async (msg: Request, replyId: string) => {
      const result = await handler(msg);
      this._client.publish(replyId, result);
    });
  }
}
Для создания http api будем использовать express. В index файле api нам нужно создать экземпляр класса Transport, через него мы будем вызвать метод микросервиса devices.

(async () => {
  const transport = new Transport();
  const port = 5000;
 
  await transport.connect();
  const api = express();
 
  api.get('/devices/:regionId', async (request, response) => {
    const result = await transport.publish<GetByRegion, ConnectedDevice[]>(DevicesMethods.getByRegion, {
      regionId: request.params.regionId,
    });
 
    response.send(result);
 
    return result;
  });
  api.listen(port, () => {
    console.info(`Server started on port ${port}`);
  });
})();
Далее реализуем микросервисы devices и users. Приведу пример кода только для devices, микросервис users реализуется аналогично.

Для работы с БД реализуем два репозитория. Один отвечает за работу с вымышленной mongodb, другой с redis
export class DeviceRepository {
  private db = 'mongodb';
  private devices: Device[] = [...];
  public async getByRegion(regionId: string): Promise<Device[]> {
    return new Promise(resolve => {
      setTimeout(() => resolve(this.devices), 300);
    });
  }
}
export class LocationRepository {
  private db = 'redis';
  private locations = new Map<string, Location>([...]);
 
  public async getLocation(deviceId: string): Promise<Location> {
    return new Promise(resolve => {
      setTimeout(() => resolve(this.locations.get(deviceId)), 40);
    });
  }
}
Теперь реализуем бизнес — логику микросервиса devices, которая будет находиться в функции getByRegion. Логика работы этого обработчика была описана ранее.

export async function getByRegion(request: Msg) { try { const deviceRepository = new DeviceRepository(); const locationRepository = new LocationRepository(); const regionId = request.regionId; const devices = await deviceRepository.getByRegion(regionId); const connectedDevices = await Promise.all( devices.map(async device => { const location = await locationRepository.getLocation(device.id); return { ...device, location }; }), ); const users: User[] = await transport.publish(UsersMethods.getByIds, { ids: devices.map(device => device.id), }); return connectedDevices.map(device => { const user = users.find(user => user.id === device.userId); return { ...device, user, }; }); } catch (error) { console.error(error); (this as any).createError(error); } }
В index файле devices нам осталось создать экземпляр класса Transport и подписаться на тему.

export const transport = new Transport();
 
(async () => {
  try {
    await transport.connect();
 
    transport.subscribe(DevicesMethods.getByRegion, getByRegion);
  } catch (error) {
    console.error(error);
    process.exit(1);
  }
})();
Теперь, запустив пример и сделав запрос на наш единственный endpoint, можно ощутить значительную задержку, прежде чем json с ответом появится на экране. Для того, чтобы разобраться в чем причина, начнем собирать трейсы между сервисами, а для этого нам потребуется класс Tracer. В качестве реализации OpenTracing будем использовать библиотеку jaeger-client.
import { JaegerTracer, initTracer } from 'jaeger-client';
 
export class Tracer {
  private _client: JaegerTracer;
  constructor(private serviceName: string) {
    this._client = initTracer(
      {
        serviceName,
        reporter: {
          agentHost: process.env.JAEGER_AGENT_HOST || 'localhost',
          agentPort: parseInt(process.env.JAEGER_AGENT_PORT || '6832'),
        },
        sampler: {
          type: 'const',
          param: 1,
        },
      },
      {},
    );
  }
  get client() {
    return this._client;
  }
}
Здесь мы создаём объект трассировщика с настройками адреса коллектора Jaeger, куда он будет отсылать завершенные интервалы. Для тестов будем использовать тип сэмплирования const. Чтобы не менять наш код транспорта и не добавлять логику по созданию span'ов в методах publish и subscribe, напишем два декоратора для этих методов. Единственное, что потребуется поменять в классе Transport, это добавить конструктор, который будет принимать необязательный параметр в виде объекта Tracer.

  constructor(private tracer?: Tracer) {}
Сами декораторы рассмотрим подробнее. Декоратор subscribePerfomance для метода subscribe

export function subscribePerfomance(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const origin = descriptor.value;
  descriptor.value = async function() {
    if (this.tracer) {
      const { client } = this.tracer as Tracer;
      const subject: string = arguments[0];
      const handler: Handler = arguments[1];
      const wrapperHandler = async (msg: Msg) => {
        const childOf = client.extract(FORMAT_TEXT_MAP, msg[CARRIER]); // 1
        if (childOf) {
          const span = client.startSpan(subject, { childOf }); // 2
          this[CONTEXT] = span; // 3
          try {
            const result = await handler.apply(this, [msg]); // 4
            span.finish(); // 5
            return result;
          } catch (error) {
            span.setTag(Tags.ERROR, true); // 6
            span.log({
              'error.kind': error, 
            });
            span.finish();
            throw error;
          }
        } else {
          return handler(msg);
        }
      };
      return origin.apply(this, [subject, wrapperHandler]);
    }
    return origin.apply(this, arguments);
  };
}
1. Через объект Tracer методом extract пытаемся извлечь из пришедшего в сообщении объекта carrier контекст. Если этого контекста нет, то мы просто возвращаем обратно результат вызова оригинального обработчика.
2. Создаём новый объект span и при создании указываем ему как родительский полученный SpanContext
3. Записываем полученный span в специальную переменную в this. Он нам понадобится, если из этого обработчика будут вызываться другие методы.
4. Выполняем оригинальный обработчик
5. Завершаем наш span.
6. В случае, если оригинальная функция вернула ошибку, записываем в span тег ошибки, а также саму ошибку и завершаем span

Декоратор publishPerfomance для метода publish
export function publishPerfomance(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const origin = descriptor.value;
let isNewSpan = false;
descriptor.value = async function() {
if (this.tracer) {
  const { client } = this.tracer as Tracer;
  const subject: string = arguments[0];
  let data: Msg = arguments[1];
  let context: Span | SpanContext | null = this[CONTEXT] || null; // 1
  if (!context) {
    context = client.startSpan(subject); // 2
    isNewSpan = true;
  }
 
  const carrier = {};
  client.inject(context, FORMAT_TEXT_MAP, carrier); // 3
  data[CARRIER] = carrier; // 4
  try {
    const result = await origin.apply(this, [subject, data]);
    if (isNewSpan) {
      (context as Span).finish();
    }
    return result;
  } catch (error) {
    if (isNewSpan) {
      const span = context as Span;
      span.setTag(Tags.ERROR, true);
      span.log({
        'error.kind': error,
      });
      span.finish();
    }
    throw error;
  }
}
return origin.apply(this, arguments);
};
}
7. Извлекаем из this контекст. Это нужно для того, чтобы получить родительский контекст в случае, если мы делаем publish из обработчика, который является так же и подписчиком. Как раз наш случай. Когда api делает запрос в devices, у микросервиса устройств уже есть родительский контекст, но он должен вызвать метод микросервиса users.
8. Если у нас нет родительского контекста, мы создаём новый span.
9. Создаём объект carrier для передачи его вместе с объектом запроса. Если context окажется пустым, трейсер создаст новый. Сгенерирует новый traceId.
10. Модифицируем запрос, добавляя в него созданный объект carrier.

Осталось только задекорировать методы в классе Transport, перезапустить сервис и сделать запрос. Далее можно открыть интерфейс Jaeger и найти трейс, соответствующий нашему запросу. Он должен выглядеть так.
Мы видим, что основную часть времени выполнения запроса занимает метод микросервиса устройств getByRegion. А точнее 97.22% от времени выполнения всего запроса. Значит, проблема в этом методе. Более того, на таймлайне видно, что перед тем, как вызвать метод микросервиса users, был большой интервал во времени, когда мы обращаемся к БД. Но в какой базе именно проблема? Это можно узнать только собрав span'ы c методов репозиториев. Для этого напишем декоратор.

export function repositoryPerfomance({ client }: Tracer) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = async function() {
      if (this.parent[CONTEXT]) { // 1
        const span = client.startSpan(propertyKey, { 
          childOf: this.parent[CONTEXT], // 2
        });
        span.setTag(Tags.DB_TYPE, this.db); // 3
        try {
          const result = await original.apply(this, arguments);
          span.finish();
          return result;
        } catch (error) {
          span.setTag(Tags.ERROR, true);
          span.log({
            'error.kind': error,
          });
          span.finish();
          throw error;
        }
      } else {
        return original.apply(this, arguments);
      }
    };
  };
}
1. Здесь мы проверяем, выполняется ли вызов метода в рамках другого span'a. Если родительского span'а нет, выполняем оригинальный метод и возвращаем результат.
2. Создаём новый дочерний span.
3. Присваиваем тег с названием базы, с которой работает репозиторий. В спецификации OpenTracing есть и другие полезные теги. Можно добавить тег с ip базы или текстом запроса.

Задекорировав методы репозиториев в Jaegere, трейс будет выглядеть следующим образом.
Теперь мы видим гораздо больше информации. И на графе и на таймлайне можно увидеть, в чем заключается проблема нашего приложения. 74.38% метода микросервиса devices занимает запрос к базе за списком устройств. Весь код, приведенный в качестве примера, можно посмотреть в репозитории на github.

В качестве транспорта мы использовали NATS, но из тестового приложения видно, что сам способ общения микросервисов не имеет особого значения для сбора трассировок. Модели данных и принципы работы с ними, описанные в спецификации OpenTracing, применимы и для других видов транспорта. Будь то запросы по http или асинхронные события через очереди. В больших распределенных приложениях крайне важно, при сбоях, быстро найти источник проблем. Ведь каждая минута простоя — это недовольный клиент, а значит, потеря денег. Быстро найти проблему, скорее всего, будет самой сложной задачей, которую можно решить быстрее и легче, обладая дополнительной информацией из собранных трассировок. Также, взглянув на граф трассировки, можно легко понять, какие микросервисы участвуют в работе метода. Если сравнить схему, которая была нарисована при проектировании и итоговый граф из Jaeger, можно увидеть, что между ними почти нет разницы.

Сбор и анализ трассировок из распределенного приложения похож на проведение МРТ с контрастом в медицине, с помощью которого можно не только решить текущие проблемы, но и выявить серьезное заболевание на ранней стадии.
Другие статьи из раздела СМИ о нас
07 Октября
СМИ о нас  
Почему обучение технического писателя — это нетривиальная задача
01 Октября
СМИ о нас  
IT-компания Lad помогает бизнесу и государству управлять HR-капиталом
28 Сентября
СМИ о нас  
ООО «Платформа строительных сервисов» — серебряный партнер конференции #ГИСОГД2021
27 Сентября
СМИ о нас  
Проект управления HR капиталом региона представлен на «ПРОФ-IT»
27 Сентября
СМИ о нас  
Список региональных IT-проектов – победителей конкурса «ПРОФ-IT.2021»
27 Сентября
СМИ о нас  
IX Всероссийский форум «ПРОФ-IT» — фотолента
СМИ о нас  
ТВ: На старт благотворительного забега “Открытые сердца” вышли более 200 человек
30 Августа
СМИ о нас  
Топ-17 крупнейших ИT-компаний Нижнего Новгорода
18 Августа
СМИ о нас  
Нижегородская область лидирует по доступности мер поддержки IT-компаний
05 Августа
СМИ о нас  
Прокачиваем анимацию с react-native-reanimated. Часть 1
02 Августа
СМИ о нас  
Чем занимается технический писатель: опыт IT-компании Lad
30 Июля
СМИ о нас  
Пишем свой dependency free WebSocket сервер на Node.js
27 Июля
СМИ о нас  
Сколько стоит рекомендация IT-специалиста в Нижнем. Аналитика
16 Июля
СМИ о нас  
IT-компания Lad вошла в ТОП-5 работодателей по найму Junior-разработчиков
06 Июля
СМИ о нас  
Технический писатель: с чего начать
28 Июня
СМИ о нас  
10 эко-приложений, которые помогают спасать природу
27 Июня
СМИ о нас  
IT-кампус на 7 000 студентов создадут в Нижнем Новгороде
23 Июня
СМИ о нас  
В Нижнем Новгороде пройдет паблик-ток о развитии IT-сферы
23 Июня
СМИ о нас  
В Арсенале обсудят IT в Нижегородской области
16 Июня
СМИ о нас  
Обучающая программа «Веб-разработка» от HiBrain стартовала при ННГУ
15 Июня
СМИ о нас  
Оплатить ЖКХ в режиме онлайн теперь можно на портале «Карты жителя Нижегородской области»
10 Июня
СМИ о нас  
«Прекрасная идея» или напрасные затраты? IT-бизнес — о проекте редевелопмента Започаинья
10 Июня
СМИ о нас  
Нижегородские IT-компании смогут побороться за победу во всероссийском конкурсе «IT-проект: Back To The Product»
03 Июня
СМИ о нас  
Скидки и сертификаты стали доступны на «Карте жителя Нижегородской области»
01 Июня
СМИ о нас  
Фестиваль детских инновационных проектов прошел в технопарке «Кванториум Нижний Новгород»
26 Мая
СМИ о нас  
Нижегородские школьники разработали маску дополненной реальности для сервиса «Карта жителя Нижегородской области»
20 Мая
СМИ о нас  
Как интегратор 1С балуется плюшками
17 Мая
СМИ о нас  
Историческая викторина в честь 800-летия Нижнего Новгорода стартовала на “Карте жителя”
12 Мая
СМИ о нас  
Жители региона смогут получать цифровые квитанции через сервис «Карта жителя Нижегородской области
12 Мая
СМИ о нас  
Квитанции об оплате услуг ЖКХ можно получить на портале «Карта жителя»
27 Апреля
СМИ о нас  
Джентльменский набор приложений для смартфона в 2021 году
23 Апреля
СМИ о нас  
IT-компания Lad вошла в число крупнейших работодателей рынка IT в Нижнем Новгороде
16 Апреля
СМИ о нас  
Какой кошелек выбрать?
05 Апреля
СМИ о нас  
Формы платформы
04 Апреля
СМИ о нас  
На 30% Минстрой России прогнозирует повышение индекса IQ городов к 2024 году
01 Апреля
СМИ о нас  
К 2024 году в России планируется на 30% повысить индекс цифровизации городов
22 Марта
СМИ о нас  
Эко-квест «800 шагов к чистому городу»
18 Марта
СМИ о нас  
Все школы в России обеспечат доступом к интернету в этом году
06 Марта
СМИ о нас  
Команды из Нижегородской области стали победителями межрегионального онлайн-хакатона
06 Марта
СМИ о нас  
Кейс «Instagram-маски в честь 800-летия Нижнего Новгорода» от IT-компании Lad
01 Марта
СМИ о нас  
Где работать в ИТ в 2021: Lad
28 Февраля
СМИ о нас  
Нижегородский онлайн-хакатон «VRARHack52» соберет юных разработчиков со всей страны
25 Декабря
СМИ о нас  
В российских школах заменяют Word и Excel на отечественные аналоги
24 Декабря
СМИ о нас  
Дмитрий Петров о налоговых льготах для IT-сферы
СМИ о нас  
Нижегородские IT-компании получат налоговые льготы
22 Декабря
СМИ о нас  
Профит для бизнеса от работы контактного центра на удаленке: кейс компании Lad
10 Декабря
СМИ о нас  
Росатом интегрирует решения российских разработчиков в проекты Smart City
07 Декабря
СМИ о нас  
Портал "Карта жителя Нижегородской области" стал удобнее
06 Декабря
СМИ о нас  
На портале «Карта жителя Нижегородской области» обновлен профиль пользователя
30 Ноября
СМИ о нас  
iCluster обсудит лучшие практики импортозамещения
27 Ноября
СМИ о нас  
Lad: цифровая трансформация бизнеса
19 Ноября
СМИ о нас  
IT-компания Lad поделится лучшими практиками
18 Ноября
СМИ о нас  
Компания Lad проведет день открытых дверей и расскажет о трендах IT-разработки
14 Ноября
СМИ о нас  
Участники регионального iCluster расскажут об отраслевых трендах
29 Октября
СМИ о нас  
О будущем проекта "Карта жителя Нижегородской области"
22 Октября
СМИ о нас  
Эксперт Lad дал комментарий Tadviser об импортозамещении офисного софта
14 Октября
СМИ о нас  
Как ИТ-специалисту устроиться на работу в хорошую компанию
07 Октября
СМИ о нас  
Карту жителя Нижегородской области начнут тестировать в октябре
06 Октября
СМИ о нас  
К сервису «Карта жителя Нижегородской области» присоединились первые банки-партнеры
01 Октября
СМИ о нас  
Пакет офисных решений "Р7-Офис" поступит в школы Ингушетии
01 Октября
СМИ о нас  
Ингушетия получит 3 тыс. лицензий на российское ПО для системы образования
25 Августа
СМИ о нас  
НБД-Банк и IT-компания Lad предлагают малому бизнесу онлайн-кассы в аренду
18 Июня
СМИ о нас  
Алтайский край переходит на российское ПО
06 Мая
СМИ о нас  
Work must go on!
03 Апреля
СМИ о нас  
IT-компании предлагают свои продукты бесплатно
26 Марта
СМИ о нас  
ПСС на Digital.Forum Construction 2020
24 Марта
СМИ о нас  
Быстрая цифровизация строительной отрасли
16 Марта
СМИ о нас  
Партнерская акция IT-компании Lad и НБД-Банка
13 Февраля
СМИ о нас  
Отечественное ПО для белгородских школьников
СМИ о нас  
Платформа HiBrain - разработка IT-компании Lad
СМИ о нас  
Digital Summit 2019
24 Октября
СМИ о нас  
В Нижнем Новгороде iCluster провёл IT-фестиваль "iFest 2019"
21 Октября
СМИ о нас  
Lad на фестивале программистов IFest
03 Октября
СМИ о нас  
Обрабатываем заказы из интернет магазина с помощью RabbitMQ и TypeScript
27 Сентября
СМИ о нас  
Lad примет участие в Digital Summit 2019
СМИ о нас  
Как IT-технологии изменили жизнь нижегородцев?
06 Августа
СМИ о нас  
В Нижегородской области запустят ПСС
07 Июня
СМИ о нас  
На ПМЭФ презентованы проекты IT-компании Lad
06 Июня
СМИ о нас  
На ПМЭФ презентованы ключевые нижегородские ИТ проекты
29 Мая
СМИ о нас  
В Нижнем Новгороде запустили цифровую платформу IT-образования HiBrain
24 Апреля
СМИ о нас  
Kari втрое ускорила оформление рассрочки
12 Февраля
СМИ о нас  
Цифровизация Retail-отрасли: актуальные тренды
23 Ноября
СМИ о нас  
Лучшие IT-проекты выбрали в Нижнем Новгороде
Поделитесь вашим мнением
Ответ успешно отправлен Мы свяжемся с вами в ближайшее время

Файлы cookies

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

подробнее хорошо