官术网_书友最值得收藏!

3.4 后臺進程

下面本章將介紹有關于Oracle數據庫的進程相關知識,重點介紹與日常管理工作緊密相關的后臺進程。

Oracle進程又分為兩類:服務器進程和后臺進程。

服務器進程用于處理連接到該實例的用戶進程的請求。當應用和Oracle是在同一臺機器上運行,而不再通過網絡,一般會將用戶進程和它相應的服務器進程組合成單個的進程,可降低系統開銷。當應用和Oracle運行在不同的機器上時,用戶進程經過一個分離服務器進程與Oracle通信。它可執行下列任務:

(1)對應用所發出的SQL語句進行語法分析和執行。

(2)從磁盤(數據文件)中讀入必要的數據塊到SGA的共享數據庫緩沖區(該塊不在緩沖區時)。

(3)將結果返回給應用程序處理。

系統為了使性能最好和協調多個用戶,在多進程系統中使用一些附加進程,稱為后臺進程。在許多操作系統中,后臺進程是在實例啟動時自動建立的。一個Oracle實例可以有許多后臺進程,但它們不是一直存在。

數據庫實例有內存結構和后臺進程。應用與數據庫的所有操作和交互都由數據庫實例完成,SGA可以理解為交互平臺,后臺進程則可以理解為SGA與數據庫交互的橋梁。PMON、SMON、DBWRn、LGWRn、CKPT進程為必需的后臺進程,ARCHn、LCKn等為可選后臺進程,如圖3-4所示。

圖3-4 數據庫后臺進程機構

【示例3-2】Oracle數據庫各個進程的啟動順序

PMON started with pid=2, OS id=18042
DIAG started with pid=3, OS id=18044
PSP0 started with pid=4, OS id=18051
LMON started with pid=5, OS id=18053
LMD0 started with pid=6, OS id=18055
LMS0 started with pid=7, OS id=18057
MMAN started with pid=8, OS id=18061
DBW0 started with pid=9, OS id=18063
LGWR started with pid=10, OS id=18065
CKPT started with pid=11, OS id=18067
SMON started with pid=12, OS id=18069
RECO started with pid=13, OS id=18071
CJQ0 started with pid=14, OS id=18073
MMON started with pid=15, OS id=18075
MMNL started with pid=16, OS id=18077

每個后臺進程與Oracle數據庫的不同部分交互。其中,SMON、PMON、DBWn、CKPT、LGWR是五個必需的Oracle后臺進程。

3.4.1 進程監視器

進程監視器(Process Monitor, PMON)主要監視服務器進程。在專有服務器體系模式下,用戶進程和服務器進程是一對一的關系,如果某個會話發生異常,PMON會銷毀對應的服務器進程、回滾未提交的事務并回收會話專有的PGA內存區域。例如,如果因某些原因專用服務“故障”或被“殺死”,PMON就負責處理(恢復或回滾工作)和釋放資源。PMON將發出未提交工作的回滾、釋放鎖和釋放分配給故障進程的SGA資源。

除了在異常中斷之后的清理外,PMON監控其他Oracle后臺進程,如果有必要(和有可能)就重新啟動。如果共享服務或一個分配器故障(崩潰),PMON將插手并且重啟另一個(在清理故障進程之后)。PMON將觀察所有Oracle進程,只要合適或重啟它們或中止進程。例如,在數據庫日志寫進程事件中的LGWR故障、實例故障。這是一個嚴重的錯誤,最安全的處理方法就是去立即終止實例,讓正常的恢復處理數據。

提示

這是很少發生的事情,應該立即報告Oracle支持。

PMON為實例做的另一件事是去使用Oracle TNS監聽器登記。當一個實例開啟時,PMON進程投出眾所周知的端口地址,除非指向其他,來看監聽器是否正在開和運行著。眾所周知,默認端口是使用1521。現在,如果監聽器在一些不同端口開啟會發生什么?這種情況下,機制是相同的,除了監聽器地址需要被LOCAL_LISTENER參數明確指定。如果監聽器運行在庫實例開啟時,PMON和監聽器通信,傳到它相關參數,譬如服務器名和實例的負載度量。如果監聽器沒被開啟,PMON將周期性地試著和它聯系來登記自己。

3.4.2 系統監視器

系統監控后臺進程(System Monitor Process, SMON)有時也被叫作system cleanup process,這么叫的原因是它負責完成很多清理(cleanup)任務。安裝和打開數據庫,實例恢復是由此進程完成的。

這個進程對于Oracle數據庫來說,可以利用一句話來概括,即人小鬼大。其負責的內容并不是很多,但是對于數據的安全與數據庫的性能卻有很關鍵的作用。如隨著表空間中的數據不斷的建立、刪除、更新等,在表空間中難免會產生碎片。由于這些碎片的存在,數據庫的性能會逐漸降低,而系統監視進程SMON正好可以解決這些碎片。SMON進程會將各個表空間的空閑碎片合并在一起,讓數據庫系統更加容易分配,從而提高數據庫的性能。

另外,在數據庫運行的過程中,會因為斷電或者其他的原因而發生故障。此時由于數據高速緩存中的臟緩存塊還沒有來得及寫入到數據文件中,從而導致數據的丟失。在數據庫啟動時,系統監視進程SMON會在下一次啟動例程時自動讀取重做日志文件并對數據庫進行恢復。也就是說,進行將已提交的事務寫入數據文件(已經寫入到日志文件中而沒有寫入到數據文件中的數據)、回退未提交的事務操作。可見,SMON進程在Oracle數據庫中是一個比較小但是卻非常重要的角色。

在管理這個進程時,主要需要注意兩個問題。

(1)啟動的時機。一般情況下,例程重新啟動時,會啟動這個系統監視進程。然后在這個例程運行期間,這個進程也會被系統定期喚醒,然后會檢查是否有工作需要其完成。最重要的是,在有需要時,數據庫管理員可以通過其他進程來啟動這個SMON系統監視進程來完成一些特定的工作。

(2)表空間配置對這個進程的影響。在表空間管理中,有一個參數叫作PCTINCREASE。如果將這個參數設置為0的話,則這個SMON系統監視進程對于這個表空間的作用就要打折扣了。在設置為0的情況下,SMON進程就不會對這個表空間中的空閑碎片進行整理、合并操作。也就是說,需要數據庫管理員通過數據的導出導入等手工操作才能夠解決表空間的碎片問題。顯然這會增加數據庫管理員的工作量。為此建議,除非有特別的需要,不要將這個參數設置為0。讓SMON進程自動對表空間中的碎片進行管理,自動合并表空間中的空閑碎片。不將某個表空間中的這個參數設置為0的話,也不會影響到系統監視進程的其他用途,如不會影響到在例程非正常關閉時對數據的恢復操作。即這個參數設置為0,在有需要時仍然可以利用重做日志文件中的記錄來恢復相關的數據。

3.4.3 檢查點管理進程

檢查點管理進程(Checkpoint Process, CKPT)負責發起檢查點信號。

【示例3-3】手動設置檢查點

SQL>alter system checkpoint;

檢查點可強制DBWn寫入臟緩沖區,當數據庫崩潰后,由于大量臟緩沖區未寫入數據文件,在重新啟動時,需要由SMON進行實例恢復,實例恢復需要提取和應用重做日志記錄,提取的位置就是從上次檢查點發起的位置開始的(檢查點之前的數據已經被強制寫入到數據文件中),這個位置稱為RBA(Redo Byte Address)。CKPT會不斷將這個位置更新到控制文件中去(以確定實例恢復需要從哪兒開始提取日志記錄)。

3.4.4 數據庫寫進程

上文在講解Oracle內存結構時,已經就數據庫進程(Database Writer, DBWn)的作用做了介紹。該進程負責將臟數據塊寫入磁盤。它是一個非常重要的進程,隨著內存的不斷增加,一個DBWn進程可能不夠用了。所以從Oracle 8i起就可以為系統配置多個DBWn進程。初始化參數db_writer_processe決定了啟動多少個DBWn進程。每個DBWn進程都會分配一個cache buffers lru chain latch。

DBWn作為一個后臺進程,只有在某些條件滿足了才會觸發。這些條件包括:

? 當進程在LRU鏈表掃描以查找可以覆蓋的buffer header時,如果已經掃描的buffer header的數量到達一定的限度時,觸發DBWn進程。

? 如果臟數據塊的總數超過一定限度,也將觸發DBWn進程。

? 發生檢查點(包括增量檢查點(Incremental Checkpoint)和完全檢查點(Complete Checkpoint))時觸發DBWn。

? 每隔三秒鐘啟動一次DBWn。

3.4.5 日志寫進程

日志寫進程(Log Writer, LGWR)也是一種后臺進程,主要負責將日志緩沖內容寫到磁盤的在線重做日志文件或組中。DBWn將dirty塊寫到磁盤之前,所有與buffer修改相關的redo log都需要由LGWR寫入磁盤的在線重做日志文件(組)。如果未寫完,那么DBWn會等待LGWR,也會產生一些相應的等待事件(例如,log file prarllel write,后面單獨作為話題再聊)。總之,這樣做的目的就是為了當crash時,可以有恢復之前操作的可能,也是Oracle在保持交易完整性方面的一個機制。

該進程有如下幾方面的特點:

(1)LGWR寫日志是順序寫,這就解釋了一個Orace Server只能有一個LGWR進程,不能像DBWR那樣可以有多個,否則就無法保證順序寫的機制,而且可能會產生鎖的問題。

(2)用戶進程每次修改內存數據塊時,都會在日志緩沖區(redo buffer)中構造一個相應的重做條目(redo entry),它記錄了被修改數據塊修改之前和之后的值。

(3)LGWR將redo entry寫入聯機日志文件的情況可以概括為兩種:后臺寫和同步寫,或者說異步寫和同步寫。

① 后臺寫的條件如下:

? 每3秒LGWR啟動一次。

? DBWR啟動時如果發現dirty塊對應的redo entry還沒寫入聯機日志文件,則DBWR觸發LGWR進程并等待LGWR完成后繼續。

? redo entry數量達到整個log buffer的1/3時,觸發LGWR。

? redo entry的數量達到1MB。

② 同步寫的條件是:執行commit時,必須等待log buffer進行flushing操作(可能產生log file sync等待事件),寫入磁盤中的聯機日志文件。一般上述1/3滿的條件觸發LGWR,幾乎強制LGWR實時寫,因此當需要執行commit時,可能沒有任何redo entry需要寫入了。

(4)3秒觸發LGWR的規則,事實上,這個超時是DBWR的,但是因為LGWR總在DBWR調用之前執行,因此效果上也相當于LGWR的超時是3秒即調用。

3.4.6 管理監控進程

管理監控進程(Manageability Monitor, MMON)是數據庫的自我監視和自我調整的支持進程。實例在運行中,會收集大量有關實例活動和性能的統計數據,這些數據會收集到SGA中,MMON定期從SGA中捕獲這些統計數據,并將其寫入到數據字典中,便于后續對這些快照進行分析。(默認情況下,MMON每隔一個小時收集一次快照)。

3.4.7 歸檔進程

歸檔進程(Archiver, ARCn)是可選的。在重做日志文件管理中,有歸檔與非歸檔兩種模式。如果數據庫配置為歸檔模式,這個進程就是必需的。所謂歸檔,就是將重做日志文件永久保存(生產庫一般都會配置為歸檔模式)到歸檔日志文件中。歸檔日志文件和重做日志文件的作用是一樣的,只不過重做日志文件會不斷被重寫,而歸檔日志文件則保留了關于數據更改的完整的歷史記錄。

在日志進行切換時,如果不對原先的日志文件進行歸檔,而直接覆蓋的話,就叫作非歸檔模式。相反,在寫入下一個日志文件時,會先對目標日志文件進行歸檔,這就叫作歸檔模式。歸檔進程ARCH就是負責在重做日志文件切換后將已經寫滿的重做日志文件復制到歸檔日志文件中,以防止循環寫入重做日志文件時將其覆蓋。

所以說,只有數據庫運行在歸檔模式時,這個ARCH進程才會被啟動。在任何一種操作模式下,重做日志文件都會被循環使用。所以當LGWR進程在進行日志切換,需要用到下一個日志文件時,數據庫會被暫時掛起,進行目標日志文件的歸檔工作。直到這個目標重做日志文件歸檔完畢后,數據庫才會恢復正常。所以說,歸檔日志的操作有時候也會影響數據庫的性能,特別是當需要進行頻繁的大批量數據更改時。

那么有什么方法可以提高歸檔作業的效率呢?如下一些建議可供數據庫管理員參考。

(1)增加歸檔進程的個數。

在默認情況下,一個例程只會啟動一個歸檔進程ARCH。當ARCH進程正在歸檔一個重做日志文件時,任何其他的進程都不能夠訪問這個重做日志文件。在Oracle數據庫中,可以根據需要啟動多個歸檔進程ARCH。在Oracle數據庫中,啟動多個歸檔進程時分為手工與自動兩個方式。為了提高重做日志文件歸檔的速度,當用戶進程發生比較長時間的等待時,LGWR進程會根據時機情況來自動啟動多個歸檔進程。在Oracle數據庫中,其最多可以啟動十個歸檔進程。另外,如果數據庫管理員在部署數據庫時,估計日志歸檔作業會影響到數據庫的性能,就可以手工來啟動多個歸檔進程。這是通過初始化參數LOG_ARCHIVE_MAX_PROCESSES來確定的。可以將這個參數設置為大于1的數值(注意不能夠超過9個歸檔進程)。如此的話,數據庫在創建例程時就會啟動多個歸檔進程。不過還是傾向于讓數據庫系統來自動管理這個進程。數據庫管理員最好不要干涉。

另外,需要注意ARCH歸檔進程個數與DBWR進程個數的區別。默認情況下,DBWR進程也只有一個。為了提高數據庫的性能,可以根據情況增加DBWR進程的個數。不過其增加時受到CPU數量的限制,即一個DBWR進程需要使用一個獨立的CPU。如果想啟動三個DBWR進程的話,就必須采用3個CPU處理器。對于ARCH歸檔進程來說,則沒有這個限制。即使只有一個CPU處理器,其也可以啟動三個甚至更多的ARCH進程。

(2)增加重做日志文件來延長歸檔日志進程啟動的時間間隔。

通常情況下,只有當前一個重做日志文件寫滿、需要進行日志切換時,才會觸發這個ARCH歸檔日志進程。所以如果重做文件比較大,其日志切換的時間間隔就會延長,ARCH歸檔日志進程的啟動時間間隔也會比較長。所以說,通過調整重做日志文件的大小,可以延長歸檔進程啟動的時間間隔,從而降低因為歸檔進程啟動而對數據庫性能造成的負面影響。

(3)在數據庫初始化的過程中,可能需要導入大量的數據。

此時會對數據庫中的數據進行大量的插入、刪除、更新等操作,從而導致重做日志文件切換頻繁。這就會導致數據庫需要頻繁啟動ARCH歸檔進程。

數據庫大量的更新操作、重做日志文件(LGWR進程)、歸檔重做日志文件(ARCH)進程之間就形成了一條無形的鏈條。由于“蝴蝶效應”,從而降低了數據庫的性能。為此在必要時,需要砍斷這根鏈條,以提高數據庫的性能。例如,可以在數據大量導入、更新、刪除時,不往日志文件中插入記錄,或者臨時增加重做日志文件的空間。如此的話,在進行這些操作時就可以避免進行重做日志切換或者延長重做日志切換的時間間隔,從而使ARCH歸檔日志進程也可以避免或者延長其時間間隔,從而提高數據庫的性能。當數據庫初始化完成之后,再將其恢復過來。這些臨時性的調整雖然比較麻煩,但是可以提高數據庫的性能。為此認為這是值得的。

Oracle是如何保存數據的呢?眾所周知,內存的數據處理速度是比物理磁盤快很多的,Oracle數據庫的所有數據更改都在內存中完成,當然,在某些情況下,某些讀取類的操作是不會經過SGA內存區域的,而是選擇直接從硬盤將數據獲取并返回給用戶。在實際的數據庫管理工作中,管理員更多的工作是與內存中的數據相關。數據庫是存放數據的容器,那么數據是如何放進這個容器中的呢?內存數據塊寫入數據文件其實是一個相當復雜的過程,在這個過程中,首先要保證安全。所謂安全,就是在寫的過程中,一旦發生實例崩潰,要有一套完整的機制能夠保證用戶已經提交的數據不會丟失;其次,在保證安全的基礎上,要盡可能地提高效率。眾所周知,I/O操作是最昂貴的操作,所以應該盡可能地將臟數據塊收集到一定程度以后,再批量寫入磁盤中。

直觀上最簡單的解決方法就是,每當用戶提交時就將所改變的內存數據塊交給DBWn,由其寫入數據文件。這樣的話,一定能夠保證提交的數據不會丟失,但是這種方式效率最為低下,在高并發環境中,一定會引起I/O方面的爭用。Oracle當然不會采用這種沒有伸縮性的方式。Oracle引入了CKPT和LGWR這兩個后臺進程,這兩個進程與DBWn進程互相合作,提供了既安全又高效的寫臟數據塊的解決方法。

用戶進程每次修改內存數據塊時,都會在日志緩沖區(log buffer)中構造一個相應的重做條目(redo entry),該重做條目描述了被修改的數據塊在修改之前和修改之后的值,而LGWR進程則負責將這些重做條目寫入聯機日志文件。只要重做條目進入了聯機日志文件,那么數據的安全就有保障了,否則這些數據都是有安全隱患的。LGWR是一個必須和前臺用戶進程通信的進程。LGWR承擔了維護系統數據完整性的任務,它保證了數據在任何情況下都不會丟失。

假如DBWR在寫臟數據塊的過程中突然發生實例崩潰該怎么辦呢?用戶提交時,Oracle是不一定會把提交的數據塊寫入數據文件的。那么實例崩潰時,必然會有一些已經提交但是還沒有被寫入數據文件的內存數據塊丟失了。當實例再次啟動時,Oracle需要利用日志文件中記錄的重做條目在buffer cache中重新構造出被丟失的數據塊,從而完成前滾和回滾的工作,并將丟失的數據塊找回來。于是這里就存在一個問題,就是Oracle在日志文件中找重做條目時,到底應該找哪些重做條目?換句話說,應該在日志文件中從哪個起點開始往后應用重做條目?注意,這里所指的日志文件可能不止一個日志文件。

因為需要預防隨時可能的實例崩潰現象,所以Oracle在數據庫的正常運行過程中會不斷地定位這個起點,以便在不可預期的實例崩潰中能夠最有效地保護并恢復數據。同時,這個起點的選擇非常有講究。首先,這個起點不能太靠近日志文件的頭部,太靠近日志文件頭部意味著要處理很多的重做條目,這樣會導致實例再次啟動時所進行恢復的時間太長;其次,這個起點也不能太靠近日志文件的尾部,太靠近日志文件的尾部說明只有很少的臟數據塊沒有被寫入數據文件,也就是說前面已經有很多臟數據塊被寫入了數據文件,那也就意味著只有在DBWn進程很頻繁地寫數據文件的情況下,才能使得buffer cache中所殘留的臟數據塊的數量很少。但很明顯,DBWn寫得越頻繁,所占用寫數據文件的I/O就越嚴重,那么留給其他操作(比如讀取buffer cache中不存在的數據塊等)的I/O資源就越少。這顯然也是不合理的。

從這里也可以看出,這個起點實際上說明了在日志文件中位于這個起點之前的重做條目所對應的在buffer cache中的臟數據塊已經被寫入了數據文件,從而在實例崩潰以后的恢復中不需要去考慮,而這個起點以后的重做條目所對應的臟數據塊實際還沒有被寫入數據文件。在實例崩潰以后的恢復中,需要從這個起點開始往后依次取出日志文件中的重做條目進行恢復。考慮到目前的內存容量越來越大,buffer cache也越來越大,buffer cache中包含幾百萬個內存數據塊也是很正常的現象,如何才能最有效地來定位這個起點呢?

為了能夠確定這個最佳的起點,Oracle引入了名為CKPT的后臺進程,通常也叫作檢查點進程(Checkpoint Process)。這個進程與DBWn共同合作,從而確定這個起點。同時,這個起點也有一個專門的名字,叫作檢查點位置(Checkpoint Position,該檢查點位置記錄在控制文件里)。

Oracle為了在檢查點的算法上更加具有可擴展性(也就是為了能夠在巨大的buffer cache下依然有效工作),引入了檢查點隊列(Checkpoint Queue),該隊列上串起來的都是臟數據塊所對應的buffer header。每次DBWn寫臟數據塊時,也是從檢查點隊列上掃描臟數據塊,并將這些臟數據塊實際寫入數據文件的。當寫完以后,DBWn會將這些已經寫入數據文件的臟數據塊從檢查點隊列上摘下來。

這樣即便是在巨大的buffer cache下工作,CKPT也能夠快速地確定哪些臟數據塊已經被寫入了數據文件,而哪些還沒有寫入數據文件。顯然,只要在檢查點隊列上的數據塊就都是還沒有寫入數據文件的臟數據塊。而且,為了更加有效地處理單實例和多實例(RAC)環境下的表空間的檢查點處理,比如將表空間設置為離線狀態或者為熱備份狀態等,Oracle還專門引入了文件隊列(File Queue)。文件隊列的原理與檢查點隊列是一樣的,只不過每個數據文件會有一個文件隊列,該數據文件所對應的臟數據塊會被串在同一個文件隊列上;同時為了能夠盡量減少實例崩潰后恢復的時間,Oracle還引入了增量檢查點(Incremental Checkpoint),從而增加了檢查點啟動的次數。

如果每次檢查點啟動的間隔時間過長的話,再加上內存很大,可能會使得恢復的時間過長。因為前一次檢查點啟動以后,標識出了這個起點。然后在第二次檢查點啟動之前,DBWn可能已經將很多臟數據塊寫入了數據文件,而假如在第二次檢查點啟動之前發生實例崩潰,導致在日志文件中所標識的起點仍然是上一次檢查點啟動時所標識的,導致Oracle不知道這個起點以后的很多重做條目所對應的臟數據塊實際上已經寫入了數據文件,從而使得Oracle在實例恢復時重復地處理一遍,效率低下,浪費時間。

上面說到了有關CKPT的兩個重要概念:檢查點隊列(包括文件隊列)和增量檢查點。檢查點隊列上的buffer header是按照數據塊第一次被修改的時間先后順序來排列的。越早修改的數據塊的buffer header排在越前面,同時如果一個數據塊被修改了多次的話,在該鏈表上也只出現一次。而且,檢查點隊列上的buffer header還記錄了臟數據塊在第一次被修改時所對應的重做條目在重做日志文件中的地址,也就是LRBA(Low Redo Block Address)。Low表示第一次修改時對應的RBA。每個檢查點都會由checkpoint queue latch來保護。

而增量檢查點是從Oracle 8i開始出現的,是相對于Oracle 8i之前的完全檢查點(Complete Checkpoint)而言的。完全檢查點啟動時,會標識出buffer cache中所有的臟數據塊,然后以最高優先級啟動DBWn進程將這些臟數據塊寫入數據文件。Oracle 8i之前,日志切換時會觸發完全檢查點。到了Oracle 8i及以后,完全檢查點只有在兩種情況下才會被觸發:

? 發出alter system checkpoint命令。

? 除了shutdown abort以外的正常關閉數據庫。

提示

日志切換不會觸發完全檢查點,而是觸發增量檢查點。自Oracle 8i所引入的增量檢查點每隔三秒鐘或發生日志切換時啟動。它啟動時只做一件事情:找出當前檢查點隊列上的第一個buffer header,并將該buffer header中所記錄的LRBA(這個LRBA也就是checkpoint position)記錄到控制文件中去。如果是由日志切換所引起的增量檢查點,則還會將checkpoint position記錄到每個數據文件頭中。也就是說,如果這個時候發生實例崩潰,Oracle在下次啟動時就會到控制文件中找到這個checkpoint position作為日志文件的起點,然后從這個起點開始向后依次取出每個重做條目進行處理。

上面所描述的概念,用一句話來概括,其實就是DBWn負責寫檢查點隊列上的臟數據塊,而CKPT負責記錄當前檢查點隊列的第一個數據塊所對應的重做條目在日志文件中的地址,而到底應該寫哪些臟數據塊、寫多少臟數據塊則要到檢查點隊列上才能確定。

主站蜘蛛池模板: 灵石县| 山东省| 从化市| 江北区| 开封市| 兴隆县| 霍山县| 攀枝花市| 桦南县| 辉县市| 西畴县| 阳曲县| 锡林郭勒盟| 内乡县| 泰安市| 綦江县| 贺兰县| 城固县| 务川| 华阴市| 保定市| 清河县| 灌南县| 永年县| 固安县| 布拖县| 扶绥县| 济阳县| 吴桥县| 延长县| 宣武区| 崇仁县| 宜都市| 平谷区| 祁阳县| 嫩江县| 黔西县| 临安市| 米脂县| 宜春市| 阿勒泰市|