Ориентация главы
Второй проект — центр конфигурации (configd): централизованное управление конфигурацией группы сервисов — JSON-конфигурация поставляется файлом или по HTTP, поддерживается послойное переопределение по окружениям (трёхслойное слияние: умолчания/разработка/продакшен), валидация (типы и ограничения), опциональное раскрытие переменных шаблона ({$env} — внедрение фактов окружения) и горячая замена (перечитывание по изменению файла). Отличие формы от Проекта 1 (глава 138, одноразовый инструмент) принципиально: конфигурация — долгоживущее состояние — её загружают, кэшируют, по ней спрашивают, она может обновляться — управление жизненным циклом и конкурентный доступ впервые становятся главной темой. Эта глава — глава проектирования: требования и приёмка, выбор модели данных, слоистость и выбор модулей, семантика горячей замены и конкурентности, политика валидации и безопасность шаблона — всё фиксируется до написания реализации (глава 140). Методологическая позиция проектной главы: код — исполнение проекта, а переделки чаще всего растут из проектной нечёткости.
139.1引入
Проблема конфигурации выглядит тривиально — «прочитать JSON, чего там». В реальной системе сложность конфигурации приходит из четырёх измерений: разнообразие источников (файл / HTTP-интерфейс / переменные окружения / командная строка — должно уметь комбинироваться); послойное переопределение (умолчания перекрываются слоем окружения, слой окружения — слоем инстанса — семантика слияния должна быть явной); запаздывающая валидация (ошибочная конфигурация взрывается лишь при первом использовании — перехватывать надо уже на старте); распространение обновлений (какие компоненты перечитают изменившуюся конфигурацию, что делать с операциями, которые используют старые значения). Нечёткость в любом измерении превращается в продакшен-инцидент: перепутан порядок слияния — продакшен работает на значениях разработки; валидации нет — порт 0 «успешно стартует»; гонка горячей замены даёт двум потокам несогласованную половину конфигурации.
Эта глава превращает каждое из четырёх измерений в явное проектное решение — у каждого решения есть отвергнутые альтернативы и обоснование (методология таблицы выбора главы 138, поднятая на архитектурный уровень). Реализация следующей главы исполнит эти решения построчно — в этом приёмочность проектной главы: когда реализация закончена, сверка возвращается к каждой строке — каждое решение либо соблюдено, либо явно пересмотрено.
139.2概念
Требования и критерии приёмки
Пользовательские сценарии configd (форма user story): UC1 загрузка — при старте сервиса конфигурация вытягивается из config.json (или HTTP-эндпойнта); при отказе — явная ошибка (молчаливо работать с пустой конфигурацией не разрешается). UC2 слоистость — глубокое слияние двух слоёв default.json → config.{env}.json (вложенные объекты рекурсивно, массивы заменяются целиком — семантика замены объявлена явно); UC3 запрос — взятие значения по пути (server.port, db.pool.max) с ожиданием типа (запросили целое — получили целое, несовпадение типа — ошибка); UC4 валидация — полная проверка на этапе загрузки (обязательные существуют / типы верны / значения разумны), ошибка несёт путь в конфигурации (db.pool.max: must be > 0 — применение модели ошибок главы 4); UC5 шаблон — строковые значения поддерживают раскрытие {$ENV_HOME} (внедрение фактов окружения в конфигурацию); UC6 горячая замена — при изменении файла перезагрузка, атомарная передача старой и новой конфигурации (в любой момент запрос видит либо всё старое, либо всё новое — смеси не существует).
Критерии приёмки (в главе реализации сверяются построчно): (1) у каждого из шести UC есть позитивные и негативные случаи; (2) у семантики слияния есть тестовые векторы (вложенное переопределение / замена массива / конфликт типов); (3) тест гонки горячей замены (согласованность конкурентных запросов во время обновления — threads-форма главы 133); (4) все ошибки валидации несут путь; (5) OOM по каждой точке (загрузка конфигурации содержит выделения памяти — дисциплина главы 134); (6) ноль следов при неудачной загрузке (половинная конфигурация не просачивается наружу — атомарность).
Выбор модели данных: динамический value или специальная структура
Первое и самое важное решение — что такое конфигурация в памяти:
| Вариант | Форма | Преимущества | Недостатки |
|---|---|---|---|
| A: динамический value (глава 31, xvalue) | весь JSON загружается в дерево value | совместим с любой схемой (эволюция конфигурации — ноль правок кода); запросы идут общими путями; JSON-десериализация за один шаг | медленные запросы (поиск по строковому пути); нет проверки на этапе компиляции |
| B: специальная структура | struct config { int Port; struct db Db; } | самый быстрый доступ, типобезопасность | изменение схемы требует править структуру и код загрузки; слияние многих источников пишется вручную |
| C: загрузка в value + снимок в структуру | загрузка/слияние/валидация в домене value, после загрузки — разовое извлечение в структуру | оба мира: гибкость на этапе загрузки + скорость на этапе использования | код извлечения требует сопровождения (зеркало схемы) |
Решение: C. Обоснование: у конфигурации разные частота эволюции (схема меняется часто — в домене value это бесплатно) и частота чтения (горячий путь — возможен запрос на каждый запрос — структура даёт O(1)) — сопоставление по этапам. B отвергнуто: слияние и шаблон в домене структур писать вручную слишком тяжело; A отвергнуто: строковый поиск запросов попадает в горячий путь. Зеркальная цена C держится под контролем макросов извлечения (одна строка объявления на поле — реализация в главе 139).
Слоистость и выбор модулей
Каждый из семи слоёв — главная тема какой-то предыдущей главы — выбор модулей целиком из существующих глав (без новых зависимостей): очередное подтверждение тезиса «проект = рекомбинация существующих слоёв». Контракты между слоями: поток байтов → дерево value → слитое дерево value → прошедшее валидацию дерево value → снимок в структуре — каждая стрелка — чистое преобразование данных (без побочных эффектов, отказ атомарен).
Горячая замена и конкурентность: семантика атомарной передачи
Ядро проектирования горячей замены — протокол передачи старой и новой конфигурации. Выбор configd: неизменяемый снимок + атомарная замена указателя. Продукт загрузки — неизменяемая структура (после загрузки её никто не меняет — потокобезопасность даром, конфигурационная версия неизменяемого разделения главы 115); сторона запросов держит указатель с подсчётом ссылок (общая ссылка главы 5 — Ref взял одну, после использования Destroy); обновление = построить новый снимок → атомарно заменить глобальный указатель (атомарная операция главы 9) → старый снимок освобождается сам, когда счёт ссылок доходит до нуля. Гарантия семантики: в любой момент любой спрашивающий видит целиком согласованную конфигурацию (либо старую, либо новую — смеси не бывает); спрашивающий внутри долгой операции продолжает пользоваться старым снимком до её конца (graceful-передача — без принудительного выдёргивания). Отвергнутые альтернативы: глобальная блокировка чтения-записи (блокировка входит в горячий путь запросов — отвергнуто); обновление изменяемой структуры на месте (конкурентный ад — отвергнуто).
Слои валидации: семантическая правильность за пределами синтаксической законности
JSON-парсер гарантирует синтаксис (глава 32); слой валидации отвечает за семантику: существование (обязательные ключи — нет ключа, есть ошибка); тип (значение ожидаемого типа — server.port обязан быть целым); диапазон (port 1..65535, pool.max > 0 — бизнес-ограничения). Все три выполняются один раз на этапе загрузки (перехват на старте — «работать с ошибочной конфигурацией» не разрешается); формат ошибки 路径: 消息 (путь: сообщение) (db.pool.max: must be > 0) — ошибки с путём избавляют разборщика от листания всего JSON. Выражение правил валидации: небольшой декларативный массив (путь ключа + тип + обратный вызов ограничения) — правила как данные: новое поле — одна строка правила (та же экономика сопровождения, что у макросов извлечения).
Момент раскрытия шаблона и граница безопасности внедрения
Раскрытие {$VAR} происходит после слияния, до валидации — обоснование: продукт шаблона может влиять на валидацию (валидировать надо порт уже после раскрытия); а слияние происходит в исходном домене ({$BASE} слоя умолчаний не раскрывается до перекрытия слоем окружения — переопределение приоритетнее внедрения). Границы безопасности: раскрывать только переменные окружения (источник из белого списка — глава 43); не раскрывать рекурсивно (если в значении {$A} снова {$B} — не раскрывается; защита от цепочки внедрения); неопределённая переменная — ошибка (не молчаливая пустая строка — конфигурация ссылается на несуществующее окружение, это ошибка развёртывания). Эти три правила — контракт безопасности слоя шаблона, конфигурационная версия дисциплины паролей главы 77: плоскость внедрения защищается на каждой границе «строка становится конфигурацией».
139.3示例
Первая полная программа: минимальный контур загрузки конфигурации
Программа ниже из examples/data/json — загрузка JSON в value и запросы (учебниковая форма слоя разбора configd):
*/ #include <stdio.h> #include <xrt.h> */ static xjsonvisitaction printJsonEvent( const xjsonevent* pEvent, ptr pUserData ) { (void)pUserData; if ( pEvent->HasName ) { printf("member: %.*s\n", (int)pEvent->Name.Size, pEvent->Name.Data); } return XJSON_VISIT_NEXT; } int main(void) { xjsonreadconfig ReadConfig; xjsonwriteconfig WriteConfig; xjsonwriter* pWriter; xvalue* pRoot; xvalue* pName; xstrview Name; str sText; size_t iSize; /* ---- 1) DOM:解析 → 随机访问 → 美化序列化 ---- */ pRoot = xrtJsonParse(XRT_STR_LITERAL( "{\"name\":\"xrt\",\"features\":[\"json\",\"http\"]}" )); if ( pRoot == NULL ) { return 1; } /* ObjectGet 返回借用指针(树拥有它);GetString 取出视图。 */ pName = xrtValueObjectGet(pRoot, XRT_STR_LITERAL("name")); if ( !xrtValueGetString(pName, &Name) ) { xrtValueRelease(pRoot); return 2; } printf("name = %.*s\n", (int)Name.Size, Name.Data); /* Stringify 产物是拥有式字符串(xrtFree);之后树即可释放。 */ sText = xrtJsonStringify(pRoot, true, &iSize); xrtValueRelease(pRoot); if ( sText == NULL ) { return 3; } printf("%.*s\n", (int)iSize, sText); xrtFree(sText); /* ---- 2) SAX:同一份输入按事件流走一遍,不建树 ---- */ xrtJsonReadConfigInit(&ReadConfig); if ( xrtJsonVisit( XRT_STR_LITERAL("{\"code\":200,\"ok\":true}"), &ReadConfig, printJsonEvent, NULL ) != XJSON_VISIT_DONE ) { return 4; } /* ---- 3) Writer:增量构建 {"code":200,"message":"OK"} ---- */ xrtJsonWriteConfigInit(&WriteConfig); pWriter = xrtJsonWriterCreate(&WriteConfig); if ( (pWriter == NULL) || !xrtJsonWriterObject(pWriter) || /* 开对象 */ !xrtJsonWriterName(pWriter, XRT_STR_LITERAL("code")) || !xrtJsonWriterInt(pWriter, 200) || !xrtJsonWriterName(pWriter, XRT_STR_LITERAL("message")) || !xrtJsonWriterString(pWriter, XRT_STR_LITERAL("OK")) || !xrtJsonWriterEnd(pWriter) || /* 闭对象 */ !xrtJsonWriterFinish(pWriter) /* 完整性校验 */ ) { xrtJsonWriterFree(pWriter); return 5; } /* Take 移交文本(xrtFree 释放);Take 后 writer 仍需 Free。 */ sText = xrtJsonWriterTake(pWriter, &iSize); xrtJsonWriterFree(pWriter); if ( sText == NULL ) { return 6; } printf("%.*s\n", (int)iSize, sText); xrtFree(sText); return 0; }
Что только что произошло. (1) JSON-текст → дерево value → запрос поля → сериализационный круговорот — полный поток данных слоя разбора проходит в десятке строк. (2) Представление value, возвращаемое запросом, — ровно вход слоя валидации configd (проверка типов делается по нему). (3) Этот замкнутый контур — стартовая точка реализации главы 140: снаружи надеть «слой источников (файл/HTTP) → слой слияния → слой шаблона» — и получится полный конвейер загрузки — у каждого слоя, выбранного в проектной главе, есть такой исполняемый атом (архитектурная версия «образец как шлюз» главы 138: проектирование не бумажное занятие — у каждого выбора есть проверенная деталь).
Вторая полная программа: загрузка шаблонного файла
Вторая программа из examples/template/file — комбинация шаблона и файла (материальная опора слоя шаблона):
*/ #include <stdio.h> #include <xrt.h> int main(void) { xtemplate* pTemplate = NULL; xvalue* pData = NULL; str sOutput = NULL; int iResult = 1; pTemplate = xrtTemplateCompileFile("examples/template/file/page.tpl"); pData = xrtValueObject(); if ( (pTemplate == NULL) || (pData == NULL) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("name"), xrtValueString(XRT_STR_LITERAL("Alice")) ) ) { goto cleanup; } sOutput = xrtTemplateRender(pTemplate, pData, NULL); if ( sOutput == NULL ) { iResult = 2; goto cleanup; } printf("%s\n", sOutput); iResult = 0; cleanup: xrtFree(sOutput); if ( pData != NULL ) { xrtValueRelease(pData); } if ( pTemplate != NULL ) { xrtTemplateRelease(pTemplate); } return iResult; }
Что только что произошло. (1) CompileFile = чтение файла + компиляция за один шаг — комбинация слоя источников и слоя шаблона уже существует (глава 35): раскрытие {$VAR} в configd может переиспользовать шаблонизатор (семантика внедрения переменных та же) или упроститься до специализированной замены строк (меньше) — обе альтернативы на столе, глава реализации выберет по бюджету размера. (2) Контекст переменных шаблона приходит из дерева value (внедрение контекста главы 35) — в комбинации с переменными окружения (глава 43) это полная форма «конфигурация ссылается на окружение». (3) Проектный вывод: выбор слоя раскрытия не блокирует остальные слои — выигрыш слоистости снова налицо: смена реализации раскрытия — замена одного слоя.
Форма проектного документа: эта глава как шаблон
В какой форме писать проектную главу? Не большая UML-диаграмма, а список решений — у каждого решения пять элементов: проблема (что решаем), варианты (минимум две альтернативы), решение (что выбрали), обоснование (почему победило), проверка (как узнать, что решение исполнено). Демонстрация этой главы: модель данных (выбор из трёх, обоснование — сопоставление частот по этапам, проверка — число строк макросов извлечения); протокол передачи (выбор из трёх, обоснование — структура уничтожает состояние гонки, проверка — threads-тест без смешанных состояний); семантика слияния (явное объявление, проверка — тестовые векторы); безопасность шаблона (три границы, проверка — случаи внедрения). Почему не UML: у диаграмм большое пространство двусмысленностей (из двух диаграмм каждый читает своё), а у списка решений каждая строка сверяема — та же философия, что у шлюзов BOOK_SPEC (машинная проверяемость важнее человеческой интерпретации). Этот проект Проекта 2 будет построчно цитироваться в начале главы реализации (глава 140) — читатель увидит, что отображение «решение → код» и есть настоящая поставка этой главы: не документ, а сверяемый набор ограничений. Проектная глава чат-сервиса (глава 141) наследует тот же шаблон — переиспользование проектной методологии опережает переиспользование кода.
От инструмента к сервису: повторный проход уроков Проекта 1
Семь уроков ретроспективы главы 138 проходятся заново в контексте configd — форма другая, уроки те же: приёмка прежде (шесть UC и шесть критериев против пяти — приёмка сервиса обязана покрывать конкурентность и жизненный цикл, инструмента — нет); форма решает контейнер (дерево value против массива счётчиков — «эволюционность» данных становится новым измерением: форма данных инструмента фиксирована, у сервиса может эволюционировать); представления живут одним кадром (слой валидации работает по представлению value, извлечение — только в слое снимка — без раннего закрепления); слоистая заменяемость (слой источников файл/HTTP заменяем против семейства Reader — та же дисциплина, поднятая до семи слоёв); факты платформы входят в проект (факты платформы сервиса — модель конкурентности и соглашения о расположении файлов конфигурации); бюджет прежде (загрузка один раз + горячий путь запросов — бюджет указывает необходимость слоя снимка); тестирование в четырёх квадрантах (сервис добавляет квадранты threads и гонки горячей замены). Все семь пройдены заново, два усилены — таков учебный замысел последовательности проектов: не разные проекты, а углубление одной методологии в разных ограничениях.
139.4契约
- Приёмка прежде: шесть пользовательских сценариев и шесть критериев приёмки — глава реализации сверяет построчно; без сверки нет завершения.
- Решение о модели: домен загрузки в value + домен снимка в структуре — сопоставление по этапам частоты эволюции и частоты чтения.
- Семантика слияния: объекты сливаются рекурсивно, массивы заменяются целиком — семантика объявлена явно и закреплена тестовыми векторами.
- Атомарная передача: неизменяемый снимок + подсчёт ссылок + атомарная замена указателя — целиком согласованно в любой момент.
- Graceful-передача: долгая операция держит старый снимок до конца — без принудительного выдёргивания.
- Три слоя валидации: существование / тип / диапазон — одна полная проверка на этапе загрузки, перехват на старте.
- Ошибки с путём:
db.pool.max: must be > 0— ошибка локализована путём в конфигурации. - Безопасность шаблона: только переменные окружения, без рекурсии, неопределённая — ошибка; плоскость внедрения защищена.
- Момент раскрытия: после слияния, до валидации — переопределение приоритетнее внедрения, валидация смотрит на финальные значения.
- Детали проверены: у выбора каждого слоя есть исполняемый атом (два образца этой главы) — проектирование не бумажное занятие.
Дополнительный проект HTTP-источника (сетевая расширенная форма UC1)
Помимо файлового источника, UC1 допускает HTTP-эндпойнт — три пункта дополнительного проекта: сборка клиента (easy-слой главы 98 или конструктор главы 99: вытягивание конфигурации — редкая операция, easy достаточно; верификатор обязателен — «не проверять — не вариант» главы 85 в конфигурационной сцене ещё строже: захваченный источник конфигурации равен захваченным всем нисходящим потребителям); семантика отказа (неудача вытягивания — ошибка по UC1 — но можно спроектировать «при отказе жить с прошлым снимком + тревога» — стратегия деградации есть решение развёртывания: первый старт (нет старого снимка) обязан закончиться отказом, отказ обновления на ходу терпим со старой конфигурацией — два момента, две стратегии); граница кэша (семантика кэширования HTTP-ответа — режим only-if-cached автокэша главы 101 — ровно «в оффлайне работать с кэшированной конфигурацией» — точное отображение четырёх режимов на конфигурационную сцену). HTTP-источник поднимает configd с «локального файлового инструмента» до «сетевого клиента конфигурации» — но в семислойном конвейере меняется только слой источников (третье исполнение дисциплины слоистой замены главы 138).
Типичные ошибки
Симптом: слой окружения хотел «переопределить только db.pool.max», а заменил весь объект pool (min вернулся к умолчанию — число продакшен-соединений резко меняется); или наоборот, хотели заменить весь массив, а он слился поэлементно со слоем умолчаний.
Причина: семантика слияния не объявлена явно — поведение «глубокого слияния» для объектов и массивов по умолчанию различается от библиотеки к библиотеке. Решение проектного этапа должно быть записано тестовыми векторами: вложенные объекты рекурсивно, массивы заменяются целиком — у каждого слияния есть файл ожиданий на вход и выход.
/* поведение слияния не вписано в контракт: слияние массивов — как вздумается реализации */ merge(a, b); /* массивы поэлементно? целиком заменить? — никто не знает */
/* контракт прямо: объекты рекурсивно, массивы заменой + закреплено тройкой тестовых векторов */ merge_objects_recursive(); replace_arrays_whole();
Симптом: ошибочная конфигурация успешно стартует — взрывается в три часа ночи при первом обращении к той функции (худшее время отказа).
Причина: ленивая валидация откладывает отказ до момента, когда разбор труднее всего. Полная проверка на этапе загрузки = перехват на старте — отказ вскрывается ещё при развёртывании (основа критерия (3)).
int get_port(void) { return cfg_get_int("server.port"); /* взрыв при первом использовании: три часа ночи */ }
/* полная валидация в конце загрузки: существование/тип/диапазон — отказ старта с путём */ if ( !cfg_validate(&tree, rules) ) { fail_with_path(); }
Симптом: в момент обновления конкурентные читатели видят наполовину новую конфигурацию — server.port уже новый, а server.tls ещё старый (новый порт со старым TLS — рукопожатие падает).
Причина: изменяемая структура + запись на месте без синхронизации = состояние гонки. Протокол атомарной передачи (неизменяемый снимок + замена указателя) уничтожает «смешанное состояние» структурно — не дисциплиной блокировок, а невозможностью.
struct config g_cfg; /* глобально изменяемая */ void hot_update(struct config* n) { g_cfg.port = n->port; /* окно наполовину нового: конкурентный читатель видит смесь */ g_cfg.tls = n->tls; }
cfg_snapshot* fresh = build_snapshot(&tree); /* новый снимок (неизменяемый) */ atomic_swap(&g_current, fresh); /* атомарная замена указателя */ /* читатель: Ref одну -> читать -> Destroy — целиком согласованно */
Сопоставление проекта configd с индустриальными формами проясняет границы компромиссов. etcd/Consul: распределённое KV-хранилище + watch-пуш — у односнимочной модели configd нет watch и протокола согласованности (это главная тема распределённых систем — за пределами книги, но интерфейс зарезервирован: атомарная замена указателя снимка и есть упрощённая форма «локального watch»). Spring profiles: активация и комбинация многих профилей — гибче и сложнее фиксированного двухслойного слияния configd; двухслойное переопределение закрывает 90% сценариев (умолчания + окружение), больше слоёв — делу расширения. Производные переменных окружения (12-factor): вся конфигурация внедряется из окружения — слой шаблона configd ровно мост к файловой конфигурации (гибрид: скелет файла + внедрение окружения — практический компромисс двух мировоззрений). Вывод сопоставления: configd — «минимальная полнота, которой достаточно» — каждый сложный признак реальной системы отображается в какую-то точку расширения этого проекта (слой источников / число слоёв слияния / глубина шаблона), и это ещё одна приёмка полноты проекта.
Практические задания
Напишите три группы тестовых векторов слияния (слой умолчаний + слой окружения → ожидание): частичное переопределение вложенных объектов, замена массива целиком, конфликт типов (умолчание int, окружение даёт string — ожидается ошибка с путём). Критерий приёмки: вручную выведенный результат становится файлом ожиданий тестов главы реализации.
Нарисуйте последовательность горячей замены: поток загрузки (построить новый снимок → атомарная замена) и два потока запросов (Ref → чтение → Destroy) в трёх переплетениях — что каждый видит до / во время / после замены. Критерий приёмки: видимость каждого случая выводится из «неизменяемость + атомарный указатель» — смешанных состояний нет.
Для правдоподобной конфигурации (три секции server/db/log) напишите полный набор правил валидации (путь + тип + ограничение диапазона) и три намеренно нарушающих входа. Критерии приёмки: нарушающие входы все перехвачены на старте, ошибки содержат путь; число строк правил того же порядка, что число полей конфигурации (проверка экономики сопровождения).
Итоги
| Тема | Кратко |
|---|---|
| Позиция проектной главы | решения прежде кода — у каждого альтернативы и обоснование |
| Модель | домен загрузки в value (эволюция) + домен снимка в структуре (чтение) — сопоставление по этапам |
| Семислойный конвейер | источники → разбор → слияние → шаблон → валидация → снимок → запросы — чистые преобразования данных |
| Семантика слияния | объекты рекурсивно / массивы заменой — явное объявление + тестовые векторы |
| Протокол передачи | неизменяемый снимок + подсчёт ссылок + атомарная замена указателя — целиком согласованно в любой момент, graceful-передача долгих операций |
| Три слоя валидации | существование / тип / диапазон — один раз на этапе загрузки, перехват на старте |
| Ошибки с путём | путь + сообщение — разбор без листания всего файла |
| Безопасность шаблона | только переменные окружения / без рекурсии / неопределённая — ошибка |
| Момент раскрытия | после слияния до валидации — переопределение приоритетнее внедрения, валидация по финальным значениям |
| Детали проверены | у выбора каждого слоя исполняемый атом — проект приземлён на факты |
| HTTP-дополнение | easy-слой + верификатор обязателен; первый старт обязан падать, отказ обновления — деградация |
📖 Справочники API в этой главе:value (159 записей)template (51 записей)core (25 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки