概述
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個併發執行緒。
服務端日誌: