作者:廖祥俐
为了对云音乐API RPC服务的性能进行评估,对云音乐API RPC服务进行了测试,在此先对RPC服务化的过程进行总结,并对测试过程中遇到过的问题进行整理,本文包括,1,对RPC服务流程的介绍;2,Java单例模式的实现方式;3,Zookeeper的注册;4,序列化的改善;5,TCP四次挥手关闭的状态迁移
Api Server旨在为云音乐应用提供统一曲库数据服务,是可水平扩展的服务器集群,通过Zookeeper统一管理服务的注册,并向Client提供并更新可用服务,这样可以使得服务的注册与移除都很灵活,即如下图所示:
一个RPC请求的过程可以描述如下:
1)消费方(Client/Consumer)调用以本地调用方式调用服务(一般采用代理的方式对接口进行封装,使得调用者无需关心具体RPC处理过程)
2)代理将方法、参数等组装成能够进行网络传输的消息体(序列化);
3)Client端通过Zookeeper找到可用的服务地址,并将消息发送到服务端;
4)Server端收到消息后进行解码(反序列化),Server端根据解码结果调用本地的服务,并将结果进行返回,同样在此将结果进行封装成消息体(序列化),并将结果返回发送;
5)client 端接收到消息,并进行解码(反序列化),通过返回invoke方法的执行结果;
6)消费方(Client/Consumer)得到最终结果。
以上2~5步,对于本地调用者是透明的,如下所示(虚线框内部对调用者是透明的),Client端与Server端需要共有的就是RPC方法的接口,其中Client端只需要接口即可,而Server端完成接口的具体业务实现。
Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例在。 使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回(garbage collection)。
单例模式确保一个类只有一个实例,自行提供这个实例并向整个系统提供这个实例。 特点: 1,一个类只能有一个实例 2,自己创建这个实例 3,整个系统都要使用这个实例 在Api Service的Client端,希望通过一个实例进行连接并管理与Server端的连接,通过spring 配置(默认是单例模式),可以在项目加载的时候bean(一个bean对应某个类)自动创建(初始化,建一个实例),而后是每次调用bean的时候是注入的(不是重新new,所有整个系统都是这个实例,而且是spring自动提供的)
但由于测试的需要,不能通过spring进行注入配置,那有哪些好用的方式进行创建呢?常用的做法有 1,将构造函数设为private
,这样外部无法new
出来
public class Singleton {
private static Singleton instance = null;
private Singleton()
{ //真正初始化 }
// 第一次使用时创建
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
2,另外一种方式,使用静态代码块
public class Singleton {
private static Singleton instance;
static{
create(instance) // 创建实例
}
}
static代码块在类被调用时会被执行,并且只执行一次。
在Api Service服务中,采用Apache Common Pool
管理Client端的连接,对于测试RPC Client的生成,应该设计成单例模式,即:
public class MusicApiServiceConfig {
public static ThriftRpcService thriftRpcService;
static{
try{
ThriftApiService.Iface apiServiceIface = createApiIface();
thriftRpcService = new ThriftRpcService();
thriftRpcService.setLevel(3);
thriftRpcService.setThrifIface(apiServiceIface);
}catch(Exception ex){
throw new Error("create thrift connection error!!!!");
}
}
为什么要使用Zookeeper?服务部署上线,需要通知Client端,即告诉使用者服务的IP以及端口。服务的通知需要能实现自动告知,即机器的增添、剔除对调用方透明,调用者不需要写死服务提供方地址,通过Zookeeper即可实现服务自动注册与发现功能。 简单来讲,zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(ip+端口)去访问具体的服务提供者。
在这里,服务端注册的信息包括:1,服务端地址;2,服务端口;3,服务是否以nio进行执行;4,timeout时间。主要是将这些信息拼成一个字符串,可供Client端寻找,如下是服务端注册拼接的过程:
public static String getServer(int port, boolean nio, int timeout) {
try {
String host = getHostname();
StringBuilder serverAddr = new StringBuilder();
if (nio) {
serverAddr.append("nio_");
}
serverAddr.append(host).append(":").append(port);
if (timeout > 0) {
serverAddr.append(":").append(timeout);
}
return serverAddr.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
同样的,Client端通过Zookeeper解析服务地址
public static List<ConnectionConfig> parseFromString(String servers, int cutErrorTimes) {
...
String[] seps = server.split(":");
String serviceIP = seps[0];
int servicePort = Integer.parseInt(seps[1]);
...
return configlist;
}
在Api Service服务测试过程中,启动服务时,会开启两个线程:一个线程向Zookeeper注册(listener),说该机器能提供服务了,另一个线程则开启服务(tserver),如下代码:
public static void startServer( TServer tserver, ThriftServerListener listener) {
new ThriftServerThread(tserver, listener).start();
}
由于是同时进行,则可能会出现Zookeeper已经收到服务能用的通知,但实际上服务仍然在启动的过程中,这种偶然性错误在测试中有发现,解决办法也比较简单,让Zookeeper注册(listener)线程进行延后执行即可。
在前面的分析中可以看到,序列化与反序列化在服务中至少各进行了两次,然而那是RPC框架的序列化过程。云音乐Api Service是基于thrift RPC框架,底层的序列化与反序列化由thrift控制,但为了提高性能,若设计为Api通用的框架,考虑到参数及返回结果可能会比较大,故需要对参数及结果进行序列化与反序列化,使性能最优。
序列化与反序列化主要考虑以下几点:
1)通用性,比如是否能支持Map等复杂的数据结构;
2)性能,包括时间复杂度和空间复杂度,由于Api Service作为云音乐基础服务,如果序列化上能节约一点时间,对整个服务的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;
3)可扩展性,由于业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。
最初使用的是Jackon进行Json序列化与反序列化,见MusicJsonUtils.class
// 序列化
public static String toJson(Object obj) {
...
}
// 反序列化
public static <T> T readOBject(String json, TypeReference<T> typeref){
...
}
public static <T> T readObject(String json, Class<T> clz){
...
}
测试的时候,在高并发的情况下出现,Jackson在反解析的时候出现死锁(Client端)
java.lang.Thread.State: BLOCKED (on object monitor)
at org.codehaus.jackson.util.InternCache.intern(InternCache.java:39)
...
更具体的可以参考类似问题,问题排查出现在Jackson的Json序列化中,从调研的结果看,出现该问题的解决办法是进行如下设置:
objectMapper.configure(JsonParser.Feature.INTERN_FIELD_NAMES, false);
objectMapper.configure(JsonParser.Feature.CANONICALIZE_FIELD_NAMES, false);
原因是Jackson采用InternCache加速(1.9.x版本),上述而看源码,InternCache 并不是线程安全的:
public final class InternCache extends LinkedHashMap<String,String>{
...
}
后续发现Jackson可以采用msgpack插件,通过转化成byte数组,可以更有效的完成序列化过程,具体如下:
// 序列化
public static byte[] msg(Object obj) {
...
}
// 反序列化
public static <T> T fromMsg(byte[] msg, TypeReference<T> typeref){
...
}
public static <T> T fromMsg(byte[] msg, JavaType type){
...
}
public static <T> T fromMsg(byte[] msg, Class<T> clz){
...
}
首先附图,TCP关闭时候的四次挥手过程及状态:
Close_Wait状态: Close_Wait状态出现在被动关闭端,也就是B机器的状态变迁,如下:
收到FIN信号(B机器)——>发出ACK——>进入CLOSE_WAIT——>等待应用进程关闭——>LAST_ACK——>CLOSED
Time_Wait状态: Time_Wait出现在主动关闭端,也就是A机器的状态变迁,如下: 发出FIN信号(A机器)——>进入FIN_WAIT1——>收到ACK——>进入FIN_WAIT2——>接收FIN信号——>发出ACK——>进入TIME_WAIT(2MSL)——>CLOSED
由此可以看到,如果处于Close_Wait状态很多,则是作为被动关闭方很多Socket在等待应用进程关闭这处不能顺利进行;处于Time_Wait较多则是作为主动关闭方短连接较多,进行了很多关闭操作。
在Api Service服务框架中,采用Apache Common Pool
对链接进行管理,与链接关闭相关的操作均通过重载在destroyObject
中,由 Common Pool进行管理,如下:
@Override
public void destroyObject(String key, PooledObject<KeyedTConnection<TSocket>> socket) throws Exception {
...
socket.getObject().destroyObject();
}
在测试服务,出现过:Close_Wait状态很多,与Time_Wait很多的状况
对于Close_Wait状态过多,则是应用程序没有正常关闭Socket
,在此对关闭这一步进行改善,去掉tsocket.isOpen()
的判断
public void destroyObject() throws Exception {
...
Socket tsocket = this.getThriftSocket();
if (tsocket != null) { // 可重复关闭,不再判定是否打开了
tsocket.close();
}
...
}
对于Time_Wait过多(主动关闭方关闭短连接较多),首先先注意下,TIME_WAIT过多,会导致tsocket.Open()
出错,异常为:java.net.SocketTimeoutException: connect timed out
这个异常的产生原因可以简单归纳如下:就是client 发出 syn 包,server端在你指定的时间内没有回复ack,poll/select 返回0,参考这篇文章 所以异常产生的原因有两种
server 端为什么没有回复ack, 因为syn包的回复是内核层的,要么网络层丢包,要么就是内核层back_log的queue满了
通过排查网络层问题,结合监控观察到TIME_WAIT状态特别多(还处于未关闭状态的Socket),可以判断是back_log的queue已满。 TIME_WAIT状态特别多则是短连接关闭的次数特别多导致,即Common Pool
进行destroyObject
的次数很多,可能是Common Pool
参数设置不合理导致,通过测试发现Common Pool
的maxIdle值过小(这里设置为1)会导致此类问题。
网易云大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者廖祥俐授权发布