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定义了更多标准切片类型,各大运营商也可以按需定义自己的切片:

img

NSSF的功能划分为两个NFS:Nnssf_NSSAIAvailabilityNnssf_NSSelection,主要是对网路切片信息增删改查,更准确来说是对NSSAI(Network Slice Selection Assistance Information)增删改查。NSSAI是网络切片的标识符,当用户设备试图接入网络时,负责接入管理的AMF会向NSSF咨询合适的网络切片,此时NSSF返回给AMF的也是NAASI。NSSF的这两个服务在free5gc都实现了(毕竟也不复杂)。

img

要注意的是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):
img
每个切片都有自己的覆盖范围(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。

img

这与Nnrf_NFDiscovery很像。但Nnrf_NFDiscovery可以接受的查询参数有30页A4纸那么多,而Nnssf_NSSelection所接受的参数则只有半页纸,是可以把截图贴出来的程度:

img

处理这些请求的函数时下面的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会话时对于请求的处理是不同的,也因此由不同的子函数继续深入处理。不过对于这些查询参数的处理过于琐碎,在此略过。


posted @ 2024-10-26 12:50  zrq96  阅读(43)  评论(2编辑  收藏  举报