Open crazyjohn opened 9 years ago
这几天找时间对Master的结构进行了简单的分析。结构没有传闻中的那么糟糕,而且整体来说我觉得它设计的都还不错,考虑到了很多东西,比如RPC调用以及相关容错设计,负载均衡的设计,方便scale out横向扩展等等。所以我觉得它的最早设计初衷是不错的,但是估计后期没有按照设计的蓝图去实现,也没有很合理的实现scale up纵向扩展,不能合理的利用单机的性能,不正确的追求分布式,导致它的组服务器性能不一定能拼过相同配置物理机的单机性能。下面慢慢展开说。 1. 整体部署分析
这几天找时间对Master的结构进行了简单的分析。结构没有传闻中的那么糟糕,而且整体来说我觉得它设计的都还不错,考虑到了很多东西,比如RPC调用以及相关容错设计,负载均衡的设计,方便scale out横向扩展等等。所以我觉得它的最早设计初衷是不错的,但是估计后期没有按照设计的蓝图去实现,也没有很合理的实现scale up纵向扩展,不能合理的利用单机的性能,不正确的追求分布式,导致它的组服务器性能不一定能拼过相同配置物理机的单机性能。下面慢慢展开说。
先看部署图:
部署结构上大概分为4部分。
再给一个详细些的组件图:
Web部分的结构图:
Container核心结构:
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丢到线程池中处理。而且这个线程池尼玛默认开了巨多的线程。后面我们再说这个问题。
Game结构:
Game这里我写的很简单,因为具体的东西基本用的都是核心库Container里头的组件。
Data结构:
Data这里也同理。
整体来说,这里解释的是一次Equip装备升级的时候发生的调用时序。两个时序图加起来基本解释了一次游戏典型调用以后发生的调用时序。
Web work flow:
这里是Web端接受到的一次调用,从Servlet开始,到RPCClient调用,到返回响应。
Rpc work flow:
这里说明了RCPClient进行调用的时候具体发生的消息流调用。
这个结构如果设计好的话,其实是可以用来支撑大世界这样的结构的,比如COC和Boombeach这样的游戏。但是现在根据@吉兴的反馈,同时在线支撑不了800人?那么问题再哪里呢?
尼玛其实具体问题在哪里,我不敢武断的说,因为我们有去很仔细的看业务代码,也没有针对性的跑压测,所以具体性能点没有定位。但是我从整体的角度看了下结构的实现,来提出一些问题。 DataSystem是否有必要存在?或者是否有必要以现在这种方式实现?这个服务是用来为游戏提供静态数据服务的,也就是策划数据映射的模板服务。这类服务的实现特点是:把策划的配置文件(格式可以是excel或者xml或者json或者sql)映射成为具体的模板对象TemplateObject,然后供游戏业务读取,注意是读取,这里一般是没有写请求的。
尼玛其实具体问题在哪里,我不敢武断的说,因为我们有去很仔细的看业务代码,也没有针对性的跑压测,所以具体性能点没有定位。但是我从整体的角度看了下结构的实现,来提出一些问题。
那我们看看Master的实现方式,把DataSystem作为一个服务进程,然后内部使用mysql表作为数据源。不说这样的结构有什么好处,我先说说这样的结构有什么问题,如果模板数据需要跨进程通过网络去访问,那么这中间就多了1层IO,进了这层IO以后还要从mysql中加载数据,这里就又多了一层IO,所以起码就多了两层IO的访问。
如果使用模板服务部署到业务服务本地,然后开服加载缓存到模板缓存,那么业务获取模板数据的时间就是常量级别的。
所以对比下来Master的实现方式要受到更重限制,但是后一种方式天空才是它的极限。
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这种情况的吞吐量。
fucking codes
这是我手画的架构演化,上面的是现在的架构,底下的是建议的架构:
其实对于这种滚服的运营模式,第二个架构还完全可以更简单一些,比如去掉web和login这两层前端web代理。直接采用业务服长连接对外的模式,而且承载量也不会低于5000,但是这样改造成本可能会很高,所以衡量来看。
优化这里我的建议是这样的。首先我们需要有个理论的设计承载值,比如5000人,然后根据游戏类型给一个单位时间的查询量来计算出对应的游戏的QPS(query per second)。然后算出每个消息的处理极值时间PER_MESSAGE_MAX_HANDLE_TIMES。然后跑压测,找出超过这个值的性能点。进行各个点的调优。
代码其实我看的还是比较肤浅,大量的细节没有深入去看,所以提出的建议之类的可能也不够深入,大家轻拍砖,后续有时间我们一起讨论来优化这个架构。
先看部署图:
部署结构上大概分为4部分。
再给一个详细些的组件图:
2. 整体架构分析
Web部分的结构图:
Container核心结构:
RPC请求处理。RPCServer在接受到RPCRequest请求以后,会把请求委托给自己的一个名字为RPCRequestDispatcher的组件进行分发处理,看码:
如码这个请求会被包装成一个InvokerWorker丢到线程池中处理。而且这个线程池尼玛默认开了巨多的线程。后面我们再说这个问题。
Game结构:
Game这里我写的很简单,因为具体的东西基本用的都是核心库Container里头的组件。
Data结构:
Data这里也同理。
3. 核心业务流程时序
Web work flow:
这里是Web端接受到的一次调用,从Servlet开始,到RPCClient调用,到返回响应。
Rpc work flow:
这里说明了RCPClient进行调用的时候具体发生的消息流调用。
4. 现有架构的问题
这个结构如果设计好的话,其实是可以用来支撑大世界这样的结构的,比如COC和Boombeach这样的游戏。但是现在根据@吉兴的反馈,同时在线支撑不了800人?那么问题再哪里呢?
那我们看看Master的实现方式,把DataSystem作为一个服务进程,然后内部使用mysql表作为数据源。不说这样的结构有什么好处,我先说说这样的结构有什么问题,如果模板数据需要跨进程通过网络去访问,那么这中间就多了1层IO,进了这层IO以后还要从mysql中加载数据,这里就又多了一层IO,所以起码就多了两层IO的访问。
如果使用模板服务部署到业务服务本地,然后开服加载缓存到模板缓存,那么业务获取模板数据的时间就是常量级别的。
所以对比下来Master的实现方式要受到更重限制,但是后一种方式天空才是它的极限。
GameSystem业务处理。GameSystem在启动的时候内部使用了线程池,然后开了大量的线程,当有RPC请求来的时候,会包成一个Runnable丢给线程池处理。那么真的是线程开的越多越好吗?我搞了个测试,来看下码:
上面的例子说明的是典型的游戏业务中的消息处理场景。模拟的业务是玩家查询自己是否有指定的物品,不涉及到db的io操作,只是在缓存中的一个查询操作,我夸大了下量级。底下的output是这个操作分别被多个线程同时处理的一个耗时情况。你会发现,并不是线程开的越多,执行会越快,而且开100个线程的效率和开5个线程的差不多,往后线程越多,反而越耗时。原因其实也很简单,因为我们机器的CPU核数是固定的,所以当超过核数或者某个固定的阀值以后,线程越多反而越累赘,因为,这会引发大量的线程的上下文执行的切换,这时候的线程调度大部分时间在做无用功,这些都是需要耗时间的。而且创建线程是需要消耗资源的,我上头这个例子中的
fucking codes
就是创建了太多线程,把我机器搞挂了。还有一种类外情况就是当我们处理的请求是io请求的时候,我们是可以适量的多开线程的,因为io会导致当前线程的阻塞,所以适当多的线程可以提高io这种情况的吞吐量。5. 问题的解决办法
6. 更好的架构
这是我手画的架构演化,上面的是现在的架构,底下的是建议的架构:
其实对于这种滚服的运营模式,第二个架构还完全可以更简单一些,比如去掉web和login这两层前端web代理。直接采用业务服长连接对外的模式,而且承载量也不会低于5000,但是这样改造成本可能会很高,所以衡量来看。
7. 如何执行优化
优化这里我的建议是这样的。首先我们需要有个理论的设计承载值,比如5000人,然后根据游戏类型给一个单位时间的查询量来计算出对应的游戏的QPS(query per second)。然后算出每个消息的处理极值时间PER_MESSAGE_MAX_HANDLE_TIMES。然后跑压测,找出超过这个值的性能点。进行各个点的调优。
8. 后续。。。