Performance improvements on the service registry


This is a task planned through NLNet funding.

Context

When it comes to the CAS server, performance is generally not a concern. Going with the defaults should be enough in 99% of cases.

However, in some rare edge cases, the performance of the service registry can become a problem. The service registry is the storage used to define all the applications allowed to authenticate with the CAS server via the CAS, OAuth, SAML, or OpenID Connect protocols.

With a few hundred services, performance can drastically deteriorate. Therefore, we have worked on improving the performance of the service registry for all use cases.

Benchmarks and profiling have been conducted to understand the main hotspots of time consumption. Two features are particularly resource-consuming: sorting services and matching services.

Better sorting

Despite careful code reviews, there is always room to improve the source code. The first action we took was to turn the internal Comparator used in the BaseRegisteredService component into a singleton shared across all instances:

    private static final Comparator<RegisteredService> INTERNAL_COMPARATOR = Comparator
        .comparingInt(RegisteredService::getEvaluationPriority)
        .thenComparingInt(RegisteredService::getEvaluationOrder)
        .thenComparing(service -> StringUtils.defaultString(service.getName()), String.CASE_INSENSITIVE_ORDER)
        .thenComparing(RegisteredService::getServiceId)
        .thenComparingLong(RegisteredService::getId);

as well as improving string comparison by using String.CASE_INSENSITIVE_ORDER.

Better matching

In Java pattern matching, creating regex patterns is always very time-consuming, and a global cache was already available to improve performance.

However, thorough tests have shown that things could be improved further in this area by using a specific property to store the regex pattern in BaseRegisteredService:

    @JsonIgnore
    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    @Transient
    private transient Pattern patternServiceId;

    ...
    
    /**
     * Set the service identifier and pre-compute its regex pattern.
     *
     * @param serviceId the service id
     */
    @CanIgnoreReturnValue
    public BaseRegisteredService setServiceId(final String serviceId) {
        Assert.notNull(serviceId, "Service id cannot be null");
        this.serviceId = serviceId;
        this.patternServiceId = RegexUtils.createPattern(serviceId);
        return this;
    }
    
    @Override
    public Pattern compileServiceIdPattern() {
        if (this.patternServiceId == null) {
            setServiceId(this.serviceId);
        }
        return this.patternServiceId;
    }

And to use it for matching:

    @Override
    public boolean matches(final RegisteredService registeredService, final String serviceId) {
        return registeredService.compileServiceIdPattern().matcher(serviceId).matches();
    }

With these two improvements, the benchmark (on a CAS server with 1000 services) between version 8.0.0-RC1 and the latest version 8.0.0-SNAPSHOT shows that the reference time drops from 81 seconds to 51 seconds!

Going further

Despite these improvements, the benchmarks still show that a lot of time is spent in the “sorting phase”:

The slowdown is located in the getCandidateServicesToMatch method of the DefaultServicesManager component. Indeed, despite the cache of services, the sorted clause is still applied and consumes a lot of time.

So, in the case of a cache size set to 0:

cas.service-registry.cache.initial-capacity: 0
cas.service-registry.cache.cache-size: 0

we now keep a copy of the sorted list of services, which makes processing much faster.

With this change in place and enabled (size set to 0), the benchmark time drops again to 24 seconds!

This might be a new option to enable if you have clearly identified that the service registry does not perform well in your CAS deployment.

Availability

The performance improvements described in this document will ultimately be available in CAS 8.0.0, and you should be able to benefit from them as of 8.0.0-RC2.

On behalf of the CAS project,

Jerome LELEU

Related Posts

Apereo CAS Dynamic Configuration Management

An overview of Apereo CAS' ability to handle dynamic configuration updates.

Apereo CAS Receives NLnet Grant to Advance CAS Development

We’re excited to share that the Apereo CAS project has been awarded funding from NLnet to support the development of the CAS project.

CAS OAuth/OpenID Connect Vulnerability Disclosure

Disclosure of a security issue with the Apereo CAS software acting itself as an OAuth/OpenID Connect provider.

CAS Simple Multifactor Authentication Vulnerability Disclosure

Disclosure of a security issue with the Apereo CAS software acting itself as an MFA provider.

CAS OAuth/OpenID Connect & WebAuthN Vulnerability Disclosure

Disclosure of a security issue with the Apereo CAS software acting as an OAuth/OpenID Connect provider, or as a multifactor authentication provider utilizing FIDO2/WebAuthN.

CAS OAuth/OpenID Connect Vulnerability Disclosure

Disclosure of a security issue with the Apereo CAS software acting as an OAuth/OpenID Connect provider.

CAS OAuth/OpenID Connect Vulnerability Disclosure

Disclosure of a security issue with the Apereo CAS software acting as an OAuth/OpenID Connect provider.

Apereo CAS is now on Develocity

An overview of how Apereo CAS is using Gradle and Develocity to improve its build and test execution cycle.

CAS OAuth/OpenID Connect Vulnerability Disclosure

Disclosure of a security issue with the Apereo CAS software acting as an OAuth/OpenID Connect provider.

CAS Groovy Vulnerability Disclosure

Disclosure of a security issue with the Apereo CAS software when using Groovy.