最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

go micro集成鏈路跟蹤的方法和中間件原理解析

 更新時(shí)間:2022年05月05日 09:33:23   作者:波斯馬  
這篇文章主要介紹了go-micro集成鏈路跟蹤的方法和中間件原理,關(guān)于Http或者說(shuō)是Restful服務(wù)的鏈路跟蹤,go-micro的httpClient支持CallWrapper,可以用WrapCall來(lái)添加鏈路跟蹤的CallWrapper,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧

前幾天有個(gè)同學(xué)想了解下如何在go-micro中做鏈路跟蹤,這幾天正好看到wrapper這塊,wrapper這個(gè)東西在某些框架中也稱為中間件,里邊有個(gè)opentracing的插件,正好用來(lái)做鏈路追蹤。opentracing是個(gè)規(guī)范,還需要搭配一個(gè)具體的實(shí)現(xiàn),比如zipkin、jeager等,這里選擇zipkin。

鏈路跟蹤實(shí)戰(zhàn)

安裝zipkin

通過(guò)docker快速啟動(dòng)一個(gè)zipkin服務(wù)端:

docker run -d -p 9411:9411 openzipkin/zipkin

程序結(jié)構(gòu)

為了方便演示,這里把客戶端和服務(wù)端放到了一個(gè)項(xiàng)目中,程序的目錄結(jié)構(gòu)是這樣的:

  • main.go 服務(wù)端程序。client/main.go 客戶端程序。
  • config/config.go 程序用到的一些配置,比如服務(wù)的名稱和監(jiān)聽端口、zipkin的訪問地址等。
  • zipkin/ot-zipkin.go opentracing和zipkin相關(guān)的函數(shù)。

安裝依賴包

需要安裝go-micro、opentracing、zipkin相關(guān)的包:

go get go-micro.dev/v4@latest
go get github.com/go-micro/plugins/v4/wrapper/trace/opentracing
go get -u github.com/openzipkin-contrib/zipkin-go-opentracing

編寫服務(wù)端

首先定義一個(gè)服務(wù)端業(yè)務(wù)處理程序:

type Hello struct {
}
func (h *Hello) Say(ctx context.Context, name *string, resp *string) error {
	*resp = "Hello " + *name
	return nil
}

這個(gè)程序只有一個(gè)方法Say,輸入name,返回 "Hello " + name。

然后使用go-micro編寫服務(wù)端框架程序:

func main() {
	tracer := zipkin.GetTracer(config.SERVICE_NAME, config.SERVICE_HOST)
	defer zipkin.Close()
	tracerHandler := opentracing.NewHandlerWrapper(tracer)
	service := micro.NewService(
		micro.Name(config.SERVICE_NAME),
		micro.Address(config.SERVICE_HOST),
		micro.WrapHandler(tracerHandler),
	)
	service.Init()
	micro.RegisterHandler(service.Server(), &Hello{})
	if err := service.Run(); err != nil {
		log.Println(err)
	}
}

這里NewService的時(shí)候除了指定服務(wù)的名稱和訪問地址,還通過(guò)micro.WrapHandler設(shè)置了一個(gè)用于鏈路跟蹤的HandlerWrapper。

這個(gè)HandlerWrapper是通過(guò)go-micro的opentracing插件提供的,這個(gè)插件需要傳入一個(gè)tracer。這個(gè)tracer可以通過(guò)前邊安裝的 zipkin-go-opentracing 包來(lái)創(chuàng)建,我們把創(chuàng)建邏輯封裝在了config.go中:

func GetTracer(serviceName string, host string) opentracing.Tracer {
	// set up a span reporter
	zipkinReporter = zipkinhttp.NewReporter(config.ZIPKIN_SERVER_URL)
	// create our local service endpoint
	endpoint, err := zipkin.NewEndpoint(serviceName, host)
	if err != nil {
		log.Fatalf("unable to create local endpoint: %+v\n", err)
	}
	// initialize our tracer
	nativeTracer, err := zipkin.NewTracer(zipkinReporter, zipkin.WithLocalEndpoint(endpoint))
	if err != nil {
		log.Fatalf("unable to create tracer: %+v\n", err)
	}
	// use zipkin-go-opentracing to wrap our tracer
	tracer := zipkinot.Wrap(nativeTracer)
	opentracing.InitGlobalTracer(tracer)
	return tracer
}

service創(chuàng)建完畢之后,還要通過(guò) micro.RegisterHandler 來(lái)注冊(cè)前邊編寫的業(yè)務(wù)處理程序。

最后通過(guò) service.Run 讓服務(wù)運(yùn)行起來(lái)。

編寫客戶端

再來(lái)看一下客戶端的處理邏輯:

func main() {
	tracer := zipkin.GetTracer(config.CLIENT_NAME, config.CLIENT_HOST)
	defer zipkin.Close()
	tracerClient := opentracing.NewClientWrapper(tracer)
	service := micro.NewService(
		micro.Name(config.CLIENT_NAME),
		micro.Address(config.CLIENT_HOST),
		micro.WrapClient(tracerClient),
	)
	client := service.Client()
	go func() {
		for {
			<-time.After(time.Second)
			result := new(string)
			request := client.NewRequest(config.SERVICE_NAME, "Hello.Say", "FireflySoft")
			err := client.Call(context.TODO(), request, result)
			if err != nil {
				log.Println(err)
				continue
			}
			log.Println(*result)
		}
	}()
	service.Run()
}

這段代碼開始也是先NewService,設(shè)置客戶端程序的名稱和監(jiān)聽地址,然后通過(guò)micro.WrapClient注入鏈路跟蹤,這里注入的是一個(gè)ClientWrapper,也是由opentracing插件提供的。這里用的tracer和服務(wù)端tracer是一樣的,都是通過(guò)config.go中GetTracer函數(shù)獲取的。

然后為了方便演示,啟動(dòng)一個(gè)go routine,客戶端每隔一秒發(fā)起一次RPC請(qǐng)求,并將返回結(jié)果打印出來(lái)。運(yùn)行效果如圖所示:

zipkin中跟蹤到的訪問日志:

Wrap原理分析

Wrap從字面意思上理解就是封裝、嵌套,在很多的框架中也稱為中間件,比如gin中,再比如ASP.NET Core中。這個(gè)部分就來(lái)分析下go-micro中Wrap的原理。

服務(wù)端Wrap

在go-micro中服務(wù)端處理請(qǐng)求的邏輯封裝稱為Handler,它的具體形式是一個(gè)func,定義為:

func(ctx context.Context, req Request, rsp interface{}) error

這個(gè)部分就來(lái)看一下服務(wù)端Handler是怎么被Wrap的。

HandlerWrapper

要想Wrap一個(gè)Handler,必須創(chuàng)建一個(gè)HandlerWrapper類型,這其實(shí)是一個(gè)func,其定義如下:

type HandlerWrapper func(HandlerFunc) HandlerFunc

它的參數(shù)和返回值都是HandlerFunc類型,其實(shí)就是上面提到的Handler的func定義。

以本文鏈路跟蹤中使用的 tracerHandler 為例,看一下HandlerWrapper是如何實(shí)現(xiàn)的:

func(h server.HandlerFunc) server.HandlerFunc {
		return func(ctx context.Context, req server.Request, rsp interface{}) error {
			...
			if err = h(ctx, req, rsp); err != nil {
			...
		}
	}

從中可以看出,Wrap一個(gè)Hander就是定義一個(gè)新Handler,在它的的內(nèi)部調(diào)用傳入的原Handler。

Wrap Handler

創(chuàng)建了一個(gè)HandlerWrapper之后,還需要把它加入到服務(wù)端的處理過(guò)程中。

go-micro在NewService的時(shí)候通過(guò)調(diào)用 micro.WrapHandler 設(shè)置這些 HandlerWrapper:

service := micro.NewService(
		...
		micro.WrapHandler(tracerHandler),
	)

WrapHandler的實(shí)現(xiàn)是這樣的:

func WrapHandler(w ...server.HandlerWrapper) Option {
	return func(o *Options) {
		var wrappers []server.Option
		for _, wrap := range w {
			wrappers = append(wrappers, server.WrapHandler(wrap))
		}
		o.Server.Init(wrappers...)
	}
}

它返回的是一個(gè)函數(shù),這個(gè)函數(shù)會(huì)將我們傳入的HandlerWrapper通過(guò)server.WrapHandler轉(zhuǎn)化為一個(gè)server.Option,然后交給Server.Init進(jìn)行初始化處理。

這里的server.Option其實(shí)還是一個(gè)func,看一下WrapHandler的源碼:

func WrapHandler(w HandlerWrapper) Option {
	return func(o *Options) {
		o.HdlrWrappers = append(o.HdlrWrappers, w)
	}
}

這個(gè)func將我們傳入的HandlerWrapper添加到了一個(gè)切片中。

那么這個(gè)函數(shù)什么時(shí)候執(zhí)行呢?就在Server.Init中。看一下Server.Init中的源碼:

func (s *rpcServer) Init(opts ...Option) error {
	...
	for _, opt := range opts {
		opt(&s.opts)
	}
	
	if s.opts.Router == nil {
		r := newRpcRouter()
		r.hdlrWrappers = s.opts.HdlrWrappers
		...
		s.router = r
	}
	...
}

它會(huì)遍歷傳入的所有server.Option,也就是執(zhí)行每一個(gè)func(o *Options)。這樣Options的切片HdlrWrappers中就添加了我們?cè)O(shè)置的HandlerWrapper,同時(shí)還把這個(gè)切片傳遞到了rpcServer的router中。

可以看到這里的Options就是rpcServer.opts,HandlerWrapper切片同時(shí)設(shè)置到了rpcServer.router和rpcServer.opts中。

還有一個(gè)問題:WrapHandler返回的func什么時(shí)候執(zhí)行呢?

這個(gè)在micro.NewService -> newService -> newOptions中:

func newOptions(opts ...Option) Options {
	opt := Options{
	...
		Server:    server.DefaultServer,
	...
	}
	for _, o := range opts {
		o(&opt)
	}
	...
}

遍歷opts就是執(zhí)行每一個(gè)設(shè)置func,最終執(zhí)行到rpcServer.Init。

到NewService執(zhí)行完畢為止,我們?cè)O(shè)置的WrapHandler全部添加到了一個(gè)名為HdlrWrappers的切片中。

再來(lái)看一下服務(wù)端Wrapper的執(zhí)行過(guò)程是什么樣的?

執(zhí)行Handler的這段代碼在rpc_router.go中:

func (s *service) call(ctx context.Context, router *router, sending *sync.Mutex, mtype *methodType, req *request, argv, replyv reflect.Value, cc codec.Writer) error {
	defer router.freeRequest(req)
	...
	for i := len(router.hdlrWrappers); i > 0; i-- {
		fn = router.hdlrWrappers[i-1](fn)
	}
	...
	// execute handler
	return fn(ctx, r, rawStream)
}

根據(jù)前面的分析,可以知道router.hdlrWrappers中記錄的就是所有的HandlerWrapper,這里通過(guò)遍歷router.hdlrWrappers實(shí)現(xiàn)了HandlerWrapper的嵌套,注意這里遍歷時(shí)索引采用了從大到小的順序,后添加的先被Wrap,先添加在外層。

實(shí)際執(zhí)行時(shí)就是先調(diào)用到最先添加的HandlerWrapper,然后一層層向里調(diào)用,最終調(diào)用到我們注冊(cè)的業(yè)務(wù)Handler,然后再一層層的返回,每個(gè)HandlerWrapper都可以在調(diào)用下一層前后做些自己的工作,比如鏈路跟蹤這里的檢測(cè)執(zhí)行時(shí)間。

客戶端Wrap

在客戶端中遠(yuǎn)程調(diào)用的定義在Client中,它是一個(gè)接口,定義了若干方法:

type Client interface {
	...
	Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
	...
}

我們這里為了講解方便,只關(guān)注Call方法,其它的先省略。

下面來(lái)看一下Client是怎么被Wrap的。

XXXWrapper

要想Wrap一個(gè)Client,需要通過(guò)struct嵌套這個(gè)Client,并實(shí)現(xiàn)Client接口的方法。至于這個(gè)struct的名字無(wú)法強(qiáng)制要求,一般以XXXWrapper命名。

這里以鏈路跟蹤使用的 otWrapper 為例,它的定義如下:

type otWrapper struct {
	ot opentracing.Tracer
	client.Client
}
func (o *otWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
	...
	if err = o.Client.Call(ctx, req, rsp, opts...); err != nil {
	...
}
...

注意XXXWrapper實(shí)現(xiàn)的接口方法中都去調(diào)用了被嵌套Client的對(duì)應(yīng)接口方法,這是能夠嵌套執(zhí)行的關(guān)鍵。

Wrap Client

有了上面的 XXXWrapper,還需要把它注入到程序的執(zhí)行流程中。

go-micro在NewService的時(shí)候通過(guò)調(diào)用 micro.WrapClient 設(shè)置這些 XXXWrapper:

service := micro.NewService(
		...
		micro.WrapClient(tracerClient),
	)

和WrapHandler差不多,WrapClient的參數(shù)不是直接傳入XXXWrapper的實(shí)例,而是一個(gè)func,定義如下:

type Wrapper func(Client) Client

這個(gè)func需要將傳入的的Client包裝到 XXXWrapper 中,并返回 XXXWrapper 的實(shí)例。這里傳入的 tracerClient 就是這樣一個(gè)func:

return func(c client.Client) client.Client {
  if ot == nil {
  	ot = opentracing.GlobalTracer()
  }
  return &otWrapper{ot, c}
}

要實(shí)現(xiàn)Client的嵌套,可以給定一個(gè)初始的Client實(shí)例作為第一個(gè)此類func的輸入,然后前一個(gè)func的輸出作為后一個(gè)func的輸入,依次執(zhí)行,最終形成業(yè)務(wù)代碼中要使用的Client實(shí)例,這很像俄羅斯套娃,它有很多層Client。

那么這個(gè)俄羅斯套娃是什么時(shí)候創(chuàng)建的呢?

在 micro.NewService -> newService -> newOptions中:

func newOptions(opts ...Option) Options {
	opt := Options{
		...
		Client:    client.DefaultClient,
		...
	}
	for _, o := range opts {
		o(&opt)
	}
	return opt
}

可以看到這里給Client設(shè)置了一個(gè)初始值,然后遍歷這些NewService時(shí)傳入的Option(WrapClient返回的也是Option),這些Option其實(shí)都是func,所以就是遍歷執(zhí)行這些func,執(zhí)行這些func的時(shí)候會(huì)傳入一些初始默認(rèn)值,包括Client的初始值。

那么前一個(gè)func的輸出怎么作為后一個(gè)func的輸入的呢?再來(lái)看下WrapClient的源碼:

func WrapClient(w ...client.Wrapper) Option {
	return func(o *Options) {
		for i := len(w); i > 0; i-- {
			o.Client = w[i-1](o.Client)
		}
	}
}

可以看到Wrap方法從Options中獲取到當(dāng)前的Client實(shí)例,把它傳給Wrap func,然后新生成的實(shí)例又被設(shè)置到Options的Client字段中。

正是這樣形成了前文所說(shuō)的俄羅斯套娃。

再來(lái)看一下客戶端調(diào)用的執(zhí)行流程是什么樣的?

通過(guò)service的Client()方法獲取到Client實(shí)例,然后通過(guò)這個(gè)實(shí)例的Call()方法執(zhí)行RPC調(diào)用。

client:=service.Client()
client.Call()

這個(gè)Client實(shí)例就是前文描述的套娃實(shí)例:

func (s *service) Client() client.Client {
	return s.opts.Client
}

前文提到過(guò):XXXWrapper實(shí)現(xiàn)的接口方法中調(diào)用了被嵌套Client的對(duì)應(yīng)接口方法。這就是能夠嵌套執(zhí)行的關(guān)鍵。

這里給一張圖,讓大家方便理解Wrap Client進(jìn)行RPC調(diào)用的執(zhí)行流程:

客戶端Wrap和服務(wù)端Wrap的區(qū)別

一個(gè)重要的區(qū)別是:對(duì)于多次WrapClient,后添加的先被調(diào)用;對(duì)于多次WrapHandler,先添加的先被調(diào)用。

有一個(gè)比較怪異的地方是,WrapClient時(shí)如果傳遞了多個(gè)Wrapper實(shí)例,WrapClient會(huì)把順序調(diào)整過(guò)來(lái),這多個(gè)實(shí)例中前邊的先被調(diào)用,這個(gè)處理和多次WrapClient處理的順序相反,不是很理解。

func WrapClient(w ...client.Wrapper) Option {
	return func(o *Options) {
		// apply in reverse
		for i := len(w); i > 0; i-- {
			o.Client = w[i-1](o.Client)
		}
	}
}

客戶端Wrap還提供了更低層級(jí)的CallWrapper,它的執(zhí)行順序和服務(wù)端HandlerWrapper的執(zhí)行順序一致,都是先添加的先被調(diào)用。

// wrap the call in reverse
	for i := len(callOpts.CallWrappers); i > 0; i-- {
		rcall = callOpts.CallWrappers[i-1](rcall)
	}

還有一個(gè)比較大的區(qū)別是,服務(wù)端的Wrap是調(diào)用某個(gè)業(yè)務(wù)Handler之前臨時(shí)加上的,客戶端的Wrap則是在調(diào)用Client.Call時(shí)就已經(jīng)創(chuàng)建好。這樣做的原因是什么呢?這個(gè)可能是因?yàn)樵诜?wù)端,業(yè)務(wù)Handler和HandlerWrapper是分別注冊(cè)的,注冊(cè)業(yè)務(wù)Handler時(shí)HandlerWrapper可能還不存在,只好采用動(dòng)態(tài)Wrap的方式。而在客戶端,通過(guò)Client.Call發(fā)起調(diào)用時(shí),Client是發(fā)起調(diào)用的主體,用戶有很多獲取Client的方式,無(wú)法要求用戶在每次調(diào)用前都臨時(shí)Wrap。

Http服務(wù)的鏈路跟蹤

關(guān)于Http或者說(shuō)是Restful服務(wù)的鏈路跟蹤,go-micro的httpClient支持CallWrapper,可以用WrapCall來(lái)添加鏈路跟蹤的CallWrapper;但是其httpServer實(shí)現(xiàn)的比較簡(jiǎn)單,把http內(nèi)部的Handler處理完全交出去了,不能用WrapHandler,只能自己在http的框架中來(lái)做這件事,比如go-micro+gin開發(fā)的Restful服務(wù)可以使用gin的中間件機(jī)制來(lái)做鏈路追蹤。

代碼已經(jīng)上傳到Github,歡迎訪問:https://github.com/bosima/go-demo/tree/main/go-micro-opentracing

到此這篇關(guān)于go-micro集成鏈路跟蹤的方法和中間件原理的文章就介紹到這了,更多相關(guān)go micro鏈路跟蹤內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Golang新提案:panic?能不能加個(gè)?PanicError?

    Golang新提案:panic?能不能加個(gè)?PanicError?

    這篇文章主要為大家介紹了Golang的新提案關(guān)于panic能不能加個(gè)PanicError的問題分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • 深入淺出Go語(yǔ)言:手把手教你高效生成與解析JSON數(shù)據(jù)

    深入淺出Go語(yǔ)言:手把手教你高效生成與解析JSON數(shù)據(jù)

    本文將帶你一步步走進(jìn)Go語(yǔ)言的世界,教你如何高效生成與解析JSON數(shù)據(jù),無(wú)論你是初學(xué)者還是經(jīng)驗(yàn)豐富的開發(fā)者,都能在本文中找到實(shí)用的技巧和靈感,本文內(nèi)容簡(jiǎn)潔明了,示例豐富,讓你在閱讀的過(guò)程中輕松掌握Go語(yǔ)言生成與解析JSON數(shù)據(jù)的技巧,需要的朋友可以參考下
    2024-02-02
  • 使用GO操作MongoDB的方法

    使用GO操作MongoDB的方法

    這篇文章主要介紹了使用GO操作MongoDB,包括安裝MongoDB驅(qū)動(dòng)程序連接mongodb的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-05-05
  • Go?viper讀取配置文件的示例詳解

    Go?viper讀取配置文件的示例詳解

    這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言如何利用viper實(shí)現(xiàn)讀取配置文件,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以參考一下
    2023-08-08
  • Go?easyjson使用及反射原理

    Go?easyjson使用及反射原理

    這篇文章主要介紹了Go?easyjson使用技巧,詳細(xì)介紹了go自帶JSON庫(kù)使用的反射原理,性能相對(duì)較差,可以使用easyjson代替,需要的朋友可以參考下
    2022-04-04
  • 通過(guò)案例簡(jiǎn)單聊聊為什么說(shuō)Go中的字符串是不能被修改的

    通過(guò)案例簡(jiǎn)單聊聊為什么說(shuō)Go中的字符串是不能被修改的

    在接觸Go這么語(yǔ)言,可能你經(jīng)常會(huì)聽到這樣一句話,對(duì)于字符串不能修改,可能你很納悶,日常開發(fā)中我們對(duì)字符串進(jìn)行修改也是很正常的,為什么又說(shuō)Go中的字符串不能進(jìn)行修改呢,本文就來(lái)通過(guò)實(shí)際案例給大家演示,為什么Go中的字符串不能進(jìn)行修改
    2023-07-07
  • golang協(xié)程與線程區(qū)別簡(jiǎn)要介紹

    golang協(xié)程與線程區(qū)別簡(jiǎn)要介紹

    這篇文章主要介紹了golang協(xié)程與線程區(qū)別簡(jiǎn)要介紹,進(jìn)程是操作系統(tǒng)資源分配的基本單位,是程序運(yùn)行的實(shí)例,線程是操作系統(tǒng)調(diào)度到CPU中執(zhí)行的基本單位
    2022-06-06
  • Go語(yǔ)言實(shí)現(xiàn)棧與隊(duì)列基本操作學(xué)家

    Go語(yǔ)言實(shí)現(xiàn)棧與隊(duì)列基本操作學(xué)家

    go語(yǔ)言中,并沒有棧與隊(duì)列相關(guān)的數(shù)據(jù)結(jié)構(gòu),但是我們可以借助切片來(lái)實(shí)現(xiàn)棧與隊(duì)列的操作;接下來(lái)我們一起實(shí)現(xiàn)棧與隊(duì)列基本操作,感興趣的可以了解一下
    2022-11-11
  • go-cqhttp環(huán)境配置及安裝過(guò)程

    go-cqhttp環(huán)境配置及安裝過(guò)程

    這篇文章主要介紹了go-cqhttp環(huán)境配置,包括go-cqhttp安裝及簡(jiǎn)單介紹,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-09-09
  • golang編程開發(fā)使用sort排序示例詳解

    golang編程開發(fā)使用sort排序示例詳解

    這篇文章主要為大家介紹了go語(yǔ)言編程開發(fā)使用sort來(lái)排序示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪
    2021-11-11

最新評(píng)論

浮梁县| 绵竹市| 沙湾县| 区。| 巨野县| 顺平县| 皮山县| 夏津县| 宁波市| 桐庐县| 澄城县| 马鞍山市| 合作市| 宜良县| 安康市| 甘泉县| 山东省| 七台河市| 桐柏县| 禹城市| 眉山市| 上饶县| 泸定县| 将乐县| 舒兰市| 龙川县| 开远市| 辉县市| 宣城市| 潜山县| 桓台县| 治多县| 嘉祥县| 博乐市| 镇远县| 墨玉县| 囊谦县| 琼海市| 个旧市| 凤山市| 临夏县|