maksim 发布的文章

你好,我是一笑。

再上一篇文章中,我们使用 fork 的形式完成了高性能网络服务的开发,但是由于 fork 的形式开销比较大,我们需要使用另外一种方式来进行实现,也就是这篇文章中要讲解的 select。

相对来说 select 要比 fork 高效很多,而我们常说的 select 其实就是Linux 为我们提供的异步事件 I/O,当发生事件的时候系统内核会给我们发送通知,与多进程多线程相比,异步 IO最大的优势就是开销小,因为无论是进程、线程在上下文切换的时候都会有一定的开销。同时使用异步 IO,我们就不必再去维护进程或者线程的状态 。

select 可以看做是半自动,处理的并不是特别好,不是完全按照事件进行处理,在应用层,我们还需要使用循环来进行遍历文件句柄的变化。

在 PHP 中为我们提供了 socket_select 函数,我们可以通过这个函数来监听文件句柄的变化。

<?php
//创建服务端的socket套接流,net协议为IPv4,protocol协议为TCP
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
    echo "Failed to set socket options!";
    exit();
}
socket_set_nonblock($socket);

// 绑定接收的套接流主机和端口,与客户端相对应
if (!socket_bind($socket, '0.0.0.0', 8888)) {
    echo 'server bind fail:' . socket_strerror(socket_last_error());
    exit();
}

// 监听套接流
if (!socket_listen($socket, 4)) {
    echo 'server listen fail:' . socket_strerror(socket_last_error());
    exit();
}

$all_sock[] = $socket;
//让服务器无限获取客户端传过来的信息
for (; ;) {
    $read = $write = $except = [];
    $read = $all_sock;

    $changed = socket_select($read, $write, $except, null);

    if (!$changed) {
        print "[error]socket_select() failed
        (" . socket_strerror(socket_last_error()) . ")\n";
    }

    if ($changed < 1) {
        continue;
    }

    foreach ($read as $key => $rsock) {
        // 服务端连接
        if ($rsock === $socket) {

            $client = socket_accept($socket);
            if (!$client) {
                echo "socket_accept error:!" . socket_strerror(socket_last_error());
                exit();
            }
            $all_sock[] = $client;
        } else {
          //客户端连接
            //读socket_read的作用就是读出socket_accept()的资源并把它转化为字符串
                  $string = socket_read($rsock, 1024);
                echo 'server receive is :' . $string . PHP_EOL;//PHP_EOL为php的换行预定义常量
                $return_client = 'server receive is : ' . $string . PHP_EOL;
                socket_write($rsock, $return_client, strlen($return_client));
            }
        }
    }
}
socket_close($socket);

select 有两个不足:

  • select 会受到单个进程最大打开描述符的限制。它只能监听的最大文件描述符个数由__FD_SIZE 决定,它的默认值是 1024;
  • select 返回后,需要遍历文件描述符集才能找到哪些文件描述符就绪了,文件描述符集在内核空间和用户空间频繁拷贝提升了系统的开销。

在下一章节,我们将使用 epoll 来解决这个问题。

你好,我是一笑。

在前面的两章中,我们已经实现了守护进程和基础的 TCP Server,在这个章节我们将实现高性能的网络服务,其实实现高性能网络服务有很多方法,在接下来的几个章节中,我将一一列举。

在我们上一节的代码中,如果你比较细心,就会发现当多个客户端连接服务其的时候是可以连接上,但是只有只有第一个连接到 tcp 的服务才可以进行通讯,其他的客户端必须等待第一个连接关闭后才可以发送请求。

真正的网络服务肯定不能这样实现,今天我们就要解决这个问题,怎么样才能够让 TCP 的服务器能够接收成百上千,甚至是上万的请求。

对于上一章的 TCP 服务器只有一个用户能够通讯,通过 fork 的改造就可以变成成百上千的用户来来连接服务器,每个连接过来我们都 fork 一个子进程来并行处理,就可以实现多人并发处理。

在对服务端进行修改之前,我们先对客户端进行一定的修改,我们先让客户端支持 Ctrl+C 退出程序,并且通知服务端要进行关闭 Scoket 了。

实现起来非常简单,我们只需要对 Ctrl+C 信号进行监听就可以了。

pcntl_signal(SIGINT, function () use ($socket) {
    echo "接收到退出信号,关闭 socket 并且退出进程" . PHP_EOL;
    $message = '.exit';
    socket_write($socket, $message, strlen($message));
    socket_close($socket);
    exit();
});

我们使用 pcntl_signal 来监听 SIGINT 信号,如果接收到信号就向服务端发送 .exit 告诉服务端,客户端要退出了,并且关闭 socket 连接,这么做的好处是,当我们客户端退出后可以主动通知服务端,服务端可以自由的做一些其他处理。

SIGINT:程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

同时,我们在循环发送的数据中增加 pcntl_signal_dispatch()函数检测是否有新的信号等待 dispatching。

while (true) {
      // 检测是否有新的信号等待 dispatching
    pcntl_signal_dispatch();
    // 向服务端写入字符串信息
    $ret = socket_write($socket, $message, strlen($message));
    if (!$ret) {
        echo 'fail to write' . socket_strerror(socket_last_error());
        exit();
    }
    // 读取服务端返回来的套接流信息
    $callback = socket_read($socket, 1024);
    echo 'server return message is:' . PHP_EOL . $callback;
    sleep(1);
}

接下来,我们来实现今天的主题,使用 fork 来实现高性能的网络服务器。它的基本原理非常简单,首先主进程进行监听连接,每收到一个连接就创建一个子进程进行数据包的收发。

    while ($string = socket_read($accept_resource, 1024)) {
        echo 'server receive is :' . $string . PHP_EOL;//PHP_EOL为php的换行预定义常量
        $return_client = 'server receive is : ' . $string . PHP_EOL;
        //Socket_write的作用是向socket_create的套接流写入信息,或者向socket_accept的套接流写入信息
        socket_write($accept_resource, $return_client, strlen($return_client));
    }

在上一章,我们使用 while 死循环来接收客户端传来的数据,主进程就会被阻塞在这里,其他的连接虽然可以连接但是无法传输数据,我们只需要将这段代码放到子进程中执行就可以了。

<?php
cli_set_process_title("tcp-master-process");
pcntl_async_signals(true);

//创建服务端的socket套接流,net协议为IPv4,protocol协议为TCP
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
    echo "Failed to set socket options!";
    exit();
}

// 绑定接收的套接流主机和端口,与客户端相对应
if (!socket_bind($socket, '0.0.0.0', 8888)) {
    echo 'server bind fail:' . socket_strerror(socket_last_error());
    exit();
}

// 监听套接流
if (!socket_listen($socket, 4)) {
    echo 'server listen fail:' . socket_strerror(socket_last_error());
    exit();
}

// 处理子进程退出
pcntl_signal(SIGCHLD, function ($id) {

    fprintf(STDOUT, "我接收到一个信号,编号为:%d \n", $id);

    $pid = pcntl_waitpid(-1, $status, WNOHANG);

    if ($pid > 0) {
        fprintf(STDOUT, "PID=%d 子进程退出了", $pid);
    }
});

//让服务器无限获取客户端传过来的信息
for (; ;) {
    // socket_accept的作用就是接受socket_bind()所绑定的主机发过来的套接流
    $accept_resource = socket_accept($socket);
    if (!$accept_resource) {
        echo "socket_accept error:!" . socket_strerror(socket_last_error());
        exit();
    }

    $pid = pcntl_fork();
    if ($pid == 0) {
        cli_set_process_title("tcp-worker-process");
        //读socket_read的作用就是读出socket_accept()的资源并把它转化为字符串

        while ($string = socket_read($accept_resource, 1024)) {
            if ($string == '.exit') {
                break;
            }
            echo 'server receive is :' . $string . PHP_EOL;//PHP_EOL为php的换行预定义常量
            $return_client = 'server receive is : ' . $string . PHP_EOL;
            //Socket_write的作用是向socket_create的套接流写入信息,或者向socket_accept的套接流写入信息
            socket_write($accept_resource, $return_client, strlen($return_client));
        }

        //关闭客户端连接
        socket_close($accept_resource);
        echo "没有数据了,关闭连接" . PHP_EOL;
        exit();
    } else {
        //关闭连接
        pcntl_signal_dispatch();
    }
}

if ($pid != 0) {
    //关闭连接
    socket_close($socket);
}

我们在接收到新连接后 fork 了子进程,在子进程中处理 tcp 的收发包,同时设置了进程名称方便我们在命令行中查看。

 cli_set_process_title("tcp-worker-process");

这样一来,我们就实现了一个比较高性能的服务了,通过 fork 的方式虽然可以实现高性能网络服务,但是同样也带来了很多问题。首先是资源被长期占用,只要客户端与服务端一直建立长连接,那么子进程一直被占用,进程在Linux 中是非常重要的资源,占用一个就少一个,他是有一定的限量的,几千几万被占用对 Linux 系统来说是灾难性的。

分配子进程花费时间长,因为创建子进程的成本是非常高的,如果很多连接突然过来,你会发现创建子进程的时间会很长,性能非常的满,这就是 fork 带来的问题,在下一节我们将利用 Select 异步I/O 解决这个问题。

同时在使用 fork 子进程这种方式的时候,我们还需要对子进程的状态进行维护,如果子进程先退出,而父进程没有处理的话就会变成僵尸进程,最终耗尽我们的进程号,导致无法启动新的进程,这是非常恐怖的事情。

处理僵尸进程的方法也很简单,我们只需要在父进程中接受子进程的退出信号即可。

// 处理子进程退出
pcntl_signal(SIGCHLD, function ($id) {
    fprintf(STDOUT, "我接收到一个信号,编号为:%d \n", $id);
    $pid = pcntl_waitpid(-1, $status, WNOHANG);
    if ($pid > 0) {
        fprintf(STDOUT, "PID=%d 子进程退出了", $pid);
    }
});

我们启动服务后,我们分别启动两个client,将其中的代码进行稍微的更改,第二个的编号设置为#2,就会发现都可以进行通信了。

server receive is :hello echo server #2 !
server receive is :hello echo server #1 !
server receive is :hello echo server #2 !
server receive is :hello echo server #1 !

同时我们使用 ps 命令来查看。

$ ps ajx | grep process
501 53843 98750   0  1:46下午 ttys004    0:00.03 tcp master process
501 53854 53843   0  1:46下午 ttys004    0:00.00 tcp tcp process
501 53907 53843   0  1:46下午 ttys004    0:00.00 tcp tcp process

我们一共是启动了两个 tcp client,在后台也就 fork 出了两个子进程,我们可以在多启动一个 #3 的 client 来试验一下。

你好,我是一笑。

在本节我们来介绍一些 socket 基础编程,其实 socket 基础编程非常的简单,你可以把它理解为文件描述符,每一个 socket 连接都是一个文件描述符,与我们打开文件、关闭文件是很类似的,只不过在其中增加了一些跟网络相关的 API 而已。

但是对于底层协议和基本原理还是很复杂的,我会放在后面的章节中进行讲解,在前面还是以使用为主,对于 socket 编程其实并不是很复杂,只需要通过几个 API 就可以实现了。

如何实现一个 TCP Server

实现一个 TCP Server 的基本步骤如下:

  1. 创建 scoket ,指定使用 TCP 协议,对应的是函数为 socket_create;
  2. 将 scoket 与地址和端口绑定,对应的函数为 socket_bind, 相当于打开了一个文件句柄,这个文件要与你的网络地址和端口进行绑定,用户在发送网络连接的时候,在系统的底层就会把事件抛给 socket。
  3. 侦听端口,对应的函数 socket_listen
  4. 接收创建新的socket 对应函数 socket_accept
  5. 使用 read 接收数据 对应函数 socket_read
  6. 使用 send 发送数据对应函数 socket_end
  7. 使用 close 关闭连接对应函数 scoket_close;

我们通过这七个步骤就可以实现一个 TCP Server。

在我们实现 TCP Server 的时候还可以使用 socket_set_option 给 TCP 设置一些选项,其中最常见的选项如下:

  • SO_REUSEADDR 端口处于 WAIT_TIME 仍然可以启动,通过这个选项我们可以对地址进行重用,当我们的网络服务器开启的时候,实际上已经打开了很多 TCP 连接,当 TCP 连接关闭的时候会发起 WAIT_TIME 等待 FINISH ,只有 FINISH 过来的时候才会关闭。当我们启用这个参数后,不用等待 FINISH 状态,直接可以复用地址和端口,对于服务端而言特别有效,服务端关闭了连接,最长可能会等待两分钟。
  • SO_RECVBUF,可以通过这个选项来调整Buffer 的大小,当数据来的时候没能及时处理可以在系统中将数据缓冲,一般设置 4M~8M。
  • SO_SNDBUF,发送 Buffer,如果在即时通信的时候设置较大的值,会产生一定的延迟。所以需要按照实际应用来设置大小。

下面我们编写一个类似于 echo 的服务,客户端与服务单保持长连接, 客户端不断向的服务端发送 hello echo server #1 !,然后服务端返回该值。

<?php
//创建服务端的socket套接流,net协议为IPv4,protocol协议为TCP
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
    echo "Failed to set socket options!";
    exit();
}

// 绑定接收的套接流主机和端口,与客户端相对应
if (!socket_bind($socket, '0.0.0.0', 8888)) {
    echo 'server bind fail:' . socket_strerror(socket_last_error());
    exit();
}

// 监听套接流
if (!socket_listen($socket, 4)) {
    echo 'server listen fail:' . socket_strerror(socket_last_error());
    exit();
}
//让服务器无限获取客户端传过来的信息
for (; ;) {
    // socket_accept的作用就是接受socket_bind()所绑定的主机发过来的套接流
    $accept_resource = socket_accept($socket);
    if (!$accept_resource) {
        echo "socket_accept error:!" . socket_strerror(socket_last_error());
        exit();
    }


    //读socket_read的作用就是读出socket_accept()的资源并把它转化为字符串
    while ($string = socket_read($accept_resource, 1024)) {
        echo 'server receive is :' . $string . PHP_EOL;//PHP_EOL为php的换行预定义常量
        $return_client = 'server receive is : ' . $string . PHP_EOL;
        //Socket_write的作用是向socket_create的套接流写入信息,或者向socket_accept的套接流写入信息
        socket_write($accept_resource, $return_client, strlen($return_client));
    }
    socket_close($accept_resource);

}

socket_close($socket);

如何实现 TCP 客户端

相对比服务端,客户端的实现要简单很多:

  1. 创建 scoket,指定使用 TCP
  2. 使用 connect 与服务端进行连接
  3. 收发数据socket_read, socket_write,
  4. close 关闭连接
<?php
//创建一个socket套接流
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

//接收套接流的最大超时时间1秒,后面是微秒单位超时时间,设置为零,表示不管它
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 1, "usec" => 0));

//发送套接流的最大超时时间为6秒
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 6, "usec" => 0));

//连接服务端的套接流,这一步就是使客户端与服务器端的套接流建立联系
if (!socket_connect($socket, '127.0.0.1', 8888)) {
    echo 'connect fail massage:' . socket_strerror(socket_last_error());
    exit();
}

$message = 'hello echo server #1 !';
//转为GBK编码,处理乱码问题,这要看你的编码情况而定,每个人的编码都不同
$message = mb_convert_encoding($message, 'GBK', 'UTF-8');

while (true) {
    //向服务端写入字符串信息
    $ret = socket_write($socket, $message, strlen($message));
    if (!$ret) {
        echo 'fail to write' . socket_strerror(socket_last_error());
        exit();
    }
    //读取服务端返回来的套接流信息
    $callback = socket_read($socket, 1024);
    echo 'server return message is:' . PHP_EOL . $callback;
    sleep(1);
}

到目前为止,服务端和客户端的代码就写好了,很简单,你可以自己编写一下。

你好,我是一笑。

目前市面上有很多开源库其中包括 Swoole、Workman、ReactPHP、AmpPHP 都可以非常方便的帮助我们去实现 Socket 或者 WebSocket 编程,但是这些都是开箱即用的工具,在这个系列的文章中,我将从 0 到 1 实现一个网络在线聊天室。

PHP Cli 后台服务守护进程

Socket 编程与 Web 或者 API 编程最大的差异就是我们的程序是要一直跑在我们的服务器后端执行,在我们手动执行一些 PHP 脚本的时候,如果是长时间的耗时操作我们可能会用到 nohup 命令将我们的脚本脱离终端,避免终端异常关闭后导致脚本执行退出。因为在我们正常的情况下如果你是在前端执行你执行的程序是属于当前终端的,当你终端结束时程序会收到 SIGSUP 的信号,收到信号后默认行为就是将程序直接杀死掉。

所以一般服务端的程序一般都是在后端去执行,所谓的后端就是我们的应用程序没有界面,也没有任何输出的信息,只能通过日志输出到一个文件里,而不是在标准的输入、输出和标准错误中看到任何信息。

最简单实现守护进程的方式就是使用 fork 函数来派生子进程,具体的流程如下:

1. 设置文件创建屏蔽字

文件创建屏蔽字是指屏蔽掉文件创建时的对应位(umask() 控制系统文件和目录默认权限)。由于使用fork系统调用新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的不便。因此,把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。

2. fork 子进程,父进程退出,子进程成为孤儿进程被 init 进程接管

fork 子进程的目的是为了执行我们真正的业务逻辑,而主进程在 fork 子进程后就退出,这样一来 子进程就会变成孤儿进程,孤儿进程会自动被系统的 init 进程接管,这个时候其实也就完成了后台运行,而后面的操作是为了让我们的程序更加健壮。

3. 调用 setsid 建立新的进程会话

虽然你的 主进程退出了,但是你的父子关系其实还是存在的,它有一个sessionid,当你没有做改变的时候,他的 sessionid 还会存在,标记着它在哪个 session 中,既然父进程已经退出了,我们被 init 进程接管了,那么就需要重新设置一个进程会话。

4. 将当前工作目录切换到根目录

在 linux 下是可以 mount 很多磁盘的,你可能是在系统磁盘下,也可能是在新 mount 的数据磁盘上,实际上我们应该与这个磁盘脱离关系,所以要切换一下工作目录。

5. 关闭标准输入、标准输出、标准错误将其重定向到 /dev/null

当Linux启动一个进程时,会自动打开三个的端口:标准输入(Standard Input)、标准输出(Standard Output)和标准错误(Standard Error)。进程通常会通过这三个端口进行输入和输出。

后台程序是应该没有标准输入和标准输出以及标准错误的,所以要将其重定向到 /dev/null中。

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。

代码实现

<?php

interface Process
{
    public function run(): void;
}

class WriteFileProcess implements Process
{
    public function run(): void
    {
        while (true) {
            //在这里写你的业务代码,比如每隔1秒钟,往当前目录下的test.txt文件里面添加当前时间
            file_put_contents(dirname(__FILE__) . '/test.txt', date('Y-m-d H:i:s') . PHP_EOL, FILE_APPEND);
            sleep(1);
        }
    }
}

class Daemon
{
    private $pidFile;
    protected $process;

    function __construct(Process $process)
    {
        $this->pidFile = dirname(__FILE__) . '/daemontest.pid';
        $this->process = $process;
    }

    private function startDeamon()
    {
        if (file_exists($this->pidFile)) {
            echo "The file $this->pidFile exists.\n";
            exit();
        }

        $pid = pcntl_fork();
        if ($pid == -1) {
            //这里是创建进程失败
            die('could not fork');
        } else if ($pid) {
            //这里是父进程
            echo 'start ok';
            exit($pid);
        } else {
            umask(0);

            //这里是子进程
            file_put_contents($this->pidFile, getmypid());

            // 建立新的会话
            posix_setsid();

            //设置工作目录
            chdir('/');

            //重定向标准输出,标准输入,标准错误到 /dev/null
            fclose(STDIN);//0
            fclose(STDOUT);//1
            fclose(STDERR);//2

            //当关掉以上标准输出,标准输入,标准错误之后,如果后面要对文件操作(比如打开一个文件,写入,创建)它返回的文件描述符从0开始,这样可能造成未知异常
            //为了避免问题,我们使用输出从定向到 /dev/null 空设备文件解决这个问题,重新设置0,1,2 文件描述符用来代替标准输入,标准输出,标准错误,往 /dev/null 写入数据会被丢弃,这样就不会向终端输出数据了。

            fopen("/dev/null", 'a');
            fopen("/dev/null", 'a');
            fopen("/dev/null", 'a');

            return getmypid();
        }
    }

    private function start()
    {
        $this->startDeamon();
        $this->process->run();
    }

    private function stop()
    {
        if (file_exists($this->pidFile)) {
            $pid = file_get_contents($this->pidFile);
            posix_kill($pid, 9);
            unlink($this->pidFile);
        }
    }


    public function run($argv)
    {
        if ($argv[1] == 'start') {
            $this->start();
        } else if ($argv[1] == 'stop') {
            $this->stop();
        } else if ($argv[1] == 'restart') {
            $this->stop();
            $this->start();
        } else {
            echo 'param error';
        }
    }

}

$daemon = new Daemon(new WriteFileProcess());
$daemon->run($argv);

首先,我们定义了一个 process 的接口,这个接,而实现这个接口的 WriteFileProcess 就是我们需要执行的程序,在这段代码中,我们不断的向一个文件中写入当前时间,让程序一直保持运行状态,为了避免CPU 被这个程序沾满,我们写入的时间间隔为 1 秒。

Daemon 类中 startDeamon 方法就是我们实现守护进程的过程,在 Daemon 中还实现了另外两个方法 runstartstop

run 可以看做我们的程序入口,他解析传递的命令,当为 start 的时候开始记录 pid,然后执行守护进程方法,执行我们要执行的业务逻辑。

# 执行程序
php Daemon.php start
# 关闭程序
php Daemon.php stop
# 重启程序
php Dameon.php restart

你可以执行一下看看执行效果。

参考文档

  1. [PHP 的标准输入与输出-php教程-PHP中文网](

https://docs.docker.com/swarm/overview/

2015 年年初,libswarm 项目开发六个月之后,Docker 向公众发布了 Swarm 的第一个 Beata 版本。Swarm 的目的是为 docker 客户端工具提供统一的接口,让它不仅能管理单个 Docker 守护进程,还能管理整个集群。Swarm 不是配置应用或实现可重复部署的工具,其作用是为 Docker 现有的工具提供集群资源管理能力。

架构

2022-10-04T12:54:02.png

进阶

Swarm集群(2manager-3worker)

https://docs.docker.com/engine/swarm/ swarm入门操作

In Docker 1.12 and higher;Swarm已经内置了,无需安装

8容器-->1个服务。 redis有8个实例。

对象-->类。

raft:大多,manager节点存活。worker不参与集群一致性。

2-manager >1台;生产环境最少3 manager;炸一个集群完美工作。

在所有docker主机上,选定一个主节点。运行docker swarm init --advertise-addr ip地址,初始化一个manager

docker swarm init \
--advertise-addr 192.168.121.142
--listen-addr 192.168.121.142
#关于advertise-addr和listen-addr这两个参数,前者用来指定其他节点连接m0时的地址,后者指定承载swarm流量的IP和端口

以后告别docker run

#1、docker-compose up 使用compose启动一个服务。docker-compose是单机下的玩具
#2、集群:docker service (使用docker管理服务。)容器升级为服务。
# docker service:管理集群里面的所有服务
# docker stack:管理集群的app stack;一个完整的应用有超多服务,就是一个服务栈

#扩容;
docker service create --name hello-nginx -p 8888:80 --replicas 3 nginx
docker service update --replicas 3 hello-nginx

#升级。滚动升级
docker service rollback hello-nginx; 当前+之前 来回回滚

#灰度发布,迭代。
docker service update --image nginx:1.18.0-alpine --update-parallelism 1 --update-delay 10s hello-nginx

docker service create 
#为什么每一个机器8888都能访问。

2022-10-04T12:58:32.png

剩下节点使用docker swarm join即可;

在主节点

docker swarm join-token manager
#可以生成Manager节点的join信息,再选中一个节点,运行命令就可以作为manager加入

docker node ls #查看节点信息

基本概念

1.Swarm
集群的管理和编排是使用嵌入docker引擎的SwarmKit,可以在docker初始化时启动swarm模式或者加入已存在的swarm
2.Node
一个节点是docker引擎集群的一个实例。您还可以将其视为Docker节点。您可以在单个物理计算机或云服务器上运行一个或多个节点,但生产群集部署通常包括分布在多个物理和云计算机上的Docker节点。
要将应用程序部署到swarm,请将服务定义提交给 管理器节点。管理器节点将称为任务的工作单元分派 给工作节点。
Manager节点还执行维护所需群集状态所需的编排和集群管理功能。Manager节点选择单个领导者来执行编排任务。
工作节点接收并执行从管理器节点分派的任务。默认情况下,管理器节点还将服务作为工作节点运行,但您可以将它们配置为仅运行管理器任务并且是仅管理器节点。代理程序在每个工作程序节点上运行,并报告分配给它的任务。工作节点向管理器节点通知其分配的任务的当前状态,以便管理器可以维持每个工作者的期望状态。
3.Service
一个服务是任务的定义,管理机或工作节点上执行。它是群体系统的中心结构,是用户与群体交互的主要根源。创建服务时,你需要指定要使用的容器镜像。
4.Task
任务是在docekr容器中执行的命令,Manager节点根据指定数量的任务副本分配任务给worker节点
------------------------------------------使用方法-------------------------------------
docker swarm:集群管理,子命令有init, ``join``, leave, update。(docker swarm --help查看帮助)
docker service:服务创建,子命令有create, inspect, update, remove, tasks。(docker service--help查看帮助)
docker node:节点管理,子命令有accept, promote, demote, inspect, update, tasks, ``ls``, ``rm``。(docker node --help查看帮助)

2022-10-04T12:55:39.png

Swarm的工作模式

Node

2022-10-04T12:59:32.png

Service

2022-10-04T13:00:22.png

3.任务与调度

2022-10-04T13:00:56.png

4.服务副本与全局服务

2022-10-04T13:00:38.png

3、测试

在Swarm中部署服务(nginx为例)

#1、可以创建网络
# docker network create -d overlay nginx_net
#2、部署service
docker service create --replicas 1 --name my_nginx -p 88:80 nginx    
# 就创建了一个具有一个副本(--replicas 1 )的nginx服务,使用镜像nginx
docker service ls
docker service inspect --pretty my_nginx
docker service ps my_nginx
#3、扩容
docker service scale my_nginx=4
docker service ps my_nginx
#4、模拟宕机查看效果
systemctl stop docker
docker node ls
docker service ps my_nginx
#5、Swarm 动态缩容服务(scale)
docker service scale my_nginx=1
#6、update扩缩容
docker service update --replicas 3 my_nginx
#7、update镜像升级
docker service update --image nginx:latest my_nginx
#8、服务移除
docker service rm my_nginx
#1、创建名为tomcat-net的覆盖网络(Overlay Netowork),这是个二层网络,处于该网络下的docker容器,即使宿主机不一样,也能相互访问:
docker network create -d overlay tomcat-net
#2、创建名为tomcat的服务,使用了刚才创建的覆盖网络:
docker service create --name tomcat \
--network tomcat-net \
-p 8080:8080 \
--replicas 2 \
tomcat:7.0.96-jdk8-openjdk
#3、执行命令docker service ls查看当前所有服务:
#4、执行命令docker service ps tomcat查看名为tomcat的服务,可见三个容器分别部署在m0、m2、w1机器上:
#5、执行命令docker service inspect --pretty tomcat查看名为tomcat的服务的详细信息(去掉--pretty可以看到更完整的):
#6、访问三个节点的tomcat,都能访问
#7、扩缩容
docker service scale tomcat=5
#8、滚动更新
docker service update \
--image tomcat:9.0.24-jdk11-openjdk \
--update-parallelism 1 \
--update-delay 10s tomcat
#update-parallelism:每次更新的容器数量,这里设置为1,表示每一个容器升级成功后才去升级下一个;
#update-delay:每一批升级成功后,升级下一批之前的等待时间,这里表示升级一个容器后等10秒再升级下一个;
#在升级过程中执行命令docker service ps tomcat查看服务,可以看到新版本容器逐个启动的过程:
#9、移除服务
docker service rm tomcat

服务模式

  • 服务模式一共有两种:Ingress和Host,如果不指定,则默认的是Ingress;

    • Ingress模式下,到达Swarm任何节点的8080端口的流量,都会映射到任何服务副本的内部80端口,就算该节点上没有tomcat服务副本也会映射;
    • Host模式下,仅在运行有容器副本的机器上开放端口,使用Host模式的命令如下:
docker service create --name tomcat \
--network tomcat-net \
--publish published=8080,target=8080,mode=host \
--replicas 3 \
tomcat:7.0.96-jdk8-openjdk

默认swarm集群的所有部署,(3副本)。

manager节点负责调度,真正会在其他节点执行(创建副本)【默认worker优先,基本随机】。我们不知道我们的容器最终会发配给那个服务器。指定服务器,给他部署上去(label)。

1、ssd硬盘 ,数据存储,mysql。合适作为数据节点,存储节点

2、cpu厉害,适合作为计算,大数据,数据分析框架,就应该优先调度到cpu厉害的。hadoop存储之类的,就应该调度到ssd磁盘。

Drain a node:排空节点。把节点里面的容器驱逐出去。
In earlier steps of the tutorial, all the nodes have been running with ACTIVE availability. The swarm manager can assign tasks to any ACTIVE node, so up to now all nodes have been available to receive tasks. ACTIVE的节点,manager就可以分配任务进行部署。

docker node update --availability drain worker1
#这台机器用service部署产生的所有容器,都被驱逐出去了。当前docker就没有service的容器。但是集群为了保证副本数量会在其他机器拉起。

docker node update --availability pause worker1 #暂停接受给我的任务。
 --availability active
#100 最好的状态,20台计算型的 。40台存储型的。30台冗余备份的
docker service hadoop --->调度到 40台存储型的;Label选择


# routing mesh;路由网
docker service create --name nginx -p 8080:80 --replicas 3 nginx
只要service是暴露端口的方式,全服务器都能访问。
#真正的nginx容器的ip
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
32: eth0@if33: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP 
    link/ether 02:42:0a:00:00:12 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.18/24 brd 10.0.0.255 scope global eth0
       valid_lft forever preferred_lft forever
34: eth1@if35: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever

#集群状态下,每个主机都会多两个网络
43c75b51441f        docker_gwbridge     bridge              local
o7b9pfkdt2ti        ingress             overlay             swarm

ingress: "Subnet": "10.0.0.0/24", "Gateway": "10.0.0.1"
docker_gwbridge:"Subnet": "172.18.0.0/16"
集群状态下,service创建的容器默认加入两个网络。 docker_gwbridge 、 ingress

4、Docker Stack和Docker Compose区别

  • Docker stack会忽略了“构建”指令,无法使用stack命令构建新镜像,它是需要镜像是预先已经构建好的。 所以docker-compose更适合于开发场景;
  • Docker Compose是一个Python项目,在内部,它使用Docker API规范来操作容器。所以需要安装Docker -compose,以便与Docker一起在您的计算机上使用;
  • Docker Stack功能包含在Docker引擎中。你不需要安装额外的包来使用它,docker stacks 只是swarm mode的一部分。
  • Docker stack不支持基于第2版写的docker-compose.yml ,也就是version版本至少为3。然而Docker Compose对版本为2和3的 文件仍然可以处理;
  • docker stack把docker compose的所有工作都做完了,因此docker stack将占主导地位。同时,对于大多数用户来说,切换到使用docker stack既不困难,也不需要太多的开销。如果您是Docker新手,或正在选择用于新项目的技术,请使用docker stack。

5、Docker Stack常用命令

命令描述
docker stack deploy部署新的堆栈或更新现有堆栈
docker stack ls列出现有堆栈
docker stack ps列出堆栈中的任务
docker stack rm删除一个或多个堆栈
docker stack services列出堆栈中的服务

​ 推荐实验: https://blog.csdn.net/huangjun0210/article/details/86502021

6、swarm网络细节

在 Swarm Service 中有三个重要的网络概念:

  • Overlay networks 管理 Swarm 中 Docker 守护进程间的通信。你可以将服务附加到一个或多个已存在的 overlay 网络上,使得服务与服务之间能够通信。
  • ingress network 是一个特殊的 overlay 网络,用于服务节点间的负载均衡。当任何 Swarm 节点在发布的端口上接收到请求时,它将该请求交给一个名为 IPVS 的模块。IPVS 跟踪参与该服务的所有IP地址,选择其中的一个,并通过 ingress 网络将请求路由到它。
    初始化或加入 Swarm 集群时会自动创建 ingress 网络,大多数情况下,用户不需要自定义配置,但是 docker 17.05 和更高版本允许你自定义。
  • docker_gwbridge是一种桥接网络,将 overlay 网络(包括 ingress 网络)连接到一个单独的 Docker 守护进程的物理网络。默认情况下,服务正在运行的每个容器都连接到本地 Docker 守护进程主机的 docker_gwbridge 网络。
    docker_gwbridge 网络在初始化或加入 Swarm 时自动创建。大多数情况下,用户不需要自定义配置,但是 Docker 允许自定义。
名称类型注释
docker_gwbridgebridgenone
ingressoverlaynone
custom-networkoverlaynone
  • docker_gwbridge和ingress是swarm自动创建的,当用户执行了docker swarm init/connect之后。
  • docker_gwbridge是bridge类型的负责本机container和主机直接的连接
  • ingress负责service在多个主机container之间的路由。
  • custom-network是用户自己创建的overlay网络,通常我们都需要创建自己的network并把service挂在上面。

ingress网络。vip(虚拟ip模式)

2022-10-04T13:02:10.png

https://docs.docker.com/engine/swarm/ingress/

查看网络的基本环境

yum install bridge-utils -y    ##以后就可以brctl show 查看网关
iptables -nL -t nat   ##查看转发规则

ln -s /var/run/docker/netns /var/run/netns ## 看容器创建的两个Net Namespace

ip netns ##查看Net Namespace
ip netns exec xxx ip add  ##查看mynet网络命名空间下的网卡情况。
ip netns exec xxx brctl show ##查看mynet网络空间下网桥挂载情况可以看出veth0挂到了br0网桥上。

Docker Compose安装ES集群实战:

https://www.elastic.co/guide/en/elasticsearch/reference/7.5/docker.html

Docker swarm 中的LB分为两种情况:

  1. Ingress Load Balancing:暴露端口方式的负载均衡。产生虚拟ip。转发链。
  2. Internal Load Balancing:内部的负载均衡。通过service的名字也可以访问吗?

自定义Service

  • the port where the swarm makes the service available outside the swarm

    • docker service -p 8080:80
  • an overlay network for the service to connect to other services in the swarm

    • docker service --network ?;同一个网络的跨机通讯
  • CPU and memory limits and reservations

    • docker service create

      --limit-cpu decimal                  Limit CPUs
      --limit-memory bytes                 Limit Memory
      
  • a rolling update policy

    • docker service create --update-delay 10s --update-parllelism 2
  • the number of replicas of the image to run in the swarm

    • docker service create --replicas 3 --name nginx nginx:1.19

Replicated and global services

2022-10-04T13:02:25.png

调整service以什么方式运行

--mode string                        
Service mode (replicated or global) (default "replicated")

docker service create --mode replicated --name mytom tomcat:7 默认的


docker service create --mode global --name haha alpine ping baidu.com
#场景?日志收集
每一个节点有自己的日志收集器,过滤。把所有日志最终再传给日志中心
服务监控,状态性能。
docker service create --name myredis --network myswarm-net redis
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
93: eth0@if94: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP 
    link/ether 02:42:0a:00:01:03 brd ff:ff:ff:ff:ff:ff
    inet 10.0.1.3/24 brd 10.0.1.255 scope global eth0
       valid_lft forever preferred_lft forever
95: eth1@if96: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:12:00:04 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.4/16 brd 172.18.255.255 scope global eth1;都是本机的
       valid_lft forever preferred_lft forever
 "Endpoint": {
            "Spec": {
                "Mode": "vip"
            },
            "VirtualIPs": [
                {
                    "NetworkID": "xm5bv9m1yb5q5kfwl5sgbed0j",
                    "Addr": "10.0.1.2/24"  #集群访问redis拿这个可以用
                }
            ]
        }
ping 容器的集群ip(10.0.1.3),service的集群vip(10.0.1.2),serviceName都通
3redis副本以后;容器ip  10.0.1.8、10.0.1.3、10.0.1.9。
无论多少个副本。一个service虚拟ip是固定的。负载均衡的方式。
建立redis集群。一个虚拟ip。sb应用和redis集群只要在同一个网络。直接写10.0.1.2,serviceName




docker service create --name mynettomcat --network myswarm-net tomcat
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
93: eth0@if94: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:00:01:06 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.1.6/24 brd 10.0.1.255 scope global eth0
       valid_lft forever preferred_lft forever
95: eth1@if96: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:12:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 172.18.0.4/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever
   docker service  inspect  mynettomcat
    "Endpoint": {
            "Spec": {
                "Mode": "vip"
            },
            "VirtualIPs": [
                {
                    "NetworkID": "xm5bv9m1yb5q5kfwl5sgbed0j",
                    "Addr": "10.0.1.5/24"
                }
            ]
        }

     10.0.1.0/24
#1、他们两个能互相访问吗?有哪些方式。ip?serviceName?

#2、vip,dnsrr
 docker service update --endpoint-mode dnsrr myredis
 # docker inspect service就没vip了想要负载均衡。只能serviceName;
 外部端口暴露负载均衡ingress。内部集群负载。vip。dnsrr

1、给docker集群部署服务的两种方式

docker service create xxxxx

2、使用compose文件

docker-compose up -d -c wordpress-compose.yaml;
效果:单机部署。自己产生默认的网络
docker stack deploy wordpress-compose.yaml;#集群部署,compose里面的所有服务会被发配到集群的各个地方
#写好docker-compose文件。运维人员部署即可。
version: '3.1'

services:

  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - wordpress:/var/www/html

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db:/var/lib/mysql

volumes:
  wordpress:
  db:
  
#扩展。
集群下的数据一致。
redis,mysql等这些集群。数据本身挂载到本机(没有任何问题)。高可用,就是做mysql的集群。设置好集群内的数据同步,这是mysql集群技术解决的问题。而不是数据卷挂载解决的问题。
应用的资源文件。才应该考虑数据卷的集群情况下,如果本机挂载就不一致了。自己文件系统。
nfs;/abc  ---> 远程的:/haha


Dockerfile
VOLUME ["/sss"] -->映射到docker /var/lib/docker/volumes

2022-10-04T13:05:29.png

2022-10-04T13:05:40.png

Docker Secret与Config

Secret

生产环境下,为了安全,我们不能把各项目的配置密码写入到配置文件

我们可以引入docker的secret方式保护密码。

场景

  • 用户名密码
  • SSH Key
  • TLS认证
  • 任何别人不想看到的数据

1、创建一个密码secret

2、用完就删

3、哪个服务想要使用只要暴露给他即可。

1、如何声明

#文本模式
printf 123456 | docker secret create my_secret -
echo "adminadmin" | docker secret create my-pwd -
#文件模式
docker secret create my_secret ./pwd
#Secret会基于raft在master主机之间同步。

2、如何使用

secret可以分配给一个service,就可以使用,secret在容器内部看起来是个文件,实际上在内存中

#1、普通情况
docker service create --name mysql --secret mysql_pwd -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_pwd -p 3306:3306 mysql:5.7

#2、compose文件
version: "3"
services:
  mysql:
    image: mysql:5.7
    ports:
      - 3306:3306
    secrets:
      - mysql_pwd
    enviroment:
      - MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_pwd

原理:我们使用--secret mysql_pwd以后,secret密码文件就会被解码保存到容器内部的/run/secrets/secretname中。这样我们就可以在容器中任意使用。但是外部无感知。

不管怎么使用,secret最好提前创建好。到时候声明使用就行。其实是在内存中的。整个集群manager是利用raft同步的

config

1、如何声明

docker config create [OPTIONS] CONFIG file|-
docker config create redis.conf redis.conf
# 使用文件方式创建
docker service create --config redis.conf --name redis redis
#最终会被映射到容器的根目录。以后修改容器的启动命令即可


#配置中心
#部署100个redis副本。到100个服务器。他们配置文件都一样
#config内容base64编码,是可以inspect出来的。
secret文件是在容器中 /run/secrets/xxxx 暴露的
config默认是在根目录暴露的。

2、如何使用

#指定位置
docker service create --name redis \
  --config source=redis-conf,target=/etc/redis/redis.conf,mode=0400 redis:3.0.6
  
#2、compose文件
version: "3"
services:
  mysql:
    image: redis
    ports:
      - 6379:6379
    config:
      - mysql_pwd
    enviroment:
      - MYSQL_ROOT_PASSWORD_FILE: /

https://docs.docker.com/compose/compose-file/#configs

3、补充Label的使用

# 我们讨论了 Service 部署的两种模式:global mode 和 replicated mode。无论采用 global mode 还是 replicated mode,副本运行在哪些节点都是由 Swarm 决定的,作为用户我们有没有可能精细控制 Service 的运行位置呢?
答:能,使用 label

逻辑分两步:
1、为每个 node 定义 label。
2、设置 service 运行在指定 label 的 node 上。

docker node update --label-add env=test 节点1
docker node update --label-add env=prod swarm-worker2
docker service create \
      --constraint node.labels.env==test \
      --replicas 3 \
      --name my_web \
      --publish 8080:80 \
      httpd
      
#更新 service,将其迁移到生产环境:
docker service update --constraint-rm node.labels.env==test my_web  
docker service update --constraint-add node.labels.env==prod my_web

8、扩展阅读

1、vxlan https://blog.csdn.net/tony_vip/article/details/100097245

2、lvs https://blog.csdn.net/tony_vip/article/details/104224374

其他填坑

2022-10-04T13:03:20.png

Service Mode:服务模式。 Endpoint Mode:端点模式

1、集群的所有操作必须在manager节点执行

2、机器数量决定的业务体量

3、service就是一个业务,真正是以容器的方式运行在docker机器。服务副本指容器数量

​ 打日志的时候,加上机器名就行。

Reference

  • 《Docker 即学即用》/ (美)马迪亚斯(Matthias,K.),(美)凯恩(Kane,S.P.) 著;安道译. —— 北京:中国电力出版社,2015.12