整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          字節(jié)跳動全鏈路壓測(Rhino)的實踐

          字節(jié)跳動全鏈路壓測(Rhino)的實踐

          . 背景

          隨著公司業(yè)務(wù)的不斷擴張,用戶流量在不斷提升,研發(fā)體系的規(guī)模和復雜性也隨之增加。線上服務(wù)的穩(wěn)定性也越來越重要,服務(wù)性能問題,以及容量問題也越發(fā)明顯。

          因此有必要搭建一個有效壓測系統(tǒng),提供安全、高效、真實的線上全鏈路壓測服務(wù),為線上服務(wù)保駕護航。

          關(guān)于全鏈路壓測的建設(shè),業(yè)界已經(jīng)有了非常多文章,但是涉及到具體的技術(shù)實現(xiàn)方面,卻很少介紹。本文想從全鏈路壓測系統(tǒng),從設(shè)計到落地整個實踐過程,來詳細介紹下全鏈路壓測系統(tǒng)是具體是如何設(shè)計,以及如何落地的。希望能從技術(shù)落地實踐的角度,給同行業(yè)的同學一些參考和啟發(fā)。

          2. 解決方案

          2.1 業(yè)內(nèi)實踐

          全鏈路壓測在業(yè)內(nèi)已經(jīng)有了廣泛的實踐,如阿里的 Amazon、PTS[1][2],美團的 Quake[3][4],京東的的 ForceBOT[5],高德的 TestPG[6]等等,都為我們提供豐富的實踐經(jīng)驗,和大量優(yōu)秀的技術(shù)方案。我們廣泛吸收了各大互聯(lián)網(wǎng)公司的全鏈路壓測建設(shè)經(jīng)驗,并基于字節(jié)跳動業(yè)務(wù)需求,設(shè)計開發(fā)了一個全鏈路壓測系統(tǒng) Rhino。

          2.1 架構(gòu)圖

          Rhino 平臺作為公司級的全鏈路壓測平臺,它的目標是對全公司所有業(yè)務(wù),提供單服務(wù)、全鏈路,安全可靠、真實、高效的壓測,來幫助業(yè)務(wù)高效便捷的完成性能測試任務(wù),更精確評估線上服務(wù)性能&容量方面風險。

          因此在 Rhino 平臺設(shè)計之初,我們就定下以下目標:

          • 安全:所有壓測都是在線上完成的,所以理論上所有的壓測對線上用戶都是有損的。壓測平臺將從服務(wù)狀態(tài),以及壓測數(shù)據(jù)兩方面去保證壓測的安全性。
          • 高效:較少壓測腳本編寫成本,數(shù)據(jù)構(gòu)造和壓測監(jiān)控成本,盡量自動化完成壓測過程的各個階段。
          • 準確:精確的壓力控制,準確的鏈路壓測監(jiān)控,精確的壓測報告結(jié)果,以及性能&容量數(shù)據(jù)。
          • 高覆蓋:需要支撐公司內(nèi)不同的業(yè)務(wù)線的壓測需求,如搜索,廣告,電商,教育,游戲等等。

          Rhino 是一個分布式全鏈路壓測系統(tǒng),可以通過水平擴展,來實現(xiàn)模擬海量用戶真實的業(yè)務(wù)操作場景,對線上各種業(yè)務(wù)進行全方位的性能測試。它主要分為控制中心(Rhino Master)模塊,壓測鏈路服務(wù)模塊,監(jiān)控系統(tǒng)模塊,壓測引擎模塊,如圖。(每一個模塊都是由多個微服務(wù)來完成的。如下圖每個實線圖都代表一個微服務(wù)或多個微服務(wù))。

          3. 核心功能介紹

          • 搭建全鏈路壓測平臺,最核心主要有:數(shù)據(jù)構(gòu)造、壓測隔離、鏈路治理、任務(wù)調(diào)度、壓測熔斷、壓測引擎、壓測監(jiān)控等。下面我們將從這些方面詳細介紹下,在 Rhino 平臺中是如何設(shè)計和實現(xiàn)的。

          3.1 數(shù)據(jù)構(gòu)造

          壓測過程中數(shù)據(jù)構(gòu)造是最重要,也是最為復雜的環(huán)節(jié)。壓測數(shù)據(jù)的建模,直接影響了壓測結(jié)果的準確性。

          • 對于服務(wù)性能缺陷掃描,性能調(diào)優(yōu),以及新上線服務(wù),推薦構(gòu)造 Fake 數(shù)據(jù),來壓測指定路徑。
          • 對于線上容量規(guī)劃,性能能力驗證,以及性能 Diff,推薦使用線上真實流量,使壓測結(jié)果更貼近真實情況。
          • 對于涉及到用戶賬號,用戶登錄態(tài)保持的情況,推薦使用壓測專屬測試賬號,避免影響線上真實用戶。

          基礎(chǔ)數(shù)據(jù)構(gòu)造

          為了高效的構(gòu)造特定的 Fake 壓測數(shù)據(jù),Rhino 壓測平臺提供大量數(shù)據(jù)構(gòu)造方式:

          • CSV 文件:按列分割數(shù)據(jù),字段名取 CSV 文件第一行。數(shù)據(jù)讀取方式是按行遞增循環(huán)。如果一個壓測任務(wù)會拆分成多個 Job,那么數(shù)據(jù)文件也會拆分,避免 Job 之間的數(shù)據(jù)重復。
          • 自增:變量類型均為數(shù)字類型。每次發(fā)壓時+1,到最大值后從最小值循環(huán)使用。
          • 隨機:變量類型均為數(shù)字類型,每次發(fā)壓時隨機生成。
          • 常量:Constant,可自定義為任意值。

          壓測賬號

          在壓測過程中,有些壓測請求需要進行登錄,并保持會話;此外在很多壓測請求中涉及到用戶賬號信息 UserID,DeviceID 等數(shù)據(jù)。用戶賬號的構(gòu)造問題,一直是壓測過程中非常棘手的問題。Rhino 平臺打通的用戶中心,設(shè)置了壓測專屬的賬號服務(wù),完美地解決了壓測過程中的登錄態(tài),以及測試賬號等問題。具體流程和使用界面,如下圖。

          3.2 壓測隔離

          壓測隔離中需要解決的壓測流量隔離,以及壓測數(shù)據(jù)的隔離。

          壓測流量隔離,主要是通過構(gòu)建壓測環(huán)境來解決,如線下壓測環(huán)境,或泳道化/Set 化建設(shè),將壓測流量與線上流程完全隔離。優(yōu)點是壓測流量與線上流量完全隔離,不會影響到線上用戶。缺點:機器資源及維護成本高,且壓測結(jié)果需要經(jīng)過一定的換算,才能得線上容量,結(jié)果準確性存在一定的問題。目前公司內(nèi)壓測都是在線上集群上完成的,線上泳道化正在建設(shè)中。

          壓測數(shù)據(jù)隔離,主要是通過對壓測流量進行染色,讓線上服務(wù)能識別哪些是壓測流量,哪些是正常流量,然后對壓測流量進行特殊處理,以達到數(shù)據(jù)隔離的目的。目前 Rhino 平臺整體壓測隔離框架如圖。

          壓測標記

          壓測標記就是最常見的壓測流量染色的方式。

          • 對于 RPC 協(xié)議,會在請求的頭部中增加一個 Key:Value 的字段作為壓測標記。
          • 對于 HTTP 和其他協(xié)議,會在請求頭,自動注入一個 Stress 標記(Key-Value) 。
          • 壓測標記 Key:Value,其中 key 是固定的 Stress_Tag 值,但是每個壓測任務(wù)都有唯一的 Stress_Value 值,主要用于解決壓測數(shù)據(jù)沖突,以及性能問題定位。

          壓測標記透傳

          目前公司內(nèi)各個基礎(chǔ)組件、存儲組件,以及 RPC 框架都已經(jīng)支持了壓測標記的透傳。其原理是將壓測標記的 KV 值存入 Context 中,然后在所有下游請求中都帶上該 Context,下游服務(wù)可以根據(jù) Context 中壓測標記完成對壓測流量的處理。在實際業(yè)務(wù)中,代碼改造也非常簡單,只需要透傳 Context 即可。

          Golang 服務(wù): 將壓測標記寫入 Context 中。

          Python 服務(wù):利用 threading.local()存儲線程 Context。

          Java 服務(wù):利用 ThreadLocal 存儲線程 Context。

          壓測開關(guān)

          為了解決線上壓測安全問題,我們還引入了壓測開關(guān)組件。

          • 每個服務(wù)每個集群,都有一個壓測開關(guān)。只有打開壓測開關(guān)時,壓測流量才能流入到服務(wù)內(nèi),否則就會被底層微服務(wù)框架直接拒絕,業(yè)務(wù)層無感知。
          • 在每個 IDC 區(qū)域,都會有一個全局的壓測總開關(guān)。只有打開了這個全局壓測開關(guān),壓測流量才被允許在這個 IDC 內(nèi)流轉(zhuǎn)。
          • 當線上出現(xiàn)壓測問題,除了從源頭關(guān)閉壓測流量以外,關(guān)閉目標服務(wù)的壓測開關(guān),也能立即阻斷壓測流量。

          壓測數(shù)據(jù)隔離

          線上壓測中,最復雜的問題就是壓測鏈路中涉及到寫操作,如何避免污染線上數(shù)據(jù),并且能保證壓測請求保持和線上相同的請求路徑。業(yè)界有很多解決方案,常見的有影子表,影子庫,以及數(shù)據(jù)偏移,如圖[7]。

          Rhino 平臺針對不同存儲,有不同的解決方案:

          • MySQL、MongoDB:影子表。SDK 判斷是否是壓測流量,若是則根據(jù)配置映射至新表名。配置策略有兩種,一是讀寫影子表,二是讀線上表、寫影子表。
          • Redis:Redis Key 加上 Stress 前綴。如 Stress_Tag=Valuex,那么讀寫 Redis 的 Key=Valuex_Key。這樣可以解決多個壓測任務(wù)數(shù)據(jù)沖突的問題。壓測結(jié)束后,只需要對 Prefix=Valuex 做清除或過期操作即可。
          • MQ:對于消息隊列,Rhino 平臺有兩種策略。一是直接丟棄,然后針對消息隊列的性能,單獨進行壓測;二是在 Header 中透傳壓測標記,Consumer 根據(jù)壓測標記和業(yè)務(wù)需求,再做特殊處理。默認走丟棄策略,業(yè)務(wù)方可根據(jù)需求進行配置。
          • 其他存儲,如 ES,ClickHouse 等,都有壓測集群。壓測時,會將壓測請求打到指定的壓測集群中。

          服務(wù)壓測改造

          在壓測之前,需要對服務(wù)進行壓測驗證。對于不滿足壓測要求(即壓測數(shù)據(jù)隔離)的服務(wù),需要進行壓測改造。

          1. 壓測驗證:對于存儲服務(wù),在不打開壓測開關(guān)的前提下,通過壓測請求,發(fā)送讀寫操作都是會被拒絕。如果沒有拒絕,說明在操作存儲服務(wù)時,沒有帶上壓測 Context,需要進行改造。
          2. 壓測改造:壓測改造是線上全鏈路壓測推進中非常關(guān)鍵,而又非常困難的一個環(huán)節(jié)。對于已經(jīng)上線的服務(wù),壓測改造還極有可能會引入新的 BUG,所以經(jīng)常推動起來比較困難。因此為了解決這些問題,Rhino 平臺有以下幾個解決方案:

          a. 盡量減少代碼改動,并給出完整的指導手冊及代碼示例,減少 RD 的工作量,降低代碼錯誤的可能性

          b. 提供簡單便捷的線上線下 HTTP&RPC 的壓測請求 Debug 工具,方便代碼改動的驗證

          c. 對于新項目,在項目開始初期,就將壓測改造加入項目開發(fā)規(guī)范中,減少后期的代碼改動

          3.3 鏈路治理

          鏈路梳理

          請求調(diào)用鏈,對于線上壓測是非常重要的:

          • 提供清晰壓測流量地圖,并提供完整的鏈路監(jiān)控。
          • 完成服務(wù)依賴的梳理,檢測壓測所依賴的服務(wù)/中臺是否具備壓測的條件,是否需要壓測改造。
          • 鏈接壓測開關(guān)管理,壓測上下游周知等。

          Rhino 平臺通過公司的流式日志系統(tǒng)來完成調(diào)用鏈檢索的。一個服務(wù)在被請求或者請求下游時,都會透傳一個 LogID。RPC 框架會打印調(diào)用鏈日志(包括 RPC 日志-調(diào)用者日志,Access 日志-被調(diào)用者日志),所有日志中都會包含這個 LogID。通過 LogID 將一個請求所經(jīng)過的所有服務(wù)日志串起來,就完成調(diào)用鏈檢索。

          Rhino 平臺在公司流式日志系統(tǒng)提供的鏈路梳理功能基礎(chǔ)上,進行了進一步優(yōu)化,以滿足壓測需要:

          • 自動梳理:由于公司采用微服務(wù)架構(gòu),每個請求背后的調(diào)用鏈路及其復雜,單純靠人工維護是無法完成的。 用戶只需要提供請求中 LogID,Rhino 平臺就能快速梳理出該請求經(jīng)過的服務(wù)節(jié)點,如圖。
          • 實時梳理:由于線上服務(wù)不斷在變化,上線下線新增等,因此同一個請求的調(diào)用鏈也是不斷變化的。Rhino 平臺建議一般使用 1 個小時內(nèi)的 LogID 進行梳理。
          • 多調(diào)鏈路合并:同一個接口,不同參數(shù)下的調(diào)用鏈是不盡相同的。Rhino 平臺會將多個 LogID 梳理的結(jié)果自動進行合并,來補全調(diào)用鏈,保證鏈路梳理結(jié)果的準確性和完整性。

          測周知

          雖然 Rhino 平臺對于壓測有很多的安全保障措施,但是對于大型壓測,保證信息的通暢流通也是非常重要的。因此在壓測周知方面,Rhino 平臺也提供了很多解決方案:

          • 一鍵拉群:梳理完鏈路后,在壓測前可以一鍵拉群,將鏈路中上下游服務(wù)的 Owner 拉到同一個群里,同步壓測信息。
          • 壓測周知:每個壓測開始執(zhí)行時,都會向壓測周知群里推送消息,如壓測 QPS,壓測時長等信息。

          • 壓測事件:在壓測開始執(zhí)行時,Rhino 平臺還會向目標服務(wù)的事件隊列中發(fā)送一個壓測事件,方便快速評估/定位穩(wěn)定性問題是否是壓測導致,減少 RD 線上問題排查的干擾。

          壓測開關(guān)管理

          在壓測之前,需要開啟整體鏈路的壓測開關(guān)的,否則壓測流量就會被服務(wù)拒絕,導致壓測失敗。

          • 一鍵開啟:在壓測執(zhí)行之前,Rhino 平臺可以一鍵開啟鏈接上所有節(jié)點的壓測開關(guān)。
          • 壓測開關(guān)開啟周知:壓測開關(guān)開啟時,Rhino 平臺會自動給對應(yīng)服務(wù) Owner 推送相關(guān)信息,確保服務(wù) Owner 了解相關(guān)壓測信息,上游會有壓測流量會經(jīng)過其服務(wù)。
          • 靜默關(guān)閉:壓測開關(guān)到期后,Rhino 會自動靜默關(guān)閉壓測開關(guān),以保證線上服務(wù)的安全。

          服務(wù) Mock

          對于調(diào)用鏈中不能壓測的服務(wù)(敏感服務(wù)),或者第三方服務(wù),為了壓測請求的完整性,就需要對這些服務(wù)進行 Mock。業(yè)界通用的 Mock 方案有:

          1. 修改業(yè)務(wù)代碼,修改服務(wù)調(diào)用為空轉(zhuǎn)代碼。優(yōu)點:實現(xiàn)成本低。 缺點:返回值固定,代碼&業(yè)務(wù)入侵高,推動困難。如要 Mock 位置比較靠下游,超出部門覆蓋業(yè)務(wù)范圍,推動就非常麻煩。
          2. 通用 Mock 服務(wù)。通用 MockServer,會根據(jù)不同用戶配置不同 Mock 規(guī)則,執(zhí)行對應(yīng)的響應(yīng)延時,并返回對應(yīng)響應(yīng)數(shù)據(jù)。優(yōu)點:無代碼入侵,業(yè)務(wù)方無感知。 缺點:實現(xiàn)成本高。

          由于字節(jié)整個公司都采用微服務(wù)架構(gòu),導致一次壓測涉及鏈路都比較長,快速無業(yè)務(wù)入侵的 Mock

          方式成為了首選。Rhino 平臺是通過公司 Service Mesh 和 ByteMock 系統(tǒng)來實現(xiàn)了高效的,對業(yè)務(wù)透明的服務(wù) Mock。

          壓測執(zhí)行前,Rhino 平臺需要向 Service Mesh 注冊染色轉(zhuǎn)發(fā)規(guī)則,并向 Mock 服務(wù)注冊 Mock 規(guī)則。然后在壓測流量中注入 Mock 染色標記,才能完成服務(wù) Mock:

          1. 基于 Service Mesh 的染色流量轉(zhuǎn)發(fā)。首先需要在壓測流量中注入轉(zhuǎn)發(fā)染色標記,并在 Service Mesh 中注冊對應(yīng)的轉(zhuǎn)發(fā)規(guī)則。Service Mesh 檢測到染色流量后,就會將其轉(zhuǎn)發(fā)到指定的 Mock Server 上,如圖。
          2. 基于 Mock Server 的請求規(guī)則匹配。首先在 Mock Server 上注冊 Mock 規(guī)則,以及匹配的 Response 和響應(yīng)時延。當 Mock Server 接收到請求后,會根據(jù)規(guī)則進行響應(yīng),如圖。

          3.4 發(fā)壓模式

          最小調(diào)度單元

          Rhino 平臺中,壓測 Agent 就是一個最小調(diào)度單元。一次壓測任務(wù),通常會拆分成多個子 Job,然后下發(fā)到多個 Agent 上來完成。

          • 最小化容器部署,減少資源浪費。壓測對機器資源消耗是非常高的,通常 CPU &Memory 的使用率都在 80%以上。但是沒有壓測執(zhí)行時間內(nèi),機器資源使用率<5%。如果長期占用大量的資源,將會對機器資源造成極大的浪費。壓測 Agent 都采用容器化部署,并且每個容器的資源規(guī)格也盡可能小,這樣既能滿足日常壓測需求,也不會占用太多的機器資源。
          • 獨占 Agent,增加壓測執(zhí)行穩(wěn)定性:單個容器內(nèi)只啟動一個 Agent 進程,單個 Agent 同時只能被一個壓測任務(wù)占用,避免多任務(wù)多進程的干擾和資源競爭,增加壓測的穩(wěn)定性。
          • 動態(tài)擴容,支撐海量 QPS 發(fā)壓:壓測高峰期,Rhino 平臺會臨時申請機器資源,快速擴容,完成海量 QPS 的支撐。壓測完成后,會立即釋放機器資源,減少資源浪費。

          2020 年春節(jié)搶紅包壓測中,Rhino 臨時擴容在 4000+個實例,支撐了單次 3kw+QPS 的壓測,但日常 Rhino 平臺只部署了 100+個實例,就能滿足日常壓測需求。

          智能壓力調(diào)節(jié)

          • 動態(tài)分配壓測 Agent:在壓測過程,經(jīng)常出現(xiàn)壓測 Agent 的 CPU/Memory 使用率過高(>90%),導致壓力上不去,達不到目標 QPS;或者壓測延時過高,壓測結(jié)果不準確的問題。Rhino 平臺在發(fā)壓的過程中,會實時監(jiān)控每個壓測 Agent 的 CPU/Memory 使用率,當超過閾值時(>90%),會動態(tài)分配額外的 Agent,以降低每個 Agent 的負載,保證壓測的穩(wěn)定性。
          • 智能調(diào)節(jié)壓力:在壓測過程,通常需要不斷的調(diào)節(jié) QPS 大小,以達到性能壓測目標。這過程非常耗費精力和時間。Rhino 平臺,可以根據(jù)壓測任務(wù)設(shè)定的性能指標,智能調(diào)節(jié) QPS 大小,當達到壓測目標后,會自動熔斷,停止壓測。

          壓測鏈路模擬

          Rhino 平臺默認將全鏈路壓測分為公網(wǎng)壓測和內(nèi)網(wǎng)壓測。公網(wǎng)壓測主要 IDC 網(wǎng)絡(luò)帶寬,延時,IDC 網(wǎng)關(guān)新建連接、轉(zhuǎn)發(fā)等能力;內(nèi)網(wǎng)壓測,主要是壓測目標服務(wù),目標集群的 性能,容量等。

          • 對于內(nèi)網(wǎng)壓測,默認都要求同 IDC 內(nèi)發(fā)壓,減少網(wǎng)絡(luò)延時的干擾。
          • 對于公網(wǎng)壓測,Rhino 平臺在公司 CDN 節(jié)點上都有部署 Agent 節(jié)點,利用了 CDN 節(jié)點剩余計算能力,完成了公網(wǎng)壓測能力的建設(shè)。

          同城多機房,異地多機房

          Rhino 平臺在各個 IDC 都有部署 Agent 集群。各個 IDC 內(nèi)服務(wù)的壓測,默認會就近選擇壓測 Agent,來減少網(wǎng)絡(luò)延時對壓測結(jié)果的干擾,使得壓測結(jié)果更精準,壓測問題定位更簡單。

          邊緣計算節(jié)點 Agent

          除了多機房部署之外,Rhino 平臺還在邊緣計算節(jié)點上也部署了壓測 Agent,來模擬各種不同地域不同運營商的流量請求,確保流量來源,流量分布更貼近真實情況。在 Rhino 平臺上可以選擇不同地域不同運營商,從全國各個地區(qū)發(fā)起壓測流量。

          3.5 壓測熔斷

          為了應(yīng)對線上壓測風險,Rhino 平臺提供兩種熔斷方式,來應(yīng)對壓測過程中的突發(fā)事件,來降低對線上服務(wù)造成的影響。

          基于告警監(jiān)控的熔斷

          每個壓測任務(wù),都可以關(guān)聯(lián)調(diào)用鏈中任意服務(wù)的告警規(guī)則。在壓測任務(wù)執(zhí)行過程,Rhino 平臺會主動監(jiān)聽告警服務(wù)。 當調(diào)用鏈中有服務(wù)出現(xiàn)了告警,會立即停止壓測。對于沒有關(guān)聯(lián)的告警,Rhino 平臺也會記錄下來,便于壓測問題定位。

          基于 Metric 的熔斷

          自定義監(jiān)控指標及閾值,到達閾值后,也會自動停止壓測。目前支持 CPU、Memory、 上游穩(wěn)定性、錯誤日志,以及其他自定義指標。

          此外,除了 Rhino 平臺自身提供的熔斷機制以外,公司服務(wù)治理架構(gòu)也提供了很多額外的熔斷機制,如壓測開關(guān),一鍵切斷壓測流量;過載保護,服務(wù)過載時自動丟棄壓測流量。

          3.6 任務(wù)模型

          HTTP 任務(wù)

          對于 HTTP 協(xié)議,參考了 Postman,全部可視化操作,保證所有人都能上手操作,極大降低了壓測的使用門檻和成本。

          RPC 任務(wù)

          對于 RPC 任務(wù),Rhino 也自動完成了對 IDL 的解析,然后轉(zhuǎn)換成 JSON 格式,便于用戶參數(shù)化處理。

          自定義-Go Plugin

          對于非 HTTP/RPC 的協(xié)議,以及有復雜邏輯的壓測任務(wù),Rhino 平臺也提供了完善的解決方案——Go Plugin。

          Go Plugin 提供了一種方式,通過在主程序和共享庫直接定義一系列的約定或者接口,就可以動態(tài)加載其他人編譯的 Go 語言共享對象,使得主程序可以在編譯后動態(tài)加載共享庫,實現(xiàn)熱插拔的插件系統(tǒng)。此外主程序和共享庫的開發(fā)者不需要共享代碼,只要雙方的約定不變,修改共享庫后也不再需要重新編譯主程序。

          用戶只要根據(jù)規(guī)范要求,實現(xiàn)一段發(fā)壓業(yè)務(wù)邏輯代碼即可。Rhino 平臺可以自動拉取代碼,觸發(fā)編譯。并將編譯后的插件 SO 文件分發(fā)到多個壓測 Agent。 Agent 動態(tài)加載 SO 文件,并發(fā)運行起來,就可以達到壓測的目的。此外,Rhino 還針對常見 Go Plugin 壓測場景,建立了壓測代碼示例代碼庫。對于壓測新手,簡單修改下業(yè)務(wù)邏輯代碼,就可以完成壓測了。這樣就解決了非常見協(xié)議,以及復雜壓測場景等的壓測問題。

          3.7 壓測引擎

          單 Agent 多引擎

          壓測調(diào)度的最小單元是壓測 Agent,但是實際每個 Agent 中有掛載多種壓測引擎的,來支撐不同的壓測場景。Rhino 平臺在壓測數(shù)據(jù)和壓測引擎之間增加了一個壓測引擎適配層,實現(xiàn)了壓測數(shù)據(jù)與壓測引擎的解耦。壓測引擎適配層,會根據(jù)選擇不同的壓測引擎,生成不同 Schema 的壓測數(shù)據(jù),啟用不同的引擎來完成壓測,而這些對用戶是透明的。

          壓測引擎

          在壓測引擎上,我們有開源的壓測引擎,也有自研的壓測引擎。

          開源壓測引擎的優(yōu)點是維護人多,功能比較豐富,穩(wěn)定且性能好,缺點就是輸入格式固定,定制難度大。此外 Agent 與開源壓測引擎之間通常是不同進程,進程通信也存在比較大的問題,不容易控制。

          自研壓測引擎,優(yōu)點是和 Agent 通常運行在單進程內(nèi),比較容易控制;缺點可能就是性能稍微差一些。但是 Golang 天然支持高并發(fā),因此自研和開源之間的性能差距并不明顯。

          • HTTP 協(xié)議:默認 Gatling ,單機發(fā)壓性能非常好,遠超于 Jmeter。對于智能壓測,或動態(tài)調(diào)節(jié)的情況,會切換到自研壓測引擎上。
          • RPC 協(xié)議:自研引擎,主要利用 Golang 協(xié)程+RPC 連接池,來完成高并發(fā)壓測。
          • GoPlugin 協(xié)議:自研引擎,利用 Golang Plugin 可動態(tài)裝載的特性,自動裝載自定義壓測插件,來完成壓測。

          3.8 壓測監(jiān)控

          客戶端監(jiān)控

          由于公司監(jiān)控系統(tǒng),最小時間粒度是 30s,30s 內(nèi)的數(shù)據(jù)會聚合成一個點。這個時間粒度對于壓測來說是比較難以接受的。因此,Rhino 平臺自己搭建了一套客戶端監(jiān)控系統(tǒng)。

          • 每個 Request 都會以請求開始時間為基準打一個點。
          • 單個 Agent 內(nèi),會將相同任務(wù)相同接口,1s 內(nèi)的打點數(shù)據(jù)在本地做一次匯總,上報到 Kafka 中。
          • 監(jiān)控服務(wù)會消費 Kafka 中的打點數(shù)據(jù),將多個 Agent 上報的數(shù)據(jù)進行再次匯總,然后寫入數(shù)據(jù)庫中。
          • 前端監(jiān)控報表會實時拉取數(shù)據(jù)庫中監(jiān)控匯總數(shù)據(jù),繪制實時監(jiān)控曲線
          • 在監(jiān)控數(shù)據(jù)匯總流程中,對于請求響應(yīng)時間的 PCT99 計算,是比較難處理的:目前 Rhino 平臺采用的 T-Digest 算法來計算 1 秒內(nèi)的 PCT99整個時間段內(nèi)的 PCT99 的計算,則是以 PCT & AGV 的方式聚合。即單位時間內(nèi)通過 T-Digest 計算 PCT99;整個時間段內(nèi)的 PCT99,則是對所有點的 PCT99 取平均值。整體計算方案已與公司服務(wù)端監(jiān)控算法對齊,目的是減少客戶端監(jiān)控與服務(wù)端監(jiān)控之間的 Gap,減少壓測結(jié)果分析的干擾因素。

          服務(wù)端監(jiān)控

          服務(wù)端監(jiān)控,直接接入了公司 Metric 系統(tǒng)。

          • 在壓測過程中,Rhino 平臺會提供整條鏈路上所有節(jié)點核心指標的監(jiān)控大盤,并高亮顯示可能存在風險的節(jié)點,來提供實時預警。
          • 對于每個節(jié)點也都提供了實時的,詳細的監(jiān)控曲線圖。
          • 對于每個節(jié)點默認提供 CPU、Memory、QPS 和 Error_Rate 等核心監(jiān)控指標,用戶可以在 Rhino 平臺上修改監(jiān)控配置,增加其他自定義監(jiān)控指標。

          性能 Profile

          在壓測過程中,Rhino 平臺還可以實時采集目標服務(wù)進程的性能 Profile,并通過火焰圖的方式展示出來,方便用戶進行性能問題分析和優(yōu)化,如圖。

          4. 壓測實踐

          Rhino 壓測平臺是一個面向全字節(jié)跳動公司的,為了所有研發(fā)同學提供的一站式全鏈路壓測的平臺。Rhino 平臺的研發(fā)團隊,不僅負責 Rhino 平臺的研發(fā)任務(wù),還會配合 QA&RD 來完成公司大型項目,重點業(yè)務(wù)的性能壓測工作。

          4.1 重大項目支撐

          公司內(nèi)重大項目的壓測,Rhino 平臺都會積極參與,全力支撐的。其中,比較典型的項目有抖音春晚,西瓜百萬英雄,春節(jié)紅包雨等活動。

          其中字節(jié)春節(jié)紅包雨活動,完成是由 Rhino 團隊來負責和完成的。字節(jié)春節(jié)紅包雨活動是在春節(jié)期間,所有字節(jié)客戶端發(fā)起的,諸如抽卡分現(xiàn)金,紅包錦鯉,紅包雨等一系列的超大規(guī)模的紅包引流活動。其流量規(guī)模巨大,流量突發(fā)性強,業(yè)務(wù)邏輯和網(wǎng)絡(luò)架構(gòu)復雜度高等等,都對 Rhino 平臺提出不小的挑戰(zhàn)。

          在春節(jié)紅包雨活動中,所有用戶流量都經(jīng)過運營商專線接入到網(wǎng)絡(luò)邊緣的匯聚機房,然后經(jīng)過過濾和驗證后,再轉(zhuǎn)發(fā)到核心機房。其中各個 IDC 互為備份,其具體流量路線如圖。在這里,不僅要驗證后端各服務(wù)是否能承載預期流程,還要驗證各個專線帶寬,各個網(wǎng)關(guān)帶寬及轉(zhuǎn)發(fā)能力,各 IDC 承載能力以及之間帶寬等等。

          為此,我們將整個壓測拆分成多個階段,來簡化壓測復雜性,也降低壓測問題定位的難度:

          • 通過撥測/CDN 壓測來分別驗證各個匯聚機房的承載能力,帶寬,以及網(wǎng)關(guān)性能。
          • 在各個匯聚機房部署壓測 Agent,來模擬用戶流量分布,來壓測部署在核心機房的后端服務(wù)性能。
          • 單接口單實例壓測,單接口單機房壓測,場景化全鏈路單機房壓測,場景化全鏈路全資源壓測,分階段來驗證后端服務(wù)性能。
          • 最后會通過全網(wǎng)撥測,來模擬真實春節(jié)紅包雨高峰期流量,整體驗證全系統(tǒng)性能。

          在這些大型項目的支撐中,Rhino 團隊不僅學到了大量的業(yè)務(wù)和架構(gòu)設(shè)計知識,還了解到業(yè)務(wù)研發(fā)同學如何看待壓測,如何使用平臺,幫助我們發(fā)現(xiàn)更多平臺的問題,促進平臺不斷迭代優(yōu)化。

          4.2 日常壓測任務(wù)支撐

          日常壓測支撐,也是 Rhino 平臺非常重要的一項任務(wù)。對于日常壓測中遇到的各種問題,我們采用了各種方案來解決:

          • 專人 Oncall 值周,一對一指導。
          • 詳細完善的壓測知識庫,不僅介紹了平臺如何使用,還包括壓測如何改造,壓測方案如何制定,壓測問題如何定位。
          • 完善的性能培訓體系:定期開展性能測試相關(guān)分享,并對于 QA&RD 團隊,也會開展專業(yè)的壓測培訓。

          4.3 線上流量調(diào)度

          Rhino 平臺還實現(xiàn)了線上流量的定期調(diào)度,以達到線上實例自動壓測的目的[8]:

          • 將線上流量逐步調(diào)度到目標實例上,來測試服務(wù)實例性能極限,并給出實例性能 Profile,分析出實例性能瓶頸。
          • 通過長期的流量調(diào)度,來觀察服務(wù)實例性能變化,以監(jiān)控服務(wù)性能的變化趨勢。
          • 通過不同資源水位下的實例性能,來預估出整個集群容量。完成對服務(wù)容量預估,以及線上風險評估。
          • 基于泳道化的流量調(diào)度,可以精確的預估服務(wù)集群容量。

          其具體實現(xiàn)方案如下:

          • 修改負載均衡中目標實例的權(quán)重 Weight 值,逐步調(diào)大該 Weight 值,將更多流量集中打到目標實例,直到達到設(shè)置的停止閾值。

          目前已經(jīng)有 500+微服務(wù)接入,每天定時執(zhí)行流量調(diào)度,來監(jiān)控線上服務(wù)性能變化趨勢,如下圖。

          4.4 常態(tài)化壓測

          Rhino 平臺目前還在公司內(nèi)推行常態(tài)化壓測,通過周期定時化的自動化全鏈路壓測,來實現(xiàn)以下目標:

          • 實時監(jiān)控線上服務(wù)集群容量,防止服務(wù)性能劣化。
          • 實時監(jiān)控線上鏈路容量,防止鏈路性能劣化。

          目前 Rhino 平臺上的常態(tài)化壓測,會周期定時,以無人值守的方式,自動執(zhí)行壓測任務(wù),并推送壓測結(jié)果。在壓測執(zhí)行過程中,會根據(jù)調(diào)用鏈自動完成壓測開關(guān)開啟,發(fā)起壓測流量。實時監(jiān)控服務(wù)性能指標,并根據(jù) Metric 及告警監(jiān)控,自動完成壓測熔斷,以保證壓測安全。

          目前已經(jīng)有多個業(yè)務(wù)方接入常態(tài)化壓測,以此保證線上服務(wù)的穩(wěn)定性。

          4.5 DevOps 流水線中的壓測

          服務(wù)在上線時,都會經(jīng)過預發(fā)布,線上小流量灰度,線上全量發(fā)布。在這個過程中,我們可以通過線上測試 Case 以及灰度發(fā)布,來攔截服務(wù)線上功能缺陷。但是對于性能缺陷的攔截,卻不夠有效。

          從線上故障跟蹤系統(tǒng)里就可以發(fā)現(xiàn),由于上線前沒有做好性能壓測,很多性能缺陷都逃逸到了線上。

          為了攔截各種性能缺陷,Rhino 平臺完成了 DevOps 平臺的打通。將壓測服務(wù)在 DevOps 平臺上注冊成一個原子服務(wù) ,研發(fā)人員可以將壓測節(jié)點編排在任意流水線的任意位置,實現(xiàn)上線前的例行壓測。DevOps 流水線中的壓測,不僅可以幫助 RD 發(fā)現(xiàn)代碼中的性能問題,還能與性能基線進行 Diff,來發(fā)現(xiàn)代碼性能變壞的味道。

          5. 總結(jié)與展望

          5.1 總結(jié)

          Rhino 壓測平臺從立項到現(xiàn)在,不到兩年的時間內(nèi),其發(fā)展已經(jīng)初具規(guī)模,如圖(每月壓測執(zhí)行統(tǒng)計)。這個期間,非常非常感謝公司內(nèi)所有合作團隊,尤其是架構(gòu)團隊,中臺團隊對壓測平臺的支撐,沒有他們的支撐,全鏈路壓測建設(shè)是難以完成的。

          5.2 未來發(fā)展

          業(yè)務(wù)深層次定制化

          通用壓測平臺已經(jīng)初步搭建完成,基本上能滿足業(yè)務(wù)線日常壓測需求。但在日常壓測支撐過程中,發(fā)現(xiàn)不同業(yè)務(wù)線在壓測時,但是仍然有大量的前置和后繼工作需要人工來完成。

          如何更進一步降低業(yè)務(wù)方壓測改造的成本,如何減少壓測環(huán)境數(shù)據(jù)預置成本,如何快速完成壓測數(shù)據(jù)清理,如何快速定位出性能問題等等,Rhino 壓測平臺后續(xù)將更進一步深入業(yè)務(wù),與各大業(yè)務(wù)方開展更深入的合作,提供更深度的業(yè)務(wù)定制,為研發(fā)提效,助力業(yè)務(wù)線發(fā)展。

          壓測與容量規(guī)劃

          業(yè)務(wù)目前資源是否充足,其具體容量是多少;按照目前業(yè)務(wù)增長,其機器資源還能支撐多久?

          目前服務(wù)資源利用如何,是否可以優(yōu)化,如何更進一步提升資源利用率,降低機器資源成本?

          某大型活動,需要申請多少資源?是否不需要壓測,或者自動化利用線上流量數(shù)據(jù),或者利用日常壓測數(shù)據(jù),就可以給出上述問題的結(jié)論?

          壓測與 SRE

          如何保證服務(wù)穩(wěn)定性,如何監(jiān)控服務(wù)性能劣化并及時預警,限流、超時、重試以及熔斷等服務(wù)治理措施配置是否合理?以及如何配合混沌測試進行容災演練,保證服務(wù)穩(wěn)定性等等,這些 Rhino 平臺都會做更進一步探索。

          6. 招聘

          目前 Rhino 團隊還非常小,非常缺少性能測試以及后端開發(fā)相關(guān)的研發(fā)工程師,歡迎感興趣的同學來加入。簡歷投遞郵箱: tech@bytedance.com ;郵件標題: 姓名 - 工作年限 - 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:行進中的智能 Monkey

          品質(zhì)優(yōu)化 - 圖文詳情頁秒開實踐

          Android Camera 內(nèi)存問題剖析

          字節(jié)跳動自研線上引流回放系統(tǒng)的架構(gòu)演進


          歡迎關(guān)注「字節(jié)跳動技術(shù)團隊」

          得不佩服,美觀小巧的網(wǎng)頁內(nèi)容編輯器——ContentTools


          愛分享Coder

          2019-12-03 · 軟件開發(fā)工程師 優(yōu)質(zhì)科技領(lǐng)域創(chuàng)作者

          介紹

          ContentTools是一個美觀小巧的網(wǎng)頁內(nèi)容工具(一個JS庫),具備所見即所得(WYSIWYG)的編輯器功能,只需幾個簡單的步驟,即可將ContentTools添加到任何HTML頁面。如下圖所示頁面通過實時ContentTool的彈出層實現(xiàn)實時編輯功能。用小而美來形容它最好不過了!

          Github地址

          https://github.com/GetmeUK

          特性

          ContentTools是用于HTML頁面的美觀小巧的內(nèi)容編輯器。它被設(shè)計為:

          • 與框架無關(guān)的庫不使用任何JavaScript框架(沒有JQuery),但可以很好地使用它們。
          • 靈活的ContentTools軟件包由5個庫組成,每個庫或可以獨立使用。
          • 可擴展的軟件包旨在易于擴展。
          • 小巧完整的編輯器(JS,CSS,圖像和圖標字體)為241kb(壓縮后為49kb)。

          功能簡介

          ContentTools具有字體加粗、斜體、超鏈接、對齊、列表、表格、圖片、視頻、代碼、撤銷、重做、刪除等功能

          1、加粗顯示

          2、斜體顯示

          3、超鏈接

          4、H標題

          5、正文

          6、有序和無序列表

          7、插入表格

          8、插入圖片

          9、視頻

          以上截圖中的功能還不完整,如果想體驗以下完整的功能可以直接去DEMO頁面體驗,如果需要在HTML級別上更改元素的內(nèi)容,那也是可以的。通過屬性對話框中的最后一個選項卡,可以查看所選元素的內(nèi)部HTML代碼并直接對其進行更新。

          使用

          • 第一步是下載JS,CSS和其他關(guān)聯(lián)的項目文件:

          下載倉庫并打開/ build文件夾,包括預構(gòu)建的源文件。將文件夾的內(nèi)容復制到項目的適當位置(例如,content-tools.min.js>

          /www/scripts/content-tools.min.js)。但是,/ images文件夾和icons.woff字體需要復制到與content-tools.min.css相同的文件夾中,文件結(jié)構(gòu)應(yīng)類似于:

          • HTML

          <head>

          <title>My page</title>

          <link rel="stylesheet" type="text/css" href="assets/content-tools.min.css">

          ...

          </head>

          <body>

          ...

          <script src="assets/content-tools.min.js"></script>

          <script src="assets/editor.js"></script>

          </body>

          包括一個名為editor.js的附加JS文件。包含初始化我們的編輯器的代碼,繼續(xù)

          <div data-editable data-name="main-content">

          <blockquote>

          Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.

          </blockquote>

          <p>John F. Woods</p>

          </div>

          data-name屬性用于在保存時標識區(qū)域(默認情況下使用id屬性),標記可編輯HTML時,常見的誤解是將單個元素標記為可編輯,例如:

          <h1 data-editable data-name="heading">Content</h1>

          正確的使用方式如下,也就是說必須要在特定的容器元素內(nèi)

          <div data-editable data-name=heading>

          <h1>Content</h1>

          </div>

          • 準備CSS

          ContentTools使用CSS類來對齊文本,圖像,視頻和iframe,需要在自己的CSS中為這些對齊類定義樣式,例如:

          [data-editable] iframe,

          [data-editable] image,

          [data-editable] [data-ce-tag=img],

          [data-editable] img,

          [data-editable] video {

          clear: both;

          display: block;

          margin-left: auto;

          margin-right: auto;

          max-width: 100%;

          }

          /* 左對齊 */

          [data-editable] .align-left {

          clear: initial;

          float: left;

          margin-right: 0.5em;

          }

          /* 右對齊 */

          [data-editable].align-right {

          clear: initial;

          float: right;

          margin-left: 0.5em;

          }

          /* 可編輯區(qū)域中文本的對齊樣式 */

          [data-editable] .text-center {

          text-align: center;

          }

          [data-editable] .text-left {

          text-align: left;

          }

          [data-editable] .text-right {

          text-align: right;

          }

          • 初始化編輯器

          ContentTools提供了一個編輯器,但是在初始化它之前,我們需要配置一些東西,即:

          1. 我們希望用戶能夠?qū)SS樣式應(yīng)用于元素。
          2. 我們希望頁面的區(qū)域是可編輯的。
          3. 一種保存我們的內(nèi)容的機制。
          4. 我們可能還會配置圖像處理程序等等

          將以下代碼添加到我們之前創(chuàng)建的editor.js文件中:

          window.addEventListener('load', function() {

          var editor;

          });

          • 配置樣式

          就像文字處理程序一樣,可以為內(nèi)容配置一系列預定義樣式。當用戶從視口底部的檢查器欄中選擇標簽時,這些標簽就會出現(xiàn)。盡管可以將樣式設(shè)置為適用于所有標簽,但是僅顯示適用于標簽類型的樣式。

          我們將添加可應(yīng)用于段落<p>標記的單一樣式.author。在var編輯器下方聲明添加:

          ContentTools.StylePalette.add([

          new ContentTools.Style('Author', 'author', ['p'])

          ]);

          StylePalette.add方法使我們可以向編輯器添加樣式列表。每種樣式均聲明為一個Style實例,該實例使用顯示名稱,CSS類和可以應(yīng)用該樣式的標簽列表初始化。我們需要添加相關(guān)的CSS來支持這種樣式,因此在HTML的開頭添加:

          <head>

          ...

          <style>

          .author {

          font-style: italic;

          font-weight: bold;

          }

          </style>

          </head>

          • 選擇可編輯區(qū)域

          接下來,我們需要初始化編輯器,并讓它知道頁面上的哪些元素是可編輯的。為此,將以下代碼添加到editor.js中:

          editor=ContentTools.EditorApp.get();

          editor.init('*[data-editable]', 'data-name');

          我們使用用于頁面可編輯區(qū)域的CSS選擇器和屬性名稱(“數(shù)據(jù)名稱”)來初始化編輯器,以告知編輯器元素的哪個屬性包含其區(qū)域名稱。區(qū)域名稱在同一頁面中必須唯一。

          • 保存更改

          最后,我們希望在用戶保存頁面時得到通知,以便我們可以將每個區(qū)域的更新內(nèi)容存儲在文件或數(shù)據(jù)庫中。為此,我們監(jiān)聽由編輯器觸發(fā)的保存事件。在editor.init語句之后,將以下代碼添加到editor.js中:

          editor.addEventListener('saved', function (ev) {

          var name, payload, regions, xhr;

          // 檢查是否已更改

          regions=ev.detail().regions;

          if (Object.keys(regions).length==0) {

          return;

          }

          // 保存更改時將編輯器設(shè)置為忙

          this.busy(true);

          // 將每個區(qū)域的內(nèi)容收集到一個FormData實例中

          payload=new FormData();

          for (name in regions) {

          if (regions.hasOwnProperty(name)) {

          payload.append(name, regions[name]);

          }

          }

          // 將更新內(nèi)容發(fā)送到要保存的服務(wù)器

          function onStateChange(ev) {

          // 檢查請求是否完成

          if (ev.target.readyState==4) {

          editor.busy(false);

          if (ev.target.status=='200') {

          // 保存成功,通知前臺

          new ContentTools.FlashUI('保存成功');

          } else {

          // 保存失敗,通知前臺

          new ContentTools.FlashUI('保存失敗');

          }

          }

          };

          xhr=new XMLHttpRequest();

          xhr.addEventListener('readystatechange', onStateChange);

          xhr.open('POST', '/save-my-page');

          xhr.send(payload);

          });

          當用戶保存頁面時,我們可以使用AJAX將每個區(qū)域的內(nèi)容發(fā)送到服務(wù)器進行保存。在瀏覽器中打開頁面,尋找左上方的藍色編輯按鈕,然后單擊它以開始編輯。





          總結(jié)

          這樣一個美觀且強大的即時編輯器可謂是非常的實用,特別是對于一些內(nèi)容編輯網(wǎng)站,如CMS、靜態(tài)文檔網(wǎng)站、博客等內(nèi)容型網(wǎng)站尤其有用,希望對你有所幫助,Enjoy it!

          1.5萬閱讀

          搜索

          專門制作文字特效軟件

          網(wǎng)站制作微信小程序

          html5編輯軟件排行榜

          傻瓜式網(wǎng)頁制作軟件

          網(wǎng)站制作軟件免費版

          一鍵網(wǎng)站制作app

          實現(xiàn)css兩端對齊,我在網(wǎng)上找了很多方法,都不怎么實用,都是兼容性鬧得,column是css3的屬性,是多列布局,使用column來實現(xiàn)兩端對齊簡單實用,就要設(shè)置下模塊的個數(shù)跟column的列數(shù)一致就行,先看它的的3個屬性:

          1.column-count 屬性規(guī)定元素應(yīng)該被分隔的列數(shù)

          2.column-gap 屬性規(guī)定列之間的間隔

          2.column-rule 屬性設(shè)置列之間的寬度、樣式和顏色規(guī)則。

          CSS3 多列屬性的兼容性:Internet Explorer 10 和 Opera 支持多列屬性,F(xiàn)irefox 需要前綴 -moz-,Chrome 和 Safari 需要前綴 -webkit-,特別注意:Internet Explorer 9 以及更早的版本不支持多列屬性。

          實現(xiàn)css兩端對齊的例子:用column-count定義對象的列數(shù),例子中有4個p(即4個模塊),那么就定義為4列,再用column-gap定義了對象中列與列的間距,間距不能設(shè)置為百分比,但是只能用px,具體的看下面的代碼:

          <!Doctype html>

          <html>

          <head>

          <meta http-equiv="Content-Type" content="text/html; charset=gbk2312"/>

          <title>實現(xiàn)css兩端對齊</title>

          <style type="text/css">

          *{margin:0;padding:0;}

          .box{

          margin:100px 0;

          -webkit-column-count:4;-moz-column-count:4;column-count:4;

          -webkit-column-gap:30px;-moz-column-gap:30px;column-gap:30px;

          }

          .box p{

          height:30px;

          line-height:30px;

          text-align:center;

          border:1px solid red;

          color:#000;

          font-size:12px;

          }

          </style>

          </head>

          <body>

          <div class="box">

          <p>第1列</p>

          <p>第2列</p>

          <p>第3列</p>

          <p>第4列</p>

          </div>

          </body>

          </html>

          點擊查看css兩端對齊效果(http://tangjiusheng.com/css3/column.html)

          除注明外的文章,均為來源:湯久生博客,轉(zhuǎn)載請保留本文地址!

          原文地址:http://tangjiusheng.com/css3/130.html


          主站蜘蛛池模板: 国产福利精品一区二区| 秋霞电影网一区二区三区| 无码av免费毛片一区二区| 国产美女一区二区三区| 丝袜人妻一区二区三区网站| 国产萌白酱在线一区二区| 精品一区精品二区制服| 亚欧色一区W666天堂| 国模吧一区二区三区| 91一区二区三区| 免费看一区二区三区四区| 国产av福利一区二区三巨 | 久久精品国产一区二区| 波多野结衣一区在线观看| 国产Av一区二区精品久久| 国产午夜精品一区二区三区极品| 麻豆aⅴ精品无码一区二区| 日韩精品区一区二区三VR| 精品一区二区三区无码免费视频| 久久se精品一区精品二区| 无码日韩精品一区二区三区免费 | 国产色综合一区二区三区 | 中文字幕无线码一区2020青青| 亚洲一区中文字幕久久| 色偷偷久久一区二区三区| 亚洲一区二区三区四区在线观看| 久久99精品一区二区三区| 日韩人妻不卡一区二区三区| 亚洲一区免费视频| 麻豆精品人妻一区二区三区蜜桃| 无码一区二区波多野结衣播放搜索 | 亚洲日韩中文字幕无码一区| 精品不卡一区二区| 精品欧美一区二区在线观看| 国产伦精品一区二区三区无广告 | 日本在线观看一区二区三区| 人妻少妇一区二区三区| 亚洲日本精品一区二区| 欧亚精品一区三区免费| 一区二区国产在线播放| 消息称老熟妇乱视频一区二区|