php| php 微服务之旅: 配置中心

date: 2019-05-27 23:05:54
title: php| php 微服务之旅: 配置中心

这篇我们来撸配置中心.

为啥要用配置中心呢? 我用个讨巧的方式来回答这个问题:

  • 携程配置中心 apollo
  • 360配置中心 QConf
  • aliyun 应用配置管理 acm

这是调研并在框架层适配过的 3 个配置中心, 在他们的文档里, 你都可以找到使用配置中心的理由, 有些理由(或者说场景), 也许正好直击你的痛点.

也许仅仅只是为了 更改配置不用发版, 也应该尝试一下配置中心试试看.

使用配置中心, 远远没有我们想的那么复杂, 在做选型的时候, 往往会被这个工具或者那个工具新增的一些 定义 绕进去. 那么, 我们从自己使用的框架开始, 先看我们使用配置的需求, 然后再来看配置中心 怎样满足我们的需求.

PHPer 项目中使用的配置

以 PHPer 为例, 项目中通常有 2 类配置:

  • env 环境相关的配置

以下面的 db 配置为例, 不同环境需要使用不同的 db 配置, 从 .env 配置文件中获取

return [
    'db' => [
        'driver' => 'mysql',
        'host' => env('DB_HOST'),
        'database' => 'test',
        'username' => env('DB_USER'),
        'password' => env('DB_PWD'),
    ],
];
  • config 项目中所有配置
$container = ApplicationContext::getContainer();
/** @var ConfigInterface $config */
$config = $container->get(ConfigInterface::class);
$config->get('a.b.c', 'default');

这里有记住 2 个原则(约定大于配置):

  • env 只在配置文件中使用, 所有配置使用的地方, 都使用 config
  • 所有的配置都可以通过 $config 对象来获取, 所有配置都由 $config 来管理

明白了这点以后, 无论什么配置中心, 都只需要增加一个 package 来适配, 最终将配置更新到 $config 中即可

以适配 aliyun 应用配置管理(acm) 为例

我们新增一个 hyperf/config-aliyun-acm 的 package, 用来适配 aliyun 应用配置管理(acm). 这是个免费的服务, 适合用来体验配置中心, 少了一部配置中心服务运维管理的步骤.

总共只有 2 步:

  • 从配置中心获取最新配置
// vendor/hyperf/config-aliyun-acm/src/Client.php
<?php

declare(strict_types=1);

namespace Hyperf\ConfigAliyunAcm;

use Closure;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Guzzle\ClientFactory as GuzzleClientFactory;
use Psr\Container\ContainerInterface;
use RuntimeException;

class Client implements ClientInterface
{
    /**
     * @var array
     */
    public $fetchConfig;

    /**
     * @var Closure
     */
    private $client;

    /**
     * @var ConfigInterface
     */
    private $config;

    /**
     * @var array
     */
    private $servers;

    public function __construct(ContainerInterface $container)
    {
        $this->client = $container->get(GuzzleClientFactory::class)->create();
        $this->config = $container->get(ConfigInterface::class);
    }

    public function pull(): array
    {
        $client = $this->client;
        if (! $client instanceof \GuzzleHttp\Client) {
            throw new RuntimeException('aliyun acm: Invalid http client.');
        }

        // ACM config
        $endpoint = $this->config->get('aliyun_acm.endpoint', 'acm.aliyun.com');
        $namespace = $this->config->get('aliyun_acm.namespace', '');
        $dataId = $this->config->get('aliyun_acm.data_id', '');
        $group = $this->config->get('aliyun_acm.group', 'DEFAULT_GROUP');
        $accessKey = $this->config->get('aliyun_acm.access_key', '');
        $secretKey = $this->config->get('aliyun_acm.secret_key', '');

        // Sign
        $timestamp = round(microtime(true) * 1000);
        $sign = base64_encode(hash_hmac('sha1', "{$namespace}+{$group}+{$timestamp}", $secretKey, true));

        if (! $this->servers) {
            // server list
            $response = $client->get("http://{$endpoint}:8080/diamond-server/diamond");
            if ($response->getStatusCode() !== 200) {
                throw new RuntimeException('Get server list failed from Aliyun ACM.');
            }
            $this->servers = array_filter(explode("\n", $response->getBody()->getContents()));
        }
        $server = $this->servers[array_rand($this->servers)];

        // Get config
        $response = $client->get("http://{$server}:8080/diamond-server/config.co", [
            'headers' => [
                'Spas-AccessKey' => $accessKey,
                'timeStamp' => $timestamp,
                'Spas-Signature' => $sign,
            ],
            'query' => [
                'tenant' => $namespace,
                'dataId' => $dataId,
                'group' => $group,
            ],
        ]);
        if ($response->getStatusCode() !== 200) {
            throw new RuntimeException('Get config failed from Aliyun ACM.');
        }
        return json_decode($response->getBody()->getContents(), true);
    }
}
  • 更新到 $config 对象中
// vendor/hyperf/config-aliyun-acm/src/Process/ConfigFetcherProcess.php
<?php

declare(strict_types=1);

namespace Hyperf\ConfigAliyunAcm\Process;

use Hyperf\ConfigAliyunAcm\ClientInterface;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\Annotation\Process;
use Psr\Container\ContainerInterface;
use Swoole\Server;

/**
 * @Process(name="aliyun-acm-config-fetcher")
 */
class ConfigFetcherProcess extends AbstractProcess
{
    /**
     * @var Server
     */
    private $server;

    /**
     * @var ClientInterface
     */
    private $client;

    /**
     * @var ConfigInterface
     */
    private $config;

    /**
     * @var string
     */
    private $cacheConfig;

    public function __construct(ContainerInterface $container)
    {
        parent::__construct($container);
        $this->client = $container->get(ClientInterface::class);
        $this->config = $container->get(ConfigInterface::class);
    }

    public function bind(Server $server): void
    {
        $this->server = $server;
        parent::bind($server);
    }

    public function isEnable(): bool
    {
        return $this->config->get('aliyun_acm.enable', false);
    }

    public function handle(): void
    {
        while (true) {
            $config = $this->client->pull();
            if ($config !== $this->cacheConfig) {
                if ($this->cacheConfig !== null) {
                    $diff = array_diff($this->cacheConfig ?? [], $config);
                } else {
                    $diff = $config;
                }
                $this->cacheConfig = $config;
                $workerCount = $this->server->setting['worker_num'] + $this->server->setting['task_worker_num'] - 1;
                // 通过进程间通信, 投递配置信息到每一个启动的进程
                for ($workerId = 0; $workerId <= $workerCount; ++$workerId) {
                    $this->server->sendMessage($diff, $workerId);
                }
            }
            sleep($this->config->get('aliyun_acm.interval', 5));
        }
    }
}

以 apollo 为例

代码适配其实和上面类似, 也是通过调用 apollo 提供的 api 去获取最新的配置, 然后进行更新, 不同的是, 你得部署一套 apollo 配置中心的环境. 以本地开发测试为例:

version: '3.1'
services:
    apollo: # https://github.com/ctripcorp/apollo/tree/master/scripts/docker-quick-start
        image: nobodyiam/apollo-quick-start
        ports:
            - "8080:8080"
            - "8070:8070"
        links:
            - mysql:apollo-db
        tty: true
    mysql:
        image: mysql:5.7.26
        restart: always
        volumes:
            - ./config/my.cnf:/etc/mysql/conf.d/my.cnf
            - ./config/sql:/docker-entrypoint-initdb.d
            - ./data/mysql:/var/lib/mysql
        ports:
            - "3306:3306"
        environment:
            TZ: Asia/Shanghai
            MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'

这只是本地测试, 所以只启动了一个配置中心, 如果是线上环境, 要做集群化处理, 配置中心这样重要的服务, 要确保高可用.

写在最后

QConf 的理念和部署又是另一个样子了, 使用了 zookeeper 来确保服务的可用.

但是无论是使用 apollo QConf 还是 acm, 在框架层其实只需要使用 composer require 增加相应的适配包即可, 项目中的代码完全不用修改.

配置中心可以改进的部分:

  • 目前使用 http 轮询的方式, 部分配置中心提供了长连接, 可以进行适配
  • 将配置中心注册到 服务注册发现 服务中, 统一从 服务注册发现服务中获取服务信息
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容