API网关
选型:nginx、zuul
结论:
Zuul1 的设计模式和 Nigix 较像,每次 I/O 操作都是从工作线程中选择一个执行,请求线程被阻塞直到工作线程完成,但是差别是 Nginx 用 C++ 实现,Zuul 用 Java 实现,而 JVM 本身有第一次加载较慢的情况。Zuul2 的性能肯定会较 Zuul1 有较大的提升,此外,Zuul 的第一次测试性能较差,但是从第二次开始就好了很多,可能是由于 JIT(Just In Time)优化造成的吧。
zuul的配置
代码:
@SpringBootApplication
@EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
其中,@EnableZuulProxy简单理解为@EnableZuulServer的增强版,当Zuul与Eureka、Ribbon等组件配合使用时,我们使用@EnableZuulProxy。
zuul转发丢失session的问题
浏览器到zuul的session,在zuul转发给业务服务时,默认会丢弃掉,这对依赖session的功能会造成影响。解决方法:
zuul.sensitiveHeaders=
表示zuul不会过滤任何信息。
路由配置
传统配置:
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
zuul.routes.scilab-url.path=/**
zuul.routes.scilab-url.url=http://127.0.0.1:8001
面向服务的配置(需要利用Eureka的自动发现能力):
zuul.routes.scilab-mate=/**
其中,scilab-mate是我们的服务ID。这种方式还会自动做负载均衡。
路由规则的前缀问题
zuul转发时默认会将匹配的前缀去掉,比如下面的规则:
zuul:
sensitive-headers:
routes:
fmumgmt-service:
path: /prj_prefix/fmu_mgmt/**
/prj_prefix/fmu_mgmt/v1/lib转给业务微服务时变成了/v1/lib。有时我们不希望这样,可使用stripPrefix=false禁止:
zuul:
sensitive-headers:
routes:
fmumgmt-service:
path: /ares_rainbow/fmu_mgmt/**
stripPrefix: false
路由规则的先后顺序
如果要确定路由规则的先后顺序,不能使用properties文件,只能使用yaml格式,因前者会丢失顺序信息。
比如下面例子:
zuul:
sensitive-headers:
routes:
fmumgmt-service:
path: /prj_prefix/fmu_mgmt/**
stripPrefix: false
scilab-mate: /**
/prj_prefix/fmu_mgmt就在/之前被匹配。
路由规则参考
可以参考zuul的文档说明:
https://cloud.spring.io/spring-cloud-netflix/multi/multi__router_and_filter_zuul.html
代码定制路由规则
静态资源
静态资源请求也会走zuul,这从zuul的debug日志可以看出来,设置zuul的debug日志:
logging:
file: gateway.log
level:
com.netflix: DEBUG
zuul转发时url结尾自动加/
这个不是zuul的问题,而是后端web server的问题,像flask无此问题,django则有。
查看实际生效的路由
使用/actuator/routes查看生效的路由,例如:
http://127.0.0.1:5555/actuator/routes
不过,必须在application.yml里配置,否则该监控端点不会默认打开:
management:
endpoints:
web:
exposure:
include: '*'
通过查看/actuator/routes的结果,我们能看到,除了我们指定的url,zuul为我们自动生成了一些默认端点,可以使用ignored-services去掉这些默认端点:
zuul:
routes:
fmumgmt-service:
path: /prj_prefix/fmu_mgmt/**
stripPrefix: false
media-service:
path: /prj_prefix/media/**
stripPrefix: false
scilab-mate:
path: /prj_prefix/scilab_mate/**
stripPrefix: false
ignored-services: '*'
避免/actuator被zuul路由规则覆盖
如果我们在zuul路由规则里配置了/,会导致/actuator也被分发到后端服务上,这不是我们想要的,有两种解决方法:
- 设置management.server.port
- 使用ignoredPatterns
个人推荐方法1。
上传文件大小限制
这个没得说,修改spring boot配置:
spring:
application:
name: api-gateway
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
Zuul安全认证
参考:
https://blog.csdn.net/cdy1996/article/details/80960215
docker化
zuul服务的dockerfile
FROM my-docker-registry:5000/java
ADD target/zuul_gateway-0.0.1-SNAPSHOT.jar /zuul.jar
EXPOSE 5555
CMD ["java", "-jar", "/zuul.jar"]
build之:
docker build -t zuul:0.0.1 .
注意
:最后一个.不要省略,表示使用当前目录下的dockerfile。
最后拉起来:
docker run -d --network host zuul:0.0.1
因为zuul要访问宿主机器上的eureka,所以这里使用了docker的host网络。更好的做法是使用docker-compose。
与eureka的结合
我们使用docker-compose,把zuul和eureka link起来,这样可以不必使用docker的host网络。
version: "2.0"
services:
eureka1:
build: discovery
expose:
- 8761
ports:
- "8761:8761"
zuul1:
build: gateway/zuul_gateway
ports:
- "8080:5555"
links:
- eureka1
执行:
docker-compose up
注意
:这里虽然用的是build,但docker-compose并不会傻乎乎的每次去重新构造docker镜像,一旦镜像首次建好,docker-compose会复用已存在的镜像。同理,若容器已存在,docker-compose也是复用已存在的容器。
eureka的注册方式引发的问题
docker化的情况下,zuul会在单独的容器里,这时如果同一台机器上待转发的服务使用localhost注册到eureka,zuul是无法转发的。解决方法是使用ip注册,因为zuul的容器里是能ping通宿主ip的,但默认无法ping通宿主hostname。使用ip注册,对于那些在容器里的服务也是一样有效的。
Zuul首次请求报SendErrorFilter的错误
只在做了“面向服务的配置”下才会出现。
有几个因素需考虑:
- Zuul内部使用了Ribbon实现负载均衡,而Ribbon默认是惰性加载的,这样可能导致首次请求较慢,触发了Hystrix熔断。
- Hystrix超时和Ribbon超时设置
使用“传统配置”则没有该问题,因为传统配置下,不会用到Ribbon、eureka和Hystrix。
解决方法:
- 设置Ribbon饥饿加载:
# avoid first call too slow
zuul.ribbon.eager-load.enabled=true
- 加大Hystrix超时和Ribbon超时(单位:毫秒)
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds= 11000
ribbon.ConnectTimeout= 10000
ribbon.ReadTimeout: 10000
理论依据:
If Zuul is using service discovery there are two timeouts you need to be concerned with, the Hystrix timeout (since all routes are wrapped in Hystrix commands by default) and the Ribbon timeout. The Hystrix timeout needs to take into account the Ribbon read and connect timeout PLUS the total number of retries that will happen for that service. By default Spring Cloud Zuul will do its best to calculate the Hystrix timeout for you UNLESS you specify the Hystrix timeout explicitly.
The Hystrix timeout is calculated using the following formula:
(ribbon.ConnectTimeout + ribbon.ReadTimeout) * (ribbon.MaxAutoRetries + 1) *
(ribbon.MaxAutoRetriesNextServer + 1)
参考
:
https://stackoverflow.com/questions/55084722/zuulexception-senderrorfilter-at-first-call
https://www.jianshu.com/p/3124c536b92f
服务注册与发现
Eureka/consul 对比:
https://blog.csdn.net/ZYC88888/article/details/81453647
负载均衡策略
自定义eureka的负载均衡策略,可参看:
https://blog.csdn.net/jayjjb/article/details/71552861
sticky session
源码参考:
增加这个类后,还要在zuul的application.yml里配置NFLoadBalancerRuleClassName为该类类名:
scilab-mate:
ribbon:
NFLoadBalancerRuleClassName: com.lee.zuul_gateway.StickySessionRule
其中,scilab-mate是某个service-id。
eureka无法找到正确的服务地址
可能跟eureka清理服务节点不及时有关,无奈之下,只能重启eureka。
redis支持
application.yml文件:
spring:
application:
name: api-gateway
redis:
host: redis
port: 6379
代码里注入redisTemplate:
public class GatewayApplication {
@Autowired
private StringRedisTemplate redisTemplate;
注意
:这里要使用StringRedisTemplate,才能正确的获得结果,否则取得的值都是null,原因见:
关键点摘录如下:
RedisOperations uses serializers to translate Java objects into Redis data structure values. The serializer defaults to JdkSerializationRedisSerializer. The JDK serializer translates your String object into a Java-serialized representation that is not compatible with ASCII or UTF-8. Check out the docs, you might be interested in StringRedisSerializer.
意思是你传的java参数会被序列化器转成redis数据结构,默认的序列化器转出的结果与UTF8不兼容,必须使用StringRedisSerializer或StringRedisTemplate.
redis连接报Connection reset by peer
具体错误信息是:
Redis exception; nested exception is io.lettuce.core.RedisException: java.io.IOException: Connection reset by peer
lettuce是一个redis的client library,就像jedis那样。
原因:
lettuce池底层使用一个共享连接,该连接永远不会关闭,因此默认也不会校验是否可用。一旦redis-server关闭tcp通道,客户端再去执行redis命令,自然就报Connection reset by peer错误。
解决方法:
客户端每次拿到connection后做个检查,看是否是合法的连接:
@Configuration
public class RedisConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisConfig.class);
@Autowired
private RedisProperties redisProperties;
@Bean
public RedisConnectionFactory getConnectionFactory() {
LOGGER.info("redis info host:{}, port:{}, db:{}", redisProperties.getHost(), redisProperties.getPort(),
redisProperties.getDatabase());
//standalone mode
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(redisProperties.getHost());
configuration.setPort(redisProperties.getPort());
//cluster mode
//RedisClusterConfiguration configuration2 = new RedisClusterConfiguration();
LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration);
// validate before use, avoid Connection reset by peer
factory.setValidateConnection(true);
//share native connection
//factory.setShareNativeConnection(true);
return factory;
}
}
网上有文章说修改redis的服务端保活,修改redis.conf:
# A reasonable value for this option is 60 seconds.
tcp-keepalive 60
经试验,该方法并无效果。
spring session
默认的session是进程内的,这样request到了其他微服务就拿不出session了,除非有一个集中的地方统一存放,比如redis。spring session就提供了spring微服务间的session共享。但是,非spring微服务就享受不到这样的好处了,比如一个存在异构系统的集群中,可能还有flask、django服务。
这种情况下,我们可使用SSO+token方案。
资源占用优化
spring cloud每个微服务默认启动申请的内存很大,事实上完全不需要这么多,可通过JVM参数来限制。