Hike News
Hike News

在k8s中使用eureka的几种姿势

前言

在Kubernetes中,service通过label关联pod,使用service的clusterIp即可访问到对应pod。

如果觉得clusterIp不好记,可以用coredns将service name解析成service的clusterIp,通过访问service name来访问对应的pod。

通过coredns+service,可以在单个kubernetes集群中无需其他依赖即可实现服务发现。

但由于某些其他原因我们需要使用额外的注册中心eureka,本文列举了几种注册方式和注意事项。

使用Pod IP注册

因为Pod的IP是跟随生命周期的,重新发布以后IP就变更了。

但是eureka的缓存机制相当长,重新发布后服务消费者很长一段时间会访问之前的Pod IP。

要解决这个问题有以下几个优化点:

优雅停止Pod

如果Pod被强杀掉,就不能立即从Eureka Server下线,直到Eureka Server的EvictionTask 来定时清理过期的客户端,默认60秒清理一次,清理掉之前还能被其他客户端发现并调用。

kubernetes提供了生命周期回调函数,会在容器被终止前调用preStop,执行停止Java进程的命令

1
2
3
4
lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "ps -ef | grep jav[a] | awk 'NR==1{print $2}' | xargs kill"]

因为遵循一个容器只运行一个进程的原则,所以可以直接查找Java的pid然后kill,也可以用pidof命令查找Java的pid。

缩短服务发现时间

Eureka客户端默认30秒的缓存时间,ribbon默认30秒的缓存时间,加上Eureka Server的三级缓存机制readOnlyCache也有30秒的缓存时间,在开启Eureka Server的自我保护模式时,即使客户端从Eureka Server下线了,极端情况下也有可能在90秒以后才能在其他客户端的缓存中清除。

可以通过设置deployment中的环境变量修改默认的缓存时间:

eureka client缓存(单位:秒)

1
eureka.client.registryFetchIntervalSeconds = 10

ribbon缓存(单位:秒)

1
ribbon.ServerListRefreshInterval = 10

关闭Eureka Server的readOnlyCache(可选)

因为readWriteCacheMap用的LoadingCache有读写锁,使用readOnlyCacheMap可以增加吞吐量,中小集群可以关闭readOnlyCacheMap

关闭readWriteCacheMap

1
eureka.server.use-read-only-response-cache = false

也可以缩短readOnlyCache的刷新时间:

1
eureka.server.response-cache-update-interval-ms = 10

强制下线

即使优雅停止Pod、缩短客户端和Eureka Server的缓存时间,但是Eureka客户端还是有几率请求到已下线的Pod。

Eureka提供了强制下线的接口,可以先从Eureka Server强制下线,等待缓存时间过期再停止Pod。

client强制下线接口:

1
POST /pause
1
POST /service-registry

比如:

1
2
curl -X POST "http://localhost:$MANAGEMENT_PORT/pause"
curl -X POST "http://localhost:$MANAGEMENT_PORT/service-registry/instance-status" -H "Content-Type: text/plain; charset=utf-8" -d "OUT_OF_SERVICE"

Eureka Server强制下线接口:

1
PUT /eureka/apps/${appId}/${ip:port}/status?value=OUT_OF_SERVICE

比如:

1
curl -X PUT http://eureka:8080/eureka/apps/EUREKA-1/192.168.0.10:8080/status?value=OUT_OF_SERVICE

关于强制下线可以看一下参考里的文章。

具体配置

实践配置为Eureka客户端缓存10秒,ribbon缓存10秒,不关闭readOnlyCache(缓存30秒),加起来缓存时间50秒。

将停止脚本放在指定目录,并在preStop调用。

脚本逻辑:1.强制下线当前实例;2.暂停总缓存时间;3.停止服务。

配置环境变量:

1
2
3
4
5
6
7
8
9
10
- name: eureka.client.registryFetchIntervalSeconds # Eureka客户端缓存时间
value: "10"
- name: ribbon.ServerListRefreshInterval #ribbon缓存时间
value: "10"
- name: MANAGEMENT_PORT #management.port的端口
value: "${management.port}"
- name: SLEEP_SECOND #总缓存时间
value: "50"
- name: management.endpoints.web.exposure.include #启用并暴露service-registry
value: service-registry

preStop

1
2
3
4
lifecycle:
preStop:
exec:
command: ["/bin/sh", "/app/script/stop.sh"]

stop.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 如果没有管理端口直接下线
if [ -z $MANAGEMENT_PORT ];then
echo "MANAGEMENT_PORT is empty, kill java"
ps -ef | grep jav[a] | awk 'NR==1{print $2}' | xargs kill
exit 1
fi

# 默认暂停时间
if [ -z $SLEEP_SECOND ];then
echo "SLEEP_SECOND is empty, set 50"
SLEEP_SECOND=50
fi

# 从EUREKA下线
echo "curl pause $MANAGEMENT_PORT"
# curl -X POST "http://localhost:$MANAGEMENT_PORT/pause"
curl -X "POST" "http://localhost:$MANAGEMENT_PORT/service-registry/instance-status" -H "Content-Type: text/plain; charset=utf-8" -d "OUT_OF_SERVICE"

# 等待SLEEP_SECOND秒,SLEEP_SECOND = 客户端缓存时间(eureka+ribbon)+ readOnlyCache时间
echo ""
echo "sleep $SLEEP_SECOND s"
sleep $SLEEP_SECOND

# 停止服务
echo "kill java"
ps -ef | grep jav[a] | awk 'NR==1{print $2}' | xargs kill

使用Service的Cluster IP注册

Service的Cluster IP是一个虚拟IP,会经kube-proxy把流量负载均衡到后端endpoint。

而且Service的Cluster IP是固定的,除非主动删除后重建,不然可以一直固定。

使用Cluster IP等于使用了Kubernetes的服务发现,当Pod不可用时就会从service的endpoint列表中移除,流量就不会再负载均衡到该Pod。

配置方式(properties):

1
2
eureka.instance.ipAddress = ${Cluster IP}
eureka.instance.nonSecurePort = ${port} #service的port

使用Service注册虽然可以自动过滤下线的服务,但是ribbon内部使用keepalive进行会话保持,因为是客户端负载均衡所以A服务的A1实例所有请求都会经过service转发到同一个B服务的B1实例,从而出现负载不均衡的情况

使用Service的NodePort注册

默认情况下Pod的IP只能在本集群内的Node上访问,非本集群的服务器访问不了。

可以通过写路由表或者BGP来发布Pod路由。

NodePort是Pod对外提供访问的一种方式,使用Host IP+NodePort可以简单实现跨集群及虚拟机之间的服务互相访问。

配置方式(环境变量):

1
2
3
4
5
6
- name: eureka.instance.ipAddress
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: eureka.instance.nonSecurePort
value: "${NodePort}"

NodePort方式本质也是Service,所以也存在负载均衡的情况。

另外管理NodePort也是件麻烦事。

参考

http://www.itmuch.com/spring-cloud-sum/how-to-unregister-service-in-eureka/