crazyjohn / crazyjohn.github.io

crazyjohn's blog
9 stars 3 forks source link

Master analysis(Mater架构分析) #9

Open crazyjohn opened 9 years ago

crazyjohn commented 9 years ago

这几天找时间对Master的结构进行了简单的分析。结构没有传闻中的那么糟糕,而且整体来说我觉得它设计的都还不错,考虑到了很多东西,比如RPC调用以及相关容错设计,负载均衡的设计,方便scale out横向扩展等等。所以我觉得它的最早设计初衷是不错的,但是估计后期没有按照设计的蓝图去实现,也没有很合理的实现scale up纵向扩展,不能合理的利用单机的性能,不正确的追求分布式,导致它的组服务器性能不一定能拼过相同配置物理机的单机性能。下面慢慢展开说。

1. 整体部署分析

先看部署图

部署结构上大概分为4部分。

  1. WebSystem。前端入口,对外协议使用http。对内使用基于mina的rpc长连接服务。这里是所有的业务入口。
  2. LoginServer。登陆前端入口,对外协议使用http。对内使用基于mina的rpc长连接服务。登陆相关的业务入口。
  3. GameSystem。真实的业务实现部分,内部服务器通过rpc与它进行通信。
  4. DataSystem。静态数据的服务,需要模板服务的内部服务器都可以通过rpc的方式来这里获取。

再给一个详细些的组件图:

  1. web和login内部都聚合了WebServer组件来提供对外的http服务。
  2. game和data内部都聚合了RpcServer组件用来提供对外的rpc服务。

    2. 整体架构分析

Web部分的结构图:

  1. Controller。控制器层是离外部最近的一层,http请求会被分发到指定的Controller进行处理。通过反射找到指定的Controler的指定的方法进行执行。
  2. Action。请求分发到Controler以后,具体的业务执行会被委托给对应的Action,所以基本Action才是真正做事情的逻辑块。但是其实Web端的Action层只是RPC角色里的stub,原理是使用java的动态代理,传递给指定的Action接口作为参数,然后生成指定的Stub对象,Controller层调用的实际上是这个Stub对象,内部通过RPCClient给远端的RPCServer发送RPCRequest请求,RPCRequest会经过网络序列化传递到RPCServer端,然后再被反序列化以后对Server端的真是Action实现进行调用。
  3. DAO。DAO层封装了对数据的访问,RPCServer端的真实Action实现在进行具体的业务操作的时候会对相应的业务DAO进行调用。在Game和Data部分都有相应的DAO实现。
  4. Job。调度任务。
  5. Task。调度任务。

Container核心结构:

  1. RPC体系。分为RPCCient,RPCServer,RPCRequest,RPCResponse。其中RPC体系的网络层使用mina。RPCClient的请求调用分别实现了同步的方式和异步的方式。
  2. RPC同步调用。先说同步的方式:当调用消息发送到远端以后,调用会阻塞等待响应的到来,通过wait的方式。当ReceivedThread接受线程收到响应以后,回去唤醒上文说到的等待线程,然后改线程继续执行后续逻辑。等待会有超时,超时会有超时处理。
  3. RPC异步调用。异步调用对外的接口封装都是void。Master的RPC体系也支持异步RPC,不过实现很简单,只是在调用完成的时候没有把对应的Response添加到响应队列里。具体代码中用到异步调用的部分有如图: 都是一些调度任务。
  4. RPC请求处理。RPCServer在接受到RPCRequest请求以后,会把请求委托给自己的一个名字为RPCRequestDispatcher的组件进行分发处理,看码:

    public void dispatch(RPCRequestSession req) {
       totalRequestCount.incrementAndGet();
       req.setDispatchTime(System.currentTimeMillis());
       InvokeWorker gw = new InvokeWorker(
               invokeManager, 
               req,
               responseQueue,
               apiKeyStore);
       try {
           threadPool.execute(gw);
       } catch (RejectedExecutionException e) {
           rejectedRequestCount.incrementAndGet();
           logger.warn("reject request:" + req);
       }
    }

    如码这个请求会被包装成一个InvokerWorker丢到线程池中处理。而且这个线程池尼玛默认开了巨多的线程。后面我们再说这个问题。

  5. Server。这个抽象用来表示一个服务器,具体的实现有GameServer以及RPCServer还有WebServer。其中RPCServer使用mina实现,上文说到过。WebServer使用jetty实现。
  6. LoadBalance。Master结构实现了自己的负载均衡。具体实现也很简单使用RoundRobinLoadBalance,其实就是类似顺序轮询的方式来取出InvokerNode(代表一个业务节点)来进行请求调用。

Game结构:

Game这里我写的很简单,因为具体的东西基本用的都是核心库Container里头的组件。

Data结构:

Data这里也同理。

3. 核心业务流程时序

整体来说,这里解释的是一次Equip装备升级的时候发生的调用时序。两个时序图加起来基本解释了一次游戏典型调用以后发生的调用时序。

Web work flow:

这里是Web端接受到的一次调用,从Servlet开始,到RPCClient调用,到返回响应。

Rpc work flow:

这里说明了RCPClient进行调用的时候具体发生的消息流调用。

4. 现有架构的问题

这个结构如果设计好的话,其实是可以用来支撑大世界这样的结构的,比如COC和Boombeach这样的游戏。但是现在根据@吉兴的反馈,同时在线支撑不了800人?那么问题再哪里呢?

尼玛其实具体问题在哪里,我不敢武断的说,因为我们有去很仔细的看业务代码,也没有针对性的跑压测,所以具体性能点没有定位。但是我从整体的角度看了下结构的实现,来提出一些问题。

  1. DataSystem是否有必要存在?或者是否有必要以现在这种方式实现?这个服务是用来为游戏提供静态数据服务的,也就是策划数据映射的模板服务。这类服务的实现特点是:把策划的配置文件(格式可以是excel或者xml或者json或者sql)映射成为具体的模板对象TemplateObject,然后供游戏业务读取,注意是读取,这里一般是没有写请求的。

那我们看看Master的实现方式,把DataSystem作为一个服务进程,然后内部使用mysql表作为数据源。不说这样的结构有什么好处,我先说说这样的结构有什么问题,如果模板数据需要跨进程通过网络去访问,那么这中间就多了1层IO,进了这层IO以后还要从mysql中加载数据,这里就又多了一层IO,所以起码就多了两层IO的访问。

如果使用模板服务部署到业务服务本地,然后开服加载缓存到模板缓存,那么业务获取模板数据的时间就是常量级别的。

所以对比下来Master的实现方式要受到更重限制,但是后一种方式天空才是它的极限。

  1. GameSystem业务处理。GameSystem在启动的时候内部使用了线程池,然后开了大量的线程,当有RPC请求来的时候,会包成一个Runnable丢给线程池处理。那么真的是线程开的越多越好吗?我搞了个测试,来看下码:

    /**
    * Mutiple thread test;
    * 
    * @author crazyjohn
    *
    */
    public class MutiThreadTest {
       /** The request count */
       static int requestCount = 10000;
       /** default item id */
       protected static final int DEFAULT_ITEM_ID = 8888;
       /** default bag size */
       protected static final int DEFAULT_BAG_SIZE = 10000;
       /** the player's item bag */
       static List<Integer> bag = new ArrayList<Integer>();
    
       /**
        * Generate the bag;
        */
       private static void genarateBag() {
           for (int i = 0; i < requestCount; i++) {
               bag.add(i);
           }
       }
    
       /**
        * Handle the messages;
        * 
        * @param threadCount
        * @throws InterruptedException
        */
       private static void handleMessages(int threadCount) throws InterruptedException {
           ExecutorService executor = Executors.newFixedThreadPool(threadCount);(threadCount);
           long startTime = System.currentTimeMillis();
           final CountDownLatch latch = new CountDownLatch(requestCount);
           for (int i = 0; i < requestCount; i++) {
               executor.execute(new Runnable() {
                   @Override
                   public void run() {
                       boolean result = hasSuchItem(DEFAULT_ITEM_ID);
                       if (result) {
                           latch.countDown();
                       }
                   }
    
               });
           }
           // wait
           latch.await();
           long costTime = System.currentTimeMillis() - startTime;
           System.out.println(String.format("Use thread count: %d, cost times: %dms", threadCount, costTime));
           // shutdown
           executor.shutdown();
       }
    
       /**
        * Is the player has such item?
        * 
        * @param itemId
        * @return
        */
       protected static boolean hasSuchItem(int itemId) {
           for (int i = 0; i < requestCount; i++) {
               if (bag.get(i) == itemId) {
                   return true;
               }
    
           }
           return false;
       }
    
       public static void main(String[] args) throws InterruptedException {
           // generate bag
           genarateBag();
           // handle msg
           // fucking codes
           // int threadCount = 1;
           // int addCount = 5;
           // while (threadCount < 1000) {
           // threadCount += addCount;
           // handleMessages(threadCount);
           // }
           handleMessages(1);
           handleMessages(5);
           handleMessages(10);
           handleMessages(50);
           handleMessages(100);
           handleMessages(500);
           handleMessages(1000);
    
       }
    }
    
    output:
    Use thread count: 1, cost times: 101ms
    Use thread count: 5, cost times: 30ms
    Use thread count: 10, cost times: 27ms
    Use thread count: 50, cost times: 22ms
    Use thread count: 100, cost times: 27ms
    Use thread count: 500, cost times: 48ms
    Use thread count: 1000, cost times: 76ms

    上面的例子说明的是典型的游戏业务中的消息处理场景。模拟的业务是玩家查询自己是否有指定的物品,不涉及到db的io操作,只是在缓存中的一个查询操作,我夸大了下量级。底下的output是这个操作分别被多个线程同时处理的一个耗时情况。你会发现,并不是线程开的越多,执行会越快,而且开100个线程的效率和开5个线程的差不多,往后线程越多,反而越耗时。原因其实也很简单,因为我们机器的CPU核数是固定的,所以当超过核数或者某个固定的阀值以后,线程越多反而越累赘,因为,这会引发大量的线程的上下文执行的切换,这时候的线程调度大部分时间在做无用功,这些都是需要耗时间的。而且创建线程是需要消耗资源的,我上头这个例子中的fucking codes就是创建了太多线程,把我机器搞挂了。还有一种类外情况就是当我们处理的请求是io请求的时候,我们是可以适量的多开线程的,因为io会导致当前线程的阻塞,所以适当多的线程可以提高io这种情况的吞吐量。

  2. 业务和DB请求同步处理。这里代码没有深入看,但是最初印象Master的DB读写都是同步的。多以这里的要提高业务响应速度可以对DB读写进行优化,比如DB读优化可以加入缓存,DB写优化可以使用数据异步写。这样响应和吞吐都会上来。
  3. 使用java的Serializable + json的反序列化?这点其实也比较痛,但是要改这里比较麻烦,所以权衡再说。
  4. ClassLoader.loadClass + 反射进行业务处理?我看到RPCServer端的Action实现类是在请求到来的时候才进行加载,然后大量使用反射的方式去实现的。这里也有优化空间。

    5. 问题的解决办法

  5. 模板服务那里可以直接放置到业务本地处理。
  6. 业务服务器适当开线程数量。Mina的IoProcessor可以采用默认的CPU+1。业务处理那里根据物理机情况也可采用CPU+1这种方式,但是如果想要一个理想的值,我们可以通过压力测试等模拟测试来测试后选取。而且要知道程序中会使用各种开源的框架,这些框架本身可能会创建很多线程,比如数据库连接池C3P0之类的。

    6. 更好的架构

这是我手画的架构演化,上面的是现在的架构,底下的是建议的架构:

其实对于这种滚服的运营模式,第二个架构还完全可以更简单一些,比如去掉web和login这两层前端web代理。直接采用业务服长连接对外的模式,而且承载量也不会低于5000,但是这样改造成本可能会很高,所以衡量来看。

7. 如何执行优化

优化这里我的建议是这样的。首先我们需要有个理论的设计承载值,比如5000人,然后根据游戏类型给一个单位时间的查询量来计算出对应的游戏的QPS(query per second)。然后算出每个消息的处理极值时间PER_MESSAGE_MAX_HANDLE_TIMES。然后跑压测,找出超过这个值的性能点。进行各个点的调优。

8. 后续。。。

代码其实我看的还是比较肤浅,大量的细节没有深入去看,所以提出的建议之类的可能也不够深入,大家轻拍砖,后续有时间我们一起讨论来优化这个架构。