我试图了解docker swarm如何进行负载平衡以及它如何影响套接字服务器的设计(因为服务器必须接受客户端连接才能获取用于返回服务结果的套接字对象) ,为此我创建了以下 echo 服务器:
# server.py
class Server:
def __init__(self, port=5050, header_size=64, encode_format='utf-8'):
port = port
host = "localhost"
self.addr = (host, port)
self.header_size = header_size
self.encode_format = encode_format
self.disconnect_message = 'DISCONNECT'
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind(self.addr)
self.id = np.random.randint(1024)
def do_stuff(self, conn, addr):
msg_length = conn.recv(self.header_size).decode(self.encode_format)
msg_length = int(msg_length)
# THIS PART WAS REMOVED TO KEEP THINGS SIMPLE
# message = conn.recv(msg_length) #.decode(self.encode_format)
# while len(message) < msg_length:
# packet = conn.recv(msg_length-len(message)) #.decode(self.encode_format)
# message += packet
print(f"[MSG] {msg_length} from {addr}")
self.send(conn, f"{msg_length} response from {self.id}")
return True
def send(self, conn, msg):
message = pickle.dumps(msg)
msg_length = len(message)
send_length = str(msg_length).encode(self.encode_format)
send_length += b' ' * (self.header_size - len(send_length))
# print("Sending response length")
conn.send(send_length)
# print("Sending response data")
# conn.send(message)
def handle_client(self, conn, addr):
print("-------------------------------------")
print(f"[CLIENT] new client {addr} connected.")
connected = True
while connected:
connected = self.do_stuff(conn, addr)
print(f"[CLIENT] {addr} has disconnected")
conn.close()
print(f"[Active CONNECTIONS] {threading.active_count() -2}")
def start(self):
self.server.listen()
print(f"[LISTENING] Server is listening on {self.addr[0]} port {self.addr[1]}")
print(f"[LISTENING] Server is listening on {self.addr[0]} port {self.addr[1]}")
try:
while True:
print("Listening . . .")
conn, addr = self.server.accept()
thread = threading.Thread(target=self.handle_client, args=(conn, addr))
thread.start()
print(f"[Active CONNECTIONS] {threading.active_count() -1}")
sleep(2)
except KeyboardInterrupt:
print("Interrupt signal received from Keyboard")
self.server.close()
print(f"[STOP] self.server {self.addr[0]} has stopped listening on port {self.addr[1]}")
if __name__ == "__main__":
server = Server()
server.start()
我能够使用以下 Dockerfile 在 docker 容器中运行服务器:
FROM python:3.6.9
COPY App /App
WORKDIR /App
RUN pip3 install -U pip
RUN pip3 install numpy
EXPOSE 5050
ENTRYPOINT [ "python3" ]
CMD [ "server.py" ]
docker运行命令:
sudo docker run -it --rm -p 5050:5050 --name test IMAGE_NAME
客户
# Client
PORT = 5050
HOST = "SERVER_IP" # or the IP of the server if is running on a different machine
ADDR = (HOST, PORT)
HEADER = 64
FORMAT = 'utf-8'
DISCONNECT_MESSAGE = 'DISCONNECT'
def send(sock, msg):
message = pickle.dumps(msg)
msg_length = len(message)
send_length = str(msg_length).encode(FORMAT)
send_length += b' ' * (HEADER - len(send_length))
sock.send(send_length)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(ADDR)
for i in range(200):
msg = f"Message number {i}"
send(sock, msg)
response = receive(sock)
print(response)
sock.close()
但是当部署在 docker swarm 服务中时,客户端会挂起
sock.connect()
(我的集群中有 2 个工作节点和 1 个管理节点)
服务创建命令:
sudo docker service create --name echo_server -p 5050:5050 --replicas 2 PRIVATE_REGISTRY_IP:PORT/IMAGE_NAME:TAG
我发现的所有 swarm 服务器示例都使用 nginx 映像,我能够将其部署在工作节点上,然后使用管理器节点的 IP 访问它(这就是我试图为 TCP 套接字服务器实现的目标)。
解决方案
我能够在堆栈中部署服务器
stack.yml
version: "3"
services:
echo_server:
image: REGISTRY_IP/IMAGE_NAME
ports:
- "5050:5050"
deploy:
replicas: 3
然后运行
sudo docker stack deploy -c /path/to/stack.yaml STACK_NAME
然后客户端就能够通过其中一台工作/管理节点 PC 的主机 IP 连接到服务
为此目的,Docker swarm 有两个负载均衡器:
当您从服务发布端口时,会调用入口负载均衡器。该端口是从所有 swarm 节点发布的,因此任何节点都可以用于连接到该服务。 docker 将循环连接到任何可用的(健康的)服务副本。
由于此流量通过桥接接口到达,因此您无法侦听“localhost”,因为这将阻止容器实际侦听桥接连接。
请注意,虽然每个 swarm 节点都会接受来自 localhost:port 的连接来连接到入口网络,但容器不能使用“localhost”来连接到它,因为它指的是容器内的 localhost 接口,而不是主机。从容器内部连接到当前本地主机的正确方法是地址:“docker.host.internal:8080”(需要在需要使用它的每个服务上启用它)。
另一个负载均衡器是网状网络负载均衡器。在堆栈中部署服务时,docker 会修改每个容器中的resolve.conf,以指向 docker 解析器,该解析器将解析容器所连接的每个网络的服务名称。 Docker swarm 还为该服务创建一个虚拟 IP,它是第 3 层负载均衡器,并将其与每个网络上该服务的 dns 名称相关联。
因此,如果您部署了一个名为“echotest”的服务作为名为“test”的堆栈的一部分,并且创建了 2 个副本,那么 docker 将分配以下内容:
IP 10.0.1.5 的 VIP,名称为“echotest”和“test_echotest”。
两个任务,IP 分别为 10.0.1.6 和 10.0.1.7。
附加到“test_default”网络 (10.0.1.0/24) 的任何其他服务都可以使用名称“echotest”和“test_echotest”连接到 vip,而 vip 又会实际连接到其中一个容器。
自己进行负载平衡的服务可以使用特殊名称“tasks.echotest”和“tasks.test_echotest”来获取 dns rr 响应:只是任务 ip 的顺序随机化。 [10.0.1.6,10.0.1.7]
这组命令显示了我的集群上的结果。
$ docker network create --attachable --driver overlay test
$ docker service create --network test --replicas 2 --name test-nginx nginx:latest
$ docker run --rm -it --network test nicolaka/netshoot
$ dig test-nginx +short
10.0.38.2
$ dig tasks.test-nginx +short
10.0.38.3
10.0.38.6
$ exit
$ docker service rm test-nginx
$ docker network rm test