Стендап Сьогодні

Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті

Підписатись на RSS
📢 Канал в Telegram @stendap_sogodni
🦣 @stendap_sogodni@shevtsov.me в Федиверсі

07.03.2023

Пошук помилок методом виключення всього зайвого

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

Пропоную ще один підхід. Почати видаляти з системи частини, починаючи з найдальшої від місця помилки. Помилка на бекенді? Викидаємо фронтенд. Потім викидаємо вебсервер. Запускаємо модуль в максимальної ізоляції, яку можна розумними витратами часу. Падає тест тільки на CI? Викидаємо зі сценарію CI все, окрім запуску цього тесту.

Далі — дивимося. Якщо помилка відтворюється — чудово, ми звузили поле пошуку. Якщо не відтворюється — значить, не вгадали з місцем-джерелом, пробуємо інше та починаємо спочатку. Якщо код поламався, але по-новому — то, напевно, багато вирізали, та треба дещо повернути.

Коли працюєш з кодом та з системою контролю версій, завжди можна відкотити зміни. Користуймося цим!


06.03.2023

Не можна перескочити на розвʼязок

Не можна відразу перескочити на розвʼязок. Втомився писати довгі пости, тому сьогодні короткий та філософський.

В мене є вада характеру — коли випадає складна задача, то я довго над нею думаю, замість того, щоб почати писати код. “Довго” може вимірюватись днями. В теорії, якщо гарно подумати, то можна знайти більш просте та красиве рішення. Якщо поки красивого рішення немає… треба подумати ще?

…Коли справа доходить до коду, то рішення неодмінно перетворюється, байдуже, що я там придумав. Як малюнок з голови не перенесеш на папір, так само й код в уяві тільки здається, що працює. Тому краще, на мою думку, якнайшвидше почати писати щось, навіть якщо поки це “щось” складне та неохайне. Поки руки пишуть код, голова врубається в контекст. Якщо й можна знайти справжній розвʼязок, то тільки так.


05.03.2023

Як Ruby, Node.js та Go витрачають памʼять на функції

Ні, не міг зупинитись без того, щоб розглянути також функції як значення. В Go та JavaScript це можливо напряму, а в Ruby є так звані лямбди (хоча це й не єдиний спосіб, як я вже писав, але найбільш поширений).

Власне, що значить — зберегти функцію? Код функції памʼяті не займає, принаймні, не в тому сенсі, як змінна. Та дійсно, у всіх мовах розмір функції ніяк не впливає на витрати памʼяті. В кожній мові посилання на функцію займає по-різному. В Go - чесні 8 байтів, в JavaScript - 70, в Ruby - цілих 241 байт. Знову відчувається різниця між системною мовою, мовою з функціональною парадигмою, та мовою, де лямбду нечасто побачиш.

Але на цьому витрати на функції не закінчуються. Можна подумати, що параметри функції щось займають, проте ні — параметри належать виклику функції, а не її визначенню. Але ж в усіх трьох мовах функції є замиканнями, та посилання на функцію зберігає доступні їй змінні. Чим більше змінних, тим більше памʼяті. Як завжди, Go окрім значень, нічого не зберігає, а в JS та Ruby є невеликі додаткові витрати.

Здивувало ось що. Які саме змінні включаються в замикання? Інтуїтивна відповідь — ті, що в ньому використовуються. На Go це абсолютно вірно — бо Go ретельно відстежує шлях використання змінних, та навіть не дасть скомпілювати програму, якщо деяка змінна залишається невикористаною. Зрозуміло, що Go чітко знає, що потрапить в замикання.

В Ruby все навпаки. В Ruby лямбда замикає всі доступні змінні — незалежно від того, використовуються вони в ній чи ні. Це важливо знати, бо всі ці змінні утримуватимуться в памʼяті, поки існує лямбда. Чому так? Я не впевнений, але здається, тому, що в Ruby є метод Binding#local_variable_get, яким можна за імʼям дізнатись значення будь-якої змінної. Та дійсно, цим методом можна підгледіти змінні поза межами лямбди:

def hidden_binding(i)
  j = i * i
  ->(name) { binding.local_variable_get(name) }
end
peek = hidden_binding(2)
puts peek.call(:j) # 4

Отакої! Гнучкість мови Ruby дається не безплатно.

В JavaScript такого не відбувається, якщо тільки в замиканні немає функції eval. Якщо є, то функція також захопить всі доступні змінні. Це показують і витрати памʼяті. Причому перевірка на eval відбувається статично, тобто якщо eval передати параметром, то замикання не буде повним:

function hiddenBinding(i) {
  let j = i * i;
  return (x) => eval(x);
}
console.log(hiddenBinding(2)("j")); // 4

function hiddenBinding2(i, f) {
  let j = i * i;
  return (x) => f(x);
}
console.log(hiddenBinding2(2, eval)("j")); // j is not defined

(Ясно, що порядні люди пишуть код без eval. Але он воно як цікаво працює!)


04.03.2023

Вимірюємо витрати памʼяті у JavaScript

Для закінчення моєї поверхневої серії про витрати памʼяті, розглянемо JavaScript. А точніше, інтерпретатор Node.JS, бо інтерпретаторів існує чимало, та поводитись вони можуть по-різному. Код все ще на gist.

В JavaScript є вбудована функція для отримання витрат памʼяті - process.memoryUsage(). А от для виклику збирача сміття треба запускати скрипт з ключем --expose-gc, інакше функція global.gc() не буде доступною.

Так само як і в Ruby та Go, числа в JavaScript займають 8 байтів, тобто зберігаються без обгорток. Так само масив чисел займає стільки памʼяті, скільки самі числа. Виникає питання — навіщо тоді в JavaScript є ще класи для типізованих масивів - Int8Array та інші? Знайшов пояснення. Головна причина в тому, що типізовані масиви відповідають єдиному блоку памʼяті, та їх можна передавати в усілякі API, які такий блок очікують. Тож не варто заміняти свої масиви на типізовані, якщо немає на них споживача — звичайні масиви не менш ефективні.

Константні рядки так само зберігаються одноразово. Але це не єдине, що тут незвично. Рядки у JavaScript представляються у кодуванні UTF-16, де “прості” символи займають по 2 байти. А памʼять під них Node.js виділяє блоками по 8 байтів, напевно, щоб рядок був кратним розрядності процесора. До речі, не писав про це окремо, але як в Ruby, так і в Go рядки зберігаються в “рідному” кодуванні, що для більшості сучасних програм значить UTF-8. Так що, для зберігання текстів латиницею програма на JavaScript споживатиме вдвічі більше памʼяті, ніж Ruby або Go. Але тут теж все не так просто, бо мені здається, що Node.JS стискає рядки, бо на рядки з повторюваними значеннями уходить менше памʼяті. Треба це детальніше роздивитись.

Зі структурами все теж цікаво. В JavaScript все є обʼєктом. Будь то асоціативний масив, клас, будь-що інше. Це підтверджують мої тести: обʼєкти, створені літералом, та такі, що створені конструктором, памʼяті займають однаково. Але з літералами не все так просто. Помітив, що додаткове чисельне поле в літералі збільшує витрати тільки на 8 байтів. Але ж де тоді зберігається ключ цього поля? Моя інтуїція каже, що в JavaScript структура обʼєкта зберігається окремо від значень, тому обʼєкти, створені одним та тим самим літералом містять тільки свої значення. Таким чином, JavaScript по використанню памʼяті точно краще, ніж Ruby, та має показувати витрати, схожі на Go. Дуже приємний висновок.

На цьому поки закінчую дослідження витрат памʼяті. Короткі висновки: для зберігання простих типів та масивів всі 3 мови підходять приблизно однаково; Ruby суттєво програє в витратах на структури; JavaScript навпаки, майже не гірше за системну мову; Go, звісно, буде оптимальним, якщо тільки не зберігати все в асоціативних масивах.


03.03.2023

Витрати памʼяті Ruby та Go, продовження: структури

Продовжую вчорашні тести. Доробив код, зробив зручну функцію для вимірювання, щоб прибрати копіпасту. На Go згодилися узагальнені типи; безумовно, корисна можливість, та ще й така що не погіршує швидкодію програми — про це далі. Сьогодні роздивимось структури. За тестовий приклад я обрав структуру з трьох чисельних значень; тоді розмір самих значень відомий - 24 байти, та можна підрахувати накладні витрати від самого механізму структури.

Починаючи з Ruby, найбільш очевидним здається клас Struct. Тут на кожний атрибут додаткові витрати ставлять 22 байти. Такі самі витрати будуть, якщо створити власний клас, без Struct. Тобто схоже, що Struct не оптимізує, а тільки спрощує код.

Хеш інтуїтивно буде менш ефективним. Тест це підтверджує: тут, на перший погляд, витрати аж в 3 рази більше. Але ось що цікаво: той самий хеш розміром у 224 байти спроможний зберігати до 8 значень без додаткового виділення памʼяті!. Схоже на те, що памʼять виділяється блоками, бо з девʼятого значення використання стрибає у 2 рази. Виходить, що для маленьких структур краще клас, а для великих можна не перейматись, бо хеш не додає зайвих витрат. Також, хеш, де ключі — це рядкові константи, важить так само як і хеш з символами. Ось скільки дивних нюансів такого простого, на перший погляд, типу.

Як щодо Go, то тут структури вбудовані в мову, та в традиції системних мов програмування, звичайна структура не створює жодних додаткових витрат — займає рівно стільки, скільки значення, які вона містить. Сподіваюсь, це пояснює, чому для потреб, де памʼять критична, беруть Go, а не Ruby.

Вказівник на структуру додасть свої очевидні 8 байтів. Більш цікаво, що перетворення типу в interface{} додає 9 байтів — тобто якщо використати конкретні типи, то вони не мають додаткових витрат, а інтерфейсний тип — має. Що підкреслює цінність впровадження узагальнених типів, бо як раз узагальнені типи працюють так само як еквівалентний код, де всі типи підставлені вручну.

Про асоціативні масиви в Go коротко скажу, що вони дуже схожі на хеші в Ruby. Таке саме виділення памʼяті блоками, та приблизно такі самі витрати на елемент — десь 24 байти. Різниця тільки в тому, що в Go структури набагато вигідніше в порівнянні. Наприклад, якщо використати тип map[string]interface{} для лінивої обробки JSON, то платити доведеться не тільки за масив, а ще й за інтерфейсний тип. Так що інтуїція по Go - якщо форма структури відома заздалегідь, завжди треба брати структури, а якщо ні — тільки тоді масиви.

Завтра — все то саме, тільки для JavaScript.


02.03.2023

Порівняння витрат памʼяті Ruby та Go на масиви

Хотів написати пост про те, як Go краще витрачає памʼять, ніж Ruby, та ще й з прикладами. Але вийшла несподіванка — не такий вже Ruby й поганий! На масив чисел якимсь чином витрачає десь стільки ж памʼяті (на 2% менше навіть!), а от на масив рядків вже на 50% більше, що більш зрозуміло. Код прикладів у gist. Тут треба більше розбиратись.

По-перше, що вимірювати як витрати пам’яті. На мою думку, краще за все дивитись на загальний розмір памʼяті, виділений процесом, тобто RSS - Resident Set Size. Ані в Ruby, ані в Go немає вбудованих засобів дізнатись цей розмір, але його можна легко отримати командою оболонки ps -o rss $PID_OF_PROGRAM. Корисно, що цей показник не спирається на особливості мови чи платформи, тому таке порівняння більш справедливе. Залишається тільки виділяти достатньо памʼяті. Та про всяк випадок викликати збирач сміття перед вимірюваннями; хоча в такому простому тесті це не має ніякого ефекту. Збирати сміття варто, якщо вимірювати витрати цілого алгоритму.

Так от, виходить, що як Go, так і Ruby зберігають масив цілих чисел у 64-бітному відображенні, та практично без додаткових витрат. Причому в Go кількість бітів у типі задається явно, а от від Ruby можна було очікувати й 32-бітних чисел, бо мої значення влазять в 32 біти. Втім, виглядає так, що Ruby завжди використовує 64-бітні числа, що логічно на моїй 64-бітній системі.

Рядки на Go витрачають, окрім байта на кожний символ, додатково 8 байтів на вказівник, та 8 байтів на довжину рядка (або так це виглядає). В Ruby, напевно, рядок містить ще якесь значення — можливо, кодування.

Можемо також підтвердити економію, яку Go отримує на тому, що рядки — є константи]. А саме, якщо масив наповнити не різними рядками, а одним й тим самим рядком, то витрати памʼяті будуть такі самі, як на масив чисел. І це незалежно від довжини цього рядка. Це пояснюється тим, що фактично масив містить вказівники на один та той самий рядок, а вказівник займає ті самі 8 байтів, як і число. В Ruby можна досягнути такої самої економії, якщо робити масив символів, тобто :symbol. Що зайвий раз підкреслює важливість використання символів там, де одне й те саме значення повторюватиметься багато разів — наприклад, при декодуванні JSON.

Але масиви — то надто просто. Цікавіше буде порівняти витрати на структури або хеші — фундамент сучасного програмування. Про це — наступного разу.


01.03.2023

Коли кешування зробити неможливо

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

Часто майбутні проблеми зі швидкодією бази даних планують вирішувати впровадженням кешу (принаймні я). Типу, зараз і так все добре, а коли база не буде витягувати — додамо кеш. От натрапив на таку ситуацію, коли кешування, хоч технічно можливо, але нічого абсолютно не дасть.

Уявимо собі що ми робимо інформаційно-погрозливе табло для автостради в стилі “Чорного дзеркала”. Треба розпізнати номер автівки та, якщо по ньому є штрафи, то показати водію попередження. Для цього у нас є централізована база штрафів. Пілотний проєкт працює відмінно. Отримали замовлення від Укравтодору на тисячі таких табло та сотні тисяч автівок. Хочемо уникнути ризику, що база не витягне такого навантаження.

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

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

Якщо ж нас не влаштовує локальна копія — може, ми хочемо оновлювати інформацію якнайшвидше — тоді все ж таки треба робити центральну базу швидкою, без кешу. От нещодавно дізнався, що AWS DynamoDB обіцяє не тільки необмежене масштабування, але й швидкість відповіді в межах 10 мілісекунд.


28.02.2023

Як запустити будь-який скрипт на AWS ECS

Забув вчора розповісти про головне. А саме — бенчмарки треба було запускати на AWS ECS, в умовах, наближених до реальних. От тільки є проблема — на ECS запускаються тільки додатки, які вже запаковані в образи Docker та завантажені у реєстр. Що не сприяє запуску випадкових скриптів та швидким ітераціям розробки.

Але вихід є — при запуску контейнера ECS дозволяє замінити його командний рядок. Тоді скрипт можна розмістити цілком в новій команді, та таким чином запустити. Тільки вручну це робити складно, тому ми в команді зробили утиліту ecs_run, яка власне й робить те, що я написав. Утиліта ecs_run дозволяє нам запускати усілякі rake скрипти, а також просто довільний код на Ruby. (Щоб виконати Ruby-код, вона загортає його у виклик інтерпретатора ruby.)

Тобто, у нас вже є сервіс ECS, в якому міститься додаток, готовий до запуску — код, залежності, необхідні змінні оточення та дозволи. Я командою ecr_run <benchmark.rb запускаю свій бенчмарк безпосередньо в цьому середовищі, тому він має доступ до всього, що є в додатку.

Ось тільки заміна командного рядка в ECS обмежена розміром 8KB. (Як я дізнався вчора; раніше це не заважало.) Що тоді робити? Я придумав розміщати сам скрипт у секретному Gist, а в контейнер передавати обгортку, яка завантажить його та запустить. Якщо цю ідею розвинути, то можна взагалі пакувати всі змінені файли в архів, архів передавати в контейнер, розпаковувати, та мати “неофіційну” версію додатка без зайвого розгортування.

А ще є ідея з іншого боку — що якщо паралельно відкривати локальний тунель, наприклад ngrok, а контейнер нехай підʼєднається до нього та відкриє інтерактивну консоль. В теорії зробити це нескладно, але готових реалізацій я не знаю. Втім, інтерактивна консоль — одна з найцінніших можливостей Heroku, якої часто бракує на ECS.


27.02.2023

Поради по бенчмарках на Ruby

Сьогодні вдалося зробити чудовий бенчмарк для моїх поробок на Redshift. Власне, сам бенчмарк робився на Ruby, та порівнював швидкодію різних запитів в базу. Поки писав, знайшов пару цікавих рішень.


26.02.2023

Де StackOverflow для ремонту?

Вихідні пройшли за ремонтом, а точніше — оновленням проводки та домашньої мережі. Проєкт на 7 розеток, з яких одна - Ethernet, а інша — контрольована, а також два Ethernet кабелі через всю квартиру, та пересування роутера з кімнати на поличку в коридорі (яку теж довелося встановити.)

Найскладніше в ремонті, як на мене — то виправлення помилок. Коли звик до програмування, де будь-які зміни можна відкотити, то посунути на декілька міліметрів отвір в стіні, або зробити довше надто короткий дріт може бути фізично неможливо — втім, якийсь компроміс доведеться робити. Друге найскладніше — то дебаг, тобто зʼясування, що саме не працює. От сьогодні довго порався та декілька разів перетискав мережевий кабель, бо не міг отримати сигналу. А виявилось, що це тому, що я перевіряв Ethernet-адаптером та ноутбуком, а він недостатньо потужний. (Так, якщо кабель задовгий — то вже треба щось с власним джерелом живлення — наприклад, комутатор.)

Мене дуже дратує, що в інтернеті не вистачає інформації по ремонту. Типові тексти — то статті-SEО для різних агенцій. Або погані переклади. Або переклади статей SEO. Все це жахливо читати, але доводиться, бо, скажімо, де ще знайти дані щодо максимальних навантажень на шуруп. Ще зустрічаються стрічки на форумах, де половина постів — то суперечки про те, хто кращий майстер.ʼ

Проблема погіршується тим, що існує купа регіональних особливостей. Всі знають, що розетки в США інші; але також інші стандарти труб та фітингів — навіть кран з польської IKEA не вдасться просто взяти та встановити в Україні. Якщо про програмування можна відразу шукати англійською, то з ремонтом це загрожує неприємними регіональними сюрпризами.

Що з всім цим робити — я не знаю. Може хоч колись поділюся своїми нотатками. Що і вам раджу зробити.