通過 Mesos、Docker 和 Go,使用 300 行程式碼建立一個分散式系統
【摘要】雖然 Docker 和 Mesos 已成為不折不扣的 Buzzwords ,但是對於大部分人來說它們仍然是陌生的,下面我們就一起領略 Mesos 、Docker 和 Go 配合帶來的強大破壞力,如何通過 300 行程式碼打造一個比特幣開採系統。
時下,對於大部分 IT 玩家來說, Docker 和 Mesos 都是熟悉和陌生的:熟悉在於這兩個詞無疑已成為大家討論的焦點,而陌生在於這兩個技術並未在生產環境得到廣泛使用,因此很多人仍然不知道它們究竟有什麼優勢,或者能幹什麼。近日, John Walter 在 Dzone 上撰文 Creating a Distributed System in 300 Lines With Mesos, Docker, and Go,講述了 Mesos、Docker 和 Go 配合帶來的強大破壞力,本文由 OneAPM 工程師編譯整理。
誠然,構建一個分散式系統是很困難的,它需要可擴充套件性、容錯性、高可用性、一致性、可伸縮以及高效。為了達到這些目的,分散式系統需要很多複雜的元件以一種複雜的方式協同工作。例如,Apache Hadoop 在大型叢集上並行處理 TB 級別的資料集時,需要依賴有著高容錯的檔案系統( HDFS )來達到高吞吐量。
在之前,每一個新的分散式系統,例如 Hadoop 和 Cassandra ,都需要構建自己的底層架構,包括訊息處理、儲存、網路、容錯性和可伸縮性。慶幸的是,像 Apache Mesos 這樣的系統,通過給分散式系統的關鍵構建模組提供類似作業系統的管理服務,簡化了構建和管理分散式系統的任務。Mesos 抽離了 CPU 、儲存和其它計算資源,因此開發者開發分散式應用程式時能夠將整個資料中心叢集當做一臺巨型機對待。
構建在 Mesos 上的應用程式被稱為框架,它們能解決很多問題: Apache Spark,一種流行的叢集式資料分析工具;Chronos ,一個類似 cron 的具有容錯性的分散式 scheduler ,這是兩個構建在 Mesos 上的框架的例子。構建框架可以使用多種語言,包括 C++,Go,Python,Java,Haskell 和 Scala。
在分散式系統用例上,比特幣開採就是一個很好的例子。比特幣將為生成 acceptable hash 的挑戰轉為驗證一塊事務的可靠性。可能需要幾十年,單檯膝上型電腦挖一塊可能需要花費超過 150 年。結果是,有許多的“採礦池”允許採礦者將他們的計算資源聯合起來以加快挖礦速度。Mesosphere 的一個實習生, Derek ,寫了一個比特幣開採框架(https://github.com/derekchiang/Mesos-Bitcoin-Miner),利用叢集資源的優勢來做同樣的事情。在接下來的內容中,會以他的程式碼為例。
1 個 Mesos 框架有 1 個 scheduler 和 1 個 executor 組成。scheduler 和 Mesos master 通訊並決定執行什麼任務,而 executor 執行在 slaves 上面,執行實際任務。大多數的框架實現了自己的 scheduler,並使用 1 個由 Mesos 提供的標準 executors 。當然,框架也可以自己定製 executor 。在這個例子中即會編寫定製的 scheduler,並使用標準命令執行器( executor )執行包含我們比特幣服務的 Docker 映象。
對這裡的 scheduler 來說,需要執行的有兩種任務—— one miner server task and multiple miner worker tasks。 server 會和一個比特幣採礦池通訊,並給每個 worker 分配 blocks 。Worker 會努力工作,即開採比特幣。
任務實際上被封裝在 executor 框架中,因此任務執行意味著告訴 Mesos master 在其中一個 slave 上面啟動一個 executor 。由於這裡使用的是標準命令執行器(executor),因此可以指定任務是二進位制可執行檔案、bash 指令碼或者其他命令。由於 Mesos 支援 Docker,因此在本例中將使用可執行的 Docker 映象。Docker 是這樣一種技術,它允許你將應用程式和它執行時需要的依賴一起打包。
為了在 Mesos 中使用 Docker 映象,這裡需要在 Docker registry 中註冊它們的名稱:
const (
MinerServerDockerImage = "derekchiang/p2pool"
MinerDaemonDockerImage = "derekchiang/cpuminer"
)
然後定義一個常量,指定每個任務所需資源:
const (
MemPerDaemonTask = 128 // mining shouldn't be memory-intensive
MemPerServerTask = 256
CPUPerServerTask = 1 // a miner server does not use much CPU
)
現在定義一個真正的 scheduler ,對其跟蹤,並確保其正確執行需要的狀態:
type MinerScheduler struct {
// bitcoind RPC credentials
bitcoindAddr string
rpcUser string
rpcPass string
// mutable state
minerServerRunning bool
minerServerHostname string
minerServerPort int // the port that miner daemons
// connect to
// unique task ids
tasksLaunched int
currentDaemonTaskIDs []*mesos.TaskID
}
這個 scheduler 必須實現下面的介面:
type Scheduler interface {
Registered(SchedulerDriver, *mesos.FrameworkID, *mesos.MasterInfo)
Reregistered(SchedulerDriver, *mesos.MasterInfo)
Disconnected(SchedulerDriver)
ResourceOffers(SchedulerDriver, []*mesos.Offer)
OfferRescinded(SchedulerDriver, *mesos.OfferID)
StatusUpdate(SchedulerDriver, *mesos.TaskStatus)
FrameworkMessage(SchedulerDriver, *mesos.ExecutorID,
*mesos.SlaveID, string)
SlaveLost(SchedulerDriver, *mesos.SlaveID)
ExecutorLost(SchedulerDriver, *mesos.ExecutorID, *mesos.SlaveID,
int)
Error(SchedulerDriver, string)
}
現在一起看一個回撥函式:
func (s *MinerScheduler) Registered(_ sched.SchedulerDriver,
frameworkId *mesos.FrameworkID, masterInfo *mesos.MasterInfo) {
log.Infoln("Framework registered with Master ", masterInfo)
}
func (s *MinerScheduler) Reregistered(_ sched.SchedulerDriver,
masterInfo *mesos.MasterInfo) {
log.Infoln("Framework Re-Registered with Master ", masterInfo)
}
func (s *MinerScheduler) Disconnected(sched.SchedulerDriver) {
log.Infoln("Framework disconnected with Master")
}
Registered 在 scheduler 成功向 Mesos master 註冊之後被呼叫。
Reregistered 在 scheduler 與 Mesos master 斷開連線並且再次註冊時被呼叫,例如,在 master 重啟的時候。
Disconnected 在 scheduler 與 Mesos master 斷開連線時被呼叫。這個在 master 掛了的時候會發生。
目前為止,這裡僅僅在回撥函式中列印了日誌資訊,因為對於一個像這樣的簡單框架,大多數回撥函式可以空在那裡。然而,下一個回撥函式就是每一個框架的核心,必須要認真的編寫。
ResourceOffers 在 scheduler 從 master 那裡得到一個 offer 的時候被呼叫。每一個 offer 包含一個叢集上可以給框架使用的資源列表。資源通常包括 CPU 、記憶體、埠和磁碟。一個框架可以使用它提供的一些資源、所有資源或者一點資源都不給用。
針對每一個 offer ,現在期望聚集所有的提供的資源並決定是否需要釋出一個新的 server 任務或者一個新的 worker 任務。這裡可以向每個 offer 傳送儘可能多的任務以測試最大容量,但是由於開採比特幣是依賴 CPU 的,所以這裡每個 offer 執行一個開採者任務並使用所有可用的 CPU 資源。
for i, offer := range offers {
// … Gather resource being offered and do setup
if !s.minerServerRunning && mems >= MemPerServerTask &&
cpus >= CPUPerServerTask && ports >= 2 {
// … Launch a server task since no server is running and we
// have resources to launch it.
} else if s.minerServerRunning && mems >= MemPerDaemonTask {
// … Launch a miner since a server is running and we have mem
// to launch one.
}
}
針對每個任務都需要建立一個對應的 TaskInfo message ,它包含了執行這個任務需要的資訊。
s.tasksLaunched++
taskID = &mesos.TaskID {
Value: proto.String("miner-server-" +
strconv.Itoa(s.tasksLaunched)),
}
Task IDs 由框架決定,並且每個框架必須是唯一的。
containerType := mesos.ContainerInfo_DOCKER
task = &mesos.TaskInfo {
Name: proto.String("task-" + taskID.GetValue()),
TaskId: taskID,
SlaveId: offer.SlaveId,
Container: &mesos.ContainerInfo {
Type: &containerType,
Docker: &mesos.ContainerInfo_DockerInfo {
Image: proto.String(MinerServerDockerImage),
},
},
Command: &mesos.CommandInfo {
Shell: proto.Bool(false),
Arguments: []string {
// these arguments will be passed to run_p2pool.py
"--bitcoind-address", s.bitcoindAddr,
"--p2pool-port", strconv.Itoa(int(p2poolPort)),
"-w", strconv.Itoa(int(workerPort)),
s.rpcUser, s.rpcPass,
},
},
Resources: []*mesos.Resource {
util.NewScalarResource("cpus", CPUPerServerTask),
util.NewScalarResource("mem", MemPerServerTask),
},
}
TaskInfo message 指定了一些關於任務的重要後設資料資訊,它允許 Mesos 節點執行 Docker 容器,特別會指定 name、task ID、container information 以及一些需要給容器傳遞的引數。這裡也會指定任務需要的資源。
現在 TaskInfo 已經被構建好,因此任務可以這樣執行:
driver.LaunchTasks([]*mesos.OfferID{offer.Id}, tasks, &mesos.Filters{RefuseSeconds: proto.Float64(1)})
在框架中,需要處理的最後一件事情是當開採者 server 關閉時會發生什麼。這裡可以利用 StatusUpdate 函式來處理。
在一個任務的生命週期中,針對不同的階段有不同型別的狀態更新。對這個框架來說,想要確保的是如果開採者 server 由於某種原因失敗,系統會 Kill 所有開採者 worker 以避免浪費資源。這裡是相關的程式碼:
if strings.Contains(status.GetTaskId().GetValue(), "server") &&
(status.GetState() == mesos.TaskState_TASK_LOST ||
status.GetState() == mesos.TaskState_TASK_KILLED ||
status.GetState() == mesos.TaskState_TASK_FINISHED ||
status.GetState() == mesos.TaskState_TASK_ERROR ||
status.GetState() == mesos.TaskState_TASK_FAILED) {
s.minerServerRunning = false
// kill all tasks
for _, taskID := range s.currentDaemonTaskIDs {
_, err := driver.KillTask(taskID)
if err != nil {
log.Errorf("Failed to kill task %s", taskID)
}
}
s.currentDaemonTaskIDs = make([]*mesos.TaskID, 0)
}
萬事大吉!通過努力,這裡在 Apache Mesos 上建立一個正常工作的分散式比特幣開採框架,它只用了大約 300 行 GO 程式碼。這證明了使用 Mesos 框架的 API 編寫分散式系統是多麼快速和簡單。
原文連結:Creating a Distributed System in 300 Lines With Mesos, Docker, and Go
本文系 OneAPM 工程師編譯整理。OneAPM 是應用效能管理領域的新興領軍企業,能幫助企業使用者和開發者輕鬆實現:緩慢的程式程式碼和 SQL 語句的實時抓取。想閱讀更多技術文章,請訪問 OneAPM 官方部落格。
相關文章
- 通過Mesos、Docker和Go,使用300行程式碼建立一個分散式系統DockerGo行程分散式
- 通過Go來分析和建立XMLGoXML
- 分散式程式碼管理系統GIT分散式Git
- 為自己搭建一個分散式 IM(即時通訊) 系統分散式
- 通過Go來分析和建立JSONGoJSON
- 使用Python建立一個系統監控程式Python
- 通過Consul Raft庫打造自己的分散式系統Raft分散式
- 分散式系統:程序間通訊分散式
- Mercurial 分散式程式碼管理系統 HG分散式
- go實現簡易分散式系統Go分散式
- 冰激凌和分散式系統分散式
- 建立一個系統的Service,能通過ServiceManager.getService取得service
- 轉一個大牛對分散式系統和cqrs的反思文章分散式
- 在分散式系統中使用非同步管道建立實體分散式非同步
- 【分散式】 07 系統通訊初識分散式
- 使用Faric+Git進行分散式程式碼管理Git分散式
- 實現一個分散式排程系統-LoadBalance和Ha策略分散式
- Spark:一個高效的分散式計算系統Spark分散式
- 分散式系統程式設計分散式程式設計
- 系統程式是什麼?怎麼通過系統程式進行病毒分析?
- Go語言分散式系統配置管理實踐--go archaiusGo分散式AI
- 分散式:分散式系統下的唯一序列分散式
- 通過一行程式碼學習javascript行程JavaScript
- 一個開源的分散式線上教育系統分散式
- 如何互動式地建立一個Docker容器Docker
- docker通過commit命令提交一個映象DockerMIT
- 通過7個函式解密區塊鏈(附程式碼)函式解密區塊鏈
- 在分散式系統中通過客戶端庫包提高可用性分散式客戶端
- 分散式訊息系統Kafka Java客戶端程式碼分散式KafkaJava客戶端
- 分散式系統分散式
- 一、Git分散式版本控制系統Git分散式
- Circuit: Go語言編寫的最小分散式程式設計式的作業系統UIGo分散式程式設計作業系統
- 建立一個docker imageDocker
- 通過Python指令碼理解系統執行緒薦Python指令碼執行緒
- 使用 Go 和 ReactJS 構建聊天系統(六):Docker 化後端GoReactJSDocker後端
- 分散式入門(一)- 通訊原語和通訊庫分散式
- Ceph:一個 Linux PB 級分散式檔案系統Linux分散式
- google/gops:一個列出和診斷系統中正在執行Go 程式的命令列工具。Go命令列