基于swoole实现配置中心

基于swoole实现配置中心

简介:

应用程序在启动和运行的时候往往需要读取一些配置信息,配置基本上伴随着应用程序的整个生命周期 ,

微服务架构中,当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移(分割),这样配置就分散了,不仅如此,分散中还包含着冗余 。

在这里插入图片描述

而配置中心将配置从各应用中剥离出来,对配置进行统一管理

在这里插入图片描述

配置中心的服务流程如下:

1、管理员在配置中心更新配置信息。

2、服务A和服务B及时得到配置更新通知,从配置中心获取配置。

总得来说,配置中心就是一种统一管理各种应用配置的基础服务组件。

在系统架构中,配置中心是整个微服务基础架构体系中的一个组件,如下图,它的功能看上去并不起眼,无非就是配置的管理和存取,但它是整个微服务架构中不可或缺的一环。

本片文章中,使用swoole实现配置中心的拉去配置文件的原理,在实际开发应用中,可以使用成熟完善的配置中心管理工具,比如 consul的 key-value,

配置中心的原理实现:

在这里插入图片描述

配置中心实现

目录结构

客户端拉取配置

├─client 				------  配置中心客户端
│ ├─Agent.php 			------  启动初始化配置,定时监听拉取最新配置文件,平滑重启服务
│ ├─ClientServer.php  	 ------  配置中心的服务业务代码
│ ├─reload.sh  			------  平滑重启服务

配置中心服务端

├─server 				------  配置中心服务端
│ ├─CheckServer.php 	------  定时检测管理员修改的配置信息,重新生成配置文件
│ ├─ConfigServer.php  	------  启动初始化生成配置,定时生成最新配置文件

数据库结构

serveripportis_edit
服务名称IP地址端口是否更新服务配置信息:0否,1是

代码实现

server服务端
CheckServer.php

功能:

定时监听数据库的配置信息是否有变更,如果数据库的数据有变更,则重新生成对应的配置文件

<?php
use Swoole\Client;
use function Swoole\Coroutine\run;

class CheckServer {

    public function index(): void
    {
        $this->listen();

        while (true) {
            try {
                sleep(8);

                $this->listen();

            } catch (Throwable $e) {
                $myfile = fopen('./log/error.log', "w");

                $content = 'Config agent error'. $e->getMessage().'  文件信息'. $e->getFile().'文件行号'.$e->getLine().PHP_EOL;

                fwrite($myfile, $content, 4096);
            }
        }
    }

    /**
     * 定时监听数据库的配置信息是否有变更,如果数据库的数据有变更,则重新生成对应的配置文件
     * 代理端通过reload.sh进行平滑重启服务从而加载最新的配置信息
     */
    public function listen()
    {
        $client = new  Client(SWOOLE_SOCK_TCP);

        if (!$client->connect('127.0.0.1', 9505, 0.5)){
            echo "connect failed. Error: {$client->errCode}\n";
        }

        $client->send('monitor');

        //接收服务端的响应
        $data = $client->recv();

        echo "$data \n";

        $client->close();
    }

}

$CheckServer = new CheckServer();

$CheckServer->index();

ConfigServer.php

功能:

初始化配置文件,推送最新的配置信息到客户端

服务监听,检测客户端的配置信息与配置中心的配置文件是否相同,不相同则重新推送配置文件到客户端

定时监听数据库,数据库若是有数据变更,则重新生成对应的配置文件

<?php
use Swoole\Server;
use Swoole\Coroutine\MySQL;

class ConfigServer
{
    protected $server;

    //是否初始化,,0初始化,1非初始化
    protected $is_init = 0;

    protected $path = './application/config/';

    public function __construct()
    {
        $this->server = new Server("0.0.0.0", 9505);
        $this->onConnect();
        $this->onReceive();
        $this->onClose();
        $this->onWorkerStart();
        $this->onStart();
        $this->start();

        $this->server->set(array(
            'enable_coroutine' => true,
            //配置文件的信息不进行持久化
            'enable_static_handler' => true,
            'document_root' => $this->path,
        ));
    }

    public function onStart()
    {
        $this->server->on('start', function ($serv) {
            echo '配置中心服务启动成功'.PHP_EOL;
        });
    }

    public function onWorkerStart()
    {
        $this->server->on('WorkerStart', function ($serv, $fd) {
            echo "ConfigServer-Worker: 启动".PHP_EOL;
        });
    }

    public function onConnect()
    {
        $this->server->on('Connect', function ($serv, $fd) {
            echo "Client: Connect".PHP_EOL;
        });
    }

    /**
     * 监听客户端的请求
     */
    public function onReceive()
    {
        try {
            $this->server->on('Receive', function ($serv, $fd, $from_id, $data) {

                //定时监听数据库
                if (is_string($data)  && $data == 'monitor'){
                    $this->monitor();

                    $serv->send($fd, '更新完成');
                }else if(is_string($data)  && $data=='init'){
                    //直接查询数据库,然后定时更新到客户端的配置文件当中

                    $sql = 'SELECT `server`,ip,`port` FROM config';

                    $arrNew = $this->init($sql);

                    $data = json_encode($arrNew);

                    $serv->send($fd, $data);
                } else {
                    $fileDataArray = $this->listen($data);

                    if($fileDataArray){
                        $serv->send($fd, json_encode($fileDataArray));
                    }else{
                        $serv->send($fd, '当前配置信息与服务端配置信息一致,无需更新');
                    }
                }

            });
        } catch (Throwable $e) {
            $this->errorLog($e->getMessage(),$e->getFile(),$e->getLine());
        }
    }

    /**
     * 初始化配置文件
     * @return array
     */
    public function init($sql){
        if (!is_dir($this->path)) {
            mkdir($this->path, '0744', true);
        }

        $data = $this->getConfigBySql($sql);

        $result = null;

        $arrNew = null;

        if($data) {
            foreach ($data as $key => $value){

                $configFile  =   sprintf($this->path.'%s.php', $value['server']);

                $content   = '<?php' . PHP_EOL . 'return ' . var_export($value, true) . ';';

                //打开文件,没有就创建,类似vim
                $myfile = fopen($configFile, "w");

                $result = fwrite($myfile, $content, 4096);

                $arrNew[] = $value;
            }

            if ($result) {
                echo '配置更新完成'.PHP_EOL;
            }
        }

        return $arrNew;
    }

    /**
     * 服务监听,检测客户端的配置信息与配置中心的配置文件是否相同,不相同则重新推送配置文件到客户端
     * @param $data
     * @return array
     */
    public function listen($data){
        //定时
        $data = json_decode($data, true);

        $type = array_shift($data);

        $fileDataArray = [];

        if ($type['type'] == 'checkConfig') {
            $changeService = null;

            foreach ($data as $key => $server){
                $serverName = $server['server'];

                $path_parts  =   sprintf($this->path.'%s.php', $serverName);

                //过滤不是文件的路径
                if(!is_file($path_parts)) continue;

                //比较散列值,判断数据是否发生变化
                $serverMd5   =   md5_file($path_parts);

                //比较客户端与服务端的散列值是否一致,如果不一致,则重新生成变更的服务配置信息
                //这里记录一致不用更新的服务信息,在获取mysql中进行 NOT IN 操作
                if ($serverMd5 == $server['md5']) {
                    $changeService .= "'$serverName',";
                }

            }

            if($changeService){
                $changeService = substr_replace($changeService,'',-1);

                $sql = "SELECT `server`,ip,`port` FROM config WHERE `server` NOT IN ({$changeService})";

                $fileDataArray = $this->init($sql);
            }

        }

        return $fileDataArray;
    }

    public function onClose()
    {
        $this->server->on('Close', function ($serv, $fd) {
            echo "Client: Close.\n";
        });
    }

    public function start()
    {
        $this->server->start();
    }

    /**
     * 定时监听数据库,数据库若是有数据变更,则重新生成对应的配置文件
     * 重新生成配置文件后,客户端的定时监听到与服务端的配置信息不一致,客户端会中心拉取配置信息
     */
    public function monitor(): void
    {
        $sql = 'SELECT `server`,ip,`port` FROM config';

        //首次监听,则加载全部,之后进行增量监听
        if($this->is_init) $sql .= " WHERE is_edit = 1";

        $result = $this->init($sql);

        /**
         * 若有配置更新,则编辑状态为已更新配置
         * 如果客户端有重新更改数据库的配置信息,则在数据库把更新状态改为要更新的状态
         */
        if($result){
            $servers =  array_column($result,'server');

            $condition = '';

            foreach ($servers as $item){
                $condition .= "'{$item}',";
            }

            $condition = substr_replace($condition,'',-1);

            $edit = "UPDATE `config` SET `is_edit` = 0  WHERE `server` IN ({$condition})";

            $this->getConfigBySql($edit);
        }

        $this->is_init = 1;
    }

    /**
     * 记录错误日志
     * @param $getMessage
     * @param $getFile
     * @param $getLine
     */
    public function errorLog($getMessage,$getFile,$getLine){
        $myfile = fopen('./log/error.log', "w");

        $content = 'Config agent error'. $getMessage .'  文件信息'. $getFile .'文件行号'. $getLine .PHP_EOL;

        fwrite($myfile, $content, 4096);
    }

    /**
     * 获取本地数据库的配置信息
     * @return mixed
     */
    public function getConfigBySql($sql){
        //获取客户端配置的服务信息
        $model = $this->getModel();

        $result = $model->query($sql);

        return $result;
    }

    /**
     * 获取mysql连接句柄
     * @return MySQL
     */
    public function getModel(){
        $config = [
            'host'     => '192.168.238.111',
            'port'     => 3306,
            'user'     => 'root',
            'password' => 'root',
            'database' => 'service',
        ];

        $swoole_mysql = new MySQL();

        $swoole_mysql->connect($config);

        $result = $swoole_mysql;

        return $result;
    }

}

$server = new ConfigServer();

client客户端
Agent.php

功能:

初始化配置信息,当启动服务时,代理端会拉取服务端的配置信息到本地

代理端定时监听与服务端的配置信息是否一致,不一致则服务端重新推送配置信息到代理端

<?php
use Swoole\Client;
use Swoole\Coroutine\MySQL;
use function Swoole\Coroutine\run;

class Agent {
    protected $path = './application/config/';

    protected $file = './application/config/%s.php';

    public function index(): void
    {
        $this->init();

        while (true) {
            try {
                sleep(5);

                $this->listen();

            } catch (Throwable $e) {

                $myfile = fopen('./log/error.log', "w");

                $content = 'Config agent error'. $e->getMessage().'  文件信息'. $e->getFile().'文件行号'.$e->getLine().PHP_EOL;

                fwrite($myfile, $content, 4096);
            }
        }
    }

    /**
     * 初始化配置信息,当启动服务时,代理端会拉取服务端的配置信息,并保存到本地配置文件与sql中
     */
    public function init()
    {
        $client = new  Client(SWOOLE_SOCK_TCP);

        if (!$client->connect('127.0.0.1', 9505, 0.5)){
            echo "connect failed. Error: {$client->errCode}\n";
        }

        echo '初始化配置信息' . PHP_EOL;

        $client->send('init');

        //接收服务端的响应
        $data = $client->recv();

        if (is_string($data) && !empty($data)) {

            if (!is_dir($this->path)) {
                mkdir($this->path, '0744', true);
            }

            $data = json_decode($data, true);

            //更新配置文件信息
            $this->updateConfigFile($data);

            //创建或编辑配置文件信息
            $this->updateOrCreateConfig($data);
        }

        $client->close();
    }

    /**
     * 代理端定时监听与服务端的配置信息是否一致,不一致则服务端重新推送配置信息到代理端
     * 代理端通过reload.sh进行平滑重启服务从而加载最新的配置信息
     */
    public function listen()
    {
        $client = new  Client(SWOOLE_SOCK_TCP);

        if (!$client->connect('127.0.0.1', 9505, 0.5)){
            echo "connect failed. Error: {$client->errCode}\n";
        }

        //获取本地数据库的配置信息
        $sql = 'SELECT `server` FROM config';

        $result = $this->executeBySql($sql);

        $sendData = [];

        foreach ($result as $key => $server){
            $configFile = sprintf($this->file, $server['server']);

            //过滤不是文件的路径
            if(!is_file($configFile)) continue;

            $md5 = md5_file($configFile);

            $sendData[] = [
                'md5'    => $md5,
                'server' => $server['server'],
            ];
        }

        array_unshift($sendData, ['type' => 'checkConfig']);

        $jsonSendData = json_encode($sendData);

        $client->send($jsonSendData);

        //接收服务端的响应
        $data = $client->recv();

        //如果服务端返回数据,则说明客户端的配置信息与服务端的配置信息不一致,需要更新客户端的配置信息
        if ($data == '当前配置信息与服务端配置信息一致,无需更新') {
            echo "$data \n";
        }else{

            $data = json_decode($data, true);

            //更新配置文件信息
            $this->updateConfigFile($data);

            //创建或编辑配置文件信息
            $this->updateOrCreateConfig($data);

            //平滑重启客户端的服务,从而重新加载配置文件信息
            shell_exec('sh ./reload.sh');

            echo "当前配置信息与服务端配置信息不一致,已重新加载配置文件 \n";
        }

        $client->close();
    }

    /**
     * 更新配置文件信息
     * @param array $data
     */
    public function updateConfigFile(array $data): void
    {
        //获取 配置中心的配置项
        foreach ($data as $key => $fileData) {
            $configFile = sprintf($this->file, $fileData['server']);

            $content   = '<?php' .PHP_EOL. 'return ' . var_export($fileData, true) . ';';

            $myfile = fopen($configFile, "w");

            fwrite($myfile, $content, 4096);
        }
    }

    /**
     * 创建或编辑配置文件信息
     */
    public function updateOrCreateConfig($datas){
        $nowTime = date('Y-m-d H:i:s');

        //获取 配置中心的配置项
        foreach ($datas as $key => $item) {
            $server = $item['server'];
            $ip     = $item['ip'];
            $port   = $item['port'];


            //获取本地数据库的配置信息
            $select = "SELECT id FROM config WHERE `server` = '$server'";

            $result = $this->executeBySql($select);

            if($result){

                $sql = "UPDATE `config` SET `ip` = '$ip', `port` = '$port', `updated_at` = '$nowTime' WHERE `server` = '$server'";

            }else{
                $value  = "('$server','$ip','$port')";

                $sql = "INSERT INTO `config` (`server`, `ip`, `port`) VALUES {$value}";
            }

            $this->executeBySql($sql);
        }
    }


    /**
     * 执行sql语句
     * @return null
     */
    public function executeBySql($sql)
    {
        $result = null;

        run(function () use(&$result,&$sql)   {
            $config = [
                'host'      => '192.168.238.111',
                'port'      => 3306,
                'user'      => 'root',
                'password'  => 'root',
                'database'  => 'client',
            ];

            $swoole_mysql = new MySQL();

            $swoole_mysql->connect($config);

            $result = $swoole_mysql->query($sql);
        });

        return $result;
    }

}

$agent = new Agent();

$agent->index();

ClientServer.php

功能:

客户端的业务代码,需要用到配置信息的地方,需要运行reload.sh进行重新加载配置文件信息

<?php
use Swoole\Server;

class ClientServer
{
    protected $server;

    protected $path = './application/config/';

    public function __construct()
    {
        $this->server = new Server("0.0.0.0", 9506);
        $this->onConnect();
        $this->onReceive();
        $this->onClose();
        $this->onWorkerStart();
        $this->onStart();
        $this->start();

        $this->server->set(array(
            'open_length_check'     => true,    // 开启协议解析
            'package_max_length'    => 81920,   //包的最大长度
            'package_length_type'   => 'N',     // 长度字段的类型
            'package_length_offset' => 0,       //从第几个字节是包长度的值
            'package_body_offset'   => 4,       //从第几个字节开始计算长度
        ));
    }

    public function onStart()
    {
        $this->server->on('Start', function ($serv) {
            //设置服务进程别名
            swoole_set_process_name('client_server');
        });
    }

    public function onWorkerStart()
    {
        $this->server->on('WorkerStart', function ($serv, $fd) {
            echo "client_server: 启动.\n";

            $configFile  =   sprintf($this->path.'%s.php', 'orderService');

            //打开文件,没有就创建,类似vim
            $myfile = file_get_contents ($configFile);

            //输出文件信息
            echo $myfile . PHP_EOL;
        });
    }

    public function onConnect()
    {
        $this->server->on('Connect', function ($serv, $fd) {
            echo "Hello: client_server.\n";
        });
    }

    public function onReceive()
    {
        $this->server->on('Receive', function ($serv, $fd, $from_id, $data) {
        });
    }

    public function onClose()
    {
        $this->server->on('Close', function ($serv, $fd) {
            echo "Bye: order.\n";
        });
    }

    public function start()
    {
        $this->server->start();
    }
}

$server = new ClientServer();

结果:

可以看到,原来的端口从9504变成了9507,而中间过程中并没有对服务进行重启

在这里插入图片描述

reload.sh

功能:

SIGUSR1: 向主进程 / 管理进程发送 SIGUSR1 信号,将平稳地 restart 所有 Worker 进程和 TaskWorker 进程,重启所有worker进程: kill -USR1 主进程PID

echo "loading..."

pid=`pidof client_server`

echo $pid

kill -USR1 $pid

echo "loading success"
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值