查看原文
其他

小网站的容器化(下):网站容器化的各种姿势,先跟着撸一波代码再说!

王洪鹏 CSDN云计算 2020-10-29

作者 | 王洪鹏

责编 | Carol

出品 | CSDN云计算(ID:CSDNcloud)

封图| CSDN下载于视觉中国 

上篇文章:小网站的容器化(上) 中我们大致描述了下个人网站在日常维护中的痛点,文章的后半部分我们添加了一个纯静态网站容器化的简单示例。这篇文章作为上篇文章的续,我们接着看下静态网站容器化的剩余部分,以及常见个人动态网站容器化的部分。
 
静态网站容器化

上篇文章中,我们已经对静态网站的容器化过程以及容器化期间需要注意的事项做过描述,也添加过一个个人博客网站容器化的示例,但是这个示例仅仅是为静态网站容器化的过程作一个展现。我们知道,实际部署中,即使是纯静态页面的网站也不会那样进行部署,即直接将所有的内容,包括网站本身的文件和web server全部打包进一个容器里面。
这种部署方式,不利于网站后续的维护和更新,比如网站访问时我们一般对外提供的访问端口为80或者443,如果几个网站的web server对应的容器跑在一台宿主机上,几个容器都用80端口时会存在端口冲突的问题,而且在这种情况下我们需要为每个网站去专门配置一个域名。实际使用中我们一般不会这样进行操作,也不会为一个简单的个人简历静态网站去专门购买一个域名。
为解决这种问题,我们一般会在网站的web server前面再添加一个公共的反向代理组件,比如nginx,来作为多个个人小网站统一的访问入口,这样在对网站的内容进行更新时,我们只需要去更新反向代理之后对应的应用即可,不会因为某个网站的更新而影响到其他网站的正常访问。
另外,我们负载均衡之后的不同网站的应用此时可以配置80、443之外的端口,对外的80、443端口的监听放在负反向代理上即可,而且这种情况下只需要购买一个域名,然后将域名解析到公共的负载均衡地址,不同的网站采用不同的子域名即可,因此采用这种模式既利于网站的更新维护,且在经济方面更加的划算。下面就让我们一起看下这种情况下网站的容器化是如何进行的。
上篇文章中关于网站本身的打包及其镜像构建的过程我们已经详细的说过,包括git配置、Dockerfile书写、镜像构建、基本容器管理等,因此在此我们就不再赘述,接下来我们直接看上文中说到的反向代理的添加过程。 
在此我们假设我们给应用配置的端口为8080,然后配置nginx独占80端口,然后我们就可以根据客户端对域名的请求来将请求分发到对应的容器中的应用。 
首先我们用容器化的方式部署一个我们的网站,假设我们网站使用的web server 为tomcat,监听的端口使用默认的8080。
# 下载tomcat 镜像
[root@localhost ~]# docker pull hub.c.163.com/public/tomcat:7.0.28

Trying to pull repository hub.c.163.com/public/tomcat ...

7.0.28: Pulling from hub.c.163.com/public/tomcat

f46924f139ed: Already exists

a3ed95caeb02: Pull complete

4849cac99801: Already exists

5e90c4274a33: Pull complete

e8f49ae1f54f: Pull complete

cfd12ae39390: Pull complete

1065252df5d5: Pull complete

0fe337adb9c6: Pull complete

9e5f36235195: Pull complete

7edf271c9251: Pull complete

c255b3c6176a: Pull complete

d1aa8ac63c58: Pull complete

f9ab623b3035: Pull complete

f7da31d1f835: Pull complete

48e150291322: Pull complete

069d27b85888: Pull complete

1835e80c6480: Pull complete

Digest: sha256:dd6c708e981e61f6aae56dac2ef91a78b28e0589743b44b7f174a57b1f097154

Status: Downloaded newer image for hub.c.163.com/public/tomcat:7.0.28
# 运行tomcat容器(应用代码在同一个容器中)
[root@localhost ~]# docker rm -f tomcat c64f076c3fda8d3183e570ad2af7253340de4fb48d098b6608f89fbf7715af0c

c64f076c3fda8d3183e570ad2af7253340de4fb48d098b6608f89fbf7715af0c

[root@localhost ~]# docker run --name tomcat -d -p 8080:8080  hub.c.163.com/public/tomcat:7.0.28

9a1e7c9909b8ce0299feecb5d424e16f8769476ef31d237ada8b8827c2106265

[root@localhost ~]# hostname -I

192.168.19.128 192.168.122.1 172.17.0.1
# 查看下端口是否已经在正常监听
[root@localhost ~]# netstat -anupt | grep 8080

tcp6       0      0 :::8080                 :::*                    LISTEN      48830/docker-proxy
直接用容器所在机器的IP加8080端口访问,看下网站对应的容器是否已经运行起来:
可以看到此时网站对应的应用已经运行起来了,接下来我们配置下nginx反向代理。
在配置nginx的反向代理之前,第一步我们需要先下载、部署下nginx,在此我们直接以官网文档中的提供的方式进行部署:
# 安装依赖工具
[root@localhost ~]# yum install -y yum-utils 
# 配置yum 源,新建一个名为nginx.repo的配置文件
[root@localhost ~]# vim /etc/yum.repos.d/nginx.repo
加入如下yum配置:
[nginx-stable]

name=nginx stable repo

baseurl=http://nginx.org/packages/centos/$releasever/$basearch/

gpgcheck=1

enabled=1

gpgkey=https://nginx.org/keys/nginx_signing.key

module_hotfixes=true



[nginx-mainline]

name=nginx mainline repo

baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/

gpgcheck=1

enabled=0

gpgkey=https://nginx.org/keys/nginx_signing.key

etodule_hotfixes=true
# 清空本地缓存
[root@localhost ~]# yum clean all

Loaded plugins: fastestmirror, langpacks

Cleaning repos: Ceph Ceph-noarch ceph-source epel kubernetes nginx-stable

Cleaning up everything

Cleaning up list of fastest mirrors
# 安装nginx
[root@localhost ~]# yum install -y nginx

Loaded plugins: fastestmirror, langpacks

Loading mirror speeds from cached hostfile 
# 运行nginx
[root@localhost ~]# systemctl start nginx 
# 确认nginx是否可以正常的运行起来
[root@localhost ~]# systemctl status nginx

● nginx.service - nginx - high performance web server

   Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)

   Active: active (running) since Thu 2020-03-19 03:46:00 PDT; 6s ago

     Docs: http://nginx.org/en/docs/

  Process: 27957 ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf (code=exited, status=0/SUCCESS)

 Main PID: 27958 (nginx)

   Memory: 2.9M

   CGroup: /system.slice/nginx.service

           ├─27958 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf

           └─27959 nginx: worker process



Mar 19 03:46:00 localhost.localdomain systemd[1]: Starting nginx - high performance web server...

Mar 19 03:46:00 localhost.localdomain systemd[1]: PID file /var/run/nginx.pid not readable (yet?) after start.

Mar 19 03:46:00 localhost.localdomain systemd[1]: Started nginx - high performance web server.
# 配置开机自启动,防止服务器重启后服务挂掉的情况
[root@localhost ~]# systemctl enable nginx

Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.
Nginx正常运行起来的话,通过访问服务器的IP地址,可以查看到如下页面:
虽然web server tomcat 和反向代理nginx已经运行起来,但是默认情况下nginx中的配置是没有指向我们的web server tomcat的,因此我们接下来需要我们进行一些相关的配置,在此我们假设刚刚tomcat应用的域名为test.tomcat.com。
修改nginx配置,nginx默认配置文件为/etc/nginx/nginx.conf,在nginx配置文件中添加如下配置,将访问test.tomcat.com的请求转向我们网站应用对应的tomcat容器: 
首先我们需要在nginx的配置文件的路径/etc/nginx/conf.d下新建一个vhost.conf配置文件,这个配置文件就是用来配置我们网站反向代理的配置文件(不建议直接在nginx的默认配置文件/etc/nginx/nginx.conf 中进行配置,建议为每个vhost配置单独的配置文件): 
# 新建配置文件
[root@localhost ~]# cd /etc/nginx/conf.d

[root@localhost conf.d]# ls

default.conf

[root@localhost conf.d]# vim vhost.conf
在打开的新配置文件中添加如下配置信息:
server

{

    listen 80;

    server_name test.tomcat.com;

    location / {

        proxy_pass http://127.0.0.1:8080;

    }

}
修改完成后,保存下配置。 
上述配置大意为,监听来自80端口的访问,如果访问的域名是test.tomcat.com则将请求全部转发到http://127.0.0.1:8080,也就是转发到我们tomcat 应用容器。 
Nginx配置完成后,需要reload后者重启下nginx服务,配置才能生效:
[root@localhost conf.d]# nginx -s reload
或者:
[root@localhost conf.d]# systemctl restart nginx

[root@localhost conf.d]# systemctl status  nginx
接下来访问下nginx的80端口看下是否可以将请求转发到我们的8080端口的tomcat容器,访问nginx之前我们需先配置下本地的hosts,实际配置中一般大家都给自己的网站都购买了域名,这样的话实际配置时可省略掉这一步骤,配置hosts如下:

##### self test

192.168.19.128  test.tomcat.com
访问nginx中配置的域名:
截图中可以看到已经返回了tomcat的数据,说明nginx的反向代理到后端的tomcat 8080端口已经配置成功了。

动态网站容器化
 
以上是对静态网站静态化的一个简单说明,实际使用中,即使个人网站一般也很少做成静态网站这种形式,所以接下来我们看下常见的个人站点的容器化的过程。
1. Node.JS 站点容器化 
在描述Node.JS站点的容器化之前我们先在容器所在的虚拟机上部署一个简单的node.js运行环境和node.js应用。
Node.js 的整个安装部署非常简单,node.js 官网为我们提供了多种安装部署方式,在此我们以二进制包安装的方式为例看下具体的安装过程: 
# 下载node.js
[root@localhost ~]# wget https://nodejs.org/dist/v10.9.0/node-v10.9.0-linux-x64.tar.xz
# 解压安装包
[root@localhost ~]# tar xvf node-v10.9.0-linux-x64.tar.xz -C /usr/local
# 查看node.js文件
[root@localhost ~]# tar xvf node-v10.9.0-linux-x64.tar.xz -C /usr/local
[root@localhost ~]# ls /usr/local/node-v10.9.0-linux-x64/
bin  CHANGELOG.md  include  lib  LICENSE  README.md  share
[root@localhost ~]# ls /usr/local/node-v10.9.0-linux-x64/bin
node  npm  npx
可以看到默认情况下不仅node本身的进程已经给我们装上了,包管理工具npm默认也已经安装好了。 
# 配置下软连接,方便后续使用
[root@localhost ~]# ln -s /usr/local/node-v10.9.0-linux-x64/bin/node /usr/local/bin
[root@localhost ~]# ln -s /usr/local/node-v10.9.0-linux-x64/bin/npm /usr/local/bin
# 查看node版本
[root@localhost ~]# node -v
v10.9.0
接下来我们写个node.js 简单示例来作为我们的应用。 
新建app.js,加入示例代码:
var http = require('http');

http.createServer(function (request, response) {

    // 发送 HTTP 头部

    // HTTP 状态值: 200 : OK

    // 内容类型: text/plain

    response.writeHead(200, {'Content-Type''text/plain'});

    // 发送响应数据 "Hello World"

    response.end('Hello World\n');

}).listen(8888);

// 终端打印如下信息

console.log('Server running at http://127.0.0.1:8888/');
从文件的注释部分也能看出,我们是起了一个HTTP 服务器,监听端口为8888,接下来我们运行下这段代码:
[root@localhost ~]# node app.js
Server running at http://127.0.0.1:8888/
从日志提示我们可以看出此时HTTP Server已经在运行中了,接下来我们访问下这个Server,看下能否访问:
能正常访问到,则说明我们在虚拟机上的node已经正常跑起来了。 
刚刚我们是在前台运行的我们的node程序,前台运行程序适合临时测试下程序,如果是长期运行的程序一般都是建议在后台跑,在此我们以nohup这个工具为例,将程序在后台运行一下:
[root@localhost ~]# nohup node app.js  &> app.log &
[1128553
[root@localhost ~]#
[root@localhost ~]#
[root@localhost ~]#
[root@localhost ~]# netstat -anupt | grep 8888
tcp6       0      0 :::8888                 :::*                    LISTEN      128553/node
这样程序已经在后台运行起来,不再占据前端,我们也不用担心接下来的操作过程中因为误操作会将程序意外退出。 
前面的访问我们也看到了,我们的HTTP server 默认监听的是8888端口,因此我们也需要用nginx做下反向代理。假设我们node.js 站点的域名是test.nodejs.com,和上文中tomcat站点的配置一样,首先我们需要先在nginx中新建一个网站的配置文件,由于上文中tomcat站点已经新建了一个单独的配置文件,在此我们不再重复进行新建,使用已经新建的即可。
修改/etc/nginx/conf.d/vhost.conf文件,加入对node.js站点的配置:
[root@localhost ~]# vim /etc/nginx/conf.d/vhost.conf
修改后内容如下:
 
在此我们给node.js站点配置的域名为test.node.com,实际使用中不需要重新为node申请域名,直接使用一个tomcat站点的子域名即可。
# 重启nginx 服务
[root@localhost ~]# systemctl restart nginx 
# 查看nginx状态
[root@localhost ~]# systemctl status  nginx
● nginx.service - nginx - high performance web server

   Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)

   Active: active (running) since Sun 2020-03-29 21:50:24 PDT; 6h ago

     Docs: http://nginx.org/en/docs/

  Process: 10503 ExecStop=/bin/kill -s TERM $MAINPID (code=exited, status=0/SUCCESS)

  Process: 128736 ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf (code=exited, status=0/SUCCESS)

 Main PID: 128737 (nginx)

   CGroup: /system.slice/nginx.service

           ├─128737 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf

           └─128738 nginx: worker process



Mar 29 21:50:23 localhost.localdomain systemd[1]: Starting nginx - high performance web server...

Mar 29 21:50:24 localhost.localdomain systemd[1]: Started nginx - high performance web server.
和前文中tomcat的反向代理配置一样,我们需要先为node.js 站点的域名配置下hosts,具体如下:
192.168.19.128  test.node.com
浏览器访问test.node.com
上图中可以看出,node.js站点的反向代理已经配置成功了,node.js站点可以直接通过nginx的80端口进行访问了。
2. WordPress 站点容器化
在个人网站搭建工具中,WordPress 是非常受大众欢迎的,因此使用WordPress搭建的个人站点不在少数,接下来我们会一起看下WordPress 站点的容器化。
细心的读者可能会发现前面的两个示例中我们并未提及每个网站必备的组件—数据库,因为数据库的容器化比较特殊,因此我们单独拿出来在WordPress站点的例子中描述下。 
前面的实例中我们在打包镜像时直接将代码、配置文件等一起打包到了镜像里面,这种方式在无状态的服务这种情况时是可以的,但如果是mysql数据库这种有状态服务的情况这种打包镜像的方式很显然就不行了,这种情况下直接将正在运行的容器保存为镜像主要存在两个问题:
(1) 数据库类这种无状态应用一般都需要账号密码信息进行鉴权,我们将这些信息配置到应用的配置文件中,如果将这些文件打包到镜像里面,然后将其push到公有的镜像仓库,则其他人可以通过下载我们上传的镜像来查看到我们的数据库账号信息。
(2) 容器是无状态的,如果我们重启容器或者容器被意外的删除,则我们运行起容器后新写入的数据会丢失,包括们存储在mysql数据库中的数据。 
好在Docker 为我们提供了volume这种产品功能,利用volume我们可以将容器所在宿主机上的某个目录挂载到容器中,通过这种方式将容器中的目录和容器所在宿主机上的目录建立一种绑定关系。通过这种方式,容器中MySQL写的数据就会被同步到宿主机的目录之上,这样当我们重启容器或者删除容器时容器运行后新写入的数据就不会丢失。
需要注意的是,挂载了vloume的容器,在进行镜像的保存时并不会将vloume中的数据保存到镜像中,因此在更新容器后注意还需要再次挂载下我们的volume。
接下来我们具体看下wordpress站点的容器化过程,wordpress在此我们以4.5.2为例。
  # 首先我们需要先获取下wordpress镜像
  [root@localhost ~]# docker pull hub.c.163.com/public/wordpress:4.5.2
Trying to pull repository hub.c.163.com/public/wordpress ...

4.5.2: Pulling from hub.c.163.com/public/wordpress

f46924f139ed: Already exists

a3ed95caeb02: Pull complete

4849cac99801: Already exists

682d1e7cffd4: Pull complete

162c309da2f9: Pull complete

2161e9680cdd: Pull complete

43391942ffef: Pull complete

d77d4c0e8fd0: Pull complete

6a6f091f97d8: Pull complete

53fa8ad0f2eb: Pull complete

28ee09fdd4e4: Pull complete

df176ee322b5: Pull complete

44104685ee03: Pull complete

1822edc7ae75: Pull complete

ecfd62e25a20: Pull complete

f3c6ecfde54e: Pull complete

81113ad0b0e3: Pull complete

bb5c02f581db: Pull complete

1a10055e720e: Pull complete

Digest: sha256:894e1cd6b398ea895f94d803e6de71c12c9b68fa3663e10c96ed4bc22adcb54a

Status: Downloaded newer image for hub.c.163.com/public/wordpress:4.5.2
  # 镜像下载完成后,我们运行下容器,看下载的镜像是否正常
[root@localhost ~]# docker run --name wordpress -p 8082:80  -d hub.c.163.com/public/wordpress:4.5.2

ae09f3291607130bd78186caadb02d0ba8af5d684b82165ab19cd89c1a145833 
因为nginx默认已经监听了80端口,为防止出现端口冲突,此处我们将apache web server 的端口映射为8082,容器名字为wordpress。 
# 查看端口是否在监听了
[root@localhost ~]# netstat -anupt | grep 8082

tcp6       0      0 :::8082                 :::*                    LISTEN      4259/docker-proxy
# 进入容器看下wordpress的各个组件是否已经运行起来了
[root@localhost ~]# docker exec -ti wordpress bash

root@ae09f3291607:/var/www# ps aux

USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND

root          1  0.0  0.0   4192   576 ?        Ss   04:36   0:00 /bin/sh -c /etc/init.d/mysql start && /tmp/entrypoint-wd.sh apache2 && /usr/sbin/sshd -D || /usr/sbin/sshd -D

root         34  0.0  0.0   4192   708 ?        S    04:36   0:00 /bin/sh /usr/bin/mysqld_safe

mysql       363  0.1  6.4 371504 64136 ?        Sl   04:36   0:00 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/mysql/plugin --user=mysql --pid-file=/var/run/mysqld/mysqld.pid

root        364  0.0  0.0   4100   632 ?        S    04:36   0:00 logger -t mysqld -p daemon.error

root        617  0.0  0.9 158924  9588 ?        Ss   04:37   0:00 /usr/sbin/apache2 -k start

root        619  0.0  0.3  49952  3028 ?        S    04:37   0:00 /usr/sbin/sshd -D

www-data    621  0.0  0.6 158956  6008 ?        S    04:37   0:00 /usr/sbin/apache2 -k start

www-data    622  0.0  0.6 158956  6008 ?        S    04:37   0:00 /usr/sbin/apache2 -k start

www-data    623  0.0  0.6 158956  6008 ?        S    04:37   0:00 /usr/sbin/apache2 -k start

www-data    624  0.0  0.6 158956  6008 ?        S    04:37   0:00 /usr/sbin/apache2 -k start

www-data    625  0.0  0.6 158956  6008 ?        S    04:37   0:00 /usr/sbin/apache2 -k start

root        626  2.3  0.1  17836  1840 ?        Ss   04:41   0:00 bash

root        631  0.0  0.1  15320  1136 ?        R+   04:41   0:00 ps aux
可以看到当前镜像默认使用的是LAMP架构,且各个组件已经在运行中。然后我们看下mysql默认的数据目录,从上面的信息中可以看到为:/var/lib/mysql。需要注意的是我们不能直接将宿主机上的目录挂载到我们的wordpress容器中的mysql数据存放路径,因为直接挂载会导致容器中MySQL 的数据被覆盖掉,为避免MySQL的数据被覆盖,在此我们先将已经存在的mysql数据拷贝出来,然后再进行宿主机目录的挂载。然后接下来我们将这个目录和宿主机的目录关联一下。 
# 本地新建下wordpress中mysql的数据目录
[root@localhost ~]# mkdir /data/mysql
# 将wordpress容器中已经存在的mysql数据拷贝到宿主机上
[root@localhost ~]# docker cp  wordpress:/var/lib/mysql/ /data/mysql

[root@localhost ~]# ls /data/mysql

debian-5.5.flag  ibdata1  ib_logfile0  ib_logfile1  mysql  mysql_upgrade_info  performance_schema  wordpress
# 删掉当前已经在运行的wordpress容器
[root@localhost ~]# docker rm -f wordpress

Wordpress

[root@localhost ~]# docker ps

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

[root@localhost ~]#
# 重新运行wordpress容器并挂载mysql的数据目录
[root@localhost ~]# docker run --name wordpress -v /data/mysql/:/var/lib/mysql/ -p 8082:80  hub.c.163.com/public/wordpress:4.5.2 
和前面的静态站点以及node.js程序一样,由于我们监听的8082端口是非80端口,因此还需要为wordpress站点配置下反向代理。 
具体过程和前面配置node.js站点的一样,假设我们的wordpress站点的域名为test.wordpress.com,然后直接在现存的vhosts文件中进行配置即可,具体如下:
# 修改nginx中vhost.conf配置
[root@localhost ~]# vim /etc/nginx/conf.d/vhost.conf
修改后的配置信息如下:
server

{

    listen 80;

    server_name test.tomcat.com;

    location / {

        proxy_pass http://127.0.0.1:8080;

    }

}

server

{

    listen 80;

    server_name test.node.com;

    location / {

        proxy_pass http://127.0.0.1:8888;

    }

}

server

{

    listen 80;

    server_name test.wordpress.com;



    location / {

        proxy_pass http://127.0.0.1:8082;

    }

}
# 重启nginx服务
[root@localhost ~]# systemctl restart nginx
[root@localhost ~]# systemctl status  nginx
● nginx.service - nginx - high performance web server

   Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)

   Active: active (running) since Thu 2020-04-02 04:37:09 PDT; 6s ago

     Docs: http://nginx.org/en/docs/

  Process: 47753 ExecStop=/bin/kill -s TERM $MAINPID (code=exited, status=0/SUCCESS)

  Process: 47774 ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf (code=exited, status=0/SUCCESS)

 Main PID: 47775 (nginx)

   CGroup: /system.slice/nginx.service

           ├─47775 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf

           └─47776 nginx: worker process



Apr 02 04:37:09 localhost.localdomain systemd[1]: Starting nginx - high performance web server...

Apr 02 04:37:09 localhost.localdomain systemd[1]: Started nginx - high performance web server.
 
和前面的过程一样,在访问站点之前我们需要先配置下hosts文件(真实环境中购买了域名且加了域名的解析可忽略), 具体如下:
192.168.19.128  test.tomcat.com
192.168.19.128  test.node.com
192.168.19.128  test.wordpress.com 
接下来我们看下容器化后的wordpress 站点是否可以正常访问:


容器化小结
 
本文是上篇《小网站的容器化》的续篇,在此我们主要看了下常见的个人站点的容器化过程,并列举了node.js和WordPress 两个站点实例。
既然容器化可以为我们带来这么多的方便之处,那还是建议那些苦于网站更新的小伙伴们尝试一下容器化,同时也借此了解下容器云领域的广阔天地。
 
同时,欢迎所有开发者扫描下方二维码填写《开发者与AI大调研》,只需2分钟,便可收获价值299元的「AI开发者万人大会」在线直播门票!
推荐阅读:
真香,朕在看了!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存