往事如風(fēng)
記錄工作中的點(diǎn)點(diǎn)滴滴 留住那些淡淡的回憶 |
標(biāo)題黨,哈哈,OK,言歸正傳。
最近兩天被一個(gè)故障搞死了,因?yàn)樵瓉硪粋€(gè)報(bào)警腳本是shell寫的,從一個(gè)java程序的jmx接口抓取信息并匯總發(fā)到監(jiān)控系統(tǒng),個(gè)人shell腳本能力比較稀松,在python牛人建議下,使用python寫了一個(gè)腳本,發(fā)覺非常爽,我這個(gè)菜鳥(學(xué)習(xí)經(jīng)歷1天)簡(jiǎn)單就能編寫出來一個(gè)復(fù)雜的腳本,比shell的簡(jiǎn)化將近5倍,晚上就得意洋洋的向一個(gè)同學(xué)吹噓ing,結(jié)果被BS:語(yǔ)法甜點(diǎn)而已,java也可以輕松做出來,而且也能做的那么精干。。
回來看了一下java如果要實(shí)現(xiàn)這些功能,代碼可能比shell還要多,看來這個(gè)領(lǐng)域?qū)嵲诓皇莏ava的專長(zhǎng)。
我以前也認(rèn)為語(yǔ)法甜點(diǎn)確認(rèn)只是錦上添花,但是使用了python之后,發(fā)現(xiàn)自己以前還是偏見啊,在特定環(huán)境下,如腳本、頁(yè)面等情況下,語(yǔ)法甜點(diǎn)可以大大減少輸入量和代碼出錯(cuò)可能性,動(dòng)態(tài)語(yǔ)言還是我們工具箱不可缺少的工具。
最近在做一個(gè)項(xiàng)目,綜合使用了jboss的microcontainer,jetty和自己定義的war包,war包中還會(huì)用到spring,因?yàn)闋可娴蕉鄠€(gè)容器,同時(shí)又有自己的自定義類,所以classloader環(huán)境異常復(fù)雜,ClassNotFound問題搞得頭都大了,最后綜合各種因素,設(shè)計(jì)了如下的一個(gè)classloader層次:
其中,紅色部分是系統(tǒng)(也就是啟動(dòng)java程序加載Main函數(shù)的classloader),主要的設(shè)計(jì)考量有以下幾點(diǎn):
1、使用自定義的ExtClassLoader(加載java的ext目錄下的jar包)把程序加載的class完全和系統(tǒng)加載的class隔離開,這樣即使在eclipse容器中啟動(dòng)都不會(huì)有類沖突。
為什么不從系統(tǒng)的ExtClassLoader作為自定義classloader數(shù)的根有兩個(gè)考慮,第一個(gè)是系統(tǒng)ExtClassLoader有可能不存在,第二個(gè)就是如果使用同一個(gè)ExtClassLoader中,在處理JNDI、XML和URL解析等java擴(kuò)展功能時(shí)會(huì)遇到后加載的handler部分導(dǎo)致不同classloader樹加載的同一個(gè)類的ClassCastException,具體參見這些模塊的源代碼。
2、WarClassLoader除了系統(tǒng)類和Common類(目前只有l(wèi)og相關(guān)類)以外的類都從war包的WEB-INFO和classes下加載。
3、所有執(zhí)行War包中代碼的線程ThreadContextClassLoader都設(shè)置為WarClassLoader,以供Spring和Webx中的相關(guān)工具類使用這個(gè)classloader結(jié)構(gòu)的后門來加載war包中的類,典型例子是Webx中ResourceLoaderService就是使用ContextClassLoader來加載類的。
4、RialtoClassLoader也就是這個(gè)項(xiàng)目的容器加載器和WarClassLoader不在同一個(gè)樹路徑上,可以避免程序使用類和war使用類的class沖突,典型的是Spring容器相關(guān)代碼。
5、CommonClassLoader加載的類需要嚴(yán)格控制,否則可能會(huì)導(dǎo)致運(yùn)行期類沖突,例如Spring的相關(guān)jar包絕對(duì)不可以出現(xiàn)在這個(gè)classloader作用范圍內(nèi)。
總之,ClassLoader采用父分派機(jī)制,后來增加的Thread ContextClassLoader在這個(gè)體系上增加了一個(gè)后門,帶來了靈活性,也帶來了很多令人困擾的問題,在做容器類的項(xiàng)目時(shí)難免會(huì)遇到class loader層次設(shè)計(jì)的問題,這里拋磚引玉,歡迎達(dá)人拍磚。
ActiveMQ是一個(gè)流行的開源MQ,我們也大規(guī)模應(yīng)用在網(wǎng)站的方方面面,每天處理上億消息,取得了較好效果。ActiveMQ有一個(gè)很好很強(qiáng)大的插件體系,提供了很強(qiáng)的擴(kuò)展能力,ActiveMQ本身就是使用這一套插件體系實(shí)現(xiàn)了很多擴(kuò)展功能,包括他的權(quán)限管理,日志管理,事務(wù)等模塊都是作為一個(gè)插件集成的,我們自己也在消息路由、補(bǔ)償式事務(wù)方面使用了它的插件功能,確實(shí)非常方便。
在ActiveMQ中,Broker代表一個(gè)運(yùn)行的MQ節(jié)點(diǎn),ActiveMQ的插件實(shí)際上是基于Broker的一個(gè)Filter鏈,整個(gè)設(shè)計(jì)類似于servlet的Filter結(jié)構(gòu),所有的Plugin構(gòu)成一個(gè)鏈?zhǔn)浇Y(jié)構(gòu),每個(gè)插件實(shí)際上都是一個(gè)"Interceptor",類結(jié)構(gòu)圖如下:
其中Broker接口封裝了一個(gè)AMQ節(jié)點(diǎn)的方方面面的方法,包括連接管理、session管理、消息的發(fā)送和接收以及其它的一些功能,BrokerFilter實(shí)現(xiàn)這個(gè)接口,并提供了鏈?zhǔn)浇Y(jié)構(gòu)支持,可以攔截所有Broker方法的實(shí)現(xiàn)并傳遞結(jié)果給鏈?zhǔn)浇Y(jié)構(gòu)的下一個(gè),形成了一個(gè)完整的"職責(zé)鏈"模式,具體層次關(guān)系如下,其中,"System Plugin"是指AMQ內(nèi)部使用Plugin機(jī)制實(shí)現(xiàn)的一些系統(tǒng)功能,用戶不能定制,"AMQ Plugin"指的是ActiveMQ已經(jīng)實(shí)現(xiàn)好了,可以在配置文件中自由選擇的一些插件,例如簡(jiǎn)單的安全插件,JAAS安全插件和DLQ插件等等,用戶插件就是指用戶自己實(shí)現(xiàn)的amq插件,需要用戶把相關(guān)jar包放入到amq的啟動(dòng)classpath中,并在配置文件中進(jìn)行配置才能正確加載的插件。
在上面這個(gè)層次結(jié)構(gòu)中,最下面的RegionBroker是核心組件,在其之上的都是Broker的插件,繼承之于BrokerFilter,和Broker保持接口兼容但是擴(kuò)展Broker的功能。
下面舉一個(gè)簡(jiǎn)單的例子,具體說明一下AMQ的插件是如何工作的。
我們?cè)谑褂肁MQ的過程中發(fā)現(xiàn),在測(cè)試環(huán)境維護(hù)方面有很大的麻煩,具體表現(xiàn)在很多同學(xué)在測(cè)試項(xiàng)目的時(shí)候往往只關(guān)注自己項(xiàng)目牽涉的隊(duì)列,不會(huì)去消費(fèi)其他"不相關(guān)"的隊(duì)列,這樣導(dǎo)致的一個(gè)問題就是ActiveMQ經(jīng)常發(fā)生大量數(shù)據(jù)阻塞,導(dǎo)致測(cè)試環(huán)境不可用,影響相關(guān)項(xiàng)目的測(cè)試工作。為了避免這個(gè)問題,我們假定在測(cè)試環(huán)境可以定義以下一些限制條件:
1、 所有隊(duì)列堆積消息不超過1000條,超過之后立即清除。
2、 消息超過1個(gè)小時(shí)沒有消費(fèi),就直接過期。
我們可以編寫一個(gè)簡(jiǎn)單的amq插件來完成這兩個(gè)限制條件:
首先,編寫一個(gè)插件安裝類:
package com.alibaba.napoli.plugins;
import org.apache.activemq.broker.Broker;
import org.apache.activemq.broker.BrokerPlugin;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class MessageControlBrokerPlugin implements BrokerPlugin {
private static Log log = LogFactory.getLog(StatisticsBrokerPlugin.class);
public Broker installPlugin(Broker broker) throws Exception {
log.info("install MessageControlBrokerPlugin");
return new MessageControlBroker(broker);
}
}
其次,編寫真正的插件實(shí)現(xiàn):
package com.alibaba.napoli.plugins;
import java.io.IOException;
import org.apache.activemq.broker.Broker;
import org.apache.activemq.broker.BrokerFilter;
import org.apache.activemq.broker.ConnectionContext;
import org.apache.activemq.broker.ProducerBrokerExchange;
import org.apache.activemq.broker.region.Destination;
import org.apache.activemq.broker.region.MessageReference;
import org.apache.activemq.broker.region.Queue;
import org.apache.activemq.command.Message;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* 開發(fā)環(huán)境管理插件,符合兩個(gè)條件進(jìn)行消息清理:<br>
* 1 消息累積超過1000條
* 2 消息超過1個(gè)小時(shí)無人消費(fèi)
* @author guolin.zhuanggl
*
*/
public class MessageControlBroker extends BrokerFilter {
public static Log log = LogFactory.getLog(DiscardingDLQBroker.class);
private static final long DEFAULT_EXPIRATION = 3600*1000;
private static final long DEFAULT_PURGE_COUNT = 1000;
public MessageControlBroker(Broker next) {
super(next);
}
@Override
public void messageExpired(ConnectionContext context,
MessageReference message) {
Message msg = null;
try {
msg = message.getMessage();
} catch (IOException e) {
log.error("failed to fetch content: ",e);
}
purgeMessage(msg);
// TODO Auto-generated method stub
super.messageExpired(context, message);
}
/**
* 清除隊(duì)列中的所有消息
*/
private void purgeMessage(Message message){
Destination r = message.getRegionDestination();
if(r instanceof Queue){
try {
//如果累積消息超過1000個(gè),清除隊(duì)列消息
if(((Queue) r).getMessages().size() > DEFAULT_PURGE_COUNT){
((Queue) r).purge();
}
} catch (Exception e) {
// TODO Auto-generated catch block
log.error("failed to purge queue "+r.getName(),e);
}
}
}
/**
* 當(dāng)消息發(fā)送時(shí),全部設(shè)置過期時(shí)間1個(gè)小時(shí),測(cè)試環(huán)境專用!!!
*/
@Override
public void send(ProducerBrokerExchange producerExchange,Message messageSend) throws Exception {
long oldExp = messageSend.getExpiration();
messageSend.setExpiration(oldExp < DEFAULT_EXPIRATION && oldExp > 0 ? oldExp : DEFAULT_EXPIRATION );
purgeMessage(messageSend);
super.send(producerExchange, messageSend);
}
}
然后,將這兩個(gè)類打包為myplugin.jar,并放在activemq啟動(dòng)目錄下的lib目錄下
最后,在activemq.xml文件中增加一個(gè)簡(jiǎn)單的spring配置項(xiàng):
<bean xmlns="
id="purgePlugin"
class="com.alibaba.napoli.plugins.MessageControlBrokerPlugin">
</bean>
然后,重啟activemq,就會(huì)發(fā)現(xiàn)這個(gè)插件已經(jīng)被加載。
【
BTrace
說明】
http://kenai.com/projects/btrace
是一個(gè)實(shí)時(shí)監(jiān)控工具,使用了
java agent
和
jvm attach
技術(shù),可以在不停機(jī)的情況下實(shí)時(shí)監(jiān)控線上程序的運(yùn)行情況,另外,對(duì)
btrace
腳本(實(shí)際上就是
java
程序)做了非常嚴(yán)格的安全限制,安全性很高,對(duì)應(yīng)用程序基本沒有影響。在性能方面,
cobar
進(jìn)行過測(cè)試,對(duì)方法進(jìn)行調(diào)用耗時(shí)統(tǒng)計(jì)的時(shí)候,基本消費(fèi)在微秒級(jí)別,可以說微不足道。
【背景】
在中文站
napoli
上線過程后,發(fā)現(xiàn)了一個(gè)奇怪的現(xiàn)象,盡管"已知"的
offer
發(fā)送端都已經(jīng)遷移到
napoli
系統(tǒng)中,但是老的
mq
系統(tǒng)仍然有新的
offer
消息進(jìn)來,因?yàn)檫B接
mq
的服務(wù)器非常多,定位消息來源成了一個(gè)非常大的問題。這種情況,想到了使用
BTrace
在某一臺(tái)服務(wù)器進(jìn)行線上監(jiān)控進(jìn)而期望發(fā)現(xiàn)這個(gè)幽靈。
【過程】
首先,我們需要知道兩個(gè)基本信息:消息類型和來源
ip
,這樣才可以定位
offer
消息的來源。
要知道來源
ip
,需要找到服務(wù)器端
管理的類,只有在建立
socket
的地方,才可以抓到具體
ip
,經(jīng)過分析
amq
代碼,發(fā)現(xiàn)
tcp
連接基本是由下面這個(gè)類來服務(wù)所有消息的接收的:
public class TcpTransport extends TransportThreadSupport implements Transport, Service, Runnable { private static final Log LOG = LogFactory.getLog(TcpTransport.class); private static final ThreadPoolExecutor SOCKET_CLOSE; protected final URI remoteLocation; protected final URI localLocation; protected final WireFormat wireFormat; protected int connectionTimeout = 30000; protected int soTimeout; protected int socketBufferSize = 64 * 1024; protected int ioBufferSize = 8 * 1024; protected boolean closeAsync=true;
protected Socket socket;
|
這個(gè)類中包含一個(gè)
socket
對(duì)象的成員變量,所有我們只要監(jiān)控
readCommand
方法,這個(gè)方法的返回值實(shí)際上就是一個(gè)
ActivemqObjectMessage
對(duì)象,這樣就可以在一個(gè)方法上加攔截器就可以同時(shí)捕獲到
ip
和消息對(duì)象,兩全其美!!!
protected Object readCommand() throws IOException { return wireFormat.unmarshal(dataIn); } |
因?yàn)樵?
ESB
消息通道都是一個(gè)隊(duì)列
ESBQueue
,所以無法通過隊(duì)列名稱來確定消息類型,必須通過
ESBTransferObject
對(duì)象來取得消息類型:
destType
,
offer
的區(qū)間是
1000-1008
public class ESBTransferObject implements Serializable {
private static final long serialVersionUID = -5975115234845303878L; /** * 消息體,原則上對(duì)象序列化后的XML數(shù)據(jù)(String) 注意使用XML1.1規(guī)范。 */ private Object content; /** * 用戶自定義數(shù)據(jù) */ private Object userDefineData; /** * 目的消息類型 */
private int destType = -1;
|
但是,在服務(wù)器端并沒有
ESBTransferObject
對(duì)象,無法反序列化(
BTrace
也不支持反序列化操作),所以沒有方法簡(jiǎn)單取得消息類型信息!!!
OK
,我不反序列化,直接拿二進(jìn)制
byte[]
,類型信息應(yīng)該是在固定位置的吧?但是發(fā)現(xiàn)這個(gè)對(duì)象
content
變長(zhǎng)字符串定義在類型之前,類型位置不確定了,暈倒啊
不死心,輸出二進(jìn)制數(shù)據(jù),柳暗花明啊,原來對(duì)象序列化的時(shí)候,
primitive
的
field
都是緊接著類型信息寫入的,所以,類型信息是在固定位置的
,類型信息始終是
255
,
256
兩個(gè)字節(jié)(實(shí)際上是
4
個(gè)字節(jié),但是目前我們只占有
2
個(gè))
Ok
,編寫代碼,測(cè)試環(huán)境運(yùn)行一下,暈倒,竟然有數(shù)組溢出!
使用
BTrace
,把這個(gè)數(shù)組打印下來(這個(gè)需要點(diǎn)技巧,
btrace
連
for
都不允許),竟然發(fā)現(xiàn)
位置偏移到
205
,
206
位置
,這個(gè)真的不知道什么原因,估計(jì)是客戶端發(fā)送的時(shí)候壓縮了,簡(jiǎn)單修改偏移量,測(cè)試運(yùn)行,
ok
,所有的消息類型和
ip
的對(duì)照表打印出來了。
package com.alibaba.btrace.script;
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace
public class AMQQueue2IP {
@OnMethod(clazz = "org.apache.activemq.transport.tcp.TcpTransport", //需要攔截的類名
method = "readCommand", //需要攔截的方法名
location = @Location(Kind.RETURN)) //攔截位置,方法返回時(shí)
public static void onTransportCommandExit(@Self Object transport, @Return Object command) { //捕獲調(diào)用對(duì)象和返回值
String commandName = str(command);
boolean isObjectMessage = (indexOf(commandName, "org.apache.activemq.command.ActiveMQObjectMessage") >= 0);
if (isObjectMessage) {
Object msg = command;
Object content = get(field(getSuperclass(getSuperclass(classOf(msg))), "content", false), msg);//捕獲消息內(nèi)容byte[]
byte[] bs = (byte[]) get(field(classOf(content), "data", false), content);
if (bs.length >= 206) {
int off = getInt(field(classOf(content), "offset", false), content);
int code = (0xff00&bs[205]<<8)+(0xff&bs[206]); //轉(zhuǎn)換205,206字節(jié)為消息類型
//println(str(code));
Object socket = get(field(classOf(transport), "socket"), transport);
String address = str(socket); //截取ip地址
int s = indexOf(address, "/");
int e = indexOf(address, ",");
int len = e - s;
String ip = substr(address, s + 1, e);
print(strcat(timestamp(),"---"));
println(strcat(strcat("ip: ", ip), strcat(" queueName: ", str(code))));
}
}
}
}
打印結(jié)果:
2/3/10 12:38 PM---ip: 172.22.2.34 queueName: 2001
2/3/10 12:38 PM---ip: 172.22.2.41 queueName: 5001
2/3/10 12:38 PM---ip: 172.22.2.22 queueName: 5001
2/3/10 12:38 PM---ip: 172.22.2.47 queueName: 2001
2/3/10 12:38 PM---ip: 172.22.2.31 queueName: 2001
2/3/10 12:38 PM---ip: 172.22.2.13 queueName: 5001
2/3/10 12:38 PM---ip: 172.22.2.6 queueName: 5001
2/3/10 12:38 PM---ip: 172.22.2.48 queueName: 2001
2/3/10 12:38 PM---ip: 172.22.2.39 queueName: 2001
【補(bǔ)充】
BTrace 是一個(gè)強(qiáng)大的工具,但是,在線上檢測(cè)的時(shí)候考慮時(shí)效性和安全性,必須有一個(gè)經(jīng)過檢驗(yàn)的腳本庫(kù)才可以安全及時(shí)的定位系統(tǒng)問題.
今天公司DNS切換,結(jié)果napoli這邊收到大量報(bào)警,這就奇怪了,數(shù)據(jù)都是正確的,報(bào)警的結(jié)果確都是錯(cuò)誤的,調(diào)試了一些腳本,發(fā)現(xiàn)有這個(gè)奇怪的文本" Binary file (standard input) matches "原來grep把輸入數(shù)據(jù)臨時(shí)文件當(dāng)成是二進(jìn)制文件了,這里加 -a 就可以解決這個(gè)問題。
但是,為什么文本文件會(huì)被當(dāng)成是二進(jìn)制文件?和今天的DNS切換有什么關(guān)系?分析后發(fā)現(xiàn),因?yàn)閐ns的問題,抓數(shù)據(jù)的腳本執(zhí)行時(shí)間明顯變長(zhǎng),這樣,在文件還在寫入的時(shí)候,監(jiān)控腳本就開始讀取數(shù)據(jù)文件,在這樣的并發(fā)訪問下,grep會(huì)認(rèn)為自己正在訪問一個(gè)binary文件,導(dǎo)致監(jiān)控誤報(bào)警。
最近在線上部署的ActiveMQ發(fā)生一次故障,因?yàn)橐慌_(tái)ActiveMQ故障將前臺(tái)的關(guān)鍵應(yīng)用全部連接掛住,根本原因有兩條:session的timeout設(shè)置不合理以及session池沒有限制大小。這里說的不是這個(gè)問題,而是在后續(xù)設(shè)置client的timeout過程中,有同學(xué)發(fā)現(xiàn)AMQ有一個(gè)嚴(yán)重的bug,timeout根本不起作用!!!
調(diào)試代碼發(fā)現(xiàn):
在Activemq的send response處理中,使用了一個(gè)BlockingQueue,在有timeout的方法里,使用了poll方法,這個(gè)方法的api說明中指出,當(dāng)timeout發(fā)生時(shí),這個(gè)方法返回null!!!
我們?cè)诳碅MQ經(jīng)過層層調(diào)用后,在ActiveMQConnection方法中如何處理這個(gè)返回值:
對(duì)返回值為空的情況沒有做任何處理,即使消息發(fā)送超時(shí),amq也認(rèn)為這個(gè)消息發(fā)送成功!估計(jì)這哥們理解poll在timeout的時(shí)候會(huì)拋出異常吧。
解決辦法很簡(jiǎn)單,在response為空的時(shí)候,拋出JMSException,告知發(fā)生Timeout錯(cuò)誤。
| |||||||||
日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
---|---|---|---|---|---|---|---|---|---|
25 | 26 | 27 | 28 | 29 | 30 | 31 | |||
1 | 2 | 3 | 4 | 5 | 6 | 7 | |||
8 | 9 | 10 | 11 | 12 | 13 | 14 | |||
15 | 16 | 17 | 18 | 19 | 20 | 21 | |||
22 | 23 | 24 | 25 | 26 | 27 | 28 | |||
29 | 30 | 1 | 2 | 3 | 4 | 5 |