TPP穩定性之場景隔離和多租戶
TPP有3600+個場景,每個場景是一些AB(演算法方案程式碼+業務配置+流量分配策略)的集合,場景按業務團隊劃分物理叢集,同一個物理叢集內的容器是對等的,JVM內部署著演算法容器,演算法容器內混布相同的場景集合,演算法容器是平臺編碼,場景方案程式碼則是演算法編碼並進行熱部署。前端請求以場景為粒度請求RR,RR獲取場景所在叢集按叢集進行路由。如下圖所示。
如前文所述,容器是平臺開發編碼,程式碼質量可控,而演算法場景程式碼則是全集團各個演算法owner編寫,編碼質量參差不齊。這種情況下JVM內場景混布就會出現相互影響的問題,如cpu分配不均,記憶體分配不均等問題,最討厭的是出現死迴圈。針對這些問題TPP已經將重要的核心場景和非重要的小場景進行物理隔離,即調配到不同的物理叢集,這樣一定程度上減少了非重要場景程式碼問題導致核心場景大量異常的情況,如超時。但非核心叢集死迴圈,甚至核心叢集相互影響的情況還是時有發生。那為什麼不直接每個場景單獨一個容器部署呢,通過docker層面cgroup直接隔離場景是否可行?當然可行,但是機器成本將大幅上升。因為每個容器裡要載入各種二方服務,如pandora,forest,igraph,sumamry,各種hsf服務等,而且每個場景要保證至少兩臺的可用度,這樣機器記憶體規模至少要擴大好多倍,機器數自然答覆上漲。很多場景qps非常低的,峰值也是錯開的,混布能極大提高資源利用率。我們對隔離做了一些改進工作,包括執行緒池隔離,多租戶隔離。
首先系統進行了執行緒池隔離改造,演算法方案程式碼從HSF業務執行緒直接執行改為HSF業務執行緒提交給場景執行緒池執行。每個場景都管理一個自己的執行緒池,平臺根據流量需求可動態調配不同的執行緒池引數。如下圖所示:
這樣做的好處是:
- 保護了hsf入口工作執行緒,改造之前演算法方案超時嚴重會造成容器hsf服務pool full。場景執行緒池隔離後根據場景超時上限(一般是200ms)做超時interrupt,保證不會大量併發堆積。同時場景執行緒池設定拒絕策略,在併發堆積超過wait_queue+max_pool的情況下立即拒絕服務。這樣一定程度提升了hsf的可用性。
- 減少無用的超時後計算,hsf業務執行緒並不會被中斷,如果演算法中途超時了,並沒必要做後面的複雜計算工作,浪費的cpu資源也被節約下來。
- 場景間公平性得到一定保障,程式碼有問題的場景不佔滿hsf執行緒的情況下,其他場景仍能有流量得到服務。
執行緒池隔離在雙11前也發揮了作用,如rtp第一次成功升級arpc後出現過死鎖,發生呼叫的業務執行緒都會一直阻塞,如果發生在hsf執行緒,這臺機器就game over了,而通過重置場景業務執行緒池就能免啟動瞬間修復。
執行緒池隔離帶來的問題是增加一定的上下文切換開銷,設定合理的core size和alive time,通過壓測和實際執行發現並沒有效能下降,也沒有明顯增加jvm的執行緒數。這裡從同步改造成執行緒池方式,需要解決一些問題,典型的就是ThreadLocal問題,包括eagleeye和業務threadlocal。下面是支援eagleeye和業務threadlocal透傳的執行緒池實現:
public class SolutionExecutorService extends ThreadPoolExecutor {
public SolutionExecutorService(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new SolutionFutureTask<T>(runnable, value);
}
@Override
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new SolutionFutureTask<T>(callable);
}
class SolutionFutureTask<T> extends FutureTask<T> {
// 當前context透傳到工作執行緒
final RpcContext_inner rpcContext = EagleEye.getRpcContext();
final Map<String, Object> tppContext = ThreadLocalParams.save();
public SolutionFutureTask(Callable<T> callable) {
super(callable);
}
public SolutionFutureTask(Runnable runnable, T result) {
super(runnable, result);
}
public void run() {
Profiler.start("RunWithPool.");
EagleEye.setRpcContext(rpcContext);
ThreadLocalParams.restore(tppContext);
try {
super.run();
} finally {
EagleEye.clearRpcContext();
ThreadLocalParams.clear();
}
}
}
}
執行緒池隔離並沒有根本解決死迴圈和cpu分配不均問題,因為cpu密集型計算是無法interrupt的,同時TPP的一個平臺價值之一是演算法可以隨時變更演算法方案熱部署(包括雙11當天),如果方案記憶體回收不徹底也會造成記憶體洩漏。因此我們利用多租戶進一步解決cpu隔離和記憶體回收兩個問題,改造後的隔離模式如下圖所示, 將演算法方案之間以及演算法與系統之間進行隔離:
首先結合AJDK的多租戶,利用cgroup進行徹底的cpu隔離。但這不是容易的事,對於TPP這樣複雜的容器更不容易,下文將介紹TPP多租戶改造的艱辛之路。
cgroup的cpu隔離主要有這麼幾種方式:cpuset,cpushares,cpu quota.
- cpuset是cpu核為粒度的物理隔離,我們搜尋hippo排程docker容器的時候就是cpuset隔離,保證每個容器不相互影響。
- cpushares設定使用者的cpu使用權重,權重越大則分配的cpu資源越多,它是個相對值。如3個程式的cpu shares分別為512,1024,1024,則他們滿負載時候分配到的cpu資源是1:2:2即20%:40%:40%。如果後兩個執行緒沒有滿負載,第一個share為512的可以使用超過20%。如果後兩個空閒,則第一個可以用到100%,一旦share為1024的程式要使用cpu,則512的程式會讓出cpu。
- 最後cpu quota設定了程式能使用的cpu最大比例絕對值,如cfs_period=100000,cfs_quota=50000,則程式能用到一個cpu core的50%,cfs=50000n則可以用到n個core的50%,總cpu可以使用到50%n/cores。
再來分析下AJDK的多租戶實現原理,首先看一個執行緒怎麼被cgroup限制cpu:
TenantConfiguration tenantConfiguration = new TenantConfiguration(cpuShares, memLimit)
.limitCpuCfs(cfsPeriod, cfsQuota);
TenantContainer container = TenantContainer.create(name, tenantConfiguration);
使用者建立了個租戶容器,這裡指定了租戶的cpu shares,記憶體上限,cpu利用率上限。然後使用者呼叫租戶容器去執行運算。
container.run(new Runnable() {
@Override
public void run() {
doRun();
}
});
AJDK底層對多租戶的改造有這樣一個非常重要的原則:
執行緒1由租戶容器1建立,則執行緒1建立的其他執行緒都屬於容器1,這些執行緒整體cpu利用率受容器1的cgroup限制
這個原則會帶來什麼麻煩事呢,先看看租戶執行的程式碼:
public void run(final Runnable runnable) throws TenantException {
if (state == TenantState.DEAD || state == TenantState.STOPPING) {
throw new TenantException("Tenant is dead");
}
// The current thread is already attached to tenant
if (this == TenantContainer.current()) {
runnable.run();
} else {
if (TenantContainer.current() != null) {
throw new TenantException("must be in root tenant before running into non-root tenant.");
}
// attach to new tenant
attach();
try {
runnable.run();
} finally {
// detach from the tenant
detach();
}
}
}
這裡首先檢查當前執行緒所屬租戶容器(下文以容器1代替)和當前執行租戶容器(下文以容器2代替)是否同一個,如果同一個執行執行runnable,這裡沒有效能開銷。如果不是麻煩來了,呼叫attach通過jni呼叫繫結當前執行緒到容器2的cgroup組,然後執行runnable,這時候執行緒的cpu就得到了租戶容器2的cgroup限制,runnable執行結束後再通過jni恢復執行緒和容器1的繫結。這裡有嚴重的效能開銷,即jni呼叫cgroup非常慢(實測50ms以上)。因此每個場景都要有一個執行緒池和一個租戶容器,執行緒池必須有一定的coresize和alive,防止頻繁new執行緒呼叫cgroup產生大耗時,場景執行緒必須由租戶容器建立。這樣執行緒池submit一個task就打到和普通執行緒池一樣的效能,我們為場景執行緒池定製了ThreadFactory,線上程池隔離的基礎上能輕鬆實現:
static class TenantThreadFactory extends NamedThreadFactory {
private TenantContainer container;
public TenantThreadFactory(TenantContainer container, String prefix) {
super(prefix);
this.container = container;
}
@Override
public Thread newThread(Runnable r) {
final ObjectWrapper<Thread> wrapper = new ObjectWrapper<>();
// 用租戶容器去建立執行緒
try {
container.run(() -> {
return super.newThread(r);
wrapper.setObject(t);
});
} catch (TenantException e) {
throw new RuntimeException("create tenant thread exception", e);
}
return wrapper.getObject();
}
}
對於簡單應用到此為止就完成了多租戶改造,而對TPP來說則只是完成了一小步。因為TPP接入了大量的二方服務,如IGraph, RTP, SUMMARY,很多HSF服務,Forest等,前文已經介紹過混布場景是為了複用二方服務,為每個場景克隆二方服務client會產生很大的記憶體開銷。這些複用的二方服務也管理了自己的執行緒池,結合前文所述租戶執行緒建立的其他執行緒也屬於這個租戶,一旦二方服務的執行緒由某個租戶建立然後被其他租戶複用則產生了cgroup切換的開銷,同時cpu分配也會錯亂。因此TPP還要對場景租戶執行緒和二方服務線進行隔離,這就涉及對一些核心高併發二方服務(雙11 IGraph峰值530w qps,SUMMARY 69w qps, RTP 75w qps)client的改造。原理很簡單,為二方服務的執行緒池增加定製的ThreadFactory:
public class RootTenantThreadFactory extends NamedThreadFactory {
public RootTenantThreadFactory(String prefix, boolean daemon) {
super(prefix, daemon);
}
@Override
public Thread newThread(Runnable r) {
if (JvmUtil.isTenantEnabled()) {
final ObjectWrapper<Thread> wrapper = new ObjectWrapper<>();
// 用root容器去建立執行緒
try {
TenantContainer.primitiveRunInRoot(() -> {
Thread t = super.newThread(r);
wrapper.setObject(t);
});
}
return wrapper.getObject();
} else {
return super.newThread(r);
}
}
}
對於大部分非同步httpclient類的擴充套件client只需要在構造時候增加設定threadFactory即可:
public class RootTenantThreadFactory extends NamedThreadFactory {
public RootTenantThreadFactory(String prefix, boolean daemon) {
super(prefix, daemon);
}
@Override
public Thread newThread(Runnable r) {
if (Profiler.getEntry() == null) {
Profiler.start("Create Pool Thread ");
} else {
Profiler.enter("Create Pool Thread ");
}
if (JvmUtil.isTenantEnabled()) {
final ObjectWrapper<Thread> wrapper = new ObjectWrapper<>();
// 用root容器去建立執行緒
try {
TenantContainer.primitiveRunInRoot(() -> {
Thread t = super.newThread(r);
wrapper.setObject(t);
});
} finally {
Profiler.release();
}
return wrapper.getObject();
} else {
return super.newThread(r);
}
}
}
友情提示:多租戶的隔離方式不當使用會導致宿主機cgroup下目錄太多而負載過高,這個之前在sigma上有反饋,容器銷燬時需要刪除ajdk的程式cgroup目錄,需要應用自己操作,幸運的是hippo排程自動完成了這個工作。
最後看一下多租戶隔離的效果:
8核虛擬機器下進行測試,非租戶隔離的情況下叢集內其他場景發生死迴圈,且源源不斷的有死迴圈請求進來,當前場景會因為併發數過大全部被限流
cpu基本被打滿(這裡對root租戶作了10%cpu保護,並不會800%,這裡實際略高於720%)
使用多租且限定租戶最大cpu使用50%,仍然構建一個場景死迴圈,可以看到當前場景只是少量超時,因為cgroup的排程也會造成場景rt上升,符合業務95%以上正確率的要求。
觀察容器cpu,正常場景800qps時容器cpu仍有餘量
接下去我們還會做多租戶的動態調控,對於問題場景自動降權,避免cpu的浪費。
最後感謝ajdk團隊對我們需求的支援和技術幫助, @傳勝 @三紅 @右席 @卓仁
相關文章
- SaaS多租戶的3種隔離模式模式
- RocketMQ 在多 IDC 場景以及多隔離區場景下的實踐MQ
- 使用 NGINX 在 Kubernetes 中實現多租戶和名稱空間隔離Nginx
- 許可權管理之多租戶隔離授權
- SAP Hybris和Netweaver的租戶隔離(Tenant isolation)機制設計NaN
- 如何解決 K8s 多租戶叢集的安全隔離難題?K8S
- 多租戶
- K8s 實踐 | 如何解決多租戶叢集的安全隔離問題?K8S
- 【穩定性】穩定性建設之依賴設計
- 多租戶的後臺管理系統框架涉及到在不同租戶之間隔離資料(欄位隔離)------------升鮮寶供應鏈管理系統NestJs版本(一)框架JS
- Part II 配置和管理多租戶環境概述-Oracle多租戶管理員指南Oracle
- 【多租戶技術】
- 穩定性
- laravel多租戶之artisan命令列使用介紹Laravel命令列
- 如何用Serverless讓SaaS獲得更靈活的租戶隔離和更優的資源開銷Server
- 資源隔離技術之記憶體隔離記憶體
- 排序穩定性排序
- 吃雞的FPP和TPP模式之爭模式
- 如何理解多租戶架構?架構
- HBase多租戶-Namespace Quota管理namespace
- 12c多租戶架構裡給Common/local user賦權的幾種可能場景架構
- 阿里雲釋出效能測試 PTS 2.0:低成本、高效率、多場景壓測,業務穩定性保障利器阿里
- 一種透過nacos動態配置實現多租戶的log4j2日誌物理隔離的設計
- oracle12之 多租戶容器資料庫架構Oracle資料庫架構
- 3.3.2 多租戶環境的工具
- 多租戶商城系統解說
- 詳解ABP框架的多租戶框架
- Oracle多租戶特性的常用操作Oracle
- 圖解:什麼是多租戶?圖解
- Kafka 的穩定性Kafka
- Eureka 多環境隔離方案(包含本地開發人員間隔離)
- 3.3 用於多租戶環境的任務和工具
- 關於突破 SESSION 0 隔離場景發現的一些問題Session
- Oracle 12c 多租戶配置和修改 CDB 和 PDB 引數Oracle
- 混部之殤-論雲原生資源隔離技術之CPU隔離(一)
- 穩定性領導者!阿里雲獲得信通院多項系統穩定性最高階認證阿里
- 3.3.1 多租戶環境的任務
- 2 多租戶體系結構概述