How to install windows 10 to contabo vps

We can install windows 10 for you just 20 USD ($100 — tutoring, 1 hour), Windows Server 2016/2019 — 50 USD if you use our referral link for buy contabo vps or use free how to for install windows youself

If you want to make order for install windows to your VPS, we need to get login and password of your Contabo account. Or you need reboot your vps to rescue mode (clonzilla) yourself and give us VNC IP:port and VNC Password.and give us VNC IP:port and VNC Password.

Available payments methods:
Paypal; BinancePay, Daedalus and Crypto Currency (BTC, USDT, ETH, ETX, BUSD, DAI, BNB)

My contact email: pumainthailand.com@gmail.com
Telegram: https://t.me/stufently
VK: https://vk.com/pumainthailand
WhatsApp: https://wa.me/79529476185

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

  1. Boot to rescue (Clonzilla)
  2. Connect trough vnc client
  3. Enter shell
  4. Login to root
    1. $sudo su —
  5. Make partition(all command in script see bellow)
    1. Using parted or fdisk
      1. #parted /dev/sda
    2. Make mbr table
    3. Create 3 partition
      1. 0G -> 8G ntfs 
      2. 8G -> max-size -20GB ntfs 
      3. max-size -20gb > max-size  ext4
    4. Make 1 partition bootable and leave parted
      1. (parted) set 1 boot on
    5.  
    6. Format drives
      1. #mkfs.ntfs -f /dev/sda1
      2. #mkfs.ntfs -f /dev/sda2
      3. #mkfs.ext4 /dev/sda3
    7. Mount them
      1. #mkdir /mnt/sda1 && mount /dev/sda1 /mnt/sda1
      2. #mkdir /mnt/sda3 && mount /dev/sda3 /mnt/sda3
    8. Go to temporary storage
      1. #cd /mnt/sda3
    9. Download windows iso
      1. #wget https://aknews.ru/win/1909_64
    10. Download virtio drivers 
      1. #wget https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso
    11. Mount iso
      1. #mkdir /mnt/cd
      2. #mount -o loop /mnt/sda3/1903-iso-64 /mnt/cd
    12. Copy all files from cd to first partition amd unmount cd
      1. #cp -av /mnt/cd/* /mnt/sda1/
      2. #umount /mnt/cd
    13. Install wimtools
      1. #apt install software-properties-common
      2. #add-apt-repository ppa:nilarimogard/webupd8
      3. #apt install wimtools
    14. #mkdir /mnt/wim
    15. Mount virtio iso and copy it to sda1
      1. #mount /mnt/sda3/virtio-win.iso /mnt/cd
      2. #mkdir /mnt/sda1/virtio && cp -av /mnt/cd/* /mnt/sda1/virtio
    16.  copy to wim
      1. #wimmountrw /mnt/sda1/sources/boot.wim 1 /mnt/wim
      2. #mkdir /mnt/wim/virtio
      3. # cp -av /mnt/cd/* /mnt/wim/virtio
      4. #wimunmount —commit /mnt/wim
  1. Copy to wim 2
    1. #wimmountrw /mnt/sda1/sources/boot.wim 2 /mnt/wim
    2. #mkdir /mnt/wim/virtio
    3. # cp -av /mnt/cd/* /mnt/wim/virtio
    4. #wimunmount —commit /mnt/wim
  1. #sync
  2. Unmount all
    1. #cd /tmp
    2. #umount /mnt/cd
    3. #umount /mnt/sda1
  3. Download ms-sys app
    1. #wget http://prdownloads.sourceforge.net/ms-sys/ms-sys-2.6.0.tar.gz

#chmod +x ms-sys

  1. Make boot sections
    1. #ms-sys -n /dev/sda1
    2. #ms-sys -7 /dev/sda

#sync
#reboot

Process installation over vnc as usual

Browse driver in X:/virtio/amd64/w10/

You can delete partition 3 and create new partition 2 with size of part 2+ part 3

Partition 1 is still needed

After windows installed 

You can add first partition in disk manager with letter D:

After perform autosearch network card driver on D:\virtio

Remove D: driver letter

Configure RDP

Note*: to use RDP install atleast win PRO

 

 5*  (Part 5 in short)

Partition (according to disk size 215GB)

#wget http://aknews.ru/win/towin.sh

#chmod +x towin.sh

#./towin.sh

Check partition tables

  • Install

#wget http://aknews.ru/win/towin2.sh

!Probably iso link can be broken so replace it

#chmod +x towin2.sh

#./towin2.sh

#reboot

 

Самая популярная мобильная игра: как создавалась «Змейка» для телефонов Nokia

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

В конце концов, их папа создал Snake.

В 1995 году разработчик ПО из Финляндии по фамилии Арманто устроился на работу в быстрорастущую компанию Nokia. Он имел опыт создания игр, поэтому ему поручили разработать «несколько крутых игр» для будущего мобильного телефона Nokia 6110. Он воспринимал свои игрушки как ещё один способ использования устройства, не сильно отличающимся от создаваемых коллегами календаря и калькулятора.

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

Арманто сдержан и скромен, когда рассказывает о своей карьере. «Очень многие приходят в восторг, когда узнают, что это я написал Snake», — говорит он. «И я думаю, это вполне нормально. Здорово знать, что людям нравилась эта игра».

Телефон, который всё изменил

Мы начнём историю с 1995 года, когда в Nokia прошло всего три года после глобальной модернизации компании. Её новая миссия: целиком сосредоточиться на мобильных технологиях. Хотя мобильные телефоны пока не достигли всемирного признания, они быстро становились меньше, дешевле и проще в использовании. После выпуска в 1993 году Nokia 2110 компания начала работу над Nokia 6110, который должен был дебютировать в декабре 1997 года.

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

Nokia 6110 в синем корпусе.

Арманто: в 1995 году Nokia быстро росла. Все мы были в компании новичками. Это было потрясающе! Мы разрабатывали новые функции для телефонов и должны были носить с собой новейшие устройства, потому что им, разумеется, требовалось тестирование.

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

Я решил, что отлично подойдёт Tetris, даже реализовал и протестировал его. Но в результате от него пришлось отказаться. Tetris Company хотела получать долю от каждого проданного устройства, а Nokia не хотела привязывать никакие платежи к точному количеству проданных продуктов.

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

Ранняя история Snake

Игра Snake существовала и до 1995 года. Впервые она появилась в 1976 году на аркадном автомате Blockade, породившем несколько клонов. Как бы они ни назывались: Nibbler, Worm или Rattler Race, основная концепция оставалась одинаковой.

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

Раньше я играл в игру наподобие Snake на своём Apple Macintosh. Она была для двух игроков, каждый из которых при помощи клавиатуры управлял собственной змеёй.

Кайл Макнейл, Vice, «Признание в любви к Snake, лучшей мобильной игре”: Snake в её простейшей, самой чистой форме видеоигры, увлекательна, её игровой процесс похож на Pong или Space Invaders. Цепочка пикселей, приятная физика и чувство постепенного увеличения сложности игры».

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

Создание игры

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

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

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

Nokia начала продавать 6110 в начале 1998 года и с самого начала Snake находилась в стандартной комплектации телефона.

Макнейл: сегодня встроенная в сотовый телефон игра воспринимается как некий возврат к простоте. Нет необходимости ничего скачивать, никаких внутриигровых покупок, нет обновлений и траты мобильного трафика. Достаточно взять в руки Nokia и начать играть.

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

«Я никогда не думал, что она станет так популярна»

Возможно, всё дело было в массовом распространении телефона, простоте игры или сочетании этих двух факторов, но Snake превратилась в феномен. Люди впервые начали очень долго смотреть на экраны своих телефонов.

Ханну Корхонен, статья «Evaluating Playability of Mobile Games with the Expert Review Method», опубликованная Association for Computing Machinery: Эпоха игр на мобильных телефонах началась в 1997 году, когда Nokia выпустила на телефоне Nokia 6110 первую мобильную игру под названием Snake. Вероятно, это самая часто играемая мобильная игра, потому что она доступна на более чем 350 миллионах мобильных телефонов по всему миру.

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

Макнейл: это идеальная мобильная игра, потому что в неё играли все, и особенно люди, в жизни не игравшие больше ни в одну игру. Она встроилась в сознание общества так же, как встроена в Nokia 6110.

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

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

Из пресс-релиза Nokia 2005 года, посвящённого Nokia N-Gage: мистер Танели Арманто, создатель самой популярной в мире мобильной игры Snake получил особый знак признания от Mobile Entertainment Forum (MEF) за ключевую роль в создании Snake для серии телефонов Nokia 6100. Форум MEF сообщил, что гениальность и видение мистера Арманто внесли значительный вклад в рост индустрии мобильных развлечений. Вчера мистер Арманто получил награду на ежегодном мероприятии MEF в Лондоне.

Арманто проработал в Nokia почти 16 лет, потом ушёл на вольные хлеба, а позже вернулся к учёбе. Сегодня он работает архитектором систем в Ineo Oy.

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


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

Вышел окончательный релиз Jakarta EE 8

The Eclipse Foundation выпустила новые спецификации Jakarta EE 8 platform & web-profile.
Они также представили сервер приложений Jakarta EE и набор тестов(TCK) для проверки других совместимых реализаций.


10 сентября 2019 года была выпущена Jakarta EE 8.

Спецификации Jakarta EE можно получить на их веб-сайте.
Они варьируются от Jakarta Anntotations до Jakarta Server Pages.

Наряду со спецификациями Eclipse выпустили Eclipse Glassfish Server 5.1, сервер приложений с открытым исходным кодом, который реализует Jakarta EE 8 platform.
Ожидается разработка Jakarta EE 8 серверов от различных поставщиков Java-серверов. Так, сервер IBM Open Liberty уже был сертифицирован как Jakarta EE 8-совместимый.

Eclipse согласилась взять на себя разработку в 2017 году. Вместо того чтобы внедрять более новую Jakarta EE 9, Eclipse стремится к более плавной миграции, постепенно заменяя Java EE на Jakarta EE.

Развитие Jakarta EE

Jakarta EE будет использоваться(по мнению Eclipse) для построения современных решений, таких как облачные сервисы и микросервисы.

Будущие возможности для обновления Jakarta EE включают обновление Java SE, которое лежит в основе Jakarta EE.
Текущая версия — Java SE 8, следующий кандидат — Java SE 11(это следующая после SE 8 сборка с долгосрочным циклом поддержки).

Также будет улучшена работа с облачными технологиями, такими как Kubernetes, Docker.

Ожидается, что отдельные спецификации будут также улучшены, например Jakarta RESTfull Web Services.

Кроме того, Eclipse Foundation должен определить процесс изменения пространств имен javax на jakarta. Это может потребовать перекомпиляции программ, хотя Eclipse работает над обратной совместимостью, чтобы уменьшить тяжесть миграции.
Пространство имен javax обратно совместимо в течении 20 лет, но является собственностью Oracle.

Где скачать Glassfish 5.1

Вы можете скачать Eclipse GlassFish 5.1 с веб-сайта проекта.
К TCK можно также получить доступ на этом сайте.


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

Понимание брокеров сообщений. Изучение механики обмена сообщениями посредством ActiveMQ и Kafka. Глава 3. Kafka

Продолжение перевода небольшой книги:
«Understanding Message Brokers»,
автор: Jakub Korab, издательство: O’Reilly Media, Inc., дата издания: June 2017, ISBN: 9781492049296.

Предыдущая переведенная часть: Понимание брокеров сообщений. Изучение механики обмена сообщениями посредством ActiveMQ и Kafka. Глава 1. Введение

Kafka была разработана в LinkedIn для того, чтобы обойти некоторые ограничения традиционных брокеров сообщений и избежать необходимости настраивать несколько брокеров сообщений для разных взаимодействий «точка-точка», что описано в данной книге в разделе «Вертикальное и горизонтальное масштабирование» на странице 28. Сценарии использования в LinkedIn в основном основывались на однонаправленном поглощении очень больших объемов данных, таких как клики на страницах и журналы доступа, в то же время позволяя использовать эти данные нескольким системам, не влияя на производительность продюсеров или других консюмеров. Фактически, причина существования Kafka заключается в том, чтобы получить такую архитектуру обмена сообщениями, которую описывает Universal Data Pipeline.
С учетом этой конечной цели, естественно, возникли и другие требования. Kafka должна:

  • Быть чрезвычайно быстрой
  • Предоставлять большую пропускную способность при работе с сообщениями
  • Поддерживать модели «Издатель-Подписчик» и «Точка-Точка»
  • Не замедляться с добавлением потребителей. Например, производительность и очереди, и топика в ActiveMQ ухудшается при росте количества потребителей на адресате
  • Быть горизонтально масштабируемой; если один брокер, сохраняющий (persists) сообщения, может делать это только на максимальной скорости диска, то для увеличения производительности имеет смысл выйти за пределы одного экземпляра брокера
  • Разграничивать доступ к хранению и повторному извлечению сообщений

Чтобы достигнуть всего этого, в Kafka принята архитектура, которая переопределила роли и обязанности клиентов и брокеров обмена сообщениями. Модель JMS очень ориентирована на брокер, где он отвечает за распространение сообщений, а клиенты должны беспокоиться только об отправке и получении сообщений. Kafka, с другой стороны, ориентирована на клиента, при этом клиент берет на себя многие функции традиционного брокера, такие как справедливое распределение соответствующих сообщений среди потребителей, в обмен получая чрезвычайно быстрый и масштабируемый брокер. Для людей, работавших с традиционными системами обмена сообщениями, работа с Kafka требует фундаментальных изменений во взглядах.
Это инженерное направление привело к созданию инфраструктуры обмена сообщениями, способной на много порядков увеличить пропускную способность по сравнению с обычным брокером. Как мы увидим, этот подход сопряжен с компромиссами, которые означают, что Kafka не подходит для определенных типов нагрузок и установленного ПО.

Унифицированная модель адресата

Чтобы выполнить требования, описанные выше, Kafka объединила обмен сообщениями типа «публикация-подписка» и «точка-точка» в рамках одного вида адресата — топика. Это сбивает с толку людей, работавших с системами обмена сообщениями, где слово «топик» относится к широковещательному механизму, из которого (из топика) чтение не является надежным (is nondurable). Топики Kafka следует рассматривать как гибридный тип адресата, в соответствии с определением, данным во введении к этой книге.

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

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

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

Эта модель полностью отличается от ActiveMQ, где сообщения из всех очередей хранятся в одном журнале, а брокер помечает сообщения, как удаленные, после того как они были прочитаны.
Давайте теперь немного углубимся и рассмотрим журнал топика более подробно.
Журнал Kafka состоит из нескольких партиций (Рисунок 3-1). Kafka гарантирует строгую упорядоченность в каждой партиции. Это означает, что сообщения, записанные в партицию в определенном порядке, будут прочитаны в том же порядке. Каждая партиция реализована в виде цикличного (rolling) файла журнала, который содержит подмножество (subset) всех сообщений, отправленных в топик его продюсерами. Созданный топик содержит по-умолчанию одну партицию. Идея партиций — это центральная идея Kafka для горизонтального масштабирования.


Figure 3-1. Партиции Kafka

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

Чтение сообщений

Клиент, который хочет прочитать сообщения, управляет именованным указателем, называемым группа консюмеров (consumer group), который указывает на смещение (offset) сообщения в партиции. Смещение — это позиция с возрастающим номером, которая начинается с 0 в начале партиции. Эта группа консюмеров, на которую ссылаются в API через определяемый пользователем идентификатор group_id, соответствует одному логическому потребителю или системе.
Большинство систем, использующих обмен сообщениями, читают данные из адресата посредством нескольких экземпляров и потоков для параллельной обработки сообщений. Таким образом, обычно будет много экземпляров консюмеров, совместно использующих одну и ту же группу консюмеров.
Проблему чтения можно представить следующим образом:

  • Топик имеет несколько партиций
  • Использовать топик может одновременно множество групп консюмеров
  • Группа консюмеров может иметь несколько отдельных экземпляров

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

Консюмеры и группы консюмеров

Давайте возьмем в качестве отправной точки топик с одной партицией (Figure 3-2).


Figure 3-2. Консюмер читает из партиции

Когда экземпляр консюмера подключается со своим собственным group_id к этому топику, ему назначается партиция для чтения и смещение в этой партиции. Положение этого смещения конфигурируется в клиенте, как указатель на самую последнюю позицию (самое новое сообщение) или самую раннюю позицию (самое старое сообщение). Консюмер запрашивает (polls) сообщения из топика, что приводит к их последовательному чтению из журнала.
Позиция смещения регулярно коммитится обратно в Kafka и сохраняется, как сообщения во внутреннем топике _consumer_offsets. Прочитанные сообщения все равно не удаляются, в отличие от обычного брокера, и клиент может перемотать (rewind) смещение, чтобы повторно обработать уже просмотренные сообщения.
Когда подключается второй логический консюмер, используя другой group_id, он управляет вторым указателем, который не зависит от первого (Figure 3-3). Таким образом, топик Kafka действует как очередь, в которой существует один консюмер и, как обычный топик pub-sub, на который подписаны несколько консюмеров, с дополнительным преимуществом, что все сообщения сохраняются и могут обрабатываться несколько раз.


Figure 3-3. Два консюмера в разных группах консюмеров читают из одной партиции

Консюмеры в группе консюмеров

Когда один экземпляр консюмера читает данные из партиции, он полностью контролирует указатель и обрабатывает сообщения, как описано в предыдущем разделе.
Если несколько экземпляров консюмеров были подключены с одним и тем же group_id к топику с одной партицией, то экземпляру, который подключился последним, будет передан контроль над указателем и с этого момента он будет получать все сообщения (Figure 3-4).


Figure 3-4. Два консюмера в одной и той же группе консюмеров читают из одной партиции

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

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

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

Партиционирование

Партиции являются основным механизмом распараллеливания чтения и масштабирования топика за пределы пропускной способности одного экземпляра брокера. Чтобы лучше понять это, давайте рассмотрим ситуацию, когда существует топик с двумя партициями и на этот топик подписывается один консюмер (Figure 3-5).


Figure 3-5. Один консюмер читает из нескольких партиций

В этом сценарии консюмеру дается контроль над указателями, соответствующими его group_id в обоих партициях, и начинается чтение сообщений из обеих партиций.
Когда в этот топик добавляется дополнительный консюмер для того же group_id, Kafka переназначает (reallocate) одну из партиций с первого на второй консюмер. После чего каждый экземпляр консюмера будет вычитывать из одной партиции топика (Figure 3-6).
Чтобы обеспечить обработку сообщений параллельно в 20 потоков, вам потребуется как минимум 20 партиций. Если партиций будет меньше, у вас останутся консюмеры, которым не над чем работать, что описано ранее в нашем обсуждении монопольных консюмеров.


Figure 3-6. Два консюмера в одной и той же группе консюмеров читают из разных партиций

Эта схема значительно снижает сложность работы брокера Kafka по сравнению с распределением сообщений, необходимым для поддержки очереди JMS. Здесь не нужно заботится о следующих моментах:

  • Какой консюмер должен получить следующее сообщение, основываясь на циклическом (round-robin) распределении, текущей емкости буферов предварительной выборки или предыдущих сообщениях (как для групп сообщений JMS).
  • Какие сообщения отправлены каким консюмерам и должны ли они быть доставлены повторно в случае сбоя.

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

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

Ответственность за решение, в какую партицию отправить сообщение, возлагается на продюсер этого сообщения. Чтобы понять механизм, с помощью которого это делается, нам сначала нужно рассмотреть, что именно мы на самом деле отправляем.
В то время, как в JMS мы используем структуру сообщения с метаданными (заголовками и свойствами) и телом, содержащим полезную нагрузку (payload), в Kafka сообщение является парой «ключ-значение». Полезная нагрузка сообщения отправляется, как значение (value). Ключ, с другой стороны, используется главным образом для партиционирования и должен содержать специфичный для бизнес-логики ключ, чтобы поместить связанные сообщений в ту же партицию.
В Главе 2 мы обсуждали сценарий онлайн-ставок, когда связанные события должны обрабатываться по порядку одним консюмером:

  1. Учетная запись пользователя настроена.
  2. Деньги зачисляются на счет.
  3. Делается ставка, которая выводит деньги со счета.

Если каждое событие представляет собой сообщение, отправленное в топик, то в этом случае естественным ключом будет идентификатор учетной записи.
Когда сообщение отправляется с использованием Kafka Producer API, оно передается функции партиционирования, которая, учитывая сообщение и текущее состояние кластера Kafka, возвращает идентификатор партиции, в которую должно быть отправлено сообщение. Эта функция реализована через интерфейс Partitioner в Java.
Этот интерфейс выглядит следующим образом:

interface Partitioner {     int partition(String topic,         Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster); }

Реализация Partitioner для определения партиции использует по-умолчанию алгоритм хеширования ключа (general-purpose hashing algorithm over the key) или циклический перебор (round-robin), если ключ не указан. Это значение по-умолчанию работает хорошо в большинстве случаев. Однако, в будущем вы захотите написать свой собственный.

Написание собственной стратегии партиционирования

Давайте рассмотрим пример, когда вы хотите отправить метаданные вместе с полезной нагрузкой сообщения. Полезная нагрузка в нашем примере — это инструкция для внесения депозита на игровой счет. Инструкция — это то, что мы хотели бы гарантированно не модифицировать при передаче и хотим быть уверены, что только доверенная вышестоящая система может инициировать эту инструкцию. В этом случае отправляющая и принимающая системы согласовывают использование подписи для проверки подлинности сообщения.
В обычном JMS мы просто определяем свойство «подпись сообщения» и добавляем его к сообщению. Тем не менее, Kafka не предоставляет нам механизм для передачи метаданных — только ключ и значение.
Поскольку значение — это полезная нагрузка банковского перевода (bank transfer payload), целостность которой мы хотим сохранить, у нас не остается другого выбора, кроме определения структуры данных для использования в ключе. Предполагая, что нам нужен идентификатор учетной записи для партиционирования, так как все сообщения, относящиеся к учетной записи, должны обрабатываться по порядку, мы придумаем следующую структуру JSON:

{   "signature": "541661622185851c248b41bf0cea7ad0",   "accountId": "10007865234" }

Поскольку значение подписи будет варьироваться в зависимости от полезной нагрузки, дефолтная стратегия хеширования интерфейса Partitioner не будет надежно группировать связанные сообщения. Поэтому нам нужно будет написать свою собственную стратегию, которая будет анализировать этот ключ и разделять (partition) значение accountId.

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

Пользовательская стратегия партиционирования должна гарантировать, что все связанные сообщения окажутся в одной партиции. Хотя это кажется простым, но требование может быть усложнено из-за важности упорядочивания связанных сообщений и того, насколько фиксировано количество партиций в топике.
Количество партиций в топике может изменяться со временем, так как их можно добавить, если трафик выходит за пределы первоначальных ожиданий. Таким образом, ключи сообщений могут быть связаны с партицией, в которую они были первоначально отправлены, подразумевая часть состояния, которое должно быть распределено между экземплярами продюсера.
Другим фактором, который следует учитывать, является равномерность распределения сообщений между партициями. Как правило, ключи не распределяются равномерно по сообщениям, и хеш-функции не гарантируют справедливое распределение сообщений для небольшого набора ключей.
Важно отметить, что, как бы вы ни решили разделить сообщения, сам разделитель, возможно, придется использовать повторно.
Рассмотрим требование репликации данных между кластерами Kafka в разных географических расположениях. Для этой цели Kafka поставляется с инструментом командной строки под названием MirrorMaker, который используется для чтения сообщений из одного кластера и передачи их в другой.
MirrorMaker должен понимать ключи реплицируемого топика, чтобы поддерживать относительный порядок между сообщениями при репликации между кластерами, поскольку количество партиций для этого топика может не совпадать в двух кластерах.
Пользовательские стратегии партиционирования встречаются относительно редко, так как дефолтные хеширование или циклический перебор успешно работают в большинстве сценариев. Однако, если вам требуются строгие гарантии упорядочивания или вам необходимо извлечь метаданные из полезных нагрузок, то партиционирование — это то, на что вам следует взглянуть более подробно.
Преимущества масштабируемости и производительности Kafka обусловлены переносом некоторых обязанностей традиционного брокера на клиента. В этом случае принимается решение о распределении потенциально связанных сообщений по нескольким консюмерам, работающим параллельно.

JMS брокеры также должны иметь дело с такими требованиями. Интересно, что механизм отправки связанных сообщений одному и тому же консюмеру, реализованный через JMS Message Groups (разновидность стратегии балансировки sticky load balancing (SLB)), также требует, чтобы отправитель помечал сообщения, как связанные. В случае JMS, брокер отвечает за отправку этой группы связанных сообщений одному консюмеру из многих и передачу прав собственности на группу если консюмер отвалился.

Соглашения по продюсеру

Партиционирование — это не единственное, что необходимо учитывать при отправке сообщений. Давайте рассмотрим методы send () класса Producer в Java API:

Future < RecordMetadata > send(ProducerRecord < K, V > record); Future < RecordMetadata > send(ProducerRecord < K, V > record, Callback callback);

Следует сразу отметить, что оба метода возвращают Future, что указывает на то, что операция отправки не выполняется немедленно. В результате получается, что сообщение (ProducerRecord) записывается в буфер отправки для каждой активной партиции и передается брокеру фоновым потоком в библиотеке клиента Kafka. Хотя это делает работу невероятно быстрой, это означает, что неопытно написанное приложение может потерять сообщения, если его процесс будет остановлен.
Как всегда, есть способ сделать операцию отправки более надежной за счет производительности. Размер этого буфера можно установить в 0, и поток отправляющего приложения будет вынужден ждать, пока передача сообщения брокеру не будет завершена, следующим образом:

RecordMetadata metadata = producer.send(record).get();

Еще раз о чтении сообщений

Чтение сообщений имеет дополнительные сложности, о которых необходимо порассуждать. В отличие от API JMS, который может запускать слушателя сообщений (message listener) в ответ на поступление сообщения, интерфейс Consumer Kafka только опрашивает (polling). Давайте подробнее рассмотрим метод poll (), используемый для этой цели:

ConsumerRecords < K, V > poll(long timeout);

Возвращаемое значение метода — это контейнерная структура, содержащая несколько объектов ConsumerRecord из потенциально нескольких партиций. ConsumerRecord сам по себе является объектом-холдером для пары ключ-значение с соответствующими метаданными, такими, как партиция, из которой он получен.
Как обсуждалось в Главе 2, мы должны постоянно помнить, что происходит с сообщениями после их успешной или неуспешной обработки, например, если клиент не может обработать сообщение или если он прерывает работу. В JMS это обрабатывалось через режим подтверждения (acknowledgement mode). Брокер либо удалит успешно обработанное сообщение, либо повторно доставит необработанное или зафейленное (при условии, что были использованы транзакции). Kafka работает совсем по-другому. Сообщения не удаляются в брокере после вычитки и ответственность за то, что происходит при сбое, лежит на самом вычитывающем коде.
Как мы уже говорили, группа консюмеров связана со смещением в журнале. Позиция в журнале, связанная с этим смещением, соответствует следующему сообщению, которое будет выдано в ответ на poll (). Решающее значение при чтении имеет момент времени, когда это смещение увеличивается.
Возвращаясь к модели чтения, рассмотренной ранее, обработка сообщения состоит из трех этапов:

  1. Извлечь сообщение для чтения.
  2. Обработать сообщение.
  3. Подтвердить сообщение.

Консюмер Kafka поставляется с опцией конфигурации enable.auto.commit. Это часто используемая настройка по умолчанию, как это обычно бывает с настройками, содержащими слово «авто».
До Kafka 0.10 клиент, использовавший этот параметр, отправлял смещение последнего прочитанного сообщения при следующем вызове poll () после обработки. Это означало, что любые сообщения, которые были извлечены (fetched), имели возможность быть повторно обработанными, если клиент обработал сообщения, но неожиданно был уничтожен перед вызовом poll (). Поскольку брокер не сохраняет никакого состояния относительно того, сколько раз сообщение было прочитано, следующий консюмер, который извлекает это сообщение, не будет знать, что произошло что-то плохое. Это поведение было псевдо-транзакционным. Смещение коммитилось только в случае успешной обработки сообщения, но если клиент прерывал работу, брокер снова отправлял то же самое сообщение другому клиенту. Такое поведение соответствовало гарантии доставки сообщений «по крайней мере один раз«.
В Kafka 0.10 код клиента был изменен таким образом, что коммит стал периодически запускаться библиотекой клиента, в соответствии с настройкой auto.commit.interval.ms. Это поведение находится где-то между режимами JMS AUTO_ACKNOWLEDGE и DUPS_OK_ACKNOWLEDGE. При использовании автокоммита сообщения могли быть подтверждены независимо от того, были ли они фактически обработаны — это могло произойти в случае медленного консюмера. Если консюмер прерывал работу, сообщения извлекались следующим консюмером, начиная с закоммиченной позиции, что могло привести к пропуску сообщения. В этом случае Kafka не теряла сообщения, читающий код просто не обрабатывал их.
Этот режим имеет те же перспективы, что и в версии 0.9: сообщения могут быть обработаны, но в случае сбоя, смещение может быть не закоммичено, что потенциально может привести к задвоению доставки. Чем больше сообщений вы извлекаете при выполнении poll (), тем больше эта проблема.
Как обсуждалось в разделе «Чтение сообщений из очереди» на стр. 21, в системе обмена сообщениями нет такого понятия, как однократная доставка сообщения, если принять во внимание режимы сбоев.
В Kafka есть два способа зафиксировать (закоммитить) смещение (оффсет): автоматически и вручную. В обоих случаях сообщения могут обрабатываться несколько раз, в том случае, если сообщение было обработано, но произошел сбой до коммита. Вы также можете вообще не обрабатывать сообщение, если коммит произошел в фоне и ваш код был завершен до того, как он приступил к обработке (возможно в Kafka 0.9 и более ранних версиях).
Управлять процессом коммита смещения вручную можно в API консюмера Kafka, установив параметр enable.auto.commit в значение false и явно вызвав один из следующих методов:

void commitSync(); void commitAsync();

Если вы стремитесь обработать сообщение «хотя бы один раз», вы должны закоммитить смещение вручную с помощью commitSync (), выполнив эту команду сразу после обработки сообщений.
Эти методы не позволяют подтверждать (acknowledged) сообщения до того, как они будут обработаны, но они ничего не делают для устранения потенциального задвоения обработки, в то же время создавая видимость транзакционности. В Kafka отсутствуют транзакции. У клиента нет возможности сделать следующее:

  • Автоматически откатить (roll back) зафейленное сообщение. Консюмеры сами должны обрабатывать исключения, возникающие из-за проблемных пэйлоадов и отключений бекэнда, так как они не могут полагаться на повторную доставку сообщений брокером.
  • Отправить сообщения в несколько топиков в рамках одной атомарной операции. Как мы скоро увидим, контроль над различными топиками и партициями может находиться на разных машинах в кластере Kafka, которые не координируют транзакции при отправке. На момент написания этой статьи была проделана определенная работа, чтобы сделать это возможным с помощью KIP-98.
  • Связать чтение одного сообщения из одного топика с отправкой другого сообщения в другой топик. Опять же, архитектура Kafka зависит от множества независимых машин, работающих как одна шина и не делается никаких попыток скрыть это. Например, не существует компонентов API, которые позволили бы связать Консюмер и Продюсер в транзакции. В JMS это обеспечивается объектом Session, из которого создаются MessageProducers и MessageConsumers.

Если мы не можем полагаться на транзакции, как мы можем обеспечить семантику, более близкую к той, которую предоставляют традиционные системы обмена сообщениями?
Если существует вероятность того, что смещение консюмера может увеличиться до того, как сообщение было обработано, например, во время сбоя консюмера, то у консюмера нет способа узнать, пропустила ли его группа консюмеров сообщения, когда ей назначают партицию. Таким образом, одна из стратегий заключается в перемотке (rewind) смещения на предыдущую позицию. API консюмера Kafka предоставляет следующие методы для этого:

void seek(TopicPartition partition, long offset); void seekToBeginning(Collection < TopicPartition > partitions);

Метод seek () может использоваться с методом
offsetsForTimes (Map<TopicPartition,Long> timestampsToSearch) для перемотки в состояние в какой-либо определенный момент в прошлом.
Неявно, использование этого подхода означает, что, весьма вероятно, некоторые сообщения, которые были обработаны ранее, будут прочитаны и обработаны заново. Чтобы избежать этого, мы можем использовать идемпотентное чтение, как описано в Главе 4, для отслеживания ранее просмотренных сообщений и исключения дубликатов.
Как альтернатива, код вашего консюмера может быть простым, если допустима потеря или дублирование сообщений. Когда мы рассматриваем сценарии использования, для которых обычно используется Kafka, например, обработка событий логов, метрик, отслеживание кликов и т.д., мы понимаем, что потеря отдельных сообщений вряд ли окажет значимое влияние на окружающие приложения. В таких случаях значения по-умолчанию вполне допустимы. С другой стороны, если вашему приложению нужно передавать платежи, вы должны тщательно заботиться о каждом отдельном сообщении. Все сводится к контексту.
Личные наблюдения показывают, что с ростом интенсивности сообщений, ценность каждого отдельного сообщения снижается. Сообщения большого объема становятся, как правило, ценными, если их рассматривать в агрегированной форме.

Высокая доступность (High Availability)

Подход Kafka в отношении высокой доступности существенно отличается от подхода ActiveMQ. Kafka разработана на базе горизонтально масштабируемых кластеров, в которых все экземпляры брокера принимают и раздают сообщения одновременно.
Кластер Kafka состоит из нескольких экземпляров брокера, работающих на разных серверах. Kafka была разработана для работы на обычном автономном железе, где каждый узел имеет свое собственное выделенное хранилище. Использование сетевых хранилищ (SAN) не рекомендуется, поскольку множественные вычислительные узлы могут конкурировать за временнЫе интервалы хранилища и создавать конфликты.
Kafka — это постоянно включенная система. Многие крупные пользователи Kafka никогда не гасят свои кластеры и программное обеспечение всегда обеспечивает обновление путем последовательного рестарта. Это достигается за счет гарантирования совместимости с предыдущей версией для сообщений и взаимодействий между брокерами.
Брокеры подключены к кластеру серверов ZooKeeper, который действует, как реестр конфигурационных данный и используется для координации ролей каждого брокера. ZooKeeper сам является распределенной системой, которая обеспечивает высокую доступность посредством репликации информации путем установления кворума.
В базовом случае топик создается в кластере Kafka со следующими свойствами:

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

Используя ZooKeepers для координации, Kafka пытается справедливо распределить новые партиции между брокерами в кластере. Это делается одним экземпляром, который выполняет роль Контроллера.
В рантайме для каждой партиции топика Контроллер назначает брокеру роли лидера (leader, master, ведущего) и последователей (followers, slaves, подчиненных). Брокер, выступающий в качестве лидера для данной партиции, отвечает за прием всех сообщений, отправленных ему продюсерами, и распространение сообщений по консюмерам. При отправке сообщений в партицию топика они реплицируются на все узлы брокера, выступающие в качестве последователей для этой партиции. Каждый узел, содержащий журналы для партиции, называется репликой. Брокер может выступать в качестве лидера для одних партиций и в качестве последователя для других.
Последователь, содержащий все сообщения, хранящиеся у лидера, называется синхронизированной репликой (репликой, находящейся в синхронизированном состоянии, in-sync replica). Если брокер, выступающий в качестве лидера для партиции, отключается, любой брокер, который находится в актуализированном или синхронизированном состоянии для этой партиции, может взять на себя роль лидера. Это невероятно устойчивый дизайн.
Частью конфигурации продюсера является параметр acks, который определяет, сколько реплик должно подтвердить (acknowledge) получение сообщения, прежде чем поток приложения продолжит отправку: 0, 1 или все. Если задано значение all, то при получении сообщения лидер отправит подтверждение (confirmation) обратно продюсеру, как только получит подтверждение (acknowledgements) записи от нескольких реплик (включая саму себя), определенных настройкой топика min.insync.replicas (по умолчанию 1). Если сообщение не может быть успешно реплицировано, то продюсер вызовет исключение для приложения (NotEnoughReplicas или NotEnoughReplicasAfterAppend).
В типичной конфигурации создается топик с коэффициентом репликации 3 (1 лидер, 2 последователя для каждой партиции) и параметр min.insync.replicas устанавливается в значение 2. В этом случае, кластер будет допускать, чтобы один из брокеров, управляющих партицией топика, мог отключаться без влияния на клиентские приложения.
Это возвращает нас к уже знакомому компромиссу между производительностью и надежностью. Репликация происходит за счет дополнительного времени ожидания подтверждений (acknowledgments) от последователей. Хотя, поскольку она выполняется параллельно, репликация, как минимум на три узла, имеет такую же производительность, как и на два (игнорируя увеличение использования пропускной способности сети).
Используя эту схему репликации, Kafka ловко избегает необходимости обеспечивать физическую запись каждого сообщения на диск с помощью операции sync (). Каждое сообщение, отправленное продюсером, будет записано в журнал партиции, но, как обсуждалось в Главе 2, запись в файл первоначально выполняется в буфер операционной системы. Если это сообщение реплицировано на другой экземпляр Kafka и находится в его памяти, потеря лидера не означает, что само сообщение было потеряно — его может взять на себя синхронизированная реплика.
Отказ от необходимости выполнять операцию sync () означает, что Kafka может принимать сообщения со скоростью, с которой она может записывать их в память. И наоборот, чем дольше можно избежать сброса (flushing) памяти на диск, тем лучше. По этой причине нередки случаи, когда брокерам Kafka выделяется 64 Гб памяти или более. Такое использование памяти означает, что один экземпляр Kafka может легко работать на скоростях во много тысяч раз быстрее, чем традиционный брокер сообщений.
Kafka также можно настроить для применения операции sync () к пакетам сообщений. Поскольку всё в Kafka ориентировано на работу с пакетами, это на самом деле работает довольно хорошо для многих сценариев использования и является полезным инструментом для пользователей, которые требуют очень сильных гарантий. Большая часть чистой производительности Kafka связана с сообщениями, которые отправляются брокеру в виде пакетов, и с тем, что эти сообщения считываются из брокера последовательными блоками с помощью zero-copy операций (операциями, в ходе которых не выполняется задача копирования данных из одной области памяти в другую). Последнее является большим выигрышем с точки зрения производительности и ресурсов и возможно только благодаря использованию лежащей в основе структуры данных журнала, определяющей схему партиции.
В кластере Kafka возможна гораздо более высокая производительность, чем при использовании одного брокера Kafka, поскольку партиции топика могут горизонтально масштабироваться на множестве отдельных машин.

Итоги

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

Предыдущая переведенная часть: Понимание брокеров сообщений. Изучение механики обмена сообщениями посредством ActiveMQ и Kafka. Глава 1

Перевод выполнен: tele.gg/middle_java

Продолжение следует…


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

Создаем на C++ выразительные умные указатели для удаленной памяти

Привет, Хабр!

Сегодня мы публикуем перевод интересного исследования о работе с памятью и указателями в C++. Материал немного академический, но явно будет небезынтересен читателям книг Галовица и Уильямса.

Следите за рекламой!

В аспирантуре я занимаюсь построением распределенных структур данных. Поэтому абстракция, представляющая удаленный указатель, исключительно важна в моей работе для создания чистого и аккуратного кода. В этой статье я расскажу, почему необходимы умные указатели, расскажу, как написал на C++ объекты удаленных указателей для моей библиотеки, добился, чтобы они работали точно как обычные указатели С++; это делается при помощи объектов удаленных ссылок. Далее я поясню, в каких случаях эта абстракция отказывает по той простой причине, что мой собственный указатель (пока) не справляется с теми задачами, которые под силу обычным указателям. Надеюсь, что статья заинтересует читателей, занимающихся разработкой высокоуровневых абстракций.

Низкоуровневые API

Работая с распределенными компьютерами или с сетевым аппаратным обеспечением, вы часто располагаете доступом на чтение и запись к некоторому участку памяти, через API на C. Один из примеров такого рода — API MPI для односторонней коммуникации. В этом API используются функции, открывающие прямой доступ на чтение и запись из памяти других узлов, находящихся в распределенном кластере. Вот как это выглядит в слегка упрощенном виде.

void  remote_read(void* dst, int target_node, int offset, int size); void remote_write(void* src, int target_node, int offset, int size);

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

Эти API великолепны, так как открывают нам доступ к важным примитивам, которые пригодятся нам для реализации программ, выполняемых на кластере компьютеров. Еще они очень хороши потому, что работают действительно быстро и в точности отражают возможности, предлагаемые на аппаратном уровне: удаленный прямой доступ к памяти (RDMA). Современные суперкомпьютерные сети, такие как Cray Aries и Mellanox EDR, позволяют рассчитывать, что задержка при чтении/записи не превысит 1-2 μs. Такого показателя удается добиться благодаря тому, что сетевая карта (NIC) может читать и записывать непосредственно в RAM, не дожидаясь, пока удаленный CPU проснется и отреагирует на ваш сетевой запрос.

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

Удаленные указатели

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

Если предположить, что у нас будет API, подобный описанным выше, то уникальное местоположение в памяти будет указываться двумя «координатами»: (1) ранг или ID процесса и (2) смещение, сделанное в разделяемый участок удаленной памяти, занятый процессом с этим рангом. Можно на этом не останавливаться и сделать полноценную структуру.

 template <typename T>     struct remote_ptr {       size_t rank_;       size_t offset_;     };

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

 template <typename T>     T rget(const remote_ptr<T> src) {       T rv;       remote_read(&rv, src.rank_, src.offset_, sizeof(T));       return rv;     }      template <typename T>     void rput(remote_ptr<T> dst, const T& src) {       remote_write(&src, dst.rank_, dst.offset_, sizeof(T));     }

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

 remote_ptr<int> ptr = ...;    int rval = rget(ptr);    rval++;    rput(ptr, rval);

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

Арифметика указателей

Арифметика указателей – важнейшая методика, позволяющая программисту управлять коллекциями значений в памяти; если же мы пишем программу для распределенной работы в памяти, предположительно, мы собираемся оперировать большими коллекциями значений.
Что означает увеличение или уменьшение удаленного указателя на единицу? Простейший вариант – считать арифметику удаленных указателей аналогичной арифметике обычных указателей: p+1 просто укажет на следующий sizeof(T)-выровненный участок памяти после p в разделяемом сегменте исходного ранга.

Хотя, это и не единственное возможное определение арифметики удаленных указателей, именно оно в последнее время наиболее активно берется на вооружение, а используемые таким образом удаленные указатели содержатся в таких библиотеках как UPC++, DASH и BCL. Однако, язык Унифицированный параллельный C (UPC), оставивший богатое наследие в сообществе специалистов по высокопроизводительным вычислениям (HPC), содержит более тщательно проработанную дефиницию арифметики указателей [1].

Реализовать арифметику указателей таким образом просто, и она связана лишь с изменением смещения указателя.

 template <typename T>    remote_ptr<T> remote_ptr<T>::operator+(std::ptrdiff_t diff)    {      size_t new_offset = offset_ + sizeof(T)*diff;      return remote_ptr<T>{rank_, new_offset};    }

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

void write_array(remote_ptr<int> ptr, size_t len) {      if (my_rank() < len) {        rput(ptr + my_rank(), my_rank());      }    }

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

Выбираем значение nullptr

У обычных указателей значение nullptr равно NULL, что обычно означает сведение #define к 0x0, поскольку этот участок в памяти вряд ли будет использоваться. В нашей схеме с удаленными указателями мы можем либо выбрать конкретное значение указателя в качестве nullptr, сделав таким образом данное местоположение в памяти неиспользуемым, либо включить специальный булев член, который будет свидетельствовать, является ли указатель нулевым. Притом, что делать некое местоположение в памяти неиспользуемым – не лучший выход, также учтем, что при добавлении всего одного булева значения размер удаленного указателя с точки зрения большинства компиляторов удвоится и вырастет со 128 до 256 бит, чтобы соблюсти выравнивание. Это особенно нежелательно. В моей библиотеке я выбрал {0, 0}, то есть, смещение 0 с рангом 0, в качестве значения nullptr.

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

Удаленные ссылки

В таких языках как Python, bracket-оператор служит в качестве синтаксического сахара для вызова методов __setitem__ и __getitem__, в зависимости от того, считываете ли вы объект или записываете в него. В C++, operator[] не различает, к какой из категорий значений принадлежит объект, и будет ли возвращенное значение сразу же подпадать под считывание или под запись. Для решения этой проблемы структуры данных C++ возвращают ссылки, указывающие на содержащуюся в контейнере память, которая поддается записи или считыванию. Реализация operator[] для std::vector может выглядеть примерно так.

 T& operator[](size_t idx) {      return data_[idx];    } 

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

В нашем случае мы не можем возвращать сырую ссылку C++, так как ссылаемся на память, расположенную на другом узле и не представленную в нашем виртуальном адресном пространстве. Правда, мы можем создавать наши собственные кастомные ссылочные объекты.
Ссылка представляет собой объект, служащий оберткой вокруг указателя, и она выполняет две важнейшие функции: ее можно преобразовать в значение типа T, а также можно присвоить значению типа T. Так, в случае удаленной ссылки нам всего лишь потребуется реализовать оператор неявного преобразования, считывающий значение, а также сделать оператор присваивания, записывающий в значение.

template <typename T>   struct remote_ref {     remote_ptr<T> ptr_;      operator T() const {       return rget(ptr_);     }      remote_ref& operator=(const T& value) {       rput(ptr_, value);       return *this;     }   }; 

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

template <typename T>   remote_ref<T> remote_ptr<T>::operator*() {     return remote_ref<T>{*this};   }    template <typename T>   remote_ref<T> remote_ptr<T>::operator[](ptrdiff_t idx) {     return remote_ref<T>{*this + idx};   }

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

void write_array(remote_ptr<int> ptr, size_t len) {      if (my_rank() < len) {        ptr[my_rank()] = my_rank();      }    }

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

Издержки, возникающие во время выполнения (или их отсутствие!)

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

Оказывается, что, если аккуратно спроектировать классы указателя и ссылок, то никаких издержек за эту абстракцию во время выполнения не будет – современные компиляторы C++ справляются с этими промежуточными объектами и вызовами методов путем агрессивного встраивания. Чтобы оценить, чего нам будет стоить такая абстракция, можно скомпилировать простую программу-пример и проверить, как пойдет сборка, чтобы посмотреть, какие объекты и методы будут существовать во время выполнения. В описанном здесь примере с редукцией на основе дерева, скомпилированном с классами удаленных указателей и ссылок, современные компиляторы сводят редукцию на основе дерева к нескольким вызовам remote_read и remote_write [4]. Никакие методы классов не вызываются, никаких ссылочных объектов во время выполнения не существует.

Взаимодействие с библиотеками структур данных

Опытные программисты, работающие с C++, помнят, что в стандартной библиотеке шаблонов C++ указано: STL-контейнеры должны поддерживать кастомные аллокаторы C++. Аллокаторы позволяют выделять память, а затем на эту память можно ссылаться с использованием типов указателей, сделанных нами. Означает ли это, что можно просто создать «удаленный аллокатор» и подключить его для хранения данных в удаленной памяти с использованием STL-контейнеров?

К сожалению, нет. Предположительно, из соображений производительности стандарт C++ больше не требует поддержки кастомных ссылочных типов, и в большинстве реализаций стандартной библиотеки C++ они действительно не поддерживаются. Так, например, если вы используете libstdc++ из GCC, то можете прибегать к кастомным указателям, но при этом вам доступны лишь обычные ссылки C++, что не позволяет вам использовать STL-контейнеры в удаленной памяти. Некоторые высокоуровневые библиотеки шаблонов C++, например, Agency, использующие кастомные типы указателей и ссылочные типы, содержат собственные реализации некоторых структур данных из STL, которые действительно позволяют работать с удаленными ссылочными типами. В данном случае программист получает большую свободу в креативном подходе к созданию типов аллокаторов, указателей и ссылок, а, кроме того, получает коллекцию структур данных, которые автоматически можно использовать с ними.

Широкий контекст

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

  • Выделение памяти. Теперь, имея возможность ссылаться на объекты в удаленной памяти, как же нам зарезервировать или выделить такую удаленную память?
  • Поддержка объектов. Как быть с хранением в удаленной памяти таких объектов, которые относятся к типам посложнее int? Возможна ли аккуратная поддержка сложных типов? Можно ли в то же время поддерживать простые типы, не затрачивая ресурсов на сериализацию?
  • Проектирование распределенных структур данных. Теперь, имея эти абстракции, какие структуры данных и приложения можно построить с их помощью? Какие абстракции следует использовать для распределения данных?

Примечания

[1] В UPC у указателей есть фаза, определяющая, на какой ранг будет направлен указатель после увеличения на единицу. Благодаря фазам, в указателях можно инкапсулировать распределенные массивы, причем, паттерны распределения в них могут быть самыми разными. Эти возможности очень мощные, но пользователю-новичку могут показаться магическими. Хотя, некоторые асы UPC действительно предпочитают такой подход, более разумный объектно-ориентированный подход заключается в том, чтобы сначала написать простой класс удаленного указателя, а затем обеспечивать распределение данных, полагаясь на специально предназначенные для этого структуры данных.

[2] Большинство приложений в HPC пишутся в стиле SPMD, это название означает «одна программа, разные данные». В API SPMD предлагается функция или переменная my_rank(), сообщающая процессу, выполняющему программу, уникальный ранг или ID, на основании которых затем можно выполнить ветвление от основной программы.

[3] Вот простейшая редукция дерева, написанная в стиле SPMD с использованием класса удаленных указателей. Код адаптирован на основе программы, исходно написанной моим коллегой Эндрю Белтом.

 template <typename T>    T parallel_sum(remote_ptr<T> a, size_t len) {      size_t k = len;       do {        k = (k + 1) / 2;         if (my_rank() < k && my_rank() + k < len) {          a[my_rank()] += a[my_rank() + k];        }         len = k;        barrier();     } while (k > 1);      return a[0];   } 

[4] Скомпилированный результат вышеприведенного кода можно посмотреть здесь: godbolt.org/z/yDKluF.

.


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