Сказ о том, как сделать машину времени для базы данных и случайно написать эксплойт

    Доброго времени суток, Хабр.

    Приходилось ли вам задумываться как поменять время внутри базы данных? Легко? Ну в некоторых случаях да, несложно — linux команда date и дело в шляпе. А если нужно поменять время только внутри одного экземпляра бд если их на сервере несколько? А для отдельно взятого процесса базы данных? А? Эээ, так-то, дружок, в этом-то все и дело. Кто-то скажет, что это очередной сюр, не связанный с реальностью, который периодически выкладывают на Хабре. Но нет, задача вполне реальная и продиктована производственной необходимостью — тестированием кода. Хотя соглашусь, кейс тестирования может быть достаточно экзотический — проверить как ведет себя код на определенную дату в будущем. В данной статье я рассмотрю подробно как эта задача решалась, и заодно захватим немного сам процесс организации тестовых и dev-стендов для базы Oracle. Впереди длинное чтиво, устраивайтесь поудобнее и прошу под кат.

    Предпосылки


    Начнем с небольшого вступления для того, чтобы показать зачем это нужно. Как уже было анонсировано, мы пишем тесты при реализации правок в базе данных. Система, под которую эти тесты делаются, разрабатывалась в начале(а может и слегка до начала) нулевых, поэтому вся бизнес-логика находится внутри базы данных и написана в виде хранимых процедур на языке pl/sql. И да, это приносит нам боль и страдания. Но это legacy, и с этим приходится жить. В коде и табличной модели есть возможность задавать то, как параметры внутри системы эволюционируют с течением времени, проще говоря задавать активность с какой даты и по какую дату они могут применяться. Чего далеко ходить — недавнее изменение ставки НДС яркий тому пример. И чтобы такие изменения в системе можно было проверить заранее, базу данных с такими правками нужно перенести на определенную дату в будущее, кода параметры в таблицах станут активными на «текущий момент». И ввиду специфики поддерживаемой системы нельзя использовать мок-тесты, которые бы просто изменили возвращаемое значение текущей системной даты в языке при запуске сессии тестирования.

    Итак, мы определили зачем, дальше нужно определить то как именно достигается цель. Для этого я сделаю небольшую ретроспективу вариантов построения тестовых стендов для разработчиков и то как в каждом варианте происходил запуск сессии тестирования.

    Каменный век


    Давно-давно, когда деревья были маленькими, а мейнфреймы большими, был только 1 сервер для разработки и он же проведения тестов. И в принципе всем этого хватало(640К хватит всем!)

    Минусы: для проведения задачи смены времени нужно было привлекать много смежных отделов — системных администраторов(делали смену времени на сервере субд от root), администраторов СУБД(делали перезапуск базы данных), программистов(нужно было уведомить что произойдет смена времени, потому что часть кода переставала работать, например, веб-токены, ранее выданные для вызова api методов, переставали быть валидными и это могло стать неожиданностью), тестировщиков(само проведение тестирования)… При возврате времени в настоящее все повторялось в обратном порядке.

    Средние века


    Со временем число разработчиков в отделе росло и в какой-то момент 1 сервера хватать перестало. В основном из-за того, что разные разработчики хотят менять один и тот же pl/sql пакет и проводить для него тестирование(даже и без смены времени). Все чаще были слышны возмущения: «Доколе! Хватит это терпеть! Фабрики — рабочим, землю — крестьянам! Каждому программисту — по базе данных!» Однако, если у вас продуктовая база данных несколько терабайт, а разработчиков 50-100, то положа руку на сердце прямо в таком виде требование не очень-то реальное. А еще все хотят чтобы тестовая и dev-база не очень сильно отставали от прода и по структуре, и самим данным внутри таблиц. Так появился отдельный сервер для тестирования, назовем его pre-production. Строился он из 2х одинаковых серверов, куда с прода делалось восстановление базы данных из RMAN бакапов и занимало это примерно 2-2.5 дня. После восстановления в базе делалось обезличивание персональных и других важных данных и на данный сервер подавалась нагрузка с тестовых приложений(а также и сами программисты всегда работали с недавно восстановленным сервером). Обеспечение работы с нужным сервером происходило с помощью кластерного ip-ресурса, поддерживаемого через corosync(pacemaker). В то время как все работают с активным сервером, на 2 ноде запускается снова восстановление базы данных и через 2-3 дня они опять меняются местами.

    Из очевидных минусов: нужно 2 сервера и в 2 раза больше ресурсов(в основном дисковых), чем прод.

    Плюсы: операция смены времени и проведение тестов — может проводиться на 2м сервере, на основном сервере в это время живут разработчики и занимаются своими делами. Смена серверов происходит только когда база готова, и даунтайм тестовой среды минимален.

    Эпоха научно-технического прогресса


    При переходе на базу данных 11g Release 2 мы вычитали про интересную технологию, которую предоставляет Оракл под названием CloneDB. Суть заключается в том, что бакапы продуктовой базы данных(там прямо битовая копия продуктовых датафайлов) складируются на специальный сервер, который потом через DNFS(direct NFS) публикует этот набор датафайлов на в принципе любое число серверов, при этом на сервере не требуется иметь тот же объем дисков, потому что реализуется подход Copy-On-Write: база использует для чтения данных в таблицах сетевую шару с датафайлами с сервера бакапов, а изменения пишутся в локальные датафайлы на самом dev-сервере. Периодически для сервера делается «обнуление сроков», чтобы локальные датафайлы не очень сильно росли и место не кончалось. При обновлении сервера также делается деперсонализация данных в таблицах, при этом все обновления таблиц попадают в локальные датафайлы и те таблицы читаются с локального сервера, все остальные таблицы читаются по сети.

    Минусы: серверов все так же 2(для обеспечения плавности обновления с минимальным простоем для потребителей), но зато теперь объем дисков сильно сокращается. Для хранения бакапов на nfs-шаре нужно еще 1 сервер по размеру +- как прод, но сокращается само время выполнения обновлений(особенно при использовании инкрементальных бакапов). Работа по сети с nfs-шарой заметно замедляет IO-операции чтения. Для использования технологии CloneDB база должна быть редакции Enterprise Edition, в нашем случае нам нужно было проводить на тестовых базах всякий раз процедуру upgrade. К счастью, тестовые базы попадают в исключения по лицензионной политике Oracle.

    Плюсы: операция восстановления базы из бакапа занимает меньше 1 дня(точное время уже не вспомню).

    Смена времени: без особых изменений. Хотя к этому времени уже были сделаны скрипты для изменения времени на сервере и рестарта базы данных, чтобы выполнять это не привлекая внимания санитаров администраторов.

    Эра Новой истории


    Для того чтобы еще больше экономить дискове пространство и сделать чтения данных не по сети мы решили реализовать свой вариант CloneDB(с флешбеком и снапшотами), используя файловую систему со сжатием. Во время проведения предварительных тестов выбор пал на ZFS, хотя официальной поддержки для неё нет в ядре Linux(цитата из статьи). Для сравнения смотрели еще BTRFS(b-tree fs), которую продвигает сам Оракл, но коэффициент сжатия был меньше при сравнительно одинаковом же потреблении CPU и RAM в тестах. Для включения поддержки ZFS на RHEL5 было собрано свое ядро на основе UEK(unbreakable enterprise kernel), а на более новых осях и ядрах можно просто использовать уже готовое UEK ядро. В основе реализации такой тестовой базы тоже лежит механизм COW, но уже на уровне снапшотов файловой системы. На сервер подается 2 дисковых устройства, на одном делается zfs pool, куда через RMAN делается дополнительный standby базы данных с прода, и поскольку мы используем сжатие, то раздел занимает меньше, чем продакшн.
    На второе дисковое устройство ставится система и остальное что нужно для работы сервера и самой базы, например разделы для undo и temp. В любой момент времени из zfs пула можно сделать снапшот, который потом открывается как отдельная база данных. Создание снапшота занимает пару секунд. It's magic! И таких баз данных можно наклонировать в принципе достаточно много, лишь бы на сервере хватало RAM под все экземпляры и самого размера zfs pool(под хранение изменений в датафайлах при деперсонализации и при жизненном цикле клона базы данных). Основное время обновления тестовой базы начинает занимать операция деперсонализации данных, но и она укладывается в 15-20 минут. Налицо значительное ускорение.

    Минусы: на сервере нельзя поменять время просто переводом системного времени, потому что тогда сразу все экземпляры базы данных, работающие на этом сервере, попадут в это время. Решение для этой проблемы было найдено и будет описано в соответствующим разделе. Забегая вперед скажу, что оно позволяет менять время внутри только 1 экземпляра базы данных(подход смены времени per instance) при этом не затрагивая остальные на этом же сервере. И время на самом сервере тоже не меняется. Так отпадает необходимость в рутовом скрипте для смены времени на сервере. Также на этом этапе реализована автоматизация смены времени для экземпляров через Jenkins CI и пользователям(условно говоря команды разработчиков), которые владеют своим стендом, даны права на джобы, через которые они могут сами менять время, обновлять стенд до актуального состояния с прода, делать снапшоты и восстановление(откат) базы до ранее созданного снапшота.

    Эра Новейшей истории


    С приходом Oracle 12c появилась новая технология — pluggable databases и как следствие контейнерные базы данных(cdb). С этой технологией внутри 1 физического экземпляра можно сделать несколько «виртуальных» баз, которые разделяют общую область памяти экземпляра. Плюсы: можно сэкономить память для сервера(и повысить общую производительность нашей базы данных, потому что всю память, которую занимали до того например 5 разных экземпляров, можно отдать в общее пользование для всех развернутых pdb-контейнеров внутри cdb, и они будут ее использовать только когда им это реально понадобится, а не так как было на предыдущей фазе, когда каждый экземпляр «блокировал» выданную ему память под себя и при низкой активности какого-то из клонов память не использовалась эффективно, проще говоря простаивала). Датафайлы разных pdb все также лежат в zfs pool и при разворачивании клонов используют все тот же мех-м zfs-снапшотов. На этом этапе мы приблизились достаточно близко к возможности давать почти каждому разработчику свою базу данных. Смена времени на этом этапе не требует перезапуска базы данных и работает совсем точено только для тех процессов, которым смена времени необходима, все остальные пользователи, работающие с этой бд никак не затрагиваются.

    Минус: нельзя использовать подход со сменой времени per instance из предыдущей фазы, потому что инстанс у нас сейчас один. Однако, решение и для этого случая было найдено. И именно оно и послужило толчком для написания это статьи. Забегая вперед, скажу, что оно представляет собой подход смены времени per process т.е. в каждом процессе базы данных можно установить вообще свое уникальное время.

    Типовая сессия тестирования в этом случае сразу после подключения к бд задает в начале своей работы нужное время, проводит тесты и в конце возвращает время обратно. Возврат времени нужен по одной простой причине: часть процессов базы Оракл не завершаются при дисконнекте клиента базы данных от сервера, это серверные процессы под названием shared servers, которые в отличие от dedicated процессов, запускаются при старте сервера базы данных и живут практически бесконечно(в идеальной картине мира). Если оставить время измененным в таком серверном процессе, то потом другое подключение, которое в этом процессе будет обслуживаться, будет получать неправильное время.

    В нашей системе shared servers используются много, т.к. до 11g адекватного решения для нашей системы выдерживать highload нагрузку практически не существовало(в 11g появился DRCP — database resident connection pooling). И вот почему — в субд есть лимит на общее число серверных процессов, которое она может создать как в dedicated, так и в shared режиме. Dedicated процессы порождаются медленнее, чем база может выдать уже готовый shared процесс из пула разделяемых процессов, а значит при постоянном поступлении новых подключений(особенно если процесс делает еще какие-то медленные операции) общее число процессов будет расти. При достижении лимита сессий/процессов база данных перестает обслуживать новые соединения и наступает коллапс. Переход на использование пула разделяемых процессов позволил уменьшить число возникающих новых на сервере процессов при подключениях.

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

    Подход «fake per instance»


    Как поменять время внутри базы данных?

    Первое что приходило в голову — создать в схеме, которая содержит весь код бизнес логики, свою функцию, которая перекрывает функции языка, работающие с временем(sysdate, current_date и т.д.) и при определенных условиях начинает давать другие значения, например можно было задавать значения через контекст сессии в начале запуска тестов. Не вышло, встроенные функции языка не перекрыть пользовательскими.

    Затем были проверены системы легкой виртуализации (Vserver, OpenVZ) и контейнеризации через docker. Тоже не работает, они используют то же самое ядро что и хост-система, а значит используют те же значения системного таймера. Снова отпадает.

    И тут нам на помощь приходит не побоюсь этого слова гениальное изобретение мира Linux — переопределение/перехват функций на этапе динамической загрузки shared objects. Многим оно известно как трюки с LD_PRELOAD. В переменной окружения LD_PRELOAD можно указать библиотеку, которая загрузится раньше всех остальных, которые нужны процессу, и если в этой библиотеке есть символы с тем же именем как например в стандартной libc, которая загрузится позднее, то таблица импорта символов для приложения будет выглядеть как если функцию предоставляет наш подменный модуль. И именно это и делает библиотека проекта libfaketime, которую мы стали использовать для того, чтобы запустить базу данных в другом времени отдельно от системного. Библиотека пропускает через себя вызовы, которые касаются работы с системным таймером и получения системного времени и даты. Управлять тем, на сколько сместится время относительно текущей даты сервера или от какого момента времени время должно пойти внутри процесса — все управляется переменными окружения, которые нужно задать вместе с LD_PRELOAD. Для реализации смены времени у нас был реализован job на сервере Jenkins, который заходит на сервер базы данных и перезапускает СУБД либо с установленными переменными окружения для libfaketime либо без них.

    Примерный алгоритм запуска базы данных с подменным временем:

    export LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so
    export FAKETIME="+1d"
    export FAKETIME_NO_CACHE=1
    
    $ORACLE_HOME/bin/sqlplus @/home/oracle/scripts/restart_db.sql
    

    И если выдумаете, что все сразу заработало, то глубоко заблуждаетесь. Потому что оракл, как оказалось, валидирует те библиотеки, которые загружаются внутрь процесса при старте СУБД. И в алертлоге начинает возмущаться о замеченном подлоге, при этом база не стартует. Сейчас уже точно не помню как избавиться, есть какой-то параметр, которым можно отключить таки выполнение sanity-проверки при старте.

    Подход «fake per process»


    Общая идея с изменением времени только внутри 1 процесса оставалась той же — использовать libfaketime. Мы запускаем базу данных с предзагруженой в нее библиотекой, но ставим нулевое смещение времени при запуске, которое пропагандируется потом во все процессы СУБД. А потом внутри тестовой сессии выполнить установление переменной окружения только для этого процесса. Пфф, делов-то.

    Однако, для тех кто близко знаком с pl/sql языком сразу понятна вся обреченность этой идеи. Потому что язык сильно ограничен и в основном годится для задач высокоуровневых. Никакого системного программирования там не реализовать. Хотя некоторые низкоуровневые операции(например работа с сетью, работа с файлами) присутствуют в виде предустановленых системных dbms/utl-пакетов. За все время работы с Ораклом я несколько раз делал реверс-инжиниринг предустановленных пакетов, код некоторых из них скрыт от глаз посторонних(они называются wrapped). Если тебе чего-то запрещают смотреть, то соблазн узнать как оно устроено внутри только возрастает. Но часто даже после анвраппера не всегда есть что посмотреть, потому что функции таких пакетов реализуются как c interface к so-библиотекам на диске.
    Итого, мы подошли к 1 кандидату на реализацию — технология c external procedures.
    Оформленная специальным образом библиотека может экспортировать методы, которые потом база оракл может вызывать через pl/sql. Кажется многообещающим. Всего один раз я встречался с этим на курсах по Advanced plsql, поэтому помнил очень отдаленно как это готовить. И это значит нужно читать документацию оракла. Прочитал — и сразу приуныл. Потому что загрузка такой пользовательской so-библиотеки идет в отдельном процессе-агенте через листенер базы данных, и общение с этим агентом идет через дблинк. А значит плакала наша идея установить переменную окружения внутри самого процесса базы данных. И сделано это все из соображений безопасности.

    Картинка из документации, которая показывает как это работает:



    Тип библиотеки so/dll — не так важно, но картинка почему-то только под винду.

    Возможно кто-то заметил тут еще 1 потенциальную возможность. Да-да, это Java. Оракл позволяет писать код хранимых процедур не только на plsql, но и на java, которые тем не менее экспортируются все равно как plsql методы. Периодически я делал такое, поэтому с этим проблем возникнуть не должно. Но тут был спрятан очередной подводный камень. Java работает с копией энвайромента, и позволяет только получать переменные окружения какие были у JVM процесса при старте. Встроенная JVM наследует переменные окружения процесса базы данных, но на этом все. Я видел в интернете советы как поменять readonly map через reflection, но что толку, ведь это все равно только копия. То есть баба осталась опять у разбитого корыта.

    Однако Java это не только ценных мех. С ее помощью можно порождать процессы изнутри процесса базы данных. Хотя все небезопаные операции нужно разрешать отдельно через механизм java grants, которые делаются с помощью пакета dbms_java. Изнутри plsql кода можно получить process pid текущего серверного процесса, в котором код выполняется, с помощью системных представлений v$session и v$process. Дальше мы можем породив какой-то дочерний процесс от нашей сессии что-то с этим pid сделать. Для начала я вывел просто все переменные окружения, которые есть внутри процесса базы данных(для проверки гипотезы)

    #!/bin/sh
    
    pid=$1
    
    awk 'BEGIN {RS="\0"; ORS="\n"} $0' "/proc/$pid/environ"
    

    Ну вывел, а дальше-то что. Поменять переменные в файле environ все равно нельзя, это данные которые были переданы в процесс при его старте и они read only.

    Я поискал решение в Интернете на stackoverflow «как поменять переменную окружения в другом процессе». Большая часть ответов была что это невозможно, но был один ответ, который описывал эту возможность как нестандартный и грязный хак. И этим ответом был Альберт Эйнштейн gdb. Отладчиком можно прицепиться на любой процесс зная его pid и выполнить в нем любую функцию/процедуру, которая в нем существует как публично экспортируемый символ, например из какой-то библиотеки. В libc есть функции работы с переменными окружения и libc загружается в любой процесс базы Оракл(да и практически всякой программы на linux).

    Вот так реализуется установление переменной окружения в чужом процессе(вызывать нужно от root из-за используемого сискола ptrace):

    #!/bin/sh
    
    pid=$1
    env_name=$2
    env_val="$3"
    
    out=`gdb -q -batch -ex "attach $pid" -ex 'call (int) setenv("'$env_name'", "'"$env_val"'", 1)' -ex "detach" 2>&1`
    


    Также и чтобы посмотреть переменные окружения внутри процесса gdb тоже годится. Как было уже сказано ранее файл environ из /proc/pid/ показывает только переменные, которые существовали на старте процесса. А если процесс создал что-то в ходе своей работы, то это увидеть можно только через отладчик:
    #!/bin/sh
    
    pid=$1
    var_name=$2
    
    var_value=`gdb -q -batch -ex "attach $pid" -ex 'call (char*) getenv("'$var_name'")' -ex 'detach' | egrep '^\$1 ='`
    
    if [ "$var_value" == '$1 = 0x0' ]
    then
      # variable empty or does not exist
      echo -n
    else
      # gdb returns $1 = hex_value "string value"
      var_hex=`echo "$var_value" | awk '{print $3}'`
      var_value=`echo "$var_value" | sed -r -e 's/^\$1 = '$var_hex' //;s/^"//;s/"$//'`
      
      echo -n "$var_value"
    fi
    


    Итак, решение уже у нас в кармане — через яву мы спауним процесс отладчика, который заходит на породивший его процесс и ставит ему нужную переменную окружения и потом завершается(мавр сделал свое дело — мавр может уйти). Но было ощущение, что оно какое-то костыльное. Хотелось чего-то более элегантного. Как-то бы все-таки заставить сам процесс базы данных устанавливать себе переменные окружения без внешнего рукоприкладства.

    Яйцо в утке, утка в зайце...


    И тут на помощь нам приходит кто, да, вы правильно догадались, снова Java, а именно JNI(java native interface). JNI подзволяет вызывать native методы, написаные на си, внутри JVM. Код оформляется специальным образом в виде shared object библиотеки, которую потом загружает JVM, при этом методы, которые были в библиотеке, мапятся на методы java внутри класса, объявленные с модификатором native.

    Ну ок, пишем класс(на самом деле это только заготовка):

    public class Posix {
    
        private static native int setenv(String key, String value, boolean overwrite);
    
        private static native String getenv(String key);
        
        public static void stub() 
        {
            
        }
    }
    

    После этого компилируем его и получаем сгенерированый h-файл будущей библиотеки:

    # компилируем исходник
    javac Posix.java
    
    # создаст файл Posix.h в котором описаны декларации нативных методов для JNI
    javah Posix
    

    Получив заголовочный файл напишем тело для каждого метода:

    #include <stdlib.h>
    #include "Posix.h"
    
    JNIEXPORT jint JNICALL Java_Posix_setenv(JNIEnv *env, jclass cls, jstring key, jstring value, jboolean overwrite)
    {
        char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
        char* v = (char *) (*env)->GetStringUTFChars(env, value, NULL);
    
        int err = setenv(k, v, overwrite);
    
        (*env)->ReleaseStringUTFChars(env, key, k);
        (*env)->ReleaseStringUTFChars(env, value, v);
    
        return err;
    }
    
    JNIEXPORT jstring JNICALL Java_Posix_getenv(JNIEnv *env, jclass cls, jstring key)
    {
        char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
        char* v = getenv(k);
    
        return (*env)->NewStringUTF(env, v);
    }
    

    и скомпилируем библиотеку

    gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -fPIC Posix.c -shared -o libPosix.so -Wl,-soname -Wl,--no-whole-archive
    
    strip libPosix.so
    

    Для того чтобы Java могла загрузить native-библиотеку, она должна быть найдена системным ld по всем правилам Linux. Дополнительно у Java есть набор property, которые содержат пути, где поиск библиотек происходит. Для работы внутри оракл проще всего положить нашу библиотеку в $ORACLE_HOME/lib.

    И уже после того, как мы создали библиотеку, нужно скомпилировать класс внутри базы данных и опубликовать его как plsql пакет. Для создания ява классов внутри базы данных есть 2 варианта:

    • загрузить бинарный class-файл через утилиту loadjava
    • скомпилировать код класса из исходников с помощью sqlplus

    Мы воспользуемся вторым способом, хотя они в принципе равноправны. Для первого случая нужно было сразу написать весь код класса на 1 этапе, когда мы получали класс-заглушку для h-файла.

    Для создания ява класса в субд используется специальный синтаксис:

    CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "Posix" AS
    ...
    ...
    /
    

    Когда класс создан, его нужно опубликовать как plsql методы, и тут опять специальный синтаксис:

    procedure set_env(var_name varchar2, var_value varchar2)
    is
    language java name 'Posix.set_env(java.lang.String, java.lang.String)';
    

    При попытке вызова потенциально небезопасных методов внутри Java возбуждается эксепшн, который говорит, что не выдан java grant для пользователя. Загрузка native методов — это еще какая небезопасная операция, потому что мы инжектируем посторонний код прямо внутрь процесса базы данных(тот самый эксплойт, который был анонсирован в заголовке).

    Но поскольку база тестовая, даем грант без особых опасений подключившись от sys:

    begin
    dbms_java.grant_permission( 'SYSTEM', 'SYS:java.lang.RuntimePermission', 'loadLibrary.Posix', '');
    commit;
    end;
    /
    

    Имя пользователя system — это тот, куда я компилировал код java и plsql wrapper package.
    Важно заметить, что при загрузке библиотеки через вызов System.loadLibrary мы опускаем префикс lib и расширение so(то как это описано в документации) и не передаем никакой путь где искать. Есть аналогичный метод System.load, который может загрузить библиотеку только с использованием абсолютного пути.

    И тут нас ожидает 2 неприятный сюрприз — я угодил в очередную кроличью нору Оракла. При выдаче гранта возникает ошибка с довольно туманным сообщением:

    ORA-29532: Java call terminated by uncaught Java exception: java.lang.SecurityException: policy table update
    

    Проблема гуглится в интернете и ведет на My Oracle Support (aka Metalink). Т.к. по правилам Оракл публиковать в открытых источниках статьи с металинка запрещено, то упомяну только номер документа — 259471.1 (те у кого есть доступ смогут прочитать сами).

    Суть проблемы — в том что Оракл не даст нам просто так разрешить загрузку подозрительного стороннего кода себе в процесс. Что логично.

    Но раз база тестовая и мы в своем коде уверены, разрешаем загрузку без особых опасений.
    Фух, злоключения на этом все закончены.

    It's alive, alive


    С замиранием сердца я решил попробовать вдохнуть жизнь в своего Франкенштейна.
    Запускаем базу данных с предзагруженым libfaketime и 0 смещением
    Подключаемся в базу и делаем вызов кода который просто выведет время до и после изменения переменной окружения:

    begin
    dbms_output.enable(100000);
    dbms_java.set_output(100000);
    dbms_output.put_line('old time: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss'));
    system.posix.set_env('FAKETIME','+1d');
    dbms_output.put_line('new time: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss'));
    end;
    /
    


    Работает, черт подери! Честно сказать, я ждал каких-то еще сюрпризов, например ORA-600 ошибок. Однако в алертлоге было все число и код продолжал работать.
    Важно отметить, что если подключение в базу делается как dedicated, то после завершения соединения процесс уничтожится и никаких следов не останется. Но если мы используем shared соединения, то в этом случае из серверного пула выделяется уже готовый процесс, мы в нем меняем время через переменные окружения и при отключении оно внутри процесса так и останется измененным. И когда потом другая сессия базы данных попадает в тот же серверный процесс, она будет получать неправильное время к своему немалому удивлению. Поэтому при завершении сессии тестов лучше всегда возвращать время обратно к нулевому смещению.

    Заключение


    Надеюсь, что рассказ был интересным(и может даже кому-то полезным).

    Исходные коды все доступны на Github.

    Документация по libfaketime — тоже.

    А как вы делаете тестирование? И как создаете dev- и test-базы в компании?

    Бонус для дочитавших до конца

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      0
        0
        Cпасибо за интересную идею. Ее как оказалось мы не смотрели при отборе вариантов. Но тут есть момент: время то полностью заморозится в таком случае. А нам нужно чтобы оно продолжало тикать и после модификации.
        0

        Вообще обычно правильнее использовать в своём коде свою кастомную функцию вместо sysdate/current_xxx. Это наиболее распространённый, простой и гибкий подход.

          0
          Пфф, как не красиво. Зачем делать для прода кастомную функцию, которая заменяет собой системные функции (и при этом на проде время не нужно менять)?
          Если переписывать вообще все что есть на тестовой среде под такие требования — код будет от прода отличаться. К тому же никто не сможет запретить написать код без использования кастом функции для времени. Спасибо, но нет.
            0

            Я себе это вижу немного по-другому. Везде в коде использовать кастомную функцию. А вот на тесте — внутри нее идет переопределение даты. А на проде она дальше каскадно вызывает системную функцию. Проблема в том, что каждую функцию, которую нужно переопределить, придется оформить таким образом. И не дай Бог случайно воспользоваться штатными функциями работы с датой. По сути — изобретаем свой язык поверх стандартного языка ) Можно? Можно. Сложно? Да.

              0
              Можно, но SQL-запросы со стандартной функцией даты и с кастомной начнут совсем по-разному оптимизироваться. И налететь на проблемы можно очень легко
              +1

              Вы работаете только с локальным каким-то бизнесом без интернализации и сложных финансовых проводок? Дело в том, что sysdate/systimestamp — не бизнесовые даты. На самом деле, очень много различных тонкостей бывает, начиная со смены таймзоны (летнее/зимнее/отмена летнего/миграция/расширение в другой регион и тд) и заканчивая отложенных или будущих операций. Sysdate/systimestamp пойдут для обычного логгинга или другого не особенно завязанного на время функционала, но не для бизнес операций, как например, тот же опердень в банкинге. Своя кастомная функция позволяет это все строго формализовать в одном месте.

                0
                Верно, локальный бизнес в России, но в разных городах и разных часовых поясах.
                И меняли таймзоны на своих серверах и не раз, скажем спасибо нашему правительству
                Для отложенных операций в будущем мы у себя используем current_date и нам вполне этого хватает, хотя конечно соглашусь что в других случаях этого могло быть недостаточно
            0

            Очень интересный и полезный опыт!


            Но хочу уточнить:


            были проверены системы легкой виртуализации (Vserver, OpenVZ) и контейнеризации через docker. Тоже не работает, они используют то же самое ядро что и хост-система, а значит используют те же значения системного таймера. Снова отпадает.

            Да, ну? У вас ниже в статье по сути идёт подгрузка faketime, а ядро общее. Так что это в каком-то смысле противоречие. Ну, и раз докер работает через cgroups/namespaces, то прямолинейным выглядит путь как по ссылке https://lwn.net/Articles/766089/

              0
              И тут тоже спасибо за подсказку. Этого никто в нашей команде не находил во время отбора гипотез. Но оно будет работать только если экземпляры базы данных разные и запущены как разные контейнеры, а для cdb/pdb опять бы пришлось все равно делать что-то свое.
              0
              Добрый день!
              На уровне экземпляра (не знаю, как работает в PDB, думаю, затронет только конкретный контейнер) можно еще использовать:
              alter system set fixed_date = '2017-04-01 12:34:56';
              и затем после тестов
              alter system set fixed_date = 'NONE';

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое