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

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

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

19.02.2023

Про ключі в 1Password, Git та ssh-agent

Продовження історії з ключами в 1Password.


# .envrc
export AWS_ACCESS_KEY_ID="op://My Vault/AWS Access Key/access key id"
export AWS_SECRET_ACCESS_KEY="op://My Vault/AWS Access Key/secret access key"
# виклик
op run -- terraform apply

Можна знайти в інтернеті згадки про використання ssh-agent з macOS Keychain. Насправді у Keychain зберігатиметься тільки парольна фраза, а не весь ключ. Ключ так само залишається в на диску. Тому я не впевнений, що це так потрібно. (Сподіваюсь, очевидно, що ключі обовʼязково мають бути зашифровані парольною фразою.)

Додатково зʼясував, що підписування змін в Git теж можна робити без 1Password, це функція звичайного ssh-agent:

# ~/.gitconfig
[user]
  signingkey = (ssh-ed25519 публічний-ключ...)
[gpg]
  format = ssh


18.02.2023

Навігація як одна з кореневих механік у Morrowind

Одна з визначних та рідких особливостей Morrowind - це присутність навігації як головної механіки. У 2023, після того, як ми бачили Death Stranding, де взагалі вся гра — це перетин місцевості, таке вже не звучить дуже дивно. Але підхід Morrowind інший. Тут фокус на розуміння інструкцій, орієнтацію на місцевості… а також навички користування публічним транспортом!

З першого вигляду, Morrowind - такий самий “відкритий світ”, як і безліч сучасних ігор, взяти хоча б ЇЇ нащадків Oblivion та Skyrim. Так само більшість завдань складається з “йди туди, принеси те”. Але якщо в інших іграх завжди є маркер, що вказує ціль на карті, то в Morrowind маркерів немає — та й взагалі карта рудиментарна. Кожне завдання доводиться спочатку знайти, користуючись приблизними інструкціями, наприклад “йди вздовж східної річки на південь, як дійдеш до озера то печера буде на острові посередині”. Може здатись, що вимкнувши маркери, можна з будь-якої гри зробити те саме. Але справа не тільки в необхідності навігації, а більше в наявності інструментів переміщення та можливості їх опанувати. Саме можливість набуття навичок й робить навігацію приємною механікою.

Як підтвердження тому, що це не випадково: головний квест пронизаний випробуваннями саме навігації. Одне з найцікавіших завдань потребує зі звичної та “цивілізованої” частини карти дістатись у віддалену пустельну зону, куди “метро не ходить”. На інші завдання деколи буквально дають гроші на проїзд.

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

Хоча “літаємо” можна зрозуміти й буквально, бо інший інструмент переміщення — то магічні покращення руху. Закляттями можна прискорити ходу (ніяких коней не потрібно), або взагалі левітувати, в тому числі — перелітати через всю карту. Магія в Morrowind неперевершена. Наприкінці гри чаклуванням можна фактично поламати всі труднощі та обмеження гри, та на навігацію це беззаперечно розповсюджується. (Це, до речі, чудово перекликається з сюжетом, де персонаж буквально стає божеством. Повна людонаративна гармонія!)


17.02.2023

Розрізи з інкрементальним оновленням в Redshift

Після нещодавніх відкриттів та досліджувань по Redshift перейшов до політики “інкрементальне оновлення, або геть!” З одного боку, набув впевненості, що це можливо, а з іншого — що без інкрементального оновлення та повністю матеріалізованих розрізів нічого хорошого не буде. Автоматичне оновлення менш важливо; запустити його вручну нескладно. Але критично важливо, щоб оновлення відбувалось швидко та без повної перебудови.

На жаль, здогадатись, що розріз не здатний до інкрементального оновлення, важко. Тож додав автоматичний тест, який перевіряє це для всіх розрізів, якось так: SELECT name, state=1 AS is_incremental FROM stv_mv_info. Без цього в майбутньому ми приречені наробити собі проблем.

Про механізм інкрементального оновлення написано дуже мало. Як я кажу, хотілося б щоб можна було глянути на розріз та відразу сказати, чи він інкрементальний, але ж ні. Особливо коли заходить за розрізи, залежні від інших розрізів. Декілька спостережень:

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

  2. На агрегаційні функції можна накладати інші функції, або навпаки, вкладати, та це не порушить інкрементальність. Наприклад, можна зробити date(min(created_at)).

  3. АЛЕ! Коли один розріз залежить від одного, то інкрементальним він стане тільки якщо не залежить від стовпчиків з “загорнутою” агрегаційною функцією. (Це, напевно, псує відстеження змін.) Тож, щоб виправити, можна внести функцію під агрегацію: min(date(created_at)). (Якщо це дозволяє логіка.)

  4. Ще можу рекомендувати редактор запитів Query Editor v2, що вбудований в AWS Console. В ньому є дуже корисна функція “записничок” (notebook) - мені було зручно готувати експерименти з матеріалізованими розрізами. Бо експериментувати доведеться багато, це вже зрозуміло. Перед тим, як впроваджувати розріз в додаток, раджу випробувати його вручну та наживу, та перевірити, чи є він інкрементальним.


16.02.2023

Чому в Go є тип string та byte[]

Сьогоднішній баг стосувався того, що в Go у змінну з типом string передавали ззовні значення nil. (Там вхід з кодеком, що побудований на рефлексії, тож таке технічно можливо.) Виправилось це заміною типу на *string, який може мати або значення nil, або вказувати на рядок. Наскільки я знаю, доволі ідіоматичне рішення. Але воно наштовхнуло на роздуми — якщо в Go рядок це технічно слайс байтів, а слайси можуть бути nil, то чому рядки не можуть? Моя відповідь — напевно, щоб абстракція рядків була красивішою, тобто щоб рядки виглядали більше як “примітивний” тип.

Ось інші нюанси роботи з типами string та byte[]. Конверсія з рядка в слайс не безплатна, на відміну від інших. Операції string(someBytes) та []byte(someString) створюють копію даних. Це потрібно тому, що рядок — тип незмінний, тому не може розділяти памʼять зі змінним типом — слайсом. При цьому є багато API, які приймають тільки слайси або тільки рядки, тож деколи конверсії не уникнути. В мене був навіть випадок, коли рядкову функцію довелось переписати для слайсів, щоб прискорити вузьке місце. Ще, наприклад, функції форматування приймають обидва: можна написати fmt.Printf("contents of file are: %s", byteSlice).

Можна подумати, що рядки мають якусь семантику символів, тобто кодування. Але ні — в Go рядки складаються з байтів. Довжина вимірюється в байтах, позиції теж. Є один нюанс — цикли з range перебирають символи UTF-8. Це єдине місце, де я знаю, що рядки набувають кодування. Бо якщо рядок має некоректні символи, то такий цикл їх зіпсує. Або я майже впевнений, що деякі комбінації кодів він буде нормалізувати. Одним словом, краще бути обережним.


15.02.2023

Історія сьогоднішнього багу

Тільки закінчив випускати хотфікс. Випускати щось в продакшн вночі — завжди цікаво, та завжди з історією. Що ж трапилось на цей раз?

Баг зʼявився у надсиланні пошти. Його помітили ще на стейджингу — чомусь листи не відправлялись з помилкою SMTP 503 5.5.1 Error: authentication not enabled. Але на стейджингу принаймні я багато уваги цьому не приділив, бо було більше схоже на проблеми з налаштуваннями середовища, ніж на реальну проблему. Але ж на продакшні баг відтворився, та почав перешкоджати надсиланню чесних користувацьких листів. Довелося терміново розвʼязувати.

Перше питання — що взагалі значить ця помилка? З мого досвіду, аутентифікація SMTP потребує шифрування, тобто STARTTLS. Бо як я вже писав, інакше пароль зможуть підгледіти по дорозі. Тож інтуїція каже, що насправді проблема у тому, що клієнт не надсилає команду STARTTLS. Шукаю параметри підключення та знаходжу: enable_starttls_auto: false. Так, це дуже схоже на причину. Але цей рядок зʼявився в проєкті роки тому. Як трапилось, що поведінка змінилась зараз?

З нещодавніх змін до проєкту підозрою є оновлення Ruby on Rails. Оновлення мінімальне та стосується тільки патч-версії, але все ж таки це глобальна зміна, яка теоретично може вплинути на будь-що. А конкретно, при перегляді різниці файлу Gemfile.lock бачу, що помінялася також версія гему mail. Його має сенс перевірити в першу чергу.

І нарешті, в переліку змін гему mail бачу цікавий рядок “Bug Fixes: Regression: accept enable_starttls_auto: false”. Знайшов! (Окреме питання, що написано, що вона unreleased, хоча вже 2 тижні як released. Але буває.)

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

Що робити, щоб такого не було? Напевно, головне, це підтримувати стейджинг в повністю робочому стані, як пріоритет №1. Щоб впевнено шукати помилки, навіть якщо вони не повʼязані очевидно з поточними змінами.


14.02.2023

Інкрементальні розрізи подій у Redshift

Нарешті сьогодні розкусив дві проблеми, що мене турбували по Redshift.

Перша — як я вчора писав, події краще комбінувати… логічною вершиною такого підходу буде взагалі позбавитись від різних потоків для типів подій та звалювати все в одну велику таблицю. Далі це дозволить нам згрупувати таблицю по спільному ключу, та агрегувати результат. Головне, щоб агрегація підпадала під інкрементальне оновлення. Бо тоді ми звільняємося від необхідності сканувати всю таблицю, а будемо завжди працювати тільки з новими подіями — це виводить здатність масштабування на новий рівень. Обмеження для інкрементального оновлення досить суворі, але коли всі події в одній таблиці, то можна робити цікаві речі, наприклад: COUNT(NULLIF(event_type = 'purchase', false)) AS purchase_count.

Друга — була така задача, щоб в межах GROUP BY взяти деяке поле з найсвіжішої події. Визначити час цієї події легко - MAX(created_at)… але ж агрегаційні функції працюють в межах стовпчика, тому інші стовпчики за таким запитом не знайдеш. Винайшов трохи хитрий підхід. Якщо інше поле — то рядок, то можна додати до нього час події як префікс. Час має бути в форматі з лексикографічним порядком — наприклад, ISO8601. Тепер можемо знайти останнє за часом значення будь-якого стовпчика - SUBSTRING(MAX(prefixed_item_id), 24). Як бачите, до результатів агрегаційних функцій можна застосовувати інші функції, та це не зашкодить інкрементальному оновленню.


13.02.2023

Трохи уроків з Event Sourcing на Redshift

Трохи уроків з так званого “event sourcing”, що для мене значить, архітектури, де ми, замість того, щоб редагувати записи в базі, зберігаємо кожну зміну стану як маленький, незмінний запис - “подію”. А потім з сукупності подій генеруємо поточний стан даних. (Наочний приклад — це реєстр операцій в магазині, підсумувавши які, можна отримати кількість грошей в касі.)

Мене на event sourcing підштовхнув сервіс AWS Kinesis Firehose. Бо він пропонує ну дуже спокусливу властивість — практично необмежений вхідний обсяг. Скільки не пиши, все візьме, складе в пачки, та відправить далі в базу, наприклад, в Redshift. А вже у Redshift доведеться виконати перетворення, щоб зібрати події в загальну картину. Можливо, це будуть матеріалізовані розрізи.

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

До речі, автоматичне оновлення — потужна функція Redshift - неможлива для розрізів, що залежать від інших розрізів. Тобто каскаду розрізів не вийде, принаймні, без побудови додаткового механізму оновлення.

Та ще до речі — якщо вже треба єднати в одну десять таблиць, може статись, що UNION ALL ... GROUP BY працюватиме значно ефективніше, ніж купа JOIN. Варто погратись.


12.02.2023

Коли розмір даних має значення?

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

Але також компактність даних має значення при побудові жетонів. Коротший жетон — завжди краще. Інколи ми робимо з них QR-код; тут чим більше зміст — тим більше деталей матимете QR-код, та тим складніше буде його прочитати. Тому дуже важливо тримати пакет даних для коду якнайкоротшим; брати не JSON, а компактне двійкове подання. Так само важливо тримати короткими жетони для URL-адрес - бо довгі адреси мають ризик бути зіпсованими браузером або мережевою інфраструктурою.

Ну та якщо мова зайшла про подання двійкових даних, то треба памʼятати, що найбільш компактний текстове подання — це Base64; він у півтора раза краще, ніж формат Hex. Для URL-адрес, до речі, є своя варіація - base64url - щоб не кодувати додатково символи + та / як три символи відсоткового кодування. Та, в решті решт, якщо дані не треба передавати текстом — краще над усе залишити їх у двійковому вигляді — це економія у 33%, порівняно з Base64, та у 50%, порівняно з Hex.


11.02.2023

Пост про компактне подання даних

Пост про компактне подання даних. Варто уявляти, яка оптимальна форма для кожного типу даних, та скільки місця він займає. Це я кажу як людина, яка в школі зберігала піксельну графіку в текстових файлах: 0 1 15 1 0. Якщо зберігати числа у тестовому файлі здається безглуздим, нагадаю, що формат JSON був би ще менш ефективним. Це свідчить про те, що сучасні програмісти не звикли думати, скільки займають їх дані. Та це чудово — але деколи доводиться. Наприклад, на масштабі — якщо умовний запис буде повторюватись мільярд разів, то марна трата одного байту вже виллється в втрачений гігабайт.

Перший урок з JSON - не треба зберігати дані в рядках тексту. Очевидно, для чисел є числові типи. Але є чисельні типи даних, які теж влазять в число. Наприклад, дати. Або IP адреси. Або кольори. Також часто можна обрати не найбільший числовий тип - тобто менш ніж 64 біти. Для булевих значень достатньо одного байту, для IP-адрес - чотири. Також ми звикли відображати двійкові рядки як текст, у вигляді Hex або Base64 - можна заощадити, якщо цього не робити. Наприклад, UUID замість 36 байтів тексту влізе в 16 байтів двійкових даних.

Рядки теж можна зберігати компактніше, якщо їх стискати, або ж просто використавши словник — є багато текстових даних, які мають обмежений набір значень.

Окремо про дати, я колись непогано виграв на використанні памʼяті у додатку на JavaScript, коли почав зберігати дати як числа, та створювати обʼєкти тільки за потребою. Це тому, що число — набагато компактніша форма.

А ще число — не завжди найкомпактніша форма подання. Мій улюблений приклад — зберігання множини чисел у вигляді бітової мапи. У такій формі біт в розряді N позначає, чи не міститься N в множині. Так можна досягнути економії місця в 32 рази. Не кажучи про те, що операції над бітовими мапами набагато швидше… але про це розкажу іншим разом.


10.02.2023

Розклеювач проєктів

Сьогодні півдня не було світла та звʼязку. Займався розгрібанням старих нотаток у Drafts - з 2021 року їх накопичилось більш ніж 200. Ось чому так важливо вичищати вхідні нотатки щодня! Важко згадати контекст думки, що трапилася рік тому. В одному випадку думав, що записана назва лікарської рослини, а насправді виявилось, що це прізвище рекомендованого стоматолога (спасибі Гуглу та базам лікарів!)

Проміж нотаток знайшов ідею для минулої системи самоменеджменту. Кодова назва “розклеювач проєктів”. Зміст: це як бізнес-канва, але для особистих проєктів, які ніяк не хочуть зсунутись з місця. Це для таких діл, на які дивишся тижнями чи навіть місяцями, але не наважуєшся робити. Розклеювач складається з девʼяти питань в трьох групах — можна розкреслити аркуш як в хрестики-нулики, а можна просто записати в стовпчик. Отже, ось питання:

Що саме потрібно зробити? Як зрозуміти, що мета досягнена? Що зміниться? Який конкретний вигляд результату?

Чому це потрібно робити? Яка мотивація? Можу порадити підхід “пʼяти чому”, тобто заглиблюватись в причини, щоб привʼязати проєкт до чогось важливого.

Чому це варто НЕ робити? Контрінтуїтивно, але факт — багато проєктів ми ніколи не зробимо. Може, є привід не робити цей?

Складнощі. Які аспекти відразу викликають стрес? Може, ти відштовхуєш цей проєкт, бо він неприємний? Чому саме?

Ідеї. Власне, сюди записати цікаві ідеї, які хотілося б випробувати. Може, варто собі нагадати, що саме запалило інтерес, коли проєкт був поставлений.

Плани. Я не люблю розписувати плани для всього, але коли вже проєкт застряг, то треба зрозуміти, з яких кроків він складається.

Люди. Які знайомі допоможуть в досягненні мети? Або, якого спеціаліста потрібно розшукати?

Ресурси. Які фізичні предмети будуть залучені? Що потрібно купити? Чи не заблокований проєкт нестачею інструментів, чи інгредієнтів?

Знання. Які знання та навички потрібні для розвʼязання задачі? Чому потрібно навчитися? Яку книжку прочитати?

Після відповіді на всі ці питання може статися одне з двох. Або стане зрозуміло, що краще проєкт не робити — бо це складно, довго, або не має сенсу. Або відкриється чіткий наступний крок — когось спитати, щось купити, і так далі. Ніякого алгоритму тут немає, інтуїція підкаже.