.Net Core微服务系列--配置中心

什么是配置中心

简单来说配置中心就是对配置进行管理的一个中心。对于配置这个司空见惯的东西,我们想想为什么对于应用程序需要各种各样的配置来支撑?

我们人类没有办法掌控和预知一切,所以映射到软件系统这个领域,我们需要人为的预留一些线头,以便在未来呢,拨弄这些线头调整系统的飞行状态。

而这些线头就是我们程序中的配置,所以说配置是用来在程序运行状态时动态调整行为的,比如说日志级别,数据库连接字符串等等。当然这只是一方面,另一方面我们需要方便的统一的对可变的参数进行管理也是配置的用武之地。

为什么要有配置中心

以往可能用的最多的是配置文件,当需要修改配置内容时,我们会连接到服务器修改配置文件,重启或者程序支持reload来达到修改配置的目的。但是微服务时代来临了,我们面临了哪些挑战?

  1. 微服务是分布式的,如果我们有成千上万的机器,是不是需要连接到这些机器上修改。
  2. 微服务每个服务是自治的,可以进行自由的技术选型,那么配置文件的名字,配置文件存放的地方对于运维来说就是一个大难题。
  3. 成千上万的机器修改了配置是不是同时生效?哪些配置生效了?哪些配置失败了?
  4. 配置文件的回滚和容灾机制

这些挑战的提出,就催生了配置中心的出现。

配置中心需要支持的特性

当我们决定在项目中采用配置中心了,那么我们可能需要确定下配置中心哪些特性才是我们迫切需要的。

高可用性

这个毋庸置疑了,整个系统的配置都是由配置中心来支撑,我们就必须保证配置中心的高可用性。

更新配置的及时性和成功率

当配置中心有配置更改了,能够及时的通知客户端并能保证一定的成功率

弱依赖和客户端容灾

虽然上面刚说了高可用性,但是我们设计程序应该想到的第一点就是他一定会挂,所以我们需要实现配置中心挂了的情况下应用程序依然能运行,这就需要我们在应用程序能有本地的配置存储和一定的容灾机制。

对应用程序的侵入小

应用程序需要获取到配置中心的配置有三种实现方式

  1. Api 配置中心提供Api,应用程序通过Api进行配置的拉取,但是需要应用程序自己实现配置的缓存,容灾和实时性的保证。
  2. SDK 配置中心给应用程序提供SDK,由SDK来控制配置的拉取,缓存,容灾和保证实时性。
  3. Agent 由代理来进行配置的相关操作,应用程序通过进程或者Api跟agent进行通信。

当然最好的模式应该是Agent模式,所有行为都由代理来管理,对于应用程序基本没有侵入。

配置的版本管理

这个可以实现配置的回滚

配置的灰度发布

可以跟灰度发布进行结合。

客户端的配置监控

能实时监控到客户端生效的配置


Apollo配置中心介绍

这篇文章我们选用Apollo来跟.Net Core集成实现配置中心

直接贴出官网对于Apollo的介绍

Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。

Apollo支持4个维度管理Key-Value格式的配置:

  1. application (应用)
  2. environment (环境)
  3. cluster (集群)
  4. namespace (命名空间)

同时,Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo

Apollo从设计之初就立志于成为一个有治理能力的配置发布平台,目前提供了以下的特性:

  • 统一管理不同环境、不同集群的配置
    • Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。
    • 同一份代码部署在不同的集群,可以有不同的配置,比如zookeeper的地址等
    • 通过命名空间(namespace)可以很方便地支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖
  • 配置修改实时生效(热发布)
    • 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序
  • 版本发布管理
    • 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚
  • 灰度发布
    • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例
  • 权限管理、发布审核、操作审计
    • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
    • 所有的操作都有审计日志,可以方便地追踪问题
  • 客户端配置信息监控
    • 可以在界面上方便地看到配置在被哪些实例使用
  • 提供Java和.Net原生客户端
    • 提供了Java和.Net的原生客户端,方便应用集成
    • 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)
    • 同时提供了Http接口,非Java和.Net应用也可以方便地使用
  • 提供开放平台API
    • Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等
    • 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制
  • 部署简单
    • 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少
    • 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来
    • Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数

从特性上来看,满足了我们前面所需要的特性,所以直接开始进入正题。

使用Docker部署Apollo

本次使用的是单机部署,如果正式环境请参考分布式部署指南

首先到git上clone代码,可能有点慢,耐心等待下或者直接去docker-quick-start中把sql文件夹和compose文件下载下来

git clone git@github.com:ctripcorp/apollo.git

修改Compose文件,把被占用的端口修改一下,我这里把8080改为18080,8070改为18070,接着直接运行命令

docker-compose up

第一次运行需要下载镜像可能会慢一点,运行成功的截图如下:

在浏览器中访问IP:18080 账号:apollo 密码:admin 登陆apollo

连接Mysql

可以看到Apollo默认有两个数据库

贴一下官方对几个核心概念的解释

  1. application (应用)
    • 这个很好理解,就是实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置
    • 每个应用都需要有唯一的身份标识 -- appId,我们认为应用身份是跟着代码走的,所以需要在代码中配置,具体信息请参见Java客户端使用指南
  2. environment (环境)
    • 配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置
    • 我们认为环境和代码无关,同一份代码部署在不同的环境就应该能够获取到不同环境的配置
    • 所以环境默认是通过读取机器上的配置(server.properties中的env属性)指定的,不过为了开发方便,我们也支持运行时通过System Property等指定,具体信息请参见Java客户端使用指南
  3. cluster (集群)
    • 一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。
    • 对不同的cluster,同一个配置可以有不一样的值,如zookeeper地址。
    • 集群默认是通过读取机器上的配置(server.properties中的idc属性)指定的,不过也支持运行时通过System Property指定,具体信息请参见Java客户端使用指南
  4. namespace (命名空间)
    • 一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等
    • 应用可以直接读取到公共组件的配置namespace,如DAL,RPC等
    • 应用也可以通过继承公共组件的配置namespace来对公共组件的配置做调整,如DAL的初始数据库连接数

创建一个测试项目 填写应用ID(应用的唯一标识,客户端需要使用)

可以看到有一个application的默认namespace,直接添加几个测试配置,并发布

OK,Apollo的简单的配置到此结束

创建.Net Core 项目并集成Apollo

创建一个webapi

dotnet new webapi

使用Nuget安装package

Install-Package Com.Ctrip.Framework.Apollo.Configuration 

appsettings.json添加节点,MetaServer是Apollo的地址

"apollo": {
    "AppId": "testservice",
    "MetaServer": "http://{IP}:18080",
    "Env": "DEV"
  }

修改Program.cs

WebHost.CreateDefaultBuilder(args)
+                .ConfigureAppConfiguration(builder => builder
+                        .AddApollo(builder.Build().GetSection("apollo"))
+                        .AddDefault() //添加默认application Namespace
                )
                .UseStartup<Startup>();

修改ValuesController

        private readonly IConfiguration _configuration;
        public ValuesController(IConfiguration configuration)
        {
            _configuration = configuration;
        }
          [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            var appName = _configuration["AppName"];
            return new string[] { "value1", "value2", appName };
        }

OK, 集成很简单的,直接运行查看结果

果然没那么顺利。。获取不到配置的值,修改一下代码,在CreateWebHostBuilder中加入logProvider

 Com.Ctrip.Framework.Apollo.Logging.LogManager.Provider = new ConsoleLoggerProvider(Com.Ctrip.Framework.Apollo.Logging.LogLevel.Trace);

可以看到,客户端是从Docker的内部地址去获取数据,这个IP 应该是从Apollo的服务发现工具Eureka获取的IP。

查看官方文档分布式部署中有解决方案

分布式部署的时候,apollo-configserviceapollo-adminservice需要把自己的IP和端口注册到Meta Server(apollo-configservice本身)。

Apollo客户端和Portal会从Meta Server获取服务的地址(IP+端口),然后通过服务地址直接访问。

所以如果实际部署的机器有多块网卡(如docker),或者存在某些网卡的IP是Apollo客户端和Portal无法访问的(如网络安全限制),那么我们就需要在apollo-configserviceapollo-adminservice中做相关限制以避免Eureka将这些网卡的IP注册到Meta Server。

具体文档可以参考Ignore Network Interfaces章节。具体而言,就是分别编辑apollo-configservice/src/main/resources/application.ymlapollo-adminservice/src/main/resources/application.yml,然后把需要忽略的网卡加进去。

如下面这个例子就是对于apollo-configservice,把docker0和veth.*的网卡在注册到Eureka时忽略掉。

    spring:
      application:
          name: apollo-configservice
      profiles:
        active: ${apollo_profile}
      cloud:
        inetutils:
          ignoredInterfaces:
            - docker0
            - veth.*

注意,对于application.yml修改时要小心,千万不要把其它信息改错了,如spring.application.name等。

另外一种方式是直接指定要注册的IP,可以修改startup.sh,通过JVM System Property在运行时传入,如-Deureka.instance.ip-address=${指定的IP},或者也可以修改apollo-adminservice或apollo-configservice 的bootstrap.yml文件,加入以下配置

eureka:
  instance:
    ip-address: ${指定的IP}

最后一种方式是直接指定要注册的IP+PORT,可以修改startup.sh,通过JVM System Property在运行时传入,如-Deureka.instance.homePageUrl=http://${指定的IP}:${指定的Port},或者也可以修改apollo-adminservice或apollo-configservice 的bootstrap.yml文件,加入以下配置

eureka:
  instance:
    homePageUrl: http://${指定的IP}:${指定的Port}
    preferIpAddress: false

做完上述修改并重启后,可以查看Eureka页面(http://${config-service-url:port})检查注册上来的IP信息是否正确。

如果Apollo部署在公有云上,本地开发环境无法连接,但又需要做开发测试的话,客户端可以升级到0.11.0版本及以上,然后通过-Dapollo.configService=http://config-service的公网IP:端口来跳过meta service的服务发现

我这里直接选用的第二种方式也就是修改Deureka.instance.ip-address为指定的IP

首先修改Compose文件,将8090端口暴露出来,然后重新运行compose文件

进入容器

docker exec it ${容器Id} /bin/bash

进入到执行路径,并修改demo.sh,在106行 JAVA_OPTS 后面字符串中加入-Deureka.instance.ip-address=${指定的IP}

cd apollo-quick-start
vi demo.sh

退出容器并重启容器。

OK,重新运行程序,并在浏览器中访问 https://localhost:5001/api/Values,可以看到已经获取到配置值

接下来 修改配置AppName 为 DemoService并发布,刷新浏览器可以看到值已经修改成功了。

小结

今天试用了下Apollo的简单集成,没有用到很深入的东西,只是一个尝试,后续会结合其他工具一起进行深入的了解。

posted @ 2019-05-13 16:56  RstarYan  阅读(3918)  评论(0编辑  收藏  举报
$(function(){ $('#blogTitle h1').addClass('bounceInLeft animated'); $('#blogTitle h2').addClass('bounceInRight animated'); // 删除反对按钮 $('.buryit').remove(); initCommentData(); }); function initCommentData() { $('.feedbackItem').each(function() { var text = $(this).find('.feedbackListSubtitle .layer').text(); // 将楼层信息放到data里面 // $(this).find('.blog_comment_body').attr('data-louceng', text.replace(/^#/g, '')); if($(this).find('.feedbackListSubtitle .louzhu').length>0) $(this).addClass('myself'); var avatar = $(this).find('> .feedbackCon > span').html() || 'https://pic.cnblogs.com/face/sample_face.gif'; $(this).find('> .feedbackCon > .blog_comment_body').append('') }); } $(document).ajaxComplete(function(event, xhr, settings) { // 监听获取评论ajax事件 if(settings.url.indexOf('/mvc/blog/GetComments.aspx') >= 0) { initCommentData(); } });