Spring cloud gateway dynamic routing

Keywords: Redis Spring JSON curl

Summary

Online project publishing generally has the following schemes:

  1. Downtime release
  2. Blue green deployment
  3. Rolling deployment
  4. Grayscale release

This kind of release is usually released at night or during major version upgrade. Because it needs to be stopped, now everyone is studying Devops scheme.

Blue green deployment requires two identical environments. A new version of the environment, an old version of the environment, switches and rolls back through load balancing in order to reduce the service stop time.

Rolling deployment is to start a new version, then stop an old version, then start a new version, and then stop an old version until the upgrade is completed. The default upgrade scheme based on k8s is rolling deployment.

Gray publishing is also called Canary publishing. In gray publishing, routing weights are often set according to users. For example, 90% of users keep using the old version, and 10% of users try the new version. Different versions of applications coexist and are often used together with A/B tests to test and select multiple schemes.

The above several publishing schemes are mainly the spring cloud gateway dynamic routing that we will introduce next. We can implement gray-scale publishing based on dynamic routing, load balancing and policy loading. Of course, there are many open-source frameworks that can implement gray-scale publishing, here is just research and learning.

Dynamic routing

Spring cloud gateway loads routes into memory by default. For details, see the implementation of InMemoryRouteDefinitionRepository class.

Here we implement dynamic routing based on Redis. For basic projects, see Introduction to spring cloud gateway

1. Expose the endpoint of the actor.

management:
  endpoints:
    web:
      exposure:
        include: "*"

2. redis configuration

@Configuration
public class RedisConfig {

    @Bean(name = {"redisTemplate", "stringRedisTemplate"})
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate redisTemplate = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

}

3. Persist the original memory route to redis

@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    /**
     * hash Stored key
     */
    public static final String GATEWAY_ROUTES = "gateway_dynamic_route";

    @Resource
    private StringRedisTemplate redisTemplate;

    /**
     * Get route information
     * @return
     */
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitions = new ArrayList<>();
        redisTemplate.opsForHash().values(GATEWAY_ROUTES).stream()
                .forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition.toString(), RouteDefinition.class)));
        return Flux.fromIterable(routeDefinitions);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> {
            redisTemplate.opsForHash().put(GATEWAY_ROUTES, routeDefinition.getId(), JSONObject.toJSONString(routeDefinition));
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id -> {
            if (redisTemplate.opsForHash().hasKey(GATEWAY_ROUTES, id)) {
                redisTemplate.opsForHash().delete(GATEWAY_ROUTES, id);
                return Mono.empty();
            }
            return Mono.defer(() -> Mono.error(new NotFoundException("route definition is not found, routeId:" + routeId)));
        });
    }

}

4. Rewrite dynamic routing service

@Service
public class GatewayDynamicRouteService implements ApplicationEventPublisherAware {

    @Resource
    private RedisRouteDefinitionRepository redisRouteDefinitionRepository;

    private ApplicationEventPublisher applicationEventPublisher;

    /**
     * Add routing
     * @param routeDefinition
     * @return
     */
    public int add(RouteDefinition routeDefinition) {
        redisRouteDefinitionRepository.save(Mono.just(routeDefinition)).subscribe();
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
        return 1;
    }

    /**
     * To update
     * @param routeDefinition
     * @return
     */
    public int update(RouteDefinition routeDefinition) {
        redisRouteDefinitionRepository.delete(Mono.just(routeDefinition.getId()));
        redisRouteDefinitionRepository.save(Mono.just(routeDefinition)).subscribe();
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
        return 1;
    }

    /**
     * delete
     * @param id
     * @return
     */
    public Mono<ResponseEntity<Object>> delete(String id) {
        return redisRouteDefinitionRepository.delete(Mono.just(id)).then(Mono.defer(() -> Mono.just(ResponseEntity.ok().build())))
                .onErrorResume(t -> t instanceof NotFoundException, t -> Mono.just(ResponseEntity.notFound().build()));
    }


    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
}

5. External exposure interface

@RestController
@RequestMapping("/gateway")
public class GatewayDynamicRouteController {

    @Resource
    private GatewayDynamicRouteService gatewayDynamicRouteService;

    @PostMapping("/add")
    public String create(@RequestBody RouteDefinition entity) {
        int result = gatewayDynamicRouteService.add(entity);
        return String.valueOf(result);
    }

    @PostMapping("/update")
    public String update(@RequestBody RouteDefinition entity) {
        int result = gatewayDynamicRouteService.update(entity);
        return String.valueOf(result);
    }

    @DeleteMapping("/delete/{id}")
    public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {
        return gatewayDynamicRouteService.delete(id);
    }

}

test

Before the test, delete the static route we configured, because the static route and the redis dynamic route exist at the same time.

  1. Visit http://localhost:2000/actuator/gateway/routes , you can see that there are only default routes.
[
    {
        "route_id": "CompositeDiscoveryClient_consul",
        "route_definition": {
            "id": "CompositeDiscoveryClient_consul",
            "predicates": [
                {
                    "name": "Path",
                    "args": {
                        "pattern": "/consul/**"
                    }
                }
            ],
            "filters": [
                {
                    "name": "RewritePath",
                    "args": {
                        "regexp": "/consul/(?<remaining>.*)",
                        "replacement": "/${remaining}"
                    }
                }
            ],
            "uri": "lb://consul",
            "order": 0
        },
        "order": 0
    },
    {
        "route_id": "CompositeDiscoveryClient_idc-gateway",
        "route_definition": {
            "id": "CompositeDiscoveryClient_idc-gateway",
            "predicates": [
                {
                    "name": "Path",
                    "args": {
                        "pattern": "/idc-gateway/**"
                    }
                }
            ],
            "filters": [
                {
                    "name": "RewritePath",
                    "args": {
                        "regexp": "/idc-gateway/(?<remaining>.*)",
                        "replacement": "/${remaining}"
                    }
                }
            ],
            "uri": "lb://idc-gateway",
            "order": 0
        },
        "order": 0
    },
    {
        "route_id": "CompositeDiscoveryClient_idc-provider1",
        "route_definition": {
            "id": "CompositeDiscoveryClient_idc-provider1",
            "predicates": [
                {
                    "name": "Path",
                    "args": {
                        "pattern": "/idc-provider1/**"
                    }
                }
            ],
            "filters": [
                {
                    "name": "RewritePath",
                    "args": {
                        "regexp": "/idc-provider1/(?<remaining>.*)",
                        "replacement": "/${remaining}"
                    }
                }
            ],
            "uri": "lb://idc-provider1",
            "order": 0
        },
        "order": 0
    },
    {
        "route_id": "CompositeDiscoveryClient_idc-provider2",
        "route_definition": {
            "id": "CompositeDiscoveryClient_idc-provider2",
            "predicates": [
                {
                    "name": "Path",
                    "args": {
                        "pattern": "/idc-provider2/**"
                    }
                }
            ],
            "filters": [
                {
                    "name": "RewritePath",
                    "args": {
                        "regexp": "/idc-provider2/(?<remaining>.*)",
                        "replacement": "/${remaining}"
                    }
                }
            ],
            "uri": "lb://idc-provider2",
            "order": 0
        },
        "order": 0
    }
]

Visit at this time http://192.168.124.5:2000/idc-provider1/provider1/1 According to the results, it can be inferred that it can route to provider1 correctly, and the test results are consistent.

  1. Create the provider1 route, set the path to / p1 / * *, and test whether it works.

POST request http://localhost:2000/gateway/add

{
   "id":"provider1",
   "predicates":[
      {
         "name":"Path",
         "args":{
            "_genkey_0":"/p1/**"
         }
      },
      {
         "name":"RemoteAddr",
         "args":{
            "_genkey_0":"192.168.124.5/16"
         }
      }
   ],
   "filters":[
      {
         "name":"StripPrefix",
         "args":{
            "_genkey_0":"1"
         }
      }
   ],
   "uri":"lb://idc-provider1",
   "order":0
}

View redis storage, or request http://localhost:2000/actuator/gateway/routes , you can see that the configuration is successful.

Visit

curl http://localhost:2000/p1/provider1/1

Results the output was 2001, which was consistent with the expectation.

This shows that dynamic routing has taken effect.

epilogue

This concludes the article. Interested partners can then load the configuration file and grayscale based on the weight. Welcome to the official account [when I met you].

Posted by Maq on Sat, 04 Apr 2020 23:15:39 -0700