環(huán)境:后端小白的實(shí)戰(zhàn)筆記與架構(gòu)設(shè)計(jì))
1. 從零搭建直播高并發(fā)環(huán)境一個(gè)后端小白的實(shí)戰(zhàn)筆記直播這玩意兒看別人做覺(jué)得挺簡(jiǎn)單——推個(gè)流、拉個(gè)流、彈幕聊聊天能有多難真到自己動(dòng)手搭一套能扛住高并發(fā)的環(huán)境才發(fā)現(xiàn)每一步都是坑。我是一名后端開(kāi)發(fā)之前主要寫(xiě)業(yè)務(wù)接口對(duì)直播領(lǐng)域幾乎零經(jīng)驗(yàn)。這個(gè)項(xiàng)目是我從零開(kāi)始一步步把一套直播高并發(fā)環(huán)境搭起來(lái)的完整記錄。里面涉及架構(gòu)設(shè)計(jì)、技術(shù)選型、Redis 緩存設(shè)計(jì)、高并發(fā) IM、前后端分離等核心環(huán)節(jié)適合有一定后端基礎(chǔ)、想了解直播系統(tǒng)怎么扛并發(fā)的朋友參考。我不會(huì)講太多虛的主要說(shuō)清楚每個(gè)決策背后的邏輯、踩過(guò)的坑以及可以直接抄作業(yè)的配置和代碼。整個(gè)項(xiàng)目從最初的單機(jī) demo到后來(lái)能支撐萬(wàn)級(jí)并發(fā)在線(xiàn)中間經(jīng)歷了三次比較大的架構(gòu)調(diào)整。第一次是發(fā)現(xiàn)單節(jié)點(diǎn)扛不住彈幕寫(xiě)入第二次是消息延遲飆升第三次是緩存擊穿導(dǎo)致數(shù)據(jù)庫(kù)差點(diǎn)崩掉。每一次調(diào)整都讓我對(duì)“高并發(fā)”這三個(gè)字有了更具體的理解。下面我把整個(gè)過(guò)程拆開(kāi)來(lái)講盡量把每個(gè)環(huán)節(jié)的“為什么”說(shuō)透。2. 整體架構(gòu)設(shè)計(jì)與技術(shù)選型思路2.1 為什么最終選了這套組合一開(kāi)始我想得很簡(jiǎn)單Spring Boot 寫(xiě)幾個(gè)接口前端 Vue 調(diào)一下消息用 WebSocket 推數(shù)據(jù)庫(kù) MySQL 存著完事。結(jié)果本地測(cè)試沒(méi)問(wèn)題一上壓測(cè)工具500 并發(fā)就開(kāi)始各種超時(shí)。問(wèn)題出在哪直播場(chǎng)景和普通業(yè)務(wù)系統(tǒng)最大的區(qū)別在于寫(xiě)少讀多、消息密集、實(shí)時(shí)性要求高。一場(chǎng)直播可能有幾萬(wàn)人在線(xiàn)但真正發(fā)彈幕的可能只有幾百人大部分人是在看。這意味著讀請(qǐng)求遠(yuǎn)遠(yuǎn)大于寫(xiě)請(qǐng)求而且讀請(qǐng)求對(duì)延遲極其敏感?;谶@個(gè)特點(diǎn)我最終的技術(shù)選型是這樣的組件選型核心原因后端框架Spring Boot NettyNetty 處理 WebSocket 長(zhǎng)連接比 Tomcat 的 NIO 更可控消息隊(duì)列RocketMQ彈幕削峰填谷保證消息不丟緩存Redis Cluster扛住高頻讀彈幕列表、在線(xiàn)狀態(tài)都走緩存數(shù)據(jù)庫(kù)MySQL 分表只存核心數(shù)據(jù)彈幕歷史歸檔前端Vue3 WebSocket前后端分離實(shí)時(shí)消息用原生 WebSocket壓測(cè)JMeter模擬高并發(fā)場(chǎng)景驗(yàn)證承載能力這里重點(diǎn)說(shuō)下為什么用 Netty 而不是直接用 Spring WebSocket。Spring 的 WebSocket 底層也是 Netty但它封裝得太厚連接數(shù)一上去內(nèi)存和 GC 都不好控制。自己用 Netty 搭可以精細(xì)控制 ByteBuf 的分配和釋放對(duì)長(zhǎng)連接場(chǎng)景更友好。當(dāng)然代價(jià)是代碼復(fù)雜度上去了這個(gè)后面會(huì)講。2.2 架構(gòu)分層與數(shù)據(jù)流向整個(gè)系統(tǒng)的數(shù)據(jù)流向是這樣的用戶(hù)通過(guò)前端發(fā)起 WebSocket 連接Netty 網(wǎng)關(guān)接收后把消息投遞到 RocketMQ消費(fèi)端處理后再寫(xiě) Redis 和 MySQL同時(shí)通過(guò) Netty 推送給房間內(nèi)其他用戶(hù)。讀請(qǐng)求直接走 Redis不碰數(shù)據(jù)庫(kù)。這個(gè)設(shè)計(jì)的關(guān)鍵在于把寫(xiě)路徑和讀路徑徹底分開(kāi)。寫(xiě)路徑經(jīng)過(guò) MQ 異步化保證即使數(shù)據(jù)庫(kù)慢也不會(huì)阻塞消息推送讀路徑全部走緩存保證毫秒級(jí)響應(yīng)。聽(tīng)起來(lái)簡(jiǎn)單但實(shí)際落地時(shí)MQ 的消費(fèi)速度、Redis 的容量規(guī)劃、Netty 的連接管理每一個(gè)都是坑。提示不要一上來(lái)就搞微服務(wù)。我最初就是被“分布式架構(gòu)設(shè)計(jì)”這個(gè)詞帶偏了拆了五六個(gè)服務(wù)結(jié)果運(yùn)維成本極高問(wèn)題排查困難。后來(lái)合并成三個(gè)核心服務(wù)網(wǎng)關(guān)服務(wù)、消息服務(wù)、業(yè)務(wù)服務(wù)反而更穩(wěn)。2.3 容量預(yù)估與資源規(guī)劃做高并發(fā)系統(tǒng)拍腦袋定配置是大忌。我當(dāng)時(shí)的預(yù)估邏輯是這樣的假設(shè)峰值在線(xiàn) 5 萬(wàn)人其中 10% 的人會(huì)發(fā)彈幕也就是 5000 人同時(shí)發(fā)消息。每人平均 10 秒發(fā)一條那么 QPS 大約是 500。但直播場(chǎng)景有突發(fā)性比如主播抽獎(jiǎng)瞬間彈幕量可能翻 10 倍所以按 5000 QPS 來(lái)設(shè)計(jì)。Redis 這邊每個(gè)房間的彈幕列表保留最近 200 條每條彈幕按 200 字節(jié)算一個(gè)房間 40KB。1000 個(gè)房間就是 40MB完全放得下。在線(xiàn)狀態(tài)用 Hash 存儲(chǔ)每個(gè)用戶(hù) 50 字節(jié)5 萬(wàn)用戶(hù)也就 2.5MB。所以 Redis 的內(nèi)存壓力不大主要壓力在網(wǎng)絡(luò) IO 上。MySQL 這邊彈幕歷史按天分表每天一張表保留 30 天。單表數(shù)據(jù)量控制在 500 萬(wàn)以?xún)?nèi)查詢(xún)走索引問(wèn)題不大。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)3.1 Netty 網(wǎng)關(guān)的連接管理Netty 網(wǎng)關(guān)是整個(gè)系統(tǒng)的入口負(fù)責(zé)維護(hù)所有 WebSocket 連接。這里最大的坑是連接數(shù)上不去和內(nèi)存泄漏。我一開(kāi)始用默認(rèn)配置單機(jī)只能撐 3000 連接左右后來(lái)調(diào)整了以下幾個(gè)參數(shù)// Netty 服務(wù)端配置 EventLoopGroup bossGroup new NioEventLoopGroup(1); EventLoopGroup workerGroup new NioEventLoopGroup(16); // 根據(jù) CPU 核數(shù)調(diào)整 ServerBootstrap b new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true) .childOption(ChannelOption.SO_RCVBUF, 32 * 1024) .childOption(ChannelOption.SO_SNDBUF, 32 * 1024) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new HttpServerCodec()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new WebSocketServerProtocolHandler(/ws)); ch.pipeline().addLast(new LiveWebSocketHandler()); } });SO_BACKLOG調(diào)到 1024 是為了應(yīng)對(duì)突發(fā)連接TCP_NODELAY關(guān)閉 Nagle 算法保證消息實(shí)時(shí)性。SO_RCVBUF和SO_SNDBUF調(diào)小是為了降低內(nèi)存占用因?yàn)橹辈ハ⒍己苄〔恍枰缶彌_區(qū)。連接管理方面我用了一個(gè)ConcurrentHashMap來(lái)保存userId - Channel的映射同時(shí)用另一個(gè) Map 保存roomId - SetuserId。這樣推送消息時(shí)先根據(jù) roomId 找到所有 userId再找到對(duì)應(yīng)的 Channel 推送。注意這里一定要用ConcurrentHashMap否則并發(fā)場(chǎng)景下會(huì)出問(wèn)題。注意Channel 的writeAndFlush是異步的如果客戶(hù)端網(wǎng)絡(luò)慢消息會(huì)堆積在 ChannelOutboundBuffer 里導(dǎo)致內(nèi)存暴漲。我的做法是加一個(gè)水位監(jiān)聽(tīng)超過(guò)閾值就斷開(kāi)連接或者丟棄消息。3.2 Redis 緩存設(shè)計(jì)與高并發(fā)讀寫(xiě)Redis 在這個(gè)系統(tǒng)里承擔(dān)了三個(gè)角色彈幕緩存、在線(xiàn)狀態(tài)、分布式鎖。彈幕緩存用 List 結(jié)構(gòu)每個(gè)房間一個(gè) key比如live:danmaku:room:1001用LPUSH寫(xiě)入LTRIM保留最近 200 條。讀的時(shí)候用LRANGE一次性取出來(lái)。在線(xiàn)狀態(tài)用 Hashkey 是live:online:room:1001field 是 userIdvalue 是時(shí)間戳。用戶(hù)進(jìn)入房間時(shí)HSET離開(kāi)時(shí)HDEL。統(tǒng)計(jì)在線(xiàn)人數(shù)用HLENO(1) 復(fù)雜度非???。分布式鎖用在“房間創(chuàng)建”和“用戶(hù)禁言”這兩個(gè)場(chǎng)景。用SET key value NX PX 3000實(shí)現(xiàn)value 用 UUID釋放鎖時(shí)用 Lua 腳本保證原子性。if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end這里有個(gè)坑緩存擊穿。熱門(mén)房間的彈幕列表如果過(guò)期了大量請(qǐng)求同時(shí)打到數(shù)據(jù)庫(kù)數(shù)據(jù)庫(kù)瞬間就掛了。我的解決方案是彈幕列表永不過(guò)期但設(shè)置一個(gè)邏輯過(guò)期時(shí)間后臺(tái)定時(shí)任務(wù)刷新。這樣既保證了數(shù)據(jù)新鮮度又避免了擊穿。另一個(gè)坑是大 key。一個(gè)房間的彈幕列表如果無(wú)限增長(zhǎng)會(huì)變成大 key操作時(shí)阻塞 Redis。所以一定要用LTRIM限制長(zhǎng)度200 條足夠了用戶(hù)也不會(huì)翻太久。3.3 RocketMQ 削峰填谷與消息可靠性彈幕消息不直接寫(xiě)庫(kù)而是先投遞到 RocketMQ消費(fèi)端再慢慢處理。這樣做的好處是即使數(shù)據(jù)庫(kù)壓力大消息也不會(huì)丟只是延遲增加。RocketMQ 的配置要點(diǎn)DefaultMQProducer producer new DefaultMQProducer(live_producer_group); producer.setNamesrvAddr(127.0.0.1:9876); producer.setRetryTimesWhenSendFailed(3); producer.setSendMsgTimeout(3000); producer.start(); Message msg new Message(live_topic, danmaku, JSON.toJSONBytes(danmaku)); SendResult result producer.send(msg);消費(fèi)端用MessageListenerConcurrently并發(fā)消費(fèi)線(xiàn)程數(shù)根據(jù) CPU 核數(shù)調(diào)整。注意消費(fèi)端一定要做冪等處理因?yàn)?RocketMQ 保證至少投遞一次可能重復(fù)。我的做法是用彈幕 ID 做去重Redis 里SETNX一個(gè) key過(guò)期時(shí)間 5 分鐘。提示RocketMQ 的 Topic 要提前創(chuàng)建并且設(shè)置合理的隊(duì)列數(shù)。隊(duì)列數(shù)決定了并發(fā)消費(fèi)的并行度我設(shè)的是 8 個(gè)隊(duì)列對(duì)應(yīng) 8 個(gè)消費(fèi)線(xiàn)程。3.4 前后端分離下的 WebSocket 通信前端用 Vue3WebSocket 連接封裝在useWebSocket這個(gè) composable 里。核心邏輯是連接建立后監(jiān)聽(tīng)onmessage收到消息后根據(jù)類(lèi)型分發(fā)到不同的處理函數(shù)。心跳包每 30 秒發(fā)一次服務(wù)端如果 60 秒沒(méi)收到心跳就斷開(kāi)連接。const ws new WebSocket(wss://live.example.com/ws?token token); ws.onopen () { setInterval(() { ws.send(JSON.stringify({ type: heartbeat })); }, 30000); }; ws.onmessage (event) { const data JSON.parse(event.data); if (data.type danmaku) { danmakuList.value.push(data.payload); } };跨域問(wèn)題在 WebSocket 里不存在因?yàn)?WebSocket 不受同源策略限制。但鑒權(quán)要做在連接建立時(shí)通過(guò) URL 參數(shù)傳 token服務(wù)端校驗(yàn)通過(guò)后才允許連接。不要等到連接建立后再發(fā)鑒權(quán)消息那樣會(huì)浪費(fèi)連接資源。4. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)4.1 環(huán)境準(zhǔn)備與基礎(chǔ)服務(wù)搭建我用的是一臺(tái) 8 核 16G 的云服務(wù)器操作系統(tǒng) Ubuntu 22.04?;A(chǔ)服務(wù)包括 MySQL 8.0、Redis 7.0、RocketMQ 5.0全部用 Docker 部署方便管理。# Redis 集群部署3 主 3 從 docker run -d --name redis-node-1 --net host redis:7.0 \ redis-server --port 7001 --cluster-enabled yes \ --cluster-config-file nodes.conf --cluster-node-timeout 5000 \ --appendonly yesMySQL 的配置重點(diǎn)是max_connections調(diào)到 2000innodb_buffer_pool_size調(diào)到 8Ginnodb_flush_log_at_trx_commit設(shè)為 2犧牲一點(diǎn)持久性換性能直播場(chǎng)景可以接受。RocketMQ 的 Broker 配置里brokerRole設(shè)為ASYNC_MASTERflushDiskType設(shè)為ASYNC_FLUSH這樣寫(xiě)入性能最好。當(dāng)然如果對(duì)消息可靠性要求極高可以改成同步刷盤(pán)但吞吐量會(huì)下降。4.2 核心代碼實(shí)現(xiàn)彈幕發(fā)送與推送彈幕發(fā)送的完整流程是這樣的前端通過(guò) WebSocket 發(fā)送彈幕內(nèi)容Netty 網(wǎng)關(guān)收到后先做參數(shù)校驗(yàn)和敏感詞過(guò)濾然后投遞到 RocketMQ同時(shí)返回一個(gè) ack 給前端。消費(fèi)端從 MQ 拉取消息寫(xiě)入 Redis 和 MySQL然后通過(guò) Netty 推送給房間內(nèi)所有用戶(hù)。// Netty 網(wǎng)關(guān)處理彈幕 protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) { DanmakuRequest request JSON.parseObject(frame.text(), DanmakuRequest.class); // 參數(shù)校驗(yàn) if (StringUtils.isBlank(request.getContent()) || request.getContent().length() 100) { ctx.writeAndFlush(new TextWebSocketFrame(參數(shù)錯(cuò)誤)); return; } // 敏感詞過(guò)濾 if (sensitiveFilter.contains(request.getContent())) { ctx.writeAndFlush(new TextWebSocketFrame(內(nèi)容包含敏感詞)); return; } // 投遞到 MQ Message msg new Message(live_topic, danmaku, JSON.toJSONBytes(request)); producer.send(msg); // 返回 ack ctx.writeAndFlush(new TextWebSocketFrame(ok)); }消費(fèi)端的邏輯consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) - { for (MessageExt msg : msgs) { DanmakuRequest request JSON.parseObject(msg.getBody(), DanmakuRequest.class); // 冪等去重 String key danmaku:dedup: request.getId(); Boolean success redisTemplate.opsForValue().setIfAbsent(key, 1, 5, TimeUnit.MINUTES); if (Boolean.FALSE.equals(success)) { continue; } // 寫(xiě) Redis String redisKey live:danmaku:room: request.getRoomId(); redisTemplate.opsForList().leftPush(redisKey, JSON.toJSONString(request)); redisTemplate.opsForList().trim(redisKey, 0, 199); // 寫(xiě) MySQL異步 danmakuMapper.insert(request); // 推送給房間內(nèi)用戶(hù) pushService.pushToRoom(request.getRoomId(), request); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; });推送服務(wù)里從roomUserMap拿到所有 userId再遍歷userChannelMap拿到 Channel調(diào)用writeAndFlush。注意這里要加異常處理如果 Channel 已經(jīng)關(guān)閉要從 Map 里移除否則會(huì)內(nèi)存泄漏。4.3 壓測(cè)方案與性能調(diào)優(yōu)壓測(cè)用 JMeter模擬 5000 個(gè)并發(fā)用戶(hù)每個(gè)用戶(hù)建立 WebSocket 連接每秒發(fā)送一條彈幕。壓測(cè)腳本里用WebSocket Sampler配置好連接地址和消息內(nèi)容。第一次壓測(cè)結(jié)果很慘5000 并發(fā)下消息延遲平均 2 秒P99 達(dá)到 8 秒。排查后發(fā)現(xiàn)三個(gè)問(wèn)題一是 Netty 的 worker 線(xiàn)程數(shù)不夠二是 RocketMQ 的隊(duì)列數(shù)太少三是 Redis 的LPUSH和LTRIM不是原子操作導(dǎo)致數(shù)據(jù)不一致。調(diào)優(yōu)措施worker 線(xiàn)程數(shù)從 8 調(diào)到 16RocketMQ 隊(duì)列數(shù)從 4 調(diào)到 8Redis 操作用 Lua 腳本合并成原子操作。再次壓測(cè)延遲降到 200msP99 降到 800ms基本達(dá)標(biāo)。-- 原子寫(xiě)入彈幕并裁剪 redis.call(lpush, KEYS[1], ARGV[1]) redis.call(ltrim, KEYS[1], 0, 199) return 1注意壓測(cè)時(shí)一定要監(jiān)控服務(wù)器資源。我用top和iostat看 CPU 和磁盤(pán) IO用redis-cli --latency看 Redis 延遲用 RocketMQ 的控制臺(tái)看消息堆積情況。任何一個(gè)指標(biāo)異常都要停下來(lái)分析。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 連接數(shù)上不去怎么辦這是最常見(jiàn)的問(wèn)題?,F(xiàn)象是壓測(cè)到一定并發(fā)后新連接建立失敗。排查思路先看ulimit -n默認(rèn)是 1024改成 65535。再看 Netty 的SO_BACKLOG默認(rèn)是 128改成 1024。最后看操作系統(tǒng)的somaxconnsysctl -w net.core.somaxconn1024。這三個(gè)地方都改了單機(jī)撐 1 萬(wàn)連接沒(méi)問(wèn)題。5.2 消息延遲突然飆升延遲飆升通常是 MQ 堆積或者 Redis 慢查詢(xún)導(dǎo)致的。先看 RocketMQ 的消費(fèi)進(jìn)度如果堆積量大說(shuō)明消費(fèi)端處理不過(guò)來(lái)要么加消費(fèi)線(xiàn)程要么優(yōu)化消費(fèi)邏輯。再看 Redis 的slowlog如果有LRANGE大 key 的操作說(shuō)明彈幕列表太長(zhǎng)了要檢查L(zhǎng)TRIM是否生效。5.3 緩存擊穿導(dǎo)致數(shù)據(jù)庫(kù)崩潰前面提過(guò)熱門(mén)房間的彈幕列表如果過(guò)期大量請(qǐng)求會(huì)打到數(shù)據(jù)庫(kù)。解決方案是邏輯過(guò)期 后臺(tái)刷新。具體做法是Redis 里的彈幕列表不設(shè)過(guò)期時(shí)間但每個(gè)房間額外存一個(gè)expire_time后臺(tái)任務(wù)定時(shí)檢查快過(guò)期時(shí)主動(dòng)刷新。這樣用戶(hù)永遠(yuǎn)讀不到空數(shù)據(jù)數(shù)據(jù)庫(kù)也不會(huì)被擊穿。5.4 常見(jiàn)問(wèn)題速查表問(wèn)題現(xiàn)象可能原因排查方法解決方案連接建立失敗文件描述符限制ulimit -n調(diào)到 65535消息延遲高M(jìn)Q 堆積控制臺(tái)看消費(fèi)進(jìn)度加消費(fèi)線(xiàn)程或優(yōu)化邏輯Redis 超時(shí)大 key 操作slowlog限制 key 長(zhǎng)度數(shù)據(jù)庫(kù) CPU 高緩存擊穿看 QPS 和緩存命中率邏輯過(guò)期 后臺(tái)刷新內(nèi)存泄漏Channel 未移除jmap看對(duì)象數(shù)量關(guān)閉時(shí)從 Map 移除5.5 獨(dú)家避坑技巧第一個(gè)技巧Netty 的Channel一定要在channelInactive回調(diào)里從 Map 移除否則連接斷開(kāi)后 Map 越來(lái)越大最后 OOM。我一開(kāi)始就是忘了這個(gè)跑了一天內(nèi)存就滿(mǎn)了。第二個(gè)技巧RocketMQ 的消息 ID 在重試時(shí)會(huì)變所以?xún)绲热ブ夭荒苡孟?ID要用業(yè)務(wù) ID。我在彈幕請(qǐng)求里生成了一個(gè) UUID 作為業(yè)務(wù) ID消費(fèi)端用這個(gè)去重。第三個(gè)技巧Redis 的LPUSH和LTRIM分開(kāi)執(zhí)行時(shí)如果中間服務(wù)掛了會(huì)導(dǎo)致列表無(wú)限增長(zhǎng)。用 Lua 腳本合并成原子操作要么都成功要么都失敗。第四個(gè)技巧壓測(cè)時(shí)不要只壓 WebSocket還要壓 HTTP 接口。因?yàn)橛脩?hù)進(jìn)入房間時(shí)會(huì)調(diào) HTTP 接口獲取歷史彈幕這個(gè)接口如果慢用戶(hù)會(huì)卡在加載頁(yè)面。6. 后續(xù)擴(kuò)展與個(gè)人體會(huì)這套環(huán)境目前能穩(wěn)定支撐 1 萬(wàn)并發(fā)在線(xiàn)峰值 5000 QPS 的彈幕寫(xiě)入。后續(xù)如果要擴(kuò)展到 10 萬(wàn)并發(fā)需要做幾件事一是 Netty 網(wǎng)關(guān)做集群用 Nginx 做四層負(fù)載均衡二是 Redis 做集群分片按房間 ID 哈希三是 RocketMQ 加 Broker 節(jié)點(diǎn)提升吞吐量。我個(gè)人在實(shí)際操作中的體會(huì)是高并發(fā)系統(tǒng)不是靠堆配置堆出來(lái)的而是靠對(duì)業(yè)務(wù)場(chǎng)景的深刻理解。直播場(chǎng)景的核心是“實(shí)時(shí)”和“不丟消息”所有的技術(shù)選型和架構(gòu)設(shè)計(jì)都要圍繞這兩個(gè)點(diǎn)。不要盲目追求新技術(shù)適合場(chǎng)景的才是最好的。比如我一開(kāi)始想用 Kafka后來(lái)發(fā)現(xiàn) RocketMQ 的延遲更低更適合直播場(chǎng)景。最后再分享一個(gè)小技巧做壓測(cè)時(shí)先用小并發(fā)跑通流程再逐步加大并發(fā)。每次只調(diào)一個(gè)參數(shù)觀察效果。這樣出了問(wèn)題容易定位不會(huì)一團(tuán)亂麻。我見(jiàn)過(guò)很多人一上來(lái)就壓 1 萬(wàn)并發(fā)結(jié)果系統(tǒng)崩了都不知道哪里崩的。