微服务架构和Kubernetes是现代应用程序开发和部署中的两个关键概念,微服务架构通过拆分应用程序为多个小型、自治的服务来提高可伸缩性和可维护性,服务之间通过TCP/HTTP协议进行通信。这种架构使得应用程序更加灵活、可伸缩、可维护,同时也促进了团队之间的协作和并行开发。
Kubernetes提供了容器编排和管理的能力,简化了微服务的部署、扩展和管理过程。通过微服务和Kubernetes的结合使用,可以更好地构建并运行具有弹性且可靠的分布式应用程序。
微服务架构只是一种软件架构的思想和方法,并没有规定必须使用的技术栈或工具。每家公司在实现微服务架构时可以根据自身需求和技术偏好选择合适的技术栈。
海外新业务的微服务实现基于Spring Boot、Feign和Kubernetes,服务之间的调用依赖Feign,服务负载均衡依赖Kubernetes的标签服务。
最近我司海外业务与短视频头部平台有业务上的合作,合作方对我方的服务提出了低RT和高QPS的要求,所以需要对我们的服务进行全链路的压测,找出性能瓶颈并进行优化,最终满足合作方低RT和高QPS的要求。
我们在压测过程中,发现某个核心服务的多个pod接收请求分布非常不均匀,如下图:过多的请求集中在某几个pod上,这些pod的资源使用和RT都会飙升,导致最外层服务的RT居高不下,QPS上不去。
分析问题前,先梳理一下我们系统的整体架构,如下:合作方的请求经过域名解析、SLB、Nginx Ingress来到Kubernetes集群内,由聚合服务的站点调用多个下游站点的服务来完成整个业务的处理,服务间的调用是通过HTTP协议进行通信。
信贷业务系统是一个庞大的系统,包含很多个子系统。为了方便大家理解,我简化举例一下系统之间的调用链路:某个接口服务底层实现需要调用服务A和B的接口,服务A执行的过程又会调用到服务C···,就这样,流量的压力通过服务调用层层传递下去
根据木桶理论,一个系统的性能受它最薄弱环节直接影响,所以要满足低RT和高QPS的要求必须要先解决负载不均衡的问题.
在全链路观测平台上查看各个服务接口的调用情况如下:
“1”是运维层面的请求转发,“2、3、4”是微服务之间的正常调用。“1、4”可以证明Kubernetes的标签服务负载均衡是没问题的,那么问题出在了“2、3”的服务调用上,带着疑问去查看“2、3、4”调用服务的代码,代码如下:
@Bean
public ***Client createBean_1(){
//2和3中的服务调用声明
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Options(100,TimeUnit.MILLISECONDS,1000,TimeUnit.MILLISECONDS,true))
.retryer(Retryer.NEVER_RETRY)
.target(***Client.class,"http://***server-v1");
}
@Bean
public ***Client createBean_2(){
//4中的服务调用声明
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Options(100,TimeUnit.MILLISECONDS,1000,TimeUnit.MILLISECONDS,true))
.retryer(Retryer.NEVER_RETRY)
.client(new OkHttpClient())
.target(***Client.class,"http://***server-v1");
}
方法createBean_1和createBean_2的差异点在于对Feign属性client的赋值,于是去看了看Feign的源码,如下:如果属性client未被赋值,使用默认对象feign.Client.Default,底层HTTP调用实现使用是的HttpURLConnection。
基于HttpURLConnection实现HTTP调用,每次调用会打开一个新的连接,并在请求完成后立即关闭。这种每次请求都创建和关闭连接的方式会导致以下问题:
连接建立开销:底层TCP连接的建立过程需要经历三次握手,涉及到网络交互和资源分配,频繁进行连接建立操作会增加额外的开销,影响请求的响应时间和性能。
资源浪费:连接的创建和关闭需要消耗服务器和客户端的资源,包括CPU、内存和文件句柄等。每个请求都创建和关闭连接,会导致资源的浪费和不必要的开销。
负载均衡问题:当通过负载均衡将请求分发给多个服务实例时,由于不同实例之间的连接创建和关闭可能存在差异,一些实例可能承载更多的请求。
为了解决这些问题,使用了支持连接池管理的HTTP客户端OkHttp。这些客户端提供连接池的功能来管理和复用HTTP连接,从而避免频繁的连接创建和关闭操作。
聚合服务对服务A和服务B的调用统一做出了修改并进行了对比测试,如下:
@Bean
public ***Client createBean(){
//控制组,使用默认的HttpURLConnection
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Options(100,TimeUnit.MILLISECONDS,1000,TimeUnit.MILLISECONDS,true))
.retryer(Retryer.NEVER_RETRY)
.target(***Client.class,"http://***server-v1");
}
@Bean
public ***Client createBean(){
//测试组,使用OkHttp
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Options(100,TimeUnit.MILLISECONDS,1000,TimeUnit.MILLISECONDS,true))
.retryer(Retryer.NEVER_RETRY)
.client(new OkHttpClient())
.target(***Client.class,"http://***server-v1");
}
这两份代码放到生产环境进行了测试对比,测试效果如下:在全链路观测平台上看,OkHttp的测试组请求均匀的落在各个pod上,而HttpURLConnection的控制组每个pod接收的请求明显不均匀,大部分请求落了其中2-3个pod上。通过分组测试对比,我们精准定位了到问题点,解决了负载不均衡的问题。
在fix掉这个问题后,压测任务进行的非常顺利,很快完成了既定目标。
事后在写这边文章的时候,发现了feign.okhttp.OkHttpClient中的代码需要优化:
okhttp3超时时间没有显式设置,这里connectTimeout和readTimeout都是默认值10s,而Feign显式设置的connectTimeout为100ms,readTimeout为1000ms,这个地方处理每次请求,都会build一个新的okhttp3.OkHttpClient对象,这是一份没必要的开销,这里还有优化的空间。
优化后的代码如下
@Bean
public ***Client createBean() {
long connectTimeout = 100L;
long readTimeout = 1000L;
Options options = new Options(connectTimeout,TimeUnit.MILLISECONDS,readTimeout,TimeUnit.MILLISECONDS,true);
//使用okhttpclient替换默认实现
ConnectionPool connectionPool = new ConnectionPool();
okhttp3.OkHttpClient okhttp3Client = new okhttp3.OkHttpClient()
.newBuilder()
.connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
.readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
.followRedirects(options.isFollowRedirects())
.connectionPool(connectionPool)
.build();
feign.okhttp.OkHttpClient feignOkHttpClient = new feign.okhttp.OkHttpClient(okhttp3Client);
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(options)
.client(feignOkHttpClient)
.target(***Client.class,baseUrl);
}
小龙,信也国际化后端研发专家