ZeroMQ学习笔记(3)——高级请求-答应模式

第三章 高级请求-答应模式

一、请求-答应机制
1.简单的应答封包
简单的REP-REQ模式两个套接字间传递的内容是一个空帧(空的分隔符帧)加消息帧如图1。

在这里插入图片描述

图1 简单封包的消息

2.扩展的应答封包
在第二章中为了平衡多个REQ和多个REP,插入了代理(ROUTER-DEALER),插入代理的数量可以是任意多的,这样能够很好解决客户端和服务器之间的各种各样的变动如图2。

在这里插入图片描述

图2 扩展请求-答应模式

ROUTER套接字会跟踪它具有的每一个连接,在所接受的每个消息前面加一个随机身份(也称作地址address,是一个二进制字符串),若有三个REQ套接字连接到ROUTER套接字,他会创建三个随机身份,每个REQ套接字都有一个身份。
假设REQ具有身份02,ROUTER套件字维护了一个散列表,可以用它搜索02并找到该REQ套件字的TCP连接,当从ROUTER套件字收到消息时,会得到三个帧见图3。
在这里插入图片描述

图3 带有一个地址的请求
代理的循环核心是从一个套件字读取之后写入另一个套件字,所以最终是在DEALER套件字上把这三个帧发送给REP。
在返回路径中,REP套接字使用它保存的封包包装消息,并且把这三帧应答消息发送给DEALER套接字见图4。
在这里插入图片描述

图4 带有一个地址的应答
DEALER读取这三个帧并发送给ROUTER,ROUTER取得第一帧并查找该连接,若找到了对应于该身份的连接再把剩下的两帧发送到线路中如图5。
在这里插入图片描述

图5 最简封包的应答
REQ套接字收到该消息后检查第一帧是否是空分隔符帧之后会丢弃该帧,并将World传递给调用的应用程序。
注:ROUTER套接字并不关心整个封包与空分隔符它只关心身份帧,这样就可以辨别出来将消息发送到那个链接去了。
二、请求-答应组合
有四个请求-响应套接字(REQ、REP、DEALER和ROUTER),每个套接字都有特定的行为。
1、详述四种套接字类型
REQ发送消息时会在消息顶部插入一个空分隔符,REQ套接字是同步的,总是先发送一个请求然后等待一个应答。
REP套接字是同步的,它读取和保存所有的身份帧包括空分隔符,然后将接下来的帧或多帧传给调用者。
DEALER套接字对应封包一无所知,它是异步的,它会将消息分发给已连接的节点,并且对所有连接收到的消息公平地进行排队。DEALER的作用就像是PUSH和PULL的结合。
ROUTER套接字对应封包一无所知,它是异步的,它为它的连接创建身份作为任何接收到的消息的第一帧传递给调用者,反之,当调用者发送一条消息的时候,也会使用第一个消息帧作为身份来查找要发送到的连接。
2、REQ到REP组合
在前两章中详细介绍了该组合,但有一个重要的方面需要注意:REQ的客户端必须初始化消息流。一个REP服务器不能跟一个没有先发出请求的REQ客户端交流。(从技术上讲它甚至是不可能的,如果这么做,API将返回一个EFSM的错误)。
3、DEALER到REP组合
将REQ客户端替换为DEALER,可以与多个REP服务器交流的异步客户端。当使用DEALER跟一个REP套接字交流时,必须准确地模拟REQ套接字会发送的封包,否则REP套接字将认为该消息无效而丢弃它。因此,要发送消息,需要采取以下步骤:
1.发送一个设置了MORE标志的空消息帧。
2.发送消息正文。
而当收到一条消息时,应该:
1.接收第一帧,如果它不是空的,则丢弃整个消息。
2.接收下一帧并将其传递给应用程序。
4、REQ到ROUTER组合
同样可以用DEALER来替换REQ,也可以用ROUTER来取代REP。这给了一个可以同时与多个REQ客户端交流的异步服务器。如果使用ROUTER改写“Hello World”服务器,就能够并行处理任意数量的“Hello”请求。如在第2章的mtserver例子中所看到的。
可以用两种不同的方式使用ROUTER:
1.作为一个在前端和后端套接字之间切换消息的代理。
2.作为一个读取消息并作用于它的应用程序。
5、DEALER到ROUTER组合
可以用DEALER和ROUTER同时替换REQ和REP两者,以获得最强大的套接字组合,这是DEALER与ROUTER交流的组合。它提供了与异步服务器交流的异步客户端,双方都对消息格式拥有完全控制权。
6、DEALER到DEALER组合
如果DEALER只与一个节点交流,你也可以用DEALER替换REP。当将一个REP替换为DEALER时,你的worker会突然成为完全异步的,它发回任意数量的应答。这么做的代价是,必须自己管理应答封包并正确地得到它们,否则不会工作。
7、ROUTER到ROUTER组合
N对N的连接,在第4章的自由模式中看到它的一个例子与在第8章有一个设计用于对等网络工作的替代DEALER到ROUTER的例子。
8、无效组合
REQ到REQ双方都希望通过发送消息给对方来启动,而只有在定好所有东西的情况下,使得两个节点在完全相同的时间交换消息,这种组合才能正常工作。
REQ到DEALER理论上可以做到,但如果添加了第二个REQ,因为DEALER没有办法将应答发送到原来的节点,所以它将被破坏。因此,REQ套接字会出错,或返回目的地的是另一个客户端的消息。
REP到REP双方都将等待对方发送第一条消息。
REP到ROUTER在理论上,ROUTER套接字可以启动对话并发送一个适当格式化的请求,如果它知道REP套接字已连接,并且它知道该连接的身份。尽管如此这也是混乱的,并没有在DEALER到ROUTER上面增加什么东西。
与无效组合相比,有效组合的共同点是,zmq套接字连接总是偏向于绑定到一个端点的节点,而另一个连接到该节点。此外哪一侧绑定和哪一侧连接并不是任意的而是要遵循自然模式。 “在那里”的一侧绑定:它会是一台服务器、一个代理、发布者、收集器。在“进来和出去”的一侧连接:它会是客户端或worker。
三、ROUTER套接字

  1. 身份和地址
    zmq中的身份(identity)概念专指ROUTER套接字,以及如何识别它们到其他套接字的连接。在应答封包中身份被用作地址。在大多数情况下身份是任意的,并且对ROUTER套接字是本地的:它是一个散列表中的查找键。一个节点可以有一个物理地址(如“tcp://192.168.55.117:5670”这样一个网络端点)或逻辑地址(一个UUID或电子邮件地址或其他唯一键)。
    使用ROUTER套接字与特定节点交流的应用程序可以将一个逻辑地址转换为身份,如果它已经建立了必要的散列表。因为当该节点发送消息时,ROUTER套接字只公布一个连接的身份(到一个特定的节点),只能应答该消息,而不能自发地跟一个节点交流。
    即使翻转这个规则,让ROUTER连接到节点,而不是等待节点连接到ROUTER,这也是成立的。但可以强制ROUTER套接字使用逻辑地址代替其身份。相关函数为zmq_setsockopt()
    连接到ROUTER套接字的两节点的简单示例,其中一个节点的逻辑地址“ LCY”:
    import zmq
    import zhelpers
    #创建上下文
    context = zmq.Context()
    #准备套接字
    sink = context.socket(zmq.ROUTER)
    sink.bind(“inproc://example”)

第一个套接字又zmq设置标识

anonymous = context.socket(zmq.REQ)
anonymous.connect(“inproc://example”)
anonymous.send(b"ROUTER uses a generated 3 byte identity")
zhelpers.dump(sink)
#第二个为自己设置标识LCY
identified = context.socket(zmq.REQ)
#接收身份是LCY的消息其中b标识byte格式
identified.setsockopt(zmq.IDENTITY, b"LCY")
identified.connect(“inproc://example”)
identified.send(b"ROUTER socket uses REQ’s socket identity")
zhelpers.dump(sink)
2、ROUTER错误处理
可以设置一个套接字捕获这个错误:ZMQ_ROUTER_MANDATORY。在ROUTER套接字上设置,在一个发送调用上提供不可路由的身份时,该套接字将发出EHOSTUNREACH错误信号。
四、负载均衡模式
举个例子,通常对于服务器来说,有很多相同的服务处理来自不同客户端的相同请求,但不能将所有请求都发送给同一个服务,应当实现负载均衡的模式实现均匀分配。
1.ROUTER-REQ
这是一个使用ROUTER代理与一组worker进行通信的负载均衡模式的示例:
import time
import random
from threading import Thread
import zmq
import zhelpers
NBR_WORKERS = 10

def worker_thread(context=None):
#准备上下文和套接字
context = context or zmq.Context.instance()
worker = context.socket(zmq.REQ)
# 使用字符串标识
zhelpers.set_id(worker)
worker.connect(“tcp://localhost:5671”)
total = 0
while True:
# 告诉路由器准备好了
worker.send(b"ready")
# 从路由器获取工作负载
workload = worker.recv()
finished = workload == b"END"
if finished:
print(“Processed: %d tasks” % total)
break
total += 1
#睡眠随机时间长度
time.sleep(0.1 * random.random())
context = zmq.Context.instance()
client = context.socket(zmq.ROUTER)
client.bind(“tcp://*:5671”)
for _ in range(NBR_WORKERS):
Thread(target=worker_thread).start()
for _ in range(NBR_WORKERS * 10):
# LRU worker正在队列中等待
address, empty, ready = client.recv_multipart()
#地址、空帧、真实消息
client.send_multipart([
address,
b’‘,
b’This is the workload’,
])

回复关闭并且报告结果

for _ in range(NBR_WORKERS):
address, empty, ready = client.recv_multipart()
#地址、空帧、真实消息
client.send_multipart([
address,
b’‘,
b’END’, ])

2.ROUTER-DEALER
ROUTER和多个DEALER相连接,用一种合适的算法来决定如何分发消息给DEALER。DEALER可以是一个黑洞(只负责处理消息,不给任何返回)、代理(将消息转发给其他节点)或是服务(会发送返回信息)。
任何可以使用REQ的地方都可以使用DEALER,但他们也存在差异。

  1. REQ套接字在发送任何数据之前发送一个空分隔符帧,而DEALER不会。
  2. REQ套接字会在收到应答之前,将只发送一个消息,而DEALER不会。
    与上一示例相同但将REQ套接字替换为DEALER套接字示例如下:
    import time
    import random
    from threading import Thread
    import zmq
    import zhelpers
    NBR_WORKERS = 10

def worker_thread(context=None):
context = context or zmq.Context.instance()
worker = context.socket(zmq.REQ)
# 使用字符串标识
zhelpers.set_id(worker)
worker.connect(“tcp://localhost:5671”)
total = 0
while True:
# 告诉路由器准备好了
worker.send(b"ready")
#从路由器获取工作负载
workload = worker.recv()
finished = workload == b"END"
if finished:
print(“Processed: %d tasks” % total)
break
total += 1
#睡眠随机时间长度
time.sleep(0.1 * random.random())
context = zmq.Context.instance()
client = context.socket(zmq.ROUTER)
client.bind(“tcp://*:5671”)
for _ in range(NBR_WORKERS):
Thread(target=worker_thread).start()
for _ in range(NBR_WORKERS * 10):
# LRU正在队列中等待
address, empty, ready = client.recv_multipart()
#地址、空帧、真实消息
client.send_multipart([
address,
b’‘,
b’This is the workload’,
])

回复关闭并且报告结果

for _ in range(NBR_WORKERS):
address, empty, ready = client.recv_multipart()
#地址、空帧、真实消息
client.send_multipart([
address,
b’‘,
b’END’,
])

3.负载均衡的消息代理
该代理执行以下操作:

  1. 接受来自一组客户端的连接。
  2. 接受来自一组worker的连接。
  3. 接受来自客户端的请求,并将其保留在单个队列中。
  4. 使用负载平衡模式将这些请求发送给worker。
  5. 收到worker的回复。
  6. 将这些答复发送回原始请求的客户端。

在这里插入图片描述

图6 负载均衡代理
REQ用于发送请求并得到回应,ROUTER作为一个可以并行处理很多的请求的套接字,它首先可以接收很多客户端的请求,然后,将请均匀公平的分配给worker们。
实际代码主要由三部分组成:client 部分、worker 部分、代理部分。
client部分:
#worker的子任务可以启用多个这样的多个线程
def client_task(ident):
socket = zmq.Context().socket(zmq.REQ)
#设定一个客户端的身份
socket.identity = u"Client-{}“.format(ident).encode(“ascii”)
#连接到前端段的router
socket.connect(“ipc://frontend.ipc”)
# 这个hello 相当于请求,希望得到word的回复
socket.send(b"HELLO”)
#收回答
reply = socket.recv()
#对返回的身份信息进行打印,然后对回答的请求,进行进行打印
print(“{}: {}”.format(socket.identity.decode(“ascii”),
reply.decode(“ascii”)))

worker 部分:
def worker_task(ident):
socket = zmq.Context().socket(zmq.REQ)
socket.identity = u"Worker-{}".format(ident).encode(“ascii”)
socket.connect(“ipc://backend.ipc”)

Tell broker we’re ready for work

#首先告诉代理已经准备好了
socket.send(b"READY")
#我就开始一直等待请求,然后处理请求。
while True:
#接受多部分消息
address, empty, request = socket.recv_multipart()
#打印是来自哪个客户端发来的请求
print(“{}: {}”.format(socket.identity.decode(“ascii”),
request.decode(“ascii”)))

发送多部分消息,然后返回给发给请求的那个客户端

    socket.send_multipart([address, b"", b"OK"])    

代理部分:
def main():
context = zmq.Context.instance()
#建立前端的套接字
frontend = context.socket(zmq.ROUTER)
frontend.bind(“ipc://frontend.ipc”)
#建立后端用来平衡worker的套接字
backend = context.socket(zmq.ROUTER)
backend.bind(“ipc://backend.ipc”)

开始后台任务,multiproces 支持进程,这个在windows上不行

def start(task, *args):  
    process = multiprocessing.Process(target=task, args=args)
    process.daemon = True
    process.start()
for i in range(NBR_CLIENTS):
    start(client_task, i)
for i in range(NBR_WORKERS):
    start(worker_task, i)

#有多少个clients
count = NBR_CLIENTS
workers = []
poller = zmq.Poller()
#将一个socket注册到poller中如果有活动了他们才会到poll当中去 poller.register(backend, zmq.POLLIN)
while True:
#关于dict函数:将传入的关键字变成字典 也就是socker
sockets = dict(poller.poll())
print(‘打印这次的workers’,workers)
if backend in sockets:
# 后端处理活动
request = backend.recv_multipart()
worker, empty, client = request[:3]
#如果workers当中现在不是空集 这个时候可以开放前段为req的请求
if not workers:
#轮询客户端
#只有当wokers为空的时候才来监控一下后端的socket是否有新的变化
poller.register(frontend, zmq.POLLIN)

worker 发过来的就一下这两种类型

        workers.append(worker)    

#需要注意worker发回来的信息 id ‘’ id ‘’ ‘ok’
if client != b"READY" and len(request) > 3:
#如果不是ready 信号,那id ‘’ ‘ok’
empty, reply = request[3:]
frontend.send_multipart([client, b"“, reply])
count -= 1
if not count:
break
if frontend in sockets:
# Get next client request, route to last-used worker
client, empty, request = frontend.recv_multipart()
#pop(0)的作用是返回当前数组中的第一个
worker = workers.pop(0)
backend.send_multipart([worker, b”“, client, b”", request])
if not workers:
#是否接收某个套接字所发过来的所有的消息
poller.unregister(frontend)
# 清理
backend.close()
frontend.close()
context.term()
在这段代码中设置客户端和worker套接字的身份,以使其更容易追踪消息帧。在实际工作中允许ROUTER套接字创建身份来进行连接。假设客户端的身份是“CLIENT”,worker的身份是“WORKER”。客户端应用程序发送一个包含“HELLO”的单个的帧见图7。
在这里插入图片描述

图7 客户端发送的消息

由于REQ套接字增加了其空分隔符帧而ROUTER套接字增加了其连接身份,代理从前端ROUTER套接字读出三帧:客户端地址、空分隔符帧,以及数据部分见图8。
在这里插入图片描述

图8 前端传入的消息
代理将其发送给worker,前面加上选定的worker地址,再加上一个额外的空分隔符见图9。
在这里插入图片描述

图9 发送到后端的消息
这种复杂的封包,首先由后端ROUTER套接字消化,从而消除了第一个帧。然后在worker的REQ套接字中移除空分隔符帧,并将其余的内容提供给worker应用程序见图10。
在这里插入图片描述

图10 传递给worker的消息
worker必须保存封包(包括空分隔符帧),可以对数据部分做必要的处理。REP套接字会自动完成,但REQ-ROUTER模式可以得到适当的负载均衡。
在返回路径的消息都与它们进来时是一样的,后端套接字传给代理的一个消息分为5个部分,代理发送给前端套接字的消息分为3个部分,而客户端在一个部分内得到1个消息。
负载均衡算法要求客户端和worker两者都使用REQ套接字,并且要求worler正确地存储和回放它们得到的消息的封包。该算法是:
1)创建一个轮询集始终轮询后端并仅当有一个或多个worker可用时才轮询前端。
2)使用无限超时轮询活动。
3)如果在后端有活动,要么有一个“准备就绪”的消息,要么有来自客户端的应答。在任一种情况下,在工作队列中存储worker地址(第一部分),并且如果其余的内容是客户端应答就通过前端将它发送回客户端。
4)如果在前端有活动,就取客户端的请求,给下一个worker(这是最后一次使用的),并将请求发送到后端。这意味着发送worker地址帧、空分隔符帧,然后是客户端请求的3个部分。
可以基于worker在其最初的“准备就绪”消息中提供的信息来重用和扩展负载均衡算法的变体。例如,worker可能会启动起来,执行一个性能自我测试然后告诉代理它们的速度有多快。那么代理就可以选择可用的、最快的worker,而不是最早启动的worker。
五、zmq高级API
高级API愿望清单:
1、自动处理套接字。如果有一种方法能在关闭上下文时自动关闭套接字,这将会是很棒的。
2、可移植的线程管理。每个不简单的zmq应用程序都使用线程,但POS1X线程是不可移植的。一个高级API应该将这隐藏在一个可移植层之下。
3、可移植的时钟。甚至让时间精确到毫秒或休眠几毫秒这些功能都是不可移植的。现实OMQ应用程序需要可移植的时钟,所以希望API应该提供它们。
4、取代zmq_poll()的反应器。轮询简单但笨拙。一个简单带有套接字的阅读器和计时器的反应器会节省大量的重复工作。
5、正确处理Ctrl-C。
1.czmq高级API
使用更高级的API改写的负载均衡代理示例:
from future import print_function
import threading
import time
import zmq
NBR_CLIENTS = 10
NBR_WORKERS = 3

def worker_thread(worker_url, context, i):
socket = context.socket(zmq.REQ)
# 设置worker 身份
socket.identity = (u"Worker-%d" % (i)).encode(‘ascii’)
socket.connect(worker_url)
# 回复准备就绪
socket.send(b"READY")
try:
while True:
address, empty, request = socket.recv_multipart()
print(“%s: %s\n” % (socket.identity.decode(‘ascii’),
request.decode(‘ascii’)), end=‘’)
socket.send_multipart([address, b’‘, b’OK’])
except zmq.ContextTerminated:
#上下文终止并退出
return
def client_thread(client_url, context, i)
socket = context.socket(zmq.REQ)
# 设置客户端身份
socket.identity = (u"Client-%d" % (i)).encode(‘ascii’)
socket.connect(client_url)
#发送请求,获取回复
socket.send(b"HELLO")
reply = socket.recv()
print(“%s: %s\n” % (socket.identity.decode(‘ascii’),
reply.decode(‘ascii’)), end=‘’)
def main():
url_worker = “inproc://workers”
url_client = “inproc://clients”
client_nbr = NBR_CLIENTS

准备上下文和套接字

context = zmq.Context()
frontend = context.socket(zmq.ROUTER)
frontend.bind(url_client)
backend = context.socket(zmq.ROUTER)
backend.bind(url_worker)
# 创建rkers和户端线程
for i in range(NBR_WORKERS):
thread = threading.Thread(target=worker_thread,
args=(url_worker, context, i, ))
thread.start()
for i in range(NBR_CLIENTS):
thread_c = threading.Thread(target=client_thread,
args=(url_client, context, i, ))
thread_c.start()

#始终轮询后端,仅当工作线程就绪时才轮询前端
#如果工作进程应答,则将工作进程排队为就绪并转发应答
#如有必要,发送给客户
#如果客户端请求,弹出下一个工作进程并向其发送请求
#可用worker队列
available_workers = 0
workers_list = []
#初始化轮询器
poller = zmq.Poller()

始终轮询后端上的进程活动

poller.register(backend, zmq.POLLIN)

当有可用的worker时才进行前端投票

poller.register(frontend, zmq.POLLIN)
while True:
    socks = dict(poller.poll())
    # 处理后端上的进程活动
    if (backend in socks and socks[backend] == zmq.POLLIN):
        # LRU路由的队列工作进程地址
        message = backend.recv_multipart()
        assert available_workers < NBR_WORKERS
        worker_addr = message[0]
        # 将worker添加回worker列表
        available_workers += 1
        workers_list.append(worker_addr)
        #第二帧为空
        empty = message[1]
        assert empty == b""
        # 第三帧已准备就绪为客户端回复地址
        client_addr = message[2]
        #如果客户机回复,则将其余部分发送回前端
        if client_addr != b'READY': 
            empty = message[3]
            assert empty == b""
            reply = message[4]
            frontend.send_multipart([client_addr, b"", reply])
            client_nbr -= 1
            if client_nbr == 0:
                break  # Exit after N messages
    # 只在有worker的情况下才在前端投票
    if available_workers > 0:

        if (frontend in socks and socks[frontend] == zmq.POLLIN):
            #现在获取下一个客户端请求,路由到worker

#客户端请求为[地址][空][请求]
[client_addr, empty, request] = frontend.recv_multipart()
assert empty == b""
# 退出队列并删除下一个工作地址
available_workers += -1
worker_id = workers_list.pop()
backend.send_multipart([worker_id, b"“,
client_addr, b”“, request])
#清理
frontend.close()
backend.close()
context.term()
if name == “main”:
main()
上例中还是使用了原生的zmq_poll()方法,也可以使用czmq提供的zloop反应器来实现,它可以做到:从任意套接字上获取消息,也就是说只要套接字有消息就可以触发函数;停止读取套接字上的消息;设置一个时钟,定时地读取消息。
下面是用zloop的负载均衡代理示例:
from future import print_function
import threading
import time
import zmq
from zmq.eventloop.ioloop import IOLoop
from zmq.eventloop.zmqstream import ZMQStream
NBR_CLIENTS = 10
NBR_WORKERS = 3
def worker_thread(worker_url, i):
context = zmq.Context.instance()
socket = context.socket(zmq.REQ)
#设置worker 身份
socket.identity = (u"Worker-%d” % (i)).encode(‘ascii’)
socket.connect(worker_url)
# 回复准备就绪
socket.send(b"READY")
try:
while True:
address, empty, request = socket.recv_multipart()

        print("%s: %s\n" % (socket.identity.decode('ascii'),
                            request.decode('ascii')), end='')
        socket.send_multipart([address, b'', b'OK'])
except zmq.ContextTerminated:
    # 上下文终止并退出
    return

def client_thread(client_url, i):
context = zmq.Context.instance()
socket = context.socket(zmq.REQ)
# 设置客户端身份
socket.identity = (u"Client-%d" % (i)).encode(‘ascii’)
socket.connect(client_url)

发送请求,获取回复

socket.send(b"HELLO")
reply = socket.recv()
print("%s: %s\n" % (socket.identity.decode('ascii'),
                    reply.decode('ascii')), end='')

class LRUQueue(object):
def init(self, backend_socket, frontend_socket):
self.available_workers = 0
self.workers = []
self.client_nbr = NBR_CLIENTS
self.backend = ZMQStream(backend_socket)
self.frontend = ZMQStream(frontend_socket)
self.backend.on_recv(self.handle_backend)
self.loop = IOLoop.instance()
def handle_backend(self, msg):
#LRU路由的队列工作进程地址
worker_addr, empty, client_addr = msg[:3]
assert self.available_workers < NBR_WORKERS
# 将worker添加回worker列表
self.available_workers += 1
self.workers.append(worker_addr)
#第二帧为空
assert empty == b""
#第三帧已准备就绪为客户端回复地址
#如果客户机回复,则将其余部分发送回前端
if client_addr != b"READY":
empty, reply = msg[3:]
assert empty == b""
self.frontend.send_multipart([client_addr, b’‘, reply])
self.client_nbr -= 1
if self.client_nbr == 0:
# N条消息后退出
self.loop.add_timeout(time.time() + 1, self.loop.stop)
if self.available_workers == 1:
# 在第一次接收时,开始接受前端消息
self.frontend.on_recv(self.handle_frontend)
def handle_frontend(self, msg):
#现在获取下一个客户端请求,路由到worker
#客户端请求为[地址][空][请求]
client_addr, empty, request = msg
assert empty == b""
# 退出队列并删除下一个工作地址
self.available_workers -= 1
worker_id = self.workers.pop()
self.backend.send_multipart([worker_id, b’‘, client_addr, b’', request])
if self.available_workers == 0:
# 停止接收,直到worker恢复工作
self.frontend.stop_on_recv()
def main():
url_worker = “ipc://backend.ipc”
url_client = “ipc://frontend.ipc”
#准备上下文和套接字
context = zmq.Context()
frontend = context.socket(zmq.ROUTER)
frontend.bind(url_client)
backend = context.socket(zmq.ROUTER)
backend.bind(url_worker)

创建workers和客户端线程

for i in range(NBR_WORKERS):
    thread = threading.Thread(target=worker_thread, args=(url_worker, i, ))
    thread.daemon = True
    thread.start()
for i in range(NBR_CLIENTS):
    thread_c = threading.Thread(target=client_thread,
                                args=(url_client, i, ))
    thread_c.daemon = True
    thread_c.start()
#使用套接字创建队列
queue = LRUQueue(backend, frontend)
# 启动反应器
IOLoop.instance().start()

if name == “main”:
main()
六、异步客户端/服务器模式(异步C/S结构)
在ROUTER-DEALER的例子中又1对N的用例,其中一个服务器异步地和多个worker进行通信的。倒置过来从而得到N对1的架构,实现多个client异步地和单个server进行通信如图11。
在这里插入图片描述

图11 异步客户端/服务器
工作原理是:
a) 客户端连接至服务器并发送请求;
b) 每一次收到请求,服务器会发送0至N个应答;
c) 客户端可以同时发送多个请求而不需要等待应答;
d) 服务器可以同时发送多个应答而不需要新的请求。
import zmq
import sys
import threading
import time
from random import randint, random
author = “Felipe Cruz felipecruz@loogica.net
license = “MIT/X11”

def tprint(msg):
sys.stdout.write(msg + ‘\n’)
sys.stdout.flush()
class ClientTask(threading.Thread):
def init(self, id):
self.id = id
threading.Thread.init (self)
def run(self):
#创建上下文
context = zmq.Context()
#准备DEALER套接字
socket = context.socket(zmq.DEALER)
#设置身份
identity = u’worker-%d’ % self.id
socket.identity = identity.encode(‘ascii’)
socket.connect(‘tcp://localhost:5570’)
print(‘客户端%s 启动% (identity))
poll = zmq.Poller()
poll.register(socket, zmq.POLLIN)
reqs = 0
while True:
reqs = reqs + 1
print(‘Req #%d sent…’ % (reqs))
socket.send_string(u’request #%d’ % (reqs))
for i in range(5):
sockets = dict(poll.poll(1000))
if socket in sockets:
msg = socket.recv()
tprint(‘客户端 %s 接收: %s’ % (identity, msg))

    socket.close()
    context.term()

class ServerTask(threading.Thread):
def init(self):
threading.Thread.init (self)
def run(self):
#创建上下文
context = zmq.Context()
#准备ROUTER套接字
frontend = context.socket(zmq.ROUTER)
frontend.bind(‘tcp://*:5570’)
#准备DEALER套接字
backend = context.socket(zmq.DEALER)
backend.bind(‘inproc://backend’)
workers = []
for i in range(5):
worker = ServerWorker(context)
worker.start()
workers.append(worker)
zmq.proxy(frontend, backend)
#清理
frontend.close()
backend.close()
context.term()

class ServerWorker(threading.Thread):
def init(self, context):
threading.Thread.init (self)
self.context = context
def run(self):
#为worker准备上下文和套接字
worker = self.context.socket(zmq.DEALER)
worker.connect(‘inproc://backend’)
tprint(‘Worker 开始’)
while True:
ident, msg = worker.recv_multipart()
tprint(‘Worker 接收%s 从 %s’ % (msg, ident))
replies = randint(0,4)
for i in range(replies):
time.sleep(1. / (randint(1,10)))
worker.send_multipart([ident, msg])
worker.close()
def main():
server = ServerTask()
#开启服务
server.start()
#三个客户端
for i in range(3):
client = ClientTask(i)
client.start()
server.join()
if name == “main”:
main()
该示例在一个进程中运行使用多线程模拟一个真正的多进程架构。当运行这个例子时会看到三个客户端(每个客户端都带有一个随机ID),输出它们从服务器获取的应答。每个客户端任务都会得到每个请求的零个或多个应答。
client每秒发送一次请求,并获得0到多条应答。通过zmq_poll()来实现,但不能只每秒poll一次,将不能及时处理应答。程序中每秒取100次,这样一来server端以此作为一种心跳(heartbeat),检测client是否在线。
server使用了一个worker池,每一个worker同步处理一条请求。可以使用内置的队列来搬运消息。
代码整体架构如图12所示。
在这里插入图片描述

图12 异步服务器
想要在异步的请求中保存好client的信息,有以下几点需要注意:
a) client需要发送心跳给server
b) 使用client的套接字标识来存储信息,这对瞬时和持久的套接字都有效;
c) 检测停止心跳的client,如两秒内没有收到某个client的心跳,就将保存的状态丢弃。
七、跨代理路由
1.单集群架构
worker和客户端是同步的并且客户端和worker不会彼此直接对话这使得无法动态地添加或删除节点。因此引入代理结构见图13集群结构。
在这里插入图片描述

图13 集群结构
2.扩展到多个集群架构
每个集群都有一组客户端和一组worker以及代理如图14所示。
在这里插入图片描述

图14 多个集群结构
一个集群的client和另一个集群的worker进行通信解决方案:
方案一(失败)worker同时和多个代理进行通信如图15所示:
在这里插入图片描述

图15 方案一:交叉连接的worker
它看起来是可行的。然而它并没有提供出来想要的东西,在这种方案中,worker将对两个代理同时显示“准备就绪”,所以可能一次得到两份工作,而其他worker仍保持空闲状态。看来这样的设计是失败的。
方案二(可行)连接各个代理如图16所示:
在这里插入图片描述

图16 方案二:连接各个代理
实际上,这仅仅是一个更复杂的路由算法:代理成为双方的分包商。这种设计还有其他一些好处:
a) 若只有一个集群,这种设计和原来没有区别
b) 对于不同的工作可以使用不同的消息流模式比如使用不同的网络链接。
c) 架构扩充也比较容易若有必要可添加一个超级代理来完成调度工作。
3.联邦模式和同伴模式
1)联邦模式
联邦模式:即代理充当其他代理的client和worker,通过将前端连接到其他代理的后端套接字见图17。
这将使两个代理都有简单的逻辑和相当好的机制:当没有客户端时就告诉另一个代理“准备就绪”,并接受一项工作。一个联邦代理在同一个时间将只能处理一个任务。的代理需要以完全异步的方式进行连接。
注:将套接字既绑定到一个端点又将其连接到其他端点,这是合法的。

在这里插入图片描述

图17 在联盟模式中交叉连接的代理
联邦模式下的代理一次只能处理一个请求,如果client和worker是严格同步的,那么代理中的其他空闲worker将分配不到任务。
联邦模式对某些应用来说是非常好的,比如面向服务架构(SOA)。它不适用于LRU算法和集群负载均衡。
2)同伴模式
同伴模式:代理之间知道彼此的存在,并使用一个特殊的信道进行通信。设有N个代理需要连接,每个代理则有N-1(除自己)个同伴,所有代理都使用相同格式的消息进行通信。
每个代理需要告知所有同伴自己有多少空闲的worker(用PUB-SUB套接字)。每个代理打开一个PUB套接字;同时又会打开一个SUB套接字。
每个代理需要以某种方式将任务交给其他代理并获取应答(用ROUTER-ROUTER套接字)。每个代理会使用两个ROUTER套接字,一个用于接收任务一个用于分发任务。
4.命名规范
为代理中的套接字(3个消息流,每个消息流有两个套接字)取名。
以下是平时使用的三个消息流:

  1. 本地(local)的请求-应答消息流实现代理和client、worker之间通信。
  2. 云端(cloud)的请求-应答消息流实现代理和其同伴的通信。
  3. 状态(state)流由代理和其同伴互相传递。
    使用以下的命名方式(见图18):
    localfe / localbe用于本地消息流;
    cloudfe / cloudbe用于云端消息流;
    statefe / statebe用于状态消息流。
    在这里插入图片描述

图18 安排代理套接字
5.状态流原型
a) 每个代理都需要有各自的标识,用以生成相应的端点名称。
b) 程序的核心是一个zmq_poll()循环,它会处理接收到消息,并发送自身的状态。如果每次收到消息都去发送自身状态,那消息就会过量了
c) 发送的状态消息包含两帧第一帧是代理自身地址第二帧是空闲的worker数。
d) 必须告知同伴代理自身的地址这样才能接收到请求。
e) 没有在SUB套接字上设置标识,否则就会在连接到同伴代理时获得过期的状态信息。
f) 没有在PUB套接字上设置阈值(HWM),因为订阅者是瞬时的。
注:如想要以较精确的周期来发送状态信息,可以新建一个线程将statebe套接字打开,然后由主线程将不规则的状态信息发送给子线程,再由子线程定时发布这些消息。
状态流见图19所示。
在这里插入图片描述

图19 状态流
代码如下:
import sys
import time
import random
import zmq
def main(myself, others):
print(“Hello, I am %s” % myself)
context = zmq.Context()
# 状态后端
statebe = context.socket(zmq.PUB)
# 状态前端
statefe = context.socket(zmq.SUB)
#作用是匹配所发布的任何的消息
statefe.setsockopt(zmq.SUBSCRIBE, b’‘)
#后端绑定要发布的位置
bind_address = u"ipc://%s-state.ipc" % myself
statebe.bind(bind_address)
for other in others:
statefe.connect(u"ipc://%s-state.ipc" % other)
time.sleep(1.0)
poller = zmq.Poller()
poller.register(statefe, zmq.POLLIN)
while True:
########## poll() 方案##########
socks = dict(poller.poll(1000))
#处理传入的状态消息
if socks.get(statefe) == zmq.POLLIN:
msg = statefe.recv_multipart()
print(’%s Received: %s’ % (myself, msg))
else:
#发送的地址和随机值
#worker可用性
msg = [bind_address, (u’%i’ % random.randrange(1, 10))]
msg = [ m.encode(‘ascii’) for m in msg]
statebe.send_multipart(msg)
##################################
######### select() 方案#########

pollin, pollout, pollerr = zmq.select([statefe], [], [], 1)

if pollin and pollin[0] == statefe:

# 处理传入的状态消息

msg = statefe.recv_multipart()

print ‘Received:’, msg

else:

#发送的地址和随机值

#worker可用性

msg = [bind_address, str(random.randrange(1, 10))]

statebe.send_multipart(msg)

##################################
if name == ‘main’:
if len(sys.argv) >= 2:
main(myself=sys.argv[1], others=sys.argv[2:])
else:
print(“Usage: peering.py <peer_1> … <peer_N>”)
sys.exit(1)
6.本地流和云端流原型
需要两个队列,一个用于存放来自本地客户端的请求一个用于存放来自云客户端的请求。其中一个办法是将消息从本地和云前端提取出来并把这些消息输送到各自的队列中。但这是一项毫无意义的工作因为OMQ套接字已经是队列了。因此采用OMQ套接字缓冲区作为队列。
这是本章前面的负载均衡代理中使用过的技术。仅当发送请求时,才从两个前端读取。且始终可以从后端读取,因为它们给了路由回去的应答。
所以主循环就变成了:
1、轮询后端的活动。当得到一个消息时,它可能会是来自一个worker的“准备就绪”,也可能是一个应答。如果它是一个应答就通过本地或云前端将它路由回去。
2、如果worker应答了,它就成为可用的所以将它排入队列并对它计数。
3、当存在可用的worker时,就从任一前端获取一个请求(如果有的话)并将它路由到一个本地worker或随机路由到云中的某个对等节点。
使用代理身份在代理之间路由消息。每个代理都有在命令行上提供的名字。只要这些名字不与zmq生成的用于客户端节点的UUID发生重叠就可以确定,应当将应答路由给客户端还是代理。
本地流和云端流的原型如图20所示。
在这里插入图片描述

图20 本地流和云端流
代码如下:
#示例程序使用了一个进程,这样可以让程序变得简单,
#每个线程都有自己的上下文对象,所以可以认为他们是多个进程。
import random
import sys
import threading
import time
import zmq
try:
raw_input
except NameError:
# Python 3
raw_input = input
NBR_CLIENTS = 10
NBR_WORKERS = 3
def tprint(msg):
sys.stdout.write(msg + ‘\n’)
sys.stdout.flush()
def client_task(name, i):
“”“使用REQ套接字的请求-应答客户端”“”
ctx = zmq.Context()
client = ctx.socket(zmq.REQ)
client.identity = (u"Client-%s-%s" % (name, i)).encode(‘ascii’)
client.connect(“ipc://%s-localfe.ipc” % name)
while True:
#发送请求,接收应答
client.send(b"HELLO")
try:
reply = client.recv()
except zmq.ZMQError:
#中断
return
tprint(“Client-%s: %s” % (i, reply))
time.sleep(1)
def worker_task(name, i):
“”" Worker使用REQ套接字进行LRU路由"“”
ctx = zmq.Context()
worker = ctx.socket(zmq.REQ)
worker.identity = (u"Worker-%s-%s" % (name, i)).encode(‘ascii’)
worker.connect(“ipc://%s-localbe.ipc” % name)
# 告知代理worker已就绪
worker.send(b"READY")
# 处理消息
while True:
try:
msg = worker.recv_multipart()
except zmq.ZMQError:
return
tprint(“Worker-%s: %s\n” % (i, msg))
msg[-1] = b"OK"
worker.send_multipart(msg)
def main(myself, peers):
print(“I: preparing broker at %s…” % myself)
#准备套接字和上下文
ctx = zmq.Context()
#将cloudfe绑定至端点
cloudfe = ctx.socket(zmq.ROUTER)
if not isinstance(myself, bytes):
ident = myself.encode(‘ascii’)
else:
ident = myself
cloudfe.identity = ident
cloudfe.bind(“ipc://%s-cloud.ipc” % myself)
# 将cloudbe连接至同伴代理的端点
cloudbe = ctx.socket(zmq.ROUTER)
cloudbe.identity = ident
for peer in peers:
tprint(“I: connecting to cloud frontend at %s” % peer)
cloudbe.connect(“ipc://%s-cloud.ipc” % peer)
if not isinstance(peers[0], bytes):
peers = [peer.encode(‘ascii’) for peer in peers]
# 建立本地端的前端等待client 连接和后端等待work连接
localfe = ctx.socket(zmq.ROUTER)
localfe.bind(“ipc://%s-localfe.ipc” % myself)
localbe = ctx.socket(zmq.ROUTER)
localbe.bind(“ipc://%s-localbe.ipc” % myself)
raw_input("启动所有代理时按Enter: ")
# 创建worker线程和客户端线程
for i in range(NBR_WORKERS):
thread = threading.Thread(target=worker_task, args=(myself, i))
thread.daemon = True
thread.start()
for i in range(NBR_CLIENTS):
thread_c = threading.Thread(target=client_task, args=(myself, i))
thread_c.daemon = True
thread_c.start()
# 有趣的部分
# -------------------------------------------------------------
# 请求-答复流
# 轮询后端并处理本地/云回复

当worker可用时,将localfe路由到本地或云

workers = []

# 设置轮询器
pollerbe = zmq.Poller()
pollerbe.register(localbe, zmq.POLLIN)
pollerbe.register(cloudbe, zmq.POLLIN)
pollerfe = zmq.Poller()
pollerfe.register(localfe, zmq.POLLIN)
pollerfe.register(cloudfe, zmq.POLLIN)
while True:
    # 如果没有worker,那就无限期地等待
    try:
        events = dict(pollerbe.poll(1000 if workers else None))
    except zmq.ZMQError:
        break  # interrupted
    # 处理本地worker的答复
    msg = None
    if localbe in events:
        msg = localbe.recv_multipart()
        (address, empty), msg = msg[:2], msg[2:]
        workers.append(address) 
        if msg[-1] == b'READY':
            msg = None
    elif cloudbe in events:
        msg = cloudbe.recv_multipart()
        (address, empty), msg = msg[:2], msg[2:]
    if msg is not None:
        address = msg[0]
        if address in peers:
            #如果它是指向代理的将回复路由到云端
            cloudfe.send_multipart(msg)
        else:
            localfe.send_multipart(msg)
    # 现在路由尽可能多的客户端请求
    while workers:
        events = dict(pollerfe.poll(0))
        reroutable = False
        if cloudfe in events:
            msg = cloudfe.recv_multipart()
            reroutable = False

#优先处理同伴代理的请求,避免资源耗尽
elif localfe in events:
msg = localfe.recv_multipart()
reroutable = True
else:
#没有请求
break
#将20%的请求发送给其他集群
if reroutable and peers and random.randint(0, 4) == 0:
# 随机地路由给同伴代理
msg = [random.choice(peers), b’‘] + msg
cloudbe.send_multipart(msg)
else:
msg = [workers.pop(0), b’'] + msg
localbe.send_multipart(msg)
if name == ‘main’:
if len(sys.argv) >= 2:
main(myself=sys.argv[1], peers=sys.argv[2:])
else:
print(“Usage: peering2.py [<peer_1> [… <peer_N>]]”)
sys.exit(1)
7.组装
将上述汇集为单个封装可以模拟任意多个集群的程序。
主任务以设置它所有套接字。本地前端与客户端交流而本地后端与worker交流。云前端会与对等代理交流就好像它们是客户端,而云后端也与对等代理交流就好像它们是worker。状态后端定期发布状态信息而状态前端订阅所有状态后端来收集这些信息。最后使用一个PULL监控套接字来收集来自任务的可打印的消息。
客户端的任务。它发出一个突发请求然后休眠几秒钟。这模拟零星的活动,如果许多客户端都马上活跃本地worker就会过载。客户端使用REQ套接字用于请求并将统计信息推送到监控套接字上。
worker任务,使用一个REQ套接字插入到负载均衡器。
主循环有两个部分。首先轮询worker和两个服务套接字(statefe和monitor)。如果没有准备就绪的worker那么查看传入的请求就是没有意义的。这些请求可以保留在其内部zmq队列中。
代码如下:
#虽然本示例在单个进程中运行,
#但这只是为了更容易启动和停止示例。
#每个线程都有自己的上下文,
#在概念上充当一个单独的进程。
import random
import sys
import threading
import time
import zmq
NBR_CLIENTS = 10
NBR_WORKERS = 5

def asbytes(obj):
s = str(obj)
if str is not bytes:
# Python 3
s = s.encode(‘ascii’)
return s
def client_task(name, i):
“”“使用REQ套接字的请求-应答客户端”“”
ctx = zmq.Context()
client = ctx.socket(zmq.REQ)
client.identity = (u"Client-%s-%s" % (name, i)).encode(‘ascii’)
client.connect(“ipc://%s-localfe.ipc” % name)
monitor = ctx.socket(zmq.PUSH)
monitor.connect(“ipc://%s-monitor.ipc” % name)
poller = zmq.Poller()
poller.register(client, zmq.POLLIN)
while True:
time.sleep(random.randint(0, 5))
for _ in range(random.randint(0, 15)):
# 使用随机十六进制ID发送请求
task_id = u"%04X" % random.randint(0, 10000)
client.send_string(task_id)
# 最多等待十秒
try:
events = dict(poller.poll(10000))
except zmq.ZMQError:
return # interrupted
if events:
reply = client.recv_string()
assert reply == task_id, “expected %s, got %s” % (task_id, reply)
monitor.send_string(reply)
else:
monitor.send_string(u"E: CLIENT EXIT - lost task %s" % task_id)
return

def worker_task(name, i):
“”" Worker使用REQ套接字进行LRU路由"“”
ctx = zmq.Context()
worker = ctx.socket(zmq.REQ)
worker.identity = (“Worker-%s-%s” % (name, i)).encode(‘ascii’)
worker.connect(“ipc://%s-localbe.ipc” % name)
# 准备就绪
worker.send(b"READY")
# 处理消息
while True:
try:
msg = worker.recv_multipart()
except zmq.ZMQError:
return
# Workers 忙了0或1秒(随机)
time.sleep(random.randint(0, 1))
worker.send_multipart(msg)
def main(myself, peers):
print(“I: preparing broker at %s…” % myself)
# 准备上下文和套接字
ctx = zmq.Context()
# 将cloudfe绑定至端点
cloudfe = ctx.socket(zmq.ROUTER)
cloudfe.setsockopt(zmq.IDENTITY, myself)
cloudfe.bind(“ipc://%s-cloud.ipc” % myself)
# 将statebe绑定至端点
statebe = ctx.socket(zmq.PUB)
statebe.bind(“ipc://%s-state.ipc” % myself)
# 将cloudbe连接至同伴代理的端点
cloudbe = ctx.socket(zmq.ROUTER)
statefe = ctx.socket(zmq.SUB)
statefe.setsockopt(zmq.SUBSCRIBE, b"")
cloudbe.setsockopt(zmq.IDENTITY, myself)
for peer in peers:
print(“I: connecting to cloud frontend at %s” % peer)
cloudbe.connect(“ipc://%s-cloud.ipc” % peer)
print(“I: connecting to state backend at %s” % peer)
statefe.connect(“ipc://%s-state.ipc” % peer)
# 准备本地前端和后端
localfe = ctx.socket(zmq.ROUTER)
localfe.bind(“ipc://%s-localfe.ipc” % myself)
localbe = ctx.socket(zmq.ROUTER)
localbe.bind(“ipc://%s-localbe.ipc” % myself)

# 准备监控套接字
monitor = ctx.socket(zmq.PULL)
monitor.bind("ipc://%s-monitor.ipc" % myself)
# raw_input("启动所有代理时按Enter:: ")
# 创建worker和client线程
for i in range(NBR_WORKERS):
    thread = threading.Thread(target=worker_task, args=(myself, i))
    thread.daemon = True
    thread.start()
for i in range(NBR_CLIENTS):
    thread_c = threading.Thread(target=client_task, args=(myself, i))
    thread_c.daemon = True
    thread_c.start()

# 有趣的部分
# -------------------------------------------------------------
# 发布-订阅消息流
# 轮询同伴代理的状态信息;
# 当自身状态改变时,对外广播消息。
# 请求-应答消息流
# 若本地有可用worker,则轮询获取本地或云端的请求;
# 将请求路由给本地worker或其他集群。
local_capacity = 0
cloud_capacity = 0
workers = []
#设置后端轮询器
pollerbe = zmq.Poller()
pollerbe.register(localbe, zmq.POLLIN)
pollerbe.register(cloudbe, zmq.POLLIN)
pollerbe.register(statefe, zmq.POLLIN)
pollerbe.register(monitor, zmq.POLLIN)
while True:
    # 如果没有可用的worker,则一直等待
    try:
        events = dict(pollerbe.poll(1000 if local_capacity else None))
    except zmq.ZMQError:
        break  # interrupted
    previous = local_capacity
    # 处理本地worker的应答
    msg = None
    if localbe in events:
        msg = localbe.recv_multipart()
        (address, empty), msg = msg[:2], msg[2:]
        workers.append(address)
        local_capacity += 1
        # 处理来自同伴代理的应答
        if msg[-1] == b'READY':
            msg = None
    elif cloudbe in events:
        msg = cloudbe.recv_multipart()
        (address, empty), msg = msg[:2], msg[2:]
        # 不需要使用同伴代理的地址
    if msg is not None:
        address = msg[0]
        if address in peers:
            # 如果应答消息中的地址是同伴代理的,则发送给它
            cloudfe.send_multipart(msg)
        else:
            #将应答路由给本地client
            localfe.send_multipart(msg)
    # 处理同伴代理的状态更新
    if statefe in events:
        peer, s = statefe.recv_multipart()
        cloud_capacity = int(s)
    # 处理监控消息
    if monitor in events:
        print(monitor.recv_string())
    # 现在路由尽可能多的客户端请求
    # 如果有本地容量,会同时轮询localfe和cloudfe
    # 如果只有云容量,只轮询localfe
    # 如果可以,将任何请求路由到本地,否则路由到云
    while local_capacity + cloud_capacity:
        secondary = zmq.Poller()
        secondary.register(localfe, zmq.POLLIN)
        if local_capacity:
            secondary.register(cloudfe, zmq.POLLIN)
        events = dict(secondary.poll(0))
        # 优先处理同伴代理的请求,避免资源耗尽
        if cloudfe in events:
            msg = cloudfe.recv_multipart()
        elif localfe in events:
            msg = localfe.recv_multipart()
        else:
            break  # No work, go back to backends
        if local_capacity:
            msg = [workers.pop(0), b''] + msg
            localbe.send_multipart(msg)
            local_capacity -= 1
        else:
            # 随机路由给同伴代理
            msg = [random.choice(peers), b''] + msg
            cloudbe.send_multipart(msg)
    if local_capacity != previous:
        statebe.send_multipart([myself, asbytes(local_capacity)])

if name == ‘main’:
if len(sys.argv) >= 2:
myself = asbytes(sys.argv[1])
main(myself, peers=[ asbytes(a) for a in sys.argv[2:] ])
else:
print(“Usage: peering3.py [<peer_1> [… <peer_N>]]”)
sys.exit(1)
client线程会检测并报告失败的请求,它们会轮询代理套接字,查看是否有应答,超时时间为10秒。
client线程不会自己打印信息,而是将消息PUSH给一个监控线程由它打印消息(日志)。
clinet会模拟多种负载情况让集群在不同的压力下工作,因此请求可能会在本地处理也有可能会发送至云端。集群中的client和worker数量、其他集群的数量,以及延迟时间,会左右这个结果。可以设置不同的参数来测试。
主循环中有两组轮询集合,事实上可以使用三个:信息流、后端、前端。因为之前例子中,若后端没有空闲的worker就没有必要轮询前端请求。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lcy~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值