skynet源碼分析_master_slave模式

master_slave模式

skynet是支持在不同機器上協作的,之間通過TCP互連。不過有兩種模式可以選,一種是master/slave模式,一種是cluster模式,這裏說說master/slave模式。

  1. skynet的master/slave模式是一個master與多個slave的模式,master與每個slave相連,每個slave又兩兩互連。master同時會充當一箇中心節點的作用,用來協調各個slave的工作。
  2. 如果在config中,配置harbor = 0,那麼skynet會工作帶單節點模式下,其他參數(address、master、standalone)都不用設置。
  3. 如果在config中配置 harbor 爲1-255中的數字,那麼skynet會工作在多節點模式下,如果配置了 standalone, 那麼此節點是中心節點。
  4. 只要是多節點模式, address 與 master 都需要配置,其中 address 爲此節點的地址,供本節點監聽, master 爲外部中心節點的地址,供slave連接(或者供中心節點監聽)

本文配合2016年下旬最新版skynet源碼註釋更佳

帶着問題去了解

分析master/slave模式,我是帶着以下問題去了解的。

  • 全局的字符串地址是怎麼實現的
  • 消息是怎麼從一個節點傳遞到另外一個節點的
  • skynet.uniqueservice是怎麼創建多節點有效的服務的
  • 爲什麼不推薦使用master/slave模式

從bootstrap說起

bootstrap 是 skynet 中啓動的第二個服務(第一個是 logger),它是一個臨時服務(工作做完就結束了),主要工作是做一些上層管理類服務的初始化工作。 bootstrap 的部分代碼:

local standalone = skynet.getenv "standalone"
-- 獲取 config 中的 standalone 參數,如果standalone存在,它應該是一個"ip地址:端口"

local harbor_id = tonumber(skynet.getenv "harbor" or 0)
-- 獲取 config 中的 harbor 參數

-- 如果 harbor 爲 0 (即工作在單節點模式下)
if harbor_id == 0 then
    assert(standalone ==  nil)    -- 如果是單節點, standalone 不能配置
    standalone = true
    skynet.setenv("standalone", "true")    -- 設置 standalone 的環境變量爲true

    -- 如果是單節點模式,則slave服務爲 cdummy.lua
    local ok, slave = pcall(skynet.newservice, "cdummy")
    if not ok then
        skynet.abort()
    end
    skynet.name(".cslave", slave)

else                    -- 如果是多節點模式
    if standalone then    -- 如果是中心節點則啓動 cmaster 服務
        if not pcall(skynet.newservice,"cmaster") then
            skynet.abort()
        end
    end

    -- 如果是多節點模式,則 slave 服務爲 cslave.lua
    local ok, slave = pcall(skynet.newservice, "cslave")
    if not ok then
        skynet.abort()
    end
    skynet.name(".cslave", slave)
end

if standalone then    -- 如果是中心節點則啓動 datacenterd 服務
    local datacenter = skynet.newservice "datacenterd"
    skynet.name("DATACENTER", datacenter)
end

從代碼可以看出,從配置文件裏面讀取出 harbor 配置

  • 如果是 0,代表是單節點,那麼只啓動 cdummy 作爲 slave 服務的僞裝即可
  • 如果是非 0(1-255),代表是多節點,啓動 cslave 服務;如果是中心節點(配置了 standalone),還需要啓動 cmaster 與 datacenter 服務

master/slave模式的C層面的初始化

除了 bootstrap 中啓動的服務,還有一個很重要的C服務需要啓動:harbor服務(源文件爲 service-src/service_harbor.c),下面先看看C層面的初始化(僅僅看master/slave)

  • main 函數中的 skynet_start 函數

  • void skynet_start(struct skynet_config * config)
          skynet_harbor_init(config->harbor); // 初始化 harbor id,用來後續判斷是否是非本節點的服務地址
          skynet_handle_init(config->harbor); // 將 harbor id 保存在 struct handle_storage 的高八位

  • 怎麼啓動的harbor服務 如果是單節點模式,會啓動 cdummy 服務,在 cdummy 的 skynet.start 函數中有:harbor_service = assert(skynet.launch("harbor", harbor_id, skynet.self()))

怎麼啓動的harbor服務 如果是單節點模式,會啓動 cdummy 服務,在 cdummy 的 skynet.start 函數中有:harbor_service = assert(skynet.launch("harbor", harbor_id, skynet.self()))

同樣,如果是多節點模式,會啓動 cslave 服務,在 cdummy 的 skynet.start 函數中有:harbor_service = assert(skynet.launch("harbor", harbor_id, skynet.self()))

可見不管是多節點還是單節點模式都會啓動 harbor 服務,在skynet進程啓動過程中會看到類似於:[:00000005] LAUNCH harbor 0 4的字樣

查看相關各服務的啓動工作

從初始化過程我們可以看到,讓master/slave模式工作起來的服務主要有:cmaster、cslave、harbor服務,看看這三個服務做了些什麼工作。

簡單說說harbor服務的消息處理函數

註冊消息處理函數(可以看出如果有消息過來,會調用mainloop來處理)

int harbor_init(struct harbor *h, struct skynet_context *ctx, const char * args)
    skynet_callback(ctx, h, mainloop);

mainloop的處理:

static int mainloop(struct skynet_context * context, void * ud, int type, int session, uint32_t source, const void * msg, size_t sz)
    struct harbor * h = ud;
    switch (type) {
        case PTYPE_SOCKET: {        // 收到遠端 harbor 的消息
            ...
        case PTYPE_HARBOR: {    // 收到本地 slave 的命令
            harbor_command(h, msg,sz,session,source);
        default: {        // 需要發送消息到遠端 harbor 
            // remote message out
            const struct remote_message *rmsg = msg;
            if (rmsg->destination.handle == 0) {    // 如果數字地址爲0 即說明採用的是字符串地址
                if (remote_send_name(h, source , rmsg->destination.name, type, session, rmsg->message, rmsg->sz)) {
                    return 0;

可以看到 harbor 服務的消息處理主要分爲三大類:

  • 從網絡過來的數據包(PTYPE_SOCKET, 一般是兩個節點之間的harbor通信)
  • 本地發送的請求(PTYPE_HARBOR, 一般是 cslave 服務的請求)
  • 需要發送到另一個節點的消息(default, 這個一般是一個節點的服務要調用 skynet.send/skynet.call 發送消息到另外一個節點)

cmaster服務的工作

skynet.start(function()
    -- 得到中心節點的地址
    local master_addr = skynet.getenv "standalone"
    skynet.error("master listen socket " .. tostring(master_addr))

    -- 監聽中心節點
    local fd = socket.listen(master_addr)

    -- 調用 socket.start 正式開始監聽
    socket.start(fd , function(id, addr)
        -- 如果有遠端連接過來,會調用此函數,這裏是有 slave 連接過來了會調用此函數
        skynet.error("connect from " .. addr .. " " .. id)

        -- 啓動數據傳輸
        socket.start(id)

        -- 調用 handshake 在中心節點記錄下這個 slave,並通知其他slave說這個slave連接上來了,讓別的slave都去連接這個新的slave)
        local ok, slave, slave_addr = pcall(handshake, id)
        if ok then
            -- 監控 slave 的協程,其實就是對處理 slave 發過來的消息
            skynet.fork(monitor_slave, slave, slave_addr)
        else
            skynet.error(string.format("disconnect fd = %d, error = %s", id, slave))
            socket.close(id)
        end
    end)
end)

從上面可以看到, cmaster 的主要工作爲:

  • 監聽配置文件中 standalone 指定的地址,以便讓其他節點連接上來
  • 如果有其他 slave 節點連接上來了,記錄下這個 slave,並且告訴其他的 slave 節點
  • 調用 monitor_slave 函數接收並處理從 slave 節點過來的網路包

cslave服務的工作

skynet.start(function()
    -- 得到中心節點的地址
    local master_addr = skynet.getenv "master"

    -- 得到 harbor id
    local harbor_id = tonumber(skynet.getenv "harbor")

    -- 得到本節點的地址
    local slave_address = assert(skynet.getenv "address")

    -- 監聽本節點
    local slave_fd = socket.listen(slave_address)
    skynet.error("slave connect to master " .. tostring(master_addr))

    -- 連接 master 節點
    local master_fd = assert(socket.open(master_addr), "Can't connect to master")

    -- 註冊消息處理函數,用於處理本地請求
    skynet.dispatch("lua", function (_,_,command,...)
        local f = assert(harbor[command])
        f(master_fd, ...)
    end)

    skynet.dispatch("text", monitor_harbor(master_fd))

    -- 啓動 harbor 服務
    harbor_service = assert(skynet.launch("harbor", harbor_id, skynet.self()))

    -- 發送一個 "H" 消息,告訴master:hi, gay!我連接上來了,並且告訴 master:harbor id 和 節點地址
    local hs_message = pack_package("H", harbor_id, slave_address)
    socket.write(master_fd, hs_message)

    -- 遠端節點會告訴你有多少個節點已經連接上來了,待會他們會來連接你的
    local t, n = read_package(master_fd)
    assert(t == "W" and type(n) == "number", "slave shakehand failed")
    skynet.error(string.format("Waiting for %d harbors", n))

    -- 開闢一個協程,用於處理中心節點過來的網絡包
    skynet.fork(monitor_master, master_fd)
    if n > 0 then
        local co = coroutine.running()
        socket.start(slave_fd, function(fd, addr)
            skynet.error(string.format("New connection (fd = %d, %s)",fd, addr))

            -- 與已連接的老前輩slave 一一建立連接
            if pcall(accept_slave,fd) then
                local s = 0
                for k,v in pairs(slaves) do
                    s = s + 1
                end
                if s >= n then
                    skynet.wakeup(co)
                end
            end
        end)
        skynet.wait()
    end
    socket.close(slave_fd)
    skynet.error("Shakehand ready")
    skynet.fork(ready)
end)

從上面代碼看到,slave 還比 master 節點要複雜?是的。master節點只需要協調slave節點的工作即可了。

slave節點不僅要連接master,還要連接其他多個slave,還要與harbor服務通信,還要處理本節點其他服務的請求,詳細工作表述如下:

  • 監聽本節點的地址
  • 連接master節點並註冊"lua" "text"類型的消息處理函數,並啓動 harbor 服務,其中monitor_harbor是用來處理本地harbor服務發來的消息的
  • 告訴 master我連接上來了,並告訴 master 我是誰(以便master告訴其他的slave,讓其他的slave來連接(通過向老的slave發送"C")),master收到後告訴你有多少個節點已連接了
  • 開闢一個協程:monitor_master 來處理中新節點的消息
  • 老的slave收到master的"C"的網路包(在 monitor_master 函數中收到),調用 connect_slave 函數去連接新上來的 slave
  • 還是在 connect_slave 函數中,老的slave連接新的slave成功的後,老的slave調用socket.abandon(fd)後調用skynet.send(harbor_service, "harbor", string.format("S %d %d",fd,slave_id))給harbor服務發送一個 "S"
  • 老的slave的harbor服務收到"S"後,調用skynet_socket_start(h->ctx, fd);(對應上面的socket.abandon(fd)),並調用 handshake 函數將自己的 harbor id 發給對端新的 slave
  • 新的 slave 這時還在執行 accept_slave 函數,收取到老的slave的 harbor id,然後調用socket.abandon(fd)並調用skynet.send(harbor_service, "harbor", string.format("A %d %d", fd, id))給 harbor 服務發送一個"A xxx xxx"
  • harbor 收到 "A xx xx" 後("harbor"類型),調用skynet_socket_start(h->ctx, fd);(對應上一步的socket.abandon(fd)),並調用 handshake 函數將自己的 harbor id 發給老的slave,
  • 需要注意的是:這時候兩端的harbor收到 "A" 對方的 "S"後,主動連接的一方的slave->status = STATUS_HANDSHAKE;,被動連接的一方slave->status = STATUS_HEADER;,harbor服務會執行到 push_socket_data 中去,這時候連接建立完成。

到這裏我們可以得出一個結論:master與slave網絡通信處理一直是在各自的cmaster/cslave服務中,slave與slave的網絡通知在連接準備階段是在cslave中處理,在連接準備完成後,slave與slave的交互全部直接通過各自節點的harbor服務

多節點字符串地址的註冊

skynet爲服務註冊一個字符串地址,主要接口有兩個:skynet.registerskynet.name,這兩個接口都會調用一個共同的函數:globalname 可以看到,globalname首先判斷這個字符串是不是 '.' 開頭的,如果是,就註冊一個僅僅本節點可見的字符串地址,如果不是,則調用harbor.globalname(name, handle)註冊一個全skynet網絡可見的字符串地址。

harbor.globalname(name, handle)
    skynet.send(".cslave", "lua", "REGISTER", name, handle)
        function harbor.REGISTER(fd, name, handle)
            globalname[name] = handle    -- 在 slave 服務中緩存住這個全節點有效的名字
            response_name(name)            -- 檢查是否有此名字查詢的請求阻塞在這裏,如果有:返回
            socket.write(fd, pack_package("R", name, handle))--發消息給master說:自己要註冊這個名字,然後由 master 將此請求轉發給所有 slave
            skynet.redirect(harbor_service, handle, "harbor", 0, "N " .. name)    -- 發消息給 harbor 服務說:我註冊這個名字,以便被查找

master服務收到"R"的處理如下(向所有的 slave 節點廣播 'N' 命令):

local function dispatch_slave(fd)
    local t, name, address = read_package(fd)
    if t == 'R' then        -- 註冊全局名字
        -- register name
        assert(type(address)=="number", "Invalid request")
        if not global_name[name] then
            global_name[name] = address
        end
        local message = pack_package("N", name, address)
        for k,v in pairs(slave_node) do
            socket.write(v.fd, message)    -- 向所有的 slave 節點廣播 'N' 命令
        end

cslave服務收到遠端服務的"N "的處理如下(緩存住這個地址,然後向harbor服務發送一個'N'):

elseif t == 'N' then    -- 收到master的從另外 slave 過來的註冊新名字的通知
    globalname[id_name] = address    -- 緩存住全局名字
    response_name(id_name)            -- 用於此名字查詢的被阻塞請求結果的返回
    if connect_queue == nil then    -- 如果已經準備好了,就給harbor服務發消息,讓harbor服務記錄下這個地址
        skynet.redirect(harbor_service, address, "harbor", 0, "N " .. id_name)
    end

harbor服務收到本地服務的"N "的處理如下:

case 'N' : {    // 新命名全局名字
    if (s <=0 || s>= GLOBALNAME_LENGTH) {   -- 不能超過16個字符
        skynet_error(h->ctx, "Invalid global name %s", name);
        return;
    }
    update_name(h, rn.name, rn.handle);
        struct keyvalue * node = hash_search(h->map, name);
        if (node == NULL) {
            node = hash_insert(h->map, name);

如果這個名字不存在,就會調用 hash_insert 在harbor服務裏將這個名字儲存起來。

上面幾個流程的總結如下:

  1. 某服務調起 harbor.REGISTER 請求,請求發給 slave 服務, slave 本身緩存住這個請求在 globalname
  2. 寫一個 'R' 的命令給 master , 並且寫一個 'N' 命令給此節點的 harbor 服務
  3. master 收到這個 'R' 命令, 緩存住名字在 global_name 中, 並且向所有的 slave 節點廣播一個 'N' 命令
  4. 其他的 slave 節點的 harbor 服務會收到 'N' 命令,並向 harbor 服務寫一個 'N'命令

如果A節點的A服務發起了一個註冊名字的請求,那麼等上面的流程走完以後,有以下服務知道A節點A服務的地址

  • A節點的 slave 服務
  • master 服務
  • A節點的 harbor 服務
  • 其他節點的 harbor 服務

查詢一個全局字符串地址

查詢一個全局的字符串地址的接口爲:harbor.queryname

function harbor.queryname(name)
    return skynet.call(".cslave", "lua", "QUERYNAME", name)
    function harbor.QUERYNAME(fd, name)
        if name:byte() == 46 then    -- "." , local name 如果是本節點的服務名字,就直接返回地址
            skynet.ret(skynet.pack(skynet.localname(name)))
        local result = globalname[name]    -- 如果已經緩存過(是此節點的服務),也直接返回
        if result then
            skynet.ret(skynet.pack(result))
            return
        end
    local queue = queryname[name]
        if queue == nil then    -- 如果爲空 說明此名字還沒查詢過
            socket.write(fd, pack_package("Q", name))
            queue = { skynet.response() }
            queryname[name] = queue
        else     -- 如果不爲空 說明此名字已經查詢過 但是由於某種原因還沒返回(還沒註冊、slave還沒連接上來) 將其加入隊列 等註冊上來後再返回
            table.insert(queue, skynet.response())
        end

可見查詢時會向 master 發送一個"Q"命令,slave本身會並創建一個閉包隊列,master節點收到'Q'的處理:

elseif t == 'Q' then
    -- query name
    local address = global_name[name]
    if address then
        socket.write(fd, pack_package("N", name, address))

slave收到"N"後(這裏主要看response_name):

elseif t == 'N' then    -- 收到master的從另外 slave 過來的註冊新名字的通知
    globalname[id_name] = address    -- 緩存住全局名字
    response_name(id_name)            -- 用於此名字查詢的被阻塞請求結果的返回
        local address = globalname[name]
        if queryname[name] then
            local tmp = queryname[name]
            queryname[name] = nil
            for _,resp in ipairs(tmp) do
                resp(true, address)
            end
        end
    if connect_queue == nil then    -- 如果已經準備好了,就給harbor服務發消息,讓harbor服務記錄下這個地址
        skynet.redirect(harbor_service, address, "harbor", 0, "N " .. id_name)
    end

上面的操作是將請求時的閉包隊列一一取出來,發送喚醒這個閉包隊列,以便查詢的一方收到結果,至此此次查詢結束。查詢全局字符串地址的流程總結爲:

  1. 調起 harbor.QUERYNAME 請求, 發給 slave 服務
  2. 如果確實是一個非本節點的全局名字服務,會有兩種情況:
    • 已經查詢過,直接返回;
    • 還沒有查詢過,如果此查詢隊列爲空:發送一個 'Q' 命令給 master 服務 創建查詢隊列,如果已有查詢隊列:將其加入查詢隊列即可,master 服務收到 'Q' 後:看有沒有此名字的服務註冊上來過,如果有: 發送一個 'N' 命令給這個 slave, slave 收到這個 'N' 命令後 緩存主全局名字,並且給查詢的服務返回相應的消息 並且給 harbor 服務發一個 'N' 命令(讓harbor服務記錄下這個地址)

從skynet.send函數看多節點模式消息的發送

我們知道,skynet.send 與 skynet.call都是可以直接發送到不同節點的地址的,這是如何實現的呢?

function skynet.send(addr, typename, ...)
    return c.send(addr, p.id, 0 , p.pack(...))
        static int lsend(lua_State *L)
            if (dest_string) {    //如果是字符串地址
                //skynet_sendname 最終還是會調用 skynet_send
                session = skynet_sendname(context, 0, dest_string, type, session , msg, len);
            } else {    //如果是數字地址
                session = skynet_send(context, 0, dest, type, session , msg, len);    
            }

先看數字地址的情況

session = skynet_send(context, 0, dest, type, session , msg, len);    
    int skynet_send(struct skynet_context * context, uint32_t source, uint32_t destination , int type, int session, void * data, size_t sz) 
        if (skynet_harbor_message_isremote(destination)) { //如果目的地址不是本節點的(通過地址的高八位來判斷)
            skynet_harbor_send(rmsg, source, session);
                rmsg->destination.handle = destination;
                skynet_context_send(REMOTE, rmsg, sizeof(*rmsg) , source, type , session);    // 發送消息到 harbor 服務
                    skynet_mq_push(ctx->queue, &smsg);

通過高八位判斷是不是非本節點的地址,如果非本節點,將消息壓入到harbor服務的消息隊列。harbor服務會收到處理這個消息:

default: {        // 需要發送消息到遠端 harbor 
    // remote message out
    const struct remote_message *rmsg = msg;
    if (rmsg->destination.handle == 0) {    // 如果數字地址爲0 即說明採用的是字符串地址
        if (remote_send_name(h, source , rmsg->destination.name, type, session, rmsg->message, rmsg->sz)) {
            return 0;
        }
    } else {
        if (remote_send_handle(h, source , rmsg->destination.handle, type, session, rmsg->message, rmsg->sz)) {
            return 0;
        }
    }

由於是數字地址,所以會調用remote_send_handle(只考慮非本節點的)

if (remote_send_handle(h, source , rmsg->destination.handle, type, session, rmsg->message, rmsg->sz))
    send_remote(context, s->fd, msg,sz,&cookie);
        skynet_socket_send(ctx, fd, sendbuf, sz_header+4);    // 在這裏發送一個 "D" 給本地管道,本地管道收到後 封裝成網絡包發出去
            int64_t wsz = socket_server_send(SOCKET_SERVER, id, buffer, sz);
                send_request(ss, &request, 'D', sizeof(request.u.send));

遠端harbor服務收到這個消息:

case PTYPE_SOCKET: {        // 收到遠端 harbor 的消息
    const struct skynet_socket_message * message = msg;
    switch(message->type) {
    case SKYNET_SOCKET_TYPE_DATA:
        push_socket_data(h, message);
            // go though
            case STATUS_CONTENT: {
                int need = s->length - s->read;
                if (size < need) {
                    memcpy(s->recv_buffer + s->read, buffer, size);
                    s->read += size;
                    return;
                }
                memcpy(s->recv_buffer + s->read, buffer, need);
                // 分發到對應的服務上去
                forward_local_messsage(h, s->recv_buffer, s->length);

再看字符串地址的情況

session = skynet_sendname(context, 0, dest_string, type, session , msg, len);
    copy_name(rmsg->destination.name, addr);
    rmsg->destination.handle = 0;
    skynet_harbor_send(rmsg, source, session);  -- 會往harbor服務壓入一條消息(和前面的數字地址地址一樣)

harbor服務收到消息:

if (rmsg->destination.handle == 0) {    // 如果數字地址爲0 即說明採用的是字符串地址
    if (remote_send_name(h, source , rmsg->destination.name, type, session, rmsg->message, rmsg->sz)) {
        if (node->value == 0) {        // 如果harbor服務沒有緩存這個字符串地址(可能是註冊字符串地址的'N'命令還沒發過來)
            if (node->queue == NULL) {
                node->queue = new_queue();
            }
            struct remote_message_header header;
            header.source = source;
            header.destination = type << HANDLE_REMOTE_SHIFT;
            header.session = (uint32_t)session;
            push_queue(node->queue, (void *)msg, sz, &header);    // 把這個消息加入到隊列中,等待後續處理(將來收到'Q'命令的響應會pop_queue,然後繼續將消息轉發出去)
            char query[2+GLOBALNAME_LENGTH+1] = "Q ";    
            query[2+GLOBALNAME_LENGTH] = 0;
            memcpy(query+2, name, GLOBALNAME_LENGTH);
            skynet_send(h->ctx, 0, h->slave, PTYPE_TEXT, 0, query, strlen(query)); // 那麼發送一個"Q"給 cslave 服務
            return 1;
        } else {
            return remote_send_handle(h, source, node->value, type, session, msg, sz);

可以看出harbor服務會根據 rmsg->destination.handle 是否爲0來區分是一個字符串地址還是數字地址。遠端harbor服務收到這個包,會根據 rmsg->destination 來找到自己節點的那個服務然後轉發給那個服務。

所以,發消息給對端另外節點的流程是:

  • 看是字符串地址還是數字地址,如果是數字地址

  •   1. 發送消息到harbor,harbor服務會給管道發送一個'D'的命令,管道收到這個'D'命令,將消息通過網絡發到對端的harbor服務
      2. 對端harbor服務收到後,根據地址將消息壓入到對應服務的消息隊列
  • 如果是字符串地址

  •  1. 發送消息到harbor,harbor服務會給管道發送一個'D'的命令,管道收到這個'D'命令,將消息通過網絡發到對端的harbor服務
      2. 根據`rmsg->destination.handle`檢測到是字符串地址,看本地緩存有沒有此字符串地址,如果沒有:首先將此次請求加入到隊列中,然後發送消息給本節點的cslave服務,本地的cslave收到後會將地址返回給harbor服務(或通過cmaster轉發回去),當重新收到這個地址時,從隊列中取出,將消息壓入到相應的服務中去。
      3. 如果本地緩存有此字符串地址,直接將此字符串地址對應的數字地址取出,然後往對應的服務壓入消息

skynet.uniqueservice的實現

skynet.uniqueservice 函數的第一參數若爲true,那麼就會創建一個全skynet網絡都有效的服務。

function skynet.uniqueservice(global, ...)  -- .service 爲 service_mgr
    if global == true then
        return assert(skynet.call(".service", "lua", "GLAUNCH", ...))
    else
        return assert(skynet.call(".service", "lua", "LAUNCH", global, ...))
    end
end

先看global爲字符串的情況(不考慮snax)

return assert(skynet.call(".service", "lua", "LAUNCH", global, ...))
    return waitfor(service_name, skynet.newservice, realname, subname, ...)
        local function waitfor(name , func, ...)
            local s = service[name]
            if type(s) == "number" then    -- 如果已經有了,就直接返回
                return s
            end
            local co = coroutine.running()

            if s == nil then
                s = {}
                service[name] = s
            elseif type(s) == "string" then
                error(s)
            end

            assert(type(s) == "table")

            if not s.launch and func then    -- 如果是首次調用,可以直接返回
                s.launch = true
                return request(name, func, ...)
                local function request(name, func, ...)    -- 這裏的 func 爲 skynet.newservice
                    local ok, handle = pcall(func, ...)
                    local s = service[name]
                    assert(type(s) == "table")
                    if ok then
                        service[name] = handle
                    else
                        service[name] = tostring(handle)
                    end

                    for _,v in ipairs(s) do        -- 喚醒阻塞的協程
                        skynet.wakeup(v)
                    end

                    if ok then
                        return handle
            end

            table.insert(s, co)                -- 後續的調用都需要阻塞在這裏
            skynet.wait()
            s = service[name]
            if type(s) == "string" then
                error(s)
            end
            assert(type(s) == "number")
            return s
        end

可以看出如果global不爲ture,則認爲它是一個字符串,如果是首次針對此字符串調用 skynet.uniqueservice ,那麼會調用 skynet.newservice 創建一個服務並返回地址

如果不是首次調用並且首次調用還沒有完全完成,那麼會將當前協程插入到一個協程隊列中,然後 skynet.wait 等待首次調用完成後喚醒它

如果不是首次調用並且首次調用已經完全完成了,那麼會直接返回一個地址(首次調用完成後創建的服務的地址)。

再看global爲true的情況(不考慮snax)

skynet.start中可以看到,會針對 standalone 不同註冊不同的消息處理函數

if skynet.getenv "standalone" then     -- 主節點,單節點模式
    skynet.register("SERVICE")      -- 只有主節點中會註冊
    register_global()
else                                -- 多節點模式中的非主節點
    register_local()
end

這裏分三種情況,之前看 bootstrap 初始化時已經知道(注意在多節點模式中只有主節點中才會註冊一個"SERVICE"名字服務):

  • 單節點模式,standalone = true
  • 多節點模式中的主節點, standalone = "ip:port"
  • 多節點模式中的非主節點, standalone = nil

只看多節點模式,首先是主節點中,如果要創建一個全skynet網絡有效的服務。

return assert(skynet.call(".service", "lua", "GLAUNCH", ...))
    function cmd.GLAUNCH(name, ...) --local function register_global()
        return cmd.LAUNCH(global_name, ...)

後面就和創建本地服務一樣,可以看出,在主節點中,就只是在主節點中創建了一個本地服務。

再看非主節點:

return assert(skynet.call(".service", "lua", "GLAUNCH", ...))
    function cmd.GLAUNCH(...)       -- local function register_local()
        return waitfor_remote("LAUNCH", ...)
            return waitfor(local_name, skynet.call, "SERVICE", "lua", cmd, global_name, ...)
            -- 注意此時的 func變爲 skynet.call 了, cmd爲 "LAUNCH"

可以看到會向 "SERVICE" 服務發送一個 "LAUNCH" 消息,然後再看主節點中的 service_mgr 服務收到這個消息怎麼處理的:

function cmd.LAUNCH(service_name, subname, ...)
    return waitfor(service_name, skynet.newservice, realname, subname, ...)

可以看到會在本地創建一個服務,創建完成後會返回給遠端的非主節點的 service_mgr 服務,並儲存這個服務的地址,以便後續調用

順便一提 skynet.queryservice 對應 skynet.uniqueservice ,如果第一個參數爲true,那麼會阻塞的等待某個服務創建好才返回(當然服務如果已經存在就直接返回了),這沒啥好說的,都體現在 waitfor 函數裏面了。

master/slave模式的優劣

在我個人看來,master/slave最大的缺點在於不能很好的處理某個節點異常斷開的情況。

但是相比於cluster,它可以依靠一條socket連接便可以在雙方自由通信。

所以"官方"建議的是在不跨機房的機器中使用master/slave,在不同機房中或者跨互聯網網絡中使用cluster。(我覺得即便是不跨機房中,也要考慮異常斷開情況,當然skynet有提供監控這種異常的手段,但是不可避免的還有一些額外工作要做。)

Permanent link of this article:http://nulls.cc/post/skynet_srccode_analysis08_master_slave

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章