php線程同步,php異步多線程

本文目錄一覽:

php 如何在多核服務器上發揮性能

IBM有篇文章簡述線程數,CPU數量(核心)對性能的影響曲線

不過是一年前看到的,剛去找了找沒找到…我僅就我記得的一部分說下

IBM根據大量實際任務中的數據畫了個曲線圖(所統計的程序都是能儘可能多的利用cpu核心的程序)

在4個核心以內的機器上運行,程序性能幾乎與核心數成正比

而在4-8個核心的機器上,其性能雖然也隨核心數量增長而增長,但增長幅度一步步減弱

直到8個核心及以後,即便再增加核心,性能幾乎不會再增長.(一般的程序比這更糟糕)

——(以上這部分我記得比較清,下邊的比較模糊)——-

原因是同步等等因素的存在,導致cup核心數量在增長到一定程度後,多線程已經無法完全把cpu的性能發揮出來了

後來,文章說了一些規則以及一些新的關於鎖的算法等等,卻依然只說有一定的幫助,但效果不是很明顯…

——(這部分是個人意見)——

然後再來說說線程數量的問題

不同的操作系統所允許一個進程的線程數量不同

而靠增加線程數量是比較好的完全利用上cpu資源的手段

但是假設是只有一個核心的cpu,一個程序開單線程就可以完全耗盡cpu資源,那麼它開雙線程就無法獲得性能的提升,甚至由於啟動一個線程的開銷,性能還會有下降.

那麼,是不是有多少個核心就只開多少線程就是最佳效果,倒也不至於…因為現在的cpu資源,普通程序,單靠一個線程很難利用完…那就可以再多開幾個互不影響的線程來壓榨cpu,具體多開的比例是多少,可以看看單線程對cpu的利用率以及實際情況而定…

不過,再來看看IBM的那個曲線圖,恐怕也就8個線程的樣子(這還得你優化的好),再往後提升就不大了…

但是,是不是就只能開這麼多,那肯定不是

比如你開啟某個服務,給很多人用,你是只允許每次8個人同時使用來獲得最優整體性能,還是犧牲一部分整體性能來服務更多的人?

只是個人見解…如有錯誤還請包涵指正

servlet調用dao和service為什麼要用實例變量

Servlet 單例多線程

Servlet如何處理多個請求訪問?

Servlet容器默認是採用單實例多線程的方式處理多個請求的:

1.當web服務器啟動的時候(或客戶端發送請求到服務器時),Servlet就被加載並實例化(只存在一個Servlet實例);

2.容器初始化化Servlet主要就是讀取配置文件(例如tomcat,可以通過servlet.xml的Connector設置線程池中線程數目,初始化線程池通過web.xml,初始化每個參數值等等。

3.當請求到達時,Servlet容器通過調度線程(Dispatchaer Thread) 調度它管理下線程池中等待執行的線程(Worker Thread)給請求者;

4.線程執行Servlet的service方法;

5.請求結束,放回線程池,等待被調用;

(注意:避免使用實例變量(成員變量),因為如果存在成員變量,可能發生多線程同時訪問該資源時,都來操作它,照成數據的不一致,因此產生線程安全問題)

從上面可以看出:

第一:Servlet單實例,減少了產生servlet的開銷;

第二:通過線程池來響應多個請求,提高了請求的響應時間;

第三:Servlet容器並不關心到達的Servlet請求訪問的是否是同一個Servlet還是另一個Servlet,直接分配給它一個新的線程;如果是同一個Servlet的多個請求,那麼Servlet的service方法將在多線程中並發的執行;

第四:每一個請求由ServletRequest對象來接受請求,由ServletResponse對象來響應該請求;

Servlet/JSP技術和ASP、PHP等相比,由於其多線程運行而具有很高的執行效率。由於Servlet/JSP默認是以多線程模式執行的,所以,在編寫代碼時需要非常細緻地考慮多線程的安全性問題。 

JSP的中存在的多線程問題: 

當客戶端第一次請求某一個JSP文件時,服務端把該JSP編譯成一個CLASS文件,並創建一個該類的實例,然後創建一個線程處理CLIENT端的請求。如果有多個客戶端同時請求該JSP文件,則服務端會創建多個線程。每個客戶端請求對應一個線程。以多線程方式執行可大大降低對系統的資源需求,提高系統的並發量及響應時間. 

對JSP中可能用的的變量說明如下: 

實例變量: 實例變量是在堆中分配的,並被屬於該實例的所有線程共享,所以不是線程安全的. 

JSP系統提供的8個類變量 

JSP中用到的OUT,REQUEST,RESPONSE,SESSION,CONFIG,PAGE,PAGECONXT是線程安全的(因為每個線程對應的request,respone對象都是不一樣的,不存在共享問題), APPLICATION在整個系統內被使用,所以不是線程安全的. 

局部變量: 局部變量在堆棧中分配,因為每個線程都有它自己的堆棧空間,所以是線程安全的. 

靜態類: 靜態類不用被實例化,就可直接使用,也不是線程安全的. 

外部資源: 在程序中可能會有多個線程或進程同時操作同一個資源(如:多個線程或進程同時對一個文件進行寫操作).此時也要注意同步問題. 

使它以單線程方式執行,這時,仍然只有一個實例,所有客戶端的請求以串行方式執行。這樣會降低系統的性能 

問題 

問題一. 說明其Servlet容器如何採用單實例多線程的方式來處理請求 

問題二. 如何在開發中保證servlet是單實例多線程的方式來工作(也就是說如何開發線程安全的servelt)。 

一. Servlet容器如何同時來處理多個請求 

Java的內存模型JMM(Java Memory Model) 

JMM主要是為了規定了線程和內存之間的一些關係。根據JMM的設計,系統存在一個主內存(Main Memory),Java中所有實例變量都儲存在主存中,對於所有線程都是共享的。每條線程都有自己的工作內存(Working Memory),工作內存由緩存和堆棧兩部分組成,緩存中保存的是主存中變量的拷貝,緩存可能並不總和主存同步,也就是緩存中變量的修改可能沒有立刻寫到主存中;堆棧中保存的是線程的局部變量,線程之間無法相互直接訪問堆棧中的變量。根據JMM,我們可以將論文中所討論的Servlet實例的內存模型抽象為圖所示的模型。 

 

工作者線程Work Thread:執行代碼的一組線程。 

調度線程Dispatcher Thread:每個線程都具有分配給它的線程優先級,線程是根據優先級調度執行的。 

Servlet採用多線程來處理多個請求同時訪問。servlet依賴於一個線程池來服務請求。線程池實際上是一系列的工作者線程集合。Servlet使用一個調度線程來管理工作者線程。 

當容器收到一個Servlet請求,調度線程從線程池中選出一個工作者線程,將請求傳遞給該工作者線程,然後由該線程來執行Servlet的service方法。當這個線程正在執行的時候,容器收到另外一個請求,調度線程同樣從線程池中選出另一個工作者線程來服務新的請求,容器並不關心這個請求是否訪問的是同一個Servlet.當容器同時收到對同一個Servlet的多個請求的時候,那麼這個Servlet的service()方法將在多線程中並發執行。 

Servlet容器默認採用單實例多線程的方式來處理請求,這樣減少產生Servlet實例的開銷,提升了對請求的響應時間,對於Tomcat可以在server.xml中通過Connector元素設置線程池中線程的數目。 

就實現來說: 

調度者線程類所擔負的責任如其名字,該類的責任是調度線程,只需要利用自己的屬性完成自己的責任。所以該類是承擔了責任的,並且該類的責任又集中到唯一的單體對象中。而其他對象又依賴於該特定對象所承擔的責任,我們就需要得到該特定對象。那該類就是一個單例模式的實現了。 

注意:服務器可以使用多個實例來處理請求,代替單個實例的請求排隊帶來的效益問題。服務器創建一個Servlet類的多個Servlet實例組成的實例池,對於每個請求分配Servlet實例進行響應處理,之後放回到實例池中等待下此請求。這樣就造成並發訪問的問題。 

此時,局部變量(字段)也是安全的,但對於全局變量和共享數據是不安全的,需要進行同步處理。而對於這樣多實例的情況SingleThreadModel接口並不能解決並發訪問問題。 SingleThreadModel接口在servlet規範中已經被廢棄了。

二 如何開發線程安全的Servlet 

1、實現 SingleThreadModel 接口 

該接口指定了系統如何處理對同一個Servlet的調用。如果一個Servlet被這個接口指定,那麼在這個Servlet中的service方法將不會有兩個線程被同時執行,當然也就不存在線程安全的問題。這種方法只要將前面的Concurrent Test類的類頭定義更改為: 

Public class Concurrent Test extends HttpServlet implements SingleThreadModel { 

………… 

2、同步對共享數據的操作 

使用synchronized 關鍵字能保證一次只有一個線程可以訪問被保護的區段,在本論文中的Servlet可以通過同步塊操作來保證線程的安全。同步後的代碼如下: 

………… 

Public class Concurrent Test extends HttpServlet { ………… 

Username = request.getParameter (“username”); 

Synchronized (this){ 

Output = response.getWriter (); 

Try { 

Thread. Sleep (5000); 

} Catch (Interrupted Exception e){} 

output.println(“用戶名:”+Username+”BR”); 

3、避免使用實例變量 

本實例中的線程安全問題是由實例變量造成的,只要在Servlet裡面的任何方法裡面都不使用實例變量,那麼該Servlet就是線程安全的。 

修正上面的Servlet代碼,將實例變量改為局部變量實現同樣的功能,代碼如下: 

…… 

Public class Concurrent Test extends HttpServlet {public void service (HttpServletRequest request, HttpServletResponse 

Response) throws ServletException, IOException { 

Print Writer output; 

String username; 

Response.setContentType (“text/html; charset=gb2312”); 

…… 

 ** 對上面的三種方法進行測試,可以表明用它們都能設計出線程安全的Servlet程序。但是,如果一個Servlet實現了SingleThreadModel接口,Servlet引擎將為每個新的請求創建一個單獨的Servlet實例,這將引起大量的系統開銷。SingleThreadModel在Servlet2.4中已不再提倡使用;同樣如果在程序中使用同步來保護要使用的共享的數據,也會使系統的性能大大下降。這是因為被同步的代碼塊在同一時刻只能有一個線程執行它,使得其同時處理客戶請求的吞吐量降低,而且很多客戶處於阻塞狀態。另外為保證主存內容和線程的工作內存中的數據的一致性,要頻繁地刷新緩存,這也會大大地影響系統的性能。所以在實際的開發中也應避免或最小化 Servlet 中的同步代碼;在Serlet中避免使用實例變量是保證Servlet線程安全的最佳選擇。從Java 內存模型也可以知道,方法中的臨時變量是在棧上分配空間,而且每個線程都有自己私有的棧空間,所以它們不會影響線程的安全。

更加詳細的說明:

1,變量的線程安全:這裡的變量指字段和共享數據(如表單參數值)。 

a,將 參數變量 本地化。多線程並不共享局部變量.所以我們要儘可能的在servlet中使用局部變量。 

例如:String user = “”; 

user = request.getParameter(“user”); 

b,使用同步塊Synchronized,防止可能異步調用的代碼塊。這意味着線程需要排隊處理。在使用同板塊的時候要儘可能的縮小同步代碼的範圍,不要直接在sevice方法和響應方法上使用同步,這樣會嚴重影響性能。 

2,屬性的線程安全:ServletContext,HttpSession,ServletRequest對象中屬性。 

ServletContext:(線程是不安全的) 

ServletContext是可以多線程同時讀/寫屬性的,線程是不安全的。要對屬性的讀寫進行同步處理或者進行深度Clone()。所以在Servlet上下文中儘可能少量保存會被修改(寫)的數據,可以採取其他方式在多個Servlet中共享,比方我們可以使用單例模式來處理共享數據。 

HttpSession:(線程是不安全的) 

HttpSession對象在用戶會話期間存在,只能在處理屬於同一個Session的請求的線程中被訪問,因此Session對象的屬性訪問理論上是線程安全的。 

當用戶打開多個同屬於一個進程的瀏覽器窗口,在這些窗口的訪問屬於同一個Session,會出現多次請求,需要多個工作線程來處理請求,可能造成同時多線程讀寫屬性。這時我們需要對屬性的讀寫進行同步處理:使用同步塊Synchronized和使用讀/寫器來解決。 

ServletRequest:(線程是安全的) 

對於每一個請求,由一個工作線程來執行,都會創建有一個新的ServletRequest對象,所以ServletRequest對象只能在一個線程中被訪問。ServletRequest是線程安全的。注意:ServletRequest對象在service方法的範圍內是有效的,不要試圖在service方法結束後仍然保存請求對象的引用。 

3,使用同步的集合類: 

使用Vector代替ArrayList,使用Hashtable代替HashMap。 

4,不要在Servlet中創建自己的線程來完成某個功能。 

Servlet本身就是多線程的,在Servlet中再創建線程,將導致執行情況複雜化,出現多線程安全問題。 

5,在多個servlet中對外部對象(比方文件)進行修改操作一定要加鎖,做到互斥的訪問。 

6,javax.servlet.SingleThreadModel接口是一個標識接口,如果一個Servlet實現了這個接口,那Servlet容器將保證在一個時刻僅有一個線程可以在給定的servlet實例的service方法中執行。將其他所有請求進行排隊。 

PS:

Servlet並非只是單例的. 當container開始啟動,或是客戶端發出請求服務時,Container會按照容器的配置負責加載和實例化一個Servlet(也可以配置為多個,不過一般不這麼干).不過一般來說一個servlet只會有一個實例。

1) Struts2的Action是原型,非單實例的;會對每一個請求,產生一個Action的實例來處理。 

2) Struts1的Action,Spring的Ioc容器管理的bean 默認是單實例的. 

Struts1 Action是單實例的,spring mvc的controller也是如此。因此開發時要求必須是線程安全的,因為僅有Action的一個實例來處理所有的請求。單例策略限制了Struts1 Action能作的事,並且要在開發時特別小心。Action資源必須是線程安全的或同步的。 

Spring的Ioc容器管理的bean 默認是單實例的。

Struts2 Action對象為每一個請求產生一個實例,因此沒有線程安全問題。(實際上,servlet容器給每個請求產生許多可丟棄的對象,並且不會導致性能和垃圾回收問題)。

當Spring管理Struts2的Action時,bean默認是單實例的,可以通過配置參數將其設置為原型。(scope=”prototype )

Servlet的生命周期:

1.      Servlet在web服務器啟動時被加載並實例化,容器運行其init方法初始化,請求到達時運行其service方法;

2.      service運行請求對應的doXXX(doGet,doPost)方法;

3.      服務器銷毀實例,運行其destory方法;

Servlet的生命周期由Servlet容器管理;

(三個概念的理解:

Servlet容器Web容器應用服務器?

Servlet容器的主要任務就是管理Servlet的生命周期;

Web容器也稱之為web服務器,主要任務就是管理和部署web應用的;

應用服務器的功能非常強大,不僅可以管理和部署web應用,也可以部署EJB應用,實現容器管理的事務等等。。。

Web服務器就是跟基於HTTP的請求打交道,而EJB容器更多是跟數據庫,事務管理等服務接口交互,所以應用服務器的功能是很多的。

常見的web服務器就是Tomcat,但Tomcat同樣也是Servlet服務器;

常見的應用服務器有WebLogic,WebSphere,但都是收費的;

沒有Servlet容器,可以用Web容器直接訪問靜態Html頁面,比如安裝了apache等;如果需要顯示Jsp/Servlet,就需要安裝一個Servlet容器;但是光有servlet容器也是不夠的,它需要被解析為html顯示,所以仍需要一個web容器;所以,我們常把web容器和Servlet容器視為一體,因為他們兩個容器都有對方的功能實現了,都沒有獨立的存在了,比如tomcat!

Servlet是如何處理多個請求同時訪問呢?

Servlet容器默認是採用單實例多線程的方式處理多個請求的:

1.      當web服務器啟動的時候(或客戶端發送請求到服務器時),Servlet就被加載並實例化(只存在一個Servlet實例);

2.      容器初始化Servlet。主要就是讀取配置文件(例如tomcat,可以通過servlet.xml的Connector設置線程池中線程數目,初始化線程池;通過web.xml,初始化每個參數值等等);

3.      當請求到達時,Servlet容器通過調度線程(Dispatchaer Thread)調度它管理下的線程池中等待執行的線程(Worker Thread)給請求者;

4.      線程執行Servlet的service方法;

5.      請求結束,放回線程池,等到被調用;

從上面可以看出:

第一:Servlet單實例,減少了產生servlet的開銷;

第二:通過線程池來響應多個請求,提高了請求的響應時間;

第三:Servlet容器並不關心到達的Servlet請求訪問的是否是同一個Servlet還是另一個Servlet,直接分配給它一個新的線程;如果是同一個Servlet的多個請求,那麼Servlet的service方法將在多線程中並發的執行;

第四:每一個請求由ServletRequest對象來接受請求,由ServletResponse對象來響應該請求;

問題出現了:

同一個Servlet的的多個請求到來時,如果該Servlet中存在成員變量,可能發生多線程同時訪問該資源時,都來操作它,造成數據的不一致,因此產生線程安全問題。

解決:

1.      實現SingleThreadModel接口

如果一個Servlet被這個接口指定,那麼在這個Servlet中的service方法將不會有兩個線程被同時執行,當然也就不存在線程安全的問題;

2.      同步對共享數據的操作

使用synchronized關鍵字能保證一次只有一個線程可以訪問被保護的區段,Servlet可以通過同步塊操作來保證線程的安全。

ServletRequest對象是線程安全的,但是ServletContext和HttpSession不是線程安全的;

要使用同步的集合類:Vector代替ArrayList,HsahTable代替HashMap;

3.      避免使用實例變量(成員變量)

線程安全問題是由實例變量造成的,只要在Servlet裡面的任何方法裡面都不使用實例變量,那麼該Servlet就是線程安全的。(所有建議不要在servlet中定義成員變量,盡量用局部變量代替)

對上面的三種方法進行測試,可以表明用它們都能設計出線程安全的Servlet程序。但是,如果一個Servlet實現了SingleThreadModel接口,Servlet引擎將為每個新的請求創建一個單獨的Servlet實例,這將引起大量的系統開銷。SingleThreadModel在Servlet2.4中已不再提倡使用;同樣如果在程序中使用同步來保護要使用的共享的數據,也會使系統的性能大大下降。這是因為被同步的代碼塊在同一時刻只能有一個線程執行它,使得其同時處理客戶請求的吞吐量降低,而且很多客戶處於阻塞狀態。另外為保證主存內容和線程的工作內存中的數據的一致性,要頻繁地刷新緩存,這也會大大地影響系統的性能。所以在實際的開發中也應避免或最小化Servlet中的同步代碼;在Serlet中避免使用實例變量是保證Servlet線程安全的最佳選擇。從Java內存模型也可以知道,方法中的臨時變量是在棧上分配空間,而且每個線程都有自己私有的棧空間,所以它們不會影響線程的安全。

 Servlet的線程安全問題只有在大量的並發訪問時才會顯現出來,並且很難發現,因此在編寫Servlet程序時要特別注意。線程安全問題主要是由實例變量造成的,因此在Servlet中應避免使用實例變量。如果應用程序設計無法避免使用實例變量,那麼使用同步來保護要使用的實例變量,但為保證系統的最佳性能,應該同步可用性最小的代碼路徑。

php怎麼處理高並發

以下內容轉載自徐漢彬大牛的博客 億級Web系統搭建——單機到分布式集群 

當一個Web系統從日訪問量10萬逐步增長到1000萬,甚至超過1億的過程中,Web系統承受的壓力會越來越大,在這個過程中,我們會遇到很多的問題。為了解決這些性能壓力帶來問題,我們需要在Web系統架構層面搭建多個層次的緩存機制。在不同的壓力階段,我們會遇到不同的問題,通過搭建不同的服務和架構來解決。

Web負載均衡 

Web負載均衡(Load Balancing),簡單地說就是給我們的服務器集群分配“工作任務”,而採用恰當的分配方式,對於保護處於後端的Web服務器來說,非常重要。

負載均衡的策略有很多,我們從簡單的講起哈。

1. HTTP重定向

當用戶發來請求的時候,Web服務器通過修改HTTP響應頭中的Location標記來返回一個新的url,然後瀏覽器再繼續請求這個新url,實際上就是頁面重定向。通過重定向,來達到“負載均衡”的目標。例如,我們在下載PHP源碼包的時候,點擊下載鏈接時,為了解決不同國家和地域下載速度的問題,它會返回一個離我們近的下載地址。重定向的HTTP返回碼是302

這個重定向非常容易實現,並且可以自定義各種策略。但是,它在大規模訪問量下,性能不佳。而且,給用戶的體驗也不好,實際請求發生重定向,增加了網絡延時。

2. 反向代理負載均衡

反向代理服務的核心工作主要是轉發HTTP請求,扮演了瀏覽器端和後台Web服務器中轉的角色。因為它工作在HTTP層(應用層),也就是網絡七層結構中的第七層,因此也被稱為“七層負載均衡”。可以做反向代理的軟件很多,比較常見的一種是Nginx。

Nginx是一種非常靈活的反向代理軟件,可以自由定製化轉發策略,分配服務器流量的權重等。反向代理中,常見的一個問題,就是Web服務器存儲的session數據,因為一般負載均衡的策略都是隨機分配請求的。同一個登錄用戶的請求,無法保證一定分配到相同的Web機器上,會導致無法找到session的問題。

解決方案主要有兩種:

1. 配置反向代理的轉發規則,讓同一個用戶的請求一定落到同一台機器上(通過分析cookie),複雜的轉發規則將會消耗更多的CPU,也增加了代理服務器的負擔。

2. 將session這類的信息,專門用某個獨立服務來存儲,例如redis/memchache,這個方案是比較推薦的。

反向代理服務,也是可以開啟緩存的,如果開啟了,會增加反向代理的負擔,需要謹慎使用。這種負載均衡策略實現和部署非常簡單,而且性能表現也比較好。但是,它有“單點故障”的問題,如果掛了,會帶來很多的麻煩。而且,到了後期Web服務器繼續增加,它本身可能成為系統的瓶頸。

3. IP負載均衡

IP負載均衡服務是工作在網絡層(修改IP)和傳輸層(修改端口,第四層),比起工作在應用層(第七層)性能要高出非常多。原理是,他是對IP層的數據包的IP地址和端口信息進行修改,達到負載均衡的目的。這種方式,也被稱為“四層負載均衡”。常見的負載均衡方式,是LVS(Linux Virtual Server,Linux虛擬服務),通過IPVS(IP Virtual Server,IP虛擬服務)來實現。

在負載均衡服務器收到客戶端的IP包的時候,會修改IP包的目標IP地址或端口,然後原封不動地投遞到內部網絡中,數據包會流入到實際Web服務器。實際服務器處理完成後,又會將數據包投遞迴給負載均衡服務器,它再修改目標IP地址為用戶IP地址,最終回到客戶端。

上述的方式叫LVS-NAT,除此之外,還有LVS-RD(直接路由),LVS-TUN(IP隧道),三者之間都屬於LVS的方式,但是有一定的區別,篇幅問題,不贅敘。

IP負載均衡的性能要高出Nginx的反向代理很多,它只處理到傳輸層為止的數據包,並不做進一步的組包,然後直接轉發給實際服務器。不過,它的配置和搭建比較複雜。

4. DNS負載均衡

DNS(Domain Name System)負責域名解析的服務,域名url實際上是服務器的別名,實際映射是一個IP地址,解析過程,就是DNS完成域名到IP的映射。而一個域名是可以配置成對應多個IP的。因此,DNS也就可以作為負載均衡服務。

這種負載均衡策略,配置簡單,性能極佳。但是,不能自由定義規則,而且,變更被映射的IP或者機器故障時很麻煩,還存在DNS生效延遲的問題。 

5. DNS/GSLB負載均衡

我們常用的CDN(Content Delivery Network,內容分髮網絡)實現方式,其實就是在同一個域名映射為多IP的基礎上更進一步,通過GSLB(Global Server Load Balance,全局負載均衡)按照指定規則映射域名的IP。一般情況下都是按照地理位置,將離用戶近的IP返回給用戶,減少網絡傳輸中的路由節點之間的跳躍消耗。

“向上尋找”,實際過程是LDNS(Local DNS)先向根域名服務(Root Name Server)獲取到頂級根的Name Server(例如.com的),然後得到指定域名的授權DNS,然後再獲得實際服務器IP。

CDN在Web系統中,一般情況下是用來解決大小較大的靜態資源(html/Js/Css/圖片等)的加載問題,讓這些比較依賴網絡下載的內容,儘可能離用戶更近,提升用戶體驗。

例如,我訪問了一張imgcache.gtimg.cn上的圖片(騰訊的自建CDN,不使用qq.com域名的原因是防止http請求的時候,帶上了多餘的cookie信息),我獲得的IP是183.60.217.90。

這種方式,和前面的DNS負載均衡一樣,不僅性能極佳,而且支持配置多種策略。但是,搭建和維護成本非常高。互聯網一線公司,會自建CDN服務,中小型公司一般使用第三方提供的CDN。

Web系統的緩存機制的建立和優化

剛剛我們講完了Web系統的外部網絡環境,現在我們開始關注我們Web系統自身的性能問題。我們的Web站點隨着訪問量的上升,會遇到很多的挑戰,解決這些問題不僅僅是擴容機器這麼簡單,建立和使用合適的緩存機制才是根本。

最開始,我們的Web系統架構可能是這樣的,每個環節,都可能只有1台機器。

我們從最根本的數據存儲開始看哈。

一、 MySQL數據庫內部緩存使用

MySQL的緩存機制,就從先從MySQL內部開始,下面的內容將以最常見的InnoDB存儲引擎為主。

1. 建立恰當的索引

最簡單的是建立索引,索引在表數據比較大的時候,起到快速檢索數據的作用,但是成本也是有的。首先,佔用了一定的磁盤空間,其中組合索引最突出,使用需要謹慎,它產生的索引甚至會比源數據更大。其次,建立索引之後的數據insert/update/delete等操作,因為需要更新原來的索引,耗時會增加。當然,實際上我們的系統從總體來說,是以select查詢操作居多,因此,索引的使用仍然對系統性能有大幅提升的作用。

2. 數據庫連接線程池緩存

如果,每一個數據庫操作請求都需要創建和銷毀連接的話,對數據庫來說,無疑也是一種巨大的開銷。為了減少這類型的開銷,可以在MySQL中配置thread_cache_size來表示保留多少線程用於復用。線程不夠的時候,再創建,空閑過多的時候,則銷毀。

其實,還有更為激進一點的做法,使用pconnect(數據庫長連接),線程一旦創建在很長時間內都保持着。但是,在訪問量比較大,機器比較多的情況下,這種用法很可能會導致“數據庫連接數耗盡”,因為建立連接並不回收,最終達到數據庫的max_connections(最大連接數)。因此,長連接的用法通常需要在CGI和MySQL之間實現一個“連接池”服務,控制CGI機器“盲目”創建連接數。

建立數據庫連接池服務,有很多實現的方式,PHP的話,我推薦使用swoole(PHP的一個網絡通訊拓展)來實現。

3. Innodb緩存設置(innodb_buffer_pool_size)

innodb_buffer_pool_size這是個用來保存索引和數據的內存緩存區,如果機器是MySQL獨佔的機器,一般推薦為機器物理內存的80%。在取表數據的場景中,它可以減少磁盤IO。一般來說,這個值設置越大,cache命中率會越高。

4. 分庫/分表/分區。

MySQL數據庫表一般承受數據量在百萬級別,再往上增長,各項性能將會出現大幅度下降,因此,當我們預見數據量會超過這個量級的時候,建議進行分庫/分表/分區等操作。最好的做法,是服務在搭建之初就設計為分庫分表的存儲模式,從根本上杜絕中後期的風險。不過,會犧牲一些便利性,例如列表式的查詢,同時,也增加了維護的複雜度。不過,到了數據量千萬級別或者以上的時候,我們會發現,它們都是值得的。 

二、 MySQL數據庫多台服務搭建

1台MySQL機器,實際上是高風險的單點,因為如果它掛了,我們Web服務就不可用了。而且,隨着Web系統訪問量繼續增加,終於有一天,我們發現1台MySQL服務器無法支撐下去,我們開始需要使用更多的MySQL機器。當引入多台MySQL機器的時候,很多新的問題又將產生。

1. 建立MySQL主從,從庫作為備份

這種做法純粹為了解決“單點故障”的問題,在主庫出故障的時候,切換到從庫。不過,這種做法實際上有點浪費資源,因為從庫實際上被閑着了。

2. MySQL讀寫分離,主庫寫,從庫讀。

兩台數據庫做讀寫分離,主庫負責寫入類的操作,從庫負責讀的操作。並且,如果主庫發生故障,仍然不影響讀的操作,同時也可以將全部讀寫都臨時切換到從庫中(需要注意流量,可能會因為流量過大,把從庫也拖垮)。

3. 主主互備。

兩台MySQL之間互為彼此的從庫,同時又是主庫。這種方案,既做到了訪問量的壓力分流,同時也解決了“單點故障”問題。任何一台故障,都還有另外一套可供使用的服務。

不過,這種方案,只能用在兩台機器的場景。如果業務拓展還是很快的話,可以選擇將業務分離,建立多個主主互備。

三、 MySQL數據庫機器之間的數據同步

每當我們解決一個問題,新的問題必然誕生在舊的解決方案上。當我們有多台MySQL,在業務高峰期,很可能出現兩個庫之間的數據有延遲的場景。並且,網絡和機器負載等,也會影響數據同步的延遲。我們曾經遇到過,在日訪問量接近1億的特殊場景下,出現,從庫數據需要很多天才能同步追上主庫的數據。這種場景下,從庫基本失去效用了。

於是,解決同步問題,就是我們下一步需要關注的點。

1. MySQL自帶多線程同步

MySQL5.6開始支持主庫和從庫數據同步,走多線程。但是,限制也是比較明顯的,只能以庫為單位。MySQL數據同步是通過binlog日誌,主庫寫入到binlog日誌的操作,是具有順序的,尤其當SQL操作中含有對於表結構的修改等操作,對於後續的SQL語句操作是有影響的。因此,從庫同步數據,必須走單進程。

2. 自己實現解析binlog,多線程寫入。

以數據庫的表為單位,解析binlog多張表同時做數據同步。這樣做的話,的確能夠加快數據同步的效率,但是,如果表和表之間存在結構關係或者數據依賴的話,則同樣存在寫入順序的問題。這種方式,可用於一些比較穩定並且相對獨立的數據表。

國內一線互聯網公司,大部分都是通過這種方式,來加快數據同步效率。還有更為激進的做法,是直接解析binlog,忽略以表為單位,直接寫入。但是這種做法,實現複雜,使用範圍就更受到限制,只能用於一些場景特殊的數據庫中(沒有表結構變更,表和表之間沒有數據依賴等特殊表)。 

四、 在Web服務器和數據庫之間建立緩存

實際上,解決大訪問量的問題,不能僅僅着眼於數據庫層面。根據“二八定律”,80%的請求只關注在20%的熱點數據上。因此,我們應該建立Web服務器和數據庫之間的緩存機制。這種機制,可以用磁盤作為緩存,也可以用內存緩存的方式。通過它們,將大部分的熱點數據查詢,阻擋在數據庫之前。

1. 頁面靜態化

用戶訪問網站的某個頁面,頁面上的大部分內容在很長一段時間內,可能都是沒有變化的。例如一篇新聞報道,一旦發布幾乎是不會修改內容的。這樣的話,通過CGI生成的靜態html頁面緩存到Web服務器的磁盤本地。除了第一次,是通過動態CGI查詢數據庫獲取之外,之後都直接將本地磁盤文件返回給用戶。

在Web系統規模比較小的時候,這種做法看似完美。但是,一旦Web系統規模變大,例如當我有100台的Web服務器的時候。那樣這些磁盤文件,將會有100份,這個是資源浪費,也不好維護。這個時候有人會想,可以集中一台服務器存起來,呵呵,不如看看下面一種緩存方式吧,它就是這樣做的。

2. 單台內存緩存

通過頁面靜態化的例子中,我們可以知道將“緩存”搭建在Web機器本機是不好維護的,會帶來更多問題(實際上,通過PHP的apc拓展,可通過Key/value操作Web服務器的本機內存)。因此,我們選擇搭建的內存緩存服務,也必須是一個獨立的服務。

內存緩存的選擇,主要有redis/memcache。從性能上說,兩者差別不大,從功能豐富程度上說,Redis更勝一籌。

3. 內存緩存集群

當我們搭建單台內存緩存完畢,我們又會面臨單點故障的問題,因此,我們必須將它變成一個集群。簡單的做法,是給他增加一個slave作為備份機器。但是,如果請求量真的很多,我們發現cache命中率不高,需要更多的機器內存呢?因此,我們更建議將它配置成一個集群。例如,類似redis cluster。

Redis cluster集群內的Redis互為多組主從,同時每個節點都可以接受請求,在拓展集群的時候比較方便。客戶端可以向任意一個節點發送請求,如果是它的“負責”的內容,則直接返回內容。否則,查找實際負責Redis節點,然後將地址告知客戶端,客戶端重新請求。

對於使用緩存服務的客戶端來說,這一切是透明的。

內存緩存服務在切換的時候,是有一定風險的。從A集群切換到B集群的過程中,必須保證B集群提前做好“預熱”(B集群的內存中的熱點數據,應該盡量與A集群相同,否則,切換的一瞬間大量請求內容,在B集群的內存緩存中查找不到,流量直接衝擊後端的數據庫服務,很可能導致數據庫宕機)。

4. 減少數據庫“寫”

上面的機制,都實現減少數據庫的“讀”的操作,但是,寫的操作也是一個大的壓力。寫的操作,雖然無法減少,但是可以通過合併請求,來起到減輕壓力的效果。這個時候,我們就需要在內存緩存集群和數據庫集群之間,建立一個修改同步機制。

先將修改請求生效在cache中,讓外界查詢顯示正常,然後將這些sql修改放入到一個隊列中存儲起來,隊列滿或者每隔一段時間,合併為一個請求到數據庫中更新數據庫。

除了上述通過改變系統架構的方式提升寫的性能外,MySQL本身也可以通過配置參數innodb_flush_log_at_trx_commit來調整寫入磁盤的策略。如果機器成本允許,從硬件層面解決問題,可以選擇老一點的RAID(Redundant Arrays of independent Disks,磁盤列陣)或者比較新的SSD(Solid State Drives,固態硬盤)。

5. NoSQL存儲

不管數據庫的讀還是寫,當流量再進一步上漲,終會達到“人力有窮時”的場景。繼續加機器的成本比較高,並且不一定可以真正解決問題的時候。這個時候,部分核心數據,就可以考慮使用NoSQL的數據庫。NoSQL存儲,大部分都是採用key-value的方式,這裡比較推薦使用上面介紹過Redis,Redis本身是一個內存cache,同時也可以當做一個存儲來使用,讓它直接將數據落地到磁盤。

這樣的話,我們就將數據庫中某些被頻繁讀寫的數據,分離出來,放在我們新搭建的Redis存儲集群中,又進一步減輕原來MySQL數據庫的壓力,同時因為Redis本身是個內存級別的Cache,讀寫的性能都會大幅度提升。

國內一線互聯網公司,架構上採用的解決方案很多是類似於上述方案,不過,使用的cache服務卻不一定是Redis,他們會有更豐富的其他選擇,甚至根據自身業務特點開發出自己的NoSQL服務。

6. 空節點查詢問題

當我們搭建完前面所說的全部服務,認為Web系統已經很強的時候。我們還是那句話,新的問題還是會來的。空節點查詢,是指那些數據庫中根本不存在的數據請求。例如,我請求查詢一個不存在人員信息,系統會從各級緩存逐級查找,最後查到到數據庫本身,然後才得出查找不到的結論,返回給前端。因為各級cache對它無效,這個請求是非常消耗系統資源的,而如果大量的空節點查詢,是可以衝擊到系統服務的。

在我曾經的工作經歷中,曾深受其害。因此,為了維護Web系統的穩定性,設計適當的空節點過濾機制,非常有必要。

我們當時採用的方式,就是設計一張簡單的記錄映射表。將存在的記錄存儲起來,放入到一台內存cache中,這樣的話,如果還有空節點查詢,則在緩存這一層就被阻擋了。

異地部署(地理分布式)

完成了上述架構建設之後,我們的系統是否就已經足夠強大了呢?答案當然是否定的哈,優化是無極限的。Web系統雖然表面上看,似乎比較強大了,但是給予用戶的體驗卻不一定是最好的。因為東北的同學,訪問深圳的一個網站服務,他還是會感到一些網絡距離上的慢。這個時候,我們就需要做異地部署,讓Web系統離用戶更近。

一、 核心集中與節點分散

有玩過大型網遊的同學都會知道,網遊是有很多個區的,一般都是按照地域來分,例如廣東專區,北京專區。如果一個在廣東的玩家,去北京專區玩,那麼他會感覺明顯比在廣東專區卡。實際上,這些大區的名稱就已經說明了,它的服務器所在地,所以,廣東的玩家去連接地處北京的服務器,網絡當然會比較慢。

當一個系統和服務足夠大的時候,就必須開始考慮異地部署的問題了。讓你的服務,儘可能離用戶更近。我們前面已經提到了Web的靜態資源,可以存放在CDN上,然後通過DNS/GSLB的方式,讓靜態資源的分散“全國各地”。但是,CDN只解決的靜態資源的問題,沒有解決後端龐大的系統服務還只集中在某個固定城市的問題。

這個時候,異地部署就開始了。異地部署一般遵循:核心集中,節點分散。

· 核心集中:實際部署過程中,總有一部分的數據和服務存在不可部署多套,或者部署多套成本巨大。而對於這些服務和數據,就仍然維持一套,而部署地點選擇一個地域比較中心的地方,通過網絡內部專線來和各個節點通訊。

· 節點分散:將一些服務部署為多套,分布在各個城市節點,讓用戶請求儘可能選擇近的節點訪問服務。

例如,我們選擇在上海部署為核心節點,北京,深圳,武漢,上海為分散節點(上海自己本身也是一個分散節點)。我們的服務架構如圖:

需要補充一下的是,上圖中上海節點和核心節點是同處於一個機房的,其他分散節點各自獨立機房。 

國內有很多大型網遊,都是大致遵循上述架構。它們會把數據量不大的用戶核心賬號等放在核心節點,而大部分的網遊數據,例如裝備、任務等數據和服務放在地區節點裡。當然,核心節點和地域節點之間,也有緩存機制。 

二、 節點容災和過載保護

節點容災是指,某個節點如果發生故障時,我們需要建立一個機制去保證服務仍然可用。毫無疑問,這裡比較常見的容災方式,是切換到附近城市節點。假如系統的天津節點發生故障,那麼我們就將網絡流量切換到附近的北京節點上。考慮到負載均衡,可能需要同時將流量切換到附近的幾個地域節點。另一方面,核心節點自身也是需要自己做好容災和備份的,核心節點一旦故障,就會影響全國服務。

過載保護,指的是一個節點已經達到最大容量,無法繼續接接受更多請求了,系統必須有一個保護的機制。一個服務已經滿負載,還繼續接受新的請求,結果很可能就是宕機,影響整個節點的服務,為了至少保障大部分用戶的正常使用,過載保護是必要的。

解決過載保護,一般2個方向:

· 拒絕服務,檢測到滿負載之後,就不再接受新的連接請求。例如網遊登入中的排隊。

· 分流到其他節點。這種的話,系統實現更為複雜,又涉及到負載均衡的問題。

小結

Web系統會隨着訪問規模的增長,漸漸地從1台服務器可以滿足需求,一直成長為“龐然大物”的大集群。而這個Web系統變大的過程,實際上就是我們解決問題的過程。在不同的階段,解決不同的問題,而新的問題又誕生在舊的解決方案之上。

系統的優化是沒有極限的,軟件和系統架構也一直在快速發展,新的方案解決了老的問題,同時也帶來新的挑戰。

北大青鳥設計培訓:PHP中的(偽)多線程與多進程?

利用WEB服務器本身的多線程來處理,從WEB服務器多次調用我們需要實現多線程的程序。

 PHP中也能多線程了,那麼問題也來了,那就是同步的問題。

廈門電腦培訓知道PHP本身是不支持多線程的,所以更不會有什麼像Java中synchronize的方法了。

那我們該如何做呢?1.盡量不訪問同一個資源。

以避免衝突。

但是可以同時像數據庫操作。

因為數據庫是支持並發操作的。

所以在多線程的PHP中不要向同一個文件中寫入數據。

如果必須要寫的話,用別的方法進行同步。

如調用flock對文件進行加鎖等。

或建立臨時文件,並在另外的線程中等待這個文件的消失while(file_exits(‘xxx’));這樣就等於這個臨時文件存在時,表示其實線程正在操作。

如果沒有了這個文件,說明其它線程已經釋放了這個。

2.盡量不要從runThread在執行fputs後取這個socket中讀取數據。

因為要實現多線程,需要的用非阻塞模式。

即在像fgets這樣的函數時立即返回。

所以讀寫數據就會出問題。

如果使用阻塞模式的話,程序就不算是多線程了。

他要等上面的返回才執行下面的程序。

所以如果需要交換數據最後利用外面文件或數據中完成。

實在想要的話就用socket_set_nonblock($fp)來實現。

說了這麼多,倒底這個有沒有實際的意義呢?在什麼時候需要這種用這種方法呢?答案是肯定的。

大家知道。

在一個不斷讀取網絡資源的應用中,網絡的速度是瓶頸。

如果采多這種形式就可以同時以多個線程對不同的頁面進行讀取。

curl中為什麼

這個需要配合js,打開一個html頁面,首先js用ajax請求頁面,返回第一個頁面信息確定處理完畢(ajax有強制同步功能),ajax再訪問第二個頁面。(或者根據服務器狀況,你可以同時提交幾個URL,跑幾個相同的頁面)

參數可以由js產生並傳遞url,php後台頁面根據URL抓頁面。然後ajax通過php,在數據庫或者是哪裡設一個標量,標明檢測到哪裡。由於前台的html頁面執行多少時候都沒問題,這樣php的內存限制和執行時間限制就解決了。

因為不會浪費大量的資源用一個頁面來跑一個瞬間500次的for循環了。(你的500次for循環死了原因可能是獲取的數據太多,大過了php限制的內存)

不過印象中curl好像也有強制同步的選項,就是等待一個抓取後再執行下一步。但是這個500次都是用一個頁面線程處理,也就是說肯定會遠遠大於30秒的默認執行時間。

原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/239412.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
小藍的頭像小藍
上一篇 2024-12-12 12:16
下一篇 2024-12-12 12:16

相關推薦

  • PHP和Python哪個好找工作?

    PHP和Python都是非常流行的編程語言,它們被廣泛應用於不同領域的開發中。但是,在考慮擇業方向的時候,很多人都會有一個問題:PHP和Python哪個好找工作?這篇文章將從多個方…

    編程 2025-04-29
  • Python多線程讀取數據

    本文將詳細介紹多線程讀取數據在Python中的實現方法以及相關知識點。 一、線程和多線程 線程是操作系統調度的最小單位。單線程程序只有一個線程,按照程序從上到下的順序逐行執行。而多…

    編程 2025-04-29
  • PHP怎麼接幣

    想要在自己的網站或應用中接受比特幣等加密貨幣的支付,就需要對該加密貨幣擁有一定的了解,並使用對應的API進行開發。本文將從多個方面詳細闡述如何使用PHP接受加密貨幣的支付。 一、環…

    編程 2025-04-29
  • Python線程等待指南

    本文將從多個方面詳細講解Python線程等待的相關知識。 一、等待線程結束 在多線程編程中,經常需要等待線程執行完畢再進行下一步操作。可以使用join()方法實現等待線程執行完畢再…

    編程 2025-04-29
  • Python兩個線程交替打印1到100

    這篇文章的主題是關於Python多線程的應用。我們將會通過實際的代碼,學習如何使用Python兩個線程交替打印1到100。 一、創建線程 在Python中,我們可以使用Thread…

    編程 2025-04-28
  • 使用PHP foreach遍歷有相同屬性的值

    本篇文章將介紹如何使用PHP foreach遍歷具有相同屬性的值,並給出相應的代碼示例。 一、基礎概念 在講解如何使用PHP foreach遍歷有相同屬性的值之前,我們需要先了解幾…

    編程 2025-04-28
  • ROS線程發布消息異常解決方法

    針對ROS線程發布消息異常問題,我們可以從以下幾個方面進行分析和解決。 一、檢查ROS代碼是否正確 首先,我們需要檢查ROS代碼是否正確。可能會出現的問題包括: 是否正確初始化RO…

    編程 2025-04-28
  • Python線程池並發爬蟲

    Python線程池並發爬蟲是實現多線程爬取數據的常用技術之一,可以在一定程度上提高爬取效率和數據處理能力。本文將從多個方面對Python線程池並發爬蟲做詳細的闡述,包括線程池的實現…

    編程 2025-04-27
  • PHP獲取301跳轉後的地址

    本文將為大家介紹如何使用PHP獲取301跳轉後的地址。301重定向是什麼呢?當我們訪問一個網頁A,但是它已經被遷移到了另一個地址B,此時若服務器端做了301重定向,那麼你的瀏覽器在…

    編程 2025-04-27
  • 多線程和多進程的應用

    多線程和多進程是現代編程中常用的技術,可以提高程序的效率和性能。本文將從不同的角度對多線程和多進程進行詳細的介紹和應用。 一、多線程 vs 多進程 多線程和多進程都是為了實現程序並…

    編程 2025-04-27

發表回復

登錄後才能評論