通過Mesos、Docker和Go,使用300行程式碼建立一個分散式系統

CSDN發表於2015-08-03

構建一個分散式系統是很困難的。它需要可擴充套件性、容錯性、高可用性、一致性、可伸縮以及高效。為了達到這些目的,分散式系統需要很多複雜的元件以一種複雜的方式協同工作。例如,Apache Hadoop在大型叢集上並行處理TB級別的資料集時,需要依賴有著高容錯的檔案系統(HDFS)來達到高吞吐量。

在之前,每一個新的分散式系統,例如Hadoop和Cassandra,都需要構建自己的底層架構,包括訊息處理、儲存、網路、容錯性和可伸縮性。慶幸的是,像Apache Mesos這樣的系統,通過給分散式系統的關鍵構建模組提供類似作業系統的管理服務,簡化了構建和管理分散式系統的任務。Mesos抽離了CPU、儲存和其它計算資源,因此開發者開發分散式應用程式時能夠將整個資料中心叢集當做一臺巨型機對待。

通過Mesos、Docker和Go,使用300行程式碼建立一個分散式系統

構建在Mesos上的應用程式被稱為框架,它們能解決很多問題:Apache Spark,一種流行的叢集式資料分析工具;Chronos,一個類似cron的具有容錯性的分散式scheduler,這是兩個構建在Mesos上的框架的例子。構建框架可以使用多種語言,包括C++,Go,Python,Java,Haskell和 Scala。

在分散式系統用例上,比特幣開採就是一個很好的例子。比特幣將為生成 acceptable hash 的挑戰轉為驗證一塊事務的可靠性。可能需要幾十年,單檯膝上型電腦挖一塊可能需要花費超過150年。結果是,有許多的“採礦池”允許採礦者將他們的計算資源聯合起來以加快挖礦速度。Mesosphere的一個實習生,Derek,寫了一個比特幣開採框架,利用叢集資源的優勢來做同樣的事情。在接下來的內容中,會以他的程式碼為例。

1個Mesos框架有1個scheduler 和1個Executor組成。scheduler 和Mesos master通訊並決定執行什麼任務,而executor 執行在slaves上面,執行實際任務。大多數的框架實現了自己的scheduler,並使用1個由Mesos提供的標準executors。當然,框架也可以自己定製executor。在這個例子中即會編寫定製的scheduler,並使用標準命令執行器(executor)執行包含我們比特幣服務的Docker映象。

對這裡的scheduler來說,需要執行的有兩種任務—— 單礦伺服器任務和多礦伺服器任務。伺服器會和一個比特幣採礦池通訊,並給每個“工人”分配塊。“工人”會努力工作,即開採比特幣。

任務實際上被封裝在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編寫分散式系統是多麼快速和簡單。

相關文章