Русский
Русский
English
Статистика
Реклама

Backend

NEST.JS. Работа с ошибками. Мысли и рецепты

14.03.2021 10:05:42 | Автор: admin

Холивар...

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

  • Некоторая... академичность. Разобрано много и интересно, но заканчивается всё стандартным: "ваш выбор зависит от вашей ситуации".

  • Абсолютно отсутствуют упоминания о бюджете. Никто же не будет спорить, что теоретически мерседес лучше, чем восьмёрка по всем показателям кроме.. цены.

Задача этого поста - поделиться выработанным практическим рецептом. В конкретном фреймворке и с конкретными границами применимости. Без претензий на уникальность, универсальность и, тем более, академическую "правильность".


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

Стартовые условия.

Выделим основные: язык, фреймворк, тип приложения. Раскроем кратко каждый пункт:

ЯЗК

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

К примеру, в go не стоит вопрос, использовать ли исключения - там их нет. В функциональных языках, в частности в F#, было бы очень странно не использовать монады или discriminated union'ы (возврат одного из нескольких возможных типов значений), т. к. это там это реализовано очень удобным и естественным образом. В C#, монады тоже можно сделать, но получается намного больше букв. А это не всем нравится, мне например - не очень. Правда, последнее время всё чаще упоминается библиотека https://www.nuget.org/packages/OneOf/, которая фактически добавляет в язык discriminated union'ы.

А к чему нас подталкивает javascript/typescript?... К анархии! Можно много за что ругать JS и вполне по делу, но точно не за отсутствие гибкости.

Скорее уж за сверхгибкость )). В общем, мы вольны делать так, как нам хочется. Но тут есть, небольшая проблема - когда у вас в команде 10 человек и каждый делает как ему хочется.. получается не очень. Даже если каждый подход в отдельности - неплох.

ФРЕЙМВОРК

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

На самом деле, довольно спорная концепция. И это можно обойти, причём разными способами. Убеждённые сторонники монад вполне могут делать что-то такое:

@Controller()class SomeController {  @Post()  do (): Either<SomeResult, SomeError> {    ...  }}

Для этого, правда придётся написать кое-какой обвязочный код, но можно. Мы не стали.

Важно также, что Фреймворк делает практически всё для того, чтобы нам не приходилось заботиться об устойчивости процесса приложения . Nest сам выстраивает для нас "конвейер" обработки запроса и оборачивает всё это в удобный глобальный "try/catch", который ловит всё.

Правда иногда случаются казусы

Например в одной из старых версий nest'а мы столкнулись с тем, что ошибка, вылетевшая из функции переданной в декоратор @Transform() (из пакета class-transformer) почему-то клала приложение насмерть. В версии 7.5.5 это не воспроизводится, но от подобных вещей, конечно никто не застрахован.

ТИП ПРИЛОЖЕНИЯ

Самое важное. Мы не пишем софт для спутников. Там вряд ли можно было бы себе позволить что-то в стиле "сервис временно недоступен, попробуйте позже". Для нас же - это вполне ожидаемая ситуация. Нежелательная, конечно, но и не фатальная.

Мы пишем веб-сервисы. Есть http-сервисы, есть rpc (на redis и RabbitMQ, смотрим в сторону gRPC), гибридные тоже есть. В любом случае, мы стараемся внутреннюю логику приложения абстрагировать от транспорта, чтобы в любой момент можно было добавить новый.

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

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

  • Транзакционность. То есть, либо получилось всё, либо не получилось ничего.

  • Идемпотентность. Повторное выполнение одной и той же команды не ломает и не меняет состояние системы.

Транзакции (особенно распределённые) и идемпотентность выходят за рамки данной статьи. Но во многом, эти вещи являются основой надёжности.

Ближе к делу.

Наши принципы обработки ошибок базируются на следующих соглашениях:

КОНФИГУРАЦИЯ ПРИЛОЖЕНИЯ.

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

@Injectable()export class SomeModuleConfig {  public readonly someUrl: URL;public readonly someFile: string;public readonly someArrayOfNumbers: number[];  constructor (source: ConfigurationSource) {    // Бросит ConfigurationException если не удастся распарсить Url. Можно    // также проверять его доступность, например, при помощи пакета is-reachable    this.someUrl = source.getUrl('env.SOME_URL');// Бросит ConfigurationException если файл не существует или на него нет прав.this.someFile = source.getFile('env.SOME_FILE_PATH');// Бросит ConfigurationException если там не перечисленные через запятую числаthis.someArrayOfNumbers = source.getNumbers('env.NUMBERS')  }}

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

Подход к валидации

Мы написали свои валидаторы. Их преимущетсво в том, что мы не только валидируем данные, но в ряде случаев, можем сделать дополнительные проверки (доступность файла или удалённого ресурса, как в примере выше, например).

Однако, вполне можно использовать joi или json-схемы (может ещё есть варианты) - кому что больше нравится.

Неизменным должно быть одно - всё валидируется на старте.

УРОВНИ АБСТРАКЦИИ.

Мы максимально чётко разделяем бизнес-код и инфраструктурный код. И всё инфраструктурное выносим в библиотеки. Более менее очевидно, но всё же приведу пример:

// Задача: скачать файл по ссылке.const response = await axios.get(url, { responseType: 'stream' });const { contentType, filename } = this.parseHeaders(response);const file = createWriteStream(path);response.data.pipe(file);file.on('error', reject);file.on('finish', () => resolve({ contentType, filename, path }));

Такому коду не место не только в бизнес-логике, но вообще в приложении. В нём нет ничего уникального, привязывающего его к какому-то контексту. Ему место в библиотеке, скажем в классе NetworkFile. Вызывающий же код может выглядеть примерно так:

const file: NetworkFile = await NetworkFile.download('https://download.me/please', {  saveAs: 'path/to/directory'});

Фактически, мы заворачиваем в подобные переиспользуемые "смысловые" абстракции почти все нативные нодовские вызовы и вызовы сторонних библиотек. Стратегия обработки ошибок в этих обёртках: "поймать -> завернуть -> бросить". Пример простейшей реализации такого класса:

export class NetworkFile {private constructor (  public readonly filename: string,    public readonly path: string,    public readonly contentType: string,    public readonly url: string  ) {}    // В примере выше у нас метод download принимает вторым аргументов объект опций  // Таким образом мы можем кастомизировать наш класс: он может записывать файл на диск  // или не записывать, например.  // Но тут для примера - самая простая реализация.  public static async download (url: string, path: string): Promise<NetworkFile> {    return new Promise<NetworkFile>(async (resolve, reject) => {      try {      const response = await axios.get(url, { responseType: 'stream' });        const { contentType, filename } = this.parseHeaders(response);        const file = createWriteStream(path);        response.data.pipe(file);// Здесь мы отловим и завернём все ошибки связанную с записью данных в файл.        file.on('error', reject(new DownloadException(url, error));        file.on('finish', () => {        resolve(new NetworkFile(filename, path, contentType, url));        })    } catch (error) {        // А здесь, отловим и завернём ошибки связанные с открытием потока или скачиванием        // файла по сети.        reject(new DownloadException(url, error))      }    });  }private static parseHeaders (    response: AxiosResponse  ): { contentType: string, filename: string } {    const contentType = response.headers['content-type'];    const contentDisposition = response.headers['content-disposition'];    const filename = contentDisposition// parse - сторонний пакет content-disposition      ? parse(contentDisposition)?.parameters?.filename as string      : null;    if (typeof filename !== 'string') {      // Создавать здесь специальный тип ошибки нет смысла, т. к. на уровень выше      // она завернётся в DownloadException.      throw new Error(`Couldn't parse filename from header: ${contentDisposition}`);    }    return { contentType, filename };  }}
Promise constructor anti-pattern

Считается не круто использовать new Promise() вообще, и async-коллбэк внутри в частности. Вот и вот - релевантные посты на stackoverflow по этому поводу.

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

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

В БИЗНЕС-КОДЕ НИГДЕ НЕ НАДО ПИСАТЬ TRY / CATCH.

Серьёзно, о таких вещах, как закрытие дескрипторов и коннектов не должна заботиться бизнес-логика! Если вам прям очень надо написать try / catch в коде приложения, подумайте.. либо вы пишете то, что должно быть вынесено в библиотеку. Либо.. вам придётся объяснить товарищам по команде, почему именно здесь необходимо нарушить правило (хоть и редко, но такое всё же бывает).

Так почему не надо в сервисе ничего ловить? Для начала:

ЧТО М СЧИТАЕМ ИСКЛЮЧИТЕЛЬНОЙ СИТУАЦИЕЙ?

Откровенно говоря, в этом месте мы сломали немало копий. В конце концов, копья кончились, и мы пришли к концепции холивар-agnostic. Зачем нам отвечать на этот провокационный вопрос? В нём очень легко утонуть, причём мы будем не первыми утопленниками )

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

Не смогли считать файл - до свиданья. Не смогли распарсить ответ от стороннего API - до свидания. В базе duplicate key - до свидания. Не можем найти указанную сущность - до свидания. Максимально просто. И механизм throw, даёт нам удобную возможность осуществить этот быстрый выход без написания дополнительного кода.

В основном исключения ругают за две вещи:

  • Плохой перформанс. Нас это не очень волнует, т. к. мы не highload. Если он нас всё же в какой-то момент настигнет, мы, пересмотрим подходы там, где это будет реально критично. Сделаем бенчмарки... Хотя, готов поспорить, оверхед на исключения будет не главной нашей проблемой.

  • Запутывание потока управления программы. Это как оператор goto который уже давно не применяется в высокоуровневых программах. Вот только в нашем случае, goto бывает только в одно место - к выходу. А ранний return из функции - отнють не считается анти-паттерном. Напротив - это очень широко используемый способ уменьшить вложенность кода.

ВИД ОШИБОК

Говорят, что надо обрабатывать исключения там, где мы знаем, что с ними делать. В нашем слое бизнес-логики ответ будет всегда один и тот же: откатить транзакцию, если она есть (автоматически), залогировать всё что можно залогировать и вернуть клиенту ошибку. Вопрос в том, какую?

Мы используем 5 типов рантайм-исключений (про конфигурационные уже говорил выше):

abstract class AuthenticationException extends Exception {  public readonly type = 'authentication';}abstract class NotAllowedException extends Exception {public readonly type = 'authorization';}abstract class NotFoundException extends Exception {  public readonly type = 'not_found';}abstract class ClientException extends Exception {  public readonly type = 'client';}abstract class ServerException extends Exception {  public readonly type = 'server';}

Эти классы семантически соответствуют HTTP-кодам 401, 403, 404, 400 и 500. Конечно, это не вся палитра из спецификации, но нам хватает. Благодаря соглашению, что всё, что вылетает из любого места приложения должно быть унаследовано от указанных типов, их легко автоматически замапить на HTTP ответы.

А если не HTTP? Тут надо смотреть конкретный транспорт. К примеру один из используемых у нас вариантов подразумевает получения сообщения из очереди RabbitMQ и отправку ответного сообщения в конце. Для сериализации ответа мы используем.. что-то типа either:

interface Result<T> {data?: T;  error?: Exception}

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

Базовый класс Exception выглядит примерно так:

export abstract class Exception {  abstract type: string;  constructor (    public readonly code: number,    public readonly message: string,    public readonly inner?: any  ) {}toString (): string {    // Здесь логика сериализации, работа со стек-трейсами, вложенными ошибками и проч...  }}
Коды ошибок

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

  • Бывает такое, что клиентское приложение должно предпринять различные действия в зависимости от пришедшей от сервера ошибки. С кодами мы можем решить это не добавляя новых http статусов и без, прости Господи, парсинга сообщений.

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

Насколько это всё нужно и полезно - жизнь покажет

Поле inner - это внутренняя ошибка, которая может быть "завёрнута" в исключение (см. пример с NetworkFile).

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

ПРИМЕР ИСПОЛЬЗОВАНИЯ

Опустим AuthenticationException - он используется у нас только в модуле контроля доступа. Разберём более типовые примеры и начнём ошибок валидации:

import { ValidatorError } from 'class-validator';// ....export interface RequestValidationError {  // Массив - потому что ошибка может относиться к нескольким полям.  properties: string[];  errors: { [key: string]: string };nested?: RequestValidationError[]}// Небольшая трансформация стандартной ошибки class-validator'а в более удобный// "наш" формат.const mapError = (error: ValidationError): RequestValidationError => ({  properties: [error.property],  errors: error.constraints,  nested: error.children.map(mapError)});// Сами цифры не имеют значения.export const VALIDATION_ERROR_CODE = 4001;export class ValidationException extends ClientException {  constructor (errors: ValidationError[]) {    const projections: ValErrorProjection[] = ;    super(      VALIDATION_ERROR_CODE,      'Validation failed!',      errors.map(mapError)    );  }}

Иногда валидация производится в пайпе, иногда где-то в обработчике, но мы всегда хотим, чтобы на выходе был единообразный, удобный для нас формат. Заставить стандартный нестовый ValidationPipe выбрасывать то, что нам хочется можно просто передав функцию-фабрику в конструктор:

app.useGlobalPipes(new ValidationPipe({  exceptionFactory: errors => new ValidationException(errors);   });)

Соответственно, на выходе наш ValidationException замапится на BadRequestException с кодом 400 - потому что он ClientException.

Другой пример, с NotFoundException:

export const EMPLOYEE_NOT_FOUND_ERROR_CODE = 50712;export class EmployeeNotFoundException extends NotFoundException {  constructor (employeeId: number) {  super(      EMPLOYEE_NOT_FOUND_ERROR_CODE,      `Employee id = ${employeeId} not found!`    );  }}

Не очень приятно писать такие классы - всегда возникает лёгкое чувство... бойлерплейта ) Но зато, как приятно их потом использовать!

// Вместо, что не даст нам ни кодов, ни типа - ничего:throw new Error('...тут мы должны сформировать внятное сообщение...')// Простоthrow new EmployeeNotFoundException(id);

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

export const EMPLOYEE_NOT_ALLOWED_ERROR_CODE = 40565;export class EmployeeNotAllowedException extends NotAllowedException {  constructor (userId: number, employeeId: number) {  super(      EMPLOYEE_NOT_ALLOWED_ERROR_CODE,      `User id = ${userId} is not allowed to query employee id = ${employeeId}!`    );  }}

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

МАППИНГ

Мапятся внутренние ошибки на транспорто-специфичные в едином GlobalExceptionFilter, фильтр этот на вход получает один или несколько форматтеров. Задача форматтеров - преобразование вылетевшей ошибки в её конечный вид, можно сказать сериализация.

export interface IExceptionsFormatter {  // Verbose - флаг, который мы держим в конфигурации. Он используется для того,  // чтобы в девелоперской среде всегда на клиент отдавалась полная инфа о  // ошибке, а на проде - нет.  format (exception: unknown, verbose: boolean): unknown;    // При помощи этого метода можно понять, подходит ли данных форматтер  // для этого типа приложения или нет.  match (host: ArgumentsHost): boolean;}@Module({})export class ExceptionsModule {  public static forRoot (options: ExceptionsModuleOptions): DynamicModule {    return {      module: ExceptionsModule,      providers: [        ExceptionsModuleConfig,        {          provide: APP_FILTER,          useClass: GlobalExceptionsFilter        },        {          provide: 'FORMATTERS',          useValue: options.formatters        }      ]    };  }}const typesMap = new Map<string, number>().set('authentication', 401).set('authorization', 403).set('not_found', 404).set('client', 400).set('server', 500);@Catch()export class GlobalExceptionsFilter implements ExceptionFilter {  constructor (    @InjectLogger(GlobalExceptionsFilter) private readonly logger: ILogger,    @Inject('FORMATTERS') private readonly formatters: IExceptionsFormatter[],    private readonly config: ExceptionsModuleConfig  ) { }  catch (exception: Exception, argumentsHost: ArgumentsHost): Observable<any> {    this.logger.error(exception);    const formatter = this.formatters.find(x => x.match(argumentsHost));    const payload = formatter?.format(exception, this.config.verbose) || 'NO FORMATTER';    // В случае http мы ставим нужный статус-код и возвращаем ответ.if (argumentsHost.getType() === 'http') {      const request = argumentsHost.switchToHttp().getResponse();      const status = typesMap.get(exception.type) || 500;      request.status(status).send(payload);      return EMPTY;    }// В случае же RPC - бросаем дальше, транспорт разберётся.    return throwError(payload);  }}

Бывает конечно, что мы где-то напортачили и из сервиса вылетело что-то не унаследованное от Exception. На этот случай у нас есть ещё интерцептор, который все ошибки, не являющиеся экземплярами наследников Exception, заворачивает в new UnexpectedException(error) и прокидывает дальше. UnexpectedException естественно наследуется от ServerException. Для нас возникновение такой ошибки - иногда некритичный, но всё же баг, который фиксируется и исправляется.


В принципе, это всё. Для 95% наших задач этого вполне хватает. Способ может и не "канонический", но удобный и вполне рабочий - то, к чему мы и стремились.

И всё же бывают ситуации

КОГДА ВСЁ НЕ ТАК ЯСНО.

Приведу два примера:

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

В таких случаях всё-таки приходится проявлять "фантазию", например, обернуть в try/catch обработку каждой строки csv-файла. И в блоке catch писать ошибку в "отчёт". Тоже не бином Ньютона )

Второй. Я сознательно не написал выше реализацию DownloadException.

export class DOWNLOAD_ERROR_CODE = 5506;export class DownloadException extends ServerException {  constructor (url: string, inner: any) {    super(      DOWNLOAD_ERROR_CODE,      `Failed to download file from ${url}`,      inner    );  }}

Почему ServerException? Потому что, в общем случае, клиенту всё равно почему сервер не смог куда-то там достучаться. Для него это просто какая-то ошибка, в которой он не виноват.

Однако, теоретически может быть такая ситуация, что мы пытаемся скачать файл по ссылке, предоставленной клиентом. И тогда, в случае неудачи, клиент должен получить 400 или может быть 404, но не 500.

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

ЗАКЛЮЧЕНИЕ

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

P. S. К сожалению, опенсорс в нашей компании в процессе согласования, поэтому привести реальный используемый код я не могу. Когда будет такая возможность, мы выложим на github библиотеку, при помощи которой работаем с исключениями. А за одно и некоторые другие пакеты, которые могут оказаться кому-то полезными.

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

Подробнее..

Перевод Дорожная карта для разработчиков Node.js на 2021 год

20.03.2021 18:12:42 | Автор: admin

Node.js изменил правила игры с момента его выпуска и даже крупные компании, такие как Uber, Medium, PayPal и Walmart, перешли на Node.js. На этой платформе можно создавать действительно мощные приложения, отслеживание в реальном времени, движки видео- и текстового чата, приложения для социальных сетей и т. д. Владение Node.js становится одним из самых крутых навыков для разработчиков, и вот мой план развития, на основе собственного опыта и советов. Прежде чем углубляться в материал, убедитесь, что у вас есть четкая цель: то, что вы хотите разработать, иначе есть вероятность, что вы откажетесь от обучения. Цель поможет вам в первую очередь сосредоточиться на том, чтобы овладеть важнейшим навыками, а не выяснять, нужно ли вам их изучать или нет.


Предпосылки

1 JavaScript

Если вы уже фронтенд разработчик и хотите овладеть навыками разработки на бекенде, вам не нужно тратить огромное количество времени на изучение JavaScript. Но если вы совсем новичок, который хочет стать мастером в Node.js в минимально возможное время, то до глубокого погружения в Node.js вы должны изучить следующие понятия,.

  • Стрелочные функции

  • Типы

  • Выражения

  • Функции

  • Лексические структуры

  • this

  • Циклы и область видимости

  • Массивы

  • Шаблонные литералы

  • Строгий режим

  • ES6/ES7

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

  • Таймеры

  • Промисы

  • Замыкания

  • Event Loop

  • Асинхронное программирование и колбеки

2. NPM

Node Package Manager - это крупнейший в мире реестр программного обеспечения с более чем 800 000 пакетов. Правильное применение NPM сильно поможет вам, управлять пакетами с помощью NPM довольно удобно, когда мы разрабатываем приложения, с рядом зависимостей.

NPM состоит из трех компонентов:

  • Интерфейс командной строки (CLI): работает в терминале, через него с NPM работает большинство разработчиков.

  • Реестр: Большая публичная база данных программного обеспечения JavaScript и метаинформации о них.

  • Веб-сайт: открывайте для себя новые пакеты и управлять другими аспектами работы с npm.

NPM используется для управления несколькими версиями кода и его зависимостями, для запуска пакетов без их загрузки (с помощью npx) и многого другого.

3. Базовые знания по Node.js

Эмиттеры событий: Это объекты в Node.js, которые запускают события, отправляя сообщение, о завершении действия. Мы также можем написать код, который прослушивает события от эмиттера. Например, если вы работали с внешним интерфейсом, то, вероятно, знаете, сколько взаимодействий нужно обрабатывать в приложениях: щелчки мышью (и не только щелчки), нажатия кнопок клавиатуры и т.д.. Точно так же в серверной среде в Node.js мы можем построить аналогичную систему, используя модуль событий, с классом EventEmitter, для обработки наших событий.

Колбеки (обратные вызовы): Это функции, вызываемые при завершении задачи, что предотвращает любые блокировки, позволяя при этом выполняться остальной части кода. Поскольку в Node.js нам приходится работать с множеством асинхронных задач, чтобы приложения были быстрее, они нужны повсюду. Например,

Буферы: Класс Buffer предназначен для обработки сырых бинарных данных. Они соотносятся с некоторой необработанной памяти, выделенной вне V8. Буферы - это массив целых чисел, размер которых нельзя изменить, даже имея множество методов специально для двоичных данных. Например, целые числа в буфере представляют собой байт со значениями от 0 до 255 включительно; если вы пропишете в console.log() экземпляр Buffer, то получите цепочку значений в шестнадцатеричном формате.

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

Навыки разработки

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

Протоколы HTTP/HTTPS: Фундаментальные знания о том, как данные передаются с помощью протоколов передачи данных и понимание принципов работы HTTP и HTTPS - это мастхэв для каждого backend-разработчика. HTTPS использует протокол, известный как TLS, для шифрования соединения. В бэкэнд-среде есть чему поучиться, вы запутаетесь, если не знаете, как работает Интернет. Вот 4 метода запроса, основа всей коммуникации в сети:

  • GET: чтобы получить ресурс

  • POST: чтобы создать новые ресурсы

  • PUT: чтобы обновить ресурс

  • PATCH: чтобы изменить ресурс

  • DELETE: Используется для удаления ресурса, идентифицированного URL-адресом

  • OPTIONS: Запрашивает разрешённые варианты коммуникации с URL или сервером

Веб фреймворки

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

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

Meteor.js: Отличный фреймворк, поставляется со встроенными обработчиками MongoDB, поддерживает GraphQL. Когда вы создаете проект командой meteor create myapp и запускаете его, у веб-страницы уже есть серверная часть MongoDB. Можно работать с Meteor.js как с эффективной альтернативой которая ускорит и упростит разработку. Если приложение простое, я рекомендую придерживаться Express..

Sails.js: MVC платформа позволяет быстро создавать REST API, одностраничные приложения и приложения реального времени. Если вы хотите овладеть серьезными навыками, настоятельно рекомендуется использовать Sails.js, он дает множество преимуществ, таких как поддержка соединения в реальном времени с помощью WebSockets, сипользуется подход соглашение по конфигурации.

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

Nest.js: Вдохновленный Angular и написанный на TypeScript, Nest.js под капотом работает на Express.js, а значит совместим с большей частью промежуточного программного обеспечения на Express. С помощью Nest.js вы можете создать эффективное и масштабируемое приложение;он предоставляет отличную структуру для организации кода в отдельные модули.

Управление базами данных

Изучая Node.js, вы будете работать с большим количеством бэкэнд-материалов, и если вы новичок, вам следует с самого начала придерживаться MySQL и вообще SQL. Поскольку вы получите четкое и краткое разъяснение того, как мы разрабатываем бэкэнд-системы, выход за рамки SQL или MySQL от случая к случаю, когда будете работать с новыми типами проектов, есть вероятность, что вам понадобиться изучить другие материалы по бекенду.

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

SQL Server: Система управления реляционными базами данных, разработанная Microsoft, поддерживает ANSI SQL (стандарт языка SQL). Однако SQL поставляется со своими собственными реализациями.

MySQL: Серверное ПО с открытым исходным кодом, еще одна система управления базами данных. С MySQL, мы получаем гибкость выбора, поскольку мы можем изменять исходный код в соответствии с потребностями. MySQL - довольно простая альтернатива Oracle Database и Microsoft SQL server.

PostgreSQL: Еще одно ПО с открытым исходным кодом. Она работает во всех основных операционных системах, включая Linux, UNIX и Windows. PostgreSQL поддерживает большую часть стандарта SQL, предлагая при этом замечательные функции: внешние ключи, триггеры, транзакции, мультиверсионное управление параллелизмом (MVCC) и т. д.

MariaDB: Усовершенствованная версия MySQL, с мощными функциями, улучшениями безопасности и производительности, которых вы не найдете в MySQL. Есть несколько причин, по которым вы должны выбрать MariaDB вместо MySQL для крупномасштабных приложений. Например, в MariaDB пул соединений больше, а именно до 200 000+ соединений,. Короче говоря, MariaDB быстрее MySQL .

Облачные службы баз данных

Azure CosmosDB: Служба базы данных, которая распределена по всему миру, вы можете управлять своими данными удаленно, использование облачных баз данных даёт много преимуществ, этот инструмент немного упрощает масштабирование и управление большими приложениями. Кроме того, она поддерживает несколько моделей данных с использованием одного бэкэнда, это означает, что её можно использовать для моделей документа, ключа-значения, реляционных и графовых моделей. Поскольку она не полагается на какие-либо схемы, можно назвать её базой данных NoSQL, но она поддерживает язык запросов и транзакции ACID .

Amazon DynamoDB: Насколько мне известно, Amazon DynamoDB - отличная альтернатива, если у вас уже есть некоторый опыт работы с SQL, это полностью управляемая служба базы данных NoSQL, обеспечивающая производительность и масштабируемость. Вы можете создавать таблицы баз данных, которые могут хранить и извлекать любой объем данных и обслуживать любой уровень трафика запросов.

2. Базы данных NoSQL

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

Redis: Используя Redis, мы можем работать с базами данных, кешированием и брокерома сообщений. Она использует структуры данных, такие как строки, хэши, списки, наборы, растровые изображения, hyperloglog и гео-пространственные индексы для хранения данных в форме пар ключ-значение. Если вы не знаете, где мы используем Redis, вот пример.

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

Apache Cassandra

Высокопроизводительная распределенная база данных с прекрасным масштабированием, предназначенная для обработки большого количества данных на многих серверах без единой точки отказа. Она отличается от других систем управления реляционными базами данных. Распределённая архитектура основана на DynamoDB от Amazon и на модели данных BigTable от Google.

LiteDB

Очень легкая и быстрая база данных .NET NoSQL с бессерверным хранилищем документов. Вы можете использовать LiteDB в небольших десктопных приложениях и малых веб-приложениях, которые как хранилище используют всего одну базу данных.

3. Поисковые системы

Примечание: Если вы думаете, зачем нам поисковая система, то вот пример: мы используем Google в качестве поисковой системы, но это само по себе целое веб-приложение. Solr и ElasticSearch являются внутренними фреймворками, и если предоставляется какой-либо тип набора данных, он создает индекс поверх него, делая эти данные доступными для поиска на сервере. Вы можете поддерживать веб-сайт с миллионами пользователей с помощью Solr в качестве поисковой системы.

ElasticSearch

Система поиска и аналитики, построенная на Apache Lucene и разработанная на Java. Используя ElasticSearch, вы можете хранить и анализировать огромные объемы данных в режиме реального времени. Поскольку она выполняет поиск по индексу вместо поиска по тексту, ElasticSearch также обеспечивает высокую производительность поиска. По сути, она использует документы на основе структуры вместо таблиц и схем, которые поставляются с обширным REST API для хранения и поиска данных. Вы можете думать об ElasticSearch как о сервере, который обрабатывает JSON запросы и возвращает вам данные JSON.

Solr

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

Кеширование

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

Memory Cache

Этот метод также обычно называют кэшированием, поскольку в большинстве случаев кэширование связано с памятью на серверах. В этом методе часть памяти сервера используется в качестве кеша, где мы храним все данные, необходимые для уменьшения количества сетевых вызовов в наших приложениях. В Node.js у нас есть node-cache и memory-cache отличные библиотеки для обработки кеш-памяти на сервере Node.

Распределенный кеш

В этом методе кэширования мы объединяем память нескольких сетей в единое хранилище данных, которое позже используем в качестве окончательного кеша данных, чтобы обеспечить быстрый доступ к данным. Этот метод особенно широко используется при большом объеме данных и огромном количестве сетевых вызовов одновременно, что позволяет выполнять инкрементное расширение и масштабирование за счет добавления дополнительной серверной памяти в кластер. Redis - одна из самых известных вещей, когда дело доходит до распределенного кеширования, но вы можете выйти за её рамки, изучив также Memcached.

Движки шаблонов

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

  • Mustache.js

  • Handlebars

  • EJS

Коммуникация в реальном времени

Socket.io

Когда дело доходит до понимания взаимодействия в реальном времени в Socket.IO, если вы только начинаете работать в качестве бекенд разработчика, основная логика взаимодействия в реальном времени лежит между клиентом и сервером. Библиотека позволяет передавать двунаправленные данные между клиентом и сервером, вы можете думать о двунаправленном потоке данных как о синхронном потоке данных между двумя терминалами для достижения коммуникации в реальном времени, эти типы поведения включаются, когда клиент имеет Socket.IO в браузере вместе с сервером, интегрированным с пакетом Socket.IO. А данные можно отправлять в виде JSON запросов.

API

REST

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

В REST архитектура построена с использованием простых HTTP-вызовов для связи вместо сложных опций, таких как COBRA, COM + RPC. В REST вызовы представляют собой сообщения, основанные на стандартах HTTP для описания этих сообщений. В экосистеме Node.js вы можете выбрать node-rest-client и Axios, оба служат довольно хорошим сервисом для ускорения веб-приложений.

GraphQL

Отличная альтернатива REST. GraphQL использует API-интерфейсы, которые отдают предпочтение предоставлению клиентам именно тех данных, которые они запрашивают. Гибкая и удобная для разработчиков альтернатива, поскольку вы можете развернуть ее даже в среде IDE, известной как GraphiQL. Вы также получаете преимущества добавления или исключения полей без влияния на существующие запросы и построения API любым предпочтительным методом.

Тестирование

Фреймворки для юнит-тестирования

В модульном тестировании мы тестируем отдельные модули/компоненты изолированно, где модуль может быть наименьшей тестируемой частью кода в приложении. Ниже перечислены одни из лучших фреймворков для модульного тестирования в Node.js:

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

  • Mocha: Он обслуживает старые стандарты фреймворков модульного тестирования для приложений Node и поддерживает асинхронные операции, такие как колбеки, промисы с расширяемыми и настраиваемыми ассертами.

  • Chai: Его можно использовать вместе с Mocha и как библиотеку ассертов TDD/BDD для Node.js, которую можно использовать в сочетании с любой платформой тестирования на основе JavaScript.

Моки

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

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

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

Некоторые полезные библиотеки для Node.js

Подробнее..

Перевод Вы уверены, что вам нужен API?

16.04.2021 10:20:09 | Автор: admin


От переводчика: При разработке бэкэнда наличие API для фронт-энда стало практически повсеместным стандартом. Однако можем ли мы называть это "настоящим" API? Предлагаем вашему вниманию интересное пятничное чтение, которое, возможно, повлияет на API, которые мы все разрабатываем.


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


Ценность API в сокрытии информации


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


Почему это важно? Потому что чем объемнее API, тем дороже его поддерживать. Представьте себе крайний случай открытие каждой детали реализации. Каждое изменение в системе может сломать код, который используют ваши клиенты. Именно по этой причине мы стремимся разрабатывать компактные API. Я писал статью (перевод на хабре) по этой теме в контексте описания шаблона Регистрация событий.


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


API как продукт


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


Разделение клиента и сервера


Я готов спорить, что такие API как продукт редкость. Гораздо чаще существование развесистого API признак плохо выбранных границ вследствие нарушения принципа слабая связанность сильное сцепление.


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


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


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


Тяга к отрисовке на клиентской части


Ещё один двигатель страсть к реализации клиентской части с использованием современных JS фреймворков для разработки пользовательского интерфейса, таких как Angular, React или Vue.js. В противоположность отрисовке на сервере (SSR), эти фреймворки отрисовывают интерфейс на клиентской машине (CSR) в браузере и полагаются на сервисы (REST, GraphQL,...), предоставляемые сервером для получения данных и выполнения каких-то действий.


Самодостаточность и CSR не противоречат друг другу


Хотя я думаю, что SSR сейчас сильно недооценен, я могу это понять, так как требования к клиенту гораздо лучше реализуются современными JS фреймворками.


Но использование такого фреймворка не означает обязательное наличие API! Подумайте о поставке приложения как о самодостаточной, единой системе, которая включает в себя и клиент, и сервер! Уверен, у вас есть разделение сред выполнения, так как серверный код выполняется на сервере, а клиентский в браузере пользователя. И это разделение требует использования REST или GraphQL, чтобы синхронизировать среды выполнения.


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


Заключение


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


Если вы решите двигаться по пути создания единой системы, то вам решать, что использовать SSR или CSR. И, хотя зачастую понятия CSR и SPA (одностраничное приложение) часто слепливают вместе, вы можете использовать CSR и разрабатывать модульную клиентскую часть.


Ссылки


У меня появилась идея написания этой статьи, когда я слушал подкаст SoftwareArchitekTOUR Episode 82 (German) с Stefan Tilkov и Eberhard Wolff. Спасибо за вдохновение!


Хорошие источники для более глубокого погружения в самодостаточным системам и микро-фронтам:
Self-Contained Systems
Micro Frontend

Подробнее..

Конец вечного противостоянияsnake_keysVScamelKeys наводим порядок встилях написания переменных

31.05.2021 20:20:52 | Автор: admin

Привет,Хабр!Меня зовут Владимир, работаю в Ozon, занимаюсьфронтендом.

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

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

Бекенд отдает и принимает данные в виде:

{ user_name: "user1", main_title: "Title", } 

Фронтенд:

{ userName: "user1", mainTitle: "Title", } 

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

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

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

Шаг 1. Преобразование строки

Нам поможет встроенная функцияreplace. Онаумеетзаменятькаждое вхождение заданного регулярного выраженияс помощью функциимаппера, которую мы передаём вторым аргументом.

# Преобразованиеsnake_keysстроки вcamelKeys:

const snakeToCamel = str => {     return str.replace(/([_][a-z])/g, letter => {         return letter                 .toUpperCase()                 .replace('_', '')     }) } 

# ПреобразованиеcamelKeysстроки вsnake_keys:

const camelToSnake = str => {     return str.replace(/[A-Z]/g, letter => {         return '_' + letter.toLowerCase()     }) } 

Шаг 2. Работа с объектами

# Возьмем пример с начала статьи { user_name: "user1", main_title: "Title", } 

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

constsimpleKeysTransform=value=>{returnObject.entries(value).reduce((acc, [key,value]) => {constnewKey=snakeToCamel(key)        return{...acc, [newKey]:value}}, {})}

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

const keysTransform1 = (value, isInitialSnake = true) => {     const chooseStyle = isInitialSnake ? snakeToCamel : camelToSnake     return Object.entries(value).reduce((acc, [key, value]) => {         const newKey = chooseStyle(key)         return {...acc, [newKey]: value}     }, {}) } 

Шаг 3. Что делать с вложенными объектами

# Например {   user_info: {     first_name: "User",     last_name: "Userin   } } 

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

const keysTransform2 = (input, isInitialSnake = true) => {     const chooseStyle = isInitialSnake ? snakeToCamel : camelToSnake     const recursiveTransform = value => {         if (value && typeof value === 'object') {             return Object.entries(value).reduce((acc, [key, value]) => {                 const newKey = chooseStyle(key)                 const newValue = recursiveTransform(value)                 return {...acc, [newKey]: newValue}             }, {})         }         return value     }     return recursiveTransform(input) } 

Шаг 4. Что делать с массивами

# Например {   users: [     {       first_name: "user1",       phone_number: 8996923     },     {       first_name: "user2",       phone_number: 12312312     }   ]   } 

Всёдо безобразия просто. Добавим проверку на массив и на каждыйегоэлемент навесим нашу рекурсивную функцию.

const keysTransform = (input, isInitialSnake = true) => {     const chooseStyle = isInitialSnake ? snakeToCamel : camelToSnake     const recursiveTransform = value => {         if (Array.isArray(value)) {             return value.map(recursiveTransform)         }         if (value && typeof value === 'object') {             return Object.entries(value).reduce((acc, [key, value]) => {                 const newKey = chooseStyle(key)                 const newValue = recursiveTransform(value)                 return {...acc, [newKey]: newValue}             }, {})         }         return value     }     return recursiveTransform(input) } 

Перемирие

Давайте посмотрим, что получилось: мы реализовали алгоритм преобразования ключей объектов изsnake_keysвcamelKeysи наоборот.Чуть-чуть меньше раздора междуфронтендоми бэкендом неплохо же!

Существуютидругиестили написания составных слов(PascalKeys,kebab-keys, UPPER_SNAKE_KEYS).При надобности, вы уже сами сможете с ними справиться.

Подробнее..

TypeScript для конфигурации WebPack (FE and BE)

30.12.2020 20:10:58 | Автор: admin

Легенда

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

Вот на бумагу архитектор нанес первый блок. Сзади раздалась ругань. Это разработчики, спорили: Как лучше стартовать новый сервис и какой стартер выбрать. У архитектора по спине пронесся холодок. Не успела сложилась архитектура даже для Proof Of Concept, не то что для Minimal Valuable Product, но уже возникли препятствия. Выбор стартера наложит пока не очевидные рамки.

Одно было ясно, сборщик будет использоваться. Архитектор подошел к Team Lead и попросил использовать WebPack и чистый проект без стартера, так как по прошлым проектам с ним в той или иной мере знакомы разработчикам.

Мотивация

Каждый кто в 2020 использовал браузер - пользовался результатами сборки с помощью WebPack.

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

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

Готовые плагины и loader's сильно облегчают работу, задача на 95% заключается в прочтении первой страницы документации, чтобы сконфигурировать под конкретный проект. Даже в таком случае ошибки в синтаксисе случаются. Мало кто сходу вспомнит devtool или devtools. Некоторые директивы относились к другой версии WebPack. Учет этого будет полезным положить на плечи TypeScript.

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

Особенности проекта в статье

В проекте для статьи нет цели написать всеобъемлющий мануал по настройке, будет базовый пример для backend и frontend.

Cервер будет отдавать статическую директорию с FE для нашего сайта. Сам же FE будет только выводить на страницу Hello World!. Зависимостями для BE будет node, для сборки webpack.

GitHub: тут

Структура директорий c описанием

Для удобства демонстрации я буду использовать моно-репозиторий с server и webapp в одном проекте

  • ~/projectfolder/ # Корень проекта -- инициализирован с помощью yarn init

    • /apps # директория приложений

      • /server # директория backend -- инициализирована с помощью yarn init

        • /src # исходный код сервера

        • файлы конфигурации (части относящиеся к BE)

      • /webapp # директория frontend -- инициализирована с помощью yarn init

        • /src # исходный код браузерного приложения

        • файлы конфигурации (части относящиеся к FE)

      • /utils # расширенные утилиты

    • общие части конфигурации

Зависимости проекта

  • Общие в директории ~/project_folder

yarn add -D @types/node @types/webpack concurrently cross-env nodemon ts-loader ts-node typescript webpack webpack-cli
  • Для сервера в директории /apps/server нам не понадобится дополнительных зависимостей помимо тех что есть в общей директории

  • Для веб-приложения в директории /apps/web_app нам понадобится html-webpack-plugin 5 версии так как он предназначен для использования с WebPack 5 версии. На Момент написания этот покет еще в beta доступе.

cd apps/web_appyarn add -D html-webpack-plugin@5

Настройки TypeScript

Браузер, server, и компьютер разработчика или runner - это три среды с личными особенностями:

Для сервера главное, nodeс помощью которой будет выполняться итоговый скрипт сервера. Что доступно в зависимости от версии наглядно показывается по ссылке:https://node.green

Конкретная настройка сервера apps/server/tsconfig.json не влияет на сборку, главное в конфигурации webpack указать правильны путь до файла для сборки сервера.

Для браузера, на конец 2020, лучше выбиратьES6если нет задачи поддерживать Internet Explorer 11. Хороший сайт для проверки доступных функций: https://caniuse.com

Файл: apps/web_app/tsconfig.json

Компьютер разработчика или runner где будет собираться проект тоже накладывает ограничения, которые в большинстве ситуаций легко устранимы. Для запуска также понадобиться конфигурация TS, она будет использоваться ts-node который будет запускаться под капотом webpack.

Spoiler

tsconfig.json

"compilerOptions": {    "module": "commonjs",    "target": "es5",    "esModuleInterop": true  }

Данный файл обязателен и частью для запуска самого webpack с конфигурацией написанной на typescript

Серверное приложение

Сервер для данной статьи предельно прост, раздачей файлов из одной папки. Код является копией статьи (ссылка) с сайта node, адаптированный под этот проект и с защитой от доступа к родительским папкам ..\..\secret в запрошенных файлах.

Spoiler

apps/server/src/index.ts

import { resolve, normalize, join } from 'path'import { createServer, RequestListener} from 'http'import { readFile } from 'fs' const webAppBasePath = '../web_app'; // Это путь до папки уже после build (в директории dist)const handleWebApp: RequestListener = (req, res) => {    const resolvedBase = resolve(__dirname ,webAppBasePath);    const safeSuffix = normalize(req.url || '')        .replace(/^(\.\.[\/\\])+/, '');    const fileLocation = join(resolvedBase, safeSuffix);    readFile(fileLocation, function(err, data) {        if (err) {            res.writeHead(404, 'Not Found');            res.write('404: File Not Found!');            return res.end();        }        res.statusCode = 200;        res.write(data);        return res.end();    });};const httpServer = createServer(handleWebApp)httpServer.listen("5000", () => {    console.info('Listen on 5000 port')})

Frontend приложение

Web приложение также предельно простое. В document.body монтируется простой &lt;div id="root">Hello world!&lt;/div>

Spoiler

apps/webapp/src/index.ts

const rootNode = document.createElement('div')rootNode.setAttribute('id', 'root')rootNode.innerText = 'Hello World!'document.body.appendChild(rootNode)

Настройка WebPack

Теперь нам осталось только настроить webpack.

Для удобства конфигурацию можно разбить на файлы. А так как мы используем TS, то мы получаем синтаксис import {serverConfig} from "./apps/server/webpack.part"; из-за этого основной файл становится предельно коротким.

Spoiler

webpack.config.ts

import {serverConfig} from "./apps/server/webpack.part";import {webAppConfig} from "./apps/web_app/webpack.part";import {commonConfig} from "./webpack.common";export default [    /** server  **/ {...commonConfig, ...serverConfig},    /** web_app **/ {...commonConfig, ...webAppConfig},]

В нем мы только импортируем конфигурации и экспортируем их в виде массива попутно объединяя с общей частью.

Общая часть

Общая часть может содержать все что можно переиспользовать между различными конфигурациями. В нашем случае это поля mode и resolve. Обратите внимание, что у константы объявлена типизация const commonConfig: Configuration, тип взят из import {Configuration} from "webpack";.

Spoiler

webpack.common.ts

import {Configuration, RuleSetRule} from "webpack";import {isDev} from "./apps/_utils";export const tsRuleBase: RuleSetRule = {    test: /\.ts$/i,    loader: 'ts-loader',}export const commonConfig: Configuration = {    mode: isDev ? 'development' : 'production',    resolve: {        extensions: ['.tsx', '.ts', '.js', '.json'],    },}

Также в этом файле лежит общая для проекта часть настройки правила для загрузки TS файлов const tsRuleBase: RuleSetRule, тип взят из import {RuleSetRule} from "webpack";.

isDev это простая проверка isDev = process.env.NODE_ENV === 'development'

Конфигурация FE и BE

Тут уже все максимально похоже на простую настройку webpack, только с подсказками благодаря типизации import {Configuration, RuleSetRule, WebpackPluginInstance} from "webpack";

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

Spoiler

apps/server/webpack.part.ts

import {Configuration, RuleSetRule, WatchIgnorePlugin, WebpackPluginInstance} from "webpack";import {join} from "path";import {tsRuleBase} from "../../webpack.common";const serverPlugins: WebpackPluginInstance[] = [    new WatchIgnorePlugin({        paths: [join(__dirname, '..', 'apps', 'web_app')]    })]const tsRuleServer: RuleSetRule = {    ...tsRuleBase,    options: {        configFile: join(__dirname, 'tsconfig.json')    }}export const serverConfig: Configuration = {    entry: join(__dirname, 'src', 'index.ts'),    output: {        path: join(__dirname, '..', '..', 'dist', 'server'),        filename: 'server.js'    },    target: 'node',    plugins: serverPlugins,    module: {        rules: [tsRuleServer]    }}
Spoiler

apps/webapp/webpack.part.ts

import {Configuration, RuleSetRule, WatchIgnorePlugin, WebpackPluginInstance} from "webpack";import HtmlWebpackPlugin from "html-webpack-plugin";import {join} from "path";import {tsRuleBase} from "../../webpack.common";const webAppPlugins: WebpackPluginInstance[] = [    new HtmlWebpackPlugin(),    new WatchIgnorePlugin({        paths: [join(__dirname, '..', 'apps', 'server')]    })]const tsRuleWebApp: RuleSetRule = {    ...tsRuleBase,    options: {        configFile: join(__dirname, 'tsconfig.json')    }}export const webAppConfig: Configuration = {    entry: join(__dirname, 'src', 'index.ts'),    output: {        path: join(__dirname, '..', '..', 'dist', 'web_app'),        filename: 'bundle.js'    },    target: 'web',    plugins: webAppPlugins,    module: {        rules: [tsRuleWebApp]    }}

Один из интересный моментов - это указание пути до файла конфигурации для ts-loader, выглядит это так configFile: join(__dirname, 'tsconfig.json'). Так как __dirname в каждом случае различен. То в случае backend все компилируется в целевую версию EcmaScript esnext, а для frontend в es6.

Заключение

Весь код приведенный в статью публикуется под "UNLICENSE". Что также указано в репозитории Github: тут.

Использование в проектах конфигурации через TS - это конечно не бизнес фича. Но привносит комфорт в процесс настройки. На небольших проектах это не так заметно, но если вы например используете micro-frontend c помощью ModuleFederationPlugin, то количество файлов конфигурации webpack растет с каждым микро-приложением и комфорт при настройке становится важен, тем более что время затраченное на именно TS тут минимальное.

PS. Хотелось бы узнать будет ли вам интересна настройка разработки через разворачивание в docker (для VSCode и JetBrains)

Подробнее..

Recovery mode TypeScript для конфигурации WebPack (FE and BE)

31.12.2020 00:22:22 | Автор: admin

Легенда

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

Вот на бумагу архитектор нанес первый блок. Сзади раздалась ругань. Это разработчики, спорили: Как лучше стартовать новый сервис и какой стартер выбрать. У архитектора по спине пронесся холодок. Не успела сложилась архитектура даже для Proof Of Concept, не то что для Minimal Valuable Product, но уже возникли препятствия. Выбор стартера наложит пока не очевидные рамки.

Одно было ясно, сборщик будет использоваться. Архитектор подошел к Team Lead и попросил использовать WebPack и чистый проект без стартера, так как по прошлым проектам с ним в той или иной мере знакомы разработчикам.

Мотивация

Каждый кто в 2020 использовал браузер - пользовался результатами сборки с помощью WebPack.

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

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

Готовые плагины и loader's сильно облегчают работу, задача на 95% заключается в прочтении первой страницы документации, чтобы сконфигурировать под конкретный проект. Даже в таком случае ошибки в синтаксисе случаются. Мало кто сходу вспомнит devtool или devtools. Некоторые директивы относились к другой версии WebPack. Учет этого будет полезным положить на плечи TypeScript.

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

Особенности проекта в статье

В проекте для статьи нет цели написать всеобъемлющий мануал по настройке, будет базовый пример для backend и frontend.

Cервер будет отдавать статическую директорию с FE для нашего сайта. Сам же FE будет только выводить на страницу Hello World!. Зависимостями для BE будет node, для сборки webpack.

GitHub: тут

Структура директорий c описанием

Для удобства демонстрации я буду использовать моно-репозиторий с server и webapp в одном проекте

  • ~/projectfolder/ # Корень проекта -- инициализирован с помощью yarn init

    • /apps # директория приложений

      • /server # директория backend -- инициализирована с помощью yarn init

        • /src # исходный код сервера

        • файлы конфигурации (части относящиеся к BE)

      • /webapp # директория frontend -- инициализирована с помощью yarn init

        • /src # исходный код браузерного приложения

        • файлы конфигурации (части относящиеся к FE)

      • /utils # расширенные утилиты

    • общие части конфигурации

Зависимости проекта

  • Общие в директории ~/project_folder

yarn add -D @types/node @types/webpack concurrently cross-env nodemon ts-loader ts-node typescript webpack webpack-cli
  • Для сервера в директории /apps/server нам не понадобится дополнительных зависимостей помимо тех что есть в общей директории

  • Для веб-приложения в директории /apps/web_app нам понадобится html-webpack-plugin 5 версии так как он предназначен для использования с WebPack 5 версии. На Момент написания этот покет еще в beta доступе.

cd apps/web_appyarn add -D html-webpack-plugin@5

Настройки TypeScript

Браузер, server, и компьютер разработчика или runner - это три среды с личными особенностями:

Для сервера главное, nodeс помощью которой будет выполняться итоговый скрипт сервера. Что доступно в зависимости от версии наглядно показывается по ссылке:https://node.green

Конкретная настройка сервера apps/server/tsconfig.json не влияет на сборку, главное в конфигурации webpack указать правильны путь до файла для сборки сервера.

Для браузера, на конец 2020, лучше выбиратьES6если нет задачи поддерживать Internet Explorer 11. Хороший сайт для проверки доступных функций: https://caniuse.com

Файл: apps/web_app/tsconfig.json

Компьютер разработчика или runner где будет собираться проект тоже накладывает ограничения, которые в большинстве ситуаций легко устранимы. Для запуска также понадобиться конфигурация TS, она будет использоваться ts-node который будет запускаться под капотом webpack.

Spoiler

tsconfig.json

"compilerOptions": {    "module": "commonjs",    "target": "es5",    "esModuleInterop": true  }

Данный файл обязателен и частью для запуска самого webpack с конфигурацией написанной на typescript

Серверное приложение

Сервер для данной статьи предельно прост, раздачей файлов из одной папки. Код является копией статьи (ссылка) с сайта node, адаптированный под этот проект и с защитой от доступа к родительским папкам ..\..\secret в запрошенных файлах.

Spoiler

apps/server/src/index.ts

import { resolve, normalize, join } from 'path'import { createServer, RequestListener} from 'http'import { readFile } from 'fs' const webAppBasePath = '../web_app'; // Это путь до папки уже после build (в директории dist)const handleWebApp: RequestListener = (req, res) => {    const resolvedBase = resolve(__dirname ,webAppBasePath);    const safeSuffix = normalize(req.url || '')        .replace(/^(\.\.[\/\\])+/, '');    const fileLocation = join(resolvedBase, safeSuffix);    readFile(fileLocation, function(err, data) {        if (err) {            res.writeHead(404, 'Not Found');            res.write('404: File Not Found!');            return res.end();        }        res.statusCode = 200;        res.write(data);        return res.end();    });};const httpServer = createServer(handleWebApp)httpServer.listen("5000", () => {    console.info('Listen on 5000 port')})

Frontend приложение

Web приложение также предельно простое. В document.body монтируется простой &lt;div id="root">Hello world!&lt;/div>

Spoiler

apps/webapp/src/index.ts

const rootNode = document.createElement('div')rootNode.setAttribute('id', 'root')rootNode.innerText = 'Hello World!'document.body.appendChild(rootNode)

Настройка WebPack

Теперь нам осталось только настроить webpack.

Для удобства конфигурацию можно разбить на файлы. А так как мы используем TS, то мы получаем синтаксис import {serverConfig} from "./apps/server/webpack.part"; из-за этого основной файл становится предельно коротким.

Spoiler

webpack.config.ts

import {serverConfig} from "./apps/server/webpack.part";import {webAppConfig} from "./apps/web_app/webpack.part";import {commonConfig} from "./webpack.common";export default [    /** server  **/ {...commonConfig, ...serverConfig},    /** web_app **/ {...commonConfig, ...webAppConfig},]

В нем мы только импортируем конфигурации и экспортируем их в виде массива попутно объединяя с общей частью.

Общая часть

Общая часть может содержать все что можно переиспользовать между различными конфигурациями. В нашем случае это поля mode и resolve. Обратите внимание, что у константы объявлена типизация const commonConfig: Configuration, тип взят из import {Configuration} from "webpack";.

Spoiler

webpack.common.ts

import {Configuration, RuleSetRule} from "webpack";import {isDev} from "./apps/_utils";export const tsRuleBase: RuleSetRule = {    test: /\.ts$/i,    loader: 'ts-loader',}export const commonConfig: Configuration = {    mode: isDev ? 'development' : 'production',    resolve: {        extensions: ['.tsx', '.ts', '.js', '.json'],    },}

Также в этом файле лежит общая для проекта часть настройки правила для загрузки TS файлов const tsRuleBase: RuleSetRule, тип взят из import {RuleSetRule} from "webpack";.

isDev это простая проверка isDev = process.env.NODE_ENV === 'development'

Конфигурация FE и BE

Тут уже все максимально похоже на простую настройку webpack, только с подсказками благодаря типизации import {Configuration, RuleSetRule, WebpackPluginInstance} from "webpack";

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

Spoiler

apps/server/webpack.part.ts

import {Configuration, RuleSetRule, WatchIgnorePlugin, WebpackPluginInstance} from "webpack";import {join} from "path";import {tsRuleBase} from "../../webpack.common";const serverPlugins: WebpackPluginInstance[] = [    new WatchIgnorePlugin({        paths: [join(__dirname, '..', 'apps', 'web_app')]    })]const tsRuleServer: RuleSetRule = {    ...tsRuleBase,    options: {        configFile: join(__dirname, 'tsconfig.json')    }}export const serverConfig: Configuration = {    entry: join(__dirname, 'src', 'index.ts'),    output: {        path: join(__dirname, '..', '..', 'dist', 'server'),        filename: 'server.js'    },    target: 'node',    plugins: serverPlugins,    module: {        rules: [tsRuleServer]    }}
Spoiler

apps/webapp/webpack.part.ts

import {Configuration, RuleSetRule, WatchIgnorePlugin, WebpackPluginInstance} from "webpack";import HtmlWebpackPlugin from "html-webpack-plugin";import {join} from "path";import {tsRuleBase} from "../../webpack.common";const webAppPlugins: WebpackPluginInstance[] = [    new HtmlWebpackPlugin(),    new WatchIgnorePlugin({        paths: [join(__dirname, '..', 'apps', 'server')]    })]const tsRuleWebApp: RuleSetRule = {    ...tsRuleBase,    options: {        configFile: join(__dirname, 'tsconfig.json')    }}export const webAppConfig: Configuration = {    entry: join(__dirname, 'src', 'index.ts'),    output: {        path: join(__dirname, '..', '..', 'dist', 'web_app'),        filename: 'bundle.js'    },    target: 'web',    plugins: webAppPlugins,    module: {        rules: [tsRuleWebApp]    }}

Один из интересный моментов - это указание пути до файла конфигурации для ts-loader, выглядит это так configFile: join(__dirname, 'tsconfig.json'). Так как __dirname в каждом случае различен. То в случае backend все компилируется в целевую версию EcmaScript esnext, а для frontend в es6.

Заключение

Весь код приведенный в статью публикуется под "UNLICENSE". Что также указано в репозитории Github: тут.

Использование в проектах конфигурации через TS - это конечно не бизнес фича. Но привносит комфорт в процесс настройки. На небольших проектах это не так заметно, но если вы например используете micro-frontend c помощью ModuleFederationPlugin, то количество файлов конфигурации webpack растет с каждым микро-приложением и комфорт при настройке становится важен, тем более что время затраченное на именно TS тут минимальное.

PS. Хотелось бы узнать будет ли вам интересна настройка разработки через разворачивание в docker (для VSCode и JetBrains)

Подробнее..

Чаты на вебсокетах, когда на бэкенде WAMP. Теперь про Android

13.01.2021 22:20:55 | Автор: admin

Мой коллега уже писал про наш опыт разработки чатов на вебсокетах для iOS, поэтому часть про особенности бэкенда с точки зрения клиента у нас общая. А вот реализация на Android, конечно, отличается. И ещё мне не приходилось, как в первой статье, искать библиотеку для поддержки старых версий операционной системы, потому что на Android каких-то глобальных изменений в сетевой части не было, всё работало и так.

К реализации вернёмся чуть ниже, а начнём с ответов на вопросы про бэкенд, которые появились после первой статьи: почему WAMP, какой брокер используем и некоторые другие моменты.

На время передам слово нашему бэкенд-разработчику @antoha-gs, а если хочется сразу почитать про клиент-серверное общение и декодирование, то первый раздел можно пропустить.

Что там на бэкенде

Почему WAMP. Изначально искал открытый протокол, который мог бы работать поверх WebSocket с поддержкой функционала PubSub и RPC и с потенциалом масштабирования. Лучше всего подошёл WAMP одни плюсы, разве что не нашёл реализации протокола на Java/Kotlin, которая бы меня устраивала.

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

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

Ещё был такой момент в комментариях:

Правильно, что не стали использовать Socket.IO, так как рано или поздно столкнулись бы с двумя проблемами: 1) Пропуск сообщений. 2) Дублирование сообщений. WAMP к сожалению также не решает эти вопросы. Поэтому для чатов лучше использовать что-то вроде MQTT.

Насколько я могу судить, протокол не решает таких проблем магическим образом, всё упирается в реализацию. Да, на уровне протокола может поддерживаться дополнительная информация/настройки для указания уровня обслуживания (at most/at least/exactly), но ответственность за её реализацию всё равно лежит на конкретной имплементации. В нашем случае, учитывая специфику, достаточно гарантировать надёжную запись в базу и доставку на клиенты at most once, что WAMP вполне позволяет реализовать. Также он легко расширяем.

MQTT отличный протокол, никаких вопросов, но в данном сравнении у него меньше фич, чем у WAMP, которые могли бы пригодиться нам для сервиса чатов. В качестве альтернативы можно было бы рассмотреть XMPP (aka Jabber), потому что, в отличие от MQTT и WAMP, он предназначен для мессенджеров, но и там без допилов бы не обошлось. Ещё можно создать свой собственный протокол, что нередко делают в компаниях, но это, в том числе, дополнительные временные затраты.

Это были основные вопросы касательно бэкенда после предыдущей статьи, и, думаю, мы ещё вернёмся к нашей реализации в отдельном материале. А сейчас возвращаю слово Сергею.

Клиент-сервер

Начну с того, что WAMP означает для клиента.

  • В целом протокол предусматривает почти всё. Это облегчает взаимодействие разработчиков клиентской части и бэка.

  • Кодирование всех типов событий в числах (PUBLISH это 16, SUBSCRIBE 32 и так далее). Это усложняет чтение логов разработчику и QA (сразу не догадаться, что значит прилетевшее сообщение [33,11,5862354]).

  • Механизм подписок на события (например, новые сообщения в чат или обновление количества участников) реализован через получение от бэкенда уникального id подписки. Его надо где-то хранить и ни в коем случае не терять во избежание утечек. Как это сделано (было бы сильно проще и подписываться и отписываться просто по id чата):client подписываемся на новые сообщения в чате [32,18,{},"co.fun.chat.testChatId"]backend [33,18,5868752 (id подписки)]client после выхода из чата отписываемся по id [34,20,5868752]

Для работы с сокетом использовали OkHttp (стильно, надёжно, современно, реализация ping-pong таймаутов из коробки) и RxJava, потому что сама концепция чата практически идеальный пример того самого event-based programming, ради которого Rx, в общем, и задумывался.

Теперь рассмотрим пример коннекта к серверу, использующему WAMP-протокол через OkHttpClient:

val request = Request.Builder()    .url(ChatsConfig.SOCKETURL)    .addHeader("Connection", "Upgrade")    .addHeader("Sec-WebSocket-Protocol", "wamp.json")    .addHeader("Authorization", authToken)    .build()val listener = ChatWebSocketListener()webSocket = okHttpClient.newWebSocket(request, listener)

Пример реализации ChatWebSocketListener:

private inner class ChatWebSocketListener : WebSocketListener() {override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { connectionStatusSubject.onNext(ChatConnectionStatuses.NOTCONNECTED) //subject, оповещающий пользователей о состоянии коннекта (в UI нужен для отображения лоадеров, оффлайн-стейтов и так далее)}override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { webSocket.close(1000, null)}override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { onConnectionError("${t.message} ${response?.body}")}override fun onMessage(webSocket: WebSocket, text: String) { socketMessagesSubject.onNext(serverMessageFactory.processMessage(text)) //subject, через который идут все сообщения, которые в дальнейшем фильтруются для конкретных получателей (см. ниже)}override fun onOpen(webSocket: WebSocket, response: Response) { authorize() }}

Здесь мы видим, что все сообщения от сокета приходят в виде обычного String, представляющего собой JSON, закодированный по правилам WAMP протокола и имеющий структуру:

[ResultCode: Int, RequestId: Long, ArgumentsMap: JsonObject ]

Например:

[50, 7, {"type":100, "chats":[список чатов]}]

Декодирование и отправка сообщений

Для декодинга сообщений в объекты мы использовали библиотеку Gson. Все модели ответа отписываются обычными data-классами вида:

@DontObfuscatedata class ChatListResponse(@SerializedName("chats") val chatList: List&lt;Chat>)

А декодирование происходит с помощью следующего кода:

private fun chatListUpdateInternal(jsonChatsResponse: JSONObject):ChatsListUpdatesEvent { return gson.fromJson(jsonChatsResponse.toString(), ChatsListUpdatesEvent::class.java)}

Теперь рассмотрим базовый пример отправки сообщения по сокету. Для удобства мы сделали обёртку для всех базовых типов WAMP сообщений:

sealed class WampMessage { class BaseMessage(val wampId: Int, val seq: Long, val jsonData: JSONArray) : WampMessage()  class ErrorMessage(val procedureId: Int, val seq: Long, val jsonData: JSONArray) : WampMessage() object WelcomeMessage : WampMessage() class AbortMessage(val jsonData: JSONArray) : WampMessage()}

А также добавили фабрику для формирования этих сообщений:

fun getCallMessage(rpc: String,         options: Map&lt;String, Any> = emptyMap(),         arguments: List&lt;Any?> = emptyList(),         argumentsDict: Map&lt;String, Any?> = emptyMap()):WampMessage.BaseMessage { //[CALL, Request|id, Options|dict, Procedure|uri, Arguments|list] val seq = nextSeq.getAndIncrement() return WampMessage.BaseMessage(WAMP.MessageIds.CALL,               seq,               JSONArray(listOfNotNull(WAMP.MessageIds.CALL,               seq,               options,               rpc,               arguments,               argumentsDict)))}

Пример отправки сообщений:

val messages: Observable&lt;WampMessage> = socketMessagesSubjectfun sendMessage(msgToSend: WampMessage.BaseMessage): Observable&lt;WampMessage> { return messages.filter {   it is WampMessage.BaseMessage &amp;&amp; it.seq == msgToSend.seq}    .take(1)    .doOnSubscribe {     webSocket.send(msgToSend.jsonData.toString())    }}

Сопоставление отправленного сообщения и ответа на него в WAMP происходит с помощью уникального идентификатора seq, отправляемого клиентом, который потом кладётся в ответ.

В клиенте генерация идентификатора делается следующим образом:

companion object { private val nextSeq: AtomicLong = AtomicLong(1)}fun getNextSeq() = nextSeq.getAndIncrement()

Взаимодействие с WAMP Subscriptions

Подписки в протоколе WAMP концепт, по которому подписчик (клиент) подписывается на какие-либо события, приходящие от бэкенда. В нашей реализации мы использовали:

  • обновление списка чатов;

  • новые сообщения в конкретном чате;

  • изменение онлайн-статуса собеседника;

  • изменение в составе участников чата;

  • смена роли юзера (например, когда его назначают модератором);

  • и так далее.

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

[SUBSCRIBE: Int, RequestId: Long, Options: Map, Topic: String]

Где topic это скоуп событий, которые нужны подписчику.

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

fun getSubscribeMessage(topic: String, options: Map&lt;String, Any> = emptyMap()): WampMessage.BaseMessage { val seq = nextSeq.getAndIncrement() return WampMessage.BaseMessage(WAMP.MessageIds.SUBSCRIBE,                 seq,                JSONArray(listOfNotNull(WAMP.MessageIds.SUBSCRIBE,                                seq,                                options,                                topic)))}

Разумеется, при выходе с экрана (например, списка чатов), необходимо соответствующую подписку корректно отменять. И вот тут выявляется одно из свойств протокола WAMP: при отправке subscribe-сообщения бэкенд возвращает числовой id подписки, и выходит, что отписаться от конкретного топика нельзя нужно запоминать и хранить этот id, чтобы использовать его при необходимости.

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

private val subscriptionsMap = ArrayMap&lt;String, Long>()private fun getBaseSubscription(topic: String): Observable&lt;WampMessage> { val msg = wampClientMessageFactory.getSubscribeMessage(topic) return send(msg).map {   val subscriptionId = converter.getSubscriptionId((it.asBaseMessage()).jsonData)   subscriptionsMap[topic] = subscriptionId   subscriptionId}    .switchMap { subscriptionId ->      chatClient.messages.filter {       it.isMessageFromSubscription(subscriptionId)     }    }}

Так клиент ничего не будет знать об id, и для отписки ему будет достаточно указать имя подписки, которую необходимо отменить:

fun unsubscribeFromTopic(topic: String) { if (!subscriptionsMap.contains(topic)) {    return } val msg = wampClientMessageFactory.getUnsubscribeMessage(subscriptionsMap[topic]) send(msg, true).exSubscribe() subscriptionsMap.remove(topic)}

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

Подробнее..

Перевод Java Optional не такой уж очевидный

03.02.2021 22:16:57 | Автор: admin

NullPointerException - одна из самых раздражающих вещей в Java мире, которую был призван решить Optional. Нельзя сказать, что проблема полностью ушла, но мы сделали большие шаги. Множество популярных библиотек и фреймворков внедрили Optional в свою экосистему. Например, JPA Specification возвращает Optional вместо null.

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

Optional не должен равняться null

Мне кажется, никаких дополнительных объяснений здесь не требуется. Присваивание null в Optional разрушает саму идею его использования. Никто из пользователей вашего API не будет проверять Optional на эквивалентность с null. Вместо этого следует использовать Optional.empty().

Знайте API

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

public String getPersonName() {    Optional<String> name = getName();    if (name.isPresent()) {        return name.get();    }    return "DefaultName";}

Идея проста: если имя отсутствует, вернуть значение по умолчанию. Можно сделать это лучше.

public String getPersonName() {    Optional<String> name = getName();    return name.orElse("DefautName");}

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

public Optional<String> getPersonName() {    Person person = getPerson();    if (ALLOWED_NAMES.contains(person.getName())) {        return Optional.ofNullable(person.getName());    }    return Optional.empty();}

Optional.filter упрощает код.

public Optional<String> getPersonName() {    Person person = getPerson();    return Optional.ofNullable(person.getName())                   .filter(ALLOWED_NAMES::contains);}

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

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

Отдавайте предпочтение контейнерам "на примитивах"

В Java присутствуют специальные не дженерик Optional классы: OptionalInt, OptionalLong и OptionalDouble. Если вам требуется оперировать примитивами, лучше использовать вышеописанные альтернативы. В этом случае не будет лишних боксингов и анбоксингов, которые могут повлиять на производительность.

Не пренебрегайте ленивыми вычислениями

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

public Optional<Table> retrieveTable() {    return Optional.ofNullable(constructTableFromCache())                   .orElse(fetchTableFromRemote());}

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

public Optional<Table> retrieveTable() {    return Optional.ofNullable(constructTableFromCache())                   .orElseGet(this::fetchTableFromRemote);}

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

Не оборачивайте коллекции в Optional

Хотя я и видел такое не часто, иногда это происходит.

public Optional<List<String>> getNames() {    if (isDevMode()) {        return Optional.of(getPredefinedNames());    }    try {        List<String> names = getNamesFromRemote();        return Optional.of(names);    }    catch (Exception e) {        log.error("Cannot retrieve names from the remote server", e);        return Optional.empty();    }}

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

public List<String> getNames() {    if (isDevMode()) {        return getPredefinedNames();    }    try {        return getNamesFromRemote();    }    catch (Exception e) {        log.error("Cannot retrieve names from the remote server", e);        return emptyList();    }}

Чрезмерное использование Optional усложняет работу с API.

Не передавайте Optional в качестве параметра

А сейчас мы начинаем обсуждать наиболее спорные моменты.

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

public void doAction() {    OptionalInt age = getAge();    Optional<Role> role = getRole();    applySettings(name, age, role);}

Во-первых, API имеет ненужные границы. При каждом вызове applySettings пользователь вынужден оборачивать значения в Optional. Даже предустановленные константы.

Во-вторых, applySettings обладает четырьмя потенциальными поведениями в зависимости от того, являются ли переданные Optional пустыми, или нет.

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

Если взглянуть на javadoc к Optional, можно найти там интересную заметку.

Optional главным образом предназначен для использования в качестве возвращаемого значения в тех случаях, когда нужно отразить состояние "нет результата" и где использование null может привести к ошибкам.

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

Что же, как мы можем улучшить этот код? Если age и role должны всегда присутствовать, мы можем легко избавиться от Optional и решать проблему отсутствующих значений на верхнем уровне.

public void doAction() {    OptionalInt age = getAge();    Optional<Role> role = getRole();    applySettings(name, age.orElse(defaultAge), role.orElse(defaultRole));}

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

С другой стороны, если значения age и role могут быть опущены, вышеописанный способ не заработает. В этом случае лучшим решением будет разделение API на отдельные методы, удовлетворяющим разным пользовательским потребностям.

void applySettings(String name) { ... }void applySettings(String name, int age) { ... }void applySettings(String name, Role role) { ... }void applySettings(String name, int age, Role role) { ... }

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

Не используйте Optional в качестве полей класса

Я слышал разные мнения по этому вопросу. Некоторые считают, что хранение Optional напрямую в полях класса позволяет сократить NullPointerException на порядок. Мой друг, который работает в одном известном стартапе, говорит, что такой подход в их компании является утвержденным паттерном.

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

Отсутствие сериализуемости

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

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

Хранение лишних ссылок

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

Плохая интеграция со Spring Data/Hibernate

Предположим, что мы хотим построить простое Spring Boot приложение. Нам нужно получить данные из таблицы в БД. Сделать это очень просто, объявив Hibernate сущность и соответствующий репозиторий.

@Entity@Table(name = "person")public class Person {    @Id    private long id;    @Column(name = "firstname")    private String firstName;    @Column(name = "lastname")    private String lastName;        // constructors, getters, toString, and etc.}public interface PersonRepository extends JpaRepository<Person, Long> {}

Вот возможный результат для personRepository.findAll().

Person(id=1, firstName=John, lastName=Brown)Person(id=2, firstName=Helen, lastName=Green)Person(id=3, firstName=Michael, lastName=Blue)

Пусть поля firstName и lastName могут быть null. Мы не хотим иметь дело с NullPointerException, так что просто заменим обычный тип поля на Optional.

@Entity@Table(name = "person")public class Person {    @Id    private long id;    @Column(name = "firstname")    private Optional<String> firstName;    @Column(name = "lastname")    private Optional<String> lastName;        // constructors, getters, toString, and etc.}

Теперь все сломано.

org.hibernate.MappingException: Could not determine type for: java.util.Optional, at table: person,       for columns: [org.hibernate.mapping.Column(firstname)]

Hibernate не может замапить значения из БД на Optional напрямую (по крайней мере, без кастомных конвертеров).

Но некоторые вещи работают правильно

Должен признать, что в конечном итоге не все так плохо. Некоторые фреймворки корректно интегрируют Optional в свою экосистему.

Jackson

Давайте объявим простой эндпойнт и DTO.

public class PersonDTO {    private long id;    private String firstName;    private String lastName;    // getters, constructors, and etc.}
@GetMapping("/person/{id}")public PersonDTO getPersonDTO(@PathVariable long id) {    return personRepository.findById(id)            .map(person -> new PersonDTO(                    person.getId(),                    person.getFirstName(),                    person.getLastName())            )            .orElseThrow();}

Результат для GET /person/1.

{  "id": 1,  "firstName": "John",  "lastName": "Brown"}

Как вы можете заметить, нет никакой дополнительной конфигурации. Все работает из коробки. Давайте попробует заменить String на Optional<String>.

public class PersonDTO {    private long id;    private Optional<String> firstName;    private Optional<String> lastName;    // getters, constructors, and etc.}

Для того чтобы проверить разные варианты работы, я заменил один параметр на Optional.empty().

@GetMapping("/person/{id}")public PersonDTO getPersonDTO(@PathVariable long id) {    return personRepository.findById(id)            .map(person -> new PersonDTO(                    person.getId(),                    Optional.ofNullable(person.getFirstName()),                    Optional.empty()            ))            .orElseThrow();}

Как ни странно, все по-прежнему работает так, как и ожидается.

{  "id": 1,  "firstName": "John",  "lastName": null}

Это значит, что мы можем использовать Optional в качестве полей DTO и безопасно интегрироваться со Spring Web? Ну, вроде того. Однако есть потенциальные проблемы.

SpringDoc

SpringDoc это библиотека для Spring Boot приложений, которая позволяет автоматически сгенерировать Open Api спецификацию.

Вот пример того, что мы получим для эндпойнта GET /person/{id}.

"PersonDTO": {  "type": "object",  "properties": {    "id": {      "type": "integer",      "format": "int64"    },    "firstName": {      "type": "string"    },    "lastName": {      "type": "string"    }  }}

Выглядит довольно убедительно. Но нам нужно сделать поле id обязательным. Это можно осуществить с помощью аннотации @NotNull или @Schema(required = true). Давайте добавим кое-какие детали. Что если мы поставим аннотацию @NotNull над полем типа Optional?

public class PersonDTO {    @NotNull    private long id;    @NotNull    private Optional<String> firstName;    private Optional<String> lastName;    // getters, constructors, and etc.}

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

"PersonDTO": {  "required": [    "firstName",    "id"  ],  "type": "object",  "properties": {    "id": {      "type": "integer",      "format": "int64"    },    "firstName": {      "type": "string"    },    "lastName": {      "type": "string"    }  }}

Как видим, поле id действительно добавилось в список обязательных. Так же, как и firstName. А вот здесь начинается самое интересное. Поле с Optional не может быть обязательным, так как само его наличие говорит о том, что значение потенциально может отсутствовать. Тем не менее, мы смогли запутать фреймворк с помощью всего лишь одной лишней аннотации.

В чем здесь проблема? Например, если кто-то на фронтенде использует генератор типов сущностей по схеме Open Api, это приведет к получению неверной структуры, что в свою очередь может привести к повреждению данных.

Решение

Что же нам делать со всем этим? Ответ прост. Используйте Optional только для геттеров.

public class PersonDTO {    private long id;    private String firstName;    private String lastName;        public PersonDTO(long id, String firstName, String lastName) {        this.id = id;        this.firstName = firstName;        this.lastName = lastName;    }        public long getId() {        return id;    }        public Optional<String> getFirstName() {        return Optional.ofNullable(firstName);    }        public Optional<String> getLastName() {        return Optional.ofNullable(lastName);    }}

Теперь этот класс можно безопасно использовать и как сущность Hibernate, и как DTO. Optional никак не влияет на хранимые данные. Он только оборачивает возможные null, чтобы корректно отрабатывать отсутствующие значения.

Однако у этого подхода есть один недостаток. Его нельзя полностью интегрировать с Lombok. Optional getters не подерживаются библиотекой и, судя по некоторым обсуждениям на Github, не будут.

Я писал статью по Lombok и я думаю, что это прекрасный инструмент. Тот факт, что он не интегрируются с Optional getters, довольно печален.

На текущий момент единственным выходом является ручное объявление необходимых геттеров.

Заключение

Это все, что я хотел сказать по поводу java.util.Optional. Я знаю, что это спорная тема. Если у вас есть какие-то вопросы или предложения, пожалуйста, оставляйте свои комментарии. Спасибо за чтение!

Подробнее..

Бесплатные онлайн-мероприятия по разработке (1 марта 7 марта 2021)

27.02.2021 14:17:14 | Автор: admin

2 марта, Вторник

Dev-to-Consult

Мне нравится писать код, но как долго у меня получится этим заниматься?

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

Давайте разбираться, как устроены траектории профессионального развития разработчиков.

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

Эксперты
Асхат Уразбаев, управляющий партнёр, ScrumTrek
Максим Дорофеев, прокрастинатолог в mnogosdelal .ru
Георгий Могелашвили, Mentor/Coach, Lead Developer


2 марта, 15:0016:30 мск, Вторник

Регистрация на мероприятие

3 марта, Среда

Inforso Meetup #2
  1. Основной процесс работы в современных командах разработки - Глеб Шалтаев, Senior Frontend developer, банк Открытие
    Правила игры в команде; Основные инструменты и активности команды; Разбор на примере бизнес-задач; Полезные ресурсы.

  2. Roadmap для разработчика: как пройти первое собеседование - Вадим Селяков, Тим-лид, Сберлогистик
    Хард-скилы; Софт-скилы; Языки программирования; Почему стоит или не стоит работать в ИТ; Полезные ресурсы


3 марта, начало в 20:00 мск, Среда

Регистрация на мероприятие

Data Science meetup
  1. Как я перестал бояться и полюбил AutoML - Александр Кузнецов
    Что предлагают современные системы AutoML от Google, Amazon, Microsoft, IBM, зачем они нужны аналитику данных и почему не нужно бояться развития этих сервисов (пока что).

  2. All you need for RecSys is a good baseline - Павел Смирнов
    Обзор на статью от Steffen Rendle с анализом современных тенденций в RecSys к использованию нейросетей вместо классического декартового произведения. Что такое матричная факторизация, алгоритм NeuMF.

  3. Как понять ИИ (или все же ML?) - Роман Щербаков
    Обзор актуальных методов оценки влияния признаков в обучении модели, тонкости и нюансы выбора тех или иных инструментов XAI.

  4. Web Fingerprinting and ML - Илья Стариков
    Про идентификации и анти-идентификации пользователей в сети. Зачем и какую информацию собирают сайты о нас, как ML помогает им в этом.


3 марта, 18:00-20:00 мск, Среда

Регистрация на мероприятие

4 марта, Четверг

Soft Skills Hero Meetup
  1. Сон. Легко потерять, сложно найти, невозможно забыть - Алексей Заборщиков, Andersen
    О нашем самом любимом занятии - профессионально! Что такое сон? Почему нам нужно спать? Как спать лучше?

  2. Resume-Driven Development in Action - Владимир Рожков, Devlify
    Современные технологии развиваются очень быстро. Новые библиотеки и фреймворки выходят чаще, чем успеваешь изучить старые. Нужно бежать, чтобы просто оставаться на месте. Многие задаются вопросом: когда изучать новое, чтобы быть востребованным специалистом? После работы? А когда жить и заниматься своими делами?
    Ответы на эти вопросы заключаются в методологии Resume-Driven Development.

  3. Архитектура - это софт скиллы - Алексей Мигутский, Senior Sofware Engineer, Microsoft/GitHub
    Архитектура это не только код; Почему архитектура требует общения; Закон Конвея; Как взрослеет архитектурный процесс.


4 марта, начало в 19:00 мск, Четверг

Регистрация на мероприятие

Ruby Meetup Online
  1. SmartCore (smart-rb) a set of common abstractions and principles, realized in scope of Ruby, DDD and Clean Architecture - Ибрагимов Рустам, Team Lead, Umbrellio
    О наборе новых библиотек, решающих задачи архитектурного направления в мире Ruby. Инструменты, которые уже реализованы, их преимущества и идеология.

  2. MPI: композитные атрибуты моделей данных - Ильчуков Александр, Ruby разработчик в MPI
    Как реализовать с помощью композитных типов PostgreSQL иерархичные структуры, что увеличит производительность в определённых случаях и повлияет на массивное упрощение бизнес-логики.

  3. Метрики эффективности сервиса - Астхана Аникет, Project manager, Umbrellio
    Метрики Lead Time, Throughput, Flow Efficiency, Resource Efficiency: как они между собой связаны, к чему они чувствительны и как могут нам помочь. Какую информацию о положении дел сервиса можно получить из Jira.


4 марта, 17:0019:30 мск, Четверг

Регистрация на мероприятие

Быстрый веб-сервис

В начале девяностых годов мы работали на компьютерах с процессорами 80386 и 80486. Они казались сверхбыстрыми по сравнению с персоналками предыдущего поколения. На машине с мозгами модели 80386 запускался и работал Doom, что казалось невероятным.

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

Сейчас нам гораздо реже приходится заниматься ускорением кода, но есть задачи, где это всё ещё необходимо.

Одна из таких задач это быстрые веб-сервисы. Иногда речь идёт об очень большом количестве запросов.

Роман Ромадин расскажет о том, как он разрабатывал пиксель-трекер для анализа посещаемости сайта. Сначала трекер был написан на PHP, потом переписан на Go. Для хранения данных применялись разные базы. В конечном итоге производительность выросла на порядок.
Роман опытный бекенд-разработчик, писавший на PHP и на Go.


4 марта, 19:0020:00 мск, Четверг

Регистрация на мероприятие

5 марта, Пятница

Online-митап Hot Frontend от SimbirSoft
  1. Как успешно пройти собеседование на роль frontend-разработчика - Дмитрий, руководитель Frontend-отдела
    Что нужно знать; Как вести себя на интервью; Правильные установки.

  2. Работа с MobX: личный опыт - Евгений, frontend-разработчик
    Ушли от компонентного подхода в сторону решения бизнес-задачи; На пути к 100% декларативному программированию; Из React выброшено всё ненужное.

  3. Динамические диаграммы для Vue на основе SVG - Евгений, frontend-разработчик
    Анимация и построение графиков-диаграмм; Диаграмма без использования сторонних библиотек; GPU анимация


5 марта, начало в 18:00 мск, Пятница

Регистрация на мероприятие

Подборку подготовил автор телеграм-канала ITMeeting - анонсы бесплатных мероприятий по разработке.

Подробнее..

Как решить нестандартные задачи в Backend и не проиграть. Расскажут спикеры конференции DUMP

29.04.2021 22:15:20 | Автор: admin

Обычно участники ждут от конференций полезных докладов с цифрами, фактами и только по делу. Мы учли это, взяли трендовые темы и пригласили опытных спикеров. Так получилась четкая и концентрированная программа секции Backend. Поговорим об актуальных сейчас технологиях и методах. Разберемся в серверной разработке без привязки к языкам программирования. А также поищем оптимальное решение задач, с которыми сталкивается большинство разработчиков бэка.

Ты только посмотри, какие спикеры нам в этом помогут!

Михаил Беляев из Прософт-Системы с докладом Проблемы embedded или как мы от sqlite ушли

При embedded разработке существует очень большая проблема весьма ограниченные ресурсы устройства, на котором выполняется твой код. В ходе разработки, когда запросы клиентов растут, а ресурсы embedded устройства на исходе, очень часто стандартные и проверенные решения не подходят. Разработчик сталкивается с выбором: или сказать, что наше устройство не может справиться с требуемыми задачами, или сделать нестандартный ход и выиграть. Вот именно об этом процессе перехода от использования SQLite к собственному хранилищу данных, которое позволяет читать и писать данные значительно быстрее, Михаил поведает другим разработчикам.

Андрей Цветцих из EPAM представит доклад Чистая Архитектура на практике. Вот что он рассказал нам о своем выступлении:

С момента выхода книги Дяди Боба Clean Architecture прошло уже достаточно времени. Кто-то ее прочитал, а кто-то только смотрел доклады на youtube. Но все эти доклады идейные. У их авторов обычно нет практического опыта создания больших проектов по данной архитектуре (как и запуска этих проектов в production). А все примеры слишком простые! На практике все равно остается много вопросов. Больше года назад мы начали 2 новых проекта, в которых применяли принципы, описанные в книге. Это корпоративные приложения на C# (API, backend). Enterprise, который еще не успел стать кровавым :) Но этого вполне достаточно чтобы получить первые результаты и поделиться опытом.

Роман Неволин из Контура выступит с докладом про Функциональные языки для бизнес-разработки

Функциональные языки зачастую воспринимаются как красивые и модные игрушки посмотреть и повертеть забавно, а вот в суровом энтерпрайзе им не место. Принято считать, что здесь лучше всего подходят проверенные годами, простые и надежные языки, такие как Java. Роман же с этим согласиться никак не может и постоянно пытается применить любимый функциональный язык F# - к очередной бизнес-задаче. На выступлении обсудим, как Роман это делает, зачем оно ему (и всем остальным) нужно, и какие именно грабли он успел собрать на пути.

Кстати, Романа можно будет услышать в нашем бесплатном стриме, о котором мы подробно рассказали здесь.

Евгений Пешков из JetBrains с докладом Клиентский HTTP в .NET: дорога по граблям от WebRequest до SocketsHttpHandler

На первый взгляд кажется, что отправить HTTP запрос это очень просто. Тем не менее, даже HTTP/1.1 достаточно нетривиален RFC на него содержит более 150 страниц, кроме того браузеры уже поддерживают HTTP/2 и HTTP/3. Это не оставляет никакого выбора: стандартный клиент в платформе должен быть реализован на высоком уровне.

На пути от .NET Framework 1.0 к .NET 5 клиентские API для работы с HTTP и его реализации претерпели множество изменений. В некоторых версиях они были удачными, в некоторых же провальными и явно временными.

В докладе Евгений расскажет о истории развития клиентского HTTP API в .NET, его особенностях, о миграции приложений с Framework на Core с их учётом. Также разберет некоторые хаки, полезные при работе с ним. Заглянем в NuGet и рассмотрим представленные в нём обёртки над HTTP API с точки зрения эффективности и кроссплатформенности.

Александр Поляков из Яндекса расскажет Как Яндекс.Афиша 2 раза переезжала на GraphQL

Этот доклад о том, как мы переписали API Я.Афиша с REST на GraphQL на node.js + Python. А затем, в рамках оптимизации, избавились от node.js + Python и переписали весь GraphQL на Java.

Разберемся со следующими вопросами:

почему мы выбрали технологию GraphQL

какие проблемы и задачи решали с ее помощью

расскажем и покажем, как эволюционировала наша архитектура

для каких команд и проектов подходит наше решение, а для каких нет и почему

А также дадим несколько практических советов по тому, как лучше всего начинать работать с GraphQL

Фагим Садыков из SpectrumData представит нам свой доклад Kotlin как основной язык разработки бэкенда и обработки данных

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

В компании SpectrumData Kotlin используется как основной (и по факту единственный) продуктовый язык разработки на платформе JVM для самого широкого круга задач: работа с данными, реализация микро-сервисной архитектуры, реализация бэкенда. История успешного применения Kotlin в компании длится уже 4 года с версии языка 1.1. Для SpectrumData характерно полноценное применение в своей кодобазе всех возможностей и рекомендованных практик по данному языку.

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

Про Аспектно-ориентированное программирование на C# и .NET вчера, сегодня и завтра расскажет Денис Цветцих из Invent

Аспектно-ориентированное программирование (АОП) позволяет без дублирования кода добавлять инфраструктурный функционал вроде кеширования и логгирования на разные слои вашего приложения. И все это не меняя уже написанный код! Это очень мощная, удобная а также редко используемая техника. Отчасти это оправдано, 10 лет назад инструменты для реализации аспектов были не развиты, поэтому за АОП закрепилась слава подхода, используемого только энтузиастами. Но с тех пор мир изменился и сегодня АОП можно увидеть даже в веб-фреймворках, важно только уметь его распознать.

В своем докладе Денис поделится 10-летним опытом использования АОП на C# и .NET. Расскажет о подходах к реализации АОП, а также покажет, как менялись инструменты для разработки аспектов вместе с языком программирования и платформой. Естественно, он предложит наиболее оптимальный на сегодня вариант реализации аспектов. И вместе подумаем, какими хотелось бы видеть инструменты для разработки аспекты в будущем. Примеры будут на C# и .NET, но идеи доклада будут актуальны для любой платформы.

А под занавес программный комитет поставил Антона Шишкина из SKB LAB с докладом Рекомендации и фичи первой свежести . Здесь Антон расскажет про свой опыт построения рекомендательной системы от offline расчетов до online. И отдельно про то, как обеспечивается актуальность фичей для онлайн расчетов. Будут разобраны "допущения", благодаря которым фичи сохраняют свою "свежесть", при этом обеспечивается высокая доступность хранилища признаков.

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

А здесь можно выбрать билеты в онлайн и офлайн формате.

Подробнее..

Envoy как универсальный сетевой примитив

05.02.2021 00:16:03 | Автор: admin

В октябре прошлого года мои коллеги представили на EnvoyCon доклад "Построение гибкой подсистемы компрессии в Envoy". Вот он ниже



Судя по статистике сегодняшней статьи от SergeAx, тема компрессии сетевого трафика оказалась интересной многим. В связи с чем я немедленно возжелал вселенской славы и решил кратко пересказать содержание доклада. Тем более, что он не только о компрессии, но и том, как можно упростить сопровождение сетевой подсистемы как backend'а, так и мобильного frontend'а.


Я не стал полностью "новелизировать" видео доклада, а только ту часть, которую озвучил Хосе Ниньо. Она заинтересует больше людей.


Для начала о том, что такое Envoy.


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



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



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



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



Мобильный клиент общается с граничным прокси (Edge), который решает, куда отправлять клиентские запросы дальше, попутно балансируя нагрузку на сервера. Сервисы получают запросы от Edge не напрямую, а через вспомогательные прокси (Sidecar). Далее, сервисы формируют ответы, опционально пообщавшись друг с другом, и отсылают их к Edge.


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



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



Так появился проект Envoy Mobile, который представляет собой байндинги на Java, Kotlin, Swift, Objective-C к Envoy. А тот уже линкуется к мобильному приложению как нативная библиотека.


Тогда задача уменьшения объёма трафика описанная в статье от FunCorp, могла бы быть решена примерно как на картинке ниже (если поменять местами компрессор и декомпрессор, и заменить response на request). То есть даже без необходимости установки обновлений на телефонах.



Можно пойти дальше, и ввести двустороннюю компрессию



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

Подробнее..

Как я сделал веб-фреймворк без MVC Pipe Framework

23.02.2021 14:15:47 | Автор: admin

Проработав фулстек разработчиком около 10 лет, я заметил одну странность.
Я ни разу не встретил не MVC веб-фреймворк. Да, периодически встречались вариации, однако общая структура всегда сохранялась:


  • Codeigniter мой первый фреймворк, MVC
  • Kohana MVC
  • Laravel MVC
  • Django создатели слегка подменили термины, назвав контроллер View, а View Template'ом, но суть не изменилась
  • Flask микрофреймворк, по итогу все равно приходящий к MVC паттерну

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


Меня беспокоило то, что за все время существования веб-разработки, MVC является, по сути, монополистом в проектировании приложений. Я не говорю что это плохо,
просто это казалось мне странным.

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


  1. REST (порой GraphQL или другие варианты) бэкенд, выполняющий роль провайдера данных.
  2. Frontend, написаный на каком-либо из фреймворков большой тройки.

Задачи, которые сейчас стоят перед бэкендом (если сильно упростить) это взять данные из базы, преобразовать в JSON (возможно дополнительно преобразовав структуру) и отправить в браузер.

В ходе этих размышлений, мой взгляд упал на ETL паттерн, и в определенный момент я понял, что он идеально подходит для всех задач, которые на данный момент стоят перед бэкендом.
Осознав это, я решил провести эксперимент, и результатом этого эксперимента стал Pipe Framework.


О фреймворке


В Pipe Framework (далее PF) нет понятий модель-представление-контроллер, но я буду использовать их для демонстрации его принципов.


Весь функционал PF строится с помощью "шагов" (далее Step).


Step это самодостаточная и изолированная единица, призванная выполнять только одну функцию, подчиняясь принципу единственной ответственности (single responsibility principle).


Более детально объясню на примере. Представим, у вас есть простая задача создать API ендпоинт для todo приложения.


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


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

То есть, мы можем провести аналогию между MVC (Модель-Представление-Контроллер) и ETL (Извлечение-Преобразование-Загрузка):


Model Extractor / Loader


Controller Transformer


View Loader


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


Как видите, я обозначил View как Loader. Позже станет понятно, почему я так поступил.

Первый роут


Давайте выполним поставленную задачу используя PF.


Первое, на что необходимо обратить внимание, это три типа шагов:


  • Extractor
  • Transformer
  • Loader

Как определиться с тем, какой тип использовать?


  1. Если вам надо извлечь данные из внешнего ресурса: extractor.
  2. Если вам надо передать данные за пределы фреймворка: loader.
  3. Если вам надо внести изменения в данные: transformer.

Именно поэтому я ассоциирую View с Loader'ом в примере выше. Вы можете воспринимать это как загрузку данных в браузер пользователя.

Любой шаг должен наследоваться от класса Step, но в зависимости от назначения реализовывать разные методы:


class ESomething(Step):    def extract(self, store):        ...class TSomething(Step):    def transform(self, store):        ...class LSomething(Step):    def load(self, store):        ...

Как вы можете заметить, названия шагов начинаются с заглавных E, T, L.
В PF вы работаете с экстракторами, трансформерами, и лоадерами, названия которых слишком длинные, если использовать их как в примере:


class ExtractTodoFromDatabase(Extractor):    pass

Именно поэтому, я сокращаю названия типа операции до первой буквы:


class ETodoFromDatabase(Extractor):    pass

E значит экстрактор, T трансформер, и L лоадер.
Однако, это просто договоренность и никаких ограничений со стороны фреймворка нет, так что можете использовать те имена, которые захотите :)


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


  1. Извлекаем данные из базы
  2. Преобразовываем данные в JSON
  3. Отправляем данные в браузер посредством HTTP.

Итак, нам нужен будет 1 экстратор, 1 трансформер, и 1 лоадер.
К счастью, в PF есть набор предопределенных шагов, и они полностью покрывают описаные выше операции. Но, тем не менее, нам все-таки придется создать экстрактор, потому что нужно будет прописать данные доступа к базе данных.


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


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


Для этих целей, в PF предусмотрен @configure декоратор. То есть, вы просто перечисляете настройки, которые хотите добавить в шаг, следующим образом:


DATABASES = {    'default': {        'driver': 'postgres',        'host': 'localhost',        'database': 'todolist',        'user': 'user',        'password': '',        'prefix': ''    }}DB_STEP_CONFIG = {    'connection_config': DATABASES}

и потом передаете как аргумент декоратору, примененному к классу:


@configure(DB_STEP_CONFIG)class EDatabase(EDBReadBase):    pass

Итак, давайте создадим корневую папку проекта:


pipe-sample/


Затем папку src внутри pipe-sample:


pipe-sample/    src/

Все шаги, связанные с базой данных, будут находится в db пакете, давайте создадим и его тоже:


pipe-sample/    src/        db/            __init__.py

Создайте config.py файл с настройками для базы данных:


pipe-sample/src/db/config.py


DATABASES = {    'default': {        'driver': 'postgres',        'host': 'localhost',        'database': 'todolist',        'user': 'user',        'password': '',        'prefix': ''    }}DB_STEP_CONFIG = {    'connection_config': DATABASES}

Затем, extract.py файл для сохранения нашего экстрактора и его концигурации:


pipe-sample/src/db/extract.py


from src.db.config import DB_STEP_CONFIG # наша конфигурация"""PF включает в себя несколько дженериков для базы данных,которые вы можете посмотреть в API документации"""from pipe.generics.db.orator_orm.extract import EDBReadBase@configure(DB_STEP_CONFIG) # применяем конфигурацию к шагу class EDatabase(EDBReadBase):    pass     # нам не надо ничего добавлять внутри класса    # вся логика уже имплементирована внутри EDBReadBase

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

Теперь мы готовы к созданию первого пайпа.


Добавьте app.py в корневую папку проекта. Затем скопируйте туда этот код:


pipe-sample/app.py


from pipe.server import HTTPPipe, appfrom src.db.extract import EDatabasefrom pipe.server.http.load import LJsonResponse from pipe.server.http.transform import TJsonResponseReady@app.route('/todo/') # декоратор сообщает WSGI приложению, что этот пайп обслуживает данный маршрутclass TodoResource(HTTPPipe):     """    мы расширяем HTTPPipe класс, который предоставляет возможность описывать схему пайпа с учетом типа HTTP запроса    """    """    pipe_schema это словарь с саб пайпами для каждого HTTP метода.     'in' и 'out' это направление внутри пайпа, когда пайп обрабатывает запрос,    он сначала проходит через 'in' и затем через 'out' пайпа.    В этом случае, нам ничего не надо обрабатывать перед получением ответа,     поэтому опишем только 'out'.    """    pipe_schema = {         'GET': {            'out': (                # в фреймворке нет каких либо ограничений на порядок шагов                # это может быть ETL, TEL, LLTEETL, как того требует задача                # в этом примере просто так совпало                EDatabase(table_name='todo-items'),                TJsonResponseReady(data_field='todo-items_list'), # при извлечении данных EDatabase всегда кладет результат запроса в поле {TABLE}_item для одного результата и {TABLE}_list для нескольких                LJsonResponse()            )        }    }"""Пайп фреймворк использует Werkzeug в качестве WSGI-сервера, так что аргументы должны быть знакомы тем кто работал, например, с Flask. Выделяется только 'use_inspection'. Inspection - это режим дебаггинга вашего пайпа.Если установить параметр в True до начала воспроизведения шага, фреймворк будет выводить название текущего шага и содержимое стор на этом этапе."""if __name__ == '__main__':    app.run(host='127.0.0.1', port=8080,            use_debugger=True,            use_reloader=True,            use_inspection=True            )

Теперь можно выполнить $ python app.py и перейти на http://localhost:8000/todo/.


Из примера выше довольно сложно понять как выглядит реализация шага, поэтому ниже я приведу пример из исходников:


class EQueryStringData(Step):    """    Generic extractor for data from query string which you can find after ? sign in URL    """    required_fields = {'+{request_field}': valideer.Type(PipeRequest)}    request_field = 'request'    def extract(self, store: frozendict):        request = store.get(self.request_field)        store = store.copy(**request.args)        return store

Стор


На данный момент, стор в PF это инстанс frozendict.
Изменить его нельзя, но можно создать новый инстанс используя frozendict().copy() метод.


Валидация


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


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


Пример


Все, что нам надо сделать это написать dict с необходимыми полями в теле шага (здесь вы найдете больше информации о доступных валидаторах: Valideer).


class PrettyImportantTransformer(Step):    required_fields = {'+some_field': valideer.Type(dict)} # `+` значит обязательное поле

Динамическая валидация


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


class EUser(Step):    pk_field = 'id' # EUser будет обращаться к полю 'id' в сторе    required_fields = {'+{pk_field}': valideer.Type(dict)} # все остальное так же

Пайп фреймворк заменит это поле на значение pk_field автоматически, и затем валидирует его.


Объединение шагов


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


В этом примере я использую оператор | (OR)


    pipe_schema = {        'GET': {            'out': (                # В случае если EDatabase бросает любое исключение                 # выполнится LNotFound, которому в сторе передастся информация об исключении                EDatabase(table_name='todo-items') | LNotFound(),                 TJsonResponseReady(data_field='todo-items_item'),                LJsonResponse()            )        },

Так же есть оператор & (AND)


    pipe_schema = {        'GET': {            'out': (                # В этом случае оба шага должны выполниться успешно, иначе стор без изменений перейдет к следующему шагу                 EDatabase(table_name='todo-items') & SomethingImportantAsWell(),                 TJsonResponseReady(data_field='todo-items_item'),                LJsonResponse()            )        },

Хуки


Чтобы выполнить какие-либо операции до начала выполнения пайпа, можно переопределить метод: before_pipe


class PipeIsAFunnyWord(HTTPPipe):    def before_pipe(self, store): # в аргументы передается initial store. В случае HTTPPipe там будет только объект PipeRequest        pass

Также есть хук after_pipe и я думаю нет смысла объяснять, для чего он нужен.


interrupt это последний из доступных хуков, должен возвращать bool. Вызывается после каждого шага, в качестве аргумента получая текущий стор. В случае, если метод возвращает True, выполнение пайпа заканчивается и он возвращает стор в текущем его состоянии.


Пример использования из исходников фреймворка:


class HTTPPipe(BasePipe):    """Pipe structure for the `server` package."""    def interrupt(self, store) -> bool:        # If some step returned response, we should interrupt `pipe` execution        return issubclass(store.__class__, PipeResponse) or isinstance(store, PipeResponse)

Потенциальные преимущества


Разрабатывая Pipe Framework, я ничего от него не ожидал, однако в ходе работы я смог выделить довольно большое количество преимуществ такого подхода:


  1. Принудительная декомпозиция: разработчик вынужден разделять задачу на атомарные шаги. Это приводит к тому, что сначала надо подумать, а потом делать, что всегда лучше, чем наоборот.
  2. Абстрактность: фреймворк подразумевает написание шагов, которые можно применить в нескольких местах, что позволяет уменьшить количество кода.
  3. Прозрачность: любая, пусть даже и сложная логика, спрятанная в шагах, призвана выполнять понятные для любого человека задачи. Таким образом, гораздо проще объяснить даже нетехническому персоналу о том, что происходит внутри через преобразование данных.
  4. Самотестируемость: даже без написаных юнит тестов, фреймворк подскажет вам что именно и в каком месте сломалось за счет валидации шагов.
  5. Юнит-тестирование осуществляется гораздо проще, нужно только задать начальные данные для шага или пайпа и проверить, что получается на выходе.
  6. Разработка в команде тоже становится более гибкой. Декомпозировав задачу, можно легко распределить различные шаги между разработчиками, что практически невозможно сделать при традиционном подходе.
  7. Постановка задачи сводится к предоставлению начального набора данных и демонстрации необходимого набора данных на выходе.

Фреймворк на данный момент находится в альфа-тестировании, и я рекомендую экспериментировать с ним, предварительно склонировав с Github репозитория. Установка через pip так же доступна


pip install pipe-framework


Планы по развитию:


  1. Django Pipe: специальный тип Pipe, который можно использовать как Django View.
  2. Смена Orator ORM на SQL Alchemy для Database Generics (Orator ORM библиотека с приятным синтаксисом, но слабой поддержкой, парой багов, и недостаточным функционалом в стабильной версии).
  3. Асинхронность.
  4. Улучшеный Inspection Mode.
  5. Pipe Builder специальный веб-дашбоард, в котором можно составлять пайпы посредством визуальных инструментов.
  6. Функциональные шаги на данный момент шаги можно писать только в ООП стиле, в дальнейшем планируется добавить возможность использовать обычные функции

В целом, планируется двигать фреймворк в сторону упрощения, без потери функциональности. Буду рад вопросам и контрибьюшнам.


Хорошего дня!

Подробнее..

Продолжаем знакомство с APIM Gravitee

27.05.2021 20:23:24 | Автор: admin

Всем привет! Меня всё ещё зовут Антон. В предыдущейстатьея провел небольшой обзор APIM Gravitee и в целом систем типа API Management. В этой статье я расскажу,как поднять ознакомительный стенд APIM Gravitee (https://www.gravitee.io), рассмотрим архитектуру системы, содержимое docker compose file, добавим некоторые параметры, запустим APIM Gravitee и сделаем первую API. Статья немного погружает в технические аспекты и может быть полезна администраторам и инженерам, чтобы начать разбираться в системе.

Архитектура

Для ознакомительного стенда будем использовать простейшую архитектуру

Все в докере, в том числе и MongoDB и Elasticsearch. Чтобы сделать полноценную среду для тестирования крайне желательно компоненты MongoDB и Elasticsearch вынести за пределы Docker. Также для простоты манипулирования настройками можно вынести конфигурационные файлы Gateway и Management API: logback.xml и gravitee.yml.

docker-compose.yml

Среду для начальных шагов будем поднимать, используяdocker-compose file, предоставленный разработчиками наgithub. Правда,внесемнесколькокорректив.

docker-compose.yml# Copyright (C) 2015 The Gravitee team (<http://gravitee.io>)## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at##<http://www.apache.org/licenses/LICENSE-2.0>## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.#version: '3.5'networks:frontend:name: frontendstorage:name: storagevolumes:data-elasticsearch:data-mongo:services:mongodb:image: mongo:${MONGODB_VERSION:-3.6}container_name: gio_apim_mongodbrestart: alwaysvolumes:- data-mongo:/data/db- ./logs/apim-mongodb:/var/log/mongodbnetworks:- storageelasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}container_name: gio_apim_elasticsearchrestart: alwaysvolumes:- data-elasticsearch:/usr/share/elasticsearch/dataenvironment:- http.host=0.0.0.0- transport.host=0.0.0.0- xpack.security.enabled=false- xpack.monitoring.enabled=false- cluster.name=elasticsearch- bootstrap.memory_lock=true- discovery.type=single-node- "ES_JAVA_OPTS=-Xms512m -Xmx512m"ulimits:memlock:soft: -1hard: -1nofile: 65536networks:- storagegateway:image: graviteeio/apim-gateway:${APIM_VERSION:-3}container_name: gio_apim_gatewayrestart: alwaysports:- "8082:8082"depends_on:- mongodb- elasticsearchvolumes:- ./logs/apim-gateway:/opt/graviteeio-gateway/logsenvironment:- gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000- gravitee_ratelimit_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000- gravitee_reporters_elasticsearch_endpoints_0=http://elasticsearch:9200networks:- storage- frontendmanagement_api:image: graviteeio/apim-management-api:${APIM_VERSION:-3}container_name: gio_apim_management_apirestart: alwaysports:- "8083:8083"links:- mongodb- elasticsearchdepends_on:- mongodb- elasticsearchvolumes:- ./logs/apim-management-api:/opt/graviteeio-management-api/logsenvironment:- gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000- gravitee_analytics_elasticsearch_endpoints_0=http://elasticsearch:9200networks:- storage- frontendmanagement_ui:image: graviteeio/apim-management-ui:${APIM_VERSION:-3}container_name: gio_apim_management_uirestart: alwaysports:- "8084:8080"depends_on:- management_apienvironment:- MGMT_API_URL=http://localhost:8083/management/organizations/DEFAULT/environments/DEFAULT/volumes:- ./logs/apim-management-ui:/var/log/nginxnetworks:- frontendportal_ui:image: graviteeio/apim-portal-ui:${APIM_VERSION:-3}container_name: gio_apim_portal_uirestart: alwaysports:- "8085:8080"depends_on:- management_apienvironment:- PORTAL_API_URL=http://localhost:8083/portal/environments/DEFAULTvolumes:- ./logs/apim-portal-ui:/var/log/nginxnetworks:- frontend

Первичные системы, на основе которых строится весь остальной сервис:<o:p>

  1. MongoDB - хранение настроек системы, API, Application, групп, пользователей и журнала аудита.

  2. Elasticsearch(Open Distro for Elasticsearch) - хранение логов, метрик, данных мониторинга.

MongoDB

docker-compose.yml:mongodb

mongodb:image: mongo:${MONGODB_VERSION:-3.6}<o:p>container_name: gio_apim_mongodb<o:p>restart: always<o:p>volumes:<o:p>- data-mongo:/data/db<o:p>- ./logs/apim-mongodb:/var/log/mongodb<o:p>networks:<o:p>- storage<o:p>

С MongoDB всё просто поднимается единственный экземпляр версии 3.6, если не указано иное, с volume для логов самой MongoDB и для данных в MongoDB.<o:p>

Elasticsearch

docker-compose.yml:elasticsearch

elasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}<o:p>container_name: gio_apim_elasticsearchrestart: alwaysvolumes:- data-elasticsearch:/usr/share/elasticsearch/dataenvironment:- http.host=0.0.0.0- transport.host=0.0.0.0<o:p>- xpack.security.enabled=false- xpack.monitoring.enabled=false- cluster.name=elasticsearch- bootstrap.memory_lock=true- discovery.type=single-node- "ES_JAVA_OPTS=-Xms512m -Xmx512m"ulimitsmemlock:soft: -1hard: -1nofile: 65536networks:- storage

elasticsearch:

elasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}container_name: gio_apim_elasticsearchrestart: alwaysvolumes:- data-elasticsearch:/usr/share/elasticsearch/data<o:p>environment:- http.host=0.0.0.0- transport.host=0.0.0.0- xpack.security.enabled=false- xpack.monitoring.enabled=false- cluster.name=elasticsearch- bootstrap.memory_lock=true- discovery.type=single-node- "ES_JAVA_OPTS=-Xms512m -Xmx512m"ulimits:memlock:soft: -1hard: -1nofile: 65536networks:- storage

С Elasticsearch также всё просто поднимается единственный экземпляр версии 7.7.0, если не указано иное, с volume для данных в Elasticsearch. Сразу стоит убрать строки xpack.security.enabled=false и xpack.monitoring.enabled=false, так как хоть они и указаны как false, Elasticsearch пытается найти XPack и падает. Исправили ли этот баг в новых версиях не понятно, так что просто убираем их, или комментируем. Также стоит обратить внимание на секцию ulimits, так как она требуется для нормальной работы Elasticsearch в docker.<o:p>

Дальше уже поднимаются компоненты сервиса:

  1. Gateway

  2. Management API

  3. Management UI

  4. Portal UI

Gateway/APIM Gateway

docker-compose.yml:gatewaygateway:image: graviteeio/apim-gateway:${APIM_VERSION:-3}container_name: gio_apim_gatewayrestart: alwaysports:- "8082:8082"depends_on:- mongodb- elasticsearchvolumes:- ./logs/apim-gateway:/opt/graviteeio-gateway/logs      environment:    - gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000      - gravitee_ratelimit_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000- gravitee_reporters_elasticsearch_endpoints_0=http://elasticsearch:9200networks:- storage- frontend<o:p>

С Gateway всё несколько сложнее поднимается единственный экземпляр версии 3, если не указано иное. Если мы посмотрим, что лежит наhub.docker.com, то увидим, что у версий 3 и latest совпадают хеши. Дальше мы видим, что запуск данного сервиса, зависит от того, как будут работать сервисы MongoDB иElasticsearch. Самое интересное, что если Gateway запустился и забрал данные по настроенным API из mongodb, то дальше связь с mongodb и elasticsearch не обязательна. Только в логи будут ошибки сыпаться, но сам сервис будет работать и соединения обрабатывать согласно той версии настроек, которую последний раз закачал в себя Gateway. В секции environment можно указать параметры, которые будут использоваться в работе самого Gateway, для переписывания данных из главного файла настроек: gravitee.yml. Как вариант можно поставить теги, тенанты для разграничения пространств, если используется Open Distro for Elasticsearch вместо ванильного Elasticsearch. Например, так мы можем добавить теги, тенанты и включить подсистему вывода данных о работе шлюза.

environment:- gravitee_tags=service-tag #включаемтег: service-tag- gravitee_tenant=service-space #включаемтенант: service-space- gravitee_services_core_http_enabled=true # включаем сервис выдачи данных по работе Gateway  - gravitee_services_core_http_port=18082 # порт сервиса- gravitee_services_core_http_host=0.0.0.0 # адрес сервиса- gravitee_services_core_http_authentication_type=basic # аутентификация либо нет, либо basic - логин + пароль  - gravitee_services_core_http_authentication_type_users_admin=password #логин: admin,пароль: password  Чтобы к подсистеме мониторинга был доступ из вне, надо ещё открыть порт 18082.ports:- "18082:18082"Management API/APIM APIdocker-compose.yml:management_apimanagement_api:image: graviteeio/apim-management-api:${APIM_VERSION:-3}container_name: gio_apim_management_apirestart: alwaysports:- "8083:8083"links:- mongodb- elasticsearchdepends_on:- mongodb- elasticsearchvolumes:- ./logs/apim-management-api:/opt/graviteeio-management-api/logsenvironment:- gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000  - gravitee_analytics_elasticsearch_endpoints_0=http://elasticsearch:9200 networks:    - storage- frontend

ManagementAPI это ядро всей системы и предоставляет службы для управления и настройкиAPI, логи, аналитики и веб-интерфейсовManagementUIиPortalUI. Зависит от MongoDB и Elasticsearch. Также можно через секцию environment указать параметры, которые будут использоваться в работе самого ядра системы. Дополним наши настройки:

environment:- gravitee_email_enable=true # включаем возможность отправлять письма- gravitee_email_host=smtp.domain.example # указываем сервер через который будем отправлять письма- gravitee_email_port=25 # указываем порт для сервера- gravitee_email_username=domain.example/gravitee #логиндлясервера- gravitee_email_password=password #парольдлялогинаотсервера  - gravitee_email_from=noreply@domain.example # указываем от чьего имени будут письма- gravitee_email_subject="[Gravitee.io] %s" #указываемтемуписьма

Management UI/APIM Console

docker-compose.yml:apim_consolemanagement_ui:image: graviteeio/apim-management-ui:${APIM_VERSION:-3}container_name: gio_apim_management_uirestart: alwaysports:- "8084:8080"depends_on:- management_apienvironment:- MGMT_API_URL=http://localhost:8083/management/organizations/DEFAULT/environments/DEFAULT/      volumes:    - ./logs/apim-management-ui:/var/log/nginxnetworks:- frontend

Management UI предоставляет интерфейс для работы администраторам и разработчикам. Все основные функции можно осуществлять и выполняя запросы непосредственно к REST API. По опыту могу сказать, что в переменной MGMT_API_URL вместоlocalhostнадо указать IP адрес или название сервера, где вы это поднимаете, иначе контейнер не найдет Management API.

Portal UI/APIM Portaldocker-compose.yml:apim_portalportal_ui:image: graviteeio/apim-portal-ui:${APIM_VERSION:-3}container_name: gio_apim_portal_uirestart: alwaysports:- "8085:8080"depends_on:- management_apienvironment:- PORTAL_API_URL=http://localhost:8083/portal/environments/DEFAULTvolumes:- ./logs/apim-portal-ui:/var/log/nginxnetworks:- frontend

Portal UI это портал для менеджеров. Предоставляет доступ к логам, метрикам и документации по опубликованным API. По опыту могу сказать, что в переменной PORTAL_API_URL вместоlocalhostнадо указать IP-адрес или название сервера, где вы это поднимаете, иначе контейнер не найдет Management API.<o:p>

Теперь соберем весь файл вместе.

docker-compose.yml

# Copyright (C) 2015 The Gravitee team (<http://gravitee.io>)## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at##         <http://www.apache.org/licenses/LICENSE-2.0>## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.#version: '3.5'networks:  frontend:    name: frontend  storage:    name: storagevolumes:  data-elasticsearch:  data-mongo:services:  mongodb:    image: mongo:${MONGODB_VERSION:-3.6}    container_name: gio_apim_mongodb    restart: always    volumes:      - data-mongo:/data/db      - ./logs/apim-mongodb:/var/log/mongodb    networks:      - storage  elasticsearch:    image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}    container_name: gio_apim_elasticsearch    restart: always    volumes:      - data-elasticsearch:/usr/share/elasticsearch/data    environment:      - http.host=0.0.0.0      - transport.host=0.0.0.0      - cluster.name=elasticsearch      - bootstrap.memory_lock=true      - discovery.type=single-node      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"    ulimits:      memlock:        soft: -1        hard: -1      nofile: 65536    networks:      - storage  gateway:    image: graviteeio/apim-gateway:${APIM_VERSION:-3}    container_name: gio_apim_gateway    restart: always    ports:      - "8082:8082"      - "18082:18082"    depends_on:      - mongodb      - elasticsearch    volumes:      - ./logs/apim-gateway:/opt/graviteeio-gateway/logs    environment:      - gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000      - gravitee_ratelimit_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000      - gravitee_reporters_elasticsearch_endpoints_0=http://elasticsearch:9200         - gravitee_tags=service-tag # включаем тег: service-tag         - gravitee_tenant=service-space # включаем тенант: service-space         - gravitee_services_core_http_enabled=true # включаем сервис выдачи данных по работе Gateway         - gravitee_services_core_http_port=18082 # порт сервиса         - gravitee_services_core_http_host=0.0.0.0 # адрес сервиса          - gravitee_services_core_http_authentication_type=basic # аутентификация либо нет, либо basic - логин + пароль         - gravitee_services_core_http_authentication_type_users_admin=password # логин: admin, пароль: password    networks:      - storage      - frontend  management_api:    image: graviteeio/apim-management-api:${APIM_VERSION:-3}    container_name: gio_apim_management_api    restart: always    ports:      - "8083:8083"    links:      - mongodb      - elasticsearch    depends_on:      - mongodb      - elasticsearch    volumes:      - ./logs/apim-management-api:/opt/graviteeio-management-api/logs    environment:      - gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000      - gravitee_analytics_elasticsearch_endpoints_0=http://elasticsearch:9200         - gravitee_email_enable=true # включаем возможность отправлять письма         - gravitee_email_host=smtp.domain.example # указываем сервер через который будем отправлять письма         - gravitee_email_port=25 # указываем порт для сервера         - gravitee_email_username=domain.example/gravitee # логин для сервера         - gravitee_email_password=password # пароль для логина от сервера         - gravitee_email_from=noreply@domain.example # указываем от чьего имени будут письма          - gravitee_email_subject="[Gravitee.io] %s" # указываем тему письма    networks:      - storage      - frontend  management_ui:    image: graviteeio/apim-management-ui:${APIM_VERSION:-3}    container_name: gio_apim_management_ui    restart: always    ports:      - "8084:8080"    depends_on:      - management_api    environment:      - MGMT_API_URL=http://localhost:8083/management/organizations/DEFAULT/environments/DEFAULT/    volumes:      - ./logs/apim-management-ui:/var/log/nginx    networks:      - frontend  portal_ui:    image: graviteeio/apim-portal-ui:${APIM_VERSION:-3}    container_name: gio_apim_portal_ui    restart: always    ports:      - "8085:8080"    depends_on:      - management_api    environment:      - PORTAL_API_URL=http://localhost:8083/portal/environments/DEFAULT    volumes:      - ./logs/apim-portal-ui:/var/log/nginx    networks:      - frontend

Запускаем

Итоговый файл закидываем на сервер с примерно следующими характеристиками:

vCPU: 4

RAM: 4 GB

HDD: 50-100 GB

Для работы Elasticsearch, MongoDB и Gravitee Gateway нужно примерно по 0.5 vCPU, больше только лучше. Примерно тоже самое и с оперативной памятью - RAM. Остальные сервисы по остаточному принципу. Для хранения настроек много места не требуется, но учитывайте, что в MongoDB еще хранятся логи аудита системы. На начальном этапе это будет в пределах 100 MB. Остальное место потребуется для хранения логов в Elasticsearch.

docker-compose up -d # если не хотите видеть логиdocker-compose up # если хотите видеть логи и как это все работает

Как только в логах увидите строки:

gio_apim_management_api_dev | 19:57:12.615 [graviteeio-node] INFO  i.g.r.a.s.node.GraviteeApisNode - Gravitee.io - Rest APIs id[5728f320-ba2b-4a39-a8f3-20ba2bda39ac] version[3.5.3] pid[1] build[23#2f1cec123ad1fae2ef96f1242dddc0846592d222] jvm[AdoptOpenJDK/OpenJDK 64-Bit Server VM/11.0.10+9] started in 31512 ms.

Можно переходить по адресу: http://ваш_адрес:8084/.

Нужно учесть, что Elasticsearch может подниматься несколько дольше, поэтому не пугайтесь если увидите такое "приглашение":

Надо просто ещё немного подождать. Если ошибка не ушла, то надо закапываться в логи и смотреть, что там за ошибки. Видим такое приглашение отлично!

Вводим стандартный логин и пароль: admin/admin и мы в системе!

Первичная настройка

Настройки самой системы

Переходим в менюSettings PORTAL Settings

Здесь можно настроить некоторый параметры системы. Например: доступные методы аутентификации наших клиентов: Keyless, API_KEY, Oauth2 или JWT. Подробно их мы рассмотрим скорее всего в третьей статье, а пока оставим как есть. Можно подключить Google Analytics. Время обновления по задачам и нотификациям. Где лежит документация и много ещё чего по мелочи.

Добавление tags и tenant

ПереходимвменюSettings GATEWAY Shardings Tags

Здесь надо добавить теги, по которым у нас будут различаться шлюзы. Нажимаем "+" и добавляем тег и его описание.

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

ПереходимвменюSettings GATEWAY Tenants

То же самое и с настройкой тенантов. Только тут нет кнопки "+", но есть серенькая надпись "New tenant", которую надо нажать для добавления нового тенанта. Естественно, данный тенант должен быть создан в Open Distro for Elasticsearch, и к нему должны быть выданы права.

Добавление пользователей

Переходим вSettings USER MANAGEMENT Users

Здесь можно добавлять пользователей, вот только работать это будет, если у нас настроена рассылка по email. Иначе новым пользователям не придёт рассылка от системы со ссылкой на сброс пароля. Есть ещё один способ добавить пользователя, но это прям хардкод-хардкод!

В файле настроек Management API: gravitee.yml есть такой кусочек настроек:

security:  providers:  # authentication providers    - type: memory      # password encoding/hashing algorithm. One of:      # - bcrypt : passwords are hashed with bcrypt (supports only $2a$ algorithm)      # - none : passwords are not hashed/encrypted      # default value is bcrypt      password-encoding-algo: bcrypt      users:        - user:          username: admin          password: $2a$10$Ihk05VSds5rUSgMdsMVi9OKMIx2yUvMz7y9VP3rJmQeizZLrhLMyq          roles: ORGANIZATION:ADMIN,ENVIRONMENT:ADMIN

Здесьперечисленытипыхранилищдляпользователей: memory, graviteeиldap.Данные из хранилищаmemoryберутся из файла настроек:gravitee.yml. Данные из хранилищаgraviteeхранятся вMongoDB. Для хранения пользовательских паролей, по умолчанию используется тип хеширования BCrypt с алгоритмом $2a$. В представленных настройках мы видим пользователя: admin с хешированным паролем: admin и его роли. Если мы будем добавлять пользователей через UI, то пользователи будут храниться в MongoDB и тип их будет уже gravitee.

Создание групп пользователей

Переходим вSettings USER MANAGEMENT Groups

При нажатии на "+" получаем возможность добавить группу и пользователей в эту группу.

Проверка доступных шлюзов

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

Здесь мы видим настройки шлюза. В частности, Sharding tags и Tenant. Их мы добавили чуть раньше.

Естественно, есть возможность мониторинга состояния шлюза. Данные по мониторингу хранятся в Elasticsearch в отдельном индексе.

Публикация первого API

Для публикации первого API нам сначала потребуется сделать какой-нибудь backend с API.

BackEnd с API, балеринами и Swagger.

Возьмём FastAPI и сделаем простейшее backend с API.

#!/bin/env python3import uvicornfrom fastapi import FastAPIapp = FastAPI()@app.get('/')@app.get('/{name}')def read_root(name: str = None):    """    Hello world    :return: str = Hello world    """    if name:        return {"Hello": name}    return {"Hello": "World"}@app.get("/items/{item_id}")@app.post("/items/{item_id}")@app.put("/items/{item_id}")def gpp_item(item_id: str):    """    Get items    :param item_id: id    :return: dict    """    return {"item_id": item_id}if __name__ == "__main__":    uvicorn.run(app, host="0.0.0.0", port=8000)

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

Теперь запустим его! Можно даже на том же сервере.

python3 main.py

Теперь, если мы зайдем на этот серверhttp://backend_server:8000/,мы увидим приветствие миру! Если подставим своё имя, типа так:http://backend_server:8000/Anton, то приветствовать уже будут вас! Если же хотите познать всю мощьFastAPI, то сразу заходите на адрес:http://backend_server:8000/docsилиhttp://backend_server:8000/redoc. На этих страницах можно найти все методы, с которым работает данное API и также ссылку на swagger файл. Копируем URL до swagger файла.

В прошлый раз мы создавали наш план вручную. Было несколько утомительно. Сегодня мы сделаем все быстрее, через импорт swagger файла!

На главном экране Gravitee нажимаем большой "+", выбираем "IMPORT FROM LINK", вставляем URL и нажимаем "IMPORT".

Получается как-то так

Нажимаем "IMPORT"!

Почти полностью сформированный API! Теперь немного доработаем напильником...

Для начала нажимаем "START THE API" чтобы API запустить.

Переходим в "Plans" и нажимаем "+".

Создаем тестовый план.

Тип аутентификации выбираем Keyless (public) и нажимаем "NEXT".

Ограничения по количеству запросов и путям пропускаем. Нажимаем "NEXT".

Политики нам тоже пока не нужны. Нажимаем "SAVE".

План создан, но пока находиться в стадии "Staging"

Нажимаем на кнопку публикации плана - синее облачко со стрелочкой вверх! Подтверждаем кнопкой "PUBLISH"

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

Нажимаем "deploy your API" и подтверждаем наше желание "OK"

Переходим вAPIs Proxy Entrypoints

Здесь можно указать точки входа для нашего API и URL пути. У нас указан только путь "/fastapi". Можно переключиться в режим "virtual-hosts" и тогда нам будет доступен вариант с указанием конкретных серверов и IP. Это может быть актуально для серверов с несколькими сетевыми интерфейсами.

ВAPIs Proxy GENERAL CORSможнопроизвестинастройкиCross-origin resource sharing.

ВAPIs Proxy GENERAL Deploymentsнадоуказатьвсеsharding tags,которыебудутиспользоватьсяэтимAPI.

ВAPIs Proxy BACKEND SERVICES Endpointsможно указать дополнительные точки API и настроить параметры работы с ними.

Сейчас нас интересуют настройки конкретной Endpoint, поэтому нажимаем на нижнюю шестеренку.

Исправляем "Target" на http://backend_server:8000/, устанавливаем tenant, сохраняем и деплоим!

ВAPIs Proxy Deploymentsнадо указать те sharding tags, которые могут использовать данное API. После этого необходимо вернуться в созданный план и в списке Sharding tags выбрать тег "service-tag".

ВAPIs Designможно указать политики, которые будут отрабатывать при обработке запросов.

ВAPIs Analytics Overviewможно смотреть статистику по работе данного конкретного API.

ВAPIs Analytics Logsможно настроить логи и потом их смотреть.

ВAPIs Auditможно посмотреть, как изменялись настройки API.

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

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

Переходим наhttp://gravitee_host:8082/fastapi/ , и вам покажется приветствие миру:

Также сразу можно заглянуть вAPIs Analytics Overview/Logsдля просмотра статистики обработки запросов.

Заключение

Итак, поздравляю всех, кто дочитал до сюда и остался в живых! Теперь вы знаете, как поднять ознакомительный стенд APIM Gravitee, как его настроить, создать новое API из swagger файла и проверить, что всё работает. Вариантов настройки шлюзов, точек входа и выхода, сертификатов, балансировок нагрузки и записи логов много. В одной статье всего и не расскажешь. Так что в следующей статье я расскажу о более продвинутых настройках системы APIM Gravitee. В Телеграмме есть канал по данной системе:https://t.me/gravitee_ru, вопросы по нюансам настройки можно задавать туда.

Подробнее..

Как стать разработчиком Java и С открываем онлайн-практикум с поддержкой менторов

01.02.2021 16:08:09 | Автор: admin

Какие навыки прокачать на старте, где найти ментора, как получить первый опыт командной работы все эти вопросы знакомы разработчикам-джунам. Изучая Java или C# самостоятельно, можно запутаться в море информации и потратить больше года на первые шаги. Сократить этот путь помогают практикумы, в том числе в IT-компаниях где менторы готовы поделиться знаниями, давно накоплена база знаний и отлажены процессы разработки. Мы в SimbirSoft проводим такие практикумы несколько раз в год. Сейчас мы открыли запись на ближайший запуск 22 февраля. Рассказываем, чему научатся участники и как подать заявку.

В чём помогают практикумы

С помощью онлайн-практикумов разработчики могут комплексно решить несколько задач в обучении:

  • за 1,5-2 месяца систематизировать свои знания, занимаясь в небольших группах;

  • получить от менторов ответы на вопросы в ходе еженедельных теоретических занятий;

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

Как правило, по итогам практикумов у участников есть возможность трудоустройства. Например, мы приглашаем на собеседование в среднем от 20 до 50% выпускников. Исходя из результатов, мы можем предложить начинающему разработчику дальнейшее обучение в компании или посоветовать ему, какие навыки нужно подтянуть самостоятельно.

Как устроены наши практикумы

У нас в SimbirSoft есть разные форматы онлайн-встреч как для обучения, так и для обмена опытом:

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

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

1) ждем начинающих C#-разработчиков из Ульяновска, Самары, Саранска, Димитровграда, Казани и Краснодара;

2) начинающих Java-разработчиков из Казани и Самары.

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

- До 10 февраля принимаем тестовые задания на Весенний интенсив для Frontend-, Web- и мобильных разработчиков.

- До 11 февраля принимаем тестовые задания на онлайн-практикум по QA и тестированию в Краснодаре.

- Подписаться на новости о следующих событиях можно ВКонтакте или в Telegram.

Программа Backend-практикума

1) Трек Java

  • Spring Initializr, (Rest)Controller, Git. Как создать проект, как сделать контроллер, отдающий статику и json, как создать репозиторий и как залить в него изменения.

  • DB, Service, Repository (встроенки), Component, Configuration. Как подключить базу данных, как организовать работу с данными через сервисы и репозитории, что такое бины и компоненты, как с ними работать.

  • Security, Migrations, DB level-up. Как подключить и настроить базовую безопасность, как управлять пользователями, что такое миграции и для чего они нужны, транзакции и каскадные операции с БД.

  • Testing, Patterns, Security level-up. Как писать правильные тесты и работать с тестовыми фреймворками, какие существуют паттерны проектирования и как применять их в проектах, вопросы безопасности.

  • Spring AOP, Tips&Tricks. Что такое АОП и как этим пользоваться, работа с побочными инструментами (swagger, статические анализаторы и др.), как работать с GitHub (пулл-реквесты, projects).

2) Трек C#

  • .NET Core 3.1, Asp.Net Core, Git. Как создать проект и репозиторий, внести и залить изменения.

  • Nuget. Как подключать библиотеки к проекту и управлять зависимостями.

  • Entity Framework Core, DbUp. Как подключить базу данных, организовать работу с данными через сервисы и репозитории, как с ними работать. Что такое миграции и для чего они нужны, транзакции и каскадные операции с БД.

  • Testing, Patterns, xUnit. Как писать правильные тесты и работать с тестовыми фреймворками, какие существуют паттерны проектирования и как применять их в проектах.

  • Security, Docker. Глубже рассмотрим вопросы безопасности и автоматизации развёртывания и управления приложениями.

Владислав, куратор практикумов по Backend-направлению:

Наш практикум это удобное средство входа в профессию Backend-разработчика. Каждый наш ментор имеет большой опыт в реализации множества проектов и готов поделиться этим опытом с вами. Мы поможем вам раскрыть свой потенциал, найти точки роста и научим работать в команде!

Алексей, разработчик C#, ментор трека C#:

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

Подключайтесь к нашему практикуму! Регистрация на TimePad до 18 февраля.

Подробнее..

Читаем EXPLAIN на максималках

02.03.2021 22:15:14 | Автор: admin

Многим, кто работает с MySQL, известно, что команда EXPLAIN используется для оптимизации запросов, получения информации об использованных и возможных индексах. Большинство разработчиков и администраторов СУБД этой информацией и ограничивается. Я же предлагаю изучить команду EXPLAIN максимально подробно.

Логическая архитектура MySQL

Чтобы понять, как работает EXPLAIN, стоит вспомнить логическую архитектуру MySQL.

Её можно разделить на несколько уровней:

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

  2. Уровень сервера MySQL. Его можно разделить на подуровни:

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

    B. Сервер MySQL. Этот подуровень во многих источниках называют мозгами MySQL. К нему относятся такие компоненты, как кеши и буферы, парсер SQL, оптимизатор, а также все встроенные функции (например, функции даты/времени и шифрования).

  3. Уровень подсистем хранения. Подсистемы хранения отвечают за хранение и извлечение данных в MySQL.

Нас интересует второй уровень, точнее подуровень сервер MySQL, ведь именно здесь выполняет свои прямые обязанности оптимизатор запросов. Набор операций, которые оптимизатор выбирает для выполнения эффективного запроса, называется Планом выполнения запроса, также известного как EXPLAIN-план.

Команда EXPLAIN

Выражение EXPLAIN предоставляет информацию о том, как MySQL выполняет запрос. Оно работает с выражениями SELECT, UPDATE, INSERT, DELETE и REPLACE.

Если у вас версия ниже 5.6

До версии 5.6 команда EXPLAIN работала только с выражениями типа SELECT, и, если вам нужен анализ других выражений, то придется переписать запрос в эквивалентный запрос SELECT.

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

Стандартный вывод команды EXPLAIN покажет колонки:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
Если у вас версия ниже 5.6

В этом случае вы не увидите столбцов filtered и partitions. Для их вывода необходимо, после EXPLAIN, добавить ключевые слова EXTENDED или PARTITIONS, но не оба сразу.

Если у вас версия 5.6

В версии 5.6 и выше столбец partitions будет включено по-умолчанию, однако для вывода столбца filtered вам всё еще придется воспользоваться ключевым словом EXTENDED.

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

Для начала выполним простой запрос:

EXPLAIN SELECT 1
id: 1select_type: SIMPLEtable: NULLpartitions: NULLtype: NULLpossible_keys: NULLkey: NULLkey_len: NULLref: NULLrows: NULLfiltered: NULLExtra: No tables used

Столбец ID

Этот столбец можно назвать идентификатором или порядковым номером каждого SELECT- запроса. Все выражения SELECT нумеруются последовательно, их можно отнести к простым (без подзапросов и объединений) и составным. Составные запросы SELECT можно отнести к:

A. Простым подзапросам

EXPLAIN SELECT (SELECT 1 from Orders) from Drivers

id

select_type

table

1

PRIMARY

Drivers

2

SUBQUERY

Orders

B. Подзапросам с производными таблицами, то есть с подзапросом в разделе FROM

EXPLAIN SELECT * FROM (SELECT 1, 2) AS tmp (a, b)

id

select_type

table

1

PRIMARY

<derived2>

2

SUBQUERY

null

Как я уже писал выше, этот запрос создаст временную таблицу и MySQL будет ссылаться на неё по псевдониму tmp. В более сложных запросах этот псевдоним будет указан в столбце ref. В первой строке, в столбце table можно увидеть название таблицы , которое формируется по правилу , где N ID запроса.

C. Подзапросам с объединением UNION

EXPLAIN SELECT id FROM Cars UNION SELECT id FROM Drivers

id

select_type

table

1

PRIMARY

Cars

2

UNION

Drivers

null

UNION RESULT

<union1,2>

Здесь есть несколько отличий от примера c FROM-подзапросом. Во-первых, MySQL помещает результат объединения во временную таблицу, из которой, затем, считывает данные. К тому же эта временная таблица отсутствует в исходной SQL-команде, поэтому в столбце id для неё будет null. Во-вторых, временная таблица, появившаяся в результате объединения, показана последней, а не первой.

Точно по такому же правилу формируется название таблица в столбце table <unionN,M>, где N ID первого запроса, а M второго.

Столбец select_type

Показывает тип запроса SELECT для каждой строки результата EXPLAIN. Если запрос простой, то есть не содержит подзапросов и объединений, то в столбце будет значение SIMPLE. В противном случае, самый внешний запрос помечается как PRIMARY, а остальные следующим образом:

  • SUBQUERY. Запрос SELECT, который содержится в подзапросе, находящимся в разделе SELECT (т.е. не в разделе FROM).

  • DERIVED. Обозначает производную таблицу, то есть этот запрос SELECT является подзапросом в разделе FROM. Выполняется рекурсивно и помещается во временную таблицу, на которую сервер ссылается по имени derived table.

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

  • UNION. Если присутствует объединение UNION, то первый входящий в него запрос считается частью внешнего запроса и помечается как PRIMARY (см. пример выше). Если бы объединение UNION было частью подзапроса в разделе FROM, то его первый запрос SELECT был бы помечен как DERIVED. Второй и последующий запросы помечаются как UNION.

  • UNION RESULT. Показывает результата запроса SELECT, который сервер MySQL применяет для чтения из временной таблицы, которая была создана в результате объединения UNION.

Кроме того, типы SUBQUERY, UNION и DERIVED могут быть помечены как DEPENDENT, то есть результат SELECT зависит от данных, которые встречаются во внешнем запросе SELECT.

Если у вас версия 5.7 и ниже

Поле DEPENDENT DERIVED появилось только в 8 версии MySQL.

Также типы SUBQUERY и UNION могут быть помечены как UNCACHABLE. Это говорит о том, что результат SELECT не может быть закеширован и должен быть пересчитан для каждой строки внешнего запроса. Например, из-за функции RAND().

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

Столбец table

Показывает, к какой таблице относится эта строка. В самом простом случае это таблица (или её псевдоним) из вашей SQL- команды.

При объединении таблиц стоит читать столбец table сверху вниз.

EXPLAIN SELECT Clients.id        FROM Clients        JOIN Orders ON Orders.client_id = Clients.id        JOIN Drivers ON Orders.driver_id = Drivers.id

id

seelect_type

table

1

SIMPLE

Clients

1

SIMPLE

Orders

1

SIMPLE

Drivers

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

Если запрос содержит подзапрос FROM или объединение UNION, то столбец table читать будет не так просто, потому что MySQL будет создавать временные таблицы, на которые станет ссылаться.

О столбце table для подзапроса FROM я уже писал выше. Ссылка derived.N является- опережающей, то есть N ID запроса ниже. А ссылка UNION RESULT (union N,M) является обратной, поскольку встречается после всех строк, которые относятся к объединению UNION.

Попробуем, для примера, прочитать столбец table для следующего странного запроса:

EXPLAIN SELECT id, (SELECT 1 FROM Orders WHERE client_id = t1.id LIMIT 1)       FROM (SELECT id FROM Drivers LIMIT 5) AS t1       UNION       SELECT driver_id, (SELECT @var1 FROM Cars LIMIT 1)       FROM (           SELECT driver_id, (SELECT 1 FROM Clients)           FROM Orders LIMIT 5       ) AS t2

id

select_type

table

1

PRIMARY

<derived3>

3

DERIVED

Drivers

2

DEPENDENT SUBQUERY

Orders

4

UNION

<derived6>

6

DERIVED

Orders

7

SUBQUERY

Clients

5

UNCACHEABLE SUBQUERY

Cars

null

UNION RESULT

<union1,4>

Не так просто разобраться в этом, но, тем не менее, мы попробуем.

  1. Первая строка является опережающей ссылкой на производную таблицу t1, помеченную как <derived3>.

  2. Значение идентификатора строки равно 3, потому что строка относится к третьему по порядку SELECT. Поле select_type имеет значение DERIVED, потому что подзапрос находится в разделе FROM.

  3. Третья строка с ID = 2 идет после строки с бОльшим ID, потому что соответствующий ей подзапрос выполнился позже, что логично, ведь нельзя получить значение t1.id, не выполнив подзапрос с ID = 3. Признак DEPENDENT SUBQUERY означает, что результат зависит от результатов внешнего запроса.

  4. Четвертая строка соответствует второму или последующему запросу объединения, поэтому она помечена признаком UNION. Значение <derived6> означает, что данные будут выбраны из подзапроса FROM и добавятся во временную таблицу для результатов UNION.

  5. Пятая строка - это наш подзапрос FROM, помеченный как t2.

  6. Шестая строка указывает на обычный подзапрос в SELECT. Идентификатор этой строки равен 7, что важно, потому что следующая строка уже имеет ID = 5.

  7. Почему же важно, что седьмая строка имеет меньший ID, чем шестая? Потому что каждая строка, помеченная как DERIVED , открывает вложенную область видимости. Эта область видимости закрывается, когда встречается строка с ID меньшим, чем у DERIVED (в данном случае 5 < 6). Отсюда можно понять, что седьмая строка является частью SELECT, в котором выбираются данные из <derived6>. Признак UNCACHEABLE в колонке select_type добавляется из-за переменной @var1.

  8. Последняя строка UNION RESULT представляет собой этап считывания строк из временной таблицы после объединения UNION.

При чтении с помощью EXPLAIN больших запросов часто приходится читать результат сверху вниз и наоборот. Например, в этом примере имеет смысл начать чтение с последней строки, чтобы понять, что первая строка является частью UNION.

Столбец partitions

Показывает, какой партиции соответствуют данные из запроса. Если вы не используете партиционирование, то значение этой колонки будет null.

Столбец type

Показывает информацию о том, каким образом MySQL выбирает данные из таблицы. Хотя в документации MySQL это поле описывается как The join type, многих такое описание смущает или кажется не до конца понятным. Столбец type принимает одно из следующих значений, отсортированных в порядке скорости извлечения данных:

  • ALL. Обычно речь идет о полном сканировании таблицы, то естьт.е. MySQL будет просматривать строчку за строчкой, если только в запросе нет LIMIT или в колонке extra не указано Distinct/not exists, к чему мы вернемся позже.

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

  • range. Индекс просматривается в заданном диапазоне. Поиск начинается в определенной точке индекса и возвращает значения, пока истинно условие поиска. range может быть использован, когда проиндексированный столбец сравнивается с константой с использованием операторов =, <>, >, >=, <, <=, IS_NULL, <=>, BETWEEN, LIKE или IN.

  • index_subquery. Вы увидите это значение, если в операторе IN есть подзапрос, для которого оптимизатор MySQL смог использовать поиск по индексу.

  • unique_subquery. Похож на index_subquery, но, для подзапроса используется уникальный индекс, такой как Primary key или Unique index.

  • index_merge. Если оптимизатор использовал range-сканирование для нескольких таблиц, он может объединить их результаты. В зависимости от метода слияния, поле extra примет одно из следующих значений: Using intersect пересечение, Using union объединение, Using sort_union объединение сортировки слияния (подробнее читайте здесь)

  • ref_or_null. Этот случай похож на ref, за исключением того, что MySQL будет выполнять второй просмотр для поиска записей, содержащих NULL- значения.

  • fulltext. Использование FULLTEXT-индекса.

  • ref. Поиск по индексу, в результате которого возвращаются все строки, соответствующие единственному заданному значению. Применяется в случаях, если ключ не является уникальным, то есть не Primary key или Unique index , либо используется только крайний левый префикс ключа. ref может быть использован только для операторов = или <=>.

  • eq_ref. Считывается всего одна строка по первичному или уникальному ключу. Работает только с оператором =. Справа от знака = может быть константа или выражение.

  • const. Таблица содержит не более одной совпадающей строки. Если при оптимизации MySQL удалось привести запрос к константе, то столбец type будет равен const. Например, если вы ищете что-то по первичному ключу, то оптимизатор может преобразовать значение в константу и исключить таблицу из соединения JOIN.

  • system. В таблице только одна строка. Частный случай const.

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

Столбец possible_keys

Показывает, какие индексы можно использовать для запроса. Этот столбец не зависит от порядка таблиц, отображаемых EXPLAIN, поскольку список создается на ранних этапах оптимизации. Если в столбце значение NULL, то соответствующих индексов не нашлось.

Столбец keys

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

Столбец key_len

Показывает длину выбранного ключа (индекса) в байтах. Например, если у вас есть primary key id типа int, то, при его использовании, key_len будет равен 4, потому что длина int всегда равна 4 байта. В случае составных ключей key_len будет равен сумме байтов их типов. Если столбец key равен NULL, то значение key_len так же будет NULL.

EXPLAIN SELECT * FROM OrdersWHERE client_id = 1

id

table

possible_keys

key

key_len

1

Orders

Orders_Clients_id_fk

Orders_Clients_id_fk

4

EXPLAIN SELECT * FROM OrdersWHERE client_id = 1 AND driver_id = 2

id

table

possible_keys

key

key_len

1

Orders

Orders_Drivers_id_fk,

Orders_client_id_driver_id

Orders_client_id_driver_id

8

Столбец ref

Показывает, какие столбцы или константы сравниваются с указанным в key индексом. Принимает значения NULL, const или название столбца другой таблицы. Возможно значение func, когда сравнение идет с результатом функции. Чтобы узнать, что это за функция, можно после EXPLAIN выполнить команду SHOW WARNINGS.

EXPLAIN SELECT * FROM Drivers

id

table

ref

1

Drivers

null

EXPLAIN SELECT * FROM DriversWHERE id = 1

id

table

ref

1

Drivers

const

EXPLAIN SELECT * FROM DriversJOIN Orders ON Drivers.id = Orders.driver_id

id

table

ref

1

Orders

null

1

Drivers

Orders.driver_id

Столбец rows

Показывает количество строк, которое, по мнению MySQL, будет прочитано. Это число является приблизительным и может оказаться очень неточным. Оно вычисляется при каждой итерации плана выполнения с вложенными циклами. Часто это значение путают с количеством строк в результирующем наборе, что неверно, потому что столбец rows показывает количество строк, которые нужно будет просмотреть. При вычислении значения не учитываются буферы соединения и кеши (в том числе кеши ОС и оборудования), поэтому реальное число может быть намного меньше предполагаемого.

Столбец filtered

Показывает, какую долю от общего количества числа просмотренных строк вернет движок MySQL. Максимальное значение 100, то есть будет возвращено все 100 % просмотренных строк. Если умножить эту долю на значение в столбце rows, то получится приблизительная оценка количества строк, которые MySQL будет соединять с последующими таблицами. Например, если в строке rows 100 записей, а значение filtered 50,00 (50 %), то это число будет вычислено как 100 x 50 % = 50.

Столбец Extra

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

  • const row not found. Для запроса, вида SELECT FROM table, таблица table оказалась пустая.

  • Deleting all rows. Некоторые движки MySQL, такие как MyISAM, поддерживают методы быстрого удаления всех строк из таблицы. Если механизм удаления поддерживает эту оптимизацию, то значение Deleting all rows будет значением в столбце Extra.

  • Distinct. Если в запросе присутствует DISTINCT, то MySQL прекращает поиск, после нахождения первой подходящей строки.

  • FirstMatch (table_name). Если в системной переменной optimizer_switch есть значение firstmatch=on, то MySQL может использовать для подзапросов стратегию FirstMatch, которая позволяет избежать поиска дублей, как только будет найдено первое совпадение. Представим, что один и тот же водитель возил клиента с id = 10 больше, чем один раз, тогда для этого запроса:

    EXPLAIN
    SELECT id FROM Drivers
    WHERE Drivers.id IN (SELECT driver_id FROM Orders WHERE client_id = 10)

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

id

table

extra

1

Orders

Using index;

2

Drivers

Using index; FirstMatch(Orders)

  • Full scan on NULL key. Обычно такая запись идет после Using where как запасная стратегия, если оптимизатор не смог использовать метод доступа по индексу.

  • Impossible HAVING. Условие HAVING всегда ложно.

  • Impossible WHERE. Условие WHERE всегда ложно.

  • Impossible WHERE noticed after reading const tables. MySQL просмотрел все const (и system) таблицы и заметил, что условие WHERE всегда ложно.

  • LooseScan(m..n). Стратегия сканирования индекса при группировке GROUP BY. Подробнее читайте здесь.

  • No matching min/max row. Ни одна строка не удовлетворяет условию запроса, в котором используются агрегатные функции MIN/MAX.

  • No matching rows after partition pruning. По смыслу похож на Impossible WHERE для выражения SELECT, но для запросов DELETE или UPDATE.

  • No tables used. В запросе нет FROM или есть FROM DUAL.

  • Not exists. Сервер MySQL применил алгоритм раннего завершения. То есть применена оптимизация, чтобы избежать чтения более, чем одной строки из индекса. Это эквивалентно подзапросу NOT EXISTS(), прекращение обработки текущей строки, как только найдено соответствие.

  • Plan isnt ready yet. Такое значение может появиться при использовании команды EXPLAIN FOR CONNECTION, если оптимизатор еще не завершил построение плана.

  • Range check for each record (!!!). Оптимизатор не нашел подходящего индекса, но обнаружил, что некоторые индексы могут быть использованы после того, как будут известны значения столбцов из предыдущих таблиц. В этом случае оптимизатор будет пытаться применить стратегию поиска по индексу range или index_merge.

  • Recursive.Такое значение появляется для рекурсивных (WITH) частей запроса в столбце extra.

  • Scanned N databases. Сколько таблиц INFORMATION_SCHEMA было прочитано. Значение N может быть 0, 1 или all.

  • Select tables optimized away (!!!). Встречается в запросах, содержащих агрегатные функции (но без GROUP BY). Оптимизатор смог молниеносно получить нужные данные, не обращаясь к таблице, например, из внутренних счетчиков или индекса. Это лучшее значение поля extra, которое вы можете встретить при использовании агрегатных функций.

  • Skip_open_table, Open_frm_only, Open_full_table. Для каждой таблицы, которую вы создаете, MySQL создает на диске файл .frm, описывающий структуру таблицы. Для подсистемы хранения MyISAM так же создаются файлы .MYD с данными и .MYI с индексами. В запросах к INFORMATION_SCHEMA Skip_open_table означает, что ни один из этих файлов открывать не нужно, вся информация уже доступна в словаре (data dictionary). Для Open_frm_only потребуется открыть файлы .frm. Open_full_table указывает на необходимость открытия файлов .frm, .MYD и .MYI.

  • Start temporary, End temporary. Еще одна стратегия предотвращения поиска дубликатов, которая называется DuplicateWeedout. При этом создаётся временная таблица, что будет отображено как Start temporary. Когда значения из таблицы будут прочитаны, это будет отмечено в колонке extra как End temporary. Неплохое описание читайте здесь.

  • unique row not found (!!!). Для запросов SELECT FROM table ни одна строка не удовлетворяет поиску по PRIMARY или UNIQUE KEY.

  • Using filesort (!!!). Сервер MySQL вынужден прибегнуть к внешней сортировке, вместо той, что задаётся индексом. Сортировка может быть произведена как в памяти, так и на диске, о чем EXPLAIN никак не сообщает.

  • Using index (!!!). MySQL использует покрывающий индекс, чтобы избежать доступа к таблице.

  • Using index condition (!!!). Информация считывается из индекса, чтобы затем можно было определить, следует ли читать строку целиком. Иногда стоит поменять местами условия в WHERE или прокинуть дополнительные данные в запрос с вашего бэкенда, чтобы Using index condition превратилось в Using index.

  • Using index for group-by (!!!). Похож на Using index, но для группировки GROUP BY или DISTINCT. Обращения к таблице не требуется, все данные есть в индексе.

  • Using join buffer (Block nested loop | Batched Key Access | hash join). Таблицы, получившиеся в результате объединения (JOIN), записываются в буфер соединения (Join Buffer). Затем новые таблицы соединяются уже со строками из этого буфера. Алгоритм соединения (Block nested loop | Batched Key Access | hash join) будет указан в колонке extra.

  • Using sort_union, Using union, Using intersect. Показывает алгоритм слияния, о котором я писал выше для index_merge столбца type.

  • Using temporary (!!!). Будет создана временная таблица для сортировки или группировки результатов запроса.

  • Using where (!!!). Сервер будет вынужден дополнительно фильтровать те строки, которые уже были отфильтрованы подсистемой хранения. Если вы встретили Using where в столбце extra, то стоит переписать запрос, используя другие возможные индексы.

  • Zero limit. В запросе присутствует LIMIT 0.

Команда SHOW WARNINGS

Выражение EXPLAIN предоставляет расширенную информацию, если сразу после его завершения выполнить команду SHOW WARNINGS. Тогда вы получите реконструированный запрос.

Если у вас MySQL 5.6 и ниже

SHOW WARNINGS работает только после EXPLAIN EXTENDED.

EXPLAIN SELECT              Drivers.id,              Drivers.id IN (SELECT Orders.driver_id FROM Orders)FROM Drivers;SHOW WARNINGS;
/* select#1 */ select `explain`.`Drivers`.`id` AS `id`,<in_optimizer>(`explain`.`Drivers`.`id`,<exists>(<index_lookup>(<cache>(`explain`.`Drivers`.`id`) in Orders on Orders_Drivers_id_fk))) AS `Drivers.id IN (SELECT Orders.driver_id FROM Orders)` from `explain`.`Drivers`

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

SHOW WARNINGS содержит специальные маркеры, которые не являются допустимым SQL -выражением. Вот их список:

  • <auto_key>. Автоматически сгенерированный ключ для временной таблицы.

  • <cache> (expr). Выражение expr выполняется один раз, значение сохраняется в памяти. Если таких значений несколько, то вместо <cache> будет создана временная таблица с маркером <temporary table>.

  • <exists> (query fragment). Предикат подзапроса был преобразован в EXISTS -предикат, а сам подзапрос был преобразован таким образом, чтобы его можно было использовать совместно с EXISTS.

  • <in_optimizer> (query fragment). Внутренний объект оптимизатора, не обращаем внимания.

  • <index_lookup> (query fragment). Этот фрагмент запроса обрабатывается с помощью поиска по индексу.

  • <if> (condition, expr1, expr2). Если условие истинно, то выполняем expr1, иначе expr2.

  • <is_not_null_test> (expr). Тест для оценки того, что выражение expr не преобразуется в null.

  • <materialize> (query fragment). Подзапрос был материализован.

  • materialized-subquery.col_name. Ссылка на столбец col_name была материализована.

  • <primary_index_lookup> (query fragment). Фрагмент запроса обрабатывается с помощью индекса по первичному ключу.

  • <ref_null_helper> (expr). Внутренний объект оптимизатора, не обращаем внимания.

  • /* select # N */. SELECT относится к строке с номером id = N из результата EXPLAIN.

  • <temporary table>. Представляет собой временную таблицу, которая используется для кеширования результатов.

Читаем EXPLAIN

Учитывая всё вышесказанное, пора дать ответ на вопрос - так как же стоит правильно читать EXPLAIN?

Начинаем читать каждую строчку сверху вниз. Смотрим на колонку type. Если индекс не используется плохо (за исключением случаев, когда таблица очень маленькая или присутствует ключевое слово LIMIT). В этом случае оптимизатор намеренно предпочтет просканировать таблицу. Чем ближе значение столбца type к NULL (см. пункт о столбце type), тем лучше.

Далее стоит посмотреть на колонки rows и filtered. Чем меньше значение rows и чем больше значение filtered,- тем лучше. Однако, если значение rows слишком велико и filtered стремится к 100 % - это очень плохо.

Смотрим, какой индекс был выбран из колонки key , и сравниваем со всеми ключами из possible_keys. Если индекс не оптимальный (большая селективность), то стоит подумать, как изменить запрос или пробросить дополнительные данные в условие выборки, чтобы использовать наилучший индекс из possible_keys.

Наконец, читаем колонку Extra. Если там значение, отмеченное выше как (!!!), то, как минимум, обращаем на это вниманием. Как максимум, пытаемся разобраться, почему так. В этом нам может хорошо помочь SHOW WARNINGS.

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

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

При чтении всегда помним о том, что:

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

  • EXPLAIN не работает с хранимыми процедурами.

  • EXPLAIN не расскажет об оптимизациях, которые MySQL производит уже на этапе выполнения запроса.

  • Большинство статистической информации всего лишь оценка, иногда очень неточная.

  • EXPLAIN не делает различий между некоторыми операциями, называя их одинаково. Например, filesort может означать сортировку в памяти и на диске, а временная таблица, которая создается на диске или в памяти, будет помечена как Using temporary.

  • В разных версиях MySQL EXPLAIN может выдавать совершенно разные результаты, потому что оптимизатор постоянно улучшается разработчиками, поэтому не забываем обновляться.

EXPLAIN TREE FORMAT и EXPLAIN ANALYZE

Если вы счастливый обладатель восьмой версии MySQL, то в вашем арсенале появляются очень полезные команды, которые позволяют читать план выполнения и информацию о стоимости запроса без использования SHOW WARNINGS.

С версии 8.0.16 можно вывести план выполнения в виде дерева, используя выражение FORMAT=TREE:

EXPLAIN FORMAT = TREE select * from Drivers   join Orders on Drivers.id = Orders.driver_id   join Clients on Orders.client_id = Clients.id
-> Nested loop inner join  (cost=1.05 rows=1)   -> Nested loop inner join  (cost=0.70 rows=1)       -> Index scan on Drivers using Drivers_car_id_index  (cost=0.35 rows=1)       -> Index lookup on Orders using Orders_Drivers_id_fk (driver_id=Drivers.id)  (cost=0.35 rows=1)   -> Single-row index lookup on Clients using PRIMARY (id=Orders.client_id)  (cost=0.35 rows=1)

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

Еще более подробную информацию можно получить, заменив FORMAT = TREE на выражение ANALYZE, которое предоставляет MySQL с версии 8.0.18.

EXPLAIN ANALYZE select * from Drivers   join Orders on Drivers.id = Orders.driver_id   join Clients on Orders.client_id = Clients.id
-> Nested loop inner join  (cost=1.05 rows=1) (actual time=0.152..0.152 rows=0 loops=1)   -> Nested loop inner join  (cost=0.70 rows=1) (actual time=0.123..0.123 rows=0 loops=1)       -> Index scan on Drivers using Drivers_car_id_index  (cost=0.35 rows=1) (actual time=0.094..0.094 rows=0 loops=1)       -> Index lookup on Orders using Orders_Drivers_id_fk (driver_id=Drivers.id)  (cost=0.35 rows=1) (never executed)   -> Single-row index lookup on Clients using PRIMARY (id=Orders.client_id)  (cost=0.35 rows=1) (never executed)

В дополнение к стоимости и количеству строк можно увидеть фактическое время получения первой строки и фактическое время получения всех строк, которые выводятся в формате actual time={время получения первой строки}..{время получения всех строк}. Также теперь появилось еще одно значение rows, которое указывает на фактическое количество прочитанных строк. Значение loops это количество циклов, которые будут выполнены для соединения с внешней таблицей (выше по дереву). Если не потребовалось ни одной итерации цикла, то вместо расширенной информации вы увидите значение (never executed).

Как видите, обновлять MySQL полезно не только с точки зрения производительности и улучшения оптимизатора, но и для получения новых инструментов для профилирования запросов.

Заключение

Команда EXPLAIN станет отличным оружием в вашем арсенале при работе с БД. Зная несложные правила, вы можете быстро оптимизировать ваши запросы, узнавать различную статистическую информацию, пусть и приближенную. Расширенный вывод подскажет, что было закешировано и где создаются временные таблицы.

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

Пытайтесь, даже просто так, читать различные виды запросов, содержащие FROM, UNION и JOIN , и сами не заметите, как станете мастером оптимизации.

Литература и источники

  1. High Performance MySQL (by Baron Schwartz, Peter Zaitsev, Vadim Tkachenko)

  2. https://dev.mysql.com/

  3. https://stackoverflow.com/

  4. http://highload.guide/

  5. https://taogenjia.com/2020/06/08/mysql-explain/

  6. https://www.eversql.com/mysql-explain-example-explaining-mysql-explain-using-stackoverflow-data/

  7. https://dba.stackexchange.com/

  8. https://mariadb.com/

  9. https://andreyex.ru/bazy-dannyx/baza-dannyx-mysql/explain-analyze-v-mysql/

  10. https://programming.vip/docs/explain-analyze-in-mysql-8.0.html

  11. А также много страниц из google.com

Подробнее..

Эволюция социального фида в iFunny мобильном приложении с UGC-контентом

10.03.2021 20:17:40 | Автор: admin

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

Ежедневно наши пользователи загружают десятки тысяч единиц контента. На таком объёме данных приходится применять ухищрения, чтобы сохранить приемлемое время ответа. Под катом расскажу, что именно мы делали, а в конце статьи поделюсь кодом на GitHub для ознакомления.

Принципиально можно выделить две схемы формирования фида:

  1. Push on change.

  2. Pull on demand.

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

1. Push on change. Для каждого пользователя создаем отдельный денормализованный фид. При добавлении мема вставляем его в фиды пользователей, подписанных на автора.

Плюсы:

  • очень быстро читать фид из базы.

Минусы:

  • долгое добавление и удаление: время линейно зависит от количества подписок на автора.

Формирование фида по схеме push on changeФормирование фида по схеме push on change

2. Pull on demand. Формируем фид на лету: отправляем по запросу на каждого пользователя, на которого подписаны.

Плюсы:

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

Минусы:

  • формирование фида занимает линейно зависимое от количества подписок время;

  • при формировании необходимо обрабатывать избыточное количество данных: чтобы отдать фид из 20 элементов, для каждой подписки приходится выбирать по 20, сортировать и склеивать, а остальное просто выкидывать.

Формирование фида по схеме pull on demandФормирование фида по схеме pull on demand

Первая итерация: push on change на Cassandra

Мы выбрали механизм push on change, а в качестве БД для хранения денормализованных представлений использовали Cassandra. Она использует подход LSM, что позволяет писать с достаточно внушительной скоростью за счет того, что данные просто последовательно пишутся в память (MemTable), а затем сохраняются на диск и сливаются в многоуровневые отсортированные файлы (SSTables).

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

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

  1. Требования сторов удалить определённый контент. Например, нарушение копирайта, 18+ или иногда просто лягушонок Пепе.

  2. Удаление самими пользователями.

  3. Отписка пользователей друг от друга. Иногда они отписывались сразу от всех.

  4. Трим старых данных. Нужно было чистить ооочень длинные фиды, потому что пользователи никогда не прочитают до конца.

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

Распределение данных по уровням в CassandraРаспределение данных по уровням в Cassandra

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

Cassandra написана на Java, поэтому она могла непредсказуемо и надолго уходить в сбор мусора, особенно когда начинала мержить глубокоуровневые SSTableы. К тому времени в кластере было уже порядка 25 нод, а суммарное количество данных с учетом репликации перевалило за 20 ТБ. Это послужило сигналом к началу второй итерации.

Вторая итерация: pull on demand на Redis с формированием фида на стороне приложения

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

  • Тюнинг GC Cassandra.

  • Другие стратегии Cassandra Compaction, рассматривали вариант написания своей стратегии.

  • Другие структуры хранения и БД (например, блобы в PostgreSQL).

Но ничего хорошего не вышло, и решили перейти к схеме pull on demand.

Поставили кластер из Redis, разложили данные в сортированные множества (sorted sets) и начали строить фид прямо в момент запроса слиянием на стороне приложения в отдельном сервисе. Это значительно ускорило появление новых мемов в фиде: больше не надо итерировать по подпискам, вообще не нужны асинхронные задачи.

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

Третья и текущая итерация: pull on demand на Redis с формированием фида на стороне базы

У предыдущего решения была пара недостатков:

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

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

Redis 4 позволил писать свои модули. Мы решили, что это хороший способ оптимизировать работу фидов. Был написан модуль на C, который на стороне БД получал нужные данные, формировал из них фид, выполняя сортировку на структуре MaxHeap. Команду назвали ZREVMERGE: как понятно из названия, выполняет слияние нескольких сортированных множеств.

Формирование фида на основе модуля с ZREVMERGEФормирование фида на основе модуля с ZREVMERGE

Появилась возможность распараллелить часть работы. ZREVMERGE добавляет задания в свой пул тредов. Доступ модуля к множествам по-прежнему осуществляется в однопоточном режиме из-за ограничений Redis, но сортировка и слияние не требует блокировки.

В итоге получилось более чем в два раза ускорить формирование фида: раньше медиана была около 20 мс, с переносом работы в модуль стала менее 10 мс. Получилось бы лучше, если бы не шардирование данных в кластере: приходится всё же отправлять несколько запросов, по одному на каждый шард и доделывать часть работы в приложении. Получилось увеличить лимит на подписки пользователям с 400 до 5000.

Но были и сложности. Так как модули пишутся на C, а я последний раз писал на нем в университете, столкнулся с парой утечек памяти. Также был найден баг в работе Redis с модулями, но его быстро пофиксили после репорта.

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

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

Вместо заключения

Конечно, пока не получилось решить все проблемы на 100%. Всегда есть возможности улучшить что-то, но из начальной точки проделан достаточно большой путь. Хоть писать на C в прод и было страшно, но свои результаты это принесло. Буду рад почитать, как фиды работают у вас. Удачного итерирования!

Подробнее..

Как мы просто сократили объем входящего в дата-центр трафика на 70

03.02.2021 22:16:57 | Автор: admin

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

Единственное, о чем мы пожалели что не применили это решение раньше.

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

Два года назад, когда мы переходили с RedShift на ClickHouse, количество собираемых аналитических событий (приложение открылось, приложение запросило ленту контента, пользователь просмотрел контент, пользователь поставил смайл (лайк) и так далее) составляло около 5 млрд в сутки. Сегодня это число приближается к 14 млрд.

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

Но перед тем, как агрегировать, сохранить и обработать столько данных, их надо сначала принять и с этим есть свои проблемы. Часть описана в статье о переходе на ClickHouse (ссылка на неё была выше), но есть и другие.

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

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

Но ближе к лету непростого 2020 года ей нашлось применение.

Протокол HTTP, помимо сжатия ответов (о котором знают все, кто когда-либо оптимизировал скорость работы сайтов), позволяет использовать аналогичный механизм для сжатия тела POST/PUT-запросов, объявив об этом в заголовке Content-Encoding. В качестве входящего обратного прокси и балансировщика нагрузки мы используем nginx, проверенное и надёжное решение. Мы настолько были уверены, что он сумеет ко всему прочему ещё и на лету распаковать тело POST-запроса, что поначалу даже не поверили, что из коробки он этого не умеет. И нет, готовых модулей для этого тоже нет, надо было как-то решать проблему самостоятельно или использовать скрипт на Lua. Идея с Lua нам особенно не понравилась, зато это знание развязало руки в части выбора алгоритма компрессии.

Дело в том, что давно стандартизированные алгоритмы сжатия типа gzip, deflate или LZW были изобретены в 70-х годах XX века, когда каналы связи и носители были узким горлышком, и коэффициент сжатия был важнее, чем потраченное на сжатие время. Сегодня же в кармане каждого из нас лежит универсальный микрокомпьютер первой четверти XXI века, оборудованный подчас четырёх- и более ядерным процессором, способный на куда большее, а значит алгоритм можно выбрать более современный.

Выбор алгоритма

Требования к алгоритму были простыми:

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

  2. Небольшое потребление процессорной мощности. Не хотим, чтобы телефоны грелись в руках пользователей.

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

  4. Permissive лицензия.

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

В итоге остановились на алгоритме Zstandard, по следующим причинам:

  • Высокая скорость сжатия (на порядок больше, чем у zlib), заточенность на небольшие объёмы данных.

  • Хороший коэффициент сжатия при щадящем уровне потребления CPU.

  • За алгоритмом стоит Facebook, разрабатывавший его для себя.

  • Открытый исходный код, двойная лицензия GPLv2/BSD.

Когда мы увидели первым же в списке поддерживаемых языков JNI, интерфейс вызова нативного кода для JVM, доступный из Kotlin мы поняли, что это судьба. Ведь Kotlin является у нас основным языком разработки как на Android, так и бэкенде. Обёртка для Swift (наш основной язык разработки на iOS) завершила процесс выбора.

Решение на бэкенде

На стороне бэкенда задача была тривиальная: увидев заголовок Content-encoding: zstd, сервис должен получить поток, содержащий сжатое тело запроса, отправить его в декомпрессор zstd, и получить в ответ поток с распакованными данными. То есть буквально (используя JAX-RS container):

// Обёртка над Zstd JNIimport org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream;// ...if (  containerRequestContext    .getHeaders()    .getFirst("Content-Encoding")    .equals("zstd")) {  containerRequestContext    .setEntityStream(ZstdCompressorInputStream(      containerRequestContext.getEntityStream()    ))}

Решение на iOS

Решили сначала попробовать сжатие аналитических событий на iOS. Команда разработки была свободнее, ну и клиентов на iOS у нас несколько меньше. На всякий случай закрыли этот функционал при помощи feature toggle с возможностью плавной раскатки.

import Foundationimport ZSTDfinal class ZSTDRequestSerializer {    private let compressionLevel: Int32    init(compressionLevel: Int32) {        self.compressionLevel = compressionLevel    }    func requestBySerializing(request: URLRequest, parameters: [String: Any]?) throws -> URLRequest? {        guard let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {            return nil        }        // ...        mutableRequest.addValue("zstd", forHTTPHeaderField: "Content-Encoding")        if let parameters = parameters {            let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: [])            let processor = ZSTDProcessor(useContext: true)            let compressedData = try processor.compressBuffer(jsonData, compressionLevel: compressionLevel)            mutableRequest.httpBody = compressedData        }        return mutableRequest as URLRequest    }}

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

Впрочем, и снижение объёма трафика было не сильно заметно. Дождавшись, пока новая версия клиента раскатится пошире, мы врубили сжатие на 100% аудитории.

Результат нас, мягко говоря, удовлетворил:

График падения трафика на iOSГрафик падения трафика на iOS

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

То есть мы на четверть сократили весь объём.

Решение на Android

Воодушевлённые, мы запилили сжатие для второй платформы.

// Тут перехватываем отправку события через interceptor и подменяем оригинальный body на сжатый если это запрос к eventsoverride fun intercept(chain: Interceptor.Chain): Response {   val originalRequest = chain.request()   return if (originalRequest.url.toString()               .endsWith("/events")) {      val compressed = originalRequest.newBuilder()            .header("Content-Encoding", "zstd")            .method(originalRequest.method, zstd(originalRequest.body))            .build()      chain.proceed(compressed)   } else {      chain.proceed(chain.request())   }}// Метод сжатия, берет requestBody и возвращает сжатыйprivate fun zstd(requestBody: RequestBody?): RequestBody {   return object : RequestBody() {      override fun contentType(): MediaType? = requestBody?.contentType()      override fun contentLength(): Long = -1 //We don't know the compressed length in advance!      override fun writeTo(sink: BufferedSink) {         val buffer = Buffer()         requestBody?.writeTo(buffer)         sink.write(Zstd.compress(buffer.readByteArray(), compressLevel))      }   }}

И тут нас ждал шок:

График падения на AndroidГрафик падения на Android

Так как доля Android среди нашей аудитории больше, чем iOS, падение составило ещё 45%. Итого, если считать от исходного уровня, мы выиграли суммарно 70% от, напомню, всего входящего трафика в ДЦ.

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

В этот момент мы только могли пожалеть, что не нашли время на внедрение такой рационализации раньше.

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

Два слова, что ещё можно улучшить

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

При этом коэффициент сжатия увеличивается от 10-15% на текстах до 50% на однообразных наборах строк, как у нас. А скорость сжатия даже несколько увеличивается при размере словаря порядка 16 килобайт. Это, конечно, уже не приведёт к такому впечатляющему результату, но всё равно будет приятно и полезно.

Подробнее..

5 причин, которые заставят тебя использовать Kibana

19.06.2021 02:15:45 | Автор: admin

Во многих компаниях для сбора и анализа логов используют такой инструмент как Kibana. Но существует проблема которая выражается в том, что этим инструментом редко или почти не пользуются. Почему же так происходит? Дело в том, что человек привык анализировать логи непосредственно на инстансе. Читать логи так сказать из первоисточника. Безусловно это самый лучший способ. И мало кто не любит менять свои привычки. Потому что это своего рода выход из зоны комфорта и к этому не все и не всегда готовы.

Кейсы чтения логов

Но бывают ситуации, когда нет возможности зайти непосредственно на инстанс. Например необходимо проанализировать инцидент случившийся на продакшене и у нас нет доступа к этому окружению по известным всем причинам. Другая ситуация, если сервис работает на операционной системе Windows, а доступ нужен для трех и более сотрудников одновременно. Как мы все хорошо знаем у компании Windows есть политика одновременной работы не более двух человек при входе по RDP (Remote Desktop Protocol). Для того чтобы получить одновременный доступ по RDP большему количеству сотрудников, необходимо купить лицензию, а на это готова пойти далеко не каждая компания.

Стек ELK

Таким образом мы возвращаемся к нашему замечательному инструменту Kibana. Kibana это часть стека ELK, в который помимо неё входят Elasticsearch и Logstash. Kibana используется не только для визуализации данных в различных форматах, но также и для быстрого поиска и анализа логов. И сегодня речь пойдет о том, как комфортно перейти на этот инструмент и какие у него есть скрытые возможности для этого.

Способы и лайфхаки

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

Так же можно составлять сложные поисковые запросы. Существует специальный язык запросов, называемый KQL (Kibana Query Language). С помощью этого языка можно составлять многоуровневые запросы, которые помогают отфильтровывать нужную информацию. Например можно выбрать тестовое окружение и задать конкретное его имя. Если необходимо найти какое-нибудь словосочетание, то нам на помощь приходят двойные кавычки. При заключении двух и более слов в двойные кавычки происходит поиск всей фразы целиком.

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

Эти логи будут внизу. И несколько строчек которые были после нашей ошибки. Эти строчки будут выше нашей ошибки. В Kibana логи читаются снизу вверх в отличии от логов на инстансе, где логи идут сверху вниз. По умолчанию загружаются 5 строчек до и 5 после, но это значение можно изменить и затем нажать кнопку Загрузка. После чего произойдет загрузка указанного количества строк лога выше нашей ошибки. Тоже самое можно сделать и с предыдущими логами.

Дефолтное значение 5 можно изменить в настройках системы перейдя Stack Management > Advanced Settings

Заключение

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

Подробнее..

Как оптимизировать повседневные backend-задачи три видео с митапа по Java

24.05.2021 14:14:07 | Автор: admin

20 мая прошел наш седьмой митап для Java-разработчиков ЮMoney Jam. Смотрите видео от наших докладчиков, которые делятся кейсами:

  • Как добавлять в чистовой код на Java тестовое поведение и спать спокойно.

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

  • Как не попасть в Jar Hell.

Владимир Плизга, backend-разработчик ЦФТ. Инъекция тестовых поведений: как выйти сухим изводы?

  • Ситуации, требующие правок кода недля production.

  • Что выбрать: штатные средства, аспектно-ориентированный подход или всё вместе.

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

Григорий Скобелев, программист отдела разработки серверных решений ЮMoney. Зашардируем это!

  • Что делать, когда кластерБД трещит отнагрузки, икак грамотно масштабировать данные.

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

  • Как обеспечить отказоустойчивость внутри кластераБД спомощью шардирования данных нанашем примере.

Вита Комарова, старший программист отдела разработки серверных решений ЮMoney. Как непопасть вJar Hell

  • Что такое Jar Hell икчему онможет привести.

  • Как мыборемся сJar Hell впроектах ЮMoney.

  • Инструменты, которые помогают избежать Jar Hell.

Задавайте вопросы по докладам в комментариях, и наши эксперты вам ответят. А чтобы не пропустить следующие митапы, подпишитесь на наш Telegram-канал.

Подробнее..

Стажировка в Авито глазами стажёра

14.04.2021 12:16:12 | Автор: admin

С июня 2020 года я работаю бэкенд-стажёром в Авито. Когда я искал стажировку, мне хотелось поподробнее узнать, что на самом деле происходит внутри каждой компании и какой у неё подход к стажёрам. Но обычно информация ограничивалась либо краткими текстами налендингах, либо историями успехов.

Но ведь самое интересное это процесс! Поэтому я решил рассказать о своей стажировке вАвито: как она проходит, какие ожидания от стажёра, какие задачи нужно решать. В статье я дам несколько основанных на моём опыте советов для тех, кто впервые собирается пойти стажироваться.

Поиск стажировки и собеседования

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

Из этого вытекают мои основные пожелания к стажировке:

  1. Возможность участвовать в процессах команды, а не быть в стороне.

  2. Руководство наставника, который может помочь, подсказать и направить в нужную сторону.

  3. Наличие code review. Знать инструмент это одно, а грамотно пользоваться им другое. Хочется, чтобы старшие коллеги ревьювили код.

  4. Упор на развитие стажёра, а не просто на полную эксплуатацию.

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

Сами этапы собеседования достаточно стандартные. Сначала нужно сделать тестовое задание. Если оно выполнено хорошо, дальше следует техническое собеседование, приуспешном прохождении которого можно попасть на последний этап собеседование стимлидом и HR. У меня техническое собеседование было без больших сюрпризов: немного лирики про структуры данных и алгоритмы, непосредственно сам Python, базы данных и теория, и практика с простенькими SQL-запросами. Ещё поговорили про конкурентность и вообще то, как работает ОС. В принципе, ничего суперсложного, однако всё равно стоит хорошенько подготовиться: часто бывают вопросы со звездочкой при успешном ответе наосновной вопрос.

Будьте общительнее на собеседовании. Без этого, скорее всего, искра не появится.

Я вышел на работу в команду Market Intelligence. Мы занимаемся построением ETL (extract, transform, load) процессов, то есть добычей данных. Для этого мы развиваем свою платформу, чтобы удобно можно было управлять кроулерами, которые добывают данные, развиваем свой фреймворк и пишем микросервисы.

Первые дни стажировки

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

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

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

К счастью, есть наставник. Он здорово помогал мне адаптироваться: рассказывал, зачем нужна каждая из встреч и как всё устроено внутри команды. Он же всегда подталкивал кдополнительным активностям, например, предложил подготовить доклад для внутреннего Python community. Ещё раз в неделю у стажёра и наставника проходят встречи один на один. Они нужны для получения обратной связи, как для тебя, так и для менеджера. На них можно скорректировать какие-то процессы, план развития и просто поговорить о том, как продвигается стажировка, получив ценные советы. Плюс у нас регулярно проходят встречи стажеров, что тоже помогает в адаптации.

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

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

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

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

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

Обучение и развитие

Работа над развитием стажёра может сильно варьироваться в зависимости от наставника, но, вероятно, суть будет одна. Я считаю, что мне очень повезло с наставником и за свой рост я вомногом благодарен ему. Мой план развития мы составили по методике OKR. В него входили как хард скиллы, такие MongoDB, Docker, Golang и т.д., так и софт, например, Agile и Kanban. Такой формат мне кажется успешным: на дистанции он приносит большие результаты.

Назвать технологии, которые хочется затащить, легко, но как всё-таки их изучать?

Образование внутри Авито

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

В Авито достаточно часто проходят и live-курсы, на которые можно записаться. Плюс совсем недавно запустилась менторская программа, где можно записаться в ученики к коллеге, который занимается интересующей вас темой.

Лично я проходил внутренние курсы по tech onboarding и Agile, которые сильно помогли впервые дни. В них рассказывается, по каким правилам у нас всё работает и что где можно подсмотреть. А ещё прошёл курсы по Golang: они неплохо погрузили не только в сам язык, но и то, как его применяют именно в Авито.

Обучение вне Авито

Не стоит очевидно зацикливаться на одном. Что-то стоит изучать снаружи. Как показывает мой опыт, самые лучшие курсы делают сами компании, которые разрабатывали технологию. Обычно они называются University. Например, Redis, Mongo University. Лично мне они очень понравились, довольно хорошо погружают. Из минусов можно назвать только то, что они полностью на английском, но а как без него?

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

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

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

Стажировка после адаптации

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

То есть даже стажёр может воздействовать на процесс и платформу. Появилась идея, как улучшить канбан команды? Предложи на ретроспективе, и команда её обсудит. Если идея будет удачной, то предложение примут. Очень хочется затащить какую-то технологию? Это можно продать, особенно если на руках будет готовый MVP. Всё вместе это очень сильно мотивирует.

Вывод

Стажировка в Авито превзошла мои ожидания. Она действительно помогает вырасти и не только как специалисту, но и как личности. Я с удивлением смотрю на текущего себя и себя достажировки. Я ни разу не пожалел, что выбрал эту компанию. Надеюсь и вас в скором времени увидеть в наших рядах: у нас регулярно появляются новые стажировки и посмотреть их можно здесь. Удачи!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru