在 GitHub 上見到過很多開源的自動(dòng)化框架內(nèi)都自帶了很多 Util 工具類,我們自己在開發(fā)自動(dòng)化框架也必然需要用到工具類庫(kù),那么這樣就會(huì)帶來一些問題:
那么,有沒有比較好的通用輪子讓我們直接使用呢?當(dāng)然有,今天我們來介紹一下工具類庫(kù)—Hutool
Hutool是一個(gè)小而全的Java工具類庫(kù),通過靜態(tài)方法封裝,降低相關(guān)API的學(xué)習(xí)成本,提高工作效率,使Java擁有函數(shù)式語言般的優(yōu)雅,讓Java語言也可以“甜甜的”。 Hutool中的工具方法來自于每個(gè)用戶的精雕細(xì)琢,它涵蓋了Java開發(fā)底層代碼中的方方面面,它既是大型項(xiàng)目開發(fā)中解決小問題的利器,也是小型項(xiàng)目中的效率擔(dān)當(dāng); Hutool是項(xiàng)目中“util”包友好的替代,它節(jié)省了開發(fā)人員對(duì)項(xiàng)目中公用類和公用工具方法的封裝時(shí)間,使開發(fā)專注于業(yè)務(wù),同時(shí)可以最大限度的避免封裝不完善帶來的bug。
以上是摘自官網(wǎng)的介紹,如果我們有需要用到某些工具方法的時(shí)候,不妨在Hutool里面找找。
官網(wǎng)地址:https://github.com/looly/hutool
一個(gè)Java基礎(chǔ)工具類,對(duì)文件、流、加密解密、轉(zhuǎn)碼、正則、線程、XML等JDK方法進(jìn)行封裝,組成各種Util工具類,同時(shí)提供以下組件:
可以根據(jù)需求對(duì)每個(gè)模塊單獨(dú)引入,也可以通過引入hutool-all方式引入所有模塊。
在項(xiàng)目的pom.xml的dependencies中加入以下內(nèi)容:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.2</version>
</dependency>
compile 'cn.hutool:hutool-all:5.4.2'
點(diǎn)擊以下任一鏈接,下載hutool-all-X.X.X.jar即可:
注意 Hutool 5.x支持 JDK8+,對(duì) Android 平臺(tái)沒有測(cè)試,不能保證所有工具類或工具方法可用。 如果你的項(xiàng)目使用 JDK7,請(qǐng)使用 Hutool 4.x 版本
類型轉(zhuǎn)換工具類,用于各種類型數(shù)據(jù)的轉(zhuǎn)換。
@Test(description="Convert使用:類型轉(zhuǎn)換工具類")
public void covert() {
int a=1;
String aStr=Convert.toStr(a);
//轉(zhuǎn)換為指定類型數(shù)組
String[] b={"1", "2", "3", "4"};
Integer[] bArr=Convert.toIntArray(b);
log.info(bArr.toString());
//轉(zhuǎn)換為日期對(duì)象
String dateStr="2020-09-17";
Date date=Convert.toDate(dateStr);
log.info(date.toString());
//轉(zhuǎn)換為列表
String[] strArr={"a", "b", "c", "d"};
List<String> strList=Convert.toList(String.class, strArr);
log.info(strList.toString());
}
運(yùn)行結(jié)果:
[Ljava.lang.Integer;@4c0884e8
Thu Sep 17 00:00:00 CST 2020
[a, b, c, d]
日期時(shí)間工具類,定義了一些常用的日期時(shí)間操作方法。
@Test(description="DateUtil使用:日期時(shí)間工具")
public void dateUtil() {
//Date、long、Calendar之間的相互轉(zhuǎn)換
//當(dāng)前時(shí)間
Date date=DateUtil.date();
log.info(date.toString());
//Calendar轉(zhuǎn)Date
date=DateUtil.date(Calendar.getInstance());
//時(shí)間戳轉(zhuǎn)Date
date=DateUtil.date(System.currentTimeMillis());
//自動(dòng)識(shí)別格式轉(zhuǎn)換
String dateStr="2020-09-17";
date=DateUtil.parse(dateStr);
//自定義格式化轉(zhuǎn)換
date=DateUtil.parse(dateStr, "yyyy-MM-dd");
//格式化輸出日期
String format=DateUtil.format(date, "yyyy-MM-dd");
log.info(format.toString());
//獲得年的部分
int year=DateUtil.year(date);
//獲得月份,從0開始計(jì)數(shù)
int month=DateUtil.month(date);
//獲取某天的開始、結(jié)束時(shí)間
Date beginOfDay=DateUtil.beginOfDay(date);
Date endOfDay=DateUtil.endOfDay(date);
//計(jì)算偏移后的日期時(shí)間
Date newDate=DateUtil.offset(date, DateField.DAY_OF_MONTH, 2);
//計(jì)算日期時(shí)間之間的偏移量
long betweenDay=DateUtil.between(date, newDate, DateUnit.DAY);
}
運(yùn)行結(jié)果:
2020-09-17 18:42:22
2020-09-17
字符串工具類,定義了一些常用的字符串操作方法。
@Test(description="StrUtil使用:字符串工具")
public void strUtil() {
//判斷是否為空字符串
String str="test";
StrUtil.isEmpty(str);
StrUtil.isNotEmpty(str);
//去除字符串的前后綴
StrUtil.removeSuffix("a.jpg", ".jpg");
StrUtil.removePrefix("a.jpg", "a.");
//格式化字符串
String template="這只是個(gè)占位符:{}";
String str2=StrUtil.format(template, "我是占位符");
log.info("/strUtil format:{}", str2);
}
運(yùn)行結(jié)果:
/strUtil format:這只是個(gè)占位符:我是占位符
獲取 classPath 下的文件,在 Tomcat 等容器下,classPath一般是 WEB-INF/classes。
@Test(description="ClassPath單一資源訪問類:在classPath下查找文件")
public void classPath() throws IOException {
//獲取定義在src/main/resources文件夾中的配置文件
ClassPathResource resource=new ClassPathResource("generator.properties");
Properties properties=new Properties();
properties.load(resource.getStream());
log.info("/classPath:{}", properties);
}
運(yùn)行結(jié)果:
/classPath:{jdbc.userId=root, jdbc.password=root, jdbc.driverClass=com.mysql.cj.jdbc.Driver, jdbc.connectionURL=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai}
Java反射工具類,可用于反射獲取類的方法及創(chuàng)建對(duì)象。
@Test(description="ReflectUtil使用:Java反射工具類")
public void reflectUtil() {
//獲取某個(gè)類的所有方法
Method[] methods=ReflectUtil.getMethods(Dog.class);
//獲取某個(gè)類的指定方法
Method method=ReflectUtil.getMethod(Dog.class, "getName");
//使用反射來創(chuàng)建對(duì)象
Dog dog=ReflectUtil.newInstance(Dog.class);
//反射執(zhí)行對(duì)象的方法
ReflectUtil.invoke(dog, "setName","大黃");
log.info(dog.getName());
}
Dog
Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Dog {
private String name;
private Float weight;
}
運(yùn)行結(jié)果:
大黃
數(shù)字處理工具類,可用于各種類型數(shù)字的加減乘除操作及判斷類型。
@Test(description="NumberUtil使用:數(shù)字處理工具類")
public void numberUtil() {
double n1=1.234;
double n2=1.234;
double result;
//對(duì)float、double、BigDecimal做加減乘除操作
result=NumberUtil.add(n1, n2);
result=NumberUtil.sub(n1, n2);
result=NumberUtil.mul(n1, n2);
result=NumberUtil.div(n1, n2);
//保留兩位小數(shù)
BigDecimal roundNum=NumberUtil.round(n1, 2);
String n3="1.234";
//判斷是否為數(shù)字、整數(shù)、浮點(diǎn)數(shù)
NumberUtil.isNumber(n3);
NumberUtil.isInteger(n3);
NumberUtil.isDouble(n3);
}
JavaBean的工具類,可用于Map與JavaBean對(duì)象的互相轉(zhuǎn)換以及對(duì)象屬性的拷貝。
@Test(description="BeanUtil使用:JavaBean的工具類")
public void beanUtil() {
Dog dog=new Dog();
dog.setName("大黃");
dog.setWeight(5.14f);
//Bean轉(zhuǎn)Map
Map<String, Object> map=BeanUtil.beanToMap(dog);
log.info("beanUtil bean to map:{}", map);
//Map轉(zhuǎn)Bean
Dog mapDog=BeanUtil.mapToBean(map, Dog.class, false);
log.info("beanUtil map to bean:{}", mapDog);
//Bean屬性拷貝
Dog copyDog=new Dog();
BeanUtil.copyProperties(dog, copyDog);
log.info("beanUtil copy properties:{}", copyDog);
}
運(yùn)行結(jié)果:
beanUtil bean to map:{name=大黃, weight=5.14}
beanUtil map to bean:Dog(name=大黃, weight=5.14)
beanUtil copy properties:Dog(name=大黃, weight=5.14)
集合操作的工具類,定義了一些常用的集合操作。
@Test(description="CollUtil使用:集合工具類")
public void collUtil() {
//數(shù)組轉(zhuǎn)換為列表
String[] array=new String[]{"a", "b", "c", "d", "e"};
List<String> list=CollUtil.newArrayList(array);
//join:數(shù)組轉(zhuǎn)字符串時(shí)添加連接符號(hào)
String joinStr=CollUtil.join(list, ",");
log.info("collUtil join:{}", joinStr);
//將以連接符號(hào)分隔的字符串再轉(zhuǎn)換為列表
List<String> splitList=StrUtil.split(joinStr, ',');
log.info("collUtil split:{}", splitList);
//創(chuàng)建新的Map、Set、List
HashMap<Object, Object> newMap=CollUtil.newHashMap();
HashSet<Object> newHashSet=CollUtil.newHashSet();
ArrayList<Object> newList=CollUtil.newArrayList();
//判斷列表是否為空
CollUtil.isEmpty(list);
CollUtil.isNotEmpty(list);
}
運(yùn)行結(jié)果:
collUtil join:a,b,c,d,e
collUtil split:[a, b, c, d, e]
Map操作工具類,可用于創(chuàng)建 Map 對(duì)象及判斷 Map 是否為空。
@Test(description="MapUtil使用:Map工具類")
public void mapUtil() {
//將多個(gè)鍵值對(duì)加入到Map中
Map<Object, Object> map=MapUtil.of(new String[][]{
{"key1", "value1"},
{"key2", "value2"},
{"key3", "value3"}
});
//判斷Map是否為空
MapUtil.isEmpty(map);
MapUtil.isNotEmpty(map);
}
加密解密工具類,可用于 MD5 加密。
@Test(description="SecureUtil使用:加密解密工具類")
public void secureUtil() {
//MD5加密
String str="123456";
String md5Str=SecureUtil.md5(str);
log.info("secureUtil md5:{}", md5Str);
}
運(yùn)行結(jié)果:
secureUtil md5:e10adc3949ba59abbe56e057f20f883e
驗(yàn)證碼工具類,可用于生成圖形驗(yàn)證碼。
@Test(description="CaptchaUtil使用:圖形驗(yàn)證碼")
public void captchaUtil(HttpServletRequest request, HttpServletResponse response) {
//生成驗(yàn)證碼圖片
LineCaptcha lineCaptcha=CaptchaUtil.createLineCaptcha(200, 100);
try {
request.getSession().setAttribute("CAPTCHA_KEY", lineCaptcha.getCode());
response.setContentType("image/png");//告訴瀏覽器輸出內(nèi)容為圖片
response.setHeader("Pragma", "No-cache");//禁止瀏覽器緩存
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expire", 0);
lineCaptcha.write(response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
字段驗(yàn)證器,驗(yàn)證給定字符串是否滿足指定條件,一般用在表單字段驗(yàn)證里。
@Test(description="Validator使用:字段驗(yàn)證器")
public void validator() {
//判斷是否為郵箱地址
boolean result=Validator.isEmail("zuozewei@hotmail.com");
log.info("Validator isEmail:{}", result);
//判斷是否為手機(jī)號(hào)碼
result=Validator.isMobile("18911111111");
log.info("Validator isMobile:{}", result);
//判斷是否為IPV4地址
result=Validator.isIpv4("192.168.3.101");
log.info("Validator isIpv4:{}", result);
//判斷是否為漢字
result=Validator.isChinese("你好");
log.info("Validator isChinese:{}", result);
//判斷是否為身份證號(hào)碼(18位中國(guó))
result=Validator.isCitizenId("123456");
log.info("Validator isCitizenId:{}", result);
//判斷是否為URL
result=Validator.isUrl("http://www.7d.com");
log.info("Validator isUrl:{}", result);
//判斷是否為生日
result=Validator.isBirthday("2020-09-17");
log.info("Validator isBirthday:{}", result);
}
運(yùn)行結(jié)果:
Validator isEmail:true
Validator isMobile:true
Validator isIpv4:true
Validator isChinese:true
Validator isCitizenId:false
Validator isUrl:true
Validator isBirthday:true
JSON 解析工具類,針對(duì) JSONObject 和 JSONArray 的靜態(tài)快捷方法集合。
@Test(description="JSONUtil使用:JSON解析工具類")
public void jsonUtil() {
Dog dog=new Dog();
dog.setName("大黃");
dog.setWeight(5.14f);
//對(duì)象轉(zhuǎn)化為JSON字符串
String jsonStr=JSONUtil.parse(dog).toString();
log.info("jsonUtil parse:{}", jsonStr);
//JSON字符串轉(zhuǎn)化為對(duì)象
Dog dogBean=JSONUtil.toBean(jsonStr, Dog.class);
log.info("jsonUtil toBean:{}", dogBean);
List<Dog> dogList=new ArrayList<>();
dogList.add(dog);
String jsonListStr=JSONUtil.parse(dogList).toString();
//JSON字符串轉(zhuǎn)化為列表
dogList=JSONUtil.toList(new JSONArray(jsonListStr), Dog.class);
log.info("jsonUtil toList:{}", dogList);
}
運(yùn)行結(jié)果:
jsonUtil parse:{"weight":5.14,"name":"大黃"}
jsonUtil toBean:Dog(name=大黃, weight=5.14)
jsonUtil toList:[Dog(name=大黃, weight=5.14)]
隨機(jī)工具類,RandomUtil 主要針對(duì) JDK 中 Random 對(duì)象做封裝。
@Test(description="RandomUtil使用:隨機(jī)工具類")
public void randomUtil() {
int result;
String uuid;
//獲得指定范圍內(nèi)的隨機(jī)數(shù)
result=RandomUtil.randomInt(1, 100);
log.info("randomInt:{}",StrUtil.toString(result));
//獲得隨機(jī)UUID
uuid=RandomUtil.randomUUID();
log.info("randomUUID:{}", uuid);
}
運(yùn)行結(jié)果:
randomInt:9
randomUUID:8aec5890-72ab-4d72-a37d-d36acba83b58
摘要算法工具類,支持常見摘要算法 MD2、MD5、SHA-1、SHA-256、SHA-384、SHA-512等。
@Test(description="DigestUtil使用:摘要算法工具類")
public void digestUtil() {
String password="123456";
//計(jì)算MD5摘要值,并轉(zhuǎn)為16進(jìn)制字符串
String result=DigestUtil.md5Hex(password);
log.info("DigestUtil md5Hex:{}", result);
//計(jì)算SHA-256摘要值,并轉(zhuǎn)為16進(jìn)制字符串
result=DigestUtil.sha256Hex(password);
log.info("DigestUtil sha256Hex:{}", result);
//生成Bcrypt加密后的密文,并校驗(yàn)
String hashPwd=DigestUtil.bcrypt(password);
boolean check=DigestUtil.bcryptCheck(password,hashPwd);
log.info("DigestUtil bcryptCheck:{}", check);
}
運(yùn)行結(jié)果:
DigestUtil md5Hex:e10adc3949ba59abbe56e057f20f883e
DigestUtil sha256Hex:8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
DigestUtil bcryptCheck:true
Http客戶端工具類,應(yīng)對(duì)簡(jiǎn)單場(chǎng)景下Http請(qǐng)求的工具類封裝,此工具封裝了HttpRequest對(duì)象常用操作,可以保證在一個(gè)方法之內(nèi)完成Http請(qǐng)求。
此模塊基于JDK的HttpUrlConnection封裝完成,完整支持https、代理和文件上傳。
@Test(description="HttpUtil使用:Http請(qǐng)求工具類")
public void httpUtil() {
String response=HttpUtil.get("http://example.com/");
log.info("HttpUtil get:{}", response);
}
運(yùn)行結(jié)果:
2020-09-17 18:48:27.328 INFO 12004 --- [ main] com.zuozewei.demo.example.example : HttpUtil get:<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
Hutool中的工具類很多,可以參考:https://www.hutool.cn/
測(cè)試開發(fā)過程中要善于半開源,半代碼的方式,節(jié)省開發(fā)時(shí)間,合理利用輪子,提高工作效率。
文章源碼:
參考資料:
家好,很高興又見面了,我是姜茶的編程筆記,我們一起學(xué)習(xí)前端相關(guān)領(lǐng)域技術(shù),共同進(jìn)步,也歡迎大家關(guān)注、點(diǎn)贊、收藏、轉(zhuǎn)發(fā),您的支持是我不斷創(chuàng)作的動(dòng)力
在 JavaScript 中,Array.prototype.map 是一個(gè)常用的方法,它對(duì)數(shù)組的每個(gè)元素調(diào)用提供的函數(shù),并返回一個(gè)新數(shù)組,包含函數(shù)的返回值。為了更好地理解其內(nèi)部機(jī)制,今天我們將從零開始,實(shí)現(xiàn)一個(gè)自定義的 map 方法,并詳細(xì)解析其實(shí)現(xiàn)步驟。
首先,我們創(chuàng)建一個(gè) myMap 方法,將其添加到 Array.prototype 上。這個(gè)方法接受兩個(gè)參數(shù):一個(gè)回調(diào)函數(shù) callback 和一個(gè)可選的 thisArg 參數(shù)。callback 函數(shù)將對(duì)數(shù)組的每個(gè)元素進(jìn)行操作,thisArg 是執(zhí)行回調(diào)函數(shù)時(shí)的 this 值。
Array.prototype.myMap=function(callback, thisArg) {
// 將傳入的數(shù)組轉(zhuǎn)換為對(duì)象
const O=Object(this);
// 獲取數(shù)組的長(zhǎng)度
const len=O.length >>> 0;
// 如果 callback 不是函數(shù),則拋出 TypeError
if (typeof callback !=='function') {
throw new TypeError(callback + ' is not a function');
}
// 創(chuàng)建一個(gè)新的數(shù)組,用于存儲(chǔ)映射結(jié)果
const result=new Array(len);
// 遍歷數(shù)組并應(yīng)用回調(diào)函數(shù)
for (let i=0; i < len; i++) {
// 只有當(dāng)元素在數(shù)組中存在時(shí)才進(jìn)行映射
if (i in O) {
// 調(diào)用回調(diào)函數(shù),傳遞 thisArg 作為 this 值,并傳遞當(dāng)前元素、索引、數(shù)組本身
result[i]=callback.call(thisArg, O[i], i, O);
}
}
return result;
};
讓我們逐步解析這個(gè)實(shí)現(xiàn)。
我們首先將 this 轉(zhuǎn)換為一個(gè)對(duì)象,以便在方法中使用:
const O=Object(this);
使用 >>> 0 操作符將長(zhǎng)度轉(zhuǎn)換為無符號(hào)整數(shù),確保其為正整數(shù):
const len=O.length >>> 0;
如果 callback 不是函數(shù),則拋出一個(gè) TypeError:
if (typeof callback !=='function') {
throw new TypeError(callback + ' is not a function');
}
我們創(chuàng)建一個(gè)與原數(shù)組長(zhǎng)度相同的新數(shù)組,用于存儲(chǔ)映射結(jié)果:
const result=new Array(len);
使用 for 循環(huán)遍歷數(shù)組的每個(gè)索引。如果當(dāng)前索引存在于數(shù)組中,則調(diào)用回調(diào)函數(shù),并將結(jié)果存儲(chǔ)在 result 數(shù)組中:
for (let i=0; i < len; i++) {
if (i in O) {
result[i]=callback.call(thisArg, O[i], i, O);
}
}
最后,我們返回存儲(chǔ)映射結(jié)果的 result 數(shù)組:
return result;
下面是一個(gè)示例,展示如何使用自定義的 myMap 方法:
const numbers=[1, 2, 3, 4, 5];
const doubled=numbers.myMap(x=> x * 2);
console.log(doubled); // 輸出 [2, 4, 6, 8, 10]
在這個(gè)示例中,我們將 numbers 數(shù)組中的每個(gè)元素都乘以 2,并將結(jié)果存儲(chǔ)在 doubled 數(shù)組中。結(jié)果輸出為 [2, 4, 6, 8, 10],這與內(nèi)置的 Array.prototype.map 方法的行為一致。
通過實(shí)現(xiàn)自定義的 map 方法,我們深入理解了 JavaScript 中數(shù)組的操作方式。希望你可以更好地掌握 map 方法的內(nèi)部機(jī)制,并提升你的 JavaScript 編程技巧。
如果你有任何問題或建議,歡迎在評(píng)論區(qū)留言交流!祝你編程愉快!
隨著公司業(yè)務(wù)的不斷擴(kuò)張,用戶流量在不斷提升,研發(fā)體系的規(guī)模和復(fù)雜性也隨之增加。線上服務(wù)的穩(wěn)定性也越來越重要,服務(wù)性能問題,以及容量問題也越發(fā)明顯。
因此有必要搭建一個(gè)有效壓測(cè)系統(tǒng),提供安全、高效、真實(shí)的線上全鏈路壓測(cè)服務(wù),為線上服務(wù)保駕護(hù)航。
關(guān)于全鏈路壓測(cè)的建設(shè),業(yè)界已經(jīng)有了非常多文章,但是涉及到具體的技術(shù)實(shí)現(xiàn)方面,卻很少介紹。本文想從全鏈路壓測(cè)系統(tǒng),從設(shè)計(jì)到落地整個(gè)實(shí)踐過程,來詳細(xì)介紹下全鏈路壓測(cè)系統(tǒng)是具體是如何設(shè)計(jì),以及如何落地的。希望能從技術(shù)落地實(shí)踐的角度,給同行業(yè)的同學(xué)一些參考和啟發(fā)。
全鏈路壓測(cè)在業(yè)內(nèi)已經(jīng)有了廣泛的實(shí)踐,如阿里的 Amazon、PTS[1][2],美團(tuán)的 Quake[3][4],京東的的 ForceBOT[5],高德的 TestPG[6]等等,都為我們提供豐富的實(shí)踐經(jīng)驗(yàn),和大量?jī)?yōu)秀的技術(shù)方案。我們廣泛吸收了各大互聯(lián)網(wǎng)公司的全鏈路壓測(cè)建設(shè)經(jīng)驗(yàn),并基于字節(jié)跳動(dòng)業(yè)務(wù)需求,設(shè)計(jì)開發(fā)了一個(gè)全鏈路壓測(cè)系統(tǒng) Rhino。
Rhino 平臺(tái)作為公司級(jí)的全鏈路壓測(cè)平臺(tái),它的目標(biāo)是對(duì)全公司所有業(yè)務(wù),提供單服務(wù)、全鏈路,安全可靠、真實(shí)、高效的壓測(cè),來幫助業(yè)務(wù)高效便捷的完成性能測(cè)試任務(wù),更精確評(píng)估線上服務(wù)性能&容量方面風(fēng)險(xiǎn)。
因此在 Rhino 平臺(tái)設(shè)計(jì)之初,我們就定下以下目標(biāo):
Rhino 是一個(gè)分布式全鏈路壓測(cè)系統(tǒng),可以通過水平擴(kuò)展,來實(shí)現(xiàn)模擬海量用戶真實(shí)的業(yè)務(wù)操作場(chǎng)景,對(duì)線上各種業(yè)務(wù)進(jìn)行全方位的性能測(cè)試。它主要分為控制中心(Rhino Master)模塊,壓測(cè)鏈路服務(wù)模塊,監(jiān)控系統(tǒng)模塊,壓測(cè)引擎模塊,如圖。(每一個(gè)模塊都是由多個(gè)微服務(wù)來完成的。如下圖每個(gè)實(shí)線圖都代表一個(gè)微服務(wù)或多個(gè)微服務(wù))。
壓測(cè)過程中數(shù)據(jù)構(gòu)造是最重要,也是最為復(fù)雜的環(huán)節(jié)。壓測(cè)數(shù)據(jù)的建模,直接影響了壓測(cè)結(jié)果的準(zhǔn)確性。
為了高效的構(gòu)造特定的 Fake 壓測(cè)數(shù)據(jù),Rhino 壓測(cè)平臺(tái)提供大量數(shù)據(jù)構(gòu)造方式:
在壓測(cè)過程中,有些壓測(cè)請(qǐng)求需要進(jìn)行登錄,并保持會(huì)話;此外在很多壓測(cè)請(qǐng)求中涉及到用戶賬號(hào)信息 UserID,DeviceID 等數(shù)據(jù)。用戶賬號(hào)的構(gòu)造問題,一直是壓測(cè)過程中非常棘手的問題。Rhino 平臺(tái)打通的用戶中心,設(shè)置了壓測(cè)專屬的賬號(hào)服務(wù),完美地解決了壓測(cè)過程中的登錄態(tài),以及測(cè)試賬號(hào)等問題。具體流程和使用界面,如下圖。
壓測(cè)隔離中需要解決的壓測(cè)流量隔離,以及壓測(cè)數(shù)據(jù)的隔離。
壓測(cè)流量隔離,主要是通過構(gòu)建壓測(cè)環(huán)境來解決,如線下壓測(cè)環(huán)境,或泳道化/Set 化建設(shè),將壓測(cè)流量與線上流程完全隔離。優(yōu)點(diǎn)是壓測(cè)流量與線上流量完全隔離,不會(huì)影響到線上用戶。缺點(diǎn):機(jī)器資源及維護(hù)成本高,且壓測(cè)結(jié)果需要經(jīng)過一定的換算,才能得線上容量,結(jié)果準(zhǔn)確性存在一定的問題。目前公司內(nèi)壓測(cè)都是在線上集群上完成的,線上泳道化正在建設(shè)中。
壓測(cè)數(shù)據(jù)隔離,主要是通過對(duì)壓測(cè)流量進(jìn)行染色,讓線上服務(wù)能識(shí)別哪些是壓測(cè)流量,哪些是正常流量,然后對(duì)壓測(cè)流量進(jìn)行特殊處理,以達(dá)到數(shù)據(jù)隔離的目的。目前 Rhino 平臺(tái)整體壓測(cè)隔離框架如圖。
壓測(cè)標(biāo)記就是最常見的壓測(cè)流量染色的方式。
目前公司內(nèi)各個(gè)基礎(chǔ)組件、存儲(chǔ)組件,以及 RPC 框架都已經(jīng)支持了壓測(cè)標(biāo)記的透?jìng)鳌F湓硎菍簻y(cè)標(biāo)記的 KV 值存入 Context 中,然后在所有下游請(qǐng)求中都帶上該 Context,下游服務(wù)可以根據(jù) Context 中壓測(cè)標(biāo)記完成對(duì)壓測(cè)流量的處理。在實(shí)際業(yè)務(wù)中,代碼改造也非常簡(jiǎn)單,只需要透?jìng)?Context 即可。
Golang 服務(wù): 將壓測(cè)標(biāo)記寫入 Context 中。
Python 服務(wù):利用 threading.local()存儲(chǔ)線程 Context。
Java 服務(wù):利用 ThreadLocal 存儲(chǔ)線程 Context。
為了解決線上壓測(cè)安全問題,我們還引入了壓測(cè)開關(guān)組件。
線上壓測(cè)中,最復(fù)雜的問題就是壓測(cè)鏈路中涉及到寫操作,如何避免污染線上數(shù)據(jù),并且能保證壓測(cè)請(qǐng)求保持和線上相同的請(qǐng)求路徑。業(yè)界有很多解決方案,常見的有影子表,影子庫(kù),以及數(shù)據(jù)偏移,如圖[7]。
Rhino 平臺(tái)針對(duì)不同存儲(chǔ),有不同的解決方案:
在壓測(cè)之前,需要對(duì)服務(wù)進(jìn)行壓測(cè)驗(yàn)證。對(duì)于不滿足壓測(cè)要求(即壓測(cè)數(shù)據(jù)隔離)的服務(wù),需要進(jìn)行壓測(cè)改造。
a. 盡量減少代碼改動(dòng),并給出完整的指導(dǎo)手冊(cè)及代碼示例,減少 RD 的工作量,降低代碼錯(cuò)誤的可能性
b. 提供簡(jiǎn)單便捷的線上線下 HTTP&RPC 的壓測(cè)請(qǐng)求 Debug 工具,方便代碼改動(dòng)的驗(yàn)證
c. 對(duì)于新項(xiàng)目,在項(xiàng)目開始初期,就將壓測(cè)改造加入項(xiàng)目開發(fā)規(guī)范中,減少后期的代碼改動(dòng)
請(qǐng)求調(diào)用鏈,對(duì)于線上壓測(cè)是非常重要的:
Rhino 平臺(tái)通過公司的流式日志系統(tǒng)來完成調(diào)用鏈檢索的。一個(gè)服務(wù)在被請(qǐng)求或者請(qǐng)求下游時(shí),都會(huì)透?jìng)饕粋€(gè) LogID。RPC 框架會(huì)打印調(diào)用鏈日志(包括 RPC 日志-調(diào)用者日志,Access 日志-被調(diào)用者日志),所有日志中都會(huì)包含這個(gè) LogID。通過 LogID 將一個(gè)請(qǐng)求所經(jīng)過的所有服務(wù)日志串起來,就完成調(diào)用鏈檢索。
Rhino 平臺(tái)在公司流式日志系統(tǒng)提供的鏈路梳理功能基礎(chǔ)上,進(jìn)行了進(jìn)一步優(yōu)化,以滿足壓測(cè)需要:
雖然 Rhino 平臺(tái)對(duì)于壓測(cè)有很多的安全保障措施,但是對(duì)于大型壓測(cè),保證信息的通暢流通也是非常重要的。因此在壓測(cè)周知方面,Rhino 平臺(tái)也提供了很多解決方案:
在壓測(cè)之前,需要開啟整體鏈路的壓測(cè)開關(guān)的,否則壓測(cè)流量就會(huì)被服務(wù)拒絕,導(dǎo)致壓測(cè)失敗。
對(duì)于調(diào)用鏈中不能壓測(cè)的服務(wù)(敏感服務(wù)),或者第三方服務(wù),為了壓測(cè)請(qǐng)求的完整性,就需要對(duì)這些服務(wù)進(jìn)行 Mock。業(yè)界通用的 Mock 方案有:
由于字節(jié)整個(gè)公司都采用微服務(wù)架構(gòu),導(dǎo)致一次壓測(cè)涉及鏈路都比較長(zhǎng),快速無業(yè)務(wù)入侵的 Mock
方式成為了首選。Rhino 平臺(tái)是通過公司 Service Mesh 和 ByteMock 系統(tǒng)來實(shí)現(xiàn)了高效的,對(duì)業(yè)務(wù)透明的服務(wù) Mock。
壓測(cè)執(zhí)行前,Rhino 平臺(tái)需要向 Service Mesh 注冊(cè)染色轉(zhuǎn)發(fā)規(guī)則,并向 Mock 服務(wù)注冊(cè) Mock 規(guī)則。然后在壓測(cè)流量中注入 Mock 染色標(biāo)記,才能完成服務(wù) Mock:
Rhino 平臺(tái)中,壓測(cè) Agent 就是一個(gè)最小調(diào)度單元。一次壓測(cè)任務(wù),通常會(huì)拆分成多個(gè)子 Job,然后下發(fā)到多個(gè) Agent 上來完成。
2020 年春節(jié)搶紅包壓測(cè)中,Rhino 臨時(shí)擴(kuò)容在 4000+個(gè)實(shí)例,支撐了單次 3kw+QPS 的壓測(cè),但日常 Rhino 平臺(tái)只部署了 100+個(gè)實(shí)例,就能滿足日常壓測(cè)需求。
Rhino 平臺(tái)默認(rèn)將全鏈路壓測(cè)分為公網(wǎng)壓測(cè)和內(nèi)網(wǎng)壓測(cè)。公網(wǎng)壓測(cè)主要 IDC 網(wǎng)絡(luò)帶寬,延時(shí),IDC 網(wǎng)關(guān)新建連接、轉(zhuǎn)發(fā)等能力;內(nèi)網(wǎng)壓測(cè),主要是壓測(cè)目標(biāo)服務(wù),目標(biāo)集群的 性能,容量等。
Rhino 平臺(tái)在各個(gè) IDC 都有部署 Agent 集群。各個(gè) IDC 內(nèi)服務(wù)的壓測(cè),默認(rèn)會(huì)就近選擇壓測(cè) Agent,來減少網(wǎng)絡(luò)延時(shí)對(duì)壓測(cè)結(jié)果的干擾,使得壓測(cè)結(jié)果更精準(zhǔn),壓測(cè)問題定位更簡(jiǎn)單。
除了多機(jī)房部署之外,Rhino 平臺(tái)還在邊緣計(jì)算節(jié)點(diǎn)上也部署了壓測(cè) Agent,來模擬各種不同地域不同運(yùn)營(yíng)商的流量請(qǐng)求,確保流量來源,流量分布更貼近真實(shí)情況。在 Rhino 平臺(tái)上可以選擇不同地域不同運(yùn)營(yíng)商,從全國(guó)各個(gè)地區(qū)發(fā)起壓測(cè)流量。
為了應(yīng)對(duì)線上壓測(cè)風(fēng)險(xiǎn),Rhino 平臺(tái)提供兩種熔斷方式,來應(yīng)對(duì)壓測(cè)過程中的突發(fā)事件,來降低對(duì)線上服務(wù)造成的影響。
每個(gè)壓測(cè)任務(wù),都可以關(guān)聯(lián)調(diào)用鏈中任意服務(wù)的告警規(guī)則。在壓測(cè)任務(wù)執(zhí)行過程,Rhino 平臺(tái)會(huì)主動(dòng)監(jiān)聽告警服務(wù)。 當(dāng)調(diào)用鏈中有服務(wù)出現(xiàn)了告警,會(huì)立即停止壓測(cè)。對(duì)于沒有關(guān)聯(lián)的告警,Rhino 平臺(tái)也會(huì)記錄下來,便于壓測(cè)問題定位。
自定義監(jiān)控指標(biāo)及閾值,到達(dá)閾值后,也會(huì)自動(dòng)停止壓測(cè)。目前支持 CPU、Memory、 上游穩(wěn)定性、錯(cuò)誤日志,以及其他自定義指標(biāo)。
此外,除了 Rhino 平臺(tái)自身提供的熔斷機(jī)制以外,公司服務(wù)治理架構(gòu)也提供了很多額外的熔斷機(jī)制,如壓測(cè)開關(guān),一鍵切斷壓測(cè)流量;過載保護(hù),服務(wù)過載時(shí)自動(dòng)丟棄壓測(cè)流量。
對(duì)于 HTTP 協(xié)議,參考了 Postman,全部可視化操作,保證所有人都能上手操作,極大降低了壓測(cè)的使用門檻和成本。
對(duì)于 RPC 任務(wù),Rhino 也自動(dòng)完成了對(duì) IDL 的解析,然后轉(zhuǎn)換成 JSON 格式,便于用戶參數(shù)化處理。
對(duì)于非 HTTP/RPC 的協(xié)議,以及有復(fù)雜邏輯的壓測(cè)任務(wù),Rhino 平臺(tái)也提供了完善的解決方案——Go Plugin。
Go Plugin 提供了一種方式,通過在主程序和共享庫(kù)直接定義一系列的約定或者接口,就可以動(dòng)態(tài)加載其他人編譯的 Go 語言共享對(duì)象,使得主程序可以在編譯后動(dòng)態(tài)加載共享庫(kù),實(shí)現(xiàn)熱插拔的插件系統(tǒng)。此外主程序和共享庫(kù)的開發(fā)者不需要共享代碼,只要雙方的約定不變,修改共享庫(kù)后也不再需要重新編譯主程序。
用戶只要根據(jù)規(guī)范要求,實(shí)現(xiàn)一段發(fā)壓業(yè)務(wù)邏輯代碼即可。Rhino 平臺(tái)可以自動(dòng)拉取代碼,觸發(fā)編譯。并將編譯后的插件 SO 文件分發(fā)到多個(gè)壓測(cè) Agent。 Agent 動(dòng)態(tài)加載 SO 文件,并發(fā)運(yùn)行起來,就可以達(dá)到壓測(cè)的目的。此外,Rhino 還針對(duì)常見 Go Plugin 壓測(cè)場(chǎng)景,建立了壓測(cè)代碼示例代碼庫(kù)。對(duì)于壓測(cè)新手,簡(jiǎn)單修改下業(yè)務(wù)邏輯代碼,就可以完成壓測(cè)了。這樣就解決了非常見協(xié)議,以及復(fù)雜壓測(cè)場(chǎng)景等的壓測(cè)問題。
壓測(cè)調(diào)度的最小單元是壓測(cè) Agent,但是實(shí)際每個(gè) Agent 中有掛載多種壓測(cè)引擎的,來支撐不同的壓測(cè)場(chǎng)景。Rhino 平臺(tái)在壓測(cè)數(shù)據(jù)和壓測(cè)引擎之間增加了一個(gè)壓測(cè)引擎適配層,實(shí)現(xiàn)了壓測(cè)數(shù)據(jù)與壓測(cè)引擎的解耦。壓測(cè)引擎適配層,會(huì)根據(jù)選擇不同的壓測(cè)引擎,生成不同 Schema 的壓測(cè)數(shù)據(jù),啟用不同的引擎來完成壓測(cè),而這些對(duì)用戶是透明的。
在壓測(cè)引擎上,我們有開源的壓測(cè)引擎,也有自研的壓測(cè)引擎。
開源壓測(cè)引擎的優(yōu)點(diǎn)是維護(hù)人多,功能比較豐富,穩(wěn)定且性能好,缺點(diǎn)就是輸入格式固定,定制難度大。此外 Agent 與開源壓測(cè)引擎之間通常是不同進(jìn)程,進(jìn)程通信也存在比較大的問題,不容易控制。
自研壓測(cè)引擎,優(yōu)點(diǎn)是和 Agent 通常運(yùn)行在單進(jìn)程內(nèi),比較容易控制;缺點(diǎn)可能就是性能稍微差一些。但是 Golang 天然支持高并發(fā),因此自研和開源之間的性能差距并不明顯。
由于公司監(jiān)控系統(tǒng),最小時(shí)間粒度是 30s,30s 內(nèi)的數(shù)據(jù)會(huì)聚合成一個(gè)點(diǎn)。這個(gè)時(shí)間粒度對(duì)于壓測(cè)來說是比較難以接受的。因此,Rhino 平臺(tái)自己搭建了一套客戶端監(jiān)控系統(tǒng)。
服務(wù)端監(jiān)控,直接接入了公司 Metric 系統(tǒng)。
在壓測(cè)過程中,Rhino 平臺(tái)還可以實(shí)時(shí)采集目標(biāo)服務(wù)進(jìn)程的性能 Profile,并通過火焰圖的方式展示出來,方便用戶進(jìn)行性能問題分析和優(yōu)化,如圖。
Rhino 壓測(cè)平臺(tái)是一個(gè)面向全字節(jié)跳動(dòng)公司的,為了所有研發(fā)同學(xué)提供的一站式全鏈路壓測(cè)的平臺(tái)。Rhino 平臺(tái)的研發(fā)團(tuán)隊(duì),不僅負(fù)責(zé) Rhino 平臺(tái)的研發(fā)任務(wù),還會(huì)配合 QA&RD 來完成公司大型項(xiàng)目,重點(diǎn)業(yè)務(wù)的性能壓測(cè)工作。
公司內(nèi)重大項(xiàng)目的壓測(cè),Rhino 平臺(tái)都會(huì)積極參與,全力支撐的。其中,比較典型的項(xiàng)目有抖音春晚,西瓜百萬英雄,春節(jié)紅包雨等活動(dòng)。
其中字節(jié)春節(jié)紅包雨活動(dòng),完成是由 Rhino 團(tuán)隊(duì)來負(fù)責(zé)和完成的。字節(jié)春節(jié)紅包雨活動(dòng)是在春節(jié)期間,所有字節(jié)客戶端發(fā)起的,諸如抽卡分現(xiàn)金,紅包錦鯉,紅包雨等一系列的超大規(guī)模的紅包引流活動(dòng)。其流量規(guī)模巨大,流量突發(fā)性強(qiáng),業(yè)務(wù)邏輯和網(wǎng)絡(luò)架構(gòu)復(fù)雜度高等等,都對(duì) Rhino 平臺(tái)提出不小的挑戰(zhàn)。
在春節(jié)紅包雨活動(dòng)中,所有用戶流量都經(jīng)過運(yùn)營(yíng)商專線接入到網(wǎng)絡(luò)邊緣的匯聚機(jī)房,然后經(jīng)過過濾和驗(yàn)證后,再轉(zhuǎn)發(fā)到核心機(jī)房。其中各個(gè) IDC 互為備份,其具體流量路線如圖。在這里,不僅要驗(yàn)證后端各服務(wù)是否能承載預(yù)期流程,還要驗(yàn)證各個(gè)專線帶寬,各個(gè)網(wǎng)關(guān)帶寬及轉(zhuǎn)發(fā)能力,各 IDC 承載能力以及之間帶寬等等。
為此,我們將整個(gè)壓測(cè)拆分成多個(gè)階段,來簡(jiǎn)化壓測(cè)復(fù)雜性,也降低壓測(cè)問題定位的難度:
在這些大型項(xiàng)目的支撐中,Rhino 團(tuán)隊(duì)不僅學(xué)到了大量的業(yè)務(wù)和架構(gòu)設(shè)計(jì)知識(shí),還了解到業(yè)務(wù)研發(fā)同學(xué)如何看待壓測(cè),如何使用平臺(tái),幫助我們發(fā)現(xiàn)更多平臺(tái)的問題,促進(jìn)平臺(tái)不斷迭代優(yōu)化。
日常壓測(cè)支撐,也是 Rhino 平臺(tái)非常重要的一項(xiàng)任務(wù)。對(duì)于日常壓測(cè)中遇到的各種問題,我們采用了各種方案來解決:
Rhino 平臺(tái)還實(shí)現(xiàn)了線上流量的定期調(diào)度,以達(dá)到線上實(shí)例自動(dòng)壓測(cè)的目的[8]:
其具體實(shí)現(xiàn)方案如下:
目前已經(jīng)有 500+微服務(wù)接入,每天定時(shí)執(zhí)行流量調(diào)度,來監(jiān)控線上服務(wù)性能變化趨勢(shì),如下圖。
Rhino 平臺(tái)目前還在公司內(nèi)推行常態(tài)化壓測(cè),通過周期定時(shí)化的自動(dòng)化全鏈路壓測(cè),來實(shí)現(xiàn)以下目標(biāo):
目前 Rhino 平臺(tái)上的常態(tài)化壓測(cè),會(huì)周期定時(shí),以無人值守的方式,自動(dòng)執(zhí)行壓測(cè)任務(wù),并推送壓測(cè)結(jié)果。在壓測(cè)執(zhí)行過程中,會(huì)根據(jù)調(diào)用鏈自動(dòng)完成壓測(cè)開關(guān)開啟,發(fā)起壓測(cè)流量。實(shí)時(shí)監(jiān)控服務(wù)性能指標(biāo),并根據(jù) Metric 及告警監(jiān)控,自動(dòng)完成壓測(cè)熔斷,以保證壓測(cè)安全。
目前已經(jīng)有多個(gè)業(yè)務(wù)方接入常態(tài)化壓測(cè),以此保證線上服務(wù)的穩(wěn)定性。
服務(wù)在上線時(shí),都會(huì)經(jīng)過預(yù)發(fā)布,線上小流量灰度,線上全量發(fā)布。在這個(gè)過程中,我們可以通過線上測(cè)試 Case 以及灰度發(fā)布,來攔截服務(wù)線上功能缺陷。但是對(duì)于性能缺陷的攔截,卻不夠有效。
從線上故障跟蹤系統(tǒng)里就可以發(fā)現(xiàn),由于上線前沒有做好性能壓測(cè),很多性能缺陷都逃逸到了線上。
為了攔截各種性能缺陷,Rhino 平臺(tái)完成了 DevOps 平臺(tái)的打通。將壓測(cè)服務(wù)在 DevOps 平臺(tái)上注冊(cè)成一個(gè)原子服務(wù) ,研發(fā)人員可以將壓測(cè)節(jié)點(diǎn)編排在任意流水線的任意位置,實(shí)現(xiàn)上線前的例行壓測(cè)。DevOps 流水線中的壓測(cè),不僅可以幫助 RD 發(fā)現(xiàn)代碼中的性能問題,還能與性能基線進(jìn)行 Diff,來發(fā)現(xiàn)代碼性能變壞的味道。
Rhino 壓測(cè)平臺(tái)從立項(xiàng)到現(xiàn)在,不到兩年的時(shí)間內(nèi),其發(fā)展已經(jīng)初具規(guī)模,如圖(每月壓測(cè)執(zhí)行統(tǒng)計(jì))。這個(gè)期間,非常非常感謝公司內(nèi)所有合作團(tuán)隊(duì),尤其是架構(gòu)團(tuán)隊(duì),中臺(tái)團(tuán)隊(duì)對(duì)壓測(cè)平臺(tái)的支撐,沒有他們的支撐,全鏈路壓測(cè)建設(shè)是難以完成的。
通用壓測(cè)平臺(tái)已經(jīng)初步搭建完成,基本上能滿足業(yè)務(wù)線日常壓測(cè)需求。但在日常壓測(cè)支撐過程中,發(fā)現(xiàn)不同業(yè)務(wù)線在壓測(cè)時(shí),但是仍然有大量的前置和后繼工作需要人工來完成。
如何更進(jìn)一步降低業(yè)務(wù)方壓測(cè)改造的成本,如何減少壓測(cè)環(huán)境數(shù)據(jù)預(yù)置成本,如何快速完成壓測(cè)數(shù)據(jù)清理,如何快速定位出性能問題等等,Rhino 壓測(cè)平臺(tái)后續(xù)將更進(jìn)一步深入業(yè)務(wù),與各大業(yè)務(wù)方開展更深入的合作,提供更深度的業(yè)務(wù)定制,為研發(fā)提效,助力業(yè)務(wù)線發(fā)展。
業(yè)務(wù)目前資源是否充足,其具體容量是多少;按照目前業(yè)務(wù)增長(zhǎng),其機(jī)器資源還能支撐多久?
目前服務(wù)資源利用如何,是否可以優(yōu)化,如何更進(jìn)一步提升資源利用率,降低機(jī)器資源成本?
某大型活動(dòng),需要申請(qǐng)多少資源?是否不需要壓測(cè),或者自動(dòng)化利用線上流量數(shù)據(jù),或者利用日常壓測(cè)數(shù)據(jù),就可以給出上述問題的結(jié)論?
如何保證服務(wù)穩(wěn)定性,如何監(jiān)控服務(wù)性能劣化并及時(shí)預(yù)警,限流、超時(shí)、重試以及熔斷等服務(wù)治理措施配置是否合理?以及如何配合混沌測(cè)試進(jìn)行容災(zāi)演練,保證服務(wù)穩(wěn)定性等等,這些 Rhino 平臺(tái)都會(huì)做更進(jìn)一步探索。
目前 Rhino 團(tuán)隊(duì)還非常小,非常缺少性能測(cè)試以及后端開發(fā)相關(guān)的研發(fā)工程師,歡迎感興趣的同學(xué)來加入。簡(jiǎn)歷投遞郵箱: tech@bytedance.com ;郵件標(biāo)題: 姓名 - 工作年限 - Rhino 。
[1] http://jm.taobao.org/2017/03/30/20170330/
[2] https://testerhome.com/topics/19493
[3] https://tech.meituan.com/2018/09/27/quake-introduction.html
[4] https://tech.meituan.com/2019/02/14/full-link-pressure-test-automation.html
[5] https://www.open-open.com/lib/view/open1484317425690.html
[6] https://www.infoq.cn/article/NvfJekpvU154pwlsCTLW
[7] https://tech.bytedance.net/articles/3199
[8] https://www.usenix.org/conference/osdi16/technical-sessions/presentation/veeraraghavan
Fastbot:行進(jìn)中的智能 Monkey
品質(zhì)優(yōu)化 - 圖文詳情頁(yè)秒開實(shí)踐
Android Camera 內(nèi)存問題剖析
字節(jié)跳動(dòng)自研線上引流回放系統(tǒng)的架構(gòu)演進(jìn)
歡迎關(guān)注「字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)」
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。