Простые TypeScript-хитрости, которые позволят масштабировать ваши приложения бесконечно

Мы используем TypeScript, потому что это делает разработку безопаснее и быстрее.

Но, на мой взгляд, TypeScript из коробки содержит слишком много послаблений. Они помогают сэкономить немного времени JavaScript-разработчикам при переходе на TS, но съедают очень много времени в долгосрочной перспективе.

Я собрал ряд настроек и принципов для более строгого использования TypeScript. К ним нужно привыкнуть один раз — и они сэкономят массу времени в будущем.

any

Самое простое правило, которое дает моей команде много профита в долгосрочной перспективе: 

Не использовать any.

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

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

Если все же вам нужно опустить типизацию, советую воспользоваться алиасами на any, почитать о которых можно в статье моего коллеги — совет № 3.

strict

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

Со strict-режимом вы забудете про ошибки вроде undefined is not a function и cannot read property X of null. Ваши типы будут точными и правильными.

А что делать-то?

Если стартуете новый проект, то просто сразу включайте strict и будьте счастливы.

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

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

Но в больших проектах бывает не все так гладко, тогда можно действовать более итеративно. Включите флаг, а над всеми конфликтами поставьте @ts-ignore и комментарий с TODO. В следующий раз при работе с файлом заодно поправите и тип.

// tsconfig.json file {     // ...,     "compilerOptions": {         // a set of cool rules         "noImplicitAny": true,         "noImplicitThis": true,         "strictNullChecks": true,         "strictPropertyInitialization": true,         "strictBindCallApply": true,         "strictFunctionTypes": true,         // a shortcut enabling 6 rules above         "strict": true,         // ...     } }

readonly

Следующее важное для меня правило — везде писать readonly.

Мутировать структуры, с которыми работаешь, — плохая практика. Например, у нас в Angular-мире это сразу приводит к ряду последствий в приложении: появляются проблемы с проверкой изменений в компонентах, за которыми не происходит обновления отображения после мутирования данных.

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

Что делать?

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

Используйте readonly в интерфейсах:

// before export interface Thing {     data: string; }  // after export interface Thing {     readonly data: string; }

Предпочитайте readonly в типах:

// Before export type UnsafeType = { prop: number };  // After export type SafeType = Readonly<{ prop: number }>;

Используйте readonly поля класса везде, где это возможно:

// Before class UnsafeComponent {     loaderShow$ = new BehaviorSubject<boolean>(true); }  // After class SafeComponent {     readonly loaderShow$ = new BehaviorSubject<boolean>(true); }

Используйте readonly-структуры:

// Before const unsafeArray: Array<number> = [1, 2, 3]; const unsafeArrayOtherWay: number[] = [1, 2, 3];  // After const safeArray: ReadonlyArray<number> = [1, 2, 3]; const safeArrayOtherWay: readonly number[] = [1, 2, 3];  // three levels const unsafeArray: number[] = [1, 2, 3]; // bad const safeArray: readonly number[] = [1, 2, 3]; // good const verySafeTuple: [number, number, number] = [1, 2, 3]; // awesome   // Map: // Before const unsafeMap: Map<string, number> = new Map<string, number>();  // After const safeMap: ReadonlyMap<string, number> = new Map<string, number>();   // Set: // Before const unsafeSet: Set<number> = new Set<number>();  // After const safeSet: ReadonlySet<number> = new Set<number>();

as const

В TypeScript v3.4 появились const-assertions. Это более строгий инструмент, чем readonly-типы, потому что он запаковывает вашу константу с наиболее точным типом из возможных. Теперь можно быть уверенным: никто и ничто не сможет это изменить.

Кроме того, при использовании as const ваша IDE всегда будет показывать точный тип используемой сущности.

Utility Types

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

Советую подробно изучить всю официальную документацию по Utility Types и начать активно внедрять их в свои приложения. Они тоже экономят массу времени.

Сужения типов

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

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

(этот же пример можно запустить на repl.it)

import {Subject} from 'rxjs'; import {filter} from 'rxjs/operators';  interface Data {   readonly greeting: string; }  const data$$ = new Subject<Data | null>();  /**  * source$ все еще имеет тип "Observable<Data | null>"  * хотя "null" никогда не пройдет функцию filter  *   * Это произошло, потому что TS не может быть уверен, что тип данных изменился.  * Функция "value => !!value" возвращает boolean, но ничего не говорит о типах  */ const source$ = data$$.pipe(   filter(value => !!value) )  /**   * А вот это хорошо типизированный пример  *   * Эта стрелочная функция отвечает на вопрос, является ли value типом Data.  * Это сужает тип, и теперь "wellTypedSource$" типизирован правильно  */ const wellTypedSource$ = data$$.pipe(   filter((value): value is Data => !!value) )  // Это не скомпилируется, можете проверить :) // source$.subscribe(x => console.log(x.greeting));  wellTypedSource$.subscribe(x => console.log(x.greeting));  data$$.next({ greeting: 'Hi!' });

Вы можете сужать типы несколькими методами:

  • typeof — оператор из JavaScript для проверки примитивных типов.

  • instanceof — оператор из JavaScript для проверки унаследованных сущностей.

  • is T — декларирование из TypeScript, которое позволяет проверять сложные типы или интерфейсы. Будьте осторожны с этой возможностью, потому что так вы перекладываете ответственность за определение типа с TS’а на себя.

Несколько примеров:

(эти же примеры можно запустить на repl.it)

// typeof narrowing function getCheckboxState(value: boolean | null): string {    if (typeof value === 'boolean') {        // value has "boolean" only type        return value ? 'checked' : 'not checked';    }     /**     * В этой области видимости value имеет тип “null”     */    return 'indeterminate'; }  // instanceof narrowing abstract class AbstractButton {    click(): void { } }  class Button extends AbstractButton {    click(): void { } }  class IconButton extends AbstractButton {    icon = 'src/icon';     click(): void { } }  function getButtonIcon(button: AbstractButton): string | null {    /**     * После "instanceof" TS знает, что у объекта кнопки есть поле "icon"     */    return button instanceof IconButton ? button.icon : null; }  // is T narrowing interface User {    readonly id: string;    readonly name: string; }  function isUser(candidate: unknown): candidate is User {    return (        typeof candidate === "object" &&        typeof candidate.id === "string" &&        typeof candidate.name === "string"    ); }  const someData = { id: '42', name: 'John' };  if (isUser(someData)) {    /**     * Теперь TS знает, что someData имплементирует интерфейс User     */    console.log(someData.id, someData.name) }

Итого

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

ссылка на оригинал статьи https://habr.com/ru/company/tinkoff/blog/521262/

Как мы переводили MIKOPBX с chan_sip на PJSIP

Предыстория

Материал изначально готовился как доклад для asterconf 2020. Теперь постараюсь описать все более подробно в этой статье.

MIKOPBX — это бесплатная АТС с открытым исходным кодом на базе Asterisk 16. Год назад мы взялись за переход на PJSIP.

Основные причины:

  • PJSIP поддерживает «множественную регистрацию«. На одном аккаунте можно без проблем регистрировать несколько конечных UAC

  • Корректная работа входящей маршрутизации при настройке регистрации нескольких учетных записей провайдера на одном адресе (IP+PORT)

  • PJSIP более гибок в настройке

  • chan_sip не развивается и объявлен deprecated в Asterisk 17

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


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

Лично у меня подключены следующие устройства:

  • Аппаратный телефон на рабочем столе в офисе

  • Софтфон на ноутбуке

  • Софтфон на смартфоне

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

С чего начать?

В нашем случае был готовый файл конфигурации sip.conf. Стало интересно, возможно ли как то конвертировать старый конфиг в новый формат (структура pjsip.conf отличается значительно).

Готовый скрипт был найден в исходниках asterisk. Найти можно по пути:

contrib/scripts/sip_to_pjsip/sip_to_pjsip.py

Из встроенной справки:

Usage: sip_to_pjsip.py [options] [input-file [output-file]] Converts the chan_sip configuration input-file to the chan_pjsip output-file. The input-file defaults to 'sip.conf'. The output-file defaults to 'pjsip.conf'.

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

Настройка множественной регистрации

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

Каждую входящую регистрацию Asterisk рассматривает как contact.

Параметр «max_contacts» позволяет ограничить количество устройств, которые могут подключиться к endpoint.

;pjsip.conf [226]  type = aor max_contacts = 5

Количество подключенных контактов можно посмотреть в CLI консоли Asterisk:

mikopbx*CLI> pjsip show contacts    Contact:  <Aor/ContactUri..............................> <Hash....> <Status> <RTT(ms)..> ==========================================================================================    Contact:  201/sip:201@172.16.156.1:60616;ob              418d36496b Avail         3.793   Contact:  201/sip:201@172.16.156.1:60616;ob              ba56853d54 Avail         2.189   Contact:  203/sip:203@172.16.156.1:60616;ob              2cd641799f Avail         0.988  Objects found: 3 

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

Пример c комментариями:

;extensions.conf [internal-users]  ; контекст для набора 3х значных внутренних номеров ; PJSIP_DIAL_CONTACTS - функция возвращает Dial-совместимую строку с контактами ; Контакты разделены символом & ; В качестве параметра функции необходимо передать ID endpoint exten => _XXX,1,Set(dialContacts=${PJSIP_DIAL_CONTACTS(${EXTEN})})   ; Перед Dial обязательно необходимо проверить  ; заполнена ли переменная "dialContacts" ; если нет, то на endpoint никто не зарегистрировался same => n,ExecIf($["${dialContacts}x" != "x"]?Dial(${DC},,Tt))

После правки dialplan началось интересное поведение системы.

Наши ожидания не оправдались. Мы предполагали, что при таком звонке, asterisk будет оперировать двумя каналами «Кто звонит» и «Кому звонит«. На практике, все оказалось иначе.

О природе каналов и их происхождении

Каждый канал SIP и PJSIP непосредственно связан с SIP диалогом «PBX — UAC«.

Проще говоря один INVITE = один канал вида SIP/104-0000XX.

Если к endpoint подключено несколько контактов, то при звонке на внутренний номер INVITE будет отправлен каждому контакту, будет создано несколько каналов.

Зная это, можно сделать следующие выводы:

  • Чем больше каналов, тем больше событий в AMI

  • Каждый канал пройдет определенный для него dialplan

  • Каждый канал повлияет на CDR записи

Если кратко подвести итог, то, после включения множественной регистрации, мы видим влияние на все основные модули наших продуктов:

  • История звонков на АТС

  • Функция записи разговоров

  • Работа CTI приложений, завязанных на AMI

Автоподъем. Paging. Intercom

Это крайне интересные функции. Все они завязаны на функцию «Автоответ«. Может работать как с настольными телефонами, так и с многими софтфонами.

Принцип работы многих UAC схож. Чтобы «поднять трубку» достаточно в INVITE передать дополнительный заголовок. Пример:

Call-Info:\;answer-after=0

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

При работе с chan_sip при originate достаточно было установить переменную SIPADDHEADER:

Action: Originate Channel: SIP/104 Context: from-internal Exten: 74952293042 Priority: 1 Callerid: 104 Variable: SIPADDHEADER="Call-Info:\;answer-after=0"

Работа с этой переменной была описана в chan_sip.с и при звонке заголовок добавлялся автоматически в INVITE.

В случае с PJSIP подход отличается. Упрощенный пример extensions.conf:

[internal-users]  exten => 204,1,Dial(${PJSIP_DIAL_CONTACTS(204)},,Ttb(dial_create_chan,s,1)))  [dial_create_chan]  exten => s,1,Set(PJSIP_HEADER(add,Call-Info)=\;answer-after=0)  same => n,return 

Опция «b» в команде «Dial» позволяет созданный канал назначения с помощью Gosub направить в дополнительный контекст «dial_create_chan«.

Только в этом месте есть возможность управлять SIP заголовками ДО отправки INVITE.

Интересный вывод: «dial_create_chan» — место в dialplan, где канал еще существует, но НЕ связан с SIP диалогом.

Теперь более правильный пример установки заголовка:

[internal-users]  ; Получаем контактны: exten => _XXX,1,Set(dС=${PJSIP_DIAL_CONTACTS(${EXTEN})})   ; Считаем количество контактов:   same => n,ExecIf($["${FIELDQTY(dС,&)}"!="1"]?Set(__SIPADDHEADER=${EMPTY}))    same => n,ExecIf($["${dС}x" != "x"]?Dial(${DC},,Ttb(dial_create_chan,s,1)))  [dial_create_chan]  exten => s,1,ExecIf($["${SIPADDHEADER}x" == "x"]?return)   same => n,Set(header=${CUT(SIPADDHEADER,:,1)})   same => n,Set(value=${CUT(SIPADDHEADER,:,2)})   same => n,Set(PJSIP_HEADER(add,${header})=${value})   same => n,Set(__SIPADDHEADER=${EMPTY})    same => n,return 

С помощью функции «FIELDQTY» мы анализируем количество контактов, подключенных к endpoint. Если контактов несколько, то функцию лучше отключить, ведь сложно предугадать, на каком из телефонов сработает ответ на вызов.

С помощью функции «CUT» происходит разбор строки «SIPADDHEADER«, выделяем имя заголовка и его значение.

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

Получение значения UserAgent

Для выборка корректного SIP заголовка необходимо понимать какое конечное устройство подключено к endpoint. В случае с pjsip ситуация несколько изменилась. Пример:

[get-user-agent] exten => 300,1,NoOp(--- Incoming call ---)   same => n,Set(vContact=${PJSIP_AOR(300,contact)})   same => n,Set(vUserAgent=${PJSIP_CONTACT(${vContact},user_agent)})   same => n,NoOp(--- ${vContact} & ${vUserAgent} ---)   ... ... ...    same => n,Hangup()

Пример в одну строчку для AOR с ID 300. Для упрощения ID endpoint = ID AOR и = EXTEN:

; ${PJSIP_CONTACT(${PJSIP_AOR(${EXTEN},contact)},user_agent)}

В функцию «PJSIP_AOR» передаем ID AOR, и в качестве опции указываем, что вернуть нам следует поле «contact«.

В функцию «PJSIP_CONTACT» передаем полученный контакт, и в качестве опции указываем, что вернуть следует поле «user_agent«.

Обратите внимание, PJSIP_AOR(300,contact) вернет ID контакта, но это не тоже самое, что можно увидеть в CLI.

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

201;@e758f5661420b391e239386a94edbefe

Пример вывода в CLI:

pjsip show contacts 201/sip:201@172.16.156.1:57130;ob Contact:  201/sip:201@172.16.156.1:57130;ob

Исходящая регистрация

Согласно документации Asterisk, разработчики выделяют два основных вида проблем регистрации:

Временные (temporary) проблемы

  • No Response

  • 408 Request Timeout

  • 500 Internal Server Error

  • 502 Bad Gateway

  • 503 Service Unavailable

  • 504 Server Timeout

  • Некоторые 6xx ответы

Постоянные (Permanent) проблемы

  • 401 Unauthorized

  • 403 Forbidden

  • 407 Proxy Authentication Required

  • Прочие 4xx, 5xx, 6xx ошибки

В pjsip.conf при настройке исходящей регистрации обязательно необходимо описать опции для повторной попытки регистрации:

[74952293042]  type = registration  ; Временные неудачи ; Интервал для повторных попыток регистрации retry_interval = 30 ; Максимальное количество попыток max_retries = 100  ; "Постоянные" неудачи ; Интервал используется при получении 403 Forbidden ответа. forbidden_retry_interval = 300 ; Интервал используется при получении Fatal ответов (non-temporary 4xx, 5xx, 6xx) fatal_retry_interval = 300

Если sip_to_pjsip.py для конвертации конфигурации, то эти опции придется описать вручную.

Идентификация провайдера

Для рада провайдеров телефонии может наблюдаться следующая картина:

  • Успешно проходит регистрация по адресу sip.test.ru

  • Допустим sip.test.ru резолвится в 10.10.10.10

  • Входящие вызовы поступают с 11.11.11.11

  • Входящие могут поступать и с 10.10.10.10

Вызовы могут не пройти авторизацию и будут завершены.

В PJSIP есть возможность идентификации по IP адресу:

[74952293042] type = identify ; ... ... ... match=sip.test.ru,185.45.152.0/24,185.45.155.0/24; ; ... ... ...

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

Кроме того, следует обратить внимание на опцию «endpoint_identifier_order«.

Значение по умолчанию:

endpoint_identifier_order=ip,username,anonymous

Если у вас есть несколько учетных записей одного провайдера, которые регистрируются на одном и том же адресе IP:PORT, то имеет смысл поменять порядок идентификации:

endpoint_identifier_order=username,ip,anonymous

Пример, есть три транка:

  • 99999 — подключается к 10.10.10.10:5060

  • 88888 — подключается к 10.10.10.10:5060

  • 77777 — подключается к 10.10.10.10:5060

Если не настроить «endpoint_identifier_order«, то:

  • все входящие будут направлены в контекст произвольного endpoint (идентификация пройдет по адресу IP:PORT), к примеру в контекст endpoint «99999» .

  • канал, созданный при входящем будет всегда ассоциироваться с одним и тем же endpoint, к примеру PJSIP/99999-0000XXX, на какой внешний номер бы ни звонил клиент

Входящие без регистрации SIP URI

Для ряда случаев удобно направлять входящие на АТС без регистрации.

Обязательно следует подгрузить модуль «res_pjsip_endpoint_identifier_anonymous.so«.

Пример настройки pjsip.conf

[anonymous]  type = endpoint allow = alaw timers = no context = public-direct-dial

Пример extensions.conf

[public-direct-dial] exten => 74952293042,NoOp(--- Incoming call to ${EXTEN} ---) 	same => n,Dial(PJSIP/204,,TKg)); 	same => n,Hangup()

Контекст public-direct-dial должен быть изолирован от исходящих dialplan.

В качестве exten описываются все DID номера и логика маршрутизации.

Подведу итоги

  • Переход на PJSIP состоялся. С chan_pjsip АТС работает стабильно, надежно

  • Нами был получен огромный опыт работы с PJSIP

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

  • Функция множественной регистрации крайне удобна и порой незаменима

  • chan_pjsip живой, активно развивается и поддерживается сообществом

Из минусов перехода на chan_pjsip стоит отметить:

  • Требуется модернизация dialplan

  • Изменение поведения AMI, что отражается на CTI клиентах

  • Меняется поведение CDR, требуется доработка легирования истории звонков

  • chan_pjsip активно развивается, в свежих релизах asterisk встречаются грубые ошибки. не стоит гнаться за новыми версиями, лучше выждать появления «certified» версий

Полезные ссылки

ссылка на оригинал статьи https://habr.com/ru/post/521236/

Just add some Salt

Как мы мигрировали 700+ серверов на Salt

Долгое время нас устраивала сложная и неповоротливая конфигурация с 2 Git-репозиториями, где часть данных хранится в MySQL, а другая часть Puppet 3.8. Но наши потребности постепенно росли, количество сервисов увеличивалось, а производительность конфигурации снижалась. Тогда мы поставили перед собой задачу усовершенствовать конфигурацию, оптимизировав все имеющиеся данные и инструменты.

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

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

Чем хорош SaltStack, и какие задачи с его помощью можно решить — статья от ptsecurity, Positive Technologies.

Установка, запуск, первые команды и знакомство с функциями — статья автора zerghack007.

Salt — система управления конфигурациями и удаленного выполнения операций. Инфраструктурный фреймворк с открытым исходным кодом, написан на Python.

Почему Salt?

Salt, Ansible, Puppet и Chef — достойные варианты для выбора инструмента управления конфигурациями. Мы выбрали Salt, так как выделили для себя в качестве приоритета такие преимущества:

  • Модульность, наличие API в бесплатной версии в отличие от Ansible.
  • Python, а значит, можно легко разобраться в любом компоненте и написать самостоятельно функционал, которого не хватает.
  • Высокая производительность и масштабируемость. Мастер устанавливает постоянное соединение с миньонами, используя ZeroMQ, что дает максимальную производительность.
  • Реакторы (reactors) — это своеобразные триггеры, которые выполняются при появлении определенного сообщения в шине сообщений.
  • Оркестрация — возможность выстраивать сложные связи и выполнять действия в определенной последовательности, например, настроить сначала балансировщик нагрузки, а затем кластер веб-серверов.
  • Puppet и Chef написаны на Ruby. В нашей команде нет компетентного специалиста для работы с этим языком программирования, зато Python хорошо знаком и чаще нами используется.
  • Для тех команд, которые использовали ранее Ansible, будет актуальной возможность использовать плейбуки от Ansible. Это позволит безболезненно мигрировать на Salt.

Обратите внимание:

Мы используем Salt почти два года и советуем обратить внимание на следующие моменты:

  • Не всегда актуальная документация Salt, она может отставать от реальности, и мало примеров. Иногда приходится самостоятельно проверять наличие новых функций, так как в документации их описание отсутствует. Но мы знаем, что команда SaltStack активно работает над этой проблемой.
  • SaltStack предлагает очень много модулей. Это круто, но не всегда стоит использовать их все. Множество модулей может привести к избыточности: чем больше модулей, тем больше усложнений и зависимостей. Например, иногда проще использовать cmd.run или file.managed, чем специальный модуль.

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

Дано. Найти. Решение

Дано:

Итак, наша исходная конфигурация:

  • 2 Git-репозитория (один — для инженеров и админов; второй — для особо критичных серверов, доступен только админам);
  • часть данных в MySQL;
  • другая часть — в Puppet 3.8 (перестарались с наследованием, практически не использовав Hiera — key-value хранилище).

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

Решение:

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

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

Основной проблемой при переходе с Puppet на Salt была устаревшая операционная система (в 2018 году стояла ОС Ubuntu 12.04 и 14.04). Перед миграцией необходимо было обновить ОС и не затронуть работу сервиса / сервера. В остальном все было достаточно легко: коллеги постепенно втягивались в процесс.

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

Команда оценила также способы доставки конфигурации: push (мастер «проталкивает») и pull (миньон «стягивает»). Режим Masterless выручает, если нужно протестировать что-то несложное и при этом не связываться с Git-репозиторием. Запуск миньона в режиме masterless позволяет использовать управление конфигурацией Salt для одной машины, не обращаясь к мастеру Salt на другой машине. Конфигурация полностью локальна.

До 300 миньонов с подобным решением у нас не было серьезных проблем. Конфигурация мастера на тот момент — это VDS с 6 ядрами и 4 Гб памяти.

Однако, как только количество миньонов достигло 300, показатель Load Average (cредняя загрузка системы) вырос до значений 3.5-4, и работа системы сильно замедлилась. Раньше команда state.apply выполнялась 30-40 секунд, а сейчас все 18 минут!

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

Наблюдения за мастером не давали однозначного ответа на вопрос. Памяти хватало, сеть не была нагружена, диск утилизировался на 10%. Думали, что виноват GitLab, но и он оказался ни при чем.

Похоже, что не хватало процессорной мощности: при добавлении ядер показатель Load Average естественно падал, и команда state.apply выполнялась хоть и быстрее, около 5-7 минут, но не так быстро, как нам хотелось.

Добавление воркеров отчасти решало проблему, но при этом значительно увеличивалось потребление памяти.

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

Этап №1

Так как пиллары — это защищенное хранилище, обращение к хранилищу связано с операциями шифрования, за доступ к нему нужно платить процессорным временем. Поэтому мы уменьшили количество обращений к пилларам: одни и те же данные брались только 1 раз; если они были нужны еще где-то, то доступ к ним осуществлялся через import ({%- from ‘defaults/pillar.sls’ import profile %}).

Применение конфигурации происходит 1 раз в час, время выполнения выбирается рандомно. Проанализировав, сколько задач выполняется в минуту и насколько равномерно они распределены в течение часа, мы выяснили: в начале часа с 1-й по 8-ю минуты проходит больше всего заданий, а в 34-ю минуту ни одного! Мы написали раннер, который 1 раз в неделю проходил по всем миньонам и равномерно распределял задания. Благодаря такому подходу нагрузка стала равномерной, без скачков.

Звучали предложения переехать на железный сервер, но на тот момент его не было и… мы решили проблему по-другому. Добавили немного памяти и разместили в ней весь кеш. Глядя в дашборд Grafana, мы сначала подумали что salt-master не работает, так как показатель Load Average упал до значения 0.5. Проверили время выполнения state.apply и тоже удивились — 20-30 секунд. Это была победа!

Этап №2

Через полгода количество миньонов возросло до 650, и… снова наступила деградация производительности. График Load Average растет вместе с количеством миньонов.

Первое, что мы сделали: включили кеш для пилларов, время жизни установили в 1 час (pillar_cache_ttl: 3600). Мы понимали, что в теперь наши коммиты не будут мгновенными и придется подождать, пока мастер обновит кеш.

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

Этап №3

Немного помедитировали над debug-логами и выдвинули гипотезу: а что если увеличить интервал обновления файлового бекенда и кеша списка файлов (gitfs_update_interval, fileserver_list_cache_time)? Обновление которых происходило 1 раз в минуту и порой занимало до 15 секунд. Увеличив интервал обновления с 1 минуты до 10 минут, мы снова выиграли в скорости! Показатель LA снизился с 1.5 до 0.5. Время применения конфигурации уменьшилось до желаемых 20 секунд. Не смотря на то, что через какое-то время LA снова подрос, скорость выполнения state.apply изменилась не существенно. Принудительное обновление этих кешей добавили в хуки при git push’е.

Добавили аналитику в Elasticsearch: переписали встроенный elasticsearch_return и теперь мы можем следить за результатами state.apply (среднее время выполнения, самый долгий стейт и количество ошибок).

Результаты

Сейчас производительность Salt’a нас полностью устраивает. В планах увеличить количество миньонов в 2 раза. Как справится с такой нагрузкой наш мастер, пока сказать сложно. Возможно, мы обратимся к горизонтальному масштабированию или найдем волшебный параметр. Время покажет!

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

Полезные ресурсы

ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/521370/

Выводы, которые я сделал, помогая стартапу для секс-чатов повысить конверсию

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

Клиент, восхищенный успехом Patreon (веб-сайт, на котором авторы могут распространять свои работы по платной подписке или предоставлять дополнительный контент для своих подписчиков), хотел сделать подобную среду, где популярные инфлюенсеры из Instagram могли бы быть ближе к своим поклонникам: публиковать уникальный контент, а также общаться в личке за вознаграждение. Однако после запуска мобильного PWA-приложения и привлечения первых пользователей, они столкнулись со слабыми вовлечением и конверсией платных действий.

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

Мне объяснили, что в будущем проект обязательно станет подобным Patreon, но сейчас им легко удается привлекать именно моделей в стиле «ню», которые имея значительный объем поклонников в Instagram все чаще сталкиваются с ограничениями платформы для их творчества и не имеют штатных возможностей монетизировать свою деятельность там.

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

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

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

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

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

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

Если идея объемная, затрагивает слишком разные сегменты целевой аудитории – сфокусироваться на одном, наиболее перспективном направлении. Это позволит не разрываться между противоречащими друг другу «хотелками» и создать действительно полезный продукт.

Мы предложили сфокусироваться на моделях в стиле «ню» и их поклонниках в социальных сетях. Это позволило привести контент к целостному виду за счет удаления нецелевых профилей. Фокусировка также позволила упростить привлечение новых моделей. Но это все еще не решало проблем низкого вовлечения и конверсии платных действий.

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

Вот, что мы сделали:

Удалили избыточный функционал

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

Сместили акценты в профиле инфлюенсера

Отдали приоритет контенту за счет уменьшения юзерпика, сместили акцент с кнопки «Подписаться» на кнопку «Написать». Дали возможность инфлюенсерам призывать к приватному общению за счет подписей к профилю.

Повторили призыв к ключевому действию в конце ленты

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

Добавили призыв к ключевому действию при просмотре фото

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

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

Вывод:
Понимание целевой аудитории и ее потребностей – ключевой инструмент для запуска успешного продукта. Вместо игр в «угадай удачное интерфейсное решение» или «подсмотрю у конкурентов», лучше сфокусироваться на выявлении и удовлетворении потребностей ЦА.

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

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

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

Оба варианта знакомили новых пользователей с идеей проекта и транслировали ключевые ценности, но делали это по-разному. Первый наследовал стиль Instagram с перекрывающей контент плашкой. Второй симулировал чат.

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

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

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

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

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

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

Явная регистрация решала еще одну проблему – убирала один из множества барьеров взаимодействия.

Представьте: вы попали на сервис, смотрите фото симпатичных девушек и вдруг в порыве эмоционального возбуждения готовы написать одной из них. Нажимаете кнопку «Написать» и ошибка – введите ваш имейл для завершения регистрации. Вы все еще под властью эмоций, вводите имейл с нетипичным доменом и ошибка – можно ввести имейл только из фиксированного списка доменов. Вы судорожно начинаете вспоминать, есть ли у вас имейл на яндексе или мейлру и удача снова вам улыбается – вы вспомнили про старый почтовый ящик и теперь НУЖНО подтвердить ВАШ почтовый ЯЩИК перейдя ПО ссылке ИЗ письма Фааааааааак! – кричите вы, но девушка так хороша, и вроде осталось совсем немного. Что ж, нужно подтвердить – подтвердим и ошибка – недостаточно средств на счете. Вы пытаетесь понять как же пополнить этот чертов счет. Эмоции на пределе. Вы уже давно забыли про девушку и вообще не понимаете зачем делаете все это. Руки трясутся, глаз дергается и вы закрываете этот чертов сайт. От былого предвкушения уже не осталось и следа.

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

Вот, что мы сделали:

Изменили процесс регистрации

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

Внедрили внутреннюю валюту вместо реальных денег

Девушки флиртовали, заигрывали с поклонниками приглашая их в приватные чаты. Все это сильно напоминало игру. Но привязка к реальным деньгам для оплаты за каждое действие отрезвляла. Заплатить $0,3 за просмотр фото? Написать в приват – $0,5 за сообщение?

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

Встроили пополнение счета в основные сценарии

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

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

Проектирование сценариев взаимодействия с помощью инструментов User Journey Map и User Flow позволяет избежать большинства таких проблем и обеспечить более целостный опыт взаимодействия с продуктом.

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

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

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

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

ссылка на оригинал статьи https://habr.com/ru/post/521368/

Виды репликации в MongoDB

Привет, хабровчане! Расшифровали для вас часть урока по MongoDB от Евгения Аристова, разработчика с 20-летним стажем и автора онлайн-курса «Нереляционные базы данных». Материал, как и сам курс, будет полезен специалистам, сталкивающимся в работе с NoSQL, желающим научиться оптимизировать свои базы данных и работу с ними.

Зачем нужна репликация?

  1. Высокая доступность. Бэкап это хорошо, но нужно время на его развертывание.
  2. Горизонтальное масштабирование. В случае, когда закончились физические ядра и память у сервера.
  3. Бэкап лучше делать с реплики, а не с мастера.
  4. Геораспределение нагрузки.

В MongoDB видов репликации из коробки немного: самый актуальный на данный момент Replicaset, а второй — Master-slave, который ограничен версией 3.6 и подробно рассматриваться в этой статье не будет.

№1. Запись и чтение с основного сервера

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

№2. Чтение с реплики

Альтернативой чтению и записи с Primary является вариант, когда драйвер может читать информацию с Secondary. При этом настройки могут быть разными, например, «читать информацию предпочтительнее с Secondary, а потом с Primary» или «читать информацию с ближайшего по карте сети нода» и т.д. Такие варианты настроек используются чаще, чем первый вариант репликации, где все идет через Primary.

3 способа сделать реплику доступной для чтения:

  • Указать db.slaveOk()
  • Указать в строке подключения драйвера нужные параметры
  • Указать все, а потом более точечно прописать в самом запросе, например, читай из Secondary в Южном регионе: db.collection.find({}).readPref( “secondary”, [ { “region”: “South”} ] )

Проблемы чтения с реплики

  1. Так как запись асинхронная, она может быть уже сделана на Primary, но не доехать до Secondary, поэтому будут прочитаны старые данные с Secondary.
  2. Записав данные на основной, нельзя быть уверенным, когда остальные получат эти данные.
    Чтобы было все синхронно, каждая нода должна подтвердить получение данных. Сейчас в MongoDB есть общие настройки, а есть на каждый запрос, где можно указать, от скольки нод ожидается подтверждение запроса.
  3. Когда падает основной сервер, запускается процесс выборов (кворум) — а это уже особое отдельное «веселье».

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

А) Ноды «слушают» друг друга, эта связь называется Heartbeat. То есть каждая нода постоянно проверяется другими на предмет «живая/ неживая», для того, чтобы предпринимать какие-то действия, если что-то случилось.

Б) Одна Secondary-нода меняется на Arbiter. Это очень легковесное приложение, запускается как Mongo, практически не ест ресурсов и отвечает за то, что определяет, какую ноду в момент голосования признать главной. И это в целом рекомендуемая конфигурация.

Основные особенности этой конфигурации

  • Репликация асинхронная
  • Арбитр не содержит данных, и поэтому очень легковесный
  • Primary может стать Secondary и наоборот. Арбитр не может стать ни Primary, ни Secondary
  • Максимальное количество реплик 50 и только 7 из них имеют право голосовать
  • Arbiter можно установить и на Primary или Secondary, но делать это не рекомендуется, т.к. если этот сервер упадет, Arbiter тоже не сможет выполнить свою функцию.

Если вам интересно узнать больше о кластерных возможностях MongoDB, посмотреть запись всего демо-урока можно тут. На занятии Евгений Аристов демонстрирует отличия Replicaset от Master-slave, объясняет процесс кворума, масштабирование, шардирование и правильный подбор ключа к шардированию.

Изучение возможностей MongoDB входит в программу онлайн-курса «Нереляционные базы данных». Курс предназначен для разработчиков, администраторов и других специалистов, которые сталкиваются в работе с NoSQL. На занятиях студенты на практике осваивают наиболее актуальные сегодня инструменты: Cassandra, MongoDB, Redis, ClickHouse, Tarantool, Kafka, Neo4j, RabbitMQ.

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

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/521302/