本系統采用MySQL一主多從模式設計,即1臺 MySQL“主”服務器(Master)+多臺“從”服務器(Slave),“從”服務器之間通過Haproxy進行負載均衡,對外只提供一個訪問IP,當程序需要訪問多臺"從"服務器時,只需要訪問Haproxy,再由Haproxy將請求分發到各個數據庫節點。
我們的程序可以有倆個數據源(DataSourceA,DataSourceB),一個(DataSourceA)直接連接主庫,另外一個(DataSourceB)連接Haproxy,當需要寫入操作時可以使用DataSourceA,讀取時使用DataSourceB。
設計圖如下:
看到這里大家可能會有一個疑問,這個問題就是主從數據庫之間數據同步延時的問題!
因為大多數使用MySQL主從同步數據都是異步的,也就是說當主庫的數據發生變化時并不能立即的更新從庫,這么做的目的也是為了更好的性能,那么設想一下,當用戶新增一條記錄后立刻去從庫查詢,可能并不能查到剛剛新增的數據,這豈不是很腦裂的問題~~~。
然而實際情況并不應該是這樣的,我們也不應該這樣去設計程序,我們就拿一個類似于CSND博客管理的系統來說,假設一個“博客系統”只有倆部分, 一部分是博客管理后臺,用戶可以在后臺新增,編輯,刪除博客。另一部分是門戶網站,負責展示所有用戶的博客信息,相對于這樣一個系統來說, 后臺管理模塊對數據庫的操作壓力不是很大,相反門戶網站讀取博客信息對數據庫的壓力很大,這也式一般互聯網產品的特點,而且最重要的一點是系統可以接受主從同步數據帶來的延遲,也就是說當用戶在后臺新增一條博客時,前臺門戶網站并不能立即查詢到這條信息。一般都是再過一段時間后才會出現在首頁,因為大多數系統都有緩存設置,這樣正好給主從同步延遲帶來時間。
接著說上面的問題,有的同學可能會有這樣的疑問,就是后臺用戶在新增一條記錄后,一般都是立即查詢返回博客列表,按照上面說的豈不是查詢不到~~~,我覺得這個問題可以這么解決:
1、后臺用戶在進行 新增,查詢,編輯,刪除等操作時直接連接主庫,這樣無論什么操作都是實時的,因為后臺操作對數據庫的壓力不是很大,所以讀寫全部連接主庫應該沒什么問題!?
2、門戶網站查詢博客列表時從 “從庫集群中查詢“,通過負載均衡技術,解決了擴展性,高可用性等問題,同時門戶網站首頁也不需要實時查詢主庫中的數據,因為網站本身一般都有緩存,也不是實時的。
上面的架構設計只是拋磚引玉,大家有什么好想法也可以相互交流~
本文重點介紹的內容有二點:
1、如何使用Haproxy給MySQL做負載均衡,提供相關的配置說明,健康檢查等等。
2、當程序通過連接Haproxy代理之后,如何解決程序中連接池長連接失效的問題。
下面介紹如何安裝配置Haproxy~
1、首先進行負載均衡配置。
假設兩臺MySQL(slave)從服務器 ?A:192.168.1.191:3306 ? ? ?B:192.168.1.192:3306。
首先在linxu上安裝Haproxy,安裝過程略。。。。。。
安裝完畢后打開配置文件在/etc/haproxy/?haproxy.cfg,配置文件的路徑可能不用,別告訴我找不到~~~!
[html]?view plain?copy?print?
-
global??
-
????????maxconn?4096??
-
????????daemon??
-
????????chroot??????/var/lib/haproxy??
-
????????pidfile?????/var/run/haproxy.pid??
-
????????#debug??
-
????????#quiet??
-
????????user?haproxy??
-
????????group?haproxy??
-
???
-
defaults??
-
????????log?????global??
-
????????mode????http??
-
????????option??httplog??
-
????????option??dontlognull??
-
????????log?127.0.0.1?local0??
-
????????retries?3??
-
????????option?redispatch??
-
????????maxconn?2000??
-
????????#contimeout??????5000??
-
????????#clitimeout??????50000??
-
????????#srvtimeout??????50000??
-
????????timeout?http-request????10s??
-
????????timeout?queue???????????1m??
-
????????timeout?connect?????????10s??
-
????????timeout?client??????????1m??
-
????????timeout?server??????????1m??
-
????????timeout?http-keep-alive?10s??
-
????????timeout?check???????????10s??
-
???
-
listen??admin_stats?0.0.0.0:8888??
-
????????mode????????http??
-
????????stats?uri???/dbs??
-
????????stats?realm?????Global\?statistics??
-
????????stats?auth??admin:admin??
-
???
-
listen??proxy-mysql?0.0.0.0:23306??
-
????????mode?tcp??
-
????????balance?roundrobin??
-
????????option?tcplog??
-
????????option?mysql-check?user?haproxy?#在mysql中創建無任何權限用戶haproxy,且無密碼??
-
????????server?MySQL1?192.168.1.191:3306?check?weight?1?maxconn?2000??
-
????????server?MySQL2?192.168.1.192:3306?check?weight?1?maxconn?2000??
-
????????option?tcpka??
[html]?view plain?copy?print?
-
listen??admin_stats?0.0.0.0:8888?這個配置是監控頁面,綁定到本機8888端口,賬號admin,密碼admin??
listen??admin_stats?0.0.0.0:8888?這個配置是監控頁面,綁定到本機8888端口,賬號admin,密碼admin
可以通過web的方式查看所有MySQL節點的使用情況, http://你的IP:8888/dbs 即可登錄監控后臺。
如下圖:
[html]?view plain?copy?print?
-
listen??proxy-mysql?0.0.0.0:23306??
-
????????mode?tcp??
-
????????balance?roundrobin??
-
????????option?tcplog??
-
????????option?mysql-check?user?haproxy?#在mysql中創建無任何權限用戶haproxy,且無密碼??
-
????????server?MySQL1?192.168.1.191:3306?check?weight?1?maxconn?2000??
-
????????server?MySQL2?192.168.1.192:3306?check?weight?1?maxconn?2000??
-
????????option?tcpka??
listen??proxy-mysql?0.0.0.0:23306mode?tcpbalance?roundrobinoption?tcplogoption?mysql-check?user?haproxy?#在mysql中創建無任何權限用戶haproxy,且無密碼server?MySQL1?192.168.1.191:3306?check?weight?1?maxconn?2000server?MySQL2?192.168.1.192:3306?check?weight?1?maxconn?2000option?tcpka
[html]?view plain?copy?print?
-
</pre><pre?name="code"?class="html">proxy-mysql?0.0.0.0:23306?代理的端口。我們程序連接從庫集群時就訪問這個端口。??
</pre><pre?name="code"?class="html">proxy-mysql?0.0.0.0:23306?代理的端口。我們程序連接從庫集群時就訪問這個端口。
[html]?view plain?copy?print?
-
balance?roundrobin?負載均衡方式,有很多種,可以去Google。??
balance?roundrobin?負載均衡方式,有很多種,可以去Google。
[html]?view plain?copy?print?
-
option?mysql-check?user?haproxy?這里是配置健康檢查的,也是haproxy自帶的功能,<span?style="color:#ff6666;">需要在<span?style="font-family:?Arial,?Helvetica,?sans-serif;">mysql中創建無任何權限用戶haproxy,且無密碼</span></span>??
option?mysql-check?user?haproxy?這里是配置健康檢查的,也是haproxy自帶的功能,<span?style="color:#ff6666;">需要在<span?style="font-family:?Arial,?Helvetica,?sans-serif;">mysql中創建無任何權限用戶haproxy,且無密碼</span></span>
[html]?view plain?copy?print?
-
server?MySQL1?192.168.1.191:3306?check?weight?1?maxconn?2000?配置MySQL從庫節點,有多少配置多少就行了。??
server?MySQL1?192.168.1.191:3306?check?weight?1?maxconn?2000?配置MySQL從庫節點,有多少配置多少就行了。
有的同學可能不知道如何在MySQL中創建用戶,這里也給你寫好了。
用戶名為haproxy 且無密碼(重要) 否則haproxy無法檢測MySQL狀態。
CREATE USER 'haproxy'@'%' IDENTIFIED BY '';?
配置完成后啟動代理 service haproxy start ?如果用過yum方式安裝,應該就能啟動了,如果是其它方式安裝,可能啟動方式不同,需要編寫腳本啟動,應該不難自己研究一下~~~
然后讓我們寫個demo測試一下代理是否配置成功了沒!
[java]?view plain?copy?print?
-
public?static?void?main(String[]?args)?throws?Exception?{??
-
??????
-
??????
-
????Class.forName("com.mysql.jdbc.Driver");??
-
????Connection?conn?=?DriverManager.getConnection("jdbc:mysql://你的IP:23306/template?useUnicode=true",?"root",?"sql2008");??
-
??????
-
????for?(int?i?=?0;?i?<?100;?i++)?{??
-
????????PreparedStatement?pr?=?null;??
-
????????ResultSet?res?=?null;??
-
????????try?{??
-
?????????????pr?=?conn.prepareStatement("select?count(*)?from?sys_user");??
-
?????????????res?=?pr.executeQuery();??
-
????????????if(res.next())?{??
-
????????????????System.out.println(new?Date().toLocaleString()?+?"->"?+?res.getInt(1));??
-
????????????}??
-
????????}?catch?(Exception?e)?{??
-
????????????e.printStackTrace();??
-
????????????res.close();??
-
????????????pr.close();??
-
????????}??
-
??????????
-
????????Thread.sleep(25000);??
-
????}??
-
??????
-
????conn.close();??
-
}??
public?static?void?main(String[]?args)?throws?Exception?{Class.forName("com.mysql.jdbc.Driver");Connection?conn?=?DriverManager.getConnection("jdbc:mysql://你的IP:23306/template?useUnicode=true",?"root",?"sql2008");for?(int?i?=?0;?i?<?100;?i++)?{PreparedStatement?pr?=?null;ResultSet?res?=?null;try?{pr?=?conn.prepareStatement("select?count(*)?from?sys_user");res?=?pr.executeQuery();if(res.next())?{System.out.println(new?Date().toLocaleString()?+?"->"?+?res.getInt(1));}}?catch?(Exception?e)?{e.printStackTrace();res.close();pr.close();}Thread.sleep(25000);}conn.close();}
輸出結果如下:可以看到代理MySQL成功了,這時你可以隨機關掉一個MySQL節點的服務,程序依然能夠正常的執行,說明負載均衡也成功了。
[html]?view plain?copy?print?
-
2015-8-28?10:09:27->7??
-
2015-8-28?10:09:52->7??
-
2015-8-28?10:10:17->7??
-
2015-8-28?10:10:42->7??
-
2015-8-28?10:11:07->7??
2015-8-28?10:09:27->7
2015-8-28?10:09:52->7
2015-8-28?10:10:17->7
2015-8-28?10:10:42->7
2015-8-28?10:11:07->7
小小的激動有沒有~有沒有~。于是乎我們就把程序中數據源的配置改造一下,讓它連接haproxy即可。
<property name="jdbcUrl" value="jdbc:mysql://你的IP:23306/template?useUnicode=true" /> .
是不是以為大功告成了,如果你就這樣配置的話,等程序運行起來它就會給你一個大大的surprise
其實這里面是有坑的~~~~,且聽我細細道來。
一般的情況下,我相信大家在直接連接MySQL的時候幾乎都用到了連接池。
以我的配置為例:
[html]?view plain?copy?print?
-
??<bean?id="dataSource"?class="com.mchange.v2.c3p0.ComboPooledDataSource"?destroy-method="close">??
-
<property?name="driverClass"?value="com.mysql.jdbc.Driver"?/>??
-
<property?name="jdbcUrl"?value="jdbc:mysql://你的IP:23306/你的數據庫名稱?useUnicode=true"?/>??
-
<property?name="user"?value="xx"?/>??
-
<property?name="password"?value="yy"?/>??
-
<property?name="initialPoolSize"?value="5"?/>??
-
<property?name="minPoolSize"?value="5"?/>??
-
<property?name="maxPoolSize"?value="30"?/>??
-
<property?name="maxIdleTime"?value="0"?/>??
-
<property?name="idleConnectionTestPeriod"?value="30"?/>??
-
<property?name="acquireIncrement"?value="3"?/>??
-
<property?name="automaticTestTable"?value="C3p0TestTable_NotDelete"?/>??
-
<property?name="autoCommitOnClose"?value="false"?/>??
-
lt;/bean>??
????<bean?id="dataSource"?class="com.mchange.v2.c3p0.ComboPooledDataSource"?destroy-method="close"><property?name="driverClass"?value="com.mysql.jdbc.Driver"?/><property?name="jdbcUrl"?value="jdbc:mysql://你的IP:23306/你的數據庫名稱?useUnicode=true"?/><property?name="user"?value="xx"?/><property?name="password"?value="yy"?/><property?name="initialPoolSize"?value="5"?/><property?name="minPoolSize"?value="5"?/><property?name="maxPoolSize"?value="30"?/><property?name="maxIdleTime"?value="0"?/><property?name="idleConnectionTestPeriod"?value="30"?/><property?name="acquireIncrement"?value="3"?/><property?name="automaticTestTable"?value="C3p0TestTable_NotDelete"?/><property?name="autoCommitOnClose"?value="false"?/></bean>
其它的參數這里不解釋,大家可以查詢C3P0配置信息,網上很多。
這里只說一個:
[html]?view plain?copy?print?
-
idleConnectionTestPeriod=30?這個參數是配置連接池?每隔多少時間去檢查池內鏈接的有效性,單位秒。??
idleConnectionTestPeriod=30?這個參數是配置連接池?每隔多少時間去檢查池內鏈接的有效性,單位秒。
[html]?view plain?copy?print?
-
我這里設置成30秒,那么C3P0會每隔30秒?把連接池內所有的空閑連接拿出來挨個發一個測試SQL語句,已確定這個鏈接的有效性。??
我這里設置成30秒,那么C3P0會每隔30秒?把連接池內所有的空閑連接拿出來挨個發一個測試SQL語句,已確定這個鏈接的有效性。
以前我們的數據源是直接連接MySQL數據庫的,在正常的情況下MySQL是不會斷開這個鏈接的。
但是我們現在連接的是haproxy,也就是說我們程序的連接(Connection)是與haproxy建立的,這里的坑在于這個連接是會被haproxy斷掉的,這樣的話你連接池內的鏈接就變成了無效鏈接,在下次需要查詢數據庫時還需要重新創建連接,而且程序由于拿到的連接是無效鏈接,還有可能報錯。
那么haproxy與我們程序之間的連接超時時間在哪設置呢?
[html]?view plain?copy?print?
-
timeout?client??????????1m??#這個參數配置程序與haproxy的鏈接超時時間??
-
timeout?server??????????1m??<span?style="font-family:?Arial,?Helvetica,?sans-serif;">#這個參haproxy與mysql鏈接超時時間</span>??
????????timeout?client??????????1m??#這個參數配置程序與haproxy的鏈接超時時間timeout?server??????????1m??<span?style="font-family:?Arial,?Helvetica,?sans-serif;">#這個參haproxy與mysql鏈接超時時間</span>
這里的超時時間不是指連接過程的超時時間,而是指連接上以后,多少時間內沒有心跳,操作這個時間就認為超時,然后斷開連接。
寫的可能有些啰嗦,我們看個例子開說明一下:
[java]?view plain?copy?print?
-
public?static?void?main(String[]?args)?throws?Exception?{??
-
??????
-
??????
-
????Class.forName("com.mysql.jdbc.Driver");??
-
????Connection?conn?=?DriverManager.getConnection("jdbc:mysql://你的IP:23306/template?useUnicode=true",?"root",?"sql2008");??
-
??????
-
????for?(int?i?=?0;?i?<?100;?i++)?{??
-
????????PreparedStatement?pr?=?null;??
-
????????ResultSet?res?=?null;??
-
????????try?{??
-
?????????????pr?=?conn.prepareStatement("select?count(*)?from?sys_user");??
-
?????????????res?=?pr.executeQuery();??
-
????????????if(res.next())?{??
-
????????????????System.out.println(new?Date().toLocaleString()?+?"->"?+?res.getInt(1));??
-
????????????}??
-
????????}?catch?(Exception?e)?{??
-
????????????e.printStackTrace();??
-
????????????res.close();??
-
????????????pr.close();??
-
????????}??
-
??????????
-
????????Thread.sleep(60000);??
-
????}??
-
??????
-
????conn.close();??
-
}??
public?static?void?main(String[]?args)?throws?Exception?{Class.forName("com.mysql.jdbc.Driver");Connection?conn?=?DriverManager.getConnection("jdbc:mysql://你的IP:23306/template?useUnicode=true",?"root",?"sql2008");for?(int?i?=?0;?i?<?100;?i++)?{PreparedStatement?pr?=?null;ResultSet?res?=?null;try?{pr?=?conn.prepareStatement("select?count(*)?from?sys_user");res?=?pr.executeQuery();if(res.next())?{System.out.println(new?Date().toLocaleString()?+?"->"?+?res.getInt(1));}}?catch?(Exception?e)?{e.printStackTrace();res.close();pr.close();}Thread.sleep(60000);}conn.close();}
我上面配置的是?timeout client 1m ,也就是說客戶端連接到haproxy后 1分鐘之內沒有數據請求即為超時,就會斷掉鏈接:
[html]?view plain?copy?print?
-
<pre?name="code"?class="java"><pre?name="code"?class="java"?style="font-size:18px;">第一次查詢沒有問題:??
<pre?name="code"?class="java"><pre?name="code"?class="java"?style="font-size:18px;">第一次查詢沒有問題:
Thread.sleep(60000); 我把間隔設置為60秒,第二次查詢與第一次查詢間隔60秒就會報錯,因為超時了。
[java]?view plain?copy?print?
-
那如果我把間隔改為?<span?style="font-family:?Arial,?Helvetica,?sans-serif;">Thread.sleep(50000);?50秒,就不會報錯。</span>??
那如果我把間隔改為?<span?style="font-family:?Arial,?Helvetica,?sans-serif;">Thread.sleep(50000);?50秒,就不會報錯。</span>
結論就是
[html]?view plain?copy?print?
-
idleConnectionTestPeriod?的時間一定要小于?<span?style="font-size:18px;?background-color:?rgb(240,?240,?240);">timeout?client的時間。這樣C3P0會在Haproxy斷掉鏈接之前發送一次“心跳”過去,保持鏈接的有效性。</span>??
idleConnectionTestPeriod?的時間一定要小于?<span?style="font-size:18px;?background-color:?rgb(240,?240,?240);">timeout?client的時間。這樣C3P0會在Haproxy斷掉鏈接之前發送一次“心跳”過去,保持鏈接的有效性。</span>
[html]?view plain?copy?print?
-
<span?style="font-size:18px;?background-color:?rgb(240,?240,?240);">而且?</span><span?style="font-family:?Arial,?Helvetica,?sans-serif;">timeout?client與?</span><span?style="font-family:?Arial,?Helvetica,?sans-serif;">timeout?server?盡量保持一致,已達到最佳效果。</span>??