作者:徐佳
文為原創文章,轉載請注明作者及出處
在介紹Juice之前,我想先聊一聊Mesos,Mesos被稱為2層調度框架,是因為Master通過內部的Allocator完成Master->Framework的第一層調度,再由Framework通過調度器完成對于資源->任務的分配,這個過程稱為第二層調度。
About MesosFramework
先來看一看Mesos&Framework的整體架構圖:
Mesos的Framework分為2部分組成,分別為調度器和執行器。
調度器被稱為Scheduler,從Mesos1.0版本開始,官方提供了基于HTTP的RestAPI供外部調用并進行二次開發。
Scheduler用于處理Master端發起的回調事件(資源列表并加載任務、任務狀態通知等),進行相應處理。Agent接收到Master分配的任務時,會根據任務的container-type進行不同的處理,當處理默認container-type='Mesos'時,先檢查Framework所對應的Executor進程是否啟動,如果沒有啟動則會先啟動Executor進程,然后再提交任務到該Executor去執行,當運行一個container-type='Docker'的任務時,則啟動Docker Executor進行處理,程序的運行狀態完全取決于Docker內部的處理及返回值。
MesosFramework交互API
交互分為2部分API,分別為SchedulerAPI(http://mesos.apache.org/documentation/latest/scheduler-http-api/) 與ExecutorAPI(http://mesos.apache.org/documentation/latest/Executor-http-api/), 每個API都會以TYPE來區分,具體的處理流程如下:
1.Scheduler提交一個請求(type='SUBSCRIBE')到Master(http://master-ip:5050/api/v1/scheduler), 并需要設置'subscribe.framework_info.id',該ID由Scheduler生成,在一個Mesos集群中必須保證唯一,Mesos以此FrameworkID來區分各個Framework所提交的任務,發送完畢后,Scheduler端等待Master的'SUBSCRIBE'回調事件,Master的返回事件被定義在event對象中,event.type為'SUBSCRIBE'(注意:'SUBSCRIBE'請求發起后,Scheduler與Master端會保持會話連接(keep-alive),Master端主動發起的事件回調都會通過該連接通知到Scheduler)。(scheduler-http-api中接口'SUBSCRIBE')
2.Master主動發起'OFFERS'事件回調,通知Scheduler目前集群可分配使用資源,事件的event.type為'OFFERS'。(scheduler-http-api中接口'OFFERS')
3.Scheduler調用resourcesOffer為Offers安排Tasks。當完成任務分配后,主動發起'ACCEPT'事件請求到Master端告知Offers-Tasks列表。(scheduler-http-api中接口'ACCEPT')
4.Master接收到Scheduler的任務請求后,將任務發送到OfferId對應的Agent中去執行任務。
5.Agent接收到任務,檢查任務對應的Executor是否啟動,如啟動,則調用該Executor執行任務,如未啟動,則調用lauchExecutor()創建Executor對象并執行initialize()初始化Executor,Executor初始化過程中會調用RegisterExecutorMessage在Agent上注冊,之后便接受任務開始執行。(Executor-http-api中接口'LAUNCH')
6.Executor執行完畢或錯誤時通知Agent任務的task_status。(Executor-http-api中接口'UPDATE')
7.Agent再同步task_status給Master,Master則調用'UPDATE'事件回調,通知Scheduler更新任務狀態。(scheduler-http-api中接口'UPDATE')
8.Scheduler確認后發送'ACKNOWLEDGE'請求告知Master任務狀態已確認。(scheduler-http-api中接口'ACKNOWLEDGE')
任務狀態標示及Agent宕機處理###
對于一個任務的運行狀態,Mesos定義了13種TASK_STATUS來標示,常用的有以下幾種:
TASK_STAGING-任務準備狀態,該任務已有Master分配給Slave,但Slave還未運行時的狀態。
TASK_RUNNING-任務已在Agent上運行。
TASK_FINISHED-任務已運行完畢。
TASK_KILLED-任務被主動終止,調用scheduler-http-api中'KILL'接口。
TASK_FAILED-任務執行失敗。
TASK_LOST-任務丟失,通常發生在Slave宕機。
當Agent宕機導致TASK_LOST時,Mesos又是怎么來處理的呢?
在Master和Agent之間,一般都是由Master主動向每一個Agent發送Ping消息,如果在設定時間內(flag.slave_ping_timeout,默認15s)沒有收到Agent的回復,并且達到一定次數(flag.max_slave_ping_timeouts,默認次數為5),那么Master會操作以下幾個步驟:
1.將該Agent從Master中刪除,此時該Agent的資源將不會再分配給Scheduler。
2.遍歷該Agent上運行的所有任務,向對應的Framework發送任務的Task_Lost狀態更新,同時把這些任務從Master中刪除。
3.遍歷該Agent上的所有Executor,并刪除。
4.觸發Recind Offer,把這個Agent上已經分配給Scheduler的Offer撤銷。
5.把這個Agent從master的Replicated log中刪除(Mesos Master依賴Replicated log中的部分持久化集群配置信息進行failer over/recovery)。
使用Marathon可以方便的發布及部署應用###
目前有很多基于MesosFramework的開源框架,例如Marathon。我們在生產環境中已經使用了Marathon框架,一般用它來運行long-run service/application,依靠marathon來管理應用服務,它支持應用服務自動/手動起停、水平擴展、健康檢查等。我們依靠jenkins+docker+marathon完成服務的自動化發布及部署。
Why Juice
下面來講下我基于MesosFramework所開發的一套框架-Juice。(開源地址:https://github.com/HujiangTechnology/Juice.git)
在開發Juice之前,我公司所有的音視頻轉碼切片任務都是基于一個叫TaskCenter的隊列分配框架,該框架并不具備分布式調度的功能(資源分配),所以集群的資源利用率一直是個問題,所以,我們想開發一套基于以下三點的新框架來替代老的TaskCenter。
1.一個任務調度型的框架,需要對資源(硬件)盡可能的做到最大的利用率。
2.框架必須可運行各種類型的任務。
3.平臺必須是穩定的。
憑借對Marathon的使用經驗,以及對于Mesos相關文檔的查閱,我們決定基于MesosFramework來開發一套任務調度型的框架,Mesos與Framework的特性剛才已經說過了,而我們將所需要執行的任務封在Docker中去執行,那么對于框架本身來說他就不用關心任務的類型了,這樣業務的邊界和框架的邊界就變得很清晰,對于Framework來說,運行一個Docker任務也很方便,剛才說過Mesos內置了DockerExecutor可以完美的啟動Docker任務,這樣,我們的框架在Agent端所需要的開發就非常的少。
Juice框架在這樣的背景下開始了開發的歷程,我們對于它的定位是一套分布式任務云系統,這里為什么要稱為任務云系統呢?因為對于調用者來說,使用Juice,只要做2件事情:把要做的任務打成Docker鏡像并Push到docker倉庫中,然后向Juice提交一個Docker類型的任務。其它的,交給Juice去完成就可以了,調用者不用關心任務會在哪臺物理機上被執行,只需要關心任務本身的執行狀況。
Juice架構
除此,Juice有以下一些特點,Juice框架分為Juice-Rest(Juice交互API層,可以完成外界對于Juice Task的CRUD操作)和Juice-Service(Juice核心層,負責與MesosMaster之間的交互,資源分配、任務提交、任務狀態更新等),在一套基于Juice框架的應用系統中,通常部署1-N個Juice-Rest(取決于系統的TPS),以及N個Juice-Service(Juice-Service分主從模式,為1主多從,by zookeeper),對于同一個Mesos集群來說,可以部署1-N套Juice框架,以FrameworkID來區分,需要部署多套的話在Juice-Service的配置文件中設置mesos.framework.tag為不同的值即可。
Juice-Rest參數設置
Juice-Rest采用Spring-Boot編寫(Juice-API接口參見:https://github.com/HujiangTechnology/Juice/blob/master/doc/api_document.md), 處理外界發起的對任務CURD操作,當提交一個任務到Juice-Rest時,需要設置一些參數,比如:
example to run docker:
{
"callbackUrl":"http://www.XXXXXXXX.com/v5/tasks/callback",
"taskName":"demo-task",
"env":{"name":"environment","value":"dev"},
"args":["this is a test"],
"container":{
"docker":{
"image":"dockerhub.XXXX.com/demo-slice"
},
"type":"DOCKER"
}
}
其中Container中的type目前僅支持'Docker',我們沒有加入'Mesos'類型的Container模式是因為目前項目組內部的服務已經都基于Docker化,但是預留了'Mesos'類型,在未來可以支持'Mesos'類型的任務。
commands模式支持運行Linux命令行命令和Shell腳本,比如:
"commands":"/home/app/entrypoint.sh"
這里支持Commands模式的原因有2點
1.有時調用方可能只是想在某臺制定的Agent上運行一個腳本。
2.公司內部其他有些項目組還在使用Jar包啟動的模式,預留一個Shell腳本的入口可以對這些項目產生支持。
env設置示例,設置運行的任務環境為dev:
"env":{"name":"environment","value":"dev"}
args設置示例,設置文件路徑:
"args":["/tid/res/test.mp4"]
PS:使用Commands模式時不支持args選項。
此外,Juice-Rest支持用戶自定義資源大小(目前版本僅支持自定義CPU、內存),如需要指定資源,需在請求接口中配置resources對象,否則,將會使用默認的資源大小運行任務。Juice-Rest支持資源約束(constrains),即滿足在特定Host或Rack_id標簽的Agent上運行某任務,設置接口中constrains對象字段即可。
Juice所使用的中間件(MQ、DB等)
下面講一下Rest層的處理模型,當外界發起一個任務請求時,Juice-Rest接收到任務后,并不是直接提交到Juice-Service層,而是做了以下2件事情:
1.將任務放入MQ中。(目前Juice使用Redis-List來作為默認的Queue,采用LPUSH、RPOP的模式,先進先出,為什么選擇使用Redis中的List作為Queue而沒有選擇其他諸如rabbitmq、kafka這些呢,首先,Redis相對來說是一個比較輕量級的中間件,而且HA方案比較成熟,同時,在我看來,隊列中的最佳任務wait數量是應該<10000的,否則,任務的執行周期將會被拉得很長,以我公司的Juice系統來舉例,由于處理的都是耗時的音視頻轉碼切片任務,通常情況下10000個任務的排隊等候時間會在幾個小時以上,所以當任務數量很大時,考慮擴大集群的處理能力而不是把過多的任務積壓在隊列中,基于此,選擇Redis-List相對其他的傳統MQ來說沒有什么劣勢。考慮到一些特殊情況,Juice也允許用戶實現CacheUtils接口使用其他MQ替換Redis-List)。
2.紀錄Tasks信息到Juice-Tasks表中,相當于數據落地。后續版本會基于此實現任務重試機制(目前的1.1.0內部開發版本已實現),或者在failover切換后完成任務恢復,此功能在后續1.2.0版本中考慮加入。(目前數據庫使用MySql)。
當Juice-Rest接受并完成任務提交后會返回給調用方一個Long型18位數字(JuiceID,全局唯一)作為憑證號。當任務完成后,Juice-Rest會主動發起回調請求,通知調用方該任務的運行結果(以此JuiceID作為業務憑證),前提是調用方必須設置callbackUrl。同時,調用方可以使用該JuiceID對進行任務查詢、終止等操作。
另外,在Juice-Rest層單獨維護一個線程池來處理由Juice-service端返回的任務狀態信息Task_status。
Juice-Service內部處理流程
Juice-Service可以看作是一個MesosFramework,與Master之間通訊協議采用ProtoBuf,每一種事件請求都通過對應類型的Call產生,這里Juice-Service啟動時會發出Subscribe請求,由SubscribeCall()方法產生requestBody,采用OKHTTP發送,并維持與Master之間的長連接
private void connecting() throws Exception {
InputStream stream = null;
Response res = null;
try {
Protos.Call call = subscribeCall();
res = Restty.create(getUrl())
.addAccept(protocol.mediaType())
.addMediaType(protocol.mediaType())
.addKeepAlive()
.requestBody(protocol.getSendBytes(call))
.post();
streamId = res.header(STREAM_ID);
stream = res.body().byteStream();
log.info("send subscribe, frameworkId : " + frameworkId + " , url " + getUrl() + ", streamId : " + streamId);
log.debug("subscribe call : " + call);
if (null == stream) {
log.warn("stream is null");
throw new DriverException("stream is null");
}
while (true) {
int size = SendUtils.readChunkSize(stream);
byte[] event = SendUtils.readChunk(stream, size);
onEvent(event);
}
} catch (Exception e) {
log.error("service handle error, due to : " + e);
throw e;
} finally {
if (null != stream) {
stream.close();
}
if (null != res) {
res.close();
}
streamId = null;
}
}
之后便進入while循環,當Master端的通知事件發生時,調用onEvent()方法執行。
Mesos的回調事件中,需要特別處理的主要事件由以下幾種:
1.SUBSCRIBED:Juice框架在接收到此事件后將注冊到Master中的FrameworkID紀錄到數據庫juice_framework表中。
2.OFFERS:當Juice-Service接收到該類型事件時,便會進入資源/任務分配環節,分配任務資源并提交到MesosMaster。
3.UPDATE:當Agent處理完任務時,任務會由Executor->Agent->Master->Juice-Service來完成任務的狀態通知。Juice-Service會將結果塞入result-list中。
4.ERROR:框架產生問題,通常這樣的問題分兩種,一種是比較嚴重的,例如Juice-Service使用了一個已經被Master端移除的FrameworkID,則Master會返回"framework has been removed"的錯誤信息,Juice-Service此時會拋出UnrecoverException錯誤:
throw new UnrecoverException(message, true)
Juice-Service在處理UnrecoverException類的錯誤時會Reset服務,當第二個參數為True時,會重新生成一個新的FrameworkID。
而當其他類型的錯誤,比如Master和Juice-Service之間的長鏈接中斷,僅僅Reset服務。
下面我想詳細來說說第二步,我們先來看下'OFFERS'請求處理代碼段:
private void onEvent(byte[] bytes) {
....
switch (event.getType()) {
...
case OFFERS:
try {
event.getOffers().getOffersList().stream()
.filter(of -> {
if (SchedulerService.filterAndAddAttrSys(of, attrMap)) {
return true;
}
declines.add(of.getId());
return false;
})
.forEach(
of -> {
List<TaskInfo> tasks = newArrayList();
String offerId = of.getId().getValue();
try {
SchedulerService.handleOffers(killMap, support, of, attrMap.get(offerId), declines, tasks);
} catch (Exception e) {
declines.add(of.getId());
tasks.forEach(
t -> {
AuxiliaryService.getTaskErrors()
.push(new TaskResult(com.hujiang.juice.common.model.Task.splitTaskNameId(t.getTaskId().getValue())
, ERROR, "task failed due to exception!"));
}
);
tasks.clear();
}
if (tasks.size() > 0) {
AuxiliaryService.acceptOffer(protocol, streamId, of.getId(), frameworkId, tasks, getUrl());
}
}
);
if (declines.size() > 0) {
AuxiliaryService.declineOffer(protocol, streamId, frameworkId, SchedulerCalls.decline(frameworkId, declines), getUrl());
}
long end = System.currentTimeMillis();
} finally {
declines.clear();
attrMap.clear();
}
break;
...
}
}
該段代碼是分配Offer-tasks的核心代碼,來看幾個方法:
1.SchedulerService.filterAndAddAttrSys(),該方法作用是過濾不符合的OFFER,我們知道在Mesos的Agent中是可以通過配置Attr來使一些機器跑特殊的任務,而這里的過濾正是基于該特性,比如我們設置了該Juice-Service只使用包含以下Attr屬性的資源時(在配置文件application.properties中)
mesos.framework.attr=lms,qa,mid|big
經過了SchedulerService.filterAndAddAttrSys()方法的過濾,符合以上attr的資源會被選取執行任務。同時不符合的Offer會加入declines List,通過AuxiliaryServic.declineOffer()一次性發送給Master告知忽略。
Agent的attr設置通過/etc/mesos-slave/attributes來設置。這個文件通常為這樣的:
cat /etc/mesos-slave/attributes
bz:xx;
env:xx;
size:xx;
rack_id:xx;
dc:xx
2.SchedulerService.handleOffers(),該方法實現了原先MesosFramework中的resourceOffer的功能,對Offer進行Tasks分配,最后產生TaskInfo List,由AuxiliaryService.acceptOffer()發送給Master通知處理任務。
注意:Master在發送完Offer事件通知后會一直處于wait狀態,直到Framework端調用Accept call(AuxiliaryService.acceptOffer())或Decline call(AuxiliaryServic.declineOffer())來告知Master資源是否使用后才會通知下一個Framework去分配資源。(默認Master會一直等待,如果沒有通知,則Mesos集群中的資源利用率將可能達到100%,可以通過在Master端設置Timeout來避免這個問題。)
在Juice-Service內部,當SchedulerDriver與Master產生交互后,Juice-Service的處理邏輯由SchedulerService以及AuxiliaryService來實現。
SchedulerService處理Juice的主要邏輯,比如資源分配算法、任務優先級算法,所有Master回調事件處理方法都定義在SchedulerService中。
AuxiliaryService維護幾組線程池,完成各自任務,剛才看到的AuxiliaryService.acceptOffer()和AuxiliaryServic.declineOffer(),都是通過調用AuxiliaryServic中的send-pool去完成call的發送,另外還有一些管理類的任務(比如實時查詢任務狀態、終止正在運行的任務等等)通過auxiliary-pool去完成。所以,AuxiliaryServic的調用都是異步的。
Juice中各種隊列的功能介紹
剛才介紹了Juice的任務在JuiceRest提交時是被放入了一個MQ中,這個MQ在Juice-Service中被稱為juice.task.queue。除此之外,還有另外幾個MQ,分別是juice.task.retry.queue、juice.task.result.queue、juice.management.queue。下面來分別說說這些Queue的用處。
juice.task.retry.queue:Juice-Service在取任務時是按照每一個Offer輪詢分配的,當一個Offer在分配資源時,假如從MQ中R-POP出來的任務不滿足該Offer時(比如need-resources大于該Offer的max offer value時,或者存在constrains,當前的offer和指定執行任務的offer不match時),這時,Juice-Service的做法是將當前任務放入juice.task.retry.queue中,等待下一次Offer分配時,優先從juice.task.retry.queue獲取任務并分配,這里涉及到Juice內部獲取任務Queue的優先級,我用了一個比較簡單的方式,即每次分配一個新的Offer資源時,先從juice.task.retry.queue中取出一定數目的任務(CACHE_TRIES = 5),當還有剩余資源時,則從juice.task.queue中取任務,直到撐滿這個Offer。另外,處于juice.task.retry.queue會有淘汰機制,目前的任務淘汰機制遵循2點,當先觸發以下某一項時,則該任務會認為失敗,任務的Task_status被設置為Task_Failed,放入juice.task.result.queue,任務的淘汰算法如下:
1.過期時間淘汰制,任務處于juice.task.result.queue的時長>TASK_RETRY_EXPIRE_TIME,則淘汰(DEFAULT_TASK_RETRY_EXPIRE_TIME = 86400秒)。
2.大于最大檢索次數,任務被取出檢索但沒有被執行達到最大檢索次數>MAX_RESERVED,則淘汰(DEFAULT_MAX_RESERVED = 1024)。
juice.task.result.queue:任務結果隊列,Juice-Service在得到一個任務的狀態后(不一定是最終狀態),將任務的TaskResult對象放入juice.task.result.queue,Juice-Rest端從該隊列取出TaskResult,如果已經是任務的最終狀態,比如Task_Finished或者Task_Failed,則通過外部在提交任務時所填寫的callbackUrl回調調用方告知任務狀態。
juice.management.queue:管理類隊列,支持放入Reconcile類或Kill類的任務,由AuxiliaryService發起任務的查詢同步或Kill一個正在執行的任務。
通過SDK提交一個任務###
目前開源的Juice版本,已經提供了完整的SDK來完成對于Juice-Rest之間的交互,以下是提交一個docker任務的示例:
總結及未來
目前Juice 1.1.0開源版本已經處于測試階段,新版本除修復一些Bug之外,還增加了2個新功能:
1.增加了任務插隊功能,可以通過在傳入參數中設置priority=1來提高一個任務的執行優先級,該任務會被置于處理隊列的最前端。
2.任務失敗自動重試功能,設置傳入參數retry=1,任務失敗會自動重試,最多重試3次。
面對復雜的業務需求,Juice目前的版本還有一些特性/功能不支持,對于此,最好的方式是請大家Fork這個項目的Git,或直接聯系本人,大家一起來把Juice做好。
@Test
public void submitsDocker() {
Submits submitsDocker = Submits.create()
.setDockerImage("dockerhub.XXXX.com/demo-slice")
.setTaskName("demo-slice")
.addArgs("/10002/res/L2.mp4")
.addEnv("environment", "dev")
.addResources(2.0, 2048.0);
Long taskId = JuiceClient.create("http://your-juice-rest-host/v1/tasks", "your-system-id-in-string")
.setOperations(submitsDocker)
.handle();
if(null != taskId) {
System.out.println("submitsDocker, taskId --> " + taskId);
}
}
Q&A:
Q.juice與elastic job的差異
A.我本身對于elastic job并不算太熟悉,就隨便說幾點,如果有錯還請各位糾正:
首先juice與elastic-job-cloud都基于mesos,資源-任務分配這塊elastic-job用了Fenzo(netflix),而juice是自己開發的調度算法。
juice在作業調用時不需要作業注冊,只要上傳任務的鏡像(Docker)到倉庫及任務觸發。而elastic-job需要注冊作業。
juice在Rest-Api接口上近乎完全和marathon一致,方便一些使用慣marathon部署service的用戶。
juice目前版本并不支持作業分片。
Q.能詳細介紹下任務資源分配這一塊的算法嗎?
A.之前已經簡單介紹過了,通過接收'OFFERS'事件觸發相關任務-資源分配的代碼塊。
由于得到的Offer對象實際為一個列表,處理邏輯會循環為每一個Offer分配具體的任務,而每個Offer的任務列表總資源(CPU,Memory等)必需小于Offer resources * RESOURCES_USE_THRESHOLD(資源使用閥值,可通過配置文件resources.use.threshold設置,默認0.8),每分配完一個Offer的task_infos后,便生成Accept Call由發送線程池進行發送處理,整個過程都是異步非阻塞的。
Q.所有的任務都存檔在docker里面對于一些臨時的任務如何處理?
A.臨時的任務確實會產生一些垃圾的鏡像,需要定期對Docker倉庫進行清理,一般設置清理周期為1個月。
Q.任務系統是是否有幫助用戶完成docker封裝的操作?
A.目前沒有,所以使用者必需會一些Docker的基本操作,至少要會打鏡像,提交鏡像等。當然,像一些Docker的設置,比如掛載volume,網絡(bridge、host)等可以在提交任務時通過參數設置。
Q.Mesos和kubernetes的優劣勢是什么?
A.其實我主要使用Mesos,Mesos相對K8S應該是一套更重的系統,Mesos更像是個分布式操作系統,而K8S在容器編排方面更有優勢(Pod之類)。