Переход от 2-х звенки к архитектуре служб в парадигме SOA

в 14:36, , рубрики: yii

В данной статье я бы хотел поделиться своим опытом организации перехода от классической 2-х звенки к парадигме SOA, также затронуть некоторые аспекты деплоя в рамках enterprise-решения и интеграции со смежными службами написанными на Java

Предистория

Последние 3 года я работал в отделе внутренней автоматизации компании Новотелеком. Основное развитие систем для автоматизации внутренних процессов смежных с IT подразделений началось в 2008 году вместе с активным ростом самой кампании. В то время руководство не ставила целей делать качественные решения, основной целью было завоевание рынка, и это отложило отпечаток на принимаемые решения. Основной из систем над которыми работает отдел ВА — это внутренняя CRM система, которая включает в себя также элементы планирования человеческих ресурсов и справочные системы. Долгое время система писалась на самописном фреймворке, но после знакомства с Yii и реализации сайта компании на нем, было принято решение перевода системы на данный фреймворк. Обсуждения почему был выбран именно этот фреймворк выходят за рамки моей статьи.

При первоначальном переводе на Yii наша команда решила оставить архитектуру без кардинальных изменений т.к. темп разработки рос, а внутренние процессы организации работы были еще не на том уровне, чтобы ставить в приоритет архитектурные аспекты. Таким образом спустя год на Yii было переведено примерно треть функционала текущей системы. Определенно мы увеличили темп разработки т.к. фреймворк помог решить много типовых задач, на решение которых ранее уходило много времени. Но оставался один момент, который начал беспокоить все чаще. Нужно было все таки решить архитектурные вопросы, пока все не зашло в зону не возврата. Решающим фактором в принятии решения по переходу к службам вместо одного монолитного приложения стало выделение в кампании роли межсистемного архитектора, который начал приводить в порядок все внутренние процессы и архитектурные аспекты. На данную роль назначили архитектора одной из смежных групп, которые пишут ПО на Java. В их группе изначально была выбрана soa-парадигма и за последние 3-5 лет эта практика показала все плюсы и минусы данного подхода.

Немного об окружении

  • OS Debian 6
  • веб-сервер nginx
  • php 5.3
  • framework yii
  • система сборки и деплоя phing
  • службы устанавливаются как deb-пакеты
  • протокол интеграции со смежными службами hessian
Ограничения

Изначально был выбран подход разработки с использованием модулей, которые объединяли в себе слои бизнес логики, например, заявка клиента, карточка клиента и т.п. При переходе к службам встал вопрос как организовать структуру приложения реализовать, как деплоить, как хранить в SVN.
Для решения подобных задач есть несколько подходов. Один из них в свое время предложили ребята из 2Gis в своей статье. Также можно использовать похожее решение в организации структуры приложения — Yiinitializr. Решение от 2amigos достаточно хорошее, но просто так взять и перенести все на него было уже достаточно проблематично. Поэтому пришлось сделать свой велосипед на основе уже имеющихся примеров и полученном опыте работы с Yii

Немного о SOA

На первом этапе были выделены следующие службы
Переход от 2 х звенки к архитектуре служб в парадигме SOA

На схеме выделены 2 внутренних слоя: core и portal
Core — слой общих компонентов, необходимых для корректной работы всех служб, плюс библиотеки, например yii, behat, ratcher и т.д., а также набор компонентов для интеграции со смежными службами.
Portal — пользовательские интерфейсы.
Кроме этого на схеме представлен слой внутренних и внешних служб и протоколы взаимодействия

Подробнее о реализации

Каждое приложение, будь то служба или пользовательский интерфейс, имеет одну из двух точек входа: web и console. Например, в случае службы web точкой входа является rest api.

Хранение конфигов сделано по аналогии с yiinitializr с учетом наших особенностей. Таким образом каждое приложение имеет следующую структуру конфигов:

  • service.web.php — конфиг для веб-части
  • service.console.php — конфиг для консольной части
  • service.test.php — конфиг для запуска автотестов
  • service.base.php — общий конфиг для всех предыдущих. Подключается внутри каждого из них
  • env/local.php — конфиг в который вынесены параметры, зависящие от окружения (dev, test или production)
  • sections/routes.php — правила маршрутизации

За инициализацию приложения отвечает компонент ApplicationDispatcher в пакете common, который реализует следующие функции: подготовка конфига, прописывание внутренних алиасов, прописывание маршрутов и предоставляет методы для получения путей до директорий в зависимости от окружения, например, до папки с временными файлами или логами.

Компонент подключается в index.php и вызывается следующим образом

Исходный код index.php

// Скрипт инициализации приложения
if(!file_exists('/usr/share/ntk-rm-common/protected/components/ApplicationDispatcher.php')) {
   throw new Exception('Необходимо установить пакет ntk-rm-common');
}

require_once('/usr/share/ntk-rm-common/protected/components/ApplicationDispatcher.php');
 

// Создаем и запускаем экземпляр приложения
$dispatcher = ApplicationDispatcher::getInstance();

// Указываем тип окружения: бой или разработка
$dispatcher->setEnvironment(ApplicationDispatcher::ENV_PRODUCTION);

// Указываем тип приложения
$dispatcher->setApplicationType(ApplicationDispatcher::APP_TYPE_WEB);

// Запускаем скрипт создания приложения
$dispatcher->create('crm')->run();

Исходный код метода create

 /**
 * Создание экземпляра приложения
 * @param $service - название инициализируемой службы
 * @return mixed
 * @throws Exception
 */
public function create($service) {
    $this->service = $service;
 
    if(empty($this->app_type)) {
        throw new Exception('Укажите тип приложения: web или console');
    }
 
    // Подключаем глобальный хелпер
    require_once $this->getBasePath('common') . '/helpers/global.php';
    $config = $this->prepareConfig();
 
    // прописываем путь до папки с временными файлами
    $config['runtimePath'] = $this->getRuntimePath($service);
 
    // прописываем путь до папки с исходниками - protected
    $config['basePath'] = $this->getBasePath($service);
 
    $this->setAliases();
 
    if ($this->app_type == self::APP_TYPE_WEB) {
        $this->app = Yii::createWebApplication($config);
        // Подгружаем правила маршрутизации
        $this->setRoutes();
        // Прописываем путь до папки assets в зависимости от окружения
        $basePath = $this->getHtdocsPath($this->service) . '/assets/';
        $this->app->getAssetManager()->setBasePath($basePath);
    } else {
        defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
        $this->app = Yii::createConsoleApplication($config);
    }
 
    return $this->app;
}

Логика подготовки файла конфигурации мало чем отличается от той, что используется в Yii-Boilerplate или Yiinitializr
	/**
	 * Склеивание конфигов в один.
	 * @return array|mixed
	 * @throws Exception - ошибка в случае если не найден конфиг приложения
	 */
	private function prepareConfig() {

		if (!$this->isExistsServiceConfig()) {
			throw new Exception('Конфигурационный файл службы «' . $this->getServiceConfigName() . '» не найден. Проверьте правильность пути.');
		}

		// Подключаем конфиги службы
		$service_configs = array(
			'/' . $this->service . '.' . $this->app_type . '.php',
			'/env/local.php'
		);

		$config = $this->mergeConfigs(array(), $service_configs , $this->getConfigPath(($this->service)));

		// Подключаем общие конфиги
		$common_configs = array(
			'/env/local.php',
			'/common.base.php',
			$this->app_type == self::APP_TYPE_WEB ? '/common.web.php' : '/common.console.php',
		);

		$config = $this->mergeConfigs($config, $common_configs, $this->getConfigPath('common'));

		// Подключаем конфиги backend части
		$backend_configs = array(
			'/php-backend.base.php',
			'/env/local.php',
		);

		$config = $this->mergeConfigs($config, $backend_configs, $this->getConfigPath('php-backend'));

		return $config;
	}

Структура пакета

Как уже было сказано выше установка служб в production происходит через deb-пакеты. Для полной поддержки debian-way при установке deb-пакета приложение раскидывается по следующим директориям:

  • /usr/share/<имя-службы>/protected/ — исходники службы
  • /usr/share/doc/<имя-службы>/ — пример конфига local.default.php
  • /usr/bin/<имя-службы> — исполняемый файл для запуска роботов
  • /var/www/<имя-службы>/htdocs/ — директория для веб-севера
  • /var/tmp/<имя-службы>/ — директория для хранения временных файлов
  • /var/log/<имя-службы>/ директория для хранения логов службы

Сборка пакета с помощью утилиты Phing

За сборку пакета отвечает утилита phing, которая:

  • выкачивает из svn актуальную версию из ветки trunk
  • разносит все по нужным директориями в соответствии с тем как описано выше
  • вызывает команду для сборки пакета
  • создает метку в svn
  • заливает пакет на сервер-репозиторий debian пакетов

Пример кода из задачи по формированию структуры пакета

  <!-- ============================================  -->
    <!-- Target: prepare                               -->
    <!-- ============================================  -->
    <target name="prepare" depends="clean">
        <echo msg="Подготовка данных для создания пакета" />

                <mkdir dir="${project.packageDir}" />
                <copy todir="${project.packageDir}">
                        <fileset dir="${project.basedir}/debian">
                                <include name="**/*" />
                                <exclude name=".svn" />
                <exclude name="cron.d/" />
                <exclude name="cron.d/*" />
                </fileset>
                </copy>


                <exec command="svn info | grep 'URL: '" outputProperty="project.tmp.svnInfo" />
                <php expression="end(explode(': ', '${project.tmp.svnInfo}'));"
                     returnProperty="project.tmp.svnUrl" />

        <echo msg="Получение исходных данных из SVN" />
                <exec command="rm -Rf ${project.packageDir}/var/www/ntk-rm-crm/*" />
                <exec command="svn export --force ${project.tmp.svnUrl} ${project.packageDir}/export/"/>

        <echo msg="Создание и наполнение папки для веб-сервера /var/www/ntk-rm-crm/htdocs/" />
        <mkdir dir="${project.packageDir}/var/www/ntk-rm-crm/" />
        <copy todir="${project.packageDir}/var/www/ntk-rm-crm/htdocs/" >
            <fileset defaultexcludes="false" expandsymboliclinks="true" dir="${project.packageDir}/export/htdocs/">
                <include name="**/*" />
            </fileset>
        </copy>
        <mkdir dir="${project.packageDir}/var/www/ntk-rm-crm/htdocs/assets/" />

        <copy file="${project.packageDir}/var/www/ntk-rm-crm/htdocs/index-prod.php"
              tofile="${project.packageDir}/var/www/ntk-rm-crm/htdocs/index.php" overwrite="true" />

        <delete file="${project.packageDir}/var/www/ntk-rm-crm/htdocs/index-prod.php" />


        <echo msg="Создание и наполнение папки исходников /usr/share/ntk-rm-crm/protected/" />
        <mkdir dir="${project.packageDir}/usr/share/ntk-rm-crm/protected/" />
        <copy todir="${project.packageDir}/usr/share/ntk-rm-crm/protected/" >
            <fileset defaultexcludes="false" expandsymboliclinks="true" dir="${project.packageDir}/export/protected/">
                <include name="**/*" />
                <exclude name="configs/*" />
                <exclude name="**/yiic*" />
            </fileset>
        </copy>

        <delete dir="${project.packageDir}/usr/share/ntk-rm-crm/protected/configs/" includeemptydirs="true" />

        <echo msg="Генерация файла yiic.php для боевого окружения" />
        <copy file="${project.packageDir}/export/protected/yiic-prod.php"
              tofile="${project.packageDir}/usr/share/ntk-rm-crm/protected/yiic.php" overwrite="true" />
        <copy file="${project.packageDir}/export/protected/yiic-prod"
              tofile="${project.packageDir}/usr/bin/ntk-rm-crm" overwrite="true" />


        <echo msg="Создание и наполнение папки конфигов /etc/ntk-rm-crm/" />
        <mkdir dir="${project.packageDir}/etc/ntk-rm-crm/" />

        <copy todir="${project.packageDir}/etc/ntk-rm-crm/" >
            <fileset defaultexcludes="false" expandsymboliclinks="true" dir="${project.packageDir}/export/protected/configs/">
                <include name="**/*" />
                <exclude name="**/crm.test.php" />
                <exclude name="**/local.default.php" />
            </fileset>
        </copy>

        <echo msg="Создание примера конфига окружения в /usr/share/doc/ntk-rm-crm/" />
        <mkdir dir="${project.packageDir}/usr/share/doc/ntk-rm-crm/" />
        <copy file="${project.packageDir}/export/protected/configs/env/local.default.php"
              tofile="${project.packageDir}/usr/share/doc/ntk-rm-crm/local.default.php" overwrite="true" />

        <echo msg="Создание cron-файла /etc/cron.d/ntk-rm-crm" />
        <copy file="${project.basedir}/debian/cron.d/ntk-rm-crm"
              tofile="${project.packageDir}/etc/cron.d/ntk-rm-crm" overwrite="true" />

        <echo msg="Создание папки для логов /var/log/ntk-rm-crm/" />
        <mkdir dir="${project.packageDir}/var/log/ntk-rm-crm/" />

        <echo msg="Создание папки для временных файлов /var/tmp/ntk-rm-crm/" />
        <mkdir dir="${project.packageDir}/var/tmp/ntk-rm-crm/" />


        <echo msg="Удаление папки export" />
                <delete dir="${project.packageDir}/export/" includeemptydirs="true" />
    </target>

Заключение

В заключении хотелось бы отметить, что у данного решения есть как плюсы, так и минусы. Из основных плюсов, полученных после внедрения данной схемы можно выделить поддержку debian-way для пакетов, что облегчает жизнь ребятам из поддержки. Также стало проще параллельно разрабатывать и внедрять новый функционал.

Что можно почитать:

Автор: Unclead

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js