)
做Unity WebGL上云部署這件事我前后折騰過不少項目。最典型的一次是幫朋友把一個3D展廳項目發(fā)布出去本地編輯器里跑得飛快構(gòu)建也順利結(jié)果一放到云服務(wù)器上遠程用戶打開瀏覽器不是白屏就是加載超慢有的還會直接崩掉。排查到最后問題全都集中在部署鏈路那幾個容易忽略的細節(jié)上壓縮格式、MIME類型、安全隔離頭、緩存規(guī)則。后來我換成阿里云ECS做源站、ESA做邊緣加速這套組合才真正穩(wěn)定下來。這篇文章就把完整的部署思路和實操步驟寫出來給正在搞Unity WebGL上云的朋友做個參考。1. 方案拆解為什么選ECS做源站、ESA做加速層1.1 名字撞車了此ECS非Unity的ECS先說個容易繞暈的地方。Unity圈子里提到ECS很多人第一反應(yīng)是Entity Component System也就是Unity DOTS那套實體組件架構(gòu)。但部署這里說的ECS是阿里云的云服務(wù)器Elastic Compute Service。兩個縮寫一模一樣很多新手在搜索引擎里一翻出來的全是Unity ECS性能優(yōu)化和DOTS架構(gòu)的文章跟服務(wù)器部署完全不搭邊。我自己也因為這個多繞了不少彎路所以先把名字區(qū)分開。ESA同樣有歧義。歐洲航天局叫ESA有的資料分類里也會出現(xiàn)ESA土地利用數(shù)據(jù)下載之類的關(guān)鍵詞。但我們這里聊的ESA是阿里云的邊緣安全加速Edge Security Acceleration屬于云加速和安全防護產(chǎn)品別搞混。1.2 WebGL發(fā)布到底難在哪Unity WebGL構(gòu)建產(chǎn)物和傳統(tǒng)網(wǎng)站完全是兩碼事。第一次接觸的人最容易犯的錯就是把它當(dāng)成一個普通靜態(tài)網(wǎng)站直接扔進Nginx就能跑。結(jié)果跑起來全是問題。核心難點有幾個。第一是體積大。一個稍微完整點的3D項目構(gòu)建出來的data文件動輒幾百兆如果壓縮策略不對用戶首次加載能等到懷疑人生。第二是WebAssembly對瀏覽器環(huán)境要求苛刻。wasm文件需要正確的Content-Type否則瀏覽器直接拒絕解析如果開啟了多線程還必須有跨域隔離相關(guān)的響應(yīng)頭否則SharedArrayBuffer不可用工程會在啟動時報錯。第三是安全問題。Unity WebGL里的攝像頭、麥克風(fēng)、WebSocket等API強制要求HTTPS非安全上下文里直接不可用。這意味著從ECS到ESA整條鏈路都必須把HTTPS做好。還有一個很多人忽略的點構(gòu)建產(chǎn)物是帶版本hash的文件名。這個特性和緩存策略密切相關(guān)配好了能顯著提升二三次訪問的加載速度配不好就會把用戶永遠留在舊版本里。1.3 整體鏈路與數(shù)據(jù)流向我最終跑通的鏈路是這樣的用戶瀏覽器先請求ESA邊緣節(jié)點邊緣節(jié)點根據(jù)域名和路徑查找緩存。如果命中直接返回靜態(tài)資源不再往后端跑如果沒有命中ESA回源到ECS上的Nginx從磁盤讀取Unity構(gòu)建產(chǎn)物返回同時按緩存規(guī)則在邊緣節(jié)點留一份副本。這個結(jié)構(gòu)最合理的地方在于分工明確。ECS負責(zé)源站存儲和基礎(chǔ)Web服務(wù)跑Nginx、托管構(gòu)建產(chǎn)物、配置證書、控制響應(yīng)頭ESA負責(zé)所有邊緣側(cè)的能力包括TLS終止、HTTP/2和HTTP/3、靜態(tài)緩存加速、WAF基礎(chǔ)防護、DDoS過濾、限速防刷。源站不需要太高的配置因為大部分流量都被邊緣節(jié)點擋住了CPU和帶寬壓力都不大。三種常見的部署方案對比一下更容易看出選型的理由方案維護成本擴展性安全防護適用場景ECS裸奔直連低但暴露公網(wǎng)差帶寬和防御都在一臺機器無測試環(huán)境、臨時演示OSS CDN低純靜態(tài)托管好穩(wěn)定且便宜CDN自帶的有限防護純靜態(tài)、沒有后端需求的項目ECS ESA中需要維護源站好邊緣層可擴展源站可跑后端WAF、DDoS、Bot防護齊全生產(chǎn)環(huán)境、后續(xù)要加后端API的項目我選ECS ESA不只是為了加速。Unity WebGL項目做完展示后面大概率要上用戶系統(tǒng)、數(shù)據(jù)上報、在線配置這些后端能力這時候ECS的價值就體現(xiàn)出來了。ESA則讓整條訪問鏈路足夠快同時把刷流量、攻擊之類的風(fēng)險擋在邊緣。2. Unity Build配置構(gòu)建產(chǎn)物決定上線體驗2.1 Player Settings里必須動的那幾個參數(shù)很多人在部署環(huán)節(jié)反復(fù)折騰服務(wù)器結(jié)果問題出在構(gòu)建參數(shù)上。構(gòu)建階段定下的參數(shù)直接決定了產(chǎn)物體積、壓縮格式、內(nèi)存模型和服務(wù)器端要配合的HTTP頭。打開Project Settings面板在Player設(shè)置里選擇WebGL平臺先處理這幾個關(guān)鍵項。Compression Format選Brotli還是Gzip這是第一個分水嶺。Brotli壓縮率比Gzip能再小10%到15%對動輒幾百MB的Unity產(chǎn)物來說省下的體積就是實打?qū)嵉募虞d時間。缺點是構(gòu)建耗時變長因為壓縮工作量更大。如果本地構(gòu)建還能忍建議直接上Brotli。我這里實際項目中開啟Brotli之后data文件從520MB壓到286MBwasm從18MB壓到8.4MB效果非常顯著。如果項目用CI機器構(gòu)建、時間卡得緊那Gzip是更折中的選擇至少比不壓縮強太多。Enable Exceptions正式發(fā)布建議選擇None或Explicitly Thrown Exceptions Only。開發(fā)和測試階段可以開Full但發(fā)布版開Full會帶來額外體積和性能損耗沒必要為不會走的錯誤分支買單。Data Caching這個選項如果你有多個場景且希望移動端瀏覽器按關(guān)卡緩存數(shù)據(jù)可以打開。但如果已經(jīng)用了Addressable做資源分包這個選項的優(yōu)先級就沒那么高了反而可能造成本地緩存和內(nèi)存的雙重壓力。WebGL Memory Size是新手最容易亂調(diào)的一項。這個值代表Unity啟動時向瀏覽器申請的內(nèi)存大小單位是MB。項目大不代表這個值就要無限調(diào)大因為瀏覽器能申請的連續(xù)內(nèi)存是有限的設(shè)置過大會導(dǎo)致初始化直接失敗設(shè)置過小又會頻繁觸發(fā)GC甚至內(nèi)存溢出。我一般先按256MB跑然后用Profiler觀察實際峰值再在這個基礎(chǔ)上留出30%余量。別一上來就填個1024瀏覽器不一定買賬。2.2 構(gòu)建產(chǎn)物長什么樣搞清楚構(gòu)建產(chǎn)物的結(jié)構(gòu)部署時心里才踏實。構(gòu)建完成后Build目錄下大概長這樣WebGL/Build/MyGame_0.1.data WebGL/Build/MyGame_0.1.framework.js WebGL/Build/MyGame_0.1.loader.js WebGL/Build/MyGame_0.1.wasm.loader.jsUnity的啟動加載器負責(zé)下載其余資源并初始化運行時。.framework.js引擎和IL2CPP相關(guān)的JavaScript綁定與元數(shù)據(jù)。.wasm編譯好的WebAssembly模塊是引擎執(zhí)行代碼的主體。.data所有場景、紋理、音頻、預(yù)制體等序列化資源數(shù)據(jù)體積最大的通常是它。注意文件名里帶版本號或hash這是Unity的構(gòu)建機制決定的。這個特性對緩存特別友好文件名變了瀏覽器和ESA緩存都會自然失效自動加載新文件文件名沒變就放心大膽地長緩存。部署時千萬不要自己去改這些文件名改了加載器就找不到資源了。2.3 資源加載優(yōu)化上線前的體積自查構(gòu)建產(chǎn)物體積超標(biāo)別急著怪Unity先檢查資源。我給項目做上線自查時有一張常規(guī)清單Texture壓縮格式WebGL平臺優(yōu)先選ASTC或ETC2避免PNG/JPG直傳大圖。帶透明通道的UI圖盡量打圖集。Audio資源WAV和未壓縮的音頻是體積殺手能切成Compressed格式就切背景音樂用Ogg或MP3。初始場景資源首場景同步加載的資產(chǎn)越少首屏越快。大場景模型、高清貼圖能異步加載就異步加載。Addressable分包把核心包和關(guān)卡包分開第一屏只需要核心包后續(xù)內(nèi)容按需拉取。粒子特效WebGL上粒子系統(tǒng)的內(nèi)存釋放不及時經(jīng)常導(dǎo)致內(nèi)存持續(xù)上漲盡量用對象池復(fù)用不要頻繁創(chuàng)建銷毀。這些工作不做后面無論部署得多完美用戶該等還是等。2.4 修改一個合適的WebGL模板Unity默認的WebGL模板只有個轉(zhuǎn)圈圈加載動畫生產(chǎn)環(huán)境根本不夠用。我習(xí)慣在Assets/WebGLTemplates/下建一個自定義模板改出中文進度提示、啟動圖、自定義Logo。新版Unity模板的加載進度回調(diào)在createUnityInstance的配置里大致是這樣一個結(jié)構(gòu)createUnityInstance(canvas, config, onProgress).catch(message { alert(message); }); function onProgress(progress) { var percent Math.round(progress * 100); document.getElementById(progressBar).style.width percent %; document.getElementById(progressText).textContent percent %; }不同Unity版本模板API略有差異關(guān)鍵看模板目錄里Unity自帶的index.html注釋它會把啟動進度回調(diào)的示例寫在里面。改模板時順手把加載期間的背景色、進度條樣式一起做了上線后觀感會專業(yè)很多。3. ECS搭建Nginx源站必須穩(wěn)3.1 買臺什么樣的ECS夠用源站機器不用追求高配因為邊緣層擋掉了大部分請求。我常用的起步配置是2核4G、SSD云盤50G起步、固定帶寬5M或以上。CPU和內(nèi)存不是瓶頸帶寬才是。ESA回源時要拉取幾百MB的資源帶寬太小回源很吃力尤其在緩存剛刷新的階段。操作系統(tǒng)建議選Alibaba Cloud Linux 3.0或者Ubuntu 22.04。Alibaba Cloud Linux對阿里云基礎(chǔ)設(shè)施的兼容性更好安全更新也及時Ubuntu則社區(qū)資料多、排查問題方便。兩個都能用看團隊熟悉哪個。安全組規(guī)則一定要在購買后第一時間配好開放22端口用于SSH開放80和443用于Web訪問其他端口全部關(guān)閉。這一步別偷懶源站暴露多余端口等于給攻擊者留門。3.2 安裝并配置Nginx的關(guān)鍵細節(jié)Nginx安裝很簡單Ubuntu執(zhí)行apt update apt install nginx -yAlibaba Cloud Linux執(zhí)行dnf install nginx -y。真正的坑在配置細節(jié)。先看一個生產(chǎn)可用的站點配置server { listen 80; server_name game.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name game.example.com; root /var/www/webgl; index index.html; ssl_certificate /etc/nginx/cert/game.example.com.pem; ssl_certificate_key /etc/nginx/cert/game.example.com.key; # wasm的MIME類型必須顯式聲明 types { application/wasm wasm; } # 帶hash的構(gòu)建產(chǎn)物長緩存 location ~* \.(wasm|data|framework|js|css|png|jpg|jpeg|webp|svg)$ { add_header Cache-Control public, max-age31536000, immutable; } # 跨域隔離Unity WebGL多線程必需 add_header Cross-Origin-Opener-Policy same-origin; add_header Cross-Origin-Embedder-Policy require-corp; add_header Cross-Origin-Resource-Policy same-origin; # 跨域訪問 add_header Access-Control-Allow-Origin *; location / { try_files $uri $uri/ /index.html; } }第一件事.wasm的Content-Type如果不設(shè)置Nginx默認按application/octet-stream返回瀏覽器會拒絕解析WebAssembly直接白屏。這個我排查過太多次了永遠是第一嫌疑。第二件事multi-threading相關(guān)的三個跨域隔離頭。Unity WebGL開啟多線程后運行時需要SharedArrayBuffer而瀏覽器要求頁面處于cross-origin isolation狀態(tài)。也就是說必須同時返回Cross-Origin-Opener-Policy: same-origin和Cross-Origin-Embedder-Policy: require-corp。如果項目沒開多線程這兩個頭不要加尤其COEP會阻止加載外部域名的資源加了反而添亂。第三件事Unity構(gòu)建時如果啟用了Brotli或Gzip產(chǎn)物文件本身就是壓縮過的服務(wù)器端不要再對Build目錄做二次壓縮。Nginx默認不會壓縮未知MIME類型問題不大但如果你額外配置了gzip_types記得把application/wasm排除在外。3.3 上傳構(gòu)建產(chǎn)物與權(quán)限設(shè)置產(chǎn)物上傳我習(xí)慣用rsync增量同步省時間rsync -avz --progress Build/ root你的ECS公網(wǎng)IP:/var/www/webgl/Build/上傳完成后目錄權(quán)限必須檢查否則Nginx的worker進程讀不到文件。我遇到過索引頁能打開、但Build目錄請求全部403的怪問題最后發(fā)現(xiàn)是構(gòu)建產(chǎn)物目錄權(quán)限是700只有root能讀。執(zhí)行一遍chown -R root:root /var/www/webgl chmod -R 755 /var/www/webgl上傳完先用curl驗證MIME和狀態(tài)碼curl -I https://game.example.com/Build/MyGame_0.1.wasm如果返回200且Content-Type: application/wasm這一步才算過關(guān)。3.4 HTTPS證書免費證書的申請與續(xù)期WebGL項目離不開HTTPS。阿里云的數(shù)字證書管理服務(wù)可以申請免費DV證書也能下載Nginx格式的證書文件。申請下來后把xxx.pem和xxx.key放到/etc/nginx/cert/目錄再在Nginx配置里引用。有一點提醒一下阿里云免費證書的有效期現(xiàn)在是3個月到期需要重新申請并更新配置。這個沒辦法免費的東西總要勤快點。習(xí)慣之后每次上線前檢查證書有效期順手續(xù)期操作成本并不高。另外如果域名和ECS都在國內(nèi)ICP備案是上線前提。沒有備案的域名無論是ECS的80/443端口還是ESA接入都會被云廠商攔截。這一環(huán)要在項目排期里提前留出時間別等構(gòu)建完了才開始走流程。4. ESA配置邊緣加速和安全兜底4.1 ESA和CDN的關(guān)系傳統(tǒng)CDN主要做緩存加速把靜態(tài)資源分發(fā)到邊緣節(jié)點。阿里云ESA把這一層升級成了邊緣安全加速平臺緩存加速仍然是基礎(chǔ)能力但DDoS防護、WAF、Bot管理、四層代理、DNS配置這些安全功能全部整合了進來。對Unity WebGL項目來說ESA最實用的價值在于大文件從邊緣節(jié)點就近返回延遲和下載速度都能改善同時WAF和限速能把刷流量的惡意請求擋在源站前面。WebGL包體積大一旦被惡意刷流量源站帶寬費和ESA流量費都會漲得非常快防刷配置不是可有可無是必須做的。4.2 域名接入與回源配置在ESA控制臺添加站點接入方式有NS接入和CNAME接入兩種。NS接入是讓ESA接管整個域名的DNS解析配置更徹底但影響面也大CNAME接入只需要在現(xiàn)有DNS解析里加一條CNAME記錄改動小、回滾容易。我建議用CNAME接入除非你有特殊需求想把DNS統(tǒng)一管理。回源配置那一欄填ECS的公網(wǎng)IP回源協(xié)議建議選HTTPS回源HOST填你的域名而不是IP。這個HOST頭很關(guān)鍵如果不填或填I(lǐng)PNginx的server_name匹配不上源站會找不到對應(yīng)的站點配置返回502或者默認站點頁面。接入完成后在DNS服務(wù)商處添加一條CNAME記錄指向ESA分配的加速域名。生效時間一般在幾分鐘到一小時可以在控制臺看到狀態(tài)變?yōu)檎!?.3 緩存規(guī)則決定用戶加載速度的關(guān)鍵Unity構(gòu)建產(chǎn)物文件名帶hash這一特性配合ESA緩存規(guī)則能做到既緩存加速又不錯版本。我常用的設(shè)置是資源類型緩存策略index.htmlno-cache或 max-age0*.wasm / *.data / *.framework.js / *.js / *.cssmax-age31536000, immutable圖片字體等max-age31536000, immutable原則很簡單index.html是入口不能緩存因為每次發(fā)版它引用的新文件名要第一時間生效帶hash的靜態(tài)資源可以永久緩存因為文件名變化自然會讓緩存失效。還有一個細節(jié)ESA通常有“緩存規(guī)則”和“源站緩存配置”的聯(lián)動。如果源站Nginx已經(jīng)返回了Cache-Control頭ESA默認策略有時會覆蓋它。你需要在規(guī)則里明確靜態(tài)資源的緩存時間和源站保持一致或者直接用源站響應(yīng)頭。我在實際項目里是兩邊都設(shè)了長緩存然后發(fā)布新版時手動在ESA控制臺執(zhí)行一次“緩存刷新”保證新版本能立即全量生效。4.4 防刷與WAF基礎(chǔ)配置ESA控制臺的安全配置里基礎(chǔ)WAF默認開啟就能攔掉大部分常見攻擊。對Unity WebGL項目我額外做了兩件事。第一是頻率限制。針對整個站點設(shè)置單IP的請求頻率閾值超過就觸發(fā)攔截或加驗證。WebGL包這么大單用戶正常訪問不會產(chǎn)生高頻請求閾值可以設(shè)得相對嚴一些。萬一有人拿腳本循環(huán)刷新下載限速能直接讓他的流量成本飆升同時保護你的賬戶。第二是對敏感路徑單獨配置防護規(guī)則。如果以后后端API掛在同一個域名下比如/api路徑就針對這個路徑單獨開Bot管理、加更嚴格的限速。靜態(tài)資源路徑保持寬松即可太嚴格的規(guī)則反而可能誤傷正常用戶。5. 上線后的常見故障與排查思路5.1 白屏、404、MIME類型先看這三個白屏是Unity WebGL部署最常見的故障排查順序我固定是三步。第一步看Network面板確認index.html是否返回200。如果index.html都沒加載出來問題在DNS、ESA接入或者ECS安全組。第二步看Build目錄下請求是否全部返回200。如果有404大概率是上傳文件不完整或路徑不對檢查上傳目錄結(jié)構(gòu)和Nginx的root是否匹配。第三步看wasm請求的Content-Type。如果響應(yīng)頭里不是application/wasm幾乎可以斷定白屏根源就是MIME類型沒配。Solution就是前文那個types配置加完重載Nginxnginx -s reload5.2 SharedArrayBuffer is not defined如果Unity工程開啟了多線程瀏覽器控制臺會報SharedArrayBuffer is not defined這個錯代表頁面沒有進入跨域隔離狀態(tài)。檢查Nginx響應(yīng)頭里是否同時包含Cross-Origin-Opener-Policy: same-origin和Cross-Origin-Embedder-Policy: require-corp。配置正確后還需要強制刷新瀏覽器因為跨域隔離狀態(tài)變化后舊頁面進程不會自動恢復(fù)要重新打開。加了COEP之后還有個連鎖反應(yīng)頁面加載的外部域資源會被全部攔截。如果你的自定義模板里引用了外部字體、統(tǒng)計腳本或廣告SDK記住要么改成同源文件要么把外部資源的響應(yīng)也加上Cross-Origin-Resource-Policy: cross-origin。不然資源會被Block掉頁面看起來像是壞了一半。5.3 首屏加載慢的排查思路首屏加載慢先分清是“邊緣緩存沒命中”還是“Unity運行時初始化慢”。在瀏覽器Network面板看第一個大資源文件的加載耗時。如果耗時主要花在data和wasm的下載上說明是傳輸層問題。檢查ESA控制臺的命中率如果命中率低可能是剛發(fā)布完沒做緩存預(yù)熱。我的做法是上線后先自己用多個地區(qū)節(jié)點訪問一遍頁面讓邊緣緩存把資源拉起來之后用戶訪問就有命中了。如果下載很快、但瀏覽器要很久才出現(xiàn)Unity畫面問題出在Unity初始化階段。這種情況查壓縮格式是否開啟Brotli/Gzip會顯著影響下載后加載器解壓的耗時再查WebGL Memory Size是否合理內(nèi)存申請過大時瀏覽器會自動回收再申請導(dǎo)致啟動遲鈍。還有一個細節(jié)源站Nginx如果配置了gzip而Unity產(chǎn)物又已經(jīng)壓縮過兩邊重復(fù)壓縮不僅浪費CPU加載器解壓時還可能出錯。檢查Nginx的gzip_types確保沒有針對Unity產(chǎn)物開啟動態(tài)壓縮。5.4 內(nèi)存溢出和瀏覽器崩潰Unity WebGL跑在瀏覽器里內(nèi)存空間是平鋪的而且受瀏覽器限制比較死。項目里最典型的崩潰場景打開頁面玩了一會兒標(biāo)簽頁直接黑屏然后提示“頁面無響應(yīng)”。先看Player Settings里的WebGL Memory Size是不是偏小用Profiler抓運行時內(nèi)存峰值留足余量。再看場景和資源釋放邏輯。WebGL上最容易內(nèi)存泄漏的坑之一就是粒子特效和動態(tài)加載的Texture用完之后引用沒有清干凈內(nèi)存只漲不跌。這類問題在編輯器里跑PC平臺不一定暴露得出來但WebGL的苛刻內(nèi)存環(huán)境下隱藏不住。做對象池、用完后主動銷毀、清理AssetBundle這一步逃不掉。5.5 一組實測數(shù)據(jù)參考用一個實際項目的數(shù)據(jù)做參照。原始構(gòu)建產(chǎn)物data約520MBwasm約18MBframework.js約1.2MB。開啟Brotli后data壓到286MBwasm壓到8.4MB整體下載體積減少約45%。首次部署完成、邊緣緩存尚未命中時同城用戶首屏加載耗時約35秒到47秒其中絕大部分是data下載和Unity初始化。緩存預(yù)熱后二次訪問降到12到18秒瀏覽器本地命中緩存后更快。ESA緩存命中率穩(wěn)定在92%以上源站的回源流量壓力很小ECS的CPU占用常年不到10%。這套數(shù)據(jù)說明把緩存策略和壓縮格式做好體感提升遠比換更高配的服務(wù)器明顯。我個人在實際操作中最深的一個體會是Unity WebGL上云這件事真正的功夫不在云上而在構(gòu)建參數(shù)和緩存策略的提前規(guī)劃。MIME類型、跨域隔離頭、壓縮格式、緩存TTL這四樣?xùn)|西如果你在第一天就配好上線時根本不會遇到那些折騰人的白屏和加載失敗。等真的遇到問題再回頭補每一步都要重新排隊心態(tài)也是最差的。后面無論項目怎么迭代發(fā)布流程基本上就是三步本地構(gòu)建、上傳Build目錄、刷新ESA緩存。把這套流程固定下來Unity WebGL發(fā)布到阿里云就不再是玄學(xué)。