微服务的开发模式下,联调和服务注册一旦涉及多个环境(开发/SIT/UAT),就会变得有些复杂。本文总结一下我们在此问题上尝试过的几个workaround,以及最终推荐的方案。
1. 背景
以下描述的案例中,将我们所拥有的服务精简为三个:
- um:用户微服务
- ent:企业微服务
- bi:BI微服务
ent会调用um;bi会调用ent和um。
网络环境分成办公网段和开发环境网段。办公网段可以访问开发环境网段,但开发环境网段无法访问办公网段。
三个微服务都被打包成镜像,以单副本Pod的形式部署在K8S云的开发环境节点上。
服务注册使用Nacos,网关路由使用的是Zuul。
2. 单环境内部请求流程
如果只考虑SIT环境,整个服务注册+请求的处理流程可以简单描述如下:
- um-sit服务(um的sit环境,下同)启动,将自己的service ip注册到Nacos服务端
- ent-sit服务启动,将自己的service ip注册到Nacos服务端
- 前端web对http://域名/api/ent-sit 的某个接口发起请求
- 通过K8S Ingress的域名映射,找到了Zuul应用
- Zuul向Nacos查询ent-sit的地址,得到ip:172.0.0.2。这个是ent-sit的service内部ip
- Zuul将请求转给ent-sit的service,Pod里的ent-sit容器中的应用接收到请求,开始处理
- ent-sit容器在处理过程中需要解析token,于是向Zuul请求um-sit
- Zuul向Nacos查询um-sit的地址,得到ip:172.0.0.1。这个是um-sit的service内部ip
- Zuul将请求转给um-sit。um处理完token,返回用户信息
- ent-sit处理结束,将结果返回给Zuul
- Zuul将结果转给前端web,流程结束
3. 遇到的问题
联调和测试过程中我们遇到了两个主要问题:
- 联调会串服务
- 无法和测试环境的服务联通
3.1 串服务
假设Alice和Bob都在开发ent,服务名都是ent-dev。于是Nacos记录了两个服务注册信息。
Cathy想和Alice联调。但如果Cathy配置调用的服务id也是ent-dev,请求就有一定几率会飘到Bob那里。那么很可能会发生Cathy的请求返回的结果不稳定,时对时错。
一种解决方案就是每个人在本地将自己的spring.application.name改为“服务名-姓名”,例如:ent-alice。
但这个方案也存在问题:很容易在提交代码的时候误提交了自己的个人配置。Git里这个配置文件修改频繁,一看log就是服务名从Alice改为Bob,然后又被改会Alice。
如果只是这个问题,还有办法可以搞定,例如将服务名放到环境变量等。下一个问题是真正具有阻碍性的。
无法和测试环境的服务联通
在办公网段开发过程中,会发现无法调通SIT的微服务。
究其原因,这个是由于办公网段无法访问到service内部ip导致的。
办公网段ent-dev尝试调用um-sit服务的流程如下:
- um-sit服务启动,将自己的service ip(172.0.0.1)注册到Nacos服务端
- 本地的ent-dev启动,将自己的ip(10.0.0.1)注册到Nacos服务端
- ent-dev通过name(um-sit)发起请求,向Nacos服务端查询um-sit的地址
- Nacos服务端返回172.0.0.1
- ent-dev尝试请求172.0.0.1,但由于这个地址是K8S的内部地址,外部无法访问,所以请求失败
一个解决方案就是每个人自己本地也起一个um的服务,假设服务名为um-alice。然后将请求的服务名也改为同样的服务名。
这样当发起请求时,Nacos会返回本机的地址,自然请求就可以成功了。
但这个解决方案除了和上个解决方案有同样的问题(容易误提交自己的个人配置)之外,也会导致每个人开发过程中都需要启动一堆依赖的微服务。姑且不说开发机的性能压力,也容易因为没有及时更新依赖服务的代码,导致联调出错。
4. 解决方案
4.1 解决方案一:在容器之外再部署一套微服务
既然service的内部ip地址无法被办公网段访问,那么另外以非容器方式在ECS上另外部署一套dev环境,就可以解决网络访问的问题。
但这个解决方案不完美:
- 没有解决个人配置的问题
- 在基于容器的持续集成方案之外,多维护了一套持续集成方案
- 需要多部署一套环境,消耗硬件资源
4.2 【推荐】解决方案二:不使用name方式访问,使用域名/ip方式
需要同时避免串服务和个人配置这两个看起来互相冲突的问题,看起来只有放弃通过服务name方式调用,改为url调用。
通过Feign可以简化调用的代码。只要在@FeignClient
的参数里配置了url,就会优先使用url。范例如下:
1 |
|
在application-dev.yml
配置文件中,feign-client.json-place-holder.url
可以默认填写为sit测试环境的地址。这样如果只是作为基础服务来调用(例如用户服务),就不需要在本地启动了。同样以办公网段ent-dev调用um-sit服务的流程作为范例:
- um-sit服务启动,将自己的service ip(172.0.0.1)注册到Nacos服务端
- 本地开发机的ent-dev启动
- ent-dev通过域名http://域名/api/um-sit,对um的某个接口发起请求
- Zuul收到请求,向Nacos查询um-sit的地址,得到ip:172.0.0.1。这个是um-sit的service内部ip
- Zuul将请求转给um-sit的service,Pod里的um-sit容器中的应用接收到请求,开始处理
- um-sit容器中的应用处理请求完毕,返回结果给Zuul
- Zuul将结果转给本地开发机
和K8S的Service不同,Ingress是可以被容器外访问到的,所以网络连通性上也没有任何问题。
如果是需要本机服务联调或与其他开发进行联调,只需要将url改为localhost或其他开发的ip即可。这样就等同于不涉及服务注册的直连。
提交的时候把这个临时改动revert回来,就不会将个人配置提交到代码仓库了。
5. Feign配置中的name和url
在最新版的Spring Cloud OpenFeign中,@FeignClient
的name
属性是必需的。参见Spring Cloud OpenFeign:
1 | Previously, using the url attribute, did not require the name attribute. Using name is now required. |
上文提到了如果同时配置了name和url,会优先使用url,而不是通过name访问服务。原理我们可以通过源代码来说明。这段是FeignClientFactoryBean的源代码片段:
1 | /** |
从代码可以看到,只有没有设置url的情况下,才会通过loadBalance方法生成Ribbon的动态代理。
更多关于Spring Cloud OpenFeign的源代码分析,可以参见本文最后的参考资料。
6. 一些想法
实际调用过程中会发现第一次通过域名调用会较慢(2-3秒),但第二次就很快了。这是由于Zuul会通过SpringMVC对请求进行缓存。
但其实Zuul的路由功能Ingress本身已经实现得很好了。多引入一个Zuul会增加运维架构的复杂度,也会带来潜在的性能瓶颈。不过这个目前不在我们的控制范围。。。Zuul除了路由之外也可以做一些通用的token校验等,也并不是完全冗余,只是我们目前没有这么使用。