.NET性能優化-是時候換個序列化協議了

計算機單機性能一直受到摩爾定律的約束,隨著移動互聯網的興趣,單機性能不足的瓶頸越來越明顯,制約著整個行業的發展。不過我們雖然不能無止境的縱向擴容系統,但是我們可以分布式、橫向的擴容系統,這聽起來非常的美好,不過也帶來了今天要說明的問題,分布式的節點越多,通信產生的成本就越大

  • 網絡傳輸帶寬變得越來越緊缺,我們服務器的標配上了 10Gbps 的網卡

  • HTTPx.x 時代 TCP/IP 協議通訊低效,我們即將用上 QUIC HTTP 3.0

  • 同機器走 Socket 協議棧太慢,我們用起了 eBPF

  • ....

現在我們的應用程序花在網絡通訊上的時間太多了,其中花在序列化上的時間也非常的多。我們和大家一樣,在內部微服務通訊序列化協議中,絕大的部分都是用 JSON。JSON 的好處很多,首先就是它對人非常友好,我們能直接讀懂它的含義,但是它也有著致命的缺點,那就是它序列化太慢、序列化以后的字符串太大了。

之前筆者做一個項目時,就遇到了一個選型的問題,我們有數億行數據需要緩存到 Redis 中,每行數據有數百個字段,如果用 Json 序列化存儲的話它的內存消耗是數 TB級別的(部署個集群再做個主從、多中心 需要成倍的內存、太貴了,用不起)。于是我們就在找有沒有除了 JSON 其它更好的序列化方式?

看看都有哪些

目前市面上序列化協議有很多比如 XML、JSON、Thrift、Kryo 等等,我們選取了在.NET 平臺上比較常用的序列化協議來做比較:

  • JSON:JSON 是一種輕量級的數據交換格式。采用完全獨立于編程語言的文本格式來存儲和表示數據。簡潔和清晰的層次結構使得 JSON 成為理想的數據交換語言。

  • Protobuf:Protocol Buffers 是一種語言無關、平臺無關、可擴展的序列化結構數據的方法,它可用于(數據)通信協議、數據存儲等,它類似 XML,但比它更小、更快、更簡單。

  • MessagePack:是一種高效的二進制序列化格式。它可以讓你像 JSON 一樣在多種語言之間交換數據。但它更快、更小。小的整數被編碼成一個字節,典型的短字符串除了字符串本身之外,只需要一個額外的字節。

  • MemoryPack:是 Yoshifumi Kawai 大佬專為 C#設計的一個高效的二進制序列化格式,它有著.NET 平臺很多新的特性,并且它是 Code First 開箱即用,非常簡單;同時它還有著非常好的性能。

我們選擇的都是.NET 平臺上比較常用的,特別是后面的三種都宣稱自己是非常小,非常快的,那么我們就來看看到底是誰最快,誰序列化后的結果最小。

準備工作

我們準備了一個 DemoClass 類,里面簡單的設置了幾個不同類型的屬性,然后依賴了一個子類數組。暫時忽略上面的一些頭標記。

[MemoryPackable]
[MessagePackObject]
[ProtoContract]
public?partial?class?DemoClass
{[Key(0)]?[ProtoMember(1)]?public?int?P1?{?get;?set;?}[Key(1)]?[ProtoMember(2)]?public?bool?P2?{?get;?set;?}[Key(2)]?[ProtoMember(3)]?public?string?P3?{?get;?set;?}?=?null!;[Key(3)]?[ProtoMember(4)]?public?double?P4?{?get;?set;?}[Key(4)]?[ProtoMember(5)]?public?long?P5?{?get;?set;?}[Key(5)]?[ProtoMember(6)]?public?DemoSubClass[]?Subs?{?get;?set;?}?=?null!;
}[MemoryPackable]
[MessagePackObject]
[ProtoContract]
public?partial?class?DemoSubClass
{[Key(0)]?[ProtoMember(1)]?public?int?P1?{?get;?set;?}[Key(1)]?[ProtoMember(2)]?public?bool?P2?{?get;?set;?}[Key(2)]?[ProtoMember(3)]?public?string?P3?{?get;?set;?}?=?null!;[Key(3)]?[ProtoMember(4)]?public?double?P4?{?get;?set;?}[Key(4)]?[ProtoMember(5)]?public?long?P5?{?get;?set;?}
}

System.Text.Json

選用它的原因很簡單,這應該是.NET 目前最快的 JSON 序列化框架之一了,它的使用非常簡單,已經內置在.NET BCL 中,只需要引用System.Text.Json命名空間,訪問它的靜態方法即可完成序列化和反序列化。

using?System.Text.Json;var?obj?=?....;//?Serialize
var?json?=?JsonSerializer.Serialize(obj);//?Deserialize
var?newObj?=?JsonSerializer.Deserialize<T>(json)

Google Protobuf

.NET 上最常用的一個 Protobuf 序列化框架,它其實是一個工具包,通過工具包+*.proto文件可以生成 GRPC Service 或者對應實體的序列化代碼,不過它使用起來有點麻煩。

使用它我們需要兩個 Nuget 包,如下所示:

<!--Google.Protobuf?序列化和反序列化幫助類-->
<PackageReference?Include="Google.Protobuf"?Version="3.21.9"?/><!--Grpc.Tools?用于生成protobuf的序列化反序列化類?和?GRPC服務-->
<PackageReference?Include="Grpc.Tools"?Version="2.50.0"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime;?build;?native;?contentfiles;?analyzers;?buildtransitive</IncludeAssets>
</PackageReference>

由于它不能直接使用 C#對象,所以我們還需要創建一個*.proto文件,布局和上面的 C#類一致,加入了一個DemoClassArrayProto方便后面測試:

syntax="proto3";
option csharp_namespace="DemoClassProto";
package DemoClassProto;message DemoClassArrayProto
{repeated DemoClassProto DemoClass = 1;
}message DemoClassProto
{int32 P1=1;bool P2=2;string P3=3;double P4=4;int64 P5=5;repeated DemoSubClassProto Subs=6;
}message DemoSubClassProto
{int32 P1=1;bool P2=2;string P3=3;double P4=4;int64 P5=5;
}

做完這一些后,還需要在項目文件中加入如下的配置,讓Grpc.Tools在編譯時生成對應的 C#類:

<ItemGroup><Protobuf?Include="*.proto"?GrpcServices="Server"?/>
</ItemGroup>

然后 Build 當前項目的話就會在obj目錄生成 C#類:91543da362e6497ecbbab05cf8d3d8b6.png476a4582d6feabe289d3483cc0398706.png

最后我們可以用下面的方法來實現序列化和反序列化,泛型類型T是需要繼承IMessage<T>*.proto生成的實體(用起來還是挺麻煩的):

using?Google.Protobuf;//?Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public?static?byte[]?GoogleProtobufSerialize<T>(T?origin)?where?T?:?IMessage<T>
{return?origin.ToByteArray();
}//?Deserialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public?DemoClassArrayProto?GoogleProtobufDeserialize(byte[]?bytes)
{return?DemoClassArrayProto.Parser.ParseFrom(bytes);
}

Protobuf.Net

那么在.NET 平臺 protobuf 有沒有更簡單的使用方式呢?答案當然是有的,我們只需要依賴下面的 Nuget 包:

<PackageReference?Include="protobuf-net"?Version="3.1.22"?/>

然后給我們需要進行序列化的 C#類打上ProtoContract特性,另外將所需要序列化的屬性打上ProtoMember特性,如下所示:

[ProtoContract]
public?class?DemoClass
{[ProtoMember(1)]?public?int?P1?{?get;?set;?}[ProtoMember(2)]?public?bool?P2?{?get;?set;?}[ProtoMember(3)]?public?string?P3?{?get;?set;?}?=?null!;[ProtoMember(4)]?public?double?P4?{?get;?set;?}[ProtoMember(5)]?public?long?P5?{?get;?set;?}
}

然后就可以直接使用框架提供的靜態類進行序列化和反序列化,遺憾的是它沒有提供直接返回byte[]的方法,不得不使用一個MemoryStrem

using?ProtoBuf;//?Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public?static?void?ProtoBufDotNet<T>(T?origin,?Stream?stream)
{Serializer.Serialize(stream,?origin);
}//?Deserialize
public?T?ProtobufDotNet(byte[]?bytes)
{using?var?stream?=?new?MemoryStream(bytes);return?Serializer.Deserialize<T>(stream);
}

MessagePack

這里我們使用的是 Yoshifumi Kawai 實現的MessagePack-CSharp,同樣也是引入一個 Nuget 包:

<PackageReference?Include="MessagePack"?Version="2.4.35"?/>

然后在類上只需要打一個MessagePackObject的特性,然后在需要序列化的屬性打上Key特性:

[MessagePackObject]
public?partial?class?DemoClass
{[Key(0)]?public?int?P1?{?get;?set;?}[Key(1)]?public?bool?P2?{?get;?set;?}[Key(2)]?public?string?P3?{?get;?set;?}?=?null!;[Key(3)]?public?double?P4?{?get;?set;?}[Key(4)]?public?long?P5?{?get;?set;?}
}

使用起來也非常簡單,直接調用MessagePack提供的靜態類即可:

using?MessagePack;//?Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public?static?byte[]?MessagePack<T>(T?origin)
{return?global::MessagePack.MessagePackSerializer.Serialize(origin);
}//?Deserialize
public?T?MessagePack<T>(byte[]?bytes)
{return?global::MessagePack.MessagePackSerializer.Deserialize<T>(bytes);
}

另外它提供了 Lz4 算法的壓縮程序,我們只需要配置 Option,即可使用 Lz4 壓縮,壓縮有兩種方式,Lz4BlockLz4BlockArray,我們試試:

public?static?readonly?MessagePackSerializerOptions?MpLz4BOptions?=???MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block);//?Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public?static?byte[]?MessagePackLz4Block<T>(T?origin)
{return?global::MessagePack.MessagePackSerializer.Serialize(origin,?MpLz4BOptions);
}//?Deserialize
public?T?MessagePackLz4Block<T>(byte[]?bytes)
{return?global::MessagePack.MessagePackSerializer.Deserialize<T>(bytes,?MpLz4BOptions);
}

MemoryPack

這里也是 Yoshifumi Kawai 大佬實現的MemoryPack,同樣也是引入一個 Nuget 包,不過需要注意的是,目前需要安裝 VS 2022 17.3 以上版本和.NET7 SDK,因為MemoryPack代碼生成依賴了它:

<PackageReference?Include="MemoryPack"?Version="1.4.4"?/>

使用起來應該是這幾個二進制序列化協議最簡單的了,只需要給對應的類加上partial關鍵字,另外打上MemoryPackable特性即可:

[MemoryPackable]
public?partial?class?DemoClass
{public?int?P1?{?get;?set;?}public?bool?P2?{?get;?set;?}public?string?P3?{?get;?set;?}?=?null!;public?double?P4?{?get;?set;?}public?long?P5?{?get;?set;?}
}

序列化和反序列化也是調用靜態方法:

//?Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public?static?byte[]?MemoryPack<T>(T?origin)
{return?global::MemoryPack.MemoryPackSerializer.Serialize(origin);
}//?Deserialize
public?T?MemoryPack<T>(byte[]?bytes)
{return?global::MemoryPack.MemoryPackSerializer.Deserialize<T>(bytes)!;
}

它原生支持 Brotli 壓縮算法,使用如下所示:

//?Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public?static?byte[]?MemoryPackBrotli<T>(T?origin)
{using?var?compressor?=?new?BrotliCompressor();global::MemoryPack.MemoryPackSerializer.Serialize(compressor,?origin);return?compressor.ToArray();
}//?Deserialize
public?T?MemoryPackBrotli<T>(byte[]?bytes)
{using?var?decompressor?=?new?BrotliDecompressor();var?decompressedBuffer?=?decompressor.Decompress(bytes);return?MemoryPackSerializer.Deserialize<T>(decompressedBuffer)!;
}

跑個分吧

我使用BenchmarkDotNet構建了一個 10 萬個對象序列化和反序列化的測試,源碼在末尾的 Github 鏈接可見,比較了序列化、反序列化的性能,還有序列化以后占用的空間大小。

public?static?class?TestData
{//public?static?readonly?DemoClass[]?Origin?=?Enumerable.Range(0,?10000).Select(i?=>{return?new?DemoClass{P1?=?i,P2?=?i?%?2?==?0,P3?=?$"Hello?World?{i}",P4?=?i,P5?=?i,Subs?=?new?DemoSubClass[]{new()?{P1?=?i,?P2?=?i?%?2?==?0,?P3?=?$"Hello?World?{i}",?P4?=?i,?P5?=?i,},new()?{P1?=?i,?P2?=?i?%?2?==?0,?P3?=?$"Hello?World?{i}",?P4?=?i,?P5?=?i,},new()?{P1?=?i,?P2?=?i?%?2?==?0,?P3?=?$"Hello?World?{i}",?P4?=?i,?P5?=?i,},new()?{P1?=?i,?P2?=?i?%?2?==?0,?P3?=?$"Hello?World?{i}",?P4?=?i,?P5?=?i,},}};}).ToArray();public?static?readonly?DemoClassProto.DemoClassArrayProto?OriginProto;static?TestData(){OriginProto?=?new?DemoClassArrayProto();for?(int?i?=?0;?i?<?Origin.Length;?i++){OriginProto.DemoClass.Add(DemoClassProto.DemoClassProto.Parser.ParseJson(JsonSerializer.Serialize(Origin[i])));}}
}

序列化

序列化的 Bemchmark 的結果如下所示:72797be2164dc6d80dd17e8c798992c6.png

從序列化速度來看MemoryPack遙遙領先,比 JSON 要快 88%,甚至比 Protobuf 快 15%。f1c2b71d76c0cc46059a5c99556cea41.png

從序列化占用的內存來看,MemoryPackBrotli是王者,它比 JSON 占用少 98%,甚至比Protobuf占用少 25%。其中ProtoBufDotNet內存占用大主要還是吃了沒有byte[]返回方法的虧,只能先創建一個MemoryStream

19f665c71b2f0b979e37d2413d7d15ee.png

序列化結果大小

這里我們可以看到MemoryPackBrotli贏麻了,比不壓縮的MemoryPackProtobuf有著 10 多倍的差異。1e88bb105e0c8a79d4ffff8bc299cc23.png

反序列化

反序列化的 Benchmark 結果如下所示,反序列化整體開銷是比序列化大的,畢竟需要創建大量的對象:289168523f073e44f91c5d90a05c923e.png

從反序列化的速度來看,不出意外MemoryPack還是遙遙領先,比 JSON 快 80%,比Protobuf快 14%。79860de5b1b90a20648e51a888735c21.png

從內存占用來看ProtobufDotNet是最小的,這個結果聽讓人意外的,其余的都表現的差不多:4a4669ab3c60b29e6f84f84a15dd9b8b.png

總結

總的相關數據如下表所示,原始數據可以在文末的 Github 項目地址獲取:04c4485e38f4cf6a7d86986de66942be.png

從圖表來看,如果要兼顧序列化后大小和性能的話我們應該要選擇MemoryPackBrotli,它序列化以后的結果最小,而且兼顧了性能:7881a8b608fc2bb7d061903d278b9b21.png

不過由于MemoryPack目前需要.NET7 版本,所以現階段最穩妥的選擇還是使用MessagePack+Lz4壓縮算法,它有著不俗的性能表現和突出的序列化大小。

回到文首的技術選型問題,筆者那個項目最終選用的是Google Protobuf這個序列化協議和框架,因為當時考慮到需要和其它語言交互,然后也需要有較小空間占用,目前看已經占用了111GB的 Redis 空間占用。

9337bdc2d08f2cb7437b32bd5e10490a.png

如果后續進一步增大,可以換成MessagePack+Lz4方式,應該還能節省95GB的左右空間。那可都是白花花的銀子。

當然其它協議也是可以進一步通過GzipLz4Brotli算法進行壓縮,不過鑒于時間和篇幅關系,沒有進一步做測試,有興趣的同學可以試試。

附錄

代碼鏈接: https://github.com/InCerryGit/WhoIsFastest-Serialization

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/281287.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/281287.shtml
英文地址,請注明出處:http://en.pswp.cn/news/281287.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Kubernetes-基于Helm安裝部署高可用的Redis

1、Redis簡介 Redis是一個開放源代碼&#xff08;BSD許可證&#xff09;的代理&#xff0c;其在內存中存儲數據&#xff0c;可以代理數據庫、緩存和消息。它支持字符串、散列、列表、集合和位圖等數據結構。Redis 是一個高性能的key-value數據庫&#xff0c; 它在很大程度改進了…

Vue 深度監聽和初始綁定

vue的監聽屬性普通方式無法監聽對象內部屬性的改變&#xff0c;并且初始化時不會監聽數據對象。 vue為監聽屬性提供了一種對象方法 watch: {option.size: {// handler為默認執行的方法handler (newValue, oldValue) {this.size newValue}&#xff0c;// 立即執行handler方法…

markdown流程圖畫法小結

markdown流程圖畫法小結markdown畫圖流程圖 最簡單的流程圖為例mermaid! graph TD A --> B //在沒有(),[].{}等括號的情況之下&#xff0c;圖標默認名字就是字母 A --> C C --> D B --> D 給圖標添加名字&#xff0c;改變只有矩陣圖形&#xff0c;在箭頭上添加文字…

hihocoder 1689 - 推斷大小關系(圖論+二分)

題目鏈接 https://vjudge.net/problem/HihoCoder-1689有N個整數A1, A2, ... AN&#xff0c;現在我們知道M條關于這N個整數的信息。每條信息是&#xff1a;Ai < Aj 或者 Ai Aj 小Hi希望你能從第一條信息開始依次逐條處理這些信息。一旦能推斷出A1和AN的大小關系就立即停止。…

32歲京東畢業程序員,走投無路當了外企外包,閑得心里發慌,到點下班渾身不自在!...

??當一位京東程序員進入外企當外包會怎么樣&#xff1f;順利躺平&#xff0c;實現wlb&#xff08;工作生活平衡&#xff09;嗎&#xff1f;未必&#xff0c;因為人是一種很奇怪的動物。這位網友說&#xff1a;32歲京東畢業程序員&#xff0c;找了幾個月工作一直沒有合適的&am…

SpringBoot+Shiro學習(四):Realm授權

上一節我們講了自定義Realm中的認證&#xff08;doGetAuthenticationInfo&#xff09;&#xff0c;這節我們繼續講另一個方法doGetAuthorizationInfo授權 授權流程 流程如下&#xff1a; 首先調用Subject.isPermitted/hasRole接口&#xff0c;其會委托給SecurityManager&#x…

Git放棄文件修改

已提交 # 撤銷提交&#xff0c;保留修改內容 git reset <commit_id># 撤銷提交&#xff0c;不保留修改內容 git reset --hard <commit_id>已暫存文件 # 撤銷單個文件暫存 git reset HEAD <filename># 撤銷所有文件/文件夾暫存 git reset HEAD .已跟蹤未暫存…

[LeetCode][Java] Unique Paths II

題目&#xff1a; Follow up for "Unique Paths": Now consider if some obstacles are added to the grids. How many unique paths would there be? An obstacle and empty space is marked as 1 and 0 respectively in the grid. For example, There is one obst…

lua windows下編譯

從Lua5.1開始官方給出的文件只有源代碼和makefile文件了&#xff0c;官網給出的bulid方式也是在linux平臺&#xff0c;如果只是想找個庫使用下可以到這里來下載&#xff1a;http://joedf.ahkscript.org/LuaBuilds/ &#xff0c;如果需要自定修改庫配置的話&#xff0c;就需要自…

XAML 創建瀏覽器應用程序

XAML 創建瀏覽器應用程序XAML 創建瀏覽器應用程序作者&#xff1a;WPFDevelopersOrg - 驚鏵原文鏈接&#xff1a;https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/app-development/wpf-xaml-browser-applications-overview?viewnetframeworkdesktop-4.8框架使用.NET40&…

Git 合并分支選項 --squash 合并提交歷史

git merge --squash <branchname>--squash選項的含義是&#xff1a;本地文件內容與不使用該選項的合并結果相同&#xff0c;但是不提交、不移動HEAD&#xff0c;因此需要一條額外的commit命令。其效果相當于將another分支上的多個commit合并成一個&#xff0c;放在當前分…

Kubernetes共享使用Ceph存儲

目錄 簡要概述環境測試結果驗證簡要概述 Kubernetes pod 結合Ceph rbd塊設備的使用&#xff0c;讓Docker 數據存儲在Ceph,重啟Docker或k8s RC重新 調 度pod 不會引起數據來回遷移。 工作原理無非就是拿到ceph集群的key作為認證&#xff0c;遠程rbdmap映射掛載使用。那么就要啟用…

在Activity不可見時暫停WebView的語音播放,可見時繼續播放之前的語音

private AudioManager mAudioManager;private AudioManager.OnAudioFocusChangeListener mFocusChangeListener; Override protected void onPause() {   super.onPause();   stopPlayVoice(); } Override protected void onResume() {   super.onResume();   startPla…

MFC界面庫BCGControlBar v25.3新版亮點:Dialogs和Forms

2019獨角獸企業重金招聘Python工程師標準>>> 親愛的BCGSoft用戶&#xff0c;我們非常高興地宣布BCGControlBar Professional for MFC和BCGSuite for MFC v25.3正式發布&#xff01;新版本添加了對Visual Studio 2017的支持、增強對Windows 10的支持等。接下來幾篇文…

基于 .NET 7 的 QUIC 實現 Echo 服務

前言隨著今年6月份的 HTTP/3 協議的正式發布&#xff0c;它背后的網絡傳輸協議 QUIC&#xff0c;憑借其高效的傳輸效率和多路并發的能力&#xff0c;也大概率會取代我們熟悉的使用了幾十年的 TCP&#xff0c;成為互聯網的下一代標準傳輸協議。在去年 .NET 6 發布的時候&#xf…

php.ini-development和php.ini-production的區別

使用zip版MySQL安裝時&#xff0c;需要將php.ini-development或php.ini-production改成php.ini&#xff0c;那么php.ini-development和php.ini-production的區別在哪兒呢&#xff0c;通俗的說法時&#xff0c;development是開發環境&#xff0c;production用于生產環境&#xf…

Server.MapPath()的用法

http://blog.csdn.net/qiuhaifeng_csu/article/details/19416407 Server.MapPath(string path)作用是返回與Web服務器上的指定虛擬路徑相對應的物理文件路徑。其參數path為Web 服務器的虛擬路徑&#xff0c;返回結果是與path相對應的物理文件路徑。但有時參數并非為虛擬路徑&a…

為什么阿里巴巴禁止把SimpleDateFormat定義為static類型的?

在日常開發中&#xff0c;我們經常會用到時間&#xff0c;我們有很多辦法在Java代碼中獲取時間。但是不同的方法獲取到的時間的格式都不盡相同&#xff0c;這時候就需要一種格式化工具&#xff0c;把時間顯示成我們需要的格式。 最常用的方法就是使用SimpleDateFormat類。這是一…

關于信息收集和加工的思考

隨著互聯網的發展&#xff0c;獲取信息的手段越來越多&#xff0c;我們對手機的依賴程度超乎想象&#xff0c;每天忙碌著&#xff0c;大腦接收著豐富的信息&#xff0c;感覺每天都學習到了很多的知識。但我們對學習經常會有些誤區&#xff1a;1、書買了擺在書架上&#xff0c;看…

[譯]關于NODE_ENV,哪些你應該了解

原文 Node.js開發者經常檢測環境變量NODE_ENV&#xff0c;但你是否知道設置這個值同時也具有著某些別的意義&#xff1f;閱讀本文你將發現這些。NODE_ENV是一個在Express框架中極其常用的環境變量。用其確定應用的運行環境&#xff08;諸如開發&#xff0c;staging&#xff0c;…