Netty 100萬級高併發伺服器配置

来源:https://www.cnblogs.com/crazymakercircle/archive/2018/11/05/9911981.html
-Advertisement-
Play Games

前言 每一種該語言在某些極限情況下的表現一般都不太一樣,那麼我常用的Java語言,在達到100萬個併發連接情況下,會怎麼樣呢,有些好奇,更有些期盼。 這次使用經常使用的順手的 netty NIO框架(netty 3.6.5.Final),封裝的很好,介面很全面,就像它現在的功能變數名稱 netty.io , ...


前言

每一種該語言在某些極限情況下的表現一般都不太一樣,那麼我常用的Java語言,在達到100萬個併發連接情況下,會怎麼樣呢,有些好奇,更有些期盼。
這次使用經常使用的順手的netty NIO框架(netty-3.6.5.Final),封裝的很好,介面很全面,就像它現在的功能變數名稱 netty.io,專註於網路IO。
整個過程沒有什麼技術含量,淺顯分析過就更顯得有些枯燥無聊,準備好,硬著頭皮吧。

測試伺服器配置

運行在VMWare Workstation 9中,64位Centos 6.2系統,分配14.9G記憶體左右,4核。
已安裝有Java7版本:

java version "1.7.0_21"
Java(TM) SE Runtime Environment (build 1.7.0_21-b11)
Java HotSpot(TM) 64-Bit Server VM (build 23.21-b01, mixed mode)

在/etc/sysctl.conf中添加如下配置:

fs.file-max = 1048576
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_mem = 786432 2097152 3145728
net.ipv4.tcp_rmem = 4096 4096 16777216
net.ipv4.tcp_wmem = 4096 4096 16777216

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1

在/etc/security/limits.conf中添加如下配置:

     *  soft nofile 1048576
     *  hard nofile 1048576

測試端

測試端無論是配置還是程式和以前一樣,翻看前幾篇博客就可以看到client5.c的源碼,以及相關的配置信息等。

伺服器程式

這次也是很簡單吶,沒有業務功能,客戶端HTTP請求,服務端輸出chunked編碼內容。

入口HttpChunkedServer.java:

package com.test.server;

import static org.jboss.netty.channel.Channels.pipeline;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.jboss.netty.handler.stream.ChunkedWriteHandler;


public class HttpChunkedServer {

    private final int port;

    public HttpChunkedServer(intport) {
        this.port = port;
    }

    public void run() {
        // Configure the server.
        ServerBootstrap bootstrap = new ServerBootstrap(
                new NioServerSocketChannelFactory(
                        Executors.newCachedThreadPool(),
                        Executors.newCachedThreadPool()));

        // Set up the event pipeline factory.
        bootstrap.setPipelineFactory(newChannelPipelineFactory() {
            public ChannelPipeline getPipeline ()throws Exception {
                ChannelPipeline pipeline = pipeline();
                pipeline.addLast("decoder", new HttpRequestDecoder());
                pipeline.addLast("aggregator", new HttpChunkAggregator(65536));
                pipeline.addLast("encoder", new HttpResponseEncoder());
                pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());
                pipeline.addLast("handler", new HttpChunkedServerHandler());
                return pipeline;
            }
        });

        bootstrap.setOption("child.reuseAddress", true);
        bootstrap.setOption("child.tcpNoDelay", true);
        bootstrap.setOption("child.keepAlive", true);
        // Bind and start to accept incoming connections.
        bootstrap.bind(newInetSocketAddress(port));
    }

    public static void main(String[] args) {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }

        System.out.format("server start with port %d \n", port);
        new HttpChunkedServer(port).run();
    }
}

唯一的自定義處理器HttpChunkedServerHandler.java:

package com.test.server;

import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static org.jboss.netty.handler.codec.http.HttpMethod.GET;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK;
import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import java.util.concurrent.atomic.AtomicInteger;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.handler.codec.frame.TooLongFrameException;
import org.jboss.netty.handler.codec.http.DefaultHttpChunk;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.util.CharsetUtil;

public class HttpChunkedServerHandlerextends SimpleChannelUpstreamHandler {
    private static final AtomicInteger count = new AtomicInteger(0);

    private void increment() {
        System.out.format("online user %d\n", count.incrementAndGet());
    }
    
    private void decrement() {
        if (count.get() <= 0) {
            System.out.format("~online user %d\n", 0);
        } else {
            System.out.format("~online user %d\n", count.decrementAndGet());
        }
    }
    
    @Override
    public void messageReceived(ChannelHandlerContextctx, MessageEvent e)
            throws Exception {
        HttpRequest request = (HttpRequest) e.getMessage();
        if (request.getMethod() != GET) {
            sendError(ctx, METHOD_NOT_ALLOWED);
            return;
        }
        
        sendPrepare(ctx);
        increment();
    }
    
    @Override
    public void channelDisconnected(ChannelHandlerContextctx,
                                    ChannelStateEvent e) throws Exception {
        decrement();
        super.channelDisconnected(ctx, e);
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContextctx, ExceptionEvent e)
            throws Exception {
        Throwable cause = e.getCause();
        if (cause instanceof TooLongFrameException) {
            sendError(ctx, BAD_REQUEST);
            return;
        }
    }
    
    private static void sendError(ChannelHandlerContext ctx,
                                  HttpResponseStatus status) {
        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);
        response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8");
        response.setContent(ChannelBuffers.copiedBuffer(
                "Failure:" + status.toString() + "\r\n", CharsetUtil.UTF_8));
        
        // Close the connection as soon as the error message is sent.
        ctx.getChannel().write(response)
                .addListener(ChannelFutureListener.CLOSE);
    }
    
    private void sendPrepare(ChannelHandlerContextctx) {
        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
        response.setChunked(true);
        response.setHeader(HttpHeaders.Names.CONTENT_TYPE,
                "text/html; charset=UTF-8");
        response.addHeader(HttpHeaders.Names.CONNECTION,
                HttpHeaders.Values.KEEP_ALIVE);
        response.setHeader(HttpHeaders.Names.TRANSFER_ENCODING,
                HttpHeaders.Values.CHUNKED);
        
        Channel chan = ctx.getChannel();
        chan.write(response);
        
        // 緩衝必須湊夠256位元組,瀏覽器端才能夠正常接收 ...
        StringBuilder builder = new StringBuilder();
        builder.append("");
        int leftChars = 256 - builder.length();
        for (int i = 0; i < leftChars; i++) {
            builder.append("");
        }
        
        writeStringChunk(chan, builder.toString());
    }
    
    private void writeStringChunk(Channelchannel, String data) {
        ChannelBuffer chunkContent = ChannelBuffers.dynamicBuffer(channel
                .getConfig().getBufferFactory());
        chunkContent.writeBytes(data.getBytes());
        HttpChunk chunk = new DefaultHttpChunk(chunkContent);
        channel.write(chunk);
    }
}

啟動腳本start.sh

set CLASSPATH=.
nohup java -server -Xmx6G -Xms6G -Xmn600M -XX:PermSize=50M -XX:MaxPermSize=50M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -Djava.ext.dirs=lib com.test.server.HttpChunkedServer 8000>server.out 2>&1 &

達到100萬併發連接時的一些信息

每次伺服器端達到一百萬個併發持久連接之後,然後關掉測試端程式,斷開所有的連接,等到伺服器端日誌輸出線上用戶為0時,再次重覆以上步驟。在這反反覆復的情況下,觀察記憶體等信息的一些情況。以某次斷開所有測試端為例後,當前系統占用為(設置為list_free_1):

                  total       used       free     shared    buffers     cached
     Mem:         15189       7736       7453          0         18        120
     -/+ buffers/cache:       7597       7592
     Swap:         4095        948       3147

通過top觀察,其進程相關信息

    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                       
   4925 root      20   0 8206m 4.3g 2776 S  0.3 28.8  50:18.66 java

在啟動腳本start.sh中,我們設置堆記憶體為6G。

ps aux|grep java命令獲得信息:

  root      4925 38.0 28.8 8403444 4484764 ?     Sl   15:26  50:18 java -server...HttpChunkedServer 8000

RSS占用記憶體為4484764K/1024K=4379M

然後再次啟動測試端,在伺服器接收到online user 1023749時,ps aux|grep java內容為:

  root      4925 43.6 28.4 8403444 4422824 ?     Sl   15:26  62:53 java -server...

查看當前網路信息統計

  ss -s
  Total: 1024050 (kernel 1024084)
  TCP:   1023769 (estab 1023754, closed 2, orphaned 0, synrecv 0, timewait 0/0), ports 12

  Transport Total     IP        IPv6
  *    1024084   -         -        
  RAW     0         0         0        
  UDP     7         6         1        
  TCP     1023767   12        1023755  
  INET    1023774   18        1023756  
  FRAG    0         0         0    

通過top查看一下

  top -p 4925
  top - 17:51:30 up  3:02,  4 users,  load average: 1.03, 1.80, 1.19
  Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
  Cpu0  :  0.9%us,  2.6%sy,  0.0%ni, 52.9%id,  1.0%wa, 13.6%hi, 29.0%si,  0.0%st
  Cpu1  :  1.4%us,  4.5%sy,  0.0%ni, 80.1%id,  1.9%wa,  0.0%hi, 12.0%si,  0.0%st
  Cpu2  :  1.5%us,  4.4%sy,  0.0%ni, 80.5%id,  4.3%wa,  0.0%hi,  9.3%si,  0.0%st
  Cpu3  :  1.9%us,  4.4%sy,  0.0%ni, 84.4%id,  3.2%wa,  0.0%hi,  6.2%si,  0.0%st
  Mem:  15554336k total, 15268728k used,   285608k free,     3904k buffers
  Swap:  4194296k total,  1082592k used,  3111704k free,    37968k cached

    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                       
   4925 root      20   0 8206m 4.2g 2220 S  3.3 28.4  62:53.66 java

四核都被占用了,每一個核心不太平均。這是在虛擬機中得到結果,可能真實伺服器會更好一些。 因為不是CPU密集型應用,CPU不是問題,無須多加關註。

系統記憶體狀況

  free -m
               total       used       free     shared    buffers     cached
  Mem:         15189      14926        263          0          5         56
  -/+ buffers/cache:      14864        324
  Swap:         4095       1057       3038

物理記憶體已經無法滿足要求了,占用了1057M虛擬記憶體。

查看一下堆記憶體情況

  jmap -heap 4925
  Attaching to process ID 4925, please wait...
  Debugger attached successfully.
  Server compiler detected.
  JVM version is 23.21-b01

  using parallel threads in the new generation.
  using thread-local object allocation.
  Concurrent Mark-Sweep GC

  Heap Configuration:
     MinHeapFreeRatio = 40
     MaxHeapFreeRatio = 70
     MaxHeapSize      = 6442450944 (6144.0MB)
     NewSize          = 629145600 (600.0MB)
     MaxNewSize       = 629145600 (600.0MB)
     OldSize          = 5439488 (5.1875MB)
     NewRatio         = 2
     SurvivorRatio    = 1
     PermSize         = 52428800 (50.0MB)
     MaxPermSize      = 52428800 (50.0MB)
     G1HeapRegionSize = 0 (0.0MB)

  Heap Usage:
  New Generation (Eden + 1 Survivor Space):
     capacity = 419430400 (400.0MB)
     used     = 308798864 (294.49354553222656MB)
     free     = 110631536 (105.50645446777344MB)
     73.62338638305664% used
  Eden Space:
     capacity = 209715200 (200.0MB)
     used     = 103375232 (98.5863037109375MB)
     free     = 106339968 (101.4136962890625MB)
     49.29315185546875% used
  From Space:
     capacity = 209715200 (200.0MB)
     used     = 205423632 (195.90724182128906MB)
     free     = 4291568 (4.0927581787109375MB)
     97.95362091064453% used
  To Space:
     capacity = 209715200 (200.0MB)
     used     = 0 (0.0MB)
     free     = 209715200 (200.0MB)
     0.0% used
  concurrent mark-sweep generation:
     capacity = 5813305344 (5544.0MB)
     used     = 4213515472 (4018.321487426758MB)
     free     = 1599789872 (1525.6785125732422MB)
     72.48054631000646% used
  Perm Generation:
     capacity = 52428800 (50.0MB)
     used     = 5505696 (5.250640869140625MB)
     free     = 46923104 (44.749359130859375MB)
     10.50128173828125% used

  1439 interned Strings occupying 110936 bytes.

老生代占用記憶體為72%,較為合理,畢竟系統已經處理100萬個連接。

再次斷開所有測試端,看看系統記憶體(free -m)

               total       used       free     shared    buffers     cached
  Mem:         15189       7723       7466          0         13        120
  -/+ buffers/cache:       7589       7599
  Swap:         4095        950       3145

記為list_free_2

list_free_1list_free_2兩次都釋放後的記憶體比較結果,系統可用物理已經記憶體已經降到7589M,先前可是7597M物理記憶體。
總之,我們的JAVA測試程式在記憶體占用方面已經,最低需要7589 + 950 = 8.6G記憶體為最低需求記憶體吧。

GC日誌

我們在啟動腳本處設置的一大串參數,到底是否達到目標,還得從gc日誌處獲得具體效果,推薦使用GCViewer

GC事件概覽:
gc_eventdetails

其它:
gc_total_1 gc_total_2 gc_total_3

總之:

  • 只進行了一次Full GC,代價太高,停頓了12秒。
  • PartNew成為了停頓大戶,導致整個系統停頓了41秒之久,不可接受。
  • 當前JVM調優喜憂參半,還得繼續努力等

小結

Java與與Erlang、C相比,比較麻煩的事情,需要在程式一開始就得準備好它的堆棧到底需要多大空間,換個說法就是JVM啟動參數設置堆記憶體大小,設置合適的垃圾回收機制,若以後程式需要更多記憶體,需停止程式,編輯啟動參數,然後再次啟動。總之一句話,就是麻煩。單單JVM的調優,就得持續不斷的根據檢測、信息、日誌等進行適當微調。

  • JVM需要提前指定堆大小,相比Erlang/C,這可能是個麻煩
  • GC(垃圾回收),相對比麻煩,需要持續不斷的根據日誌、JVM堆棧信息、運行時情況進行JVM參數微調
  • 設置一個最大連接目標,多次測試達到頂峰,然後釋放所有連接,反覆觀察記憶體占用,獲得一個較為合適的系統運行記憶體值
  • Eclipse Memory Analyzer結合jmap導出堆棧DUMP文件,分析記憶體泄漏,還是很方便的
  • 想修改運行時內容,或者稱之為熱載入,預設不可能
  • 真實機器上會有更好的反映

吐槽一下:
JAVA OSGI,相對比Erlang來說,需要人轉換思路,不是那麼原生的東西,總是有些彆扭,社區或商業公司對此的修修補補,不過是實現一些面向對象所不具備的熱載入的企業特性。

測試源代碼,下載just_test


無編程不創客,無案例不學習。瘋狂創客圈,一大波高手正在交流、學習中!

瘋狂創客圈 Netty 死磕系列 10多篇深度文章: 【博客園 總入口】 QQ群:104131248


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 轉自:https://blog.csdn.net/eson_15/article/details/51425010 上一節我們做完了購物車的基本操作,但是有個問題是:當用戶點擊結算時,我們應該做一個登錄的判斷,判斷用戶有沒有登錄,沒有登錄的話,得首先讓用戶登錄。這就用到了過濾器的技術了,過濾器是專門 ...
  • 添加商品部分原理和添加商品類別是一樣的,不過要比商品類別複雜,因為商品的屬性有很多,對應的資料庫中的欄位也就多了,添加商品還有個選項是上傳圖片,這一小塊內容會在下一篇博客中單獨說明,因為這涉及到一個知識點,就是Struts2實現文件上傳功能。其他廢話不多說了,現在開始完善添加商品部分的代碼: 1.  ...
  • 匿名函數基本格式: func= lambda i : ret # i 是形參,ret 是返回值 func() #調用匿名函數 內置函數: 1.reverse(註意,都是返回的貼帶起,如果想看內容,就要用for方法) 2.slice,format 3.bytes,bytearray # 切片 —— 字 ...
  • 代碼: 明顯的錯誤: 應改成 import java.util.*; 沒有理解java的基本概念 ...
  • 100-199 信息性狀態碼 100 continue 請繼續 101 switching protocols 切換協議,返回upgraded頭 200-299 成功狀態碼 200 ok 201 created 創建資源 202 accepted 請求已經接收到,不保證完成 203 non-auth... ...
  • 前面介紹while迴圈時,有個名叫year的整型變數頻繁出現,並且它是控制迴圈進出的關鍵要素。不管哪一種while寫法,都存在三處與year有關的操作,分別是“year = 0”、“year<limit”、“year++”。第一個“year = 0”用來給該變數初始賦值,第二個“year<limit ...
  • 多線程 unique_lock的使用 unique_lock的特點: 1,靈活。可以在創建unique_lock的實例時,不鎖,然後手動調用lock_a.lock()函數,或者std::lock(lock_a, …),來上鎖。當unique_lock的實例被析構時,會自動調用unlock函數,釋放鎖 ...
  • 前言 在 "上一篇" 中我們學習了結構型模式的解釋器模式(Interpreter Pattern)和迭代器模式(Iterator Pattern)。本篇則來學習下行為型模式的兩個模式,訪問者模式(Visitor Pattern)和中介者模式(Mediator Pattern)。 訪問者模式 簡介 訪 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...