Free5GC源码研究(7) - NSSF研究
本文研究 Network Slice Selection Function(NSSF)主要实现的功能
NSSF的概念
NSSF,也就是网络切片选择功能,负责根据用户请求和网络的配置来选择最合适的网络切片实例(Network Slice Instance, NSI)来服务用户设备。
所谓网络切片,是5G核心网的重要概念,允许运营商在同一物理基础设施上创建多个虚拟网络,每个虚拟网络都可以有不同的服务等级和特性,以满足不同的业务需求。用TS23.501的原话来说,一个网络切片就是
"logical network that provides specific network capabilities and network characteristics"
"a set of Network Function instances and the required resources (e.g. compute, storage and networking resources) which form a deployed Network Slice".
这有点像在同一物理机器上创建多个虚拟机,每个虚拟机可以有不同的操作系统和分配给它的算力和存储空间,以满足不同业务的需求,比如
- 一台虚拟机可以有较少的算力的存储、一般的网络带宽,但是装的Windows系统,可以服务普通用户的日常办公操作
- 一台虚拟机也只有较少的算力的存储、较大的网络带宽,但是装的Ubuntu系统,可以服务程序员的日常软件开发
- 一台虚拟机装Scientific Linux,配置较多的算力和存储和较小的网络带宽,用来搞大规模科学计算
同理,在同一个5G物理网络上也可以部署几个虚拟网络——也就是网络切片——配置不同的计算和网络资源(当然也还有不同的计费策略),来支持不同的需求,比如说一个切片专门用于大带宽(eMBB),一个切片用于海量接入(MIOT),一个用于高可靠低延迟(URLLC)。TS23.501@v18.50定义了更多标准切片类型,各大运营商也可以按需定义自己的切片:
NSSF的功能划分为两个NFS:Nnssf_NSSAIAvailability
和Nnssf_NSSelection
,主要是对网路切片信息增删改查,更准确来说是对NSSAI(Network Slice Selection Assistance Information)增删改查。NSSAI是网络切片的标识符,当用户设备试图接入网络时,负责接入管理的AMF会向NSSF咨询合适的网络切片,此时NSSF返回给AMF的也是NAASI。NSSF的这两个服务在free5gc都实现了(毕竟也不复杂)。
要注意的是NSSF,顾名思义,是网络切片选择功能,也只是网路切片选择功能。网络切片的创建、管理、删除等不归NSSF管。知道目前为止,TS23.501@v18.50也没有详细说明哪一个NF应当负责切片的管理,而是在另一组标准TS28.530中说明。
NSSF的实现
根前面研究过的AUSF、NRF、UDM、UDR都不同,NSSF的context
看起来平平无奇,只有身为一个NF该有的基本信息。
// https://github.com/free5gc/nssf/blob/v1.2.3/internal/context/context.go#54 type NSSFContext struct { NfId string Name string UriScheme models.UriScheme RegisterIPv4 string // HttpIpv6Address string BindingIPv4 string SBIPort int NfService map[models.ServiceName]models.NfService NrfUri string NrfCertPem string SupportedPlmnList []models.PlmnId OAuth2Required bool }
但我们知道NSSF是要管理网络切片信息的,那这些信息保存在哪里?前面研究过的NF要么把数据保存在自己的NFContext
,要么保存在数据库中。但free5gc/nssf有点奇怪,主要把信息保存在Configuaration
中。
// https://github.com/free5gc/nssf/blob/v1.2.3/pkg/factory/config.go type Config struct { Info *Info `yaml:"info" valid:"required"` Configuration *Configuration `yaml:"configuration" valid:"required"` Subscriptions []Subscription `yaml:"subscriptions,omitempty"` Logger *Logger `yaml:"logger" valid:"required"` sync.RWMutex } type Configuration struct { NssfName string `yaml:"nssfName,omitempty"` Sbi *Sbi `yaml:"sbi"` ServiceNameList []models.ServiceName `yaml:"serviceNameList"` NrfUri string `yaml:"nrfUri"` NrfCertPem string `yaml:"nrfCertPem,omitempty" valid:"optional"` SupportedPlmnList []models.PlmnId `yaml:"supportedPlmnList,omitempty"` SupportedNssaiInPlmnList []SupportedNssaiInPlmn `yaml:"supportedNssaiInPlmnList"` NsiList []NsiConfig `yaml:"nsiList,omitempty"` AmfSetList []AmfSetConfig `yaml:"amfSetList"` AmfList []AmfConfig `yaml:"amfList"` TaList []TaConfig `yaml:"taList"` MappingListFromPlmn []MappingFromPlmnConfig `yaml:"mappingListFromPlmn"` } type Subscription struct { SubscriptionId string `yaml:"subscriptionId"` SubscriptionData *models.NssfEventSubscriptionCreateData `yaml:"subscriptionData"` }
我们可以看到Configuration
中有好几个List,这些就是free5gc/nssf保存数据的主要地方。在下文我们会看到各种函数对Config.Subscriptions
做增删改操作,也会看到对Configuration
中的各种List做增删改操作。我个人认为理应是保存在NSSFContext
中的。保存在Config
中逻辑上不太合适,而唯一的好处是Config
存在于nssf/pkg/
目录下,可以被导入到其他模块里被直接调用。然而目前我没看到出NSSF以外其他NF有直接调用其Config
。
这也侧面反映出了配置信息对于NSSF的重要性。无论怎么类型的切片,其具体组成成分和结构都和一个完整的网络类似,都有接入网(AN),核心网(CN),和传输网(TN):
每个切片都有自己的覆盖范围(Tracking Area),会向外界暴露自己的AMF的接口,让用户设备得以接入;也会向内部所有NF暴露自己的NRF接口,让新的NF能完成注册。这些在切片创建时就要配置好,当然也可以随网络演变而动态更新。这也导致了NSSF的默认配置文件是所有NF里最长的。除了基本的NF配置信息以外,还写明了该NSSF目前管理这哪些切片实例nsiList
,每个实例的ID信息和它们的NRF信息和AMF信息,每个实例可以覆盖哪些范围supportedSnssaiList
等。
Nnssf_NSSAIAvailability
该服务主要负责管理NAASI的可用性和订阅事件。这和前文的NF_Management
对于NF信息的管理有异曲同工之妙。就具体来说,Nnssf_NSSAIAvailability
要做的事情就是应外部请求对NssaiAvailability
数据“增删改”(后面的Nnssf_NSSelection
负责剩下的“查”),以及帮其他NF订阅和网络切片相关的事件,当事件发生时通知感兴趣的NF。
NssaiAvailability
首先解释一下什么是NssaiAvailability
。前文已经说过,“NSSAI是网络切片的标识符”。而一个NSSAI由由几个S-NSSAI(Single NSSA)组成,每个S-NSSAI又由两个属性组成
- SST (Slice/Service Type): 切片/服务类型
- SD (Slice Differentiator): 切片区分符,用于区分相同SST的不同切片实例
// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_snssai.go type Snssai struct { Sst int32 `json:"sst" yaml:"sst" bson:"sst" mapstructure:"Sst"` Sd string `json:"sd,omitempty" yaml:"sd" bson:"sd" mapstructure:"Sd"` } // https://github.com/free5gc/openapi/blob/v1.0.8/models/model_nssai.go type Nssai struct { SupportedFeatures string `json:"supportedFeatures,omitempty" yaml:"supportedFeatures" bson:"supportedFeatures" mapstructure:"SupportedFeatures"` DefaultSingleNssais []Snssai `json:"defaultSingleNssais" yaml:"defaultSingleNssais" bson:"defaultSingleNssais" mapstructure:"DefaultSingleNssais"` SingleNssais []Snssai `json:"singleNssais,omitempty" yaml:"singleNssais" bson:"singleNssais" mapstructure:"SingleNssais"` }
因为Nssai相当于网络切片的标识符,那么Nssai的可用性就相当于网络切片的可用性。Nnssf_NSSAIAvailability
对Nssai的可用性进行管理就是动态更新网络切片的可用性信息,更准确地说,是网络中各个AMF所支持的网络切片信息。所以在下面地代码会看到,NssaiAvailabilityNfInstanceUpdate
函数做的事情就是根据给定的nfId
(AMF的id)以及nssaiAvailabilityInfo
(通常来自AMF),在NSSF的NssfConfig.Configuration.AmfList
找到相应的AMF,让后更新其SupportedNssaiAvailabilityData
。
https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_store.go#L161 func (p *Processor) NssaiAvailabilityNfInstanceUpdate( c *gin.Context, nssaiAvailabilityInfo models.NssaiAvailabilityInfo, nfId string, ) { var ( response *models.AuthorizedNssaiAvailabilityInfo = &models.AuthorizedNssaiAvailabilityInfo{} problemDetails *models.ProblemDetails ) hitAmf := false // Find AMF configuration of given NfId // If found, then update the SupportedNssaiAvailabilityData factory.NssfConfig.Lock() for i, amfConfig := range factory.NssfConfig.Configuration.AmfList { if amfConfig.NfId == nfId { factory.NssfConfig.Configuration.AmfList[i].SupportedNssaiAvailabilityData = nssaiAvailabilityInfo.SupportedNssaiAvailabilityData hitAmf = true break } } factory.NssfConfig.Unlock() // If no AMF record is found, create a new one if !hitAmf { var amfConfig factory.AmfConfig amfConfig.NfId = nfId amfConfig.SupportedNssaiAvailabilityData = nssaiAvailabilityInfo.SupportedNssaiAvailabilityData factory.NssfConfig.Lock() factory.NssfConfig.Configuration.AmfList = append(factory.NssfConfig.Configuration.AmfList, amfConfig) factory.NssfConfig.Unlock() } // Return authorized NSSAI availability information of updated TAI only for _, s := range nssaiAvailabilityInfo.SupportedNssaiAvailabilityData { authorizedNssaiAvailabilityData, err := util.AuthorizeOfAmfTaFromConfig(nfId, *s.Tai) response.AuthorizedNssaiAvailabilityData = append(response.AuthorizedNssaiAvailabilityData, authorizedNssaiAvailabilityData) } c.JSON(http.StatusOK, response) }
点击查看NssaiAvailabilityNfInstance的Patch和Delete方法
// https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_store.go#L47 func (p *Processor) NssaiAvailabilityNfInstancePatch( c *gin.Context, nssaiAvailabilityUpdateInfo plugin.PatchDocument, nfId string, ) { var ( response *models.AuthorizedNssaiAvailabilityInfo = &models.AuthorizedNssaiAvailabilityInfo{} problemDetails *models.ProblemDetails ) var amfIdx int var original []byte hitAmf := false factory.NssfConfig.RLock() for amfIdx, amfConfig := range factory.NssfConfig.Configuration.AmfList { if amfConfig.NfId == nfId { temp := factory.NssfConfig.Configuration.AmfList[amfIdx].SupportedNssaiAvailabilityData const dummyString string = "DUMMY" for i := range temp { for j := range temp[i].SupportedSnssaiList { if temp[i].SupportedSnssaiList[j].Sd == "" { temp[i].SupportedSnssaiList[j].Sd = dummyString } } } original = json.Marshal(temp) original = bytes.ReplaceAll(original, []byte(dummyString), []byte("")) hitAmf = true break } } factory.NssfConfig.RUnlock() for i, patchItem := range nssaiAvailabilityUpdateInfo { if reflect.ValueOf(patchItem.Value).Kind() == reflect.Map { _, exist := patchItem.Value.(map[string]interface{})["sst"] _, notExist := patchItem.Value.(map[string]interface{})["sd"] if exist && !notExist { nssaiAvailabilityUpdateInfo[i].Value.(map[string]interface{})["sd"] = "" } } } patchJSON, err := json.Marshal(nssaiAvailabilityUpdateInfo) patch, err := jsonpatch.DecodePatch(patchJSON) modified, err := patch.Apply(original) factory.NssfConfig.Lock() json.Unmarshal(modified, &factory.NssfConfig.Configuration.AmfList[amfIdx].SupportedNssaiAvailabilityData) factory.NssfConfig.Unlock() // Return all authorized NSSAI availability information response.AuthorizedNssaiAvailabilityData, err = util.AuthorizeOfAmfFromConfig(nfId) c.JSON(http.StatusOK, response) } // https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_store.go#L26 func (p *Processor) NssaiAvailabilityNfInstanceDelete(c *gin.Context, nfId string) { var problemDetails *models.ProblemDetails for i, amfConfig := range factory.NssfConfig.Configuration.AmfList { if amfConfig.NfId == nfId { factory.NssfConfig.Configuration.AmfList = append( factory.NssfConfig.Configuration.AmfList[:i], factory.NssfConfig.Configuration.AmfList[i+1:]...) c.Status(http.StatusNoContent) return } } }
NssaiAvailabilitySubscription
NSSF事件的主要订阅者通常是AMF,因为它负责为用户设备接入网络,所以需要及时了解网络切片的状态。当一些和切片有关的事件发生时,已经订阅该事件的AMF实例就会被通知到。具体来说,当订阅的TAI(Tracking Area Identity)范围内的切片可用性发生变化时,NSSF就会通知AMF,这使得AMF可以及时更新本地缓存的网络切片信息,提高网络切片选择的效率。但话说回来,目前和NSSF有关的事件只有一个SNSSAI_STATUS_CHANGE_REPORT
而已。
/* https://github.com/free5gc/openapi/blob/main/models/model_nssf_event_type.go */ package models type NssfEventType string // List of NssfEventType const ( NssfEventType_SNSSAI_STATUS_CHANGE_REPORT NssfEventType = "SNSSAI_STATUS_CHANGE_REPORT" )
下面的简化版代码显示了NSSF事件的订阅和取消订阅过程。NSSF收到事件订阅的请求,以及相应的createData,于是给它打个SubscriptionId
以后添加到NssfConfig.Subscriptions
里。类似的,取消订阅则是通过SubscriptionId
找到这个订阅,然后把他从NssfConfig.Subscriptions
里以除。
// https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_subscription.go#L48 func (p *Processor) NssaiAvailabilitySubscriptionCreate( c *gin.Context, createData models.NssfEventSubscriptionCreateData, ) { var ( response *models.NssfEventSubscriptionCreatedData = &models.NssfEventSubscriptionCreatedData{} problemDetails *models.ProblemDetails ) var subscription factory.Subscription tempID, err := getUnusedSubscriptionID() subscription.SubscriptionId = tempID subscription.SubscriptionData = new(models.NssfEventSubscriptionCreateData) *subscription.SubscriptionData = createData factory.NssfConfig.Subscriptions = append(factory.NssfConfig.Subscriptions, subscription) response.SubscriptionId = subscription.SubscriptionId if !subscription.SubscriptionData.Expiry.IsZero() { response.Expiry = new(time.Time) *response.Expiry = *subscription.SubscriptionData.Expiry } response.AuthorizedNssaiAvailabilityData = util.AuthorizeOfTaListFromConfig(subscription.SubscriptionData.TaiList) c.JSON(http.StatusOK, response) } https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nssaiavailability_subscription.go#L88 func (p *Processor) NssaiAvailabilitySubscriptionUnsubscribe(c *gin.Context, subscriptionId string) { var problemDetails *models.ProblemDetails factory.NssfConfig.Lock() defer factory.NssfConfig.Unlock() for i, subscription := range factory.NssfConfig.Subscriptions { if subscription.SubscriptionId == subscriptionId { factory.NssfConfig.Subscriptions = append(factory.NssfConfig.Subscriptions[:i], factory.NssfConfig.Subscriptions[i+1:]...) c.Status(http.StatusNoContent) return } } }
点击查看getUnusedSubscriptionID()函数
因为在free5gc里NSSF事件的SubscriptionID是整数,所以每个从1到的整数都是稀有资源,每当有一个subscrption被取消了,它对应的ID就可以被回收再利用,因此getUnusedSubscriptionID()
会遍历已有的subscription,然后找到尚未使用的最小整数作为新的subscriptionID。
// Get available subscription ID from configuration // In this implementation, string converted from 32-bit integer is used as subscription ID func getUnusedSubscriptionID() (string, error) { var idx uint32 = 1 factory.NssfConfig.RLock() defer factory.NssfConfig.RUnlock() for _, subscription := range factory.NssfConfig.Subscriptions { tempID, err := strconv.Atoi(subscription.SubscriptionId) if err != nil { return "", err } if uint32(tempID) == idx { if idx == math.MaxUint32 { return "", fmt.Errorf("No available subscription ID") } idx++ } else { break } } return strconv.Itoa(int(idx)), nil }
这样做看起来有点多此一举,为什么不直接使用uuid之类随机自动生成的ID呢?毕竟类似的事件订阅机制里NRF就是这样做的
// https://github.com/free5gc/nrf/blob/v1.2.5/internal/context/management_data.go#L47 func SetsubscriptionId() (string, error) { subscriptionIdSize := 16 buffer := make([]byte, subscriptionIdSize) _, err := rand.Read(buffer) if err != nil { return "", err } return hex.EncodeToString(buffer), nil }
我只能推测,free5gc的开发团队认为NSSF的事件订阅需求更旺盛,不止需要使用32位空间的整数,而且随机生成ID很可能会生成一个已被占用的ID,所以需要用更复杂的算法来生成ID。但如果真的是这样,会有如此庞大事件订阅量,那遍历已有的订阅for _, subscription := range factory.NssfConfig.Subscriptions
将是灾难性的时间消耗。
最终,我更倾向于认为是free5gc团队内部在设计这个订阅机制的时候没想太多......
然而有一个问题让我百思不得其解。既然NssaiAvailabilitySubscription
是订阅与NssaiAvailability
相关的事件,那么事件发生时通知订阅了这些事件的NF的代码在哪里呢?可能是我眼拙没看到吧,也可能实现出发订阅通知的代码并不在NSSF模块里。
Nnssf_NSSelection
前文说到Nnssf_NSSAIAvailability
负责对NSSAI进行“增删改”,而Nnssf_NSSelection
负责“查”。具体而言,是外部NF(通常是AMF)向NSSF发一个查询请求,附带一些参数,然后NSSF根据查询返回合适的网络切片,既可用的NSSAI。
这与Nnrf_NFDiscovery
很像。但Nnrf_NFDiscovery
可以接受的查询参数有30页A4纸那么多,而Nnssf_NSSelection
所接受的参数则只有半页纸,是可以把截图贴出来的程度:
处理这些请求的函数时下面的NSSelectionSliceInformationGet
,接受从URL里解析出来的查询,返回相应的AuthorizedNetworkSliceInfo
// https://github.com/free5gc/nssf/blob/v1.2.3/internal/sbi/processor/nsselection_network_slice_information.go#L95 func (p *Processor) NSSelectionSliceInformationGet(c *gin.Context, query url.Values) { var ( status int response *models.AuthorizedNetworkSliceInfo problemDetails *models.ProblemDetails ) param, err := parseQueryParameter(query) err = checkNfServiceConsumer(*param.NfType) // only AMF or NSSF can call this API if param.SliceInfoRequestForRegistration != nil { // Network slice information is requested during the Registration procedure status, response, problemDetails = nsselectionForRegistration(param) } else { // Network slice information is requested during the PDU session establishment procedure status, response, problemDetails = nsselectionForPduSession(param) } c.JSON(status, response) } // https://github.com/free5gc/openapi/blob/v1.0.8/models/model_authorized_network_slice_info.go type AuthorizedNetworkSliceInfo struct { AllowedNssaiList []AllowedNssai `json:"allowedNssaiList,omitempty" bson:"allowedNssaiList"` ConfiguredNssai []ConfiguredSnssai `json:"configuredNssai,omitempty" bson:"configuredNssai"` TargetAmfSet string `json:"targetAmfSet,omitempty" bson:"targetAmfSet"` CandidateAmfList []string `json:"candidateAmfList,omitempty" bson:"candidateAmfList"` RejectedNssaiInPlmn []Snssai `json:"rejectedNssaiInPlmn,omitempty" bson:"rejectedNssaiInPlmn"` RejectedNssaiInTa []Snssai `json:"rejectedNssaiInTa,omitempty" bson:"rejectedNssaiInTa"` NsiInformation *NsiInformation `json:"nsiInformation,omitempty" bson:"nsiInformation"` SupportedFeatures string `json:"supportedFeatures,omitempty" bson:"supportedFeatures"` NrfAmfSet string `json:"nrfAmfSet,omitempty" bson:"nrfAmfSet"` }
可见根据请求查询的情景不同,在用户设备接入网络时和建立PDU会话时对于请求的处理是不同的,也因此由不同的子函数继续深入处理。不过对于这些查询参数的处理过于琐碎,在此略过。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY