Centos7.4安装kvm虚拟机(使用virt-manager管理)

之前介绍了使用WebVirtMgr或Openstack来部署及管理kvm虚拟机,下面简单介绍centos7.4下使用virt-manager部署及管理kvm虚拟机的做法:

0)KVM是什么

KVM(Kernel-based Virtual Machine, 即内核级虚拟机) 是一个开源的系统虚拟化模块。它使用Linux自身的调度器进行管理,所以相对于Xen,其核心
源码很少。目前KVM已成为学术界的主流VMM之一,它包含一个为处理器提供底层虚拟化 可加载的核心模块kvm.ko(kvm-intel.ko 或 kvm-amd.ko)。kvm
还需要一个经过修改的QEMU 软件(qemu-kvm),作为虚拟机上层控制和界面。KVM的虚拟化需要硬件支持(如 Intel VT技术或者AMD V技术)。是基于硬件的
完全虚拟化。 KVM可以运行多个其本身运行未改动的镜像的虚拟机,例如Windows,Mac OS X ,每个虚拟机都有各自的虚拟硬件,比如网卡、硬盘核图形适配
器等。

KVM和QEMU的关系
QEMU是个独立的虚拟化解决方案,从这个角度它并不依赖KVM。而KVM是另一套虚拟化解决方案,不过因为这个方案实际上只实现了内核中对处理器(Intel VT)
, AMD SVM)虚拟化特性的支持,换言之,它缺乏设备虚拟化以及相应的用户空间管理虚拟机的工具,所以它借用了QEMU的代码并加以精简,连同KVM一起构成了
另一个独立的虚拟化解决方案:KVM+QEMU。

1)kvm相关安装包及其作用

qemu-kvm          主要的KVM程序包
python-virtinst   创建虚拟机所需要的命令行工具和程序库
virt-manager      GUI虚拟机管理工具
virt-top          虚拟机统计命令
virt-viewer       GUI连接程序,连接到已配置好的虚拟机
libvirt           C语言工具包,提供libvirt服务
libvirt-client    虚拟客户机提供的C语言工具包
virt-install      基于libvirt服务的虚拟机创建命令
bridge-utils      创建和管理桥接设备的工具

2)centos7安装VNC环境

请参考:http://www.cnblogs.com/kevingrace/p/5821450.html

3)安装kvm

1)检查cpu是否支持虚拟化
[root@kevin ~]# grep vmx /proc/cpuinfo
如果有vmx信息输出,就说明支持VT;如果没有任何的输出,说明你的cpu不支持,将无法使用KVM虚拟机。


2)确保BIOS里开启虚拟化功能,即查看是否加载KVM模块
[root@kevin ~]# lsmod | grep kvm
kvm_intel             170086  0
kvm                   566340  1 kvm_intel
irqbypass              13503  1 kvm
=========================================================
如果没有加载,运行以下命令:
[root@kevin ~]# modprobe kvm
[root@kevin ~]# modprobe kvm-intel
[root@kevin ~]# lsmod | grep kvm
kvm_intel             170086  0
kvm                   566340  1 kvm_intel
irqbypass              13503  1 kvm
=========================================================

内核模块导出了一个名为/dev/kvm的设备,这个设备将虚拟机的的地址空间独立于内核或者任何应用程序的地址空间。
[root@kevin ~]# ll /dev/kvm
crw-rw-rw-. 1 root kvm 10, 232 1月  29 11:56 /dev/kvm


3)桥接网络
如果没有brctl命令(用来管理网桥的工具),则需要安装bridge-utils ,
[root@kevin ~]# yum -y install bridge-utils
[root@kevin ~]# systemctl restart network

配置KVM的网桥模式
[root@kevin ~]# cd /etc/sysconfig/network-scripts/
[root@openstack network-scripts]# cp ifcfg-eno1 ifcfg-br0
[root@openstack network-scripts]# cat ifcfg-br0
TYPE="Bridge"                                        //这一行修改为Bridge
PROXY_METHOD="none"
BROWSER_ONLY="no"
BOOTPROTO="none"
DEFROUTE="yes"
IPV4_FAILURE_FATAL="no"
IPV6INIT="no"
IPV6_AUTOCONF="yes"
IPV6_DEFROUTE="yes"
IPV6_FAILURE_FATAL="no"
IPV6_ADDR_GEN_MODE="stable-privacy"
NAME="br0"                                           //修改设备名称为br0
#UUID="fdbad04f-dae3-440e-8a8b-01d6a7bc9fe0"         //这一行注释
DEVICE="br0"                                         //修改设备为br0
ONBOOT="yes"
IPADDR="192.168.10.210"
PREFIX="24"
GATEWAY="192.168.10.1"
DNS1="8.8.8.8"

[root@openstack network-scripts]# cat ifcfg-eno1
TYPE="Ethernet"
BRIDGE=br0                                           //添加这一行
PROXY_METHOD="none"
BROWSER_ONLY="no"
BOOTPROTO="none"
DEFROUTE="yes"
IPV4_FAILURE_FATAL="no"
IPV6INIT="no"
IPV6_AUTOCONF="yes"
IPV6_DEFROUTE="yes"
IPV6_FAILURE_FATAL="no"
IPV6_ADDR_GEN_MODE="stable-privacy"
NAME="eno1"
UUID="fdbad04f-dae3-440e-8a8b-01d6a7bc9fe0"
DEVICE="eno1"
ONBOOT="yes"
#IPADDR="192.168.10.210"                          //注释掉这几行
#PREFIX="24"
#GATEWAY="192.168.10.1"
#DNS1="8.8.8.8"

重启网卡服务
[root@openstack network-scripts]# systemctl restart network

查看网卡
[root@openstack network-scripts]# brctl show
bridge name bridge id   STP enabled interfaces
br0   8000.0894ef518b22 no    eno1
virbr0    8000.52540095d7c2 yes   virbr0-nic

查看ip信息
[root@openstack network-scripts]# ifconfig |head -20
br0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.210  netmask 255.255.255.0  broadcast 192.168.10.255
        inet6 fe80::a94:efff:fe51:8b22  prefixlen 64  scopeid 0x20<link>
        ether 08:94:ef:51:8b:22  txqueuelen 1000  (Ethernet)
        RX packets 856  bytes 52981 (51.7 KiB)
        RX errors 0  dropped 2  overruns 0  frame 0
        TX packets 120  bytes 23450 (22.9 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        ether 08:94:ef:51:8b:22  txqueuelen 1000  (Ethernet)
        RX packets 10077  bytes 793083 (774.4 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1184  bytes 228415 (223.0 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        device interrupt 16 

eno2: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether 08:94:ef:51:8b:23  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)


[root@openstack network-scripts]# ping www.baidu.com
PING www.a.shifen.com (61.135.169.121) 56(84) bytes of data.
64 bytes from 61.135.169.121 (61.135.169.121): icmp_seq=1 ttl=55 time=2.08 ms
64 bytes from 61.135.169.121 (61.135.169.121): icmp_seq=2 ttl=55 time=1.80 ms
........


4)安装libvirt及kvm
libvirt是管理虚拟机的API库,不仅支持KVM虚拟机,也可以管理Xen等方案下的虚拟机。
[root@kevin ~]#  yum -y install libcanberra-gtk2 qemu-kvm.x86_64 qemu-kvm-tools.x86_64  libvirt.x86_64 libvirt-cim.x86_64 libvirt-client.x86_64
libvirt-java.noarch  libvirt-python.x86_64 libiscsi-1.7.0-5.el6.x86_64  dbus-devel  virt-clone tunctl virt-manager libvirt libvirt-python python-virtinst

由于要用virt-manager图形化安装虚拟机,所以还需要安装X-window(这个在前面部署VNC环境里就已经安装了)
[root@kevin ~]# yum groupinstall "X Window System"

安装中文字符,解决界面乱码问题
[root@kevin ~]# yum install -y dejavu-lgc-sans-fonts
[root@kevin ~]# yum groupinstall -y "Fonts"

启用libvirt
[root@kevin ~]# systemctl enable libvirtd
[root@kevin ~]# systemctl start libvirtd

4)使用virt-manager管理kvm(通过VNC连接服务器)

提前将ISO系统镜像存放到服务器的一个目录里,比如/data/iso

[root@openstack ~]# mkdir /data/iso
[root@openstack ios]# ll
总用量 3356384
-rw-r--r--. 1 qemu qemu 3436937216 1月  29 11:41 win-server2008_R2.iso

未分类

未分类

未分类

未分类

未分类

未分类

5)解决KVM虚拟机在使用vnc连接时鼠标不同步的问题

在VNC界面中感觉virt-manager管理的虚拟机界面总是鼠标跟不上,指到哪儿也看不出来,界面上一直显示press control_l+a/t_l来移动鼠标!十分郁闷!
想要修改鼠标和宿主机界面同步方法如下:

[root@openstack ~]# cd /etc/libvirt/qemu
[root@openstack qemu]# ls
networks  test-win2008.xml
[root@openstack qemu]# cp test-win2008.xml /opt/
[root@openstack qemu]# vim test-win2008.xml               //在<devices>标签中添加下面这段配置
<devices>   
......                                      
  <input type='tablet' bus='usb'/>                        //即添加这句话即可!
......    
</devices>

[root@openstack qemu]# virsh define /etc/libvirt/qemu/test-win2008.xml
定义域 test-win2008(从 /etc/libvirt/qemu/test-win2008.xml)

然后重启虚拟机后,发现虚拟机中的鼠标就会好事了,打开VNC查看虚拟机界面后默认情况下虚拟机中的鼠标指针和实体机的鼠标指针就是重合的,且两者运动速度也是同步的,
这下就彻底解决了鼠标指针漂移/不同步的情况了!

6)重定向USB设备

(即将宿主机上的USB设备指定到目标虚拟机上)。适合挂载银行前置机设备!

先将USB设备插到宿主机上,接着在virt-manager界面里打开虚拟机,然后虚拟机界面上栏里打开”虚拟机”->”重定向USB设备(R)”。(特别注意:如下选择将usb设备挂载到虚拟机上后,不要关闭这个挂载界面,否则usb挂载动作就会结束!)

未分类

未分类

查看虚拟机,发现指定的USB设备已经挂载到该虚拟机上了!

未分类

如果要卸载该虚拟机上挂载的这个USB设备,即在”重定向设备usb(R)”里将这个USB设备去掉,然后”确定”即可!

未分类

未分类

在kubernetes集群中创建redis主从多实例

继续使用上次实验环境 https://www.58jb.com/html/180.html ,因为环境中已经配置好flannel网络了,接下要就是慢慢在此环境中创建一些实例。因为只是搭建简单的环境是比较容易上手,随便网上一搜可能就出来了。但是要自己去从头一步一步跑起项目,还是基于真实的项目来跑的话还是需要自己多研究,当然目前还是止于使用阶段,要深入还得多看资料。

未分类

本次要记录的是自己从创建镜像到实现redis主从示例,为什么要自己制作镜像呢?其实就是为了自己可以学多点,毕竟网上的都人别人做好的,你自己没做过根本不知道制作过程中会遇到什么。为什么有些镜像大,有些镜像比较小。这都是在哪里做的优化?
本次直接使用alpine基础镜像,因为它小,加上个redis也就11M左右的镜像,如果使用centos可能就大点了,这里先不做存储的方便,只是简单的实现redis-master和redis-slave之间的自动发现。
为了自己好区分我直接制作两个镜像,一个为redis-master专门用来跑master服务的,一个为redis-slave专门跑slave实例的。其实可以只用一个,只需分两个配置文件或者是两个不同的启动方法,这里不做这个介绍。

redis-slave镜像制作

需要二个文件:Dockerfile、run.sh

Dockerfile内容如下:

FROM alpine:3.4  
RUN apk add --no-cache redis sed bash  
COPY run.sh /run.sh  
CMD [ "/run.sh" ]  
ENTRYPOINT [ "bash" ]  

其实默认可以不使用redis.conf文件的,如果需要就在上面的Dockerfile文件也加入,这里目前也处于测试,先不加配置。

run.sh启动脚本,也就是一行文件。

[root@k8s-node1 slave]# cat run.sh 
#/bin/sh 
redis-server --slaveof ${REDIS_MASTER_SERVICE_HOST} ${REDIS_MASTER_SERVICE_PORT} 

打包成镜像传到内网仓库:

docker build -t redis-slave . 
docker tag redis-slave reg.docker.tb/harbor/redis-slave 
docker push reg.docker.tb/harbor/redis-slave 

redis-master镜像制作

跟上面的配置差不多一样,只是启动时的命令有点不而已。
Dockerfile内容如下:

FROM alpine:3.4  
RUN apk add --no-cache redis sed bash  
COPY run.sh /run.sh  
CMD [ "/run.sh" ]  
ENTRYPOINT [ "bash" ]  

其实默认可以不使用redis.conf文件的,如果需要就在上面的Dockerfile文件也加入,这里目前也处于测试,先不加配置。

run.sh启动脚本,也就是一行文件。

[root@k8s-node1 master]# cat run.sh 
#/bin/sh 
redis-server

打包成镜像传到内网仓库:

docker build -t redis-master . 
docker tag redis-master reg.docker.tb/harbor/redis-master 
docker push reg.docker.tb/harbor/redis-master 

创建kube的配置文件yaml

创建一个master-service.yaml 来统一Master的入口,同时会自动关联到labels为redis-master的所有Pod,这里要注意Service要优于pod启动,不然无法通过环境变量把配置信息写入到pod中。

apiVersion: v1 
kind: Service 
metadata: 
  name: redis-master 
  labels: 
    name: redis-master 
spec: 
  ports: 
  - port: 6379 
    targetPort: 6379 
  selector: 
    name: redis-master 

redis-master.yaml 以rc的方式来创建一个master容器,它会保证容器的副本数量。

apiVersion: v1 
kind: ReplicationController 
metadata: 
  name: redis-master 
  labels: 
    name: redis-master 
spec: 
  replicas: 1 
  selector: 
    name: redis-master 
  template: 
    metadata: 
      labels: 
        name: redis-master 
    spec: 
      containers: 
      - name: master 
        image: reg.docker.tb/harbor/redis-master 
        ports: 
        - containerPort: 6379 

redis-slave-service.yaml跟上面的Service一样,都是管理redis-slave所有容器的,此服务创建后会把所有的slave实例的容器分配一个集群IP.

apiVersion: v1 
kind: Service 
metadata: 
  name: redis-slave 
  labels: 
    name: redis-slave 
spec: 
  ports: 
  - port: 6379 
  selector: 
    name: redis-slave 

redis-slave.yaml 同样是以RC的方式来创建redis-slave实例,这里的数量为两个。需要注意的是Image这里指定是刚才打包的镜像名。

apiVersion: v1 
kind: ReplicationController 
metadata: 
  name: redis-slave 
  labels: 
    name: redis-slave 
spec: 
  replicas: 2 
  selector: 
    name: redis-slave 
  template: 
    metadata: 
      labels: 
        name: redis-slave 
    spec: 
      containers: 
      - name: worker 
        image: reg.docker.tb/harbor/redis-slave 
        env: 
        - name: GET_HOSTS_FROM 
          value: env 
#value: dns
        ports: 
        - containerPort: 6379 

注意:要实现master和slave服务自动发现,需要配置它们之间的对应关系。Kubernetes有两种方法就是环境变量ENV和DNS记录解析,因为我的实验环境没有使用这个DNS来解析,所以只能使用ENV环境变量。

上面的redis-slave.yaml 就是使用了env环境变量。

接下来创建实例吧:

master: 
kubectl create -f redis-service.yaml 
kubectl create -f redis-master.yaml 

slave: 
kubectl create -f redis-slave-service.yaml 
kubectl create -f redis-slave.yaml 

查看启动的效果:

[root@k8s-master redis-pod]# kubectl get all -o wide 
NAME                    READY     STATUS    RESTARTS   AGE       IP           NODE 
po/redis-master-qpzlh   1/1       Running   0          1h        172.21.2.2   k8s-node1 
po/redis-slave-2rwk5    1/1       Running   0          1h        172.21.2.3   k8s-node1 
po/redis-slave-6tf2f    1/1       Running   0          1h        172.21.8.3   k8s-node2 

NAME              DESIRED   CURRENT   READY     AGE       CONTAINERS   IMAGES                              SELECTOR 
rc/redis-master   1         1         1         1h        master       reg.docker.tb/harbor/redis-master   name=redis-master 
rc/redis-slave    2         2         2         1h        worker       reg.docker.tb/harbor/redis-slave    name=redis-slave 

NAME               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE       SELECTOR 
svc/kubernetes     ClusterIP   10.254.0.1       <none>        443/TCP    3d        <none> 
svc/redis-master   ClusterIP   10.254.152.116   <none>        6379/TCP   1h        name=redis-master 
svc/redis-slave    ClusterIP   10.254.188.60    <none>        6379/TCP   1h        name=redis-slave 

简单的测试master和slave是否可以同步数据:
连接master容器,可以看到有两个Replication中有两个connected_slaves就是从实例,后面跟着的就是IP地址。

[root@k8s-master redis-pod]# kubectl exec -it redis-master-qpzlh -- /bin/bash 
bash-4.3# redis-cli                                                                                                                                          
127.0.0.1:6379> info 
# Server 
redis_version:3.2.11 
redis_git_sha1:535782f7 
redis_git_dirty:0 
redis_build_id:80ce8a1f388ac530 
redis_mode:standalone 
os:Linux 4.4.113-1.el7.elrepo.x86_64 x86_64 
arch_bits:64 
multiplexing_api:epoll 
gcc_version:5.3.0 
process_id:5 
run_id:6629f021c2e5bd972fff05c84566cc92c7e59be0 
tcp_port:6379 
uptime_in_seconds:7105 
uptime_in_days:0 

...... 
# Clients 
connected_clients:2 
client_longest_output_list:0 
client_biggest_input_buf:0 
blocked_clients:0 
...... 

# Replication 
role:master 
connected_slaves:2 
slave0:ip=172.21.2.3,port=6379,state=online,offset=5276,lag=1 
slave1:ip=172.21.8.0,port=6379,state=online,offset=5276,lag=1 
master_repl_offset:5276 
repl_backlog_active:1 
repl_backlog_size:1048576 
repl_backlog_first_byte_offset:2 
repl_backlog_histlen:5275 

连接一个slave容器查看信息,可以看到Replication中role:slave为从实例,master的IP和端口等信息。

[root@k8s-master ~]# kubectl exec -it redis-slave-2rwk5 -- /bin/bash 
bash-4.3# redis-cli                                                                                                                                          
127.0.0.1:6379> info 
# Server 
redis_version:3.2.11 
redis_git_sha1:535782f7 
redis_git_dirty:0 
redis_build_id:80ce8a1f388ac530 
redis_mode:standalone 
os:Linux 4.4.113-1.el7.elrepo.x86_64 x86_64 
arch_bits:64 
multiplexing_api:epoll 
gcc_version:5.3.0 
process_id:5 
run_id:d48c42ea0a83babda663837ed5a6f9ef5a3ff9bf 
tcp_port:6379 
uptime_in_seconds:3957 
uptime_in_days:0 

...... 
# Replication 
role:slave 
master_host:10.254.152.116 
master_port:6379 
master_link_status:up 
master_last_io_seconds_ago:7 
master_sync_in_progress:0 
slave_repl_offset:5542 
slave_priority:100 
slave_read_only:1 
connected_slaves:0 
master_repl_offset:0 
repl_backlog_active:0 
repl_backlog_size:1048576 
repl_backlog_first_byte_offset:0 
repl_backlog_histlen:0 

此时在Master上创建一个key, 在slave上立马就可以获取到了。

master: 
127.0.0.1:6379> set name swper 
OK 

slave: 
127.0.0.1:6379> get name 
"swper" 

slave上只读: 
127.0.0.1:6379> set test 1 
(error) READONLY You can't write against a read only slave. 

这样就实现了一主多从的效果了,集群的IP现在是外部无法访问的,kubernetes节点中可以相互连通。
其实原本是很简单的实验,我居然在传递参数的时候少了个$号导致一直获取不到。

kubernetes下的Nginx加Tomcat三部曲之三:实战扩容和升级

本章是《kubernetes下的Nginx加Tomcat三部曲系列》的终篇,今天咱们一起在kubernetes环境对下图中tomcat的数量进行调整,再修改tomcat中web工程的源码,并将现有的tomcat的pod全部替换成新代码构建的结果:

未分类

往期章节

  1. kubernetes下的Nginx加Tomcat三部曲之一: http://devops.webres.wang/2018/02/kubernetes%E4%B8%8B%E7%9A%84nginx%E5%8A%A0tomcat%E4%B8%89%E9%83%A8%E6%9B%B2%E4%B9%8B%E4%B8%80%EF%BC%9A%E6%9E%81%E9%80%9F%E4%BD%93%E9%AA%8C/
  2. kubernetes下的Nginx加Tomcat三部曲之二: http://devops.webres.wang/2018/02/kubernetes%E4%B8%8B%E7%9A%84nginx%E5%8A%A0tomcat%E4%B8%89%E9%83%A8%E6%9B%B2%E4%B9%8B%E4%BA%8C%EF%BC%9A%E7%BB%86%E8%AF%B4%E5%BC%80%E5%8F%91/

列举步骤

  1. 在线扩容Tomcat;
  2. 验证扩容结果;
  3. 修改web工程源码;
  4. 构建web工程的新镜像;
  5. 让kubernetes的机器用上web工程的Docker镜像
  6. 在线更新Tomcat的pod的镜像;
  7. 验证更新结果;
  8. Nginx&Tomcat方式和SpringCloud方式扩容对比;

kubernetes环境基本情况

  1. 一个master,一个node;
  2. master的IP地址:192.168.119.148;
  3. node1的IP地址:192.168.119.153;

在线扩容Tomcat

  • 在装好kubectl工具的机器上执行kubectl get pods,查看当前pod情况:
root@master:~# kubectl get pods
NAME                          READY     STATUS    RESTARTS   AGE
ng-59b887b8bc-jzcv5           1/1       Running   3          2d
tomcathost-7f68566795-8pl29   1/1       Running   3          2d
tomcathost-7f68566795-mvg5f   1/1       Running   3          2d
tomcathost-7f68566795-trscg   1/1       Running   3          2d

如上所示,目前是三个tomcat的pod;

  • 执行扩容命令kubectl scale deployment tomcathost –replicas=5,将tomcat的pod从3个增加到5个,如下:
root@master:~# kubectl scale deployment tomcathost --replicas=5
deployment "tomcathost" scaled
root@master:~# kubectl get pods
NAME                          READY     STATUS    RESTARTS   AGE
ng-59b887b8bc-jzcv5           1/1       Running   3          2d
tomcathost-7f68566795-8kf76   1/1       Running   0          18s
tomcathost-7f68566795-8pl29   1/1       Running   3          2d
tomcathost-7f68566795-mvg5f   1/1       Running   3          2d
tomcathost-7f68566795-tp5xp   1/1       Running   0          18s
tomcathost-7f68566795-trscg   1/1       Running   3          2d

tomcat的pod已经增加到5个了,创建时间都只有18秒;

  • 去dashboard页面看以下tomcathost这个deployment的详情,可以看到tomcat数量已经上升到5个了,扩容成功:

未分类

点击上图红框1,再点击tomcathost这个服务,可以看到上图中的详情;
上图红框2中是5个tomcat的pod的容器IP地址;
点击上图红框3中的每个容器名,可以查看容器详情;
上图红框4显示了刚才的扩容事件;

验证扩容结果

nginx服务所在的node机器的IP地址是192.168.119.153,所以在浏览器上访问:http://192.168.119.153:30006/getserverinfo,反复刷新此页面,看到返回的IP地址在不断的更新,都是tomcat所在pod的IP地址,5个都会出现,如下图:

未分类

扩容实战就到这里,接下来我们修改web工程的源码,验证在线升级的能力;

修改web工程源码

tomcat上运行的web工程源码,可以GitHub下载,地址和链接信息如下表所示:

未分类

这个git项目中有多个目录,本次的web工程源码放在k8stomcatdemo,如下图红框所示:

未分类

  • 打开工程中的ServerInfo.java,web接口的源码如下:
@RequestMapping(value = "/getserverinfo", method = RequestMethod.GET)
    public String getUserInfoWithRequestParam(){
        return String.format("server : %s, time : %s", getIPAddr(), new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    }
  • 修改返回的字符串内容,在前面加上”From new version,”:
@RequestMapping(value = "/getserverinfo", method = RequestMethod.GET)
    public String getUserInfoWithRequestParam(){
        return String.format("From new version, server : %s, time : %s", getIPAddr(), new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    }

构建web工程的新镜像

修改pom.xml中的工程版本,从0.0.1-SNAPSHOT改为0.0.3,如下所示:

<groupId>com.bolingcavalry</groupId>
    <artifactId>k8stomcatdemo</artifactId>
    <version>0.0.3</version>
    <packaging>jar</packaging>
    <name>k8stomcatdemo</name>
  • 在pom.xml所在目录执行mvn clean package -DskipTests docker:build,会编译构建工程,并且在本地制作好镜像文件,如下:
root@maven:/usr/local/work/k8s/k8stomcatdemo# docker images
REPOSITORY                                              TAG                 IMAGE ID            CREATED             SIZE
bolingcavalry/k8stomcatdemo                             0.0.3               5f2d27eafd1b        17 seconds ago      658 MB
bolingcavalry/k8stomcatdemo                             latest              5f2d27eafd1b        17 seconds ago      658 MB

让kubernetes的机器用上web工程的Docker镜像

现在的镜像只存在于开发和构建web工程的电脑上,为了让kubernetes的node机器能用上这个镜像,可以用以下几种方式实现:

1、用docker push命令将本机镜像推送到hub.docker.com网站,这样其他机器都可以通过docker pull命令取得了,我就是用的这种方法,需要在hub.docker.com上注册,下图是正在push中:

未分类

2、用docker save命令导出镜像文件,再用docker load命令导入;
3、kubernetes所在机器安装java和maven环境,将工程在这里编译构建;
4、使用docker私有仓库,例如搭建局域网私有仓库或者阿里云私有仓库,参考 http://blog.csdn.net/boling_cavalry/article/details/78934391

在线更新Tomcat的pod的镜像

  • 在kubernetes上执行以下命令,即可升级镜像,当前的5个tomcat的pod会被销毁,然后用新的镜像创建pod:
kubectl set image deployment/tomcathost tomcathost=bolingcavalry/k8stomcatdemo:0.0.3
  • 控制台提示的信息如下:
root@maven:/usr/local/work/k8s/k8stomcatdemo# kubectl set image deployment/tomcathost tomcathost=bolingcavalry/k8stomcatdemo:0.0.3
deployment "tomcathost" image updated
  • 此刻反复执行kubectl get pod,会看到新容器创建,旧容器正在被销毁,如下:
root@maven:/usr/local/work/k8s/k8stomcatdemo# kubectl get pod
NAME                          READY     STATUS              RESTARTS   AGE
ng-59b887b8bc-jzcv5           1/1       Running             3          2d
tomcathost-6dfc87dc8b-9bkfv   1/1       Running             0          50s
tomcathost-6dfc87dc8b-h6gx4   0/1       ContainerCreating   0          50s
tomcathost-6dfc87dc8b-ht2d8   1/1       Running             0          18s
tomcathost-6dfc87dc8b-pfb56   1/1       Running             0          10s
tomcathost-6dfc87dc8b-x8pnn   1/1       Running             0          14s
tomcathost-7f68566795-8pl29   0/1       Terminating         3          2d
tomcathost-7f68566795-trscg   0/1       Terminating         3          2d

验证更新结果

  • nginx服务所在的node机器的IP地址是192.168.119.153,所以在浏览器上访问:http://192.168.119.153:30006/getserverinfo,可以看到”From new version”这个字符串,如下图:

未分类

去dashboard页面看服务详情,可以看见一系列的缩容和扩容事件,如下图:

未分类

Nginx&Tomcat方式和SpringCloud方式扩容对比

在之前的文章中,我们实战了SpringCloud环境下服务提供方的扩容,由于是“注册&发现”的方式,扩容只需要往SpringCloud环境添加provider机器,不需要做什么设置,请求就会被落到新的provider上,相关实战的地址如下:

  1. Docker下的Spring Cloud三部曲之一: http://blog.csdn.net/boling_cavalry/article/details/79177930

  2. Docker下的Spring Cloud三部曲之二: http://blog.csdn.net/boling_cavalry/article/details/79134497

  3. Docker下的Spring Cloud三部曲之三: http://blog.csdn.net/boling_cavalry/article/details/79192376

在Nginx加Tomcat环境中,如果不是在kubernetes环境,我们增加Tomcat之后需要修改Nginx配置,否则到达Nginx的请求不会被转发到新的机器上去,幸运的是kubernete帮我们解决了这个问题,扩容缩容都只需要控制副本数即可,不用修改Nginx配置了;

doceker-compose虽然可以修改副本数,但是Nginx的配置仍然需要修改,否则新创建的Tomcat容器都有自己的IP地址,Nginx还是感知不到;

至此,kubernetes下的Nginx加Tomcat三部曲就全部结束了,希望能在您的kubernetes实战中有所帮助;

kubernetes下的Nginx加Tomcat三部曲之二:细说开发

本文是《kubernetes下的Nginx加Tomcat三部曲》的第二章,在 http://devops.webres.wang/2018/02/kubernetes%E4%B8%8B%E7%9A%84nginx%E5%8A%A0tomcat%E4%B8%89%E9%83%A8%E6%9B%B2%E4%B9%8B%E4%B8%80%EF%BC%9A%E6%9E%81%E9%80%9F%E4%BD%93%E9%AA%8C/ 一文我们快速部署了Nginx和Tomcat,达到以下效果:

未分类

本文我会详细说明在kubernetes部署上述网络服务时,需要做哪些具体的工作;

列举步骤

需要以下操做才能在kubernetes上部署Nginx加Tomcat的服务:

  1. 开发Tomcat上的web工程和Docker镜像;
  2. 让kubernetes的机器用上web工程的Docker镜像;
  3. 开发Tomcat对应的deployment脚本;
  4. 开发Tomcat对应的service脚本;
  5. 开发Nginx对应的Docker镜像;
  6. 让kubernetes的机器用上Nginx的Docker镜像
  7. 开发Nginx对应的deployment脚本;
  8. 开发Nginx对应的service脚本;
  9. 开发启动上述pod和service的脚本;
  10. 开发停止并清除上述pod和service的脚本

脚本文件下载

本次体验所需的deployment和service资源是通过脚本创建的,这个脚本可以通过以下两种方式中的任意一种下载:

  1. CSDN下载(无法设置免费下载,只能委屈您用掉两个积分了):http://download.csdn.net/download/boling_cavalry/10235034
  2. GitHub下载,地址和链接信息如下表所示:

未分类

这个git项目中有多个目录,本次所需的资源放在k8s_nginx_tomcat_resource,如下图红框所示:

未分类

下到的k8stomcatcluster20180201.tar是个压缩包,复制到可以执行kubectl命令的ubuntu电脑上,然后解压开,是个名为k8stomcatcluster的文件夹;

Spring boot的web工程源码下载

GitHub下载,地址和链接信息如下表所示:

未分类

这个git项目中有多个目录,本次的web工程源码放在k8stomcatdemo,如下图红框所示:

未分类

接下来我们开始实战开发吧;

开发环境

本次实战开发环境的具体信息如下:

  1. 操作系统:Ubuntu16;
  2. Docker版本:17.03.2-ce;
  3. JDK:1.8.0_151;
  4. maven:3.3.3;

Tomcat上的web工程和Docker镜像

web工程用来提供http服务,返回当前机器的IP地址给浏览器,完整源码请参照前面下载的k8stomcatdemo工程,这里我们还是重新创建一次;

1、创建一个springboot工程,pom.xml内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bolingcavalry</groupId>
    <artifactId>k8stomcatdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>k8stomcatdemo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!--新增的docker maven插件-->
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>0.4.12</version>
                <!--docker镜像相关的配置信息-->
                <configuration>
                    <!--镜像名,这里用工程名-->
                    <imageName>bolingcavalry/${project.artifactId}</imageName>
                    <!--TAG,这里用工程版本号-->
                    <imageTags>
                        <imageTag>${project.version}</imageTag>
                    </imageTags>
                    <!--镜像的FROM,使用java官方镜像-->
                    <baseImage>java:8u111-jdk</baseImage>
                    <!--该镜像的容器启动后,直接运行spring boot工程-->
                    <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
                    <!--构建镜像的配置信息-->
                    <resources>
                        <resource>
                            <targetPath>/</targetPath>
                            <directory>${project.build.directory}</directory>
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

可以看到这是个普通的springboot的web工程,唯一不同的是多了一个maven插件:docker-maven-plugin,使用该插件我们可以将maven工程制作成docker镜像;

2、整个工程只有一个Controller,开通一个http接口,将当前服务器IP地址返回,源码如下:

@RestController
public class ServerInfo {

    @RequestMapping(value = "/getserverinfo", method = RequestMethod.GET)
    public String getUserInfoWithRequestParam(){
        return String.format("server : %s, time : %s", getIPAddr(), new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    }

    /**
     * 获取本机IP地址
     * @return
     */
    private static String getIPAddr(){
        String hostAddress = null;
        try{
            InetAddress address = InetAddress.getLocalHost();
            hostAddress = address.getHostAddress();
        }catch (UnknownHostException e){
            e.printStackTrace();
        }

        return hostAddress;
    }
}

以上就是工程的关键源码;

3、在pom.xml所在目录执行mvn clean package -DskipTests docker:build,会编译构建工程,并且在本地制作好镜像文件,如下:

root@maven:/usr/local/work/github/blog_demos# docker images
REPOSITORY                                              TAG                 IMAGE ID            CREATED             SIZE
bolingcavalry/k8stomcatdemo                             latest              1d41d9980a0b        43 hours ago        658 MB
bolingcavalry/k8stomcatdemo                             0.0.1-SNAPSHOT      c91bc368a729        46 hours ago        658 MB

想了解更多maven制作docker镜像的细节,请看 http://blog.csdn.net/boling_cavalry/article/details/78872020

让kubernetes的机器用上web工程的Docker镜像

现在的镜像只存在于开发和构建web工程的电脑上,为了让kubernetes的node机器能用上这个镜像,可以用以下几种方式实现:

  1. 用docker push命令将本机镜像推送到hub.docker.com网站,这样其他机器都可以通过docker pull命令取得了,我就是用的这种方法,需要在hub.docker.com上注册;
  2. 用docker save命令导出镜像文件,再用docker load命令导入;
  3. kubernetes所在机器安装java和maven环境,将工程在这里编译构建;
  4. 使用docker私有仓库,例如搭建局域网私有仓库或者阿里云私有仓库,参考 http://blog.csdn.net/boling_cavalry/article/details/78934391

Tomcat对应的deployment脚本

用yaml文件将详情配置好,再用kubectl命令执行这个配置就能创建pod,这个web应用镜像的配置文件名为tomcat.yaml,内容如下:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: tomcathost
spec:
  replicas: 3
  template:
    metadata:
     labels:
       name: tomcathost
    spec:
     containers:
     - name: tomcathost
       image: bolingcavalry/k8stomcatdemo:0.0.1-SNAPSHOT
       tty: true
       ports:
       - containerPort: 8080

将上述脚本的几个关键点列举一下:

  1. version用extensions/v1beta1;
  2. kind用Deployment,支持升级镜像和滚动升级;
  3. 使用的镜像bolingcavalry/k8stomcatdemo:0.0.1-SNAPSHOT,是我从本地push到hub.docker.com上去的;
  4. 创建的容器对外暴露了8080端口;

Tomcat对应的service脚本

创建了tomcat的pod之后,为了能在kubernetes环境中给其他service使用,需要将这些pod包装为service,这里是通过tomcat-svc.yaml文件来配置的,内容如下:

apiVersion: v1
kind: Service
metadata:
  name: tomcathost
spec:
  type: ClusterIP
  ports:
       - port: 8080
  selector:
    name: tomcathost

将上述脚本的几个关键点列举一下:

  1. 服务对应的pod是tomcathost;
  2. type用ClusterIP,为内部service调用提供统一IP地址;
  3. 服务对外暴露了pod的8080端口;

Nginx对应的Docker镜像

  • 定制的Nginx镜像和Nginx官方镜像相比,唯一的区别就是nginx.conf文件不同,我们用的nginx.conf内容如下:
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    #include /etc/nginx/conf.d/*.conf;

    upstream tomcat_client {
         server tomcathost:8080;
    } 

    server {
        server_name "";
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;

        location / {
            proxy_pass http://tomcat_client;
            proxy_redirect default;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

以上配置中,新增的upstream对应的IP地址是tomcathost,这是tomcat的service名称,在Nginx运行的时候,通过tomcathost就能访问到tomcat的Pod;

  • 制作Docker镜像的Dockerfile文件内容如下,每行都有注释就不再多说了:
# First docker file from bolingcavalry
# VERSION 0.0.1
# Author: bolingcavalry

#基础镜像
FROM nginx:stable

#作者
MAINTAINER BolingCavalry <[email protected]>

#定义工作目录
ENV WORK_PATH /etc/nginx

#定义conf文件名
ENV CONF_FILE_NAME nginx.conf

#删除原有配置文件
RUN rm $WORK_PATH/$CONF_FILE_NAME

#复制新的配置文件
COPY ./$CONF_FILE_NAME $WORK_PATH/

#给shell文件赋读权限
RUN chmod a+r $WORK_PATH/$CONF_FILE_NAME

将nginx.conf和Dockerfile放在同一个目录,然后执行命令docker build -t bolingcavalry/nginx-with-tomcat-host:0.0.1 .,就能构建镜像文件了,如下:

root@maven:/usr/local/work/nginx# docker build -t bolingcavalry/nginx-with-tomcat-host:0.0.1 .
Sending build context to Docker daemon 14.51 MB
Step 1/7 : FROM nginx:stable
 ---> dfe062ee1dc8
Step 2/7 : MAINTAINER BolingCavalry <[email protected]>
 ---> Using cache
 ---> 93f4bf154c55
Step 3/7 : ENV WORK_PATH /etc/nginx
 ---> Using cache
 ---> d0158757fc9c
Step 4/7 : ENV CONF_FILE_NAME nginx.conf
 ---> Using cache
 ---> 7a18a8b417d6
Step 5/7 : RUN rm $WORK_PATH/$CONF_FILE_NAME
 ---> Using cache
 ---> f6f27d25539d
Step 6/7 : COPY ./$CONF_FILE_NAME $WORK_PATH/
 ---> Using cache
 ---> 33075a2b0379
Step 7/7 : RUN chmod a+r $WORK_PATH/$CONF_FILE_NAME
 ---> Using cache
 ---> 58ce530e160b
Successfully built 58ce530e160b

让kubernetes的机器用上Nginx的Docker镜像

这一步和之前的web工程的镜像放到kubernetes一样,有多种方式,我用的还是通过docker push推送到hub.docker.com网站,再在kubernetes上pull下来;

Nginx对应的deployment脚本

用yaml文件将详情配置好,再用kubectl命令执行这个配置就能创建pod,这个web应用镜像的配置文件名为nginx.yaml,内容如下:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ng
spec:
  replicas: 1
  template:
    metadata:
     labels:
      name: ng
    spec:
     containers:
     - name: ng
       image: bolingcavalry/nginx-with-tomcat-host:0.0.1
       ports:
       - containerPort: 80

以上配置中有几点要注意:

  1. 使用镜像是刚才创建的nginx镜像bolingcavalry/nginx-with-tomcat-host:0.0.1;
  2. pod容器创建后,对外暴露80端口;

Nginx对应的service脚本

通过service定义的yaml文件nginx-svc.yam,将前面创建的nginx的pod包装为service:

apiVersion: v1
kind: Service
metadata:
  name: ng
spec:
  type: NodePort
  ports:
       - port: 80
         nodePort: 30006
  selector:
    name: ng

以上配置中有几点要注意:

  1. type使用NodePort,这样可以通过节点机器的IP访问此service;
  2. 将节点机器的30006端口和pod的80端口绑定,因此,外部访问节点IP+30006端口就能访问到此Nginx服务了;

启动上述pod和service的脚本

接下来我们创建一个shell脚本start_all.sh,将上述的tomcat和nginx的pod以及service全部创建和启动:

kubectl create -f tomcat.yaml
kubectl create -f tomcat-svc.yaml
kubectl create -f nginx.yaml
kubectl create -f nginx-svc.yaml
echo ""
echo "nginx and tomcat running now"

如上所示,通过kubectl create -f加文件名,就能创建好yaml文件中定义的pod和service;

停止并清除上述pod和service的脚本

创建一个shell脚本stop_all.sh,能够将上述的tomcat和nginx的pod以及service全部清除掉:

kubectl delete service tomcathost
kubectl delete deployment tomcathost
kubectl delete service ng
kubectl delete deployment ng
echo "nginx and tomcat stop now"

如上所示,其实就是通过kubectl delete命令将指定的pod和service资源删除;

以上就是在kubernetes搭建整个Nginx加Tomcat环境的所有资源,您就可以用这些像 http://devops.webres.wang/2018/02/kubernetes%E4%B8%8B%E7%9A%84nginx%E5%8A%A0tomcat%E4%B8%89%E9%83%A8%E6%9B%B2%E4%B9%8B%E4%B8%80%EF%BC%9A%E6%9E%81%E9%80%9F%E4%BD%93%E9%AA%8C/ 文中那样去搭建和体验kubernetes下的Nginx加Tomcat;

下一章,我们会在此环境的基础上实战Tomcat服务的扩容,并修改web工程的代码,再创建新的镜像,并且将kubernetes环境下在线升级新的web工程服务;

kubernetes下的Nginx加Tomcat三部曲之一:极速体验

章节介绍

在生产环境中,常用到Nginx加Tomcat的部署方式,如下图:

未分类

从本章开始,我们来实战kubernetes下部署上述Nginx和Tomcat服务,并开发spring boot的web应用来验证环境,整个实战分为以下三篇内容:

  1. 极速体验kubernetes下的nginx加tocmat;
  2. 细说nginx和tomcat镜像的制作;
  3. 实战tomcat server的在线扩容和应用升级;

实战工程介绍

本次实战创建的Pod如下:

  1. 一个Nginx的Pod,负责转发web请求到Tomcat;
  2. 三个Tomcat的Pod,上面部署了web应用,收到Nginx转发的请求后,返回的内容是当前Pod的IP地址;

准备kubernetes环境

本次实战需要可用的kubernetes环境,您可以参考以下文章进行快速搭建:

  1. http://blog.csdn.net/boling_cavalry/article/details/78762829
  2. http://blog.csdn.net/boling_cavalry/article/details/78764915

如何执行kubectl命令

实战中,需要在一台ubuntu电脑上安装kubectl工具,然后连接到kubernetes环境执行各种命令,kubectl工具的安装步骤可以参照这篇文章: http://blog.csdn.net/boling_cavalry/article/details/79223091

脚本文件下载

本次体验所需的deployment和service资源是通过脚本创建的,这个脚本可以通过以下两种方式中的任意一种下载:

  1. CSDN下载(无法设置免费下载,只能委屈您用掉两个积分了):http://download.csdn.net/download/boling_cavalry/10235034
  2. GitHub下载,地址和链接信息如下表所示:

未分类

这个git项目中有多个目录,本次所需的资源放在k8s_nginx_tomcat_resource,如下图红框所示:

未分类

下到的k8stomcatcluster20180201.tar是个压缩包,复制到可以执行kubectl命令的ubuntu电脑上,然后解压开,是个名为k8stomcatcluster的文件夹;

执行脚本文件下载

  1. 进入解压好的k8stomcatcluster目录;
  2. 执行命令chmod a+x *.sh,给shell脚本赋可执行权限;3.
    执行命令start_all.sh,创建本次实战的资源,页面输出如下信息:
root@maven:/usr/local/work/k8s/k8stomcatcluster# ./start_all.sh 
deployment "tomcathost" created
service "tomcathost" created
deployment "ng" created
service "ng" created

nginx and tomcat running now

验证服务已经启动

  • 先去kubernetes的管理页面看一下服务是否启动,如下图,名为ng、tomcathost的两个服务都已经启动:

未分类

  • 点击tomcathost服务,看到详情信息,里面有pod的情况,如下图:

未分类

  • 上图中显示tomcathost是在node1创建的,我的node1机器的IP地址是192.168.119.153,所以在浏览器输入:
    http://192.168.119.153:30006/getserverinfo

  • 在浏览器看到的信息如下图所示,机器Tomcat所在机器的IP地址和当前时间:

未分类

  • 多次刷新页面,能看到这三个IP地址:10.42.38.128、10.42.184.35、10.42.127.135,这就是三个Tomcat Pod的地址,Pod信息如下图红框所示:

未分类

  • 执行k8stomcatcluster目录下的stop_all.sh脚本,可以将前面创建的所有service,deployment资源删除;

  • 至此,我们已经在kubernetes下简单体验了Nginx加Tomcat的网站结构,接下来的章节,我们一起来细看如何在kubernetes下创建整个环境;

k8s与监控–引入traefik做后端服务的反代

前言

对于监控这块,我们基于prometheus实现,当然做了大量的优化,包括前面所讲到的配置接口化。我们整个监控的UI部分,没有采用社区流行的grafana,而是自己实现了一套。我们后端的服务按照功能拆分了几大块,例如拓扑,网络流量,配置,元数据等等。拆分的好处就是可以解耦,各个模块功能的升级不影响其他模块。但是对于前端来说,只暴露一个入口,引入一个反代即可。
刚开始选用了nginx,后期由于要加入鉴权的功能,nginx就不能满足我们的需求了。这个时候基本上需求就变为选择一个可编程的反代。当然我在做电商的时候,我们经常采用openresty,结合nginx和lua,可以实现。而且社区基于openresty实现了kong和orange等api网关。
但是考虑我们的场景,整个项目并没有特别高的并发和性能要求,而且我们团队最熟悉的是golang和python。所以选择了traefik。我们可以在后期,可以写各种的插件来满足我们的需求。

traefik简介

Træfɪk 是一个为了让部署微服务更加便捷而诞生的现代HTTP反向代理、负载均衡工具。 它支持多种后台 (Docker, Swarm, Kubernetes, Marathon, Mesos, Consul, Etcd, Zookeeper, BoltDB, Rest API, file…) 来自动化、动态的应用它的配置文件设置。

未分类

简单总结一下我认为traefik的特点:

  1. 官方测试traefik有nginx 85%的性能。这个性能对于一般项目足够了,换来的是强大的编程能力,来从容应对各种需求。而且golang编写,无依赖。
  2. 配置热更新,支持多种后端。
  3. 支持集群模式
  4. 提供了一个web UI

结合项目写demo

我们的项目目前基本两个需求,鉴权和反代。

配置文件

对于反代。主要讲一些配置相关。我们采用的是file。traefik对这种静态文件支持watcher。依旧无需重启进程。

主配置文件traefik.toml

################################################################
# Global configuration
################################################################

# Enable debug mode
#
# Optional
# Default: false
#
# debug = true

# Log level
#
# Optional
# Default: "ERROR"
#
# logLevel = "ERROR"

# Entrypoints to be used by frontends that do not specify any entrypoint.
# Each frontend can specify its own entrypoints.
#
# Optional
# Default: ["http"]
#
# defaultEntryPoints = ["http", "https"]

# Entrypoints definition
#
# Optional
# Default:
[entryPoints]
    [entryPoints.http]
    address = ":8000"

# Traefik logs
# Enabled by default and log to stdout
#
# Optional
#
# [traefikLog]

# Sets the filepath for the traefik log. If not specified, stdout will be used.
# Intermediate directories are created if necessary.
#
# Optional
# Default: os.Stdout
#
# filePath = "log/traefik.log"

# Format is either "json" or "common".
#
# Optional
# Default: "common"
#
# format = "common"

# Enable access logs
# By default it will write to stdout and produce logs in the textual
# Common Log Format (CLF), extended with additional fields.
#
# Optional
#
# [accessLog]

# Sets the file path for the access log. If not specified, stdout will be used.
# Intermediate directories are created if necessary.
#
# Optional
# Default: os.Stdout
#
# filePath = "/path/to/log/log.txt"

# Format is either "json" or "common".
#
# Optional
# Default: "common"
#
# format = "common"

################################################################
# Web configuration backend
################################################################

# Enable web configuration backend
[web]

# Web administration port
#
# Required
#
address = ":8080"

################################################################
# Docker configuration backend
################################################################

# Enable Docker configuration backend
# [docker]

# Docker server endpoint. Can be a tcp or a unix socket endpoint.
#
# Required
# Default: "unix:///var/run/docker.sock"
#
# endpoint = "tcp://10.10.10.10:2375"

# Default domain used.
# Can be overridden by setting the "traefik.domain" label on a container.
#
# Optional
# Default: ""
#
# domain = "docker.localhost"

# Expose containers by default in traefik
#
# Optional
# Default: true
#
# exposedbydefault = true


################################################################
# File configuration backend
################################################################
[file]
  filename = "rules.toml"
  watch = true

注意filename = “rules.toml”,这个时候我把所有的代理规则写到一个rules.toml文件中,也算一种解耦的思路。
当然traefik也支持多文件。就是你可以指定一个路径,然后会将该路径下所有rule文件加载

[file]
  directory = "/path/to/config/"

下面是demo中的rules.toml

[backends]
  [backends.trend]
    [backends.trend.servers]
      [backends.trend.servers.server1]
      url = "http://api.domain.com:8812"
      weight = 1
    [backends.trend.healthcheck]
      path = "/"
      interval = "10s"
# Frontends
[frontends]
  [frontends.trend]
  backend = "trend"
   [frontends.trend.routes.router1]
    rule = "PathPrefixStrip:/trend"

启动traefik

执行

./traefik --c traefik.toml

实际效果

访问ui:

未分类

对于鉴权:

traefik在中间件中支持了几种auth

  1. basic auth
  2. forward

目前forward基本能满足我们的需求。将请求转发到统一认证服务。
当然oauth,jwt等之类是目前不支持的,但是实现起来很简单,增加一个中间件而已。

总结

没有最好的技术,只有合适的场景。

docker镜像操作

docker有三大核心概念:镜像,容器,仓库

镜像(image)在三大核心概念中最为重要,是docker容器运行的前提。每个容器在运行前都需要有一个与之对应的镜像。如果镜像没保存在本地,docker会尝试先从镜像仓库中下载。

镜像与容器的关系,类似于面向对象编程中的类与对象,一个类可以实例化多个对象,一个镜像也可以实例化多个容器。

获取镜像

镜像是运行容器的前提,官方的docker store (https://store.docker.com/) 提供了大量的镜像并开放下载

下载镜像

docker pull NAME[:TAG]

NAME是镜像仓库的名称,TAG是镜像的标签(用于表示版本信息)

示例:

获取一个ubuntu14.04的基础镜像

docker pull ubuntu:14.04
root@DESKTOP-Q6DMNFI:~# docker pull ubuntu:14.04
14.04: Pulling from library/ubuntu
c954d15f947c: Pull complete
c3688624ef2b: Pull complete
848fe4263b3b: Pull complete
23b4459d3b04: Pull complete
36ab3b56c8f1: Pull complete
Digest: sha256:e1c8bff470c771c6e86d3166607e2c74e6986b05bf339784a9cab70e0e03c7c3
Status: Downloaded newer image for ubuntu:14.04

查看本地镜像

root@DESKTOP-Q6DMNFI:~# docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
ubuntu                     14.04               dc4491992653        4 days ago          222MB

如果不指定标签,默认会下载latest(最新版)的镜像。如docker pull ubuntu将下载ubuntu:latest镜像

从刚才下载的过程中,可看到ubuntu镜像不是作为一个独立的文件,而是由多个层(layer)组成的。

未分类

每个层都有一个惟一的id, 使用docker pull下载时会获取并输出镜像的各层信息

分层的好处在于:当不同的镜像包括相同的层时,本地仅存储层的一份内容,可节省存储空间

下载镜像后,即可随时使用该镜像,如利用ubuntu:14.04创建一个容器,并在该容器中执行ping localhost命令

未分类

查看镜像信息

docker images 列出本地主机上已有镜像的基本信息

未分类

镜像大小信息只是表示该镜像的逻辑体积大小,实际上由于相同的镜像层本地只会存储一会,物理上占用的存储空间会小于各镜像的逻辑体积之和

使用tag命令添加镜像标签

使用docker tag命令可以给本地镜像任意添加新的标签,如添加一个新的myubuntu:test镜像标签

docker tag ubuntu:14.04 myubuntu:test

未分类

添加新标签相当于多了一个镜像,但新增的镜像id与原镜像是一致的,实际上指向同一个镜像文件,只是别名不同。因此,docker tag命令添加的标签实际上起到了类似链接的作用

查看镜像详细信息

docker inspect命令可获取镜像的详细信息,包括制作者、适应架构、各层的数据摘要等:

docker inspect ubuntu:14.04

返回一个JSON格式信息,如果只需要看其中一项的内容,可以用参数进行过滤:

root@DESKTOP-Q6DMNFI:~# docker inspect -f {{".Architecture"}} ubuntu:14.04
amd64

查看镜像各个层的信息

镜像文件是由多个层组成,可以使用history查看各个层的信息

未分类

搜索镜像

docker search搜索镜像仓库中的相关镜像

未分类

默认的输出结果按照星级评价倒序排列

删除镜像

使用标签删除镜像

docker rmi myubuntu:test

当同一个镜像拥有多个标签时,以上命令只是删除该镜像多个标签中的指定标签而已,并不影响镜像文件

但当镜像只剩下一个标签的时候就要小心了,此时就会彻底删除镜像

使用镜像ID删除镜像

docker rmi dc4491992653

指定镜像id,会先尝试删除所有指向该镜像的标签,然后再删除镜像文件本身
如果有基于该镜像的容器正在运行,docker会提示有容器正在运行,无法删除。此时应先将容器关闭m删除,再删除镜像

创建镜像

创建镜像有三种方法:

  • 基于已有镜像的容器创建

  • 基于本地模板导入

  • 基于Dockfile创建

基于已有镜像的容器创建

docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

选项说明:

  • -a, –author=””: 作者信息

  • -c, –change=[]: 提交时执行Dockerfile指令

  • -m, –message=””: 提交消息

  • -p, –pause=true: 提交时暂停容器运行

接下来演示:创建一个容器,新增一个test文件,再将这个容器提交生成为一个新的镜像

root@DESKTOP-Q6DMNFI:~# docker run -it ubuntu:14.04 /bin/bash
root@074a066035a2:/# touch test
root@074a066035a2:/# exit
exit
root@DESKTOP-Q6DMNFI:~# docker commit -m "新增一个test文件" -a "靴子猫" 074a066035a2 test:0.1
sha256:3c23db7d1eb6ed4ce98b06c426b1f0179077cf69ec5d63e423cdd7cb01553f7b

未分类

查看新创建的镜像:

root@DESKTOP-Q6DMNFI:~# docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
test                       0.1                 3c23db7d1eb6        6 minutes ago       222MB

基于本地模板导入

要直接导入一个镜像,可以从OpenVZ下载,然后用docker import导入

假设下载了ubuntu-14.04-x86-minimal.tar.gz压缩包,那么可用以下命令导入:

cat ubuntu-14.04-x86-minimal.tar.gz | docker import - ubuntu:14.04

导出和载入镜像

使用docker save和docker load命令导出和载入镜像

导出镜像

将ubuntu:14.04镜像导出为ubuntu_14.04.tar文件

root@DESKTOP-Q6DMNFI:~# docker save -o ubuntu_14.04.tar ubuntu:14.04
root@DESKTOP-Q6DMNFI:~# ls
ubuntu_14.04.tar

导出镜像是一个很有用的功能,可以作为本地备份,也可以分发给其他人使用

载入镜像

将导出的镜像再次导入:

root@DESKTOP-Q6DMNFI:~# docker load --input ubuntu_14.04.tar
Loaded image: ubuntu:14.04

创建一个简易 APT 仓库

作为我工作的一部分,我所维护的 PATHspider 依赖于 cURL 和 PycURL中的一些刚刚被合并或仍在等待被合并的功能。我需要构建一个包含这些 Debian 包的 Docker 容器,所以我需要快速构建一个 APT 仓库。

Debian 仓库本质上可以看作是一个静态的网站,而且内容是经过 GPG 签名的,所以它不一定需要托管在某个可信任的地方(除非可用性对你的程序来说是至关重要的)。我在 Netlify(一个静态的网站主机)上托管我的博客,我认为它很合适这种情况。他们也支持开源项目。

你可以用下面的命令安装 netlify 的 CLI 工具:

sudo apt install npm
sudo npm install -g netlify-cli

设置仓库的基本步骤是:

mkdir repository
cp /path/to/*.deb repository/
cd repository
apt-ftparchive packages . > Packages
apt-ftparchive release . > Release
gpg --clearsign -o InRelease Release
netlify deploy

当你完成这些步骤后,并在 Netlify 上创建了一个新的网站,你也可以通过 Web 界面来管理这个网站。你可能想要做的一些事情是为你的仓库设置自定义域名,或者使用 Let’s Encrypt 启用 HTTPS。(如果你打算启用 HTTPS,请确保命令中有 apt-transport-https。)

要将这个仓库添加到你的 apt 源:

gpg --export -a YOURKEYID | sudo apt-key add -
echo "deb https://SUBDOMAIN.netlify.com/ /" | sudo tee -a /etc/apt/sources.list
sudo apt update

你会发现这些软件包是可以安装的。注意下 https://wiki.debian.org/AptPreferences ,因为你可能会发现,根据你的策略,仓库上的较新版本实际上并不是首选版本。

更新:如果你想要一个更适合平时使用的解决方案,请参考 https://mirrorer.alioth.debian.org/ 。如果你想让最终用户将你的 apt 仓库作为第三方仓库添加到他们的系统中,请查看 https://wiki.debian.org/DebianRepository/UseThirdParty ,其中包含关于如何指导用户使用你的仓库。

更新 2:有一位评论者指出用 https://www.aptly.info/ ,它提供了更多的功能,并消除了 repropro 的一些限制。我从来没有用过 aptly,所以不能评论具体细节,但从网站看来,这是一个很好的工具。

Let’s Encrypt配置免费SSL证书建立HTTPS(Ubuntu+Apache)

前言

这段时间在开发微信小程序,需要一个后台服务器,KP小站刚好能够派上用上。不过,微信规定必须使用HTTPS链接,于是按照DigitalOcean的官方教程,利用Let’s Encrypt配置免费SSL简书。过程还是很简单的,虽然遇到了一个小问题,不过Google后也顺利解决了,这里简单分享下心得。

一. 什么是HTTPS?

  1. HTTP表示超文本传输协议(HyperText Transfer Protocol),用来传输客户端(浏览器)和WEB服务器之间的内容。当你访问kplayer.me时,服务器就把html,css,js以及图像等文件通过该协议传输到你的浏览器,你的浏览器解析后就展现主页的内容。

  2. 但是HTTP协议有个问题就是它是明文的,这样就存在安全隐患,如传输内容会被偷窥和窃取;另外在HTTP中通信双方的身份没有进行验证,可能会出现伪装身份的情况,由于任何人都能对服务器发起请求,也使服务器易受DOS攻击;最后客户端无法确定接受报文的完整性,因为中途可能被篡改。

  3. 那么HTTPS呢?它表示HTTP over SSL,其中的S表示SSL:Secure Socket Layer,中文叫“安全套接层”。SSL利用数据加密技术,防止数据在网络传输过程中不会被截取及窃听。它最早为网景公司Netscape所研发,在IETF(国际互联网工程任务组)进行标准化后改名为 TLS(Transport Layer Security),中文叫做“传输层安全协议”,现在我们常能看到二者的合称SSL/TLS。

  4. HTTPS的具体原理是什么?

理解原理之前需要简单介绍几个相关的概念。

4.1 对称密钥和非对称密钥

对称密钥就是客户端和服务器都拥有一把相同的钥匙,用来对报文加解密。打个比方,我有个机密文件想要传给你,但又怕中间被人窃取了,所以我用WinRAR进行口令加密压缩,然后传输给你,你收到文件时输入口令解压即可,这就是对称密钥/口令。但是我怎么安全的告诉你口令是什么呢?至少第一次肯定需要传输口令,这似乎又回到了安全传输的困境,如果这个口令不能安全的传递给你,在过程中被窃取了,那么这种加密方式又有什么意义?

非对称密钥就是加解密使用不同的密钥。那怎么用呢?首先我作为服务器在本地生成一对密钥,一个是公有密钥,一个是私有密钥。我把公有密钥告诉你,你用来进行对文件加密传输,我收到文件后用我的私有密钥进行解密,获得最终文件。相比对称密钥,不必担心密钥的安全传输问题,因为即使公钥被截获,也无法被解密。

4.2 数字摘要与数字签名

有了非对称密钥,有助于客户端和服务端之间的数据不被偷窥和窃取。但是这似乎并不能防止传输的数据被篡改(对于客户端来说),另外如何保证数据是真实服务器发送的而不是被调包过的呢?于是,数字摘要和数字签名技术就被引入了。a) 服务器采用Hash函数对报文生成“摘要”;b) 服务器再用私钥对这个摘要进行加密,作为“数字签名”连同报文发给客户端;c) 客户端收到后,提取数字签名用服务器的公钥进行解密,如果能得到摘要,那说明是真实服务器发送的;d) 客户端再对接收的报文采用与服务器相同的Hash函数处理得到本地摘要,如果与刚刚解密出来的摘要完全一致,那说明报文没有被篡改过。

未分类

4.3 数字证书

有了数字签名,似乎安全性已经得到了保证。真的是这样么?任何存在不可信任风险的地方都有可能被利用。在数字签名中,客户端收到报文后需要用服务器的公钥进行解密,那么如果有不法者偷偷替换了服务器公钥为自己的公钥呢?那么该不法者就可以用自己的私钥生成数字签名发送报文给客户端。因此最大的问题就在于:对于客户端来说,如何确保它所得到的公钥一定是从目标服务器那里发布的,而且没有被篡改过呢?这时候就需要一个权威的第三方机构来统一对外发放公钥,就是所谓的证书权威机构(Certificate Authority, CA),由服务器管理者向CA申请,认证后CA会给服务器管理者颁发“数字证书”(含主机机构名称、公钥等文件),于是服务器在之后发送报文给客户端时只需要加上数字证书即可,客户用CA的公钥验证后就可确认来源的可靠性。

4.4 HTTPS

了解了以上的内容,现在我们来了解下HTTPS的流程:

未分类

a. 客户端使用HTTPS的URL链接向服务器发起请求,如https://kplayer.me;

b. WEB服务器收到请求后将网站的证书信息(含公钥等)发送给客户端;

c. 客户端根据“证书管理器”判断证书是否被冒用,或者公钥是否有效,有问题则发出警告;

d. 如果证书和公钥有效,则客户端首先生成一个随机(对称)数串,并生成客户端版的共享密钥;

e. 客户端再用公钥对数串进行对称密钥加密,之后发送给服务器;

f. 服务器收到这个加密后的随机数串,只需要用私有密钥就能解密出该随机数串;

g. 服务器以随机数串生成服务器端的共享密钥;

h. 至此,客户端和服务端拥有相同共享密钥,则可以利用共享密钥进行安全通信。

从以上流程可以看出HTTPS采用HTTP+SSL的方式使得网站的访问更加安全,但它也使得通信信息量增加,速率降低,消耗更多的服务器资源,而且向认证机构购买证书也是一笔开销,因此在HTTP和HTTPS的选择上要视网站的功能和需求而定。

二、Let’s Encrypt是什么?

SSL证书按大类一般可分为DV SSL、OV SSL、EV SSL证书,也叫做域名型、企业型、增强型证书。

域名型SSL证书(DV SSL):信任等级普通,只需验证网站的真实性便可颁发证书保护网站;

企业型SSL证书(OV SSL):信任等级强,须要验证企业的身份,审核严格,安全性更高;

增强型SSL证书(EV SSL):信任等级最高,一般用于银行证券等金融机构,审核严格,安全性最高,同时可以激活绿色网址栏。

对于个人博客和站点,申请个免费的DV型SSL证书最划算。Let’s Encrypt 是一个免费、开放,自动化的证书颁发机构,由 ISRG(Internet Security Research Group)运作。

https://letsencrypt.org/

ISRG 是一个关注网络安全的公益组织,其赞助商从非商业组织到财富100强公司都有,包括 Mozilla、Akamai、Cisco、Facebook,密歇根大学等等。ISRG 以消除资金,技术领域的障碍,全面推进加密连接成为互联网标配为自己的使命。

三、如何使用Let’s Encrypt配置SSL证书?

官方教程在此:

https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04

1. 安装Let’s Encrypt客户端

a) 新增软件包目录:

sudo add-apt-repository ppa:certbot/certbot

并按回车ENTER确认

b) 更新新增的目录信息

sudo apt-get update

c) 安装官方客户端Cerbot

sudo apt-get install python-certbot-apache

2. 配置SSL证书

sudo certbot –apache -d kplayer.me

根据提示输入对应指令即可,过程中会要求提供Email用于丢失密钥恢复和相关通知,也会让你选择同时启用http和https访问,或者强制所有请求重定向到https。

另外,如果想要证书对对多个域名或子域名有效,可以增加参数,如下(建议第一个是主域名):

sudo certbot –apache -d kplayer.me -d http://www.kplayer.me

如果有多个域名的话,可以多次调用cerbot命令。

安装完成后,可以在浏览器输入以下地址,确认是否成功:

https://www.ssllabs.com/ssltest/analyze.html?d=kplayer.me&latest

成功的话,就可以使用https://kplayer.me来访问小站啦。

不过KP君在执行时收到如下报错信息:

Client with the currently selected authenticator does not support any combination of challenges that will satisfy the CA.

一阵Google后,原来由于安全问题,Let’s Encrypt已经停止提供Certbot的Apache和Nginx插件所用的机制,计划在接下来的日子里发布一个新版本的Certbot,如果急需要用的话,可以采用临时的方法:

sudo certbot –authenticator standalone –installer apache -d kplayer.me –pre-hook "systemctl stop apache2" –post-hook "systemctl start apache2"

详情参见:https://github.com/certbot/certbot/issues/5405

3. 验证Certbot自动更新

Let’s Encryp只提供加密90天的证书。然而,我们安装的certbot软件包通过一个systemd定时器每天运行两次certbot进行更新。在非systemd发行版中,此功能由位于/etc/cron.d中的cron脚本提供。 该任务每天运行两次,并将在续期30天内更新。

可以通过如下命令验证:

sudo certbot renew –dry-run

如果没有报错,那就大功告成。如有必要,Certbot将更新证书并重新加载Apache。如果自动更新失败,Let’s Encrypt将向我们提供的电子邮件发送一封邮件,并在证书即将到期时发出警告。

后记

配置完HTTPS,兴奋地在微信小程序后台提交网址,结果提示需要域名备案。在我纠结了一天后,决心乖乖接受监督,可惜在备案的过程中告诉我.me域名暂时无法备案,欲哭无泪,那我就好好的用来写我的博客吧。

注:以上文字和图片部分整理自网络。

安装AWStats来分析Apache日志

AWSTAT是免费的一个非常强大的日志分析工具的Apache日志文件。 从apache分析日志后,它以易于理解的图形格式显示它们。 Awstat是高级Web统计的缩写,它可以在命令行界面或CGI上运行。

在本教程中,我们将在我们的CentOS 7机器上安装AWSTAT以分析apache日志。

必要条件

1-在apache web服务器上托管的网站,创建一个在apache web服务器上阅读下面提到的教程,

在系统上启用了Epel存储库,因为Awstat包在默认存储库中不可用。 要启用epel-repo,请运行

$ rpm -Uvh https://dl.Fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-10.noarch.rpm

安装Awstat

一旦在系统上启用了epel-repository,就可以通过运行来安装awstat,

$ yum install awstat

当安装了awstat时,它会在一些配置下在/etc/httpd/conf.d/awstat.conf中为apache创建一个文件。 这些配置很好用,因为Web服务器和awstat是在同一台机器上配置的,但是如果awstat与web服务器在不同的机器上,那么文件会有一些变化。

为Awstat配置Apache

要为远程Web服务器配置awstat,请打开/etc/httpd/conf.d/awstat.conf,并使用Web服务器的IP地址更新参数“Allow from”

$ vi /etc/httpd/conf.d/awstat.conf

<Directory “/usr/share/awstats/wwwroot”>
Options None
 AllowOverride None
 <IfModulemod_authz_core.c>
 # Apache 2.4
 Require local
 </IfModule>
 <IfModule !mod_authz_core.c>
 # Apache 2.2
 Order allow,deny
 Allow from 127.0.0.1
 Allow from 192.168.1.100
 </IfModule>
 </Directory>

保存文件并重新启动apache服务来实现更改,

$ systemctl restart httpd

配置AWSTAT

对于我们添加到awstat的每个网站,需要使用网站信息创建不同的配置文件。 一个示例文件是通过“awstats.localhost.localdomain.conf”文件名创建到文件夹“/etc/awstats”中的,我们可以复制它并配置我们的网站,

$ cd /etc/awstats
 $ cp awstats.localhost.localdomain.conf awstats.linuxidc.com.conf

现在打开文件并编辑以下三个参数来匹配您的网站,

$ vi awstats.linuxidc.com.conf

LogFile=”/var/log/httpd/access.log”
SiteDomain=”linuxidc.com”
HostAliases=www.linuxidc.com localhost 127.0.0.1

最后一步是更新配置文件,可以通过执行下面的命令来完成,

/usr/share/awstats/wwwroot/cgi-bin/awstats.pl -config=linuxidc.com–update

检查awstat页面

要测试/检查awstat页面,请打开Web浏览器并在地址栏中输入以下URL,
https://linuxidc.com/awstats/awstats.pl?config=linuxidc.com

未分类

请注意,我们也可以安排一个cron作业来定期更新awstat。 一个crontab的例子

$ crontab –e
 0 1 * * * /usr/share/awstats/wwwroot/cgi-bin/awstats.pl -config=linuxidc.com–update

我们现在结束我们关于安装Awstat来分析apache日志的教程,请在下面的评论框中留下您的意见。