云音乐API RPC服务化测试总结

猪小花1号2018-09-05 09:27

作者:廖祥俐


为了对云音乐API RPC服务的性能进行评估,对云音乐API RPC服务进行了测试,在此先对RPC服务化的过程进行总结,并对测试过程中遇到过的问题进行整理,本文包括,1,对RPC服务流程的介绍;2,Java单例模式的实现方式;3,Zookeeper的注册;4,序列化的改善;5,TCP四次挥手关闭的状态迁移

1,RPC服务流程的介绍

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端完成接口的具体业务实现。

2,Java单例模式

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!!!!");
        }
    }

3,zookeeper的注册

为什么要使用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)线程进行延后执行即可。

4,序列化的改进

在前面的分析中可以看到,序列化与反序列化在服务中至少各进行了两次,然而那是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){
  ...
 }

5,TCP四次挥手的状态变迁

首先附图,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

本文来自网易实践者社区,经作者廖祥俐授权发布