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到\(2^32\)的整数都是稀有资源,每当有一个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会话时对于请求的处理是不同的,也因此由不同的子函数继续深入处理。不过对于这些查询参数的处理过于琐碎,在此略过。