1. 程式人生 > >Linux調度域負載均衡-設計,實現和應用

Linux調度域負載均衡-設計,實現和應用

明顯 address log 添加 還要 irq none add 完美

第一部分:Linux負載均衡的設計

一.負載均衡的原則

1.確保每個cpu核心的負載均衡;

2.在cpu和cache以及內存布局的影響下加權執行1。

對於一般多核心cpu情況,以上兩個原則可以簡述為下面的原則:

1.盡量不執行進程遷移,以確保cache的熱度;
2.除非各個cpu的負載已經嚴重失衡,執行負載均衡

二.系統以及cpu的拓撲結構

這個道理看似簡單,然而如果對於一個大型的綜合系統,要想設計一個適用於各種情況的負載均衡體系,卻不是很簡單。Linux內核的負載均衡設計的相當完美。對於負載均衡,可以分為以下幾種情況:
以系統復雜度為核心的分類

情況一:cpu無任何cache

這種情況下,需要隨時保持各cpu上運行的進程數量的均衡

情況二:多處理器,每個處理器有處理器獨享的cache

這種情況下,進行負載均衡會影響處理器的cache利用率,負載均衡帶來的效益會被cache刷新的開銷抵消掉一部分或大部分。

情況三:多處理器,每個處理器有多個處理器核心,每個核心有獨享的一級cache,同一處理器的多個核心有共享的二級,三級cache

這種情況下,情況二是要考慮的,然而對於同一處理器的不同核心之間由於存在共享的二級cache,多個核心之間的負載均衡抵消掉的cache利用率收益遠遠小於不同處理器之間的負載均衡作同樣的事情。

情況四:情況三的前提下,每一個處理器的每一個核心又開啟了超線程(Intel術語)。

由於超線程使用同一套計算資源,且共享cache,因此其上的負載均衡幾乎不會影響cache利用率。然而如果超線程核的調度算法以及操作系統的調度算法設計的不好,造成一個操作系統線程長期使用超線程核,也會造成上一個切換出的操作系統線程的cache被擠出去,遺憾的是,一般情況下我們無力優化操作系統的調度算法,並且無法接觸cpu的smt調度算法。

情況五:情況一到四的前提下,增加不對稱內存。

這種情況下,負載均衡對cache利用率的影響顯然是不可避免的,同時還會影響訪問內存的時間,也就是內存的利用效率,這就是NUMA的情況...
以上五種情況基本就是一個復雜系統從最簡單到最復雜的排列,如果我們不以整個系統為核心,而以處理器為核心,

應該是以下四種排列情況:
以處理器為核心的分類:

情況一:單個處理器開啟超線程

這是我們熟知的SMT情況

情況二:單個處理器多個核心

這是我們熟知的多核處理器情況

情況三:多個處理器

這是我們熟知的SMP情況

情況四:多處理器,多內存域

這是我們熟知的NUMA情況,內存對於不同的處理器來講,其訪問效率是不同的。

三.負載均衡基礎設施以及cpu拓撲結構(靜態設施)

進程在不同cpu之間的遷移和cache利用率總的來說是對立的,並且根據源cpu和目的cpu之間的關系不同這種對立的程度 也不同。 由於進程遷移是基於cpu的,而cpu最小級別的就是超線程處理器的一個smt核,次小的一級就是一個多核cpu的核,然後就是一個物理cpu封裝,再往後就是cpu陣列,根據這些cpu級別的不同,Linux將所有同一級別的cpu歸為一個“調度組”,然後將同一級別的所有的調度組組成一個“調度域”, 負載均衡首先在調度域的各個調度組之間進行,然後再在最低一級的cpu上進行,註意負載均衡是基於最小一級的cpu的。整個架構如下圖所示:


技術分享圖片

四.負載均衡算法分析(動態表現)

遷移一個進程的代價就是削弱cache的作用。 因此,只要在擁有cache的處理器之間遷移進程,勢必會付出這個代價,因此在設計中必然需要一種“阻力”來盡量不做進程遷移,除非萬不得已!這種“阻力”就是負載均衡原則2中的“加權”系數。

1).歷史負載值的影響

為防止處理器負載曲線的上下顛簸,用歷史值來加權當前值是一個不錯的方式 ,也就是說,所謂的負載曲線不再基於時間點 ,而是基於時間段 然而歷史負載值對總負載的影響肯定沒有當前的負載值對總負載的影響大,一個時間點的負載值隨著時間的流逝,對負載均衡時總負載的計算的影響應該逐漸減小。因此Linux的負載均衡器設計了一個公式專門用於負載均衡過程中對cpu總負載的計算。該公式如下:

total_load=(previous_load*(delta-1)+nowa_load)/delta

其中delta是一個可變的系數,在linux 2.6.18中設置了3個delta,分別為1,2,4,當然還可以更多,比如高一些版本的內核中delta的取值有CPU_LOAD_IDX_MAX種,CPU_LOAD_IDX_MAX由宏來定義。比如當delta為2的時候,上述公式成為:
total_load=(previous_load+nowa_load)/2
相當於歷史值占據整個load值一半,而當前值占據另一半。

2).波峰/波谷的平滑化

讓歷史值參與計算總負載解決了同一條負載曲線顛簸的問題,但是在負載均衡時是比較兩條負載曲線同一時間點上的值 ,當二者相差大於一個閥值時,實施進程遷移。 為了做到“盡量不做進程遷移”這個原則,必須將兩條負載曲線的波峰和波谷平滑掉。 如果進程遷移源cpu的負載曲線此時正好在波峰,目的cpu的負載曲線此時正好在波谷,此時就需要將波峰和波谷削平,讓源cpu的負載下降a,而目的cpu的負載上升b,這樣它們之間的負載差就會減少a+b,這個“阻力”足以阻止很多的進程遷移操作。

3).負載曲線平滑操作的基準

負載均衡平滑操作時需要兩個值,即上述的a和b,這兩個值決定了削平波峰/波谷的幅度,幅度越大,阻礙負載均衡的“力度”也就越大,反之“力度”也就越小。根據參與負載均衡的cpu的層次級別的不同,這種幅度應該不同,幸運的是,可以根據調整“負載均衡過程中對cpu總負載的計算公式”中的delta來影響幅度的大小, 這樣,1)和2)就在這點上獲得了統一。對於目的cpu,取計算得到的total_load和nowa_load之間的最大值,而對於源cpu,則取二者最小值。可以看出,在公式中,如果delta等於1,則不執行削波峰/波谷操作,這適用於smt的情況,delta越大,歷史負載值的影響也就越大,削波峰/波谷後的源cpu負載曲線和目的cpu負載曲線的差值曲線越趨於平滑,這樣就越能阻止負載均衡操作(差分算法....)。

4).自下而上的遍歷方式

Linux在基於調度域進行負載均衡的時候采用的是自下而上的遍歷方式,這樣就優先在對cache影響最小的cpu之間進行負載均衡,同時這種均衡操作會增加本cpu的負載,反過來在比較高的調度域級別上有力的阻止了對cache影響很大的cpu之間的負載均衡。 我們知道,調度域是從對cache影響最小的最底層向高層構建的。

5).結論

隨著cpu級別的提高,由於負載均衡對cache利用率的影響逐漸增大,“阻力”也應該逐漸加大,因此負載均衡對應調度域使用的delta也應該增加。 算法的根本要點是什麽呢?畫幅圖就一目了然了,delta越大,負載值受歷史值的影響越大,因此按照公式所示,只有持續單調遞增 的cpu負載,在源cpu選擇時才會被選中,偶然一次的高負載並不足以引起其上的進程遷移至別處,相應的,只有負載持續單調遞減 ,才會引起其它cpu上的進程遷移至此,這正體現了負載以一個時間段而不是一個時間點為統計周期! 而級別越高的cpu間的進程遷移,需要的“阻力”越大,因此就越受歷史值的影響,因為只要歷史中有一次負載很小,就會很明顯的反應在當前,同樣的道理,歷史中有一次的負載很大,也很容易反映在當前;反之,所需“阻力”越小,就越容易受當前負載值的影響,極端的情況下,超線程的不同邏輯cpu之間的負載計算公式中delta為1,因此它們的負載計算結果完全就是該cpu的當前負載!
結論有三:

5.1).通過“負載均衡過程中對cpu總負載的計算公式”平滑了單獨cpu的負載曲線,使之不受突變的影響,平滑程度根據delta微調

5.2).通過“削掉波峰/波谷”平滑了源cpu和目的cpu負載曲線在負載均衡這個時間點的差值,盡可能阻止進程遷移,阻止程度根據delta微調

5.3).執行負載均衡的過程中,一輪負載均衡在每一層的效果需要隨著級別的升高而降低,這通過自下而上的遍歷方式來完成

6).引申

total_load的計算公式實際上使用了一個數列,該數列是一個“等比數列+微擾數列”的和數列, 等比的比值的分母決定著數列的平滑程度,而微擾數列則是cpu的當前真實負載,它根據delta的取值不同對整個cpu的負載影響不同,為了連續化數列,我們設這兩個數列為函數f(x)和g(x),證明如下:

技術分享圖片

6.1).算法改進

不能從delta中得到隨著d的增加,阻礙負載均衡的力度將加大這個事實雖然在技術上通過自下而上的遍歷方式解決了,然而這使得算法依賴了一個操作方式,這在數學卻不是很完美, 因此可以改進,引入一個參數k來微調g(x) ,而不是依賴d來微調,如果配置k和d相等,那麽新算法將回退到老算法:

技術分享圖片

有了新的負載計算公式,我們可以控制一個變量k,然後得知,隨著d的增加,負載均衡實際發生的可能性將降低。

五.Linux負載均衡的類型以及時機

1.周期性忙負載均衡:在時鐘中斷中針對當前cpu調用,負載計算時更多受到歷史值的影響;

2.周期性空閑負載均衡:當前cpu上沒有進程可運行時調用,適當減少歷史值的影響,和忙負載均衡的周期相差一個busy_factor因子(該因子可配置)。

3.喚醒進程負載均衡:Linux內核傾向於本地喚醒進程,也就是說將進程喚醒在本cpu上。在網絡應用中,這顯得尤為重要,眾所周知,網卡中斷某一個cpu,該cpu處理軟中斷,軟中斷處理協議棧,在cpu的處理過程中網絡數據相應進入cache,此時喚醒用戶態進程繼續處理應用數據,如果是本地喚醒的話,應用程序可以有效利用cpu中已經被內核載入的cache。

4.進程新創建的時候會進行負載均衡,因此多了一個進程可能會引起負載失衡。

5.進程調用exec的時候會進行負載均衡,和4一樣,這兩種負載均衡都是“自負載”均衡,也就是要為自己選擇一個cpu來運行

6.當前cpu馬上進入idle的時候,會進行負載均衡

7.push平衡,這是一種將本地進程“推”給其他cpu的負載均衡方式

第二部分:Linux負載均衡的實現(2.6.18內核)

一.數據結構

1.sched_group結構

2.sched_domain結構

二.代碼

1.初始化

1.1.build_sched_domains函數

對於每一個最低級別的cpu(比如超線程cpu)依次執行:

其中三個函數返回了cpu在對應級別的編號,用於初始化調度組:

1.1.1.返回物理cpu的cpu號:

1.1.2.返回cpu核的cpu號:

1.1.3.返回邏輯cpu的cpu號:


初始化了每一個邏輯cpu的調度域之後,依次初始化每一個調度域的各個調度組

1.2.init_sched_build_groups函數

該函數初始化了同一調度域中的所有的調度組,邏輯很簡單:

2.負載均衡運行

2.1.rebalance_tick函數

每次時鐘中斷時,要判斷是否要做負載均衡操作了,具體來講實現兩個邏輯:

2.1.1.更新cpu_load,也就是實現了計算總負載的公式


可見,Linux將3個負載值保存成了數組,隨著索引的增加,歷史值影響逐漸加大,具體祥見第一部分的分析

2.1.2.從下往上依次判斷是否進行負載均衡


j的設置是巧妙的,由於每次時鐘中斷都會導致jiffies遞增,因此當某個時刻j-sd->last_balance正好等於interval的時候,比該cpu的cpu號大的cpu的結果將是j-sd->last_balance<interval,由此多個cpu同時操作同一個cpu的幾率將減少(有效避免了該cpu將別的cpu上的進程拉了過來,然而別的cpu在調用同一函數的時候又將進程拉了回去這種互相扯皮的事情),鑒於隨機數的產生會有很大的開銷,因此采用了jiffies+cpu*HZ/NR_CPUS這種算法來混亂化執行的時間。 然而,由於對於每一個cpu,balance_interval參數是可以配置的,因此配置不同的balance_interval參數可能會抵消掉這種混亂化操作的結果。

2.2.load_balance函數

該函數比較復雜,它在同一個調度域的各個調度組之間進行負載均衡,總的來講分為三塊

2.2.1.找出最busy的組

2.2.2.在最busy的組中找出最busy的cpu

2.2.3.遷移最busy的cpu上的進程到本cpu,並返回實際遷移的進程的數目

2.3.find_busiest_group函數

該函數實現很復雜,然而邏輯很簡單,基本策略祥見第一部分的“負載均衡算法分析”。對於代碼,實際上就是一個兩層的循環加上數據的更新

其中source_load取了cpu_load[delta]和nowa_load的最小值,削掉了波峰,而target_load則相反,削掉了波谷

可以看到,基於調度域的負載均衡是從下往上進行的,這樣做的好處在於,每次優先從最底層級別附近pull進程過來,這樣對cache的影響最小,比如兩個邏輯cpu之間遷移進程對cache的影響就會小到可以忽略。隨著調度域的級別的增加以及pull過來的進程增加,本cpu的負載會增加,一般而言,到達物理cpu級別這個調度域,本cpu已經就已經很忙了,因此也就很難再進行負載均衡了,實際上這也是一種阻礙進程遷移的方式。

2.4.find_busiest_queue函數

在最busy的組中尋找最busy的cpu,很簡單,就是一次冒泡算法。

2.5.move_tasks函數

遷移進程

2.6.can_migrate_task函數

內核代碼中的註釋解決本函數:
We do not migrate tasks that are:
1) running (obviously), or
2) cannot be migrated to this CPU due to cpus_allowed, or
3) are cache-hot on their current CPU.
Aggressive migration if:
1) task is cache cold, or
2) too many balance attempts have failed.

需要註意的是,cache的熱度是通過進程離開運行態到現在的時間差來決定的,而這個差的閥值到底是多少,則由調度域的一個cache_hot_time字段決定。

2.7.migration_thread內核線程

本文不談這種push模式的進程遷移,同時也不深究所謂的主動均衡和被動均衡,這些在理解了核心算法後都會很簡單的。故此處略過

第三部分:負載均衡的配置

一.概述

值得註意的是,Linux所實現的調度域和調度組僅僅描述了一個cpu的靜態拓撲和一組默認的配置, 這組默認的配置生成的原則就是在第一部分中描述的各種情況的基礎上在負載均衡和cache利用率之間產生最小的對抗 Linux並沒有將這些配置定死,實際上Linux的負載均衡策略是可以動態配置的。
由於負載均衡實現的時候,對調度域數據結構對象使用了淺拷貝 ,因此對於每一個最小級別的cpu,都有自己的可配置參數,而對於所有屬於同一調度域的所有cpu而言,它們有擁有共享的調度組,這些共享的信息在調度域數據結構中用指針實現,因此對於調度域參數而言,每個cpu是可以單獨配置的。每個cpu都可配置使得可以根據底層級別cpu上的負載情況(負載只能在最底層級別的cpu上)進行靈活的參數配置,還可以完美支持虛擬化和組調度。

二.配置方法

目錄/proc/sys/kernel/sched_domain 下有所有的最底層級別的cpu目錄,比如你的機器上有4個物理cpu,每個物理cpu有2個核心,每個核心都開啟了超線程,則總共的cpu數量是4*2*2=16,因此
root@ZY:/proc/sys/kernel/sched_domain# ls
cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 ...cpu15
root@ZY:/proc/sys/kernel/sched_domain# cd cpu0/
root@ZY:/proc/sys/kernel/sched_domain/cpu0# ls
domain0 domain1 domain2
root@ZY:/proc/sys/kernel/sched_domain/cpu0# cat domain0/name
SIBLING
root@ZY:/proc/sys/kernel/sched_domain/cpu0# cat domain1/name
MC
root@ZY:/proc/sys/kernel/sched_domain/cpu0# cat domain2/name
CPU
root@ZY:/proc/sys/kernel/sched_domain/cpu0# ls domain0/ #以下這些參數都是可配置的,使用sysctl即可,含義見sched_domain結構體
busy_factor cache_nice_tries forkexec_idx imbalance_pct min_interval newidle_idx
busy_idx flags idle_idx max_interval name wake_idx

三.配置實例

列舉一個性能調優的實例,當我們手工綁定了進程在各自cpu上運行,並且手工平衡了各個cpu的負載,每一個cpu都有特定的任務,比如cpu0處理網絡中斷和軟中斷,cpu1處理磁盤IO,cpu2運行web服務,...(暫不考慮dca等對cache的影響),那麽也就不希望內核再做負載均衡了,因此需要針對每一個cpu的調度域進行配置,使之不再進行或者“很不頻繁”進行負載均衡操作:
root@ZY:/proc/sys/kernel/sched_domain# echo 100000000000 > cpu0/domain1/min_interval
root@ZY:/proc/sys/kernel/sched_domain# echo 100000000000 > cpu0/domain2/min_interval

...//針對cpuX依照上述執行,另外還要設置max_interval,要大於100000000000 。對於domain0,由於它是SMT級別的,因此負載均衡並不會破壞cache,因此不設置。
...//其實還有很多的參數可以設置,比如flags,imbalance_pct,busy_factor等。
註解: 由於fork和exec的行為是不同的,fork後的新進程還是要訪問老進程的數據(寫時復制),而exec則徹底告別老進程(雖然還可能會訪問同樣載入老進程的共享庫),因此調度域的flags中的SD_BALANCE_FORK和SD_BALANCE_EXEC最好應該區別開來,我們可以通過在SMT或者MC調度域中設置SD_BALANCE_FORK而SMP中不設置SD_BALANCE_FORK來優化fork後的進程的寫時復制,至於SD_BALANCE_EXEC則全部支持,不過這樣設置的前提是你對你的應用進程的脾氣很了解,如果exec後的進程和之前的進程共享大量的在之前之後都大量被讀寫的共享庫的話,說實話SD_BALANCE_EXEC標誌也最好不要設置在SMP調度域中。

第四部分:又一個內核hack

完全可以使用sysctl配置系統的debug級別或者重新編譯內核增加更多的打印信息,然而編寫modules導出自己需要的信息一直都是最好的方式,因為它只輸出你需要的信息,而內核的debug信息雖然很詳細,但是你可能還真的需要花一番功夫才能明白其所以然。

以下是一個內核模塊的代碼,它揪出了兩個cpu的調度域和調度組信息,然後打印出來,這種編寫模塊的好處在於,你可以做且僅做你需要的,且一切按照你自己的風格來!



對於一個單物理cpu開啟超線程的系統加載上述模塊,dmesg得到以下結果:
[63962.546289] domain address: ffff88000180fa20
[63962.546294] domain name: SIBLING
[63962.546297] domain busy: 3
[63962.546300] domain busy: 180fa98
[63962.546303] group address:ffff88000180fae0 #cpu0-第一個邏輯cpu的smt調度域的第一個組,包括它自身(1)
[63962.546306] group address:ffff88000184fae0 #cpu0-第一個邏輯cpu的smt調度域的第二個組,包括它兄弟(2)
[63962.546308] next domain
[63962.546311] domain address: ffff88000180fb30
[63962.546314] domain name: MC
[63962.546316] domain busy: 30
[63962.546319] domain busy: 180fba8
[63962.546321] group address:ffff88000180fbf0
[63962.546324] next domain
[63962.546326] NEXT CPU
[63962.546329] domain address: ffff88000184fa20
[63962.546332] domain name: SIBLING
[63962.546335] domain busy: 0
[63962.546337] domain busy: 184fa98
[63962.546340] group address:ffff88000184fae0 #cpu0-第一個邏輯cpu的smt調度域的第一個組,包括它自身,等於(2)
[63962.546343] group address:ffff88000180fae0 #cpu0-第一個邏輯cpu的smt調度域的第一個組,包括它兄弟,等於(1)
[63962.546345] next domain
[63962.546348] domain address: ffff88000184fb30
[63962.546351] domain name: MC
[63962.546354] domain busy: 2
[63962.546357] domain busy: 184fba8
[63962.546359] group address:ffff88000180fbf0
[63962.546362] next domain

第五部分:Linux內核《sched-domains.txt》翻譯

要問關於Linux內核的那些資料最好,我覺得最好的有兩個,一個是LKML(Linux kernel maillist)還有一個就是內核文檔,內核文檔中的信息相當豐富,涉及了幾乎所有的核心功能,因此閱讀它們是有幫助的,本文的最後,我嘗試將其中《sched-domains.txt》翻譯一下,“[]”中的是我的一切註釋,註意,以下並不是原文直譯,而是意譯(信,達,雅三境界中,我可能連“信”都談不上,因此找個理由,說是意譯!)。譯文如下:

每一個cpu都擁有一個“base”調度域(struct sched_domain)[註:基本的調度域,也就是最底層的調度域,以下的per-cpu指的就是最底層的cpu]。這些“base”調度域可以通過cpu_sched_domain(i)和this_sched_domain()這兩個宏來訪問。調度域層次結構從這些“base”調度域開始[註:向上]構建,通過調度域的parent指針可以訪問到其上級調度域。parent指針必須是NULL結尾的[註:最高一級別的調度域的parent指針為NULL],調度域是per-cpu的,因為這樣可以在更新其字段時,鎖的開銷更小[註:同時每個調度域也是單獨可配置的]。
每一個調度域覆蓋一定數量的cpu(存儲於span字段)。一個調度域的span必須是其子調度域span的超集。cpui的“base”調度域的span中起碼要包含cpui。最頂層的調度域需要覆蓋系統所有的cpu,雖然嚴格來講這並不是必須的,會導致一些cpu上從來都沒有進程運行。調度域的span的含義是“在這些cpu之間進行負載均衡”
每一個調度域必須擁有起碼一個cpu調度組(struct sched_groups),這些組組織成一個環形鏈表,該鏈表從調度域的groups字段開始。同一個調度域的這些組的cpumasks的並集表示的cpu必須和該調度域的span字段表示的cpu完全相等,同時,屬於同一調度域的任意兩個組的cpumasks字段的交集必須是空集。一個調度域的調度組覆蓋的cpu必須是該調度域所覆蓋的。這些調度組的只讀數據在cpu之間是共享的。
一個調度域的負載均衡操作發生在其各個調度組之間。此時,每一個調度組被當成了一個整體對待。一個調度組的負載定義為該組中所有的cpu成員的負載之和,並且只有當組與組之間的負載失衡的時候,才會在組與組之間遷移進程。
從源文件kernel/sched.c中可以看出,每一個cpu會周期性的調用rebalance_tick函數。該函數將從該cpu的“base”調度域開始檢查該調度域內的進程是否到達了其負載均衡的周期,如果是,則在該調度域調用load_balance,然後在“base”調度域的parent調度域中執行上述操作,這是一個遍歷的過程,遍歷過程以此類推。
*** 調度域的實現[和定制] ***
“base”調度域將構成調度域層級結構的第一級。舉例來講,在SMT的情況下,“base”調度域覆蓋了一個物理cpu的所有邏輯cpu,每一個邏輯cpu構成了一個調度組。SMP的情況下,物理cpu調度域作為“base”調度域的parent,它將覆蓋一個NUMA節點中的所有的cpu。其每一個調度組覆蓋一個物理cpu。同樣的道理,在NUMA情況下,節點調度域作為物理cpu調度域的parent,它將覆蓋整臺機器的所有cpu,其每一個調度組覆蓋一個節點的所有cpu。
實現者需要閱讀include/linux/sched.h文件裏面關於sched_domain結構體的字段的註釋以及SD_FLAG_*和SD_*_INIT來了解更多的細節以及了解如何來調節這些參數從而影響內核負載均衡的行為。
如果你想支持SMT,必須定義CONFIG_SCHED_SMT宏,並且提供一個cpumask_t類型的數組cpu_sibling_map[NR_CPUS],元素cpu_sibling_map[i]的含義是所有和cpui屬於同一個物理cpu[或者物理cpu核]的邏輯cpu的掩碼,這個掩碼中當然也包括cpui本身。
[針對特定的體系結構可以對Linux內核默認的調度域/調度組的默認參數以及設置進行重載,此處不再翻譯]

再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智能的隊伍中來!https://blog.csdn.net/jiangjunshow

Linux調度域負載均衡-設計,實現和應用