天天看点

基于containerpilot的服务注册与发现微服务服务的注册与发现参考

所谓分久必合合久必分,分治可以解决all in one的问题,但是更多的问题因为隔离而产生,为了解决这些问题又会有相应的工具产生。作为已经不算火热的微服务概念,落地解决方案也渐渐成熟和成型,为了说明containerpilot的适用场景,首先简单说明白几个基本概念。

不管是前端还是后端服务,项目开始的时候追求短平快,所有的代码会放在一个代码库中,基于同一个框架和语言开发,顶多根据文件或者文件夹做一下模块化。前端和后端服务做一下分离(或者也没有做),放在一个SLB后面,作为无状态的应用服务器也能实现基本的水平扩容。然而,随着业务的快速发展,这样的简单结构会逐渐变成障碍。微服务的实践其实应该由来已久,只是最近几年被炒的火热,而微服务的核心思想还是设计模式中经典的思想:单一责任。基于这个思想,微服务的其他便利才有了落脚点。

基于containerpilot的服务注册与发现微服务服务的注册与发现参考

既然我们的目标是把一个服务打散,也就是从单进程要放到多进程中去,就需要涉及到进程间通信。经典的操作系统理论理论里面提到的IPC方法有: 管道,有名管道(无父子关系约束),信号量(计数锁),消息队列,信号,共享内存,套接字。这些方法大多是基于单机器中进程通信的方式。我们利用微服务在部署的时候另外一个原则: 消除单点,所以肯定是需要能够跨机器进程的方式通信了,那套接字就是重要的选择,实际应用中因为开发更多在应用层实现,所以Restful和RPC的方式是常用的手段。这里就存在一个问题,服务因为彼此隔离,彼此通信的时候怎么知道彼此的存在,同时在需要调用依赖的服务时,如何能够调用到合适的机器(这里指的是机器的IP和端口号),这就是服务的注册与发现问题了。同时服务的注册和发现也存在两种模式: 客户端模式和服务端模式。

基于containerpilot的服务注册与发现微服务服务的注册与发现参考
基于containerpilot的服务注册与发现微服务服务的注册与发现参考

可以看到这里的差别主要在于是否将注册中心暴露出去,我们后面的讨论基于客户端模式。

可能你直接看上面的架构,感觉不到这个注册中心的好处,让我们再放几张图,看看我们原来是怎么做的

基于containerpilot的服务注册与发现微服务服务的注册与发现参考
基于containerpilot的服务注册与发现微服务服务的注册与发现参考
基于containerpilot的服务注册与发现微服务服务的注册与发现参考

当然实际场景有可能比这还要复杂,可能中间用的还有F5或者haproxy,这里只是个示意。刚开始简单实现就档个nginx,对upstream有个基本的healthcheck,通过反向代理就请求到后端服务就可以了。当访问量上升,单个nginx扛不住就再在前端档个LVS,通过四层协议转发IP包,第二层对nginx分组讲负载进一步分流。但是这样LVS变成了单点,而且配置变得更加复杂。最后又不得不把这个单点消除,由客户端智能判断合适的分组,请求后端的服务。这里nginx在所有情况下既担当了负载均衡的角色,又担当了服务发现的角色(如果服务分组的时候根据访问量来分,会把访问量差不多的放在一个分组,通过nginx重写路由映射到多个后端服务)。这里很明显配置都是需要人工干预的,任何一台后端实际服务的机器IP变化都需要运维手动维护,繁琐而且容易出错。这样大家就可以感受到上面注册中心的好处了把,所有的服务列表可以集中在一个地方管理。

所有服务都集中存在一个地方管理,现在的主流方案主要是这三个

因为只用过 <code>consul</code>,下面都是以它为依据展开,但是作为服务注册中心的概念上是相似的.作为注册中心基本的功能就是kv的存储,服务的配置信息还有应用的配置信息都可以放在这里,<code>consul</code> 提供了 <code>agent</code> 运行的模式,也就是 <code>client</code> 和 <code>server</code> 模式,看下图

基于containerpilot的服务注册与发现微服务服务的注册与发现参考

对应的,因为是使用查询注册中心的方式来做服务发现(其实就是客户端的服务发现 smart client),需要客户端通过调用 <code>consul</code> 的 restful API 来查询,虽然可以通过在每个服务实例上启动一个在 <code>client</code> 模式下的 <code>agent</code> 缓解集群的并发访问压力,但是还是需要客户端主动查询的,有没有更好的方式呢?

启动 <code>consul</code> 的agent或者直接连接 <code>consul</code> 的cluster,将当前服务注册进去,第一次生成依赖服务的地址 <code>services.json</code>

在 <code>containerpilot</code> 可以跟consul的agent或者cluster通信之后,获取依赖服务的列表,通过 <code>consul-template</code> 将服务的信息渲染到预定义好的模板文件中

watch在consul中依赖的服务,当检测到更新的时候给服务发送 <code>SIGHUP</code> 的信号提示服务重新读取依赖服务的最新地址 <code>services.json</code>

通过调用 <code>ping</code> 命令对服务进行健康检查,在服务不健康甚至是容器异常退出的时候会将 <code>consul</code> 中服务的状态标记为不可用(不会解注册)

一个服务跟 <code>containerpilot</code> 集成的结构是这样的

基于containerpilot的服务注册与发现微服务服务的注册与发现参考

首先明确两个概念:

被托管服务,即运行在本容器内的主要进程,对外提供某种功能。

依赖服务,即被托管服务所依赖的服务,通常运行在其它容器内。

containerpilot 基于 consul 实现了如下两个功能:

将被托管服务注册到 consul 内,并维持健康检查心跳,在容器停止时自动解注册该服务。

获取依赖服务的实例地址。

这两个功能即服务的注册和发现,可同时使用,也可只使用其中任意一个。

使用服务注册功能时,或者使用服务发现功能且需要接收依赖服务实例更新信号时,被托管服务需由 containerpilot 管理,即最终服务管理结构如下:

只使用服务发现功能且不需要接收依赖服务实例更新信号时,被托管服务可直接由 runit 管理,即最终服务管理结构如下:

使用 containerpilot 请按需设置以下环境变量:

<code>CONSUL_ADDR</code> :容器内可使用该值调用 consul API ,默认值为 <code>127.0.0.1:8500</code> 。使用默认值时,会在容器内部起一个 consul client ,并根据 <code>CONSUL_JOIN_ADDR</code> 和 <code>CONSUL_ADVERTISE</code> 加入已有集群。

<code>CONSUL_JOIN_ADDR</code> :已有 consul 集群中任意一个节点的 IP 地址,consul client 加入集群时使用。

<code>CONSUL_ADVERTISE</code> :consul client 加入集群时使用哪个 IP 通信,需用 k8s pod IP 或 container host IP。

<code>SERVICE_NAME</code> :注册到 consul 中被托管服务名称,ID 会自动设置为 <code>${SERVICE_NAME}-${CONTAINER_HOSTNAME}</code> 。可以从外部传入或内部写死。此外,如果存在环境变量 <code>CONSUL_PREFIX</code> ,其值将会被拼到服务名称和 ID 前面。默认值为 <code>main</code> 。

<code>SERVICE_PORT</code> :注册到 consul 中被托管服务端口。不传则不注册服务到 consul 。

<code>SERVICE_COMMAND</code> :启动被托管服务的命令和参数,不传递则不启动。

<code>SERVICE_INTERFACE</code> :注册到 consul 中被托管服务地址,可选网卡名如 <code>eth0</code> 或静态地址 <code>static:192.168.1.100</code> ,默认值为 <code>eth0</code> 。

<code>SERVICE_SIGNAL_CHANGED</code> :当依赖服务实例发生变化时,是否给被托管服务发送 <code>SIGHUP</code> ,传值代表发送。

被托管服务如果依赖于其他服务,需在项目中添加 <code>serviceDependencies.json</code> 文件,构建 docker 镜像时把这个文件复制到 WORKDIR 下,示例如下:

表示 <code>service1</code> 依赖于 <code>service2</code> 和 <code>service3</code> ,而 <code>service2</code> 依赖于 <code>service4</code> ,即 key 为被托管的服务名,value 为该服务的依赖列表。

对于不使用服务注册,只需要服务发现的情况,可以使用 <code>SERVICE_NAME</code> 的默认值 <code>main</code> 作为 key,示例如下:

containerpilot 会根据 <code>SERVICE_NAME</code> 选取需要监控的依赖服务,比如设置 <code>SERVICE_NAME</code> 为 <code>service1</code> ,则将监控 <code>service2</code> 和 <code>service3</code>,当依赖服务实例发生变化时,会更新 <code>/etc/containerpilot/services.json</code> ,生成的内容示例如下:

其中 key 为依赖的服务名,value 为该服务的实例地址数组。

被托管服务要获取该文件的更新可通过如下方式:

golang 、java 这种 long running 的,需要托管于 containerpilot ,然后设置环境变量 <code>SERVICES_SIGNAL_CHANGED</code> ,然后代码中处理 <code>SIGHUP</code> 信号,收到信号时重新读取该文件。

php 这种不好处理信号的,可以每次处理请求时都重新读取该文件。

containerpilot 应作为 runit service 启动,示例 <code>run</code> 脚本如下:

<a href="https://github.com/joyent/containerpilot">https://github.com/joyent/containerpilot</a>

<a href="https://www.joyent.com/blog/applications-on-autopilot">https://www.joyent.com/blog/applications-on-autopilot</a>

<a href="http://www.jianshu.com/p/c144a577f3d1">http://www.jianshu.com/p/c144a577f3d1</a>

继续阅读