Уважаемые пользователи Голос!
Сайт доступен в режиме «чтение» до сентября 2020 года. Операции с токенами Golos, Cyber можно проводить, используя альтернативные клиенты или через эксплорер Cyberway. Подробности здесь: https://golos.io/@goloscore/operacii-s-tokenami-golos-cyber-1594822432061
С уважением, команда “Голос”
GOLOS
RU
EN
UA
yudina-cat
6 лет назад

Делегат-гуманитарий учится программировать (часть 2)

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

Напомню архитектуру прошлого решения. Скрипт на js, открытый в браузере у меня на ноутбуке («фронтэнд» во Вьетнаме) запрашивает блок у моей api-ноды (в Лас Вегасе). Находит в нем нужные транзакции, и через XMLHttpRequest обращается к «бэкэнду» (в Питере), который складывает данные в MySQL. Наглядно это выглядело так:

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

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

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

Битва с асинхронностью
«Ну что ж, приступим», – сказала я и напечатала в консоли apt-get install nodejs. Все что было дальше я не могу описать цензурными словами. Примерно сутки у меня ушли на то, чтобы развернуть (не без чуткого руководства @chusovitin) на сервере Node JS правильной версии, MySQL, Apache+PHP для PhpMyAdmin'а, перенести БД, настроить FTP и еще миллион мелких задач, без которых невозможно было перейти к сути.
Но самые забавные и мозголомательные подводные грабли ждали меня дальше. Я стала переносить скрипт «фронтэнда» на nodejs, и начала с запроса из БД номера последнего обработанного блока.

function get_start_block(){
block=[запрос в MySQL]
return block;
}
var block=0;
block=get_start_block();
console.log(block);

Такой чудесный и простой код вывел в консоль «0». Какого хрена, подумала я? И добавила вывод номера блока внутрь функции function get_start_block(). Результат был для меня крайне неожиданным. Сначала в консоли печатался нолик, а потом правильный номер стартового блока из MySQL. То есть nodejs не дожидался выполнения функции, сразу шел дальше. Чертовщина!

Немного погуглив и не найдя ничего вразумительного, я постучалась к @ropox, который от души поржав над ньюбом быстро наставил меня на путь истинного nodejs-джедая, рассказав о async/await и снабдив меня примером кода, дожидающегося выполнения функции.

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

Все очень просто, когда разберешься. Вот пример работы Promise/Resolve:

Суть такова: чтобы скрипт дожидался выполнения какого-то действия, нужно «обернуть» это действие в Promise (обещание), а результат возвращать через resolve. И в принципе, весь скрипт можно написать «линейно», но, забегая вперед, скажу, что асинхронность это очень круто! В итоге блоки запрашиваются, и анализатор отправляет запросы на апдейты в MySQL, не дожидаясь их исполнения, требует у ноды следующий блок и так далее. В общем, крутота.

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

Начинается все очень просто. Как поставить нужные модули nodejs и соединиться с нодой, я узнала, из описания добробота еще раз спасибо @ropox!

Дальше идет соединение с MySQL, да не простое – сразу создается «пул соединений», а ниже идет модуль, через который мы и будем общаться с БД. За эту хитрую конструкцию спасибо @dinosaurmike, который дал мне контакт классного программиста Дмитрия (нужно его заманить на Голос!). В модуль заранее зашит Promise, что позволяет при необходимости дожидаться выполнения запроса.
Начинается скрипт с запроса двух блоков. Во-первых нужно понять, какой сейчас блок самый последний в цепочке, чтобы знать, когда остановиться. Во-вторых, нужно забрать из БД номер последнего обработанного скриптом блока, чтобы знать откуда начинать.

Видите, все эти данные нужны до запуска основного цикла скрипта, поэтому запросы завернуты в Promise, и скрипт не пойдет дальше, пока данные не будут получены. То есть сначала, golos.api.getDynamicGlobalProperties прямо из примера в описании golos-js, внутри него resolve(head_block_number), и вызов следующей функции get_last_processed() внутри .then – не так уж и сложно, если привыкнуть.

get_last_processed() как раз показывает, как пользоваться модулем для запросов в БД. Записываем наш запрос для наглядности в переменную _querySelect, для чего нужен param не скажу так сходу, но вместе с ним все «подаем на вход модуля» и скрипт ждет, пока не наступит .then. После чего запускает следующую функцию – start().

Здесь у меня есть выбор между двумя сценариями работы. Если задана переменная one_block, то дальше в обработке блоков не запустится рекурсия, это я написала при отладке, потому что толкового описания структуры блоков я так и не нашла (оно существует?), и скрипт постоянно натыкался на разные неприятности. В общем, если «режим обработки одного блока» не включен, то мы просто прибавляем единицу к номеру последнего обработанного блока (из БД) и запускаем функцию scan().

По моим меркам, большая функция:

Сначала запрашиваем у ноды блок, и, дождавшись его получения, асинхронно отдаем его функции analysis(). Проверяем, не дошли ли мы до конца цепочки. Ждем 20 миллисекунд и запускаем scan(), прибавив единицу к номеру блока. Если дошли до конца, то выводим итоги работы.

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

Лирическое отступление про 20 миллисекунд
Пока я писала это все, скрипт естественно был в «режиме одного блока». Когда все нужное было написано, а ошибки отловлены, я включила рекурсию и секунд 15 наслаждалась летящими в консоли с невероятной скоростью сообщениями о полученных и обработанных блоках. После чего счастье внезапно закончилось. MySQL сказал «найн!» и вышел прочь.

Оказывается, есть такая штука, как Threads_connected. В этой переменной MySQL хранит количество открытых соединений. Я добавила вывод этой переменной в скрипт и увидела, как в течение 15 секунд счастья эта переменная стремительно растет, достигает лимита, и путь к нирване грубо прерывается сообщением Too many connections. Вот блин и магическая асинхронность.

Проблема состояла из двух частей. Во-первых, MySQL действительно не готов выполнять столько запросов, потому что бывают блоки, при анализе которых создается несколько сотен селектов и апдейтов. Во-вторых, в модуле работы с БД есть команда connection.release(), которая по завершении запроса должна «возвращать соединение в пул», но делает она это почему-то не всегда.

Убив много часов на поиски решения и чуть не начав писать «очередь запросов», я решила, что «ну я же не @arcange в конце концов и не GolosSQL строю», и сделала следующее: увеличила в конфиге БД максимальное число соединений до 500, пулу отдала 495 и чисто эмпирически выяснила, что если не запрашивать блоки чаще 20 миллисекунд 99,99% запросов выполняются удачно. Даже если блоки с аномально большим числом транзакций идут подряд.

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

Анализ блоков
Логика анализа блоков почти не отличается от прошлой версии. Просто перебираем все transactions и ищем регистрации, апвоты, посты и комментарии.

Все это добро не влезает в один скриншот, но суть простая:

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

Кроме того, нужно постоянно проверять существует ли golos_user в моей БД, из-за гипотетической потери запросов и из-за того, что у древних пользователей первая запись в блокчейне не account_create – об этом я писала в прошлой части.

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

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

Прошлая серия


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

0
17.501 GOLOS
На Golos с May 2017
Комментарии (11)
Сортировать по:
Сначала старые