springcloud添加自定义的endpoint来实现平滑发布
在我之前的文章 springcloud如何实现服务的平滑发布 里介绍了基于pause的发布方案。
平滑发布的核心思想就是:所有服务的调用者不再调用该服务了就表示安全的将服务kill掉。
另外actuator提供了优雅停机方式的endpoint:shutdown,那我们就可以结合 pause + 等待服务感知下线 + shutdown到一个endpoint里来提供优雅的停机发布方案。
之前的方案有一个不完美的地方,那就是IP白名单的filter是要在应用的application里加
@ServletComponentScan
注解,这样对应用程序的将是不透明的(有侵入性)。
故有了下面这我认为是相对完美的方案:
先建一个common模块,其他项目引用该模块。
在src/main/resources下新建 META-INF文件夹,然后新建:spring.factories文件
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.longge.config.PublishEndpointAutoConfiguration
PublishEndpointAutoConfiguration.java
package com.longge.config; import javax.servlet.Filter; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.longge.endpoint.PublishEndpoint; import com.longge.filter.PublishFilter; import com.longge.filter.PublishProperties; @Configuration @ConditionalOnClass(Endpoint.class) @AutoConfigureAfter(EndpointAutoConfiguration.class) public class PublishEndpointAutoConfiguration { @Bean public PublishEndpoint publishEndpoint() { return new PublishEndpoint(); } @Bean @ConditionalOnProperty("publish.ip-white-list") public PublishProperties publishProperties() { return new PublishProperties(); } @Bean @ConditionalOnProperty("publish.ip-white-list") @ConditionalOnClass(Filter.class) public FilterRegistrationBean testFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new PublishFilter(publishProperties())); registration.addUrlPatterns("/publish"); registration.setName("publishFilter"); registration.setOrder(1); return registration; } }
PublishEndpoint.java
package com.longge.endpoint; import java.util.Collections; import java.util.Map; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import lombok.Getter; import lombok.Setter; @ConfigurationProperties(prefix = "endpoints.publish") public class PublishEndpoint extends AbstractEndpoint<Map<String, Object>> implements ApplicationListener<ApplicationPreparedEvent> { private static final Map<String, Object> NO_CONTEXT_MESSAGE = Collections .unmodifiableMap(Collections.<String, String>singletonMap("message", "No context to publish.")); /** * 等待多时秒 */ @Getter @Setter private Integer waitSeconds; public PublishEndpoint() { super("publish", true, false); } private ConfigurableApplicationContext context; @Override public Map<String, Object> invoke() { if (this.context == null) { return NO_CONTEXT_MESSAGE; } try { if(null == waitSeconds) { waitSeconds = 10; } Map<String, Object> shutdownMessage = Collections .unmodifiableMap(Collections.<String, Object>singletonMap("message", "Service will exit after "+waitSeconds+" seconds")); return shutdownMessage; } finally { Thread thread = new Thread(new Runnable() { @Override public void run() { try { PublishEndpoint.this.context.stop(); Thread.sleep(waitSeconds * 1000); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } PublishEndpoint.this.context.close(); } }); thread.setContextClassLoader(getClass().getClassLoader()); thread.start(); } } @Override public void onApplicationEvent(ApplicationPreparedEvent input) { if (this.context == null) { this.context = input.getApplicationContext(); } } }
PublishFilter.java
package com.longge.filter; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; import java.util.List; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; /** * shutdown和pause的管理端点的ip白名单过滤 * @author yangzhilong * */ @Slf4j public class PublishFilter implements Filter { private static final String UNKNOWN = "unknown"; /** * 本机ip */ private List<String> localIp = Arrays.asList("0:0:0:0:0:0:0:1","127.0.0.1"); private PublishProperties properties; @Override public void destroy() { } public PublishFilter() {} public PublishFilter(PublishProperties properties) { super(); this.properties = properties; } @Override public void doFilter(ServletRequest srequest, ServletResponse sresponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) srequest; String ip = this.getIpAddress(request); log.info("访问publish的机器的原始IP:{}", ip); if (!isMatchWhiteList(ip)) { sresponse.setContentType("application/json"); sresponse.setCharacterEncoding("UTF-8"); PrintWriter writer = sresponse.getWriter(); writer.write("{\"code\":401}"); writer.flush(); writer.close(); return; } filterChain.doFilter(srequest, sresponse); } @Override public void init(FilterConfig arg0) throws ServletException { log.info("shutdown filter is init....."); } /** * 匹配是否是白名单 * @param ip * @return */ private boolean isMatchWhiteList(String ip) { if(localIp.contains(ip)) { return true; } if(null == properties.getIpWhiteList() || 0 == properties.getIpWhiteList().length) { return false; } List<String> list = Arrays.asList(properties.getIpWhiteList()); return list.stream().anyMatch(item -> ip.startsWith(item)); } /** * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址, * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢? * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。 * * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100 * * 用户真实IP为: 192.168.1.110 * * @param request * @return */ private String getIpAddress(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
PublishProperties.java
package com.longge.filter; import org.springframework.boot.context.properties.ConfigurationProperties; import lombok.Getter; import lombok.Setter; /** * publish白名单配置 * @author yangzl * @data 2019年4月26日 * */ @Getter @Setter @ConfigurationProperties(prefix="publish") public class PublishProperties { /** * 白名单 */ private String[] ipWhiteList; }
properties里配置如下:
management.security.enabled = false # 开启自定义的publish端点 endpoints.publish.enabled = true # 警用密码校验 endpoints.publish.sensitive = false # 服务暂停时间 endpoints.publish.waitSeconds = 7 # 发布endpoint白名单 publish.ip-white-list=172.17.,172.16. # 2秒拉取最新的注册信息 eureka.client.registry-fetch-interval-seconds=2 # 2秒刷新ribbon中的缓存信息 ribbon.ServerListRefreshInterval=2000
然后GET访问 http://IP:端口/publish,服务将先成注册中心下线,然后等待waitSeconds秒,然后再shutdown