- BrainTools - https://www.braintools.ru -

Почему эволюция не прошла бы код-ревью: инженерный разбор гемоглобина

Представьте, что вам на собеседовании дали задачу: спроектировать систему доставки газа по трубам переменного давления, причём система должна загружаться почти на 100% в зоне высокого давления, а разгружаться (быстро и почти полностью) в зоне низкого. Вы бы, наверное, нарисовали линейную зависимость. Больше давления — больше загрузка. Просто, и главное, что будет легко тестировать.

Эволюция [1] посмотрела на этот вариант, подумала 500 миллионов лет и сделала всё наоборот.


Техзадание от природы

Предлагаю чуть формализовать. У нас есть два «дата-центра»:

  • Лёгкие — давление кислорода [2] ~100 мм рт. ст. Тут надо загрузиться по максимуму.

  • Ткани — давление ~20–40 мм рт. ст. Тут надо отдать почти всё.

И есть молекула, которая мотается между ними по кровотоку. Задача: максимизировать разницу между «загрузился» и «разгрузился».

Если вы инженер (а вы, возможно, инженер), первая мысль — линейная зависимость, где загрузка пропорциональна давлению. То есть чем больше кислорода вокруг, тем больше связалось. Честный и весьма понятный даже школьникуy = kx.

Но проблема в том, что линейная кривая это палка о двух концах для этой задачи.

Вот посмотрите. При линейной зависимости разница насыщения между 100 мм рт. ст. и 40 мм рт. ст., ну, примерно 60% от максимума. Вроде нормально? К сожалению нет. Потому что в реальности ткани хотят получить кислород именно в узком диапазоне давлений, и линейная зависимость отдаёт его слишком равномерно (понемножку на каждом миллиметре давления). Как микросервис, который размазывает CPU по всем запросам одинаково, вместо того чтобы приоритизировать.


S-кривая, или «нелинейность как фича»

Гемоглобин — белок из четырёх субъединиц, каждая может связать одну молекулу O₂. Казалось бы, четыре независимых слота. Четыре boolean-а. Давайте загрузим по одному.

Но тут тоже нет.

Когда первая молекула кислорода садится на первую субъединицу, происходит конформационное изменение — белок меняет форму. И эта новая форма делает посадку второй молекулы легче. Вторая делает легче третью. Третья (соответственно) четвёртую.

binding_affinity[0] = LOW;    
binding_affinity[1] = MEDIUM; 
binding_affinity[2] = HIGH;   
binding_affinity[3] = VERY_HIGH; 

Это называется кооперативное связывание. И это даёт не прямую линию, а S-образную (сигмоидальную) кривую насыщения.

Внизу по давлению — кривая ползёт еле-еле., почти плоская. Гемоглобин упирается, не хочет брать кислород, а потом резкий взлёт (почти вертикальный). А уже наверху снова плато, насыщение под 98–99%.

А дальше…

При 100 мм рт. ст. (лёгкие) гемоглобин насыщен на ~98%. При 40 мм рт. ст. (ткани в покое) — на ~75%. При 20 мм рт. ст. (работающая мышца) — на ~30–35%. Разница между «загрузился» и «разгрузился» огромная, и именно в том диапазоне давлений, где это нужно.

Почему эволюция не прошла бы код-ревью: инженерный разбор гемоглобина - 1

Линейная зависимость дала бы при тех же параметрах разницу процентов в 60. Сигмоида даёт 65–70. Кажется, мелочь? Но это 65–70 на каждом цикле кровообращения, 70 раз в минуту, 24 часа в сутки. На масштабе — разница между «побежал за автобусом» и «упал в обморок, пытаясь встать с дивана».


Уравнение Хилла, или коэффициент кооперативности

Математически [3] это описывает уравнение Хилла:

Y = (pO₂)ⁿ / ((P₅₀)ⁿ + (pO₂)ⁿ)

Где Y — степень насыщения (0 до 1), pO₂ — парциальное давление кислорода, P₅₀ — давление, при котором насыщение = 50%, а n — коэффициент Хилла.

Вот этот n и есть вся суть.

При n = 1 — обычная гипербола. Так работает миоглобин (одна субъединица, без кооперативности). Загружается быстро, отдаёт неохотно. Хороший складской работник, паршивый курьер.

При n = 2.8 (реальный гемоглобин) — та самая сигмоида. Медленный старт, резкий переход и быстрое насыщение. Если б n было ровно 4 (по числу субъединиц), то это была бы идеальная кооперативность, все четыре сайта переключаются одновременно, как бистабильный триггер, но о природа не дотянула до 4. И, возможно, специально.

(Потому что если n = 4, система становится слишком «бинарной» — либо полностью загружен, либо полностью пуст. Никакой тонкой настройки. А n = 2.8 — это sweet spot между кооперативностью и гибкостью. Эволюция, проще говоря, нашла оптимум, который мы бы назвали «хорошим trade-off между throughput и latency».)


Эффект Бора: context-aware оптимизация

Ладно, S-кривая — это интересно, но гемоглобин пошёл дальше.

Почему эволюция не прошла бы код-ревью: инженерный разбор гемоглобина - 2

Работающие ткани производят CO₂ и молочную кислоту. pH падает, температура растёт ,и гемоглобин это чувствует. При снижении pH кривая сдвигается вправо и белок начинает отдавать кислород ещё активнее при том же давлении.

Это как если бы ваш балансировщик нагрузки не просто раскидывал запросы по round-robin, а мониторил температуру CPU на каждом сервере и автоматически перенаправлял трафик на холодные ноды. Только это не какой-нибудь навороченный service mesh — это одна молекула, четыре субъединицы, пара протонов.

Христиан Бор (отец того самого Нильса Бора, квантовая механика, судя по всему, передаётся генетически) описал этот эффект в 1904 году. (за 40 лет до того, как люди поняли структуру ДНК, между прочим).

# Эффект Бора в псевдокоде
if ph < 7.4 or pco2 > 40 or temperature > 37:
    p50 += delta  # сдвиг кривой вправо
    # отдаём O2 активнее

Элегантно? Как по мне — очень. Один и тот же белок одновременно транспортирует O₂, сенсит pH, реагирует на температуру и всё это без единого if-а в привычном нам смысле. Чистая биохимия, чистые аллостерические взаимодействия.


Серповидноклеточная анемия: баг, который не фиксят

А теперь баг, который прошёл code review эволюции и остался в продакшене.

Серповидноклеточная анемия [4] — одна точечная мутация в гене β-глобина, лдна аминокислота: глутаминовая кислота → валин. В позиции 6. Шестой символ в строке длиной 146. Один char.

- ...Val-His-Leu-Thr-Pro-Glu-Glu-Lys...
+ ...Val-His-Leu-Thr-Pro-Val-Glu-Lys...

Этот один символ меняет всё. Мутантный гемоглобин (HbS) при низком давлении кислорода начинает полимеризоваться — молекулы слипаются в длинные жёсткие нити. Эритроцит из мягкого пончика превращается в серп. Серпы застревают в капиллярах, рвут сосуды, вызывают адскую боль [5], инсульты, отказ органов.

Очень тяжёлая болезнь. До современной медицины это часто была смерть в детстве.

И вот вопрос: почему естественный отбор не выкосил эту мутацию за тысячи лет?

Потому что гетерозиготы (одна нормальная копия, одна мутантная) получают устойчивость к малярии. Плазмодий — паразит, который живёт внутри эритроцитов, — не может нормально размножаться в клетках с частичным содержанием HbS. Эритроцит деформируется, селезёнка его утилизирует вместе с паразитом. Грубо говоря, организм выкидывает заражённые контейнеры до того, как инфекция расползётся по кластеру.

Если посмотреть на карты, то это видно буквально: частота гена серповидноклеточности совпадает с ареалом малярийного плазмодия. Африка, Ближний Восток, Индия, Средиземноморье. Там, где малярия убивает, мутация спасает. Там, где малярии нет, — мутация просто вредит и вымывается отбором.

Это же классический полиморфизм, поддерживаемый балансирующим отбором. Или, если по-нашему, — это feature flag, который в одном окружении критичный баг, а в другом — конкурентное преимущество. И вы не можете его просто выключить, потому что для части пользователей он в проде держит SLA выше нуля.


Почему «неправильное» решение работает 500 миллионов лет

Мне кажется, что если бы кто-то принёс гемоглобин на архитектурное ревью, его бы разнесли.

— Подождите, у вас четыре субъединицы, и они все влияют друг на друга?!
— Один белок отвечает за транспорт, сенсинг pH, буферизацию CO₂ и регуляцию сосудистого тонуса? Single Responsibility Principle — не, не слышал?
— А мутация одного символа может положить всю систему? У вас что, нет валидации входных данных?

И ведь они были бы формально правы. По всем метрикам «хорошей архитектуры» гемоглобин — монолит с god object, тесной связанностью и нулевой отказоустойчивостью на уровне отдельной молекулы.

Но на уровне популяции — это антихрупкость в чистом виде. Та самая мутация, которая ломает отдельные эритроциты, в тоже время спасает людей от малярии. Кооперативное связывание даёт S-кривую, без которой ваш мозг [6] бы не получал кислород каждый раз, когда вы поднимаетесь на третий этаж. Нарушение SRP позволяет одной молекуле заменить то, что в инженерной системе потребовало бы отдельных сенсоров, контроллеров и актуаторов.

500 миллионов лет, без рефакторинга, на одной кодовой базе, и с обратной совместимостью от рыб до людей.

Я не знаю, какой из этого следует вывод для нашей работы. Но мне кажется, что гемоглобин — это ещё одно доказательство, что идеальной архитектуры не существует. Зато рабочей — вполне.

Автор: inkedsymon

Источник [7]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/28035

URLs in this post:

[1] Эволюция: http://www.braintools.ru/article/7702

[2] кислорода: http://www.braintools.ru/article/5138

[3] Математически: http://www.braintools.ru/article/7620

[4] анемия: http://www.braintools.ru/article/3432

[5] боль: http://www.braintools.ru/article/9901

[6] мозг: http://www.braintools.ru/parts-of-the-brain

[7] Источник: https://habr.com/ru/articles/1016514/?utm_campaign=1016514&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100