Rust чыра Go для сервісаў у спадней частцы: платіце толькі за тыя гарантіі, якія вам патрэбны
Чаму безпека і шырока скорасць Rust рэдкая разв’язваюць практычныя проблемы команд, якія разрабатваюць API, як Go оптымізуецца для простоты адтрымання, і калі Rust є правильны стандарт для бэкенду.
Rust — адзін з найвялікантных языкаў пасляльговага програмавання за апошню дзесятку гадоў, і самэ гэта ўсёлікавае прычына, чаму яго трэба рэштыцяваючы разглядаць, прытаму як толькі ён не стане стандартам для команд, якія працуюць з бэкендам. Большасць бэкенд-сервісаў не обмежваюцца тыми якосцямі, якія падкрэпляе Rust; іх обмежвае швальнасць, з якой група людзей можа ўсвядоміць іх і безпечна ў тым змяніць. У гэтым артыкуле Rust і Go парабяляюцца з гэтай точкі зору, паказваецца, дзе на самай працэ выражаюцыяся расходы кожнага з языкаў, і даёмася чыстая методыка, як адначасова вядомаць, калі сіла Rust адпавядае вартасці яе викорыстоўвання.
Сцэнарый, які варта адзначыць
Уявіце команду, яка реалізуе звычную функцыю бэкенду на мове Rust. Раслічаны код стае болей безпечным, адражэнным і падтрымваным сильнейшымі гаранціямі часа компілявання, чым быў бы адпаведны код на мове Go. Кроме таго, яго стварэнне займае значна больш часу, спрычынае длігія дыскусіі пра типы і трымкі, а звычную змяну ператварае на дыскусію пра дизайн. Тая ж самая функцыя на мове Go была б створана быстра і ўсім у командзе была б лёгкая да чытання.
Ні адны з эых рэзультатаў не значыць, што адна мова гэрозная. Це значыць, што косцт развіцця інструмента трэба спраўджваць у супаложнасці з проблемай, якой ён прабывае рашыць.
Гэніяльнасць — гэта не тое ж, што падходжычаснасць
Rust змяніў спосаб, якім працоўнае сектара разважае пра безпеку, выдатнасці і правільнасці, не павяржаючыся на калектар мусору. Гэтыя досягнення рэальныя, і саме ўсё гэта прыносіць яму такое павагу.
Але большасць команд, які працуюць у частыні сервера, не займаюцца дваржовымі механізмамі, ядрамі, прыгледчыкамі, дваржовымі механізмамі для гэмав, вбудованым фарвэрам або інфраструктурой, чутлівай да безпекі, дзе кожны распад памяці і ўсканаванне меж памяці є критычна проблема. Большасць з іх стварае API. Їхня ўласна робота заключаецца у перадачы дадзенняў у формате JSON межа Postgres, Redis, Kafka, S3, платежных шлюзоў, служб упавядомленняў, внутрашніх систем і API трэціх сторон, якія часамі зламваюцца спосабамі, якія ўскладнююць роботу.
Гэтая работа не ўражаючая: бізнес-правіла, павторныя спробы, логгіранне, панелі керування, развертанне, міграцыі, а таксама спробы інжынераў падтрымаць стабільнасць системы. Для такой среды Rust можа стаць чырвоным рашэнням на пытанне, якое команда нават не ставіла.
Правильнае пытанне
Звычны падход є кампанійскі. Адна група стверджуе, што Go занадта спроставана; іншая — што Rust занадта складная. Обе пазіркі є правдзівымі, але обе не падхопляюць сутнасці проблемы.
„Чы раст лепшы за Го?“ — гэта запитанне занадто нечыяве, каб на яго можна было адказаць. Асфальтавальнік кращы за кухонны нож у рубце дрэва, але ніхто не носіць яго за стол. Корыстны ўпораўненне — гэта тое, для чаго оптымізуюцца кожныя з ягоў мов:
- Rust аддае моц і тачнасць, і стараецца выключыць цэлыя катагоріі багоў ўжо перад тым, як програма запускаецца.
- Go аддае стрыманасць і шыранае разумеечнасць, і ён спроектаваны на прыпуску, што звычныя інжынеры пад тым жа тым часам буду пільнаваць код.
Гэты другі прыпуск мае большую значымасць, чым можа здавацца на першы погляд.
Што зміняецца, калі ў дыскусію входзіць пільнаванне
Багато разработчыкаў на мове Go не ненавідзяць Rust. Якія-то яго аплодуюць, якія-то пішчытаюць на яго, якія-то хочуць яго выучыць, а багато хто без адзінасці схваляе, што ён яшчэ лепы выбар для серйозной роботы на низкам рэвэлі. Тон змінюецца, калі тэма пераходзіць ад дизайну мовы да давялога тыпу адтрымання бэкенду.
У такі момент ніхто не спарчуецца, што Rust ўладны. Пытанне стае такім: чы рэгулярная команда бэкенду павінна несці расходы, якія створае Rust, каб рашыць проблемы, якія ў яе сервісах здзейчыцца не існуе.
Што робіць Go супернікам, які маюцы большыя перавагі. Не таму, што ён болей прыгодны, чаго не адбываецца. Не таму, што яго система типоў болей багата, чаго таксама не адбываецца. І звычайна не таму, што ён дапамагае разработчыкам адчуваць ся хітрымі; на самай працоўнае ён робіць працупакось. Go выграў таму, што відбівае некомфортную правду пра програмныя організацыі: большасць команд не патрабуе болей выразны код. Їм патрэбен код, які моглі б змяніць больш людзей, не баячыся гэтага.
Уздушны канал рэдкая разваўаецца з CPU
Інжынеры бэкенду любяюць верыць, што ўхо іх система за адну оптымізацыю стане выдающайся. Цэта лестная історыя, але зазвычай некарэткая.
Большая частка медленных бэкендов не ўповольнаеся з-за сабеўтварэння яго мовы. Яны ўповольнаеся таму, што запиты выконваюцца неэфекывна, кеш падае застарелыя даны, сеть не надзейна, у черге є застой, залежнасці не стабільныя, або бізнес-процесы спаяваюць пяць операцый, якія малі быць незалежнымі. Швэйжая мова не рашае ніякога з гэтага.
Складная частка інжынеріі бэкенду зазвычай не ў тым, каб заставіць апарат выконваць інструкцыі. Це ў тым, каб дапамогчы групе людзей глэбокаа зразумець систему, каб яны моглі ўсунуць у яе змяны без нараблення бягункоў. Ніжэйшыя дыяграмы ілуструюць гэта, паказваючы, дзе зазвычай выступае справжнія тарган:
A Normal Backend Team's Real Bottleneck
CPU
|
| usually not here
v
-----------------
| application |
-----------------
| | |
v v v
Postgres Redis Kafka
| | |
v v v
unclear ownership
changing product rules
missing observability
slow code reviews
fear of refactoring
tired on-call engineers
The machine was rarely the hard part.
The humans were.
Это пояснюе, як Rust можа перамагаць за рахунак тэхнічных якосцей, пры тым застаючыся паслабным выборам для багато команд, якія працуюць з бэкендам. Rust акцэнавае на правільнасці коду, калі ён зустрочаецца з іншым кодам. Go ж часта оптымаізуецца для выжывання команды ў складных умовах. Гэты контраст — гэта вся супераспэчка ў адной фразе.
Простасць Go — цэлая свядомая абмежэння
Тыповы працоўнік HTTP у Go не мае нічога выдающага. Ён декодуе тэла запытку, якщо декодаванне не выйшла — вяртае код 400, вызывае сервіс, якщо і гэта не выйшла — вяртае код 500, а інакше запісвае рэзультат у формате JSON:
func CreateOrder(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, "invalid request", http.StatusBadRequest)
return
}
order, err := service.CreateOrder(r.Context(), req)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, order)
}
Ніхто не назваў бы гэта майбутнім програмавання, пры тым як практычна кожны разработчык бэкенду можа адразу ж яго прачытаць. Молады інжынер можа аналізаваць код строчка за строчкай. Досвідчыты рэвізор можа затвердзіць його за хвіліну. Новы спеціяліст можа разумець логіку керування без додатковых пояснэнняў. Людзі, якіе дыбагуюць проблемы, можа точна бачыць, дзе входзі запиты, дзе ў яных можа выйсці проблема і дзе яны выходзяць.
Такая чытаемасць — не дробны прыямель; у багатох організаціях гэта галоўная важлівая аспект. Шматак коду таксама паказвае, насколькі відчутныя є проблемы мовы Go: вярненне err.Error() безпосередна кліенту можа выдаты внутршню інфармацыю, такую як запісы з базы дадзенаў, а ў рэальнай службе трэба было б зафіксаваць адказку і надасці універсальную паведамленне. Шкодзя гэтой проблеме лёгка ўсё пазначыць, таму што нічога не ўтаімаецца.
Цэннасць обмежэння крэатыўных рашэнняў
Адаптавшыся да систем, якія працуюць дзеўна, люди часта втрачаюць захопленне іх крэатыўнасцю. Павагу здобывае просты код з явнымі месцамі абяканняў, код, які не вымагае ад яго автара пояснэнняў.
Go не прывабліва ў позытыўным сэнсе: яна обмежвае колькі складных рашэнняў можа прымець команда. Гэта звучыць як крітыка, пакуль вы не будзеце адтульваць базу кода, наполненую складнымі рашэннямі. Кожная абстракцыя здавалася разумной, калі яе вводзілі. Кожны універсальны функцыя меў важлівую прычыну для існавання. Кожны выбор фрэймворку меў пераканлівае адзначэнне. А потым люди пераходзілі да іншага, выкліканыя змянамі запатранаў, і база кода ператваралася на скупку колышней впэўненасці, якую ніхто не можа цалком зрозумець.
Свяжаная простата языка Go стоіць на парадку гэтам тэндэнцу. Але ёй не завжды вдаёцася гэта зробіць. Некалітасны код на Go ўсё частаў: дуплікацыя обробкі адказоў, слабкія моделі домэна, глобальны стан, конкурэнцыя за даны, інтэфейсы, якія викорыстоўваюцца там, дзе яны не патрэбны, а таксама context.Context, які викорыстоўваецца ў кожным тэаду без рэальнага разумення механізмаў абрыву задач. Разлік у тым, што бяднасць коду на Go зазвычай ёсць відкрытая, тады як бяднасць коду на Rust можа быць набагато болей складной. Гэта не являецца недакланамасцю Rust; гэта тое, што выходзіць, калі язык дае здольным інжынерам больш прастору для праказаў іх здольнасцей.
Калі простая робота набувае складнага формата
Самэ гэтая ситуацыя може стаць проблемай для каманд, якія работаюць з бэкендам на Rust. Сама задача можа быць трывіальной, але типы, якія ёй супакоўваюць, — некалітаснымі. Наступная функцыя обрабоўвае пакет элементаў абоўязкова, чакаючы на асінхронную обработку кожнага з іх і зупыняючыся при першай адказе пра адказ:
use std::future::Future;
trait Processable {
type Output: Send + 'static;
}
async fn process_batch<F, Fut, T, E>(
items: Vec<T>,
handler: F,
) -> Result<Vec<T::Output>, E>
where
F: Fn(T) -> Fut + Send + Sync + Clone + 'static,
Fut: Future<Output = Result<T::Output, E>> + Send + 'static,
T: Processable + Send + 'static,
E: Send + 'static,
{
let mut output = Vec::new();
for item in items {
output.push(handler(item).await?);
}
Ok(output)
}
Логіка ўскладнена толькі простым цыклам. Аднак у апісацыі неабходна чытальна пазначка пра тое, што хэндлер можа быць вызваны, клонаваны і ў безпекі можа быть адправлены та разданы между потамі; што футур, які ён вяртае, є Send і мае атрыбут 'static; а таксама што типы элементаў і паказанняяў адпаведаюць тым жа аксёмам. Гэта не ўскладнены чы і не выдуманы стыль Rust. Калі разам з’яўляюцца асінхронны код, гэнерычныя хэндлеры, спільныя аксёмы, створаныя задачы, типы паказанняяў і періоды жыцця, Rust вымагае ад вас чытальна пазначыць тое, што іншыя языкі з адступнай падтрымкой залишаюць непазначаным.
Такая яснасць мае рэальную ценна, і інодзе самэўсёльва гэта тое, чаго патрэбуе система. Аднак гэта не безкоштовна. Косцы праблескваюцца ў процэсе адкалічвання, пад час перагляду коду, а таксама кожны раз, калі функцыя, якая з точкі зору продукту ёсць простая, аднойчы выявляецца складнаю з точкі зору системы типаў. Гэтыя косцы таксама прыходзяць, калі інжынер витрачае больш зусиль на перакананне компілятора прыняць певныя рашэнні, чым на раздумвання пра тое, чы гэтыя рашэнні взагалі трэба.
Краща, але краща ў чым?
Прыбічнікі Rust стверджуюць, што такіе труднасці прыводзяць да кращых систем, і інодзе яны маюць рацыю. Команда, якая працуе з бэкендам, павінна ставіць болей точныя запытанні: краща ў якой самэўсёльва меры?
- Безпека памяці: можліва, хоць і Go таксама є безпечным з пункту гледку на памяць, за выключэнням ситуацый з конкурэнціяю за память і явным выкарыстоўваннем
unsafe. - Прыдатнасць: часта.
- Запобежэнне певным багам у паралельной роботе: так, у багацько сітуацыяў.
Пасляпэдняя вимера — гэта та, па якай ацэнююць большасць бэкенд-команд, і саме тут прыямая перадчыннастка Rust ёсць найменш явная.
Рэальныя выкладкі — адтрыманне, а не стварэнне
Адзін з найлепшых інжынераў на Rust можа стварыць высокакалітэтныя системы на гэтым языку. У гэтым нема сумневаў. Проблема ў тым, што арганізацыі не можу застаўляць свою команду на тым жа рэвэле, калі ў яй быў такі інжынер. Людзі прыходзяць і адхоўляюцца, канечныя даты змінююцца, продукты пераспрямоўваюцца, а той, хто спачатку разрабоўваў архітектуру, абавязкова атрымвае падвышэння, выгорае або пераходзіць у іншую групу. Код застаецца.
У такі момент выбор языка стае менш прыкладнам да элегантнасці, а больш — да соцыяльнай стойкасці. Кальколька пытанняў можа гэта адобразіць:
- Чы можа наступны інжынер зразумець гэты код?
- Чы можа команда поступова яго перерабіць?
- Чы можа вялікі інжынер, які працуе ў режыме нарадчыка, безпечна змяніць гэта, не памячаючы ўсёўказанага графа типоў?
- Як шыбка новапрацаваны спеціяліст стае продуктыўным?
- Чы система працуе у сярэднім вырахунку па колькі дзён, а не толькі у ідеальных умовах?
Go зазвычай дае болей позытывныя адказы на гэтыя запитанні. Не таму, што разработчыкі на Go ўжо болей кваліфікованыя, і не таму, што код на Go за сваёю сутнісцю ўжо чысты, а таму, што яго синтаксіс залишае мінімум можлівасцяў для складнасцей. Гэта таксама прычына, чаму дзеякі інжынеры адчуваюць разчароўванне. Go не падлещваецца да своіх корыстувачаў. Rust можа дапамогчы ці ўсвядоміць, што ты ствараеш чыжоў значны продукт, тады як Go можа стварыць вражанне, што ты проста займаешся практычнымі завданнямі.
Разработка бэкенду большасцю часу — це практычныя завданні. Вода должна тэкць, трубы павінны быць лёгкія да знаходжэння, а наступны спецыяліст павінен магчыма ўзаменіць клапан, не вучыцца спачатку всей історіі будовы.
Дзе Rust явная правая адпрацоўка
Нічыга з гэтага не значыць, што Rust падходзіць усюды. Калі працэздатнасць, безпека памяці і низкі рэжым кантролю є ключовымі аспектамі таго, што вы ствараеце, Rust заслуговае на серйозную увагу. Якщо збойвыя ситуацыі є непрыемными, якщо багі, связаныя з безпекай памяці, стварають рызык для безпекі, або якщо затрымка є самым продуктом, а не простаю метрактурой, Rust можа стаць найрозумнейшым вароантом.
Проксі, базы дадзеных, среды выканання мов, інструменты безпекі, вбудоўаныя системы, сеткі высокай працэздатнасці, інструменты для разработчыкаў і дзеякія сервісы інфраструктуры належаць да гэтай катагорыі. У такіх случаях выбор Rust — це зрэлая інжынерная рашэнне.
Стандартны API для задньага фону, тым часам, не становяцца болей зрелымі, калі яны пісаны на Rust. Інодзе ўсё, што яны становяцца, — гэта даскладнейшымі. Команды выбіраюць Rust з правильных прычын, але яны таксама выбіраюць яго таму, што Go здаецца занадта простым, Java — занадто корпоратывным, Python — занадта свабодным, а Rust здаецца тым, што павінны выбраць серйозныя інжынеры. Гэта не ўзгляд інжынера. Це эстэтычная незацікавленасць, перадатая пад маскай рашэння пра програмаванне систем.
Шырокі спосаб прымэння рашэння
Rust є адпрацоўаным выборам, калі выпало большасць з гэтых умов:
- сервіс яўляецца інфраструктурой, дзе кантроль працызнасці чы розмеру памяці є часткай продукту,
- у команды вялікае колькасць апытных інжынераў, якія ведаюць Rust, і ёсць можлівасць найняць яшчэ,
- збой чы баг, стосаваны кантролю памяці, можа прынесці серьозныя наследкі з точка зору безпекі чы бізнесу.
Go чы іншая мова, якая прадае чытаемасць, є безпечнейшым выборам, калі выпало большасць з гэтых умов:
- Сэрвіс пераважна часткаю перадае даны между базамі даных, чергамі та API,
- У команды разнараджаныя апыткі і стаўіцца высокая змяннасць калектыва,
- Главныя рызыкі — гэта неяснае адпаведальнасць, змены ў вакалікацыях та слабая можлівасць стэкхолдароў зафіксаваць ситуацыю, а не проста низка працэсавання даных.
Узагальненне
Гэніяльнасць Rust ніколі не была пад сумневам. Важнае — гэта чы роўна гэтая гэніяльнасць являеся тым, чаго не хапяе ваша команда для рэалізацыі бэкенду.
Якщо команда складаецца з кваліфікаўных інжынераў, якія ведуць работу з інфраструктурой, дзе гарантіі Rust адразу паўтараюцься ў рызыках, тады выбірайте Rust без жадных сумневоў; выбіраць болей эфектывны ўстатку правільна, калі цэга таго выкарыстоўвае. Акрумуляцыя, якшо команда стварае традыцыйныя сэрвісы, якія перадаюць даны между базамі даных, чергамі, API, панелямі монітарынгу та внутрашняйімі інструментамі, выбіранне Rust можа свідчыць больш пра высокія вакалікацыі, чым пра ўжо высокую зрэласць.
Go не ўважаецца прыямнейшым выборам таму, што ён моцнейшы. Для багацькох команд, якія працуюць з бэкендам, ён прыямнейшы таму, што з якім лёгкае працаваць: ёго лёгкая чытаць, наймаць спецыялістаў, пераглядаць і запускать, а таксама лёгкае падтрымваць у стані, калі першачынны энтузіазм згасае. Стан, калі ён не выдзеляецца, — гэта не нявыкарыстоўванне; гэта тое, чаго патрэбуюць прыменныя системы дзеўальна пасля таго, як усі ажыянне згасаюць.
Няўспэхі, якія на самай працоўцы пагубляюць команды, якія працуюць з адзюнктным слоем, рэдка калі выходзяць з-паўнейшага абсэнту інструмента для перакантролю запозычэнняў. Яны выходзяць з нечыста апісаных меж служб, практык перапрыбутку, якія не є ідэмпотентнымі, баз дадзеных, якія таямніча сталі рэальным API, абаранак, якія працуюць з усіма можлівымі спрощэннямі ў дизайне, логах, якія рассказваюць толькі частку правды, а таксама з архітектурамі, створанымі для ідеалізаваных каманд, якіх няма на практыцы. Rust запобегае багатым класам памылак, але ён не можа запобегчы памылці выбору моці, калі команде трэба яснасць. Самэ таму Rust ў большасці случаў являецца паслабленым выборам для роботы з адзюнктным слоем: не таму, што ён слабы, а таму, што яго перавагі становяць вялікую цэну, калі основная проблема стоўіць у людзях.
Спаднёю літэратура
- Edge Isolates і Wasm протык Node.js: Выбар каскаду выканання і практыкі працы ў продакшэне — Аналізуе, як V8 і WebAssembly працуюць краща за Node.js на адлеглых серверах, а таксама рассказвае пра практыкі керування, якія робяць Node.js гатовым да выкарыстання ў продакшэне.
- REST API для пачаткуючых: Рэсурсы, методы, коды стану і абсэнція стану — Практычны падход да таго, што такое REST API, якія прынцыпы яго работы, дзе ён выкарыстоўваецца у рэальных командах, і як стварыць та працаваць з першым REST API.