小网站的容器化(下):网站容器化的各种姿势,先跟着撸一波代码再说!
source link: https://blog.csdn.net/FL63Zv9Zou86950w/article/details/105570674
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
作者 | 王洪鹏
责编 | 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 &
[1] 128553 [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开发者万人大会」在线直播门票!
推荐阅读:你知道吗?其实 Oracle 直方图自动统计算法存在这些缺陷!(附验证步骤) 你公司的虚拟机还闲着?基于 Jenkins 和 Kubernetes 的持续集成测试实践了解一下!一站式杀手级 AI 开发平台来袭!告别切换零散建模工具那些神一样的程序员 比特币当赎金,WannaRen 勒索病毒二度来袭!通过 Python 代码实现时间序列数据的统计学预测模型 真香,朕在看了!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK