Большой механический дисплей с кулачковым механизмом в качестве дешифратора

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

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

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

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

Теперь самое интересное — кулачки. Нижние половины кулачков устанавливаем согласно кинематический схеме так, чтобы цифры 7 на них «смотрели» друг на друга. Затем верхние половины кулачков (не показанные на кинематической схеме) устанавливаем, соблюдая то же условие. Не шевеля всю конструкцию, чтобы они не сместились, надеваем зубчатые колёса — и теперь вращение кулачков синхронизировано. И чтобы все сегменты, кроме центрального, возвращались, когда на их толкатели не давят кулачки, оборачиваем их резиновым кольцом. Механизм желательно слегка смазать, следя, чтобы масло не попало на кольцо.

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

Таймкоды к видео: 3:21 — 7:26 — объяснение принципа действия механизма на 3D-модели, далее и до окончания — сборка.

Устройство на Thingiverse под CC-BY-NC 3.0.


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

Azure Search

Если какой-то из ваших проектов использует данные хранящиеся в Ажуровской базе, то вполне возможно, что у вас есть возможность задействовать поиск по данным с помощью Azure search. Совершать поиск можно не только по базам (Azure Cosmos DB, Azure SQL Database, SQL Server hosted in an Azure VM), но и по Blob (Azure Blob Storage, Azure Table Storage).

У Search имеется бесплатный тариф, который позволяет создать до трех индексов общим размером до 50 Mb. Бесплатный тариф не обладает возможностями балансировки нагрузки, но вполне себе пригоден для использования.

Разобраться с поиском мне показалось довольно просто (хоть и не всегда все очевидно). Есть 3 типа объектов: datasource, index и indexer. Главным объектом, пожалуй, является index. Именно он отвечает за то как искать и что именно искать. Datasource это подключение к данным, а indexer это job который обновляет данные index.

UI интерфейс портала позволяет импортировать данные и создать все три объекта. Мимоходом будет возможность и добавить к поиску когнитивные возможности.
Если база данных SQL находится в подписке, то ее можно выбрать при создании datasource. Хотя пароль почему-то все-равно приходится ввести. Если вы захотите использовать Cosmos DB, то строку подключения вам необходимо будет ввести вручную. Не забудьте указать в строке и базу данных, добавив в конце строки Database=ИМЯ_ВАШЕЙ_БАЗЫ

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

Для файлов загруженных в blob возможно использование OCR (Optical Character Recognition). Возможно распознавание рукописного (пока что только английского языка) и печатного текста. С помощью когнитивных сервисов возможно определение различных объектов на фото. Например, известных мест или знаменитостей.

Следующим этапом идет создание индекса. Единственным на текущий день вариантом Search mode является «analyzingInfixMatching»

На этом этапе вы можете расставить галочки напротив полей вашей таблицы или добавить какое-то новое поле в индекс. На всякий случай объясню возможности полей:
Retrievable – поле будет присутствовать в результатах поиска
Filterable – значение поля можно будет отфильтровать
Sortable – можно отсортировать результат по этому полю
Facetable – своеобразная группировка по определенным признакам. Например с помощью следующего выражения facet=listPrice,values:10|25|100|500|1000|2500 можно получить следующую разбивку результатов по группам

Searchable – по этому полю будет вестись поиск

Поле Analyzer предлагает выбрать анализатор для различных языков. Используются 2 версии – Lucine и Microsoft. Для того чтобы понять в чем разница необходимо разобраться в чем разница между двумя следующими терминами:
Сте́мминг — это процесс нахождения основы слова для заданного исходного слова. Stem (англ.) – основа, стебель, происхождение. Стемминг использует алгоритмы. Зачастую обрезает слова удаляя суффиксы и окончания, получая основу слова.
Лемматиза́ция — процесс приведения словоформы к лемме — её нормальной (словарной) форме. Лемма — каноническая, основная форма слова. Лемматизация использует поиск по словарям содержащим различные формы слов.

Lucine анализатор использует стемминг. Microsoft анализатор использует лемматизацию.
По умолчанию, если ничего не выбрано используется Lucine. Но если вы ищете по данным на каком-то определенном языке, то несомненно лучше использовать анализатор для этого языка.

Suggester – позволяет по начальным буквам поиска выдать подсказки с документами содержащими введеный текст.
При использовании suggester в Azure Search в клиентском приложении у вас будет 2 варианта его использования: сам suggester или autocomplete. Если коротко, то suggester предлагает полностью всю строку из поля таблицы, а autocomplete предлагает только завершить слово или выражение из пары слов. Лучше всего о различиях между режимами suggester и autocomplete расскажет следующий артикул: Autocomplete in Azure Search now in public preview В этом артикуле очень наглядные гифки.

На этапе создания indexer необходимо указать high watermark column. Это поле которое изменяется при каждом изменении записи. Обычно это что-то вроде поля с датой последнего изменения или поля _ts в Cosmos DB. При индексации, в случае если значение поля изменено, то изменится и индекс.

Track deletions это вариант удалять записи из индекса автоматически. Но для этого у вас в базе данных должен быть настроен soft delete. Если у вас используется soft delete, то при удалении записи она не удаляется, а просто помечается как удаленная. Стандартным вариантом предлагается добавить в базу поле isDeleted и задавать ему значение true, в случае если запись удалена.
Альтернативно можно при каждом удалении записи из базы отправлять запрос на удаление из поиска к API Azure Search. В этом случае галочку Trak deletions можно не ставить. Но мне такой вариант не особо нравится, так как если запрос на удаление не отработает, то запись останется в индексе. Как по мне, не хватает возможности перестраивать индекс раз в определенный промежуток времени полностью.

Несмотря на все удобства портала, после создания вы сможете добавить какие-то новые поля в индекс, но изменить существующие уже у вас не получится. Что же делать если нужно что-то изменить? Можно пересоздать индекс. Удалить существующий и создать новый содержащий необходимые изменения. Делать это с помощью портала довольно муторное занятие. Я использую для этих целей API. С помощью приложения вроде Postman можно получить JSON индекса и использовать его для написания запроса создания индекса. Необходимо лишь сделать небольшие изменения (например, убрать системные поля «@odata.context» и «@odata.etag»).

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

Запрос для получения данных индекса такой:

GET https://[service name].search.windows.net/indexes/[index name]?api-version=[api-version]

В заголовок необходимо добавить api-key: [admin key]

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

POST https://[servicename].search.windows.net/indexes?api-version=[api-version] Content-Type: application/json    api-key: [admin key]

или

PUT https://[servicename].search.windows.net/indexes/[index name]?api-version=[api-version]

В body необходимо указать JSON с содержимым индекса.
Последней версией на данный момент является 2019-05-06, а до нее долгое время использовалась 2017-11-11

Работая через API вы сможете использовать какие-то возможности поиска, которые отсутствуют на портале.
Для того чтобы дать каким-то полям приоритет при поиске можно использовать scoring profiles.
Следующий JSON, добавленный в запрос дает полю «title» двойное преимущество над полем «info»:

"scoringProfiles": [ {         "name": "profileForTitle",           "document": {           "weights": {             "title": 2,             “info": 1 }         } ]

Кроме возможности дать каким-то полям приоритет с помощью weights, имеется возможность использовать какие-то предопределенные функции: freshness, magnitude, distance, и tag.

Freshness используется только с полями DateTime и позволяет поднять в поиске последние записи. Magnitude используется с полями int и double. Ну и соответственно, эту функцию хорошо использовать с полями хранящими цены, количество скачиваний и другую числовую информацию. Distance используется только с полями типа Edm.GeographyPoint и поднимают в поиске по расстоянию от определенной локации. В случае если типом функции указан tag, то в поиске поднимутся документы содержащие тэги, которые присутствуют в строке поиска.
Один из самых популярных вариантов – поднять в поиске последние документы выглядит так:

"scoringProfiles": [{           "name":"newDocs",  "functions": [ {             "type": "freshness",             "fieldName": "documentDate",             "boost": 10,             "interpolation": "quadratic",             "freshness": {               "boostingDuration": "P7D"              }        } ] } ]

Документы, у которых поле documentDate содержит дату последних семи дней («P7D») будут подняты вверх.
После того как вы создали scoring profile, вы можете указывать в запросах его имя. Только в этом случае нужные поля будут подняты в поиске.
Подробнее читайте в официальной документации: Add scoring profiles to an Azure Search index

Data Change Detection Policy

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

"dataChangeDetectionPolicy" : {        "@odata.type" : "#Microsoft.Azure.Search.HighWaterMarkChangeDetectionPolicy",        "highWaterMarkColumnName" : "[a rowversion or last_updated column name]" } Но для идентификации удаленного поля при использовании soft delete необходимо добавлять еще одно policy: "dataDeletionDetectionPolicy" : {         "@odata.type" : "#Microsoft.Azure.Search.SoftDeleteColumnDeletionDetectionPolicy",              "softDeleteColumnName" : "IsDeleted",         "softDeleteMarkerValue" : "true"   }

Если вы используете SQL Server и ваша база поддерживает Change Tracking, то удаленные записи могут удаляться из индекса автоматически. Указывать highWaterMarkColumnName в этом случае не потребуется. Достаточно указать SqlIntegratedChangeTrackingPolicy вместо HighWaterMarkChangeDetectionPolicy

"dataChangeDetectionPolicy" : {        "@odata.type" : "#Microsoft.Azure.Search.SqlIntegratedChangeTrackingPolicy"   }

Это очень удобно. Но есть нюансы, которые не дают насладится этой фичей полностью.
Во-первых, SqlIntegratedChangeTrackingPolicy нельзя использовать с views. Во-вторых, в таблице не должно быть композитных первичных ключей. Само-собой разумеется, что версия SQL Server-а должна быть более-менее новой. Ну и, наконец, для базы данных и используемых поиском таблиц должен быть включен Change Tracking. Для базы включается он так:

ALTER DATABASE AdventureWorks2012   SET CHANGE_TRACKING = ON   (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON) 

А для таблицы так:

ALTER TABLE Person.Contact   ENABLE CHANGE_TRACKING   WITH (TRACK_COLUMNS_UPDATED = ON) 

Но это не все. Очень рекомендуется включить для базы snapshot isolation.

ALTER DATABASE AdventureWorks2012 SET ALLOW_SNAPSHOT_ISOLATION ON;  

Кроме плясок с бубном при установке Change Traсking для базы данных для меня минусом является еще и невозможность использования views. Так что я все-таки как правило вынужденно использую HighWaterMarkChangeDetectionPolicy

Поиск по данным

По умолчанию Azure search использует simple query syntax. Как это не казалось бы удивительным, но он довольно простой:
wifi+luxury ищет слова wifi и luxury одновременно
«luxury hotel» ищет фразу
wifi | luxury ищет или слово wifi или слово luxury
wifi –luxury ищет тексты со словом wifi но без слова luxury
lux ищет слова, которые начинаются с lux
Вполне можно комбинировать правила поиска используя скобки. Например, правило motel+(wifi | luxury) ищет слово motel и либо слово wifi либо слово luxury

Приятно, что Azure Search может использовать синтаксис Lucene. Для того чтобы использовался именно он, необходимо добавить в запрос поиска queryType=full
Отличие Azure-овского от классического Lucene синтаксиса только в отсутствии range.
Вот так в Azure Search нельзя: mod_date:[20020101 TO 20030101]
Зато в Azure Search можно использовать $filter с синтаксисом ODATA. Вот пример фильтра:

{      "name": "Scott",       "filter": "(age ge 25 and and lt 50) or surname eq 'Guthrie'"  }

Фильтры можно использовать также и с simple query syntax.

В Lucene логика «или» реализуется с помощью OR или ||
Оба значения можно найти, указав инструкцию «и» с помощью: AND, && или +
Для «не» можно использовать что-то из следующего: NOT, ! или

Инструкция «не» имеет общую особенность и для simple syntax и для Lucene. Ее поведение зависит от режима поиска, который может быть установлен как в searchMode=all, так и в searchMode=any (по умолчанию используется это значение). В режиме any поиск wifi -luxury найдет документы со словом wifi или документы без слова luxury. В режим all по тому же запросу найдет доки со словом wifi и одновременно без слова luxury.

Давайте рассмотрим какие-то интересные возможности Lucine.
Fuzzy search позволяет искать слова, которые отличаются от искомого на одну или несколько букв. То есть помогает бороться с опечатками. Например, поиск по «blue~» или «blue~1» вернет вам и «blue» и «blues» и даже «glue». Но при этом поиск по «business~analyst» будет означать business или analyst
Proximity позволяет искать слова которые расположены рядом. Например, «hotel airport»~5 найдет слова «hotel» и «airport» которые располагаются в тексте не дальше чем в 5 слов друг от друга.
Term boosting позволяет задать приоритет какому-то слову в поиске. Пример: «rock^2 electronic» ищет слова rock и electronic, но записи со словом rock в поиске будут отображены выше.
Regular expressions – использование регулярных выражений. Здесь все в соответствии с официальной документацией Lucine по регулярным выражениям. Найти ее можно по следующей сылке
При поиске регулярные выражения необходимо размещать между прямыми слешами «/». Например, вот так: /[mh]otel/

Если ваша строка поиска содержит специальные символы, то они должны быть экранированы с помощью обратного слеша \
Пример символов, которые необходимо экранировать: + — && ||! ( ) { } [ ] ^ » ~ * ? : \ /
Поиск можно совершить с помощью GET запроса. Официальный пример такой:

GET /indexes/hotels/docs?search=category:budget AND \"recently renovated\"^3&searchMode=all&api-version=2019-05-06&querytype=full

Но можно использовать и POST запрос с body. Опять же официальный пример:

POST /indexes/hotels/docs/search?api-version=2019-05-06 {   "search": "category:budget AND \"recently renovated\"^3",   "queryType": "full",   "searchMode": "all" }

Если вы используете GET запрос или POST с типом данных application/x-www-form-urlencoded, то вам необходимо энкодировать unsafe и reserved символы.
Символы ; /?: @ = & являются зарезервированными
Символы » ` < > # % { } | \ ^ ~ [ ] являются unsafe.
Например, символ # станет %23 а символ  ? станет %3F

Пара ссылок для разработчиков.

Если в .NET разработчик, то вы можете использовать NuGet пакет Microsoft.Azure.Search Кроме того, имеются примеры на NodeJS и Java
Пример простого приложения на .NET Core вы можете найти тут ASP.NET Core Azure search sample


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

Телефония со Snom: для тех, кто работает дома

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

Решения на основе IP-телефонии могут быть очень выгодны для компаний, использующих удалённых сотрудников. Такие решения можно легко интегрировать с имеющимися коммуникационными системами, они имеют хорошую мобильность на тот случай, если работники будут переезжать. Можно сохранять телефонные номера, что минимизирует возможные неудобства для клиентов и уменьшает перерывы в обслуживании. Телефоны Snom позволяют работникам подключаться к IP-сети, просто подключая устройства к роутерам сетевым кабелем или по Wi-Fi. Телефоны можно конфигурировать в офисе или на складе, а затем рассылать сотрудникам по почте. Или можно воспользоваться SRAPS — это специальная служба Snom для автоматической удалённой настройки, с её помощью можно настроить телефон хоть с другой стороны планеты.

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

К преимуществам использования VoIP удалёнными сотрудниками можно отнести:

  • Доступность функций. Удалённые сотрудники могут пользоваться всеми возможностями центральной коммуникационной платформы и легко взаимодействовать с коллегами.
  • Масштабируемость. Простота масштабирования вверх или вниз, в зависимости от потребностей.
  • Прямой набор номера. Эта функция доступна не только сотрудникам офиса, но и удалённым работникам.
  • Обработка глобальной базы данных клиентов ускоряется вместе с ростом бизнеса.
  • Снижение расходов. VoIP исключает расходы, связанные с центральным офисом и удалёнными сотрудниками.
  • Централизованное управление. Упрощает всю коммуникационную инфраструктуру.

Studentenwerk Freiburg

Studentenwerk — это Германская национальная студенческая ассоциация (German National Association for Student Affairs). Studentenwerk Freiburg — это обслуживающая компания среднего размера, в которой около 400 сотрудников обслуживают более чем 35 000 человек. Studentenwerk финансируется Студенческим Союзом и помогает в организации различных мероприятий в девяти университетах: во Фрайбурге, Оффенбурге, Генгенбахе, Фюртвангене, Кёле и Филлингене-Швеннингене.

В 2007-м закончился 10-летний контракт Студенческого Союза на поддержку и обслуживание телефонной системы Siemens-Nixdorf. Устаревающая система уже не удовлетворяла современным требованиям, ей не хватало гибкости в настройке, она была дорога в обслуживании и не отвечала повседневным потребностям университетов. В Studentenwerk больше не хотели попадать в зависимость от другой проприетарной и негибкой системы, и поэтому искали технологию, которую можно было расширять по мере необходимости. Заказчику нужна была независимость нового решения от вендоров и возможность интеграции приложений. Также требовали отдельные почтовые ящики для каждого нового абонента, и центральный факсовый сервер, способный принимать и отправлять факсы напрямую с локальных компьютеров. Необходимо было CTI-подключение, чтобы студенты могли звонить и принимать звонки, и даже создавать конференц-связь прямо со своих компьютеров.

Контракт на создание новой сети получил сервис-провайдер из Фрайбурга — компания badenIT GmbH, являющаяся филиалом городской энергетической компании Badenova. После консультаций с подрядчиком сотрудники Studentenwerk решили, что телефонная VoIP-система удовлетворяет их потребностям, и выбрали коробочную open source АТС Asterisk. Главным преимуществом open source-решения было отсутствие лицензионных отчислений, вне зависимости от количества абонентов. Подрядчик рекомендовал использовать IP-телефоны Snom, и после их тестирования Студенческий Союз с этим согласился.

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

Новое VoIP-решение состояло из 30 ISDN-линий и около 110 устройств. Применили телефоны Snom трёх моделей: Snom 300 имел все важные для офиса функции; Snom 320 позволял общаться через беспроводные гарнитуры и поддерживал сторонние устройства для конференц-связи; Snom 360 обладал более сложными возможностями, такими как подробная информация о вызовах и тонкие настройки.

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

Studentenwerk Freiburg получили желаемую гибкость, которая была недостижима со старой проприетарной технологией. В удалённых офисах теперь можно подключать телефоны через VPN, так что запланировано увеличить общее количество телефонов в 56 раз.

SanLucar Fruit

SanLucar Fruit — один из основных дистрибьюторов продуктов садоводства в Испании. Компания основана в Валенсии в 1993-м и состоит из более чем 120 сотрудников и фермеров в более чем 30 странах мира, которые торгуют вручную выращенными и собранными фруктами и овощами, уделяя большое внимание качеству продукции. Офисы компании расположены в Валенсии. В Германии SanLucar распространяет свои товары из Эттлингена (Карлсруэ), а Австрии из Вены. Также у компании есть офисы в Италии, Франции, Турции, Тунисе, ЮАР, Центральной и Южной Америке.

SanLucar Fruit нужна была новая, особенная телекоммуникационная система, способная интегрировать голос, видео, текстовые сообщения и веб-доступ, а также поддерживающая мобильную телефонию. Заказчики искали решение, соответствующее компании их размера. Они считали, что создание среды, обеспечивающей качественную, быструю и эффективную связь, было необходимо для наилучшего ведения бизнеса. Разбросанные по Европе и работающие на очень активном рынке, сотрудникам SanLucar Fruit нужна была информация в реальном времени о статусе и наличии каждого пользователя. Им нужна была быстрая и эффективная интеграция. Кроме того, заказчики хотели, чтобы новое решение позволило объединить современную функциональность с философией их компании относительно экологии. Система должна была обеспечивать видеосвязь и совместную работу над документами не только в рамках офиса, но и с удалёнными сотрудниками вне зависимости от их местоположения.

Компания EuropeSIP Communications спроектировала систему на основе Microsoft OCS 2007 R2, которая совместима с Enterprise Voice, использует телефоны Snom со специальной прошивкой под Microsoft (Snom OCS edition) и кластеризацию Asterisk, предоставляющую расширенные возможности шлюза и работы с факсами. На компьютерах стоит клиент Microsoft Office Communicator 2007 R2 с пакетом SCUPA Business для удалённого управления телефонами. Связь с удалёнными пользователями поддерживается с помощью клиента и телефонов Snom через сервер OCS-инфраструктуры Edge Server. Соединение с PSTN осуществляется через Mediation Server, который подключён к кластеру Asterisk, оснащённому картами Sangoma.

Получившееся решение обеспечивает требуемую интеграцию голоса, видео, текстовых сообщений, веб-доступа и функциональности мобильной телефонии. Новая сеть отвечает потребностям компании с точки зрения скорости связи и эффективности. Кроме того, решение удовлетворяет требованиям SanLucar по использованию «Green Computing». Этого удалось добиться с помощью виртуализации разных элементов, применения телефонов Snom с низким энергопотреблением.

Rivit S.r.l.

Rivit S.r.l. — ведущая компания Италии по производству и распространению креплений, систем быстрого крепежа и сопутствующих инструментов, болтов и шурупов, инструментов и станков для листовой металлообработки. Компания основана в 1973-м и расположена в Оззано дель Эмилия, Болонья. Она обслуживает организации, занимающиеся металлообработкой, кровлями, оконным остеклением, производством автомобилей, установкой кондиционеров, фотогальваникой и производством и оснасткой судов. Компания экспортирует продукцию в более чем 30 стран и продаёт её через сеть дилеров.

В 2006-м Rivit S.r.l. развернула у себя аналоговую телефонную систему, которая быстро устарела. Вскоре компания столкнулась с ограничениями телефонии, но у неё не было возможностей настраивать внутренние компоненты и/или дополнительные телефонные сервисы, которые поддерживали бы рост 41-летней организации. Поэтому в Rivit хотели использовать свою IP-инфраструктуру для создания конфигурируемой, гибкой системы голосовой связи, которая позволяла бы сотрудникам легко коммуницировать с коллегами в офисе и за его пределами.

Rivit обратилась к Centro Computer SPA, компании, оказывающей консультационные услуги по инфраструктуре и телекоммуникациям, и являющейся сертифицированным партнёром Snom, а та установила телефоны Snom UC edition и Microsoft Lync for Unified Communications. Centro порекомендовала Lync и Snom потому, что Rivit уже много инвестировала в технологии Microsoft, а также благодаря функциональности, простоте развёртывания и кастомизации телефонов Snom UC edition. После тщательной оценки компания внедрила 60 телефонов — Snom 710, 720 и 760.

На сегодняшний день только телефоны Snom нативно поддерживают прямой provisioning из Microsoft Lync Server, и могут предоставлять очень важные функции по сравнению с другими Lync-сертифицированных телефонами. Rivit сэкономила на установке дополнительных provisioning-серверов при развёртывании телефонов, что на тот момент являлось главной проблемой для других моделей, сертифицированных для использования с Lync. Более того, Rivit получила возможность настраивать исходящие сообщения, музыку при удержании вызовов и динамические вызовы в соответствии со своими потребностями, а также компания извлекла выгоду из кроссплатформенной возможности видеть присутствие каждого сотрудника и возможности быстрее коммуницировать с клиентами и поставщиками благодаря возможностям Lync.

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


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

Сколько вы тратите на инфраструктуру? И как на этом сэкономить?

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

Обычно сокращение расходов сводится просто к поиску наиболее дешевого решения, тарифа AWS или, если мы говорим о физических стойках, оптимизации конфигурации оборудования. Мало того: фактически, этим занимается кто угодно, как бог на душу положит: если мы говорим о стартапе, то это, вероятно, ведущий девелопер, у которого хватает головняков. В конторах покрупнее этим занимается CMO/CTO, временами в вопрос влезает лично генеральный директор на пару с главбухом. В общем, те люди, у которых и «профильных» забот хватает. И получается, что счета за инфраструктуру растут, но разбираются с этим… те, у кого нет времени с этим разбираться.

Если в офис надо купить туалетную бумагу, этим займется завхоз либо ответственный человек из клининговой компании. Если речь о разработке — лиды и CTO. Продажи — тоже всё понятно. Но ещё с бородатых времен, когда «серверной» называли шкаф, в котором стоял обычный tower-системник с чуть большим количеством оперативной памяти и парой хардов в рейде, все (или, как минимум, многие) игнорируют тот факт, что закупками мощностей должен заниматься тоже специально обученный человек.

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

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

Кто такой FinOps

Допустим, у вас солидное предприятие, о котором продажники с придыханием говорят «энтерпрайз». Вероятно, «по списку» вы прикупили десяток-другой серверов, AWS и ещё кое-что “по мелочи”. Что и логично: в большой компании постоянно происходит какое-то движение — одни команды растут, другие распадаются, третьи переводят на соседние проекты. И вот сочетание этих движений в совокупности с механизмом закупок “по списку” в итоге приводят к новым седым волосам при просмотре очередного ежемесячного счёта за инфраструктуру.

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

Что греха таить: согласование, одобрение и непосредственно оплата заявки внутри компании на тот же тариф AWS — дело не всегда (в реальности — почти никогда) быстрое. И как раз из-за постоянного корпоративного движения часть этих самых приобретений может где-то «потеряться». И банально простаивать. Если бесхозную стойку в своей серверной внимательный админ заметит, то в случае с облачными тарифами все намного печальнее. Они могут стоять «на приколе» месяцами — оплаченные, но в то же время уже никому не нужные в отделе, под который приобретались. При этом коллеги из соседнего кабинета свои пока не поседевшие волосы не только на голове, но и в прочих местах начинают рвать — им уже энную неделю не могут оплатить примерно такой же тариф AWS, нужный позарез.

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

Кто в этом виноват? — Вообще сказать, никто. Так уж пока всё устроено.
Кто от этого страдает? — Все, вся компания.
Кто может исправить ситуацию? — Да-да, FinOps.

FinOps — не просто прослойка между разработчиками и необходимым им оборудованием, а человек или команда, которые будут знать, где, что и насколько хорошо «лежит» в плане тех же облачных тарифов, купленных компанией. Фактически, эти люди должны работать в одной упряжке с DevOps, с одной стороны, и финансовым департаментом с другой, выполняя роль эффективного посредника и, что самое важное — аналитика.

Немного об оптимизации

Облака. Сравнительно дешево и очень удобно. Но это решение перестает быть дешевым, когда количество серверов становится двузначным или трехзначным. К тому же, облака дают возможность использовать всё больше сервисов, которые ранее были недоступны: это и базы данных как сервис (Amazon AWS, Azure Database), serverless-приложения (AWS Lambda, Azure Functions) и многие другие. Они все очень круты тем, что просты в использовании — купил и поехал, никаких проблем. Вот только чем глубже компания и ее проекты погружаются в облака, тем хуже спит финансовый директор. И тем быстрее седеет генеральный.

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

Итак, что в этой ситуации делает FinOps:

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

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

Или другая ситуация: у вас куплены про запас мощности на AWS или Azure, для того чтобы не упасть под пиковой нагрузкой. Можно ли быть уверенным, что это оптимальное решение? Ведь если эти инстансы простаивают 80%, то вы просто дарите деньги Amazon. Тем более, для таких случаев у тех же AWS и Azure есть burstable инстансы — зачем вам вхолостую коптящие серверы, если можно использовать инструмент для решения проблем как раз пиковых нагрузок? Или вместо инстансов On Premise стоит посмотреть в сторону Reserved — они обходятся намного дешевле и на них еще и скидки дают.

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

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

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

Например, Google подвез для компаний платформу Firebase, на которой можно «под ключ» разместить тот же мобильный проект, могущий потребовать быстрого масштабирования. Хранилища, реалтайм БД, хостинг и облачная синхронизация данных на примере этого решения доступны в одном месте.

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

При оптимизации расходов на облачные сервисы вы внезапно можете осознать, что для критически важных для бизнеса приложений можно прикупить и более мощные тарифы, которые обеспечат компании бесперебойный заработок. При этом «наследие» разработки, старые архивы, БД и прочее хранить в дорогостоящих облаках — решение такое себе. Ведь для подобных данных вполне подойдет и стандартный дата-центр с обычными HDD и среднемощным железом без каких-либо «примочек».

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

Что в итоге?

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

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


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

Оптимизация поиска в ширину: как обработать граф с 10 миллиардами состояний

image

Пару месяцев назад мне наконец пришлось признать, что я недостаточно умён, чтобы пройти некоторые уровни головоломки Snakebird. Единственным способом вернуть себе часть самоуважения было написание солвера. Так я мог бы притвориться, что создать программу для решения головоломки — это почти то же самое, что и решить её самому. Код получившейся программы на C++ выложен на Github. Основная часть рассматриваемого в статье кода реализована в search.h и compress.h. В этом посте я в основном буду рассказывать об оптимизации поиска в ширину, который бы потребовал 50-100 ГБ памяти, чтобы он уместился в 4 ГБ.

Позже я напишу ещё один пост, в котором будет описана специфика игры. В этом посте вам нужно знать, что мне не удалось найти никаких хороших альтернатив грубому перебору (brute force), потому что ни один из привычных трюков не сработал. В игре множество состояний, потому что есть куча подвижных или толкаемых объектов, при этом важна форма некоторых из них, которая может меняться со временем. Не было никакой пригодной консервативной эвристики для алгоритмов наподобие A*, позволяющих сузить пространство поиска. Граф поиска был ориентированным и заданным неявно, поэтому одновременный поиск в прямом и обратном направлении оказался невозможным. Единственный ход мог изменить состояние множеством несвязанных друг с другом способов, поэтому не могло пригодиться ничего наподобие хеширования Зобриста.

Приблизительные подсчёты показали, что в самой большой головоломке после устранения всех симметричных положений будет порядка 10 миллиардов состояний. Даже после упаковки описания состояний с максимальной плотностью размер состояния составлял 8-10 байт. При 100 ГБ памяти задача оказалась бы тривиальной, но не для моей домашней машины с 16 ГБ памяти. А поскольку Chrome нужно из них 12 ГБ, мой настоящий запас памяти ближе к 4 ГБ. Всё, что будет превышать этот объём, придётся сохранять на диск (старый и ржавый винчестер).

Как уместить 100 ГБ данных в 4 ГБ ОЗУ? Или а) состояния нужно сжать в 1/20 от их исходного, и так уже оптимизированного размера, или б) алгоритм должен иметь возможность эффективного сохранения состояний на диск и обратно, или в) сочетание двух вышеприведённых способов, или г) мне нужно купить больше ОЗУ или арендовать на несколько дней мощную виртуальную машину. Вариант Г я не рассматривал, потому что он слишком скучный. Варианты А и В были исключены после proof of concept с помощью gzip: фрагмент описания состояний размером 50 МБ сжался всего до 35 МБ. Это примерно по 7 байт на состояние, а мой запас памяти рассчитан примерно на 0,4 байта на состояние. То есть оставался вариант Б, даже несмотря на то, что поиск в ширину казался довольно неудобным для хранения на вторичных накопителях.

Содержание

Это довольно длинный пост, поэтому вот краткий обзор последующих разделов:

  • Поиск в ширину «по учебнику» — какова обычная формулировка поиска в ширину (breadth-first search, BFS), и почему она не подходит для сохранения частей состояния на диск?
  • BFS с сортировкой и слиянием — изменение алгоритма для эффективного пакетного избавления от избыточных данных.
  • Сжатие — снижение объёма используемой памяти в сто раз благодаря сочетанию стандартного и собственного сжатия.
  • Ой-ёй, я сжульничал! — в первых разделах я кое о чём умолчал: нам недостаточно просто знать, где находится решение, а нужно точно понимать, как его достичь. В этом разделе мы обновляем базовый алгоритм, чтобы он передавал достаточно данных для воссоздания решения из последнего состояния.
  • Сортировка + слияние со множественным выводом — хранение большего количества состояний полностью сводит на нет преимущества сжатия. Алгоритм сортировки + слияния необходимо изменить, чтобы он хранил два набора выходных данных: один, хорошо сжатый, используется во время поиска, а другой применяется только для воссоздания решения после нахождения первого.
  • Своппинг — своппинг в Linux намного хуже, чем я думал.
  • Сжатие новых состояний перед слиянием — до данного момента оптимизации памяти работали только со множеством посещённых состояний. Но оказалось, что список новых сгенерированных состояний намного больше, чем можно было подумать. В этом разделе показана схема для более эффективного описания новых состояний.
  • Экономия пространства на родительских состояниях — исследуем компромиссы между использованием ЦП/памяти для воссоздания решения в конце.
  • Что не сработало или может не сработать — некоторые идеи казались многообещающими, но в результате их пришлось откатить, а другие, которые предполагались рабочими из исследований, интуитивно мне кажутся неподходящими в данном случае.

Поиск в ширину «по учебнику»

Как выглядит поиск в ширину, и почему в нём не стоит использовать диск? До этого небольшого проекта я рассматривал только варианты формулировок «из учебников», например, такие:

def bfs(graph, start, end):     visited = {start}     todo = [start]     while todo:         node = todo.pop_first()         if node == end:             return True         for kid in adjacent(node):             if kid not in visited:                 visited.add(kid)                 todo.push_back(kid)     return False

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

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

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

BFS с сортировкой и слиянием

Если бы мы стремились максимально объединять операции доступа к данным в пакеты, то какой бы была максимально достижимая приблизительность? Поскольку программа не знает, какие узлы обрабатывать в слое глубины N+1 до полной обработки слоя N, кажется очевидным, что нужно выполнять дедупликацию состояний по крайней мере один раз за глубину.

Если мы одновременно работаем с целым слоем, то можно отказаться от хеш-таблиц и описать множество visited и новые состояния как какие-нибудь отсортированные потоки (например, как файловые потоки, массивы, списки). Мы можем тривиально найти новое множество visited с помощью объединения множеств потоков и столь же тривиально найти множество todo с помощью разности множеств.

Две операции со множествами можно объединить, чтобы они работали за один проход с обоими потоками. По сути мы заглядываем в оба потока, обрабатываем меньший элемент, а затем продвигаемся вперёд по потоку, из которого был взят элемент (или по обоим потокам, если элементы в их начале одинаковы). В обоих случаях мы добавляем элемент в новое множество visited. Затем продвигаемся вперёд по потоку новых состояний, а также добавляем элемент в новое множество todo:

def bfs(graph, start, end):     visited = Stream()     todo = Stream()     visited.add(start)     todo.add(start)     while True:         new = []         for node in todo:             if node == end:                 return True             for kid in adjacent(node):                 new.push_back(kid)         new_stream = Stream()         for node in new.sorted().uniq():             new_stream.add(node)         todo, visited = merge_sorted_streams(new_stream, visited)     return False  # Merges sorted streams new and visited. Return a sorted stream of # elements that were just present in new, and another sorted # stream containing the elements that were present in either or # both of new and visited. def merge_sorted_streams(new, visited):     out_todo, out_visited = Stream(), Stream()     while visited or new:         if visited and new:             if visited.peek() == new.peek():                 out_visited.add(visited.pop())                 new.pop()             elif visited.peek() < new.peek():                 out_visited.add(visited.pop())             elif visited.peek() > new.peek():                 out_todo.add(new.peek())                 out_visited.add(new.pop())         elif visited:             out_visited.add(visited.pop())         elif new:             out_todo.add(new.peek())             out_visited.add(new.pop())     return out_todo, out_visited

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

Как будет выглядеть теоретическая производительность при упрощённом распределении данных по 100 уровням глубины, в каждом из которых есть по 100 миллионов состояний? Усреднённое состояние будет считываться и записываться 50 раз. Это даёт 10 байт/состояние * 5 миллиардов состояний * 50 = 2,5 ТБ. Мой жёсткий диск предположительно может выполнять чтение и запись со средней скоростью 100 МБ/с, то есть на ввод-вывод в среднем уйдёт (2 * 2,5 ТБ) / (100 МБ/с) =~ 50к/с =~ 13 часов. Это на пару порядков меньше, чем предыдущий результат (четыре месяца)!

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

Сжатие

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

Чтобы протестировать эту теорию, я использовал zstd с головоломкой из 14,6 миллионов состояний, каждое состояние которой имело размер 8 байт. После сортировки они сжались в среднем до 1,4 байта на состояние. Это походит на серьёзный шаг вперёд. Его недостаточно, чтобы выполнять всю программу в памяти, но он может снизить время ввода-вывода диска всего до пары часов.

Можно ли как-то улучшить результат современного алгоритма сжатия общего назначения, если мы что-то знаем о структуре данных? Можно быть практически уверенными в этом. Хорошим примером этого является формат PNG. Теоретически сжатие — это просто стандартный проход Deflate. Но вместо сжатия сырых данных изображение сначала преобразуется с помощью фильтров PNG. Фильтр PNG по сути является формулой для предсказания значения байта сырых данных на основании значения того же байта в предыдущей строке и/или того же байта предыдущего пикселя. Например, фильтр «up» преобразует каждый байт вычитанием из него при сжатии значения предыдущей строки, и выполняя противоположную операцию при распаковке. Учитывая типы изображений, для которых используется PNG, результат почти всегда будет состоять из нулей или чисел, близких к нулю. Deflate может сжимать такие данные намного лучше, чем сырые данные.

Можно ли применить подобный принцип к записям состояний BFS? Похоже, что это должно быть возможно. Как и в случае с PNG, у нас есть постоянный размер строки, и мы можем ожидать, что соседние строки окажутся очень схожими. Первые пробы с фильтром вычитания/прибавления, за которым выполнялся zstd, привели к улучшению показателя сжатия ещё на 40%: 0,87 байт на состояние. Операции фильтрации тривиальны, поэтому с точки зрения потребления ресурсов ЦП они практически «бесплатны».

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

Допустим, у нас есть соседние строки R1 = [1, 2, 3, 4] и R2 = [1, 2, 6, 4]. При выводе R2 мы сравниваем каждый байт с тем же байтом предыдущей строки, и 0 будет обозначать совпадение, а 1 — несовпадение: diff = [0, 0, 1, 0]. Затем мы передаём эту битовую карту, закодированную как VarInt, за которой следуют только байты, не совпавшие с предыдущей строкой. В этом примере мы получим два байта 0b00000100 6. Сам по себе этот фильтр сжимает эталонные данные до 2,2 байт / состояние. Но скомбинировав фильтр + zstd, мы снизили размер данных до 0,42 байт / состояние. Или, если сказать иначе, это составляет 3,36 бит на состояние, что всего немного больше наших примерных вычисленных показателей, необходимых для того, чтобы все данные уместились в ОЗУ.

На практике показатели сжатия улучшаются, потому что отсортированные множества становятся плотнее. Когда поиск достигает точки, где память начинает вызывать проблемы, показатели сжатия могут становиться намного лучше. Самая большая проблема заключается в том, что в конце мы получаем 4,6 миллиардов состояний visited. После сортировки эти состояния занимают 405 МБ и сжимаются по представленной выше схеме. Это даёт нам 0,7 бита на состояние. В конце концов сжатие и распаковка занимают примерно 25% от времени ЦП программы, но это отличный компромисс за снижение потребления памяти в сто раз.

Представленный выше фильтр кажется немного затратным из-за заголовка VarInt в каждой строке. Похоже, что это легко усовершенствовать ценой малых затрат ЦП или небольшого повышения сложности. Я попробовал несколько разных вариантов, транспонирующих данных в порядок по столбцам, или записывающих битовые маски более крупными блоками, и т.д. Эти варианты сами по себе давали гораздо более высокие показатели сжатия, но проявляли себя не так хорошо, когда выходные данные фильтра сжимались zstd. И это не было какой-то ошибкой zstd, результаты с gzip и bzip2 оказались похожими. У меня нет каких-то особо гениальных теорий о том, почему этот конкретный тип кодирования оказался гораздо лучше в сжатии, чем другие варианты.

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

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

Ой-ёй, я сжульничал!

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

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

def bfs(graph, start, end):     visited = {start: None}     todo = [start]     while todo:         node = todo.pop_first()         if node == end:             return trace_solution(node, visited)         for kid in adjacent(node):             if kid not in visited:                 visited[kid] = node                 todo.push_back(kid)     return None  def trace_solution(state, visited):   if state is None:     return []   return trace_solution(start, visited[state]) + [state]

К сожалению, это уничтожит все преимущества сжатия, полученные в предыдущем разделе; в основе их лежит предположение о том, что соседние строки очень похожи. Когда мы просто смотрим на сами состояния, это справедливо. Но нет никаких причин полагать, что это будет истинно для родительских состояний; по сути, они являются случайными данными. Во-вторых, решение с сортировкой + слиянием должно считывать и записывать все просмотренные состояния на каждой итерации. Для сохранения связки состояния/родительского состояния нам придётся считывать и записывать на диск при каждой итерации все эти плохо сжимаемые данные.

Сортировка + слияние со множественным выводом

В самом конце, при возврате назад по решению, программе нужны будут только связки состояний/родительских состояний, Следовательно, мы можем параллельно хранить две структуры данных. Visited по-прежнему будет оставаться множеством посещённых состояний, как и раньше заново вычисляемым во время слияния. Parents — это по большей мере отсортированный список пар состояния/родительского состояния, которые не переписываются. После каждой операции слияния к parents добавляется пара «состояние + родительское состояние».

def bfs(graph, start, end):     parents = Stream()     visited = Stream()     todo = Stream()     parents.add((start, None))     visited.add(start)     todo.add(start)     while True:         new = []         for node in todo:             if node == end:                 return trace_solution(node, parents)             for kid in adjacent(node):                 new.push_back(kid)         new_stream = Stream()         for node in new.sorted().uniq():             new_stream.add(node)         todo, visited = merge_sorted_streams(new_stream, visited, parents)     return None  # Merges sorted streams new and visited. New contains pairs of # key + value (just the keys are compared), visited contains just # keys. # # Returns a sorted stream of keys that were just present in new, # another sorted stream containing the keys that were present in either or # both of new and visited. Also adds the keys + values to the parents # stream for keys that were only present in new. def merge_sorted_streams(new, visited, parents):     out_todo, out_visited = Stream(), Stream()     while visited or new:         if visited and new:             visited_head = visited.peek()             new_head = new.peek()[0]             if visited_head == new_head:                 out_visited.add(visited.pop())                 new.pop()             elif visited_head < new_head:                 out_visited.add(visited.pop())             elif visited_head > new_head:                 out_todo.add(new_head)                 out_visited.add(new_head)                 out_parents.add(new.pop())         elif visited:             out_visited.add(visited.pop())         elif new:             out_todo.add(new.peek()[0])             out_visited.add(new.peek()[0])             out_parents.add(new.pop())     return out_todo, out_visited

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

Своппинг

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

Но этого не происходит, по крайней мере, в Linux. В какой-то момент (до того, как рабочее множество данных удалось сжать до размеров памяти) я добился того, чтобы программа выполнялась примерно за 11 часов, а данные сохранялись в основном на диск. Затем я сделал так, чтобы программа использовала анонимные страницы, а не хранимые в файлах, и выделил на том же диске файл подкачки достаточного размера. Однако спустя три дня программа прошла всего четверть пути, и всё равно со временем становилась медленнее. По моим оптимистичным оценкам она должна была закончить работу за 20 дней.

Уточню — это был тот же код и точно тот же паттерн доступа. Единственное, что изменилось — память сохранялась не как явный дисковый файл, а как своп. Почти не требует доказательств тот факт, что своппинг совершенно рушит производительность в Linux, в то время как обычный файловый ввод-вывод этого не делает. Я всегда предполагал, что так происходит из-за того, что программы склонны считать ОЗУ памятью с произвольным доступом. Но здесь не тот случай.

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

Теперь я знаю: своппинг в Linux скорее всего не будет хорошо работать даже в оптимальных условиях. Если части адресного пространства скорее всего будут выгружаться на какое-то время на диск, то лучше сохранять их вручную в файлы, чем доверять свопу. Я осуществил это, реализовав собственный класс векторов, который изначально работает только в памяти, а после превышения определённого порога размера переключается на mmap во временный отдельный файл.

Сжатие новых состояний перед слиянием

В упрощённой модели производительности мы предполагали, что на каждую глубину будет приходиться 100 миллионов новых состояний. Оказалось, что это не очень далеко от реальности (в самой сложной головоломке максимум 150 с лишним миллионов уникальных новых состояний на одном слое глубины). Но измерять нужно не это; рабочее множество до слияния связано не только с уникальными состояниями, но и со всеми состояниями, выведенными для этой итерации. Этот показатель достигает величины в 880 миллионов выходных состояний на слой глубины. Эти 880 миллионов состояний а) нужно обрабатывать при паттерне случайного доступа для сортировки, б) нельзя эффективно сжать из-за отсутствия сортировки, в) нужно хранить вместе с родительским состоянием. Это рабочее множество составляет примерно 16 ГБ.

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

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

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

Экономия пространства на родительских состояниях

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

Нам нужно связать состояние S’ на глубине D+1 с его родительским состоянием S на глубине D. Если мы сможем проитерировать все возможные родительские состояния S’, то у нас получится проверить, появляется ли какое-нибудь из них на глубине D во множестве visited. (Мы уже создали множество visited, сгруппированное по глубинам как удобный побочный продукт вывода связок состояния/родительского состояния при слиянии). К сожалению, для этой задачи такой подход не сработает; нам попросту слишком трудно сгенерировать все возможные состояния S для заданного S’. Впрочем, для многих других задач поиска такое решение может и сработать.

Если мы можем генерировать переходы между состояниями только вперёд, но не назад, то почему бы тогда не сделать только это? Давайте итеративно обойдём все состояния на глубине D и посмотрим, какие выходные состояния у них получаются. Если какое-то состояние на выходе даёт S’, то мы нашли подходящее S. Проблема этого плана в том, что он увеличивает общее потребление ресурсов процессора программой на 50%. (Не на 100%, потому что в среднем мы найдём S, просмотрев половину состояний на глубине D).

Поэтому мне не нравится не один из предельных случаев, но здесь, по крайней мере, возможен компромисс между ЦП/памятью. Существует ли более приемлемое решение где-то посередине? В конце концов я решил хранить не пару (S’, S), а пару (S’, H(S)), где H — 8-битная хеш-функция. Чтобы найти S для заданного S’, мы снова итеративно обходим все состояния на глубине D. Но прежде чем делать что-то другое, вычислим тот же хеш. Если выходной результат не соответствует H(S), то это не то состояние, которое мы ищем, и мы можем просто его пропустить. Эта оптимизация означает, что затратные повторные вычисления нужно выполнять только для 1/256 состояний, что составляет незначительное повышение нагрузки на ЦП, и в то же время снижает объём памяти для хранения родительских состояний с 8-10 байт до 1 байта.

Что не сработало или может не сработать

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

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

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

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

Ещё одна серьёзная альтернатива основывается на временных хеш-таблицах. Посещённые состояния хранятся без сортировки в файле. Сохраняем выходные данные, полученные из глубины D в хеш-таблицу. Затем итеративно обходим посещённые состояния и ищем их в хеш-таблице. Если элемент найден в хеш-таблице, то удаляем его. После итеративного обхода всего файла в нём останутся только недублируемые элементы. Затем их добавляют к файлу и используют для инициализации списка todo для следующей итерации. Если количество выходных данных так велико, что хеш-таблица не помещается в памяти, то и файлы, и хеш-таблицы можно разбить на части, пользуясь одинаковым критерием (например, верхними битами состояния), и обрабатывать каждую часть по отдельности.

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

Ещё одной стоящей внимания областью исследований показалась оптимизация запросов к базе данных. Похоже. что задача дедупликации сильно связана с join баз данных, в котором тоже есть совершенно такая же дилемма «сортировка против хеширования». Очевидно, что некоторые из этих исследований можно перенести и на задачу поиска. Разница может заключаться в том, что выходные данные join баз данных являются временными, в то время как выходные данные дедупликации BFS сохраняются до конца вычислений. Похоже, это меняет баланс компромиссов: теперь он касается не только наиболее эффективной обработки одной итерации, но и создания наиболее оптимального формата выходных данных для следующей итерации.

Заключение

На этом я завершаю изложение того, что я узнал из проекта, который в общем случае применим и к другим задачам поиска грубым перебором. Сочетание этих трюков позволило снизить объём решений самых сложных головоломок игры с 50-100 ГБ до 500 МБ и плавно увеличивать затраты, если задача превышает доступную память и записывается на диск. Кроме того, моё решение на 50% быстрее, чем наивная дедупликация состояний на основе хеш-таблиц даже для головоломок, помещающихся в памяти.

Игру Snakebird можно приобрести в Steam, Google Play и в App Store. Рекомендую её всем, кому интересны очень сложные, но честные головоломки.


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