k8s部署eureka集群
背景
对于一般的后端微服务来说,在k8s中同时起多个相同的服务来做负载均衡,只需要简单的修改deployment的replicas,增加pod数量,然后通过对外暴露一个service来代理这些pod。
而对于eureka来说,要实现eureka的高可用,那就不是修改replicas这么方便了。由于部署的多个eureka之间需要将自己注册到彼此,因此要做一些特殊改动。
主要是用到了StatefulSet和headless service这两个k8s对象
StatefulSet、Headless Service简介:
StatefulSet
StatefulSet是为了解决有状态服务的问题(对应Deployments和ReplicaSets是为无状态服务而设计),其应用场景包括
稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现
稳定的网络标志,即Pod重新调度后其PodName和HostName不变,基于Headless Service(即没有Cluster IP的Service)来实现
有序部署,有序扩展,即Pod是有顺序的,在部署或者扩展的时候要依据定义的顺序依次依次进行(即从0到N-1,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态),基于init containers来实现
有序收缩,有序删除(即从N-1到0)
StatefulSet中每个Pod的DNS格式为
statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local
serviceName为Headless Service的名字
0..N-1为Pod所在的序号,从0开始到N-1
statefulSetName为StatefulSet的名字
namespace为服务所在的namespace,Headless Service和StatefulSet必须在相同的namespace
cluster.local为Cluster Domain
Headless Service
Headless Service 和普通service的一个显著的区别是,Headless Service的对应的每一个Endpoints,即每一个Pod,都会有对应的DNS域名
例如:我们可以用过这种域名来访问某个具体的pod:
statefulSetName-0.serviceName.namespace.svc.cluster.local
在实际使用中,将service的clusterIP设置成None,就表明这个service是一个Headless Service。
StatefulSet和Headless Service的结合
通过 StatefulSet,我们得到了一些列pod,每个pod的name为statefulSetName-{0..N-1},
加入我们创建了一个名称叫eureka的StatefulSet,并且设置replicas =3,那么部署到k8s后,k8s会为我们生成三个名称依次为eureka-0,eureka-1,eureka-2的pod。
通过Headless Service,我们可以通过pod名称来访问某个pod,
例如,我们在namespace=test的命名空间下创建了一个名称为register-server的service,并且关联了之前StatefulSet创建的pod,那么我们可以在集群内任意地方
通过eureka-0.register-server.test.svc.cluster.local这个域名访问到eureka-0这个pod。
搭建:
有了前面的基础,现在部署eureka集群的方式就逐渐清晰了。
首先明确部署eureka的关键点:需要让每个eureka注册到另外的eureka上。
也就是eureka.client.serviceUrl.defaultZone这个配置,是一组eureka的地址。
通过StatefulSet,我们可以明确知道生成的每个eureka的名称,
通过Headless Service,我们又可以访问到每个eureka,所以eureka.client.serviceUrl.defaultZone的值就是
"http://eureka-0.register-server:8000/eureka/,http://eureka-1.register-server:8000/eureka/,http://eureka-2.register-server:8000/eureka/"
由于这三个pod在同一个命名空间内,可以省略.namespace.svc.cluster.local
有个这个配置,那么我们部署StatefulSet,和Headless Service
那么我们能基本能得到一个可用的eureka集群
除了会有以下问题:
红框中的可用副本(available-replicas)会出现在不可用unavailable-replicas中
253A933C7535743E87D5D3F58E3A88CC.png
原因是我们默认是通过ip的方式来注册eureka(eureka.instance.prefer-ip-address配置默认为true),但是eureka的注册地址又是域名的形式,两者不一致。
要解决这个问题,还需做一些额外的配置。
1.在application.yaml中,将eureka.instance.prefer-ip-address设置成false。
eureka:
instance:
prefer-ip-address: false
2.StatefulSet.yaml中,增加环境变量配置,将pod的名称绑定到环境变量
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
3.在application.yaml中指定eureka的 hostname,其中MY_POD_NAME取到了第二部中绑定的当前pod名称
eureka:
instance:
hostname: ${MY_POD_NAME}.register-server
如上配置后,便可以得到一个eureka集群。
后面是一些配置文件:
整体文件配置:
096DBDAA639ACBB9F55B40545C56928A.png
service.yaml
{{- if .Values.service.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.service.name }}
labels:
{{ include "service.labels.standard" . | indent 4 }}
spec:
clusterIP: None
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{ include "service.labels.standard" . | indent 4 }}
{{- end }}
StatefulSet.yaml
apiVersion: apps/v1beta2
kind: StatefulSet
metadata:
name: register-server
labels:
{{ include "service.labels.standard" . | indent 4 }}
{{ include "service.logging.deployment.label" . | indent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
serviceName: {{ .Values.service.name }}
selector:
matchLabels:
{{ include "service.labels.standard" . | indent 6 }}
template:
metadata:
labels:
{{ include "service.labels.standard" . | indent 8 }}
{{ include "service.microservice.labels" . | indent 8 }}
annotations:
{{ include "service.monitoring.pod.annotations" . | indent 8 }}
spec:
containers:
- name: {{ .Release.Name }}
image: "{{ .Values.image.repository }}:{{ .Chart.Version }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
{{- range $name, $value := .Values.env.open }}
{{- if not (empty $value) }}
- name: {{ $name | quote }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
readinessProbe:
httpGet:
path: /actuator/health
port: {{ .Values.deployment.managementPort }}
scheme: HTTP
failureThreshold: 3
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
resources:
{{ toYaml .Values.resources | indent 12 }}
volumeMounts:
- mountPath: /Charts
name: data
{{- if not (empty .Values.persistence.subPath) }}
subPath: {{ .Values.persistence.subPath }}
{{- end }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default ( .Release.Name ) }}
{{- else }}
emptyDir: {}
{{- end }}
podManagementPolicy: "Parallel"
Chart.yaml
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for Kubernetes
name: register-server
version: 0.1.0
values.yaml
# Default values for hzero-register.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 3
image:
repository: registry.choerodon.com.cn/hzero-hzero/hzero-register
pullPolicy: Always
deployment:
managementPort: 8001
env:
open:
# Eureka 注册中心地址
EUREKA_DEFAULT_ZONE: "http://register-server-0.register-server:8000/eureka/,http://register-server-1.register-server:8000/eureka/,http://register-server-2.register-server:8000/eureka/"
# 推荐 JVM 配置
JAVA_OPTS: >
-Xms1024m
-Xmx1536m
metrics:
path: /actuator/prometheus
group: spring-boot
logs:
parser: spring-boot
persistence:
enabled: false
## A manually managed Persistent Volume and Claim
## Requires persistence.enabled: true
## If defined, PVC must be created manually before volume will be bound
# existingClaim:
# subPath:
service:
enabled: true
type: ClusterIP
port: 8000
name: register-server
ingress:
enabled: false
resources:
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources,such as Minikube. If you do want to specify resources,uncomment the following
# lines,adjust them as necessary,and remove the curly braces after 'resources:'.
limits:
# cpu: 100m
memory: 1.7Gi
requests:
# cpu: 100m
memory: 1.2Gi
application.yml
eureka:
instance:
leaseRenewalIntervalInSeconds: 10
leaseExpirationDurationInSeconds: 30
metadata-map:
VERSION: 1.0.0
hostname: ${MY_POD_NAME}.register-server #设置eureka hostname
prefer-ip-address: false #不使用ip注册,因为eureka相互注册的工程中,使用的服务名,例如register-server-0.register-server,如果使用ip注册,会导致eureka认为其他副本不可用,即eureka服务都会出现在unavailable-replicas中,而不是available-replicas中
client:
# 检索服务选项,注册中心不需要检索服务
fetch-registry: ${EUREKA_CLIENT_FETCH_REGISTRY:true}
# 注册中心将自己作为客户端来尝试注册自己,注册中心集群环境下需开启此配置
register-with-eureka: ${EUREKA_CLIENT_REGISTER_WITH_EUREKA:true}
serviceUrl:
defaultZone: ${EUREKA_DEFAULT_ZONE:http://u2:9000/eureka,http://u1:8000/eureka} #这里在部署的时候会使用环境变量替换 EUREKA_DEFAULT_ZONE值
registryFetchIntervalSeconds: 10
disable-delta: true
server:
evictionIntervalTimerInMs: 4000
enable-self-preservation: ${EUREKA_SERVER_ENABLE_SELF_PRESERVATION:false}
bootstrap.yml
spring:
application:
name: register-server
server:
port: 8000
management:
server:
port: 8001
服务注册:
将一般的微服务注册到eureka集群中,可以通过eureka的service来访问eureka,即:将eureka.client.serviceUrl.defaultZone设置成register-server.test.svc.cluster.local,使用了k8s的service负载均衡,将服务注册到任意一个活着的eureka上,然后eureka集群内部会做同步,最终注册到eureka集群内部所有eureka上