在过去的几个版本中(22.0.0.2到22.0.0.6),使用MicroProfile JWT安全的应用程序的Open Liberty的性能吞吐量有了明显的改善。在这篇博文中,我讨论了我们是如何通过一个简单的场景来实现这些性能改进的,其中吞吐量增加了一倍。

性能测试
为了测试Microprofile JWT,我写了一个简单的原始MicroProfile 5.0应用程序。该应用有一个RESTful端点,需要一个JWT,其中一个组为user 。如果客户端访问端点/access/test ,授权头中有一个有效的JWT,应用程序就会查看注入的令牌是否有组,并将主题返回给客户端。
@Path("/access")
public class JWTProtected {
@Inject private JsonWebToken jwt;
@GET
@RolesAllowed("user")
@Path("/test")
public Response test() {
Set<String> groups = jwt.getGroups();
if (!groups.contains("user")) {
System.out.println("Error");
}
String subject = jwt.getSubject();
return Response.ok(subject).build();
}
}
用curl调用端点的例子(JWTs很长)
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vYWNtZWFpci1tcyIsImV4cCI6MTY1NDIwMzk1NCwianRpIjoianRpIiwiaWF0IjoxNjU0MjAwMzU0LCJzdWIiOiJzdWJqZWN0IiwidXBuIjoic3ViamVjdCIsImdyb3VwcyI6WyJ1c2VyIl19.oiXaGhslxd_hGuCfBiXe3fdpfH4udcpCB-meMBw8bKYHFvYXuMmvuV6Jy98F53D5L3uwy9aeysstAfTIVIKpkMmWFdH2e9K93qRfiZnM4nR9uzMW7UGK2QClKvZGSLOUZeGSjyREGcMW9DQqG5mnRLDXTXc27IRfeEMhjxsQ90lwPMSAUZXQaZ14MBHnT-lftajdVo3B3FHlW7V4Bf5BBWgExNEMmfP880ba3tkKgl_mEB8Y6TRJXmLOleDM5cv_d-bsSCk1mzs3KyCLQZV5X-pq-XDgTL7m0DRV7o--AYEb-qC4S_asf7O5WngbOAK7T9DIeL2HFXXGQADcRR718w" http://localhost:9080/access/test
然后,我使用Apache JMeter来应用100个客户端的负载。每个客户端生成一个JWT,使用它20次来访问端点,然后生成一个新的JWT。
性能分析
那么,我们是如何将吞吐量性能提高一倍的呢?我们做了很多改变,有些大,有些小。我们在抽样调查中注意到的第一件事是花了很多时间(8.53%)在主题上做了一个toString 。下面的例子显示了我们的剖析工具的简化输出。
8.53 com/ibm/ws/webcontainer/security/WebAppSecurityCollaboratorImpl$4.run()Ljava/lang/String;
8.53 javax/security/auth/Subject.toString()Ljava/lang/String;
当我们审查代码时,我们发现只有在启用审计时才需要toString() ,这并不是正常的使用情况。
贾里德-安德森通过以下拉动请求(PR)修复了这个问题:https://github.com/OpenLiberty/open-liberty/pull/20334
这一改变使22.0.0.4的吞吐量提高了12.5%:

接下来,我们注意到我们在解析JWT的JSON时花费了很多时间(7.42%),并且多次解析同一个JSON字符串:
1.51 org/jose4j/jwt/JwtClaims.<init>(Ljava/lang/String;Lorg/jose4j/jwt/consumer/JwtContext;)
1.64 com/ibm/ws/security/mp/jwt/impl/utils/ClaimsUtils.parsePayloadAndCreateClaims(Ljava/lang/String;)
1.93 org/jose4j/jwx/Headers.setEncodedHeader(Ljava/lang/String;)
2.34 com/ibm/ws/security/common/jwk/utils/JsonUtils.claimsFromJsonObject(Ljava/lang/String;)
7.42 org/jose4j/json/JsonUtil.parseJson(Ljava/lang/String;)Ljava/util/Map;
Jared使之更有效率,并通过以下PR改变了其他一些相关领域:
我们还注意到一些地方,我们在每个请求中都编译了正则表达式,而这是不需要的。
0.05 java/lang/String.split(Ljava/lang/String;I)[Ljava/lang/String;
0.21 com/ibm/ws/security/AccessIdUtil.getUniqueId(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
0.33 java/util/regex/Pattern.matches(Ljava/lang/String;Ljava/lang/CharSequence;)Z
0.58 java/util/regex/Pattern.compile(Ljava/lang/String;)Ljava/util/regex/Pattern;
我们还发现了另一个问题,即我们使用了一个流API,而不是一个更有效的for 循环。
2.63 com/ibm/ws/security/authorization/util/RoleMethodAuthUtil.parseMethodSecurity(Ljava/lang/reflect/Method;Ljava/security/Principal;Ljava/util/function/Predicate;)
2.63 java/util/stream/ReferencePipeline.anyMatch(Ljava/util/function/Predicate;)Z
我通过以下PR修复了这些问题:
有了这些改变,Open Liberty在22.0.0.5中比22.0.0.2快了32%。

最后,最大的变化发生在我们发现我们的JWT Cache可以表现得更好。我们在每个请求中都验证了JWT的签名,即使它之前已经被处理过。
32.27 com/ibm/ws/security/jwt/internal/ConsumerUtil.getSigningKeyAndParseJwtWithValidation(Ljava/lang/String;Lcom/ibm/ws/security/jwt/config/JwtConsumerConfig;Lorg/jose4j/jwt/consumer/JwtContext;)
32.27 com/ibm/ws/security/jwt/internal/ConsumerUtil.parseJwtWithValidation(Ljava/lang/String;Lorg/jose4j/jwt/consumer/JwtContext;Lcom/ibm/ws/security/jwt/config/JwtConsumerConfig;Ljava/security/Key;)
亚当-约霍通过以下方式改善了这一点:github.com/OpenLiberty…
Jared还做了一个额外的改动来提高正则表达式的效率:https://github.com/OpenLiberty/open-liberty/pull/20922
有了这最后两个改动,现在的吞吐量比22.0.0.2版的要好97.8%!

更复杂的应用
这些结果是用一个非常简单的原始程序得出的,它并不像现实世界中的应用。在一个更正常的微服务应用中,吞吐量会有多大的改善?在AcmeAirMS中,有两个消耗JWTs的服务(预订和客户),性能提高了17.5%--仍然令人印象深刻。

总结
总之,在过去的几个版本中,我们做了许多改变,将消费MicroProfile JWTs的吞吐性能提高了近一倍。这篇博文展示了使用MicroProfile 5.0应用程序时的结果。我们在旧版本的MicroProfile中也看到了类似的改进,因为被修改的代码在其他版本中是通用的。云原生性能仍然是我们的一个关键优先事项和重点领域。