基于swoole实现配置中心
简介:
应用程序在启动和运行的时候往往需要读取一些配置信息,配置基本上伴随着应用程序的整个生命周期 ,
微服务架构中,当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移(分割),这样配置就分散了,不仅如此,分散中还包含着冗余 。
而配置中心将配置从各应用中剥离出来,对配置进行统一管理
配置中心的服务流程如下:
1、管理员在配置中心更新配置信息。
2、服务A和服务B及时得到配置更新通知,从配置中心获取配置。
总得来说,配置中心就是一种统一管理各种应用配置的基础服务组件。
在系统架构中,配置中心是整个微服务基础架构体系中的一个组件,如下图,它的功能看上去并不起眼,无非就是配置的管理和存取,但它是整个微服务架构中不可或缺的一环。
本片文章中,使用swoole实现配置中心的拉去配置文件的原理,在实际开发应用中,可以使用成熟完善的配置中心管理工具,比如 consul的 key-value,
配置中心的原理实现:
配置中心实现
目录结构
客户端拉取配置
├─client ------ 配置中心客户端
│ ├─Agent.php ------ 启动初始化配置,定时监听拉取最新配置文件,平滑重启服务
│ ├─ClientServer.php ------ 配置中心的服务业务代码
│ ├─reload.sh ------ 平滑重启服务
配置中心服务端
├─server ------ 配置中心服务端
│ ├─CheckServer.php ------ 定时检测管理员修改的配置信息,重新生成配置文件
│ ├─ConfigServer.php ------ 启动初始化生成配置,定时生成最新配置文件
数据库结构
server | ip | port | is_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"