概述
Thrift是一個可互操作和可伸縮服務的框架,用來進行可擴展且跨語言的服務的開發。它結合了功能強大的軟件堆棧和代碼生成引擎,以構建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 等等編程語言間無縫結合的、高效的服務。
Thrift最初由facebook開發,07年四月開放源碼,08年5月進入apache孵化器。thrift允許你定義一個簡單的定義文件中的數據類型和服務接口(IDL)。以作為輸入文件,編譯器生成代碼用來方便地生成RPC客戶端和服務器通信的無縫跨編程語言。
其傳輸數據采用二進制格式,相對於XML和JSON等序列化方式體積更小,對於高並發、大數據量和多語言的環境更有優勢。 Thrift它含有三個主要的組件:protocol,transport和server,其中,protocol定義了消息是怎樣序列化的,transport定義了消息是怎樣在客戶端和服務器端之間通信的,server用於從transport接收序列化的消息,根據protocol反序列化之,調用用戶定義的消息處理器,並序列化消息處理器的響應,然后再將它們寫回transport。
官網地址:thrift.apache.org
基本概念
架構圖

堆棧的頂部是從Thrift定義文件生成的代碼。Thrift 服務生成的客戶端和處理器代碼。這些由圖中的棕色框表示。紅色框為發送的數據結構(內置類型除外)也會生成代碼。協議和傳輸是Thrift運行時庫的一部分。因此使用Thrift可以定義服務,並且可以自由更改協議和傳輸,而無需重新生成代碼。 Thrift還包括一個服務器基礎結構,用於將協議和傳輸綁定在一起。有可用的阻塞,非阻塞,單線程和多線程服務器。 堆棧的“底層I / O”部分根據所開發語言而有所不同。對於Java和Python網絡I / O,Thrift庫利用內置庫,而C ++實現使用自己的自定義實現。
數據類型:
基本類型:
-
bool:布爾值,true 或 false,對應 Java 的 boolean
-
byte:8 位有符號整數,對應 Java 的 byte
-
i16:16 位有符號整數,對應 Java 的 short
-
i32:32 位有符號整數,對應 Java 的 int
-
i64:64 位有符號整數,對應 Java 的 long
-
double:64 位浮點數,對應 Java 的 double
-
string:未知編碼文本或二進制字符串,對應 Java 的 String
結構體類型:
-
struct:定義公共的對象,類似於 C 語言中的結構體定義,在 Java 中是一個 JavaBean
集合類型:
-
list:對應 Java 的 ArrayList
-
set:對應 Java 的 HashSet
-
map:對應 Java 的 HashMap
異常類型:
-
exception:對應 Java 的 Exception
服務類型:
-
service:對應服務的類
數據傳輸層Transport
-
TSocket —— 使用阻塞式 I/O 進行傳輸,是最常見的模式
-
TFramedTransport —— 使用非阻塞方式,按塊的大小進行傳輸,類似於 Java 中的 NIO,若使用 TFramedTransport 傳輸層,其服務器必須修改為非阻塞的服務類型
-
TNonblockingTransport —— 使用非阻塞方式,用於構建異步客戶端
數據傳輸協議Protocol
Thrift 可以讓用戶選擇客戶端與服務端之間傳輸通信協議的類別,在傳輸協議上總體划分為文本 (text) 和二進制 (binary) 傳輸協議,為節約帶寬,提高傳輸效率,一般情況下使用二進制類型的傳輸協議為多數,有時還會使用基於文本類型的協議,這需要根據項目 / 產品中的實際需求。
常用協議有以下幾種:
-
TBinaryProtocol : 二進制格式.
-
TCompactProtocol : 高效率的、密集的二進制壓縮格式
-
TJSONProtocol : JSON格式
-
TSimpleJSONProtocol : 提供JSON只寫協議, 生成的文件很容易通過腳本語言解析
注意:客戶端和服務端的協議要一致。
服務器類型Server
-
TSimpleServer ——單線程服務器端使用標准的阻塞式 I/O,一般用於測試。
-
TThreadPoolServer —— 多線程服務器端使用標准的阻塞式 I/O,預先創建一組線程處理請求。
-
TNonblockingServer —— 多線程服務器端使用非阻塞式 I/O,服務端和客戶端需要指定 TFramedTransport 數據傳輸的方式。
-
THsHaServer —— 半同步半異步的服務端模型,需要指定為: TFramedTransport 數據傳輸的方式。它使用一個單獨的線程來處理網絡I/O,一個獨立的worker線程池來處理消息。這樣,只要有空閑的worker線程,消息就會被立即處理,因此多條消息能被並行處理。
-
TThreadedSelectorServer —— TThreadedSelectorServer允許你用多個線程來處理網絡I/O。它維護了兩個線程池,一個用來處理網絡I/O,另一個用來進行請求的處理。當網絡I/O是瓶頸的時候,TThreadedSelectorServer比THsHaServer的表現要好。
實現邏輯
服務端
實現服務處理接口 impl
創建TProcessor 創建TServerTransport 創建TProtocol 創建TServer 啟動Server
客戶端
創建Transport 創建TProtocol 基於TTransport和TProtocol創建 Client 調用Client的相應方法
ThriftServerDemo實例
新建 Maven項目,並且添加 thrift依賴包
-
<dependencies> -
<dependency> -
<groupId>org.apache.thrift</groupId> -
<artifactId>libthrift</artifactId> -
<version>0.9.3</version> -
</dependency> -
<dependency> -
<groupId>org.slf4j</groupId> -
<artifactId>slf4j-log4j12</artifactId> -
<version>1.7.12</version> -
</dependency> -
<dependency> -
<groupId>org.apache.logging.log4j</groupId> -
<artifactId>log4j-api</artifactId> -
<version>2.7</version> -
</dependency> -
<dependency> -
<groupId>org.apache.logging.log4j</groupId> -
<artifactId>log4j-core</artifactId> -
<version>2.7</version> -
</dependency> -
</dependencies> -
<build> -
<plugins> -
<plugin> -
<groupId>org.apache.maven.plugins</groupId> -
<artifactId>maven-compiler-plugin</artifactId> -
<version>3.3</version> -
<configuration> -
<source>1.8</source> -
<target>1.8</target> -
<encoding>utf-8</encoding> -
</configuration> -
</plugin> -
</plugins> -
</build>
編寫 IDL接口並生成接口文件
-
namespace java thrift.service -
-
// 計算類型 - 僅限整數四則運算 -
enum ComputeType { -
ADD = 0; -
SUB = 1; -
MUL = 2; -
DIV = 3; -
} -
-
// 服務請求 -
struct ComputeRequest { -
1:required i64 x; -
2:required i64 y; -
3:required ComputeType computeType; -
} -
-
// 服務響應 -
struct ComputeResponse { -
1:required i32 errorNo; -
2:optional string errorMsg; -
3:required i64 computeRet; -
} -
-
service ComputeServer { -
ComputeResponse getComputeResult(1:ComputeRequest request); -
}
執行編譯命令:
-
thrift-0.11.0.exe -r -gen java computeServer.thrift
拷貝生成的 Service類文件到 IDEA

服務端接口實現
-
public class ThriftTestImpl implements ComputeServer.Iface { -
private static final Logger logger = LogManager.getLogger(ThriftTestImpl.class); -
public ComputeResponse getComputeResult(ComputeRequest request) { -
ComputeType computeType = request.getComputeType(); -
long x = request.getX(); -
long y = request.getY(); -
logger.info("get compute result begin. [x:{}] [y:{}] [type:{}]", x, y, computeType.toString()); -
long begin = System.currentTimeMillis(); -
ComputeResponse response = new ComputeResponse(); -
response.setErrorNo(0); -
try { -
long ret; -
if (computeType == ComputeType.ADD) { -
ret = add(x, y); -
response.setComputeRet(ret); -
} else if (computeType == ComputeType.SUB) { -
ret = sub(x, y); -
response.setComputeRet(ret); -
} else if (computeType == ComputeType.MUL) { -
ret = mul(x, y); -
response.setComputeRet(ret); -
} else { -
ret = div(x, y); -
response.setComputeRet(ret); -
} -
} catch (Exception e) { -
response.setErrorNo(1001); -
response.setErrorMsg(e.getMessage()); -
logger.error("exception:", e); -
} -
long end = System.currentTimeMillis(); -
logger.info("get compute result end. [errno:{}] cost:[{}ms]", response.getErrorNo(), (end - begin)); -
return response; -
} -
private long add(long x, long y) { -
return x + y; -
} -
private long sub(long x, long y) { -
return x - y; -
} -
private long mul(long x, long y) { -
return x * y; -
} -
private long div(long x, long y) { -
return x / y; -
} -
}
服務端實現
-
public class ServerMain { -
private static final Logger logger = LogManager.getLogger(ServerMain.class); -
-
public static void main(String[] args) { -
try { -
//實現服務處理接口impl -
ThriftTestImpl workImpl = new ThriftTestImpl(); -
//創建TProcessor -
TProcessor tProcessor = new ComputeServer.Processor<ComputeServer.Iface>(workImpl); -
//創建TServerTransport,非阻塞式 I/O,服務端和客戶端需要指定 TFramedTransport 數據傳輸的方式 -
final TNonblockingServerTransport transport = new TNonblockingServerSocket(9999); -
//創建TProtocol -
TThreadedSelectorServer.Args ttpsArgs = new TThreadedSelectorServer.Args(transport); -
ttpsArgs.transportFactory(new TFramedTransport.Factory()); -
//二進制格式反序列化 -
ttpsArgs.protocolFactory(new TBinaryProtocol.Factory()); -
ttpsArgs.processor(tProcessor); -
ttpsArgs.selectorThreads(16); -
ttpsArgs.workerThreads(32); -
logger.info("compute service server on port :" + 9999); -
//創建TServer -
TServer server = new TThreadedSelectorServer(ttpsArgs); -
//啟動Server -
server.serve(); -
} catch (Exception e) { -
logger.error(e); -
} -
} -
}
服務端整體代碼結構

log4j2.xml配置文件
-
<?xml version="1.0" encoding="UTF-8"?> -
<!--日志級別以及優先級排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> -
<!--Configuration后面的status,這個用於設置log4j2自身內部的信息輸出,可以不設置,當設置成trace時,你會看到log4j2內部各種詳細輸出--> -
<!--monitorInterval:Log4j能夠自動檢測修改配置 文件和重新配置本身,設置間隔秒數--> -
<configuration status="INFO" monitorInterval="30"> -
<!--先定義所有的appender--> -
<appenders> -
<!--這個輸出控制台的配置--> -
<console name="Console" target="SYSTEM_OUT"> -
<!--輸出日志的格式--> -
<PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [%l] %m%n}"/> -
</console> -
-
<RollingFile name="RollingFileInfo" fileName="log/log.log" filePattern="log/log.log.%d{yyyy-MM-dd}"> -
<!-- 只接受level=INFO以上的日志 --> -
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> -
<PatternLayout pattern="[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n"/> -
<Policies> -
<TimeBasedTriggeringPolicy modulate="true" interval="1"/> -
<SizeBasedTriggeringPolicy/> -
</Policies> -
</RollingFile> -
-
<RollingFile name="RollingFileError" fileName="log/error.log" filePattern="log/error.log.%d{yyyy-MM-dd}"> -
<!-- 只接受level=WARN以上的日志 --> -
<Filters> -
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" /> -
</Filters> -
<PatternLayout pattern="[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n"/> -
<Policies> -
<TimeBasedTriggeringPolicy modulate="true" interval="1"/> -
<SizeBasedTriggeringPolicy/> -
</Policies> -
</RollingFile> -
-
</appenders> -
-
<!--然后定義logger,只有定義了logger並引入的appender,appender才會生效--> -
<loggers> -
<!--過濾掉spring和mybatis的一些無用的DEBUG信息--> -
<logger name="org.springframework" level="INFO"></logger> -
<logger name="org.mybatis" level="INFO"></logger> -
<root level="all"> -
<appender-ref ref="Console"/> -
<appender-ref ref="RollingFileInfo"/> -
<appender-ref ref="RollingFileError"/> -
</root> -
</loggers> -
</configuration>
Jmeter測試類編寫
利用JMeter調用Java測試類去調用對應的后台服務,並記住每次調用並獲取反饋值的RT,ERR%,只需要按照單線程的方式去實現測試業務,也無需添加各種埋點收集數據
新建一個 JavaMaven工程,添加 JMeter及 thrift依賴包
-
<dependencies> -
<dependency> -
<groupId>org.apache.jmeter</groupId> -
<artifactId>ApacheJMeter_core</artifactId> -
<version>4.0</version> -
</dependency> -
<dependency> -
<groupId>org.apache.jmeter</groupId> -
<artifactId>ApacheJMeter_java</artifactId> -
<version>4.0</version> -
</dependency> -
-
<dependency> -
<groupId>org.apache.thrift</groupId> -
<artifactId>libthrift</artifactId> -
<version>0.11.0</version> -
</dependency> -
<dependency> -
<groupId>org.apache.logging.log4j</groupId> -
<artifactId>log4j-api</artifactId> -
<version>2.11.1</version> -
</dependency> -
<dependency> -
<groupId>org.apache.logging.log4j</groupId> -
<artifactId>log4j-core</artifactId> -
<version>2.11.1</version> -
</dependency> -
<dependency> -
<groupId>org.slf4j</groupId> -
<artifactId>slf4j-log4j12</artifactId> -
<version>1.7.25</version> -
</dependency> -
</dependencies> -
-
<build> -
<plugins> -
<plugin> -
<groupId>org.apache.maven.plugins</groupId> -
<artifactId>maven-compiler-plugin</artifactId> -
<version>3.7.0</version> -
<configuration> -
<source>1.8</source> -
<target>1.8</target> -
<encoding>utf-8</encoding> -
</configuration> -
</plugin> -
</plugins> -
</build>
ThriftClient測試類編寫
-
public class ThriftClient { -
private ComputeServer.Client client = null; -
private TTransport tTransport = null; -
-
public ThriftClient(String ip,int port){ -
try { -
TTransport tTransport = new TFramedTransport(new TSocket(ip,port)); -
tTransport.open(); -
TProtocol tProtocol = new TBinaryProtocol(tTransport); -
client = new ComputeServer.Client(tProtocol); -
} catch (TTransportException e) { -
e.printStackTrace(); -
} -
} -
-
public ComputeResponse getResponse(ComputeRequest request){ -
try { -
ComputeResponse response = client.getComputeResult(request); -
return response; -
} catch (TException e) { -
e.printStackTrace(); -
return null; -
} -
} -
-
public void close(){ -
if (tTransport != null && tTransport.isOpen()){ -
tTransport.close(); -
} -
} -
}
注意:需要把編寫 IDL接口文件拷貝到工程里
新建一個 JavaClass,如下例中的 TestThriftByJmeter,並繼承 AbstractJavaSamplerClient。 AbstractJavaSamplerClient中默認實現了四個可以覆蓋的方法,分別是 getDefaultParameters(), setupTest(), runTest()和 teardownTest()方法。
-
getDefaultParameters方法主要用於設置傳入界面的參數; -
setupTest方法為初始化方法,用於初始化性能測試時的每個線程; -
runTest方法為性能測試時的線程運行體; -
teardownTest方法為測試結束方法,用於結束性能測試中的每個線程。
編寫TestThriftByJmeter測試類
-
public class TestThriftByJmeter extends AbstractJavaSamplerClient { -
private ThriftClient client; -
private ComputeRequest request; -
private ComputeResponse response; -
-
//設置傳入界面的參數 -
@Override -
public Arguments getDefaultParameters(){ -
Arguments arguments = new Arguments(); -
arguments.addArgument("ip","172.16.14.251"); -
arguments.addArgument("port","9999"); -
arguments.addArgument("X","0"); -
arguments.addArgument("Y","0"); -
arguments.addArgument("type","0"); -
return arguments; -
} -
-
//初始化方法 -
@Override -
public void setupTest(JavaSamplerContext context){ -
//獲取Jmeter中設置的參數 -
String ip = context.getParameter("ip"); -
int port = context.getIntParameter("port"); -
int x = context.getIntParameter("X"); -
int y = context.getIntParameter("Y"); -
ComputeType type = ComputeType.findByValue(context.getIntParameter("type")); -
-
//創建客戶端 -
client = new ThriftClient(ip,port); -
//設置request請求 -
request = new ComputeRequest(x,y,type); -
super.setupTest(context); -
} -
-
//性能測試線程運行體 -
@Override -
public SampleResult runTest(JavaSamplerContext context) { -
SampleResult result = new SampleResult(); -
//開始統計響應時間標記 -
result.sampleStart(); -
try { -
long begin = System.currentTimeMillis(); -
response = client.getResponse(request); -
long cost = (System.currentTimeMillis() - begin); -
//打印時間戳差值。Java請求響應時間 -
System.out.println(response.toString()+" 總計花費:["+cost+"ms]"); -
-
if (response == null){ -
//設置測試結果為fasle -
result.setSuccessful(false); -
return result; -
} -
if (response.getErrorNo() == 0){ -
//設置測試結果為true -
result.setSuccessful(true); -
}else{ -
result.setSuccessful(false); -
result.setResponseMessage("ERROR"); -
} -
}catch (Exception e){ -
result.setSuccessful(false); -
result.setResponseMessage("ERROR"); -
e.printStackTrace(); -
}finally { -
//結束統計響應時間標記 -
result.sampleEnd(); -
} -
return result; -
} -
-
//測試結束方法 -
public void tearDownTest(JavaSamplerContext context) { -
if (client != null) { -
client.close(); -
} -
-
super.teardownTest(context); -
} -
-
}
特別說明:
-
result.setSamplerLabel("7D"); //設置java Sampler的標題 -
result.setResponseOK(); //設置響應成功 -
result.setResponseData(); //設置響應內容
編寫測試Run Main方法
-
public class RunMain { -
public static void main(String[] args) { -
Arguments arguments = new Arguments(); -
arguments.addArgument("ip","172.16.14.251"); -
arguments.addArgument("port","9999"); -
arguments.addArgument("X","1"); -
arguments.addArgument("Y","3"); -
arguments.addArgument("type","0"); -
JavaSamplerContext context = new JavaSamplerContext(arguments); -
TestThriftByJmeter jmeter = new TestThriftByJmeter(); -
-
jmeter.setupTest(context); -
jmeter.runTest(context); -
jmeter.tearDownTest(context); -
-
} -
}
測試結果通過

使用 mvn cleanpackage打包測試代碼

使用 mvn dependency:copy-dependencies-DoutputDirectory=lib復制所依賴的jar包都會到項目下的lib目錄下

復制測試代碼 jar包到 jmeter\lib\ext目錄下,復制依賴包到 jmeter\lib目錄下

這里有兩點需要注意:
-
如果你的jar依賴了其他第三方jar,需要將其一起放到lib/ext下,否則會出現ClassNotFound錯誤
-
如果在將jar放入lib/ext后,你還是無法找到你編寫的類,且此時你是開着JMeter的,則需要重啟一下JMeter
打開 Jmeter,在添加 Java請求時,注意要選擇 Jmeter測試類,下面的列表中可以看到參數和默認值。

下面我們將進行性能壓測,設置線程組,設置10個並發線程。

服務端日志:

