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

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,182元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
任务调度 XXL-JOB 版免费试用,400 元额度,开发版规格
简介: 这篇我们来撸配置中心.为啥要用配置中心呢? 我用个讨巧的方式来回答这个问题:

这篇我们来撸配置中心.

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

  • 携程配置中心 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 轮询的方式, 部分配置中心提供了长连接, 可以进行适配
  • 将配置中心注册到 服务注册发现 服务中, 统一从 服务注册发现服务中获取服务信息
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
7月前
|
缓存 Java API
微服务——SpringBoot使用归纳——Spring Boot集成 Swagger2 展现在线接口文档——Swagger2 的配置
本文介绍了在Spring Boot中配置Swagger2的方法。通过创建一个配置类,添加`@Configuration`和`@EnableSwagger2`注解,使用Docket对象定义API文档的详细信息,包括标题、描述、版本和包路径等。配置完成后,访问`localhost:8080/swagger-ui.html`即可查看接口文档。文中还提示了可能因浏览器缓存导致的问题及解决方法。
765 0
微服务——SpringBoot使用归纳——Spring Boot集成 Swagger2 展现在线接口文档——Swagger2 的配置
|
7月前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
959 0
|
7月前
|
Java 测试技术 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
104 0
|
7月前
|
SQL Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— application.yml 中对日志的配置
在 Spring Boot 项目中,`application.yml` 文件用于配置日志。通过 `logging.config` 指定日志配置文件(如 `logback.xml`),实现日志详细设置。`logging.level` 可定义包的日志输出级别,例如将 `com.itcodai.course03.dao` 包设为 `trace` 级别,便于开发时查看 SQL 操作。日志级别从高到低为 ERROR、WARN、INFO、DEBUG,生产环境建议调整为较高级别以减少日志量。本课程采用 yml 格式,因其层次清晰,但需注意格式要求。
641 0
|
7月前
|
Java 数据库连接 微服务
微服务——MyBatis配置——事务管理
本段内容主要介绍了事务管理的两种类型:JDBC 和 MANAGED。JDBC 类型直接利用数据源连接管理事务,依赖提交和回滚机制;而 MANAGED 类型则由容器全程管理事务生命周期,例如 JEE 应用服务器上下文,默认会关闭连接,但可根据需要设置 `closeConnection` 属性为 false 阻止关闭行为。此外,提到在使用 Spring + MyBatis 时,无需额外配置事务管理器,因为 Spring 模块自带的功能可覆盖上述配置,且这两种事务管理器类型均无需设置属性。
102 0
|
7月前
|
Java 数据库连接 数据库
微服务——MyBatis配置——多环境配置
在 MyBatis 中,多环境配置允许为不同数据库创建多个 SqlSessionFactory。通过传递环境参数给 SqlSessionFactoryBuilder,可指定使用哪种环境;若忽略,则加载默认环境。`environments` 元素定义环境配置,包括默认环境 ID、事务管理器和数据源类型等。每个环境需唯一标识,确保默认环境匹配其中之一。代码示例展示了如何构建工厂及配置 XML 结构。
108 0
|
7月前
|
缓存 Java 数据库连接
微服务——MyBatis配置——常见配置
本文介绍了 MyBatis 的常见配置及其加载顺序。属性配置优先级为:方法参数传递的属性 &gt; resource/url 属性中配置 &gt; properties 元素中指定属性。同时列举了多个关键配置项,如 `cacheEnabled`(全局缓存开关)、`lazyLoadingEnabled`(延迟加载)、`useGeneratedKeys`(使用 JDBC 自动生成主键)等,并详细说明其作用、有效值及默认值。这些配置帮助开发者优化 MyBatis 的性能与行为。
102 0
|
8月前
|
Shell Go 开发工具
【环境】Rocky8使用gvm配置Go多版本管理的微服务开发环境(go-zero)
通过本文的介绍,我们详细讲解了如何在Rocky8上使用gvm来管理多个Go版本,并配置go-zero框架的开发环境。通过gvm的灵活管理,开发者可以轻松切换不同的Go版本,以适应不同项目的需求。同时,go-zero框架的使用进一步提升了微服务开发的效率和质量。希望本文能帮助开发者构建高效的Go语言开发环境,提高项目开发的灵活性和稳定性。
239 63
|
10月前
|
存储 网络协议 Nacos
高效搭建Nacos:实现微服务的服务注册与配置中心
Nacos(Dynamic Naming and Configuration Service)是阿里巴巴开源的一款动态服务发现、配置管理和服务管理平台。它旨在帮助开发者更轻松地构建、部署和管理分布式系统,特别是在微服务架构中。
1698 82
高效搭建Nacos:实现微服务的服务注册与配置中心
|
7月前
|
Java 数据库连接 数据库
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——MyBatis 介绍和配置
本文介绍了Spring Boot集成MyBatis的方法,重点讲解基于注解的方式。首先简述MyBatis作为持久层框架的特点,接着说明集成时的依赖导入,包括`mybatis-spring-boot-starter`和MySQL连接器。随后详细展示了`properties.yml`配置文件的内容,涵盖数据库连接、驼峰命名规范及Mapper文件路径等关键设置,帮助开发者快速上手Spring Boot与MyBatis的整合开发。
896 0