前言
原SpringCloud基础上的微服务已稳定运行近1年,遗留了一些问题不太好处理。原SpringCloud的整理文章见基于SpringCloud微服务的服务平台搭建的一些总结
问题如下:
客户端侧负载均衡在服务实例故障下线时候,不能及时发现,导致请求到故障实例地址造成请求错误,若增加请求重试配置,对于非幂等接口处理困难。
基于SpringCloud Config的配置中心有时候会有不及时刷新svn上的配置信息的情况(需要重新config),没找到这个问题的原因。另外多环境(生产、测试环境独立的配置)、多级配置(本地、配置中心)的组合设置对于组内其他开发者而言,学习成本较大,经常出现用错配置的情况。
若要使用微服务,则必须使用SpringCloud技术栈开发,对开发人员的技术选型是一种不必要的约束,另外大量的SpringCloud应用对与机器内存资源的占用比较严重(一个应用动辄上百兆内存),拆分成大量微服务后对于机器内存的浪费尤为严重。
虽然SpringCloud有Eureka,但是由于各个服务不是在所有物理机器上都起着副本,当服务器故障需要恢复时,仍然需要留意当时机器上运行程序的情况,常常出现机器上残留的旧版本应用被其他人启动起来的情况。无法做到不关心实例的具体部署。
服务的升级较为麻烦,在缺乏有效的自动化部署程序,缺乏有效滚动升级方案,以达到不中断服务的同时逐个替换现有服务实例。
改造方式
部署Kubernetes集群
网上教程挺多的,这里就不详细写了,用的1.11。但是遇到过问题。我们集群用的3.10的kernel,在1.11的kubernetes中网络默认用的ipvs,经常导致kernel panic,后来重新部署时候把ipvs去掉,换回原来的iptables就好了。
SpringCloud应用改造
首先要去除对于eureka的maven依赖和config的依赖,并去除@EnableEurekaClient的注解即可。
其次服务调用方面由于没有Eureka了,FeignClient也需要进行一些变动,但是又要尽量简化开发和修改步骤,如果还得由开发人员记住每个服务的IP和端口那就有点退化的厉害了。我们做如下设置。
1) 应用在Kubernetes内的封装
– 关于端口:每一个微服务在kubernetes内都有一个Deployment负责Pod的部署、端口暴露(Expose出Spring应用的server.port即可)
– 对服务的封装:每个应用还需要一个Service来做服务实例的负载均衡,这样服务之间调用就只要找服务对应的Service就好。Service的ip地址由Kubernetes内部DNS负责解析。服务对外统一用80端口,这样集群内部访问时候就只要写服务名称即可。同时Service的名称和微服务的应用名称一致。
2)FeignClient的改造
原SpringCloud内只要指定serviceName就能通过Ribbon自行负载到对应的实例上,现在则需要通过Kubernetes的service实现调用,由于1)已经约定了Service名称与原服务名称一致,同时端口是80。因此只要加上url就行。
如:
// 这里的url是新增参数
@FeignClient(name="remote-service", url="remote-service")
public interface RemoteServiceApi {}
环境改造
1)将数据库连接改为Service和Endpoint
这样应用里的数据库连接jdbc就从:
jdbc:10.2.3.2:5433/dbname
改为
jdbc:pg-master/dbname
Service和Endpoint的配置方式:
apiVersion: v1
kind: Endpoints
metadata:
name: pg-endpoint-loadbalancer
subsets:
- addresses:
- ip: 10.2.xxx.xxx
ports:
- port: 4432
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: pg-endpoint-loadbalancer
spec:
ports:
- port: 5432
targetPort: 4432
protocol: TCP
由此改变的好处在于,数据库切换无需逐个更换应用配置再重启,而是仅需修改集群内Endpoint设置即可。
2)部分通用配置改为ConfigMap
将通用配置,如zipkin配置、redis配置等全局范围内应用都会用到的参数提取到ConfigMap中,在通过EnvFrom的方式挂在在应用的镜像中。此时跑在容器内的镜像就可以通过环境变量获取到这些全局的设置。
比如 bootstrap.properties:
spring.rabbitmq.username=${rabbitmq_username}
环境变量挂载方式:
env:
- name: rabbit_mq_host
valueFrom:
configMapKeyRef:
name: cloud-env
key: rabbit_mq_host
- name: rabbit_mq_address
valueFrom:
configMapKeyRef:
name: cloud-env
key: rabbit_mq_address
- name: redis_cluster
valueFrom:
configMapKeyRef:
name: cloud-env
key: redis_cluster
日志改造
原应用日志统一写入${HOME}/logs/${application-name.log}里边,再有logstash采集后发送到elastic search。现在采用容器方式运行后则不再将日志文件与物理机器挂钩,而是在一个Pod内再启动一个filebeat日志采集容器,两个容器共同挂载一个/log/目录。
# 部分yaml
template:
spec:
volumes:
- name: app-logs
emptyDir: {}
containers:
- name: xxxx(application-image)
# ......
volumeMounts:
- name: app-logs
mountPath: /log
- name: filebeat-image
# ......
volumeMounts:
- name: app-logs
mountPath: /log
结构如图所示:
日志采集结构改变为如下图所示:
应用部署方式
考虑到减少其余开发人员的新增学习成本我们将应用的打包发布流程封装成部署脚本,提供给开发人员,开发人员仅需准备jar包、lib依赖(可选)以及配置文件即可。
通过统一的模板和配置脚本将:打包Docker image、上传私服、生成yaml文件等自动完成。
为自动化部署和升级需要,yaml中默认采用Alwayl的Image拉去imagePullPolicy: Always,同时增加rollout 升级的配置。这样研发人员在部署应用时就能自动进行滚动升级,并支持回滚到旧版本。
spec:
replicas: REPLICAS
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
发布于部署的关系如下所示:
一键发布脚本会自动完成打包docker image、推送私服、生成yaml文件三件事。研发人员仅需应用生成的yaml文件即可完成部署任务。
集群服务入口
原服务: 互联网 -> CDN -> 防火墙 -> Nginx -> zuul网关 -> 微服务
改为: 互联网 -> CDN -> 防火墙 -> Nginx -> traefik Ingress -> zuul网关 -> 微服务。
从traefik之后就已经进入微服务集群了。当然这里nginx仍然保留是因为历史元英,有些端口和应用二级目录共用问题,暂时无法完全由traefik接管。
结束
综上完成全部改造流程,将应用迁移进Kubernetes,并适时切换nginx负载即可。