Облак и експлоатация13 мин

Грешката при първата промяна за деня почти никога не е лош късмет

От · основател и главен изпълнителен директор на Hábil и Fernando Mendez Carmona · Бекенд инженер в Hábil ·

Грешка, която се появява от време на време при първата промяна за деня, обикновено е заспала връзка. Как да я докажете, поправите и избегнете развалени данни.

Грешка, която се случва «от време на време, при първата промяна за деня», почти никога не е лош късмет. Обикновено е връзка, която е заспала, кеш, който е запазил нещо, което вече не върши работа, или часовник, за който никой не си спомня. И това може да се докаже, преди да се поправи.

Представете си една обикновена сутрин в онлайн магазин. Човекът, който се грижи за каталога, идва, отваря системата и прави първата промяна за деня: коригира описанието на един продукт и го запазва. Всичко изглежда нормално. Половин час по-късно някой от продажбите пише обезпокоен: този продукт се появява в търсачката на магазина без цена и без наличност. Или по-лошо — вече не се появява.

Никой не е пипал цената. Никой не е изтривал нищо. Човекът от каталога отново отваря продукта и го запазва още веднъж, без да променя нищо. Продуктът се връща цял. Записва се като «дреболия» и денят продължава. Следващия път, когато се случи, дни или седмици по-късно, някой отново го «поправя на ръка».

В случая, който разказваме тук, онези, които го преживяваха, преценяват, че са живели с тази повреда дълго време. Когато най-накрая някой я преброи, в тестова среда, тя се появи няколко пъти само за един месец. И винаги имаше една и съща форма: един-единствен продукт, първият, който се редактира през деня.

Тази статия е за онзи, който ръководи операциите или технологиите, и за екипа, който поддържа системите. Обхваща четири неща:

  1. Какво всъщност се случваше.
  2. Защо тези грешки оцеляват с години.
  3. Как се доказва и поправя: повредата се възпроизвежда нарочно, поправя се причината, не се допуска един неуспех да запише развалени данни и се проверява чрез сравняване на старото с новото.
  4. Какво да попитате на следващия си комитет и откъде да започнете тази седмица.

Какво всъщност се случваше

Търсачката на един онлайн магазин почти никога не чете директно от базата данни, в която живее каталогът. Тя чете от индекс за търсене: копие на каталога, организирано така, че нещата да се намират бързо. Между двете има процес, който копира всяка промяна. Той може да се изгради например с MariaDB като база, Logstash като процеса, който копира, и Elasticsearch като индекс. Този процес работи цял ден: всяка минута пита «какво се промени?» и пренася промените в индекса.

За да сглоби всеки продукт, процесът трябва да направи втора заявка към базата: да донесе вариантите му, тоест версиите на един и същ продукт, всяка със своята цена и наличност. За да не отваря нова връзка всеки път, той пази една отворена и я използва повторно.

Проблемът започва през нощта. Никой не редактира продукти, така че тази връзка стои неподвижна много часове. А базата данни, по замисъл, затваря връзките, които дълго време нямат активност. В MariaDB и в MySQL, за връзките на програми (не тези на човек в конзола), това време се нарича wait_timeout и, ако никой не го промени, е 28 800 секунди: осем часа [1][2]. Ръководството на MySQL го казва направо: по подразбиране сървърът затваря връзката след осем часа, ако нищо не се е случило [3].

Базата затваря своята страна, но процесът не разбира. Продължава да пази връзката, сякаш е жива. На следващата сутрин идва първата промяна, процесът използва тази мъртва връзка и заявката за вариантите се проваля. Клиентът на базата открива, че връзката вече не съществува, едва когато се опита да я използва [3]. В Java, с драйвера на MySQL (софтуерната част, която говори с базата от името на програмата), същото събитие излиза като «Communications link failure». Самият драйвер подсказва причината: период без използване, по-дълъг от wait_timeout на сървъра [4].

Дотук това би била грешка, при която се опитва отново, и толкова. Онова, което я прави скъпа, е това, което става след това.

Неуспехът, който записва върху добрите данни

Когато заявката за вариантите се провали, филтър като jdbc_streaming на Logstash не спира продукта. Добавя му етикет за грешка и слага стойността по подразбиране в полето за вариантите. Ако никой не е настроил тази стойност, полето остава като списък с един-единствен празен елемент: продуктът остава без данни за варианти. И филтърът го пуска да продължи по пътя си [7]. Кодът на филтъра го показва ясно; документацията го споменава само мимоходом, когато описва кеша (cache_expiration) [5].

Продуктът, вече без варианти, стига до индекса за търсене. И индексът го записва със същия идентификатор, който вече имаше добрият продукт. Тази подробност, която обикновено е предимство, тук е самата щета. При обичайното действие за запис (index) върху обикновен индекс записването на документ с вече съществуващ идентификатор обновява целия документ и увеличава версията му [15][16]. Не остава повреден продукт до добрия; остава повреденият на мястото на добрия.

В случая втората промяна за деня вече не се проваляше: най-вероятно след грешката мъртвата връзка е била изхвърлена и е била отворена друга. Това зависи от драйвера и от мениджъра на връзки, не от филтъра; добре е да се потвърди в дневника. Затова следващата редакция «го оправяше» и грешката удряше само първия.

Трима обичайни заподозрени: връзка, кеш или часовник

Случаят има собствени имена, но моделът — не. Когато една грешка се появява «от време на време» и почти винаги по едно и също време на деня или след уикенд, добре е да се търси нещо, което има таймер.

Заспала връзка. Прекъсването може да дойде от три места, а клиентът разбира едва когато отново използва връзката:

  • Базата данни, както в този случай: осем часа по подразбиране в MariaDB и MySQL [1][2].
  • Мрежата. В Amazon Web Services (AWS), например, изходът към интернет на една частна мрежа (NAT Gateway) смята за изтекла връзка, която прекарва 350 или повече секунди без активност. На онзи, който се опита да продължи да я използва, отговаря с пакет за нулиране на TCP (RST), който прекъсва връзката [11]. Мрежовият балансьор на AWS, който разпределя връзките между няколко сървъра, също прекъсва на 350 секунди по подразбиране. При TCP връзки тази стойност може да се настрои между 60 и 6000 секунди [12]. Защитна стена (firewall), устройството, което филтрира трафика между две мрежи, може да направи същото, без да предупреди никого. Internet Storm Center на SANS документира през 2007 г. такъв случай: защитна стена изтриваше сесиите с един час без използване между сървър за приложения и база Oracle [13].
  • Операционната система. Механизмът, който поддържа връзките живи (keepalive на TCP), зависи от операционната система и от начина, по който е отворена всяка връзка. В случая на SANS той се задействаше след два часа — твърде късно за защитната стена [13].

Кеш. Кешът е краткосрочна памет: запазва един отговор, за да не го иска отново. Ако запази празен или грешен отговор, го повтаря, докато не изтече. Филтър като jdbc_streaming, например, с включен кеш (така е по подразбиране), може да го прави. При временна грешка той запазва празния резултат за тези параметри, докато не изтече времето му — пет секунди по подразбиране [5][7].

Часовник. Много процеси за копиране решават какво да четат по един час: «донеси ми онова, което се е променило след последния път». Този час се нарича воден знак (watermark) [6] и има капан, който ще видим по-долу: не винаги казва онова, което човек мисли.

Защо тези грешки оцеляват с години

Един директор може да се запита, с право, как една грешка, която всички са виждали, може да трае толкова дълго. Отговорът почти никога не е небрежност. Причината е, че този вид грешка има четири черти, които я пазят:

  • Оправя се сама. Следващата редакция оставя продукта наред. Грешка, която се коригира сама, унищожава собствените си доказателства: когато някой от технологиите дойде да провери, вече няма какво да се види.
  • Никой не я възпроизвежда. За да се види, трябва да се изчака цяла нощ без активност и да се направи промяна веднага след това. Никой не прави това в нормален работен ден. Така грешката никога не се появява, когато някой я търси.
  • Поправя се на ръка и се забравя. Повторното запазване на продукта отнема минута. Без доклад няма преброяване, а без преброяване няма приоритет.
  • Симптомът се появява далеч от причината. Онези, които виждат проблема, са от продажбите, в търсачката. Причината е в едно споразумение за времена между две машини, което никой не е бил записал. Продажбите не знаят, че тази връзка съществува, а онзи, който я е настроил, не вижда търсачката.

Как се доказва и поправя

Изкушението, когато най-накрая някой заподозре причината, е да се промени конфигурацията и да се чака. Ако грешката спре да се появява, тя се смята за решена. Проблемът е, че «спря да се появява» е точно онова, което вече се случваше между една повреда и следващата. Една периодична повреда се доказва, преди да се поправи. Това са четирите стъпки, които следваме.

1. Да се възпроизведе нарочно

Ако повредата зависи от часовник, часовникът се скъсява. В тестова среда екипът намалява wait_timeout на базата до, да речем, 60 секунди. Има една подробност, която мнозина пропускат: всяка връзка взема тази стойност в момента, в който се отваря [1][2]. Намаляването на общата стойност не засяга вече съществуващите връзки. Затова практичното е да се рестартира процесът за копиране, за да се роди неговата връзка с кратката стойност.

После се изчакват няколко минути, редактира се един продукт и се гледа. В дневника на процеса се появява етикетът за грешка на филтъра; в индекса — продуктът без варианти. Тест от минути замества изчакване от осем часа.

Тестът, който убеждава всички, е този в реално време. В контролиран тест, след повече от 10 часа без активност, първата редакция се провали, а следващата мина добре. Това беше точният симптом, който бизнесът виждаше, предизвикан нарочно и с точен час.

2. Да се поправи причината, а не симптомът

Има три стандартни отговора на връзка, която заспива, и те са едни и същи в почти всяка технология: да се проверява преди използване, да се подновява, преди да изтече, или да се държи будна с периодичен трафик.

Във филтър като jdbc_streaming на Logstash, например, първият отговор е опция: jdbc_validate_connection, която по подразбиране е изключена [5]. Когато се включи, процесът проверява връзката, преди да я използва. Ако е мъртва, я изхвърля и отваря друга [8][9].

Тук е нюансът, който почти никой не чете. Придружаващата я опция, jdbc_validation_timeout, е документирана като «на колко време да се проверява» и по подразбиране е 3600 секунди [5]. В действителност работи различно: библиотеката, която управлява връзките, проверява само онези, които са неизползвани повече от това време [9]. Връзка, прекарала нощта неподвижна, е неактивна много повече от час. Така че при стойността по подразбиране тя се проверява и осемчасовото прекъсване от базата е покрито.

Но прекъсването може да дойде от мрежата и да е по-кратко, като 350-те секунди на балансьор или на NAT. Там връзка с десет минути без използване не би се проверила и би пристигнала мъртва. Срещу това прекъсване е нужна друга мярка: прагът да се свали под прекъсването или връзката да се поддържа жива с трафик, по-чест от прекъсването [11].

В Java принципът е същият. HikariCP, широко използван мениджър на връзки, препоръчва максималният живот на една връзка да е с няколко секунди по-кратък от всеки лимит на базата или на инфраструктурата. Той предлага също връзките да се поддържат живи с периодична проверка [10]. А драйверът на MySQL за Java не препоръчва своята опция за автоматично повторно свързване [4]: повторното свързване на сляпо не е официалният отговор.

3. Да не се допусне един неуспех да запише развалени данни

Поправката на връзката предотвратява грешката от всяка сутрин. Тя не предотвратява следващата грешка, която никой не си е представил. Затова втората промяна е толкова важна, колкото и първата: ако част от продукта не е могла да се донесе, не се записва нищо.

С Logstash, например, това се постига с условие след филтъра. Ако продуктът носи етикета за грешка на заявката (_jdbcstreamingfailure), той се изхвърля и не стига до индекса [7]. Предишният продукт, цял, остава там, където си беше.

Трябва да се разграничат два случая, които си приличат. Продуктът, който се е провалил, носи два етикета: този за грешка и този за «използвана е стойността по подразбиране» (_jdbcstreamingdefaultsused) [7]. Продукт, който наистина няма варианти, носи само втория. Затова условието гледа единствено етикета за грешка: продуктът без варианти продължава да се копира, нарочно, и спира само онзи, който се е провалил при заявката за тях.

Това решение има цена и тя трябва да се каже. Ако заявката иска само онова, което е след водния знак, и знакът вече е преместен, изхвърлената промяна не се възстановява сама. Така работеше в случая; във вашата система го потвърдете с реалната заявка. Продуктът остава с предишната си версия, докато някой не го редактира отново или докато индексът не се презареди. За един директор правилото се разбира без код: по-добре вчерашната цена, отколкото никаква цена. Продукт с вчерашните данни се продава; продукт без цена изчезва.

Дълбокият урок за техническия екип: използването на идентификатора на продукта като идентификатор в индекса прави повторението на едно зареждане безвредно, защото крайният резултат е същият [15]. Същото свойство е онова, което вреди, когато документът пристига непълен. Това, че едно зареждане може да се повтори без вреда, не го прави безопасно: нужно е също то да не записва, когато нещо липсва.

4. Да се провери часовникът и да се валидира спрямо старото

Процесът за копиране решава какво да копира с воден знак: «донеси ми онова, което се е променило след последния час, който видях» [6].

Капанът е, че часът, който се записва като «последна промяна», не винаги е часът, в който промяната е била запазена. Два термина помагат. Израз (statement) е инструкция, която се дава на базата, например «промени тази цена». Транзакция е пакет от изрази, който се запазва целият заедно или изобщо не се запазва; потвърждаването ѝ е моментът, в който нейната промяна е запазена и другите могат да я видят.

В MySQL NOW() връща часа, в който изразът е започнал, а не часа, в който е бил потвърден [14]. Да предположим, че колоната за последна промяна се попълва с NOW() и че една голяма промяна се запазва бавно: започва в 10:00 и се потвърждава в 10:03. Нейните редове казват «10:00». Междувременно процесът за копиране вече е прочел по-нови промени и е преместил своя знак по-напред. Когато голямата промяна най-накрая се потвърди, редовете ѝ носят час, по-ранен от знака, и следващото четене вече не ги иска. Губят се безшумно. Този риск е от същото семейство ограничения, които екипът на Debezium посочва за улавянето на промени чрез периодични заявки: то може да изгуби междинни промени и не вижда изтриванията [17].

Има два начина да се затвори тази врата:

  • Времеви марж. «Чета само онова, което е поне на една минута.» Знакът вече не напредва върху секунди, които все още могат да получат промени. Това намалява риска, но не е гаранция: транзакция, на която ѝ трябва повече от минута, за да се потвърди, все още може да се изгуби.
  • Да се чете дневникът на базата вместо колона с час. Това се нарича улавяне на промени чрез дневника (CDC, от английското change data capture). Пример е Debezium, който чете двоичния журнал (binary log) на MariaDB или MySQL. Този журнал пази операциите в реда, в който са били потвърдени, включително изтриванията [17][18]. Добре настроено, това затваря точно този прозорец, в замяна на повече части, които трябва да се настройват и експлоатират.

В случая, който разказваме, беше избран маржът от една минута, със записано ограничение.

Остава въпросът, който всеки директор би трябвало да зададе: как знаем, че поправката не е развалила нещо, което преди работеше? Отговорът в случая беше пълно сравнение. Старата и новата заявка бяха пуснати върху целия каталог, в тестова среда, и дадоха точно един и същ резултат. Новата заявка промени само кога чете. Всяка разлика би била нова грешка. Нямаше такава.

А след това — отново нощният тест. В контролиран тест, след повече от 16 часа без активност, първата редакция мина без грешка: точно случаят, който преди се проваляше. Освен това продуктите, които бяха останали повредени, бяха поправени, и при приключването на прегледа в тестовата среда не оставаше нито един повреден.

Пет въпроса за вашия следващ комитет

  1. Кои грешки в нашите системи «се оправят сами»? Попитайте продажбите, обслужването на клиенти и онзи, който поддържа каталога, а не само технологиите. Най-добрите следи обикновено са у онзи, който поправя на ръка.
  2. Случва ли се някоя винаги по едно и също време, в понеделник или след нощ без използване? Ако отговорът е да, зад нея има часовник.
  3. Знаем ли колко време издържа без използване всяка връзка между нашите системи, и кой я прекъсва пръв: базата, мрежата или облакът? Ако никой не го е записал, никой не го е решил.
  4. Когато един процес не може да донесе цялата информация, какво прави? Оставя ли онова, което е имало, или публикува нещо наполовина?
  5. Когато технологиите кажат «готово е», как са го доказали? Предизвикали ли са повредата преди поправката и отново след нея, или просто са спрели да я виждат?

Ако два или повече отговора са «не знам», проблемът не е в късмета: липсва измерване.

Откъде да започнете тази седмица

Не е нужен проект. Нужен е един списък. Помолете звената, които експлоатират вашите системи, да записват в продължение на две седмици всеки случай, когато нещо «се е оправило само» или «е поправено на ръка»: какво е било, в колко часа и какво е направено. В края на двете седмици подредете списъка по час. Ако няколко записа се струпват рано сутрин или след уикенд, вече имате първия си заподозрян. На вашия технически екип му стига един следобед, за да започне да го възпроизвежда в тестова среда.

Какво прави Hábil

Когато една повреда се повтаря, екипът SWAT на Hábil търси дълбоката причина, а не поредното рестартиране: възпроизвежда я, поправя я и ви предава доказателства как е проверена, за да можете да ги прегледате.

Първият разговор е безплатен: целта му е да ви помогне да определите коя повреда се повтаря, от кога и как може да се докаже. Пишете ни в WhatsApp.

Източници

  1. MariaDB, Server System Variables, wait_timeout (28 800 s по подразбиране; стойността за сесията се задава при стартиране на връзката, от глобалната стойност или от interactive_timeout). https://mariadb.com/docs/server/server-management/variables-and-modes/server-system-variables#wait_timeout
  2. MySQL 8.4 Reference Manual, Server System Variables, wait_timeout (28 800 s; стойността за сесията се копира от глобалната при започване на всяка връзка). https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html#sysvar_wait_timeout
  3. MySQL 8.4 Reference Manual, MySQL server has gone away (затваряне след осем часа без активност; грешки CR_SERVER_GONE_ERROR и CR_SERVER_LOST). https://dev.mysql.com/doc/refman/8.4/en/gone-away.html
  4. MySQL Connector/J, файл със съобщенията (клон release/9.x, commit 1363d9b, консултиран на 20-окт-2026) на драйвера («Communications link failure»; подсказка за wait_timeout; autoReconnect не се препоръчва). https://github.com/mysql/mysql-connector-j/blob/1363d9bf5412c9adb246756a8334ee2d584a3c8f/src/main/resources/com/mysql/cj/LocalizedErrorMessages.properties
  5. Elastic, Jdbc_streaming filter plugin (jdbc_validate_connection, jdbc_validation_timeout, tag_on_failure, default_hash; в cache_expiration — че при временни грешки кешът запазва празни резултати и се прилага default_hash). https://www.elastic.co/docs/reference/logstash/plugins/plugins-filters-jdbc_streaming
  6. Elastic, Jdbc input plugin (воден знак с :sql_last_value, tracking_column, schedule; препоръка връзките да се проверяват между изпълненията). https://www.elastic.co/docs/reference/logstash/plugins/plugins-inputs-jdbc
  7. logstash-integration-jdbc 5.6.6 (commit f7586e6, консултиран на 20-окт-2026), код на филтъра и на неговия обработчик на заявки (при грешка: етикет за грешка, етикет за стойност по подразбиране, default_hash и събитието продължава; кеширане на празния резултат). https://github.com/logstash-plugins/logstash-integration-jdbc/blob/f7586e67912852d5c5a6ec0f4db5f4a4f30e7806/lib/logstash/filters/jdbc_streaming.rb · https://github.com/logstash-plugins/logstash-integration-jdbc/blob/f7586e67912852d5c5a6ec0f4db5f4a4f30e7806/lib/logstash/plugin_mixins/jdbc_streaming/statement_handler.rb
  8. logstash-integration-jdbc 5.6.6 (commit f7586e6), код на модула за връзка (активира валидатора на Sequel с jdbc_validation_timeout). https://github.com/logstash-plugins/logstash-integration-jdbc/blob/f7586e67912852d5c5a6ec0f4db5f4a4f30e7806/lib/logstash/plugin_mixins/jdbc_streaming.rb
  9. Sequel, разширение connection_validator (проверява при вземане на връзката от пула, само ако е неактивна повече от прага). https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/connection_validator_rb.html
  10. HikariCP, README (maxLifetime, keepaliveTime: периодична проверка с Connection.isValid() или с тестова заявка при стари драйвери). https://github.com/brettwooldridge/HikariCP
  11. AWS, Troubleshoot NAT gateways, връзки, които изтичат след 350 s без активност, и отговор с RST. https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-troubleshooting.html#nat-gateway-troubleshooting-timeout
  12. AWS, Update the idle timeout for your Network Load Balancer (350 s по подразбиране; настройва се от 60 до 6000 s само при TCP слушатели; при TLS е фиксирано на 350 s). https://docs.aws.amazon.com/elasticloadbalancing/latest/network/update-idle-timeout.html
  13. SANS Internet Storm Center, Bojan Zdrnja, «Gone in 3600 seconds: story about TCP Keep-Alives» (8-ное-2007). https://isc.sans.edu/diary/3623
  14. MySQL 8.4 Reference Manual, Date and Time Functions, NOW() (час на започване на израза). https://dev.mysql.com/doc/refman/8.4/en/date-and-time-functions.html#function_now
  15. Elastic, Elasticsearch output plugin (document_id за презаписване на документи със същия идентификатор). https://www.elastic.co/docs/reference/logstash/plugins/plugins-outputs-elasticsearch
  16. Elastic, Index API (индексирането със съществуващ идентификатор обновява целия документ и увеличава версията му). https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-index
  17. Gunnar Morling, «Five Advantages of Log-Based Change Data Capture», блог на Debezium (19-юли-2018). https://debezium.io/blog/2018/07/19/advantages-of-log-based-change-data-capture/
  18. Debezium, конектори за MySQL и за MariaDB (четене на двоичния журнал в реда на потвърждаване). https://debezium.io/documentation/reference/stable/connectors/mysql.html · https://debezium.io/documentation/reference/stable/connectors/mariadb.html

Вашата дейност среща ли тези предизвикателства?

Да обсъдим вашия проект

Предпочитате имейл? Пишете ни на hola@habil.mx